문제 설명처럼 sql injection을 이용해야 flag를 획득할 수 있다.

근데 문제 제목 처럼 UNION sql injection을 이용해야 하는 것같다.

문제 파일 속 코드를 살펴보자.

1. app.py

import os
from flask import Flask, request, render_template
from flask_mysqldb import MySQL

app = Flask(__name__)
app.config['MYSQL_HOST'] = os.environ.get('MYSQL_HOST', 'localhost')
app.config['MYSQL_USER'] = os.environ.get('MYSQL_USER', 'user')
app.config['MYSQL_PASSWORD'] = os.environ.get('MYSQL_PASSWORD', 'pass')
app.config['MYSQL_DB'] = os.environ.get('MYSQL_DB', 'secret_db')
mysql = MySQL(app)

@app.route("/", methods = ["GET", "POST"])
def index():

    if request.method == "POST":
        uid = request.form.get('uid', '')
        upw = request.form.get('upw', '')
        if uid and upw:
            cur = mysql.connection.cursor()
            cur.execute(f"SELECT * FROM users WHERE uid='{uid}' and upw='{upw}';")
            data = cur.fetchall()
            if data:
                return render_template("user.html", data=data)

            else: return render_template("index.html", data="Wrong!")

        return render_template("index.html", data="Fill the input box", pre=1)
    return render_template("index.html")

if __name__ == '__main__':
    app.run(host='0.0.0.0')

여기서 우리가 집중해서 봐야할 부분은 입력값에 대한 아무런 검증이 없다는 점이다.

2. init.sql

CREATE DATABASE secret_db;
GRANT ALL PRIVILEGES ON secret_db.* TO 'dbuser'@'localhost' IDENTIFIED BY 'dbpass';

USE `secret_db`;
CREATE TABLE users (
  idx int auto_increment primary key,
  uid varchar(128) not null,
  upw varchar(128) not null,
  descr varchar(128) not null
);

INSERT INTO users (uid, upw, descr) values ('admin', 'apple', 'For admin');
INSERT INTO users (uid, upw, descr) values ('guest', 'melon', 'For guest');
INSERT INTO users (uid, upw, descr) values ('banana', 'test', 'For banana');
FLUSH PRIVILEGES;

CREATE TABLE fake_table_name (
  idx int auto_increment primary key,
  fake_col1 varchar(128) not null,
  fake_col2 varchar(128) not null,
  fake_col3 varchar(128) not null,
  fake_col4 varchar(128) not null
);

INSERT INTO fake_table_name (fake_col1, fake_col2, fake_col3, fake_col4) values ('flag is ', 'DH{sam','ple','flag}');

하지만 여기 있는 테이블 명과 컬럼명은 실제와 다르다.

UNION sql injection을 실행하려면 실제 테이블 명을 알아야 하기에 이와 관련된 공격 먼저 실행한다.


풀이 과정

1. 테이블 명을 찾는다.

공격 페이로드:

uid: ' UNION SELECT 1, table_name, 'x', 'x' FROM information_schema.tables WHERE table_schema=database() #

upw: 공백 제외 아무 값

' 앱이 넣어준 uid='...'에서 앞쪽 따옴표를 닫는 역할. 그래서 uid=''까지만 “uid 값”으로 인식되고, 뒤는 새 쿼리처럼 해석
UNION SELECT 앞의 SELECT * FROM users WHERE uid='' 결과 뒤에, 우리가 만든 SELECT 결과를 이어 붙여서 한 번에 반환하게 만든다. (앞 SELECT는 조건 때문에 0행이라 UNION 결과만 보이게 된다.)
1, table_name, 'x', 'x' 총 4개 컬럼을 만들어서, users 테이블의 컬럼 개수(4개)에 맞춤. 1번 컬럼은 숫자 1, 2번 컬럼은 table_name, 3·4번 컬럼은 문자 'x'.
FROM information_schema.tables MySQL이 관리하는 메타 데이터베이스. 여기에 모든 테이블 이름 정보가 들어 있다.
WHERE table_schema=database() 현재 사용 중인 데이터베이스(예: secret_db)에 속한 테이블만 가져오라는 조건.
# 뒤에 오는 쿼리(' and upw='1'; 등)를 전부 주석 처리해서 실행되지 않게 함. 그래서 문법 오류 없이 우리가 의도한 부분까지만 실행된다.

아래와 같은 결과가 나온다.