Zeek使用与实践探索三

admin 2024年4月23日23:44:39评论4 views字数 8744阅读29分8秒阅读模式

背景

最近用suricata替换了zeek,但是在使用suricata时发现协议解析没有zeek做的细致。正好遇到需要检测mysql弱口令的需求。对比suricata,zeek修改起来难度更低。这篇文章可以帮助同样有检测需求的人,利用zeek实现mysql弱口令的检测。

01
MySQL登录报文

首先搭建环境抓取MySQL登录认证包如下:

Zeek使用与实践探索三

比较关键的报文即三次握手之后,服务器主动发送的Server Greeting和客户端返回的响应Login Request。

在Server Greeting包中,我们可以看到服务器使用的认证插件为mysql_native_password,这也是现在大部分mysql服务器默认的认证方式。

Zeek使用与实践探索三

在Login Request包中,我们可以看到客户端向服务器发送的username和password,这里明文密码是123456,可以看到在mysql_native_password插件认证方式下,明文密码被替换成了hash值进行传输。

Zeek使用与实践探索三

多登录几次并进行抓包,可以发现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值一致我们就检测到了弱口令。

整个检测过程如图:

Zeek使用与实践探索三

Zeek使用与实践探索三
02
协议解析

通过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使用与实践探索三
03
弱口令检测

可以使用任意方式采集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}
Zeek使用与实践探索三

原文始发于微信公众号(Desync InfoSec):Zeek使用与实践探索三

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年4月23日23:44:39
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Zeek使用与实践探索三https://cn-sec.com/archives/2685152.html

发表评论

匿名网友 填写信息