본문 바로가기

Webhacking/WebGoat

[WebGoat] Injection-SQL Injection (mitigation) 풀이

728x90
반응형

이번엔 SQL Injection 대응 방안에 대한 내용이다.
Static Queries 방법을 사용했을때 문제가 발생하고, 우리는 Parameterized Queries 방법이나 아니면 정적인 쿼리만을 수행하는 Stored Procedures 방법을 사용하면 된다.

Stored Procedure란 실행시킬 쿼리를 하나의 명령어처럼 만들어서, 이를 호출해서 사용하도록 하는 방식이다. 
주로 Safe Stored Procedure 방식을 사용하기에, SQL Injection으로 부터 보호받을 수 있으나, 간혹 Injectable하게 procedure안에 또 다른 쿼리를 넣어 dynamic하게 만드는 실수를 하는 개발자분들이 있다. 이 경우 SQL Injection으로부터 위험해진다.

또 여기 설명은 되어있지 않지만 JPA나 Mybatis같은 ORM을 사용하는 방법도 있다.
WebGoat에서는 주로 Parameterized Queries 방법을 설명하고 있고 또 권고하고 있다. 그리고 우리가 직접 실습을 통해 학습하기에도 가장 편하고 쉬운 방법이므로, 이를 이용하여 SQL Injection 조치를 직접 한번 수행해볼 것이다.

직접 조치를 수행해볼 문제는 바로 SQL Injection advanced 5번 문제이다.
왜 이 문제를 수정하느냐! 바로 취약한 코드와 안전한 코드 각각의 예시를 이 문제 하나를 통해 다 볼 수 있기 때문이다.
기억하는가, Login 기능에서는 SQL Injection이 되지 않았고, Register기능에서만 인젝션이 가능했던것을... 우선 안전하지 않았던 Register 메뉴에 대한 코드부터 보자.

SqlInjectionChallenge.java 파일을 보면 Register 기능에 해당하는 코드를 볼 수 있다.
여기서 가장 중요한 부분은 바로 checkUserQuery 변수에 대입되고 있는 Sql 쿼리문이다. 
select userid from sql_challenge_users where userid= '"+ username_reg + "'"; 이런식으로 코드가 구현되어있다. 여기서 username_reg는 클라이언트가 파라미터를 통해 보낸 가입하고자하는 사용자 id일 것이다. 이 코드의 문제점이 무엇인지 바로 알겠는가?
바로 개발자가 의도한 쿼리부분과, 사용자가 전달한 값의 구분이 전혀 되지 않는다는 것이다.

저렇게 그냥 하나의 문자열로 이어붙인 뒤 DBMS에서 전달해서 실행시키라고 한다면, DBMS입장에서 어디가 개발자가 의도한 쿼리고, 어디가 사용자의 입력값인지를 구별할 수 있을까? 당연히 할 수 없다. 그렇게 때문에 공격자가 임의로 싱글쿼터를 넣어 문자열의 범위를 닫고 다른 쿼리를 써 조건을 맘대로 변경해도 DBMS는 그저 실행시킬수 밖에 없는 것이다.

그렇다면 이번엔  바로밑에 있는 insert into쪽을 한번 봐보자.
아마도 저 부분은 중복확인 이후, 회원가입 과정 자체를 담당하는것 같은데, 기존에는 그냥 Statement 클래스를 사용했었는데, 여기서는 PreparedStatement 클래스를 사용한다는 차이가 있으며, 또 문자열을 그냥 이어붙인것이 아닌 ?로 넣어두었음을 알 수 있다. 여기서 사용된 PreparedStatement 클래스가 바로 대표적인 Parameterized Queries를 사용하는 예시라고 할 수 있다.

PreparedStatement는 기본적으로 사용할 쿼리 중 사용자의 입력에 따라 값이 변하는 부분, 즉 데이터가 들어가야하는 부분은 ?로 채워놓고, 그 이외 고정되는 개발자가 의도한 쿼리를 미리 DBMS에게 전달해 pre-compile시킨 뒤, 데이터를 추후에 ?부분에 삽입하는 형태로 사용된다. 즉 데이터를 파라미터 형태로 전달한다.

