Dojo challenge #49 Secret Manager solution

Article hero image

The solution and the writeup provided were written by the hunter: garthoid

Description

The Secret Manager application exposes three separate vulnerabilities that chain together into a single exploit: argument injection via filename into cp, path traversal into the vault directory, and finally a specially crafted inject into the BusyBox implementation of grep. None of these alone is sufficient and the exploit only works because all three fire in sequence within a single request. The result of this change is the exposure, deletion, or modification of highly sensitive information.

PoC

Vulnerability 1: Argument Injection via filename into cp

The filename parameter permits us to inject command line options into the cp command. By providing a space separated list of filenames we are able to bypass limited input validation code to inject parameters into the cp command through two steps in the code.

The following code loops through each filename provided and writes the provided content to it.

1for filename in filenames:
2 file_path = os.path.join(UPLOAD_FOLDER, filename)
3 with open(file_path, "w") as f:
4 f.write(content)

If we provide a filename with the name "-r" we are ready for the next step. Note also is that we can also use this code to overwrite any file in the vault, modifying contents, or destroying data. This leads us to mark this as a HIGH integrity CVSS value.

Consider the code fragment below:

1# Backing up the changes to vaults
2os.chdir(UPLOAD_FOLDER)
3os.system(f'cp * {VAULT_FOLDER} 2>/dev/null') # ← shell glob

When you provide a file named -r, the shell expands alphabetically. Since -r sorts before regular filenames, the command inserts "-r" before any filenames the glob () finds. Since "-r" is a recursion flag for the cp command which dumps the contents of internal_secrets/ folder into the VAULT_FOLDER.

We are now ready for the next step.

Vulnerability 2: Path Traversal into Vault Directory

The input validation code on the filename variable is insufficient:

1if filename.startswith('/'): # blocks absolute paths
2elif '\\' in filename or '..' in filename: # blocks traversal

While the code blocks filenames containing .. or starting with /, it does not normalize paths before use. The current checks are effective against simple traversal but does not protect against other dangerous characters (like -- or -). Our attack payload of "vaults/--" completely pass this input validation and a file called "--" gets written into VAULT_FOLDER. (/tmp/uploads/vaults/--). We will see later how "--" is important in the grep.

Vulnerability 3: Busybox grep does not support --exclude-dir

The developer comment provides a clue:

1# We just moved from GNU to BusyBox, our developers are on it.
2 result = os.popen(f'grep -r "{grep}" * --exclude-dir=internal_secrets 2>/dev/null').read()

Upon investigation we learn that GNU grep supports --exclude-dir. BusyBox grep does not. On BusyBox, the flag --exclude-dir=internal_secrets is either silently ignored or treated as an error, meaning the recursive search will crawl into internal_secrets/ which is the very directory that's supposed to be protected.

In our testbed we learn that this command fails silently (2>/dev/null) so it is treated as an error. We need to be able to disable the --exclude-dir option. Fortunately, the "--" indicates "end of options" to grep in both GNU and BusyBox. The "--" can also bypass the limited input validation.

The "*" now expands inside VAULT_FOLDER, which contains our "--"file. Alphabetically "--" sorts first, so the command becomes:

1grep -r "." -- extracted internal_secrets user_secrets.txt z --exclude-dir=internal_secrets

The "." grep parameter means "match any character in regex and so it returns everything, including our flag.

Exploitation

1filenames = z -r vaults/--
2action = search
3grep = .
4content = test

Risk

All three vulnerabilities chain together into a single, unauthenticated, one-shot exploit that completely bypasses all security controls and leaks protected secrets. No authentication, no rate limiting, no brute force needed. The Secret Manager application contains critical vulnerabilities that allow any unauthenticated attacker to fully read and overwrite all protected secret, including those explicitly marked as internal, with a single HTTP request requiring no special tools or expertise.

Remediation

Input validation is one of the most important defences against attacks from untrusted input. Comprehensive input validation protects software from a wide range of attack categories. When combined with output encoding in a web application, input validation can provide a significant barrier to attacks. Instead of checking for .. and /, resolve the full path and verify it stays within the intended directory. For example:

1for filename in filenames:
2 resolved = os.path.realpath(os.path.join(UPLOAD_FOLDER, filename))
3 if not resolved.startswith(UPLOAD_FOLDER + '/'):
4 message = "Error: Invalid filename"
5 error = True
6 break

Next, reconsider invoking system commands directly from code using input from an untrusted zone. This is a significant risk for remote command injection as we see in the ability to manipulate existing code based on our input. Never use globs (*) with user controlled filenames. Instead, consider:

1import shutil
2
3for item in os.listdir(UPLOAD_FOLDER):
4 src = os.path.join(UPLOAD_FOLDER, item)
5 dst = os.path.join(VAULT_FOLDER, item)
6 if os.path.isfile(src): # explicitly skip directories
7 shutil.copy2(src, dst)

Also consider replacing grep shell command with a python equivalent.

1import re
2
3results_list = []
4for root, dirs, files in os.walk(VAULT_FOLDER):
5 # Explicitly skip internal_secrets in Python, not via shell flag
6 dirs[:] = [d for d in dirs if d != 'internal_secrets']
7 for file in files:
8 file_path = os.path.join(root, file)
9 try:
10 with open(file_path, 'r') as f:
11 for line in f:
12 if re.search(grep, line):
13 results_list.append(f"{file_path}: {line.strip()}")
14 except:
15 pass
16result = '\n'.join(results_list)

In general, user input should never reach a shell command.