A Journey into Synology NAS 系列四: HTTP请求流程和案例分析

前言

前面两篇文章从局域网的角度出发,对群晖NAS设备上开放的部分服务进行了分析。而在大部分情况下,群晖NAS设备是用于远程访问的场景中,即唯一的入口是通过5000/http(5001/https)进行访问(暂不考虑使用QuickConnect或其他代理的情形)。因此,本篇文章将主要对HTTP请求流程和处理机制进行分析,并分享在部分套件中发现的几个安全问题。

HTTP请求处理流程

在正常登录过程中抓取的部分请求如下,可以看到请求url包含query.cgilogin.cgientry.cgi等。根据群晖的开发者手册可知,与设备进行交互的大概流程如下:

  1. 通过query.cgi获取API相关的信息;
  2. 通过login.cgiencryption.cgi进行认证,获取session id
  3. 通过entry.cgi发送请求、解析响应;
  4. 完成交互后登出。

某个具体的请求示例如下,可以看到有点类似于JSON-RPC。对于大部分请求,其url均为"/webapi/entry.cgi"。在POST data部分,api参数表示要请求的API名称,method表示要请求的API中的方法,version表示要请求的API版本。

针对API请求,群晖在后端采用json元数据文件SYNO.***.***.lib来定义与API相关的信息,示例如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
    "SYNO.Core.PersonalNotification.Event": { // API名称
        "allowUser": [ "admin.local"], // 哪个组可以访问该API
        "appPriv""",
        "authLevel"1, // 是否需要认证 (0表示无需认证)
        "disableSocket"false,
        "lib""lib/SYNO.Core.PersonalNotification.so", // 处理具体请求的文件
        "maxVersion"1,
        "methods": { // API中支持的方法以及对应的版本
            "1": [{
                    "fire": {
                        "allowUser": [ "admin.local","normal.local" ], // 覆盖上面的定义
                        "grantByUser"false,
                        "grantable"true }
                }]
        },
        "minVersion"1,
        "priority"0,
        "socket"""
    }
}

根据上述信息,可以知道如何构造一个具体的请求来触发后端的某个处理程序。

整体的HTTP请求处理流程大概如下。首先,请求通过5000端口发送给设备,基于请求的urlnginx服务会将该请求分发给不同的cgi,如query.cgilogin.cgientry.cgi,其中,entry.cgi是大部分POST请求的端点。这些cgi会与另外两个服务synocgidsynoscgi进行通信,其中synocgid负责处理与session相关的事务,而synoscgi则负责分发具体的请求到最终的处理程序。

安全问题

在理解了HTTP请求流程和处理机制后,便可以对群晖NAS设备的功能模块进行分析。在群晖NAS设备上,主要包含两大攻击面:DSM操作系统本身和群晖提供的大量套件。下面结合具体的实例进行分析。

Diagnosis Tool

前面提到过,Diagnosis Tool是群晖提供的一个工具套件,支持抓包、调试等功能。该工具的界面和具体的抓包请求示例如下。

