これは?
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 networkを以下のコマンドで作成する。
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の中に何もデータが入っていない状態なので、どんなusername/passwordを入れても "Login Failed" になるはず。ちゃんとサイトが表示されたら、今度は http://localhost:3004/initUser にアクセスしてmongoDBにフラグがパスワードのadminとかのユーザーのデータを入れる(node index.js
したコンソールにユーザーのデータが表示されたらちゃんとデータが入ったということ)。
その後、/initUser
にまたアクセスしちゃうと重複してデータが追加されちゃうので、一回データが入ってることを確認したら、challenge/index.js
のapp.get("/initUser")
みたいな所をコメントアウトする。
一通りちゃんと動きそうなら、docker-compose up
の際に自動でnode index.js
をしてWebサーバーを起動してほしいので、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
これを実行すると "Enter Password" 聞かれるので、.env
で書いたパスワードを入れる。
無事入れたら、以下のコマンド実行すれば代々できてるかどうかわかる。
> show collections # usersとか表示される
> db.users.find() # これでフラグがパスワードのadminとか出てきたらちゃんと連携されてる