Introducción

Top 10 – Parte 1: Encoding

10 Herramientas de Decodificación (Encoding) que Todo Hacker Debe Conocer

Cuando estás frente a un terminal, un volcado de memoria o un tráfico de red interceptado,
el mayor enemigo no es la complejidad del algoritmo — es el encoding.
Una cadena en Base64, un string escapado en Unicode o una secuencia de hex pueden parecer ruido
hasta que sabes exactamente qué estás mirando y por qué está ahí.

Este artículo es el primero de una trilogía. Aquí cubrimos solo encoding (codificación reversible).
Los próximos artículos cubrirán hashing y encriptación por separado.


¿Qué es encoding y por qué importa?

El encoding transforma datos de un formato a otro usando una tabla de correspondencia
pública y reversible. No requiere clave secreta. Cualquiera puede codificar y decodificar.

Encoding ≠ Encriptación ≠ Hashing

PropiedadEncodingEncriptaciónHashing
ReversibleSí (públicamente)Sí (con clave)No
Requiere claveNoNo
Longitud de salidaVariableVariableFija
Uso principalRepresentaciónConfidencialidadIntegridad

1. Base64

Convierte datos binarios en texto ASCII usando 64 caracteres: A-Z, a-z, 0-9, +, /.
Cada 3 bytes → 4 caracteres. Padding con = para completar el bloque final.

Es, con diferencia, el encoding más ubicuo en seguridad ofensiva. Lo encuentras en
cookies de sesión, tokens de autenticación, Data URIs en phishing, payloads de malware,
y prácticamente cualquier lugar donde se necesite representar binario como texto.

Usos en CTF

  • Cookies de sesión: El estado de autenticación suele almacenarse en cookies
    codificadas en Base64. Una cookie YWRtaW46ZmFsc2U= decodifica a admin:false.
    Modificarla a admin:true y recodificar permite escalar privilegios sin romper
    el formato del token.
  • Doble codificación: Cadenas codificadas 3, 5 o 10 veces seguidas para
    ofuscar la flag. Cada capa hay que decodificarla individualmente.
  • Alfabeto personalizado: Algunos retos modifican el alfabeto estándar
    reemplazando +/ por -_ u otros pares. Hay que traducir al alfabeto estándar
    antes de decodificar.
  • Payloads HTTP ofuscados: Parámetros GET o cuerpos POST que esconden
    comandos o flags en Base64.
  • Imágenes embebidas: Las Data URIs data:image/png;base64,... pueden
    contener flags en los metadatos EXIF o simplemente ser el vehículo de la flag.

Usos en el mundo real

  • Autenticación HTTP Basic: El header Authorization: Basic transmite
    credenciales en Base64. En logs de proxy o servidor, decodificarlas revela
    credenciales en texto plano si no se usa HTTPS.
  • Análisis de malware: El malware suele almacenar configuración, C2 URLs o
    la segunda etapa en Base64 para ofuscar cadenas. Decodificarlas revela la
    infraestructura del atacante (ej: Emotet usaba este patrón).
  • Data URIs en phishing: Los atacantes embeben páginas completas en Base64
    dentro del propio HTML para evadir filtros de red que analizan URLs externas.
  • APIs REST: Intercambio de archivos codificados en Base64 dentro de JSON
    cuando el medio no soporta binario.
  • Forense: Extraer archivos de volcados de memoria, tráfico de red o
    imágenes de disco donde aparecen como cadenas Base64.

🧪 Código Python — Base64

import base64
import re

# ─── Básico ───────────────────────────────────────────────
def base64_encode(data: str) -> str:
    return base64.b64encode(data.encode("utf-8")).decode("utf-8")

def base64_decode(data: str) -> str:
    return base64.b64decode(data).decode("utf-8")

# ─── Detección ────────────────────────────────────────────
def es_base64(cadena: str) -> bool:
    patron = r'^[A-Za-z0-9+/]*={0,2}$'
    return bool(re.match(patron, cadena)) and len(cadena) % 4 == 0

# ─── Doble Base64 recursivo ───────────────────────────────
def decode_recursive(data: str, max_depth: int = 10) -> str:
    current = data
    for _ in range(max_depth):
        try:
            decoded = base64.b64decode(current).decode("utf-8", errors="ignore")
            if not es_base64(decoded):
                return decoded
            current = decoded
        except Exception:
            return current
    return current

# ─── Alfabeto personalizado ───────────────────────────────
CUSTOM_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
STANDARD_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

def decode_custom_b64(data: str) -> str:
    trans = str.maketrans(CUSTOM_ALPHABET, STANDARD_ALPHABET)
    return base64.b64decode(data.translate(trans)).decode()

# ─── Cookie de sesión ─────────────────────────────────────
cookie = "YWRtaW46ZmFsc2U="
print(base64_decode(cookie))             # admin:false
print(base64_encode("admin:true"))       # YWRtaW46dHJ1ZQ==

# ─── Data URI ─────────────────────────────────────────────
def parse_data_uri(uri: str) -> tuple:
    from urllib.parse import unquote
    match = re.match(r'data:(.*?)(;base64)?,(.*)', uri)
    if not match:
        return None, None
    mediatype = match.group(1) or "text/plain"
    is_base64 = bool(match.group(2))
    data = match.group(3)
    if is_base64:
        return mediatype, base64.b64decode(data)
    return mediatype, unquote(data).encode()

2. URL Encode (Percent Encoding)

