Login to Huawei WS5200 Router Using Java Program
The reason I did this is because I realized that I have very poor time management skills. Often, when I’m halfway through reading a book and want to look up something for 10 minutes, I get distracted by news about a certain country dumping nuclear waste. Then I continue reading and see some unruly tourists picking fruits at a certain mountainous scenic area, so I click to take a look. After that, I discover various news articles, and by the time I’m satisfied with reading, it’s already been half an hour or even an hour. I only spend a few minutes looking up information, and the rest of the time is spent reading news. So I wondered if it would be possible to control the router through a program, so that during my reading time, 10 minutes would be exactly 10 minutes, and the internet would be disconnected when the time is up.(English version Translated by GPT-3.5, 返回中文)
Introduction
Enough talk, let’s get to work. Since I only have the WS5200 router at hand, and my computer is connected to this router, I’ll start with this router.
Exploring the Algorithm
Initial Login
First, open the router, press F12 to login, and then look at the “ALL” section. I noticed the following requests for login:
- The router first requests the user_login_nonce interface. - 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- POST /api/system/user_login_nonce HTTP/1.1 
 Host: 10.0.0.10
 Connection: keep-alive
 Content-Length: 214
 Pragma: no-cache
 Cache-Control: no-cache
 Accept: application/json, text/javascript, */*; q=0.01
 X-Requested-With: XMLHttpRequest
 _ResponseFormat: JSON
 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.128 Safari/537.36
 Content-Type: application/json; charset=UTF-8
 Origin: http://10.0.0.10
 Referer: http://10.0.0.10/html/index.html
 Accept-Encoding: gzip, deflate
 Accept-Language: zh-CN,zh;q=0.9
 Cookie: SessionID_R3=4Xj1P0SvG5u5274ClvmYz0g7NoEBIeagDK8UokAbbWfu0zfaqdQYJOyhPEJpCOwAAZpX9LvR38hJaFIegMuSe5FG6eiSpO1zRlZyRaCTZ2qqf3a5os2heaMYcY9jpajW
 Response Header
 HTTP/1.1 200 OK
 Cache-Control: no-cache, no-store, max-age=0, must-revalidate
 Pragma: no-cache
 Content-Type: application/javascript
 X-Download-Options: noopen
 X-Frame-Options: SAMEORIGIN
 X-XSS-Protection: 1; mode=block
 Content-Security-Policy: default-src 'self' 'unsafe-inline' 'unsafe-eval'
 X-Content-Type-Options: nosniff
 Content-Length: 337
 Request Payload
 {
 "data": {
 "username": "admin",
 "firstnonce": "aa0591fd5a971da559b37eb6369eb2d2b25252e4a115e3ab31f6cde19df1eb72"
 },
 "csrf": {
 "csrf_param": "U7FMqd5SX0twxRA0kaKqLiPCfWqkCjLJ",
 "csrf_token": "tUcxR4fDHHrUSNMU650Mp0rd5UXt0iJ7"
 }
 }
 Response Payload
 {
 "csrf_token": "rTtnLfQMHAuGJItKd7MlxaL4WNXmP6EE",
 "salt": "07f0b7e87c2ad06ebd2938c326909307ac1da81460b27fa6d84a8b240ac795fe",
 "csrf_param": "LWFNHzU7dSG0paH0vlUZ70GJG9PItZAL",
 "err": 0,
 "modeselected": 1,
 "servernonce": "aa0591fd5a971da559b37eb6369eb2d2b25252e4a115e3ab31f6cde19df1eb72JREAGMZitcXiwQp2tsnUhLFViZev60z0",
 "isopen": 0,
 "iterations": 100
 }
- Then it immediately requests user_login_proof. - 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- POST /api/system/user_login_proof HTTP/1.1 
 Host: 10.0.0.10
 Connection: keep-alive
 Content-Length: 308
 Pragma: no-cache
 Cache-Control: no-cache
 Accept: application/json, text/javascript, */*; q=0.01
 X-Requested-With: XMLHttpRequest
 _ResponseFormat: JSON
 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.128 Safari/537.36
 Content-Type: application/json; charset=UTF-8
 Origin: http://10.0.0.10
 Referer: http://10.0.0.10/html/index.html
 Accept-Encoding: gzip, deflate
 Accept-Language: zh-CN,zh;q=0.9
 Cookie: SessionID_R3=4Xj1P0SvG5u5274ClvmYz0g7NoEBIeagDK8UokAbbWfu0zfaqdQYJOyhPEJpCOwAAZpX9LvR38hJaFIegMuSe5FG6eiSpO1zRlZyRaCTZ2qqf3a5os2heaMYcY9jpajW
 Response Header
 HTTP/1.1 200 OK
 Cache-Control: no-cache, no-store, max-age=0, must-revalidate
 Pragma: no-cache
 Content-Type: application/javascript
 X-Download-Options: noopen
 X-Frame-Options: SAMEORIGIN
 X-XSS-Protection: 1; mode=block
 Content-Security-Policy: default-src 'self' 'unsafe-inline' 'unsafe-eval'
 X-Content-Type-Options: nosniff
 Content-Length: 183
 Request Payload
 {
 "data": {
 "clientproof": "5b2b96e5b4a421241e05c841c3877cf03d643c30ef8358628a0f8825bd5f4fc8",
 "finalnonce": "aa0591fd5a971da559b37eb6369eb2d2b25252e4a115e3ab31f6cde19df1eb72JREAGMZitcXiwQp2tsnUhLFViZev60z0"
 },
 "csrf": {
 "csrf_param": "LWFNHzU7dSG0paH0vlUZ70GJG9PItZAL",
 "csrf_token": "rTtnLfQMHAuGJItKd7MlxaL4WNXmP6EE"
 }
 }
 Response Payload
 {
 "ishilink": 0,
 "errorCategory": "user_pass_err",
 "err": 4784230,
 "csrf_param": "XzvfGOA3bx41wFc3aKrgKw8bEVT0KZ4d",
 "count": 1,
 "maxfailtimes": 3,
 "csrf_token": "E8KE4pwzHUN350Lpf1KzrDDgIMtETpdn"
 }
 这是登录成功的响应
 {
 "level": 2,
 "rsapubkeysignature": "xxxxxxxxx",
 "rsae": "010001",
 "ishilink": 0,
 "rsan": "yyyyyyyyyyy",
 "err": 0,
 "serversignature": "zzzzzzzzz",
 "csrf_param": "eaXn0d7oYBdOe1HWpe0JrFvpqqt2AGyB", // 这里不一定和上面一致,因为我重新刷新过页面了
 "csrf_token": "iuhI91oH1YoSH90XVVnjhITrpxcMA4Mg"
 }
