【Node.js+Express+MongoDB on Docker】環境構築 2021 (CTFのNoSQLi練習用サーバー)

# これは？
MongoDBに対するNoSQL Injectionを題材としたCTFの問題サーバーを用意したく作成したので**実用には向いてません**。
m1z0r3というCTFチームの勉強会用に作成したので所々`m1z0r3`とか`mizore`とかあります。
https://qiita.com/sho_U/items/43f6483aac8ca45a12f6 の記事を参考に作らせていただきました。

<br/>

# 用意するファイルたち

## 全体像
```
├── .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 (空ディレクトリ)
```

## 各ファイルの中身
```ruby:.env
MONGO_INITDB_ROOT_USERNAME=<mongoDBのrootのユーザー名>
MONGO_INITDB_ROOT_PASSWORD=<mongoDBのrootのパスワード>
MONGO_INITDB_DATABASE=<mongoDBのデータベース名>
```
```ruby:.gitignore
node_modules/    
data/    
secret_file/
```
```dockerfile:Dockerfile
FROM node:12    
WORKDIR /app
RUN apt-get update && apt-get install -y vim    
RUN npm install
```
```yml:docker-compose.yml
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
```
```ruby:secret_file/db.env
DB_USER=<mongoDBのユーザー名(自分は.envと同じにした)>
DB_PASS=<mongoDBのパスワード(自分は.envと同じにした)>
DB_NAME=<mongoDBのデータベース名(自分は.envと同じにした)>
```
```ruby:secret_file/db_init/mongo_init_user.js
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]);
}
```
```javascript:challenge/controller/initUserController.js
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 };
```
```javascript:challenge/index.js
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はホストで実際に開いてるポート
```
```json:challenge/package.json
{
  "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"
  }
}
```
```javascript:challenge/models/User.js
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);
```
```javascript:challenge/routes/index.js
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;
```
```html:challenge/views/index.ejs
<!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>
```
```javascript:challenge/views/js/main.js
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;
		}
	});
});
```

<br/>

# 構築する
とりあえず以下の`setup.sh`を作る。  

```sh: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を以下のコマンドで作成する。

```shell
docker network create mizore-network
# ネットワーク名はdocker-compose.ymlで定義したやつ
```

次に、以下のコマンドを実行する。

```shell
docker-compose build
```

最後らへんちょろっと数行赤いエラーが出るが気にしない。そのまま以下のコマンドを実行。

```shell
docker-compose run app /bin/bash
```

これをするとコンテナが作成されてそのコンテナにbashで入ると思うので、`exit`で抜け出し、以下のコマンドを実行してそのコンテナIDをコピーする。

```shell
docker ps -a
```

コンテナIDがコピーできたら、先程の `setup.sh` を以下のようにして実行する。

```shell
./setup.sh <コピーしたコンテナID>
```

うまく行けば、最後の `docker-compose exec app bash`でbashに入れる。

<br/>

# mongoDBにデータをセット + Webサーバー立ち上げ
bashに入ったあと、`/app` ディレクトリにいると思うので、そのまま以下を実行。

```shell
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:`のところを以下のように変更する。

```yml:docker-compose.yml
app:
  # ... <略> ...
  command: bash

# 上記を下記に変更！！

app:
  # ... <略> ...
  command: node /app/index.js
```

<br/>

## mongoDBの中の覗き方
mongoDBとうまく連携できなかったので詰まったので、実際にデータがちゃんと入ってるのか確認するため、mongoDBの中を確認する方法が下記。
まず、mongoの方のコンテナに以下のようにして入る。

```shell
docker-compose exec mongo bash
```

これでmongoの方のコンテナのbashに入れるので、その後以下を実行する。

```shell
mongo <.envに書いたデータベース名> -u <.envに書いたユーザー名> -p
```

これを実行すると "Enter Password" 聞かれるので、`.env`で書いたパスワードを入れる。
無事入れたら、以下のコマンド実行すれば代々できてるかどうかわかる。

```shell
> show collections # usersとか表示される
> db.users.find() # これでフラグがパスワードのadminとか出てきたらちゃんと連携されてる
```