news 2026/10/20 17:37:39

使用Python分析Linux安全日志:SSH暴力破解IP统计实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
使用Python分析Linux安全日志:SSH暴力破解IP统计实战

半夜手机突然连续震动,登录服务器一看,/var/log/auth.log里全是Failed password记录,刷屏速度比我翻日志还快。这不是什么新鲜事,跑在公网上的 Linux 服务器只要开了 SSH,每天被扫描、被暴力破解几乎是例行公事。手动翻日志、复制 IP、再去查归属,一套下来半小时就没了,而且明天还得再来一遍。

这篇文章就是来解决这个问题的:用 Python 脚本自动读取 Linux 安全日志,把反复尝试登录的暴力破解源 IP 按次数统计出来,排出优先级,一次性告诉你要重点盯谁。整个脚本只依赖 Python 标准库,不用pip install任何东西,直接复制就能跑。适合刚接触 Linux 运维的开发者、兼职管服务器的程序员,以及所有不想在凌晨三点被日志刷到怀疑人生的人。

1. 拆解需求:暴力破解日志为什么值得单独写脚本

1.1 暴力破解的常见现象和日志痕迹

先还原一下真实场景。一台 Linux 服务器的 SSH 端口暴露在公网后,不需要有人特意针对你,各类扫描工具会自动全网扫描开放 22 端口的机器,然后拿常见用户名和弱密码字典轮番尝试。反映到日志里,就是这几类关键行:

  • Failed password for root from 203.0.113.10 port 50021 ssh2:密码试错了,最常见
  • Invalid user admin from 198.51.100.23 port 51234:尝试一个根本不存在的用户名
  • authentication failure:PAM 认证层报错,覆盖 sshd、sudo、su 等多个场景
  • Bad protocol version identification:客户端协议版本异常,多数是扫描器特征

公网服务器一天产生几千条这类记录很平常。问题是这些记录是流水账,一行行混在正常登录、系统任务、cron 日志里。你想知道“哪个 IP 最活跃”“总共试了多少次”,光靠肉眼加 Ctrl+F 根本不现实。

我最早就是拿grep加管道组合命令来做,比如grep "Failed password" /var/log/auth.log | awk '{print $11}' | sort | uniq -c | sort -rn。这个方法应急可以,但有两个毛病:一是隐私字段位置在不同发行版上有差异,awk 列号经常对不上;二是想加时间过滤、阈值判断、归档日志支持,命令会越来越长,最终变成一串只有自己能看懂的天书。

1.2 为什么用 Python 而不是直接写 grep/awk 命令

用 Python 不是炫技,是这类统计任务做到后面必然的选择。grep擅长过滤,awk擅长按列处理,但它们本质上还是文本流工具,一旦涉及“多个正则条件组合判断”“跨多个归档文件处理”“按时间窗口过滤”“输出结构化报告”,命令行的可读性和可维护性就撑不住了。

Python 这边的好处很直接:正则表达式一次写好,逻辑分行写清楚,改阈值、改时间范围、加输出格式都动一个参数就行。而且这个脚本不需要装第三方库,re、argparse、collections.Counter、gzip全是标准库。也就是说,任何一台有 Python 3 的 Linux 机器都能直接跑,不需要联网装包,也不需要 virtualenv。

选型背后还有一个更实际的考量:脚本写完不用一次性,它会变成你每周固定要跑的工具。既然要长期用,就得选择一个能让你三个月后打开还能看懂、六个月后想扩展还能改得动的方案。命令行一条龙做不到,Python 可以。

2. 看懂安全日志:匹配规则的前提是把日志读明白

2.1 auth.log 和 secure 日志的 3 类关键行

先认准日志位置。Debian、Ubuntu 等系统安全日志在/var/log/auth.log,RHEL、CentOS 系列在/var/log/secure。不同发行版路径不一样,但日志行格式大体相同,脚本里把路径作为参数传进去就行。默认值我设成了/var/log/auth.log,你用 CentOS 就改成/var/log/secure。

拿到日志后要分清三类关键行,因为它们代表完全不同的事件:

日志内容含义你需要关心的程度
Failed password for root from ...密码错误,可能是手滑也可能是爆破高,结合次数看
Invalid user guest from ...尝试不存在的用户高,基本可以断定是扫描器
Accepted publickey for root from ...登录成功极高,特别是出现在大量失败记录之后

