PHP
filter伪协议
![26c8067dca4fce61c798889c277dc031](C:\Users\1\Pictures\Saved Pictures\26c8067dca4fce61c798889c277dc031.png)
过滤器的read/write可以没有 [ php://filter/convert.base64-encode/resource=hello.php ]
include(‘php://filter/resource=hello.php’) 等同于 include(‘hello.php’)
文件路径应该是相对于当前工作目录或是一个绝对路径。
读取文件
//明文读取
index.php?file1=php://filter/resource=file.txt
//编码读取
index.php?file1=php://filter/read=convert.base64-encode/resource=file.txt
写入文件
//明文写入
index.php?file2=php://filter/resource=test.txt&txt=helloworld
//编码写入
index.php?file2=php://filter/write=convert.base64-encode/resource=test.txt&txt=helloworld
##ROT13(otate by 13 places)也叫回转13位,是一种替换式密码。
ROT13会把每一个字母替换成13位之后的字母,也就是把a换成n,b换成o,以此类推;如果超过了26个字母的范围,就会从开头重新开始。
弱比较
php中其中两种比较符号:
==:先将字符串类型转化成相同,再比较
===:先判断两种字符串的类型是否相等,再比较
字符串和数字比较使用==时,字符串会先转换为数字类型再比较
var_dump(‘a’ == 0);//true,此时a字符串类型转化成数字,因为a字符串开头中没有找到数字,所以转换为0
var_dump(‘123a’ == 123);//true,这里’123a’会被转换为123
var_dump(‘a123’ == 123);//false,因为php中有这样一个规定:字符串的开始部分决定了它的值,如果该字符串以合法的数字开始,则使用该数字至和它连续的最后一个数字结束,否则其比较时整体值为0。
举例:
var_dump(‘123a1’ == 123);//true
var_dump(‘1233a’ == 123);//false
<、>、<=、>=都存在和==相同的弱类型
其他伪协议
这个伪协议允许你访问 PHP 脚本的原始输入流。在处理 POST 请求时,你可以使用它来获取请求体中的数据。例如:
$post_data = file_get_contents(‘php://input’);
这在处理 RESTful API 请求时很有用,因为它允许你直接访问请求的原始 JSON 或其他格式的数据。
php://output 是一个用于输出的流,可以将数据发送到浏览器或者服务器的输出缓冲区。比如,你可以将内容输出到客户端:
$hello = “Hello, World!”;
file_put_contents(‘php://output’, $hello);
这对于需要直接输出内容而不需要保存到文件的情况非常有用。
php://stderr 允许你将错误消息发送到 Web 服务器的错误日志或者命令行的控制台。这在调试和记录错误时非常有用,例如:
file_put_contents(‘php://stderr’, ‘An error occurred!’);
4. ##### php://temp 和 php://memory
php://temp 和 php://memory 是两个用于创建临时数据流的伪协议。
php://temp 用于创建临时文件,适用于大型数据。
php://memory 则将数据保存在内存中,适用于小型数据。
这些伪协议在需要临时存储数据但不想在文件系统中留下痕迹时非常有用。
这两个伪协议允许你读写压缩过的数据流。比如:
$compressed_data = file_get_contents(‘compress.zlib://path/to/compressed/file.gz’);
这使得处理压缩文件变得更加方便。
expect:// 伪协议可以让你直接执行一个外部命令,并返回其输出。但是需要格外小心使用,因为它容易受到命令注入的攻击。
$output = file_get_contents(‘expect://ls -la’);
7. ##### glob://
glob:// 伪协议允许你像使用 glob() 函数一样列出匹配的文件。比如:
foreach (glob(‘glob://path/to/directory/*.txt’) as $file) {
echo “$file\n”;
}
这样可以方便地处理文件系统中的多个文件。
phar:// 伪协议允许你像处理压缩归档文件一样处理 PHAR (PHP Archive) 文件。比如:
$phar = new Phar(‘phar://path/to/archive.phar’);
这在处理自包含的 PHP 应用程序或者库时非常有用。
data伪协议
array_search函数(遍历数组找对应值)
采用的是若比较,即==,在php中’字符串’==0是成立的
例.array_search(“DGGJ”,$c[“n”])如果n=0或数组n中有元素等于0,返回值即为1
绕过
空格=${IFS}
/=$printf(${IFS}”\57”)
字符串中间加“”没影响(cat=ca””t)
魔术方法
魔术方法 | 调用时机 |
---|---|
__construct () | 实例化时调用 |
__destrct() | 销毁时调用 |
__call() | 在对象中调用一个不可访问方法时 |
__callStatic() | 在静态上下文中调用一个不可访问方法时 |
__get() | 读取不可访问(protected 或 private)或不存在的属性的值时 |
__set() | 给不可访问(protected 或 private)或不存在的属性赋值时 |
__isset() | 对不可访问(protected 或 private)或不存在的属性调用 isset() 或 empty() 时 |
__unset() | 对不可访问(protected 或 private)或不存在的属性调用 unset() 时 |
__sleep() | 执行 serialize() 时 |
__wakeup() | 执行 unserialize() 时 |
__toString() | 把类当成字符串时 |
__invoke() | 把对象当成函数调用时 |
__debugInfo() | 使用 var_dump, print_r 时 |
__set_state() | 调用var_export()导出类时 |
__clone() | 当对象复制完成时 |
__autoload() | 尝试加载未定义的类 |
下面还有两个特殊的魔术方法__serialize() ``__unserialize()
serialize()
函数会检查类中是否存在一个魔术方法 __serialize()
。如果存在,该方法将在任何序列化之前优先执行。它必须以一个代表对象序列化形式的 键/值 成对的关联数组形式来返回,如果没有返回数组,将会抛出一个 TypeError
错误。
注意:
如果类中同时定义了
__serialize()
和__sleep()
两个魔术方法,则只有__serialize()
方法会被调用。__sleep()
方法会被忽略掉。如果对象实现了Serializable
接口,接口的serialize()
方法会被忽略,做为代替类中的__serialize()
方法会被调用
__serialize()
的预期用途是定义对象序列化友好的任意表示。 数组的元素可能对应对象的属性,但是这并不是必须的。
相反,unserialize()
检查是否存在具有名为 __unserialize()
的魔术方法。此函数将会传递从 __serialize()
返回的恢复数组。然后它可以根据需要从该数组中恢复对象的属性
注意:
如果类中同时定义了
__unserialize()
和__wakeup()
两个魔术方法,则只有__unserialize()
方法会生效,__wakeup()
方法会被忽略
注意:
此特性自 PHP 7.4.0 起可用
# 反序列化绕过
# php7.1 + 反序列化对类属性不敏感
private
使用:私有的类的名称 (考虑到继承的情况) 和字段名组合\x00类名称\x00字段名
protected
使用:*
和字段名组合\x00*\x00字段名
我们前面说了如果变量前是 protected,序列化结果会在变量名前加上 \x00*\x00
但在特定版本 7.1 以上则对于类属性不敏感,比如下面的例子即使没有 \x00*\x00
也依然会输出 abc
1 | <?php |
# 绕过__wakeup (CVE-2016-7124)
版本:
PHP5 < 5.6.25
PHP7 < 7.0.10
利用方式:序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup 的执行
1 | <?php |
如果执行 unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');
输出结果为 666
而把对象属性个数的值增大执行 unserialize('O:4:"test":2{s:1:"a";s:3:"abc";}');
输出结果为 abc
# 绕过部分正则
preg_match('/^O:\d+/')
匹配序列化字符串是否是对象字符串开头,这在曾经的 CTF 中也出过类似的考点
・利用加号绕过(注意在 url 里传参时 + 要编码为 %2B)
・serialize (array (a) ) ; // a));//a));//a 为要反序列化的对象 (序列化结果开 头是 a,不影响作为数组元素的 $a 的析构)
1 | <?php |
# 十六进制绕过字符匹配
可以使用十六进制搭配上已转义字符串来绕过对某些字符的检测
1 | <?php |
这里检测了是否包含 flag
字符,我们可以尝试使用 flag
的十六进制 \66\6c\61\67
来绕过,构造以下:
1 | 'O:4:"Read":1:{s:4:"name";S:4:"\66\6c\61\67";}' |
可以用下面 python 脚本将字符串转化为 Hex
1 | str = input('Enter a string: ') |
# 利用‘引用’
对于需要判断两个变量是否相等时,我们可以考虑使用引用来让两个变量始终相等.
1 | <?php |
上面这个例子将 $b
设置为 $a
的引用,可以使 $a
永远与 $b
相等
# php 反序列化字符逃逸
# 情况一:过滤后字符过多
例如以下情形:
1 | <?php |
正常情况,传入 name=mao
如果此时多传入一个 x 的话会怎样,毫无疑问反序列化失败,由于溢出 (s 本来是 4 结果多了一个字符出来),我们可以利用这一点实现字符串逃逸
那我们传入 name=maoxxxxxxxxxxxxxxxxxxxx";i:1;s:6:"woaini";}
传入 name=maoxxxxxxxxxxxxxxxxxxxx";i:1;s:6:"woaini";}
";i:1;s:6:"woaini";}
这一部分一共二十个字符
由于一个 x 会被替换为两个,我们输入了一共 20 个 x,现在是 40 个,多出来的 20 个 x 其实取代了我们的这二十个字符 ";i:1;s:6:"woaini";}
,从而造成 ";i:1;s:6:"woaini";}
的溢出,而 “ 闭合了前串,使得我们的字符串成功逃逸,可以被反序列化,输出 woaini
最后的;} 闭合反序列化全过程导致原来的 ";i:1;s:7:"I am 11";}"
被舍弃,不影响反序列化过程
# 情况二:过滤后字符变少
例如:
1 | <?php |
正常情况传入 name=mao&age=11
的结果
构造一下
简单来说,就是前面少了一半,导致后面的字符被吃掉,从而执行了我们后面的代码;
我们来看,这部分是 age 序列化后的结果
s:3:“age”;s:28:“11”;s:3:“age”;s:6:“woaini”;}”
由于前面是 40 个 x 所以导致少了 20 个字符,所以需要后面来补上, ";s:3:"age";s:28:"
11 这一部分刚好 20 个,后面由于有” 闭合了前面因此后面的参数就可以由我们自定义执行了
# 利用不完整类绕过序列化回旋镖
当存在 serialize(unserialize($x)) != $x
这种很神奇的东西时,我们可以利用不完整类 __PHP_Incomplete_Class
来进行处理
当我们尝试反序列化到一个不存在的类是,PHP 会使用 __PHP_Incomplete_Class_Name
这个追加的字段来进行存储
我们于是可以尝试自己构造一个不完整类
1 | <?php |
这样就可以绕过了
更近一步,我们可以通过这个让一个对象被调用后凭空消失,只需要手动构造无 __PHP_Incomplete_Class_Name
的不完整对象
# serialize () 函数在处理 __PHP_Incomplete_Class
对象时所进行的特殊操作
unserialize () 在发现当前 PHP 上下文中没有包含相关类的类定义时将创建一个 __PHP_Incomplete_Class
对象。而 serialize () 在发现需要进行序列化的对象是 __PHP_Incomplete_Class
后,将对其进行 特殊处理 以得到描述实际对象而非 __PHP_Incomplete_Class
对象的序列化文本,而这里就包含了 将属性的描述值减一 这一步。
那么对象所属类的名称是否会发生替换,序列化文本中的 __PHP_Incomplete_Class_Name
是否会被自动删除以使得序列化文本中的属性个数描述值与实际相符呢?对此,请参考如下示例:
1 | <?php |
执行结果
1 | string(69) "O:7:"MyClass":2:{s:4:"name";s:8:"RedHeart";s:6:"nation";s:5:"China";}" |
结合前面观察到的种种现象,我们可以总结出 serialize () 函数对 __PHP_Incomplete_Class 对象执行了如下 特殊操作(操作描述顺序并非 serialize 函数的实际操作顺序):
将 __PHP_Incomplete_Class
对象中的 属性个数减一 并将其作为序列化文本中 对实际对象属性个数的描述值。
将 __PHP_Incomplete_Class
对象的 __PHP_Incomplete_Class_Name
作为序列化文本中 对象所属类的描述值。若未从 __PHP_Incomplete_Class
对象 中检查到 __PHP_Incomplete_Class_Name
属性,则跳过此步。
将 __PHP_Incomplete_Class
对象的序列化文本中对 __PHP_Incomplete_Class_Name
属性的描述删去。若没有发现相关描述,则跳过此步。
关于 __PHP_Incomplete_Class
更详细的介绍 <PHP 反序列化漏洞:__PHP_Incomplete_Class 与 serialize (unserialize ($x)) !== $x >
# 对象注入
当用户的请求在传给反序列化函数 unserialize()
之前没有被正确的过滤时就会产生漏洞。因为 PHP 允许对象序列化,攻击者就可以提交特定的序列化的字符串给一个具有该漏洞的 unserialize
函数,最终导致一个在该应用范围内的任意 PHP 对象注入。
对象漏洞出现得满足两个前提
1、
unserialize
的参数可控。
2、 代码里有定义一个含有魔术方法的类,并且该方法里出现一些使用类成员变量作为参数的存在安全问题的函数。
比如:
1 | <?php |
在脚本运行结束后便会调用 _destruct
函数,同时会覆盖 test 变量输出 maomi
# POP
# ———— 魔法函数 ———
我需要再次提出魔法函数并且需要细致的解释供我更加深刻的理解
1 | __wakeup() //执行unserialize()时,先会调用这个函数 |
会发现,我在很多魔法函数的触发方式的解释中对象后面都加了(属性),这与 php 官方手册和其他博客文章的解释有些许不同,在查找资料时产生了很多疑惑,比如我翻阅的其中一篇博客:
原文说此处触发了 __toString
函数,可明明只是将属性当作字符串,
再比如同一篇文章的另一处:
触发了 __invoke
函数
在与小伙伴讨论之后,认为可以将属性看作对象
在弄清楚各种魔法函数触发条件之后就要开始构建 pop 链了
# POP 链
POP 链构造首先就是要找到头和尾,也就是用户能传入参数的地方(头)和最终要执行函数方法的地方(尾)。找到头尾之后进行反推过程,从尾部开始一步步找到能触发上一步的地方,直到找到传参处,此时完整的 POP 链就显而易见了。CTF 赛中一般尾部就是 get flag 的方法,头部则是 GET/POST 传参
举个例子:
1 | <?php |
先找链子的头和尾,头部明显是 GET 传参,尾部是 Uwant
类中的 getshell
,然后往上倒推, Uwant
类中的 __get()
中调用了 getshell
, Show
类中的 toString
调用了 __get()
,然后 Hello
类中的 __destruct()
,而我们 GET 传参之后会先进入 __destruct()
,这样子头和尾就连上了,所以说完整的链子就是:
1 | 头 -> Hello::__destruct() -> Show::__toString() -> Uwant::__get() -> Uwant::getshell -> 尾 |
# 例题
# newstarctf 2023 week3 | POP Gadget
1 | <?php |
由此可以构造出 POP 链子
1 | Begin::__destruct -> Then::toString -> Super::__invoke -> Handle::__call -> CTF::end -> WhiteGod::__unset |
由于链子调用中成员属性有 private 和 protected,用 construct 方法去调用链子,最后再使用 url 编码绕过
exp
1 | <?php |
稍微总结一下,POP 链的头一般是 GET/POST 传参引发 __wakeup
, __construct
, __destruct
结尾一般是输出敏感信息或者执行系统命令所在函数,即 GetFlag 的点
# [MRCTF2020]Ezpop1
1 | Welcome to index.php |
先找出可以 getflag 的点,在 Modifier
类中 append
函数有 include
函数可以文件包含,可以利用, __invoke
函数直接调用了 append
,在 Testlei 中 __get
将 p 作为函数调用,会触发 __invoke
, 在 __totring
方法中 $this->str
赋予 test
类,在 test
类读取 source
变量,(因为 test
类中没有 source
属性,则是访问了不可访问的属性)则会自动调用 __get
魔术方法, __wakeup
函数将对象进行正则匹配,会触发 __toString
,而 __wakeup
在反序列化时会调用,可以当作 pop 链头,而尾时 include
函数,可以利用 var
构造 php 为协议获取 flag
pop 链:
1 | Show:__wakeup -> Show:__toString -> Test:__get -> Modifier:__invoke ->Modifier: append |
exp:
1 | <?php |
# 2021 强网杯 赌徒
1 | <meta charset="utf-8"> |
尾部可以看到 Room
类中有个 Get_hint()
方法,里面有一个 file_get_contents
,可以实现任意文件读取,我们就可以利用这个读取 flag 文件了,然后就是往前倒推, Room
类中 __invoke()
方法调用了 Get_hint()
,然后 Room
类的 __get()
里面有个 return $function()
可以调用 __invoke()
,再往前看, Info
类中的 __toString()
中有 Room
类中不存在的属性,所以可以调用 __get()
,然后 Start
类中有个 _sayhello()
可以调用 __toString()
,然后在 Start
类中 __wakeup()
方法中直接调用了 _sayhello()
,而我们知道的是,输入字符串之后就会先进入 __wakeup()
1 | 头 -> Start::__wakeup() -> Start::_sayhello() -> Info::__toString() -> Room::__get() -> Room::invoke() -> Room::Get_hint() |
在构建 pop 链时,除 __construct
函数一般不需要写出, 变量的权限与源码保持一致,在串联对象时,需要与源码的对应关系保持一致,比如: $b -> file['filename'] = $c;
- Title:
- Author: mapl3miss
- Created at : 2024-10-20 19:24:23
- Updated at : 2024-09-28 17:59:28
- Link: https://redefine.ohevan.com/2024/10/20/PHP/
- License: This work is licensed under CC BY-NC-SA 4.0.