PHP代码审计18—PHP代码审计小结

Posted W0ngk

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了PHP代码审计18—PHP代码审计小结相关的知识,希望对你有一定的参考价值。

文章目录


前言:经过一段时间的代码审计学习,在这里对代码审计这方面的知识做一个简单的小结,一部分是个人的的知识总结,为了能更全面的总结到代码审计的方法结论,所以也参考了很多的资料,都写在了文末。

一、前期准备

1、工具准备

这里总结我用到过的或者说我了解的比较好的一些工具。

  • php代码调试工具

    • PHPstorm+xdebug
  • 代码阅读工具

    • notePad++
    • cublimeTxt 3
  • 静态代码审计工具

    • Fortify

      HP出的一款静态代码审计工具,支持21种开发语言,是一款功能强大的商业代码审计工具。
      
    • Seay

      Seay大佬协议的代码审计工具,国产、开源。
      
    • CodeQl

      Githubt推出的代码审计项目,在国外受到众多安全研究着追捧。
      
    • Xcheck

      Xcheck的php引擎支持原生php的安全检查,也支持对国内主流框架编写的web应用进行安全检查,覆盖包括Thinkphp,Laravel,CodeIgniter,Yii,Yaf等web框架。
      
    • RIPS

      轻量级的代码审计工具,来源于国外专业的代码审计公司RIPS。但是RIPS的开源版本已经多年未更新,并且不支持面向对象的代码审计。
      
  • 浏览器与插件

    • 浏览器:FireFox
    • 浏览器插件:HackBar、FoxyProxy、EditThisCookie
  • 数据库管理软件

    • Phpmyadmin
    • Navicat

2、审计环境准备

  • 操作系统

    一般采用Windows系统进行审计,对于大多数审计工具来说,Windows系统下图形化界面在使用的时候会更加方便。
    
  • PHP集成环境

    • phpStudy
    • 宝塔面板
  • PHP版本

    对于PHP版本的选择,为了使用实际使用环境,应该尽量使用php5.4以后的版本
    
  • 数据库

    对于数据库,对于PHP程序来说,一般都是采用的mysql,在审计过程中,最好将mysql设置为5.7版本以后。
    

二、了解系统架构

对于系统架构,我们在确认了审计目标之后,在审计之前,需要先了解目标系统的基本架构,比如目录情况如何、是否使用了框架、存在那些路由、有没有安全过滤函数、有没有全局参数过滤等等。这里就要分为有框架和无框架来进行分析。

1、使用了开发框架

1) ThinkPHP框架

这里一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的方式传入变量。

2) laravel框架

