–由kimi翻译
寻找华硕路由器中的未授权 N-day 漏洞
摘要
在阅读了网上关于几个影响华硕路由器的已公布关键CVE的详细信息后,我们决定分析易受攻击的固件,并可能编写一个N-day漏洞利用程序。虽然我们确定了易受攻击的代码片段,并成功编写了一个利用程序来获得远程代码执行(RCE),我们还发现在现实世界的设备中,报告的漏洞的“未认证远程”属性并不总是成立,这取决于设备的当前配置。
引言
去年是物联网(IoT)和路由器安全方面非常出色的一年。许多设备被攻破,许多CVE被发布。由于@suidpit和我https://github.com/TheZ3ro 喜欢通过逆向工程研究物联网设备,而且这些CVE并没有太多的公开细节或概念验证,我们有机会应用CVE North Stars 方法,由clearbluejar提出。
特别是,我们选择了以下影响各种华硕SOHO路由器的CVE:
-
CVE-2023-39238
-
CVE-2023-39239
-
CVE-2023-39240
CVE描述中的声明相当大胆,但我们回想起几个月前发布的同一设备上的一些CVE(例如CVE-2023-35086),它们描述了完全相同场景下的其他格式字符串:
“未认证的远程攻击者可以利用这个漏洞无需特权执行远程任意代码执行”
请仔细注意这些声明,因为它们将是我们从现在开始所有假设的基础!
从CVE的详细信息中,我们可以推断出一些有趣的信息,例如受影响的设备和版本。以下是每个设备包含补丁的固件版本:
-
Asus RT-AX55: 3.0.0.4.386_51948 or later
-
Asus RT-AX56U_V2: 3.0.0.4.386_51948 or later
-
Asus RT-AC86U: 3.0.0.4.386_51915 or later
此外,我们可以了解到这个漏洞据说是一个格式字符串问题,受影响的模块是 set_iperf3_cli.cgi
、 set_iperf3_srv.cgi
和 apply.cgi
。
由于我们之前没有处理过华硕设备的经验,我们首先从供应商的网站上下载了易受攻击和已修复的固件版本。
用 bindiff 来 Patch Diffing
一旦我们获得了固件,我们使用Unblob进行了提取。
通过快速使用 find
/ ripgrep
搜索,我们发现受影响的模块并不是像人们期望的那样是CGI文件,而是编译后在 /usr/sbin/httpd
二进制文件内处理的函数。
然后我们在Ghidra中加载了新的和旧的httpd二进制文件,分析了它们,并使用BinDiff的BinExport导出了相关信息,以执行补丁差异比较。
补丁差异比较是将一个二进制的易受攻击版本与一个已打补丁的版本进行比较。目的是突出显示变化,帮助发现不同版本二进制文件中的新功能、缺失功能和有趣的功能。
对 httpd
二进制文件进行补丁差异比较突出了一些变化,但都没有对我们的目的产生兴趣。特别是,如果我们看看易受攻击CGI模块的处理程序,可以看到它们根本没有变化。
有趣的是,它们都有一个共同的模式。notify_rc
函数的输入没有被修复,而是来自用户控制的JSON请求。
notify_rc
函数定义在 /usr/lib/libshared.so
中:这解释了为什么对 httpd
二进制文件进行差异比较是无效的。
对 libshared.so
进行差异比较得到了一个不错的发现:在 notify_rc
函数的前几行,增加了一个名为 validate_rc_service
的新函数调用。在这一点上,我们相当有信心这个函数是负责修补格式字符串漏洞的。
validate_rc_service
函数对 rc_service
JSON字段进行语法检查。Ghidra反编译的代码不容易阅读:基本上,如果 rc_service
字符串只包含字母数字、空格或下划线 _
和分号 ;
字符,函数返回1,否则返回0。
显然,在我们的易受攻击固件中,我们可以通过控制 rc_service
字段中的内容来利用格式字符串漏洞。我们还没有设备来确认这一点,但我们不想花时间和金钱,以防这是一个死胡同。让我们模拟吧!
用 Qiling 来模拟运行
如果了解我们,我们打赌你知道我们喜欢Qiling,所以我们的第一个想法是:“如果我们尝试用Qiling模拟固件并在其中重现漏洞会怎样?”
从Qiling骨架项目开始,遗憾的是 httpd
崩溃并报告了各种错误。
特别是,华硕设备使用NVRAM外设来存储许多配置。firmadyne的团队开发了一个库来模拟这种行为,但我们无法让它工作,所以我们决定在我们的Qiling脚本中重新实现它。
脚本在堆中创建一个结构,然后劫持 httpd
用于读写NVRAM的所有函数,将它们重定向到堆结构。
之后,我们只需要修复一些次要的系统调用实现和钩子,然后瞧!我们可以从我们的浏览器加载模拟的路由器Web界面。
与此同时,我们对 do_set_iperf3_srv_cgi
/ do_set_iperf3_cli_cgi
函数进行了逆向工程,以了解我们应该发送什么样的输入来配合格式字符串。
结果发现,以下JSON就是你利用 set_iperf3_srv.cgi
端点所需要的全部内容:
{
'iperf3_svr_port': '8888',
'rc_service': '%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p'
}
在Qiling控制台中,我们得到了这样的输出:
此时,格式字符串漏洞得到了确认,我们知道如何通过Qiling的固件模拟来触发它。此外,我们知道修复措施在 libshared.so
共享库导出的 notify_rc
函数中引入了对 validate_rc_message
的调用。为了编写一个针对真实设备的工作n-day,我们购买了目标设备之一(华硕RT-AX55),并开始分析漏洞,以理解其根本原因以及如何控制它。
根本原因分析
由于修复措施被添加到了 notify_rc
函数中,我们首先对旧版易受攻击版本中该函数的汇编进行了逆向工程。以下是该函数的伪代码片段:
int notify_rc(char*message)
{
// ...
pid = getpid();
psname(pid,proc_name,0x10);
pid = getpid();
cprintf("<rc_service> [i:%s] %d:notify_rc %sn",proc_name,pid,message);
pid = getpid();
logmessage_normal("rc_service","%s %d:notify_rc %s",proc_name,pid,message);
// ...
}
这个函数似乎负责通过单一的集中输出点记录来自不同地方的消息。
logmessage_normal
函数是同一库的一部分,其代码非常简单,易于逆向工程:
void logmessage_normal(char*logname, char*fmt, ...)
{
char buf [512];
va_list args;
va_start(args, fmt);
vsnprintf(buf,0x200,fmt_string,args);
openlog(logname,0,0);
syslog(0,buf); // buf can be controlled by the user!
closelog();
va_end(args);
return;
Ghidra似乎无法自动识别可变参数列表,但这个函数是 syslog
的一个包装器,它负责打开选定的日志,发送消息,最后关闭它。
漏洞就在这个函数中,准确地说是在使用 syslog
函数时,这个函数使用的字符串可以被攻击者控制。为了理解为什么,让我们检查一下libc手册中它的签名:
[ void syslog(int priority, const char *format, …); ]
根据它的签名, syslog
期望一个类似于 *printf
系列的参数列表。快速搜索显示,实际上,这个函数是已知的格式字符串漏洞的汇聚点。
利用 – 利用现有资源的过程
格式字符串漏洞对攻击者来说非常有用,它们通常提供任意读写原语。在这种情况下,由于输出记录在只有管理员才能看到的系统日志中,我们假设未认证的远程攻击者不应该能够读取日志,因此失去了漏洞利用的“读取”原语。
路由器的操作系统上启用了ASLR,编译时为二进制文件实现的缓解措施如下所示:
Arch: arm-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x10000)
根据这种情况,开发一个漏洞利用程序的典型方式将包括寻找一个适合进行GOT覆盖的目标,尝试找到一个接受用户控制输入的函数,并将其劫持到 system
。
然而,以纯粹的利用现有资源的方式,我们花了一些时间寻找另一种方法,这种方法不会破坏进程内部结构,而是利用二进制文件中已经实现的逻辑来获得一些好处(即,一个shell)。
在二进制文件中首先要寻找的地方是调用 system
函数的地方,希望找到好的注入点,以引导我们强大的写原语。
在这次搜索的多个结果中,有一段代码看起来值得进一步调查:
void sys_script(char*script)
{
int cmp;
char*pcVar1;
char buf [64];
char*cmd;
undefined4 local_10c;
snprintf(buf,0x40,"/tmp/%s",script);
cmp = strcmp(script,"syscmd.sh");
if(cmp == 0) {
if(SystemCmd[0] != ' ') {
snprintf((char*)&cmd,256,
"%s > /tmp/syscmd.log 2>&1 && echo 'XU6J03M6' >> /tmp/syscmd.log &n",SystemCmd);
system((char*)&cmd);
strlcpy(SystemCmd,&DAT_0007e451,0x80);
return;
}
f_write_string("/tmp/syscmd.log",&DAT_0007e451,0);
return;
}
// ...
}
让我们简要评论这段代码以理解重要点:
-
SystemCmd
是一个全局变量,它保存了一个字符串。 -
当
sys_script
被调用并传入syscmd.s
参数时,它会将SystemCmd
中存在的任何命令传递给system
函数,然后它将再次清零这个全局变量。
这似乎是一个利用漏洞的好目标,只要我们作为攻击者能够:
-
覆盖
SystemCmd
的内容。 -
触发
sys_script("syscmd.sh")
函数。
第1点由格式字符串漏洞提供:由于二进制文件不是位置无关的, SystemCmd
全局变量的地址在二进制文件中是硬编码的,所以我们不需要泄露就可以写入它。在我们易受攻击的固件中, SystemCmd
全局变量的偏移是 0x0f3ecc
。
关于第2点,Web UI中的一些端点被用来通过 sys_script
函数合法执行命令。当执行GET请求时,这些端点将调用以下名为 ej_dump
的函数:
int ej_dump(int eid,FILE*wp,int argc,char**argv)
{
// ...
ret = ejArgs(argc,argv,"%s %s",&file,&script);
if(ret < 2) {
fputs("Insufficient argsn",wp);
return-1;
}
ret = strcmp(script,"syscmd.sh");
if(ret == 0) {
sys_script(script);
}
// ...
}
一旦 SystemCmd
全局变量被覆盖,只需访问 Main_Analysis_Content.asp
或 Main_Netstat_Content.asp
就会触发我们的漏洞利用程序。
为你的思想提供一个Shell
我们将省略格式字符串利用基础,只需记住 %n
可以将到目前为止写入的字符数写入其偏移量指向的地址。
结果我们遇到了一些限制,其中一些是格式字符串利用的典型问题,而另一些是特定于我们的场景。
第一个问题是有效载荷必须在JSON对象中发送,所以我们需要避免“破坏”JSON正文,否则解析器将引发错误。幸运的是,我们可以使用原始字节组合插入正文(被解析器接受),双重编码(使用 %25
代替 %
来注入格式说明符)和UTF编码终止地址的空字节( u0000
)。
第二个问题是,解码后,我们有效载荷存储在C字符串中,所以空字节将提前终止它。这意味着我们只能有一个空字节,它必须位于我们格式字符串的末尾。
第三个问题是,格式字符串的长度有限制。我们可以通过使用 %hn
格式一次写入少量字节来克服这个问题。
第四个问题(是的,更多的问题)是,在格式字符串中,我们的输入之前有一个可变的字符数,这将打乱 %hn
将计数的字符数,随后将写入我们目标地址。这是因为 logmessage_normal
函数被调用时,进程名称( httpd
或 httpsd
)和 pid(1到5个字符)作为参数。
最后,我们准备好了有效载荷,一切都完美地打磨好了,是时候执行漏洞利用并获得我们设备上的一个shell了……
等等,什么鬼???
被认证还是不被认证
发送有效载荷时不带任何cookie会导致重定向到登录页面!
此时我们完全震惊了。CVE报告提到“未认证的远程攻击者”,而且我们针对Qiling模拟器的漏洞利用在没有任何认证的情况下工作得很好。出了什么问题?
在购买真实设备之前,我们使用Qiling进行模拟时,我们从互联网上下载了一个NVRAM状态的转储。如果 httpd
进程加载了转储中不存在的密钥,我们自动将它们设置为空字符串,有些在明确崩溃/段错误的情况下手动调整。
结果发现,一个名为 x_Setting
的重要键决定了路由器是否配置。基于此,大多数CGI端点的访问被启用或禁用。我们在Qiling中使用的NVRAM状态包含了设置为 0 的 x_Setting
键,而我们的真实设备(常规配置)将其设置为 1。
但等等,还有更多!
我们研究了之前报告的影响其他端点的格式字符串CVE,以测试它们对我们设置的影响。我们发现在线的漏洞利用程序将 Referer 和 Origin 头设置为目标主机,而其他一些则通过发送简单的GET请求而不是带有JSON正文的POST请求。最后,为了尽可能准确地再现他们的设置,我们甚至模拟了其他设备的固件(例如 华硕RT-AX86U)。
没有一个在NVRAM中 x_Setting=1
的环境中起作用。
你知道吗?如果路由器没有配置,WAN接口就不会远程暴露,使攻击者无法访问。
结论
这项研究给我们留下了苦涩的味道。
此时的可能性是:
-
仍然存在一个未修复的额外认证绕过漏洞👀,因此它没有出现在差异中。
-
CVE中提到的“未认证的远程攻击者”指的是类似CSRF的场景。
-
所有以前的研究人员在没有考虑NVRAM内容的情况下通过模拟固件发现了漏洞。
无论如何,我们正在我们的poc仓库的GitHub上发布我们的PoC漏洞利用代码和Qiling模拟器脚本。
原文始发于微信公众号(3072):寻找华硕路由器中的未授权 N-day(译)