DASCTF_GFCTF_2024

DASCTF X GFCTF 2024|四月开启第一局

Web

cool_index

先看看附件,逻辑并不复杂

import express from "express";
import jwt from "jsonwebtoken";
import cookieParser from "cookie-parser";
import crypto from "crypto";
const FLAG = process.env.DASFLAG || "DASCTF{fake_flag}";
const app = express();
app.use(express.json());
app.use(cookieParser());
app.use(express.static("static"));
app.set("view engine", "ejs");

const JWT_SECRET = crypto.randomBytes(64).toString("hex");

const articles = [
{
line1: "我还是在这里 我还是",
line2: "如约而至地出现了"
},
{
line1: "你们有成为更好的自己吗",
line2: "真的吗 那可太好了"
},
{
line1: "你知道吗 我经常说",
line2: "把更多的时间花在 CTF 上(?)"
},
{
line1: "这是一种信念感",
line2: "就像我出来那给你们"
},
{
line1: "我也希望你们能把更多时间花在热爱的事情上",
line2: "我是一个特别固执的人"
},
{
line1: "我从来不会在意别人跟我说什么",
line2: "让我去做以及怎么做 我不管"
},
{
line1: "如果 你也可以像我一样",
line2: "那我觉得 这件事情"
},
{
line1: "欢迎参加 DASCTF x GFCTF 2024!",
line2: FLAG,
},
];

app.get("/", (req, res) => {
const token = req.cookies.token;
if (token) {
try {
const decoded = jwt.verify(token, JWT_SECRET);
res.render("home", {
username: decoded.username,
subscription: decoded.subscription,
articles: articles,
});
} catch (error) {
res.clearCookie("token");
res.redirect("/register");
}
} else {
res.redirect("/register");
}
});

app.get("/register", (req, res) => {
res.render("register");
});

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: "未登录" });
}
});

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

一来就看到了ejs3.1.9版本,这个是有一个不算漏洞的漏洞的,因为作者不认为他是一个漏洞,如果有如下的渲染参数,则可以RCE:

app.post('/', (req, res) => {
let data = req.body;
res.render('echo.ejs', data);
});

可通过传参

settings[view%20options][closeDelimiter]=1")%3bprocess.mainModule.require('child_process').execSync('calc')%3b//

或者

settings: {
'view options': {
debug: true,
client: true,
escapeFunction: '(() => {});return process.mainModule.require("child_process").execSync("cat /app/flag*").toString()'
}
}

回到题目,有render()的地方只有一个,就是/路由的用户名那里,但是!!,用户名并不是像漏洞渲染的那样接受全部的参数的,而是经过了jet的验证后传出来的,所以这里不能使用这个打法了

接着往下看题目,也可以顺着题目逻辑来获取flag的,那就是通过/register路由来注册一个premium身份的账号,然后在/article路由才能选择id=7来获得flag,但是注册premium身份的账号要指导FLAG + JWT_SECRET,这又是不可能的事

最后也之恶能在传参index上下手了,可以看到index从请求体中传过来后,还会经过一个index = parseInt(index);才会取文章。并且在解析后还进一个判断Number.isNaN(index) || index > articles.length - 1,要求一定是数字并且不能溢出数组才通过

parseint()就是将字符串解析成数字,在文档中看到一个用法

image-20240422004941238

parseint之前几次数值的比较,例如decoded.subscription !== "premium" && index >= 7,我们去试试带着别的字母会不会影响比较:

image-20240422005314821

不影响,那就直接改参数传入7a之类的

image-20240422005446567

EasySignin

先看题目,可以注册登录,在注册的时候就能发现已经有admin账户了,随便注册一个账户进去可以看到有查看图片,修改密码,退出登录的几个功能

image-20240422141224808

改密码改包能修改admin的密码

image-20240422141357629

使用修改的密码来登录admin账户就能查看图片了,查看图片的url长这样

/getpicture.php?url=https://tvax3.sinaimg.cn//large/0072Vf1pgy1foxkjfmwohj31kw0w0x2o.jpg

传了一个url参数,可能存在ssrf的,尝试一下file协议

image-20240422141737892

base64解码是nonono,经过尝试flagfileetcdict等都会返回nonono,但是还有一个gopher能用,扫了一下目录有一个.DS_store文件,但是并没有什么用

经过多次尝试发现有3306端口是可以通过本地访问的gopher://127.0.0.1:3306

image-20240422144140760

gopher通过ssrf打mysql有工具生成payload的,猜测无需密码的mysql账户时root,尝试直接读文件

image-20240422145716799
image-20240422145810892

image-20240422145825407