挖洞经验 | 看我如何发现价值3万6千美金的谷歌RCE漏洞

2018-05-28 278077人围观 ,发现 5 个不明物体 WEB安全

看我如何发现价值3万6千美金的谷歌RCE漏洞

本文讲述了乌拉圭公立大学18岁学生 Ezequiel Pereira 发现谷歌(Google)最高级别RCE漏洞的相关过程。在今年年初,Ezequiel发现了谷歌Google App Engine (GAE)某非生产环境下的一个漏洞,利用该漏洞可以实现对谷歌部署环境的间接访问,并能发现和调用谷歌内部API接口,按照谷歌漏洞评价机制,该漏洞被评定为最高级别的远程代码执行漏洞(RCE),最终,Ezequiel凭借该漏洞,获得了谷歌赏金项目单个漏洞最高奖励的3万6千美金($36,337)。以下是Ezequiel对该漏洞的挖掘思路和相关过程分享。

提示:本文涉及了大量谷歌GAE的使用和操作,需要读者具备一些GAE应用经验。另外,漏洞测试过程中的一些概念和引用来自于 Ezequiel 在GAE上架设的这个App示例应用-http://save-the-expanse.appspot.com/,你可以在他的Github中来查看,其中包含了整个漏洞测试过程中涉及的源码和过程,有在线的Nmap、 gRPC C++ client 和 Google Protocol Buffe(谷歌内部的混合语言数据标准)。

先导概念

Google App Engine:GAE,是谷歌旗下一个开发托管网络应用程序的平台,它使用了谷歌数据管理中心的云计算技术,能跨越多个服务器和数据中心来虚拟化应用程序,可用任意编程语言构建可扩展的网页后端和移动后端,目标旨在开放的云端平台上打造新型的网页和移动应用。App Engine 支持 Node.js、Java、Ruby、C#、Go、Python 和 PHP,开发者只需轻松编写代码即可。

Google Protocol Buffer( 简称 Protobuf) 是 Google 公司内部的混合语言数据标准,用于 RPC 系统和持续数据存储系统,Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化(序列化),它很适合做数据存储或 RPC 数据交换格式,可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。目前Google Protocol Buffer提供了 C++、Java、Python 三种语言的 API。

猜测琢磨

早前,我不经意间发现,谷歌GAE应用对每个HTTP请求的响应中都包含了一个”X-Cloud-Trace-Context”的头信息,由此,我先假设任何返回该头信息的网站都可能在GAE上运行。

幸运的是,后来我了解到 appengine.google.com 也运行在GAE上,但它却能执行一些通用用户和其它环境应用中不能执行的操作,因此,我尝试着来看看它是如何来执行这些操作的。很明显,它肯定是调用了一些API、接口或谷歌自身应用之类的东西来实现这些操作的,但我想应该总有一种方法来一窥究竟的。

一开始,我了解了GAE app应用的内部操作机制,比如日志写入或OAuth令牌获取,之后,我发现在Java 8环境中,它会向谷歌位于亚马逊DNS服务器地址http://169.254.169.253:10001/rpc_http上的某个内部HTTP服务端,发送二进制线型格式(wire format)的谷歌内部Protocol Buffer混合语言数据消息(PB 消息),其涉及到的HTTP请求大致如下:

POST /rpc_http HTTP/1.1
Host: 169.254.169.253:10001
X-Google-RPC-Service-Endpoint: app-engine-apis
X-Google-RPC-Service-Method: /VMRemoteAPI.CallRemoteAPI
Content-Type: application/octet-stream
Content-Length: <LENGTH>
<PROTO_MESSAGE>

而这些PB消息则是一些 “apphosting.ext.remote_api.Request” 请求消息,它具备以下几个属性:

service_name = 要调用的API名称

method = 要调用的API方法名称

request = 内部的PB请求消息字节 (由二进制线型格式编码)

request_id = 安全票据(随每个GAE请求一起提供给app),标记为可选,但是却是必需的安全验证方式

HTTP请求的响应与表示API响应或错误消息的PB消息相对应。

Java 8环境下的安全票据可以通过以下代码行来进行获取:

