ECW 2018 - Web - Intrusion (5 challenges)

ECW 2018 is a French Jeopardy challenge organized by the PEC (French Pôle d’Excellence Cyber) in partnership with the Bretagne county, Airbus and Thales. Intrusion is a 4 (+1 extra) challenge based realist web scenario. It aims Ruby on Rails and cookie manipulation in order to become admin on the production website.

From 06/10 to 21/10 2018, it will be used as a pre-selection for the final Capture The Flag event, which will start on 21 November 2018 at the the European Cyber Week conference in Rennes.

A very interesting challenge on several points. It allowed me to learn several tricks. Furthermore, I spent a lot of time on it. Let’s go !

Table of Content :

Intrusion 1

Discovery and Information Gathering

The challenge begins with the following statement and a company’s website.

Cette startup est prometteuse!
Ils proposent de collecter de nombreux identifiants dans un seul endroit...
Peut-on s'introduire dans leur system?

An authentication page and a “Work in Progress” page are available. Some tests on the authentication form do not reveal vulnerabilities at this level. Moreover, the indication “support us with cookies:)” on the page clearly indicates that the challenge will be around cookies.

After some research we inspect the files that are used when loading the website.


Development domain and Flag 1

Nothing unusual…. Except in the file “thor.css” where a paragraph seems to have been added manually.


A new sub-domain! It seems to be the application development website… Maybe vulnerabilities to exploit?

We go to the sub-domain and look at the headers….


First flag : check !

Intrusion 2

Comme ça vous êtes arrivé dans un lieu inattendu?
Continuez de creuser!

HTTP request manipulation

We are now on the development domain and we have to find out what to do. A good first thing may be to look at HTTP requests and see if it is possible to modify them. After a few attempts, you notice a strange behavior when you change the HTTP method used.


Thus, using the “OPTIONS” method, a strange error page appears, very verbose… For example, we have access to our decrypted cookie and some server-side variables.


Getting a web console (CVE-2015-3224)

Okay, new information… What can we get out of it? One thing must be obvious to us (well, in real life, it took me some time to notice it…) and it’s the line :
With this, it should be possible to spoof an internal IP and pretend to be an internal network machine!

We intercept a new request in which we add a header X-Forwarded-For: and magic…


A web console ! This could be useful, a console that will allow us to execute commands on the server. I learned afterwards that this is a known vulnerability, the CVE-2015-3224.

Good ! However, it was necessary to manually modify the request in order to obtain this console. For practical reasons, we would like all requests to be modified with the header previously used. No worries ! We configure Burp by specifying the scope ( and telling it to add a header for each request. Once this is done, we can test the well behavior of the system.


Perfect ! Let’s continue.

Search and Flag 2

We now have access to the server, through a kind of shell. What can we do with it? After some research about the Rails console, we discover a way to list the content of a directory:


Now we have to search the server for interesting information… After browsing the directories, you come across a “web_console.rb” file located in one of the configuration directories. This would be the logical starting point given that we are in this console. It is also possible to read the contents of a file.


A lot of information contained in there, including a long hexadecimal string. Once decoded….

466c616732203d20274543577b35393438343632323131643030633963656334363866643139346537366335667d27 ==> "Flag2 = 'ECW{5948462211d00c9cec468fd194e76c5f}'""

Second flag : check !

Intrusion hint

Discorery and Identification

This challenge has been added during the CTF challenge in order to help people. Totally uncorrelated to the scenario environment, we come across a single page with a dialog box.


It seems that we are facing an SQL injection, given the indications. The application’s behavior is tested with correct data.


The indications seem to point towards a SQL Injection using the LIKE condition. Assuming that the query used by the application is of the form :

SELECT <hint> from <table> where <message> LIKE "<entree utilisateur";

An attempt can be made to inject the % character into the query. If there is an injection, the application should answer us with the retrieved lines number.


Exploitation and Results

So we have a functional POC. In order to test, assuming that a flag of the form “ECW{xxx}” is hidden, we can try the following injection E%.


Perfect ! Using that, it will be possible to list all hidden hints through a blind injection, based on the message returned to the user.

Example :

E%   ==> "1 hint found in database"
EC%  ==> "1 hint found in database"
ECA% ==> "O hint found in database"
ECW% ==> "1 hint found in database"

Below, a script to semi-automate the process. Indeed, it is not uncommon to have a request that fails. Since my script does not handle errors, it is necessary to restart it by manually adding strings already retrieved.

import requests
import string
import sys

# Request needed informations
DATA = {
    'request': '',

# Static variables
FALSE_KEYWORD = "0 hint found in database"
INJECTION_USED = "request=E%"

# Password variables
result = ""
tmpResult = ""
charset =  string.printable.replace("%", "").replace("i", "")
#charset = string.ascii_letters + string.digits + ".://{}"

if __name__ == "__main__":
    print("##    ECW 2018 - SQLi   ##")

    print("[+] URL injected : %s" % BASE_URL)
    print("[+] Vulnerable parameter : %s" % VULNERABLE_PARAM)
    print("[+] Injection used : %s" % INJECTION_USED)

    print("\n[+] Starting bruteforce\n")

    for firstLetter in charset:
        result = ""
        DATA['request'] = firstLetter + "%"
        req =, data=DATA)
        if FALSE_KEYWORD not in req.text:
            #result += firstLetter
            result = ""
            #print("FOUND ONE ! letter : %c" % firstLetter)
            for i in range(0,100):
                for letter in charset:
                    DATA['request'] = result + letter + "%"
                    req =, data=DATA)
                    if FALSE_KEYWORD not in req.text:
                        result += letter
                        print("result : %s" % result)

