前言

代码审计,是对应用程序源代码进行系统性检查的工作。 目的是为了找到并且修复应用程序在开发阶段存在的一些漏洞或者程序逻辑错误,避免程序漏洞被非法利用给企业带来不必要的风险.

基础

phpinfo

phpinfo(int $flags = INFO_ALL): bool
flags:
INFO_ALL -1 显示以上所有信息。(默认)
INFO_GENERAL 1 配置的命令行、php.ini 的文件位置、建立的时间、Web 服务器、系统及更多其他信息。
INFO_ENVIRONMENT 16 环境变量信息也可以用 $_ENV 获取。
INFO_VARIABLES 32 显示所有来自 EGPCS (Environment, GET, POST, Cookie, Server) 的 预定义变量。

phpinfo()函数返回的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Core
PHP_VERSION php版本号
allow_url_fopen
allow_url_include
open_basedir 访问限制
disable_functions
upload_tmp_dir 临时目录
Loaded Configuration File php.ini地址

PHP Variables
$_SERVER['DOCUMENT_ROOT'] 网站绝对路径
$_SERVER['SERVER_SOFTWARE'] 服务器软件
$_SERVER['HTTP_HOST'] 网站真实ip
$_SERVER['PATH_INFO'] url访问目录
$_SERVER['QUERY_STRING'] = url参数
$_SERVER['REQUEST_URI'] $_SERVER['PATH_INFO']+$_SERVER['QUERY_STRING']

php预变量

常量

1
2
3
4
5
6
7
8
PHP_VERSION     PHP_VERSION
__NAMESPACE__ 当前命名空间名称的字符串
__FILE__ 文件的完整路径和文件名。
__DIR__ 文件所在的目录。等价于 dirname(__FILE__)。目录名不包括末尾的斜杠.
__FUNCTION__ 当前函数的名称。匿名函数则为 {closure}。
__CLASS__ 当前类的名称。
defined('FOO') or define('FOO', 'something');||const FOO="something"; 自定义常量
PHP_OS 运行 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
29
30
31
    $_SERVER['HTTP_HOST']   主机地址

$_SERVER['PHP_SELF'] 当前执行脚本的文件名

  $_SERVER['SERVER_ADDR'] 当前运行脚本所在的服务器的ip地址。

  $_SERVER['REQUEST_METHOD'] 访问页面使用的请求方法;例如,'GET'、'HEAD'、'POST'、'PUT'。

  $_SERVER['REMOTE_ADDR'] 正在浏览当前页用户的ip地址。

$_SERVER['REQUEST_URI'] URI 用来指定要访问的页面。例如 “/index.html”。

$_SERVER['SCRIPT_FILENAME'] 当前执行脚本的绝对路径

除了上面列出的元素之外,PHP 还将使用请求报头中的值创建其它元素,这些条目将命名为 HTTP_ 后跟报头名称,大写且使用下划线而不是连字符。
例如 Accept-Language 报头将作为 $_SERVER['HTTP_ACCEPT_LANGUAGE'] 提供。
$_FILES 文件上传变量,此数组的概况在文件上传处

  $_COOKIE 通过HTTPCookie传递到脚本的信息。这些是由执行php脚本时,通过setcookie()设置的。

  $_SESSION 包含与所有会话变量有关的信息。$_SESSION变量主要应用于会话控制和页面间值的传递。

$_ENV 环境变量.读$ip = getenv('REMOTE_ADDR'),写putenv("UNIQID=$uniqid");

$_REQUEST 设置request默认以 $_GET,$_POST 和 $_COOKIE 的顺序。即在request数组中post可以覆盖get

  $_POST 通过post方法传递的参数信息。

  $_GET 通过get方法传递的参数信息。

$_GLOBALS 由所有已定义的全局变量组成的数组(数值不受局部变量的干扰)。数组的键就是变量的名字。

命名空间

定义命名空间

PHP 命名空间类似与文件系统,可分为相对命名空间和绝对命名空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//file1.php
<?php
namespace Foo\Bar\subnamespace;
class foo{static function staticmethod() {}}
?>

//file2.php
<?php
namespace Foo\Bar;
include 'file1.php';
class foo{ static function staticmethod() {}}
/* 非限定名称 */
foo::staticmethod(); // 解析为类 Foo\Bar\foo 的静态方法 staticmethod
/* 限定名称 */
subnamespace\foo::staticmethod(); // 解析为类 Foo\Bar\subnamespace\foo,
// 以及类的方法 staticmethod
/* 完全限定名称 */
\Foo\Bar\foo::staticmethod(); // 解析为类 Foo\Bar\foo, 以及类的方法 staticmethod
?>

