mirror of
https://github.com/HackTricks-wiki/hacktricks.git
synced 2025-10-10 18:36:50 +00:00
Translated ['src/pentesting-web/h2c-smuggling.md'] to ko
This commit is contained in:
parent
5f447c33d9
commit
48b27f3b60
@ -6,25 +6,25 @@
|
|||||||
|
|
||||||
#### HTTP2 Over Cleartext (H2C) <a href="#http2-over-cleartext-h2c" id="http2-over-cleartext-h2c"></a>
|
#### HTTP2 Over Cleartext (H2C) <a href="#http2-over-cleartext-h2c" id="http2-over-cleartext-h2c"></a>
|
||||||
|
|
||||||
H2C, 또는 **http2 over cleartext**는 표준 HTTP **연결을 지속적인 연결로 업그레이드**하여 일시적인 HTTP 연결의 규범에서 벗어납니다. 이 업그레이드된 연결은 평문 HTTP의 단일 요청 특성과는 달리 지속적인 통신을 위해 http2 이진 프로토콜을 사용합니다.
|
H2C, 또는 **http2 over cleartext**, 표준 HTTP **연결을 지속적인 연결로 업그레이드**하여 일시적인 HTTP 연결의 규범에서 벗어납니다. 이 업그레이드된 연결은 평문 HTTP의 단일 요청 특성 대신 지속적인 통신을 위해 http2 이진 프로토콜을 사용합니다.
|
||||||
|
|
||||||
밀수 문제의 핵심은 **리버스 프록시**의 사용에서 발생합니다. 일반적으로 리버스 프록시는 HTTP 요청을 처리하고 백엔드로 전달한 후 백엔드의 응답을 반환합니다. 그러나 HTTP 요청에 `Connection: Upgrade` 헤더가 있을 경우(웹소켓 연결에서 일반적으로 볼 수 있음), 리버스 **프록시는 클라이언트와 서버 간의 지속적인 연결을 유지**하여 특정 프로토콜에서 요구하는 지속적인 교환을 용이하게 합니다. H2C 연결의 경우, RFC 준수를 위해 세 가지 특정 헤더가 필요합니다:
|
밀수 문제의 핵심은 **리버스 프록시**의 사용에서 발생합니다. 일반적으로 리버스 프록시는 HTTP 요청을 처리하고 백엔드로 전달하며, 그 후 백엔드의 응답을 반환합니다. 그러나 HTTP 요청에 `Connection: Upgrade` 헤더가 포함되어 있을 때(웹소켓 연결에서 일반적으로 볼 수 있음), 리버스 **프록시는 클라이언트와 서버 간의 지속적인 연결을 유지**하여 특정 프로토콜에서 요구하는 지속적인 교환을 용이하게 합니다. H2C 연결의 경우, RFC 준수를 위해 세 가지 특정 헤더가 필요합니다:
|
||||||
```
|
```
|
||||||
Upgrade: h2c
|
Upgrade: h2c
|
||||||
HTTP2-Settings: AAMAAABkAARAAAAAAAIAAAAA
|
HTTP2-Settings: AAMAAABkAARAAAAAAAIAAAAA
|
||||||
Connection: Upgrade, HTTP2-Settings
|
Connection: Upgrade, HTTP2-Settings
|
||||||
```
|
```
|
||||||
취약점은 연결을 업그레이드한 후 리버스 프록시가 개별 요청을 관리하는 것을 중단하고, 연결 설정 후 라우팅 작업이 완료되었다고 가정할 때 발생합니다. H2C Smuggling을 이용하면 요청 처리 중 적용된 리버스 프록시 규칙(예: 경로 기반 라우팅, 인증 및 WAF 처리)을 우회할 수 있으며, H2C 연결이 성공적으로 시작되었다고 가정합니다.
|
취약점은 연결을 업그레이드한 후, 리버스 프록시가 개별 요청을 관리하지 않게 되어 연결 설정 후 라우팅 작업이 완료되었다고 가정할 때 발생합니다. H2C Smuggling을 이용하면 요청 처리 중 적용된 리버스 프록시 규칙(예: 경로 기반 라우팅, 인증 및 WAF 처리)을 우회할 수 있으며, H2C 연결이 성공적으로 시작되었다고 가정합니다.
|
||||||
|
|
||||||
#### 취약한 프록시 <a href="#exploitation" id="exploitation"></a>
|
#### 취약한 프록시 <a href="#exploitation" id="exploitation"></a>
|
||||||
|
|
||||||
취약점은 리버스 프록시가 `Upgrade` 및 때때로 `Connection` 헤더를 처리하는 방식에 따라 달라집니다. 다음 프록시는 프록시 패스 중 이러한 헤더를 본질적으로 전달하여 H2C 스머글링을 본질적으로 가능하게 합니다:
|
취약점은 리버스 프록시가 `Upgrade` 및 때때로 `Connection` 헤더를 처리하는 방식에 따라 달라집니다. 다음 프록시는 프록시 패스 중 이러한 헤더를 본질적으로 전달하여 H2C 스머글링을 가능하게 합니다:
|
||||||
|
|
||||||
- HAProxy
|
- HAProxy
|
||||||
- Traefik
|
- Traefik
|
||||||
- Nuster
|
- Nuster
|
||||||
|
|
||||||
반대로, 이러한 서비스는 프록시 패스 중 두 헤더를 본질적으로 전달하지 않습니다. 그러나 불안전하게 구성될 수 있어 `Upgrade` 및 `Connection` 헤더의 필터링되지 않은 전달을 허용할 수 있습니다:
|
반대로, 이러한 서비스는 프록시 패스 중 두 헤더를 본질적으로 전달하지 않습니다. 그러나 불안전하게 구성될 수 있어 `Upgrade` 및 `Connection` 헤더의 필터링 없는 전달을 허용할 수 있습니다:
|
||||||
|
|
||||||
- AWS ALB/CLB
|
- AWS ALB/CLB
|
||||||
- NGINX
|
- NGINX
|
||||||
@ -37,7 +37,7 @@ Connection: Upgrade, HTTP2-Settings
|
|||||||
|
|
||||||
#### 악용 <a href="#exploitation" id="exploitation"></a>
|
#### 악용 <a href="#exploitation" id="exploitation"></a>
|
||||||
|
|
||||||
모든 서버가 H2C 연결 업그레이드에 필요한 헤더를 본질적으로 전달하지 않는다는 점에 유의해야 합니다. 따라서 AWS ALB/CLB, NGINX 및 Apache Traffic Server와 같은 서버는 자연스럽게 H2C 연결을 차단합니다. 그럼에도 불구하고, 일부 백엔드가 표준을 준수하지 않을 수 있으므로 `Connection: Upgrade` 변형을 테스트해 볼 가치가 있습니다. 이 변형은 `Connection` 헤더에서 `HTTP2-Settings` 값을 제외합니다.
|
모든 서버가 H2C 연결 업그레이드에 필요한 헤더를 본질적으로 전달하지 않는다는 점에 유의해야 합니다. 따라서 AWS ALB/CLB, NGINX 및 Apache Traffic Server와 같은 서버는 자연스럽게 H2C 연결을 차단합니다. 그럼에도 불구하고, `Connection: Upgrade` 변형을 사용하여 테스트해 볼 가치가 있으며, 이는 `Connection` 헤더에서 `HTTP2-Settings` 값을 제외합니다. 일부 백엔드는 표준을 준수하지 않을 수 있습니다.
|
||||||
|
|
||||||
> [!CAUTION]
|
> [!CAUTION]
|
||||||
> `proxy_pass` URL에 지정된 특정 **경로**(예: `http://backend:9999/socket.io`)와 관계없이, 설정된 연결은 기본적으로 `http://backend:9999`로 설정됩니다. 이는 이 기술을 활용하여 해당 내부 엔드포인트 내의 모든 경로와 상호작용할 수 있게 합니다. 따라서 `proxy_pass` URL에 경로를 지정하는 것은 접근을 제한하지 않습니다.
|
> `proxy_pass` URL에 지정된 특정 **경로**(예: `http://backend:9999/socket.io`)와 관계없이, 설정된 연결은 기본적으로 `http://backend:9999`로 설정됩니다. 이는 이 기술을 활용하여 해당 내부 엔드포인트 내의 모든 경로와 상호작용할 수 있게 합니다. 따라서 `proxy_pass` URL에 경로를 지정하는 것은 접근을 제한하지 않습니다.
|
||||||
@ -48,15 +48,15 @@ Connection: Upgrade, HTTP2-Settings
|
|||||||
|
|
||||||
## 웹소켓 스머글링
|
## 웹소켓 스머글링
|
||||||
|
|
||||||
웹소켓 스머글링은 프록시를 통해 접근 가능한 엔드포인트에 HTTP2 터널을 생성하는 것과 달리, 잠재적인 프록시 제한을 우회하고 엔드포인트와 직접 통신하기 위해 웹소켓 터널을 설정합니다.
|
웹소켓 스머글링은 프록시를 통해 접근할 수 있는 엔드포인트에 HTTP2 터널을 생성하는 것과 달리, 잠재적인 프록시 제한을 우회하고 엔드포인트와 직접 통신하기 위해 웹소켓 터널을 설정합니다.
|
||||||
|
|
||||||
### 시나리오 1
|
### 시나리오 1
|
||||||
|
|
||||||
이 시나리오에서는 공용 웹소켓 API와 접근할 수 없는 내부 REST API를 제공하는 백엔드가 악의적인 클라이언트의 공격 대상이 됩니다. 공격은 여러 단계로 진행됩니다:
|
이 시나리오에서는 공개 웹소켓 API와 접근할 수 없는 내부 REST API를 제공하는 백엔드가 악의적인 클라이언트의 공격 대상이 됩니다. 공격은 여러 단계로 진행됩니다:
|
||||||
|
|
||||||
1. 클라이언트는 잘못된 `Sec-WebSocket-Version` 프로토콜 버전을 헤더에 포함하여 리버스 프록시로 업그레이드 요청을 보냅니다. 프록시는 `Sec-WebSocket-Version` 헤더를 검증하지 못하고 업그레이드 요청이 유효하다고 믿고 이를 백엔드로 전달합니다.
|
1. 클라이언트는 잘못된 `Sec-WebSocket-Version` 프로토콜 버전을 헤더에 포함하여 리버스 프록시로 업그레이드 요청을 보냅니다. 프록시는 `Sec-WebSocket-Version` 헤더를 검증하지 못하고 업그레이드 요청이 유효하다고 믿고 백엔드로 전달합니다.
|
||||||
2. 백엔드는 `Sec-WebSocket-Version` 헤더의 잘못된 프로토콜 버전을 나타내는 상태 코드 `426`으로 응답합니다. 리버스 프록시는 백엔드의 응답 상태를 간과하고 웹소켓 통신 준비가 완료되었다고 가정하며 응답을 클라이언트에게 전달합니다.
|
2. 백엔드는 `Sec-WebSocket-Version` 헤더의 잘못된 프로토콜 버전을 나타내는 상태 코드 `426`으로 응답합니다. 리버스 프록시는 백엔드의 응답 상태를 간과하고 웹소켓 통신 준비가 완료되었다고 가정하며 클라이언트에게 응답을 전달합니다.
|
||||||
3. 결과적으로 리버스 프록시는 클라이언트와 백엔드 간에 웹소켓 연결이 설정되었다고 잘못 믿게 되지만, 실제로 백엔드는 업그레이드 요청을 거부했습니다. 그럼에도 불구하고 프록시는 클라이언트와 백엔드 간에 열린 TCP 또는 TLS 연결을 유지하여 클라이언트가 이 연결을 통해 비공식 REST API에 무제한으로 접근할 수 있게 합니다.
|
3. 결과적으로 리버스 프록시는 클라이언트와 백엔드 간에 웹소켓 연결이 설정되었다고 잘못 믿게 되며, 실제로는 백엔드가 업그레이드 요청을 거부했습니다. 그럼에도 불구하고 프록시는 클라이언트와 백엔드 간에 열린 TCP 또는 TLS 연결을 유지하여 클라이언트가 이 연결을 통해 비공식 REST API에 무제한으로 접근할 수 있게 합니다.
|
||||||
|
|
||||||
영향을 받는 리버스 프록시에는 문제를 해결하지 않기로 결정한 Varnish와 업그레이드 메커니즘이 변경된 Envoy 프록시 버전 1.8.0 이하가 포함됩니다. 다른 프록시도 취약할 수 있습니다.
|
영향을 받는 리버스 프록시에는 문제를 해결하지 않기로 결정한 Varnish와 업그레이드 메커니즘이 변경된 Envoy 프록시 버전 1.8.0 이하가 포함됩니다. 다른 프록시도 취약할 수 있습니다.
|
||||||
|
|
||||||
@ -64,10 +64,10 @@ Connection: Upgrade, HTTP2-Settings
|
|||||||
|
|
||||||
### 시나리오 2
|
### 시나리오 2
|
||||||
|
|
||||||
이 시나리오는 공용 웹소켓 API와 건강 검사를 위한 공용 REST API, 그리고 접근할 수 없는 내부 REST API를 가진 백엔드를 포함합니다. 공격은 더 복잡하며 다음 단계로 진행됩니다:
|
이 시나리오는 공개 웹소켓 API와 건강 검사를 위한 공개 REST API, 그리고 접근할 수 없는 내부 REST API를 가진 백엔드를 포함합니다. 공격은 더 복잡하며 다음 단계로 진행됩니다:
|
||||||
|
|
||||||
1. 클라이언트는 건강 검사 API를 트리거하기 위해 POST 요청을 보내고, 추가 HTTP 헤더 `Upgrade: websocket`을 포함합니다. NGINX는 리버스 프록시로서 이를 `Upgrade` 헤더만 기반으로 표준 업그레이드 요청으로 해석하고 요청의 다른 측면을 무시하며 백엔드로 전달합니다.
|
1. 클라이언트는 건강 검사 API를 트리거하기 위해 POST 요청을 보내고 추가 HTTP 헤더 `Upgrade: websocket`을 포함합니다. NGINX는 리버스 프록시로서 이를 `Upgrade` 헤더만 기반으로 표준 업그레이드 요청으로 해석하고 요청의 다른 측면을 무시하며 백엔드로 전달합니다.
|
||||||
2. 백엔드는 건강 검사 API를 실행하고, 공격자가 제어하는 외부 리소스에 접근하여 상태 코드 `101`이 포함된 HTTP 응답을 반환합니다. 이 응답은 백엔드에 의해 수신되고 NGINX로 전달되며, 프록시는 상태 코드만 검증하여 웹소켓 연결이 설정되었다고 착각하게 됩니다.
|
2. 백엔드는 건강 검사 API를 실행하고 공격자가 제어하는 외부 리소스에 접근하여 상태 코드 `101`이 포함된 HTTP 응답을 반환합니다. 이 응답은 백엔드에 의해 수신되고 NGINX로 전달되며, 프록시는 상태 코드만 검증하여 웹소켓 연결이 설정되었다고 잘못 생각하게 됩니다.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@ -81,9 +81,9 @@ Connection: Upgrade, HTTP2-Settings
|
|||||||
|
|
||||||
#### 실습
|
#### 실습
|
||||||
|
|
||||||
[https://github.com/0ang3el/websocket-smuggle.git](https://github.com/0ang3el/websocket-smuggle.git)에서 두 시나리오를 테스트할 수 있는 실습을 확인하십시오.
|
두 시나리오를 테스트하기 위한 실습을 확인하십시오 [https://github.com/0ang3el/websocket-smuggle.git](https://github.com/0ang3el/websocket-smuggle.git)
|
||||||
|
|
||||||
### 참고 문헌
|
### 참고자료
|
||||||
|
|
||||||
- [https://blog.assetnote.io/2021/03/18/h2c-smuggling/](https://blog.assetnote.io/2021/03/18/h2c-smuggling/)
|
- [https://blog.assetnote.io/2021/03/18/h2c-smuggling/](https://blog.assetnote.io/2021/03/18/h2c-smuggling/)
|
||||||
- [https://bishopfox.com/blog/h2c-smuggling-request](https://bishopfox.com/blog/h2c-smuggling-request)
|
- [https://bishopfox.com/blog/h2c-smuggling-request](https://bishopfox.com/blog/h2c-smuggling-request)
|
||||||
|
10
theme/elasticlunr.min.js
vendored
Normal file
10
theme/elasticlunr.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,554 +1,40 @@
|
|||||||
/* ────────────────────────────────────────────────────────────────
|
/* ht_searcher.js --------------------------------------------------------- */
|
||||||
Polyfill so requestIdleCallback works everywhere (IE 11/Safari)
|
(() => {
|
||||||
─────────────────────────────────────────────────────────────── */
|
const WRAPPER = document.getElementById('search-wrapper');
|
||||||
if (typeof window.requestIdleCallback !== "function") {
|
const TOGGLE = document.getElementById('search-toggle');
|
||||||
window.requestIdleCallback = function (cb) {
|
const INPUT = document.getElementById('searchbar');
|
||||||
const start = Date.now();
|
const LIST = document.getElementById('searchresults');
|
||||||
return setTimeout(function () {
|
const HOTKEY = 83; // “s”
|
||||||
cb({
|
let worker, debounce;
|
||||||
didTimeout: false,
|
|
||||||
timeRemaining: function () {
|
|
||||||
return Math.max(0, 50 - (Date.now() - start));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 1);
|
|
||||||
};
|
|
||||||
window.cancelIdleCallback = window.clearTimeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* ────────────────────────────────────────────────────────────────
|
function startWorker() {
|
||||||
search.js
|
if (worker) return;
|
||||||
─────────────────────────────────────────────────────────────── */
|
worker = new Worker('/search-worker.js', { type:'module' });
|
||||||
|
worker.onmessage = ({data}) => {
|
||||||
"use strict";
|
LIST.innerHTML = data.slice(0,30).map(h =>
|
||||||
window.search = window.search || {};
|
`<li><a href="${h.doc.url}">${h.doc.title}</a></li>`
|
||||||
(function search(search) {
|
).join('');
|
||||||
// Search functionality
|
};
|
||||||
//
|
|
||||||
// You can use !hasFocus() to prevent keyhandling in your key
|
|
||||||
// event handlers while the user is typing their search.
|
|
||||||
|
|
||||||
if (!Mark || !elasticlunr) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//IE 11 Compatibility from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith
|
async function openUI() {
|
||||||
if (!String.prototype.startsWith) {
|
WRAPPER.classList.remove('hidden');
|
||||||
String.prototype.startsWith = function(search, pos) {
|
INPUT.focus();
|
||||||
return this.substr(!pos || pos < 0 ? 0 : +pos, search.length) === search;
|
startWorker(); // fetches CDN/GitHub in parallel
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var search_wrap = document.getElementById('search-wrapper'),
|
TOGGLE.addEventListener('click', openUI);
|
||||||
search_modal = document.getElementById('search-modal'),
|
document.addEventListener('keydown', e => {
|
||||||
searchbar = document.getElementById('searchbar'),
|
if (!e.metaKey && !e.ctrlKey && !e.altKey && e.keyCode === HOTKEY) {
|
||||||
searchbar_outer = document.getElementById('searchbar-outer'),
|
e.preventDefault(); openUI();
|
||||||
searchresults = document.getElementById('searchresults'),
|
}
|
||||||
searchresults_outer = document.getElementById('searchresults-outer'),
|
});
|
||||||
searchresults_header = document.getElementById('searchresults-header'),
|
|
||||||
searchicon = document.getElementById('search-toggle'),
|
INPUT.addEventListener('input', e => {
|
||||||
content = document.getElementById('content'),
|
clearTimeout(debounce);
|
||||||
|
debounce = setTimeout(() => {
|
||||||
searchindex = null,
|
worker?.postMessage(e.target.value.trim());
|
||||||
doc_urls = [],
|
}, 120); // small debounce keeps typing smooth
|
||||||
results_options = {
|
});
|
||||||
teaser_word_count: 30,
|
})();
|
||||||
limit_results: 30,
|
|
||||||
},
|
|
||||||
search_options = {
|
|
||||||
bool: "AND",
|
|
||||||
expand: true,
|
|
||||||
fields: {
|
|
||||||
title: {boost: 1},
|
|
||||||
body: {boost: 1},
|
|
||||||
breadcrumbs: {boost: 0}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mark_exclude = [],
|
|
||||||
marker = new Mark(content),
|
|
||||||
current_searchterm = "",
|
|
||||||
URL_SEARCH_PARAM = 'search',
|
|
||||||
URL_MARK_PARAM = 'highlight',
|
|
||||||
teaser_count = 0,
|
|
||||||
|
|
||||||
SEARCH_HOTKEY_KEYCODE = 83,
|
|
||||||
ESCAPE_KEYCODE = 27,
|
|
||||||
DOWN_KEYCODE = 40,
|
|
||||||
UP_KEYCODE = 38,
|
|
||||||
SELECT_KEYCODE = 13;
|
|
||||||
|
|
||||||
function hasFocus() {
|
|
||||||
return searchbar === document.activeElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeChildren(elem) {
|
|
||||||
while (elem.firstChild) {
|
|
||||||
elem.removeChild(elem.firstChild);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to parse a url into its building blocks.
|
|
||||||
function parseURL(url) {
|
|
||||||
var a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
return {
|
|
||||||
source: url,
|
|
||||||
protocol: a.protocol.replace(':',''),
|
|
||||||
host: a.hostname,
|
|
||||||
port: a.port,
|
|
||||||
params: (function(){
|
|
||||||
var ret = {};
|
|
||||||
var seg = a.search.replace(/^\?/,'').split('&');
|
|
||||||
var len = seg.length, i = 0, s;
|
|
||||||
for (;i<len;i++) {
|
|
||||||
if (!seg[i]) { continue; }
|
|
||||||
s = seg[i].split('=');
|
|
||||||
ret[s[0]] = s[1];
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
})(),
|
|
||||||
file: (a.pathname.match(/\/([^/?#]+)$/i) || [,''])[1],
|
|
||||||
hash: a.hash.replace('#',''),
|
|
||||||
path: a.pathname.replace(/^([^/])/,'/$1')
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to recreate a url string from its building blocks.
|
|
||||||
function renderURL(urlobject) {
|
|
||||||
var url = urlobject.protocol + "://" + urlobject.host;
|
|
||||||
if (urlobject.port != "") {
|
|
||||||
url += ":" + urlobject.port;
|
|
||||||
}
|
|
||||||
url += urlobject.path;
|
|
||||||
var joiner = "?";
|
|
||||||
for(var prop in urlobject.params) {
|
|
||||||
if(urlobject.params.hasOwnProperty(prop)) {
|
|
||||||
url += joiner + prop + "=" + urlobject.params[prop];
|
|
||||||
joiner = "&";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (urlobject.hash != "") {
|
|
||||||
url += "#" + urlobject.hash;
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to escape html special chars for displaying the teasers
|
|
||||||
var escapeHTML = (function() {
|
|
||||||
var MAP = {
|
|
||||||
'&': '&',
|
|
||||||
'<': '<',
|
|
||||||
'>': '>',
|
|
||||||
'"': '"',
|
|
||||||
"'": '''
|
|
||||||
};
|
|
||||||
var repl = function(c) { return MAP[c]; };
|
|
||||||
return function(s) {
|
|
||||||
return s.replace(/[&<>'"]/g, repl);
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
function formatSearchMetric(count, searchterm) {
|
|
||||||
if (count == 1) {
|
|
||||||
return count + " search result for '" + searchterm + "':";
|
|
||||||
} else if (count == 0) {
|
|
||||||
return "No search results for '" + searchterm + "'.";
|
|
||||||
} else {
|
|
||||||
return count + " search results for '" + searchterm + "':";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatSearchResult(result, searchterms) {
|
|
||||||
var teaser = makeTeaser(escapeHTML(result.doc.body), searchterms);
|
|
||||||
teaser_count++;
|
|
||||||
|
|
||||||
// The ?URL_MARK_PARAM= parameter belongs inbetween the page and the #heading-anchor
|
|
||||||
var url = doc_urls[result.ref].split("#");
|
|
||||||
if (url.length == 1) { // no anchor found
|
|
||||||
url.push("");
|
|
||||||
}
|
|
||||||
|
|
||||||
// encodeURIComponent escapes all chars that could allow an XSS except
|
|
||||||
// for '. Due to that we also manually replace ' with its url-encoded
|
|
||||||
// representation (%27).
|
|
||||||
var searchterms = encodeURIComponent(searchterms.join(" ")).replace(/\'/g, "%27");
|
|
||||||
|
|
||||||
return '<a href="' + path_to_root + url[0] + '?' + URL_MARK_PARAM + '=' + searchterms + '#' + url[1]
|
|
||||||
+ '" aria-details="teaser_' + teaser_count + '">' + result.doc.breadcrumbs
|
|
||||||
+ '<span class="teaser" id="teaser_' + teaser_count + '" aria-label="Search Result Teaser">'
|
|
||||||
+ teaser + '</span>' + '</a>';
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeTeaser(body, searchterms) {
|
|
||||||
// The strategy is as follows:
|
|
||||||
// First, assign a value to each word in the document:
|
|
||||||
// Words that correspond to search terms (stemmer aware): 40
|
|
||||||
// Normal words: 2
|
|
||||||
// First word in a sentence: 8
|
|
||||||
// Then use a sliding window with a constant number of words and count the
|
|
||||||
// sum of the values of the words within the window. Then use the window that got the
|
|
||||||
// maximum sum. If there are multiple maximas, then get the last one.
|
|
||||||
// Enclose the terms in <em>.
|
|
||||||
var stemmed_searchterms = searchterms.map(function(w) {
|
|
||||||
return elasticlunr.stemmer(w.toLowerCase());
|
|
||||||
});
|
|
||||||
var searchterm_weight = 40;
|
|
||||||
var weighted = []; // contains elements of ["word", weight, index_in_document]
|
|
||||||
// split in sentences, then words
|
|
||||||
var sentences = body.toLowerCase().split('. ');
|
|
||||||
var index = 0;
|
|
||||||
var value = 0;
|
|
||||||
var searchterm_found = false;
|
|
||||||
for (var sentenceindex in sentences) {
|
|
||||||
var words = sentences[sentenceindex].split(' ');
|
|
||||||
value = 8;
|
|
||||||
for (var wordindex in words) {
|
|
||||||
var word = words[wordindex];
|
|
||||||
if (word.length > 0) {
|
|
||||||
for (var searchtermindex in stemmed_searchterms) {
|
|
||||||
if (elasticlunr.stemmer(word).startsWith(stemmed_searchterms[searchtermindex])) {
|
|
||||||
value = searchterm_weight;
|
|
||||||
searchterm_found = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
weighted.push([word, value, index]);
|
|
||||||
value = 2;
|
|
||||||
}
|
|
||||||
index += word.length;
|
|
||||||
index += 1; // ' ' or '.' if last word in sentence
|
|
||||||
};
|
|
||||||
index += 1; // because we split at a two-char boundary '. '
|
|
||||||
};
|
|
||||||
|
|
||||||
if (weighted.length == 0) {
|
|
||||||
return body;
|
|
||||||
}
|
|
||||||
|
|
||||||
var window_weight = [];
|
|
||||||
var window_size = Math.min(weighted.length, results_options.teaser_word_count);
|
|
||||||
|
|
||||||
var cur_sum = 0;
|
|
||||||
for (var wordindex = 0; wordindex < window_size; wordindex++) {
|
|
||||||
cur_sum += weighted[wordindex][1];
|
|
||||||
};
|
|
||||||
window_weight.push(cur_sum);
|
|
||||||
for (var wordindex = 0; wordindex < weighted.length - window_size; wordindex++) {
|
|
||||||
cur_sum -= weighted[wordindex][1];
|
|
||||||
cur_sum += weighted[wordindex + window_size][1];
|
|
||||||
window_weight.push(cur_sum);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (searchterm_found) {
|
|
||||||
var max_sum = 0;
|
|
||||||
var max_sum_window_index = 0;
|
|
||||||
// backwards
|
|
||||||
for (var i = window_weight.length - 1; i >= 0; i--) {
|
|
||||||
if (window_weight[i] > max_sum) {
|
|
||||||
max_sum = window_weight[i];
|
|
||||||
max_sum_window_index = i;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
max_sum_window_index = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// add <em/> around searchterms
|
|
||||||
var teaser_split = [];
|
|
||||||
var index = weighted[max_sum_window_index][2];
|
|
||||||
for (var i = max_sum_window_index; i < max_sum_window_index+window_size; i++) {
|
|
||||||
var word = weighted[i];
|
|
||||||
if (index < word[2]) {
|
|
||||||
// missing text from index to start of `word`
|
|
||||||
teaser_split.push(body.substring(index, word[2]));
|
|
||||||
index = word[2];
|
|
||||||
}
|
|
||||||
if (word[1] == searchterm_weight) {
|
|
||||||
teaser_split.push("<em>")
|
|
||||||
}
|
|
||||||
index = word[2] + word[0].length;
|
|
||||||
teaser_split.push(body.substring(word[2], index));
|
|
||||||
if (word[1] == searchterm_weight) {
|
|
||||||
teaser_split.push("</em>")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return teaser_split.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function init(config) {
|
|
||||||
results_options = config.results_options;
|
|
||||||
search_options = config.search_options;
|
|
||||||
searchbar_outer = config.searchbar_outer;
|
|
||||||
doc_urls = config.doc_urls;
|
|
||||||
searchindex = elasticlunr.Index.load(config.index);
|
|
||||||
|
|
||||||
// Set up events
|
|
||||||
searchicon.addEventListener('click', function(e) { searchIconClickHandler(); }, false);
|
|
||||||
search_wrap.addEventListener('click', function(e) { searchIconClickHandler(); }, false);
|
|
||||||
search_modal.addEventListener('click', function(e) { e.stopPropagation(); }, false);
|
|
||||||
searchbar.addEventListener('keyup', function(e) { searchbarKeyUpHandler(); }, false);
|
|
||||||
document.addEventListener('keydown', function(e) { globalKeyHandler(e); }, false);
|
|
||||||
// If the user uses the browser buttons, do the same as if a reload happened
|
|
||||||
window.onpopstate = function(e) { doSearchOrMarkFromUrl(); };
|
|
||||||
// Suppress "submit" events so the page doesn't reload when the user presses Enter
|
|
||||||
document.addEventListener('submit', function(e) { e.preventDefault(); }, false);
|
|
||||||
|
|
||||||
// If reloaded, do the search or mark again, depending on the current url parameters
|
|
||||||
doSearchOrMarkFromUrl();
|
|
||||||
}
|
|
||||||
|
|
||||||
function unfocusSearchbar() {
|
|
||||||
// hacky, but just focusing a div only works once
|
|
||||||
var tmp = document.createElement('input');
|
|
||||||
tmp.setAttribute('style', 'position: absolute; opacity: 0;');
|
|
||||||
searchicon.appendChild(tmp);
|
|
||||||
tmp.focus();
|
|
||||||
tmp.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
// On reload or browser history backwards/forwards events, parse the url and do search or mark
|
|
||||||
function doSearchOrMarkFromUrl() {
|
|
||||||
// Check current URL for search request
|
|
||||||
var url = parseURL(window.location.href);
|
|
||||||
if (url.params.hasOwnProperty(URL_SEARCH_PARAM)
|
|
||||||
&& url.params[URL_SEARCH_PARAM] != "") {
|
|
||||||
showSearch(true);
|
|
||||||
searchbar.value = decodeURIComponent(
|
|
||||||
(url.params[URL_SEARCH_PARAM]+'').replace(/\+/g, '%20'));
|
|
||||||
searchbarKeyUpHandler(); // -> doSearch()
|
|
||||||
} else {
|
|
||||||
showSearch(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url.params.hasOwnProperty(URL_MARK_PARAM)) {
|
|
||||||
var words = decodeURIComponent(url.params[URL_MARK_PARAM]).split(' ');
|
|
||||||
marker.mark(words, {
|
|
||||||
exclude: mark_exclude
|
|
||||||
});
|
|
||||||
|
|
||||||
var markers = document.querySelectorAll("mark");
|
|
||||||
function hide() {
|
|
||||||
for (var i = 0; i < markers.length; i++) {
|
|
||||||
markers[i].classList.add("fade-out");
|
|
||||||
window.setTimeout(function(e) { marker.unmark(); }, 300);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (var i = 0; i < markers.length; i++) {
|
|
||||||
markers[i].addEventListener('click', hide);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Eventhandler for keyevents on `document`
|
|
||||||
function globalKeyHandler(e) {
|
|
||||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.target.type === 'textarea' || e.target.type === 'text' || !hasFocus() && /^(?:input|select|textarea)$/i.test(e.target.nodeName)) { return; }
|
|
||||||
|
|
||||||
if (e.keyCode === ESCAPE_KEYCODE) {
|
|
||||||
e.preventDefault();
|
|
||||||
searchbar.classList.remove("active");
|
|
||||||
setSearchUrlParameters("",
|
|
||||||
(searchbar.value.trim() !== "") ? "push" : "replace");
|
|
||||||
if (hasFocus()) {
|
|
||||||
unfocusSearchbar();
|
|
||||||
}
|
|
||||||
showSearch(false);
|
|
||||||
marker.unmark();
|
|
||||||
} else if (!hasFocus() && e.keyCode === SEARCH_HOTKEY_KEYCODE) {
|
|
||||||
e.preventDefault();
|
|
||||||
showSearch(true);
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
searchbar.select();
|
|
||||||
} else if (hasFocus() && e.keyCode === DOWN_KEYCODE) {
|
|
||||||
e.preventDefault();
|
|
||||||
unfocusSearchbar();
|
|
||||||
searchresults.firstElementChild.classList.add("focus");
|
|
||||||
} else if (!hasFocus() && (e.keyCode === DOWN_KEYCODE
|
|
||||||
|| e.keyCode === UP_KEYCODE
|
|
||||||
|| e.keyCode === SELECT_KEYCODE)) {
|
|
||||||
// not `:focus` because browser does annoying scrolling
|
|
||||||
var focused = searchresults.querySelector("li.focus");
|
|
||||||
if (!focused) return;
|
|
||||||
e.preventDefault();
|
|
||||||
if (e.keyCode === DOWN_KEYCODE) {
|
|
||||||
var next = focused.nextElementSibling;
|
|
||||||
if (next) {
|
|
||||||
focused.classList.remove("focus");
|
|
||||||
next.classList.add("focus");
|
|
||||||
}
|
|
||||||
} else if (e.keyCode === UP_KEYCODE) {
|
|
||||||
focused.classList.remove("focus");
|
|
||||||
var prev = focused.previousElementSibling;
|
|
||||||
if (prev) {
|
|
||||||
prev.classList.add("focus");
|
|
||||||
} else {
|
|
||||||
searchbar.select();
|
|
||||||
}
|
|
||||||
} else { // SELECT_KEYCODE
|
|
||||||
window.location.assign(focused.querySelector('a'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showSearch(yes) {
|
|
||||||
if (yes) {
|
|
||||||
search_wrap.classList.remove('hidden');
|
|
||||||
searchicon.setAttribute('aria-expanded', 'true');
|
|
||||||
} else {
|
|
||||||
search_wrap.classList.add('hidden');
|
|
||||||
searchicon.setAttribute('aria-expanded', 'false');
|
|
||||||
var results = searchresults.children;
|
|
||||||
for (var i = 0; i < results.length; i++) {
|
|
||||||
results[i].classList.remove("focus");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showResults(yes) {
|
|
||||||
if (yes) {
|
|
||||||
searchresults_outer.classList.remove('hidden');
|
|
||||||
} else {
|
|
||||||
searchresults_outer.classList.add('hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Eventhandler for search icon
|
|
||||||
function searchIconClickHandler() {
|
|
||||||
if (search_wrap.classList.contains('hidden')) {
|
|
||||||
showSearch(true);
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
searchbar.select();
|
|
||||||
} else {
|
|
||||||
showSearch(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Eventhandler for keyevents while the searchbar is focused
|
|
||||||
function searchbarKeyUpHandler() {
|
|
||||||
var searchterm = searchbar.value.trim();
|
|
||||||
if (searchterm != "") {
|
|
||||||
searchbar.classList.add("active");
|
|
||||||
doSearch(searchterm);
|
|
||||||
} else {
|
|
||||||
searchbar.classList.remove("active");
|
|
||||||
showResults(false);
|
|
||||||
removeChildren(searchresults);
|
|
||||||
}
|
|
||||||
|
|
||||||
setSearchUrlParameters(searchterm, "push_if_new_search_else_replace");
|
|
||||||
|
|
||||||
// Remove marks
|
|
||||||
marker.unmark();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update current url with ?URL_SEARCH_PARAM= parameter, remove ?URL_MARK_PARAM and #heading-anchor .
|
|
||||||
// `action` can be one of "push", "replace", "push_if_new_search_else_replace"
|
|
||||||
// and replaces or pushes a new browser history item.
|
|
||||||
// "push_if_new_search_else_replace" pushes if there is no `?URL_SEARCH_PARAM=abc` yet.
|
|
||||||
function setSearchUrlParameters(searchterm, action) {
|
|
||||||
var url = parseURL(window.location.href);
|
|
||||||
var first_search = ! url.params.hasOwnProperty(URL_SEARCH_PARAM);
|
|
||||||
if (searchterm != "" || action == "push_if_new_search_else_replace") {
|
|
||||||
url.params[URL_SEARCH_PARAM] = searchterm;
|
|
||||||
delete url.params[URL_MARK_PARAM];
|
|
||||||
url.hash = "";
|
|
||||||
} else {
|
|
||||||
delete url.params[URL_MARK_PARAM];
|
|
||||||
delete url.params[URL_SEARCH_PARAM];
|
|
||||||
}
|
|
||||||
// A new search will also add a new history item, so the user can go back
|
|
||||||
// to the page prior to searching. A updated search term will only replace
|
|
||||||
// the url.
|
|
||||||
if (action == "push" || (action == "push_if_new_search_else_replace" && first_search) ) {
|
|
||||||
history.pushState({}, document.title, renderURL(url));
|
|
||||||
} else if (action == "replace" || (action == "push_if_new_search_else_replace" && !first_search) ) {
|
|
||||||
history.replaceState({}, document.title, renderURL(url));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function doSearch(searchterm) {
|
|
||||||
|
|
||||||
// Don't search the same twice
|
|
||||||
if (current_searchterm == searchterm) { return; }
|
|
||||||
else { current_searchterm = searchterm; }
|
|
||||||
|
|
||||||
if (searchindex == null) { return; }
|
|
||||||
|
|
||||||
// Do the actual search
|
|
||||||
var results = searchindex.search(searchterm, search_options);
|
|
||||||
var resultcount = Math.min(results.length, results_options.limit_results);
|
|
||||||
|
|
||||||
// Display search metrics
|
|
||||||
searchresults_header.innerText = formatSearchMetric(resultcount, searchterm);
|
|
||||||
|
|
||||||
// Clear and insert results
|
|
||||||
var searchterms = searchterm.split(' ');
|
|
||||||
removeChildren(searchresults);
|
|
||||||
for(var i = 0; i < resultcount ; i++){
|
|
||||||
var resultElem = document.createElement('li');
|
|
||||||
resultElem.innerHTML = formatSearchResult(results[i], searchterms);
|
|
||||||
searchresults.appendChild(resultElem);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display results
|
|
||||||
showResults(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
(async function loadSearchIndex(lang = window.lang || "en") {
|
|
||||||
const branch = lang === "en" ? "master" : lang;
|
|
||||||
const rawUrl =
|
|
||||||
`https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/refs/heads/${branch}/searchindex.js`;
|
|
||||||
const localJs = "/searchindex.js";
|
|
||||||
const TIMEOUT_MS = 10_000;
|
|
||||||
|
|
||||||
const injectScript = (src) =>
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
const s = document.createElement("script");
|
|
||||||
s.src = src;
|
|
||||||
s.onload = () => resolve(src);
|
|
||||||
s.onerror = (e) => reject(e);
|
|
||||||
document.head.appendChild(s);
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
/* 1 — download raw JS from GitHub */
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
||||||
|
|
||||||
const res = await fetch(rawUrl, { signal: controller.signal });
|
|
||||||
clearTimeout(timer);
|
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
||||||
|
|
||||||
/* 2 — wrap in a Blob so the browser sees application/javascript */
|
|
||||||
const code = await res.text();
|
|
||||||
const blobUrl = URL.createObjectURL(
|
|
||||||
new Blob([code], { type: "application/javascript" })
|
|
||||||
);
|
|
||||||
|
|
||||||
/* 3 — execute it */
|
|
||||||
await injectScript(blobUrl);
|
|
||||||
|
|
||||||
/* ───────────── PATCH ─────────────
|
|
||||||
heavy parsing now deferred to idle time
|
|
||||||
*/
|
|
||||||
requestIdleCallback(() => init(window.search));
|
|
||||||
return; // ✔ UI remains responsive
|
|
||||||
} catch (eRemote) {
|
|
||||||
console.warn("Remote JS failed →", eRemote);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ───────── fallback: local copy ───────── */
|
|
||||||
try {
|
|
||||||
await injectScript(localJs);
|
|
||||||
|
|
||||||
/* ───────────── PATCH ───────────── */
|
|
||||||
requestIdleCallback(() => init(window.search));
|
|
||||||
return;
|
|
||||||
} catch (eLocal) {
|
|
||||||
console.error("Local JS failed →", eLocal);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Exported functions
|
|
||||||
search.hasFocus = hasFocus;
|
|
||||||
})(window.search);
|
|
40
theme/search-worker.js
Normal file
40
theme/search-worker.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/* search-worker.js ------------------------------------------------------- */
|
||||||
|
/* Make code written for window work in a worker: */
|
||||||
|
self.window = self;
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
// 1. elasticlunr.min.js : CDN first → local fallback
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
try {
|
||||||
|
importScripts('https://cdn.jsdelivr.net/npm/elasticlunr@0.9.5/elasticlunr.min.js');
|
||||||
|
} catch (e) {
|
||||||
|
importScripts('/elasticlunr.min.js'); // ship this with your site
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
// 2. searchindex.js : GitHub Raw first → local fallback
|
||||||
|
// We fetch → wrap in a Blob({type:'application/javascript'}) to bypass
|
||||||
|
// GitHub’s text/plain + nosniff MIME blocking.
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
'https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/refs/heads/master/searchindex.js',
|
||||||
|
{mode: 'cors'}
|
||||||
|
);
|
||||||
|
if (!res.ok) throw new Error(res.status);
|
||||||
|
const blobUrl = URL.createObjectURL(
|
||||||
|
new Blob([await res.text()], { type:'application/javascript' })
|
||||||
|
);
|
||||||
|
importScripts(blobUrl); // correct MIME, runs once
|
||||||
|
} catch (e) {
|
||||||
|
importScripts('/searchindex.js'); // offline fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
// 3. Build the index once and answer queries
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
const idx = elasticlunr.Index.load(self.search.index);
|
||||||
|
|
||||||
|
self.onmessage = ({data: q}) => {
|
||||||
|
postMessage(idx.search(q, { bool:'AND', expand:true }));
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user