|
线上一个内部管理服务被扫出 pprof 接口暴露,信息泄路径是漏漏 /debug/pprof/。攻击者直接拿到了 goroutine 栈、洞修heap 分配信息,信息泄甚至通过 profile 接口触发了 30 秒的漏漏 CPU 采样。这不是洞修理论风险,是信息泄真实发生的案例。pprof 本是漏漏 Go 自带的性能分析工具,但默认挂载到 HTTP 服务上时,洞修如果没有做访问控制,信息泄就等于把进程内存布局、漏漏函数调用关系、洞修甚至部分业务数据(比如某些字符串常量)直接摊开给任何人看。信息泄 很多人以为 pprof 只暴露性能数据,漏漏不涉及敏感信息。洞修错了。heap profile 里会包含对象类型和大小,如果代码里把密码、token 存在全局变量或长生命周期对象中,这些字符串可能出现在堆转储的采样里。goroutine 栈更直接,函数名、参数值、局部变量地址都可能被打印。攻击者不需要复杂利用,curl 一下就能拿到这些信息,用来做后续攻击的跳板。 问题出在默认配置上 Go 的 net/http/pprof 包在 init 函数里自动注册路由到 DefaultServeMux。只要你的服务用了 http.ListenAndServe 且没自定义 mux,pprof 就默默生效了。很多团队在开发阶段为了调试方便,直接 import 这个包,上线时忘了摘掉。更隐蔽的是,有些框架(比如 gin)通过 gin.Default() 也默认带了 debug 中间件,但 pprof 需要手动注册,反而容易被忽略。 我见过一个项目,生产环境跑在 Kubernetes 里,Service 只暴露了业务端口,但 Pod 的 liveness 探针端口没做网络策略限制。结果集群内其他 Pod 可以随意访问 pprof。横向移动时,攻击者从被攻陷的 Pod 直接拉取目标服务的 heap profile,找到了数据库连接串的明文片段——因为代码里用了 sql.Open 后没及时关闭的 *sql.DB 对象,其内部结构被 heap 采样捕获。 修复不是简单删掉 import 直接注释掉 import _ "net/http/pprof" 确实能关掉,但你就失去了线上诊断能力。更好的做法是:把 pprof 挂到一个独立的、只监听 localhost 或内网管理端口的 HTTP 服务上。比如: go func() { mux := http.NewServeMux() mux.HandleFunc("/debug/pprof/", pprof.Index) mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) mux.HandleFunc("/debug/pprof/profile", pprof.Profile) mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) mux.HandleFunc("/debug/pprof/trace", pprof.Trace) http.ListenAndServe("127.0.0.1:6060", mux) }() 这样外部无法直接访问。如果必须远程采集,加一层认证中间件,比如校验一个静态 token 或者走 mTLS。别用 Basic Auth 明文传输,除非你确定链路是加密的。 另一个坑是 pprof 的 symbol 接口。它允许通过 POST 提交地址来解析符号,如果不限制,可能被用来做内存探测。Go 1.20 之后这个接口默认只允许 GET,但老版本需要自己处理。还有 trace 接口,采集时间更长,对生产环境影响更大,建议只在需要时临时开启。 检查你现在的暴露面 跑一下 curl http://your-service:port/debug/pprof/,如果返回 HTML 页面,说明已经暴露。别只看主端口,检查所有监听地址,包括 metrics 端口、health 端口、grpc 的 http 网关端口。有些服务用 http.DefaultServeMux 注册了多个 handler,pprof 混在其中很难发现。 最稳妥的修复方案:在 main 函数开头调用 http.DefaultServeMux = http.NewServeMux() 重置默认 mux,然后显式注册你需要的路由。这样即使某个依赖包偷偷 import 了 pprof,也不会生效。代价是所有依赖 DefaultServeMux 的第三方库需要手动迁移,但安全收益值得。 最后提醒一句:pprof 信息泄漏不是低危漏洞。它直接降低攻击者的信息收集成本,配合其他漏洞能造成实质破坏。修完记得用 go tool pprof 本地验证一下,确保采集功能还在,只是访问路径变了。 |