从日志里找攻击 · 网络安全入门
有人在猜我们服务器的密码。怎么从日志里把他们找出来?
课程目录为什么需要它
预防总有失手的时候,所以防守方还需要检测。服务器会为每一次登录尝试写一行日志,几行 Python 就能把成千上万行日志变成一份可疑地址的短名单。这是安全分析师的日常工作,而且同样的技能(解析、计数、设阈值)适用于任何日志。
直觉理解
日志行就像句式固定的句子:时间、结果、用户、地址。正则表达式把这个句式描述一次,就能把各部分取出来:
\S+表示「一个或多个不是空格的字符」(...)把一部分捕获下来,之后用m.group(1)、m.group(2)等读取(FAIL|OK)表示「FAIL 或 OK」
接着用字典按 IP 地址统计失败次数,把超过阈值的地址都标出来。还有一种模式非常重要:同一个地址先失败很多次,然后突然成功,可能意味着攻击者已经进来了。
(这里的地址都来自专门留给文档使用的地址段,比如 203.0.113.0/24,不属于任何人。)
定义
正则表达式
A pattern that describes the shape of text. `re.match(pattern, line)` returns a match whose groups hold the captured parts, or None if the line doesn't fit.描述文字形状的模式。`re.match(pattern, line)` 返回一个匹配对象,它的分组里保存着捕获到的部分;如果这一行不符合,就返回 None。
暴力破解检测
Counting failed logins per source over a period and alerting when the count passes a threshold.在一段时间内按来源统计登录失败次数,超过阈值就报警。
示例
从一行里取出字段
动手试试一个模式描述了所有行。第 2 组和第 4 组分别是结果和 IP。
import re
lines = [
"2024-05-01 10:00:01 FAIL user=root ip=203.0.113.5",
"2024-05-01 10:00:02 FAIL user=admin ip=203.0.113.5",
"2024-05-01 10:00:04 FAIL user=amy ip=198.51.100.7",
"2024-05-01 10:00:05 FAIL user=test ip=203.0.113.5",
"2024-05-01 10:00:09 OK user=amy ip=198.51.100.7",
]
pattern = r"(\S+ \S+) (FAIL|OK) user=(\S+) ip=(\S+)"
for line in lines:
m = re.match(pattern, line)
print(m.group(2), m.group(3), m.group(4))- L9四个分组:时间、结果、用户、IP。
- L12
re.match从行首开始用模式匹配这一行。 - L13group(0) 是整行;分组从 1 开始数。
FAIL root 203.0.113.5
按地址统计失败次数
动手试试某个地址第一次出现时,counts.get(ip, 0) 给出 0。
import re
lines = [
"2024-05-01 10:00:01 FAIL user=root ip=203.0.113.5",
"2024-05-01 10:00:02 FAIL user=admin ip=203.0.113.5",
"2024-05-01 10:00:04 FAIL user=amy ip=198.51.100.7",
"2024-05-01 10:00:05 FAIL user=test ip=203.0.113.5",
"2024-05-01 10:00:09 OK user=amy ip=198.51.100.7",
]
pattern = r"(\S+ \S+) (FAIL|OK) user=(\S+) ip=(\S+)"
fails = {}
for line in lines:
m = re.match(pattern, line)
if m.group(2) == "FAIL":
ip = m.group(4)
fails[ip] = fails.get(ip, 0) + 1
for ip, n in fails.items():
print(ip, n, "ALERT" if n >= 3 else "")- L14只统计失败的尝试。
- L16给这个地址的计数加一。
- L18三次失败达到了阈值 3。
203.0.113.5 3 ALERT - L18失败一次后成功:大概是 amy 打错了。
常见错误
只盯着失败
失败说明有人在试;一连串失败后紧跟一次成功,说明对方可能已经进来了。一定要看看每个可疑地址接下来做了什么。
不看时间窗口
两秒内失败十次是攻击;一个月里失败十次只是健忘的用户。真正的检测会在时间窗口内计数,比如 5 分钟。
小测验
1. 这段代码会输出什么?
import re
m = re.match(r"(\S+) user=(\S+)", "FAIL user=root extra")
print(m.group(1), m.group(2))
print(re.match(r"OK", "FAIL user=x"))查看答案
答案:A
\S+ 遇到第一个空格就停,所以第 2 组只是 root。不符合模式的行得到 None。
2. 下面哪种日志模式最令人担心?
查看答案
答案:B
短时间大量失败是在猜密码;随后的成功说明可能猜中了。先调查那个账号。
动手练习
3 道练习,分三层:先预测程序的输出,再按新需求改程序,最后从零写一个。代码在浏览器里运行,提交马上判题。
- 预测输出统计用户
准确写出这段程序的输出。
- 改需求报警名单
程序会打印每个地址的失败次数。修改它,让它只打印失败不少于 3 次的地址,格式为
ALERT <ip> <次数>,次数多的排在前面。 - 从零写他们进来了吗?
写一个函数
compromised(lines, limit)。每行的格式是"<时间> FAIL|OK user=<用户名> ip=<IP>",按时间顺序排列。返回一个排好序的列表,包含所有满足条件的 IP:失败次数至少达到limit,并且在那之后有一次成功登录(OK)。
本课术语
| 术语 | 中文 | 英文定义 |
|---|---|---|
| Log | 日志 | A record a program writes for each event, one line at a time. |
| Regular expression | 正则表达式 | A pattern describing the shape of text, used to match and extract parts. |
| Threshold | 阈值 | The value above which an alert is raised. |
| Indicator of compromise | 入侵迹象 | Evidence that an attacker may have got in, such as a success after many failures. |