简介
什么是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

测试文件里面将CURLOPT_URL设置为file:///etc/test
再访问测试文件http://test/test.php

可以看到原本打算进行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) { 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] == '/') { 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.
|
我把程序前一部分的逻辑画了张图

会发现这个函数把file://{somedomain.com}/etc/passwd
上面{}中的所有给忽略掉,而只使用path,即使是别的域名也会最终读取到本地的对应文件中.
所以假设一个情况:
1
| var_dump(parse_url('file://qq.com/etc/passwd'));
|

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


所以可以想象一下一个场景
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:

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

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

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

如果拼接了前半部分目前来说,又想使用file协议是无计可施的.
但是你想要用其它协议,没问题.curl如果没有读取到传入curl使用的协议,或者遇到不规范的url.会自行对以下协议进行重组.

就是说假如你想使用一个ftp协议来下载东西,但是ftp协议被禁用了.你根据它的判断规则传入一个url.
当在内网的ftp服务器域名前缀是ftp.的情况下libcurl还是会根据你传入的url发起一个ftp请求的.
感觉这题可以出一道题,有一股浓浓的ctf味道.假如说能重组file协议的话,会是一个不得了的大洞呢,可惜了.