聊聊最近挖 Security Bounty 的感受
文章头图来自 @大空水獭 https://t.bilibili.com/637532953957629970
起因
有将近四个月没写点东西了。赶着周六的凌晨,带着些许睡意,想来分享下从去年年末到现在挖 Security Bounty 的感受与经验。文中会挑选几个我觉得有意思的漏洞,分析其背后的故事以及我的想法。
记得三年前,大二上学期刚开学协会招新的时候,跟新生聊到协会的 @Li4n0 学长大二就 Typora RCE 连拿两个 CVE,那新生便问我有没有 CVE 编号。我一时语塞,挠挠头尴尬地回答没有。可能就是这个原因吧,后来自己特别想要有个 CVE 编号。 之后虽然也在空闲的时候陆陆续续地去挖了一些 SRC,赚了点小钱,但最终的报告又不公开,自己拿了钱确实爽,但对外没啥能吹得。😅
时间来到了去年年末十二月份,我突然对国外某产品的一个功能的代码实现很感兴趣,便去翻他们网站上关于此功能的设计文档。看完后暗自佩服的同时也在想这么大个公司会不会有啥洞呢?趁热打铁挖了一波,还真有!后面陆陆续续地交了这个公司的几个洞,小赚了一笔大的。但,依旧没有 CVE。
(虽然到后面给补上了)
后面我便转换了下思路,专盯着那些 stars 数很高的开源项目。时间来到一月下旬,当时公司在搞一套内部的统一鉴权系统(SSO),用于各项独立服务的登录鉴权。国内有很多仿照海外 Auth0 做的产品,但价格都太贵了。最后选择使用开源项目 casdoor (https://github.com/casdoor/casdoor) 进行自建。casdoor 是基于著名 casbin 项目发展而来的,两者有着千丝万缕的关系。同时 casdoor 也是使用 Go 语言进行开发,我便试着白盒扫了下。好家伙,还真给我捡了漏了,扫到一处 SQL 注入。
CVE-2022-24124 casdoor SQL 注入
漏洞触发的原理很简单,有几个公开的 Web API 查询接口支持对表中任意字段的模糊查找,具体的代码实现是字段名直接从 field
的 Query 参数中传入,格式化字符串拼接进 "%s like ?"
语句,导致 LIKE
前面的内容可控,从而引发 SQL 注入。那这管你套啥 ORM,神仙也救不了你。
PoC 见 #439
官方修复的 PR 刚开始是用黑名单过滤字符,我 review 时直接给绕了哈哈。我给的修复建议是用反射解析结构体里的字段,作为 field
参数的白名单进行过滤。官方后面觉得这样太复杂了,直接正则检验只能传入大小写 + 数字,给牢牢地限制死了。
可惜的是我貌似是第一个给 casdoor 提交安全漏洞的人,官方以前并没有相关的漏洞处理流程。最后只能自己默默地去申请了 CVE 编号,CVE 下来的那天我还在回老家的车上,看到手机上收到的邮件兴奋地不得了。(但我其实更希望的是官方能主动帮我申请,也算是一种特别的感谢与肯定。)
CVE-2022-24123 marktext XSS -> RCE
在挖到 casdoor 的 SQL 注入之后的第二天,我微信上刷到了一篇文章,文章介绍的是在 Typora 收费后,作者说大家可以使用 marktext 这个开源免费的 Markdown 编辑器作为替代。想起之前 @Li4n0 挖到了 Typora 的 RCE,正巧这个 marktext 也是基于 Electron 实现的跨平台桌面应用,我也想来试试。
可惜我一看到 JavaScript 就头大,根本不想去认真审,随即胡乱地在翻着 marktext 的 issue。突然发现了这个长达一年之久的 issue #2504。他里面提到 marktext 的 Mermaid 图表功能存在 bug,输入类似 HTML 的标签 <something_in_chevrons>
自动给闭合变成了 <something_in_chevrons>some text</something_in_chevrons>
。
我一看,好家伙,这不说明输入被当做 HTML 解析了嘛,这不妥妥的 XSS 嘛。我在 marktext 中把他 issue 里的标签内容改成 <img src=1 onerror="alert(1)">
,直接就弹窗了。改成
<img src=1 onerror="require('child_process').exec('open /System/Applications/Calculator.app')">
直接弹计算器了。成了! 真的是白捡了一个 RCE,提给后官方很快就修了。但根据 marktext 之前几个 RCE 的 issue,最终都是漏洞提交者去申请了 CVE。所以我又只能自己去申请 CVE 编号,凄惨。
后面我简单的跟了一下这个洞,发现是直接将 innerHTML
设置成用户输入导致的。后来全局搜索代码,也发现了一处同样的问题,不过读取的是用户剪贴板中复制的内容。想了下好像没啥能利用的可能,毕竟用户哪会傻到去复制一段自己都看不懂的奇怪代码进来。可是…… 就在我这个 CVE 公开的几天后,一个韩国老哥交了这个剪贴板复制导致 RCE 的洞,居然还被承认了!血亏啊!
与 Cloudflare 的纠缠
过年期间住在奶奶家的时候,晚上睡前会随便网上冲浪到处看看。那个时候我把 GitHub Advisory Database 里所有 Go 相关的历史漏洞信息全部爬了下来,整理成了一个 Excel 慢慢看,企图从中总结出一些 Go 相关漏洞的特点。看到之前 Iris 框架之前上传文件目录穿越的洞。漏洞的成因是 Iris 想修目录穿越,但只是用了很简单的分步 strings.ReplaceAll
进行替换,这个的绕过不用说了吧…… 双写一下就完事了:....//
。
// Fix an issue that net/http has,
// an attacker can push a filename
// which could lead to override existing system files
// by ../../$header.
// Reported by Frank through security reports.
header.Filename = strings.ReplaceAll(header.Filename, "../", "")
header.Filename = strings.ReplaceAll(header.Filename, "..\\", "")
我看了真的觉得好笑,不会吧,不会吧!不会真有人这么防目录穿越吧?!全局搜了下,好家伙,还真有,还是大名鼎鼎的 Cloudflare。 他们在这个 commit 里修复了 CVE-2021-3907 这个下载文件时目录穿越可能导致 RCE 的高危漏洞。修复的方式也是很简单粗暴:
path = strings.ReplaceAll(path, "../", "")
我按照 GitHub Advisory 下的指南给他们发送了邮件,几天后他们就给修了,并且发布了新的 GitHub Advisory。我便发邮件多问了下能否在这个 GitHub Advisory 下给我的 GitHub 账号加个 Credit,这样我的 GitHub Profile 下面也有一个好看的小徽章了!
对方隔了半个多月回邮件了,没直说不行,而是让我去 HackerOne 上再提交一波,然后给我 Bounty。可是…… 比起钱,我还是更想要这个好看的徽章,大家都有就我没有,我好没面子。 😭😭😭
开始使用 huntr.dev
后来在 Twitter 上刷到了 huntr.dev 这个平台。他们的目标是提高 GitHub 上开源项目的安全性,只要提交 GitHub 上开源项目的漏洞,他们作为平台方就会帮你联系项目的开发者,并在漏洞确认后给予一定的 Bounty 奖励,甚至还能帮忙申请 CVE。给予的 Bounty 金额好像跟项目的 stars 数正相关。我查了下 Cardinal,发现交 Cardinal 的洞都能赚个 10 美刀。那我自己往 Cardinal 里写洞自己交,左脚踩右脚是不是能上天?
CVE-2022-0415 Gogs RCE
恰好那段时间无闻邀请我进了 Gogs 的组织中,闲聊的过程中他也提到了 huntr,说最近有很多人通过这个平台给 Gogs 提交漏洞让他确认。huntr 现在也成为了 Gogs 项目推荐的漏洞上报方式。 就当熟悉 huntr 的提交流程了,我粗略地看了下 gogs 的源码,直接对着危险函数硬搜。(说是粗略也不是,之前写 CRUD 的时候项目结构都是借鉴的 Gogs,看了无数遍了)
结果还真找到了一处 RCE。
时间要回到大一下学期的暑假。我是个很懒的人,平时很少复现漏洞,除非那个洞的利用过程很吸引我,不然我就是看一眼网上复现的文章就结束。到目前为止我认真复现过的漏洞数量屈指可数。大一暑假的时候我看到土爷发了一篇复现 Gitea RCE (CVE-2019-11229) 的文章,其中的利用过程很巧妙:
通过 go-ini 库存在的 CRLF 漏洞,逃逸引号出来改写本地 Git 仓库的 .git/config
文件,通过设置 core.sshCommand
参数,在 Git 仓库被 pull 和 fetch 时,对应的命令将会被执行,从而达到 RCE 的目的。这个 core.sshCommand
的 trick 我到现在还在用,真的屡试不爽。如果以后有人问我印象最深的漏洞是哪个,我绝对会回答是这个!它吸引我的点在于,它在一个合法的正常的我们日常都在用的程序 (git)中找到了一个因为恶意的配置,导致可以 RCE 的操作。
Gogs 的这处 RCE 最终的原理也是如此。我在文件上传处看到其从上到下是这样处理上传文件的路径的。
dirPath := path.Join(localPath, opts.TreePath)
...
// Prevent copying files into .git directory, see https://gogs.io/gogs/issues/5558.
if isRepositoryGitPath(upload.Name) {
continue
}
...
targetPath := path.Join(dirPath, upload.Name)
其中 targetPath 是最后文件写入的路径。后半段文件名 upload.Name
做了检测,防止复制文件进入 .git
目录,而前半段 dirPath
中的 opts.TreePath
是来自用户传入的可控参数,这个参数却没有被检测。所以我们在上传文件时构造 tree_path=/.git/
即可将文件上传至仓库的 .git 目录中,后续 Gogs 会本地 pull + push 我们的仓库,使用上面的 trick 覆盖 /.git/config
文件即可实现 RCE。当然,我们也可以直接 tree_path=../../../xxx
目录穿越写系统定时任务弹 Shell,利用的方法数不胜数。
目前 Gogs 的 main 分支已经修复该漏洞,且在最新发布的 0.12.6 中得到修复。具体报告 huntr 已公开:https://huntr.dev/bounties/b4928cfe-4110-462f-a180-6d5673797902/
但离谱的是,在 huntr 提交报告时网页上已明确说明此项目会给申请 CVE。但直到漏洞确认修补后,huntr 官方也没动作。无奈我只能找 huntr 管理员,有趣的是这个管理员在 GitHub huntr 仓库提了个 issue,抱怨每天都有一堆人找他手动申请 CVE,他想要一个自动化的方案,同时把所有找他申请 CVE 的人全截图挂在了 issue 下。对没错,我也被挂了。😡
但不管怎么说,我最终都如愿以偿地获得了第一个 GitHub Advisory Credit!感谢无闻老师!🥳
[CVE 待申请] Gitea 任意文件删除
提交完这个 RCE 后,我第一时间肯定是去看 Gitea 是否存在类似的问题,可惜 Gitea 后面改成了直接对 git 的 Index 等进行操作,相当于直接操作 git 数据库了,不再是像 Gogs 一样本地模拟用户添加文件再 add + commit + push 的操作。
但我又想起 Gitea 喜欢整花活,啥有用没用的功能都往里面塞,比如它就支持 Git LFS。嘻嘻,这 LFS 你总得老老实实地上传文件了吧?可惜 Gitea 做了严格的过滤。
我又继续搜起了危险函数来,发现上传后的 LFS 文件 Gitea 都会对文件名做哈希,然后取文件名哈希前 1、2 位,3、4 位,建立目录,作为文件最终的存放路径。这种操作在很多包管理系统中都很常见,iOS 的 CocoaPods 就是这样的。
例如我们在 Gitea 上的文件名是 48076e66a051950bd5cd7fc489924a5d67865dac
,那么它将被存放在 48/07/48076e66a051950bd5cd7fc489924a5d67865dac
下面。具体的代码实现是这样的:
func AttachmentRelativePath(uuid string) string {
return path.Join(uuid[0:1], uuid[1:2], uuid)
}
那要是我传入一个文件名为 ....foo
的 uuid,它是不是路径拼接后就把 ../../foo
的文件给删了?确实是这样的捏~
但是 LFS 文件的添加和修改接口,在操作前都会查询一遍数据库确保这个 uuid 存在。但对于删除操作,是 ORM 删除一下数据库的记录,然后再删除文件。Go ORM 的删除操作都一样的特性,根本不管你 WHERE
条件是否能查到记录进行删除,删了个寂寞也给你返回成功。最好在执行删除操作后再检查一下 RowsAffected
确认影响的行数。
所以通过构造 ....%2fcustom%2fconf%2fapp.ini
这样的 uuid,我们就能轻松的删掉 Gitea 的配置文件。可惜只有在程序重启后才会触发重新安装的操作。删除了 SQLite 的数据库也只是给你 500 报错而已。目前倒是没想到很好的利用方式。
具体报告 huntr 已公开:https://huntr.dev/bounties/c5ed8660-a896-4101-b6a7-05772443485b/
令我不开心的是,Gitea 明明在报告中表明要在博客文章中对我进行感谢,且询问了我的用户名,但是在最终发布的文章中却漏了。我在报告中询问后他们提了个 PR 说会补上,但是这个 PR 现在就卡在那里没人 review 没人合;以及 huntr 说的帮申请 CVE 到现在也没消息。
最后说几句
所以这不欺负老实人嘛?直到现在,挖了这么多洞之后,我都没能有一次畅快的经历。 CVE 得我自己申请,官方的感谢要不是没有,就算有了最后也给漏了。然后 Cloudflare,加个 GitHub Advisory Credit 是会判刑还是怎么?同一个项目中,别人有我就没有。交了 Hackerone 还跟我掰扯半天问我为啥能 RCE,你之前那个洞不是自己定的高危 RCE 然后自己没修好嘛?好家伙双标是吧? 啊对对对,我承认我就是追名逐利,就看中这些虚无缥缈的感谢啊,徽章啊。这些就是我跟别人瞎吹逼的资本,所以我在意。
嘛,接下来有空的时候会去尝试做更有效率的开源软件漏洞挖掘,不想再像上面那些纯靠运气成分或人工肉眼看了。现在脑子里其实已经有一些酷炫的想法想要去做了,奈何自己太菜了还有不少前置知识得去学习的。不过至少,今年跨年定的年度目标:获得人生中第一个 CVE 编号,以及今年 Bug Bounty 总金额超过 [已删除] 元这两个目标已经提前圆满完成了。
喜欢这篇文章?为什么不打赏一下呢?