使用命名空间

use是使用命名空间,相当于java中的导包,前提是包中的文件需要提前require或者include进来

1
2
3
4
5
6
7
8
//导入类
use Think\Storage;
//导入类同时起别名
use My\Full\Classname as Another;
//导入函数
use function some\namespace\fn_a;
//导入常量
use const some\namespace\ConstA;

自动加载

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
// function __autoload($class) {
// include 'classes/' . $class . '.class.php';
// }
function my_autoloader($class) {
include 'classes/' . $class . '.class.php';
}
spl_autoload_register('my_autoloader');
// 或者可以使用匿名函数
spl_autoload_register(function ($class) {
include 'classes/' . $class . '.class.php';
});
?>

php反射

在 PHP 中,反射(Reflection)是一种机制,用于在运行时动态地获取类、接口、函数、方法等的信息。反射机制允许我们在运行时分析和修改代码结构,包括类的属性和方法等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyClass
{
private $myProperty = 'hello';

public function myMethod($param1, $param2)
{
echo $this->myProperty . ' ' . $param1 . ' ' . $param2;
}
}

$reflection = new ReflectionClass('MyClass');//使用 ReflectionClass 类来获取 MyClass 类的反射对象

$property = $reflection->getProperty('myProperty');//getProperty() 方法来获取 $myProperty 属性的反射对象
$property->setAccessible(true);//使用 setAccessible() 方法将其设为可访问
$property->setValue(new MyClass(), 'world');//使用 setValue() 方法修改了 $myProperty 的值为 world

$constructor = $reflect->getConstructor();// 一个 ReflectionMethod 对象,反射了类的构造函数,或者当类不存在构造函数时返回 null。
$constructor->getNumberOfParameters()//以数组的形式返回参数列表。顺序为源码中定义的顺序。

$method = ReflectionMethod("MyClass", "myMethod");// objectOrMethod 包含方法的类名或者对象(类的实例)。method方法名。
$method->invokeArgs($object,$args);//将参数作为数组传递给函数,object调用方法的对象,如果是静态对象,设置为 null,args使用 array 传送的方法参数。

回调

如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,就说这是回调函数。能够降低系统的复杂度,提高代码的可维护性。

验证

is_callable 验证值是否可以在当前范围内作为函数调用。

PHP的url解析特性

