【使用Docker搭建Node.js+Express+MongoDB环境】2021年(用于CTF的NoSQLi练习服务器)
这是什么?
MongoDBに対するNoSQL Injectionを題材としたCTFの問題サーバーを用意したく作成したので実用には向いてません。
m1z0r3というCTFチームの勉強会用に作成したので所々m1z0r3とかmizoreとかあります。
https://qiita.com/sho_U/items/43f6483aac8ca45a12f6 の記事を参考に作らせていただきました。
准备的文件们
整体的图像
├── .env
├── .gitignore
├── Dockerfile
├── challenge
│ ├── controller
│ │ └── initUserController.js
│ ├── index.js
│ ├── models
│ │ └── User.js
│ ├── package.json
│ ├── routes
│ │ └── index.js
│ └── views
│ ├── index.ejs
│ └── js
│ └── main.js
├── data
│ └── db (空ディレクトリ)
├── docker-compose.yml
├── secret_file
│ ├── db.env
│ └── db_init
│ └── mongo_init_user.js
├── setup.sh
└── src (空ディレクトリ)
各个文件的内容
MONGO_INITDB_ROOT_USERNAME=<mongoDBのrootのユーザー名>
MONGO_INITDB_ROOT_PASSWORD=<mongoDBのrootのパスワード>
MONGO_INITDB_DATABASE=<mongoDBのデータベース名>
node_modules/
data/
secret_file/
FROM node:12
WORKDIR /app
RUN apt-get update && apt-get install -y vim
RUN npm install
version: '3'
services:
app:
build: ./
container_name: nosqli-web
ports:
- "3004:3000"
restart: always
working_dir: /app
tty: true
volumes:
- ./src:/app
env_file:
- ./secret_file/db.env
command: bash
networks:
- mizore-network
depends_on:
- mongo
mongo:
image: mongo:latest
container_name: nosqli-db
ports:
- "3005:27017"
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: ${MONGO_INITDB_ROOT_USERNAME}
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD}
MONGO_INITDB_DATABASE: ${MONGO_INITDB_DATABASE}
volumes:
- ./data/db:/data/db
- ./secret_file/db_init/:/docker-entrypoint-initdb.d
env_file:
- ./secret_file/db.env
command:
- mongod
networks:
- mizore-network
networks:
mizore-network: (このネットワーク名は適当に変える)
external: true
DB_USER=<mongoDBのユーザー名(自分は.envと同じにした)>
DB_PASS=<mongoDBのパスワード(自分は.envと同じにした)>
DB_NAME=<mongoDBのデータベース名(自分は.envと同じにした)>
let users = [
{
user: "<mongoDBのユーザー名(これも自分は.envと同じにした)>",
pwd: "<mongoDBのパスワード(これも自分は.envと同じにした)>",
roles: [
{
role: "dbOwner",
db: "<mongoDBのデータベース名(これも自分は.envと同じにした)>"
}
]
}
];
for (let i = 0, length = users.length; i < length; ++i) {
db.createUser(users[i]);
}
const InitUser = require('../models/User');
const user = () => {
let initUser = new InitUser({
username: "admin",
password: "m1z0r3{...flag....}"
})
initUser.save((error, data) => {
if (error) {
console.log(error);
}
console.log(data);
})
let initUser2 = new InitUser({
username: "admin",
password: "mmmmmmimmmmmmm_mm_mmmmmi"
})
initUser2.save((error, data) => {
if (error) {
console.log(error);
}
console.log(data);
})
let initUser3 = new InitUser({
username: "test",
password: "passwd"
})
initUser3.save((error, data) => {
if (error) {
console.log(error);
}
console.log(data);
})
}
module.exports = { user };
const express = require("express");
const app = express();
const bodyParser = require("body-parser");
const routes = require("./routes");
const mongoose = require("mongoose");
mongoose.connect(
`mongodb://${process.env.DB_USER}:${process.env.DB_PASS}@mongo:27017/<先程のmongoDBのデータベース名>`,
{ useNewUrlParser: true, useUnifiedTopology: true }
);
// "@mongo" のmongoはdocker-compose.ymlの "mongo:" に対応しているのでlocalhostとかじゃできないので注意
// 後ポートの27017はコンテナ側のポート(":"で区切った時の右の方)
// { useNewUrlParser: true, useUnifiedTopology: true } の部分はこれを丸コピ(他のだとうまく行かないという記事をみた)
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
extended: true
}));
app.set('view engine', "ejs");
app.use(routes);
// 最初 /initUser にアクセスしてmongoDBにユーザー(データとしてのユーザー、mongoDBの認証関連のユーザーじゃない)のデータを入れる。
const initUserController = require("./controller/initUserController");
app.get("/initUser", initUserController.user);
app.all("*", (req, res) => {
return res.status(404).send({
message: '404 page not found'
});
});
app.listen(3000, () => console.log("Listening on port 3004"));
// 3000はコンテナの方のポートで、3004はホストで実際に開いてるポート
{
"name": "mizore-app",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "nodemon ./bin/www"
},
"dependencies": {
"bcrypt": "^5.0.0",
"body-parser": "^1.19.0",
"connect-flash": "^0.1.1",
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"ejs": "^3.1.5",
"express": "~4.16.1",
"express-ejs-layouts": "^2.5.0",
"express-generator": "^4.16.1",
"express-session": "^1.17.1",
"express-validator": "^6.7.0",
"http-errors": "~1.6.3",
"http-status-codes": "^2.1.4",
"method-override": "^3.0.0",
"mongoose": "^5.11.9",
"morgan": "~1.9.1",
"nodemon": "^2.0.6",
"passport": "^0.4.1",
"passport-local-mongoose": "^6.0.1"
}
}
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
let User = new Schema({
username: {
type: String
},
password: {
type: String
}
}, {
collection: 'users'
});
module.exports = mongoose.model("User", User);
var express = require('express');
var router = express.Router();
var User = require("../models/User");
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
router.post("/login", (req, res) => {
let { username, password } = req.body;
if(username && password) {
return User.find({
username, password
})
.then((user) => {
if(user.length == 1) {
return res.json({logged: 1, message: `Login Successful, welcome back ${user[0].username} : ${user[0].password}` });
} else {
return res.json({logged: 0, message: `Login Failed`});
}
})
.catch(() => res.json({ message: "Something went wrong" }));
}
return res.json({ message: "Invalid username or password" });
});
module.exports = router;
<!DOCTYPE html>
<html>
<head>
<title>NoSQLi Practice</title>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<h1>NoSQLi Practice</h1>
<p>Search User here</p>
<form action="/login" method="post">
<label for="username">username:</label>
<input type="text" id="username" name="username"><br/>
<label for="password">password:</label>
<input type="text" id="password" name="password"><br/>
<input type="submit" value="login">
</form>
</body>
</html>
const login = document.getElementById("login");
const response = document.getElementById("response");
login.addEventListener("submit", e => {
e.preventDefault();
fetch("/login", {
method: "POST",
body: new URLSearchParams(new FormData(e.target))
})
.then(resp => resp.json())
.then(data => {
if(data.logged) {
login.remove();
response.innerHTML = data.message;
} else {
response.innerHTML = data.message;
}
});
});
构建
暂时先创建下面的setup.sh。
# usage: ./setup.sh <containerID>
# sudo rm -rf data/db/* && sudo rm -rf src/*
# dc build
# docker network create mizore-network
# dc run app /bin/bash
docker restart $1 && \
docker exec $1 npx express-generator -f --view=ejs && \
docker cp ./challenge/index.js $1:/app/ && echo "[OK] index.js" && \
docker cp ./challenge/package.json $1:/app/ && echo "[OK] package.json" && \
docker exec $1 mkdir /app/models && \
docker cp ./challenge/models/User.js $1:/app/models/ && echo "[OK] models/User.js" && \
docker cp ./challenge/routes/index.js $1:/app/routes/ && echo "[OK] routes/index.js" && \
docker cp ./challenge/views/index.ejs $1:/app/views/ && echo "[OK] views/index.ejs" && \
docker exec $1 mkdir /app/views/js && \
docker cp ./challenge/views/js/main.js $1:/app/views/js/ && echo "[OK] views/js/main.js" && \
docker exec $1 mkdir /app/controller && \
docker cp ./challenge/controller/initUserController.js $1:/app/controller/ && echo "[OK] controller/initUserController.js" && \
docker exec $1 npm install && \
docker-compose up -d && \
docker stop $1 && docker rm $1 && \
docker-compose exec app bash
# docker-compose exec app node /app/index.js
那些注释掉的部分可以保持注释掉的状态。
在运行 chmod +x setup.sh 后,首先使用以下命令创建docker网络。
docker network create mizore-network
# ネットワーク名はdocker-compose.ymlで定義したやつ
接下来,执行以下指令。
docker-compose build
不要在意的是,在最后几行会出现一些红色错误。请直接执行以下命令。
docker-compose run app /bin/bash
执行这个命令会创建一个容器,然后通过bash进入该容器。在容器内使用exit命令退出,然后执行以下命令来复制该容器的ID。
docker ps -a
如果可以复制容器ID,请按以下方式执行前面的 setup.sh。
./setup.sh <コピーしたコンテナID>
如果一切顺利,最后可以通过 docker-compose exec app bash 命令进入bash。
将数据存入MongoDB + 启动Web服务器
想必你已经进入了bash,并且在/app目录下,那么可以直接执行以下操作。
node index.js
如果没有错误的话,应该会显示像 console.log 一样的 “Listening on 3004″,然后访问 http://localhost:3004 来确认网站是否正常显示。
在此阶段,由于MongoDB中还没有任何数据,所以无论输入什么样的用户名/密码,都应该会显示”登录失败”。当网站正确显示后,接下来访问 http://localhost:3004/initUser ,将在MongoDB中插入一个类似于管理员密码为admin的用户数据(如果在执行node index.js命令时控制台显示了用户数据,则表明数据已经成功插入)。
在确认数据已经被录入后,如果再次访问/initUser,会导致重复添加数据,因此请将challenge/index.js中的app.get(“/initUser”)部分注释掉。
如果一切都正常工作,希望在执行docker-compose up时自动启动Web服务器,执行命令为node index.js,请将docker-compose.yml中的command:修改如下。
app:
# ... <略> ...
command: bash
# 上記を下記に変更!!
app:
# ... <略> ...
command: node /app/index.js
在MongoDB中的查看方式
由于无法成功地与MongoDB连接而陷入困境,为了确认数据是否正确输入,以下是确认MongoDB中数据的方法。
首先,按照以下方式进入mongo容器。
docker-compose exec mongo bash
现在可以进入Mongo容器的Bash,然后执行以下操作。
mongo <.envに書いたデータベース名> -u <.envに書いたユーザー名> -p
当执行此操作时,会提示输入密码,请将在.env文件中编写的密码输入。
如果成功输入后,只需执行以下命令,即可确认是否成功连接。
> show collections # usersとか表示される
> db.users.find() # これでフラグがパスワードのadminとか出てきたらちゃんと連携されてる