본문 바로가기

Dreamhack/웹해킹

[LEVEL 1] mongoboard

웹 서비스에 접속하면 화면 상단에는 MongoBoard라는 제목이 표시되며, 그 아래에는 no, Title, Author, publish_date로 구성된 게시판 형태의 목록이 나타납니다.

즉, 기본적인 게시판 구조의 관리 페이지 초기 화면임을 확인할 수 있습니다.

 

이번 화면은 글 작성 페이지로, 상단에는 Title, Author, Contents 입력란이 제공되며, 필요 시 Secret 옵션을 체크할 수도 있습니다.
하단에는 SaveCancel 버튼이 있어 글을 저장하거나 취소할 수 있습니다.

 

글을 등록하면 목록에 새로운 항목이 추가되며, no 부분에는 랜덤한 해시값이 자동 생성되는 것을 확인할 수 있습니다.

 

자세히 보기 위해서  코드를 살펴보겠습니다.

//app.js

var express     = require('express');
var app         = express();
var bodyParser  = require('body-parser');
var mongoose    = require('mongoose');
var path        = require('path');

// Connect to MongoDB
var db = mongoose.connection;
db.on('error', console.error);
db.once('open', function(){
    console.log("Connected to mongod server");
});
mongoose.connect('mongodb://localhost/mongoboard');

// model
var Board = require('./models/board');

// app Configure
app.use('/static', express.static(__dirname + '/public'));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

app.all('/*', function(req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT");
  res.header("Access-Control-Allow-Headers", "Content-Type");
  next();
});

// router
var router = require(__dirname + '/routes')(app, Board);
app.get('/', function(req, res) {
    res.sendFile(path.join(__dirname + '/index.html'));
});

// run
var port = process.env.PORT || 8080;
var server = app.listen(port, function(){
 console.log("Express server has started on port " + port)
});
//board.js

var mongoose = require('mongoose');
var Schema = mongoose.Schema;

var boardSchema = new Schema({
    title: {type:String, required: true},
    body: {type:String, required: true},
    author: {type:String, required: true},
    secret: {type:Boolean, default: false},
    publish_date: { type: Date, default: Date.now  }
}, {versionKey: false });

module.exports = mongoose.model('board', boardSchema);
//index.js

module.exports = function(app, MongoBoard){
    app.get('/api/board', function(req,res){
        MongoBoard.find(function(err, board){
            if(err) return res.status(500).send({error: 'database failure'});
            res.json(board.map(data => {
                return {
                    _id: data.secret?null:data._id,
                    title: data.title,
                    author: data.author,
                    secret: data.secret,
                    publish_date: data.publish_date
                }
            }));
        })
    });

    app.get('/api/board/:board_id', function(req, res){
        MongoBoard.findOne({_id: req.params.board_id}, function(err, board){
            if(err) return res.status(500).json({error: err});
            if(!board) return res.status(404).json({error: 'board not found'});
            res.json(board);
        })
    });

    app.put('/api/board', function(req, res){
        var board = new MongoBoard();
        board.title = req.body.title;
        board.author = req.body.author;
        board.body = req.body.body;
        board.secret = req.body.secret || false;

        board.save(function(err){
            if(err){
                console.error(err);
                res.json({result: false});
                return;
            }
            res.json({result: true});

        });
    });
}
  • 게시글 전체 조회 (GET /api/board)
    • 데이터베이스에 저장된 모든 글을 불러옵니다.
    • secret 옵션이 켜진 글은 _id가 null로 처리되어 외부에서 식별할 수 없게 반환됩니다.
  • 특정 글 조회 (GET /api/board/:board_id)
    • URL 파라미터로 전달된 _id 값을 기준으로 특정 글을 찾아 반환합니다.
    • 존재하지 않으면 404, DB 에러 발생 시 500을 반환합니다.
  • 글 작성 (PUT /api/board)
    • 요청 바디에서 title, author, body, secret 값을 받아 새 글을 생성합니다.
    • secret 값이 지정되지 않으면 기본적으로 false로 설정됩니다.
    • 저장 성공 시 {result: true}, 실패 시 {result: false}를 응답합니다.

index.js 코드를 분석하여 실행해본 결과, 아래와 같이 /api/board 요청 시 JSON 형식으로 게시글 목록이 반환되는 것을 확인할 수 있었습니다.

 

_id 값을 확인해본 결과, 이는 MongoDB에서 문서를 구분하기 위해 자동 생성하는 ObjectId임을 알 수 있었습니다.

🔎 Object ID이란?

