DCS-960L Equipment Analysis

DCS-960L Equipment Analysis

Preliminary Analysis

I came into contact with this device during security research, and I took time to briefly study this device in depth.

The firmware used in this analysis is 1.09, and the download address is

https://www.dlinktw.com.tw/techsupport/ProductInfo.aspx?m=DCS-960L

Web Service Architecture Analysis

We can directly use binwalk to unpack this firmware package.

1
2
3
4
DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
10264 0x2818 LZMA compressed data, properties: 0x5D, dictionary size: 16777216 bytes, uncompressed size: 5230012 bytes
1565730 0x17E422 Squashfs filesystem, little endian, version 4.0, compression:lzma, size: 8614736 bytes, 1569 inodes, blocksize: 131072 bytes, created: 2038-04-24 08:06:24

After unpacking, you can see following things.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
17:53:04 z1r0@z1r0deMBP.lan squashfs-root l
total 8
drwxr-xr-x@ 20 z1r0 staff 640B 7 19 12:32 .
drwxr-xr-x 10 z1r0 staff 320B 7 18 16:32 ..
drwxr-xr-x@ 54 z1r0 staff 1.7K 11 28 2019 bin
drwxr-xr-x@ 4 z1r0 staff 128B 11 28 2019 dev
drwxr-xr-x@ 50 z1r0 staff 1.6K 11 28 2019 etc
drwxr-xr-x@ 11 z1r0 staff 352B 11 28 2019 home
drwxr-xr-x@ 46 z1r0 staff 1.4K 11 28 2019 lib
drwxr-xr-x@ 6 z1r0 staff 192B 11 27 2019 mnt
drwxr-xr-x@ 13 z1r0 staff 416B 11 27 2019 mydlink
drwxr-xr-x@ 2 z1r0 staff 64B 11 27 2019 proc
drwxr-xr-x@ 2 z1r0 staff 64B 11 27 2019 root
drwxr-xr-x@ 29 z1r0 staff 928B 11 28 2019 sbin
drwxr-xr-x@ 24 z1r0 staff 768B 11 28 2019 server
drwxr-xr-x@ 2 z1r0 staff 64B 11 27 2019 share
drwxr-xr-x@ 2 z1r0 staff 64B 11 27 2019 sys
lrwxr-xr-x@ 1 z1r0 staff 7B 11 28 2019 tmp -> var/tmp
drwxr-xr-x@ 6 z1r0 staff 192B 11 27 2019 usr
drwxr-xr-x@ 2 z1r0 staff 64B 11 27 2019 var
drwxr-xr-x@ 14 z1r0 staff 448B 7 19 16:54 web

In etc/passwd_default I found the default user and password. user name is admin, password is empty.

1
admin::0:0:root:/:/bin/sh

After analyzing the rcS startup scripts (etc/init.d/rcS

I found out that it starts the httpd program.

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
#!/bin/sh

# httpd: This starts and stops http server.
#
# chkconfig: 2345 90 10
# description: httpd
#
# processname: /web/httpd

# automatically export variables to the environment
set -a

PATH=/sbin:/bin:/usr/sbin:/usr/bin

http_port=`/usr/sbin/userconfig -read HTTP Port`
https_enable=`/usr/sbin/userconfig -read HTTPS Enable`

start() {
echo -n "Starting httpd ... "
cd /web; ./httpd $http_port 1&
echo "."
}

stop() {
echo -n "Stopping httpd ... "
kill `pidof httpd`
echo "."
}

restart() {
stop
sleep 1
start
}

reload() {
echo -n "Reloading httpd ... "
kill -USR1 `pidof httpd`
}

case "$1" in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
reload)
reload
;;
*)
echo $"Usage $0 {start|stop|restart}"
exit 1
esac

exit $?

The httpd is a small web server, so I decided to analyze it, the relevant code is as follows.

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
75
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // $v0
int v5; // $s1
int v6; // $a0
int v7; // $v0
int v8; // $v0
int v9; // $s2
const char *Peer6; // $v0
__pid_t v11; // $v0
__pid_t v12; // $v0
char v13; // [sp+20h] [-18h] BYREF
char v14[7]; // [sp+21h] [-17h] BYREF
char *dest; // [sp+28h] [-10h]
const char *port; // [sp+2Ch] [-Ch]
int v17; // [sp+30h] [-8h]
int v18; // [sp+34h] [-4h]

