关于 CTF 中 php 考点与绕过那些事的总结

关于 CTF 中常见 php 绕过的总结可以参考我之前的博客:

CTF之PHP特性与绕过

PHP特性之CTF中常见的PHP绕过-CSDN博客

其中主要介绍了 md5()、sha1()、strcmp、switch、intval、$_SERVER 函数、三元运算符、strpos() 、数组、非法参数名传参等相关的绕过。

在此基础上我们进行一些其他补充

目录

[1、PHP 系统预定义变量(超全局变量)](#1、PHP 系统预定义变量(超全局变量))

[2、 PHP 错误控制运算符](#2、 PHP 错误控制运算符)

[3、PHP 变量默认值](#3、PHP 变量默认值)

[4、_GET 和 _POST](#4、_GET 和 _POST)

5、内置函数的松散性

[(1)php 弱类型](#(1)php 弱类型)

[(2)array_search 函数 与 in_array 函数](#(2)array_search 函数 与 in_array 函数)

[(3)switch 函数](#(3)switch 函数)

[(4)is_numeric 函数](#(4)is_numeric 函数)

[(5)ereg 函数](#(5)ereg 函数)

[(6)preg_match 函数](#(6)preg_match 函数)

[(7)extract 变量覆盖](#(7)extract 变量覆盖)

[(8)parse_str 函数变量覆盖缺陷](#(8)parse_str 函数变量覆盖缺陷)

[(9)$$ 变量覆盖](#(9)$$ 变量覆盖)

[(10)unset 函数](#(10)unset 函数)

[(11)mt_rand() 伪随机数](#(11)mt_rand() 伪随机数)

[(12)rand() 函数](#(12)rand() 函数)


1、PHP 系统预定义变量(超全局变量)

php 复制代码
$_POST //获取post数据,是一个字典

$_GET //获取get数据,是一个字典

$_COOKIE //获取cookie数据

$_SESSION //获取session数据

$_FILES //获取上传的文件

$_REQUEST //获取$_GET, $_POST, $_COOKIE中的数据

$_SERVER //用户和服务器的基本信息数据库

$_ENV //环境数据

$GLOBALS //所有全局变量

2、 PHP 错误控制运算符

PHP 支持一个错误控制运算符:@

当将其放置在一个 PHP 表达式之前,该表达式可能产生的任何错误信息都被忽略掉。

(阻止错误信息显示在用户的界面上,并继续执行代码)

测试代码:

由于文件不存在,file_get_contents() 函数将会产生一个警告

php 复制代码
<?php

// 启用错误报告
error_reporting(E_ALL);
ini_set('display_errors', 1);

$file = 'nonexistentfile.txt';

// 如果文件不存在,则会触发警告
$content = file_get_contents($file);

if ($content !== false) {
    echo "文件内容:$content";
} else {
    echo "无法读取文件或文件不存在!";
}

?>

运行结果:

下面我们使用 @ 来抑制错误信息的显示

@ 符号直接加在可能导致错误的函数或表达式前面

运行结果:

3、PHP 变量默认值

在PHP中,各种类型的变量在被定义但未被赋值时,它们的默认值如下:

整数 (integer):默认值为0。

浮点数 (float):默认值为0.0。

字符串 (string):默认值为空字符串 ''。

布尔值 (boolean):默认值为false。

数组 (array):默认值为一个空数组 array()。

对象 (object):默认值为一个空对象。

4、_GET 和 _POST

如果 GET 参数中设置 name[]=a,那么 _GET\['name'\] = \[a\],php 会把 \[\]=a 当成数组传入, _GET 会自动对参数调用 urldecode 函数进行 URL 解码。

_POST 同样存在这样的漏洞,提交的表单数据,user\[\]=admin,_POST['user'] 得到的是['admin'],是一个数组。

5、内置函数的松散性

关于 md5()、sha1()、strcmp、switch()、intval()、strpos() 等函数我们在前面的文章中已经进行过例题的演示和讲解,这里不再过多赘述,下面我们进行补充。

(1)php 弱类型

php 中有两种比较的符号: == 与 ===

=== 是严格相等运算符,在进行比较时,会先判断两种字符串的类型是否相等,再比较;

== 是相等运算符,在进行比较时,会先将字符串类型转化成相同,再比较,比如比较一个数字和字符串或者比较涉及到数字内容的字符串,字符串会被转换成数值并按照数值来进行比较

先来看一个测试:

php 复制代码
<?php

var_dump("myon" == 0);
var_dump("1myon" == 1);
var_dump("myon1" == 0);
var_dump("m1y2o3n4" == 0);
var_dump("0e12345" == "0e6789");

?>

运行结果:

从运行结果我们可以看到,所有比较均为 true

"myon" == 0 比较时,会将 myon 转化成数值,强制转化成数值,由于 myon 是字符串,转化的结果是 0 ,自然和 0 是相等的;

第二个 "1myon" == 1 可推断出比较时将 1myon 转化成数值,结果为 1;

但是第三个 "myon1" == 0 ,为什么比较时 myon1 会被转化成 0 呢?

查了 php 手册得知:

当一个字符串被当作一个数值来取值,其结果和类型如下:如果该字符串没有包含

'.','e','E'并且其数值值在整形的范围之内,该字符串被当作 int 来取值,其他所有情

况下都被作为 float 来取值,该字符串的开始部分决定了它的值,如果该字符串以合

法的数值开始,则使用该数值,否则其值为 0。

因此 "m1y2o3n4" == 0 中,m1y2o3n4 也被转化成了 0 ;

至于为什么 "0e12345" == "0e6789",在比较时,会将 0e 这类字符串识别为科学计数法的数字,0 的无论多少次方都是零,所以相等。

array_search() 函数用于在数组中搜索指定的值,如果找到则返回其键,否则返回 false。

语法:array_search(value,array,strict)

参数 描述
value 必需。规定在数组中搜索的键值。
array 必需。规定被搜索的数组。
strict 可选。如果该参数被设置为 TRUE,则函数在数组中搜索数据类型和值都一致的元素。可能的值: * true * false - 默认 如果设置为 true,则在数组中检查给定值的类型,比如数字 5 和字符串 5 是不同的。

这样一看 array_search 函数其实就类似于 == (在第三个参数为 false 的情况下)

测试代码:

php 复制代码
<?php

$a = array(0,1);
var_dump(array_search("myon",$a));
var_dump(array_search("1myon",$a));

?>

运行结果:

可以看到 myon 被转化成了 0 ;

1myon 则被转化成了 1 ;

与我们前面使用 == 测试的效果一样。

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

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

参数 描述
needle 必需。规定要在数组搜索的值。
haystack 必需。规定要搜索的数组。
strict 可选。如果该参数设置为 TRUE,则 in_array() 函数检查搜索的数据与数组的值的类型是否相同。

基本功能是一样的,因此绕过方法也是一样的,同上。

(3)switch 函数

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

测试代码:

php 复制代码
<?php

$content = "2myon";

switch ($content) {
    case 0:
    case 1:
    case 2:
        echo "content 小于3";
        break;
    case 3:
        echo "content 是3";
        break;
    default:
        echo "没有匹配";
}

?>

运行结果:

输出的是content 小于3,是由于 switch() 函数将 $content 进行了类型转换,转换结果为 2。

如果 $content = "m2yon" ,则转换结果为 0。

(4)is_numeric 函数

PHP 提供了 is_numeric 函数,用于检测变量是否为数字或数字字符串。但是函数的范围比较广 泛,不仅仅是十进制的数字,它还可以识别其他进制的数字字符串,以及科学计数法表示的数字等。

测试代码:

php 复制代码
<?php

var_dump(is_numeric(123));
var_dump(is_numeric('123'));
var_dump(is_numeric(0x7e));
var_dump(is_numeric('0x7e'));
var_dump(is_numeric(0e123));
var_dump(is_numeric('0e123'));

?>

运行结果:

原本我以为都会返回 true

但是十六进制的字符型返回了 false

而科学计数法 0e 开头的数字则是都返回的 true

对于数字和字母组合的字符串,无论是否以数字开头,都是返回 false

因此我们可以将 payload 转换为其他进制传入,或者传入 0e 开头的参数来实现绕过。

(5)ereg 函数

ereg() 函数用于进行正则表达式的匹配,不过它已经在PHP 5.3.0中被废弃。

关于它的两个特性:

ereg() 只能处理字符串,遇到数组会返回null,null !== false;

当 ereg() 读到 %00 的时候,就截止了。

(6)preg_match 函数

preg_match 函数用于执行一个正则表达式匹配。

用法:preg_match(pattern, input, matches, flags, offset)

参数 描述
pattern 必需。包含一个正则表达式,指示要搜索的内容
input 必需。将在其中执行搜索的字符串
matches 可选。此参数中使用的变量将填充一个包含所有找到的匹配项的数组
flags 可选。一组改变匹配数组结构的选项: * PREG_OFFSET_CAPTURE - 启用此选项后,每个匹配项将不是一个字符串,而是一个数组,其中第一个元素是包含匹配项的子字符串,而 第二个元素是输入中子字符串的第一个字符的位置。 * PREG_UNMATCHED_AS_NULL - 启用此选项后,不匹配的子模式将返回为 NULL 而不是空字符串。
offset 可选。默认为 0。指示开始搜索到字符串的深度。 preg_match() 函数不会找到出现在此参数中给定位置之前的匹配项

关于这个函数的一些特性与绕过:

preg_match 只能处理字符串,当传入数组时会返回 false;

测试代码:

我们匹配字符 world,原本是能匹配到的,但是由于我们传入的是数组,则会直接匹配失败。

php 复制代码
<?php

$pattern = '/world/';
$string = array('hello', 'world');
$result = preg_match($pattern, $string);
var_dump($result); 

运行结果:

回溯绕过:pcre.backtrack_limit 给 pcre 设定了一个回溯次数上限,默认为1000000,如果回溯次数超过这个数字,preg_match 就会返回 false,即匹配失败,从而绕过正则匹配。

%0a 换行绕过:.不会匹配换行符,并且在非多行模式下,当出现换行符 %0a 的时候,会被当做两行处理,而此时只可以匹配第一行,后面的行就会被忽略。(原因其实是正则书写不当)

(7)extract 变量覆盖

extract() 函数从数组中把变量导入到当前的符号表中。对于数组中的每个元素, 键名用于变量名,键值用于变量值。

例子:

php 复制代码
<?php

$flag='flag.php';
extract($_GET);  
if(isset($ceshi))
{
 $content=trim(file_get_contents($flag));
if($ceshi==$content)
{
    echo'flag{xxxxxxx}';
}
else
{
    echo'Oh.no';
}
}

?>

大致分析是要求我们GET传参进去值会经过extract()函数,下来会有两个if,第一个 if 判断 ceshi 这个变量是否存在,存在则继续执行 if 里面的,使用 file_get_contents() 读取 flag 变量里面的文件传递给 content 变量 之后,再进行判断传进来 ceshi 变量的值等不等于 $content,如果等于则打印出 flag 。

因为通过extract()函数我们传进的值会变成一个变量,例如我们GET传入 ceshi=1 ,则会存在$ceshi=1 ,所以我们构造 GET 传参 pyaload:

php 复制代码
$ceshi=&$flag=  

这样就会有两个为空的变量,而 flag=空 则覆盖了上面的 flag 中的值,进行判断都是空,所以为真,则可以得到 flag 。

(8)parse_str 函数变量覆盖缺陷

parse_str() 的作用是解析字符串,并注册成变量。在注册变量之前不会验证当前变量是否存在,所以会直接覆盖掉已有变量。与 parse_str() 类似的函数还有 mb_parse_str(),parse_str 会将字符串解析成多个变量,如果参数 str 是 URL 传递入的查询字符串(query string),则将它解析为变量并设置到当前作用域。

这里我们也来看一个例子:

php 复制代码
$a = "test123";
$b = $_GET['b'];
@parse_str($b);
if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO')) {
   echo $flag;

虽然 a 的值看似已经固定,但是我们可以利用变量覆盖对 a 进行重新赋值

构造 payload:

(这里还利用了 md5 的缺陷,它会把每个以0E开头的哈希值都解释为0

php 复制代码
?b=a[0]=s878926199a

(9)$$ 变量覆盖

如果把变量本身的 key 也当变量,也就是使用了 $$,就可能存在问题。

(10)unset 函数

unset(bar) 用来销毁指定的变量,如果变量 bar 包含在请求参数中,可能出 现销毁一些变量而实现程序逻辑绕过。

(11)mt_rand() 伪随机数

mt_rand() 函数是一个伪随机发生器,即如果知道随机数种子是可以预测的。

linux 64 位系统中,rand() 和 mt_rand() 产生的最大随机数都是 2147483647,正好是 2^31-1,也就是说随机播种的种子也是在这个范围中的, 0 -- 2147483647 的这个范围是可以爆破的。 但是用 php 爆破比较慢,有一个 C 的版本,可以根据随机数,爆破出种子 php_mt_seed。

在 php > 4.2.0 的版本中,不再需要用 srand() 或 mt_srand() 函数给随机数 发生器播种,现已由 PHP 自动完成。php 中产生一系列的随机数时,只进行了一 次播种,而不是每次调用 mt_rand() 都进行播种。

(12)rand() 函数

在使用 rand() 函数生成随机数时,如果没有先调用 srand() 函数来设置随机数种子,生成的随机数会遵循伪随机数生成算法,其生成的随机数序列就会有规律可循。

此外,关于 PHP 代码相关的漏洞还有文件包含、RCE、反序列化等,关于这些漏洞的利用方法与绕过技巧,可以参考我之前 web 专栏的博客。

创作不易,期待大家的关注与支持!

相关推荐
保持低旋律节奏14 分钟前
linux——进程状态
android·linux·php
计算机学姐1 小时前
基于php的摄影网站系统
开发语言·vue.js·后端·mysql·php·phpstorm
Neolnfra1 小时前
渗透测试标准化流程
开发语言·安全·web安全·http·网络安全·https·系统安全
计算机学姐1 小时前
基于php的旅游景点预约门票管理系统
开发语言·后端·mysql·php·phpstorm
Aerelin2 小时前
《静态分析:GUI程序的明码比较》
逆向·ctf
百***07452 小时前
GPT-5.2国内稳定接入实战指南:中转调用全链路方案(Python适配)
python·gpt·php
小阿宁的猫猫4 小时前
sqlmap的使用
sql·网络安全·php
roman_日积跬步-终至千里4 小时前
【Starrocks】StarRocks 排错:`Invalid method name: ‘heartbeat‘`(BE 心跳端口/协议错误)
服务器·网络·php
Maybe I Simple4 小时前
PHP常用方法封装
php
Aerelin4 小时前
Windows GUI 逆向分析题(CrackMe)
逆向·ctf