탄핵안이 잘 표결되길 바라며 오늘도 즐겁게 문제를 풀어보자
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).
- Access-Control-Allow-Origin: *:
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. 코드 동작 흐름
- MongoDB 데이터베이스(mongoboard)와 연결.
- 게시판 모델(Board)을 로드.
- 정적 파일 경로와 CORS 정책을 설정.
- **라우터 파일(routes)**을 통해 API 엔드포인트 설정.
- index.html 파일을 홈 경로(/)에서 제공.
- 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에서 모든 게시글 데이터를 검색합니다.
동작 과정:
- MongoDB 데이터 검색:
- MongoBoard.find를 통해 데이터베이스에서 모든 게시글을 가져옵니다.
- 에러 처리:
- 에러 발생 시 HTTP 상태 코드 500과 에러 메시지를 반환.
- 데이터 가공:
- 데이터를 가공하여 **비밀 게시물(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);
});
});
기능:
- 특정 게시글의 상세 정보를 가져옵니다.
동작 과정:
- 게시글 검색:
- URL 매개변수(req.params.board_id)로 전달된 ID를 사용하여 특정 게시글 검색.
- MongoBoard.findOne을 사용.
- 에러 처리:
- 데이터베이스 에러 발생 시 HTTP 상태 코드 500과 에러 메시지 반환.
- 해당 ID의 게시글이 없는 경우 HTTP 상태 코드 404와 에러 메시지 반환.
- 데이터 반환:
- 요청한 게시글 데이터를 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});
});
});
기능:
- 새로운 게시글을 추가합니다.
동작 과정:
- 새 게시글 객체 생성:
- new MongoBoard()로 새로운 게시글 객체를 생성.
- 클라이언트에서 받은 데이터를 **요청 본문(req.body)**으로부터 가져와 저장:
- title: 게시글 제목.
- author: 작성자.
- body: 게시글 내용.
- secret: 비밀 여부. 기본값은 false.
- 데이터베이스 저장:
- board.save로 MongoDB에 게시글 저장.
- 에러 처리:
- 저장 실패 시 {result: false} 반환.
- 성공 시 응답:
- 저장 성공 시 {result: true} 반환.
3. 코드 전체 흐름
- 게시글 조회 (GET)
- /api/board: 전체 게시글 목록 조회.
- /api/board/:board_id: 특정 게시글 상세 조회.
- 게시글 생성 (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를 수동으로 추정하거나 탐색:
- 비밀 게시물의 _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 사용
- /api/board로 요청을 보낸 후 응답 데이터를 캡처.
- 응답 JSON 데이터에서 secret 속성이 true인 항목 확인.
- 해당 _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. 추가 확인
- 서버 로그를 확인하여 모든 게시물 데이터를 얻는 방법도 있습니다.
- 관리자 권한으로 서버에 접근 가능하다면 MongoDB 데이터베이스에 직접 접근:
mongo use mongoboard db.boards.find()
결론: 비밀 게시물을 확인하려면 다음 단계를 따라야 합니다:
- /api/board 요청으로 비밀 게시물의 존재 여부 확인.
- _id 추정 또는 반복 요청을 통해 비밀 게시물의 데이터를 추출.
- 서버에서 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
'Dreamhack > Dreamhack Wargame (Challenge)' 카테고리의 다른 글
[89] IT 비전공자 [dreamhack]Inject ME!!!문제 풀기 (0) | 2024.12.07 |
---|---|
[88] IT 비전공자 [dreamhack]Snowing!문제 풀기 (0) | 2024.12.06 |
[86] IT 비전공자 [dreamhack]basic_heap_overflow문제 풀기 (1) | 2024.12.04 |
[85] IT 비전공자 [dreamhack]random-test문제 풀기 (0) | 2024.12.03 |
[84] IT 비전공자 [dreamhack]what-is-my-ip문제 풀기 (0) | 2024.12.02 |