记一次ThinkPHP源码审计

栏目: PHP · 发布时间: 7年前

内容简介:记一次ThinkPHP源码审计

一、写在前面

周末闲得蛋疼,审了一套朋友给的系统,过程挺有意思的,开始的时候觉得基于TP3.0二次开发的系统应该是蛮简单的,毕竟TP爆了很多漏洞。后来发现开发做了不少安全措施。因此记录一下这次审计,当时自己学习的记录吧。

二、后台注入

最开始的时候,因为快吃饭了,就用RIPS扫了扫(事实证明,没有什么毛用)。说是扫到了一个对象注入,然后就看了看代码,大致如下:

$where = unserialize(base64_decode($_REQUEST['_where']));
$result=$m->where($where)->order("id desc")->select();

讲真,这里一眼就能看出来是有个注入的,最简单的方式就是 $_REQUEST['_where'] 经过 base64_decode() -> unserialize() 是字符串,直接就可以注入了。

但是这里到底有没有对象注入呢?我的理解是反序列化导致的对象注入,至少得有对应的魔术方法 __wakeup 或者 __destruct ,并且在魔术方法中有敏感的操作。不仅如此,有时候可能还需要构造POP链,具体参照 这篇文章

因此,我搜索了一下对应的两个魔术方法,全文都没有定义,基本上是无解了。

因为后台的漏洞一般都是比较鸡肋的,因此放弃后台漏洞的挖掘,转而移步前台。

二、前台注入

public function Exit()
{
    $res = M('member')->where(array('id'=>$_GET['userid']))->count();
    if($res){
      echo "1";
    }else{
       echo "0";
    }
}

低版本的TP在这里一定是有漏洞的,至于为什么可以看 这篇文章 。然后,我们就可以构造 userid[0]=exp&userid[1]=xxxx'or 1=1# 之类的语句了,事实上这套系统被修改过了,尝试的过程中发现被一个叫 checkPost 的函数过滤了,如下所示:

static private function checkPost() {
    if( isset($_POST) && is_array($_POST)) {
        foreach($_POST as $key=>$data){
            if(is_array($data))
            {
                self::selfCheck($data);
            }
        }
    }
    if( isset($_GET) && is_array($_GET)) {
        foreach($_GET as $key=>$data){
            if(is_array($data))
            {
                self::selfCheck($data);
            }
        }
    }
}
static private function selfCheck($data)
{
    $ary=array('exp','eq','neq','gt','egt','lt','elt','like','notlike','not like','in','notin','not in','between','notbetween','not between');
    if(is_array($data))
    {
        if(count($data)>0)
        {
            array_map(array('Think','selfCheck'),$data);
        }
    }
    else
    {
        if($data!="")
        {
            if( in_array(trim(strtolower($data)),$ary) )
            {
                throw_exception("参数有非法参数");
            }
            else
            {
                //匹配 not in  、not between、not like
                preg_match('/^not[\ ]+(in|between|like)$/',trim(strtolower($data)), $matches);
                if( !empty($matches) )
                {
                    throw_exception("参数有非法参数");
                }
            }
        }
    }
}

也就是说,ThinkPHP中的 exp 等那一波漏洞都不能用了。这就很尴尬了,而且前台可以输入的地方真的不多。

没办法,挨着看有点受不了,就开始搜索 $_GET$_POST 以及 $_REQUEST 之类的全局变量,其实除了这三种方法获取输入,系统还使用了 I 方法(我特么真的是服了, I 方法是TP-3.1.3之后才添加的,这里系统显示的是TP-3.0)。

果不其然,发现了唯一一处字符串拼接的地方

$saleData = M('report')->where("(infoid='{$this->userinfo['id']}' or userid='{$this->userinfo['id']}') and id={$_GET['id']}")->find();

问题到这里就解决了,其实它还用到了 I 方法获取输入, 参考这里 。我看了看,这个完全是TP-3.2的东西啊,最开始应该早就走了弯路。好吧,没有发现字符串拼接的地方。

三、逻辑漏洞

朋友告诉我,这套系统注册的功能关闭了,虽然是前台注入,还是很鸡肋啊。掩面哭泣啊,这种MVC的框架,一般验证登录状态都在父类中做好了。

无奈,看了很久登录验证的问题,没有绕过去。 也就是说,没有登录无法绕过又没有账号是没法注入的 。似乎到现在已经陷入了僵局,此时我看了看网站目录,我发现网站还有个应用 Install

