一般来说审计的话都可以从入口文件代码开始看起,因为从入口文件代码中可以给到非常多的信息为后面审计做好铺垫,比如根目录下的 index.php
基本都会为程序的入口文件。
index.php:
include(dirname(__FILE__).'/global.php');
可以看到 index.php
文件第一行代码就是包含了根目录下面的 global.php
文件,一般我的话就会继续跟入 global.php
这个文件里面,看看里面做了什么处理,有些师傅可能习惯将整个入口文件读完之后再去跟入所包含的文件,这样也是可以的,但是有点不好的就是如果入口文件代码一多,那么这其中很多代码有可能会在所包含的文件里面做的处理,因此可能你就不知道这个变量或者数组到底从哪来,所以推荐如果有包含的文件,根据文件名字判断重要性,随即继续跟入。
global.php:
define('APP_PATH',dirname(__FILE__).'/');
define('CONFIG_PATH',APP_PATH.'/config/');
define('DATA_PATH',APP_PATH.'/data/');
define('LIB_PATH',APP_PATH.'/app/include/');
define('TPL_PATH',APP_PATH.'/app/template/');
define('MODEL_PATH',APP_PATH.'/model/');
define('PLUS_PATH',DATA_PATH.'/plus/');
define('ALL_PS','conn');
可以看到 global.php
最顶部先是定义了非常多的常量,定义了一些配置文件或者其它文件的路径,因为在开发中在全局定义常量是为了整个项目文件都可以用到,比如后期可能会用到某个路径,这个时候直接使用常量即可,就不用继续在做一些路径的处理,继续往下看代码。
include(CONFIG_PATH.'db.config.php');
include_once(PLUS_PATH.'config.php');
include(CONFIG_PATH.'db.safety.php');
在定义完常量的下方可以看到又包含了一些文件,从字面上的意思猜的是包含的 配置文件
以及 全局过滤文件
,可以看到首先第一个包含的文件为:(CONFIG_PATH.'db.config.php')
也就是 /config/db.config.php
文件:
<?php
$db_config = array(
'dbtype'=>'mysql',
'dbhost'=>'localhost',
'dbuser'=>'root',
'dbpass'=>'root',
'dbname'=>'phpyun',
'def'=>'phpyun_',
'charset'=>'utf8',
'timezone'=>'PRC',
'coding'=>'2c8c4d53878158c06481a39e6d352dbc', //生成cookie加密
);
?>
这里存着数据库账号、密码、数据库名、表前缀等信息,可以看下一个文件,包含的是网站的配置信息文件,这里就不贴出来了,因为与今天所讲的漏洞没有太大联系。
继续往下看 (CONFIG_PATH.'db.safety.php');
这个字面意思是可以看的出来为一个全局过滤文件,一般程序的全局过滤文件都是过滤的 XSS
、SQL注入
较为多,全局过滤文件在审计中决定了我们审计到的可疑点是不是可以利用,所以如果我们知道了一个程序存在全局过滤文件的话,我们首先需要尽可能的分析这个文件的过滤流程是怎么样的。
一般全局过滤文件将传进来的 POST
、GET
、COOKIE
的参数进行过滤。
/config/db.safety.php:
这里拿 POST
为例,因为其它的处理流程基本都是一致。
foreach($_POST as $id=>$v){
if($id != 'uimage'){
$str = html_entity_decode($v,ENT_QUOTES);
$v = common_htmlspecialchars($id,$v,$str,$config);
safesql($id,$v,"POST",$config);
$id = sfkeyword($id,$config);
$v = sfkeyword($v,$config);
}
if(trim($id)){
$_POST[$id] = $v;
}
}
可以看到程序是将传递进来的 POST
数据做了一个循环遍历处理,可以将 $_POST
看成是一个数组,比如POST
传递进来的数据为:content=test&user_id=1
,那么最终这段数据在PHP
中被$_POST
所接收呈现的样子则如下:
Array
(
[content] => test
[user_id] => 1
)
在循环$_POST
中会将键名每次循环时候赋值给$id
,将值赋值给$v
,也就是说在传递的数据为上方所展示的话,那么在这个foreach
循环中$id
还有$v
的值分别如下:
$id
content
user_id
$v
test
1
因为传递进来的数据只有两组,因此只会循环两次,每一次循环都会判断传递进来的键名是否等于uimage
,如果不等于的话就会经过过滤处理。这里如果等于那么也仅仅对键名为:uimage
的值不进行过滤处理,其它的参数依旧进行过滤处理。
可以看到首先是调用了函数html_entity_decode
,这个函数作用是将 HTML
实体转换为字符并且赋值给了$str
变量,随即调用了 common_htmlspecialchars
函数,这个时候我们可以根据这个函数里面看看做了什么处理。
function common_htmlspecialchars($key,$str,$str2,$config){
if(is_array($str)){
//该函数先判断传递进来的值是否为数组,如果是数组的话进行一次循环,只有到最后传递进来的不是数组才会进行过滤处理。
foreach($str as $str_k=>$str_v){
$str[$str_k] = common_htmlspecialchars($str_k,$str_v,$str2,$config);
}
}else{
$str = preg_replace('/([\x00-\x08\x0b-\x0c\x0e-\x19])/', '', $str);
/*
这里可以看到会先判断你传递进来的键是不是在数组里面。
如果按照传递进来的数据:content=test&user_id=1
那么content则会进入假区间,user_id进入真区间,因为content是在数组里面而user_id不在数组里。
*/
if(!in_array($key,array('content','config','group_power','description','body','job_desc','eligible','other','code','intro','doc','traffic','media','packages','booth','participate'))){
//如果不在数组里面就走进这个区间-真区间
$str = strip_tags($str);
$str = gpc2sql($str,$str2);//真区间只做SQL过滤处理。
}else{
//如果在数组里面就走进这个区间-假区间
//假区间会做XSS过滤以及SQL过滤
if(!isset($_SESSION)){
session_start();
}
if($_SESSION['xsstooken'] != sha1($config['sy_safekey'])){
$str = RemoveXSS(urldecode($str));
$str = gpc2sql($str,$str2);
}
}
}
return $str;
}
接下来我们跟进 RemoveXSS
、gpc2sql
函数分别是怎么样的一个过滤规则。
function gpc2sql($str,$str2) {
if(preg_match("/select|insert|update|delete|load_file|outfile/is", $str)){
exit(safe_pape());
}
if(preg_match("/select|insert|update|delete|load_file|outfile/is", $str2)){
exit(safe_pape());
}
$arr=array("sleep"=>"Sleep"," and "=>" an d "," or "=>" Or ","xor"=>"xOr","%20"=>" ","select"=>"Select","update"=>"Update","count"=>"Count","chr"=>"Chr","truncate"=>"Truncate","union"=>"Union","delete"=>"Delete","insert"=>"Insert","\""=>"“","'"=>"“","--"=>"- -",""=>"(",""=>")","00000000"=>"OOOOOOOO","0x"=>"Ox");
foreach($arr as $key=>$v){
$str = preg_replace('/'.$key.'/isU',$v,$str);
}
return $str;
}
首先会正则匹配传递进来的$str
是否包含这些关键字,如果包含的话就直接终止程序并且将安全提示内容显示出来,然后判断将 HTML
实体转换为字符的新变量传递进来的值是否包含这些关键字,如果包含的话就直接终止程序并且将安全提示内容显示出来。
随即定义了一个新数组,然后对这个数组进行循环遍历,在循环遍历处理过程中调用preg_replace
函数,执行一个正则表达式的搜索和替换。也就是它会匹配如果你传递进来的字符串存在上面数组的键中,那么就会进行一个替换操作。例如你传递进来的字符串包含:sleep
,那么最后处理完的字符串就会变成:Sleep
,首字母是转换成了全角。
function RemoveXSS($val) {
$val = preg_replace('/([\x00-\x08\x0b-\x0c\x0e-\x19])/', '', $val);
$search = 'abcdefghijklmnopqrstuvwxyz';
$search .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
$search .= '1234567890!@#$%^&*()';
$search .= '~`";:?+/={}[]-_|\'\\';
for ($i = 0; $i < strlen($search); $i++) {
$val = preg_replace('/(&#[xX]0{0,8}'.dechex(ord($search[$i])).';?)/i', $search[$i], $val); // with a ;
$val = preg_replace('/(�{0,8}'.ord($search[$i]).';?)/', $search[$i], $val); // with a ;
}
$ra1 = Array('javascript', 'vbscript', 'expression', 'applet', 'meta', 'xml', 'blink', 'link', 'script', 'embed', 'object', 'iframe', 'frame', 'frameset', 'ilayer', 'layer', 'bgsound', 'title', 'base');
$ra2 = Array('onabort', 'onactivate', 'onafterprint', 'onafterupdate', 'onbeforeactivate', 'onbeforecopy', 'onbeforecut', 'onbeforedeactivate', 'onbeforeeditfocus', 'onbeforepaste', 'onbeforeprint', 'onbeforeunload', 'onbeforeupdate', 'onblur', 'onbounce', 'oncellchange', 'onchange', 'onclick', 'oncontextmenu', 'oncontrolselect', 'oncopy', 'oncut', 'ondataavailable', 'ondatasetchanged', 'ondatasetcomplete', 'ondblclick', 'ondeactivate', 'ondrag', 'ondragend', 'ondragenter', 'ondragleave', 'ondragover', 'ondragstart', 'ondrop', 'onerror', 'onerrorupdate', 'onfilterchange', 'onfinish', 'onfocus', 'onfocusin', 'onfocusout', 'onhelp', 'onkeydown', 'onkeypress', 'onkeyup', 'onlayoutcomplete', 'onload', 'onlosecapture', 'onmousedown', 'onmouseenter', 'onmouseleave', 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup', 'onmousewheel', 'onmove', 'onmoveend', 'onmovestart', 'onpaste', 'onpropertychange', 'onreadystatechange', 'onreset', 'onresize', 'onresizeend', 'onresizestart', 'onrowenter', 'onrowexit', 'onrowsdelete', 'onrowsinserted', 'onscroll', 'onselect', 'onselectionchange', 'onselectstart', 'onstart', 'onstop', 'onsubmit', 'onunload');
$ra = array_merge($ra1, $ra2);
$found = true;
while ($found == true) {
$val_before = $val;
for ($i = 0; $i < sizeof($ra); $i++) {
$pattern = '/';
for ($j = 0; $j < strlen($ra[$i]); $j++) {
if ($j > 0) {
$pattern .= '(';
$pattern .= '(&#[xX]0{0,8}([9ab]);)';
$pattern .= '|';
$pattern .= '|(�{0,8}([9|10|13]);)';
$pattern .= ')*';
}
$pattern .= $ra[$i][$j];
}
$pattern .= '/i';
$replacement = substr($ra[$i], 0, 2).'<x>'.substr($ra[$i], 2);
$val = preg_replace($pattern, $replacement, $val);
if ($val_before == $val) {
$found = false;
}
}
}
return $val;
}
简单来说这里的处理流程就是对你输入的字符串进行一个检测是否符合,随即就是看你传递的字符串中是不是包含一些标签、事件这种关键字,如果包含的话,那么就会对其进行一个替换操作。最后再将处理完的值返回出来。
到了这里也就将安全过滤文件的一个大致流程分析完了,继续回到global.php
往下读
if(is_file(LIB_PATH.'webscan360/360safe/360webscan.php')){
require_once(LIB_PATH.'webscan360/360safe/360webscan.php');
}
可以看到这里判断是否存在360webscan.php
这个文件,如果存在的话就进行引入。其里边的模式大致与上面分析的一致,因此不做分析,只是规则不同。也就是说如果挖掘一些XSS
、SQL注入
等漏洞并且两个安全过滤文件已生效的话,那么就需要考虑绕过两个安全过滤文件。
然后继续往下看global.php
代码
include_once(LIB_PATH.'public.function.php');
include_once(LIB_PATH.'public.domain.php');
include(LIB_PATH.'public.url.php');
include(PLUS_PATH.'seo.cache.php');
LIB_PATH.'public.function.php'
这个文件是一个公共函数库,在我们使用危险函数回溯方法的时候,可以看看这个公共函数库里面的函数,然后根据函数再回溯。
大致的审计流程就是这样,接下来直接进入到漏洞所在处:/member/user/model/expectq.class.php
function save_action(){
$IntegralM=$this->MODEL('integral');
if($_POST['submit']){
$_POST=$this->post_trim($_POST);
$eid=(int)$_POST['eid'];
$data['doc']=str_replace("&","&",$_POST['doc']);
$_POST['lastupdate']=mktime();
$_POST['integrity']=100;
unset($_POST['eid']);
unset($_POST['submit']);
unset($_POST['doc']);
if(!$eid){
/**无关要紧代码省略**/
}else{
$_POST['height_status']='0';
$this->obj->update_once("resume_expect",$_POST,array("id"=>$eid));
$nid=$this->obj->update_once("resume_doc",$data,array("uid"=>$this->uid,"eid"=>$eid));
if($nid){
$this->obj->update_once('resume',array('lastupdate'=>time()),array('uid'=>$this->uid));
$this->obj->member_log("更新粘贴简历",2,2);
$this->ACT_layer_msg("更新成功!",9,"index.php?c=resume");
}else{
$this->ACT_layer_msg("更新失败!",8,"index.php?c=resume");
}
}
}
}
正常函数不正常用法指的是:update_once("resume_doc",$data,array("uid"=>$this->uid,"eid"=>$eid));
这里,这个时候跟进内部里面看一下。
/*
我们来记录下传递进来的参数分别是什么
$table 表名
$data 传递的数据
$w where条件
*/
function update_once($table,$data=array(),$w=''){
$this->db->connect();
$value=array();
$where=array();
include(PLUS_PATH.'dbstruct.cache.php');//包含了一个文件,咱们后边再跟入一下。
$TableFullName=$this->def.$table;//这里的结果最后是表前缀(phpyun_)拼接上传进来的表名(resume_expect)。最后完整的表名为:phpyun_resume_expect
if(is_array($$TableFullName)){
$fields=array_keys($$TableFullName);
}
/*
这里判断的是可变变量是不是为一个数组,也就是 `$TableFullName`变量的值前面加上一个`$`.结果为:$phpyun_resume_expect。
但是我们全文中并没有这个变量,这个时候就要注意了看代码附近有没有包含引入什么文件,有的话就需要跟入进去看看这个变量的值是什么。
最后在dbstruct.cache.php这个代码文件中发现该变量就是一个以表名定义的一个数组:
$phpyun_resume_expect=array('id'=>'int(11)','uid'=>'int(11)','name'=>'varchar(25)' 因为太多后边省略); 这里存放的就是phpyun_resume_expect这张表的表结构。
可以判断$$TableFullName为一个数组,因此走进真区间。
紧接着调用array_keys函数取这个数组的全部键,也就是将$phpyun_resume_expect数组里面的键:`id`、`uid`等键取出来存在$fields中。这里只取键不取值。
*/
if(is_array($fields)){
if(is_array($data)){
foreach($data as $key=>$v){
if(in_array($key,$fields)){
$v = $this->FilterStr($v);
$value[]="`".$key."`='".$this->db->escape_string($v)."'";
}
}
/*
这里可以看成前面讲的全局过滤文件那样: foreach($_POST as $key=>$v)。
因为传递进来的是整个POST,所以可以看成这种方式。
然后判断你传递进来的数据键是不是在$fields中。
也就是会判断你传递进来的键是不是属于这张表的字段。如果不是的话那么就不会处理。
如果在表字段里面就会进行数据的拼接,比如你传递过来的数据为:content=test&user_id=1
最后拼接完给$value数组的数据呈现如下:
array(
[0]=>`content`='test'
[1]=>`user_id`='1'
)
*/
}
if(is_array($w)){
//这里where条件的处理
foreach($w as $key=>$v){
if(in_array($key,$fields)){
$v = $this->FilterStr($v);
$where[]="`".$key."`='".$this->db->escape_string($v)."'";
}
}
$where=@implode(" and ",$where);
}else{
$where = $w;
}
$value=@implode(",",$value);//最后会将$value数组以逗号分割成字符串,也就是:content=test,user_id=1 这种形式
return $this->DB_update_all($table,$value,$where);//我们继续跟入DB_update_all这个方法看看做了些什么操作。
}
}
DB_update_all函数:
function DB_update_all($tablename, $value, $where = 1,$pecial=''){
if($pecial!=$tablename){
$where =$this->site_fetchsql($where,$tablename);
}
$SQL = "UPDATE `" . $this->def . $tablename . "` SET $value WHERE ".$where;
$this->db->query("set sql_mode=''");
$return=$this->db->query($SQL);
return $return;
}
首先我们看到最重要的一行代码,也就是最后的SQL语句拼接。
最终将会拼接成如下的SQL语句:
UPDATE phpyun_resume_expect SET content='test',user_id='1' where id = $eid
也就是我们现在已经知道了$value
是可控的,最终将会以一个修改的形式对数据库字段进行修改,所以就相当于我们可以控制这张表的全部字段的数据了,也就是任意修改某个字段的值。
最后一个有意思的地方,可以看到where
条件,仅仅对id
也就是简历id作为一个条件进行修改这条数据并没有以当前登陆会员的会员id进行判断,按照正常where
应该是这样的:where uid = 当前登陆会员id AND id = $eid
而它这里并没有对uid
进行判断,所以这里是导致越权的,只要我们知道了简历ID,就可以修改其它会员的简历信息。
最后来分析一下漏洞利用前提条件是什么:
POST
并且必须包含submit
参数$eid
必须要有值,并且需要为有效的简历ID,这里的$eid
对应的其实就是简历ID。如果说想要修改简历为置顶的话,我们可以看到数据库表内的字段:top
、topdate
一个是置顶状态,一个是置顶时间。
可以看到目前简历状态是未置顶的。
以eid
做为修改条件,只要在请求包中新增需要修改的参数以及参数值即可。
最后可以看到这里有一个uid
字段,那么我们就可以以id
做为条件然后修改uid
值,将其它会员的简历归属改成我的。
也就是只要在数据包加个uid=2
就可以将id = 3
的这条数据中uid
这个字段的值改为2