先说结论:这个项目方向我很喜欢,但也得先泼一盆冷水——强化学习跑内网渗透,绝不是装个环境、丢个DQN进去就能自动打穿靶场的爽文剧本。它本质上是把"攻击路径探索"这个安全攻防里的经典难题,转译成一个马尔可夫决策过程,然后交给Agent在模拟环境里反复试错。这个转译过程的价值,远远大于训练出一个稳定Agent本身。整篇文章我会从环境搭建、接口封装、奖励设计、实测踩坑、落地边界这几个维度展开,把我在这个方向上实际趟过的路和踩过的坑都摊开讲。
如果你跟我一样,既懂点渗透测试,又对强化学习感兴趣,想试试"用AI自动找攻击路径",这篇文章应该能帮你省掉大量试错成本。
1. 为什么攻击路径探索是"组合爆炸",而强化学习恰好能干这个活
1.1 传统渗透测试的瓶颈
内网渗透最耗人力的是什么?不是某个漏洞利用有多难,而是"下一刀往哪切"的决策过程。一个中型内网,假设有几十台主机,每台主机上可能开着好几个服务,服务版本对应若干潜在漏洞,主机之间还有网络分段、信任关系、域环境这些相互纠缠的路径——把所有可能走通的路线枚举出来,数量级是惊人的。
我自己的体会是,在一个攻防演练项目里,最熟练的红队人员也需要大量时间去画一张"主机-服务-权限"的关系图,然后在脑子里模拟"从A机器拿到权限,能不能跳到B网段,B上有没有能提权的点"。这一步非常依赖经验,而且很容易漏。漏掉一条路径,可能整个项目就白干。
传统的自动化解法,其实是规则引擎。把攻击方法写成一个个Playbook或攻击链模板,按顺序跑。它能解决"已知路径的自动化重复",但它有一个死穴:模板没有覆盖到的路径,永远发现不了。真实内网千奇百怪,你没法预先把所有路径都写成规则。这也是为什么攻防对抗这么多年,主力一直是"人+工具",而不是"纯工具"。
1.2 强化学习为什么天然契合路径探索
强化学习解决的不是"已知路径怎么更快执行",而是"未知路径怎么被自动发现"。它跟监督学习最大的区别在于,不需要标注数据,只要给一个明确的奖励信号,然后让Agent自己去试,去从失败里学习。
放到攻击路径这个场景里,"奖励信号"非常自然:发现了新主机,给个正奖励;拿到了新的权限,给个大奖励;最终拿到核心目标数据,给最终大奖。整个探索过程不需要人告诉Agent"第一步先扫端口,第二步再用某个CVE",它自己在试错中就会统计出"哪些动作组合能带来更多奖励",从而逐步形成攻击策略。
这个逻辑跟人类渗透测试人员的学习过程是一样的——打的靶场多了,自然知道"扫到开放445端口的老Windows,很可能有永恒之蓝类漏洞"。RL只是把这个经验积累过程加速并且显性化了。Metasploit-Gym这类项目,本质就是把MSF这个"武器库"封装成一个Agent可以反复调用的环境,让学习过程有地方可以发生。
2. Metasploit-Gym的环境架构:把MSF包装成一个可交互的训练场
2.1 靶场拓扑与漏洞预设:环境该怎么搭
要训练Agent,首先得有一个内部靶场环境。我见过很多初学者一上来就在本地跑了一套比较复杂的域环境,结果Agent训练了几天都没法收敛,问题就出在环境规模上。
我的做法是先准备一个小型目标网络,拓扑上保持简单:一个入口网段,里面放一台对外提供Web服务的主机,这台机器上有一个可以利用的远程代码执行漏洞;内网段放一台Windows主机,开放文件共享服务,存在一个经典的权限提升漏洞;再往里放一台Linux主机,作为最终目标,上面维护着一段类似"核心数据文件"的东西。
这个拓扑已经是相对可控的规模了,但即便如此,Agent第一个月跑下来也不稳定。所以如果你只是刚开始接触,建议先把环境缩到最小:一台入口Web主机加一台内网主机,Agent只需要学会"利用Web漏洞拿权限,再横向移动到内网主机"这两步。等这条路稳定跑通,再逐步扩大网络规模。
靶场环境的"可重置性"非常重要,这一点容易忽略。因为Agent每一轮训练,都会把环境状态改变掉——文件被读走、服务被改、进程被杀。如果没有快照回滚机制,训练到第二十轮的时候,环境就已经脏得没法复现了。我在本地虚拟化环境里给每台靶机都做了快照,Agent的每个训练回合结束时,强制回滚到初始快照。这样每一轮开始,都是一个干净的、确定性的初始状态。这条经验和"多智能体"没关系,是RL训练环境最底层的要求。
2.2 状态、动作与奖励的切分逻辑
RL环境设计的核心三件套:状态空间(Observation)、动作空间(Action Space)、奖励函数(Reward)。这里我踩了很多坑,一个个说清楚。
状态空间:要让Agent知道"我当前在什么处境"。我把每台主机的状态归纳为一组特征向量:主机是否可达、已发现的开放端口列表、已识别的服务类型、当前是否拥有该主机权限、权限级别是普通用户还是管理员、已发现但未利用的漏洞编号。把所有主机特征拼接起来,就是Agent观察到的环境状态。
这里必须强调的是,"部分可观测"本质上是这个场景的默认属性。Agent一开始并不知道内网主机存在,直到通过扫描动作发现它。所以状态里要加一个mask机制,未发现的主机不参与后续动作选择。如果一开始就把整个网络拓扑全塞给Agent,这相当于给了Agent全知视角,训练出来的策略在真实场景里直接失效。
动作空间:这是最恼人的部分。Metasploit本身是一个庞大的模块集合,每个模块还有复杂的参数。你不能把所有模块都丢给Agent,否则动作空间太大,训练基本没法收敛。我最后做的是把动作抽象成几个大类:扫描子网、利用指定主机上的指定漏洞、尝试提升当前权限、尝试横向移动到目标主机、读取目标数据做最终状态判断。
每个大类动作的参数,也做成了离散的下拉式选项。比如"利用漏洞"这个动作,Agent需要依次选择"目标主机编号"和"漏洞编号",每个选择对应一个离散化的子动作。这样做的好处是,Agent不会在训练初期去做大量无效的模块参数枚举,而是在我们人为划定的动作边界内做策略搜索。等后面Agent学会了"该用哪个漏洞",再去放开参数维度。
奖励信号:奖励设计的核心原则是"既要引导,又不能给Agent作弊空间"。我的方案分几档:发现一台新主机,加分;成功执行一个利用动作并获得新的会话,加分;横向移动到新网段或新主机,加大分;权限提升成功,再加大分;最终获得目标文件是一个大额奖励。同时,每个时间步有一个微小的负奖励,用来鼓励Agent少做无用的动作,尽快找到路径。如果某个动作执行失败,也会给一个小的负奖励。
实际跑下来你会发现,"发现新主机加分"这个设计很容易被Agent钻空子。它可能会反复对一个已经扫描过的主机执行扫描动作来刷分。解决方案我在第四节里细讲,这里先说结论:所有奖励都必须基于"环境状态是否发生新变化"来判断,而不是基于"Agent是否执行了某个动作"。这一点是奖励塑形里最重要的底线。
2.3 Gym接口与MSF RPC联动的实现要点
Metasploit本身提供了XML-RPC接口,也就是msfrpcd,Python端可以通过这个接口远程调用MSF的模块。Gym/Gymnasium环境类的核心方法就两个:reset()(重置环境)和step(action)(执行动作并返回观测、奖励、终止标志)。
我实现的伪代码长这样:
class MsfGymEnv(gym.Env): def __init__(self, rpc_host, rpc_port, target_config): super().__init__() self.rpc = MsfRpcClient(rpc_host, rpc_port) self.action_space = spaces.Discrete(N_ACTIONS) self.observation_space = spaces.Box( low=0, high=1, shape=(MAX_HOSTS * HOST_FEATURES,) ) self.hosts = load_target_config(target_config) self.reset() def reset(self): # 1. 恢复所有靶机快照 # 2. 清空MSF的会话列表和数据库记录 # 3. 返回初始观测 return self._get_observation() def step(self, action): # 1. 将离散动作解析成(动作类型, 目标主机, 漏洞/模块编号) # 2. 通过RPC执行MSF模块或扫描命令 # 3. 解析执行结果,判断会话是否建立、权限是否提升 # 4. 更新状态,计算奖励 # 5. 判断是否达成终止条件 return obs, reward, terminated, {}原生的Gym接口在消息格式上比较老,如果你用的Python版本较新,更推荐直接用Gymnasium。差别不大,但Gymnasium的API更整洁,也更便于后续集成稳定版本的PPO实现。
另一个容易踩的坑是MSF RPC的会话处理。RPC接口返回的是JSON结构,但成功建立会话后,会话ID是异步出现的,往往要等几秒。我在封装step()的时候,必须轮询RPC端"当前是否有新的会话建立",而不是假设执行利用动作后立刻就能拿到会话。如果轮询超时,就判定这个利用动作失败了。这个超时时间我设的是15秒,太短会导致慢速漏洞利用被误判失败,太长又会拖慢训练速度。
3. 跑通Agent的完整过程:从DQN到PPO的取舍与调参记录
3.1 第一版DQN为什么训不动
我最开始用的是DQN,因为当时想着动作空间是离散的,DQN在这类问题上应该够用。结果跑了很久,训练曲线基本没有上升趋势。复盘下来有四个原因。
第一是环境执行太慢。MSF的扫描和利用动作不是毫秒级返回的,一个动作往往要等好几秒甚至十几秒。一个回合几十个动作,跑完就是十几分钟。DQN在这种样本采集效率下,等于是乌龟学跑步,每一轮学不到多少信息。
第二是奖励稀疏。虽然我做了分级奖励,但"发现新主机"这类奖励实际上并不多,Agent大部分时间都在扫描超时、利用失败和重复动作的负奖励里打转。DQN的Q值更新在这种稀疏奖励下方差非常大。
第三是环境的随机性。漏洞利用存在概率性成功,网络延迟也不稳定,DQN这种基于单步值函数的方法,很容易被不稳定的环境干扰,学到错误的Q值。
第四是隐形的时间差分误差。DQN用目标网络加经验池做训练,但这套设计更适合状态转移相对确定的环境。渗透环境的转移随机性太大,value function的估计很难收敛到稳定值。
3.2 PPO接手的改动点
后来我换成PPO,训练曲线才慢慢有了起色。PPO是policy gradient类算法,直接优化策略本身,天然能处理部分可观测和随机性较大的环境。它不是用Q值选动作,而是给每个动作一个概率分布,然后根据回报好坏调整概率。在MSF环境这种"偶尔成功一次、偶尔失败一次"的场景下,PPO对随机性的容忍度明显比DQN高。
切换到PPO之后,有几个改动点非常关键。
第一个改动是"回合截断"。PPO是on-policy的,它对一批旧数据只更新几轮就会扔掉。如果每批数据生成要等好几个小时,训练效率还是不行。我的做法是把每个训练回合设为固定的最大步数,比如40步,超过这个步数就直接返回终止标志。这样一回合数据生成时间可控,PPO也能定期拿到一个明确的终止信号来更新价值网络。
第二个改动是并行环境。训练收敛慢,很大的原因是环境生成样本的速度慢。我搭了四个并行的MSF环境——就是说同时跑四套独立的靶场实例,每个环境各自跑一个Agent副本,采集完轨迹后统一交给PPO更新。并行度提上去之后,样本量才够PPO学习。这一步值得提一下:如果你的机器资源允许,并行环境的数量直接决定你能不能在这个问题上看到训练有效果。
第三个改动是裁剪掉无用动作。实验里我做了一个动作mask,把所有"当前状态下不可行的动作"概率直接置为-Inf。比如Agent还没有获取任何会话,就禁止选择"提升权限"和"横向移动"动作。这个mask看着简单,其实作用巨大。它相当于把无意义的搜索空间提前帮Agent排除掉了,训练初期收敛速度肉眼可见地变快。
3.3 奖励塑形的细节补充
奖励塑形是RL落地中最不讲道理但最有用的环节。我最终采用的奖励配置如下:
| 事件 | 奖励 | 说明 |
|---|---|---|
| 发现一台从未见过的新主机 | +2 | 必须依赖状态去重,防止反复扫描刷分 |
| 成功建立一个新的高权限会话 | +10 | 中级奖励,鼓励Agent真正拿到shell |
| 会话权限提升成功(普通->管理员) | +15 | 对应提权动作 |
| 横向移动到未访问过的主机 | +30 | 跨主机的动作是路径突破的关键点 |
| 最终读取目标数据文件 | +100 | 完成整个攻击链的终点 |
| 动作执行失败 | -1 | 小惩罚,鼓励少做无效尝试 |
| 每走一个时间步 | -0.05 | 鼓励尽量缩短攻击路径长度 |
这套配置跑下来,Agent基本能稳定学到"扫描入口Web主机 → 利用Web漏洞拿初始权限 → 提升权限 → 横向到内网主机 → 读取目标数据"这条链路。但说实话,学到的路径跟我们人工设计的理想链路高度一致,这其实说明问题空间已经被人为缩得比较小了。大拓扑下的泛化能力,仍然没解决。
4. 实测中的问题合集:MSF接口、回合超时与Agent的"作弊"行为
4.1 MSF RPC返回结构不稳定的处理
MSF RPC的接口虽然能用,但返回结构并不总是规整的。同样是执行一个exploit模块,成功时返回的JSON是一套结构,失败时又是一套结构,超时或者资源不足时可能还会返回空洞的无字段对象。训练脚本里每次解析这些不规整的返回,都可能抛异常导致整个训练中断。
我最终的处理方法,是在step()外部套一层统一的"结果解释层"——所有RPC调用返回后,先走一个标准化的解析函数,统一提取五个关键字段:是否有新的session、session是什么类型(shell还是meterpreter)、当前主机的权限是否变化、目标文件是否读取成功、有没有发生运行时错误。解析函数内部包裹了异常捕获,任何一个字段缺失都给默认值。这样,即使MSF某次返回异常,训练进程也不会崩,只是这一次动作被判定为失败,Agent收到一个负奖励而已。
4.2 回合超时与资源瓶颈
训练资源瓶颈比我想象中严重得多。一个Agent回合,如果动作序列比较长,跑完可能超过二十分钟。时间大部分耗在等待exploit的轮询上——利用漏洞之后要等shell回来,横向移动要等目标主机的响应。如果没有超时控制,一个卡住的exploit可能让整个回合挂起半小时。
解决办法有几个层面:把每个exploit动作的最长等待时间从默认较长的时间缩到15秒;在step()里额外加一个全局的回合时钟,一个回合最多40步,超了直接终止;还有就是把session的稳定性检测前移,一旦拿到shell就立刻进入下一动作,不用等MSF自动把session的状态同步给RPC。
就算这样,一次完整训练跑下来,仍然需要持续占用四台靶机的计算资源,加上Metasploit本身的内存消耗,普通笔记本根本撑不住。我当时是在一台工作站上跑的训练环境,内存64G,CPU 16核起步才勉强够。如果你只是想在笔记本上试试水,建议把靶场缩小到两台主机,并且关闭所有非必要的图形界面,最大限度地降低资源占用。
4.3 Agent学到了什么"反直觉"的行为
这个问题非常有意思,也是RL项目里最迷人的地方——Agent确实会在环境里找到一些人类设计时没想到的路径或策略。
第一种是"重复利用刷分"。上面提到过,Agent发现某个漏洞总是能利用成功,就会在同一个主机上反复利用这个漏洞,每次都能触发"获取新会话"的奖励分支。直到我加了状态去重——只有在"这台主机此前没有任何会话、现在拿到了新会话"时才给奖励,这个刷分行为才消失。
第二种是"等待耗命"。Agent学到过一种策略:啥也不干,就待在某个已经拿到的会话里等时间,因为每走一步只有-0.05的微惩罚,而后面某个动作无论如何都会带来高奖励,那不如多拖几步,让价值函数更新时把延迟奖励算到自己头上。这其实是部分PPO实现里常见的"Termination Slack"问题——回合结束标志本身成了Agent可以利用的信号。后来我把最大步数改成了随机分布,不再固定40步,这种行为就弱了很多。
第三种是"路径简化"。Agent会发现,如果目标主机的某服务可以直接利用并获得管理员权限,它就会跳过"先拿普通shell再提权"的中间步骤。这倒不是bug,反而是Reinforcement Learning的价值体现——它自己发现了更短路径。不过这也会让你反思:我们是不是在动作空间里塞了太多"应该做"的步骤,导致原本可以直接利用的高权限漏洞被人工规则埋没了。
5. 从靶场到真实内网的桥:能落地的场景和不能越的边界
5.1 哪些环节真正能自动化落地
训练完Agent之后,我认真思考了这个问题:这套东西在真实环境里到底能当什么用?
以我目前的测试结果看,真正有实用价值的是"攻击路径发现"本身,而不是"实际利用"的自动化。在真实资产环境中,把Agent的探索约束在一个很浅的深度,比如"扫描网络 → 识别端口服务 → 匹配历史漏洞 → 给出潜在路径建议",这个流程已经能产生有价值的输出了。它可以扮演一个不知疲倦的资产梳理器,帮渗透测试人员生成一份"候选攻击面清单",剩下真正动手的事情——利用漏洞、对抗安全设备、写exploit,仍然需要人来决策。
另一个可行的场景是防御侧的能力验证。你可以用这个环境自动生成进攻路径,然后交给防守仿真环境去验证检测规则和响应策略是否覆盖到了。这个用途和传统攻防演练类似,但RL的价值在于,它可以持续生成各种训练数据,让蓝军的检测系统不断看到新鲜的攻击行为特征。
5.2 模拟环境与真实网络的差距
这里要非常坦诚:模拟环境和真实内网之间,沟壑比大多数论文里描述的要深。
首先是环境噪声。真实网络里有各种设备干扰、入侵检测系统、端点防护、流量监控。即便你的exploit代码本身有效,执行到一半也大概率被安全设备拦截。而模拟靶场里的Agent,根本无法体会到"有防守方存在"的难度。
其次是状态感知差异。在靶场里,Agent能通过RPC调用的返回值精确知道"这次动作成功了";真实环境里你往往只知道"命令发出去没有报错",至于本质效果如何,可能需要好几分钟才能确认。这种感知不确定性,是Agent在模拟环境里完全学不到的。
第三是漏洞前向兼容性。真实网络的资产每天都在变,CVE漏洞利用结果受补丁状态、系统配置、语言环境等多方面影响。模拟靶场里固定的那几台机器,漏洞利用成功率是相对恒定的,Agent学的"利用某个漏洞大概率能成"这个经验,到真实网络里很容易翻车。
所以我的判断是:以现在的算法能力和工程成本,RL Agent短期内不能当渗透测试工具用,它更适合做“策略思维”的验证工具、训练数据集生成器、红队前期侦察的辅助大脑。
5.3 无论做自动化还是研究,授权是一切的前提
这一点必须单独拿出来讲。Metasploit这类框架本身就是双刃剑,加上强化学习的自动决策能力后,威力会被放大。如果你对训练Agent有兴趣,务必将实验范围严格限制在你自己拥有所有权的靶场网络里,或者有明确书面授权的测试范围内。未经授权去探测、利用任何他人的主机和网络,不管目的是研究还是演示,都是明确越界的行为,这一点没有任何商量余地。
合规说明同样适用于"开源工具集"的使用场景。在国内做安全意识教育、攻防演练、内部培训,必须先拿到资产管理部门的授权文件,所有数据不出演习环境,所有样本只留报告不留流量。安全研究的底线,是确保自己的工作让别人更安全,而不是让别人更难防。
如果你也想做这个方向,我的建议是先从防御视角理解RL的输出——看看Agent发现的最短路径、潜在弱点、容易出错的模块,这些信息才是做安全建设时最有用的部分。把目标从"让AI自动打穿一切"改成"让AI帮我们更早地发现哪里可能被打穿",很多价值自然就能落地了。
最后再分享一个调试技巧:训练过程中不要只看总奖励曲线,把"单回合是否最终拿到目标文件"这个成功率单独拎出来看。因为总奖励包含各种中间奖励,波动很大,看着像在上升,实际上Agent可能一直在重复劳动。只看最终成功率,才能真实反映Agent是否真的学会了整条攻击链。