Skip to content

Commit b2f98b6

Browse files
committed
Improved crawling
1 parent 5a5aebf commit b2f98b6

5 files changed

Lines changed: 333 additions & 39 deletions

File tree

lib/core/option.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2360,6 +2360,7 @@ def _setKnowledgeBaseAttributes(flushAll=True):
23602360

23612361
if flushAll:
23622362
kb.checkSitemap = None
2363+
kb.crawledHosts = set() # hosts whose robots.txt / well-known paths were already probed
23632364
kb.headerPaths = {}
23642365
kb.keywords = set(getFileItems(paths.SQL_KEYWORDS))
23652366
kb.lastCtrlCTime = None

lib/core/settings.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from thirdparty import six
2121

2222
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
23-
VERSION = "1.10.7.187"
23+
VERSION = "1.10.7.188"
2424
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
2525
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
2626
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)
@@ -790,6 +790,21 @@
790790
# Extensions skipped by crawler
791791
CRAWL_EXCLUDE_EXTENSIONS = frozenset(("3ds", "3g2", "3gp", "7z", "DS_Store", "a", "aac", "accdb", "access", "adp", "ai", "aif", "aiff", "apk", "ar", "asf", "au", "avi", "bak", "bin", "bk", "bkp", "bmp", "btif", "bz2", "c", "cab", "caf", "cfg", "cgm", "cmx", "com", "conf", "config", "cpio", "cpp", "cr2", "cue", "dat", "db", "dbf", "deb", "debug", "djvu", "dll", "dmg", "dmp", "dng", "doc", "docx", "dot", "dotx", "dra", "dsk", "dts", "dtshd", "dvb", "dwg", "dxf", "dylib", "ear", "ecelp4800", "ecelp7470", "ecelp9600", "egg", "elf", "env", "eol", "eot", "epub", "error", "exe", "f4v", "fbs", "fh", "fla", "flac", "fli", "flv", "fpx", "fst", "fvt", "g3", "gif", "go", "gz", "h", "h261", "h263", "h264", "ico", "ief", "img", "ini", "ipa", "iso", "jar", "java", "jpeg", "jpg", "jpgv", "jpm", "js", "jxr", "ktx", "lock", "log", "lvp", "lz", "lzma", "lzo", "m3u", "m4a", "m4v", "mar", "mdb", "mdi", "mid", "mj2", "mka", "mkv", "mmr", "mng", "mov", "movie", "mp3", "mp4", "mp4a", "mpeg", "mpg", "mpga", "msi", "mxu", "nef", "npx", "nrg", "o", "oga", "ogg", "ogv", "old", "otf", "ova", "ovf", "pbm", "pcx", "pdf", "pea", "pgm", "pic", "pid", "pkg", "png", "pnm", "ppm", "pps", "ppt", "pptx", "ps", "psd", "py", "pya", "pyc", "pyo", "pyv", "qt", "rar", "ras", "raw", "rb", "rgb", "rip", "rlc", "rs", "run", "rz", "s3m", "s7z", "scm", "scpt", "service", "sgi", "shar", "sil", "smv", "so", "sock", "socket", "sqlite", "sqlitedb", "sub", "svc", "swf", "swo", "swp", "sys", "tar", "tbz2", "temp", "tga", "tgz", "tif", "tiff", "tlz", "tmp", "toast", "torrent", "ts", "ttf", "uvh", "uvi", "uvm", "uvp", "uvs", "uvu", "vbox", "vdi", "vhd", "vhdx", "viv", "vmdk", "vmx", "vob", "vxd", "war", "wav", "wax", "wbmp", "wdp", "weba", "webm", "webp", "whl", "wm", "wma", "wmv", "wmx", "woff", "woff2", "wvx", "xbm", "xif", "xls", "xlsx", "xlt", "xm", "xpi", "xpm", "xwd", "xz", "yaml", "yml", "z", "zip", "zipx"))
792792

793+
# Endpoint mining inside JavaScript bundles during crawling (SPA API routes live in .js, invisible to
794+
# href/src scraping): quoted absolute-path or same-origin-URL string literals (fetch/axios/XHR targets).
795+
# MAX_* caps per-bundle results so a noisy minified file cannot flood the target list.
796+
JAVASCRIPT_ENDPOINT_REGEX = r'''["'`](?P<result>(?:https?:)?//[\w.:@-]+/[^"'`\s]*|/[A-Za-z0-9_][^"'`\s]*)["'`]'''
797+
MAX_JAVASCRIPT_ENDPOINTS = 200
798+
# only the leading slice of a bundle/source-map is scanned for endpoints - bounds CPU/RAM on a hostile response
799+
MAX_JAVASCRIPT_MINE_SIZE = 1 * 1024 * 1024
800+
# max source distance (chars) between a string-constant assignment and a `name + "/suffix"` use it may fold into
801+
MAX_JAVASCRIPT_FOLD_DISTANCE = 2048
802+
# cap on Disallow/Allow/Sitemap lines consumed from a (potentially hostile) robots.txt
803+
MAX_ROBOTS_ENTRIES = 1000
804+
# bounds on sitemap parsing: number of sitemap documents fetched (recursion fan-out) and total URLs kept
805+
MAX_SITEMAP_FETCHES = 100
806+
MAX_SITEMAP_URLS = 100000
807+
793808
# Patterns often seen in HTTP headers containing custom injection marking character '*'
794809
# Note: the ';q=' quality-value class excludes '*' so a user-placed injection mark right after a
795810
# quality value (e.g. 'Accept: ...;q=0.9*') is not swallowed (ref: #5357 - header injection was then
@@ -983,6 +998,10 @@
983998
# GraphQL endpoint paths to probe when the user supplies a base URL with --graphql (no explicit /graphql)
984999
GRAPHQL_ENDPOINT_PATHS = ("/graphql", "/api/graphql", "/v1/graphql", "/api/v1/graphql", "/graphql/api", "/graphql/console", "/graphql.php", "/graphiql", "/graph", "/gql", "/query")
9851000

