Totolink NR1800X漏洞复现

新发布漏洞分析固件模拟
2026-01-30 13:36
5889

信息收集

下载对应版本固件NR1800X_V9.1.0u.6279_B20210910.bin:https://www.totolink.net/home/menu/detail/menu_listtpl/download/id/225/ids/36.html
binwalk提取固件:
image.png
file命令查看固件文件信息,该固件基于 MIPS 小端序 架构
image.png
checksec命令查看文件保护机制,关闭了所有现代安全防护机制(无栈保护、栈段可执行、无地址随机化)

image.png

固件模拟

FirmAE 模拟失败
image.png
使用系统模拟:
● 镜像文件:debian_squeeze_mipsel_standard.qcow2
● 内核文件:vmlinux-3.2.0-4-4kc-malta
image.png
创建虚拟网卡(tap):

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

启动 QEMU 模拟器:

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

QEMU 模拟器设置IP

ifconfig eth0 192.168.10.2 netmask 255.255.255.0

挂载必要内核接口

cd squashfs-root/
mount -t proc /proc ./proc
mount -o bind /dev ./dev
chroot . /bin/sh

该路由器的web服务由/usr/sbin/lighttpd管理,“尝试”直接在根目录启动
提示需要config,搜索后发现在固件包中有现成的/lighttp/lighttpd.conf配置文件,直接加载就行lighttpd -f /lighttp/lighttpd.conf:
image.png
显示报错,手动创建缺失的目录

mkdir -p /var/run
mkdir -p /var/log
mkdir -p /var/tmp

再次尝试启动

lighttpd -f /lighttp/lighttpd.conf

image.png
成功启动,在宿主机浏览器访问: 打开浏览器,输入虚拟机的 IP 地址: http://192.168.10.2

image.png
模拟成功。

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

登陆页面随意输入,burp抓取登录时的数据包:
image.png
IDA打开cstecgi.cgi,搜索formLoginAuth.htm字符串,根据响应包的Location可以确定是使用了第三个:

image.png
交叉引用,定位到sub_42AEEC,我们将这个函数重命名为check,该函数的核心作用是:作为登录验证的后端接口,它解析前端提交的原始登录字符串,将其与系统 NVRAM 中存储的账号密码进行比对,并根据验证结果构造包含身份标志位 authCode 的重定向 URL,引导浏览器跳转至 Web 服务器(lighttpd)进行 Session 会话建立 。

int __fastcall check(int a1)
{
  // ... 变量声明 ...
  
  // 初始化缓冲区
  memset(login_ie.html_, 0, sizeof(login_ie.html_));
  // ... 省略部分 memset ...

  // 1. 获取前端传入的原始登录字符串(通常是 key1=val1&key2=val2 格式)
  Var = websGetVar(a1, "loginAuthUrl", (int)"");
  Object = cJSON_CreateObject();
  
  // 2. 将 URL 格式字符串解析并填充到 cJSON 对象中,方便后续读取
  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); // 存入 cJSON: { "username": "xxx", "password": "yyy" }
    }
    v3 = v5;
  }

  // 3. 提取用户输入的各字段
  v6 = websGetVar(Object, "username", (int)"");
  v7 = websGetVar(Object, "password", (int)"");
  v8 = websGetVar(Object, "http_host", (int)"");
  v9 = websGetVar(Object, "flag", (int)&word_4370EC); // 区分 PC 端或手机端
  v10 = websGetVar(Object, "verify", (int)&word_4370EC);
  v11 = atoi(v10);

  // 4. 验证码与登录向导(Wizard)逻辑处理
  int = nvram_get_int("wizard_flag");
  // ... 略去向导状态判断逻辑 ...

  // 5. 解码密码并获取系统真实的账号密码进行对比
  urldecode(v7, v35); // 关键:对输入的密码进行 URL 解码
  v14 = nvram_safe_get("http_username"); // 获取 NVRAM 中存储的真用户名
  strcpy(v30, v14);
  v15 = nvram_safe_get("http_passwd");   // 获取 NVRAM 中存储的真密码
  strcpy(v32, v15);

  // 6. 核心比对环节
  v17 = strcmp(v6, v30);                  // 比对用户名
  // v18 是 authCode 的逻辑基础:如果用户名或密码不对,v18 保持非 0
  v18 = strcmp(v35, v32) || v17 != 0;

  if ( !strcmp(v6, v30) && !strcmp(v35, v32) ) // 验证通过
  {
    // 根据 flag 选择跳转的页面 (home.html 或 wizard.html)
    // ... 页面分配逻辑 ...
    
    // 触发云端更新检查等系统行为
    nvram_set_int_temp("cloudupg_checktype", 1);
    doSystem("lktos_reload %s", "cloudupdate_check 2>/dev/null");
    
    v18 = 1; // 登录成功的 authCode 标志
  }
  else // 验证失败
  {
    // ... 分配登录失败后的回退页面 ...
    if ( v18 )
    {
LABEL_54:
      // 如果登录失败次数多或验证码错误,创建标志文件
      system("echo ''> /tmp/login_flag");
      v18 = 0; 
    }
  }

  // 7. 构造响应 JSON
  // 这里构造了 redirectURL,注意 authCode=%d 处填入了 v18
  // 这个 authCode 会被传递给后续的 lighttpd 处理,用于建立 Session
  snprintf(v24, 4096, "{\"httpStatus\":\"%s\",\"host\":\"%s\"", "302", v29);
  // ... 拼接 redirectURL 字符串 ...
  
  // 8. 解析构造好的 JSON 并提取 URL 执行重定向
  v20 = cJSON_Parse(v24);
  v21 = websGetVar(v20, "redirectURL", (int)"");
  
  // 输出标准的 HTTP 302 响应头部
  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); // 浏览器收到后会跳转到 formLoginAuth.htm
  printf("protal page");
  
  return 0;
}

