
# تحدي CTF يحاكي CVE-2025-31137 في Remix/React Router Express. تعلّم كيفية استغلال ثغرة من جانب الخادم للعثور على علم المسؤول.
بعد قراءة مقال @zhero___ في منشوره الشخصي على مدونته، قررت بناء مسابقة CTF هذه لتعلم كيفية عمل الأشياء، وبعد ذلك قررت مشاركتها مع أي شخص يريد تعلم كيفية استغلال هذه الثغرة. حاولت إنشاء المسابقة باستخدام الإصدار [email protected]، لكن عندما فحصت @remix-run/express لاحظت أنهم قاموا بتصحيح الكود، فقمت بنسخ ولصق الكود القابل للاستغلال من مستودعهم على GitHub، لذا يجب عليك تغيير كود حزمة @remix-run/express كما سأوضح أدناه.
يجب عليك العثور على العلم في صفحة المشرف؛ وبقية أجزاء التطبيق غير فعالة، لذا ركّز فقط على صفحة المشرف واعثر على العلم. كما أنني أوصيك بقراءة هذا المقال الرائع "https://zhero-web-sec.github.io/research-and-things/react-router-and-the-remixed-path" وتعلم كيف اكتشف الباحث هذه الثغرة، ويمكنك أيضًا قراءة الكود لتفهم كيف تعمل الأمور.
اتبع هذه الخطوات لإعداد المشروع:
git clone https://github.com/pouriam23/vulnerability-in-Remix-React-Router-CVE-2025-31137-.git
cd vulnerability-in-Remix-React-Router-CVE-2025-31137-
تأكد من تثبيت pnpm لديك، ثم نفّذ:
pnpm install
استبدل محتويات الملف التالي:
/my-remix-app/node_modules/@remix-run/express/dist/server.js
بالكود أدناه وأعد تسمية الملف إلى:
server.ts
// IDK why this is needed when it's in the tsconfig..........
// YAY PROJECT REFERENCES!
/// <reference lib="DOM.Iterable" />
import type * as express from "express";
import type { AppLoadContext, ServerBuild } from "@remix-run/node";
import {
createRequestHandler as createRemixRequestHandler,
createReadableStreamFromReadable,
writeReadableStreamToWritable,
} from "@remix-run/node";
/**
* A function that returns the value to use as `context` in route `loader` and
* `action` functions.
*
* You can think of this as an escape hatch that allows you to pass
* environment/platform-specific values through to your loader/action, such as
* values that are generated by Express middleware like `req.session`.
*/
export type GetLoadContextFunction = (
req: express.Request,
res: express.Response
) => Promise<AppLoadContext> | AppLoadContext;
export type RequestHandler = (
req: express.Request,
res: express.Response,
next: express.NextFunction
) => Promise<void>;
/**
* Returns a request handler for Express that serves the response using Remix.
*/
export function createRequestHandler({
build,
getLoadContext,
mode = process.env.NODE_ENV,
}: {
build: ServerBuild | (() => Promise<ServerBuild>);
getLoadContext?: GetLoadContextFunction;
mode?: string;
}): RequestHandler {
let handleRequest = createRemixRequestHandler(build, mode);
return async (
req: express.Request,
res: express.Response,
next: express.NextFunction
) => {
try {
let request = createRemixRequest(req, res);
let loadContext = await getLoadContext?.(req, res);
let response = await handleRequest(request, loadContext);
await sendRemixResponse(res, response);
} catch (error: unknown) {
next(error);
}
};
}
export function createRemixHeaders(
requestHeaders: express.Request["headers"]
): Headers {
let headers = new Headers();
for (let [key, values] of Object.entries(requestHeaders)) {
if (values) {
if (Array.isArray(values)) {
for (let value of values) {
headers.append(key, value);
}
} else {
headers.set(key, values);
}
}
}
return headers;
}
export function createRemixRequest(
req: express.Request,
res: express.Response
): Request {
let [, hostnamePort] = req.get("X-Forwarded-Host")?.split(":") ?? [];
let [, hostPort] = req.get("host")?.split(":") ?? [];
let port = hostnamePort || hostPort;
let resolvedHost = `${req.hostname}${port ? `:${port}` : ""}`;
let url = new URL(`${req.protocol}://${resolvedHost}${req.originalUrl}`);
let controller: AbortController | null = new AbortController();
let init: RequestInit = {
method: req.method,
headers: createRemixHeaders(req.headers),
signal: controller.signal,
};
if (req.method !== "GET" && req.method !== "HEAD") {
init.body = createReadableStreamFromReadable(req);
(init as { duplex: "half" }).duplex = "half";
}
res.on("finish", () => (controller = null));
res.on("close", () => controller?.abort());
return new Request(url.href, init);
}
export async function sendRemixResponse(
res: express.Response,
nodeResponse: Response
): Promise<void> {
res.statusMessage = nodeResponse.statusText;
res.status(nodeResponse.status);
for (let [key, value] of nodeResponse.headers.entries()) {
res.append(key, value);
}
if (nodeResponse.headers.get("Content-Type")?.match(/text\/event-stream/i)) {
res.flushHeaders();
}
if (nodeResponse.body) {
await writeReadableStreamToWritable(nodeResponse.body, res);
} else {
res.end();
}
}