[WebGoat] (A3)-2. SQL Injection (advanced)

신지훈·2025년 7월 22일

WebGoat(웹 해킹)

목록 보기
7/8
post-thumbnail

1. SQL Injection (advanced)

이전에 살펴본 SQL Injection내용으로 실질적인 공격이 어렵다. 실제와 더 비슷한 환경에서 사용되는 다양한 공격 기법에 대해 알아보자.

2. 모의 해킹 실습

3번 문제-1. SQL query chaining

먼저 문제를 살펴보면 아래와 같이 테이블의 구조가 주어졌고 우리는 Dave라는 사용자의 passwrod를 알아내야 한다.

그럼 먼저 아래쪽 입력칸 Name에 아무런 입력값을 입력해 보자.

입력해보면 위에서 주어진 user_data테이블에서 이름을 조회해오는 것을 알 수 있다.

하지만 테이블의 구조를 보면 우리가 원하는 password의 대한 정보는 user_system_data에 있는 것을 알 수 있다.

그럼 우리는 SQL query chaining을 이용해 user_system_data에서 전체 정보를 조회하면 Dave의 password를 알아 낼 수 있을 것 같다.

그럼 우리는 ;를 사용하여 새로운 쿼리를 뒤에 붙이고, 뒤에 있는 문자들로 인해 오류가 일어나는 것을 막기 위해 주석인 --를 추가하여 asdf' ; select * from user_system_data-- 이렇게 공격 구문을 만들 수 있다.

위의 공격 구문으로 Dave의 비밀번호를 알아냈고 해당 비밀번호를 입력하면 문제를 해결할 수 있다.

3번 문제-2. union

위의 방법으로 문제를 해결할 수 있지만 서비스에 따라 query chaining를 허용하지 않을 수도 있다.

이런 경우 우리는 아이디를 조회하는 user_data와 password가 있는 user_system_data의 구조를 알기 때문에 union를 적절히 사용하면 query chaining없이 password값을 알 수 있다.

union

union에 대해서 간단히 설명하면 union의 앞뒤에 있는 서로 다른 쿼리로부터 나오는 결과를 합쳐서 결과를 나타내 주는 것이다. 여기서 핵심은 2개의 쿼리에서의 결과에서 합치고자 하는 값들의 타입이 서로 같아야 한다는 것이다.


앞선 쿼리에서 보면 user_data에서 * 즉, 데이터 구조를 보면 7개의 컬럼의 데이터를 가져오는 것을 확인할 수 있고, 우리는 이 데이터들과 4개의 컬럼이 있는 user_system_data의 결과를 합쳐서 결과를 조회해야 한다.

커럼이 7,4개로 개수가 다르므로 null로써 부족한 컬럼을 채워야 하고 user_system_data의 4개의 데이터들의 타입에 user_data의 타입에 맞춰서 조회해야 한다. 이를 위해서 다음과 같이 쿼리를 작성하여 조회할 수 있다.

asdf' union select userid, user_name, password, null, null, cookie, null from user_system_data--

  • 같은 타입의 데이터들만 합쳐질 수 있다.
user_datauser_system_data
userid - (int)userid - (int)
first_name - (varchar)user_name - (varchar)
last_name - (varchar)password - (varchar)
cc_number - (varchar)null
cc_type - (varchar)null
cookie - (varchar)cookie - (varchar)
login_count - (int)null

이렇게 하면 asdf에 대한 데이터는 없기 때문에 union 뒤에 있는 select 쿼리의 결과 값이 형식에 맞게 다음과 같이 알맞게 조회되는 것을 확인할 수 있다.

5번 문제

이전 까지의 문제는 실행되는 쿼리문을 확인할 수 있어서 비교적 쉽게 취약점을 공략할 수 있었다. 하지만 실제 서비스들은 쿼리 실행 결과를 직접 노출하지 않고 에러 핸들링을 잘 해두어 오류 내용이 나타나는 경우가 매우 드물다. 이럴 때 사용할 수 있는 방법이 Blind SQL Injection이다.

Blind SQL Injection

Blind SQL Injection은 쿼리를 직접 볼 수는 없지만 응답을 통해 쿼리의 참과 거짓을 알 수 있다면 데이터를 잘게 나누어 특정 값을 알아내는 방법이다. 이 기법의 종류로는 데이터 기반(Error based), 시간 차(Time based), 응답 본문(Content based)을 기준으로 구분하는 방법이 있다.

1. 취약점 찾기

그럼 실습 문제를 살펴보면 우리는 Tom으로 로그인을 해야한다. 먼저 로그인 화면에 test와 전형적인 공격 기법이 test',test' or 1=1--처럼 전형적인 공격 기법을 했을 때 특별히 다른 점이 없다.

그럼 로그인 화면에는 취약점이 없다고 생각하고 Register화면에서 이어서 시도해보자.

회원가입 페이지에서 test라는 Username으로 2가입을 시도해보니 다음과 같은 메세지를 확인할 수 있었다.

이번에 test'를 통해 '의 개수를 홀수가 되게 하여 에러를 유도해 보았다. 그 결과 다음과 같이 뭔가가 잘못됐다는 글과 함께 에러가 발생하는 것을 보아 취약점이 있다는 것을 유추할 수 있다.

2. Blind SQL Injection 사용 가능 여부 확인

