开始练习代码审计,先从小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甚至是大型框架才有经验和底气。

一直没有机会沉下心来好好研究代码审计,这是我代码审计的开始,迈出的第一步,很是欣喜。