모의해킹 실습 - WebGoat / SQL Injection (advanced)

이태현·2026년 3월 11일

모의해킹

목록 보기
8/19
post-thumbnail

WebGoat / SQL Injection (advanced)

첫 번째 문제 - Union

첫 번째 문제를 번역기로 해석해 보니
시도해 보세요! 다른 테이블에서 데이터 가져오기
아래 입력 필드는 사용자의 성을 기준으로 데이터를 가져오는 데 사용됩니다.
이 테이블의 이름은 'user_data'입니다:

CREATE TABLE user_data (userid int not null,
                        first_name varchar(20),
                        last_name varchar(20),
                        cc_number varchar(30),
                        cc_type varchar(10),
                        cookie varchar(20),
                        login_count int);

실험을 통해 이 필드가 SQL 주입에 취약하다는 것을 발견했습니다. 이제 그 지식을 사용하여 다른 테이블의 내용을 얻고자 합니다.
데이터를 가져오려는 테이블은 다음과 같습니다:

CREATE TABLE user_system_data (userid int not null primary key,
			                   user_name varchar(12),
			                   password varchar(10),
			                   cookie varchar(30));

요약
1. 2개의 테이블이 존재한다.
2. 기존 테이블을 사용하여 다른 테이블의 데이터를 가져와라.

문제를 확인해 보니
6.a) 테이블에서 모든 데이터 검색
6.b) 알아냈을 때... 데이브의 비밀번호는 무엇인가요?
테이블의 전체 데이터를 조회하고 거기서 Dave의 비밀번호를 알아내는 게 이번 문제의 핵심 같습니다. 이번 문제를 해결하는 데 있어서 2가지 방법이 있는데, 첫 번째는 Union을 이용한 방법이고 두 번째는 새로운 SQL 문을 추가해서 풀어보라는 것 같습니다.

Step 1 - Dave 입력해보기

Dave의 비밀번호를 구하라고 했으니 한번 Dave를 넣어서 입력해 보았습니다. 질의를 확인해 보니 싱글 쿼터를 사용하는 걸로 보아 문자열 범위를 벗어나는 공격을 시도해 보면 좋을 거 같습니다.

Step 2 - 문자열 범위 벗어나기

문자열 범위를 벗어나 조회하는 데는 성공했지만, Dave는 존재하지 않았습니다. 아마 user_system_data 테이블에 있는 거 같습니다. 그렇다면 다른 테이블을 조회하기 위해서는 다른 테이블을 조회할 수 있는 질의를 작성해야 합니다. 문제에서 알려주던 방법인 Union이나 SQL 질의를 추가할 방법을 써봐야 할 거 같습니다.

결과 - Union

Query : 123' union select userid, user_name, password, null, null, cookie, null from user_system_data--
Union은 두 개 이상의 SELECT문의 결과를 하나로 합쳐서 중복된 행은 제외하고 고유한 값만 보여줍니다.
중요
Union을 사용할 때 반드시 지켜야 할 사항이 있습니다.
1. 컬럼의 갯수
2. 컬럼의 타입
3. 컬럼의 순서
ex)
1. user_data -> 7개의 컬럼, 각각의 타입들이 int타입, varchar타입을 가지고, userid~login_count까지의 순서를 지키고 있습니다.
2. user_system_data -> 4개의 컬럼, 각각의 타입들이 int타입, varchar타입을 가지고, userid~cookie까지의 순서를 지키고 있습니다.
여기서 우리는 Union을 사용하기 위해서 컬럼의 개수, 타입, 순서를 맞추는 작업을 합니다. 위에 제가 작성한 질의를 확인해 보면 중간에 null 값이 포함되어 있는데 이게 바로 컬럼갯수와 컬럼타입을 맞추기 위해서 null 값을 사용한 겁니다. null은 타입이 존재하지 않기 때문에 user_data 테이블과 user_system_data 테이블의 컬럼갯수와, 컬럼타입을 알맞게 채워 넣을 수 있습니다. 이렇게 해서 두 테이블의 컬럼갯수, 컬럼타입과 컬럼순서까지 동일하게 맞춰서 조회하면 user_data 테이블과 user_system_data 테이블이 합쳐지고 중복을 제외한 고유한 값만 반환되면서 이번 문제를 해결할 수 있습니다.

결과 - query chaining

Query : 123';select * from user_system_data--
다른 방법으로는 앞에서 공부했던 방법인 쿼리 체이닝(query chaining)을 사용하는 방법입니다. 문제에서도 제시했듯이 새로운 질의를 추가하라 했으니, 기존의 방식대로 공격을 시도하면 문제를 해결할 수 있습니다.

두 번째 문제 - Blind

두 번째 문제를 번역기로 해석해 보니
Blind SQL injection
블라인드 SQL 인젝션은 데이터베이스에 참 또는 거짓 질문을 하고 애플리케이션의 응답에 따라 답을 결정하는 SQL 인젝션 공격의 일종입니다. 이 공격은 웹 애플리케이션이 일반적인 오류 메시지를 표시하도록 구성되어 있지만 SQL 인젝션에 취약한 코드를 완화하지 않은 경우에 자주 사용됩니다.