一个容易被忽略的细节:很多人只盯着Failed password,却忘了在攻击成功或已经入侵之后,日志里会出现Accepted开头的行。如果某个 IP 先试了几百次然后突然成功登录,那问题就不是“封不封 IP”这么简单了,而是这台机器可能已经被控制。脚本虽然只统计失败记录,但你应该在拿到结果之后顺手搜一下这些可疑 IP 有没有对应的Accepted记录。这一点放到后面处置部分详细说。

2.2 日志里的时间、进程和 IP,正则分别怎么取

随便挑一行真实格式的日志拆开看:

Nov 5 03:12:07 web01 sshd[12345]: Failed password for root from 203.0.113.10 port 50021 ssh2

从左到右分别是:月份、日期、时间、主机名、进程名加 PID、冒号、消息正文。学正则的时候总想着用一个表达式把整行一口吞下去,那样写出来的东西既难读又脆弱。更稳妥的做法是分两步:

第一步先切结构。用一行正则把“时间”和“冒号后的正文”分别提取出来:

r'^(\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})\s+\S+\s+\S+(?:\[\d+\])?:\s+(.*)$'

第二步在正文里找关键词和 IP。先判断这行是不是失败认证记录,再从中抽取 IP:

r'(Failed password|Invalid user|authentication failure|Bad protocol version identification)' r'\b(\d{1,3}(?:\.\d{1,3}){3})\b'

把两个正则拆开还有个好处:日志格式再怎么变,你只需要调整其中一个部分。比如换个发行版后时间戳带年份,就改第一段;关注的行为变了,就改第二段。逻辑解耦,调试的时候一眼能看出是结构没匹配上还是关键词没命中。

关于时间戳,新手最容易忽略跨年问题。日志里的月份缩写没有年份,如果用当前年份去解析 12 月 31 日的日志,在 1 月看时会算成未来时间。脚本里加了一个简单纠正:如果解析出的时间比当前时间还晚一天以上,就把年份减一。这个处理覆盖了绝大多数跨年场景,够用。

3. 完整代码与核心逻辑:从读文件到输出排行

3.1 完整 Python 脚本

直接上代码,文件命名为auth_log_analyzer.py,Python 3.6 以上版本都能运行:

#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ auth_log_analyzer.py 分析 Linux 安全日志(/var/log/auth.log、/var/log/secure 等), 统计产生失败认证记录的源 IP,并按次数从高到低输出。 用法示例: python3 auth_log_analyzer.py python3 auth_log_analyzer.py /var/log/auth.log.1 /var/log/auth.log.2.gz python3 auth_log_analyzer.py --threshold 20 --days 7 """ import argparse import gzip import re import sys from collections import Counter from datetime import datetime, timedelta # 日志行结构:时间 主机名 进程名[PID]: 消息内容 # 同时兼容没有 PID 的 logger 输出 LINE_RE = re.compile( r'^(\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})\s+\S+\s+\S+(?:\[\d+\])?:\s+(.*)$' ) # 与暴力破解 / 可疑扫描相关的内容 BAD_MSG_RE = re.compile( r'(Failed password|Invalid user|authentication failure|' r'Bad protocol version identification)' ) IP_RE = re.compile(r'\b(\d{1,3}(?:\.\d{1,3}){3})\b') def read_lines(path): """按行读取日志,支持 .gz 压缩文件。""" if path.endswith('.gz'): with gzip.open(path, 'rt', errors='ignore') as f: yield from f else: with open(path, 'r', errors='ignore') as f: yield from f def extract(line): """从一行日志中提取 (时间字符串, 源IP),不匹配则返回 None。""" m = LINE_RE.match(line) if not m: return None ts_str = m.group(1) body = m.group(2) if not BAD_MSG_RE.search(body): return None m_ip = IP_RE.search(body) if not m_ip: return None return ts_str, m_ip.group(1) def parse_ts(ts_str, now): """把 'Nov 5 10:05:23' 转成 datetime,并做简单的跨年纠正。""" try: ts = datetime.strptime(f'{now.year} {ts_str}', '%Y %b %d %H:%M:%S') except ValueError: return None if ts > now + timedelta(days=1): ts = ts.replace(year=ts.year - 1) return ts def main(): ap = argparse.ArgumentParser(description='统计 Linux 安全日志中的暴力破解源 IP') ap.add_argument('logs', nargs='*', default=['/var/log/auth.log'], help='日志文件或归档文件,默认读取 /var/log/auth.log') ap.add_argument('--threshold', type=int, default=5, help='只输出失败次数达到该值的 IP,默认 5') ap.add_argument('--days', type=int, default=0, help='只看最近 N 天的记录,0 表示不限制,默认 0') args = ap.parse_args() counter = Counter() last_ts = {} now = datetime.now() cutoff = now - timedelta(days=args.days) if args.days > 0 else None for path in args.logs: for line in read_lines(path): result = extract(line) if not result: continue ts_str, ip = result if cutoff is not None: ts = parse_ts(ts_str, now) if ts is None or ts < cutoff: continue counter[ip] += 1 last_ts[ip] = ts_str if not counter: print('没有在指定日志中发现匹配的失败认证记录。') return 0 print(f'{"源 IP":<20}{"失败次数":>8} 最后匹配时间') print('-' * 48) for ip, cnt in counter.most_common(): if cnt >= args.threshold: print(f'{ip:<20}{cnt:>8} {last_ts.get(ip, "-")}') over = sum(1 for v in counter.values() if v >= args.threshold) print(f'\n共发现 {len(counter)} 个不同来源 IP,其中达到阈值({args.threshold})的有 {over} 个。') return 0 if __name__ == '__main__': sys.exit(main())

