一道非常有意思的反序列化漏洞的题目

花费了我不少时间理解和记忆

这里简单记录其中精髓

首先打开是一个登陆页面

dirsearch扫描到了www.zip源码备份

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
}
?>

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
}
?>

class.php

<?php
require('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);

config.php

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

本以为是传统的文件上传

尝试后发现不行

审计源码发现

文件上传后文件名被md5加密了,思路断掉

看了大佬wp后发现关键在于update页面上传的所有参数都被序列化后存入数据库

$user->update_profile($username, serialize($profile));

存入时先对序列化后的字符串进行了正则过滤

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);
}

flag存放在config.php中

而update页面的数据提交后,会跳转到展示页面,其实就是读取了数据

$photo = base64_encode(file_get_contents($profile['photo']));

那也就是说只要能读取到config.php就能得到flag

这里就需要利用到反序列化字符串逃逸漏洞

这里引用大佬的例子来介绍原理https://www.cnblogs.com/litlife/p/11690918.html

看一个简单的序列化

<?php
$kk = "123";
$kk_seri = serialize($kk); //s:3:"123"; echo unserialize($kk_seri); //123 $not_kk_seri = 's:4:"123""'; echo unserialize($not_kk_seri); //123"

从上例可以看到,序列化后的字符串以"作为分隔符,但是注入"并没有导致后面的内容逃逸。这是因为反序列化时,反序列化引擎是根据长度来判断的。

也正是因为这一点,如果程序在对序列化之后的字符串进行过滤转义导致字符串内容变长/变短时,就会导致反序列化无法得到正常结果。看一个例子

<?php

    $username = $_GET['username'];
$sign = "hi guys";
$user = array($username, $sign); $seri = bad_str(serialize($user)); echo $seri; // echo "<br>"; $user=unserialize($seri); echo $user[0];
echo "<br>";
echo "<br>";
echo $user[1]; function bad_str($string){
return preg_replace('/\'/', 'no', $string);
}

先对一个数组进行序列化,然后把结果传入bad_str()函数中进行安全过滤,将单引号转换成no,最后反序列化得到的结果并输出。看一下正常的输出:

用户ka1n4t的个性签名很友好。如果在用户名处加上单引号,则会被程序转义成no,由于长度错误导致反序列化时出错。

那么通过这个错误能干啥呢?我们可以改写可控处之后的所有字符,从而控制这个用户的个性签名。我们需要先把我们想注入的数据写好,然后再考虑长度溢出的问题。比如我们把他的个性签名改成no hi,长度为5,在本程序中序列化的结果应该是i:1;s:5:"no hi";,再跟前面的username的双引号以及后面的结束花括号闭合,变成";i:1;s:5:"no hi";}。见下图

我们要让'经过bad_str()函数转义成no之后多出来的长度刚好对齐到我们上面构造的payload。由于上面的payload长度是19,因此我们只要在payload前输入19个',经过bad_str()转义后刚好多出了19个字符。

尝试payload:ka1n4t'''''''''''''''''''";i:1;s:5:"no hi";}

成功注入序列化字符。

这道题的原理也是这样的

突破口

我们发现一个问题,我们反序列化字符逃逸,首先序列化的字符是可控的,还有前面的长度是可控的。但update.php将参数序列化,我们可控变量的长度就已经写死了,怎么才能去控制呢。这道题的突破口其实就是序列化过后数据过滤替换那里,看似更加安全,其实更加危险。

因为序列化后的字符串在存入数据库之前进行了过滤 ,我们查看源码发现,这里是将‘select‘, ‘insert‘, ‘update‘, ‘delete‘, ‘where‘替换成‘hacker‘,其中使得位数变长的只有where替换成hacker,长度多出了一位

我们写入where替换成hacker之后字符串实际的长度就+1,因此实际的长度大于序列化固定的长度(变量前面‘s’里的值)。利用反序列化字符串逃逸,反序列化时只能将字符串中nickname前面的s后面长度的字符串反序列化成功,这个是传参的时候就固定好了。剩下的字符串我们构造成class.php因为里面包含了flag,并且让他在photo位置上,然后把photo给扔掉,这样在profile.php中读取的photo就是我们构造的config.php了,也就是读取到了flag

我们要记住一点,我们的字符串是在某变量被反序列化得到的字符串受某函数的所谓过滤处理后得到的,而且经过处理之后,字符串的某一部分会加长,但描述其长度的数字没有改变(该数字由反序列化时变量的属性决定),就有可能导致PHP在按该数字读取相应长度字符串后,本来属于该字符串的内容逃逸出了该字符串的管辖范围,轻则反序列化失败,重则自成一家成为一个独立于原字符串的变量,若是这个独立出来的变量末尾是个 ";} ,则可能会导致反序列化成功结束,后面的内容也就被丢弃了。此处能逃逸的字符串的长度由经过滤后字符串增加的长度决定。

先确定好我们要读取的文件的序列化内容

s:5:"photo";s:10:"config.php";}

简单的理解就是利用后端的过滤函数替换字符,导致实际长度增加,增加的部分(config.php)被挤了出来,到了本来photo的位置上,然后闭合。(原本的photo被丢弃)

这里还有一个知识点

因为传入nickname时后端写了正则过滤

        if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');

限制了nickname的长度

所以通过传入数组来绕过长度限制(抓包修改nickname为nickname[])

又因为数组序列化后不同于数组,会变成{~~~~~~}的形式

所以构造payload时前面要加上";}来闭合数组

否则反序列化无法正常执行

那就获取不到任何后端传来的数据

