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

부재중입니다·2024년 3월 15일

가상화란?

하드웨어를 소프트웨어(파일)로 표현한 것이다.

버츄얼머신 setting 화면

  • Bridged: 가상 머신이 물리적 네트워크에 직접 연결되는 것처럼 설정한다. 이 옵션을 사용하면 가상 머신이 네트워크의 다른 장치들과 동등한 위치에서 통신할 수 있게 된다.

  • NAT (Network Address Translation): 가상 머신이 호스트 컴퓨터의 IP 주소를 공유해서 인터넷에 접속한다. 애플리케이션처럼 네트워크에 연결한다. 호스트 머신이 가상 머신으로부터 오는 트래픽을 인터넷으로 전달해주며, 인터넷으로부터 오는 트래픽을 가상 머신으로 전달해준다.

Host-only: 가상 머신이 호스트 컴퓨터와만 통신할 수 있는 개인 네트워크를 만든다. 이 옵션은 가상 머신이 외부 네트워크와 분리되어 있길 원할 때 유용하다.

Custom: 사용자가 정의한 특정 가상 네트워크에 가상 머신을 연결한다. 이를 통해 특정 네트워킹 시나리오를 구성할 수 있다.

WebGoat 실행

작업관리자(Ctrl+Alt+Del 또는 상태바에서 마우스 오른쪽 클릭해서 선택)를 실행해서 MySQL이 있는 경우 작업 끝내기

C:\FullstackLAB\run.bat 실행

이클립스에서 Tomcat 서버를 실행

  1. JavaEE 퍼스펙티브로 전환 (옵션)
  2. 퍼스펙티브를 초기화 (옵션)
  3. Servers 탭을 클릭 ⇒ 등록된 서버를 확인
  4. Tomcat를 선택 ⇒ 실행할 서버를 선택
  5. 디버거 모드 또는 실행 모드로 서버를 실행

서버 실행을 확인

만약, 아래와 같이 오류가 발생하는 경우 ⇒ 프로젝트 재빌드 후 배포


서비스 포트를 확인 ⇒ 서비스 포트가 충돌 나는 경우, 아래 정보를 수정 후 저장하고 서버를 재기동

http://localhost:8080/WebGoat 접속해서 동작을 확인 ⇒ 사용자 이름 / 비밀번호 : webgoat / webgoat

404에러가 떠도 당황하지 말자.

http://localhost:8080/WebGoat/attack 주소로 접속

Kali 가상머신에서 WebGoat으로 접속

http://host.pc:8080/WebGoat

로그인 후 404 오류가 나오는 경우

이클립스에서 WebGoat > src > main > webapp 에서 오른쪽 클릭해서 New > HTML File을 선택해서 index.html 파일을 추가



index.html 파일의 위치를 확인

다음과 같이 저장

Kali 가상머신에 프록시를 설정

Burpsuite 실행

프록시 설정을 확인 ⇒ Proxy > Proxy settings > Proxy listener가 체크되어 있는지 확인

Firefox 브라우저에 프록시 사용 설정



