| name | add-vulhub-env |
| description | Guide for adding new vulnerability reproduction environments to the Vulhub project. Use this skill whenever the user wants to add a new CVE environment, create a vulnerability Docker lab, contribute a new vulnerability to vulhub, write vulnerability documentation for vulhub, or create docker-compose.yml / Dockerfile / README for a vulnerability environment. Also trigger when the user mentions a specific CVE they want to add, asks about vulhub contribution workflow, or wants to set up a reproducible exploit environment. |
Adding a New Vulhub Environment
Vulhub is an open-source collection of pre-built vulnerable Docker environments for security education. Each environment lets users reproduce a real-world vulnerability by running docker compose up -d.
This skill guides you through creating a complete environment from scratch.
Prerequisites
Before starting, verify the vulnerability meets these criteria:
- High impact — high CVSS score or widely exploited in the wild
- Open source — the affected software is freely available
- Docker-friendly — runs in a Linux Docker container (no Windows-only or proprietary firmware)
- Reproducible — the exploit is reliable and deterministic
- Not already covered — check existing directories and
environments.toml
Required Deliverables
Every new environment requires these files:
base/<software>/<version>/Dockerfile # Only if this version doesn't exist
<software>/<CVE-ID>/docker-compose.yml # Required
<software>/<CVE-ID>/README.md # Required (English)
<software>/<CVE-ID>/README.zh-cn.md # Required (Chinese)
<software>/<CVE-ID>/1.png, 2.png, ... # Screenshots (referenced in README, captured by humans later)
environments.toml # Add entry
If the vulnerability is not a CVE, the directory name should be lowercase vulnerability name and the CVE-ID should be replaced with the directory name.
Naming Conventions
| Item | Rule | Example |
|---|
| Software directory | lowercase | grafana, apache-cxf |
| CVE directory | UPPERCASE | CVE-2024-9264 |
| Non-CVE directory | lowercase | admin-ssrf, weak_password |
| File extensions | lowercase | .yml, .md, .png |
| Compose file | must be .yml | docker-compose.yml (NOT .yaml) |
| Branch name | lowercase | add-grafana-cve-2024-9264 |
| Main vuln service in compose | web if it serves HTTP | services.web: (not services.grafana:) |
Step 1: Research the Vulnerability
Gather this information before writing any files:
- Official advisory or CVE description
- Affected software version ranges (both start and end)
- Root cause (e.g., deserialization, injection, path traversal)
- Exploit method and proof-of-concept
- Default credentials (if relevant)
- Network ports the service uses
Step 2: Create Base Dockerfile (if needed)
Check if base/<software>/<version>/ already exists. If it does, skip this step.
If creating a new base image:
FROM <official-upstream-image>:<version>
LABEL maintainer="phithon <root@leavesongs.com>"
RUN set -ex \
&& apt-get update \
&& apt-get install -y --no-install-recommends <packages> \
&& <install vulnerability dependencies> \
&& apt-get purge -y <build-only-packages> \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/*
Guidelines:
- Use official upstream images as the base when available
- Clean up package manager caches (
rm -rf /var/lib/apt/lists/*)
- Prefer Debian-based images, do not choose
-alpine variants when available
- Must pass
hadolint linting (the CI runs this automatically)
Java environments: enable JDWP remote debugging
For any Java-based vulnerability environment, configure the base image so a JDWP agent listens on port 5005, and publish 5005:5005 in the docker-compose.yml. Figure out the right mechanism yourself based on the server and JDK version. This is an infrastructure convenience — do not mention it in the README (see Step 5).
Step 3: Write docker-compose.yml
Keep it minimal. Reference pre-built images from the vulhub/ Docker Hub namespace:
services:
web:
image: vulhub/<software>:<version>
ports:
- "<host-port>:<container-port>"
Rules:
- Do NOT include
version: '2' or any version header — newer environments omit it
- Name the main vulnerable container
web whenever it serves HTTP (most do). This way users can always run docker compose exec web ... no matter which environment they are in, instead of having to look up the service name each time. For non-HTTP main services (e.g., a raw RPC daemon, a database vulnerability), pick a short descriptive name like app, server, or the protocol name (redis, ftp, etc.).
- Only expose ports users need to interact with
- Use default port if possible, do not change the port number
- Add environment variables only if required for the vulnerability
- Java environments: also publish
"5005:5005" for the JDWP debug agent (see Step 2)
- For multi-service setups (e.g., app + database), use
depends_on. Prefer the simple list form (depends_on: - db) over condition: service_healthy unless the vuln container's entrypoint genuinely cannot tolerate the dependency being unready — most images have their own retry loops, in which case adding a healthcheck block to the dependency just for service_healthy is dead weight.
Multi-service example:
services:
web:
image: vulhub/craftcms:5.6.16
depends_on:
- db
ports:
- "8088:80"
db:
image: mysql:8.4
environment:
- MYSQL_ROOT_PASSWORD=root
- MYSQL_DATABASE=craftcms
Step 4: Register in environments.toml
Add an [[environment]] block in alphabetical order among the same software's entries:
[[environment]]
name = "Human-readable Vulnerability Title"
cve = ["CVE-XXXX-XXXXX"]
app = "Software Name"
path = "<software>/<CVE-ID>"
dockerfile = {"vulhub/<software>:<version>" = "base/<software>/<version>"}
tags = ["RCE"]
Field requirements:
| Field | Type | Description |
|---|
name | string | Human-readable vulnerability title |
cve | list | CVE IDs; use [] if no CVE assigned |
app | string | Software name with proper casing (e.g., "Grafana") |
path | string | Relative path with exactly 2 path components |
dockerfile | inline table | Maps each vulhub/* image name to its Dockerfile directory under base/; use {} if no vulhub image |
tags | list | At least one tag from the available tags |
Available tags (defined at the top of environments.toml):
# Vulnerability type
"Auth Bypass", "Backdoor", "Deserialization", "DoS", "Environment Injection",
"Expression Injection", "File Upload", "File Deletion", "Hard Coding",
"Info Disclosure", "Path Traversal", "Privilege Escalation", "RCE",
"SQL Injection", "SSRF", "SSTI", "XXE", "XSS"
# Application type
"CMS", "Database", "Framework", "Message Queue", "Webserver", "LLM"
# Other
"Other"
Step 5: Write Documentation
Create both README.md (English) and README.zh-cn.md (Chinese).
Read references/readme-writing-guide.md in this skill directory for the complete writing guide with templates.
Also read AGENTS.md in the project root for additional style guidance and reference examples of well-written READMEs.
Critical rules summary:
- ALWAYS specify affected version ranges
References: section uses plain text (NOT a ## heading), URLs in <> brackets, max 5 links
- Reproduction steps must be narrative paragraphs (NEVER use numbered lists or bullet points for reproduction steps)
- Use
docker compose up -d (NOT docker-compose up -d)
- English README: add
[中文版本(Chinese version)](README.zh-cn.md) below the title
- Chinese README: do NOT link to English version; do NOT add spaces between Chinese characters and English/numbers
- Reference at least one screenshot in the README — but see "Screenshots" below: you leave the
 placeholders, a human captures the images later
- Prefer safe demonstration payloads (e.g.,
id command output over reverse shells)
- Never mention the JDWP / 5005 debug port in the README. Java environments expose it for research convenience only — it is not part of the vulnerability reproduction and should not appear in user-facing documentation.
Screenshots
Do NOT capture screenshots yourself. LLM-captured screenshots rarely add real value and waste significant time — a human will shoot the images after reviewing the PR. Your task is only to place correctly-positioned  placeholders so the human knows what each number should depict.
Think about where a visual artifact would genuinely help a reader follow the exploit (typical spots: the HTTP response showing id/whoami output rendered by the server, the admin panel reached after auth bypass, /etc/passwd contents returned by a path-traversal/LFI, a SQL injection response with leaked database rows, a successful file-upload page triggering RCE). Put the placeholder at that exact paragraph. 1–3 images usually suffices.
Do NOT create the .png files and do NOT fabricate them (no rendered HTML, no synthetic terminal shots) — leaving the reference dangling is intentional.
For naming, alt-text, and language-specific placement rules, see the Screenshots section of references/readme-writing-guide.md.
Step 6: Test Locally
cd base/<software>/<version>
docker build -t vulhub/<software>:<version> .
cd <software>/CVE-XXXX-XXXXX
docker compose up -d
docker compose down -v
Confirm:
- Container starts without errors
- Vulnerability reproduces exactly as documented
- Exploit output matches README descriptions
- For Java environments: verify JDWP is actually listening with
printf 'JDWP-Handshake' | nc -w 3 127.0.0.1 5005 — the server must echo the same string back.
Step 7: Submit Pull Request
git checkout -b add-<software>-cve-xxxx-xxxxx
git add base/<software>/<version>/Dockerfile \
<software>/CVE-XXXX-XXXXX/ \
environments.toml
git commit -m "Add <Software> CVE-XXXX-XXXXX <vulnerability type> environment"
git push -u origin add-<software>-cve-xxxx-xxxxx
gh pr create --title "Add <Software> CVE-XXXX-XXXXX environment" --base master
CI Checks
Every PR runs these checks automatically:
| Check | What It Validates |
|---|
format-check | LF line endings, hadolint, environments.toml completeness, naming conventions |
markdown-check | Markdown formatting |
Common failures:
- Missing
environments.toml entry for new docker-compose.yml
- Missing or incorrect
dockerfile field in environments.toml entry
- CRLF line endings (all text files must use LF)
- Archive files > 4096 bytes
- Wrong directory casing (software dirs lowercase, CVE dirs UPPERCASE)
- Using
docker-compose.yaml instead of docker-compose.yml
Pre-submission Checklist