Skip to content

Commit 6b03f79

Browse files
committed
Add support for multiple wordlist files via -w flag
Implemented: - Changed Wordlist (string) to Wordlists ([]string) to support multiple -w flags - Created loadWordlist() helper function to load individual wordlist files * Detects format (rainbow table vs plain text) * Auto-generates 8.3 filenames for plain text using Gen8dot3() * Returns records and format flag - Created mergeWordlists() helper function for smart deduplication * Uses filename+extension as key for uniqueness * Prioritizes rainbow table entries over plain text * Tracks duplicate count for logging - Updated main wordlist loading logic to: * Loop through multiple wordlist files * Merge with deduplication * Enable rainbow mode if ANY wordlist is rainbow format * Log informative messages (entries, files, duplicates) - Updated README with usage examples and documentation Features: ✅ Multiple wordlist files supported (-w file1 -w file2 -w file3) ✅ Smart deduplication (rainbow entries override plain text) ✅ Mix plain text and rainbow table formats ✅ Auto-generate 8.3 names for plain text wordlists ✅ Backward compatible (single -w still works) ✅ Informative logging at INFO level Testing: ✅ Multiple wordlists: Works, deduplication confirmed ✅ Single wordlist: Works, backward compatible ✅ No wordlist: Default embedded wordlist works ✅ Build successful, no compilation errors
1 parent 3960f24 commit 6b03f79

File tree

2 files changed

+205
-55
lines changed

2 files changed

+205
-55
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Additional features implemented in this fork:
2525