Simple Analysis
We can see that the Huawei router makes two requests, the second of which is the actual login request. From the requests, I can roughly guess that the username for the router is “admin”, although this information is not really useful. In the second request, the response contains “user_pass_err”, which indicates that I entered a random password. Here are the main things I noticed:
- Every time the router logs in, it carries a “csrf_param” and “csrf_token”. From the two requests mentioned above, it can be inferred that the login request includes the two parameters mentioned earlier. So, I can guess that whenever a new csrf is received, the current csrf parameter needs to be updated. But why is the csrf parameter already present in the first request for user_login_nonce?
- I couldn’t find any passwords or numbers in the requests. It’s not a simple encryption like MD5, because I tried logging in with the password “123456”, and the MD5 hash of “123456” starts with “e10”. However, there was no related content in the requests, so the password must have been processed.
- Looking at the parameters returned by the login, “count” and “maxfailtimes” tell us not to exceed the maximum number of login failures, or we’ll be blocked for a minute.
- There’s also the “Cookie” parameter, which plays a crucial role in HTTP requests. It’s important to see where this parameter is set, as there is usually a “Set-Cookie” response header for setting cookies.
Obtaining the Initial csrf Parameter
Since the first user_login_nonce already includes the csrf parameter, this means that there must be a place where this parameter is given before entering the login page, or it is generated automatically.
Because the token I used for user_login_nonce is “bFOBG8SSFQ4ZqD92j0rOSZXrQnWluYQs”, I just need to search in the Chrome Network for any page or JS that mentions this value. And, indeed, I found it!
On the initial index.html page:
| 1 | GET /html/index.html HTTP/1.1 | 
I found that this token is exactly the same, down to every letter, just like twins!
| 1 | <meta name="csrf_param" content="U7FMqd5SX0twxRA0kaKqLiPCfWqkCjLJ"/> | 
So, it can be concluded that the initial token is obtained from the response of index.html.
Finding the Encryption Part
This encryption has definitely been processed. But usually, when you open the Chrome webpage debugger’s Network tab and filter for JavaScript, you can find the encryption there. Luckily, there aren’t many JS files in this case.
I searched one by one and searched for the keyword ‘user_login_proof’. I found it in the seventh file I checked.
Click on the {} button below the search bar to format the JS text and extract the login JS code as follows:
| 1 | login: function(context, data) { | 
The basic logic can be seen below:
- First, request user_login_nonce with the parameters “username” and “firstNonce”. - 1 - 其中firstNonce是从scram.nonce().toString()算出来的。 
- Then, take the “salt” parameter and “serverNonce” from the response mentioned above. Use them as login parameters and perform the following code calculation, converting the “clientKey” to hexadecimal, and send it in the request. - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16- var salt = CryptoJS.enc.Hex.parse(res['salt']); 
 var iter = res['iterations'];
 var finalNonce = res['servernonce'];
 var authMsg = firstNonce + "," + finalNonce + "," + finalNonce;
 var saltPassword = scram.saltedPassword(password, salt, iter).toString();
 var serverKey = scram.serverKey(CryptoJS.enc.Hex.parse(saltPassword));
 var clientKey = scram.clientKey(CryptoJS.enc.Hex.parse(saltPassword)).toString();
 var storekey = scram.storedKey(CryptoJS.enc.Hex.parse(clientKey));
 storekey = storekey.toString();
 var clientsignature = scram.signature(CryptoJS.enc.Hex.parse(storekey), authMsg);
 clientsignature = clientsignature.toString();
 clientsignature = CryptoJS.enc.Hex.parse(clientsignature);
 clientKey = CryptoJS.enc.Hex.parse(clientKey);
 for (var i = 0; i < clientKey.sigBytes / 4; i++) {
 clientKey.words[i] = clientKey.words[i] ^ clientsignature.words[i]
 }
Organizing the Encryption Part
- Nonce Part - In the above process, the nonce part seems like a random value. When I manually ran this code, it always returned a random string of length 64 composed of [0-9, a-f]. Although I confirmed later that it is indeed a random number. - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12- > CryptoJS.SCRAM({keySize: 8}).nonce().toString() 
 "4f5e4baeb43914052e5ad5236fae7ad4d29d90d46cfab72f6e0919722df7b4a4"
 > CryptoJS.SCRAM({keySize: 8}).nonce().toString()
 "2f914d316187ae65c63d83603569d960a6c0bae0451858a22da60e549c088170"
 > CryptoJS.SCRAM({keySize: 8}).nonce().toString()
 "3a6cd54f571ef42bb3751ce09c0888da3b705ead93e294a4676d140930020438"
 > CryptoJS.SCRAM({keySize: 8}).nonce().toString()
 "af03afe7b13f0cdf70d2f70c4dc71e5b91df0ca144069c563bed6c49c71c51a5"
 > CryptoJS.SCRAM({keySize: 8}).nonce().toString()
 "6844a2c4984b34b698291dffb0cc93ce2e1775b4ce061ddacd52c9d55512fccb"
 > CryptoJS.SCRAM({keySize: 8}).nonce().toString()
 "ada7c66cbe74b9efb9e1d9d32ea424423c4de0552be62ce4a975c32ac58bc1bb"
- User Login Encryption Part - This part is more complicated because it involves multiple algorithms. For now, let’s separate it first. It calls the cat_enc.js file, so after extracting it, create a new HTML page, import the cat_enc.js file, and write the following JS code: - 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- var scram = CryptoJS.SCRAM({keySize: 8}) 
 var firstNonce = "aa0591fd5a971da559b37eb6369eb2d2b25252e4a115e3ab31f6cde19df1eb72";
 console.log("firstNonce: " + firstNonce)
 var password = "123456";
 var salt = CryptoJS.enc.Hex.parse("07f0b7e87c2ad06ebd2938c326909307ac1da81460b27fa6d84a8b240ac795fe");
 var iter = 100;
 var finalNonce = "aa0591fd5a971da559b37eb6369eb2d2b25252e4a115e3ab31f6cde19df1eb72JREAGMZitcXiwQp2tsnUhLFViZev60z0";
 var authMsg = firstNonce + "," + finalNonce + "," + finalNonce;
 var saltPassword = scram.saltedPassword(password, salt, iter).toString();
 console.log("saltPassword: " + saltPassword)
 var serverKey = scram.serverKey(CryptoJS.enc.Hex.parse(saltPassword));
 var clientKey = scram.clientKey(CryptoJS.enc.Hex.parse(saltPassword)).toString();
 console.log("clientKey: " + clientKey)
 var storekey = scram.storedKey(CryptoJS.enc.Hex.parse(clientKey));
 storekey = storekey.toString();
 console.log("storeKey: " + storekey)
 var clientsignature = scram.signature(CryptoJS.enc.Hex.parse(storekey), authMsg);
 console.log("clientsignature: " + clientsignature)
 clientsignature = clientsignature.toString();
 clientsignature = CryptoJS.enc.Hex.parse(clientsignature);
 clientKey = CryptoJS.enc.Hex.parse(clientKey);
 for (var i = 0; i < clientKey.sigBytes / 4; i++) {
 clientKey.words[i] = clientKey.words[i] ^ clientsignature.words[i]
 }
 console.log("clientProof: " + clientKey.toString())- After executing it in the browser, the result is as follows, which is exactly the same as when making the request. - 1 
 2
 3
 4
 5
 6- firstNonce: aa0591fd5a971da559b37eb6369eb2d2b25252e4a115e3ab31f6cde19df1eb72 
 saltPassword: d051d0ce9b09584087012c6a36c78732cd7b919b5d4d02d35fb1827331044ca0
 clientKey: 9df927a1d72d2e77fdf0c59b5c34b02e1c50801b4b193789ce2dbbfb9671574f
 storeKey: 5d461845ea9ea0311c1ed476314eb2357c4a32b11b332695e6e23f5982d900b8
 clientsignature: c6d2b14463890f53e3f50dda9fb3ccde2134bc2ba49a6feb442233de2b2e1887
 clientProof: 5b2b96e5b4a421241e05c841c3877cf03d643c30ef8358628a0f8825bd5f4fc8
Step by Step Decomposition, and Implementing and Calculating the Encryption in Java
- First, “firstNonce”. I chose a randomly generated 64-bit hashed string. - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10- private String randomNonce() { 
 String rand = "abcdef1234567890";
 Random random = new Random();
 StringBuilder sb = new StringBuilder();
 for (int i = 0; i < 64; i++) {
 int number = random.nextInt(rand.length());
 sb.append(rand.charAt(number));
 }
 return sb.toString();
 }
- Then, try to write the line “scram.saltedPassword(password, salt, iter)”. - First, go to - cat_enc.js:2403and see the following content:- 1 
 2
 3
 4
 5
 6
 7- saltedPassword: function(password, salt, iterations) { 
 return CryptoJS.PBKDF2(password, salt, {
 keySize: this.cfg.keySize,
 iterations:iterations,
 hasher: this.cfg.hasher
 });
 }- It can be seen that PBKDF2 encryption algorithm is used here. After searching in Java, the corresponding algorithm name is - PBKDF2WithHmacSHA256. After googling, write the following code. As for why the key length is 256 below, I’m not entirely sure either. I always thought it was 32 because- this.cfg.keySizereturns 32, but I found that the encrypted length was different from the JS code. When I changed it to 256, the lengths matched. At the same time, I found that the length of the calculated- saltedPasswordis exactly 32.- 1 
 2
 3
 4
 5- private byte[] getSaltedPassword(String password, byte[] salt, int iterations) throws NoSuchAlgorithmException, InvalidKeySpecException { 
 KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, 256);
 SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
 return f.generateSecret(spec).getEncoded();
 }
