Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a2f71a6
v8-exploitation: builder, tests, solves, and writeups for the origina…
zardus Feb 2, 2026
0b50997
intermediate levels and capstones to bridge learning gaps
zardus Feb 8, 2026
fc3ae27
v8-exploitation: add maglev-warmup compilation observation lab
claude Feb 15, 2026
5692f50
v8-exploitation: add maglev-1 incomplete object initialization challenge
claude Feb 15, 2026
6dd3de6
v8-exploitation: add maglev-2 allocation folding proof lab
claude Feb 15, 2026
35283be
v8-exploitation: add maglev-3, cve-2024-0517, cve-2024-0517-real, cve…
claude Feb 15, 2026
a23a882
v8-exploitation: add independent gap analysis (v2)
claude Feb 16, 2026
42b9d39
cleanup
zardus Mar 27, 2026
d3a99c7
plan
zardus Mar 27, 2026
9221704
Stage curriculum manifest scaffold moves
zardus Mar 28, 2026
b6e7a4d
Refresh level-6f GC relocation tests
zardus Mar 28, 2026
2a83419
Fix level-6f async writeup title
zardus Mar 28, 2026
cd5fa49
Rework typer-warmup as OOB proof lab
zardus Mar 28, 2026
8db1b09
Rework typer-oob-cage for sandboxed heap R/W
zardus Mar 28, 2026
6c8ccdc
Implement typer-mismatch-cage challenge
zardus Mar 28, 2026
f695f97
Fix common template resolution for typer mismatch cage
zardus Mar 28, 2026
5402162
Remove orphaned v8 exploitation levels
zardus Mar 28, 2026
6fe7d5c
Reorder v8 exploitation module sections
zardus Mar 28, 2026
bc33974
Squash V8 updates after module reorder
zardus Jun 19, 2026
92db8e1
Move modern V8 chains to practice
zardus Jun 19, 2026
a246296
Add V8 learning-2 triangle ramp
zardus Jun 19, 2026
c07c27b
Revamp V8 learning curricula
zardus Jun 20, 2026
41abfa9
stuff?
zardus Aug 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
4 changes: 0 additions & 4 deletions .agents/skills/authoring-challenges/agents/openai.yaml

This file was deleted.

1 change: 0 additions & 1 deletion .claude

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ your archetype.
Disable nondeterminism that would break the intended technique (e.g. ASLR off for a
fixed-address lesson). What the user objected to was a solve whose *target value was
random* and whose *flag came through a side channel*, not randomness as such.
- **Never disable ASLR to make a capstone or full exploit chain pass.** ASLR-off is only
acceptable when fixed addresses are the explicit lesson of that level. A capstone must
derive live process-image, libc, stack, and control-flow targets through the intended
leak or primitive. If a copied practice solve hardcodes values like `0x555555...` for
PIE or `0x7fffffff...` for stack, treat the challenge as invalid until the solve is
rewritten and `pwnshop test` passes with ASLR enabled. Do not relabel that as
"determinism"; it is hiding a missing exploit stage.
- **Randomness is legitimate and often desirable** — as long as it's *not a coin-flip in
the solve path*: seeded build-time secrets (endpoint/parameter/table names via
`random_names.j2`, admin passwords), and per-instance/per-run secret *values the
Expand Down Expand Up @@ -199,6 +206,8 @@ your archetype.
3. Made an unverified factual claim in learner text or to the user.
4. Made the solve nondeterministic, or gated the flag behind a guess / needless side channel.
Or assumed a fixed flag length (hardcoded a byte count / too-small pad capacity) — flags vary.
5. Crammed more than one concept into a level, or mismatched the module's voice.
6. Didn't consult siblings / `runtime/` before building or before giving up.
7. Claimed success (tests, push) without verifying, or attributed a change to the user.
5. Disabled ASLR for a capstone/full chain, or copied a fixed-address practice exploit
instead of deriving live ASLR anchors through the intended primitive.
6. Crammed more than one concept into a level, or mismatched the module's voice.
7. Didn't consult siblings / `runtime/` before building or before giving up.
8. Claimed success (tests, push) without verifying, or attributed a change to the user.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Interpreted "checker" challenges (asm/ELF graded by a Python checker)

One of the four archetypes (see `SKILL.md` §7). This is the dominant pattern in
`computing-101` (control-flow, memory, the-stack, bitwise-operations, numbers-as-strings). The learner
`computing-101` (assembly-crash-course, control-flow, memory, the-stack). The learner
submits assembly / an ELF / a `.so`; a SUID Python **checker** statically and/or
dynamically validates it and grants the flag. Reuse the shared harness — don't hand-roll.

Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.

This file was deleted.