Difference
먼저 일반 SQL 인젝션과 블라인드 SQL 인젝션의 차이점부터 시작하겠습니다. 일반 SQL 인젝션에서는 데이터베이스의 오류 메시지가 표시되며 쿼리가 어떻게 작동하는지 알 수 있는 충분한 정보를 제공합니다. 또는 UNION 기반 SQL 인젝션의 경우 애플리케이션이 웹 페이지에 직접 정보를 반영하지 않습니다. 따라서 아무것도 표시되지 않는 경우 참 또는 거짓 문장을 기반으로 데이터베이스에 질문을 시작해야 합니다. 그렇기 때문에 블라인드 SQL 인젝션은 악용하기가 훨씬 더 어렵습니다.

블라인드 SQL 주입에는 여러 가지 유형이 있습니다: 콘텐츠 기반 SQL 주입과 시간 기반 SQL 주입입니다.

요약
1. 실제 운영되는 웹 서비스들은 질의 실행 결과가 나오는 경우는 사실 없다.
2. 서버의 응답을 통해 질의의 참/거짓을 구분하여 공격을 시도한다.
3. 대표적인 blind SQL injection

  • 오류를 참고해 참/거짓을 구분하는 Error based blind SQL injection
  • 쿼리 실행에 소요되는 시간 차를 기준으로 구분하는 Time based blind SQL injection
  • 서버의 응답 본문을 기준으로 구분하는 Content based blind SQL injection

문제를 확인해 보니
우리가 배운 SQL injection 공격에 모든 것을 동원해서 Tom으로 로그인에 성공해야 하는 문제 같습니다.

Step 1 - 로그인에서 임의의 값 입력

먼저 우리가 배웠던 공격을 시도해 보겠습니다. 싱글 쿼터를 사용해서 질의문에 오류가 발생하는지 확인해 봤지만, 오류는 나오지 않고 로그인에 실패했다는 메시지만 보입니다. 해당 로그인 부분에는 SQL injection 취약점이 존재하지 않다는 걸 알 수 있습니다.

Step 2 - 회원가입에서 임의의 값 입력

1 - 회원가입 진행

먼저 임의의 회원으로 회원가입을 진행하였습니다.

그런 다음 다시 한번 더 같은 이름으로 회원가입을 진행해 봤습니다. 메시지를 통해 이미 가입이 된 회원이 있음을 알 수 있습니다.

2 - 회원가입에서 공격 시도해 보기

이번에는 싱글 쿼터를 사용하여 회원가입을 시도해 봤습니다. 그런데 이번에는 메시지에 다른 문구가 나온 걸 확인할 수 있습니다. 가입 성공이나 회원중복 메시지가 아닌 알 수 없는 메시지가 나온걸 볼 수 있는데요. 추측이지만 싱글 쿼터로 인해서 정상적인 질의 부분이 실행되지 않아 생기는 오류로 생각됩니다. 그렇다면 취약점이 존재한다고 생각할 수 있습니다. 하지만 질의를 알 수 없으니 우리는 다른 공격 방법인 Blind SQL injection 공격을 생각해 볼 수 있겠습니다.

Step 3 - Blind SQL injection 시도해 보기

기존의 공격과 다른 점이 있다면 바로 OR 연산자를 사용하지 않고 AND 연산자를 사용했습니다. AND를 사용한 이유는 123계정이 이미 존재하므로 앞에 조건이 무조건 참이기 때문에 OR를 사용하면 뒤에는 어떤 값을 넣어도 참이 되기 때문에 AND를 사용해야 뒤에 문장이 참/거짓 인지를 확인할 수 있으므로 AND를 사용했습니다. 즉, 1<2라는 조건을 사용함으로써 무조건 참이 되는 경우이므로 결과적으로 모두 참이 되므로 123계정을 조회하게 되는 겁니다. 결국 실제 계정 이름이 아닌데도 이미 가입된 계정이라는 메시지를 확인할 수 있게 되어 확실히 공격이 가능하다는 걸 알 수 있게 되는 과정이었습니다.

전체 조건이 모두 참일 때는 계정이 이미 존재한다는 메시지를 확인하였습니다. 이번에는 조건을 반대로 주어서 1>2 조건일 때를 확인해 보겠습니다. 결과를 확인해 보니 계정이 새로 생성된 것을 확인할 수 있습니다.

요약
1. 싱글 쿼터를 사용해 오류를 발견해서 공격이 가능할 수 있다는 추측을 함
2. 확실히 알기 위해서 질의에 조건을 주어서 참/거짓을 확인할 수 있었다.

Step 4 - Table, Column 알아내기

1 - Table 알아내기

