본문 바로가기
컴소니/보안

[웹 해킹] Dreamhack simple_sqli(Level 1)

by 금소니 2023. 8. 23.
반응형

#264

 

1. 개요

워게임 명 : simple_sqli

난이도 : Level 1

관련 개념 : Python, DBMS, SQLite, SQL Injection

문제 : SQL Injection을 통한 flag값 획득

SQL Injection이란?

웹 페이지에서 로그인이나 게시글을 조회할 때 값을 입력할 경우 DB 서버에서는 해당 입력 값을 쿼리로 받아들여 명령을 수행합니다.

여기서 공격자가 악의적인 쿼리를 삽입하여 DB 정보들을 추출해내는 공격을 SQL Injection이라고 합니다.

SQL Injection 강의에 포함된 워게임입니다.

2. 소스 코드 확인

1) HTML

1-1) index.html

가장 기본 페이지입니다.

1-2) login.html

로그인과 관련된 페이지입니다.

2) python

2-1) 초기 선언 부분

#!/usr/bin/python3
from flask import Flask, request, render_template, g
import sqlite3
import os
import binascii

app = Flask(__name__)
app.secret_key = os.urandom(32)

try:
    FLAG = open('./flag.txt', 'r').read()
except:
    FLAG = '[**FLAG**]'

2-2)데이터베이스 생성

#데이터베이스 생성
DATABASE = "database.db" #DB명
#데이터베이스가 없을 경우 아래와 같이 생성
if os.path.exists(DATABASE) == False:
    db = sqlite3.connect(DATABASE)
    db.execute('create table users(userid char(100), userpassword char(100));') #users 테이블 생성
    #계정 생성, admin 계정은 랜덤한 값으로 패스워드를 설정하여 생성
    db.execute(f'insert into users(userid, userpassword) values ("guest", "guest"), ("admin", "{binascii.hexlify(os.urandom(16)).decode("utf8")}");')
    db.commit()
    db.close()

2-3) get_db 함수

#DB에 연결하는 함수
def get_db():
    #DB 속성값 불러오기
    db = getattr(g, '_database', None)
    #없을 경우 데이터베이스 연결
    if db is None:
        db = g._database = sqlite3.connect(DATABASE)
    db.row_factory = sqlite3.Row
    return db

2-4) query_db함수

#DB에 쿼리를 질의하는 함수
def query_db(query, one=True):
    cur = get_db().execute(query)
    rv = cur.fetchall()
    cur.close()
    return (rv[0] if rv else None) if one else rv

2-5) close_connection함수

#DB 접속을 해제하는 함수
@app.teardown_appcontext
def close_connection(exception):
    db = getattr(g, '_database', None)
    if db is not None:
        db.close()

2-6) app.route('/')

#기본페이지
@app.route('/')
def index():
    return render_template('index.html')

2-7) app.route('/login', method=['GET', 'POST'])

#로그인 페이지
@app.route('/login', methods=['GET', 'POST'])
def login():
    #GET 메소드 사용
    if request.method == 'GET':
        return render_template('login.html')
    #POST 메소드 사용
    else:
        #ID, 패스워드 입력 받기
        userid = request.form.get('userid')
        userpassword = request.form.get('userpassword')
        #DB에서 ID와 패스워드를 확인하여 일치한 정보 가져오기
        res = query_db(f'select * from users where userid="{userid}" and userpassword="{userpassword}"')
        #일치할 경우
        if res:
            userid = res[0]
            #admin일 경우
            if userid == 'admin':
                #Flag값 반환
                return f'hello {userid} flag is {FLAG}'
            #admin이 아닐 경우
            return f'<script>alert("hello {userid}");history.go(-1);</script>'
        #일치한 정보가 없을 경우
        return '<script>alert("wrong");history.go(-1);</script>'

2-8) 서비스 실행

app.run(host='0.0.0.0', port=8000)

3. 웹 화면 확인

1) 루트 페이지

가장 기본 화면입니다.

심플하게 로그인을 위한 링크만 위치하고 있습니다.

 

2) 로그인 페이지

로그인을 위한 페이지입니다.

아이디, 패스워드를 입력받도록 하고 있습니다.

4.  문제 풀이

1) 요구사항 파악

로그인 페이지가 존재하고 소스 코드를 확인한 결과 admin으로 로그인해야 flag 값을 얻을 수 있습니다.

하지만 admin 계정의 패스워드의 경우 랜덤한 값으로 생성되었기 때문에 알기가 쉽지 않습니다.

따라서 SQL Injection을 통하여 패스워드를 입력하지 않아도 로그인될 수 있도록 해야합니다.

 

2) guest 로그인

소스 코드에 언급되어 있는 guest 계정을 이용하여 먼저 로그인 시도해보도록 하겠습니다.

예상하였던 결과를 확인할 수 있었습니다.

3) SQL Injection을 통한 admin 계정 로그인

먼저 소스 코드를 확인하였을 때 로그인 시 사용되는 쿼리는 아래와 같습니다.

 

SELECT * FROM users WHERE userid="{userid}" AND userpassword="{userpassword}"

 

여기서 userid와 userpassword를 입력받아 사용자를 조회하고 로그인합니다.

현재 이 서비스의 경우 입력값을 체크하고 있지 않아 로그인 시 아이디만 입력하고 주석처리를 하면 SQL Injection 공격을 시도할 수 있습니다.

SQLite에서 사용하는 주석은 "--"입니다.

 

따라서 아래와 같이 쿼리를 입력하면 패스워드에 어떤 값을 입력하여도 주석이 되버리기 때문에 로그인이 가능합니다.

그러면 admin 권한으로 로그인하여 flag 값을 얻을 수 있습니다.

SQL Injection에는 이 밖에도 사용되는 쿼리들이 있습니다.

쿼리 실행 시 참이되도록만 만들면 위와 동일하게 로그인을 우회할 수 있습니다.

반응형

댓글