Netatalk RCE漏洞分析(CVE-2022-23121 )

keeeee  932天前

image.png

作者:kejaly@白帽汇安全研究院

背景

Netatalk 是一个对 AFP 协议进行实现的著名开源组件,广泛应用于 NAS 上,Netatalk 3.1.13 之前存在多个严重漏洞,多款著名NAS产品受影响。这里对 CVE-2022-23121 这个漏洞进行分析。

Netatalk 介绍

Apple Filing Protocol (AFP) 是一个类似 SMB 的文件传输协议,用于 MAC 上来传输分享文件。但是 AFP 并没有进行开源,为了能让 linux 系统也可以使用 AFP 协议,于是便诞生了 Netatalk 。为了让 Mac 能更方便的使用 NAS 来分享文件,几乎每一个 NAS 都会使用 Netatalk 。 (参考 Your NAS is not your NAS !

Netatalk 官方网站: https://netatalk.sourceforge.io/

github 上的镜像:https://github.com/Netatalk/Netatalk

漏洞简介

这个漏洞 CVE 官网并上没有简介:

image.png

这里简单概况就是, Netatalk 服务端在 处理客户端发起的 openfork 命令去打开 appleDouble 文件的请求 时,对 appleDouble 文件的处理有误,导致内存数据被恶意 ”任意“读 和 ”任意“ 写,最终能造成 RCE。

环境配置

安装

# 这里采用的是 netatalk 3.1.11 版本
https://versaweb.dl.sourceforge.net/project/netatalk/netatalk/3.1.11/netatalk-3.1.11.tar.bz2
sudo apt install -y libcrack2-dev libgssapi-krb5-2 libgssapi3-heimdal libgssapi-perl libkrb5-dev libtdb-dev libevent-dev  libdb-dev

sudo apt-get install -y libgcrypt11-dev libdb-dev libgcrypt-dev
export CFLAGS='-g -O0'    # 用 -O0 ,解决 Clion 动态调试的时候一些bi'a optimized out 的问题。  http://www.cache.one/read/16475645


./configure         --with-init-style=debian-systemd         --without-libevent         --without-tdb         --with-cracklib         --enable-krbV-uam      --enable-debug  --with-pam-confdir=/etc/pam.d         --with-dbus-daemon=/usr/bin/dbus-daemon         --with-dbus-sysconf-dir=/etc/dbus-1/system.d         --with-tracker-pkgconfig-version=1.0  
make 
make install

配置

mkdir /tmp/afp_tmp/
mkdir /tmp/afp_tmp/Public
mkdir /tmp/afp_tmp/test


echo test > /tmp/afp_tmp/test/test.txt
echo hello > /tmp/afp_tmp//Public/hello.txt
chmod 777 -R /tmp/afp_tmp/Public /tmp/afp_tmp/test
gedit /tmp/afp_tmp/afp.conf   # afp.conf 内容如下:
[ Global ]
uam list = uams_guest.so,uams_clrtxt.so,uams_dhx2.so
save password = no
unix charset = UTF8
use sendfile = yes
zeroconf = no
guest account = nobody

[ Public ]
path =/tmp/afp_tmp/Public
ea = auto
convert appledouble = no
stat vol = no
file perm = 777
directory perm = 777
veto files = '/Network Trash Folder/.!@#$recycle/.systemfile/lost+found/Nas_Prog/.!@$mmc/'
rwlist = "admin","nobody","@allaccount"
valid users = "admin","nobody","@allaccount"
invalid users =

[ test ]
path = /tmp/afp_tmp/test
ea = auto
convert appledouble = no
stat vol = no
file perm = 777
directory perm = 777
veto files = '/Network Trash Folder/.!@#$recycle/.systemfile/lost+found/Nas_Prog/.!@$mmc/'
rwlist = "admin","nobody","@allaccount"
valid users = "admin","nobody","@allaccount"
invalid users =

启动

cnid_me tad -d -F /tmp/afp_tmp/afp.conf # 需要先启动 cnid_me tad 

# afpd 启动
afpd -F /tmp/afp_tmp/afp.conf   -d  
#gdbserver :12345 afpd -F /tmp/afp_tmp/afp.conf -d
#gdbserver :12345 --attach ${afp_d_pid}

clion 添加动态调试

然后本地准备一份源码,设置好 Path mappings ,配置一个 Remote Debug ,就可以调试了。

image.png

因为我们关注的是 afpd 的子进程,所以修改 clion 中 gdb 的 .gdbint 为:

set detach-on-fork off
set schedule-multiple on

一些断点

我这边为了方便,在 /etc/afpd/main.c 这下了一个断点:

image.png

如果用 gdbserver 启动 afpd 的话,会直接断到这个位置的:

image.png

继续运行之后,程序就开始等待客户端的请求。

举个例子,使用 nmap 中的 afp-ls 脚本:

nmap  192.168.44.130 -p 548 --sc ript=afp-ls

在 afp_openvol() 函数下断点,就会断到 afp_openvol 函数。


image.png

前置知识

AppleDouble 文件

AppleDouble 简单来说就是 mac 上的一种存储数据的文件。

文件格式

AppleDouble 格式文档下载地址:https://web.archive.org/web/20180311140826if_/http://kaiser-edv.de/documents/AppleSingle_AppleDouble.pdf

AppleDouble 文件可以分为 header 和 data 两部分,header 的结构如下:

image.png

data 则是指 header 中声明的各个 entry 的数据。

其中 entry ID 表示的类型如下:


image.png

本次漏洞用到的是 finder info entry 以及 resource fork entry 。

这里以一个简单的包含 finder info entry 和 resource fork entry 的 appledouble 文件为例子:

image.png

怎么生成 appledouble 文件?

这是一开始头疼的地方,后来在 github 上面找了找,发现了 npm 中有个开源的库 xattr-file 可以来生成 appledouble 文件,简单的写一个测试一下:

var xattr = require("xattr-file")
var fs = require('fs')
var buffer = xattr.create({
 "com.example.Attribute": "my data"
});

fs.writeFile('./hello.apple', buffer, err => {
 if (err) {
   console.error(err)
   return
}
}
)

成功生成 appledouble 文件:

image.png

但是用 01editor 看了看,发现用 xattr-file 包生成的 appledouble 正好有 Finder info entry 以及 Resource fork entry ,只不过 Resource fork entry 长度是 0 :

image.png

然后看了看 xattr-file 源码 发现并没有直接存放 Resource fork 数据的接口。

所以需要自己像下面这样额外添加 resource fork 数据,并且再用 01editor 去修改 header 中 Resource fork length 字段:

var xattr = require("xattr-file");
const fs = require('fs')

var buffer = xattr.create({
 "com.example.Attribute": "my data"
});

// resource fork data 部分:
var buffer2 = Buffer.from("a".repeat(0x12))
var buffer3 = Buffer.from("a".repeat(0x34))
console.log(Buffer.concat([buffer2,buffer3]).length)  // 打印的 resource fork data 长度。

var buffer4 = Buffer.concat([buffer,buffer2,buffer3])
fs.writeFile('./1234.apple', buffer4, err => {
 if (err) {
   console.error(err)
   return
}
 //文件写入成功。
}
)

DSI 包结构

AFP 协议是基于 DSI 协议传输的,Data Stream Interface 协议 wiki :https://en.wikipedia.org/wiki/Data_Stream_Interface

DSI header 结构:


image.png

DSI 包中 Command字段的类型有如下 8 种,这里需要关注的是 2 ,DSICommand 。也就是说如果 DSI header 中 Command 字段是 2 ,那么就表示 DSI 包的 payload 部分就携带着 AFP 命令。

image.png

我们以本次漏洞涉及的 OpenFork 命令为例来看看:


image.png

客户端发送的 AFP 命令如何映射到服务端函数

关于 netatalk 处理请求流程 ,可以参考 https://gtrboy.github.io/posts/netatalk/, 写得非常详细。

这里简单说一下,客户端发送 AFP 命令到服务端函数的映射过程。首先客户端在发送的 DSICommand 包中指定请求的 AFP 命令的编号,服务端在收到请求的时候,会拿这个编号,根据一个名叫 afp_switch 的函数数组进行映射到不同的函数上。

比如,如下数据包中,Command 是 60 :

image.png

60 表示 AFP_READ_EXT (具体是在 include\atalk\afp.h 中定义的):

image.png

通过 afp_switch 数组进行映射之后,服务端就会跳到我们请求的 command 对应的处理函数上。


image.png


image.png

怎么发送 AFP 请求包?

知道 DSI 包结构之后,我们怎么构造本次漏洞涉及的 Openfork 请求呢?

在网上搜了搜,发现 nmap 中有 afp 相关的模块 nselib\afp.lua和一些脚本:


image.png

试了试 afp-ls这个脚本,果然能行:

nmap --sc ript=afp-ls  192.168.131.139 -p548


image.png

但是后续也发现一些问题,现成的脚本只有这么几个,所以为了发送不同类型的 command 数据包,我们需要自己编写 nse 脚本。所以这里就需要一些 nse 脚本以及 lua 基础的知识,lua 基础我是直接看的 【无废话30分钟】Lua快速入门教程 - 4K超清 就可以上手了,另外 nse 脚本的环境,我选择的是 vscode + lua 插件 + lua debug 插件 lua 环境 vscode配置。关于 nse 脚本的话,也不需要重头去看 nse 的官方文档,这里直接照模样画虎,拷贝 afp-ls ,然后修改里面的关键代码就行了,这里就以及如何实现 read 命令来举例:

首先在 srcipts 目录下面新建 afp-read.nse,拷贝 afp-ls.nse 内容,然后删掉里面进行 ls 操作的代码,从 nselib\afp.lua 中我们发现afpHelper中已经有了ReadFile函数:


image.png

那么我们直接在 afp-read.nse 中添加代码调用之(这里不想用参数传递的话,直接写死 path 就好了,毕竟只是为了达到能发送 afp 包的目的):


image.png

这样就构造好了 read 请求(实际是对应服务端 afp_read_ext 函数)


image.png

image.png

完整的 afp-read.nse 如下(不过这个 afp-read.nse并不能触发本次的漏洞,需要修改 afp.lua 中 ReadFile 函数调用 fp_open_fork 时传入的 flag ,具体原因见后续分析):

local afp = require "afp"
local nmap = require "nmap"
local shortport = require "shortport"
local stdnse = require "stdnse"
local ls = require "ls"

desc ription = [[
Attempts to get useful information about files from AFP volumes.
The output is intended to resemble the output of <code>fork</code>.
]]

author = "kee"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"discovery", "safe"}

