来源于微信公众号:FreeBuf 2019年1月,由于默认安装的服务snapd API中的一个bug,通过默认安装的Ubuntu Linux被发现存在特权提升漏洞,任何本地用户都可以利用此漏洞直接获取root权限。 概述首先在此提供dirty_sock代码仓库中两个有效的exploit:
两者都对默认安装的Ubuntu有效。大部分测试是在18.10版本完成的,不过旧版本也受改漏洞影响。值得一提的是,snapd团队对此漏洞回应迅速且处理妥善。直接与他们合作也是非常愉快。 snapd提供了附加到本地UNIX_AF socket的REST API,通过查询与该socket连接的关联UID来实现对API的访问控制。在for循环进行字符串解析的过程中,用户可控的socket数据可以覆盖UID变量,从而允许任何用户访问任何API函数。而通过访问API,有多种方法可以获取root权限,上面链接的exploit就展示了两种可能性。 背景:什么是snap?为了简化Linux系统上的打包应用程序,各种新的竞争标准纷纷出现。作为其中的一个发行版,Ubuntu Linux的开发商Canonical也在推广他们的“Snap”,类似于Windows应用程序,snap将所有应用程序依赖项转换为单个二进制文件。 Snap生态包含一个“应用商店”,开发人员可以在其中发布和维护即时可用的软件包。 本地的snap和在线商店的通信部分由系统服务“snapd”处理。此服务自动安装在Ubuntu中,并在“root”用户的上下文中运行。Snapd正在发展成为Ubuntu操作系统的重要组成部分,特别是在用于云和物联网的“Snappy Ubuntu Core”等更精简的发行版中。 漏洞总览有趣的Linux操作系统信息snapd服务在位于/lib/systemd/system/snapd.service的unit文件中被描述。 以下是前几行: [Unit] 顺着这个我们找到systemd socket unit文件,位于/lib/systemd/system/snapd.socket,其中提供了一些有趣的信息: [Socket] Linux通过称为“AFUNIX”的socket在同一台机器上的进程之间进行通信。“AFINET”和“AF_INET6”socket则用于通过网络连接的进程通信。上面显示的内容告诉我们系统创建了两个socket文件。'0666'模式则为所有人设置文件读写权限,只有这样才可以允许任何进程连接并进行socket通信。 我们可以通过文件系统在查看这些socket文件: $ ls -aslh /run/snapd* 我们可以通过Linux中的nc工具(只要是BSD风格)连接到像这样的AF_UNIX socket。以下是一个示例。 $ nc -U /run/snapd.socket 碰巧,攻击者在入侵计算机后要做的第一件事就是查找在root上下文中运行的隐藏服务,HTTP服务器是利用的主要目标,而它们通常与网络套接字有关。 现在我们知道有一个很好的利用目标 - 一个隐藏可能没有被广泛测试的HTTP服务。另外,我正在开发一个提权工具uptux,该工具可识别出此漏洞。 存在漏洞的代码作为一个开源项目,我们利用源代码继续进行静态分析。开发人员提供了有关此REST API的文档。 对于利用而言,一个非常需要的API函数是“POST/v2/create-user”,简称为“创建本地用户”。文档告诉我们这个调用需要root权限才能执行。那么守护进程究竟是如何确定访问API的用户是否已经拥有root权限? 顺着代码我们找到了这个文件,现在来看这一行: ucred, err := getUcred(int(f.Fd()), sys.SOL_SOCKET, sys.SO_PEERCRED) 这是调用golang的标准库之一,用来收集与套接字连接相关的用户信息。基本上,AF_UNIX socket系列有一个选项,可以在附加数据中接收发送过程的凭据(请参阅Linux命令行中的man unix)。这是确定访问API的进程权限的一种相当可靠的方法。 通过使用名为delve的golang调试器,我们可以确切地看到上文执行“nc”命令时返回的内容。下面是在此函数中设置断点时调试器的输出,然后使用delve的“print”命令来显示变量“ucred”当前包含的内容: > github.com/snapcore/snapd/daemon.(*ucrednetListener).Accept() 不错。它知道了我的uid为1000,即将拒绝我访问敏感的API函数。如果程序在这种状态下调用这些变量,那么结果就符合预期了,然而事实并非如此。 其实在此函数中还包含一些额外的处理,其中连接信息与上面发现的值会一起被添加到一个新对象: func (wc *ucrednetConn) RemoteAddr() net.Addr { 这些值被拼接成一个字符串变量: func (wa *ucrednetAddr) String() string { 最后经由函数解析,字符串再次被分解为单个变量 func ucrednetGet(remoteAddr string) (pid uint32, uid uint32, socket string, err error) { 最后一个函数的作用是将字符串用“;”字符拆分,然后查找以“uid =”开头的任何内容。当它遍历完所有拆分时,第二次出现的“uid =”会覆盖掉第一个。 所以如果我们能以某种方式将任意文本注入此函数中... 回到delve调试器,我们可以查看一下“remoteAddr”字符串,看看在实现正确的HTTP GET请求的“nc”连接中它包含了什么: 请求: $ nc -U /run/snapd.socket 调试器输出: github.com/snapcore/snapd/daemon.ucrednetGet() 现在的情况是,我们有一个字符串变量,其中所有变量都拼接在一起,该字符串包含四个元素。第二个元素“uid = 1000”是当前控制权限的内容。 函数将此字符串通过“;”拆分并迭代,如果字符串包含“uid=”),则可能会覆盖第一个“uid =”。 第一个(socket=/run/snapd.socket)是用来监听socket的本地“网络地址”:是服务所定义的绑定文件路径。我们无法修改snapd,也无法让其使用另一个socket名来运行。但是字符串末尾的“@”符号是什么? 这个是从哪里来的?变量名“remoteAddr”给了一个很好的提示。在调试器中费了些周折,我们可以看到golang标准库(net.go)返回本地网络地址和远程地址。你可以在下面的调试会话中看到输出为“laddr”和“raddr”。 > net.(*conn).LocalAddr() /usr/lib/go-1.10/src/net/net.go:210 (PC: 0x77f65f) 远程地址会被设置为神秘的@符号。进一步阅读man unix帮助信息后,我们了解到这与“抽象命名空间”有关,用来绑定独立于文件系统的socket。命名空间中的socket开头为null-byte,该字符在终端中通常会显示为@。 我们可以创建绑定到我们控制的文件名的socket,而不依赖netcat利用的抽象套接字命名空间。这应该允许我们影响想要修改的字符串变量的最后部分,也就是上文的“raddr”变量。 使用一些python代码,我们可以创建一个包含“;uid=0;”字符串的文件名,通过socket绑定该文件,来启动与snapd API的连接。 以下为PoC代码片段: ## 设置包含payload的socket名称 现在再看一下remoteAddr变量,观察调试器中发生的事情: > github.com/snapcore/snapd/daemon.ucrednetGet() 我们注入了一个假的uid 0,即root用户,它会在最后一次迭代中覆盖实际的uid。这样我们就能够访问API的受保护功能。 在调试器中继续观察来验证这一点,并看到uid被设置为0: > github.com/snapcore/snapd/daemon.ucrednetGet() 武器化使用版本一dirty_sockv1利用的是“POST/v2/create-user”这个API函数。要利用该漏洞,我们只需在Ubuntu SSO上创建一个账户,然后将SSH公钥上传到账户目录中,接下来使用如下命令来利用漏洞(使用注册的邮箱和关联的SSH私钥): $ dirty_sockv1.py -u 你的@邮箱.com -k id_rsa 这种方法是非常可靠的,可以安全执行。你可以止步这里并自己尝试获得root权限。 还在看? 好吧,对互联网连接和SSH服务的要求一直在变,我想看看我是否可以在更受限制的环境中利用。这导致我们有了版本二。 版本二dirty_sockv2使用了“POST/v2/snaps” API来侧加载snap,该snap中包含一个bash脚本,可以添加一个本地用户。这个版本适用于没有运行SSH服务的系统,也适用于没有互联网连接的新版Ubuntu。然而,侧加载需要一些核心snap依赖,如果不存在这些依赖,可能会触发snapd服务的更新操作。这个场景下,我发现这个版本仍然有效,但只能使用一次。 snap本身运行在沙箱环境中,并且数字签名需要匹配主机已信任的公钥。然而我们可以通过处于开发模式(“devmode”)的snap来降低这些限制条件,这样snap就能像其他应用那样访问主机操作系统。 此外snap引入了“hooks”机制,其中“install hook”会在snap安装时运行,并且“install hook”可以是一个简单的shell脚本。如果snap配置为“devmode”,那么这个hook会在root上下文中运行。 我创建了一个简单的snap,该snap没有其他功能,只是会在安装阶段执行的一个bash脚本。 该脚本会运行如下命令: useradd dirty_sock -m -p '$6$sWZcW1t25pfUdBuX$jWjEZQF2zFSfyGy9LbvG3vFzzHRjXfBYK0SOGfMD1sLyaS97AwnJUs7gDCY.fg19Ns3JwRdDhOcEmDpBVlF9m.' -s /bin/bash 上面加密字符串只是使用Python crypt.crypt()函数处理“dirty_sock”所创建的文本。 以下命令显示了详细创建此快照的过程,这都是在开发机器上完成的,而不是目标机器。snap创建完毕后,我们可以将其转换为base64文本,以便包含到完整的python利用代码中。 ## 安装必要工具 一旦有了snap文件,我们就可以通过bash将它转换为base64,如下所示: $ base64 <snap-filename.snap> base64编码的文本可以放在dirtysock.py漏洞利用代码开头的全局变量“TROJANSNAP”中。 漏洞利用代码本身是用python中写的,可以执行以下操作:
![]() 预防和补救措施打上补丁,snapd团队在披露后迅速修复了漏洞。 *参考来源:shenaniganslabs,thehackernews,FB小编Covfefe编译,转载请注明来自FreeBuf.COM ![]() ---------------------------------------------------------------------------------------------------------------------- 我们尊重原创,也注重分享,文章来源于微信公众号:FreeBuf,建议关注公众号查看原文。如若侵权请联系qter@qter.org。 ---------------------------------------------------------------------------------------------------------------------- |