Codifica caracteres especiales como %20 (espacio), %3C (<), %3E (>), %22 (").
Es el encoding estándar para transmitir datos en URLs y formularios web.

Cada carácter que no sea seguro en una URL se representa como % seguido de su valor
hexadecimal en dos dígitos. Es totalmente reversible y no ofrece seguridad — solo
garantiza que la URL se transmita sin ambigüedades.

Usos en CTF

  • Bypass de WAFs: Los firewalls de aplicaciones web decodifican una vez.
    La doble codificación URL (%253C en lugar de %3C) hace que el WAF vea
    %3C (inofensivo) mientras el servidor lo decodifica a < (peligroso).
  • Path traversal ofuscado: ..%2F..%2F..%2Fetc%2Fpasswd se usa para
    bypassear filtros de traversal que buscan ../ literal.
  • Parámetros ocultos: Flags escondidas en query strings que fueron
    ofuscadas con codificación URL parcial.
  • CSRF tokens: Tokens anticsfr que vienen codificados en formularios y
    necesitan decodificarse para entender su estructura.

Usos en el mundo real

  • Análisis de logs de servidor: Los logs web registran las URLs exactamente
    como llegan. Decodificarlas revela intentos de ataque reales: un log con
    %2e%2e/%2e%2e/etc/passwd no es más que un escaneo automático de LFI.
  • Construcción de payloads SSRF/LFI: Al codificar caracteres especiales,
    se pueden bypassear validaciones de entrada que filtran patrones literales.
  • Ataques XSS: Al codificar <script> como %3Cscript%3E, se puede
    bypassear filtros básicos que buscan cadenas exactas.
  • Depuración de APIs: Los parámetros de API suelen llegar URL-encoded.
    Decodificarlos es el primer paso para entender una petición.

🧪 Código Python — URL Encode

from urllib.parse import quote, unquote
import re

# ─── Básico ───────────────────────────────────────────────
def url_encode(data: str) -> str:
    return quote(data, safe='')

def url_decode(data: str) -> str:
    return unquote(data)

def double_url_encode(data: str) -> str:
    return url_encode(url_encode(data))

# ─── Detección ────────────────────────────────────────────
def tiene_url_encoding(data: str) -> bool:
    return bool(re.search(r'%[0-9A-Fa-f]{2}', data))

def contar_pasos_url_encode(data: str) -> int:
    current = data
    steps = 0
    while re.search(r'%[0-9A-Fa-f]{2}', current):
        current = unquote(current)
        steps += 1
    return steps

# ─── Bypass de WAF ────────────────────────────────────────
payload = "<script>alert(1)</script>"
print(f"Single: {url_encode(payload)}")
print(f"Double: {double_url_encode(payload)}")

# ─── Path traversal ───────────────────────────────────────
path_traversal = url_encode("../../../etc/passwd")
# %2E%2E%2F%2E%2E%2F%2E%2E%2Fetc%2Fpasswd

# ─── Análisis de logs ─────────────────────────────────────
def analizar_log_urls(log: str) -> list:
    urls = re.findall(r'(?:GET|POST|PUT)\s+(\S+)', log)
    decoded = [url_decode(url) for url in urls]
    return list(zip(urls, decoded))

log_line = 'GET /%2e%2e/%2e%2e/%2e%2e/etc/passwd HTTP/1.1'
print(analizar_log_urls(log_line))
# → [('/%2e%2e/%2e%2e/%2e%2e/etc/passwd', '/../../../etc/passwd')]

3. Hex Encode / Decode

Representación hexadecimal (base 16) de datos binarios. Cada byte → 2 caracteres hex (0-9, A-F).
La forma más directa de leer y escribir datos no imprimibles.

Es el encoding estándar para volcados de memoria, shellcode, direcciones de memoria,
magic bytes de archivos, y cualquier representación de bajo nivel.

Usos en CTF

  • Shellcode y exploits: Los exploits se distribuyen en hex porque contienen
    bytes no imprimibles. \x31\xc0\x50\x68\x2f\x2f\x73\x68 es la representación
    clásica de un shellcode execve(«/bin/sh»).
  • Direcciones de memoria: En retos de buffer overflow, ROP o ret2libc, las
    direcciones se escriben en hex y el endianness importa: 0x0804a000 en
    little-endian se convierte en \x00\xa0\x04\x08.
  • Magic bytes: Identificar tipos de archivo por su cabecera hexadecimal.
    FF D8 FF E0 = JPEG, 89 50 4E 47 = PNG, 7F 45 4C 46 = ELF.
  • Volcados de memoria: Flags escondidas en medio de bytes no imprimibles
    que solo se ven al convertir el volcado a hex.
  • LSB Steganography: Datos ocultos en el bit menos significativo de cada
    píxel; se extraen como bits y se convierten a hex y luego a texto.

Usos en el mundo real

  • Análisis de malware: Convertir binarios a hex para buscar strings
    ofuscados, direcciones IP hardcodeadas o firmas de malware conocidas.
  • Forense de discos: Recuperar datos de sectores dañados, analizar
    metadatos del sistema de archivos o reconstruir archivos borrados.
  • Análisis de tráfico de red: Los protocolos binarios (DNS, SMB, TCP)
    se leen naturalmente en hex. Un túnel DNS puede esconder datos en
    campos TXT codificados en hex.
  • Direcciones MAC: Se representan en hex con separadores: aa:bb:cc:dd:ee:ff.
  • Ingeniería inversa: Opcodes de CPU, parches binarios, modificación de
    ejecutables a nivel de bytes.

🧪 Código Python — Hex

import binascii
import re

# ─── Básico ───────────────────────────────────────────────
def hex_encode(data: str) -> str:
    return data.encode("utf-8").hex()

def hex_decode(data: str) -> str:
    return bytes.fromhex(data).decode("utf-8")

# ─── Detección ────────────────────────────────────────────
def es_hex(cadena: str) -> bool:
    return bool(re.fullmatch(r'[0-9A-Fa-f]+', cadena)) and len(cadena) % 2 == 0

def hex_con_spaces(data: str) -> str:
    clean = data.replace(" ", "").replace("\n", "").replace("\t", "")
    return hex_decode(clean)

# ─── Hexdump formateado ───────────────────────────────────
def format_hex_pretty(data: bytes, cols: int = 16) -> str:
    result = []
    for i in range(0, len(data), cols):
        chunk = data[i:i+cols]
        hex_part = " ".join(f"{b:02x}" for b in chunk)
        ascii_part = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
        result.append(f"{i:08x}  {hex_part:<48}  {ascii_part}")
    return "\n".join(result)

# ─── Endianness ───────────────────────────────────────────
def hex_little_endian(hex_str: str) -> str:
    bytes_list = [hex_str[i:i+2] for i in range(0, len(hex_str), 2)]
    return "".join(reversed(bytes_list))

def addr_to_bytes(addr: int, arch: str = "x64") -> bytes:
    if arch == "x64":
        return addr.to_bytes(8, byteorder="little")
    return addr.to_bytes(4, byteorder="little")

# ─── Magic bytes ──────────────────────────────────────────
MAGIC_BYTES = {
    "89504e47": "PNG", "ffd8ffe0": "JPEG", "25504446": "PDF",
    "504b0304": "ZIP/DOCX", "7f454c46": "ELF", "4d5a": "PE",
    "424d": "BMP",
}

def identificar_tipo_archivo(hex_header: str) -> str:
    for magic, tipo in MAGIC_BYTES.items():
        if hex_header.lower().startswith(magic):
            return tipo
    return "Desconocido"

# ─── MAC addresses ────────────────────────────────────────
def mac_a_hex(mac: str) -> str:
    return mac.replace(":", "").replace("-", "").replace(".", "")

def hex_a_mac(hex_str: str, sep: str = ":") -> str:
    bytes_list = [hex_str[i:i+2] for i in range(0, len(hex_str), 2)]
    return sep.join(bytes_list)

4. HTML Entities Encode / Decode

Convierte caracteres especiales HTML en entidades: <&lt;, >&gt;, &&amp;,
"&quot;, caracteres Unicode → &#x26; o &#38;.

Es el mecanismo nativo del navegador para representar caracteres que tienen
significado especial en HTML, y también se usa para ofuscar contenido.

Usos en CTF

  • Bypass de filtros XSS: Si el servidor bloquea <script> pero renderiza
    &lt;script&gt; sin re-escaparlo, se ejecuta XSS. El navegador interpreta
    las entidades HTML y produce <script> en el DOM.
  • Contenido oculto: Flags escondidas en el código fuente como entidades
    HTML numéricas o con nombre. Solo se ven al decodificarlas.
  • Doble codificación: &lt; se puede codificar de nuevo → &amp;lt;.
    Si el servidor decodifica solo una vez, el resultado sigue siendo &lt;,
    que el navegador interpreta como <.
  • Web scraping: Páginas que ofuscan texto con entidades HTML para
    dificultar la extracción automática.

Usos en el mundo real

  • Auditoría de XSS: El paso 1 de cualquier auditoría es inyectar
    &lt;script&gt; y observar si el servidor lo escapa o lo renderiza.
  • Construcción de payloads: Cuando un WAF bloquea <, usar &#60; o
    &#x3C; puede evadirlo. Los motores de plantillas HTML interpretan
    ambas formas como el mismo carácter.
  • Análisis de emails de phishing: Los phishers esconden URLs maliciosas
    en entidades HTML para evadir filtros de texto plano. Decodificarlas
    revela el destino real del enlace.
  • Decodificación de feeds RSS/API: Muchos feeds devuelven contenido
    con entidades HTML. Decodificarlo es necesario para leerlo correctamente.

🧪 Código Python — HTML Entities

import html
import re

# ─── Básico ───────────────────────────────────────────────
def html_encode(data: str) -> str:
    return html.escape(data, quote=True)

def html_decode(data: str) -> str:
    return html.unescape(data)

# ─── Decodificación manual (numéricas + con nombre) ──────
def decode_all_html_entities(data: str) -> str:
    result = html.unescape(data)
    result = re.sub(r'&#(\d+);', lambda m: chr(int(m.group(1))), result)
    result = re.sub(r'&#x([0-9A-Fa-f]+);', lambda m: chr(int(m.group(1), 16)), result)
    return result

# ─── Bypass XSS ───────────────────────────────────────────
def construir_payload_xss_bypass(payload: str) -> str:
    mapping = {
        '<': '&lt;', '>': '&gt;', '"': '&quot;',
        "'": '&#x27;', '&': '&amp;',
    }
    return "".join(mapping.get(c, c) for c in payload)

def double_html_encode(data: str) -> str:
    return html_encode(html_encode(data))

# ─── Phishing: extraer URLs de HTML ofuscado ──────────────
def extract_urls_from_entities(html_source: str) -> list:
    decoded = html_decode(html_source)
    return re.findall(r'href=["\'](https?://[^"\']+)["\']', decoded)

5. Base64URL (JWT y más)

Variante de Base64 que usa - y _ en lugar de + y /, y omite el padding =.
Específicamente diseñada para contextos donde + y / tienen significado en URLs.

Su aplicación más conocida es JWT (JSON Web Tokens), donde la cabecera y el payload
van codificados en Base64URL. Pero también aparece en tokens OAuth, enlaces seguros
y cualquier lugar donde Base64 necesite ser «URL-safe».

Usos en CTF

  • JWT — Algoritmo none: El servidor acepta tokens sin firma cambiando
    el algorimo a none. Basta con codificar el payload modificado y la firma se omite.
  • JWT — Confusión RS256 → HS256: Si el servidor usa clave pública RSA para
    verificar pero la expone, se puede cambiar a HMAC usando la clave pública
    como secret simétrico.
  • JWT — Firma débil: Secrets como «secret», «password» o «123456». Se
    fuerza bruta con hashcat o wordlist.
  • JWT — KID injection: El campo kid (Key ID) puede ser vulnerable a
    path traversal o SQLi si no se valida.
  • Manipulación de payload: Cambiar {"user":"guest","admin":false} a
    {"user":"admin","admin":true} es un clásico.
  • Tokens en parámetros URL: A veces los tokens viajan en la URL, por lo
    que el Base64 estándar con +/ rompe la URL. Base64URL soluciona esto.

Usos en el mundo real

  • Depuración de integraciones: Desarrolladores y pentesters decodifican
    JWT para verificar claims, expiración y roles del token.
  • Auditoría de seguridad OAuth/OpenID: Los ID Tokens son JWTs. Verificar
    que la firma se valida, que no usa algoritmos débiles y que los claims
    son correctos es parte fundamental de cualquier auditoría.
  • OWASP Top 10 A2 (Broken Authentication): JWT mal configurados son una
    de las causas principales de autenticación rota.
  • Mobile app reversing: Extraer JWT almacenados en apps móviles para
    escalar privilegios.

🧪 Código Python — Base64URL / JWT

import base64
import json
import hmac
import hashlib
import time

# ─── Base64URL básico ─────────────────────────────────────
def base64url_encode(data: str) -> str:
    return base64.urlsafe_b64encode(data.encode()).decode().rstrip("=")

def base64url_decode(data: str) -> str:
    padding = 4 - len(data) % 4
    if padding != 4:
        data += "=" * padding
    return base64.urlsafe_b64decode(data).decode()

# ─── JWT: decodificar sin verificar ───────────────────────
def jwt_decode_payload(token: str) -> dict:
    parts = token.split(".")
    return json.loads(base64url_decode(parts[1]))

def jwt_decode_header(token: str) -> dict:
    return json.loads(base64url_decode(token.split(".")[0]))

# ─── JWT: forjar payload (sin firma) ──────────────────────
def jwt_forjar_payload(token: str, new_payload: dict) -> str:
    header = jwt_decode_header(token)
    h = base64url_encode(json.dumps(header))
    p = base64url_encode(json.dumps(new_payload))
    return f"{h}.{p}."

# ─── JWT: ataque "alg: none" ──────────────────────────────
def jwt_none_attack(payload: dict) -> str:
    h = base64url_encode(json.dumps({"alg": "none", "typ": "JWT"}))
    p = base64url_encode(json.dumps(payload))
    return f"{h}.{p}."

# ─── JWT: verificar / crackear secret ─────────────────────
def jwt_verificar_secret(token: str, secret: str) -> bool:
    parts = token.split(".")
    msg = f"{parts[0]}.{parts[1]}".encode()
    expected = base64.urlsafe_b64encode(
        hmac.new(secret.encode(), msg, hashlib.sha256).digest()
    ).rstrip(b"=").decode()
    return expected == parts[2]

def jwt_crack_secret(token: str, wordlist_path: str) -> str:
    with open(wordlist_path, "r", errors="ignore") as f:
        for line in f:
            secret = line.strip()
            if jwt_verificar_secret(token, secret):
                return secret
    return None

# ─── JWT: auditoría completa ──────────────────────────────
def jwt_analizar(token: str) -> dict:
    header = jwt_decode_header(token)
    payload = jwt_decode_payload(token)
    info = {
        "header": header,
        "payload": payload,
        "algoritmo": header.get("alg"),
        "expira": None, "expira_en": None,
    }
    if "exp" in payload:
        exp = payload["exp"]
        info["expira"] = time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime(exp))
        delta = exp - time.time()
        info["expira_en"] = f"{delta/3600:.1f}h" if delta > 0 else "EXPIRADO"
    return info

