Como ya explicamos en artÃculos anteriores libmodsecurity es un WAF(Web Application Firewall), este nos permite detectar ciertos tipos de ataques en base a unas reglas predefinidas, mediante estas firmas podremos detectar inyecciones SQL, XSS, LFI, RFI. En esta ocasión vamos a montar un sistema en el que seremos noficiados vÃa Telegram cuando se detecte un ataque, se aplicarán reglas de PF y se mostrará una web donde se informará del motivo del baneo.
El artÃculo se divide en varias secciones:
- Compilación de Nginx
- Instalación de libmodsecurity
- Whitelistear URLs
- Deshabilitar reglas
- Limpieza de logs
- Sistema de análisis de logs y baneo
- Instalación de Redis
- modsecurityAnalizer
- modsecurityNotifier
- Reglas de filtrado PF
- Configuración HaProxy
- Dashboard atacantes
- Programas útiles
- Resultado final
Compilación de Nginx
Si ya tenemos instalado Nginx debemos consultar si fué compilado con soporte para libmodsecurity:
MODSECURITY3 : off
NOTA: La versión full(nginx-full) tampoco lleva la opción habilitada.
En versiones anteriores a 03 Jun 2020 18:49:04 Nginx debÃa ser compilado con soporte para modsecurity3, tener la librerÃa modsecurity3 y además instalar el conector modsecurity3-nginx, en versiones posteriores a dicha fecha si compilamos Nginx con soporte para modsecurity3 ya lleva incorporado el conector, pero debemos añadir la orden de carga del módulo en la configuración de Nginx, en la versión vieja este paso era innecesario.
r537834 | joneum | 2020-06-03 20:49:04 +0200 (Wed, 03 Jun 2020) | 11 lines
Merge r532727 from www/nginx-devel:
Convert the following third-party modules to dynamic:
o) accept_language
o) modsecurity3-nginx
Fix the third-party auth_krb5 module build.
Sponsored by: Netzkommune GmbH
Si instalamos modsecurity3-nginx con la versión nueva de Nginx y cargamos el módulo desde la configuación de Nginx veremos el siguiente error:
2020/06/06 14:46:36 [emerg] 24011#101851: module "/usr/local/libexec/nginx/ngx_http_modsecurity_module.so" is not binary compatible in /usr/local/etc/nginx/nginx.conf:4
Además pkg nos advertirá de que el paquete modsecurity3-nginx y nginx entran en conflicto ya que los dos intentan instalar el mismo conector.
Compilamos mediante ports con las opciones que deseemos, nos aseguramos de habilitar modsecurity3 y recordad que una vez se dá el paso a ports hay que gestionar todos los paquetes desde ports, podéis encontrar mas información en este artÃculo anterior.
Antes de compilar e intalar Nginx desde los ports desinstalamos la versión actual.
Preparamos el sistema de ports:
cd /usr/ports
make fetchindex
Realizamos una búsqueda:
Port: nginx-1.22.1_5,3
Path: /usr/ports/www/nginx
Info: Robust and small WWW server
Maint: joneum@FreeBSD.org
B-deps: pcre-8.45_3
R-deps: pcre-8.45_3
WWW: https://nginx.com/
Configuramos las opciones:
make config
Compilamos e instalamos:
make install
Limpiamos ficheros temporales:
Instalación de libmodsecurity
Instalamos la librerÃa libmodsecurity:
make config
make
make install
make clean
Nos bajamos las reglas de OWASP:
git clone https://github.com/SpiderLabs/owasp-modsecurity-crs.git
cd owasp-modsecurity-crs
cp crs-setup.conf.example crs-setup.conf
Nos bajamos el fichero de configuración base de libmodsecurity:
cd /usr/local/etc/modsec
fetch https://raw.githubusercontent.com/SpiderLabs/ModSecurity/v3/master/modsecurity.conf-recommended
fetch https://raw.githubusercontent.com/SpiderLabs/ModSecurity/49495f1925a14f74f93cb0ef01172e5abc3e4c55/unicode.mapping
mv modsecurity.conf-recommended modsecurity.conf
Nos aseguramos de tener ciertos parámetros con los valores indicados:
SecRuleEngine On
SecAuditLogFormat json
SecAuditEngine RelevantOnly
SecAuditLog /var/log/modsec_audit.log
Incluimos en la configuración principal el fichero base y las reglas de OWASP:
# Include the recommended configuration
Include /usr/local/etc/modsec/modsecurity.conf
# OWASP CRS v3 rules
Include /usr/local/owasp-modsecurity-crs/crs-setup.conf
Include /usr/local/owasp-modsecurity-crs/rules/*.conf
Habilitamos libmodsecurity en la configuración de Nginx añadiendo como primera lÃnea el load del módulo:
load_module /usr/local/libexec/nginx/ngx_http_modsecurity_module.so;
server {
...
modsecurity on;
modsecurity_rules_file /usr/local/etc/modsec/main.conf;
Reiniciamos el servicio:
Probamos cualquier tipo de ataque, en mi caso una inyección MySQL con el siguiente payload:
' or '1==1'; --
Veremos una entrada nueva en el log y la petición web habrá sido bloqueada con un 403 Forbidden:
Si vamos a generar muchas entradas en el log es preferible escribirlas en formato concurrent, de este modo no se logearán en un fichero de texto de forma serializada, si no que se hará en distintos ficheros de forma paralela:
#SecAuditLogType Serial
SecAuditLogType Concurrent
#SecAuditLog /var/log/modsec_audit.log
SecAuditLogStorageDir /opt/modsecurity/var/audit
Reiniciamos el servicio:
Creamos el directorio donde almacenar las entradas de log:
chown -R www:www /opt/modsecurity/var/audit/
chmod 775 /opt/modsecurity/var/audit/
Instalamos la herramienta jq, esta nos será útil para visualizar la información de los logs de forma mas cómoda:
make config
make
make install
make clean
Consultamos el campo message de una de las entradas:
"SQL Injection Attack Detected via libinjection"
"Inbound Anomaly Score Exceeded (Total Score: 5)"
NOTA: No es necsario filtrar si queremos ver la información completa simplemente empipamos la salida del cat a un jq sin parámetros.
Whitelistear URLs
Si en nuestra aplicación web hay alguna funcionalidad que requiera un comportamiento un poco fuera de lo normal podemos whitelistearla para que las reglas no salten cuando se trate de esa sección:
vi /usr/local/owasp-modsecurity-crs/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf
SecRule REQUEST_URI "@beginsWith /CUSTOM_WEB_PATH" \
"id:1001,\
phase:1,\
pass,\
nolog,\
ctl:ruleEngine=Off"
Deshabilitar reglas
Si por otro lado queremos deshabilitar alguna regla en concreto podemos utilizar la directiva SecRuleRemoveById
NOTA : Esta directiva debe ser especificada después de la regla que se esté deshabilitando.
En mi caso estaba teniendo problemas con la regla:
CGI source code leakage
"ruleId": "950140",
"file": "/usr/local/owasp-modsecurity-crs/rules/RESPONSE-950-DATA-LEAKAGES.conf",
"lineNumber": "66",
Al mostrar código por la web la alarma salta por pensar que se está filtrando código fuente de la propia página, añadimos el fichero de reglas deshabilitadas:
# Include the recommended configuration
Include /usr/local/etc/modsec/modsecurity.conf
# OWASP CRS v3 rules
Include /usr/local/owasp-modsecurity-crs/crs-setup.conf
Include /usr/local/owasp-modsecurity-crs/rules/*.conf
# Disabled rules
Include /usr/local/etc/modsec/disabledRules.conf
Indicamos que regla deshabilitar:
SecRuleRemoveById 950140
En cambio si queremos deshabilitarla solo para una IP(necesario si habilitamos el check en un Ha-Proxy):
SecRule REMOTE_ADDR "@ipMatch IP_ADDRESS" "id:1,phase:1,t:none,nolog,pass,ctl:ruleRemoveById=920280"
Limpieza de logs
Una buena idea es eliminar los logs de libmodsecurity cada X tiempo, para ello yo tengo el siguiente CRON habilitado:
00 11 * * * /bin/rm -rf /usr/local/bastille/jails/MetaCortex/root/opt/modsecurity/var/audit/* >/dev/null 2>&1
Sistema de análisis de logs y baneo
El sistema se compone de un servidor Redis central y varios programas escritos en Go:
- Análisis de logs de libmodsecurity: modsecurityAnalizer
- Notificaciones vÃa Telegram, baneo mediante PF y configuración de ACLs-RAM y listas en Ha-Proxy: modsecurityNotifier
Según la función del servidor este deberá ejecutar un software u otro:
- Nginx con libmodsecurity instalado: modsecurityanalizer
- Servidor fÃsico de jails: modsecuritynotifier
La idea es que los Nginx que estarán repartidos por varios servidores fÃsicos en forma de jail, escriban en el Redis central.
Y los servidores fÃsicos reconfiguren PF y los HaProxys(uno por servidor fÃsico) en base a la información del Redis.
NOTA: En mi caso solo tengo un servidor fÃsico, asà que lo ejecutaré todo en el servidor padre y configuraré los paths para que se busquen los ficheros dentro del path de las jails(/usr/local/bastille/jails).
Instalación de Redis
Para que los distintos scripts puedan comunicarse, instalaremos un servidor Redis:
Lo bindeamos a su ip y asignamos un password:
bind 192.168.69.2
requirepass XXXXXXXXXX
Habilitamos y arrancamos el servicio:
service redis start
Consultamos el estado del redis:
# Server
redis_version:7.0.8
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:8e3f49544e856f48
redis_mode:standalone
os:FreeBSD 13.1-RELEASE-p5 amd64
.......
modsecurityAnalizer
Mediante este programa analizaremos los logs generados por libmodsecurity y generaremos las keys asociadas en Redis.
cd modsecurityAnalizer
go mod init modsecurityAnalizer
go get github.com/fsnotify/fsnotify
go get github.com/redis/go-redis/v9
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/fsnotify/fsnotify"
"github.com/redis/go-redis/v9"
)
const (
apiKey = "XXXXXXX"
telegramURL = "https://api.telegram.org/bot%s/sendMessage"
userID = "XXXXXXX"
haproxyIP = "192.168.69.19"
redisAddr = "192.168.69.2:6379"
redisPass = "XXXXXXX"
auditPath = "/usr/local/bastille/jails/MetaCortex/root/opt/modsecurity/var/audit/"
)
var (
ctx = context.Background()
rdb *redis.Client
)
type ModSecLog struct {
Transaction struct {
TimeStamp string `json:"time_stamp"`
ClientIP string `json:"client_ip"`
Request struct {
Method string `json:"method"`
URI string `json:"uri"`
Headers map[string]string `json:"headers"`
} `json:"request"`
Messages []struct {
Message string `json:"message"`
Details struct {
Data string `json:"data"`
} `json:"details"`
} `json:"messages"`
} `json:"transaction"`
}
func sendTelegram(msg string) {
url := fmt.Sprintf(telegramURL, apiKey)
payload, _ := json.Marshal(map[string]string{
"chat_id": userID,
"text": msg,
})
resp, err := http.Post(url, "application/json", bytes.NewBuffer(payload))
if err != nil {
fmt.Printf("++ Error sending telegram: %v\n", err)
return
}
defer resp.Body.Close()
}
func processFile(path string) {
// Short pause for ModSecurity to finish writing in FreeBSD
time.Sleep(250 * time.Millisecond)
content, err := os.ReadFile(path)
if err != nil || len(content) == 0 {
return
}
// Clean possible null characters at the end of the JSON
content = bytes.Trim(content, "\x00")
var log ModSecLog
if err := json.Unmarshal(content, &log); err != nil {
// Retry: ModSecurity sometimes wraps the JSON in an array []
var logs []ModSecLog
if err2 := json.Unmarshal(content, &logs); err2 == nil && len(logs) > 0 {
log = logs[0]
} else {
return
}
}
foundAlert := false
for _, msgLine := range log.Transaction.Messages {
message := msgLine.Message
if message == "" || strings.Contains(message, "Anomaly Score Exceeded") {
continue
}
attacker := log.Transaction.Request.Headers["x-forwarded-for"]
if attacker == "" {
attacker = log.Transaction.ClientIP
}
// Ignore if the attacker is our own HAProxy or is empty
if attacker == haproxyIP || attacker == "" {
continue
}
foundAlert = true
fmt.Printf("[%s] ATTACK DETECTED: %s -> %s\n", time.Now().Format("15:04:05"), attacker, message)
// Increment counter in Redis
rdb.Incr(ctx, attacker)
rdb.Expire(ctx, attacker, time.Hour)
// Save detailed data
data := []string{
log.Transaction.TimeStamp,
attacker,
log.Transaction.Request.Headers["sec-ch-ua-platform"],
log.Transaction.Request.Headers["user-agent"],
message,
log.Transaction.Request.Headers["host"],
log.Transaction.Request.URI,
log.Transaction.Request.Method,
msgLine.Details.Data,
}
rdb.Set(ctx, "data"+attacker, strings.Join(data, "++"), time.Hour)
}
if !foundAlert {
fmt.Printf("[-] File processed (no alerts): %s\n", filepath.Base(path))
}
}
func main() {
rdb = redis.NewClient(&redis.Options{
Addr: redisAddr,
Password: redisPass,
DB: 0,
})
if err := rdb.Ping(ctx).Err(); err != nil {
fmt.Println("++ ERROR: Cannot connect to Redis")
os.Exit(1)
}
watcher, err := fsnotify.NewWatcher()
if err != nil {
panic(err)
}
defer watcher.Close()
// Function to add folders to the monitoring system (kqueue)
watchDir := func(path string) {
err := watcher.Add(path)
if err == nil {
fmt.Printf("[*] Monitoring: %s\n", path)
}
}
// Initial scan of the entire audit structure
filepath.Walk(auditPath, func(path string, info os.FileInfo, err error) error {
if err == nil && info.IsDir() {
watchDir(path)
}
return nil
})
fmt.Println("\n-- ModSecurity Analyzer online. Waiting for attacks...")
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
// Capture creation of folders or files
if event.Has(fsnotify.Create) || event.Has(fsnotify.Write) {
info, err := os.Stat(event.Name)
if err != nil {
continue
}
if info.IsDir() {
// If ModSecurity creates a subfolder (e.g.: 20260228-1250), we monitor it right away
watchDir(event.Name)
// Preventive scan in case ModSecurity created files before Go reacted
filepath.Walk(event.Name, func(p string, i os.FileInfo, e error) error {
if e == nil && !i.IsDir() {
processFile(p)
} else if e == nil && i.IsDir() && p != event.Name {
watchDir(p)
}
return nil
})
} else {
// It's a JSON log file: to be processed
processFile(event.Name)
}
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
fmt.Printf("Watcher Error: %v\n", err)
}
}
}
Compilamos y copiamos el binario al sistema:
cp modsecurityAnalizer /usr/local/bin
El script RC de gestión del servicio quedarÃa del siguiente modo:
#!/bin/sh
#
# PROVIDE: modsecurityAnalizer
# REQUIRE: DAEMON
# KEYWORD: shutdown
. /etc/rc.subr
name=modsecurityAnalizer
rcvar=modsecurityAnalizer_enable
command="/usr/local/bin/modsecurityAnalizer"
start_cmd="modsecurityAnalizer_start"
stop_cmd="${name}_stop"
status_cmd="${name}_status"
pidfile="/var/run/${name}.pid"
modsecurityAnalizer_start(){
echo "Starting modsecurityAnalizer (Go version)."
/usr/sbin/daemon -c -f -p ${pidfile} ${command}
}
modsecurityAnalizer_stop(){
if [ -f ${pidfile} ]; then
echo "Stopping service: ${name}"
kill -s TERM $(cat ${pidfile})
rm -f ${pidfile}
sleep 3
else
echo "It appears ${name} is not running."
fi
}
modsecurityAnalizer_status(){
if [ -f ${pidfile} ] && kill -0 $(cat ${pidfile}) 2>/dev/null; then
echo "${name} running with PID: $(cat ${pidfile})"
else
echo "It appears ${name} is not running."
[ -f ${pidfile} ] && rm -f ${pidfile}
fi
}
load_rc_config $name
run_rc_command "$1"
Asignamos permisos:
chown root:wheel /usr/local/etc/rc.d/modsecurityAnalizer
Habilitamos el servicio y lo arrancamos:
service modsecurityAnalizer start
modsecurityNotifier
Este programa consultará el Redis, avisará vÃa Telegram de los ataques recibidos, configurará reglas PF y mantendrá una ACL-RAM y una lista de HaProxy con las ips de los atacantes.
La razón por la que configuraremos ACLs y listas simultáneamente es porque las ACLs se insertarán por SocketAdmin de HaProxy, al reiniciar el HaProxy las ACLs desaparecen, para evitar esto mantendremos también la lista /usr/local/etc/badguys.list de este modo en caso de reinicio la lista contendrá las mismas ips que habÃan en la ACL.
cd modsecurityNotifier/
go mod init modsecurityNotifier
go get github.com/redis/go-redis/v9
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/redis/go-redis/v9"
"io"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
)
const (
ApiKey = "XXXXXXXXXXXXXX"
TelegramUrl = "https://api.telegram.org/bot" + ApiKey + "/sendMessage"
UserId = "XXXXXXXXXXXXXX"
HaProxyBlacklistFile = "/usr/local/bastille/jails/Atlas/root/usr/local/etc/badguys.list"
HaProxyAdminSocketDir = "/usr/local/bastille/jails/Atlas/root/var/run/"
BadguysAclPattern = `.*\(/usr/local/etc/badguys\.list\) pattern loaded from file '/usr/local/etc/badguys\.list'.*`
)
var ctx = context.Background()
type TelegramPayload struct {
ChatID string `json:"chat_id"`
Text string `json:"text"`
}
func sendTelegram(text string) {
payload := TelegramPayload{
ChatID: UserId,
Text: text,
}
jsonData, _ := json.Marshal(payload)
resp, err := http.Post(TelegramUrl, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
fmt.Printf("++ Error sending telegram message: %v\n", err)
return
}
defer resp.Body.Close()
}
func getPFBannedHosts() []string {
cmd := exec.Command("pfctl", "-t", "badguys", "-T", "show")
output, err := cmd.Output()
if err != nil {
return []string{}
}
lines := strings.Split(string(output), "\n")
var hosts []string
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed != "" {
hosts = append(hosts, trimmed)
}
}
return hosts
}
// Interacting with HAProxy via Unix Socket
func haproxyCommand(cmd string) (string, error) {
// Look for the socket in the specified directory
files, _ := filepath.Glob(filepath.Join(HaProxyAdminSocketDir, "*.sock"))
if len(files) == 0 {
// Try direct path if no dynamic .sock files are found
files = append(files, filepath.Join(HaProxyAdminSocketDir, "haproxy.sock"))
}
var lastErr error
for _, socketPath := range files {
conn, err := net.Dial("unix", socketPath)
if err != nil {
lastErr = err
continue
}
defer conn.Close()
fmt.Fprintf(conn, cmd+"\n")
var buf bytes.Buffer
io.Copy(&buf, conn)
return buf.String(), nil
}
return "", fmt.Errorf("cant connect with HAProxy socket: %v", lastErr)
}
func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "192.168.69.2:6379",
Password: "XXXXXXXXXXXXXX",
DB: 0,
})
if err := rdb.Ping(ctx).Err(); err != nil {
hostname, _ := os.Hostname()
msg := fmt.Sprintf("ERROR %s (modsecurityNotifier.go): Cant connect to redis server", hostname)
fmt.Println("++ " + msg)
sendTelegram(msg)
os.Exit(1)
}
for {
fmt.Println("\n------------------------------------------------")
fmt.Println(">> Checking REDIS content")
fmt.Println("------------------------------------------------")
iter := rdb.Scan(ctx, 0, "*", 0).Iterator()
for iter.Next(ctx) {
attacker := iter.Val()
if strings.HasPrefix(attacker, "data") || strings.HasPrefix(attacker, "geoipdata") {
continue
}
// Always ignore localhost
if attacker == "127.0.0.1" || attacker == "::1" {
continue
}
rediscounter, _ := rdb.Get(ctx, attacker).Result()
count, _ := strconv.Atoi(rediscounter)
if count >= 5 {
dataStr, err := rdb.Get(ctx, "data"+attacker).Result()
if err == nil {
parts := strings.Split(dataStr, "++")
if len(parts) >= 9 {
timestamp := parts[0]
ip := parts[1]
uaPlatform := parts[2]
ua := parts[3]
message := parts[4]
host := parts[5]
url := parts[6]
method := parts[7]
payload := parts[8]
fmt.Printf("Attacker: %s Counter: %s\n", attacker, rediscounter)
fmt.Printf("timestamp: %s\nattacker: %s\nmethod: %s\n", timestamp, ip, method)
// PF Ban check
bannedHosts := getPFBannedHosts()
isBanned := false
for _, bh := range bannedHosts {
if bh == ip {
isBanned = true
break
}
}
if !isBanned {
fmt.Printf("-- Banning time for: %s\n", ip)
exec.Command("/sbin/pfctl", "-t", "badguys", "-T", "add", ip).Run()
exec.Command("/sbin/pfctl", "-k", ip).Run()
// Update HAProxy File
f, err := os.OpenFile(HaProxyBlacklistFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err == nil {
f.WriteString(ip + "\n")
f.Close()
}
// Update HAProxy Runtime ACL
aclResp, err := haproxyCommand("show acl")
if err == nil {
re := regexp.MustCompile(BadguysAclPattern)
lines := strings.Split(aclResp, "\n")
aclID := ""
for _, line := range lines {
if re.MatchString(line) {
fields := strings.Fields(line)
if len(fields) > 0 {
aclID = strings.TrimPrefix(fields[0], "#")
break
}
}
}
if aclID != "" {
haproxyCommand(fmt.Sprintf("add acl #%s %s", aclID, ip))
}
}
// Send Telegram Alerts
sendTelegram("-- Banning time for: " + ip + " https://badguys.alfaexploit.com/")
alertMsg := fmt.Sprintf("Date: %s\nAttacker: %s\nUA Platform: %s\nUA: %s\nHost: %s\nUrl: %s\nAlert: %s\nPayload: %s",
timestamp, ip, uaPlatform, ua, host, url, message, payload)
sendTelegram(alertMsg)
}
}
}
}
}
// Cleanup PF
pfHosts := getPFBannedHosts()
for _, bh := range pfHosts {
exists, _ := rdb.Exists(ctx, bh).Result()
if exists == 0 {
fmt.Printf(">> PF Unbanning: %s\n", bh)
exec.Command("/sbin/pfctl", "-t", "badguys", "-T", "delete", bh).Run()
}
}
// Cleanup HAProxy File
if _, err := os.Stat(HaProxyBlacklistFile); err == nil {
input, _ := os.ReadFile(HaProxyBlacklistFile)
lines := strings.Split(string(input), "\n")
var newLines []string
for _, line := range lines {
ip := strings.TrimSpace(line)
if ip == "" {
continue
}
exists, _ := rdb.Exists(ctx, ip).Result()
if exists > 0 {
newLines = append(newLines, ip)
} else {
fmt.Printf(">> Unblacklisting HAProxy (file): %s\n", ip)
}
}
os.WriteFile(HaProxyBlacklistFile, []byte(strings.Join(newLines, "\n")+"\n"), 0644)
}
// Cleanup HAProxy Runtime ACL
aclResp, err := haproxyCommand("show acl")
if err == nil {
re := regexp.MustCompile(BadguysAclPattern)
lines := strings.Split(aclResp, "\n")
for _, line := range lines {
if re.MatchString(line) {
fields := strings.Fields(line)
id := strings.TrimPrefix(fields[0], "#")
content, _ := haproxyCommand("show acl #" + id)
contentLines := strings.Split(content, "\n")
for _, cLine := range contentLines {
cFields := strings.Fields(cLine)
if len(cFields) >= 2 {
entryIP := cFields[1]
exists, _ := rdb.Exists(ctx, entryIP).Result()
if exists == 0 {
// Do not unban localhost
if entryIP != "127.0.0.1" && entryIP != "::1" {
fmt.Printf(">> Unblacklisting HAProxy (RAM-ACL): %s\n", entryIP)
haproxyCommand(fmt.Sprintf("del acl #%s %s", id, entryIP))
}
}
}
}
}
}
}
time.Sleep(5 * time.Second)
}
}
Compilamos y copiamos el binario al sistema:
cp modsecurityNotifier /usr/local/bin
El script RC de gestión del servicio quedarÃa del siguiente modo:
#!/bin/sh
#
# PROVIDE: modsecurityNotifier
# REQUIRE: DAEMON
# KEYWORD: shutdown
. /etc/rc.subr
name=modsecurityNotifier
rcvar=modsecurityNotifier_enable
command="/usr/local/bin/modsecurityNotifier"
pidfile="/var/run/${name}.pid"
start_cmd="modsecurityNotifier_start"
stop_cmd="modsecurityNotifier_stop"
status_cmd="modsecurityNotifier_status"
modsecurityNotifier_start() {
if [ -f "${pidfile}" ] && kill -0 $(cat "${pidfile}") 2>/dev/null; then
echo "${name} already running."
else
echo "Starting ${name}..."
/usr/sbin/daemon -c -f -p ${pidfile} ${command}
fi
}
modsecurityNotifier_stop() {
if [ -f "${pidfile}" ]; then
echo "Stopping ${name}..."
kill -s INT $(cat ${pidfile})
rm -f ${pidfile}
echo "Stopped."
else
echo "${name} is not running."
fi
}
modsecurityNotifier_status() {
if [ -f "${pidfile}" ] && kill -0 $(cat "${pidfile}") 2>/dev/null; then
echo "${name} is running with PID: $(cat ${pidfile})"
else
echo "${name} is not running."
fi
}
load_rc_config $name
run_rc_command "$1"
Asignamos permisos:
chown root:wheel /usr/local/etc/rc.d/modsecurityNotifier
Habilitamos y arrancamos el servicio:
service modsecurityNotifier start
Reglas de filtrado PF
Para bloquear el tráfico desde PF utilizaremos una tabla llamada badguys, todo el tráfico proveniente de estas ips será bloqueado con la excepción del puerto 80/443:
ext_if = "nfe0"
set block-policy return
scrub in on $ext_if all fragment reassemble
set skip on lo
table <badguys> persist
table <jails> persist
nat on $ext_if from <jails> to any -> ($ext_if:0)
rdr-anchor "rdr/*"
antispoof for $ext_if inet
block log all
pass out quick
pass in proto tcp to 192.168.69.2 port 7777
# SMTP -> HellStorm
pass in proto tcp to 192.168.69.17 port 25
# HTTP/HTTPS -> Atlas
pass in proto tcp to 192.168.69.19 port 80
pass in proto tcp to 192.168.69.19 port 443
# Continue attending badguys in HA-Proxy, we have a surprise for them:
pass in quick proto tcp from <badguys> to 192.168.69.19 port 80
pass in quick proto tcp from <badguys> to 192.168.69.19 port 443
# Xbox -> Paradox
pass in proto tcp from 192.168.69.196 to 192.168.69.18 port 80
# TARS -> Paradox
pass in proto tcp from 192.168.69.198 to 192.168.69.18 port 80
# Garrus -> Paradox, testing purpose
pass in proto tcp from 192.168.69.4 to 192.168.69.18 port 80
pass in proto tcp to any port 22
# Block all traffic from badguys, except 80,443 that was allowed previously:
block in from <badguys>
Configuración Ha-Proxy
Como ya hemos comentado, la idea es mantener sincronizadas las ACLs de HA-Proxy en running-config utilizando el AdminSocket y una lista de ips en un fichero en el sistema de ficheros, de este modo si se reinicia el Ha-Proxy todo seguirá funcionando, ya que leerá el fichero que también tiene la lista de ips.
La configuración del Ha-Proxy quedarÃa del siguiente modo:
global
daemon
maxconn 5000
log 192.168.69.19:514 local1
user nobody
group nobody
stats socket /var/run/haproxy.sock user nobody group nobody mode 660 level admin
ssl-default-bind-ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
defaults
timeout connect 10s
timeout client 30s
timeout server 30s
mode http
# ENABLE LOGGING:
# TCP connections
#log global
# Backend connections
#option httplog
listen stats
bind *:8404
stats enable
stats uri /stats
stats refresh 5s
stats auth kr0m:8XEn3EL1aYIeKI
frontend HTTP
bind :80
# Allow LetsEncrypt certificate renew and redirect to HTTPS all other requests:
acl letsencrypt path_beg /.well-known/acme-challenge/
http-request redirect scheme https unless letsencrypt
use_backend letsencrypt-backend if letsencrypt
frontend HTTP-SSL
bind :443 ssl crt /usr/local/etc/haproxy_certs.pem
http-request add-header X-Forwarded-Proto https
acl mail hdr_dom(host) mail.alfaexploit.com
acl prometheus hdr_dom(host) prometheus.alfaexploit.com
acl grafana hdr_dom(host) grafana.alfaexploit.com
acl gdrive hdr_dom(host) gdrive.alfaexploit.com
acl www_alfaexploit hdr_dom(host) www.alfaexploit.com
acl badguys_alfaexploit hdr_dom(host) badguys.alfaexploit.com
acl alfaexploit hdr_dom(host) alfaexploit.com
http-request deny deny_status 400 if !mail !prometheus !grafana !gdrive !www_alfaexploit !badguys_alfaexploit !alfaexploit
# Show banned list to anyone who wants to view it
use_backend badguys if badguys_alfaexploit
# Route Badguys traffic:
acl badguy_ips src -f /usr/local/etc/badguys.list
use_backend badguys if badguy_ips
# Order matters because acl alfaexploit -> alfaexploit.com is the most generic that covers all of them
use_backend mail if mail
use_backend prometheus if prometheus
use_backend prometheus if grafana
use_backend gdrive if gdrive
use_backend alfaexploit if www_alfaexploit
use_backend alfaexploit if alfaexploit
backend letsencrypt-backend
server letsencrypt 192.168.69.19:88
backend mail
server HellStorm 192.168.69.17:80 check send-proxy-v2
backend prometheus
server RECLog 192.168.69.21:80 check send-proxy-v2
backend gdrive
server Paradox 192.168.69.18:8080 check send-proxy-v2
backend alfaexploit
server MetaCortex 192.168.69.20:80 check send-proxy-v2
backend badguys
http-request set-path / unless { path_beg /images }
server Atlas 192.168.69.19:8888 check send-proxy-v2
Dashboard atacantes
En el servidor donde tendremos la dashboard de atacantes instalaremos Nginx y PHP:
Habilitamos y arrancamos el servicio PHP:
service php-fpm start
Incluimos nuestra configuración desde el fichero global de Nginx:
http {
include badguys.conf;
server {
listen 192.168.69.19:8888 proxy_protocol;
server_name badguys.alfaexploit.com;
set_real_ip_from 192.168.69.19;
real_ip_header proxy_protocol;
root /usr/local/www/nginx;
index index.php;
location ~ \.php$ {
try_files $uri =404;
include fastcgi_params;
fastcgi_index index.php;
fastcgi_split_path_info ^(.+\.php)(.*)$;
fastcgi_keep_conn on;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass 127.0.0.1:9000;
}
}
Habilitamos y arrancamos el servicio Nginx:
service nginx start
Creamos el directorio donde almacenar los logos de los sistemas operativos:
Nos bajamos los logos de los sistemas operativos:
wget https://alfaexploit.com/files/libmodSecurityFreeBSD/images.tar.gz
tar xvzf images.tar.gz<>
El código de la dashboard serÃa el siguiente:
<?php
$os_array = [
'windows nt 10' => 'WindowsNT',
'windows nt 6.3' => 'WindowsNT',
'windows nt 6.2' => 'WindowsNT',
'windows nt 6.1|windows nt 7.0' => 'WindowsNT',
'windows nt 6.0' => 'WindowsNT',
'windows nt 5.2' => 'WindowsNT',
'windows nt 5.1' => 'WindowsNT',
'windows xp' => 'WindowsXP',
'windows nt 5.0|windows nt5.1|windows 2000' => 'WindowsNT',
'windows me' => 'WindowsME',
'windows nt 4.0|winnt4.0' => 'WindowsNT',
'windows ce' => 'WindowsCE',
'windows 98|win98' => 'Windows98',
'windows 95|win95' => 'Windows95',
'win16' => 'Windows16',
'mac os x 10.1[^0-9]' => 'MacOS',
'macintosh|mac os x' => 'MacOS',
'mac_powerpc' => 'MacOS',
'ubuntu' => 'Linux',
'iphone' => 'iPhone',
'ipod' => 'iPod',
'ipad' => 'iPad',
'android' => 'Android',
'blackberry' => 'BlackBerry',
'webos' => 'WebOS',
'linux' => 'Linux',
'(media center pc).([0-9]{1,2}\.[0-9]{1,2})'=>'WindowsMediaCenter',
'(win)([0-9]{1,2}\.[0-9x]{1,2})'=>'Windows',
'(win)([0-9]{2})'=>'Windows',
'(windows)([0-9x]{2})'=>'Windows',
'Win 9x 4.90'=>'WindowsME',
'(windows)([0-9]{1,2}\.[0-9]{1,2})'=>'Windows',
'win32'=>'Windows',
'(java)([0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,2})'=>'Java',
'(Solaris)([0-9]{1,2}\.[0-9x]{1,2}){0,1}'=>'Solaris',
'dos x86'=>'DOS',
'Mac OS X'=>'MacOS',
'Mac_PowerPC'=>'MacOS',
'(mac|Macintosh)'=>'MacOS',
'(sunos)([0-9]{1,2}\.[0-9]{1,2}){0,1}'=>'SunOS',
'(beos)([0-9]{1,2}\.[0-9]{1,2}){0,1}'=>'BeOS',
'(risc os)([0-9]{1,2}\.[0-9]{1,2})'=>'RISCOS',
'unix'=>'Unix',
'os/2'=>'OS2',
'freebsd'=>'FreeBSD',
'openbsd'=>'OpenBSD',
'netbsd'=>'NetBSD',
'irix'=>'IRIX',
'plan9'=>'Plan9',
'aix'=>'AIX',
'GNU Hurd'=>'GNUHurd',
'(fedora)'=>'Linux',
'(kubuntu)'=>'Linux',
'(ubuntu)'=>'Linux',
'(debian)'=>'Linux',
'(CentOS)'=>'Linux',
'(Mandriva).([0-9]{1,3}(\.[0-9]{1,3})?(\.[0-9]{1,3})?)'=>'Linux',
'(SUSE).([0-9]{1,3}(\.[0-9]{1,3})?(\.[0-9]{1,3})?)'=>'Linux',
'(Dropline)'=>'Linux',
'(ASPLinux)'=>'Linux',
'(Red Hat)'=>'Linux',
'(linux)'=>'Linux',
'(amigaos)([0-9]{1,2}\.[0-9]{1,2})'=>'AmigaOS',
'amiga-aweb'=>'AmigaOS',
'amiga'=>'AmigaOS',
'AvantGo'=>'PalmOS',
'[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,3}'=>'Linux',
'(webtv)/([0-9]{1,2}\.[0-9]{1,2})'=>'WebTV',
'Dreamcast'=>'DreamcastOS',
'GetRight'=>'Windows',
'go!zilla'=>'Windows',
'gozilla'=>'Windows',
'gulliver'=>'Windows',
'ia archiver'=>'Windows',
'NetPositive'=>'Windows',
'mass downloader'=>'Windows',
'microsoft'=>'Windows',
'offline explorer'=>'Windows',
'teleport'=>'Windows',
'web downloader'=>'Windows',
'webcapture'=>'Windows',
'webcollage'=>'Windows',
'webcopier'=>'Windows',
'webstripper'=>'Windows',
'webzip'=>'Windows',
'wget'=>'Wget',
'Java'=>'Java',
'flashget'=>'FlashGet',
'MS FrontPage'=>'Windows',
'(msproxy)/([0-9]{1,2}.[0-9]{1,2})'=>'Windows',
'(msie)([0-9]{1,2}.[0-9]{1,2})'=>'Windows',
'libwww-perl'=>'Perl',
'UP.Browser'=>'WindowsCE',
'NetAnts'=>'Windows',
];
function user_agent_os($user_agent, $os_array) {
foreach ($os_array as $regex => $value) {
if (preg_match('{\b('.$regex.')\b}i', $user_agent)) {
return $value;
#$key = array_rand($os_array);
#$value = $os_array[$key];
#return $value;
}
}
return 'Unknown';
}
function secs_to_str ($duration) {
$periods = array(
'day' => 86400,
'hour' => 3600,
'minute' => 60,
'second' => 1
);
$parts = array();
foreach ($periods as $name => $dur) {
$div = floor($duration / $dur);
if ($div == 0)
continue;
else
if ($div == 1)
$parts[] = $div . " " . $name;
else
$parts[] = $div . " " . $name . "s";
$duration %= $dur;
}
$last = array_pop($parts);
if (empty($parts))
return $last;
else
return join(', ', $parts) . " and " . $last;
}
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ip_address = $_SERVER['HTTP_CLIENT_IP'];
}
elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip_address = $_SERVER['HTTP_X_FORWARDED_FOR'];
}
else {
$ip_address = $_SERVER['REMOTE_ADDR'];
}
$redis = new Redis();
$redis->connect('192.168.69.2', 6379);
$redis->auth('XXXXXXXX');
$allKeys = $redis->keys('*');
echo "<!DOCTYPE html>";
echo "<html>";
echo "<head>";
echo "<style>";
echo ".attacker_data {";
echo "position: relative;";
echo "top: -6px;";
echo "padding-left: 8px;";
echo "}";
echo "table, td, th {";
echo "border: 1px solid black;";
echo "margin-bottom: 15px;";
echo "}";
echo "table {";
echo " border-collapse: collapse;";
echo " width: 100%;";
echo "}";
echo "td {";
echo " text-align: center;";
echo "}";
echo "</style>";
echo "</head>";
echo "<body>";
echo "<table>";
echo "<tr>";
echo "<td>Date</td>";
echo "<td>IP</td>";
echo "<td>I'm watching you</td>";
echo "<td>Attack</td>";
echo "<td>Unban remaining time</td>";
echo "</tr>";
foreach ($allKeys as $key) {
$attacker_data = array();
if (str_starts_with($key, 'data')) {
$counter_key = substr($key, 4);
$counter = $redis->get($counter_key);
if ($counter >= 5){
$ttl = $redis->ttl($key);
$ttl_human = secs_to_str($ttl);
$raw_data = $redis->get($key);
$data = explode("++", $raw_data);
$i = 0;
# We are iterating Redis data: 0-> Date, 1 -> attacker_ip, 2 -> useragent_platform, 3 -> useragent, 4 -> message
$get_os_from_useragent = False;
foreach ($data as $field) {
$field = trim($field);
if ($i == 0) {
$attacker_data[] = $field;
} elseif ($i == 1) {
$attacker_ip = $field;
## We have limits requesting /ip-api.com url so we try to cache it
if ($redis->exists('geoipdata'.$attacker_ip)) {
$geoipdata = ($redis->get('geoipdata'.$attacker_ip));
$attacker_data[] = $geoipdata;
} else {
$ips = array();
$ips[] = $field;
$endpoint = 'http://ip-api.com/batch';
$options = [
'http' => [
'method' => 'POST',
'user_agent' => 'AlfaExploit/1.0',
'header' => 'Content-Type: application/json',
'content' => json_encode($ips)
]
];
if (($response = @file_get_contents($endpoint, false, stream_context_create($options))) === false) {
$attacker_data[] = 'UNKNOWN';
# Cache data: 3600s -> 1h
$redis->set('geoipdata'.$attacker_ip, 'UNKNOWN');
$redis->expire('geoipdata'.$attacker_ip, 3600);
} else {
$array = json_decode($response, true);
$status = $array[0]['status'];
if ($status == 'fail' ){
$attacker_data[] = $array[0]['message'];
# Cache data: 3600s -> 1h
$redis->set('geoipdata'.$attacker_ip, $array[0]['message']);
$redis->expire('geoipdata'.$attacker_ip, 3600);
} else {
$country = $array[0]['country'];
$regionName = $array[0]['regionName'];
$lat = $array[0]['lat'];
$lon = $array[0]['lon'];
$isp = $array[0]['isp'];
$attacker_data[] = $country.'/'.$regionName.'('.$lat.' '.$lon.')'. $isp;
# Cache data: 3600s -> 1h
$redis->set('geoipdata'.$attacker_ip, $country.'/'.$regionName.'('.$lat.' '.$lon.')'. $isp);
$redis->expire('geoipdata'.$attacker_ip, 3600);
}
}
}
} elseif ($i == 2) {
# useragent_platform is NULL, continue, we will try to get it from user-agent
if ($field == 'NULL') {
$get_os_from_useragent = True;
} else {
$attacker_data[] = str_replace('"', '', $field);;
}
} elseif ($i == 3 && $get_os_from_useragent) {
# Try to get OS from user-agent
$attacker_data[] = user_agent_os($field, $os_array);
} elseif ($i == 4) {
$attacker_data[] = $field;
}
$i = $i + 1;
}
# Print table data
echo "<tr>";
# DATE:
echo "<td>$attacker_data[0]</td>";
# IP:
if ($ip_address == $attacker_ip) {
echo "<td><span style=\"color:#E20D41;\">$attacker_ip</span></td>";
} else {
echo "<td>$attacker_ip</td>";
}
# ATTACKER DATA
echo "<td><img src=/images/$attacker_data[2].png><span class='attacker_data'>$attacker_data[1]</span></td>";
echo "<td>$attacker_data[3]</td>";
echo "<td>$ttl_human</td>";
echo "</tr>";
}
}
}
echo "</table>";
echo '<a href="https://t.me/AlfaExploia">Telegram channel</a> Email: echo \'a3IwbSAoYXQpIGFsZmFleHBsb2l0IChkb3QpIGNvbQo=\' | base64 -d';
echo '<form action="https://alfaexploit.com">';
echo '<button type="submit">AlfaExploit</button>';
echo '</form>';
echo "</body>";
echo "</html>";
Programas útiles
Este programa nos permitirá ver de forma rápida los atacantes baneados:
cd showBadGuys
go mod init showBadGuys
go get github.com/redis/go-redis/v9
package main
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"net"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/redis/go-redis/v9"
)
const (
HaProxyBlacklistFile = "/usr/local/bastille/jails/Atlas/root/usr/local/etc/badguys.list"
HaProxyAdminSocketDir = "/usr/local/bastille/jails/Atlas/root/var/run/"
BadguysAclPattern = `.*\(/usr/local/etc/badguys\.list\) pattern loaded from file '/usr/local/etc/badguys\.list'.*`
)
var ctx = context.Background()
// Function to interact with HAProxy via Unix Socket
func haproxyCommand(cmd string) (string, error) {
files, _ := filepath.Glob(filepath.Join(HaProxyAdminSocketDir, "*.sock"))
if len(files) == 0 {
files = append(files, filepath.Join(HaProxyAdminSocketDir, "haproxy.sock"))
}
var lastErr error
for _, socketPath := range files {
conn, err := net.Dial("unix", socketPath)
if err != nil {
lastErr = err
continue
}
defer conn.Close()
fmt.Fprintf(conn, cmd+"\n")
var buf bytes.Buffer
io.Copy(&buf, conn)
return buf.String(), nil
}
return "", fmt.Errorf("could not connect to socket: %v", lastErr)
}
// Format seconds to H:MM:SS
func formatDuration(d time.Duration) string {
d = d.Round(time.Second)
h := d / time.Hour
d -= h * time.Hour
m := d / time.Minute
d -= m * time.Minute
s := d / time.Second
return fmt.Sprintf("%02d:%02d:%02d", h, m, s)
}
func main() {
// 1. REDIS Connection
rdb := redis.NewClient(&redis.Options{
Addr: "192.168.69.2:6379",
Password: "XXXXXXXXXXX",
DB: 0,
})
if err := rdb.Ping(ctx).Err(); err != nil {
fmt.Println("++ ERROR: Cannot connect to Redis server")
return
}
fmt.Println("\n---------------")
fmt.Println(">> REDIS")
fmt.Println("---------------")
iter := rdb.Scan(ctx, 0, "*", 0).Iterator()
for iter.Next(ctx) {
attacker := iter.Val()
if strings.HasPrefix(attacker, "data") || strings.HasPrefix(attacker, "geoipdata") {
continue
}
rediscounter, _ := rdb.Get(ctx, attacker).Result()
ttl, _ := rdb.TTL(ctx, attacker).Result()
fmt.Printf("> Attacker: %s Counter: %s\n", attacker, rediscounter)
fmt.Printf("TTL: %d RemainingTime: %s\n", int(ttl.Seconds()), formatDuration(ttl))
if count := 0; count >= 0 { // Banned logic
c, _ := fmt.Sscanf(rediscounter, "%d", &count)
if c > 0 && count >= 5 {
fmt.Println("Banned: YES")
} else {
fmt.Println("Banned: NO")
}
}
// Additional data (data+attacker)
dataStr, err := rdb.Get(ctx, "data"+attacker).Result()
if err == nil {
parts := strings.Split(dataStr, "++")
if len(parts) >= 9 {
fmt.Printf("timestamp: %s\n", parts[0])
fmt.Printf("useragent_platform: %s\n", parts[2])
fmt.Printf("useragent: %s\n", parts[3])
fmt.Printf("message: %s\n", parts[4])
fmt.Printf("host: %s\n", parts[5])
fmt.Printf("url: %s\n", parts[6])
fmt.Printf("method: %s\n", parts[7])
}
}
}
// 2. PF Section
fmt.Println("\n---------------")
fmt.Println(">> PF")
fmt.Println("---------------")
pfCmd := exec.Command("pfctl", "-t", "badguys", "-T", "show")
pfOut, _ := pfCmd.Output()
fmt.Printf("PF Banned hosts: \n%s", string(pfOut))
// 3. HA-Proxy FILE Section
fmt.Println("\n----------------")
fmt.Println(">> HA-Proxy FILE")
fmt.Println("----------------")
if _, err := os.Stat(HaProxyBlacklistFile); err == nil {
file, _ := os.Open(HaProxyBlacklistFile)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
file.Close()
}
// 4. HA-Proxy ACL Section
fmt.Println("\n----------------")
fmt.Println(">> HA-Proxy ACL")
fmt.Println("---------------")
aclResp, err := haproxyCommand("show acl")
if err != nil {
fmt.Printf("ERROR: %v\n", err)
} else {
re := regexp.MustCompile(BadguysAclPattern)
lines := strings.Split(aclResp, "\n")
aclID := ""
for _, line := range lines {
if re.MatchString(line) {
fields := strings.Fields(line)
if len(fields) > 0 {
aclID = strings.TrimPrefix(fields[0], "#")
break
}
}
}
if aclID != "" {
content, _ := haproxyCommand("show acl #" + aclID)
cLines := strings.Split(content, "\n")
for _, cLine := range cLines {
cFields := strings.Fields(cLine)
if len(cFields) >= 2 {
fmt.Println(cFields[1]) // The second field is the IP/Pattern
}
}
}
}
}
Compilamos y copiamos el binario al sistema:
cp showBadGuys /usr/local/bin/
Al ejecutarlo veremos la siguiente salida:
---------------
>> REDIS
---------------
> Attacker: 192.168.69.202 Counter: 19
TTL: 3522 RemainingTime: 0:58:42
Banned: YES
timestamp: Sat Feb 25 12:56:26 2023
useragent_platform: "Linux"
useragent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36
message: SQL Injection Attack Detected via libinjection
---------------
>> PF
---------------
PF Banned hosts:
192.168.69.202
----------------
>> HA-Proxy FILE
----------------
192.168.69.202
----------------
>> HA-Proxy ACL
---------------
192.168.69.202
Otra función realmente útil es poder flushear todos los baneos:
cd flushBadGuys
go mod init flushBadGuys
go get github.com/redis/go-redis/v9
package main
import (
"bytes"
"context"
"fmt"
"io"
"net"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"github.com/redis/go-redis/v9"
)
const (
HaProxyBlacklistFile = "/usr/local/bastille/jails/Atlas/root/usr/local/etc/badguys.list"
HaProxyAdminSocketDir = "/usr/local/bastille/jails/Atlas/root/var/run/"
BadguysAclPattern = `badguys\.list`
)
var ctx = context.Background()
// haproxyExecute handles multiple commands in a single session
func haproxyExecute(commands []string) (string, error) {
files, _ := filepath.Glob(filepath.Join(HaProxyAdminSocketDir, "*.sock"))
if len(files) == 0 {
files = append(files, filepath.Join(HaProxyAdminSocketDir, "haproxy.sock"))
}
var lastErr error
for _, socketPath := range files {
conn, err := net.Dial("unix", socketPath)
if err != nil {
lastErr = err
continue
}
defer conn.Close()
for _, cmd := range commands {
fmt.Fprintf(conn, cmd+"\n")
}
var buf bytes.Buffer
io.Copy(&buf, conn)
return buf.String(), nil
}
return "", fmt.Errorf("no connection to HAProxy socket: %v", lastErr)
}
func main() {
// 1. REDIS FLUSH
rdb := redis.NewClient(&redis.Options{Addr: "192.168.69.2:6379", Password: "XXXXXXXX", DB: 0})
fmt.Println("\n>> REDIS: Flushing database...")
if err := rdb.FlushDB(ctx).Err(); err != nil {
fmt.Printf(" Error: %v\n", err)
} else {
fmt.Println(" Success: Redis database cleared.")
}
// 2. PF FLUSH
fmt.Println("\n>> PF: Flushing table 'badguys'...")
if err := exec.Command("pfctl", "-t", "badguys", "-T", "flush").Run(); err != nil {
fmt.Printf(" Error: %v\n", err)
} else {
fmt.Println(" Success: PF table cleared.")
}
// 3. FILE TRUNCATE
fmt.Println("\n>> FILE: Truncating 'badguys.list'...")
if err := os.WriteFile(HaProxyBlacklistFile, []byte(""), 0644); err != nil {
fmt.Printf(" Error: %v\n", err)
} else {
fmt.Println(" Success: File truncated.")
}
// 4. HA-PROXY ACL FLUSH
fmt.Println("\n>> HA-PROXY: Searching for ACL-Badguys...")
resp, err := haproxyExecute([]string{"show acl"})
if err != nil {
fmt.Printf(" Socket Error: %v\n", err)
return
}
re := regexp.MustCompile(BadguysAclPattern)
lines := strings.Split(resp, "\n")
var targetAcls []string
for _, line := range lines {
if re.MatchString(line) {
fields := strings.Fields(line)
if len(fields) > 0 {
// Your HAProxy returns ID as the first field (e.g., "12")
id := strings.TrimPrefix(fields[0], "#")
targetAcls = append(targetAcls, id)
}
}
}
if len(targetAcls) == 0 {
fmt.Println(" Notice: No ACL linked to 'badguys.list' was found.")
return
}
for _, id := range targetAcls {
fmt.Printf(" Action: Cleaning entries for ACL-Badguys (ID: %s)...\n", id)
// Attempting fast clear (HAProxy 2.2+)
clearResp, _ := haproxyExecute([]string{fmt.Sprintf("clear acl #%s", id)})
// Fallback to manual deletion if 'clear' is unsupported
if strings.Contains(clearResp, "Unknown command") {
fmt.Println(" Fallback: 'clear' command unsupported. Deleting entries manually...")
content, _ := haproxyExecute([]string{fmt.Sprintf("show acl #%s", id)})
cLines := strings.Split(content, "\n")
var delCmds []string
count := 0
for _, cLine := range cLines {
cFields := strings.Fields(cLine)
// Format: [reference] [value/IP]
if len(cFields) >= 2 {
ip := cFields[1]
delCmds = append(delCmds, fmt.Sprintf("del acl #%s %s", id, ip))
count++
}
}
if len(delCmds) > 0 {
haproxyExecute(delCmds)
fmt.Printf(" Success: %d entries removed from ACL-Badguys.\n", count)
} else {
fmt.Println(" Notice: ACL-Badguys was already empty.")
}
} else {
fmt.Println(" Success: ACL-Badguys flushed via 'clear acl'.")
}
}
fmt.Println("\n>> Cleanup process completed successfully.")
}
Compilamos y copiamos el binario al sistema:
cp flushBadGuys /usr/local/bin/
Al ejecutarlo veremos la siguiente salida:
>> REDIS: Flushing database...
Success: Redis database cleared.
>> PF: Flushing table 'badguys'...
Success: PF table cleared.
>> FILE: Truncating 'badguys.list'...
Success: File truncated.
>> HA-PROXY: Searching for ACL-Badguys...
Action: Cleaning entries for ACL-Badguys (ID: 12)...
Success: ACL-Badguys flushed via 'clear acl'.
>> Cleanup process completed successfully.
Resultado final
Las notificaciones en Telegram tendrán este aspecto:
Y el dashboard este: