
문제 설명처럼 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'; 등)를 전부 주석 처리해서 실행되지 않게 함. 그래서 문법 오류 없이 우리가 의도한 부분까지만 실행된다. |
아래와 같은 결과가 나온다.