if ( (unsigned int)(argc - 2) >= 4 )
{
sub_401A40("httpd");
return 1;
}
v13 = 1;
v14[0] = 1;
if ( argc >= 2 )
port = argv[1];
else
port = "80";
if ( argc < 3 )
v4 = AuthType;
else
v4 = atoi(argv[2]);
AuthType = v4;
v5 = skOpen6(0, port, 2);
if ( v5 < 0 )
logx(3, "%s %s %d", "httpd.c", "main", 1275);
setpgrp();
signal(13, (__sighandler_t)1);
signal(18, handler);
signal(2, (__sighandler_t)sub_401950);
signal(15, (__sighandler_t)sub_401950);
signal(16, (__sighandler_t)sub_402BF0);
signal(17, (__sighandler_t)sub_402BF0);
addr = mmap(0, 0x6590u, 3, 2049, -1, 0);
if ( !addr )
{
fwrite("httpd: mmap failed\\n", 1u, 0x13u, stderr);
return 1;
}
cfgRead("HTTP", "SnapshotAuthentication", &v13);
cfgRead("HTTP", "LivevideoAuthentication", v14);
if ( v13 )
{
if ( v14[0] )
{
fwrite("httpd: Authentication Mode: Normal\\n", 1u, 0x23u, stderr);
usrInit(1);
}
else
{
fwrite("httpd: Authentication Mode: Streaming pass\\n", 1u, 0x2Bu, stderr);
usrInit(3);
}
}
else if ( v14[0] )
{
fwrite("httpd: Authentication Mode: Snapshot pass\\n", 1u, 0x2Au, stderr);
usrInit(2);
}
else
{
fwrite("httpd: Authentication Mode: Snapshot and streaming pass\\n", 1u, 0x38u, stderr);
usrInit(4);
}

First put httpd on port 80 for listening, then a series of signal processing mechanisms will be registered.

Call mmap() function to map a memory space with a size of 0x6590 bytes, and assign the returned mapped address to the variable addr.

If assign success, It will read the SnapshotAuthentication and LivevideoAuthentication values from configuration file, and initialize the user authentication mode based on these two values.

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
	sub_402BF0(17);
AspInitial();
if ( !dword_422C88 )
{
dest = (char *)&unk_4223D0;
v6 = v5;
do
{
v7 = skSelect(v6, 0, 500000);
if ( v7 == -1 )
{
logx(3, "%s %s %d", "httpd.c", "main", 1324);
}
else if ( v7 )
{
v8 = skAccept(v5);
v9 = v8;
if ( v8 < 0 )
logx(3, "%s %s %d", "httpd.c", "main", 1327);
Peer6 = (const char *)skGetPeer6(v9, 1);
strcpy(dest, Peer6);
v11 = fork();
if ( v11 == -1 )
{
LABEL_31:
logx(3, "%s %s %d", "httpd.c", "main", 1337);
}
else if ( !v11 )
{
skClose(v5);
fd = v9;
v18 = dword_422414;
v17 = AuthType;
v12 = getpid();
sub_403038(v18, v17, v12, port);
goto LABEL_31;
}
skClose(v9);
}
usleep(0x30D40u);
v6 = v5;
}
while ( !dword_422C88 );
}
skClose(v5);
munmap(addr, 0x6590u);
usrFree();
AspRelease();
kill(0, 15);
waitpid(0, 0, 0x40000000);
return 0;
}

Call the AspInitial() function to initialize the corresponding ASP, enter the do while loop to monitor whether someone is connected.

If someone connects, fork a child process to call the sub_403038 function to handle client requests.

