Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 100 additions & 38 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ plugins {
}

group = 'gg.effront'
version = '2.1.0'
version = '2.2.0'

java {
toolchain {
Expand Down
18 changes: 15 additions & 3 deletions src/main/java/gg/effront/anarcry/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public static void main(String[] args) throws Exception {
boolean headless = false;
boolean tui = false;
String target = null;
String format = "anarcry";
int timeoutMinutes = HEADLESS_TIMEOUT_MINUTES;

int idx = 0;
Expand All @@ -50,6 +51,9 @@ public static void main(String[] args) throws Exception {
} else if ("--timeout".equals(arg) && idx + 1 < args.length) {
timeoutMinutes = Integer.parseInt(args[idx + 1]);
idx += 2;
} else if ("--format".equals(arg) && idx + 1 < args.length) {
format = args[idx + 1];
idx += 2;
} else {
idx++;
}
Expand All @@ -66,15 +70,16 @@ public static void main(String[] args) throws Exception {
}

if (headless) {
runHeadless(env, target, timeoutMinutes);
runHeadless(env, target, timeoutMinutes, format);
} else if (tui) {
new AnarCryTui().start(env);
} else {
new AnarCryGui().start(env);
}
}

private static void runHeadless(EnvLoader env, String targetArg, int timeoutMinutes) throws Exception {
private static void runHeadless(EnvLoader env, String targetArg, int timeoutMinutes, String format)
throws Exception {
String target = targetArg;
String jwt1 = "";
String jwt2 = "";
Expand Down Expand Up @@ -123,7 +128,14 @@ private static void runHeadless(EnvLoader env, String targetArg, int timeoutMinu
.forEach(f -> System.out.printf(" %-8s [%s] %s%n", f.severity(), f.module(), f.title()));

if (!findings.isEmpty()) {
String report = new ReportEngine().export(new ArrayList<>(findings), target);
ReportEngine engine = new ReportEngine();
List<Finding> snapshot = new ArrayList<>(findings);
String report = switch (format.toLowerCase()) {
case "h1" -> engine.exportH1(snapshot, target, "AnarCry Scan");
case "bugcrowd" -> engine.exportBugcrowd(snapshot, target);
case "sarif" -> engine.exportSarif(snapshot, target);
default -> engine.export(snapshot, target);
};
System.out.println("\n[*] Report: " + report);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,81 @@
package gg.effront.anarcry.exploit.validator;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public final class BaselineDiffer {

private static final double SIMILARITY_THRESHOLD = 0.85;
private static final double DEFAULT_SIMILARITY_THRESHOLD = 0.85;

private static final Pattern DYNAMIC_CONTENT = Pattern
.compile("(?:\"(?:timestamp|nonce|csrf|requestId|traceId|date)\"\\s*:\\s*\"[^\"]*\")"
+ "|(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})" + "|(?:\\d{10,13})");

private static final Pattern HTML_TAG = Pattern.compile("<[^>]+>");

private BaselineDiffer() {
}

/**
* Original flat-threshold comparison -- uses default 0.85 threshold for all
* modules.
*/
public static boolean isSignificantlyDifferent(String baseline, String withPayload) {
return isSignificantlyDifferent(baseline, withPayload, DEFAULT_SIMILARITY_THRESHOLD);
}

/**
* Module-aware comparison -- uses a per-module Jaccard similarity threshold.
* Error-based injection modules (SQLi, CmdInjection) use a lower threshold
* because even small changes in the response body can indicate successful
* injection. XSS uses a higher threshold because reflection typically causes
* more visible changes.
*/
public static boolean isSignificantlyDifferent(String baseline, String withPayload, String module) {
double threshold = thresholdForModule(module);
return isSignificantlyDifferent(baseline, withPayload, threshold);
}

public static boolean isSignificantlyDifferent(int baselineStatus, int payloadStatus) {
if (baselineStatus == payloadStatus) {
return false;
}
int baseClass = baselineStatus / 100;
int payloadClass = payloadStatus / 100;
return baseClass != payloadClass;
}

/**
* Structural HTML comparison -- strips all text content and compares only the
* HTML tag skeleton. This catches XSS injection where new tags are introduced
* (e.g. script, img onerror) even when the Jaccard similarity on full text
* misses the difference.
*/
public static boolean isStructurallyDifferent(String baseline, String withPayload) {
if (baseline == null && withPayload == null) {
return false;
}
if (baseline == null || withPayload == null) {
return true;
}
String baseStructure = extractTagStructure(baseline);
String payloadStructure = extractTagStructure(withPayload);
return !baseStructure.equals(payloadStructure);
}

static double thresholdForModule(String module) {
return switch (module) {
case "SQLi", "CmdInjection" -> 0.70; // error-based injection changes little
case "XSS" -> 0.90; // XSS reflection usually changes more
case "SSRF", "LFI" -> 0.75; // file inclusion changes response significantly
default -> 0.85;
};
}

private static boolean isSignificantlyDifferent(String baseline, String withPayload, double threshold) {
if (baseline == null && withPayload == null) {
return false;
}
Expand All @@ -27,16 +88,7 @@ public static boolean isSignificantlyDifferent(String baseline, String withPaylo
return false;
}
double similarity = jaccardSimilarity(normBase, normPayload);
return similarity < SIMILARITY_THRESHOLD;
}

public static boolean isSignificantlyDifferent(int baselineStatus, int payloadStatus) {
if (baselineStatus == payloadStatus) {
return false;
}
int baseClass = baselineStatus / 100;
int payloadClass = payloadStatus / 100;
return baseClass != payloadClass;
return similarity < threshold;
}

private static String normalize(String s) {
Expand All @@ -47,8 +99,8 @@ private static double jaccardSimilarity(String a, String b) {
if (a.isEmpty() && b.isEmpty()) {
return 1.0;
}
var setA = new java.util.HashSet<String>();
var setB = new java.util.HashSet<String>();
var setA = new HashSet<String>();
var setB = new HashSet<String>();
for (String word : a.split("\\s+")) {
setA.add(word);
}
Expand All @@ -64,4 +116,17 @@ private static double jaccardSimilarity(String a, String b) {
}
return (double) intersection.size() / union.size();
}

/**
* Extracts all HTML tags from the input and joins them into a single structure
* string.
*/
private static String extractTagStructure(String html) {
Matcher m = HTML_TAG.matcher(html);
List<String> tags = new ArrayList<>();
while (m.find()) {
tags.add(m.group());
}
return String.join("", tags);
}
}
Loading
Loading