import com.google.apphosting.api.ApiProxy;
import java.lang.reflect.Method;
Method getSecurityTicket = ApiProxy.getCurrentEnvironment().getClass().getDeclaredMethod("getSecurityTicket");
getSecurityTicket.setAccessible(true);
String security_ticket = (String) getSecurityTicket.invoke(ApiProxy.getCurrentEnvironment());

可以大概描述一个过程示例,比如我要获取这个 “https://www.googleapis.com/auth/xapi.zoo” 测试范围的谷歌认证令牌,那么会涉及以下几个步骤:

1 生成一个apphosting.GetAccessTokenRequest请求消息,其中包含:

scope = ["https://www.googleapis.com/auth/xapi.zoo"]

2 生成一个apphosting.ext.remote_api.Request请求消息,其中包含:

service_name = “app_identity_service” (API提供的GAE服务账户)

method = “GetAccessTokenRequest”

request = 以二进制线型格式编码的,前一步骤中生成的PB消息字节

request_id = Security ticket

3 发送HTTP请求

4 解码”apphosting.GetAccessTokenResponse“响应信息

由于这种Java 8环境中,可以通过这个被发送了PB消息的内部服务端来访问到谷歌的一些内部资源,我猜想该服务端肯定和 “appengine.google.com” 的内部操作相关,但烦心的是我还暂时未在该服务端中发现某些蛛丝马迹。

测试验证

起初,我认为整个PB消息和HTTP请求交互过程,可能还用到了谷歌在亚马逊DNS服务器地址169.254.169.253中的其它服务端,所以,我还向GAE服务中上传了一份静态的Nmap程序来扫描探测169.254.169.253的各种端口,因为Nmap要在GAE中运行,所以我以app方式进行了上传,由于其它的文件系统是只读权限,因此需要运行时,我就把它复制到/tmp目录下赋予执行权。这是一个Nmap运行实例