ObjectId는 MongoDB에서 각 문서를 식별하기 위해 사용되는 고유하고 불변한 식별자입니다. 관계형 데이터베이스의 AUTO_INCREMENT 값과 유사한 역할을 하지만, 훨씬 더 복잡하고 강력한 구조를 가지고 있습니다.
_id 필드에 값을 직접 지정하지 않는 경우, MongoDB는 자동으로 ObjectId를 생성하여 부여합니다. 이 값은 단순한 무작위 문자열이 아니라 12바이트(24자리 16진수)로 구성된 체계적인 구조를 가지며, 분산 환경에서도 충돌 없이 고유성을 보장합니다.

ObjectId의 구성 요소

ObjectId는 총 12바이트로 이루어져 있으며, 4개의 주요 구성 요소가 결합되어 생성됩니다. 16진수 문자열로 표현하면 총 24글자가 됩니다.

  1. 타임스탬프 (4바이트): ObjectId의 첫 4바이트(16진수 8자리)는 문서가 생성된 시점의 Unix Timestamp를 나타냅니다. 이 덕분에 ObjectId를 보면 문서가 언제 생성되었는지 알 수 있으며, 별도의 인덱스 없이도 시간 순으로 정렬할 수 있습니다.
  2. 머신 식별자 (3바이트): 다음 3바이트(16진수 6자리)는 문서를 생성한 서버의 고유 ID를 나타냅니다. 여러 서버에 MongoDB가 분산되어 있을 때, 어떤 서버에서 문서를 생성했는지 식별하는 데 사용됩니다.
  3. 프로세스 ID (2바이트): 다음 2바이트(16진수 4자리)는 해당 서버 내에서 문서를 생성한 프로세스의 ID입니다.
  4. 카운터 (3바이트): 마지막 3바이트(16진수 6자리)는 동일한 초 내에서 생성된 문서의 순서를 나타내는 카운터입니다. 이 카운터 덕분에 동일한 서버의 동일한 프로세스에서 1초에 여러 문서가 생성되어도 각각 고유한 ObjectId를 갖게 됩니다.

예시: 68bc2bd280d577a0a7d115cc라는 ObjectId를 살펴보겠습니다.

68bc2bd280d577a0a7d115cc
  • 68bc2bd2 (앞 4바이트) → 생성 시점의 타임스탬프를 의미합니다.
  • 80d577a0a7 (중간 5바이트) → 머신 및 프로세스 정보를 바탕으로 한 랜덤 값입니다.
  • d115cc (마지막 3바이트) → 같은 초 안에서 증가하는 카운터 값입니다.

이처럼 ObjectId는 단순한 문자열이 아니라 시간 + 랜덤 값 + 카운터로 체계적으로 나뉘어 있음을 확인할 수 있습니다.

 

앞의 68bc2bd2 값을 16진수에서 10진수로 변환해 보면 1757162450이 나오며, 이는 유닉스 타임스탬프 값입니다. 이를 다시 변환기에 적용하면 2025-09-06 12:40:50이라는 날짜가 계산되고, 실제 게시판에 표시된 시간과 일치함을 확인할 수 있습니다.

 

그래서 저는 이 과정을 응용하여 FLAG 문서의 _id 값을 직접 추출해보려고 합니다. 앞에서 진행했던 변환 과정을 이번에는 역순으로 적용하여 _id를 얻어내는 방식을 시도하겠습니다.

 

2025-09-06T12:40:52.398Z라는 값을 타임스탬프 변환기에 넣어보면, 1757162452가 계산되는 것을 확인할 수 있습니다.

 

10진수 1757162452를 16진수로 변환한 결과 68BC2BD4라는 값이 나왔습니다. 이 값은 ObjectId의 앞 4바이트에 해당하는 것이라는 것을 알 수 있습니다.

 

중간 부분의 프로세스 ID 영역은 모든 ObjectId에서 동일하게 나타나는 것을 확인할 수 있습니다.

마지막 카운터 영역(6바이트 부분)은 게시판 데이터를 비교해 보면 뒤쪽 자리 수가 1씩 증가하는 형태를 띠고 있었으며, 이를 통해 최종적으로 FLAG 문서의 _id 값을 도출할 수 있었습니다.

 

따라서 최종적으로 도출된 FLAG 문서의 _id 값은 68BC2BD480d577a0a7d115ce임을 확인할 수 있습니다.

_id : 68BC2BD480d577a0a7d115ce

 

최종적으로 구한 _id를 API 요청에 직접 입력하여 호출한 결과, FLAG 문서를 조회할 수 있었고 그 안에서 플래그를 획득할 수 있었습니다.

'Dreamhack > 웹해킹' 카테고리의 다른 글

[LEVEL 1] csrf-2  (1) 2025.09.08
[LEVEL 1] xss-2  (0) 2025.09.07
[LEVEL 1] php-1  (0) 2025.09.06
[LEVEL 1] simple-ssti  (0) 2025.09.04
[LEVEL 1] image-storage  (0) 2025.09.04