portrule = shortport.port_or_service(548, {"afp"})

action = function(host, port)

 local afpHelper = afp.Helper:new()
 local args = nmap.registry.args
 local users = nmap.registry.afp or { ['nil'] = 'nil' }


 if ( args['afp.username'] ) then
   users = {}
   users[args['afp.username']] = args['afp.password']
 end

 for username, password in pairs(users) do

   local status, response = afpHelper:OpenSession(host, port)
   if ( not status ) then
     stdnse.debug1("%s", response)
     return
   end

   -- if we have a username attempt to authenticate as the user
   -- Attempt to use No User Authentication?
   if ( username ~= 'nil' ) then
     status, response = afpHelper:Login(username, password)
   else
     status, response = afpHelper:Login()
   end

   if ( not status ) then
     stdnse.debug1("Login failed")
     stdnse.debug3("Login error: %s", response)
     return
   end

   -- ------------------------------------- 前面的是在登录 --------------

   -- local vols
   -- status, vols = afpHelper:ListShares()

   -- if status then
   --   for _, vol in ipairs( vols ) do
   --     local status, tbl = afpHelper:Dir( vol )
   --     if ( not(status) ) then
   --       ls.report_error(output, ("ERROR: Failed to list the contents of %s"):format(vol))
   --     else
   --       ls.new_vol(output, vol, true)
   --       for _, item in ipairs(tbl[1]) do
   --         if item and item.name then
   --           if not (item.privs and item.create) then
   --             ls.report_error(output, ("ERROR: Failed to retrieve file details for %/%s"):format(vol, item.name))
   --           else
   --             local continue = ls.add_file(output, {
   --                         item.privs, item.uid, item.gid,
   --                         item.fsize, item.create, item.name
   --                       })
   --             if not continue then
   --               ls.report_info(output, ("maxfiles limit reached (%d)"):format(maxfiles))
   --               break
   --             end
   --           end
   --         end
   --       end
   --       ls.end_vol(output)
   --     end
   --   end
   -- end

   local str_path = args["path"]
   -- 不想通过参数传递,这里直接赋值也可以。
   -- str_path = "/test/test.txt"

   local content
   status, content = afpHelper:ReadFile(str_path)
   

   status, response = afpHelper:Logout()
   status, response = afpHelper:CloseSession()

   -- -- stop after first successful attempt
   -- -- if #output["volumes"] > 0 then
   -- --   ls.report_info(output, ("information retrieved as %s"):format(username))
   -- --   return ls.end_listing(output)
   -- -- end

   return content

 end
 return