下一个数据包:
经过cstecgi.cgi会得到http://192.168.10.2/formLoginAuth.htm?authCode=0&userName=&goURL=login.html&action=login,
由于目标文件formLoginAuth.htm为htm文件,所以需要交由lighttpd处理。对lighttpd逆向搜索"formLoginAuth.htm",来到userloginAuth函数:
image.png
userloginAuth 是路由器 Web 服务的权限验证入口分发函数。它负责识别当前的 URL 请求类型,并将其导向登录、注销或常规会话检查逻辑 。

BOOL __fastcall userloginAuth(int a1, int a2, _BYTE *a3)
{
  int v4; // $s2

  // 1. 从请求结构体偏移 320 处提取当前请求的路径/文件名 (URI) [cite: 295, 296]
  v4 = **(_DWORD **)(a1 + 320);

  // 2. 检查请求是否包含 "formLoginAuth.htm"
  // 这是登录验证的核心接口,攻击者通过构造含有该字符串的 URL 触发此逻辑 [cite: 296, 351]
  if ( strstr(v4, "formLoginAuth.htm") )
  {
    // 调用 Form_Login 函数进行具体的参数解析与会话生成 
    // 关键点:该函数内存在直接解析 URL 参数 authCode 的漏洞 [cite: 346, 371]
    Form_Login(a1, a2, (int)a3);
    return 1;
  }

  // 3. 处理注销请求逻辑
  if ( strstr(v4, "formLogout.htm") )
  {
    Form_Logout(a1, a2, a3);
    return 1;
  }

  // 4. 处理注销所有会话请求逻辑
  if ( strstr(v4, "formLogoutAll.htm") )
  {
    Form_LogoutAll(a1, a2);
    return 1;
  }

  // 5. 数据结构清理:初始化或清除 a3 指向的内存空间 [cite: 312, 313, 314]
  a3[3] = 0;
  *a3 = 0;
  a3[1] = 0;
  a3[2] = 0;

  // 6. 默认权限检查:对于非登录/注销页面,通过 checkLoginUser 检查当前 Cookie 是否有效 
  // 如果 checkLoginUser 返回 1,表示用户已授权;否则拒绝访问 
  return checkLoginUser(a1, a2) == 1;
}

查看Form_Login 函数, 这段函数是 lighttpd 内部处理用户登录逻辑的核心代码

试读结束,发布七天后转为公开

公开时间:2026年2月6日 13:36:11

提前解锁全文,需花费 50 积分

登录解锁全文
分享到

参与评论

0 / 200

全部评论 0

暂无人评论
投稿
签到
联系我们
关于我们