记一次ThinkPHP源码审计

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

内容简介:记一次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 之类的命令。


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

查看所有标签

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

The Haskell School of Expression

The Haskell School of Expression

Paul Hudak / Cambridge University Press / 2000-01 / USD 95.00

Functional programming is a style of programming that emphasizes the use of functions (in contrast to object-oriented programming, which emphasizes the use of objects). It has become popular in recen......一起来看看 《The Haskell School of Expression》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

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

在线 XML 格式化压缩工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试