背景
最近用suricata替换了zeek,但是在使用suricata时发现协议解析没有zeek做的细致。正好遇到需要检测mysql弱口令的需求。对比suricata,zeek修改起来难度更低。这篇文章可以帮助同样有检测需求的人,利用zeek实现mysql弱口令的检测。
首先搭建环境抓取MySQL登录认证包如下:
比较关键的报文即三次握手之后,服务器主动发送的Server Greeting和客户端返回的响应Login Request。
在Server Greeting包中,我们可以看到服务器使用的认证插件为mysql_native_password,这也是现在大部分mysql服务器默认的认证方式。
在Login Request包中,我们可以看到客户端向服务器发送的username和password,这里明文密码是123456,可以看到在mysql_native_password插件认证方式下,明文密码被替换成了hash值进行传输。
多登录几次并进行抓包,可以发现password的hash值是不断变化的。根据go语言版本的mysql驱动源码,我们可以了解到客户端在做登录认证时,如何处理的明文密码:
// Hash password using 4.1+ method (SHA1)
func scramblePassword(scramble []byte, password string) []byte {
if len(password) == 0 {
return nil
}
// stage1Hash = SHA1(password)
crypt := sha1.New()
crypt.Write([]byte(password))
stage1 := crypt.Sum(nil)
// scrambleHash = SHA1(scramble + SHA1(stage1Hash))
// inner Hash
crypt.Reset()
crypt.Write(stage1)
hash := crypt.Sum(nil)
// outer Hash
crypt.Reset()
crypt.Write(scramble)
crypt.Write(hash)
scramble = crypt.Sum(nil)
// token = scrambleHash XOR stage1Hash
for i := range scramble {
scramble[i] ^= stage1[i]
}
return scramble
}
根据源码可知,在认证时服务器发送的Greeting报文中包含Salt,而客户端收到Salt之后会使用SHA1算法将明文密码做2次SHA1,将结果与拼接Salt,再计算一次SHA1。
因此我们想在流量侧检测mysql弱口令,只能抓取salt值对弱口令字典生成相应的hash,再对比报文中的密码hash值,如果hash值一致我们就检测到了弱口令。
整个检测过程如图:
通过MySQL官方文档和我们抓取的pcap包,得到登录认证过程中的报文格式后,直接用zeek去跑流量,发现zeek的mysql.log日志中并没有salt值和password字段。
在查看zeek源码之后,我们可以知道zeek的内置协议解析器是用binpac这个开源组件编写的。我们只要修改mysql的协议解析器源码,让zeek在检测到mysql登录认证时,提取salt和password字段写入mysql.log日志就可以了。
首先编辑events.bif文件,在mysql_server_version事件的参数中增加salt参数。在mysql_handshake事件中增加password参数
event mysql_server_version%(c: connection, ver: string, salt: string%);
event mysql_handshake%(c: connection, username: string, password: string%);
然后编辑mysql-protocol.pac文件,依据mysql数据包修改Handshake_v10的结构、Handshake_Response_Packet_v10的结构。
type Handshake_v10 = record {
server_version : NUL_String;
connection_id : uint32;
auth_plugin_data_part_1: uint8[8];
filler_1 : uint8;
capability_flag_1 : uint16;
character_set : uint8;
status_flags : uint16;
capability_flags_2 : uint16;
auth_plugin_data_len : uint8;
pad : padding[10];
auth_plugin_data_part_2: uint8[12];
filler_2 : uint8;
auth_plugin_name : NUL_String;
};
type Handshake_Response_Packet_v10 = record {
cap_flags : uint32;
max_pkt_size : uint32;
char_set : uint8;
pad : padding[23];
username : NUL_String;
pad : padding[1];
password : uint8[20];
auth_plugin_name : NUL_String;
connection_attributes : bytestring &restofdata;
&let {
deprecate_eof: bool = $context.connection.set_deprecate_eof(cap_flags & CLIENT_DEPRECATE_EOF);
};
然后编辑mysql-analyzer.pac文件,修改处理mysql_server_version事件和mysql_handshake事件的函数逻辑。
function proc_mysql_initial_handshake_packet(msg: Initial_Handshake_Packet): bool
%{
if ( mysql_server_version )
{
if ( ${msg.version} == 10 )
{
std::string resultstring = zeek::util::fmt("%02x%02x%02x%02x%02x%02x%02x%02x"
"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
${msg.handshake10.auth_plugin_data_part_1[0]},
${msg.handshake10.auth_plugin_data_part_1[1]},
${msg.handshake10.auth_plugin_data_part_1[2]},
${msg.handshake10.auth_plugin_data_part_1[3]},
${msg.handshake10.auth_plugin_data_part_1[4]},
${msg.handshake10.auth_plugin_data_part_1[5]},
${msg.handshake10.auth_plugin_data_part_1[6]},
${msg.handshake10.auth_plugin_data_part_1[7]},
${msg.handshake10.auth_plugin_data_part_2[0]},
${msg.handshake10.auth_plugin_data_part_2[1]},
${msg.handshake10.auth_plugin_data_part_2[2]},
${msg.handshake10.auth_plugin_data_part_2[3]},
${msg.handshake10.auth_plugin_data_part_2[4]},
${msg.handshake10.auth_plugin_data_part_2[5]},
${msg.handshake10.auth_plugin_data_part_2[6]},
${msg.handshake10.auth_plugin_data_part_2[7]},
${msg.handshake10.auth_plugin_data_part_2[8]},
${msg.handshake10.auth_plugin_data_part_2[9]},
${msg.handshake10.auth_plugin_data_part_2[10]},
${msg.handshake10.auth_plugin_data_part_2[11]}
);
zeek::BifEvent::enqueue_mysql_server_version(connection()->zeek_analyzer(),
connection()->zeek_analyzer()->Conn(),
zeek::make_intrusive<zeek::StringVal>(c_str(${msg.handshake10.server_version})),
zeek::make_intrusive<zeek::StringVal>(resultstring));
}
if ( ${msg.version} == 9 )
zeek::BifEvent::enqueue_mysql_server_version(connection()->zeek_analyzer(),
connection()->zeek_analyzer()->Conn(),
zeek::make_intrusive<zeek::StringVal>(c_str(${msg.handshake9.server_version})),
zeek::make_intrusive<zeek::StringVal>(c_str(${msg.handshake9.scramble})));
}
return true;
%}
function proc_mysql_handshake_response_packet(msg: Handshake_Response_Packet): bool
%{
if ( ${msg.version} == 9 || ${msg.version == 10} )
connection()->zeek_analyzer()->AnalyzerConfirmation();
// If the client requested SSL and didn't provide credentials, switch to SSL
if ( ${msg.version} == 10 && ( ${msg.v10_response.cap_flags} & CLIENT_SSL ) )
{
connection()->zeek_analyzer()->StartTLS();
return true;
}
if ( mysql_handshake )
{
if ( ${msg.version} == 10 )
{
std::string resultstring = zeek::util::fmt("%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x"
"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
${msg.v10_response.password[0]},
${msg.v10_response.password[1]},
${msg.v10_response.password[2]},
${msg.v10_response.password[3]},
${msg.v10_response.password[4]},
${msg.v10_response.password[5]},
${msg.v10_response.password[6]},
${msg.v10_response.password[7]},
${msg.v10_response.password[8]},
${msg.v10_response.password[9]},
${msg.v10_response.password[10]},
${msg.v10_response.password[11]},
${msg.v10_response.password[12]},
${msg.v10_response.password[13]},
${msg.v10_response.password[14]},
${msg.v10_response.password[15]},
${msg.v10_response.password[16]},
${msg.v10_response.password[17]},
${msg.v10_response.password[18]},
${msg.v10_response.password[19]}
);
zeek::BifEvent::enqueue_mysql_handshake(connection()->zeek_analyzer(),
connection()->zeek_analyzer()->Conn(),
zeek::make_intrusive<zeek::StringVal>(c_str(${msg.v10_response.username})),
zeek::make_intrusive<zeek::StringVal>(resultstring));
}
if ( ${msg.version} == 9 )
zeek::BifEvent::enqueue_mysql_handshake(connection()->zeek_analyzer(),
connection()->zeek_analyzer()->Conn(),
zeek::make_intrusive<zeek::StringVal>(c_str(${msg.v9_response.username})),
zeek::make_intrusive<zeek::StringVal>(c_str(${msg.v9_response.password})));
}
return true;
%}
最后根据自己的需要,修改mysql的zeek脚本,编译后重新部署zeek,就成功得到了mysql登录过程中的salt值和password值。
{
"ts": 1713799007.950703,
"uid": "CzJc2757Pq7acqSuc",
"id_orig_h": "10.x.x.x",
"id_orig_p": 51752,
"id_resp_h": "10.x.x.x",
"id_resp_p": 3306,
"cmd": "login",
"version": "5.7.30",
"salt": "326356*********a7f6b3b372f275135",
"username": "root",
"password": "1c889**********b089bff3dd",
"success": true,
"rows": 0
}
可以使用任意方式采集zeek的mysql.log日志(如:filebeat),写入消息队列(如kafka)。再写一个程序,读取消息队列,得到salt值和password的hash进行碰撞,就可以检测弱口令了。下面是一个demo
type ZeekMySQLLog struct {
Ts float64 `json:"ts"`
Uid string `json:"uid"`
SrcIP string `json:"id_orig_h"`
SrcPort int `json:"id_orig_p"`
DstIP string `json:"id_resp_h"`
DstPort int `json:"id_resp_p"`
Cmd string `json:"cmd"`
Arg string `json:"arg"`
Version string `json:"version"`
Salt string `json:"salt"`
Username string `json:"username"`
Password string `json:"password"`
Success bool `json:"success"`
Rows int `json:"rows"`
}
func checkPasswd(mysqlLog []byte) {
var zeekLog ZeekMySQLLog
err := json.Unmarshal(mysqlLog, &zeekLog)
if err != nil {
logger.WithFields(logrus.Fields{"error": err}).Error("parse mysql log failed")
return
}
if zeekLog.Cmd != "login" {
//exclude non-login log
return
}
if zeekLog.Success != true {
//exclude login failed log
return
}
salt := zeekLog.Salt
password := zeekLog.Password
scramble, err := hex.DecodeString(salt)
if err != nil {
logger.WithFields(logrus.Fields{"error": err, "salt": salt}).Error("decode salt failed")
return
}
var knownpwd string
for _, pwd := range checker.weakPasswdList {
knownPwd := scramblePassword(scramble, pwd)
if knownPwd == password {
//detect weakpasswd
knownpwd = pwd
break
}
}
if len(knownpwd) > 0 {
fmt.Println(zeekLog)
fmt.Println(knownpwd)
return
}
}
func initWeakPasswdList(filePath string) ([]string, error) {
pwdFile, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("open %s failed: %v", filePath, err)
}
defer pwdFile.Close()
var pwdList []string
scanner := bufio.NewScanner(pwdFile)
for scanner.Scan() {
pwdList = append(pwdList, scanner.Text())
}
if err = scanner.Err(); err != nil {
return nil, fmt.Errorf("scan file failed: %v", err)
}
return pwdList, nil
}
func scramblePassword(scramble []byte, password string) string {
if len(password) == 0 {
return ""
}
// stage1Hash = SHA1(password)
crypt := sha1.New()
crypt.Write([]byte(password))
stage1 := crypt.Sum(nil)
// scrambleHash = SHA1(scramble + SHA1(stage1Hash))
// inner Hash
crypt.Reset()
crypt.Write(stage1)
hash := crypt.Sum(nil)
// outer Hash
crypt.Reset()
crypt.Write(scramble)
crypt.Write(hash)
scramble = crypt.Sum(nil)
// token = scrambleHash XOR stage1Hash
for i := range scramble {
scramble[i] ^= stage1[i]
}
scrambleString := hex.EncodeToString(scramble)
return scrambleString
}
原文始发于微信公众号(Desync InfoSec):Zeek使用与实践探索三
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论