Dreamhack/Dreamhack Wargame (Challenge)

[87] IT 비전공자 [dreamhack]basic_heap_overflow문제 풀기

imaginefuture-1 2024. 12. 5. 09:03

 

탄핵안이 잘 표결되길 바라며 오늘도 즐겁게 문제를 풀어보자

 

 

 

몽고 DB!

 

 

 

FLAG를 눌렀더니 비밀문서라고 뜬다
홈페이지 모습이다
헬로우 문서를 누르니 WORLD!가 뜬다

 

몽고 월드~
굿 ~바이~

 

 

 

 

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)
});

 

 

 

 

 

 

 

 

 

이 코드는 Node.js 기반의 Express.js를 사용해 만든 웹 애플리케이션입니다. 애플리케이션은 MongoDB 데이터베이스와 연결되어 있으며, 간단한 게시판(Board) 기능을 제공하는 것으로 보입니다.


1. 주요 모듈

  • express: 웹 애플리케이션 프레임워크. 라우팅 및 서버 관리.
  • body-parser: 요청의 본문을 JSON이나 URL-encoded 형식으로 파싱.
  • mongoose: MongoDB와 상호작용하기 위한 ODM(Object Data Modeling) 라이브러리.
  • path: 파일 및 디렉터리 경로를 다루는 Node.js 내장 모듈.

2. 코드의 구성 요소

1) 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');
  • MongoDB와 연결을 설정합니다.
  • mongodb://localhost/mongoboard:
    • localhost는 MongoDB가 로컬에서 실행 중임을 나타냄.
    • mongoboard는 MongoDB 데이터베이스 이름.
  • 이벤트 처리:
    • db.on('error'): 연결 중 오류가 발생했을 때 처리.
    • db.once('open'): 데이터베이스 연결이 성공적으로 열렸을 때 처리.

2) 데이터 모델

var Board = require('./models/board');
  • Board:
    • 데이터베이스에서 사용할 게시판(Board) 모델을 불러옵니다.
    • 이 모델은 MongoDB 컬렉션과 매핑된 데이터 구조를 정의하는 파일(./models/board)에서 관리됩니다.

3) 애플리케이션 설정

app.use('/static', express.static(__dirname + '/public'));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
  • /static:
    • /public 디렉터리를 정적 파일 경로로 설정.
    • 예: HTML, CSS, JavaScript 같은 정적 파일 제공.
  • bodyParser:
    • 요청의 본문 데이터를 파싱.
    • urlencoded와 json 형식 지원.

4) CORS 설정

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();
});
  • CORS(Cross-Origin Resource Sharing) 정책을 설정.
    • Access-Control-Allow-Origin: *:
      • 모든 도메인에서의 요청을 허용.
    • Access-Control-Allow-Methods:
      • 허용하는 HTTP 메서드(POST, GET, OPTIONS, PUT).
    • Access-Control-Allow-Headers:
      • 요청 헤더에서 허용할 속성(Content-Type).

5) 라우팅 설정

var router = require(__dirname + '/routes')(app, Board);
app.get('/', function(req, res) {
    res.sendFile(path.join(__dirname + '/index.html'));
});
  • 라우터 설정:
    • /routes 디렉터리에 정의된 라우터를 불러오고, app과 Board를 전달.
  • 홈페이지 경로:
    • GET / 요청 시 index.html 파일을 반환.
    • 정적 HTML 파일 제공.

6) 서버 실행

var port = process.env.PORT || 8080;
var server = app.listen(port, function(){
 console.log("Express server has started on port " + port)
});
  • 포트 설정:
    • 환경 변수 PORT가 설정되어 있으면 이를 사용, 아니면 기본값 8080.
  • 서버 시작:
    • 애플리케이션을 지정된 포트에서 실행.
    • 성공적으로 시작되면 콘솔에 메시지를 출력.

3. 코드 동작 흐름

  1. MongoDB 데이터베이스(mongoboard)와 연결.
  2. 게시판 모델(Board)을 로드.
  3. 정적 파일 경로CORS 정책을 설정.
  4. **라우터 파일(routes)**을 통해 API 엔드포인트 설정.
  5. index.html 파일을 홈 경로(/)에서 제공.
  6. Express 서버를 실행하여 요청을 처리.