코드를 보면 Insert into sql_callenge_users Values(?, ?, ?) 실제로 값이 들어가야할 부분은 ?로 채워두고 preparedStatement.setString 메서드를 통해 몇번째 파라미터에 어떤값을 넣을지를 코드로 작성하게 된다.  이렇게 구현할 경우, 개발자가 실행시키고자 의도한 쿼리부분과, 사용자의 입력값이 들어가는 데이터 부분이 명확히 구분되며 또 쿼리를 보면 알겠지만 싱글쿼터나, 더블쿼터를 직접 쿼리에 넣지 않아도, setString/setLong 등 어떤 메서드를 호출하냐에 따라 삽입할 값의 타입을 맞춰주기 때문에 공격자가 임의로 범위를 벗어나려해도 벗어날 수 없다. 즉 Sql Injection으로부터 안전한 서비스를 운영할 수 있게 된다.

이건 Login 기능에 해당하는 SqlInjectionChallengeLogin.java 코드의 일부이다.
드래그된 부분을 보면, 위에서 설명한 PreparedStatement 형태로 데이터를 받아 실행시킨다는 것을 알 수 있다. 그래서 아무리 공격을해도 성공하지 못했던 것이다.

그렇다면 학습한 두개의 안전한 코드를 참고하여 취약한 코드를 수정해보자.

빌드를 완료한 뒤, 다시 실행하고 Register 부분에 공격을 시도해본다. Payload는 asdfasdf' or 1=1--  만약 인젝션에 성공한다면 asdfasdf라는 계정이 없기에 1=1이라는 참 조건으로 쿼리를 실행하게 될 것이다. 
실행해본 결과 계정이 잘 생성되었다. 이번에는 거짓인 값으로 바꾸어보자.

1=1 조건을 1=3이라는 당연히 거짓인 값으로 수정했다. 하지만 또 가입에 성공했다는것을 알 수 있다.
즉 Sql Injection이 되지 않아, payload자체가 id로 가입이 되어버렸다는것을 의미한다.

이렇게 PreparedStatement 클래스를 이용한 SQL Injection 취약점을 조치해보았다. 
가끔 어떤 개발자분들은 그냥 특수문자 필터링을 하면 되지 않느냐 라고 물어보시는데, 물론 그렇게도 조치가 가능은하다. 하지만 실수로 특수문자를 놓치거나, 특수문자 필터링을 수행하는 로직 태우는걸 누락했거나 하는 경우, 문제가 발생할 수 있다. 
이번에 WebGoat 문제를 통해 어떤 케이스가 있는지 알아보자.

SQL Injection (mitigation) 9번문제를 보면, 입력값 필터링만으로는 부족하지 않다는 제목으로 문제를 하나 내고있다.
입력값 필터링을 통해 SQL Injection을 방어한 문제인데, 이를 우회해서 user_system_data 테이블 내 데이터를 전체 조회하면 된다.

이것저것 입력을 하다보면, Using spaces is not allowed라는 문구를 볼 수 있다. 즉 이 문제는 공백을 필터링 해둔것이다.
공백을 우회할수 있는 방법은 다양하다. 줄바꿈을 이용하거나, 괄호를이용하거나, 주석을 이용하는 방법 등이 있다. 이번에는 주석을 이용해 볼 것이다.

WebGoat에서 사용하는 HSQL은 JAVA처럼 /* 으로 시작해서 */ 으로 끝나는 부분을 주석으로 설정할 수 있다. 그렇다면 이런식으로 공격을 해보면 된다. 
Payload : asdf';select/**/*/**/from/**/user_system_data-- 
*이 많아 헷갈릴 수 있다. /**/ 주석을 열고 닫아 공백 대신 사용할 수 있기에, Payload와 같이 쿼리체이닝으로 테이블 내 데이터를 조회할 것이다.

문제를 해결했다. 다음문제로 넘어가자.

동일한 문제같지만, 추가로 필터링을 한 문제라고 한다. 우선 이전에 풀이한 Payload를 그대로 넣어보자.

뭔가 이상하다. 내가 입력한 select와 from이 사라졌다. 이를통해 이번문제는 일부 문자열도 필터링 하고있음을 알 수 있다. 입력값 필터링 자체가 지양하는 방법이지만 더욱이나 필터링 방법이 매우 잘못된것도 있다. 문자열을 아예 없애버리면 이런식으로 우회를 할 수 있다.
selselectect를 입력하면 가운데 select가 사라지면서 앞뒤가 붙어 select가 될 것이다. from도 똑같을 것이다. 이 내용을 바탕으로 Payload를 수정해보자.

Payload : asdf';selselectect/**/*/**/frfromom/**/user_system_data-- 