1001+
# Self-describing JSON endpoint directories probed once per host during crawling: OIDC discovery lists the
1002+
# auth/token/userinfo URLs, OpenAPI/Swagger specs enumerate the whole API (their paths are mined as endpoints)
1003+
WELL_KNOWN_ENDPOINT_PATHS = ("/.well-known/openid-configuration", "/swagger.json", "/openapi.json", "/swagger/v1/swagger.json", "/api-docs", "/v2/api-docs", "/v3/api-docs", "/api/swagger.json", "/api/openapi.json")
1004+
9861005
# Seed field/argument names used to recover a GraphQL schema from "Did you mean" suggestion error
9871006
# messages when introspection is disabled (the field-suggestion / "Clairvoyance" technique)
9881007
GRAPHQL_FIELD_WORDLIST = ("user", "users", "me", "search", "login", "node", "post", "posts",

lib/parse/sitemap.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,18 @@
1313
from lib.core.data import logger
1414
from lib.core.datatype import OrderedSet
1515
from lib.core.exception import SqlmapSyntaxException
16+
from lib.core.settings import MAX_SITEMAP_FETCHES
17+
from lib.core.settings import MAX_SITEMAP_URLS
1618
from lib.request.connect import Connect as Request
1719
from thirdparty.six.moves import http_client as _http_client
1820

1921
abortedFlag = None
2022

21-
def parseSitemap(url, retVal=None, visited=None):
23+
def parseSitemap(url, retVal=None, visited=None, urlFilter=None):
24+
"""Parse a sitemap (recursively following nested sitemap indexes). 'urlFilter' - when given - must
25+
return True for a URL to be fetched or kept; it is enforced on the initial URL AND every nested
26+
sitemap fetched recursively, so a hostile sitemap index cannot pull the crawler out of scope."""
27+
2228
global abortedFlag
2329

2430
if retVal is not None:
@@ -30,11 +36,17 @@ def parseSitemap(url, retVal=None, visited=None):
3036
retVal = OrderedSet()
3137
visited = set()
3238

33-
if url in visited:
39+
if url in visited or (urlFilter is not None and not urlFilter(url)):
3440
return retVal
3541

3642
visited.add(url)
3743

44+
if len(visited) > MAX_SITEMAP_FETCHES or len(retVal) >= MAX_SITEMAP_URLS: # bound a hostile sitemap graph
45+
if not abortedFlag: # warn once on the False->True transition
46+
logger.warning("sitemap parsing stopped after reaching the fetch/URL limit. sqlmap will use partial list")
47+
abortedFlag = True
48+
return retVal
49+
3850
try:
3951
content = Request.getPage(url=url, raise404=True)[0] if not abortedFlag else ""
4052
except _http_client.InvalidURL:
@@ -45,7 +57,7 @@ def parseSitemap(url, retVal=None, visited=None):
4557
content = re.sub(r"<!--.*?-->", "", content, flags=re.DOTALL) # Note: strip (possibly multi-line) XML comments so commented-out <loc> entries aren't harvested
4658

4759
for match in re.finditer(r"<\w*?loc[^>]*>\s*([^<]+)", content, re.I):
48-
if abortedFlag:
60+
if abortedFlag or len(retVal) >= MAX_SITEMAP_URLS:
4961
break
5062

5163
foundUrl = htmlUnescape(match.group(1).strip())
@@ -60,8 +72,8 @@ def parseSitemap(url, retVal=None, visited=None):
6072
kb.followSitemapRecursion = readInput(message, default='N', boolean=True)
6173

6274
if kb.followSitemapRecursion:
63-
parseSitemap(foundUrl, retVal, visited)
64-
else:
75+
parseSitemap(foundUrl, retVal, visited, urlFilter)
76+
elif urlFilter is None or urlFilter(foundUrl):
6577
retVal.add(foundUrl)
6678

6779
except KeyboardInterrupt:

lib/request/redirecthandler.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,13 @@ def http_error_302(self, req, fp, code, msg, headers):
122122
redurl = _urllib.parse.urljoin(req.get_full_url(), redurl)
123123

124124
self._infinite_loop_check(req)
125-
if conf.scope:
125+
crawlRedirectFilter = getattr(threadData, "crawlRedirectFilter", None)
126+
if crawlRedirectFilter is not None and not crawlRedirectFilter(redurl):
127+
# a crawler recon/source-map fetch carries the session cookie; it must NOT follow an
128+
# (attacker-controlled) redirect out of scope - reject the hop before it is made, so the
129+
# cookie never reaches the off-scope destination (works even when no explicit --scope is set)
130+
redurl = None
131+
elif conf.scope:
126132
if not re.search(conf.scope, redurl, re.I):
127133
redurl = None
128134
else:

0 commit comments

Comments
 (0)