Typecho 曝出 SSRF 漏洞,我自己的博客也中了,顺手把补丁打上

今天刷到一个安全博客的更新,标题带着 CVE 编号,讲的是 Typecho 的一个 SSRF 漏洞。我博客用的就是 Typecho,看到这种跟自己有关的东西肯定要点进去看,看完之后心情有点复杂。

先说这个洞是怎么回事。Typecho 有个 pingback 功能,是博客之间互相引用的时候用来通知对方的。它在 /action/service 这个接口上,请求里带上 do=ping 就会走到发送的代码里。正常来说这里要先校验一个 token,token 是拿密钥和时间戳算出来的 sha1 字符串。问题就出在校验那一行,写的是两个等号。PHP 里面两个等号是松散比较,只要把 token 传成布尔值 true,那么 sha1 不管算出什么东西,跟 true 一比都成立,校验直接就过去了。不用登录,不用任何凭证,一个请求就进去了。

过了校验之后还有更麻烦的。发送逻辑会拿攻击者给的地址去发请求,第一发是访问这个地址,然后从返回内容里读 X-Pingback 这个头,或者读页面里声明 pingback 的那个链接。读到之后,再拿这个地址发第二发 XML-RPC 请求。这两发都完全没有检查过是不是内网地址,第二发的地址还完全由攻击者说了算,因为第一发的响应本来就是攻击者自己的服务器返回的。也就是说,可以让你的服务器去访问 127.0.0.1,访问 10 开头的内网段,访问云服务器上那个 169.254.169.254 的元数据地址,这些平时从外面根本碰不到的东西。

看完我第一件事就是去看自己的版本。后台一看是 1.3.0,文章里写的影响范围是 1.3.0 及以下,正中。

然后我又去官方仓库翻了翻。最新发布的版本还是 1.3.0,master 分支上那行两个等号也还在,而框架里那个专门判断内网地址的函数 checkSafeHost 倒是早就写好了,只是 pingback 这条路上从来没人去调它。所以想等官方出修复版本,短期内是指望不上的,只能自己动手。

动手之前先确认自己是不是真的有问题。文章里给的验证方法挺安全,不会真的让服务器往外发包。往接口 POST 一段 JSON,token 填布尔 true,pingback 填一个空数组。空数组的意思是后面那个遍历循环根本不会跑,服务器不会去访问任何外部地址,只看返回的状态码就能判断,200 就是校验被绕过去了,403 就是被挡住了。

我照着试了一下,两种写法都是 200,那就不用怀疑了,确实中招。

修复我做了两处。一处是把那行松散比较改掉,换成 hash_equals 严格比较,前面再加一个判断,传进来的不是字符串就直接拒绝,这样布尔 true、数组、数字这些乱七八糟的类型都进不来。另一处是在 HTTP 客户端的 send 方法里加了检查,只放行 http 和 https 协议,再用 checkSafeHost 把内网地址、回环地址、保留地址全部挡下来。选在这个地方动手,是因为不管 pingback 的第一发第二发还是 trackback,所有往外发的请求最后都会走到这个方法,把这一处堵上就都堵上了。

动手之前两个文件我都先备份了一份,文件名后面挂了个时间戳,真改出问题还能还原回去。

改完验证了一下。PHP 语法检查两个文件都过了,没有报错。再拿刚才那个空数组的办法试,token 传 true 的现在返回 403,传错误字符串的也是 403。博客首页、feed、关于页面、后台登录都正常,没有哪里坏掉。

有句话得说在前面,这个补丁不是百分百严实。文章里提到有一种叫 DNS Rebinding 的手法,checkSafeHost 校验的时候解析一次域名拿到的 IP,跟 curl 实际去连接的时候解析出来的 IP 未必一样,理论上可以拿一个先返回公网 IP 再返回内网 IP 的域名骗过去。要彻底堵住得把校验过的 IP 直接钉给 curl 用,这一层我这次没做。不过比修复前已经安全很多了,先这样放着。

写这篇主要是给自己留个记录。要是你也用 Typecho,可以照这个思路查一查自己那边。这个洞不需要登录就能触发,后台那个不显示 pingback 的开关也拦不住它,因为问题出在发送那边,跟你前台显不显示没有关系。

评论加载中...