4. 확장 가능성

  • RESTful API:
    • routes 디렉터리에서 API 엔드포인트를 추가하고 게시판 데이터를 CRUD로 관리.
  • MongoDB 스키마 확장:
    • ./models/board 파일을 수정하여 게시판 데이터를 더욱 세분화 가능(예: 제목, 내용, 작성자).
  • 프론트엔드와의 통합:
    • public 디렉터리에 프론트엔드 파일을 저장하여 클라이언트와 서버를 통합.

5. 실행 전 확인

  • MongoDB 실행 여부 확인:
    sudo service mongod start
    
  • 의존성 설치:
    npm install express body-parser mongoose
    

이 코드는 게시판 애플리케이션의 기본 구조를 제공하며, 클라이언트와 서버 간의 통신 및 데이터베이스 연동이 가능합니다. 😊

 

 

 

 

 

 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});

        });
    });
}

 

 

이 소스코드는 MongoDB와 연동된 게시판 API의 라우트 설정을 정의하고 있습니다. Express.js를 기반으로 작성되었으며, 게시판 데이터를 처리하기 위한 다양한 엔드포인트가 포함되어 있습니다.


1. 기본 구조

  • module.exports: 다른 파일에서 이 라우트를 사용할 수 있도록 내보내는 역할.
  • app: Express 애플리케이션 객체.
  • MongoBoard: MongoDB와 연결된 게시판 데이터 모델.

2. 주요 API 설명

(1) GET /api/board

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
            };
        }));
    });
});

기능:

  • 전체 게시글 목록을 가져옵니다.
  • MongoBoard.find:
    • MongoDB에서 모든 게시글 데이터를 검색합니다.

동작 과정:

  1. MongoDB 데이터 검색:
    • MongoBoard.find를 통해 데이터베이스에서 모든 게시글을 가져옵니다.
  2. 에러 처리:
    • 에러 발생 시 HTTP 상태 코드 500과 에러 메시지를 반환.
  3. 데이터 가공:
    • 데이터를 가공하여 **비밀 게시물(secret)**인 경우 _id를 숨깁니다.
    • 반환 데이터 예시:
      [
        {
          "_id": null,
          "title": "비밀 게시물 제목",
          "author": "익명",
          "secret": true,
          "publish_date": "2024-12-01"
        },
        {
          "_id": "12345",
          "title": "공개 게시물 제목",
          "author": "홍길동",
          "secret": false,
          "publish_date": "2024-12-02"
        }
      ]
      

(2) GET /api/board/:board_id

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);
    });
});

기능:

  • 특정 게시글의 상세 정보를 가져옵니다.

동작 과정:

  1. 게시글 검색:
    • URL 매개변수(req.params.board_id)로 전달된 ID를 사용하여 특정 게시글 검색.
    • MongoBoard.findOne을 사용.
  2. 에러 처리:
    • 데이터베이스 에러 발생 시 HTTP 상태 코드 500과 에러 메시지 반환.
    • 해당 ID의 게시글이 없는 경우 HTTP 상태 코드 404와 에러 메시지 반환.
  3. 데이터 반환:
    • 요청한 게시글 데이터를 JSON 형식으로 반환.

(3) PUT /api/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});
    });
});

기능:

  • 새로운 게시글을 추가합니다.

동작 과정:

  1. 새 게시글 객체 생성:
    • new MongoBoard()로 새로운 게시글 객체를 생성.
    • 클라이언트에서 받은 데이터를 **요청 본문(req.body)**으로부터 가져와 저장:
      • title: 게시글 제목.
      • author: 작성자.
      • body: 게시글 내용.
      • secret: 비밀 여부. 기본값은 false.
  2. 데이터베이스 저장:
    • board.save로 MongoDB에 게시글 저장.
  3. 에러 처리:
    • 저장 실패 시 {result: false} 반환.
  4. 성공 시 응답:
    • 저장 성공 시 {result: true} 반환.

3. 코드 전체 흐름

  1. 게시글 조회 (GET)
    • /api/board: 전체 게시글 목록 조회.
    • /api/board/:board_id: 특정 게시글 상세 조회.
  2. 게시글 생성 (PUT)
    • /api/board: 클라이언트에서 전달받은 데이터를 기반으로 새로운 게시글 저장.

4. 추가 기능 확장 가능성

  • 게시글 수정:
    • POST /api/board/:board_id로 특정 게시글 수정.
  • 게시글 삭제:
    • DELETE /api/board/:board_id로 특정 게시글 삭제.
  • 유효성 검사:
    • 요청 데이터의 유효성을 검증하여 더 안전한 입력 처리 가능.