6. ASCII / Char Code Converter

Convierte caracteres a su valor numérico ASCII (decimal) y viceversa. A65, 65A.
Incluye conversión a otras bases: hexadecimal, octal y binario.

Aunque es la herramienta más simple de la lista, es la base de todas las demás.
Cada carácter que ves en pantalla tiene un número detrás, y entender esa relación
es fundamental para manipular datos a bajo nivel.

Usos en CTF

  • Decodificación de flags en números: Una flag puede aparecer como
    secuencia de valores ASCII: 84 104 101 32 102 108 97 103. Convertir
    cada número a carácter revela el texto. Es uno de los retos más comunes
    en categorías «misc» o «crypto básico».
  • XOR cipher: El XOR opera sobre valores numéricos. Para aplicarlo,
    conviertes cada carácter a su código ASCII, aplicas XOR, y conviertes
    de vuelta. Forzar clave desconocida implica probar 256 valores.
  • Strings ofuscados en binarios: Algunos ejecutables ofuscan cadenas
    almacenando sus valores ASCII como números para evadir strings.
    [112, 97, 115, 115, 119, 100] es «password».
  • Construcción de shellcode: Los opcodes son valores numéricos.
    Conocerlos permite construir exploits manualmente.

Usos en el mundo real

  • Construcción manual de payloads: Saber que 0x0a es LF, 0x00 es
    null terminator, 0x20 es espacio, es esencial para construir exploits
    y entender por qué ciertos caracteres rompen protocolos.
  • Ingeniería inversa: Código descompilado muestra strings como arreglos
    de bytes. Convertirlos a ASCII permite leer el texto original.
  • Análisis de tráfico de red: Protocolos de bajo nivel se leen byte a
    byte. Identificar caracteres de control (SOH, STX, ETX, etc.) es
    fundamental para depurar comunicaciones.

