From 0052cb7706ebd4c5fb9c45313f49c41d41368d59 Mon Sep 17 00:00:00 2001 From: Translator Date: Sat, 17 May 2025 05:01:36 +0000 Subject: [PATCH] Translated ['src/pentesting-cloud/gcp-security/gcp-to-workspace-pivoting --- .../gcp-to-workspace-pivoting/README.md | 54 ++-- theme/ai.js | 236 ++++++++++-------- 2 files changed, 158 insertions(+), 132 deletions(-) diff --git a/src/pentesting-cloud/gcp-security/gcp-to-workspace-pivoting/README.md b/src/pentesting-cloud/gcp-security/gcp-to-workspace-pivoting/README.md index a1f35be3f..119d727c1 100644 --- a/src/pentesting-cloud/gcp-security/gcp-to-workspace-pivoting/README.md +++ b/src/pentesting-cloud/gcp-security/gcp-to-workspace-pivoting/README.md @@ -4,12 +4,12 @@ ## **Από GCP σε GWS** -### **Βασικά της Εξουσιοδότησης σε Επίπεδο Τομέα** +### **Βασικά της εξουσιοδότησης σε επίπεδο τομέα** -Η Εξουσιοδότηση σε Επίπεδο Τομέα του Google Workspace επιτρέπει σε ένα αντικείμενο ταυτότητας, είτε μια **εξωτερική εφαρμογή** από το Google Workspace Marketplace είτε έναν εσωτερικό **Λογαριασμό Υπηρεσίας GCP**, να **πρόσβαση σε δεδομένα σε όλο το Workspace εκ μέρους των χρηστών**. +Η εξουσιοδότηση σε επίπεδο τομέα του Google Workspace επιτρέπει σε ένα αντικείμενο ταυτότητας, είτε μια **εξωτερική εφαρμογή** από το Google Workspace Marketplace είτε έναν εσωτερικό **GCP Service Account**, να **πρόσβαση σε δεδομένα σε όλο το Workspace εκ μέρους των χρηστών**. > [!NOTE] -> Αυτό σημαίνει βασικά ότι οι **λογαριασμοί υπηρεσίας** μέσα σε έργα GCP μιας οργάνωσης μπορεί να είναι σε θέση να **παριστάνουν τους χρήστες του Workspace** της ίδιας οργάνωσης (ή ακόμη και από μια διαφορετική). +> Αυτό σημαίνει βασικά ότι οι **service accounts** μέσα σε έργα GCP μιας οργάνωσης μπορεί να είναι σε θέση να **παριστάνουν τους χρήστες του Workspace** της ίδιας οργάνωσης (ή ακόμα και από μια διαφορετική). Για περισσότερες πληροφορίες σχετικά με το πώς λειτουργεί αυτό ακριβώς, ελέγξτε: @@ -17,13 +17,13 @@ gcp-understanding-domain-wide-delegation.md {{#endref}} -### Συμβιβασμός Υφιστάμενης Εξουσιοδότησης +### Συμβιβασμός υπάρχουσας εξουσιοδότησης -Εάν ένας επιτιθέμενος **συμβιβάσει κάποια πρόσβαση μέσω GCP** και **γνωρίζει μια έγκυρη διεύθυνση email χρήστη του Workspace** (κατά προτίμηση **super admin**) της εταιρείας, θα μπορούσε να **καταγράψει όλα τα έργα** στα οποία έχει πρόσβαση, **να καταγράψει όλους τους Λογαριασμούς Υπηρεσίας** των έργων, να ελέγξει σε ποιους **λογαριασμούς υπηρεσίας έχει πρόσβαση**, και **να επαναλάβει** όλα αυτά τα βήματα με κάθε Λογαριασμό Υπηρεσίας που μπορεί να παριστάνει.\ -Με μια **λίστα όλων των λογαριασμών υπηρεσίας** στους οποίους έχει **πρόσβαση** και τη λίστα των **emails του Workspace**, ο επιτιθέμενος θα μπορούσε να προσπαθήσει να **παριστάνει τον χρήστη με κάθε λογαριασμό υπηρεσίας**. +Εάν ένας επιτιθέμενος **συμβιβάσει κάποια πρόσβαση μέσω GCP** και **γνωρίζει μια έγκυρη διεύθυνση email χρήστη του Workspace** (κατά προτίμηση **super admin**) της εταιρείας, θα μπορούσε να **καταγράψει όλα τα έργα** στα οποία έχει πρόσβαση, **να καταγράψει όλους τους SAs** των έργων, να ελέγξει σε ποιους **service accounts έχει πρόσβαση**, και **να επαναλάβει** όλα αυτά τα βήματα με κάθε SA που μπορεί να παριστάνει.\ +Με μια **λίστα όλων των service accounts** που έχει **πρόσβαση** και τη λίστα με τις **διευθύνσεις email του Workspace**, ο επιτιθέμενος θα μπορούσε να προσπαθήσει να **παριστάνει τον χρήστη με κάθε service account**. > [!CAUTION] -> Σημειώστε ότι κατά τη ρύθμιση της εξουσιοδότησης σε επίπεδο τομέα δεν απαιτείται κανένας χρήστης του Workspace, επομένως αρκεί να γνωρίζετε **έναν έγκυρο για την παριστία**.\ +> Σημειώστε ότι κατά τη ρύθμιση της εξουσιοδότησης σε επίπεδο τομέα δεν απαιτείται κανένας χρήστης του Workspace, επομένως αρκεί να γνωρίζετε **έναν έγκυρο και απαιτείται για την παριστία**.\ > Ωστόσο, οι **προνομίες του παριστάμενου χρήστη θα χρησιμοποιηθούν**, οπότε αν είναι Super Admin θα μπορείτε να έχετε πρόσβαση σε όλα. Αν δεν έχει καμία πρόσβαση, αυτό θα είναι άχρηστο. #### [GCP Generate Delegation Token](https://github.com/carlospolop/gcp_gen_delegation_token) @@ -36,22 +36,26 @@ python3 gen_delegation_token.py --user-email --key-file --key-file --scopes "https://www.googleapis.com/auth/userinfo.email, https://www.googleapis.com/auth/cloud-platform, https://www.googleapis.com/auth/admin.directory.group, https://www.googleapis.com/auth/admin.directory.user, https://www.googleapis.com/auth/admin.directory.domain, https://mail.google.com/, https://www.googleapis.com/auth/drive, openid" ``` +#### [**DelePwn**](https://github.com/n0tspam/delepwn) + +Βασισμένο στο εργαλείο DeleFriend, αλλά με κάποιες προσθήκες όπως η δυνατότητα να καταμετρά το domain, drive, gmail, calendar και να εκτελεί άλλες λειτουργίες. + #### [**DeleFriend**](https://github.com/axon-git/DeleFriend) -Αυτό είναι ένα εργαλείο που μπορεί να εκτελέσει την επίθεση ακολουθώντας αυτά τα βήματα: +Αυτό είναι ένα εργαλείο που μπορεί να εκτελέσει την επίθεση ακολουθώντας τα παρακάτω βήματα: -1. **Αναγνωρίστε τα GCP Projects** χρησιμοποιώντας το Resource Manager API. -2. Επαναλάβετε για κάθε πόρο έργου και **αναγνωρίστε τους πόρους λογαριασμού υπηρεσίας GCP** στους οποίους έχει πρόσβαση ο αρχικός χρήστης IAM χρησιμοποιώντας το _GetIAMPolicy_. -3. Επαναλάβετε για **κάθε ρόλο λογαριασμού υπηρεσίας** και βρείτε ενσωματωμένους, βασικούς και προσαρμοσμένους ρόλους με άδεια _**serviceAccountKeys.create**_ στον πόρο λογαριασμού υπηρεσίας στόχου. Πρέπει να σημειωθεί ότι ο ρόλος του Editor κατέχει εγγενώς αυτή την άδεια. -4. Δημιουργήστε ένα **νέο `KEY_ALG_RSA_2048`** ιδιωτικό κλειδί για κάθε πόρο λογαριασμού υπηρεσίας που βρέθηκε με σχετική άδεια στην πολιτική IAM. -5. Επαναλάβετε για **κάθε νέο λογαριασμό υπηρεσίας και δημιουργήστε ένα `JWT`** **αντικείμενο** γι' αυτό που αποτελείται από τα διαπιστευτήρια του ιδιωτικού κλειδιού SA και μια έκταση OAuth. Η διαδικασία δημιουργίας ενός νέου _JWT_ αντικειμένου θα **επαναλάβει όλους τους υπάρχοντες συνδυασμούς εκτάσεων OAuth** από τη λίστα **oauth_scopes.txt**, προκειμένου να βρει όλες τις δυνατότητες αντιπροσώπευσης. Η λίστα **oauth_scopes.txt** ενημερώνεται με όλες τις εκτάσεις OAuth που έχουμε βρει ότι είναι σχετικές για την κατάχρηση ταυτοτήτων Workspace. -6. Η μέθοδος `_make_authorization_grant_assertion` αποκαλύπτει την ανάγκη να δηλωθεί ένας **στόχος χρήστη workspace**, αναφερόμενος ως _subject_, για τη δημιουργία JWTs υπό DWD. Ενώ αυτό μπορεί να φαίνεται ότι απαιτεί έναν συγκεκριμένο χρήστη, είναι σημαντικό να συνειδητοποιήσουμε ότι **η DWD επηρεάζει κάθε ταυτότητα εντός ενός τομέα**. Συνεπώς, η δημιουργία ενός JWT για **οποιονδήποτε χρήστη τομέα** επηρεάζει όλες τις ταυτότητες σε αυτόν τον τομέα, σύμφωνα με τον έλεγχο συνδυασμού μας. Απλά, ένας έγκυρος χρήστης Workspace είναι επαρκής για να προχωρήσουμε.\ -Αυτός ο χρήστης μπορεί να οριστεί στο αρχείο _config.yaml_ του DeleFriend. Εάν δεν είναι ήδη γνωστός ένας χρήστης στόχος workspace, το εργαλείο διευκολύνει την αυτόματη αναγνώριση έγκυρων χρηστών workspace σκανάροντας τους χρήστες τομέα με ρόλους σε έργα GCP. Είναι σημαντικό να σημειωθεί (ξανά) ότι τα JWT είναι συγκεκριμένα για τομέα και δεν δημιουργούνται για κάθε χρήστη. Επομένως, η αυτόματη διαδικασία στοχεύει σε μία μοναδική ταυτότητα ανά τομέα. -7. **Αναγνωρίστε και δημιουργήστε ένα νέο bearer access token** για κάθε JWT και επικυρώστε το token μέσω του tokeninfo API. +1. **Καταμέτρηση GCP Projects** χρησιμοποιώντας το Resource Manager API. +2. Επαναλάβετε για κάθε πόρο έργου, και **καταμετρήστε τους πόρους GCP Service account** στους οποίους έχει πρόσβαση ο αρχικός IAM χρήστης χρησιμοποιώντας το _GetIAMPolicy_. +3. Επαναλάβετε για **κάθε ρόλο service account**, και βρείτε ενσωματωμένους, βασικούς και προσαρμοσμένους ρόλους με άδεια _**serviceAccountKeys.create**_ στον πόρο του στόχου service account. Πρέπει να σημειωθεί ότι ο ρόλος Editor κατέχει εγγενώς αυτή την άδεια. +4. Δημιουργήστε ένα **νέο `KEY_ALG_RSA_2048`** ιδιωτικό κλειδί για κάθε πόρο service account που βρέθηκε με σχετική άδεια στην πολιτική IAM. +5. Επαναλάβετε για **κάθε νέο service account και δημιουργήστε ένα `JWT`** **αντικείμενο** για αυτό που αποτελείται από τα διαπιστευτήρια του ιδιωτικού κλειδιού SA και μια OAuth έκταση. Η διαδικασία δημιουργίας ενός νέου _JWT_ αντικειμένου θα **επαναλάβει όλους τους υπάρχοντες συνδυασμούς OAuth εκτάσεων** από τη λίστα **oauth_scopes.txt**, προκειμένου να βρει όλες τις δυνατότητες αντιπροσώπευσης. Η λίστα **oauth_scopes.txt** ενημερώνεται με όλες τις OAuth εκτάσεις που έχουμε βρει ότι είναι σχετικές για την κατάχρηση ταυτοτήτων Workspace. +6. Η μέθοδος `_make_authorization_grant_assertion` αποκαλύπτει την ανάγκη να δηλωθεί ένας **στόχος χρήστη workspace**, αναφερόμενος ως _subject_, για τη δημιουργία JWTs υπό DWD. Ενώ αυτό μπορεί να φαίνεται ότι απαιτεί έναν συγκεκριμένο χρήστη, είναι σημαντικό να συνειδητοποιήσουμε ότι **η DWD επηρεάζει κάθε ταυτότητα εντός ενός domain**. Συνεπώς, η δημιουργία ενός JWT για **οποιονδήποτε χρήστη του domain** επηρεάζει όλες τις ταυτότητες σε αυτό το domain, σύμφωνα με τον έλεγχο καταμέτρησης συνδυασμών μας. Απλά, ένας έγκυρος χρήστης Workspace είναι επαρκής για να προχωρήσουμε.\ +Αυτός ο χρήστης μπορεί να οριστεί στο αρχείο _config.yaml_ του DeleFriend. Εάν δεν είναι ήδη γνωστός ένας στόχος χρήστη workspace, το εργαλείο διευκολύνει την αυτόματη αναγνώριση έγκυρων χρηστών workspace σκανάροντας τους χρήστες του domain με ρόλους σε GCP projects. Είναι σημαντικό να σημειωθεί (ξανά) ότι τα JWT είναι συγκεκριμένα για το domain και δεν δημιουργούνται για κάθε χρήστη· επομένως, η αυτόματη διαδικασία στοχεύει σε μία μοναδική ταυτότητα ανά domain. +7. **Καταμέτρηση και δημιουργία ενός νέου bearer access token** για κάθε JWT και επικύρωση του token μέσω του tokeninfo API. #### [Gitlab's Python script](https://gitlab.com/gitlab-com/gl-security/threatmanagement/redteam/redteam-public/gcp_misc/-/blob/master/gcp_delegation.py) -Η Gitlab έχει δημιουργήσει [αυτό το Python script](https://gitlab.com/gitlab-com/gl-security/gl-redteam/gcp_misc/blob/master/gcp_delegation.py) που μπορεί να κάνει δύο πράγματα - να καταγράψει τον κατάλογο χρηστών και να δημιουργήσει έναν νέο διοικητικό λογαριασμό ενώ υποδεικνύει ένα json με διαπιστευτήρια SA και τον χρήστη που θα μιμηθεί. Να πώς θα το χρησιμοποιούσατε: +Η Gitlab έχει δημιουργήσει [αυτό το Python script](https://gitlab.com/gitlab-com/gl-security/gl-redteam/gcp_misc/blob/master/gcp_delegation.py) που μπορεί να κάνει δύο πράγματα - να καταγράψει τον κατάλογο χρηστών και να δημιουργήσει έναν νέο διαχειριστικό λογαριασμό ενώ υποδεικνύει ένα json με διαπιστευτήρια SA και τον χρήστη που θα μιμηθεί. Εδώ είναι πώς θα το χρησιμοποιούσατε: ```bash # Install requirements pip install --upgrade --user oauth2client @@ -77,15 +81,15 @@ pip install --upgrade --user oauth2client Είναι δυνατόν να **ελέγξετε τις Domain Wide Delegations στο** [**https://admin.google.com/u/1/ac/owl/domainwidedelegation**](https://admin.google.com/u/1/ac/owl/domainwidedelegation)**.** -Ένας επιτιθέμενος με την ικανότητα να **δημιουργεί λογαριασμούς υπηρεσίας σε ένα έργο GCP** και **δικαιώματα super admin στο GWS θα μπορούσε να δημιουργήσει μια νέα εξουσιοδότηση που επιτρέπει στους SAs να προσποιούνται ορισμένους χρήστες του GWS:** +Ένας επιτιθέμενος με την ικανότητα να **δημιουργεί λογαριασμούς υπηρεσίας σε ένα έργο GCP** και **προνόμια super admin στο GWS θα μπορούσε να δημιουργήσει μια νέα εξουσιοδότηση που επιτρέπει στους SAs να προσποιούνται κάποιους χρήστες του GWS:** 1. **Δημιουργία Νέου Λογαριασμού Υπηρεσίας και Σχετικού Ζεύγους Κλειδιών:** Στο GCP, νέοι πόροι λογαριασμού υπηρεσίας μπορούν να παραχθούν είτε διαδραστικά μέσω της κονσόλας είτε προγραμματισμένα χρησιμοποιώντας άμεσες κλήσεις API και εργαλεία CLI. Αυτό απαιτεί το **ρόλο `iam.serviceAccountAdmin`** ή οποιονδήποτε προσαρμοσμένο ρόλο εξοπλισμένο με την **άδεια `iam.serviceAccounts.create`**. Μόλις δημιουργηθεί ο λογαριασμός υπηρεσίας, θα προχωρήσουμε στη δημιουργία ενός **σχετικού ζεύγους κλειδιών** (**άδεια `iam.serviceAccountKeys.create`**). -2. **Δημιουργία νέας εξουσιοδότησης**: Είναι σημαντικό να κατανοήσουμε ότι **μόνο ο ρόλος Super Admin έχει τη δυνατότητα να ρυθμίσει παγκόσμια Domain-Wide εξουσιοδότηση στο Google Workspace** και η Domain-Wide εξουσιοδότηση **δεν μπορεί να ρυθμιστεί προγραμματισμένα,** μπορεί να δημιουργηθεί και να ρυθμιστεί **χειροκίνητα** μέσω της κονσόλας Google Workspace. +2. **Δημιουργία νέας εξουσιοδότησης**: Είναι σημαντικό να κατανοήσουμε ότι **μόνο ο ρόλος Super Admin έχει τη δυνατότητα να ρυθμίσει παγκόσμια Domain-Wide εξουσιοδότηση στο Google Workspace** και η Domain-Wide εξουσιοδότηση **δεν μπορεί να ρυθμιστεί προγραμματισμένα,** μπορεί να δημιουργηθεί και να προσαρμοστεί **χειροκίνητα** μέσω της κονσόλας Google Workspace. - Η δημιουργία του κανόνα μπορεί να βρεθεί στη σελίδα **API controls → Manage Domain-Wide delegation in Google Workspace Admin console**. -3. **Επισύναψη δικαιωμάτων OAuth scopes**: Κατά τη ρύθμιση μιας νέας εξουσιοδότησης, η Google απαιτεί μόνο 2 παραμέτρους, το Client ID, το οποίο είναι το **OAuth ID του πόρου GCP Service Account**, και **OAuth scopes** που καθορίζουν ποιες κλήσεις API απαιτεί η εξουσιοδότηση. +3. **Σύνδεση προνομίων OAuth scopes**: Κατά τη ρύθμιση μιας νέας εξουσιοδότησης, η Google απαιτεί μόνο 2 παραμέτρους, το Client ID, το οποίο είναι το **OAuth ID του πόρου GCP Service Account**, και **OAuth scopes** που καθορίζουν ποιες κλήσεις API απαιτεί η εξουσιοδότηση. - Η **πλήρης λίστα των OAuth scopes** μπορεί να βρεθεί [**εδώ**](https://developers.google.com/identity/protocols/oauth2/scopes), αλλά εδώ είναι μια σύσταση: `https://www.googleapis.com/auth/userinfo.email, https://www.googleapis.com/auth/cloud-platform, https://www.googleapis.com/auth/admin.directory.group, https://www.googleapis.com/auth/admin.directory.user, https://www.googleapis.com/auth/admin.directory.domain, https://mail.google.com/, https://www.googleapis.com/auth/drive, openid` -4. **Δράση εκ μέρους της στοχευμένης ταυτότητας:** Σε αυτό το σημείο, έχουμε ένα λειτουργικό εξουσιοδοτημένο αντικείμενο στο GWS. Τώρα, **χρησιμοποιώντας το ιδιωτικό κλειδί του GCP Service Account, μπορούμε να εκτελέσουμε κλήσεις API** (στο πεδίο που καθορίζεται στην παράμετρο OAuth scope) για να το ενεργοποιήσουμε και **να δράσουμε εκ μέρους οποιασδήποτε ταυτότητας που υπάρχει στο Google Workspace**. Όπως μάθαμε, ο λογαριασμός υπηρεσίας θα δημιουργήσει διαπιστευτήρια πρόσβασης ανάλογα με τις ανάγκες του και σύμφωνα με την άδεια που έχει για τις εφαρμογές REST API. -- Ελέγξτε την **προηγούμενη ενότητα** για ορισμένα **εργαλεία** για να χρησιμοποιήσετε αυτήν την εξουσιοδότηση. +4. **Δράση εκ μέρους της στοχευμένης ταυτότητας:** Σε αυτό το σημείο, έχουμε ένα λειτουργικό εξουσιοδοτημένο αντικείμενο στο GWS. Τώρα, **χρησιμοποιώντας το ιδιωτικό κλειδί του GCP Service Account, μπορούμε να εκτελέσουμε κλήσεις API** (στο πεδίο που καθορίζεται στην παράμετρο OAuth scope) για να το ενεργοποιήσουμε και **να δράσουμε εκ μέρους οποιασδήποτε ταυτότητας που υπάρχει στο Google Workspace**. Όπως μάθαμε, ο λογαριασμός υπηρεσίας θα δημιουργήσει διακριτικά πρόσβασης ανάλογα με τις ανάγκες του και σύμφωνα με την άδεια που έχει για τις εφαρμογές REST API. +- Ελέγξτε την **προηγούμενη ενότητα** για κάποια **εργαλεία** για να χρησιμοποιήσετε αυτή την εξουσιοδότηση. #### Διασυνοριακή εξουσιοδότηση @@ -93,7 +97,7 @@ pip install --upgrade --user oauth2client ### Δημιουργία Έργου για την καταμέτρηση του Workspace -Κατά **προεπιλογή**, οι χρήστες του Workspace έχουν την άδεια να **δημιουργούν νέα έργα**, και όταν δημιουργείται ένα νέο έργο, ο **δημιουργός αποκτά το ρόλο του Ιδιοκτήτη**. +Από **προεπιλογή** οι χρήστες του Workspace έχουν την άδεια να **δημιουργούν νέα έργα**, και όταν δημιουργείται ένα νέο έργο, ο **δημιουργός αποκτά το ρόλο του Ιδιοκτήτη** σε αυτό. Επομένως, ένας χρήστης μπορεί να **δημιουργήσει ένα έργο**, **να ενεργοποιήσει** τις **API** για να καταμετρήσει το Workspace στο νέο του έργο και να προσπαθήσει να **καταμετρήσει** αυτό. @@ -152,9 +156,9 @@ gcloud auth login --enable-gdrive-access ### Κλιμάκωση προνομίων Google Groups -Από προεπιλογή, οι χρήστες μπορούν να **ενταχθούν ελεύθερα σε ομάδες Workspace της Οργάνωσης** και αυτές οι ομάδες **μπορεί να έχουν εκχωρημένα δικαιώματα GCP** (ελέγξτε τις ομάδες σας στο [https://groups.google.com/](https://groups.google.com/)). +Από προεπιλογή, οι χρήστες μπορούν να **εντάσσονται ελεύθερα σε ομάδες Workspace της Οργάνωσης** και αυτές οι ομάδες **μπορεί να έχουν εκχωρημένα δικαιώματα GCP** (ελέγξτε τις ομάδες σας στο [https://groups.google.com/](https://groups.google.com/)). -Εκμεταλλευόμενοι την **google groups privesc**, μπορεί να είστε σε θέση να κλιμακώσετε σε μια ομάδα με κάποιο είδος προνομιακής πρόσβασης στο GCP. +Εκμεταλλευόμενοι την **κλιμάκωση προνομίων google groups**, μπορεί να είστε σε θέση να κλιμακωθείτε σε μια ομάδα με κάποιο είδος προνομιακής πρόσβασης στο GCP. ### Αναφορές diff --git a/theme/ai.js b/theme/ai.js index bb8af53b7..c94992d5f 100644 --- a/theme/ai.js +++ b/theme/ai.js @@ -1,27 +1,28 @@ /** - * 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) - * ------------------------------------------------------------------------ + * HackTricks AI Chat Widget v1.16 – resizable sidebar + * --------------------------------------------------- + * ❶ Markdown rendering + sanitised (same as before) + * ❷ NEW: drag‑to‑resize panel, width persists via localStorage */ (function () { - const LOG = "[HackTricks-AI]"; - - /* ---------------- User-tunable constants ---------------- */ - const MAX_CONTEXT = 3000; // highlighted-text char limit - const MAX_QUESTION = 500; // question char limit + const LOG = "[HackTricks‑AI]"; + /* ---------------- User‑tunable constants ---------------- */ + const MAX_CONTEXT = 3000; // highlighted‑text char limit + const MAX_QUESTION = 500; // question char limit + const MIN_W = 250; // ← resize limits → + const MAX_W = 600; + const DEF_W = 350; // default width (if nothing saved) const TOOLTIP_TEXT = - "💡 Highlight any text on the page,\nthen click to ask HackTricks AI about it"; + "💡 Highlight any text on the page,\nthen click to ask HackTricks AI about it"; const API_BASE = "https://www.hacktricks.ai/api/assistants/threads"; - const BRAND_RED = "#b31328"; // HackTricks brand + const BRAND_RED = "#b31328"; /* ------------------------------ State ------------------------------ */ let threadId = null; let isRunning = false; + /* ---------- helpers ---------- */ const $ = (sel, ctx = document) => ctx.querySelector(sel); if (document.getElementById("ht-ai-btn")) { console.warn(`${LOG} Widget already injected.`); @@ -31,44 +32,37 @@ ? document.addEventListener("DOMContentLoaded", init) : init()); - /* ==================================================================== */ - /* 🔗 1. 3rd-party libs → Markdown & sanitiser */ - /* ==================================================================== */ + /* =================================================================== */ + /* 🔗 1. 3rd‑party libs → Markdown & sanitiser */ + /* =================================================================== */ function loadScript(src) { - return new Promise((resolve, reject) => { + return new Promise((res, rej) => { const s = document.createElement("script"); s.src = src; - s.onload = resolve; - s.onerror = () => reject(new Error(`Failed to load ${src}`)); + s.onload = res; + s.onerror = () => rej(new Error(`Failed to load ${src}`)); document.head.appendChild(s); }); } - async function ensureDeps() { const deps = []; - if (typeof marked === "undefined") { + if (typeof marked === "undefined") deps.push(loadScript("https://cdn.jsdelivr.net/npm/marked/marked.min.js")); - } - if (typeof DOMPurify === "undefined") { + 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); } + const mdToSafeHTML = (md) => + DOMPurify.sanitize(marked.parse(md, { mangle: false, headerIds: false }), { + USE_PROFILES: { html: true } + }); - function mdToSafeHTML(md) { - // 1️⃣ Markdown → raw HTML - const raw = marked.parse(md, { mangle: false, headerIds: false }); - // 2️⃣ Purify - return DOMPurify.sanitize(raw, { USE_PROFILES: { html: true } }); - } - - /* ==================================================================== */ + /* =================================================================== */ async function init() { - /* ----- make sure marked & DOMPurify are ready before anything else */ try { await ensureDeps(); } catch (e) { @@ -76,14 +70,14 @@ return; } - console.log(`${LOG} Injecting widget… v1.15`); + console.log(`${LOG} Injecting widget… v1.16`); await ensureThreadId(); injectStyles(); const btn = createFloatingButton(); createTooltip(btn); - const panel = createSidebar(); + const panel = createSidebar(); // ← panel with resizer const chatLog = $("#ht-ai-chat"); const sendBtn = $("#ht-ai-send"); const inputBox = $("#ht-ai-question"); @@ -100,15 +94,8 @@ 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; - } - + b[cls === "ht-ai" ? "innerHTML" : "textContent"] = + cls === "ht-ai" ? mdToSafeHTML(text) : text; chatLog.appendChild(b); chatLog.scrollTop = chatLog.scrollHeight; return b; @@ -116,30 +103,28 @@ const LOADER_HTML = ''; - function setInputDisabled(d) { + const setInputDisabled = (d) => { inputBox.disabled = d; sendBtn.disabled = d; - } - function clearThreadCookie() { + }; + const clearThreadCookie = () => { document.cookie = "threadId=; Path=/; Max-Age=0"; - threadId = null; - } - function resetConversation() { + threadId = null; + }; + const 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."); + alert("Please highlight some text first."); return; } if (savedSelection.length > MAX_CONTEXT) { - alert( - `Highlighted text is too long (${savedSelection.length} chars). Max allowed: ${MAX_CONTEXT}.` - ); + alert(`Highlighted text is too long. Max ${MAX_CONTEXT} chars.`); return; } chatLog.innerHTML = ""; @@ -157,11 +142,10 @@ 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 loading = addMsg("", "ht-ai"); + loading.innerHTML = LOADER_HTML; const content = context ? `### Context:\n${context}\n\n### Question to answer:\n${question}` @@ -178,43 +162,39 @@ 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."; + else if (res.status === 429) err = "Rate limit exceeded."; } catch (_) {} - loadingBubble.textContent = err; + loading.textContent = err; return; } const data = await res.json(); - loadingBubble.remove(); + loading.remove(); if (Array.isArray(data.response)) - data.response.forEach((p) => { + 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."; + loading.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}.` - ); + alert(`Question too long (${q.length}). Max ${MAX_QUESTION}.`); return; } inputBox.value = ""; @@ -228,9 +208,9 @@ handleSend(); } }); - } + } /* end init */ - /* ==================================================================== */ + /* =================================================================== */ async function ensureThreadId() { const m = document.cookie.match(/threadId=([^;]+)/); if (m && m[1]) { @@ -241,62 +221,67 @@ 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; + 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."); + alert("Failed to initialise the conversation. Please refresh."); 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}`; +#ht-ai-btn{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);min-width:60px;height:60px;border-radius:30px;background:linear-gradient(45deg, #b31328, #d42d3f, #2d5db4, #3470e4);background-size:300% 300%;animation:gradientShift 8s ease infinite;color:#fff;font-size:18px;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:99999;box-shadow:0 2px 8px rgba(0,0,0,.4);transition:opacity .2s;padding:0 20px} +#ht-ai-btn span{margin-left:8px;font-weight:bold} +@keyframes gradientShift{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}} +#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%;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 */ +.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} +/* NEW: resizer handle */ +#ht-ai-resizer{position:absolute;left:0;top:0;width:6px;height:100%;cursor:ew-resize;background:transparent} +#ht-ai-resizer:hover{background:rgba(255,255,255,.05)}`; const s = document.createElement("style"); 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 = "🤖"; + d.innerHTML = "🤖HackTricksAI"; document.body.appendChild(d); return d; } - function createTooltip(btn) { const t = document.createElement("div"); t.id = "ht-ai-tooltip"; @@ -311,11 +296,16 @@ btn.addEventListener("mouseleave", () => t.classList.remove("show")); } + /* =================================================================== */ function createSidebar() { + const saved = parseInt(localStorage.getItem("htAiWidth") || DEF_W, 10); + const width = Math.min(Math.max(saved, MIN_W), MAX_W); + const p = document.createElement("div"); p.id = "ht-ai-panel"; + p.style.width = width + "px"; // ← applied width p.innerHTML = ` -
HackTricks AI Chat +
HackTricks AI Chat
@@ -326,7 +316,39 @@
`; + /* NEW: resizer strip */ + const resizer = document.createElement("div"); + resizer.id = "ht-ai-resizer"; + p.appendChild(resizer); document.body.appendChild(p); + addResizeLogic(resizer, p); return p; } + + /* ---------------- resize behaviour ---------------- */ + function addResizeLogic(handle, panel) { + let startX, startW, dragging = false; + + const onMove = (e) => { + if (!dragging) return; + const dx = startX - e.clientX; // dragging leftwards ⇒ +dx + let newW = startW + dx; + newW = Math.min(Math.max(newW, MIN_W), MAX_W); + panel.style.width = newW + "px"; + }; + const onUp = () => { + if (!dragging) return; + dragging = false; + localStorage.setItem("htAiWidth", parseInt(panel.style.width, 10)); + document.removeEventListener("mousemove", onMove); + document.removeEventListener("mouseup", onUp); + }; + handle.addEventListener("mousedown", (e) => { + dragging = true; + startX = e.clientX; + startW = parseInt(window.getComputedStyle(panel).width, 10); + document.addEventListener("mousemove", onMove); + document.addEventListener("mouseup", onUp); + }); + } })();