PHP代码审计基础

代码审计入门的小总结

常见 PHP 框架

  • ThinkPHP
  • Yaf
  • Laravel
  • Kohana
  • Codelgniter
  • Yii
  • Smyfony
  • doitphp

先看用户手册

处理流程

获取请求 =》全局过滤 =》模块文件 =》C函数内容 =》M函数内容 =》V显示

网站目录结构

  • 主目录

  • 模块目录

  • 插件目录

  • 上层目录

  • 模板目录

  • 数据目录

  • 配置目录

  • 配置文件

  • 公共函数文件

  • 安全过滤文件

  • 数据库结构

  • 入口文件

常见方法

通读原文

  • 函数集文件
  • 配置文件
  • 安全过滤文件
  • index 文件

适用于比较小的网站或者 CMS

敏感关键字回溯参数

这是常见方法,但是不能了解程序的基本框架,覆盖不了逻辑漏洞

查找可控变量

  • 可控变量

  • 进入函数的变量

功能点定向审计

  • 程序安装
  • 文件上传
  • 文件管理
  • 登陆验证
  • 备份恢复
  • 找回密码

PHP核心配置

语法

  • 大小写敏感
  • 运算符:|, &, ~, !
  • 空值:foo = ; 或者 foo = none;

安全模式

  • 安全模式

    safe_mode = off

    限制文档的存取,限制环境变量的存取,控制外部程序的执行

    在 PHP5.4.0 被移除

  • 限制环境变量存取

    safe_mode_allowed_env_vars = string

    指定 PHP 程序可以改变的环境变量的前缀

  • 外部程序执行目录

    safe_mode_exec_dir = "path"

  • 禁用函数

    disable_functions =

控制变量

  • 全局变量注册开关

    register_globals = off

    off 时服务端使用 $_GET[‘name’] 获取数据,on 时服务端通过 POST 或 GET 提交的数据将使用全局变量来接收

  • 魔术引号自动过滤

    magic_quotes_gpc = on

    在 PHP5.4.0 被移除

远程文件

  • 是否允许包含远程文件

    allow_url_include = off

  • 是否允许打开远程文件

    allow_url_open = off

目录权限

  • HTTP 头部版本信息

    expose_http = off

  • 文件上传临时目录

    upload_tmp_dir =

  • 用户可访问目录

    open_basedir = path

错误信息

  • 内部错误选项

    display_errors = on

  • 错误报告级别

    error_reporting = E_ALL&~E_NOTICE

审计中涉及的超全局变量

  • 全局变量

    在函数外面定义的变量,不能在函数中直接使用。在函数中使用时加上global

  • 超全局变量

    作用域在所有脚本,比如$_GET,$_SERVER。除$_GET, $_POST, $_SERVER, $_COOKIE等之外的超全局变量保存在 $GLOBALS 数组中

$GLOBALS

  • global

    定义全局变量,只应用于当前网页而不是整个网站,可以视为参数的传递

  • $GLOBALS

    在 PHP 脚本中的任意位置访问全局变量,可以视为变量的作用域设置全局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$var1 = 1;
$var2 = 2;

function test1(){
$GLOBALS['var1'] = $GLOBALS['var2'];
}
test1();
echo $var1; //2

function test2(){
global $var1,$var2;
$var1 = $var2
}
test2();
echo $var1; //2
?>

$_POST 和 $_GET

  • POST

    隐藏传参,将表单内各个字段与其内容放在 Request Header 内传给服务器

  • GET

    URL 传参,将参数放在提交表单的 ACTION 属性所指的 URL 中

$_REQUEST

  • PHP 中 $_REQUEST 可以获取 以 POST 和 GET 方法提交的数据
  • 尽量不要使用

$_SERVER

  • 这种超全局变量保存关于报头、路径和脚本位置的信息
  • 是一个包含了诸如头信息(header)、路径(path)、以及脚本位置(script locations)等等信息的数组。这个数组中的项目由 Web 服务器创建。不能保证每个服务器都提供全部项目;服务器可能会忽略一些,或者提供一些没有在这里列举出来的项目。
  • 数组

$_FILE

  • 保存上传文件的信息
  • 数组

$_SESSION

  • 保存 SESSION 信息
  • 数组
  • 保存 COOKIE 信息
  • 数组

$_ENV

  • 包含服务器环境变量的数组
  • 只是被动的接受服务器端的环境变量转换为数组

变量覆盖

  • 变量未初始化,我们自定义的参数值可以替换程序原有的变量值

$$

1
2
3
4
5
6
7
8
<?php
$x = '123';
$b = '456';

