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
An overview of SMB
ZDI-23-979 analysis and proof-of-concept
ZDI-23-980 analysis and proof-of-concept
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.
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).
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.
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:
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.
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:
But it does not assert work->next_smb2_rcv_hdr_off + hdr->NextCommand <= get_rfc1002_len(work->request_buf), which is the official patch.
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:
To leak 2 bytes using SMB_ECHO:
Set smb2_echo_req->StructureSize = p16(4)
Set smb2_echo_req->hdr.NextCommand = sizeof(smb2_echo_req->hdr) + smb2_echo_req->StructureSize
Read echo response, with the last 2 bytes being an OOB read.
For the SMB_WRITE path, here's the struct and the steps:
Set smb2_write_req->StructureSize = 49
Set smb2_write_req->DataOffset = smb2_write_req->StructureSize + 64 to start reading the content without the packet
Set smb2_write_req->Length = 65536 to write 65536 bytes from the packet to the file
Set smb2_write_req->hdr.NextCommand = smb2_write_req->Length + smb2_write_req->DataOffset to spoof the request size
Open a file in the SMB share in read/write mode: file_id = smb_open("dump.bin", "rw")
Set smb2_write_req->PersistentFileId = file_id
Send the request
Read the file in the SMB share: dump = smb_read(file_id)
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].