🧪 Código Python — ASCII / Char Code

# ─── Básico ───────────────────────────────────────────────
def ascii_to_codes(text: str) -> list:
    return [ord(c) for c in text]

def codes_to_ascii(codes: list) -> str:
    return "".join(chr(c) for c in codes)

# ─── Formateo flexible ────────────────────────────────────
def ascii_format(text: str, sep: str = " ", base: int = 10) -> str:
    if base == 10: return sep.join(str(ord(c)) for c in text)
    if base == 16: return sep.join(hex(ord(c)) for c in text)
    if base == 8:  return sep.join(oct(ord(c)) for c in text)
    if base == 2:  return sep.join(bin(ord(c)) for c in text)

def codes_from_format(data: str, sep: str = " ", base: int = 10) -> str:
    parts = data.split(sep)
    chars = []
    for p in parts:
        p = p.strip()
        if not p: continue
        if base == 16: p = p.replace("0x", ""); chars.append(chr(int(p, 16)))
        elif base == 8:  p = p.replace("0o", ""); chars.append(chr(int(p, 8)))
        elif base == 2:  p = p.replace("0b", ""); chars.append(chr(int(p, 2)))
        else: chars.append(chr(int(p)))
    return "".join(chars)

# ─── XOR cipher y bruteforce ──────────────────────────────
def xor_encrypt(data: str, key: int) -> str:
    return codes_to_ascii([c ^ key for c in ascii_to_codes(data)])

