口令喷洒遇认证串号:从VLAN到光耦的隔离实战复盘 前一阵做内网口令安全评估我写了个批量口令喷洒脚本对着三十几台新到的网络设备跑。跑到半程结果让我一下子清醒A设备的admin账号密码明明是对的系统却返回认证失败B设备根本没测过几次guest日志里却显示guest因为连续错误被锁定。我第一反应是脚本的线程把结果写串了于是翻代码、看日志、加锁、重跑折腾到凌晨发现每条输出对应的IP都对得上。脚本是好的问题出在测试链路本身。那段时间我满脑子就四个字共享状态。后面发生的事情让我把隔离这件事从网络到容器再到电路完整走了一遍最后还揭开了一个黑盒设备的一角。这篇文章就是完整的过程复盘对刚入行做安全测试、负责环境搭建或者写并发脚本的工程师应该都能找到点有用的东西。1. 口令实验撞上的串号事故测试结果为什么会互相污染1.1 实验场景35台设备一套集中认证这次任务是给公司新购入的一批网络设备做口令安全评估。设备共35台都是同一个厂商同一个型号管理端统一接入一台用了很多年的认证服务器走RADIUS做AAA认证。所谓AAA就是认证、授权、计费简单说就是设备自己不做密码判断把用户名和密码转发给认证服务器认证服务器说通过设备才放行。为了不干扰正常业务网段管理员要求所有测试流量经过跳板机进出。跳板机做了源NAT所有从测试机发过去的请求源IP都会被改写成同一个地址。这一步本来是为了管理方便谁也想不到它成了后续所有怪象的伏笔。测试脚本用Python写的线程池开8个worker每个worker负责若干台设备的Web登录页往登录接口提交用户名和口令组合再根据HTTP响应判断是否成功。脚本本身很普通扔到任何一台机器上都能跑。问题也正出在这个普通上它太依赖一个假设——每个请求都是独立的。1.2 三种诡异现象误报、虚放、误锁事故的现象有三类我很清楚地列在文档里。第一是误报。A设备用正确的admin口令登录Web页面明确提示用户名或密码错误。手动用浏览器登录同一台设备同样的口令一登就进。设备和口令都没问题。第二是虚放。B设备用错误的guest口令登录居然偶尔能跳转到管理主界面。这意味着有些错误的尝试被认证服务器判定为通过这是比误报更严重的事。第三是误锁。C设备的admin账号脚本只提交过两次错误口令但设备日志显示它已经触发了连续失败锁定策略账号直接被锁。奇怪的是三种现象不是同时出现在每台设备上而是随机分布在不同设备和不同时间点。这让我最初完全没有规律可循。1.3 三条排查路线程、单台、并发验证排查第一步审查脚本。我把所有并发线程改成串行执行一次只跑一台设备。结果很干净所有正确口令全通过错误口令全被拒锁定策略不再误触发。这一步说明设备和口令库本身没有毛病。第二步恢复并发但只保留一台设备。结果同样正常。说明单纯的高并发也不会导致问题。第三步恢复并发且保留全部35台设备同时把线程数提升到16个。问题立刻回来了。于是我的怀疑范围缩小到一个很具体的条件并发加多台设备同时向同一台认证服务器发起认证。为了确认网络层的因素我在跳板机上抓包。抓包结果很有趣RADIUS请求正常发出响应也正常返回但对不上的不是报文本身而是请求和响应之间的事务对应关系。简单说A设备发出的认证请求拿到的是B设备认证请求的响应。到这里我已经非常确定问题出在某种共享状态上只是不知道它藏在哪一层。2. 共享状态的三种藏身之处连接池、全局变量与设备固件2.1 共享状态到底是什么共享状态说得直白一点就是多个执行单元共同读写同一份数据。写单线程程序时这份数据是你的私有变量一旦开了多线程、多进程或者多台设备同时往同一个服务发请求这份数据就变成了公共资源谁都能碰谁都会改它。我习惯拿公寓水电表打比方如果每家每户有独立分表谁用了多少电一清二楚但整栋楼只有一块总表某一户开了个大功率空调整栋楼的账单都会跳。认证服务器里那张会话表就是那块总表。2.2 藏身点之一客户端连接池和会话对象第一类常见的共享状态在客户端。很多刚写并发脚本的工程师第一反应是共享requests.Session因为Session里带着Cookie和连接池多线程共用同一个Session时如果服务端按Keep-Alive连接缓存认证上下文线程A的请求就可能被线程B继承了状态。这种问题在单纯访问公开接口时不容易暴露因为无状态接口不依赖上下文。但在口令实验里登录接口天然依赖会话一旦服务端针对连接而不是针对用户维护状态串号就来了。我早年被这种问题坑过所以这次一开始也怀疑过它但很快被排除了就算把脚本改成每个线程独立Session问题依然在。2.3 藏身点之二服务端全局注册表第二类在服务端。很多老系统为了节省内存会把会话注册表做成一个全局的单例对象用哈希表保存所有在线会话。如果Key设计不合理比如只按源IP区分那么来自同一源IP的并发请求就会共享同一个会话槽。伪代码大致是这样# 错误示范会话Key只按源IP区分 session_table[source_ip] session_context更合理的Key应该组合源IP、目标设备IP、用户名甚至随机序列号。这也是我在排查过程中最怀疑的地方因为我们的场景里所有请求都来自同一个源IP和这种错误Key的特征完全吻合。2.4 藏身点之三设备固件里看不见的全局会话表最后就是这次真正的主角。那台认证服务器是闭源的文档只写了支持并发认证请求但没有说明内部如何管理会话。它的固件里极大概率维护了一个当前活动会话的全局变量或者一张以源IP为索引的会话表。新请求到达时如果源IP已经有一个活动会话新请求会直接把旧会话顶掉。认证响应生成时固件读取当前会话槽自然把对应另一个请求的结果返回回去。这样一来任何来自同一IP的并发请求响应都可能被装到别的请求头上。用一句话概括它把一个源IP当成了一个客户端并且默认一个客户端不会有并发认证。2.5 为什么口令实验里共享状态的杀伤力被放大如果只是普通接口的并发测试这种串号顶多让返回数据对不上重试两次就完事。但口令实验里共享状态带来的后果严重得多正确口令被记录为失败你会误判设备存在弱口令问题。错误口令被判定为通过弱口令问题反而被掩盖。多次错配会触发服务端的账号锁定策略直接把合法账号锁死影响业务。如果共享会话中偶然残留其他用户的口令信息还可能在日志里留下敏感数据。所以当确认问题的性质后我几乎没有犹豫就确定了解决办法的方向隔离。把不该共享的状态拆开把测试流量从一口锅里分到独立灶台上。3. 隔离方案的四个工程层次从VLAN到光耦的取舍那晚之后我把隔离手段按层次梳理了一遍发现一个很清晰的分界线从网络域做隔离到容器资源隔离再到内核驱动清理最后是电路级的光耦隔离。每多走一层隔离更彻底成本也更高。下面把这四层详细说一下。3.1 网络域隔离VLAN划分与ACL配置最便宜的隔离是网络域隔离。把测试网段从生产网段中剥离开让测试流量只可能到达目标设备。在实验开始之前我就应该先把测试区划到独立VLAN可惜当时跳板机直接接入设备管理网省了这一步也就漏掉了第一层防护。配置VLAN和ACL的思路很简单示例命令如下不同厂商语法有差异思路一致# 创建测试VLAN 100网段10.10.100.0/24 vlan 100 name test-net # 三层接口 interface vlanif 100 ip address 10.10.100.1 255.255.255.0 # ACL只允许测试网段访问目标设备网段10.10.20.0/24 acl number 3001 rule 5 permit ip source 10.10.100.0 0.0.0.255 destination 10.10.20.0 0.0.0.255 rule 10 deny ip # 接入端口 interface g0/0/1 port link-type access port default vlan 100 traffic-filter inbound acl 3001这样配置之后生产网段的流量进不到测试区测试区也访问不到生产网段。代价是增加了网络规划工作量好处是万一测试脚本失控爆炸半径被限制在测试VLAN内。但这里必须说清楚VLAN隔离解决的是域的问题它可解决不了认证服务器内部的会话表问题。两台设备哪怕不在一个VLAN只要它们把认证请求发给同一个服务器且源IP相同会话照样串。所以网络域隔离只是第一步不是全部。3.2 容器资源隔离给每个测试进程一个独立网络空间我们的核心需求从认证服务器的视角看是让35台设备的并发请求看起来来自35个不同客户端。最直接的实现方式就是用容器给每个测试进程分配独立的网络命名空间和独立IP。当时我用的方案是Docker配合macvlan网络# 创建基于VLAN 100的macvlan网络 docker network create -d macvlan \ --subnet10.10.100.0/24 \ --gateway10.10.100.1 \ -o parenteth0.100 test-macvlan # 每台设备一个容器分配独立IP for i in $(seq 1 35); do docker run -d --name spray-$i \ --network test-macvlan \ --ip 10.10.100.$((i10)) \ spray-image --target 10.10.20.$i done每个容器拥有自己独立的协议栈、路由表、ARP表发出的请求都会带上自己的源IP。认证服务器看到的就不再是同一个IP下的高并发而是35个独立IP的登录请求。做完这一步之后串号现象立刻消失。容器隔离本质上是Linux内核的namespace机制在起作用。network namespace隔离了网络栈PID namespace隔离了进程视图。它比虚拟机轻量但也有一个隐含代价容器共享宿主内核。宿主机内核一旦被某个不兼容驱动污染容器网络就可能集体出问题。这就到了下一层。3.3 内核与驱动隔离当隔离工具本身不干活我在搭容器网络的时候正好撞上了这个问题。宿主机之前装过一版第三方网络准入客户端它在内核里注册了网络过滤钩子。macvlan给容器分配虚拟网卡时这个过滤钩子把所有非本地流量都给拦了导致容器能启动但网络完全不通。排查过程比较痛苦因为现象是几乎所有网络命令都正常但容器一个包都发不出去。最终定位到驱动层处理方式如下# 查看已加载的冲突模块 lsmod | grep access # 停掉依赖该驱动的服务 systemctl stop access-client # 从内核卸载模块 rmmod access_driver # 永久禁用把模块加入黑名单 echo blacklist access_driver /etc/modprobe.d/blacklist-test.conf update-initramfs -u reboot注意一点rmmod的时候如果模块正被进程引用内核会拒绝卸载并提示Module in use。这时候强来不行得先停服务再卸载模块。生产环境的服务器千万别随手执行这些命令最好先确认模块用途再在维护窗口操作。内核对驱动的不兼容很难从日志里直接看出来搭建实验环境时最好从一开始就用干净的宿主机尤其是做网络实验时。所谓内核隔离在实验场景里其实就是要保证宿主内核没有因为某个驱动被额外共享到影响所有容器。这是很多人容易忽略的一层。3.4 电路级隔离9600波特率下的光耦隔离选型隔离的最后一层在物理电路上。这次实验里有一台老设备网络管理口已经失效只能靠串口调试。我把USB转串口接到笔记本上数据时断时续偶尔还冒出乱码。一测设备电源地和笔记本地之间存在将近2伏的压差。地环路引起的漏电问题靠的就是电气隔离来解决。光耦隔离的原理很直接输入端把电平信号转换成红外光输出端检测到光之后再转换成电平信号。输入和输出之间没有电气连接地环路被直接切断。需要注意光耦隔离通信中波特率与器件延迟的关系至关重要选型时一定要算清楚。9600波特率是串口最常用的低速档。按8N1格式一个bit的时长是1除以9600秒约104.17微秒。普通光耦PC817的传播延迟典型值在4到10微秒加上上升下降时间一个bit周期内还有足够的裕量完成翻转所以隔离9600波特率的串口信号PC817勉强能用但边沿质量比较差。我当时用的是PC817把光耦做成了一个很小的转接板插在串口线上实验数据凑合能用但如果要求信号波形更干净我会换高速光耦。如果波特率提到115200一个bit只剩8.68微秒PC817的延迟就明显不够了必须上6N137这类高速光耦。6N137的传播延迟小于100纳秒适合几Mbps到10Mbps的速率。我整理了一个简单的选型表型号传播延迟推荐速率典型用途PC8174-10微秒不超过9600波特率低速IO隔离、简单串口隔离6N137小于100纳秒最高10Mbps高速串口、RS232/RS485隔离TLP2361小于100纳秒数十Mbps紧凑型高速隔离顺带提一句如果去看电源设计会发现还有非隔离高效率LCC拓扑、非隔离式Buck-Boost电路这类方案。它们的优点是效率高、体积小、成本低坏处就是没有电气隔离调试时人和设备都直接暴露在危险电位下。隔离型方案加了一个变压器或光耦成本高一点但故障时能切断危险传导路径。这个取舍逻辑和软件层隔离是一样的多一层隔离多一点安全边际代价是更多一点成本和复杂度。4. 黑盒一角会话串号背后的固件会话管理机制4.1 控制变量法锁定元凶隔离方案上线之后我把完整测试流程重新跑了一遍。结果非常干净所有正确口令全部一次通过错误口令全部被拒锁定策略只在账号真正连续多次失败后才触发。与之前唯一的差别就是源IP从1个变成了35个。我还没敢直接下结论又做了一组对照实验。这次故意把全部容器的源IP统一改成一个地址跑5分钟后串号现象立刻复活。改回独立IP现象消失。来来回回三次每次结果一致。到这里元凶被彻底锁定认证服务器内部维护着一份以源IP为索引的共享会话表。4.2 抓包还原请求与响应的错位过程为了把机制还原得更精确我在认证服务器的RADIUS端口和其中两台设备的网口上同时抓包。时间线是这样的T1时刻源IP 10.10.100.11上的管理员用户A发起认证请求认证服务器为它创建会话S1。 T2时刻源IP 10.10.100.11上的管理员用户B发起认证请求。固件发现该源IP已有活动会话用新会话S2覆盖了S1。 T3时刻认证结果返回。响应头里带的会话标识是S2对应的用户B的认证结果。于是用户A拿到的回执其实是用户B的结果。用文字表达就是这样请求A正确口令 → 会话S1 → 被请求B覆盖 请求B错误口令 → 会话S2 → 响应返回到请求A的位置S1的结果被S2覆盖丢失S2的结果被A设备读取。于是A设备的正确口令失败了B设备的错误口令却通过了。锁定策略的误触发也是一样的逻辑某次认证请求明明没失败但被错误响应标记为失败计数累加最终锁账号。4.3 为什么说这只是黑盒的一角说它是黑盒是因为这台AAA服务器是闭源的老设备官方文档只写了支持并发认证请求完全没提内部的会话管理方式。从外部只能看到请求和响应的报文内部固件怎么组织状态是看不见的。我所有排查本质上都是在黑盒外部做行为推断如果不是隔离实验把变量砍干净了我可能到现在还在怀疑脚本。现在我能确切知道一个内部行为它把一个源IP当成了一个活动客户端同一源IP的活动会话只有一份。但我不知道它内部是用全局变量、单向链表还是某种哈希表来实现的也不知道它还藏着多少类似限制。盒子里只露出了一条缝看到了一根线但这一根线足以解释所有奇怪现象。4.4 发现带来的直接改变这个黑盒行为颠覆了我对这类老认证服务器并发支持的信任。之后的测试环境规范里我加了一条硬性要求任何涉及AAA认证的口令实验都必须为并发请求分配多个源IP禁止用单一跳板源IP做高并发喷洒。能走独立管理口的目标设备尽量分开管理不能分开的就用容器或者虚拟网络把源IP拆散。我也给厂商提了问题单附上了对照实验记录和抓包时间线。对方后来回复产品固件的确为简化会话管理做了这个设计同一来源IP只维护一个活动会话且官网文档没有明确写出该限制。这个回复让我挺感慨你以为在测试一个黑盒最后发现黑盒里装的是人家当初一拍脑袋做的简化方案。5. 可复现的口令隔离实验三步走与环境清单5.1 第零步画一张共享点拓扑图动手之前先把拓扑画清楚。别嫌麻烦排查时这张图能帮你快速锁定变量。拓扑图上至少要标出三类信息流量路径测试机到跳板、跳板到设备网段、设备到认证服务器的完整路径。NAT点哪里做了源地址转换改成了什么地址。状态存储点认证服务器、会话管理组件、连接池等可能保存状态的位置。我当时就是因为跳板机上那个源NAT没标出来排查时多绕了一大圈。如果一开始就知道所有请求从认证服务器视角看都同源可能第一轮就能想到共享会话表。5.2 环境搭建VLAN、容器与测试工具按下面的清单准备环境基本可以覆盖大多数口令评估实验准备项配置说明测试机独立Linux服务器建议有2个以上网口网络隔离独立VLAN 100测试网段10.10.100.0/24目标接入设备网段10.10.20.0/24与测试VLAN可互通容器网络Docker加macvlan每个容器独立IP测试脚本Python并发脚本每个线程或进程绑定独立源IP抓包工具tcpdump或Wireshark客户端与服务端同时抓容器网络创建方式在3.2节给过直接抄作业即可。5.3 分阶段执行冒烟、基线、并发、验证执行流程我建议分成五个阶段顺序不要乱冒烟单线程、单容器只测一台设备确认工具本身没问题。基线串行跑完所有设备记下每台设备的正确口令和弱口令基线。这一步是后续判断并发是否串号的参照物。并发测试所有容器同时启动每个容器独立源IP按计划做口令喷洒。复现实验有意把所有容器源IP改成一个复跑一小段确认黑盒行为可复现为问题单留证据。回归验证恢复独立源IP再跑一遍确认修复有效。每一步做完把结果文件按设备IP命名归档避免后面混淆。5.4 踩坑清单这次实验最大的五个坑症状根因解决办法正确口令被拒绝认证服务器共享会话表被并发请求覆盖多源IP并发不做单源高并发错误口令偶尔通过响应错配到另一个请求抓包比对事务ID或会话ID账号莫名锁定锁定策略被并发误判触发评估锁定阈值控制测试频率容器网卡反复掉线内核驱动与macvlan冲突卸载冲突模块并加入黑名单串口数据乱码设备地与笔记本地存在压差串口线加光耦隔离这张表里的前四条在普通业务系统测试里也可能遇到。尤其是第五条看似是硬件问题本质还是共享地导致的隔离缺失。5.5 善后数据归档与问题反馈实验结束后抓包文件、脚本版本、容器配置、结果数据全部归档命名按日期和实验批次来。口令评估的结果数据非常敏感不要散落在临时目录测试结束后一并将容器销毁、VLAN清理。如果需要向厂商反馈黑盒行为附上对照实验时间线和抓包会话比任何文字描述都有说服力。那次实验之后我把隔离状态写进了自己的测试环境设计清单。共享状态本身不是坏事很多并发框架恰恰靠它才获得高性能但在口令实验里一旦状态串了轻则测试结论报废重则账号被锁、业务受损。我现在动手做任何测试之前第一件事都是盯着拓扑图问自己哪些状态是共享的哪些是可以在开始之前就先隔开的把源IP拆开、网络域分开、驱动清理干净、串口做物理隔离很多坑其实是可以在踩进去之前就绕开的。这比事后抓包省心太多了也顺便把黑盒的一角看得更清楚了。