And here is an example of how to recover one hint.


Once all elements have been extracted, we obtain this:

Hint Flag : check !

Intrusion 3

Certaines fois privesc no signifie pas être root...
Ce flag est un point de passage vers la prochaine étape (ECW{<md5>})

Cookies decryption

I first retrieved the link to Github’s gist. This one shows a piece of code used to decrypt some cookies in Ruby. Maybe it’s the same system used in the challenge ?

require 'cgi'
require 'json'
require 'active_support'

def verify_and_decrypt_session_cookie(cookie, secret_key_base)
  cookie = CGI::unescape(cookie)
  salt         = 'encrypted cookie'
  signed_salt  = 'signed encrypted cookie'
  key_generator =, iterations: 1000)
  secret = key_generator.generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len]
  sign_secret = key_generator.generate_key(signed_salt)
  encryptor =, sign_secret, serializer: JSON)

Based on this assumption, several elements are necessary :

  • The encryption salt ;
  • The signature salt ;
  • The key to encrypt/decrypt the cookie.

We must therefore find these elements. To do this, we start again a research phase on the server, starting with the second file recovered in hints : /home/web200/smart_stuff/config/secrets.yml


Good ! It seems we have found some encryption keys. One for the dev domain, and one for something else (useful?). So we are missing salts. After a lot of research, we come across an interesting file.


Two variables encrypted_cookie_salt='ECW-secret-salt' and encrypted_signed_cookie_salt='ECW-signature-secret-salt' are used. Perfect ! We have everything we need to decrypt our cookie. After many tests and attempts to decrypt a cookie locally (through the second script), I tried to process them directly in the web console. It turns out that it works very well with the first script given.


It should also be noted that the script returns by default data formatted in JSON. In our case, however, it is Marshal. It took me some time to find out why the cookie was misread and especially to find out what the Marshal was…

Crafting and connexion to dev domain

We now know what the cookie is made of. You can see a “User” object containing only “nil” (=null) fields. Makes sense since we don’t have a user. But… Would it be possible to impersonate one with fake informations ?

Knowing the different elements used to decrypt the cookie, we should be able to encrypt it again.

The modification of the User object is done like this :

cookie['user']['password'] = "YouLostTheGame"

This way, you can modify all necessary attributes.


Once the cookie has been crafted, you can try to modify your own session cookie by intercepting a request with Burp. Result…


Connected with admin privileges on the development domain ! Functional POC:)

Getting 3rd flag

We now know how to decrypt a cookie, modify it and then encrypt it again in order to use it to connect. But we still don’t have any information about the location of flag 3….

However, we know that the purpose of the challenge is to connect to the production domain and the statement of challenge 3 indicates that the flag is located on a crossing point to the last challenge. From this, we suspect that it is necessary to find the encryption key of the production domain.

Another remaining hint… This is the manual page of “systemctl”. After some research on systemctl, we learnt that “units” can be created for systemd. These “units” behave according to instructions written in a configuration file, located in the /etc/systemd/system/ directory. Why don’t we take a look around this ?

[NOTE] I didn’t saw it because I didn’t solved this challenge before doing the hint, but it turns out that indications about using systemd are also available, especially in the file web_console.rb


There are two files rails-dev.service and rails-prd.service. Nothing very interesting on the dev file, however, on the production configuration file….


The key to production domain ! Which also serves as a flag for the 3rd challenge.

Third flag : check !

Intrusion 4

Le dernier Flag, vous devriez savoir où il est caché désormais!
Mais l'attraper est plus facile à dire qu'à faire ;)

Now we have all necessary elements. First, we decrypt the cookie we retrieve from the production domain. Cookie which seems to be much shorter than the previous ones.


No user for this cookie… But can we add one? The same procedure as the development cookie is reproduced by simply changing the encryption key to secret_key_base = "A_cookie_of_course" and once the cookie is crafted, an attempt is made to inject it by intercepting a request.


Connection and Flag 4

New cookie results…


We are connected as administrator on the production domain ! It is now enough to look for the last flag :). The /admin/console page is unavailable and redirects to the home page. However, when you search the application files, you will find an /admin.


I don’t have the screenshot anymore, but accessing return a page containing the flag !

Flag 4 : check !! (For some reason, the flag was no longer present when I did the challenge again to take screenshots ¯_(ツ)_/¯).

Conclusion and Feedback

A rather interesting and pleasant challenge, despite some negative points, the position of the flags. Indeed, these ones were located in places that may have required a little luck or guessing. Nevertheless, I personally spent a lot of time on this scenario and learned a lot of little things! Congratz' to the organizers and all participants!

ECW 2018 - Web - Troll.JSP Santhacklaus CTF 2018 - Solved Challenges