- Implementing - scram.clientKey(CryptoJS.enc.Hex.parse(saltPassword)).toString(). This piece of JS code is exactly the same as generating the ClientKey, just replacing “Client Key” with “authMsg” here. And authMsg is a combination of “firstNonce + “,” + finalNonce + “,” + finalNonce”. So, this code can also be replaced with- getHmac, but with the key parameter changed to authMsg.
- Implementing - scram.storedKey(CryptoJS.enc.Hex.parse(clientKey)). This line is equivalent to calculating the digest of the clientKey generated above using SHA256. The code is only one line.- 1 
 2
 3- private byte[] getStoreKey(byte[] clientKey) throws NoSuchAlgorithmException { 
 return MessageDigest.getInstance("SHA-256").digest(clientKey);
 }
- Implementing - scram.signature(CryptoJS.enc.Hex.parse(storekey), authMsg). This piece of JS code is the same as generating the ClientKey, except that it replaces “Client Key” with authMsg. The authMsg here is a combination of “firstNonce + “,” + finalNonce + “,” + finalNonce”. So, this code can also use- getHmac, but with the key parameter changed to authMsg.
- The last step is to perform XOR. Because I’m not very familiar with CryptoJS, but looking at its loop logic, I guess it XORs each byte of clientKey with each byte of clientSignature, and coincidentally, they are the same length. - 1 
 2
 3- for (var i = 0; i < clientKey.sigBytes / 4; i++) { 
 clientKey.words[i] = clientKey.words[i] ^ clientsignature.words[i]
 }- The corresponding Java code is much easier to understand. - 1 
 2
 3- for (int i = 0; i < clientKey.length; i++) { 
 clientKey[i] = (byte) (clientKey[i] ^ clientSignature[i]);
 }
