CVE-2023-1773 is not a single bug. It is a complete exploit chain in RockOA 2.3.2 that combines an unauthenticated callback, weak cryptography, SQL injection, session pollution, and PHP config file writes. This analysis answers the key audit question: why
webmainConfig.phpcan be written and then executed. Keywords: CVE-2023-1773, RockOA, code injection.
The technical specification snapshot provides the attack surface at a glance
| Parameter | Details |
|---|---|
| Product | RockOA / Xinhu OA 2.3.2 |
| Languages | PHP, Python |
| Vulnerability types | Unauthenticated access, weak cryptography, SQL injection, config file code injection |
| Key entry points | webmain/task/api/reimplatAction.php, webmain/system/cog/cogAction.php |
| Key protocols | HTTP/HTTPS, Session, custom POST body encryption/decryption |
| Triggered file | webmain/webmainConfig.php |
| Core dependencies | file_put_contents(), require(), custom jmChajian, database wrapper record() |
| Reference popularity | The original material is a blog-based reproduction and does not provide repository star metrics |
This exploit chain is fundamentally a multi-bug interaction
Many vulnerability databases simplify CVE-2023-1773 as code injection in webmainConfig.php, but that is only the final sink. A more accurate description is this: attacker-controlled data enters the system through an unauthenticated interface, abuses weak cryptography and SQL injection to pollute administrator identity fields, and is ultimately concatenated into a PHP configuration file during system settings persistence and then executed.
This type of chained vulnerability has high audit value because it reveals more than a dangerous function. It exposes the full path from entry point to authentication bypass, data flow, persistence, and final execution.
The vulnerable write sink exists in the system configuration save logic
The core dangerous code appears in savecongAjax() inside webmain/system/cog/cogAction.php:
public function savecongAjax() {
if(getconfig('systype')=='demo') exit('Settings are disabled in demo mode');
if($this->getsession('isadmin')!='1') exit('Non-admin users cannot perform this action');
// Directly concatenates the admin name into PHP configuration source code
$str = '<?php if(!defined(\'HOST\'))die(\'not access\'); //['.$this->adminname.'] at '.$this->now.' used [System → System Tools → System Settings] to save changes to the configuration file return array( '.$str1.' );';
// Writes directly to the configuration file
@$bo = file_put_contents($_confpath, $str);
}
The problem is not file_put_contents() itself. The real issue is that $this->adminname is concatenated directly into a PHP source-code comment context. Once the comment is escaped, the payload can become executable PHP code.
The configuration file path can be reconstructed precisely
The logic that builds $_confpath is:
$_confpath = $this->rock->strformat('?0/?1/?1Config.php', ROOT_PATH, PROJECT);
It resolves to:
ROOT_PATH/PROJECT/PROJECTConfig.php
Under the default deployment, that becomes:
/webroot/webmain/webmainConfig.php
This confirms that the frequently mentioned webmainConfig.php in vulnerability databases is not a false positive. It is the real target file written at the end of the chain.
webmainConfig.php is actively included and executed by the application
File write capability alone is not enough. You must also prove that the file is later loaded as PHP code. That condition is satisfied in config/config.php:
$_confpath = $rock->strformat('?0/?1/?1Config.php', ROOT_PATH, PROJECT);
if(file_exists($_confpath)){
// Loads the configuration file; if its contents were injected, the payload executes
$_tempconf = require($_confpath);
foreach($_tempconf as $_tkey=>$_tvs) $config[$_tkey] = $_tvs;
}
Because both the homepage and the main routing flow include this configuration, any malicious PHP fragment written into the file will execute during later requests to index.php.
AI Visual Insight: This screenshot shows source-code search results that demonstrate index.php or the initialization flow indirectly loads config/config.php, which then requires webmainConfig.php. That step upgrades the issue from a file write to executable inclusion, and it is the key evidence that turns arbitrary content write into code execution.
adminname comes from the session, so the attacker must first solve the pollution problem
In the parent Action class, the login state restoration logic is:
public function getlogin($lx=0) {
$this->adminid = (int)$this->getsession('adminid',0);
$this->adminuser = $this->getsession('adminuser');
$this->adminname = $this->getsession('adminname'); // Critical pollution point
$this->admintoken = $this->getsession('admintoken');
}
This means that if an attacker can influence the administrator record and force the administrator to log in again or refresh the session, $this->adminname becomes attacker-controlled input. The real question then shifts to who can modify the administrator name.
SQL injection appears in the REIM callback interface
The vulnerable logic is located in webmain/task/api/reimplatAction.php:
if($msgtype=='editmobile'){
$user = arrvalue($data, 'user');
$mobile = arrvalue($data, 'mobile');
$where = "`user`='$user'";
$upstr = "`mobile`='$mobile'"; // User input flows directly into SQL
$db->update($upstr, $where);
}
The underlying record() method concatenates SQL directly:
public function record($table,$array,$where='') {
if($addbool){
$sql = "insert into `$table` set $cont";
}else{
$where = $this->getwhere($where);
$sql = "update `$table` set $cont where $where"; // No parameterization
}
return $this->tranbegin($sql);
}
This shows that both mobile and user can flow into raw SQL statements, creating injection in the update query path.
The callback encryption is not secure, and the key degrades to a fixed value when unset
Although the interface decrypts the POST body, the algorithm is custom-built and the key source contains fallback logic:
public function gethkey() {
$key = $this->reimplat_huitoken;
if(isempt($key)) $key = $this->reimplat_secret;
if(isempt($key)) $key = $this->reimplat_cnum;
return md5($key);
}
When all REIM parameters are empty, the result is fixed:
import hashlib
# If REIM parameters are not configured, the final key becomes fixed
fixed_key = hashlib.md5(b"").hexdigest()
print(fixed_key) # d41d8cd98f00b204e9800998ecf8427e
This code verifies the fixed-key degradation issue in the default deployment state.
Unauthenticated access exists because the parent authentication logic was overridden
reimplatClassAction overrides the initialization logic and retains only display-related control, without inheriting the parent token validation flow. As a result, an attacker can access the callback functionality without a valid authenticated session.
AI Visual Insight: This image shows the official CVE record page and reflects how external intelligence sources initially classified the issue mainly as config file code injection. From an audit perspective, its technical value is in highlighting the gap between the official summary and the real source-level exploit chain.
AI Visual Insight: This screenshot shows how a third-party vulnerability database reused the same description, illustrating that industry intelligence often focuses on the final filename while ignoring the prerequisite exploitation conditions. For researchers, this is a reminder to trace the real entry point and authorization boundary in source code.
AI Visual Insight: This image further shows the homogeneous reuse of vulnerability descriptions across external databases, with no layered explanation of the REIM callback, default key, SQL injection, and session pollution. That information gap is exactly what source-code auditing must close.
The exploitation path can be abstracted into a four-step closed loop
First, call the unauthenticated reimplat callback interface.
Second, use the fixed key to build a decryptable POST body.
Third, abuse editmobile injection to modify the administrator name field.
Fourth, trigger savecongAjax() as an administrator so the malicious name is written into webmainConfig.php and then executed through require().
Route reachability makes exploitation practical and reliable
Based on the main routing rules, the entry URL can be resolved to the target action:
$m = $rock->get('m', $m);
$a = $rock->get('a', $a);
$d = $rock->get('d', $d);
// Build the Action file path and invoke the method
$actfile = $rock->strformat('?0/?1Action.php', $actpath, $m);
$clsname = ''.$m.'ClassAction';
$actname = ($ajaxbool == 'true') ? ''.$a.'Ajax' : ''.$a.'Action';
Therefore, ?m=reimplat|api&a=index&d=task can reach webmain/task/api/reimplatAction.php, while ?a=savecong&m=cog&d=system&ajaxbool=true can trigger the configuration save logic.
This proves the vulnerability is not a theoretical path. It exposes a real attack surface with explicit URL-to-handler mapping.
From an audit perspective, the three root causes are the most important takeaways
Root cause one is that the authentication boundary was broken by business-layer overrides
The parent class contains token validation, but the child class override drops that validation. This is a classic object-oriented security regression. During auditing, any location where initAction(), beforeAction(), or construct() is overridden by a child class should receive priority review.
Root cause two is that custom encryption hides a fixed-key risk
The system does not use a mature cryptographic protocol. Instead, it implements its own character shifting and Base64 variant. Even worse, the key can degrade into a public constant, which means the so-called encrypted interface is effectively just a predictably encoded interface.
Root cause three is that the configuration generation logic inserts user data into PHP source code
Writing user-controlled fields into a PHP configuration file fundamentally mixes business data with executable code boundaries. Even when the content appears to be inside a comment, missing strict escaping can still turn it into code injection.
AI Visual Insight: This image shows an encrypted POST body being sent to the callback interface in Burp, demonstrating that an attacker can interact directly with the backend protocol without a legitimate frontend context. The technical focus is that the raw body is attacker-controlled and encoded according to the server’s custom algorithm.
AI Visual Insight: This screenshot shows the request effect after triggering savecongAjax(). Its technical significance is that it confirms the malicious adminname has reached the configuration write path. This is the critical transition from database field pollution to filesystem persistence.
AI Visual Insight: This image shows the execution result after revisiting the homepage, verifying that the malicious PHP payload takes effect when webmainConfig.php is included. It proves that the final step in the chain is not just successful file write, but successful execution in the runtime context.
Effective remediation must cover the entire chain instead of patching a single sink
- Restore and enforce centralized token validation, and prevent child classes from bypassing parent security initialization.
- Replace all database updates with parameterized queries and remove string-concatenation-based
record()logic entirely. - Never write PHP source code into configuration files; use JSON, INI, or database-backed storage instead.
- Apply strict encoding and context-aware escaping before writing user fields into logs or comments.
- Retire the custom encryption algorithm and replace it with standard signing and authentication mechanisms.
def secure_principles():
controls = [
"Centralized authentication middleware", # Prevent child-class overrides from bypassing authentication
"Parameterized SQL", # Block injection from reaching the database layer
"Separation of configuration data and code", # Avoid writing into PHP source code
"Standard signature validation" # Replace predictable custom encryption
]
return controls
This code summarizes the four most important remediation controls for this exploit chain.
FAQ provides the most important clarifications
Q1: Why is the CVE described as config file code injection when it also involves unauthenticated access and SQL injection?
Because webmainConfig.php is only the final execution point. The real cause is the combined effect of an unauthenticated callback, a fixed key, custom decryption, SQL injection, and administrator name pollution.
Q2: If file_put_contents() is removed, does the problem disappear completely?
No. Removing that write sink alone is not enough. Unauthenticated access, weak key derivation, and SQL injection would still exist, and an attacker could still search for another dangerous sink. The full chain must be fixed as a whole.
Q3: What is the most effective entry point for auditing legacy PHP systems like this?
Start by tracing entry routing, parent-child authentication overrides, custom encryption and decryption, the database abstraction layer, and configuration generation logic. These five components are the most likely to form cross-layer exploit chains.
The core summary reconstructs the real cause of CVE-2023-1773
This article reconstructs the real root cause of CVE-2023-1773 in RockOA 2.3.2: an unauthenticated REIM callback, default key degradation, SQL injection, session pollution, and configuration file writes combine into a code execution chain. It also provides the key source-code paths and the main lessons for security auditing.