2626
| Feature | Description |
2727
|---------|-------------|
28+
| Multiple Wordlists (`-w`) | Support for specifying multiple wordlist files using multiple `-w` flags. Wordlists are merged with automatic deduplication. Supports mixing plain text and rainbow table formats. Rainbow table entries take priority when duplicates are found. |
2829
| Relaxed Match Mode (`-R`) | Enables detection of tentative matches where the final status matches the negative marker. Useful for Jakarta/CFM files that can be loaded via their 8.3 short filenames. Tentative matches are marked with yellow color in human output and `"tentative": true` in JSON output. |
2930
| Case-insensitive `INDEX_ALLOCATION` | The `replaceBinALLOCATION()` function now uses case-insensitive matching for `bin::$INDEX_ALLOCATION` paths. |
3031
| WAF Evasion | Changed session identifier from `(S(x))` to `(S(d))` to avoid detection by sensitive WAFs. |
@@ -120,6 +121,18 @@ shortscan -R http://example.org/ # Enable relaxed match mode
120121
shortscan -R -v 1 http://example.org/ # With debug logging to see status details
121122
```
122123

124+
To use multiple wordlists (supports both plain text and rainbow tables):
125+
```
126+
# Use multiple wordlists - they will be merged with deduplication
127+
shortscan -w custom.txt -w pkg/shortscan/resources/dll.txt http://example.org/
128+
129+
# Mix plain text and rainbow tables (rainbow entries take priority)
130+
shortscan -w pkg/shortscan/resources/wordlist.txt -w custom_dll.txt http://example.org/
131+
132+
# Combine multiple specialized wordlists
133+
shortscan -w aspx_files.txt -w dll_files.txt -w config_files.txt http://example.org/
134+
```
135+
123136
### Advanced features
124137

125138
The following options allow further tweaks:

pkg/shortscan/shortscan.go

Lines changed: 192 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ var requestDelay time.Duration
143143
// Command-line arguments and help
144144
type arguments struct {
145145
Urls []string `arg:"positional,required" help:"url to scan (multiple URLs can be provided; a file containing URLs can be specified with an «at» prefix, for example: @urls.txt)" placeholder:"URL"`
146-
Wordlist string `arg:"-w" help:"combined wordlist + rainbow table generated with shortutil" placeholder:"FILE"`
146+
Wordlists []string `arg:"--wordlist,-w,separate" help:"combined wordlist + rainbow table generated with shortutil (can be specified multiple times)" placeholder:"FILE"`
147147
Headers []string `arg:"--header,-H,separate" help:"header to send with each request (use multiple times for multiple headers)"`
148148
Concurrency int `arg:"-c" help:"number of requests to make at once" default:"20"`
149149
Timeout int `arg:"-t" help:"per-request timeout in seconds" placeholder:"SECONDS" default:"10"`
@@ -1121,6 +1121,122 @@ func Scan(urls []string, hc *http.Client, st *httpStats, wc wordlistConfig, mk m
11211121

11221122
}
11231123

1124+
// loadWordlist loads a single wordlist file and returns its records and format type
1125+
func loadWordlist(filePath string) ([]wordlistRecord, bool, error) {
1126+
var records []wordlistRecord
1127+
var isRainbow bool
1128+
1129+
// Open the file
1130+
fh, err := os.Open(filePath)
1131+
if err != nil {
1132+
return nil, false, err
1133+
}
1134+
defer fh.Close()
1135+
1136+
// Scan the file
1137+
scanner := bufio.NewScanner(fh)
1138+
lineNum := 0
1139+
for scanner.Scan() {
1140+
line := scanner.Text()
1141+
1142+
// Check first line for rainbow table magic value
1143+
if lineNum == 0 && line == rainbowMagic {
1144+
isRainbow = true
1145+
lineNum++
1146+
continue
1147+
}
1148+
1149+
// Skip blank lines and comments
1150+
if len(line) == 0 || line[0] == '#' {
1151+
lineNum++
1152+
continue
1153+
}
1154+
1155+
// Parse based on format
1156+
if isRainbow {
1157+
// Rainbow table format: checksum\tfile83\text83\tfilename\textension
1158+
if strings.Count(line, "\t") != 4 {
1159+
return nil, false, fmt.Errorf("invalid rainbow table entry at line %d: incorrect tab count", lineNum)
1160+
}
1161+
1162+
parts := strings.Split(line, "\t")
1163+
checksum, f83, e83, filename, extension := parts[0], parts[1], parts[2], parts[3], parts[4]
1164+
if len(extension) > 0 {
1165+
extension = "." + extension
1166+
}
1167+
records = append(records, wordlistRecord{checksum, filename, extension, f83, e83})
1168+
1169+
} else {
1170+
// Plain text format: filename.extension
1171+
var r wordlistRecord
1172+
if p := strings.LastIndex(line, "."); p > 0 && line[0] != '.' {
1173+
// Has extension
1174+
filename, ext := line[:p], line[p:]
1175+
_, f83, e83 := shortutil.Gen8dot3(filename, ext)
1176+
r = wordlistRecord{"", filename, ext, f83, e83}
1177+
} else {
1178+
// No extension (or starts with dot)
1179+
_, f83, _ := shortutil.Gen8dot3(line, "")
1180+
r = wordlistRecord{"", line, "", f83, ""}
1181+
}
1182+
records = append(records, r)
1183+
}
1184+
1185+
lineNum++
1186+
}
1187+
1188+
if err := scanner.Err(); err != nil {
1189+
return nil, false, err
1190+
}
1191+
1192+
return records, isRainbow, nil
1193+
}
1194+
1195+
// mergeWordlists merges multiple wordlists with deduplication, prioritizing rainbow table entries
1196+
func mergeWordlists(wordlists [][]wordlistRecord, rainbowFlags []bool) ([]wordlistRecord, int, bool) {
1197+
// Use map for deduplication with key = filename + extension
1198+
recordMap := make(map[string]wordlistRecord)
1199+
duplicateCount := 0
1200+
hasRainbow := false
1201+
1202+
// Check if any wordlist is a rainbow table
1203+
for _, isRainbow := range rainbowFlags {
1204+
if isRainbow {
1205+
hasRainbow = true
1206+
break
1207+
}
1208+
}
1209+
1210+
// Merge all wordlists
1211+
for i, records := range wordlists {
1212+
isRainbow := rainbowFlags[i]
1213+
1214+
for _, record := range records {
1215+
key := record.filename + record.extension
1216+
1217+
if existing, exists := recordMap[key]; exists {
1218+
duplicateCount++
1219+
// Priority: rainbow table entries override plain text entries
1220+
if isRainbow && existing.checksums == "" {
1221+
// Current is rainbow, existing is plain text - override
1222+
recordMap[key] = record
1223+
}
1224+
// Otherwise keep existing (either both are rainbow, both are plain, or existing is rainbow)
1225+
} else {
1226+
recordMap[key] = record
1227+
}
1228+
}
1229+
}
1230+
1231+
// Convert map to slice
1232+
merged := make([]wordlistRecord, 0, len(recordMap))
1233+
for _, record := range recordMap {
1234+
merged = append(merged, record)
1235+
}
1236+
1237+
return merged, duplicateCount, hasRainbow
1238+
}
1239+
11241240
// Run kicks off scans from the command line
11251241
func Run() {
11261242

@@ -1222,76 +1338,97 @@ func Run() {
12221338
// Compile the checksum detection regex
12231339
checksumRegex = regexp.MustCompile(".{1,2}[0-9A-F]{4}")
12241340

1225-
// Select the wordlist
1226-
var s *bufio.Scanner
1227-
if args.Wordlist != "" {
1228-
log.WithFields(log.Fields{"file": args.Wordlist}).Info("Using custom wordlist")
1229-
fh, err := os.Open(args.Wordlist)
1230-
if err != nil {
1231-
log.WithFields(log.Fields{"err": err}).Fatal("Unable to open wordlist")
1232-
}
1233-
s = bufio.NewScanner(fh)
1234-
} else {
1235-
log.Info("Using built-in wordlist")
1236-
fh, _ := defaultWordlist.Open("resources/wordlist.txt")
1237-
s = bufio.NewScanner(fh)
1238-
}
1341+
// Load wordlist(s)
1342+
if len(args.Wordlists) > 0 {
1343+
// Load multiple custom wordlists
1344+
var allWordlists [][]wordlistRecord
1345+
var rainbowFlags []bool
12391346

1240-
// Read the wordlist into memory
1241-
n := 0
1242-
for s.Scan() {
1347+
for _, filePath := range args.Wordlists {
1348+
log.WithFields(log.Fields{"file": filePath}).Info("Loading wordlist")
1349+
records, isRainbow, err := loadWordlist(filePath)
1350+
if err != nil {
1351+
log.WithFields(log.Fields{"file": filePath, "err": err}).Fatal("Unable to load wordlist")
1352+
}
1353+
allWordlists = append(allWordlists, records)
1354+
rainbowFlags = append(rainbowFlags, isRainbow)
1355+
}
12431356

1244-
// Read the line
1245-
line := s.Text()
1357+
// Merge wordlists with deduplication
1358+
merged, duplicates, hasRainbow := mergeWordlists(allWordlists, rainbowFlags)
1359+
wc.wordlist = merged
1360+
wc.isRainbow = hasRainbow
12461361

1247-
// Check the first line for the rainbow table magic value
1248-
if n == 0 && line == rainbowMagic {
1249-
wc.isRainbow = true
1250-
log.Info("Rainbow table provided, enabling auto dechecksumming")
1251-
continue
1362+
// Log results
1363+
if len(args.Wordlists) == 1 {
1364+
log.WithFields(log.Fields{"entries": len(merged)}).Info("Loaded wordlist")
1365+
} else {
1366+
if duplicates > 0 {
1367+
log.WithFields(log.Fields{"entries": len(merged), "files": len(args.Wordlists), "duplicates": duplicates}).Info("Loaded unique entries from wordlists")
1368+
} else {
1369+
log.WithFields(log.Fields{"entries": len(merged), "files": len(args.Wordlists)}).Info("Loaded entries from wordlists")
1370+
}
12521371
}
12531372

1254-
// Skip blank lines and comments
1255-
if l := len(line); l == 0 || line[0] == '#' {
1256-
continue
1373+
if hasRainbow {
1374+
log.Info("Rainbow table enabled, auto dechecksumming available")
12571375
}
12581376

1259-
// Add the line to the wordlist
1260-
if wc.isRainbow {
1261-
1262-
// Check tab count
1263-
if strings.Count(line, "\t") != 4 {
1264-
log.WithFields(log.Fields{"line": line}).Fatal("Wordlist entry invalid (incorrect tab count)")
1265-
log.Fatal("")
1377+
} else {
1378+
// Use embedded default wordlist
1379+
log.Info("Using built-in wordlist")
1380+
fh, _ := defaultWordlist.Open("resources/wordlist.txt")
1381+
scanner := bufio.NewScanner(fh)
1382+
1383+
lineNum := 0
1384+
for scanner.Scan() {
1385+
line := scanner.Text()
1386+
1387+
// Check first line for rainbow table magic value
1388+
if lineNum == 0 && line == rainbowMagic {
1389+
wc.isRainbow = true
1390+
log.Info("Rainbow table provided, enabling auto dechecksumming")
1391+
lineNum++
1392+
continue
12661393
}
12671394

1268-
// Split the line and add the word
1269-
c := strings.Split(line, "\t")
1270-
f, e, f83, e83 := c[3], c[4], c[1], c[2]
1271-
if len(e) > 0 {
1272-
e = "." + e
1395+
// Skip blank lines and comments
1396+
if len(line) == 0 || line[0] == '#' {
1397+
lineNum++
1398+
continue
12731399
}
1274-
wc.wordlist = append(wc.wordlist, wordlistRecord{c[0], f, e, f83, e83})
12751400

1276-
} else {
1401+
// Add the line to the wordlist
1402+
if wc.isRainbow {
1403+
// Check tab count
1404+
if strings.Count(line, "\t") != 4 {
1405+
log.WithFields(log.Fields{"line": line}).Fatal("Wordlist entry invalid (incorrect tab count)")
1406+
}
1407+
1408+
// Split the line and add the word
1409+
c := strings.Split(line, "\t")
1410+
f, e, f83, e83 := c[3], c[4], c[1], c[2]
1411+
if len(e) > 0 {
1412+
e = "." + e
1413+
}
1414+
wc.wordlist = append(wc.wordlist, wordlistRecord{c[0], f, e, f83, e83})
12771415

1278-
// Split the line into file and extension and generate an 8.3 version
1279-
var r wordlistRecord
1280-
if p := strings.LastIndex(line, "."); p > 0 && line[0] != '.' {
1281-
f, e := line[:p], line[p:]
1282-
_, f83, e83 := shortutil.Gen8dot3(f, e)
1283-
r = wordlistRecord{"", f, e, f83, e83}
12841416
} else {
1285-
_, f83, _ := shortutil.Gen8dot3(line, "")
1286-
r = wordlistRecord{"", line, "", f83, ""}
1417+
// Split the line into file and extension and generate an 8.3 version
1418+
var r wordlistRecord
1419+
if p := strings.LastIndex(line, "."); p > 0 && line[0] != '.' {
1420+
f, e := line[:p], line[p:]
1421+
_, f83, e83 := shortutil.Gen8dot3(f, e)
1422+
r = wordlistRecord{"", f, e, f83, e83}
1423+
} else {
1424+
_, f83, _ := shortutil.Gen8dot3(line, "")
1425+
r = wordlistRecord{"", line, "", f83, ""}
1426+
}
1427+
wc.wordlist = append(wc.wordlist, r)
12871428
}
1288-
wc.wordlist = append(wc.wordlist, r)
12891429

1430+
lineNum++
12901431
}
1291-
1292-
// Next
1293-
n += 1
1294-
12951432
}
12961433

12971434
// Let's go!

0 commit comments

Comments
 (0)