简介

什么是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类库的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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()这个函数的源代码了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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

1
2
3
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行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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解析的函数

首先

1
2
3
4
5
6
7
8
9
10
11
12
13
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进行对比.程序猿还在注释里面写了这么一段话.

1
2
3
4
` /* 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协议的规定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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,即使是别的域名也会最终读取到本地的对应文件中.

所以假设一个情况:

1
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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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协议的话,会是一个不得了的大洞呢,可惜了.

Comments

⬆︎TOP