- The core login code mentioned above has all been written, now let’s test it. - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21- public static void main(String[] args) throws InvalidKeySpecException, NoSuchAlgorithmException, InvalidKeyException { 
 String firstNonce = "aa0591fd5a971da559b37eb6369eb2d2b25252e4a115e3ab31f6cde19df1eb72";
 String salt = "07f0b7e87c2ad06ebd2938c326909307ac1da81460b27fa6d84a8b240ac795fe";
 int iterations = 100;
 String finalNonce = "aa0591fd5a971da559b37eb6369eb2d2b25252e4a115e3ab31f6cde19df1eb72JREAGMZitcXiwQp2tsnUhLFViZev60z0";
 HuaweiWS5200AccessUtils huaweiWS5200AccessUtils = new HuaweiWS5200AccessUtils();
 byte[] saltedPassword = huaweiWS5200AccessUtils.getSaltedPassword(password, hexToByteArray(salt), iterations);
 System.out.println("SaltedPassword: " + bytesToHex(saltedPassword));
 byte[] clientKey = huaweiWS5200AccessUtils.getHmac("Client Key", saltedPassword);
 System.out.println("ClientKey: " + bytesToHex(clientKey));
 byte[] storeKey = huaweiWS5200AccessUtils.getStoreKey(clientKey);
 System.out.println("StoreKey: " + bytesToHex(storeKey));
 String authMsg = firstNonce + "," + finalNonce + "," + finalNonce;
 byte[] clientSignature = huaweiWS5200AccessUtils.getHmac(authMsg, storeKey);
 System.out.println("ClientSignature: " + bytesToHex(clientSignature));
 for (int i = 0; i < clientKey.length; i++) {
 clientKey[i] = (byte) (clientKey[i] ^ clientSignature[i]);
 }
 System.out.println("Final ClientProof: " + bytesToHex(clientKey));
 }- After running it, the output is as follows, and the result is exactly the same. - 1 
 2
 3
 4
 5
 6
 7- SaltedPassword: d051d0ce9b09584087012c6a36c78732cd7b919b5d4d02d35fb1827331044ca0 
 ClientKey: 9df927a1d72d2e77fdf0c59b5c34b02e1c50801b4b193789ce2dbbfb9671574f
 StoreKey: 5d461845ea9ea0311c1ed476314eb2357c4a32b11b332695e6e23f5982d900b8
 ClientSignature: c6d2b14463890f53e3f50dda9fb3ccde2134bc2ba49a6feb442233de2b2e1887
 Final ClientProof: 5b2b96e5b4a421241e05c841c3877cf03d643c30ef8358628a0f8825bd5f4fc8
 Process finished with exit code 0
Trying to Get All Hosts
In fact, at this point, the entire content is finished. Now let’s simply get all the hosts.
| 1 | GET /api/system/HostInfo HTTP/1.1 | 
Let’s start writing the request.
| 1 | public static void main(String[] args) throws InvalidKeySpecException, NoSuchAlgorithmException, InvalidKeyException { | 
Alright, we’re done. Here’s the complete Java test code.
| 1 | package com.ruterfu.test; | 