5. 요약

  • GET /api/board: 모든 게시글의 요약 정보 반환.
  • GET /api/board/:board_id: 특정 게시글의 상세 정보 반환.
  • PUT /api/board: 새로운 게시글 추가.

이 코드는 게시판 API의 기본적인 기능을 구현하며, 데이터 조회와 저장을 위한 핵심 기능을 제공합니다. 😊

 


 

 

 

여기서 flag가 비밀 게시물에 숨겨져있는데 어떻게 하면 볼 수 있을까?





비밀 게시물(Secret 게시물)에 숨겨진 FLAG를 보기 위해서는 다음과 같은 접근 방법을 사용할 수 있습니다.


1. 소스 코드 분석을 통한 데이터 접근

비밀 게시물은 /api/board 요청의 응답에서 secret 속성이 true로 설정된 게시물로 나타납니다. 그러나 소스 코드에서 다음과 같은 처리가 되어 있습니다:

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
    };
}));
  • 문제:
    • _id가 null로 변환되기 때문에, 비밀 게시물의 _id 값을 알 수 없게 되어 /api/board/:board_id 경로로 접근할 수 없습니다.

2. 공격 벡터: 직접 요청 조작

방법 1: 서버 로직 우회

/api/board/:board_id에서 비밀 게시물의 _id를 수동으로 추정하거나 탐색:

  1. 비밀 게시물의 _id 추정:
    • MongoDB는 _id 필드에 고유한 값을 자동으로 생성합니다. 이 값은 보통 24자리의 헥사코드입니다.
    • 예를 들어, _id의 형태는 다음과 같습니다:
      64fbe1214d1c41234d123abc
      
    • 추정 값을 사용하여 /api/board/:board_id 요청을 반복적으로 시도.
    • Python 스크립트 또는 Burp Suite의 Intruder를 이용하여 가능.

방법 2: 코드 수정

secret 속성을 서버에서 숨기지 않도록 소스 코드를 수정:

res.json(board);  // 데이터 전체를 반환
  • 수정 후 GET /api/board 요청으로 모든 데이터를 확인할 수 있습니다.

3. HTTP 요청 직접 조작

1) Burp Suite 사용

  1. /api/board로 요청을 보낸 후 응답 데이터를 캡처.
  2. 응답 JSON 데이터에서 secret 속성이 true인 항목 확인.
  3. 해당 _id 값을 추정하거나 수동으로 입력하여 /api/board/:board_id로 접근.

2) CURL 명령어 사용

비밀 게시물의 _id를 알고 있다고 가정하고 다음 명령어를 실행:

curl -X GET http://<SERVER_IP>:8080/api/board/<BOARD_ID>

4. 만약 추가 보호 메커니즘이 있을 경우

key 기반 인증 우회

  • 비밀 게시물의 _id를 조회하기 위해 인증 메커니즘이 존재한다면, 데이터의 key를 유출해야 할 수 있습니다.
  • MongoDB에서 key와 같은 인증 데이터를 사용하는 경우 이를 우회하기 위한 세부적인 공격이 필요할 수 있습니다.

5. 코드의 잠재적 취약점

데이터 노출

현재 서버 코드에서 secret 게시물의 _id가 null로 처리되고 있지만, 다른 필드(title, author, publish_date)는 여전히 표시됩니다. 이 정보를 사용하여 다음을 수행할 수 있습니다:

  • 비밀 게시물 추정:
    • 게시물의 title이나 author 필드를 통해 FLAG와 관련된 게시물을 유추.
  • 요청 반복:
    • 비밀 게시물의 _id를 추정하여 수동으로 요청을 보내는 방식.

6. 추가 확인

  1. 서버 로그를 확인하여 모든 게시물 데이터를 얻는 방법도 있습니다.
  2. 관리자 권한으로 서버에 접근 가능하다면 MongoDB 데이터베이스에 직접 접근:
    mongo
    use mongoboard
    db.boards.find()
    

결론: 비밀 게시물을 확인하려면 다음 단계를 따라야 합니다:

  1. /api/board 요청으로 비밀 게시물의 존재 여부 확인.
  2. _id 추정 또는 반복 요청을 통해 비밀 게시물의 데이터를 추출.
  3. 서버에서 FLAG를 확인.

이 과정에서 Burp Suite나 CURL과 같은 HTTP 도구를 활용하면 효율적으로 접근할 수 있습니다.

 


 

/api/board로 이동해보자

 

 

 

