代码审计入门的小总结
常见 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 脚本中的任意位置访问全局变量,可以视为变量的作用域设置全局
<?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
- 保存 COOKIE 信息
- 数组
$_ENV
- 包含服务器环境变量的数组
- 只是被动的接受服务器端的环境变量转换为数组
变量覆盖
- 变量未初始化,我们自定义的参数值可以替换程序原有的变量值
$$
<?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 值以被覆盖
<?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 - 将变量作为引用提取。导入的变量仍然引用了数组参数的值。
<?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
?>
<?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 参数,由该函数设置的变量将覆盖已存在的同名变量。
<?php
$name = 'peri0d';
parse_str('name=peri0d_2&sex=1');
echo $name."<br>";
echo $sex;
//peri0d_2 1
?>
<?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),实现变量覆盖。
反序列化漏洞
序列化和反序列化
- 序列化:把一个复杂的数据类型压缩为一个字符串
- 反序列化:把一个字符串恢复成复杂的数据类型
<?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
<?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
弱类型
变量类型
- 标准类型:布尔,整型,浮点,字符
- 复杂类型:数据,对象
- 特殊类型:资源
操作之间的比较
字符串和数字
<?php var_dump(0 == "admin"); //T var_dump("1admin" == 1); //T var_dump("admin1" == 1); //F var_dump("admin1" == 0); //T ?>
数字和数组
<?php $arr = array(); var_dump(0 == $arr); //F var_dump(123 == $arr); //F ?>
字符串和数组
<?php $arr = array(); var_dump('0' == $arr); //F var_dump('123' == $arr); //F ?>
"合法数字+e+合法数字" 类型的字符串
<?php var_dump("0e1234" == "0e56789"); //T var_dump("1e1123" == "10"); //F var_dump("1e1" == "10"); //T ?>
== 和 ===
在PHP里面 == 比较指比较值,不同类型会转换成同一类型比较。用 === 比较时,必须值和类型都一样才为true
empty 与 isset
- 变量为:0, "0", null, false, array() 时,使用 empty 函数,返回值为 true
- 变量未定义或为 null 时,isset 函数返回 false,其他都返回 true
md5 函数
传入数组进行比较时全为 true
<?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
<?php
$pass = '123456';
if(isset($_GET['pwd'])){
if(strcmp($_GET['pwd'], $pass) == 0){
echo 'success';
}else{
echo 'fail';
}
}
?>
payload:?pwd[]=1
in_array() 与 array_search()
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_fopen 和 allow_url_include 影响
- 常与文件包含结合在一起使用
php://filter
- 读取源代码并以base-64编码形式输出,不受 allow_url_fopen 和 allow_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