有些 Serverless 实现在应用程序生命周期结束之后,程序文件的清理上进入了僵局。一方面开发者希望借助容器 “对 Linux Cgroup 和 Namespace 进行管理的特性” 用于实现限制应用的资源访问能力和进程权限的需求;在此之上,开发者希望能更快的达到用户文件清理的目的,避免反复初始化容器环境带来的时间和资源上的消耗,复用同一个容器环境。
而在蓝军的视角里,这样的处理方式会导致多个用户的应用会存在多个用户在不同时间段使用一个容器环境的情况,在安全性上是比较难得到保障的。
以面向 Python 开发者的 Serverless 架构为例,开发者所构想的简化模型是这样的:
用户文件清理的代码实现上,简化可参考:
在进行包括上述文件删除动作在内的一系列环境清理工作之后,容器内外的主调度进程会写入其他租户的代码到当前容器内,此时这个容器就进入了下一个应用的 Serverless 生命周期。
虽然,主框架代码实现内有很多类似调用系统命令拼接目录等参数进行执行的代码实现,但是类似命令注入的问题大多只能影响到当前的生命周期;而又因为用户权限的问题,我们没办法修改其他目录下的文件。
于是我们构建了这样一个目录和文件:
当程序执行 rm -rf * 时,因为 bash glob 对 * 号的返回是有默认排序的,这里可参考下方 bash 的文档,只要我们不去修改 LC_ALL 的环境变量,我们构造的 –help 文件会排列在文件列表的最前方,导致 rm 会执行 rm –help 而终止,恶意文件得以保留。
Pathname Expansion
After word splitting, unless the -f option has been set, bash scans each word for the characters *, ?, and [. If one of these characters appears, then the word is regarded as a pattern, and replaced with an alphabetically sorted list of file names matching the pattern.
我们可以简单在 bash 内进行验证,可以看到 rm -rf * 命令被 –help 强行终止,我们所植入的恶意文件还依然存在没有被清理掉,同时 rm –help 的命令执行返回为 0,不会产生 OS ERROE Code,清理进程会认为这里的清除命令已经成功执行:
而实际在 serverless log 里的返回,可参考:
此时,serverless 的主调度程序会以为自己已经正常清理了容器环境,并写入另外一个租户的源码包进行执行,而当另外一个租户的代码执行至 import requests 时,我们驻留在应用目录下的 requests.py 内的恶意代码就会被执行。
不过值得注意的是,因为 serverless 的生命周期一般极为有限,所以此时获取的 shell 可能会在短时间结束,触发新一轮的反弹 shell,且 Servless 容器环境内的信息相对单一和简便。所以容器环境里值得我们探索和翻找的地方也不多,一般需要关注:]
新的代码
代码内部配置
环境变量
秘钥、证书、密码信息等
不同的 serverless 架构实现对于存储和传递相应信息的方式各有不同。