오 ㅋㅋㅋㅋㅋ

 

{"_id":null,"title":"FLAG","author":"admin","secret":true,"publish_date":"2024-12-04T23:30:03.083Z"},

 

flag게시물은 secret이 true라서 no가 null로 출력된걸 볼 수 있다

 

 

/api/board/:board_id로 이동해보자 

id는 hello world게시물 id를 이용할꺼다

http://host3.dreamhack.games:9036/api/board/6750e5f37935ff9de74908b9

 

 

 

해당 게시물의 도큐먼트가 조회되어 출력되고

body필드에 게시물의 내용이 뜨는 것을 볼 수 있다.

 

FLAG 게시물의 _id를 알아낼 수 있으면 위와 같이 접근해서 게시물의 내용을 읽을 수 있을 것이다.

 

FLAG 게시물의 _id를 알아내기 위해서는 _id가 생성되는 방식을 알아야 한다.

_id는 하나의 컬렉션 내에 있는 각각의 도큐먼트마다 고유한 값을 가지며

어떤 데이터형이든 가질 수 있지만 기본 데이터형은 ObjectId이다.

 

ObjectId에 대해서는 MongoDB에서 제공하는 Manual에 잘 설명되어 있다.

https://www.mongodb.com/docs/manual/reference/method/ObjectId/

출처 ㅣ https://studyforall.tistory.com/55

 

 

ObjectId는 총 12-byte이며

 

4-byte는 초 단위로 찍은 타임스탬프,

5-byte는 프로세스 단위로 생성한 랜덤 값,

3-byte는 증가하는 counter

로 구성되어 있다.

 

 

예를 들어 이렇다

618b62c96070fd2f29ed7bfa
timestamp를 뜻하는 자리

618b62c96070fd2f29ed7bfa
프로세스 당 한번 생성되는 임의의 값

618b62c96070fd2f29ed7bfa
counter를 뜻하는 자리

출처ㅣhttps://lrtk.tistory.com/58

 

공개되어 있는 게시물들의 _id를 보자
보기 좋게 4-byte, 5-byte, 3-byte로 끊어보자


6750e5f3 | 7935ff9de7 | 4908b9


6750e5f8 | 7935ff9de7 | 4908ba

null

6750e5fc | 7935ff9de7 | 4908bc

 

프로세스 당생성된 가운데 5byte값이 모두같다 flag 값도 가운데는 값을 것이다

 

7935ff9de7  ?

 

그리고 couner에 해당하는 마지막은 1byte씩 증가한다 flag역시 1씩 증가할꺼다

 

 7935ff9de7  4908bb

 

첫 4-byte는 타임스탬프이므로 publish_date와 함께 보면 계산할 수 있다

 

6750e5f3 | 7935ff9de7 | 4908b9

2024-12-04T23:29:55.066Z


6750e5f8 | 7935ff9de7 | 4908ba

2024-12-04T23:30:00.077Z

null

2024-12-04T23:30:03.083Z

6750e5fc | 7935ff9de7 | 4908bc


2024-12-04T23:30:04.088Z

 

첫번째 게시물과 두번째 게시물은 타임스탬프가 2만큼 차이나고,
publish_date는 2초만큼 차이나는 것을 볼 수 있다.
두번째 게시물과 네번째 게시물은 타임스탬프가 6만큼 차이나고,
publish_date도 6초만큼 차이나는 것을 볼 수 있다.

두번째 게시물과 FLAG 게시물은 publish_date가 2초만큼 차이나므로
타임스탬프도 2만큼 차이가 나서 FLAG의 타임스탬프는 64e43019일 것으로 예상할 수 있다.

출처 ㅣ https://studyforall.tistory.com/55

 

내 데이터에서는 

첫번째 게시물과 두번째 게시물은 타임스탬프가 5만큼 차이나고,
publish_date는 5초만큼 차이나는 것을 볼 수 있다.
두번째 게시물과 네번째 게시물은 타임스탬프가 4만큼 차이나고,
publish_date도 4초만큼 차이나는 것을 볼 수 있다.

두번째 게시물과 FLAG 게시물은 publish_date가 3초만큼 차이나므로
타임스탬프도 3만큼 차이가 나서 FLAG의 타임스탬프는 6750e5fb  일 것으로 예상할 수 있다.

 

 

6750e5fb   7935ff9de7  4908bb

 

http://host3.dreamhack.games:9036/api/board/6750e5fb7935ff9de74908bb

 

 

나왔다