Translated ['src/pentesting-cloud/gcp-security/gcp-to-workspace-pivoting

This commit is contained in:
Translator
2025-05-17 05:01:08 +00:00
parent 697ada40f8
commit 83ca5c8dcd
2 changed files with 146 additions and 120 deletions

View File

@@ -6,7 +6,7 @@
### **Conceptos básicos de Delegación a Nivel de Dominio** ### **Conceptos básicos de Delegación a Nivel de Dominio**
La delegación a nivel de dominio de Google Workspace permite que un objeto de identidad, ya sea una **aplicación externa** del Marketplace de Google Workspace o una **Cuenta de Servicio GCP** interna, **acceda a datos en todo el Workspace en nombre de los usuarios**. La delegación a nivel de dominio de Google Workspace permite que un objeto de identidad, ya sea una **aplicación externa** del Google Workspace Marketplace o una **Cuenta de Servicio GCP** interna, **acceda a datos en todo el Workspace en nombre de los usuarios**.
> [!NOTE] > [!NOTE]
> Esto significa básicamente que las **cuentas de servicio** dentro de los proyectos de GCP de una organización podrían ser capaces de **suplantar a los usuarios de Workspace** de la misma organización (o incluso de una diferente). > Esto significa básicamente que las **cuentas de servicio** dentro de los proyectos de GCP de una organización podrían ser capaces de **suplantar a los usuarios de Workspace** de la misma organización (o incluso de una diferente).
@@ -26,9 +26,9 @@ Con una **lista de todas las cuentas de servicio** a las que tiene **acceso** y
> Ten en cuenta que al configurar la delegación a nivel de dominio no se necesita ningún usuario de Workspace, por lo tanto, solo saber que **uno válido es suficiente y requerido para la suplantación**.\ > Ten en cuenta que al configurar la delegación a nivel de dominio no se necesita ningún usuario de Workspace, por lo tanto, solo saber que **uno válido es suficiente y requerido para la suplantación**.\
> Sin embargo, se utilizarán los **privilegios del usuario suplantado**, así que si es Super Admin podrás acceder a todo. Si no tiene ningún acceso, esto será inútil. > Sin embargo, se utilizarán los **privilegios del usuario suplantado**, así que si es Super Admin podrás acceder a todo. Si no tiene ningún acceso, esto será inútil.
#### [GCP Generate Delegation Token](https://github.com/carlospolop/gcp_gen_delegation_token) #### [GCP Generar Token de Delegación](https://github.com/carlospolop/gcp_gen_delegation_token)
Este script simple **generará un token OAuth como el usuario delegado** que luego puedes usar para acceder a otras APIs de Google con o sin `gcloud`: Este simple script **generará un token OAuth como el usuario delegado** que luego puedes usar para acceder a otras APIs de Google con o sin `gcloud`:
```bash ```bash
# Impersonate indicated user # Impersonate indicated user
python3 gen_delegation_token.py --user-email <user-email> --key-file <path-to-key-file> python3 gen_delegation_token.py --user-email <user-email> --key-file <path-to-key-file>
@@ -36,6 +36,10 @@ python3 gen_delegation_token.py --user-email <user-email> --key-file <path-to-ke
# Impersonate indicated user and add additional scopes # Impersonate indicated user and add additional scopes
python3 gen_delegation_token.py --user-email <user-email> --key-file <path-to-key-file> --scopes "https://www.googleapis.com/auth/userinfo.email, https://www.googleapis.com/auth/cloud-platform, https://www.googleapis.com/auth/admin.directory.group, https://www.googleapis.com/auth/admin.directory.user, https://www.googleapis.com/auth/admin.directory.domain, https://mail.google.com/, https://www.googleapis.com/auth/drive, openid" python3 gen_delegation_token.py --user-email <user-email> --key-file <path-to-key-file> --scopes "https://www.googleapis.com/auth/userinfo.email, https://www.googleapis.com/auth/cloud-platform, https://www.googleapis.com/auth/admin.directory.group, https://www.googleapis.com/auth/admin.directory.user, https://www.googleapis.com/auth/admin.directory.domain, https://mail.google.com/, https://www.googleapis.com/auth/drive, openid"
``` ```
#### [**DelePwn**](https://github.com/n0tspam/delepwn)
Basado en la herramienta DeleFriend, pero con algunas adiciones como la capacidad de enumerar el dominio, unidad, gmail, calendario y realizar otras operaciones.
#### [**DeleFriend**](https://github.com/axon-git/DeleFriend) #### [**DeleFriend**](https://github.com/axon-git/DeleFriend)
Esta es una herramienta que puede realizar el ataque siguiendo estos pasos: Esta es una herramienta que puede realizar el ataque siguiendo estos pasos:
@@ -45,9 +49,9 @@ Esta es una herramienta que puede realizar el ataque siguiendo estos pasos:
3. Iterar sobre **cada rol de cuenta de servicio** y encontrar roles integrados, básicos y personalizados con permiso _**serviceAccountKeys.create**_ en el recurso de cuenta de servicio objetivo. Cabe señalar que el rol de Editor posee inherentemente este permiso. 3. Iterar sobre **cada rol de cuenta de servicio** y encontrar roles integrados, básicos y personalizados con permiso _**serviceAccountKeys.create**_ en el recurso de cuenta de servicio objetivo. Cabe señalar que el rol de Editor posee inherentemente este permiso.
4. Crear una **nueva clave privada `KEY_ALG_RSA_2048`** para cada recurso de cuenta de servicio que se encuentre con el permiso relevante en la política IAM. 4. Crear una **nueva clave privada `KEY_ALG_RSA_2048`** para cada recurso de cuenta de servicio que se encuentre con el permiso relevante en la política IAM.
5. Iterar sobre **cada nueva cuenta de servicio y crear un `JWT`** **objeto** para ella que esté compuesto por las credenciales de la clave privada de la SA y un alcance de OAuth. El proceso de creación de un nuevo objeto _JWT_ **iterará sobre todas las combinaciones existentes de alcances de OAuth** de la lista **oauth_scopes.txt**, con el fin de encontrar todas las posibilidades de delegación. La lista **oauth_scopes.txt** se actualiza con todos los alcances de OAuth que hemos encontrado relevantes para abusar de las identidades de Workspace. 5. Iterar sobre **cada nueva cuenta de servicio y crear un `JWT`** **objeto** para ella que esté compuesto por las credenciales de la clave privada de la SA y un alcance de OAuth. El proceso de creación de un nuevo objeto _JWT_ **iterará sobre todas las combinaciones existentes de alcances de OAuth** de la lista **oauth_scopes.txt**, con el fin de encontrar todas las posibilidades de delegación. La lista **oauth_scopes.txt** se actualiza con todos los alcances de OAuth que hemos encontrado relevantes para abusar de las identidades de Workspace.
6. El método `_make_authorization_grant_assertion` revela la necesidad de declarar un **usuario de workspace objetivo**, referido como _subject_, para generar JWTs bajo DWD. Si bien esto puede parecer requerir un usuario específico, es importante darse cuenta de que **DWD influye en cada identidad dentro de un dominio**. En consecuencia, crear un JWT para **cualquier usuario de dominio** afecta a todas las identidades en ese dominio, de acuerdo con nuestra verificación de enumeración de combinaciones. En pocas palabras, un usuario válido de Workspace es suficiente para avanzar.\ 6. El método `_make_authorization_grant_assertion` revela la necesidad de declarar un **usuario de workspace objetivo**, referido como _subject_, para generar JWTs bajo DWD. Si bien esto puede parecer requerir un usuario específico, es importante darse cuenta de que **DWD influye en cada identidad dentro de un dominio**. En consecuencia, crear un JWT para **cualquier usuario del dominio** afecta a todas las identidades en ese dominio, de acuerdo con nuestra verificación de enumeración de combinaciones. En pocas palabras, un usuario válido de Workspace es suficiente para avanzar.\
Este usuario puede definirse en el archivo _config.yaml_ de DeleFriend. Si no se conoce ya un usuario de workspace objetivo, la herramienta facilita la identificación automática de usuarios de workspace válidos escaneando usuarios de dominio con roles en proyectos de GCP. Es clave notar (nuevamente) que los JWT son específicos del dominio y no se generan para cada usuario; por lo tanto, el proceso automático se dirige a una única identidad única por dominio. Este usuario puede definirse en el archivo _config.yaml_ de DeleFriend. Si un usuario de workspace objetivo no se conoce ya, la herramienta facilita la identificación automática de usuarios válidos de workspace escaneando usuarios de dominio con roles en proyectos de GCP. Es clave notar (nuevamente) que los JWT son específicos del dominio y no se generan para cada usuario; por lo tanto, el proceso automático se dirige a una única identidad única por dominio.
7. **Enumerar y crear un nuevo token de acceso bearer** para cada JWT y validar el token contra la API de tokeninfo. 7. **Enumerar y crear un nuevo token de acceso portador** para cada JWT y validar el token contra la API de tokeninfo.
#### [Script de Python de Gitlab](https://gitlab.com/gitlab-com/gl-security/threatmanagement/redteam/redteam-public/gcp_misc/-/blob/master/gcp_delegation.py) #### [Script de Python de Gitlab](https://gitlab.com/gitlab-com/gl-security/threatmanagement/redteam/redteam-public/gcp_misc/-/blob/master/gcp_delegation.py)
@@ -79,19 +83,19 @@ Es posible **ver las Delegaciones de Dominio Amplio en** [**https://admin.google
Un atacante con la capacidad de **crear cuentas de servicio en un proyecto de GCP** y **privilegios de superadministrador en GWS podría crear una nueva delegación que permita a las cuentas de servicio suplantar a algunos usuarios de GWS:** Un atacante con la capacidad de **crear cuentas de servicio en un proyecto de GCP** y **privilegios de superadministrador en GWS podría crear una nueva delegación que permita a las cuentas de servicio suplantar a algunos usuarios de GWS:**
1. **Generación de una nueva cuenta de servicio y par de claves correspondiente:** En GCP, se pueden producir nuevos recursos de cuenta de servicio de forma interactiva a través de la consola o programáticamente utilizando llamadas a la API y herramientas de CLI directas. Esto requiere el **rol `iam.serviceAccountAdmin`** o cualquier rol personalizado equipado con el **permiso `iam.serviceAccounts.create`**. Una vez creada la cuenta de servicio, procederemos a generar un **par de claves relacionado** (**`iam.serviceAccountKeys.create`** permiso). 1. **Generación de una nueva cuenta de servicio y par de claves correspondiente:** En GCP, se pueden producir nuevos recursos de cuentas de servicio de forma interactiva a través de la consola o programáticamente utilizando llamadas a la API y herramientas de CLI directas. Esto requiere el **rol `iam.serviceAccountAdmin`** o cualquier rol personalizado equipado con el **permiso `iam.serviceAccounts.create`**. Una vez que se crea la cuenta de servicio, procederemos a generar un **par de claves relacionado** (**permiso `iam.serviceAccountKeys.create`**).
2. **Creación de nueva delegación**: Es importante entender que **solo el rol de Super Administrador tiene la capacidad de configurar la delegación global de Dominio Amplio en Google Workspace** y la delegación de Dominio Amplio **no se puede configurar programáticamente,** solo se puede crear y ajustar **manualmente** a través de la **consola** de Google Workspace. 2. **Creación de nueva delegación**: Es importante entender que **solo el rol de Super Admin posee la capacidad de configurar la delegación global de Dominio Amplio en Google Workspace** y la delegación de Dominio Amplio **no se puede configurar programáticamente,** solo se puede crear y ajustar **manualmente** a través de la **consola** de Google Workspace.
- La creación de la regla se puede encontrar en la página **Controles de API → Administrar delegación de Dominio Amplio en la consola de administración de Google Workspace**. - La creación de la regla se puede encontrar en la página **Controles de API → Administrar delegación de Dominio Amplio en la consola de administración de Google Workspace**.
3. **Adjuntando privilegios de ámbitos de OAuth**: Al configurar una nueva delegación, Google requiere solo 2 parámetros, el ID de cliente, que es el **ID de OAuth del recurso de Cuenta de Servicio de GCP**, y los **ámbitos de OAuth** que definen qué llamadas a la API requiere la delegación. 3. **Adjuntando privilegios de ámbitos de OAuth**: Al configurar una nueva delegación, Google requiere solo 2 parámetros, el ID de Cliente, que es el **ID de OAuth del recurso de Cuenta de Servicio de GCP**, y los **ámbitos de OAuth** que definen qué llamadas a la API requiere la delegación.
- La **lista completa de ámbitos de OAuth** se puede encontrar [**aquí**](https://developers.google.com/identity/protocols/oauth2/scopes), pero aquí hay una recomendación: `https://www.googleapis.com/auth/userinfo.email, https://www.googleapis.com/auth/cloud-platform, https://www.googleapis.com/auth/admin.directory.group, https://www.googleapis.com/auth/admin.directory.user, https://www.googleapis.com/auth/admin.directory.domain, https://mail.google.com/, https://www.googleapis.com/auth/drive, openid` - La **lista completa de ámbitos de OAuth** se puede encontrar [**aquí**](https://developers.google.com/identity/protocols/oauth2/scopes), pero aquí hay una recomendación: `https://www.googleapis.com/auth/userinfo.email, https://www.googleapis.com/auth/cloud-platform, https://www.googleapis.com/auth/admin.directory.group, https://www.googleapis.com/auth/admin.directory.user, https://www.googleapis.com/auth/admin.directory.domain, https://mail.google.com/, https://www.googleapis.com/auth/drive, openid`
4. **Actuando en nombre de la identidad objetivo:** En este punto, tenemos un objeto delegado funcional en GWS. Ahora, **usando la clave privada de la Cuenta de Servicio de GCP, podemos realizar llamadas a la API** (en el ámbito definido en el parámetro de ámbito de OAuth) para activarlo y **actuar en nombre de cualquier identidad que exista en Google Workspace**. Como aprendimos, la cuenta de servicio generará tokens de acceso según sus necesidades y de acuerdo con los permisos que tiene para las aplicaciones de la API REST. 4. **Actuando en nombre de la identidad objetivo:** En este punto, tenemos un objeto delegado funcional en GWS. Ahora, **usando la clave privada de la Cuenta de Servicio de GCP, podemos realizar llamadas a la API** (en el ámbito definido en el parámetro de ámbito de OAuth) para activarlo y **actuar en nombre de cualquier identidad que exista en Google Workspace**. Como aprendimos, la cuenta de servicio generará tokens de acceso según sus necesidades y de acuerdo con los permisos que tiene para las aplicaciones de la API REST.
- Consulta la **sección anterior** para algunas **herramientas** para usar esta delegación. - Consulta la **sección anterior** para algunas **herramientas** para usar esta delegación.
#### Delegación entre organizaciones #### Delegación entre organizaciones
El ID de SA de OAuth es global y se puede usar para **delegación entre organizaciones**. No se ha implementado ninguna restricción para prevenir la delegación cruzada global. En términos simples, **las cuentas de servicio de diferentes organizaciones de GCP se pueden usar para configurar la delegación de dominio amplio en otras organizaciones de Workspace**. Esto resultaría en **solo necesitar acceso de Super Administrador a Workspace**, y no acceso a la misma cuenta de GCP, ya que el adversario puede crear cuentas de servicio y claves privadas en su cuenta de GCP controlada personalmente. El ID de SA de OAuth es global y se puede usar para **delegación entre organizaciones**. No se ha implementado ninguna restricción para prevenir la delegación cruzada global. En términos simples, **las cuentas de servicio de diferentes organizaciones de GCP se pueden usar para configurar la delegación de dominio amplio en otras organizaciones de Workspace**. Esto resultaría en **solo necesitar acceso de Super Admin a Workspace**, y no acceso a la misma cuenta de GCP, ya que el adversario puede crear Cuentas de Servicio y claves privadas en su cuenta de GCP controlada personalmente.
### Creando un proyecto para enumerar Workspace ### Creando un Proyecto para enumerar Workspace
Por **defecto**, los **usuarios** de Workspace tienen el permiso para **crear nuevos proyectos**, y cuando se crea un nuevo proyecto, el **creador obtiene el rol de Propietario** sobre él. Por **defecto**, los **usuarios** de Workspace tienen el permiso para **crear nuevos proyectos**, y cuando se crea un nuevo proyecto, el **creador obtiene el rol de Propietario** sobre él.
@@ -131,7 +135,7 @@ Puede encontrar más información sobre el flujo de `gcloud` para iniciar sesió
{{#endref}} {{#endref}}
Como se explica allí, gcloud puede solicitar el alcance **`https://www.googleapis.com/auth/drive`** que permitiría a un usuario acceder al drive del usuario.\ Como se explica allí, gcloud puede solicitar el alcance **`https://www.googleapis.com/auth/drive`** que permitiría a un usuario acceder al drive del usuario.\
Como atacante, si ha comprometido **físicamente** la computadora de un usuario y el **usuario aún está conectado** con su cuenta, podría iniciar sesión generando un token con acceso al drive usando: Como atacante, si ha comprometido **físicamente** la computadora de un usuario y el **usuario sigue conectado** con su cuenta, podría iniciar sesión generando un token con acceso al drive usando:
```bash ```bash
gcloud auth login --enable-gdrive-access gcloud auth login --enable-gdrive-access
``` ```
@@ -152,7 +156,7 @@ Si un atacante tiene acceso completo a GWS, podrá acceder a grupos con acceso p
### Escalación de privilegios en Google Groups ### Escalación de privilegios en Google Groups
Por defecto, los usuarios pueden **unirse libremente a grupos de Workspace de la Organización** y esos grupos **pueden tener permisos de GCP** asignados (verifique sus grupos en [https://groups.google.com/](https://groups.google.com/)). Por defecto, los usuarios pueden **unirse libremente a grupos de Workspace de la Organización** y esos grupos **pueden tener permisos de GCP** asignados (verifica tus grupos en [https://groups.google.com/](https://groups.google.com/)).
Abusando de la **escalación de privilegios de google groups**, podrías ser capaz de escalar a un grupo con algún tipo de acceso privilegiado a GCP. Abusando de la **escalación de privilegios de google groups**, podrías ser capaz de escalar a un grupo con algún tipo de acceso privilegiado a GCP.

View File

@@ -1,27 +1,28 @@
/** /**
* HackTricks AI Chat Widget v1.15 Markdown rendering + sanitised * HackTricks AI Chat Widget v1.16 resizable sidebar
* ------------------------------------------------------------------------ * ---------------------------------------------------
* • Replaces the static “…” placeholder with a three-dot **bouncing** loader * ❶ Markdown rendering + sanitised (same as before)
* • Renders assistant replies as Markdown while purging any unsafe HTML * ❷ NEW: dragtoresize panel, width persists via localStorage
* (XSS-safe via DOMPurify)
* ------------------------------------------------------------------------
*/ */
(function () { (function () {
const LOG = "[HackTricks-AI]"; const LOG = "[HackTricksAI]";
/* ---------------- Usertunable constants ---------------- */
/* ---------------- User-tunable constants ---------------- */ const MAX_CONTEXT = 3000; // highlightedtext char limit
const MAX_CONTEXT = 3000; // highlighted-text char limit const MAX_QUESTION = 500; // question char limit
const MAX_QUESTION = 500; // question char limit const MIN_W = 250; // ← resize limits →
const MAX_W = 600;
const DEF_W = 350; // default width (if nothing saved)
const TOOLTIP_TEXT = const TOOLTIP_TEXT =
"💡 Highlight any text on the page,\nthen click to ask HackTricks AI about it"; "💡 Highlight any text on the page,\nthen click to ask HackTricks AI about it";
const API_BASE = "https://www.hacktricks.ai/api/assistants/threads"; const API_BASE = "https://www.hacktricks.ai/api/assistants/threads";
const BRAND_RED = "#b31328"; // HackTricks brand const BRAND_RED = "#b31328";
/* ------------------------------ State ------------------------------ */ /* ------------------------------ State ------------------------------ */
let threadId = null; let threadId = null;
let isRunning = false; let isRunning = false;
/* ---------- helpers ---------- */
const $ = (sel, ctx = document) => ctx.querySelector(sel); const $ = (sel, ctx = document) => ctx.querySelector(sel);
if (document.getElementById("ht-ai-btn")) { if (document.getElementById("ht-ai-btn")) {
console.warn(`${LOG} Widget already injected.`); console.warn(`${LOG} Widget already injected.`);
@@ -31,44 +32,37 @@
? document.addEventListener("DOMContentLoaded", init) ? document.addEventListener("DOMContentLoaded", init)
: init()); : init());
/* ==================================================================== */ /* =================================================================== */
/* 🔗 1. 3rd-party libs → Markdown & sanitiser */ /* 🔗 1. 3rdparty libs → Markdown & sanitiser */
/* ==================================================================== */ /* =================================================================== */
function loadScript(src) { function loadScript(src) {
return new Promise((resolve, reject) => { return new Promise((res, rej) => {
const s = document.createElement("script"); const s = document.createElement("script");
s.src = src; s.src = src;
s.onload = resolve; s.onload = res;
s.onerror = () => reject(new Error(`Failed to load ${src}`)); s.onerror = () => rej(new Error(`Failed to load ${src}`));
document.head.appendChild(s); document.head.appendChild(s);
}); });
} }
async function ensureDeps() { async function ensureDeps() {
const deps = []; const deps = [];
if (typeof marked === "undefined") { if (typeof marked === "undefined")
deps.push(loadScript("https://cdn.jsdelivr.net/npm/marked/marked.min.js")); deps.push(loadScript("https://cdn.jsdelivr.net/npm/marked/marked.min.js"));
} if (typeof DOMPurify === "undefined")
if (typeof DOMPurify === "undefined") {
deps.push( deps.push(
loadScript( loadScript(
"https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.2.5/purify.min.js" "https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.2.5/purify.min.js"
) )
); );
}
if (deps.length) await Promise.all(deps); if (deps.length) await Promise.all(deps);
} }
const mdToSafeHTML = (md) =>
DOMPurify.sanitize(marked.parse(md, { mangle: false, headerIds: false }), {
USE_PROFILES: { html: true }
});
function mdToSafeHTML(md) { /* =================================================================== */
// 1⃣ Markdown → raw HTML
const raw = marked.parse(md, { mangle: false, headerIds: false });
// 2⃣ Purify
return DOMPurify.sanitize(raw, { USE_PROFILES: { html: true } });
}
/* ==================================================================== */
async function init() { async function init() {
/* ----- make sure marked & DOMPurify are ready before anything else */
try { try {
await ensureDeps(); await ensureDeps();
} catch (e) { } catch (e) {
@@ -76,14 +70,14 @@
return; return;
} }
console.log(`${LOG} Injecting widget… v1.15`); console.log(`${LOG} Injecting widget… v1.16`);
await ensureThreadId(); await ensureThreadId();
injectStyles(); injectStyles();
const btn = createFloatingButton(); const btn = createFloatingButton();
createTooltip(btn); createTooltip(btn);
const panel = createSidebar(); const panel = createSidebar(); // ← panel with resizer
const chatLog = $("#ht-ai-chat"); const chatLog = $("#ht-ai-chat");
const sendBtn = $("#ht-ai-send"); const sendBtn = $("#ht-ai-send");
const inputBox = $("#ht-ai-question"); const inputBox = $("#ht-ai-question");
@@ -100,15 +94,8 @@
function addMsg(text, cls) { function addMsg(text, cls) {
const b = document.createElement("div"); const b = document.createElement("div");
b.className = `ht-msg ${cls}`; b.className = `ht-msg ${cls}`;
b[cls === "ht-ai" ? "innerHTML" : "textContent"] =
// ✨ assistant replies rendered as Markdown + sanitised cls === "ht-ai" ? mdToSafeHTML(text) : text;
if (cls === "ht-ai") {
b.innerHTML = mdToSafeHTML(text);
} else {
// user / context bubbles stay plain-text
b.textContent = text;
}
chatLog.appendChild(b); chatLog.appendChild(b);
chatLog.scrollTop = chatLog.scrollHeight; chatLog.scrollTop = chatLog.scrollHeight;
return b; return b;
@@ -116,30 +103,28 @@
const LOADER_HTML = const LOADER_HTML =
'<span class="ht-loading"><span></span><span></span><span></span></span>'; '<span class="ht-loading"><span></span><span></span><span></span></span>';
function setInputDisabled(d) { const setInputDisabled = (d) => {
inputBox.disabled = d; inputBox.disabled = d;
sendBtn.disabled = d; sendBtn.disabled = d;
} };
function clearThreadCookie() { const clearThreadCookie = () => {
document.cookie = "threadId=; Path=/; Max-Age=0"; document.cookie = "threadId=; Path=/; Max-Age=0";
threadId = null; threadId = null;
} };
function resetConversation() { const resetConversation = () => {
chatLog.innerHTML = ""; chatLog.innerHTML = "";
clearThreadCookie(); clearThreadCookie();
panel.classList.remove("open"); panel.classList.remove("open");
} };
/* ------------------- Panel open / close ------------------- */ /* ------------------- Panel open / close ------------------- */
btn.addEventListener("click", () => { btn.addEventListener("click", () => {
if (!savedSelection) { if (!savedSelection) {
alert("Please highlight some text first to then ask HackTricks AI about it."); alert("Please highlight some text first.");
return; return;
} }
if (savedSelection.length > MAX_CONTEXT) { if (savedSelection.length > MAX_CONTEXT) {
alert( alert(`Highlighted text is too long. Max ${MAX_CONTEXT} chars.`);
`Highlighted text is too long (${savedSelection.length} chars). Max allowed: ${MAX_CONTEXT}.`
);
return; return;
} }
chatLog.innerHTML = ""; chatLog.innerHTML = "";
@@ -157,11 +142,10 @@
addMsg("Please wait until the current operation completes.", "ht-ai"); addMsg("Please wait until the current operation completes.", "ht-ai");
return; return;
} }
isRunning = true; isRunning = true;
setInputDisabled(true); setInputDisabled(true);
const loadingBubble = addMsg("", "ht-ai"); const loading = addMsg("", "ht-ai");
loadingBubble.innerHTML = LOADER_HTML; loading.innerHTML = LOADER_HTML;
const content = context const content = context
? `### Context:\n${context}\n\n### Question to answer:\n${question}` ? `### Context:\n${context}\n\n### Question to answer:\n${question}`
@@ -178,43 +162,39 @@
try { try {
const e = await res.json(); const e = await res.json();
if (e.error) err = `Error: ${e.error}`; if (e.error) err = `Error: ${e.error}`;
else if (res.status === 429) else if (res.status === 429) err = "Rate limit exceeded.";
err = "Rate limit exceeded. Please try again later.";
} catch (_) {} } catch (_) {}
loadingBubble.textContent = err; loading.textContent = err;
return; return;
} }
const data = await res.json(); const data = await res.json();
loadingBubble.remove(); loading.remove();
if (Array.isArray(data.response)) if (Array.isArray(data.response))
data.response.forEach((p) => { data.response.forEach((p) =>
addMsg( addMsg(
p.type === "text" && p.text && p.text.value p.type === "text" && p.text && p.text.value
? p.text.value ? p.text.value
: JSON.stringify(p), : JSON.stringify(p),
"ht-ai" "ht-ai"
); )
}); );
else if (typeof data.response === "string") else if (typeof data.response === "string")
addMsg(data.response, "ht-ai"); addMsg(data.response, "ht-ai");
else addMsg(JSON.stringify(data, null, 2), "ht-ai"); else addMsg(JSON.stringify(data, null, 2), "ht-ai");
} catch (e) { } catch (e) {
console.error("Error sending message:", e); console.error("Error sending message:", e);
loadingBubble.textContent = "An unexpected error occurred."; loading.textContent = "An unexpected error occurred.";
} finally { } finally {
isRunning = false; isRunning = false;
setInputDisabled(false); setInputDisabled(false);
chatLog.scrollTop = chatLog.scrollHeight; chatLog.scrollTop = chatLog.scrollHeight;
} }
} }
async function handleSend() { async function handleSend() {
const q = inputBox.value.trim(); const q = inputBox.value.trim();
if (!q) return; if (!q) return;
if (q.length > MAX_QUESTION) { if (q.length > MAX_QUESTION) {
alert( alert(`Question too long (${q.length}). Max ${MAX_QUESTION}.`);
`Your question is too long (${q.length} chars). Max allowed: ${MAX_QUESTION}.`
);
return; return;
} }
inputBox.value = ""; inputBox.value = "";
@@ -228,9 +208,9 @@
handleSend(); handleSend();
} }
}); });
} } /* end init */
/* ==================================================================== */ /* =================================================================== */
async function ensureThreadId() { async function ensureThreadId() {
const m = document.cookie.match(/threadId=([^;]+)/); const m = document.cookie.match(/threadId=([^;]+)/);
if (m && m[1]) { if (m && m[1]) {
@@ -241,62 +221,67 @@
const r = await fetch(API_BASE, { method: "POST", credentials: "include" }); const r = await fetch(API_BASE, { method: "POST", credentials: "include" });
const d = await r.json(); const d = await r.json();
if (!r.ok || !d.threadId) throw new Error(`${r.status} ${r.statusText}`); if (!r.ok || !d.threadId) throw new Error(`${r.status} ${r.statusText}`);
threadId = d.threadId; threadId = d.threadId;
document.cookie = document.cookie =
`threadId=${threadId}; Path=/; Secure; SameSite=Strict; Max-Age=7200`; `threadId=${threadId}; Path=/; Secure; SameSite=Strict; Max-Age=7200`;
} catch (e) { } catch (e) {
console.error("Error creating threadId:", e); console.error("Error creating threadId:", e);
alert("Failed to initialise the conversation. Please refresh and try again."); alert("Failed to initialise the conversation. Please refresh.");
throw e; throw e;
} }
} }
/* ==================================================================== */ /* =================================================================== */
function injectStyles() { function injectStyles() {
const css = ` const css = `
#ht-ai-btn{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);width:60px;height:60px;border-radius:50%;background:#1e1e1e;color:#fff;font-size:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:99999;box-shadow:0 2px 8px rgba(0,0,0,.4);transition:opacity .2s} #ht-ai-btn{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);min-width:60px;height:60px;border-radius:30px;background:linear-gradient(45deg, #b31328, #d42d3f, #2d5db4, #3470e4);background-size:300% 300%;animation:gradientShift 8s ease infinite;color:#fff;font-size:18px;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:99999;box-shadow:0 2px 8px rgba(0,0,0,.4);transition:opacity .2s;padding:0 20px}
#ht-ai-btn:hover{opacity:.85} #ht-ai-btn span{margin-left:8px;font-weight:bold}
@media(max-width:768px){#ht-ai-btn{display:none}} @keyframes gradientShift{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}}
#ht-ai-tooltip{position:fixed;padding:6px 8px;background:#111;color:#fff;border-radius:4px;font-size:13px;white-space:pre-wrap;pointer-events:none;opacity:0;transform:translate(-50%,-8px);transition:opacity .15s ease,transform .15s ease;z-index:100000} #ht-ai-btn:hover{opacity:.85}
#ht-ai-tooltip.show{opacity:1;transform:translate(-50%,-12px)} @media(max-width:768px){#ht-ai-btn{display:none}}
#ht-ai-panel{position:fixed;top:0;right:0;height:100%;width:350px;max-width:90vw;background:#000;color:#fff;display:flex;flex-direction:column;transform:translateX(100%);transition:transform .3s ease;z-index:100000;font-family:system-ui,-apple-system,Segoe UI,Roboto,"Helvetica Neue",Arial,sans-serif} #ht-ai-tooltip{position:fixed;padding:6px 8px;background:#111;color:#fff;border-radius:4px;font-size:13px;white-space:pre-wrap;pointer-events:none;opacity:0;transform:translate(-50%,-8px);transition:opacity .15s ease,transform .15s ease;z-index:100000}
#ht-ai-panel.open{transform:translateX(0)} #ht-ai-tooltip.show{opacity:1;transform:translate(-50%,-12px)}
@media(max-width:768px){#ht-ai-panel{display:none}} #ht-ai-panel{position:fixed;top:0;right:0;height:100%;max-width:90vw;background:#000;color:#fff;display:flex;flex-direction:column;transform:translateX(100%);transition:transform .3s ease;z-index:100000;font-family:system-ui,-apple-system,Segoe UI,Roboto,"Helvetica Neue",Arial,sans-serif}
#ht-ai-header{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid #333} #ht-ai-panel.open{transform:translateX(0)}
#ht-ai-header .ht-actions{display:flex;gap:8px;align-items:center} @media(max-width:768px){#ht-ai-panel{display:none}}
#ht-ai-close,#ht-ai-reset{cursor:pointer;font-size:18px;background:none;border:none;color:#fff;padding:0} #ht-ai-header{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid #333}
#ht-ai-close:hover,#ht-ai-reset:hover{opacity:.7} #ht-ai-header .ht-actions{display:flex;gap:8px;align-items:center}
#ht-ai-chat{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:12px;font-size:14px} #ht-ai-close,#ht-ai-reset{cursor:pointer;font-size:18px;background:none;border:none;color:#fff;padding:0}
.ht-msg{max-width:90%;line-height:1.4;padding:10px 12px;border-radius:8px;white-space:pre-wrap;word-wrap:break-word} #ht-ai-close:hover,#ht-ai-reset:hover{opacity:.7}
.ht-user{align-self:flex-end;background:${BRAND_RED}} #ht-ai-chat{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:12px;font-size:14px}
.ht-ai{align-self:flex-start;background:#222} .ht-msg{max-width:90%;line-height:1.4;padding:10px 12px;border-radius:8px;white-space:pre-wrap;word-wrap:break-word}
.ht-context{align-self:flex-start;background:#444;font-style:italic;font-size:13px} .ht-user{align-self:flex-end;background:${BRAND_RED}}
#ht-ai-input{display:flex;gap:8px;padding:12px 16px;border-top:1px solid #333} .ht-ai{align-self:flex-start;background:#222}
#ht-ai-question{flex:1;min-height:40px;max-height:120px;resize:vertical;padding:8px;border-radius:6px;border:none;font-size:14px} .ht-context{align-self:flex-start;background:#444;font-style:italic;font-size:13px}
#ht-ai-send{padding:0 18px;border:none;border-radius:6px;background:${BRAND_RED};color:#fff;font-size:14px;cursor:pointer} #ht-ai-input{display:flex;gap:8px;padding:12px 16px;border-top:1px solid #333}
#ht-ai-send:disabled{opacity:.5;cursor:not-allowed} #ht-ai-question{flex:1;min-height:40px;max-height:120px;resize:vertical;padding:8px;border-radius:6px;border:none;font-size:14px}
/* Loader animation */ #ht-ai-send{padding:0 18px;border:none;border-radius:6px;background:${BRAND_RED};color:#fff;font-size:14px;cursor:pointer}
.ht-loading{display:inline-flex;align-items:center;gap:4px} #ht-ai-send:disabled{opacity:.5;cursor:not-allowed}
.ht-loading span{width:6px;height:6px;border-radius:50%;background:#888;animation:ht-bounce 1.2s infinite ease-in-out} /* Loader */
.ht-loading span:nth-child(2){animation-delay:0.2s} .ht-loading{display:inline-flex;align-items:center;gap:4px}
.ht-loading span:nth-child(3){animation-delay:0.4s} .ht-loading span{width:6px;height:6px;border-radius:50%;background:#888;animation:ht-bounce 1.2s infinite ease-in-out}
@keyframes ht-bounce{0%,80%,100%{transform:scale(0);}40%{transform:scale(1);} } .ht-loading span:nth-child(2){animation-delay:0.2s}
::selection{background:#ffeb3b;color:#000} .ht-loading span:nth-child(3){animation-delay:0.4s}
::-moz-selection{background:#ffeb3b;color:#000}`; @keyframes ht-bounce{0%,80%,100%{transform:scale(0);}40%{transform:scale(1);} }
::selection{background:#ffeb3b;color:#000}
::-moz-selection{background:#ffeb3b;color:#000}
/* NEW: resizer handle */
#ht-ai-resizer{position:absolute;left:0;top:0;width:6px;height:100%;cursor:ew-resize;background:transparent}
#ht-ai-resizer:hover{background:rgba(255,255,255,.05)}`;
const s = document.createElement("style"); const s = document.createElement("style");
s.id = "ht-ai-style"; s.id = "ht-ai-style";
s.textContent = css; s.textContent = css;
document.head.appendChild(s); document.head.appendChild(s);
} }
/* =================================================================== */
function createFloatingButton() { function createFloatingButton() {
const d = document.createElement("div"); const d = document.createElement("div");
d.id = "ht-ai-btn"; d.id = "ht-ai-btn";
d.textContent = "🤖"; d.innerHTML = "🤖<span>HackTricksAI</span>";
document.body.appendChild(d); document.body.appendChild(d);
return d; return d;
} }
function createTooltip(btn) { function createTooltip(btn) {
const t = document.createElement("div"); const t = document.createElement("div");
t.id = "ht-ai-tooltip"; t.id = "ht-ai-tooltip";
@@ -311,11 +296,16 @@
btn.addEventListener("mouseleave", () => t.classList.remove("show")); btn.addEventListener("mouseleave", () => t.classList.remove("show"));
} }
/* =================================================================== */
function createSidebar() { function createSidebar() {
const saved = parseInt(localStorage.getItem("htAiWidth") || DEF_W, 10);
const width = Math.min(Math.max(saved, MIN_W), MAX_W);
const p = document.createElement("div"); const p = document.createElement("div");
p.id = "ht-ai-panel"; p.id = "ht-ai-panel";
p.style.width = width + "px"; // ← applied width
p.innerHTML = ` p.innerHTML = `
<div id="ht-ai-header"><strong>HackTricks AI Chat</strong> <div id="ht-ai-header"><strong>HackTricks AI Chat</strong>
<div class="ht-actions"> <div class="ht-actions">
<button id="ht-ai-reset" title="Reset">↺</button> <button id="ht-ai-reset" title="Reset">↺</button>
<span id="ht-ai-close" title="Close">✖</span> <span id="ht-ai-close" title="Close">✖</span>
@@ -326,7 +316,39 @@
<textarea id="ht-ai-question" placeholder="Type your question…"></textarea> <textarea id="ht-ai-question" placeholder="Type your question…"></textarea>
<button id="ht-ai-send">Send</button> <button id="ht-ai-send">Send</button>
</div>`; </div>`;
/* NEW: resizer strip */
const resizer = document.createElement("div");
resizer.id = "ht-ai-resizer";
p.appendChild(resizer);
document.body.appendChild(p); document.body.appendChild(p);
addResizeLogic(resizer, p);
return p; return p;
} }
/* ---------------- resize behaviour ---------------- */
function addResizeLogic(handle, panel) {
let startX, startW, dragging = false;
const onMove = (e) => {
if (!dragging) return;
const dx = startX - e.clientX; // dragging leftwards ⇒ +dx
let newW = startW + dx;
newW = Math.min(Math.max(newW, MIN_W), MAX_W);
panel.style.width = newW + "px";
};
const onUp = () => {
if (!dragging) return;
dragging = false;
localStorage.setItem("htAiWidth", parseInt(panel.style.width, 10));
document.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseup", onUp);
};
handle.addEventListener("mousedown", (e) => {
dragging = true;
startX = e.clientX;
startW = parseInt(window.getComputedStyle(panel).width, 10);
document.addEventListener("mousemove", onMove);
document.addEventListener("mouseup", onUp);
});
}
})(); })();