9th Geek SSRF

前言

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

代码分析

 <?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 是目标。

扫描二维码关注公众号,回复: 8562966 查看本文章

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

在这里插入图片描述

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

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

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

构造 payload:http://[email protected]: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://[email protected]:80 @www.baidu.com/flag.php,此时变量 e 指向末尾的 p,变量 s 指向 http:// 之后的 S(e-s) 代表所指字符之间的内容(包含),所以函数 zend_memrchr 处理的内容即 [email protected]: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 经行调用,由于能力原因就不继续往下分析了。

在这里插入图片描述

另一道非预期解

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

 <?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 的通解了

参考链接

发布了29 篇原创文章 · 获赞 8 · 访问量 6750

猜你喜欢

转载自blog.csdn.net/qq_39293438/article/details/84899550
9th