그럼 취약점이 있다고 생각하는 부분을 찾았으니 전형적인 SQL Injection 공격을 해보자. 여기서 특이한 점은 이미 test라는 회원을 가입했기 때문에 test을 그대로 사용하기 위해서 이전과는 다르게 or이 아닌 and를 사용해야한다. 그 이유는 test'까지의 조건은 참이 되기 때문에 or을 사용하게 된다면 뒤의 조건은 확인하지 않기 때문에 뒤의 조건까지 확인하게 만들기 위해 and를 사용하여 다음과 같이 2번의 공격을 실행해보았다.

첫 번째 공격 구문은 test도 참, 뒤의 1<2의 조건도 참이 되어 where 조건문에서 true값을 반환하기 때문에 공격 구문임에도 already exists라고 이미 존재하는 아이디라고 응답했다.

두 번째는 test는 참 1>2는 거짓으로 where 조건문에서 false값이 반환되어 존재하지 않는 아이디라고 판단하여 아이디가 새로 만들어지는 것을 확인할 수 있다.

위의 결과를 통해 우리는 조건이 참과 거짓일 경우 서버에서 응답해주는 값이 달라 공격에 사용한 조건이 참인지 거짓인지 구별할 수 있다는 것을 알았다. 그럼 우리는 Blind SQL Injection 공격기법을 사용해 원하는 정보를 알아낼 수 있을 것 같다.

그럼 실습의 편의를 위해 코드에서 username과 password가 어디에서 관리하는지 살펴보자.

회원가입 시 sql_challenge_users테이블에서 userid를 조회하는 것을 확인할 수 있다.

로그인 할 때도 마찬가지로 sql_challenge_users 테이블에서 useridpassword를 조회하는 것을 확인할 수 있다.

정리하면 취약점이 있는 회원가입 페이지에서 sql_challenge_users에서 조회하는데 이 테이블에 password에 대한 정보가 있다. 따라서 회원가입 페이지에서 password를 알아내기 위한 Blind SQL Injection 공격을 하면 된다.

register 메소드Login메소드 의 코드는 각 링크에서 확인할 수 있다.

3. Blind SQL Injection 공격!

먼저 우리는 궁극적으로 tom의 password를 알아내야 한다. 먼저 password의 길이를 알아내기 위해서 다음과 같이 공격해 볼 수 있다.

결과를 보면 이미 존재한다는 아이디에서 조건문에 true가 되었음을 알 수 있다. 그럼 tom이라는 userid가 있고 password의 길이가 30 이하인 것을 확인할 수 있다.

이런 방법으로 tom' and length(password)>20--,tom' and length(password)=25--등의 공격을 통해 password의 길이를 알아낼 수 있다.

이렇게 범위를 줄여가며 확인해 본 결과 tom' and length(password)=23-- 의 결과가 역시 참의 값으로 길이가 23인것을 확인할 수 있다.

그럼 password가 23자리인 것을 확인했고 각 자리에 어떤 값인지 일일이 확인해야 한다. 이론적으로 tom' and ascii(substring(password,1,1))<100--,tom' and ascii(substring(password,2,1))<100-- 처럼 각 자리마다 비교에 알맞은 아스키 코드 값을 찾아내야 한다.

하지만 일일이 하기엔 너무 많은 시간이 소비되닌 brup suite의 Intruder기능을 사용해 보자. 먼저 tom' and ascii(substring(password,1,1))=100--를 넣어 실행해보자.

그리고 burp suite에서 Proxy-HTTP history에서 우리가 공격한 요청을 찾아서 다음과 같이 Intruder로 보낸다.

Intruder에서 우리는 아스키 코드 값으로 들어가 100에 대해서 Payload에 추가해준다.

그리고 여기에 아스키 코드표에서 문자로 들어가는 33~126의 값을 비교하기 위해 아래와 같이 설정한 후 공격을 실행한다.

실행 결과 길이가 가장 짧은 것의 응답을 보니 이미 존재한다는 뜻으로 where 조건문에서 true값을 반환 했음을 알 수 있고 이에 해당하는 값은 Request에서 116임을 확인할 수 있다. 아스키코드표를 참고하면 116은 t임을 확인할 수 있다.

이렇게 첫 번째 자리의 값을 알았다. 다음으로는 tom' and ascii(substring(password,1,1))=100-- 에서 substring의 값을 password,2,1,password,3,1,password,4,1...로 늘려가면서 각 자리의 값을 알아내야 한다.

이렇게 알아낸 비밀번호는 thisisasecretfortomonlytom아이디로 로그인을 하면 문제를 해결된다.

3. 정리

SQL Injection의 취약점은 전형적인 공격기법인 test', test' and 1<2--,union 으로 알아낼 수 있다.

취약점과 공격할 테이블의 구조를 알 수 있다면 Blind SQL Injection 공격기법을 활용할 수 있다. Blind SQL Injection 공격은 에러 기반, 시간 차, 응답 본문에 따라 데이터를 조금씩 유추할 수 있다.

보안을 위해 이런 취약점이 생기지 않도록 sql문을 하드하게 작성하는 것은 지양해야한다.

대신 다음과 같이 sql를 작성하는 경우 ?를 사용하여 쿼리 구조와 입력값을 분리하여 처리해야한다.

String sql = "SELECT * FROM users WHERE username = ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, userid);

또한 JPA과 같은 ORM프레임워크는 내부적으로 파라미터 바인딩을 지원하기 때문에 이를 사용해도 취약점을 개선할 수 있다.

profile
주주주주니어 개발자

0개의 댓글