该请求由packet_capture.cgi程序进行处理,部分示例代码如下。在handle_action_start()中,获取请求中的参数后将其以json字符串的形式传给tcpdump_wrapper程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
__int64 __fastcall handle_action_start(__int64 a1, __int64 a2, const char *a3, const char *a4)
{
  // ...
  Json::Value::Value((Json::Value *)&v39, (const std::string *)&v28);
  v17 = Json::Value::operator[](&v35, "output_dir");
  Json::Value::operator=(v17, &v39);
  Json::Value::~Value((Json::Value *)&v39);
  Json::Value::Value((Json::Value *)&v40, v4);
  v18 = Json::Value::operator[](&v35, "expression");
  Json::Value::operator=(v18, &v40);
  Json::Value::~Value((Json::Value *)&v40);
  Json::Value::Value((Json::Value *)&v41, v6);
  v19 = Json::Value::operator[](&v35, "interface");
  Json::Value::operator=(v19, &v41);
  Json::Value::~Value((Json::Value *)&v41);
  Json::FastWriter::write((Json::FastWriter *)&v33, (const Json::Value *)&v37);
  std::string::assign((std::string *)&v29, (const std::string *)&v33);
  // ...
  if (SLIBCExec("/var/packages/DiagnosisTool/target/bin/tcpdump_wrapper""--params", v29, 0LL, 0LL) == -1 )
    // ...

tcpdump_wrapper中,调用sub_401F10()解析得到output_direxpressioninterface参数,并传入RunTcpDump(),其最终调用execve()执行命令tcpdump -i <interface> -w <file> -C 10 -s 0 filter_expression

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__int64 __fastcall main(signed int a1, char **a2, char **a3)
{
  if ( a1 > 1 )
  {
    // ...
    if ( v3 != 2 && !strcmp(v4[1], "--params") )
    {
      std::string::string(&v11, v4[2], &v6);
      // resolve parameters from json string
      sub_401F10(&v11, &output_dir, &expression,&interface);
      // ...
    }
  }
  if (sub_4019D0(&output_dir) )
  {
    if (sub_401900() && !RunTcpdump(&output_dir, &expression, &interface) )
    {
     // ... 

调用execve()来执行命令,相对比较安全,避免了命令注入的问题,但其中的filter_expression参数是可控的。通过查看tcpdump命令的帮助文档,发现-z选项与-C-G选项组合也可达到命令执行的目的。

针对tcpdump -i <interface> -w <file> -C 10 -s 0 filter_expression,其中已包含-C选项,因此通过伪造filter_expression参数为-z<path to your shell script>,即通过注入命令选项,可实现命令执行的效果。

DS File

DS File是群晖提供的一个移动应用程序,便于从移动设备上访问和管理DiskStation上的文件,使用该应用访问DiskStation的流程与通过web的流程类似。当尝试登录到DiskStation时,认证过程采用基于PKI的加密机制。而在某些情形下如目标IP输入错误,或者网络临时不可用,正常的请求会失败,DS File会发送额外的请求。

通过查看对应的第3个请求发现,在请求头中包含经过Base64编码后的Authorization信息,相当于明文。

因此,在一个不安全的网络环境中,当尝试通过DS File应用访问DiskStation时,通过简单地丢弃或重定向对应的请求,”中间人’’可窃取用户的明文账号信息。

Synology Calendar

该套件是一个基于Web的应用程序,用于管理日常的事件和任务,其支持在事件中添加附件和分享日程等功能。其中,添加附件的功能支持从本地上传和从 NAS中上传两种方式。普通用户创建事件并添加附件的示例如下,同时给出了与附件链接相关的部分前端代码。

可以看到,上传文件的名称被拼接到href链接中。如果伪造一个文件名,能否控制对应的href链接呢?经过测试发现,由于未对文件名进行校验,通过伪造一个合适的文件名,可以更改对应的href链接,同时让显示的文件名称看起来正常。

此外,借助日程分享功能,还可以将该事件分享到管理员组中。当管理员组中的某个人查看该事件并点击对应的附件之后,该请求就会被执行。因此,利用该漏洞,一个普通权限的用户可以以”管理员”的权限执行”任意”请求,比如将其添加到管理员组中。

Media Server

Media Server套件提供与多媒体相关的服务,允许在NAS上通过DLNA/UPnP播放多媒体内容。在安装该套件后,会启动一些自定义的服务,如下。

通过简单的分析,发现dms中存在一些可供访问的url,且无需认证。

1个比较有意思的apivideotranscoding.cgi,对应的请求url格式为http://%s:%d/transcoder/videotranscoding.cgi/%s/id=%d%s,处理该请求的部分代码如下。可以看到,如果url中包含字符串id=和字符?,就将id=?之间的内容拷贝到dest缓冲区中。由于没有考虑两者出现的先后顺序,如果请求urlhttp://%s:%d/transcoder/videotranscoding.cgi/VideoStation?id=1,在调用strncpy()时就会出现整数下溢问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__int64 sub_406E80(__int64 a1)
{
  // ...
  v4 = getenv("REQUEST_URI");
  snprintf(s, 0x800uLL, "%s", v4);
  v99 = strstr(s, "id=");
  if ( v99 )
  {
    v5 = strchr(s, '?');
    if ( v5 )
      strncpy(dest, v99 + 3, v5 - (v99 + 3)); // integer underflow
  }
  // ...
  std::string::assign(v3, dest, strlen(dest));
  // ...
  sub_403F50(a1, v1, v3, (std::string *)(a1 + 136));

假设请求url的格式和程序预期的一致,函数sub_403F50()将会在后续被调用,其第3个参数对应前面拷贝的请求urlid=?之间的内容。在sub_403F50()中,对参数a2进行简单校验后,参数a3会被当做id后面的参数进行格式化。由于未对参数a3进行适当校验,且参数a3外部可控,因此会存在SQL注入的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__int64 sub_403F50(__int64 a1, std::string *a2, _QWORD *a3, std::string *a4)
{
  // ...
  if ( !(unsigned int)std::string::compare(a2, "MediaServer") )
  {
    std::string::assign((std::string *)v32, "mediaserver"0xBuLL);
    std::string::assign((std::string *)&v34, "MediaServer"0xBuLL);
    std::string::assign((std::string *)v33, "video"5uLL);
  }
  else
  {
    if ( (unsigned int)std::string::compare(a2, "VideoStation") )
      goto LABEL_4;
    std::string::assign((std::string *)v32, "video_metadata"0xEuLL);
    std::string::assign((std::string *)&v34, "VideoStation"0xCuLL);
    std::string::assign((std::string *)v33, "video_file"0xAuLL);
  }
  snprintf(s, 0x100uLL, "SELECT * from %s where id = %s", v33[0], (const char *)*a3); // SQL injection

另外1个类似的apijpegtnscaler.cgi,对应的请求url格式为http://%s:%d/transcoder/jpegtnscaler.cgi/%s/%d.%s,处理该请求的部分代码如下。可以看到,在调用strncpy()前未对其长度参数进行校验,通过构造请求如http://%s:%d/transcoder/jpegtnscaler.cgi/<a*0x450>/1,可造成缓冲区溢出。

1
2
3
4
5
6
7
8
9
10
11
__int64 main(__int64 a1, char **a2, char **a3)
{
  // ...
  v3 = getenv("REQUEST_URI");
  // ...
  v4 = strrchr(v3, '/');
  v5 = v4;
  // ...
  v6 = strtol(v4 + 10LL, 10);
  bzero(s, 0x400uLL);
  strncpy(s, v3, v5 - v3);  // buffer overflow

Audio Station

Audio Station套件提供收听广播节目、管理音乐库、建立个人播放清单等功能,并支持随时随地与朋友分享。安装该套件后,在其安装路径下会存在一些自定义的cgi程序,如media_server.cgiweb_player.cgiaudiotransfer.cgi等。在使用该套件的同时进行抓包,部分请求示例如下。

在前面提到的HTTP请求处理流程中,execl_cgi()负责处理自定义的cgi请求。更重要的是,在某些情形下,认证的处理由自定义的cgi程序负责。

通过分析,最有意思的apiaudiotransfer.cgi,对应的请求url格式为http://%s:%d/webman/3rdparty/AudioStation/webUI/audiotransfer.cgi/%s.%s,处理该请求的部分代码如下。可以看到,在main()函数开始处调用sub_402730()。在函数sub_402730()中,先获取请求url路径最后面的内容,然后将其传给MediaIDDecryption()。在MediaIDDecryption()中,先计算参数a1的长度,在拷贝前6个字节后,调用snprintf()。由于调用snprintf()时,其size参数和后面的字符串内容可控,存在缓冲区溢出问题。更重要的是,这个过程中没有对认证进行处理,即无需认证,因此通过构造并发送特定的请求,远程未认证的用户可触发该缓冲区溢出漏洞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
__int64 main(__int64 a1, char **a2, char **a3)
{
  sub_402730((__int64)v5);


_BOOL8 sub_402730(__int64 a1)
{
  // ...
  v8 = getenv("REQUEST_URI");
  snprintf(s, 0x400uLL, "%s", v8);
  // ...
  v11 = strrchr(s, '/');
  v12 = v11;
  if ( v11 )
  {
    // ...
    v15 = MediaIDDecryption((__int64)(v12 + 1));


__int64 MediaIDDecryption(const char *a1)
{
  // ...
  v1 = strlen(a1);
  if ( v1 > 5 )
  {
    v3 = (v1 - 6) >> 1;
    snprintf(s, 7uLL, "%s", a1);
    v14 = 0; v4 = s; v5 = (char *)&v14;
    do
    {
      v6 = *v4; --v5; ++v4; v5[6] = v6;
    }
    while ( v5 != &v13 );   // copy first 6 bytes
    __isoc99_sscanf(s, "%x", &v8);
    __isoc99_sscanf(&v14, "%x", &v9);
    snprintf(v17, v3 + 1"%s", a1 + 6);
    snprintf(v18, v3 + 1"%s", &a1[v3 + 6]); // buffer overflow

关于漏洞利用,知道创宇的@fenix师傅基于DSM 5.2-5592Audio Station 5.4-2860进行了分析和测试,其中相关的条件包括x86架构NX保护ASLR为半随机,感兴趣的可以去看看。这里补充几点:

  • 针对x86架构,基于DSM 6.xASLR为全随机,通过寻找合适的gadgets,可实现稳定利用,无需堆喷或爆破;

  • DSM 6.x上,获取到shell后,还需要进行提权操作;

  • 针对x64架构,由于存在地址高位截断的问题,暂时未找到合适的思路进行利用。

    如果师傅们有合适的思路,欢迎交流 :)

One More Thing

上面只是列举了几个典型的套件,以及在其中发现的部分问题。实际上,群晖的DSM系统中有非常多的功能,以及大量的套件可供分析。群晖官方会不定期发布其产品的安全公告,结合群晖的镜像仓库,可以很方便地去做补丁分析和漏洞挖掘。

小结

针对群晖NAS的远程使用场景,本文重点对web接口上请求的流程和处理机制进行了分析。同时,结合几个典型的套件,基于上述流程,分享了在其中发现的部分安全问题。

本文是该系列的最后一篇,希望对群晖NAS设备感兴趣的同学有所收获。

相关链接

  • Synology-SA-20:07 Synology Calendar
  • Synology-SA-21:21 Audio Station
  • 聊聊 Synology NAS Audio Station 套件未授权 RCE 调试及 EXP 构造
  • 群晖产品安全公告
  • 群晖镜像仓库

本文首发于安全客,文章链接:https://www.anquanke.com/post/id/266297