简介

什么是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协议的话,会是一个不得了的大洞呢,可惜了.