$x = $_GET['x'];
eval("var_dump($$x);");
eval("var_dump($x);");
?>

变量 x 初始化为 ‘123’

传入参数 ?x=b,$$x 就相当于 $b,这时的输出为 string(3) “456”,string(1) “b”

传入参数 ?x=x=789,$$x 相当于 ${x=789},这时输出为 int(789),int(789),x 值以被覆盖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
include "flag.php";

$_403 = "Access Denied";
$_200 = "Welcome Admin";

if ($_SERVER["REQUEST_METHOD"] != "POST")
die("CTF is here :p…");

if ( !isset($_POST["flag"]) )
die($_403);

foreach ($_GET as $key => $value)
$$key = $$value;

foreach ($_POST as $key => $value)
$$key = $value;

if ( $_POST["flag"] !== $flag )
die($_403);

echo "This is your flag : ". $flag . "\n";
die($_200);
?>

payload:?_200=flag post:flag=1

通过 $$key=$$value 将 flag 的值赋给 _200,post 中的 flag 为:${flag}=1,所以 post 的值永远和 $flag 相同,接着利用 die($_200) 将真实的 flag 输出

extract()

extract(array,extract_rules,prefix)

extract() 函数使用数组键名作为变量名,使用数组键值作为变量值,创建这些变量。该函数返回成功设置的变量数目。

extract_rules 参数:

  • EXTR_OVERWRITE - 默认。如果有冲突,则覆盖已有的变量。
  • EXTR_SKIP - 如果有冲突,不覆盖已有的变量。
  • EXTR_PREFIX_SAME - 如果有冲突,在变量名前加上前缀 prefix。
  • EXTR_PREFIX_ALL - 给所有变量名加上前缀 prefix。
  • EXTR_PREFIX_INVALID - 仅在不合法或数字变量名前加上前缀 prefix。
  • EXTR_IF_EXISTS - 仅在当前符号表中已有同名变量时,覆盖它们的值。其它的都不处理。
  • EXTR_PREFIX_IF_EXISTS - 仅在当前符号表中已有同名变量时,建立附加了前缀的变量名,其它的都不处理。
  • EXTR_REFS - 将变量作为引用提取。导入的变量仍然引用了数组参数的值。
1
2
3
4
5
6
7
8
<?php
$a = "Original";
$my_array = array("a" => "Cat", "b" => "Dog", "c" => "Horse");
echo $a;
extract($my_array);
echo "\$a = $a; \$b = $b; \$c = $c";
//Original $a = Cat; $b = Dog; $c = Horse
?>
1
2
3
4
5
6
7
8
<?php
if($_SERVER["REQUEST_METHOD"]=="POST"){
extract($_POST);
if($pass == $password_hard){
echo "peri0d".'<br>';
}
}
?>

payload:post:pass=123&password_hard=123

传入的 $_POST 是一个数组,为 array(2) {["pass"]=>string(3) "123" ["password_hard"]=>string(3) "123"}

parse_str()

parse_str(string,array)

parse_str() 函数把查询字符串解析到变量中。如果未设置 array 参数,由该函数设置的变量将覆盖已存在的同名变量。

1
2
3
4
5
6
7
<?php
$name = 'peri0d';
parse_str('name=peri0d_2&sex=1');
echo $name."<br>";
echo $sex;
//peri0d_2 1
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
if(empty($_GET['x'])){
show_source(__FILE__);
die();
}else{
include('flag.php');
$m = "guest";
$x = $_GET['x'];
@parse_str($x);

if($m[0] == "admin"){
echo $flag;
}else{
exit("so easy!");
}
}
?>

payload:?x=m[0]=admin

parse_str($x) 即为 parse_str(m[0]=admin),实现变量覆盖。

反序列化漏洞

序列化和反序列化

  • 序列化:把一个复杂的数据类型压缩为一个字符串
  • 反序列化:把一个字符串恢复成复杂的数据类型
1
2
3
4
5
6
7
8
<?php
$x = "peri0d 2019";
$y = array("peri0d",2019);
echo serialize($x).'<br>';
echo serialize($y);
//s:11:"peri0d 2019";
//a:2:{i:0;s:6:"peri0d";i:1;i:2019;}
?>

漏洞成因

  • 反序列化对象中存在魔术方法,而且魔术方法中的代码可以被控制,漏洞根据不同的代码可以导致各种攻击
  • unserialize 函数的变量可控
  • php 文件存在可利用的类,类中有魔术方法

序列化的不同结果

  • public
  • private
  • protect