代码不复杂,核心就三个正则和一个Counter。但有几个细节是踩过坑之后才加进去的,下面逐个说。

3.2 脚本里最值得抄的 4 个细节

第一个细节是gzip自动识别。日志轮转是 Linux 的常规机制,/var/log/auth.log可能会被 logrotate 切成auth.log.1,再往前是auth.log.2.gz。如果不处理压缩文件,你只能分析当前日志,之前的攻击记录全丢。判断文件后缀.gz后使用gzip.open的一个小分支就解决了,脚本调用时把多个文件一次性传进去即可。

第二个细节是Counter配合most_common()做统计排序。计数这种事情用defaultdict也行,但Counter直接把“统计次数”和“按次数排序”封装好了,拿到手就是排好序的列表,代码少写五六行。这是 Python 标准库里被低估的好工具,遇到任何“统计频率、取 Top N”的需求,优先想它。

第三个细节是--days时间窗口。你可能只想看昨天的攻击,不想把三个月前的旧账翻出来。这个参数按天过滤,基于日志行里解析出的时间戳与当前时间比较。时间解析时的跨年纠正也在这里起作用,否则 1 月看 12 月的日志会把数据错误过滤掉。

第四个细节是--threshold阈值。默认值是 5,也就是失败次数达到 5 次的 IP 才输出。这个数字不要盲目用,内网测试机器可以设小一点,公网服务器建议调到 20 或 50,不然输出列表会很长。阈值其实就是报警灵敏度,调太低了天天误报,调太高了真出事时发现得晚。

3.3 运行一次,输出怎么读

在服务器上直接执行:

python3 auth_log_analyzer.py /var/log/auth.log

输出大概是这样的:

源 IP 失败次数 最后匹配时间 ------------------------------------------------ 203.0.113.10 128 Nov 5 03:12:07 198.51.100.23 42 Nov 5 09:45:33 192.0.2.7 23 Nov 4 22:18:10 共发现 37 个不同来源 IP,其中达到阈值(5)的有 3 个。

看结果时先看排名靠前的 IP,再看最后匹配时间。最后匹配时间离现在越近,说明攻击当前还在发生,优先级更高。如果输出为空,只有两种可能:日志路径不对,或者这台机器近期确实没有暴力破解尝试。前者你心里有数,后者恭喜你,你的网络环境比大多数人干净。

排名数字本身也要结合场景解读。一个 IP 尝试了 128 次,基本可以断定是字典爆破;一个 IP 只试了 3 次就被阈值过滤掉,可能是因为它刚扫到你的 IP 还没来得及展开攻击。所以脚本输出的是一个“优先级参考”,不是“攻击者完整名单”。

4. 验证与落地:没有攻击日志也能让脚本先跑通

4.1 用 logger 命令模拟暴力破解记录

刚拿到脚本,手头服务器可能干干净净一条失败记录都没有。这时候先别怀疑脚本,用系统自带的logger命令自己造几条记录出来验证。

logger是 Linux 标准的系统日志写入命令,可以指定日志类型和内容。在 root 权限下执行:

for ip in 203.0.113.10 203.0.113.10 198.51.100.23; do logger -p auth.notice -t sshd "Failed password for root from ${ip} port 50021 ssh2" done

这段命令会往 auth 日志里写入三条模拟的失败认证记录,其中203.0.113.10出现了两次。然后跑一下脚本:

python3 auth_log_analyzer.py /var/log/auth.log

如果你在输出里看到203.0.113.10计数为 2、198.51.100.23计数为 1,说明脚本解析、统计、排序的整条链路都是通的。这个测试方法会真实写入日志文件,介意的话做完了可以把对应行删掉,或者干脆在测试机上操作。

这种用系统工具模拟输入来验证脚本的套路,值得养成习惯。不管写什么解析脚本,先造一批可控的样本数据跑通逻辑,再上真实环境,能省掉大量排查时间。

4.2 把归档日志和 journalctl 数据也喂给脚本

生产环境里日志是分片的。/var/log/auth.log只是当前文件,历史记录在auth.log.1、auth.log.2.gz等归档里。脚本支持同时传入多个文件:

python3 auth_log_analyzer.py /var/log/auth.log /var/log/auth.log.1 /var/log/auth.log.2.gz

如果你用 systemd 系统,也可以从 journal 里取数据喂给脚本:

journalctl -u ssh --since "7 days ago" --no-pager > /tmp/ssh_log.txt python3 auth_log_analyzer.py /tmp/ssh_log.txt

这里要提醒一个兼容性问题。journalctl 默认输出的时间格式和传统 syslog 可能不同,如果时间格式带年份或带时区偏移,脚本里的LINE_RE第一段就匹配不上。遇到这种情况,先导出一小段看格式,再调整LINE_RE的时间部分。这也是做日志解析最常说的一句话:先看数据长什么样,再写解析规则。

4.3 接入 crontab 和告警,让脚本变成常用工具

一次跑一个文件只能看到当天情况,真正有价值的是定期汇总。服务器上配一条 cron 任务,让脚本每天早上自动跑一次,把结果追加到报告文件:

0 8 * * * /usr/bin/python3 /opt/scripts/auth_log_analyzer.py --days 1 --threshold 10 >> /var/log/ssh_daily_report.txt 2>&1

如果想更主动一点,把输出通过邮件发给自己。Linux 上配好mail命令后:

0 8 * * * /usr/bin/python3 /opt/scripts/auth_log_analyzer.py --days 1 --threshold 20 | mail -s "昨日SSH暴力破解TOP" you@example.com

这里要区分一个概念:脚本是审计和报告工具,不是自动防御工具。自动封禁 IP 的工作建议交给 fail2ban 这类专门软件,它会自己监控日志、触发封禁、处理解封。脚本的价值在于给你一张“攻击热度排行榜”,让你知道哪些 IP 值得关注、攻击趋势是在涨还是在跌。两件事分开做,各司其职,出了问题也好排查。

5. 踩坑记录与 IP 找到之后的处置建议

5.1 新手最容易踩的 5 个坑

坑现象解决办法
日志路径不对脚本输出“没有匹配记录”先确认系统是 auth.log 还是 secure,用 ls 查看实际路径
权限不足PermissionError用 sudo 运行,或把脚本放到有日志读取权限的用户下
正则匹配不到同一行肉眼能看到 IP,脚本就是抓不到导出样本日志,逐行检查时间格式和进程标记差异
时间跨年误判解析时间全在未来,--days 过滤后结果为空用脚本内置的跨年纠正逻辑,注意只覆盖单年跨年
IPv6 漏统计来自 IPv6 地址的攻击不出现在结果里脚本目前只匹配 IPv4,可自行添加 IPv6 正则

权限问题值得多说一句。/var/log/auth.log一般只有 root 和 adm 组能读,直接用普通用户跑大概率报错。实际使用中我建议把脚本放在/opt/scripts/下,用 sudo 手动跑,或者给脚本单独配一个可读取日志的系统用户,不要在 root 下跑定时任务跑上瘾,万一脚本被人改了就麻烦了。

正则匹配不到这个坑,最常见的原因是不同系统默认输出格式有差异。我见过有的系统日志里进程名后面没有 PID,有的时间戳格式多加了一个年份字段。排查方法很简单:head -n 20 /var/log/auth.log,拿真实日志对照LINE_RE的每一段,一段一段试。

5.2 找到 IP 之后,到底该不该手动封禁

脚本把可疑 IP 列出来了,下一步动作要冷静。很多人的第一反应是立刻iptables封掉,但实操中要区分情况。

