espilon-source/espilon_bot/components/mod_redteam/rt_captive.c
Eun0us 6d45770d98 epsilon: merge command system into core + add 5 new modules
Move command registry from components/command/ into components/core/.
New modules: mod_canbus, mod_honeypot, mod_fallback, mod_redteam, mod_ota.
Replace mod_proxy with tun_core (multiplexed SOCKS5 tunnel).
Kconfig extended with per-module settings and async worker config.
2026-02-28 20:07:59 +01:00

292 lines
9.0 KiB
C

/*
* rt_captive.c
* Captive portal detection and bypass strategies.
*
* Detection: HTTP GET to connectivitycheck.gstatic.com/generate_204
* - 204 = no portal (internet open)
* - 200/302 = captive portal detected
* - Connection failed = unknown
*
* Bypass strategies (in order):
* 1. Direct C2 port — often not intercepted by portals
* 2. POST accept — parse 302 redirect, POST to portal accept URL
* 3. Wait + retry — some portals open after DNS traffic
*/
#include "sdkconfig.h"
#include "rt_captive.h"
#ifdef CONFIG_MODULE_REDTEAM
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include "esp_log.h"
#include "lwip/sockets.h"
#include "lwip/netdb.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "utils.h"
static const char *TAG = "RT_CAPTIVE";
#define CAPTIVE_TIMEOUT_S 5
#define CAPTIVE_RX_BUF 512
/* ============================================================
* Raw HTTP request to check connectivity
* ============================================================ */
/* Resolve hostname to IP */
static bool resolve_host(const char *host, struct in_addr *out)
{
struct addrinfo hints = {0};
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
struct addrinfo *res = NULL;
int err = lwip_getaddrinfo(host, NULL, &hints, &res);
if (err != 0 || !res) {
ESP_LOGW(TAG, "DNS resolve failed for '%s'", host);
return false;
}
struct sockaddr_in *addr = (struct sockaddr_in *)res->ai_addr;
*out = addr->sin_addr;
lwip_freeaddrinfo(res);
return true;
}
/* Send raw HTTP GET, return HTTP status code (0 on failure) */
static int http_get_status(const char *host, int port, const char *path,
char *location_out, size_t location_cap)
{
struct in_addr ip;
if (!resolve_host(host, &ip)) return 0;
int s = lwip_socket(AF_INET, SOCK_STREAM, 0);
if (s < 0) return 0;
struct timeval tv = { .tv_sec = CAPTIVE_TIMEOUT_S, .tv_usec = 0 };
lwip_setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
lwip_setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr = ip;
if (lwip_connect(s, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
lwip_close(s);
return 0;
}
/* Send HTTP request */
char req[256];
int req_len = snprintf(req, sizeof(req),
"GET %s HTTP/1.0\r\n"
"Host: %s\r\n"
"Connection: close\r\n"
"User-Agent: Mozilla/5.0\r\n"
"\r\n",
path, host);
if (lwip_write(s, req, req_len) <= 0) {
lwip_close(s);
return 0;
}
/* Read response header */
char buf[CAPTIVE_RX_BUF];
int total = 0;
int len;
while (total < (int)sizeof(buf) - 1) {
len = lwip_recv(s, buf + total, sizeof(buf) - 1 - total, 0);
if (len <= 0) break;
total += len;
/* Stop after headers (double CRLF) */
buf[total] = '\0';
if (strstr(buf, "\r\n\r\n")) break;
}
lwip_close(s);
if (total == 0) return 0;
buf[total] = '\0';
/* Parse status code: "HTTP/1.x NNN ..." */
int status = 0;
char *sp = strchr(buf, ' ');
if (sp) {
status = atoi(sp + 1);
}
/* Extract Location header if present (for 302 redirects) */
if (location_out && location_cap > 0) {
location_out[0] = '\0';
char *loc = strstr(buf, "Location: ");
if (!loc) loc = strstr(buf, "location: ");
if (loc) {
loc += 10; /* skip "Location: " */
char *end = strstr(loc, "\r\n");
if (end) {
size_t copy_len = end - loc;
if (copy_len >= location_cap) copy_len = location_cap - 1;
memcpy(location_out, loc, copy_len);
location_out[copy_len] = '\0';
}
}
}
return status;
}
/* ============================================================
* Captive portal detection
* ============================================================ */
rt_portal_status_t rt_captive_detect(void)
{
ESP_LOGI(TAG, "Checking for captive portal...");
int status = http_get_status(
"connectivitycheck.gstatic.com", 80,
"/generate_204", NULL, 0);
if (status == 204) {
ESP_LOGI(TAG, "No captive portal (got 204)");
return RT_PORTAL_NONE;
}
if (status == 200 || status == 302 || status == 301) {
ESP_LOGW(TAG, "Captive portal detected (HTTP %d)", status);
return RT_PORTAL_DETECTED;
}
if (status == 0) {
/* Try alternative check endpoint */
status = http_get_status(
"captive.apple.com", 80,
"/hotspot-detect.html", NULL, 0);
if (status == 200) {
/* Apple endpoint returns 200 with "Success" if no portal */
/* and 200 with redirect content if portal — tricky */
/* For now, assume it's potentially a portal */
ESP_LOGW(TAG, "Apple check returned 200 — may be portal");
return RT_PORTAL_DETECTED;
}
ESP_LOGW(TAG, "Connectivity check failed (no response)");
return RT_PORTAL_UNKNOWN;
}
ESP_LOGW(TAG, "Unexpected status %d — assuming portal", status);
return RT_PORTAL_DETECTED;
}
/* ============================================================
* Captive portal bypass
* ============================================================ */
bool rt_captive_bypass(void)
{
ESP_LOGI(TAG, "Attempting captive portal bypass...");
/* Strategy 1: Direct C2 port
* Most captive portals only intercept 80/443.
* Our C2 is on port 2626 — might go through. */
{
int s = lwip_socket(AF_INET, SOCK_STREAM, 0);
if (s >= 0) {
struct timeval tv = { .tv_sec = CAPTIVE_TIMEOUT_S, .tv_usec = 0 };
lwip_setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(CONFIG_SERVER_PORT);
addr.sin_addr.s_addr = inet_addr(CONFIG_SERVER_IP);
if (lwip_connect(s, (struct sockaddr *)&addr, sizeof(addr)) == 0) {
lwip_close(s);
ESP_LOGI(TAG, "Bypass: direct C2 port %d reachable!", CONFIG_SERVER_PORT);
return true;
}
lwip_close(s);
}
ESP_LOGW(TAG, "Bypass strategy 1 (direct C2 port) failed");
}
/* Strategy 2: POST accept
* Get the redirect URL from the portal, POST to accept. */
{
char location[256] = {0};
int status = http_get_status(
"connectivitycheck.gstatic.com", 80,
"/generate_204", location, sizeof(location));
if ((status == 302 || status == 301) && location[0]) {
ESP_LOGI(TAG, "Portal redirect to: %s", location);
/* Parse host from location URL */
/* Expected format: http://host/path or https://host/path */
char *host_start = strstr(location, "://");
if (host_start) {
host_start += 3;
char *path_start = strchr(host_start, '/');
char host_buf[64] = {0};
if (path_start) {
size_t hlen = path_start - host_start;
if (hlen >= sizeof(host_buf)) hlen = sizeof(host_buf) - 1;
memcpy(host_buf, host_start, hlen);
} else {
strncpy(host_buf, host_start, sizeof(host_buf) - 1);
path_start = "/";
}
/* Try to just GET the portal page (some portals auto-accept) */
int p_status = http_get_status(host_buf, 80, path_start, NULL, 0);
ESP_LOGI(TAG, "Portal page status: %d", p_status);
/* Check if we now have internet */
vTaskDelay(pdMS_TO_TICKS(2000));
int check = http_get_status(
"connectivitycheck.gstatic.com", 80,
"/generate_204", NULL, 0);
if (check == 204) {
ESP_LOGI(TAG, "Bypass: portal auto-accepted!");
return true;
}
}
}
ESP_LOGW(TAG, "Bypass strategy 2 (POST accept) failed");
}
/* Strategy 3: Wait + retry
* Some portals open after seeing DNS traffic. Wait 10s. */
{
ESP_LOGI(TAG, "Bypass strategy 3: waiting 10s...");
vTaskDelay(pdMS_TO_TICKS(10000));
int status = http_get_status(
"connectivitycheck.gstatic.com", 80,
"/generate_204", NULL, 0);
if (status == 204) {
ESP_LOGI(TAG, "Bypass: portal opened after wait!");
return true;
}
ESP_LOGW(TAG, "Bypass strategy 3 (wait) failed");
}
msg_info(TAG, "All captive portal bypass strategies failed", NULL);
return false;
}
#else /* !CONFIG_MODULE_REDTEAM */
rt_portal_status_t rt_captive_detect(void) { return RT_PORTAL_UNKNOWN; }
bool rt_captive_bypass(void) { return false; }
#endif /* CONFIG_MODULE_REDTEAM */