1
2
3
4
5
6
7
8
9
10
<?php
class test{
private $x = "peri0dx";
public $y = "peri0dy";
protected $z = "peri0dz";
}
$t = new test();
echo serialize($t);
//O:4:"test":3:{s:7:"testx";s:7:"peri0dx";s:1:"y";s:7:"peri0dy";s:4:"*z";s:7:"peri0dz";}
?>

魔术方法

  • construct() : 当一个类被创建时自动调用
  • destruct() : 当一个类被销毁时自动调用
  • invoke() : 当把一个类当作函数使用时自动调用
  • toString() : 当把一个类当作字符串使用时自动调用
  • wakeup() : 当调用unserialize()函数时自动调用
  • sleep() : 当调用serialize()函数时自动调用
  • call() : 当要调用的方法不存在或权限不足时自动调用
  • get() : 这个方法用来获取私有成员属性值的,有一个参数,参数传入你要获取的成员属性的名称,返回获取的属性值
  • set() : 将数据写入不可访问属性

例子

CVE-2016-7124

弱类型

变量类型

  • 标准类型:布尔,整型,浮点,字符
  • 复杂类型:数据,对象
  • 特殊类型:资源

操作之间的比较

  1. 字符串和数字

    1
    2
    3
    4
    5
    6
    <?php
    var_dump(0 == "admin"); //T
    var_dump("1admin" == 1); //T
    var_dump("admin1" == 1); //F
    var_dump("admin1" == 0); //T
    ?>
  2. 数字和数组

    1
    2
    3
    4
    5
    <?php
    $arr = array();
    var_dump(0 == $arr); //F
    var_dump(123 == $arr); //F
    ?>
  3. 字符串和数组

    1
    2
    3
    4
    5
    <?php
    $arr = array();
    var_dump('0' == $arr); //F
    var_dump('123' == $arr); //F
    ?>
  4. “合法数字+e+合法数字” 类型的字符串

    1
    2
    3
    4
    5
    <?php
    var_dump("0e1234" == "0e56789"); //T
    var_dump("1e1123" == "10"); //F
    var_dump("1e1" == "10"); //T
    ?>
  5. == 和 ===

    在PHP里面 == 比较指比较值,不同类型会转换成同一类型比较。用 === 比较时,必须值和类型都一样才为true

empty 与 isset

  • 变量为:0, “0”, null, false, array() 时,使用 empty 函数,返回值为 true
  • 变量未定义或为 null 时,isset 函数返回 false,其他都返回 true

md5 函数

传入数组进行比较时全为 true

1
2
3
4
5
<?php
$arr1 = array('test1', 'test2', '2019');
$arr2 = array('test3', 'test4', '2019');
var_dump(md5($arr1) == md5($arr2)); //T
?>

strcmp 函数

strcmp(string1, string2)

比较 string1 和 string2。如果相等返回 0;如果 string1 小于 string2,返回 <0;如果 string1 大于 string2,返回 >0

1
2
3
4
5
6
7
8
9
10
<?php
$pass = '123456';
if(isset($_GET['pwd'])){
if(strcmp($_GET['pwd'], $pass) == 0){
echo 'success';
}else{
echo 'fail';
}
}
?>

payload:?pwd[]=1

in_array() 函数搜索数组中是否存在指定的值。如果在数组中找到值则返回 TRUE,否则返回 FALSE。

bool in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] )

array_search() 函数在数组中搜索某个键值,并返回对应的键名。如果在数组中找到指定的键值,则返回对应的键名,否则返回 FALSE。如果在数组中找到键值超过一次,则返回第一次找到的键值所匹配的键名。

array_search(value, array, strict)

switch

如果 switch 是数字类型的 case 判断时,switch 会将参数转换为 int 类型

伪协议

file://

  • 用于访问本地系统文件,不受 allow_url_fopenallow_url_include 影响
  • 常与文件包含结合在一起使用

php://filter

  • 读取源代码并以base-64编码形式输出,不受 allow_url_fopenallow_url_include 影响
  • 常与文件包含结合在一起使用
  • 经典用法:?file=php://filter/read=convert.base64-encode/resource=./index.php

php://input

  • 可以访问请求的原始数据的只读流,allow_url_include 为 on 时可以使用,不受 allow_url_fopen 影响

会话认证漏洞

  • Session 固定攻击
  • Session 劫持攻击
  • 通常出现在 cookie 验证上,通常不使用 session 认证

Session 劫持攻击

  • 获取用户的 session id,然后修改数据

Session 固定攻击

  • 用户使用了黑客发送的 session id,网站就不会给用户发送 session id