Looting Symfony with EOS

Rédigé par Matthieu Barjole - 23/04/2020 - dans Tools , Pentest - Téléchargement
We wrote a new tool that automatically loots all sensitive information from misconfigured Symfony applications. This post describes the type of data it can loot and how. If you just want to use it, check our Github repo!

So let's get started and see what we can grab from the web profiler.

Symfony is a popular PHP web application framework. During a recent assessment, we stumbled upon a Symfony instance deployed in dev mode. In this configuration, Symfony enables a debug component called the web profiler.

This component bundled as symfony/web-profiler-bundle offers multiple features for developers to inspect the application at runtime. For attackers, a bunch of information can be extracted from the profiler: routes, cookies, credentials, files, etc. To loot all this intel, we created the Enemies of Symfony (EOS) tool, named after the popular Friends Of Symfony (FOS) bundle.

So let's get started and see what we can grab from the web profiler.

Note: this tool does not exploit any Symfony vulnerability. The profiler is a useful component for developers and EOS simply takes advantage of misconfigured Symfony applications. In fact, the profiler documentation prominently warns developers:

Never enable the profiler in production environments as it will lead to major security vulnerabilities in your project.

Thanks to all the Symfony team for their awesome work!

Note: Symfony provides a demo application that can be used to test the following elements. A Dockerfile is available on the EOS github repo.


The first component offered by the web profiler is its toolbar. Depending on the Symfony version and its configuration, the toolbar may be displayed by default on every pages, or only when going through the app_dev.php page.

Indeed, in older Symfony versions, applications embed 2 kernels: web/app.php and web/app_dev.php, the former being the default one and the latter being created in dev mode and used when requesting the app_dev.php page. However by default, this page can only be reached from

if (isset($_SERVER['HTTP_CLIENT_IP'])
    || isset($_SERVER['HTTP_X_FORWARDED_FOR'])
    || !(in_array(@$_SERVER['REMOTE_ADDR'], ['', '::1']) || php_sapi_name() === 'cli-server')
) {
    header('HTTP/1.0 403 Forbidden');
    exit('You are not allowed to access this file. Check '.basename(__FILE__).' for more information.');

$kernel = new AppKernel('dev', true);

On recent Symfony versions, the application only creates 1 kernel in public/index.php using the environment defined in its configuration files.

$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);


The toolbar already displays some information such as the Symfony and PHP versions used, their configuration and a link to a phpinfo page. This page may contain valuable information as many Symfony settings are set as environment variables: database credentials, API tokens, the APP_SECRET (more on that later), etc.


Request inspection

The Symfony kernel has a request profiler component (Symfony\Component\HttpKernel\Profiler) that gathers information about resquests and responses, and associate them with a token. The web profiler bundle, as its name implies, provides a web interface to access this data, under the _profiler/{token} path.


The Routing panels shows the route matching logs. The profiler lists each registered route compared against the requested path until one matches. By inspecting a 404 response, we can then retrieve the entire list.



Requests and responses can be inspected individually in the dedicated panel. For looting, we are especially interested in POST requests paramaters. Symfony hides sensitive parameters but their value can be retrieved in the raw request content.



From this panel, one can also retrieve cookies, roles and other session attributes.


Regarding cookies, Symfony offers a Remember Me feature through a dedicated cookie served to the user after a successful authentication. This identifier is derived from the username, his password hash, the user class path and the expiration timestamp. These elements are concatenated and hashed with HMAC-SHA256 with the APP_SECRET as key. Finally, the result is Base64 encoded.

// Symfony/Component/Security/Http/RememberMe/TokenBasedRememberMeServices.php
protected function generateCookieHash(string $class, string $username, int $expires, string $password)
        return hash_hmac('sha256', $class.self::COOKIE_DELIMITER.$username.self::COOKIE_DELIMITER.$expires.self::COOKIE_DELIMITER.$password, $this->getSecret());

EOS provides a command to generate such cookies from the previous parameters. This could be used in scenarios where the attacker is able to retrieve password hashes (through an SQL injection for instance).

$ eos cookies -u 'jane_admin' -H '$2y$13$IMalnQpo7xfZD5FJGbEadOcqyj2mi/NQbQiI8v2wBXfjZ4nwshJlG' -s '67d829bf61dc5f87a73fd814e2c9f629'

Note however that even if commonly used, the Remember Me feature is not enabled by default.

Reading files

From Symfony 3.x, the web profiler allows reading application files under the root directory. This feature is used by developper to quickly identify the source code responsible for handling a request. However, it can be abused to read configuration files.


