【使用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=<mongoDBrootのユーザー名>
MONGO_INITDB_ROOT_PASSWORD=<mongoDBrootのパスワード>
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とか出てきたらちゃんと連携されてる
广告
将在 10 秒后关闭
bannerAds