목적
SQL Injection은 단순히 "주입이 된다 / 안 된다"의 문제가 아니다. 단 한 번의 쿼리로 모든 것을 알 수 있다면 좋겠지만, 현실에서 대부분의 SQLi는 부분적인 정보 획득을 위한 수많은 요청의 반복으로 이루어진다. 특히 Blind SQL Injection, Time-based Injection 으로 제한된 환경에서는 더더욱 그렇다.
예를 들어, 직접 출력이 없는 Blind SQL Injection 상황에서 FLAG 값을 하나하나 알아내려면 다음과 같은 방식이 사용된다.
' and substr(flag, 1, 1) = 'a'-- -
Blind SQL Injection에서는 서버의 직접적인 응답 대신 참/거짓에 따른 간접적인 반응(예: 시간 지연, 응답 내용 차이)을 통해 데이터를 추출한다. 이때 흔히 사용되는 방식이, 한 글자씩 가능한 모든 값을 대입해보며 확인하는 브루트포스 방식이다.
예를 들어, 각 문자를 a부터 z까지 26개의 소문자만으로 확인한다고 가정하면, 한 문자당 최대 26번의 요청이 필요하다. 만약 FLAG가 32자라면, 최악의 경우 총 26 × 32 = 832회의 요청이 발생한다.
여기에 대소문자(A–Z, a–z), 숫자(0–9), 특수문자까지 포함해 가능한 문자 집합이 커지면, 요청 수는 그에 비례해 기하급수적으로 증가하게 된다. 예를 들어, 가능한 문자의 수가 62개(A–Za–z0–9)로 늘어나면, 최악의 경우 32 × 62 = 1984회 요청이 필요해진다.
이는 CTF 같은 실습 환경에서는 가능할지 몰라도, 실제 환경에서는 매우 비효율적이며 의심받기 좋은 행동이다.
실제 공격 시나리오에서는 단순히 데이터를 추출하는 것을 넘어, 얼마나 빠르고 조용하게 공격을 수행할 수 있느냐가 관건이다. 이때 요청 수를 최소화하는 전략은 다음과 같은 중요한 목적을 달성하는 데 결정적인 역할을 한다.
1. 시간 단축(속도 최적화)
수백~수천 번의 요청이 필요한 공격은 단순히 오래 걸릴 뿐 아니라, 공격자가 결과를 실시간으로 확인하거나 자동화하는 데에도 큰 부담이 된다. 요청 수를 줄이면 전체 공격 시간을 획기적으로 단축할 수 있다.
2. 방어 로직 우회
대부분의 실제 서비스는 Rage Limiting, WAF, IDS 같은 보안 장치를 운영하고 있다. 공격 요청이 일정 수를 초과하면 IP 차단이나 탐지 알림이 발생할 수 있다. 요청 수를 줄이면 이러한 보안 장치에 감지될 가능성을 낮출 수 있다.
3. 스텔스 공격(은밀한 침투)
공격의 흔적을 최소화하는 것도 중요한 전략 중 하나다. 요청 수가 많을수록 서버 로그, 트래픽 분석, 포렌식 도구 등에 의해 흔적이 남을 확률이 높아진다. 최소한의 요청으로 원하는 정보를 얻는다면 침투 사실을 감지하지 못한 채 공격이 완료될 수도 있다.
이러한 이유로 고급 SQL Injection에서는 단순한 주입 성공만으로 만족하지 않는다. 얼마나 효율적으로 데이터를 추출하고, 얼마나 탐지되지 않고 침투할 수 있는가가 성공을 좌우하게 된다.
최적화 방법
요청 수 줄이기
요청 수를 줄이는 전략은 출력이 제한된 Blind SQL Injection 환경에서 사용된다.
이진 탐색(Binary Search)
문자의 ASCII 값을 기준으로 대소 비교를 반복하면서 값의 범위를 절반씩 줄여가는 방식이다. 대부분 이진 탐색에서 ASCII 코드 범위를 기준으로 중앙값을 설정한다.
문자 | ASCII 코드 |
a | 97 |
z | 122 |
따라서 중간값은 109(m) 또는 110(n)이 사실상 이진 탐색의 기준점으로 적절한 중앙값이라고 볼 수 있다. 당연하게도 문자셋이 커지면 기준도 달라질 것이다.
# substr(flag, 1, 1)이 'm'보다 큰지 확인
' and ascii(substr(flag, 1, 1)) > 109-- -
# True일 경우
# (110 + 122) // 2 = 116
' and ascii(substr(flag, 1, 1)) > 116-- -
# False일 경우
# (97 + 109) // 2 = 103
' and ascii(substr(flag, 1, 1)) > 103-- -
이러한 과정을 반복하여 FLAG 전체를 복원할 수 있다.
단계 | 범위(low, high) | mid | 비교 |
1 | 97 ~ 122 | 109 | 'm'보다 큰가? |
2(True) | 110 ~ 122 | 116 | 't'보다 큰가? |
2(False) | 97 ~ 109 | 103 | 'g'보다 큰가? |
이처럼 이진 탐색은 가능한 문자 범위를 절반씩 좁혀가며, 최대 7번의 비교만으로 하나의 문자를 정확히 추출할 수 있다. 단순한 브루트포스 방식에 비해 요청 수를 획기적으로 줄일 수 있는 매우 효율적인 전략이다.
비트 연산(Bitwise Extraction)
하나의 문자를 이루는 ASCII 값을 비트 단위로 직접 확인하여, 출력 없이도 정보를 정밀하게 복원할 수 있는 고급 기법이다. 문자의 ASCII 값을 얻은 후, 그 값을 비트 마스크로 나눠서 비트 하나하나를 확인할 수 있다.
' AND IF((ORD(SUBSTR(flag,1,1)) & 64) = 64, SLEEP(1), 0)-- -
' AND IF((ORD(SUBSTR(flag,1,1)) & 32) = 32, SLEEP(1), 0)-- -
' AND IF((ORD(SUBSTR(flag,1,1)) & 16) = 16, SLEEP(1), 0)-- -
' AND IF((ORD(SUBSTR(flag,1,1)) & 8) = 8, SLEEP(1), 0)-- -
' AND IF((ORD(SUBSTR(flag,1,1)) & 4) = 4, SLEEP(1), 0)-- -
' AND IF((ORD(SUBSTR(flag,1,1)) & 2) = 2, SLEEP(1), 0)-- -
' AND IF((ORD(SUBSTR(flag,1,1)) & 1) = 1, SLEEP(1), 0)-- -
위 결과를 조합해서 이진수를 정수로 만들고 다시 ASCII 문자로 변환하면 된다.
마스크 | 결과 | 비트 위치 |
64 | True | 1 |
32 | False | 0 |
16 | False | 0 |
8 | False | 0 |
4 | False | 0 |
2 | False | 0 |
1 | True | 1 |
위 예시에서 완성된 이진수는 01000001로 'A'를 의미한다.
이진 탐색과 달리 값이 명확이 0 또는 1로 판별되므로, 해석 실수는 경계 조건 오류 없이 결정적인 결과를 얻을 수 있다는 점에서 안정이 높다.
쿼리 결과 압축 / 병합
여러 개의 정보를 하나의 요청에 묶어서 추출함으로써, 결과적으로 요청 수를 줄이는 전략이다. 하지만 이 방식은 쿼리 결과가 출력되는 환경에서만 사용할 수 있기 때문에, Blind SQL Injection에서 요청 수 자체를 줄이기 위한 이진 탐색이나 비트 연산과는 근본적으로 적용 조건이 다르다.
기본적으로 SQL Injection은 출력이 가능한 경우라면 다음과 같이 정보를 하나씩 추출할 수 있다.
' UNION SELECT NULL, user FROM users LIMIT 0,1-- -
' UNION SELECT NULL, user FROM users LIMIT 1,1-- -
' UNION SELECT NULL, user FROM users LIMIT 2,1-- -
하지만 위 방식으로는 사용자 한 명당 한 번씩 요청이 필요하다.
문자열 연결 연산자를 활용하여 쿼리를 압축하면 한 번의 요청으로 다수의 정보를 획득할 수 있다.
예를 들어, 사용자명과 비밀번호 해시를 각각 따로 추출하면 요청이 두 번 필요하지만, concat과 같은 문자열 연결 함수를 사용하면 다음과 같이 한 번에 추출이 가능하다.
' UNION SELECT NULL, CONCAT(username, ':', password) FROM users-- -
admin:hashedpw
여러 행을 한 줄로 묶고 싶다면 group_concat 등을 함께 사용하여 더욱 효율적으로 압축할 수 있다. 해당 방법으로 1회 요청으로 테이블 전체의 데이터를 추출할 수 있다.
' UNION SELECT NULL, GROUP_CONCAT(CONCAT(username, ':', password) SEPARATOR '|') FROM users-- -
admin:hashedpw1|guest:hashedpw2|test:hashedpw3
출력 기반 SQL Injection에서 공격 효율을 극적으로 높이는 방법이 될 수 있으며, 수백 건의 요청이 필요하던 상황을 단 한 줄의 쿼리로 대체할 수 있다.
이진 탐색이나 비트 연산은 출력 없이도 정보를 추출해야 하는 Blind SQL Injection 상황에서 유효하며, CONCAT(), GROUP_CONCAT() 등의 문자열 연결 함수는 출력 기반 SQL Injection에서 매우 효율적인 정보 추출 수단이 된다. 하지만 어떤 기법이 가장 뛰어난지를 따지는 것보다 중요한 것은, 마주한 환경에서 어떤 전략이 유효한가를 판단하는 것이라고 생각한다. 공격 기법은 상황에 따라 유연하게 조합되어야 하며, 특정 전략에 의존하기보다는 환경에 따라 전략을 선택하고 최적화하는 사고방식을 가질 필요가 있다.
'보안 > 웹' 카테고리의 다른 글
PHAR Deserialization Vulnerability (1) | 2025.04.15 |
---|---|
Dom Clobbering (0) | 2025.03.10 |