def xor_bruteforce(data: str) -> list:
    results = []
    for key in range(256):
        result = xor_encrypt(data, key)
        if result.isprintable() and any(c.isalpha() for c in result):
            results.append((key, result))
    return results

# ─── Análisis de payloads ─────────────────────────────────
def analizar_payload(data: bytes) -> list:
    return [
        (c, chr(c), "printable") if 32 <= c < 127 else (c, f"\\x{c:02x}", "non-printable")
        for c in data
    ]

7. Binary Encode / Decode

Convierte texto a binario (0s y 1s) y viceversa. Cada carácter se representa por 8 bits.
La forma más fundamental de representar datos digitales.

Es el «encoding original» — toda la información digital no es más que unos y ceros.
Convertir entre texto y binario es la herramienta más básica para entender cómo
funciona la computación a nivel de hardware.

Usos en CTF

  • Flags en binario en descripciones: Secuencias de 0s y 1s en la
    descripción del reto que hay que convertir a texto para obtener la flag.
    Es común en retos de calentamiento («warmup»).
  • LSB Steganography: El bit menos significativo de cada canal de color
    en una imagen puede contener datos ocultos. Se extraen los bits, se
    agrupan en bytes y se convierten a texto.
  • Permisos de archivos: rwxr-xr-x en binario es 111101101, que en
    octal es 755. Los CTFs de Linux a veces preguntan por la representación
    binaria de los permisos.
  • Direcciones IP en binario: 192.168.1.1 = 11000000.10101000.00000001.00000001.
    Entender esto es clave para subnetting.

Usos en el mundo real

  • Permisos en Linux: chmod 755 = rwxr-xr-x = 111101101. Entender
    la representación binaria es esencial para hardening de sistemas.
  • Subnetting y CIDR: Las máscaras de red se entienden mejor en binario.
    255.255.255.0 = 11111111.11111111.11111111.00000000 = /24.
  • Protocolos de bajo nivel: Cabeceras TCP/IP con flags SYN (000010),
    ACK (010000), FIN (000001). Leer un volcado de paquetes requiere
    entender estos bits.
  • Depuración IoT: Comunicación con sensores y dispositivos a través de
    protocolos serie (UART, I2C, SPI) donde los datos se transmiten bit a bit.

🧪 Código Python — Binary

# ─── Básico ───────────────────────────────────────────────
def binary_encode(text: str) -> str:
    return " ".join(format(ord(c), "08b") for c in text)

def binary_decode(binary: str) -> str:
    clean = binary.replace(" ", "").replace("\n", "")
    chars = []
    for i in range(0, len(clean), 8):
        byte = clean[i:i+8]
        if len(byte) == 8:
            chars.append(chr(int(byte, 2)))
    return "".join(chars)

# ─── Variantes ────────────────────────────────────────────
def binary_encode_raw(text: str) -> str:
    return "".join(format(ord(c), "08b") for c in text)

def binary_decode_raw(data: str) -> str:
    clean = "".join(c for c in data if c in "01")
    return binary_decode(clean)

# ─── Permisos de archivos ─────────────────────────────────
def permisos_a_binario(permisos: str) -> str:
    mapping = {'r': '1', 'w': '1', 'x': '1', '-': '0'}
    return "".join(mapping.get(c, '0') for c in permisos)

def binario_a_permisos(binary: str) -> str:
    bits = [('r', '100'), ('w', '010'), ('x', '001')]
    result = ""
    for i in range(0, 9, 3):
        for bit_char, expected in bits:
            result += bit_char if binary[i:i+3] == expected else '-'
    return result

# ─── IP a binario ─────────────────────────────────────────
def ip_a_binario(ip: str) -> str:
    return ".".join(format(int(o), "08b") for o in ip.split("."))

def binario_a_ip(binary: str) -> str:
    return ".".join(str(int(o, 2)) for o in binary.split("."))

