基于Python的攻击图生成器:自动构建攻击路径的可视化分析
发布时间:2026/10/1 4:28:13来源:尧图网络
简介基于Python的attack-graph-generator是一套面向安全分析师的自动化攻击图生成源码项目帮助快速梳理漏洞与攻击路径降低攻击图构建门槛适用于渗透测试、风险评估与安全培训等场景。压缩包共101个文件大小37.75MB包含44个JSON配置、YAML环境定义、DOT拓扑描述、PDF报告、Python源码及字节码、Shell脚本等JSON/YAML负责参数与场景配置DOT用于图形化输出Pyc则体现编译优化环节格式分工明确。已有321人学习下载适合希望以工程化方式实践攻击图自动生成的Python开发者与安全入门人员。项目中还包含Git配置、说明文档等辅助文件源码目录结构清晰可帮助读者理解配置文件编写、图形生成与结果导出流程并在此基础上二次开发或开展安全分析实验覆盖从输入配置到可视化输出的一整套实现思路。1. 安全评审里最怕的一句话攻击者到底怎么进来的做红队评估或者防御验证时最常被领导追问的场景就是内网里有几百台主机、几十条漏洞攻击者到底会挑哪条路打手工画攻击路径图画到第三层就乱套了。这时候基于 Python 的 attack-graph-generator 自动化攻击图生成器源码就能派上用场——它把主机资产、漏洞信息、网络可达性作为输入自动生成从攻击起点到核心资产的可能路径。这套源码解决的核心问题不是“某个漏洞能不能打”而是“一组漏洞连起来能打到哪里”。适合正在做渗透测试报告、安全防御体系建设、红队自动化评估的从业者也适合想把图分析能力合入自己安全编排平台的开发。它不替代漏洞扫描器而是把扫描结果变成攻击者的视角地图。本文会从建模原理、运行步骤、核心模块改法一路拆到验证方法确保你能在本地把项目跑起来并且能按自己的网络环境改输入数据。2. 攻击图的建模逻辑为什么用图而不是一张漏洞清单2.1 攻击图与漏洞清单的本质区别漏洞扫描器给出的是一份清单哪个 IP 有哪个 CVE、风险等级是多少。这份清单没法直接回答“攻击者先打哪台、再打哪台、最后落脚在哪”的问题。攻击图的思路是把攻击过程建模成状态转换一个节点表示一台主机处于某个状态例如“未授权访问”或“已被攻陷”一条边表示一次攻击动作例如“利用某个漏洞从主机 A 跳转到主机 B”。从初始节点出发沿边遍历最终能到达的所有状态就是攻击者可能的影响面。我用过几种建模方式最常被采用的是“属性攻击图”每个节点除了主机 ID还附带一组布尔属性比如“可达”“有漏洞”“有高权限”。边的生成依赖攻击规则库。例如规则“如果主机 A 可达且存在 CVE-2023-1234 漏洞则 A 可以被攻陷”当输入数据满足条件时这条规则被实例化图上就多了一条边。这个建模方式的好处是规则和网络数据分离换一个环境只需要换数据文件不需要重写代码。对比之下攻击树更适合描述单次攻击的细化步骤攻击链则偏向时间序列描述而攻击图在表达多个攻击者路径的并行关系时最自然。为了方便边计算源码里通常会用一个状态向量存放节点属性。我一般参考下面的映射关系来理解图结构图里的节点对应一台主机边的来源节点表示攻击前状态边的目标节点表示攻击后状态边上的标签是漏洞编号或攻击动作类型边上的权重是攻击难度评分。把这张表记在脑子里读源码的时候就不会迷路。2.2 用 Python 表示图数据结构怎么选attack-graph-generator 源码里核心的数据结构无非三种字典嵌套、自定义类、networkx 图对象。直接基于字典做状态遍历写起来最快但后续做路径枚举、连通分量分析时会重复造轮子。我一般会在原型阶段用字典正式改造时换到 networkx因为它的 DiGraph 类型天然支持有向图、节点属性、边属性路径枚举算法也齐全。用一个极简样例说明建图过程。假设内网有三台主机入口机 web、内网中间机 app、核心数据库 db。若 web 对192.168.1.0/24网段开放 SSH 端口且 app 开放 8080 端口则攻击路径会这样生成攻击者先攻陷 web然后访问 app再尝试跳到 db。这一段给出一份输入样例和一段快速建图代码方便对源码结构建立直觉。输入样例主机清单与漏洞数据合并视图{ hosts: [ {id: web, ip: 192.168.1.10, services: [ssh], vulns: [CVE-2021-1234]}, {id: app, ip: 192.168.1.11, services: [http], vulns: [CVE-2022-4567]}, {id: db, ip: 192.168.1.12, services: [mysql], vulns: []} ], reachability: [ {src: attacker, dst: web, ports: [22]}, {src: web, dst: app, ports: [8080]}, {src: app, dst: db, ports: [3306]} ] }import networkx as nx def build_attack_graph(hosts, reachability): 根据主机与可达性数据构造攻击图。 hosts: 主机列表每个元素含 id/services/vulns reachability: 网络可达列表每个元素含 src/dst/ports 返回: 一个 networkx.DiGraph 实例 G nx.DiGraph() # 1. 把所有主机作为节点加入图节点属性带上服务与漏洞信息 for h in hosts: G.add_node(h[id], servicesh[services], vulnsh[vulns]) # 2. 可达关系直接作为边的基础条件也可后续做规则校验 for r in reachability: G.add_edge(r[src], r[dst], portsr[ports], risk0.5) return G # 调用示意 if __name__ __main__: data { hosts: [ {id: web, services: [ssh], vulns: [CVE-2021-1234]}, {id: app, services: [http], vulns: [CVE-2022-4567]}, {id: db, services: [mysql], vulns: []} ], reachability: [ {src: attacker, dst: web, ports: [22]}, {src: web, dst: app, ports: [8080]}, {src: app, dst: db, ports: [3306]} ] } graph build_attack_graph(data[hosts], data[reachability]) print(graph.edges(dataTrue))这段代码的逻辑是基于“可达即潜在攻击面”的粗粒度建模真正的 attack-graph-generator 会在加边前做更细的规则校验比如检查目标端口是否与漏洞影响的服务吻合、是否存在前置权限要求。关键参数中最重要的是reachability里的ports字段——很多使用者在导入数据时漏了端口导致边永远加不上。另外注意节点属性里塞了vulns列表后续路径枚举时可以直接靠这条属性做过滤只展示“利用了至少一个漏洞”的边减少非攻击型可达边带来的噪声。2.3 规则引擎与攻击动作的定义攻击图自动化生成的核心是规则引擎。一条规则通常包含三部分前置条件攻击者当前拥有的权限级别、目标主机上开放的端口、动作描述利用某个 CVE、暴力破解、绕过认证、后置效果获得目标主机的用户权限或 root 权限。源码里规则一般存成独立文件例如rules.json运行时加载进内存。{ rules: [ { id: RULE_001, name: SSH弱口令爆破, pre_conditions: { source_compromised: true, target_port_open: [22] }, action: { type: brute_force, cve: null }, post_effects: { target_compromised: true, privilege: user } } ] }这一段规则的含义是当源主机已被攻陷一个节点成为攻击跳板且目标主机的 22 端口开放则一次 SSH 弱口令爆破动作可以导致目标主机失去用户权限层面的安全性。在源码的执行流程中规则被循环应用到每一对“已攻陷节点 可达目标节点”上只要前置条件全部满足就在图上增加一条带规则 ID 的边。参数中最重要的变化点是privilege的值——在横向移动场景里权限级别决定攻击能否继续推进如果只拿到user权限就停了那这条路径不会继续扩展。3. 把 attack-graph-generator 至少跑通环境、目录与最小命令3.1 环境准备Python 版本与依赖安装在开始运行源码之前先确认环境。attack-graph-generator 类项目大多基于 Python 3.8 以上开发依赖项集中在requirements.txt里核心包包括networkx、jsonschema、python-dateutil。另外可视化部分通常依赖pygraphviz或pydot这两者在 Windows 上安装经常遇到缺 Graphviz 可执行文件的问题。我推荐的安装顺序是先建虚拟环境再装依赖。用 venv 而不是全局安装原因是这类源码项目依赖版本较敏感——尤其 networkx 从 2.x 升到 3.x 时部分 API 名称有调整直接全局装容易让其他项目的依赖坏掉。python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate pip install --upgrade pip pip install -r requirements.txt说明venv是 Python 自带的虚拟环境模块激活后后续命令都运行在隔离环境里。如果要跑 Graphviz 可视化还需要去下载安装 Graphviz 本体并配置路径这一步不是 pip 能替代的。多数启动异常都源于这一步没做——具体排错在避坑章节展开。装完依赖后可以用一条快速验证命令确认核心包版本python -c import networkx; print(networkx.__version__)3.2 目录结构与输入数据格式源码的典型目录结构与我常用的项目结构一致attackgraph/存放核心包rules/存放规则文件examples/存放示例数据output/存放生成的结果文件。读源码先读attackgraph/main.py它会串联“加载数据 → 校验数据 → 构建图 → 计算路径”四步。输入格式方面项目多数支持 YAML 或 JSON。JSON 格式更通用也更容易与漏洞扫描报告对接。核心字段一般包括hosts、reachability、rules_file、attack_start。注意attack_start是路径搜索的根节点这个字段缺失时程序通常会在日志里报错退出。最小运行命令一般长这样python -m attackgraph examples/lan.json -o output/lan_result.json --format json这里-m attackgraph表示把项目当作模块运行examples/lan.json是输入网络数据-o指定输出文件--format控制结果格式。运行后输出目录里会多出结果文件包含所有节点、边、路径列表与每条路径的风险评分。若可视化相关依赖已装好还可以加--draw参数直接导出图片。3.3 第一次运行后该看什么第一次跑通不要急着看完整报告先看三样东西日志前 20 行是否有 warning、输出文件里的节点数量是否与输入主机数量一致、路径列表的起点是否正确指向attack_start指定的节点。日志里的 warning 大多来自规则文件中引用的漏洞 CVE 在输入主机漏洞列表中不存在——这类 warning 不是说程序跑不动而是提醒你攻击边可能比预期少。输出文件中的节点数量如果少于输入主机数量大概率是 JSON 里某台主机缺少 ID 字段被校验逻辑跳过了。路径列表中如果起点不对去检查attack_start字段它必须匹配某个主机 ID。这里给一份运行时参数速查表方便后续调参参数作用常见取值--format输出格式json、dot、yaml--max-depth限制攻击路径最大深度防止图爆炸3、5、10--prune是否剪枝弱关联边true、false--rule-filter只使用指定规则集规则文件 ID--draw输出可视化图文件—--config指定配置文件路径config.yaml在 2.8 版本的networkx与新版 3.x 混用时部分旧写法比如G.node会报错源码若基于 2.x 开发需要微调。这是该方向最常见的翻车现场之一不要慌后续避坑章节详细给方案。4. 改造源码核心模块种子节点、扩展规则与路径输出4.1 种子节点定义种子节点是指攻击者在图中起始所在节点集合。默认情况下attack-graph-generator 以外部攻击者为唯一起点。现实网络里内部威胁或失陷终端也常作为起点。修改源码时常见的做法是在配置文件中增加一个seed_nodes列表然后修改建图逻辑让这些种子节点在初始状态就被标记为“已攻陷”。给出一段改法参考def add_initial_compromised_nodes(graph, seed_nodes): 将种子节点标记为已攻陷。 graph: DiGraph seed_nodes: 节点 ID 列表例如 [web, office-pc-01] for node_id in seed_nodes: if node_id in graph.nodes: graph.nodes[node_id][compromised] True graph.nodes[node_id][compromised_at] 0 # 0 表示初始时刻这段代码的关键在于compromised_at属性被下游路径枚举逻辑读取。如果把种子节点直接标成已攻陷而不赋值这个属性部分路径计数逻辑会因 KeyError 崩溃。参数上要注意seed_nodes必须与主机 ID 完全一致大小写敏感空格也会导致匹配失败。4.2 扩展规则加载器增加自定义攻击检测源码的规则引擎一般提供默认加载器从rules/目录读 JSON 文件。自定义规则时一个容易踩的坑是多人协作时规则格式不统一。为了避免这个问题我会在加载器里加一层格式校验强制要求每条规则包含id、pre_conditions、post_effects三个字段。给出一段在源码基础上加校验的代码import json import jsonschema RULE_SCHEMA { type: object, required: [id, pre_conditions, post_effects], properties: { id: {type: string}, pre_conditions: {type: object}, post_effects: {type: object} } } def load_rules(rules_path): 从规则文件加载并校验攻击规则。 rules_path: 规则 JSON 文件路径 返回: 规则字典列表 with open(rules_path, r, encodingutf-8) as f: rules json.load(f) validated [] for rule in rules: try: jsonschema.validate(rule, RULE_SCHEMA) validated.append(rule) except jsonschema.ValidationError as e: print(f规则 {rule.get(id, unknown)} 校验失败: {e.message}) return validated逻辑说明部分jsonschema.validate在字段缺失时抛异常这里捕获后只打印日志不中断整体加载——但最终规则次数会变少所以日志要留痕。参数上需要知道rules_path指向的 JSON 顶层必须是一个数组如果规则文件被编辑器写成了对象加载后rules不是列表for rule in rules会直接失败。这类问题经常导致启动时报错但日志不明显。4.3 路径枚举与深度限制源码默认用 DFS深度优先搜索枚举攻击路径。网络规模小的时候没问题一旦主机数量超过 200路径数量会爆炸式增长。源码给了--max-depth和--prune两个开关其中--prune的实现一般基于边权重阈值。给出一段枚举路径并限制深度的参考实现def enumerate_paths(graph, start, max_depth5): 从 start 出发枚举所有攻击路径路径深度不超过 max_depth。 graph: DiGraph start: 起始节点 ID 返回: 路径列表每条路径是节点 ID 列表 paths [] def dfs(current, path, depth): if depth max_depth: return # 记录当前路径快照 paths.append(list(path)) for neighbor in graph.successors(current): if neighbor not in path: # 避免环路 path.append(neighbor) dfs(neighbor, path, depth 1) path.pop() dfs(start, [start], 1) return paths这段逻辑的关键successors在有向图中表示沿边方向可达的下一个节点集合neighbor not in path避免环路上的无限递归。参数上max_depth直接影响枚举数量与耗时深度每加一路径数可能翻倍。实际使用中我一般把上限设为 5超过 5 的路径在告警分析里已经很难人工读完了。把这段替换源码中原来的递归实现时注意原实现可能返回的是带边信息的对象而不是纯节点列表——下游输出代码若引用了路径上的边标签需要同步修改。5. 常见问题与避坑记录跑不起来、边偏少、图爆炸5.1 Graphviz 找不到可执行文件导致画图失败现象运行--draw参数时程序报错Executable not found或者pydot提示无法调用dot命令。原因pygraphviz/pydot只是 Python 包装库真正干活的 Graphviz 软件没装或者装了但不在系统 PATH 中。解决安装 Graphviz 本体Windows 选安装包Linux 用apt install graphviz或yum install graphviz装完在终端执行dot -V能输出版本号才算成功。Windows 下装完后需要重开终端否则 PATH 不刷新。这个坑几乎是每个用 Graphviz 的人都会遇到的不是个例。5.2 networkx 版本差异导致G.node属性访问报错现象源码跑到一半报AttributeError: DiGraph object has no attribute node。原因networkx 2.x 时代用G.node访问节点属性3.x 改为G.nodes。源码如果基于 2.x 编写装依赖时又被解析到 3.x就会触发。解决的办法有两种。推荐直接改代码全局搜索.node[替换成.nodes[。不推荐锁定 networkx 2.x 版本因为后续可能与其他库冲突。这类问题在旧源码项目里太常见属于“源码本身没变但环境变了”的典型翻车。5.3 生成的攻击路径比预期少很多现象明明扫描出了几十个漏洞生成的攻击路径只有三五条。原因多数情况不是源码逻辑坏了而是输入数据的“可达性”字段没有覆盖所有场景。我之前排查过一个案例防火墙规则允许 web 访问 app 的 8080 端口但输入数据里ports写成80规则引擎判断条件不满足这条边被丢弃。解决对照真实网络配置逐条核对reachability中的源、目标、端口三元组另外检查规则文件的漏洞编号是否与主机漏洞列表完全一致CVE 编号大小写不匹配也会导致规则不生效。建议加一行断言调试把被跳过的边打印出来看# 在规则应用逻辑处加临时调试代码 if rule_condition_matched: graph.add_edge(...) else: print(f规则 {rule[id]} 未匹配: {host[id]}-{target[id]})5.4 图规模太大导致内存溢出或运行时间过长现象主机超过 500 台时程序长时间不结束或者内存占用飙升到数 GB。原因路径枚举的复杂度是指数级的数据量大时全量枚举不可行。解决必须启用--max-depth并把值调小到 3 或 4开启--prune并按边权重过滤低风险路径再不行就给规则库瘦身把不属于当前场景的规则文件移出rules/目录。这个方向的项目普遍有这个临界点不是源码缺陷是计算模型的固有代价。5.5 输出 JSON 文件里节点 ID 乱码或中文变问号现象主机名如果是中文生成的 JSON 或 DOT 文件里显示为乱码。原因输出文件时没有指定 UTF-8 编码Windows 默认编码非 UTF-8。解决在打开输出文件的代码中强制指定encodingutf-8并在 JSON dump 时设置ensure_asciiFalse。给出一段修复示意with open(output_path, w, encodingutf-8) as f: json.dump(result, f, ensure_asciiFalse, indent2)6. 验证攻击图是否可靠断言逻辑与结果核对方法生成攻击图只是第一步关键是如何确认生成的图是可信的。我的习惯是构造一个小型但答案明确的测试环境用已知答案验证源码输出。先准备一个三主机网络攻击者从外部可达 webweb 可以访问 appapp 可以访问 db并且 web 上存在一个可以被利用的漏洞预期结果是生成一条attacker → web → app → db的完整路径。运行工具后检查输出路径列表是否包含这条链路。具体验证脚本可以这样写读取生成的 JSON 结果的路径列表对每条路径做断言路径首节点必须是attacker必须经过web最终节点必须是db与否取决于是否允许非完全成功路径。给出一段参考代码import json def verify_paths(result_file): 验证生成的攻击路径是否符合预期。 result_file: 前面输出的 JSON 结果文件路径 返回: 布尔值全部断言通过才为真 with open(result_file, r, encodingutf-8) as f: data json.load(f) for path in data[paths]: assert path[0] attacker, f路径起点错误: {path} assert web in path, f路径缺少 web: {path} print(f验证通过共 {len(data[paths])} 条路径) return True if __name__ __main__: verify_paths(output/lan_result.json)代码逻辑很简单攻击图生成器如果正确读取了输入数据路径列表就应该严格满足起点与必经节点的约束。这里注意断言“最终节点必须是 db”不宜放在所有路径上因为攻击者可能在中途换路线停止推进——这在实际攻击中也是合理的。所以我把最终节点断言放在一个更小的范围内这里只验证了起点必经节点更细的落点可以在你的实际环境里调整。除此之外还有两个验证技巧。第一个是路径条数合理性判断如果三主机网络只生成了一条路径说明规则匹配过于严格或可达性数据不全如果生成上百条重复路径说明环路剪枝不彻底需要检查是否漏了路径去重逻辑。第二个是边权重检查攻击路径上的边权重应该与漏洞评分正相关拿着高权重 CVE 的边若出现在低风险路径中需要核对规则的post_effects是否设置正确。收个尾我自己的习惯是每次跑完生成结果之后必做一次“最小网络反向验证”删掉某个关键漏洞重新生成确认攻击图中确实少了对应的边。这个动作能发现输入数据映射上的隐性错误。另一个习惯是生成 DOT 文件后不急着看渲染图直接用文本搜索检查边方向有没有反向或者缺失。希望这些核对方法帮到你——把攻击图从“看起来有用”变成“真正能被业务方信任”功夫都在生成之后这一步。本文还有配套的精品资源点击获取
网站建设高端定制企业官网