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 pl
This commit is contained in:
parent
b450963916
commit
062eaa2bd4
@ -6,17 +6,17 @@
|
||||
|
||||
#### HTTP2 Over Cleartext (H2C) <a href="#http2-over-cleartext-h2c" id="http2-over-cleartext-h2c"></a>
|
||||
|
||||
H2C, czyli **http2 over cleartext**, odbiega od normy przejrzystych połączeń HTTP, aktualizując standardowe połączenie HTTP **do połączenia trwałego**. To zaktualizowane połączenie wykorzystuje binarny protokół http2 do bieżącej komunikacji, w przeciwieństwie do jednorazowego charakteru tekstowego HTTP.
|
||||
H2C, czyli **http2 over cleartext**, odbiega od normy przejrzystych połączeń HTTP, aktualizując standardowe połączenie HTTP **do połączenia trwałego**. To zaktualizowane połączenie wykorzystuje binarny protokół http2 do ciągłej komunikacji, w przeciwieństwie do jednorazowego charakteru tekstowego HTTP.
|
||||
|
||||
Sedno problemu z przemycaniem pojawia się przy użyciu **reverse proxy**. Zwykle reverse proxy przetwarza i przekazuje żądania HTTP do backendu, zwracając odpowiedź backendu po tym. Jednak gdy nagłówek `Connection: Upgrade` jest obecny w żądaniu HTTP (często spotykanym w połączeniach websocket), reverse **proxy utrzymuje trwałe połączenie** między klientem a serwerem, ułatwiając ciągłą wymianę wymaganą przez niektóre protokoły. Dla połączeń H2C przestrzeganie RFC wymaga obecności trzech specyficznych nagłówków:
|
||||
Sedno problemu z smugglingiem pojawia się przy użyciu **reverse proxy**. Zwykle reverse proxy przetwarza i przekazuje żądania HTTP do backendu, zwracając odpowiedź backendu po tym. Jednak gdy nagłówek `Connection: Upgrade` jest obecny w żądaniu HTTP (często spotykanym w połączeniach websocket), reverse **proxy utrzymuje trwałe połączenie** między klientem a serwerem, ułatwiając ciągłą wymianę wymaganą przez niektóre protokoły. Dla połączeń H2C przestrzeganie RFC wymaga obecności trzech specyficznych nagłówków:
|
||||
```
|
||||
Upgrade: h2c
|
||||
HTTP2-Settings: AAMAAABkAARAAAAAAAIAAAAA
|
||||
Connection: Upgrade, HTTP2-Settings
|
||||
```
|
||||
Wrażliwość pojawia się, gdy po zaktualizowaniu połączenia, odwrotny proxy przestaje zarządzać poszczególnymi żądaniami, zakładając, że jego zadanie routingu jest zakończone po nawiązaniu połączenia. Wykorzystanie H2C Smuggling pozwala na obejście zasad odwrotnego proxy stosowanych podczas przetwarzania żądań, takich jak routowanie oparte na ścieżkach, uwierzytelnianie i przetwarzanie WAF, zakładając, że połączenie H2C zostało pomyślnie nawiązane.
|
||||
Wrażliwość pojawia się, gdy po zaktualizowaniu połączenia, odwrotny proxy przestaje zarządzać poszczególnymi żądaniami, zakładając, że jego zadanie związane z routowaniem jest zakończone po nawiązaniu połączenia. Wykorzystanie H2C Smuggling pozwala na obejście reguł odwrotnego proxy stosowanych podczas przetwarzania żądań, takich jak routowanie oparte na ścieżkach, uwierzytelnianie i przetwarzanie WAF, zakładając, że połączenie H2C zostało pomyślnie nawiązane.
|
||||
|
||||
#### Wrażliwe proxy <a href="#exploitation" id="exploitation"></a>
|
||||
#### Wrażliwe Proxysy <a href="#exploitation" id="exploitation"></a>
|
||||
|
||||
Wrażliwość zależy od obsługi przez odwrotny proxy nagłówków `Upgrade` i czasami `Connection`. Następujące proxy z natury przekazują te nagłówki podczas proxy-pass, co z natury umożliwia H2C smuggling:
|
||||
|
||||
@ -37,10 +37,10 @@ Z drugiej strony, te usługi nie przekazują z natury obu nagłówków podczas p
|
||||
|
||||
#### Wykorzystanie <a href="#exploitation" id="exploitation"></a>
|
||||
|
||||
Ważne jest, aby zauważyć, że nie wszystkie serwery z natury przekazują nagłówki wymagane do zgodnej aktualizacji połączenia H2C. W związku z tym serwery takie jak AWS ALB/CLB, NGINX i Apache Traffic Server, między innymi, naturalnie blokują połączenia H2C. Niemniej jednak warto przetestować niezgodną wersję `Connection: Upgrade`, która wyklucza wartość `HTTP2-Settings` z nagłówka `Connection`, ponieważ niektóre backendy mogą nie przestrzegać standardów.
|
||||
Ważne jest, aby zauważyć, że nie wszystkie serwery z natury przekazują nagłówki wymagane do zgodnej aktualizacji połączenia H2C. W związku z tym serwery takie jak AWS ALB/CLB, NGINX i Apache Traffic Server, między innymi, naturalnie blokują połączenia H2C. Niemniej jednak warto przetestować wariant niezgodny `Connection: Upgrade`, który wyklucza wartość `HTTP2-Settings` z nagłówka `Connection`, ponieważ niektóre backendy mogą nie przestrzegać standardów.
|
||||
|
||||
> [!CAUTION]
|
||||
> Niezależnie od konkretnej **ścieżki** wyznaczonej w URL `proxy_pass` (np. `http://backend:9999/socket.io`), nawiązane połączenie domyślnie odnosi się do `http://backend:9999`. Umożliwia to interakcję z dowolną ścieżką w tym wewnętrznym punkcie końcowym, wykorzystując tę technikę. W związku z tym określenie ścieżki w URL `proxy_pass` nie ogranicza dostępu.
|
||||
> Niezależnie od konkretnej **ścieżki** wyznaczonej w URL `proxy_pass` (np. `http://backend:9999/socket.io`), nawiązane połączenie domyślnie odnosi się do `http://backend:9999`. Umożliwia to interakcję z dowolną ścieżką w tym wewnętrznym punkcie końcowym, wykorzystując tę technikę. W związku z tym, określenie ścieżki w URL `proxy_pass` nie ogranicza dostępu.
|
||||
|
||||
Narzędzia [**h2csmuggler by BishopFox**](https://github.com/BishopFox/h2csmuggler) i [**h2csmuggler by assetnote**](https://github.com/assetnote/h2csmuggler) ułatwiają próby **obejścia zabezpieczeń nałożonych przez proxy** poprzez nawiązanie połączenia H2C, co umożliwia dostęp do zasobów chronionych przez proxy.
|
||||
|
||||
@ -54,8 +54,8 @@ Websocket smuggling, w przeciwieństwie do tworzenia tunelu HTTP2 do punktu koń
|
||||
|
||||
W tym scenariuszu atakowany jest backend, który oferuje publiczne API WebSocket obok niedostępnego wewnętrznego API REST. Złośliwy klient dąży do uzyskania dostępu do wewnętrznego API REST. Atak przebiega w kilku krokach:
|
||||
|
||||
1. Klient inicjuje, wysyłając żądanie Upgrade do odwrotnego proxy z nieprawidłową wersją protokołu `Sec-WebSocket-Version` w nagłówku. Proxy, nie weryfikując nagłówka `Sec-WebSocket-Version`, uważa żądanie Upgrade za ważne i przekazuje je do backendu.
|
||||
2. Backend odpowiada kodem statusu `426`, wskazując na nieprawidłową wersję protokołu w nagłówku `Sec-WebSocket-Version`. Odwrotny proxy, ignorując status odpowiedzi backendu, zakłada gotowość do komunikacji WebSocket i przekazuje odpowiedź do klienta.
|
||||
1. Klient inicjuje, wysyłając żądanie Upgrade do odwrotnego proxy z nieprawidłową wersją protokołu `Sec-WebSocket-Version` w nagłówku. Proxy, nie weryfikując nagłówka `Sec-WebSocket-Version`, uważa, że żądanie Upgrade jest ważne i przekazuje je do backendu.
|
||||
2. Backend odpowiada kodem statusu `426`, wskazując na nieprawidłową wersję protokołu w nagłówku `Sec-WebSocket-Version`. Odwrotny proxy, pomijając status odpowiedzi backendu, zakłada gotowość do komunikacji WebSocket i przekazuje odpowiedź do klienta.
|
||||
3. W konsekwencji odwrotny proxy jest wprowadzany w błąd, wierząc, że nawiązano połączenie WebSocket między klientem a backendem, podczas gdy w rzeczywistości backend odrzucił żądanie Upgrade. Mimo to proxy utrzymuje otwarte połączenie TCP lub TLS między klientem a backendem, co pozwala klientowi na nieograniczony dostęp do prywatnego API REST przez to połączenie.
|
||||
|
||||
Dotknięte odwrotne proxy obejmują Varnish, który odmówił rozwiązania problemu, oraz wersję proxy Envoy 1.8.0 lub starszą, przy czym nowsze wersje zmieniły mechanizm aktualizacji. Inne proxy mogą być również podatne.
|
||||
@ -64,9 +64,9 @@ Dotknięte odwrotne proxy obejmują Varnish, który odmówił rozwiązania probl
|
||||
|
||||
### Scenariusz 2
|
||||
|
||||
Ten scenariusz dotyczy backendu z publicznym API WebSocket i publicznym API REST do sprawdzania stanu, a także niedostępnego wewnętrznego API REST. Atak, bardziej złożony, obejmuje następujące kroki:
|
||||
Ten scenariusz dotyczy backendu z publicznym API WebSocket oraz publicznym API REST do sprawdzania stanu, a także niedostępnego wewnętrznego API REST. Atak, bardziej złożony, obejmuje następujące kroki:
|
||||
|
||||
1. Klient wysyła żądanie POST, aby uruchomić API sprawdzania stanu, w tym dodatkowy nagłówek HTTP `Upgrade: websocket`. NGINX, działający jako odwrotny proxy, interpretuje to jako standardowe żądanie Upgrade wyłącznie na podstawie nagłówka `Upgrade`, ignorując inne aspekty żądania, i przekazuje je do backendu.
|
||||
1. Klient wysyła żądanie POST, aby uruchomić API sprawdzania stanu, w tym dodatkowy nagłówek HTTP `Upgrade: websocket`. NGINX, działający jako odwrotny proxy, interpretuje to jako standardowe żądanie Upgrade wyłącznie na podstawie nagłówka `Upgrade`, pomijając inne aspekty żądania, i przekazuje je do backendu.
|
||||
2. Backend wykonuje API sprawdzania stanu, kontaktując się z zewnętrznym zasobem kontrolowanym przez atakującego, który zwraca odpowiedź HTTP z kodem statusu `101`. Ta odpowiedź, po odebraniu przez backend i przekazaniu do NGINX, wprowadza proxy w błąd, myśląc, że nawiązano połączenie WebSocket z powodu jego weryfikacji tylko na podstawie kodu statusu.
|
||||
|
||||

|
||||
@ -77,13 +77,13 @@ Ostatecznie NGINX jest wprowadzany w błąd, wierząc, że istnieje połączenie
|
||||
|
||||

|
||||
|
||||
Większość odwrotnych proxy jest podatna na ten scenariusz, ale wykorzystanie zależy od obecności zewnętrznej wrażliwości SSRF, zazwyczaj uważanej za problem o niskim ciężarze.
|
||||
Większość odwrotnych proxy jest podatna na ten scenariusz, ale wykorzystanie zależy od obecności zewnętrznej podatności SSRF, zazwyczaj uważanej za problem o niskim ciężarze.
|
||||
|
||||
#### Laboratoria
|
||||
|
||||
Sprawdź laboratoria, aby przetestować oba scenariusze w [https://github.com/0ang3el/websocket-smuggle.git](https://github.com/0ang3el/websocket-smuggle.git)
|
||||
|
||||
### Odniesienia
|
||||
### Odnośniki
|
||||
|
||||
- [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)
|
||||
|
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 @@
|
||||
/* ────────────────────────────────────────────────────────────────
|
||||
Polyfill so requestIdleCallback works everywhere (IE 11/Safari)
|
||||
─────────────────────────────────────────────────────────────── */
|
||||
if (typeof window.requestIdleCallback !== "function") {
|
||||
window.requestIdleCallback = function (cb) {
|
||||
const start = Date.now();
|
||||
return setTimeout(function () {
|
||||
cb({
|
||||
didTimeout: false,
|
||||
timeRemaining: function () {
|
||||
return Math.max(0, 50 - (Date.now() - start));
|
||||
}
|
||||
});
|
||||
}, 1);
|
||||
};
|
||||
window.cancelIdleCallback = window.clearTimeout;
|
||||
}
|
||||
|
||||
/* ht_searcher.js --------------------------------------------------------- */
|
||||
(() => {
|
||||
const WRAPPER = document.getElementById('search-wrapper');
|
||||
const TOGGLE = document.getElementById('search-toggle');
|
||||
const INPUT = document.getElementById('searchbar');
|
||||
const LIST = document.getElementById('searchresults');
|
||||
const HOTKEY = 83; // “s”
|
||||
let worker, debounce;
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────
|
||||
search.js
|
||||
─────────────────────────────────────────────────────────────── */
|
||||
|
||||
"use strict";
|
||||
window.search = window.search || {};
|
||||
(function search(search) {
|
||||
// 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;
|
||||
function startWorker() {
|
||||
if (worker) return;
|
||||
worker = new Worker('/search-worker.js', { type:'module' });
|
||||
worker.onmessage = ({data}) => {
|
||||
LIST.innerHTML = data.slice(0,30).map(h =>
|
||||
`<li><a href="${h.doc.url}">${h.doc.title}</a></li>`
|
||||
).join('');
|
||||
};
|
||||
}
|
||||
|
||||
//IE 11 Compatibility from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith
|
||||
if (!String.prototype.startsWith) {
|
||||
String.prototype.startsWith = function(search, pos) {
|
||||
return this.substr(!pos || pos < 0 ? 0 : +pos, search.length) === search;
|
||||
};
|
||||
|
||||
async function openUI() {
|
||||
WRAPPER.classList.remove('hidden');
|
||||
INPUT.focus();
|
||||
startWorker(); // fetches CDN/GitHub in parallel
|
||||
}
|
||||
|
||||
var search_wrap = document.getElementById('search-wrapper'),
|
||||
search_modal = document.getElementById('search-modal'),
|
||||
searchbar = document.getElementById('searchbar'),
|
||||
searchbar_outer = document.getElementById('searchbar-outer'),
|
||||
searchresults = document.getElementById('searchresults'),
|
||||
searchresults_outer = document.getElementById('searchresults-outer'),
|
||||
searchresults_header = document.getElementById('searchresults-header'),
|
||||
searchicon = document.getElementById('search-toggle'),
|
||||
content = document.getElementById('content'),
|
||||
|
||||
searchindex = null,
|
||||
doc_urls = [],
|
||||
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);
|
||||
|
||||
TOGGLE.addEventListener('click', openUI);
|
||||
document.addEventListener('keydown', e => {
|
||||
if (!e.metaKey && !e.ctrlKey && !e.altKey && e.keyCode === HOTKEY) {
|
||||
e.preventDefault(); openUI();
|
||||
}
|
||||
});
|
||||
|
||||
INPUT.addEventListener('input', e => {
|
||||
clearTimeout(debounce);
|
||||
debounce = setTimeout(() => {
|
||||
worker?.postMessage(e.target.value.trim());
|
||||
}, 120); // small debounce keeps typing smooth
|
||||
});
|
||||
})();
|
||||
|
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