简单的来说,要序列化的字符是由我们所确认的,而后台因为正则过滤字符导致字符串长度更长了,那么就会把我们后面的字符挤出去,长了多少就挤出去多少,挤出去的字符就脱离了nickname参数的控制,又因为被挤出去的字符最后是";},闭合了序列化字符串,所以原本的photo被丢弃,我们构造的恶意 photo就鸠占鹊巢,取而代之

那么最终的payload为

wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}

得得到

文件被请求到页面时被base64加密了

解密即可得到flag

[0CTF 2016] piapiapia的更多相关文章

  1. 刷题记录:[0CTF 2016]piapiapia

    目录 刷题记录:[0CTF 2016]piapiapia 一.涉及知识点 1.数组绕过正则及相关 2.改变序列化字符串长度导致反序列化漏洞 二.解题方法 刷题记录:[0CTF 2016]piapiap ...

  2. BUUCTF |[0CTF 2016]piapiapia

    步骤: nickname[]=wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere ...

  3. [0CTF 2016]piapiapia{PHP反序列化漏洞(PHP对象注入)}

    先上学习链接: https://www.freebuf.com/column/202607.html https://www.cnblogs.com/ichunqiu/p/10484832.html ...

  4. [0CTF 2016]piapiapia(反序列逃逸)

    我尝试了几种payload,发现有两种情况. 第一种:Invalid user name 第二种:Invalid user name or password 第一步想到的是盲注或者报错,因为fuzz一 ...

  5. [原题复现+审计][0CTF 2016] WEB piapiapia(反序列化、数组绕过)[改变序列化长度,导致反序列化漏洞]

    简介  原题复现:  考察知识点:反序列化.数组绕过  线上平台:https://buuoj.cn(北京联合大学公开的CTF平台) 榆林学院内可使用信安协会内部的CTF训练平台找到此题 漏洞学习 数组 ...

  6. 2016 piapiapia 数组绕过

    0x00.感悟      写完这道题,我感觉到了扫源码的重要性.暑假复现的那些CVE,有的就是任意文件读取,有的是任意命令执行,这些应该都是通过代码审计,得到的漏洞.也就和我们的CTF差不多了.    ...

  7. 从一道ctf看php反序列化漏洞的应用场景

    目录 0x00 first 前几天joomla爆出个反序列化漏洞,原因是因为对序列化后的字符进行过滤,导致用户可控字符溢出,从而控制序列化内容,配合对象注入导致RCE.刚好今天刷CTF题时遇到了一个类 ...

  8. GYCTF easyphp 【反序列化配合字符逃逸】

    基础知识可以参考我之前写的那个 0CTF 2016 piapiapia  那个题只是简单记录了一下,学习了一下php反序列化的思路 https://www.cnblogs.com/tiaopideju ...

  9. BUUCTF知识记录

    [强网杯 2019]随便注 先尝试普通的注入 发现注入成功了,接下来走流程的时候碰到了问题 发现过滤了select和where这个两个最重要的查询语句,不过其他的过滤很奇怪,为什么要过滤update, ...

随机推荐

  1. JSON.toJSONString(Object object,SerializerFeature...features)详解

    D package myProject; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; ...

  2. UI布局 自定义布局

    今天学习了UI布局当中的自定义的布局的部分,在开始的时候先动手写了一个跟随手指移动的小兔子的实例,初步的了解了布局管理器的概念之后开始正式进行布局管理器,其中包括相对布局,线性布局,帧布局,表格布局, ...

  3. Hive文件与记录格式

    1. Hive文件与记录格式 Create table 有多种用法,例如STORED AS SEQUENCEFILE, ROW FORMAT DELIMITED, SERDE, INPUTFORMAT ...

  4. Java实现JSONObject对象与Json字符串互相转换

    Java实现JSONObject对象与Json字符串互相转换 JSONObject 转 JSON 字符串 Java代码: JSONObject jsonObject = new JSONObject( ...

  5. VS“无法查找或打开PDB文件”是怎么回事?如何解决

    有时候,我们使用 VS(Visual Studio)编译程序时会出现“无法查找或打开PDB文件”的提示,并且此时程序会生成失败,无法运行,如下图所示: 大家不要惊慌,出现这种提示并不是代码写错了,而是 ...

  6. docker删除镜像时报错解决办法

    报错信息 [root@kvm ~]# docker rmi 4950a54ede5a Error response from daemon: conflict: unable to delete 49 ...

  7. Dataset数据的XML持久化处理

    主要方法是用ADO.NET的DataTale 填充到Dataset Dataset 内置了XML持久化的方法,WriteXML和ReadXML:简单的WinFrom实例:从数据库的表 private ...

  8. java框架-Mybatis

    一.Mybatis介绍 mybatis是一个持久层的框架,是对JDBC操作数据库的封装,使开发者只需要关注业务本身,不需要花费精力去处理加载驱动.创建数据库连接对象.创建statement语句对象.参 ...

  9. Bootstrap环境安装加使用---开启Bootstrap 之旅

    1.首先去Bootstrap官网https://getbootstrap.com/上下载Bootstrap 的最新版本 对应的中文页面如下: 下载自己所需要的版本(目前我自己下载的是编译的css和js ...

  10. Codeforces Round #614 (Div. 2)E(思维,构造,DP)

    构造边权,从0开始给边赋值,初始选取一条边权为0,每次赋值的贡献为这一条链两侧的结点(包含链的端点)个数之积,下一次赋值以当前链其一端点续一条边,边权为上次赋的值+1.先DFS找到点的组合这条链两侧结 ...