문제가 풀렸다. 마지막 문제로 넘어가자.

마지막 문제는 12번에서 설명하고 있는 order by 구문에 의한 SQL Injection이다.
order by 구문을 사용할 때 사용자의 입력을 그대로 사용하는것은 매우 지양하고 있다. 왜냐하면 order by 구문은 where절과 다르게, 데이터를 입력하는게 아니라 실제로 실행될 쿼리를 넣는 부분이기에 PreparedStatement로 보호받을 수 없기 때문이다. 그래서 order by구문은 사용자로부터는 특정 데이터를 받고, 어떤 데이터가 들어오면, 어떤 정렬을 수행한다는 조건 분기문을 만들어 매핑시키는것이 권고된다. 
우선은 문제를 한번 풀어보자. 아무거나 정렬을 시도해보자.

column 파라미터로 데이터를 보내는것을 알 수 있다. 아마 저 데이터가 실제 컬럼명이기 때문에 SQL Injection이 발생할 것이다. 이번에 알아내야하는 것은 webgoat-prd라는 hostname을 가진 서버의 IP주소이다.

다만 이 문제는 코드를 보면 알다싶이 preparedstatement를 사용하였기 때문에 쿼리체이닝이나 union 공격을 할 수 없다. 실행할 쿼리는 정해져있으니 말이다. 우리가 할 수 있는건 order by 구문에 조건문을 넣어 참, 거짓에 다른 응답값을 구분하여 Blind SQL Injection을 하는 수 밖에 없다. 그래서 문제에서 IP 앞 3자리만을 얻으라고 하는것이다. (노가다를 방지하기 위함)

우선 공격을 하기전에 column에 아무거나 입력해보니, SQL 오류내용이 잔뜩 노출되었다. 이 중 중요한건 바로 실제 실행되는 쿼리가 공격자에게 노출되었다는 것이다. 실제 컬럼명과 테이블명이 모두 노출되었다. 이제 우리는 서브쿼리를 이용해서 order by 구문에 case when 조건을 넣어 Blind SQL Injection을 매우 쉽게 할 수 있게 되었다.

우선은 (case when(1<2) then hostname else ip end) 로 공격을 수행했다. 1<2라는 조건은 당연히 참이 되므로, 인젝션이 잘 된다면 hostname기준으로 정렬이 될 것이다. 옵션을 안넣었으니 오름차순으로 정렬이 될 것이다. 이로써 쿼리가 참일때의 결과가 잘 나온다는것을 확인했다.

다음은 (case when(1>2) then hostname else ip end) 로 공격을 수행했다. 차이점은 1>2라는 조건으로 거짓된 조건을 이용하여 공격을 수행했다는 것이다. 그러자 else 구문을 타고 ip 기준으로 오름차순 된 응답값을 확인할 수 있었다. 이제 서브쿼리를 활용하여 문제를 풀어보자.

Payload : (case when(ascii(substring((select ip from servers where hostname='webgoat-prd'),1,1))<127) then hostname else ip end)
Payload를 해석하면 servers 테이블에서 hostname이 webgoat-prd인 ip를 뽑는데, 이를 substring하여 맨 앞글자 한 개를 잘라낸 뒤, 이를 다시 ascii코드로 치환한 값이 127 미만일 경우, hostname으로 정렬하고 아닐경우 ip로 정렬하라는 공격 구문이다.
응답을 보면 hostname으로 정렬된것을 볼 수 있다. 그렇다면 이제 이 구문에서 부등호를 등호로 치환한 뒤 intruder를 이용해 쉽게 풀어보도록 하자.

첫번째 자리를 구하는 intruder payload position 설정이다. 대입될 부분은 ascii 코드를 비교할 부분이며, 그 바로옆 %3C를 %3D(등호)로 바꿔주면 된다. IP주소는 무조건 숫자이기 때문에 공격은 숫자 0의 ascii 코드인 48부터 숫자 9의 ascii code인 57까지 진행하면 된다.

intruder로 공격을 하고나면, 유일하게 딱 하나의 response만 hostname기준으로 정렬되어 있을 것이다. 그것이 실제값이며, 이를 3번 진행하면 맨 앞자리 3자를 얻을 수 있다.
바로 '104' 이다. 문제에서 제공해준 데이터와 합치면 답은 '104.130.219.202'가 된다.

길었던 mitigation 끝


 

728x90
반응형