end

漏洞详情

漏洞点

客户端向服务端发送 openfork 命令去打开一个文件的时候,netatalk 服务端中会调用 afp_openfork 函数,afp_openfork 函数会调用 ad_open 函数:


image.png

如果请求中是否设置了 ADFLAGS_RF 这个 flag(在 nmap afp.lua中的 ReadFile 函数将 flag 参数写死了为 0 ,所以直接使用 ReadFile 不会进入 ad_open_rf 函数,需要进行修改)


image.png

修改 ReadFile :


image.png

ad_open 函数会调用 ad_open_rf 函数:


image.png

ad_open_rf 函数进而会调用 ad_open_rf_ea 函数:


image.png

ad_open_rf_ea 函数会调用 ad_header_read_osx 函数解析客户端指定的 adouble 文件:


image.png

并且将解析的结果放到 adosx 中:


image.png

对于 adouble 文件 entries 部分,ad_header_read_osx 函数中是调用 parse_entries 函数进行解析的:


image.png

parse_entries 函数是这个漏洞的产生的源头,简单的来说解析 entries 的时候 ,每个 entry 的 offset 字段是我们都是可控的,并且竟然可以设置为超过 adouble 文件的长度,处理 entries 的逻辑见下图:


image.png

导致的直观结果就是得到的 adosx 结构体中 entries 的 offset 可以超过超过整个 adouble 文件的大小:


image.png


image.png

漏洞危害

上面讲到由于 parse_entries 的错误处理,会导致生成的 adosx 结构体中 entries 的 offset 可以超过整个 adouble 文件的大小,下面我们来具体看看这个畸形的 adosx 结构体会产生什么更严重的危害。

"任意"写

先是”任意写“,这里说的是任意写,并不是能实现任意虚拟内存地址的任意写,而是能实现高于 map + 0x20 地址的任意写(map 指的是 mmap 将我们请求的 AppleDouble 文件映射到的内存位置)。

在 ad_header_read_osx 函数调用 parse_entries 函数解析了 entries 之后,会进行一个将 AppleDouble 文件转化为适应于 Netatlk 的文件的操作,转化函数是 ad_convert_osx 函数:


image.png

正如上面的注释所说,AppleDouble 文件中的 FinderInfo entry 的数据可能会超过 32 字节(携带着 xattrs ),但是 Netatalk 不会处理这里 xattrs ,所以会删除这些 attrs(也就是说删除 FinderInfo entry 数据中超过 32 字节的部分)。

删除的操作步骤是先将原文件映射到内存中(使用的 mmap ),然后利用 memmove 函数将 resource fork entry 地址整体向前移动以覆盖FinderInfo entry 多出32字节的数据。


image.png