Finally, call some function to close corresponding request. So then start analyzing sub_403038 function

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
void __fastcall __noreturn sub_403038(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, const char *a9)
{
struct _IO_FILE *v10; // $a0
const char **v11; // $s0
const char *v12; // $v0
int v13; // $v0
bool v14; // dc
_BOOL4 v15; // $v0
const char *v16; // $v0
char *v17; // $v1
int v18; // $a3
int v19; // $a2
int v20; // $a1
int v21; // $a1
int v22; // $a0
int v23; // $v0
const char *v24; // $s0
size_t v25; // $v0
int v26; // $v0
const char *v27; // $a0
const char **v28; // $s6
const char *v29; // $s7
char *v30; // $v0
const char *v31; // $a0
char *v32; // $s6
char *v33; // $v0
int v34; // $s7
const char **v35; // [sp+20h] [-430h] BYREF
char v36[256]; // [sp+24h] [-42Ch] BYREF
int v37; // [sp+124h] [-32Ch] BYREF
int v38; // [sp+128h] [-328h] BYREF
char v39[256]; // [sp+12Ch] [-324h] BYREF
char v40[516]; // [sp+22Ch] [-224h] BYREF
char *v41; // [sp+430h] [-20h]
char *v42; // [sp+434h] [-1Ch]
char *v43; // [sp+438h] [-18h]
char *v44; // [sp+43Ch] [-14h]
char *v45; // [sp+440h] [-10h]
char *v46; // [sp+444h] [-Ch]
char *v47; // [sp+448h] [-8h]
char *format; // [sp+44Ch] [-4h]

if ( (dword_422C8C & 2) != 0 )
v10 = stderr;
else
v10 = 0;
v11 = (const char **)reqInit(v10);
v35 = v11;
memset(v36, 0, sizeof(v36));
v37 = 0;
v38 = 0;
if ( dword_422C90 && v11 )
v11[10] = (const char *)1;
if ( (dword_422C8C & 1) != 0 )
fprintf(stderr, "%s: connected\\n", a9);
close(0);
close(1);
v41 = ".cgi";
v42 = "/fvcgi/storage/localstorage_download.ts";
v44 = "/onvif/onvif_service";
v45 = "/cam/webapi_service";
v46 = "/hnap/hnap_service";
v47 = "/localrecording/";
format = "%s";
while ( 1 )
{
do
{
if ( !v35 || reqRead(v35, fd) < 0 )
{
LABEL_75:
if ( (dword_422C8C & 1) != 0 )
fprintf(stderr, "%s: disconnected\\n", a9);
sub_401950(0);
if ( v35 )
reqFree(v35);
munmap(addr, 0x6590u);
exit(0);
}
v12 = *v35;
}
while ( !**((_BYTE **)*v35 + 6) || !**((_BYTE **)v12 + 5) );
if ( !a1 )
*(_DWORD *)v12 = 0;
v13 = sub_402B78();
v14 = v13 >= 1024;
v15 = v13 < 3072;
if ( v14 )
{
if ( v15 )
sleep(1u);
v38 = 0;
}
else
{
sleep(3u);
v38 = 0;
}
readUpFwStatus(&v38);
if ( v38 == 1 )
{
v16 = "503 Service Not Available\\r\\nFirmware Upgrading Please Wait\\r\\n";
v17 = v39;
do
{
v18 = *((_DWORD *)v16 + 1);
v19 = *((_DWORD *)v16 + 2);
v20 = *((_DWORD *)v16 + 3);
*(_DWORD *)v17 = *(_DWORD *)v16;
*((_DWORD *)v17 + 1) = v18;
*((_DWORD *)v17 + 2) = v19;
*((_DWORD *)v17 + 3) = v20;
v16 += 16;
v17 += 16;
}
while ( v16 != "ease Wait\\r\\n" );
v21 = *(_DWORD *)v16;
v22 = *((_DWORD *)v16 + 1);
v23 = *((_DWORD *)v16 + 2);
*(_DWORD *)v17 = v21;
*((_DWORD *)v17 + 1) = v22;
*((_DWORD *)v17 + 2) = v23;
memset(&v39[60], 0, 0xC4u);
v24 = v35[8];
if ( v24
&& (!strcmp(v35[8], "/cgi/finish.cgi")
|| !strcmp(v24, "/cgi/admin/finish.cgi")
|| !strcmp(v24, "/cgi/maker/finish.cgi")) )
{
skPrint(fd, "HTTP/1.1 200\\r\\n");
skPrint(fd, "Content-Type: text/plain\\r\\n");
skPrint(fd, "Content-Length: %d\\r\\n\\r\\n", 10);
skPrint(fd, "finish=%d\\r\\n", v38);
fprintf(stderr, "finish=%d (response by httpd)\\n", v38);
sub_401950(0);
reqFree(v35);
exit(0);
}
skPrint(fd, "HTTP/1.1 503\\r\\n");
skPrint(fd, "Content-Type: text/plain\\r\\n");
v25 = strlen(v39);
skPrint(fd, "Content-Length: %d\\r\\n\\r\\n", v25);
skPrint(fd, "%s", v39);
sub_401950(0);
reqFree(v35);
exit(0);
}
if ( strstr(a9, "127.0.0.1") )
v26 = sub_401C2C(0, v35, v36, a9) + 414;
else
v26 = sub_401C2C(a2, v35, v36, a9);
v27 = a9;
switch ( v26 )
{
case -414:
sub_402898(v35, "414.html", "414", *(_DWORD *)*v35);
goto LABEL_70;
case -413:
sub_402898(v35, "413.html", "413", *(_DWORD *)*v35);
goto LABEL_70;
case -412:
case -411:
case -410:
case -409:
case -408:
case -407:
case -406:
case -405:
case -402:
v27 = a9;
goto LABEL_42;
case -404:
sub_402898(v35, "404.html", "404", *(_DWORD *)*v35);
goto LABEL_70;
case -403:
sub_402898(v35, "403.html", "403", *(_DWORD *)*v35);
goto LABEL_70;
case -401:
if ( !**((_BYTE **)*v35 + 8) )
goto LABEL_61;
sub_402E20(a9, 1);
sub_401B6C(a9, &v37);
if ( v37 < 4 )
goto LABEL_61;
sub_402898(v35, "401.html", "200", *(_DWORD *)*v35);
sub_402E20(a9, 0);
goto LABEL_70;
default:
LABEL_42:
sub_402E20(v27, 0);
v28 = v35;
v43 = (char *)v35[4];
if ( !strcmp(v43, v41) )
goto LABEL_64;
v29 = v28[8];
if ( !strcmp(v29, v42)
|| !strcmp(v29, v44)
|| !strcmp(v29, v45)
|| !strcmp(v29, v46)
|| !strncmp(v29, v47, 0x10u) )
{
goto LABEL_64;
}
if ( strncmp(v29, "/videostorage/", 0xEu) )
{
if ( strncmp(v29, "/greece/", 8u) )
{
if ( strcmp(v43, ".asp") )
{
sub_402898(v28, v28[7], "200", *(_DWORD *)*v28);
}
else
{
v34 = hoInit(v28);
if ( aspLoad(v28[7], v34, v28[1], v36) >= 0 )
{
sub_4025F8(*(_DWORD *)*v28, "200", ".asp", v28[8]);
hoFree(v34, fd, dword_422C8C & 4);
}
else
{
sub_402898(v28, "404.html", "404", *(_DWORD *)*v28);
hoFree(v34, 0, dword_422C8C & 4);
}
}
}
else
{
LABEL_64:
sub_402040(&v35, a4, v36);
}
goto LABEL_70;
}
memset(v39, 0, 16);
if ( !v35[1] )
goto LABEL_57;
memset(v40, 0, 512);
snprintf(v40, 0x200u, format, v35[1]);
v30 = strstr(v40, "access_token=");
v31 = a9;
if ( v30 )
{
v32 = v30 + 13;
v33 = strchr(v30 + 13, 38);
if ( v33 )
*v33 = 0;
snprintf(v39, 0x10u, format, v32);
LABEL_57:
v31 = a9;
}
if ( !strstr(v31, "127.0.0.1")
&& checkToken_localrecording(a9, v39) == -1
&& checkToken_localrecording(a9, *((_DWORD *)*v35 + 16)) == -1 )
{
LABEL_61:
sub_402898(v35, "401.html", "401", *(_DWORD *)*v35);
}
else
{
sub_402898(v35, v35[7], "200", *(_DWORD *)*v35);
}
LABEL_70:
if ( byte_422418[0] && !*(_DWORD *)*v35 && strstr(v35[8], byte_422418) )
goto LABEL_75;
break;
}
}
}