基本的目录结构:

 |——app  包含了站点的controllers(控制器),models(模型),views(视图)和assets(资源
 |——bootstrap 存放系统启动时的必要文件,这些文件会被index.php这样的文件调用。
 |——public 系统运行的公开数据,包括静态资CSS文件、js文件等
 |——vender 第三方类库

与thinkPHP还是一样的,主要的应用程序文件都存放在了app目录下。

2、没使用开发框架

在没使用开发框架的情况下,我们就需要去判断应用程序是否采用了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作为切入点,依次分析相关代码即可。

三、参数过滤分析

传统的参数过滤分析,通常会以函数的方式进行过滤,但是某些系统也可能会在参数传入后,进行全局参数过滤,所以我们在审计之前,就需要先了解应用程序采用了那些过滤方法,是否使用了全局参数过滤。

1、MVC模式下的过滤情况分析

首先以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则是我们要是用的过滤器,这里默认为空,也就是不进行任何过滤。

所以在进行审计的时候,我们就需要判断是否设置了默认的全局过滤选项,或者说是否在获取参数的时候,设置了新的过滤器。

2、原生PHP模式下的过滤分析

对于原生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 $dbtbpreenewspl_$restb set checked='$docheck' where ".$add);
	if($sql)
	
		....
  

比如这里,后台评论管理的地方,获取了评论给ID等参数,在未经过滤的情况下,传入了CheckPl_all()函数,可见这里的参数都没有调用了过滤函数进行处理,但是好在都对这些参数使用了int强制类型转换。还算是比较安全。

四、常见漏洞审计方法总结

1、SQL注入

首先需要了解SQL注入常见的业务场景与漏洞类型:

  • 用户登录—万能密码
  • 数据搜索—搜索型注入
  • 获取HTTP头
  • 商品购买—insert、update型注入等
  • 信息查询—union联合注入、报错注入等

可以说,任何与数据库进行交互的地方都可能存在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语句查询出的结果,我们就需要重点关注这个参数是否来源于其他的用户输入,如果是,则需要考虑师傅存在二次注入的情况。

2、XSS漏洞

XSS漏洞重点需要关注的就是一些输出函数,比如下面这些:

print()
echo 
print_f()
die()
var_dump()
print_r()
......

然后去判断输出的内容中是否存在可控变量,并检测这些变量在输如或者输出的时候,是否采用了html实体编码,或者是否对输入进行了过滤。如果都没有,在输出内容可控的情况下则很可能存在XSS漏洞。

3、代码执行

这里的代码执行,包含了代码执行和命令执行两个部分。同样的,重点需要关注的也是一些函数:

代码执行:
eval()
assert()
preg_replace()
array_map()
call_user_funcn()
.....
命令执行:
system()
exec()
shell_exec()
passthru()
popen()
proc_open()
.....

对于代码执行函数来说,eval()函数是最常见的代码执行函数,需要重点关注。

assert()函数在PHP中与eval类似,但是只能执行一行代码,在PHP7中则取消了该函数执行动态代码的功能,也就是说执行执行固定的代码了。

其他的代码执行函数,大多数为回调函数,具备了调用php代码的功能。

对于命令执行漏洞来说,主要还是存在于一些获取系统信息的地方,可能会通过执行命令的方式来获取,若果说执行的命令可控的话,则可能导致命令执行。

对于命令执行的其中几个方法,存在一定的差异:

  • system()是执行系统命令并返回执行结果。
  • exec()则是执行命令后,返回一个结果句柄,并不直接返回结果。
  • shell_exec()则是执行不返回命令执行后的任何信息。

但是对于代码执行和命令执行需要注意的是,在PHP中,通过的方式同样能够执行代码,同时还能通过动态拼接代码的方式来执行PHP代码。通过使用双反引号的方式,也能够做到等同意system的命令执行效果。

4、文件上传、删除、下载

1) 文件上传漏洞

常见的业务场景:

  • 头像上传
  • 备份文件上传
  • 配置文件上传

对于文件上传漏洞,需要关注的函数为:move_uplaod_file()函数。

在审计的时候就只需要去搜欧索这一个函数,然后判断是否对文件上传的格式做限制,或者说是否可以绕过文件后缀限制。

如果说不能绕过的话,则需要去检测是否存在文件解析漏洞或者文件包含漏洞,然后进行绕过利用。

2)文件删除漏洞

对于文件删除漏洞,触发漏洞的函数同样只有一个,那就是unlink()函数,我们在审计的时候,判断文件名是否可控,同时检测是否允许文件命中存在路径穿越符,如果允许,则说明存在任意文件删除漏洞,危害较大。

3)文件下载漏洞

文件读取或者文件下载漏洞,常见的触发漏洞的函数如下:

file_get_content()
fopen()
readfile()
fread()
file()
......

对于这种漏洞,可以在黑盒的情况下先去找文件读取和下载的的功能点,然后通过请求的URL去分析功能对应的具体代码,再来判断读取的文件,其文件名是否可控,是否允许文件名存在路径穿越符等。

如果存在漏洞的情况下,利用方法一时读取系统文件,而是读取网站源码,在读取的时候,我们可以使用php的伪协议来读取源码。

5、XXE漏洞

XXE漏洞,我们需要关心的函数是:simplexml_load_string(),我们需要判断被解析的XML数据是否允许我们外部输入,并且是否允许代用外部实体,然后在进行详细的分析与利用。

6、SSRF漏洞

SSRF漏洞的引发函数主要是能用于远程请求资源的一些函数,比如:

