反序列化
PHP 属性和权限
属性的权限,可以分为:
public
权限 外部可以通过箭头访问到private
权限 内部通过$this->username
访问到protected
权限 表示 自身及其子类 和父类 能够访问
抽象类
- 不能被
new
,也就是不能被直接实例化对象
- 不能被
接口
interface
- 为了实现多继承效果
implements
可以实现多个接口
方法的属性修饰符
public
private
protected
修饰:
- 静态属性
static
final
属性final
- 静态属性
序列化与反序列化
- 如果属性权限为
private
,那么序列化后,存储的属性名字为%00+类名+%00+属性名
- 如果属性权限为
protected
,那么序列化后,存储的属性名字为%00+*+%00+属性名
- 序列化是将一个对象变为一个可以传输的字符串
serialize(对象)
返回序列化后的字符串 - 反序列化就是将一个可以传输的字符串变为一个可以调用的对象
unserialize(反序列化后的字符串)
返回对象
反序列化示例:
1 |
|
- 反序列化时,PHP会做以下操作:
- 找到反序列化字符串规定的类名字
- 实例化这个类,但不会调用构造方法
- 有了实例化的类对象,对它的属性进行赋值
- 执行魔术方法
__wakeup()
和__unserialize()
- 返回构造好的对象
1 |
|
结果是:
1
2
3
4
5
6
7
8
9
10
11O:4:"User":3:{
s:4:"name";s:8:"John Doe";
s:7:"address";O:7:"Address":2:{
s:4:"city";s:8:"New York";
s:3:"zip";s:5:"10001";
}
s:12:"phoneNumbers";a:2:{
i:0;s:12:"123-456-7890";
i:1;s:12:"098-765-4321";
}
}解读序列化字符串:
识别外层对象:
O:4:"User":3:
O
表示对象(Object)。4
表示类名长度。"User"
是类名。3
表示对象的属性数量。
解析对象属性:
{s:4:"name";s:8:"John Doe";s:7:"address";O:7:"Address":2:{s:4:"city";s:8:"New York";s:3:"zip";s:5:"10001";}s:12:"phoneNumbers";a:2:{i:0;s:12:"123-456-7890";i:1;s:12:"098-765-4321";}}
- 解析第一个属性:
s:4:"name";s:8:"John Doe"
- 解析第二个属性:
s:7:"address";O:7:"Address":2:{s:4:"city";s:8:"New York";s:3:"zip";s:5:"10001";}
s:7:"address"
表示字符串属性名,长度为 7,内容是 “address”O:7:"Address":2:
表示一个嵌套的 Address 对象{s:4:"city";s:8:"New York";s:3:"zip";s:5:"10001";}
包含了 Address 对象的属性。s:4:"city";s:8:"New York";
表示 Address 对象的第一个属性。s:4:"city"
表示字符串属性名,长度为 4,内容是 “city”。s:8:"New York"
表示字符串属性值,长度为 8,内容是 “New York”。s:3:"zip";s:5:"10001";
表示 Address 对象的第二个属性。s:3:"zip"
表示字符串属性名,长度为 3,内容是 “zip”。s:5:"10001"
表示字符串属性值,长度为 5,内容是 “10001”。
$this
解释为当前对象里的:__construct
:当一个对象被创建时自动调用这个方法,可以用来初始化对象的属性。__destruct
:当 PHP 脚本执行结束前一秒当一个对象被销毁前自动调用这个方法,可以用来释放对象占用的资源。__call
:在对象中调用一个不存在的方法时自动调用这个方法,可以用来实现动态方法调用。__callStatic
:在静态上下文中调用一个不存在的方法时自动调用这个方法,可以用来实现动态静态方法调用。__get
:当读取(echo
)访问($myObject->age;
)一个不存在或不可访问的属性时,__get
方法会被自动调用。__set
:当设置(赋值)一个不存在或不可访问的属性时,__set
方法会被自动调用。__isset
:当使用isset()
或empty()
测试一个对象的属性是否存在时自动调用这个方法,可以用来实现属性的访问控制。__unset
:当使用unset()
删除一个对象的属性时自动调用这个方法,可以用来实现属性的访问控制。__toString
:当一个对象被当做字符串时(echo $myObject;
)(preg_match
时)自动调用这个方法,可以用来实现对象的字符串表示。__invoke
:当一个对象被作为函数调用$myObject('ChatGPT');
自动调用这个方法,可以用来实现对象的可调用性。__set_state
:当使用var_export()
导出一个对象时自动调用这个方法,可以用来实现对象的序列化和反序列化。__clone
:当一个对象被克隆时自动调用这个方法,可以用来实现对象的克隆。__debugInfo
:当使用var_dump()
或print_r()
输出一个对象时自动调用这个方法,可以用来控制对象的调试信息输出。__sleep
:在对象被序列化之前自动调用这个方法,可以用来控制哪些属性被序列化。__wakeup
:在对象被反序列化之后自动调用这个方法,可以用来重新初始化对象的属性。__unserialize()
是 PHP 7.4 中引入的一个魔术方法,将对象序列化为数组,同时在序列化时对敏感数据(如密码)进行加密处理。在PHP7.4.0开始,如果类中同时定义了__unserialize()
和__wakeup()
两个魔术方法。__serialize()
方法:反序列化时,从数组中恢复对象状态,并对敏感数据(如密码)进行解密处理。使用serialize()
将对象序列化为字符串时,__serialize()
方法被调用,并将属性打包为数组。
这里是被调用的例子
1 |
|
三种方法赋值
直接写只能写字符串:
private $username = 'xxxxxx';
外部写意图把类里的
$a
变量其他$b
,这样就写出了 pop,即:1
2
3$b = new SHOW;
$s = new CTF;
$s->a = $b;- 但是不能对私有属性进行赋值。
构造方法赋值:
- 以上缺点都没了:
1
2
3
4public function __construct()
{
$this->class = new backdoor();
}
- 以上缺点都没了:
构造 pop 链
- 重点找起始和 RCE 终点,期间的变量赋值为变量时要外部赋值,普通变量就直接赋值。
- 链子:终点开始编写链子,期间用各种魔法方法到起点(利用
new
关键词开始construct
,destruct
,wakeup
),再把起点对象序列化。 - 检查就是从后往前读了。
序列化绕过
- 绕过
\0
脚本或利用 PHP 7.1+ 的特性,直接用public
生成字符串但容错机制。 - 指针引用:
1
2
3
4
5$b->a1 =& $b->a2;
$a = 10;
$b = &$a; // $b 是 $a 的引用
$b = 20; // 改变 $b 也会改变 $a
echo $a; // 输出 20 - 畸形字符串
- 绕过
wakeup
。
- 绕过
- 利用将属性值变大。
使用 C 代替 O
适用版本:
5.3.0 - 5.3.29
5.4.0 - 5.4.45
5.5.0 - 5.5.38
5.6.0 - 5.6.40
7.0.0 - 7.0.33
7.1.0 - 7.1.33
7.2.0 - 7.2.34
7.3.0 - 7.3.28
7.4.0 - 7.4.16
8.0.0 - 8.0.3
- 只能执行
construct()
函数,无法添加任何内容,然后析构函数最后执行。
序列化机制构造一个对象,其中包含对象引用:
7.0.0 - 7.0.14
7.1.0
5.4.14 - 5.4.45
5.5.0 - 5.5.38
5.6.0 - 5.6.29
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<?php
//https://3v4l.org/iLSA7
//https://bugs.php.net/bug.php?id=73367
class obj {
var $ryat;
function __wakeup() {
$this->ryat = null;
throw new Exception("Not a serializable object");
}
function __destruct() {
if ($this->ryat == 1) {
var_dump('dtor!');
}
}
}
$poc = 'O:3:"obj":2:{s:4:"ryat";i:1;i:0;O:3:"obj":1:{s:4:"ryat";R:1;}}';//构造一个对象,其中 ryat 被设置为 1,然后让对象的另一个属性通过引用指向 ryat __wakeup() 修改了 ryat,但由于引用的存在,这个修改在某些地方不起作用,从而在 __destruct() 中成功触发了你原本不希望被触发的代码。
unserialize($poc);- 多写一个
i:0;O:3:"obj":1:{s:4:"ryat";R:1;}
再改对象数仿照以上。
利用 fastdestruct
机制让 destruct
跑到前面去:
- 一般删最后的
}
就行。
字符 O 绕过
条件:
<7.1.33
测试脚本:
- https://3v4l.org/YclXi
1
2
3
4
5
6
7<?php
//https://3v4l.org/YclXi
class D {
}
class C {
}
unserialize('O:+1:"C":0:{}');
字符 i、d 绕过
条件:
<8.0.3
(全版本)
测试脚本:
- https://3v4l.org/SJm2g
1
2
3
4
5
6
7
8
9
10
11<?php
//https://3v4l.org/SJm2g
// echo serialize(0);
echo unserialize('i:-1;');
echo "\n";
echo unserialize('i:+1;');
echo "\n";
echo unserialize('d:-1.1;');
echo "\n";
echo unserialize('d:+1.2;');
利用数组特性
数组特性:
1
2$arr = [new A, 'a的方法'];
$arr(); // 会直接调用 a 方法反序列化利用:
1
2
3
4
5
6
7
8
9public function _destruct(){
unserialize($this->key)(); //
在这里运用数组特性
$this->mod2 ="welcome".$this->modl;
}
$arr = [$gf, 'get flag'];
$f = new func;
$f->key = serialize($arr);
利用原生类
查看 flag 文件名:
1 |
|
读取文件:
1 |
|
GMP
Phar 八股文
- 这段代码展示了如何生成一个
.phar
文件,并将任意 PHP 代码作为 stub。当这个.phar
文件被解析或执行时,代码中的 stub 会被执行。 - 如果 WAF 过滤了
phar://
,则可以使用:compress.bzip2://phar://
compress.zlib://
1
2
3
4
5
6
7
8
9
10
11
12<?php
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //echo ($o) 变为八股文
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($o);
$phar->addFromString("test.txt","text");
$phar->stopBuffering(); //执行生成 phar.phar 文件
@system("gzip phar.phar"); // 将 Phar 文件压缩为 gzip 格式
echo urlencode(file_get_contents("phar.phar.gz")); // 输出压缩后的 Phar 文件内容并进行 URL 编码,根据情况删掉后两行
?>
伪造为 GIF 的 Phar
1 |
|
然后就是利用,可以利用的函数:
file_get_contents
fileatime
filectime
file_ctime
is_dir
is_file
is_executable
copy
unlink
stat
readfile
利用 file_get_contents
可以访问自己网站上的 Phar:
cd /var/www/html
sudo chown www-data:www-data /var/www/html -R
sudo nano writefile.php
或者得到的数据直接上传到任意路径中,再用 phar://xxxxx
传到 file_get_content
利用 data://
在 PHP 终端制造:
1 |
|
- 让
file_get_contents
包含{data://text/plain;base64(写入终端结果)}
到xxx.html
(后缀不重要),最后让file_get_contents
包含phar://xxx.html
。
总结一下利用 PHP 反序列化逃逸的步骤
- 确定利用目标
- 按照原程序正常序列化的步骤做一遍,看看正常序列化字符串的结构,基于此考虑攻击方式。注意,键值对设置的顺序会影响序列化结果,一定要按照程序内的方式设置值。
- 计算逃逸总共需要的字符,考虑需要构建多少被替换的字符。
- 插入 payload,结合本地运行结果查看 payload 是否成功
PHP 反序列化逃逸的标志就是,在序列化完成后对序列化结果的字符串做替换。只要程序这么写,绝对有问题。
CRLF 攻击
利用条件:
- 源码需要进行反序列化
- 源码调用一个方法,且该方法不存在。以此激活
__call()
利用范围(PHP 5, PHP 7):
1 |
|
Exploit
1 |
|