Binary file not shown.
Binary file not shown.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
�^�r{��4�@��sަh����tr� .QNh)mAr�ː��Skhh0��B��2R����i�&3F�D�k�*t�֘z����������u\�bh�_ ��  ��@,�l;�xĪ���X�|��h��A*�k�m���Q�6
�N��o��?�?Uq[����#٘u�E�1���1\�m֬N��I�:�H6,Y���֗�hg�'���̱���3����4����آ�wE�� <qCʪɏ�q>���;�N�l^�A8
+J+���f� �2F���߁P��sm)�g����M�U�N௜7k�Q�v�
Binary file not shown.
Binary file not shown.
26 changes: 12 additions & 14 deletions .github/actions/determine-modified-challenges/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,29 +44,27 @@ runs:
with:
fetch-depth: 0

- uses: ./.github/actions/setup-nix

- name: List modified challenges
id: list-challenges
shell: 'nix shell nixpkgs#uv nixpkgs#git --command bash --noprofile --norc -euo pipefail {0}'
env:
MODIFIED_SINCE: ${{ steps.resolve-ref.outputs.ref }}
run: |
challenges_file="$RUNNER_TEMP/modified-challenges.txt"
./pwnshop list ./challenges --modified-since="$MODIFIED_SINCE" > "$challenges_file"
echo "challenges_file=$challenges_file" >> "$GITHUB_OUTPUT"
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
cache-dependency-glob: tools/pwnshop/**

- uses: actions/github-script@v8
id: resolve-challenges
env:
CHALLENGE_FILTER: ${{ inputs.challenge_filter }}
CHALLENGES_FILE: ${{ steps.list-challenges.outputs.challenges_file }}
MODIFIED_SINCE: ${{ steps.resolve-ref.outputs.ref }}
with:
script: |
const fs = require("fs");
const childProcess = require("child_process");
const path = require("path");
const ref = process.env.MODIFIED_SINCE || "";
const challengeFilter = process.env.CHALLENGE_FILTER.trim() || "**";
const output = fs.readFileSync(process.env.CHALLENGES_FILE, "utf8").trim();
const output = childProcess.execFileSync("./pwnshop", ["list", "./challenges", `--modified-since=${ref}`], {
stdio: ["ignore", "pipe", "inherit"],
})
.toString()
.trim();
const listedChallenges = output.split("\n").map((line) => line.trim()).filter(Boolean);
const groupChallenge = (challenge) => {
const parts = challenge.split("/");
Expand Down
4 changes: 2 additions & 2 deletions .github/actions/test-challenges/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ runs:
MODIFIED_SINCE: ${{ inputs.modified_since_ref }}
run: |
echo "::group::Challenges to be tested"
pwnshop list "$CHALLENGES_DIR" --modified-since="$MODIFIED_SINCE"
./pwnshop list "$CHALLENGES_DIR" --modified-since="$MODIFIED_SINCE"
echo "::endgroup::"

echo "::group::Testing challenges"
pwnshop test \
./pwnshop test \
--modified-since="$MODIFIED_SINCE" \
--jobs "$(( $(nproc) * 2 ))" \
--attempts 5 \
Expand Down
1 change: 1 addition & 0 deletions .github/pwnshop-ci-baselines/v8-exploitation
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1e9e6c450b1b8b00ca9f1dd7e7d870c58d5fdee2
5 changes: 3 additions & 2 deletions .github/workflows/check-encryption.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/setup-nix

- name: Install git-crypt
run: sudo apt-get install -y git-crypt

- name: Ensure tests_private files are encrypted
shell: 'nix shell nixpkgs#git nixpkgs#git-crypt --command bash --noprofile --norc -euo pipefail {0}'
run: |
unencrypted=$(git crypt status | grep tests_private | grep "^not" || true)
if [ -n "$unencrypted" ]; then
Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/check-maintainers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
- uses: ./.github/actions/setup-nix

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
cache-dependency-glob: tools/maintainers/check-maintainers

- name: Check Maintainers
shell: 'nix shell nixpkgs#uv nixpkgs#gnupg --command bash --noprofile --norc -euo pipefail {0}'
run: tools/maintainers/check-maintainers ./maintainers.yml
37 changes: 16 additions & 21 deletions .github/workflows/publish-challenges.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,21 +43,35 @@ jobs:
- uses: ./.github/actions/setup-nix
- uses: ./.github/actions/setup-challenge-runtime

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
cache-dependency-glob: tools/dojo/**

- name: Test challenges
uses: ./.github/actions/test-challenges
with:
challenges: ${{ matrix.challenges }}
modified_since_ref: ""
gpg_private_key: ${{ secrets.GPG_ROOT_PRIVATE_KEY }}

- name: Install rclone
run: sudo apt-get update && sudo apt-get install -y rclone

- uses: ./.github/actions/read-registry-config
id: registry-config

- name: Prepare challenge registry artifacts
id: prepare-registry-artifacts
- name: Publish challenges
shell: 'nix develop -c bash -euo pipefail {0}'
env:
CHALLENGES_DIR: challenges/${{ matrix.challenges }}
R2_BUCKET_NAME: ${{ steps.registry-config.outputs.bucket_name }}
REGISTRY_SECRET: ${{ secrets.REGISTRY_SECRET }}
RCLONE_CONFIG_R2_TYPE: s3
RCLONE_CONFIG_R2_PROVIDER: Cloudflare
RCLONE_CONFIG_R2_ENDPOINT: https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com
RCLONE_CONFIG_R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY }}
RCLONE_CONFIG_R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_KEY }}
run: |
containerd_blobs_dir="/mnt/pwn.college/containerd/io.containerd.content.v1.content/blobs"
publish_blobs_dir="/mnt/publish/blobs"
Expand Down Expand Up @@ -87,35 +101,16 @@ jobs:
docker image ls --filter "label=pwncollege.challenge" --no-trunc --quiet \
| xargs -r docker image inspect --format '{{.Id}} {{index .Config.Labels "pwncollege.challenge"}}'
)
{
echo "publish_blobs_dir=$publish_blobs_dir"
echo "publish_manifests_dir=$publish_manifests_dir"
} >> "$GITHUB_OUTPUT"

- name: Publish challenge registry artifacts
shell: 'nix shell nixpkgs#rclone --command bash --noprofile --norc -euo pipefail {0}'
env:
R2_BUCKET_NAME: ${{ steps.registry-config.outputs.bucket_name }}
RCLONE_CONFIG_R2_TYPE: s3
RCLONE_CONFIG_R2_PROVIDER: Cloudflare
RCLONE_CONFIG_R2_ENDPOINT: https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com
RCLONE_CONFIG_R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY }}
RCLONE_CONFIG_R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_KEY }}
run: |
publish_blobs_dir="${{ steps.prepare-registry-artifacts.outputs.publish_blobs_dir }}"
publish_manifests_dir="${{ steps.prepare-registry-artifacts.outputs.publish_manifests_dir }}"

echo "::group::rclone blobs"
rclone copy "$publish_blobs_dir" "r2:${R2_BUCKET_NAME}/blobs" --ignore-existing --config /dev/null --stats-one-line --stats-log-level NOTICE
echo "::endgroup::"

echo "::group::rclone manifests"
rclone copy "$publish_manifests_dir" "r2:${R2_BUCKET_NAME}/manifests" --config /dev/null --stats-one-line --stats-log-level NOTICE
echo "::endgroup::"

- name: Update dojo
if: ${{ vars.DOJO_URL != '' && hashFiles(format('challenges/{0}/dojo.yml', matrix.challenges)) != '' }}
shell: 'nix shell nixpkgs#uv --command bash --noprofile --norc -euo pipefail {0}'
env:
CHALLENGES_DIR: challenges/${{ matrix.challenges }}
DOJO_URL: ${{ vars.DOJO_URL }}
Expand Down
13 changes: 10 additions & 3 deletions .github/workflows/sync-maintainers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,16 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
- uses: ./.github/actions/setup-nix

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
cache-dependency-glob: tools/maintainers/sync-maintainers

- name: Install git crypt
run: sudo apt-get update && sudo apt-get install -y git-crypt

- name: Unlock git-crypt
shell: 'nix shell nixpkgs#git nixpkgs#git-crypt nixpkgs#gnupg --command bash --noprofile --norc -euo pipefail {0}'
env:
GPG_ROOT_PRIVATE_KEY: ${{ secrets.GPG_ROOT_PRIVATE_KEY }}
run: |
Expand All @@ -34,8 +40,9 @@ jobs:
git crypt unlock

- name: Sync maintainers
shell: 'nix shell nixpkgs#uv nixpkgs#git nixpkgs#git-crypt nixpkgs#gnupg --command bash --noprofile --norc -euo pipefail {0}'
run: |
set -eo pipefail

git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

Expand Down
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
__pycache__/
casts/
.discord-feedback/
tools/pwnshop/src/pwnshop.egg-info
35 changes: 6 additions & 29 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ per archetype (templated web/service, interpreted checker, compiled SUID binary,
path. Grant the flag only on a real solve, via the archetype's normal mechanism (SUID
binary prints it; checker `give_flag`; web app reveals on exploit; `submit-number` for
read-a-value levels). Don't add wrappers/extra binaries/indirection the spec doesn't need.
**Never disable ASLR to make a capstone/full exploit chain pass** — ASLR-off is only for
levels whose explicit lesson is fixed-address mechanics. Capstone solves must derive live
process-image/libc/stack/control-flow targets through the intended primitive; copied
practice exploits with hardcoded `0x555555...` PIE or `0x7fffffff...` stack addresses are
invalid until rewritten and tested with ASLR enabled.
**Never assume the flag's length** — it's a variable-length implementation detail (~57
bytes today, not a contract). Size buffers/byte-counts dynamically from the real `/flag`,
or pad to a fixed capacity **≥ 128 bytes** kept in sync across every coupled file; an
Expand All @@ -68,10 +73,7 @@ per archetype (templated web/service, interpreted checker, compiled SUID binary,
qualify a pass with its environment.
- **Git/comms:** on an unpushed WIP branch just amend obvious fixes (no permission menus);
state persistence precisely (working-tree vs committed vs pushed); never assert an
uncommitted change is "the user's" without evidence. **Before every commit run the
secret-test encryption hook** (`tools/git-hooks/pre-commit`, see below) — or install it
once so `git commit` runs it for you — so an unencrypted `tests_private` solution never
lands in history.
uncommitted change is "the user's" without evidence.

## Key Commands

Expand Down Expand Up @@ -118,31 +120,6 @@ pwnshop run --user 0 --volume /tmp/debug challenges/web-security/path-traversal-

DO NOT run these scripts without pwnshop: the dependencies are not installed in the host, and some of these challenges do permanent damage to their environment.

### Secret-Test Encryption (pre-commit hook)

Everything under `tests_private/` is the solution and **must** be git-crypt encrypted in
every commit (each `challenges/MODULE_ID/.gitattributes` routes `tests_private/` through a
per-module `git-crypt` filter). A plaintext `tests_private` file leaks the flag-grade solve
into history.

`tools/git-hooks/pre-commit` enforces this against the **staged index**. Entering `nix
develop` installs it automatically (idempotent, and it won't clobber a pre-existing hook), so
`git commit` runs it for you. To install by hand, or to run it on demand:

```bash
# install once per clone (uses Git's resolved hooks dir, so worktrees work too)
ln -sf ../../tools/git-hooks/pre-commit "$(git rev-parse --git-path hooks)/pre-commit"

# or run on demand, after `git add` and before `git commit`
tools/git-hooks/pre-commit
```

It blocks the commit when a staged file is unencrypted but either (a) `.gitattributes` routes
it through a `git-crypt` filter, or (b) lives under `tests_private/` (so a missing or
misconfigured module `.gitattributes` can't slip plaintext through). On a hit, `git-crypt
unlock` and re-`git add` the file so the clean filter encrypts it, then commit. This catches
the leak locally, before the `check-encryption` CI gate would reject it.

## Architecture

### Directory Structure
Expand Down
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,7 @@ Primary entry points:
- `./pwnshop test` — render, build, and run all `test*/test_*` files inside the challenge.
- `./pwnshop run` — render, build, and drop into an interactive shell inside the challenge container (use `--user=<uid>` to control the interactive user, default `1000`, `--volume <path>` to mount host paths read-only, or append a command after the challenge to run it instead of `/bin/bash`).

Any future automation that renders, builds, runs, or tests challenges should shell out to `./pwnshop ...` rather than reimplementing those pieces of the workflow.

The standalone `discord-feedback` helper is available in `nix develop` for Discord-derived curriculum feedback automation.
Any future automation (GitHub Actions, local scripts, etc.) should shell out to `./pwnshop ...` rather than reimplementing pieces of the workflow.

# Building and testing

Expand Down
3 changes: 0 additions & 3 deletions challenges/computing-101/DESCRIPTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,3 @@ This dojo is a work in progress *and* a community effort!
If you are interested in contributing, please make your way over to [github](https://github.com/pwncollege/computing-101)!
If you have questions, comments, feedback, and so on, join us on [the Discord channel](https://discord.com/channels/750635557666816031/1298046190444609666).

**NOTE:**
If you are looking for the old Assembly Crash Course, it has [been archived](https://pwn.college/archive/assembly-crash-course).
Computing 101 is the current path through that material, with the concepts split across the modules below.
5 changes: 2 additions & 3 deletions challenges/computing-101/assembly-assortment/DESCRIPTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@ You have a working knowledge of assembly from your journey thus far.
Let's broaden it!

This module will explore the effects of a number of different assembly instructions, teaching you to recognize them and not panic in their presence.
In most of these challenges, you'll reverse-engineer a binary to understand an instruction's effect on data and feed it input it will accept.
In a few others, you'll put an instruction to work yourself, writing a small function around it.
Either way, if you understand what the instruction does, you will get flags!
Generally, each challenge will force you to understand its effect on data and generate data that it will accept.
If you can do this, you will get flags!

This file was deleted.

Loading