Netatalk RCE漏洞分析(CVE-2022-23121 )
作者: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 官网并上没有简介:
这里简单概况就是, 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 ,就可以调试了。
因为我们关注的是 afpd 的子进程,所以修改 clion 中 gdb 的 .gdbint 为:
set detach-on-fork off
set schedule-multiple on
一些断点
我这边为了方便,在 /etc/afpd/main.c 这下了一个断点:
如果用 gdbserver 启动 afpd 的话,会直接断到这个位置的:
继续运行之后,程序就开始等待客户端的请求。
举个例子,使用 nmap 中的 afp-ls
脚本:
nmap 192.168.44.130 -p 548 --sc ript=afp-ls
在 afp_openvol() 函数下断点,就会断到 afp_openvol
函数。
前置知识
AppleDouble 文件
AppleDouble 简单来说就是 mac 上的一种存储数据的文件。
文件格式
AppleDouble 格式文档下载地址:https://web.archive.org/web/20180311140826if_/http://kaiser-edv.de/documents/AppleSingle_AppleDouble.pdf
AppleDouble 文件可以分为 header 和 data 两部分,header 的结构如下:
data 则是指 header 中声明的各个 entry 的数据。
其中 entry ID 表示的类型如下:
本次漏洞用到的是 finder info entry 以及 resource fork entry 。
这里以一个简单的包含 finder info entry 和 resource fork entry 的 appledouble 文件为例子:
怎么生成 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 文件:
但是用 01editor 看了看,发现用 xattr-file
包生成的 appledouble 正好有 Finder info entry 以及 Resource fork entry ,只不过 Resource fork entry 长度是 0 :
然后看了看 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 结构:
DSI 包中 Command字段的类型有如下 8 种,这里需要关注的是 2 ,DSICommand
。也就是说如果 DSI header 中 Command 字段是 2 ,那么就表示 DSI 包的 payload 部分就携带着 AFP 命令。
我们以本次漏洞涉及的 OpenFork 命令为例来看看:
客户端发送的 AFP 命令如何映射到服务端函数
关于 netatalk 处理请求流程 ,可以参考 https://gtrboy.github.io/posts/netatalk/, 写得非常详细。
这里简单说一下,客户端发送 AFP 命令到服务端函数的映射过程。首先客户端在发送的 DSICommand 包中指定请求的 AFP 命令的编号,服务端在收到请求的时候,会拿这个编号,根据一个名叫 afp_switch
的函数数组进行映射到不同的函数上。
比如,如下数据包中,Command 是 60 :
60 表示 AFP_READ_EXT (具体是在 include\atalk\afp.h
中定义的):
通过 afp_switch 数组进行映射之后,服务端就会跳到我们请求的 command 对应的处理函数上。
怎么发送 AFP 请求包?
知道 DSI 包结构之后,我们怎么构造本次漏洞涉及的 Openfork 请求呢?
在网上搜了搜,发现 nmap 中有 afp 相关的模块 nselib\afp.lua
和一些脚本:
试了试 afp-ls
这个脚本,果然能行:
nmap --sc ript=afp-ls 192.168.131.139 -p548
但是后续也发现一些问题,现成的脚本只有这么几个,所以为了发送不同类型的 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
函数:
那么我们直接在 afp-read.nse
中添加代码调用之(这里不想用参数传递的话,直接写死 path 就好了,毕竟只是为了达到能发送 afp 包的目的):
这样就构造好了 read 请求(实际是对应服务端 afp_read_ext
函数)
完整的 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 函数:
如果请求中是否设置了 ADFLAGS_RF 这个 flag(在 nmap afp.lua
中的 ReadFile
函数将 flag
参数写死了为 0 ,所以直接使用 ReadFile
不会进入 ad_open_rf 函数,需要进行修改)
修改 ReadFile :
ad_open 函数会调用 ad_open_rf 函数:
ad_open_rf 函数进而会调用 ad_open_rf_ea 函数:
ad_open_rf_ea 函数会调用 ad_header_read_osx 函数解析客户端指定的 adouble 文件:
并且将解析的结果放到 adosx 中:
对于 adouble 文件 entries 部分,ad_header_read_osx 函数中是调用 parse_entries 函数进行解析的:
parse_entries 函数是这个漏洞的产生的源头,简单的来说解析 entries 的时候 ,每个 entry 的 offset 字段是我们都是可控的,并且竟然可以设置为超过 adouble 文件的长度,处理 entries 的逻辑见下图:
导致的直观结果就是得到的 adosx 结构体中 entries 的 offset 可以超过超过整个 adouble 文件的大小:
漏洞危害
上面讲到由于 parse_entries 的错误处理,会导致生成的 adosx 结构体中 entries 的 offset 可以超过整个 adouble 文件的大小,下面我们来具体看看这个畸形的 adosx 结构体会产生什么更严重的危害。
"任意"写
先是”任意写“,这里说的是任意写,并不是能实现任意虚拟内存地址的任意写,而是能实现高于 map + 0x20
地址的任意写(map 指的是 mmap 将我们请求的 AppleDouble 文件映射到的内存位置)。
在 ad_header_read_osx 函数调用 parse_entries 函数解析了 entries 之后,会进行一个将 AppleDouble 文件转化为适应于 Netatlk 的文件的操作,转化函数是 ad_convert_osx 函数:
正如上面的注释所说,AppleDouble 文件中的 FinderInfo entry 的数据可能会超过 32 字节(携带着 xattrs ),但是 Netatalk 不会处理这里 xattrs ,所以会删除这些 attrs(也就是说删除 FinderInfo entry 数据中超过 32 字节的部分)。
删除的操作步骤是先将原文件映射到内存中(使用的 mmap ),然后利用 memmove 函数将 resource fork entry 地址整体向前移动以覆盖FinderInfo entry 多出32字节的数据。
代码逻辑为:
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 我们都是可控的:
也就是我们可以将整个 resource fork data 部分覆盖到 map + a +0x20
处!
再看一下 mmap :
这里 origlen 是 header 中 resource fork offset + resource fork length 的值,这两个我们都是可控的,对于下图中的 b,c:
注意如果修改 b + c 过大的话,会映射到大概如下位置:
测试小于 0x1000 会映射到如下位置,可以看到此时的 map 值(这里指的是 adouble 文件映射的初始位置) 向上不远处正好是 ld.so 的位置(后续正是覆盖 ld.so 的数据段实现 RCE 的)。
注:
上图是使用 gdbserver 启动 afpd 时候的内存映射,由于 gdb 启动的时候默认会关闭随机化,所以并不具有说服力。为了找到内存布局的规律,需要使用 gdbserver attach ,这里多次使用 gdbserver attach 上去,当 mmap 映射的长度是 0x2000 的时候,距离 ld.so 数据段偏移均为 0x3000:
另外如果长度是 0x1000 的话,偏移是 0x2000 :
"任意"读
这里的任意读,其实也是有限制的,只能读取比 ad->ad_data 地址高的任意 0x20 字节大小,具体见下面分析:
任意读发生在任意写( memmove 函数)不远处的 ad_rebuild_adouble_osx 中。
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 数据段。
adbuf + ADEDOFF_FINDERI_OSX 是:26 + entry_num * 12 , 也就是 map 起始地址 +appledouble 数据部分离文件开头的偏移。
ad_entry(ad, ADEID_FINDERI)
得到 ad 结构体对象中 ad_data 的起始地址 + finder_offset
。( finder_offset 可控)
这里注意一点, ad 这个结果体对象是在栈上的(所以可以利用这一点读取 libc_start_main_ret 从而得到 libc 版本):
在 ad_rebuild_adouble_osx 之后,服务端会调用 ummap 写回文件 (这就实现了任意读的回显)。
总结一下任意读利用步骤:
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
的地址,然后两者之间的偏移也就可以得到了。
具体操作如下:
得到 &ad.ad_data
地址为:
0x7fffffffda32
然后切换到 main 函数的栈帧,查看上面的 saved rip at 就是栈上保存着 libc_start_main_ret
的地址。
0x7fffffffe3e8
两者一减,得到偏移:
0x9B6
我们构造一个名为 ._read
的文件:
上传至服务器,这里我就直接用 xftp 传上去了(实际情况中,可以自己写 nse 脚本进行上传),此外,还需要创建一个 read 文件(内容随意,不然会报错)。
然后nmap 这边发送读取 test/read
(实际读取的是 test/.__read
) 的请求 (fp_open_fork 的 flag 需要设置为 2 ),触发漏洞进而泄露 libc_start_main_ret ,从而拿到 system 函数地址。
发送之后,再看看 ._read 文件,查看原本应该是 finder data 的地方,成功泄露出了 __libc_start_main_ret
。
https://libc.rip 上面验证一下 libc 版本,果然是我们的 libc 版本,并且能得到相应的 system 函数的偏移。
覆盖 __rtld_local
由于 netatalk 内部的报错处理机制,如果程序报错,不会直接退出,而在处理错误的时候,会调用到 dl_open
函数。dl_open
函数源码:https://code.woboq.org/userspace/glibc/elf/dl-open.c.html
可以看到,在调用 dl_open
函数的时候会调用 __rtld_lock_lock_recursive(GL(dl_load_lock));
而 __rtld_lock_lock_recursive
和 GL(dl_load_lock)
都是在 ld.so 的 .data 节中:
__rtld_lock_lock_recursive
:(这里因为静态符号表被删除的原因,ida 识别为了 qword_22AF60 ,可以看到 qword_22AF60 是在 .data 节)
_rtld_global
:(也是在 .data 节的)
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
查看所有全局变量的地址:
发现是 0x00007ffff7ffd060 _rtld_global , 也就是距离 0x00007ffff7ffd0 (ld.so 数据段起始地址)偏移 0x60 。因为 rtld_global 是 int64 类型的数组 ,所以 _rtld_global[289]
距离 ld.so 数据段起始地址的偏移为:289*8 + 0x60 = 0x968
确定函数偏移
因为 __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)
2.resource_fork 数据部分:
0x968 为 "touch /tmp/123\x00" ,0xF60 为 system 函数地址(0x00007FFFF738a420)。
可以用如下 exp.js 生成恶意 appleDouble file
exp.js 的脚本:
3.然后 01 editor 修改 resource fork 的长度为exp.js 输出的长度 (其实就是 0xF60 + 8 = 0xF68
:
附一张成功覆盖 ld.so 中数据段的截图:
完整攻击步骤
1.上传构造好恶意的 ._read 文件 和任意内容的 read 文件到 test 共享目录下,nmap 发送读取 test/read
(实际读取的是 test/.__read
) 的请求 (fp_open_fork 的 flag 设置为 2 ),触发漏洞进而泄露 libc_start_main_ret ,从而拿到 system 函数地址。
2.根据 system 函数地址,构造好的恶意 ._exp 文件和任意内容的 exp 文件上传到 test 共享目录下,nmap 发送读取 test/exp
触发任意写覆盖 ld.so 数据段中的 __rtld_local[289]
和 __rtld_lock_lock_recursive
,从而导致 RCE 。
exp.js :
发送之后,成功 pwned:
补丁分析
下载 3.13 和 3.12 版本进行对比,可以看到在最新版本 3.13 中,在 parse_entries 函数中如果发现 entry 的 offset 大于 appleDouble 文件的长度的话,就直接 return -1
:
参考
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
最新评论