BurpSuite에서 Interceptor를 설정한 상태에서 Firefox를 이용해 WebGoat 사이트(http://host.pc:8080/WebGoat)로 요청


요청을 인터셉터해서 어떻게 처리할 건지를 물어봄

Forward를 클릭
Forward를 클릭

HTTP History 탭을 통해 요청/응답 과정을 확인 (interceptor는 해제한 상태로 진행)

http//host.pc:8080/WebGoat 으로 요청했을 때 리다이렉터 응답(http://host.pc:8080/WebGoat/로 재요청)을 받음

http//host.pc:8080/WebGoat/ 으로 요청 ⇒ 디렉터리 리스팅 옵션 확인 후 기본 페이지를 검색 ⇒ 기본 페이지(index.html)를 응답으로 반환 ⇒ index.html 파일 내용에 attack 페이지로 재요청이 포함되어 있음

http://host.pc:8080/WebGoat/attack 페이지를 요청 ⇒ attack 파일의 내용(또는 실행 결과)을 응답으로 반환

입력데이터 검증 및 표현

프로그램 입력값에 대한 검증 누락 또는 부적절한 검증, 데이터의 잘못된 형식지정, 일관되지 않은 언어셋 사용 등으로 인해 발생되는 보안약점으로 SQL 삽입, 크로스사이트 스크립트(XSS) 등의 공격을 유발할 수 있다.

왜 입력값 검증이 필요한가요?

입력값 검증은 정보 시스템의 안전하고 예상된 방식으로의 동작을 보장하기 위해 필수적입니다. 이 과정은 크게 세 단계로 이루어집니다: 입력 → 처리 → 출력.

  1. 입력: 시스템으로 들어오는 정보이다.
  • 신뢰할 수 있는 입력: 안전한 출처, 예를 들어 시스템 내부에서 생성된 값들로부터 온 입력이다.
  • 신뢰할 수 없는 입력: 외부에서 들어온, 예를 들어 사용자가 입력한 값들이다. 이 값들은 실수나 악의적인 의도로 인해 잘못되거나 변조될 수 있다.
  1. 처리: 입력된 정보를 기반으로 시스템이 작업을 수행한다.
  • 안전한 처리: 개발자의 의도에 맞게 시스템이 정확하고 예상된 방식으로 동작한다.
  1. 출력: 처리 결과가 사용자나 다른 시스템에 전달된다.
    입력값 검증의 역할
  • 신뢰할 수 있는 입력: 자체적으로 안전하다고 간주되어 바로 사용될 수 있다.
  • 신뢰할 수 없는 입력: 직접적으로 사용하기 전에 검증하고 제한해야 한다. 검증 과정을 통해 입력값이 시스템에 해를 끼치지 않도록 하며, 시스템이 예상한 대로 동작하도록 보장한다.

인젝션(Injection, 삽입) 취약점

어떤 처리가 있을 때,
입력값에 처리를 조작할 수 있는 문자열 포함 여부를 확인하지 않고 처리에 사용하는 경우
처리의 구조와 의미가 변형되어 원래 의도했던 처리와 다르게 처리되는 문제점

유형

SQL Injection ⇐ 입력값이 SQL문을 만들고 실행하는데 사용
XPath Injection ⇐ 입력값이 XPath 구문을 만들고 실행하는데 사용
XQuery Injection ⇐ 입력값이 XQuery 구문을 만들고 실행하는데 사용
Command Injection ⇐ 입력값이 운영체제 명령어 또는 명령어의 일부로 사용

방어

1) 입력값에 처리를 조작하는 문자열 포함 여부를 확인하고 사용 ⇒ 입력값 검증, 제한
입력값에 처리를 조작하는 문자열이 포함되어 있는 경우

  • a) 오류 처리
  • b) 제거하고 사용
  • c) 처리를 조작하는 문자열을 일반 문자열로 해석되도록 변경해서 사용 ⇒ 이스케이프 처리

2) 각 기능에서 제공하는 안전한 방법을 사용 ⇒ 구조를 정의하고 정의된 구조에 입력값을 검증된 기능을 통해서 대입하는 방식으로 구현 ⇒ 구조화된 쿼리 실행, 파라미터화된 쿼리 실행

SQL Injection

외부 입력값에 쿼리 조작 문자열 포함 여부를 확인하지 않고, 쿼리문(SQL문)을 생성, 실행하는데 사용하는 경우
원래 의도했던 쿼리의 구조와 내용이 변경되어 실행되는 것

예상되는 문제들

데이터베이스 내 권한이 없는 데이터에 접근 가능
쿼리 실행을 통해 기능을 비정상적으로 이용 가능
예시: 로그인 기능

정상적인 입력

사용자가 ID abc와 비밀번호 xyz를 입력
시스템은 select * from users where id = 'abc' and pw = 'xyz' 쿼리를 실행
일치하는 사용자가 있으면 로그인 성공, 없으면 로그인 실패

비정상적인 입력 (SQL Injection)

사용자가 ID abc와 비밀번호 xyz' or 'a' = 'a를 입력
시스템은 select * from users where id = 'abc' and pw = 'xyz' or 'a' = 'a' 쿼리를 실행
'a' = 'a'는 항상 참이므로, 조건과 상관없이 로그인 성공 처리
결과적으로, SQL Injection은 데이터베이스 쿼리의 구조를 변경하여 무단으로 데이터에 접근하거나 시스템 기능을 우회하는 위험한 보안 취약점이다.

