Skip to content

eogod/Chainspill

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Chainspill

Chainspill is a deliberately vulnerable attack-defense CTF service for ForcAD-style labs. The theme is a legacy spillway / hydroelectric control panel that stores user "memory chains" — ordered collections of text nodes with public or private visibility.

Warning: Python 2 is long EOL. Run this only inside an isolated CTF lab container. Never expose to the internet or reuse as an application template.


Quick Start

docker compose up --build

Service listens on http://127.0.0.1:8080. SQLite database and export cache live in /app/storage inside the container. The root page (/) serves an interactive Chainspill operator console.

Session token signing uses a hardcoded legacy secret in app/config.py.


API

All authenticated endpoints require Authorization: Bearer <token>.

Method Path Auth Description
GET /health Service liveness check
POST /register Create account: {"username":"…","password":"…"}
POST /login Obtain token: {"username":"…","password":"…"}
POST /chain/create user Create chain: {"title":"…","visibility":"private|public"}
POST /chain/node/add user Add node: {"chain_id":"…","body":"…","visibility":"private|public"}
POST /chain/node/edit user Edit node: {"node_id":"…","body":"…"}
GET /chain/<id> user Read chain and its visible nodes
POST /chain/export user Export chain as pickle, or read a cached file by filename
POST /chain/import user Import chain from base64 pickle
GET /debug/profile?label=<string> debug Profile a chain's private nodes
GET /debug/readahead?chain_id=<id>&rounds=<n> debug Threaded readahead prefetch for a chain's cache file

Checker

No third-party dependencies required.

# Verify service health
python checker/checker.py check 127.0.0.1

# Place a flag (prints JSON state)
python checker/checker.py put 127.0.0.1 <flag_id> FORCAD_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

# Retrieve a flag
python checker/checker.py get 127.0.0.1 '<json state>' FORCAD_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Flags are stored in private nodes owned by a fresh user created per flag round. The flag format is FORCAD_[A-Z0-9]{32}.


Intended Vulnerabilities

The service has multiple intended attack paths with different difficulty levels.

Easy — first-hour patches

1. Hardcoded token secret

LEGACY_TOKEN_SECRET in app/config.py is a well-known static string. Anyone who reads the source can register a normal user, forge that user's token with a debug role, and call debug endpoints. This is the entry point for the harder vulnerabilities below.

Medium difficulty

2. Path traversal in export filename

POST /chain/export with a filename field resolves paths relative to the authenticated user's cache subdirectory, but the boundary check is against the top-level STORAGE_ROOT rather than the user's own directory. A traversal like ../<other_user_id>/<chain>.pkl reaches another user's cached pickle, leaking their private node contents.

3. Pickle import trusts owner_id

POST /chain/import deserialises a base64 pickle and uses the owner_id field from the payload directly. An attacker can craft or replay a leaked pickle while changing owner_id, visibility, and chain_id, making the imported chain readable as their own.

Hard difficulty

4. CVE-2018-1000030-inspired race condition in /debug/readahead

GET /debug/readahead drives unsafe_threaded_readahead(), which stores the current target user, target chain, read offset, and shared cache file object in READAHEAD_STATE without any locking. Concurrent worker threads race while seeking and reading that shared file object, so one session can receive chunks from another user's chain cache file.

5. Python-level pack_string overflow in /debug/profile

GET /debug/profile?label=<string> writes the label into a 48-byte fixed buffer via pack_string_legacy(). The endpoint accepts no chain_id URL parameter — the debug session starts targeting the caller's own most-recent chain. DebugSession stores that label buffer, target_chain_id, and owner_id in one contiguous Python bytearray. The legacy copy loop writes attacker input linearly across the whole frame instead of stopping at the label limit, so A * 48 + <chain_id> overwrites the adjacent target_chain_id field and redirects the profile dump to another chain. This is a safe service-level simulation of the old Python 2 hotshot pack_string class of bug, not native CPython memory corruption.


Defensive Patches

Vulnerability Hint
Hardcoded token secret Replace LEGACY_TOKEN_SECRET with a strong secret outside the source tree and rotate existing tokens
Path traversal Tighten the boundary check in read_constrained_filename to the user's own cache subdirectory
Pickle owner_id trust Force owner_id = request.chainspill_user['id'] in import_chain_object; ignore the payload field
Race condition Protect READAHEAD_STATE with a threading.Lock in legacy.py
pack_string overflow Reject labels longer than PROFILE_LABEL_LIMIT, or store debug fields outside the packed buffer

Lab Notes

  • Target runtime: Python 2.7.14 (python:2.7.14-stretch image).
  • Storage volume: chainspill_storage — wipe with docker compose down -v between rounds during testing.
  • The operator UI at / covers all endpoints and is useful for manual interaction.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors