Skip to content

Commit e088ee6

Browse files
committed
Add react2shell file scanning support: batch domain processing with automatic live hosts collection
- Added file attachment support to Discord /react2shell command - Added CLI support: react2shell scan -f <domains_file> - For each domain: automatically collects live hosts, then runs smart scan - Concise output: shows only scanned hosts count and vulnerable hosts (if any) - Updated README with new features
1 parent af01c6b commit e088ee6

4 files changed

Lines changed: 549 additions & 24 deletions

File tree

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ AutoAR is a comprehensive, modular security automation toolkit designed for bug
1414

1515
### 🛡️ **Vulnerability Scanning**
1616
- **Nuclei Integration**: 1000+ vulnerability templates with custom rate limiting
17-
- **React2Shell Scanner**: React Server Components RCE detection (CVE-2025-55182) with WAF bypass methods, source code exposure checks, and DoS testing
17+
- **React2Shell Scanner**: React Server Components RCE detection (CVE-2025-55182) with WAF bypass methods, source code exposure checks, DoS testing, and batch domain processing with automatic live hosts collection
1818
- **WordPress Plugin Confusion**: Automated WP plugin/theme confusion attack detection
1919
- **Dependency Confusion**: GitHub repository dependency confusion scanning
2020
- **S3 Bucket Enumeration**: AWS S3 bucket discovery and analysis (pure Go via AWS SDK v2, no aws CLI required). Supports both authenticated and unauthenticated testing - automatically falls back to HTTP-based public access testing when credentials are missing
@@ -279,6 +279,8 @@ autoar github-wordlist scan -o orgname
279279
autoar apkx scan -i /path/to/app.apk # Analyze APK/IPA with embedded apkX engine
280280
autoar apkx mitm -i /path/to/app.apk # Patch APK for MITM inspection (standalone command)
281281
autoar apkx mitm -p com.example.app # Download and patch APK by package name
282+
autoar react2shell scan -d example.com [-t 100] [--dos-test] [--enable-source-exposure] # Scan single domain (collects live hosts, then smart scan)
283+
autoar react2shell scan -f domains.txt [-t 100] [--dos-test] [--enable-source-exposure] # Scan multiple domains from file
282284
autoar bot # Start Discord bot
283285
autoar api # Start REST API server
284286
autoar both # Start both bot and API
@@ -345,7 +347,8 @@ Once the bot is running, use these slash commands in Discord:
345347

346348
#### Vulnerability Scanning
347349
- `/nuclei domain:example.com [threads:100]` - Run Nuclei scans
348-
- `/react2shell_scan domain:example.com [threads:100] [enable_source_exposure:false] [dos_test:false]` - Scan domain hosts for React Server Components RCE (CVE-2025-55182) using next88 smart scan (sequential: normal → WAF bypass → Vercel WAF → paths)
350+
- `/react2shell domain:example.com [threads:100] [enable_source_exposure:false] [dos_test:false]` - Scan domain hosts for React Server Components RCE (CVE-2025-55182) using next88 smart scan (sequential: normal → WAF bypass → Vercel WAF → paths). Automatically collects live hosts first, then runs smart scan.
351+
- `/react2shell file:<domains.txt> [threads:100] [enable_source_exposure:false] [dos_test:false]` - Process multiple domains from file. For each domain: collects live hosts, then runs smart scan. Perfect for batch scanning.
349352
- `/react2shell url:https://example.com [verbose:false]` - Test single URL for React Server Components RCE using next88 smart scan
350353
- `/jwt_scan token:<JWT_TOKEN> [skip_crack:false] [skip_payloads:false] [wordlist:] [max_crack_attempts:]` - JWT token vulnerability scanning using jwt-hack
351354
- `/wpdepconf domain:example.com` - WordPress plugin confusion

cmd/autoar/main.go

Lines changed: 313 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import (
55
"context"
66
"fmt"
77
"os"
8+
"os/exec"
89
"path/filepath"
910
"strconv"
1011
"strings"
12+
"time"
1113

1214
"github.com/h0tak88r/AutoAR/internal/modules/backup"
1315
apkxmod "github.com/h0tak88r/AutoAR/internal/modules/apkx"
@@ -40,6 +42,7 @@ import (
4042
"github.com/h0tak88r/AutoAR/internal/modules/envloader"
4143
"github.com/h0tak88r/AutoAR/internal/tools/apkx/downloader"
4244
"github.com/h0tak88r/AutoAR/internal/tools/apkx/mitm"
45+
next88 "github.com/h0tak88r/AutoAR/internal/tools/next88"
4346
)
4447

4548
var (
@@ -145,6 +148,10 @@ Commands:
145148
keyhack validate <provider> <api_key> Generate validation command for API key
146149
keyhack add <keyname> <command> <desc> [notes] Add a new template
147150
jwt scan --token <JWT_TOKEN> [OPTIONS] Scan JWT token for vulnerabilities using jwt-hack
151+
Options: --skip-crack, --skip-payloads, --test-attacks, -w wordlist, --max-crack-attempts N
152+
react2shell scan -d <domain> [-t <threads>] [--dos-test] [--enable-source-exposure]
153+
react2shell scan -f <domains_file> [-t <threads>] [--dos-test] [--enable-source-exposure]
154+
For each domain: collects live hosts, then runs smart scan
148155
149156
Workflows:
150157
lite run -d <domain>
@@ -828,15 +835,15 @@ func handleApkXMitm(args []string) error {
828835

829836
// handleJWTCommand parses:
830837
//
831-
// autoar jwt scan --token <JWT_TOKEN> [--skip-crack] [--skip-payloads] [-w wordlist] [--max-crack-attempts N]
832-
// autoar jwt scan <JWT_TOKEN> [--skip-crack] [--skip-payloads] [-w wordlist] [--max-crack-attempts N]
838+
// autoar jwt scan --token <JWT_TOKEN> [--skip-crack] [--skip-payloads] [--test-attacks] [-w wordlist] [--max-crack-attempts N]
839+
// autoar jwt scan <JWT_TOKEN> [--skip-crack] [--skip-payloads] [--test-attacks] [-w wordlist] [--max-crack-attempts N]
833840
//
834841
// Internally this is normalized to:
835842
//
836843
// jwt-hack scan <JWT_TOKEN> [flags...]
837844
func handleJWTCommand(args []string) error {
838845
if len(args) == 0 || args[0] != "scan" {
839-
return fmt.Errorf("usage: jwt scan --token <JWT_TOKEN> [--skip-crack] [--skip-payloads] [-w wordlist] [--max-crack-attempts N]")
846+
return fmt.Errorf("usage: jwt scan --token <JWT_TOKEN> [--skip-crack] [--skip-payloads] [--test-attacks] [-w wordlist] [--max-crack-attempts N]")
840847
}
841848
raw := args[1:]
842849
if len(raw) == 0 {
@@ -993,6 +1000,306 @@ func generateKeyhackCommand(t db.KeyhackTemplate, apiKey string) string {
9931000
return strings.Join(curlParts, " ")
9941001
}
9951002

1003+
// handleReact2ShellCommand handles CLI react2shell scan
1004+
// Usage: react2shell scan -d <domain> | -f <domains_file> [-t <threads>] [--dos-test] [--enable-source-exposure]
1005+
func handleReact2ShellCommand(args []string) error {
1006+
if len(args) == 0 || args[0] != "scan" {
1007+
return fmt.Errorf("usage: react2shell scan -d <domain> | -f <domains_file> [-t <threads>] [--dos-test] [--enable-source-exposure]")
1008+
}
1009+
args = args[1:]
1010+
1011+
var domain, domainsFile string
1012+
threads := 100
1013+
dosTest := false
1014+
enableSourceExposure := false
1015+
1016+
for i := 0; i < len(args); i++ {
1017+
switch args[i] {
1018+
case "-d", "--domain":
1019+
if i+1 < len(args) {
1020+
domain = args[i+1]
1021+
i++
1022+
}
1023+
case "-f", "--file":
1024+
if i+1 < len(args) {
1025+
domainsFile = args[i+1]
1026+
i++
1027+
}
1028+
case "-t", "--threads":
1029+
if i+1 < len(args) {
1030+
if t, err := strconv.Atoi(args[i+1]); err == nil {
1031+
threads = t
1032+
}
1033+
i++
1034+
}
1035+
case "--dos-test":
1036+
dosTest = true
1037+
case "--enable-source-exposure":
1038+
enableSourceExposure = true
1039+
}
1040+
}
1041+
1042+
if domain == "" && domainsFile == "" {
1043+
return fmt.Errorf("either -d <domain> or -f <domains_file> must be provided")
1044+
}
1045+
if domain != "" && domainsFile != "" {
1046+
return fmt.Errorf("cannot use both -d and -f together")
1047+
}
1048+
1049+
var domains []string
1050+
if domain != "" {
1051+
domains = []string{domain}
1052+
} else {
1053+
// Read domains from file
1054+
file, err := os.Open(domainsFile)
1055+
if err != nil {
1056+
return fmt.Errorf("failed to open domains file: %w", err)
1057+
}
1058+
defer file.Close()
1059+
1060+
scanner := bufio.NewScanner(file)
1061+
for scanner.Scan() {
1062+
line := strings.TrimSpace(scanner.Text())
1063+
if line != "" && !strings.HasPrefix(line, "#") {
1064+
domains = append(domains, line)
1065+
}
1066+
}
1067+
if err := scanner.Err(); err != nil {
1068+
return fmt.Errorf("error reading domains file: %w", err)
1069+
}
1070+
if len(domains) == 0 {
1071+
return fmt.Errorf("no valid domains found in file")
1072+
}
1073+
}
1074+
1075+
// Process each domain
1076+
for idx, targetDomain := range domains {
1077+
// Step 1: Get live hosts
1078+
liveHostsFile, err := getLiveHostsForCLI(targetDomain, threads)
1079+
if err != nil {
1080+
fmt.Printf("[%d/%d] %s: ERROR - %v\n", idx+1, len(domains), targetDomain, err)
1081+
continue
1082+
}
1083+
1084+
// Step 2: Normalize hosts
1085+
hosts, err := normalizeHostsForCLI(liveHostsFile)
1086+
if err != nil {
1087+
fmt.Printf("[%d/%d] %s: ERROR - %v\n", idx+1, len(domains), targetDomain, err)
1088+
continue
1089+
}
1090+
1091+
if len(hosts) == 0 {
1092+
fmt.Printf("[%d/%d] %s: 0 hosts scanned\n", idx+1, len(domains), targetDomain)
1093+
continue
1094+
}
1095+
1096+
// Step 3: Run smart scan
1097+
smartScanResults, err := runNext88ScanForCLI(hosts, []string{"-smart-scan"})
1098+
if err != nil {
1099+
fmt.Printf("[%d/%d] %s: ERROR - %v\n", idx+1, len(domains), targetDomain, err)
1100+
continue
1101+
}
1102+
1103+
// Collect all vulnerable hosts
1104+
allVulnerable := make(map[string]bool)
1105+
for _, h := range smartScanResults {
1106+
allVulnerable[h] = true
1107+
}
1108+
1109+
// Step 4: DoS test (if enabled)
1110+
if dosTest {
1111+
dosResults, err := runNext88ScanForCLI(hosts, []string{"-dos-test", "-dos-requests", "100"})
1112+
if err == nil {
1113+
for _, h := range dosResults {
1114+
allVulnerable[h] = true
1115+
}
1116+
}
1117+
}
1118+
1119+
// Step 5: Source exposure check (if enabled)
1120+
if enableSourceExposure {
1121+
sourceResults, err := runNext88ScanForCLI(hosts, []string{"-check-source-exposure"})
1122+
if err == nil {
1123+
for _, h := range sourceResults {
1124+
allVulnerable[h] = true
1125+
}
1126+
}
1127+
}
1128+
1129+
// Print concise output
1130+
vulnerableList := make([]string, 0, len(allVulnerable))
1131+
for h := range allVulnerable {
1132+
vulnerableList = append(vulnerableList, h)
1133+
}
1134+
1135+
if len(vulnerableList) > 0 {
1136+
fmt.Printf("[%d/%d] %s: %d hosts scanned, %d vulnerable:\n", idx+1, len(domains), targetDomain, len(hosts), len(vulnerableList))
1137+
for _, host := range vulnerableList {
1138+
fmt.Printf(" - %s\n", host)
1139+
}
1140+
} else {
1141+
fmt.Printf("[%d/%d] %s: %d hosts scanned\n", idx+1, len(domains), targetDomain, len(hosts))
1142+
}
1143+
}
1144+
1145+
return nil
1146+
}
1147+
1148+
// Helper functions for CLI
1149+
func getLiveHostsForCLI(domain string, threads int) (string, error) {
1150+
resultsDir := os.Getenv("AUTOAR_RESULTS_DIR")
1151+
if resultsDir == "" {
1152+
resultsDir = "new-results"
1153+
}
1154+
1155+
subsDir := filepath.Join(resultsDir, domain, "subs")
1156+
1157+
// Ensure subdomains exist first
1158+
subCmd := exec.Command(os.Args[0], "subdomains", "get", "-d", domain, "-t", fmt.Sprintf("%d", threads), "-s")
1159+
if err := subCmd.Run(); err != nil {
1160+
return "", fmt.Errorf("subdomain enumeration failed: %w", err)
1161+
}
1162+
1163+
// Run livehosts
1164+
cmd := exec.Command(os.Args[0], "livehosts", "get", "-d", domain, "-t", fmt.Sprintf("%d", threads), "--silent")
1165+
if err := cmd.Run(); err != nil {
1166+
return "", fmt.Errorf("livehosts failed: %w", err)
1167+
}
1168+
1169+
// Check for live hosts file
1170+
liveHostsFile := filepath.Join(subsDir, "live-subs.txt")
1171+
if fileInfo, err := os.Stat(liveHostsFile); err == nil && fileInfo.Size() > 0 {
1172+
return liveHostsFile, nil
1173+
}
1174+
1175+
// Fallback to all-subs.txt
1176+
allSubsFile := filepath.Join(subsDir, "all-subs.txt")
1177+
if fileInfo, err := os.Stat(allSubsFile); err == nil && fileInfo.Size() > 0 {
1178+
return allSubsFile, nil
1179+
}
1180+
1181+
return "", fmt.Errorf("no live hosts file found")
1182+
}
1183+
1184+
func normalizeHostsForCLI(hostsFile string) ([]string, error) {
1185+
data, err := os.ReadFile(hostsFile)
1186+
if err != nil {
1187+
return nil, err
1188+
}
1189+
1190+
lines := strings.Split(string(data), "\n")
1191+
normalized := make([]string, 0, len(lines))
1192+
1193+
for _, line := range lines {
1194+
host := strings.TrimSpace(line)
1195+
if host == "" {
1196+
continue
1197+
}
1198+
host = strings.TrimSuffix(host, "/")
1199+
if !strings.HasPrefix(host, "http://") && !strings.HasPrefix(host, "https://") {
1200+
host = "https://" + host
1201+
}
1202+
normalized = append(normalized, host)
1203+
}
1204+
1205+
return normalized, nil
1206+
}
1207+
1208+
func runNext88ScanForCLI(hosts []string, extraFlags []string) ([]string, error) {
1209+
if len(hosts) == 0 {
1210+
return []string{}, nil
1211+
}
1212+
1213+
opts := next88.ScanOptions{
1214+
Timeout: 10 * time.Second,
1215+
VerifySSL: false,
1216+
FollowRedirects: true,
1217+
SafeCheck: false,
1218+
Windows: false,
1219+
WAFBypass: false,
1220+
WAFBypassSizeKB: 128,
1221+
VercelWAFBypass: false,
1222+
Paths: nil,
1223+
DoubleEncode: false,
1224+
SemicolonBypass: false,
1225+
CheckSourceExp: false,
1226+
CustomHeaders: make(map[string]string),
1227+
Threads: 10,
1228+
Quiet: true,
1229+
Verbose: false,
1230+
NoColor: true,
1231+
AllResults: true,
1232+
DiscordWebhook: "",
1233+
DOSTest: false,
1234+
DOSRequests: 100,
1235+
SmartScan: false,
1236+
}
1237+
1238+
if len(hosts) < opts.Threads {
1239+
opts.Threads = len(hosts)
1240+
if opts.Threads == 0 {
1241+
opts.Threads = 1
1242+
}
1243+
}
1244+
1245+
for i := 0; i < len(extraFlags); i++ {
1246+
flag := extraFlags[i]
1247+
switch flag {
1248+
case "-smart-scan":
1249+
opts.SmartScan = true
1250+
case "-dos-test":
1251+
opts.DOSTest = true
1252+
case "-dos-requests":
1253+
if i+1 < len(extraFlags) {
1254+
if v, err := strconv.Atoi(extraFlags[i+1]); err == nil && v > 0 {
1255+
opts.DOSRequests = v
1256+
}
1257+
i++
1258+
}
1259+
case "-check-source-exposure":
1260+
opts.CheckSourceExp = true
1261+
}
1262+
}
1263+
1264+
results, err := next88.Run(hosts, opts)
1265+
if err != nil {
1266+
return nil, err
1267+
}
1268+
1269+
vulnerableHosts := make(map[string]bool)
1270+
for _, res := range results {
1271+
if res.Vulnerable != nil && *res.Vulnerable {
1272+
host := res.Host
1273+
if host == "" {
1274+
host = res.TestedURL
1275+
}
1276+
if host == "" {
1277+
continue
1278+
}
1279+
hostname := extractHostnameForCLI(host)
1280+
if hostname != "" {
1281+
vulnerableHosts[hostname] = true
1282+
}
1283+
}
1284+
}
1285+
1286+
out := make([]string, 0, len(vulnerableHosts))
1287+
for h := range vulnerableHosts {
1288+
out = append(out, h)
1289+
}
1290+
1291+
return out, nil
1292+
}
1293+
1294+
func extractHostnameForCLI(url string) string {
1295+
url = strings.TrimPrefix(url, "https://")
1296+
url = strings.TrimPrefix(url, "http://")
1297+
parts := strings.Split(url, "/")
1298+
host := parts[0]
1299+
parts = strings.Split(host, ":")
1300+
return parts[0]
1301+
}
1302+
9961303
// handleMisconfigCommand routes misconfig subcommands
9971304
//
9981305
// autoar misconfig scan <target> [service] [delay]
@@ -2101,6 +2408,9 @@ func main() {
21012408
case "misconfig":
21022409
err = handleMisconfigCommand(args)
21032410

2411+
case "react2shell":
2412+
err = handleReact2ShellCommand(args)
2413+
21042414
case "monitor":
21052415
err = handleMonitorCommand(args)
21062416

0 commit comments

Comments
 (0)