代码逻辑为:

    memmove(map + ad_getentryoff(ad, ADEID_FINDERI) + ADEDLEN_FINDERI,  // ADEDLEN_FINDERI 是 32 .
           map + ad_getentryoff(ad, ADEID_RFORK),
           ad_getentrylen(ad, ADEID_RFORK));

// ad_getentryoff(ad, ADEID_FINDERI) 作用是从 ad 结构体对象中取 FinderInfo entry 的 offset 。(在上面 漏洞点 小节那里我们说到过,这个 offset 是我们可控的,并且可以超过整个 adouble 文件的大小)   => 可控

// ad_getentryoff(ad, ADEID_RFORK) 作用是从 ad 结构体对象中取 resource fork entry 的 offset 。 => 可控

// ad_getentrylen(ad, ADEID_RFORK) 作用是从 ad 结构体对象中取 resource fork entry 的 长度。   => 可控

所以上面的 memove 可以简化为:

    memmove(map + a + 0x20,  map + b, c);
  // 下面构造的恶意文件中,设置 map + a + 0x20 为 ld.so 数据段基地址。
  // 这样就会将 resource fork data 覆盖掉 ld.so 整个数据段。

也就是下图中的 a ,b ,c 我们都是可控的:


image.png

也就是我们可以将整个 resource fork data 部分覆盖到 map + a +0x20 处!

再看一下 mmap :


image.png

这里 origlen 是 header 中 resource fork offset + resource fork length 的值,这两个我们都是可控的,对于下图中的 b,c:


image.png

注意如果修改 b + c 过大的话,会映射到大概如下位置:


image.png

测试小于 0x1000 会映射到如下位置,可以看到此时的 map 值(这里指的是 adouble 文件映射的初始位置) 向上不远处正好是 ld.so 的位置(后续正是覆盖 ld.so 的数据段实现 RCE 的)。


image.png

注:

上图是使用 gdbserver 启动 afpd 时候的内存映射,由于 gdb 启动的时候默认会关闭随机化,所以并不具有说服力。为了找到内存布局的规律,需要使用 gdbserver attach ,这里多次使用 gdbserver attach 上去,当 mmap 映射的长度是 0x2000 的时候,距离 ld.so 数据段偏移均为 0x3000:


image.png

image.png

另外如果长度是 0x1000 的话,偏移是 0x2000 :


image.png

"任意"读

这里的任意读,其实也是有限制的,只能读取比 ad->ad_data 地址高的任意 0x20 字节大小,具体见下面分析:

任意读发生在任意写( memmove 函数)不远处的 ad_rebuild_adouble_osx 中。


image.png

ad_rebuild_adouble_osx 函数的作用是通过 ad 结构体对象( ad 结构体是通过解析原来的 adouble 文件得到的)来修改原来的 adoublefile 文件。ad_rebuild_adouble_osx 函数中有一处 memcpy 的调用:

   memcpy(adbuf + ADEDOFF_FINDERI_OSX, ad_entry(ad, ADEID_FINDERI), ADEDLEN_FINDERI); // 任意读的漏洞点,第二个参数是我们伪造的 ad 对象基地址 +  finder_offet 的值。

// adbuf 的值是 mmap appledouble 文件到内存的起始地址(和任意写中 map 的值一样),adbuf + ADEDOFF_FINDERI_OSX 的值是 appledouble 文件数据部分的内存起始地址。

// ADEDLEN_FINDERI 是 0x20 。

// 所以会把 ad_entry(ad, ADEID_FINDERI) 这个地址的数据,读取 32 位放到新文件的 finder 数据段。


image.png

adbuf + ADEDOFF_FINDERI_OSX 是:26 + entry_num * 12 , 也就是 map 起始地址 +appledouble 数据部分离文件开头的偏移。


image.png

image.png

ad_entry(ad, ADEID_FINDERI) 得到 ad 结构体对象中 ad_data 的起始地址 + finder_offset 。( finder_offset 可控)


image.png

这里注意一点, ad 这个结果体对象是在栈上的(所以可以利用这一点读取 libc_start_main_ret 从而得到 libc 版本):


