[代码审计] bluecms v1.6
开始练习代码审计,先从小cms开始。
审计思路
在审计之前还把《代码审计--企业级Web代码安全架构》前几章的审计方法好好看了一下,书中主要介绍了四种代码审计思路:
- 检查敏感函数,逆向跟踪参数(回溯变量)。
- 正向跟踪变量,查看输入变量传递过程。
- 直接挖掘功能点漏洞
- 通读全部代码,审计代码
这四种审计思路有好有坏,我比较喜欢正向跟踪变量和直接挖掘功能点漏洞,这样审计速度比通读全文代码速度会快很多,当然也会有很多遗漏,慢慢修炼吧。
搭建环境
bluecms是从网上下的,i春秋有个bluecms审计的帖子可以下载。在win7虚拟机安装了phpstudy简陋的实现了环境。将解压bluecms文件夹放进网站根目录进入/install 安装。
开始审计
边在网站各个地方随便点点看看有啥功能(方便快速理解代码),边按顺序打开主目录下的php文件看看。

观察到所有文件几乎都包含了/include/common.inc.php ,在里面发现有对输入addslashes 。但遗漏了$_SERVER,可能会存在IP的注入。如果有时间的话还是建议大致把功能函数都看一看,可以得到不少信息。
sql注入
虽然addslashes了,但是我们可以看到在ad_js.php中
require_once dirname(__FILE__) . '/include/common.inc.php'; $ad_id = !empty($_GET['ad_id']) ? trim($_GET['ad_id']) : ''; $ad = $db->getone("SELECT * FROM ".table('ad')." WHERE ad_id =".$ad_id);
对,$ad_id没有intval(),在构造sql的时候也没有用单引号括起来,说明前面的addslashes是没用的。注入就产生了。

在admin/ad.php $act='edit'
也发现一个同样的注入,在这里就不细讲啦。
任意url跳转
在user.php中,很明显$act是选择功能。当<em>$act </em>== 'do_login'
时,处理用户登陆信息,由于sql语句都用单引号包住了,无法通过通常方法注入(其实宽字节注入是可以的,但是由于它先判断了是否是admin用户组再验证登录,导致利用困难)
这里我们主要是关注另一个问题,任意url跳转。我们可以看到有一个$from变量,再结合登录成功后显示回到该变量指向参数,我们可以猜测这个$from保存来源url,方便用户登陆后回到原来浏览的页面。
$from = !empty($from) ? base64_decode($from) : 'user.php';
#登入成功会运行下面
showmsg('欢迎您 '.$user_name.' 回来,现在将转到...', $from);
统筹观察一下,$from并没有被其他函数过滤,直接利用一下试试(注意$from应该base64加密一下)

试验成功!
存储型XSS
还是在user.php文件,$act='do_add_news'
,用户发布新闻。
include_once 'include/upload.class.php';
$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);
看代码发现content没有htmlspecialchars,而是filter_data,跟踪看一下。
function filter_data($str)
{
$str = preg_replace("/<(\/?)(script|i?frame|meta|link)(\s*)[^<]*>/", "", $str);
return $str;
}
只过滤这些东西还是很好利用的。直接利用走起。


咦,为啥不能用,反复看了还是没有看到过滤的地方,猜测是前端过滤了,我们抓包看看

果然是前端替换了敏感字符,修改重放

成功了
sql注入(IP注入)
前面讲到了,通用函数文件中并没有给$_SERVER添加addslashes,说明如果服务器在某处收集用户IP或者UA时没有对参数进行检验,那一定会造成注入问题。
$sql = "INSERT INTO " . table('guest_book') . " (id, rid, user_id, add_time, ip, content)
VALUES ('', '$rid', '$user_id', '$timestamp', '$online_ip', '$content')";
在guest_book.php中sql语句中发现插入一个$online_ip,跟踪看看
$online_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;
}
IP是可以伪造的,而且加上前面的分析,可以注入。将X-Forwarded-For
做如下修改。

成功注入。

宽字节注入拿管理员后台
在data/config.php中我们发现数据库配置信息,其中有一条是define('BLUE_CHARSET','gb2312');
说明很有可能存在宽字节注入问题,那么引号就不存在限制了。
经过审阅,最容易利用的地方是admin后台登录的地方。
$admin_name = isset($_POST['admin_name']) ? trim($_POST['admin_name']) : '';
$admin_pwd = isset($_POST['admin_pwd']) ? trim($_POST['admin_pwd']) : '';
$remember = isset($_POST) ? intval($_POST['rememberme']) : 0;
if(check_admin($admin_name, $admin_pwd)){
update_admin_info($admin_name);
if($remember == 1){
setcookie('Blue[admin_id]', $_SESSION['admin_id'], time()+86400);
setcookie('Blue[admin_name]', $admin_name, time()+86400);
setcookie('Blue[admin_pwd]', md5(md5($admin_pwd).$_CFG['cookie_hash']), time()+86400);
}
}else{
showmsg('您输入的用户名和密码有误');
}
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' or 1=1 -- -
getone函数就会随便取到一个管理员数据返回,直接登录。(注意直接在浏览器输入%df会被urlencode,所以应该抓包发送)

成功登录。
总结
这个cms问题还是大多出在用户输入没有处理好,所以导致注入问题才那么多。太菜了逻辑漏洞并没有挖到。现在多审计几个小cms,等到过渡到中型cms甚至是大型框架才有经验和底气。
一直没有机会沉下心来好好研究代码审计,这是我代码审计的开始,迈出的第一步,很是欣喜。