The function is very long and important, it does a lot of things.

First call the reqInit() function to initialize the request, then call the sub_401C2C() function to handle requests, and do different processing according to the return value.

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
int __fastcall sub_401C2C(int a1, const char **a2, int a3)
{
bool v6; // dc
int result; // $v0
const char *v8; // $s3
size_t v9; // $v0
int v10; // $v0
const char *v11; // $a0
int v12; // $v0
size_t v13; // $s1
const char *v14; // $s3
const char *v15; // $a0
int v16; // $a1
int v17; // $s3
struct stat v18; // [sp+20h] [-124h] BYREF
int v19; // [sp+B8h] [-8Ch] BYREF
char v20[64]; // [sp+BCh] [-88h] BYREF
char v21[72]; // [sp+FCh] [-48h] BYREF

v6 = atoi(*((const char **)*a2 + 12)) >= 0xD00001;// Content-Length
result = -414;
if ( v6 )
return result;
v8 = a2[7]; // url
result = -404;
if ( !v8 )
return result;
v9 = strlen(a2[5]);
if ( strncmp(v8, a2[5], v9) && strncmp(v8, "/mnt/storage", 0xCu) && strncmp(v8, "/var/tmp/device", 0xFu) )
{
v10 = strncmp(v8, "/var/tmp/mpegts", 0xFu);
v11 = v8;
if ( !v10 )
goto LABEL_9;
v6 = strncmp(v8, "/mnt/storage/local_recording", 0x1Cu) != 0;
result = -404;
if ( v6 )
return result;
}
v11 = v8;
LABEL_9:
v6 = stat(v11, &v18) != 0;
result = -404;
if ( v6 )
return result;
if ( v18.st_blocks <= 0 )
return -404;
v12 = v18.st_nlink & 0xF000;
if ( v12 == 0x8000 || (v6 = v12 != 40960, result = -404, !v6) )
{
if ( !a1 )
{
if ( a3 )
{
memset(v20, 0, sizeof(v20));
memset(v21, 0, 64);
cfgRead("USER_ADMIN", "Username1", v20);
cfgRead("USER_ADMIN", "Password1", v21);
usrEncBasic(v20, v21, a3);
return 0;
}
return 0;
}
v13 = strlen(a2[5]);
v14 = a2[7];
if ( !strncmp(v14, "/var/tmp/device", 0xFu) || !strncmp(v14, "/mnt/storage/local_recording", 0x1Cu) )
return 0;
if ( strncmp(v14, "/var/tmp/mpegts", 0xFu) )
{
if ( !strncmp(v14, "/mnt/storage", 0xCu) )
v13 = 12;
}
else
{
v13 = 15;
}
if ( dword_422C90 && (v15 = a2[9]) != 0 )
{
return usrAuth(v15, &v14[v13], *((_DWORD *)*a2 + 5), a3);
}
else
{
v19 = 0;
v16 = 0;
v17 = 0;
while ( 1 )
{
++v17;
if ( !strcmp(a2[8], &aImage2JpegCgi[128 * v16]) )
break;
v16 = v17;
if ( v17 == 11 )
{
usrAuth(*((_DWORD *)*a2 + 8), &a2[7][v13], *((_DWORD *)*a2 + 5), a3);
v19 = 0;
cfgRead("HTTP", "Authenticate", &v19);
return usrAuthByAuthenticate(*((_DWORD *)*a2 + 8), &a2[7][v13], *((_DWORD *)*a2 + 5), a3);
}
}
return usrAuthByAuthenticate(*((_DWORD *)*a2 + 8), &a2[7][v13], *((_DWORD *)*a2 + 5), a3);
}
}
return result;
}

