Skip to content

Commit 6dc62f0

Browse files
committed
Enhanced shortscan with custom features
- Fixed merge conflict between PR bitquark#23 and PR bitquark#16 - Added case-insensitive matching for bin::$INDEX_ALLOCATION - Changed session identifier from (S(x)) to (S(d)) for WAF evasion - Implemented -R flag for relaxed match mode (tentative matches) * Useful for Jakarta/CFM files loaded via 8.3 filenames * Tentative matches shown in yellow with [tentative] label * JSON output includes 'tentative' field - Updated README with custom enhancements documentation - Added usage examples for new features - Code formatting improvements (gofmt)
1 parent 019dfd5 commit 6dc62f0

File tree

4 files changed

+135
-25
lines changed

4 files changed

+135
-25
lines changed

README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,54 @@
22

33
An IIS short filename enumeration tool.
44

5+
## 🔧 Custom Fork Information
6+
7+
**Fork Status**: Enhanced local version with upstream PRs applied
8+
**Cloned Date**: October 21, 2025
9+
**Upstream Repository**: [bitquark/shortscan](https://github.com/bitquark/shortscan)
10+
**Fork Repository**: [irsdl/shortscan](https://github.com/irsdl/shortscan)
11+
12+
### Applied Upstream Pull Requests
13+
14+
This fork includes the following upstream PRs that have not yet been merged into the main repository:
15+
16+
| PR # | Status | Title | Author | Description |
17+
|------|--------|-------|--------|-------------|
18+
| [#16](https://github.com/bitquark/shortscan/pull/16) | ✅ Applied | Added support for detecting Source Code Disclosure while autocomplete | ke0ge | Adds `replaceBinALLOCATION()` function to handle `bin::$INDEX_ALLOCATION` paths for downloading .DLL files. Use `--autocomplete method` to avoid timeout issues with large DLL files. |
19+
| [#23](https://github.com/bitquark/shortscan/pull/23) | ✅ Applied | Implemented rate limit for shortscan cmd | soerlemans | Adds `-r` flag for rate limiting (supports fractional values like `-r 0.2` for 1 request every 5 seconds). Includes `delayRequest()` function to prevent overwhelming targets during bug bounty testing. |
20+
| [#24](https://github.com/bitquark/shortscan/pull/24) | ✅ Applied | Updated error handling on inaccessible servers | sp0ilerr | Improves error handling to continue execution when a server is inaccessible, allowing batch scans to proceed even when encountering unreachable servers. |
21+
22+
### Custom Enhancements
23+
24+
Additional features implemented in this fork:
25+
26+
| Feature | Description |
27+
|---------|-------------|
28+
| 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. |
29+
| Case-insensitive `INDEX_ALLOCATION` | The `replaceBinALLOCATION()` function now uses case-insensitive matching for `bin::$INDEX_ALLOCATION` paths. |
30+
| WAF Evasion | Changed session identifier from `(S(x))` to `(S(d))` to avoid detection by sensitive WAFs. |
31+
32+
### Integration Notes
33+
34+
- All three PRs have been successfully merged on the `merge-upstream-prs` branch
35+
- Merge conflict between PR #23 and PR #16 in `shortscan.go` has been resolved (both `delayRequest()` and `replaceBinALLOCATION()` functions coexist)
36+
- The `main` branch is kept in sync with `upstream/main` for easier future updates
37+
38+
### Future Upstream Synchronization
39+
40+
When updating from upstream in the future:
41+
42+
1. **If these PRs are merged upstream**: Simply fetch and merge from upstream/main - no conflicts expected
43+
2. **If these PRs remain unmerged**: Reapply them after syncing with upstream/main
44+
3. **If upstream changes conflict with these PRs**: Manual conflict resolution will be needed
45+
46+
To check upstream PR status before syncing:
47+
```bash
48+
# Check if PRs are still open
49+
git fetch upstream
50+
# Visit https://github.com/bitquark/shortscan/pulls to verify PR status
51+
```
52+
553
## Functionality
654

755
Shortscan is designed to quickly determine which files with short filenames exist on an IIS webserver. Once a short filename has been identified the tool will try to automatically identify the full filename.
@@ -55,6 +103,23 @@ To check whether a site is vulnerable without performing file enumeration use:
55103
shortscan --isvuln
56104
```
57105

106+
To limit the request rate (useful for bug bounty testing to avoid overwhelming targets):
107+
```
108+
shortscan -r 2.5 http://example.org/ # 2.5 requests per second
109+
shortscan -r 0.2 http://example.org/ # 1 request every 5 seconds
110+
```
111+
112+
To use method-based autocomplete for downloading DLL files without timeout issues:
113+
```
114+
shortscan --autocomplete method --fullurl http://example.org/
115+
```
116+
117+
To enable relaxed matching for Jakarta/CFM files (reports tentative matches):
118+
```
119+
shortscan -R http://example.org/ # Enable relaxed match mode
120+
shortscan -R -v 1 http://example.org/ # With debug logging to see status details
121+
```
122+
58123
### Advanced features
59124

60125
The following options allow further tweaks:

pkg/levenshtein/levenshtein.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package levenshtein
22

33
import (
4-
"unicode/utf8"
54
"github.com/bitquark/shortscan/pkg/maths"
5+
"unicode/utf8"
66
)
77

88
// Distance returns the Levenshtein edit distance for two strings

pkg/shortscan/shortscan.go

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ type attackConfig struct {
8585
type resultOutput struct {
8686
Type string `json:"type"`
8787
FullMatch bool `json:"fullmatch"`
88+
Tentative bool `json:"tentative"`
8889
BaseUrl string `json:"baseurl"`
8990
File string `json:"shortfile"`
9091
Ext string `json:"shortext"`
@@ -156,6 +157,7 @@ type arguments struct {
156157
Characters string `arg:"-C" help:"filename characters to enumerate" default:"JFKGOTMYVHSPCANDXLRWEBQUIZ8549176320-_()&'!#$%@^{}~"`
157158
Autocomplete string `arg:"-a" help:"autocomplete detection mode (auto = autoselect; method = HTTP method magic; status = HTTP status; distance = Levenshtein distance; none = disable)" placeholder:"mode" default:"auto"`
158159
IsVuln bool `arg:"-V" help:"bail after determining whether the service is vulnerable" default:"false"`
160+
RelaxMatch bool `arg:"-R" help:"relax final negative match check and report tentative matches (useful for Jakarta/CFM files)" default:"false"`
159161
}
160162

161163
func (arguments) Version() string {
@@ -174,7 +176,6 @@ func pathEscape(url string) string {
174176
return strings.Replace(nurl.QueryEscape(url), "+", "%20", -1)
175177
}
176178

177-
<<<<<<< HEAD
178179
// Helper variable to the requestDelay function, keeps track of the time since the last request
179180
var lastRequestTime time.Time
180181

@@ -188,24 +189,24 @@ func delayRequest() {
188189

189190
// Update last request time
190191
lastRequestTime = time.Now()
191-
=======
192-
// replace bin::$INDEX_ALLOCATION to /(S(x))/b/(S(x))in/ to download .DLL
192+
}
193+
194+
// replace bin::$INDEX_ALLOCATION to /(S(d))/b/(S(d))in/ to download .DLL
193195
func replaceBinALLOCATION(url string) string {
194196
u, _ := nurl.Parse(url)
195197
segments := strings.Split(strings.Trim(u.Path, "/"), "/")
196198
lastSegment := segments[len(segments)-1]
197199

198-
if lastSegment == "bin::$INDEX_ALLOCATION" {
200+
if strings.EqualFold(lastSegment, "bin::$INDEX_ALLOCATION") {
199201
newPath := strings.Join(segments[:len(segments)-1], "/")
200202
if newPath == "" {
201-
newPath = "(S(x))/b/(S(x))in/"
203+
newPath = "(S(d))/b/(S(d))in/"
202204
} else {
203-
newPath += "/(S(x))/b/(S(x))in/"
205+
newPath += "/(S(d))/b/(S(d))in/"
204206
}
205207
url = u.Scheme + "://" + u.Host + "/" + newPath
206208
}
207209
return url
208-
>>>>>>> pr-16
209210
}
210211

211212
// fetch requests the given URL and returns an HTTP response object, handling retries gracefully
@@ -539,6 +540,7 @@ func enumerate(sem chan struct{}, wg *sync.WaitGroup, hc *http.Client, st *httpS
539540
o := resultOutput{
540541
Type: "result",
541542
FullMatch: fnr != "",
543+
Tentative: false,
542544
BaseUrl: br.url,
543545
File: br.file,
544546
Tilde: br.tilde,
@@ -552,9 +554,52 @@ func enumerate(sem chan struct{}, wg *sync.WaitGroup, hc *http.Client, st *httpS
552554

553555
} else if err == nil && len(br.ext) > 0 {
554556

555-
// This gets hit if the full match response is the same as the negative match (may need future work)
556-
log.WithFields(log.Fields{"status": res.StatusCode, "statusNeg": mk.statusNeg, "filename": br.file + br.tilde + br.ext + ac.suffix}).
557-
Debug("Possible hit, but status is the same as a negative match")
557+
// This gets hit if the full match response is the same as the negative match
558+
if args.RelaxMatch {
559+
// When RelaxMatch is enabled, treat this as a tentative match
560+
log.WithFields(log.Fields{"status": res.StatusCode, "statusNeg": mk.statusNeg, "filename": br.file + br.tilde + br.ext + ac.suffix}).
561+
Info("Tentative match (status matches negative)")
562+
563+
// Indicate which parts of the filename are uncertain
564+
fn, fe := br.file, br.ext
565+
if len(fn) >= 6 {
566+
fn = fn + "?"
567+
}
568+
if len(fe) >= 4 {
569+
fe = fe + "?"
570+
}
571+
572+
// Output the tentative match
573+
if args.Output == "human" {
574+
var fp string
575+
if len(br.file) < 6 {
576+
fn = color.YellowString(fn)
577+
}
578+
if len(br.ext) < 4 {
579+
fe = color.YellowString(fe)
580+
}
581+
fp = strings.Replace(fn+fe, "?", color.HiBlackString("?"), -1)
582+
printHuman(fmt.Sprintf("%-20s %-28s %s", br.file+br.tilde+br.ext, fp, color.YellowString("[tentative]")))
583+
} else {
584+
// Output JSON result if requested
585+
o := resultOutput{
586+
Type: "result",
587+
FullMatch: false,
588+
Tentative: true,
589+
BaseUrl: br.url,
590+
File: br.file,
591+
Tilde: br.tilde,
592+
Ext: br.ext,
593+
Partname: fn + fe,
594+
Fullname: "",
595+
}
596+
printJSON(o)
597+
}
598+
} else {
599+
// Original behavior: just log as debug
600+
log.WithFields(log.Fields{"status": res.StatusCode, "statusNeg": mk.statusNeg, "filename": br.file + br.tilde + br.ext + ac.suffix}).
601+
Debug("Possible hit, but status is the same as a negative match")
602+
}
558603

559604
}
560605

pkg/shortutil/shortutil.go

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,19 @@
88
package shortutil
99

1010
import (
11-
"io"
12-
"os"
11+
"bufio"
1312
"fmt"
13+
"github.com/alexflint/go-arg"
14+
"github.com/bitquark/shortscan/pkg/maths"
15+
"github.com/fatih/color"
16+
"io"
1417
"log"
1518
"math"
19+
"net/url"
20+
"os"
1621
"path"
17-
"bufio"
1822
"regexp"
1923
"strings"
20-
"net/url"
21-
"github.com/fatih/color"
22-
"github.com/alexflint/go-arg"
23-
"github.com/bitquark/shortscan/pkg/maths"
2424
)
2525

2626
type wordlistRecord struct {
@@ -82,14 +82,14 @@ func ChecksumOriginal(f string) string {
8282

8383
var ck uint16
8484
ck = (uint16(f[0])<<8 + uint16(f[1])) & 0xffff
85-
for i := 2; i < len(f); i+=2 {
86-
if ck & 1 == 1 {
85+
for i := 2; i < len(f); i += 2 {
86+
if ck&1 == 1 {
8787
ck = 0x8000 + ck>>1 + uint16(f[i])<<8
8888
} else {
8989
ck = ck>>1 + uint16(f[i])<<8
9090
}
91-
if (i+1 < len(f)) {
92-
ck += uint16(f[i+1]) & 0xffff
91+
if i+1 < len(f) {
92+
ck += uint16(f[i+1]) & 0xffff
9393
}
9494
}
9595

@@ -110,7 +110,7 @@ func Gen8dot3(file string, ext string) (bool, string, string) {
110110
er := shortReplacer.Replace(eu)
111111

112112
// Determine whether a short filename was required
113-
r := len(file) > 8 || len (ext) > 3 || fu != fr || eu != er
113+
r := len(file) > 8 || len(ext) > 3 || fu != fr || eu != er
114114

115115
// Trim and return the names
116116
return r, fr[:maths.Min(len(fr), 6)], er[:maths.Min(len(er), 3)]
@@ -139,7 +139,7 @@ func ChecksumWords(fh io.Reader, paramRegex *regexp.Regexp) []wordlistRecord {
139139
// Split the file and extension
140140
var f, e string
141141
if p := strings.LastIndex(w, "."); p > 0 && w[0] != '.' {
142-
f, e = w[:p], w[p + 1:]
142+
f, e = w[:p], w[p+1:]
143143
} else {
144144
f, e = w, ""
145145
}
@@ -181,7 +181,7 @@ func Run() {
181181
// Parse command-line arguments
182182
p := arg.MustParse(&args)
183183
if p.Subcommand() == nil {
184-
fmt.Println(color.New(color.FgBlue, color.Bold).Sprint("Shortutil v" + version), "·", color.New(color.FgWhite, color.Bold).Sprint("a short filename utility by bitquark"))
184+
fmt.Println(color.New(color.FgBlue, color.Bold).Sprint("Shortutil v"+version), "·", color.New(color.FgWhite, color.Bold).Sprint("a short filename utility by bitquark"))
185185
p.WriteHelp(os.Stderr)
186186
os.Exit(1)
187187
}

0 commit comments

Comments
 (0)