在php中””包裹的字符串是可以被进一步解析的,例echo("$_GET[a]");,而’’包裹的字符串则不能,PHP的url参数的值可视为包裹在””的字符串

  1. 查询字符串解析
    值得注意的是,查询字符串在解析的过程中会将某些字符删除或用下划线代替。例如,/?%20news[id%00=42会转换为Array([news_id] => 42)

1567560448_5d6f13004035f

[会与\匹配(匹配遵循就进原则)没有匹配上的[不会被转义,即匹配成功就不会进行第二次替换,所以可以利用[传入含有特殊字符的变量,如/?my[secret.flag=123=my.secret.flag
2. $_SERVER['QUERY_STRING']
$_SERVER['QUERY_STRING']提取?后的内容不会进行URL解码,而$_GET$_REQUEST解析url时会先进行URL解码,可以进行URL编码绕过

  1. $_REQUEST
    $_REQUEST在同时接收GET和POST参数时,POST优先级更高,优先接受post参数。

  2. basename函数

     basename() 函数返回路径中的文件名部分
     basename()不能识别ascii值为`47、128-255`的字符
    
     http://localhost/?file=%ffindex.php/%ff
     //index.php
     http://localhost/?file=%ffindex.php
     //index.php
     http://localhost/?file=index.php%ff
     //index.php
    
     <?php
     $path = "/testweb/home.php";
    
     //显示带有文件扩展名的文件名
     echo basename($path);
    
     //显示不带有文件扩展名的文件名
     echo basename($path,".php");
     ?> 
     home.php
     home
    

php进阶

PDO

PDO (PHP Data Objects) 是PHP的一种数据库扩展,PDO使用的是面向对象的编程风格,php5之后自带PDO,它有以下‘好处’:

  1. 支持不同数据库
  2. 预处理语使用PDO预处理语句来执行数据库操作可以避免SQL注入的风险
    PDO会对传入的每个参数进行转义和类型转换(如果传入的参数是字符串类型,但数据库需要的是整型,PDO会将字符串转换成整型,以确保数据类型的正确性)

    使用

    PDO 预处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    // 发送预处理指令
    $stmt=$pdo->prepare($pre_sql);//返回PDOStatement对象
    // 捆绑值
    $stmt->bindValue(':id',$id);

    //绑定变量
    $stmt->bindParam(':id',$id);//占位符的占位符(狗头)
    //执行
    $stmt->execute();
    //# 查结果
    //PDOStatement::fetch — 从结果集中获取下一行
    $data=$stmt->fetchall(PDO::FETCH_ASSOC);
    //PDO::FETCH_ASSOC:返回一个索引为结果集列名的数组
    //PDO::FETCH_BOTH(默认):返回一个索引为结果集列名和以0开始的列号的数组

    绕过

md5绕过

  1. md5的弱比较绕过
    0e开头且e后为数字可以被解析成科学计数法

    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
    这里附上常见的0E开头的MD5
    0e开头的md5和原值:
    QNKCDZO
    0e830400451993494058024219903391
    240610708
    0e462097431906509019562988736854
    s1091221200a
    0e940624217856561557816327384675
    s1836677006a
    0e481036490867661113260034900752
    s532378020a
    0e220463095855511507588041205815
    s1665632922a
    0e731198061491163073197128363787
    s1184209335a
    0e072485820392773389523109082030
    s1885207154a
    0e509367213418206700842008763514
    s155964671a
    0e342768416822451524974117254469
    s1502113478a
    0e861580163291561247404381396064
    s214587387a
    0e848240448830537924465865611904
    s878926199a
    0e545993274517709034328855841020
    0e215962017
    0e291242476940776845150308577824
  2. md5的强比较绕过
    强类型比较(===),判断内容的基础上,还会判断类型是否相同
    但md5不能加密数组,传入数组会报错,但会继续执行并且返回结果为null,所以md5加密后的结果是下面这样null===null,结果返回true

  3. MD5碰撞
    MD5碰撞也叫哈希碰撞,是指两个不同内容的输入,经过散列算法后,得到相同的输出,也就是两个不同的值的散列值相同

    1
    2
    3
    4
    5
    6
    7
    8
    md5
    a=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1U%5D%83%60%FB_%07%FE%A2&b=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%02%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1%D5%5D%83%60%FB_%07%FE%A2

    !!!! !注意写题时抓包修改,不要使用HackBar插件

    sha1
    a=%25PDF-1.3%0A%25%E2%E3%CF%D3%0A%0A%0A1%200%20obj%0A%3C%3C/Width%202%200%20R/Height%203%200%20R/Type%204%200%20R/Subtype%205%200%20R/Filter%206%200%20R/ColorSpace%207%200%20R/Length%208%200%20R/BitsPerComponent%208%3E%3E%0Astream%0A%FF%D8%FF%FE%00%24SHA-1%20is%20dead%21%21%21%21%21%85/%EC%09%239u%9C9%B1%A1%C6%3CL%97%E1%FF%FE%01%7FF%DC%93%A6%B6%7E%01%3B%02%9A%AA%1D%B2V%0BE%CAg%D6%88%C7%F8K%8CLy%1F%E0%2B%3D%F6%14%F8m%B1i%09%01%C5kE%C1S%0A%FE%DF%B7%608%E9rr/%E7%ADr%8F%0EI%04%E0F%C20W%0F%E9%D4%13%98%AB%E1.%F5%BC%94%2B%E35B%A4%80-%98%B5%D7%0F%2A3.%C3%7F%AC5%14%E7M%DC%0F%2C%C1%A8t%CD%0Cx0Z%21Vda0%97%89%60k%D0%BF%3F%98%CD%A8%04F%29%A1
    b=%25PDF-1.3%0A%25%E2%E3%CF%D3%0A%0A%0A1%200%20obj%0A%3C%3C/Width%202%200%20R/Height%203%200%20R/Type%204%200%20R/Subtype%205%200%20R/Filter%206%200%20R/ColorSpace%207%200%20R/Length%208%200%20R/BitsPerComponent%208%3E%3E%0Astream%0A%FF%D8%FF%FE%00%24SHA-1%20is%20dead%21%21%21%21%21%85/%EC%09%239u%9C9%B1%A1%C6%3CL%97%E1%FF%FE%01sF%DC%91f%B6%7E%11%8F%02%9A%B6%21%B2V%0F%F9%CAg%CC%A8%C7%F8%5B%A8Ly%03%0C%2B%3D%E2%18%F8m%B3%A9%09%01%D5%DFE%C1O%26%FE%DF%B3%DC8%E9j%C2/%E7%BDr%8F%0EE%BC%E0F%D2%3CW%0F%EB%14%13%98%BBU.%F5%A0%A8%2B%E31%FE%A4%807%B8%B5%D7%1F%0E3.%DF%93%AC5%00%EBM%DC%0D%EC%C1%A8dy%0Cx%2Cv%21V%60%DD0%97%91%D0k%D0%AF%3F%98%CD%A4%BCF%29%B1
  4. md5($pass,true)的绕过
    string 必需。要计算的字符串。
    raw 默认不写为FALSE:32位16进制的字符串,TRUE:16位原始二进制格式的字符串

    1
    2
    3
    4
    5
    6
    7
    8
    9
    content: ffifdyop
    hex: 276f722736c95d99e921722cf9ed621c
    raw: 'or'6\xc9]\x99\xe9!r,\xf9\xedb\x1c
    string: 'or'6]!r,b

    content: 129581926211651571912466741651878684928
    hex: 06da5430449f8f6f23dfc1276f722738
    raw: \x06\xdaT0D\x9f\x8fo#\xdf\xc1'or'8
    string: T0Do#'or'8

    在mysql里面,在用作布尔型判断时,以数字开头的字符串会被当做整型数,那么返回值也是true。

    字符串检测绕过

  5. strcmp() 函数比较两个字符串(适用5.3版本以前的php)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    $str1 = "aixuexiwang.com";
    $str2 = "aixuexiwang.com";
    $str3 = "aixuexiwang";
    echo strcmp($str1,$str2);  //输出结果为0,因为,两个字符串相等。
    echo strcmp($str1,$str3);  //输出结果为4,因为,参数1比参数2,多了4个字符。
    echo strcmp($str3,$str1);  //输出结果为-4,因为,参数1比参数2少了4个字符。

    $str4 = "abc";
    $str5 = "ABC";
    echo strcmp($str4,$str5);  //输出结果为1,因为,区分大小写。
    echo strcmp($str5,$str4);  //输出结果为-1,因为,区分大小写。
    echo strcasecmp($str4,$str5);  //输出结果为0,因为,不区分大小写,所以,相等。

    strcmp()在比较字符串和数组的时候直接返回0,可用于绕过。

ereg()与strpos()两个函数同样不能用数组作为参数,否则返回NULL
2. ereg()类似于preg_match但发现模式会返回true,否则返回false(PHP 5.3后已弃用)
eregi函数还可以使用%00来进行截断匹配
3. strpos() f函数查找字符串在另一字符串中第一次出现的位置(区分大小写),如果没有找到字符串则返回 FALSE(字符串位置从 0 开始,不是从 1 开始。)
相关函数:
strrpos() - 查找字符串在另一字符串中最后一次出现的位置(区分大小写)
stripos() - 查找字符串在另一字符串中第一次出现的位置(不区分大小写)
strripos() -查找字符串在另一字符串中最后一次出现的位置(不区分大小写)

  1. preg_match绕过
    preg_match(返回 pattern 的匹配结果,0 (不匹配)或 1false)
    preg_match_all(所有匹配 pattern 给定正则表达式的匹配结果并且将它们以 flag 指定顺序输出到$array数组中,返回值为完整匹配次数,输出的 $array 数组中,$array[0]保存完整模式的所有匹配, $array[1] 保存第一个子组的所有匹配)作用相当于python中的re.findall()

如果检查为if(preg_match(‘/…/‘, $json)),不管preg_match返回0还是false,if都不成立

  • 数组绕过
    preg_match只能处理字符串,当传入的subject是数组时会返回false

  • 在非多行模式(没有m修饰符)利用%0a绕过

非多行模式下,preg_match只能识别一行

1
2
3
4
5
6
7
8
9
10
if (preg_match('/^.*(flag).*/', $json)) {
echo 'Hacking attempt detected<br/><br/>';
}
$json="%0aflag" 只检测%0a

if (preg_match('/^.*([\x00-\x1F]+).*([\x00-\x1F]+).*$/', $json)) {
echo 'Hacking attempt detected<br/><br/>';
}
cmd=%0A{%0A"cmd":"/bin/cat /home/rceservice/flag"%0A} $对%0A不敏感,若%0A后没有'}',则为一行,有则为两行。当匹配两行时,/^...$/无法匹配成功

  • 利用PCRE回溯次数限制绕过(只能post)
    膜拜大佬https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html?page=2#reply-list
    DFA: 从起始状态开始,一个字符一个字符地读取输入串,并根据正则来一步步确定至下一个转移状态,直到匹配不上或走完整个输入
    由于NFA的执行过程存在回溯,所以其性能会劣于DFA,但它支持更多功能。大多数程序语言都使用了NFA作为正则引擎,其中也包括PHP使用的PCRE库。

PHP为了防止正则表达式的拒绝服务攻击(reDOS),给pcre设定了一个回溯次数上限pcre.backtrack_limit=1000000,超过10万次回溯就会返回false
一· 贪婪的.*
51bfc7bb-fd9a-402e-971a-a2247b226f3d.3adc35af4c1d
.*匹配剩余的所以字符,但正则后续要匹配,就会触发回溯

二. 懒惰的.+?

1
2
3
4
5
if(preg_match('/UNION.+?SELECT/is', $input)) {
die('SQL Injection');
}

//用union/*aa....*/selsect绕过
  1. str_replace函数绕过
  2. intval() 函数用于获取变量的整数值
    成功时返回 var 的 integer 值,失败时返回 0。 空的 array 返回 0,非空的 array 返回 1。

最大的值取决于操作系统。 32 位系统最大带符号的 integer 范围是 -2147483648 到 2147483647(2的31次方)。
举例:在这样的系统上, intval(‘1000000000000’) ,只取后31位会返回 2147483647。
64 位系统上,最大带符号的 integer 值是 9223372036854775807。

字符串有可能返回 0,虽然取决于字符串最左侧的字符。

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
echo intval(0x1A);                    // 0x表示16进制,转化为十进制为26
echo intval(0b100); // 0b表示2进制,转换为十进制为4
echo intval(12^10); // 按位异或(0b1100^0b1010) 0b0110 6
echo intval(12|10); // 按位或 (0b1100|0b1010) 1110 14
echo intval(12&10); // 按位和 (0b1100&0b1010) 1000 8
echo intval(~~100); // 100
echo intval(1<<2); // 左移乘二
echo intval(42000000); // 42000000
echo intval('42000000000000000000'); // 利用int的取值范围,5106511852580896768
echo intval(1e10); // 10000000000
echo intval('1e10'); // 1
echo intval("1000"); // 1000
echo intval('10ab'); // 10
echo intval(100*10); // 1000
echo intval('100*10'); // 100
echo intval(42); // 42
echo intval(4.2); // 4
echo intval('42'); // 42
echo intval('+42'); // 42
echo intval('-42'); // -42
echo intval(042); // 34
echo intval('042'); // 42
echo intval(42, 8); // 42
echo intval('42', 8); // 34
echo intval(array()); // 0
echo intval(array('foo', 'bar')); // 1

利用

查看信息

get_defined_vars() 函数返回作用域内所有已定义的变量、环境变量、服务器变量、用户定义变量列表。 
phpinfo() 输出关于php配置的信息
getenv() 获取一个环境变量的值
get_current_user() 获取当前php脚本所有者的名称
getlastmod() 获取页面最后修改的时间
ini_get() 获取一个配置选项的值
glob() 寻找与模式匹配的文件路径

为变量赋值

  1. file_get_contents()函数将整个文件读入一个字符串中,并返回这个值,我们也可以利用php://inputdata://text/plain,自定义参数值

  2. extract()函数:从数组中将变量导入当前符号表。
    定义:
    从数组中将变量导入到当前的符号表
    该函数使用数组键名作为变量名,使用数组键值作为变量值。针对数组中的每个元素,将在当前符号表中创建对应的一个变量
    语法:extract(array,extract_rules,prefix),array,必需,要使用的数组

    1
    2
    3
    4
    5
    6
    <?php
    $a="hello";
    $b= array('a' =>"world" ,"b"=>"gogogo");
    extract($b);
    echo $a; //world
    ?>

    如上所示,会存在一个覆盖漏洞。

SSRF的危险函数

file_get_contents()
fsockopen()
curl_exec()
fopen()
readfile()