这里总结我用到过的或者说我了解的比较好的一些工具。
PHP代码调试工具
代码阅读工具
静态代码审计工具
Fortify
HP出的一款静态代码审计工具,支持21种开发语言,是一款功能强大的商业代码审计工具。
Seay
Seay大佬协议的代码审计工具,国产、开源。
CodeQl
Githubt推出的代码审计项目,在国外受到众多安全研究着追捧。
Xcheck
Xcheck的php引擎支持原生php的安全检查,也支持对国内主流框架编写的web应用进行安全检查,覆盖包括Thinkphp,Laravel,CodeIgniter,Yii,Yaf等web框架。
RIPS
轻量级的代码审计工具,来源于国外专业的代码审计公司RIPS。但是RIPS的开源版本已经多年未更新,并且不支持面向对象的代码审计。
浏览器与插件
数据库管理软件
操作系统
一般采用Windows系统进行审计,对于大多数审计工具来说,Windows系统下图形化界面在使用的时候会更加方便。
PHP集成环境
PHP版本
对于PHP版本的选择,为了使用实际使用环境,应该尽量使用php5.4以后的版本
数据库
对于数据库,对于PHP程序来说,一般都是采用的Mysql,在审计过程中,最好将mysql设置为5.7版本以后。
对于系统架构,我们在确认了审计目标之后,在审计之前,需要先了解目标系统的基本架构,比如目录情况如何、是否使用了框架、存在那些路由、有没有安全过滤函数、有没有全局参数过滤等等。这里就要分为有框架和无框架来进行分析。
这里一ThinkPHP5为例,框架的大体目录结构如下:
www WEB部署目录(或者子目录)
├─application 应用目录
│ ├─common 公共模块目录(可以更改)
│ ├─module_name 模块目录
│ │ ├─config.php 模块配置文件
│ │ ├─common.php 模块函数文件
│ │ ├─controller 控制器目录
│ │ ├─model 模型目录
│ │ ├─view 视图目录
│ │ └─ ... 更多类库目录
│ │
│ ├─command.php 命令行工具配置文件
│ ├─common.php 公共函数文件
│ ├─config.php 公共配置文件
│ ├─route.php 路由配置文件
│ ├─tags.php 应用行为扩展定义文件
│ └─database.php 数据库配置文件
│
├─public WEB目录(对外访问目录)
│ ├─index.php 入口文件
│ ├─router.php 快速测试文件
│ └─.htaccess 用于apache的重写
│
├─thinkphp 框架系统目录
│ ├─lang 语言文件目录
│ ├─library 框架类库目录
│ │ ├─think Think类库包目录
│ │ └─traits 系统Trait目录
│ │
│ ├─tpl 系统模板目录
│ ├─base.php 基础定义文件
│ ├─console.php 控制台入口文件
│ ├─convention.php 框架惯例配置文件
│ ├─helper.php 助手函数文件
│ ├─phpunit.xml phpunit配置文件
│ └─start.php 框架入口文件
│
├─extend 扩展类库目录
├─runtime 应用的运行时目录(可写,可定制)
├─vendor 第三方类库目录(Composer依赖库)
├─build.php 自动生成定义文件(参考)
├─composer.json composer 定义文件
├─LICENSE.txt 授权说明文件
├─README.md README 文件
├─think 命令行入口文件
其中,applicatinon目录和我们的Public目录,使我们的应用程序文件目录和运行数据存放目录,属于重要关注对象。
同时application目录下的config.php、database.php、route.php分别是系统配置文件、数据库操作文件、系统路由文件,都需要仔细分析。
对于ThinkPHP框架开发的应用,其访问系统的URL通常是这样的:/index.php/模块名/控制器名/函数名/[参数名/参数值]
其中模块名对应了application目录下的文件夹的名字。控制器名对应了模块名目录下的controller文件夹下的php文件名。参数则是可选的,可以按照/参数名/参数值
的方式同时传入多个参数,也可以按照传统方式使用?参数1=参数值1&参数2=参数值2
的方式传入变量。
基本的目录结构:
|——app 包含了站点的controllers(控制器),models(模型),views(视图)和assets(资源
|——bootstrap 存放系统启动时的必要文件,这些文件会被index.php这样的文件调用。
|——public 系统运行的公开数据,包括静态资CSS文件、js文件等
|——vender 第三方类库
与thinkPHP还是一样的,主要的应用程序文件都存放在了app目录下。
在没使用开发框架的情况下,我们就需要去判断应用程序是否采用了MVC模式,如果有的话,就需要去查看系统的路由文件,看看控制程序是如何通过路由定位的。比如PHPCMS,就是用了自己开发的MVC控制器来进行路由:
访问index.php,可以看到,包含了phpcms/base.php
define('PHPCMS_PATH', dirname(__FILE__).DIRECTORY_SEPARATOR);
include PHPCMS_PATH.'/phpcms/base.php';
进入base.php文件,就能看到,这里定义了特别多的路由以及初始化的一些类以及方法:
//PHPCMS框架路径
define('PC_PATH', dirname(__FILE__).DIRECTORY_SEPARATOR);
if(!defined('PHPCMS_PATH')) define('PHPCMS_PATH', PC_PATH.'..'.DIRECTORY_SEPARATOR);
//缓存文件夹地址
define('CACHE_PATH', PHPCMS_PATH.'caches'.DIRECTORY_SEPARATOR);
//主机协议
define('SITE_PROTOCOL', isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == '443' ? 'https://' : 'http://');
//当前访问的主机名
define('SITE_URL', (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : ''));
//来源
define('HTTP_REFERER', isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '');
//定义网站根路径
define('WEB_PATH',pc_base::load_config('system','web_path'));
//js 路径
define('JS_PATH',pc_base::load_config('system','js_path'));
//css 路径
define('CSS_PATH',pc_base::load_config('system','css_path'));
//img 路径
define('IMG_PATH',pc_base::load_config('system','img_path'));
//动态程序路径
define('APP_PATH',pc_base::load_config('system','app_path'));
//应用静态文件路径
define('PLUGIN_STATICS_PATH',WEB_PATH.'statics/plugin/');
class pc_base {
......
}
对于没有使用框架,也没有使用自定义的MVC模型的程序,就只需要按照传统的脚本格式的访问方式就行,以index.php作为切入点,依次分析相关代码即可。
传统的参数过滤分析,通常会以函数的方式进行过滤,但是某些系统也可能会在参数传入后,进行全局参数过滤,所以我们在审计之前,就需要先了解应用程序采用了那些过滤方法,是否使用了全局参数过滤。
首先以ThinkPHP来说,获取参数的方法有两种,一是通过原生的$_GET
等方式获取参数,二是通过$Request()对象获取参数。在审计thinkPHP程序的时候,就需要对这两种获取参数的方式进行审计。
在ThinkPHP中,会在config文件中定义一个默认的全局过滤器:
// 默认全局过滤方法 用逗号分隔多个
'default_filter' => '',
比如上面这样,默认的过滤器就为空。也就是说在使用$_GET
等原始方法获取参数的时候,就是不会进行过滤的。
然后我们再来看看$request对象获取参数的时候是如何进行的,比如erquest对象的get方法,是用于获取GET方式传入的参数的:
public function get($name = '', $default = null, $filter = '')
{
if (empty($this->get)) {
$this->get = $_GET;
}
if (is_array($name)) {
$this->param = [];
return $this->get = array_merge($this->get, $name);
}
return $this->input($this->get, $name, $default, $filter);
}
可以看到,这里定义了三个形参,分别是name、default、filter。其中name是我们要获取的变量名,而fileter则是我们要是用的过滤器,这里默认为空,也就是不进行任何过滤。
所以在进行审计的时候,我们就需要判断是否设置了默认的全局过滤选项,或者说是否在获取参数的时候,设置了新的过滤器。
对于原生PHP开发的应用程序,往往是通过函数的方式来设置过滤规则,所以就需要在参数获取的地方来看看,是否调用了参数过滤函数来进行过滤,并判断是否是有效过滤。
这里以帝国CMS为例,进行一个简单的分析。帝国CMS的过滤函数在e/class/connect.php中,定义了三个参数处理函数,这里以其中一个为例,进行分析:
//参数处理函数
function RepPostVar($val){
if($val!=addslashes($val))
{
exit();
}
CkPostStrChar($val);
$val=str_replace("%","",$val);
$val=str_replace(" ","",$val);
$val=str_replace("`","",$val);
$val=str_replace("\t","",$val);
$val=str_replace("%20","",$val);
$val=str_replace("%27","",$val);
$val=str_replace("*","",$val);
$val=str_replace("'","",$val);
$val=str_replace("\"","",$val);
$val=str_replace("/","",$val);
$val=str_replace(";","",$val);
$val=str_replace("#","",$val);
$val=str_replace("--","",$val);
$val=RepPostStr($val,1);
$val=addslashes($val);
//FireWall
FWClearGetText($val);
return $val;
}
可见,这里对%、空格、`、%20、%27、‘,“等进行了过滤。
然后我们就可以进入具体的文件中,看看传入的参数是否都调用了该函数进行过滤。
//审核评论
{
$plid=$_POST['plid'];
$id=$_POST['id'];
$bclassid=$_POST['bclassid'];
$classid=$_POST['classid'];
CheckPl_all($plid,$id,$bclassid,$classid,$logininid,$loginin);
}
function CheckPl_all($plid,$id,$bclassid,$classid,$userid,$username){
global $empire,$class_r,$dbtbpre,$public_r;
//验证权限
$restb=(int)$_POST['restb'];
$count=count($plid);
if(empty($count)||!$restb){
printerror("NotCheckPlid","history.go(-1)");
}
if(!strstr($public_r['pldatatbs'],','.$restb.',')){
printerror("NotCheckPlid","history.go(-1)");
}
$add='';
$docheck=(int)$_POST['docheck'];
$docheck=$docheck?1:0;
for($i=0;$i<$count;$i++){
$add.="plid='".intval($plid[$i])."' or ";
}
$add=substr($add,0,strlen($add)-4);
$sql=$empire->query("update {$dbtbpre}enewspl_{$restb} set checked='$docheck' where ".$add);
if($sql)
{
....
}
}
比如这里,后台评论管理的地方,获取了评论给ID等参数,在未经过滤的情况下,传入了CheckPl_all()函数,可见这里的参数都没有调用了过滤函数进行处理,但是好在都对这些参数使用了int强制类型转换。还算是比较安全。
首先需要了解SQL注入常见的业务场景与漏洞类型:
可以说,任何与数据库进行交互的地方都可能存在SQL注入,对于其漏洞存场景也是多种多样,这里并不能很好的进行举例。
对于代码审计中的SQL注入审计方法,首先需要关注的就是数据库操作的关键字。
比如在PHP原生代码中,就可以多关注这些关键字:
select
mysqli_connect
mysqli_query
mysqli_fetch_row
mysqli_fetch_array
update
indert into
delete
.......
在一下CMS或者框架中,就需要多关注一下下面这些关键字或者方法:
name()
where()
find()
select()
.....
通过定位这些关键字,我们就能够定位到执行SQL语句的地方,然后去判断执行SQL语句中的参数是否采用了拼接SQL的方式,如果是则去判断是否存在参数过滤以及参数是否可控。
如果参数不可控,来源于某一条SQL语句查询出的结果,我们就需要重点关注这个参数是否来源于其他的用户输入,如果是,则需要考虑师傅存在二次注入的情况。
XSS漏洞重点需要关注的就是一些输出函数,比如下面这些:
print()
echo
print_f()
die()
var_dump()
print_r()
......
然后去判断输出的内容中是否存在可控变量,并检测这些变量在输如或者输出的时候,是否采用了Html实体编码,或者是否对输入进行了过滤。如果都没有,在输出内容可控的情况下则很可能存在XSS漏洞。
这里的代码执行,包含了代码执行和命令执行两个部分。同样的,重点需要关注的也是一些函数:
代码执行:
eval()
assert()
preg_replace()
array_map()
call_user_funcn()
.....
命令执行:
system()
exec()
shell_exec()
passthru()
popen()
proc_open()
.....
对于代码执行函数来说,eval()函数是最常见的代码执行函数,需要重点关注。
assert()函数在PHP中与eval类似,但是只能执行一行代码,在PHP7中则取消了该函数执行动态代码的功能,也就是说执行执行固定的代码了。
其他的代码执行函数,大多数为回调函数,具备了调用php代码的功能。
对于命令执行漏洞来说,主要还是存在于一些获取系统信息的地方,可能会通过执行命令的方式来获取,若果说执行的命令可控的话,则可能导致命令执行。
对于命令执行的其中几个方法,存在一定的差异:
但是对于代码执行和命令执行需要注意的是,在PHP中,通过{}
的方式同样能够执行代码,同时还能通过动态拼接代码的方式来执行PHP代码。通过使用双反引号的方式,也能够做到等同意system的命令执行效果。
常见的业务场景:
对于文件上传漏洞,需要关注的函数为:move_uplaod_file()函数。
在审计的时候就只需要去搜欧索这一个函数,然后判断是否对文件上传的格式做限制,或者说是否可以绕过文件后缀限制。
如果说不能绕过的话,则需要去检测是否存在文件解析漏洞或者文件包含漏洞,然后进行绕过利用。
对于文件删除漏洞,触发漏洞的函数同样只有一个,那就是unlink()函数,我们在审计的时候,判断文件名是否可控,同时检测是否允许文件命中存在路径穿越符,如果允许,则说明存在任意文件删除漏洞,危害较大。
文件读取或者文件下载漏洞,常见的触发漏洞的函数如下:
file_get_content()
fopen()
readfile()
fread()
file()
......
对于这种漏洞,可以在黑盒的情况下先去找文件读取和下载的的功能点,然后通过请求的URL去分析功能对应的具体代码,再来判断读取的文件,其文件名是否可控,是否允许文件名存在路径穿越符等。
如果存在漏洞的情况下,利用方法一时读取系统文件,而是读取网站源码,在读取的时候,我们可以使用php的伪协议来读取源码。
XXE漏洞,我们需要关心的函数是:simplexml_load_string(),我们需要判断被解析的XML数据是否允许我们外部输入,并且是否允许代用外部实体,然后在进行详细的分析与利用。
SSRF漏洞的引发函数主要是能用于远程请求资源的一些函数,比如:
file-get_content()
curl()
fopen()
readline()
......
常见的漏洞场景为:
对于检测方法主要也是通过判断这些函数获取的URL或者文件名是否可控,如果可控,则可能造成漏洞,同时也容易造成上面提到的任意文件读取漏洞。
首先简单的介绍一下GPC,magic_quotes_gpc是php.ini里面的配置选项,在php5.2以前的版本默认开启,在php版本为5.2—5.4之间的版本则默认关闭。在PHP 5.4 以后的版本中则直接取消了这一个配置项。
该配置项的作用就是对我们传入的POST、GET、COOKIE变量中的'
,"
,\
,NULL
等字符前面加上反斜杠\
进行转义。
在PHP中,卡其GPC的方法有两种,一是在PHP.ini中配置 magic_quotes_gpc 选项的值为 on,还有一种就是在PHP代码中使用get_magic_quotes_gpc()函数判断是否开启该选项,如果没有则使用addslashes()函数进行转义。比如下面这样:
if (!get_magic_quotes_gpc()) { //magic_quotes_gpc 配置为 ON 则返回1,如果配置为OFF 则返回0
$_POST['message'] = addslashes($_POST['message']);
} else {
.......
}
在PHP中,我们提到了POST等变量会受导全局配置GPC的影响,但是有一个变量并不会,那就是$_SERVER,其中获取的http头并不收到GPC的保护,如果说获取的HTTP头存在于数据库交互的行为,则可能导致SQL注入的产生。
同时由于使用了GPC对特殊字符进行转义,我们都知道宽字节注入的漏洞原理,如果说在开启了GPC的情况下,还设置了数据库编码为GBK模式,则可能导致宽字节注入的存在。在PHP中设置数据库编码的方式有下面两种:
方法1: mysqli_set_charset($connnect,'GBK')
方法2: mysqli_query("set names 'gbk'")
这两种方法都能设置数据库的编码模式,在开启了GPC的情况下,就容易出现宽字节注入的问题。
在了解这个问题之前,我们需要知道,在PHP中,报错信息分为多个等级,可以通过配置PHP.ini的display_error =on 或者在代码中使用 error_reporting()来设置报错等级,常见的一些报错等级如下:
1、E_ERROR //致命的运行时错误。这类错误一般是不可恢复的情况,例如内存分配导致的问题。后果是导致脚本终止不再继续运行。
2、E_WARNING //运行时警告 (非致命错误)。仅给出提示信息,但是脚本不会终止运行。
3、E_PARSE //编译时语法解析错误。解析错误仅仅由分析器产生
4、E_NOTICE //运行时通知。
5、E_USER_ERROR //用户产生的错误信息。
6、E_USER_WARNING //用户产生的警告信息。
7、E_USER_NOTICE
8、E_STRICT //启用 PHP 对代码的修改建议,以确保代码具有最佳的互操作性和向前兼容性。
9、E_ALL //E_STRICT除外的所有错误和警告信息。
在PHP中,大多数的错误都会显示错误的文件路径与集体位置,在渗透测试中,经常会遇到上传或者写入webshell的的情况需要知道网站绝对路径,这时候我们就可以考虑使用PHP报错来获取网站路径。
对于PHP程序来说,大多数开发者都会使用trim()函数来去除参数首尾的空调,但是当我们传入的参数是一个数组的时候,比如/index.php?a[]=test
,这时候如果采用trim($_GET('a'))
来去除a参数的空格的话,就会导致程序报错。
类似的函数有很多,比如:
addcslashes()
bin2hex()
chr()
exho()
explode()
crypto()
md5()
..........
字符串截断利用最多的则是在文件上传的时候,但是%00空字符即NULL,在开启了GPC的情况下会受到影响为不能正常利用。同时在PHP5.3以后,修复了这一问题,所以利用场景相对目前来比较少见。这里就只简单的提一下。
但是存在另外一种字符串截断的情况,那就是使用了iconv()函数进行编码转换时,比如UTF-8抓环为GBK编码,总会存在一些差异,导致转换出现乱码。所以在使用了iconv()函数进行转换时,如果出现了错误,则会不在进行转换,导致了字符串被截断的问题。
有大佬测试了相关问题,发现在使用iconv()函数将UTF-8编码准尉GBK编码的情况下,遇到cahr(128)—chr(255)之间的字符否存在被截断的可能。
比如下面这样的情况:
$ip=$_SERVER('HTTP_CLIENT_IP');
if(preg_match('/\d+\.\d+\.\d+\.\d+/',$ip)){
echo $ip;
}
这里如果我们传入client_ip为 127.0.0.1alert(xss)
则会成功的通过正则校验,输出IP信息,导致XSS漏洞的产生。