[WebGoat] (A3)-3. SQL Injection (mitigation)

신지훈·2025년 7월 28일

WebGoat(웹 해킹)

목록 보기
8/8
post-thumbnail

1. SQL Injection 대응 방법

SQL Injection의 취약점은 DBMS에서 쿼리를 받을 때 쿼리가 사용자의 입력값을 구분하지 못하고 그대로 붙여 씀으로써 취약점이 발생한다.

그럼 어떻게 DBMS가 사용자의 입력값을 구분할 수 있도록 만들 수 있을까? 대표적인 방법으로는 Parameterized Query방법으로 2개의 예시가 있다.

1) Prepared Statement

Prepared Statement는 미리 준비된 구문을 뜻한다. 이는 쿼리를 미리 준비하는 것으로 데이터가 대입될 부분은 ?로 표시한 쿼리를 DBMS에 전달하면 쿼리를 컴파일을 마치고 캐싱 한다. 이후에는 ?에 들어갈 데이터만 바인딩해 실행하는 방식이다.
아래 코드를 보면 setString()를 사용하여 데이터를 바인딩 할 때 어떤 타입의 데이터를 바인딩 할지 설정하기 때문에 Injection 공격구문이 실패하게 된다.

Connection conn = DriverManager.getConnection(DBURL, DBUSER, DBPW);

PreparedStatement statement = conn.prepareStatement("SELECT status FROM usersWHERE name= ? AND mail= ?");


statement.setString(1, "user");
statement.setString(2, "test@test.com");

2) ORM

ORM은 Object Relational Mapping의 줄임말로 객체와 데이터베이스의 데이터를 자동으로 매핑해주는 도구이다. 대표적으로 Mybatis, JPA가 있으며 직접 쿼리를 작성하지 않고 코드를 이용해 DB 작업을 실행할 수 있다. 또한 바인딩도 자동으로 형 변환까지 수행해주기 때문에 SQL Injection의 걱정 없이 안정한 개발이 가능하다.

2. 모의 해킹 실습

위에서의 SQL Injection에 대해 간략히 살펴보았다. 하지만 가끔 이 방법 외로 SQL Injection에 대응하다가 역으로 공격당하는 경우가 있다. 대표적인 예로 입력값 필터링이다. 다음 문제들을 통해 이런 필터링이 어떤 문제들이 있는지 모의 해킹을 통해의 해킹을 통해 살펴보자.

1) 9번 문제 : 공백 필터링

5번 문제는 이전에 union를 통해 해킹했던 내용을 입력하는 것으로부터 시작한다.
이전에 asdf' union select userid, user_name, password, null, null, cookie, null from user_system_data-- 공격구문을 사용해 봤더니 이전과는 다르게 다음과 같이 공백을 허용하지 않는다고 한다.

공백을 사용하지 못하도록 필터링을 했지만 이를 우회할 수 있는 방법이 있다. 이전 공격 구문에서 --의 주석은 공백을 대신할 수 없지만 /*,*/를 사용한 주석은 공백을 대신할 수 있다. 공백 대신 /**/ 를 활용해 asdf'/**/union/**/select/**/userid,user_name,password,null,null,cookie,null/**/from/**/user_system_data-- 공격 구문을 시도해 보자.

그 결과 /**/이 공백을 대신해 공격 구문이 정상적으로 작동하는 것을 확인할 수 있다.

2) 10번 문제 : 단어 필터링

이어서 6번에서는 /**/를 활용한 공격구문을 넣어보자.

결과를 살펴보니 select, from과 같이 sql문에 필요한 단어들이 필터링 된것을 확인할 수 있다.

이 역시 select -> selselectect를 입력하게 되면 가운데의 select지워지면서 다시 select단어를 만드는 방법으로 우회할 수 있다.

이런 우회 방법을 활용해 asdf'/**/union/**/selselectect/**/userid,user_name,password,null,null,cookie,null/**/frfromom/**/user_system_data-- 과 같이 공격구문을 작성할 수 있다.

그결과 selselectect -> select frfromom -> from으로 단어가 만들어져 정삭적으로 공격구문이 작동하는 것을 확인할 수 있다.

이처럼 필터링을 통해 공격을 까다롭게 할 순 있지만 우회할 수 있는 방법은 훨씬 다양하기 때문에
필터링으로 SQL Injection공격을 막기에는 역부족이다.

3) 12번 문제 : Order by 취약점

앞서 Prepared Statement의 설명을 보면 완벽해 보이지만 Order by 구문에 사용자의 입력값을 대입하려고 하면 어쩔 수 없이 문자열을 이어붙이는 방법을 사용해 취약점이 발생할 수 있다. 따라서 Order by 구문에 사용자의 입력값을 대입하지 않는 것이 중요하다.