image.png

在 ad_rebuild_adouble_osx 之后,服务端会调用 ummap 写回文件 (这就实现了任意读的回显)。


image.png

总结一下任意读利用步骤:

1.首先构造一个触发任意读的恶意 adouble 文件,写到服务端上。

2.发送 openfork 命令来打开这个恶意的 adouble 文件,在服务端处理完 openfork 请求之后,就会将比 ad->ad_data 地址高的任意 0x20 字节大小的内容写回到恶意的 adouble 文件中。

3.再去读取修改完之后的 adouble 文件,这样就能得到读取的内容。

读 + 写 => RCE

虽然程序开启了 ASLR 和 PIE ,但是因为 netatlk 处理客户 afp 请求时建立的 TCP 连接都是由 fork 出的子进程来进行处理的,而子进程和父进程的地址空间是相同的,所以只要父进程不挂,fork 出来的子进程 libc 内存地址就是不变的。这就允许我们通过 ”任意“读来泄露出 子进程的 libc 基地址(进而得到子进程的 system 函数地址 ),随后再建立一次连接,通过”任意写“将上一次请求拿到的 system 函数地址(虽然是两个不同的子进程,但是由于都是由同一个父进程 fork 出来,所以两个子进程的 system 函数地址是一样的)写到特定区域,进而导致 RCE ,具体怎么 RCE 见下面分析。

读取 libc_start_main_ret

从上面我们可以知道能读取 ad 结构体对象中 ad_data 的起始地址 + finder_offset(可控)的内容。那我们怎么调整 finder_offset 从而来读取到 libc_start_main_ret 的内容呢?

这里利用的就是 ad 结构体对象在栈上,libc_start_main_ret 也在栈上,而同一个程序运行时栈内偏移是固定的。所以我们可以直接通过 gdb 调试来看 ad 结构体对象中 ad_data 的起始地址 的地址,再通过 gdb 得到站上保存着 libc_start_main_ret 的地址,然后两者之间的偏移也就可以得到了。

具体操作如下:


image.png

image.png

得到 &ad.ad_data地址为:

0x7fffffffda32

然后切换到 main 函数的栈帧,查看上面的 saved rip at 就是栈上保存着 libc_start_main_ret 的地址。

image.png

0x7fffffffe3e8

两者一减,得到偏移:

0x9B6

我们构造一个名为 ._read 的文件:

image.png



上传至服务器,这里我就直接用 xftp 传上去了(实际情况中,可以自己写 nse 脚本进行上传),此外,还需要创建一个 read 文件(内容随意,不然会报错)。

image.png


然后nmap 这边发送读取 test/read (实际读取的是 test/.__read) 的请求 (fp_open_fork 的 flag 需要设置为 2 ),触发漏洞进而泄露 libc_start_main_ret ,从而拿到 system 函数地址。


image.png

image.png

发送之后,再看看 ._read 文件,查看原本应该是 finder data 的地方,成功泄露出了 __libc_start_main_ret


image.png

https://libc.rip 上面验证一下 libc 版本,果然是我们的 libc 版本,并且能得到相应的 system 函数的偏移。


image.png


image.png

覆盖 __rtld_local

由于 netatalk 内部的报错处理机制,如果程序报错,不会直接退出,而在处理错误的时候,会调用到 dl_open 函数。dl_open 函数源码:https://code.woboq.org/userspace/glibc/elf/dl-open.c.html


image.png

可以看到,在调用 dl_open函数的时候会调用 __rtld_lock_lock_recursive(GL(dl_load_lock));

__rtld_lock_lock_recursiveGL(dl_load_lock) 都是在 ld.so 的 .data 节中:

image.png

__rtld_lock_lock_recursive:(这里因为静态符号表被删除的原因,ida 识别为了 qword_22AF60 ,可以看到 qword_22AF60 是在 .data 节)


image.png

_rtld_global :(也是在 .data 节的)


image.png

