
Galah: Um honeypot web alimentado por LLM.
TL;DR: Galah (/ɡəˈlɑː/ - pronuncia-se 'guh-laa') é um honeypot web baseado em LLM projetado para imitar várias aplicações e responder dinamicamente a requisições HTTP arbitrárias. Galah suporta os principais provedores de LLM, incluindo OpenAI, GoogleAI, Vertex AI da GCP, Anthropic, Cohere e Ollama.
Diferente dos honeypots web tradicionais que emulam manualmente aplicações ou vulnerabilidades específicas, o Galah cria dinamicamente respostas relevantes — incluindo cabeçalhos HTTP e conteúdo do corpo — para qualquer requisição HTTP. As respostas geradas pelo LLM são armazenadas em cache por um período configurável para evitar geração repetitiva para requisições idênticas, reduzindo custos de API. O cache é específico por porta, garantindo que respostas geradas para uma determinada porta não sejam reutilizadas para a mesma requisição em uma porta diferente.
O Galah pode opcionalmente inspecionar requisições HTTP recebidas contra um conjunto de regras Suricata, correspondendo a vários buffers HTTP incluindo método, URI, cabeçalhos, cookies e corpo da requisição (a implementação atual não suporta todas as palavras-chave do Suricata e o tratamento de PCRE é limitado). Para habilitar e configurar a correspondência de regras, veja Suricata HTTP Rule Matching.
A configuração do prompt é fundamental neste honeypot. Embora você possa atualizar o prompt no arquivo de configuração, é crucial manter o segmento que direciona o LLM a produzir respostas no formato JSON especificado.
Nota: Galah foi desenvolvido como um projeto divertido de fim de semana para explorar as capacidades dos LLMs na criação de mensagens HTTP. O honeypot pode ser identificado por vários métodos, como técnicas de impressão digital de rede, tempos de resposta prolongados dependendo do provedor e modelo LLM, e respostas não padronizadas. Para se proteger contra ataques de Denial of Wallet, certifique-se de definir limites de uso na sua API LLM.
config.yaml se necessário.git clone [email protected]:0x4D31/galah.git
cd galah
go mod download
mkdir bin
go build -o bin/galah ./cmd/galah
./bin/galah --help
██████ █████ ██ █████ ██ ██
██ ██ ██ ██ ██ ██ ██ ██
██ ███ ███████ ██ ███████ ███████
██ ██ ██ ██ ██ ██ ██ ██ ██
██████ ██ ██ ███████ ██ ██ ██ ██
llm-based web honeypot | version 1.1.1
author: Adel "0x4D31" Ka
Usage: galah --provider PROVIDER --model MODEL [--server-url SERVER-URL] [--temperature TEMPERATURE] [--api-key API-KEY] [--cloud-location CLOUD-LOCATION] [--cloud-project CLOUD-PROJECT] [--interface INTERFACE] [--config-file CONFIG-FILE] [--rules-config-file RULES-CONFIG-FILE] [--event-log-file EVENT-LOG-FILE] [--cache-db-file CACHE-DB-FILE] [--cache-duration CACHE-DURATION] [--log-level LOG-LEVEL] [--suricata-enabled] [--suricata-rules-dir SURICATA-RULES-DIR]
Options:
--provider PROVIDER, -p PROVIDER
LLM provider (openai, googleai, gcp-vertex, anthropic, cohere, ollama) [env: LLM_PROVIDER]
--model MODEL, -m MODEL
LLM model (e.g. gpt-3.5-turbo-1106, gemini-1.5-pro-preview-0409) [env: LLM_MODEL]
--server-url SERVER-URL, -u SERVER-URL
LLM Server URL (required for Ollama) [env: LLM_SERVER_URL]
--temperature TEMPERATURE, -t TEMPERATURE
LLM sampling temperature (0-2). Higher values make the output more random [default: 1, env: LLM_TEMPERATURE]
--api-key API-KEY, -k API-KEY
LLM API Key [env: LLM_API_KEY]
--cloud-location CLOUD-LOCATION
LLM cloud location region (required for GCP's Vertex AI) [env: LLM_CLOUD_LOCATION]
--cloud-project CLOUD-PROJECT
LLM cloud project ID (required for GCP's Vertex AI) [env: LLM_CLOUD_PROJECT]
--interface INTERFACE, -i INTERFACE
interface to serve on
--config-file CONFIG-FILE, -c CONFIG-FILE
Path to config file [default: config/config.yaml]
--rules-config-file RULES-CONFIG-FILE, -r RULES-CONFIG-FILE
Path to rules config file (rule engine disabled if omitted)
--event-log-file EVENT-LOG-FILE, -o EVENT-LOG-FILE
Path to event log file [default: event_log.json]
--cache-db-file CACHE-DB-FILE, -f CACHE-DB-FILE
Path to database file for response caching [default: cache.db]
--cache-duration CACHE-DURATION, -d CACHE-DURATION
Cache duration for generated responses (in hours). Use 0 to disable caching, and -1 for unlimited caching (no expiration). [default: 24]
--log-level LOG-LEVEL, -l LOG-LEVEL
Log level (debug, info, error, fatal) [default: info]
--suricata-enabled Enable Suricata HTTP rule checking (default: false)
--suricata-rules-dir SURICATA-RULES-DIR
Directory containing Suricata .rules files to check HTTP requests against
--help, -h display this help and exit
% git clone [email protected]:0x4D31/galah.git
% cd galah
% mkdir logs
% export LLM_API_KEY=your-api-key
% docker build -t galah-image .
% docker run -d --name galah-container -p 8080:8080 -v $(pwd)/logs:/galah/logs -e LLM_API_KEY galah-image -o logs/galah.json -p openai -m gpt-3.5-turbo-1106
./galah -p openai -m gpt-4.1-mini --suricata-enabled --suricata-rules-dir rules
Teste:
curl --http1.1 --path-as-is -X POST \
-H 'SOAPAction: "http://purenetworks.com/HNAP1/GetGuestNetworkSettings"' \
-H 'Content-Type: text/xml' \
--data '<GetGuestNetworkSettings xmlns="http://purenetworks.com/HNAP1/">' \
http://127.0.0.1:8888/HNAP1/ -v
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying 127.0.0.1:8888...
* Connected to 127.0.0.1 (127.0.0.1) port 8888
> POST /HNAP1/ HTTP/1.1
> Host: 127.0.0.1:8888
> User-Agent: curl/8.7.1
> Accept: */*
> SOAPAction: "http://purenetworks.com/HNAP1/GetGuestNetworkSettings"
> Content-Type: text/xml
> Content-Length: 64
>
* upload completely sent off: 64 bytes
< HTTP/1.1 200 OK
< Server: TP-LINK HTTP Server/1.0
< Date: Mon, 21 Apr 2025 01:28:43 GMT
< Content-Length: 545
< Content-Type: text/xml; charset=utf-8
<
<?xml version="1.0" encoding="utf-8"?>
<GetGuestNetworkSettingsResponse xmlns="http://purenetworks.com/HNAP1/">
<GetGuestNetworkSettingsResult>OK</GetGuestNetworkSettingsResult>
<GuestNetworkEnabled>true</GuestNetworkEnabled>
<GuestNetworkSSID>TPLink_Guest</GuestNetworkSSID>
<GuestNetworkSecurity>WPA2-PSK</GuestNetworkSecurity>
<GuestNetworkPassword>guest1234</GuestNetworkPassword>
<GuestNetworkIsolation>true</GuestNetworkIsolation>
<GuestNetworkSSIDBroadcast>true</GuestNetworkSSIDBroadcast>
Log de eventos JSON:
{
"eventTime": "2025-04-21T02:28:43.583386+01:00",
"httpRequest": {
"body": "<GetGuestNetworkSettings xmlns=\"http://purenetworks.com/HNAP1/\">",
"bodySha256": "836c42168ebbad0b7192daa70ad8e4ea8d5930097162f513045f5ecb6ae9d5bd",
"headers": {
"Accept": "*/*",
"Content-Length": "64",
"Content-Type": "text/xml",
"Soapaction": "\"http://purenetworks.com/HNAP1/GetGuestNetworkSettings\"",
"User-Agent": "curl/8.7.1"
},
"headersSorted": "Accept,Content-Length,Content-Type,Soapaction,User-Agent",
"headersSortedSha256": "3a44fecf9284eca3947c45ffeb2301ce6d9b3d0a3cc5a7491ccea2b6ed61edaa",
"method": "POST",
"protocolVersion": "HTTP/1.1",
"request": "/HNAP1/",
"sessionID": "1745198923587092000_qHLNEPBNPrH6qw==",
"userAgent": "curl/8.7.1"
},
"httpResponse": {
"headers": {
"Content-Length": "454",
"Content-Type": "text/xml; charset=utf-8",
"Server": "TP-LINK HTTP Server/1.0"
},
"body": "<?xml version=\"1.0\" encoding=\"utf-8\"?> <GetGuestNetworkSettingsResponse xmlns=\"http://purenetworks.com/HNAP1/\"> <GetGuestNetworkSettingsResult>OK</GetGuestNetworkSettingsResult> <GuestNetworkEnabled>true</GuestNetworkEnabled> <GuestNetworkSSID>TPLink_Guest</GuestNetworkSSID> <GuestNetworkSecurity>WPA2-PSK</GuestNetworkSecurity> <GuestNetworkPassword>guest1234</GuestNetworkPassword> <GuestNetworkIsolation>true</GuestNetworkIsolation> <GuestNetworkSSIDBroadcast>true</GuestNetworkSSIDBroadcast> </GetGuestNetworkSettingsResponse>"
},
"level": "info",
"msg": "successfulResponse",
"port": "8888",
"responseMetadata": {
"generationSource": "llm",
"info": {
"model": "gpt-4.1-mini",
"provider": "openai",
"temperature": 1
}
},
"sensorName": "mbp",
"srcHost": "localhost",
"srcIP": "127.0.0.1",
"srcPort": "62418",
"suricataMatches": [
{
"msg": "ET WEB_SPECIFIC_APPS D-Link DIR-823G Multiple HNAP SOAPAction Endpoints Authentication Bypass",
"sid": "2061623"
}
],
"tags": null,
"time": "2025-04-21T02:28:43.587152+01:00"
}
Veja mais exemplos aqui.
O pacote galah pode ser usado como uma biblioteca independente. Crie um galah.Service e chame GenerateHTTPResponse com um http.Request para produzir uma resposta. Se ConfigFile, EventLogFile ou CacheDBFile forem omitidos, seus caminhos padrão (config/config.yaml, event_log.json e cache.db) serão usados. Especifique RulesConfigFile para habilitar a verificação de regras; deixá-lo vazio desativa completamente o mecanismo de regras. Options.Logger pode ser fornecido com um *log.Logger; se for nil, o Galah criará um logger de texto padrão.
svc, err := galah.NewService(context.Background(), galah.Options{
LLMProvider: "openai",
LLMModel: "gpt-4.1-mini",
LLMAPIKey: "YOUR_KEY",
ConfigFile: "config/config.yaml",
RulesConfigFile: "config/rules.yaml", // omit to disable rule engine
EventLogFile: "event_log.json",
CacheDBFile: "cache.db",
Logger: log.NewWithOptions(os.Stderr, log.Options{}),
})
if err != nil {
log.Fatal(err)
}
req, _ := http.NewRequest("GET", "https://example.com", nil)
respBytes, err := svc.GenerateHTTPResponse(req, "8080")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(respBytes))
Você também pode construir um serviço a partir de uma configuração já carregada:
cfg, _ := config.LoadConfig("config.yaml")
rulesCfg, _ := config.LoadRules("rules.yaml")
svc, err := galah.NewServiceFromConfig(context.Background(), cfg, rulesCfg.Rules, galah.Options{
LLMProvider: "openai",
LLMModel: "gpt-4.1-mini",
LLMAPIKey: "YOUR_KEY",
EventLogFile: "event_log.json", // use default if empty
CacheDBFile: "cache.db", // use default if empty
Logger: log.NewWithOptions(os.Stderr, log.Options{}),
})
Para desativar completamente a verificação de regras, omita RulesConfigFile ao criar um serviço ou passe nil/uma slice vazia para NewServiceFromConfig. Qualquer campo de opção não utilizado pode ser deixado vazio para usar seus valores padrão.
O serviço também expõe métodos auxiliares para cache e registro de eventos quando usado como biblioteca:
respBytes, _ := svc.CheckCache(req, "8080") // returns nil if no cached entry
svc.LogEvent(req, llm.JSONResponse{Headers: map[string]string{"Content-Type": "text/plain"}, Body: "hi"}, "8080", "llm", nil)