2025 Win 反序列化
一、讲义内作业
- PCRE回溯次数限制:

图中第5步有红颜色,表示匹配不成功。此时b{1,3}已经匹配到了2个字符“b”,准备尝试第三个时,结果发现接下来的字符是“c”。那么就认为b{1,3}就已经匹配完毕。然后状态又回到之前的状态(即第6步,与第4步一样),最后再用子表达式c,去匹配字符“c”。当然,此时整个表达式匹配成功了preg_match的匹配存在回溯,回溯次数上限是1000000次,超过上限后函数直接返回false。
- fastcoll 工具
内容不同但是md5值是相同的,伟大!无需多言!

其中被解析的字符第三行开头就有不一样的
yle=”color:#000000;”>is_numeric()函数—获取变检测变量是否为数字或数字字符串**
- 如果var是数字或数字字符串则返回 TRUE,否则返回 FALSE
- strcmp()函数—比较字符串大小函数
- strcmp只会处理字符串,如果给个数组的话呢,就会返回NULL
- sha1()函数—sha1 加密函数
- 以下值在sha1 加密后以0E开头:
1 | aaroZmOk |
- extract()函数
- 从数组中将变量到导入到当前符号表,该函数使用数组键名作为变量名,使用数组键值作为变量值。针对数组中的每个元素,将在当前符号表创建对应的一个变量
- in_array()函数
- 用途:用来判断一个值是否在某一个数组列表里面
- 缺陷:当第三个参数不设置为true时(即严格模式),存在自动类型转换(弱比较) ,当输入数字1后再紧跟其他字符串能够Bypass检测数组的功能
- parse_str()变量覆盖
- parse_str — 将字符串解析成多个变量:void parse_str ( string $encoded_string [, array &$result ] )
- 如果设置了第二个变量 result,变量将会以数组元素的形式存入到这个数组,作为替代
- 解析字符串并注册成变量,在注册变量之前不会验证当前变量是否存在,所以直接覆盖掉已有变量,当parse_str()函数的参数值可以被用户控制时,则存在变量覆盖漏洞。
- 字符串逃逸
- 字符串逃逸:在反序列化范围之外的字符(如花括号外的字符)都会被忽略,不影响反序列化的正常进行,一般分两种:字符数增多和字符数减少
- 反序列化之所以存在字符串逃逸,最主要的原因是代码中存在针对序列化(serialize())后的字符串进行了过滤操作(变多或者变少)
- 当字符增多:在输入的时候再加上精心构造的字符。经过过滤函数,字符变多之后,就把我们构造的给挤出来。从而实现字符逃逸
- 当字符减少:在输入的时候再加上精心构造的字符。经过过滤函数,字符减少后,会把原有的吞掉,使构造的字符实现代替
- phar反序列化
- phar反序列化phar文件本质上是一种压缩文件,会以序列化的形式存储用户自定义的meta-data。当受影响的文件操作函数调用phar文件时,会自动反序列化meta-data内的内容
- phar文件:
- 在软件中,PHAR(PHP归档)文件是一种打包格式,通过将许多PHP代码文件和其他资源(例如图像,样式表等)捆绑到一个归档文件中来实现应用程序和库的分发。php通过用户定义和内置的“流包装器”实现复杂的文件处理功能。内置包装器可用于文件系统函数,如(fopen(),copy(),file_exists()和filesize()。 phar://就是一种内置的流包装器
- 常见的流包装器:
1 | file:// — 访问本地文件系统,在用文件系统函数时默认就使用该包装器 |
漏洞利用条件:phar可以上传到服务器端(存在文件上传)
要有可用的魔术方法作为“跳板”。
文件操作函数的参数可控,且 : 、 / 、phar 等特殊字符没有被过滤
phar生成
1 |
|
绕过方法:当环境限制了phar不能出现在前面的字符里。可以使用compress.bzip2://和compress.zlib://等绕过
1 | compress.bzip://phar:///test.phar/test.txt |
也可以利用其它协议:
1 | php://filter/read=convert.base64-encode/resource=phar://phar.phar |
二、刷题
(一)[HCTF 2018]Warmup
还是这道题,之前写过了,这次复习一下
进去还是滑稽图

看源代码

找 source.php 文件,打开后又有一串 PHP 代码

先看一下这个 emmm 类的代码
1 | class emmm |
- 公有静态函数 checkFile
- 变量 whitelist 包含 source.php 和 hint.php 文件
- 第一个 if
- 条件: page 里为随机值或者 page 里的值的长度为 0
- 输出 you can’t see it
- 返回值为 false
- 第二个 if
- 条件:page 是否在whitelist 内
- 返回值为真
- 对 page 用mb_substr 函数
- mb_substr 函数的参数里有一个mb_strpos 函数
- mb_strpos 函数中先是把?连接在 page 变量的后面之后返回?的位置,即 page 的字符串长度加 1
- 之后执行mb_substr 函数,从 0 开始,即返回 page 里的字符串
- 该代码的作用是从 page 中提取? 前的部分内容。如果 page 中没有 ?,它会返回整个字符串。 通过这种方式,可以有效移除 page 中的查询参数部分
- 第三个 if
- 条件:page 是否在whitelist 内
- 返回值为真
- 对 page 进行 url 加密
- 对 page 用mb_substr 函数(含义同上)
- 第四个 if
- 条件:page 是否在whitelist 内
- 返回值为真
- 输出you can’t see it
- 返回值为 false
之后看一下外面的 if 代码
1 | if (! empty($_REQUEST['file']) |
- 条件:file 的 REQUEST 传参不为空 且 file 字符串长度不为 0 且 checkFile 函数返回值为 true
- 如果条件为真则把 file 请求的文件包含进来
- 如果条件为假则输出滑稽图
所以先让 file 的值为 hint.php 把该文件包含进来,不包含 source.php 文件的原因为该文件就是 source.php

所以之后可以用用 ? 跳过文件检测(该函数只截取到?之前的内容)
一层一层在文件找ffffllllaaaagggg 就行
构造 playload:
1 | ?file=hint.php?/../../../../ffffllllaaaagggg |
拿到 flag

(二)[FSCTF 2023]ez_php2
打开靶场发现以下 PHP 代码:

先每个类进行分析
Class Rd:
1 | Class Rd{ |
- 先定义了三个变量:ending、c1、poc,且它们都是公有的
- 之后使用__destruct()魔术方法:当对象被销毁时会触发这个方法
- 当对象被销毁后输出:All matters have concluded
- 结束代码
- 使用 _call()魔术方法:当调用一个不可访问的对象时会触发这个方法
- 这里它遍历参数,如果有某个键为 POC 且值为”1111“,则把 c1 的 var1 属性设置为”system“
Class Poc
1 | class Poc{ |
- 先定义了两个变量:playload、fun,且它们都是公有的
- 之后使用 set 魔术方法(当给一个不可访问的属性赋值时会触发这个方法),把输入的属性值分别赋值给 playload 和 fun
- 定义 getflag 函数,形参为 playload 的值
- 输出:Have you genuinely accomplished what you set out to do?
- 用 file_get_contents($paylaod)尝试获取$playload 里的文件
Class Er
1 | class Er{ |
- 先定义了两个变量:symbol、Flag,且它们都是公有的
- 使用构造函数,把 symbol 属性变为 True
- 使用 set 魔术方法它会将传入的值作为函数调用,并将Flag属性作为参数传入这个函数
Class Ha
1 | class Ha{ |
- 先定义了三个变量:start、start1、start2,且它们都是公有的
- 使用构造函数
- 输出 start1 属性的值并换行
- 使用__destruct()魔术方法(当调用对象被销毁时触发这个魔术方法)
- 如果 start2 属性的值为 11111,则调用 start1 的 Love 方法
- 并传入 start 属性作为参数
- 然后输出You are Good!
1 | if(isset($_GET['Ha_rde_r']) |
- 检查Ha_rde_r是否为随机值,
- 如果为真则对Ha_rde_r 进行反序列化操作
- 否则输出You are Silly goose!
分析代码后发现这道题入口是 Ha 类里面(毕竟 start 都写在里面了)
之后触发__destruct()魔术方法进入 Love 语句。
使得其触发__call 魔术方法,然后给start赋值( [‘POC’=>‘1111’] )
之后 c1 中的 var1触发__set 魔术方法,之后它的 value就成为了system
修改$Flag就可以修改执行的命令了
构造以下脚本:
1 |
|
先按照题目的内容写几个类和对象(其中 Flag 要赋值为 ‘cat /flag’,之后要用到 system 函数输出)。从 Ha 入手,由于从 c1 在 Rd 内,所以先给 a 中 start1 用类 Rd 赋值,并把其中键 POC 对应的值赋值为 “1111“,之后用 Er 类给 c1 赋值,之后输出 a。这样就满足条件了。
输出结果为
1 | O:2:"Ha":3:{s:5:"start";a:1:{s:3:"POC";s:4:"1111";}s:6:"start1";O:2:"Rd":3:{s:6:"ending";N;s:2:"cl";O:2:"Er":2:{s:6:"symbol";N;s:4:"Flag";s:9:"cat /flag";}s:3:"poc";N;}s:6:"start2";s:5:"11111";} |
传入参数
1 | ?Ha_rde_r=O:2:"Ha":3:{s:5:"start";a:1:{s:3:"POC";s:4:"1111";}s:6:"start1";O:2:"Rd":3:{s:6:"ending";N;s:2:"cl";O:2:"Er":2:{s:6:"symbol";N;s:4:"Flag";s:9:"cat /flag";}s:3:"poc";N;}s:6:"start2";s:5:"11111";} |
拿到 flag

(三)[安洵杯 2019]easy_serialize_php
进入靶场发现这个连接

点进入发现是一串 PHP 代码

注意到:

进去之后注意到可疑文件
下面还有这一段代码,存在反序列化,应该就是入口了

先尝试把 f 赋值为 show_image,之后出现以下界面

没有头绪,先分析代码吧
1 | $function = @$_GET['f']; |
GET 传参
1 | function filter($img){ |
把php、flag、php5、php4和flig 替换为为空字符串
1 | if($_SESSION){ |
对$_SESSION 重新赋值,user 对应 guest,function 对应$function
1 | if(!$function){ |
如果 function 为 0,则输出该页面
1 | if(!$_GET['img_path']){ |
GET 传参 img_path,如果 img_path 为 0,则对guest_img.png 进行 base64 加密,否则就对 img_path 先进行 base64 加密,再进行 sha1 加密
1 | $serialize_info = filter(serialize($_SESSION); |
对$_SESSION 序列化的值进行过滤并赋值给serialize_info
1 | if($function == 'highlight_file'){ |
最后就是对 function 的判断
- 如果 function 的值为highlight_file,则输出该页面
- 如果 function 的值为 phpinfo,则输出该 PHP 代码的信息
- 如果 function的值为 show_image 则把serialize_info 进行反序列化并赋值给 userinfo,并输出 base64 解密后的 userinfo 的 img。
所以本体的思路为:
该 PHP 代码会将userinfo中的[‘img’]值做base64解码,然后吧提到的整个文件读入一个字符串中,所以我们就要构造img的值,让代码读出内容,经过上文的分析,想要修改img的值可以通过设置img_path,或者修改userinfo[‘img’]的内容
首先看修改img_path
这个方法有一个小问题,修改img_path,传入的内容会被base64和sha1加密,然而,高亮文件内容时只做了base64的解密,没有 sha1 解密,所以修改img_path是无法读到文件的,所以只能修改userinfo[‘img’] 的内容
userinfo[‘img’]的内容由_SESSION组成,所以我们可以修改user和function,考虑到有过滤,可以采用反序列化字符逃逸来构造SESSION[‘img’]的值,其中ZDBnM19mMWFnLnBocA==是 d0g3_f1ag.php 的 base64 编码后的结果
关于本题的键值逃逸
- 因为序列化的字符串是严格的,对应的格式不能错,比如s:4:“name”,那s:4就必须有一个字符串长度是4的否则就往后要。
- 并且反序列化会把多余的字符串当垃圾处理,在花括号内的就是正确的,花括号外的就都被扔掉。
先构造img属性

即
1 | s:3:"img";s:20:"ZDBnM19mMWFnLnBocA=="; |
之后我们要确定_SESSION[ ] 里面的东西:
原字符串:
1 | a:2:{s:7:"phpflag";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";} |
经过filter过滤后phpflag就会被替换成空字符串
所以 s:7:“phpflag”就变成了 s:7:””
- 但是这里会出现问题,因为这里要求的字符串的长度为7,但是这里却是空字符串。所以它会向后索取字符串。直到长度正好为7。细心的话,可以看到 “;s:48: 这个字符串的长度正好为7
当phpflag被替换成空字符串时,原本的键值对就变成:
- 第一个变量的名: s:7:””;s:48:”;
- 第一个变量的值: s:1:“1”;
- 第二个变量的名: s:3:“img”;
- 第二个变量的值: s:20:“ZDBnM19mMWFnLnBocA==”;
再加上PHP序列化的严格规定,会把后面多余的字符串丢弃。就变成了:
1 | a:1:{s:7:"";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";} |
所以构造 playload:
1 | _SESSION[phpflag]=;s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";} |

但是页面源代码出现了这个,所以在进行对/d0g3_fllllllag base64 加密

所以最终 playload 为
1 | http://node7.anna.nssctf.cn:21451/index.php?f=show_image |

(四)[第五空间 2021]pklovecloud

分析代码
1 | include 'flag.php'; |
- 把 flag.php 文件包含进来
- 定义了一个类 pkshow
- 在类中定义了一个函数 echo_name()
- 返回值为 Pk very safe^.^
1 | class acp |
- 定义了一个类 acp
- 类中有 public 成员 neutron 和 nova 以及 protected 成员 cinder
- 使用魔术方法__construct() ,在对象被创建时调用
- 新建立一个对象并赋值给 cinder
- 使用魔术方法__toString(), 类被当成字符串时的回应方法
- 先判断 cinder 是否为随机值,如果是则返回 echo_name 函数的值
1 | class ace |
- 定义了一个类 ace
- 公有成员为filename、openstack、docker
- 定义函数 echo_name()
- 对docker 进行反序列化并把值赋值给openstack
- 把 heat 赋值给 openstack 中的 neutron
- 判断 openstack 中的 neutron 的值是否与 openstack 中 nova 的值相等
- 如果相等则把 filename 的值赋值给 file
- 判断 file 里是否有内容
- 如果有则返回 file 文件的内容
- 否则返回keystone lost~
1 | if (isset($_GET['pks']) |
- 这判断 pks 是否是随机值
- 如果是随机值则把 pks 反序列化的值赋值给 logData 并输出其值
- 否则显示代码
pks 要是我们序列化的代码,
所以构造序列化代码:
1 |
|
其中要把 flag.php 的内容赋值给 filename 方便以后打开文件,并且 docker 要为空从而使得neutron 与 nova 相等。
输出结果:

即
1 | O%3A3%3A%22acp%22%3A3%3A%7Bs%3A9%3A%22%00%2A%00cinder%22%3BO%3A3%3A%22ace%22%3A3%3A%7Bs%3A8%3A%22filename%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A9%3A%22openstack%22%3BN%3Bs%3A6%3A%22docker%22%3BN%3B%7Ds%3A7%3A%22neutron%22%3BN%3Bs%3A4%3A%22nova%22%3BN%3B%7D |
所以传上去看看

查看源代码得到这个,提示我们 flag 在 /nssctfasdasdflag 这里

所以把 flag.php 改为nssctfasdasdflag 在输出一遍即可
playload:
1 | ?pks=O%3A3%3A%22acp%22%3A3%3A%7Bs%3A9%3A%22%00%2A%00cinder%22%3BO%3A3%3A%22ace%22%3A3%3A%7Bs%3A8%3A%22filename%22%3Bs%3A34%3A%22..%2F..%2F..%2F..%2F..%2F..%2Fnssctfasdasdflag%22%3Bs%3A9%3A%22openstack%22%3BN%3Bs%3A6%3A%22docker%22%3Bs%3A17%3A%22O%3A6%3A%22pkshow%22%3A0%3A%7B%7D%22%3B%7Ds%3A7%3A%22neutron%22%3BN%3Bs%3A4%3A%22nova%22%3BN%3B%7D |