데이터베이스 서버의 제어권을 탈취하여 원격 제어가 가능해질 수도 있다.

https://www.hahwul.com/cullinan/history-of-owasp-top-10/

방어기법

1) 입력값에 쿼리 조작 문자열 포함 여부를 확인하고 사용

  • a) 오류 처리
  • b) 제거하고 사용
  • c) 안전한 형태로 변경해서 사용

2) PreparedStatement(Query Parameters)와 같은 구조화된 쿼리 실행(파라미터화된 쿼리 실행)을 보장하는 것을 사용
3) 오류 메시지에 상세한 내용(데이터베이스 및 쿼리 구조, 쿼리 실행과 관련한 프로그램 구조 등)이 포함되지 않도록 처리
4) 어플리케이션에서 사용하는 DB 사용자의 권한을 필요한 만큼의 최소한으로 부여

3) 4)은 SQL Injection 공격을 완화시키기 위해서 필요한 방어 기법

파이썬에서 SQL Injection을 방어하는 방법 ⇒ https://realpython.com/prevent-python-sql-injection/

실습 - 동작 원리 이해

Kali 가상머신에서 WebGoat으로 접속(http://host.pc:8080/WebGoat) > Injection Flaws > String SQL Injection > 모든 사용자의 계좌 정보가 출력되도록 해 보세요.

정상적인 처리 과정을 유추

정상적인 입력

Enter your last name: Smith

개발자 도구를 이용해서 서버로 전달되는 내용을 분석

attack?Screen=34&menu=1100&account_name=Smith&SUBMIT=Go!

                      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                      POST 방식으로 해당 내용은 요청 본문을 통해서 전달되나, 
                      여기에서는 편의를 위해서 GET 방식(주소를 통해서 전달)으로 묘사
                                             

F12 클릭

요청 파라미터로 전달된 값이 서버에서 어떻게 사용되는지를 유추 ⇒ 아마도 특정 테이블에 데이터를 조회하는 쿼리를 만들고 실행하는데 사용
SELECT * FROM user_data WHERE last_name = 'Smith'

만약, 입력값이 서버에서 검증 없이 그대로 쿼리를 만드는데 사용된다면, 모든 사용자 데이터를 조회하는 쿼리 형태를 생성

SELECT * FROM user_data WHERE last_name = 'Smith' or 'a' = 'a' ⇐ 항상 참이 되는 조건을 추가
| |
+------------------+
last_name 컬럼의 데이터 타입이 문자열인 것을 확인

사용자 화면에서 쿼리 조작 문자열을 포함해서 요청을 전달

Enter your last name: Smith' or 'a' = 'a

⇒ 외부 입력값을 쿼리 조작 문자열(' = or) 포함 여부를 확인하지 않고 그대로 쿼리 생성 및 실행에 사용해서
쿼리의 원래 의미(이름이 일치하는 데이터를 조회해서 반환)를 변경(모든 데이터를 조회)해서 실행

Numberic SQL Injection

선택한 지역의 날씨 정보를 제공하는 웹 페이지에서 모든 지역의 날씨 정보가 출력되도록 해 보세요.

정상적인 동작

Select your local weather station: Columbia 를 선택하면 101이 station 파라미터 값으로 전달

개발자 도구를 이용해서 소스 코드를 분석

<form accept-charset="UNKNOWN" method="POST" name="form" action="attack?Screen=44&amp;menu=1100" enctype=""><p>Select your local weather station: <select name="station"><option value="101">Columbia</option><option value="102">Seattle</option><option value="103">New York</option><option value="104">Houston</option></select></p><p><input name="SUBMIT" type="SUBMIT" value="Go!"></p><pre>SELECT * FROM weather_data WHERE station = 102</pre><table cellpadding="1" border="1"><tbody><tr><td><b>STATION</b></td><td><b>NAME</b></td><td><b>STATE</b></td><td><b>MIN_TEMP</b></td><td><b>MAX_TEMP</b></td></tr><tr><td>102</td><td>Seattle</td><td>WA</td><td>-15</td><td>90</td></tr></tbody></table></form>

서버로 전달되는 값은 아래와 같은 형태로 서버로 전달

attack?Screen=44&menu=1100&station=101&SUBMIT=Go!

서버에서 요청으로 전달된 값을 아래와 같은 쿼리를 생성하고 실행하는데 사용

SELECT * FROM weather_data WHERE station = 101

모든 데이터를 조회하는 쿼리를 작성

항상 참이 되는 조건을 추가 (원래 쿼리의 형태를 고려해서 추가해야 함)
SELECT * FROM weather_data WHERE station = 101 or 1 = 1

조작하는 쿼리를 요청 파라미터로 전달 ⇒ 입력창이 아니므로 값을 직접 입력하는 것이 불가능

방법1. 개발자 도구를 이용해서 선택되었을 때 서버로 전달되는 값을 조작

<form accept-charset="UNKNOWN" method="POST" name="form" action="attack?Screen=44&amp;menu=1100" enctype=""><p>Select your local weather station: <select name="station"><option value="101 or 1 = 1">Columbia</option><option value="102">Seattle</option><option value="103">New York</option><option value="104">Houston</option></select></p><p><input name="SUBMIT" type="SUBMIT" value="Go!"></p><pre>SELECT * FROM weather_data WHERE station = 102</pre><table cellpadding="1" border="1"><tbody><tr><td><b>STATION</b></td><td><b>NAME</b></td><td><b>STATE</b></td><td><b>MIN_TEMP</b></td><td><b>MAX_TEMP</b></td></tr><tr><td>102</td><td>Seattle</td><td>WA</td><td>-15</td><td>90</td></tr></tbody></table></form>```
![](https://velog.velcdn.com/images/jaeeeeeeeez/post/4c8c929d-948b-4ee7-936e-25c51f71837b/image.png)

방법2. Proxy를 이용해서 서버로 전달되기 전에 값을 변경해서 전달

#1 실습 리셋

#2 요청 데이터를 잡아서 수정하기 위해서 Intercept를 설정

#3 요청을 발생 ⇒ 선택된 지역의 지역번호가 요청 파라미터로 전달

#4 인터셉터된 요청의 내용(요청 파라미터의 값)을 변조한 후 인터셉터를 해제 ⇒ 변조된 요청이 서버로 전달


모든 지역의 날씨 데이터가 조회되는 것을 확인

입력값 검증의 중요성

인젝션을 통한 데이터 접근

  • 조회 결과에서 6개 지역을 얻을 수 있지만, 서비스는 4개 지역에만 날씨 정보를 제공.
  • 인젝션을 사용하면 서비스되지 않는 지역에 대한 정보도 접근 가능.

입력 컴포넌트의 제한

  • 텍스트 입력란에서는 사용자가 어떤 값을 입력하든지 가능.
  • 반면 선택 컴포넌트(라디오 버튼, 체크 박스, 셀렉트 박스)는 시스템에서 제공하는 옵션 중 하나를 선택하도록 제한.

클라이언트 사이드의 제한

  • UI 컴포넌트를 통해 사용자가 제한된 옵션(예: 101, 102, 103, 104) 중 하나만을 선택하도록 함.
  • 서버 사이드에서는 이러한 제한을 검증하지 않으면 보안 문제가 발생할 수 있음.

서버 사이드 검증의 중요성

  • 클라이언트 사이드의 보안 기능만으로는 충분하지 않음.
  • 서버 사이드에서도 입력값을 검증하고 제한하여 보안을 강화해야 함.

보안 취약점

  • 보안 기능이 결정되는 데 사용되는 입력값이 부적절할 때 발생.
  • 서버 사이드에서의 추가 검증이 필요.

SQL 인젝션 유형 정리

  1. Error Based SQL Injection
    사용자 입력에 의해 에러를 유발시켜 에러 메시지로부터 정보를 수집
    예시: 홑따옴표 ' 사용하여 데이터 타입 오류 또는 쿼리 구문 오류 유발

  2. Always True SQL Injection
    쿼리 조건식을 항상 참으로 만들어 권한 밖 데이터 접근 가능
    예시: or 1=1을 추가하여 쿼리가 항상 참이 되도록 조작

  3. Union Based SQL Injection
    원래 쿼리에 공격자의 쿼리를 UNION을 이용해 추가하여 실행
    두 쿼리의 결과를 결합하여 원하는 정보를 함께 출력
    예시: 정상 쿼리에 and 1=2 UNION select ...를 추가하여 공격자가 원하는 정보 조회

  4. Stored Procedure Injection
    데이터베이스의 저장 프로시저를 호출하는 구문을 입력하여 실행
    데이터베이스 제어권 탈취 가능
    예시: ; exec xp_cmdshell 'cmd.exe /c dir'로 시스템 명령어 실행

  5. Blind SQL Injection
    쿼리의 결과에 따른 서버 반응을 통해 정보 수집
    예시: 존재하는 id에 대해 and 1=1과 and 1=2 조건을 추가하여 결과 변화를 관찰하며 정보 수집

Blind Numeric SQL Injection 실습

사용자가 입력한 계좌 번호(Account Number)의 유효성(있다, 없다)를 확인해 주는 웹 페이지
해당 계좌가 존재하는 경우, Account number is valid.를 출력하고,
해당 계좌가 존재하지 않는 경우, Invalid account number.를 출력

문제는 pins 테이블에서 cc_number 컬럼의 값이 1111222233334444와 일치하는 pin 컬럼의 값을 찾으시오.

The goal is to find the value of the field pin in table pins for the row with the cc_number of 1111222233334444. The field is of type int, which is an integer.

⇒ 공격자가 찾고자 하는 pin 값인 2364 임

Blind String SQL Injection

pins 테이블에서 cc_number 컬럼의 값이 4321432143214321와 일치하는 name 컬럼의 값을 찾으시오.
(문자열 타입의 데이터를 저장하는 컬럼)

아스키 코드 추출을 이용

취약한 소스코드 확인

실행된 eclipse에서 Ctrl + Shift + R (Open Resource, 열려 있는 프로젝트에서 특정 패턴의 파일을 검색해서 열어주는 도구)

  protected Element createContent(WebSession s)
    {
	ElementContainer ec = new ElementContainer();

	try
	{
	    Connection connection = DatabaseUtilities.getConnection(s);

	    ec.addElement(new P().addElement("Enter your Account Number: "));

                                                                "account_number"를 값으로 가지는 상수  
                                                                ~~~~~~~~ 
	    String accountNumber = s.getParser().getRawParameter(ACCT_NUM, "101");      ----+
                                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~          |
                                  요청 파라미터 중 ACCT_NUM 파라미터의 값을 가져와서 반환  |
                                  만약 파라미터 또는 파라미터의 값이 없는 경우 101을 반환  |                      
	    Input input = new Input(Input.TEXT, ACCT_NUM, accountNumber.toString());        |
	    ec.addElement(input);                                                           |
                                                            외부 입력값에 쿼리 조작 문자열 포함 여부를 확인하지 않고 
	    Element b = ECSFactory.makeButton("Go!");        문자열 결합 방식의 쿼리문 생성하고 있음 
	    ec.addElement(b);                                → 외부 입력값에 의해 쿼리의 구조와 의미가 변형될 수 있음
                                                                                           |
	    String query = "SELECT * FROM user_data WHERE userid = " + accountNumber;  -----+ 
                          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~   ~~~~~~~~~~~~~
                          문자열 결합 방식으로 쿼리를 생성               
	    String answer_query;
//	    if (runningOnWindows())
//	    {
//		answer_query = "SELECT TOP 1 first_name FROM user_data WHERE userid = "
//			+ TARGET_CC_NUM;
//	    } else
//	    {
		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
		{

		    Statement statement = connection.createStatement(			⇐ Statement 구문을 통해서
			    ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
		    ResultSet results = statement.executeQuery(query);			⇐ 만들어진 쿼리를 그대로 실행

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


Java의 Statement 객체
Statement 		⇒ 만들어진 문자열 형태의 쿼리를 그대로 전달해서 실행
                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~
                         쿼리 생성 책임이 개발자에게 있음 
   
PreparedStatement	⇒ 미리 정의한 쿼리 구조에 맞춰서 쿼리를 생성해서 DB로 전달해서 실행 
                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                         쿼리 구조 정의는 개발자가 하고, 쿼리 생성은 해당 객체가 책임지고 수행
 
CallableStatement 	⇒ DB에 정의되어 있는 Stored Procedure를 호출할 때 사용 

0개의 댓글