If requested Content-Length ≥ 13631489, return 414.

If url is empty, return 404.

If it is a whitelist, return 0.

If file does not exist, return 404.

Different validations are determined according to the values of a1 and a3.

  • If a1 == 0 && a3 ≠ 0
    • Encrypt username and password with base64
  • if a1 == 0 && a3 == 0
    • return 0

If url is not whitelist.

  • Authenticate
  • return status code

Call the corresponding case according to the return status code.

If the status code is equal to 200.

  • if request is scheduled

    • execute sub_402040 function
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    int __fastcall sub_402040(_DWORD *a1, int a2, int a3)
    {
    char *const *Env; // $s2
    char *const *Arg; // $s3

    Env = (char *const *)reqMakeEnv(*a1, fd, a2, a3);
    Arg = (char *const *)reqMakeArg(*a1);
    if ( dup(fd) || dup(fd) != 1 )
    logx(3, "%s %s %d", "httpd.c", "runcgi", 823);
    fcntl(fd, 2, 1);
    execve(*(const char **)(*a1 + 28), Arg, Env);
    sub_401950();
    reqFree(*a1);
    return logx(3, "%s %s %d", "httpd.c", "runcgi", 830);
    }

    call the execve function to execute

  • if request is .asp

    • execute asp function

During the response, the access_token will be checked, if the access_token is wrong, it will be 401. Call checkToken_localrecording() for access control.

In the above analysis, it was found that there is a hnap service.

hnap service is a very old protocol, d-link began to deprecate this protocol in 2016, so many bugs happen here.

Vulnerability

Unauthenticated Information Disclosure

