LoginSignup
1
1

More than 1 year has passed since last update.

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

Posted at

これは?

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 (空ディレクトリ)

各ファイルの中身

.env
MONGO_INITDB_ROOT_USERNAME=<mongoDBrootのユーザー名>
MONGO_INITDB_ROOT_PASSWORD=<mongoDBrootのパスワード>
MONGO_INITDB_DATABASE=<mongoDBのデータベース名>
.gitignore
node_modules/    
data/    
secret_file/
Dockerfile
FROM node:12    
WORKDIR /app
RUN apt-get update && apt-get install -y vim    
RUN npm install
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
secret_file/db.env
DB_USER=<mongoDBのユーザー名(自分は.envと同じにした)>
DB_PASS=<mongoDBのパスワード(自分は.envと同じにした)>
DB_NAME=<mongoDBのデータベース名(自分は.envと同じにした)>
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]);
}
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 };
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はホストで実際に開いてるポート
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"
  }
}
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);
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;
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>
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;
        }
    });
});


構築する

とりあえず以下のsetup.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を以下のコマンドで作成する。

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.jsapp.get("/initUser")みたいな所をコメントアウトする。

一通りちゃんと動きそうなら、docker-compose upの際に自動でnode index.jsをしてWebサーバーを起動してほしいので、docker-compose.ymlcommand:のところを以下のように変更する。

docker-compose.yml
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とか出てきたらちゃんと連携されてる
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1