def mascara_a_binario(prefix: int) -> str:
    return "1" * prefix + "0" * (32 - prefix)

# ─── LSB Steganography (requiere Pillow) ──────────────────
def extract_lsb_text(image_path: str) -> str:
    from PIL import Image
    import numpy as np
    img = Image.open(image_path)
    pixels = np.array(img)
    bits = []
    for row in pixels:
        for pixel in row:
            for channel in pixel[:3]:
                bits.append(str(channel & 1))
    return binary_decode("".join(bits))

8. Unicode Escape (\uXXXX)

Representa caracteres Unicode usando \u seguido de 4 dígitos hexadecimales.
Ej: \u0041 = A, \u03B1 = α (alpha), \u00E9 = é.

También existe la variante \UXXXXXXXX (32 bits) caracteres fuera del BMP.
Es el encoding usado internamente por motores JavaScript y Java para
representar cadenas en código fuente.

Usos en CTF

  • Ofuscación de JavaScript: Es la técnica clásica de evasión de WAF para
    XSS. \u0061\u006c\u0065\u0072\u0074(1) ejecuta alert(1). Cada letra
    está ofuscada pero el motor JS lo interpreta igual.
  • Bypass de filtros de caracteres: Si ( está bloqueado, \u0028
    funciona como paréntesis en la mayoría de motores JS.
  • IDN Homograph Attack: Caracteres Unicode visualmente idénticos a
    ASCII: а cirílico (U+0430) vs a latino (U+0061). El dominio
    microsоft.com con la о cirílica se ve idéntico al legítimo.
  • Normalización Unicode bypasseada: Caracteres de ancho completo como
    <script> (U+FF1C y U+FF1E) se normalizan (NFKC) a <script>,
    evadiendo filtros que buscan los caracteres ASCII originales.
  • Retos de «encoding hell»: Cadenas que pasan por URL encode → Unicode
    escape → hex → Base64 en secuencia. Hay que revertir cada capa.

Usos en el mundo real

  • Ataques homográficos en phishing: El atacante registra un dominio con
    caracteres Unicode visualmente idénticos. арple.com (cirílico) se ve
    como apple.com. Miles de usuarios caen porque la barra de direcciones
    muestra caracteres casi idénticos.
  • WAF bypass: Inyectar payloads con Unicode escape para evadir reglas
    que buscan patrones ASCII exactos. \u003Cscript\u003E pasa filtros que
    bloquean <script>.
  • Normalización Unicode: Ataques que explotan cómo las aplicaciones
    normalizan caracteres. La é puede ser un carácter (U+00E9) o dos
    (e + \u0301, U+0065 + U+0301). Un filtro que revisa la primera
    forma puede no detectar la segunda.

🧪 Código Python — Unicode Escape

import re
import unicodedata

# ─── Básico ───────────────────────────────────────────────
def unicode_escape_encode(text: str) -> str:
    return "".join(f"\\u{ord(c):04x}" for c in text)

def unicode_escape_decode(data: str) -> str:
    return re.sub(r'\\u([0-9a-fA-F]{4})',
                  lambda m: chr(int(m.group(1), 16)), data)

# ─── Con soporte para \UXXXXXXXX (32-bit) ─────────────────
def unicode_escape_decode_completo(data: str) -> str:
    def replace(m):
        return chr(int(m.group(1), 16))
    result = re.sub(r'\\u([0-9a-fA-F]{4})', replace, data)
    result = re.sub(r'\\U([0-9a-fA-F]{8})', replace, result)
    return result

# ─── Ofuscación JS ────────────────────────────────────────
def build_xss_payload(script: str) -> str:
    return "".join(f"\\u{ord(c):04x}" if c.isalpha() else c for c in script)

# ─── Homograph: construir dominios falsos ─────────────────
HOMOGRAPH_CHARS = {
    'a': '\u0430', 'e': '\u0435', 'o': '\u043e',
    'c': '\u0441', 'p': '\u0440',
}

def build_homograph_domain(domain: str) -> str:
    return "".join(HOMOGRAPH_CHARS.get(c, c) for c in domain)

def detect_homograph(text: str) -> list:
    suspicious = []
    for c in text:
        if ord(c) > 127 and chr(ord(c)).lower() in "abcdefghijklmnopqrstuvwxyz":
            suspicious.append((c, hex(ord(c))))
    return suspicious

# ─── Normalización bypass ─────────────────────────────────
def unicode_normalize_bypass(payload: str) -> str:
    alternatives = {
        '<': '\uff1c', '>': '\uff1e', '(': '\uff08', ')': '\uff09',
    }
    return "".join(alternatives.get(c, c) for c in payload)

# ─── Análisis de dominio sospechoso ───────────────────────
def analizar_dominio_sospechoso(url: str) -> dict:
    result = {"original": url, "sospechoso": False, "detalles": []}
    match = re.search(r'https?://([^/]+)', url)
    if not match:
        return result
    domain = match.group(1)
    for c in domain:
        if ord(c) > 0x7f:
            result["sospechoso"] = True
            result["detalles"].append({
                "char": c,
                "codepoint": f"U+{ord(c):04X}",
                "name": unicodedata.name(c, "UNKNOWN"),
            })
    result["normalized"] = unicodedata.normalize("NFKC", domain)
    return result

9. Base32

Similar a Base64 pero con 32 caracteres: A-Z y 2-7. Cada 5 bytes → 8 caracteres.
Padding con =. Es más legible que Base64 (no mezcla mayúsculas/minúsculas) y
no tiene caracteres ambiguos como 0, 1, 8 o 9.

Es menos usado que Base64, pero tiene aplicaciones muy específicas donde la
legibilidad humana y la ausencia de ambigüedad son importantes.

Usos en CTF

  • Secretos TOTP / 2FA: Las claves compartidas para Google Authenticator
    y otras apps 2FA se distribuyen en Base32. Un reto puede darte una clave
    TOTP y pedirte que generes el código actual o decodifiques la clave.
  • HMAC-based tokens: Algunos protocolos usan HMAC con salida codificada
    en Base32 para tener tokens cortos y legibles.
  • Retos de identificación: El formato Base32 es fácil de reconocer
    (solo mayúsculas y 2-7). Los CTFs a veces lo usan para esconder flags
    esperando que el participante no lo identifique.

Usos en el mundo real

  • Autenticación de dos factores (2FA/TOTP): El estándar RFC 6238 y
    RFC 4226 definen que las claves compartidas se representan en Base32.
    Es la aplicación más común de este encoding.
  • QR codes en autenticación: Las URLs otpauth://totp/... contienen
    el secret en Base32. Escanear el QR es equivalente a copiar la clave
    Base32.
  • DNS TXT records: A veces se usa Base32 para almacenar datos binarios
    en registros DNS de texto.

🧪 Código Python — Base32

import base64
import re
import hmac
import hashlib

# ─── Básico ───────────────────────────────────────────────
def base32_encode(data: str) -> str:
    return base64.b32encode(data.encode()).decode()

def base32_decode(data: str) -> str:
    return base64.b32decode(data).decode()

# ─── Detección ────────────────────────────────────────────
def es_base32(cadena: str) -> bool:
    return bool(re.fullmatch(r'[A-Z2-7]+=*', cadena))

# ─── HMAC en Base32 ───────────────────────────────────────
def hmac_base32(secret: str, message: str) -> str:
    h = hmac.new(secret.encode(), message.encode(), hashlib.sha256)
    return base64.b32encode(h.digest()).decode()

# ─── TOTP / 2FA ───────────────────────────────────────────
def generate_otp_auth_url(secret: str, issuer: str, account: str) -> str:
    clean = secret.replace("=", "")
    return f"otpauth://totp/{issuer}:{account}?secret={clean}&issuer={issuer}"

# ─── Comparación de densidades ────────────────────────────
def comparar_encodings(data: str) -> dict:
    raw = data.encode()
    return {
        "original": len(raw),
        "base64": len(base64.b64encode(raw)),
        "base32": len(base64.b32encode(raw)),
        "hex": len(raw) * 2,
        "binary": len(raw) * 8,
    }

10. Quoted-Printable

Usado en emails MIME para representar texto con caracteres no-ASCII.
Un carácter no seguro = = seguido de su valor hex (ej: =E1 para á).
Los espacios al final de línea se codifican como =20. Las líneas no pueden
superar 76 caracteres (se parten con =\n).

Es el encoding MIME más común para texto plano con caracteres acentuados
y símbolos especiales. Su ventaja es que el texto ASCII puro pasa sin
codificación, haciéndolo legible incluso sin decodificar.

Usos en CTF

  • Análisis de cabeceras de email: Las cabeceras MIME con =?UTF-8?Q?...?=
    contienen texto codificado en Quoted-Printable. Decodificarlas revela
    asuntos, remitentes o flags ocultas en metadatos de correo.
  • Ofuscación en phishing: Los atacantes codifican URLs carácter por
    carácter en QP para evadir filtros de texto plano. h=74t=74p=3A...
    no es detectado como URL por regex simples.
  • Header injection: Buscar patrones =0D=0A (CRLF codificado) en
    el cuerpo de un email revela intentos de inyección de cabeceras.

Usos en el mundo real

  • Análisis de correos phishing: Los emails maliciosos usan QP para
    ocultar contenido malicioso. Decodificarlos es el primer paso para
    extraer URLs, adjuntos y scripts.
  • Header injection detection: El patrón =0D=0A en un campo codificado
    es un indicador de intento de inyección de cabeceras. Detectarlo permite
    identificar correos maliciosos.
  • Decodificación de MIME: Cualquier herramienta de análisis de correo
    necesita decodificar QP para leer el contenido real del mensaje.
  • Campañas de spear-phishing: Los atacantes usan QP para codificar
    selectivamente ciertos caracteres y bypassear filtros corporativos.

🧪 Código Python — Quoted-Printable

import quopri
import re
from email.header import decode_header

# ─── Básico ───────────────────────────────────────────────
def qp_encode(data: str) -> str:
    return quopri.encodestring(data.encode("utf-8")).decode("utf-8")

def qp_decode(data: str) -> str:
    return quopri.decodestring(data.encode("utf-8")).decode("utf-8")

# ─── Cabeceras MIME ───────────────────────────────────────
def decode_mime_headers(headers: str) -> dict:
    decoded = {}
    for line in headers.strip().split("\n"):
        if ":" in line:
            key, value = line.split(":", 1)
            decoded_parts = decode_header(value.strip())
            text = "".join(
                part.decode(charset or "utf-8") if isinstance(part, bytes) else part
                for part, charset in decoded_parts
            )
            decoded[key.strip()] = text
    return decoded

# ─── Ofuscación de URLs ───────────────────────────────────
def qp_obfuscate_url(url: str) -> str:
    return "".join(
        f"={ord(c):02X}" if c not in "-._~:/?#[]@!$&'()*+,;=" else c
        for c in url
    )

# ─── Detección de header injection ────────────────────────
def detect_qp_header_injection(raw_email: str) -> list:
    patterns = [
        r'=0D=0A(Bcc|Cc|To|Reply-To|X-):',
        r'=0D=0A=0D=0A',
    ]
    results = []
    for pattern in patterns:
        matches = re.findall(pattern, raw_email, re.IGNORECASE)
        if matches:
            results.append({"pattern": pattern, "matches": matches})
    return results

Bonus: Detector universal de encoding

Combina todos los detectores anteriores para identificar automáticamente
qué encoding tiene una cadena, sin necesidad de probar manualmente.
🧪 Código Python — Detector universal

def detect_encoding(cadena: str) -> str:
    """Detecta qué encoding tiene una cadena."""
    if not cadena:
        return "empty"

    # Quoted-Printable
    if re.search(r'=[0-9A-F]{2}', cadena) and "=" in cadena[:50]:
        try:
            qp_decode(cadena)
            return "quoted-printable"
        except: pass

    # URL encode
    if re.search(r'%[0-9A-F]{2}', cadena):
        return "url-encode"

    # Unicode escape
    if re.search(r'\\u[0-9a-fA-F]{4}', cadena):
        return "unicode-escape"

    # Base64
    if re.fullmatch(r'[A-Za-z0-9+/]*={0,2}', cadena) and len(cadena) % 4 == 0 and len(cadena) > 4:
        try:
            base64.b64decode(cadena)
            return "base64"
        except: pass

    # Base64URL
    if re.fullmatch(r'[A-Za-z0-9\-_]*', cadena) and len(cadena) > 4 and "." not in cadena:
        try:
            base64url_decode(cadena)
            return "base64url"
        except: pass

    # Base32
    if re.fullmatch(r'[A-Z2-7]+=*', cadena) and len(cadena) > 4:
        try:
            base64.b32decode(cadena)
            return "base32"
        except: pass

    # Hex
    if re.fullmatch(r'[0-9a-fA-F]+', cadena) and len(cadena) % 2 == 0 and len(cadena) > 2:
        try:
            bytes.fromhex(cadena)
            return "hex"
        except: pass

    # Binary
    if re.fullmatch(r'[01\s]+', cadena) and len(cadena.replace(" ", "")) % 8 == 0:
        return "binary"

    return "unknown"

Multi-encoding decoder

Si una cadena ha pasado por múltiples capas de encoding (ej: Base64 → URL encode → Hex),
este decodificador las prueba todas recursivamente:
🧪 Código Python — Multi-decoder

from urllib.parse import unquote

DECODERS = {
    "base64": lambda x: base64.b64decode(x).decode("utf-8", errors="replace"),
    "base64url": lambda x: base64.urlsafe_b64decode(
        x + "=" * (4 - len(x) % 4) if len(x) % 4 else x
    ).decode("utf-8", errors="replace"),
    "base32": lambda x: base64.b32decode(x).decode("utf-8", errors="replace"),
    "hex": lambda x: bytes.fromhex(x).decode("utf-8", errors="replace"),
    "url": unquote,
    "html": html.unescape,
    "unicode_escape": lambda x: re.sub(
        r'\\u([0-9a-fA-F]{4})', lambda m: chr(int(m.group(1), 16)), x
    ),
    "qp": lambda x: quopri.decodestring(x.encode("utf-8")).decode("utf-8", errors="replace"),
}

def multi_decode(cadena: str, max_iterations: int = 20) -> list:
    """Intenta decodificar recursivamente hasta llegar a texto plano."""
    history = [("input", cadena)]
    current = cadena

    for _ in range(max_iterations):
        best = None
        best_name = None

        for name, decoder in DECODERS.items():
            try:
                result = decoder(current)
                if result != current and result.isprintable():
                    if best is None or len(result) < len(current):
                        best = result
                        best_name = name
            except Exception:
                continue

        if best is None:
            break

        current = best
        history.append((best_name, current))

    return history

# Uso: doble URL encode de /../../../
encoded = "JTJGJTNFJTNFJTJGJTNFJTJG"
for step, (decoder, result) in enumerate(multi_decode(encoded)):
    print(f"  {step}. [{decoder}] {result}")

Tabla comparativa

#EncodingCaracteresOutput ratioPaddingCTF ★Real ★
1Base64A-Z a-z 0-9 + /4:3=★★★★★★★★★☆
2URL Encode% + hex3:1 (variable)★★★★☆★★★★★
3Hex0-9 A-F2:1★★★★★★★★★☆
4HTML Entities&…;variable★★★☆☆★★★★☆
5Base64URLA-Z a-z 0-9 – _4:3no★★★★☆★★★★★
6ASCII Codes0-255 + sep3:1 apx.★★★☆☆★★★☆☆
7Binary018:1★★★☆☆★★☆☆☆
8Unicode Escape\uXXXX6:1★★★★☆★★★☆☆
9Base32A-Z 2-78:5=★★☆☆☆★★★☆☆
10Quoted-Printable=XXvariable=★★☆☆☆★★★★☆

Conclusión

Estas 10 herramientas de encoding son el equivalente funcional a tener una navaja suiza digital.
Ninguna resuelve un problema complejo por sí sola, pero todas juntas — bien entendidas —
te permiten atravesar el 90% de las barreras de encoding que encuentras tanto en un CTF
de fin de semana como en un incidente real a las 3 de la madrugada.

La próxima vez que veas una cadena críptica, pregúntate:
¿Qué encoding es este y por qué está aquí?

Esa pregunta es el primer paso para convertirte en un mejor analista de seguridad.


Este artículo es el primero de una trilogía. Próximos:

  • Hashing — funciones unidireccionales, identificación y cracking
  • Encriptación — cifrado simétrico y asimétrico en CTFs y en el mundo real