코드 경로 :
vi src/main/java/org/owasp/webgoat/lessons/sqlinjection/advanced/SqlInjectionChallenge.java

웹 경로 : https://github.com/WebGoat/WebGoat/tree/v2023.3/src/main/java/org/owasp/webgoat/lessons/sqlinjection/advanced/SqlInjectionChallenge.java
우리는 SQL injection 공격을 할 때 항상 테이블명과 컬럼명을 알아야 했습니다. 하지만 지금 상태에서는 알아낼 방법이 없습니다. 물론 다른 방법이 있지만 그건 현재 문제에서 요구하는 방법이 아닙니다. 그래서 이번에는 예외로 어쩔 수 없이 직접 코드를 확인해서 알아내야 합니다.

회원가입을 구성하는 해당 코드를 살펴보면 sql_challenge_users 테이블에서 userid로 조회하는걸 볼 수 있습니다. 즉, 우리가 알아내고자 했던 테이블명이 바로 sql_challenge_users 입니다. 이제 알아야 할 것은 컬럼명입니다. 같은 방식으로 진행해 보겠습니다.

2 - Column 알아내기

코드 경로 :
vi src/main/java/org/owasp/webgoat/lessons/sqlinjection/advanced/SqlInjectionChallengeLogin.java

웹 경로 : https://github.com/WebGoat/WebGoat/blob/v2023.3/src/main/java/org/owasp/webgoat/lessons/sqlinjection/advanced/SqlInjectionChallengeLogin.java
로그인 부분을 확인해 보니 같은 테이블을 사용하고 있고 컬럼명은 password 라고 나와 있습니다. 이렇게 해서 우리는 sql_challenge_users 테이블과 password컬럼의 이름을 알게 되었으니 이제 본격적으로 공격을 시도해 봐야겠죠?

Step 5 - 문자열 길이 알아내기

Blind SQL injection은 데이터를 하나씩 잘라서 특정 값과 비교하여 알아냅니다. 그 전에 알아야 할 것이 바로 문자열의 길이입니다. 문자열의 길이(비밀번호)를 알아야 몇 번을 반복할지 알 수 있기 때문입니다. 문자열 길이를 알기 위해서는 SQL에 있는 내장함수를 사용하면 됩니다. 물론 DB에 따라 함수명이 다를 수 있습니다. 보통 LENGTH() 또는 LEN()을 사용합니다.
ex) LENGTH('문자열')
1. LENGTH('가나다') → 3

1 - 문자열길이가 30보다 작은지 확인해 보기

해당 공격은 password컬럼에 존재하는 Tom의 비밀번호 길이를 구하기 위해 위와 같이 작성하였습니다. 그 결과로 비밀번호 길이가 30 미만이라는 것을 알 수 있었습니다. 그렇다면 좀 더 숫자를 줄여서 확인해 보겠습니다.

2 - 22보다는 크다

어느덧 숫자를 줄여나가다 보니 22까지 내려왔습니다. 그런데 이번에는 회원가입이 성공했다는 메시지를 받았습니다. 그 말뜻은 뒤에 조건이 거짓이 되면서 22보다는 크다는 걸 알 수 있습니다.

결과

드디어 Tom의 비밀번호 길이를 구하였습니다. Tom의 문자열 길이는 23자리입니다.

Step 6 - 데이터 변환

이제 문자열 길이도 알았으니 각 위치에 맞는 데이터를 찾아서 변환해 주면 됩니다. 이번에도 마찬가지로 내장함수를 사용하여 알아낼 겁니다. 첫 번째는 문자열을 자르는 함수이고 두 번째는 ASCII 코드로 변환하는 함수를 사용할 겁니다.
ex) SUBSTRING(문자열, 시작 위치, 길이)
1. SUBSTRING('Hello World', 1, 5) → 'Hello'
ex) ASCII('A')
1. ASCII('A') -> 65

메시지를 통해 계정이 생성되었으니, 뒤에 문장이 거짓이므로 첫 번째 자리의 값이 100보다 큰 숫자라는 걸 알 수 있습니다. 공격을 효율적으로 하기 위해서 Burp Suite의 Intruder 기능을 사용하여 공격해 보겠습니다.

Step 7 - Intruder 공격 자동화

126값에서 이미 존재하는 계정이 있다고 나온 걸 보니 해당 값이 첫 번째 자리의 아스키코드 값인 걸 알 수 있습니다. 해당 126값은 아스키코드로 변환하면 't'라는 문자가 나옵니다.

결과

반복되는 작업은 생략하였습니다. Tom의 비밀번호는 thisisasecretfortomonly였습니다. 알아낸 비밀번호를 입력하면 문제를 해결할 수 있습니다.

마무리

이번 실습은 실제 웹서비스 같은 상태를 예로 들어서 질의를 모르는 상태에서 가능한 공격을 진행하는 Blind SQL injection 공격에 대해서 진행하였습니다.

profile
이해하고 분석하고 지배한다

0개의 댓글