ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

自动化攻击图生成器:从网络拓扑到攻击路径的Python实现

自动化攻击图生成器:从网络拓扑到攻击路径的Python实现 简介一份面向安全分析师、渗透测试人员与网络安全学习者的自动化攻击图生成器源码。项目基于Python开发并集成Shell脚本用于将网络拓扑、漏洞情报和潜在攻击路径组织为可视化攻击图降低手工建模的门槛。压缩包约37.75MB共101个文件44个JSON与10个YAML负责配置解析10个DOT描述攻击图与拓扑图结构10个PDF用作分析报告另有Python源码、pyc字节码、Shell脚本、Git配置与readme说明目录清晰便于按模块阅读和二次开发。目前已有321人学习下载。通过源码可掌握从配置读取、节点和边构建到DOT渲染、PDF汇总的完整数据流字节码与源码并存便于对照执行逻辑Shell脚本则有助于自动化部署适合课程设计、安全研究及内部威胁建模工具开发。无论用于毕业设计还是企业内网安全评估都能快速搭建攻击路径分析原型。1. 自动化攻击图生成器到底在干什么一个反直觉的性价比结论先说一个容易误判的结论基于 Python 的 attack-graph-generator 自动化攻击图生成器源码本质上不是一个“扫描器”也不是“画图工具”而是一个把网络拓扑、漏洞情报、攻击者权限变化这三件事压缩成图数据的推理引擎。我第一次用这类工具时也以为是扫描器后来发现它最大的价值是把“某台机器能打某台机器”变成可查询、可排序、可自动扩展的数据结构而不是给你一张好看的图。这个方向适合三类人一类是攻防演练里天天要写报告的安全工程师一类是负责内网风险基线的运维/安全团队一类是刚接触安全自动化、想在 Python 生态里把手动路径分析换成代码实现的从业者。如果你只想“画个攻击关系图”Graphviz 手写 DOT 就够了没必要碰攻击图生成器如果你想让系统根据漏洞和拓扑自动推断攻击路径、排序风险那这套源码思路才是你真正要的东西。下面我把常见做法里的核心设计、关键代码和踩坑点一次讲清楚。2. 攻击图生成的最小可运行架构选型理由、数据模型与项目骨架2.1 为什么选 Python 做攻击图生成不选 igraph 的直观理由常见做法是攻击图生成器第一版多半落在 Python 里。原因不是 Python 性能好而是这个场景的瓶颈根本不在图计算而在输入数据的解析上。你需要处理 Nmap 的 XML 扫描结果、Nessus 的 CSV 漏洞列表、CMDB 导出的拓扑表甚至还要读某些资产管理系统里导出的 Excel。Python 对这些格式的解析支持最全csv、xml、json、openpyxl 都有成熟库能省下大量体力活。另一个理由是图计算的生态。networkx 虽然性能不如 igraph但上层封装丰富社区例子多对“原型到落地”的节奏非常友好。你在源码里读到的构建邻接矩阵、找可达路径、算最短攻击链networkx 都有对应 API不需要自己从零写图结构。如果未来数据量到百万节点级别再考虑换 igraph 或迁移到图数据库但第一版用 networkx 完全够。还有一点是安全圈子自己的数据习惯。漏洞库、ATTCK 战术映射、CWE 分类这些情报大多以 JSON/CSV 形式分发Python 在这类数据处理上有天然优势。拿到的源码如果基于 Python通常意味着你可以直接改 parser 接入新的数据源而不必懂一堆底层编译细节。对于工程师来说这意味着后续扩展成本低团队里随便一个人都能接手改。2.2 attack-graph-core攻击图的三个核心数据模型我把代码里的核心抽象拆成三层。第一层是 Host表示网络里的一个节点包含 IP、开放端口、服务、漏洞列表。第二层是 Edge表示一次攻击动作能否从 A 到 B包含源、目标、利用条件、动作名。第三层是 AttackGraph把 Host 和 Edge 组合成有向图再加上起始攻击者位置和目标节点就能做路径查询。这里我一般不会把漏洞直接塞在 Edge 上而是拆成 Host.vulns 和 Edge.condition原因是漏洞列表通常来自扫描器而攻击动作的条件判断来自规则引擎两者更新频率完全不同。扫描器跑一次、规则库更新几十次如果耦合在一个对象里每次更新规则都要重扫一遍拓扑很蠢。还有一种常见设计是把攻击者位置建模成一个特殊 Host叫 attacker_node。它不参与实际扫描只是路径枚举的起点。这样你可以描述“攻击者从外网入口进入”或者“从一台失陷主机横向移动”变化的是 attacker_node 连接的 Edge而不是图结构本身。这种设计看起来简单却能让路径计算代码完全复用源码里值得多看的也是这部分。2.3 最小项目骨架attack_graph_generator 目录怎么搭依赖怎么装如果让你自己搭一个可工作的 attack_graph_generator我会建议目录按输入解析、模型定义、算法、报告输出、可视化五块拆。一个常见的骨架长这样attack_graph_generator/ ├── attack_graph_generator/ │ ├── __init__.py │ ├── models.py # Host/Edge/AttackGraph 定义 │ ├── parser.py # 拓扑、漏洞、权限数据解析 │ ├── algorithm.py # 矩阵构建、路径枚举、评分 │ ├── report.py # 导出 DOT / JSON │ └── viz.py # Graphviz 渲染 SVG ├── config/ │ ├── topology.yaml │ ├── vulnerabilities.yaml │ └── attacker.yaml ├── data/ # 原始扫描结果放这里 ├── output/ # 生成的图和报告输出到这里 ├── main.py # 入口 └── requirements.txt依赖安装方面最低限度只需要四样networkx、pyyaml、graphvizPython 绑定库系统还需要装 Graphviz 本体。我常用的是 Python 3.8 以上版本配合 pip 安装命令如下python3 -m venv venv source venv/bin/activate pip install networkx pyyaml graphviz sudo apt-get install graphviz这段命令里venv 是隔离环境避免把依赖装进系统 Pythonpip 安装的是 Python 侧的库sudo apt-get install graphviz装的是 Graphviz 本体程序dot 命令就靠它。如果你的系统是 WindowsGraphviz 本体去官网下载安装包然后把安装目录下的 bin 加进 PATH否则 Python 调用graphviz.render()时找不到 dot 程序这也是新手最常见的翻车点。3. 从拓扑和漏洞到攻击路径核心算法与源码级的逐步实现3.1 构建邻接矩阵先读懂这个攻击图生成器的关键一步攻击图生成器源码里最重要的一段是从网络拓扑构建邻接矩阵。这个矩阵的每一行、每一列对应一个主机单元格的值表示“从这台机器能否到达那台机器”。很多入门者以为邻接矩阵就是网络连通性两张网卡能 ping 通就算 1实际上在攻击图里值不是 0/1而是“可利用性评分”。我一般会先读拓扑文件把主机装进列表再逐条判断可达性。以下是一段可运行的构建逻辑import ipaddress from models import Host def build_network_matrix(hosts: list[Host]) - list[list[float]]: 构建网络层邻接矩阵值表示源主机到目标主机的可达性评分(0~1)。 1.0 表示直连可达0.0 表示不可达。 matrix [[0.0 for _ in hosts] for _ in hosts] for i, src in enumerate(hosts): for j, dst in enumerate(hosts): if i j: matrix[i][j] 1.0 # 自身视为可达 continue # 判断源是否在目标允许的网段内 if is_in_subnet(src.ip, dst.allowed_cidrs): matrix[i][j] 1.0 return matrix def is_in_subnet(ip: str, cidrs: list[str]) - bool: addr ipaddress.ip_address(ip) return any(addr in ipaddress.ip_network(cidr, strictFalse) for cidr in cidrs)这段代码的逻辑很简单每台主机声明自己允许哪些 CIDR 访问自己源 IP 落在其中任何一个网段就算直连可达。strictFalse的意思是把网络地址本身也视为网段内地址这在处理主机地址和网络地址混用时不报错。注意这里只表示网络层可达后续还要叠加漏洞条件矩阵才会从“能访问”变成“能攻破”。我在实际使用中会把全网段计算提前缓存因为主机数量多的时候每对主机都做一次 ipaddress 转换会比较慢。一个优化点是先把每个 CIDR 转成 ipaddress.ip_network 对象存起来不要做一次判断转换一次。这个优化在几十台主机时感觉不明显但在几百台以上时差距非常可观。3.2 漏洞条件与攻击边生成矩阵从“能访问”变成“能攻破”有了网络层可达关系下一步是把漏洞数据叠加进去。这里要区分两类数据主机上装的是什么服务以及这个服务版本有没有已知漏洞。只考虑“主机开着 445 端口”就连接线那生成的不是攻击图是网络拓扑图。攻击边生成的常见逻辑是源主机到目标主机网络可达目标主机存在某个漏洞且源主机具备利用该漏洞所需的条件比如有对应端口的访问权限、有必要的凭据、源主机上已运行了对应的攻击工具。我写了一个简化的规则判断函数from models import Edge, Host def generate_attack_edges(matrix, hosts, vulnerability_db): 遍历网络邻接矩阵为满足漏洞利用条件的主机对生成攻击边。 edges [] for i, src in enumerate(hosts): for j, dst in enumerate(hosts): if matrix[i][j] 0.0 or i j: continue for vuln in dst.vulns: rule vulnerability_db.get(vuln.cve_id) if not rule: continue # 规则里声明了所需源条件例如本地权限、端口可达 if rule.requires_src_access and src.has_service_access(dst.ip, rule.target_port): edges.append(Edge( srcsrc.ip, dstdst.ip, vuln_idvuln.cve_id, scorerule.score, actionrule.action_name )) break # 一条利用链只需一个漏洞入口 return edges这里的 vulnerability_db 是从 CVE 情报整理出来的规则表每条规则包含目标端口、所需源条件、评分和行为动作。break是很重要的一行一台主机即使有五个漏洞攻击者实际也只需要选最合适的一条路径进入都生成边会导致后续路径爆炸。如果你希望表达多漏洞多路径可以在枚举时保留前 N 条高分边而不是全部。实际工程里 vulnerability_db 通常来自本地 JSON 文件结构大概是{CVE-2021-44228: {target_service: log4j, score: 9.8, action: rce}}。规则匹配除了 CVE 编号还经常要匹配服务版本范围所以建议预先把版本字符串转成可比较的版本对象而不是用字符串startswith否则 2.1 和 2.10 的版本比较会让你怀疑人生。3.3 路径枚举用 BFS 而不是 DFS剪枝条件是性能分水岭攻击图路径枚举本质上是在有向图里找“从攻击者起点到目标端点”的所有可能路径。源码里最常见的有两种写法DFS 递归和 BFS 队列。我强烈建议第一版用 BFS因为 DFS 递归在路径深度一高就暴露递归深度限制问题而且容易把一条路径反复扩展成指数级爆炸。下面是一个 BFS 枚举核心代码from collections import deque def enumerate_attack_paths(graph, start_node, target_node, max_depth6): 从 start_node 出发BFS 枚举到 target_node 的攻击路径。 max_depth 用于限制攻击链长度防止路径爆炸。 result [] queue deque() queue.append((start_node, [start_node])) while queue: current, path queue.popleft() if current target_node: result.append(path) continue if len(path) max_depth: continue # 剪枝路径超过深度直接丢弃 for neighbor in graph.successors(current): if neighbor in path: continue # 避免环路 queue.append((neighbor, path [neighbor])) return result这个 BFS 的重点有两个一是if neighbor in path这是环路检测没有它你会看到横向移动图里 A→B→A→B 无限循环二是max_depth剪枝攻击路径一般不会超过五到六跳超过这个深度基本是扫描误报或者规则写得太松保留长路径只会拖慢下游排序和可视化。实际用下来路径枚举在主机数量超过 200 台、边数量上千条时就开始吃力。这时你需要两步优化先在图构建阶段就把不可达节点过滤掉缩小图规模再对路径枚举做“按评分优先扩展”优先走分高的边而不是全量展开。很多开源源码里图难看、跑不动问题多半出在这两个优化没做。3.4 导出 DOT 与 JSON攻击图生成器的“落地手”路径算完必须导出成标准格式才能对接下游。DOT 格式用于 Graphviz 渲染JSON 格式用于上报、入库、前端展示。DOT 的生成看起来简单实际上是最容易出现中文乱码、节点 ID 冲突的地方。def generate_dot(graph_nodes, attack_edges, output_path): 生成攻击图的 DOT 描述文件。 lines [digraph attack_graph {, rankdirLR;] for node in graph_nodes: lines.append(f {node.ip} [label{node.ip}\\n{node.hostname}];) for edge in attack_edges: label edge.vuln_id or edge.action_name lines.append(f {edge.src} - {edge.dst} [label{label}];) lines.append(}) with open(output_path, w, encodingutf-8) as f: f.write(\n.join(lines)) return output_path参数说明rankdirLR表示从左到右布局比默认的自上而下更适合攻击链阅读节点 label 用字典式的双引号包裹防止 IP 里的点号被 DOT 语法误解析文件编码强制 utf-8不然中文标签在 Graphviz 渲染时会变成乱码方块。JSON 导出就简单得多直接把路径列表和边列表序列化即可。这里要提醒一个细节DOT 里的节点 ID 和 label 不要用同一个字段。ID 保持 IP 或内部标识label 才放展示文本。很多人图渲染出来连接线对不上就是因为 ID 里带了空格或特殊字符。4. 把单机脚本吃成工具配置化、规则扩展、可视化闭环4.1 配置化YAML 描述拓扑和漏洞源码里不写死任何主机单机脚本最让人头疼的就是数据写死在代码里。拓扑一变你就要改代码重新跑。我习惯的工程做法是把拓扑、漏洞、攻击者起点全部拆成 YAML 配置文件源码只负责读配置和算路径。下面是 topology.yaml 的一个片段hosts: - ip: 192.168.1.10 hostname: web-server os: ubuntu ports: [80, 443, 22] allowed_cidrs: - 0.0.0.0/0 vulns: - cve_id: CVE-2021-44228 service_version: 2.10 - ip: 192.168.1.20 hostname: db-server os: centos ports: [3306, 22] allowed_cidrs: - 192.168.1.0/24 vulns: - cve_id: CVE-2022-22965 service_version: 5.6 attacker: start_node: 192.168.1.10 target_node: 192.168.1.20 max_depth: 6这套配置的好处是你换一套环境不用动代码只改 YAML。对于经常做内网评估的工程师来说配置化意味着可以沉淀“模板”比如“标准 Web 三层架构”“办公网最小域环境”需要时直接套模板。源码里只要写一个 load_yaml 函数用 pyyaml 读入然后构造 Host 对象列表。有一个容易踩的坑allowed_cidrs 配置太宽比如0.0.0.0/0表示所有主机都能访问它如果所有机器都这么配生成出来的攻击图就变成一个全连通图攻击路径数量爆炸。实际配置时一定要按真实防火墙策略写别偷懒。4.2 规则扩展从 CVE 到攻击动作的映射决定路径可信度光有漏洞列表还不够攻击图生成器源码里需要一套“漏洞规则库”描述某个 CVE 具体能以什么动作、从什么条件被利用。我在 3.2 节提过 vulnerability_db 的 JSON 结构这里展开讲规则设计。一条规则通常会包含五个字段cve_id、target_service、source_condition、action、score。source_condition 是判断源主机需要具备什么前置条件比如“需要本地用户权限”或“需要可访问目标端口”。score 后续用于路径排序。规则库的设计质量直接决定攻击图的实用性。如果规则太宽比如任何漏洞都认为可被任何主机利用生成的全是假路径如果规则太严比如要求攻击者必须已经拿到目标机器上的某个特定文件则会出现漏报。我建议第一版规则库只做“服务匹配 端口可达 评分”把 source_condition 先留成可扩展字段后续需要再加。另外规则库不要和扫描器结果混在同一个文件里。漏洞规则库更新频率高且通常由安全团队维护扫描结果是一次性的快照。源码里两个文件分开加载合并逻辑放在内存里做这样你重扫一次只需要替换数据目录下的文件不用动规则。4.3 可视化闭环DOT 转 SVG用 Graphviz 渲染可信路径攻击图生成器如果没有可视化输出报告会很难写。常见做法是把 DOT 文件交给 Graphviz 渲染成 SVG然后在浏览器里查看。这样不仅保留了路径关系的可读性还能把节点标签做成可点击的锚点。我常用的渲染代码如下import graphviz from pathlib import Path def render_svg(dot_path: str, output_dir: str) - str: 用 Graphviz 将 DOT 文件渲染成 SVGfighting 图可用于报告。 dot_source Path(dot_path).read_text(encodingutf-8) g graphviz.Source(dot_source) svg_path g.render(filenameattack_graph, directoryoutput_dir, formatsvg) return svg_path这个函数最需要注意的是参数filename控制输出文件名directory控制输出目录format指定格式。graphviz.Source.render()会在指定目录生成 SVG 文件同时默认也会生成一个中间 DOT 文件。如果你在自动化流水线中反复调用记得先清理输出目录否则旧文件残留会影响后续使用。实际项目里我还会在 SVG 渲染前对节点做一次分层把攻击起点放在最左列目标主机放最右列中间按路径深度铺开。Graphviz 原生有ranksame机制可以实现分层但需要手工给节点设置 rank 属性。这一步不是必须的但做了之后攻击路径的可读性会提升一个档次汇报时领导扫一眼就能看出攻击链路顺序。5. 自动化攻击图生成器的避坑与排查5 条一线翻车记录5.1 CIDR 解析翻车IPv4/IPv6 混用导致网络矩阵全是 0现象构建出来的邻接矩阵全是 0任何两个主机之间都没有边攻击图空无一物。排查半天发现配置文件里有几台主机的 allowed_cidrs 写的是 IPv6 地址而主机 IP 是 IPv4。原因ipaddress.ip_network()解析 IPv6 网段后拿 IPv4 地址去判断包含关系Python 直接抛 TypeError但如果你在异常处理里把它吞了就会表现为“不可达”。解决在is_in_subnet里先判断地址族一致再比较不一致直接返回 False。顺带在设计配置模板时强制要求同一份拓扑文件里要么全 IPv4、要么全 IPv6别混写。这种事看起来低级但真实环境里 CMDB 导出的数据常常就是这么脏。5.2 递归 DFS 炸栈攻击链 20 层直接 RecursionError现象路径枚举在拓扑规模不大时正常一旦某台主机存在大量全连通边递归深度超过 Python 默认的 1000 层直接抛 RecursionError程序闪退。原因DFS 递归实现没有深度限制路径长度不受控。攻击图里一台域控可能和几百台机器有信任关系递归展开会越来越深。解决改用 BFS 队列实现路径枚举见 3.3 节代码同时加上 max_depth 剪枝。如果实在要用递归就在sys.setrecursionlimit上设置 5000但这只是拖延不是根治。我这里统一建议源码里出现递归版本的时候直接替换成 BFS 版从根上避免这一类崩溃。5.3 DOT 中文乱码渲染出来的节点标签全是方框现象DOT 文件在文本编辑器里看是正常中文Graphviz 渲染成 PNG/SVG 后变成方块或问号。原因Graphviz 渲染时找不到支持中文的字体或者 DOT 文件没声明字体名。中文字体问题在 Linux 服务器上尤其明显通常只装了 sans-serif 默认字集。解决生成 DOT 时在 digraph 声明里加fontnameMicrosoft YaHei或者服务器上实际存在的中文字体名比如WenQuanYi Micro Hei。同时确保文件以 UTF-8 编码写入。如果你在 Windows 本机跑没问题部署到 Linux 就乱码优先检查系统字体而不是怀疑代码。5.4 路径爆炸全连通图弱规则导致结果文件几个 GB现象一台主机配置了0.0.0.0/0漏洞规则又没判断源条件BFS 展开后路径数量指数增长生成的结果文件几个 GB程序直接卡死。原因攻击图本身是稠密图的时候路径枚举不做剪枝或者剪枝条件太弱复杂度爆炸。很多入门项目在这里死掉不是算法写错是数据没约束。解决三管齐下。第一在配置阶段严格写 allowed_cidrs别用全通配第二路径枚举的 max_depth 设到 5 或 6第三在 BFS 里加“单条路径上每个节点只访问一次”的环路检测同时全局限制总路径条数比如 5000 条就停止扩展。前两个是业务约束第三个是保底措施。5.5 版本比较玄学服务版本“2.10”和“2.9”谁更大现象规则库里写的是log4j:2.10 受影响扫描报告里服务版本是2.9。字符串比较下2.10 2.9成立但按语义 2.10 比 2.9 新规则匹配完全颠倒。原因用字符串直接比较版本号。Python 里2.10和2.9按字典序比较2.1排前面所以 2.10 被当成比 2.9 旧。解决用 packaging 库里的packaging.version.parse()把版本字符串解析成版本对象再比较。这个库是 pip 安装的几乎所有 Python 环境都带。不要自己写拆分split(.)再逐段比较的代码边界情况太多了省这点依赖不值得。6. 把攻击图从“画得出来”变成“跑得可信”验证方法、模块化进化与一条实战习惯攻击图生成器最容易被诟病的是“明明画出了路径但这条路径真实攻击者根本走不通”。所以源码写完之后最重要的一步不是美化图而是验证图的可信度。我习惯用三个指标来检验覆盖度、误报率、路径平均深度。覆盖度指的是图里是否包含了所有已知真实攻击路径误报率指的是生成的路径中有多少在真实环境里走不通路径平均深度用来观察规则是不是太松或太紧。验证做法很简单找一组已知结果的环境比如过去一次攻防演练中确认过的 5 条攻击链把这些链手工写在测试用例里然后跑生成器看它能否覆盖这些路径。这一步看起来土却能把规则配置里的问题全部暴露出来。我在自己的项目里专门建了一个test_known_paths.py每次改规则库就跑一遍任何回归都能立刻发现。接着是模块化进化方向。第一版源码把 parser、algorithm、report 全放在几个文件里已经够了但当你要接入实时数据、多租户隔离、或者做成 Web 服务时就要把核心逻辑抽成独立的库再包一层 API。我建议保留一个稳定的接口输入是 topology vulnerabilities attacker config输出是 paths DOT JSON。只要这个接口不变内部怎么重构都安全。最后说一条实战习惯也是我踩过坑之后才养成的攻击图生成器永远只做决策辅助不做自动执行依据。哪怕图里显示 A 能打穿到 B真实攻击前也要手工验证一次目标端口、服务版本和漏洞影响范围。生成器帮你缩小排查范围但代替不了验证。这个习惯帮我挡掉过不少假路径带来的误判希望能帮到你。本文还有配套的精品资源点击获取
返回列表