Scanning all ports on 169.254.169.253
Starting Nmap 6.49BETA1 ( http://nmap.org ) at 2018-05-22 08:41 GMT
Cannot find nmap-payloads. UDP payloads are disabled.
Nmap scan report for 169.254.169.253
Host is up (0.00022s latency).
Not shown: 65533 closed ports
PORT      STATE SERVICE
4/tcp     open  unknown
10001/tcp open  unknown
Nmap done: 1 IP address (1 host up) scanned in 16.30 seconds

之后,经过扫描我发现169.254.169.253上开放了4个端口,它返回了一堆奇怪的杂乱数据,但它有一些清晰的字符串,经在线对比查找,我发现这是一个gRPC服务

我尝试创建一个运行于GAE上的Java gRPC客户端,但是,貌似由于我的gRPC库文件不太完整,而且当我之后再次上传后,它还是使用原来的内置库,所以我只好在GAE上创建了一个C++的gRPC客户端。

经过几番测试和错误响应之后,我发现gRPC服务就像HTTP服务端一样,在它上面运行有一个名为”apphosting.APIHost“的API接口。而且它有所不同,如对PB消息的编码方式,除了二进制编码外还有可选的JSON编码,所以,对测试来说这就相对简单了。以下是该C++ gRPC客户端的运行实例:

gRPC client - GET parameters:
api = API to call (i.e. app_identity_service)
method = Method to invoke (i.e. GetAccessToken)
req = JSON representing the request (i.e. {"scope":["https://www.googleapis.com/auth/xapi.zoo"]})
setPb = 1 (Set the PB field), 0 (Do not set the PB field)
error: 0
error_message: ""
pb: "{\"default_gcs_bucket_name\":\"save-the-expanse.appspot.com\"}"
cpu_usage: 0
gRPC client exit code: 0

一番折腾,围绕亚马逊DNS服务器169.254.169.253确实找不到其它有用信息,我猜想”appengine.google.com” 中执行的操作,可能会与其它内部的不同服务器相关,或者是使用了RPC服务(HTTP/gRPC)来实现了隐藏API接口或方法的调用。为此,我用上传的Nmap应用尝试去发现其它服务器,但最终我只发现了没啥用的谷歌元数据服务器(Metadata server),所以,此时我更倾向于猜测它肯定是用到了一些隐藏API接口或方法的调用,但是,问题来了,怎么来发现它们呢?

去发现隐藏API接口/方法

首先,我从.JAR文件中提取的.CLASS文件以及运行时的一些二进制文件中,来尽可能地收集各种Google Protocol Buffer定义和相关隐藏API线索信息(如果你对此感兴趣,可参考我整理的一些PB定义文件)。

几经分析,我发现这个”apphosting/base/appmaster.proto“文件反映的信息非常有用,它包含了几条PB信息,貌似是用来修改App Engine内部设置的一些内部方法,而且,该文件中还包含一个名为”AppMaster” 且有多种方法定义的API接口,但是,经过测试,我却还是无法发现对这些方法的调用方法。

由于未在PB消息定义中找到任何隐藏API接口和方法,我只有把关注点转向其它方面。我试着去看看那些生成的二进制数据,它们非常庞大而且难于理解,我只能笨拙地用 strings + grep 方法组合查找,老实说我的逆向基础非常差劲。但是,就在我检查主要的二进制数据块 “java_runtime_launcher_ex” 时,我发现它其中包含了很多命令行参数,我就想看看它在GAE环境下,它会接收到一些什么参数呢?

起初,由于我想尽量把每个JAVA变量对应的参数都连接测试一遍,所以要发现接收参数非常之难,几乎是不可能的事。于是乎,我采取了更灵活的方法:用C++创建一个JAVA库,用来读取传递给Java launcher的实参并把它们进行返回。好在Stack Overflow论坛的这篇帖子帮了我大忙,我能非常上手的写出以下读取实参的代码段:

int argc = -1;
char **argv = NULL;
static void getArgs(int _argc, char **_argv, char **_env) {
  argc = _argc;
  argv = _argv;
}
__attribute__((section(".init_array"))) static void *ctr = (void*) getArgs;

之后,我又把这些读取到的实参转化成了JAVA数组,这里的在线结果为其运行实例。

在运行了以上方法之后,我得到了很多有用的参数,其中就包括以下这个(为了方便阅读,我把它们进行了分行显示):

--api_call_deadline_map=
  app_config_service:60.0,
  blobstore:15.0,
  datastore_v3:60.0,
  datastore_v4:60.0,
  file:30.0,
  images:30.0,
  logservice:60.0,
  modules:60.0,
  rdbms:60.0,
  remote_socket:60.0,
  search:10.0,
  stubby:10.0

从上述参数可以发现,之前用过的一些API接口,比如日志写入的 “logservice”,所以我推断这些API接口都可被谷歌的内部HTTP服务端调用。另外,我还注意到名为”stubby”的API服务,它在我之前看过的谷歌网络可靠性工程(SRE)说明中被提到过,它是一个RPC架构服务,在此,它可能是 “appengine.google.com” 用来执行内部操作的一种方式。

太好了,现在我基本摸清了该过程中涉及的隐藏的谷歌内部API名称了,但是,它又存在哪些隐藏的调用方法呢?我试着用C++ gRPC客户端测试了几种方法名,但是,它们都返回了不存在的错误响应消息。所以,我只好继续用Google查询搜索相关引用,之后,我发现一篇2010年的群组帖子提到一个谷歌的内部错误消息:

The API call stubby.Send() took too long to respond and was cancelled.

所以,我试了试Send方法,但错误消息提示该方法不存在。

我可以肯定的是,Send方法一定存在,只是我一时无法访问或方法不对而已,而且有些错误消息也可能是误报。之后,我的测试结果就反复在一个“不存在”的错误消息(示例)和一个误报消息(示例)中徘徊,再之后,我又发现:如果我在gRPC客户端中构造一个未设置”apphosting.APIRequest.pb“区域的请求,它对自认为不存在的方法会返回显示一个”not-exist” 错误消息, 且对那些即使假定不存在,但却真实发生调用的方法也会返回显示一个”incomplete request”的错误消息。由此综合前面的帖子判断,我想,stubby.Send方法肯定是存在的。

那怎么来访问这种stubby.Send方法呢?我始终想不出一种在GAE生产部署环境中来访问stubby.Send方法的途径,但后来我琢磨了一会,我想到在我之前发现的谷歌某服务依赖配置漏洞中,我能利用漏洞访问到谷歌GAE中的模拟(staging)环境staging-appengine.sandbox.googleapis.com和测试环境test-appengine.sandbox.googleapis.com,通常来说,普通用户是不具备这种访问权限的。通过再次对谷歌GAE的模拟和测试环境进行研究之后,我终于对其中的app调用机制有了大概了解:

1 先上传一份包含手动扩展的App应用实例到appengine中 ,但出于某种奇怪原因,它返回了403 Forbidden消息;

2 发起一个对 “www.appspot.com” 的请求,在请求消息中把主机头信息更改为:

 ”<PROJECT-NAME>.prom-<qa/nightly>.sandbox.google.com”

3 如果App应用实例能正常在appengine中运行,如访问 “save-the-expanse.appspot.com” 这样,那么就把appengine中的 “<PROJECT-NAME>” 替换成你的预置名称,如这里是”save-the-expanse”;如果你把你的App实例上传到了模拟的GAE环境中,那么,应该把”<qa/nightly>”替换成”qa”;如果上传到的是测试的GAE环境中,那么应该把 “<qa/nightly>” 替换成  “nightly” 。

如我曾用  “the-expanse.prom-nightly.sandbox.google.com” 的域名实例进行了测试。(其中没有save,因为美剧《The Expanse》后期没被取消,所以我没加上save)

The Expanse :美剧《太空无垠》,为美NBC旗下Syfy频道的力作,在5月初,因版权问题被限制之后被Syfy取消发行该剧第四季,该剧粉丝随即在社交媒体上发起了#savetheexpanse的行动。之后,媒体报导正筹划购买 The Expanse 最新版权,最终发行没被取消。本文作者是该剧的忠实粉丝。

看我如何发现价值3万6千美金的谷歌RCE漏洞

漏洞测试

当我用gRPC客户端上传App应用实例时,我发现,在GAE的模拟/测试环境(staging/test) 下,我竟然能访问到”stubby.Send”方法!

经过一些快速测试和错误消息排除之后,我总算明白了如何执行一次简单的Stubby调用:

1 用以下JSON格式的PB消息来调用 “stubby.GetStubId”方法:

{
  "host": "<HOST>"
}

其中的<HOST>则是一些你想调用方法的具体托管网站,如”google.com:80″, “pantheon.corp.google.com:80″, “blade:monarch-cloud_prod-streamz”,这里的”blade:<SERVICE>” 则是谷歌的内部DNS名称的表示,如”blade:cloudresourcemanager-project” 这个内部谷歌DNS,其外部名称为”cloudresourcemanager.googleapis.com” ;而有些内部DNS是没有对应外部名称的,如”blade:monarch-cloud_prod-streamz”。

2 结合隐藏API/方法发现中,对App应用实例的请求发起后,GAE系统会返回一个存储有”stub_id”值的JSON格式PB消息;

3 然后用以下JSON格式PB消息调用方法”stubby.Send”:

{
  "stubby_method": "/<SERVICE>.<METHOD>",
  "stubby_request": "<PB>",
  "stub_id": "<STUB_ID>"
}

要查找 “stubby_method” 的值,可以像我这样把这里实例中的 “/ServerStatus.GetServices“方法带有的”stubby_request” 值设置为空,之后,会返回一个 “rpc.ServiceList” 服务列出串,其中包含了各个服务对应的方法。这里的<PB>是PB消息的线型格式二进制字节。

4 如果成功,调用机制会返回一个带有 “stubby_response” 的JSON格式PB消息,其中也包含了响应的PB消息线型格式二进制字节。

发现了这些,我很快作了一些测试,但还是发现不了任何stubby方法调用的异常之处。唉,不管了,我头都大了,现在,从以上返回的各种服务和方法来看,至少可以说明谷歌内部服务存在泄露问题。因此,我考虑先把这个问题上报给谷歌安全团队处理,还不错,最后,还获得了谷歌团队P1的Bug响应处理级别。

RCE漏洞挖掘- 发现隐藏API/方法

有了上述漏洞初报,我又重新整理了一下我的整个测试思路,并尝试继续发现一些可用于攻击的变量,之后,我注意到,除了”stubby”方法之外 ,在之前提取的JAVA launcher二进制数据的实参中,还存在一个名为 “app_config_service” 的方法,其中存在一个隐藏的API接口,Wow!

这个方法是在之前的PB消息定义查找和谷歌搜索中,没有直接被发现的。后来,我又在 “apphosting/base/quotas.proto” 中发现了它的踪迹。例如这里提到过过”APP_CONFIG_SERVICE_GET_APP_CONFIG“,我经过一些简单测试,确实发现 “app_config_service.GetAppConfig” 属于隐藏的调用方法。

这个隐藏的 “app_config_service” 中存在一些有意思的方法调用,但我认为最奇妙的要属 “app_config_service.ConfigApp” 和 “app_config_service.SetAdminConfig” 这两个方法调用,因为我能利用它们进行一些内部设置,如邮件的允许发件人、App应用的服务账户ID、权限分配忽略,甚至是能把我自己的App应用实例修改为”SuperApp“(超级App,我不知道如何来表示,反正就是权限很高很超级),而且还能给予 “FILE_GOOGLE3_ACCESS“的赋权,Google3估计是谷歌版本控制系统Piper的一部分,通过它把文件与谷歌的API和服务相关联。

方法”app_config_service.SetAdminConfig”中则包含 “apphosting.SetAdminConfigRequest“的请求消息;方法”app_config_service.ConfigApp”中则包含了 “apphosting.GlobalConfig“  的请求消息。

另外,围绕 “apphosting/base/quotas.proto” 的分析,我还发现了其它的API和方法( APIs/methods),比如 “basement.GaiaLookupByUserEmail“。

有了这些后续发现,我又把它们整理成型,迅速上报给了谷歌安全团队,让我惊讶的是,谷歌安全团队看到了我的问题报告之后,立即升级了这个问题的内部处理优先级,并向我发出停止测试的邮件警告:

Please stop exploring this further, as it seems that you could easily break something using these internal APIs.

劳请阁下停止深入测试,你正在利用的API接口可能会对我们的系统造成破坏。

我注意到,谷歌安全团队发给我的这封邮件还转发给了谷歌其他的一些内部项目负责人员:看我如何发现价值3万6千美金的谷歌RCE漏洞我当然只好收手了。几天之后,针对GAE非生产环境上述API和方法的调用访问都被谷歌作了拦截处理,并会返回错误消息页面,如果发起大量请求,则返回429的错误状态码:看我如何发现价值3万6千美金的谷歌RCE漏洞

收获$36,337赏金

又过了几天,我收到了谷歌安全团队发来的以下邮件,邮件中表示,经过评定,我的漏洞可以被奖励 $36,337美金!哇…..:看我如何发现价值3万6千美金的谷歌RCE漏洞

当时,我有点不敢相信。后来我才知道,按照谷歌的漏洞奖励规则,我的这些发现被谷歌方面评定为最高漏洞级别的远程代码执行漏洞(RCE),其对应奖金为 $31337,之后又加上了谷歌网站可靠性工程方面的$5000美金奖励,一共 $36337,真是太意外了!看我如何发现价值3万6千美金的谷歌RCE漏洞

漏洞上报进程

2018.2月:   发现漏洞

2018.2.25:  对涉及”stubby” API的漏洞初报

2018.3.4-2018.3.5:  对涉及 “app_config_service” API 的漏洞初报

2018.3.6- 2018.3.13:  谷歌对相关API作了拦截封堵

2018.3.13:  谷歌向我确认$36,337美金的赏金

2018.3.16:  谷歌方面修复了漏洞

后记

该谷歌的大奖漏洞writeup一经发布,就在国外多个技术社区引起强烈反响,尤其是18岁的漏洞发现者Ezequiel Pereira,此前在上高中时就曾四次发现了谷歌重要漏洞,并分别囊获了谷歌奖励的$500美金、$5000美金、$7500美金和$10000美金。在社区讨论中,Ezequiel Pereira 也透露自己家境辛苦,从小父母离异,其母亲一人把他们三姐妹拉扯大,而母亲一年的收入才有一万多美元,所以,为了缓解家庭负担,懂事的Ezequiel Pereira平常也靠挖洞贴补家用。有网友建议Ezequiel,凭他的技术可以直接申请谷歌的实习生。

*参考来源:google,FreeBuf 小编 clouds 编译,转载请注明来自 FreeBuf.COM

这些评论亮了

  • 周红衣 回复
    国内50京东E卡,不能再多了
    )11( 亮了
Loading...

特别推荐

推荐关注

活动预告

填写个人信息

姓名
电话
邮箱
公司
行业
职位
css.php