In the above analysis, there is a very interesting code snippet.

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
void __fastcall __noreturn sub_403038(int live, int authtype, int pid, int port)
{
......
v37 = "/fvcgi/storage/localstorage_download.ts";
v39 = "/onvif/onvif_service";
v40 = "/cam/webapi_service";
v41 = "/hnap/hnap_service";
v42 = "/localrecording/";
......
if ( !strcmp(v24, v37)
|| !strcmp(v24, v39)
|| !strcmp(v24, v40)
|| !strcmp(v24, v41)
|| !strncmp(v24, v42, 0x10u) )
{
goto LABEL_64;
}
......
LABEL_64:
sub_402040((char *)&v6 + 5, port, v31);
}
......
if ( !strstr(v26, "127.0.0.1")
&& checkToken_localrecording(v46, v34) == -1
&& checkToken_localrecording(v46, *(_DWORD *)(*(_DWORD *)v6 + 64)) == -1 )
{
.......

If url is /hnap/hnap_service , it will go to LABEL_64, LABEL_64 call sub_402040 function, sub_402040 function use execve function to call hnap_service cgi program.

But after execution is over, checkToken_localrecording() will be called for access control. So hnap_service can be used without authentication.

If you access hnap_service with a GET request, the GetDeviceSettings function will be called.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int __cdecl main(int argc, const char **argv, const char **envp)
{
....
if ( !strcmp(v8, "GET") )
{
memset(v25, 0, sizeof(v25));
v18 = sub_4021EC(0, "GetDeviceSettings", 0);
v14 = v18;
if ( !v18 )
{
v13 = 0;
goto LABEL_21;
}
...

The GetDeviceSettings function will output import informations such as FirmwareVersion/MacAddr and so on.

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
int GetDeviceSettings()
{
int Document; // $s4
int Element; // $s5
char *v2; // $s0
size_t v3; // $s1
int appended; // $s1
int v6; // $s1
int v7; // $s0
char v9[512]; // [sp+28h] [-450h] BYREF
char v10[256]; // [sp+228h] [-250h] BYREF
char v11[256]; // [sp+328h] [-150h] BYREF
char v12[68]; // [sp+428h] [-50h] BYREF
int v13; // [sp+46Ch] [-Ch]
__int16 v14; // [sp+470h] [-8h]
unsigned __int16 v15; // [sp+472h] [-6h] BYREF
unsigned __int16 v16; // [sp+474h] [-4h] BYREF
__int16 v17; // [sp+476h] [-2h] BYREF

memset(&v9[1], 0, 0x1FFu);
memset(v10, 0, sizeof(v10));
memset(v11, 0, sizeof(v11));
memset(v12, 0, 65);
v15 = 0;
v16 = 0;
v17 = 0;
Document = ixmlDocument_createDocument();
Element = ixmlDocument_createElement(Document, "GetDeviceSettingsResponse");
ixmlElement_setAttribute(Element, "xmlns", "<http://purenetworks.com/HNAP1/>");
ixmlNode_appendChild(Document, Element);
ixmlAppendNewElement(Document, Element, "GetDeviceSettingsResult", "OK");
ixmlAppendNewElement(Document, Element, "Type", "ConnectedHomeClient");
memset(v11, 0, sizeof(v11));
cfgRead("System", "ModelName", v11);
memset(v9, 0, sizeof(v9));
memset(v12, 0, 0x41u);
cfgRead("CAMSYSTEM", "CameraName", v9);
cfgRead("CAMSYSTEM", "CameraName", v12);
if ( strlen(v9) >= 0x21 )
v9[33] = 0;
ixmlAppendNewElement(Document, Element, "DeviceName", v9);
memset(v9, 0, sizeof(v9));
cfgRead("INFO", "BrandName", v9);
ixmlAppendNewElement(Document, Element, "VendorName", v9);
memset(v9, 0, sizeof(v9));
cfgRead("INFO", "Product", v9);
ixmlAppendNewElement(Document, Element, "ModelDescription", v9);
ixmlAppendNewElement(Document, Element, "ModelName", v11);
v13 = 0;
v14 = 0;
net_get_hwaddr("br0");
sprintf(
v9,
"%02X:%02X:%02X:%02X:%02X:%02X",
HIBYTE(v13),
BYTE1(v13),
BYTE2(v13),
(unsigned __int8)v13,
HIBYTE(v14),
(unsigned __int8)v14);
ixmlAppendNewElement(Document, Element, "DeviceMacId", v9);
cfgRead("System", "MainVersion", &v15);
cfgRead("System", "FirmwareVersion", &v16);
cfgRead("System", "CustomerVersion", &v17);
sprintf(v9, "%d.%02d", v15, v16);
ixmlAppendNewElement(Document, Element, "FirmwareVersion", v9);
ixmlAppendNewElement(Document, Element, "FirmwareRegion", "Default");
sprintf(v9, "%d.%02d", v15, v16);
ixmlAppendNewElement(Document, Element, "LatestFirmwareVersion", v9);
memset(v9, 0, sizeof(v9));
cfgRead("INFO", "HWVersion", v9);
ixmlAppendNewElement(Document, Element, "HardwareVersion", v9);
ixmlAppendNewElement(Document, Element, "HNAPVersion", "0111");
cfgRead("Bonjour", "PresentationURL", v10);
if ( v10[0] )
{
sprintf(v9, "http://%s.local./", v10);
v2 = v9;
}
else
{
if ( v12[0] )
sprintf(v9, "http://%s.local./", v12);
else
sprintf(v9, "http://%s.local./", v11);
v2 = v9;
}
v3 = 0;
while ( v3++ < strlen(v9) )
{
*v2 = *(_WORD *)(_ctype_tolower + 2 * *v2);
++v2;
}
ixmlAppendNewElement(Document, Element, "PresentationURL", v9);
ixmlAppendNewElement(Document, Element, "CAPTCHA", "false");
appended = ixmlAppendNewElement(Document, Element, "ModuleTypes", 0);
ixmlAppendNewElement(Document, appended, "string", "Optical Recognition");
ixmlAppendNewElement(Document, appended, "string", "Environmental Sensor");
ixmlAppendNewElement(Document, appended, "string", "Camera");
v6 = ixmlAppendNewElement(Document, Element, "SOAPActions", 0);
ixmlAppendNewElement(Document, v6, "string", "<http://purenetworks.com/HNAP1/Login>");
ixmlAppendNewElement(Document, v6, "string", "<http://purenetworks.com/HNAP1/GetDCHPolicy>");
ixmlAppendNewElement(Document, v6, "string", "<http://purenetworks.com/HNAP1/SetDCHPolicy>");
ixmlAppendNewElement(Document, v6, "string", "<http://purenetworks.com/HNAP1/GetNotifierSettings>");
ixmlAppendNewElement(Document, v6, "string", "<http://purenetworks.com/HNAP1/SetNotifierSettings>");
ixmlAppendNewElement(Document, v6, "string", "<http://purenetworks.com/HNAP1/GetEventSupportList>");
ixmlAppendNewElement(Document, v6, "string", "<http://purenetworks.com/HNAP1/GetActionSupportList>");
ixmlAppendNewElement(Document, v6, "string", "<http://purenetworks.com/HNAP1/PushDCHEvent>");
ixmlAppendNewElement(Document, v6, "string", "<http://purenetworks.com/HNAP1/GetmydlinkSupportStatus>");
ixmlAppendNewElement(Document, v6, "string", "<http://purenetworks.com/HNAP1/GetmydlinkRegInfo>");
ixmlAppendNewElement(Document, v6, "string", "<http://purenetworks.com/HNAP1/SetmydlinkReg>");
ixmlAppendNewElement(Document, v6, "string", "<http://purenetworks.com/HNAP1/SetmydlinkUnregistration>");
ixmlAppendNewElement(Document, v6, "string", "<http://purenetworks.com/HNAP1/GetDeviceSettings>");
ixmlAppendNewElement(Document, v6, "string", "<http://purenetworks.com/HNAP1/SetDeviceSettings>");
ixmlAppendNewElement(Document, v6, "string", "<http://purenetworks.com/HNAP1/GetDeviceSettings2>");
ixmlAppendNewElement(Document, v6, "string", "<http://purenetworks.com/HNAP1/SetDeviceSettings2>");
ixmlAppendNewElement(Document, v6, "string", "<http://purenetworks.com/HNAP1/GetModuleProfile>");
ixmlAppendNewElement(Document, v6, "string", "<http://purenetworks.com/HNAP1/SetModuleProfile>");
ixmlAppendNewElement(Document, v6, "string", "<http://purenetworks.com/HNAP1/SetTriggerWirelessSiteSurvey>");
ixmlAppendNewElement(Document, v6, "string", "<http://purenetworks.com/HNAP1/GetSiteSurvey>");
ixmlAppendNewElement(Document, v6, "string", "<http://purenetworks.com/HNAP1/GetWLanRadios>");
ixmlAppendNewElement(Document, v6, "string", "<http://purenetworks.com/HNAP1/GetAPClientSettings>");
ixmlAppendNewElement(Document, v6, "string", "<http://purenetworks.com/HNAP1/SetAPClientSettings>");
ixmlAppendNewElement(Document, v6, "string", "<http://purenetworks.com/HNAP1/GetMultipleHNAPs>");
ixmlAppendNewElement(Document, Element, "SubDeviceURLs", 0);
v7 = ixmlNode_cloneNode(Document, 1);
ixmlDocument_free(Document);
return v7;
}

Stack Overflow

In the Login function, I can enter Action, the Action will strcpy to v42, but program do not check size, resulting in stack overflow.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __fastcall Login(int a1)
{
......
ElementByTag = ixmlGetElementByTag(a1, "Login");
v5 = ElementByTag;
if ( !ElementByTag )
{
ixmlAppendNewElement(Document, Element, "LoginResult", "failed");
v15 = Document;
goto LABEL_49;
}
ElementValueByTag = (const char *)ixmlGetElementValueByTag(ElementByTag, "Action");
if ( ElementValueByTag )
strcpy(v42, ElementValueByTag);
......
}

In the Login function, I can enter Username and LoginPassword, the username&password will strcpy to v36&v38, but program do not check the size, resulting in stack overflow.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int __fastcall Login(int a1)
{
......
v16 = (const char *)ixmlGetElementValueByTag(v5, "Username");
v17 = (const char *)ixmlGetElementValueByTag(v5, "LoginPassword");
if ( v16 )
{
strcpy((char *)v36, v16);
if ( !strcmp((const char *)v36, "Admin") )
snprintf((char *)v36, 0x20u, "%s", "admin");
}
if ( v17 )
strcpy(v38, v17);
v53 = (const char *)v36;
fprintf(stderr, "username: %s\\n", (const char *)v36);
v55 = v38;
fprintf(stderr, "loginPassword: %s\\n", v38);
......
}

In the Login function, the program use the getenv function to obtain COOKIE value, and use the snprintf function to put the value into v39, without %s, resulting format string vuln

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
int __fastcall Login(int a1)
{
......
v7 = getenv("COOKIE");
if ( v7 && *v7 )
{
memset(v43, 0, sizeof(v43));
v9 = getenv("COOKIE");
snprintf(v43, 0x80u, "%s", v9);
v10 = strstr(v43, "uid=");
if ( v10 )
{
v11 = v10 + 4;
v12 = strchr(v10 + 4, 59);
if ( v12 )
*v12 = 0;
snprintf((char *)v39, 0xBu, v11);
v13 = (const char *)&v52[18];
}
else
{
snprintf((char *)v39, 0xBu, v43);
v13 = (const char *)&v52[18];
}
......
}

Authentication Bypass

In the Login function, Enter the Username and LoginPassword, then call usrGetPass function to obtain the password corresponding to the Username.

challenge + challenge length + public_key+password + public_key length+password length performs hmac_md5 function to get private key.

Then, challenge + challenge length + private_key + private_key length performs hmac_md5 function to get login_password.

If login_password equal to save password, then login successful.

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
int __fastcall Login(int a1)
{
......
v16 = (const char *)ixmlGetElementValueByTag(v5, "Username");
v17 = (const char *)ixmlGetElementValueByTag(v5, "LoginPassword");
if ( v16 )
{
strcpy((char *)v36, v16);
if ( !strcmp((const char *)v36, "Admin") )
snprintf((char *)v36, 0x20u, "%s", "admin");
}
if ( v17 )
strcpy(v38, v17);
v53 = (const char *)v36;
fprintf(stderr, "username: %s\\n", (const char *)v36);
v55 = v38;
fprintf(stderr, "loginPassword: %s\\n", v38);
memset(v51, 0, 53);
memset(v49, 0, 33);
memset(v50, 0, 33);
v45 = 0;
v46 = 0;
v47 = 0;
v48 = 0;
usrInit(0);
usrGetPass(v53, v50, 33);
usrFree();
v54 = (const char *)v52;
sprintf(v51, "%s%s", (const char *)v52, v50);
v53 = (const char *)&v52[9];
fprintf(stderr, "My challenge: %s\\n", (const char *)&v52[9]);
fprintf(stderr, "My public_key: %s\\n", v54);
fprintf(stderr, "My password: %s\\n", v50);
v18 = strlen(v53);
v19 = strlen(v51);
hmac_md5(v53, v18, v51, v19);
sprintf(v44, "%08X%08X%08X%08X", v45, v46, v47, v48);
fprintf(stderr, "My private_key: %s\\n", v44);
v45 = 0;
v46 = 0;
v47 = 0;
v48 = 0;
v54 = (const char *)strlen(v53);
v20 = strlen(v44);
hmac_md5(v53, v54, v44, v20);
sprintf(v49, "%08X%08X%08X%08X", v45, v46, v47, v48);
fprintf(stderr, "My login_password: %s\\n", v49);
v21 = strcmp(v55, v49) == 0;
fprintf(stderr, "Check authStatus: %d\\n", v21);
v22 = Document;
if ( v21 )
{
v23 = time(0);
if ( HIBYTE(v52[3018]) && HIBYTE(v52[3027]) && v23 - v52[3031] < 301 && (v24 = &v52[3048], v23 >= v52[3031]) )
{
v25 = 1;
while ( 1 )
{
v26 = v24[1];
if ( !HIBYTE(v52[18 * v25 + 3018]) )
break;
if ( !HIBYTE(v52[18 * v25 + 3027]) )
break;
v27 = v23 - v26 >= 301;
v28 = v23 < v26;
if ( v27 )
break;
++v25;
if ( v28 )
{
--v25;
break;
}
v24 += 18;
if ( v25 == 1000 )
{
ixmlAppendNewElement(Document, Element, "LoginResult", "failed");
v15 = Document;
goto LABEL_49;
}
}
}
else
{
v25 = 0;
}
snprintf((char *)&v52[18 * v25 + 3018], 0x23u, "%s", v44);
snprintf((char *)&v52[18 * v25 + 3027], 0xBu, "%s", (const char *)v39);
v29 = &v36[9 * v25];
v29[1566] = v23;
*((_BYTE *)v29 + 12544) = 0;
if ( strcmp((const char *)v36, "admin") )
BYTE1(v52[18 * v25 + 3034]) = 1;
else
BYTE1(v52[18 * v25 + 3034]) = 0;
SIWriteBin(63, v52, 84072);
ixmlAppendNewElement(Document, Element, "LoginResult", "success");
v15 = Document;
goto LABEL_49;
}
}
v30 = Element;
}
else
......
}

But in the usrGetPass function, if the username I enter is not in the user list, v6=21, result = -1 and a2 = null . So I just need to get the challenge and public key, and then follow the above hmac_md5 calculation go get the correct passwod.

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
int __fastcall usrGetPass(const char *a1, char *a2, size_t a3)
{
int result; // $v0
int v6; // $s3
const char **v7; // $s2
const char *v8; // $v0
int v9; // $v0
size_t n; // [sp+18h] [-8h]

if ( !*a1 )
return -1;
v6 = 0;
v7 = (const char **)&dword_21DC8;
while ( 1 )
{
v8 = *v7;
v7 += 3;
if ( v8 )
{
n = a3;
v9 = strcmp(v8, a1);
a3 = n;
if ( !v9 )
break;
}
++v6;
result = -1;
if ( v6 == 21 )
return result;
}
strncpy(a2, *((const char **)&unk_21DC4 + 3 * v6 + 2), n);
return 1;
}

Affected Models

Model Download Links
DCS-960L https://d2okd4tdjucp2n.cloudfront.net/DCS-960L/DCS-960L_A1_FW_1.09.02.zip
DCS-935L https://d2okd4tdjucp2n.cloudfront.net/DCS-935L/DCS-935L_A1_FW_1.13.01_r4589.zip
DCS-8200LH https://www.dlinktw.com.tw/techsupport/download.ashx?file=11557

I only analyzed a very small part, there may be other vulnerabilities