그럼 실습 문제에서 Order by의 취약점을 찾아보고 취약점을 이용해 webgoat-prd의 ip 주소를 알아내 보자.

먼저 문제에서 아래와 같은 버튼을 누르면 각 항목에 대해 정렬이 되는 것을 확인할 수 있다. 그리고 이 요청을 brup suite를 통해 확인해 본다면 다음과 같이 파라미터로 column에 hostname이나 ip같은 항목을 전달하는 것을 확인할 수 있다.

그럼 우리는 column의 항목이 Order by 뒤에 이어붙여질 수 있는 가능성을 확인하기 위해 다음과 같이 Reapter를 통해 에러를 유도해보자.

해당 요청을 Reapter로 보내고 column의 값에 '를 하나 추가해 홀수의 '를 유도해보니 다음과 같이 에러가 뜨고 이를 통해 Order by의 취약점이 있음을 확인할 수 있다.

그럼 공격 구문으로는 HSQLDB에서 조건문으로 사용되는 case when(1<2) then를 사용해서 이렇게 공격 구문을 만들 수 있다. case when(1<2) then hostname else ip end 이 구문으로 when에 있는 조건이 hostname으로 정렬할지 ip 로 정렬되는지 확인하여 참 거짓인지를 구분하는 Blind SQL Injection 공격을 할 수 있다.

columncase when(1<2) then hostname else ip end를 특수문자들은 url인코딩하여 %28case+when%281%3c2%29+then+hostname+else+ip+end%29 를 입력하면 다음과 같이 hostname으로 정렬되는 것을 확인할 수 있다.

반대로 when(2>1)으로 false가 나오도록 %28case+when%281%3c2%29+then+hostname+else+ip+end%29column에 입력하면 역시 ip를 기준으로 정렬되는 것을 확인할 수 있다.

이렇게 물어보는 조건에 따라 참 거짓을 알려주니 우리는 이전 쳅터의 문제처럼 범위를 좁혀나가 한자리씩 알맞는 주소를 찾아야 한다.

우리는 최종적으로 webgoat-prd라는 서버의 ip를 주소를 찾아야하기 때문에 조건문에서 먼저 쿼리문을 통해 ip주소 조회가 필요하다.

그리고 이 ip주소를 substring를 이용하여 하나씩 때어오고 이를 0~9까지 하나씩 비교하여 자리마다 알맞는 숫자를 얻어올 수 있다.

따라서 최종적으로 다음과 같은 공격구문을 만들 수 있다.case(when(substring((select ip from servers where hostname='webgoat-prd'),1,1)='??') then hostname else ip end)

먼저 이 공격을 Repeter에서 case(when(substring((select ip from servers where hostname='webgoat-prd'),1,1)='9') then hostname else ip end) 공격이 작동하는지 확인해보자.

특수문자들은 url로 encode하여 다음과 같이 작성해야 한다.
%28case+when%28substring%28%28select+ip+from+servers+where+hostname%3d%27webgoat-prd%27%29%2c1%2c1%29%3d%279%27%29+then+hostname+else+ip+end%29

위의 공격구문을 실행해보니 ip주소로 정렬되는 것을 보아 첫 번째 자리는 9가 아닌 것을 확인할 수 있고 공격구문이 정상적으로 작동하는 것을 알 수 있다. 그럼 이 구문을 반복적으로 실행시키기 위해 해당 요청을 Intruder로 내다.

그런 substring에서 첫 번째 자리에서 1글자를 가져오는 것이 맞는지 확인하고 이를 비교하는 (1)자리를 positions에서 (2)추가하고 해당 자리에는 (3)0~9가 들어가도록 설정 한후 (4)공격을 실행한다.

그럼 결과에서 hostname으로 정렬되는 것을 찾고 request에서 어떤 숫자로 비교했는지 확인하면 첫 번째 자리는 1인것을 확인할 수 있다.

그럼 substring에서 자리를 (ip,2,1), (ip,3,1), (ip,4,1)...비교해 간다면 전체 ip주소를 알아 낼 수 있다. 만약 결과값 중에 hostname으로 정렬된 경우가 없다면 ip주소 구분자인 .인것을 알 수 있다. 이렇게 공격 구문을 실행한 경과 webgoat-prd의 ip주소는 104.130.219.202 인 것을 알 수 있고 이를 입력하면 문제는 해결된다.

3. 정리

  1. SQL Injection 공격을 피할 수 있는 방법으로는 Parameterized Query이 방법이 대표적이고 여기에는 preparedStatement, ORM방법이 있다.

  2. 입력값의 공백이나 특정 단어를 필터링하는 방법은 충분한 우회방법이 있기 때문에 주의해야한다.

  3. preparedStatement, ORM방법이 무적같아 보이지만 Order by 구분에 사용자의 입력값이 이어 붙을 수 있기 때문에 주의해야한다.

profile
주주주주니어 개발자

0개의 댓글