ld.so 中的 .data 节在运行的时候会映射到内存中 ld.so 的数据段。所以如果我们覆盖掉 ld.so 的数据段,将这两个地址的值覆盖为 system 函数地址,以及我们需要执行的命令,这样就能造成 RCE 了。

上面 "任意写" 的小节中我们说到了,危害是可以将整个 resource fork data 部分覆盖到 map + a +0x20 处!构造恶意写 AppleDouble file 的时候,我们可以调整 map + a + 0x20 为整个 ld.so 数据段的初始值(这样做就相当于从 ld.so 数据段起始地址开始覆盖),这样我们只要计算出 _rtld_global[289]__rtld_lock_lock_recursive 距离 ld.so 数据段偏移,然后直接在 resource fork data 相应位置修改为我们想要的地址就可以了。

确定参数偏移

gdb 中使用

info variables 

查看所有全局变量的地址:

image.png

发现是 0x00007ffff7ffd060 _rtld_global , 也就是距离 0x00007ffff7ffd0 (ld.so 数据段起始地址)偏移 0x60 。因为 rtld_global 是 int64 类型的数组 ,所以 _rtld_global[289]距离 ld.so 数据段起始地址的偏移为:289*8 + 0x60 = 0x968

image.png

确定函数偏移

因为 __rtld_lock_lock_recursive 的符号在 ld.so 中被删除了,所以不能直接通过上面 gdb 的方法查看。这里结合 ida 静态分析来算__rtld_lock_lock_recursive

静态中 ._rtld_global 基地址为 0x22A060   
静态中,_dl_rtld_lock_recursive 基地址为 0x22AF60 。

所以 _dl_rtld_lock_recursive 离 ._rtld_global 偏移为 0xF00 。
然后 ._rtld_global 动态离 ld.so data 段基地址偏移是 0x60,所以 _dl_rtld_lock_recursive 动态距离 ld.so data 段的地址偏移是 0xF00 + 0x60 = 0xF60

结合起来

构造任意写的 appleDouble file 的各个部分如下:

1.设置 finder_offset 为:0x3000 - 0x20 = 0x2FE0 :(映射大小为 0x2000 的时候,偏移是 0x3000)


image.png

image.png

2.resource_fork 数据部分:

0x968 为 "touch /tmp/123\x00" ,0xF60 为 system 函数地址(0x00007FFFF738a420)。

可以用如下 exp.js 生成恶意 appleDouble file

exp.js 的脚本:

image.png

3.然后 01 editor 修改 resource fork 的长度为exp.js 输出的长度 (其实就是 0xF60 + 8 = 0xF68


image.png


image.png

附一张成功覆盖 ld.so 中数据段的截图:

image.png

完整攻击步骤

1.上传构造好恶意的 ._read 文件 和任意内容的 read 文件到 test 共享目录下,nmap 发送读取 test/read (实际读取的是 test/.__read) 的请求 (fp_open_fork 的 flag 设置为 2 ),触发漏洞进而泄露 libc_start_main_ret ,从而拿到 system 函数地址。

image.png

2.根据 system 函数地址,构造好的恶意 ._exp 文件和任意内容的 exp 文件上传到 test 共享目录下,nmap 发送读取 test/exp触发任意写覆盖 ld.so 数据段中的 __rtld_local[289]__rtld_lock_lock_recursive,从而导致 RCE 。

exp.js :

image.png

发送之后,成功 pwned:

image.png

补丁分析

下载 3.13 和 3.12 版本进行对比,可以看到在最新版本 3.13 中,在 parse_entries 函数中如果发现 entry 的 offset 大于 appleDouble 文件的长度的话,就直接 return -1


image.png

参考

https://research.nccgroup.com/2022/03/24/remote-code-execution-on-western-digital-pr4100-nas-cve-2022-23121/

https://gtrboy.github.io/posts/netatalk/

https://devco.re/blog/2022/03/28/your-NAS-is-not-your-NAS/


本文为白帽汇原创文章,如需转载请注明来源:https://nosec.org/home/detail/4997.html 

最新评论

haishir  :  厉害!
932天前 回复
昵称
邮箱
提交评论