Repository navigation
fix: install hints resolving to unpublished PyPI names, plus four silent scanner defects - #174
Open
spierenburg wants to merge 6 commits into
Open
spierenburg wants to merge 6 commits into
spierenburg wants to merge 6 commits into
Conversation
…ished
Several install hints told users to `pip install <name>` for tools whose
upstreams have no PyPI release. Those names are unregistered, so the hint
resolves to whatever a third party uploads under them — and two (graphw00f,
gqlmap) are already occupied by third-party placeholder packages rather than
the real tools.
Verified against the PyPI JSON API: ghauri, jwt-tool, dnsreaper, whatwaf,
cloud-enum, sublert, linkfinder and graphql-cop all return 404.
Changes:
- install from upstream source (`pipx install git+<repo>` where the project
ships setup.py/pyproject, else `git clone`). Tested: ghauri and cloud_enum
install correctly via git+.
- knockpy -> knock-subdomains. PyPI `knockpy` is an unrelated statistics
package ("Knockoffs for variable selection"), not the subdomain scanner.
- gqlmap -> swisskyrepo/GraphQLmap. The cited upstream
(nicola-inchingolo/gqlmap) is a dead link; the real tool of that name is
an interactive REPL, noted as such.
- aquatone marked archived, suggests gowitness.
- bbot/x8 note the Rust toolchain requirement; mobsf pinned to python3.11.
- scoutsuite registry key -> scout, its actual binary name (was reporting a
false MISSING).
- clairvoyance left as-is and marked verified — it is genuinely published by
its upstream author.
- unescaped `\&\&` -> `&&` so hints are copy-pasteable.
- documented the rule in a comment above the array.
Registry still reports the same installed/missing counts; `--install-hint`
round-trips for every changed entry.
Phase 5 gated on `_have gqlmap`. There is no such binary: the upstream cited
in external_arsenal.sh (nicola-inchingolo/gqlmap) returns 404, and the PyPI
name is a third-party placeholder. So the gate was never true, the else branch
always ran, and summary.txt always reported:
injection_scan: skipped (gqlmap not installed)
while the built-in SQLi probe inside that else branch did the actual work.
Anyone reading the summary would conclude injection was not tested.
Removed the conditional and promoted the built-in probe to be the phase.
Renamed its output gqlmap.txt -> injection.txt to match.
Not repointed at swisskyrepo/GraphQLmap (the real tool of that name): its
entrypoint is `while True: input("GraphQLmap > ")`, an interactive REPL with no
batch mode, so driving it from this non-interactive pipeline would block on
stdin or die on EOF. Documented in SKILL.md as a manual follow-up tool instead,
with its actual CLI (-u, not --target).
Also corrected the two skip-branch install hints in this file (graphw00f,
graphql-cop) for the same reason as the arsenal change.
…ecks Three defects, all silent — nothing in the output indicates reduced coverage or a fabricated finding. 1. Catch-all detection never took effect (false positives) `head -10 "$ORDERED_SCAN" | while read` ran the loop in a subshell, so the CATCHALL_HOSTS assignments were discarded on exit and the skip below could never fire — even on hosts where "Catchall detected" was correctly logged. Replaced both pipes with process substitution so the loops run in the current shell. Effect on a host that answers 200 for unknown paths (i.e. most SPAs): all eight PROBE_PATHS were reported as [UPLOAD-CANDIDATE] and each triggered a verify_upload_poc attempt, against paths that do not exist. 2. SAML endpoint discovery had no baseline at all The check accepted 200|301|302|403 as "endpoint found" with no control probe — not even the (broken) logic the upload check attempts. Added a per-host control probe against a random path; if it returns one of those codes the check is skipped with a logged reason, since the response carries no information about SAML. Note 403 was already accepted, so a host returning 403 for unknown paths reported the entire SAML path list. Measured against a catch-all target, before -> after: phantom upload candidates 8 -> 0 phantom SAML endpoints 9 -> 0 3. Four curl calls dropped credentials on authenticated runs The catch-all baseline, the upload probe loop, SAML endpoint discovery and SAML metadata retrieval omitted $BB_AUTH_ARGS while neighbouring calls include it. The upload one mattered most: behind a login every probe got a 401, found nothing, and the run reported no findings — readable as "no upload surface" rather than "not tested". The baseline was also taken anonymously and compared against authenticated probes. Left deliberately unauthenticated, now with comments saying so, since both would be destroyed by adding the session: the MFA workflow-skip test (asks whether a page is reachable WITHOUT auth) and the SAML signature-stripping probe (forges a login; replaying with a valid session returns 200/302 regardless, producing a false CRITICAL). The three calls in recon_engine.sh are also correct as-is — they query crt.sh and web.archive.org, which must never receive a target session cookie.
…success
BREW_CRED_TOOLS=("hashcat" "theharvester")
for tool in "${BREW_CRED_TOOLS[@]}"; do
if command -v "$tool" &>/dev/null || command -v "theHarvester" &>/dev/null; then
log_ok "$tool already installed"
The `|| command -v "theHarvester"` clause applied on every iteration, not just
the theharvester one. The alias is needed because theHarvester's binary is
capitalised and never matches its formula name — but as written, any user who
already had theHarvester got "[+] hashcat already installed" and no install.
Reproduced on macOS 15 / arm64: --with-credential-attack finished reporting
15/16 with `hashcat: NOT FOUND` in its own verification block, contradicting the
earlier success line. `brew install hashcat` then worked normally. The failure
mode is a false success, so it is easy to miss.
Scoped the alias check to theharvester only.
… clone hints
Three defects found reviewing the earlier commits on this branch.
1. The catch-all skip matched by substring, not host equality
[[ "$CATCHALL_HOSTS" == *"$host"* ]] && continue
84cc1e4 fixed the subshell that made CATCHALL_HOSTS always empty — which also
promoted this comparison from dead code to live logic, and it is unanchored. A
recorded "https://api.t.com:8443" would suppress the probed "https://api.t.com",
silently skipping a host that was never detected as catch-all. Entries are now
stored ",host," and matched with a comma-anchored `case`.
Verified: recorded hosts skip, hosts that are merely substrings of a recorded
entry are probed.
2. The SAML metadata and ACS legs read the wrong awk field
endpoints.txt rows are written as
[INFORMATIONAL] [SAML-ENDPOINT] <url> | HTTP <code>
so the URL is field 3, but both consumers read field 2 — the literal tag
"[SAML-ENDPOINT]". curl rejects that at URL-parse time (exit 3), so the metadata
exposure check and the ACS_URL that feeds the signature-stripping probe have
never reached the network. Pre-existing, not introduced here.
CORRECTION TO 84cc1e4: that commit's subject claims "auth propagation in
upload/SAML checks", and it added $BB_AUTH_ARGS to the metadata call. That splat
was inert, because the leg it sits in never ran. The claim was wrong as written;
this commit makes it true by fixing the field index.
Behavioral consequence worth flagging for review: fixing this activates two legs
that previously could not fire. The metadata check is a GET. The
signature-stripping probe POSTs a forged SAMLResponse — it remains behind
unsafe_method_guard / ALLOW_UNSAFE_HTTP_TESTS, which
tests/test_vuln_scanner_review_fixes.py already asserts, so the policy gate is
the control. But it will now actually execute where the policy allows it, for
the first time.
3. Install hints chained commands without entering the clone
git clone .../jwt_tool && pip install -r requirements.txt
git clone leaves the shell in the parent directory, so requirements.txt and
./Python/setup/setup.sh resolved against the user's current directory. In a
recon directory holding cloned target source, pasting that hint installs the
target's requirements and runs its build hooks. Six hints fixed to cd into the
clone first, matching the form skills/graphql-audit/SKILL.md already used.
Regression: catch-all suppression still holds after the change to the matching
logic — against a host returning 200 for every path, phantom UPLOAD-CANDIDATE
and SAML-ENDPOINT both remain 0 (were 8 and 9 on origin/main), with the target
verified responding 200 before and after the run.
Each defect this branch fixes was silent — the scanner emitted or suppressed findings with nothing in its output to indicate coverage had changed. A regression would therefore be invisible in a manual run, and `grep` of tests/ showed zero coverage for any of the three constructs. Six source-level assertions, in the style of tests/test_vuln_scanner_review_fixes.py: - catch-all loops run in the current shell (no `head -N … | while read`) - catch-all match is comma-anchored, not unanchored substring - SAML endpoint discovery has a control probe, ordered before the path loop - endpoints.txt consumers read field 3 (the URL), never field 2 (the tag) - the four previously-anonymous curl calls carry $BB_AUTH_ARGS - the two deliberately-anonymous probes still do NOT carry it That last one is the point of the file as much as the others: the MFA workflow-skip and SAML signature-stripping probes look like missing-auth bugs to anyone auditing for them, and "fixing" either silently destroys the check. This fails loudly instead. Each assertion was mutation-tested: reintroducing the pipe, un-anchoring the match, reverting the awk field to 2, removing a splat, and wrongly adding one to the signature-stripping probe each make the corresponding test fail. A test that passes under both the fixed and broken code is not a regression guard. Two scoping notes: - The pipe check is scoped to the two loops that accumulate state across iterations. The SQLi and CMS phases also pipe into `while read`, but assign only per-iteration variables, so their subshells are harmless and must not trip this. - Multi-line curl commands are compared with backslash-continuations joined; a per-physical-line check cannot see a splat on the preceding line.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
pip install <name>resolves to whatever a third party registers under them; two (graphw00f,gqlmap) are already occupied by third-party placeholder packages. Now install from upstream source. Separately,knockpyon PyPI is an unrelated statistics library, so that hint installed the wrong software entirely.curlcalls dropped credentials on authenticated runs.Measured on a host that returns 200 for every path (as React/Vue/Angular front ends do): phantom
[UPLOAD-CANDIDATE]8 → 0, phantom[SAML-ENDPOINT]9 → 0.Type
Test Plan
— 1124 passed, 4 failed. Measured against a clean worktree ofpytest tests/passes locallymainat40b03ee, which gives 1118 passed, 4 failed — the same four, so this branch adds 6 passing tests and no failures. Leaving the box unchecked rather than claiming a green suite. The four are pre-existing and unrelated:test_benchmark.py::test_verifier_benchmark_recall_and_no_false_positives—benchmark targets failed to starttest_benchmark.py::test_shipped_demo_is_hardened—demo app failed to starttest_brain_auto_detect.py::test_openai_and_grok_keys_both_front_nothing_availabletest_brain_auto_detect.py::test_no_keys_falls_back_to_default_priorityNo hardcoded targets, API keys, or real domain names — no keys or secrets added. The only domain this diff introduces is
target.com, in oneSKILL.mdusage example, matching the placeholder convention already used ~70 times across 19 files onmain(including 40 in that same file). Flagging in case you'd rather standardise onexample.com.Scanner additions use
[CONFIRMED]/[POSSIBLE]/[INFORMATIONAL]confidence states — no new finding types are introduced; existing states are preserved, and the changes reduce false[INFORMATIONAL]output.New functionality has at least one regression test —
tests/test_vuln_scanner_catchall_guards.py, 6 assertions in the style of the existingtest_vuln_scanner_review_fixes.py. Nothing covered any of these constructs before.Each defect here was silent — the scanner emitted or suppressed findings with nothing in its output to say coverage had changed — so a regression would be invisible in a manual run. Every assertion was mutation-tested: reintroducing the pipe, un-anchoring the match, reverting the
awkfield to 2, removing a splat, and wrongly adding one to the signature-stripping probe each make the corresponding test fail. A test that passes under both the fixed and the broken code is not a regression guard.One of the six is the inverse of the others: it asserts the MFA workflow-skip and SAML signature-stripping probes still do not carry
$BB_AUTH_ARGS. Both look like missing-auth bugs to anyone auditing for them, and "fixing" either silently destroys the check — so that now fails loudly instead.tests/test_vuln_scanner_review_fixes.pypasses, includingtest_saml_signature_stripping_is_opt_in_and_policy_gated— which is why the two deliberately-anonymous probes below were left alone rather than "fixed".bash -nclean on all four shell files.Related Issue
Closes #
(No existing issue — found while using the toolkit. The supply-chain half was also reported privately per
SECURITY.md, since §1 names registry entries that are currently unclaimed.)Detail
1. Install hints resolve to unpublished PyPI names
bughunter/tools/external_arsenal.shinstructedpip/pipx install <name>for tools whose upstreams have no PyPI release. Verified against the PyPI JSON API — each returns 404:Python packages execute arbitrary code at install time, so whoever registers one of these gets execution on anyone who follows the hint — without them ever running the tool. Two names are already taken by third-party placeholder packages (
graphw00f,gqlmap, both v0.0.1) whose own description says they were registered defensively; following those hints today installs something that is not the tool.Fixed by installing from upstream source:
pipx install git+<repo>where the project shipssetup.py/pyproject.toml(tested working forghauri1.4.3 andcloud_enum0.8), elsegit clone.Also in this area:
knockpyinstalled unrelated software. PyPIknockpyis "Knockoffs for variable selection" v1.3.5, a statistics library — not the subdomain scanner. Changed toknock-subdomainsv9.0.0, which provides aknockpybinary. Tested.gqlmaphas no upstream. The cited reponicola-inchingolo/gqlmapreturns 404. Repointed atswisskyrepo/GraphQLmap, noting it is an interactive REPL. Known gap: the registry key is stillgqlmapwhile GraphQLmap's binary isgraphqlmap, so_havewill keep reporting it missing. Tell me which name you want and I'll align the key.aquatonemarked archived upstream (suggestsgowitness);bbot/x8note the Rust toolchain requirement;mobsfpinned topython3.11.scoutsuitekey →scout, its real binary name — it was reporting a falseMISSINGwhile installed. Note:commands/arsenal.md:26still saysscoutsuite, so--install-hint scoutsuitenow answers "Unknown tool". Can fix in this PR if you prefer.clairvoyancedeliberately left unchanged and marked verified — it is published by its upstream author.2. A GraphQL phase that could never run, reported as "skipped"
bughunter/tools/graphql_audit.shgated phase 5 on_have gqlmap, a binary that cannot exist (see above). The gate was never true, theelsebranch always ran, andsummary.txtalways reportedinjection_scan: skipped (gqlmap not installed)— while the built-in SQLi probe inside that branch did the actual work. A reader of the summary would conclude injection was not tested.Removed the conditional and promoted the built-in probe to be the phase. Deliberately not repointed at GraphQLmap: its entrypoint is
while True: input("GraphQLmap > "), a REPL with no batch mode, so driving it here would block on stdin or die on EOF.3. Catch-all detection never took effect → fabricated findings
Repro, no target needed:
The skip could never fire — even on hosts where "Catchall detected" was correctly logged. Replaced both pipes with process substitution (
done < <(head -N …), already used at line 398 of the same file).Separately, the SAML endpoint check accepted
200|301|302|403as "endpoint found" with no baseline at all. Added a per-host control probe against a random path; if it returns one of those codes the check is skipped with a logged reason. Since 403 was already accepted, a host returning 403 for unknown paths previously reported the entire SAML path list.4. Four curl calls dropped credentials on authenticated runs
The catch-all baseline, the upload probe loop, SAML endpoint discovery and SAML metadata retrieval omitted
$BB_AUTH_ARGSwhile neighbouring calls include it. The upload one mattered most: behind a login every probe got a 401, found nothing, and the run reported no findings — which reads as "no upload surface" rather than "not tested".Left deliberately unauthenticated, now with comments saying so, because adding the session destroys them: the MFA workflow-skip test (asks whether a page is reachable without auth) and the SAML signature-stripping probe (forges a login; a valid session returns 200/302 regardless, producing a false CRITICAL).
recon_engine.sh's three uncredentialed calls are also correct as-is — they query crt.sh and web.archive.org, which must never receive a target session cookie. Not touched.5. Three defects in my own first pass (fixed in the last commit)
I reviewed this branch before submitting and found three problems with it. Flagging them because two change how an earlier commit reads.
(a) The catch-all fix activated a latent bug. Making
CATCHALL_HOSTSsurvive the loop promoted[[ "$CATCHALL_HOSTS" == *"$host"* ]]from dead code to live logic — and that match is substring, not host equality, so a recordedhttps://api.t.com:8443would suppress a probedhttps://api.t.com. Entries are now stored,host,and matched with a comma-anchoredcase.(b) The SAML metadata and ACS legs read the wrong field, and my commit message overclaimed because of it. Rows are
[INFORMATIONAL] [SAML-ENDPOINT] <url> | HTTP <code>, so the URL is field 3 — but both consumers read field 2, the literal tag[SAML-ENDPOINT], which curl rejects at URL-parse time. Those legs have never reached the network. Pre-existing, but it means the$BB_AUTH_ARGSI added to the metadata call was inert and commit84cc1e4's subject ("auth propagation in … SAML checks") was wrong as written. Fixed to field 3.(c) Install hints chained commands without entering the clone.
git clone X && pip install -r requirements.txtleaves the shell in the parent directory, so in a recon directory holding cloned target source, pasting that hint installs the target's requirements and runs its build hooks. Six hints fixed tocdinto the clone, matching the formskills/graphql-audit/SKILL.mdalready used.6. Installer reported success for tools it skipped
The
|| command -v "theHarvester"applied on every iteration. The alias is needed — theHarvester's binary is capitalised and never matches its formula name — but as written, anyone who already had theHarvester got[+] hashcat already installedand no install. Reproduced on macOS 15 / arm64:--with-credential-attackfinished reporting 15/16 withhashcat: NOT FOUNDin its own verification block, contradicting the earlier success line. Scoped the alias check totheharvester.Relationship to #157
§3 and §4 sit upstream of the report gate in #157. That PR's description states it is report-time only and does not affect scanner detection or probing — so a phantom upload path is still probed, and a phantom SAML endpoint still written to
saml/endpoints.txt, regardless of whether the resulting report is later suppressed. Complementary, not overlapping. Also worth noting both §3 and §4 emit at[INFORMATIONAL], and if that tier isn't gated, these false positives bypass the verification spine entirely.Not included
54 invocations across
commands/andskills/still referencetools/*paths that no longer exist since thebughunter/move, and.gitignore:53'smemory/leads/is anchored to the repo root so it no longer matchesbughunter/memory/leads/(running the test suite leaves untracked files). Both are restructure fallout rather than scanner bugs — happy to open a separate issue or PR.Environment
macOS 15.x arm64, bash 3.2.57, branched from
40b03ee. PyPI status via the PyPI JSON API, repo status via the GitHub API.