file-get_content()
curl()
fopen()
readline()
......

常见的漏洞场景为:

  • 社交分享
  • 转码服务
  • 在线翻译
  • 没有使用img标签的远程图片加载

对于检测方法主要也是通过判断这些函数获取的URL或者文件名是否可控,如果可控,则可能造成漏洞,同时也容易造成上面提到的任意文件读取漏洞。

五、相关审计技巧总结

1、钻GPC的空子

首先简单的介绍一下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中设置数据库编码的方式有下面两种:

方法1mysqli_set_charset($connnect,'GBK')
方法2mysqli_query("set names 'gbk'")

这两种方法都能设置数据库的编码模式,在开启了GPC的情况下,就容易出现宽字节注入的问题。

2、利用字符处理问题

1)利用字符处理函数报错

在了解这个问题之前,我们需要知道,在PHP中,报错信息分为多个等级,可以通过配置PHP.ini的display_error =on 或者在代码中使用 error_reporting()来设置报错等级,常见的一些报错等级如下:

1E_ERROR             //致命的运行时错误。这类错误一般是不可恢复的情况,例如内存分配导致的问题。后果是导致脚本终止不再继续运行。
2E_WARNING          //运行时警告 (非致命错误)。仅给出提示信息,但是脚本不会终止运行。
3E_PARSE            //编译时语法解析错误。解析错误仅仅由分析器产生
4E_NOTICE           //运行时通知。
5E_USER_ERROR       //用户产生的错误信息。
6E_USER_WARNING     //用户产生的警告信息。
7E_USER_NOTICE
8E_STRICT           //启用 PHP 对代码的修改建议,以确保代码具有最佳的互操作性和向前兼容性。
9E_ALL              //E_STRICT除外的所有错误和警告信息。

在PHP中,大多数的错误都会显示错误的文件路径与集体位置,在渗透测试中,经常会遇到上传或者写入webshell的的情况需要知道网站绝对路径,这时候我们就可以考虑使用PHP报错来获取网站路径。

对于PHP程序来说,大多数开发者都会使用trim()函数来去除参数首尾的空调,但是当我们传入的参数是一个数组的时候,比如/index.php?a[]=test,这时候如果采用trim($_GET('a')) 来去除a参数的空格的话,就会导致程序报错。

类似的函数有很多,比如:

addcslashes()
bin2hex()
chr()
exho()
explode()
crypto()
md5()
..........

2)利用字符串截断

字符串截断利用最多的则是在文件上传的时候,但是%00空字符即NULL,在开启了GPC的情况下会受到影响为不能正常利用。同时在PHP5.3以后,修复了这一问题,所以利用场景相对目前来比较少见。这里就只简单的提一下。

但是存在另外一种字符串截断的情况,那就是使用了iconv()函数进行编码转换时,比如UTF-8抓环为GBK编码,总会存在一些差异,导致转换出现乱码。所以在使用了iconv()函数进行转换时,如果出现了错误,则会不在进行转换,导致了字符串被截断的问题。

有大佬测试了相关问题,发现在使用iconv()函数将UTF-8编码准尉GBK编码的情况下,遇到cahr(128)—chr(255)之间的字符否存在被截断的可能。

3、不严谨的正则表达式

比如下面这样的情况:

$ip=$_SERVER('HTTP_CLIENT_IP');
if(preg_match('/\\d+\\.\\d+\\.\\d+\\.\\d+/',$ip))
	echo $ip;

这里如果我们传入client_ip为 127.0.0.1</scritp>alert(xss)</scritp>

则会成功的通过正则校验,输出IP信息,导致XSS漏洞的产生。

