人人都能学编程
English 开始学习
专项 · 网络安全第 8 课 / 共 9 课免费阅读

从日志里找攻击 · 网络安全入门

有人在猜我们服务器的密码。怎么从日志里把他们找出来?

课程目录

为什么需要它

预防总有失手的时候,所以防守方还需要检测。服务器会为每一次登录尝试写一行日志,几行 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))
逐步执行
  1. L9四个分组:时间、结果、用户、IP。
  2. L12re.match 从行首开始用模式匹配这一行。
  3. 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 "")
逐步执行
  1. L14只统计失败的尝试。
  2. L16给这个地址的计数加一。
  3. L18三次失败达到了阈值 3。203.0.113.5 3 ALERT
  4. 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"))
  1. A
    FAIL root
    None
  2. B
    FAIL root extra
    None
  3. C
    FAIL user=root
    False
查看答案

答案:A

\S+ 遇到第一个空格就停,所以第 2 组只是 root。不符合模式的行得到 None。

2. 下面哪种日志模式最令人担心?

  1. A一个地址每天失败一次
  2. B一个地址一分钟内失败 40 次,然后成功了
  3. C40 个不同用户各自登录成功
查看答案

答案:B

短时间大量失败是在猜密码;随后的成功说明可能猜中了。先调查那个账号。

动手练习

3 道练习,分三层:先预测程序的输出,再按新需求改程序,最后从零写一个。代码在浏览器里运行,提交马上判题。

  1. 预测输出统计用户

    准确写出这段程序的输出。

  2. 改需求报警名单

    程序会打印每个地址的失败次数。修改它,让它只打印失败不少于 3 次的地址,格式为 ALERT <ip> <次数>,次数多的排在前面。

  3. 从零写他们进来了吗?

    写一个函数 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.