关联漏洞
介绍
## overview
after reading write up of @zhero___ in his personal blogpost i decide to build this CTF to learn how things work and after that i decide to share it with anybody who wants to learn how exploit this vulnerability , i try to make CTF with remix@2.16.0 version but when i check @remix-run/express i notice that they patch the code and i copy paste the vulnereable code from their github so you must change the code of @remix-run/express package as i say below
## Goal
you must find the flag in admin page and other part of application are dosn't functional so only focus on admin page and find the flag also i recommend you read this amazing writeup "https://zhero-web-sec.github.io/research-and-things/react-router-and-the-remixed-path" and learn how researcher find this bug , also you can read the code and find out how things work
## Getting Started
Follow these steps to set up the project:
### 1. Clone the repository
```bash
git clone https://github.com/pouriam23/vulnerability-in-Remix-React-Router-CVE-2025-31137-.git
cd vulnerability-in-Remix-React-Router-CVE-2025-31137-
```
### 2. Install dependencies
Make sure you have [pnpm](https://pnpm.io/) installed, then run:
```bash
pnpm install
```
### 3. change Remix Express Server code to vulnerable version ( when zhero find bug )
Replace the contents of the following file:
```
/my-remix-app/node_modules/@remix-run/express/dist/server.js
```
with the code below and rename the file to:
```
server.ts
```
### 📄 server.ts
```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();
}
}
```
文件快照
[4.0K] /data/pocs/53803e78cf6e672f5a81a479aeec19d759e54a62
├── [4.0K] app
│ ├── [ 561] entry.client.tsx
│ ├── [3.9K] entry.server.tsx
│ ├── [ 991] root.tsx
│ ├── [4.0K] routes
│ │ └── [5.9K] _index.tsx
│ └── [ 180] tailwind.css
├── [1.4K] package.json
├── [ 81] postcss.config.js
├── [4.0K] public
│ ├── [ 17K] favicon.ico
│ ├── [ 78K] logo-dark.png
│ └── [5.8K] logo-light.png
├── [5.0K] README.md
├── [1.4K] server.js
├── [ 467] tailwind.config.ts
├── [ 734] tsconfig.json
└── [ 519] vite.config.ts
3 directories, 15 files
备注
1. 建议优先通过来源进行访问。
2. 如果因为来源失效或无法访问,请发送邮件到 f.jinxu#gmail.com 索取本地快照(把 # 换成 @)。
3. 神龙已为您对 POC 代码进行快照,为了长期维护,请考虑为本地 POC 付费/捐赠,感谢您的支持。