9th Geek SSRF

前言

学校举办的第九届极客大挑战,其中一道根据 Blackhat 议题出的 ssrf 题目,也是第一次尝试阅读 php 源码,望大牛们勿喷

代码分析

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?php
function check_inner_ip($url)
{
$match_result=preg_match('/^(http|https)?:\/\/.*(\/)?.*$/',$url);
if (!$match_result)
{
die('url fomat error1');
}
try
{
$url_parse=parse_url($url);
}
catch(Exception $e)
{
die('url fomat error2');
}
$hostname=$url_parse['host'];
echo $url_parse['host'];
$ip=gethostbyname($hostname);
$int_ip=ip2long($ip);
return ip2long('127.0.0.0')>>24 == $int_ip>>24 || ip2long('10.0.0.0')>>24 == $int_ip>>24 || ip2long('172.16.0.0')>>20 == $int_ip>>20 || ip2long('192.168.0.0')>>16 == $int_ip>>16 || ip2long('0.0.0.0')>>24 == $int_ip>>24;
}
function safe_request_url($url)
{
if (check_inner_ip($url))
{
echo $url.' is inner ip';
}
else
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$output = curl_exec($ch);
$result_info = curl_getinfo($ch);
if ($result_info['redirect_url'])
{
safe_request_url($result_info['redirect_url']);
}
curl_close($ch);
var_dump($output);
}
}
$url = $_POST['url'];
if(!empty($url)){
safe_request_url($url);
}
else{
highlight_file(__file__);
}
//hint23333:
//flag in flag.php
//phpinfo in phpinfo.php
?>

check_inner_ip 通过 url_parse 检测是否为内网 ip 。

如果满足不是内网 ip ,通过 curl 请求 url 返回结果。

这是 github 上开源,根据 p师傅文章写的防御 ssrf 攻击代码,详情可以查看:安全编码系列–ssrf漏洞防御脚本

漏洞利用

乍一看好像并没有利用点,跳转也做了处理,最终都要经过 check_inner_ip 函数检测。但是忽略了 php_url_parsecurl 同时处理 url 不同。

【Blackhat】SSRF的新纪元:在编程语言中利用URL解析器

这里面关于 curl 的利用提到了,当处理这个地址时

curl 和 php_url_parse 处理后最终的目标不一样

php_url_parse 认为 google.com 为目标的同时,curl 认为 evil.com:80 是目标。

文章作者向 curl 团队报告了这个问题,得到了一个补丁,但是补丁又可以通过空格的方式绕过。

有趣的是当作者再次向官方团队报告漏洞时,被告知它本来就是要让你来传给他正确的URL参数的,并且他们表示,这个漏洞不会修复。:P

我们再来分析一下代码逻辑,检测是否内网 ip 通过 parse_url,而最后请求是用 curl 完成的。当遇到上面的 url 格式时,parse_url 判断的是第二个 @ 后接的地址,curl 请求的是第一个。

于是利用思路就有了,让 parse_url 处理外部网站,最后 curl 请求内网网址。

构造 payload:http://Str3am@127.0.0.1:80 @www.baidu.com/flag.php

漏洞分析

题目环境是 php 7.0.32,本地使用 phpstudy 搭建,使用 php 7.0.12,这里也就此版本分析。

这里也推荐一篇翻译的关于 php 源码阅读的基础指南:phps-source-code-for-php-developers

parse_url

函数申明位于 /ext/standard/url.h,具体可以访问 https://github.com/php/php-src/blob/PHP-7.0.12/ext/standard/url.h

具体定义位于 /ext/standard/url.c

主要函数 php_url_parse_ex() 从 97 行开始

这里迫于篇幅,不再对具体过程详解,这篇文章关于源码的详解很精彩 PHP源码分析之parse_url()的2个小trick

str 为处理的 urlsepppue 字符指针,用于标记 url 中字符位置,也是这个函数处理的大致方式。先提取协议(scheme)如 http,https 并存储,接着获取请求参数(query)和锚点(fragment),获取端口(port)最后检测主机(host)并存储。

243 行检测 userpass,对于 http://Str3am@127.0.0.1:80 @www.baidu.com/flag.php,此时变量 e 指向末尾的 p,变量 s 指向 http:// 之后的 S(e-s) 代表所指字符之间的内容(包含),所以函数 zend_memrchr 处理的内容即 Str3am@127.0.0.1:80 @www.baidu.com/flag.php

问题就出在这个 zend_memrchr 函数,定义位于 /Zend/zend_operators.h,193 行,是从字符串最末尾开始检测,那么对于两个 @ ,就会解析到最后一个

跟着下来,最后获取主机(host)的时候,(p-s) 的内容即 www.baidu.com

最后的解析结果如下

curl

定义位于 /ext/curl/php_curl.h,实现位于 /ext/curl/interface.c,这里是对 libcurl 经行调用,由于能力原因就不继续往下分析了。

另一道非预期解

比赛时因为非预期被下了,这里也来分析一波

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<?php
//error_reporting(E_ALL);
function check_inner_ip($url)
{
$match_result=preg_match('/^(http|https)?:\/\/.*(\/)?.*$/',$url);
if (!$match_result)
{
die('url fomat error');
}
try
{
$url_parse=parse_url($url);
}
catch(Exception $e)
{
dir('url fomat error');
return false;
}
$hostname=$url_parse['host'];
//var_dump($hostname);
$ip=gethostbyname($hostname);
$int_ip=ip2long($ip);
return ip2long('127.0.0.0')>>24 == $int_ip>>24 || ip2long('10.0.0.0')>>24 == $int_ip>>24 || ip2long('172.16.0.0')>>20 == $int_ip>>20 || ip2long('192.168.0.0')>>16 == $int_ip>>16;
}
function safe_request_url($url)
{
if (check_inner_ip($url))
{
echo $url.' is inner ip';
}
else
{
//var_dump($url);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$output = curl_exec($ch);
$result_info = curl_getinfo($ch);
if ($result_info['redirect_url'])
{
safe_request_url($result_info['redirect_url']);
}
curl_close($ch);
var_dump($output);
}
}
$url = $_POST['url'];
if(!empty($url)){
safe_request_url($url);
}
else{
highlight_file(__file__);
}
//hint23333:
//flag in flag.php
//phpinfo in phpinfo.php
?>

一开始对比代码,以为是 try..catch 块里的 return false 造成非预期。在师傅的指点下发现函数 check_inner_ip() 中少了对 0.0.0.0 过滤,于是试了一下 url=http://0.0.0.0/flag.php,发现竟然可以,但是在本机 windows 环境下复现时却不行。

查阅了相关资料,0.0.0.0 代表本机 ipv4 的所有地址,猜测可能发布的时候绑定用的 0.0.0.0,这样做包括本地ip和外网ip都能访问到服务,导致问题。

后记

这道题也可以使用 DNS 重绑定(DNS rebinding)解决,有兴趣的师傅们可以去了解下,个人感觉可能是 ssrf 的通解了

参考链接