原文:https://sites.google.com/site/testsitehacking/-36k-google-app-engine-rce
简介
在2018年初,我接触到了一个非生产型的Google App Engine部署环境,在那里,我可以尽情鼓捣各种内部API,经过一番折腾后,竟然找到了一个得到Google承认的远程代码执行漏洞。为此,我获得了Google漏洞奖励计划颁发的36,337美元奖金。
提示
您可以尝试运行文中Google App Engine应用所涉及的一些概念的示例代码。您可以找到该应用程序的源代码,包括gRPC C++ 客户端的源代码和各个Protocol Buffer的定义,这些都可以从本人提供的GitHub存储库中找到。
不久之前,我注意到每个Google App Engine(GAE)应用程序都使用“X-Cloud-Trace-Context”头部来响应所有HTTP请求,所以,我觉得任何返回该头部的网站都可以在GAE上运行。
在这一想法的指引下,我发现“appengine.google.com”本身就是运行在GAE上的,但是,它却可以执行一些无法在其他地方完成的操作,以及一些普通用户应用程序无法执行的操作,这极大地激发了我的好奇心,所以,我决定搞清楚这到底是咋回事。
显然,它必须使用一些API、接口或者只有谷歌自己运行的应用才可以使用的东西,并且,也许我们还可以通过某种方式来访问它们,这正是我们要探索的。
首先,在考察GAE应用程序是如何执行内部操作(例如写日志或获取OAuth令牌)之后,我发现,在Java 8环境中,这些操作都是通过向位于http://169.254.169.253:10001/rpc_http的内部HTTP端点发送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请求一起提供给应用程序),虽然它被标记为可选,但必需提供
至于这个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”范围(测试范围,没有实际使用)内使用的Google OAuth令牌,具体步骤如下所示:
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 = 安全票据
3.发送HTTP请求
4.对响应消息进行解码,该响应内容应该是“apphosting.GetAccessTokenResponse”消息
由于这个端点可以访问一些内部的东西,所以,我相信它与完成内部操作的“appengine.google.com”域肯定有关,遗憾的是,我在这个HTTP端点中没有找到任何有用的东西。
最初,我觉得它可能使用了位于同一服务器(169.254.169.253)中的其他端点,因此,我上传了一个静态链接版本的Nmap到GAE,并在该服务器上运行它(为了在GAE中运行二进制文件,我将其与应用程序一起上载,然后在运行时,将它们复制到/tmp目录中,并赋予它们执行权限——因为文件系统的其余文件都是只读的)。具体的例子,请参考这里。
我发现,端口4是开放的,所以,我向该端口发送了一些东西。之后,它回复了一堆奇怪的数据,不过,其中也有一些可识别的字符串,在搜索引擎的帮助下,我发现这是一个gRPC服务。
我曾经尝试构建一个在GAE上运行的Java gRPC客户端,但是遇到了一个问题:一方面内置的gRPC库似乎不完整,另一方面,每当我上传一个完整的gRPC库后,它仍“固执地”使用内置的库。
所以,我构建了一个C++客户端,并在GAE上运行它。
经过反复试验之后,我发现gRPC服务就像HTTP端点一样,也运行了一个“apphosting.APIHost” API。当然,两者还是有所差异的,比如在PB消息的编码方面,它不仅提供了二进制编码选项,还提供了JSON编码选项,因此,它在测试方面要更容易一些。
对于该客户端,这里提供了一个实例。
由于在该服务器中没有发现其他东西,因此,我假定“appengine.google.com”在内部执行的操作,要么是借助其他服务器完成的,要么就是使用RPC服务(HTTP/gRPC)调用了某些隐式的API/方法。
于是,我通过Nmap查找与其有关的服务器,但只找到了元数据服务器,很明显,它不可能完成上述的操作,所以,我认为它肯定使用了隐式的API,但问题是——如何找到它们呢?
首先,我收集了所能找到的所有Protocol Buffer的定义(这些可以从.JAR文件中找到的.CLASS文件以及在运行时找到的二进制文件中提取),并在其中搜索任何可能指向某些隐式API的Protocol Buffer定义(如果读者有兴趣的话,可以从这里下载我提取到的所有PB定义)。
在“apphosting/base/appmaster.proto”文件中,含有几个PB消息,看起来像是修改App Engine内部设置的内部方法,还有一个名为“AppMaster”的API,其中定义了一些方法,这些都是我们所感兴趣的——但是,经过一番尝试之后,我仍然没有找到正确调用这些方法的途径。
由于在PB定义中没有找到任何隐式的API/方法,所以我不得不到其他地方寻找。
于是,我将搜索目标转移到二进制文件上面,问题是它们过于庞大,并且里面充满了无用或者无法理解的东西(我是通过字符串+grep来完成搜索的,因为我对逆向工程还不太熟悉),后来,我在一个主要的二进制文件即“java_runtime_launcher_ex”中发现了多命令行参数,这给了我很大的启发:何不考察在GAE环境中运行时会收到哪些参数呢?
刚开始的时候,我获取参数的方法是非常费劲的,因为需要将每个可以找到的Java变量与相应的参数联系起来,这几乎是不可能完成的任务。
然后,我尝试了一些更聪明的方法:用C++创建一个Java库,并使用一个方法来读取传递给启动程序的参数,然后将其返回。
这种获取参数的方式明显要轻松多了,这是我从一个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”(用于写日志),所以我推断这些都是可以通过内部HTTP端点使用的API。
此外,我还注意到了“stubby”,之前在某些Google产品的错误消息中见过这个消息(当遇到bug时),并且在SRE中也读过这方面的东西,所以我判断这是一个RPC的基础结构,并且可能是“appengine.google.com”执行内部操作的一种方式。
太棒了,现在终于知道一个内部API的名称了,但是,它提供了哪些方法呢?
我用C++ gRPC客户端尝试了几个方法名,但是它们都返回了一个错误,说这些方法并不存在,所以,我开始借助Google进行搜索。
后来,我发现了一篇写于2010年的文章,它指出:
The API call stubby.Send() took too long to respond and was cancelled.
所以,我开始尝试“Send”方法,但系统指出该方法并不存在。
我相信该方法肯定是存在的,这里的错误消息只是为了隐藏了它存在的事实,同时,现在我仍然无法访问它。
为此,我试着通过寻找访问确实“不存在”的方法时返回的错误消息(示例)与为了掩盖真实存在的方法而返回的错误消息(示例)之间的区别来验证上面的判断,并且发现:当从我的gRPC客户端中发送一个未设置"apphosting.APIRequest.pb"字段(它被标记为可选的,但我总是至少将它设置为一个空字符串或"{}")的请求的时候,系统会为并不存在的方法(示例)返回一则“not-exist”错误消息,而对于一个实际存在的方法(示例),系统则会返回一则“incomplete request”错误消息。通过这种方式,我判断出“stubby.Send”方法确实是存在的。
现在的问题是,如何才能访问它呢?
我不知道在生产性的GAE部署环境中访问它的方式,但我知道,利用某个漏洞(通常,普通的Google用户无法访问非生产性的部署环境),我可以访问staging(staging-appengine.sandbox.googleapis.com)和测试(test-appengine.sandbox)性的GAE部署环境。
对这两种部署环境进行一番研究之后,我找到了调用在其中运行的应用程序的方法:
1.上传一个缩放类型为手动缩放的版本(否则无法正常运行,并返回403 Forbidden)
2.向“www.appspot.com”发送请求,并将Host头部改为“<project-name>.prom-<qa/nightly>.sandbox.google.com”</project-name>
如果您的应用在“save-the-expanse.appspot.com”上运行,则应该用“save-the-expanse”替换“<project-name>”;如果您要将应用上传到staging GAE环境,则应该用“qa”代替“<qa/nightly>”;如果要把该应用上传到测试GAE环境的话,则应该将"<qa/nightly>"换为 "nightly"。</project-name>
例如:我是在“the-expanse.prom-nightly.sandbox.google.com”上进行的测试(没有“保存”,因为当时The Expanse还没有被撤销)。
漏洞详情
上传好这个应用程序后,我很快就发现,在非生产(staging/测试)性的GAE环境中,我竟然可以访问“stubby.Send”方法!
经过一番快速测试(主要是阅读错误消息并猜测如何解决这些问题)后,我发现了进行简单的Stubby调用的方式:
1.使用以下JSON PB消息调用“stubby.GetStubId”方法:
{
"host": "<HOST>"
}
将'<host>'设置为要调用的方法所在的位置(例如,“google.com:80”,“pantheon.corp.google.com:80”,“blade:monarch-cloud_prod-streamz”)。</host>
“blade:<service>”似乎就像Google使用的内部DNS系统,例如,“blade:cloudresourcemanager-project”在其内部就是“cloudresourcemanager.googleapis.com”(有点像“blade:monarch-cloud_prod-streamz”,但是没有外部的对应物)。</service>
2.前一个请求将返回一个JSON PB消息,其中“stub_id”是其唯一的字段,用于存储相应的值。
3.通过以下JSON PB消息调用“stubby.Send”方法:
{
"stubby_method": "/<SERVICE>.<METHOD>",
"stubby_request": "<PB>",
"stub_id": "<STUB_ID>"
}
为了搞清楚“stubby_method”的可能取值,可以使用空的“stubby_request”将其设置为“/ServerStatus.GetServices”,这样就会返回一个“rpc.ServiceList”,从而列出目标系统支持的所有服务。
<pb>是PB消息字节数据(采用二进制线路层格式)。</pb>
4.如果成功的话,该调用将返回以“stubby_response”作为其唯一字段的JSON PB消息,其中存放响应PB消息的相关字节(采用二进制线路层格式)。
此后,我进行了一些测试,并没有发现会导致安全隐患的Stubby调用。
不过,我仍然向谷歌汇报了这个问题,它们将这个问题的优先级定为P1。
在报告这个问题之后,我又重新进行了回顾,试图找到可以成功用于攻击的一些变体,我注意到,除了“stubby”之外,通过Java启动程序二进制文件中得到的参数中,还有一个名为“app_config_service”的参数,它实际上也是一个隐式的API。
通过查看之前得到的PB定义,并没有发现这个隐式的API的方法,此外,也没有在Google搜索中找到它们,但后来从“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”,因为它们可用来完成内部设置,例如设置电子邮件发件人、应用程序的服务帐户ID、忽略配额限制,甚至可以将自己的应用程序设为“SuperApp”(我不知道这意味着什么,但听起来很牛掰),并赋予其“FILE_GOOGLE3_ACCESS”权限(我认为gooogle3是Piper的一部分,存放与gooogle API和服务相关的文件)。
“app_config_service.SetAdminConfig”方法使用“apphosting.SetAdminConfigRequest”作为其请求消息,“app_config_service.ConfigApp”方法使用“apphosting.GlobalConfig”作为其请求消息。
通过"apphosting/base/quotas.proto",我还发现了其他一些API/方法,如“basement.GaiaLookupByUserEmail”,等等。
之后,我向Google提交了这些新发现,他们提高了处理这些问题的优先级,并回复道:
请停止进一步的探索,因为您似乎可以轻松地使用这些内部API来破坏一些东西。
同时,这个安全问题被抄送给了几名员工:
几天后,访问非生产性GAE API和环境时将被阻止,并返回一个错误页面(状态码为“429 Too Many Requests”)。
您仍然可以在“staging-appengine.sandbox.googleapis.com”和“test-appengine.sandbox.googleapis.com”中看到如下所示的消息。
后来,我收到以下信件:
我得到了36,337美元的奖励!
直到那时我才意识到,这个安全问题被定性为远程代码执行漏洞(最危险的安全漏洞),这真是太让人惊喜了。
我向其中一位Google员工咨询了奖励金额问题,得到的回复是,部分奖金是为RCE漏洞支付的(请阅读SRE,RCE漏洞奖金为31,337美元),而额外的$5k则是为另外一个安全漏洞提供的奖金。
时间线
2018年2月:发现安全问题
2018年2月25日:初次报告(仅限“stubby”API)
2018年3月4日、5日:发现并报告了“app_config_service”API
2018年3月6日至13日:访问非生产性GAE环境时会被429错误页面所阻止
2018年3月13日:奖励36,337美元
2018年5月16日:确认并修复漏洞
作者联系方式:
Email: [email protected]