0xL4ugh CTF

Manifesto

Web

View on GitHub

Analysis

This problem provides us with the source code for a website about the benefits of the Clojure programming language. As expected, the website is written in Clojure as well.

Solution

The first thing we will notice when entering the website is that it has a login form. Since we have access to the source code, it is unlikely that this is a matter of brute-forcing. We can double-check that on the main function of the core.clj file, where a new admin user is added with a random-uuid as their password.

Getting access

In the code we will find the definition for all the routes that the website accepts under the [(defn routes ...)] line. One of these is the route that matches the index (/) uri. This is where we will find the snippet

;; index route
(re-matches #"/" uri)
(-> (r/response
    (render-file "index.html"
                {:prefer (or (query-params "prefer") (session "prefer") "light")
                :username (session "username")
                :url uri}))
    (assoc :session (merge {"prefer" "light"} session query-params)))

The first couple of lines specify that the template of the file index.html will be rendered using the :prefer, :username, and :url parameters. The next line, however, sets the value of the session map as the result of the merge between {"prefer" "light"}, session, and query-params. This means that whenever we request the index of this website, the session map will be the combination of the existing session parameter and all of the query parameters. This means that we can set any value of the session by simply passing it as a query parameter.

If we request the page as localhost:80?username=admin, we will see that we are not successfully logged in as the admin user.

Some lines later, we will find the following snippet:

;; display user gists, protected for now
(re-matches #"/gists" uri)
(cond (not= (session "username") "admin")
    (json-response {:error "You do not have enough privileges"})

We can see that this snippet tests to see if the current username is the session is the admin before responding to any requests to the gists uri. Given that we have logged in as the admin using our query parameter, we can now navigate to this page.

Injecting Code

The gists/ page allows you to submit snippets of code that will be saved on the website. Of course, they have to be in Clojure to be accepted. Looking at the code, we will find the following snippet that handles POST requests to this uri:

(= request-method :post)
(let [{:strs [gist]} form-params]
    ;; clojure has excellent error handling capabilities
    (try
        (insert-gist (session "username") (read-string gist))
        (r/redirect "/gists")
        (catch Exception e (json-response {:error e}))))

Here we will notice that the method read-string is being called with our input before storing it on the database. According to the documentation :

Reads one object from the string s. Optionally include reader options, as specified in read.
Note that read-string can execute code (controlled by *read-eval*), and as such should be used only with trusted sources.

This description (and many examples on the page) shows that the read-string method is not just parsing the string, but may also interpret it in some cases. More specifically, if the string contains reader macros, they will be interpreted during the method call. Here's a simple example:

=> (read-string "#=(+ 1 1)")
2

This behaviour can be seen if we submit #=(+ 1 1) as our input on the gists/ page. We can now leverage this to look for the flag. While the code itself makes no mention of the flag, the Dockerfile shows us that it is included as an environment vairable for the system. We can call environ.core/env with the name of our variable to retrieve it from the environment. Therefore, something like this should work:

#=(environ.core/env :flag)

Sending that as our input will reload the page and give us the flag:

flag{ISZmNep2qk1pW20n0aWm2PuTOUQviBql}

Last Updated in 2025
Halifax, NS
Canada