本文根据对Zeek的POP3 API的测试结果,初步归纳了一个pattern,编写Zeek脚本,输出POP3的解析日志。
我们知道,POP3协议请求获取邮件的的命令是RETR
,而根据使用Zeek解析POP3协议(1)对Zeek API的测试结果,可以初步归纳一个解析邮件内容的pattern:
- 邮件起始标志:
- client发出
RETR
命令,请求获取某封邮件; - server响应
OK
;
- client发出
- 邮件内容:多行字符串
- 邮件结束标志:遇到一个新的命令
其中起始标志的2项判断条件都要满足:RETR
请求获取邮件,OK
代表响应请求成功,而在遇到下一个请求命令之前,我们认为
之间的多行字符串都是邮件内容。因此解析邮件实质上是在API输出的多行sting中逐行跟踪分析以上pattern。
首先创建一个用于保存单封邮件信息的record Msg
:
type Msg: record {
ts: time;
uid: string;
id: conn_id;
flag_retr_succ: bool; # 标识retr请求是否获取邮件内容成功
request: string; # 请求命令
req_arg: string; # 请求命令参数
reply: string; # 响应
retr_data_linenum: int; # 邮件内容的字符串行数
retr_data: vector of string; # 保存邮件内容
};
其中flag_retr_succ
用于标识邮件起始标志2项条件是否符合。
1. 分析邮件起始
在pop3_request
追踪retr
命令:
event pop3_request(c: connection, is_orig: bool, command: string, arg: string)
{
...
if(command == "RETR") {
local retr_msg: Msg = [ts = network_time(),id = cid,uid = cuid,flag_retr_succ = F,
request = "RETR",req_arg = arg,
reply = "",retr_data_linenum = 0,
$retr_data = vector()];
g_retr_msg = retr_msg;
}
...
}
监测到retr
命令后,初始化一个全局的Msg
。
相应地,在pop3_reply
分析服务器是否响应OK
:
event pop3_reply(c: connection, is_orig: bool, cmd: string, msg: string)
{
...
if(cmd == "OK" && g_retr_msg?flag_retr_succ) { # g_retr_msg exists
if(g_retr_msgrequest == "RETR" && g_retr_msgreply == "") {
g_retr_msgflag_retr_succ = T;
g_retr_msg$reply = "OK";
}
}
}
如果发现:
- reply命令为
OK
- 全局msg中记录的request命令是
retr
,标志flag_retr_succ
为False,且reply为空
说明此reply
响应的是一个retr
的请求命令,获取邮件成功。至此邮件起始的2项条件满足:
2. 保存邮件内容
邮件起始之后的多行字符串默认为邮件内容,使用pop3_data
保存邮件内容:
event pop3_data(c: connection, is_orig: bool, data: string)
{
if(g_retr_msg?flag_retr_succ && g_retr_msgflag_retr_succ == T) {
if(g_retr_msgretr_data_linenumretr_data += data;
g_retr_msg$retr_data_linenum += 1;
}
}
}
监测到flag_retr_succ
为True,即此时data的内容都是获取的邮件信息,保存到字符串vector中再逐行解析。
分析邮件获取结束并解析邮件内容
同样使用pop3_request
追踪邮件结束:
event pop3_reply(c: connection, is_orig: bool, cmd: string, msg: string)
{
...
pop3_proc_g_retr_msg(); # 监测邮件结束标志并解析
if(cmd == "OK" && g_retr_msg?$flag_retr_succ) { # g_retr_msg exists
...
}
...
}
其中pop3_proc_g_retr_msg:
function pop3_proc_g_retr_msg()
{
if(g_retr_msg?flag_retr_succ && g_retr_msgflag_retr_succ == T) {
# 更新pop3 info信息
local rec: POP3::Info = [ts = g_retr_msgts,
uid = g_retr_msguid,
id = g_retr_msgid];
g_pop3_rec = rec;
...
# 解析邮件内容
for(idx in g_retr_msgretr_data) {
# print g_retr_msgretr_data[idx];
local data:string = g_retr_msg$retr_data[idx];
local key: string = "";
local val: string = "";
local len: int;
if(data != "") {
# 匹配to字段
if(/^[tT][oO]:/ in data) {
key = "to";
val = data[3:];
}
else if(/^[fF][rR][oO][mM]:/ in data) {
key = "from";
val = data[6:];
}
...
}
if(key != "" && val != "")
pop3_update_g_rec_data(key, val);
}
# 写入pop3日志
Log::write(POP3::LOG, g_pop3_rec);
# 解析一封邮件结束,重新初始化全局的msg
...
}
}
该函数在pop3_requrest
被调用,并且判断全局msg中flag_retr_succ
是否为T,如果T说明遇到了一个新的命令,即邮件获取完成:
之后解析保存在msg中的邮件内容,并更新pop3的info信息(参考Zeek默认的smtp解析脚本,POP3脚本也创建了一个类似的info record,用于将解析结果写入日志)。
解析保存的邮件内容字符串,同样参考Zeek smtp的解析脚本,使用正则匹配邮件关键字段,例如from, to等。
3. 脚本解析结果
用以上思路编写解析脚本,在测试邮件上的解析结果:
$ cat pop3.log | jq
{
“ts”: 1615003258.432899,
“uid”: “CeJci2byiawb4zZlk”,
“id.orig_h”: “192.168.153.18”,
“id.orig_p”: 39118,
“id.resp_h”: “192.168.153.19”,
“id.resp_p”: 110,
“command”: [
“RETR”,
“OK”
],
“arg”: [
“2”
],
“date”: ” Fri, 5 Mar 2021 23:00:37 -0500″,
“from”: “lisi lisi@localdomain.com“,
“to”: [
” zhangsan@localdomain.com”
],
“msg_id”: ” 7ea7b5a3-3e76-ceee-2a49-a9ab81d5cc4c@localdomain.com“,
“subject”: ” This is a test mail”,
“user_agent”: ” Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101″
}
可以看到,对比使用Zeek解析POP3协议(1)中测试邮件的内容,该脚本解析了邮件内容的相应字段信息。如果需要解析更多字段,增加类似的正则匹配即可。
该脚本只是基于一个比较粗糙的pattern,一些细节可能尚未考虑到。同时也有一些问题:
- 有的邮件字段信息,例如测试邮件样例中的UA跨行了,即保存在多行的字符串中,应如何处理。
- 如何解析邮件附件等。