如果是单纯尝试密码但从未成功的 IP,封禁是合理的,可以用 fail2ban 自动处理。如果某个 IP 既出现在失败列表里,又在之后的日志中留下了Accepted publickey for root的记录,那就不是封 IP 能解决的了,说明对方很可能已经成功进入了系统。此时要做的是立即隔离机器、排查登录凭据、检查是否被植入后门,而不是急着把 IP 加入黑名单。

另一个容易误伤的情况是代理和跳板。有些攻击 IP 其实是别人用代理池打的,封了一个还有下一个,意义有限。还有些时候,日志里的来源 IP 是公司出口 NAT 地址,内网很多人共用一个 IP,误封会导致正常同事也连不上。所以手工封禁前至少确认一下这个 IP 的行为特征,而不是看到次数多就一键封。

5.3 后续扩展方向

脚本目前输出的是终端文本,足够日常使用。想更进一步,可以把它扩展成三个方向:

一是输出 CSV,方便直接拖进表格工具做趋势分析。改动量很小,把结果循环里加一个csv.writer写文件即可。

二是做增量监控。每次只分析新产生的日志,记录上次读取到的文件偏移量,让脚本持续运行而不是每天全量扫描一遍。对日志量很大的服务器,这个优化能显著节省时间。

三是把结果写入数据库或直接调用告警接口。统计完成后,超过阈值的 IP 自动触发告警,相当于给服务器配了一个简易的入侵检测哨兵。

扩展的方向不一定要立刻全部实现,先让基础脚本稳定跑起来,确定你的服务器确实有暴力破解问题、确实需要更复杂的告警机制,再逐步加功能也不迟。

我在实际使用中还有一个个人习惯:脚本每周跑一次全量报告,单独存档。一个月下来回看趋势,能发现攻击高峰期、常用攻击源的变化,甚至能察觉到某些扫描行为是有规律的。这些信息单看某一天的日志得不到,但只要坚持定期统计,规律自己会浮出来。暴力破解不会消失,但被看见之后,就不再是深夜那条让你不安的通知了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 17:37:16

国产AI会议助手选型指南:语音识别、大模型与信创适配深度拆解

1. 从“能转写”到“敢放进会议室”&#xff1a;国产AI会议助手的真实门槛很多人第一次接触AI会议助手&#xff0c;关注点都落在“转写准不准”上。我一开始也是这样&#xff0c;拿一段带口音的录音丢进去&#xff0c;看谁家错字少&#xff0c;就觉得谁家强。但真正把这类工具推…

作者头像 李华
网站建设 2026/10/20 17:36:07

AI论文工具深度评测:从评分体系到TOP5榜单实测

打开任何一个平台搜索“AI论文工具”&#xff0c;你大概率会看到一堆标题几乎相同、结论高度一致、排名逻辑却经不起推敲的榜单。排在前面的往往不是“最好用的”&#xff0c;而是“广告费给得最到位的”。我前阵子帮几位刚开题的师弟师妹做工具选型&#xff0c;翻了几十篇所谓…

作者头像 李华
网站建设 2026/10/20 17:35:44

Agent Runtime 核心拆解:状态机、上下文与容错,构建生产级执行秩序

2. 为什么是 Runtime 而不是框架2.1 传统框架解决不了的问题市面上关于 Agent 的框架并不少&#xff1a;有的提供 Workflow 编排&#xff0c;有的提供声明式配置&#xff0c;有的提供拖拽式界面。但你真拿它们去跑一个稍微复杂的业务&#xff0c;就会很快撞上一堵墙&#xff1a…

作者头像 李华
网站建设 2026/10/20 17:32:09

Gemini-3-Pro-Preview登顶背后:Agent元年与LLM应用落地实践

Gemini-3-Pro-Preview登顶&#xff0c;一月的榜单刚出来的时候&#xff0c;技术群里的讨论热度比往年高了不少。大家关注的不是“谁分数又涨了两分”&#xff0c;而是榜单末尾悄悄多了一行字&#xff1a;Agent相关评测首次被纳入综合排名。这基本坐实了大家私下的判断——大模型…

作者头像 李华
网站建设 2026/10/20 17:32:09

H5企业官网源码改造:从选型到落地,打造简单大气站点

简介&#xff1a;一款基于HTML5制作的企业门户网站源码&#xff0c;设计简洁大气&#xff0c;适合中小企业用于形象展示与产品宣传&#xff0c;也适合前端学习者作为实战参考。模板采用响应式布局&#xff0c;在PC、平板和手机上均有良好浏览体验&#xff0c;并融入了HTML5语义…

作者头像 李华