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
51POST /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
63POST /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
16var 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
25var 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
6firstNonce: 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
10private 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:2403
and see the following content:1
2
3
4
5
6
7saltedPassword: 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 becausethis.cfg.keySize
returns 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 calculatedsaltedPassword
is exactly 32.1
2
3
4
5private 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 withgetHmac
, 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
3private 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 usegetHmac
, 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
3for (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
3for (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
21public 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
7SaltedPassword: 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; |