Extracting source code

While convenient, the file reader feature does not allow directory listing. Thus, one cannot easily extract the whole application source code. However, this is still possible by abusing another Symfony feature: the cache system.

Symfony implements a caching mechanism whose files are saved under the application root at var/cache/%kernel.environment% and therefore can be read from the profiler file reader.

The kernel cache container is particularly interesting as it holds all registered services and their associated class paths.

  <service id="kernel" class="App\\Kernel" public="true" synthetic="true">
    <tag name="routing.route_loader"/>
  <service id="App\\Command\\AddUserCommand" class="App\\Command\\AddUserCommand">
    <tag name="console.command"/>
    <argument type="service" id="doctrine.orm.default_entity_manager"/>
    <argument type="service" id="security.user_password_encoder.generic"/>
    <argument type="service" id="App\\Utils\\Validator"/>
    <argument type="service" id="App\\Repository\\UserRepository"/>
    <call method="setName">

The cache file name is generated from the application configuration and depends on the Symfony version used. Here is an exemple from versions 2.x to 5.x, with src being the source code root directory where the Kernel.php file is, App being the application namespace, Dev the application environment, Debug a constant string if the debug mode is enabled, and the remainder a constant suffix.

| Version   | Cache filename                     |
| 2.0 - 4.1 | srcDevDebugProjectContainer.xml    |
| 4.2 - 4.4 | srcApp_KernelDevDebugContainer.xml |
| 5.0 - 5.x | App_KernelDevDebugContainer.xml    |

Eventually, class paths can be converted to file paths by replacing the namespace root (App) with the root directory (src). This methods works pretty well on the demo application as EOS manages to retrieve the whole application source code.


In the end, EOS has been created to automate the previous operations. It is able to retrieve general information, configuration secrets, user credentials and the application source code.

$ eos scan http://localhost --output results
[+] Starting scan on http://localhost
[+] 2020-04-23 14:21:26.463352 is a great day

[+] Checks
[!] Target found in debug mode

[+] Info
[!]   Symfony 5.0.1
[!]   PHP 7.3.11-1~deb10u1
[!]   Environment: dev

[+] Request logs
[+] Found 9 POST requests
[!] Found the following credentials with a valid session:
[!]   jane_admin: kitten [ROLE_ADMIN]

[+] Phpinfo
[+] Available at http://localhost/_profiler/phpinfo
[+] Found 101 PHP variables
[!] Found the following Symfony variables:
[!]   APP_ENV: dev
[!]   APP_SECRET: 67d829bf61dc5f87a73fd814e2c9f629
[!]   DATABASE_URL: sqlite:///%kernel.project_dir%/data/database.sqlite
[!]   MAILER_URL: null://localhost

[+] Project files
[+] Found: composer.lock, run 'symfony security:check' or submit it at https://security.symfony.com
[!] Found the following files:
[!]   composer.lock
[!]   composer.json
[!]   var/cache/dev/url_matching_routes.php
[!]   var/log/dev.log

[+] Routes
[!] Found the following routes:
[!]   /{_locale}/admin/post/
[!]   /{_locale}/admin/post/
[!]   /{_locale}/admin/post/new
[!]   /{_locale}/admin/post/{id}
[!]   /{_locale}/admin/post/{id}/edit
[!]   /{_locale}/admin/post/{id}/delete
[!]   /{_locale}/blog/
[!]   /{_locale}/blog/rss.xml
[!]   /{_locale}/blog/page/{page}
[!]   /{_locale}/blog/posts/{slug}
[!]   /{_locale}/blog/comment/{postSlug}/new
[!]   /{_locale}/blog/search
[!]   /{_locale}/login
[!]   /{_locale}/logout
[!]   /{_locale}/profile/edit
[!]   /{_locale}/profile/change-password
[!]   /{_locale}

[+] Project sources
[!] Found the following source files:
[!]   src/Command/AddUserCommand.php
[!]   src/Command/DeleteUserCommand.php
[!]   src/Utils/Slugger.php
[!]   src/Utils/Validator.php

[+] Saving files to results
[+] Saved 88 files

[+] Generated tokens: 5894a5 f68efa
[+] Scan completed in 0:00:13


Exposing debug features in a production environment often leads to severe vulnerabilities. The Symfony web profiler component exposes very sensitive information and provides dangerous features that can be abused by attackers to retrieve application files.

The methods used to access this intel are straightforward but EOS can be used to quickly loot everything.