如果能够重装也是极好的,因此看了看代码

下面是控制器的代码

if((isset($_REQUEST['step']) ? $_REQUEST['step']:'') !='done' && ACTION_NAME != 'done' && file_exists('./install.lock')){
    die('重新安装请删除/Install/install.lock文件');
}

只要我们传递 step=done 就可以绕过构造器了。接着看敏感函数,发现了一个比较有意思的函数 create_admin ,通常在安装程序的时候会有创建管理员这一步,这里居然属性设置为了 public ,二话不说,添加一个管理员账号。

public function create_admin()
{
    //创建超级管理员帐号
    $Install   = D('Install');
 
    $result        = $Install->create_admin($this->langs);
 
    if( !$result ) exit( '创建管理员帐号失败' );
 
    echo 'OK';
}

四、命令执行

现在已经拥有后台权限、并且后台可以添加前台用户来进行 SQL 注入。因此现在更加关注的写文件、代码执行、命令执行等等。所以就搜索了一下关键函数,发现一处比较关键的。

 function backup(){
    $name='新数据备份';
    $name = I("post.backname/s")==""?$name:I("post.backname/s");
    if(adminshow('cliSwitch')){
        //判断Windows还是Linux
        if(IS_WIN){
            $ini = ini_get_all();                    
            $path = $ini['extension_dir']['local_value'];           
            $php_path = str_replace('\\', '/', $path);           
            $php_path = str_replace(array('/ext/', '/ext'), array('/', '/'), $php_path);           
            $real_path = $php_path . 'php.exe';
            chdir(ROOT_PATH);//更改当前工作路径
            $cmd = $real_path." ".ROOT_PATH."clibr.php Backup backall backname,".$name." >recerr.log";
            pclose(popen("start /B ". $cmd, "r"));  
        }else{
            chdir(ROOT_PATH);
            $cmd="php ".ROOT_PATH."clibr.php Backup backall backname,".$name." >recerr.log";
            exec($cmd . " &",$out,$re);
        }
        $this->ajaxReturn(array(),"正在备份中",0);
    }else{
        $result=$this->backall($name);
        if($result==""){
            $this->ajaxReturn(array(),"备份完成,用时".G('run','end').'秒',1);
        }else{
            $this->ajaxReturn(array(),$result,0);
        }
    }
}

看了下关键函数 adminshow ,其实就是查看配置文件而已。它检查 ADMIN_SHOW 这个字段中是否含有 cliSwitch 的值,我看了看,默认是没有的;如果有,就会造成字符串拼接导致命令执行漏洞。

function adminshow($shows){
    $adminshowss=explode(',',CONFIG('ADMIN_SHOW'));
    if(in_array($shows,$adminshowss)){
        return  true; 
    }
    return false;
}

接下来搜索 ADMIN_SHOW ,查看是否有设置这个配置文件的地方,果然有。

public function save(){
    $showstrss = '';
    foreach($_POST as $k=>$v)
    {
        if($k!='LOG_RECORD' && $k!='APP_DEBUG' && $k!='LOG_LEVEL')
        {
          $showstrss.=",".$k;
        }
    }
    M()->startTrans();
    CONFIG('ADMIN_SHOW',trim($showstrss,","));
}

在这个控制器下面,就可以设置 ADMIN_SHOW 这个键的配置参数了。

五、全局过滤

我发现,在执行命令的时候发现有些参数被过滤了。它配置了 DEFAULT_FILTERhtmlspecialchars默认过滤了一些字符 。因此,在命令拼接的时候可以使用 | 或者 || ,在写文件的时候可以使用 wget 或者 bitsadmin 之类的命令。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

第一行代码:Android(第2版)

第一行代码:Android(第2版)

郭霖 / 人民邮电出版社 / 2016-12-1 / CNY 79.00

本书被广大Android 开发者誉为“Android 学习第一书”。全书系统全面、循序渐进地介绍了Android软件开发的必备知识、经验和技巧。 第2版基于Android 7.0 对第1 版进行了全面更新,将所有知识点都在最新的Android 系统上进行重新适配,使用 全新的Android Studio 开发工具代替之前的Eclipse,并添加了对Material Design、运行时权限、......一起来看看 《第一行代码:Android(第2版)》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

html转js在线工具
html转js在线工具

html转js在线工具