五、参考资料

  • 代码审计方法总结
  • 00×0 前言

    最近也是边挖src边审计代码,总结下最近的php代码审计的一些思路,我一般按照顺序往下做,限于能力水平,可能会有不对或者欠缺的地方,希望各位师傅能够指导。

    00×1 前期工作,需要的工具(我使用的)

    PHPStorm|是PHP编程语言开发的集成环境。

    Fotify|代码审计静态扫描工具,商业化静态代码扫描工具,误报率相对较低。

    seay|源代码审计工具

    CodeQl | 高效的QL非商业的开源代码自动化审计工具。

    xcheck| Xcheck 是一款静态应用安全测试工具,旨在及时发现业务代码中的安全风险,尤其是由不受信输入所触发的安全漏洞。检测范围覆盖主流 Web 安全漏洞,具备速度快、误报低和准确率高等优点。

    chrome & HackerBar插件

    00×3 明确目标

    在审计之前,我们首先先确定自己此次审计的目地,我觉得会有三种情况

    1. 为了提升自己的审计经验
    2. 项目中为了审计出能进一步利用的漏洞,一般需要getshell、ssrf这种级别的。
    3. 为了挖点洞,去换钱或者换cve&cnvd。

    有什么区别呢?

    为了提升审计经验,我会去重点关注历史漏洞,并去复现。

    如果是为了能审出漏洞,去用作渗透中的进一步利用,那么我觉得,可以重点使用xcheck、Fotify等自动化代码审计,然后关注下面的文件上传、包含、sql注入等等有严重危害的漏洞

    如果是为了挖0day,搞证书什么的,那么全方位按步骤过一遍,是不错的选择。

    【一>所有资源获取<一】
    1、200份很多已经买不到的绝版电子书
    2、30G安全大厂内部的视频资料
    3、100份src文档
    4、常见安全面试题
    5、ctf大赛经典题目解析
    6、全套工具包
    7、应急响应笔记
    8、网络安全学习路线

    00×4 判断是否是用了框架

    判断是否使用了框架,是蛮重要的,能帮助我们快速定位有用的函数集,筛选不需要去看的代码。

    一般来说,我觉得使用了框架的更好审计一点,因为使用了框架的,他的函数集文件(各种方法function)会比较规整,在某些固定文件夹中,清晰可见,当然需要我们先对框架有所了解。

    目前比较主流的设计模式是MVC,即多层模型(M)、视图(V)、控制器(C),在此不多赘述,php的主流框架几乎都使用了MVC设计模式。

    PHP底下的开发框架目前见的比较多的有Laravel,ThinkPHP,yii等。

    4.1. ThinkPHP框架

    ThinkPHP这里需要区分TP3和TP5的差别,首先我们先来看看TP3的目录结构。(现在基于TP3的系统都很少了。。。了解一下就好

    其中,Application和Public目录下面都是空的。

    Application是存放项目中的重要的一些函数集,Public是公共文件夹,供用户访问的,重要的函数集千万不能放在此文件夹下。

    Application目录默认是空的,但是第一次访问入口文件会自动生成,参考后面的入口文件部分。其中框架目录ThinkPHP的结构如下:

    另外TP5和TP3实际上差距有点大,先看看TP5下载下来的默认文件结构。其中在public文件下有个route.php文件,它的作用是用于php自带webserver支持,可用于快速测试,启动命令:php -S localhost:8888 router.php。而它的相关网站功能目录也需要从根目录下的index.php入手。

    以下为TP5的目录结构。

    一般如果是审计基于框架的cms,我不会去看框架系统目录,就是上面的ThinkPHP文件夹下的东西,第三方类库vendor也不会去先看,除非是在审计过程中流向了这些文件中,才会大概看一看,而重点在Application文件夹下做文章。

    既然是MVC框架的,那么我们真正关心的是其中的控制器(C),因为功能点大部分都在C上,我们能找到的大部分漏洞也都在C上

    下图为基于TP6的ThinkAdmin项目目录

    app(也就是application),下面有admin、data、index、wechat几个文件夹,每个文件夹代表了一个应用,比如admin一般来说都是后台的服务,wechat为微信应用服务,每个应用下面都有Controller(控制器)、Module(模型)、View(视图,一般是html文件)

    现在目录很明确,目标就很明确,拿到这样基于框架的cms,就应该知道,该重点审计的地方在哪里。

    4.2. Laravel框架

    目录怎么变,MVC架构的重点还是在Controllers里

    4.3. 如果没用框架

    没用框架的话,先搞明白目录结构,一般来说

    审计过程中需要关注几个点:(在我们后面开始审计的过程中,自己要注意这些地方,经常想一想)

    1)函数集文件,通常命名包含function或者common等关键字,这些文件里面是一些公共的函数,提供其他文件统一调用,所以大多数文件都会在文件头部包含到其他文件。寻找这些文件一个非常好用的技巧就是去打开index.php或者一些功能性文件,在头部一般都能找到。

    2)配置文件,通常命名中包括config关键字,配置文件包括web程序运行必须的功能性配置选项以及数据库等配置信息。从这个文件中可以了解程序的小部分功能,另外看这个文件的时候注意观察配置文件中参数值是单引号还是用双引号括起来,如果是双引号可能就存在代码执行的问题了。

    3)安全过滤文件,安全过滤文件对代码审计至关重要,这关系到我们挖掘到的可以点能否直接利用,通常命名中带有filter、safe、check等关键字,这类文件主要是对参数进行过滤,大多数的应用其实会在参数的输入做一下addslashes()函数的过滤。

    4)index文件,index是一个程序的入口,所以通常我们只要读一读index文件就可以大致了解整个程序的架构、运行的流程、包含到的文件,其中核心的文件有哪些。而不同目录的index文件也有不同的实现方式,建议最好将几个核心目录的index文件都通读一遍。

    00×5 了解路由

    我很喜欢Thinkphp这类框架的原因是,他们的路由很好摸清,如果在哪个方法中找到了漏洞,我就能直接根据路由访问这个方法,直接利用。

    了解路由也是为了能快速定位漏洞位置,要不然,你通过审计源码找到的漏洞,却不知道在浏览器中用什么样的url去访问,这不是件很尴尬的事儿吗?

    比如Thinkphp的路由有三种方式

    5.1. 普通模式

    关闭路由,完全使用默认的pathinfo方式URL:

    ‘url_route_on’ => false,

    路由关闭后,不会解析任何路由规则,采用默认的PATH_INFO 模式访问URL:

    module/controller/action/param/value/…

    module就是使用的应用。

    controller是控制器,跟文件名一致。

    action是方法,某控制器下的方法。

    param是需要的变量

    value是参数

    但仍然可以通过Action参数绑定、空控制器和空操作等特性实现URL地址的简化

    5.2. 混合模式

    开启路由,并使用路由+默认PATH_INFO方式的混合:

    ‘url_route_on’ => true,

    该方式下面,只需要对需要定义路由规则的访问地址定义路由规则,其它的仍然按照默认的PATH_INFO模式访问URL。

    5.3. 强制模式

    开启路由,并设置必须定义路由才能访问:

    ‘url_route_on’ => true,
    ‘url_route_must’=> true,
    

    这种方式下面必须严格给每一个访问地址定义路由规则,否则将抛出异常。

    首页的路由规则是 /

    其实,在实际审计过程中,我一般会先去黑盒访问一遍功能点,分析后差不多也能知道路由怎样构成,如果有的地方不清楚,可以去源码中找路由文件

    一般带有route关键词的文件,或文件夹与路由有关。

    分析好路径,之后就可以真正的开始审计。

    00×6 审计

    在人工审计之前,可以使用我之前提到的xcheck、Fotify、codeql等自动化审计工具先审计一遍,根据报告,验证一遍,再往下去根据下面的步骤审一遍,一个项目,也就能审个七七八八了,深层次的利用也就得看自身的实力与经验了。

    如果使用了框架,可以先看看此项目还有没有框架的漏洞存在,我就不再赘述了。

    6.1. 鉴权

    首先对于项目整体的一个权限认证做一个判断,判断是否存在越权,未授权访问的情况。

    一般来说,需要权限认证的地方,是后台管理,即admin应用下的。

    所以对于admin下的控制器这些方法,需要判断是否可以未授权访问。

    目前对于整个后台管理鉴权的方式,一般是采用写一个基类,比如Base.php或者common.php,其中存在鉴权方法,然后在每个控制器类继承这个类。

    比如xiaohuanxiong漫画cms的后台,就是采用了这种方法。

    不过我也看到了,有的比较好的项目,自己二开框架,做了自己的组件,然后,每个类都继承了此组件,也是同样的原理

    比如ThinkAdmin,继承了自己组件的controller。

    我们知道了鉴权的方式,所以我们首先看的是,如果他没有这些鉴权方式,或者其他鉴权方式也没有,那么他就会存在未授权访问,即不登录也能访问后台功能。这是很危险的,一个是管理员才能看到的敏感信息,未授权就能看到,更危险的是,结合后台的漏洞,直接未授权getshell也是很有可能的,所以鉴权我们首先去看,而且容易去看的地方。

    6.2. 按照漏洞类型审计

    我认为对于我来说,比较好的审计方法是黑盒白盒一起,根据漏洞类型一个一个的去找寻可能存在漏洞的地方,然后再回溯查看是否用户可控,以此快速定位漏洞。

    所以一般我是根据漏洞类型,以及每个漏洞可能涉及的危险函数,去快速定位。

    那一般看的地方有SQL注入、XSS、CSRF、SSRF、XML外部实体注入等等

    6.2.1. sql注入

    1. 如果使用了框架,可以分辨一下框架名称以及版本,去搜索一下该版本的框架是否存在漏洞,如果存在再去cms中验证。因为本篇文章主要讲我自己在cms审计上的一些经验,因此不多深入框架的审计部分。
    2. 如果没有使用框架,则需要仔细的观察数据库函数,一般来说,cms是将select、insert等函数进行了封装的,比如$db->table(‘test’)->where(“name=admin”)便是select * from test where name=admin这种格式,而此时若是发现cms使用的是过滤+拼接,那么很有可能会出现问题,而如果使用了PDO,则继续跟进涉及到table,order by等字段的拼接去,因为这些字段是无法使用PDO的。

    审计要素:

    • 参数是否用户可控
    • 是否使用了预编译

    那么首先,如果没有使用框架封装的sql语句,那么全局搜索insert、select等sql语句关键词,然后定位到具体的语句,然后查看里面有没有拼接的变量,回溯可不可控。如果可控并且存在字符串拼接,很有可能就存在漏洞。

    使用了框架的就是搜索的关键词不一样,还是得看是否存在字符串拼接,可不可控。

    即使使用了预编译,但是如果在预编译之前字符串拼接了,那照样没有鸟用,该注入还是能注入。

    下面提供一般我会搜索的关键词(框架的根据你审计项目的框架的手册,自行搜索。)师傅们有想补充的也可以补充。

    insert
    create
    delete
    update
    order by
    group by
    where
    from
    limit
    desc
    asc
    union
    select
    

    6.2.2. xss漏洞

    审计要素

    • 是否存在全局参数过滤器,过滤规则是否符合安全要求,是否存在需过滤和不需过滤两种输出,页面是否控制恰当。
    • 输出时是否进行编码(HTML、JS等)。
    • 前端是否采用了Angularjs、React、vue.js等具有XSS防护功能的前端框架进行数据输出。

    这个的话,我就不会关键词搜了,我就是会在寻找其他漏洞的过程中,注意有没有直接把输入原样输出的地方,也没有特别关注这一块。

    如果想特意挖掘这一块,可以

    查看是否配置了全局的拦截器、过滤器。检查数据输出函数,例如常用的输出函数有print、print_r、echo、printf、sprintf、die、var_dump、var_export

    6.2.3. CSRF漏洞

    与XSS攻击相比,CSRF攻击往往不大流行(因此对其进行防范的资源也相当稀少)和难以防范,所以被认为比XSS更具危险性。

    审计要素

    • 是否在表单处存在随机token。
    • 是否存在敏感操作的表单。

    CSRF主要利用场景实际上是一些越权的操作,或者一些敏感功能存在的地方,例如管理后台、会员中心等地方。我们可以尝试搜索表单位置,查看是否会生成随机token,在查看后端代码中是否会先验证这部分的token。如果没有验证token,再进一步看看是否有refer的相关验证,如果没有,那么就存在CSRF的问题。

    可以尝试全局搜索

    csrf-token
    csrf_token
    csrftoken
    csrf
    

    下面是一个更新密码的操作,假设构造一个链接为 http://127.0.0.1/index.php?password_new=password&password_conf=password&Change=Change#的链接,直接发送给受害者点击,那么当前情况下,可以直接修改受害者的密码,因为没有进行任何的验证措施。当然一般代码不会这么写,只是拿DVWA的CSRF举个例子。

    6.2.4. SSRF漏洞

    ssrf是利用存在缺陷的web应用作为代理攻击远程和本地的服务器。常见的方式如下:

    1.可以对外网、服务器所在内网、本地进行端口扫描,获取一些服务的banner信息;

    2.攻击运行在内网或本地的应用程序(比如溢出);

    3.对内网web应用进行指纹识别,通过访问默认文件实现;

    4.攻击内外网的web应用,主要是使用get参数就可以实现的攻击(比如struts2,sqli等);

    5.利用file协议读取本地文件等。

    审计要素:

    • 是否存在可以产生SSRF漏洞的函数。
    • 是否存在内网ip地址正则过滤,且正则是否严谨。
    • 是否存在限制请求的方式只能为HTTP或者HTTPS。

    当然PHP底下经常可能会出现SSRF漏洞的主要有几个函数,它们分别是file_get_contents()、fsockopen()、curl_exec()、get_headers()。通过全文关键函数搜索,在看是否限制了访问端口,访问协议,内网ip地址等。

    利用file://、http/https:// 、dict://、gopher://协议去搞内网。

    列一下,我经常搜索的关键词

    file_get_contents
    fsockopen
    curl_exec
    get_headers
    fopen
    readfile
    

    注意

    1. 一般情况下PHP不会开启fopen的gopher wrapper
    2. file_get_contents的gopher协议不能URL编码
    3. file_get_contents关于Gopher的302跳转会出现bug,导致利用失败
    4. curl/libcurl 7.43 上gopher协议存在bug(%00截断) 经测试7.49 可用
    5. curl_exec() //默认不跟踪跳转,
    6. file_get_contents() // file_get_contents支持 php://input协议

    各种绕过,我就不在这说了。

    6.2.5. XML外部实体注入

    审计要素

    • 参数是否用户可控
    • 是否libxml版本为2.9.0以上
    • 是否禁用了外部实体

    这个一般我关注的少,仅仅是搜索“DOMDocument”,“SimpleXMLElement”和“simplexml_load_string”等关键词,分析下是否存在参数拼接的XML字符串,或未做限制的批量解析方法。对参数进行回溯,判断其是否用户可控。

    6.2.6. 文件包含漏洞

    审计要素

    • 参数是否用户可控
    • 是否存在include,require,include_once, require_once等函数。

    文件包含算是拿shell最快的方法了,所以一般要重点关注。

    无非是include,require,include_once, require_once这四个函数,全局搜索这四个函数,一个一个去看,去回溯,查看变量可不可控。

    6.2.7. 文件上传漏洞

    审计要素

    • 是否检查了上传文件的文件类型
    • 是否限制了文件上传路径
    • 是否对文件进行了重命名
    • 文件大小是否限制
    • 是否返回了文件路径或文件路径很好猜测

    有的项目,会对文件上传下载进行分装,所以可以全局搜索有关upload、file的函数,看看是不是封装了

    function upload
    function file
    

    如果封装了,那么就看这些封装好的函数,有没有上面提到的审计要素的漏洞。

    如果没封装,一般是move_uploaded_file这个函数,全局搜索,这个函数,回溯查看这些漏洞存不存在。(白盒黑盒一起搞比较好。)

    6.2.8. 变量覆盖

    审计要素

    • 是否存在造成变量覆盖的函数,例如:extract()、parse_str()、import_request_variables和$$等。
    • 是否存在可以完整利用的攻击链。

    一般就这几个函数和关键词

    extract
    parse_str
    import_request_variables
    mb_parse_str
    $$
    

    不过还有个特殊的配置,也可能造成变量覆盖

    register_globals全局变量覆盖

    php.ini中有一项为register_globals,即注册全局变量,当register_globals=On时,传递过来的值会被直接的注册为全局变量直接使用,而register_globals=Off时,我们需要到特定的数组里去得到它。

    注意:register_globals已自 PHP 5.3.0 起废弃并将自 PHP 5.4.0 起移除。

    当register_globals=On,变量未被初始化且能够用户所控制时,就会存在变量覆盖漏洞:

    <?php
    echo “Register_globals: “ . (int)ini_get(“register_globals”) . “<br/>“;
    
    if ($a) 
    echo “Hacked!”;
    
    ?>
    

    通过GET和POST方式输入变量a的值:

    当然,也可以从COOKIE中输入:

    6.2.9. 代码执行漏洞

    审计要素

    • php.ini文件中的disable_function是否有禁用函数。
    • 是否存在代码执行的敏感函数。
    • 是否输入变量可控。

    全局搜索下面的关键词,回溯参数可不可控。

    eval
    asser
    preg_replace
    create_function
    array_map
    call_user_func
    call_user_func_array
    array_filter
    usort
    uasort
    $a($b)(动态函数)
    

    6.2.10. 命令执行漏洞

    审计要素

    • 参数是否用户可控
    • 是否配置了全局过滤器,过滤规则是否符合安全规范
    • 是否所有的命令执行参数都经过了过滤器,或受白名单限制

    全局搜索下面的关键词,回溯参数可不可控。

    exec
    passthru
    proc_open
    shell_exec
    system
    pcntl_exec
    popen
    

    “(被反引号包裹的变量也可以执行)

    6.2.11. 任意文件下载/下载漏洞审计

    审计要素

    • 是否存在…/、.、…\\等特殊字符过滤。
    • 参数是否用户可控
    • 是否配置了相对路径或者绝对路径。

    查询这些关键词,查看变量是否可控,是否有过滤

    fgets
    fgetss
    file_get_contents
    readfile
    parse_ini_file
    highlight_file
    file
    fopen
    readfile
    fread
    

    **Tip:**前两天遇到个,过滤了config/database.php这样的正则匹配,还过滤了…,目的是防止目录穿越,读取服务器其他目录的文件,可是没过滤一个.

    这样我使用config/./database.php绕过了正则,照样把敏感文件读取出来了。。。

    6.2.12. 任意文件删除

    和上面的下载一样

    搜索的关键词变了

    rmdir
    unlink
    

    6.2.13. 任意文件写入

    还是一样,关键词为

    copy
    file_put_contents
    fwrite
    

    6.2.14. 会话认证漏洞

    会话认证漏洞实际上涉及的方面比较广,如cookie、session、sso、oauth等,当然这个漏洞比较常见是在cookie上,服务端直接取用cookie中的数据而没有校验,其次是cookie加密数据在可预测的情况下。

    审计要素

    • 是否cookie中的加密数据可预测。
    • 是否cookie中的数据可预测。
    • 服务端是否只依赖cookie来判断用户身份。

    全局去寻找cookie生成的逻辑,判断是否可预测,判断用户身份是否只依赖cookie,而不是随机的,比如

    鉴权是只通过cookie中的userid来判断,如果我遍历userid,可以达到登录绕过或越权的目地。

    6.2.15. 反序列化漏洞

    一般实际审计的时候,项目中见的比较少,框架中见的比较多。

    全局搜索serialize。看看存不存在可控变量。

以上是关于PHP代码审计18—PHP代码审计小结的主要内容,如果未能解决你的问题,请参考以下文章

18-PHP代码审计——kkcms逻辑漏洞分析(验证码重放)

MKCMS代码审计小结

代码审计学php还是java

代码审计思路之PHP代码审计

PHP代码审计 | 记一次CMS代码审计

CTF-代码审计——parse_str()变量覆盖